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