Skip to content

feat(blocks): NODE_ENV dev default, deco schema --check, multivariate stops at the first match - #693

Draft
tlgimenes wants to merge 10 commits into
mainfrom
feat/draft-hosts-and-headers
Draft

tlgimenes wants to merge 10 commits into
mainfrom
feat/draft-hosts-and-headers

Conversation

@tlgimenes

@tlgimenes tlgimenes commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

Three product-owner decisions (2026-10-14/15) in @decocms/blocks. The rest of the stack (#694 and up) merges this branch.

1. createCMS reads NODE_ENV again to decide dev mode (d03448c)

2. deco schema --check (fa83481)

  • For CI: generates the schema in memory, writes nothing, and exits 1 when .deco/schema.gen.json is missing or differs from what deco schema would write. The message says what moved:

    • blocks that are new in code or gone,
    • the number of changed definitions,
    • the generating version,
    • or formatting only.

    It ends with: Run deco schema and commit .deco/schema.gen.json.

  • Schema errors exit 1 with the errors. --check and --watch can't be combined, and there is no dev watch.

  • Usage line: deco schema [--root <dir>] [--watch | --check]. Seven tests in cli/run.test.ts, including one through the bin under plain Node. dd-06 follows the new usage line.

  • The migrator adds it to each site's CI workflow in feat: the v7-to-v8 migrator and skill use @decocms/tanstack and @decocms/nextjs #700.

3. multivariate stops at the first matching rule, as v7 did (ac23a70)

  • Before this change, the resolver resolved every variant's rule before the built-in multivariate ran. A matcher after the winner still ran, so a random rule there drew a number and recorded a cohort.
  • Now the built-in multivariate (marked the way lazy is) gets its rules resolved in order, one at a time, up to the first true one. Later variants are left out and their matchers are never called.
  • A multivariate that the site declares in its block map replaces the built-in and still gets every rule resolved, like any other block.
  • Legacy multivariate types (website/flags/multivariate.ts, …) go through the same path.
  • builtin-03/mv-02 now claims first-match order. That test changed because the docs claim changes. New tests cover in-order evaluation, a random rule after the winner never drawing, legacy types, and the block-map override. Mutation-checked: 4 fail without the change.
  • CLAUDE.md now says that lazy and the built-in multivariate are the resolver's special cases.

Docs site follow-up (deco-sites/docs-tanstack, /next/*)

  • api-reference / releases-and-drafts: dev defaults to NODE_ENV === "development" (guarded; false without process). Pass dev to override it, e.g. dev: import.meta.env.DEV on a Worker. Remove "the SDK reads no environment variable" wherever it refers to dev.
  • cli / checking: deco schema --check (usage line, exit codes, the message) and a CI snippet that runs deco schema --check.
  • matchers-and-variants line 75: replace "Rules aren't lazy: they're cheap booleans and always run." with "Rules run in order and stop at the first true one, as in v7: a later rule's matcher never runs, so a random rule after the winner draws nothing and sets no cookie. A multivariate you declare in your block map gets every rule."
  • how-resolution-works: lazy is no longer the only special case. Add the built-in multivariate's in-order rules.

Gates

  • bun run check passes.
  • bun run build passes.
  • skills:check and skills:readme:check pass.
  • A plain-Node import of every built export works (22), and so does deco --help.
  • bun run test (2 GB): 2092 pass, 3 fail, 95 skipped.

Draft shape and block JSON: unchanged (2026-10-16)

Two who-edited-what experiments came and went on this branch, and the net change is none:

  • A top-level edits field on the draft (00125500), reverted in a886d720.
  • A reserved __meta key on saved blocks that the resolver dropped (b1cd9a13, 36fb75f1), reverted in the commit on top. Both reverts are merged up the stack.

PO: "only have metadata and changed by on v8 hosted". Studio keeps who and when in the hosted draft object's storage metadata, so the draft stays exactly { set, delete } and blocks has no reserved metadata key. The tree at M2 equals its state before these commits.

https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu

🤖 Generated with Claude Code

…io's origins

The framework bindings (@decocms/tanstack, @decocms/nextjs) need the same
draft names and Studio origin list; sites repeat them by hand today.

- DRAFT_PARAM ("__draft"), DRAFT_COOKIE ("__deco_draft"), DRAFT_HEADER ("x-deco-draft")
- draftResponseHeaders(pointer): no-store/private, Vary: Cookie, noindex for a draft; {} otherwise
- STUDIO_ORIGINS, STUDIO_FRAME_ANCESTORS, isStudioOrigin()

No change to how createCMS decides which hosts may preview: cms.draftPointer
stays the only reader of the parameter and cookie. The v7 host allowlist goes
into the bindings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…io's origins out of @decocms/blocks

Product owner review: these belong to the framework bindings, not to the
framework layer. @decocms/tanstack and @decocms/nextjs carry them; blocks
keeps what main already had (parseDraftPointer, cms.draftPointer,
cms.draftCookie). This reverts 7ff9fb6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
@tlgimenes tlgimenes changed the title feat: export the draft names, draftResponseHeaders() and Studio's origins from @decocms/blocks chore(blocks): (empty) draft names and Studio origins moved to the bindings Oct 14, 2026
tlgimenes added a commit that referenced this pull request Oct 14, 2026
…ponse headers and Studio's origins

Moved from @decocms/blocks, unchanged values (PO review of #693): a new
browser-safe root export with DRAFT_PARAM, DRAFT_COOKIE, DRAFT_HEADER,
draftResponseHeaders (src/draft.ts) and STUDIO_ORIGINS,
STUDIO_FRAME_ANCESTORS, isStudioOrigin (src/studio.ts). The worker,
/server and the frame-ancestors use them; DecoDocument passes
STUDIO_ORIGINS to StudioBridge's now required trustedOrigins, the list
the bridge answered before. Tests moved with them, plus one that the
bridge gets Studio's origins and one that the package depends on no
@decocms/apps-* package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
tlgimenes added a commit that referenced this pull request Oct 14, 2026
…draft names and Studio's origins

Brings in the PO review: @decocms/blocks no longer exports the draft names,
draftResponseHeaders or Studio's origins (#693), @decocms/blocks/image is
platform-neutral with the VTEX and Shopify helpers in @decocms/apps-vtex/image
and @decocms/apps-shopify/image (#694), StudioBridge takes its trusted origins
from the binding (#695), and @decocms/tanstack owns its own copy (#696).
CLAUDE.md conflict: tests/ (helper guardrails) and examples/ lines combined.

@decocms/nextjs, in the same commit so every package type-checks:
- src/draft.ts (DRAFT_PARAM/COOKIE/HEADER, draftResponseHeaders) and
  src/studio.ts (STUDIO_ORIGINS, STUDIO_FRAME_ANCESTORS, isStudioOrigin): the
  package's own copy, same values as @decocms/tanstack's, never imported from
  it. The proxy, drafts.ts, /server and config.test.ts use it; the root
  exports it.
- StudioBridge (src/StudioBridge.ts): blocks' bridge with trustedOrigins set
  to STUDIO_ORIGINS, so the layout still passes only the site.
- /image-loader stays platform-neutral; its next/image golden comparison
  moves to tests/nextjs-image-loader.golden.test.ts, which composes
  @decocms/apps-vtex/image with decoCdn as a Next storefront does: 1,212
  cases from blocks' and apps-vtex's fixtures, none changed. exports.test.ts
  pins that the package depends on no @decocms/tanstack or @decocms/apps-*.
- nextjs-binding-smoke: decoCdn({ fit: "contain" }) (blocks' default fit is
  now cover) and a check of the origins the bridge answers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
tlgimenes added a commit that referenced this pull request Oct 14, 2026
Brings in the PO review down the stack: blocks without the draft names or
Studio's origins (#693), the platform-neutral @decocms/blocks/image with the
@decocms/apps-vtex/image and @decocms/apps-shopify/image helper subpaths
(#694), StudioBridge's required trustedOrigins (#695), and each binding's own
copy of the draft names and Studio's origins (#696, #697).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
tlgimenes and others added 3 commits October 15, 2026 10:44
…ys an override

The SDK's one environment read: without `dev`, createCMS keeps local files
(no hosted release swaps in) when process.env.NODE_ENV is "development".
Read guarded (typeof process, process.env), so a Worker or a browser without
`process` counts as production, and spelled process.env.NODE_ENV so a
bundler's define replaces it. An explicit `dev` (shipped in 8.0-8.2)
overrides it both ways. remoteLoader itself still reads no environment.

Conformance: H-5 default, override, production/unset and no-process cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu
The resolver resolved every variant's rule before the built-in multivariate
ran, so a matcher after the winner still ran: a random rule there drew and
recorded a cohort. The built-in multivariate (marked like lazy) now gets its
variants' rules resolved in order, one at a time, up to the first true one;
later variants are left out and their matchers never run. A multivariate in
the site's block map replaces the built-in and still gets every rule.

builtin-03/mv-02 now claims the first-match order (the docs claim changes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu
…t of date

For CI: generates the schema in memory, writes nothing, and exits 1 when the
committed file is missing or differs from what `deco schema` would write,
listing what moved (blocks new in code or gone, changed definitions, the
generating version, or formatting only) and how to fix it. Schema errors
also exit 1. --check can't be combined with --watch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu
@tlgimenes tlgimenes changed the title chore(blocks): (empty) draft names and Studio origins moved to the bindings feat(blocks): NODE_ENV dev default, deco schema --check, multivariate stops at the first match Oct 15, 2026
tlgimenes added a commit that referenced this pull request Oct 15, 2026
Brings in #693's PO decisions 1-3 and afe430c (stripAssetHosts defaults to "leading").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu
tlgimenes and others added 4 commits October 16, 2026 18:35
The draft changes body is now `{ set, delete, edits? }`. `edits` is the site
editor's record of who changed each name and when (`{ <name>: { userId, at }
}`, written by Studio); parseDraftChanges requires it to be an object when
present and returns only `set` and `delete`, so nothing of it reaches the
snapshot, layerDraft or a rendered page (PO: no v8 site is deployed yet, so
the shape can change).

Conformance: DP-11 now claims `{ set, delete, edits? }` (a non-object
`edits` is refused); DP-11b proves a draft's edits resolve to nothing: the
draft's blocks are as without them, no block is named `edits`, and no
resolved value or list carries a user id. The docs site's draft-previews
section needs the same wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu
… drops it"

This reverts commit 0012550. The PO moved the who/when record onto each
block (a reserved `__meta` key), so the draft stays exactly { set, delete }.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu
PO: "just add, on the block a metadata … something that tells last edited
by. but on the same json". A saved block may carry one reserved key next to
`__resolveType`: `__meta: { lastEditedBy: <Studio user id>, lastEditedAt:
<ISO 8601> }`, which the site editor stamps on every save (drafts, Localhost,
releases alike).

The resolver drops it wherever it walks (META_KEY): a saved entry is read
without it, and no object's `__meta` is copied into a resolved value, so no
block function, loader, list, `{ run: false }` read or rendered page sees
it, whether the block comes from a release, a bundled snapshot or a draft
layered on one. Snapshots and drafts keep the stored JSON as is; the
resolver is the one place content leaves. `deco check` already accepts it
(keys starting with `_` are never fields); the content protocol stores and
lists it as written; `deco schema` reads no content.

Tests: resolve.test (a stamped snapshot resolves, expands and lists exactly
as the plain one; no block function's props carry it, through references,
overrides and nesting), delivery DP-2b (a release's and a draft's stamped
block: nothing resolved carries it), deco check (accepted on any saved block
and a referenced one), protocol apply (passed through).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu
This reverts commits b1cd9a1 and 36fb75f. PO: "only have metadata and
changed by on v8 hosted": who edited a draft lives in the hosted draft
object's storage metadata (Studio), never in block JSON, so the resolver
has no reserved `__meta` key to drop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZsmzpZ2zahRBoATBb2tBu
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant