@akanjs/devkit 3.1.2-rc.3 → 3.2.0-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 +348 -0
- package/abstractDoc.ts +1 -1
- package/agentAssets.ts +2 -2
- package/akanConfig/akanConfig.ts +2 -2
- package/akanMcpContract.ts +2 -2
- package/apiReference.test.ts +37 -0
- package/apiReference.ts +240 -0
- package/applicationBuildReporter.test.ts +54 -0
- package/applicationBuildReporter.ts +12 -0
- package/applicationBuildRunner.test.ts +34 -14
- package/applicationBuildRunner.ts +77 -14
- package/artifact/implicitRootLayout.test.ts +42 -0
- package/artifact/implicitRootLayout.ts +5 -6
- package/artifact/storeBootstrap.ts +24 -0
- package/cloud/cloudApi.test.ts +70 -7
- package/cloud/cloudApi.ts +10 -4
- package/cloud/constants.ts +3 -0
- package/cloud/globalConfig.test.ts +36 -0
- package/cloud/globalConfig.ts +10 -1
- package/codeAgent/skills/akan-debug/SKILL.md +2 -1
- package/codeAgent/skills/akan-page/SKILL.md +30 -2
- package/codeAgent/skills/akan-service/SKILL.md +77 -1
- package/commandDecorators/command.ts +55 -9
- package/commandDecorators/commandDecorators.test.ts +131 -2
- package/declarationReader.ts +211 -0
- package/executors.test.ts +65 -8
- package/executors.ts +5 -3
- package/frameworkGraph.test.ts +121 -0
- package/frameworkGraph.ts +165 -0
- package/frontendBuild/allRoutesBuilder.test.ts +84 -0
- package/frontendBuild/allRoutesBuilder.ts +32 -12
- package/frontendBuild/buildRouteClient.test.ts +192 -2
- package/frontendBuild/bundleDefine.ts +4 -0
- package/frontendBuild/clientBuildTypes.ts +7 -1
- package/frontendBuild/clientEntriesBundler.test.ts +82 -0
- package/frontendBuild/clientEntriesBundler.ts +55 -4
- package/frontendBuild/csrArtifactBuilder.test.ts +23 -0
- package/frontendBuild/csrArtifactBuilder.ts +2 -1
- package/frontendBuild/fontOptimizer.test.ts +54 -0
- package/frontendBuild/fontOptimizer.ts +5 -4
- package/frontendBuild/index.ts +3 -0
- package/frontendBuild/routeClientBuilder.ts +6 -2
- package/frontendBuild/routeModulePreloads.test.ts +59 -0
- package/frontendBuild/routeModulePreloads.ts +52 -0
- package/frontendBuild/ssrBaseArtifactBuilder.ts +35 -10
- package/frontendBuild/ssrDevBundler.test.ts +26 -0
- package/frontendBuild/ssrDevBundler.ts +2 -1
- package/frontendBuild/vendorImportScanner.test.ts +71 -0
- package/frontendBuild/vendorImportScanner.ts +80 -0
- package/frontendBuild/vendorNarrowing.test.ts +103 -0
- package/frontendBuild/vendorNarrowing.ts +64 -0
- package/frontendBuild/vendorSpecifiers.ts +13 -0
- package/gitRevision.test.ts +82 -0
- package/gitRevision.ts +128 -0
- package/incrementalBuilder/builderGraphPayloads.ts +60 -0
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +105 -6
- package/incrementalBuilder/incrementalBuilder.proc.ts +6 -10
- package/integration/devStability.integration.test.ts +1 -1
- package/integration/devStabilityHarness.test.ts +1 -1
- package/lint/__fixtures__/no-daisyui-legacy-class/bad.tsx +4 -2
- package/lint/__fixtures__/no-js-private-class-method/bad.tsx +2 -0
- package/lint/__fixtures__/no-js-private-class-method/good.tsx +3 -2
- package/lint/no-daisyui-legacy-class.grit +9 -7
- package/lint/no-deep-internal-import.grit +1 -1
- package/lint/no-js-private-class-method.grit +9 -28
- package/maintainabilityBaseline.ts +7 -2
- package/maintainabilityContract.test.ts +183 -0
- package/maintainabilityContract.ts +145 -30
- package/maintainabilityExceptionScanner.test.ts +13 -2
- package/maintainabilityExceptionScanner.ts +16 -6
- package/maintainabilityLimits.ts +12 -0
- package/maintainabilityReport.test.ts +35 -0
- package/maintainabilityReport.ts +29 -8
- package/mobile/nativePublicEnv.test.ts +5 -1
- package/package.json +3 -3
- package/routeJsBudget.test.ts +177 -0
- package/routeJsBudget.ts +222 -0
- package/structureScanner.test.ts +167 -0
- package/structureScanner.ts +164 -0
- package/transforms/barrelAnalyzer.ts +10 -3
- package/transforms/barrelImportsPlugin.ts +13 -7
- package/transforms/serverEnvPlugin.test.ts +1 -1
- package/transforms/serverEnvPlugin.ts +1 -1
- package/transforms/transforms.test.ts +64 -12
- package/transforms/tsconfigPackageResolver.test.ts +12 -0
- package/typeChecker.test.ts +100 -0
- package/typeChecker.ts +14 -2
- package/workflow/plan.ts +79 -17
- package/workflow/types.ts +1 -0
- package/workspaceGraph.test.ts +121 -0
- package/workspaceGraph.ts +149 -16
- package/workspaceLayout.ts +1 -0
- /package/integration/{devStabilityHarness.ts → devStabilityHarness.fixture.ts} +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { realpath } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { tempDirs, writeText } from "../testHelpers";
|
|
6
|
+
import { ClientEntriesBundler } from "./clientEntriesBundler";
|
|
7
|
+
|
|
8
|
+
const makeTempRoot = tempDirs("akan-client-entries-");
|
|
9
|
+
//? Bun's metafile names real paths, and macOS's temp dir sits behind the /var symlink.
|
|
10
|
+
const realTempRoot = async () => fs.realpathSync(await makeTempRoot());
|
|
11
|
+
|
|
12
|
+
const vendorApp = async () => {
|
|
13
|
+
const root = await realTempRoot();
|
|
14
|
+
const appDir = path.join(root, "apps/demo");
|
|
15
|
+
const vendor = path.join(root, "vendor/facet.ts");
|
|
16
|
+
await writeText(path.join(root, "vendor/lib.ts"), "export const real = 1;\nexport const other = 2;\n");
|
|
17
|
+
await writeText(vendor, 'export * from "./lib";\n');
|
|
18
|
+
const app = {
|
|
19
|
+
name: "demo",
|
|
20
|
+
cwdPath: appDir,
|
|
21
|
+
dist: { cwdPath: path.join(root, "dist/apps/demo") },
|
|
22
|
+
workspace: { workspaceRoot: root },
|
|
23
|
+
getConfig: async () => ({ optimizeImports: [] }),
|
|
24
|
+
getPublicEnv: () => ({}),
|
|
25
|
+
} as never;
|
|
26
|
+
const bundle = (command: "build" | "start", entryExports?: Record<string, string[]>) =>
|
|
27
|
+
new ClientEntriesBundler({ app, entries: [vendor], command, entryExports }).bundle();
|
|
28
|
+
return { appDir, vendor, bundle };
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe("ClientEntriesBundler narrowed entries", () => {
|
|
32
|
+
test("fails the build, naming each name a route imports that its vendor does not export", async () => {
|
|
33
|
+
const { vendor, bundle } = await vendorApp();
|
|
34
|
+
|
|
35
|
+
await expect(bundle("build", { [vendor]: ["real", "notThere"] })).rejects.toThrow("facet.ts: notThere");
|
|
36
|
+
await expect(bundle("build", { [vendor]: ["real"] })).resolves.toBeDefined();
|
|
37
|
+
}, 30_000);
|
|
38
|
+
|
|
39
|
+
test("writes a narrowed alias apart from the whole one `akan start` keeps for the same entry", async () => {
|
|
40
|
+
const { appDir, vendor, bundle } = await vendorApp();
|
|
41
|
+
await bundle("start");
|
|
42
|
+
await bundle("build", { [vendor]: ["real"] });
|
|
43
|
+
|
|
44
|
+
const aliasDir = path.join(appDir, ".akan/generated/client-entry-alias/client");
|
|
45
|
+
const aliases = await Promise.all(
|
|
46
|
+
[...new Bun.Glob("*.tsx").scanSync(aliasDir)].map((file) => Bun.file(path.join(aliasDir, file)).text()),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(aliases).toHaveLength(2);
|
|
50
|
+
expect(aliases).toContain(ClientEntriesBundler.narrowedAliasSource(vendor, ["real"]));
|
|
51
|
+
}, 30_000);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("ClientEntriesBundler", () => {
|
|
55
|
+
test("takes a package's production build in a build and its default one under start", async () => {
|
|
56
|
+
const root = await realpath(await makeTempRoot());
|
|
57
|
+
await writeText(
|
|
58
|
+
path.join(root, "node_modules/twobuild/package.json"),
|
|
59
|
+
'{ "name": "twobuild", "exports": { ".": { "production": "./prod.js", "default": "./switch.js" } } }\n',
|
|
60
|
+
);
|
|
61
|
+
await writeText(path.join(root, "node_modules/twobuild/prod.js"), 'export const mark = "prod-build";\n');
|
|
62
|
+
await writeText(path.join(root, "node_modules/twobuild/switch.js"), 'export const mark = "switch-build";\n');
|
|
63
|
+
const entry = path.join(root, "Mark.tsx");
|
|
64
|
+
await writeText(entry, '"use client";\nimport { mark } from "twobuild";\nexport const Mark = () => mark;\n');
|
|
65
|
+
const app = {
|
|
66
|
+
name: "probe",
|
|
67
|
+
cwdPath: path.join(root, "app"),
|
|
68
|
+
dist: { cwdPath: path.join(root, "dist") },
|
|
69
|
+
workspace: { workspaceRoot: root },
|
|
70
|
+
getConfig: async () => ({ optimizeImports: [] }),
|
|
71
|
+
getPublicEnv: () => ({}),
|
|
72
|
+
};
|
|
73
|
+
const bundle = async (command: "build" | "start") => {
|
|
74
|
+
const result = await new ClientEntriesBundler({ app: app as never, entries: [entry], command }).bundle();
|
|
75
|
+
const output = result.entryOutputAbsByAbsPath.get(entry);
|
|
76
|
+
return output ? await Bun.file(output).text() : "";
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
expect(await bundle("build")).toContain("prod-build");
|
|
80
|
+
expect(await bundle("start")).toContain("switch-build");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -3,13 +3,14 @@ import path from "node:path";
|
|
|
3
3
|
import type { SsrManifest, SsrManifestEntry } from "akanjs/server";
|
|
4
4
|
import type { BunPlugin } from "bun";
|
|
5
5
|
import { scanUseClientExports, toClientReferencePath } from "../transforms/rscUseClientTransform";
|
|
6
|
-
import { bundleDefine } from "./bundleDefine";
|
|
6
|
+
import { bundleConditions, bundleDefine } from "./bundleDefine";
|
|
7
7
|
import {
|
|
8
8
|
type BundleClientEntriesInternalOptions,
|
|
9
9
|
type BundleClientEntriesResult,
|
|
10
10
|
CLIENT_BUNDLE_NAMING,
|
|
11
11
|
type ClientBundleTarget,
|
|
12
12
|
type ClientManifest,
|
|
13
|
+
type MetafileInput,
|
|
13
14
|
type MetafileOutput,
|
|
14
15
|
type OpaqueEntryAliases,
|
|
15
16
|
} from "./clientBuildTypes";
|
|
@@ -21,6 +22,7 @@ export class ClientEntriesBundler {
|
|
|
21
22
|
#external: readonly string[];
|
|
22
23
|
#externalSubpaths: readonly string[];
|
|
23
24
|
#externalAliases: Partial<Record<string, string>>;
|
|
25
|
+
#entryExports: Partial<Record<string, readonly string[]>>;
|
|
24
26
|
#command: "build" | "start";
|
|
25
27
|
#target: ClientBundleTarget;
|
|
26
28
|
#outputSubdir: string;
|
|
@@ -32,9 +34,11 @@ export class ClientEntriesBundler {
|
|
|
32
34
|
#entryUrlsByAbsPath = new Map<string, string>();
|
|
33
35
|
#entryOutputAbsByAbsPath = new Map<string, string>();
|
|
34
36
|
#entryDepsByAbsPath = new Map<string, string[]>();
|
|
37
|
+
#entryChunkUrlsByAbsPath = new Map<string, string[]>();
|
|
35
38
|
#clientReferenceIdByAbsPath = new Map<string, string>();
|
|
36
39
|
#opaqueEntries: OpaqueEntryAliases | null = null;
|
|
37
40
|
#metafileOutputsByAbs = new Map<string, MetafileOutput>();
|
|
41
|
+
#metafileInputs: Record<string, MetafileInput> = {};
|
|
38
42
|
|
|
39
43
|
constructor(options: BundleClientEntriesInternalOptions) {
|
|
40
44
|
this.#app = options.app;
|
|
@@ -43,6 +47,7 @@ export class ClientEntriesBundler {
|
|
|
43
47
|
this.#external = options.external ?? [];
|
|
44
48
|
this.#externalSubpaths = options.externalSubpaths ?? [];
|
|
45
49
|
this.#externalAliases = options.externalAliases ?? {};
|
|
50
|
+
this.#entryExports = options.entryExports ?? {};
|
|
46
51
|
this.#command = options.command ?? "start";
|
|
47
52
|
this.#target = options.target ?? "browser";
|
|
48
53
|
this.#outputSubdir = options.outputSubdir ?? "client";
|
|
@@ -64,6 +69,7 @@ export class ClientEntriesBundler {
|
|
|
64
69
|
naming: CLIENT_BUNDLE_NAMING,
|
|
65
70
|
metafile: true,
|
|
66
71
|
define: bundleDefine(this.#app, this.#command, "ssr"),
|
|
72
|
+
conditions: bundleConditions(this.#command),
|
|
67
73
|
minify: this.#command === "build",
|
|
68
74
|
optimizeImports: akanConfig.optimizeImports,
|
|
69
75
|
reactFastRefresh: this.#command === "start" && this.#outputSubdir === "client" && this.#reactFastRefresh,
|
|
@@ -81,9 +87,11 @@ export class ClientEntriesBundler {
|
|
|
81
87
|
this.#metafileOutputsByAbs = new Map<string, MetafileOutput>(
|
|
82
88
|
Object.entries(metafile.outputs).map(([k, v]) => [this.#absFromOutdir(k), v as MetafileOutput]),
|
|
83
89
|
);
|
|
90
|
+
this.#metafileInputs = metafile.inputs as Record<string, MetafileInput>;
|
|
84
91
|
|
|
85
92
|
for (const artifact of result.outputs.filter((a) => a.kind === "entry-point"))
|
|
86
93
|
this.#addEntryArtifact(artifact.path);
|
|
94
|
+
this.#assertNarrowedExports();
|
|
87
95
|
|
|
88
96
|
return {
|
|
89
97
|
manifest: this.#manifest,
|
|
@@ -91,6 +99,8 @@ export class ClientEntriesBundler {
|
|
|
91
99
|
entryUrlsByAbsPath: this.#entryUrlsByAbsPath,
|
|
92
100
|
entryOutputAbsByAbsPath: this.#entryOutputAbsByAbsPath,
|
|
93
101
|
entryDepsByAbsPath: this.#entryDepsByAbsPath,
|
|
102
|
+
entryChunkUrlsByAbsPath: this.#entryChunkUrlsByAbsPath,
|
|
103
|
+
externalsByChunkUrl: this.#collectExternalsByChunkUrl(),
|
|
94
104
|
clientReferenceIdByAbsPath: this.#clientReferenceIdByAbsPath,
|
|
95
105
|
};
|
|
96
106
|
}
|
|
@@ -102,11 +112,15 @@ export class ClientEntriesBundler {
|
|
|
102
112
|
const aliasedEntries = await Promise.all(
|
|
103
113
|
this.#entries.map(async (entry) => {
|
|
104
114
|
const absEntry = path.resolve(entry);
|
|
105
|
-
const
|
|
106
|
-
|
|
115
|
+
const narrowed = this.#entryExports[absEntry];
|
|
116
|
+
//? `akan build` and `akan start` share this directory, so a narrowed alias never takes the whole one's file.
|
|
117
|
+
const key = `${this.#app.name}\n${this.#outputSubdir}\n${absEntry}${narrowed ? "\nnarrowed" : ""}`;
|
|
118
|
+
const aliasPath = path.join(aliasDir, `${Bun.hash(key).toString(36)}.tsx`);
|
|
107
119
|
await Bun.write(
|
|
108
120
|
aliasPath,
|
|
109
|
-
|
|
121
|
+
narrowed
|
|
122
|
+
? ClientEntriesBundler.narrowedAliasSource(absEntry, narrowed)
|
|
123
|
+
: this.#createOpaqueEntryAliasSource(absEntry, await this.#scanEntryExportNames(absEntry)),
|
|
110
124
|
);
|
|
111
125
|
originalByAlias.set(path.resolve(aliasPath), absEntry);
|
|
112
126
|
return aliasPath;
|
|
@@ -124,6 +138,27 @@ export class ClientEntriesBundler {
|
|
|
124
138
|
return `${lines.join("\n")}\n`;
|
|
125
139
|
}
|
|
126
140
|
|
|
141
|
+
//? `export {} from` still loads the entry, so a module only ever imported for its side effects keeps them.
|
|
142
|
+
static narrowedAliasSource(absEntry: string, names: readonly string[]): string {
|
|
143
|
+
const list = names.length > 0 ? `{ ${names.join(", ")} }` : "{}";
|
|
144
|
+
return `export ${list} from ${JSON.stringify(path.resolve(absEntry))};\n`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
//? Bun silently drops a name `export { … } from` cannot find behind an `export *`.
|
|
148
|
+
#assertNarrowedExports(): void {
|
|
149
|
+
const missing = Object.entries(this.#entryExports).flatMap(([absEntry, names = []]) => {
|
|
150
|
+
const absOut = this.#entryOutputAbsByAbsPath.get(absEntry);
|
|
151
|
+
if (!absOut) return [`${path.basename(absEntry)}: no output`];
|
|
152
|
+
const exported = new Set(this.#metafileOutputsByAbs.get(absOut)?.exports);
|
|
153
|
+
const absent = names.filter((name) => !exported.has(name));
|
|
154
|
+
return absent.length > 0 ? [`${path.basename(absEntry)}: ${absent.join(", ")}`] : [];
|
|
155
|
+
});
|
|
156
|
+
if (missing.length > 0)
|
|
157
|
+
throw new Error(
|
|
158
|
+
`[ClientEntriesBundler] client code imports names its vendor does not export (${missing.join("; ")})`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
127
162
|
async #scanEntryExportNames(absEntry: string): Promise<string[]> {
|
|
128
163
|
return scanUseClientExports(await Bun.file(absEntry).text(), absEntry, this.#app.workspace.workspaceRoot);
|
|
129
164
|
}
|
|
@@ -220,6 +255,21 @@ export class ClientEntriesBundler {
|
|
|
220
255
|
return urls;
|
|
221
256
|
}
|
|
222
257
|
|
|
258
|
+
//? Bun's metafile lists an external import on the input that wrote it, never on the output chunk.
|
|
259
|
+
#collectExternalsByChunkUrl(): Map<string, string[]> {
|
|
260
|
+
const externalsByChunkUrl = new Map<string, string[]>();
|
|
261
|
+
if (this.#command !== "build" || this.#target !== "browser") return externalsByChunkUrl;
|
|
262
|
+
for (const [absOut, info] of this.#metafileOutputsByAbs) {
|
|
263
|
+
if (!absOut.endsWith(".js")) continue;
|
|
264
|
+
const externals = new Set<string>();
|
|
265
|
+
for (const input of Object.keys(info.inputs ?? {}))
|
|
266
|
+
for (const imp of this.#metafileInputs[input]?.imports ?? [])
|
|
267
|
+
if (imp.external && imp.kind !== "dynamic-import" && !/^[./]/.test(imp.path)) externals.add(imp.path);
|
|
268
|
+
externalsByChunkUrl.set(this.#toServeUrl(absOut), [...externals].sort());
|
|
269
|
+
}
|
|
270
|
+
return externalsByChunkUrl;
|
|
271
|
+
}
|
|
272
|
+
|
|
223
273
|
#collectClientDeps(absOutPath: string, visited = new Set<string>()): Set<string> {
|
|
224
274
|
if (visited.has(absOutPath)) return new Set();
|
|
225
275
|
visited.add(absOutPath);
|
|
@@ -256,6 +306,7 @@ export class ClientEntriesBundler {
|
|
|
256
306
|
this.#entryUrlsByAbsPath.set(entryPointAbs, entryUrl);
|
|
257
307
|
this.#entryOutputAbsByAbsPath.set(entryPointAbs, absOut);
|
|
258
308
|
this.#entryDepsByAbsPath.set(entryPointAbs, [...this.#collectClientDeps(absOut)].sort());
|
|
309
|
+
this.#entryChunkUrlsByAbsPath.set(entryPointAbs, chunkUrls);
|
|
259
310
|
const clientReferenceId = toClientReferencePath(entryPointAbs, this.#app.workspace.workspaceRoot);
|
|
260
311
|
this.#clientReferenceIdByAbsPath.set(entryPointAbs, clientReferenceId);
|
|
261
312
|
|
|
@@ -62,6 +62,29 @@ console.info(await result.outputs[0].text());
|
|
|
62
62
|
expect(firstOutput).not.toContain(path.basename(first));
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
test("takes a package's production build in a build and its default one under start", async () => {
|
|
66
|
+
const root = await makeTempRoot();
|
|
67
|
+
const app = { getPublicEnv: () => ({ AKAN_PUBLIC_ENV: "main" }) };
|
|
68
|
+
await writeText(
|
|
69
|
+
path.join(root, "node_modules/twobuild/package.json"),
|
|
70
|
+
'{ "name": "twobuild", "exports": { ".": { "production": "./prod.js", "default": "./switch.js" } } }\n',
|
|
71
|
+
);
|
|
72
|
+
await writeText(path.join(root, "node_modules/twobuild/prod.js"), 'export const mark = "prod-build";\n');
|
|
73
|
+
await writeText(path.join(root, "node_modules/twobuild/switch.js"), 'export const mark = "switch-build";\n');
|
|
74
|
+
await writeText(path.join(root, "entry.ts"), 'import { mark } from "twobuild";\nconsole.info(mark);\n');
|
|
75
|
+
const bundle = async (command: "build" | "start") => {
|
|
76
|
+
const result = await Bun.build({
|
|
77
|
+
...CsrArtifactBuilder.bundleOptions(app as never, command),
|
|
78
|
+
entrypoints: [path.join(root, "entry.ts")],
|
|
79
|
+
});
|
|
80
|
+
expect(result.success).toBe(true);
|
|
81
|
+
return (await result.outputs[0]?.text()) ?? "";
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
expect(await bundle("build")).toContain("prod-build");
|
|
85
|
+
expect(await bundle("start")).toContain("switch-build");
|
|
86
|
+
});
|
|
87
|
+
|
|
65
88
|
//? The shape of `@lexical/code-prism` under a `lazy()` editor, which threw in every view and CSR shell carrying it.
|
|
66
89
|
test("runs a lazily imported module's CommonJS side-effect import before the imports after it", async () => {
|
|
67
90
|
const root = await makeTempRoot();
|
|
@@ -6,7 +6,7 @@ import type { App } from "../commandDecorators";
|
|
|
6
6
|
import { type BarrelImportsPluginOptions, createBarrelImportsPlugin } from "../transforms/barrelImportsPlugin";
|
|
7
7
|
import { createBrowserDirnamePlugin } from "../transforms/browserDirnamePlugin";
|
|
8
8
|
import { SideEffectImportOrder } from "../transforms/sideEffectImportOrder";
|
|
9
|
-
import { bundleDefine } from "./bundleDefine";
|
|
9
|
+
import { bundleConditions, bundleDefine } from "./bundleDefine";
|
|
10
10
|
import { CsrEntryFiles } from "./csrEntryFiles";
|
|
11
11
|
import { type McpAppView, McpAppViewBuilder } from "./mcpAppViewBuilder";
|
|
12
12
|
import { PagesBundleBuilder } from "./pagesBundleBuilder";
|
|
@@ -114,6 +114,7 @@ export class CsrArtifactBuilder {
|
|
|
114
114
|
splitting: false,
|
|
115
115
|
minify: true,
|
|
116
116
|
define: bundleDefine(app, command, "csr"),
|
|
117
|
+
conditions: bundleConditions(command),
|
|
117
118
|
optimizeImports,
|
|
118
119
|
// A raw `.css` in the route graph is Tailwind source; the base artifact's compiled sheet is the only stylesheet.
|
|
119
120
|
plugins: [PagesBundleBuilder.createCssStubPlugin(), createBrowserDirnamePlugin(), SideEffectImportOrder.plugin()],
|
|
@@ -58,6 +58,18 @@ const chainFontList = (extra = "") => `[\n ${chainFontEntry(extra)},\n ]`;
|
|
|
58
58
|
|
|
59
59
|
const optimize = (app: App) => new FontOptimizer(app, "start").optimize();
|
|
60
60
|
|
|
61
|
+
const sizeAdjusts = (css: string) => [...css.matchAll(/size-adjust: ([^;]+);/g)].map((match) => match[1]);
|
|
62
|
+
|
|
63
|
+
const renameSfntTable = (bytes: Uint8Array, from: string, to: string) => {
|
|
64
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
65
|
+
for (let index = 0; index < view.getUint16(4); index++) {
|
|
66
|
+
const offset = 12 + index * 16;
|
|
67
|
+
if (new TextDecoder().decode(bytes.subarray(offset, offset + 4)) === from)
|
|
68
|
+
bytes.set(new TextEncoder().encode(to), offset);
|
|
69
|
+
}
|
|
70
|
+
return bytes;
|
|
71
|
+
};
|
|
72
|
+
|
|
61
73
|
describe("FontOptimizer cache", () => {
|
|
62
74
|
test("reuses subset output instead of resubsetting an unchanged font", async () => {
|
|
63
75
|
const { app } = await makeApp(layoutWith());
|
|
@@ -121,6 +133,48 @@ describe("FontOptimizer cache", () => {
|
|
|
121
133
|
});
|
|
122
134
|
});
|
|
123
135
|
|
|
136
|
+
describe("FontOptimizer fallback", () => {
|
|
137
|
+
test("writes a metric-adjusted fallback face next to the optimized face", async () => {
|
|
138
|
+
const { app } = await makeApp(layoutWith());
|
|
139
|
+
|
|
140
|
+
const { css } = await optimize(app);
|
|
141
|
+
|
|
142
|
+
expect(css).toContain('font-family: "Assistant fallback"');
|
|
143
|
+
expect(sizeAdjusts(css).length).toBeGreaterThan(0);
|
|
144
|
+
expect(css).toMatch(/ascent-override: [\d.]+%/);
|
|
145
|
+
expect(css).toMatch(/descent-override: [\d.]+%/);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("sizes the fallback from the source font, so a subset without Latin letters keeps the same size", async () => {
|
|
149
|
+
const { app: latinApp } = await makeApp(layoutWith());
|
|
150
|
+
const { app: digitsApp } = await makeApp(layoutWith(`\n subsets: [],\n subsetText: "0123456789",`));
|
|
151
|
+
|
|
152
|
+
const latin = sizeAdjusts((await optimize(latinApp)).css);
|
|
153
|
+
const digits = sizeAdjusts((await optimize(digitsApp)).css);
|
|
154
|
+
|
|
155
|
+
expect(latin.length).toBeGreaterThan(0);
|
|
156
|
+
expect(digits).toEqual(latin);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("still writes the face, with no fallback, when the source font's metrics cannot be read", async () => {
|
|
160
|
+
const { app, cwdPath } = await makeApp(`
|
|
161
|
+
export const fonts = [{ name: "Gilda", paths: [{ src: "/fonts/Gilda.ttf", weight: 400 }] }];
|
|
162
|
+
export default function Layout() {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
`);
|
|
166
|
+
const source = await Bun.file(path.resolve(path.dirname(SOURCE_FONT), "GildaDisplay-Regular.ttf")).bytes();
|
|
167
|
+
// capsize reads the average width from the OS/2 table, so without one the metrics read throws.
|
|
168
|
+
await Bun.write(path.join(cwdPath, "public/fonts/Gilda.ttf"), renameSfntTable(source, "OS/2", "XS/2"));
|
|
169
|
+
|
|
170
|
+
const result = await optimize(app);
|
|
171
|
+
|
|
172
|
+
expect(result.files).toHaveLength(1);
|
|
173
|
+
expect(result.css).toContain('font-family: "Gilda"');
|
|
174
|
+
expect(result.css).not.toContain('font-family: "Gilda fallback"');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
124
178
|
describe("FontOptimizer discovery", () => {
|
|
125
179
|
test("reads the .fonts() stage of a rootLayout() chain", async () => {
|
|
126
180
|
const { app, warnings } = await makeApp(chainLayoutWith(chainFontList()));
|
|
@@ -39,7 +39,7 @@ export class FontOptimizer {
|
|
|
39
39
|
#woff2Ready: Promise<void> | null = null;
|
|
40
40
|
|
|
41
41
|
static #ksX1001Text: string | null = null;
|
|
42
|
-
static readonly #cacheVersion =
|
|
42
|
+
static readonly #cacheVersion = 3;
|
|
43
43
|
|
|
44
44
|
constructor(app: App, command: FontOptimizerCommand = "start") {
|
|
45
45
|
this.#app = app;
|
|
@@ -163,7 +163,7 @@ export class FontOptimizer {
|
|
|
163
163
|
this.#files.push(outputPath);
|
|
164
164
|
|
|
165
165
|
faceCss.push(this.#buildOptimizedFontFaceRule(font, face));
|
|
166
|
-
const fallbackCss = await this.#buildFontaineFallbackCss(font, face,
|
|
166
|
+
const fallbackCss = await this.#buildFontaineFallbackCss(font, face, sourcePath);
|
|
167
167
|
if (fallbackCss) faceCss.push(fallbackCss);
|
|
168
168
|
}
|
|
169
169
|
|
|
@@ -468,10 +468,11 @@ export class FontOptimizer {
|
|
|
468
468
|
return `@font-face {\n${declarations.map(([prop, value]) => ` ${prop}: ${value};`).join("\n")}\n}`;
|
|
469
469
|
}
|
|
470
470
|
|
|
471
|
-
async #buildFontaineFallbackCss(font: ReactFont, face: ReactFontFace,
|
|
471
|
+
async #buildFontaineFallbackCss(font: ReactFont, face: ReactFontFace, sourcePath: string) {
|
|
472
472
|
if (font.adjustFontFallback === false) return "";
|
|
473
473
|
const { generateFontFace, getMetricsForFamily, readMetrics, resolveCategoryFallbacks } = await import("fontaine");
|
|
474
|
-
|
|
474
|
+
// The source, not the subset: without Latin glyphs a subset measures .notdef, so xWidthAvg reads as a full em.
|
|
475
|
+
const metrics = await readMetrics(Bun.pathToFileURL(sourcePath)).catch(() => null);
|
|
475
476
|
if (!metrics) return "";
|
|
476
477
|
const fallbacks = resolveCategoryFallbacks({
|
|
477
478
|
fontFamily: font.name,
|
package/frontendBuild/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ export * from "./pagesBundleBuilder";
|
|
|
18
18
|
export * from "./pagesEntrySourceGenerator";
|
|
19
19
|
export * from "./precompressArtifacts";
|
|
20
20
|
export * from "./routeClientBuilder";
|
|
21
|
+
export * from "./routeModulePreloads";
|
|
21
22
|
export * from "./routesManifestArtifactSerializer";
|
|
22
23
|
export * from "./serverGraphFile";
|
|
23
24
|
export * from "./sourceMtimeIndex";
|
|
@@ -25,5 +26,7 @@ export * from "./ssrBaseArtifactBuilder";
|
|
|
25
26
|
export * from "./ssrDevBundler";
|
|
26
27
|
export * from "./styleContract";
|
|
27
28
|
export * from "./themeValidator";
|
|
29
|
+
export * from "./vendorImportScanner";
|
|
30
|
+
export * from "./vendorNarrowing";
|
|
28
31
|
export * from "./vendorSpecifiers";
|
|
29
32
|
export * from "./watchRootResolver";
|
|
@@ -2,6 +2,7 @@ import { mkdir } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { BaseBuildArtifact, ClientManifest, ClientManifestEntry, SsrManifest } from "akanjs/server";
|
|
4
4
|
import { SSR_DEV_CHUNK, SSR_DEV_ID_PREFIX } from "akanjs/server/hmr/csrDevManifest";
|
|
5
|
+
import { StoreBootstrap } from "../artifact/storeBootstrap";
|
|
5
6
|
import type { App } from "../commandDecorators";
|
|
6
7
|
import { createBarrelImportsPlugin } from "../transforms/barrelImportsPlugin";
|
|
7
8
|
import { loaderFor } from "../transforms/moduleSyntax";
|
|
@@ -26,7 +27,7 @@ export interface BuildRouteClientOptions {
|
|
|
26
27
|
seeds: string[];
|
|
27
28
|
/** Every route's seeds: a build that runs at all bundles the whole app, since two builds each copy shared modules. */
|
|
28
29
|
graphSeeds?: string[];
|
|
29
|
-
artifact
|
|
30
|
+
artifact?: BaseBuildArtifact;
|
|
30
31
|
knownEntries?: Set<string>;
|
|
31
32
|
routeId?: string;
|
|
32
33
|
command?: "build" | "start";
|
|
@@ -46,6 +47,8 @@ export interface BuildRouteClientResult {
|
|
|
46
47
|
clientDepsByEntry?: Record<string, string[]>;
|
|
47
48
|
/** Registry mode: the entries the manifest rows name, which the SSR dev registry must hold before they are served. */
|
|
48
49
|
registryEntries?: string[];
|
|
50
|
+
/** Chunks mode: the importmap specifiers each browser chunk imports statically. */
|
|
51
|
+
externalsByChunkUrl?: Record<string, string[]>;
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
interface BootstrapEntries {
|
|
@@ -133,6 +136,7 @@ export class RouteClientBuilder {
|
|
|
133
136
|
clientDeps: [...clientDeps].sort(),
|
|
134
137
|
clientDepsByEntry,
|
|
135
138
|
...(registry ? { registryEntries: [...acceptedEntries].sort() } : {}),
|
|
139
|
+
...(browserBundle ? { externalsByChunkUrl: Object.fromEntries(browserBundle.externalsByChunkUrl) } : {}),
|
|
136
140
|
};
|
|
137
141
|
}
|
|
138
142
|
|
|
@@ -317,7 +321,7 @@ export class RouteClientBuilder {
|
|
|
317
321
|
const originalEntry = JSON.stringify(path.resolve(args.originalEntry));
|
|
318
322
|
const namedExports = args.exportNames.filter((name) => name !== "default");
|
|
319
323
|
const lines = [
|
|
320
|
-
|
|
324
|
+
...StoreBootstrap.importLines(args.appName),
|
|
321
325
|
...(namedExports.length > 0 ? [`export { ${namedExports.join(", ")} } from ${originalEntry};`] : []),
|
|
322
326
|
];
|
|
323
327
|
if (args.exportNames.includes("default")) lines.push(`export { default } from ${originalEntry};`);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { RouteModulePreloads } from "./routeModulePreloads";
|
|
4
|
+
|
|
5
|
+
describe("RouteModulePreloads", () => {
|
|
6
|
+
const root = "/repo";
|
|
7
|
+
const entry = (name: string) => path.join(root, `apps/demo/ui/${name}.tsx`);
|
|
8
|
+
const row = (...chunks: string[]) => ({
|
|
9
|
+
id: chunks[0] ?? "",
|
|
10
|
+
chunks: chunks.flatMap((chunk) => [chunk, chunk]),
|
|
11
|
+
name: "default",
|
|
12
|
+
});
|
|
13
|
+
const preloads = new RouteModulePreloads({
|
|
14
|
+
runtimeChunks: {
|
|
15
|
+
runtime: ["/_akan/client/rsc.js"],
|
|
16
|
+
vendors: { react: ["/_akan/client/react.js"], "akanjs/store": ["/_akan/client/store.js"] },
|
|
17
|
+
},
|
|
18
|
+
entriesByRoute: new Map([
|
|
19
|
+
["/:lang", [entry("Home")]],
|
|
20
|
+
["/:lang/memo", [entry("Home"), entry("Memo")]],
|
|
21
|
+
]),
|
|
22
|
+
clientManifest: {
|
|
23
|
+
"apps/demo/ui/Home.tsx#default": row("/_akan/client/home.js", "/_akan/client/chunks/s.js"),
|
|
24
|
+
"apps/demo/ui/Memo.tsx#default": row("/_akan/client/memo.js", "/_akan/client/chunks/s.js"),
|
|
25
|
+
},
|
|
26
|
+
externalsByChunkUrl: {
|
|
27
|
+
"/_akan/client/home.js": ["react"],
|
|
28
|
+
"/_akan/client/memo.js": ["akanjs/store", "node:fs"],
|
|
29
|
+
"/_akan/client/chunks/s.js": [],
|
|
30
|
+
},
|
|
31
|
+
workspaceRoot: root,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("keeps only importmap specifiers, and only for chunks that import one", () => {
|
|
35
|
+
expect(preloads.table.chunkVendors).toEqual({
|
|
36
|
+
"/_akan/client/home.js": ["react"],
|
|
37
|
+
"/_akan/client/memo.js": ["akanjs/store"],
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("lists what each route can load: the runtime, its vendors, then its chunks", () => {
|
|
42
|
+
expect(preloads.modulesByRoute()).toEqual({
|
|
43
|
+
"/:lang": [
|
|
44
|
+
"/_akan/client/rsc.js",
|
|
45
|
+
"/_akan/client/react.js",
|
|
46
|
+
"/_akan/client/home.js",
|
|
47
|
+
"/_akan/client/chunks/s.js",
|
|
48
|
+
],
|
|
49
|
+
"/:lang/memo": [
|
|
50
|
+
"/_akan/client/rsc.js",
|
|
51
|
+
"/_akan/client/react.js",
|
|
52
|
+
"/_akan/client/store.js",
|
|
53
|
+
"/_akan/client/home.js",
|
|
54
|
+
"/_akan/client/chunks/s.js",
|
|
55
|
+
"/_akan/client/memo.js",
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ClientManifest } from "akanjs/server";
|
|
2
|
+
import { ModulePreloadPlan, type ModulePreloadTable } from "akanjs/server/artifact/modulePreloadPlan";
|
|
3
|
+
import { toClientReferencePath } from "../transforms/rscUseClientTransform";
|
|
4
|
+
|
|
5
|
+
export type RuntimeChunks = Pick<ModulePreloadTable, "runtime" | "vendors">;
|
|
6
|
+
|
|
7
|
+
export interface RouteModulePreloadsInput {
|
|
8
|
+
runtimeChunks: RuntimeChunks;
|
|
9
|
+
entriesByRoute: Map<string, string[]>;
|
|
10
|
+
clientManifest: ClientManifest;
|
|
11
|
+
externalsByChunkUrl: Record<string, string[]>;
|
|
12
|
+
workspaceRoot: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class RouteModulePreloads {
|
|
16
|
+
readonly table: ModulePreloadTable;
|
|
17
|
+
#input: RouteModulePreloadsInput;
|
|
18
|
+
#chunksByReferenceId = new Map<string, Set<string>>();
|
|
19
|
+
|
|
20
|
+
constructor(input: RouteModulePreloadsInput) {
|
|
21
|
+
this.#input = input;
|
|
22
|
+
for (const [key, row] of Object.entries(input.clientManifest)) {
|
|
23
|
+
const chunks = this.#chunksByReferenceId.getOrInsert(key.slice(0, key.lastIndexOf("#")), new Set());
|
|
24
|
+
for (const url of row.chunks) chunks.add(url);
|
|
25
|
+
}
|
|
26
|
+
this.table = {
|
|
27
|
+
...input.runtimeChunks,
|
|
28
|
+
chunkVendors: Object.fromEntries(
|
|
29
|
+
Object.entries(input.externalsByChunkUrl)
|
|
30
|
+
.map(([url, specifiers]): [string, string[]] => [
|
|
31
|
+
url,
|
|
32
|
+
specifiers.filter((specifier) => specifier in input.runtimeChunks.vendors),
|
|
33
|
+
])
|
|
34
|
+
.filter(([, specifiers]) => specifiers.length > 0),
|
|
35
|
+
),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
modulesByRoute(): Record<string, string[]> {
|
|
40
|
+
const plan = new ModulePreloadPlan(this.table);
|
|
41
|
+
return Object.fromEntries(
|
|
42
|
+
[...this.#input.entriesByRoute].map(([routeId, entries]) => [
|
|
43
|
+
routeId,
|
|
44
|
+
plan.urlsOf(
|
|
45
|
+
entries.flatMap((entry) => [
|
|
46
|
+
...(this.#chunksByReferenceId.get(toClientReferencePath(entry, this.#input.workspaceRoot)) ?? []),
|
|
47
|
+
]),
|
|
48
|
+
),
|
|
49
|
+
]),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -12,13 +12,16 @@ import { DictionaryAssetBuilder } from "./dictionaryAssetBuilder";
|
|
|
12
12
|
import { FontOptimizer } from "./fontOptimizer";
|
|
13
13
|
import { PagesBundleBuilder } from "./pagesBundleBuilder";
|
|
14
14
|
import { RouteClientBuilder } from "./routeClientBuilder";
|
|
15
|
-
import {
|
|
15
|
+
import type { RuntimeChunks } from "./routeModulePreloads";
|
|
16
|
+
import { VendorNarrowing } from "./vendorNarrowing";
|
|
17
|
+
import type { VendorSpecifier } from "./vendorSpecifiers";
|
|
16
18
|
|
|
17
19
|
export interface BuildSsrBaseArtifactResult {
|
|
18
20
|
artifact: BaseBuildArtifact;
|
|
19
21
|
seedIndex: RouteSeedIndex;
|
|
20
22
|
cssCompiler: CssCompiler;
|
|
21
23
|
optimizedFonts: Awaited<ReturnType<FontOptimizer["optimize"]>>;
|
|
24
|
+
runtimeChunks: RuntimeChunks;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
export function prepareCssAsset(command: "build" | "start", basePath: string, cssText: string): string {
|
|
@@ -32,12 +35,24 @@ export class SsrBaseArtifactBuilder {
|
|
|
32
35
|
#absArtifactDir: string;
|
|
33
36
|
#started = Date.now();
|
|
34
37
|
#serverEnv: BunPlugin | undefined;
|
|
38
|
+
#vendorExports: Partial<Record<VendorSpecifier, readonly string[]>>;
|
|
35
39
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
40
|
+
/**
|
|
41
|
+
* `serverEnv`: the build's server env plugin, so the route bundle carries the env files the backend does and no more.
|
|
42
|
+
* `vendorExports`: the only names a vendor entry exports, for a build whose every client module is already bundled.
|
|
43
|
+
*/
|
|
44
|
+
constructor(
|
|
45
|
+
app: App,
|
|
46
|
+
command: "build" | "start" = "start",
|
|
47
|
+
{
|
|
48
|
+
serverEnv,
|
|
49
|
+
vendorExports = {},
|
|
50
|
+
}: { serverEnv?: BunPlugin; vendorExports?: Partial<Record<VendorSpecifier, readonly string[]>> } = {},
|
|
51
|
+
) {
|
|
38
52
|
this.#app = app;
|
|
39
53
|
this.#command = command;
|
|
40
54
|
this.#serverEnv = serverEnv;
|
|
55
|
+
this.#vendorExports = vendorExports;
|
|
41
56
|
this.#artifactDir = `${command === "build" ? app.dist.cwdPath : app.cwdPath}/.akan/artifact`;
|
|
42
57
|
this.#absArtifactDir = path.resolve(this.#artifactDir);
|
|
43
58
|
}
|
|
@@ -50,7 +65,7 @@ export class SsrBaseArtifactBuilder {
|
|
|
50
65
|
|
|
51
66
|
async build(): Promise<BuildSsrBaseArtifactResult> {
|
|
52
67
|
const akanConfig = await this.#app.getConfig();
|
|
53
|
-
const { rscClientUrl, rscRuntimeClientManifest, rscRuntimeSsrManifest, vendorMap } =
|
|
68
|
+
const { rscClientUrl, rscRuntimeClientManifest, rscRuntimeSsrManifest, vendorMap, runtimeChunks } =
|
|
54
69
|
await this.#buildRuntimeClientEntries();
|
|
55
70
|
const pageKeys = await this.#app.getPageKeys();
|
|
56
71
|
this.#app.verbose(`[base-artifact] discovered ${pageKeys.length} route files under ${this.#app.cwdPath}/page`);
|
|
@@ -106,23 +121,28 @@ export class SsrBaseArtifactBuilder {
|
|
|
106
121
|
await Bun.write(path.join(this.#absArtifactDir, "base-artifact.json"), `${JSON.stringify(artifact, null, 2)}\n`);
|
|
107
122
|
this.#app.verbose(`[base-artifact] complete in ${Date.now() - this.#started}ms`);
|
|
108
123
|
|
|
109
|
-
return { artifact, seedIndex, cssCompiler, optimizedFonts };
|
|
124
|
+
return { artifact, seedIndex, cssCompiler, optimizedFonts, runtimeChunks };
|
|
110
125
|
}
|
|
111
126
|
|
|
112
127
|
async #buildRuntimeClientEntries(): Promise<
|
|
113
128
|
Pick<BaseBuildArtifact, "rscClientUrl" | "rscRuntimeClientManifest" | "rscRuntimeSsrManifest"> & {
|
|
114
129
|
vendorMap: Record<VendorSpecifier, string>;
|
|
130
|
+
runtimeChunks: RuntimeChunks;
|
|
115
131
|
}
|
|
116
132
|
> {
|
|
117
133
|
const akanServerPath = await this.#resolveAkanServerPath();
|
|
118
134
|
const rscClientEntry = path.resolve(akanServerPath, "rscClient.tsx");
|
|
119
135
|
const rscSegmentOutletEntry = path.resolve(akanServerPath, "rscSegmentOutlet.tsx");
|
|
120
|
-
const vendorEntries =
|
|
121
|
-
specifier,
|
|
122
|
-
absPath: path.resolve(akanServerPath, "vendor", `${specifier.replaceAll("/", "-").replaceAll(".", "-")}.ts`),
|
|
123
|
-
}));
|
|
136
|
+
const vendorEntries = VendorNarrowing.entriesIn(akanServerPath);
|
|
124
137
|
const entries = [rscClientEntry, rscSegmentOutletEntry, ...vendorEntries.map((v) => v.absPath)];
|
|
125
|
-
const
|
|
138
|
+
const narrowing = new VendorNarrowing(path.dirname(akanServerPath), this.#vendorExports);
|
|
139
|
+
const clientBundle = await new ClientEntriesBundler({
|
|
140
|
+
app: this.#app,
|
|
141
|
+
entries,
|
|
142
|
+
command: this.#command,
|
|
143
|
+
entryExports: narrowing.entryExports(vendorEntries),
|
|
144
|
+
plugins: await narrowing.plugins(),
|
|
145
|
+
}).bundle();
|
|
126
146
|
const ssrBundle = await new ClientEntriesBundler({
|
|
127
147
|
app: this.#app,
|
|
128
148
|
entries: [rscSegmentOutletEntry],
|
|
@@ -158,11 +178,16 @@ export class SsrBaseArtifactBuilder {
|
|
|
158
178
|
vendorEntries.map(({ specifier, absPath }) => [specifier, clientBundle.entryUrlsByAbsPath.get(absPath) ?? ""]),
|
|
159
179
|
) as Record<VendorSpecifier, string>;
|
|
160
180
|
this.#app.verbose(`[base-artifact] rscClientUrl=${rscClientUrl} vendors=${Object.keys(vendorMap).length}`);
|
|
181
|
+
const chunksOf = (absPath: string) => clientBundle.entryChunkUrlsByAbsPath.get(absPath) ?? [];
|
|
161
182
|
return {
|
|
162
183
|
rscClientUrl,
|
|
163
184
|
rscRuntimeClientManifest: clientBundle.manifest,
|
|
164
185
|
rscRuntimeSsrManifest,
|
|
165
186
|
vendorMap,
|
|
187
|
+
runtimeChunks: {
|
|
188
|
+
runtime: [...new Set([...chunksOf(rscClientEntry), ...chunksOf(rscSegmentOutletEntry)])],
|
|
189
|
+
vendors: Object.fromEntries(vendorEntries.map(({ specifier, absPath }) => [specifier, chunksOf(absPath)])),
|
|
190
|
+
},
|
|
166
191
|
};
|
|
167
192
|
}
|
|
168
193
|
async #resolveAkanServerPath() {
|