@akanjs/devkit 3.1.0-rc.9 → 3.1.1-rc.0
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/CHANGELOG.md +382 -0
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/akanConfig/akanConfig.test.ts +8 -4
- package/akanConfig/akanConfig.ts +3 -2
- package/applicationBuildRunner.test.ts +77 -2
- package/applicationBuildRunner.ts +28 -5
- package/artifact/implicitRootLayout.test.ts +11 -1
- package/artifact/implicitRootLayout.ts +33 -8
- package/biome.base.json +8 -3
- package/biomeBase.test.ts +78 -0
- package/biomeBase.ts +1 -1
- package/codeAgent/agent/AkanCodeCompaction.ts +49 -0
- package/codeAgent/agent/AkanCodeServices.ts +2 -3
- package/codeAgent/agent/CodeAgent.ts +46 -2
- package/codeAgent/agent/CodeAgentRpcHost.ts +3 -0
- package/codeAgent/agent/akanCodeCompaction.test.ts +36 -0
- package/codeAgent/agent/codeAgent.test.ts +61 -3
- package/codeAgent/agent/codeAgentContract.test.ts +1 -0
- package/codeAgent/index.ts +1 -0
- package/codeAgent/skills/akan-code/SKILL.md +4 -0
- package/codeAgent/skills/akan-store/SKILL.md +4 -2
- package/codeAgent/tools/AkanCodePlugins.ts +3 -1
- package/codeAgent/tools/DeepSeekWeb.ts +127 -0
- package/codeAgent/tools/WebToolPack.ts +149 -25
- package/codeAgent/tools/akanCodePlugins.test.ts +191 -0
- package/codeAgent/tools/deepSeekWeb.test.ts +113 -0
- package/commandDecorators/argMeta.ts +1 -0
- package/commandDecorators/command.ts +49 -10
- package/commandDecorators/commandBuilder.ts +6 -3
- package/commandDecorators/commandDecorators.test.ts +169 -6
- package/commandDecorators/helpFormatter.ts +2 -1
- package/commandDecorators/targetMeta.ts +10 -0
- package/executors.test.ts +53 -1
- package/executors.ts +10 -6
- package/frontendBuild/csrArtifactBuilder.test.ts +40 -0
- package/frontendBuild/csrArtifactBuilder.ts +24 -1
- package/frontendBuild/cssCandidateCache.test.ts +16 -0
- package/frontendBuild/cssCandidateCache.ts +8 -6
- package/frontendBuild/dictionaryAssetBuilder.test.ts +77 -0
- package/frontendBuild/dictionaryAssetBuilder.ts +49 -0
- package/frontendBuild/fontPruner.ts +1 -0
- package/frontendBuild/mcpAppViewBuilder.test.ts +170 -0
- package/frontendBuild/mcpAppViewBuilder.ts +245 -0
- package/frontendBuild/precompressArtifacts.test.ts +42 -0
- package/frontendBuild/precompressArtifacts.ts +21 -10
- package/frontendBuild/routeClientBuilder.ts +1 -0
- package/frontendBuild/ssrBaseArtifactBuilder.ts +3 -0
- package/frontendBuild/vendorSpecifiers.ts +1 -0
- package/incrementalBuilder/buildBatch.proc.ts +3 -3
- package/incrementalBuilder/buildBatchProtocol.ts +2 -2
- package/lint/__fixtures__/no-conditional-st-hook/bad.tsx +45 -0
- package/lint/__fixtures__/no-conditional-st-hook/good.tsx +33 -0
- package/lint/no-conditional-st-hook.grit +146 -0
- package/mobile/desktopServerStage.ts +2 -1
- package/mobile/nativeApi.ts +1 -1
- package/mobile/nativeApp.ts +1 -1
- package/mobile/nativeConfig.ts +1 -1
- package/mobile/updatePackFolder.ts +1 -1
- package/mobile/updateUploader.ts +1 -1
- package/package.json +15 -15
- package/routeSourceValidator.test.ts +19 -4
- package/routeSourceValidator.ts +62 -14
- package/scanInfo.ts +36 -3
- package/subspace.test.ts +3 -2
- package/subspaceClone.ts +1 -12
- package/transforms/barrelImportsPlugin.ts +4 -2
- package/transforms/mcpAppPageTrimmer.test.ts +89 -0
- package/transforms/mcpAppPageTrimmer.ts +70 -0
- package/transforms/pureLazyMarker.test.ts +58 -0
- package/transforms/pureLazyMarker.ts +42 -0
- package/transforms/sideEffectImportOrder.test.ts +183 -0
- package/transforms/sideEffectImportOrder.ts +109 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { tempDirs, writeText } from "../testHelpers";
|
|
4
|
+
import { PureLazyMarker } from "./pureLazyMarker";
|
|
5
|
+
|
|
6
|
+
const makeTempRoot = tempDirs("akan-pure-lazy-");
|
|
7
|
+
|
|
8
|
+
describe("PureLazyMarker", () => {
|
|
9
|
+
test("marks each lazy() a declaration holds, and nothing else", () => {
|
|
10
|
+
const source = [
|
|
11
|
+
'import { lazy } from "akanjs/webkit";',
|
|
12
|
+
'export const Globe = lazy(() => import("./Globe_Dynamic"), { ssr: false });',
|
|
13
|
+
"const Editor: Component = lazy(() =>",
|
|
14
|
+
' import("./Editor"),',
|
|
15
|
+
");",
|
|
16
|
+
'const sample = `const Chart = lazy(() => import("./Chart"));`;',
|
|
17
|
+
'register(lazy(() => import("./Side")));',
|
|
18
|
+
].join("\n");
|
|
19
|
+
expect(PureLazyMarker.mark(source, "ui.tsx")).toBe(
|
|
20
|
+
[
|
|
21
|
+
'import { lazy } from "akanjs/webkit";',
|
|
22
|
+
'export const Globe = /* @__PURE__ */ lazy(() => import("./Globe_Dynamic"), { ssr: false });',
|
|
23
|
+
"const Editor: Component = /* @__PURE__ */ lazy(() =>",
|
|
24
|
+
' import("./Editor"),',
|
|
25
|
+
");",
|
|
26
|
+
'const sample = `const Chart = lazy(() => import("./Chart"));`;',
|
|
27
|
+
'register(lazy(() => import("./Side")));',
|
|
28
|
+
].join("\n"),
|
|
29
|
+
);
|
|
30
|
+
const plain = "export const one = 1;";
|
|
31
|
+
expect(PureLazyMarker.mark(plain, "plain.ts")).toBe(plain);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("lets the bundler drop a barrel's lazy component that nothing renders, with what it imports", async () => {
|
|
35
|
+
const root = await makeTempRoot();
|
|
36
|
+
await writeText(path.join(root, "lazy.ts"), "export const lazy = (load: () => Promise<unknown>) => ({ load });\n");
|
|
37
|
+
await writeText(path.join(root, "Heavy.ts"), 'export default "a heavy chunk";\n');
|
|
38
|
+
await writeText(
|
|
39
|
+
path.join(root, "ui.ts"),
|
|
40
|
+
[
|
|
41
|
+
'import { lazy } from "./lazy";',
|
|
42
|
+
'export const Heavy = lazy(() => import("./Heavy"));',
|
|
43
|
+
'export const Card = "a card";',
|
|
44
|
+
].join("\n"),
|
|
45
|
+
);
|
|
46
|
+
await writeText(path.join(root, "entry.ts"), 'import { Card } from "./ui";\nconsole.info(Card);\n');
|
|
47
|
+
const result = await Bun.build({
|
|
48
|
+
entrypoints: [path.join(root, "entry.ts")],
|
|
49
|
+
plugins: [PureLazyMarker.plugin()],
|
|
50
|
+
splitting: false,
|
|
51
|
+
});
|
|
52
|
+
const [output] = result.outputs;
|
|
53
|
+
expect(result.success).toBe(true);
|
|
54
|
+
const text = (await output?.text()) ?? "";
|
|
55
|
+
expect(text).toContain("a card");
|
|
56
|
+
expect(text).not.toContain("a heavy chunk");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { BunPlugin } from "bun";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
|
|
4
|
+
//* A view inlines every module its loads reach, so a `lazy()` component that nothing renders (one of a barrel's, or
|
|
5
|
+
//* of an `_overrides.tsx` manifest's) would still carry its chunk. `lazy()` only builds a component, so its call is
|
|
6
|
+
//* marked pure, and the bundler drops it, chunk and all, wherever its result goes unread.
|
|
7
|
+
export class PureLazyMarker {
|
|
8
|
+
static plugin(): BunPlugin {
|
|
9
|
+
return {
|
|
10
|
+
name: "akan-pure-lazy",
|
|
11
|
+
setup(build) {
|
|
12
|
+
build.onLoad({ filter: /\.tsx?$/ }, async (args) => {
|
|
13
|
+
const file = Bun.file(args.path);
|
|
14
|
+
//? A view's own entry lives only in the build's `files`, where the bundler reads it without a plugin.
|
|
15
|
+
if (!(await file.exists())) return undefined;
|
|
16
|
+
const source = await file.text();
|
|
17
|
+
const marked = PureLazyMarker.mark(source, args.path);
|
|
18
|
+
return marked === source
|
|
19
|
+
? undefined
|
|
20
|
+
: { contents: marked, loader: args.path.endsWith(".tsx") ? "tsx" : "ts" };
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static mark(source: string, filePath: string): string {
|
|
27
|
+
if (!source.includes("lazy(")) return source;
|
|
28
|
+
const kind = filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
29
|
+
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, kind);
|
|
30
|
+
const starts: number[] = [];
|
|
31
|
+
const visit = (node: ts.Node) => {
|
|
32
|
+
const call = ts.isVariableDeclaration(node) ? node.initializer : undefined;
|
|
33
|
+
if (call && ts.isCallExpression(call) && ts.isIdentifier(call.expression) && call.expression.text === "lazy")
|
|
34
|
+
starts.push(call.getStart(sourceFile));
|
|
35
|
+
ts.forEachChild(node, visit);
|
|
36
|
+
};
|
|
37
|
+
visit(sourceFile);
|
|
38
|
+
return starts
|
|
39
|
+
.sort((a, b) => b - a)
|
|
40
|
+
.reduce((marked, start) => `${marked.slice(0, start)}/* @__PURE__ */ ${marked.slice(start)}`, source);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { tempDirs, writeText } from "../testHelpers";
|
|
4
|
+
import { SideEffectImportOrder } from "./sideEffectImportOrder";
|
|
5
|
+
|
|
6
|
+
const makeTempRoot = tempDirs("akan-devkit-side-effect-order-");
|
|
7
|
+
|
|
8
|
+
const writePackage = async (root: string, name: string, files: Record<string, string>, manifest = {}) => {
|
|
9
|
+
await writeText(
|
|
10
|
+
path.join(root, "node_modules", name, "package.json"),
|
|
11
|
+
`${JSON.stringify({ name, main: "index.js", ...manifest })}\n`,
|
|
12
|
+
);
|
|
13
|
+
for (const [file, source] of Object.entries(files))
|
|
14
|
+
await writeText(path.join(root, "node_modules", name, file), source);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
//? Prism's shape: a CommonJS core that sets a global, then language files that read it, imported only by `import()`.
|
|
18
|
+
const writeHighlighter = async (root: string) => {
|
|
19
|
+
await writePackage(root, "core", {
|
|
20
|
+
"index.js": "var Core = { langs: [] };\nglobalThis.Core = Core;\nmodule.exports = Core;\n",
|
|
21
|
+
"components/a.js": 'Core.langs.push("a");\n',
|
|
22
|
+
});
|
|
23
|
+
await writePackage(
|
|
24
|
+
root,
|
|
25
|
+
"highlight",
|
|
26
|
+
{
|
|
27
|
+
"index.js": 'import "core";\nimport "core/components/a.js";\nexport const langs = () => globalThis.Core.langs;\n',
|
|
28
|
+
},
|
|
29
|
+
{ type: "module" },
|
|
30
|
+
);
|
|
31
|
+
const entry = path.join(root, "entry.ts");
|
|
32
|
+
await writeText(entry, 'import("highlight").then(({ langs }) => console.info(JSON.stringify(langs())));\n');
|
|
33
|
+
return entry;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const bundle = async (entry: string, plugins = [SideEffectImportOrder.plugin()]) => {
|
|
37
|
+
const result = await Bun.build({ entrypoints: [entry], target: "browser", splitting: false, minify: true, plugins });
|
|
38
|
+
expect(result.success).toBe(true);
|
|
39
|
+
return (await result.outputs[0]?.text()) ?? "";
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const run = async (root: string, code: string) => {
|
|
43
|
+
const outFile = path.join(root, "out.js");
|
|
44
|
+
await Bun.write(outFile, code);
|
|
45
|
+
const proc = Bun.spawn([process.execPath, outFile], { stdout: "pipe", stderr: "pipe" });
|
|
46
|
+
const [stdout, stderr] = await Promise.all([
|
|
47
|
+
new Response(proc.stdout).text(),
|
|
48
|
+
new Response(proc.stderr).text(),
|
|
49
|
+
proc.exited,
|
|
50
|
+
]);
|
|
51
|
+
return { stdout: stdout.trim(), stderr };
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
describe("SideEffectImportOrder", () => {
|
|
55
|
+
test("runs a CommonJS side-effect import before the imports after it in a module only import() reaches", async () => {
|
|
56
|
+
const root = await makeTempRoot();
|
|
57
|
+
|
|
58
|
+
const { stdout, stderr } = await run(root, await bundle(await writeHighlighter(root)));
|
|
59
|
+
|
|
60
|
+
expect(stderr).toBe("");
|
|
61
|
+
expect(stdout).toBe('["a"]');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
//? A shim would keep what the bundler drops: a bare import of a module its package declares free of side effects.
|
|
65
|
+
test("bundles bare imports of ES modules, and of modules a package calls pure, as it would without the plugin", async () => {
|
|
66
|
+
const root = await makeTempRoot();
|
|
67
|
+
await writePackage(
|
|
68
|
+
root,
|
|
69
|
+
"chunks",
|
|
70
|
+
{
|
|
71
|
+
"index.js": 'import "./a.js";\nexport const a = () => globalThis.a;\n',
|
|
72
|
+
"a.js": 'globalThis.a = "chunk";\nexport {};\n',
|
|
73
|
+
},
|
|
74
|
+
{ type: "module" },
|
|
75
|
+
);
|
|
76
|
+
await writePackage(
|
|
77
|
+
root,
|
|
78
|
+
"pure",
|
|
79
|
+
{
|
|
80
|
+
"index.js": 'import "./esm.js";\nimport "./cjs.cjs";\nexport const used = () => "used";\n',
|
|
81
|
+
"esm.js": 'globalThis.esm = "PURE_ESM";\nexport {};\n',
|
|
82
|
+
"cjs.cjs": 'globalThis.cjs = "PURE_CJS";\nmodule.exports = {};\n',
|
|
83
|
+
},
|
|
84
|
+
{ type: "module", sideEffects: false },
|
|
85
|
+
);
|
|
86
|
+
const entry = path.join(root, "entry.ts");
|
|
87
|
+
await writeText(
|
|
88
|
+
entry,
|
|
89
|
+
'import("chunks").then(({ a }) => console.info(a()));\nimport("pure").then(({ used }) => console.info(used()));\n',
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const [plugged, plain] = [await bundle(entry), await bundle(entry, [])];
|
|
93
|
+
|
|
94
|
+
expect(plugged).toBe(plain);
|
|
95
|
+
expect(plugged).not.toContain("PURE_");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("resolves a shimmed import as the bundler resolves the original, browser field included", async () => {
|
|
99
|
+
const root = await makeTempRoot();
|
|
100
|
+
await writePackage(
|
|
101
|
+
root,
|
|
102
|
+
"pick",
|
|
103
|
+
{
|
|
104
|
+
"index.js": 'globalThis.picked = "node";\nmodule.exports = {};\n',
|
|
105
|
+
"browser.js": 'globalThis.picked = "browser";\nmodule.exports = {};\n',
|
|
106
|
+
},
|
|
107
|
+
{ browser: "browser.js" },
|
|
108
|
+
);
|
|
109
|
+
await writePackage(
|
|
110
|
+
root,
|
|
111
|
+
"picker",
|
|
112
|
+
{ "index.js": 'import "pick";\nexport const picked = () => globalThis.picked;\n' },
|
|
113
|
+
{
|
|
114
|
+
type: "module",
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
const entry = path.join(root, "entry.ts");
|
|
118
|
+
await writeText(entry, 'import("picker").then(({ picked }) => console.info(picked()));\n');
|
|
119
|
+
|
|
120
|
+
const { stdout } = await run(root, await bundle(entry));
|
|
121
|
+
|
|
122
|
+
expect(stdout).toBe("browser");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
//? How the CSR shell and every view build: their entry HTML lives only in the build's `files`.
|
|
126
|
+
test("leaves an entry point a build hands in files to the bundler", async () => {
|
|
127
|
+
const root = await makeTempRoot();
|
|
128
|
+
await writeHighlighter(root);
|
|
129
|
+
const html = path.join(root, "generated/index.html");
|
|
130
|
+
const result = await Bun.build({
|
|
131
|
+
entrypoints: [html],
|
|
132
|
+
files: {
|
|
133
|
+
[html]: '<!doctype html><html><body><script type="module" src="./boot.ts"></script></body></html>\n',
|
|
134
|
+
[path.join(root, "generated/boot.ts")]:
|
|
135
|
+
'import("highlight").then(({ langs }) => console.info(langs().join()));\n',
|
|
136
|
+
},
|
|
137
|
+
target: "browser",
|
|
138
|
+
splitting: false,
|
|
139
|
+
outdir: path.join(root, "out"),
|
|
140
|
+
plugins: [SideEffectImportOrder.plugin()],
|
|
141
|
+
});
|
|
142
|
+
expect(result.success).toBe(true);
|
|
143
|
+
const script = result.outputs.find((output) => output.path.endsWith(".js"));
|
|
144
|
+
|
|
145
|
+
const { stdout, stderr } = await run(root, (await script?.text()) ?? "");
|
|
146
|
+
|
|
147
|
+
expect(stderr).toBe("");
|
|
148
|
+
expect(stdout).toBe("a");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
//? An OTA release is keyed on the CSR bundle's hash, and a shim is named after its importer's absolute path.
|
|
152
|
+
test("bundles a shimmed import identically from two checkout paths", async () => {
|
|
153
|
+
const [first, second] = [await makeTempRoot(), await makeTempRoot()];
|
|
154
|
+
|
|
155
|
+
const [firstOutput, secondOutput] = [
|
|
156
|
+
await bundle(await writeHighlighter(first)),
|
|
157
|
+
await bundle(await writeHighlighter(second)),
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
expect(firstOutput).toBe(secondOutput);
|
|
161
|
+
expect(firstOutput).not.toContain(path.basename(first));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("points only a module's bare imports at their shims", async () => {
|
|
165
|
+
const root = await makeTempRoot();
|
|
166
|
+
await writePackage(root, "mod", {}, { type: "module" });
|
|
167
|
+
const source = [
|
|
168
|
+
'import "a";',
|
|
169
|
+
'import"b";import { c } from "c";',
|
|
170
|
+
'export * from "d";',
|
|
171
|
+
'import "e" with { type: "json" };',
|
|
172
|
+
'const text = `import "f";`;',
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
const rewritten = await SideEffectImportOrder.rewrite(source.join("\n"), path.join(root, "node_modules/mod/x.js"));
|
|
176
|
+
|
|
177
|
+
expect(rewritten).toBe(
|
|
178
|
+
['import "akan-side-effect:a";', 'import"akan-side-effect:b";import { c } from "c";', ...source.slice(2)].join(
|
|
179
|
+
"\n",
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { BunPlugin } from "bun";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
|
|
6
|
+
//* Without splitting, Bun (1.4.4 included) wraps a module that only an `import()` reaches, and the wrapper runs its ESM
|
|
7
|
+
//* imports before its CommonJS ones whatever their order: `@lexical/code-prism` imports `prismjs`, then language files
|
|
8
|
+
//* that read the `Prism` global it sets, so a single-file bundle threw. A vendor module's bare import that may run late
|
|
9
|
+
//* is pointed at a shim importing the same specifier instead: an ESM import, so it keeps its place.
|
|
10
|
+
export class SideEffectImportOrder {
|
|
11
|
+
static readonly #prefix = "akan-side-effect:";
|
|
12
|
+
static readonly #vendorModule = /[\\/]node_modules[\\/].+\.m?js$/;
|
|
13
|
+
static readonly #shim = /\.akan-side-effect$/;
|
|
14
|
+
|
|
15
|
+
static plugin(): BunPlugin {
|
|
16
|
+
return {
|
|
17
|
+
name: "akan-side-effect-order",
|
|
18
|
+
setup(build) {
|
|
19
|
+
const specifiers = new Map<string, string>();
|
|
20
|
+
//? Listed after the `__dirname` plugin: Bun runs one onLoad per module, so a module that one rewrites stays as is.
|
|
21
|
+
build.onLoad({ filter: SideEffectImportOrder.#vendorModule }, async ({ path: modulePath }) => {
|
|
22
|
+
const source = await Bun.file(modulePath).text();
|
|
23
|
+
const rewritten = await SideEffectImportOrder.rewrite(source, modulePath);
|
|
24
|
+
return rewritten === source ? undefined : { contents: rewritten, loader: "js" };
|
|
25
|
+
});
|
|
26
|
+
//? The prefix alone: an onResolve that matches an import loses its package's `sideEffects`, even unanswered.
|
|
27
|
+
build.onResolve({ filter: /^akan-side-effect:/ }, ({ path: prefixed, importer }) => {
|
|
28
|
+
const specifier = prefixed.slice(SideEffectImportOrder.#prefix.length);
|
|
29
|
+
//? Beside its importer, so the bundler resolves the shim's import as it would have the importer's.
|
|
30
|
+
const shimPath = `${importer}.${Bun.hash(specifier).toString(36)}.akan-side-effect`;
|
|
31
|
+
specifiers.set(shimPath, specifier);
|
|
32
|
+
return { path: shimPath };
|
|
33
|
+
});
|
|
34
|
+
build.onLoad({ filter: SideEffectImportOrder.#shim }, ({ path: shimPath }) => ({
|
|
35
|
+
contents: `import ${JSON.stringify(specifiers.get(shimPath))};\n`,
|
|
36
|
+
loader: "js",
|
|
37
|
+
}));
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static async rewrite(source: string, filePath: string): Promise<string> {
|
|
43
|
+
if (!/\bimport\s*["']/.test(source)) return source;
|
|
44
|
+
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, false, ts.ScriptKind.JS);
|
|
45
|
+
const literals = sourceFile.statements.flatMap((statement) =>
|
|
46
|
+
ts.isImportDeclaration(statement) &&
|
|
47
|
+
!statement.importClause &&
|
|
48
|
+
!statement.attributes &&
|
|
49
|
+
ts.isStringLiteral(statement.moduleSpecifier)
|
|
50
|
+
? [statement.moduleSpecifier]
|
|
51
|
+
: [],
|
|
52
|
+
);
|
|
53
|
+
const late = await Promise.all(literals.map((literal) => SideEffectImportOrder.#runsLate(literal.text, filePath)));
|
|
54
|
+
return literals
|
|
55
|
+
.filter((_, idx) => late[idx])
|
|
56
|
+
.reverse()
|
|
57
|
+
.reduce(
|
|
58
|
+
(rewritten, literal) =>
|
|
59
|
+
`${rewritten.slice(0, literal.getStart(sourceFile))}${JSON.stringify(`${SideEffectImportOrder.#prefix}${literal.text}`)}${rewritten.slice(literal.end)}`,
|
|
60
|
+
source,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
//? Judged off the specifier, never resolved: a `Bun.resolveSync` primes the bundler's resolution, browser field lost.
|
|
65
|
+
static async #runsLate(specifier: string, importer: string) {
|
|
66
|
+
const from = path.dirname(importer);
|
|
67
|
+
const target = specifier.startsWith(".") ? path.resolve(from, specifier) : null;
|
|
68
|
+
//? An ESM import already keeps its place among the others.
|
|
69
|
+
if (target && (await SideEffectImportOrder.#isEsModule(target))) return false;
|
|
70
|
+
const sideEffects = await SideEffectImportOrder.#sideEffectsOf(
|
|
71
|
+
target ? path.dirname(target) : SideEffectImportOrder.#packageDirOf(specifier, from),
|
|
72
|
+
);
|
|
73
|
+
//? The bundler drops a bare import of a module its package declares pure, which a shim would keep.
|
|
74
|
+
return sideEffects === undefined || sideEffects === true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static async #isEsModule(filePath: string) {
|
|
78
|
+
const file = Bun.file(filePath);
|
|
79
|
+
if (!/\.m?js$/.test(filePath) || !(await file.exists())) return false;
|
|
80
|
+
const sourceFile = ts.createSourceFile(
|
|
81
|
+
filePath,
|
|
82
|
+
await file.text(),
|
|
83
|
+
ts.ScriptTarget.Latest,
|
|
84
|
+
false,
|
|
85
|
+
ts.ScriptKind.JS,
|
|
86
|
+
);
|
|
87
|
+
return ts.isExternalModule(sourceFile);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static #packageDirOf(specifier: string, from: string) {
|
|
91
|
+
const name = specifier
|
|
92
|
+
.split("/")
|
|
93
|
+
.slice(0, specifier.startsWith("@") ? 2 : 1)
|
|
94
|
+
.join("/");
|
|
95
|
+
for (let dir = from; ; dir = path.dirname(dir)) {
|
|
96
|
+
const packageDir = path.join(dir, "node_modules", name);
|
|
97
|
+
if (existsSync(packageDir)) return packageDir;
|
|
98
|
+
if (path.dirname(dir) === dir) return from;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
static async #sideEffectsOf(fromDir: string): Promise<unknown> {
|
|
103
|
+
for (let dir = fromDir; ; dir = path.dirname(dir)) {
|
|
104
|
+
const manifest = Bun.file(path.join(dir, "package.json"));
|
|
105
|
+
if (await manifest.exists()) return ((await manifest.json()) as { sideEffects?: unknown }).sideEffects;
|
|
106
|
+
if (path.dirname(dir) === dir) return undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|