@akanjs/cli 2.4.1-rc.7 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.build-stamp +1 -1
- package/{agent.command-t4346579.js → agent.command-sa12ccz3.js} +2 -2
- package/{application.command-zkmeemdt.js → application.command-jv63vv7z.js} +4 -4
- package/{applicationBuildRunner-5w8j9yp7.js → applicationBuildRunner-vzjdgv1g.js} +3 -2
- package/buildBatch.proc.js +3 -2
- package/{cloud.command-5ztqxppm.js → cloud.command-8z2nc6b2.js} +7 -7
- package/{context.command-smwxwgwf.js → context.command-0m854xme.js} +8 -8
- package/{guideline.command-1r0esdjs.js → guideline.command-dsdpae45.js} +1 -1
- package/incrementalBuilder.proc.js +3 -3
- package/{index-3sd2vape.js → index-1b9f27pm.js} +1 -1
- package/{index-fh990y67.js → index-3a4vntr0.js} +1 -1
- package/{index-jbtn8h1y.js → index-81epkybm.js} +1 -1
- package/{index-0fn1r7gg.js → index-bjpxzr6s.js} +31 -4
- package/{index-ahpcr9ss.js → index-d2e2y85b.js} +1 -1
- package/{index-0jwvs8vp.js → index-e3b8ms64.js} +6 -6
- package/{index-s9yajzkj.js → index-eja0sbm6.js} +4 -4
- package/{index-wh201a76.js → index-h6pevatb.js} +2 -2
- package/{index-2v6a4wd5.js → index-m04as81s.js} +2 -2
- package/index-m0vg9n8z.js +2219 -0
- package/{index-n5y2gbf2.js → index-n2mp0f48.js} +200 -19
- package/{index-nv7et4t3.js → index-p9f1rpbp.js} +1 -1
- package/{index-qpz4csbs.js → index-phj3ewxv.js} +7 -1
- package/{index-pya1h7wx.js → index-q4sc46ec.js} +1 -1
- package/{index-y4kg682s.js → index-qacv44mm.js} +1 -1
- package/index-rh16j3c7.js +2166 -0
- package/{index-gr4cjv99.js → index-zw4aqvwh.js} +2 -2
- package/index.js +17 -17
- package/{library.command-jrew04m6.js → library.command-fhx5ce8n.js} +2 -2
- package/{localRegistry.command-m08dkm1b.js → localRegistry.command-18f61945.js} +6 -6
- package/{module.command-p98t7522.js → module.command-29wvnb56.js} +3 -3
- package/{package.command-57dyrfmn.js → package.command-46r1dncm.js} +2 -2
- package/package.json +2 -2
- package/{page.command-tas6f3na.js → page.command-vxvqdzc0.js} +2 -2
- package/{primitive.command-bjrbsakw.js → primitive.command-yk622wmt.js} +4 -4
- package/{quality.command-zavaafbw.js → quality.command-p0mb5xwt.js} +1 -1
- package/{repair.command-je57wx67.js → repair.command-nptckp6w.js} +2 -2
- package/{scalar.command-gqqyy1kr.js → scalar.command-bss14hez.js} +2 -2
- package/templates/workspaceRoot/.cursor/rules/coding-comments.mdc.template +19 -0
- package/{workflow.command-d6mrc47s.js → workflow.command-7fhqb33d.js} +6 -6
- package/{workspace.command-bbg96z8k.js → workspace.command-xkcec7ky.js} +15 -15
- package/index-khzzttv1.js +0 -4274
|
@@ -0,0 +1,2219 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
BarrelAnalyzer,
|
|
4
|
+
ClientEntriesBundler,
|
|
5
|
+
GraphClientEntryDiscovery,
|
|
6
|
+
RouteClientBuilder,
|
|
7
|
+
VENDOR_SPECIFIERS,
|
|
8
|
+
createBarrelImportsPlugin,
|
|
9
|
+
createTsconfigPackageResolver,
|
|
10
|
+
rewriteBarrelImports,
|
|
11
|
+
transformUseClient
|
|
12
|
+
} from "./index-rh16j3c7.js";
|
|
13
|
+
import {
|
|
14
|
+
__require
|
|
15
|
+
} from "./index-r24hmh0q.js";
|
|
16
|
+
|
|
17
|
+
// 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
|
+
}
|
|
272
|
+
|
|
273
|
+
// pkgs/@akanjs/devkit/artifact/routeSeedIndex.ts
|
|
274
|
+
import path2 from "path";
|
|
275
|
+
import { assertUniqueRoutePatterns, compareRouteSpecificity, parseRouteModuleKey } from "akanjs/common";
|
|
276
|
+
function computeRouteSeedIndex(pageEntries) {
|
|
277
|
+
const layoutsByPrefix = new Map;
|
|
278
|
+
const pagesBySegments = [];
|
|
279
|
+
for (const { key, moduleAbsPath, seedAbsPaths } of pageEntries) {
|
|
280
|
+
const parsed = parseRouteModuleKey(key);
|
|
281
|
+
const files = [path2.resolve(moduleAbsPath), ...(seedAbsPaths ?? []).map((seed) => path2.resolve(seed))];
|
|
282
|
+
if (parsed.kind === "layout" || parsed.kind === "overrides") {
|
|
283
|
+
const prefix = parsed.routeSegments.join("/");
|
|
284
|
+
const prev = layoutsByPrefix.get(prefix) ?? [];
|
|
285
|
+
layoutsByPrefix.set(prefix, [...prev, ...files]);
|
|
286
|
+
} else if (parsed.kind === "page") {
|
|
287
|
+
pagesBySegments.push({
|
|
288
|
+
key,
|
|
289
|
+
pattern: parsed.pattern,
|
|
290
|
+
segments: parsed.routeSegments,
|
|
291
|
+
files,
|
|
292
|
+
includeOwnLayout: parsed.leaf === "_index"
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
assertUniqueRoutePatterns(pagesBySegments);
|
|
297
|
+
const rootLayouts = layoutsByPrefix.get("") ?? [];
|
|
298
|
+
const globalLayoutFiles = rootLayouts;
|
|
299
|
+
const seedEntries = [];
|
|
300
|
+
for (const { pattern, segments, files, includeOwnLayout } of pagesBySegments) {
|
|
301
|
+
const layouts = [];
|
|
302
|
+
const maxPrefixLength = includeOwnLayout ? segments.length : Math.max(segments.length - 1, 0);
|
|
303
|
+
for (let i = 0;i <= maxPrefixLength; i++) {
|
|
304
|
+
const prefix = segments.slice(0, i).join("/");
|
|
305
|
+
const layoutFiles = layoutsByPrefix.get(prefix);
|
|
306
|
+
if (layoutFiles)
|
|
307
|
+
layouts.push(...layoutFiles);
|
|
308
|
+
}
|
|
309
|
+
const seeds = Array.from(new Set([...layouts, ...files]));
|
|
310
|
+
const routeId = pattern || "/";
|
|
311
|
+
seedEntries.push({ routeId, pattern: routeId, seeds });
|
|
312
|
+
}
|
|
313
|
+
seedEntries.sort((a, b) => compareRouteSpecificity(a.pattern, b.pattern));
|
|
314
|
+
return { entries: seedEntries, globalLayoutFiles };
|
|
315
|
+
}
|
|
316
|
+
var ROUTE_SEED_INDEX_JSON = "route-seed-index.json";
|
|
317
|
+
function serializeRouteSeedIndexForArtifact(index, artifactDir, options = {}) {
|
|
318
|
+
const normalizedArtifactDir = path2.resolve(artifactDir);
|
|
319
|
+
if (options.production) {
|
|
320
|
+
return {
|
|
321
|
+
entries: index.entries.map((entry) => ({ routeId: entry.routeId }))
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
entries: index.entries.map((entry) => ({
|
|
326
|
+
...entry,
|
|
327
|
+
seeds: entry.seeds.map((seed) => serializeArtifactPath(seed, normalizedArtifactDir))
|
|
328
|
+
})),
|
|
329
|
+
globalLayoutFiles: index.globalLayoutFiles.map((file) => serializeArtifactPath(file, normalizedArtifactDir))
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
async function saveRouteSeedIndex(artifactDir, index, options = {}) {
|
|
333
|
+
const absPath = path2.join(path2.resolve(artifactDir), ROUTE_SEED_INDEX_JSON);
|
|
334
|
+
await Bun.write(absPath, `${JSON.stringify(serializeRouteSeedIndexForArtifact(index, artifactDir, options), null, 2)}
|
|
335
|
+
`);
|
|
336
|
+
return absPath;
|
|
337
|
+
}
|
|
338
|
+
function serializeArtifactPath(artifactPath, artifactDir) {
|
|
339
|
+
if (!path2.isAbsolute(artifactPath))
|
|
340
|
+
return artifactPath;
|
|
341
|
+
return path2.relative(artifactDir, artifactPath).split(path2.sep).join("/");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// pkgs/@akanjs/devkit/frontendBuild/routesManifestArtifactSerializer.ts
|
|
345
|
+
import path3 from "path";
|
|
346
|
+
|
|
347
|
+
class RoutesManifestArtifactSerializer {
|
|
348
|
+
#manifest;
|
|
349
|
+
#artifactDir;
|
|
350
|
+
#production;
|
|
351
|
+
constructor(manifest, artifactDir, options = {}) {
|
|
352
|
+
this.#manifest = manifest;
|
|
353
|
+
this.#artifactDir = path3.resolve(artifactDir);
|
|
354
|
+
this.#production = options.production ?? false;
|
|
355
|
+
}
|
|
356
|
+
static serialize(manifest, artifactDir, options = {}) {
|
|
357
|
+
return new RoutesManifestArtifactSerializer(manifest, artifactDir, options).serialize();
|
|
358
|
+
}
|
|
359
|
+
serialize() {
|
|
360
|
+
const serialized = {
|
|
361
|
+
...this.#manifest,
|
|
362
|
+
knownEntries: this.#manifest.knownEntries.map((entry) => this.#serializeArtifactPath(entry))
|
|
363
|
+
};
|
|
364
|
+
if (this.#production)
|
|
365
|
+
delete serialized.knownEntries;
|
|
366
|
+
return {
|
|
367
|
+
...serialized,
|
|
368
|
+
clientManifest: Object.fromEntries(Object.entries(this.#manifest.clientManifest).map(([key, row]) => [this.#serializeArtifactPath(key), row])),
|
|
369
|
+
ssrManifest: {
|
|
370
|
+
...this.#manifest.ssrManifest,
|
|
371
|
+
moduleMap: Object.fromEntries(Object.entries(this.#manifest.ssrManifest.moduleMap).map(([entryUrl, byName]) => [
|
|
372
|
+
entryUrl,
|
|
373
|
+
Object.fromEntries(Object.entries(byName).map(([name, entry]) => [
|
|
374
|
+
name,
|
|
375
|
+
{
|
|
376
|
+
...entry,
|
|
377
|
+
id: this.#serializeArtifactPath(entry.id),
|
|
378
|
+
chunks: entry.chunks.map((chunk) => this.#serializeArtifactPath(chunk))
|
|
379
|
+
}
|
|
380
|
+
]))
|
|
381
|
+
]))
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
#serializeArtifactPath(artifactPath) {
|
|
386
|
+
if (!path3.isAbsolute(artifactPath))
|
|
387
|
+
return artifactPath;
|
|
388
|
+
return path3.relative(this.#artifactDir, artifactPath).split(path3.sep).join("/");
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// pkgs/@akanjs/devkit/frontendBuild/allRoutesBuilder.ts
|
|
393
|
+
class AllRoutesBuilder {
|
|
394
|
+
#app;
|
|
395
|
+
#artifact;
|
|
396
|
+
#command;
|
|
397
|
+
#artifactDir;
|
|
398
|
+
#merged = {
|
|
399
|
+
clientManifest: {},
|
|
400
|
+
ssrManifest: { moduleLoading: null, moduleMap: {} },
|
|
401
|
+
knownEntries: []
|
|
402
|
+
};
|
|
403
|
+
#knownSet = new Set;
|
|
404
|
+
#routeIds = [];
|
|
405
|
+
#discovery = null;
|
|
406
|
+
constructor(app, artifact, command = "start") {
|
|
407
|
+
this.#app = app;
|
|
408
|
+
this.#artifact = artifact;
|
|
409
|
+
this.#command = command;
|
|
410
|
+
this.#artifactDir = `${command === "build" ? app.dist.cwdPath : app.cwdPath}/.akan/artifact`;
|
|
411
|
+
}
|
|
412
|
+
async build() {
|
|
413
|
+
const pageKeys = await this.#app.getPageKeys();
|
|
414
|
+
const pageEntries = await resolveSsrPageEntriesForApp(this.#app, pageKeys);
|
|
415
|
+
const seedIndex = computeRouteSeedIndex(pageEntries);
|
|
416
|
+
this.#app.verbose(`[build-all] discovered ${seedIndex.entries.length} routes`);
|
|
417
|
+
this.#discovery = await GraphClientEntryDiscovery.create(this.#app);
|
|
418
|
+
for (const entry of seedIndex.entries) {
|
|
419
|
+
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);
|
|
422
|
+
}
|
|
423
|
+
this.#merged.knownEntries = Array.from(this.#knownSet);
|
|
424
|
+
const manifest = {
|
|
425
|
+
routeIds: this.#routeIds,
|
|
426
|
+
...this.#merged
|
|
427
|
+
};
|
|
428
|
+
const manifestPath = path4.join(path4.resolve(this.#artifactDir), "routes-manifest.json");
|
|
429
|
+
await Bun.write(manifestPath, `${JSON.stringify(RoutesManifestArtifactSerializer.serialize(manifest, this.#artifactDir, {
|
|
430
|
+
production: this.#command === "build"
|
|
431
|
+
}), null, 2)}
|
|
432
|
+
`);
|
|
433
|
+
this.#app.verbose(`[build-all] wrote ${manifestPath} routes=${this.#routeIds.length} entries=${this.#merged.knownEntries.length}`);
|
|
434
|
+
return { manifest, manifestPath, seedIndex };
|
|
435
|
+
}
|
|
436
|
+
async#buildRoute(routeId, seeds) {
|
|
437
|
+
if (!this.#discovery)
|
|
438
|
+
throw new Error("[build-all] client entry discovery is not initialized");
|
|
439
|
+
const started = Date.now();
|
|
440
|
+
const delta = await new RouteClientBuilder({
|
|
441
|
+
app: this.#app,
|
|
442
|
+
routeId,
|
|
443
|
+
seeds,
|
|
444
|
+
artifact: this.#artifact,
|
|
445
|
+
knownEntries: this.#knownSet,
|
|
446
|
+
discovery: this.#discovery,
|
|
447
|
+
command: this.#command
|
|
448
|
+
}).build();
|
|
449
|
+
this.#app.verbose(`[build-all] ${routeId} +${delta.newEntries.length} entries (${Date.now() - started}ms)`);
|
|
450
|
+
return delta;
|
|
451
|
+
}
|
|
452
|
+
#mergeRoute(routeId, delta) {
|
|
453
|
+
for (const [key, row] of Object.entries(delta.manifestDelta))
|
|
454
|
+
this.#merged.clientManifest[key] = row;
|
|
455
|
+
for (const [url, byName] of Object.entries(delta.ssrManifestDelta.moduleMap)) {
|
|
456
|
+
this.#merged.ssrManifest.moduleMap[url] = byName;
|
|
457
|
+
}
|
|
458
|
+
for (const abs of delta.newEntries)
|
|
459
|
+
this.#knownSet.add(abs);
|
|
460
|
+
this.#routeIds.push(routeId);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
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";
|
|
470
|
+
import path5 from "path";
|
|
471
|
+
import ts from "typescript";
|
|
472
|
+
|
|
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
|
+
`;
|
|
491
|
+
}
|
|
492
|
+
static generateStatic(pageEntries) {
|
|
493
|
+
return new PagesEntrySourceGenerator(pageEntries).generateStatic();
|
|
494
|
+
}
|
|
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
|
+
`;
|
|
511
|
+
}
|
|
512
|
+
static #toImportSpecifier(moduleAbsPath) {
|
|
513
|
+
return path5.resolve(moduleAbsPath).split(path5.sep).join("/");
|
|
514
|
+
}
|
|
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;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
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;
|
|
550
|
+
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
|
+
}
|
|
560
|
+
}
|
|
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;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
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;
|
|
583
|
+
}
|
|
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 };
|
|
626
|
+
}
|
|
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;
|
|
890
|
+
}
|
|
891
|
+
#resolveWithBun(id, fromBase) {
|
|
892
|
+
for (const base of this.#resolutionBases(fromBase)) {
|
|
893
|
+
try {
|
|
894
|
+
const resolved = Bun.resolveSync(id, base);
|
|
895
|
+
if (CssImportResolver.isCssFile(resolved))
|
|
896
|
+
return resolved;
|
|
897
|
+
} catch {}
|
|
898
|
+
}
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
#resolveWithRequire(id, fromBase) {
|
|
902
|
+
for (const base of this.#resolutionBases(fromBase)) {
|
|
903
|
+
try {
|
|
904
|
+
const resolved = __require.resolve(id, { paths: [base] });
|
|
905
|
+
if (CssImportResolver.isCssFile(resolved))
|
|
906
|
+
return resolved;
|
|
907
|
+
} catch {}
|
|
908
|
+
}
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
911
|
+
async#resolveWithTsconfig(id) {
|
|
912
|
+
const exact = this.#paths[id];
|
|
913
|
+
if (exact) {
|
|
914
|
+
for (const repl of exact) {
|
|
915
|
+
const resolved = await this.#firstExisting(path7.resolve(this.#workspaceRoot, repl));
|
|
916
|
+
if (resolved)
|
|
917
|
+
return resolved;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
for (const { prefix, replacements } of this.#wildcardEntries) {
|
|
921
|
+
if (!id.startsWith(prefix))
|
|
922
|
+
continue;
|
|
923
|
+
const suffix = id.slice(prefix.length);
|
|
924
|
+
for (const repl of replacements) {
|
|
925
|
+
const replPath = repl.endsWith("/*") ? repl.slice(0, -1) : repl;
|
|
926
|
+
const resolved = await this.#firstExisting(path7.resolve(this.#workspaceRoot, replPath + suffix));
|
|
927
|
+
if (resolved)
|
|
928
|
+
return resolved;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
return null;
|
|
932
|
+
}
|
|
933
|
+
async#resolvePackageStyle(id, fromBase) {
|
|
934
|
+
const pkgName = CssImportResolver.getPackageName(id);
|
|
935
|
+
if (!pkgName)
|
|
936
|
+
return null;
|
|
937
|
+
for (const base of this.#resolutionBases(fromBase)) {
|
|
938
|
+
try {
|
|
939
|
+
const pkgPath = __require.resolve(`${pkgName}/package.json`, { paths: [base] });
|
|
940
|
+
const resolved = await this.#resolvePackageStyleFromPackageJson(id, pkgName, pkgPath);
|
|
941
|
+
if (resolved)
|
|
942
|
+
return resolved;
|
|
943
|
+
} catch {}
|
|
944
|
+
}
|
|
945
|
+
for (const pkgPath of this.#packageJsonCandidates(pkgName)) {
|
|
946
|
+
const resolved = await this.#resolvePackageStyleFromPackageJson(id, pkgName, pkgPath);
|
|
947
|
+
if (resolved)
|
|
948
|
+
return resolved;
|
|
949
|
+
}
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
async#resolvePackageStyleFromPackageJson(id, pkgName, pkgPath) {
|
|
953
|
+
try {
|
|
954
|
+
if (!await Bun.file(pkgPath).exists())
|
|
955
|
+
return null;
|
|
956
|
+
const pkgDir = path7.dirname(pkgPath);
|
|
957
|
+
const pkg = await Bun.file(pkgPath).json();
|
|
958
|
+
const subpath = id === pkgName ? "." : `.${id.slice(pkgName.length)}`;
|
|
959
|
+
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));
|
|
962
|
+
} catch {
|
|
963
|
+
return null;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
#resolutionBases(fromBase) {
|
|
967
|
+
return [fromBase, this.#workspaceRoot, path7.dirname(Bun.main), path7.resolve(path7.dirname(Bun.main), "../..")];
|
|
968
|
+
}
|
|
969
|
+
#packageJsonCandidates(pkgName) {
|
|
970
|
+
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")
|
|
975
|
+
];
|
|
976
|
+
}
|
|
977
|
+
async#firstExisting(basePath) {
|
|
978
|
+
for (const suffix of CSS_IMPORT_EXTS) {
|
|
979
|
+
const candidate = `${basePath}${suffix}`;
|
|
980
|
+
if (await Bun.file(candidate).exists())
|
|
981
|
+
return candidate;
|
|
982
|
+
}
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
985
|
+
static getPackageName(id) {
|
|
986
|
+
const parts = id.split("/");
|
|
987
|
+
if (id.startsWith("@"))
|
|
988
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
|
|
989
|
+
return parts[0] ?? null;
|
|
990
|
+
}
|
|
991
|
+
static isCssFile(filePath) {
|
|
992
|
+
return path7.extname(filePath) === ".css";
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// pkgs/@akanjs/devkit/frontendBuild/cssCompiler.ts
|
|
997
|
+
var SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
998
|
+
var NON_SOURCE_EXT_RE = /\.(json|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp3|mp4|wav)$/i;
|
|
999
|
+
var NODE_MODULES_RE = /[\\/]node_modules[\\/]/;
|
|
1000
|
+
var AKANJS_NODE_MODULE_RE = /[\\/]node_modules[\\/]akanjs[\\/]/;
|
|
1001
|
+
|
|
1002
|
+
class CssCompiler {
|
|
1003
|
+
#logger = new Logger("CssCompiler");
|
|
1004
|
+
#transpiler = new Bun.Transpiler({ loader: "tsx" });
|
|
1005
|
+
#app;
|
|
1006
|
+
#cssImportResolver = null;
|
|
1007
|
+
constructor(app) {
|
|
1008
|
+
this.#app = app;
|
|
1009
|
+
}
|
|
1010
|
+
get #candidateCachePath() {
|
|
1011
|
+
return path8.join(this.#app.cwdPath, ".akan/cache/cssCandidates.json");
|
|
1012
|
+
}
|
|
1013
|
+
#cssText = null;
|
|
1014
|
+
#cssTextByBasePath = null;
|
|
1015
|
+
#fileExistsCache = new Map;
|
|
1016
|
+
#resolvedFileCache = new Map;
|
|
1017
|
+
#resolvedSpecifierCache = new Map;
|
|
1018
|
+
#fileExists(absPath) {
|
|
1019
|
+
let cached = this.#fileExistsCache.get(absPath);
|
|
1020
|
+
if (!cached) {
|
|
1021
|
+
cached = Bun.file(absPath).exists();
|
|
1022
|
+
this.#fileExistsCache.set(absPath, cached);
|
|
1023
|
+
}
|
|
1024
|
+
return cached;
|
|
1025
|
+
}
|
|
1026
|
+
#resolveSourceFileCandidate(absPathNoExt) {
|
|
1027
|
+
let cached = this.#resolvedFileCache.get(absPathNoExt);
|
|
1028
|
+
if (cached)
|
|
1029
|
+
return cached;
|
|
1030
|
+
cached = (async () => {
|
|
1031
|
+
if (await this.#fileExists(absPathNoExt))
|
|
1032
|
+
return isSourceFile(absPathNoExt) ? absPathNoExt : null;
|
|
1033
|
+
for (const ext of SOURCE_EXTS) {
|
|
1034
|
+
const filePath = `${absPathNoExt}${ext}`;
|
|
1035
|
+
if (await this.#fileExists(filePath))
|
|
1036
|
+
return filePath;
|
|
1037
|
+
}
|
|
1038
|
+
for (const ext of SOURCE_EXTS) {
|
|
1039
|
+
const filePath = path8.join(absPathNoExt, `index${ext}`);
|
|
1040
|
+
if (await this.#fileExists(filePath))
|
|
1041
|
+
return filePath;
|
|
1042
|
+
}
|
|
1043
|
+
return null;
|
|
1044
|
+
})();
|
|
1045
|
+
this.#resolvedFileCache.set(absPathNoExt, cached);
|
|
1046
|
+
return cached;
|
|
1047
|
+
}
|
|
1048
|
+
async getCss({ refresh } = {}) {
|
|
1049
|
+
if (this.#cssText !== null && !refresh)
|
|
1050
|
+
return this.#cssText;
|
|
1051
|
+
const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh });
|
|
1052
|
+
this.#cssText = await this.compileCss(cssPaths, sourcePaths);
|
|
1053
|
+
return this.#cssText;
|
|
1054
|
+
}
|
|
1055
|
+
async getCssByBasePath({ refresh } = {}) {
|
|
1056
|
+
if (this.#cssTextByBasePath !== null && !refresh)
|
|
1057
|
+
return this.#cssTextByBasePath;
|
|
1058
|
+
const akanConfig = await this.#app.getConfig({ refresh });
|
|
1059
|
+
const pageKeys = await this.#app.getPageKeys({ refresh });
|
|
1060
|
+
const basePaths = [...akanConfig.basePaths];
|
|
1061
|
+
const rootPageKeys = pageKeys.filter((pageKey) => getPageKeyBasePath(pageKey, basePaths) === null);
|
|
1062
|
+
const cssEntries = await Promise.all([
|
|
1063
|
+
(async () => {
|
|
1064
|
+
if (rootPageKeys.length === 0)
|
|
1065
|
+
return ["", ""];
|
|
1066
|
+
const started = Date.now();
|
|
1067
|
+
const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: rootPageKeys });
|
|
1068
|
+
const css = await this.compileCss(cssPaths, sourcePaths);
|
|
1069
|
+
this.#logger.verbose(`css base=root paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`);
|
|
1070
|
+
return ["", css];
|
|
1071
|
+
})(),
|
|
1072
|
+
...basePaths.map(async (basePath) => {
|
|
1073
|
+
const basePathPageKeys = pageKeys.filter((pageKey) => getPageKeyBasePath(pageKey, basePaths) === basePath);
|
|
1074
|
+
if (basePathPageKeys.length === 0)
|
|
1075
|
+
return [basePath, ""];
|
|
1076
|
+
const started = Date.now();
|
|
1077
|
+
const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: basePathPageKeys });
|
|
1078
|
+
const css = await this.compileCss(cssPaths, sourcePaths);
|
|
1079
|
+
this.#logger.verbose(`css base=${basePath} paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`);
|
|
1080
|
+
return [basePath, css];
|
|
1081
|
+
})
|
|
1082
|
+
]);
|
|
1083
|
+
this.#cssTextByBasePath = Object.fromEntries(cssEntries);
|
|
1084
|
+
return this.#cssTextByBasePath;
|
|
1085
|
+
}
|
|
1086
|
+
async discoverCss({ refresh } = {}) {
|
|
1087
|
+
const { cssPaths } = await this.discoverCssAndSources({ refresh });
|
|
1088
|
+
return cssPaths;
|
|
1089
|
+
}
|
|
1090
|
+
async discoverCssAndSources({
|
|
1091
|
+
refresh,
|
|
1092
|
+
pageKeys
|
|
1093
|
+
} = {}) {
|
|
1094
|
+
pageKeys ??= await this.#app.getPageKeys({ refresh });
|
|
1095
|
+
const seeds = pageKeys.map((key) => path8.resolve(this.#app.cwdPath, "page", key));
|
|
1096
|
+
const cssFiles = new Set;
|
|
1097
|
+
const sourceFiles = new Set;
|
|
1098
|
+
const queue = [...seeds];
|
|
1099
|
+
const resolvePackage = await createTsconfigPackageResolver(this.#app);
|
|
1100
|
+
const analyzer = new BarrelAnalyzer({ resolvePackage });
|
|
1101
|
+
const akanConfig = await this.#app.getConfig({ refresh });
|
|
1102
|
+
while (queue.length > 0) {
|
|
1103
|
+
const filePath = queue.shift();
|
|
1104
|
+
if (!filePath || sourceFiles.has(filePath) || isIgnoredNodeModuleSource(filePath))
|
|
1105
|
+
continue;
|
|
1106
|
+
sourceFiles.add(filePath);
|
|
1107
|
+
let content;
|
|
1108
|
+
try {
|
|
1109
|
+
content = await Bun.file(filePath).text();
|
|
1110
|
+
} catch {
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
let source = content;
|
|
1114
|
+
if (akanConfig.barrelImports.length > 0) {
|
|
1115
|
+
try {
|
|
1116
|
+
const rewritten = await rewriteBarrelImports(content, akanConfig.barrelImports, analyzer);
|
|
1117
|
+
if (rewritten !== null)
|
|
1118
|
+
source = rewritten;
|
|
1119
|
+
} catch {}
|
|
1120
|
+
}
|
|
1121
|
+
let imports;
|
|
1122
|
+
try {
|
|
1123
|
+
imports = this.#transpiler.scanImports(source);
|
|
1124
|
+
} catch {
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
const importerDir = path8.dirname(filePath);
|
|
1128
|
+
for (const imp of imports) {
|
|
1129
|
+
const spec = imp.path;
|
|
1130
|
+
if (!spec)
|
|
1131
|
+
continue;
|
|
1132
|
+
if (spec.endsWith(".css")) {
|
|
1133
|
+
const cssPath = await this.#resolveCssImport(spec, importerDir);
|
|
1134
|
+
cssFiles.add(cssPath);
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
if (NON_SOURCE_EXT_RE.test(spec))
|
|
1138
|
+
continue;
|
|
1139
|
+
const resolved = await this.#resolveSourceImport(spec, importerDir, resolvePackage);
|
|
1140
|
+
if (!resolved || sourceFiles.has(resolved) || isIgnoredNodeModuleSource(resolved))
|
|
1141
|
+
continue;
|
|
1142
|
+
queue.push(resolved);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return { cssPaths: [...cssFiles], sourcePaths: [...sourceFiles] };
|
|
1146
|
+
}
|
|
1147
|
+
async compileCss(cssPaths, sourcePaths) {
|
|
1148
|
+
if (cssPaths.length === 0)
|
|
1149
|
+
return "";
|
|
1150
|
+
const compileStarted = Date.now();
|
|
1151
|
+
const compilers = await Promise.all(cssPaths.map(async (cssPath) => {
|
|
1152
|
+
const css = await Bun.file(cssPath).text();
|
|
1153
|
+
const base = path8.dirname(cssPath);
|
|
1154
|
+
const compiler = await compile(css, {
|
|
1155
|
+
base,
|
|
1156
|
+
loadStylesheet: (id, fromBase) => this.#loadStylesheet(id, fromBase),
|
|
1157
|
+
loadModule: (id, fromBase) => this.#loadModule(id, fromBase)
|
|
1158
|
+
});
|
|
1159
|
+
return { cssPath, compiler };
|
|
1160
|
+
}));
|
|
1161
|
+
const sourceDirs = new Set;
|
|
1162
|
+
for (const entry of compilers) {
|
|
1163
|
+
if (!entry)
|
|
1164
|
+
continue;
|
|
1165
|
+
for (const s of entry.compiler.sources)
|
|
1166
|
+
sourceDirs.add(s.base);
|
|
1167
|
+
}
|
|
1168
|
+
const scanStarted = Date.now();
|
|
1169
|
+
const candidates = await this.#scanCandidates(sourcePaths, [...sourceDirs]);
|
|
1170
|
+
this.#logger.verbose(`css candidates scanned count=${candidates.length} sources=${sourcePaths.length} dirs=${sourceDirs.size} in ${Date.now() - scanStarted}ms`);
|
|
1171
|
+
const parts = [];
|
|
1172
|
+
for (const entry of compilers) {
|
|
1173
|
+
if (!entry)
|
|
1174
|
+
continue;
|
|
1175
|
+
parts.push(entry.compiler.build(candidates));
|
|
1176
|
+
}
|
|
1177
|
+
this.#logger.verbose(`css compiled paths=${cssPaths.length} candidates=${candidates.length} in ${Date.now() - compileStarted}ms`);
|
|
1178
|
+
return parts.join(`
|
|
1179
|
+
`);
|
|
1180
|
+
}
|
|
1181
|
+
async#loadStylesheet(id, fromBase) {
|
|
1182
|
+
const p = await this.#resolveCssImport(id, fromBase);
|
|
1183
|
+
const content = await Bun.file(p).text();
|
|
1184
|
+
return { path: p, base: path8.dirname(p), content };
|
|
1185
|
+
}
|
|
1186
|
+
async#resolveCssImport(id, fromBase) {
|
|
1187
|
+
if (id.startsWith(".") || id.startsWith("/"))
|
|
1188
|
+
return path8.resolve(fromBase, id);
|
|
1189
|
+
const resolver = await this.#getCssImportResolver();
|
|
1190
|
+
const resolved = await resolver.resolve(id, fromBase);
|
|
1191
|
+
if (resolved)
|
|
1192
|
+
return resolved;
|
|
1193
|
+
throw new Error(`[css] failed to resolve stylesheet import "${id}" from ${fromBase}`);
|
|
1194
|
+
}
|
|
1195
|
+
async#getCssImportResolver() {
|
|
1196
|
+
if (this.#cssImportResolver)
|
|
1197
|
+
return this.#cssImportResolver;
|
|
1198
|
+
this.#cssImportResolver = await CssImportResolver.create(this.#app);
|
|
1199
|
+
return this.#cssImportResolver;
|
|
1200
|
+
}
|
|
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 };
|
|
1205
|
+
}
|
|
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;
|
|
1214
|
+
}
|
|
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;
|
|
1227
|
+
}
|
|
1228
|
+
return null;
|
|
1229
|
+
}
|
|
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];
|
|
1249
|
+
}
|
|
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;
|
|
1257
|
+
}
|
|
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;
|
|
1265
|
+
}
|
|
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
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// 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";
|
|
1284
|
+
var FONT_URL_PREFIX = "/_akan/fonts";
|
|
1285
|
+
var DEFAULT_FONT_SUBSETS = ["latin"];
|
|
1286
|
+
|
|
1287
|
+
class FontOptimizer {
|
|
1288
|
+
#app;
|
|
1289
|
+
#command;
|
|
1290
|
+
#artifactRoot;
|
|
1291
|
+
#files = [];
|
|
1292
|
+
#cssParts = [];
|
|
1293
|
+
#woff2Ready = null;
|
|
1294
|
+
static #ksX1001Text = null;
|
|
1295
|
+
static #cacheVersion = 1;
|
|
1296
|
+
constructor(app, command = "start") {
|
|
1297
|
+
this.#app = app;
|
|
1298
|
+
this.#command = command;
|
|
1299
|
+
this.#artifactRoot = path9.join(command === "build" ? app.dist.cwdPath : app.cwdPath, ".akan/artifact");
|
|
1300
|
+
}
|
|
1301
|
+
async optimize() {
|
|
1302
|
+
const fonts = await this.discoverFonts();
|
|
1303
|
+
const cacheKey = await this.#buildCacheKey(fonts);
|
|
1304
|
+
const cached = cacheKey ? await this.#readCache(cacheKey) : null;
|
|
1305
|
+
if (cached) {
|
|
1306
|
+
this.#app.verbose(`[font] reused ${cached.files.length} cached file(s); skipped subsetting`);
|
|
1307
|
+
return { css: cached.css, fonts, files: cached.files };
|
|
1308
|
+
}
|
|
1309
|
+
for (const font of fonts) {
|
|
1310
|
+
if (!this.#isFontOptimizationEnabled(font))
|
|
1311
|
+
continue;
|
|
1312
|
+
await this.#optimizeFont(font);
|
|
1313
|
+
}
|
|
1314
|
+
const fontUtilityCss = this.#buildFontUtilityRules(fonts);
|
|
1315
|
+
if (fontUtilityCss)
|
|
1316
|
+
this.#cssParts.push(fontUtilityCss);
|
|
1317
|
+
const result = { css: this.#cssParts.join(`
|
|
1318
|
+
`), fonts, files: this.#files };
|
|
1319
|
+
if (cacheKey)
|
|
1320
|
+
await this.#writeCache(cacheKey, result);
|
|
1321
|
+
return result;
|
|
1322
|
+
}
|
|
1323
|
+
get #cachePath() {
|
|
1324
|
+
return path9.join(this.#artifactRoot, "fontCache.json");
|
|
1325
|
+
}
|
|
1326
|
+
async#buildCacheKey(fonts) {
|
|
1327
|
+
const sources = [];
|
|
1328
|
+
for (const font of fonts) {
|
|
1329
|
+
if (!this.#isFontOptimizationEnabled(font))
|
|
1330
|
+
continue;
|
|
1331
|
+
for (const face of this.#getFontFaces(font)) {
|
|
1332
|
+
const sourcePath = await this.#resolveFontSourcePath(face.src);
|
|
1333
|
+
const stamp = sourcePath ? await this.#fileStamp(sourcePath) : null;
|
|
1334
|
+
if (!stamp)
|
|
1335
|
+
return null;
|
|
1336
|
+
sources.push({ optimizedSrc: face.optimizedSrc, ...stamp });
|
|
1337
|
+
}
|
|
1338
|
+
for (const filePath of font.subsetFiles ?? []) {
|
|
1339
|
+
const abs = path9.isAbsolute(filePath) ? filePath : path9.join(this.#app.cwdPath, filePath);
|
|
1340
|
+
const stamp = await this.#fileStamp(abs);
|
|
1341
|
+
if (!stamp)
|
|
1342
|
+
return null;
|
|
1343
|
+
sources.push({ subsetFile: filePath, ...stamp });
|
|
1344
|
+
}
|
|
1345
|
+
if (this.#getFontSubsets(font).includes("auto"))
|
|
1346
|
+
sources.push({ autoSubsetText: this.#hashFontConfig(await this.#collectAutoSubsetText()) });
|
|
1347
|
+
}
|
|
1348
|
+
return this.#hashFontConfig({ version: FontOptimizer.#cacheVersion, fonts, sources });
|
|
1349
|
+
}
|
|
1350
|
+
async#fileStamp(filePath) {
|
|
1351
|
+
try {
|
|
1352
|
+
const stats = await stat2(filePath);
|
|
1353
|
+
return { mtimeMs: Math.round(stats.mtimeMs), size: stats.size };
|
|
1354
|
+
} catch {
|
|
1355
|
+
return null;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
async#readCache(key) {
|
|
1359
|
+
const cache = await Bun.file(this.#cachePath).json().catch(() => null);
|
|
1360
|
+
if (cache?.version !== FontOptimizer.#cacheVersion || cache.key !== key)
|
|
1361
|
+
return null;
|
|
1362
|
+
const files = cache.files.map((relativePath) => path9.join(this.#artifactRoot, relativePath));
|
|
1363
|
+
for (const filePath of files) {
|
|
1364
|
+
if (!await Bun.file(filePath).exists())
|
|
1365
|
+
return null;
|
|
1366
|
+
}
|
|
1367
|
+
return { css: cache.css, files };
|
|
1368
|
+
}
|
|
1369
|
+
async#writeCache(key, result) {
|
|
1370
|
+
const cache = {
|
|
1371
|
+
version: FontOptimizer.#cacheVersion,
|
|
1372
|
+
key,
|
|
1373
|
+
css: result.css,
|
|
1374
|
+
files: result.files.map((filePath) => path9.relative(this.#artifactRoot, filePath))
|
|
1375
|
+
};
|
|
1376
|
+
await Bun.write(this.#cachePath, JSON.stringify(cache));
|
|
1377
|
+
}
|
|
1378
|
+
async discoverFonts() {
|
|
1379
|
+
const pageKeys = await this.#app.getPageKeys();
|
|
1380
|
+
const fonts = [];
|
|
1381
|
+
await Promise.all(pageKeys.map(async (key) => {
|
|
1382
|
+
const filePath = path9.resolve(this.#app.cwdPath, "page", key);
|
|
1383
|
+
const file = Bun.file(filePath);
|
|
1384
|
+
if (!await file.exists())
|
|
1385
|
+
return;
|
|
1386
|
+
const source = await file.text();
|
|
1387
|
+
if (!source.includes("fonts"))
|
|
1388
|
+
return;
|
|
1389
|
+
fonts.push(...this.#extractFontsExport(source, filePath));
|
|
1390
|
+
}));
|
|
1391
|
+
return this.#dedupeFonts(fonts);
|
|
1392
|
+
}
|
|
1393
|
+
async#optimizeFont(font) {
|
|
1394
|
+
const faceCss = [];
|
|
1395
|
+
for (const face of this.#getFontFaces(font)) {
|
|
1396
|
+
const sourcePath = await this.#resolveFontSourcePath(face.src);
|
|
1397
|
+
if (!sourcePath) {
|
|
1398
|
+
this.#app.logger.warn(`[font] source not found: ${face.src}`);
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
const outputPath = path9.join(this.#artifactRoot, face.optimizedSrc.replace(/^\/_akan\//, ""));
|
|
1402
|
+
await mkdir3(path9.dirname(outputPath), { recursive: true });
|
|
1403
|
+
const sourceBuffer = Buffer.from(await Bun.file(sourcePath).arrayBuffer());
|
|
1404
|
+
const outputBuffer = await this.#buildFontBuffer(font, sourceBuffer, sourcePath);
|
|
1405
|
+
await Bun.write(outputPath, outputBuffer);
|
|
1406
|
+
this.#files.push(outputPath);
|
|
1407
|
+
faceCss.push(this.#buildOptimizedFontFaceRule(font, face));
|
|
1408
|
+
const fallbackCss = await this.#buildFontaineFallbackCss(font, face, outputPath);
|
|
1409
|
+
if (fallbackCss)
|
|
1410
|
+
faceCss.push(fallbackCss);
|
|
1411
|
+
}
|
|
1412
|
+
if (faceCss.length > 0)
|
|
1413
|
+
this.#cssParts.push(...faceCss, this.#buildRootVariableRule(font));
|
|
1414
|
+
}
|
|
1415
|
+
#extractFontsExport(source, filePath) {
|
|
1416
|
+
const sourceFile = ts2.createSourceFile(filePath, source, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TSX);
|
|
1417
|
+
const fonts = [];
|
|
1418
|
+
for (const statement of sourceFile.statements) {
|
|
1419
|
+
if (!ts2.isVariableStatement(statement))
|
|
1420
|
+
continue;
|
|
1421
|
+
const modifiers = ts2.canHaveModifiers(statement) ? ts2.getModifiers(statement) : undefined;
|
|
1422
|
+
const isExported = modifiers?.some((modifier) => modifier.kind === ts2.SyntaxKind.ExportKeyword) ?? false;
|
|
1423
|
+
if (!isExported)
|
|
1424
|
+
continue;
|
|
1425
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1426
|
+
if (!ts2.isIdentifier(declaration.name) || declaration.name.text !== "fonts")
|
|
1427
|
+
continue;
|
|
1428
|
+
const value = declaration.initializer ? this.#literalToValue(declaration.initializer) : null;
|
|
1429
|
+
if (Array.isArray(value)) {
|
|
1430
|
+
fonts.push(...value.map((font) => this.#withFontDefaults(font)));
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
return fonts;
|
|
1435
|
+
}
|
|
1436
|
+
#literalToValue(node) {
|
|
1437
|
+
if (ts2.isStringLiteralLike(node))
|
|
1438
|
+
return node.text;
|
|
1439
|
+
if (ts2.isNumericLiteral(node))
|
|
1440
|
+
return Number(node.text);
|
|
1441
|
+
if (node.kind === ts2.SyntaxKind.TrueKeyword)
|
|
1442
|
+
return true;
|
|
1443
|
+
if (node.kind === ts2.SyntaxKind.FalseKeyword)
|
|
1444
|
+
return false;
|
|
1445
|
+
if (ts2.isArrayLiteralExpression(node))
|
|
1446
|
+
return node.elements.map((element) => this.#literalToValue(element));
|
|
1447
|
+
if (ts2.isObjectLiteralExpression(node)) {
|
|
1448
|
+
const obj = {};
|
|
1449
|
+
for (const prop of node.properties) {
|
|
1450
|
+
if (!ts2.isPropertyAssignment(prop))
|
|
1451
|
+
continue;
|
|
1452
|
+
const name = this.#getPropertyName(prop.name);
|
|
1453
|
+
if (!name)
|
|
1454
|
+
continue;
|
|
1455
|
+
obj[name] = this.#literalToValue(prop.initializer);
|
|
1456
|
+
}
|
|
1457
|
+
return obj;
|
|
1458
|
+
}
|
|
1459
|
+
if (ts2.isAsExpression(node) || ts2.isSatisfiesExpression(node) || ts2.isParenthesizedExpression(node)) {
|
|
1460
|
+
return this.#literalToValue(node.expression);
|
|
1461
|
+
}
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
#getPropertyName(name) {
|
|
1465
|
+
if (ts2.isIdentifier(name) || ts2.isStringLiteral(name) || ts2.isNumericLiteral(name))
|
|
1466
|
+
return name.text;
|
|
1467
|
+
return null;
|
|
1468
|
+
}
|
|
1469
|
+
#dedupeFonts(fonts) {
|
|
1470
|
+
const map = new Map;
|
|
1471
|
+
for (const font of fonts)
|
|
1472
|
+
map.set(JSON.stringify(font), font);
|
|
1473
|
+
return [...map.values()];
|
|
1474
|
+
}
|
|
1475
|
+
#withFontDefaults(font) {
|
|
1476
|
+
return { ...font, subsets: font.subsets ?? [...DEFAULT_FONT_SUBSETS] };
|
|
1477
|
+
}
|
|
1478
|
+
#getFontSubsets(font) {
|
|
1479
|
+
return font.subsets ?? DEFAULT_FONT_SUBSETS;
|
|
1480
|
+
}
|
|
1481
|
+
#getFontVariableName(font) {
|
|
1482
|
+
return font.variable ?? `--font-${font.name}`;
|
|
1483
|
+
}
|
|
1484
|
+
#getFontFallbackName(font) {
|
|
1485
|
+
return font.fallbackName ?? `${font.name} fallback`;
|
|
1486
|
+
}
|
|
1487
|
+
#isFontOptimizationEnabled(font) {
|
|
1488
|
+
return font.optimize !== false;
|
|
1489
|
+
}
|
|
1490
|
+
#getFontStyles(font) {
|
|
1491
|
+
return font.styles?.length ? font.styles : ["normal"];
|
|
1492
|
+
}
|
|
1493
|
+
#getFontFaces(font) {
|
|
1494
|
+
const enabledStyles = new Set(this.#getFontStyles(font));
|
|
1495
|
+
return font.paths.map((fontPath) => {
|
|
1496
|
+
const style = fontPath.style ?? "normal";
|
|
1497
|
+
return {
|
|
1498
|
+
font,
|
|
1499
|
+
path: fontPath,
|
|
1500
|
+
src: fontPath.src,
|
|
1501
|
+
weight: fontPath.weight,
|
|
1502
|
+
style,
|
|
1503
|
+
optimizedSrc: this.#getOptimizedFontSrc(font, fontPath)
|
|
1504
|
+
};
|
|
1505
|
+
}).filter((face) => enabledStyles.has(face.style));
|
|
1506
|
+
}
|
|
1507
|
+
#getOptimizedFontSrc(font, fontPath) {
|
|
1508
|
+
const style = fontPath.style ?? "normal";
|
|
1509
|
+
const hash = this.#hashFontConfig({
|
|
1510
|
+
name: font.name,
|
|
1511
|
+
src: fontPath.src,
|
|
1512
|
+
weight: fontPath.weight,
|
|
1513
|
+
style,
|
|
1514
|
+
display: font.display,
|
|
1515
|
+
subset: font.subset,
|
|
1516
|
+
subsets: this.#getFontSubsets(font),
|
|
1517
|
+
subsetText: font.subsetText,
|
|
1518
|
+
subsetFiles: font.subsetFiles,
|
|
1519
|
+
declarations: [...font.declarations ?? [], ...fontPath.declarations ?? []]
|
|
1520
|
+
});
|
|
1521
|
+
return `${FONT_URL_PREFIX}/${this.#slugFontPart(font.name)}-${this.#slugFontPart(String(fontPath.weight))}-${style}-${hash}.woff2`;
|
|
1522
|
+
}
|
|
1523
|
+
#hashFontConfig(value) {
|
|
1524
|
+
const input = this.#stableStringify(value);
|
|
1525
|
+
let hash = 2166136261;
|
|
1526
|
+
for (let i = 0;i < input.length; i++) {
|
|
1527
|
+
hash ^= input.charCodeAt(i);
|
|
1528
|
+
hash = Math.imul(hash, 16777619);
|
|
1529
|
+
}
|
|
1530
|
+
return (hash >>> 0).toString(36);
|
|
1531
|
+
}
|
|
1532
|
+
#stableStringify(value) {
|
|
1533
|
+
if (Array.isArray(value))
|
|
1534
|
+
return `[${value.map((item) => this.#stableStringify(item)).join(",")}]`;
|
|
1535
|
+
if (value && typeof value === "object") {
|
|
1536
|
+
const entries = Object.entries(value).filter(([, v]) => v !== undefined).sort(([a], [b]) => a.localeCompare(b));
|
|
1537
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${this.#stableStringify(v)}`).join(",")}}`;
|
|
1538
|
+
}
|
|
1539
|
+
return JSON.stringify(value);
|
|
1540
|
+
}
|
|
1541
|
+
#slugFontPart(value) {
|
|
1542
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "font";
|
|
1543
|
+
}
|
|
1544
|
+
async#resolveFontSourcePath(src) {
|
|
1545
|
+
if (!src.startsWith("/"))
|
|
1546
|
+
return null;
|
|
1547
|
+
const rel = src.replace(/^\//, "");
|
|
1548
|
+
const candidates = [
|
|
1549
|
+
this.#command === "build" ? path9.join(this.#app.dist.cwdPath, "public", rel) : null,
|
|
1550
|
+
path9.join(this.#app.cwdPath, "public", rel),
|
|
1551
|
+
this.#resolveWorkspacePublicPath(rel)
|
|
1552
|
+
].filter(Boolean);
|
|
1553
|
+
for (const candidate of candidates) {
|
|
1554
|
+
if (await Bun.file(candidate).exists())
|
|
1555
|
+
return candidate;
|
|
1556
|
+
}
|
|
1557
|
+
return null;
|
|
1558
|
+
}
|
|
1559
|
+
async#buildFontBuffer(font, sourceBuffer, sourcePath) {
|
|
1560
|
+
if (font.subset === false)
|
|
1561
|
+
return this.#convertToWoff2(sourceBuffer, sourcePath);
|
|
1562
|
+
const { default: subsetFont } = await import("subset-font");
|
|
1563
|
+
return subsetFont(sourceBuffer, await this.#getSubsetText(font), { targetFormat: "woff2" });
|
|
1564
|
+
}
|
|
1565
|
+
async#convertToWoff2(buffer, sourcePath) {
|
|
1566
|
+
const { createFont } = await import("fonteditor-core");
|
|
1567
|
+
await this.#initWoff2();
|
|
1568
|
+
const font = createFont(buffer, { type: this.#getFontType(sourcePath, buffer) });
|
|
1569
|
+
return font.write({ type: "woff2", toBuffer: true });
|
|
1570
|
+
}
|
|
1571
|
+
async#initWoff2() {
|
|
1572
|
+
this.#woff2Ready ??= import("fonteditor-core").then(({ woff2 }) => woff2.init()).then(() => {
|
|
1573
|
+
return;
|
|
1574
|
+
});
|
|
1575
|
+
return this.#woff2Ready;
|
|
1576
|
+
}
|
|
1577
|
+
#getFontType(sourcePath, buffer) {
|
|
1578
|
+
const signature = buffer.toString("ascii", 0, 4);
|
|
1579
|
+
if (signature === "wOFF")
|
|
1580
|
+
return "woff";
|
|
1581
|
+
if (signature === "wOF2")
|
|
1582
|
+
return "woff2";
|
|
1583
|
+
if (signature === "OTTO")
|
|
1584
|
+
return "otf";
|
|
1585
|
+
const ext = path9.extname(sourcePath).slice(1).toLowerCase();
|
|
1586
|
+
if (ext === "otf" || ext === "woff" || ext === "woff2")
|
|
1587
|
+
return ext;
|
|
1588
|
+
return "ttf";
|
|
1589
|
+
}
|
|
1590
|
+
#resolveWorkspacePublicPath(rel) {
|
|
1591
|
+
const [root, dep, ...rest] = rel.split("/");
|
|
1592
|
+
if (root !== "libs" || !dep || rest.length === 0)
|
|
1593
|
+
return null;
|
|
1594
|
+
return path9.join(this.#app.workspace.workspaceRoot, "libs", dep, "public", ...rest);
|
|
1595
|
+
}
|
|
1596
|
+
async#getSubsetText(font) {
|
|
1597
|
+
const parts = new Set;
|
|
1598
|
+
const subsets = this.#getFontSubsets(font);
|
|
1599
|
+
for (const subset of subsets)
|
|
1600
|
+
parts.add(await this.#getSubsetPresetText(subset));
|
|
1601
|
+
if (font.subsetText)
|
|
1602
|
+
parts.add(font.subsetText);
|
|
1603
|
+
for (const filePath of font.subsetFiles ?? []) {
|
|
1604
|
+
const abs = path9.isAbsolute(filePath) ? filePath : path9.join(this.#app.cwdPath, filePath);
|
|
1605
|
+
const file = Bun.file(abs);
|
|
1606
|
+
if (await file.exists())
|
|
1607
|
+
parts.add(await file.text());
|
|
1608
|
+
}
|
|
1609
|
+
return [...parts].join("");
|
|
1610
|
+
}
|
|
1611
|
+
async#getSubsetPresetText(subset) {
|
|
1612
|
+
if (subset === "latin")
|
|
1613
|
+
return this.#rangeText(32, 126);
|
|
1614
|
+
if (subset === "latin-ext")
|
|
1615
|
+
return `${this.#rangeText(32, 126)}${this.#rangeText(160, 591)}`;
|
|
1616
|
+
if (subset === "ks-x-1001")
|
|
1617
|
+
return FontOptimizer.#getKsX1001Text();
|
|
1618
|
+
if (subset === "auto")
|
|
1619
|
+
return this.#collectAutoSubsetText();
|
|
1620
|
+
return "";
|
|
1621
|
+
}
|
|
1622
|
+
async#collectAutoSubsetText() {
|
|
1623
|
+
const roots = ["page", "ui"].map((dir) => path9.join(this.#app.cwdPath, dir));
|
|
1624
|
+
const glob = new Bun.Glob("**/*.{ts,tsx,js,jsx,html,md}");
|
|
1625
|
+
const parts = [];
|
|
1626
|
+
await Promise.all(roots.map(async (root) => {
|
|
1627
|
+
if (!await Bun.file(root).exists())
|
|
1628
|
+
return;
|
|
1629
|
+
for await (const filePath of glob.scan({ cwd: root, absolute: true })) {
|
|
1630
|
+
parts.push(await Bun.file(filePath).text());
|
|
1631
|
+
}
|
|
1632
|
+
}));
|
|
1633
|
+
return parts.join("");
|
|
1634
|
+
}
|
|
1635
|
+
#rangeText(start, end) {
|
|
1636
|
+
let text = "";
|
|
1637
|
+
for (let code = start;code <= end; code++)
|
|
1638
|
+
text += String.fromCodePoint(code);
|
|
1639
|
+
return text;
|
|
1640
|
+
}
|
|
1641
|
+
static #getKsX1001Text() {
|
|
1642
|
+
if (FontOptimizer.#ksX1001Text)
|
|
1643
|
+
return FontOptimizer.#ksX1001Text;
|
|
1644
|
+
try {
|
|
1645
|
+
const decoder = new TextDecoder("euc-kr");
|
|
1646
|
+
const chars = new Set;
|
|
1647
|
+
for (let lead = 161;lead <= 254; lead++) {
|
|
1648
|
+
for (let trail = 161;trail <= 254; trail++) {
|
|
1649
|
+
const char = decoder.decode(Uint8Array.of(lead, trail));
|
|
1650
|
+
if (char && char !== "\uFFFD")
|
|
1651
|
+
chars.add(char);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
FontOptimizer.#ksX1001Text = [...chars].join("");
|
|
1655
|
+
} catch {
|
|
1656
|
+
FontOptimizer.#ksX1001Text = FontOptimizer.#rangeTextStatic(44032, 55203);
|
|
1657
|
+
}
|
|
1658
|
+
return FontOptimizer.#ksX1001Text;
|
|
1659
|
+
}
|
|
1660
|
+
static #rangeTextStatic(start, end) {
|
|
1661
|
+
let text = "";
|
|
1662
|
+
for (let code = start;code <= end; code++)
|
|
1663
|
+
text += String.fromCodePoint(code);
|
|
1664
|
+
return text;
|
|
1665
|
+
}
|
|
1666
|
+
#buildOptimizedFontFaceRule(font, face) {
|
|
1667
|
+
const declarations = [
|
|
1668
|
+
["font-family", this.#quote(font.name)],
|
|
1669
|
+
["src", `url(${this.#quote(face.optimizedSrc)}) format("woff2")`],
|
|
1670
|
+
["font-weight", String(face.weight)],
|
|
1671
|
+
["font-style", face.style],
|
|
1672
|
+
["font-display", font.display ?? "swap"],
|
|
1673
|
+
...this.#toDeclarationEntries(font.declarations),
|
|
1674
|
+
...this.#toDeclarationEntries(face.path.declarations)
|
|
1675
|
+
];
|
|
1676
|
+
return `@font-face {
|
|
1677
|
+
${declarations.map(([prop, value]) => ` ${prop}: ${value};`).join(`
|
|
1678
|
+
`)}
|
|
1679
|
+
}`;
|
|
1680
|
+
}
|
|
1681
|
+
async#buildFontaineFallbackCss(font, face, outputPath) {
|
|
1682
|
+
if (font.adjustFontFallback === false)
|
|
1683
|
+
return "";
|
|
1684
|
+
const { generateFontFace, getMetricsForFamily, readMetrics, resolveCategoryFallbacks } = await import("fontaine");
|
|
1685
|
+
const metrics = await readMetrics(outputPath).catch(() => null);
|
|
1686
|
+
if (!metrics)
|
|
1687
|
+
return "";
|
|
1688
|
+
const fallbacks = resolveCategoryFallbacks({
|
|
1689
|
+
fontFamily: font.name,
|
|
1690
|
+
fallbacks: font.fallbacks ?? {},
|
|
1691
|
+
metrics: { ...metrics, category: font.category }
|
|
1692
|
+
});
|
|
1693
|
+
const css = [];
|
|
1694
|
+
for (let i = fallbacks.length - 1;i >= 0; i--) {
|
|
1695
|
+
const fallback = fallbacks[i];
|
|
1696
|
+
const fallbackMetrics = await getMetricsForFamily(fallback);
|
|
1697
|
+
if (!fallbackMetrics)
|
|
1698
|
+
continue;
|
|
1699
|
+
css.push(generateFontFace({ ...metrics, category: font.category }, {
|
|
1700
|
+
name: this.#getFontFallbackName(font),
|
|
1701
|
+
font: fallback,
|
|
1702
|
+
metrics: fallbackMetrics,
|
|
1703
|
+
"font-weight": String(face.weight),
|
|
1704
|
+
"font-style": face.style
|
|
1705
|
+
}));
|
|
1706
|
+
}
|
|
1707
|
+
return css.join("");
|
|
1708
|
+
}
|
|
1709
|
+
#buildRootVariableRule(font) {
|
|
1710
|
+
return `:root { ${this.#getFontVariableName(font)}: ${this.#quote(font.name)}, ${this.#quote(this.#getFontFallbackName(font))}; }`;
|
|
1711
|
+
}
|
|
1712
|
+
#buildFontUtilityRules(fonts) {
|
|
1713
|
+
const rules = [];
|
|
1714
|
+
const seen = new Set;
|
|
1715
|
+
for (const font of fonts) {
|
|
1716
|
+
const className = font.className || `font-${font.name}`;
|
|
1717
|
+
const selector = `.${this.#escapeCssClassName(className)}`;
|
|
1718
|
+
const rule = `${selector} { font-family: var(${this.#getFontVariableName(font)}); }`;
|
|
1719
|
+
if (seen.has(rule))
|
|
1720
|
+
continue;
|
|
1721
|
+
seen.add(rule);
|
|
1722
|
+
rules.push(rule);
|
|
1723
|
+
}
|
|
1724
|
+
return rules.join(`
|
|
1725
|
+
`);
|
|
1726
|
+
}
|
|
1727
|
+
#escapeCssClassName(value) {
|
|
1728
|
+
return value.replace(/^-?\d|[^a-zA-Z0-9_-]/g, (part) => [...part].map((char) => `\\${char.codePointAt(0)?.toString(16)} `).join(""));
|
|
1729
|
+
}
|
|
1730
|
+
#toDeclarationEntries(declarations = []) {
|
|
1731
|
+
return declarations.map((declaration) => [declaration.prop, declaration.value]);
|
|
1732
|
+
}
|
|
1733
|
+
#quote(value) {
|
|
1734
|
+
return JSON.stringify(value);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
|
|
1739
|
+
import path11 from "path";
|
|
1740
|
+
|
|
1741
|
+
// pkgs/@akanjs/devkit/transforms/externalizeFrameworkPlugin.ts
|
|
1742
|
+
import path10 from "path";
|
|
1743
|
+
var DEFAULT_INCLUDE = ["akanjs/", "@apps/", "@libs/"];
|
|
1744
|
+
var DEFAULT_EXCLUDE_EXACT = new Set(["akanjs/webkit", "@akanjs/cli", "@akanjs/devkit"]);
|
|
1745
|
+
var DEFAULT_EXCLUDE_PREFIX = ["@akanjs/cli/", "@akanjs/devkit/"];
|
|
1746
|
+
var OPTIONAL_BACKEND_EXTERNAL_EXACT = new Set([
|
|
1747
|
+
"@libsql/client",
|
|
1748
|
+
"bullmq",
|
|
1749
|
+
"croner",
|
|
1750
|
+
"ioredis",
|
|
1751
|
+
"postgres",
|
|
1752
|
+
"protobufjs"
|
|
1753
|
+
]);
|
|
1754
|
+
var RUNTIME_EXTERNAL_EXACT = new Set([
|
|
1755
|
+
"react",
|
|
1756
|
+
"react-dom",
|
|
1757
|
+
"react/jsx-runtime",
|
|
1758
|
+
"react/jsx-dev-runtime",
|
|
1759
|
+
"react-server-dom-webpack",
|
|
1760
|
+
"react-server-dom-webpack/server.node",
|
|
1761
|
+
"react-server-dom-webpack/client.node",
|
|
1762
|
+
"react-server-dom-webpack/client.browser"
|
|
1763
|
+
]);
|
|
1764
|
+
var RUNTIME_EXTERNAL_PREFIX = ["react-dom/", "react-server-dom-webpack/"];
|
|
1765
|
+
var CANDIDATE_EXTS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
|
|
1766
|
+
async function createExternalizeFrameworkPlugin(options) {
|
|
1767
|
+
const tsconfig = await options.app.getTsConfig();
|
|
1768
|
+
const includePrefixes = options.include ?? DEFAULT_INCLUDE;
|
|
1769
|
+
const extraExact = new Set(options.extra ?? []);
|
|
1770
|
+
const workspaceRoot = options.app.workspace.workspaceRoot;
|
|
1771
|
+
const tsconfigPaths = tsconfig.compilerOptions.paths ?? {};
|
|
1772
|
+
const rootEntries = Object.entries(tsconfigPaths).filter(([k]) => !k.endsWith("/*")).map(([k, v]) => ({ pkg: k, entryFile: v[0] ?? null })).filter((e) => e.entryFile !== null);
|
|
1773
|
+
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);
|
|
1774
|
+
async function resolveWorkspaceSubpath(spec) {
|
|
1775
|
+
if (!workspaceRoot)
|
|
1776
|
+
return null;
|
|
1777
|
+
for (const { prefix, replacements } of wildcardEntries) {
|
|
1778
|
+
if (!spec.startsWith(prefix))
|
|
1779
|
+
continue;
|
|
1780
|
+
const suffix = spec.slice(prefix.length);
|
|
1781
|
+
for (const repl of replacements) {
|
|
1782
|
+
const replPath = repl?.endsWith("/*") ? repl.slice(0, -1) : repl ?? "";
|
|
1783
|
+
if (!replPath)
|
|
1784
|
+
continue;
|
|
1785
|
+
const candidate = path10.resolve(workspaceRoot, replPath + suffix);
|
|
1786
|
+
const hit = await firstExisting(candidate);
|
|
1787
|
+
if (hit)
|
|
1788
|
+
return hit;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
for (const { pkg, entryFile } of rootEntries) {
|
|
1792
|
+
if (spec !== pkg && !spec.startsWith(`${pkg}/`))
|
|
1793
|
+
continue;
|
|
1794
|
+
if (spec === pkg)
|
|
1795
|
+
continue;
|
|
1796
|
+
const suffix = spec.slice(pkg.length + 1);
|
|
1797
|
+
const pkgDir = path10.dirname(path10.resolve(workspaceRoot, entryFile));
|
|
1798
|
+
const candidate = path10.join(pkgDir, suffix);
|
|
1799
|
+
const hit = await firstExisting(candidate);
|
|
1800
|
+
if (hit)
|
|
1801
|
+
return hit;
|
|
1802
|
+
}
|
|
1803
|
+
return null;
|
|
1804
|
+
}
|
|
1805
|
+
return {
|
|
1806
|
+
name: "akan-externalize-framework",
|
|
1807
|
+
setup(build) {
|
|
1808
|
+
build.onResolve({ filter: /.*/ }, async (args) => {
|
|
1809
|
+
const spec = args.path;
|
|
1810
|
+
if (spec === "." || spec === ".." || spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/"))
|
|
1811
|
+
return;
|
|
1812
|
+
if (extraExact.has(spec) || DEFAULT_EXCLUDE_EXACT.has(spec))
|
|
1813
|
+
return { path: spec, external: true };
|
|
1814
|
+
for (const prefix of DEFAULT_EXCLUDE_PREFIX) {
|
|
1815
|
+
if (spec.startsWith(prefix))
|
|
1816
|
+
return { path: spec, external: true };
|
|
1817
|
+
}
|
|
1818
|
+
if (OPTIONAL_BACKEND_EXTERNAL_EXACT.has(spec))
|
|
1819
|
+
return { path: spec, external: true };
|
|
1820
|
+
if (RUNTIME_EXTERNAL_EXACT.has(spec))
|
|
1821
|
+
return { path: spec, external: true };
|
|
1822
|
+
for (const prefix of RUNTIME_EXTERNAL_PREFIX) {
|
|
1823
|
+
if (spec.startsWith(prefix))
|
|
1824
|
+
return { path: spec, external: true };
|
|
1825
|
+
}
|
|
1826
|
+
for (const prefix of includePrefixes) {
|
|
1827
|
+
if (!spec.startsWith(prefix))
|
|
1828
|
+
continue;
|
|
1829
|
+
const resolved = await resolveWorkspaceSubpath(spec);
|
|
1830
|
+
if (resolved)
|
|
1831
|
+
return { path: resolved };
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
return;
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
async function firstExisting(basePath) {
|
|
1840
|
+
if (await Bun.file(basePath).exists())
|
|
1841
|
+
return basePath;
|
|
1842
|
+
for (const ext of CANDIDATE_EXTS) {
|
|
1843
|
+
const candidate = `${basePath}${ext}`;
|
|
1844
|
+
if (await Bun.file(candidate).exists())
|
|
1845
|
+
return candidate;
|
|
1846
|
+
}
|
|
1847
|
+
for (const ext of CANDIDATE_EXTS) {
|
|
1848
|
+
const candidate = path10.join(basePath, `index${ext}`);
|
|
1849
|
+
if (await Bun.file(candidate).exists())
|
|
1850
|
+
return candidate;
|
|
1851
|
+
}
|
|
1852
|
+
return null;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
// pkgs/@akanjs/devkit/transforms/useClientBundlePlugin.ts
|
|
1856
|
+
function createUseClientBundlePlugin(options = {}) {
|
|
1857
|
+
return {
|
|
1858
|
+
name: "akan-use-client-bundle",
|
|
1859
|
+
setup(build) {
|
|
1860
|
+
build.onLoad({ filter: /\.(tsx|ts|jsx|js)$/ }, async (args) => {
|
|
1861
|
+
if (args.path.includes("/node_modules/"))
|
|
1862
|
+
return;
|
|
1863
|
+
let source;
|
|
1864
|
+
try {
|
|
1865
|
+
source = await Bun.file(args.path).text();
|
|
1866
|
+
} catch {
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
const stubbed = transformUseClient(source, {
|
|
1870
|
+
path: args.path,
|
|
1871
|
+
workspaceRoot: options.workspaceRoot
|
|
1872
|
+
});
|
|
1873
|
+
if (stubbed === null)
|
|
1874
|
+
return;
|
|
1875
|
+
return { contents: stubbed, loader: loaderFor(args.path) };
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
function loaderFor(absPath) {
|
|
1881
|
+
if (absPath.endsWith(".tsx"))
|
|
1882
|
+
return "tsx";
|
|
1883
|
+
if (absPath.endsWith(".jsx"))
|
|
1884
|
+
return "jsx";
|
|
1885
|
+
if (absPath.endsWith(".ts"))
|
|
1886
|
+
return "ts";
|
|
1887
|
+
return "js";
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
// pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
|
|
1891
|
+
var VIRTUAL_PAGES_ENTRY = "akan-pages-entry";
|
|
1892
|
+
|
|
1893
|
+
class PagesBundleBuilder {
|
|
1894
|
+
#app;
|
|
1895
|
+
#command;
|
|
1896
|
+
#pageEntries;
|
|
1897
|
+
#started = Date.now();
|
|
1898
|
+
constructor(app, command = "start", pageEntries) {
|
|
1899
|
+
this.#app = app;
|
|
1900
|
+
this.#command = command;
|
|
1901
|
+
this.#pageEntries = pageEntries;
|
|
1902
|
+
}
|
|
1903
|
+
async build() {
|
|
1904
|
+
const akanConfig = await this.#app.getConfig();
|
|
1905
|
+
const resolvedEntries = this.#pageEntries ?? await resolveSsrPageEntriesForApp(this.#app, await this.#app.getPageKeys());
|
|
1906
|
+
const entrySource = PagesEntrySourceGenerator.generate(resolvedEntries);
|
|
1907
|
+
const workspaceRoot = this.#app.workspace.workspaceRoot;
|
|
1908
|
+
const result = await Bun.build({
|
|
1909
|
+
entrypoints: [VIRTUAL_PAGES_ENTRY],
|
|
1910
|
+
outdir: `${this.#artifactDir}/server`,
|
|
1911
|
+
target: "bun",
|
|
1912
|
+
format: "esm",
|
|
1913
|
+
splitting: this.#splitting,
|
|
1914
|
+
minify: this.#command === "build",
|
|
1915
|
+
naming: {
|
|
1916
|
+
entry: "pages-[hash].[ext]",
|
|
1917
|
+
chunk: "chunks/[name]-[hash].[ext]",
|
|
1918
|
+
asset: "assets/[name]-[hash].[ext]"
|
|
1919
|
+
},
|
|
1920
|
+
define: this.#define(),
|
|
1921
|
+
plugins: [
|
|
1922
|
+
PagesBundleBuilder.createPagesEntryPlugin(entrySource),
|
|
1923
|
+
PagesBundleBuilder.createServerCssStubPlugin(),
|
|
1924
|
+
PagesBundleBuilder.createServerUseClientFetchPlugin(),
|
|
1925
|
+
await createExternalizeFrameworkPlugin({ app: this.#app, extra: akanConfig.externalLibs }),
|
|
1926
|
+
akanConfig.barrelImports.length > 0 ? await createBarrelImportsPlugin(this.#app, {
|
|
1927
|
+
pipeAfter: (source, args) => transformUseClient(source, {
|
|
1928
|
+
path: args.path,
|
|
1929
|
+
workspaceRoot
|
|
1930
|
+
})
|
|
1931
|
+
}) : createUseClientBundlePlugin({ workspaceRoot })
|
|
1932
|
+
]
|
|
1933
|
+
});
|
|
1934
|
+
if (!result.success)
|
|
1935
|
+
throw new AggregateError(result.logs, "[PagesBundleBuilder] Bun.build failed");
|
|
1936
|
+
const entryArtifact = result.outputs.find((a) => a.kind === "entry-point");
|
|
1937
|
+
if (!entryArtifact)
|
|
1938
|
+
throw new Error("[PagesBundleBuilder] Bun.build emitted no entry-point artifact");
|
|
1939
|
+
const bundlePath = path11.resolve(entryArtifact.path);
|
|
1940
|
+
const buildId = Date.now();
|
|
1941
|
+
const outputBytes = result.outputs.reduce((sum, output) => sum + output.size, 0);
|
|
1942
|
+
const chunkCount = result.outputs.filter((output) => output.kind === "chunk").length;
|
|
1943
|
+
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`);
|
|
1944
|
+
return {
|
|
1945
|
+
bundlePath,
|
|
1946
|
+
buildId,
|
|
1947
|
+
splitting: this.#splitting,
|
|
1948
|
+
entryBytes: entryArtifact.size,
|
|
1949
|
+
outputBytes,
|
|
1950
|
+
outputCount: result.outputs.length,
|
|
1951
|
+
chunkCount
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
get #artifactDir() {
|
|
1955
|
+
return `${this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath}/.akan/artifact`;
|
|
1956
|
+
}
|
|
1957
|
+
get #splitting() {
|
|
1958
|
+
return process.env.AKAN_SERVER_PAGES_SPLITTING === "1";
|
|
1959
|
+
}
|
|
1960
|
+
#define() {
|
|
1961
|
+
const nodeEnv = this.#command === "build" ? "production" : "development";
|
|
1962
|
+
return {
|
|
1963
|
+
"process.env.NODE_ENV": JSON.stringify(nodeEnv),
|
|
1964
|
+
"process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("ssr"),
|
|
1965
|
+
...Object.fromEntries(Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]))
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
static createPagesEntryPlugin(source) {
|
|
1969
|
+
return {
|
|
1970
|
+
name: "akan-pages-entry",
|
|
1971
|
+
setup(build) {
|
|
1972
|
+
build.onResolve({ filter: /^akan-pages-entry$/ }, () => ({
|
|
1973
|
+
path: VIRTUAL_PAGES_ENTRY,
|
|
1974
|
+
namespace: "akan-virtual"
|
|
1975
|
+
}));
|
|
1976
|
+
build.onLoad({ filter: /^akan-pages-entry$/, namespace: "akan-virtual" }, () => ({
|
|
1977
|
+
contents: source,
|
|
1978
|
+
loader: "tsx"
|
|
1979
|
+
}));
|
|
1980
|
+
}
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
static createServerCssStubPlugin() {
|
|
1984
|
+
return {
|
|
1985
|
+
name: "akan-server-css-stub",
|
|
1986
|
+
setup(build) {
|
|
1987
|
+
build.onLoad({ filter: /\.css$/ }, () => ({
|
|
1988
|
+
contents: "",
|
|
1989
|
+
loader: "js"
|
|
1990
|
+
}));
|
|
1991
|
+
}
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
static createServerUseClientFetchPlugin() {
|
|
1995
|
+
return {
|
|
1996
|
+
name: "akan-server-use-client-fetch",
|
|
1997
|
+
setup(build) {
|
|
1998
|
+
build.onLoad({ filter: /[/\\]lib[/\\]useClient\.(ts|tsx|js|jsx)$/ }, async (args) => {
|
|
1999
|
+
const source = await Bun.file(args.path).text();
|
|
2000
|
+
const transformed = PagesBundleBuilder.transformServerUseClientFetchSource(source);
|
|
2001
|
+
if (transformed === source)
|
|
2002
|
+
return;
|
|
2003
|
+
return { contents: transformed, loader: loaderFor2(args.path) };
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
static transformServerUseClientFetchSource(source) {
|
|
2009
|
+
if (!source.includes(`with { type: "macro" }`) || !source.includes("FetchClient.build"))
|
|
2010
|
+
return source;
|
|
2011
|
+
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 });");
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
function loaderFor2(absPath) {
|
|
2015
|
+
if (absPath.endsWith(".tsx"))
|
|
2016
|
+
return "tsx";
|
|
2017
|
+
if (absPath.endsWith(".jsx"))
|
|
2018
|
+
return "jsx";
|
|
2019
|
+
if (absPath.endsWith(".ts"))
|
|
2020
|
+
return "ts";
|
|
2021
|
+
return "js";
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
// pkgs/@akanjs/devkit/frontendBuild/precompressArtifacts.ts
|
|
2025
|
+
import fs2 from "fs";
|
|
2026
|
+
import path12 from "path";
|
|
2027
|
+
var COMPRESSIBLE_EXTS = new Set([".css", ".html", ".js", ".json", ".svg"]);
|
|
2028
|
+
var MIN_COMPRESS_BYTES = 1024;
|
|
2029
|
+
async function precompressArtifacts(app) {
|
|
2030
|
+
const roots = [path12.join(app.dist.cwdPath, ".akan/artifact/client")];
|
|
2031
|
+
const result = { files: 0, inputBytes: 0, outputBytes: 0 };
|
|
2032
|
+
await Promise.all(roots.map((root) => precompressRoot(root, result)));
|
|
2033
|
+
if (result.files > 0) {
|
|
2034
|
+
app.verbose(`[precompress] wrote ${result.files} gzip sidecars (${formatBytes(result.inputBytes)} -> ${formatBytes(result.outputBytes)})`);
|
|
2035
|
+
}
|
|
2036
|
+
return result;
|
|
2037
|
+
}
|
|
2038
|
+
async function precompressRoot(root, result) {
|
|
2039
|
+
if (!fs2.existsSync(root) || !fs2.statSync(root).isDirectory())
|
|
2040
|
+
return;
|
|
2041
|
+
const glob = new Bun.Glob("**/*");
|
|
2042
|
+
for await (const filePath of glob.scan({ cwd: root, absolute: true })) {
|
|
2043
|
+
if (!await shouldPrecompress(filePath))
|
|
2044
|
+
continue;
|
|
2045
|
+
const bytes = await Bun.file(filePath).bytes();
|
|
2046
|
+
const gz = Bun.gzipSync(toArrayBuffer(bytes));
|
|
2047
|
+
await Bun.write(`${filePath}.gz`, gz);
|
|
2048
|
+
result.files += 1;
|
|
2049
|
+
result.inputBytes += bytes.byteLength;
|
|
2050
|
+
result.outputBytes += gz.byteLength;
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
async function shouldPrecompress(filePath) {
|
|
2054
|
+
if (filePath.endsWith(".gz"))
|
|
2055
|
+
return false;
|
|
2056
|
+
if (!COMPRESSIBLE_EXTS.has(path12.extname(filePath).toLowerCase()))
|
|
2057
|
+
return false;
|
|
2058
|
+
const file = Bun.file(filePath);
|
|
2059
|
+
if (!await file.exists())
|
|
2060
|
+
return false;
|
|
2061
|
+
return file.size >= MIN_COMPRESS_BYTES;
|
|
2062
|
+
}
|
|
2063
|
+
function toArrayBuffer(bytes) {
|
|
2064
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
2065
|
+
}
|
|
2066
|
+
function formatBytes(bytes) {
|
|
2067
|
+
if (bytes < 1024)
|
|
2068
|
+
return `${bytes}B`;
|
|
2069
|
+
if (bytes < 1024 * 1024)
|
|
2070
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
2071
|
+
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// pkgs/@akanjs/devkit/frontendBuild/ssrBaseArtifactBuilder.ts
|
|
2075
|
+
import path13 from "path";
|
|
2076
|
+
import { optimize } from "@tailwindcss/node";
|
|
2077
|
+
function prepareCssAsset(command, basePath, cssText) {
|
|
2078
|
+
return optimize(cssText, { file: `${basePath || "root"}.css`, minify: command === "build" }).code;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
class SsrBaseArtifactBuilder {
|
|
2082
|
+
#app;
|
|
2083
|
+
#command;
|
|
2084
|
+
#artifactDir;
|
|
2085
|
+
#absArtifactDir;
|
|
2086
|
+
#started = Date.now();
|
|
2087
|
+
constructor(app, command = "start") {
|
|
2088
|
+
this.#app = app;
|
|
2089
|
+
this.#command = command;
|
|
2090
|
+
this.#artifactDir = `${command === "build" ? app.dist.cwdPath : app.cwdPath}/.akan/artifact`;
|
|
2091
|
+
this.#absArtifactDir = path13.resolve(this.#artifactDir);
|
|
2092
|
+
}
|
|
2093
|
+
async build() {
|
|
2094
|
+
const akanConfig = await this.#app.getConfig();
|
|
2095
|
+
const { rscClientUrl, rscRuntimeClientManifest, rscRuntimeSsrManifest, vendorMap } = await this.#buildRuntimeClientEntries();
|
|
2096
|
+
const pageKeys = await this.#app.getPageKeys();
|
|
2097
|
+
this.#app.verbose(`[base-artifact] discovered ${pageKeys.length} route files under ${this.#app.cwdPath}/page`);
|
|
2098
|
+
const pageEntries = await resolveSsrPageEntriesForApp(this.#app, pageKeys);
|
|
2099
|
+
const { cssCompiler, optimizedFonts, cssAssets } = await this.#buildStyleAssets();
|
|
2100
|
+
const pagesBundle = await new PagesBundleBuilder(this.#app, this.#command, pageEntries).build();
|
|
2101
|
+
this.#app.verbose(`[base-artifact] pages bundle -> ${pagesBundle.bundlePath} (buildId=${pagesBundle.buildId}, splitting=${pagesBundle.splitting}, entry=${pagesBundle.entryBytes} bytes, outputs=${pagesBundle.outputCount}, chunks=${pagesBundle.chunkCount}, total=${pagesBundle.outputBytes} bytes)`);
|
|
2102
|
+
const seedIndex = computeRouteSeedIndex(pageEntries);
|
|
2103
|
+
const seedIndexPath = await saveRouteSeedIndex(this.#absArtifactDir, seedIndex, {
|
|
2104
|
+
production: this.#command === "build"
|
|
2105
|
+
});
|
|
2106
|
+
this.#app.verbose(`[base-artifact] route seed index -> ${seedIndexPath} entries=${seedIndex.entries.length} globalLayouts=${seedIndex.globalLayoutFiles.length}`);
|
|
2107
|
+
const artifact = {
|
|
2108
|
+
rscClientUrl,
|
|
2109
|
+
rscRuntimeClientManifest,
|
|
2110
|
+
rscRuntimeSsrManifest,
|
|
2111
|
+
vendorMap,
|
|
2112
|
+
cssAssets,
|
|
2113
|
+
pagesBundlePath: this.#command === "build" ? path13.relative(this.#absArtifactDir, pagesBundle.bundlePath) : pagesBundle.bundlePath,
|
|
2114
|
+
pagesBundleBuildId: pagesBundle.buildId,
|
|
2115
|
+
domains: [...akanConfig.domains],
|
|
2116
|
+
subRoutes: Object.fromEntries(Array.from(akanConfig.subRoutes.entries()).map(([basePath, domains]) => [basePath, [...domains]])),
|
|
2117
|
+
basePaths: [...akanConfig.basePaths],
|
|
2118
|
+
branches: [...akanConfig.branches],
|
|
2119
|
+
i18n: akanConfig.i18n,
|
|
2120
|
+
imageConfig: akanConfig.images,
|
|
2121
|
+
deepLinkAssociations: Object.values(akanConfig.mobile.targets).filter((target) => (target.deepLinks?.domains?.length ?? 0) > 0).map((target) => ({
|
|
2122
|
+
targetName: target.name,
|
|
2123
|
+
appId: target.appId,
|
|
2124
|
+
domains: target.deepLinks?.domains ?? [],
|
|
2125
|
+
iosTeamId: target.deepLinks?.ios?.teamId,
|
|
2126
|
+
androidSha256CertFingerprints: target.deepLinks?.android?.sha256CertFingerprints
|
|
2127
|
+
}))
|
|
2128
|
+
};
|
|
2129
|
+
await Bun.write(path13.join(this.#absArtifactDir, "base-artifact.json"), `${JSON.stringify(artifact, null, 2)}
|
|
2130
|
+
`);
|
|
2131
|
+
this.#app.verbose(`[base-artifact] complete in ${Date.now() - this.#started}ms`);
|
|
2132
|
+
return { artifact, seedIndex, cssCompiler, optimizedFonts };
|
|
2133
|
+
}
|
|
2134
|
+
async#buildRuntimeClientEntries() {
|
|
2135
|
+
const akanServerPath = await this.#resolveAkanServerPath();
|
|
2136
|
+
const rscClientEntry = path13.resolve(akanServerPath, "rscClient.tsx");
|
|
2137
|
+
const rscSegmentOutletEntry = path13.resolve(akanServerPath, "rscSegmentOutlet.tsx");
|
|
2138
|
+
const vendorEntries = VENDOR_SPECIFIERS.map((specifier) => ({
|
|
2139
|
+
specifier,
|
|
2140
|
+
absPath: path13.resolve(akanServerPath, "vendor", `${specifier.replaceAll("/", "-").replaceAll(".", "-")}.ts`)
|
|
2141
|
+
}));
|
|
2142
|
+
const entries = [rscClientEntry, rscSegmentOutletEntry, ...vendorEntries.map((v) => v.absPath)];
|
|
2143
|
+
const clientBundle = await new ClientEntriesBundler({ app: this.#app, entries, command: this.#command }).bundle();
|
|
2144
|
+
const ssrBundle = await new ClientEntriesBundler({
|
|
2145
|
+
app: this.#app,
|
|
2146
|
+
entries: [rscSegmentOutletEntry],
|
|
2147
|
+
...RouteClientBuilder.resolveSsrClientExternalOptions(this.#command),
|
|
2148
|
+
outputSubdir: "client-ssr",
|
|
2149
|
+
command: this.#command
|
|
2150
|
+
}).bundle();
|
|
2151
|
+
const rscClientUrl = clientBundle.entryUrlsByAbsPath.get(rscClientEntry) ?? "";
|
|
2152
|
+
const rscRuntimeSsrManifest = {
|
|
2153
|
+
moduleLoading: null,
|
|
2154
|
+
moduleMap: Object.fromEntries(Object.entries(clientBundle.manifest).map(([key, row]) => {
|
|
2155
|
+
const ssrOutput = ssrBundle.entryOutputAbsByAbsPath.get(rscSegmentOutletEntry);
|
|
2156
|
+
if (!ssrOutput || key !== `${clientBundle.clientReferenceIdByAbsPath.get(rscSegmentOutletEntry)}#${row.name}`) {
|
|
2157
|
+
return null;
|
|
2158
|
+
}
|
|
2159
|
+
return [
|
|
2160
|
+
row.id,
|
|
2161
|
+
{ [row.name]: { id: ssrOutput, chunks: [ssrOutput, ssrOutput], name: row.name, async: true } }
|
|
2162
|
+
];
|
|
2163
|
+
}).filter((entry) => Boolean(entry)))
|
|
2164
|
+
};
|
|
2165
|
+
const vendorMap = Object.fromEntries(vendorEntries.map(({ specifier, absPath }) => [specifier, clientBundle.entryUrlsByAbsPath.get(absPath) ?? ""]));
|
|
2166
|
+
this.#app.verbose(`[base-artifact] rscClientUrl=${rscClientUrl} vendors=${Object.keys(vendorMap).length}`);
|
|
2167
|
+
return {
|
|
2168
|
+
rscClientUrl,
|
|
2169
|
+
rscRuntimeClientManifest: clientBundle.manifest,
|
|
2170
|
+
rscRuntimeSsrManifest,
|
|
2171
|
+
vendorMap
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
async#resolveAkanServerPath() {
|
|
2175
|
+
const candidates = [];
|
|
2176
|
+
try {
|
|
2177
|
+
candidates.push(path13.dirname(Bun.resolveSync("akanjs/server", this.#app.workspace.workspaceRoot)));
|
|
2178
|
+
} catch {}
|
|
2179
|
+
candidates.push(path13.join(this.#app.workspace.workspaceRoot, "pkgs/akanjs/server"), path13.join(this.#app.workspace.workspaceRoot, "node_modules/akanjs/server"));
|
|
2180
|
+
try {
|
|
2181
|
+
candidates.push(path13.dirname(Bun.resolveSync("akanjs/server", path13.dirname(Bun.main))));
|
|
2182
|
+
} catch {}
|
|
2183
|
+
candidates.push(path13.join(path13.dirname(Bun.main), "node_modules/akanjs/server"), path13.join(path13.dirname(Bun.main), "../../akanjs/server"), path13.resolve(import.meta.dir, "../../server"), path13.resolve(import.meta.dir, "../server"));
|
|
2184
|
+
for (const candidate of candidates) {
|
|
2185
|
+
if (await Bun.file(path13.join(candidate, "rscClient.tsx")).exists())
|
|
2186
|
+
return candidate;
|
|
2187
|
+
}
|
|
2188
|
+
throw new Error(`[base-artifact] failed to locate akanjs/server; looked in: ${candidates.join(", ")}`);
|
|
2189
|
+
}
|
|
2190
|
+
async#buildStyleAssets() {
|
|
2191
|
+
const cssCompiler = new CssCompiler(this.#app);
|
|
2192
|
+
const cssByBasePath = await cssCompiler.getCssByBasePath();
|
|
2193
|
+
const optimizedFonts = await new FontOptimizer(this.#app, this.#command).optimize();
|
|
2194
|
+
const cssAssets = Object.fromEntries(await Promise.all(Object.entries(cssByBasePath).flatMap(([basePath, baseCssText]) => {
|
|
2195
|
+
const cssText = [baseCssText, optimizedFonts.css].filter(Boolean).join(`
|
|
2196
|
+
`);
|
|
2197
|
+
if (!cssText)
|
|
2198
|
+
return [];
|
|
2199
|
+
return [this.#writeCssAsset(basePath, cssText)];
|
|
2200
|
+
})));
|
|
2201
|
+
if (optimizedFonts.files.length > 0)
|
|
2202
|
+
this.#app.verbose(`[base-artifact] optimized ${optimizedFonts.files.length} font files`);
|
|
2203
|
+
return { cssCompiler, optimizedFonts, cssAssets };
|
|
2204
|
+
}
|
|
2205
|
+
async#writeCssAsset(basePath, cssText) {
|
|
2206
|
+
const cssAssetName = basePath || "root";
|
|
2207
|
+
const preparedCssText = await prepareCssAsset(this.#command, basePath, cssText);
|
|
2208
|
+
const cssHash = Bun.hash(`${basePath}
|
|
2209
|
+
${preparedCssText}`).toString(36);
|
|
2210
|
+
const [cssRelPath, cssUrl] = [
|
|
2211
|
+
`styles/${cssAssetName}-${cssHash}.css`,
|
|
2212
|
+
`/_akan/styles/${cssAssetName}-${cssHash}.css`
|
|
2213
|
+
];
|
|
2214
|
+
await Bun.write(path13.join(this.#absArtifactDir, cssRelPath), preparedCssText);
|
|
2215
|
+
this.#app.verbose(`[base-artifact] wrote ${preparedCssText.length} bytes of CSS for ${basePath} -> ${cssRelPath}`);
|
|
2216
|
+
return [basePath, { cssUrl, cssRelPath }];
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
export { AllRoutesBuilder, CsrArtifactBuilder, CssCompiler, FontOptimizer, PagesBundleBuilder, precompressArtifacts, SsrBaseArtifactBuilder };
|