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