Skip to content

fix(plugins): hoist id/version regexes into defineNativePlugin to avoid TDZ on Workers (#1370)#1542

Merged
ascorbic merged 2 commits into
emdash-cms:mainfrom
marcusbellamyshaw-cell:fix/1370-define-plugin-tdz
Jul 1, 2026
Merged

fix(plugins): hoist id/version regexes into defineNativePlugin to avoid TDZ on Workers (#1370)#1542
ascorbic merged 2 commits into
emdash-cms:mainfrom
marcusbellamyshaw-cell:fix/1370-define-plugin-tdz

Conversation

@marcusbellamyshaw-cell

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes a crash that returns HTTP 500 on every route on Cloudflare Workers (workerd) when a site registers native plugins via definePlugin.

The plugin id/version validation regexes (SIMPLE_ID, SCOPED_ID, SEMVER_PATTERN) were module-scope consts read inside defineNativePlugin. Under the integration's ssr.noExternal: ["emdash", …], Rollup re-bundles emdash into the worker and flattens a circular import between the worker entry and this chunk. When a top-level definePlugin(...) (a native plugin's default export, mounted via createPlugin) runs at module-init time mid-cycle, it reaches defineNativePlugin before those consts have initialized — and const bindings throw inside their temporal dead zone:

ReferenceError: Cannot access 'SIMPLE_ID' before initialization
    at defineNativePlugin
    at definePlugin
    at createPlugin
    at <module init>

astro build / astro check pass because it's purely a bundled init-order issue; 0.15.0 was unaffected only because its bundle ordering happened to differ.

Moving the three regexes into the function body makes them evaluate at call time (after the literals are parsed), so the TDZ cannot occur regardless of how the bundler orders the circular init. Validation behavior is byte-identical — they're used only inside defineNativePlugin. The e18e/prefer-static-regex lint is suppressed on these three lines with a justification: module scope is exactly what triggers the bug, and the per-call compile cost is negligible (once per plugin registration). A leaf-module or var alternative would not survive Rollup's chunk flattening — only call-time evaluation is robust.

Closes #1370

Type of change

  • Bug fix

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes — not run cleanly locally: branching off main left the installed workspace deps (@emdash-cms/plugin-types) out of sync, producing unrelated pre-existing errors in manifest-schema.ts / types.ts. This change is type-neutral and adds no diagnostics in the touched file.
  • pnpm lint passes (oxlint, 0 errors repo-wide)
  • pnpm test passes (targeted: define-plugin.test.ts — 40 passed)
  • pnpm format has been run (oxfmt clean)
  • I have added/updated tests for my change
  • User-visible strings wrapped for translation — n/a (no admin UI strings)
  • I have added a changeset
  • New features link to an approved Discussion — n/a (bug fix)

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.8 (Claude Code)

Screenshots / test output

The literal TDZ depends on bundler output, so it isn't reproducible in a plain vitest. The existing suite already covers the validation semantics; this PR adds an issue-anchored regression test, and the manual repro below covers the runtime crash.

Manual repro (Cloudflare adapter site that registers a native plugin):

pnpm build
npx wrangler dev --config dist/server/wrangler.json
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8799/   # 500 before, 200 after

Unit test (packages/core/tests/unit/plugins/define-plugin.test.ts):

 Test Files  1 passed (1)
      Tests  40 passed (40)

…id TDZ on Workers

Module-scope regex consts (SIMPLE_ID/SCOPED_ID/SEMVER_PATTERN) could be read before initialization during the worker's circular module init under ssr.noExternal, throwing "Cannot access 'SIMPLE_ID' before initialization" and 500-ing every route on Cloudflare Workers. Moving them function-local makes them evaluate at call time, eliminating the temporal dead zone regardless of bundle ordering.

Closes emdash-cms#1370

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jun 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: eea6ccd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approach

This is the right change for the right problem. The bug (#1370) is severe — HTTP 500 on every route on Cloudflare Workers when a site registers native plugins via definePlugin — and the diagnosis is sound: under ssr.noExternal, Rollup flattens the worker-entry ↔ emdash chunk cycle, a native plugin's definePlugin(...) default export runs at module-init mid-cycle, and the module-scope const regexes are read inside their temporal dead zone. Moving the three regexes into defineNativePlugin makes them evaluate at call time (after the literals parse), which closes the TDZ window regardless of bundle ordering. That's a better fix than var (hoisted declaration, but the assignment still wouldn't have run — you'd trade the ReferenceError for a TypeError on undefined.test) or a leaf module (Rollup can flatten it back into the same chunk). Minimal, targeted, well-commented, and the lint suppressions are justified. I'd merge this.

What I checked

  • Fix is complete for this module. After the move there are no top-level const/let bindings left in define-plugin.ts (verified by grep) — only imports and hoisted function declarations. defineNativePlugin's only other call-time dependency is normalizeCapabilities, which is re-exported from the external @emdash-cms/plugin-types package, i.e. outside the worker↔emdash cycle, so it's fully initialized before the function runs. No remaining module-scope binding in the cycle is read at call time.
  • Validation semantics unchanged. The three regex literals are byte-identical to the base (/^[a-z0-9-]+$/, /^@[a-z0-9-]+\/[a-z0-9-]+$/, /^\d+\.\d+\.\d+/), and they're still used only inside defineNativePlugin (not exported, no other consumers).
  • Lint suppressions are genuinely needed. e18e/prefer-static-regex is error globally in .oxlintrc.json and is only disabled for test files — define-plugin.ts is not in that override. The oxlint-disable-next-line directives are correctly placed directly above each regex literal. Without them this PR would fail lint.
  • No perf concern. definePlugin is only called at registration time (PluginManager.register, plus the two internal plugins in emdash-runtime.ts), not per-request, so re-creating three regexes per call is negligible (and V8 caches regex literals per function site anyway).
  • Changeset/bump correct. Targets emdash (the package that exports definePlugin) at patch — appropriate for an internal-only fix with no API change.
  • Test. The added #1370 test is a behavioral guard confirming id/version validation still works after the move — not a TDZ repro. The PR description is upfront about this, and a faithful repro isn't feasible in plain vitest without modifying production code to construct the cycle (the actual cycle is worker-entry ↔ emdash chunk, transitively through emdash-runtime; define-plugin.ts itself has no import a test could leverage to build the cycle). Given that infeasibility and the documented manual repro, I don't treat the missing reproducing test as blocking. The describe-block comment is genuinely useful as a "do not re-hoist these to module scope" guardrail.

Conclusion

Solid fix — correct, complete for the affected code path, minimal, and well-documented. One optional changeset-polish nit below; nothing blocking.

Comment thread .changeset/fix-define-plugin-tdz.md Outdated
@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Jun 19, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jun 24, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@1542

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@1542

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@1542

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@1542

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@1542

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@1542

emdash

npm i https://pkg.pr.new/emdash@1542

create-emdash

npm i https://pkg.pr.new/create-emdash@1542

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@1542

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@1542

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@1542

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@1542

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@1542

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@1542

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@1542

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@1542

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@1542

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@1542

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@1542

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@1542

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@1542

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@1542

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@1542

commit: eea6ccd

Co-authored-by: emdashbot[bot] <273199577+emdashbot[bot]@users.noreply.github.com>
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Jul 1, 2026

@ascorbic ascorbic left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@ascorbic ascorbic enabled auto-merge (squash) July 1, 2026 14:16
@ascorbic ascorbic merged commit 8f7604d into emdash-cms:main Jul 1, 2026
45 checks passed
@emdashbot emdashbot Bot mentioned this pull request Jul 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core cla: signed review/needs-rereview Author pushed changes since the last review size/S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

definePlugin TDZ: "Cannot access 'SIMPLE_ID' before initialization" — every route 500s on Cloudflare Workers (0.17.1 & 0.17.2)

2 participants