@akanjs/cli 3.0.0-alpha.9 → 3.0.0-alpha.90

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 (135) 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-cefxzggj.js} +3 -5
  5. package/{agent.command-f0gqkfgz.js → agent.command-r93wmq7m.js} +11 -13
  6. package/{application.command-x903m2qq.js → application.command-5nqfqgdn.js} +62 -38
  7. package/{applicationBuildRunner-ha3y8ew8.js → applicationBuildRunner-b21m07fs.js} +23 -20
  8. package/{applicationReleasePackager-gg8ccbeh.js → applicationReleasePackager-3wh58n2m.js} +8 -9
  9. package/buildBatch.proc.js +12 -14
  10. package/capacitorApp-gzcx6txb.js +12 -0
  11. package/{cloud.command-zs6kyf83.js → cloud.command-g0pk0k60.js} +15 -18
  12. package/commandManifest.json +1 -1
  13. package/{context.command-fmepgjzg.js → context.command-qd8kqrnh.js} +19 -23
  14. package/dependencyScanner-shs82ynp.js +8 -0
  15. package/devTui-2xf0meaq.js +552 -0
  16. package/devTuiView-7jy8f30g.js +9 -0
  17. package/getDirname-dr3jz1xg.js +7 -0
  18. package/{guideline.command-n6sw0a0c.js → guideline.command-f38x8jhw.js} +16 -18
  19. package/guidelines/agentRule/agentRule.generate.json +31 -0
  20. package/guidelines/agentRule/agentRule.instruction.md +533 -0
  21. package/guidelines/conventions/conventions.instruction.md +406 -197
  22. package/guidelines/cssRule/cssRule.instruction.md +7 -0
  23. package/guidelines/framework/framework.generate.json +3 -2
  24. package/guidelines/framework/framework.instruction.md +1 -9
  25. package/guidelines/mcpRule/mcpRule.generate.json +25 -0
  26. package/guidelines/mcpRule/mcpRule.instruction.md +217 -0
  27. package/guidelines/modelStore/modelStore.instruction.md +12 -0
  28. package/guidelines/modelTemplate/modelTemplate.instruction.md +5 -0
  29. package/guidelines/queryRule/queryRule.generate.json +31 -0
  30. package/guidelines/queryRule/queryRule.instruction.md +143 -0
  31. package/guidelines/runtimeRule/runtimeRule.generate.json +25 -0
  32. package/guidelines/runtimeRule/runtimeRule.instruction.md +213 -0
  33. package/guidelines/ssrRule/ssrRule.instruction.md +49 -5
  34. package/guidelines/transportRule/transportRule.generate.json +31 -0
  35. package/guidelines/transportRule/transportRule.instruction.md +60 -0
  36. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +51 -760
  37. package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +33 -0
  38. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +402 -0
  39. package/incrementalBuilder.proc.js +26 -25
  40. package/{index-774gcx29.js → index-0emb9eat.js} +41 -34
  41. package/index-3d9y066c.js +76 -0
  42. package/{index-3vta1tqa.js → index-3vvbv974.js} +32 -32
  43. package/{index-76rn3g2c.js → index-47pj3aee.js} +4 -4
  44. package/{index-y4vkyhvv.js → index-5xdqxbn9.js} +14 -14
  45. package/{index-1577bej2.js → index-6wwzs1bc.js} +2 -2
  46. package/{index-wnp7hwq7.js → index-92zztqy1.js} +7 -7
  47. package/{index-jhpa13zj.js → index-9bwhxrqf.js} +57 -57
  48. package/{index-bjpxzr6s.js → index-9wtbw98m.js} +6 -3
  49. package/{index-fgc8r6dj.js → index-a5vmm6x4.js} +3 -1
  50. package/{index-xys926f2.js → index-by5gpp8x.js} +4 -4
  51. package/{index-kdh76j26.js → index-cezy7yb3.js} +100 -32
  52. package/{index-62955kn5.js → index-d61rcfa2.js} +758 -127
  53. package/{index-82jrh4hj.js → index-deekbtkj.js} +13 -13
  54. package/{index-rxkpk7n4.js → index-e82dyeqy.js} +4 -4
  55. package/{index-qzdjd4ct.js → index-h759vjfa.js} +18 -18
  56. package/{index-j37qq1f2.js → index-h7yw2z9n.js} +7 -7
  57. package/{index-qb61h0k7.js → index-jydtyzva.js} +1098 -1360
  58. package/{index-46tjzh6s.js → index-kcxmv48c.js} +29 -29
  59. package/{index-4m0bphac.js → index-kfnr6c19.js} +31 -30
  60. package/{index-xmmzf150.js → index-mpmyqm1y.js} +1 -1
  61. package/{index-xjd3rqzj.js → index-n68jx79w.js} +6 -15
  62. package/{index-0cj2zxbm.js → index-n85fer99.js} +12 -11
  63. package/{index-779ske1y.js → index-pgsswppj.js} +27 -27
  64. package/index-pvb4vdwr.js +159 -0
  65. package/{index-67546d0j.js → index-q0y2jq9j.js} +14 -2
  66. package/{index-z8ptfkzv.js → index-q8sn827n.js} +969 -240
  67. package/{index-5sn5w2mt.js → index-r50qen0z.js} +94 -61
  68. package/{index-eyyf2149.js → index-rntcdss1.js} +36 -37
  69. package/{index-gcv8ncfh.js → index-sjak5v5b.js} +16 -12
  70. package/{index-j9cxndc0.js → index-w8kmx1b6.js} +20 -11
  71. package/{index-jf2ya7r2.js → index-y74k5anj.js} +8 -8
  72. package/{index-d43t8af2.js → index-zkvpaggp.js} +58 -25
  73. package/index.js +22 -25
  74. package/{library.command-cv9gdbmg.js → library.command-em103wtn.js} +11 -10
  75. package/{localRegistry.command-d8fg36zc.js → localRegistry.command-gz8apn4y.js} +20 -23
  76. package/{module.command-qyr1s9cn.js → module.command-451pztkc.js} +13 -16
  77. package/{package.command-wsjx0k47.js → package.command-rfhyf38t.js} +8 -10
  78. package/package.json +5 -7
  79. package/{page.command-rdzg65rm.js → page.command-kt19da5g.js} +7 -9
  80. package/{primitive.command-89g38b53.js → primitive.command-pgy69ere.js} +13 -16
  81. package/{quality.command-0cs2z92d.js → quality.command-gpe2vqb5.js} +226 -105
  82. package/recipeScanner-fpagtgc1.js +11 -0
  83. package/{repair.command-6fphxfwj.js → repair.command-2sbrz187.js} +12 -14
  84. package/{routeSourceValidator-e4jmbfcx.js → routeSourceValidator-ze6x894c.js} +0 -2
  85. package/{scalar.command-xfer69s6.js → scalar.command-jcf4e5sv.js} +11 -14
  86. package/subspace.command-kqzqtk0p.js +755 -0
  87. package/templates/app/env/env.client.debug.ts.template +1 -5
  88. package/templates/app/env/env.client.develop.ts.template +1 -5
  89. package/templates/app/env/env.client.local.ts.template +1 -5
  90. package/templates/app/env/env.client.main.ts.template +1 -5
  91. package/templates/app/env/env.client.testing.ts.template +1 -5
  92. package/templates/app/env/env.client.type.ts +2 -6
  93. package/templates/app/env/env.server.debug.ts.template +1 -5
  94. package/templates/app/env/env.server.develop.ts.template +1 -5
  95. package/templates/app/env/env.server.local.ts.template +1 -5
  96. package/templates/app/env/env.server.main.ts.template +1 -5
  97. package/templates/app/main.ts +3 -1
  98. package/templates/appSample/lib/task/task.signal.ts +11 -6
  99. package/templates/appSample/srvkit/AuthGuard.ts +9 -5
  100. package/templates/appSample/srvkit/SessionInternalArg.ts +4 -3
  101. package/templates/crudPages/[__model__Id]/edit/page.tsx +5 -5
  102. package/templates/crudPages/[__model__Id]/page.tsx +10 -13
  103. package/templates/crudPages/new/page.tsx +5 -5
  104. package/templates/crudPages/page.tsx +9 -9
  105. package/templates/crudSinglePage/page.tsx +11 -7
  106. package/templates/env/_env.server.type.ts +7 -5
  107. package/templates/lib/db.ts +1 -1
  108. package/templates/lib/dict.ts +1 -1
  109. package/templates/lib/sig.ts +1 -1
  110. package/templates/lib/srv.ts +6 -2
  111. package/templates/module/__model__.dictionary.ts +4 -1
  112. package/templates/module/__model__.signal.ts +2 -3
  113. package/templates/module/__model__.store.ts +1 -2
  114. package/templates/workspaceRoot/.gitignore.template +0 -1
  115. package/templates/workspaceRoot/.husky/pre-commit.template +16 -0
  116. package/templates/workspaceRoot/.vscode/settings.json.template +1 -1
  117. package/templates/workspaceRoot/AGENTS.md.template +4 -0
  118. package/templates/workspaceRoot/CLAUDE.md.template +19 -0
  119. package/templates/workspaceRoot/biome.json.template +2 -1
  120. package/templates/workspaceRoot/docs/AI-DEVELOPMENT.md.template +10 -14
  121. package/templates/workspaceRoot/docs/GENERATED.md.template +12 -10
  122. package/templates/workspaceRoot/package.json.template +2 -4
  123. package/typeChecker-3xgrzc2v.js +7 -0
  124. package/typecheck.proc.js +4 -5
  125. package/{workflow.command-2ygfqt4f.js → workflow.command-m4z25a2p.js} +22 -25
  126. package/{workspace.command-88m0zs64.js → workspace.command-dqgj9zbk.js} +96 -81
  127. package/capacitorApp-9xswcgx7.js +0 -61
  128. package/dependencyScanner-f6grrep3.js +0 -9
  129. package/getDirname-m9s0cmhq.js +0 -8
  130. package/index-mxvakhsm.js +0 -66
  131. package/index-r24hmh0q.js +0 -4
  132. package/recipeScanner-tghkzrrt.js +0 -12
  133. package/typeChecker-kravn7ns.js +0 -8
  134. /package/{index-0wae5ebk.js → index-meh558jy.js} +0 -0
  135. /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-q8sn827n.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;
