mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-16 20:53:57 +08:00
d03e4dbf6f
The sealed-module rule only covered modules exporting nothing. Per symbol
now: a JS/TS top-level binding declared by its own statement (function,
const/let/var, class, interface, type, enum) in an ES module is reachable
from another file only if it's exported — the `export` keyword (node flag or
its line), an `export { a as b }` list, `export default x` or
`export default { a, b }`. Classic scripts, CommonJS, `declare global`,
`.d.ts`, qualified members, object-literal members (zustand store actions)
and `proto.x = function x()` are exempt. Applied at the reachability gate,
so the winner is rejected rather than another promoted.
A/B (edges removed / added): zod -229 (package `z` imports → a local `z`),
excalidraw -66 (DOM `Element` → a test helper's type), kit -50, typeorm -47
(`import { ColumnMetadata } from "typeorm"` → a fixture's local), trpc -26,
hono -8, axios -1; express byte-identical.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
78 lines
2.7 KiB
TypeScript
78 lines
2.7 KiB
TypeScript
/**
|
|
* A top-level binding an ES module doesn't export can't be named from another
|
|
* file. sveltekit's `generate_manifest.js` keeps an unexported `resolve` that
|
|
* other files' `resolve(…)` calls went to; typeorm's test inputs' local
|
|
* `ColumnMetadata` took `import { ColumnMetadata } from "typeorm"`. Exported
|
|
* forms all still count: the `export` keyword, `export { a as b }`,
|
|
* `export default x`, `export default { a, b }`; so do classic scripts,
|
|
* CommonJS files and functions assigned onto a prototype.
|
|
*/
|
|
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { CodeGraph } from '../src';
|
|
|
|
let root = '';
|
|
let cg: CodeGraph;
|
|
|
|
beforeAll(async () => {
|
|
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-esm-unexported-'));
|
|
const files: Record<string, string> = {
|
|
'package.json': JSON.stringify({ name: 'app', type: 'module' }),
|
|
'src/manifest.js': `import { join } from 'node:path';
|
|
function resolve(event) { return join(event); }
|
|
export function manifest() { return resolve('x'); }
|
|
`,
|
|
'src/adapters.js': `import { a } from './a.js';
|
|
function getAdapter(name) { return name; }
|
|
export default { getAdapter };
|
|
`,
|
|
'src/listed.js': `import { a } from './a.js';
|
|
function helper() { return 1; }
|
|
export { helper };
|
|
`,
|
|
'src/proto.js': `import { a } from './a.js';
|
|
function Params() {}
|
|
const prototype = Params.prototype;
|
|
prototype.render = function render() { return ''; };
|
|
export default Params;
|
|
`,
|
|
'src/a.js': `export const a = 1;
|
|
`,
|
|
'src/use.js': `import { helper } from './listed.js';
|
|
import Params from './proto.js';
|
|
export async function handle(event) {
|
|
await resolve(event);
|
|
getAdapter('xhr');
|
|
helper();
|
|
new Params().render();
|
|
}
|
|
`,
|
|
};
|
|
for (const [rel, content] of Object.entries(files)) {
|
|
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
|
fs.writeFileSync(path.join(root, rel), content);
|
|
}
|
|
cg = await CodeGraph.init(root, { index: true });
|
|
});
|
|
|
|
afterAll(() => {
|
|
cg?.close();
|
|
if (root) fs.rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('unexported ES module bindings', () => {
|
|
it('are not reached from another file; every export form still is', () => {
|
|
const handle = cg.getNodesInFile('src/use.js').find((n) => n.name === 'handle')!;
|
|
const targets = cg
|
|
.getOutgoingEdges(handle.id)
|
|
.filter((e) => e.kind === 'calls')
|
|
.map((e) => `${cg.getNode(e.target)!.filePath}:${cg.getNode(e.target)!.name}`);
|
|
expect(targets).not.toContain('src/manifest.js:resolve');
|
|
expect(targets).toContain('src/adapters.js:getAdapter');
|
|
expect(targets).toContain('src/listed.js:helper');
|
|
expect(targets).toContain('src/proto.js:render');
|
|
});
|
|
});
|