@dunx/transform 0.2.6 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-yzfpwzqk.js +159 -0
- package/dist/chunk-yzfpwzqk.js.map +14 -0
- package/dist/index.js +6 -153
- package/dist/index.js.map +3 -8
- package/dist/preload.js +5 -157
- package/dist/preload.js.map +4 -9
- package/package.json +1 -1
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/edits.ts
|
|
3
|
+
var applyEdits = (source, edits) => {
|
|
4
|
+
const sorted = [...edits].sort((left, right) => left.start - right.start || left.end - right.end);
|
|
5
|
+
let out = "";
|
|
6
|
+
let cursor = 0;
|
|
7
|
+
for (const edit of sorted) {
|
|
8
|
+
if (edit.start < cursor) {
|
|
9
|
+
throw new Error(`Overlapping edit at ${edit.start}..${edit.end}; the previous edit ended ` + `at ${cursor}.`);
|
|
10
|
+
}
|
|
11
|
+
out += source.slice(cursor, edit.start) + edit.text;
|
|
12
|
+
cursor = edit.end;
|
|
13
|
+
}
|
|
14
|
+
return out + source.slice(cursor);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/deps.ts
|
|
18
|
+
import { parseSync } from "oxc-parser";
|
|
19
|
+
|
|
20
|
+
// src/ast.ts
|
|
21
|
+
var guard = (type) => (node) => node?.type === type;
|
|
22
|
+
var isIdentifier = guard("Identifier");
|
|
23
|
+
var isMethodDefinition = guard("MethodDefinition");
|
|
24
|
+
var isTypeReference = guard("TSTypeReference");
|
|
25
|
+
var isImportDeclaration = guard("ImportDeclaration");
|
|
26
|
+
var isImportSpecifier = guard("ImportSpecifier");
|
|
27
|
+
var isParameterProperty = guard("TSParameterProperty");
|
|
28
|
+
var isClassDeclaration = (node) => node.type === "ClassDeclaration";
|
|
29
|
+
var walk = (root, visit) => {
|
|
30
|
+
if (Array.isArray(root)) {
|
|
31
|
+
for (const child of root)
|
|
32
|
+
walk(child, visit);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (root === null || typeof root !== "object")
|
|
36
|
+
return;
|
|
37
|
+
const candidate = root;
|
|
38
|
+
if (typeof candidate.type === "string" && typeof candidate.start === "number" && typeof candidate.end === "number") {
|
|
39
|
+
visit(root);
|
|
40
|
+
}
|
|
41
|
+
for (const child of Object.values(root))
|
|
42
|
+
walk(child, visit);
|
|
43
|
+
};
|
|
44
|
+
var nameOf = (node) => isIdentifier(node) ? node.name : undefined;
|
|
45
|
+
|
|
46
|
+
// src/erased.ts
|
|
47
|
+
var collectTypeOnlyNames = (program) => {
|
|
48
|
+
const names = new Set;
|
|
49
|
+
walk(program, (node) => {
|
|
50
|
+
if (isImportDeclaration(node)) {
|
|
51
|
+
for (const specifier of node.specifiers) {
|
|
52
|
+
if (!isImportSpecifier(specifier))
|
|
53
|
+
continue;
|
|
54
|
+
if (node.importKind === "type" || specifier.importKind === "type") {
|
|
55
|
+
names.add(specifier.local.name);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (node.type === "TSInterfaceDeclaration" || node.type === "TSTypeAliasDeclaration") {
|
|
61
|
+
const declared = node.id;
|
|
62
|
+
const name = nameOf(declared);
|
|
63
|
+
if (name !== undefined)
|
|
64
|
+
names.add(name);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
return names;
|
|
68
|
+
};
|
|
69
|
+
var collectTypeParameters = (klass) => {
|
|
70
|
+
const names = new Set;
|
|
71
|
+
if (klass.typeParameters === null)
|
|
72
|
+
return names;
|
|
73
|
+
walk(klass.typeParameters, (node) => {
|
|
74
|
+
if (node.type !== "TSTypeParameter")
|
|
75
|
+
return;
|
|
76
|
+
const name = nameOf(node.name);
|
|
77
|
+
if (name !== undefined)
|
|
78
|
+
names.add(name);
|
|
79
|
+
});
|
|
80
|
+
return names;
|
|
81
|
+
};
|
|
82
|
+
var erasedNames = (typeOnly, klass) => new Set([...typeOnly, ...collectTypeParameters(klass)]);
|
|
83
|
+
|
|
84
|
+
// src/deps.ts
|
|
85
|
+
var DEPS_KEY = "Symbol.for('dunx.deps')";
|
|
86
|
+
var slice = (source, node) => source.slice(node.start, node.end);
|
|
87
|
+
var constructorParams = (klass) => {
|
|
88
|
+
const found = klass.body.body.find((member) => isMethodDefinition(member) && nameOf(member.key) === "constructor");
|
|
89
|
+
return isMethodDefinition(found) ? found.value.params : [];
|
|
90
|
+
};
|
|
91
|
+
var annotationOf = (param) => {
|
|
92
|
+
const inner = isParameterProperty(param) ? param.parameter : param;
|
|
93
|
+
if (!isIdentifier(inner))
|
|
94
|
+
return;
|
|
95
|
+
return inner.typeAnnotation?.typeAnnotation;
|
|
96
|
+
};
|
|
97
|
+
var entryFor = (source, param, erased) => {
|
|
98
|
+
const unresolved = `{ unresolved: ${JSON.stringify(slice(source, param))} }`;
|
|
99
|
+
const annotation = annotationOf(param);
|
|
100
|
+
if (!annotation || !isTypeReference(annotation))
|
|
101
|
+
return unresolved;
|
|
102
|
+
const token = slice(source, annotation.typeName);
|
|
103
|
+
if (erased.has(token))
|
|
104
|
+
return unresolved;
|
|
105
|
+
return token;
|
|
106
|
+
};
|
|
107
|
+
var transform = (source, filename = "input.ts") => {
|
|
108
|
+
const parsed = parseSync(filename, source);
|
|
109
|
+
if (parsed.errors.length > 0) {
|
|
110
|
+
const detail = parsed.errors.slice(0, 3).map((error) => error.message).join("; ");
|
|
111
|
+
throw new Error(`${filename}: could not parse - ${detail}`);
|
|
112
|
+
}
|
|
113
|
+
const program = parsed.program;
|
|
114
|
+
const typeOnly = collectTypeOnlyNames(program);
|
|
115
|
+
const edits = [];
|
|
116
|
+
const annotated = [];
|
|
117
|
+
walk(program, (node) => {
|
|
118
|
+
if (!isClassDeclaration(node))
|
|
119
|
+
return;
|
|
120
|
+
const name = node.id?.name;
|
|
121
|
+
if (name === undefined)
|
|
122
|
+
return;
|
|
123
|
+
const erased = erasedNames(typeOnly, node);
|
|
124
|
+
const params = constructorParams(node);
|
|
125
|
+
if (params.length > 0) {
|
|
126
|
+
const entries = params.map((param) => entryFor(source, param, erased));
|
|
127
|
+
annotated.push(name);
|
|
128
|
+
edits.push({
|
|
129
|
+
start: node.end,
|
|
130
|
+
end: node.end,
|
|
131
|
+
text: `
|
|
132
|
+
Object.defineProperty(${name}, ${DEPS_KEY}, {
|
|
133
|
+
` + ` value: () => [${entries.join(", ")}],
|
|
134
|
+
});`
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
const code = applyEdits(source, edits);
|
|
139
|
+
return { code, changed: code !== source, annotated };
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// src/plugin.ts
|
|
143
|
+
var depsPlugin = {
|
|
144
|
+
name: "dunx-deps",
|
|
145
|
+
setup(build) {
|
|
146
|
+
build.onLoad({ filter: /\.tsx?$/ }, async ({ path }) => {
|
|
147
|
+
const source = await Bun.file(path).text();
|
|
148
|
+
const loader = path.endsWith(".tsx") ? "tsx" : "ts";
|
|
149
|
+
if (path.includes("/node_modules/"))
|
|
150
|
+
return { contents: source, loader };
|
|
151
|
+
return { contents: transform(source, path).code, loader };
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export { applyEdits, transform, depsPlugin };
|
|
157
|
+
|
|
158
|
+
//# debugId=1B91E58995A13AAC64756E2164756E21
|
|
159
|
+
//# sourceMappingURL=chunk-yzfpwzqk.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/edits.ts", "../src/deps.ts", "../src/ast.ts", "../src/erased.ts", "../src/plugin.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"export interface Edit {\n readonly start: number;\n readonly end: number;\n readonly text: string;\n}\n\n/**\n * Splices edits into the original text rather than reprinting the AST, so\n * everything untouched keeps its exact bytes - comments, formatting, and the\n * line numbers stack traces point at.\n */\nexport const applyEdits = (source: string, edits: readonly Edit[]): string => {\n const sorted = [...edits].sort(\n (left, right) => left.start - right.start || left.end - right.end,\n );\n\n let out = '';\n let cursor = 0;\n\n for (const edit of sorted) {\n if (edit.start < cursor) {\n throw new Error(\n `Overlapping edit at ${edit.start}..${edit.end}; the previous edit ended ` +\n `at ${cursor}.`,\n );\n }\n out += source.slice(cursor, edit.start) + edit.text;\n cursor = edit.end;\n }\n\n return out + source.slice(cursor);\n};\n",
|
|
6
|
+
"import { parseSync } from 'oxc-parser';\nimport {\n isClassDeclaration,\n isIdentifier,\n isMethodDefinition,\n isParameterProperty,\n isTypeReference,\n nameOf,\n walk,\n type ClassNode,\n type Node,\n} from './ast.js';\nimport { applyEdits, type Edit } from './edits.js';\nimport { collectTypeOnlyNames, erasedNames } from './erased.js';\n\n/**\n * `Symbol.for`, not `Symbol`: two copies of `@dunx/core` in one dependency tree\n * still agree on the key. Same technique as module and route markers.\n */\nconst DEPS_KEY = \"Symbol.for('dunx.deps')\";\n\nexport interface TransformResult {\n readonly code: string;\n readonly changed: boolean;\n /** Classes that received dependency metadata, in source order. */\n readonly annotated: readonly string[];\n}\n\nconst slice = (source: string, node: Node): string =>\n source.slice(node.start, node.end);\n\nconst constructorParams = (klass: ClassNode): readonly Node[] => {\n const found = klass.body.body.find(\n (member) =>\n isMethodDefinition(member) && nameOf(member.key) === 'constructor',\n );\n return isMethodDefinition(found) ? found.value.params : [];\n};\n\n/** The declared type of a parameter, unwrapping `private readonly x: X`. */\nconst annotationOf = (param: Node): Node | undefined => {\n const inner = isParameterProperty(param) ? param.parameter : param;\n if (!isIdentifier(inner)) return undefined;\n return inner.typeAnnotation?.typeAnnotation;\n};\n\n/**\n * One entry per constructor parameter. A parameter whose type names something\n * that exists at runtime becomes that expression; anything else becomes an\n * `unresolved` descriptor so the container can name it precisely at boot instead\n * of constructing a broken object.\n */\nconst entryFor = (\n source: string,\n param: Node,\n erased: ReadonlySet<string>,\n): string => {\n const unresolved = `{ unresolved: ${JSON.stringify(slice(source, param))} }`;\n const annotation = annotationOf(param);\n\n if (!annotation || !isTypeReference(annotation)) return unresolved;\n\n const token = slice(source, annotation.typeName);\n // A qualified name (`ns.Thing`) is a runtime value; a bare erased name is not.\n if (erased.has(token)) return unresolved;\n\n return token;\n};\n\n/**\n * Records each class's constructor dependencies, and each decorated field's\n * declared type, as thunks on the class itself.\n *\n * A thunk, not a literal: the body is evaluated when the record is read rather\n * than when the module is defined, so a dependency declared later in the file -\n * or in a circular import - is not a temporal-dead-zone crash. That is what\n * removes the need for Nest's `forwardRef`, and it is also why a class decorator\n * cannot read the field record while it runs: the statement is appended after the\n * class, which is after decoration.\n */\nexport const transform = (\n source: string,\n filename = 'input.ts',\n): TransformResult => {\n const parsed = parseSync(filename, source);\n\n if (parsed.errors.length > 0) {\n const detail = parsed.errors\n .slice(0, 3)\n .map((error) => error.message)\n .join('; ');\n throw new Error(`${filename}: could not parse - ${detail}`);\n }\n\n const program = parsed.program;\n const typeOnly = collectTypeOnlyNames(program);\n const edits: Edit[] = [];\n const annotated: string[] = [];\n\n walk(program, (node) => {\n if (!isClassDeclaration(node)) return;\n\n const name = node.id?.name;\n if (name === undefined) return;\n\n const erased = erasedNames(typeOnly, node);\n const params = constructorParams(node);\n\n if (params.length > 0) {\n const entries = params.map((param) => entryFor(source, param, erased));\n annotated.push(name);\n edits.push({\n start: node.end,\n end: node.end,\n text:\n `\\nObject.defineProperty(${name}, ${DEPS_KEY}, {\\n` +\n ` value: () => [${entries.join(', ')}],\\n});`,\n });\n }\n });\n\n const code = applyEdits(source, edits);\n return { code, changed: code !== source, annotated };\n};\n",
|
|
7
|
+
"/**\n * Minimal structural views over oxc's ESTree output - only the node shapes the\n * dependency transform reads. Verified against real `oxc-parser` output.\n */\nexport interface Node {\n readonly type: string;\n readonly start: number;\n readonly end: number;\n}\n\nexport interface Identifier extends Node {\n readonly type: 'Identifier';\n readonly name: string;\n readonly typeAnnotation?: TSTypeAnnotation | null;\n}\n\nexport interface ClassBody extends Node {\n readonly type: 'ClassBody';\n readonly body: readonly Node[];\n}\n\n/** `ClassDeclaration` and `ClassExpression` share every field read here. */\nexport interface ClassNode extends Node {\n readonly id: Identifier | null;\n readonly typeParameters: Node | null;\n readonly body: ClassBody;\n}\n\nexport interface FunctionExpression extends Node {\n readonly params: readonly Node[];\n}\n\nexport interface MethodDefinition extends Node {\n readonly type: 'MethodDefinition';\n readonly key: Node;\n readonly value: FunctionExpression;\n}\n\nexport interface TSParameterProperty extends Node {\n readonly type: 'TSParameterProperty';\n readonly parameter: Node;\n}\n\nexport interface TSTypeAnnotation extends Node {\n readonly type: 'TSTypeAnnotation';\n readonly typeAnnotation: Node;\n}\n\nexport interface TSTypeReference extends Node {\n readonly type: 'TSTypeReference';\n readonly typeName: Node;\n}\n\nexport interface ImportDeclaration extends Node {\n readonly type: 'ImportDeclaration';\n readonly specifiers: readonly Node[];\n readonly importKind: 'value' | 'type';\n}\n\nexport interface ImportSpecifier extends Node {\n readonly type: 'ImportSpecifier';\n readonly local: Identifier;\n readonly importKind: 'value' | 'type';\n}\n\nconst guard =\n <T extends Node>(type: T['type']) =>\n (node: Node | null | undefined): node is T =>\n node?.type === type;\n\nexport const isIdentifier = guard<Identifier>('Identifier');\nexport const isMethodDefinition = guard<MethodDefinition>('MethodDefinition');\nexport const isTypeReference = guard<TSTypeReference>('TSTypeReference');\nexport const isImportDeclaration =\n guard<ImportDeclaration>('ImportDeclaration');\nexport const isImportSpecifier = guard<ImportSpecifier>('ImportSpecifier');\nexport const isParameterProperty = guard<TSParameterProperty>(\n 'TSParameterProperty',\n);\n\n/**\n * Declarations only. A `ClassExpression`'s own name is bound inside the class\n * body, not outside it, so a statement appended after `const X = class Foo {}`\n * could not reference `Foo` - it would be a ReferenceError at load.\n */\nexport const isClassDeclaration = (node: Node): node is ClassNode =>\n node.type === 'ClassDeclaration';\n\n/**\n * Depth-first over every object in the tree. oxc's ESTree output is a plain\n * object graph with no parent links, so recursing every own value is safe and\n * needs no visitor-key table.\n */\nexport const walk = (root: unknown, visit: (node: Node) => void): void => {\n if (Array.isArray(root)) {\n for (const child of root) walk(child, visit);\n return;\n }\n if (root === null || typeof root !== 'object') return;\n\n const candidate = root as Partial<Node>;\n if (\n typeof candidate.type === 'string' &&\n typeof candidate.start === 'number' &&\n typeof candidate.end === 'number'\n ) {\n visit(root as Node);\n }\n\n for (const child of Object.values(root)) walk(child, visit);\n};\n\nexport const nameOf = (node: Node | null | undefined): string | undefined =>\n isIdentifier(node) ? node.name : undefined;\n",
|
|
8
|
+
"import {\n isImportDeclaration,\n isImportSpecifier,\n nameOf,\n walk,\n type ClassNode,\n type Node,\n} from './ast.js';\n\n/**\n * Names that exist only in the type system, so emitting them in a value position\n * would be a `ReferenceError` at runtime. Collected per file: type-only imports,\n * inline `type` specifiers, local interfaces and type aliases.\n */\nexport const collectTypeOnlyNames = (program: Node): ReadonlySet<string> => {\n const names = new Set<string>();\n\n walk(program, (node) => {\n if (isImportDeclaration(node)) {\n for (const specifier of node.specifiers) {\n if (!isImportSpecifier(specifier)) continue;\n if (node.importKind === 'type' || specifier.importKind === 'type') {\n names.add(specifier.local.name);\n }\n }\n return;\n }\n\n if (\n node.type === 'TSInterfaceDeclaration' ||\n node.type === 'TSTypeAliasDeclaration'\n ) {\n const declared = (node as { id?: Node }).id;\n const name = nameOf(declared);\n if (name !== undefined) names.add(name);\n }\n });\n\n return names;\n};\n\n/** A class's own type parameters are erased, so `T` is never a usable token. */\nexport const collectTypeParameters = (\n klass: ClassNode,\n): ReadonlySet<string> => {\n const names = new Set<string>();\n if (klass.typeParameters === null) return names;\n\n walk(klass.typeParameters, (node) => {\n if (node.type !== 'TSTypeParameter') return;\n const name = nameOf((node as { name?: Node }).name);\n if (name !== undefined) names.add(name);\n });\n\n return names;\n};\n\n/** Every name in this file that a value position cannot use. */\nexport const erasedNames = (\n typeOnly: ReadonlySet<string>,\n klass: ClassNode,\n): ReadonlySet<string> =>\n new Set([...typeOnly, ...collectTypeParameters(klass)]);\n",
|
|
9
|
+
"import type { BunPlugin } from 'bun';\nimport { transform } from './deps.js';\n\n/**\n * Rewrites TypeScript as it is loaded so the container can read constructor\n * dependencies. Usable in three places, all the same object:\n *\n * - `bunfig.toml` -> `preload = [\"@dunx/transform/preload\"]` for `bun run`\n * - `Bun.build({ plugins: [depsPlugin] })` for a production build\n * - `Bun.plugin(depsPlugin)` from a test preload\n *\n * Dependencies are skipped: a published package was already transformed by its\n * own build, and re-parsing `node_modules` on every load is pure cost.\n */\nexport const depsPlugin: BunPlugin = {\n name: 'dunx-deps',\n setup(build) {\n // A runtime plugin's onLoad must always return a result - there is no\n // \"decline and fall through\", so untransformed files are handed back as-is.\n build.onLoad({ filter: /\\.tsx?$/ }, async ({ path }) => {\n const source = await Bun.file(path).text();\n const loader = path.endsWith('.tsx') ? 'tsx' : 'ts';\n\n if (path.includes('/node_modules/')) return { contents: source, loader };\n\n return { contents: transform(source, path).code, loader };\n });\n },\n};\n"
|
|
10
|
+
],
|
|
11
|
+
"mappings": "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",
|
|
12
|
+
"debugId": "1B91E58995A13AAC64756E2164756E21",
|
|
13
|
+
"names": []
|
|
14
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,161 +1,14 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
var isIdentifier = guard("Identifier");
|
|
8
|
-
var isMethodDefinition = guard("MethodDefinition");
|
|
9
|
-
var isTypeReference = guard("TSTypeReference");
|
|
10
|
-
var isImportDeclaration = guard("ImportDeclaration");
|
|
11
|
-
var isImportSpecifier = guard("ImportSpecifier");
|
|
12
|
-
var isParameterProperty = guard("TSParameterProperty");
|
|
13
|
-
var isClassDeclaration = (node) => node.type === "ClassDeclaration";
|
|
14
|
-
var walk = (root, visit) => {
|
|
15
|
-
if (Array.isArray(root)) {
|
|
16
|
-
for (const child of root)
|
|
17
|
-
walk(child, visit);
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
if (root === null || typeof root !== "object")
|
|
21
|
-
return;
|
|
22
|
-
const candidate = root;
|
|
23
|
-
if (typeof candidate.type === "string" && typeof candidate.start === "number" && typeof candidate.end === "number") {
|
|
24
|
-
visit(root);
|
|
25
|
-
}
|
|
26
|
-
for (const child of Object.values(root))
|
|
27
|
-
walk(child, visit);
|
|
28
|
-
};
|
|
29
|
-
var nameOf = (node) => isIdentifier(node) ? node.name : undefined;
|
|
30
|
-
|
|
31
|
-
// src/edits.ts
|
|
32
|
-
var applyEdits = (source, edits) => {
|
|
33
|
-
const sorted = [...edits].sort((left, right) => left.start - right.start || left.end - right.end);
|
|
34
|
-
let out = "";
|
|
35
|
-
let cursor = 0;
|
|
36
|
-
for (const edit of sorted) {
|
|
37
|
-
if (edit.start < cursor) {
|
|
38
|
-
throw new Error(`Overlapping edit at ${edit.start}..${edit.end}; the previous edit ended ` + `at ${cursor}.`);
|
|
39
|
-
}
|
|
40
|
-
out += source.slice(cursor, edit.start) + edit.text;
|
|
41
|
-
cursor = edit.end;
|
|
42
|
-
}
|
|
43
|
-
return out + source.slice(cursor);
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
// src/erased.ts
|
|
47
|
-
var collectTypeOnlyNames = (program) => {
|
|
48
|
-
const names = new Set;
|
|
49
|
-
walk(program, (node) => {
|
|
50
|
-
if (isImportDeclaration(node)) {
|
|
51
|
-
for (const specifier of node.specifiers) {
|
|
52
|
-
if (!isImportSpecifier(specifier))
|
|
53
|
-
continue;
|
|
54
|
-
if (node.importKind === "type" || specifier.importKind === "type") {
|
|
55
|
-
names.add(specifier.local.name);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
if (node.type === "TSInterfaceDeclaration" || node.type === "TSTypeAliasDeclaration") {
|
|
61
|
-
const declared = node.id;
|
|
62
|
-
const name = nameOf(declared);
|
|
63
|
-
if (name !== undefined)
|
|
64
|
-
names.add(name);
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
return names;
|
|
68
|
-
};
|
|
69
|
-
var collectTypeParameters = (klass) => {
|
|
70
|
-
const names = new Set;
|
|
71
|
-
if (klass.typeParameters === null)
|
|
72
|
-
return names;
|
|
73
|
-
walk(klass.typeParameters, (node) => {
|
|
74
|
-
if (node.type !== "TSTypeParameter")
|
|
75
|
-
return;
|
|
76
|
-
const name = nameOf(node.name);
|
|
77
|
-
if (name !== undefined)
|
|
78
|
-
names.add(name);
|
|
79
|
-
});
|
|
80
|
-
return names;
|
|
81
|
-
};
|
|
82
|
-
var erasedNames = (typeOnly, klass) => new Set([...typeOnly, ...collectTypeParameters(klass)]);
|
|
83
|
-
|
|
84
|
-
// src/deps.ts
|
|
85
|
-
var DEPS_KEY = "Symbol.for('dunx.deps')";
|
|
86
|
-
var slice = (source, node) => source.slice(node.start, node.end);
|
|
87
|
-
var constructorParams = (klass) => {
|
|
88
|
-
const found = klass.body.body.find((member) => isMethodDefinition(member) && nameOf(member.key) === "constructor");
|
|
89
|
-
return isMethodDefinition(found) ? found.value.params : [];
|
|
90
|
-
};
|
|
91
|
-
var annotationOf = (param) => {
|
|
92
|
-
const inner = isParameterProperty(param) ? param.parameter : param;
|
|
93
|
-
if (!isIdentifier(inner))
|
|
94
|
-
return;
|
|
95
|
-
return inner.typeAnnotation?.typeAnnotation;
|
|
96
|
-
};
|
|
97
|
-
var entryFor = (source, param, erased) => {
|
|
98
|
-
const unresolved = `{ unresolved: ${JSON.stringify(slice(source, param))} }`;
|
|
99
|
-
const annotation = annotationOf(param);
|
|
100
|
-
if (!annotation || !isTypeReference(annotation))
|
|
101
|
-
return unresolved;
|
|
102
|
-
const token = slice(source, annotation.typeName);
|
|
103
|
-
if (erased.has(token))
|
|
104
|
-
return unresolved;
|
|
105
|
-
return token;
|
|
106
|
-
};
|
|
107
|
-
var transform = (source, filename = "input.ts") => {
|
|
108
|
-
const parsed = parseSync(filename, source);
|
|
109
|
-
if (parsed.errors.length > 0) {
|
|
110
|
-
const detail = parsed.errors.slice(0, 3).map((error) => error.message).join("; ");
|
|
111
|
-
throw new Error(`${filename}: could not parse - ${detail}`);
|
|
112
|
-
}
|
|
113
|
-
const program = parsed.program;
|
|
114
|
-
const typeOnly = collectTypeOnlyNames(program);
|
|
115
|
-
const edits = [];
|
|
116
|
-
const annotated = [];
|
|
117
|
-
walk(program, (node) => {
|
|
118
|
-
if (!isClassDeclaration(node))
|
|
119
|
-
return;
|
|
120
|
-
const name = node.id?.name;
|
|
121
|
-
if (name === undefined)
|
|
122
|
-
return;
|
|
123
|
-
const erased = erasedNames(typeOnly, node);
|
|
124
|
-
const params = constructorParams(node);
|
|
125
|
-
if (params.length > 0) {
|
|
126
|
-
const entries = params.map((param) => entryFor(source, param, erased));
|
|
127
|
-
annotated.push(name);
|
|
128
|
-
edits.push({
|
|
129
|
-
start: node.end,
|
|
130
|
-
end: node.end,
|
|
131
|
-
text: `
|
|
132
|
-
Object.defineProperty(${name}, ${DEPS_KEY}, {
|
|
133
|
-
` + ` value: () => [${entries.join(", ")}],
|
|
134
|
-
});`
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
const code = applyEdits(source, edits);
|
|
139
|
-
return { code, changed: code !== source, annotated };
|
|
140
|
-
};
|
|
141
|
-
// src/plugin.ts
|
|
142
|
-
var depsPlugin = {
|
|
143
|
-
name: "dunx-deps",
|
|
144
|
-
setup(build) {
|
|
145
|
-
build.onLoad({ filter: /\.tsx?$/ }, async ({ path }) => {
|
|
146
|
-
const source = await Bun.file(path).text();
|
|
147
|
-
const loader = path.endsWith(".tsx") ? "tsx" : "ts";
|
|
148
|
-
if (path.includes("/node_modules/"))
|
|
149
|
-
return { contents: source, loader };
|
|
150
|
-
return { contents: transform(source, path).code, loader };
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
};
|
|
2
|
+
import {
|
|
3
|
+
applyEdits,
|
|
4
|
+
depsPlugin,
|
|
5
|
+
transform
|
|
6
|
+
} from "./chunk-yzfpwzqk.js";
|
|
154
7
|
export {
|
|
155
8
|
transform,
|
|
156
9
|
depsPlugin,
|
|
157
10
|
applyEdits
|
|
158
11
|
};
|
|
159
12
|
|
|
160
|
-
//# debugId=
|
|
13
|
+
//# debugId=4BB6ADD68D78212464756E2164756E21
|
|
161
14
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": [
|
|
3
|
+
"sources": [],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import { parseSync } from 'oxc-parser';\nimport {\n isClassDeclaration,\n isIdentifier,\n isMethodDefinition,\n isParameterProperty,\n isTypeReference,\n nameOf,\n walk,\n type ClassNode,\n type Node,\n} from './ast.js';\nimport { applyEdits, type Edit } from './edits.js';\nimport { collectTypeOnlyNames, erasedNames } from './erased.js';\n\n/**\n * `Symbol.for`, not `Symbol`: two copies of `@dunx/core` in one dependency tree\n * still agree on the key. Same technique as module and route markers.\n */\nconst DEPS_KEY = \"Symbol.for('dunx.deps')\";\n\nexport interface TransformResult {\n readonly code: string;\n readonly changed: boolean;\n /** Classes that received dependency metadata, in source order. */\n readonly annotated: readonly string[];\n}\n\nconst slice = (source: string, node: Node): string =>\n source.slice(node.start, node.end);\n\nconst constructorParams = (klass: ClassNode): readonly Node[] => {\n const found = klass.body.body.find(\n (member) =>\n isMethodDefinition(member) && nameOf(member.key) === 'constructor',\n );\n return isMethodDefinition(found) ? found.value.params : [];\n};\n\n/** The declared type of a parameter, unwrapping `private readonly x: X`. */\nconst annotationOf = (param: Node): Node | undefined => {\n const inner = isParameterProperty(param) ? param.parameter : param;\n if (!isIdentifier(inner)) return undefined;\n return inner.typeAnnotation?.typeAnnotation;\n};\n\n/**\n * One entry per constructor parameter. A parameter whose type names something\n * that exists at runtime becomes that expression; anything else becomes an\n * `unresolved` descriptor so the container can name it precisely at boot instead\n * of constructing a broken object.\n */\nconst entryFor = (\n source: string,\n param: Node,\n erased: ReadonlySet<string>,\n): string => {\n const unresolved = `{ unresolved: ${JSON.stringify(slice(source, param))} }`;\n const annotation = annotationOf(param);\n\n if (!annotation || !isTypeReference(annotation)) return unresolved;\n\n const token = slice(source, annotation.typeName);\n // A qualified name (`ns.Thing`) is a runtime value; a bare erased name is not.\n if (erased.has(token)) return unresolved;\n\n return token;\n};\n\n/**\n * Records each class's constructor dependencies, and each decorated field's\n * declared type, as thunks on the class itself.\n *\n * A thunk, not a literal: the body is evaluated when the record is read rather\n * than when the module is defined, so a dependency declared later in the file -\n * or in a circular import - is not a temporal-dead-zone crash. That is what\n * removes the need for Nest's `forwardRef`, and it is also why a class decorator\n * cannot read the field record while it runs: the statement is appended after the\n * class, which is after decoration.\n */\nexport const transform = (\n source: string,\n filename = 'input.ts',\n): TransformResult => {\n const parsed = parseSync(filename, source);\n\n if (parsed.errors.length > 0) {\n const detail = parsed.errors\n .slice(0, 3)\n .map((error) => error.message)\n .join('; ');\n throw new Error(`${filename}: could not parse - ${detail}`);\n }\n\n const program = parsed.program;\n const typeOnly = collectTypeOnlyNames(program);\n const edits: Edit[] = [];\n const annotated: string[] = [];\n\n walk(program, (node) => {\n if (!isClassDeclaration(node)) return;\n\n const name = node.id?.name;\n if (name === undefined) return;\n\n const erased = erasedNames(typeOnly, node);\n const params = constructorParams(node);\n\n if (params.length > 0) {\n const entries = params.map((param) => entryFor(source, param, erased));\n annotated.push(name);\n edits.push({\n start: node.end,\n end: node.end,\n text:\n `\\nObject.defineProperty(${name}, ${DEPS_KEY}, {\\n` +\n ` value: () => [${entries.join(', ')}],\\n});`,\n });\n }\n });\n\n const code = applyEdits(source, edits);\n return { code, changed: code !== source, annotated };\n};\n",
|
|
6
|
-
"/**\n * Minimal structural views over oxc's ESTree output - only the node shapes the\n * dependency transform reads. Verified against real `oxc-parser` output.\n */\nexport interface Node {\n readonly type: string;\n readonly start: number;\n readonly end: number;\n}\n\nexport interface Identifier extends Node {\n readonly type: 'Identifier';\n readonly name: string;\n readonly typeAnnotation?: TSTypeAnnotation | null;\n}\n\nexport interface ClassBody extends Node {\n readonly type: 'ClassBody';\n readonly body: readonly Node[];\n}\n\n/** `ClassDeclaration` and `ClassExpression` share every field read here. */\nexport interface ClassNode extends Node {\n readonly id: Identifier | null;\n readonly typeParameters: Node | null;\n readonly body: ClassBody;\n}\n\nexport interface FunctionExpression extends Node {\n readonly params: readonly Node[];\n}\n\nexport interface MethodDefinition extends Node {\n readonly type: 'MethodDefinition';\n readonly key: Node;\n readonly value: FunctionExpression;\n}\n\nexport interface TSParameterProperty extends Node {\n readonly type: 'TSParameterProperty';\n readonly parameter: Node;\n}\n\nexport interface TSTypeAnnotation extends Node {\n readonly type: 'TSTypeAnnotation';\n readonly typeAnnotation: Node;\n}\n\nexport interface TSTypeReference extends Node {\n readonly type: 'TSTypeReference';\n readonly typeName: Node;\n}\n\nexport interface ImportDeclaration extends Node {\n readonly type: 'ImportDeclaration';\n readonly specifiers: readonly Node[];\n readonly importKind: 'value' | 'type';\n}\n\nexport interface ImportSpecifier extends Node {\n readonly type: 'ImportSpecifier';\n readonly local: Identifier;\n readonly importKind: 'value' | 'type';\n}\n\nconst guard =\n <T extends Node>(type: T['type']) =>\n (node: Node | null | undefined): node is T =>\n node?.type === type;\n\nexport const isIdentifier = guard<Identifier>('Identifier');\nexport const isMethodDefinition = guard<MethodDefinition>('MethodDefinition');\nexport const isTypeReference = guard<TSTypeReference>('TSTypeReference');\nexport const isImportDeclaration =\n guard<ImportDeclaration>('ImportDeclaration');\nexport const isImportSpecifier = guard<ImportSpecifier>('ImportSpecifier');\nexport const isParameterProperty = guard<TSParameterProperty>(\n 'TSParameterProperty',\n);\n\n/**\n * Declarations only. A `ClassExpression`'s own name is bound inside the class\n * body, not outside it, so a statement appended after `const X = class Foo {}`\n * could not reference `Foo` - it would be a ReferenceError at load.\n */\nexport const isClassDeclaration = (node: Node): node is ClassNode =>\n node.type === 'ClassDeclaration';\n\n/**\n * Depth-first over every object in the tree. oxc's ESTree output is a plain\n * object graph with no parent links, so recursing every own value is safe and\n * needs no visitor-key table.\n */\nexport const walk = (root: unknown, visit: (node: Node) => void): void => {\n if (Array.isArray(root)) {\n for (const child of root) walk(child, visit);\n return;\n }\n if (root === null || typeof root !== 'object') return;\n\n const candidate = root as Partial<Node>;\n if (\n typeof candidate.type === 'string' &&\n typeof candidate.start === 'number' &&\n typeof candidate.end === 'number'\n ) {\n visit(root as Node);\n }\n\n for (const child of Object.values(root)) walk(child, visit);\n};\n\nexport const nameOf = (node: Node | null | undefined): string | undefined =>\n isIdentifier(node) ? node.name : undefined;\n",
|
|
7
|
-
"export interface Edit {\n readonly start: number;\n readonly end: number;\n readonly text: string;\n}\n\n/**\n * Splices edits into the original text rather than reprinting the AST, so\n * everything untouched keeps its exact bytes - comments, formatting, and the\n * line numbers stack traces point at.\n */\nexport const applyEdits = (source: string, edits: readonly Edit[]): string => {\n const sorted = [...edits].sort(\n (left, right) => left.start - right.start || left.end - right.end,\n );\n\n let out = '';\n let cursor = 0;\n\n for (const edit of sorted) {\n if (edit.start < cursor) {\n throw new Error(\n `Overlapping edit at ${edit.start}..${edit.end}; the previous edit ended ` +\n `at ${cursor}.`,\n );\n }\n out += source.slice(cursor, edit.start) + edit.text;\n cursor = edit.end;\n }\n\n return out + source.slice(cursor);\n};\n",
|
|
8
|
-
"import {\n isImportDeclaration,\n isImportSpecifier,\n nameOf,\n walk,\n type ClassNode,\n type Node,\n} from './ast.js';\n\n/**\n * Names that exist only in the type system, so emitting them in a value position\n * would be a `ReferenceError` at runtime. Collected per file: type-only imports,\n * inline `type` specifiers, local interfaces and type aliases.\n */\nexport const collectTypeOnlyNames = (program: Node): ReadonlySet<string> => {\n const names = new Set<string>();\n\n walk(program, (node) => {\n if (isImportDeclaration(node)) {\n for (const specifier of node.specifiers) {\n if (!isImportSpecifier(specifier)) continue;\n if (node.importKind === 'type' || specifier.importKind === 'type') {\n names.add(specifier.local.name);\n }\n }\n return;\n }\n\n if (\n node.type === 'TSInterfaceDeclaration' ||\n node.type === 'TSTypeAliasDeclaration'\n ) {\n const declared = (node as { id?: Node }).id;\n const name = nameOf(declared);\n if (name !== undefined) names.add(name);\n }\n });\n\n return names;\n};\n\n/** A class's own type parameters are erased, so `T` is never a usable token. */\nexport const collectTypeParameters = (\n klass: ClassNode,\n): ReadonlySet<string> => {\n const names = new Set<string>();\n if (klass.typeParameters === null) return names;\n\n walk(klass.typeParameters, (node) => {\n if (node.type !== 'TSTypeParameter') return;\n const name = nameOf((node as { name?: Node }).name);\n if (name !== undefined) names.add(name);\n });\n\n return names;\n};\n\n/** Every name in this file that a value position cannot use. */\nexport const erasedNames = (\n typeOnly: ReadonlySet<string>,\n klass: ClassNode,\n): ReadonlySet<string> =>\n new Set([...typeOnly, ...collectTypeParameters(klass)]);\n",
|
|
9
|
-
"import type { BunPlugin } from 'bun';\nimport { transform } from './deps.js';\n\n/**\n * Rewrites TypeScript as it is loaded so the container can read constructor\n * dependencies. Usable in three places, all the same object:\n *\n * - `bunfig.toml` -> `preload = [\"@dunx/transform/preload\"]` for `bun run`\n * - `Bun.build({ plugins: [depsPlugin] })` for a production build\n * - `Bun.plugin(depsPlugin)` from a test preload\n *\n * Dependencies are skipped: a published package was already transformed by its\n * own build, and re-parsing `node_modules` on every load is pure cost.\n */\nexport const depsPlugin: BunPlugin = {\n name: 'dunx-deps',\n setup(build) {\n // A runtime plugin's onLoad must always return a result - there is no\n // \"decline and fall through\", so untransformed files are handed back as-is.\n build.onLoad({ filter: /\\.tsx?$/ }, async ({ path }) => {\n const source = await Bun.file(path).text();\n const loader = path.endsWith('.tsx') ? 'tsx' : 'ts';\n\n if (path.includes('/node_modules/')) return { contents: source, loader };\n\n return { contents: transform(source, path).code, loader };\n });\n },\n};\n"
|
|
10
5
|
],
|
|
11
|
-
"mappings": "
|
|
12
|
-
"debugId": "
|
|
6
|
+
"mappings": "",
|
|
7
|
+
"debugId": "4BB6ADD68D78212464756E2164756E21",
|
|
13
8
|
"names": []
|
|
14
9
|
}
|
package/dist/preload.js
CHANGED
|
@@ -1,163 +1,11 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
// src/deps.ts
|
|
6
|
-
import { parseSync } from "oxc-parser";
|
|
7
|
-
|
|
8
|
-
// src/ast.ts
|
|
9
|
-
var guard = (type) => (node) => node?.type === type;
|
|
10
|
-
var isIdentifier = guard("Identifier");
|
|
11
|
-
var isMethodDefinition = guard("MethodDefinition");
|
|
12
|
-
var isTypeReference = guard("TSTypeReference");
|
|
13
|
-
var isImportDeclaration = guard("ImportDeclaration");
|
|
14
|
-
var isImportSpecifier = guard("ImportSpecifier");
|
|
15
|
-
var isParameterProperty = guard("TSParameterProperty");
|
|
16
|
-
var isClassDeclaration = (node) => node.type === "ClassDeclaration";
|
|
17
|
-
var walk = (root, visit) => {
|
|
18
|
-
if (Array.isArray(root)) {
|
|
19
|
-
for (const child of root)
|
|
20
|
-
walk(child, visit);
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
if (root === null || typeof root !== "object")
|
|
24
|
-
return;
|
|
25
|
-
const candidate = root;
|
|
26
|
-
if (typeof candidate.type === "string" && typeof candidate.start === "number" && typeof candidate.end === "number") {
|
|
27
|
-
visit(root);
|
|
28
|
-
}
|
|
29
|
-
for (const child of Object.values(root))
|
|
30
|
-
walk(child, visit);
|
|
31
|
-
};
|
|
32
|
-
var nameOf = (node) => isIdentifier(node) ? node.name : undefined;
|
|
33
|
-
|
|
34
|
-
// src/edits.ts
|
|
35
|
-
var applyEdits = (source, edits) => {
|
|
36
|
-
const sorted = [...edits].sort((left, right) => left.start - right.start || left.end - right.end);
|
|
37
|
-
let out = "";
|
|
38
|
-
let cursor = 0;
|
|
39
|
-
for (const edit of sorted) {
|
|
40
|
-
if (edit.start < cursor) {
|
|
41
|
-
throw new Error(`Overlapping edit at ${edit.start}..${edit.end}; the previous edit ended ` + `at ${cursor}.`);
|
|
42
|
-
}
|
|
43
|
-
out += source.slice(cursor, edit.start) + edit.text;
|
|
44
|
-
cursor = edit.end;
|
|
45
|
-
}
|
|
46
|
-
return out + source.slice(cursor);
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
// src/erased.ts
|
|
50
|
-
var collectTypeOnlyNames = (program) => {
|
|
51
|
-
const names = new Set;
|
|
52
|
-
walk(program, (node) => {
|
|
53
|
-
if (isImportDeclaration(node)) {
|
|
54
|
-
for (const specifier of node.specifiers) {
|
|
55
|
-
if (!isImportSpecifier(specifier))
|
|
56
|
-
continue;
|
|
57
|
-
if (node.importKind === "type" || specifier.importKind === "type") {
|
|
58
|
-
names.add(specifier.local.name);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
if (node.type === "TSInterfaceDeclaration" || node.type === "TSTypeAliasDeclaration") {
|
|
64
|
-
const declared = node.id;
|
|
65
|
-
const name = nameOf(declared);
|
|
66
|
-
if (name !== undefined)
|
|
67
|
-
names.add(name);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
return names;
|
|
71
|
-
};
|
|
72
|
-
var collectTypeParameters = (klass) => {
|
|
73
|
-
const names = new Set;
|
|
74
|
-
if (klass.typeParameters === null)
|
|
75
|
-
return names;
|
|
76
|
-
walk(klass.typeParameters, (node) => {
|
|
77
|
-
if (node.type !== "TSTypeParameter")
|
|
78
|
-
return;
|
|
79
|
-
const name = nameOf(node.name);
|
|
80
|
-
if (name !== undefined)
|
|
81
|
-
names.add(name);
|
|
82
|
-
});
|
|
83
|
-
return names;
|
|
84
|
-
};
|
|
85
|
-
var erasedNames = (typeOnly, klass) => new Set([...typeOnly, ...collectTypeParameters(klass)]);
|
|
86
|
-
|
|
87
|
-
// src/deps.ts
|
|
88
|
-
var DEPS_KEY = "Symbol.for('dunx.deps')";
|
|
89
|
-
var slice = (source, node) => source.slice(node.start, node.end);
|
|
90
|
-
var constructorParams = (klass) => {
|
|
91
|
-
const found = klass.body.body.find((member) => isMethodDefinition(member) && nameOf(member.key) === "constructor");
|
|
92
|
-
return isMethodDefinition(found) ? found.value.params : [];
|
|
93
|
-
};
|
|
94
|
-
var annotationOf = (param) => {
|
|
95
|
-
const inner = isParameterProperty(param) ? param.parameter : param;
|
|
96
|
-
if (!isIdentifier(inner))
|
|
97
|
-
return;
|
|
98
|
-
return inner.typeAnnotation?.typeAnnotation;
|
|
99
|
-
};
|
|
100
|
-
var entryFor = (source, param, erased) => {
|
|
101
|
-
const unresolved = `{ unresolved: ${JSON.stringify(slice(source, param))} }`;
|
|
102
|
-
const annotation = annotationOf(param);
|
|
103
|
-
if (!annotation || !isTypeReference(annotation))
|
|
104
|
-
return unresolved;
|
|
105
|
-
const token = slice(source, annotation.typeName);
|
|
106
|
-
if (erased.has(token))
|
|
107
|
-
return unresolved;
|
|
108
|
-
return token;
|
|
109
|
-
};
|
|
110
|
-
var transform = (source, filename = "input.ts") => {
|
|
111
|
-
const parsed = parseSync(filename, source);
|
|
112
|
-
if (parsed.errors.length > 0) {
|
|
113
|
-
const detail = parsed.errors.slice(0, 3).map((error) => error.message).join("; ");
|
|
114
|
-
throw new Error(`${filename}: could not parse - ${detail}`);
|
|
115
|
-
}
|
|
116
|
-
const program = parsed.program;
|
|
117
|
-
const typeOnly = collectTypeOnlyNames(program);
|
|
118
|
-
const edits = [];
|
|
119
|
-
const annotated = [];
|
|
120
|
-
walk(program, (node) => {
|
|
121
|
-
if (!isClassDeclaration(node))
|
|
122
|
-
return;
|
|
123
|
-
const name = node.id?.name;
|
|
124
|
-
if (name === undefined)
|
|
125
|
-
return;
|
|
126
|
-
const erased = erasedNames(typeOnly, node);
|
|
127
|
-
const params = constructorParams(node);
|
|
128
|
-
if (params.length > 0) {
|
|
129
|
-
const entries = params.map((param) => entryFor(source, param, erased));
|
|
130
|
-
annotated.push(name);
|
|
131
|
-
edits.push({
|
|
132
|
-
start: node.end,
|
|
133
|
-
end: node.end,
|
|
134
|
-
text: `
|
|
135
|
-
Object.defineProperty(${name}, ${DEPS_KEY}, {
|
|
136
|
-
` + ` value: () => [${entries.join(", ")}],
|
|
137
|
-
});`
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
const code = applyEdits(source, edits);
|
|
142
|
-
return { code, changed: code !== source, annotated };
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
// src/plugin.ts
|
|
146
|
-
var depsPlugin = {
|
|
147
|
-
name: "dunx-deps",
|
|
148
|
-
setup(build) {
|
|
149
|
-
build.onLoad({ filter: /\.tsx?$/ }, async ({ path }) => {
|
|
150
|
-
const source = await Bun.file(path).text();
|
|
151
|
-
const loader = path.endsWith(".tsx") ? "tsx" : "ts";
|
|
152
|
-
if (path.includes("/node_modules/"))
|
|
153
|
-
return { contents: source, loader };
|
|
154
|
-
return { contents: transform(source, path).code, loader };
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
};
|
|
2
|
+
import {
|
|
3
|
+
depsPlugin
|
|
4
|
+
} from "./chunk-yzfpwzqk.js";
|
|
158
5
|
|
|
159
6
|
// src/preload.ts
|
|
7
|
+
var {plugin } = globalThis.Bun;
|
|
160
8
|
await plugin(depsPlugin);
|
|
161
9
|
|
|
162
|
-
//# debugId=
|
|
10
|
+
//# debugId=3E4DE15278F056A464756E2164756E21
|
|
163
11
|
//# sourceMappingURL=preload.js.map
|
package/dist/preload.js.map
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/preload.ts"
|
|
3
|
+
"sources": ["../src/preload.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import { plugin } from 'bun';\nimport { depsPlugin } from './plugin.js';\n\n// Side-effect entrypoint for `bunfig.toml`:\n// preload = [\"@dunx/transform/preload\"]\n//\n// Awaited, not fire-and-forget: registration has to finish before the entrypoint\n// is loaded, or the first modules through would miss the transform.\nawait plugin(depsPlugin);\n"
|
|
6
|
-
"import { parseSync } from 'oxc-parser';\nimport {\n isClassDeclaration,\n isIdentifier,\n isMethodDefinition,\n isParameterProperty,\n isTypeReference,\n nameOf,\n walk,\n type ClassNode,\n type Node,\n} from './ast.js';\nimport { applyEdits, type Edit } from './edits.js';\nimport { collectTypeOnlyNames, erasedNames } from './erased.js';\n\n/**\n * `Symbol.for`, not `Symbol`: two copies of `@dunx/core` in one dependency tree\n * still agree on the key. Same technique as module and route markers.\n */\nconst DEPS_KEY = \"Symbol.for('dunx.deps')\";\n\nexport interface TransformResult {\n readonly code: string;\n readonly changed: boolean;\n /** Classes that received dependency metadata, in source order. */\n readonly annotated: readonly string[];\n}\n\nconst slice = (source: string, node: Node): string =>\n source.slice(node.start, node.end);\n\nconst constructorParams = (klass: ClassNode): readonly Node[] => {\n const found = klass.body.body.find(\n (member) =>\n isMethodDefinition(member) && nameOf(member.key) === 'constructor',\n );\n return isMethodDefinition(found) ? found.value.params : [];\n};\n\n/** The declared type of a parameter, unwrapping `private readonly x: X`. */\nconst annotationOf = (param: Node): Node | undefined => {\n const inner = isParameterProperty(param) ? param.parameter : param;\n if (!isIdentifier(inner)) return undefined;\n return inner.typeAnnotation?.typeAnnotation;\n};\n\n/**\n * One entry per constructor parameter. A parameter whose type names something\n * that exists at runtime becomes that expression; anything else becomes an\n * `unresolved` descriptor so the container can name it precisely at boot instead\n * of constructing a broken object.\n */\nconst entryFor = (\n source: string,\n param: Node,\n erased: ReadonlySet<string>,\n): string => {\n const unresolved = `{ unresolved: ${JSON.stringify(slice(source, param))} }`;\n const annotation = annotationOf(param);\n\n if (!annotation || !isTypeReference(annotation)) return unresolved;\n\n const token = slice(source, annotation.typeName);\n // A qualified name (`ns.Thing`) is a runtime value; a bare erased name is not.\n if (erased.has(token)) return unresolved;\n\n return token;\n};\n\n/**\n * Records each class's constructor dependencies, and each decorated field's\n * declared type, as thunks on the class itself.\n *\n * A thunk, not a literal: the body is evaluated when the record is read rather\n * than when the module is defined, so a dependency declared later in the file -\n * or in a circular import - is not a temporal-dead-zone crash. That is what\n * removes the need for Nest's `forwardRef`, and it is also why a class decorator\n * cannot read the field record while it runs: the statement is appended after the\n * class, which is after decoration.\n */\nexport const transform = (\n source: string,\n filename = 'input.ts',\n): TransformResult => {\n const parsed = parseSync(filename, source);\n\n if (parsed.errors.length > 0) {\n const detail = parsed.errors\n .slice(0, 3)\n .map((error) => error.message)\n .join('; ');\n throw new Error(`${filename}: could not parse - ${detail}`);\n }\n\n const program = parsed.program;\n const typeOnly = collectTypeOnlyNames(program);\n const edits: Edit[] = [];\n const annotated: string[] = [];\n\n walk(program, (node) => {\n if (!isClassDeclaration(node)) return;\n\n const name = node.id?.name;\n if (name === undefined) return;\n\n const erased = erasedNames(typeOnly, node);\n const params = constructorParams(node);\n\n if (params.length > 0) {\n const entries = params.map((param) => entryFor(source, param, erased));\n annotated.push(name);\n edits.push({\n start: node.end,\n end: node.end,\n text:\n `\\nObject.defineProperty(${name}, ${DEPS_KEY}, {\\n` +\n ` value: () => [${entries.join(', ')}],\\n});`,\n });\n }\n });\n\n const code = applyEdits(source, edits);\n return { code, changed: code !== source, annotated };\n};\n",
|
|
7
|
-
"/**\n * Minimal structural views over oxc's ESTree output - only the node shapes the\n * dependency transform reads. Verified against real `oxc-parser` output.\n */\nexport interface Node {\n readonly type: string;\n readonly start: number;\n readonly end: number;\n}\n\nexport interface Identifier extends Node {\n readonly type: 'Identifier';\n readonly name: string;\n readonly typeAnnotation?: TSTypeAnnotation | null;\n}\n\nexport interface ClassBody extends Node {\n readonly type: 'ClassBody';\n readonly body: readonly Node[];\n}\n\n/** `ClassDeclaration` and `ClassExpression` share every field read here. */\nexport interface ClassNode extends Node {\n readonly id: Identifier | null;\n readonly typeParameters: Node | null;\n readonly body: ClassBody;\n}\n\nexport interface FunctionExpression extends Node {\n readonly params: readonly Node[];\n}\n\nexport interface MethodDefinition extends Node {\n readonly type: 'MethodDefinition';\n readonly key: Node;\n readonly value: FunctionExpression;\n}\n\nexport interface TSParameterProperty extends Node {\n readonly type: 'TSParameterProperty';\n readonly parameter: Node;\n}\n\nexport interface TSTypeAnnotation extends Node {\n readonly type: 'TSTypeAnnotation';\n readonly typeAnnotation: Node;\n}\n\nexport interface TSTypeReference extends Node {\n readonly type: 'TSTypeReference';\n readonly typeName: Node;\n}\n\nexport interface ImportDeclaration extends Node {\n readonly type: 'ImportDeclaration';\n readonly specifiers: readonly Node[];\n readonly importKind: 'value' | 'type';\n}\n\nexport interface ImportSpecifier extends Node {\n readonly type: 'ImportSpecifier';\n readonly local: Identifier;\n readonly importKind: 'value' | 'type';\n}\n\nconst guard =\n <T extends Node>(type: T['type']) =>\n (node: Node | null | undefined): node is T =>\n node?.type === type;\n\nexport const isIdentifier = guard<Identifier>('Identifier');\nexport const isMethodDefinition = guard<MethodDefinition>('MethodDefinition');\nexport const isTypeReference = guard<TSTypeReference>('TSTypeReference');\nexport const isImportDeclaration =\n guard<ImportDeclaration>('ImportDeclaration');\nexport const isImportSpecifier = guard<ImportSpecifier>('ImportSpecifier');\nexport const isParameterProperty = guard<TSParameterProperty>(\n 'TSParameterProperty',\n);\n\n/**\n * Declarations only. A `ClassExpression`'s own name is bound inside the class\n * body, not outside it, so a statement appended after `const X = class Foo {}`\n * could not reference `Foo` - it would be a ReferenceError at load.\n */\nexport const isClassDeclaration = (node: Node): node is ClassNode =>\n node.type === 'ClassDeclaration';\n\n/**\n * Depth-first over every object in the tree. oxc's ESTree output is a plain\n * object graph with no parent links, so recursing every own value is safe and\n * needs no visitor-key table.\n */\nexport const walk = (root: unknown, visit: (node: Node) => void): void => {\n if (Array.isArray(root)) {\n for (const child of root) walk(child, visit);\n return;\n }\n if (root === null || typeof root !== 'object') return;\n\n const candidate = root as Partial<Node>;\n if (\n typeof candidate.type === 'string' &&\n typeof candidate.start === 'number' &&\n typeof candidate.end === 'number'\n ) {\n visit(root as Node);\n }\n\n for (const child of Object.values(root)) walk(child, visit);\n};\n\nexport const nameOf = (node: Node | null | undefined): string | undefined =>\n isIdentifier(node) ? node.name : undefined;\n",
|
|
8
|
-
"export interface Edit {\n readonly start: number;\n readonly end: number;\n readonly text: string;\n}\n\n/**\n * Splices edits into the original text rather than reprinting the AST, so\n * everything untouched keeps its exact bytes - comments, formatting, and the\n * line numbers stack traces point at.\n */\nexport const applyEdits = (source: string, edits: readonly Edit[]): string => {\n const sorted = [...edits].sort(\n (left, right) => left.start - right.start || left.end - right.end,\n );\n\n let out = '';\n let cursor = 0;\n\n for (const edit of sorted) {\n if (edit.start < cursor) {\n throw new Error(\n `Overlapping edit at ${edit.start}..${edit.end}; the previous edit ended ` +\n `at ${cursor}.`,\n );\n }\n out += source.slice(cursor, edit.start) + edit.text;\n cursor = edit.end;\n }\n\n return out + source.slice(cursor);\n};\n",
|
|
9
|
-
"import {\n isImportDeclaration,\n isImportSpecifier,\n nameOf,\n walk,\n type ClassNode,\n type Node,\n} from './ast.js';\n\n/**\n * Names that exist only in the type system, so emitting them in a value position\n * would be a `ReferenceError` at runtime. Collected per file: type-only imports,\n * inline `type` specifiers, local interfaces and type aliases.\n */\nexport const collectTypeOnlyNames = (program: Node): ReadonlySet<string> => {\n const names = new Set<string>();\n\n walk(program, (node) => {\n if (isImportDeclaration(node)) {\n for (const specifier of node.specifiers) {\n if (!isImportSpecifier(specifier)) continue;\n if (node.importKind === 'type' || specifier.importKind === 'type') {\n names.add(specifier.local.name);\n }\n }\n return;\n }\n\n if (\n node.type === 'TSInterfaceDeclaration' ||\n node.type === 'TSTypeAliasDeclaration'\n ) {\n const declared = (node as { id?: Node }).id;\n const name = nameOf(declared);\n if (name !== undefined) names.add(name);\n }\n });\n\n return names;\n};\n\n/** A class's own type parameters are erased, so `T` is never a usable token. */\nexport const collectTypeParameters = (\n klass: ClassNode,\n): ReadonlySet<string> => {\n const names = new Set<string>();\n if (klass.typeParameters === null) return names;\n\n walk(klass.typeParameters, (node) => {\n if (node.type !== 'TSTypeParameter') return;\n const name = nameOf((node as { name?: Node }).name);\n if (name !== undefined) names.add(name);\n });\n\n return names;\n};\n\n/** Every name in this file that a value position cannot use. */\nexport const erasedNames = (\n typeOnly: ReadonlySet<string>,\n klass: ClassNode,\n): ReadonlySet<string> =>\n new Set([...typeOnly, ...collectTypeParameters(klass)]);\n",
|
|
10
|
-
"import type { BunPlugin } from 'bun';\nimport { transform } from './deps.js';\n\n/**\n * Rewrites TypeScript as it is loaded so the container can read constructor\n * dependencies. Usable in three places, all the same object:\n *\n * - `bunfig.toml` -> `preload = [\"@dunx/transform/preload\"]` for `bun run`\n * - `Bun.build({ plugins: [depsPlugin] })` for a production build\n * - `Bun.plugin(depsPlugin)` from a test preload\n *\n * Dependencies are skipped: a published package was already transformed by its\n * own build, and re-parsing `node_modules` on every load is pure cost.\n */\nexport const depsPlugin: BunPlugin = {\n name: 'dunx-deps',\n setup(build) {\n // A runtime plugin's onLoad must always return a result - there is no\n // \"decline and fall through\", so untransformed files are handed back as-is.\n build.onLoad({ filter: /\\.tsx?$/ }, async ({ path }) => {\n const source = await Bun.file(path).text();\n const loader = path.endsWith('.tsx') ? 'tsx' : 'ts';\n\n if (path.includes('/node_modules/')) return { contents: source, loader };\n\n return { contents: transform(source, path).code, loader };\n });\n },\n};\n"
|
|
5
|
+
"import { plugin } from 'bun';\nimport { depsPlugin } from './plugin.js';\n\n// Side-effect entrypoint for `bunfig.toml`:\n// preload = [\"@dunx/transform/preload\"]\n//\n// Awaited, not fire-and-forget: registration has to finish before the entrypoint\n// is loaded, or the first modules through would miss the transform.\nawait plugin(depsPlugin);\n"
|
|
11
6
|
],
|
|
12
|
-
"mappings": "
|
|
13
|
-
"debugId": "
|
|
7
|
+
"mappings": ";;;;;;AAAA;AAQA,MAAM,OAAO,UAAU;",
|
|
8
|
+
"debugId": "3E4DE15278F056A464756E2164756E21",
|
|
14
9
|
"names": []
|
|
15
10
|
}
|