Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-define-plugin-tdz.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"emdash": patch
---

Fixes a crash that returned HTTP 500 on every route on Cloudflare Workers when native plugins are registered with `definePlugin` (`Cannot access 'SIMPLE_ID' before initialization`). Plugin registration is now safe regardless of bundle ordering.
19 changes: 14 additions & 5 deletions packages/core/src/plugins/define-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,6 @@ import type {
PluginStorageConfig,
} from "./types.js";

// Plugin ID validation patterns
const SIMPLE_ID = /^[a-z0-9-]+$/;
const SCOPED_ID = /^@[a-z0-9-]+\/[a-z0-9-]+$/;
const SEMVER_PATTERN = /^\d+\.\d+\.\d+/;

/**
* Define a native EmDash plugin.
*
Expand Down Expand Up @@ -94,6 +89,20 @@ export function definePlugin<TStorage extends PluginStorageConfig>(
function defineNativePlugin<TStorage extends PluginStorageConfig>(
definition: PluginDefinition<TStorage>,
): ResolvedPlugin<TStorage> {
// Declared function-local (not module scope) on purpose. Under
// `ssr.noExternal` the worker entry can instantiate native plugins during a
// circular module init, reaching this function before module-scope consts
// initialize -> "Cannot access 'SIMPLE_ID' before initialization" -> every
// route 500s on Cloudflare Workers. Call-time consts evaluate after the
// literals are parsed, so the temporal dead zone cannot occur regardless of
// bundle ordering. See #1370.
// oxlint-disable-next-line e18e/prefer-static-regex -- call-time on purpose (see #1370)
const SIMPLE_ID = /^[a-z0-9-]+$/;
// oxlint-disable-next-line e18e/prefer-static-regex -- call-time on purpose (see #1370)
const SCOPED_ID = /^@[a-z0-9-]+\/[a-z0-9-]+$/;
// oxlint-disable-next-line e18e/prefer-static-regex -- call-time on purpose (see #1370)
const SEMVER_PATTERN = /^\d+\.\d+\.\d+/;

const {
id,
version,
Expand Down
18 changes: 18 additions & 0 deletions packages/core/tests/unit/plugins/define-plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,24 @@ describe("definePlugin", () => {
});
});

// Regression: #1370 — the id/version validation patterns must stay
// function-local (evaluated at call time). As module-scope consts, a
// circular module init on Cloudflare Workers could reach defineNativePlugin
// before they initialized, throwing "Cannot access 'SIMPLE_ID' before
// initialization" and 500-ing every route. Validation must keep working.
describe("#1370 — call-time validation regexes", () => {
it("validates id and version on every call", () => {
expect(definePlugin({ id: "first", version: "1.0.0" }).id).toBe("first");
expect(definePlugin({ id: "@scope/second", version: "2.3.4" }).version).toBe("2.3.4");
expect(() => definePlugin({ id: "Bad_Id", version: "1.0.0" })).toThrow(
INVALID_PLUGIN_ID_PATTERN,
);
expect(() => definePlugin({ id: "ok", version: "nope" })).toThrow(
INVALID_PLUGIN_VERSION_PATTERN,
);
});
});

describe("capability validation", () => {
it("accepts valid capabilities", () => {
const plugin = definePlugin({
Expand Down
Loading