Skip to content
Merged
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
157 changes: 152 additions & 5 deletions scripts/check-adr-0087-registration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,8 @@ const SELF_TEST_BATTERIES = Object.freeze({
'REX-L: the compiler is loaded only when a marker names the category': 6,
'WS (#26220): ui workspace packages, `.tsx` modules, aliases and shims': 11,
'IX (#26220): predicate 4 reads an `any` index signature; the entry may be `src/index.tsx`': 52,
'WP (#26220): a deleted published package is retired whole, `<package>#*`': 55,
'WP (#26220): a deleted published package is retired whole, `<package>#*`': 59,
'DX (#26324): an export named `default` is reached through an import, never through the word': 19,
'G6: a changeset that was ALREADY breaking at base is inherited': 1,
'R15: a changeset RENAMED AND turned breaking in the same commit': 5,
'G10: a PURE rename of an ALREADY-breaking stock changeset': 3,
Expand Down Expand Up @@ -496,7 +497,7 @@ const SELF_TEST_BATTERIES = Object.freeze({

// DELETING an entry silences that battery's floor exactly as effectively as
// zeroing it, so the roster's own size is pinned too.
const SELF_TEST_BATTERY_FLOOR = 58;
const SELF_TEST_BATTERY_FLOOR = 59;

// The key an assertion is filed under when no battery is open. It is not a
// declared battery, so it reds by the same set difference rather than silently
Expand Down Expand Up @@ -3161,9 +3162,13 @@ export function verifyRuntimeInterfaceOnly(refs, { rev, cwd, packages }) {
* @param {Map<string, string>} surfaceTexts path -> text of every metadata surface
* @param {() => Map<string, {private: boolean, file: string}>} packages workspace
* manifests, read only when an import has to be attributed to a package
* An export named `default` is read through the import arm only: see
* `defaultImportReference`.
*
* @returns {{ kind: 'import', hitPath: string } | { kind: 'mention', hitPath: string, line: string } | null}
*/
export function metadataSurfaceReference(symbol, declPath, surfaceTexts, packages) {
if (symbol === 'default') return defaultImportReference(declPath, surfaceTexts, packages);
const word = new RegExp(`\\b${symbol}\\b`);
for (const [hitPath, rawText] of surfaceTexts) {
if (hitPath === declPath || !word.test(rawText)) continue;
Expand Down Expand Up @@ -3199,6 +3204,59 @@ export function metadataSurfaceReference(symbol, declPath, surfaceTexts, package
return null;
}

/**
* The two import shapes that bind a module's DEFAULT export, each anchored at a
* statement start and each linear in the text. ⛔ Not `importSpecifiersFor`'s
* `[\s\S]*?` clause scan: that one runs only on files that already name the symbol,
* and over every metadata surface it is super-linear in the whitespace runs
* `maskComments` leaves (measured: 41 s on one `*.zod.ts`).
*
* import x from 'm' a default binding, named bindings or a namespace
* import x, { y } from 'm' beside it or not, `type` or not
* import { default as x } from 'm' `default` named inside the braces, an
* export { default } from 'm' `export ... from` re-export included
*
* and the three that bind the whole MODULE, read as reaching its default too:
*
* import * as ns from 'm' a namespace import or re-export, `ns.default`
* export * as ns from 'm' being one member read away
* import('m') require('m') a dynamic load, whose `.default` likewise
*/
const DEFAULT_BINDING_IMPORT_RE =
/(?:^|;)[ \t]*import\s+(?:type\s+)?[A-Za-z_$][\w$]*\s*(?:,\s*(?:\{[^}]*\}|\*\s*as\s+[A-Za-z_$][\w$]*)\s*)?from\s*(['"])([^'"\n]+)\1/gm;
const BRACED_IMPORT_RE = /(?:^|;)[ \t]*(?:import|export)\s+(?:type\s+)?\{([^}]*)\}\s*from\s*(['"])([^'"\n]+)\2/gm;
const NAMESPACE_IMPORT_RE = /(?:^|;)[ \t]*(?:import|export)\s+(?:type\s+)?\*\s*as\s+[A-Za-z_$][\w$]*\s*from\s*(['"])([^'"\n]+)\1/gm;
const DYNAMIC_IMPORT_RE = /\b(?:import|require)\s*\(\s*(['"])([^'"\n]+)\1\s*\)/g;

/**
* `metadataSurfaceReference` for an export named `default` (#26324): the IMPORT arm
* only. `default` is an ordinary object key and member name in exactly the files
* this step reads -- a field option `{ value: 'technology', default: true }`, a Zod
* `.default(...)` -- so the bare-word arm hit on every tree, and no retirement of a
* default export could ever certify. A metadata surface reaches a default export
* through an import that binds it (the two shapes above), or through one that binds
* the whole module (the three after them). Those three are refused on the module
* alone, never on a `.default` read after them: that member spelling is the one
* that cannot be told from the object keys above, and refusing on the binding is
* the conservative direction -- every other export name refuses them too, through
* its bare-word arm. The specifier is attributed as for any other name
* (`specifierNamesModule`), and comment spans are blanked first, so a docblock
* quoting the import is not one.
*/
function defaultImportReference(declPath, surfaceTexts, packages) {
for (const [hitPath, rawText] of surfaceTexts) {
if (hitPath === declPath || !(rawText.includes('import') || rawText.includes('export') || rawText.includes('require'))) continue;
const code = maskComments(rawText);
const specifiers = [...code.matchAll(DEFAULT_BINDING_IMPORT_RE)].map((m) => m[2]);
for (const m of code.matchAll(BRACED_IMPORT_RE)) if (/\bdefault\b/.test(m[1])) specifiers.push(m[3]);
for (const re of [NAMESPACE_IMPORT_RE, DYNAMIC_IMPORT_RE]) for (const m of code.matchAll(re)) specifiers.push(m[2]);
if (specifiers.length === 0) continue;
const pkgs = packages();
if (specifiers.some((s) => specifierNamesModule(s, hitPath, declPath, pkgs))) return { kind: 'import', hitPath };
}
return null;
}

// ---------------------------------------------------------------------------
// The sixth category: `type-surface-only` (#13080)
//
Expand Down Expand Up @@ -4802,8 +4860,12 @@ function verifyWholePackageRetired(pkg, { base, head, cwd, packagesAt, kit }) {
* any occurrence counts -- a plugin id or package reference held as a VALUE as much
* as an import. An import specifier (`from '…'`, a bare `import '…'`, `import('…')`,
* `require('…')`, by the root or a subpath) is the case reported as `import`; every
* other occurrence is a `mention`. A name that merely starts with `pkg`
* (`plugin-xy` for `plugin-x`) is not it.
* other occurrence is a `mention`, one inside a path or URL included
* (`…/node_modules/@scope/pkg/dist/index.js`, `https://npm.im/@scope/pkg`, #27022).
* A name that merely starts or ends with `pkg` (`plugin-xy` for `plugin-x`) is not
* it, and neither is a scoped package whose last segment is an unscoped `pkg`
* (`@scope/plugin-x` for `plugin-x`): a `/` may precede the name, but not one that
* closes an `@scope` segment.
*
* @param {string} pkg
* @param {string} pkgDir the package's directory, no trailing slash
Expand All @@ -4813,7 +4875,7 @@ function verifyWholePackageRetired(pkg, { base, head, cwd, packagesAt, kit }) {
export function packageReference(pkg, pkgDir, surfaceTexts) {
const name = pkg.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
const spec = new RegExp(`(?:\\bfrom\\s*|\\bimport\\s*\\(\\s*|\\brequire\\s*\\(\\s*|^\\s*import\\s+)(['"])${name}(?:/[^'"\\n]*)?\\1`, 'm');
const mention = new RegExp(`(?<![\\w@./-])${name}(?![\\w.-])`);
const mention = new RegExp(`(?<![\\w@.-])(?<!@[\\w.~-]+/)${name}(?![\\w.-])`);
// `maskComments` blanks in place, so an offset in `code` is the same offset in the file.
const lineAt = (raw, code, at) => {
const end = code.indexOf('\n', at);
Expand Down Expand Up @@ -8262,6 +8324,91 @@ function selfTest() {
]));
assert(u5?.kind === 'mention' && u5.hitPath === 'packages/spec/src/d.zod.ts' && u5.line === `export const D = { plugin: '${WP_PKG}' };`,
`WP-U5: a string VALUE naming the package is a reference in code, reported with its line; a comment is not (${JSON.stringify(u5)})`);
// #27022: an occurrence inside a path or URL is "any other occurrence" (D9); a
// `/` may precede the name, but not one closing an `@scope` segment.
const mentionIn = (pkg, line) => packageReference(pkg, 'ui/packages/plugin-x', new Map([['packages/spec/src/e.zod.ts', `${line}\n`]]));
const u6Line = `export const E = { entry: '/repo/node_modules/${WP_PKG}/dist/index.js' };`;
const u6 = mentionIn(WP_PKG, u6Line);
assert(u6?.kind === 'mention' && u6.line === u6Line,
`WP-U6: the package name inside a PATH string is a mention, reported with its line (${JSON.stringify(u6)})`);
const u7Line = `export const E = { docs: 'https://npm.im/${WP_PKG}' };`;
const u7 = mentionIn(WP_PKG, u7Line);
assert(u7?.kind === 'mention' && u7.line === u7Line,
`WP-U7: the package name inside a URL string is a mention, reported with its line (${JSON.stringify(u7)})`);
assert(['@scope/plugin-x', '/repo/node_modules/@scope/plugin-x/dist/index.js', 'https://npm.im/@scope/plugin-x']
.every((v) => mentionIn('plugin-x', `export const E = { v: '${v}' };`) === null),
'WP-U8: `@scope/plugin-x` is another package than an unscoped `plugin-x`, bare, in a path or in a URL');
assert(['/repo/node_modules/plugin-x/dist/index.js', 'https://npm.im/plugin-x']
.every((v) => mentionIn('plugin-x', `export const E = { v: '${v}' };`)?.kind === 'mention'),
'WP-U9: an unscoped `plugin-x` in a path or a URL is still a mention (the `/` refused is only a scope\'s)');
}

// ---- DX (#26324): an export named `default` ------------------------------
//
// `default` is an ordinary key in exactly the files predicate 2 reads -- a field
// option `{ value: 'technology', default: true }`, a Zod `.default(...)` -- so the
// bare-word arm refused every `default` retirement on every tree (measured on
// #26206's `@objectstack/driver-memory#default`). The import arm is the reading:
// a metadata surface reaches a default export only through an import binding it.
battery('DX (#26324): an export named `default` is reached through an import, never through the word');
{
const DX_KEYS = {
'packages/platform-objects/src/crm/account.object.ts':
"export const Account = { fields: { industry: { options: [{ label: 'Technology', value: 'technology', default: true }] } } };\n",
'packages/spec/src/data/field.zod.ts': "import { z } from 'zod';\nexport const FieldSchema = z.object({ label: z.string().default('') });\n",
};
const DX_HOST = 'packages/platform-objects/src/ui/host.object.ts';
const dx = (surfaces = {}) => run(mk(REX_FIXTURE(['default'],
'- FROM `default` TO `EmailServicePlugin`: register `new EmailServicePlugin()` with the kernel.', {
baseFiles: { [rexPath('src/index.ts')]: `${REX_INDEX_BASE}export default { id: 'com.objectstack.plugin.email' };\n`, ...DX_KEYS, ...surfaces },
})));
const DX_P2 = /`runtime-export-retired @objectstack\/plugin-email#default` \[predicate 2: no-metadata-surface\] is false: packages\/platform-objects\/src\/ui\/host\.object\.ts \(an object definition\) IMPORTS `default`/;

const dxG1 = dx();
green('DX-G1 THE #26206 SHAPE: a retired default export, beside unrelated `default:` keys and `.default(...)` calls in metadata surfaces', dxG1);
assert(/default \(export, packages\/plugins\/plugin-email\/src\/index\.ts\)/.test(dxG1.judged[0]?.detail ?? ''),
`DX-G1b: the verdict names the \`default\` operand and the entry that declared it. Got: ${dxG1.judged[0]?.detail}`);
green('DX-G2 a metadata surface that quotes the default import only in a COMMENT', dx({
[DX_HOST]: `// import emailPlugin from '${REX_PKG}';\nexport const Host = { name: 'host', default: true };\n`,
}));
green('DX-G3 a metadata surface that imports ANOTHER module\'s default', dx({
[DX_HOST]: "import other from '@objectstack/spec';\nexport const Host = { name: 'host', plugin: other };\n",
}));
red('DX-R1 a metadata surface that IMPORTS the package\'s default', dx({
[DX_HOST]: `import emailPlugin from '${REX_PKG}';\nexport const Host = { name: 'host', plugin: emailPlugin };\n`,
}), [DX_P2]);
red('DX-R2 ...and the same default named inside the braces, `{ default as x }`', dx({
[DX_HOST]: `import { default as emailPlugin } from '${REX_PKG}';\nexport const Host = { name: 'host', plugin: emailPlugin };\n`,
}), [DX_P2]);
// A binding of the whole MODULE reaches its default one member read away; every
// other export name refuses these through its bare-word arm, so `default` must too.
red('DX-R3 a NAMESPACE import of the package (`ns.default` one read away)', dx({
[DX_HOST]: `import * as email from '${REX_PKG}';\nexport const Host = { name: 'host', plugin: email.default };\n`,
}), [DX_P2]);
red('DX-R4 a DYNAMIC `import()` of the package', dx({
[DX_HOST]: `export const Host = { name: 'host', load: () => import('${REX_PKG}').then((m) => m.default) };\n`,
}), [DX_P2]);
red('DX-R5 a `require()` of the package', dx({
[DX_HOST]: `export const Host = { name: 'host', plugin: require('${REX_PKG}').default };\n`,
}), [DX_P2]);
red('DX-R6 a NAMESPACE re-export, `export * as ns from`', dx({
[DX_HOST]: `export * as email from '${REX_PKG}';\nexport const Host = { name: 'host' };\n`,
}), [DX_P2]);

const DX_DECL = rexPath('src/index.ts');
const dxPkgs = () => new Map([[REX_PKG, { private: false, file: rexPath('package.json') }]]);
const dxRef = (text) => metadataSurfaceReference('default', DX_DECL, new Map([['packages/spec/src/a.zod.ts', text]]), dxPkgs);
assert(dxRef(`import email, { LogTransport } from '${REX_PKG}';\n`)?.kind === 'import' &&
dxRef(`import type email from '${REX_PKG}/sub';\n`)?.kind === 'import' &&
dxRef(`import { z } from 'zod'; export {\n default,\n} from '${REX_PKG}';\n`)?.kind === 'import',
'DX-U1: a default binding beside named bindings, a type-only default import of a subpath, and a ' +
'multi-line `export { default } from` after a `;` on the same line, are imports');
assert(dxRef(`import { LogTransport } from '${REX_PKG}';\nexport const A = { default: LogTransport };\n`) === null &&
dxRef(`import type { T } from '${REX_PKG}';\nexport const A = z.string().default('');\n`) === null,
'DX-U2: named bindings alone, a `default:` key and a `.default(...)` call never reach the default export');
assert(metadataSurfaceReference('LogTransport', rexPath('src/log-transport.ts'),
new Map([['packages/spec/src/a.zod.ts', 'export const A = { LogTransport: true };\n']]), dxPkgs)?.kind === 'mention',
'DX-U3: every other export name keeps the bare-word arm (a key named after it is still a mention)');
}

// ---- G6: a changeset that was ALREADY breaking at base is inherited -------
Expand Down
Loading