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