@akanjs/cli 3.0.0-alpha.11 → 3.0.0-alpha.110

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.
Files changed (140) hide show
  1. package/.build-stamp +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +1 -1
  4. package/{abstractCompactor-e5yq60qz.js → abstractCompactor-mjh5ppsq.js} +3 -5
  5. package/{agent.command-a3n15c6t.js → agent.command-d2gfxfck.js} +11 -13
  6. package/{application.command-rdm3et0g.js → application.command-pj2m7584.js} +65 -38
  7. package/{applicationBuildRunner-6m8n71w4.js → applicationBuildRunner-kxwbssgn.js} +27 -23
  8. package/{applicationReleasePackager-gg8ccbeh.js → applicationReleasePackager-4yasxwv9.js} +8 -9
  9. package/buildBatch.proc.js +12 -14
  10. package/capacitorApp-36rbr858.js +12 -0
  11. package/{cloud.command-56aeekqn.js → cloud.command-gxqp8xdv.js} +15 -18
  12. package/commandManifest.json +1 -1
  13. package/{context.command-ysxawe7h.js → context.command-ez8a0zyz.js} +19 -23
  14. package/dependencyScanner-shs82ynp.js +8 -0
  15. package/devTui-40ggmd99.js +553 -0
  16. package/devTuiView-c08ccbat.js +9 -0
  17. package/getDirname-dr3jz1xg.js +7 -0
  18. package/{guideline.command-yqf9a372.js → guideline.command-xmqwx8zm.js} +24 -22
  19. package/guidelines/agentRule/agentRule.generate.json +31 -0
  20. package/guidelines/agentRule/agentRule.instruction.md +531 -0
  21. package/guidelines/conventions/conventions.instruction.md +493 -369
  22. package/guidelines/framework/framework.generate.json +3 -2
  23. package/guidelines/framework/framework.instruction.md +0 -9
  24. package/guidelines/mcpRule/mcpRule.generate.json +35 -0
  25. package/guidelines/mcpRule/mcpRule.instruction.md +460 -0
  26. package/guidelines/modelStore/modelStore.instruction.md +12 -0
  27. package/guidelines/modelTemplate/modelTemplate.instruction.md +5 -0
  28. package/guidelines/queryRule/queryRule.generate.json +31 -0
  29. package/guidelines/queryRule/queryRule.instruction.md +178 -0
  30. package/guidelines/runtimeRule/runtimeRule.generate.json +25 -0
  31. package/guidelines/runtimeRule/runtimeRule.instruction.md +220 -0
  32. package/guidelines/ssrRule/ssrRule.instruction.md +51 -5
  33. package/guidelines/transportRule/transportRule.generate.json +31 -0
  34. package/guidelines/transportRule/transportRule.instruction.md +60 -0
  35. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +54 -764
  36. package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +33 -0
  37. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +467 -0
  38. package/incrementalBuilder.proc.js +26 -25
  39. package/{index-8pd02prk.js → index-259dsnz3.js} +58 -25
  40. package/{index-yzsxc30b.js → index-2tv3fdx4.js} +975 -234
  41. package/index-3d9y066c.js +76 -0
  42. package/{index-76rn3g2c.js → index-47pj3aee.js} +4 -4
  43. package/{index-m90fn8xe.js → index-4edzf1gr.js} +32 -32
  44. package/{index-3789fdpg.js → index-600zhnve.js} +94 -61
  45. package/{index-1577bej2.js → index-6wwzs1bc.js} +2 -2
  46. package/{index-cv9cxs5k.js → index-7737gxcp.js} +18 -18
  47. package/{index-a5cs7d8r.js → index-8few9nyh.js} +13 -13
  48. package/{index-mewacxye.js → index-8pm30m73.js} +57 -57
  49. package/{index-wnp7hwq7.js → index-92zztqy1.js} +7 -7
  50. package/{index-fgc8r6dj.js → index-a5vmm6x4.js} +3 -1
  51. package/{index-cav1azma.js → index-a64esmtb.js} +1106 -1369
  52. package/{index-nwqqssnb.js → index-aj7tyh5w.js} +26 -23
  53. package/{index-xys926f2.js → index-by5gpp8x.js} +4 -4
  54. package/{index-yzmqvg23.js → index-fz5q3gd7.js} +8 -8
  55. package/{index-kcsa1zff.js → index-gyd220p2.js} +4 -4
  56. package/{index-j37qq1f2.js → index-h7yw2z9n.js} +7 -7
  57. package/{index-bjpxzr6s.js → index-hwkygn9s.js} +51 -3
  58. package/index-j9yhg404.js +159 -0
  59. package/{index-46tjzh6s.js → index-kcxmv48c.js} +29 -29
  60. package/{index-0cj2zxbm.js → index-kn1h7p1x.js} +12 -11
  61. package/{index-e4b2ae6b.js → index-ktb5njgy.js} +41 -34
  62. package/{index-etwh7sd3.js → index-mak3cd28.js} +100 -32
  63. package/{index-xjd3rqzj.js → index-n68jx79w.js} +6 -15
  64. package/{index-z1s5f4e0.js → index-ne2qy0nk.js} +36 -37
  65. package/{index-65aeh789.js → index-pnqbkf5p.js} +1 -1
  66. package/{index-67546d0j.js → index-q0y2jq9j.js} +14 -2
  67. package/{index-4mgkgba3.js → index-rwg2aa4f.js} +14 -14
  68. package/{index-bzznxgcx.js → index-v0qh106m.js} +1002 -128
  69. package/{index-j9cxndc0.js → index-w8kmx1b6.js} +20 -11
  70. package/{index-h5whaj26.js → index-ws5e0j51.js} +16 -12
  71. package/{index-9va8zayb.js → index-wt7qrtd4.js} +27 -27
  72. package/index.js +22 -25
  73. package/{library.command-x6t29a30.js → library.command-nbsky672.js} +11 -10
  74. package/{localRegistry.command-649g1n7b.js → localRegistry.command-ts6t5e5a.js} +20 -23
  75. package/{module.command-8w8ys5t3.js → module.command-29vqzrh8.js} +13 -16
  76. package/{package.command-zgy7f3fz.js → package.command-axv0abmn.js} +8 -10
  77. package/package.json +5 -7
  78. package/{page.command-yjmnkyk2.js → page.command-vs9s3zrh.js} +7 -9
  79. package/{primitive.command-gfqaagsg.js → primitive.command-cbxmqbtc.js} +13 -16
  80. package/{quality.command-ec8r1dn3.js → quality.command-gmbs7nws.js} +184 -334
  81. package/recipeScanner-fpagtgc1.js +11 -0
  82. package/{repair.command-p9ey6mkm.js → repair.command-bqsjptsj.js} +12 -14
  83. package/{routeSourceValidator-e4jmbfcx.js → routeSourceValidator-apyqw7wk.js} +90 -19
  84. package/{scalar.command-9dhmqhtm.js → scalar.command-6k1vhe9s.js} +11 -14
  85. package/subspace.command-35a3t6vv.js +755 -0
  86. package/templates/app/env/env.client.debug.ts.template +1 -5
  87. package/templates/app/env/env.client.develop.ts.template +1 -5
  88. package/templates/app/env/env.client.local.ts.template +1 -5
  89. package/templates/app/env/env.client.main.ts.template +1 -5
  90. package/templates/app/env/env.client.testing.ts.template +1 -5
  91. package/templates/app/env/env.client.type.ts +2 -6
  92. package/templates/app/env/env.server.debug.ts.template +1 -5
  93. package/templates/app/env/env.server.develop.ts.template +1 -5
  94. package/templates/app/env/env.server.local.ts.template +1 -5
  95. package/templates/app/env/env.server.main.ts.template +1 -5
  96. package/templates/app/main.ts +3 -1
  97. package/templates/app/page/_index.tsx +3 -3
  98. package/templates/app/page/_layout.tsx +31 -32
  99. package/templates/appSample/lib/task/task.signal.ts +11 -6
  100. package/templates/appSample/page/task/[taskId]/_index.tsx +22 -27
  101. package/templates/appSample/page/task/[taskId]/edit.tsx +25 -30
  102. package/templates/appSample/page/task/_index.tsx +3 -7
  103. package/templates/appSample/page/task/_layout.tsx +4 -15
  104. package/templates/appSample/page/task/new.tsx +4 -10
  105. package/templates/appSample/srvkit/AuthGuard.ts +2 -2
  106. package/templates/crudPages/[__model__Id]/edit/page.tsx +27 -29
  107. package/templates/crudPages/[__model__Id]/page.tsx +24 -32
  108. package/templates/crudPages/new/page.tsx +26 -25
  109. package/templates/crudPages/page.tsx +27 -26
  110. package/templates/crudSinglePage/page.tsx +26 -21
  111. package/templates/env/_env.server.type.ts +7 -5
  112. package/templates/lib/db.ts +1 -1
  113. package/templates/lib/dict.ts +1 -1
  114. package/templates/lib/sig.ts +5 -1
  115. package/templates/lib/srv.ts +6 -2
  116. package/templates/module/__model__.dictionary.ts +4 -1
  117. package/templates/module/__model__.signal.ts +2 -3
  118. package/templates/module/__model__.store.ts +1 -2
  119. package/templates/workspaceRoot/.gitignore.template +0 -1
  120. package/templates/workspaceRoot/.husky/pre-commit.template +16 -0
  121. package/templates/workspaceRoot/.vscode/settings.json.template +1 -1
  122. package/templates/workspaceRoot/AGENTS.md.template +4 -0
  123. package/templates/workspaceRoot/CLAUDE.md.template +19 -0
  124. package/templates/workspaceRoot/biome.json.template +1 -1
  125. package/templates/workspaceRoot/docs/AI-DEVELOPMENT.md.template +10 -14
  126. package/templates/workspaceRoot/docs/GENERATED.md.template +12 -10
  127. package/templates/workspaceRoot/package.json.template +2 -4
  128. package/typeChecker-3xgrzc2v.js +7 -0
  129. package/typecheck.proc.js +4 -5
  130. package/{workflow.command-ycwc6zr6.js → workflow.command-nwpcdc8p.js} +22 -25
  131. package/{workspace.command-xhyhajxa.js → workspace.command-aczjxzbf.js} +64 -66
  132. package/capacitorApp-mr81qdxq.js +0 -61
  133. package/dependencyScanner-f6grrep3.js +0 -9
  134. package/getDirname-m9s0cmhq.js +0 -8
  135. package/index-mxvakhsm.js +0 -66
  136. package/index-r24hmh0q.js +0 -4
  137. package/recipeScanner-tghkzrrt.js +0 -12
  138. package/typeChecker-kravn7ns.js +0 -8
  139. /package/{index-0wae5ebk.js → index-meh558jy.js} +0 -0
  140. /package/{index-ss469dec.js → index-nz8prx9g.js} +0 -0
@@ -1,284 +1,33 @@
1
1
  // @bun
2
+ import {
3
+ AsyncDefaultExportDetector,
4
+ resolveSsrPageEntriesForApp
5
+ } from "./index-2tv3fdx4.js";
2
6
  import {
3
7
  BarrelAnalyzer,
4
- ClientEntriesBundler,
5
- GraphClientEntryDiscovery,
6
- RouteClientBuilder,
7
- VENDOR_SPECIFIERS,
8
8
  createBarrelImportsPlugin,
9
9
  createTsconfigPackageResolver,
10
10
  rewriteBarrelImports,
11
- transformUseClient
12
- } from "./index-j9cxndc0.js";
13
- import {
14
- __require
15
- } from "./index-r24hmh0q.js";
11
+ GraphClientEntryDiscovery,
12
+ transformUseClient,
13
+ ClientEntriesBundler,
14
+ VENDOR_SPECIFIERS,
15
+ RouteClientBuilder
16
+ } from "./index-w8kmx1b6.js";
17
+ var __require = import.meta.require;
16
18
 
17
19
  // pkgs/@akanjs/devkit/frontendBuild/allRoutesBuilder.ts
