Skip to content
Draft
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
26 changes: 26 additions & 0 deletions packages/host/babel.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ const {
babelCompatSupport,
templateCompatSupport,
} = require('@embroider/compat/babel');
const path = require('node:path');

const {
moduleProvenancePlugin,
} = require('../runtime-common/loader-plugin.ts');

const baseDir = path.resolve(__dirname, '../base');

module.exports = {
plugins: [
Expand Down Expand Up @@ -48,6 +55,25 @@ module.exports = {
...babelCompatSupport(),
],

overrides: [
{
// A base module compiled into the host bundle is evaluated without a
// loader, so it marks each class it declares with the identifier the
// loader serves the module under, as the realm's transpiler does for a
// module the loader fetches.
test: (filename) =>
Boolean(filename) &&
filename.startsWith(`${baseDir}${path.sep}`) &&
!filename.includes(`${path.sep}node_modules${path.sep}`),
plugins: [
[
moduleProvenancePlugin,
{ moduleRoot: baseDir, modulePrefix: '@cardstack/base/' },
],
],
},
],

generatorOpts: {
compact: false,
},
Expand Down
44 changes: 9 additions & 35 deletions packages/host/lib/bundled-base-scoped-css.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,8 @@
// the loader is actually asked for get served, and one reached from inside an
// already-bundled chunk never is, so a served module has to answer for the
// stylesheets of everything it pulls in as well as its own.
//
// It also marks each class a base module declares with the module's name, as
// the loader's own transpiler does for a module it fetches. A module reached
// only from inside another module's chunk is never served, so the loader is
// never asked for it; the mark is how it can name that module's classes all
// the same.
import { sep } from 'node:path';

import { transformAsync } from '@babel/core';

import { moduleProvenancePlugin } from '../../runtime-common/loader-plugin.ts';

const REGISTRY = '__boxelBundledBaseScopedCSS';

// `<fromFile>.<encoded stylesheet>.glimmer-scoped.css` wherever it appears as
Expand Down Expand Up @@ -142,7 +132,7 @@ export function bundledBaseScopedCSS() {
// bundler resolves it: `enforce: 'post'` puts this at the end of the
// transform chain.
enforce: 'post',
async transform(code, id) {
transform(code, id) {
if (!isBaseModule(id)) {
return null;
}
Expand All @@ -158,31 +148,15 @@ export function bundledBaseScopedCSS() {
),
].filter((imported) => imported !== name);

// In a chunk `import.meta.url` is the chunk's, so the mark names the
// module by the identifier the loader serves it under.
let marked = await transformAsync(code, {
filename: id,
sourceType: 'module',
babelrc: false,
configFile: false,
sourceMaps: true,
plugins: [
[
moduleProvenancePlugin,
{ moduleIdentifier: `@cardstack/base/${name}` },
],
],
});
let result = marked?.code ?? code;

if (css.length || imports.length) {
// A name that turns out to be something other than a base module
// costs nothing: the reader walks only names the registry holds.
result +=
`\n;(globalThis.${REGISTRY} ??= {})[${JSON.stringify(name)}] = ` +
`${JSON.stringify({ css, imports })};\n`;
if (!css.length && !imports.length) {
return null;
}
return { code: result, map: marked?.map ?? null };
// A name that turns out to be something other than a base module costs
// nothing: the reader walks only names the registry holds.
let registration =
`\n;(globalThis.${REGISTRY} ??= {})[${JSON.stringify(name)}] = ` +
`${JSON.stringify({ css, imports })};\n`;
return { code: code + registration, map: null };
},
};
}
4 changes: 2 additions & 2 deletions packages/host/tests/integration/tools/read-text-file-test.gts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { RenderingTestContext } from '@ember/test-helpers';
import { getService } from '@universal-ember/test-support';
import { module, skip, test } from 'qunit';

import { moduleProvenanceMarks } from '@cardstack/runtime-common/etc/test-fixtures';
import { moduleProvenanceMark } from '@cardstack/runtime-common/etc/test-fixtures';

import RealmService from '@cardstack/host/services/realm';
import ReadTextFileTool from '@cardstack/host/tools/read-text-file';
Expand Down Expand Up @@ -89,7 +89,7 @@ module('Integration | tools | read-text-file', function (hooks) {
// A .gts path is served as its transpiled module.
assert.codeEqual(
result.content,
`import Component from '@glimmer/component';\nexport default class TestComponent extends Component {}\n${moduleProvenanceMarks([['TestComponent', 'default']])}`,
`import Component from '@glimmer/component';\nexport default class TestComponent extends Component {\n${moduleProvenanceMark('default')}\n}`,
);
});

Expand Down
28 changes: 9 additions & 19 deletions packages/host/tests/unit/loader-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,11 +241,6 @@ module('Unit | loader', function (hooks) {
let { Person } = await loader.import<{ Person: unknown }>(
`${testRealmURL}person`,
);
assert.deepEqual(loader.identify(Person), {
module: `${testRealmURL}person`,
name: 'Person',
});
// The loader knows which loader instance was used to import the card
assert.deepEqual(Loader.identify(Person), {
module: `${testRealmURL}person`,
name: 'Person',
Expand Down Expand Up @@ -725,7 +720,7 @@ module('Unit | loader', function (hooks) {
});

// This Loader is constructed without a `virtualNetwork`, so
// `captureIdentitiesOfModuleExports` records the raw shim module
// `markModuleExports` records the raw shim module
// identifier without running it through `vn.unresolveURL`. The
// identity stays in URL form. Other test setups that build a VN
// alongside the Loader see the RRI canonical form here.
Expand All @@ -743,24 +738,19 @@ module('Unit | loader', function (hooks) {
let { Declared } = await loader.import<{ Declared: unknown }>(
`${testRealmURL}declarer`,
);
let declarer = loader.identify(Declared);
// On the same network, so both loaders spell the module alike.
let { virtualNetwork } = getService('network');
let throwIfFetch = new Loader(
async () => {
throw new Error(
'fetch should not be invoked during shimmed module tests',
);
},
virtualNetwork.resolveImport,
{ virtualNetwork },
);
let declarer = { module: `${testRealmURL}declarer`, name: 'Declared' };
assert.deepEqual(Loader.identify(Declared), declarer);
let throwIfFetch = new Loader(async () => {
throw new Error(
'fetch should not be invoked during shimmed module tests',
);
});
throwIfFetch.shimModule('https://example.com/re-exporter.js', {
Declared,
});

assert.deepEqual(
throwIfFetch.identify(Declared),
Loader.identify(Declared),
declarer,
'the re-exporter does not take the credit',
);
Expand Down
35 changes: 17 additions & 18 deletions packages/realm-server/tests/transpile-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import QUnit from 'qunit';
const { module, test } = QUnit;
import { basename } from 'path';
import { transpileJS } from '@cardstack/runtime-common/transpile';
import { moduleProvenanceMark } from '@cardstack/runtime-common/etc/test-fixtures';
import '@cardstack/runtime-common/helpers/code-equality-assertion';

module(basename(import.meta.filename), function () {
Expand Down Expand Up @@ -59,40 +60,37 @@ module(basename(import.meta.filename), function () {
});

module('module provenance', function () {
test('marks each class and function the module declares and exports', async function (assert) {
test('marks each class the module declares and exports', async function (assert) {
let transpiled = await transpileJS(
`
export class A {}
export function f() {}
export const g = () => 1;
export const H = class {};
class B {}
export { B, B as Bee };
export { B as Bee, B };
export default class C {}
`,
'test-module.ts',
);
assert.codeEqual(
transpiled,
`
export class A {}
export class A {
${moduleProvenanceMark('A')}
}
export function f() {}
export const g = () => 1;
class B {}
export { B, B as Bee };
export default class C {}
function _markModuleProvenance(value, name) {
let key = Symbol.for("module-provenance");
if (typeof value === 'function' && Object.isExtensible(value) && !Object.prototype.hasOwnProperty.call(value, key)) {
Object.defineProperty(value, key, {
value: Object.freeze({ module: import.meta.url, name })
});
}
export const H = class {
${moduleProvenanceMark('H')}
};
class B {
${moduleProvenanceMark('B')}
}
export { B as Bee, B };
export default class C {
${moduleProvenanceMark('default')}
}
_markModuleProvenance(A, "A");
_markModuleProvenance(f, "f");
_markModuleProvenance(g, "g");
_markModuleProvenance(B, "B");
_markModuleProvenance(C, "default");
`,
);
});
Expand All @@ -105,6 +103,7 @@ module(basename(import.meta.filename), function () {
interface Shape { a: number }
export type { Shape };
export declare const Declared: unknown;
export declare class DeclaredClass {}
export { Imported };
export default Imported;
`,
Expand Down
31 changes: 10 additions & 21 deletions packages/runtime-common/etc/test-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import StringField from '@cardstack/base/string';
import { setComponentTemplate } from "@ember/component";
import { createTemplateFactory } from "@ember/template-factory";
export class Person extends CardDef {
${moduleProvenanceMark('Person')}
static displayName = 'Person';
static {
dt7948.g(this.prototype, "firstName", [field], function () {
Expand Down Expand Up @@ -71,30 +72,18 @@ export class Person extends CardDef {
export let counter = 0;
export function increment() {
counter++;
}
${moduleProvenanceMarks([
['Person', 'Person'],
['counter', 'counter'],
['increment', 'increment'],
])}`.trim();
}`.trim();
}

// What transpilation appends to a module that declares exports: a helper,
// then one call per declared export, as [local binding, export name].
export function moduleProvenanceMarks(exports: [string, string][]) {
return `
function _markModuleProvenance(value, name) {
let key = Symbol.for("module-provenance");
if (typeof value === 'function' && Object.isExtensible(value) && !Object.prototype.hasOwnProperty.call(value, key)) {
Object.defineProperty(value, key, {
// The static block transpilation puts first in each class a module declares
// and exports, which marks the class with that module and its export name.
export function moduleProvenanceMark(name: string) {
return `static {
Object.defineProperty(this, Symbol.for("module-provenance"), {
value: Object.freeze({
module: import.meta.url,
name
module: import.meta.moduleIdentifier ?? import.meta.url,
name: "${name}"
})
});
}
}
${exports
.map(([local, name]) => `_markModuleProvenance(${local}, "${name}");`)
.join('\n')}`.trim();
}`;
}
Loading
Loading