605
- }
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}` : ""}`);
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;
638
327
  }
639
- await this.#inlineCsrArtifacts(artifact.cssAssets ?? {});
640
- this.#app.verbose(`[csr-build] output -> ${this.#outputDir}`);
641
- return { outputDir: this.#outputDir };
328
+ return null;
642
329
  }
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;
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 {}
337
+ }
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,25 +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
- const styleEntry = (typeof exportValue === "string" ? exportValue : exportValue?.style || exportValue?.import || exportValue?.default) || pkg.exports?.["."]?.style || pkg.style || "index.css";
977
- return await this.#firstExisting(path7.resolve(pkgDir, styleEntry));
399
+ const exportedEntry = typeof exportValue === "string" ? exportValue : exportValue?.style || exportValue?.import || exportValue?.default;
400
+ if (exportedEntry)
401
+ return await this.#firstExisting(path4.resolve(pkgDir, exportedEntry));
402
+ if (subpath !== ".")
403
+ return await this.#firstExisting(path4.resolve(pkgDir, subpath));
404
+ return await this.#firstExisting(path4.resolve(pkgDir, pkg.exports?.["."]?.style || pkg.style || "index.css"));
978
405
  } catch {
979
406
  return null;
980
407
  }
981
408
  }
982
409
  #resolutionBases(fromBase) {
983
- 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), "../..")];
984
411
  }
985
412
  #packageJsonCandidates(pkgName) {
986
413
  return [
987
- path7.join(this.#workspaceRoot, "pkgs", pkgName, "package.json"),
988
- path7.join(this.#workspaceRoot, "node_modules", pkgName, "package.json"),
989
- path7.join(path7.dirname(Bun.main), "node_modules", pkgName, "package.json"),
990
- 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")
991
418
  ];
992
419
  }
993
420
  async#firstExisting(basePath) {
@@ -1005,7 +432,7 @@ class CssImportResolver {
1005
432
  return parts[0] ?? null;
1006
433
  }
1007
434
  static isCssFile(filePath) {
1008
- return path7.extname(filePath) === ".css";
435
+ return path4.extname(filePath) === ".css";
1009
436
  }
1010
437
  }
1011
438
 
@@ -1024,13 +451,15 @@ class CssCompiler {
1024
451
  this.#app = app;
1025
452
  }
1026
453
  get #candidateCachePath() {
1027
- return path8.join(this.#app.cwdPath, ".akan/cache/cssCandidates.json");
454
+ return path5.join(this.#app.cwdPath, ".akan/cache/cssCandidates.json");
1028
455
  }
1029
456
  #cssText = null;
1030
457
  #cssTextByBasePath = null;
1031
458
  #fileExistsCache = new Map;
1032
459
  #resolvedFileCache = new Map;
1033
460
  #resolvedSpecifierCache = new Map;
461
+ #discoveredCssPaths = new Set;
462
+ importedStylesheetsByBasePath = {};
1034
463
  #fileExists(absPath) {
1035
464
  let cached = this.#fileExistsCache.get(absPath);
1036
465
  if (!cached) {
@@ -1052,7 +481,7 @@ class CssCompiler {
1052
481
  return filePath;
1053
482
  }
1054
483
  for (const ext of SOURCE_EXTS) {
1055
- const filePath = path8.join(absPathNoExt, `index${ext}`);
484
+ const filePath = path5.join(absPathNoExt, `index${ext}`);
1056
485
  if (await this.#fileExists(filePath))
1057
486
  return filePath;
1058
487
  }
@@ -1064,13 +493,20 @@ class CssCompiler {
1064
493
  async getCss({ refresh } = {}) {
1065
494
  if (this.#cssText !== null && !refresh)
1066
495
  return this.#cssText;
496
+ this.#discoveredCssPaths.clear();
497
+ this.importedStylesheetsByBasePath = {};
1067
498
  const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh });
1068
- this.#cssText = await this.compileCss(cssPaths, sourcePaths);
499
+ const { css, imported } = await this.#compileWithImports(cssPaths, sourcePaths);
500
+ this.#cssText = css;
501
+ this.importedStylesheetsByBasePath = { "": imported };
502
+ await this.#warnUnreachableStylesheets();
1069
503
  return this.#cssText;
1070
504
  }
1071
505
  async getCssByBasePath({ refresh } = {}) {
1072
506
  if (this.#cssTextByBasePath !== null && !refresh)
1073
507
  return this.#cssTextByBasePath;
508
+ this.#discoveredCssPaths.clear();
509
+ this.importedStylesheetsByBasePath = {};
1074
510
  const akanConfig = await this.#app.getConfig({ refresh });
1075
511
  const pageKeys = await this.#app.getPageKeys({ refresh });
1076
512
  const basePaths = [...akanConfig.basePaths];
@@ -1081,7 +517,8 @@ class CssCompiler {
1081
517
  return ["", ""];
1082
518
  const started = Date.now();
1083
519
  const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: rootPageKeys });
1084
- const css = await this.compileCss(cssPaths, sourcePaths);
520
+ const { css, imported } = await this.#compileWithImports(cssPaths, sourcePaths);
521
+ this.importedStylesheetsByBasePath[""] = imported;
1085
522
  this.#logger.verbose(`css base=root paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`);
1086
523
  return ["", css];
1087
524
  })(),
@@ -1091,14 +528,27 @@ class CssCompiler {
1091
528
  return [basePath, ""];
1092
529
  const started = Date.now();
1093
530
  const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: basePathPageKeys });
1094
- const css = await this.compileCss(cssPaths, sourcePaths);
531
+ const { css, imported } = await this.#compileWithImports(cssPaths, sourcePaths);
532
+ this.importedStylesheetsByBasePath[basePath] = imported;
1095
533
  this.#logger.verbose(`css base=${basePath} paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`);
1096
534
  return [basePath, css];
1097
535
  })
1098
536
  ]);
1099
537
  this.#cssTextByBasePath = Object.fromEntries(cssEntries);
538
+ await this.#warnUnreachableStylesheets();
1100
539
  return this.#cssTextByBasePath;
1101
540
  }
541
+ async#warnUnreachableStylesheets() {
542
+ const pageDir = path5.join(this.#app.cwdPath, "page");
543
+ const glob = new Bun.Glob("**/*.css");
544
+ for await (const cssPath of glob.scan({ cwd: pageDir, absolute: true })) {
545
+ if (cssPath.includes(`${path5.sep}(libs)${path5.sep}`))
546
+ continue;
547
+ if (this.#discoveredCssPaths.has(cssPath))
548
+ continue;
549
+ this.#logger.warn(`css ${path5.relative(this.#app.cwdPath, cssPath)} is imported by no route and never compiled`);
550
+ }
551
+ }
1102
552
  async discoverCss({ refresh } = {}) {
1103
553
  const { cssPaths } = await this.discoverCssAndSources({ refresh });
1104
554
  return cssPaths;
@@ -1108,7 +558,7 @@ class CssCompiler {
1108
558
  pageKeys
1109
559
  } = {}) {
1110
560
  pageKeys ??= await this.#app.getPageKeys({ refresh });
1111
- 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));
1112
562
  const cssFiles = new Set;
1113
563
  const sourceFiles = new Set;
1114
564
  const queue = [...seeds];
@@ -1140,7 +590,7 @@ class CssCompiler {
1140
590
  } catch {
1141
591
  continue;
1142
592
  }
1143
- const importerDir = path8.dirname(filePath);
593
+ const importerDir = path5.dirname(filePath);
1144
594
  for (const imp of imports) {
1145
595
  const spec = imp.path;
1146
596
  if (!spec)
@@ -1158,21 +608,47 @@ class CssCompiler {
1158
608
  queue.push(resolved);
1159
609
  }
1160
610
  }
1161
- return { cssPaths: [...cssFiles], sourcePaths: [...sourceFiles] };
611
+ const tokenPaths = await this.#libTokenStylesheets(sourceFiles);
612
+ const cssPaths = [...new Set([...tokenPaths, ...cssFiles])];
613
+ for (const cssPath of cssPaths)
614
+ this.#discoveredCssPaths.add(cssPath);
615
+ return { cssPaths, sourcePaths: [...sourceFiles] };
616
+ }
617
+ async#libTokenStylesheets(sourceFiles) {
618
+ const libsRoot = path5.join(this.#app.workspace.workspaceRoot, "libs");
619
+ const libNames = new Set;
620
+ for (const filePath of sourceFiles) {
621
+ const relPath = path5.relative(libsRoot, filePath);
622
+ if (relPath.startsWith("..") || path5.isAbsolute(relPath))
623
+ continue;
624
+ const [libName] = relPath.split(path5.sep);
625
+ if (libName)
626
+ libNames.add(libName);
627
+ }
628
+ const tokenPaths = await Promise.all([...libNames].sort().map(async (libName) => {
629
+ const tokensPath = path5.join(libsRoot, libName, "ui/tokens.css");
630
+ return await this.#fileExists(tokensPath) ? tokensPath : null;
631
+ }));
632
+ return tokenPaths.filter((tokensPath) => !!tokensPath);
1162
633
  }
1163
634
  async compileCss(cssPaths, sourcePaths) {
635
+ const { css } = await this.#compileWithImports(cssPaths, sourcePaths);
636
+ return css;
637
+ }
638
+ async#compileWithImports(cssPaths, sourcePaths) {
1164
639
  if (cssPaths.length === 0)
1165
- return "";
640
+ return { css: "", imported: [] };
1166
641
  const compileStarted = Date.now();
1167
642
  const compilers = await Promise.all(cssPaths.map(async (cssPath) => {
1168
643
  const css = await Bun.file(cssPath).text();
1169
- const base = path8.dirname(cssPath);
644
+ const base = path5.dirname(cssPath);
645
+ const imported = new Map;
1170
646
  const compiler = await compile(css, {
1171
647
  base,
1172
- loadStylesheet: (id, fromBase) => this.#loadStylesheet(id, fromBase),
648
+ loadStylesheet: (id, fromBase) => this.#loadStylesheet(id, fromBase, imported),
1173
649
  loadModule: (id, fromBase) => this.#loadModule(id, fromBase)
1174
650
  });
1175
- return { cssPath, compiler };
651
+ return { cssPath, compiler, imported };
1176
652
  }));
1177
653
  const sourceDirs = new Set;
1178
654
  for (const entry of compilers) {
@@ -1185,118 +661,699 @@ class CssCompiler {
1185
661
  const candidates = await this.#scanCandidates(sourcePaths, [...sourceDirs]);
1186
662
  this.#logger.verbose(`css candidates scanned count=${candidates.length} sources=${sourcePaths.length} dirs=${sourceDirs.size} in ${Date.now() - scanStarted}ms`);
1187
663
  const parts = [];
664
+ const imported = [];
1188
665
  for (const entry of compilers) {
1189
666
  if (!entry)
1190
667
  continue;
1191
- 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
+ }
1192
677
  }
1193
678
  this.#logger.verbose(`css compiled paths=${cssPaths.length} candidates=${candidates.length} in ${Date.now() - compileStarted}ms`);
1194
- return parts.join(`
1195
- `);
679
+ return { css: parts.join(`
680
+ `), imported };
1196
681
  }
1197
- async#loadStylesheet(id, fromBase) {
682
+ async#loadStylesheet(id, fromBase, imported) {
1198
683
  const p = await this.#resolveCssImport(id, fromBase);
684
+ this.#discoveredCssPaths.add(p);
1199
685
  const content = await Bun.file(p).text();
1200
- 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 };
1201
689
  }
1202
690
  async#resolveCssImport(id, fromBase) {
1203
- if (id.startsWith(".") || id.startsWith("/"))
1204
- return path8.resolve(fromBase, id);
691
+ if (id.startsWith(".") || id.startsWith("/")) {
692
+ const filePath = path5.resolve(fromBase, id);
693
+ if (await this.#fileExists(filePath))
694
+ return filePath;
695
+ throw new Error(`[css] failed to resolve stylesheet import "${id}" from ${fromBase} (no file at ${filePath})`);
696
+ }
1205
697
  const resolver = await this.#getCssImportResolver();
1206
698
  const resolved = await resolver.resolve(id, fromBase);
1207
699
  if (resolved)
1208
700
  return resolved;
1209
701
  throw new Error(`[css] failed to resolve stylesheet import "${id}" from ${fromBase}`);
1210
702
  }
1211
- async#getCssImportResolver() {
1212
- if (this.#cssImportResolver)
1213
- return this.#cssImportResolver;
1214
- this.#cssImportResolver = await CssImportResolver.create(this.#app);
1215
- return this.#cssImportResolver;
703
+ async#getCssImportResolver() {
704
+ if (this.#cssImportResolver)
705
+ return this.#cssImportResolver;
706
+ this.#cssImportResolver = await CssImportResolver.create(this.#app);
707
+ return this.#cssImportResolver;
708
+ }
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"));
1216
1289
  }
1217
- async#loadModule(id, fromBase) {
1218
- const p = __require.resolve(id, { paths: [fromBase] });
1219
- const mod = await import(p);
1220
- return { path: p, base: path8.dirname(p), module: mod.default ?? mod };
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();
1221
1298
  }
1222
- #resolveSourceImport(id, fromBase, resolvePackage) {
1223
- const cacheKey = `${fromBase}\x00${id}`;
1224
- let cached = this.#resolvedSpecifierCache.get(cacheKey);
1225
- if (cached)
1226
- return cached;
1227
- cached = this.#resolveSourceImportUncached(id, fromBase, resolvePackage);
1228
- this.#resolvedSpecifierCache.set(cacheKey, cached);
1229
- return cached;
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));
1230
1314
  }
1231
- async#resolveSourceImportUncached(id, fromBase, resolvePackage) {
1232
- if (id.startsWith(".") || id.startsWith("/")) {
1233
- const abs = id.startsWith("/") ? id : path8.resolve(fromBase, id);
1234
- return this.#resolveSourceFileCandidate(abs);
1235
- }
1236
- const pkg = await resolvePackage(id);
1237
- if (pkg)
1238
- return pkg.entryFile;
1239
- for (const resolve of [() => resolveSourceWithBun(id, fromBase), () => resolveSourceWithRequire(id, fromBase)]) {
1240
- const resolved = await resolve();
1241
- if (resolved)
1242
- return resolved;
1243
- }
1244
- return null;
1315
+ static createInlineStyle(css) {
1316
+ return `<style data-akan-css="active">
1317
+ ${css.replace(/<\/style/gi, "<\\/style")}
1318
+ </style>`;
1245
1319
  }
1246
- async#scanCandidates(sourcePaths, dirs) {
1247
- const candidates = new Set;
1248
- const glob = new Bun.Glob("**/*.{tsx,ts,jsx,js,html}");
1249
- const files = new Set(sourcePaths);
1250
- await Promise.all(dirs.map(async (dir) => {
1251
- for await (const file of glob.scan({ cwd: dir, absolute: true })) {
1252
- if (isIgnoredNodeModuleSource(file))
1253
- continue;
1254
- files.add(file);
1255
- }
1256
- }));
1257
- const cache = await new CssCandidateCache(this.#candidateCachePath).load();
1258
- await Promise.all([...files].map(async (file) => {
1259
- for (const candidate of await cache.candidatesFor(file))
1260
- candidates.add(candidate);
1261
- }));
1262
- await cache.save(files);
1263
- this.#logger.verbose(`css candidate cache reused=${cache.reused} rescanned=${cache.rescanned}`);
1264
- return [...candidates];
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;
1336
+ }
1337
+ if (!matched)
1338
+ return html;
1339
+ return result + html.slice(lastIndex);
1265
1340
  }
1266
- }
1267
- function resolveSourceWithBun(id, fromBase) {
1268
- try {
1269
- const resolved = Bun.resolveSync(id, fromBase);
1270
- return isSourceFile(resolved) ? resolved : null;
1271
- } catch {
1272
- return null;
1341
+ static escapeInlineScript(source) {
1342
+ return source.replace(/<\/script/gi, "<\\/script");
1273
1343
  }
1274
- }
1275
- function resolveSourceWithRequire(id, fromBase) {
1276
- try {
1277
- const resolved = __require.resolve(id, { paths: [fromBase] });
1278
- return isSourceFile(resolved) ? resolved : null;
1279
- } catch {
1280
- 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);
1281
1350
  }
1282
1351
  }
1283
- function isSourceFile(filePath) {
1284
- return SOURCE_EXTS.includes(path8.extname(filePath));
1285
- }
1286
- function isIgnoredNodeModuleSource(filePath) {
1287
- return NODE_MODULES_RE.test(filePath) && !AKANJS_NODE_MODULE_RE.test(filePath);
1288
- }
1289
- function getPageKeyBasePath(pageKey, basePaths) {
1290
- const normalized = pageKey.split(path8.sep).join("/").replace(/^\.\//, "");
1291
- const segments = normalized.split("/");
1292
- const firstPublicSegment = segments.find((segment) => segment !== "[lang]" && !/^\(.+\)$/.test(segment));
1293
- return firstPublicSegment && basePaths.includes(firstPublicSegment) ? firstPublicSegment : null;
1294
- }
1295
1352
 
1296
1353
  // pkgs/@akanjs/devkit/frontendBuild/fontOptimizer.ts
1297
- import { mkdir as mkdir3, stat as stat2 } from "fs/promises";
1298
- import path9 from "path";
1299
- 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";
1300
1357
  var FONT_URL_PREFIX = "/_akan/fonts";
1301
1358
  var DEFAULT_FONT_SUBSETS = ["latin"];
1302
1359
 
@@ -1308,11 +1365,11 @@ class FontOptimizer {
1308
1365
  #cssParts = [];
1309
1366
  #woff2Ready = null;
1310
1367
  static #ksX1001Text = null;
1311
- static #cacheVersion = 1;
1368
+ static #cacheVersion = 2;
1312
1369
  constructor(app, command = "start") {
1313
1370
  this.#app = app;
1314
1371
  this.#command = command;
1315
- 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");
1316
1373
  }
1317
1374
  async optimize() {
1318
1375
  const fonts = await this.discoverFonts();
@@ -1337,7 +1394,7 @@ class FontOptimizer {
1337
1394
  return result;
1338
1395
  }
1339
1396
  get #cachePath() {
1340
- return path9.join(this.#artifactRoot, "fontCache.json");
1397
+ return path10.join(this.#artifactRoot, "fontCache.json");
1341
1398
  }
1342
1399
  async#buildCacheKey(fonts) {
1343
1400
  const sources = [];
@@ -1352,16 +1409,19 @@ class FontOptimizer {
1352
1409
  sources.push({ optimizedSrc: face.optimizedSrc, ...stamp });
1353
1410
  }
1354
1411
  for (const filePath of font.subsetFiles ?? []) {
1355
- 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);
1356
1413
  const stamp = await this.#fileStamp(abs);
1357
1414
  if (!stamp)
1358
1415
  return null;
1359
1416
  sources.push({ subsetFile: filePath, ...stamp });
1360
1417
  }
1361
1418
  if (this.#getFontSubsets(font).includes("auto"))
1362
- sources.push({ autoSubsetText: this.#hashFontConfig(await this.#collectAutoSubsetText()) });
1419
+ sources.push({ autoSubsetText: this.#cacheDigest(await this.#collectAutoSubsetText()) });
1363
1420
  }
1364
- 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");
1365
1425
  }
1366
1426
  async#fileStamp(filePath) {
1367
1427
  try {
@@ -1375,7 +1435,7 @@ class FontOptimizer {
1375
1435
  const cache = await Bun.file(this.#cachePath).json().catch(() => null);
1376
1436
  if (cache?.version !== FontOptimizer.#cacheVersion || cache.key !== key)
1377
1437
  return null;
1378
- const files = cache.files.map((relativePath) => path9.join(this.#artifactRoot, relativePath));
1438
+ const files = cache.files.map((relativePath) => path10.join(this.#artifactRoot, relativePath));
1379
1439
  for (const filePath of files) {
1380
1440
  if (!await Bun.file(filePath).exists())
1381
1441
  return null;
@@ -1387,7 +1447,7 @@ class FontOptimizer {
1387
1447
  version: FontOptimizer.#cacheVersion,
1388
1448
  key,
1389
1449
  css: result.css,
1390
- files: result.files.map((filePath) => path9.relative(this.#artifactRoot, filePath))
1450
+ files: result.files.map((filePath) => path10.relative(this.#artifactRoot, filePath))
1391
1451
  };
1392
1452
  await Bun.write(this.#cachePath, JSON.stringify(cache));
1393
1453
  }
@@ -1395,7 +1455,7 @@ class FontOptimizer {
1395
1455
  const pageKeys = await this.#app.getPageKeys();
1396
1456
  const fonts = [];
1397
1457
  await Promise.all(pageKeys.map(async (key) => {
1398
- const filePath = path9.resolve(this.#app.cwdPath, "page", key);
1458
+ const filePath = path10.resolve(this.#app.cwdPath, "page", key);
1399
1459
  const file = Bun.file(filePath);
1400
1460
  if (!await file.exists())
1401
1461
  return;
@@ -1414,8 +1474,8 @@ class FontOptimizer {
1414
1474
  this.#app.logger.warn(`[font] source not found: ${face.src}`);
1415
1475
  continue;
1416
1476
  }
1417
- const outputPath = path9.join(this.#artifactRoot, face.optimizedSrc.replace(/^\/_akan\//, ""));
1418
- 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 });
1419
1479
  const sourceBuffer = Buffer.from(await Bun.file(sourcePath).arrayBuffer());
1420
1480
  const outputBuffer = await this.#buildFontBuffer(font, sourceBuffer, sourcePath);
1421
1481
  await Bun.write(outputPath, outputBuffer);
@@ -1429,17 +1489,17 @@ class FontOptimizer {
1429
1489
  this.#cssParts.push(...faceCss, this.#buildRootVariableRule(font));
1430
1490
  }
1431
1491
  #extractFontsExport(source, filePath) {
1432
- const sourceFile = ts2.createSourceFile(filePath, source, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TSX);
1492
+ const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
1433
1493
  const fonts = [];
1434
1494
  for (const statement of sourceFile.statements) {
1435
- if (!ts2.isVariableStatement(statement))
1495
+ if (!ts.isVariableStatement(statement))
1436
1496
  continue;
1437
- const modifiers = ts2.canHaveModifiers(statement) ? ts2.getModifiers(statement) : undefined;
1438
- const isExported = modifiers?.some((modifier) => modifier.kind === ts2.SyntaxKind.ExportKeyword) ?? false;
1497
+ const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : undefined;
1498
+ const isExported = modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false;
1439
1499
  if (!isExported)
1440
1500
  continue;
1441
1501
  for (const declaration of statement.declarationList.declarations) {
1442
- if (!ts2.isIdentifier(declaration.name) || declaration.name.text !== "fonts")
1502
+ if (!ts.isIdentifier(declaration.name) || declaration.name.text !== "fonts")
1443
1503
  continue;
1444
1504
  const value = declaration.initializer ? this.#literalToValue(declaration.initializer) : null;
1445
1505
  if (Array.isArray(value)) {
@@ -1450,20 +1510,20 @@ class FontOptimizer {
1450
1510
  return fonts;
1451
1511
  }
1452
1512
  #literalToValue(node) {
1453
- if (ts2.isStringLiteralLike(node))
1513
+ if (ts.isStringLiteralLike(node))
1454
1514
  return node.text;
1455
- if (ts2.isNumericLiteral(node))
1515
+ if (ts.isNumericLiteral(node))
1456
1516
  return Number(node.text);
1457
- if (node.kind === ts2.SyntaxKind.TrueKeyword)
1517
+ if (node.kind === ts.SyntaxKind.TrueKeyword)
1458
1518
  return true;
1459
- if (node.kind === ts2.SyntaxKind.FalseKeyword)
1519
+ if (node.kind === ts.SyntaxKind.FalseKeyword)
1460
1520
  return false;
1461
- if (ts2.isArrayLiteralExpression(node))
1521
+ if (ts.isArrayLiteralExpression(node))
1462
1522
  return node.elements.map((element) => this.#literalToValue(element));
1463
- if (ts2.isObjectLiteralExpression(node)) {
1523
+ if (ts.isObjectLiteralExpression(node)) {
1464
1524
  const obj = {};
1465
1525
  for (const prop of node.properties) {
1466
- if (!ts2.isPropertyAssignment(prop))
1526
+ if (!ts.isPropertyAssignment(prop))
1467
1527
  continue;
1468
1528
  const name = this.#getPropertyName(prop.name);
1469
1529
  if (!name)
@@ -1472,13 +1532,13 @@ class FontOptimizer {
1472
1532
  }
1473
1533
  return obj;
1474
1534
  }
1475
- if (ts2.isAsExpression(node) || ts2.isSatisfiesExpression(node) || ts2.isParenthesizedExpression(node)) {
1535
+ if (ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isParenthesizedExpression(node)) {
1476
1536
  return this.#literalToValue(node.expression);
1477
1537
  }
1478
1538
  return;
1479
1539
  }
1480
1540
  #getPropertyName(name) {
1481
- if (ts2.isIdentifier(name) || ts2.isStringLiteral(name) || ts2.isNumericLiteral(name))
1541
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name))
1482
1542
  return name.text;
1483
1543
  return null;
1484
1544
  }
@@ -1562,8 +1622,8 @@ class FontOptimizer {
1562
1622
  return null;
1563
1623
  const rel = src.replace(/^\//, "");
1564
1624
  const candidates = [
1565
- this.#command === "build" ? path9.join(this.#app.dist.cwdPath, "public", rel) : null,
1566
- path9.join(this.#app.cwdPath, "public", rel),
1625
+ this.#command === "build" ? path10.join(this.#app.dist.cwdPath, "public", rel) : null,
1626
+ path10.join(this.#app.cwdPath, "public", rel),
1567
1627
  this.#resolveWorkspacePublicPath(rel)
1568
1628
  ].filter(Boolean);
1569
1629
  for (const candidate of candidates) {
@@ -1598,7 +1658,7 @@ class FontOptimizer {
1598
1658
  return "woff2";
1599
1659
  if (signature === "OTTO")
1600
1660
  return "otf";
1601
- const ext = path9.extname(sourcePath).slice(1).toLowerCase();
1661
+ const ext = path10.extname(sourcePath).slice(1).toLowerCase();
1602
1662
  if (ext === "otf" || ext === "woff" || ext === "woff2")
1603
1663
  return ext;
1604
1664
  return "ttf";
@@ -1607,7 +1667,7 @@ class FontOptimizer {
1607
1667
  const [root, dep, ...rest] = rel.split("/");
1608
1668
  if (root !== "libs" || !dep || rest.length === 0)
1609
1669
  return null;
1610
- return path9.join(this.#app.workspace.workspaceRoot, "libs", dep, "public", ...rest);
1670
+ return path10.join(this.#app.workspace.workspaceRoot, "libs", dep, "public", ...rest);
1611
1671
  }
1612
1672
  async#getSubsetText(font) {
1613
1673
  const parts = new Set;
@@ -1617,7 +1677,7 @@ class FontOptimizer {
1617
1677
  if (font.subsetText)
1618
1678
  parts.add(font.subsetText);
1619
1679
  for (const filePath of font.subsetFiles ?? []) {
1620
- const abs = path9.isAbsolute(filePath) ? filePath : path9.join(this.#app.cwdPath, filePath);
1680
+ const abs = path10.isAbsolute(filePath) ? filePath : path10.join(this.#app.cwdPath, filePath);
1621
1681
  const file = Bun.file(abs);
1622
1682
  if (await file.exists())
1623
1683
  parts.add(await file.text());
@@ -1637,16 +1697,19 @@ class FontOptimizer {
1637
1697
  }
1638
1698
  async#collectAutoSubsetText() {
1639
1699
  const libPageRoots = (await this.#app.getPageRoots()).filter((root) => root.keyPrefix).map((root) => root.dir);
1640
- const roots = [...["page", "ui"].map((dir) => path9.join(this.#app.cwdPath, dir)), ...libPageRoots];
1700
+ const roots = [...["page", "ui", "lib"].map((dir) => path10.join(this.#app.cwdPath, dir)), ...libPageRoots];
1641
1701
  const glob = new Bun.Glob("**/*.{ts,tsx,js,jsx,html,md}");
1642
1702
  const parts = [];
1643
- await Promise.all(roots.map(async (root) => {
1644
- if (!await stat2(root).then((entry) => entry.isDirectory(), () => false))
1645
- return;
1646
- for await (const filePath of glob.scan({ cwd: root, absolute: true })) {
1703
+ for (const root of [...new Set(roots)].sort()) {
1704
+ const isDir = await stat2(root).then((entry) => entry.isDirectory(), () => false);
1705
+ if (!isDir)
1706
+ continue;
1707
+ const filePaths = [];
1708
+ for await (const filePath of glob.scan({ cwd: root, absolute: true }))
1709
+ filePaths.push(filePath);
1710
+ for (const filePath of filePaths.sort())
1647
1711
  parts.push(await Bun.file(filePath).text());
1648
- }
1649
- }));
1712
+ }
1650
1713
  return parts.join("");
1651
1714
  }
1652
1715
  #rangeText(start, end) {
@@ -1714,332 +1777,210 @@ ${declarations.map(([prop, value]) => ` ${prop}: ${value};`).join(`
1714
1777
  if (!fallbackMetrics)
1715
1778
  continue;
1716
1779
  css.push(generateFontFace({ ...metrics, category: font.category }, {
1717
- name: this.#getFontFallbackName(font),
1718
- font: fallback,
1719
- metrics: fallbackMetrics,
1720
- "font-weight": String(face.weight),
1721
- "font-style": face.style
1722
- }));
1723
- }
1724
- return css.join("");
1725
- }
1726
- #buildRootVariableRule(font) {
1727
- return `:root { ${this.#getFontVariableName(font)}: ${this.#quote(font.name)}, ${this.#quote(this.#getFontFallbackName(font))}; }`;
1728
- }
1729
- #buildFontUtilityRules(fonts) {
1730
- const rules = [];
1731
- const seen = new Set;
1732
- for (const font of fonts) {
1733
- const className = font.className || `font-${font.name}`;
1734
- const selector = `.${this.#escapeCssClassName(className)}`;
1735
- const rule = `${selector} { font-family: var(${this.#getFontVariableName(font)}); }`;
1736
- if (seen.has(rule))
1737
- continue;
1738
- seen.add(rule);
1739
- rules.push(rule);
1740
- }
1741
- return rules.join(`
1742
- `);
1743
- }
1744
- #escapeCssClassName(value) {
1745
- return value.replace(/^-?\d|[^a-zA-Z0-9_-]/g, (part) => [...part].map((char) => `\\${char.codePointAt(0)?.toString(16)} `).join(""));
1746
- }
1747
- #toDeclarationEntries(declarations = []) {
1748
- return declarations.map((declaration) => [declaration.prop, declaration.value]);
1749
- }
1750
- #quote(value) {
1751
- return JSON.stringify(value);
1752
- }
1753
- }
1754
-
1755
- // pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
1756
- import path11 from "path";
1757
-
1758
- // pkgs/@akanjs/devkit/transforms/externalizeFrameworkPlugin.ts
1759
- import path10 from "path";
1760
- var DEFAULT_INCLUDE = ["akanjs/", "@apps/", "@libs/"];
1761
- var DEFAULT_EXCLUDE_EXACT = new Set(["akanjs/webkit", "@akanjs/cli", "@akanjs/devkit"]);
1762
- var DEFAULT_EXCLUDE_PREFIX = ["@akanjs/cli/", "@akanjs/devkit/"];
1763
- var OPTIONAL_BACKEND_EXTERNAL_EXACT = new Set([
1764
- "@libsql/client",
1765
- "bullmq",
1766
- "croner",
1767
- "ioredis",
1768
- "postgres",
1769
- "protobufjs"
1770
- ]);
1771
- var RUNTIME_EXTERNAL_EXACT = new Set([
1772
- "react",
1773
- "react-dom",
1774
- "react/jsx-runtime",
1775
- "react/jsx-dev-runtime",
1776
- "react-server-dom-webpack",
1777
- "react-server-dom-webpack/server.node",
1778
- "react-server-dom-webpack/client.node",
1779
- "react-server-dom-webpack/client.browser"
1780
- ]);
1781
- var RUNTIME_EXTERNAL_PREFIX = ["react-dom/", "react-server-dom-webpack/"];
1782
- var CANDIDATE_EXTS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
1783
- async function createExternalizeFrameworkPlugin(options) {
1784
- const tsconfig = await options.app.getTsConfig();
1785
- const includePrefixes = options.include ?? DEFAULT_INCLUDE;
1786
- const extraExact = new Set(options.extra ?? []);
1787
- const workspaceRoot = options.app.workspace.workspaceRoot;
1788
- const tsconfigPaths = tsconfig.compilerOptions.paths ?? {};
1789
- const rootEntries = Object.entries(tsconfigPaths).filter(([k]) => !k.endsWith("/*")).map(([k, v]) => ({ pkg: k, entryFile: v[0] ?? null })).filter((e) => e.entryFile !== null);
1790
- 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);
1791
- async function resolveWorkspaceSubpath(spec) {
1792
- if (!workspaceRoot)
1793
- return null;
1794
- for (const { prefix, replacements } of wildcardEntries) {
1795
- if (!spec.startsWith(prefix))
1796
- continue;
1797
- const suffix = spec.slice(prefix.length);
1798
- for (const repl of replacements) {
1799
- const replPath = repl?.endsWith("/*") ? repl.slice(0, -1) : repl ?? "";
1800
- if (!replPath)
1801
- continue;
1802
- const candidate = path10.resolve(workspaceRoot, replPath + suffix);
1803
- const hit = await firstExisting(candidate);
1804
- if (hit)
1805
- return hit;
1806
- }
1807
- }
1808
- for (const { pkg, entryFile } of rootEntries) {
1809
- if (spec !== pkg && !spec.startsWith(`${pkg}/`))
1810
- continue;
1811
- if (spec === pkg)
1812
- continue;
1813
- const suffix = spec.slice(pkg.length + 1);
1814
- const pkgDir = path10.dirname(path10.resolve(workspaceRoot, entryFile));
1815
- const candidate = path10.join(pkgDir, suffix);
1816
- const hit = await firstExisting(candidate);
1817
- if (hit)
1818
- return hit;
1819
- }
1820
- return null;
1821
- }
1822
- return {
1823
- name: "akan-externalize-framework",
1824
- setup(build) {
1825
- build.onResolve({ filter: /.*/ }, async (args) => {
1826
- const spec = args.path;
1827
- if (spec === "." || spec === ".." || spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/"))
1828
- return;
1829
- if (extraExact.has(spec) || DEFAULT_EXCLUDE_EXACT.has(spec))
1830
- return { path: spec, external: true };
1831
- for (const prefix of DEFAULT_EXCLUDE_PREFIX) {
1832
- if (spec.startsWith(prefix))
1833
- return { path: spec, external: true };
1834
- }
1835
- if (OPTIONAL_BACKEND_EXTERNAL_EXACT.has(spec))
1836
- return { path: spec, external: true };
1837
- if (RUNTIME_EXTERNAL_EXACT.has(spec))
1838
- return { path: spec, external: true };
1839
- for (const prefix of RUNTIME_EXTERNAL_PREFIX) {
1840
- if (spec.startsWith(prefix))
1841
- return { path: spec, external: true };
1842
- }
1843
- for (const prefix of includePrefixes) {
1844
- if (!spec.startsWith(prefix))
1845
- continue;
1846
- const resolved = await resolveWorkspaceSubpath(spec);
1847
- if (resolved)
1848
- return { path: resolved };
1849
- return;
1850
- }
1851
- return;
1852
- });
1780
+ name: this.#getFontFallbackName(font),
1781
+ font: fallback,
1782
+ metrics: fallbackMetrics,
1783
+ "font-weight": String(face.weight),
1784
+ "font-style": face.style
1785
+ }));
1853
1786
  }
1854
- };
1855
- }
1856
- async function firstExisting(basePath) {
1857
- if (await Bun.file(basePath).exists())
1858
- return basePath;
1859
- for (const ext of CANDIDATE_EXTS) {
1860
- const candidate = `${basePath}${ext}`;
1861
- if (await Bun.file(candidate).exists())
1862
- return candidate;
1787
+ return css.join("");
1863
1788
  }
1864
- for (const ext of CANDIDATE_EXTS) {
1865
- const candidate = path10.join(basePath, `index${ext}`);
1866
- if (await Bun.file(candidate).exists())
1867
- return candidate;
1789
+ #buildRootVariableRule(font) {
1790
+ return `:root { ${this.#getFontVariableName(font)}: ${this.#quote(font.name)}, ${this.#quote(this.#getFontFallbackName(font))}; }`;
1868
1791
  }
1869
- return null;
1870
- }
1871
-
1872
- // pkgs/@akanjs/devkit/transforms/useClientBundlePlugin.ts
1873
- function createUseClientBundlePlugin(options = {}) {
1874
- return {
1875
- name: "akan-use-client-bundle",
1876
- setup(build) {
1877
- build.onLoad({ filter: /\.(tsx|ts|jsx|js)$/ }, async (args) => {
1878
- if (args.path.includes("/node_modules/"))
1879
- return;
1880
- let source;
1881
- try {
1882
- source = await Bun.file(args.path).text();
1883
- } catch {
1884
- return;
1885
- }
1886
- const stubbed = transformUseClient(source, {
1887
- path: args.path,
1888
- workspaceRoot: options.workspaceRoot
1889
- });
1890
- if (stubbed === null)
1891
- return;
1892
- return { contents: stubbed, loader: loaderFor(args.path) };
1893
- });
1792
+ #buildFontUtilityRules(fonts) {
1793
+ const rules = [];
1794
+ const seen = new Set;
1795
+ for (const font of fonts) {
1796
+ const className = font.className || `font-${font.name}`;
1797
+ const selector = `.${this.#escapeCssClassName(className)}`;
1798
+ const rule = `${selector} { font-family: var(${this.#getFontVariableName(font)}); }`;
1799
+ if (seen.has(rule))
1800
+ continue;
1801
+ seen.add(rule);
1802
+ rules.push(rule);
1894
1803
  }
1895
- };
1896
- }
1897
- function loaderFor(absPath) {
1898
- if (absPath.endsWith(".tsx"))
1899
- return "tsx";
1900
- if (absPath.endsWith(".jsx"))
1901
- return "jsx";
1902
- if (absPath.endsWith(".ts"))
1903
- return "ts";
1904
- return "js";
1804
+ return rules.join(`
1805
+ `);
1806
+ }
1807
+ #escapeCssClassName(value) {
1808
+ return value.replace(/^-?\d|[^a-zA-Z0-9_-]/g, (part) => [...part].map((char) => `\\${char.codePointAt(0)?.toString(16)} `).join(""));
1809
+ }
1810
+ #toDeclarationEntries(declarations = []) {
1811
+ return declarations.map((declaration) => [declaration.prop, declaration.value]);
1812
+ }
1813
+ #quote(value) {
1814
+ return JSON.stringify(value);
1815
+ }
1905
1816
  }
1906
1817
 
1907
- // pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
1908
- var VIRTUAL_PAGES_ENTRY = "akan-pages-entry";
1818
+ // pkgs/@akanjs/devkit/frontendBuild/fontPruner.ts
1819
+ import { rm as rm2, rmdir, stat as stat3 } from "fs/promises";
1820
+ import path11 from "path";
1821
+ import { isFontOptimizationEnabled } from "akanjs/client";
1822
+ var FONT_GLOB = "**/*.{woff2,woff,ttf,otf,ttc,eot}";
1823
+ var REFERRER_GLOB = "**/*.{css,js,mjs,cjs,json,html,htm,svg,xml,webmanifest}";
1909
1824
 
1910
- class PagesBundleBuilder {
1825
+ class FontPruner {
1911
1826
  #app;
1912
- #command;
1913
- #pageEntries;
1914
- #started = Date.now();
1915
- constructor(app, command = "start", pageEntries) {
1827
+ #keepGlobs;
1828
+ #publicRoot;
1829
+ #artifactRoot;
1830
+ constructor(app, { keepFonts = [] } = {}) {
1916
1831
  this.#app = app;
1917
- this.#command = command;
1918
- this.#pageEntries = pageEntries;
1919
- }
1920
- async build() {
1921
- const akanConfig = await this.#app.getConfig();
1922
- const resolvedEntries = this.#pageEntries ?? await resolveSsrPageEntriesForApp(this.#app, await this.#app.getPageKeys());
1923
- const entrySource = PagesEntrySourceGenerator.generate(resolvedEntries);
1924
- const workspaceRoot = this.#app.workspace.workspaceRoot;
1925
- const result = await Bun.build({
1926
- entrypoints: [VIRTUAL_PAGES_ENTRY],
1927
- outdir: `${this.#artifactDir}/server`,
1928
- target: "bun",
1929
- format: "esm",
1930
- splitting: this.#splitting,
1931
- minify: this.#command === "build",
1932
- naming: {
1933
- entry: "pages-[hash].[ext]",
1934
- chunk: "chunks/[name]-[hash].[ext]",
1935
- asset: "assets/[name]-[hash].[ext]"
1936
- },
1937
- define: this.#define(),
1938
- plugins: [
1939
- PagesBundleBuilder.createPagesEntryPlugin(entrySource),
1940
- PagesBundleBuilder.createServerCssStubPlugin(),
1941
- PagesBundleBuilder.createServerUseClientFetchPlugin(),
1942
- await createExternalizeFrameworkPlugin({ app: this.#app, extra: akanConfig.externalLibs }),
1943
- akanConfig.barrelImports.length > 0 ? await createBarrelImportsPlugin(this.#app, {
1944
- pipeAfter: (source, args) => transformUseClient(source, {
1945
- path: args.path,
1946
- workspaceRoot
1947
- })
1948
- }) : createUseClientBundlePlugin({ workspaceRoot })
1949
- ]
1950
- });
1951
- if (!result.success)
1952
- throw new AggregateError(result.logs, "[PagesBundleBuilder] Bun.build failed");
1953
- const entryArtifact = result.outputs.find((a) => a.kind === "entry-point");
1954
- if (!entryArtifact)
1955
- throw new Error("[PagesBundleBuilder] Bun.build emitted no entry-point artifact");
1956
- const bundlePath = path11.resolve(entryArtifact.path);
1957
- const buildId = Date.now();
1958
- const outputBytes = result.outputs.reduce((sum, output) => sum + output.size, 0);
1959
- const chunkCount = result.outputs.filter((output) => output.kind === "chunk").length;
1960
- 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`);
1961
- return {
1962
- bundlePath,
1963
- buildId,
1964
- splitting: this.#splitting,
1965
- entryBytes: entryArtifact.size,
1966
- outputBytes,
1967
- outputCount: result.outputs.length,
1968
- chunkCount
1969
- };
1970
- }
1971
- get #artifactDir() {
1972
- return `${this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath}/.akan/artifact`;
1832
+ this.#keepGlobs = keepFonts;
1833
+ this.#publicRoot = path11.join(app.dist.cwdPath, "public");
1834
+ this.#artifactRoot = path11.join(app.dist.cwdPath, ".akan/artifact");
1835
+ }
1836
+ async prune() {
1837
+ const empty = { removed: [], kept: [], freedBytes: 0 };
1838
+ if (!await this.#isDirectory(this.#publicRoot))
1839
+ return empty;
1840
+ const candidates = await this.#collectCandidates();
1841
+ if (!candidates.length)
1842
+ return empty;
1843
+ const fonts = await new FontOptimizer(this.#app, "build").discoverFonts();
1844
+ const unoptimizedSrcs = new Set;
1845
+ const optimizedSrcs = new Set;
1846
+ for (const font of fonts) {
1847
+ const target = isFontOptimizationEnabled(font) ? optimizedSrcs : unoptimizedSrcs;
1848
+ for (const fontPath of font.paths)
1849
+ target.add(path11.posix.basename(fontPath.src));
1850
+ }
1851
+ const result = { removed: [], kept: [], freedBytes: 0 };
1852
+ const undecided = [];
1853
+ for (const candidate of candidates) {
1854
+ const reason = this.#staticKeepReason(candidate, unoptimizedSrcs);
1855
+ if (reason)
1856
+ result.kept.push({ file: candidate.rel, bytes: candidate.bytes, reason });
1857
+ else
1858
+ undecided.push(candidate);
1859
+ }
1860
+ if (undecided.length) {
1861
+ const referrers = await this.#findReferrers(undecided, optimizedSrcs);
1862
+ for (const candidate of undecided) {
1863
+ const referrer = referrers.get(candidate.rel);
1864
+ if (referrer)
1865
+ result.kept.push({ file: candidate.rel, bytes: candidate.bytes, reason: { kind: "referenced", referrer } });
1866
+ else
1867
+ result.removed.push({ file: candidate.rel, bytes: candidate.bytes });
1868
+ }
1869
+ }
1870
+ await this.#remove(result.removed.map(({ file }) => file));
1871
+ result.freedBytes = result.removed.reduce((total, { bytes }) => total + bytes, 0);
1872
+ this.#report(result);
1873
+ return result;
1973
1874
  }
1974
- get #splitting() {
1975
- return process.env.AKAN_SERVER_PAGES_SPLITTING === "1";
1875
+ #staticKeepReason(candidate, unoptimizedSrcs) {
1876
+ if (unoptimizedSrcs.has(candidate.basename))
1877
+ return { kind: "unoptimized" };
1878
+ for (const glob of this.#keepGlobs) {
1879
+ if (new Bun.Glob(glob).match(candidate.rel))
1880
+ return { kind: "declared", glob };
1881
+ }
1882
+ return null;
1976
1883
  }
1977
- #define() {
1978
- const nodeEnv = this.#command === "build" ? "production" : "development";
1979
- return {
1980
- "process.env.NODE_ENV": JSON.stringify(nodeEnv),
1981
- "process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("ssr"),
1982
- ...Object.fromEntries(Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]))
1983
- };
1884
+ async#collectCandidates() {
1885
+ const candidates = [];
1886
+ for await (const rel of new Bun.Glob(FONT_GLOB).scan({ cwd: this.#publicRoot, dot: false })) {
1887
+ const entry = await stat3(path11.join(this.#publicRoot, rel)).catch(() => null);
1888
+ if (!entry?.isFile())
1889
+ continue;
1890
+ candidates.push({ rel: rel.split(path11.sep).join("/"), basename: path11.basename(rel), bytes: entry.size });
1891
+ }
1892
+ return candidates;
1984
1893
  }
1985
- static createPagesEntryPlugin(source) {
1986
- return {
1987
- name: "akan-pages-entry",
1988
- setup(build) {
1989
- build.onResolve({ filter: /^akan-pages-entry$/ }, () => ({
1990
- path: VIRTUAL_PAGES_ENTRY,
1991
- namespace: "akan-virtual"
1992
- }));
1993
- build.onLoad({ filter: /^akan-pages-entry$/, namespace: "akan-virtual" }, () => ({
1994
- contents: source,
1995
- loader: "tsx"
1996
- }));
1997
- }
1998
- };
1894
+ async#findReferrers(candidates, optimizedSrcs) {
1895
+ const referrers = new Map;
1896
+ const authoritative = candidates;
1897
+ const bundled = candidates.filter((candidate) => !optimizedSrcs.has(candidate.basename));
1898
+ const roots = [
1899
+ { dir: this.#publicRoot, label: "public", needles: authoritative },
1900
+ { dir: path11.join(this.#artifactRoot, "styles"), label: ".akan/artifact/styles", needles: authoritative },
1901
+ { dir: path11.join(this.#artifactRoot, "client"), label: ".akan/artifact/client", needles: bundled },
1902
+ { dir: path11.join(this.#artifactRoot, "client-ssr"), label: ".akan/artifact/client-ssr", needles: bundled },
1903
+ { dir: path11.join(this.#artifactRoot, "server"), label: ".akan/artifact/server", needles: bundled },
1904
+ { dir: path11.join(this.#app.dist.cwdPath, "csr"), label: "csr", needles: bundled }
1905
+ ];
1906
+ for (const root of roots) {
1907
+ const pending = root.needles.filter((candidate) => !referrers.has(candidate.rel));
1908
+ if (!pending.length || !await this.#isDirectory(root.dir))
1909
+ continue;
1910
+ await this.#scanRoot(root, pending, referrers);
1911
+ }
1912
+ return referrers;
1999
1913
  }
2000
- static createServerCssStubPlugin() {
2001
- return {
2002
- name: "akan-server-css-stub",
2003
- setup(build) {
2004
- build.onLoad({ filter: /\.css$/ }, () => ({
2005
- contents: "",
2006
- loader: "js"
2007
- }));
1914
+ async#scanRoot(root, pending, referrers) {
1915
+ const needles = pending.map((candidate) => ({
1916
+ rel: candidate.rel,
1917
+ forms: [...new Set([candidate.basename, encodeURIComponent(candidate.basename)])]
1918
+ }));
1919
+ for await (const rel of new Bun.Glob(REFERRER_GLOB).scan({ cwd: root.dir, dot: false })) {
1920
+ const remaining = needles.filter((needle) => !referrers.has(needle.rel));
1921
+ if (!remaining.length)
1922
+ return;
1923
+ const text = await Bun.file(path11.join(root.dir, rel)).text().catch(() => "");
1924
+ if (!text)
1925
+ continue;
1926
+ for (const needle of remaining) {
1927
+ if (needle.forms.some((form) => text.includes(form)))
1928
+ referrers.set(needle.rel, `${root.label}/${rel.split(path11.sep).join("/")}`);
2008
1929
  }
2009
- };
1930
+ }
2010
1931
  }
2011
- static createServerUseClientFetchPlugin() {
2012
- return {
2013
- name: "akan-server-use-client-fetch",
2014
- setup(build) {
2015
- build.onLoad({ filter: /[/\\]lib[/\\]useClient\.(ts|tsx|js|jsx)$/ }, async (args) => {
2016
- const source = await Bun.file(args.path).text();
2017
- const transformed = PagesBundleBuilder.transformServerUseClientFetchSource(source);
2018
- if (transformed === source)
2019
- return;
2020
- return { contents: transformed, loader: loaderFor2(args.path) };
2021
- });
2022
- }
2023
- };
1932
+ async#remove(files) {
1933
+ await Promise.all(files.map((file) => rm2(path11.join(this.#publicRoot, file), { force: true })));
1934
+ const dirs = [...new Set(files.map((file) => path11.posix.dirname(file)))].filter((dir) => dir !== "." && dir !== "/").sort((a, b) => b.length - a.length);
1935
+ for (const dir of dirs)
1936
+ await rmdir(path11.join(this.#publicRoot, dir)).catch(() => {
1937
+ return;
1938
+ });
2024
1939
  }
2025
- static transformServerUseClientFetchSource(source) {
2026
- if (!source.includes(`with { type: "macro" }`) || !source.includes("FetchClient.build"))
2027
- return source;
2028
- 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 });");
1940
+ #report(result) {
1941
+ if (!result.removed.length && !result.kept.length)
1942
+ return;
1943
+ for (const { file, bytes } of result.removed)
1944
+ this.#app.verbose(`[font-prune] dropped public/${file} (${FontPruner.formatBytes(bytes)})`);
1945
+ for (const { file, reason } of result.kept)
1946
+ this.#app.verbose(`[font-prune] kept public/${file} \u2014 ${FontPruner.describe(reason)}`);
1947
+ const byReferrer = new Map;
1948
+ for (const { bytes, reason } of result.kept) {
1949
+ if (reason.kind !== "referenced")
1950
+ continue;
1951
+ const entry = byReferrer.get(reason.referrer) ?? { files: 0, bytes: 0 };
1952
+ entry.files += 1;
1953
+ entry.bytes += bytes;
1954
+ byReferrer.set(reason.referrer, entry);
1955
+ }
1956
+ if (!byReferrer.size)
1957
+ return;
1958
+ const ranked = [...byReferrer.entries()].sort(([, a], [, b]) => b.bytes - a.bytes);
1959
+ const named = ranked.slice(0, 3).map(([referrer, { files, bytes }]) => `${referrer} (${files}, ${FontPruner.formatBytes(bytes)})`);
1960
+ const rest = ranked.length - named.length;
1961
+ 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` : ""}`);
1962
+ }
1963
+ static describe(reason) {
1964
+ if (reason.kind === "unoptimized")
1965
+ return "declared with optimize: false, so the runtime CSS serves it";
1966
+ if (reason.kind === "declared")
1967
+ return `kept by assets.keepFonts "${reason.glob}"`;
1968
+ return `referenced by ${reason.referrer}`;
1969
+ }
1970
+ static formatBytes(bytes) {
1971
+ if (bytes < 1024)
1972
+ return `${bytes}B`;
1973
+ if (bytes < 1024 * 1024)
1974
+ return `${(bytes / 1024).toFixed(1)}KB`;
1975
+ return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
1976
+ }
1977
+ async#isDirectory(dir) {
1978
+ return await stat3(dir).then((entry) => entry.isDirectory(), () => false);
2029
1979
  }
2030
1980
  }
2031
- function loaderFor2(absPath) {
2032
- if (absPath.endsWith(".tsx"))
2033
- return "tsx";
2034
- if (absPath.endsWith(".jsx"))
2035
- return "jsx";
2036
- if (absPath.endsWith(".ts"))
2037
- return "ts";
2038
- return "js";
2039
- }
2040
1981
 
2041
1982
  // pkgs/@akanjs/devkit/frontendBuild/precompressArtifacts.ts
2042
- import fs2 from "fs";
1983
+ import fs from "fs";
2043
1984
  import path12 from "path";
2044
1985
  import zlib from "zlib";
2045
1986
  var COMPRESSIBLE_EXTS = new Set([".css", ".html", ".js", ".json", ".svg"]);
@@ -2060,7 +2001,7 @@ async function precompressArtifacts(app) {
2060
2001
  return result;
2061
2002
  }
2062
2003
  async function precompressRoot(root, result) {
2063
- if (!fs2.existsSync(root) || !fs2.statSync(root).isDirectory())
2004
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory())
2064
2005
  return;
2065
2006
  const glob = new Bun.Glob("**/*");
2066
2007
  for await (const filePath of glob.scan({ cwd: root, absolute: true })) {
@@ -2154,6 +2095,7 @@ class SsrBaseArtifactBuilder {
2154
2095
  branches: [...akanConfig.branches],
2155
2096
  i18n: akanConfig.i18n,
2156
2097
  imageConfig: akanConfig.images,
2098
+ web: akanConfig.web,
2157
2099
  deepLinkAssociations: Object.values(akanConfig.mobile.targets).filter((target) => (target.deepLinks?.domains?.length ?? 0) > 0).map((target) => ({
2158
2100
  targetName: target.name,
2159
2101
  appId: target.appId,
@@ -2180,7 +2122,7 @@ class SsrBaseArtifactBuilder {
2180
2122
  const ssrBundle = await new ClientEntriesBundler({
2181
2123
  app: this.#app,
2182
2124
  entries: [rscSegmentOutletEntry],
2183
- ...RouteClientBuilder.resolveSsrClientExternalOptions(this.#command),
2125
+ ...RouteClientBuilder.resolveSsrClientBundleOptions(this.#command),
2184
2126
  outputSubdir: "client-ssr",
2185
2127
  command: this.#command
2186
2128
  }).bundle();
@@ -2232,13 +2174,13 @@ class SsrBaseArtifactBuilder {
2232
2174
  `);
2233
2175
  if (!cssText)
2234
2176
  return [];
2235
- return [this.#writeCssAsset(basePath, cssText)];
2177
+ return [this.#writeCssAsset(basePath, cssText, cssCompiler.importedStylesheetsByBasePath[basePath] ?? [])];
2236
2178
  })));
2237
2179
  if (optimizedFonts.files.length > 0)
2238
2180
  this.#app.verbose(`[base-artifact] optimized ${optimizedFonts.files.length} font files`);
2239
2181
  return { cssCompiler, optimizedFonts, cssAssets };
2240
2182
  }
2241
- async#writeCssAsset(basePath, cssText) {
2183
+ async#writeCssAsset(basePath, cssText, imported) {
2242
2184
  const cssAssetName = basePath || "root";
2243
2185
  const preparedCssText = await prepareCssAsset(this.#command, basePath, cssText);
2244
2186
  const cssHash = Bun.hash(`${basePath}
@@ -2248,220 +2190,16 @@ ${preparedCssText}`).toString(36);
2248
2190
  `/_akan/styles/${cssAssetName}-${cssHash}.css`
2249
2191
  ];
2250
2192
  await Bun.write(path13.join(this.#absArtifactDir, cssRelPath), preparedCssText);
2193
+ SsrBaseArtifactBuilder.#warnDroppedImports(this.#app, cssRelPath, preparedCssText, imported);
2251
2194
  this.#app.verbose(`[base-artifact] wrote ${preparedCssText.length} bytes of CSS for ${basePath} -> ${cssRelPath}`);
2252
2195
  return [basePath, { cssUrl, cssRelPath }];
2253
2196
  }
2254
- }
2255
- // pkgs/@akanjs/devkit/frontendBuild/styleGuard.ts
2256
- var PREFIX = "(?:bg|text|border(?:-[tblrxy])?(?:-[se])?|ring(?:-offset)?|fill|stroke|shadow|from|to|via|divide|outline|decoration|placeholder|caret|accent)";
2257
- var PALETTE = "(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)";
2258
- var LEAD = "(^|[\\s\"'`{(\\[:!])";
2259
- var TAIL = "(?=[\\s\"'`})\\]:/!,]|$)";
2260
- var RAW_PALETTE_RE = new RegExp(`${LEAD}((?:${PREFIX})-)${PALETTE}-\\d{2,3}${TAIL}`, "g");
2261
- var ARBITRARY_COLOR_RE = /\[(#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|oklch|oklab|lab|lch|hwb|color)\([^\]]*\))\]/g;
2262
- var INTERPOLATED_ARBITRARY_RE = new RegExp(`${LEAD}[a-z][a-z0-9-]*-\\[[^\\]\`]*\\$\\{`, "g");
2263
- var STYLE_OBJECT_RE = /style=\{\{([\s\S]*?)\}\}/g;
2264
- var STYLE_TAG_RE = /<style[^>]*>([\s\S]*?)<\/style>/g;
2265
- var INLINE_COLOR_LITERAL_RE = /#[0-9a-fA-F]{3,8}\b|(?:rgb|rgba|hsl|hsla|oklch|oklab|lab|lch|hwb)\(/g;
2266
- 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");
2267
- var ALL_RULES = [
2268
- "raw-palette",
2269
- "arbitrary-color",
2270
- "inline-color",
2271
- "daisyui-legacy",
2272
- "interpolated-arbitrary"
2273
- ];
2274
-
2275
- class StyleGuard {
2276
- run(files) {
2277
- const out = [];
2278
- for (const file of files) {
2279
- const directives = this.#parseDirectives(file.content);
2280
- const scan = this.#stripComments(file.content);
2281
- const local = [];
2282
- this.#scanClassLiterals(file, scan, local);
2283
- this.#scanInlineColors(file, scan, local);
2284
- for (const v of local)
2285
- if (!this.#isSuppressed(v, directives))
2286
- out.push(v);
2287
- }
2288
- return out;
2289
- }
2290
- #stripComments(src) {
2291
- const out = src.split("");
2292
- const blank = (i2) => {
2293
- if (src[i2] !== `
2294
- `)
2295
- out[i2] = " ";
2296
- };
2297
- let state = "code";
2298
- let i = 0;
2299
- while (i < src.length) {
2300
- const c = src[i];
2301
- const d = src[i + 1];
2302
- if (state === "code") {
2303
- if (c === "/" && d === "/") {
2304
- blank(i);
2305
- blank(i + 1);
2306
- state = "line";
2307
- i += 2;
2308
- } else if (c === "/" && d === "*") {
2309
- blank(i);
2310
- blank(i + 1);
2311
- state = "block";
2312
- i += 2;
2313
- } else {
2314
- if (c === "'")
2315
- state = "squote";
2316
- else if (c === '"')
2317
- state = "dquote";
2318
- else if (c === "`")
2319
- state = "template";
2320
- i++;
2321
- }
2322
- } else if (state === "line") {
2323
- if (c === `
2324
- `)
2325
- state = "code";
2326
- else
2327
- blank(i);
2328
- i++;
2329
- } else if (state === "block") {
2330
- if (c === "*" && d === "/") {
2331
- blank(i);
2332
- blank(i + 1);
2333
- state = "code";
2334
- i += 2;
2335
- } else {
2336
- blank(i);
2337
- i++;
2338
- }
2339
- } else {
2340
- const close = state === "squote" ? "'" : state === "dquote" ? '"' : "`";
2341
- if (c === "\\")
2342
- i += 2;
2343
- else {
2344
- if (c === close)
2345
- state = "code";
2346
- i++;
2347
- }
2348
- }
2349
- }
2350
- return out.join("");
2351
- }
2352
- #parseDirectives(content) {
2353
- const directives = { file: null, nextLine: new Map };
2354
- const lines = content.split(`
2355
- `);
2356
- for (let i = 0;i < lines.length; i++) {
2357
- const line = lines[i];
2358
- const nextLineMatch = line.match(/styleguard-disable-next-line\b(.*)$/);
2359
- if (nextLineMatch) {
2360
- directives.nextLine.set(i + 2, this.#extractRules(nextLineMatch[1]));
2197
+ static #warnDroppedImports(app, cssRelPath, css, imported) {
2198
+ for (const { cssPath, declaredNames } of imported) {
2199
+ if (declaredNames.length === 0 || declaredNames.some((name) => css.includes(`${name}:`)))
2361
2200
  continue;
2362
- }
2363
- const fileMatch = line.match(/styleguard-disable\b(.*)$/);
2364
- if (fileMatch)
2365
- directives.file = this.#mergeFileScope(directives.file, this.#extractRules(fileMatch[1]));
2366
- }
2367
- return directives;
2368
- }
2369
- #extractRules(tail) {
2370
- const found = ALL_RULES.filter((rule) => tail.includes(rule));
2371
- return found.length === 0 ? "all" : new Set(found);
2372
- }
2373
- #mergeFileScope(prev, next) {
2374
- if (prev === "all" || next === "all")
2375
- return "all";
2376
- return new Set([...prev ?? [], ...next]);
2377
- }
2378
- #isSuppressed(v, directives) {
2379
- if (directives.file === "all" || directives.file?.has(v.rule))
2380
- return true;
2381
- const nextLine = directives.nextLine.get(v.line);
2382
- return nextLine === "all" || (nextLine?.has(v.rule) ?? false);
2383
- }
2384
- #scanClassLiterals(file, scan, out) {
2385
- for (const m of scan.matchAll(RAW_PALETTE_RE)) {
2386
- out.push(this.#violation(file, m.index ?? 0, {
2387
- rule: "raw-palette",
2388
- severity: "error",
2389
- 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."
2390
- }));
2391
- }
2392
- for (const m of scan.matchAll(ARBITRARY_COLOR_RE)) {
2393
- out.push(this.#violation(file, m.index ?? 0, {
2394
- rule: "arbitrary-color",
2395
- severity: "error",
2396
- 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."
2397
- }));
2398
- }
2399
- for (const m of scan.matchAll(DAISYUI_LEGACY_RE)) {
2400
- out.push(this.#violation(file, m.index ?? 0, {
2401
- rule: "daisyui-legacy",
2402
- severity: "error",
2403
- 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."
2404
- }));
2405
- }
2406
- for (const m of scan.matchAll(INTERPOLATED_ARBITRARY_RE)) {
2407
- out.push(this.#violation(file, m.index ?? 0, {
2408
- rule: "interpolated-arbitrary",
2409
- severity: "error",
2410
- 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."
2411
- }));
2412
- }
2413
- }
2414
- #scanInlineColors(file, scan, out) {
2415
- const push = (index) => out.push(this.#violation(file, index, {
2416
- rule: "inline-color",
2417
- severity: "error",
2418
- 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."
2419
- }));
2420
- for (const region of [STYLE_OBJECT_RE, STYLE_TAG_RE]) {
2421
- for (const m of scan.matchAll(region)) {
2422
- const inner = m[1] ?? "";
2423
- const innerStart = (m.index ?? 0) + m[0].indexOf(inner);
2424
- for (const lit of inner.matchAll(INLINE_COLOR_LITERAL_RE)) {
2425
- push(innerStart + (lit.index ?? 0));
2426
- }
2427
- }
2428
- }
2429
- }
2430
- #violation(file, index, base) {
2431
- const before = file.content.slice(0, index);
2432
- const line = before.length === 0 ? 1 : before.split(`
2433
- `).length;
2434
- const lineStart = before.lastIndexOf(`
2435
- `) + 1;
2436
- const lineEnd = file.content.indexOf(`
2437
- `, index);
2438
- const snippet = file.content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd).trim();
2439
- return { ...base, path: file.path, line, snippet };
2440
- }
2441
- static countNamedComponentClasses(css) {
2442
- const names = new Set;
2443
- const layerRe = /@layer\s+components\s*\{/g;
2444
- for (const layer of css.matchAll(layerRe)) {
2445
- const bodyStart = (layer.index ?? 0) + layer[0].length;
2446
- const body = StyleGuard.#extractBalanced(css, bodyStart);
2447
- for (const sel of body.matchAll(/(^|[\s}])\.(-?[a-zA-Z_][\w-]*)/g))
2448
- names.add(sel[2]);
2449
- }
2450
- return { count: names.size, names: [...names].sort() };
2451
- }
2452
- static #extractBalanced(css, start) {
2453
- let depth = 1;
2454
- for (let i = start;i < css.length; i++) {
2455
- const ch = css[i];
2456
- if (ch === "{")
2457
- depth++;
2458
- else if (ch === "}") {
2459
- depth--;
2460
- if (depth === 0)
2461
- return css.slice(start, i);
2462
- }
2201
+ app.logger.warn(`[base-artifact] @import ${cssPath} declares ${declaredNames.length} custom propert${declaredNames.length === 1 ? "y" : "ies"} and none of them are in ${cssRelPath}`);
2463
2202
  }
2464
- return css.slice(start);
2465
2203
  }
2466
2204
  }
2467
- export { AllRoutesBuilder, CsrArtifactBuilder, CssCompiler, FontOptimizer, PagesBundleBuilder, precompressArtifacts, SsrBaseArtifactBuilder };
2205
+ export { AllRoutesBuilder, CssCompiler, PagesBundleBuilder, CsrArtifactBuilder, FontOptimizer, FontPruner, precompressArtifacts, SsrBaseArtifactBuilder };