18
- import path4 from "path";
19
-
20
- // pkgs/@akanjs/devkit/artifact/implicitRootLayout.ts
21
- import { mkdir } from "fs/promises";
22
- import path from "path";
23
- var LAYOUT_KEY_RE = /^\.\/(.+\/)?_layout\.(tsx|ts|jsx|js)$/;
24
- async function appHasStModule(appCwdPath) {
25
- return Bun.file(path.join(appCwdPath, "lib", "st.ts")).exists();
26
- }
27
- var IMPLICIT_LAYOUT_DIR = path.join(".akan", "generated", "root-layouts");
28
- var IMPLICIT_DICT_DIR = path.join(".akan", "generated", "dict");
29
- var IMPLICIT_OVERRIDES_DIR = path.join(".akan", "generated", "overrides");
30
- var OVERRIDES_KEY_RE = /^\.\/(.+\/)?_overrides\.(tsx|ts|jsx|js)$/;
31
- async function writeGeneratedOverridesLayoutFile(opts) {
32
- const filename = `${opts.key.replace(/^\.\//, "").replace(/[^a-zA-Z0-9]+/g, "_")}.tsx`;
33
- const absPath = path.join(path.resolve(opts.appCwdPath), IMPLICIT_OVERRIDES_DIR, filename);
34
- const userRel = path.relative(path.dirname(absPath), opts.userAbsPath).split(path.sep).join("/");
35
- const userSpecifier = userRel.startsWith(".") ? userRel : `./${userRel}`;
36
- const source = `"use client";
37
- import { UiOverrideProvider } from "akanjs/ui";
38
- import { createElement, type ReactNode } from "react";
39
- import value from ${JSON.stringify(userSpecifier)};
40
-
41
- export default function AkanUiOverridesLayout({ children }: { children?: ReactNode }) {
42
- return createElement(UiOverrideProvider, { value }, children);
43
- }
44
- `;
45
- await Bun.write(absPath, source);
46
- return absPath;
47
- }
48
- function getRootBoundarySegments(key) {
49
- const match = LAYOUT_KEY_RE.exec(key);
50
- if (!match)
51
- return null;
52
- const prefix = match[1]?.replace(/\/$/, "");
53
- if (!prefix)
54
- return [];
55
- return prefix.split("/").filter(Boolean);
56
- }
57
- function implicitRootLayoutKey(segments) {
58
- return `./${[...segments, "__root_layout"].join("/")}.tsx`;
59
- }
60
- function implicitRootLayoutAbsPath(appCwdPath, segments) {
61
- const filename = segments.length ? `${segments.join("__")}__root_layout.tsx` : "__root_layout.tsx";
62
- return path.join(path.resolve(appCwdPath), IMPLICIT_LAYOUT_DIR, filename);
63
- }
64
- function implicitDictionaryMacroAbsPath(appCwdPath) {
65
- return path.join(path.resolve(appCwdPath), IMPLICIT_DICT_DIR, "useDict.ts");
66
- }
67
- function isRootBoundarySegments(segments, basePaths) {
68
- const firstVisibleIndex = segments.findIndex((segment) => !/^\(.+\)$/.test(segment));
69
- if (firstVisibleIndex === -1)
70
- return segments.length <= 1;
71
- if (segments.slice(firstVisibleIndex + 1).some((segment) => /^\(.+\)$/.test(segment)))
72
- return false;
73
- const visible = segments.slice(firstVisibleIndex);
74
- const allowedBasePaths = new Set([...basePaths].map((basePath) => basePath.trim()).filter(Boolean));
75
- return visible.length === 1 && (firstVisibleIndex > 0 || allowedBasePaths.has(visible[0] ?? ""));
76
- }
77
- function findRootBoundaries(pageKeys, appCwdPath, basePaths) {
78
- const boundaries = new Map;
79
- for (const key of pageKeys) {
80
- const segments = getRootBoundarySegments(key);
81
- if (!segments)
82
- continue;
83
- if (!isRootBoundarySegments(segments, basePaths))
84
- continue;
85
- const id = segments.join("/");
86
- boundaries.set(id, {
87
- sourceKey: key,
88
- sourceAbsPath: path.resolve(appCwdPath, "page", key.replace(/^\.\//, "")),
89
- segments
90
- });
91
- }
92
- const hasExplicitRootBoundary = [...boundaries.values()].some((boundary) => boundary.segments.length === 0);
93
- if (!hasExplicitRootBoundary && boundaries.size === 0) {
94
- boundaries.set("", { sourceKey: null, sourceAbsPath: null, segments: [] });
95
- }
96
- return [...boundaries.values()].sort((a, b) => a.segments.join("/").localeCompare(b.segments.join("/")));
97
- }
98
- function hasAncestorRootBoundary(boundary, boundaries) {
99
- return boundaries.some((candidate) => candidate !== boundary && candidate.segments.length < boundary.segments.length && candidate.segments.every((segment, index) => boundary.segments[index] === segment));
100
- }
101
- function findExplicitRootLayoutAbsPath(pageKeys, appCwdPath) {
102
- const rootLayoutKey = pageKeys.find((key) => {
103
- const segments = getRootBoundarySegments(key);
104
- return segments !== null && segments.length === 0;
105
- });
106
- return rootLayoutKey ? path.resolve(appCwdPath, "page", rootLayoutKey.replace(/^\.\//, "")) : null;
107
- }
108
- function routePrefixForSegments(segments) {
109
- const visible = segments.filter((segment) => !/^\(.+\)$/.test(segment));
110
- return visible[0] ?? null;
111
- }
112
- async function assertEnvClientConvention(appCwdPath, appName) {
113
- const envPath = path.join(appCwdPath, "env", "env.client.ts");
114
- if (!await Bun.file(envPath).exists()) {
115
- throw new Error(`[route-convention] app "${appName}" must provide env/env.client.ts exporting "env" for generated System.Provider`);
116
- }
117
- }
118
- async function writeGeneratedDictionaryMacroFile(appCwdPath, appName) {
119
- const absPath = implicitDictionaryMacroAbsPath(appCwdPath);
120
- await mkdir(path.dirname(absPath), { recursive: true });
121
- await Bun.write(absPath, `import { getAllDictionary } from "@apps/${appName}/lib/dict" with { type: "macro" };
122
-
123
- export const allDictionary = getAllDictionary();
124
- `);
125
- return absPath;
126
- }
127
- async function writeGeneratedRootLayoutFile(opts) {
128
- await assertEnvClientConvention(opts.appCwdPath, opts.appName);
129
- const dictMacroAbsPath = opts.includeSystemProvider ? await writeGeneratedDictionaryMacroFile(opts.appCwdPath, opts.appName) : null;
130
- const absPath = implicitRootLayoutAbsPath(opts.appCwdPath, opts.boundary.segments);
131
- await mkdir(path.dirname(absPath), { recursive: true });
132
- const dictMacroRel = dictMacroAbsPath ? path.relative(path.dirname(absPath), dictMacroAbsPath).split(path.sep).join("/") : null;
133
- const dictMacroSpecifier = dictMacroRel ? dictMacroRel.startsWith(".") ? dictMacroRel : `./${dictMacroRel}` : null;
134
- const sourceRel = opts.boundary.sourceAbsPath ? path.relative(path.dirname(absPath), opts.boundary.sourceAbsPath).split(path.sep).join("/") : null;
135
- const sourceSpecifier = sourceRel ? sourceRel.startsWith(".") ? sourceRel : `./${sourceRel}` : null;
136
- const inheritedSourceAbsPath = opts.rootSourceAbsPath && opts.rootSourceAbsPath !== opts.boundary.sourceAbsPath ? opts.rootSourceAbsPath : null;
137
- const inheritedSourceRel = inheritedSourceAbsPath ? path.relative(path.dirname(absPath), inheritedSourceAbsPath).split(path.sep).join("/") : null;
138
- const inheritedSourceSpecifier = inheritedSourceRel ? inheritedSourceRel.startsWith(".") ? inheritedSourceRel : `./${inheritedSourceRel}` : null;
139
- const clientImport = opts.includeStInit ? `import { st } from "@apps/${opts.appName}/client";
140
- void st;
141
- ` : `import "@apps/${opts.appName}/client";
142
- `;
143
- const inheritedImport = inheritedSourceSpecifier ? `import * as inheritedLayout from ${JSON.stringify(inheritedSourceSpecifier)};
144
- ` : `const inheritedLayout = {};
145
- `;
146
- const prefix = routePrefixForSegments(opts.boundary.segments);
147
- const userImport = sourceSpecifier ? `import UserLayout, * as userLayout from ${JSON.stringify(sourceSpecifier)};
148
- ` : `const UserLayout = ({ children }) => children;
149
- const userLayout = {};
150
- `;
151
- const source = opts.includeSystemProvider ? `import type { LayoutProps, PageProps } from "akanjs/client";
152
- import { loadFonts } from "akanjs/client";
153
- import { System } from "akanjs/ui";
154
- import { env } from "@apps/${opts.appName}/env/env.client";
155
- import { allDictionary } from ${JSON.stringify(dictMacroSpecifier)};
156
- ${clientImport}${inheritedImport}${userImport}
157
- const userFonts = userLayout.fonts ?? inheritedLayout.fonts ?? [];
158
- const defaultFonts = userFonts.filter((font) => font.default);
159
- if (defaultFonts.length > 1) throw new Error("[route-convention] only one default font is allowed per root layout");
160
- const defaultFont = defaultFonts[0];
161
- const defaultFontClassName = defaultFont ? (defaultFont.className ?? \`font-\${defaultFont.name}\`) : undefined;
162
-
163
- export async function generateHead(props: PageProps) {
164
- if (userLayout.generateHead) return userLayout.generateHead(props);
165
- if (userLayout.head !== undefined) return userLayout.head;
166
- if (inheritedLayout.generateHead) return inheritedLayout.generateHead(props);
167
- return inheritedLayout.head;
168
- }
169
-
170
- export async function generateMetadata(props: PageProps) {
171
- if (userLayout.generateMetadata) return userLayout.generateMetadata(props);
172
- if (userLayout.metadata !== undefined) return userLayout.metadata;
173
- if (inheritedLayout.generateMetadata) return inheritedLayout.generateMetadata(props);
174
- return inheritedLayout.metadata;
175
- }
176
-
177
- export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;
178
- export const Error = userLayout.Error ?? inheritedLayout.Error;
179
- export const pageConfig = userLayout.pageConfig ?? inheritedLayout.pageConfig;
180
-
181
- export default function GeneratedLayout({ children, params, searchParams }: LayoutProps) {
182
- return (
183
- <System.Provider
184
- of={GeneratedLayout as never}
185
- appName=${JSON.stringify(opts.appName)}
186
- ${prefix ? `prefix=${JSON.stringify(prefix)}
187
- ` : ""}params={params}
188
- manifest={userLayout.manifest ?? inheritedLayout.manifest}
189
- env={env}
190
- theme={userLayout.theme ?? inheritedLayout.theme}
191
- fonts={loadFonts(userFonts)}
192
- className={defaultFontClassName}
193
- gaTrackingId={userLayout.gaTrackingId ?? inheritedLayout.gaTrackingId}
194
- layoutStyle={userLayout.layoutStyle ?? inheritedLayout.layoutStyle}
195
- reconnect={userLayout.reconnect ?? inheritedLayout.reconnect ?? false}
196
- wsConnect={userLayout.wsConnect ?? inheritedLayout.wsConnect ?? true}
197
- allDictionary={process.env.AKAN_PUBLIC_RENDER_ENV === "ssr" ? allDictionary : undefined}
198
- >
199
- <UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>
200
- </System.Provider>
201
- );
202
- }
203
- ` : `import type { LayoutProps, PageProps } from "akanjs/client";
204
- ${inheritedImport}${userImport}
205
- export async function generateHead(props: PageProps) {
206
- if (userLayout.generateHead) return userLayout.generateHead(props);
207
- if (userLayout.head !== undefined) return userLayout.head;
208
- if (inheritedLayout.generateHead) return inheritedLayout.generateHead(props);
209
- return inheritedLayout.head;
210
- }
211
-
212
- export async function generateMetadata(props: PageProps) {
213
- if (userLayout.generateMetadata) return userLayout.generateMetadata(props);
214
- if (userLayout.metadata !== undefined) return userLayout.metadata;
215
- if (inheritedLayout.generateMetadata) return inheritedLayout.generateMetadata(props);
216
- return inheritedLayout.metadata;
217
- }
218
-
219
- export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;
220
- export const Error = userLayout.Error ?? inheritedLayout.Error;
221
- export const pageConfig = userLayout.pageConfig ?? inheritedLayout.pageConfig;
222
-
223
- export default function GeneratedLayout({ children, params, searchParams }: LayoutProps) {
224
- return <UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>;
225
- }
226
- `;
227
- await Bun.write(absPath, source);
228
- return absPath;
229
- }
230
- async function resolveSsrPageEntries(opts) {
231
- const absPageDir = path.resolve(opts.appCwdPath, "page");
232
- const hasSt = await appHasStModule(opts.appCwdPath);
233
- const basePaths = opts.basePaths ?? [];
234
- const rootSourceAbsPath = findExplicitRootLayoutAbsPath(opts.pageKeys, opts.appCwdPath);
235
- const rootBoundaries = findRootBoundaries(opts.pageKeys, opts.appCwdPath, basePaths);
236
- const rootLayoutKeys = new Set(opts.pageKeys.filter((key) => {
237
- const segments = getRootBoundarySegments(key);
238
- return segments !== null && isRootBoundarySegments(segments, basePaths);
239
- }));
240
- const base = await Promise.all(opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map(async (key) => {
241
- const userAbsPath = path.resolve(absPageDir, key);
242
- if (OVERRIDES_KEY_RE.test(key)) {
243
- const moduleAbsPath = await writeGeneratedOverridesLayoutFile({
244
- appCwdPath: opts.appCwdPath,
245
- key,
246
- userAbsPath
247
- });
248
- return { key, moduleAbsPath, seedAbsPaths: [userAbsPath] };
249
- }
250
- return { key, moduleAbsPath: userAbsPath };
251
- }));
252
- const generated = await Promise.all(rootBoundaries.map(async (boundary) => ({
253
- key: implicitRootLayoutKey(boundary.segments),
254
- moduleAbsPath: await writeGeneratedRootLayoutFile({
255
- appCwdPath: opts.appCwdPath,
256
- appName: opts.appName,
257
- boundary,
258
- rootSourceAbsPath,
259
- includeStInit: hasSt && boundary.segments.length === 0,
260
- includeSystemProvider: !hasAncestorRootBoundary(boundary, rootBoundaries)
261
- }),
262
- seedAbsPaths: [...new Set([boundary.sourceAbsPath, rootSourceAbsPath].filter((absPath) => absPath !== null))]
263
- })));
264
- const entries = [...base, ...generated];
265
- entries.sort((a, b) => a.key.localeCompare(b.key));
266
- return entries;
267
- }
268
- async function resolveSsrPageEntriesForApp(app, pageKeys) {
269
- const config = await app.getConfig();
270
- return resolveSsrPageEntries({ appCwdPath: app.cwdPath, appName: app.name, pageKeys, basePaths: config.basePaths });
271
- }
20
+ import path3 from "path";
272
21
 
273
22
  // pkgs/@akanjs/devkit/artifact/routeSeedIndex.ts
274
- import path2 from "path";
23
+ import path from "path";
275
24
  import { assertUniqueRoutePatterns, compareRouteSpecificity, parseRouteModuleKey } from "akanjs/common";
276
25
  function computeRouteSeedIndex(pageEntries) {
277
26
  const layoutsByPrefix = new Map;
278
27
  const pagesBySegments = [];
279
28
  for (const { key, moduleAbsPath, seedAbsPaths } of pageEntries) {
280
29
  const parsed = parseRouteModuleKey(key);
281
- const files = [path2.resolve(moduleAbsPath), ...(seedAbsPaths ?? []).map((seed) => path2.resolve(seed))];
30
+ const files = [path.resolve(moduleAbsPath), ...(seedAbsPaths ?? []).map((seed) => path.resolve(seed))];
282
31
  if (parsed.kind === "layout" || parsed.kind === "overrides") {
283
32
  const prefix = parsed.routeSegments.join("/");
284
33
  const prev = layoutsByPrefix.get(prefix) ?? [];
@@ -315,7 +64,7 @@ function computeRouteSeedIndex(pageEntries) {
315
64
  }
316
65
  var ROUTE_SEED_INDEX_JSON = "route-seed-index.json";
317
66
  function serializeRouteSeedIndexForArtifact(index, artifactDir, options = {}) {
318
- const normalizedArtifactDir = path2.resolve(artifactDir);
67
+ const normalizedArtifactDir = path.resolve(artifactDir);
319
68
  if (options.production) {
320
69
  return {
321
70
  entries: index.entries.map((entry) => ({ routeId: entry.routeId }))
@@ -330,19 +79,19 @@ function serializeRouteSeedIndexForArtifact(index, artifactDir, options = {}) {
330
79
  };
331
80
  }
332
81
  async function saveRouteSeedIndex(artifactDir, index, options = {}) {
333
- const absPath = path2.join(path2.resolve(artifactDir), ROUTE_SEED_INDEX_JSON);
82
+ const absPath = path.join(path.resolve(artifactDir), ROUTE_SEED_INDEX_JSON);
334
83
  await Bun.write(absPath, `${JSON.stringify(serializeRouteSeedIndexForArtifact(index, artifactDir, options), null, 2)}
335
84
  `);
336
85
  return absPath;
337
86
  }
338
87
  function serializeArtifactPath(artifactPath, artifactDir) {
339
- if (!path2.isAbsolute(artifactPath))
88
+ if (!path.isAbsolute(artifactPath))
340
89
  return artifactPath;
341
- return path2.relative(artifactDir, artifactPath).split(path2.sep).join("/");
90
+ return path.relative(artifactDir, artifactPath).split(path.sep).join("/");
342
91
  }
343
92
 
344
93
  // pkgs/@akanjs/devkit/frontendBuild/routesManifestArtifactSerializer.ts
345
- import path3 from "path";
94
+ import path2 from "path";
346
95
 
347
96
  class RoutesManifestArtifactSerializer {
348
97
  #manifest;
@@ -350,7 +99,7 @@ class RoutesManifestArtifactSerializer {
350
99
  #production;
351
100
  constructor(manifest, artifactDir, options = {}) {
352
101
  this.#manifest = manifest;
353
- this.#artifactDir = path3.resolve(artifactDir);
102
+ this.#artifactDir = path2.resolve(artifactDir);
354
103
  this.#production = options.production ?? false;
355
104
  }
356
105
  static serialize(manifest, artifactDir, options = {}) {
@@ -383,9 +132,9 @@ class RoutesManifestArtifactSerializer {
383
132
  };
384
133
  }
385
134
  #serializeArtifactPath(artifactPath) {
386
- if (!path3.isAbsolute(artifactPath))
135
+ if (!path2.isAbsolute(artifactPath))
387
136
  return artifactPath;
388
- return path3.relative(this.#artifactDir, artifactPath).split(path3.sep).join("/");
137
+ return path2.relative(this.#artifactDir, artifactPath).split(path2.sep).join("/");
389
138
  }
390
139
  }
391
140
 
@@ -435,7 +184,7 @@ class AllRoutesBuilder {
435
184
  routeIds: this.#routeIds,
436
185
  ...this.#merged
437
186
  };
438
- const manifestPath = path4.join(path4.resolve(this.#artifactDir), "routes-manifest.json");
187
+ const manifestPath = path3.join(path3.resolve(this.#artifactDir), "routes-manifest.json");
439
188
  await Bun.write(manifestPath, `${JSON.stringify(RoutesManifestArtifactSerializer.serialize(manifest, this.#artifactDir, {
440
189
  production: this.#command === "build"
441
190
  }), null, 2)}
@@ -477,442 +226,116 @@ class AllRoutesBuilder {
477
226
  }
478
227
  }
479
228
 
480
- // pkgs/@akanjs/devkit/frontendBuild/csrArtifactBuilder.ts
481
- import { mkdir as mkdir2, rm, unlink } from "fs/promises";
482
- import path6 from "path";
483
-
484
- // pkgs/@akanjs/devkit/frontendBuild/pagesEntrySourceGenerator.ts
485
- import fs from "fs";
229
+ // pkgs/@akanjs/devkit/frontendBuild/cssCompiler.ts
486
230
  import path5 from "path";
487
- import ts from "typescript";
231
+ import { Logger } from "akanjs/common";
232
+ import { compile } from "tailwindcss";
488
233
 
489
- class PagesEntrySourceGenerator {
490
- #pageEntries;
491
- constructor(pageEntries) {
492
- this.#pageEntries = pageEntries;
493
- }
494
- static generate(pageEntries) {
495
- return new PagesEntrySourceGenerator(pageEntries).generate();
496
- }
497
- generate() {
498
- const lines = this.#pageEntries.map(({ key, moduleAbsPath }) => {
499
- const specifier = PagesEntrySourceGenerator.#toImportSpecifier(moduleAbsPath);
500
- return ` ${JSON.stringify(key)}: () => import(${JSON.stringify(specifier)}),`;
501
- });
502
- return `export const pages = {
503
- ${lines.join(`
504
- `)}
505
- };
506
- `;
234
+ // pkgs/@akanjs/devkit/frontendBuild/cssCandidateCache.ts
235
+ import { stat } from "fs/promises";
236
+ var CANDIDATE_RE = /-?(?:[\w@]|\[[^\]\s]+\])[\w:/.-]*(?:\[[^\]\s]+\][\w:/.-]*)*/g;
237
+
238
+ class CssCandidateCache {
239
+ static #version = 3;
240
+ #path;
241
+ #entries = new Map;
242
+ #dirty = false;
243
+ #reused = 0;
244
+ #rescanned = 0;
245
+ constructor(cachePath) {
246
+ this.#path = cachePath;
507
247
  }
508
- static generateStatic(pageEntries) {
509
- return new PagesEntrySourceGenerator(pageEntries).generateStatic();
248
+ get reused() {
249
+ return this.#reused;
510
250
  }
511
- generateStatic() {
512
- const imports = this.#pageEntries.map(({ moduleAbsPath }, index) => {
513
- const specifier = PagesEntrySourceGenerator.#toImportSpecifier(moduleAbsPath);
514
- return `import * as page${index} from ${JSON.stringify(specifier)};`;
515
- });
516
- const entries = this.#pageEntries.map(({ key, moduleAbsPath }, index) => {
517
- const isAsyncDefault = PagesEntrySourceGenerator.#hasAsyncDefaultExport(moduleAbsPath);
518
- return ` ${JSON.stringify(key)}: { loader: async () => page${index}, isAsyncDefault: ${isAsyncDefault} },`;
519
- });
520
- return `${imports.join(`
521
- `)}
522
- export const pages = {
523
- ${entries.join(`
524
- `)}
525
- };
526
- `;
251
+ get rescanned() {
252
+ return this.#rescanned;
527
253
  }
528
- static #toImportSpecifier(moduleAbsPath) {
529
- return path5.resolve(moduleAbsPath).split(path5.sep).join("/");
254
+ async load() {
255
+ const raw = await Bun.file(this.#path).json().catch(() => null);
256
+ if (!raw || raw.version !== CssCandidateCache.#version || typeof raw.files !== "object")
257
+ return this;
258
+ for (const [file, entry] of Object.entries(raw.files)) {
259
+ if (typeof entry?.mtimeMs !== "number" || !Array.isArray(entry.candidates))
260
+ continue;
261
+ this.#entries.set(file, entry);
262
+ }
263
+ return this;
530
264
  }
531
- static #hasAsyncDefaultExport(moduleAbsPath) {
532
- try {
533
- const source = fs.readFileSync(path5.resolve(moduleAbsPath), "utf8");
534
- const sourceFile = ts.createSourceFile(moduleAbsPath, source, ts.ScriptTarget.Latest, true, PagesEntrySourceGenerator.#scriptKind(moduleAbsPath));
535
- return PagesEntrySourceGenerator.#sourceFileHasAsyncDefaultExport(sourceFile);
536
- } catch {
537
- return false;
265
+ async candidatesFor(file) {
266
+ const stats = await stat(file).catch(() => null);
267
+ const cached = this.#entries.get(file);
268
+ if (stats && cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
269
+ this.#reused += 1;
270
+ return cached.candidates;
271
+ }
272
+ const content = await Bun.file(file).text();
273
+ const candidates = [...new Set(Array.from(content.matchAll(CANDIDATE_RE), (m) => m[0]))];
274
+ this.#rescanned += 1;
275
+ if (stats) {
276
+ this.#entries.set(file, { mtimeMs: stats.mtimeMs, size: stats.size, candidates });
277
+ this.#dirty = true;
538
278
  }
279
+ return candidates;
539
280
  }
540
- static #sourceFileHasAsyncDefaultExport(sourceFile) {
541
- const asyncBindings = new Map;
542
- let defaultIdentifier = null;
543
- for (const statement of sourceFile.statements) {
544
- if (ts.isFunctionDeclaration(statement)) {
545
- if (PagesEntrySourceGenerator.#hasModifier(statement, ts.SyntaxKind.DefaultKeyword)) {
546
- return PagesEntrySourceGenerator.#hasModifier(statement, ts.SyntaxKind.AsyncKeyword);
547
- }
548
- if (statement.name) {
549
- asyncBindings.set(statement.name.text, PagesEntrySourceGenerator.#hasModifier(statement, ts.SyntaxKind.AsyncKeyword));
550
- }
551
- continue;
552
- }
553
- if (ts.isVariableStatement(statement)) {
554
- for (const declaration of statement.declarationList.declarations) {
555
- if (!ts.isIdentifier(declaration.name))
556
- continue;
557
- asyncBindings.set(declaration.name.text, PagesEntrySourceGenerator.#isAsyncFunctionExpression(declaration.initializer));
558
- }
559
- continue;
560
- }
561
- if (ts.isExportAssignment(statement)) {
562
- if (PagesEntrySourceGenerator.#isAsyncFunctionExpression(statement.expression))
563
- return true;
564
- if (ts.isIdentifier(statement.expression))
565
- defaultIdentifier = statement.expression.text;
281
+ async save(present) {
282
+ for (const file of [...this.#entries.keys()]) {
283
+ if (present.has(file))
566
284
  continue;
567
- }
568
- if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
569
- const exportClause = statement.exportClause;
570
- for (const specifier of exportClause.elements) {
571
- if (specifier.name.text !== "default")
572
- continue;
573
- defaultIdentifier = specifier.propertyName?.text ?? specifier.name.text;
574
- }
575
- }
285
+ this.#entries.delete(file);
286
+ this.#dirty = true;
576
287
  }
577
- return defaultIdentifier ? asyncBindings.get(defaultIdentifier) === true : false;
578
- }
579
- static #hasModifier(node, kind) {
580
- return ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some((modifier) => modifier.kind === kind) ?? false);
581
- }
582
- static #isAsyncFunctionExpression(node) {
583
- return Boolean(node && (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && PagesEntrySourceGenerator.#hasModifier(node, ts.SyntaxKind.AsyncKeyword));
584
- }
585
- static #scriptKind(moduleAbsPath) {
586
- return moduleAbsPath.endsWith(".tsx") || moduleAbsPath.endsWith(".jsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
288
+ if (!this.#dirty)
289
+ return;
290
+ this.#dirty = false;
291
+ const files = {};
292
+ for (const [file, entry] of this.#entries)
293
+ files[file] = entry;
294
+ await Bun.write(this.#path, JSON.stringify({ version: CssCandidateCache.#version, files })).catch(() => {
295
+ return;
296
+ });
587
297
  }
588
298
  }
589
299
 
590
- // pkgs/@akanjs/devkit/frontendBuild/csrArtifactBuilder.ts
591
- class CsrArtifactBuilder {
592
- #app;
593
- #command;
594
- #lang;
595
- constructor(app, command = "start", lang = "en") {
596
- this.#app = app;
597
- this.#command = command;
598
- this.#lang = lang;
300
+ // pkgs/@akanjs/devkit/frontendBuild/cssImportResolver.ts
301
+ import path4 from "path";
302
+ var CSS_IMPORT_EXTS = ["", ".css", "/styles.css", "/index.css"];
303
+
304
+ class CssImportResolver {
305
+ #workspaceRoot;
306
+ #paths;
307
+ #wildcardEntries;
308
+ constructor(workspaceRoot, paths = {}) {
309
+ this.#workspaceRoot = workspaceRoot;
310
+ this.#paths = paths;
311
+ this.#wildcardEntries = Object.entries(paths).filter(([key]) => key.endsWith("/*")).map(([key, replacements]) => ({ prefix: key.slice(0, -1), replacements })).sort((a, b) => b.prefix.length - a.prefix.length);
599
312
  }
600
- async build() {
601
- const pageKeys = await this.#app.getPageKeys();
602
- if (pageKeys.length === 0) {
603
- this.#app.log(`[cli] no route files under ${this.#app.cwdPath}/page \u2014 skipping CSR build`);
604
- return null;
313
+ static async create(app) {
314
+ const tsconfig = await app.getTsConfig();
315
+ return new CssImportResolver(app.workspace.workspaceRoot, tsconfig.compilerOptions.paths ?? {});
316
+ }
317
+ async resolve(id, fromBase) {
318
+ for (const resolve of [
319
+ () => this.#resolveWithTsconfig(id),
320
+ () => this.#resolveWithBun(id, fromBase),
321
+ () => this.#resolveWithRequire(id, fromBase),
322
+ () => this.#resolvePackageStyle(id, fromBase)
323
+ ]) {
324
+ const resolved = await resolve();
325
+ if (resolved)
326
+ return resolved;
605
327
  }
606
- const pageEntries = await resolveSsrPageEntriesForApp(this.#app, pageKeys);
607
- const akanConfig = await this.#app.getConfig();
608
- const artifact = await this.#loadCsrArtifact();
609
- const csrBasePaths = [...akanConfig.basePaths];
610
- const htmlEntries = csrBasePaths.length > 0 ? csrBasePaths : ["index"];
611
- await rm(this.#outputDir, { recursive: true, force: true });
612
- await mkdir2(path6.join(this.#app.cwdPath, ".akan/generated/csr"), { recursive: true });
613
- const generatedHtmlFiles = Object.fromEntries(htmlEntries.map((basePath) => this.#createHtmlFile(basePath)));
614
- const result = await Bun.build({
615
- target: "browser",
616
- entrypoints: Object.keys(generatedHtmlFiles),
617
- files: {
618
- ...generatedHtmlFiles,
619
- [`${this.#app.cwdPath}/.akan/generated/csr/csr.tsx`]: `
620
- import { bootCsr } from "akanjs/webkit";
621
- ${PagesEntrySourceGenerator.generateStatic(pageEntries)}
622
- void bootCsr(pages);
623
- `
624
- },
625
- root: `${this.#app.cwdPath}/.akan/generated/csr`,
626
- outdir: this.#outputDir,
627
- splitting: false,
628
- minify: true,
629
- env: "AKAN_PUBLIC_*",
630
- define: this.#define(),
631
- optimizeImports: akanConfig.optimizeImports
632
- });
633
- if (!result.success) {
634
- const logs = result.logs.map((log) => log.message).join(`
635
- `);
636
- throw new Error(`[csr-build] failed${logs ? `
637
- ${logs}` : ""}`);
328
+ return null;
329
+ }
330
+ #resolveWithBun(id, fromBase) {
331
+ for (const base of this.#resolutionBases(fromBase)) {
332
+ try {
333
+ const resolved = Bun.resolveSync(id, base);
334
+ if (CssImportResolver.isCssFile(resolved))
335
+ return resolved;
336
+ } catch {}
638
337
  }
639
- await this.#inlineCsrArtifacts(artifact.cssAssets ?? {});
640
- this.#app.verbose(`[csr-build] output -> ${this.#outputDir}`);
641
- return { outputDir: this.#outputDir };
642
- }
643
- get #outputDir() {
644
- return path6.join(this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath, this.#command === "build" ? "csr" : ".akan/artifact/csr");
645
- }
646
- #define() {
647
- const nodeEnv = this.#command === "build" ? "production" : "development";
648
- return {
649
- "process.env.NODE_ENV": JSON.stringify(nodeEnv),
650
- "process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("csr"),
651
- ...Object.fromEntries(Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]))
652
- };
653
- }
654
- #createHtmlFile(basePath) {
655
- const filename = `${basePath}.html`;
656
- return [
657
- `${this.#app.cwdPath}/.akan/generated/csr/${filename}`,
658
- `<!doctype html>
659
- <html lang="${this.#lang}">
660
- <head>
661
- <meta charset="utf-8" />
662
- <title>${this.#app.name}</title>
663
- <base href="/" />
664
- <meta name="viewport" content="width=device-width, initial-scale=1" />
665
- </head>
666
- <body>
667
- <div id="root"></div>
668
- <script type="module" src="./csr.tsx"></script>
669
- </body>
670
- </html>
671
- `
672
- ];
673
- }
674
- async#loadCsrArtifact() {
675
- const artifactDir = path6.join(this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath, ".akan/artifact");
676
- const artifactFile = Bun.file(path6.join(artifactDir, "base-artifact.json"));
677
- if (!await artifactFile.exists())
678
- return { cssAssets: {} };
679
- const artifact = await artifactFile.json();
680
- return { cssAssets: artifact.cssAssets ?? {} };
681
- }
682
- async#inlineCsrArtifacts(cssAssets) {
683
- const jsFiles = new Set;
684
- const cssFiles = new Set;
685
- for (const htmlPath of await this.#htmlOutputPaths()) {
686
- const htmlFile = Bun.file(htmlPath);
687
- if (!await htmlFile.exists())
688
- continue;
689
- const basePath = path6.basename(htmlPath, ".html") === "index" ? "" : path6.basename(htmlPath, ".html");
690
- const inlined = await this.#inlineHtmlAssets(await htmlFile.text(), htmlPath, cssAssets[basePath]);
691
- for (const filePath of inlined.jsFiles)
692
- jsFiles.add(filePath);
693
- for (const filePath of inlined.cssFiles)
694
- cssFiles.add(filePath);
695
- await Bun.write(htmlPath, inlined.html);
696
- }
697
- for (const filePath of jsFiles)
698
- await unlink(filePath).catch(() => {
699
- return;
700
- });
701
- for (const filePath of cssFiles)
702
- await unlink(filePath).catch(() => {
703
- return;
704
- });
705
- const remainingJs = await this.#listOutputFiles((filePath) => filePath.endsWith(".js"));
706
- const remainingCss = await this.#listOutputFiles((filePath) => filePath.endsWith(".css"));
707
- const remainingAssets = [...remainingJs, ...remainingCss];
708
- if (remainingAssets.length > 0) {
709
- throw new Error(`[csr-build] expected single-file HTML, but CSR assets remain:
710
- ${remainingAssets.join(`
711
- `)}`);
712
- }
713
- }
714
- async#inlineHtmlAssets(html, htmlPath, cssAsset) {
715
- const jsFiles = [];
716
- const cssFiles = CsrArtifactBuilder.collectStylesheetHrefs(html).map((href) => CsrArtifactBuilder.resolveHtmlAssetPath(htmlPath, href));
717
- let next = CsrArtifactBuilder.stripBundledStylesheetLinks(html);
718
- next = await CsrArtifactBuilder.replaceModuleScriptSrc(next, async (src) => {
719
- const jsPath = CsrArtifactBuilder.resolveHtmlAssetPath(htmlPath, src);
720
- jsFiles.push(jsPath);
721
- return await Bun.file(jsPath).text();
722
- });
723
- const bundledCss = (await Promise.all(cssFiles.map((cssFile) => Bun.file(cssFile).text().catch(() => "")))).filter(Boolean).join(`
724
- `);
725
- if (bundledCss) {
726
- const style = CsrArtifactBuilder.createInlineStyle(bundledCss);
727
- if (!next.includes(style))
728
- next = CsrArtifactBuilder.injectBeforeHeadEnd(next, style);
729
- }
730
- if (cssAsset) {
731
- const cssPath = path6.join(this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath, ".akan/artifact", cssAsset.cssRelPath);
732
- const css = await Bun.file(cssPath).text();
733
- const style = CsrArtifactBuilder.createInlineStyle(css);
734
- if (!next.includes(style))
735
- next = CsrArtifactBuilder.injectBeforeHeadEnd(next, style);
736
- }
737
- return { html: next, jsFiles, cssFiles };
738
- }
739
- async#htmlOutputPaths() {
740
- return await this.#listOutputFiles((filePath) => filePath.endsWith(".html"));
741
- }
742
- async#listOutputFiles(predicate) {
743
- const glob = new Bun.Glob("**/*");
744
- const files = [];
745
- for await (const filePath of glob.scan({ cwd: this.#outputDir, absolute: true })) {
746
- if (predicate(filePath))
747
- files.push(filePath);
748
- }
749
- return files.sort();
750
- }
751
- static injectBeforeHeadEnd(html, snippet) {
752
- const matches = [...html.matchAll(/<\/head\s*>/gi)];
753
- const bodyStart = html.search(/<body(?:\s|>)/i);
754
- const headEnd = matches.filter((match) => match.index !== undefined && (bodyStart === -1 || match.index < bodyStart)).at(-1);
755
- if (!headEnd || headEnd.index === undefined)
756
- return `${snippet}
757
- ${html}`;
758
- return `${html.slice(0, headEnd.index)}${snippet}
759
- ${html.slice(headEnd.index)}`;
760
- }
761
- static stripBundledStylesheetLinks(html) {
762
- return html.replace(/<link\b(?=[^>]*\brel=["']stylesheet["'])[^>]*>\s*/gi, "");
763
- }
764
- static collectStylesheetHrefs(html) {
765
- const linkRe = /<link\b(?=[^>]*\brel=["']stylesheet["'])(?=[^>]*\bhref=["']([^"']+)["'])[^>]*>/gi;
766
- return [...html.matchAll(linkRe)].map((match) => match[1]).filter((href) => !!href);
767
- }
768
- static createInlineStyle(css) {
769
- return `<style data-akan-css="active">
770
- ${css.replace(/<\/style/gi, "<\\/style")}
771
- </style>`;
772
- }
773
- static async replaceModuleScriptSrc(html, loadScript) {
774
- const scriptRe = /<script\b(?=[^>]*\btype=["']module["'])(?=[^>]*\bsrc=["']([^"']+)["'])[^>]*>\s*<\/script>/gi;
775
- let result = "";
776
- let lastIndex = 0;
777
- let matched = false;
778
- for (const match of html.matchAll(scriptRe)) {
779
- const full = match[0];
780
- const src = match[1];
781
- if (match.index === undefined || !src)
782
- continue;
783
- matched = true;
784
- result += html.slice(lastIndex, match.index);
785
- result += `<script type="module">
786
- ${CsrArtifactBuilder.escapeInlineScript(await loadScript(src))}
787
- </script>`;
788
- lastIndex = match.index + full.length;
789
- }
790
- if (!matched)
791
- return html;
792
- return result + html.slice(lastIndex);
793
- }
794
- static escapeInlineScript(source) {
795
- return source.replace(/<\/script/gi, "<\\/script");
796
- }
797
- static resolveHtmlAssetPath(htmlPath, src) {
798
- if (/^[a-z][a-z0-9+.-]*:/i.test(src) || src.startsWith("//")) {
799
- throw new Error(`[csr-build] cannot inline external script: ${src}`);
800
- }
801
- const normalized = src.startsWith("/") ? src.slice(1) : src;
802
- return path6.resolve(path6.dirname(htmlPath), normalized);
803
- }
804
- }
805
-
806
- // pkgs/@akanjs/devkit/frontendBuild/cssCompiler.ts
807
- import path8 from "path";
808
- import { Logger } from "akanjs/common";
809
- import { compile } from "tailwindcss";
810
-
811
- // pkgs/@akanjs/devkit/frontendBuild/cssCandidateCache.ts
812
- import { stat } from "fs/promises";
813
- var CANDIDATE_RE = /-?(?:[\w@]|\[[^\]\s]+\])[\w:/.-]*(?:\[[^\]\s]+\][\w:/.-]*)*/g;
814
-
815
- class CssCandidateCache {
816
- static #version = 3;
817
- #path;
818
- #entries = new Map;
819
- #dirty = false;
820
- #reused = 0;
821
- #rescanned = 0;
822
- constructor(cachePath) {
823
- this.#path = cachePath;
824
- }
825
- get reused() {
826
- return this.#reused;
827
- }
828
- get rescanned() {
829
- return this.#rescanned;
830
- }
831
- async load() {
832
- const raw = await Bun.file(this.#path).json().catch(() => null);
833
- if (!raw || raw.version !== CssCandidateCache.#version || typeof raw.files !== "object")
834
- return this;
835
- for (const [file, entry] of Object.entries(raw.files)) {
836
- if (typeof entry?.mtimeMs !== "number" || !Array.isArray(entry.candidates))
837
- continue;
838
- this.#entries.set(file, entry);
839
- }
840
- return this;
841
- }
842
- async candidatesFor(file) {
843
- const stats = await stat(file).catch(() => null);
844
- const cached = this.#entries.get(file);
845
- if (stats && cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
846
- this.#reused += 1;
847
- return cached.candidates;
848
- }
849
- const content = await Bun.file(file).text();
850
- const candidates = [...new Set(Array.from(content.matchAll(CANDIDATE_RE), (m) => m[0]))];
851
- this.#rescanned += 1;
852
- if (stats) {
853
- this.#entries.set(file, { mtimeMs: stats.mtimeMs, size: stats.size, candidates });
854
- this.#dirty = true;
855
- }
856
- return candidates;
857
- }
858
- async save(present) {
859
- for (const file of [...this.#entries.keys()]) {
860
- if (present.has(file))
861
- continue;
862
- this.#entries.delete(file);
863
- this.#dirty = true;
864
- }
865
- if (!this.#dirty)
866
- return;
867
- this.#dirty = false;
868
- const files = {};
869
- for (const [file, entry] of this.#entries)
870
- files[file] = entry;
871
- await Bun.write(this.#path, JSON.stringify({ version: CssCandidateCache.#version, files })).catch(() => {
872
- return;
873
- });
874
- }
875
- }
876
-
877
- // pkgs/@akanjs/devkit/frontendBuild/cssImportResolver.ts
878
- import path7 from "path";
879
- var CSS_IMPORT_EXTS = ["", ".css", "/styles.css", "/index.css"];
880
-
881
- class CssImportResolver {
882
- #workspaceRoot;
883
- #paths;
884
- #wildcardEntries;
885
- constructor(workspaceRoot, paths = {}) {
886
- this.#workspaceRoot = workspaceRoot;
887
- this.#paths = paths;
888
- this.#wildcardEntries = Object.entries(paths).filter(([key]) => key.endsWith("/*")).map(([key, replacements]) => ({ prefix: key.slice(0, -1), replacements })).sort((a, b) => b.prefix.length - a.prefix.length);
889
- }
890
- static async create(app) {
891
- const tsconfig = await app.getTsConfig();
892
- return new CssImportResolver(app.workspace.workspaceRoot, tsconfig.compilerOptions.paths ?? {});
893
- }
894
- async resolve(id, fromBase) {
895
- for (const resolve of [
896
- () => this.#resolveWithTsconfig(id),
897
- () => this.#resolveWithBun(id, fromBase),
898
- () => this.#resolveWithRequire(id, fromBase),
899
- () => this.#resolvePackageStyle(id, fromBase)
900
- ]) {
901
- const resolved = await resolve();
902
- if (resolved)
903
- return resolved;
904
- }
905
- return null;
906
- }
907
- #resolveWithBun(id, fromBase) {
908
- for (const base of this.#resolutionBases(fromBase)) {
909
- try {
910
- const resolved = Bun.resolveSync(id, base);
911
- if (CssImportResolver.isCssFile(resolved))
912
- return resolved;
913
- } catch {}
914
- }
915
- return null;
338
+ return null;
916
339
  }
917
340
  #resolveWithRequire(id, fromBase) {
918
341
  for (const base of this.#resolutionBases(fromBase)) {
@@ -928,7 +351,7 @@ class CssImportResolver {
928
351
  const exact = this.#paths[id];
929
352
  if (exact) {
930
353
  for (const repl of exact) {
931
- const resolved = await this.#firstExisting(path7.resolve(this.#workspaceRoot, repl));
354
+ const resolved = await this.#firstExisting(path4.resolve(this.#workspaceRoot, repl));
932
355
  if (resolved)
933
356
  return resolved;
934
357
  }
@@ -939,7 +362,7 @@ class CssImportResolver {
939
362
  const suffix = id.slice(prefix.length);
940
363
  for (const repl of replacements) {
941
364
  const replPath = repl.endsWith("/*") ? repl.slice(0, -1) : repl;
942
- const resolved = await this.#firstExisting(path7.resolve(this.#workspaceRoot, replPath + suffix));
365
+ const resolved = await this.#firstExisting(path4.resolve(this.#workspaceRoot, replPath + suffix));
943
366
  if (resolved)
944
367
  return resolved;
945
368
  }
@@ -969,29 +392,29 @@ class CssImportResolver {
969
392
  try {
970
393
  if (!await Bun.file(pkgPath).exists())
971
394
  return null;
972
- const pkgDir = path7.dirname(pkgPath);
395
+ const pkgDir = path4.dirname(pkgPath);
973
396
  const pkg = await Bun.file(pkgPath).json();
974
397
  const subpath = id === pkgName ? "." : `.${id.slice(pkgName.length)}`;
975
398
  const exportValue = pkg.exports?.[subpath];
976
399
  const exportedEntry = typeof exportValue === "string" ? exportValue : exportValue?.style || exportValue?.import || exportValue?.default;
977
400
  if (exportedEntry)
978
- return await this.#firstExisting(path7.resolve(pkgDir, exportedEntry));
401
+ return await this.#firstExisting(path4.resolve(pkgDir, exportedEntry));
979
402
  if (subpath !== ".")
980
- return await this.#firstExisting(path7.resolve(pkgDir, subpath));
981
- return await this.#firstExisting(path7.resolve(pkgDir, pkg.exports?.["."]?.style || pkg.style || "index.css"));
403
+ return await this.#firstExisting(path4.resolve(pkgDir, subpath));
404
+ return await this.#firstExisting(path4.resolve(pkgDir, pkg.exports?.["."]?.style || pkg.style || "index.css"));
982
405
  } catch {
983
406
  return null;
984
407
  }
985
408
  }
986
409
  #resolutionBases(fromBase) {
987
- return [fromBase, this.#workspaceRoot, path7.dirname(Bun.main), path7.resolve(path7.dirname(Bun.main), "../..")];
410
+ return [fromBase, this.#workspaceRoot, path4.dirname(Bun.main), path4.resolve(path4.dirname(Bun.main), "../..")];
988
411
  }
989
412
  #packageJsonCandidates(pkgName) {
990
413
  return [
991
- path7.join(this.#workspaceRoot, "pkgs", pkgName, "package.json"),
992
- path7.join(this.#workspaceRoot, "node_modules", pkgName, "package.json"),
993
- path7.join(path7.dirname(Bun.main), "node_modules", pkgName, "package.json"),
994
- path7.join(path7.dirname(Bun.main), "../../", pkgName, "package.json")
414
+ path4.join(this.#workspaceRoot, "pkgs", pkgName, "package.json"),
415
+ path4.join(this.#workspaceRoot, "node_modules", pkgName, "package.json"),
416
+ path4.join(path4.dirname(Bun.main), "node_modules", pkgName, "package.json"),
417
+ path4.join(path4.dirname(Bun.main), "../../", pkgName, "package.json")
995
418
  ];
996
419
  }
997
420
  async#firstExisting(basePath) {
@@ -1009,7 +432,7 @@ class CssImportResolver {
1009
432
  return parts[0] ?? null;
1010
433
  }
1011
434
  static isCssFile(filePath) {
1012
- return path7.extname(filePath) === ".css";
435
+ return path4.extname(filePath) === ".css";
1013
436
  }
1014
437
  }
1015
438
 
@@ -1028,7 +451,7 @@ class CssCompiler {
1028
451
  this.#app = app;
1029
452
  }
1030
453
  get #candidateCachePath() {
1031
- return path8.join(this.#app.cwdPath, ".akan/cache/cssCandidates.json");
454
+ return path5.join(this.#app.cwdPath, ".akan/cache/cssCandidates.json");
1032
455
  }
1033
456
  #cssText = null;
1034
457
  #cssTextByBasePath = null;
@@ -1036,6 +459,7 @@ class CssCompiler {
1036
459
  #resolvedFileCache = new Map;
1037
460
  #resolvedSpecifierCache = new Map;
1038
461
  #discoveredCssPaths = new Set;
462
+ importedStylesheetsByBasePath = {};
1039
463
  #fileExists(absPath) {
1040
464
  let cached = this.#fileExistsCache.get(absPath);
1041
465
  if (!cached) {
@@ -1057,7 +481,7 @@ class CssCompiler {
1057
481
  return filePath;
1058
482
  }
1059
483
  for (const ext of SOURCE_EXTS) {
1060
- const filePath = path8.join(absPathNoExt, `index${ext}`);
484
+ const filePath = path5.join(absPathNoExt, `index${ext}`);
1061
485
  if (await this.#fileExists(filePath))
1062
486
  return filePath;
1063
487
  }
@@ -1070,8 +494,11 @@ class CssCompiler {
1070
494
  if (this.#cssText !== null && !refresh)
1071
495
  return this.#cssText;
1072
496
  this.#discoveredCssPaths.clear();
497
+ this.importedStylesheetsByBasePath = {};
1073
498
  const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh });
1074
- this.#cssText = await this.compileCss(cssPaths, sourcePaths);
499
+ const { css, imported } = await this.#compileWithImports(cssPaths, sourcePaths);
500
+ this.#cssText = css;
501
+ this.importedStylesheetsByBasePath = { "": imported };
1075
502
  await this.#warnUnreachableStylesheets();
1076
503
  return this.#cssText;
1077
504
  }
@@ -1079,6 +506,7 @@ class CssCompiler {
1079
506
  if (this.#cssTextByBasePath !== null && !refresh)
1080
507
  return this.#cssTextByBasePath;
1081
508
  this.#discoveredCssPaths.clear();
509
+ this.importedStylesheetsByBasePath = {};
1082
510
  const akanConfig = await this.#app.getConfig({ refresh });
1083
511
  const pageKeys = await this.#app.getPageKeys({ refresh });
1084
512
  const basePaths = [...akanConfig.basePaths];
@@ -1089,7 +517,8 @@ class CssCompiler {
1089
517
  return ["", ""];
1090
518
  const started = Date.now();
1091
519
  const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: rootPageKeys });
1092
- const css = await this.compileCss(cssPaths, sourcePaths);
520
+ const { css, imported } = await this.#compileWithImports(cssPaths, sourcePaths);
521
+ this.importedStylesheetsByBasePath[""] = imported;
1093
522
  this.#logger.verbose(`css base=root paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`);
1094
523
  return ["", css];
1095
524
  })(),
@@ -1099,7 +528,8 @@ class CssCompiler {
1099
528
  return [basePath, ""];
1100
529
  const started = Date.now();
1101
530
  const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: basePathPageKeys });
1102
- const css = await this.compileCss(cssPaths, sourcePaths);
531
+ const { css, imported } = await this.#compileWithImports(cssPaths, sourcePaths);
532
+ this.importedStylesheetsByBasePath[basePath] = imported;
1103
533
  this.#logger.verbose(`css base=${basePath} paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`);
1104
534
  return [basePath, css];
1105
535
  })
@@ -1109,14 +539,14 @@ class CssCompiler {
1109
539
  return this.#cssTextByBasePath;
1110
540
  }
1111
541
  async#warnUnreachableStylesheets() {
1112
- const pageDir = path8.join(this.#app.cwdPath, "page");
542
+ const pageDir = path5.join(this.#app.cwdPath, "page");
1113
543
  const glob = new Bun.Glob("**/*.css");
1114
544
  for await (const cssPath of glob.scan({ cwd: pageDir, absolute: true })) {
1115
- if (cssPath.includes(`${path8.sep}(libs)${path8.sep}`))
545
+ if (cssPath.includes(`${path5.sep}(libs)${path5.sep}`))
1116
546
  continue;
1117
547
  if (this.#discoveredCssPaths.has(cssPath))
1118
548
  continue;
1119
- this.#logger.warn(`css ${path8.relative(this.#app.cwdPath, cssPath)} is imported by no route and never compiled`);
549
+ this.#logger.warn(`css ${path5.relative(this.#app.cwdPath, cssPath)} is imported by no route and never compiled`);
1120
550
  }
1121
551
  }
1122
552
  async discoverCss({ refresh } = {}) {
@@ -1128,7 +558,7 @@ class CssCompiler {
1128
558
  pageKeys
1129
559
  } = {}) {
1130
560
  pageKeys ??= await this.#app.getPageKeys({ refresh });
1131
- const seeds = pageKeys.map((key) => path8.resolve(this.#app.cwdPath, "page", key));
561
+ const seeds = pageKeys.map((key) => path5.resolve(this.#app.cwdPath, "page", key));
1132
562
  const cssFiles = new Set;
1133
563
  const sourceFiles = new Set;
1134
564
  const queue = [...seeds];
@@ -1160,7 +590,7 @@ class CssCompiler {
1160
590
  } catch {
1161
591
  continue;
1162
592
  }
1163
- const importerDir = path8.dirname(filePath);
593
+ const importerDir = path5.dirname(filePath);
1164
594
  for (const imp of imports) {
1165
595
  const spec = imp.path;
1166
596
  if (!spec)
@@ -1185,35 +615,40 @@ class CssCompiler {
1185
615
  return { cssPaths, sourcePaths: [...sourceFiles] };
1186
616
  }
1187
617
  async#libTokenStylesheets(sourceFiles) {
1188
- const libsRoot = path8.join(this.#app.workspace.workspaceRoot, "libs");
618
+ const libsRoot = path5.join(this.#app.workspace.workspaceRoot, "libs");
1189
619
  const libNames = new Set;
1190
620
  for (const filePath of sourceFiles) {
1191
- const relPath = path8.relative(libsRoot, filePath);
1192
- if (relPath.startsWith("..") || path8.isAbsolute(relPath))
621
+ const relPath = path5.relative(libsRoot, filePath);
622
+ if (relPath.startsWith("..") || path5.isAbsolute(relPath))
1193
623
  continue;
1194
- const [libName] = relPath.split(path8.sep);
624
+ const [libName] = relPath.split(path5.sep);
1195
625
  if (libName)
1196
626
  libNames.add(libName);
1197
627
  }
1198
628
  const tokenPaths = await Promise.all([...libNames].sort().map(async (libName) => {
1199
- const tokensPath = path8.join(libsRoot, libName, "ui/tokens.css");
629
+ const tokensPath = path5.join(libsRoot, libName, "ui/tokens.css");
1200
630
  return await this.#fileExists(tokensPath) ? tokensPath : null;
1201
631
  }));
1202
632
  return tokenPaths.filter((tokensPath) => !!tokensPath);
1203
633
  }
1204
634
  async compileCss(cssPaths, sourcePaths) {
635
+ const { css } = await this.#compileWithImports(cssPaths, sourcePaths);
636
+ return css;
637
+ }
638
+ async#compileWithImports(cssPaths, sourcePaths) {
1205
639
  if (cssPaths.length === 0)
1206
- return "";
640
+ return { css: "", imported: [] };
1207
641
  const compileStarted = Date.now();
1208
642
  const compilers = await Promise.all(cssPaths.map(async (cssPath) => {
1209
643
  const css = await Bun.file(cssPath).text();
1210
- const base = path8.dirname(cssPath);
644
+ const base = path5.dirname(cssPath);
645
+ const imported = new Map;
1211
646
  const compiler = await compile(css, {
1212
647
  base,
1213
- loadStylesheet: (id, fromBase) => this.#loadStylesheet(id, fromBase),
648
+ loadStylesheet: (id, fromBase) => this.#loadStylesheet(id, fromBase, imported),
1214
649
  loadModule: (id, fromBase) => this.#loadModule(id, fromBase)
1215
650
  });
1216
- return { cssPath, compiler };
651
+ return { cssPath, compiler, imported };
1217
652
  }));
1218
653
  const sourceDirs = new Set;
1219
654
  for (const entry of compilers) {
@@ -1226,24 +661,35 @@ class CssCompiler {
1226
661
  const candidates = await this.#scanCandidates(sourcePaths, [...sourceDirs]);
1227
662
  this.#logger.verbose(`css candidates scanned count=${candidates.length} sources=${sourcePaths.length} dirs=${sourceDirs.size} in ${Date.now() - scanStarted}ms`);
1228
663
  const parts = [];
664
+ const imported = [];
1229
665
  for (const entry of compilers) {
1230
666
  if (!entry)
1231
667
  continue;
1232
- parts.push(entry.compiler.build(candidates));
668
+ const part = entry.compiler.build(candidates);
669
+ parts.push(part);
670
+ for (const [cssPath, content] of entry.imported) {
671
+ const declaredNames = declaredCustomProperties(content);
672
+ imported.push({ cssPath, declaredNames });
673
+ if (declaredNames.length === 0 || declaredNames.some((name) => part.includes(`${name}:`)))
674
+ continue;
675
+ this.#logger.warn(`css @import ${cssPath} was loaded by ${entry.cssPath} but none of its ${declaredNames.length} declaration(s) reached the compiled CSS`);
676
+ }
1233
677
  }
1234
678
  this.#logger.verbose(`css compiled paths=${cssPaths.length} candidates=${candidates.length} in ${Date.now() - compileStarted}ms`);
1235
- return parts.join(`
1236
- `);
679
+ return { css: parts.join(`
680
+ `), imported };
1237
681
  }
1238
- async#loadStylesheet(id, fromBase) {
682
+ async#loadStylesheet(id, fromBase, imported) {
1239
683
  const p = await this.#resolveCssImport(id, fromBase);
1240
684
  this.#discoveredCssPaths.add(p);
1241
685
  const content = await Bun.file(p).text();
1242
- return { path: p, base: path8.dirname(p), content };
686
+ imported?.set(p, content);
687
+ this.#logger.verbose(`css import "${id}" from ${fromBase} -> ${p} (${content.length} bytes)`);
688
+ return { path: p, base: path5.dirname(p), content };
1243
689
  }
1244
690
  async#resolveCssImport(id, fromBase) {
1245
691
  if (id.startsWith(".") || id.startsWith("/")) {
1246
- const filePath = path8.resolve(fromBase, id);
692
+ const filePath = path5.resolve(fromBase, id);
1247
693
  if (await this.#fileExists(filePath))
1248
694
  return filePath;
1249
695
  throw new Error(`[css] failed to resolve stylesheet import "${id}" from ${fromBase} (no file at ${filePath})`);
@@ -1260,89 +706,654 @@ class CssCompiler {
1260
706
  this.#cssImportResolver = await CssImportResolver.create(this.#app);
1261
707
  return this.#cssImportResolver;
1262
708
  }
1263
- async#loadModule(id, fromBase) {
1264
- const p = __require.resolve(id, { paths: [fromBase] });
1265
- const mod = await import(p);
1266
- return { path: p, base: path8.dirname(p), module: mod.default ?? mod };
709
+ async#loadModule(id, fromBase) {
710
+ const p = __require.resolve(id, { paths: [fromBase] });
711
+ const mod = await import(p);
712
+ return { path: p, base: path5.dirname(p), module: mod.default ?? mod };
713
+ }
714
+ #resolveSourceImport(id, fromBase, resolvePackage) {
715
+ const cacheKey = `${fromBase}\x00${id}`;
716
+ let cached = this.#resolvedSpecifierCache.get(cacheKey);
717
+ if (cached)
718
+ return cached;
719
+ cached = this.#resolveSourceImportUncached(id, fromBase, resolvePackage);
720
+ this.#resolvedSpecifierCache.set(cacheKey, cached);
721
+ return cached;
722
+ }
723
+ async#resolveSourceImportUncached(id, fromBase, resolvePackage) {
724
+ if (id.startsWith(".") || id.startsWith("/")) {
725
+ const abs = id.startsWith("/") ? id : path5.resolve(fromBase, id);
726
+ return this.#resolveSourceFileCandidate(abs);
727
+ }
728
+ const pkg = await resolvePackage(id);
729
+ if (pkg)
730
+ return pkg.entryFile;
731
+ for (const resolve of [() => resolveSourceWithBun(id, fromBase), () => resolveSourceWithRequire(id, fromBase)]) {
732
+ const resolved = await resolve();
733
+ if (resolved)
734
+ return resolved;
735
+ }
736
+ return null;
737
+ }
738
+ async#scanCandidates(sourcePaths, dirs) {
739
+ const candidates = new Set;
740
+ const glob = new Bun.Glob("**/*.{tsx,ts,jsx,js,html}");
741
+ const files = new Set(sourcePaths);
742
+ await Promise.all(dirs.map(async (dir) => {
743
+ for await (const file of glob.scan({ cwd: dir, absolute: true })) {
744
+ if (isIgnoredNodeModuleSource(file))
745
+ continue;
746
+ files.add(file);
747
+ }
748
+ }));
749
+ const cache = await new CssCandidateCache(this.#candidateCachePath).load();
750
+ await Promise.all([...files].map(async (file) => {
751
+ for (const candidate of await cache.candidatesFor(file))
752
+ candidates.add(candidate);
753
+ }));
754
+ await cache.save(files);
755
+ this.#logger.verbose(`css candidate cache reused=${cache.reused} rescanned=${cache.rescanned}`);
756
+ return [...candidates];
757
+ }
758
+ }
759
+ function resolveSourceWithBun(id, fromBase) {
760
+ try {
761
+ const resolved = Bun.resolveSync(id, fromBase);
762
+ return isSourceFile(resolved) ? resolved : null;
763
+ } catch {
764
+ return null;
765
+ }
766
+ }
767
+ function resolveSourceWithRequire(id, fromBase) {
768
+ try {
769
+ const resolved = __require.resolve(id, { paths: [fromBase] });
770
+ return isSourceFile(resolved) ? resolved : null;
771
+ } catch {
772
+ return null;
773
+ }
774
+ }
775
+ function isSourceFile(filePath) {
776
+ return SOURCE_EXTS.includes(path5.extname(filePath));
777
+ }
778
+ function isIgnoredNodeModuleSource(filePath) {
779
+ return NODE_MODULES_RE.test(filePath) && !AKANJS_NODE_MODULE_RE.test(filePath);
780
+ }
781
+ function declaredCustomProperties(css) {
782
+ const withoutThemeBlocks = css.replace(/@theme[^{]*\{[^}]*\}/g, "");
783
+ return [...new Set([...withoutThemeBlocks.matchAll(/(?:^|[\s;{])(--[\w-]+)\s*:/g)].map(([, name]) => name))].filter((name) => !!name);
784
+ }
785
+ function getPageKeyBasePath(pageKey, basePaths) {
786
+ const normalized = pageKey.split(path5.sep).join("/").replace(/^\.\//, "");
787
+ const segments = normalized.split("/");
788
+ const firstPublicSegment = segments.find((segment) => segment !== "[lang]" && !/^\(.+\)$/.test(segment));
789
+ return firstPublicSegment && basePaths.includes(firstPublicSegment) ? firstPublicSegment : null;
790
+ }
791
+
792
+ // pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
793
+ import path8 from "path";
794
+
795
+ // pkgs/@akanjs/devkit/transforms/externalizeFrameworkPlugin.ts
796
+ import path6 from "path";
797
+ var DEFAULT_INCLUDE = ["akanjs/", "@apps/", "@libs/"];
798
+ var DEFAULT_EXCLUDE_EXACT = new Set(["akanjs/webkit", "@akanjs/cli", "@akanjs/devkit"]);
799
+ var DEFAULT_EXCLUDE_PREFIX = ["@akanjs/cli/", "@akanjs/devkit/"];
800
+ var OPTIONAL_BACKEND_EXTERNAL_EXACT = new Set([
801
+ "@libsql/client",
802
+ "bullmq",
803
+ "ioredis",
804
+ "postgres",
805
+ "protobufjs"
806
+ ]);
807
+ var RUNTIME_EXTERNAL_EXACT = new Set([
808
+ "react",
809
+ "react-dom",
810
+ "react/jsx-runtime",
811
+ "react/jsx-dev-runtime",
812
+ "react-server-dom-webpack",
813
+ "react-server-dom-webpack/server.node",
814
+ "react-server-dom-webpack/client.node",
815
+ "react-server-dom-webpack/client.browser"
816
+ ]);
817
+ var RUNTIME_EXTERNAL_PREFIX = ["react-dom/", "react-server-dom-webpack/"];
818
+ var CANDIDATE_EXTS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
819
+ async function createExternalizeFrameworkPlugin(options) {
820
+ const tsconfig = await options.app.getTsConfig();
821
+ const includePrefixes = options.include ?? DEFAULT_INCLUDE;
822
+ const extraExact = new Set(options.extra ?? []);
823
+ const workspaceRoot = options.app.workspace.workspaceRoot;
824
+ const tsconfigPaths = tsconfig.compilerOptions.paths ?? {};
825
+ const rootEntries = Object.entries(tsconfigPaths).filter(([k]) => !k.endsWith("/*")).map(([k, v]) => ({ pkg: k, entryFile: v[0] ?? null })).filter((e) => e.entryFile !== null);
826
+ const wildcardEntries = Object.entries(tsconfigPaths).filter(([k]) => k.endsWith("/*")).map(([k, v]) => ({ prefix: k.slice(0, -1), replacements: v })).sort((a, b) => b.prefix.length - a.prefix.length);
827
+ async function resolveWorkspaceSubpath(spec) {
828
+ if (!workspaceRoot)
829
+ return null;
830
+ for (const { prefix, replacements } of wildcardEntries) {
831
+ if (!spec.startsWith(prefix))
832
+ continue;
833
+ const suffix = spec.slice(prefix.length);
834
+ for (const repl of replacements) {
835
+ const replPath = repl?.endsWith("/*") ? repl.slice(0, -1) : repl ?? "";
836
+ if (!replPath)
837
+ continue;
838
+ const candidate = path6.resolve(workspaceRoot, replPath + suffix);
839
+ const hit = await firstExisting(candidate);
840
+ if (hit)
841
+ return hit;
842
+ }
843
+ }
844
+ for (const { pkg, entryFile } of rootEntries) {
845
+ if (spec !== pkg && !spec.startsWith(`${pkg}/`))
846
+ continue;
847
+ if (spec === pkg)
848
+ continue;
849
+ const suffix = spec.slice(pkg.length + 1);
850
+ const pkgDir = path6.dirname(path6.resolve(workspaceRoot, entryFile));
851
+ const candidate = path6.join(pkgDir, suffix);
852
+ const hit = await firstExisting(candidate);
853
+ if (hit)
854
+ return hit;
855
+ }
856
+ return null;
857
+ }
858
+ return {
859
+ name: "akan-externalize-framework",
860
+ setup(build) {
861
+ build.onResolve({ filter: /.*/ }, async (args) => {
862
+ const spec = args.path;
863
+ if (spec === "." || spec === ".." || spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/"))
864
+ return;
865
+ if (extraExact.has(spec) || DEFAULT_EXCLUDE_EXACT.has(spec))
866
+ return { path: spec, external: true };
867
+ for (const prefix of DEFAULT_EXCLUDE_PREFIX) {
868
+ if (spec.startsWith(prefix))
869
+ return { path: spec, external: true };
870
+ }
871
+ if (OPTIONAL_BACKEND_EXTERNAL_EXACT.has(spec))
872
+ return { path: spec, external: true };
873
+ if (RUNTIME_EXTERNAL_EXACT.has(spec))
874
+ return { path: spec, external: true };
875
+ for (const prefix of RUNTIME_EXTERNAL_PREFIX) {
876
+ if (spec.startsWith(prefix))
877
+ return { path: spec, external: true };
878
+ }
879
+ for (const prefix of includePrefixes) {
880
+ if (!spec.startsWith(prefix))
881
+ continue;
882
+ const resolved = await resolveWorkspaceSubpath(spec);
883
+ if (resolved)
884
+ return { path: resolved };
885
+ return;
886
+ }
887
+ return;
888
+ });
889
+ }
890
+ };
891
+ }
892
+ async function firstExisting(basePath) {
893
+ if (await Bun.file(basePath).exists())
894
+ return basePath;
895
+ for (const ext of CANDIDATE_EXTS) {
896
+ const candidate = `${basePath}${ext}`;
897
+ if (await Bun.file(candidate).exists())
898
+ return candidate;
899
+ }
900
+ for (const ext of CANDIDATE_EXTS) {
901
+ const candidate = path6.join(basePath, `index${ext}`);
902
+ if (await Bun.file(candidate).exists())
903
+ return candidate;
904
+ }
905
+ return null;
906
+ }
907
+
908
+ // pkgs/@akanjs/devkit/transforms/useClientBundlePlugin.ts
909
+ function createUseClientBundlePlugin(options = {}) {
910
+ return {
911
+ name: "akan-use-client-bundle",
912
+ setup(build) {
913
+ build.onLoad({ filter: /\.(tsx|ts|jsx|js)$/ }, async (args) => {
914
+ if (args.path.includes("/node_modules/"))
915
+ return;
916
+ let source;
917
+ try {
918
+ source = await Bun.file(args.path).text();
919
+ } catch {
920
+ return;
921
+ }
922
+ const stubbed = transformUseClient(source, {
923
+ path: args.path,
924
+ workspaceRoot: options.workspaceRoot
925
+ });
926
+ if (stubbed === null)
927
+ return;
928
+ return { contents: stubbed, loader: loaderFor(args.path) };
929
+ });
930
+ }
931
+ };
932
+ }
933
+ function loaderFor(absPath) {
934
+ if (absPath.endsWith(".tsx"))
935
+ return "tsx";
936
+ if (absPath.endsWith(".jsx"))
937
+ return "jsx";
938
+ if (absPath.endsWith(".ts"))
939
+ return "ts";
940
+ return "js";
941
+ }
942
+
943
+ // pkgs/@akanjs/devkit/frontendBuild/pagesEntrySourceGenerator.ts
944
+ import path7 from "path";
945
+ class PagesEntrySourceGenerator {
946
+ #pageEntries;
947
+ constructor(pageEntries) {
948
+ this.#pageEntries = pageEntries;
949
+ }
950
+ static generate(pageEntries) {
951
+ return new PagesEntrySourceGenerator(pageEntries).generate();
952
+ }
953
+ generate() {
954
+ const lines = this.#pageEntries.map(({ key, moduleAbsPath }) => {
955
+ const specifier = PagesEntrySourceGenerator.#toImportSpecifier(moduleAbsPath);
956
+ return ` ${JSON.stringify(key)}: () => import(${JSON.stringify(specifier)}),`;
957
+ });
958
+ return `export const pages = {
959
+ ${lines.join(`
960
+ `)}
961
+ };
962
+ `;
963
+ }
964
+ static async generateStatic(pageEntries) {
965
+ return await new PagesEntrySourceGenerator(pageEntries).generateStatic();
966
+ }
967
+ async generateStatic() {
968
+ const imports = this.#pageEntries.map(({ moduleAbsPath }, index) => {
969
+ const specifier = PagesEntrySourceGenerator.#toImportSpecifier(moduleAbsPath);
970
+ return `import * as page${index} from ${JSON.stringify(specifier)};`;
971
+ });
972
+ const entries = await Promise.all(this.#pageEntries.map(async ({ key, moduleAbsPath }, index) => {
973
+ const isAsyncDefault = await AsyncDefaultExportDetector.detect(moduleAbsPath);
974
+ return ` ${JSON.stringify(key)}: { loader: async () => page${index}, isAsyncDefault: ${isAsyncDefault} },`;
975
+ }));
976
+ return `${imports.join(`
977
+ `)}
978
+ export const pages = {
979
+ ${entries.join(`
980
+ `)}
981
+ };
982
+ `;
983
+ }
984
+ static #toImportSpecifier(moduleAbsPath) {
985
+ return path7.resolve(moduleAbsPath).split(path7.sep).join("/");
986
+ }
987
+ }
988
+
989
+ // pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
990
+ var VIRTUAL_PAGES_ENTRY = "akan-pages-entry";
991
+
992
+ class PagesBundleBuilder {
993
+ #app;
994
+ #command;
995
+ #pageEntries;
996
+ #started = Date.now();
997
+ constructor(app, command = "start", pageEntries) {
998
+ this.#app = app;
999
+ this.#command = command;
1000
+ this.#pageEntries = pageEntries;
1001
+ }
1002
+ async build() {
1003
+ const akanConfig = await this.#app.getConfig();
1004
+ const resolvedEntries = this.#pageEntries ?? await resolveSsrPageEntriesForApp(this.#app, await this.#app.getPageKeys());
1005
+ const entrySource = PagesEntrySourceGenerator.generate(resolvedEntries);
1006
+ const workspaceRoot = this.#app.workspace.workspaceRoot;
1007
+ const result = await Bun.build({
1008
+ entrypoints: [VIRTUAL_PAGES_ENTRY],
1009
+ outdir: `${this.#artifactDir}/server`,
1010
+ target: "bun",
1011
+ format: "esm",
1012
+ splitting: this.#splitting,
1013
+ minify: this.#command === "build",
1014
+ naming: {
1015
+ entry: "pages-[hash].[ext]",
1016
+ chunk: "chunks/[name]-[hash].[ext]",
1017
+ asset: "assets/[name]-[hash].[ext]"
1018
+ },
1019
+ define: this.#define(),
1020
+ plugins: [
1021
+ PagesBundleBuilder.createPagesEntryPlugin(entrySource),
1022
+ PagesBundleBuilder.createCssStubPlugin(),
1023
+ PagesBundleBuilder.createServerUseClientFetchPlugin(),
1024
+ await createExternalizeFrameworkPlugin({ app: this.#app, extra: akanConfig.externalLibs }),
1025
+ akanConfig.barrelImports.length > 0 ? await createBarrelImportsPlugin(this.#app, {
1026
+ pipeAfter: (source, args) => transformUseClient(source, {
1027
+ path: args.path,
1028
+ workspaceRoot
1029
+ })
1030
+ }) : createUseClientBundlePlugin({ workspaceRoot })
1031
+ ]
1032
+ });
1033
+ if (!result.success)
1034
+ throw new AggregateError(result.logs, "[PagesBundleBuilder] Bun.build failed");
1035
+ const entryArtifact = result.outputs.find((a) => a.kind === "entry-point");
1036
+ if (!entryArtifact)
1037
+ throw new Error("[PagesBundleBuilder] Bun.build emitted no entry-point artifact");
1038
+ const bundlePath = path8.resolve(entryArtifact.path);
1039
+ const buildId = Date.now();
1040
+ const outputBytes = result.outputs.reduce((sum, output) => sum + output.size, 0);
1041
+ const chunkCount = result.outputs.filter((output) => output.kind === "chunk").length;
1042
+ this.#app.verbose(`[PagesBundleBuilder] ${path8.basename(bundlePath)} emitted in ${Date.now() - this.#started}ms splitting=${this.#splitting} entry=${entryArtifact.size} bytes outputs=${result.outputs.length} chunks=${chunkCount} total=${outputBytes} bytes`);
1043
+ return {
1044
+ bundlePath,
1045
+ buildId,
1046
+ splitting: this.#splitting,
1047
+ entryBytes: entryArtifact.size,
1048
+ outputBytes,
1049
+ outputCount: result.outputs.length,
1050
+ chunkCount
1051
+ };
1052
+ }
1053
+ get #artifactDir() {
1054
+ return `${this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath}/.akan/artifact`;
1055
+ }
1056
+ get #splitting() {
1057
+ return process.env.AKAN_SERVER_PAGES_SPLITTING === "1";
1058
+ }
1059
+ #define() {
1060
+ const nodeEnv = this.#command === "build" ? "production" : "development";
1061
+ return {
1062
+ "process.env.NODE_ENV": JSON.stringify(nodeEnv),
1063
+ "process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("ssr"),
1064
+ ...Object.fromEntries(Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]))
1065
+ };
1066
+ }
1067
+ static createPagesEntryPlugin(source) {
1068
+ return {
1069
+ name: "akan-pages-entry",
1070
+ setup(build) {
1071
+ build.onResolve({ filter: /^akan-pages-entry$/ }, () => ({
1072
+ path: VIRTUAL_PAGES_ENTRY,
1073
+ namespace: "akan-virtual"
1074
+ }));
1075
+ build.onLoad({ filter: /^akan-pages-entry$/, namespace: "akan-virtual" }, () => ({
1076
+ contents: source,
1077
+ loader: "tsx"
1078
+ }));
1079
+ }
1080
+ };
1081
+ }
1082
+ static createCssStubPlugin() {
1083
+ return {
1084
+ name: "akan-css-stub",
1085
+ setup(build) {
1086
+ build.onLoad({ filter: /\.css$/ }, () => ({
1087
+ contents: "",
1088
+ loader: "js"
1089
+ }));
1090
+ }
1091
+ };
1092
+ }
1093
+ static createServerUseClientFetchPlugin() {
1094
+ return {
1095
+ name: "akan-server-use-client-fetch",
1096
+ setup(build) {
1097
+ build.onLoad({ filter: /[/\\]lib[/\\]useClient\.(ts|tsx|js|jsx)$/ }, async (args) => {
1098
+ const source = await Bun.file(args.path).text();
1099
+ const transformed = PagesBundleBuilder.transformServerUseClientFetchSource(source);
1100
+ if (transformed === source)
1101
+ return;
1102
+ return { contents: transformed, loader: loaderFor2(args.path) };
1103
+ });
1104
+ }
1105
+ };
1106
+ }
1107
+ static transformServerUseClientFetchSource(source) {
1108
+ if (!source.includes(`with { type: "macro" }`) || !source.includes("FetchClient.build"))
1109
+ return source;
1110
+ return source.replace(/import\s+\{\s*getSerializedSignal\s*\}\s+from\s+["']\.\/sig["']\s+with\s+\{\s*type\s*:\s*["']macro["']\s*\};/, `import { fetch as serverFetch } from "./sig";`).replace(/const\s+fetchProto\s*=\s*FetchClient\.build<[^;]+;/, "const fetchProto = FetchClient.build<typeof signal>(cnst, serverFetch.serializedSignal, { Err: pageProto.Err, base: serverFetch });");
1111
+ }
1112
+ }
1113
+ function loaderFor2(absPath) {
1114
+ if (absPath.endsWith(".tsx"))
1115
+ return "tsx";
1116
+ if (absPath.endsWith(".jsx"))
1117
+ return "jsx";
1118
+ if (absPath.endsWith(".ts"))
1119
+ return "ts";
1120
+ return "js";
1121
+ }
1122
+
1123
+ // pkgs/@akanjs/devkit/frontendBuild/csrArtifactBuilder.ts
1124
+ import { mkdir, rm, unlink } from "fs/promises";
1125
+ import path9 from "path";
1126
+ class CsrArtifactBuilder {
1127
+ #app;
1128
+ #command;
1129
+ #lang;
1130
+ constructor(app, command = "start", lang = "en") {
1131
+ this.#app = app;
1132
+ this.#command = command;
1133
+ this.#lang = lang;
1134
+ }
1135
+ async build() {
1136
+ const pageKeys = await this.#app.getPageKeys();
1137
+ if (pageKeys.length === 0) {
1138
+ this.#app.log(`[cli] no route files under ${this.#app.cwdPath}/page \u2014 skipping CSR build`);
1139
+ return null;
1140
+ }
1141
+ const pageEntries = await resolveSsrPageEntriesForApp(this.#app, pageKeys);
1142
+ const akanConfig = await this.#app.getConfig();
1143
+ const cssAssets = await this.#loadCssAssets();
1144
+ const basePaths = [...akanConfig.basePaths];
1145
+ const htmlBasePaths = basePaths.length > 0 ? basePaths : [""];
1146
+ await rm(this.#outputDir, { recursive: true, force: true });
1147
+ await mkdir(this.#generatedDir, { recursive: true });
1148
+ const generatedFiles = Object.fromEntries((await Promise.all(htmlBasePaths.map(async (basePath) => [
1149
+ this.#createHtmlFile(basePath),
1150
+ await this.#createEntryFile(basePath, CsrArtifactBuilder.pageEntriesForBasePath(pageEntries, basePath, basePaths))
1151
+ ]))).flat());
1152
+ const result = await Bun.build({
1153
+ target: "browser",
1154
+ entrypoints: htmlBasePaths.map((basePath) => this.#generatedPath(CsrArtifactBuilder.htmlFilename(basePath))),
1155
+ files: generatedFiles,
1156
+ root: this.#generatedDir,
1157
+ outdir: this.#outputDir,
1158
+ splitting: false,
1159
+ minify: true,
1160
+ env: "AKAN_PUBLIC_*",
1161
+ define: this.#define(),
1162
+ optimizeImports: akanConfig.optimizeImports,
1163
+ plugins: [PagesBundleBuilder.createCssStubPlugin()]
1164
+ });
1165
+ if (!result.success) {
1166
+ const logs = result.logs.map((log) => log.message).join(`
1167
+ `);
1168
+ throw new Error(`[csr-build] failed${logs ? `
1169
+ ${logs}` : ""}`);
1170
+ }
1171
+ await this.#inlineCsrArtifacts(cssAssets);
1172
+ this.#app.verbose(`[csr-build] output -> ${this.#outputDir}`);
1173
+ return { outputDir: this.#outputDir };
1174
+ }
1175
+ static pageEntriesForBasePath(pageEntries, basePath, basePaths) {
1176
+ return pageEntries.filter((entry) => {
1177
+ const entryBasePath = getPageKeyBasePath(entry.key, basePaths);
1178
+ return entryBasePath === null || entryBasePath === basePath;
1179
+ });
1180
+ }
1181
+ static htmlFilename(basePath) {
1182
+ return `${basePath || "index"}.html`;
1183
+ }
1184
+ static entryFilename(basePath) {
1185
+ return `${basePath || "index"}.csr.tsx`;
1186
+ }
1187
+ static basePathOfHtml(htmlPath) {
1188
+ const name = path9.basename(htmlPath, ".html");
1189
+ return name === "index" ? "" : name;
1190
+ }
1191
+ get #outputDir() {
1192
+ return path9.join(this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath, this.#command === "build" ? "csr" : ".akan/artifact/csr");
1193
+ }
1194
+ get #generatedDir() {
1195
+ return path9.join(this.#app.cwdPath, ".akan/generated/csr");
1196
+ }
1197
+ get #artifactDir() {
1198
+ return path9.join(this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath, ".akan/artifact");
1199
+ }
1200
+ #generatedPath(filename) {
1201
+ return path9.join(this.#generatedDir, filename);
1202
+ }
1203
+ #define() {
1204
+ const nodeEnv = this.#command === "build" ? "production" : "development";
1205
+ return {
1206
+ "process.env.NODE_ENV": JSON.stringify(nodeEnv),
1207
+ "process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("csr"),
1208
+ ...Object.fromEntries(Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]))
1209
+ };
1210
+ }
1211
+ async#createEntryFile(basePath, pageEntries) {
1212
+ return [
1213
+ this.#generatedPath(CsrArtifactBuilder.entryFilename(basePath)),
1214
+ `import { bootCsr } from "akanjs/webkit";
1215
+ ${await PagesEntrySourceGenerator.generateStatic(pageEntries)}
1216
+ void bootCsr(pages);
1217
+ `
1218
+ ];
1219
+ }
1220
+ #createHtmlFile(basePath) {
1221
+ return [
1222
+ this.#generatedPath(CsrArtifactBuilder.htmlFilename(basePath)),
1223
+ `<!doctype html>
1224
+ <html lang="${this.#lang}">
1225
+ <head>
1226
+ <meta charset="utf-8" />
1227
+ <title>${this.#app.name}</title>
1228
+ <base href="/" />
1229
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1230
+ </head>
1231
+ <body>
1232
+ <div id="root"></div>
1233
+ <script type="module" src="./${CsrArtifactBuilder.entryFilename(basePath)}"></script>
1234
+ </body>
1235
+ </html>
1236
+ `
1237
+ ];
1238
+ }
1239
+ async#loadCssAssets() {
1240
+ const artifactFile = Bun.file(path9.join(this.#artifactDir, "base-artifact.json"));
1241
+ if (!await artifactFile.exists())
1242
+ return {};
1243
+ const artifact = await artifactFile.json();
1244
+ return artifact.cssAssets ?? {};
1245
+ }
1246
+ async#inlineCsrArtifacts(cssAssets) {
1247
+ const jsFiles = new Set;
1248
+ for (const htmlPath of await this.#htmlOutputPaths()) {
1249
+ const htmlFile = Bun.file(htmlPath);
1250
+ if (!await htmlFile.exists())
1251
+ continue;
1252
+ const basePath = CsrArtifactBuilder.basePathOfHtml(htmlPath);
1253
+ const cssAsset = cssAssets[basePath];
1254
+ if (!cssAsset) {
1255
+ this.#app.logger.warn(`[csr-build] base-artifact.json has no compiled stylesheet for ${basePath || "root"}; ${path9.basename(htmlPath)} ships without CSS`);
1256
+ }
1257
+ const inlined = await this.#inlineHtmlAssets(await htmlFile.text(), htmlPath, cssAsset);
1258
+ for (const filePath of inlined.jsFiles)
1259
+ jsFiles.add(filePath);
1260
+ await Bun.write(htmlPath, inlined.html);
1261
+ }
1262
+ for (const filePath of jsFiles)
1263
+ await unlink(filePath).catch(() => {
1264
+ return;
1265
+ });
1266
+ const remainingAssets = await this.#listOutputFiles((filePath) => filePath.endsWith(".js") || filePath.endsWith(".css"));
1267
+ if (remainingAssets.length > 0) {
1268
+ throw new Error(`[csr-build] expected single-file HTML, but CSR assets remain:
1269
+ ${remainingAssets.join(`
1270
+ `)}`);
1271
+ }
1272
+ }
1273
+ async#inlineHtmlAssets(html, htmlPath, cssAsset) {
1274
+ let next = html;
1275
+ if (cssAsset) {
1276
+ const css = await Bun.file(path9.join(this.#artifactDir, cssAsset.cssRelPath)).text();
1277
+ next = CsrArtifactBuilder.injectBeforeHeadEnd(next, CsrArtifactBuilder.createInlineStyle(css));
1278
+ }
1279
+ const jsFiles = [];
1280
+ next = await CsrArtifactBuilder.replaceModuleScriptSrc(next, async (src) => {
1281
+ const jsPath = CsrArtifactBuilder.resolveHtmlAssetPath(htmlPath, src);
1282
+ jsFiles.push(jsPath);
1283
+ return await Bun.file(jsPath).text();
1284
+ });
1285
+ return { html: next, jsFiles };
1286
+ }
1287
+ async#htmlOutputPaths() {
1288
+ return await this.#listOutputFiles((filePath) => filePath.endsWith(".html"));
1289
+ }
1290
+ async#listOutputFiles(predicate) {
1291
+ const glob = new Bun.Glob("**/*");
1292
+ const files = [];
1293
+ for await (const filePath of glob.scan({ cwd: this.#outputDir, absolute: true })) {
1294
+ if (predicate(filePath))
1295
+ files.push(filePath);
1296
+ }
1297
+ return files.sort();
1298
+ }
1299
+ static injectBeforeHeadEnd(html, snippet) {
1300
+ const scannable = CsrArtifactBuilder.blankEmbeddedContent(html);
1301
+ const headEnd = scannable.search(/<\/head\s*>/i);
1302
+ if (headEnd !== -1)
1303
+ return `${html.slice(0, headEnd)}${snippet}
1304
+ ${html.slice(headEnd)}`;
1305
+ const bodyStart = scannable.search(/<body(?:\s|>)/i);
1306
+ if (bodyStart !== -1)
1307
+ return `${html.slice(0, bodyStart)}${snippet}
1308
+ ${html.slice(bodyStart)}`;
1309
+ return `${html}
1310
+ ${snippet}`;
1311
+ }
1312
+ static blankEmbeddedContent(html) {
1313
+ return html.replace(/<script\b[^>]*>[\s\S]*?<\/script\s*>|<style\b[^>]*>[\s\S]*?<\/style\s*>|<!--[\s\S]*?-->/gi, (match) => " ".repeat(match.length));
1267
1314
  }
1268
- #resolveSourceImport(id, fromBase, resolvePackage) {
1269
- const cacheKey = `${fromBase}\x00${id}`;
1270
- let cached = this.#resolvedSpecifierCache.get(cacheKey);
1271
- if (cached)
1272
- return cached;
1273
- cached = this.#resolveSourceImportUncached(id, fromBase, resolvePackage);
1274
- this.#resolvedSpecifierCache.set(cacheKey, cached);
1275
- return cached;
1315
+ static createInlineStyle(css) {
1316
+ return `<style data-akan-css="active">
1317
+ ${css.replace(/<\/style/gi, "<\\/style")}
1318
+ </style>`;
1276
1319
  }
1277
- async#resolveSourceImportUncached(id, fromBase, resolvePackage) {
1278
- if (id.startsWith(".") || id.startsWith("/")) {
1279
- const abs = id.startsWith("/") ? id : path8.resolve(fromBase, id);
1280
- return this.#resolveSourceFileCandidate(abs);
1281
- }
1282
- const pkg = await resolvePackage(id);
1283
- if (pkg)
1284
- return pkg.entryFile;
1285
- for (const resolve of [() => resolveSourceWithBun(id, fromBase), () => resolveSourceWithRequire(id, fromBase)]) {
1286
- const resolved = await resolve();
1287
- if (resolved)
1288
- return resolved;
1320
+ static async replaceModuleScriptSrc(html, loadScript) {
1321
+ const scriptRe = /<script\b(?=[^>]*\btype=["']module["'])(?=[^>]*\bsrc=["']([^"']+)["'])[^>]*>\s*<\/script>/gi;
1322
+ let result = "";
1323
+ let lastIndex = 0;
1324
+ let matched = false;
1325
+ for (const match of html.matchAll(scriptRe)) {
1326
+ const full = match[0];
1327
+ const src = match[1];
1328
+ if (match.index === undefined || !src)
1329
+ continue;
1330
+ matched = true;
1331
+ result += html.slice(lastIndex, match.index);
1332
+ result += `<script type="module">
1333
+ ${CsrArtifactBuilder.escapeInlineScript(await loadScript(src))}
1334
+ </script>`;
1335
+ lastIndex = match.index + full.length;
1289
1336
  }
1290
- return null;
1291
- }
1292
- async#scanCandidates(sourcePaths, dirs) {
1293
- const candidates = new Set;
1294
- const glob = new Bun.Glob("**/*.{tsx,ts,jsx,js,html}");
1295
- const files = new Set(sourcePaths);
1296
- await Promise.all(dirs.map(async (dir) => {
1297
- for await (const file of glob.scan({ cwd: dir, absolute: true })) {
1298
- if (isIgnoredNodeModuleSource(file))
1299
- continue;
1300
- files.add(file);
1301
- }
1302
- }));
1303
- const cache = await new CssCandidateCache(this.#candidateCachePath).load();
1304
- await Promise.all([...files].map(async (file) => {
1305
- for (const candidate of await cache.candidatesFor(file))
1306
- candidates.add(candidate);
1307
- }));
1308
- await cache.save(files);
1309
- this.#logger.verbose(`css candidate cache reused=${cache.reused} rescanned=${cache.rescanned}`);
1310
- return [...candidates];
1337
+ if (!matched)
1338
+ return html;
1339
+ return result + html.slice(lastIndex);
1311
1340
  }
1312
- }
1313
- function resolveSourceWithBun(id, fromBase) {
1314
- try {
1315
- const resolved = Bun.resolveSync(id, fromBase);
1316
- return isSourceFile(resolved) ? resolved : null;
1317
- } catch {
1318
- return null;
1341
+ static escapeInlineScript(source) {
1342
+ return source.replace(/<\/script/gi, "<\\/script");
1319
1343
  }
1320
- }
1321
- function resolveSourceWithRequire(id, fromBase) {
1322
- try {
1323
- const resolved = __require.resolve(id, { paths: [fromBase] });
1324
- return isSourceFile(resolved) ? resolved : null;
1325
- } catch {
1326
- return null;
1344
+ static resolveHtmlAssetPath(htmlPath, src) {
1345
+ if (/^[a-z][a-z0-9+.-]*:/i.test(src) || src.startsWith("//")) {
1346
+ throw new Error(`[csr-build] cannot inline external script: ${src}`);
1347
+ }
1348
+ const normalized = src.startsWith("/") ? src.slice(1) : src;
1349
+ return path9.resolve(path9.dirname(htmlPath), normalized);
1327
1350
  }
1328
1351
  }
1329
- function isSourceFile(filePath) {
1330
- return SOURCE_EXTS.includes(path8.extname(filePath));
1331
- }
1332
- function isIgnoredNodeModuleSource(filePath) {
1333
- return NODE_MODULES_RE.test(filePath) && !AKANJS_NODE_MODULE_RE.test(filePath);
1334
- }
1335
- function getPageKeyBasePath(pageKey, basePaths) {
1336
- const normalized = pageKey.split(path8.sep).join("/").replace(/^\.\//, "");
1337
- const segments = normalized.split("/");
1338
- const firstPublicSegment = segments.find((segment) => segment !== "[lang]" && !/^\(.+\)$/.test(segment));
1339
- return firstPublicSegment && basePaths.includes(firstPublicSegment) ? firstPublicSegment : null;
1340
- }
1341
1352
 
1342
1353
  // pkgs/@akanjs/devkit/frontendBuild/fontOptimizer.ts
1343
- import { mkdir as mkdir3, stat as stat2 } from "fs/promises";
1344
- import path9 from "path";
1345
- import ts2 from "typescript";
1354
+ import { mkdir as mkdir2, stat as stat2 } from "fs/promises";
1355
+ import path10 from "path";
1356
+ import ts from "typescript";
1346
1357
  var FONT_URL_PREFIX = "/_akan/fonts";
1347
1358
  var DEFAULT_FONT_SUBSETS = ["latin"];
1348
1359
 
@@ -1354,11 +1365,11 @@ class FontOptimizer {
1354
1365
  #cssParts = [];
1355
1366
  #woff2Ready = null;
1356
1367
  static #ksX1001Text = null;
1357
- static #cacheVersion = 1;
1368
+ static #cacheVersion = 2;
1358
1369
  constructor(app, command = "start") {
1359
1370
  this.#app = app;
1360
1371
  this.#command = command;
1361
- this.#artifactRoot = path9.join(command === "build" ? app.dist.cwdPath : app.cwdPath, ".akan/artifact");
1372
+ this.#artifactRoot = path10.join(command === "build" ? app.dist.cwdPath : app.cwdPath, ".akan/artifact");
1362
1373
  }
1363
1374
  async optimize() {
1364
1375
  const fonts = await this.discoverFonts();
@@ -1383,7 +1394,7 @@ class FontOptimizer {
1383
1394
  return result;
1384
1395
  }
1385
1396
  get #cachePath() {
1386
- return path9.join(this.#artifactRoot, "fontCache.json");
1397
+ return path10.join(this.#artifactRoot, "fontCache.json");
1387
1398
  }
1388
1399
  async#buildCacheKey(fonts) {
1389
1400
  const sources = [];
@@ -1398,16 +1409,19 @@ class FontOptimizer {
1398
1409
  sources.push({ optimizedSrc: face.optimizedSrc, ...stamp });
1399
1410
  }
1400
1411
  for (const filePath of font.subsetFiles ?? []) {
1401
- const abs = path9.isAbsolute(filePath) ? filePath : path9.join(this.#app.cwdPath, filePath);
1412
+ const abs = path10.isAbsolute(filePath) ? filePath : path10.join(this.#app.cwdPath, filePath);
1402
1413
  const stamp = await this.#fileStamp(abs);
1403
1414
  if (!stamp)
1404
1415
  return null;
1405
1416
  sources.push({ subsetFile: filePath, ...stamp });
1406
1417
  }
1407
1418
  if (this.#getFontSubsets(font).includes("auto"))
1408
- sources.push({ autoSubsetText: this.#hashFontConfig(await this.#collectAutoSubsetText()) });
1419
+ sources.push({ autoSubsetText: this.#cacheDigest(await this.#collectAutoSubsetText()) });
1409
1420
  }
1410
- return this.#hashFontConfig({ version: FontOptimizer.#cacheVersion, fonts, sources });
1421
+ return this.#cacheDigest({ version: FontOptimizer.#cacheVersion, fonts, sources });
1422
+ }
1423
+ #cacheDigest(value) {
1424
+ return new Bun.CryptoHasher("sha256").update(this.#stableStringify(value)).digest("hex");
1411
1425
  }
1412
1426
  async#fileStamp(filePath) {
1413
1427
  try {
@@ -1421,7 +1435,7 @@ class FontOptimizer {
1421
1435
  const cache = await Bun.file(this.#cachePath).json().catch(() => null);
1422
1436
  if (cache?.version !== FontOptimizer.#cacheVersion || cache.key !== key)
1423
1437
  return null;
1424
- const files = cache.files.map((relativePath) => path9.join(this.#artifactRoot, relativePath));
1438
+ const files = cache.files.map((relativePath) => path10.join(this.#artifactRoot, relativePath));
1425
1439
  for (const filePath of files) {
1426
1440
  if (!await Bun.file(filePath).exists())
1427
1441
  return null;
@@ -1433,7 +1447,7 @@ class FontOptimizer {
1433
1447
  version: FontOptimizer.#cacheVersion,
1434
1448
  key,
1435
1449
  css: result.css,
1436
- files: result.files.map((filePath) => path9.relative(this.#artifactRoot, filePath))
1450
+ files: result.files.map((filePath) => path10.relative(this.#artifactRoot, filePath))
1437
1451
  };
1438
1452
  await Bun.write(this.#cachePath, JSON.stringify(cache));
1439
1453
  }
@@ -1441,7 +1455,7 @@ class FontOptimizer {
1441
1455
  const pageKeys = await this.#app.getPageKeys();
1442
1456
  const fonts = [];
1443
1457
  await Promise.all(pageKeys.map(async (key) => {
1444
- const filePath = path9.resolve(this.#app.cwdPath, "page", key);
1458
+ const filePath = path10.resolve(this.#app.cwdPath, "page", key);
1445
1459
  const file = Bun.file(filePath);
1446
1460
  if (!await file.exists())
1447
1461
  return;
@@ -1460,8 +1474,8 @@ class FontOptimizer {
1460
1474
  this.#app.logger.warn(`[font] source not found: ${face.src}`);
1461
1475
  continue;
1462
1476
  }
1463
- const outputPath = path9.join(this.#artifactRoot, face.optimizedSrc.replace(/^\/_akan\//, ""));
1464
- await mkdir3(path9.dirname(outputPath), { recursive: true });
1477
+ const outputPath = path10.join(this.#artifactRoot, face.optimizedSrc.replace(/^\/_akan\//, ""));
1478
+ await mkdir2(path10.dirname(outputPath), { recursive: true });
1465
1479
  const sourceBuffer = Buffer.from(await Bun.file(sourcePath).arrayBuffer());
1466
1480
  const outputBuffer = await this.#buildFontBuffer(font, sourceBuffer, sourcePath);
1467
1481
  await Bun.write(outputPath, outputBuffer);
@@ -1475,41 +1489,86 @@ class FontOptimizer {
1475
1489
  this.#cssParts.push(...faceCss, this.#buildRootVariableRule(font));
1476
1490
  }
1477
1491
  #extractFontsExport(source, filePath) {
1478
- const sourceFile = ts2.createSourceFile(filePath, source, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TSX);
1479
- const fonts = [];
1492
+ const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
1493
+ const declaration = this.#findFontsDeclaration(sourceFile);
1494
+ if (!declaration)
1495
+ return [];
1496
+ const value = this.#literalToValue(declaration);
1497
+ const entries = Array.isArray(value) ? value : [];
1498
+ const fonts = entries.filter((entry) => this.#isReadableFont(entry));
1499
+ if (fonts.length !== entries.length || !Array.isArray(value))
1500
+ this.#app.logger.warn(`[font] ${path10.relative(this.#app.cwdPath, filePath)} declares fonts the build cannot read without evaluating the module \u2014 write the list inline, or nothing is subset for it and every /_akan/fonts request 404s`);
1501
+ return fonts.map((font) => this.#withFontDefaults(font));
1502
+ }
1503
+ #isReadableFont(value) {
1504
+ if (!value || typeof value !== "object")
1505
+ return false;
1506
+ const font = value;
1507
+ return typeof font.name === "string" && Array.isArray(font.paths);
1508
+ }
1509
+ #findFontsDeclaration(sourceFile) {
1480
1510
  for (const statement of sourceFile.statements) {
1481
- if (!ts2.isVariableStatement(statement))
1511
+ if (ts.isExportAssignment(statement)) {
1512
+ if (statement.isExportEquals)
1513
+ continue;
1514
+ const stage = this.#findChainStageArgument(statement.expression, "fonts");
1515
+ if (stage)
1516
+ return stage;
1482
1517
  continue;
1483
- const modifiers = ts2.canHaveModifiers(statement) ? ts2.getModifiers(statement) : undefined;
1484
- const isExported = modifiers?.some((modifier) => modifier.kind === ts2.SyntaxKind.ExportKeyword) ?? false;
1485
- if (!isExported)
1518
+ }
1519
+ if (!ts.isVariableStatement(statement))
1520
+ continue;
1521
+ const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : undefined;
1522
+ if (!modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword))
1486
1523
  continue;
1487
1524
  for (const declaration of statement.declarationList.declarations) {
1488
- if (!ts2.isIdentifier(declaration.name) || declaration.name.text !== "fonts")
1525
+ if (!ts.isIdentifier(declaration.name) || declaration.name.text !== "fonts")
1489
1526
  continue;
1490
- const value = declaration.initializer ? this.#literalToValue(declaration.initializer) : null;
1491
- if (Array.isArray(value)) {
1492
- fonts.push(...value.map((font) => this.#withFontDefaults(font)));
1493
- }
1527
+ if (declaration.initializer)
1528
+ return declaration.initializer;
1494
1529
  }
1495
1530
  }
1496
- return fonts;
1531
+ return null;
1532
+ }
1533
+ #findChainStageArgument(expression, stageName) {
1534
+ let node = this.#unwrapExpression(expression);
1535
+ let argument = null;
1536
+ while (node && ts.isCallExpression(node)) {
1537
+ const callee = this.#unwrapExpression(node.expression);
1538
+ if (!callee)
1539
+ return null;
1540
+ if (ts.isIdentifier(callee))
1541
+ return callee.text === "rootLayout" ? argument : null;
1542
+ if (!ts.isPropertyAccessExpression(callee))
1543
+ return null;
1544
+ if (callee.name.text === stageName)
1545
+ argument ??= node.arguments[0] ?? null;
1546
+ node = this.#unwrapExpression(callee.expression);
1547
+ }
1548
+ return null;
1549
+ }
1550
+ #unwrapExpression(expression) {
1551
+ let current = expression;
1552
+ while (current && (ts.isAsExpression(current) || ts.isSatisfiesExpression(current) || ts.isParenthesizedExpression(current))) {
1553
+ current = current.expression;
1554
+ }
1555
+ return current;
1497
1556
  }
1498
1557
  #literalToValue(node) {
1499
- if (ts2.isStringLiteralLike(node))
1558
+ if (ts.isStringLiteralLike(node))
1500
1559
  return node.text;
1501
- if (ts2.isNumericLiteral(node))
1560
+ if (ts.isNumericLiteral(node))
1502
1561
  return Number(node.text);
1503
- if (node.kind === ts2.SyntaxKind.TrueKeyword)
1562
+ if (node.kind === ts.SyntaxKind.TrueKeyword)
1504
1563
  return true;
1505
- if (node.kind === ts2.SyntaxKind.FalseKeyword)
1564
+ if (node.kind === ts.SyntaxKind.FalseKeyword)
1506
1565
  return false;
1507
- if (ts2.isArrayLiteralExpression(node))
1566
+ if (ts.isArrayLiteralExpression(node))
1508
1567
  return node.elements.map((element) => this.#literalToValue(element));
1509
- if (ts2.isObjectLiteralExpression(node)) {
1568
+ if (ts.isObjectLiteralExpression(node)) {
1510
1569
  const obj = {};
1511
1570
  for (const prop of node.properties) {
1512
- if (!ts2.isPropertyAssignment(prop))
1571
+ if (!ts.isPropertyAssignment(prop))
1513
1572
  continue;
1514
1573
  const name = this.#getPropertyName(prop.name);
1515
1574
  if (!name)
@@ -1518,13 +1577,13 @@ class FontOptimizer {
1518
1577
  }
1519
1578
  return obj;
1520
1579
  }
1521
- if (ts2.isAsExpression(node) || ts2.isSatisfiesExpression(node) || ts2.isParenthesizedExpression(node)) {
1580
+ if (ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isParenthesizedExpression(node)) {
1522
1581
  return this.#literalToValue(node.expression);
1523
1582
  }
1524
1583
  return;
1525
1584
  }
1526
1585
  #getPropertyName(name) {
1527
- if (ts2.isIdentifier(name) || ts2.isStringLiteral(name) || ts2.isNumericLiteral(name))
1586
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name))
1528
1587
  return name.text;
1529
1588
  return null;
1530
1589
  }
@@ -1608,8 +1667,8 @@ class FontOptimizer {
1608
1667
  return null;
1609
1668
  const rel = src.replace(/^\//, "");
1610
1669
  const candidates = [
1611
- this.#command === "build" ? path9.join(this.#app.dist.cwdPath, "public", rel) : null,
1612
- path9.join(this.#app.cwdPath, "public", rel),
1670
+ this.#command === "build" ? path10.join(this.#app.dist.cwdPath, "public", rel) : null,
1671
+ path10.join(this.#app.cwdPath, "public", rel),
1613
1672
  this.#resolveWorkspacePublicPath(rel)
1614
1673
  ].filter(Boolean);
1615
1674
  for (const candidate of candidates) {
@@ -1644,7 +1703,7 @@ class FontOptimizer {
1644
1703
  return "woff2";
1645
1704
  if (signature === "OTTO")
1646
1705
  return "otf";
1647
- const ext = path9.extname(sourcePath).slice(1).toLowerCase();
1706
+ const ext = path10.extname(sourcePath).slice(1).toLowerCase();
1648
1707
  if (ext === "otf" || ext === "woff" || ext === "woff2")
1649
1708
  return ext;
1650
1709
  return "ttf";
@@ -1653,7 +1712,7 @@ class FontOptimizer {
1653
1712
  const [root, dep, ...rest] = rel.split("/");
1654
1713
  if (root !== "libs" || !dep || rest.length === 0)
1655
1714
  return null;
1656
- return path9.join(this.#app.workspace.workspaceRoot, "libs", dep, "public", ...rest);
1715
+ return path10.join(this.#app.workspace.workspaceRoot, "libs", dep, "public", ...rest);
1657
1716
  }
1658
1717
  async#getSubsetText(font) {
1659
1718
  const parts = new Set;
@@ -1663,7 +1722,7 @@ class FontOptimizer {
1663
1722
  if (font.subsetText)
1664
1723
  parts.add(font.subsetText);
1665
1724
  for (const filePath of font.subsetFiles ?? []) {
1666
- const abs = path9.isAbsolute(filePath) ? filePath : path9.join(this.#app.cwdPath, filePath);
1725
+ const abs = path10.isAbsolute(filePath) ? filePath : path10.join(this.#app.cwdPath, filePath);
1667
1726
  const file = Bun.file(abs);
1668
1727
  if (await file.exists())
1669
1728
  parts.add(await file.text());
@@ -1683,16 +1742,19 @@ class FontOptimizer {
1683
1742
  }
1684
1743
  async#collectAutoSubsetText() {
1685
1744
  const libPageRoots = (await this.#app.getPageRoots()).filter((root) => root.keyPrefix).map((root) => root.dir);
1686
- const roots = [...["page", "ui"].map((dir) => path9.join(this.#app.cwdPath, dir)), ...libPageRoots];
1745
+ const roots = [...["page", "ui", "lib"].map((dir) => path10.join(this.#app.cwdPath, dir)), ...libPageRoots];
1687
1746
  const glob = new Bun.Glob("**/*.{ts,tsx,js,jsx,html,md}");
1688
1747
  const parts = [];
1689
- await Promise.all(roots.map(async (root) => {
1690
- if (!await stat2(root).then((entry) => entry.isDirectory(), () => false))
1691
- return;
1692
- for await (const filePath of glob.scan({ cwd: root, absolute: true })) {
1748
+ for (const root of [...new Set(roots)].sort()) {
1749
+ const isDir = await stat2(root).then((entry) => entry.isDirectory(), () => false);
1750
+ if (!isDir)
1751
+ continue;
1752
+ const filePaths = [];
1753
+ for await (const filePath of glob.scan({ cwd: root, absolute: true }))
1754
+ filePaths.push(filePath);
1755
+ for (const filePath of filePaths.sort())
1693
1756
  parts.push(await Bun.file(filePath).text());
1694
- }
1695
- }));
1757
+ }
1696
1758
  return parts.join("");
1697
1759
  }
1698
1760
  #rangeText(start, end) {
@@ -1760,332 +1822,210 @@ ${declarations.map(([prop, value]) => ` ${prop}: ${value};`).join(`
1760
1822
  if (!fallbackMetrics)
1761
1823
  continue;
1762
1824
  css.push(generateFontFace({ ...metrics, category: font.category }, {
1763
- name: this.#getFontFallbackName(font),
1764
- font: fallback,
1765
- metrics: fallbackMetrics,
1766
- "font-weight": String(face.weight),
1767
- "font-style": face.style
1768
- }));
1769
- }
1770
- return css.join("");
1771
- }
1772
- #buildRootVariableRule(font) {
1773
- return `:root { ${this.#getFontVariableName(font)}: ${this.#quote(font.name)}, ${this.#quote(this.#getFontFallbackName(font))}; }`;
1774
- }
1775
- #buildFontUtilityRules(fonts) {
1776
- const rules = [];
1777
- const seen = new Set;
1778
- for (const font of fonts) {
1779
- const className = font.className || `font-${font.name}`;
1780
- const selector = `.${this.#escapeCssClassName(className)}`;
1781
- const rule = `${selector} { font-family: var(${this.#getFontVariableName(font)}); }`;
1782
- if (seen.has(rule))
1783
- continue;
1784
- seen.add(rule);
1785
- rules.push(rule);
1786
- }
1787
- return rules.join(`
1788
- `);
1789
- }
1790
- #escapeCssClassName(value) {
1791
- return value.replace(/^-?\d|[^a-zA-Z0-9_-]/g, (part) => [...part].map((char) => `\\${char.codePointAt(0)?.toString(16)} `).join(""));
1792
- }
1793
- #toDeclarationEntries(declarations = []) {
1794
- return declarations.map((declaration) => [declaration.prop, declaration.value]);
1795
- }
1796
- #quote(value) {
1797
- return JSON.stringify(value);
1798
- }
1799
- }
1800
-
1801
- // pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
1802
- import path11 from "path";
1803
-
1804
- // pkgs/@akanjs/devkit/transforms/externalizeFrameworkPlugin.ts
1805
- import path10 from "path";
1806
- var DEFAULT_INCLUDE = ["akanjs/", "@apps/", "@libs/"];
1807
- var DEFAULT_EXCLUDE_EXACT = new Set(["akanjs/webkit", "@akanjs/cli", "@akanjs/devkit"]);
1808
- var DEFAULT_EXCLUDE_PREFIX = ["@akanjs/cli/", "@akanjs/devkit/"];
1809
- var OPTIONAL_BACKEND_EXTERNAL_EXACT = new Set([
1810
- "@libsql/client",
1811
- "bullmq",
1812
- "croner",
1813
- "ioredis",
1814
- "postgres",
1815
- "protobufjs"
1816
- ]);
1817
- var RUNTIME_EXTERNAL_EXACT = new Set([
1818
- "react",
1819
- "react-dom",
1820
- "react/jsx-runtime",
1821
- "react/jsx-dev-runtime",
1822
- "react-server-dom-webpack",
1823
- "react-server-dom-webpack/server.node",
1824
- "react-server-dom-webpack/client.node",
1825
- "react-server-dom-webpack/client.browser"
1826
- ]);
1827
- var RUNTIME_EXTERNAL_PREFIX = ["react-dom/", "react-server-dom-webpack/"];
1828
- var CANDIDATE_EXTS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
1829
- async function createExternalizeFrameworkPlugin(options) {
1830
- const tsconfig = await options.app.getTsConfig();
1831
- const includePrefixes = options.include ?? DEFAULT_INCLUDE;
1832
- const extraExact = new Set(options.extra ?? []);
1833
- const workspaceRoot = options.app.workspace.workspaceRoot;
1834
- const tsconfigPaths = tsconfig.compilerOptions.paths ?? {};
1835
- const rootEntries = Object.entries(tsconfigPaths).filter(([k]) => !k.endsWith("/*")).map(([k, v]) => ({ pkg: k, entryFile: v[0] ?? null })).filter((e) => e.entryFile !== null);
1836
- const wildcardEntries = Object.entries(tsconfigPaths).filter(([k]) => k.endsWith("/*")).map(([k, v]) => ({ prefix: k.slice(0, -1), replacements: v })).sort((a, b) => b.prefix.length - a.prefix.length);
1837
- async function resolveWorkspaceSubpath(spec) {
1838
- if (!workspaceRoot)
1839
- return null;
1840
- for (const { prefix, replacements } of wildcardEntries) {
1841
- if (!spec.startsWith(prefix))
1842
- continue;
1843
- const suffix = spec.slice(prefix.length);
1844
- for (const repl of replacements) {
1845
- const replPath = repl?.endsWith("/*") ? repl.slice(0, -1) : repl ?? "";
1846
- if (!replPath)
1847
- continue;
1848
- const candidate = path10.resolve(workspaceRoot, replPath + suffix);
1849
- const hit = await firstExisting(candidate);
1850
- if (hit)
1851
- return hit;
1852
- }
1853
- }
1854
- for (const { pkg, entryFile } of rootEntries) {
1855
- if (spec !== pkg && !spec.startsWith(`${pkg}/`))
1856
- continue;
1857
- if (spec === pkg)
1858
- continue;
1859
- const suffix = spec.slice(pkg.length + 1);
1860
- const pkgDir = path10.dirname(path10.resolve(workspaceRoot, entryFile));
1861
- const candidate = path10.join(pkgDir, suffix);
1862
- const hit = await firstExisting(candidate);
1863
- if (hit)
1864
- return hit;
1865
- }
1866
- return null;
1867
- }
1868
- return {
1869
- name: "akan-externalize-framework",
1870
- setup(build) {
1871
- build.onResolve({ filter: /.*/ }, async (args) => {
1872
- const spec = args.path;
1873
- if (spec === "." || spec === ".." || spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/"))
1874
- return;
1875
- if (extraExact.has(spec) || DEFAULT_EXCLUDE_EXACT.has(spec))
1876
- return { path: spec, external: true };
1877
- for (const prefix of DEFAULT_EXCLUDE_PREFIX) {
1878
- if (spec.startsWith(prefix))
1879
- return { path: spec, external: true };
1880
- }
1881
- if (OPTIONAL_BACKEND_EXTERNAL_EXACT.has(spec))
1882
- return { path: spec, external: true };
1883
- if (RUNTIME_EXTERNAL_EXACT.has(spec))
1884
- return { path: spec, external: true };
1885
- for (const prefix of RUNTIME_EXTERNAL_PREFIX) {
1886
- if (spec.startsWith(prefix))
1887
- return { path: spec, external: true };
1888
- }
1889
- for (const prefix of includePrefixes) {
1890
- if (!spec.startsWith(prefix))
1891
- continue;
1892
- const resolved = await resolveWorkspaceSubpath(spec);
1893
- if (resolved)
1894
- return { path: resolved };
1895
- return;
1896
- }
1897
- return;
1898
- });
1825
+ name: this.#getFontFallbackName(font),
1826
+ font: fallback,
1827
+ metrics: fallbackMetrics,
1828
+ "font-weight": String(face.weight),
1829
+ "font-style": face.style
1830
+ }));
1899
1831
  }
1900
- };
1901
- }
1902
- async function firstExisting(basePath) {
1903
- if (await Bun.file(basePath).exists())
1904
- return basePath;
1905
- for (const ext of CANDIDATE_EXTS) {
1906
- const candidate = `${basePath}${ext}`;
1907
- if (await Bun.file(candidate).exists())
1908
- return candidate;
1832
+ return css.join("");
1909
1833
  }
1910
- for (const ext of CANDIDATE_EXTS) {
1911
- const candidate = path10.join(basePath, `index${ext}`);
1912
- if (await Bun.file(candidate).exists())
1913
- return candidate;
1834
+ #buildRootVariableRule(font) {
1835
+ return `:root { ${this.#getFontVariableName(font)}: ${this.#quote(font.name)}, ${this.#quote(this.#getFontFallbackName(font))}; }`;
1914
1836
  }
1915
- return null;
1916
- }
1917
-
1918
- // pkgs/@akanjs/devkit/transforms/useClientBundlePlugin.ts
1919
- function createUseClientBundlePlugin(options = {}) {
1920
- return {
1921
- name: "akan-use-client-bundle",
1922
- setup(build) {
1923
- build.onLoad({ filter: /\.(tsx|ts|jsx|js)$/ }, async (args) => {
1924
- if (args.path.includes("/node_modules/"))
1925
- return;
1926
- let source;
1927
- try {
1928
- source = await Bun.file(args.path).text();
1929
- } catch {
1930
- return;
1931
- }
1932
- const stubbed = transformUseClient(source, {
1933
- path: args.path,
1934
- workspaceRoot: options.workspaceRoot
1935
- });
1936
- if (stubbed === null)
1937
- return;
1938
- return { contents: stubbed, loader: loaderFor(args.path) };
1939
- });
1837
+ #buildFontUtilityRules(fonts) {
1838
+ const rules = [];
1839
+ const seen = new Set;
1840
+ for (const font of fonts) {
1841
+ const className = font.className || `font-${font.name}`;
1842
+ const selector = `.${this.#escapeCssClassName(className)}`;
1843
+ const rule = `${selector} { font-family: var(${this.#getFontVariableName(font)}); }`;
1844
+ if (seen.has(rule))
1845
+ continue;
1846
+ seen.add(rule);
1847
+ rules.push(rule);
1940
1848
  }
1941
- };
1942
- }
1943
- function loaderFor(absPath) {
1944
- if (absPath.endsWith(".tsx"))
1945
- return "tsx";
1946
- if (absPath.endsWith(".jsx"))
1947
- return "jsx";
1948
- if (absPath.endsWith(".ts"))
1949
- return "ts";
1950
- return "js";
1849
+ return rules.join(`
1850
+ `);
1851
+ }
1852
+ #escapeCssClassName(value) {
1853
+ return value.replace(/^-?\d|[^a-zA-Z0-9_-]/g, (part) => [...part].map((char) => `\\${char.codePointAt(0)?.toString(16)} `).join(""));
1854
+ }
1855
+ #toDeclarationEntries(declarations = []) {
1856
+ return declarations.map((declaration) => [declaration.prop, declaration.value]);
1857
+ }
1858
+ #quote(value) {
1859
+ return JSON.stringify(value);
1860
+ }
1951
1861
  }
1952
1862
 
1953
- // pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
1954
- var VIRTUAL_PAGES_ENTRY = "akan-pages-entry";
1863
+ // pkgs/@akanjs/devkit/frontendBuild/fontPruner.ts
1864
+ import { rm as rm2, rmdir, stat as stat3 } from "fs/promises";
1865
+ import path11 from "path";
1866
+ import { isFontOptimizationEnabled } from "akanjs/client";
1867
+ var FONT_GLOB = "**/*.{woff2,woff,ttf,otf,ttc,eot}";
1868
+ var REFERRER_GLOB = "**/*.{css,js,mjs,cjs,json,html,htm,svg,xml,webmanifest}";
1955
1869
 
1956
- class PagesBundleBuilder {
1870
+ class FontPruner {
1957
1871
  #app;
1958
- #command;
1959
- #pageEntries;
1960
- #started = Date.now();
1961
- constructor(app, command = "start", pageEntries) {
1872
+ #keepGlobs;
1873
+ #publicRoot;
1874
+ #artifactRoot;
1875
+ constructor(app, { keepFonts = [] } = {}) {
1962
1876
  this.#app = app;
1963
- this.#command = command;
1964
- this.#pageEntries = pageEntries;
1965
- }
1966
- async build() {
1967
- const akanConfig = await this.#app.getConfig();
1968
- const resolvedEntries = this.#pageEntries ?? await resolveSsrPageEntriesForApp(this.#app, await this.#app.getPageKeys());
1969
- const entrySource = PagesEntrySourceGenerator.generate(resolvedEntries);
1970
- const workspaceRoot = this.#app.workspace.workspaceRoot;
1971
- const result = await Bun.build({
1972
- entrypoints: [VIRTUAL_PAGES_ENTRY],
1973
- outdir: `${this.#artifactDir}/server`,
1974
- target: "bun",
1975
- format: "esm",
1976
- splitting: this.#splitting,
1977
- minify: this.#command === "build",
1978
- naming: {
1979
- entry: "pages-[hash].[ext]",
1980
- chunk: "chunks/[name]-[hash].[ext]",
1981
- asset: "assets/[name]-[hash].[ext]"
1982
- },
1983
- define: this.#define(),
1984
- plugins: [
1985
- PagesBundleBuilder.createPagesEntryPlugin(entrySource),
1986
- PagesBundleBuilder.createServerCssStubPlugin(),
1987
- PagesBundleBuilder.createServerUseClientFetchPlugin(),
1988
- await createExternalizeFrameworkPlugin({ app: this.#app, extra: akanConfig.externalLibs }),
1989
- akanConfig.barrelImports.length > 0 ? await createBarrelImportsPlugin(this.#app, {
1990
- pipeAfter: (source, args) => transformUseClient(source, {
1991
- path: args.path,
1992
- workspaceRoot
1993
- })
1994
- }) : createUseClientBundlePlugin({ workspaceRoot })
1995
- ]
1996
- });
1997
- if (!result.success)
1998
- throw new AggregateError(result.logs, "[PagesBundleBuilder] Bun.build failed");
1999
- const entryArtifact = result.outputs.find((a) => a.kind === "entry-point");
2000
- if (!entryArtifact)
2001
- throw new Error("[PagesBundleBuilder] Bun.build emitted no entry-point artifact");
2002
- const bundlePath = path11.resolve(entryArtifact.path);
2003
- const buildId = Date.now();
2004
- const outputBytes = result.outputs.reduce((sum, output) => sum + output.size, 0);
2005
- const chunkCount = result.outputs.filter((output) => output.kind === "chunk").length;
2006
- this.#app.verbose(`[PagesBundleBuilder] ${path11.basename(bundlePath)} emitted in ${Date.now() - this.#started}ms splitting=${this.#splitting} entry=${entryArtifact.size} bytes outputs=${result.outputs.length} chunks=${chunkCount} total=${outputBytes} bytes`);
2007
- return {
2008
- bundlePath,
2009
- buildId,
2010
- splitting: this.#splitting,
2011
- entryBytes: entryArtifact.size,
2012
- outputBytes,
2013
- outputCount: result.outputs.length,
2014
- chunkCount
2015
- };
2016
- }
2017
- get #artifactDir() {
2018
- return `${this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath}/.akan/artifact`;
1877
+ this.#keepGlobs = keepFonts;
1878
+ this.#publicRoot = path11.join(app.dist.cwdPath, "public");
1879
+ this.#artifactRoot = path11.join(app.dist.cwdPath, ".akan/artifact");
1880
+ }
1881
+ async prune() {
1882
+ const empty = { removed: [], kept: [], freedBytes: 0 };
1883
+ if (!await this.#isDirectory(this.#publicRoot))
1884
+ return empty;
1885
+ const candidates = await this.#collectCandidates();
1886
+ if (!candidates.length)
1887
+ return empty;
1888
+ const fonts = await new FontOptimizer(this.#app, "build").discoverFonts();
1889
+ const unoptimizedSrcs = new Set;
1890
+ const optimizedSrcs = new Set;
1891
+ for (const font of fonts) {
1892
+ const target = isFontOptimizationEnabled(font) ? optimizedSrcs : unoptimizedSrcs;
1893
+ for (const fontPath of font.paths)
1894
+ target.add(path11.posix.basename(fontPath.src));
1895
+ }
1896
+ const result = { removed: [], kept: [], freedBytes: 0 };
1897
+ const undecided = [];
1898
+ for (const candidate of candidates) {
1899
+ const reason = this.#staticKeepReason(candidate, unoptimizedSrcs);
1900
+ if (reason)
1901
+ result.kept.push({ file: candidate.rel, bytes: candidate.bytes, reason });
1902
+ else
1903
+ undecided.push(candidate);
1904
+ }
1905
+ if (undecided.length) {
1906
+ const referrers = await this.#findReferrers(undecided, optimizedSrcs);
1907
+ for (const candidate of undecided) {
1908
+ const referrer = referrers.get(candidate.rel);
1909
+ if (referrer)
1910
+ result.kept.push({ file: candidate.rel, bytes: candidate.bytes, reason: { kind: "referenced", referrer } });
1911
+ else
1912
+ result.removed.push({ file: candidate.rel, bytes: candidate.bytes });
1913
+ }
1914
+ }
1915
+ await this.#remove(result.removed.map(({ file }) => file));
1916
+ result.freedBytes = result.removed.reduce((total, { bytes }) => total + bytes, 0);
1917
+ this.#report(result);
1918
+ return result;
2019
1919
  }
2020
- get #splitting() {
2021
- return process.env.AKAN_SERVER_PAGES_SPLITTING === "1";
1920
+ #staticKeepReason(candidate, unoptimizedSrcs) {
1921
+ if (unoptimizedSrcs.has(candidate.basename))
1922
+ return { kind: "unoptimized" };
1923
+ for (const glob of this.#keepGlobs) {
1924
+ if (new Bun.Glob(glob).match(candidate.rel))
1925
+ return { kind: "declared", glob };
1926
+ }
1927
+ return null;
2022
1928
  }
2023
- #define() {
2024
- const nodeEnv = this.#command === "build" ? "production" : "development";
2025
- return {
2026
- "process.env.NODE_ENV": JSON.stringify(nodeEnv),
2027
- "process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("ssr"),
2028
- ...Object.fromEntries(Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]))
2029
- };
1929
+ async#collectCandidates() {
1930
+ const candidates = [];
1931
+ for await (const rel of new Bun.Glob(FONT_GLOB).scan({ cwd: this.#publicRoot, dot: false })) {
1932
+ const entry = await stat3(path11.join(this.#publicRoot, rel)).catch(() => null);
1933
+ if (!entry?.isFile())
1934
+ continue;
1935
+ candidates.push({ rel: rel.split(path11.sep).join("/"), basename: path11.basename(rel), bytes: entry.size });
1936
+ }
1937
+ return candidates;
2030
1938
  }
2031
- static createPagesEntryPlugin(source) {
2032
- return {
2033
- name: "akan-pages-entry",
2034
- setup(build) {
2035
- build.onResolve({ filter: /^akan-pages-entry$/ }, () => ({
2036
- path: VIRTUAL_PAGES_ENTRY,
2037
- namespace: "akan-virtual"
2038
- }));
2039
- build.onLoad({ filter: /^akan-pages-entry$/, namespace: "akan-virtual" }, () => ({
2040
- contents: source,
2041
- loader: "tsx"
2042
- }));
2043
- }
2044
- };
1939
+ async#findReferrers(candidates, optimizedSrcs) {
1940
+ const referrers = new Map;
1941
+ const authoritative = candidates;
1942
+ const bundled = candidates.filter((candidate) => !optimizedSrcs.has(candidate.basename));
1943
+ const roots = [
1944
+ { dir: this.#publicRoot, label: "public", needles: authoritative },
1945
+ { dir: path11.join(this.#artifactRoot, "styles"), label: ".akan/artifact/styles", needles: authoritative },
1946
+ { dir: path11.join(this.#artifactRoot, "client"), label: ".akan/artifact/client", needles: bundled },
1947
+ { dir: path11.join(this.#artifactRoot, "client-ssr"), label: ".akan/artifact/client-ssr", needles: bundled },
1948
+ { dir: path11.join(this.#artifactRoot, "server"), label: ".akan/artifact/server", needles: bundled },
1949
+ { dir: path11.join(this.#app.dist.cwdPath, "csr"), label: "csr", needles: bundled }
1950
+ ];
1951
+ for (const root of roots) {
1952
+ const pending = root.needles.filter((candidate) => !referrers.has(candidate.rel));
1953
+ if (!pending.length || !await this.#isDirectory(root.dir))
1954
+ continue;
1955
+ await this.#scanRoot(root, pending, referrers);
1956
+ }
1957
+ return referrers;
2045
1958
  }
2046
- static createServerCssStubPlugin() {
2047
- return {
2048
- name: "akan-server-css-stub",
2049
- setup(build) {
2050
- build.onLoad({ filter: /\.css$/ }, () => ({
2051
- contents: "",
2052
- loader: "js"
2053
- }));
1959
+ async#scanRoot(root, pending, referrers) {
1960
+ const needles = pending.map((candidate) => ({
1961
+ rel: candidate.rel,
1962
+ forms: [...new Set([candidate.basename, encodeURIComponent(candidate.basename)])]
1963
+ }));
1964
+ for await (const rel of new Bun.Glob(REFERRER_GLOB).scan({ cwd: root.dir, dot: false })) {
1965
+ const remaining = needles.filter((needle) => !referrers.has(needle.rel));
1966
+ if (!remaining.length)
1967
+ return;
1968
+ const text = await Bun.file(path11.join(root.dir, rel)).text().catch(() => "");
1969
+ if (!text)
1970
+ continue;
1971
+ for (const needle of remaining) {
1972
+ if (needle.forms.some((form) => text.includes(form)))
1973
+ referrers.set(needle.rel, `${root.label}/${rel.split(path11.sep).join("/")}`);
2054
1974
  }
2055
- };
1975
+ }
2056
1976
  }
2057
- static createServerUseClientFetchPlugin() {
2058
- return {
2059
- name: "akan-server-use-client-fetch",
2060
- setup(build) {
2061
- build.onLoad({ filter: /[/\\]lib[/\\]useClient\.(ts|tsx|js|jsx)$/ }, async (args) => {
2062
- const source = await Bun.file(args.path).text();
2063
- const transformed = PagesBundleBuilder.transformServerUseClientFetchSource(source);
2064
- if (transformed === source)
2065
- return;
2066
- return { contents: transformed, loader: loaderFor2(args.path) };
2067
- });
2068
- }
2069
- };
1977
+ async#remove(files) {
1978
+ await Promise.all(files.map((file) => rm2(path11.join(this.#publicRoot, file), { force: true })));
1979
+ const dirs = [...new Set(files.map((file) => path11.posix.dirname(file)))].filter((dir) => dir !== "." && dir !== "/").sort((a, b) => b.length - a.length);
1980
+ for (const dir of dirs)
1981
+ await rmdir(path11.join(this.#publicRoot, dir)).catch(() => {
1982
+ return;
1983
+ });
2070
1984
  }
2071
- static transformServerUseClientFetchSource(source) {
2072
- if (!source.includes(`with { type: "macro" }`) || !source.includes("FetchClient.build"))
2073
- return source;
2074
- return source.replace(/import\s+\{\s*getSerializedSignal\s*\}\s+from\s+["']\.\/sig["']\s+with\s+\{\s*type\s*:\s*["']macro["']\s*\};/, `import { fetch as serverFetch } from "./sig";`).replace(/const\s+fetchProto\s*=\s*FetchClient\.build<[^;]+;/, "const fetchProto = FetchClient.build<typeof signal>(cnst, serverFetch.serializedSignal, { Err: pageProto.Err, base: serverFetch });");
1985
+ #report(result) {
1986
+ if (!result.removed.length && !result.kept.length)
1987
+ return;
1988
+ for (const { file, bytes } of result.removed)
1989
+ this.#app.verbose(`[font-prune] dropped public/${file} (${FontPruner.formatBytes(bytes)})`);
1990
+ for (const { file, reason } of result.kept)
1991
+ this.#app.verbose(`[font-prune] kept public/${file} \u2014 ${FontPruner.describe(reason)}`);
1992
+ const byReferrer = new Map;
1993
+ for (const { bytes, reason } of result.kept) {
1994
+ if (reason.kind !== "referenced")
1995
+ continue;
1996
+ const entry = byReferrer.get(reason.referrer) ?? { files: 0, bytes: 0 };
1997
+ entry.files += 1;
1998
+ entry.bytes += bytes;
1999
+ byReferrer.set(reason.referrer, entry);
2000
+ }
2001
+ if (!byReferrer.size)
2002
+ return;
2003
+ const ranked = [...byReferrer.entries()].sort(([, a], [, b]) => b.bytes - a.bytes);
2004
+ const named = ranked.slice(0, 3).map(([referrer, { files, bytes }]) => `${referrer} (${files}, ${FontPruner.formatBytes(bytes)})`);
2005
+ const rest = ranked.length - named.length;
2006
+ this.#app.logger.info(`[font-prune] kept ${FontPruner.formatBytes(ranked.reduce((total, [, entry]) => total + entry.bytes, 0))} of fonts in public/ because these reference them: ${named.join(", ")}${rest > 0 ? ` and ${rest} more` : ""}`);
2007
+ }
2008
+ static describe(reason) {
2009
+ if (reason.kind === "unoptimized")
2010
+ return "declared with optimize: false, so the runtime CSS serves it";
2011
+ if (reason.kind === "declared")
2012
+ return `kept by assets.keepFonts "${reason.glob}"`;
2013
+ return `referenced by ${reason.referrer}`;
2014
+ }
2015
+ static formatBytes(bytes) {
2016
+ if (bytes < 1024)
2017
+ return `${bytes}B`;
2018
+ if (bytes < 1024 * 1024)
2019
+ return `${(bytes / 1024).toFixed(1)}KB`;
2020
+ return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
2021
+ }
2022
+ async#isDirectory(dir) {
2023
+ return await stat3(dir).then((entry) => entry.isDirectory(), () => false);
2075
2024
  }
2076
2025
  }
2077
- function loaderFor2(absPath) {
2078
- if (absPath.endsWith(".tsx"))
2079
- return "tsx";
2080
- if (absPath.endsWith(".jsx"))
2081
- return "jsx";
2082
- if (absPath.endsWith(".ts"))
2083
- return "ts";
2084
- return "js";
2085
- }
2086
2026
 
2087
2027
  // pkgs/@akanjs/devkit/frontendBuild/precompressArtifacts.ts
2088
- import fs2 from "fs";
2028
+ import fs from "fs";
2089
2029
  import path12 from "path";
2090
2030
  import zlib from "zlib";
2091
2031
  var COMPRESSIBLE_EXTS = new Set([".css", ".html", ".js", ".json", ".svg"]);
@@ -2106,7 +2046,7 @@ async function precompressArtifacts(app) {
2106
2046
  return result;
2107
2047
  }
2108
2048
  async function precompressRoot(root, result) {
2109
- if (!fs2.existsSync(root) || !fs2.statSync(root).isDirectory())
2049
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory())
2110
2050
  return;
2111
2051
  const glob = new Bun.Glob("**/*");
2112
2052
  for await (const filePath of glob.scan({ cwd: root, absolute: true })) {
@@ -2200,6 +2140,7 @@ class SsrBaseArtifactBuilder {
2200
2140
  branches: [...akanConfig.branches],
2201
2141
  i18n: akanConfig.i18n,
2202
2142
  imageConfig: akanConfig.images,
2143
+ web: akanConfig.web,
2203
2144
  deepLinkAssociations: Object.values(akanConfig.mobile.targets).filter((target) => (target.deepLinks?.domains?.length ?? 0) > 0).map((target) => ({
2204
2145
  targetName: target.name,
2205
2146
  appId: target.appId,
@@ -2226,7 +2167,7 @@ class SsrBaseArtifactBuilder {
2226
2167
  const ssrBundle = await new ClientEntriesBundler({
2227
2168
  app: this.#app,
2228
2169
  entries: [rscSegmentOutletEntry],
2229
- ...RouteClientBuilder.resolveSsrClientExternalOptions(this.#command),
2170
+ ...RouteClientBuilder.resolveSsrClientBundleOptions(this.#command),
2230
2171
  outputSubdir: "client-ssr",
2231
2172
  command: this.#command
2232
2173
  }).bundle();
@@ -2278,13 +2219,13 @@ class SsrBaseArtifactBuilder {
2278
2219
  `);
2279
2220
  if (!cssText)
2280
2221
  return [];
2281
- return [this.#writeCssAsset(basePath, cssText)];
2222
+ return [this.#writeCssAsset(basePath, cssText, cssCompiler.importedStylesheetsByBasePath[basePath] ?? [])];
2282
2223
  })));
2283
2224
  if (optimizedFonts.files.length > 0)
2284
2225
  this.#app.verbose(`[base-artifact] optimized ${optimizedFonts.files.length} font files`);
2285
2226
  return { cssCompiler, optimizedFonts, cssAssets };
2286
2227
  }
2287
- async#writeCssAsset(basePath, cssText) {
2228
+ async#writeCssAsset(basePath, cssText, imported) {
2288
2229
  const cssAssetName = basePath || "root";
2289
2230
  const preparedCssText = await prepareCssAsset(this.#command, basePath, cssText);
2290
2231
  const cssHash = Bun.hash(`${basePath}
@@ -2294,220 +2235,16 @@ ${preparedCssText}`).toString(36);
2294
2235
  `/_akan/styles/${cssAssetName}-${cssHash}.css`
2295
2236
  ];
2296
2237
  await Bun.write(path13.join(this.#absArtifactDir, cssRelPath), preparedCssText);
2238
+ SsrBaseArtifactBuilder.#warnDroppedImports(this.#app, cssRelPath, preparedCssText, imported);
2297
2239
  this.#app.verbose(`[base-artifact] wrote ${preparedCssText.length} bytes of CSS for ${basePath} -> ${cssRelPath}`);
2298
2240
  return [basePath, { cssUrl, cssRelPath }];
2299
2241
  }
2300
- }
2301
- // pkgs/@akanjs/devkit/frontendBuild/styleGuard.ts
2302
- var PREFIX = "(?:bg|text|border(?:-[tblrxy])?(?:-[se])?|ring(?:-offset)?|fill|stroke|shadow|from|to|via|divide|outline|decoration|placeholder|caret|accent)";
2303
- var PALETTE = "(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)";
2304
- var LEAD = "(^|[\\s\"'`{(\\[:!])";
2305
- var TAIL = "(?=[\\s\"'`})\\]:/!,]|$)";
2306
- var RAW_PALETTE_RE = new RegExp(`${LEAD}((?:${PREFIX})-)${PALETTE}-\\d{2,3}${TAIL}`, "g");
2307
- var ARBITRARY_COLOR_RE = /\[(#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|oklch|oklab|lab|lch|hwb|color)\([^\]]*\))\]/g;
2308
- var INTERPOLATED_ARBITRARY_RE = new RegExp(`${LEAD}[a-z][a-z0-9-]*-\\[[^\\]\`]*\\$\\{`, "g");
2309
- var STYLE_OBJECT_RE = /style=\{\{([\s\S]*?)\}\}/g;
2310
- var STYLE_TAG_RE = /<style[^>]*>([\s\S]*?)<\/style>/g;
2311
- var INLINE_COLOR_LITERAL_RE = /#[0-9a-fA-F]{3,8}\b|(?:rgb|rgba|hsl|hsla|oklch|oklab|lab|lch|hwb)\(/g;
2312
- var DAISYUI_LEGACY_RE = new RegExp(`${LEAD}(?:` + "btn-(?:primary|secondary|accent|neutral|info|success|warning|error|ghost|link|outline|square|circle|wide|block|xs|sm|md|lg)|" + "badge-(?:primary|secondary|accent|neutral|info|success|warning|error|ghost|outline)|" + "alert-(?:info|success|warning|error)|" + "input-(?:bordered|primary|secondary|accent|ghost|error)|" + "select-(?:bordered|primary|ghost)|textarea-(?:bordered|primary|ghost)|" + "checkbox-(?:primary|secondary|accent)|toggle-(?:primary|secondary|accent)|" + "loading-(?:spinner|dots|ring|ball|bars|infinity)|" + "card-(?:body|title|actions)|modal-(?:box|action|backdrop)|" + "collapse-(?:title|content|arrow|plus)|dropdown-(?:content|end|start|hover)|" + "stat-(?:title|value|desc)|tabs-(?:boxed|lifted|bordered)|tab-active|" + "menu-(?:title|dropdown)|steps-(?:horizontal|vertical)|join-item|" + "mockup-(?:code|phone|browser|window)|drawer-(?:side|content|toggle)" + `)${TAIL}`, "g");
2313
- var ALL_RULES = [
2314
- "raw-palette",
2315
- "arbitrary-color",
2316
- "inline-color",
2317
- "daisyui-legacy",
2318
- "interpolated-arbitrary"
2319
- ];
2320
-
2321
- class StyleGuard {
2322
- run(files) {
2323
- const out = [];
2324
- for (const file of files) {
2325
- const directives = this.#parseDirectives(file.content);
2326
- const scan = this.#stripComments(file.content);
2327
- const local = [];
2328
- this.#scanClassLiterals(file, scan, local);
2329
- this.#scanInlineColors(file, scan, local);
2330
- for (const v of local)
2331
- if (!this.#isSuppressed(v, directives))
2332
- out.push(v);
2333
- }
2334
- return out;
2335
- }
2336
- #stripComments(src) {
2337
- const out = src.split("");
2338
- const blank = (i2) => {
2339
- if (src[i2] !== `
2340
- `)
2341
- out[i2] = " ";
2342
- };
2343
- let state = "code";
2344
- let i = 0;
2345
- while (i < src.length) {
2346
- const c = src[i];
2347
- const d = src[i + 1];
2348
- if (state === "code") {
2349
- if (c === "/" && d === "/") {
2350
- blank(i);
2351
- blank(i + 1);
2352
- state = "line";
2353
- i += 2;
2354
- } else if (c === "/" && d === "*") {
2355
- blank(i);
2356
- blank(i + 1);
2357
- state = "block";
2358
- i += 2;
2359
- } else {
2360
- if (c === "'")
2361
- state = "squote";
2362
- else if (c === '"')
2363
- state = "dquote";
2364
- else if (c === "`")
2365
- state = "template";
2366
- i++;
2367
- }
2368
- } else if (state === "line") {
2369
- if (c === `
2370
- `)
2371
- state = "code";
2372
- else
2373
- blank(i);
2374
- i++;
2375
- } else if (state === "block") {
2376
- if (c === "*" && d === "/") {
2377
- blank(i);
2378
- blank(i + 1);
2379
- state = "code";
2380
- i += 2;
2381
- } else {
2382
- blank(i);
2383
- i++;
2384
- }
2385
- } else {
2386
- const close = state === "squote" ? "'" : state === "dquote" ? '"' : "`";
2387
- if (c === "\\")
2388
- i += 2;
2389
- else {
2390
- if (c === close)
2391
- state = "code";
2392
- i++;
2393
- }
2394
- }
2395
- }
2396
- return out.join("");
2397
- }
2398
- #parseDirectives(content) {
2399
- const directives = { file: null, nextLine: new Map };
2400
- const lines = content.split(`
2401
- `);
2402
- for (let i = 0;i < lines.length; i++) {
2403
- const line = lines[i];
2404
- const nextLineMatch = line.match(/styleguard-disable-next-line\b(.*)$/);
2405
- if (nextLineMatch) {
2406
- directives.nextLine.set(i + 2, this.#extractRules(nextLineMatch[1]));
2242
+ static #warnDroppedImports(app, cssRelPath, css, imported) {
2243
+ for (const { cssPath, declaredNames } of imported) {
2244
+ if (declaredNames.length === 0 || declaredNames.some((name) => css.includes(`${name}:`)))
2407
2245
  continue;
2408
- }
2409
- const fileMatch = line.match(/styleguard-disable\b(.*)$/);
2410
- if (fileMatch)
2411
- directives.file = this.#mergeFileScope(directives.file, this.#extractRules(fileMatch[1]));
2412
- }
2413
- return directives;
2414
- }
2415
- #extractRules(tail) {
2416
- const found = ALL_RULES.filter((rule) => tail.includes(rule));
2417
- return found.length === 0 ? "all" : new Set(found);
2418
- }
2419
- #mergeFileScope(prev, next) {
2420
- if (prev === "all" || next === "all")
2421
- return "all";
2422
- return new Set([...prev ?? [], ...next]);
2423
- }
2424
- #isSuppressed(v, directives) {
2425
- if (directives.file === "all" || directives.file?.has(v.rule))
2426
- return true;
2427
- const nextLine = directives.nextLine.get(v.line);
2428
- return nextLine === "all" || (nextLine?.has(v.rule) ?? false);
2429
- }
2430
- #scanClassLiterals(file, scan, out) {
2431
- for (const m of scan.matchAll(RAW_PALETTE_RE)) {
2432
- out.push(this.#violation(file, m.index ?? 0, {
2433
- rule: "raw-palette",
2434
- severity: "error",
2435
- suggestion: "\uC2DC\uB9E8\uD2F1 \uD1A0\uD070\uC73C\uB85C \uAD50\uCCB4\uD558\uC138\uC694 \u2014 \uC608: text-gray-500\u2192text-muted-foreground, bg-red-500\u2192bg-destructive, bg-blue-500\u2192bg-info. \uBE0C\uB79C\uB4DC \uC0C9\uC740 page/styles.css \uD1A0\uD070\uC5D0\uC11C \uC870\uC815\uD569\uB2C8\uB2E4."
2436
- }));
2437
- }
2438
- for (const m of scan.matchAll(ARBITRARY_COLOR_RE)) {
2439
- out.push(this.#violation(file, m.index ?? 0, {
2440
- rule: "arbitrary-color",
2441
- severity: "error",
2442
- suggestion: "\uC784\uC758 \uC0C9 \uB9AC\uD130\uB7F4 \uB300\uC2E0 \uD1A0\uD070\uC744 \uC4F0\uC138\uC694 \u2014 bg-[#3b82f6]\u2192bg-info, text-[rgb(...)]\u2192\uC2DC\uB9E8\uD2F1 \uD1A0\uD070. \uAF2D \uD544\uC694\uD558\uBA74 \uC2A4\uCF54\uD504 \uD1A0\uD070(.campaign-x { --primary: \u2026 })\uC744 \uC815\uC758\uD558\uC138\uC694."
2443
- }));
2444
- }
2445
- for (const m of scan.matchAll(DAISYUI_LEGACY_RE)) {
2446
- out.push(this.#violation(file, m.index ?? 0, {
2447
- rule: "daisyui-legacy",
2448
- severity: "error",
2449
- suggestion: "daisyUI \uD074\uB798\uC2A4\uB294 \uC81C\uAC70\uB410\uC2B5\uB2C8\uB2E4. akanjs/ui \uD504\uB9AC\uBBF8\uD2F0\uBE0C(Button/Badge \uB4F1)\uB098 buttonRecipe()/badgeRecipe() + \uC2DC\uB9E8\uD2F1 \uD1A0\uD070\uC73C\uB85C \uAD50\uCCB4\uD558\uC138\uC694."
2450
- }));
2451
- }
2452
- for (const m of scan.matchAll(INTERPOLATED_ARBITRARY_RE)) {
2453
- out.push(this.#violation(file, m.index ?? 0, {
2454
- rule: "interpolated-arbitrary",
2455
- severity: "error",
2456
- suggestion: "\uB7F0\uD0C0\uC784 \uAC12\uC73C\uB85C \uC784\uC758\uAC12 \uD074\uB798\uC2A4\uB97C \uC870\uB9BD\uD558\uBA74 CSS \uAC00 \uC0DD\uC131\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4(\uC2A4\uCE90\uB108\uB294 \uC18C\uC2A4 \uD14D\uC2A4\uD2B8\uB97C \uC77D\uC2B5\uB2C8\uB2E4). \uD06C\uAE30/\uC704\uCE58\uB294 style prop \uC73C\uB85C \uB118\uAE30\uC138\uC694 \u2014 style={{ minHeight }}. \uAC12\uC774 enum \uC774\uBA74 \uB9AC\uD130\uB7F4 \uD074\uB798\uC2A4 \uB9F5\uC73C\uB85C \uB450\uC138\uC694."
2457
- }));
2458
- }
2459
- }
2460
- #scanInlineColors(file, scan, out) {
2461
- const push = (index) => out.push(this.#violation(file, index, {
2462
- rule: "inline-color",
2463
- severity: "error",
2464
- suggestion: "\uC778\uB77C\uC778 \uC0C9 \uB9AC\uD130\uB7F4 \uB300\uC2E0 CSS \uBCC0\uC218/\uD1A0\uD070\uC744 \uCC38\uC870\uD558\uC138\uC694 \u2014 style={{ color: 'var(--primary)' }} \uB610\uB294 \uC2DC\uB9E8\uD2F1 \uD074\uB798\uC2A4(text-primary). \uD558\uB4DC\uCF54\uB529 hex/rgb \uB294 \uD14C\uB9C8 \uC804\uD658\uC744 \uAE68\uB728\uB9BD\uB2C8\uB2E4."
2465
- }));
2466
- for (const region of [STYLE_OBJECT_RE, STYLE_TAG_RE]) {
2467
- for (const m of scan.matchAll(region)) {
2468
- const inner = m[1] ?? "";
2469
- const innerStart = (m.index ?? 0) + m[0].indexOf(inner);
2470
- for (const lit of inner.matchAll(INLINE_COLOR_LITERAL_RE)) {
2471
- push(innerStart + (lit.index ?? 0));
2472
- }
2473
- }
2474
- }
2475
- }
2476
- #violation(file, index, base) {
2477
- const before = file.content.slice(0, index);
2478
- const line = before.length === 0 ? 1 : before.split(`
2479
- `).length;
2480
- const lineStart = before.lastIndexOf(`
2481
- `) + 1;
2482
- const lineEnd = file.content.indexOf(`
2483
- `, index);
2484
- const snippet = file.content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd).trim();
2485
- return { ...base, path: file.path, line, snippet };
2486
- }
2487
- static countNamedComponentClasses(css) {
2488
- const names = new Set;
2489
- const layerRe = /@layer\s+components\s*\{/g;
2490
- for (const layer of css.matchAll(layerRe)) {
2491
- const bodyStart = (layer.index ?? 0) + layer[0].length;
2492
- const body = StyleGuard.#extractBalanced(css, bodyStart);
2493
- for (const sel of body.matchAll(/(^|[\s}])\.(-?[a-zA-Z_][\w-]*)/g))
2494
- names.add(sel[2]);
2495
- }
2496
- return { count: names.size, names: [...names].sort() };
2497
- }
2498
- static #extractBalanced(css, start) {
2499
- let depth = 1;
2500
- for (let i = start;i < css.length; i++) {
2501
- const ch = css[i];
2502
- if (ch === "{")
2503
- depth++;
2504
- else if (ch === "}") {
2505
- depth--;
2506
- if (depth === 0)
2507
- return css.slice(start, i);
2508
- }
2246
+ app.logger.warn(`[base-artifact] @import ${cssPath} declares ${declaredNames.length} custom propert${declaredNames.length === 1 ? "y" : "ies"} and none of them are in ${cssRelPath}`);
2509
2247
  }
2510
- return css.slice(start);
2511
2248
  }
2512
2249
  }
2513
- export { AllRoutesBuilder, CsrArtifactBuilder, CssCompiler, FontOptimizer, PagesBundleBuilder, precompressArtifacts, SsrBaseArtifactBuilder };
2250
+ export { AllRoutesBuilder, CssCompiler, PagesBundleBuilder, CsrArtifactBuilder, FontOptimizer, FontPruner, precompressArtifacts, SsrBaseArtifactBuilder };