@akanjs/devkit 3.2.0-rc.1 → 3.2.0-rc.10
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 +233 -11
- package/agentAssets.ts +1 -1
- package/akanApp/akanApp.host.test.ts +127 -0
- package/akanApp/akanApp.host.ts +50 -57
- package/akanApp/devHostPolicy.test.ts +11 -11
- package/akanApp/devHostPolicy.ts +2 -4
- package/akanContext.ts +2 -5
- package/akanMcpContract.ts +21 -10
- package/akanMcpToolError.ts +16 -0
- package/apiReport/apiDeclarationPrinter.ts +120 -0
- package/apiReport/apiReport.test.ts +497 -0
- package/apiReport/apiReport.ts +232 -0
- package/apiReport/apiReportDiff.ts +126 -0
- package/apiReport/apiReportPolicy.ts +65 -0
- package/apiReport/apiTargets.ts +311 -0
- package/apiReport/changesetBump.ts +19 -0
- package/applicationBuildReporter.test.ts +21 -0
- package/applicationBuildReporter.ts +8 -0
- package/applicationBuildRunner.test.ts +67 -0
- package/applicationBuildRunner.ts +6 -1
- package/artifact/implicitRootLayout.test.ts +4 -0
- package/artifact/implicitRootLayout.ts +13 -1
- package/artifact/storeBootstrap.ts +3 -2
- package/bareErrorScanner.ts +126 -0
- package/biome.base.json +2 -1
- package/codeAgent/skills/akan-debug/SKILL.md +7 -3
- package/codeAgent/skills/akan-err/SKILL.md +7 -0
- package/codeAgent/skills/akan-mcp/SKILL.md +7 -1
- package/codeAgent/skills/akan-scaffold/SKILL.md +3 -2
- package/codeAgent/skills/akan-transport/SKILL.md +2 -0
- package/codeAgent/skills/akan-validate/SKILL.md +1 -0
- package/codeAgent/tools/McpClient.ts +28 -6
- package/codeAgent/tools/McpToolPack.ts +1 -1
- package/codeAgent/tools/akanCodePlugins.test.ts +13 -1
- package/commandDecorators/argMeta.ts +4 -0
- package/commandDecorators/cliError.ts +72 -0
- package/commandDecorators/command.ts +186 -96
- package/commandDecorators/commandDecorators.test.ts +240 -17
- package/commandDecorators/exitCodeError.ts +6 -7
- package/commandDecorators/index.ts +1 -0
- package/commandDecorators/targetMeta.ts +3 -0
- package/conventionSet.test.ts +5 -2
- package/csrE2e/csrE2eHarness.fixture.ts +1 -1
- package/errorCodeReference.test.ts +282 -0
- package/errorCodeReference.ts +71 -0
- package/errorCodeScan.ts +551 -0
- package/frontendBuild/allRoutesBuilder.test.ts +16 -4
- package/frontendBuild/allRoutesBuilder.ts +24 -6
- package/frontendBuild/buildRouteClient.test.ts +43 -0
- package/frontendBuild/bundleDefine.ts +2 -0
- package/frontendBuild/clientBuildTypes.ts +5 -2
- package/frontendBuild/clientEntryDiscovery.test.ts +80 -0
- package/frontendBuild/clientEntryDiscovery.ts +53 -9
- package/frontendBuild/csrArtifactBuilder.test.ts +48 -0
- package/frontendBuild/csrArtifactBuilder.ts +9 -3
- package/frontendBuild/csrDevBundler.ts +12 -2
- package/frontendBuild/csrDevModuleCompiler.test.ts +97 -0
- package/frontendBuild/csrDevModuleCompiler.ts +37 -8
- package/frontendBuild/csrDevPatcher.test.ts +89 -2
- package/frontendBuild/csrDevPatcher.ts +47 -7
- package/frontendBuild/csrDevTypes.ts +16 -0
- package/frontendBuild/cssCompiler.test.ts +63 -1
- package/frontendBuild/devBarrelImports.ts +132 -0
- package/frontendBuild/devRegistryBundler.ts +19 -4
- package/frontendBuild/mcpAppViewBuilder.test.ts +67 -0
- package/frontendBuild/nativeCore.test.ts +148 -0
- package/frontendBuild/precompressArtifacts.ts +1 -1
- package/frontendBuild/ssrBaseArtifactBuilder.test.ts +26 -0
- package/frontendBuild/ssrBaseArtifactBuilder.ts +23 -19
- package/frontendBuild/ssrDevBundler.ts +13 -2
- package/frontendBuild/vendorImportScanner.test.ts +24 -3
- package/frontendBuild/vendorImportScanner.ts +25 -10
- package/incrementalBuilder/incrementalBuilder.host.test.ts +30 -1
- package/incrementalBuilder/incrementalBuilder.host.ts +10 -22
- package/lint/__fixtures__/no-import-common-err/bad.tsx +13 -0
- package/lint/__fixtures__/no-import-common-err/good.tsx +12 -0
- package/lint/no-import-common-err.grit +31 -0
- package/lint/no-init-fetch-in-client.grit +1 -1
- package/lint/no-web-only-api-outside-webkit.grit +3 -3
- package/maintainabilityBaseline.test.ts +49 -0
- package/maintainabilityBaseline.ts +73 -8
- package/maintainabilityContract.test.ts +302 -1
- package/maintainabilityContract.ts +87 -22
- package/maintainabilityExceptionScanner.test.ts +14 -0
- package/maintainabilityExceptionScanner.ts +5 -1
- package/maintainabilityLimits.ts +12 -0
- package/maintainabilityReport.ts +20 -3
- package/moduleFieldNames.test.ts +56 -0
- package/moduleFieldNames.ts +25 -0
- package/package.json +3 -3
- package/preview/PreviewView.ts +32 -11
- package/preview/previewView.test.ts +76 -9
- package/routeJsBudget.test.ts +47 -11
- package/routeJsBudget.ts +88 -25
- package/transforms/barrelAnalyzer.ts +46 -1
- package/transforms/barrelImportsPlugin.ts +61 -45
- package/transforms/transforms.test.ts +67 -1
- package/typecheck/projectTypecheck.ts +5 -1
- package/typecheck/typecheck.proc.ts +3 -2
- package/typecheck/typecheckFailure.ts +6 -1
- package/workflow/artifacts.ts +25 -7
- package/workflow/fieldType.ts +92 -0
- package/workflow/index.ts +2 -0
- package/workflow/plainName.ts +28 -0
- package/workflow/plan.ts +67 -22
- package/workflow/source.ts +0 -5
- package/workspaceGraph.test.ts +200 -3
- package/workspaceGraph.ts +46 -8
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import type { BunPlugin } from "bun";
|
|
3
3
|
import type { App } from "../commandDecorators";
|
|
4
|
+
import { loaderFor } from "../transforms/moduleSyntax";
|
|
4
5
|
import { CsrDevFactoryCheck } from "./csrDevFactoryCheck";
|
|
5
6
|
import { CsrDevPaths } from "./csrDevPaths";
|
|
6
7
|
import { CsrDevResolver } from "./csrDevResolver";
|
|
7
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
CsrDevBarrelStamp,
|
|
10
|
+
CsrDevCompiledModule,
|
|
11
|
+
CsrDevContext,
|
|
12
|
+
CsrDevNarrowedImport,
|
|
13
|
+
CsrDevSharedHelpers,
|
|
14
|
+
} from "./csrDevTypes";
|
|
15
|
+
import { DevBarrelImports } from "./devBarrelImports";
|
|
8
16
|
import { RouteClientBuilder } from "./routeClientBuilder";
|
|
9
17
|
|
|
10
18
|
interface OutputMetafile {
|
|
@@ -38,6 +46,7 @@ export interface CsrDevCompileResult {
|
|
|
38
46
|
modules: CsrDevCompiledModule[];
|
|
39
47
|
refusedVendors: string[];
|
|
40
48
|
unresolved: string[];
|
|
49
|
+
barrels?: Record<string, CsrDevBarrelStamp>;
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
//* Every module becomes its own CJS factory: one Bun.build with each import external, so a save re-runs only the
|
|
@@ -58,6 +67,8 @@ export class CsrDevModuleCompiler {
|
|
|
58
67
|
readonly #outDir: string;
|
|
59
68
|
readonly #routePrefix: string;
|
|
60
69
|
readonly #assets = new Set<string>();
|
|
70
|
+
#barrels: Promise<DevBarrelImports | null> | null = null;
|
|
71
|
+
readonly #narrowed = new Map<string, Record<string, CsrDevNarrowedImport>>();
|
|
61
72
|
|
|
62
73
|
constructor({ app, paths, resolver, context, outDir, routePrefix }: CsrDevModuleCompilerOptions) {
|
|
63
74
|
this.#app = app;
|
|
@@ -109,7 +120,14 @@ export class CsrDevModuleCompiler {
|
|
|
109
120
|
this.#app.verbose(
|
|
110
121
|
`[csr-dev] ${fallbacks.size} import(s) outside the browser build's graph resolved with Bun's runtime resolver: ${[...fallbacks].slice(0, 5).join(", ")}${fallbacks.size > 5 ? ", ..." : ""}`,
|
|
111
122
|
);
|
|
112
|
-
|
|
123
|
+
const modules = [...compiled.values()];
|
|
124
|
+
return { modules, refusedVendors: [], unresolved: [], barrels: await this.#barrelStamps(modules) };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async #barrelStamps(modules: CsrDevCompiledModule[]): Promise<Record<string, CsrDevBarrelStamp>> {
|
|
128
|
+
const barrels = new Set(modules.flatMap((module) => Object.keys(module.narrowed ?? {})));
|
|
129
|
+
const barrelImports = barrels.size > 0 ? await this.#barrelImports() : null;
|
|
130
|
+
return barrelImports ? await barrelImports.stamps(barrels, (file) => this.#paths.idOf(file)) : {};
|
|
113
131
|
}
|
|
114
132
|
|
|
115
133
|
async #compileRound(files: string[]): Promise<Omit<BuildRound, "tangled">> {
|
|
@@ -197,6 +215,7 @@ export class CsrDevModuleCompiler {
|
|
|
197
215
|
continue;
|
|
198
216
|
}
|
|
199
217
|
const sourceMap = sourceMaps.get(`${artifact.path}.map`);
|
|
218
|
+
const narrowed = this.#narrowed.get(file);
|
|
200
219
|
modules.push({
|
|
201
220
|
id,
|
|
202
221
|
file,
|
|
@@ -206,6 +225,7 @@ export class CsrDevModuleCompiler {
|
|
|
206
225
|
deps: this.#emittedDeps(code),
|
|
207
226
|
mtimeMs: mtimes.get(file) ?? CsrDevPaths.mtimeOf(file),
|
|
208
227
|
hash: hashes.get(file) ?? "",
|
|
228
|
+
...(narrowed && Object.keys(narrowed).length > 0 ? { narrowed } : {}),
|
|
209
229
|
});
|
|
210
230
|
}
|
|
211
231
|
return { modules, misses, tangled };
|
|
@@ -302,13 +322,17 @@ export class CsrDevModuleCompiler {
|
|
|
302
322
|
});
|
|
303
323
|
}
|
|
304
324
|
if (!vendor)
|
|
305
|
-
build.onLoad({ filter: /\.tsx$/ }, async (args) => {
|
|
325
|
+
build.onLoad({ filter: /\.(?:tsx?|jsx?)$/ }, async (args) => {
|
|
306
326
|
if (CsrDevPaths.isVendorFile(args.path)) return undefined;
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
327
|
+
const source = await Bun.file(args.path).text();
|
|
328
|
+
const rewrite = await (await this.#barrelImports())?.rewrite(source, args.path);
|
|
329
|
+
if (rewrite) this.#narrowed.set(CsrDevPaths.realpath(args.path), rewrite.narrowed);
|
|
330
|
+
const narrowed = rewrite?.contents ?? source;
|
|
331
|
+
const normalized = args.path.endsWith(".tsx")
|
|
332
|
+
? RouteClientBuilder.normalizeNamedDefaultFunctionForFastRefresh(narrowed, { path: args.path })
|
|
333
|
+
: null;
|
|
334
|
+
const contents = normalized ?? narrowed;
|
|
335
|
+
return contents === source ? undefined : { contents, loader: loaderFor(args.path) };
|
|
312
336
|
});
|
|
313
337
|
build.onResolve({ filter: /.*/ }, (args) => {
|
|
314
338
|
if (args.kind === "entry-point-build" || !args.importer) return undefined;
|
|
@@ -334,6 +358,11 @@ export class CsrDevModuleCompiler {
|
|
|
334
358
|
};
|
|
335
359
|
}
|
|
336
360
|
|
|
361
|
+
#barrelImports(): Promise<DevBarrelImports | null> {
|
|
362
|
+
this.#barrels ??= DevBarrelImports.create(this.#app, this.#context.barrelImports ?? []);
|
|
363
|
+
return this.#barrels;
|
|
364
|
+
}
|
|
365
|
+
|
|
337
366
|
//? Bun writes sources relative to the working directory; the page shows them by workspace path instead. A package
|
|
338
367
|
//? barrel inlined into its importer (react-icons) would carry its whole text into every importer's map.
|
|
339
368
|
#rebaseSourceMap(text: string): string {
|
|
@@ -119,12 +119,25 @@ describe("CsrDevPatcher", () => {
|
|
|
119
119
|
workspace: { workspaceRoot: root },
|
|
120
120
|
cwdPath: path.join(root, "apps/demo"),
|
|
121
121
|
verbose: () => undefined,
|
|
122
|
+
getTsConfig: async () => ({ compilerOptions: { paths: tsPaths } }),
|
|
122
123
|
};
|
|
123
124
|
super(app as unknown as App, { dirName: "registry", routePrefix: "/_akan/registry/", library: true });
|
|
124
125
|
this.#entry = entry;
|
|
125
126
|
}
|
|
127
|
+
barrels: string[] = [];
|
|
126
128
|
async context() {
|
|
127
|
-
return context;
|
|
129
|
+
return { ...context, barrelImports: this.barrels };
|
|
130
|
+
}
|
|
131
|
+
fingerprints = 0;
|
|
132
|
+
override async barrelImports(barrelContext: CsrDevContext) {
|
|
133
|
+
const barrelImports = await super.barrelImports(barrelContext);
|
|
134
|
+
if (!barrelImports) return null;
|
|
135
|
+
const fingerprint = barrelImports.fingerprint.bind(barrelImports);
|
|
136
|
+
barrelImports.fingerprint = async (barrel, names) => {
|
|
137
|
+
this.fingerprints += 1;
|
|
138
|
+
return await fingerprint(barrel, names);
|
|
139
|
+
};
|
|
140
|
+
return barrelImports;
|
|
128
141
|
}
|
|
129
142
|
async writeEntries() {
|
|
130
143
|
return { files: { "": this.#entry }, changed: [] };
|
|
@@ -145,13 +158,22 @@ describe("CsrDevPatcher", () => {
|
|
|
145
158
|
}
|
|
146
159
|
}
|
|
147
160
|
|
|
148
|
-
const
|
|
161
|
+
const tsPaths = { "@apps/*": ["./apps/*"] };
|
|
162
|
+
const movedRegistry = async (
|
|
163
|
+
files: Record<string, string>,
|
|
164
|
+
{ roots = [], barrels = [] }: { roots?: string[]; barrels?: string[] } = {},
|
|
165
|
+
) => {
|
|
149
166
|
const root = await mkdtemp(path.join(os.tmpdir(), "akan-csr-dev-move-"));
|
|
150
167
|
dirs.push(root);
|
|
151
168
|
const file = (relative: string) => path.join(CsrDevPaths.realpath(root), "apps/demo", relative);
|
|
152
169
|
for (const [relative, text] of Object.entries(files)) await Bun.write(file(relative), text);
|
|
170
|
+
await Bun.write(
|
|
171
|
+
path.join(root, "tsconfig.json"),
|
|
172
|
+
JSON.stringify({ compilerOptions: { baseUrl: ".", paths: tsPaths } }),
|
|
173
|
+
);
|
|
153
174
|
const bundler = new MovedRegistry(root, file("entry.ts"));
|
|
154
175
|
bundler.extraRoots = roots.map((relative) => file(relative));
|
|
176
|
+
bundler.barrels = barrels;
|
|
155
177
|
expect(await bundler.update([])).toMatchObject({ generation: 1, reload: true, first: true });
|
|
156
178
|
const patcher = new CsrDevPatcher(bundler, { resident: true });
|
|
157
179
|
expect(await patcher.update([])).toEqual({ kind: "unchanged" });
|
|
@@ -562,6 +584,71 @@ describe("CsrDevPatcher", () => {
|
|
|
562
584
|
expect(await app()).toContain('__akan.define("apps/demo/ui/Foo.tsx"');
|
|
563
585
|
});
|
|
564
586
|
|
|
587
|
+
test("an export moved to another file of a narrowed barrel compiles the importers of that name, also after a restart", async () => {
|
|
588
|
+
const { bundler, patcher, file, app, changedIds } = await movedRegistry(
|
|
589
|
+
{
|
|
590
|
+
"ui/index.ts": 'export * from "./Foo";\n',
|
|
591
|
+
"ui/Foo.tsx": 'export const Foo = () => "foo";\nexport const Bar = () => "bar";\n',
|
|
592
|
+
"page.ts": 'import { Foo } from "@apps/demo/ui";\nexport const page = Foo;\n',
|
|
593
|
+
"entry.ts":
|
|
594
|
+
'import { Bar } from "@apps/demo/ui";\nimport { page } from "./page";\nexport const run = () => Bar() + page();\n',
|
|
595
|
+
},
|
|
596
|
+
{ barrels: ["@apps/demo/ui"] },
|
|
597
|
+
);
|
|
598
|
+
const factoryOf = (text: string, id: string) =>
|
|
599
|
+
text.slice(text.indexOf(`__akan.define("${id}"`)).split("__akan.define(")[1];
|
|
600
|
+
|
|
601
|
+
await Bun.write(file("ui/Foo.tsx"), 'export const Foo = () => "foo v2";\nexport const Bar = () => "bar";\n');
|
|
602
|
+
expect(changedIds(await patcher.update([file("ui/Foo.tsx")]))).toEqual(["apps/demo/ui/Foo.tsx"]);
|
|
603
|
+
expect(bundler.fingerprints).toBe(0);
|
|
604
|
+
|
|
605
|
+
await Bun.write(file("ui/Foo.tsx"), 'export const Foo = () => "foo";\n');
|
|
606
|
+
await Bun.write(file("ui/Bar.tsx"), 'export const Bar = () => "bar moved";\n');
|
|
607
|
+
await Bun.write(file("ui/index.ts"), 'export * from "./Bar";\nexport * from "./Foo";\n');
|
|
608
|
+
const moved = await patcher.update([file("ui/Foo.tsx"), file("ui/Bar.tsx"), file("ui/index.ts")]);
|
|
609
|
+
expect(changedIds(moved)).toEqual(["apps/demo/entry.ts", "apps/demo/ui/Bar.tsx", "apps/demo/ui/Foo.tsx"]);
|
|
610
|
+
expect(bundler.fingerprints).toBe(2);
|
|
611
|
+
expect(factoryOf(await app(), "apps/demo/entry.ts")).toContain('require("akan-module:apps/demo/ui/Bar.tsx")');
|
|
612
|
+
|
|
613
|
+
patcher.forget();
|
|
614
|
+
await Bun.write(file("ui/Foo.tsx"), 'export const Foo = () => "foo";\nexport const Bar = () => "bar back";\n');
|
|
615
|
+
await Bun.write(file("ui/Bar.tsx"), "export const Gone = 1;\n");
|
|
616
|
+
const restarted = await bundler.update([]);
|
|
617
|
+
expect(restarted?.changedIds.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))).toEqual([
|
|
618
|
+
"apps/demo/entry.ts",
|
|
619
|
+
"apps/demo/ui/Bar.tsx",
|
|
620
|
+
"apps/demo/ui/Foo.tsx",
|
|
621
|
+
]);
|
|
622
|
+
expect(factoryOf(await app(), "apps/demo/entry.ts")).toContain('require("akan-module:apps/demo/ui/Foo.tsx")');
|
|
623
|
+
expect(await new CsrDevPatcher(bundler).update([])).toEqual({ kind: "unchanged" });
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
test("a move whose compile fails leaves the barrel's stamps on disk, so the next save reads it again", async () => {
|
|
627
|
+
const { bundler, file, app, changedIds } = await movedRegistry(
|
|
628
|
+
{
|
|
629
|
+
"ui/index.ts": 'export * from "./Foo";\n',
|
|
630
|
+
"ui/Foo.tsx": 'export const Foo = () => "foo";\nexport const Bar = () => "bar";\n',
|
|
631
|
+
"entry.ts": 'import { Bar } from "@apps/demo/ui";\nexport const run = Bar;\n',
|
|
632
|
+
},
|
|
633
|
+
{ barrels: ["@apps/demo/ui"] },
|
|
634
|
+
);
|
|
635
|
+
const barrelsOnDisk = async () => (await bundler.writer.readJson<CsrDevGraph>("graph.json"))?.barrels;
|
|
636
|
+
const before = await barrelsOnDisk();
|
|
637
|
+
|
|
638
|
+
await Bun.write(file("ui/Foo.tsx"), 'export const Foo = () => "foo";\n');
|
|
639
|
+
await Bun.write(file("ui/Bar.tsx"), 'import "./Missing";\nexport const Bar = () => "bar moved";\n');
|
|
640
|
+
await Bun.write(file("ui/index.ts"), 'export * from "./Bar";\nexport * from "./Foo";\n');
|
|
641
|
+
const saved = [file("ui/Foo.tsx"), file("ui/Bar.tsx"), file("ui/index.ts")];
|
|
642
|
+
await expect(new CsrDevPatcher(bundler).update(saved)).rejects.toThrow();
|
|
643
|
+
expect(await barrelsOnDisk()).toEqual(before);
|
|
644
|
+
|
|
645
|
+
await Bun.write(file("ui/Bar.tsx"), 'export const Bar = () => "bar moved";\n');
|
|
646
|
+
const fixed = await new CsrDevPatcher(bundler).update([file("ui/Bar.tsx")]);
|
|
647
|
+
expect(changedIds(fixed)).toEqual(["apps/demo/entry.ts", "apps/demo/ui/Bar.tsx", "apps/demo/ui/Foo.tsx"]);
|
|
648
|
+
expect(await app()).toContain('"bar moved"');
|
|
649
|
+
expect(await barrelsOnDisk()).not.toEqual(before);
|
|
650
|
+
});
|
|
651
|
+
|
|
565
652
|
test("a registry with no manifest is a first build, which no tab holds a module of", async () => {
|
|
566
653
|
const { bundler, dir } = await makeRegistry("same");
|
|
567
654
|
await rm(path.join(dir, CSR_DEV_MANIFEST_FILE), { force: true });
|
|
@@ -4,7 +4,7 @@ import { appGenerationOf, CSR_DEV_MANIFEST_FILE, type CsrDevManifest } from "aka
|
|
|
4
4
|
import type { CsrDevCompileResult } from "./csrDevModuleCompiler";
|
|
5
5
|
import { CsrDevPaths } from "./csrDevPaths";
|
|
6
6
|
import { CsrDevResolver } from "./csrDevResolver";
|
|
7
|
-
import type { CsrDevCode, CsrDevContext, CsrDevGraph } from "./csrDevTypes";
|
|
7
|
+
import type { CsrDevBarrelStamp, CsrDevCode, CsrDevContext, CsrDevGraph } from "./csrDevTypes";
|
|
8
8
|
import type { CsrDevUpdate, CsrDevUpdateOptions, DevRegistryBundler } from "./devRegistryBundler";
|
|
9
9
|
|
|
10
10
|
interface CsrDevPatcherState {
|
|
@@ -109,6 +109,7 @@ export class CsrDevPatcher {
|
|
|
109
109
|
const present = CsrDevPatcher.#presence(changedFiles);
|
|
110
110
|
const forgotten = this.#forgetDeletedModules(state, present);
|
|
111
111
|
const changed = onlyRoots ? new Set<string>() : await this.#changedModules(graph, changedFiles, present);
|
|
112
|
+
const restamped = onlyRoots ? null : await this.#renarrowed(context, graph, changedFiles, changed);
|
|
112
113
|
for (const id of this.#withoutFactory(state, present)) changed.add(id);
|
|
113
114
|
for (const file of entries.changed) changed.add(paths.idOf(file));
|
|
114
115
|
const rootFiles = [...Object.values(entries.files), ...roots.filter((file) => fs.existsSync(file))];
|
|
@@ -121,6 +122,8 @@ export class CsrDevPatcher {
|
|
|
121
122
|
this.#bundler.reloadsOnEntryChange &&
|
|
122
123
|
(entries.changed.length > 0 || JSON.stringify(entryIds) !== JSON.stringify(graph.entries));
|
|
123
124
|
if (changed.size === 0) {
|
|
125
|
+
//? No importer moved (none compiles), so the restamped barrels already hold.
|
|
126
|
+
if (restamped) graph.barrels = { ...graph.barrels, ...restamped };
|
|
124
127
|
if (carried.length === 0) return { kind: "unchanged" };
|
|
125
128
|
return await this.#retryCarried(context, state, carried, { announce, allowWholeAppBuilds });
|
|
126
129
|
}
|
|
@@ -188,15 +191,11 @@ export class CsrDevPatcher {
|
|
|
188
191
|
forgotten.push(id);
|
|
189
192
|
}
|
|
190
193
|
graph.pending = graph.pending.filter((id) => !orphans.includes(id));
|
|
191
|
-
this.#bundler.merge(graph, resolver, compiled);
|
|
194
|
+
this.#bundler.merge(graph, resolver, compiled, { ...restamped, ...result.barrels });
|
|
192
195
|
if (!this.#resident) delete graph.failed;
|
|
193
196
|
for (const module of compiled) this.#delegated.delete(module.id);
|
|
194
197
|
graph.entries = entryIds;
|
|
195
|
-
|
|
196
|
-
if (module.vendor || !code) continue;
|
|
197
|
-
code.modules.set(module.id, module.factory);
|
|
198
|
-
if (module.helpers) code.helpers.set(module.helpers.hash, module.helpers.definition);
|
|
199
|
-
}
|
|
198
|
+
if (code) CsrDevPatcher.#remember(code, compiled);
|
|
200
199
|
const changedIds = compiled.filter((module) => !module.vendor).map((module) => module.id);
|
|
201
200
|
const constantId = changedIds.find((id) => id.endsWith(".constant.ts"));
|
|
202
201
|
// A model class swapped under live store state would mix old and new instances; reload instead.
|
|
@@ -359,6 +358,7 @@ export class CsrDevPatcher {
|
|
|
359
358
|
modules: [...result.modules, ...more.modules],
|
|
360
359
|
refusedVendors: [...result.refusedVendors, ...more.refusedVendors],
|
|
361
360
|
unresolved: [...result.unresolved, ...more.unresolved],
|
|
361
|
+
barrels: { ...result.barrels, ...more.barrels },
|
|
362
362
|
};
|
|
363
363
|
}
|
|
364
364
|
|
|
@@ -508,6 +508,46 @@ export class CsrDevPatcher {
|
|
|
508
508
|
return changed;
|
|
509
509
|
}
|
|
510
510
|
|
|
511
|
+
//? The new stamps go into the graph with the compile that follows (`merge`), never ahead of it: a round that fails or
|
|
512
|
+
//? is handed back must leave the barrel stale for the next one.
|
|
513
|
+
async #renarrowed(
|
|
514
|
+
context: CsrDevContext,
|
|
515
|
+
graph: CsrDevGraph,
|
|
516
|
+
changedFiles: string[],
|
|
517
|
+
changed: Set<string>,
|
|
518
|
+
): Promise<Record<string, CsrDevBarrelStamp>> {
|
|
519
|
+
const { paths } = this.#bundler;
|
|
520
|
+
const saved = new Set(changedFiles.map((file) => paths.idOf(file)));
|
|
521
|
+
const stale = Object.entries(graph.barrels ?? {})
|
|
522
|
+
.filter(([, { read }]) =>
|
|
523
|
+
Object.entries(read).some(
|
|
524
|
+
([id, mtimeMs]) => saved.has(id) || CsrDevPaths.mtimeOf(paths.fileOf(id)) !== mtimeMs,
|
|
525
|
+
),
|
|
526
|
+
)
|
|
527
|
+
.map(([barrel]) => barrel);
|
|
528
|
+
const barrelImports = stale.length > 0 ? await this.#bundler.barrelImports(context) : null;
|
|
529
|
+
if (!barrelImports) return {};
|
|
530
|
+
const barrels = await barrelImports.stamps(stale, (file) => paths.idOf(file));
|
|
531
|
+
const moved = stale.filter((barrel) => barrels[barrel]?.digest !== graph.barrels?.[barrel]?.digest);
|
|
532
|
+
for (const [id, module] of Object.entries(graph.modules)) {
|
|
533
|
+
for (const barrel of moved) {
|
|
534
|
+
const narrowed = module.narrowed?.[barrel];
|
|
535
|
+
if (!narrowed || (await barrelImports.fingerprint(barrel, narrowed.names)) === narrowed.hash) continue;
|
|
536
|
+
changed.add(id);
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return barrels;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
static #remember(code: CsrDevCode, compiled: CsrDevCompileResult["modules"]) {
|
|
544
|
+
for (const module of compiled) {
|
|
545
|
+
if (module.vendor) continue;
|
|
546
|
+
code.modules.set(module.id, module.factory);
|
|
547
|
+
if (module.helpers) code.helpers.set(module.helpers.hash, module.helpers.definition);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
511
551
|
//? A new file can take over a relative import another module already resolved (`Foo.tsx` created beside
|
|
512
552
|
//? `Foo/index.tsx`), with no change to that module's own text: it compiles again, and resolves against the disk.
|
|
513
553
|
#shadowedImporters(graph: CsrDevGraph, changedFiles: string[]): string[] {
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import type { PageEntry } from "../artifact/implicitRootLayout";
|
|
2
2
|
|
|
3
|
+
export interface CsrDevNarrowedImport {
|
|
4
|
+
names: string[];
|
|
5
|
+
hash: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface CsrDevBarrelStamp {
|
|
9
|
+
/** The mtime of each file and directory the barrel's analysis read, by id. */
|
|
10
|
+
read: Record<string, number>;
|
|
11
|
+
/** Each export name with the file it resolves to, hashed: what every importer's narrowing reads. */
|
|
12
|
+
digest: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
3
15
|
export interface CsrDevGraphModule {
|
|
4
16
|
vendor: boolean;
|
|
5
17
|
mtimeMs: number;
|
|
6
18
|
hash: string;
|
|
7
19
|
deps: string[];
|
|
8
20
|
helpers?: string;
|
|
21
|
+
narrowed?: Record<string, CsrDevNarrowedImport>;
|
|
9
22
|
}
|
|
10
23
|
|
|
11
24
|
export interface CsrDevGraph {
|
|
@@ -22,6 +35,7 @@ export interface CsrDevGraph {
|
|
|
22
35
|
failed?: { roots: string[]; files: Record<string, string> };
|
|
23
36
|
/** The stamp of the signal and dictionary sources the last whole build inlined; see `RegistryMetadataFingerprint`. */
|
|
24
37
|
metadata?: string;
|
|
38
|
+
barrels?: Record<string, CsrDevBarrelStamp>;
|
|
25
39
|
}
|
|
26
40
|
|
|
27
41
|
export interface CsrDevContext {
|
|
@@ -35,6 +49,7 @@ export interface CsrDevContext {
|
|
|
35
49
|
refreshFile: string | null;
|
|
36
50
|
/** Bare specifiers the page already loads (an SSR page's import map): required as `vendor:<specifier>`, not compiled. */
|
|
37
51
|
externals?: readonly string[];
|
|
52
|
+
barrelImports?: readonly string[];
|
|
38
53
|
}
|
|
39
54
|
|
|
40
55
|
export interface CsrDevSharedHelpers {
|
|
@@ -52,6 +67,7 @@ export interface CsrDevCompiledModule {
|
|
|
52
67
|
deps: string[];
|
|
53
68
|
mtimeMs: number;
|
|
54
69
|
hash: string;
|
|
70
|
+
narrowed?: Record<string, CsrDevNarrowedImport>;
|
|
55
71
|
}
|
|
56
72
|
|
|
57
73
|
/** Module factories by id and helper definitions by hash, kept in memory so app.js is written without a pass over the disk. */
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { tempDirs, writeText as write } from "../testHelpers";
|
|
4
|
+
import { CssCompiler, declaredCustomProperties } from "./cssCompiler";
|
|
5
|
+
|
|
6
|
+
const makeTempRoot = tempDirs("akan-css-compiler-");
|
|
3
7
|
|
|
4
8
|
describe("declaredCustomProperties", () => {
|
|
5
9
|
test("names what the regex-based version named", () => {
|
|
@@ -51,3 +55,61 @@ describe("declaredCustomProperties", () => {
|
|
|
51
55
|
}
|
|
52
56
|
});
|
|
53
57
|
});
|
|
58
|
+
|
|
59
|
+
describe("CssCompiler.discoverCssAndSources", () => {
|
|
60
|
+
test("scans the files a page reaches through a client barrel's narrowed imports, never the barrel's other modules", async () => {
|
|
61
|
+
const root = await makeTempRoot();
|
|
62
|
+
const paths = { "@apps/*": ["./apps/*"], "@libs/*": ["./libs/*"] };
|
|
63
|
+
const writeSys = async (dir: string, modules: string[]) => {
|
|
64
|
+
await write(
|
|
65
|
+
path.join(dir, "client.ts"),
|
|
66
|
+
[
|
|
67
|
+
'export * as cnst from "./lib/cnst";',
|
|
68
|
+
'export { msg, usePage } from "./lib/useClient";',
|
|
69
|
+
'export { st, RootStore } from "./lib/st";',
|
|
70
|
+
...modules.map((name) => `export { ${name} } from "./lib/${name.toLowerCase()}";`),
|
|
71
|
+
"",
|
|
72
|
+
].join("\n"),
|
|
73
|
+
);
|
|
74
|
+
await write(path.join(dir, "lib/cnst.ts"), "export const Sample = 1;\n");
|
|
75
|
+
await write(path.join(dir, "lib/useClient.ts"), "export const msg = 1;\nexport const usePage = () => msg;\n");
|
|
76
|
+
for (const name of modules) {
|
|
77
|
+
await write(
|
|
78
|
+
path.join(dir, `lib/${name.toLowerCase()}/index.ts`),
|
|
79
|
+
`import * as Zone from "./${name}.Zone";\nexport const ${name} = { Zone };\n`,
|
|
80
|
+
);
|
|
81
|
+
await write(path.join(dir, `lib/${name.toLowerCase()}/${name}.Zone.tsx`), 'export const Card = () => "p-4";\n');
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const appDir = path.join(root, "apps/demo");
|
|
85
|
+
await writeSys(appDir, ["Memo", "Other"]);
|
|
86
|
+
await writeSys(path.join(root, "libs/kit"), ["Widget"]);
|
|
87
|
+
await write(path.join(root, "libs/kit/lib/st.ts"), "export class RootStore {}\nexport const st = {};\n");
|
|
88
|
+
await write(
|
|
89
|
+
path.join(appDir, "lib/st.ts"),
|
|
90
|
+
'import { RootStore as kit } from "@libs/kit/client";\nexport class RootStore {}\nexport const st = { kit };\n',
|
|
91
|
+
);
|
|
92
|
+
await write(
|
|
93
|
+
path.join(appDir, "page/_index.tsx"),
|
|
94
|
+
'import { Memo, st } from "@apps/demo/client";\nexport default function Page() { return [Memo, st]; }\n',
|
|
95
|
+
);
|
|
96
|
+
const app = {
|
|
97
|
+
cwdPath: appDir,
|
|
98
|
+
workspace: { workspaceRoot: root },
|
|
99
|
+
getTsConfig: async () => ({ compilerOptions: { paths } }),
|
|
100
|
+
getConfig: async () => ({ barrelImports: ["@apps/demo/client", "@libs/kit/client"] }),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const { sourcePaths } = await new CssCompiler(app as never).discoverCssAndSources({ pageKeys: ["_index.tsx"] });
|
|
104
|
+
|
|
105
|
+
expect(sourcePaths.map((file) => path.relative(root, file)).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))).toEqual([
|
|
106
|
+
"apps/demo/lib/memo/Memo.Zone.tsx",
|
|
107
|
+
"apps/demo/lib/memo/index.ts",
|
|
108
|
+
"apps/demo/lib/st.ts",
|
|
109
|
+
"apps/demo/lib/useClient.ts",
|
|
110
|
+
"apps/demo/page/_index.tsx",
|
|
111
|
+
"libs/kit/lib/st.ts",
|
|
112
|
+
"libs/kit/lib/useClient.ts",
|
|
113
|
+
]);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { App } from "../commandDecorators";
|
|
3
|
+
import { BarrelAnalyzer, type PackageEntry } from "../transforms/barrelAnalyzer";
|
|
4
|
+
import { createTsconfigPackageResolver, rewriteBarrelImports } from "../transforms/barrelImportsPlugin";
|
|
5
|
+
import { CsrDevPaths } from "./csrDevPaths";
|
|
6
|
+
import type { CsrDevBarrelStamp, CsrDevNarrowedImport } from "./csrDevTypes";
|
|
7
|
+
|
|
8
|
+
export interface DevBarrelRewrite {
|
|
9
|
+
contents: string | null;
|
|
10
|
+
narrowed: Record<string, CsrDevNarrowedImport>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
//* Targets are written as relative paths, which resolve against the disk with no resolution build: a save that adds a
|
|
14
|
+
//* barrel import stays a patch.
|
|
15
|
+
export class DevBarrelImports {
|
|
16
|
+
readonly #barrels: string[];
|
|
17
|
+
readonly #resolvePackage: (pkgName: string) => Promise<PackageEntry | null>;
|
|
18
|
+
readonly #analyzer: BarrelAnalyzer;
|
|
19
|
+
readonly #packages = new Map<string, Promise<PackageEntry | null>>();
|
|
20
|
+
|
|
21
|
+
constructor(barrels: readonly string[], resolvePackage: (pkgName: string) => Promise<PackageEntry | null>) {
|
|
22
|
+
this.#barrels = [...barrels];
|
|
23
|
+
this.#resolvePackage = resolvePackage;
|
|
24
|
+
this.#analyzer = new BarrelAnalyzer({ resolvePackage });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static async create(app: App, barrels: readonly string[]): Promise<DevBarrelImports | null> {
|
|
28
|
+
if (barrels.length === 0) return null;
|
|
29
|
+
return new DevBarrelImports(barrels, await createTsconfigPackageResolver(app));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async rewrite(source: string, importer: string): Promise<DevBarrelRewrite | null> {
|
|
33
|
+
const named = this.#barrels.filter((barrel) => source.includes(barrel));
|
|
34
|
+
if (named.length === 0) return null;
|
|
35
|
+
const packages = await this.#packagesOf(named);
|
|
36
|
+
const from = path.dirname(CsrDevPaths.realpath(importer));
|
|
37
|
+
const names = new Map<string, Set<string>>();
|
|
38
|
+
const contents = await rewriteBarrelImports(source, named, this.#analyzer, {
|
|
39
|
+
keepLines: true,
|
|
40
|
+
specifierOf: (subpath) => DevBarrelImports.#relative(subpath, from, packages),
|
|
41
|
+
onImport: (barrel, imported) => names.set(barrel, new Set([...(names.get(barrel) ?? []), ...imported])),
|
|
42
|
+
});
|
|
43
|
+
const narrowed = await Promise.all(
|
|
44
|
+
[...names].map(async ([barrel, set]) => {
|
|
45
|
+
const sorted = [...set].sort();
|
|
46
|
+
return [barrel, { names: sorted, hash: await this.fingerprint(barrel, sorted) }] as const;
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
return { contents, narrowed: Object.fromEntries(narrowed) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
//? Hashes what an import of these names alone narrows to, with each target as the file it resolves to: the same
|
|
53
|
+
//? names in another importer narrow to the same files, and only a moved export or a moved file changes it.
|
|
54
|
+
async fingerprint(barrel: string, names: string[]): Promise<string> {
|
|
55
|
+
const packages = await this.#packagesOf([barrel]);
|
|
56
|
+
const aliased = names.map((name, index) => `${name} as _${index}`).join(", ");
|
|
57
|
+
const narrowed = await rewriteBarrelImports(
|
|
58
|
+
`import { ${aliased} } from ${JSON.stringify(barrel)};`,
|
|
59
|
+
[barrel],
|
|
60
|
+
this.#analyzer,
|
|
61
|
+
{ specifierOf: (subpath) => DevBarrelImports.#fileOf(subpath, packages) ?? subpath },
|
|
62
|
+
);
|
|
63
|
+
return Bun.hash(narrowed ?? "").toString(36);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async stamps(barrels: Iterable<string>, idOf: (file: string) => string): Promise<Record<string, CsrDevBarrelStamp>> {
|
|
67
|
+
const stamped = await Promise.all(
|
|
68
|
+
[...barrels].map(async (barrel) => {
|
|
69
|
+
const files = await this.#analyzer.filesOf(barrel);
|
|
70
|
+
const read = [...new Set([...files, ...files.map((file) => path.dirname(file))])];
|
|
71
|
+
const stamp: CsrDevBarrelStamp = {
|
|
72
|
+
read: Object.fromEntries(read.map((file) => [idOf(file), CsrDevPaths.mtimeOf(file)])),
|
|
73
|
+
digest: await this.#digest(barrel),
|
|
74
|
+
};
|
|
75
|
+
return [barrel, stamp] as const;
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
return Object.fromEntries(stamped);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
//? Every input of an importer's fingerprint: an importer is hashed again only once this moves, so a save that
|
|
82
|
+
//? changes a component's body costs one analysis of its barrel, whatever the number of importers.
|
|
83
|
+
async #digest(barrel: string): Promise<string> {
|
|
84
|
+
const map = await this.#analyzer.analyze(barrel);
|
|
85
|
+
const packages = await this.#packagesOf([barrel]);
|
|
86
|
+
const files = new Map<string, string>();
|
|
87
|
+
const fileOf = (subpath: string) => {
|
|
88
|
+
let file = files.get(subpath);
|
|
89
|
+
if (file === undefined) {
|
|
90
|
+
file = DevBarrelImports.#fileOf(subpath, packages) ?? subpath;
|
|
91
|
+
files.set(subpath, file);
|
|
92
|
+
}
|
|
93
|
+
return file;
|
|
94
|
+
};
|
|
95
|
+
const exports = [...(map ?? [])]
|
|
96
|
+
.map(([name, target]) => [name, target.originalName, !!target.namespace, fileOf(target.subpath)] as const)
|
|
97
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
98
|
+
return Bun.hash(JSON.stringify([fileOf(barrel), map === null, exports])).toString(36);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async #packagesOf(barrels: string[]): Promise<PackageEntry[]> {
|
|
102
|
+
return (await Promise.all(barrels.map((barrel) => this.#packageOf(barrel))))
|
|
103
|
+
.filter((pkg): pkg is PackageEntry => pkg !== null)
|
|
104
|
+
.sort((a, b) => b.pkgName.length - a.pkgName.length);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#packageOf(barrel: string): Promise<PackageEntry | null> {
|
|
108
|
+
let pkg = this.#packages.get(barrel);
|
|
109
|
+
if (!pkg) {
|
|
110
|
+
pkg = this.#resolvePackage(barrel);
|
|
111
|
+
this.#packages.set(barrel, pkg);
|
|
112
|
+
}
|
|
113
|
+
return pkg;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
static #fileOf(subpath: string, packages: PackageEntry[]): string | null {
|
|
117
|
+
const pkg = packages.find(({ pkgName }) => subpath === pkgName || subpath.startsWith(`${pkgName}/`));
|
|
118
|
+
if (!pkg) return null;
|
|
119
|
+
const file = CsrDevPaths.resolveOnDisk(path.join(pkg.pkgDir, subpath.slice(pkg.pkgName.length)));
|
|
120
|
+
return file ? CsrDevPaths.realpath(file) : null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
static #relative(subpath: string, from: string, packages: PackageEntry[]): string {
|
|
124
|
+
const file = DevBarrelImports.#fileOf(subpath, packages);
|
|
125
|
+
if (!file) return subpath;
|
|
126
|
+
const relative = path.relative(from, file);
|
|
127
|
+
//? A target on another Windows drive has no relative path: `path.relative` returns it absolute.
|
|
128
|
+
if (path.isAbsolute(relative)) return file.split(path.sep).join("/");
|
|
129
|
+
const posix = relative.split(path.sep).join("/");
|
|
130
|
+
return posix.startsWith("../") ? posix : `./${posix}`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -7,7 +7,8 @@ import { CsrDevModuleCompiler } from "./csrDevModuleCompiler";
|
|
|
7
7
|
import { CsrDevPatcher } from "./csrDevPatcher";
|
|
8
8
|
import { CsrDevPaths } from "./csrDevPaths";
|
|
9
9
|
import { CsrDevResolver } from "./csrDevResolver";
|
|
10
|
-
import type { CsrDevCompiledModule, CsrDevContext, CsrDevGraph } from "./csrDevTypes";
|
|
10
|
+
import type { CsrDevBarrelStamp, CsrDevCompiledModule, CsrDevContext, CsrDevGraph } from "./csrDevTypes";
|
|
11
|
+
import { DevBarrelImports } from "./devBarrelImports";
|
|
11
12
|
import { RegistryMetadataFingerprint } from "./registryMetadataFingerprint";
|
|
12
13
|
import { WatchRootResolver } from "./watchRootResolver";
|
|
13
14
|
|
|
@@ -131,7 +132,7 @@ export abstract class DevRegistryBundler {
|
|
|
131
132
|
const resolver = new CsrDevResolver({ paths: this.paths, context, entryFiles: roots });
|
|
132
133
|
await resolver.prepass();
|
|
133
134
|
const compiler = this.compiler(context, resolver);
|
|
134
|
-
const { modules: compiled } = await compiler.compile(
|
|
135
|
+
const { modules: compiled, barrels } = await compiler.compile(
|
|
135
136
|
[...roots, ...(context.refreshFile ? [context.refreshFile] : [])],
|
|
136
137
|
new Set(),
|
|
137
138
|
);
|
|
@@ -147,7 +148,7 @@ export abstract class DevRegistryBundler {
|
|
|
147
148
|
pending: [],
|
|
148
149
|
metadata,
|
|
149
150
|
};
|
|
150
|
-
this.merge(graph, resolver, compiled);
|
|
151
|
+
this.merge(graph, resolver, compiled, barrels);
|
|
151
152
|
await this.writer.markBuilding();
|
|
152
153
|
await this.writer.writeModules(compiled);
|
|
153
154
|
const vendorFile = await this.writer.writeVendor(graph);
|
|
@@ -170,7 +171,16 @@ export abstract class DevRegistryBundler {
|
|
|
170
171
|
});
|
|
171
172
|
}
|
|
172
173
|
|
|
173
|
-
|
|
174
|
+
async barrelImports(context: CsrDevContext): Promise<DevBarrelImports | null> {
|
|
175
|
+
return await DevBarrelImports.create(this.app, context.barrelImports ?? []);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
merge(
|
|
179
|
+
graph: CsrDevGraph,
|
|
180
|
+
resolver: CsrDevResolver,
|
|
181
|
+
compiled: CsrDevCompiledModule[],
|
|
182
|
+
barrels: Record<string, CsrDevBarrelStamp> = {},
|
|
183
|
+
): void {
|
|
174
184
|
for (const module of compiled)
|
|
175
185
|
graph.modules[module.id] = {
|
|
176
186
|
vendor: module.vendor,
|
|
@@ -178,7 +188,12 @@ export abstract class DevRegistryBundler {
|
|
|
178
188
|
hash: module.hash,
|
|
179
189
|
deps: module.deps.map((dep) => (CsrDevPaths.isStub(dep) ? dep : this.paths.idOf(dep))),
|
|
180
190
|
...(module.helpers ? { helpers: module.helpers.hash } : {}),
|
|
191
|
+
...(module.narrowed ? { narrowed: module.narrowed } : {}),
|
|
181
192
|
};
|
|
193
|
+
const narrowing = new Set(Object.values(graph.modules).flatMap((module) => Object.keys(module.narrowed ?? {})));
|
|
194
|
+
graph.barrels = Object.fromEntries(
|
|
195
|
+
Object.entries({ ...graph.barrels, ...barrels }).filter(([barrel]) => narrowing.has(barrel)),
|
|
196
|
+
);
|
|
182
197
|
//? Only what this round compiled leaves pending: a root another route's check failed on stays for its retry.
|
|
183
198
|
const compiledIds = new Set(compiled.map((module) => module.id));
|
|
184
199
|
graph.pending = graph.pending.filter((id) => !compiledIds.has(id));
|