@akanjs/devkit 3.1.2-rc.4 → 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 +1 -1
- package/apiReference.test.ts +37 -0
- package/apiReference.ts +240 -0
- package/applicationBuildReporter.test.ts +25 -3
- package/applicationBuildReporter.ts +8 -1
- package/applicationBuildRunner.test.ts +34 -14
- package/applicationBuildRunner.ts +76 -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 +16 -7
- package/codeAgent/skills/akan-service/SKILL.md +48 -19
- 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.ts +6 -2
- package/incrementalBuilder/builderGraphPayloads.ts +60 -0
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +65 -1
- package/incrementalBuilder/incrementalBuilder.proc.ts +5 -36
- package/integration/devStability.integration.test.ts +1 -1
- package/integration/devStabilityHarness.test.ts +1 -1
- package/lint/no-deep-internal-import.grit +1 -1
- 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/workflow/plan.ts +79 -17
- package/workflow/types.ts +1 -0
- package/workspaceGraph.test.ts +58 -0
- package/workspaceGraph.ts +32 -3
- package/workspaceLayout.ts +1 -0
- /package/integration/{devStabilityHarness.ts → devStabilityHarness.fixture.ts} +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { realpath } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { tempDirs, writeText as write } from "../testHelpers";
|
|
4
5
|
import { createTsconfigPackageResolver } from "../transforms/barrelImportsPlugin";
|
|
@@ -9,8 +10,86 @@ import { RouteClientBuilder } from "./routeClientBuilder";
|
|
|
9
10
|
|
|
10
11
|
const makeTempRoot = tempDirs("akan-client-entry-");
|
|
11
12
|
|
|
13
|
+
const fixtureBarrels = ["@apps/demo/client", "@libs/kit/client"];
|
|
14
|
+
|
|
15
|
+
const writeBarrelFixture = async () => {
|
|
16
|
+
const root = await makeTempRoot();
|
|
17
|
+
const appDir = path.join(root, "apps/demo");
|
|
18
|
+
const libDir = path.join(root, "libs/kit");
|
|
19
|
+
const paths = { "@apps/*": ["./apps/*"], "@libs/*": ["./libs/*"] };
|
|
20
|
+
await write(path.join(root, "tsconfig.json"), JSON.stringify({ compilerOptions: { baseUrl: ".", paths } }));
|
|
21
|
+
const writeSys = async (dir: string, prefix: string, modules: string[]) => {
|
|
22
|
+
await write(
|
|
23
|
+
path.join(dir, "client.ts"),
|
|
24
|
+
[
|
|
25
|
+
'export * as cnst from "./lib/cnst";',
|
|
26
|
+
'export { msg, Err, usePage, fetch, sig } from "./lib/useClient";',
|
|
27
|
+
'export { st, RootStore } from "./lib/st";',
|
|
28
|
+
'export * as store from "./lib/st";',
|
|
29
|
+
...modules.map((name) => `export { ${name} } from "./lib/${name.toLowerCase()}";`),
|
|
30
|
+
"",
|
|
31
|
+
].join("\n"),
|
|
32
|
+
);
|
|
33
|
+
await write(
|
|
34
|
+
path.join(dir, "lib/useClient.ts"),
|
|
35
|
+
`const runtime = Object.freeze({ mark: "${prefix}runtime-marker", msg: 1, Err: 2, usePage: 3, fetch: 4, sig: 5 });\nexport const { msg, Err, usePage, fetch, sig } = runtime;\n`,
|
|
36
|
+
);
|
|
37
|
+
await write(
|
|
38
|
+
path.join(dir, "lib/cnst.ts"),
|
|
39
|
+
`export const Sample = Object.freeze({ mark: "${prefix}cnst-marker" });\n`,
|
|
40
|
+
);
|
|
41
|
+
for (const name of modules)
|
|
42
|
+
await write(
|
|
43
|
+
path.join(dir, `lib/${name.toLowerCase()}/index.ts`),
|
|
44
|
+
`import * as Zone from "./${name}.Zone";\nexport const ${name} = { Zone };\n`,
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
await writeSys(libDir, "lib-", ["Widget"]);
|
|
48
|
+
await write(
|
|
49
|
+
path.join(libDir, "lib/st.ts"),
|
|
50
|
+
'export class RootStore {}\nexport const st = Object.freeze({ mark: "lib-store-marker" });\n',
|
|
51
|
+
);
|
|
52
|
+
await write(
|
|
53
|
+
path.join(libDir, "lib/widget/Widget.Zone.tsx"),
|
|
54
|
+
'"use client";\nexport const Card = Object.freeze({ mark: "lib-ui-marker" });\n',
|
|
55
|
+
);
|
|
56
|
+
await write(
|
|
57
|
+
path.join(libDir, "ui/Field.tsx"),
|
|
58
|
+
'"use client";\nimport { cnst as kitCnst } from "@libs/kit/client";\nexport const Field = () => [kitCnst.Sample, "field-marker"];\n',
|
|
59
|
+
);
|
|
60
|
+
await writeSys(appDir, "", ["Memo", "Other"]);
|
|
61
|
+
await write(
|
|
62
|
+
path.join(appDir, "lib/st.ts"),
|
|
63
|
+
'import { RootStore as kit } from "@libs/kit/client";\nexport class RootStore {}\nexport const st = Object.freeze({ mark: "store-marker", kit });\n',
|
|
64
|
+
);
|
|
65
|
+
await write(
|
|
66
|
+
path.join(appDir, "lib/memo/Memo.Zone.tsx"),
|
|
67
|
+
'"use client";\nimport { type cnst, Memo, st } from "@apps/demo/client";\nexport const Card = (props: { sample?: typeof cnst.Sample }) => [st, Memo, props, "memo-ui-marker"];\n',
|
|
68
|
+
);
|
|
69
|
+
await write(
|
|
70
|
+
path.join(appDir, "lib/other/Other.Zone.tsx"),
|
|
71
|
+
'"use client";\nexport const Card = Object.freeze({ mark: "other-ui-marker" });\n',
|
|
72
|
+
);
|
|
73
|
+
await write(path.join(appDir, "ui/A.tsx"), '"use client";\nexport const A = () => "a";\n');
|
|
74
|
+
const app = {
|
|
75
|
+
name: "demo",
|
|
76
|
+
cwdPath: appDir,
|
|
77
|
+
dist: { cwdPath: path.join(root, "dist/apps/demo") },
|
|
78
|
+
workspace: { workspaceRoot: root },
|
|
79
|
+
getConfig: async () => ({ barrelImports: fixtureBarrels, optimizeImports: [] }),
|
|
80
|
+
getTsConfig: async () => ({ compilerOptions: { paths } }),
|
|
81
|
+
getPublicEnv: () => ({}),
|
|
82
|
+
} as never;
|
|
83
|
+
return { root, appDir, app };
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const bundledText = async (dir: string) =>
|
|
87
|
+
(
|
|
88
|
+
await Promise.all([...new Bun.Glob("**/*.js").scanSync(dir)].map((file) => Bun.file(path.join(dir, file)).text()))
|
|
89
|
+
).join("\n");
|
|
90
|
+
|
|
12
91
|
describe("route client store bootstrap", () => {
|
|
13
|
-
test("wraps client entries with
|
|
92
|
+
test("wraps client entries with the client runtime and store bootstrap before re-exporting components", () => {
|
|
14
93
|
const original = path.resolve("/repo/pkgs/akanjs/ui/Model/NewWrapper_Client.tsx");
|
|
15
94
|
const source = RouteClientBuilder.createStoreBootstrapEntrySource({
|
|
16
95
|
appName: "akan",
|
|
@@ -20,7 +99,8 @@ describe("route client store bootstrap", () => {
|
|
|
20
99
|
|
|
21
100
|
expect(source).toBe(
|
|
22
101
|
[
|
|
23
|
-
'import "@apps/akan/
|
|
102
|
+
'import "@apps/akan/lib/useClient";',
|
|
103
|
+
'import "@apps/akan/lib/st";',
|
|
24
104
|
`export { NewWrapper_Client } from ${JSON.stringify(original)};`,
|
|
25
105
|
`export { default } from ${JSON.stringify(original)};`,
|
|
26
106
|
"",
|
|
@@ -183,6 +263,116 @@ describe("route client store bootstrap", () => {
|
|
|
183
263
|
expect(carriers.filter(Boolean)).toHaveLength(1);
|
|
184
264
|
});
|
|
185
265
|
|
|
266
|
+
test("names each entry's static chunks and the importmap specifiers every chunk imports", async () => {
|
|
267
|
+
const root = await realpath(await makeTempRoot());
|
|
268
|
+
const appDir = path.join(root, "apps/demo");
|
|
269
|
+
const entryA = path.join(appDir, "ui/A.tsx");
|
|
270
|
+
const entryB = path.join(appDir, "ui/B.tsx");
|
|
271
|
+
await write(
|
|
272
|
+
entryA,
|
|
273
|
+
'"use client";\nimport { useState } from "react";\nimport { mark } from "../common/shared";\nexport const A = () => [useState, mark];\n',
|
|
274
|
+
);
|
|
275
|
+
await write(
|
|
276
|
+
entryB,
|
|
277
|
+
'"use client";\nimport { mark } from "../common/shared";\nexport const B = () => import("../common/lazy").then(() => mark);\n',
|
|
278
|
+
);
|
|
279
|
+
await write(
|
|
280
|
+
path.join(appDir, "common/shared.ts"),
|
|
281
|
+
'import { tag } from "akanjs/base";\nexport const mark = tag;\n',
|
|
282
|
+
);
|
|
283
|
+
await write(path.join(appDir, "common/lazy.ts"), 'import "tailwind-merge";\nexport const lazy = 1;\n');
|
|
284
|
+
const app = {
|
|
285
|
+
name: "demo",
|
|
286
|
+
cwdPath: appDir,
|
|
287
|
+
dist: { cwdPath: path.join(root, "dist/apps/demo") },
|
|
288
|
+
workspace: { workspaceRoot: root },
|
|
289
|
+
getConfig: async () => ({ barrelImports: [], optimizeImports: [] }),
|
|
290
|
+
getPublicEnv: () => ({}),
|
|
291
|
+
} as never;
|
|
292
|
+
|
|
293
|
+
const bundle = await new ClientEntriesBundler({
|
|
294
|
+
app,
|
|
295
|
+
entries: [entryA, entryB],
|
|
296
|
+
external: ["react", "akanjs/base", "tailwind-merge"],
|
|
297
|
+
command: "build",
|
|
298
|
+
}).bundle();
|
|
299
|
+
|
|
300
|
+
const [urlA, sharedA] = bundle.entryChunkUrlsByAbsPath.get(entryA) ?? [];
|
|
301
|
+
const [urlB, sharedB] = bundle.entryChunkUrlsByAbsPath.get(entryB) ?? [];
|
|
302
|
+
expect(bundle.entryChunkUrlsByAbsPath.get(entryA)).toHaveLength(2);
|
|
303
|
+
expect(bundle.entryChunkUrlsByAbsPath.get(entryB)).toHaveLength(2);
|
|
304
|
+
expect(sharedA).toBe(sharedB);
|
|
305
|
+
expect(bundle.externalsByChunkUrl.get(urlA ?? "")).toEqual(["react"]);
|
|
306
|
+
expect(bundle.externalsByChunkUrl.get(urlB ?? "")).toEqual([]);
|
|
307
|
+
expect(bundle.externalsByChunkUrl.get(sharedA ?? "")).toEqual(["akanjs/base"]);
|
|
308
|
+
expect([...bundle.externalsByChunkUrl.values()].flat()).toContain("tailwind-merge");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("a route carries the client runtime and stores, but no module UI it never imports", async () => {
|
|
312
|
+
const { appDir, app } = await writeBarrelFixture();
|
|
313
|
+
|
|
314
|
+
await new RouteClientBuilder({
|
|
315
|
+
app,
|
|
316
|
+
seeds: [],
|
|
317
|
+
entries: [path.join(appDir, "ui/A.tsx")],
|
|
318
|
+
artifact: {} as never,
|
|
319
|
+
browser: "chunks",
|
|
320
|
+
}).build();
|
|
321
|
+
|
|
322
|
+
const bundled = await bundledText(path.join(appDir, ".akan/artifact/client"));
|
|
323
|
+
for (const marker of ["runtime-marker", "store-marker", "lib-runtime-marker", "lib-store-marker"])
|
|
324
|
+
expect(bundled).toContain(marker);
|
|
325
|
+
for (const marker of ["memo-ui-marker", "other-ui-marker", "lib-ui-marker"]) expect(bundled).not.toContain(marker);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("a route rendering one module's UI carries no other module's or lib's UI", async () => {
|
|
329
|
+
const { appDir, app } = await writeBarrelFixture();
|
|
330
|
+
|
|
331
|
+
await new RouteClientBuilder({
|
|
332
|
+
app,
|
|
333
|
+
seeds: [],
|
|
334
|
+
entries: [path.join(appDir, "lib/memo/Memo.Zone.tsx")],
|
|
335
|
+
artifact: {} as never,
|
|
336
|
+
browser: "chunks",
|
|
337
|
+
}).build();
|
|
338
|
+
|
|
339
|
+
const bundled = await bundledText(path.join(appDir, ".akan/artifact/client"));
|
|
340
|
+
expect(bundled).toContain("memo-ui-marker");
|
|
341
|
+
expect(bundled).toContain("store-marker");
|
|
342
|
+
expect(bundled).not.toContain("other-ui-marker");
|
|
343
|
+
expect(bundled).not.toContain("lib-ui-marker");
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("a route reaching a lib's cnst through its client barrel carries no module UI", async () => {
|
|
347
|
+
const { root, appDir, app } = await writeBarrelFixture();
|
|
348
|
+
|
|
349
|
+
await new RouteClientBuilder({
|
|
350
|
+
app,
|
|
351
|
+
seeds: [],
|
|
352
|
+
entries: [path.join(root, "libs/kit/ui/Field.tsx")],
|
|
353
|
+
artifact: {} as never,
|
|
354
|
+
browser: "chunks",
|
|
355
|
+
}).build();
|
|
356
|
+
|
|
357
|
+
const bundled = await bundledText(path.join(appDir, ".akan/artifact/client"));
|
|
358
|
+
expect(bundled).toContain("field-marker");
|
|
359
|
+
expect(bundled).toContain("lib-cnst-marker");
|
|
360
|
+
for (const marker of ["memo-ui-marker", "other-ui-marker", "lib-ui-marker"]) expect(bundled).not.toContain(marker);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("an import of the store through a client barrel discovers no module UI as a client entry", async () => {
|
|
364
|
+
const { appDir, app } = await writeBarrelFixture();
|
|
365
|
+
const seed = path.join(appDir, "page/_index.tsx");
|
|
366
|
+
await write(seed, 'import { st } from "@apps/demo/client";\nexport default function Page() { return st; }\n');
|
|
367
|
+
|
|
368
|
+
const discovery = new GraphClientEntryDiscovery(
|
|
369
|
+
{ barrelImports: fixtureBarrels },
|
|
370
|
+
await createTsconfigPackageResolver(app),
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
expect(await discovery.discover([seed])).toEqual([]);
|
|
374
|
+
});
|
|
375
|
+
|
|
186
376
|
test("discovers client entries from installed akanjs package sources", async () => {
|
|
187
377
|
const root = await makeTempRoot();
|
|
188
378
|
const seed = path.join(root, "apps/demo/page/_index.tsx");
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { App } from "../commandDecorators";
|
|
2
2
|
|
|
3
|
+
//? A package's `default` export can pick its build by NODE_ENV at run time (Lexical), which keeps every export of that
|
|
4
|
+
//? build; its `production` export is the build itself, so what a page leaves unused is dropped.
|
|
5
|
+
export const bundleConditions = (command: "build" | "start") => (command === "build" ? ["production"] : []);
|
|
6
|
+
|
|
3
7
|
export const bundleDefine = (app: App, command: "build" | "start", renderEnv: "ssr" | "csr") => {
|
|
4
8
|
const nodeEnv = command === "build" ? "production" : (process.env.NODE_ENV ?? "development");
|
|
5
9
|
return {
|
|
@@ -59,6 +59,8 @@ export interface BundleClientEntriesInternalOptions extends BundleClientEntriesO
|
|
|
59
59
|
externalSubpaths?: readonly string[];
|
|
60
60
|
externalAliases?: Partial<Record<string, string>>;
|
|
61
61
|
command?: "build" | "start";
|
|
62
|
+
/** Per absolute entry, the only names its alias re-exports, in place of every name the entry declares. */
|
|
63
|
+
entryExports?: Partial<Record<string, readonly string[]>>;
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
export interface BundleClientEntriesResult {
|
|
@@ -67,12 +69,16 @@ export interface BundleClientEntriesResult {
|
|
|
67
69
|
entryUrlsByAbsPath: Map<string, string>;
|
|
68
70
|
entryOutputAbsByAbsPath: Map<string, string>;
|
|
69
71
|
entryDepsByAbsPath: Map<string, string[]>;
|
|
72
|
+
/** Each entry's served URL and the chunks it imports statically, the entry first. */
|
|
73
|
+
entryChunkUrlsByAbsPath: Map<string, string[]>;
|
|
74
|
+
externalsByChunkUrl: Map<string, string[]>;
|
|
70
75
|
/** Workspace-relative client reference id per absolute source entry. */
|
|
71
76
|
clientReferenceIdByAbsPath: Map<string, string>;
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
export type AkanConfig = Awaited<ReturnType<App["getConfig"]>>;
|
|
75
|
-
export type ScannedImport = { path: string; kind?: string };
|
|
80
|
+
export type ScannedImport = { path: string; kind?: string; external?: boolean };
|
|
81
|
+
export type MetafileInput = { imports?: ScannedImport[] };
|
|
76
82
|
export type MetafileOutput = {
|
|
77
83
|
imports: ScannedImport[];
|
|
78
84
|
entryPoint?: string;
|
|
@@ -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";
|