Skip to content

Mark exported classes in a static block and identify by the mark alone - #6617

Draft
backspace wants to merge 1 commit into
cs-13665-track-module-provenance-in-transpilejs-instead-of-a-boxelfrom
cs-13665-provenance-review-followups
Draft

backspace wants to merge 1 commit into
cs-13665-track-module-provenance-in-transpilejs-instead-of-a-boxelfrom
cs-13665-provenance-review-followups

Conversation

@backspace

Copy link
Copy Markdown
Contributor

Stacked on #6614. This PR applies the review feedback on #6614, so the two can be compared before one is chosen.

Background and Goal

#6614 marks each exported class or function with the module that declares it. A helper function is appended to every module, and the loader still keeps its own identity map. Review feedback asked for less: mark only exported classes, make Loader.identify read only the mark, use Babel's visitor pattern, and use the host's existing Babel config instead of the bundled-base Vite plugin. This PR makes those changes.

Where to start

  • moduleProvenancePlugin in runtime-common/loader-plugin.ts. It visits ExportNamedDeclaration and ExportDefaultDeclaration, and puts a static block first in each exported class that the module declares.
  • Loader in runtime-common/loader.ts. identify is now moduleProvenanceOf(value). markModuleExports replaces captureIdentitiesOfModuleExports.
  • host/babel.config.cjs. An overrides entry applies the plugin to packages/base files, with a literal @cardstack/base/<name> identifier.

Feedback, and what this PR does

Feedback Change
identify should only use the provenance mark Done. Loader.identify returns moduleProvenanceOf(value). The instance identify method and the per-loader identity map are removed.
Use the existing Babel config, not the Vite plugin Done. bundled-base-scoped-css.mjs handles only scoped CSS again.
Use ExportNamedDeclaration in the visitor Done. There is no Program exit pass.
Marks too much: only exported classes matter Done. Functions, constants and other values get no mark. All identify callers pass card or field classes.
The inlined helper adds too much code; define a global Not adopted. Each class gets one static block (one Object.defineProperty call). There is no helper, so a module with no exported class gets nothing. A global would make every marked module depend on code that must evaluate before it, which is the module-ordering risk seen in CS-13655.
captureIdentitiesOfModuleExports should not be needed Reduced, not removed. See below.

Key decisions and non-obvious mechanics

  • The mark has to be in canonical form. A code ref stores the virtual-network form of a module identifier (for example @cardstack/base/card-api), not the URL. identify no longer has a loader to convert a URL, so the loader adds import.meta.moduleIdentifier (already in canonical form) to each module it evaluates, and the mark reads it. A module evaluated without a loader falls back to import.meta.url.

  • Why the loader still marks exports. markModuleExports does two jobs:

    • It records Loader.loaders for Loader.getLoaderFor, which card-api, operations and patch-fields still use.
    • It marks exports that have no mark yet: classes in a module the loader gets as a shim (many host tests shim card classes under realm URLs), and classes in a module transpiled before marks existed. The L2 transpile cache has no transpiler version in its key, so unmarked output can be served for some time after a deploy.

    A value that already has a mark keeps it. So the mark from transpilation always wins, and identify has one source.

  • The mark is in a static block, first in the class body. So the mark is set when the class is created, before any other static code runs. That includes cases where a circular import reads the class before its module finishes evaluating. A subclass doesn't run its parent's static block, and moduleProvenanceOf reads only an own property, so a subclass never takes its parent's mark.

  • One behaviour change for shims. Before, each loader kept its own identity map. Now a shimmed class gets one mark for all loaders, and the first shim wins. Production shims (base, externals, host tools) always use the same identifier, so only a test that shims one class object under two different URLs would see a difference.

🤖 Generated with Claude Code

https://claude.ai/code/session_015idm3FXE4bujPmH1obGWFG

The provenance plugin now visits export declarations and gives each class
a module declares and exports a static block that marks it, with no
shared helper and no marks on functions or other values. The host build
applies it to base modules through babel.config.cjs instead of the
bundled-base Vite plugin.

Loader.identify reads the mark and nothing else. The loader puts the
module's canonical identifier on import.meta for the mark to use, and
marks what transpilation did not (shim exports, and modules transpiled
before marks existed) instead of keeping its own identity map.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015idm3FXE4bujPmH1obGWFG
@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

    1 files      1 suites   1h 9m 30s ⏱️
5 209 tests 5 198 ✅ 11 💤 0 ❌
5 224 runs  5 213 ✅ 11 💤 0 ❌

Results for commit fb02357.

Realm Server Test Results

    1 files    319 suites   2h 2m 1s ⏱️
5 104 tests 5 104 ✅ 0 💤 0 ❌
5 163 runs  5 163 ✅ 0 💤 0 ❌

Results for commit fb02357.

This branch has not been deployed

No deployments
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