Skip to content

metadata-core's CJS entry point is unloadable since #12843 — import.meta emitted verbatim into dist/index.cjs is a parse-time error, so the guarding try/catch never runs #12971

Description

@hotlong

@objectstack/metadata-core's CJS entry point is unloadable since #12843import.meta is emitted verbatim into dist/index.cjs, which is a PARSE-time error, so the guarding try/catch never runs.

Measured

At fc8a33935 (current main tip), from a clean pnpm turbo run build --force:

$ node -e "require('./dist/index.cjs')" # in packages/metadata-core
Warning: Failed to load the ES module: .../packages/metadata-core/dist/index.cjs.
REQUIRE FAILED: SyntaxError - Cannot use 'import.meta' outside a module

dist/index.cjs:810:

constreq=_module.createRequire.call(void0,import.meta.url);

Source: packages/metadata-core/src/artifact-forward-conversion.ts:229, in resolveInstalledSpecVersion().

Why the existing guard does not hold

The function is written to be safe in both module systems, and its comment states the intended posture:

ESM build: anchor a require at this module's own URL. In the CJS build this branch is only reachable when the branch above already failed, and its transformed import.meta.url is undefined there — createRequire then throws and the catch below answers null, the documented posture.

That assumes the bundler rewritesimport.meta.url in the CJS output. It does not — the identifier is emitted verbatim. And import.meta outside an ES module is a syntax error at parse time, not a runtime undefined: the module never begins executing, so neither the typeof require === 'function' fast path nor the try/catch around createRequire is ever reached. The whole CJS entry point of the package is unloadable, for every consumer and every code path — not just callers of resolveInstalledSpecVersion().

Bisect

packages/metadata-core/package.json declares "type": "module" with a dual exports map (require to ./dist/index.cjs), so the CJS condition is a published, supported entry point.

Downstream blast radius (measured in cloud)

Filed from cloud#1712, the consumer bump that cannot land because of this. On a cloud worktree pinned at fc8a33935, the enterprise runtime refuses to boot: @objectstack/organizations is resolved through the CJS condition, fails to load, and the ADR-0093 D5 fail-closed tenancy wall then correctly refuses to serve:

✖ FATAL: tenancy posture 'isolated' was requested but @objectstack/organizations could not be loaded,
so the organization wall is INACTIVE. Refusing to boot ...
cause: Cannot use 'import.meta' outside a module

Consequences in objectstack-ai/cloud's only required check (build-and-test):

suiteresult at fc8a33935
apps/objectos-eetest/signup-membership-policy.e2e.test.ts3 failed / 2 passed (5) — every case that boots the walled posture
apps/objectos-eetest:hotcrm-acceptance2 files failed, 12 of 13 tests skipped — collection-level, same cause
apps/cloud284/284 passed (does not boot the walled EE runtime)
packages/service-cloud1579/1579 passed (likewise)

The break is not dodgeable by picking an earlier consumer pin: 54e2d3692 is an ancestor of fc8a33935, and cloud#1712 needs a SHA at or past fc8a33935 (the os migrate host-composition fix, #12938 / #12952).

Suggested direction, not a prescription

The contract question is how resolveInstalledSpecVersion() should anchor its resolution in the CJS build at all. Two shapes worth weighing:

  1. Keep the dual-build and stop emitting import.meta into CJS — e.g. resolve the ESM anchor behind the bundler's own define/shim so the CJS output carries a real undefined, which is what the current comment already documents as the intended behaviour. Smallest change; keeps the documented posture true instead of aspirational.
  2. Drop the ESM branch and anchor only on the ambient require plus an explicit caller-supplied version, so there is one resolution path rather than two that must agree.

Worth a gate either way: nothing currently asserts that each dual-built package's require entry point actually loads. A require() smoke over the CJS conditions in the build would have caught this at the commit that introduced it, and would hold the class rather than this one line.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions