@akanjs/cli 3.0.0-alpha.1 → 3.0.0-alpha.100

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 (162) 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-byytxs22.js} +3 -5
  5. package/{agent.command-yv00p0s0.js → agent.command-v1a4746v.js} +11 -12
  6. package/{application.command-0qsczf4k.js → application.command-fzdcnzhg.js} +65 -37
  7. package/{applicationBuildRunner-ct2pxyvw.js → applicationBuildRunner-s4gsk728.js} +28 -24
  8. package/{applicationReleasePackager-gg8ccbeh.js → applicationReleasePackager-28v9vbzk.js} +8 -9
  9. package/buildBatch.proc.js +12 -13
  10. package/capacitorApp-8d6d4zkm.js +12 -0
  11. package/{cloud.command-dxfv18sa.js → cloud.command-wygy2z05.js} +15 -17
  12. package/commandManifest.json +1 -1
  13. package/{context.command-8d9e3k6b.js → context.command-4vdt1aqq.js} +19 -22
  14. package/dependencyScanner-shs82ynp.js +8 -0
  15. package/devTui-qnf1txdz.js +553 -0
  16. package/devTuiView-yzct2cq6.js +9 -0
  17. package/getDirname-dr3jz1xg.js +7 -0
  18. package/{guideline.command-3jbkxzh6.js → guideline.command-p92dq6ts.js} +16 -17
  19. package/guidelines/agentRule/agentRule.generate.json +31 -0
  20. package/guidelines/agentRule/agentRule.instruction.md +533 -0
  21. package/guidelines/conventions/conventions.generate.json +25 -0
  22. package/guidelines/conventions/conventions.instruction.md +908 -0
  23. package/guidelines/cssRule/cssRule.instruction.md +7 -0
  24. package/guidelines/framework/framework.generate.json +3 -2
  25. package/guidelines/framework/framework.instruction.md +1 -9
  26. package/guidelines/mcpRule/mcpRule.generate.json +25 -0
  27. package/guidelines/mcpRule/mcpRule.instruction.md +217 -0
  28. package/guidelines/modelStore/modelStore.instruction.md +12 -0
  29. package/guidelines/modelTemplate/modelTemplate.instruction.md +5 -0
  30. package/guidelines/queryRule/queryRule.generate.json +31 -0
  31. package/guidelines/queryRule/queryRule.instruction.md +175 -0
  32. package/guidelines/runtimeRule/runtimeRule.generate.json +25 -0
  33. package/guidelines/runtimeRule/runtimeRule.instruction.md +220 -0
  34. package/guidelines/ssrRule/ssrRule.instruction.md +49 -5
  35. package/guidelines/transportRule/transportRule.generate.json +31 -0
  36. package/guidelines/transportRule/transportRule.instruction.md +60 -0
  37. package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +24 -0
  38. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +263 -0
  39. package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +33 -0
  40. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +402 -0
  41. package/incrementalBuilder.proc.js +26 -24
  42. package/{index-mr5s7mqd.js → index-0amdxycf.js} +8 -8
  43. package/{index-t3723f99.js → index-1yxtbmbj.js} +111 -42
  44. package/{index-ej426mcr.js → index-29sxqfaw.js} +65 -38
  45. package/index-3d9y066c.js +76 -0
  46. package/{index-76rn3g2c.js → index-47pj3aee.js} +4 -4
  47. package/index-5svjba73.js +159 -0
  48. package/index-6wwzs1bc.js +7 -0
  49. package/{index-k95xkh4p.js → index-7d6vny25.js} +16 -12
  50. package/{index-ec09pws3.js → index-8p8p0cf8.js} +14 -14
  51. package/{index-wnp7hwq7.js → index-92zztqy1.js} +7 -7
  52. package/{index-fgc8r6dj.js → index-a5vmm6x4.js} +3 -1
  53. package/{index-xys926f2.js → index-by5gpp8x.js} +4 -4
  54. package/{index-0cj2zxbm.js → index-c69j78kk.js} +12 -11
  55. package/{index-rzwcpcc3.js → index-fc1bh0a7.js} +4 -4
  56. package/{index-7c6k4yvg.js → index-gq44bqjt.js} +58 -25
  57. package/{index-a00jg8x8.js → index-gtmz74tj.js} +79 -55
  58. package/{index-tdbas9r0.js → index-h7yw2z9n.js} +7 -7
  59. package/{index-bjpxzr6s.js → index-hwkygn9s.js} +51 -3
  60. package/{index-bbkek1bp.js → index-hyywke0f.js} +1 -1
  61. package/{index-46tjzh6s.js → index-kcxmv48c.js} +29 -29
  62. package/{index-xjd3rqzj.js → index-n68jx79w.js} +6 -15
  63. package/{index-pyt2b4nw.js → index-p5neav9b.js} +1129 -1356
  64. package/{index-67546d0j.js → index-q0y2jq9j.js} +14 -2
  65. package/{index-bekddz9q.js → index-qkk27bk6.js} +32 -32
  66. package/{index-hgna5rjy.js → index-qsq3q8fy.js} +1034 -257
  67. package/{index-2q1f5j9k.js → index-srdssqj7.js} +18 -18
  68. package/{index-rh16j3c7.js → index-w8kmx1b6.js} +23 -13
  69. package/{index-nm6wghb6.js → index-yks023mb.js} +13 -13
  70. package/{index-1pvex4az.js → index-yza7zkpt.js} +94 -61
  71. package/{index-yzmhasdb.js → index-zmjppp25.js} +57 -57
  72. package/{index-8hsbnyes.js → index-znynzt5w.js} +100 -30
  73. package/{index-zasydjxd.js → index-zxgr3s8s.js} +31 -30
  74. package/{index-e16gnn26.js → index-zz5jyd94.js} +1002 -128
  75. package/index.js +22 -24
  76. package/{library.command-ebdj1vnm.js → library.command-s4fszngs.js} +11 -9
  77. package/{localRegistry.command-gb178ne8.js → localRegistry.command-gyw4da9z.js} +20 -22
  78. package/{module.command-9c1bnsx4.js → module.command-6n7dwvav.js} +13 -15
  79. package/{package.command-t93p4jkj.js → package.command-gr82ypnr.js} +8 -9
  80. package/package.json +5 -7
  81. package/{page.command-k3k0meh2.js → page.command-7gvgb3jb.js} +7 -8
  82. package/{primitive.command-de6nktdt.js → primitive.command-tk5ckw38.js} +13 -15
  83. package/{quality.command-z5kth2wa.js → quality.command-38v81baf.js} +229 -138
  84. package/recipeScanner-fpagtgc1.js +11 -0
  85. package/{repair.command-ehf2g112.js → repair.command-5xbwvsxp.js} +12 -13
  86. package/{routeSourceValidator-cncd92fv.js → routeSourceValidator-ze6x894c.js} +2 -40
  87. package/{scalar.command-7a2jtv33.js → scalar.command-0epwcr5k.js} +11 -13
  88. package/subspace.command-e2t36xb2.js +755 -0
  89. package/templates/app/env/env.client.debug.ts.template +1 -5
  90. package/templates/app/env/env.client.develop.ts.template +1 -5
  91. package/templates/app/env/env.client.local.ts.template +1 -5
  92. package/templates/app/env/env.client.main.ts.template +1 -5
  93. package/templates/app/env/env.client.testing.ts.template +1 -5
  94. package/templates/app/env/env.client.type.ts +2 -6
  95. package/templates/app/env/env.server.debug.ts.template +1 -5
  96. package/templates/app/env/env.server.develop.ts.template +1 -5
  97. package/templates/app/env/env.server.local.ts.template +1 -5
  98. package/templates/app/env/env.server.main.ts.template +1 -5
  99. package/templates/app/main.ts +3 -1
  100. package/templates/app/page/styles.css.template +3 -3
  101. package/templates/appSample/lib/task/task.signal.ts +11 -6
  102. package/templates/appSample/srvkit/AuthGuard.ts +9 -5
  103. package/templates/appSample/srvkit/SessionInternalArg.ts +4 -3
  104. package/templates/crudPages/[__model__Id]/edit/page.tsx +6 -6
  105. package/templates/crudPages/[__model__Id]/page.tsx +10 -13
  106. package/templates/crudPages/new/page.tsx +6 -6
  107. package/templates/crudPages/page.tsx +9 -9
  108. package/templates/crudSinglePage/page.tsx +11 -7
  109. package/templates/env/_env.server.type.ts +7 -5
  110. package/templates/lib/db.ts +4 -2
  111. package/templates/lib/dict.ts +1 -1
  112. package/templates/lib/sig.ts +5 -1
  113. package/templates/lib/srv.ts +6 -2
  114. package/templates/lib/useClient.ts +9 -3
  115. package/templates/module/__Model__.Util.tsx +1 -1
  116. package/templates/module/__Model__.Zone.tsx +3 -5
  117. package/templates/module/__model__.dictionary.ts +4 -1
  118. package/templates/module/__model__.signal.ts +2 -3
  119. package/templates/module/__model__.store.ts +1 -2
  120. package/templates/workspaceRoot/.gitignore.template +0 -1
  121. package/templates/workspaceRoot/.husky/pre-commit.template +16 -0
  122. package/templates/workspaceRoot/.vscode/settings.json.template +1 -1
  123. package/templates/workspaceRoot/AGENTS.md.template +4 -948
  124. package/templates/workspaceRoot/CLAUDE.md.template +19 -0
  125. package/templates/workspaceRoot/biome.json.template +4 -198
  126. package/templates/workspaceRoot/docs/AI-DEVELOPMENT.md.template +15 -16
  127. package/templates/workspaceRoot/docs/GENERATED.md.template +12 -10
  128. package/templates/workspaceRoot/package.json.template +2 -4
  129. package/typeChecker-3xgrzc2v.js +7 -0
  130. package/typecheck.proc.js +4 -5
  131. package/{workflow.command-e0tt0vk2.js → workflow.command-kphbr7eh.js} +21 -23
  132. package/{workspace.command-fpda0evj.js → workspace.command-j1gatz3d.js} +108 -91
  133. package/capacitorApp-4r9sm3qg.js +0 -58
  134. package/dependencyScanner-f6grrep3.js +0 -9
  135. package/index-mxvakhsm.js +0 -66
  136. package/index-r24hmh0q.js +0 -4
  137. package/recipeScanner-tghkzrrt.js +0 -12
  138. package/templates/libRoot/base/baseLogic.ts +0 -14
  139. package/templates/libRoot/base/index.ts +0 -8
  140. package/templates/workspaceRoot/.cursor/rules/akan-page-routing.mdc.template +0 -43
  141. package/templates/workspaceRoot/.cursor/rules/akan-sync-conventions.mdc.template +0 -33
  142. package/templates/workspaceRoot/.cursor/rules/application-test-commands.mdc.template +0 -16
  143. package/templates/workspaceRoot/.cursor/rules/change-scope.mdc.template +0 -12
  144. package/templates/workspaceRoot/.cursor/rules/client-server-boundaries.mdc.template +0 -18
  145. package/templates/workspaceRoot/.cursor/rules/coding-comments.mdc.template +0 -25
  146. package/templates/workspaceRoot/.cursor/rules/coding-style.mdc.template +0 -44
  147. package/templates/workspaceRoot/.cursor/rules/domain-module-conventions.mdc.template +0 -17
  148. package/templates/workspaceRoot/.cursor/rules/format-lint.mdc.template +0 -12
  149. package/templates/workspaceRoot/.cursor/rules/lint-enforced-rules.mdc.template +0 -18
  150. package/templates/workspaceRoot/.cursor/rules/module-file-playbook.mdc.template +0 -61
  151. package/templates/workspaceRoot/.cursor/rules/naming-conventions.mdc.template +0 -15
  152. package/templates/workspaceRoot/.cursor/rules/react-component-style.mdc.template +0 -21
  153. package/templates/workspaceRoot/.cursor/rules/repo-overview.mdc.template +0 -17
  154. package/templates/workspaceRoot/.cursor/rules/scalar-modeling.mdc.template +0 -58
  155. package/templates/workspaceRoot/.cursor/rules/secrets-and-env.mdc.template +0 -13
  156. package/templates/workspaceRoot/.cursor/rules/service-signal-conventions.mdc.template +0 -83
  157. package/templates/workspaceRoot/.cursor/rules/srvkit-adapters-and-guards.mdc.template +0 -50
  158. package/templates/workspaceRoot/.cursor/rules/ssr-first.mdc.template +0 -59
  159. package/templates/workspaceRoot/.cursor/rules/typescript-imports.mdc.template +0 -16
  160. package/typeChecker-kravn7ns.js +0 -8
  161. /package/{index-0wae5ebk.js → index-meh558jy.js} +0 -0
  162. /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-qsq3q8fy.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-rh16j3c7.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
 
@@ -415,17 +164,27 @@ class AllRoutesBuilder {
415
164
  const seedIndex = computeRouteSeedIndex(pageEntries);
416
165
  this.#app.verbose(`[build-all] discovered ${seedIndex.entries.length} routes`);
417
166
  this.#discovery = await GraphClientEntryDiscovery.create(this.#app);
167
+ const allEntries = [];
168
+ const seen = new Set;
418
169
  for (const entry of seedIndex.entries) {
419
170
  const seeds = Array.from(new Set([...seedIndex.globalLayoutFiles, ...entry.seeds]));
420
- const delta = await this.#buildRoute(entry.routeId, seeds);
421
- this.#mergeRoute(entry.routeId, delta);
171
+ for (const discovered of await this.#discovery.discover(seeds)) {
172
+ if (seen.has(discovered))
173
+ continue;
174
+ seen.add(discovered);
175
+ allEntries.push(discovered);
176
+ }
177
+ this.#routeIds.push(entry.routeId);
422
178
  }
179
+ this.#app.verbose(`[build-all] ${allEntries.length} client entries across ${this.#routeIds.length} routes`);
180
+ const delta = await this.#buildEntries(allEntries);
181
+ this.#mergeDelta(delta);
423
182
  this.#merged.knownEntries = Array.from(this.#knownSet);
424
183
  const manifest = {
425
184
  routeIds: this.#routeIds,
426
185
  ...this.#merged
427
186
  };
428
- const manifestPath = path4.join(path4.resolve(this.#artifactDir), "routes-manifest.json");
187
+ const manifestPath = path3.join(path3.resolve(this.#artifactDir), "routes-manifest.json");
429
188
  await Bun.write(manifestPath, `${JSON.stringify(RoutesManifestArtifactSerializer.serialize(manifest, this.#artifactDir, {
430
189
  production: this.#command === "build"
431
190
  }), null, 2)}
@@ -433,23 +192,30 @@ class AllRoutesBuilder {
433
192
  this.#app.verbose(`[build-all] wrote ${manifestPath} routes=${this.#routeIds.length} entries=${this.#merged.knownEntries.length}`);
434
193
  return { manifest, manifestPath, seedIndex };
435
194
  }
436
- async#buildRoute(routeId, seeds) {
195
+ async#buildEntries(entries) {
437
196
  if (!this.#discovery)
438
197
  throw new Error("[build-all] client entry discovery is not initialized");
198
+ if (entries.length === 0)
199
+ return {
200
+ manifestDelta: {},
201
+ ssrManifestDelta: { moduleLoading: null, moduleMap: {} },
202
+ newEntries: [],
203
+ clientDeps: []
204
+ };
439
205
  const started = Date.now();
440
206
  const delta = await new RouteClientBuilder({
441
207
  app: this.#app,
442
- routeId,
443
- seeds,
208
+ seeds: [],
209
+ entries,
444
210
  artifact: this.#artifact,
445
211
  knownEntries: this.#knownSet,
446
212
  discovery: this.#discovery,
447
213
  command: this.#command
448
214
  }).build();
449
- this.#app.verbose(`[build-all] ${routeId} +${delta.newEntries.length} entries (${Date.now() - started}ms)`);
215
+ this.#app.verbose(`[build-all] bundled ${delta.newEntries.length} entries (${Date.now() - started}ms)`);
450
216
  return delta;
451
217
  }
452
- #mergeRoute(routeId, delta) {
218
+ #mergeDelta(delta) {
453
219
  for (const [key, row] of Object.entries(delta.manifestDelta))
454
220
  this.#merged.clientManifest[key] = row;
455
221
  for (const [url, byName] of Object.entries(delta.ssrManifestDelta.moduleMap)) {
@@ -457,436 +223,109 @@ class AllRoutesBuilder {
457
223
  }
458
224
  for (const abs of delta.newEntries)
459
225
  this.#knownSet.add(abs);
460
- this.#routeIds.push(routeId);
461
226
  }
462
227
  }
463
228
 
464
- // pkgs/@akanjs/devkit/frontendBuild/csrArtifactBuilder.ts
465
- import { mkdir as mkdir2, rm, unlink } from "fs/promises";
466
- import path6 from "path";
467
-
468
- // pkgs/@akanjs/devkit/frontendBuild/pagesEntrySourceGenerator.ts
469
- import fs from "fs";
229
+ // pkgs/@akanjs/devkit/frontendBuild/cssCompiler.ts
470
230
  import path5 from "path";
471
- import ts from "typescript";
231
+ import { Logger } from "akanjs/common";
232
+ import { compile } from "tailwindcss";
472
233
 
473
- class PagesEntrySourceGenerator {
474
- #pageEntries;
475
- constructor(pageEntries) {
476
- this.#pageEntries = pageEntries;
477
- }
478
- static generate(pageEntries) {
479
- return new PagesEntrySourceGenerator(pageEntries).generate();
480
- }
481
- generate() {
482
- const lines = this.#pageEntries.map(({ key, moduleAbsPath }) => {
483
- const specifier = PagesEntrySourceGenerator.#toImportSpecifier(moduleAbsPath);
484
- return ` ${JSON.stringify(key)}: () => import(${JSON.stringify(specifier)}),`;
485
- });
486
- return `export const pages = {
487
- ${lines.join(`
488
- `)}
489
- };
490
- `;
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;
491
247
  }
492
- static generateStatic(pageEntries) {
493
- return new PagesEntrySourceGenerator(pageEntries).generateStatic();
248
+ get reused() {
249
+ return this.#reused;
494
250
  }
495
- generateStatic() {
496
- const imports = this.#pageEntries.map(({ moduleAbsPath }, index) => {
497
- const specifier = PagesEntrySourceGenerator.#toImportSpecifier(moduleAbsPath);
498
- return `import * as page${index} from ${JSON.stringify(specifier)};`;
499
- });
500
- const entries = this.#pageEntries.map(({ key, moduleAbsPath }, index) => {
501
- const isAsyncDefault = PagesEntrySourceGenerator.#hasAsyncDefaultExport(moduleAbsPath);
502
- return ` ${JSON.stringify(key)}: { loader: async () => page${index}, isAsyncDefault: ${isAsyncDefault} },`;
503
- });
504
- return `${imports.join(`
505
- `)}
506
- export const pages = {
507
- ${entries.join(`
508
- `)}
509
- };
510
- `;
251
+ get rescanned() {
252
+ return this.#rescanned;
511
253
  }
512
- static #toImportSpecifier(moduleAbsPath) {
513
- 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;
514
264
  }
515
- static #hasAsyncDefaultExport(moduleAbsPath) {
516
- try {
517
- const source = fs.readFileSync(path5.resolve(moduleAbsPath), "utf8");
518
- const sourceFile = ts.createSourceFile(moduleAbsPath, source, ts.ScriptTarget.Latest, true, PagesEntrySourceGenerator.#scriptKind(moduleAbsPath));
519
- return PagesEntrySourceGenerator.#sourceFileHasAsyncDefaultExport(sourceFile);
520
- } catch {
521
- 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;
522
278
  }
279
+ return candidates;
523
280
  }
524
- static #sourceFileHasAsyncDefaultExport(sourceFile) {
525
- const asyncBindings = new Map;
526
- let defaultIdentifier = null;
527
- for (const statement of sourceFile.statements) {
528
- if (ts.isFunctionDeclaration(statement)) {
529
- if (PagesEntrySourceGenerator.#hasModifier(statement, ts.SyntaxKind.DefaultKeyword)) {
530
- return PagesEntrySourceGenerator.#hasModifier(statement, ts.SyntaxKind.AsyncKeyword);
531
- }
532
- if (statement.name) {
533
- asyncBindings.set(statement.name.text, PagesEntrySourceGenerator.#hasModifier(statement, ts.SyntaxKind.AsyncKeyword));
534
- }
535
- continue;
536
- }
537
- if (ts.isVariableStatement(statement)) {
538
- for (const declaration of statement.declarationList.declarations) {
539
- if (!ts.isIdentifier(declaration.name))
540
- continue;
541
- asyncBindings.set(declaration.name.text, PagesEntrySourceGenerator.#isAsyncFunctionExpression(declaration.initializer));
542
- }
543
- continue;
544
- }
545
- if (ts.isExportAssignment(statement)) {
546
- if (PagesEntrySourceGenerator.#isAsyncFunctionExpression(statement.expression))
547
- return true;
548
- if (ts.isIdentifier(statement.expression))
549
- defaultIdentifier = statement.expression.text;
281
+ async save(present) {
282
+ for (const file of [...this.#entries.keys()]) {
283
+ if (present.has(file))
550
284
  continue;
551
- }
552
- if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
553
- const exportClause = statement.exportClause;
554
- for (const specifier of exportClause.elements) {
555
- if (specifier.name.text !== "default")
556
- continue;
557
- defaultIdentifier = specifier.propertyName?.text ?? specifier.name.text;
558
- }
559
- }
285
+ this.#entries.delete(file);
286
+ this.#dirty = true;
560
287
  }
561
- return defaultIdentifier ? asyncBindings.get(defaultIdentifier) === true : false;
562
- }
563
- static #hasModifier(node, kind) {
564
- return ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some((modifier) => modifier.kind === kind) ?? false);
565
- }
566
- static #isAsyncFunctionExpression(node) {
567
- return Boolean(node && (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && PagesEntrySourceGenerator.#hasModifier(node, ts.SyntaxKind.AsyncKeyword));
568
- }
569
- static #scriptKind(moduleAbsPath) {
570
- 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
+ });
571
297
  }
572
298
  }
573
299
 
574
- // pkgs/@akanjs/devkit/frontendBuild/csrArtifactBuilder.ts
575
- class CsrArtifactBuilder {
576
- #app;
577
- #command;
578
- #lang;
579
- constructor(app, command = "start", lang = "en") {
580
- this.#app = app;
581
- this.#command = command;
582
- 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);
583
312
  }
584
- async build() {
585
- const pageKeys = await this.#app.getPageKeys();
586
- if (pageKeys.length === 0) {
587
- this.#app.log(`[cli] no route files under ${this.#app.cwdPath}/page \u2014 skipping CSR build`);
588
- return null;
589
- }
590
- const pageEntries = await resolveSsrPageEntriesForApp(this.#app, pageKeys);
591
- const akanConfig = await this.#app.getConfig();
592
- const artifact = await this.#loadCsrArtifact();
593
- const csrBasePaths = [...akanConfig.basePaths];
594
- const htmlEntries = csrBasePaths.length > 0 ? csrBasePaths : ["index"];
595
- await rm(this.#outputDir, { recursive: true, force: true });
596
- await mkdir2(path6.join(this.#app.cwdPath, ".akan/generated/csr"), { recursive: true });
597
- const generatedHtmlFiles = Object.fromEntries(htmlEntries.map((basePath) => this.#createHtmlFile(basePath)));
598
- const result = await Bun.build({
599
- target: "browser",
600
- entrypoints: Object.keys(generatedHtmlFiles),
601
- files: {
602
- ...generatedHtmlFiles,
603
- [`${this.#app.cwdPath}/.akan/generated/csr/csr.tsx`]: `
604
- import { bootCsr } from "akanjs/webkit";
605
- ${PagesEntrySourceGenerator.generateStatic(pageEntries)}
606
- void bootCsr(pages);
607
- `
608
- },
609
- root: `${this.#app.cwdPath}/.akan/generated/csr`,
610
- outdir: this.#outputDir,
611
- splitting: false,
612
- minify: true,
613
- env: "AKAN_PUBLIC_*",
614
- define: this.#define(),
615
- optimizeImports: akanConfig.optimizeImports
616
- });
617
- if (!result.success) {
618
- const logs = result.logs.map((log) => log.message).join(`
619
- `);
620
- throw new Error(`[csr-build] failed${logs ? `
621
- ${logs}` : ""}`);
622
- }
623
- await this.#inlineCsrArtifacts(artifact.cssAssets ?? {});
624
- this.#app.verbose(`[csr-build] output -> ${this.#outputDir}`);
625
- return { outputDir: this.#outputDir };
313
+ static async create(app) {
314
+ const tsconfig = await app.getTsConfig();
315
+ return new CssImportResolver(app.workspace.workspaceRoot, tsconfig.compilerOptions.paths ?? {});
626
316
  }
627
- get #outputDir() {
628
- return path6.join(this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath, this.#command === "build" ? "csr" : ".akan/artifact/csr");
629
- }
630
- #define() {
631
- const nodeEnv = this.#command === "build" ? "production" : "development";
632
- return {
633
- "process.env.NODE_ENV": JSON.stringify(nodeEnv),
634
- "process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("csr"),
635
- ...Object.fromEntries(Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]))
636
- };
637
- }
638
- #createHtmlFile(basePath) {
639
- const filename = `${basePath}.html`;
640
- return [
641
- `${this.#app.cwdPath}/.akan/generated/csr/${filename}`,
642
- `<!doctype html>
643
- <html lang="${this.#lang}">
644
- <head>
645
- <meta charset="utf-8" />
646
- <title>${this.#app.name}</title>
647
- <base href="/" />
648
- <meta name="viewport" content="width=device-width, initial-scale=1" />
649
- </head>
650
- <body>
651
- <div id="root"></div>
652
- <script type="module" src="./csr.tsx"></script>
653
- </body>
654
- </html>
655
- `
656
- ];
657
- }
658
- async#loadCsrArtifact() {
659
- const artifactDir = path6.join(this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath, ".akan/artifact");
660
- const artifactFile = Bun.file(path6.join(artifactDir, "base-artifact.json"));
661
- if (!await artifactFile.exists())
662
- return { cssAssets: {} };
663
- const artifact = await artifactFile.json();
664
- return { cssAssets: artifact.cssAssets ?? {} };
665
- }
666
- async#inlineCsrArtifacts(cssAssets) {
667
- const jsFiles = new Set;
668
- const cssFiles = new Set;
669
- for (const htmlPath of await this.#htmlOutputPaths()) {
670
- const htmlFile = Bun.file(htmlPath);
671
- if (!await htmlFile.exists())
672
- continue;
673
- const basePath = path6.basename(htmlPath, ".html") === "index" ? "" : path6.basename(htmlPath, ".html");
674
- const inlined = await this.#inlineHtmlAssets(await htmlFile.text(), htmlPath, cssAssets[basePath]);
675
- for (const filePath of inlined.jsFiles)
676
- jsFiles.add(filePath);
677
- for (const filePath of inlined.cssFiles)
678
- cssFiles.add(filePath);
679
- await Bun.write(htmlPath, inlined.html);
680
- }
681
- for (const filePath of jsFiles)
682
- await unlink(filePath).catch(() => {
683
- return;
684
- });
685
- for (const filePath of cssFiles)
686
- await unlink(filePath).catch(() => {
687
- return;
688
- });
689
- const remainingJs = await this.#listOutputFiles((filePath) => filePath.endsWith(".js"));
690
- const remainingCss = await this.#listOutputFiles((filePath) => filePath.endsWith(".css"));
691
- const remainingAssets = [...remainingJs, ...remainingCss];
692
- if (remainingAssets.length > 0) {
693
- throw new Error(`[csr-build] expected single-file HTML, but CSR assets remain:
694
- ${remainingAssets.join(`
695
- `)}`);
696
- }
697
- }
698
- async#inlineHtmlAssets(html, htmlPath, cssAsset) {
699
- const jsFiles = [];
700
- const cssFiles = CsrArtifactBuilder.collectStylesheetHrefs(html).map((href) => CsrArtifactBuilder.resolveHtmlAssetPath(htmlPath, href));
701
- let next = CsrArtifactBuilder.stripBundledStylesheetLinks(html);
702
- next = await CsrArtifactBuilder.replaceModuleScriptSrc(next, async (src) => {
703
- const jsPath = CsrArtifactBuilder.resolveHtmlAssetPath(htmlPath, src);
704
- jsFiles.push(jsPath);
705
- return await Bun.file(jsPath).text();
706
- });
707
- const bundledCss = (await Promise.all(cssFiles.map((cssFile) => Bun.file(cssFile).text().catch(() => "")))).filter(Boolean).join(`
708
- `);
709
- if (bundledCss) {
710
- const style = CsrArtifactBuilder.createInlineStyle(bundledCss);
711
- if (!next.includes(style))
712
- next = CsrArtifactBuilder.injectBeforeHeadEnd(next, style);
713
- }
714
- if (cssAsset) {
715
- const cssPath = path6.join(this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath, ".akan/artifact", cssAsset.cssRelPath);
716
- const css = await Bun.file(cssPath).text();
717
- const style = CsrArtifactBuilder.createInlineStyle(css);
718
- if (!next.includes(style))
719
- next = CsrArtifactBuilder.injectBeforeHeadEnd(next, style);
720
- }
721
- return { html: next, jsFiles, cssFiles };
722
- }
723
- async#htmlOutputPaths() {
724
- return await this.#listOutputFiles((filePath) => filePath.endsWith(".html"));
725
- }
726
- async#listOutputFiles(predicate) {
727
- const glob = new Bun.Glob("**/*");
728
- const files = [];
729
- for await (const filePath of glob.scan({ cwd: this.#outputDir, absolute: true })) {
730
- if (predicate(filePath))
731
- files.push(filePath);
732
- }
733
- return files.sort();
734
- }
735
- static injectBeforeHeadEnd(html, snippet) {
736
- const matches = [...html.matchAll(/<\/head\s*>/gi)];
737
- const bodyStart = html.search(/<body(?:\s|>)/i);
738
- const headEnd = matches.filter((match) => match.index !== undefined && (bodyStart === -1 || match.index < bodyStart)).at(-1);
739
- if (!headEnd || headEnd.index === undefined)
740
- return `${snippet}
741
- ${html}`;
742
- return `${html.slice(0, headEnd.index)}${snippet}
743
- ${html.slice(headEnd.index)}`;
744
- }
745
- static stripBundledStylesheetLinks(html) {
746
- return html.replace(/<link\b(?=[^>]*\brel=["']stylesheet["'])[^>]*>\s*/gi, "");
747
- }
748
- static collectStylesheetHrefs(html) {
749
- const linkRe = /<link\b(?=[^>]*\brel=["']stylesheet["'])(?=[^>]*\bhref=["']([^"']+)["'])[^>]*>/gi;
750
- return [...html.matchAll(linkRe)].map((match) => match[1]).filter((href) => !!href);
751
- }
752
- static createInlineStyle(css) {
753
- return `<style data-akan-css="active">
754
- ${css.replace(/<\/style/gi, "<\\/style")}
755
- </style>`;
756
- }
757
- static async replaceModuleScriptSrc(html, loadScript) {
758
- const scriptRe = /<script\b(?=[^>]*\btype=["']module["'])(?=[^>]*\bsrc=["']([^"']+)["'])[^>]*>\s*<\/script>/gi;
759
- let result = "";
760
- let lastIndex = 0;
761
- let matched = false;
762
- for (const match of html.matchAll(scriptRe)) {
763
- const full = match[0];
764
- const src = match[1];
765
- if (match.index === undefined || !src)
766
- continue;
767
- matched = true;
768
- result += html.slice(lastIndex, match.index);
769
- result += `<script type="module">
770
- ${CsrArtifactBuilder.escapeInlineScript(await loadScript(src))}
771
- </script>`;
772
- lastIndex = match.index + full.length;
773
- }
774
- if (!matched)
775
- return html;
776
- return result + html.slice(lastIndex);
777
- }
778
- static escapeInlineScript(source) {
779
- return source.replace(/<\/script/gi, "<\\/script");
780
- }
781
- static resolveHtmlAssetPath(htmlPath, src) {
782
- if (/^[a-z][a-z0-9+.-]*:/i.test(src) || src.startsWith("//")) {
783
- throw new Error(`[csr-build] cannot inline external script: ${src}`);
784
- }
785
- const normalized = src.startsWith("/") ? src.slice(1) : src;
786
- return path6.resolve(path6.dirname(htmlPath), normalized);
787
- }
788
- }
789
-
790
- // pkgs/@akanjs/devkit/frontendBuild/cssCompiler.ts
791
- import path8 from "path";
792
- import { Logger } from "akanjs/common";
793
- import { compile } from "tailwindcss";
794
-
795
- // pkgs/@akanjs/devkit/frontendBuild/cssCandidateCache.ts
796
- import { stat } from "fs/promises";
797
- var CANDIDATE_RE = /-?[\w@][\w:/.-]*(?:\[[^\]]+\][\w:/.-]*)*/g;
798
-
799
- class CssCandidateCache {
800
- static #version = 1;
801
- #path;
802
- #entries = new Map;
803
- #dirty = false;
804
- #reused = 0;
805
- #rescanned = 0;
806
- constructor(cachePath) {
807
- this.#path = cachePath;
808
- }
809
- get reused() {
810
- return this.#reused;
811
- }
812
- get rescanned() {
813
- return this.#rescanned;
814
- }
815
- async load() {
816
- const raw = await Bun.file(this.#path).json().catch(() => null);
817
- if (!raw || raw.version !== CssCandidateCache.#version || typeof raw.files !== "object")
818
- return this;
819
- for (const [file, entry] of Object.entries(raw.files)) {
820
- if (typeof entry?.mtimeMs !== "number" || !Array.isArray(entry.candidates))
821
- continue;
822
- this.#entries.set(file, entry);
823
- }
824
- return this;
825
- }
826
- async candidatesFor(file) {
827
- const stats = await stat(file).catch(() => null);
828
- const cached = this.#entries.get(file);
829
- if (stats && cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
830
- this.#reused += 1;
831
- return cached.candidates;
832
- }
833
- const content = await Bun.file(file).text();
834
- const candidates = [...new Set(Array.from(content.matchAll(CANDIDATE_RE), (m) => m[0]))];
835
- this.#rescanned += 1;
836
- if (stats) {
837
- this.#entries.set(file, { mtimeMs: stats.mtimeMs, size: stats.size, candidates });
838
- this.#dirty = true;
839
- }
840
- return candidates;
841
- }
842
- async save(present) {
843
- for (const file of [...this.#entries.keys()]) {
844
- if (present.has(file))
845
- continue;
846
- this.#entries.delete(file);
847
- this.#dirty = true;
848
- }
849
- if (!this.#dirty)
850
- return;
851
- this.#dirty = false;
852
- const files = {};
853
- for (const [file, entry] of this.#entries)
854
- files[file] = entry;
855
- await Bun.write(this.#path, JSON.stringify({ version: CssCandidateCache.#version, files })).catch(() => {
856
- return;
857
- });
858
- }
859
- }
860
-
861
- // pkgs/@akanjs/devkit/frontendBuild/cssImportResolver.ts
862
- import path7 from "path";
863
- var CSS_IMPORT_EXTS = ["", ".css", "/styles.css", "/index.css"];
864
-
865
- class CssImportResolver {
866
- #workspaceRoot;
867
- #paths;
868
- #wildcardEntries;
869
- constructor(workspaceRoot, paths = {}) {
870
- this.#workspaceRoot = workspaceRoot;
871
- this.#paths = paths;
872
- 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);
873
- }
874
- static async create(app) {
875
- const tsconfig = await app.getTsConfig();
876
- return new CssImportResolver(app.workspace.workspaceRoot, tsconfig.compilerOptions.paths ?? {});
877
- }
878
- async resolve(id, fromBase) {
879
- for (const resolve of [
880
- () => this.#resolveWithTsconfig(id),
881
- () => this.#resolveWithBun(id, fromBase),
882
- () => this.#resolveWithRequire(id, fromBase),
883
- () => this.#resolvePackageStyle(id, fromBase)
884
- ]) {
885
- const resolved = await resolve();
886
- if (resolved)
887
- return resolved;
888
- }
889
- return null;
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;
327
+ }
328
+ return null;
890
329
  }
891
330
  #resolveWithBun(id, fromBase) {
892
331
  for (const base of this.#resolutionBases(fromBase)) {
@@ -912,7 +351,7 @@ class CssImportResolver {
912
351
  const exact = this.#paths[id];
913
352
  if (exact) {
914
353
  for (const repl of exact) {
915
- const resolved = await this.#firstExisting(path7.resolve(this.#workspaceRoot, repl));
354
+ const resolved = await this.#firstExisting(path4.resolve(this.#workspaceRoot, repl));
916
355
  if (resolved)
917
356
  return resolved;
918
357
  }
@@ -923,7 +362,7 @@ class CssImportResolver {
923
362
  const suffix = id.slice(prefix.length);
924
363
  for (const repl of replacements) {
925
364
  const replPath = repl.endsWith("/*") ? repl.slice(0, -1) : repl;
926
- const resolved = await this.#firstExisting(path7.resolve(this.#workspaceRoot, replPath + suffix));
365
+ const resolved = await this.#firstExisting(path4.resolve(this.#workspaceRoot, replPath + suffix));
927
366
  if (resolved)
928
367
  return resolved;
929
368
  }
@@ -953,25 +392,29 @@ class CssImportResolver {
953
392
  try {
954
393
  if (!await Bun.file(pkgPath).exists())
955
394
  return null;
956
- const pkgDir = path7.dirname(pkgPath);
395
+ const pkgDir = path4.dirname(pkgPath);
957
396
  const pkg = await Bun.file(pkgPath).json();
958
397
  const subpath = id === pkgName ? "." : `.${id.slice(pkgName.length)}`;
959
398
  const exportValue = pkg.exports?.[subpath];
960
- const styleEntry = (typeof exportValue === "string" ? exportValue : exportValue?.style || exportValue?.import || exportValue?.default) || pkg.exports?.["."]?.style || pkg.style || "index.css";
961
- 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"));
962
405
  } catch {
963
406
  return null;
964
407
  }
965
408
  }
966
409
  #resolutionBases(fromBase) {
967
- 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), "../..")];
968
411
  }
969
412
  #packageJsonCandidates(pkgName) {
970
413
  return [
971
- path7.join(this.#workspaceRoot, "pkgs", pkgName, "package.json"),
972
- path7.join(this.#workspaceRoot, "node_modules", pkgName, "package.json"),
973
- path7.join(path7.dirname(Bun.main), "node_modules", pkgName, "package.json"),
974
- 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")
975
418
  ];
976
419
  }
977
420
  async#firstExisting(basePath) {
@@ -989,7 +432,7 @@ class CssImportResolver {
989
432
  return parts[0] ?? null;
990
433
  }
991
434
  static isCssFile(filePath) {
992
- return path7.extname(filePath) === ".css";
435
+ return path4.extname(filePath) === ".css";
993
436
  }
994
437
  }
995
438
 
@@ -1008,13 +451,15 @@ class CssCompiler {
1008
451
  this.#app = app;
1009
452
  }
1010
453
  get #candidateCachePath() {
1011
- return path8.join(this.#app.cwdPath, ".akan/cache/cssCandidates.json");
454
+ return path5.join(this.#app.cwdPath, ".akan/cache/cssCandidates.json");
1012
455
  }
1013
456
  #cssText = null;
1014
457
  #cssTextByBasePath = null;
1015
458
  #fileExistsCache = new Map;
1016
459
  #resolvedFileCache = new Map;
1017
460
  #resolvedSpecifierCache = new Map;
461
+ #discoveredCssPaths = new Set;
462
+ importedStylesheetsByBasePath = {};
1018
463
  #fileExists(absPath) {
1019
464
  let cached = this.#fileExistsCache.get(absPath);
1020
465
  if (!cached) {
@@ -1036,7 +481,7 @@ class CssCompiler {
1036
481
  return filePath;
1037
482
  }
1038
483
  for (const ext of SOURCE_EXTS) {
1039
- const filePath = path8.join(absPathNoExt, `index${ext}`);
484
+ const filePath = path5.join(absPathNoExt, `index${ext}`);
1040
485
  if (await this.#fileExists(filePath))
1041
486
  return filePath;
1042
487
  }
@@ -1048,13 +493,20 @@ class CssCompiler {
1048
493
  async getCss({ refresh } = {}) {
1049
494
  if (this.#cssText !== null && !refresh)
1050
495
  return this.#cssText;
496
+ this.#discoveredCssPaths.clear();
497
+ this.importedStylesheetsByBasePath = {};
1051
498
  const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh });
1052
- 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();
1053
503
  return this.#cssText;
1054
504
  }
1055
505
  async getCssByBasePath({ refresh } = {}) {
1056
506
  if (this.#cssTextByBasePath !== null && !refresh)
1057
507
  return this.#cssTextByBasePath;
508
+ this.#discoveredCssPaths.clear();
509
+ this.importedStylesheetsByBasePath = {};
1058
510
  const akanConfig = await this.#app.getConfig({ refresh });
1059
511
  const pageKeys = await this.#app.getPageKeys({ refresh });
1060
512
  const basePaths = [...akanConfig.basePaths];
@@ -1065,7 +517,8 @@ class CssCompiler {
1065
517
  return ["", ""];
1066
518
  const started = Date.now();
1067
519
  const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: rootPageKeys });
1068
- const css = await this.compileCss(cssPaths, sourcePaths);
520
+ const { css, imported } = await this.#compileWithImports(cssPaths, sourcePaths);
521
+ this.importedStylesheetsByBasePath[""] = imported;
1069
522
  this.#logger.verbose(`css base=root paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`);
1070
523
  return ["", css];
1071
524
  })(),
@@ -1075,14 +528,27 @@ class CssCompiler {
1075
528
  return [basePath, ""];
1076
529
  const started = Date.now();
1077
530
  const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: basePathPageKeys });
1078
- const css = await this.compileCss(cssPaths, sourcePaths);
531
+ const { css, imported } = await this.#compileWithImports(cssPaths, sourcePaths);
532
+ this.importedStylesheetsByBasePath[basePath] = imported;
1079
533
  this.#logger.verbose(`css base=${basePath} paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`);
1080
534
  return [basePath, css];
1081
535
  })
1082
536
  ]);
1083
537
  this.#cssTextByBasePath = Object.fromEntries(cssEntries);
538
+ await this.#warnUnreachableStylesheets();
1084
539
  return this.#cssTextByBasePath;
1085
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
+ }
1086
552
  async discoverCss({ refresh } = {}) {
1087
553
  const { cssPaths } = await this.discoverCssAndSources({ refresh });
1088
554
  return cssPaths;
@@ -1092,7 +558,7 @@ class CssCompiler {
1092
558
  pageKeys
1093
559
  } = {}) {
1094
560
  pageKeys ??= await this.#app.getPageKeys({ refresh });
1095
- 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));
1096
562
  const cssFiles = new Set;
1097
563
  const sourceFiles = new Set;
1098
564
  const queue = [...seeds];
@@ -1124,7 +590,7 @@ class CssCompiler {
1124
590
  } catch {
1125
591
  continue;
1126
592
  }
1127
- const importerDir = path8.dirname(filePath);
593
+ const importerDir = path5.dirname(filePath);
1128
594
  for (const imp of imports) {
1129
595
  const spec = imp.path;
1130
596
  if (!spec)
@@ -1142,21 +608,47 @@ class CssCompiler {
1142
608
  queue.push(resolved);
1143
609
  }
1144
610
  }
1145
- 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);
1146
633
  }
1147
634
  async compileCss(cssPaths, sourcePaths) {
635
+ const { css } = await this.#compileWithImports(cssPaths, sourcePaths);
636
+ return css;
637
+ }
638
+ async#compileWithImports(cssPaths, sourcePaths) {
1148
639
  if (cssPaths.length === 0)
1149
- return "";
640
+ return { css: "", imported: [] };
1150
641
  const compileStarted = Date.now();
1151
642
  const compilers = await Promise.all(cssPaths.map(async (cssPath) => {
1152
643
  const css = await Bun.file(cssPath).text();
1153
- const base = path8.dirname(cssPath);
644
+ const base = path5.dirname(cssPath);
645
+ const imported = new Map;
1154
646
  const compiler = await compile(css, {
1155
647
  base,
1156
- loadStylesheet: (id, fromBase) => this.#loadStylesheet(id, fromBase),
648
+ loadStylesheet: (id, fromBase) => this.#loadStylesheet(id, fromBase, imported),
1157
649
  loadModule: (id, fromBase) => this.#loadModule(id, fromBase)
1158
650
  });
1159
- return { cssPath, compiler };
651
+ return { cssPath, compiler, imported };
1160
652
  }));
1161
653
  const sourceDirs = new Set;
1162
654
  for (const entry of compilers) {
@@ -1169,23 +661,39 @@ class CssCompiler {
1169
661
  const candidates = await this.#scanCandidates(sourcePaths, [...sourceDirs]);
1170
662
  this.#logger.verbose(`css candidates scanned count=${candidates.length} sources=${sourcePaths.length} dirs=${sourceDirs.size} in ${Date.now() - scanStarted}ms`);
1171
663
  const parts = [];
664
+ const imported = [];
1172
665
  for (const entry of compilers) {
1173
666
  if (!entry)
1174
667
  continue;
1175
- 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
+ }
1176
677
  }
1177
678
  this.#logger.verbose(`css compiled paths=${cssPaths.length} candidates=${candidates.length} in ${Date.now() - compileStarted}ms`);
1178
- return parts.join(`
1179
- `);
679
+ return { css: parts.join(`
680
+ `), imported };
1180
681
  }
1181
- async#loadStylesheet(id, fromBase) {
682
+ async#loadStylesheet(id, fromBase, imported) {
1182
683
  const p = await this.#resolveCssImport(id, fromBase);
684
+ this.#discoveredCssPaths.add(p);
1183
685
  const content = await Bun.file(p).text();
1184
- 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 };
1185
689
  }
1186
690
  async#resolveCssImport(id, fromBase) {
1187
- if (id.startsWith(".") || id.startsWith("/"))
1188
- 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
+ }
1189
697
  const resolver = await this.#getCssImportResolver();
1190
698
  const resolved = await resolver.resolve(id, fromBase);
1191
699
  if (resolved)
@@ -1198,89 +706,654 @@ class CssCompiler {
1198
706
  this.#cssImportResolver = await CssImportResolver.create(this.#app);
1199
707
  return this.#cssImportResolver;
1200
708
  }
1201
- async#loadModule(id, fromBase) {
1202
- const p = __require.resolve(id, { paths: [fromBase] });
1203
- const mod = await import(p);
1204
- return { path: p, base: path8.dirname(p), module: mod.default ?? mod };
709
+ async#loadModule(id, fromBase) {
710
+ const p = __require.resolve(id, { paths: [fromBase] });
711
+ const mod = await import(p);
712
+ return { path: p, base: path5.dirname(p), module: mod.default ?? mod };
713
+ }
714
+ #resolveSourceImport(id, fromBase, resolvePackage) {
715
+ const cacheKey = `${fromBase}\x00${id}`;
716
+ let cached = this.#resolvedSpecifierCache.get(cacheKey);
717
+ if (cached)
718
+ return cached;
719
+ cached = this.#resolveSourceImportUncached(id, fromBase, resolvePackage);
720
+ this.#resolvedSpecifierCache.set(cacheKey, cached);
721
+ return cached;
722
+ }
723
+ async#resolveSourceImportUncached(id, fromBase, resolvePackage) {
724
+ if (id.startsWith(".") || id.startsWith("/")) {
725
+ const abs = id.startsWith("/") ? id : path5.resolve(fromBase, id);
726
+ return this.#resolveSourceFileCandidate(abs);
727
+ }
728
+ const pkg = await resolvePackage(id);
729
+ if (pkg)
730
+ return pkg.entryFile;
731
+ for (const resolve of [() => resolveSourceWithBun(id, fromBase), () => resolveSourceWithRequire(id, fromBase)]) {
732
+ const resolved = await resolve();
733
+ if (resolved)
734
+ return resolved;
735
+ }
736
+ return null;
737
+ }
738
+ async#scanCandidates(sourcePaths, dirs) {
739
+ const candidates = new Set;
740
+ const glob = new Bun.Glob("**/*.{tsx,ts,jsx,js,html}");
741
+ const files = new Set(sourcePaths);
742
+ await Promise.all(dirs.map(async (dir) => {
743
+ for await (const file of glob.scan({ cwd: dir, absolute: true })) {
744
+ if (isIgnoredNodeModuleSource(file))
745
+ continue;
746
+ files.add(file);
747
+ }
748
+ }));
749
+ const cache = await new CssCandidateCache(this.#candidateCachePath).load();
750
+ await Promise.all([...files].map(async (file) => {
751
+ for (const candidate of await cache.candidatesFor(file))
752
+ candidates.add(candidate);
753
+ }));
754
+ await cache.save(files);
755
+ this.#logger.verbose(`css candidate cache reused=${cache.reused} rescanned=${cache.rescanned}`);
756
+ return [...candidates];
757
+ }
758
+ }
759
+ function resolveSourceWithBun(id, fromBase) {
760
+ try {
761
+ const resolved = Bun.resolveSync(id, fromBase);
762
+ return isSourceFile(resolved) ? resolved : null;
763
+ } catch {
764
+ return null;
765
+ }
766
+ }
767
+ function resolveSourceWithRequire(id, fromBase) {
768
+ try {
769
+ const resolved = __require.resolve(id, { paths: [fromBase] });
770
+ return isSourceFile(resolved) ? resolved : null;
771
+ } catch {
772
+ return null;
773
+ }
774
+ }
775
+ function isSourceFile(filePath) {
776
+ return SOURCE_EXTS.includes(path5.extname(filePath));
777
+ }
778
+ function isIgnoredNodeModuleSource(filePath) {
779
+ return NODE_MODULES_RE.test(filePath) && !AKANJS_NODE_MODULE_RE.test(filePath);
780
+ }
781
+ function declaredCustomProperties(css) {
782
+ const withoutThemeBlocks = css.replace(/@theme[^{]*\{[^}]*\}/g, "");
783
+ return [...new Set([...withoutThemeBlocks.matchAll(/(?:^|[\s;{])(--[\w-]+)\s*:/g)].map(([, name]) => name))].filter((name) => !!name);
784
+ }
785
+ function getPageKeyBasePath(pageKey, basePaths) {
786
+ const normalized = pageKey.split(path5.sep).join("/").replace(/^\.\//, "");
787
+ const segments = normalized.split("/");
788
+ const firstPublicSegment = segments.find((segment) => segment !== "[lang]" && !/^\(.+\)$/.test(segment));
789
+ return firstPublicSegment && basePaths.includes(firstPublicSegment) ? firstPublicSegment : null;
790
+ }
791
+
792
+ // pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
793
+ import path8 from "path";
794
+
795
+ // pkgs/@akanjs/devkit/transforms/externalizeFrameworkPlugin.ts
796
+ import path6 from "path";
797
+ var DEFAULT_INCLUDE = ["akanjs/", "@apps/", "@libs/"];
798
+ var DEFAULT_EXCLUDE_EXACT = new Set(["akanjs/webkit", "@akanjs/cli", "@akanjs/devkit"]);
799
+ var DEFAULT_EXCLUDE_PREFIX = ["@akanjs/cli/", "@akanjs/devkit/"];
800
+ var OPTIONAL_BACKEND_EXTERNAL_EXACT = new Set([
801
+ "@libsql/client",
802
+ "bullmq",
803
+ "ioredis",
804
+ "postgres",
805
+ "protobufjs"
806
+ ]);
807
+ var RUNTIME_EXTERNAL_EXACT = new Set([
808
+ "react",
809
+ "react-dom",
810
+ "react/jsx-runtime",
811
+ "react/jsx-dev-runtime",
812
+ "react-server-dom-webpack",
813
+ "react-server-dom-webpack/server.node",
814
+ "react-server-dom-webpack/client.node",
815
+ "react-server-dom-webpack/client.browser"
816
+ ]);
817
+ var RUNTIME_EXTERNAL_PREFIX = ["react-dom/", "react-server-dom-webpack/"];
818
+ var CANDIDATE_EXTS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
819
+ async function createExternalizeFrameworkPlugin(options) {
820
+ const tsconfig = await options.app.getTsConfig();
821
+ const includePrefixes = options.include ?? DEFAULT_INCLUDE;
822
+ const extraExact = new Set(options.extra ?? []);
823
+ const workspaceRoot = options.app.workspace.workspaceRoot;
824
+ const tsconfigPaths = tsconfig.compilerOptions.paths ?? {};
825
+ const rootEntries = Object.entries(tsconfigPaths).filter(([k]) => !k.endsWith("/*")).map(([k, v]) => ({ pkg: k, entryFile: v[0] ?? null })).filter((e) => e.entryFile !== null);
826
+ const wildcardEntries = Object.entries(tsconfigPaths).filter(([k]) => k.endsWith("/*")).map(([k, v]) => ({ prefix: k.slice(0, -1), replacements: v })).sort((a, b) => b.prefix.length - a.prefix.length);
827
+ async function resolveWorkspaceSubpath(spec) {
828
+ if (!workspaceRoot)
829
+ return null;
830
+ for (const { prefix, replacements } of wildcardEntries) {
831
+ if (!spec.startsWith(prefix))
832
+ continue;
833
+ const suffix = spec.slice(prefix.length);
834
+ for (const repl of replacements) {
835
+ const replPath = repl?.endsWith("/*") ? repl.slice(0, -1) : repl ?? "";
836
+ if (!replPath)
837
+ continue;
838
+ const candidate = path6.resolve(workspaceRoot, replPath + suffix);
839
+ const hit = await firstExisting(candidate);
840
+ if (hit)
841
+ return hit;
842
+ }
843
+ }
844
+ for (const { pkg, entryFile } of rootEntries) {
845
+ if (spec !== pkg && !spec.startsWith(`${pkg}/`))
846
+ continue;
847
+ if (spec === pkg)
848
+ continue;
849
+ const suffix = spec.slice(pkg.length + 1);
850
+ const pkgDir = path6.dirname(path6.resolve(workspaceRoot, entryFile));
851
+ const candidate = path6.join(pkgDir, suffix);
852
+ const hit = await firstExisting(candidate);
853
+ if (hit)
854
+ return hit;
855
+ }
856
+ return null;
857
+ }
858
+ return {
859
+ name: "akan-externalize-framework",
860
+ setup(build) {
861
+ build.onResolve({ filter: /.*/ }, async (args) => {
862
+ const spec = args.path;
863
+ if (spec === "." || spec === ".." || spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/"))
864
+ return;
865
+ if (extraExact.has(spec) || DEFAULT_EXCLUDE_EXACT.has(spec))
866
+ return { path: spec, external: true };
867
+ for (const prefix of DEFAULT_EXCLUDE_PREFIX) {
868
+ if (spec.startsWith(prefix))
869
+ return { path: spec, external: true };
870
+ }
871
+ if (OPTIONAL_BACKEND_EXTERNAL_EXACT.has(spec))
872
+ return { path: spec, external: true };
873
+ if (RUNTIME_EXTERNAL_EXACT.has(spec))
874
+ return { path: spec, external: true };
875
+ for (const prefix of RUNTIME_EXTERNAL_PREFIX) {
876
+ if (spec.startsWith(prefix))
877
+ return { path: spec, external: true };
878
+ }
879
+ for (const prefix of includePrefixes) {
880
+ if (!spec.startsWith(prefix))
881
+ continue;
882
+ const resolved = await resolveWorkspaceSubpath(spec);
883
+ if (resolved)
884
+ return { path: resolved };
885
+ return;
886
+ }
887
+ return;
888
+ });
889
+ }
890
+ };
891
+ }
892
+ async function firstExisting(basePath) {
893
+ if (await Bun.file(basePath).exists())
894
+ return basePath;
895
+ for (const ext of CANDIDATE_EXTS) {
896
+ const candidate = `${basePath}${ext}`;
897
+ if (await Bun.file(candidate).exists())
898
+ return candidate;
899
+ }
900
+ for (const ext of CANDIDATE_EXTS) {
901
+ const candidate = path6.join(basePath, `index${ext}`);
902
+ if (await Bun.file(candidate).exists())
903
+ return candidate;
904
+ }
905
+ return null;
906
+ }
907
+
908
+ // pkgs/@akanjs/devkit/transforms/useClientBundlePlugin.ts
909
+ function createUseClientBundlePlugin(options = {}) {
910
+ return {
911
+ name: "akan-use-client-bundle",
912
+ setup(build) {
913
+ build.onLoad({ filter: /\.(tsx|ts|jsx|js)$/ }, async (args) => {
914
+ if (args.path.includes("/node_modules/"))
915
+ return;
916
+ let source;
917
+ try {
918
+ source = await Bun.file(args.path).text();
919
+ } catch {
920
+ return;
921
+ }
922
+ const stubbed = transformUseClient(source, {
923
+ path: args.path,
924
+ workspaceRoot: options.workspaceRoot
925
+ });
926
+ if (stubbed === null)
927
+ return;
928
+ return { contents: stubbed, loader: loaderFor(args.path) };
929
+ });
930
+ }
931
+ };
932
+ }
933
+ function loaderFor(absPath) {
934
+ if (absPath.endsWith(".tsx"))
935
+ return "tsx";
936
+ if (absPath.endsWith(".jsx"))
937
+ return "jsx";
938
+ if (absPath.endsWith(".ts"))
939
+ return "ts";
940
+ return "js";
941
+ }
942
+
943
+ // pkgs/@akanjs/devkit/frontendBuild/pagesEntrySourceGenerator.ts
944
+ import path7 from "path";
945
+ class PagesEntrySourceGenerator {
946
+ #pageEntries;
947
+ constructor(pageEntries) {
948
+ this.#pageEntries = pageEntries;
949
+ }
950
+ static generate(pageEntries) {
951
+ return new PagesEntrySourceGenerator(pageEntries).generate();
952
+ }
953
+ generate() {
954
+ const lines = this.#pageEntries.map(({ key, moduleAbsPath }) => {
955
+ const specifier = PagesEntrySourceGenerator.#toImportSpecifier(moduleAbsPath);
956
+ return ` ${JSON.stringify(key)}: () => import(${JSON.stringify(specifier)}),`;
957
+ });
958
+ return `export const pages = {
959
+ ${lines.join(`
960
+ `)}
961
+ };
962
+ `;
963
+ }
964
+ static async generateStatic(pageEntries) {
965
+ return await new PagesEntrySourceGenerator(pageEntries).generateStatic();
966
+ }
967
+ async generateStatic() {
968
+ const imports = this.#pageEntries.map(({ moduleAbsPath }, index) => {
969
+ const specifier = PagesEntrySourceGenerator.#toImportSpecifier(moduleAbsPath);
970
+ return `import * as page${index} from ${JSON.stringify(specifier)};`;
971
+ });
972
+ const entries = await Promise.all(this.#pageEntries.map(async ({ key, moduleAbsPath }, index) => {
973
+ const isAsyncDefault = await AsyncDefaultExportDetector.detect(moduleAbsPath);
974
+ return ` ${JSON.stringify(key)}: { loader: async () => page${index}, isAsyncDefault: ${isAsyncDefault} },`;
975
+ }));
976
+ return `${imports.join(`
977
+ `)}
978
+ export const pages = {
979
+ ${entries.join(`
980
+ `)}
981
+ };
982
+ `;
983
+ }
984
+ static #toImportSpecifier(moduleAbsPath) {
985
+ return path7.resolve(moduleAbsPath).split(path7.sep).join("/");
986
+ }
987
+ }
988
+
989
+ // pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
990
+ var VIRTUAL_PAGES_ENTRY = "akan-pages-entry";
991
+
992
+ class PagesBundleBuilder {
993
+ #app;
994
+ #command;
995
+ #pageEntries;
996
+ #started = Date.now();
997
+ constructor(app, command = "start", pageEntries) {
998
+ this.#app = app;
999
+ this.#command = command;
1000
+ this.#pageEntries = pageEntries;
1001
+ }
1002
+ async build() {
1003
+ const akanConfig = await this.#app.getConfig();
1004
+ const resolvedEntries = this.#pageEntries ?? await resolveSsrPageEntriesForApp(this.#app, await this.#app.getPageKeys());
1005
+ const entrySource = PagesEntrySourceGenerator.generate(resolvedEntries);
1006
+ const workspaceRoot = this.#app.workspace.workspaceRoot;
1007
+ const result = await Bun.build({
1008
+ entrypoints: [VIRTUAL_PAGES_ENTRY],
1009
+ outdir: `${this.#artifactDir}/server`,
1010
+ target: "bun",
1011
+ format: "esm",
1012
+ splitting: this.#splitting,
1013
+ minify: this.#command === "build",
1014
+ naming: {
1015
+ entry: "pages-[hash].[ext]",
1016
+ chunk: "chunks/[name]-[hash].[ext]",
1017
+ asset: "assets/[name]-[hash].[ext]"
1018
+ },
1019
+ define: this.#define(),
1020
+ plugins: [
1021
+ PagesBundleBuilder.createPagesEntryPlugin(entrySource),
1022
+ PagesBundleBuilder.createCssStubPlugin(),
1023
+ PagesBundleBuilder.createServerUseClientFetchPlugin(),
1024
+ await createExternalizeFrameworkPlugin({ app: this.#app, extra: akanConfig.externalLibs }),
1025
+ akanConfig.barrelImports.length > 0 ? await createBarrelImportsPlugin(this.#app, {
1026
+ pipeAfter: (source, args) => transformUseClient(source, {
1027
+ path: args.path,
1028
+ workspaceRoot
1029
+ })
1030
+ }) : createUseClientBundlePlugin({ workspaceRoot })
1031
+ ]
1032
+ });
1033
+ if (!result.success)
1034
+ throw new AggregateError(result.logs, "[PagesBundleBuilder] Bun.build failed");
1035
+ const entryArtifact = result.outputs.find((a) => a.kind === "entry-point");
1036
+ if (!entryArtifact)
1037
+ throw new Error("[PagesBundleBuilder] Bun.build emitted no entry-point artifact");
1038
+ const bundlePath = path8.resolve(entryArtifact.path);
1039
+ const buildId = Date.now();
1040
+ const outputBytes = result.outputs.reduce((sum, output) => sum + output.size, 0);
1041
+ const chunkCount = result.outputs.filter((output) => output.kind === "chunk").length;
1042
+ this.#app.verbose(`[PagesBundleBuilder] ${path8.basename(bundlePath)} emitted in ${Date.now() - this.#started}ms splitting=${this.#splitting} entry=${entryArtifact.size} bytes outputs=${result.outputs.length} chunks=${chunkCount} total=${outputBytes} bytes`);
1043
+ return {
1044
+ bundlePath,
1045
+ buildId,
1046
+ splitting: this.#splitting,
1047
+ entryBytes: entryArtifact.size,
1048
+ outputBytes,
1049
+ outputCount: result.outputs.length,
1050
+ chunkCount
1051
+ };
1052
+ }
1053
+ get #artifactDir() {
1054
+ return `${this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath}/.akan/artifact`;
1055
+ }
1056
+ get #splitting() {
1057
+ return process.env.AKAN_SERVER_PAGES_SPLITTING === "1";
1058
+ }
1059
+ #define() {
1060
+ const nodeEnv = this.#command === "build" ? "production" : "development";
1061
+ return {
1062
+ "process.env.NODE_ENV": JSON.stringify(nodeEnv),
1063
+ "process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("ssr"),
1064
+ ...Object.fromEntries(Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]))
1065
+ };
1066
+ }
1067
+ static createPagesEntryPlugin(source) {
1068
+ return {
1069
+ name: "akan-pages-entry",
1070
+ setup(build) {
1071
+ build.onResolve({ filter: /^akan-pages-entry$/ }, () => ({
1072
+ path: VIRTUAL_PAGES_ENTRY,
1073
+ namespace: "akan-virtual"
1074
+ }));
1075
+ build.onLoad({ filter: /^akan-pages-entry$/, namespace: "akan-virtual" }, () => ({
1076
+ contents: source,
1077
+ loader: "tsx"
1078
+ }));
1079
+ }
1080
+ };
1081
+ }
1082
+ static createCssStubPlugin() {
1083
+ return {
1084
+ name: "akan-css-stub",
1085
+ setup(build) {
1086
+ build.onLoad({ filter: /\.css$/ }, () => ({
1087
+ contents: "",
1088
+ loader: "js"
1089
+ }));
1090
+ }
1091
+ };
1092
+ }
1093
+ static createServerUseClientFetchPlugin() {
1094
+ return {
1095
+ name: "akan-server-use-client-fetch",
1096
+ setup(build) {
1097
+ build.onLoad({ filter: /[/\\]lib[/\\]useClient\.(ts|tsx|js|jsx)$/ }, async (args) => {
1098
+ const source = await Bun.file(args.path).text();
1099
+ const transformed = PagesBundleBuilder.transformServerUseClientFetchSource(source);
1100
+ if (transformed === source)
1101
+ return;
1102
+ return { contents: transformed, loader: loaderFor2(args.path) };
1103
+ });
1104
+ }
1105
+ };
1106
+ }
1107
+ static transformServerUseClientFetchSource(source) {
1108
+ if (!source.includes(`with { type: "macro" }`) || !source.includes("FetchClient.build"))
1109
+ return source;
1110
+ return source.replace(/import\s+\{\s*getSerializedSignal\s*\}\s+from\s+["']\.\/sig["']\s+with\s+\{\s*type\s*:\s*["']macro["']\s*\};/, `import { fetch as serverFetch } from "./sig";`).replace(/const\s+fetchProto\s*=\s*FetchClient\.build<[^;]+;/, "const fetchProto = FetchClient.build<typeof signal>(cnst, serverFetch.serializedSignal, { Err: pageProto.Err, base: serverFetch });");
1111
+ }
1112
+ }
1113
+ function loaderFor2(absPath) {
1114
+ if (absPath.endsWith(".tsx"))
1115
+ return "tsx";
1116
+ if (absPath.endsWith(".jsx"))
1117
+ return "jsx";
1118
+ if (absPath.endsWith(".ts"))
1119
+ return "ts";
1120
+ return "js";
1121
+ }
1122
+
1123
+ // pkgs/@akanjs/devkit/frontendBuild/csrArtifactBuilder.ts
1124
+ import { mkdir, rm, unlink } from "fs/promises";
1125
+ import path9 from "path";
1126
+ class CsrArtifactBuilder {
1127
+ #app;
1128
+ #command;
1129
+ #lang;
1130
+ constructor(app, command = "start", lang = "en") {
1131
+ this.#app = app;
1132
+ this.#command = command;
1133
+ this.#lang = lang;
1134
+ }
1135
+ async build() {
1136
+ const pageKeys = await this.#app.getPageKeys();
1137
+ if (pageKeys.length === 0) {
1138
+ this.#app.log(`[cli] no route files under ${this.#app.cwdPath}/page \u2014 skipping CSR build`);
1139
+ return null;
1140
+ }
1141
+ const pageEntries = await resolveSsrPageEntriesForApp(this.#app, pageKeys);
1142
+ const akanConfig = await this.#app.getConfig();
1143
+ const cssAssets = await this.#loadCssAssets();
1144
+ const basePaths = [...akanConfig.basePaths];
1145
+ const htmlBasePaths = basePaths.length > 0 ? basePaths : [""];
1146
+ await rm(this.#outputDir, { recursive: true, force: true });
1147
+ await mkdir(this.#generatedDir, { recursive: true });
1148
+ const generatedFiles = Object.fromEntries((await Promise.all(htmlBasePaths.map(async (basePath) => [
1149
+ this.#createHtmlFile(basePath),
1150
+ await this.#createEntryFile(basePath, CsrArtifactBuilder.pageEntriesForBasePath(pageEntries, basePath, basePaths))
1151
+ ]))).flat());
1152
+ const result = await Bun.build({
1153
+ target: "browser",
1154
+ entrypoints: htmlBasePaths.map((basePath) => this.#generatedPath(CsrArtifactBuilder.htmlFilename(basePath))),
1155
+ files: generatedFiles,
1156
+ root: this.#generatedDir,
1157
+ outdir: this.#outputDir,
1158
+ splitting: false,
1159
+ minify: true,
1160
+ env: "AKAN_PUBLIC_*",
1161
+ define: this.#define(),
1162
+ optimizeImports: akanConfig.optimizeImports,
1163
+ plugins: [PagesBundleBuilder.createCssStubPlugin()]
1164
+ });
1165
+ if (!result.success) {
1166
+ const logs = result.logs.map((log) => log.message).join(`
1167
+ `);
1168
+ throw new Error(`[csr-build] failed${logs ? `
1169
+ ${logs}` : ""}`);
1170
+ }
1171
+ await this.#inlineCsrArtifacts(cssAssets);
1172
+ this.#app.verbose(`[csr-build] output -> ${this.#outputDir}`);
1173
+ return { outputDir: this.#outputDir };
1174
+ }
1175
+ static pageEntriesForBasePath(pageEntries, basePath, basePaths) {
1176
+ return pageEntries.filter((entry) => {
1177
+ const entryBasePath = getPageKeyBasePath(entry.key, basePaths);
1178
+ return entryBasePath === null || entryBasePath === basePath;
1179
+ });
1180
+ }
1181
+ static htmlFilename(basePath) {
1182
+ return `${basePath || "index"}.html`;
1183
+ }
1184
+ static entryFilename(basePath) {
1185
+ return `${basePath || "index"}.csr.tsx`;
1186
+ }
1187
+ static basePathOfHtml(htmlPath) {
1188
+ const name = path9.basename(htmlPath, ".html");
1189
+ return name === "index" ? "" : name;
1190
+ }
1191
+ get #outputDir() {
1192
+ return path9.join(this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath, this.#command === "build" ? "csr" : ".akan/artifact/csr");
1193
+ }
1194
+ get #generatedDir() {
1195
+ return path9.join(this.#app.cwdPath, ".akan/generated/csr");
1196
+ }
1197
+ get #artifactDir() {
1198
+ return path9.join(this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath, ".akan/artifact");
1199
+ }
1200
+ #generatedPath(filename) {
1201
+ return path9.join(this.#generatedDir, filename);
1202
+ }
1203
+ #define() {
1204
+ const nodeEnv = this.#command === "build" ? "production" : "development";
1205
+ return {
1206
+ "process.env.NODE_ENV": JSON.stringify(nodeEnv),
1207
+ "process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("csr"),
1208
+ ...Object.fromEntries(Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]))
1209
+ };
1210
+ }
1211
+ async#createEntryFile(basePath, pageEntries) {
1212
+ return [
1213
+ this.#generatedPath(CsrArtifactBuilder.entryFilename(basePath)),
1214
+ `import { bootCsr } from "akanjs/webkit";
1215
+ ${await PagesEntrySourceGenerator.generateStatic(pageEntries)}
1216
+ void bootCsr(pages);
1217
+ `
1218
+ ];
1219
+ }
1220
+ #createHtmlFile(basePath) {
1221
+ return [
1222
+ this.#generatedPath(CsrArtifactBuilder.htmlFilename(basePath)),
1223
+ `<!doctype html>
1224
+ <html lang="${this.#lang}">
1225
+ <head>
1226
+ <meta charset="utf-8" />
1227
+ <title>${this.#app.name}</title>
1228
+ <base href="/" />
1229
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1230
+ </head>
1231
+ <body>
1232
+ <div id="root"></div>
1233
+ <script type="module" src="./${CsrArtifactBuilder.entryFilename(basePath)}"></script>
1234
+ </body>
1235
+ </html>
1236
+ `
1237
+ ];
1238
+ }
1239
+ async#loadCssAssets() {
1240
+ const artifactFile = Bun.file(path9.join(this.#artifactDir, "base-artifact.json"));
1241
+ if (!await artifactFile.exists())
1242
+ return {};
1243
+ const artifact = await artifactFile.json();
1244
+ return artifact.cssAssets ?? {};
1245
+ }
1246
+ async#inlineCsrArtifacts(cssAssets) {
1247
+ const jsFiles = new Set;
1248
+ for (const htmlPath of await this.#htmlOutputPaths()) {
1249
+ const htmlFile = Bun.file(htmlPath);
1250
+ if (!await htmlFile.exists())
1251
+ continue;
1252
+ const basePath = CsrArtifactBuilder.basePathOfHtml(htmlPath);
1253
+ const cssAsset = cssAssets[basePath];
1254
+ if (!cssAsset) {
1255
+ this.#app.logger.warn(`[csr-build] base-artifact.json has no compiled stylesheet for ${basePath || "root"}; ${path9.basename(htmlPath)} ships without CSS`);
1256
+ }
1257
+ const inlined = await this.#inlineHtmlAssets(await htmlFile.text(), htmlPath, cssAsset);
1258
+ for (const filePath of inlined.jsFiles)
1259
+ jsFiles.add(filePath);
1260
+ await Bun.write(htmlPath, inlined.html);
1261
+ }
1262
+ for (const filePath of jsFiles)
1263
+ await unlink(filePath).catch(() => {
1264
+ return;
1265
+ });
1266
+ const remainingAssets = await this.#listOutputFiles((filePath) => filePath.endsWith(".js") || filePath.endsWith(".css"));
1267
+ if (remainingAssets.length > 0) {
1268
+ throw new Error(`[csr-build] expected single-file HTML, but CSR assets remain:
1269
+ ${remainingAssets.join(`
1270
+ `)}`);
1271
+ }
1272
+ }
1273
+ async#inlineHtmlAssets(html, htmlPath, cssAsset) {
1274
+ let next = html;
1275
+ if (cssAsset) {
1276
+ const css = await Bun.file(path9.join(this.#artifactDir, cssAsset.cssRelPath)).text();
1277
+ next = CsrArtifactBuilder.injectBeforeHeadEnd(next, CsrArtifactBuilder.createInlineStyle(css));
1278
+ }
1279
+ const jsFiles = [];
1280
+ next = await CsrArtifactBuilder.replaceModuleScriptSrc(next, async (src) => {
1281
+ const jsPath = CsrArtifactBuilder.resolveHtmlAssetPath(htmlPath, src);
1282
+ jsFiles.push(jsPath);
1283
+ return await Bun.file(jsPath).text();
1284
+ });
1285
+ return { html: next, jsFiles };
1205
1286
  }
1206
- #resolveSourceImport(id, fromBase, resolvePackage) {
1207
- const cacheKey = `${fromBase}\x00${id}`;
1208
- let cached = this.#resolvedSpecifierCache.get(cacheKey);
1209
- if (cached)
1210
- return cached;
1211
- cached = this.#resolveSourceImportUncached(id, fromBase, resolvePackage);
1212
- this.#resolvedSpecifierCache.set(cacheKey, cached);
1213
- return cached;
1287
+ async#htmlOutputPaths() {
1288
+ return await this.#listOutputFiles((filePath) => filePath.endsWith(".html"));
1214
1289
  }
1215
- async#resolveSourceImportUncached(id, fromBase, resolvePackage) {
1216
- if (id.startsWith(".") || id.startsWith("/")) {
1217
- const abs = id.startsWith("/") ? id : path8.resolve(fromBase, id);
1218
- return this.#resolveSourceFileCandidate(abs);
1219
- }
1220
- const pkg = await resolvePackage(id);
1221
- if (pkg)
1222
- return pkg.entryFile;
1223
- for (const resolve of [() => resolveSourceWithBun(id, fromBase), () => resolveSourceWithRequire(id, fromBase)]) {
1224
- const resolved = await resolve();
1225
- if (resolved)
1226
- return resolved;
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);
1227
1296
  }
1228
- return null;
1297
+ return files.sort();
1229
1298
  }
1230
- async#scanCandidates(sourcePaths, dirs) {
1231
- const candidates = new Set;
1232
- const glob = new Bun.Glob("**/*.{tsx,ts,jsx,js,html}");
1233
- const files = new Set(sourcePaths);
1234
- await Promise.all(dirs.map(async (dir) => {
1235
- for await (const file of glob.scan({ cwd: dir, absolute: true })) {
1236
- if (isIgnoredNodeModuleSource(file))
1237
- continue;
1238
- files.add(file);
1239
- }
1240
- }));
1241
- const cache = await new CssCandidateCache(this.#candidateCachePath).load();
1242
- await Promise.all([...files].map(async (file) => {
1243
- for (const candidate of await cache.candidatesFor(file))
1244
- candidates.add(candidate);
1245
- }));
1246
- await cache.save(files);
1247
- this.#logger.verbose(`css candidate cache reused=${cache.reused} rescanned=${cache.rescanned}`);
1248
- return [...candidates];
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));
1249
1314
  }
1250
- }
1251
- function resolveSourceWithBun(id, fromBase) {
1252
- try {
1253
- const resolved = Bun.resolveSync(id, fromBase);
1254
- return isSourceFile(resolved) ? resolved : null;
1255
- } catch {
1256
- return null;
1315
+ static createInlineStyle(css) {
1316
+ return `<style data-akan-css="active">
1317
+ ${css.replace(/<\/style/gi, "<\\/style")}
1318
+ </style>`;
1257
1319
  }
1258
- }
1259
- function resolveSourceWithRequire(id, fromBase) {
1260
- try {
1261
- const resolved = __require.resolve(id, { paths: [fromBase] });
1262
- return isSourceFile(resolved) ? resolved : null;
1263
- } catch {
1264
- return null;
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);
1340
+ }
1341
+ static escapeInlineScript(source) {
1342
+ return source.replace(/<\/script/gi, "<\\/script");
1343
+ }
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);
1265
1350
  }
1266
- }
1267
- function isSourceFile(filePath) {
1268
- return SOURCE_EXTS.includes(path8.extname(filePath));
1269
- }
1270
- function isIgnoredNodeModuleSource(filePath) {
1271
- return NODE_MODULES_RE.test(filePath) && !AKANJS_NODE_MODULE_RE.test(filePath);
1272
- }
1273
- function getPageKeyBasePath(pageKey, basePaths) {
1274
- const normalized = pageKey.split(path8.sep).join("/").replace(/^\.\//, "");
1275
- const segments = normalized.split("/");
1276
- const firstPublicSegment = segments.find((segment) => segment !== "[lang]" && !/^\(.+\)$/.test(segment));
1277
- return firstPublicSegment && basePaths.includes(firstPublicSegment) ? firstPublicSegment : null;
1278
1351
  }
1279
1352
 
1280
1353
  // pkgs/@akanjs/devkit/frontendBuild/fontOptimizer.ts
1281
- import { mkdir as mkdir3, stat as stat2 } from "fs/promises";
1282
- import path9 from "path";
1283
- 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";
1284
1357
  var FONT_URL_PREFIX = "/_akan/fonts";
1285
1358
  var DEFAULT_FONT_SUBSETS = ["latin"];
1286
1359
 
@@ -1292,11 +1365,11 @@ class FontOptimizer {
1292
1365
  #cssParts = [];
1293
1366
  #woff2Ready = null;
1294
1367
  static #ksX1001Text = null;
1295
- static #cacheVersion = 1;
1368
+ static #cacheVersion = 2;
1296
1369
  constructor(app, command = "start") {
1297
1370
  this.#app = app;
1298
1371
  this.#command = command;
1299
- 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");
1300
1373
  }
1301
1374
  async optimize() {
1302
1375
  const fonts = await this.discoverFonts();
@@ -1321,7 +1394,7 @@ class FontOptimizer {
1321
1394
  return result;
1322
1395
  }
1323
1396
  get #cachePath() {
1324
- return path9.join(this.#artifactRoot, "fontCache.json");
1397
+ return path10.join(this.#artifactRoot, "fontCache.json");
1325
1398
  }
1326
1399
  async#buildCacheKey(fonts) {
1327
1400
  const sources = [];
@@ -1336,16 +1409,19 @@ class FontOptimizer {
1336
1409
  sources.push({ optimizedSrc: face.optimizedSrc, ...stamp });
1337
1410
  }
1338
1411
  for (const filePath of font.subsetFiles ?? []) {
1339
- 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);
1340
1413
  const stamp = await this.#fileStamp(abs);
1341
1414
  if (!stamp)
1342
1415
  return null;
1343
1416
  sources.push({ subsetFile: filePath, ...stamp });
1344
1417
  }
1345
1418
  if (this.#getFontSubsets(font).includes("auto"))
1346
- sources.push({ autoSubsetText: this.#hashFontConfig(await this.#collectAutoSubsetText()) });
1419
+ sources.push({ autoSubsetText: this.#cacheDigest(await this.#collectAutoSubsetText()) });
1347
1420
  }
1348
- 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");
1349
1425
  }
1350
1426
  async#fileStamp(filePath) {
1351
1427
  try {
@@ -1359,7 +1435,7 @@ class FontOptimizer {
1359
1435
  const cache = await Bun.file(this.#cachePath).json().catch(() => null);
1360
1436
  if (cache?.version !== FontOptimizer.#cacheVersion || cache.key !== key)
1361
1437
  return null;
1362
- const files = cache.files.map((relativePath) => path9.join(this.#artifactRoot, relativePath));
1438
+ const files = cache.files.map((relativePath) => path10.join(this.#artifactRoot, relativePath));
1363
1439
  for (const filePath of files) {
1364
1440
  if (!await Bun.file(filePath).exists())
1365
1441
  return null;
@@ -1371,7 +1447,7 @@ class FontOptimizer {
1371
1447
  version: FontOptimizer.#cacheVersion,
1372
1448
  key,
1373
1449
  css: result.css,
1374
- files: result.files.map((filePath) => path9.relative(this.#artifactRoot, filePath))
1450
+ files: result.files.map((filePath) => path10.relative(this.#artifactRoot, filePath))
1375
1451
  };
1376
1452
  await Bun.write(this.#cachePath, JSON.stringify(cache));
1377
1453
  }
@@ -1379,7 +1455,7 @@ class FontOptimizer {
1379
1455
  const pageKeys = await this.#app.getPageKeys();
1380
1456
  const fonts = [];
1381
1457
  await Promise.all(pageKeys.map(async (key) => {
1382
- const filePath = path9.resolve(this.#app.cwdPath, "page", key);
1458
+ const filePath = path10.resolve(this.#app.cwdPath, "page", key);
1383
1459
  const file = Bun.file(filePath);
1384
1460
  if (!await file.exists())
1385
1461
  return;
@@ -1398,8 +1474,8 @@ class FontOptimizer {
1398
1474
  this.#app.logger.warn(`[font] source not found: ${face.src}`);
1399
1475
  continue;
1400
1476
  }
1401
- const outputPath = path9.join(this.#artifactRoot, face.optimizedSrc.replace(/^\/_akan\//, ""));
1402
- 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 });
1403
1479
  const sourceBuffer = Buffer.from(await Bun.file(sourcePath).arrayBuffer());
1404
1480
  const outputBuffer = await this.#buildFontBuffer(font, sourceBuffer, sourcePath);
1405
1481
  await Bun.write(outputPath, outputBuffer);
@@ -1413,17 +1489,17 @@ class FontOptimizer {
1413
1489
  this.#cssParts.push(...faceCss, this.#buildRootVariableRule(font));
1414
1490
  }
1415
1491
  #extractFontsExport(source, filePath) {
1416
- 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);
1417
1493
  const fonts = [];
1418
1494
  for (const statement of sourceFile.statements) {
1419
- if (!ts2.isVariableStatement(statement))
1495
+ if (!ts.isVariableStatement(statement))
1420
1496
  continue;
1421
- const modifiers = ts2.canHaveModifiers(statement) ? ts2.getModifiers(statement) : undefined;
1422
- 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;
1423
1499
  if (!isExported)
1424
1500
  continue;
1425
1501
  for (const declaration of statement.declarationList.declarations) {
1426
- if (!ts2.isIdentifier(declaration.name) || declaration.name.text !== "fonts")
1502
+ if (!ts.isIdentifier(declaration.name) || declaration.name.text !== "fonts")
1427
1503
  continue;
1428
1504
  const value = declaration.initializer ? this.#literalToValue(declaration.initializer) : null;
1429
1505
  if (Array.isArray(value)) {
@@ -1434,20 +1510,20 @@ class FontOptimizer {
1434
1510
  return fonts;
1435
1511
  }
1436
1512
  #literalToValue(node) {
1437
- if (ts2.isStringLiteralLike(node))
1513
+ if (ts.isStringLiteralLike(node))
1438
1514
  return node.text;
1439
- if (ts2.isNumericLiteral(node))
1515
+ if (ts.isNumericLiteral(node))
1440
1516
  return Number(node.text);
1441
- if (node.kind === ts2.SyntaxKind.TrueKeyword)
1517
+ if (node.kind === ts.SyntaxKind.TrueKeyword)
1442
1518
  return true;
1443
- if (node.kind === ts2.SyntaxKind.FalseKeyword)
1519
+ if (node.kind === ts.SyntaxKind.FalseKeyword)
1444
1520
  return false;
1445
- if (ts2.isArrayLiteralExpression(node))
1521
+ if (ts.isArrayLiteralExpression(node))
1446
1522
  return node.elements.map((element) => this.#literalToValue(element));
1447
- if (ts2.isObjectLiteralExpression(node)) {
1523
+ if (ts.isObjectLiteralExpression(node)) {
1448
1524
  const obj = {};
1449
1525
  for (const prop of node.properties) {
1450
- if (!ts2.isPropertyAssignment(prop))
1526
+ if (!ts.isPropertyAssignment(prop))
1451
1527
  continue;
1452
1528
  const name = this.#getPropertyName(prop.name);
1453
1529
  if (!name)
@@ -1456,13 +1532,13 @@ class FontOptimizer {
1456
1532
  }
1457
1533
  return obj;
1458
1534
  }
1459
- if (ts2.isAsExpression(node) || ts2.isSatisfiesExpression(node) || ts2.isParenthesizedExpression(node)) {
1535
+ if (ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isParenthesizedExpression(node)) {
1460
1536
  return this.#literalToValue(node.expression);
1461
1537
  }
1462
1538
  return;
1463
1539
  }
1464
1540
  #getPropertyName(name) {
1465
- if (ts2.isIdentifier(name) || ts2.isStringLiteral(name) || ts2.isNumericLiteral(name))
1541
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name))
1466
1542
  return name.text;
1467
1543
  return null;
1468
1544
  }
@@ -1546,8 +1622,8 @@ class FontOptimizer {
1546
1622
  return null;
1547
1623
  const rel = src.replace(/^\//, "");
1548
1624
  const candidates = [
1549
- this.#command === "build" ? path9.join(this.#app.dist.cwdPath, "public", rel) : null,
1550
- 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),
1551
1627
  this.#resolveWorkspacePublicPath(rel)
1552
1628
  ].filter(Boolean);
1553
1629
  for (const candidate of candidates) {
@@ -1582,7 +1658,7 @@ class FontOptimizer {
1582
1658
  return "woff2";
1583
1659
  if (signature === "OTTO")
1584
1660
  return "otf";
1585
- const ext = path9.extname(sourcePath).slice(1).toLowerCase();
1661
+ const ext = path10.extname(sourcePath).slice(1).toLowerCase();
1586
1662
  if (ext === "otf" || ext === "woff" || ext === "woff2")
1587
1663
  return ext;
1588
1664
  return "ttf";
@@ -1591,7 +1667,7 @@ class FontOptimizer {
1591
1667
  const [root, dep, ...rest] = rel.split("/");
1592
1668
  if (root !== "libs" || !dep || rest.length === 0)
1593
1669
  return null;
1594
- return path9.join(this.#app.workspace.workspaceRoot, "libs", dep, "public", ...rest);
1670
+ return path10.join(this.#app.workspace.workspaceRoot, "libs", dep, "public", ...rest);
1595
1671
  }
1596
1672
  async#getSubsetText(font) {
1597
1673
  const parts = new Set;
@@ -1601,7 +1677,7 @@ class FontOptimizer {
1601
1677
  if (font.subsetText)
1602
1678
  parts.add(font.subsetText);
1603
1679
  for (const filePath of font.subsetFiles ?? []) {
1604
- 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);
1605
1681
  const file = Bun.file(abs);
1606
1682
  if (await file.exists())
1607
1683
  parts.add(await file.text());
@@ -1621,16 +1697,19 @@ class FontOptimizer {
1621
1697
  }
1622
1698
  async#collectAutoSubsetText() {
1623
1699
  const libPageRoots = (await this.#app.getPageRoots()).filter((root) => root.keyPrefix).map((root) => root.dir);
1624
- 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];
1625
1701
  const glob = new Bun.Glob("**/*.{ts,tsx,js,jsx,html,md}");
1626
1702
  const parts = [];
1627
- await Promise.all(roots.map(async (root) => {
1628
- if (!await stat2(root).then((entry) => entry.isDirectory(), () => false))
1629
- return;
1630
- 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())
1631
1711
  parts.push(await Bun.file(filePath).text());
1632
- }
1633
- }));
1712
+ }
1634
1713
  return parts.join("");
1635
1714
  }
1636
1715
  #rangeText(start, end) {
@@ -1701,358 +1780,255 @@ ${declarations.map(([prop, value]) => ` ${prop}: ${value};`).join(`
1701
1780
  name: this.#getFontFallbackName(font),
1702
1781
  font: fallback,
1703
1782
  metrics: fallbackMetrics,
1704
- "font-weight": String(face.weight),
1705
- "font-style": face.style
1706
- }));
1707
- }
1708
- return css.join("");
1709
- }
1710
- #buildRootVariableRule(font) {
1711
- return `:root { ${this.#getFontVariableName(font)}: ${this.#quote(font.name)}, ${this.#quote(this.#getFontFallbackName(font))}; }`;
1712
- }
1713
- #buildFontUtilityRules(fonts) {
1714
- const rules = [];
1715
- const seen = new Set;
1716
- for (const font of fonts) {
1717
- const className = font.className || `font-${font.name}`;
1718
- const selector = `.${this.#escapeCssClassName(className)}`;
1719
- const rule = `${selector} { font-family: var(${this.#getFontVariableName(font)}); }`;
1720
- if (seen.has(rule))
1721
- continue;
1722
- seen.add(rule);
1723
- rules.push(rule);
1724
- }
1725
- return rules.join(`
1726
- `);
1727
- }
1728
- #escapeCssClassName(value) {
1729
- return value.replace(/^-?\d|[^a-zA-Z0-9_-]/g, (part) => [...part].map((char) => `\\${char.codePointAt(0)?.toString(16)} `).join(""));
1730
- }
1731
- #toDeclarationEntries(declarations = []) {
1732
- return declarations.map((declaration) => [declaration.prop, declaration.value]);
1733
- }
1734
- #quote(value) {
1735
- return JSON.stringify(value);
1736
- }
1737
- }
1738
-
1739
- // pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
1740
- import path11 from "path";
1741
-
1742
- // pkgs/@akanjs/devkit/transforms/externalizeFrameworkPlugin.ts
1743
- import path10 from "path";
1744
- var DEFAULT_INCLUDE = ["akanjs/", "@apps/", "@libs/"];
1745
- var DEFAULT_EXCLUDE_EXACT = new Set(["akanjs/webkit", "@akanjs/cli", "@akanjs/devkit"]);
1746
- var DEFAULT_EXCLUDE_PREFIX = ["@akanjs/cli/", "@akanjs/devkit/"];
1747
- var OPTIONAL_BACKEND_EXTERNAL_EXACT = new Set([
1748
- "@libsql/client",
1749
- "bullmq",
1750
- "croner",
1751
- "ioredis",
1752
- "postgres",
1753
- "protobufjs"
1754
- ]);
1755
- var RUNTIME_EXTERNAL_EXACT = new Set([
1756
- "react",
1757
- "react-dom",
1758
- "react/jsx-runtime",
1759
- "react/jsx-dev-runtime",
1760
- "react-server-dom-webpack",
1761
- "react-server-dom-webpack/server.node",
1762
- "react-server-dom-webpack/client.node",
1763
- "react-server-dom-webpack/client.browser"
1764
- ]);
1765
- var RUNTIME_EXTERNAL_PREFIX = ["react-dom/", "react-server-dom-webpack/"];
1766
- var CANDIDATE_EXTS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
1767
- async function createExternalizeFrameworkPlugin(options) {
1768
- const tsconfig = await options.app.getTsConfig();
1769
- const includePrefixes = options.include ?? DEFAULT_INCLUDE;
1770
- const extraExact = new Set(options.extra ?? []);
1771
- const workspaceRoot = options.app.workspace.workspaceRoot;
1772
- const tsconfigPaths = tsconfig.compilerOptions.paths ?? {};
1773
- const rootEntries = Object.entries(tsconfigPaths).filter(([k]) => !k.endsWith("/*")).map(([k, v]) => ({ pkg: k, entryFile: v[0] ?? null })).filter((e) => e.entryFile !== null);
1774
- 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);
1775
- async function resolveWorkspaceSubpath(spec) {
1776
- if (!workspaceRoot)
1777
- return null;
1778
- for (const { prefix, replacements } of wildcardEntries) {
1779
- if (!spec.startsWith(prefix))
1780
- continue;
1781
- const suffix = spec.slice(prefix.length);
1782
- for (const repl of replacements) {
1783
- const replPath = repl?.endsWith("/*") ? repl.slice(0, -1) : repl ?? "";
1784
- if (!replPath)
1785
- continue;
1786
- const candidate = path10.resolve(workspaceRoot, replPath + suffix);
1787
- const hit = await firstExisting(candidate);
1788
- if (hit)
1789
- return hit;
1790
- }
1791
- }
1792
- for (const { pkg, entryFile } of rootEntries) {
1793
- if (spec !== pkg && !spec.startsWith(`${pkg}/`))
1794
- continue;
1795
- if (spec === pkg)
1796
- continue;
1797
- const suffix = spec.slice(pkg.length + 1);
1798
- const pkgDir = path10.dirname(path10.resolve(workspaceRoot, entryFile));
1799
- const candidate = path10.join(pkgDir, suffix);
1800
- const hit = await firstExisting(candidate);
1801
- if (hit)
1802
- return hit;
1803
- }
1804
- return null;
1805
- }
1806
- return {
1807
- name: "akan-externalize-framework",
1808
- setup(build) {
1809
- build.onResolve({ filter: /.*/ }, async (args) => {
1810
- const spec = args.path;
1811
- if (spec === "." || spec === ".." || spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/"))
1812
- return;
1813
- if (extraExact.has(spec) || DEFAULT_EXCLUDE_EXACT.has(spec))
1814
- return { path: spec, external: true };
1815
- for (const prefix of DEFAULT_EXCLUDE_PREFIX) {
1816
- if (spec.startsWith(prefix))
1817
- return { path: spec, external: true };
1818
- }
1819
- if (OPTIONAL_BACKEND_EXTERNAL_EXACT.has(spec))
1820
- return { path: spec, external: true };
1821
- if (RUNTIME_EXTERNAL_EXACT.has(spec))
1822
- return { path: spec, external: true };
1823
- for (const prefix of RUNTIME_EXTERNAL_PREFIX) {
1824
- if (spec.startsWith(prefix))
1825
- return { path: spec, external: true };
1826
- }
1827
- for (const prefix of includePrefixes) {
1828
- if (!spec.startsWith(prefix))
1829
- continue;
1830
- const resolved = await resolveWorkspaceSubpath(spec);
1831
- if (resolved)
1832
- return { path: resolved };
1833
- return;
1834
- }
1835
- return;
1836
- });
1783
+ "font-weight": String(face.weight),
1784
+ "font-style": face.style
1785
+ }));
1837
1786
  }
1838
- };
1839
- }
1840
- async function firstExisting(basePath) {
1841
- if (await Bun.file(basePath).exists())
1842
- return basePath;
1843
- for (const ext of CANDIDATE_EXTS) {
1844
- const candidate = `${basePath}${ext}`;
1845
- if (await Bun.file(candidate).exists())
1846
- return candidate;
1787
+ return css.join("");
1847
1788
  }
1848
- for (const ext of CANDIDATE_EXTS) {
1849
- const candidate = path10.join(basePath, `index${ext}`);
1850
- if (await Bun.file(candidate).exists())
1851
- return candidate;
1789
+ #buildRootVariableRule(font) {
1790
+ return `:root { ${this.#getFontVariableName(font)}: ${this.#quote(font.name)}, ${this.#quote(this.#getFontFallbackName(font))}; }`;
1852
1791
  }
1853
- return null;
1854
- }
1855
-
1856
- // pkgs/@akanjs/devkit/transforms/useClientBundlePlugin.ts
1857
- function createUseClientBundlePlugin(options = {}) {
1858
- return {
1859
- name: "akan-use-client-bundle",
1860
- setup(build) {
1861
- build.onLoad({ filter: /\.(tsx|ts|jsx|js)$/ }, async (args) => {
1862
- if (args.path.includes("/node_modules/"))
1863
- return;
1864
- let source;
1865
- try {
1866
- source = await Bun.file(args.path).text();
1867
- } catch {
1868
- return;
1869
- }
1870
- const stubbed = transformUseClient(source, {
1871
- path: args.path,
1872
- workspaceRoot: options.workspaceRoot
1873
- });
1874
- if (stubbed === null)
1875
- return;
1876
- return { contents: stubbed, loader: loaderFor(args.path) };
1877
- });
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);
1878
1803
  }
1879
- };
1880
- }
1881
- function loaderFor(absPath) {
1882
- if (absPath.endsWith(".tsx"))
1883
- return "tsx";
1884
- if (absPath.endsWith(".jsx"))
1885
- return "jsx";
1886
- if (absPath.endsWith(".ts"))
1887
- return "ts";
1888
- 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
+ }
1889
1816
  }
1890
1817
 
1891
- // pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
1892
- 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}";
1893
1824
 
1894
- class PagesBundleBuilder {
1825
+ class FontPruner {
1895
1826
  #app;
1896
- #command;
1897
- #pageEntries;
1898
- #started = Date.now();
1899
- constructor(app, command = "start", pageEntries) {
1827
+ #keepGlobs;
1828
+ #publicRoot;
1829
+ #artifactRoot;
1830
+ constructor(app, { keepFonts = [] } = {}) {
1900
1831
  this.#app = app;
1901
- this.#command = command;
1902
- this.#pageEntries = pageEntries;
1903
- }
1904
- async build() {
1905
- const akanConfig = await this.#app.getConfig();
1906
- const resolvedEntries = this.#pageEntries ?? await resolveSsrPageEntriesForApp(this.#app, await this.#app.getPageKeys());
1907
- const entrySource = PagesEntrySourceGenerator.generate(resolvedEntries);
1908
- const workspaceRoot = this.#app.workspace.workspaceRoot;
1909
- const result = await Bun.build({
1910
- entrypoints: [VIRTUAL_PAGES_ENTRY],
1911
- outdir: `${this.#artifactDir}/server`,
1912
- target: "bun",
1913
- format: "esm",
1914
- splitting: this.#splitting,
1915
- minify: this.#command === "build",
1916
- naming: {
1917
- entry: "pages-[hash].[ext]",
1918
- chunk: "chunks/[name]-[hash].[ext]",
1919
- asset: "assets/[name]-[hash].[ext]"
1920
- },
1921
- define: this.#define(),
1922
- plugins: [
1923
- PagesBundleBuilder.createPagesEntryPlugin(entrySource),
1924
- PagesBundleBuilder.createServerCssStubPlugin(),
1925
- PagesBundleBuilder.createServerUseClientFetchPlugin(),
1926
- await createExternalizeFrameworkPlugin({ app: this.#app, extra: akanConfig.externalLibs }),
1927
- akanConfig.barrelImports.length > 0 ? await createBarrelImportsPlugin(this.#app, {
1928
- pipeAfter: (source, args) => transformUseClient(source, {
1929
- path: args.path,
1930
- workspaceRoot
1931
- })
1932
- }) : createUseClientBundlePlugin({ workspaceRoot })
1933
- ]
1934
- });
1935
- if (!result.success)
1936
- throw new AggregateError(result.logs, "[PagesBundleBuilder] Bun.build failed");
1937
- const entryArtifact = result.outputs.find((a) => a.kind === "entry-point");
1938
- if (!entryArtifact)
1939
- throw new Error("[PagesBundleBuilder] Bun.build emitted no entry-point artifact");
1940
- const bundlePath = path11.resolve(entryArtifact.path);
1941
- const buildId = Date.now();
1942
- const outputBytes = result.outputs.reduce((sum, output) => sum + output.size, 0);
1943
- const chunkCount = result.outputs.filter((output) => output.kind === "chunk").length;
1944
- 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`);
1945
- return {
1946
- bundlePath,
1947
- buildId,
1948
- splitting: this.#splitting,
1949
- entryBytes: entryArtifact.size,
1950
- outputBytes,
1951
- outputCount: result.outputs.length,
1952
- chunkCount
1953
- };
1954
- }
1955
- get #artifactDir() {
1956
- 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;
1957
1874
  }
1958
- get #splitting() {
1959
- 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;
1960
1883
  }
1961
- #define() {
1962
- const nodeEnv = this.#command === "build" ? "production" : "development";
1963
- return {
1964
- "process.env.NODE_ENV": JSON.stringify(nodeEnv),
1965
- "process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("ssr"),
1966
- ...Object.fromEntries(Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]))
1967
- };
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;
1968
1893
  }
1969
- static createPagesEntryPlugin(source) {
1970
- return {
1971
- name: "akan-pages-entry",
1972
- setup(build) {
1973
- build.onResolve({ filter: /^akan-pages-entry$/ }, () => ({
1974
- path: VIRTUAL_PAGES_ENTRY,
1975
- namespace: "akan-virtual"
1976
- }));
1977
- build.onLoad({ filter: /^akan-pages-entry$/, namespace: "akan-virtual" }, () => ({
1978
- contents: source,
1979
- loader: "tsx"
1980
- }));
1981
- }
1982
- };
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;
1983
1913
  }
1984
- static createServerCssStubPlugin() {
1985
- return {
1986
- name: "akan-server-css-stub",
1987
- setup(build) {
1988
- build.onLoad({ filter: /\.css$/ }, () => ({
1989
- contents: "",
1990
- loader: "js"
1991
- }));
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("/")}`);
1992
1929
  }
1993
- };
1930
+ }
1994
1931
  }
1995
- static createServerUseClientFetchPlugin() {
1996
- return {
1997
- name: "akan-server-use-client-fetch",
1998
- setup(build) {
1999
- build.onLoad({ filter: /[/\\]lib[/\\]useClient\.(ts|tsx|js|jsx)$/ }, async (args) => {
2000
- const source = await Bun.file(args.path).text();
2001
- const transformed = PagesBundleBuilder.transformServerUseClientFetchSource(source);
2002
- if (transformed === source)
2003
- return;
2004
- return { contents: transformed, loader: loaderFor2(args.path) };
2005
- });
2006
- }
2007
- };
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
+ });
2008
1939
  }
2009
- static transformServerUseClientFetchSource(source) {
2010
- if (!source.includes(`with { type: "macro" }`) || !source.includes("FetchClient.build"))
2011
- return source;
2012
- 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);
2013
1979
  }
2014
1980
  }
2015
- function loaderFor2(absPath) {
2016
- if (absPath.endsWith(".tsx"))
2017
- return "tsx";
2018
- if (absPath.endsWith(".jsx"))
2019
- return "jsx";
2020
- if (absPath.endsWith(".ts"))
2021
- return "ts";
2022
- return "js";
2023
- }
2024
1981
 
2025
1982
  // pkgs/@akanjs/devkit/frontendBuild/precompressArtifacts.ts
2026
- import fs2 from "fs";
1983
+ import fs from "fs";
2027
1984
  import path12 from "path";
1985
+ import zlib from "zlib";
2028
1986
  var COMPRESSIBLE_EXTS = new Set([".css", ".html", ".js", ".json", ".svg"]);
2029
1987
  var MIN_COMPRESS_BYTES = 1024;
1988
+ var BROTLI_QUALITY_BY_EXT = { ".css": 11 };
1989
+ var DEFAULT_BROTLI_QUALITY = 9;
1990
+ var GZIP_LEVEL = 9;
2030
1991
  async function precompressArtifacts(app) {
2031
- const roots = [path12.join(app.dist.cwdPath, ".akan/artifact/client")];
2032
- const result = { files: 0, inputBytes: 0, outputBytes: 0 };
1992
+ const roots = [
1993
+ path12.join(app.dist.cwdPath, ".akan/artifact/client"),
1994
+ path12.join(app.dist.cwdPath, ".akan/artifact/styles")
1995
+ ];
1996
+ const result = { files: 0, inputBytes: 0, outputBytes: 0, brotliBytes: 0 };
2033
1997
  await Promise.all(roots.map((root) => precompressRoot(root, result)));
2034
1998
  if (result.files > 0) {
2035
- app.verbose(`[precompress] wrote ${result.files} gzip sidecars (${formatBytes(result.inputBytes)} -> ${formatBytes(result.outputBytes)})`);
1999
+ app.verbose(`[precompress] wrote ${result.files} sidecars (${formatBytes(result.inputBytes)} -> gzip ${formatBytes(result.outputBytes)} / br ${formatBytes(result.brotliBytes)})`);
2036
2000
  }
2037
2001
  return result;
2038
2002
  }
2039
2003
  async function precompressRoot(root, result) {
2040
- if (!fs2.existsSync(root) || !fs2.statSync(root).isDirectory())
2004
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory())
2041
2005
  return;
2042
2006
  const glob = new Bun.Glob("**/*");
2043
2007
  for await (const filePath of glob.scan({ cwd: root, absolute: true })) {
2044
2008
  if (!await shouldPrecompress(filePath))
2045
2009
  continue;
2046
2010
  const bytes = await Bun.file(filePath).bytes();
2047
- const gz = Bun.gzipSync(toArrayBuffer(bytes));
2048
- await Bun.write(`${filePath}.gz`, gz);
2011
+ const buffer = toArrayBuffer(bytes);
2012
+ const gz = Bun.gzipSync(buffer, { level: GZIP_LEVEL });
2013
+ const br = brotliCompress(buffer, path12.extname(filePath).toLowerCase());
2014
+ await Promise.all([Bun.write(`${filePath}.gz`, gz), Bun.write(`${filePath}.br`, br)]);
2049
2015
  result.files += 1;
2050
2016
  result.inputBytes += bytes.byteLength;
2051
2017
  result.outputBytes += gz.byteLength;
2018
+ result.brotliBytes += br.byteLength;
2052
2019
  }
2053
2020
  }
2021
+ function brotliCompress(buffer, ext) {
2022
+ const quality = BROTLI_QUALITY_BY_EXT[ext] ?? DEFAULT_BROTLI_QUALITY;
2023
+ return zlib.brotliCompressSync(buffer, {
2024
+ params: {
2025
+ [zlib.constants.BROTLI_PARAM_QUALITY]: quality,
2026
+ [zlib.constants.BROTLI_PARAM_SIZE_HINT]: buffer.byteLength
2027
+ }
2028
+ });
2029
+ }
2054
2030
  async function shouldPrecompress(filePath) {
2055
- if (filePath.endsWith(".gz"))
2031
+ if (filePath.endsWith(".gz") || filePath.endsWith(".br"))
2056
2032
  return false;
2057
2033
  if (!COMPRESSIBLE_EXTS.has(path12.extname(filePath).toLowerCase()))
2058
2034
  return false;
@@ -2119,6 +2095,7 @@ class SsrBaseArtifactBuilder {
2119
2095
  branches: [...akanConfig.branches],
2120
2096
  i18n: akanConfig.i18n,
2121
2097
  imageConfig: akanConfig.images,
2098
+ web: akanConfig.web,
2122
2099
  deepLinkAssociations: Object.values(akanConfig.mobile.targets).filter((target) => (target.deepLinks?.domains?.length ?? 0) > 0).map((target) => ({
2123
2100
  targetName: target.name,
2124
2101
  appId: target.appId,
@@ -2145,7 +2122,7 @@ class SsrBaseArtifactBuilder {
2145
2122
  const ssrBundle = await new ClientEntriesBundler({
2146
2123
  app: this.#app,
2147
2124
  entries: [rscSegmentOutletEntry],
2148
- ...RouteClientBuilder.resolveSsrClientExternalOptions(this.#command),
2125
+ ...RouteClientBuilder.resolveSsrClientBundleOptions(this.#command),
2149
2126
  outputSubdir: "client-ssr",
2150
2127
  command: this.#command
2151
2128
  }).bundle();
@@ -2197,13 +2174,13 @@ class SsrBaseArtifactBuilder {
2197
2174
  `);
2198
2175
  if (!cssText)
2199
2176
  return [];
2200
- return [this.#writeCssAsset(basePath, cssText)];
2177
+ return [this.#writeCssAsset(basePath, cssText, cssCompiler.importedStylesheetsByBasePath[basePath] ?? [])];
2201
2178
  })));
2202
2179
  if (optimizedFonts.files.length > 0)
2203
2180
  this.#app.verbose(`[base-artifact] optimized ${optimizedFonts.files.length} font files`);
2204
2181
  return { cssCompiler, optimizedFonts, cssAssets };
2205
2182
  }
2206
- async#writeCssAsset(basePath, cssText) {
2183
+ async#writeCssAsset(basePath, cssText, imported) {
2207
2184
  const cssAssetName = basePath || "root";
2208
2185
  const preparedCssText = await prepareCssAsset(this.#command, basePath, cssText);
2209
2186
  const cssHash = Bun.hash(`${basePath}
@@ -2213,220 +2190,16 @@ ${preparedCssText}`).toString(36);
2213
2190
  `/_akan/styles/${cssAssetName}-${cssHash}.css`
2214
2191
  ];
2215
2192
  await Bun.write(path13.join(this.#absArtifactDir, cssRelPath), preparedCssText);
2193
+ SsrBaseArtifactBuilder.#warnDroppedImports(this.#app, cssRelPath, preparedCssText, imported);
2216
2194
  this.#app.verbose(`[base-artifact] wrote ${preparedCssText.length} bytes of CSS for ${basePath} -> ${cssRelPath}`);
2217
2195
  return [basePath, { cssUrl, cssRelPath }];
2218
2196
  }
2219
- }
2220
- // pkgs/@akanjs/devkit/frontendBuild/styleGuard.ts
2221
- var PREFIX = "(?:bg|text|border(?:-[tblrxy])?(?:-[se])?|ring(?:-offset)?|fill|stroke|shadow|from|to|via|divide|outline|decoration|placeholder|caret|accent)";
2222
- var PALETTE = "(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)";
2223
- var LEAD = "(^|[\\s\"'`{(\\[:!])";
2224
- var TAIL = "(?=[\\s\"'`})\\]:/!,]|$)";
2225
- var RAW_PALETTE_RE = new RegExp(`${LEAD}((?:${PREFIX})-)${PALETTE}-\\d{2,3}${TAIL}`, "g");
2226
- var ARBITRARY_COLOR_RE = /\[(#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|oklch|oklab|lab|lch|hwb|color)\([^\]]*\))\]/g;
2227
- var INTERPOLATED_ARBITRARY_RE = new RegExp(`${LEAD}[a-z][a-z0-9-]*-\\[[^\\]\`]*\\$\\{`, "g");
2228
- var STYLE_OBJECT_RE = /style=\{\{([\s\S]*?)\}\}/g;
2229
- var STYLE_TAG_RE = /<style[^>]*>([\s\S]*?)<\/style>/g;
2230
- var INLINE_COLOR_LITERAL_RE = /#[0-9a-fA-F]{3,8}\b|(?:rgb|rgba|hsl|hsla|oklch|oklab|lab|lch|hwb)\(/g;
2231
- 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");
2232
- var ALL_RULES = [
2233
- "raw-palette",
2234
- "arbitrary-color",
2235
- "inline-color",
2236
- "daisyui-legacy",
2237
- "interpolated-arbitrary"
2238
- ];
2239
-
2240
- class StyleGuard {
2241
- run(files) {
2242
- const out = [];
2243
- for (const file of files) {
2244
- const directives = this.#parseDirectives(file.content);
2245
- const scan = this.#stripComments(file.content);
2246
- const local = [];
2247
- this.#scanClassLiterals(file, scan, local);
2248
- this.#scanInlineColors(file, scan, local);
2249
- for (const v of local)
2250
- if (!this.#isSuppressed(v, directives))
2251
- out.push(v);
2252
- }
2253
- return out;
2254
- }
2255
- #stripComments(src) {
2256
- const out = src.split("");
2257
- const blank = (i2) => {
2258
- if (src[i2] !== `
2259
- `)
2260
- out[i2] = " ";
2261
- };
2262
- let state = "code";
2263
- let i = 0;
2264
- while (i < src.length) {
2265
- const c = src[i];
2266
- const d = src[i + 1];
2267
- if (state === "code") {
2268
- if (c === "/" && d === "/") {
2269
- blank(i);
2270
- blank(i + 1);
2271
- state = "line";
2272
- i += 2;
2273
- } else if (c === "/" && d === "*") {
2274
- blank(i);
2275
- blank(i + 1);
2276
- state = "block";
2277
- i += 2;
2278
- } else {
2279
- if (c === "'")
2280
- state = "squote";
2281
- else if (c === '"')
2282
- state = "dquote";
2283
- else if (c === "`")
2284
- state = "template";
2285
- i++;
2286
- }
2287
- } else if (state === "line") {
2288
- if (c === `
2289
- `)
2290
- state = "code";
2291
- else
2292
- blank(i);
2293
- i++;
2294
- } else if (state === "block") {
2295
- if (c === "*" && d === "/") {
2296
- blank(i);
2297
- blank(i + 1);
2298
- state = "code";
2299
- i += 2;
2300
- } else {
2301
- blank(i);
2302
- i++;
2303
- }
2304
- } else {
2305
- const close = state === "squote" ? "'" : state === "dquote" ? '"' : "`";
2306
- if (c === "\\")
2307
- i += 2;
2308
- else {
2309
- if (c === close)
2310
- state = "code";
2311
- i++;
2312
- }
2313
- }
2314
- }
2315
- return out.join("");
2316
- }
2317
- #parseDirectives(content) {
2318
- const directives = { file: null, nextLine: new Map };
2319
- const lines = content.split(`
2320
- `);
2321
- for (let i = 0;i < lines.length; i++) {
2322
- const line = lines[i];
2323
- const nextLineMatch = line.match(/styleguard-disable-next-line\b(.*)$/);
2324
- if (nextLineMatch) {
2325
- 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}:`)))
2326
2200
  continue;
2327
- }
2328
- const fileMatch = line.match(/styleguard-disable\b(.*)$/);
2329
- if (fileMatch)
2330
- directives.file = this.#mergeFileScope(directives.file, this.#extractRules(fileMatch[1]));
2331
- }
2332
- return directives;
2333
- }
2334
- #extractRules(tail) {
2335
- const found = ALL_RULES.filter((rule) => tail.includes(rule));
2336
- return found.length === 0 ? "all" : new Set(found);
2337
- }
2338
- #mergeFileScope(prev, next) {
2339
- if (prev === "all" || next === "all")
2340
- return "all";
2341
- return new Set([...prev ?? [], ...next]);
2342
- }
2343
- #isSuppressed(v, directives) {
2344
- if (directives.file === "all" || directives.file?.has(v.rule))
2345
- return true;
2346
- const nextLine = directives.nextLine.get(v.line);
2347
- return nextLine === "all" || (nextLine?.has(v.rule) ?? false);
2348
- }
2349
- #scanClassLiterals(file, scan, out) {
2350
- for (const m of scan.matchAll(RAW_PALETTE_RE)) {
2351
- out.push(this.#violation(file, m.index ?? 0, {
2352
- rule: "raw-palette",
2353
- severity: "error",
2354
- 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."
2355
- }));
2356
- }
2357
- for (const m of scan.matchAll(ARBITRARY_COLOR_RE)) {
2358
- out.push(this.#violation(file, m.index ?? 0, {
2359
- rule: "arbitrary-color",
2360
- severity: "error",
2361
- 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."
2362
- }));
2363
- }
2364
- for (const m of scan.matchAll(DAISYUI_LEGACY_RE)) {
2365
- out.push(this.#violation(file, m.index ?? 0, {
2366
- rule: "daisyui-legacy",
2367
- severity: "error",
2368
- 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."
2369
- }));
2370
- }
2371
- for (const m of scan.matchAll(INTERPOLATED_ARBITRARY_RE)) {
2372
- out.push(this.#violation(file, m.index ?? 0, {
2373
- rule: "interpolated-arbitrary",
2374
- severity: "error",
2375
- 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."
2376
- }));
2377
- }
2378
- }
2379
- #scanInlineColors(file, scan, out) {
2380
- const push = (index) => out.push(this.#violation(file, index, {
2381
- rule: "inline-color",
2382
- severity: "error",
2383
- 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."
2384
- }));
2385
- for (const region of [STYLE_OBJECT_RE, STYLE_TAG_RE]) {
2386
- for (const m of scan.matchAll(region)) {
2387
- const inner = m[1] ?? "";
2388
- const innerStart = (m.index ?? 0) + m[0].indexOf(inner);
2389
- for (const lit of inner.matchAll(INLINE_COLOR_LITERAL_RE)) {
2390
- push(innerStart + (lit.index ?? 0));
2391
- }
2392
- }
2393
- }
2394
- }
2395
- #violation(file, index, base) {
2396
- const before = file.content.slice(0, index);
2397
- const line = before.length === 0 ? 1 : before.split(`
2398
- `).length;
2399
- const lineStart = before.lastIndexOf(`
2400
- `) + 1;
2401
- const lineEnd = file.content.indexOf(`
2402
- `, index);
2403
- const snippet = file.content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd).trim();
2404
- return { ...base, path: file.path, line, snippet };
2405
- }
2406
- static countNamedComponentClasses(css) {
2407
- const names = new Set;
2408
- const layerRe = /@layer\s+components\s*\{/g;
2409
- for (const layer of css.matchAll(layerRe)) {
2410
- const bodyStart = (layer.index ?? 0) + layer[0].length;
2411
- const body = StyleGuard.#extractBalanced(css, bodyStart);
2412
- for (const sel of body.matchAll(/(^|[\s}])\.(-?[a-zA-Z_][\w-]*)/g))
2413
- names.add(sel[2]);
2414
- }
2415
- return { count: names.size, names: [...names].sort() };
2416
- }
2417
- static #extractBalanced(css, start) {
2418
- let depth = 1;
2419
- for (let i = start;i < css.length; i++) {
2420
- const ch = css[i];
2421
- if (ch === "{")
2422
- depth++;
2423
- else if (ch === "}") {
2424
- depth--;
2425
- if (depth === 0)
2426
- return css.slice(start, i);
2427
- }
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}`);
2428
2202
  }
2429
- return css.slice(start);
2430
2203
  }
2431
2204
  }
2432
- export { AllRoutesBuilder, CsrArtifactBuilder, CssCompiler, FontOptimizer, PagesBundleBuilder, precompressArtifacts, SsrBaseArtifactBuilder };
2205
+ export { AllRoutesBuilder, CssCompiler, PagesBundleBuilder, CsrArtifactBuilder, FontOptimizer, FontPruner, precompressArtifacts, SsrBaseArtifactBuilder };