@akanjs/cli 2.4.1-rc.6 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.build-stamp +1 -1
- package/{agent.command-t4346579.js → agent.command-sa12ccz3.js} +2 -2
- package/{application.command-zkmeemdt.js → application.command-jv63vv7z.js} +4 -4
- package/{applicationBuildRunner-5w8j9yp7.js → applicationBuildRunner-vzjdgv1g.js} +3 -2
- package/buildBatch.proc.js +3 -2
- package/{cloud.command-5ztqxppm.js → cloud.command-8z2nc6b2.js} +7 -7
- package/{context.command-smwxwgwf.js → context.command-0m854xme.js} +8 -8
- package/{guideline.command-1r0esdjs.js → guideline.command-dsdpae45.js} +1 -1
- package/incrementalBuilder.proc.js +3 -3
- package/{index-3sd2vape.js → index-1b9f27pm.js} +1 -1
- package/{index-fh990y67.js → index-3a4vntr0.js} +1 -1
- package/{index-jbtn8h1y.js → index-81epkybm.js} +1 -1
- package/{index-0fn1r7gg.js → index-bjpxzr6s.js} +31 -4
- package/{index-ahpcr9ss.js → index-d2e2y85b.js} +1 -1
- package/{index-0jwvs8vp.js → index-e3b8ms64.js} +6 -6
- package/{index-s9yajzkj.js → index-eja0sbm6.js} +4 -4
- package/{index-wh201a76.js → index-h6pevatb.js} +2 -2
- package/{index-2v6a4wd5.js → index-m04as81s.js} +2 -2
- package/index-m0vg9n8z.js +2219 -0
- package/{index-n5y2gbf2.js → index-n2mp0f48.js} +200 -19
- package/{index-nv7et4t3.js → index-p9f1rpbp.js} +1 -1
- package/{index-qpz4csbs.js → index-phj3ewxv.js} +7 -1
- package/{index-pya1h7wx.js → index-q4sc46ec.js} +1 -1
- package/{index-y4kg682s.js → index-qacv44mm.js} +1 -1
- package/index-rh16j3c7.js +2166 -0
- package/{index-gr4cjv99.js → index-zw4aqvwh.js} +2 -2
- package/index.js +17 -17
- package/{library.command-jrew04m6.js → library.command-fhx5ce8n.js} +2 -2
- package/{localRegistry.command-m08dkm1b.js → localRegistry.command-18f61945.js} +6 -6
- package/{module.command-p98t7522.js → module.command-29wvnb56.js} +3 -3
- package/{package.command-57dyrfmn.js → package.command-46r1dncm.js} +2 -2
- package/package.json +2 -2
- package/{page.command-tas6f3na.js → page.command-vxvqdzc0.js} +2 -2
- package/{primitive.command-bjrbsakw.js → primitive.command-yk622wmt.js} +4 -4
- package/{quality.command-zavaafbw.js → quality.command-p0mb5xwt.js} +1 -1
- package/{repair.command-je57wx67.js → repair.command-nptckp6w.js} +2 -2
- package/{scalar.command-gqqyy1kr.js → scalar.command-bss14hez.js} +2 -2
- package/templates/workspaceRoot/.cursor/rules/coding-comments.mdc.template +19 -0
- package/templates/workspaceRoot/package.json.template +3 -1
- package/templates/workspaceRoot/patches/zod@3.25.76.patch.template +22 -0
- package/templates/workspaceRoot/patches/zod@4.4.3.patch.template +11 -0
- package/{workflow.command-d6mrc47s.js → workflow.command-7fhqb33d.js} +6 -6
- package/{workspace.command-bbg96z8k.js → workspace.command-xkcec7ky.js} +15 -15
- package/index-khzzttv1.js +0 -4274
package/index-khzzttv1.js
DELETED
|
@@ -1,4274 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
import {
|
|
3
|
-
__require
|
|
4
|
-
} from "./index-r24hmh0q.js";
|
|
5
|
-
|
|
6
|
-
// pkgs/@akanjs/devkit/frontendBuild/clientEntryDiscovery.ts
|
|
7
|
-
import path3 from "path";
|
|
8
|
-
|
|
9
|
-
// pkgs/@akanjs/devkit/transforms/barrelAnalyzer.ts
|
|
10
|
-
import path from "path";
|
|
11
|
-
import { Logger } from "akanjs/common";
|
|
12
|
-
var REEXPORT_RE = /(?:^|\n)\s*export\s+(?:type\s+)?(?:(\*)(?:\s+as\s+(\w+))?|\{\s*([^}]*?)\s*\})\s+from\s+(["'])([^"']+)\4;?/g;
|
|
13
|
-
var LOCAL_NAMED_RE = /(?:^|\n)\s*export\s+\{\s*([^}]*?)\s*\}(?!\s*from)/g;
|
|
14
|
-
var CANDIDATE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
15
|
-
|
|
16
|
-
class BarrelAnalyzer {
|
|
17
|
-
#logger = new Logger("BarrelAnalyzer");
|
|
18
|
-
#opts;
|
|
19
|
-
#cache = new Map;
|
|
20
|
-
#tsTranspiler = new Bun.Transpiler({ loader: "ts" });
|
|
21
|
-
#tsxTranspiler = new Bun.Transpiler({ loader: "tsx" });
|
|
22
|
-
constructor(opts) {
|
|
23
|
-
this.#opts = opts;
|
|
24
|
-
}
|
|
25
|
-
analyze(pkgName) {
|
|
26
|
-
const cached = this.#cache.get(pkgName);
|
|
27
|
-
if (cached)
|
|
28
|
-
return cached;
|
|
29
|
-
const promise = this.#analyzeSafe(pkgName);
|
|
30
|
-
this.#cache.set(pkgName, promise);
|
|
31
|
-
return promise;
|
|
32
|
-
}
|
|
33
|
-
async#analyzeSafe(pkgName) {
|
|
34
|
-
try {
|
|
35
|
-
return await this.#analyzeUncached(pkgName);
|
|
36
|
-
} catch (err) {
|
|
37
|
-
this.#logger.error(`analyze failed for ${pkgName}: ${err.message}`);
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
async#analyzeUncached(pkgName) {
|
|
42
|
-
const pkg = await this.#opts.resolvePackage(pkgName);
|
|
43
|
-
if (!pkg)
|
|
44
|
-
return null;
|
|
45
|
-
const map = new Map;
|
|
46
|
-
const visited = new Set;
|
|
47
|
-
await this.#walk(pkg.entryFile, pkg, map, visited);
|
|
48
|
-
return map;
|
|
49
|
-
}
|
|
50
|
-
async#walk(absFile, pkg, map, visited) {
|
|
51
|
-
if (visited.has(absFile))
|
|
52
|
-
return;
|
|
53
|
-
visited.add(absFile);
|
|
54
|
-
const source = await readIfExists(absFile);
|
|
55
|
-
if (source === null)
|
|
56
|
-
return;
|
|
57
|
-
const currentSubpath = this.#subpathFor(pkg, absFile);
|
|
58
|
-
if (!currentSubpath)
|
|
59
|
-
return;
|
|
60
|
-
const authoritative = this.#scanExports(source, absFile);
|
|
61
|
-
authoritative.delete("default");
|
|
62
|
-
const attributed = new Set;
|
|
63
|
-
REEXPORT_RE.lastIndex = 0;
|
|
64
|
-
let m = REEXPORT_RE.exec(source);
|
|
65
|
-
while (m !== null) {
|
|
66
|
-
const star = m[1];
|
|
67
|
-
const nsAs = m[2];
|
|
68
|
-
const namedList = m[3];
|
|
69
|
-
const spec = m[5] ?? "";
|
|
70
|
-
m = REEXPORT_RE.exec(source);
|
|
71
|
-
if (!isRelative(spec))
|
|
72
|
-
continue;
|
|
73
|
-
if (star) {
|
|
74
|
-
if (nsAs) {
|
|
75
|
-
authoritative.delete(nsAs);
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
const targetAbs = await this.#resolveRel(absFile, spec);
|
|
79
|
-
if (!targetAbs)
|
|
80
|
-
continue;
|
|
81
|
-
await this.#walk(targetAbs, pkg, map, visited);
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
if (namedList !== undefined) {
|
|
85
|
-
const targetAbs = await this.#resolveRel(absFile, spec);
|
|
86
|
-
if (!targetAbs)
|
|
87
|
-
continue;
|
|
88
|
-
const targetSubpath = this.#subpathFor(pkg, targetAbs);
|
|
89
|
-
if (!targetSubpath)
|
|
90
|
-
continue;
|
|
91
|
-
for (const item of parseNamedList(namedList)) {
|
|
92
|
-
if (item.isType)
|
|
93
|
-
continue;
|
|
94
|
-
if (item.imported === "default")
|
|
95
|
-
continue;
|
|
96
|
-
if (!authoritative.has(item.local))
|
|
97
|
-
continue;
|
|
98
|
-
attributed.add(item.local);
|
|
99
|
-
if (!map.has(item.local)) {
|
|
100
|
-
map.set(item.local, { subpath: targetSubpath, originalName: item.imported });
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
LOCAL_NAMED_RE.lastIndex = 0;
|
|
106
|
-
let n = LOCAL_NAMED_RE.exec(source);
|
|
107
|
-
while (n !== null) {
|
|
108
|
-
const body = n[1] ?? "";
|
|
109
|
-
n = LOCAL_NAMED_RE.exec(source);
|
|
110
|
-
for (const item of parseNamedList(body)) {
|
|
111
|
-
if (item.isType)
|
|
112
|
-
continue;
|
|
113
|
-
if (item.imported === "default")
|
|
114
|
-
continue;
|
|
115
|
-
if (!authoritative.has(item.local))
|
|
116
|
-
continue;
|
|
117
|
-
attributed.add(item.local);
|
|
118
|
-
if (!map.has(item.local)) {
|
|
119
|
-
map.set(item.local, { subpath: currentSubpath, originalName: item.imported });
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
for (const name of authoritative) {
|
|
124
|
-
if (attributed.has(name))
|
|
125
|
-
continue;
|
|
126
|
-
if (map.has(name))
|
|
127
|
-
continue;
|
|
128
|
-
map.set(name, { subpath: currentSubpath, originalName: name });
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
#scanExports(source, absFile) {
|
|
132
|
-
try {
|
|
133
|
-
const transpiler = [".tsx", ".jsx"].includes(path.extname(absFile)) ? this.#tsxTranspiler : this.#tsTranspiler;
|
|
134
|
-
const { exports } = transpiler.scan(source);
|
|
135
|
-
return new Set(exports);
|
|
136
|
-
} catch (err) {
|
|
137
|
-
this.#logger.error(`scan failed: ${err.message}`);
|
|
138
|
-
return new Set;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
#subpathFor(pkg, absFile) {
|
|
142
|
-
const rel = path.relative(pkg.pkgDir, absFile);
|
|
143
|
-
if (!rel || rel.startsWith("..") || path.isAbsolute(rel))
|
|
144
|
-
return null;
|
|
145
|
-
if (pkg.preserveFilePath)
|
|
146
|
-
return `${pkg.pkgName}/${rel.split(path.sep).join("/")}`;
|
|
147
|
-
const noExt = stripKnownExt(rel);
|
|
148
|
-
const tail = collapseIndex(noExt);
|
|
149
|
-
if (tail === "")
|
|
150
|
-
return pkg.pkgName;
|
|
151
|
-
return `${pkg.pkgName}/${tail.split(path.sep).join("/")}`;
|
|
152
|
-
}
|
|
153
|
-
async#resolveRel(fromFile, relSpec) {
|
|
154
|
-
if (this.#opts.resolveRelative)
|
|
155
|
-
return this.#opts.resolveRelative(fromFile, relSpec);
|
|
156
|
-
return defaultResolveRelative(fromFile, relSpec);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
var parseNamedList = (listBody) => {
|
|
160
|
-
const out = [];
|
|
161
|
-
for (const raw of listBody.split(",")) {
|
|
162
|
-
const s = raw.trim();
|
|
163
|
-
if (!s)
|
|
164
|
-
continue;
|
|
165
|
-
let rest = s;
|
|
166
|
-
let isType = false;
|
|
167
|
-
if (rest.startsWith("type ")) {
|
|
168
|
-
isType = true;
|
|
169
|
-
rest = rest.slice(5).trim();
|
|
170
|
-
}
|
|
171
|
-
const asMatch = /^(\w+)\s+as\s+(\w+)$/.exec(rest);
|
|
172
|
-
if (asMatch) {
|
|
173
|
-
out.push({ imported: asMatch[1] ?? "", local: asMatch[2] ?? "", isType });
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
if (/^\w+$/.test(rest)) {
|
|
177
|
-
out.push({ imported: rest, local: rest, isType });
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return out;
|
|
181
|
-
};
|
|
182
|
-
var isRelative = (spec) => {
|
|
183
|
-
return spec.startsWith("./") || spec.startsWith("../") || spec === "." || spec === "..";
|
|
184
|
-
};
|
|
185
|
-
var readIfExists = async (absFile) => {
|
|
186
|
-
const file = Bun.file(absFile);
|
|
187
|
-
if (!await file.exists())
|
|
188
|
-
return null;
|
|
189
|
-
return file.text();
|
|
190
|
-
};
|
|
191
|
-
var defaultResolveRelative = async (fromFile, relSpec) => {
|
|
192
|
-
const baseDir = path.dirname(fromFile);
|
|
193
|
-
const joined = path.resolve(baseDir, relSpec);
|
|
194
|
-
if (path.extname(joined)) {
|
|
195
|
-
if (await Bun.file(joined).exists())
|
|
196
|
-
return joined;
|
|
197
|
-
return null;
|
|
198
|
-
}
|
|
199
|
-
for (const ext of CANDIDATE_EXTS) {
|
|
200
|
-
const cand = joined + ext;
|
|
201
|
-
if (await Bun.file(cand).exists())
|
|
202
|
-
return cand;
|
|
203
|
-
}
|
|
204
|
-
for (const ext of CANDIDATE_EXTS) {
|
|
205
|
-
const cand = path.join(joined, `index${ext}`);
|
|
206
|
-
if (await Bun.file(cand).exists())
|
|
207
|
-
return cand;
|
|
208
|
-
}
|
|
209
|
-
return null;
|
|
210
|
-
};
|
|
211
|
-
var stripKnownExt = (relPath) => {
|
|
212
|
-
for (const ext of CANDIDATE_EXTS) {
|
|
213
|
-
if (relPath.endsWith(ext))
|
|
214
|
-
return relPath.slice(0, -ext.length);
|
|
215
|
-
}
|
|
216
|
-
return relPath;
|
|
217
|
-
};
|
|
218
|
-
var collapseIndex = (relPathNoExt) => {
|
|
219
|
-
const parts = relPathNoExt.split(path.sep);
|
|
220
|
-
if (parts[parts.length - 1] === "index")
|
|
221
|
-
parts.pop();
|
|
222
|
-
return parts.join(path.sep);
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
// pkgs/@akanjs/devkit/transforms/barrelImportsPlugin.ts
|
|
226
|
-
import path2 from "path";
|
|
227
|
-
import ts from "typescript";
|
|
228
|
-
var createBarrelImportsPlugin = async (app, { skipPath = defaultSkipPath, pipeAfter } = {}) => {
|
|
229
|
-
const akanConfig = await app.getConfig();
|
|
230
|
-
const barrels = [...new Set(akanConfig.barrelImports)].filter(Boolean);
|
|
231
|
-
const analyzer = new BarrelAnalyzer({
|
|
232
|
-
resolvePackage: await createTsconfigPackageResolver(app)
|
|
233
|
-
});
|
|
234
|
-
return {
|
|
235
|
-
name: "barrel-imports",
|
|
236
|
-
setup(build) {
|
|
237
|
-
build.onLoad({
|
|
238
|
-
filter: /^(?:(?!.*[\\/]node_modules[\\/]).*|.*[\\/]node_modules[\\/]akanjs[\\/].*)\.(tsx|ts|jsx|js)(\?v=\d+)?$/
|
|
239
|
-
}, async (args) => {
|
|
240
|
-
const realPath = args.path.replace(/\?v=\d+$/, "");
|
|
241
|
-
const loader = loaderFor(realPath);
|
|
242
|
-
if (skipPath(realPath)) {
|
|
243
|
-
const raw = await Bun.file(realPath).text();
|
|
244
|
-
return { contents: raw, loader };
|
|
245
|
-
}
|
|
246
|
-
let source = await Bun.file(realPath).text();
|
|
247
|
-
const hasMacroAttr = MACRO_ATTR_RE.test(source);
|
|
248
|
-
if (!hasMacroAttr && barrels.length > 0) {
|
|
249
|
-
let maybe = false;
|
|
250
|
-
for (const b of barrels) {
|
|
251
|
-
if (source.includes(b)) {
|
|
252
|
-
maybe = true;
|
|
253
|
-
break;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
if (maybe) {
|
|
257
|
-
const rewritten = await rewriteBarrelImports(source, barrels, analyzer);
|
|
258
|
-
if (rewritten !== null)
|
|
259
|
-
source = rewritten;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
if (pipeAfter) {
|
|
263
|
-
const piped = await pipeAfter(source, { path: realPath });
|
|
264
|
-
if (piped !== null)
|
|
265
|
-
source = piped;
|
|
266
|
-
}
|
|
267
|
-
return { contents: source, loader };
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
|
-
};
|
|
272
|
-
var createTsconfigPackageResolver = async (app) => {
|
|
273
|
-
const tsconfig = await app.getTsConfig();
|
|
274
|
-
const tsconfigPaths = tsconfig.compilerOptions.paths ?? {};
|
|
275
|
-
const wildcardEntries = Object.entries(tsconfigPaths).filter(([k]) => k.endsWith("/*")).map(([k, v]) => ({
|
|
276
|
-
prefix: k.slice(0, -1),
|
|
277
|
-
replacements: v
|
|
278
|
-
})).sort((a, b) => b.prefix.length - a.prefix.length);
|
|
279
|
-
return async (pkgName) => {
|
|
280
|
-
const exact = tsconfigPaths[pkgName];
|
|
281
|
-
if (exact && exact.length > 0) {
|
|
282
|
-
const raw = exact[0];
|
|
283
|
-
if (!raw)
|
|
284
|
-
return null;
|
|
285
|
-
const entryFile = path2.resolve(app.workspace.workspaceRoot, raw);
|
|
286
|
-
if (!await Bun.file(entryFile).exists())
|
|
287
|
-
return null;
|
|
288
|
-
const parsed = path2.parse(entryFile);
|
|
289
|
-
const lastSlash = pkgName.lastIndexOf("/");
|
|
290
|
-
if (parsed.name !== "index" && lastSlash !== -1) {
|
|
291
|
-
const facet = pkgName.slice(lastSlash + 1);
|
|
292
|
-
const parentSpec = pkgName.slice(0, lastSlash);
|
|
293
|
-
if (facet === parsed.name && parentSpec.length > 0) {
|
|
294
|
-
return { pkgName: parentSpec, entryFile, pkgDir: parsed.dir };
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
return { pkgName, entryFile, pkgDir: path2.dirname(entryFile) };
|
|
298
|
-
}
|
|
299
|
-
for (const { prefix, replacements } of wildcardEntries) {
|
|
300
|
-
if (!pkgName.startsWith(prefix))
|
|
301
|
-
continue;
|
|
302
|
-
const suffix = pkgName.slice(prefix.length);
|
|
303
|
-
for (const repl of replacements) {
|
|
304
|
-
if (!repl)
|
|
305
|
-
continue;
|
|
306
|
-
const replPath = repl.endsWith("/*") ? repl.slice(0, -1) : repl;
|
|
307
|
-
const candidate = path2.resolve(app.workspace.workspaceRoot, replPath + suffix);
|
|
308
|
-
for (const ext of CANDIDATE_EXTS2) {
|
|
309
|
-
const file = `${candidate}${ext}`;
|
|
310
|
-
if (await Bun.file(file).exists()) {
|
|
311
|
-
const lastSlash = pkgName.lastIndexOf("/");
|
|
312
|
-
if (lastSlash !== -1) {
|
|
313
|
-
const parentSpec = pkgName.slice(0, lastSlash);
|
|
314
|
-
if (parentSpec.length > 0) {
|
|
315
|
-
return { pkgName: parentSpec, entryFile: file, pkgDir: path2.dirname(file) };
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
return { pkgName, entryFile: file, pkgDir: path2.dirname(file) };
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
for (const ext of CANDIDATE_EXTS2) {
|
|
322
|
-
const file = path2.join(candidate, `index${ext}`);
|
|
323
|
-
if (await Bun.file(file).exists()) {
|
|
324
|
-
return { pkgName, entryFile: file, pkgDir: candidate };
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
return null;
|
|
329
|
-
}
|
|
330
|
-
const exported = await resolveNodePackageExport(app.workspace.workspaceRoot, pkgName);
|
|
331
|
-
if (exported)
|
|
332
|
-
return exported;
|
|
333
|
-
const pkgJsonPath = path2.join(app.workspace.workspaceRoot, "node_modules", pkgName, "package.json");
|
|
334
|
-
if (!await Bun.file(pkgJsonPath).exists())
|
|
335
|
-
return null;
|
|
336
|
-
try {
|
|
337
|
-
const pkgJson = JSON.parse(await Bun.file(pkgJsonPath).text());
|
|
338
|
-
const rel = pkgJson.module ?? pkgJson.main ?? "index.js";
|
|
339
|
-
const entryFile = path2.resolve(path2.dirname(pkgJsonPath), rel);
|
|
340
|
-
if (!await Bun.file(entryFile).exists())
|
|
341
|
-
return null;
|
|
342
|
-
return { pkgName, entryFile, pkgDir: path2.dirname(pkgJsonPath) };
|
|
343
|
-
} catch {
|
|
344
|
-
return null;
|
|
345
|
-
}
|
|
346
|
-
};
|
|
347
|
-
};
|
|
348
|
-
var CANDIDATE_EXTS2 = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
349
|
-
var NODE_MODULES_RE = /[\\/]node_modules[\\/]/;
|
|
350
|
-
var AKANJS_NODE_MODULE_RE = /[\\/]node_modules[\\/]akanjs[\\/]/;
|
|
351
|
-
var defaultSkipPath = (absPath) => NODE_MODULES_RE.test(absPath) && !AKANJS_NODE_MODULE_RE.test(absPath);
|
|
352
|
-
var resolveNodePackageExport = async (workspaceRoot, specifier) => {
|
|
353
|
-
const packageName = getPackageName(specifier);
|
|
354
|
-
if (!packageName)
|
|
355
|
-
return null;
|
|
356
|
-
const pkgJsonPath = path2.join(workspaceRoot, "node_modules", packageName, "package.json");
|
|
357
|
-
if (!await Bun.file(pkgJsonPath).exists())
|
|
358
|
-
return null;
|
|
359
|
-
try {
|
|
360
|
-
const pkgDir = path2.dirname(pkgJsonPath);
|
|
361
|
-
const pkgJson = JSON.parse(await Bun.file(pkgJsonPath).text());
|
|
362
|
-
const subpath = specifier === packageName ? "." : `.${specifier.slice(packageName.length)}`;
|
|
363
|
-
const exported = resolvePackageExport(pkgJson.exports, subpath);
|
|
364
|
-
const rel = exported ?? (subpath === "." ? pkgJson.module ?? pkgJson.main ?? "index.js" : null);
|
|
365
|
-
if (!rel || !rel.startsWith("."))
|
|
366
|
-
return null;
|
|
367
|
-
const entryFile = await resolveFileCandidate(path2.resolve(pkgDir, rel));
|
|
368
|
-
if (!entryFile)
|
|
369
|
-
return null;
|
|
370
|
-
const pkgEntryName = specifier;
|
|
371
|
-
return { pkgName: pkgEntryName, entryFile, pkgDir: path2.dirname(entryFile), preserveFilePath: true };
|
|
372
|
-
} catch {
|
|
373
|
-
return null;
|
|
374
|
-
}
|
|
375
|
-
};
|
|
376
|
-
var resolveExportValue = (value) => {
|
|
377
|
-
if (!value)
|
|
378
|
-
return null;
|
|
379
|
-
if (typeof value === "string")
|
|
380
|
-
return value;
|
|
381
|
-
if (Array.isArray(value)) {
|
|
382
|
-
for (const item of value) {
|
|
383
|
-
const resolved = resolveExportValue(item);
|
|
384
|
-
if (resolved)
|
|
385
|
-
return resolved;
|
|
386
|
-
}
|
|
387
|
-
return null;
|
|
388
|
-
}
|
|
389
|
-
for (const condition of ["source", "import", "default", "types"]) {
|
|
390
|
-
const resolved = resolveExportValue(value[condition]);
|
|
391
|
-
if (resolved)
|
|
392
|
-
return resolved;
|
|
393
|
-
}
|
|
394
|
-
return null;
|
|
395
|
-
};
|
|
396
|
-
var resolvePackageExport = (exportsMap, subpath) => {
|
|
397
|
-
if (!exportsMap)
|
|
398
|
-
return null;
|
|
399
|
-
const exact = resolveExportValue(exportsMap[subpath]);
|
|
400
|
-
if (exact)
|
|
401
|
-
return exact;
|
|
402
|
-
for (const [key, value] of Object.entries(exportsMap)) {
|
|
403
|
-
const starIdx = key.indexOf("*");
|
|
404
|
-
if (starIdx === -1)
|
|
405
|
-
continue;
|
|
406
|
-
const prefix = key.slice(0, starIdx);
|
|
407
|
-
const suffix = key.slice(starIdx + 1);
|
|
408
|
-
if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix))
|
|
409
|
-
continue;
|
|
410
|
-
const wildcard = subpath.slice(prefix.length, subpath.length - suffix.length);
|
|
411
|
-
const resolved = resolveExportValue(value);
|
|
412
|
-
if (resolved)
|
|
413
|
-
return resolved.replace("*", wildcard);
|
|
414
|
-
}
|
|
415
|
-
return null;
|
|
416
|
-
};
|
|
417
|
-
var getPackageName = (specifier) => {
|
|
418
|
-
const parts = specifier.split("/");
|
|
419
|
-
if (!parts[0])
|
|
420
|
-
return null;
|
|
421
|
-
if (specifier.startsWith("@"))
|
|
422
|
-
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
|
|
423
|
-
return parts[0];
|
|
424
|
-
};
|
|
425
|
-
var resolveFileCandidate = async (candidate) => {
|
|
426
|
-
if (await Bun.file(candidate).exists())
|
|
427
|
-
return candidate;
|
|
428
|
-
if (path2.extname(candidate))
|
|
429
|
-
return null;
|
|
430
|
-
for (const ext of CANDIDATE_EXTS2) {
|
|
431
|
-
const file = `${candidate}${ext}`;
|
|
432
|
-
if (await Bun.file(file).exists())
|
|
433
|
-
return file;
|
|
434
|
-
}
|
|
435
|
-
for (const ext of CANDIDATE_EXTS2) {
|
|
436
|
-
const file = path2.join(candidate, `index${ext}`);
|
|
437
|
-
if (await Bun.file(file).exists())
|
|
438
|
-
return file;
|
|
439
|
-
}
|
|
440
|
-
return null;
|
|
441
|
-
};
|
|
442
|
-
var MACRO_ATTR_RE = /with\s*\{\s*type\s*:\s*["']macro["']\s*\}/;
|
|
443
|
-
var rewriteBarrelImports = async (source, barrels, analyzer) => {
|
|
444
|
-
const statements = findImportStatements(source);
|
|
445
|
-
if (statements.length === 0)
|
|
446
|
-
return null;
|
|
447
|
-
let changed = false;
|
|
448
|
-
let out = source;
|
|
449
|
-
for (let i = statements.length - 1;i >= 0; i--) {
|
|
450
|
-
const stmt = statements[i];
|
|
451
|
-
if (!stmt)
|
|
452
|
-
continue;
|
|
453
|
-
if (!barrels.includes(stmt.specifier))
|
|
454
|
-
continue;
|
|
455
|
-
const map = await analyzer.analyze(stmt.specifier);
|
|
456
|
-
if (!map || map.size === 0)
|
|
457
|
-
continue;
|
|
458
|
-
const replacement = rewriteSingleStatement(stmt, map);
|
|
459
|
-
if (replacement === null)
|
|
460
|
-
continue;
|
|
461
|
-
out = out.slice(0, stmt.start) + replacement + out.slice(stmt.end);
|
|
462
|
-
changed = true;
|
|
463
|
-
}
|
|
464
|
-
return changed ? out : null;
|
|
465
|
-
};
|
|
466
|
-
var findImportStatements = (source) => {
|
|
467
|
-
const statements = [];
|
|
468
|
-
const sourceFile = ts.createSourceFile("barrel-imports.tsx", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
469
|
-
for (const statement of sourceFile.statements) {
|
|
470
|
-
if (!ts.isImportDeclaration(statement))
|
|
471
|
-
continue;
|
|
472
|
-
if (!ts.isStringLiteral(statement.moduleSpecifier))
|
|
473
|
-
continue;
|
|
474
|
-
const importClause = statement.importClause;
|
|
475
|
-
if (!importClause)
|
|
476
|
-
continue;
|
|
477
|
-
const statementStart = statement.getStart(sourceFile);
|
|
478
|
-
const statementEnd = statement.end;
|
|
479
|
-
statements.push({
|
|
480
|
-
start: statementStart,
|
|
481
|
-
end: statementEnd,
|
|
482
|
-
clause: source.slice(importClause.getStart(sourceFile), importClause.end).trim(),
|
|
483
|
-
specifier: statement.moduleSpecifier.text,
|
|
484
|
-
trailingSemicolon: source.slice(statement.moduleSpecifier.end, statement.end).includes(";"),
|
|
485
|
-
raw: source.slice(statementStart, statementEnd)
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
return statements;
|
|
489
|
-
};
|
|
490
|
-
var parseImportClause = (clause) => {
|
|
491
|
-
let rest = clause.trim();
|
|
492
|
-
let typeOnly = false;
|
|
493
|
-
if (rest.startsWith("type ")) {
|
|
494
|
-
typeOnly = true;
|
|
495
|
-
rest = rest.slice(5).trim();
|
|
496
|
-
}
|
|
497
|
-
const parsed = { typeOnly };
|
|
498
|
-
const commaMatch = /^(\w+)\s*,\s*(.+)$/.exec(rest);
|
|
499
|
-
if (commaMatch) {
|
|
500
|
-
parsed.defaultImport = commaMatch[1];
|
|
501
|
-
rest = commaMatch[2] ?? "";
|
|
502
|
-
} else if (/^\w+$/.test(rest)) {
|
|
503
|
-
parsed.defaultImport = rest;
|
|
504
|
-
return parsed;
|
|
505
|
-
}
|
|
506
|
-
if (rest.startsWith("*")) {
|
|
507
|
-
const ns = /^\*\s+as\s+(\w+)$/.exec(rest);
|
|
508
|
-
if (!ns)
|
|
509
|
-
return null;
|
|
510
|
-
parsed.namespaceImport = ns[1];
|
|
511
|
-
return parsed;
|
|
512
|
-
}
|
|
513
|
-
if (rest.startsWith("{")) {
|
|
514
|
-
const close = rest.indexOf("}");
|
|
515
|
-
if (close === -1)
|
|
516
|
-
return null;
|
|
517
|
-
const inner = rest.slice(1, close);
|
|
518
|
-
parsed.named = parseNamedImportList(inner);
|
|
519
|
-
return parsed;
|
|
520
|
-
}
|
|
521
|
-
return parsed;
|
|
522
|
-
};
|
|
523
|
-
var parseNamedImportList = (body) => {
|
|
524
|
-
const out = [];
|
|
525
|
-
for (const raw of body.split(",")) {
|
|
526
|
-
const s = raw.trim();
|
|
527
|
-
if (!s)
|
|
528
|
-
continue;
|
|
529
|
-
let isType = false;
|
|
530
|
-
let rest = s;
|
|
531
|
-
if (rest.startsWith("type ")) {
|
|
532
|
-
isType = true;
|
|
533
|
-
rest = rest.slice(5).trim();
|
|
534
|
-
}
|
|
535
|
-
const asMatch = /^(\w+)\s+as\s+(\w+)$/.exec(rest);
|
|
536
|
-
if (asMatch) {
|
|
537
|
-
out.push({ imported: asMatch[1] ?? "", local: asMatch[2] ?? "", isType });
|
|
538
|
-
continue;
|
|
539
|
-
}
|
|
540
|
-
if (/^\w+$/.test(rest)) {
|
|
541
|
-
out.push({ imported: rest, local: rest, isType });
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
return out;
|
|
545
|
-
};
|
|
546
|
-
var rewriteSingleStatement = (stmt, map) => {
|
|
547
|
-
const clause = parseImportClause(stmt.clause);
|
|
548
|
-
if (!clause)
|
|
549
|
-
return null;
|
|
550
|
-
if (clause.namespaceImport)
|
|
551
|
-
return null;
|
|
552
|
-
if (clause.typeOnly && !clause.defaultImport)
|
|
553
|
-
return null;
|
|
554
|
-
if (!clause.named || clause.named.length === 0) {
|
|
555
|
-
return null;
|
|
556
|
-
}
|
|
557
|
-
const remaining = [];
|
|
558
|
-
const rewrites = new Map;
|
|
559
|
-
for (const item of clause.named) {
|
|
560
|
-
if (item.isType) {
|
|
561
|
-
remaining.push(item);
|
|
562
|
-
continue;
|
|
563
|
-
}
|
|
564
|
-
const target = map.get(item.imported);
|
|
565
|
-
if (!target) {
|
|
566
|
-
remaining.push(item);
|
|
567
|
-
continue;
|
|
568
|
-
}
|
|
569
|
-
const list = rewrites.get(target.subpath) ?? [];
|
|
570
|
-
list.push({ imported: target.originalName, local: item.local, isType: false });
|
|
571
|
-
rewrites.set(target.subpath, list);
|
|
572
|
-
}
|
|
573
|
-
if (rewrites.size === 0)
|
|
574
|
-
return null;
|
|
575
|
-
const lines = [];
|
|
576
|
-
const tail = ";";
|
|
577
|
-
if (shouldPreserveBarrelSideEffects(stmt.specifier)) {
|
|
578
|
-
lines.push(`import "${stmt.specifier}"${tail}`);
|
|
579
|
-
}
|
|
580
|
-
if (clause.defaultImport || remaining.length > 0) {
|
|
581
|
-
const parts = [];
|
|
582
|
-
if (clause.defaultImport)
|
|
583
|
-
parts.push(clause.defaultImport);
|
|
584
|
-
if (remaining.length > 0) {
|
|
585
|
-
parts.push(`{ ${remaining.map(serializeNamedItem).join(", ")} }`);
|
|
586
|
-
}
|
|
587
|
-
lines.push(`import ${parts.join(", ")} from "${stmt.specifier}"${tail}`);
|
|
588
|
-
}
|
|
589
|
-
for (const [subpath, items] of rewrites) {
|
|
590
|
-
lines.push(`import { ${items.map(serializeNamedItem).join(", ")} } from "${subpath}"${tail}`);
|
|
591
|
-
}
|
|
592
|
-
return lines.join(`
|
|
593
|
-
`);
|
|
594
|
-
};
|
|
595
|
-
var shouldPreserveBarrelSideEffects = (specifier) => /^@(apps|libs)\/[^/]+\/client$/.test(specifier);
|
|
596
|
-
var serializeNamedItem = (item) => {
|
|
597
|
-
const prefix = item.isType ? "type " : "";
|
|
598
|
-
if (item.imported === item.local)
|
|
599
|
-
return `${prefix}${item.imported}`;
|
|
600
|
-
return `${prefix}${item.imported} as ${item.local}`;
|
|
601
|
-
};
|
|
602
|
-
var loaderFor = (absPath) => {
|
|
603
|
-
if (absPath.endsWith(".tsx"))
|
|
604
|
-
return "tsx";
|
|
605
|
-
if (absPath.endsWith(".jsx"))
|
|
606
|
-
return "jsx";
|
|
607
|
-
if (absPath.endsWith(".ts"))
|
|
608
|
-
return "ts";
|
|
609
|
-
return "js";
|
|
610
|
-
};
|
|
611
|
-
|
|
612
|
-
// pkgs/@akanjs/devkit/frontendBuild/clientEntryDiscovery.ts
|
|
613
|
-
var USE_CLIENT_RE = /^\s*(?:\/\*[\s\S]*?\*\/\s*|\/\/[^\n]*\n\s*)*["']use client["']/;
|
|
614
|
-
var SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
615
|
-
var NODE_MODULES_RE2 = /[\\/]node_modules[\\/]/;
|
|
616
|
-
var AKANJS_NODE_MODULE_RE2 = /[\\/]node_modules[\\/]akanjs[\\/]/;
|
|
617
|
-
var NON_SOURCE_EXT_RE = /\.(css|scss|sass|less|json|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp3|mp4|wav)$/i;
|
|
618
|
-
var shouldSkipNodeModule = (absPath) => NODE_MODULES_RE2.test(absPath) && !AKANJS_NODE_MODULE_RE2.test(absPath);
|
|
619
|
-
|
|
620
|
-
class GraphClientEntryDiscovery {
|
|
621
|
-
#akanConfig;
|
|
622
|
-
#resolvePackage;
|
|
623
|
-
#analyzer;
|
|
624
|
-
#tsTranspiler = new Bun.Transpiler({ loader: "tsx" });
|
|
625
|
-
#fileExistsCache = new Map;
|
|
626
|
-
#readCache = new Map;
|
|
627
|
-
#rewriteCache = new Map;
|
|
628
|
-
#importCache = new Map;
|
|
629
|
-
#resolvedFileCache = new Map;
|
|
630
|
-
#resolvedSpecifierCache = new Map;
|
|
631
|
-
#reachableEntriesCache = new Map;
|
|
632
|
-
constructor(akanConfig, resolvePackage) {
|
|
633
|
-
this.#akanConfig = akanConfig;
|
|
634
|
-
this.#resolvePackage = resolvePackage;
|
|
635
|
-
this.#analyzer = new BarrelAnalyzer({ resolvePackage });
|
|
636
|
-
}
|
|
637
|
-
static async create(app) {
|
|
638
|
-
return new GraphClientEntryDiscovery(await app.getConfig(), await createTsconfigPackageResolver(app));
|
|
639
|
-
}
|
|
640
|
-
async discover(seeds) {
|
|
641
|
-
const entries = new Set;
|
|
642
|
-
for (const seed of seeds) {
|
|
643
|
-
for (const entry of await this.#discoverFromFile(seed, new Set))
|
|
644
|
-
entries.add(entry);
|
|
645
|
-
}
|
|
646
|
-
return Array.from(entries).sort();
|
|
647
|
-
}
|
|
648
|
-
invalidate(files) {
|
|
649
|
-
for (const file of files) {
|
|
650
|
-
const absPath = path3.resolve(file);
|
|
651
|
-
this.#readCache.delete(absPath);
|
|
652
|
-
this.#rewriteCache.delete(absPath);
|
|
653
|
-
this.#importCache.delete(absPath);
|
|
654
|
-
this.#reachableEntriesCache.delete(absPath);
|
|
655
|
-
}
|
|
656
|
-
if (files.length > 0)
|
|
657
|
-
this.#reachableEntriesCache.clear();
|
|
658
|
-
}
|
|
659
|
-
async#fileExists(p) {
|
|
660
|
-
const absPath = path3.resolve(p);
|
|
661
|
-
let cached = this.#fileExistsCache.get(absPath);
|
|
662
|
-
if (!cached) {
|
|
663
|
-
cached = Bun.file(absPath).exists();
|
|
664
|
-
this.#fileExistsCache.set(absPath, cached);
|
|
665
|
-
}
|
|
666
|
-
return cached;
|
|
667
|
-
}
|
|
668
|
-
#readFile(file) {
|
|
669
|
-
const absPath = path3.resolve(file);
|
|
670
|
-
let cached = this.#readCache.get(absPath);
|
|
671
|
-
if (!cached) {
|
|
672
|
-
cached = Bun.file(absPath).text().catch(() => null);
|
|
673
|
-
this.#readCache.set(absPath, cached);
|
|
674
|
-
}
|
|
675
|
-
return cached;
|
|
676
|
-
}
|
|
677
|
-
async#resolveFileCandidate(absPathNoExt) {
|
|
678
|
-
const cacheKey = path3.resolve(absPathNoExt);
|
|
679
|
-
let cached = this.#resolvedFileCache.get(cacheKey);
|
|
680
|
-
if (cached)
|
|
681
|
-
return cached;
|
|
682
|
-
cached = (async () => {
|
|
683
|
-
if (await this.#fileExists(cacheKey))
|
|
684
|
-
return cacheKey;
|
|
685
|
-
for (const ext of SOURCE_EXTS) {
|
|
686
|
-
const f = `${cacheKey}${ext}`;
|
|
687
|
-
if (await this.#fileExists(f))
|
|
688
|
-
return f;
|
|
689
|
-
}
|
|
690
|
-
for (const ext of SOURCE_EXTS) {
|
|
691
|
-
const f = path3.join(cacheKey, `index${ext}`);
|
|
692
|
-
if (await this.#fileExists(f))
|
|
693
|
-
return f;
|
|
694
|
-
}
|
|
695
|
-
return null;
|
|
696
|
-
})();
|
|
697
|
-
this.#resolvedFileCache.set(cacheKey, cached);
|
|
698
|
-
return cached;
|
|
699
|
-
}
|
|
700
|
-
async#resolveSpecifier(spec, importerDir) {
|
|
701
|
-
const cacheKey = `${importerDir}\x00${spec}`;
|
|
702
|
-
let cached = this.#resolvedSpecifierCache.get(cacheKey);
|
|
703
|
-
if (cached)
|
|
704
|
-
return cached;
|
|
705
|
-
cached = (async () => {
|
|
706
|
-
if (spec.startsWith(".") || spec.startsWith("/")) {
|
|
707
|
-
const abs = spec.startsWith("/") ? spec : path3.resolve(importerDir, spec);
|
|
708
|
-
return this.#resolveFileCandidate(abs);
|
|
709
|
-
}
|
|
710
|
-
const pkg = await this.#resolvePackage(spec);
|
|
711
|
-
if (pkg)
|
|
712
|
-
return pkg.entryFile;
|
|
713
|
-
return null;
|
|
714
|
-
})();
|
|
715
|
-
this.#resolvedSpecifierCache.set(cacheKey, cached);
|
|
716
|
-
return cached;
|
|
717
|
-
}
|
|
718
|
-
async#getRewrittenSource(file, content) {
|
|
719
|
-
const absPath = path3.resolve(file);
|
|
720
|
-
let cached = this.#rewriteCache.get(absPath);
|
|
721
|
-
if (!cached) {
|
|
722
|
-
cached = (async () => {
|
|
723
|
-
if (this.#akanConfig.barrelImports.length === 0)
|
|
724
|
-
return content;
|
|
725
|
-
try {
|
|
726
|
-
return await rewriteBarrelImports(content, this.#akanConfig.barrelImports, this.#analyzer) ?? content;
|
|
727
|
-
} catch {
|
|
728
|
-
return content;
|
|
729
|
-
}
|
|
730
|
-
})();
|
|
731
|
-
this.#rewriteCache.set(absPath, cached);
|
|
732
|
-
}
|
|
733
|
-
return cached;
|
|
734
|
-
}
|
|
735
|
-
async#getImports(file, source) {
|
|
736
|
-
const absPath = path3.resolve(file);
|
|
737
|
-
let cached = this.#importCache.get(absPath);
|
|
738
|
-
if (!cached) {
|
|
739
|
-
cached = Promise.resolve().then(() => {
|
|
740
|
-
try {
|
|
741
|
-
return this.#tsTranspiler.scanImports(source);
|
|
742
|
-
} catch {
|
|
743
|
-
return [];
|
|
744
|
-
}
|
|
745
|
-
});
|
|
746
|
-
this.#importCache.set(absPath, cached);
|
|
747
|
-
}
|
|
748
|
-
return cached;
|
|
749
|
-
}
|
|
750
|
-
async#discoverFromFile(file, visiting) {
|
|
751
|
-
const absPath = path3.resolve(file);
|
|
752
|
-
const cached = this.#reachableEntriesCache.get(absPath);
|
|
753
|
-
if (cached)
|
|
754
|
-
return new Set(cached);
|
|
755
|
-
if (visiting.has(absPath) || shouldSkipNodeModule(absPath))
|
|
756
|
-
return new Set;
|
|
757
|
-
visiting.add(absPath);
|
|
758
|
-
const entries = new Set;
|
|
759
|
-
const content = await this.#readFile(absPath);
|
|
760
|
-
if (content === null)
|
|
761
|
-
return this.#finishDiscovery(absPath, visiting, entries);
|
|
762
|
-
if (USE_CLIENT_RE.test(content)) {
|
|
763
|
-
entries.add(absPath);
|
|
764
|
-
return this.#finishDiscovery(absPath, visiting, entries);
|
|
765
|
-
}
|
|
766
|
-
const source = await this.#getRewrittenSource(absPath, content);
|
|
767
|
-
const imports = await this.#getImports(absPath, source);
|
|
768
|
-
const importerDir = path3.dirname(absPath);
|
|
769
|
-
for (const imp of imports) {
|
|
770
|
-
const spec = imp.path;
|
|
771
|
-
if (!spec)
|
|
772
|
-
continue;
|
|
773
|
-
if (NON_SOURCE_EXT_RE.test(spec))
|
|
774
|
-
continue;
|
|
775
|
-
const resolved = await this.#resolveSpecifier(spec, importerDir);
|
|
776
|
-
if (!resolved)
|
|
777
|
-
continue;
|
|
778
|
-
if (shouldSkipNodeModule(resolved))
|
|
779
|
-
continue;
|
|
780
|
-
for (const entry of await this.#discoverFromFile(resolved, visiting))
|
|
781
|
-
entries.add(entry);
|
|
782
|
-
}
|
|
783
|
-
return this.#finishDiscovery(absPath, visiting, entries);
|
|
784
|
-
}
|
|
785
|
-
#finishDiscovery(absPath, visiting, entries) {
|
|
786
|
-
visiting.delete(absPath);
|
|
787
|
-
this.#reachableEntriesCache.set(absPath, entries);
|
|
788
|
-
return new Set(entries);
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// pkgs/@akanjs/devkit/frontendBuild/routeClientBuilder.ts
|
|
793
|
-
import { mkdir } from "fs/promises";
|
|
794
|
-
import path6 from "path";
|
|
795
|
-
|
|
796
|
-
// pkgs/@akanjs/devkit/transforms/rscUseClientTransform.ts
|
|
797
|
-
import path4 from "path";
|
|
798
|
-
var USE_CLIENT_RE2 = /^\s*(?:\/\*[\s\S]*?\*\/\s*|\/\/[^\n]*\n\s*)*["']use client["']/;
|
|
799
|
-
var IMPLICIT_ROOT_LAYOUT_RE = /[/\\]\.akan[/\\]generated[/\\](?:implicit-root-layout|root-layouts[/\\].*__root_layout)\.(tsx|ts|jsx|js)$/;
|
|
800
|
-
function toClientReferencePath(absPath, workspaceRoot) {
|
|
801
|
-
return path4.relative(path4.resolve(workspaceRoot), path4.resolve(absPath)).split(path4.sep).join("/");
|
|
802
|
-
}
|
|
803
|
-
function transformUseClient(source, args) {
|
|
804
|
-
if (!USE_CLIENT_RE2.test(source))
|
|
805
|
-
return null;
|
|
806
|
-
if (IMPLICIT_ROOT_LAYOUT_RE.test(args.path))
|
|
807
|
-
return null;
|
|
808
|
-
const transpiler = new Bun.Transpiler({ loader: loaderFor2(args.path) });
|
|
809
|
-
const { exports } = transpiler.scan(source);
|
|
810
|
-
if (exports.length === 0)
|
|
811
|
-
return null;
|
|
812
|
-
const referencePath = args.workspaceRoot ? toClientReferencePath(args.path, args.workspaceRoot) : args.path;
|
|
813
|
-
const filePathLit = JSON.stringify(referencePath);
|
|
814
|
-
const lines = [`import { registerClientReference } from "react-server-dom-webpack/server.node";`];
|
|
815
|
-
for (const name of exports) {
|
|
816
|
-
const nameLit = JSON.stringify(name);
|
|
817
|
-
const errMsg = JSON.stringify(`Attempted to call '${name}' from '${referencePath}' on the server, but it is a client-only export.`);
|
|
818
|
-
const proxy = `() => { throw new Error(${errMsg}); }`;
|
|
819
|
-
const binding = name === "default" ? `export default registerClientReference(${proxy}, ${filePathLit}, ${nameLit});` : `export const ${name} = registerClientReference(${proxy}, ${filePathLit}, ${nameLit});`;
|
|
820
|
-
lines.push(binding);
|
|
821
|
-
}
|
|
822
|
-
return lines.join(`
|
|
823
|
-
`);
|
|
824
|
-
}
|
|
825
|
-
function loaderFor2(absPath) {
|
|
826
|
-
if (absPath.endsWith(".tsx"))
|
|
827
|
-
return "tsx";
|
|
828
|
-
if (absPath.endsWith(".jsx"))
|
|
829
|
-
return "jsx";
|
|
830
|
-
if (absPath.endsWith(".ts"))
|
|
831
|
-
return "ts";
|
|
832
|
-
return "js";
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
// pkgs/@akanjs/devkit/frontendBuild/clientEntriesBundler.ts
|
|
836
|
-
import fs from "fs";
|
|
837
|
-
import path5 from "path";
|
|
838
|
-
|
|
839
|
-
// pkgs/@akanjs/devkit/frontendBuild/clientBuildTypes.ts
|
|
840
|
-
var CLIENT_BUNDLE_NAMING = {
|
|
841
|
-
entry: "[name]-[hash].[ext]",
|
|
842
|
-
chunk: "chunks/[hash].[ext]",
|
|
843
|
-
asset: "assets/[hash].[ext]"
|
|
844
|
-
};
|
|
845
|
-
|
|
846
|
-
// pkgs/@akanjs/devkit/frontendBuild/clientEntriesBundler.ts
|
|
847
|
-
class ClientEntriesBundler {
|
|
848
|
-
#app;
|
|
849
|
-
#entries;
|
|
850
|
-
#plugins;
|
|
851
|
-
#external;
|
|
852
|
-
#externalSubpaths;
|
|
853
|
-
#externalAliases;
|
|
854
|
-
#command;
|
|
855
|
-
#outputSubdir;
|
|
856
|
-
#reactFastRefresh;
|
|
857
|
-
#artifactDir;
|
|
858
|
-
#outdir;
|
|
859
|
-
#servePrefix;
|
|
860
|
-
#manifest = {};
|
|
861
|
-
#ssrManifest = { moduleLoading: null, moduleMap: {} };
|
|
862
|
-
#entryUrlsByAbsPath = new Map;
|
|
863
|
-
#entryOutputAbsByAbsPath = new Map;
|
|
864
|
-
#entryDepsByAbsPath = new Map;
|
|
865
|
-
#clientReferenceIdByAbsPath = new Map;
|
|
866
|
-
#opaqueEntries = null;
|
|
867
|
-
#metafileOutputsByAbs = new Map;
|
|
868
|
-
constructor(options) {
|
|
869
|
-
this.#app = options.app;
|
|
870
|
-
this.#entries = options.entries;
|
|
871
|
-
this.#plugins = options.plugins ?? [];
|
|
872
|
-
this.#external = options.external ?? [];
|
|
873
|
-
this.#externalSubpaths = options.externalSubpaths ?? [];
|
|
874
|
-
this.#externalAliases = options.externalAliases ?? {};
|
|
875
|
-
this.#command = options.command ?? "start";
|
|
876
|
-
this.#outputSubdir = options.outputSubdir ?? "client";
|
|
877
|
-
this.#reactFastRefresh = options.reactFastRefresh ?? false;
|
|
878
|
-
this.#artifactDir = `${this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath}/.akan/artifact`;
|
|
879
|
-
this.#outdir = `${this.#artifactDir}/${this.#outputSubdir}`;
|
|
880
|
-
this.#servePrefix = `/_akan/${this.#outputSubdir}`;
|
|
881
|
-
}
|
|
882
|
-
async bundle() {
|
|
883
|
-
const akanConfig = await this.#app.getConfig();
|
|
884
|
-
this.#opaqueEntries = await this.#createOpaqueEntryAliases();
|
|
885
|
-
const result = await Bun.build({
|
|
886
|
-
entrypoints: this.#opaqueEntries.entries,
|
|
887
|
-
outdir: this.#outdir,
|
|
888
|
-
splitting: true,
|
|
889
|
-
target: "browser",
|
|
890
|
-
format: "esm",
|
|
891
|
-
naming: CLIENT_BUNDLE_NAMING,
|
|
892
|
-
metafile: true,
|
|
893
|
-
define: this.#getDefine(),
|
|
894
|
-
minify: this.#command === "build",
|
|
895
|
-
optimizeImports: akanConfig.optimizeImports,
|
|
896
|
-
reactFastRefresh: this.#command === "start" && this.#outputSubdir === "client" && this.#reactFastRefresh,
|
|
897
|
-
plugins: this.#external.length > 0 || this.#externalSubpaths.length > 0 || Object.keys(this.#externalAliases).length > 0 ? [...this.#plugins, this.#createExternalSpecifiersPlugin()] : this.#plugins
|
|
898
|
-
});
|
|
899
|
-
if (!result.success)
|
|
900
|
-
throw new AggregateError(result.logs, "[ClientEntriesBundler] Bun.build failed");
|
|
901
|
-
await this.#rewriteExternalImportSpecifiers(result.outputs);
|
|
902
|
-
const metafile = result.metafile;
|
|
903
|
-
if (!metafile)
|
|
904
|
-
throw new Error("[ClientEntriesBundler] metafile is missing");
|
|
905
|
-
this.#metafileOutputsByAbs = new Map(Object.entries(metafile.outputs).map(([k, v]) => [this.#absFromOutdir(k), v]));
|
|
906
|
-
for (const artifact of result.outputs.filter((a) => a.kind === "entry-point"))
|
|
907
|
-
this.#addEntryArtifact(artifact.path);
|
|
908
|
-
return {
|
|
909
|
-
manifest: this.#manifest,
|
|
910
|
-
ssrManifest: this.#ssrManifest,
|
|
911
|
-
entryUrlsByAbsPath: this.#entryUrlsByAbsPath,
|
|
912
|
-
entryOutputAbsByAbsPath: this.#entryOutputAbsByAbsPath,
|
|
913
|
-
entryDepsByAbsPath: this.#entryDepsByAbsPath,
|
|
914
|
-
clientReferenceIdByAbsPath: this.#clientReferenceIdByAbsPath
|
|
915
|
-
};
|
|
916
|
-
}
|
|
917
|
-
#getDefine() {
|
|
918
|
-
const nodeEnv = this.#command === "build" ? "production" : "development";
|
|
919
|
-
return {
|
|
920
|
-
"process.env.NODE_ENV": JSON.stringify(nodeEnv),
|
|
921
|
-
"process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("ssr"),
|
|
922
|
-
...Object.fromEntries(Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]))
|
|
923
|
-
};
|
|
924
|
-
}
|
|
925
|
-
async#createOpaqueEntryAliases() {
|
|
926
|
-
const aliasDir = path5.join(this.#app.cwdPath, ".akan", "generated", "client-entry-alias", this.#outputSubdir);
|
|
927
|
-
fs.mkdirSync(aliasDir, { recursive: true });
|
|
928
|
-
const originalByAlias = new Map;
|
|
929
|
-
const aliasedEntries = await Promise.all(this.#entries.map(async (entry) => {
|
|
930
|
-
const absEntry = path5.resolve(entry);
|
|
931
|
-
const hash = Bun.hash(`${this.#app.name}
|
|
932
|
-
${this.#outputSubdir}
|
|
933
|
-
${absEntry}`).toString(36);
|
|
934
|
-
const aliasPath = path5.join(aliasDir, `${hash}.tsx`);
|
|
935
|
-
await Bun.write(aliasPath, this.#createOpaqueEntryAliasSource(absEntry, await this.#scanEntryExportNames(absEntry)));
|
|
936
|
-
originalByAlias.set(path5.resolve(aliasPath), absEntry);
|
|
937
|
-
return aliasPath;
|
|
938
|
-
}));
|
|
939
|
-
return { entries: aliasedEntries, originalByAlias, aliasDir };
|
|
940
|
-
}
|
|
941
|
-
#createOpaqueEntryAliasSource(absEntry, exportNames) {
|
|
942
|
-
const entryLit = JSON.stringify(path5.resolve(absEntry));
|
|
943
|
-
if (exportNames.length === 0)
|
|
944
|
-
return `export * from ${entryLit};
|
|
945
|
-
`;
|
|
946
|
-
const namedExports = exportNames.filter((name) => name !== "default");
|
|
947
|
-
const lines = namedExports.length > 0 ? [`export { ${namedExports.join(", ")} } from ${entryLit};`] : [];
|
|
948
|
-
if (exportNames.includes("default"))
|
|
949
|
-
lines.push(`export { default } from ${entryLit};`);
|
|
950
|
-
return `${lines.join(`
|
|
951
|
-
`)}
|
|
952
|
-
`;
|
|
953
|
-
}
|
|
954
|
-
async#scanEntryExportNames(absEntry) {
|
|
955
|
-
const source = await Bun.file(absEntry).text();
|
|
956
|
-
const transpiler = new Bun.Transpiler({ loader: this.#loaderForEntry(absEntry) });
|
|
957
|
-
return transpiler.scan(source).exports;
|
|
958
|
-
}
|
|
959
|
-
#loaderForEntry(absPath) {
|
|
960
|
-
if (absPath.endsWith(".tsx"))
|
|
961
|
-
return "tsx";
|
|
962
|
-
if (absPath.endsWith(".jsx"))
|
|
963
|
-
return "jsx";
|
|
964
|
-
if (absPath.endsWith(".ts"))
|
|
965
|
-
return "ts";
|
|
966
|
-
return "js";
|
|
967
|
-
}
|
|
968
|
-
#createExternalSpecifiersPlugin() {
|
|
969
|
-
const set = new Set(this.#external);
|
|
970
|
-
const subpathSet = new Set(this.#externalSubpaths);
|
|
971
|
-
const aliases = this.#externalAliases;
|
|
972
|
-
const specifiers = [...new Set([...this.#external, ...this.#externalSubpaths, ...Object.keys(aliases)])];
|
|
973
|
-
const escaped = specifiers.map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
974
|
-
const filter = new RegExp(`^(${escaped.join("|")})(?:/.*)?$`);
|
|
975
|
-
return {
|
|
976
|
-
name: "akan-externalize-specifiers",
|
|
977
|
-
setup(build) {
|
|
978
|
-
build.onResolve({ filter }, (args) => {
|
|
979
|
-
const alias = aliases[args.path];
|
|
980
|
-
if (alias)
|
|
981
|
-
return { path: alias, external: true };
|
|
982
|
-
if (!ClientEntriesBundler.#matchesExternalSpecifier(args.path, set, subpathSet))
|
|
983
|
-
return;
|
|
984
|
-
return { path: args.path, external: true };
|
|
985
|
-
});
|
|
986
|
-
}
|
|
987
|
-
};
|
|
988
|
-
}
|
|
989
|
-
static #matchesExternalSpecifier(specifier, exactExternals, subpathExternals) {
|
|
990
|
-
if (exactExternals.has(specifier))
|
|
991
|
-
return true;
|
|
992
|
-
if (subpathExternals.has(specifier))
|
|
993
|
-
return true;
|
|
994
|
-
for (const external of subpathExternals) {
|
|
995
|
-
if (specifier.startsWith(`${external}/`))
|
|
996
|
-
return true;
|
|
997
|
-
}
|
|
998
|
-
return false;
|
|
999
|
-
}
|
|
1000
|
-
async#rewriteExternalImportSpecifiers(outputs) {
|
|
1001
|
-
if (Object.keys(this.#externalAliases).length === 0)
|
|
1002
|
-
return;
|
|
1003
|
-
await Promise.all(outputs.filter((output) => output.path.endsWith(".js")).map(async (output) => {
|
|
1004
|
-
const source = await Bun.file(output.path).text();
|
|
1005
|
-
const rewritten = ClientEntriesBundler.rewriteExternalImportSpecifiers(source, this.#externalAliases);
|
|
1006
|
-
if (rewritten !== source)
|
|
1007
|
-
await Bun.write(output.path, rewritten);
|
|
1008
|
-
}));
|
|
1009
|
-
}
|
|
1010
|
-
static rewriteExternalImportSpecifiers(source, aliases) {
|
|
1011
|
-
let rewritten = source;
|
|
1012
|
-
for (const [specifier, alias] of Object.entries(aliases)) {
|
|
1013
|
-
if (!alias)
|
|
1014
|
-
continue;
|
|
1015
|
-
const escaped = specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1016
|
-
rewritten = rewritten.replace(new RegExp(`(\\bfrom\\s*["'])${escaped}(["'])`, "g"), (_match, prefix, suffix) => {
|
|
1017
|
-
return `${prefix}${alias}${suffix}`;
|
|
1018
|
-
}).replace(new RegExp(`(\\bimport\\s*["'])${escaped}(["'])`, "g"), (_match, prefix, suffix) => {
|
|
1019
|
-
return `${prefix}${alias}${suffix}`;
|
|
1020
|
-
});
|
|
1021
|
-
}
|
|
1022
|
-
return rewritten;
|
|
1023
|
-
}
|
|
1024
|
-
#toServeUrl(absOutPath) {
|
|
1025
|
-
const rel = path5.relative(this.#outdir, absOutPath).split(path5.sep).join("/");
|
|
1026
|
-
return `${this.#servePrefix}/${rel}`;
|
|
1027
|
-
}
|
|
1028
|
-
#absFromOutdir(p) {
|
|
1029
|
-
return path5.isAbsolute(p) ? p : path5.resolve(this.#outdir, p);
|
|
1030
|
-
}
|
|
1031
|
-
#absFromEntryPoint(p) {
|
|
1032
|
-
if (path5.isAbsolute(p))
|
|
1033
|
-
return path5.resolve(p);
|
|
1034
|
-
const candidates = [path5.resolve(process.cwd(), p), path5.resolve(this.#app.cwdPath, p)];
|
|
1035
|
-
return candidates.find((candidate) => fs.existsSync(candidate)) ?? candidates[0];
|
|
1036
|
-
}
|
|
1037
|
-
#collectChunkUrls(absOutPath, visited = new Set) {
|
|
1038
|
-
if (visited.has(absOutPath))
|
|
1039
|
-
return [];
|
|
1040
|
-
visited.add(absOutPath);
|
|
1041
|
-
const info = this.#metafileOutputsByAbs.get(absOutPath);
|
|
1042
|
-
if (!info)
|
|
1043
|
-
return [this.#toServeUrl(absOutPath)];
|
|
1044
|
-
const urls = [this.#toServeUrl(absOutPath)];
|
|
1045
|
-
for (const imp of info.imports) {
|
|
1046
|
-
if (imp.kind === "dynamic-import")
|
|
1047
|
-
continue;
|
|
1048
|
-
const absImp = this.#absFromOutdir(imp.path);
|
|
1049
|
-
if (!absImp.endsWith(".js"))
|
|
1050
|
-
continue;
|
|
1051
|
-
urls.push(...this.#collectChunkUrls(absImp, visited));
|
|
1052
|
-
}
|
|
1053
|
-
return urls;
|
|
1054
|
-
}
|
|
1055
|
-
#collectClientDeps(absOutPath, visited = new Set) {
|
|
1056
|
-
if (visited.has(absOutPath))
|
|
1057
|
-
return new Set;
|
|
1058
|
-
visited.add(absOutPath);
|
|
1059
|
-
const deps = new Set;
|
|
1060
|
-
const info = this.#metafileOutputsByAbs.get(absOutPath);
|
|
1061
|
-
if (!info)
|
|
1062
|
-
return deps;
|
|
1063
|
-
if (!this.#opaqueEntries)
|
|
1064
|
-
throw new Error("[ClientEntriesBundler] opaque entries are missing");
|
|
1065
|
-
for (const input of Object.keys(info.inputs ?? {})) {
|
|
1066
|
-
const absInput = this.#absFromEntryPoint(input);
|
|
1067
|
-
if (!absInput.includes("node_modules") && !absInput.startsWith(this.#opaqueEntries.aliasDir))
|
|
1068
|
-
deps.add(absInput);
|
|
1069
|
-
}
|
|
1070
|
-
for (const imp of info.imports) {
|
|
1071
|
-
if (imp.kind === "dynamic-import")
|
|
1072
|
-
continue;
|
|
1073
|
-
const absImp = this.#absFromOutdir(imp.path);
|
|
1074
|
-
if (!absImp.endsWith(".js"))
|
|
1075
|
-
continue;
|
|
1076
|
-
for (const dep of this.#collectClientDeps(absImp, visited))
|
|
1077
|
-
deps.add(dep);
|
|
1078
|
-
}
|
|
1079
|
-
return deps;
|
|
1080
|
-
}
|
|
1081
|
-
#addEntryArtifact(absOut) {
|
|
1082
|
-
if (!this.#opaqueEntries)
|
|
1083
|
-
throw new Error("[ClientEntriesBundler] opaque entries are missing");
|
|
1084
|
-
const info = this.#metafileOutputsByAbs.get(absOut);
|
|
1085
|
-
const buildEntryPointAbs = info?.entryPoint ? this.#absFromEntryPoint(info.entryPoint) : null;
|
|
1086
|
-
const entryPointAbs = buildEntryPointAbs ? this.#opaqueEntries.originalByAlias.get(buildEntryPointAbs) ?? buildEntryPointAbs : null;
|
|
1087
|
-
if (!entryPointAbs)
|
|
1088
|
-
return;
|
|
1089
|
-
const chunkUrls = this.#collectChunkUrls(absOut);
|
|
1090
|
-
const entryUrl = chunkUrls[0] ?? this.#toServeUrl(absOut);
|
|
1091
|
-
this.#entryUrlsByAbsPath.set(entryPointAbs, entryUrl);
|
|
1092
|
-
this.#entryOutputAbsByAbsPath.set(entryPointAbs, absOut);
|
|
1093
|
-
this.#entryDepsByAbsPath.set(entryPointAbs, [...this.#collectClientDeps(absOut)].sort());
|
|
1094
|
-
const clientReferenceId = toClientReferencePath(entryPointAbs, this.#app.workspace.workspaceRoot);
|
|
1095
|
-
this.#clientReferenceIdByAbsPath.set(entryPointAbs, clientReferenceId);
|
|
1096
|
-
const flatChunks = [];
|
|
1097
|
-
for (const url of chunkUrls)
|
|
1098
|
-
flatChunks.push(url, url);
|
|
1099
|
-
const exportNames = info?.exports && info.exports.length > 0 ? info.exports : ["default"];
|
|
1100
|
-
const ssrEntriesByName = {};
|
|
1101
|
-
for (const name of exportNames) {
|
|
1102
|
-
const key = `${clientReferenceId}#${name}`;
|
|
1103
|
-
this.#manifest[key] = { id: entryUrl, chunks: flatChunks, name, async: true };
|
|
1104
|
-
ssrEntriesByName[name] = { id: absOut, chunks: [absOut, absOut], name, async: true };
|
|
1105
|
-
}
|
|
1106
|
-
this.#ssrManifest.moduleMap[entryUrl] = ssrEntriesByName;
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
// pkgs/@akanjs/devkit/frontendBuild/vendorSpecifiers.ts
|
|
1111
|
-
var VENDOR_SPECIFIERS = [
|
|
1112
|
-
"react",
|
|
1113
|
-
"react-dom",
|
|
1114
|
-
"react-dom/client",
|
|
1115
|
-
"react/jsx-runtime",
|
|
1116
|
-
"react/jsx-dev-runtime",
|
|
1117
|
-
"react-refresh/runtime",
|
|
1118
|
-
"scheduler",
|
|
1119
|
-
"react-server-dom-webpack/client.browser",
|
|
1120
|
-
"akanjs/store",
|
|
1121
|
-
"akanjs/base",
|
|
1122
|
-
"akanjs/common",
|
|
1123
|
-
"akanjs/constant"
|
|
1124
|
-
];
|
|
1125
|
-
|
|
1126
|
-
// pkgs/@akanjs/devkit/frontendBuild/routeClientBuilder.ts
|
|
1127
|
-
var SSR_CLIENT_EXTERNALS = [
|
|
1128
|
-
"react",
|
|
1129
|
-
"react-dom",
|
|
1130
|
-
"react-dom/client",
|
|
1131
|
-
"react/jsx-runtime",
|
|
1132
|
-
"react/jsx-dev-runtime",
|
|
1133
|
-
"akanjs/fetch"
|
|
1134
|
-
];
|
|
1135
|
-
var SSR_CLIENT_ALIAS_EXTERNALS = [
|
|
1136
|
-
"react",
|
|
1137
|
-
"react-dom",
|
|
1138
|
-
"react-dom/client",
|
|
1139
|
-
"react/jsx-runtime",
|
|
1140
|
-
"react/jsx-dev-runtime"
|
|
1141
|
-
];
|
|
1142
|
-
|
|
1143
|
-
class RouteClientBuilder {
|
|
1144
|
-
#app;
|
|
1145
|
-
#seeds;
|
|
1146
|
-
#knownEntries;
|
|
1147
|
-
#command;
|
|
1148
|
-
#discovery;
|
|
1149
|
-
constructor(options) {
|
|
1150
|
-
this.#app = options.app;
|
|
1151
|
-
this.#seeds = options.seeds;
|
|
1152
|
-
this.#knownEntries = options.knownEntries ?? new Set;
|
|
1153
|
-
this.#command = options.command ?? "start";
|
|
1154
|
-
this.#discovery = options.discovery;
|
|
1155
|
-
}
|
|
1156
|
-
async build() {
|
|
1157
|
-
const discovery = this.#discovery ?? await GraphClientEntryDiscovery.create(this.#app);
|
|
1158
|
-
const discovered = await discovery.discover(this.#seeds);
|
|
1159
|
-
const entries = discovered.filter((e) => !this.#knownEntries.has(e));
|
|
1160
|
-
if (entries.length === 0)
|
|
1161
|
-
return this.#emptyResult(discovered);
|
|
1162
|
-
const bootstrapEntries = await this.#createBootstrapEntries(entries);
|
|
1163
|
-
const browserBundle = await this.#buildBrowserBundle(bootstrapEntries);
|
|
1164
|
-
const ssrBundle = await this.#buildSsrBundle(bootstrapEntries);
|
|
1165
|
-
const acceptedEntries = new Set(entries);
|
|
1166
|
-
const manifestDelta = {};
|
|
1167
|
-
const ssrModuleMap = {};
|
|
1168
|
-
const clientDeps = new Set;
|
|
1169
|
-
const clientDepsByEntry = {};
|
|
1170
|
-
for (const [key, row] of Object.entries(browserBundle.manifest)) {
|
|
1171
|
-
const manifestEntry = RouteClientBuilder.resolveOriginalManifestEntry(key, bootstrapEntries.originalByBuildEntry, browserBundle.clientReferenceIdByAbsPath, this.#app.workspace.workspaceRoot);
|
|
1172
|
-
if (!manifestEntry)
|
|
1173
|
-
continue;
|
|
1174
|
-
if (!acceptedEntries.has(manifestEntry.originalEntry))
|
|
1175
|
-
continue;
|
|
1176
|
-
manifestDelta[manifestEntry.key] = row;
|
|
1177
|
-
const ssrOutput = ssrBundle.entryOutputAbsByAbsPath.get(manifestEntry.buildEntry);
|
|
1178
|
-
if (!ssrOutput)
|
|
1179
|
-
continue;
|
|
1180
|
-
ssrModuleMap[row.id] ??= {};
|
|
1181
|
-
ssrModuleMap[row.id][row.name] = { id: ssrOutput, chunks: [ssrOutput, ssrOutput], name: row.name, async: true };
|
|
1182
|
-
}
|
|
1183
|
-
for (const entry of bootstrapEntries.buildEntries) {
|
|
1184
|
-
const buildEntry = path6.resolve(entry);
|
|
1185
|
-
const originalEntry = path6.resolve(bootstrapEntries.originalByBuildEntry.get(buildEntry) ?? buildEntry);
|
|
1186
|
-
if (!acceptedEntries.has(originalEntry))
|
|
1187
|
-
continue;
|
|
1188
|
-
const deps = new Set([originalEntry]);
|
|
1189
|
-
for (const dep of browserBundle.entryDepsByAbsPath.get(buildEntry) ?? [])
|
|
1190
|
-
deps.add(path6.resolve(dep));
|
|
1191
|
-
const sortedDeps = [...deps].sort();
|
|
1192
|
-
clientDepsByEntry[originalEntry] = sortedDeps;
|
|
1193
|
-
for (const dep of sortedDeps)
|
|
1194
|
-
clientDeps.add(dep);
|
|
1195
|
-
}
|
|
1196
|
-
return {
|
|
1197
|
-
manifestDelta,
|
|
1198
|
-
ssrManifestDelta: { moduleLoading: null, moduleMap: ssrModuleMap },
|
|
1199
|
-
newEntries: entries,
|
|
1200
|
-
discoveredEntries: discovered,
|
|
1201
|
-
clientDeps: [...clientDeps].sort(),
|
|
1202
|
-
clientDepsByEntry
|
|
1203
|
-
};
|
|
1204
|
-
}
|
|
1205
|
-
#emptyResult(discoveredEntries = []) {
|
|
1206
|
-
return {
|
|
1207
|
-
manifestDelta: {},
|
|
1208
|
-
ssrManifestDelta: { moduleLoading: null, moduleMap: {} },
|
|
1209
|
-
newEntries: [],
|
|
1210
|
-
discoveredEntries,
|
|
1211
|
-
clientDeps: [],
|
|
1212
|
-
clientDepsByEntry: {}
|
|
1213
|
-
};
|
|
1214
|
-
}
|
|
1215
|
-
async#buildBrowserBundle(bootstrapEntries) {
|
|
1216
|
-
const reactFastRefresh = process.env.AKAN_REACT_FAST_REFRESH !== "0";
|
|
1217
|
-
return new ClientEntriesBundler({
|
|
1218
|
-
app: this.#app,
|
|
1219
|
-
entries: bootstrapEntries.buildEntries,
|
|
1220
|
-
plugins: [
|
|
1221
|
-
await createBarrelImportsPlugin(this.#app, {
|
|
1222
|
-
pipeAfter: reactFastRefresh ? RouteClientBuilder.normalizeNamedDefaultFunctionForFastRefresh : undefined
|
|
1223
|
-
})
|
|
1224
|
-
],
|
|
1225
|
-
external: VENDOR_SPECIFIERS,
|
|
1226
|
-
command: this.#command,
|
|
1227
|
-
reactFastRefresh
|
|
1228
|
-
}).bundle();
|
|
1229
|
-
}
|
|
1230
|
-
async#buildSsrBundle(bootstrapEntries) {
|
|
1231
|
-
const externalOptions = RouteClientBuilder.resolveSsrClientExternalOptions(this.#command);
|
|
1232
|
-
return new ClientEntriesBundler({
|
|
1233
|
-
app: this.#app,
|
|
1234
|
-
entries: bootstrapEntries.buildEntries,
|
|
1235
|
-
plugins: [await createBarrelImportsPlugin(this.#app)],
|
|
1236
|
-
...externalOptions,
|
|
1237
|
-
outputSubdir: "client-ssr",
|
|
1238
|
-
command: this.#command
|
|
1239
|
-
}).bundle();
|
|
1240
|
-
}
|
|
1241
|
-
async#createBootstrapEntries(entries) {
|
|
1242
|
-
if (!await Bun.file(path6.join(this.#app.cwdPath, "lib", "st.ts")).exists()) {
|
|
1243
|
-
return { buildEntries: entries, originalByBuildEntry: new Map };
|
|
1244
|
-
}
|
|
1245
|
-
const outdir = path6.join(this.#app.cwdPath, ".akan", "generated", "client-entry-bootstrap");
|
|
1246
|
-
await mkdir(outdir, { recursive: true });
|
|
1247
|
-
const originalByBuildEntry = new Map;
|
|
1248
|
-
const buildEntries = await Promise.all(entries.map(async (entry) => {
|
|
1249
|
-
const absEntry = path6.resolve(entry);
|
|
1250
|
-
const hash = Bun.hash(`${this.#app.name}
|
|
1251
|
-
${absEntry}`).toString(36);
|
|
1252
|
-
const base = path6.basename(absEntry).replace(/[^A-Za-z0-9._-]/g, "_");
|
|
1253
|
-
const wrapperEntry = path6.join(outdir, `${base}-${hash}.tsx`);
|
|
1254
|
-
const exportNames = await this.#scanExportNames(absEntry);
|
|
1255
|
-
await Bun.write(wrapperEntry, RouteClientBuilder.createStoreBootstrapEntrySource({
|
|
1256
|
-
appName: this.#app.name,
|
|
1257
|
-
originalEntry: absEntry,
|
|
1258
|
-
exportNames
|
|
1259
|
-
}));
|
|
1260
|
-
originalByBuildEntry.set(path6.resolve(wrapperEntry), absEntry);
|
|
1261
|
-
return wrapperEntry;
|
|
1262
|
-
}));
|
|
1263
|
-
return { buildEntries, originalByBuildEntry };
|
|
1264
|
-
}
|
|
1265
|
-
async#scanExportNames(absEntry) {
|
|
1266
|
-
const source = await Bun.file(absEntry).text();
|
|
1267
|
-
const transpiler = new Bun.Transpiler({ loader: this.#loaderFor(absEntry) });
|
|
1268
|
-
return transpiler.scan(source).exports;
|
|
1269
|
-
}
|
|
1270
|
-
#loaderFor(absPath) {
|
|
1271
|
-
if (absPath.endsWith(".tsx"))
|
|
1272
|
-
return "tsx";
|
|
1273
|
-
if (absPath.endsWith(".jsx"))
|
|
1274
|
-
return "jsx";
|
|
1275
|
-
if (absPath.endsWith(".ts"))
|
|
1276
|
-
return "ts";
|
|
1277
|
-
return "js";
|
|
1278
|
-
}
|
|
1279
|
-
static normalizeNamedDefaultFunctionForFastRefresh(source) {
|
|
1280
|
-
let changed = false;
|
|
1281
|
-
const defaultNames = [];
|
|
1282
|
-
const next = source.replace(/(^|\n)(\s*)export\s+default\s+(async\s+)?function\s+([A-Za-z_$][\w$]*)(?=\s*(?:<|\())/g, (match, lineStart, indent, asyncKeyword, name) => {
|
|
1283
|
-
changed = true;
|
|
1284
|
-
defaultNames.push(name);
|
|
1285
|
-
return `${lineStart}${indent}${asyncKeyword ?? ""}function ${name}`;
|
|
1286
|
-
});
|
|
1287
|
-
if (!changed)
|
|
1288
|
-
return null;
|
|
1289
|
-
return `${next}
|
|
1290
|
-
${defaultNames.map((name) => `export default ${name};`).join(`
|
|
1291
|
-
`)}
|
|
1292
|
-
`;
|
|
1293
|
-
}
|
|
1294
|
-
static resolveSsrClientRuntimeAliases() {
|
|
1295
|
-
const serverEntry = RouteClientBuilder.resolveAkanServerEntry();
|
|
1296
|
-
return { [Bun.resolveSync("akanjs/fetch", serverEntry)]: "akanjs/fetch" };
|
|
1297
|
-
}
|
|
1298
|
-
static resolveSsrClientExternalOptions(command) {
|
|
1299
|
-
if (command === "start") {
|
|
1300
|
-
return {
|
|
1301
|
-
external: SSR_CLIENT_EXTERNALS,
|
|
1302
|
-
externalSubpaths: ["akanjs/fetch"],
|
|
1303
|
-
externalAliases: RouteClientBuilder.resolveSsrClientRuntimeAliases()
|
|
1304
|
-
};
|
|
1305
|
-
}
|
|
1306
|
-
return { external: SSR_CLIENT_ALIAS_EXTERNALS };
|
|
1307
|
-
}
|
|
1308
|
-
static resolveAkanServerEntry() {
|
|
1309
|
-
try {
|
|
1310
|
-
return Bun.resolveSync("akanjs/server", import.meta.dir);
|
|
1311
|
-
} catch {
|
|
1312
|
-
return path6.resolve(import.meta.dir, "../../../server/index.ts");
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
static createStoreBootstrapEntrySource(args) {
|
|
1316
|
-
const originalEntry = JSON.stringify(path6.resolve(args.originalEntry));
|
|
1317
|
-
const namedExports = args.exportNames.filter((name) => name !== "default");
|
|
1318
|
-
const lines = [
|
|
1319
|
-
`import ${JSON.stringify(`@apps/${args.appName}/client`)};`,
|
|
1320
|
-
...namedExports.length > 0 ? [`export { ${namedExports.join(", ")} } from ${originalEntry};`] : []
|
|
1321
|
-
];
|
|
1322
|
-
if (args.exportNames.includes("default"))
|
|
1323
|
-
lines.push(`export { default } from ${originalEntry};`);
|
|
1324
|
-
return `${lines.join(`
|
|
1325
|
-
`)}
|
|
1326
|
-
`;
|
|
1327
|
-
}
|
|
1328
|
-
static resolveOriginalManifestEntry(manifestKey, originalByBuildEntry, clientReferenceIdByBuildEntry = new Map, workspaceRoot = process.cwd()) {
|
|
1329
|
-
const hashIdx = manifestKey.lastIndexOf("#");
|
|
1330
|
-
if (hashIdx < 0)
|
|
1331
|
-
return null;
|
|
1332
|
-
const buildReferenceId = manifestKey.slice(0, hashIdx);
|
|
1333
|
-
const name = manifestKey.slice(hashIdx + 1);
|
|
1334
|
-
const buildEntry = [...clientReferenceIdByBuildEntry.entries()].find(([, referenceId]) => referenceId === buildReferenceId)?.[0] ?? buildReferenceId;
|
|
1335
|
-
const originalEntry = originalByBuildEntry.get(buildEntry) ?? buildEntry;
|
|
1336
|
-
const originalReferenceId = toClientReferencePath(originalEntry, workspaceRoot);
|
|
1337
|
-
return { buildEntry, originalEntry, name, key: `${originalReferenceId}#${name}` };
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
// pkgs/@akanjs/devkit/frontendBuild/allRoutesBuilder.ts
|
|
1342
|
-
import path10 from "path";
|
|
1343
|
-
|
|
1344
|
-
// pkgs/@akanjs/devkit/artifact/implicitRootLayout.ts
|
|
1345
|
-
import { mkdir as mkdir2 } from "fs/promises";
|
|
1346
|
-
import path7 from "path";
|
|
1347
|
-
var LAYOUT_KEY_RE = /^\.\/(.+\/)?_layout\.(tsx|ts|jsx|js)$/;
|
|
1348
|
-
async function appHasStModule(appCwdPath) {
|
|
1349
|
-
return Bun.file(path7.join(appCwdPath, "lib", "st.ts")).exists();
|
|
1350
|
-
}
|
|
1351
|
-
var IMPLICIT_LAYOUT_DIR = path7.join(".akan", "generated", "root-layouts");
|
|
1352
|
-
var IMPLICIT_DICT_DIR = path7.join(".akan", "generated", "dict");
|
|
1353
|
-
var IMPLICIT_OVERRIDES_DIR = path7.join(".akan", "generated", "overrides");
|
|
1354
|
-
var OVERRIDES_KEY_RE = /^\.\/(.+\/)?_overrides\.(tsx|ts|jsx|js)$/;
|
|
1355
|
-
async function writeGeneratedOverridesLayoutFile(opts) {
|
|
1356
|
-
const filename = `${opts.key.replace(/^\.\//, "").replace(/[^a-zA-Z0-9]+/g, "_")}.tsx`;
|
|
1357
|
-
const absPath = path7.join(path7.resolve(opts.appCwdPath), IMPLICIT_OVERRIDES_DIR, filename);
|
|
1358
|
-
const userRel = path7.relative(path7.dirname(absPath), opts.userAbsPath).split(path7.sep).join("/");
|
|
1359
|
-
const userSpecifier = userRel.startsWith(".") ? userRel : `./${userRel}`;
|
|
1360
|
-
const source = `"use client";
|
|
1361
|
-
import { UiOverrideProvider } from "akanjs/ui";
|
|
1362
|
-
import { createElement, type ReactNode } from "react";
|
|
1363
|
-
import value from ${JSON.stringify(userSpecifier)};
|
|
1364
|
-
|
|
1365
|
-
export default function AkanUiOverridesLayout({ children }: { children?: ReactNode }) {
|
|
1366
|
-
return createElement(UiOverrideProvider, { value }, children);
|
|
1367
|
-
}
|
|
1368
|
-
`;
|
|
1369
|
-
await Bun.write(absPath, source);
|
|
1370
|
-
return absPath;
|
|
1371
|
-
}
|
|
1372
|
-
function getRootBoundarySegments(key) {
|
|
1373
|
-
const match = LAYOUT_KEY_RE.exec(key);
|
|
1374
|
-
if (!match)
|
|
1375
|
-
return null;
|
|
1376
|
-
const prefix = match[1]?.replace(/\/$/, "");
|
|
1377
|
-
if (!prefix)
|
|
1378
|
-
return [];
|
|
1379
|
-
return prefix.split("/").filter(Boolean);
|
|
1380
|
-
}
|
|
1381
|
-
function implicitRootLayoutKey(segments) {
|
|
1382
|
-
return `./${[...segments, "__root_layout"].join("/")}.tsx`;
|
|
1383
|
-
}
|
|
1384
|
-
function implicitRootLayoutAbsPath(appCwdPath, segments) {
|
|
1385
|
-
const filename = segments.length ? `${segments.join("__")}__root_layout.tsx` : "__root_layout.tsx";
|
|
1386
|
-
return path7.join(path7.resolve(appCwdPath), IMPLICIT_LAYOUT_DIR, filename);
|
|
1387
|
-
}
|
|
1388
|
-
function implicitDictionaryMacroAbsPath(appCwdPath) {
|
|
1389
|
-
return path7.join(path7.resolve(appCwdPath), IMPLICIT_DICT_DIR, "useDict.ts");
|
|
1390
|
-
}
|
|
1391
|
-
function isRootBoundarySegments(segments, basePaths) {
|
|
1392
|
-
const firstVisibleIndex = segments.findIndex((segment) => !/^\(.+\)$/.test(segment));
|
|
1393
|
-
if (firstVisibleIndex === -1)
|
|
1394
|
-
return segments.length <= 1;
|
|
1395
|
-
if (segments.slice(firstVisibleIndex + 1).some((segment) => /^\(.+\)$/.test(segment)))
|
|
1396
|
-
return false;
|
|
1397
|
-
const visible = segments.slice(firstVisibleIndex);
|
|
1398
|
-
const allowedBasePaths = new Set([...basePaths].map((basePath) => basePath.trim()).filter(Boolean));
|
|
1399
|
-
return visible.length === 1 && (firstVisibleIndex > 0 || allowedBasePaths.has(visible[0] ?? ""));
|
|
1400
|
-
}
|
|
1401
|
-
function findRootBoundaries(pageKeys, appCwdPath, basePaths) {
|
|
1402
|
-
const boundaries = new Map;
|
|
1403
|
-
for (const key of pageKeys) {
|
|
1404
|
-
const segments = getRootBoundarySegments(key);
|
|
1405
|
-
if (!segments)
|
|
1406
|
-
continue;
|
|
1407
|
-
if (!isRootBoundarySegments(segments, basePaths))
|
|
1408
|
-
continue;
|
|
1409
|
-
const id = segments.join("/");
|
|
1410
|
-
boundaries.set(id, {
|
|
1411
|
-
sourceKey: key,
|
|
1412
|
-
sourceAbsPath: path7.resolve(appCwdPath, "page", key.replace(/^\.\//, "")),
|
|
1413
|
-
segments
|
|
1414
|
-
});
|
|
1415
|
-
}
|
|
1416
|
-
const hasExplicitRootBoundary = [...boundaries.values()].some((boundary) => boundary.segments.length === 0);
|
|
1417
|
-
if (!hasExplicitRootBoundary && boundaries.size === 0) {
|
|
1418
|
-
boundaries.set("", { sourceKey: null, sourceAbsPath: null, segments: [] });
|
|
1419
|
-
}
|
|
1420
|
-
return [...boundaries.values()].sort((a, b) => a.segments.join("/").localeCompare(b.segments.join("/")));
|
|
1421
|
-
}
|
|
1422
|
-
function hasAncestorRootBoundary(boundary, boundaries) {
|
|
1423
|
-
return boundaries.some((candidate) => candidate !== boundary && candidate.segments.length < boundary.segments.length && candidate.segments.every((segment, index) => boundary.segments[index] === segment));
|
|
1424
|
-
}
|
|
1425
|
-
function findExplicitRootLayoutAbsPath(pageKeys, appCwdPath) {
|
|
1426
|
-
const rootLayoutKey = pageKeys.find((key) => {
|
|
1427
|
-
const segments = getRootBoundarySegments(key);
|
|
1428
|
-
return segments !== null && segments.length === 0;
|
|
1429
|
-
});
|
|
1430
|
-
return rootLayoutKey ? path7.resolve(appCwdPath, "page", rootLayoutKey.replace(/^\.\//, "")) : null;
|
|
1431
|
-
}
|
|
1432
|
-
function routePrefixForSegments(segments) {
|
|
1433
|
-
const visible = segments.filter((segment) => !/^\(.+\)$/.test(segment));
|
|
1434
|
-
return visible[0] ?? null;
|
|
1435
|
-
}
|
|
1436
|
-
async function assertEnvClientConvention(appCwdPath, appName) {
|
|
1437
|
-
const envPath = path7.join(appCwdPath, "env", "env.client.ts");
|
|
1438
|
-
if (!await Bun.file(envPath).exists()) {
|
|
1439
|
-
throw new Error(`[route-convention] app "${appName}" must provide env/env.client.ts exporting "env" for generated System.Provider`);
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
async function writeGeneratedDictionaryMacroFile(appCwdPath, appName) {
|
|
1443
|
-
const absPath = implicitDictionaryMacroAbsPath(appCwdPath);
|
|
1444
|
-
await mkdir2(path7.dirname(absPath), { recursive: true });
|
|
1445
|
-
await Bun.write(absPath, `import { getAllDictionary } from "@apps/${appName}/lib/dict" with { type: "macro" };
|
|
1446
|
-
|
|
1447
|
-
export const allDictionary = getAllDictionary();
|
|
1448
|
-
`);
|
|
1449
|
-
return absPath;
|
|
1450
|
-
}
|
|
1451
|
-
async function writeGeneratedRootLayoutFile(opts) {
|
|
1452
|
-
await assertEnvClientConvention(opts.appCwdPath, opts.appName);
|
|
1453
|
-
const dictMacroAbsPath = opts.includeSystemProvider ? await writeGeneratedDictionaryMacroFile(opts.appCwdPath, opts.appName) : null;
|
|
1454
|
-
const absPath = implicitRootLayoutAbsPath(opts.appCwdPath, opts.boundary.segments);
|
|
1455
|
-
await mkdir2(path7.dirname(absPath), { recursive: true });
|
|
1456
|
-
const dictMacroRel = dictMacroAbsPath ? path7.relative(path7.dirname(absPath), dictMacroAbsPath).split(path7.sep).join("/") : null;
|
|
1457
|
-
const dictMacroSpecifier = dictMacroRel ? dictMacroRel.startsWith(".") ? dictMacroRel : `./${dictMacroRel}` : null;
|
|
1458
|
-
const sourceRel = opts.boundary.sourceAbsPath ? path7.relative(path7.dirname(absPath), opts.boundary.sourceAbsPath).split(path7.sep).join("/") : null;
|
|
1459
|
-
const sourceSpecifier = sourceRel ? sourceRel.startsWith(".") ? sourceRel : `./${sourceRel}` : null;
|
|
1460
|
-
const inheritedSourceAbsPath = opts.rootSourceAbsPath && opts.rootSourceAbsPath !== opts.boundary.sourceAbsPath ? opts.rootSourceAbsPath : null;
|
|
1461
|
-
const inheritedSourceRel = inheritedSourceAbsPath ? path7.relative(path7.dirname(absPath), inheritedSourceAbsPath).split(path7.sep).join("/") : null;
|
|
1462
|
-
const inheritedSourceSpecifier = inheritedSourceRel ? inheritedSourceRel.startsWith(".") ? inheritedSourceRel : `./${inheritedSourceRel}` : null;
|
|
1463
|
-
const clientImport = opts.includeStInit ? `import { st } from "@apps/${opts.appName}/client";
|
|
1464
|
-
void st;
|
|
1465
|
-
` : `import "@apps/${opts.appName}/client";
|
|
1466
|
-
`;
|
|
1467
|
-
const inheritedImport = inheritedSourceSpecifier ? `import * as inheritedLayout from ${JSON.stringify(inheritedSourceSpecifier)};
|
|
1468
|
-
` : `const inheritedLayout = {};
|
|
1469
|
-
`;
|
|
1470
|
-
const prefix = routePrefixForSegments(opts.boundary.segments);
|
|
1471
|
-
const userImport = sourceSpecifier ? `import UserLayout, * as userLayout from ${JSON.stringify(sourceSpecifier)};
|
|
1472
|
-
` : `const UserLayout = ({ children }) => children;
|
|
1473
|
-
const userLayout = {};
|
|
1474
|
-
`;
|
|
1475
|
-
const source = opts.includeSystemProvider ? `import type { LayoutProps, PageProps } from "akanjs/client";
|
|
1476
|
-
import { loadFonts } from "akanjs/client";
|
|
1477
|
-
import { System } from "akanjs/ui";
|
|
1478
|
-
import { env } from "@apps/${opts.appName}/env/env.client";
|
|
1479
|
-
import { allDictionary } from ${JSON.stringify(dictMacroSpecifier)};
|
|
1480
|
-
${clientImport}${inheritedImport}${userImport}
|
|
1481
|
-
const userFonts = userLayout.fonts ?? inheritedLayout.fonts ?? [];
|
|
1482
|
-
const defaultFonts = userFonts.filter((font) => font.default);
|
|
1483
|
-
if (defaultFonts.length > 1) throw new Error("[route-convention] only one default font is allowed per root layout");
|
|
1484
|
-
const defaultFont = defaultFonts[0];
|
|
1485
|
-
const defaultFontClassName = defaultFont ? (defaultFont.className ?? \`font-\${defaultFont.name}\`) : undefined;
|
|
1486
|
-
|
|
1487
|
-
export async function generateHead(props: PageProps) {
|
|
1488
|
-
if (userLayout.generateHead) return userLayout.generateHead(props);
|
|
1489
|
-
if (userLayout.head !== undefined) return userLayout.head;
|
|
1490
|
-
if (inheritedLayout.generateHead) return inheritedLayout.generateHead(props);
|
|
1491
|
-
return inheritedLayout.head;
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
export async function generateMetadata(props: PageProps) {
|
|
1495
|
-
if (userLayout.generateMetadata) return userLayout.generateMetadata(props);
|
|
1496
|
-
if (userLayout.metadata !== undefined) return userLayout.metadata;
|
|
1497
|
-
if (inheritedLayout.generateMetadata) return inheritedLayout.generateMetadata(props);
|
|
1498
|
-
return inheritedLayout.metadata;
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;
|
|
1502
|
-
export const Error = userLayout.Error ?? inheritedLayout.Error;
|
|
1503
|
-
export const pageConfig = userLayout.pageConfig ?? inheritedLayout.pageConfig;
|
|
1504
|
-
|
|
1505
|
-
export default function GeneratedLayout({ children, params, searchParams }: LayoutProps) {
|
|
1506
|
-
return (
|
|
1507
|
-
<System.Provider
|
|
1508
|
-
of={GeneratedLayout as never}
|
|
1509
|
-
appName=${JSON.stringify(opts.appName)}
|
|
1510
|
-
${prefix ? `prefix=${JSON.stringify(prefix)}
|
|
1511
|
-
` : ""}params={params}
|
|
1512
|
-
manifest={userLayout.manifest ?? inheritedLayout.manifest}
|
|
1513
|
-
env={env}
|
|
1514
|
-
theme={userLayout.theme ?? inheritedLayout.theme}
|
|
1515
|
-
fonts={loadFonts(userFonts)}
|
|
1516
|
-
className={defaultFontClassName}
|
|
1517
|
-
gaTrackingId={userLayout.gaTrackingId ?? inheritedLayout.gaTrackingId}
|
|
1518
|
-
layoutStyle={userLayout.layoutStyle ?? inheritedLayout.layoutStyle}
|
|
1519
|
-
reconnect={userLayout.reconnect ?? inheritedLayout.reconnect ?? false}
|
|
1520
|
-
wsConnect={userLayout.wsConnect ?? inheritedLayout.wsConnect ?? true}
|
|
1521
|
-
allDictionary={process.env.AKAN_PUBLIC_RENDER_ENV === "ssr" ? allDictionary : undefined}
|
|
1522
|
-
>
|
|
1523
|
-
<UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>
|
|
1524
|
-
</System.Provider>
|
|
1525
|
-
);
|
|
1526
|
-
}
|
|
1527
|
-
` : `import type { LayoutProps, PageProps } from "akanjs/client";
|
|
1528
|
-
${inheritedImport}${userImport}
|
|
1529
|
-
export async function generateHead(props: PageProps) {
|
|
1530
|
-
if (userLayout.generateHead) return userLayout.generateHead(props);
|
|
1531
|
-
if (userLayout.head !== undefined) return userLayout.head;
|
|
1532
|
-
if (inheritedLayout.generateHead) return inheritedLayout.generateHead(props);
|
|
1533
|
-
return inheritedLayout.head;
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
export async function generateMetadata(props: PageProps) {
|
|
1537
|
-
if (userLayout.generateMetadata) return userLayout.generateMetadata(props);
|
|
1538
|
-
if (userLayout.metadata !== undefined) return userLayout.metadata;
|
|
1539
|
-
if (inheritedLayout.generateMetadata) return inheritedLayout.generateMetadata(props);
|
|
1540
|
-
return inheritedLayout.metadata;
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;
|
|
1544
|
-
export const Error = userLayout.Error ?? inheritedLayout.Error;
|
|
1545
|
-
export const pageConfig = userLayout.pageConfig ?? inheritedLayout.pageConfig;
|
|
1546
|
-
|
|
1547
|
-
export default function GeneratedLayout({ children, params, searchParams }: LayoutProps) {
|
|
1548
|
-
return <UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>;
|
|
1549
|
-
}
|
|
1550
|
-
`;
|
|
1551
|
-
await Bun.write(absPath, source);
|
|
1552
|
-
return absPath;
|
|
1553
|
-
}
|
|
1554
|
-
async function resolveSsrPageEntries(opts) {
|
|
1555
|
-
const absPageDir = path7.resolve(opts.appCwdPath, "page");
|
|
1556
|
-
const hasSt = await appHasStModule(opts.appCwdPath);
|
|
1557
|
-
const basePaths = opts.basePaths ?? [];
|
|
1558
|
-
const rootSourceAbsPath = findExplicitRootLayoutAbsPath(opts.pageKeys, opts.appCwdPath);
|
|
1559
|
-
const rootBoundaries = findRootBoundaries(opts.pageKeys, opts.appCwdPath, basePaths);
|
|
1560
|
-
const rootLayoutKeys = new Set(opts.pageKeys.filter((key) => {
|
|
1561
|
-
const segments = getRootBoundarySegments(key);
|
|
1562
|
-
return segments !== null && isRootBoundarySegments(segments, basePaths);
|
|
1563
|
-
}));
|
|
1564
|
-
const base = await Promise.all(opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map(async (key) => {
|
|
1565
|
-
const userAbsPath = path7.resolve(absPageDir, key);
|
|
1566
|
-
if (OVERRIDES_KEY_RE.test(key)) {
|
|
1567
|
-
const moduleAbsPath = await writeGeneratedOverridesLayoutFile({
|
|
1568
|
-
appCwdPath: opts.appCwdPath,
|
|
1569
|
-
key,
|
|
1570
|
-
userAbsPath
|
|
1571
|
-
});
|
|
1572
|
-
return { key, moduleAbsPath, seedAbsPaths: [userAbsPath] };
|
|
1573
|
-
}
|
|
1574
|
-
return { key, moduleAbsPath: userAbsPath };
|
|
1575
|
-
}));
|
|
1576
|
-
const generated = await Promise.all(rootBoundaries.map(async (boundary) => ({
|
|
1577
|
-
key: implicitRootLayoutKey(boundary.segments),
|
|
1578
|
-
moduleAbsPath: await writeGeneratedRootLayoutFile({
|
|
1579
|
-
appCwdPath: opts.appCwdPath,
|
|
1580
|
-
appName: opts.appName,
|
|
1581
|
-
boundary,
|
|
1582
|
-
rootSourceAbsPath,
|
|
1583
|
-
includeStInit: hasSt && boundary.segments.length === 0,
|
|
1584
|
-
includeSystemProvider: !hasAncestorRootBoundary(boundary, rootBoundaries)
|
|
1585
|
-
}),
|
|
1586
|
-
seedAbsPaths: [...new Set([boundary.sourceAbsPath, rootSourceAbsPath].filter((absPath) => absPath !== null))]
|
|
1587
|
-
})));
|
|
1588
|
-
const entries = [...base, ...generated];
|
|
1589
|
-
entries.sort((a, b) => a.key.localeCompare(b.key));
|
|
1590
|
-
return entries;
|
|
1591
|
-
}
|
|
1592
|
-
async function resolveSsrPageEntriesForApp(app, pageKeys) {
|
|
1593
|
-
const config = await app.getConfig();
|
|
1594
|
-
return resolveSsrPageEntries({ appCwdPath: app.cwdPath, appName: app.name, pageKeys, basePaths: config.basePaths });
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
// pkgs/@akanjs/devkit/artifact/routeSeedIndex.ts
|
|
1598
|
-
import path8 from "path";
|
|
1599
|
-
import { assertUniqueRoutePatterns, compareRouteSpecificity, parseRouteModuleKey } from "akanjs/common";
|
|
1600
|
-
function computeRouteSeedIndex(pageEntries) {
|
|
1601
|
-
const layoutsByPrefix = new Map;
|
|
1602
|
-
const pagesBySegments = [];
|
|
1603
|
-
for (const { key, moduleAbsPath, seedAbsPaths } of pageEntries) {
|
|
1604
|
-
const parsed = parseRouteModuleKey(key);
|
|
1605
|
-
const files = [path8.resolve(moduleAbsPath), ...(seedAbsPaths ?? []).map((seed) => path8.resolve(seed))];
|
|
1606
|
-
if (parsed.kind === "layout" || parsed.kind === "overrides") {
|
|
1607
|
-
const prefix = parsed.routeSegments.join("/");
|
|
1608
|
-
const prev = layoutsByPrefix.get(prefix) ?? [];
|
|
1609
|
-
layoutsByPrefix.set(prefix, [...prev, ...files]);
|
|
1610
|
-
} else if (parsed.kind === "page") {
|
|
1611
|
-
pagesBySegments.push({
|
|
1612
|
-
key,
|
|
1613
|
-
pattern: parsed.pattern,
|
|
1614
|
-
segments: parsed.routeSegments,
|
|
1615
|
-
files,
|
|
1616
|
-
includeOwnLayout: parsed.leaf === "_index"
|
|
1617
|
-
});
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
assertUniqueRoutePatterns(pagesBySegments);
|
|
1621
|
-
const rootLayouts = layoutsByPrefix.get("") ?? [];
|
|
1622
|
-
const globalLayoutFiles = rootLayouts;
|
|
1623
|
-
const seedEntries = [];
|
|
1624
|
-
for (const { pattern, segments, files, includeOwnLayout } of pagesBySegments) {
|
|
1625
|
-
const layouts = [];
|
|
1626
|
-
const maxPrefixLength = includeOwnLayout ? segments.length : Math.max(segments.length - 1, 0);
|
|
1627
|
-
for (let i = 0;i <= maxPrefixLength; i++) {
|
|
1628
|
-
const prefix = segments.slice(0, i).join("/");
|
|
1629
|
-
const layoutFiles = layoutsByPrefix.get(prefix);
|
|
1630
|
-
if (layoutFiles)
|
|
1631
|
-
layouts.push(...layoutFiles);
|
|
1632
|
-
}
|
|
1633
|
-
const seeds = Array.from(new Set([...layouts, ...files]));
|
|
1634
|
-
const routeId = pattern || "/";
|
|
1635
|
-
seedEntries.push({ routeId, pattern: routeId, seeds });
|
|
1636
|
-
}
|
|
1637
|
-
seedEntries.sort((a, b) => compareRouteSpecificity(a.pattern, b.pattern));
|
|
1638
|
-
return { entries: seedEntries, globalLayoutFiles };
|
|
1639
|
-
}
|
|
1640
|
-
var ROUTE_SEED_INDEX_JSON = "route-seed-index.json";
|
|
1641
|
-
function serializeRouteSeedIndexForArtifact(index, artifactDir, options = {}) {
|
|
1642
|
-
const normalizedArtifactDir = path8.resolve(artifactDir);
|
|
1643
|
-
if (options.production) {
|
|
1644
|
-
return {
|
|
1645
|
-
entries: index.entries.map((entry) => ({ routeId: entry.routeId }))
|
|
1646
|
-
};
|
|
1647
|
-
}
|
|
1648
|
-
return {
|
|
1649
|
-
entries: index.entries.map((entry) => ({
|
|
1650
|
-
...entry,
|
|
1651
|
-
seeds: entry.seeds.map((seed) => serializeArtifactPath(seed, normalizedArtifactDir))
|
|
1652
|
-
})),
|
|
1653
|
-
globalLayoutFiles: index.globalLayoutFiles.map((file) => serializeArtifactPath(file, normalizedArtifactDir))
|
|
1654
|
-
};
|
|
1655
|
-
}
|
|
1656
|
-
async function saveRouteSeedIndex(artifactDir, index, options = {}) {
|
|
1657
|
-
const absPath = path8.join(path8.resolve(artifactDir), ROUTE_SEED_INDEX_JSON);
|
|
1658
|
-
await Bun.write(absPath, `${JSON.stringify(serializeRouteSeedIndexForArtifact(index, artifactDir, options), null, 2)}
|
|
1659
|
-
`);
|
|
1660
|
-
return absPath;
|
|
1661
|
-
}
|
|
1662
|
-
function serializeArtifactPath(artifactPath, artifactDir) {
|
|
1663
|
-
if (!path8.isAbsolute(artifactPath))
|
|
1664
|
-
return artifactPath;
|
|
1665
|
-
return path8.relative(artifactDir, artifactPath).split(path8.sep).join("/");
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
|
-
// pkgs/@akanjs/devkit/frontendBuild/routesManifestArtifactSerializer.ts
|
|
1669
|
-
import path9 from "path";
|
|
1670
|
-
|
|
1671
|
-
class RoutesManifestArtifactSerializer {
|
|
1672
|
-
#manifest;
|
|
1673
|
-
#artifactDir;
|
|
1674
|
-
#production;
|
|
1675
|
-
constructor(manifest, artifactDir, options = {}) {
|
|
1676
|
-
this.#manifest = manifest;
|
|
1677
|
-
this.#artifactDir = path9.resolve(artifactDir);
|
|
1678
|
-
this.#production = options.production ?? false;
|
|
1679
|
-
}
|
|
1680
|
-
static serialize(manifest, artifactDir, options = {}) {
|
|
1681
|
-
return new RoutesManifestArtifactSerializer(manifest, artifactDir, options).serialize();
|
|
1682
|
-
}
|
|
1683
|
-
serialize() {
|
|
1684
|
-
const serialized = {
|
|
1685
|
-
...this.#manifest,
|
|
1686
|
-
knownEntries: this.#manifest.knownEntries.map((entry) => this.#serializeArtifactPath(entry))
|
|
1687
|
-
};
|
|
1688
|
-
if (this.#production)
|
|
1689
|
-
delete serialized.knownEntries;
|
|
1690
|
-
return {
|
|
1691
|
-
...serialized,
|
|
1692
|
-
clientManifest: Object.fromEntries(Object.entries(this.#manifest.clientManifest).map(([key, row]) => [this.#serializeArtifactPath(key), row])),
|
|
1693
|
-
ssrManifest: {
|
|
1694
|
-
...this.#manifest.ssrManifest,
|
|
1695
|
-
moduleMap: Object.fromEntries(Object.entries(this.#manifest.ssrManifest.moduleMap).map(([entryUrl, byName]) => [
|
|
1696
|
-
entryUrl,
|
|
1697
|
-
Object.fromEntries(Object.entries(byName).map(([name, entry]) => [
|
|
1698
|
-
name,
|
|
1699
|
-
{
|
|
1700
|
-
...entry,
|
|
1701
|
-
id: this.#serializeArtifactPath(entry.id),
|
|
1702
|
-
chunks: entry.chunks.map((chunk) => this.#serializeArtifactPath(chunk))
|
|
1703
|
-
}
|
|
1704
|
-
]))
|
|
1705
|
-
]))
|
|
1706
|
-
}
|
|
1707
|
-
};
|
|
1708
|
-
}
|
|
1709
|
-
#serializeArtifactPath(artifactPath) {
|
|
1710
|
-
if (!path9.isAbsolute(artifactPath))
|
|
1711
|
-
return artifactPath;
|
|
1712
|
-
return path9.relative(this.#artifactDir, artifactPath).split(path9.sep).join("/");
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
// pkgs/@akanjs/devkit/frontendBuild/allRoutesBuilder.ts
|
|
1717
|
-
class AllRoutesBuilder {
|
|
1718
|
-
#app;
|
|
1719
|
-
#artifact;
|
|
1720
|
-
#command;
|
|
1721
|
-
#artifactDir;
|
|
1722
|
-
#merged = {
|
|
1723
|
-
clientManifest: {},
|
|
1724
|
-
ssrManifest: { moduleLoading: null, moduleMap: {} },
|
|
1725
|
-
knownEntries: []
|
|
1726
|
-
};
|
|
1727
|
-
#knownSet = new Set;
|
|
1728
|
-
#routeIds = [];
|
|
1729
|
-
#discovery = null;
|
|
1730
|
-
constructor(app, artifact, command = "start") {
|
|
1731
|
-
this.#app = app;
|
|
1732
|
-
this.#artifact = artifact;
|
|
1733
|
-
this.#command = command;
|
|
1734
|
-
this.#artifactDir = `${command === "build" ? app.dist.cwdPath : app.cwdPath}/.akan/artifact`;
|
|
1735
|
-
}
|
|
1736
|
-
async build() {
|
|
1737
|
-
const pageKeys = await this.#app.getPageKeys();
|
|
1738
|
-
const pageEntries = await resolveSsrPageEntriesForApp(this.#app, pageKeys);
|
|
1739
|
-
const seedIndex = computeRouteSeedIndex(pageEntries);
|
|
1740
|
-
this.#app.verbose(`[build-all] discovered ${seedIndex.entries.length} routes`);
|
|
1741
|
-
this.#discovery = await GraphClientEntryDiscovery.create(this.#app);
|
|
1742
|
-
for (const entry of seedIndex.entries) {
|
|
1743
|
-
const seeds = Array.from(new Set([...seedIndex.globalLayoutFiles, ...entry.seeds]));
|
|
1744
|
-
const delta = await this.#buildRoute(entry.routeId, seeds);
|
|
1745
|
-
this.#mergeRoute(entry.routeId, delta);
|
|
1746
|
-
}
|
|
1747
|
-
this.#merged.knownEntries = Array.from(this.#knownSet);
|
|
1748
|
-
const manifest = {
|
|
1749
|
-
routeIds: this.#routeIds,
|
|
1750
|
-
...this.#merged
|
|
1751
|
-
};
|
|
1752
|
-
const manifestPath = path10.join(path10.resolve(this.#artifactDir), "routes-manifest.json");
|
|
1753
|
-
await Bun.write(manifestPath, `${JSON.stringify(RoutesManifestArtifactSerializer.serialize(manifest, this.#artifactDir, {
|
|
1754
|
-
production: this.#command === "build"
|
|
1755
|
-
}), null, 2)}
|
|
1756
|
-
`);
|
|
1757
|
-
this.#app.verbose(`[build-all] wrote ${manifestPath} routes=${this.#routeIds.length} entries=${this.#merged.knownEntries.length}`);
|
|
1758
|
-
return { manifest, manifestPath, seedIndex };
|
|
1759
|
-
}
|
|
1760
|
-
async#buildRoute(routeId, seeds) {
|
|
1761
|
-
if (!this.#discovery)
|
|
1762
|
-
throw new Error("[build-all] client entry discovery is not initialized");
|
|
1763
|
-
const started = Date.now();
|
|
1764
|
-
const delta = await new RouteClientBuilder({
|
|
1765
|
-
app: this.#app,
|
|
1766
|
-
routeId,
|
|
1767
|
-
seeds,
|
|
1768
|
-
artifact: this.#artifact,
|
|
1769
|
-
knownEntries: this.#knownSet,
|
|
1770
|
-
discovery: this.#discovery,
|
|
1771
|
-
command: this.#command
|
|
1772
|
-
}).build();
|
|
1773
|
-
this.#app.verbose(`[build-all] ${routeId} +${delta.newEntries.length} entries (${Date.now() - started}ms)`);
|
|
1774
|
-
return delta;
|
|
1775
|
-
}
|
|
1776
|
-
#mergeRoute(routeId, delta) {
|
|
1777
|
-
for (const [key, row] of Object.entries(delta.manifestDelta))
|
|
1778
|
-
this.#merged.clientManifest[key] = row;
|
|
1779
|
-
for (const [url, byName] of Object.entries(delta.ssrManifestDelta.moduleMap)) {
|
|
1780
|
-
this.#merged.ssrManifest.moduleMap[url] = byName;
|
|
1781
|
-
}
|
|
1782
|
-
for (const abs of delta.newEntries)
|
|
1783
|
-
this.#knownSet.add(abs);
|
|
1784
|
-
this.#routeIds.push(routeId);
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
// pkgs/@akanjs/devkit/frontendBuild/autoImportSync.ts
|
|
1789
|
-
import { readdir, readFile, stat, writeFile } from "fs/promises";
|
|
1790
|
-
import path11 from "path";
|
|
1791
|
-
import ts2 from "typescript";
|
|
1792
|
-
var TEST_FILE_RE = /\.(test|spec)\.(ts|tsx)$/;
|
|
1793
|
-
var AKAN_BASE = {
|
|
1794
|
-
names: ["Int", "Float", "ID", "Any", "Upload", "enumOf", "dayjs"],
|
|
1795
|
-
specifier: "akanjs/base",
|
|
1796
|
-
kind: "named"
|
|
1797
|
-
};
|
|
1798
|
-
var AKAN_BASE_TYPES = { names: ["Dayjs"], specifier: "akanjs/base", kind: "named", typeOnly: true };
|
|
1799
|
-
var CNST_NS = { names: ["cnst"], specifier: "../cnst", kind: "namespace" };
|
|
1800
|
-
var DB_NS = { names: ["db"], specifier: "../db", kind: "namespace" };
|
|
1801
|
-
var SRV_NS = { names: ["srv"], specifier: "../srv", kind: "namespace" };
|
|
1802
|
-
var ERR_DICT = { names: ["Err"], specifier: "../dict", kind: "named" };
|
|
1803
|
-
var RULES = {
|
|
1804
|
-
constant: [AKAN_BASE, AKAN_BASE_TYPES, { names: ["via"], specifier: "akanjs/constant", kind: "named" }],
|
|
1805
|
-
document: [
|
|
1806
|
-
AKAN_BASE,
|
|
1807
|
-
AKAN_BASE_TYPES,
|
|
1808
|
-
CNST_NS,
|
|
1809
|
-
DB_NS,
|
|
1810
|
-
ERR_DICT,
|
|
1811
|
-
{
|
|
1812
|
-
names: ["by", "from", "into", "SchemaOf", "DataInputOf", "DocumentUpdateHelper", "documentQueryHelper", "Mdl"],
|
|
1813
|
-
specifier: "akanjs/document",
|
|
1814
|
-
kind: "named"
|
|
1815
|
-
}
|
|
1816
|
-
],
|
|
1817
|
-
service: [
|
|
1818
|
-
AKAN_BASE,
|
|
1819
|
-
AKAN_BASE_TYPES,
|
|
1820
|
-
DB_NS,
|
|
1821
|
-
SRV_NS,
|
|
1822
|
-
CNST_NS,
|
|
1823
|
-
ERR_DICT,
|
|
1824
|
-
{ names: ["serve"], specifier: "akanjs/service", kind: "named" },
|
|
1825
|
-
{
|
|
1826
|
-
names: ["DataInputOf", "ListQueryOption", "DatabaseRegistry", "getFilterInfoByKey"],
|
|
1827
|
-
specifier: "akanjs/document",
|
|
1828
|
-
kind: "named"
|
|
1829
|
-
}
|
|
1830
|
-
],
|
|
1831
|
-
signal: [
|
|
1832
|
-
AKAN_BASE,
|
|
1833
|
-
AKAN_BASE_TYPES,
|
|
1834
|
-
SRV_NS,
|
|
1835
|
-
CNST_NS,
|
|
1836
|
-
ERR_DICT,
|
|
1837
|
-
{
|
|
1838
|
-
names: ["endpoint", "internal", "slice", "Public", "Req", "Ws", "None", "Res"],
|
|
1839
|
-
specifier: "akanjs/signal",
|
|
1840
|
-
kind: "named"
|
|
1841
|
-
}
|
|
1842
|
-
],
|
|
1843
|
-
dictionary: [
|
|
1844
|
-
{
|
|
1845
|
-
names: ["modelDictionary", "scalarDictionary", "serviceDictionary"],
|
|
1846
|
-
specifier: "akanjs/dictionary",
|
|
1847
|
-
kind: "named"
|
|
1848
|
-
}
|
|
1849
|
-
],
|
|
1850
|
-
srvkit: [
|
|
1851
|
-
AKAN_BASE,
|
|
1852
|
-
AKAN_BASE_TYPES,
|
|
1853
|
-
{ names: ["Logger", "HttpClient", "sleep"], specifier: "akanjs/common", kind: "named" },
|
|
1854
|
-
{ names: ["adapt"], specifier: "akanjs/service", kind: "named" },
|
|
1855
|
-
{ names: ["SignalContext", "Guard", "InternalArg", "Middleware"], specifier: "akanjs/signal", kind: "named" }
|
|
1856
|
-
],
|
|
1857
|
-
common: [AKAN_BASE, AKAN_BASE_TYPES],
|
|
1858
|
-
store: [
|
|
1859
|
-
CNST_NS,
|
|
1860
|
-
{ names: ["store"], specifier: "akanjs/store", kind: "named" },
|
|
1861
|
-
{ names: ["fetch", "usePage", "sig"], specifier: "../useClient", kind: "named" },
|
|
1862
|
-
{ names: ["st"], specifier: "../st", kind: "named" },
|
|
1863
|
-
{ names: ["RootStore"], specifier: "../st", kind: "named", typeOnly: true }
|
|
1864
|
-
],
|
|
1865
|
-
client: [
|
|
1866
|
-
{
|
|
1867
|
-
names: ["cnst", "fetch", "st", "usePage"],
|
|
1868
|
-
specifier: (ctx) => `@${ctx.scope}/${ctx.project}/client`,
|
|
1869
|
-
kind: "named"
|
|
1870
|
-
}
|
|
1871
|
-
]
|
|
1872
|
-
};
|
|
1873
|
-
var PASCAL_CASE_RE = /^[A-Z][A-Za-z0-9]*$/;
|
|
1874
|
-
var DOMAIN_ROLE_KINDS = {
|
|
1875
|
-
constant: ["constant"],
|
|
1876
|
-
dictionary: ["constant", "document", "signal"],
|
|
1877
|
-
common: ["constant"]
|
|
1878
|
-
};
|
|
1879
|
-
var LIB_BARRELS = {
|
|
1880
|
-
Err: { barrel: "dict", kind: "named" },
|
|
1881
|
-
db: { barrel: "db", kind: "namespace" },
|
|
1882
|
-
cnst: { barrel: "cnst", kind: "namespace" },
|
|
1883
|
-
srv: { barrel: "srv", kind: "namespace" }
|
|
1884
|
-
};
|
|
1885
|
-
var DOMAIN_DENYLIST = new Set([
|
|
1886
|
-
"Map",
|
|
1887
|
-
"Set",
|
|
1888
|
-
"WeakMap",
|
|
1889
|
-
"WeakSet",
|
|
1890
|
-
"Date",
|
|
1891
|
-
"Promise",
|
|
1892
|
-
"Array",
|
|
1893
|
-
"Object",
|
|
1894
|
-
"Error",
|
|
1895
|
-
"TypeError",
|
|
1896
|
-
"RangeError",
|
|
1897
|
-
"RegExp",
|
|
1898
|
-
"Symbol",
|
|
1899
|
-
"Proxy",
|
|
1900
|
-
"Reflect",
|
|
1901
|
-
"JSON",
|
|
1902
|
-
"Math",
|
|
1903
|
-
"Number",
|
|
1904
|
-
"BigInt",
|
|
1905
|
-
"Function",
|
|
1906
|
-
"Boolean",
|
|
1907
|
-
"String",
|
|
1908
|
-
"Record",
|
|
1909
|
-
"Partial",
|
|
1910
|
-
"Required",
|
|
1911
|
-
"Readonly",
|
|
1912
|
-
"Pick",
|
|
1913
|
-
"Omit",
|
|
1914
|
-
"Exclude",
|
|
1915
|
-
"Extract",
|
|
1916
|
-
"NonNullable",
|
|
1917
|
-
"ReturnType",
|
|
1918
|
-
"Parameters",
|
|
1919
|
-
"Awaited",
|
|
1920
|
-
"InstanceType"
|
|
1921
|
-
]);
|
|
1922
|
-
|
|
1923
|
-
class AutoImportSync {
|
|
1924
|
-
#workspaceRoot;
|
|
1925
|
-
#domainCache = new Map;
|
|
1926
|
-
constructor({ workspaceRoot }) {
|
|
1927
|
-
this.#workspaceRoot = path11.resolve(workspaceRoot);
|
|
1928
|
-
}
|
|
1929
|
-
async syncForBatch(files) {
|
|
1930
|
-
const changedFiles = [];
|
|
1931
|
-
const errors = [];
|
|
1932
|
-
const seen = new Set;
|
|
1933
|
-
for (const file of files) {
|
|
1934
|
-
const pkgRoot = this.#domainPkgRootOf(file);
|
|
1935
|
-
if (pkgRoot)
|
|
1936
|
-
this.#domainCache.delete(pkgRoot);
|
|
1937
|
-
}
|
|
1938
|
-
for (const file of files) {
|
|
1939
|
-
const abs = path11.resolve(file);
|
|
1940
|
-
if (seen.has(abs))
|
|
1941
|
-
continue;
|
|
1942
|
-
seen.add(abs);
|
|
1943
|
-
const ctx = this.#contextFor(abs);
|
|
1944
|
-
if (!ctx)
|
|
1945
|
-
continue;
|
|
1946
|
-
try {
|
|
1947
|
-
const changed = await this.#syncFile(abs, ctx);
|
|
1948
|
-
if (changed)
|
|
1949
|
-
changedFiles.push(abs);
|
|
1950
|
-
} catch (err) {
|
|
1951
|
-
errors.push(`[auto-import] sync failed for ${file}: ${formatError(err)}`);
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
|
-
return { changedFiles, errors };
|
|
1955
|
-
}
|
|
1956
|
-
#contextFor(abs) {
|
|
1957
|
-
const rel = path11.relative(this.#workspaceRoot, abs);
|
|
1958
|
-
if (rel.startsWith("..") || path11.isAbsolute(rel))
|
|
1959
|
-
return null;
|
|
1960
|
-
const base = path11.basename(abs);
|
|
1961
|
-
if (TEST_FILE_RE.test(base) || base === "index.ts" || base === "index.tsx" || base.endsWith(".d.ts"))
|
|
1962
|
-
return null;
|
|
1963
|
-
const parts = rel.split(path11.sep).filter(Boolean);
|
|
1964
|
-
const [scope, project, facet] = parts;
|
|
1965
|
-
if (scope !== "apps" && scope !== "libs" || !project || !facet)
|
|
1966
|
-
return null;
|
|
1967
|
-
const role = roleFor(facet, base);
|
|
1968
|
-
if (!role)
|
|
1969
|
-
return null;
|
|
1970
|
-
return { role, scope, project };
|
|
1971
|
-
}
|
|
1972
|
-
async#syncFile(abs, ctx) {
|
|
1973
|
-
const stats = await stat(abs).catch(() => null);
|
|
1974
|
-
if (!stats?.isFile())
|
|
1975
|
-
return false;
|
|
1976
|
-
const source = await readFile(abs, "utf8");
|
|
1977
|
-
const resolveExtra = await this.#extraResolverFor(abs, ctx);
|
|
1978
|
-
const next = transformSource(source, abs, ctx, resolveExtra);
|
|
1979
|
-
if (next === null || next === source)
|
|
1980
|
-
return false;
|
|
1981
|
-
await writeFile(abs, next);
|
|
1982
|
-
return true;
|
|
1983
|
-
}
|
|
1984
|
-
async#extraResolverFor(abs, ctx) {
|
|
1985
|
-
const resolvers = [];
|
|
1986
|
-
if (ctx.role === "srvkit" || ctx.role === "common")
|
|
1987
|
-
resolvers.push(await this.#libBarrelResolver(abs, ctx));
|
|
1988
|
-
const kinds = DOMAIN_ROLE_KINDS[ctx.role];
|
|
1989
|
-
if (kinds)
|
|
1990
|
-
resolvers.push(await this.#domainResolver(abs, ctx, kinds));
|
|
1991
|
-
if (resolvers.length === 0)
|
|
1992
|
-
return;
|
|
1993
|
-
return (symbol) => {
|
|
1994
|
-
for (const resolve of resolvers) {
|
|
1995
|
-
const target = resolve(symbol);
|
|
1996
|
-
if (target)
|
|
1997
|
-
return target;
|
|
1998
|
-
}
|
|
1999
|
-
return null;
|
|
2000
|
-
};
|
|
2001
|
-
}
|
|
2002
|
-
async#domainResolver(abs, ctx, kinds) {
|
|
2003
|
-
const index = await this.#domainIndex(path11.join(this.#workspaceRoot, ctx.scope, ctx.project));
|
|
2004
|
-
const fileDir = path11.dirname(abs);
|
|
2005
|
-
return (symbol) => {
|
|
2006
|
-
if (!PASCAL_CASE_RE.test(symbol) || DOMAIN_DENYLIST.has(symbol))
|
|
2007
|
-
return null;
|
|
2008
|
-
const entries = (index.get(symbol) ?? []).filter((entry) => kinds.includes(entry.kind));
|
|
2009
|
-
if (entries.length !== 1)
|
|
2010
|
-
return null;
|
|
2011
|
-
return { specifier: relativeSpecifier(fileDir, entries[0].file), kind: "named" };
|
|
2012
|
-
};
|
|
2013
|
-
}
|
|
2014
|
-
async#libBarrelResolver(abs, ctx) {
|
|
2015
|
-
const fileDir = path11.dirname(abs);
|
|
2016
|
-
const libDir = path11.join(this.#workspaceRoot, ctx.scope, ctx.project, "lib");
|
|
2017
|
-
const available = new Map;
|
|
2018
|
-
for (const [symbol, { barrel, kind }] of Object.entries(LIB_BARRELS)) {
|
|
2019
|
-
if (await fileExists(path11.join(libDir, `${barrel}.ts`)))
|
|
2020
|
-
available.set(symbol, { specifier: relativeSpecifier(fileDir, path11.join(libDir, barrel)), kind });
|
|
2021
|
-
}
|
|
2022
|
-
return (symbol) => available.get(symbol) ?? null;
|
|
2023
|
-
}
|
|
2024
|
-
async#domainIndex(pkgRoot) {
|
|
2025
|
-
const cached = this.#domainCache.get(pkgRoot);
|
|
2026
|
-
if (cached)
|
|
2027
|
-
return cached;
|
|
2028
|
-
const index = await buildDomainIndex(path11.join(pkgRoot, "lib"));
|
|
2029
|
-
this.#domainCache.set(pkgRoot, index);
|
|
2030
|
-
return index;
|
|
2031
|
-
}
|
|
2032
|
-
#domainPkgRootOf(file) {
|
|
2033
|
-
const rel = path11.relative(this.#workspaceRoot, path11.resolve(file));
|
|
2034
|
-
if (rel.startsWith("..") || path11.isAbsolute(rel))
|
|
2035
|
-
return null;
|
|
2036
|
-
const [scope, project, facet] = rel.split(path11.sep).filter(Boolean);
|
|
2037
|
-
if (scope !== "apps" && scope !== "libs" || !project || facet !== "lib")
|
|
2038
|
-
return null;
|
|
2039
|
-
if (!domainKindOf(path11.basename(file)))
|
|
2040
|
-
return null;
|
|
2041
|
-
return path11.join(this.#workspaceRoot, scope, project);
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
var roleFor = (facet, base) => {
|
|
2045
|
-
if (facet === "lib") {
|
|
2046
|
-
if (base.endsWith(".constant.ts"))
|
|
2047
|
-
return "constant";
|
|
2048
|
-
if (base.endsWith(".document.ts"))
|
|
2049
|
-
return "document";
|
|
2050
|
-
if (base.endsWith(".service.ts"))
|
|
2051
|
-
return "service";
|
|
2052
|
-
if (base.endsWith(".signal.ts"))
|
|
2053
|
-
return "signal";
|
|
2054
|
-
if (base.endsWith(".dictionary.ts"))
|
|
2055
|
-
return "dictionary";
|
|
2056
|
-
if (base.endsWith(".store.ts"))
|
|
2057
|
-
return "store";
|
|
2058
|
-
if (base.endsWith(".tsx"))
|
|
2059
|
-
return "client";
|
|
2060
|
-
return null;
|
|
2061
|
-
}
|
|
2062
|
-
if (facet === "ui" || facet === "page")
|
|
2063
|
-
return base.endsWith(".tsx") ? "client" : null;
|
|
2064
|
-
if (facet === "webkit")
|
|
2065
|
-
return base.endsWith(".ts") || base.endsWith(".tsx") ? "client" : null;
|
|
2066
|
-
if (facet === "srvkit")
|
|
2067
|
-
return base.endsWith(".ts") ? "srvkit" : null;
|
|
2068
|
-
if (facet === "common")
|
|
2069
|
-
return base.endsWith(".ts") || base.endsWith(".tsx") ? "common" : null;
|
|
2070
|
-
return null;
|
|
2071
|
-
};
|
|
2072
|
-
var targetFor = (symbol, ctx) => {
|
|
2073
|
-
for (const rule of RULES[ctx.role]) {
|
|
2074
|
-
if (!rule.names.includes(symbol))
|
|
2075
|
-
continue;
|
|
2076
|
-
const specifier = typeof rule.specifier === "function" ? rule.specifier(ctx) : rule.specifier;
|
|
2077
|
-
return { specifier, kind: rule.kind, typeOnly: rule.typeOnly };
|
|
2078
|
-
}
|
|
2079
|
-
return null;
|
|
2080
|
-
};
|
|
2081
|
-
var transformSource = (source, fileName, ctx, resolveExtra) => {
|
|
2082
|
-
const sf = ts2.createSourceFile(fileName, source, ts2.ScriptTarget.Latest, true, scriptKindFor(fileName));
|
|
2083
|
-
const bound = collectBoundNames(sf);
|
|
2084
|
-
const used = collectUsedReferences(sf);
|
|
2085
|
-
const namedBySpecifier = new Map;
|
|
2086
|
-
const namespaceImports = [];
|
|
2087
|
-
for (const name of used) {
|
|
2088
|
-
if (bound.has(name))
|
|
2089
|
-
continue;
|
|
2090
|
-
const target = targetFor(name, ctx) ?? resolveExtra?.(name) ?? null;
|
|
2091
|
-
if (!target)
|
|
2092
|
-
continue;
|
|
2093
|
-
if (target.kind === "namespace")
|
|
2094
|
-
namespaceImports.push({ name, specifier: target.specifier });
|
|
2095
|
-
else {
|
|
2096
|
-
const names = namedBySpecifier.get(target.specifier) ?? new Map;
|
|
2097
|
-
names.set(name, target.typeOnly ?? false);
|
|
2098
|
-
namedBySpecifier.set(target.specifier, names);
|
|
2099
|
-
}
|
|
2100
|
-
}
|
|
2101
|
-
if (namedBySpecifier.size === 0 && namespaceImports.length === 0)
|
|
2102
|
-
return null;
|
|
2103
|
-
const importDecls = sf.statements.filter(ts2.isImportDeclaration);
|
|
2104
|
-
const anchor = importDecls.at(-1) ?? null;
|
|
2105
|
-
const namedImportsBySpecifier = collectExistingNamedImports(importDecls);
|
|
2106
|
-
const edits = [];
|
|
2107
|
-
const newStatements = [];
|
|
2108
|
-
for (const [specifier, names] of namedBySpecifier) {
|
|
2109
|
-
const existing = namedImportsBySpecifier.get(specifier);
|
|
2110
|
-
if (existing) {
|
|
2111
|
-
const merged = new Map(existing.names);
|
|
2112
|
-
for (const [name, isType] of names)
|
|
2113
|
-
if (!merged.has(name))
|
|
2114
|
-
merged.set(name, isType);
|
|
2115
|
-
edits.push({
|
|
2116
|
-
start: existing.decl.getStart(sf),
|
|
2117
|
-
end: existing.decl.getEnd(),
|
|
2118
|
-
text: formatNamedImport(specifier, merged)
|
|
2119
|
-
});
|
|
2120
|
-
} else
|
|
2121
|
-
newStatements.push(formatNamedImport(specifier, names));
|
|
2122
|
-
}
|
|
2123
|
-
for (const ns of namespaceImports)
|
|
2124
|
-
newStatements.push(`import * as ${ns.name} from "${ns.specifier}";`);
|
|
2125
|
-
if (newStatements.length > 0) {
|
|
2126
|
-
const anchorEdit = anchor ? edits.find((edit) => edit.start === anchor.getStart(sf)) : undefined;
|
|
2127
|
-
if (anchorEdit)
|
|
2128
|
-
anchorEdit.text = `${anchorEdit.text}
|
|
2129
|
-
${newStatements.join(`
|
|
2130
|
-
`)}`;
|
|
2131
|
-
else if (anchor)
|
|
2132
|
-
edits.push({ start: anchor.getEnd(), end: anchor.getEnd(), text: `
|
|
2133
|
-
${newStatements.join(`
|
|
2134
|
-
`)}` });
|
|
2135
|
-
else {
|
|
2136
|
-
const prologueEnd = directivePrologueEnd(sf);
|
|
2137
|
-
if (prologueEnd >= 0)
|
|
2138
|
-
edits.push({ start: prologueEnd, end: prologueEnd, text: `
|
|
2139
|
-
${newStatements.join(`
|
|
2140
|
-
`)}` });
|
|
2141
|
-
else
|
|
2142
|
-
edits.push({ start: 0, end: 0, text: `${newStatements.join(`
|
|
2143
|
-
`)}
|
|
2144
|
-
|
|
2145
|
-
` });
|
|
2146
|
-
}
|
|
2147
|
-
}
|
|
2148
|
-
if (edits.length === 0)
|
|
2149
|
-
return null;
|
|
2150
|
-
edits.sort((a, b) => b.start - a.start);
|
|
2151
|
-
let out = source;
|
|
2152
|
-
for (const edit of edits)
|
|
2153
|
-
out = out.slice(0, edit.start) + edit.text + out.slice(edit.end);
|
|
2154
|
-
return out === source ? null : out;
|
|
2155
|
-
};
|
|
2156
|
-
var collectBoundNames = (sf) => {
|
|
2157
|
-
const names = new Set;
|
|
2158
|
-
const visit = (node) => {
|
|
2159
|
-
if (ts2.isImportClause(node)) {
|
|
2160
|
-
if (node.name)
|
|
2161
|
-
names.add(node.name.text);
|
|
2162
|
-
const bindings = node.namedBindings;
|
|
2163
|
-
if (bindings && ts2.isNamespaceImport(bindings))
|
|
2164
|
-
names.add(bindings.name.text);
|
|
2165
|
-
if (bindings && ts2.isNamedImports(bindings))
|
|
2166
|
-
for (const el of bindings.elements)
|
|
2167
|
-
names.add(el.name.text);
|
|
2168
|
-
}
|
|
2169
|
-
if ((ts2.isVariableDeclaration(node) || ts2.isFunctionDeclaration(node) || ts2.isClassDeclaration(node) || ts2.isParameter(node) || ts2.isBindingElement(node) || ts2.isEnumDeclaration(node) || ts2.isTypeAliasDeclaration(node) || ts2.isInterfaceDeclaration(node)) && node.name && ts2.isIdentifier(node.name))
|
|
2170
|
-
names.add(node.name.text);
|
|
2171
|
-
ts2.forEachChild(node, visit);
|
|
2172
|
-
};
|
|
2173
|
-
ts2.forEachChild(sf, visit);
|
|
2174
|
-
return names;
|
|
2175
|
-
};
|
|
2176
|
-
var collectUsedReferences = (sf) => {
|
|
2177
|
-
const used = new Set;
|
|
2178
|
-
const visit = (node) => {
|
|
2179
|
-
if (ts2.isIdentifier(node) && isReferencePosition(node))
|
|
2180
|
-
used.add(node.text);
|
|
2181
|
-
ts2.forEachChild(node, visit);
|
|
2182
|
-
};
|
|
2183
|
-
ts2.forEachChild(sf, visit);
|
|
2184
|
-
return used;
|
|
2185
|
-
};
|
|
2186
|
-
var isReferencePosition = (node) => {
|
|
2187
|
-
const parent = node.parent;
|
|
2188
|
-
if (!parent)
|
|
2189
|
-
return true;
|
|
2190
|
-
if (ts2.isPropertyAccessExpression(parent) && parent.name === node)
|
|
2191
|
-
return false;
|
|
2192
|
-
if (ts2.isQualifiedName(parent) && parent.right === node)
|
|
2193
|
-
return false;
|
|
2194
|
-
if (ts2.isPropertyAssignment(parent) && parent.name === node)
|
|
2195
|
-
return false;
|
|
2196
|
-
if (ts2.isPropertySignature(parent) && parent.name === node)
|
|
2197
|
-
return false;
|
|
2198
|
-
if (ts2.isBindingElement(parent) && parent.propertyName === node)
|
|
2199
|
-
return false;
|
|
2200
|
-
if (ts2.isImportSpecifier(parent) || ts2.isExportSpecifier(parent))
|
|
2201
|
-
return false;
|
|
2202
|
-
return true;
|
|
2203
|
-
};
|
|
2204
|
-
var collectExistingNamedImports = (importDecls) => {
|
|
2205
|
-
const map = new Map;
|
|
2206
|
-
for (const decl of importDecls) {
|
|
2207
|
-
const clause = decl.importClause;
|
|
2208
|
-
const bindings = clause?.namedBindings;
|
|
2209
|
-
if (!clause || !bindings || !ts2.isNamedImports(bindings))
|
|
2210
|
-
continue;
|
|
2211
|
-
if (!ts2.isStringLiteral(decl.moduleSpecifier))
|
|
2212
|
-
continue;
|
|
2213
|
-
const specifier = decl.moduleSpecifier.text;
|
|
2214
|
-
if (map.has(specifier))
|
|
2215
|
-
continue;
|
|
2216
|
-
const names = new Map;
|
|
2217
|
-
for (const el of bindings.elements)
|
|
2218
|
-
names.set(el.name.text, clause.isTypeOnly || el.isTypeOnly);
|
|
2219
|
-
map.set(specifier, { decl, names });
|
|
2220
|
-
}
|
|
2221
|
-
return map;
|
|
2222
|
-
};
|
|
2223
|
-
var formatNamedImport = (specifier, names) => {
|
|
2224
|
-
const entries = [...names.entries()].sort((a, b) => compareNames(a[0], b[0]));
|
|
2225
|
-
if (entries.every(([, isType]) => isType))
|
|
2226
|
-
return `import type { ${entries.map(([name]) => name).join(", ")} } from "${specifier}";`;
|
|
2227
|
-
const parts = entries.map(([name, isType]) => isType ? `type ${name}` : name);
|
|
2228
|
-
return `import { ${parts.join(", ")} } from "${specifier}";`;
|
|
2229
|
-
};
|
|
2230
|
-
var directivePrologueEnd = (sf) => {
|
|
2231
|
-
let end = -1;
|
|
2232
|
-
for (const stmt of sf.statements) {
|
|
2233
|
-
if (ts2.isExpressionStatement(stmt) && ts2.isStringLiteral(stmt.expression))
|
|
2234
|
-
end = stmt.getEnd();
|
|
2235
|
-
else
|
|
2236
|
-
break;
|
|
2237
|
-
}
|
|
2238
|
-
return end;
|
|
2239
|
-
};
|
|
2240
|
-
var scriptKindFor = (fileName) => fileName.endsWith(".tsx") ? ts2.ScriptKind.TSX : ts2.ScriptKind.TS;
|
|
2241
|
-
var compareNames = (a, b) => {
|
|
2242
|
-
const [la, lb] = [a.toLowerCase(), b.toLowerCase()];
|
|
2243
|
-
if (la !== lb)
|
|
2244
|
-
return la < lb ? -1 : 1;
|
|
2245
|
-
return a < b ? -1 : a > b ? 1 : 0;
|
|
2246
|
-
};
|
|
2247
|
-
var formatError = (err) => err instanceof Error ? err.message : String(err);
|
|
2248
|
-
var fileExists = async (file) => stat(file).then((s) => s.isFile()).catch(() => false);
|
|
2249
|
-
var domainKindOf = (base) => {
|
|
2250
|
-
if (base.endsWith(".constant.ts"))
|
|
2251
|
-
return "constant";
|
|
2252
|
-
if (base.endsWith(".document.ts"))
|
|
2253
|
-
return "document";
|
|
2254
|
-
if (base.endsWith(".signal.ts"))
|
|
2255
|
-
return "signal";
|
|
2256
|
-
return null;
|
|
2257
|
-
};
|
|
2258
|
-
var buildDomainIndex = async (libDir) => {
|
|
2259
|
-
const index = new Map;
|
|
2260
|
-
for (const file of await collectDomainFiles(libDir)) {
|
|
2261
|
-
const kind = domainKindOf(path11.basename(file));
|
|
2262
|
-
if (!kind)
|
|
2263
|
-
continue;
|
|
2264
|
-
const src = await readFile(file, "utf8").catch(() => null);
|
|
2265
|
-
if (src === null)
|
|
2266
|
-
continue;
|
|
2267
|
-
for (const name of exportedPascalNames(src, file)) {
|
|
2268
|
-
const entries = index.get(name) ?? [];
|
|
2269
|
-
entries.push({ file, kind });
|
|
2270
|
-
index.set(name, entries);
|
|
2271
|
-
}
|
|
2272
|
-
}
|
|
2273
|
-
return index;
|
|
2274
|
-
};
|
|
2275
|
-
var collectDomainFiles = async (dir) => {
|
|
2276
|
-
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
2277
|
-
const files = [];
|
|
2278
|
-
for (const entry of entries) {
|
|
2279
|
-
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
2280
|
-
continue;
|
|
2281
|
-
const full = path11.join(dir, entry.name);
|
|
2282
|
-
if (entry.isDirectory())
|
|
2283
|
-
files.push(...await collectDomainFiles(full));
|
|
2284
|
-
else if (domainKindOf(entry.name) && !TEST_FILE_RE.test(entry.name))
|
|
2285
|
-
files.push(full);
|
|
2286
|
-
}
|
|
2287
|
-
return files;
|
|
2288
|
-
};
|
|
2289
|
-
var exportedPascalNames = (source, fileName) => {
|
|
2290
|
-
const sf = ts2.createSourceFile(fileName, source, ts2.ScriptTarget.Latest, true);
|
|
2291
|
-
const isExported = (node) => ts2.canHaveModifiers(node) && (ts2.getModifiers(node) ?? []).some((m) => m.kind === ts2.SyntaxKind.ExportKeyword);
|
|
2292
|
-
const names = [];
|
|
2293
|
-
for (const stmt of sf.statements) {
|
|
2294
|
-
if ((ts2.isClassDeclaration(stmt) || ts2.isFunctionDeclaration(stmt) || ts2.isEnumDeclaration(stmt)) && stmt.name) {
|
|
2295
|
-
if (isExported(stmt))
|
|
2296
|
-
names.push(stmt.name.text);
|
|
2297
|
-
} else if (ts2.isVariableStatement(stmt) && isExported(stmt)) {
|
|
2298
|
-
for (const decl of stmt.declarationList.declarations)
|
|
2299
|
-
if (ts2.isIdentifier(decl.name))
|
|
2300
|
-
names.push(decl.name.text);
|
|
2301
|
-
} else if (ts2.isExportDeclaration(stmt) && stmt.exportClause && ts2.isNamedExports(stmt.exportClause)) {
|
|
2302
|
-
for (const el of stmt.exportClause.elements)
|
|
2303
|
-
names.push(el.name.text);
|
|
2304
|
-
}
|
|
2305
|
-
}
|
|
2306
|
-
return names.filter((name) => PASCAL_CASE_RE.test(name));
|
|
2307
|
-
};
|
|
2308
|
-
var relativeSpecifier = (fromDir, toFile) => {
|
|
2309
|
-
const rel = path11.relative(fromDir, toFile).replace(/\.tsx?$/, "").split(path11.sep).join("/");
|
|
2310
|
-
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
2311
|
-
};
|
|
2312
|
-
|
|
2313
|
-
// pkgs/@akanjs/devkit/frontendBuild/csrArtifactBuilder.ts
|
|
2314
|
-
import { mkdir as mkdir3, rm, unlink } from "fs/promises";
|
|
2315
|
-
import path13 from "path";
|
|
2316
|
-
|
|
2317
|
-
// pkgs/@akanjs/devkit/frontendBuild/pagesEntrySourceGenerator.ts
|
|
2318
|
-
import fs2 from "fs";
|
|
2319
|
-
import path12 from "path";
|
|
2320
|
-
import ts3 from "typescript";
|
|
2321
|
-
|
|
2322
|
-
class PagesEntrySourceGenerator {
|
|
2323
|
-
#pageEntries;
|
|
2324
|
-
constructor(pageEntries) {
|
|
2325
|
-
this.#pageEntries = pageEntries;
|
|
2326
|
-
}
|
|
2327
|
-
static generate(pageEntries) {
|
|
2328
|
-
return new PagesEntrySourceGenerator(pageEntries).generate();
|
|
2329
|
-
}
|
|
2330
|
-
generate() {
|
|
2331
|
-
const lines = this.#pageEntries.map(({ key, moduleAbsPath }) => {
|
|
2332
|
-
const specifier = PagesEntrySourceGenerator.#toImportSpecifier(moduleAbsPath);
|
|
2333
|
-
return ` ${JSON.stringify(key)}: () => import(${JSON.stringify(specifier)}),`;
|
|
2334
|
-
});
|
|
2335
|
-
return `export const pages = {
|
|
2336
|
-
${lines.join(`
|
|
2337
|
-
`)}
|
|
2338
|
-
};
|
|
2339
|
-
`;
|
|
2340
|
-
}
|
|
2341
|
-
static generateStatic(pageEntries) {
|
|
2342
|
-
return new PagesEntrySourceGenerator(pageEntries).generateStatic();
|
|
2343
|
-
}
|
|
2344
|
-
generateStatic() {
|
|
2345
|
-
const imports = this.#pageEntries.map(({ moduleAbsPath }, index) => {
|
|
2346
|
-
const specifier = PagesEntrySourceGenerator.#toImportSpecifier(moduleAbsPath);
|
|
2347
|
-
return `import * as page${index} from ${JSON.stringify(specifier)};`;
|
|
2348
|
-
});
|
|
2349
|
-
const entries = this.#pageEntries.map(({ key, moduleAbsPath }, index) => {
|
|
2350
|
-
const isAsyncDefault = PagesEntrySourceGenerator.#hasAsyncDefaultExport(moduleAbsPath);
|
|
2351
|
-
return ` ${JSON.stringify(key)}: { loader: async () => page${index}, isAsyncDefault: ${isAsyncDefault} },`;
|
|
2352
|
-
});
|
|
2353
|
-
return `${imports.join(`
|
|
2354
|
-
`)}
|
|
2355
|
-
export const pages = {
|
|
2356
|
-
${entries.join(`
|
|
2357
|
-
`)}
|
|
2358
|
-
};
|
|
2359
|
-
`;
|
|
2360
|
-
}
|
|
2361
|
-
static #toImportSpecifier(moduleAbsPath) {
|
|
2362
|
-
return path12.resolve(moduleAbsPath).split(path12.sep).join("/");
|
|
2363
|
-
}
|
|
2364
|
-
static #hasAsyncDefaultExport(moduleAbsPath) {
|
|
2365
|
-
try {
|
|
2366
|
-
const source = fs2.readFileSync(path12.resolve(moduleAbsPath), "utf8");
|
|
2367
|
-
const sourceFile = ts3.createSourceFile(moduleAbsPath, source, ts3.ScriptTarget.Latest, true, PagesEntrySourceGenerator.#scriptKind(moduleAbsPath));
|
|
2368
|
-
return PagesEntrySourceGenerator.#sourceFileHasAsyncDefaultExport(sourceFile);
|
|
2369
|
-
} catch {
|
|
2370
|
-
return false;
|
|
2371
|
-
}
|
|
2372
|
-
}
|
|
2373
|
-
static #sourceFileHasAsyncDefaultExport(sourceFile) {
|
|
2374
|
-
const asyncBindings = new Map;
|
|
2375
|
-
let defaultIdentifier = null;
|
|
2376
|
-
for (const statement of sourceFile.statements) {
|
|
2377
|
-
if (ts3.isFunctionDeclaration(statement)) {
|
|
2378
|
-
if (PagesEntrySourceGenerator.#hasModifier(statement, ts3.SyntaxKind.DefaultKeyword)) {
|
|
2379
|
-
return PagesEntrySourceGenerator.#hasModifier(statement, ts3.SyntaxKind.AsyncKeyword);
|
|
2380
|
-
}
|
|
2381
|
-
if (statement.name) {
|
|
2382
|
-
asyncBindings.set(statement.name.text, PagesEntrySourceGenerator.#hasModifier(statement, ts3.SyntaxKind.AsyncKeyword));
|
|
2383
|
-
}
|
|
2384
|
-
continue;
|
|
2385
|
-
}
|
|
2386
|
-
if (ts3.isVariableStatement(statement)) {
|
|
2387
|
-
for (const declaration of statement.declarationList.declarations) {
|
|
2388
|
-
if (!ts3.isIdentifier(declaration.name))
|
|
2389
|
-
continue;
|
|
2390
|
-
asyncBindings.set(declaration.name.text, PagesEntrySourceGenerator.#isAsyncFunctionExpression(declaration.initializer));
|
|
2391
|
-
}
|
|
2392
|
-
continue;
|
|
2393
|
-
}
|
|
2394
|
-
if (ts3.isExportAssignment(statement)) {
|
|
2395
|
-
if (PagesEntrySourceGenerator.#isAsyncFunctionExpression(statement.expression))
|
|
2396
|
-
return true;
|
|
2397
|
-
if (ts3.isIdentifier(statement.expression))
|
|
2398
|
-
defaultIdentifier = statement.expression.text;
|
|
2399
|
-
continue;
|
|
2400
|
-
}
|
|
2401
|
-
if (ts3.isExportDeclaration(statement) && statement.exportClause && ts3.isNamedExports(statement.exportClause)) {
|
|
2402
|
-
const exportClause = statement.exportClause;
|
|
2403
|
-
for (const specifier of exportClause.elements) {
|
|
2404
|
-
if (specifier.name.text !== "default")
|
|
2405
|
-
continue;
|
|
2406
|
-
defaultIdentifier = specifier.propertyName?.text ?? specifier.name.text;
|
|
2407
|
-
}
|
|
2408
|
-
}
|
|
2409
|
-
}
|
|
2410
|
-
return defaultIdentifier ? asyncBindings.get(defaultIdentifier) === true : false;
|
|
2411
|
-
}
|
|
2412
|
-
static #hasModifier(node, kind) {
|
|
2413
|
-
return ts3.canHaveModifiers(node) && (ts3.getModifiers(node)?.some((modifier) => modifier.kind === kind) ?? false);
|
|
2414
|
-
}
|
|
2415
|
-
static #isAsyncFunctionExpression(node) {
|
|
2416
|
-
return Boolean(node && (ts3.isArrowFunction(node) || ts3.isFunctionExpression(node)) && PagesEntrySourceGenerator.#hasModifier(node, ts3.SyntaxKind.AsyncKeyword));
|
|
2417
|
-
}
|
|
2418
|
-
static #scriptKind(moduleAbsPath) {
|
|
2419
|
-
return moduleAbsPath.endsWith(".tsx") || moduleAbsPath.endsWith(".jsx") ? ts3.ScriptKind.TSX : ts3.ScriptKind.TS;
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2422
|
-
|
|
2423
|
-
// pkgs/@akanjs/devkit/frontendBuild/csrArtifactBuilder.ts
|
|
2424
|
-
class CsrArtifactBuilder {
|
|
2425
|
-
#app;
|
|
2426
|
-
#command;
|
|
2427
|
-
#lang;
|
|
2428
|
-
constructor(app, command = "start", lang = "en") {
|
|
2429
|
-
this.#app = app;
|
|
2430
|
-
this.#command = command;
|
|
2431
|
-
this.#lang = lang;
|
|
2432
|
-
}
|
|
2433
|
-
async build() {
|
|
2434
|
-
const pageKeys = await this.#app.getPageKeys();
|
|
2435
|
-
if (pageKeys.length === 0) {
|
|
2436
|
-
this.#app.log(`[cli] no route files under ${this.#app.cwdPath}/page \u2014 skipping CSR build`);
|
|
2437
|
-
return null;
|
|
2438
|
-
}
|
|
2439
|
-
const pageEntries = await resolveSsrPageEntriesForApp(this.#app, pageKeys);
|
|
2440
|
-
const akanConfig = await this.#app.getConfig();
|
|
2441
|
-
const artifact = await this.#loadCsrArtifact();
|
|
2442
|
-
const csrBasePaths = [...akanConfig.basePaths];
|
|
2443
|
-
const htmlEntries = csrBasePaths.length > 0 ? csrBasePaths : ["index"];
|
|
2444
|
-
await rm(this.#outputDir, { recursive: true, force: true });
|
|
2445
|
-
await mkdir3(path13.join(this.#app.cwdPath, ".akan/generated/csr"), { recursive: true });
|
|
2446
|
-
const generatedHtmlFiles = Object.fromEntries(htmlEntries.map((basePath) => this.#createHtmlFile(basePath)));
|
|
2447
|
-
const result = await Bun.build({
|
|
2448
|
-
target: "browser",
|
|
2449
|
-
entrypoints: Object.keys(generatedHtmlFiles),
|
|
2450
|
-
files: {
|
|
2451
|
-
...generatedHtmlFiles,
|
|
2452
|
-
[`${this.#app.cwdPath}/.akan/generated/csr/csr.tsx`]: `
|
|
2453
|
-
import { bootCsr } from "akanjs/webkit";
|
|
2454
|
-
${PagesEntrySourceGenerator.generateStatic(pageEntries)}
|
|
2455
|
-
void bootCsr(pages);
|
|
2456
|
-
`
|
|
2457
|
-
},
|
|
2458
|
-
root: `${this.#app.cwdPath}/.akan/generated/csr`,
|
|
2459
|
-
outdir: this.#outputDir,
|
|
2460
|
-
splitting: false,
|
|
2461
|
-
minify: true,
|
|
2462
|
-
env: "AKAN_PUBLIC_*",
|
|
2463
|
-
define: this.#define(),
|
|
2464
|
-
optimizeImports: akanConfig.optimizeImports
|
|
2465
|
-
});
|
|
2466
|
-
if (!result.success) {
|
|
2467
|
-
const logs = result.logs.map((log) => log.message).join(`
|
|
2468
|
-
`);
|
|
2469
|
-
throw new Error(`[csr-build] failed${logs ? `
|
|
2470
|
-
${logs}` : ""}`);
|
|
2471
|
-
}
|
|
2472
|
-
await this.#inlineCsrArtifacts(artifact.cssAssets ?? {});
|
|
2473
|
-
this.#app.verbose(`[csr-build] output -> ${this.#outputDir}`);
|
|
2474
|
-
return { outputDir: this.#outputDir };
|
|
2475
|
-
}
|
|
2476
|
-
get #outputDir() {
|
|
2477
|
-
return path13.join(this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath, this.#command === "build" ? "csr" : ".akan/artifact/csr");
|
|
2478
|
-
}
|
|
2479
|
-
#define() {
|
|
2480
|
-
const nodeEnv = this.#command === "build" ? "production" : "development";
|
|
2481
|
-
return {
|
|
2482
|
-
"process.env.NODE_ENV": JSON.stringify(nodeEnv),
|
|
2483
|
-
"process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("csr"),
|
|
2484
|
-
...Object.fromEntries(Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]))
|
|
2485
|
-
};
|
|
2486
|
-
}
|
|
2487
|
-
#createHtmlFile(basePath) {
|
|
2488
|
-
const filename = `${basePath}.html`;
|
|
2489
|
-
return [
|
|
2490
|
-
`${this.#app.cwdPath}/.akan/generated/csr/${filename}`,
|
|
2491
|
-
`<!doctype html>
|
|
2492
|
-
<html lang="${this.#lang}">
|
|
2493
|
-
<head>
|
|
2494
|
-
<meta charset="utf-8" />
|
|
2495
|
-
<title>${this.#app.name}</title>
|
|
2496
|
-
<base href="/" />
|
|
2497
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
2498
|
-
</head>
|
|
2499
|
-
<body>
|
|
2500
|
-
<div id="root"></div>
|
|
2501
|
-
<script type="module" src="./csr.tsx"></script>
|
|
2502
|
-
</body>
|
|
2503
|
-
</html>
|
|
2504
|
-
`
|
|
2505
|
-
];
|
|
2506
|
-
}
|
|
2507
|
-
async#loadCsrArtifact() {
|
|
2508
|
-
const artifactDir = path13.join(this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath, ".akan/artifact");
|
|
2509
|
-
const artifactFile = Bun.file(path13.join(artifactDir, "base-artifact.json"));
|
|
2510
|
-
if (!await artifactFile.exists())
|
|
2511
|
-
return { cssAssets: {} };
|
|
2512
|
-
const artifact = await artifactFile.json();
|
|
2513
|
-
return { cssAssets: artifact.cssAssets ?? {} };
|
|
2514
|
-
}
|
|
2515
|
-
async#inlineCsrArtifacts(cssAssets) {
|
|
2516
|
-
const jsFiles = new Set;
|
|
2517
|
-
const cssFiles = new Set;
|
|
2518
|
-
for (const htmlPath of await this.#htmlOutputPaths()) {
|
|
2519
|
-
const htmlFile = Bun.file(htmlPath);
|
|
2520
|
-
if (!await htmlFile.exists())
|
|
2521
|
-
continue;
|
|
2522
|
-
const basePath = path13.basename(htmlPath, ".html") === "index" ? "" : path13.basename(htmlPath, ".html");
|
|
2523
|
-
const inlined = await this.#inlineHtmlAssets(await htmlFile.text(), htmlPath, cssAssets[basePath]);
|
|
2524
|
-
for (const filePath of inlined.jsFiles)
|
|
2525
|
-
jsFiles.add(filePath);
|
|
2526
|
-
for (const filePath of inlined.cssFiles)
|
|
2527
|
-
cssFiles.add(filePath);
|
|
2528
|
-
await Bun.write(htmlPath, inlined.html);
|
|
2529
|
-
}
|
|
2530
|
-
for (const filePath of jsFiles)
|
|
2531
|
-
await unlink(filePath).catch(() => {
|
|
2532
|
-
return;
|
|
2533
|
-
});
|
|
2534
|
-
for (const filePath of cssFiles)
|
|
2535
|
-
await unlink(filePath).catch(() => {
|
|
2536
|
-
return;
|
|
2537
|
-
});
|
|
2538
|
-
const remainingJs = await this.#listOutputFiles((filePath) => filePath.endsWith(".js"));
|
|
2539
|
-
const remainingCss = await this.#listOutputFiles((filePath) => filePath.endsWith(".css"));
|
|
2540
|
-
const remainingAssets = [...remainingJs, ...remainingCss];
|
|
2541
|
-
if (remainingAssets.length > 0) {
|
|
2542
|
-
throw new Error(`[csr-build] expected single-file HTML, but CSR assets remain:
|
|
2543
|
-
${remainingAssets.join(`
|
|
2544
|
-
`)}`);
|
|
2545
|
-
}
|
|
2546
|
-
}
|
|
2547
|
-
async#inlineHtmlAssets(html, htmlPath, cssAsset) {
|
|
2548
|
-
const jsFiles = [];
|
|
2549
|
-
const cssFiles = CsrArtifactBuilder.collectStylesheetHrefs(html).map((href) => CsrArtifactBuilder.resolveHtmlAssetPath(htmlPath, href));
|
|
2550
|
-
let next = CsrArtifactBuilder.stripBundledStylesheetLinks(html);
|
|
2551
|
-
next = await CsrArtifactBuilder.replaceModuleScriptSrc(next, async (src) => {
|
|
2552
|
-
const jsPath = CsrArtifactBuilder.resolveHtmlAssetPath(htmlPath, src);
|
|
2553
|
-
jsFiles.push(jsPath);
|
|
2554
|
-
return await Bun.file(jsPath).text();
|
|
2555
|
-
});
|
|
2556
|
-
const bundledCss = (await Promise.all(cssFiles.map((cssFile) => Bun.file(cssFile).text().catch(() => "")))).filter(Boolean).join(`
|
|
2557
|
-
`);
|
|
2558
|
-
if (bundledCss) {
|
|
2559
|
-
const style = CsrArtifactBuilder.createInlineStyle(bundledCss);
|
|
2560
|
-
if (!next.includes(style))
|
|
2561
|
-
next = CsrArtifactBuilder.injectBeforeHeadEnd(next, style);
|
|
2562
|
-
}
|
|
2563
|
-
if (cssAsset) {
|
|
2564
|
-
const cssPath = path13.join(this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath, ".akan/artifact", cssAsset.cssRelPath);
|
|
2565
|
-
const css = await Bun.file(cssPath).text();
|
|
2566
|
-
const style = CsrArtifactBuilder.createInlineStyle(css);
|
|
2567
|
-
if (!next.includes(style))
|
|
2568
|
-
next = CsrArtifactBuilder.injectBeforeHeadEnd(next, style);
|
|
2569
|
-
}
|
|
2570
|
-
return { html: next, jsFiles, cssFiles };
|
|
2571
|
-
}
|
|
2572
|
-
async#htmlOutputPaths() {
|
|
2573
|
-
return await this.#listOutputFiles((filePath) => filePath.endsWith(".html"));
|
|
2574
|
-
}
|
|
2575
|
-
async#listOutputFiles(predicate) {
|
|
2576
|
-
const glob = new Bun.Glob("**/*");
|
|
2577
|
-
const files = [];
|
|
2578
|
-
for await (const filePath of glob.scan({ cwd: this.#outputDir, absolute: true })) {
|
|
2579
|
-
if (predicate(filePath))
|
|
2580
|
-
files.push(filePath);
|
|
2581
|
-
}
|
|
2582
|
-
return files.sort();
|
|
2583
|
-
}
|
|
2584
|
-
static injectBeforeHeadEnd(html, snippet) {
|
|
2585
|
-
const matches = [...html.matchAll(/<\/head\s*>/gi)];
|
|
2586
|
-
const bodyStart = html.search(/<body(?:\s|>)/i);
|
|
2587
|
-
const headEnd = matches.filter((match) => match.index !== undefined && (bodyStart === -1 || match.index < bodyStart)).at(-1);
|
|
2588
|
-
if (!headEnd || headEnd.index === undefined)
|
|
2589
|
-
return `${snippet}
|
|
2590
|
-
${html}`;
|
|
2591
|
-
return `${html.slice(0, headEnd.index)}${snippet}
|
|
2592
|
-
${html.slice(headEnd.index)}`;
|
|
2593
|
-
}
|
|
2594
|
-
static stripBundledStylesheetLinks(html) {
|
|
2595
|
-
return html.replace(/<link\b(?=[^>]*\brel=["']stylesheet["'])[^>]*>\s*/gi, "");
|
|
2596
|
-
}
|
|
2597
|
-
static collectStylesheetHrefs(html) {
|
|
2598
|
-
const linkRe = /<link\b(?=[^>]*\brel=["']stylesheet["'])(?=[^>]*\bhref=["']([^"']+)["'])[^>]*>/gi;
|
|
2599
|
-
return [...html.matchAll(linkRe)].map((match) => match[1]).filter((href) => !!href);
|
|
2600
|
-
}
|
|
2601
|
-
static createInlineStyle(css) {
|
|
2602
|
-
return `<style data-akan-css="active">
|
|
2603
|
-
${css.replace(/<\/style/gi, "<\\/style")}
|
|
2604
|
-
</style>`;
|
|
2605
|
-
}
|
|
2606
|
-
static async replaceModuleScriptSrc(html, loadScript) {
|
|
2607
|
-
const scriptRe = /<script\b(?=[^>]*\btype=["']module["'])(?=[^>]*\bsrc=["']([^"']+)["'])[^>]*>\s*<\/script>/gi;
|
|
2608
|
-
let result = "";
|
|
2609
|
-
let lastIndex = 0;
|
|
2610
|
-
let matched = false;
|
|
2611
|
-
for (const match of html.matchAll(scriptRe)) {
|
|
2612
|
-
const full = match[0];
|
|
2613
|
-
const src = match[1];
|
|
2614
|
-
if (match.index === undefined || !src)
|
|
2615
|
-
continue;
|
|
2616
|
-
matched = true;
|
|
2617
|
-
result += html.slice(lastIndex, match.index);
|
|
2618
|
-
result += `<script type="module">
|
|
2619
|
-
${CsrArtifactBuilder.escapeInlineScript(await loadScript(src))}
|
|
2620
|
-
</script>`;
|
|
2621
|
-
lastIndex = match.index + full.length;
|
|
2622
|
-
}
|
|
2623
|
-
if (!matched)
|
|
2624
|
-
return html;
|
|
2625
|
-
return result + html.slice(lastIndex);
|
|
2626
|
-
}
|
|
2627
|
-
static escapeInlineScript(source) {
|
|
2628
|
-
return source.replace(/<\/script/gi, "<\\/script");
|
|
2629
|
-
}
|
|
2630
|
-
static resolveHtmlAssetPath(htmlPath, src) {
|
|
2631
|
-
if (/^[a-z][a-z0-9+.-]*:/i.test(src) || src.startsWith("//")) {
|
|
2632
|
-
throw new Error(`[csr-build] cannot inline external script: ${src}`);
|
|
2633
|
-
}
|
|
2634
|
-
const normalized = src.startsWith("/") ? src.slice(1) : src;
|
|
2635
|
-
return path13.resolve(path13.dirname(htmlPath), normalized);
|
|
2636
|
-
}
|
|
2637
|
-
}
|
|
2638
|
-
|
|
2639
|
-
// pkgs/@akanjs/devkit/frontendBuild/cssCompiler.ts
|
|
2640
|
-
import path15 from "path";
|
|
2641
|
-
import { Logger as Logger2 } from "akanjs/common";
|
|
2642
|
-
import { compile } from "tailwindcss";
|
|
2643
|
-
|
|
2644
|
-
// pkgs/@akanjs/devkit/frontendBuild/cssImportResolver.ts
|
|
2645
|
-
import path14 from "path";
|
|
2646
|
-
var CSS_IMPORT_EXTS = ["", ".css", "/styles.css", "/index.css"];
|
|
2647
|
-
|
|
2648
|
-
class CssImportResolver {
|
|
2649
|
-
#workspaceRoot;
|
|
2650
|
-
#paths;
|
|
2651
|
-
#wildcardEntries;
|
|
2652
|
-
constructor(workspaceRoot, paths = {}) {
|
|
2653
|
-
this.#workspaceRoot = workspaceRoot;
|
|
2654
|
-
this.#paths = paths;
|
|
2655
|
-
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);
|
|
2656
|
-
}
|
|
2657
|
-
static async create(app) {
|
|
2658
|
-
const tsconfig = await app.getTsConfig();
|
|
2659
|
-
return new CssImportResolver(app.workspace.workspaceRoot, tsconfig.compilerOptions.paths ?? {});
|
|
2660
|
-
}
|
|
2661
|
-
async resolve(id, fromBase) {
|
|
2662
|
-
for (const resolve of [
|
|
2663
|
-
() => this.#resolveWithTsconfig(id),
|
|
2664
|
-
() => this.#resolveWithBun(id, fromBase),
|
|
2665
|
-
() => this.#resolveWithRequire(id, fromBase),
|
|
2666
|
-
() => this.#resolvePackageStyle(id, fromBase)
|
|
2667
|
-
]) {
|
|
2668
|
-
const resolved = await resolve();
|
|
2669
|
-
if (resolved)
|
|
2670
|
-
return resolved;
|
|
2671
|
-
}
|
|
2672
|
-
return null;
|
|
2673
|
-
}
|
|
2674
|
-
#resolveWithBun(id, fromBase) {
|
|
2675
|
-
for (const base of this.#resolutionBases(fromBase)) {
|
|
2676
|
-
try {
|
|
2677
|
-
const resolved = Bun.resolveSync(id, base);
|
|
2678
|
-
if (CssImportResolver.isCssFile(resolved))
|
|
2679
|
-
return resolved;
|
|
2680
|
-
} catch {}
|
|
2681
|
-
}
|
|
2682
|
-
return null;
|
|
2683
|
-
}
|
|
2684
|
-
#resolveWithRequire(id, fromBase) {
|
|
2685
|
-
for (const base of this.#resolutionBases(fromBase)) {
|
|
2686
|
-
try {
|
|
2687
|
-
const resolved = __require.resolve(id, { paths: [base] });
|
|
2688
|
-
if (CssImportResolver.isCssFile(resolved))
|
|
2689
|
-
return resolved;
|
|
2690
|
-
} catch {}
|
|
2691
|
-
}
|
|
2692
|
-
return null;
|
|
2693
|
-
}
|
|
2694
|
-
async#resolveWithTsconfig(id) {
|
|
2695
|
-
const exact = this.#paths[id];
|
|
2696
|
-
if (exact) {
|
|
2697
|
-
for (const repl of exact) {
|
|
2698
|
-
const resolved = await this.#firstExisting(path14.resolve(this.#workspaceRoot, repl));
|
|
2699
|
-
if (resolved)
|
|
2700
|
-
return resolved;
|
|
2701
|
-
}
|
|
2702
|
-
}
|
|
2703
|
-
for (const { prefix, replacements } of this.#wildcardEntries) {
|
|
2704
|
-
if (!id.startsWith(prefix))
|
|
2705
|
-
continue;
|
|
2706
|
-
const suffix = id.slice(prefix.length);
|
|
2707
|
-
for (const repl of replacements) {
|
|
2708
|
-
const replPath = repl.endsWith("/*") ? repl.slice(0, -1) : repl;
|
|
2709
|
-
const resolved = await this.#firstExisting(path14.resolve(this.#workspaceRoot, replPath + suffix));
|
|
2710
|
-
if (resolved)
|
|
2711
|
-
return resolved;
|
|
2712
|
-
}
|
|
2713
|
-
}
|
|
2714
|
-
return null;
|
|
2715
|
-
}
|
|
2716
|
-
async#resolvePackageStyle(id, fromBase) {
|
|
2717
|
-
const pkgName = CssImportResolver.getPackageName(id);
|
|
2718
|
-
if (!pkgName)
|
|
2719
|
-
return null;
|
|
2720
|
-
for (const base of this.#resolutionBases(fromBase)) {
|
|
2721
|
-
try {
|
|
2722
|
-
const pkgPath = __require.resolve(`${pkgName}/package.json`, { paths: [base] });
|
|
2723
|
-
const resolved = await this.#resolvePackageStyleFromPackageJson(id, pkgName, pkgPath);
|
|
2724
|
-
if (resolved)
|
|
2725
|
-
return resolved;
|
|
2726
|
-
} catch {}
|
|
2727
|
-
}
|
|
2728
|
-
for (const pkgPath of this.#packageJsonCandidates(pkgName)) {
|
|
2729
|
-
const resolved = await this.#resolvePackageStyleFromPackageJson(id, pkgName, pkgPath);
|
|
2730
|
-
if (resolved)
|
|
2731
|
-
return resolved;
|
|
2732
|
-
}
|
|
2733
|
-
return null;
|
|
2734
|
-
}
|
|
2735
|
-
async#resolvePackageStyleFromPackageJson(id, pkgName, pkgPath) {
|
|
2736
|
-
try {
|
|
2737
|
-
if (!await Bun.file(pkgPath).exists())
|
|
2738
|
-
return null;
|
|
2739
|
-
const pkgDir = path14.dirname(pkgPath);
|
|
2740
|
-
const pkg = await Bun.file(pkgPath).json();
|
|
2741
|
-
const subpath = id === pkgName ? "." : `.${id.slice(pkgName.length)}`;
|
|
2742
|
-
const exportValue = pkg.exports?.[subpath];
|
|
2743
|
-
const styleEntry = (typeof exportValue === "string" ? exportValue : exportValue?.style || exportValue?.import || exportValue?.default) || pkg.exports?.["."]?.style || pkg.style || "index.css";
|
|
2744
|
-
return await this.#firstExisting(path14.resolve(pkgDir, styleEntry));
|
|
2745
|
-
} catch {
|
|
2746
|
-
return null;
|
|
2747
|
-
}
|
|
2748
|
-
}
|
|
2749
|
-
#resolutionBases(fromBase) {
|
|
2750
|
-
return [fromBase, this.#workspaceRoot, path14.dirname(Bun.main), path14.resolve(path14.dirname(Bun.main), "../..")];
|
|
2751
|
-
}
|
|
2752
|
-
#packageJsonCandidates(pkgName) {
|
|
2753
|
-
return [
|
|
2754
|
-
path14.join(this.#workspaceRoot, "pkgs", pkgName, "package.json"),
|
|
2755
|
-
path14.join(this.#workspaceRoot, "node_modules", pkgName, "package.json"),
|
|
2756
|
-
path14.join(path14.dirname(Bun.main), "node_modules", pkgName, "package.json"),
|
|
2757
|
-
path14.join(path14.dirname(Bun.main), "../../", pkgName, "package.json")
|
|
2758
|
-
];
|
|
2759
|
-
}
|
|
2760
|
-
async#firstExisting(basePath) {
|
|
2761
|
-
for (const suffix of CSS_IMPORT_EXTS) {
|
|
2762
|
-
const candidate = `${basePath}${suffix}`;
|
|
2763
|
-
if (await Bun.file(candidate).exists())
|
|
2764
|
-
return candidate;
|
|
2765
|
-
}
|
|
2766
|
-
return null;
|
|
2767
|
-
}
|
|
2768
|
-
static getPackageName(id) {
|
|
2769
|
-
const parts = id.split("/");
|
|
2770
|
-
if (id.startsWith("@"))
|
|
2771
|
-
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
|
|
2772
|
-
return parts[0] ?? null;
|
|
2773
|
-
}
|
|
2774
|
-
static isCssFile(filePath) {
|
|
2775
|
-
return path14.extname(filePath) === ".css";
|
|
2776
|
-
}
|
|
2777
|
-
}
|
|
2778
|
-
|
|
2779
|
-
// pkgs/@akanjs/devkit/frontendBuild/cssCompiler.ts
|
|
2780
|
-
var SOURCE_EXTS2 = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
2781
|
-
var NON_SOURCE_EXT_RE2 = /\.(json|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp3|mp4|wav)$/i;
|
|
2782
|
-
var NODE_MODULES_RE3 = /[\\/]node_modules[\\/]/;
|
|
2783
|
-
var AKANJS_NODE_MODULE_RE3 = /[\\/]node_modules[\\/]akanjs[\\/]/;
|
|
2784
|
-
|
|
2785
|
-
class CssCompiler {
|
|
2786
|
-
#logger = new Logger2("CssCompiler");
|
|
2787
|
-
#transpiler = new Bun.Transpiler({ loader: "tsx" });
|
|
2788
|
-
#app;
|
|
2789
|
-
#cssImportResolver = null;
|
|
2790
|
-
constructor(app) {
|
|
2791
|
-
this.#app = app;
|
|
2792
|
-
}
|
|
2793
|
-
#cssText = null;
|
|
2794
|
-
#cssTextByBasePath = null;
|
|
2795
|
-
async getCss({ refresh } = {}) {
|
|
2796
|
-
if (this.#cssText !== null && !refresh)
|
|
2797
|
-
return this.#cssText;
|
|
2798
|
-
const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh });
|
|
2799
|
-
this.#cssText = await this.compileCss(cssPaths, sourcePaths);
|
|
2800
|
-
return this.#cssText;
|
|
2801
|
-
}
|
|
2802
|
-
async getCssByBasePath({ refresh } = {}) {
|
|
2803
|
-
if (this.#cssTextByBasePath !== null && !refresh)
|
|
2804
|
-
return this.#cssTextByBasePath;
|
|
2805
|
-
const akanConfig = await this.#app.getConfig({ refresh });
|
|
2806
|
-
const pageKeys = await this.#app.getPageKeys({ refresh });
|
|
2807
|
-
const basePaths = [...akanConfig.basePaths];
|
|
2808
|
-
const rootPageKeys = pageKeys.filter((pageKey) => getPageKeyBasePath(pageKey, basePaths) === null);
|
|
2809
|
-
const cssEntries = await Promise.all([
|
|
2810
|
-
(async () => {
|
|
2811
|
-
if (rootPageKeys.length === 0)
|
|
2812
|
-
return ["", ""];
|
|
2813
|
-
const started = Date.now();
|
|
2814
|
-
const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: rootPageKeys });
|
|
2815
|
-
const css = await this.compileCss(cssPaths, sourcePaths);
|
|
2816
|
-
this.#logger.verbose(`css base=root paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`);
|
|
2817
|
-
return ["", css];
|
|
2818
|
-
})(),
|
|
2819
|
-
...basePaths.map(async (basePath) => {
|
|
2820
|
-
const basePathPageKeys = pageKeys.filter((pageKey) => getPageKeyBasePath(pageKey, basePaths) === basePath);
|
|
2821
|
-
if (basePathPageKeys.length === 0)
|
|
2822
|
-
return [basePath, ""];
|
|
2823
|
-
const started = Date.now();
|
|
2824
|
-
const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: basePathPageKeys });
|
|
2825
|
-
const css = await this.compileCss(cssPaths, sourcePaths);
|
|
2826
|
-
this.#logger.verbose(`css base=${basePath} paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`);
|
|
2827
|
-
return [basePath, css];
|
|
2828
|
-
})
|
|
2829
|
-
]);
|
|
2830
|
-
this.#cssTextByBasePath = Object.fromEntries(cssEntries);
|
|
2831
|
-
return this.#cssTextByBasePath;
|
|
2832
|
-
}
|
|
2833
|
-
async discoverCss({ refresh } = {}) {
|
|
2834
|
-
const { cssPaths } = await this.discoverCssAndSources({ refresh });
|
|
2835
|
-
return cssPaths;
|
|
2836
|
-
}
|
|
2837
|
-
async discoverCssAndSources({
|
|
2838
|
-
refresh,
|
|
2839
|
-
pageKeys
|
|
2840
|
-
} = {}) {
|
|
2841
|
-
pageKeys ??= await this.#app.getPageKeys({ refresh });
|
|
2842
|
-
const seeds = pageKeys.map((key) => path15.resolve(this.#app.cwdPath, "page", key));
|
|
2843
|
-
const cssFiles = new Set;
|
|
2844
|
-
const sourceFiles = new Set;
|
|
2845
|
-
const queue = [...seeds];
|
|
2846
|
-
const resolvePackage = await createTsconfigPackageResolver(this.#app);
|
|
2847
|
-
const analyzer = new BarrelAnalyzer({ resolvePackage });
|
|
2848
|
-
const akanConfig = await this.#app.getConfig({ refresh });
|
|
2849
|
-
while (queue.length > 0) {
|
|
2850
|
-
const filePath = queue.shift();
|
|
2851
|
-
if (!filePath || sourceFiles.has(filePath) || isIgnoredNodeModuleSource(filePath))
|
|
2852
|
-
continue;
|
|
2853
|
-
sourceFiles.add(filePath);
|
|
2854
|
-
let content;
|
|
2855
|
-
try {
|
|
2856
|
-
content = await Bun.file(filePath).text();
|
|
2857
|
-
} catch {
|
|
2858
|
-
continue;
|
|
2859
|
-
}
|
|
2860
|
-
let source = content;
|
|
2861
|
-
if (akanConfig.barrelImports.length > 0) {
|
|
2862
|
-
try {
|
|
2863
|
-
const rewritten = await rewriteBarrelImports(content, akanConfig.barrelImports, analyzer);
|
|
2864
|
-
if (rewritten !== null)
|
|
2865
|
-
source = rewritten;
|
|
2866
|
-
} catch {}
|
|
2867
|
-
}
|
|
2868
|
-
let imports;
|
|
2869
|
-
try {
|
|
2870
|
-
imports = this.#transpiler.scanImports(source);
|
|
2871
|
-
} catch {
|
|
2872
|
-
continue;
|
|
2873
|
-
}
|
|
2874
|
-
const importerDir = path15.dirname(filePath);
|
|
2875
|
-
for (const imp of imports) {
|
|
2876
|
-
const spec = imp.path;
|
|
2877
|
-
if (!spec)
|
|
2878
|
-
continue;
|
|
2879
|
-
if (spec.endsWith(".css")) {
|
|
2880
|
-
const cssPath = await this.#resolveCssImport(spec, importerDir);
|
|
2881
|
-
cssFiles.add(cssPath);
|
|
2882
|
-
continue;
|
|
2883
|
-
}
|
|
2884
|
-
if (NON_SOURCE_EXT_RE2.test(spec))
|
|
2885
|
-
continue;
|
|
2886
|
-
const resolved = await this.#resolveSourceImport(spec, importerDir, resolvePackage);
|
|
2887
|
-
if (!resolved || sourceFiles.has(resolved) || isIgnoredNodeModuleSource(resolved))
|
|
2888
|
-
continue;
|
|
2889
|
-
queue.push(resolved);
|
|
2890
|
-
}
|
|
2891
|
-
}
|
|
2892
|
-
return { cssPaths: [...cssFiles], sourcePaths: [...sourceFiles] };
|
|
2893
|
-
}
|
|
2894
|
-
async compileCss(cssPaths, sourcePaths) {
|
|
2895
|
-
if (cssPaths.length === 0)
|
|
2896
|
-
return "";
|
|
2897
|
-
const compileStarted = Date.now();
|
|
2898
|
-
const compilers = await Promise.all(cssPaths.map(async (cssPath) => {
|
|
2899
|
-
const css = await Bun.file(cssPath).text();
|
|
2900
|
-
const base = path15.dirname(cssPath);
|
|
2901
|
-
const compiler = await compile(css, {
|
|
2902
|
-
base,
|
|
2903
|
-
loadStylesheet: (id, fromBase) => this.#loadStylesheet(id, fromBase),
|
|
2904
|
-
loadModule: (id, fromBase) => this.#loadModule(id, fromBase)
|
|
2905
|
-
});
|
|
2906
|
-
return { cssPath, compiler };
|
|
2907
|
-
}));
|
|
2908
|
-
const sourceDirs = new Set;
|
|
2909
|
-
for (const entry of compilers) {
|
|
2910
|
-
if (!entry)
|
|
2911
|
-
continue;
|
|
2912
|
-
for (const s of entry.compiler.sources)
|
|
2913
|
-
sourceDirs.add(s.base);
|
|
2914
|
-
}
|
|
2915
|
-
const scanStarted = Date.now();
|
|
2916
|
-
const candidates = await this.#scanCandidates(sourcePaths, [...sourceDirs]);
|
|
2917
|
-
this.#logger.verbose(`css candidates scanned count=${candidates.length} sources=${sourcePaths.length} dirs=${sourceDirs.size} in ${Date.now() - scanStarted}ms`);
|
|
2918
|
-
const parts = [];
|
|
2919
|
-
for (const entry of compilers) {
|
|
2920
|
-
if (!entry)
|
|
2921
|
-
continue;
|
|
2922
|
-
parts.push(entry.compiler.build(candidates));
|
|
2923
|
-
}
|
|
2924
|
-
this.#logger.verbose(`css compiled paths=${cssPaths.length} candidates=${candidates.length} in ${Date.now() - compileStarted}ms`);
|
|
2925
|
-
return parts.join(`
|
|
2926
|
-
`);
|
|
2927
|
-
}
|
|
2928
|
-
async#loadStylesheet(id, fromBase) {
|
|
2929
|
-
const p = await this.#resolveCssImport(id, fromBase);
|
|
2930
|
-
const content = await Bun.file(p).text();
|
|
2931
|
-
return { path: p, base: path15.dirname(p), content };
|
|
2932
|
-
}
|
|
2933
|
-
async#resolveCssImport(id, fromBase) {
|
|
2934
|
-
if (id.startsWith(".") || id.startsWith("/"))
|
|
2935
|
-
return path15.resolve(fromBase, id);
|
|
2936
|
-
const resolver = await this.#getCssImportResolver();
|
|
2937
|
-
const resolved = await resolver.resolve(id, fromBase);
|
|
2938
|
-
if (resolved)
|
|
2939
|
-
return resolved;
|
|
2940
|
-
throw new Error(`[css] failed to resolve stylesheet import "${id}" from ${fromBase}`);
|
|
2941
|
-
}
|
|
2942
|
-
async#getCssImportResolver() {
|
|
2943
|
-
if (this.#cssImportResolver)
|
|
2944
|
-
return this.#cssImportResolver;
|
|
2945
|
-
this.#cssImportResolver = await CssImportResolver.create(this.#app);
|
|
2946
|
-
return this.#cssImportResolver;
|
|
2947
|
-
}
|
|
2948
|
-
async#loadModule(id, fromBase) {
|
|
2949
|
-
const p = __require.resolve(id, { paths: [fromBase] });
|
|
2950
|
-
const mod = await import(p);
|
|
2951
|
-
return { path: p, base: path15.dirname(p), module: mod.default ?? mod };
|
|
2952
|
-
}
|
|
2953
|
-
async#resolveSourceImport(id, fromBase, resolvePackage) {
|
|
2954
|
-
if (id.startsWith(".") || id.startsWith("/")) {
|
|
2955
|
-
const abs = id.startsWith("/") ? id : path15.resolve(fromBase, id);
|
|
2956
|
-
return resolveSourceFileCandidate(abs);
|
|
2957
|
-
}
|
|
2958
|
-
const pkg = await resolvePackage(id);
|
|
2959
|
-
if (pkg)
|
|
2960
|
-
return pkg.entryFile;
|
|
2961
|
-
for (const resolve of [() => resolveSourceWithBun(id, fromBase), () => resolveSourceWithRequire(id, fromBase)]) {
|
|
2962
|
-
const resolved = await resolve();
|
|
2963
|
-
if (resolved)
|
|
2964
|
-
return resolved;
|
|
2965
|
-
}
|
|
2966
|
-
return null;
|
|
2967
|
-
}
|
|
2968
|
-
async#scanCandidates(sourcePaths, dirs) {
|
|
2969
|
-
const CANDIDATE_RE = /-?[\w@][\w:/.-]*(?:\[[^\]]+\][\w:/.-]*)*/g;
|
|
2970
|
-
const candidates = new Set;
|
|
2971
|
-
const glob = new Bun.Glob("**/*.{tsx,ts,jsx,js,html}");
|
|
2972
|
-
const files = new Set(sourcePaths);
|
|
2973
|
-
await Promise.all(dirs.map(async (dir) => {
|
|
2974
|
-
for await (const file of glob.scan({ cwd: dir, absolute: true })) {
|
|
2975
|
-
if (isIgnoredNodeModuleSource(file))
|
|
2976
|
-
continue;
|
|
2977
|
-
files.add(file);
|
|
2978
|
-
}
|
|
2979
|
-
}));
|
|
2980
|
-
await Promise.all([...files].map(async (file) => {
|
|
2981
|
-
const content = await Bun.file(file).text();
|
|
2982
|
-
for (const m of content.matchAll(CANDIDATE_RE))
|
|
2983
|
-
candidates.add(m[0]);
|
|
2984
|
-
}));
|
|
2985
|
-
return [...candidates];
|
|
2986
|
-
}
|
|
2987
|
-
}
|
|
2988
|
-
async function resolveSourceFileCandidate(absPathNoExt) {
|
|
2989
|
-
if (await Bun.file(absPathNoExt).exists())
|
|
2990
|
-
return isSourceFile(absPathNoExt) ? absPathNoExt : null;
|
|
2991
|
-
for (const ext of SOURCE_EXTS2) {
|
|
2992
|
-
const filePath = `${absPathNoExt}${ext}`;
|
|
2993
|
-
if (await Bun.file(filePath).exists())
|
|
2994
|
-
return filePath;
|
|
2995
|
-
}
|
|
2996
|
-
for (const ext of SOURCE_EXTS2) {
|
|
2997
|
-
const filePath = path15.join(absPathNoExt, `index${ext}`);
|
|
2998
|
-
if (await Bun.file(filePath).exists())
|
|
2999
|
-
return filePath;
|
|
3000
|
-
}
|
|
3001
|
-
return null;
|
|
3002
|
-
}
|
|
3003
|
-
function resolveSourceWithBun(id, fromBase) {
|
|
3004
|
-
try {
|
|
3005
|
-
const resolved = Bun.resolveSync(id, fromBase);
|
|
3006
|
-
return isSourceFile(resolved) ? resolved : null;
|
|
3007
|
-
} catch {
|
|
3008
|
-
return null;
|
|
3009
|
-
}
|
|
3010
|
-
}
|
|
3011
|
-
function resolveSourceWithRequire(id, fromBase) {
|
|
3012
|
-
try {
|
|
3013
|
-
const resolved = __require.resolve(id, { paths: [fromBase] });
|
|
3014
|
-
return isSourceFile(resolved) ? resolved : null;
|
|
3015
|
-
} catch {
|
|
3016
|
-
return null;
|
|
3017
|
-
}
|
|
3018
|
-
}
|
|
3019
|
-
function isSourceFile(filePath) {
|
|
3020
|
-
return SOURCE_EXTS2.includes(path15.extname(filePath));
|
|
3021
|
-
}
|
|
3022
|
-
function isIgnoredNodeModuleSource(filePath) {
|
|
3023
|
-
return NODE_MODULES_RE3.test(filePath) && !AKANJS_NODE_MODULE_RE3.test(filePath);
|
|
3024
|
-
}
|
|
3025
|
-
function getPageKeyBasePath(pageKey, basePaths) {
|
|
3026
|
-
const normalized = pageKey.split(path15.sep).join("/").replace(/^\.\//, "");
|
|
3027
|
-
const segments = normalized.split("/");
|
|
3028
|
-
const firstPublicSegment = segments.find((segment) => segment !== "[lang]" && !/^\(.+\)$/.test(segment));
|
|
3029
|
-
return firstPublicSegment && basePaths.includes(firstPublicSegment) ? firstPublicSegment : null;
|
|
3030
|
-
}
|
|
3031
|
-
|
|
3032
|
-
// pkgs/@akanjs/devkit/frontendBuild/devChangePlanner.ts
|
|
3033
|
-
import path16 from "path";
|
|
3034
|
-
var SOURCE_EXTS3 = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
3035
|
-
var CONFIG_BASENAMES = new Set(["akan.config.ts", "bunfig.toml", "tsconfig.json", "package.json"]);
|
|
3036
|
-
var BARREL_FACETS = new Set(["common", "srvkit", "ui", "webkit", "plugin"]);
|
|
3037
|
-
var CLIENT_SUFFIXES = [".Template.tsx", ".Unit.tsx", ".Util.tsx", ".View.tsx", ".Zone.tsx", ".store.ts"];
|
|
3038
|
-
var SHARED_SUFFIXES = [".constant.ts", ".dictionary.ts", ".signal.ts"];
|
|
3039
|
-
var SERVER_SUFFIXES = [".service.ts", ".document.ts"];
|
|
3040
|
-
var RUNTIME_METADATA_BASENAMES = new Set(["dict.ts", "sig.ts", "useClient.ts", "useServer.ts"]);
|
|
3041
|
-
|
|
3042
|
-
class DevChangePlanner {
|
|
3043
|
-
#workspaceRoot;
|
|
3044
|
-
constructor({ workspaceRoot }) {
|
|
3045
|
-
this.#workspaceRoot = path16.resolve(workspaceRoot);
|
|
3046
|
-
}
|
|
3047
|
-
plan({ generation, files, kinds, generatedFiles = [] }) {
|
|
3048
|
-
const fileList = uniqueResolved([...files, ...generatedFiles]);
|
|
3049
|
-
const generatedSet = new Set(generatedFiles.map((file) => path16.resolve(file)));
|
|
3050
|
-
const kindSet = new Set(kinds);
|
|
3051
|
-
const roles = new Set;
|
|
3052
|
-
const actions = new Set;
|
|
3053
|
-
const reasonByFile = {};
|
|
3054
|
-
for (const kind of kindSet) {
|
|
3055
|
-
if (kind === "css") {
|
|
3056
|
-
roles.add("css");
|
|
3057
|
-
actions.add("rebuild-css");
|
|
3058
|
-
}
|
|
3059
|
-
if (kind === "config") {
|
|
3060
|
-
roles.add("config");
|
|
3061
|
-
actions.add("restart-dev-host");
|
|
3062
|
-
}
|
|
3063
|
-
}
|
|
3064
|
-
for (const file of fileList) {
|
|
3065
|
-
const reasons = new Set;
|
|
3066
|
-
const fileRoles = this.#rolesForFile(file, { isGenerated: generatedSet.has(path16.resolve(file)), reasons });
|
|
3067
|
-
for (const role of fileRoles)
|
|
3068
|
-
roles.add(role);
|
|
3069
|
-
if (reasons.has("runtime-metadata"))
|
|
3070
|
-
actions.add("restart-builder");
|
|
3071
|
-
if (reasons.size > 0)
|
|
3072
|
-
reasonByFile[path16.resolve(file)] = [...reasons].sort();
|
|
3073
|
-
}
|
|
3074
|
-
if (roles.has("barrel"))
|
|
3075
|
-
actions.add("sync-generated");
|
|
3076
|
-
if (roles.has("server") || roles.has("shared"))
|
|
3077
|
-
actions.add("restart-backend");
|
|
3078
|
-
if (roles.has("client") || roles.has("shared"))
|
|
3079
|
-
actions.add("rebuild-client");
|
|
3080
|
-
if (roles.has("css"))
|
|
3081
|
-
actions.add("rebuild-css");
|
|
3082
|
-
return {
|
|
3083
|
-
generation,
|
|
3084
|
-
files: fileList,
|
|
3085
|
-
generatedFiles: uniqueResolved(generatedFiles),
|
|
3086
|
-
roles: [...roles].sort(),
|
|
3087
|
-
actions: [...actions].sort(),
|
|
3088
|
-
reasonByFile
|
|
3089
|
-
};
|
|
3090
|
-
}
|
|
3091
|
-
#rolesForFile(file, { isGenerated, reasons }) {
|
|
3092
|
-
const roles = new Set;
|
|
3093
|
-
const abs = path16.resolve(file);
|
|
3094
|
-
const base = path16.basename(abs);
|
|
3095
|
-
const ext = path16.extname(abs).toLowerCase();
|
|
3096
|
-
const isSource = SOURCE_EXTS3.has(ext);
|
|
3097
|
-
const rel = path16.relative(this.#workspaceRoot, abs);
|
|
3098
|
-
const parts = rel.split(path16.sep).filter(Boolean);
|
|
3099
|
-
if (CONFIG_BASENAMES.has(base)) {
|
|
3100
|
-
roles.add("config");
|
|
3101
|
-
reasons.add("config-file");
|
|
3102
|
-
}
|
|
3103
|
-
if (ext === ".css") {
|
|
3104
|
-
roles.add("css");
|
|
3105
|
-
reasons.add("css-file");
|
|
3106
|
-
}
|
|
3107
|
-
if (isGenerated || isSource && this.#isBarrelFacetChild(parts)) {
|
|
3108
|
-
roles.add("barrel");
|
|
3109
|
-
reasons.add(isGenerated ? "generated-index" : "barrel-facet-child");
|
|
3110
|
-
}
|
|
3111
|
-
if (isSource && this.#isServerBiased(abs, parts)) {
|
|
3112
|
-
roles.add("server");
|
|
3113
|
-
reasons.add("server-path");
|
|
3114
|
-
}
|
|
3115
|
-
if (isSource && this.#isClientBiased(abs, parts)) {
|
|
3116
|
-
roles.add("client");
|
|
3117
|
-
reasons.add("client-path");
|
|
3118
|
-
}
|
|
3119
|
-
if (isSource && this.#isSharedBiased(abs, parts)) {
|
|
3120
|
-
roles.add("shared");
|
|
3121
|
-
reasons.add("shared-path");
|
|
3122
|
-
}
|
|
3123
|
-
if (isSource && this.#isRuntimeMetadataFile(parts, base)) {
|
|
3124
|
-
reasons.add("runtime-metadata");
|
|
3125
|
-
}
|
|
3126
|
-
if (roles.has("server") && roles.has("client")) {
|
|
3127
|
-
roles.delete("server");
|
|
3128
|
-
roles.delete("client");
|
|
3129
|
-
roles.add("shared");
|
|
3130
|
-
reasons.add("server-client-overlap");
|
|
3131
|
-
}
|
|
3132
|
-
if (roles.size === 0 && SOURCE_EXTS3.has(ext) && this.#isWorkspaceSource(rel)) {
|
|
3133
|
-
roles.add("shared");
|
|
3134
|
-
reasons.add("workspace-source-fallback");
|
|
3135
|
-
}
|
|
3136
|
-
return roles;
|
|
3137
|
-
}
|
|
3138
|
-
#isServerBiased(abs, parts) {
|
|
3139
|
-
const base = path16.basename(abs);
|
|
3140
|
-
return parts.includes("srvkit") || SERVER_SUFFIXES.some((suffix) => base.endsWith(suffix)) || base === "main.ts" || base === "server.ts";
|
|
3141
|
-
}
|
|
3142
|
-
#isClientBiased(abs, parts) {
|
|
3143
|
-
const base = path16.basename(abs);
|
|
3144
|
-
return parts.includes("ui") || parts.includes("webkit") || parts.includes("page") || CLIENT_SUFFIXES.some((suffix) => base.endsWith(suffix));
|
|
3145
|
-
}
|
|
3146
|
-
#isSharedBiased(abs, parts) {
|
|
3147
|
-
const base = path16.basename(abs);
|
|
3148
|
-
return parts.includes("common") || SHARED_SUFFIXES.some((suffix) => base.endsWith(suffix)) || RUNTIME_METADATA_BASENAMES.has(base);
|
|
3149
|
-
}
|
|
3150
|
-
#isRuntimeMetadataFile(parts, base) {
|
|
3151
|
-
const parent = parts.at(-2);
|
|
3152
|
-
if (parent === "lib" && RUNTIME_METADATA_BASENAMES.has(base))
|
|
3153
|
-
return true;
|
|
3154
|
-
const libIndex = parts.lastIndexOf("lib");
|
|
3155
|
-
if (libIndex < 0 || parts.length <= libIndex + 1)
|
|
3156
|
-
return false;
|
|
3157
|
-
return base.endsWith(".dictionary.ts") || base.endsWith(".signal.ts");
|
|
3158
|
-
}
|
|
3159
|
-
#isBarrelFacetChild(parts) {
|
|
3160
|
-
if (parts.length < 4)
|
|
3161
|
-
return false;
|
|
3162
|
-
const [scope, , facet, child] = parts;
|
|
3163
|
-
if (scope !== "apps" && scope !== "libs")
|
|
3164
|
-
return false;
|
|
3165
|
-
if (!facet || !BARREL_FACETS.has(facet))
|
|
3166
|
-
return false;
|
|
3167
|
-
if (!child || child.startsWith(".") || child === "index.ts" || child === "index.tsx")
|
|
3168
|
-
return false;
|
|
3169
|
-
return true;
|
|
3170
|
-
}
|
|
3171
|
-
#isWorkspaceSource(rel) {
|
|
3172
|
-
if (!rel || rel.startsWith("..") || path16.isAbsolute(rel))
|
|
3173
|
-
return false;
|
|
3174
|
-
const [scope] = rel.split(path16.sep);
|
|
3175
|
-
return scope === "apps" || scope === "libs" || scope === "pkgs";
|
|
3176
|
-
}
|
|
3177
|
-
}
|
|
3178
|
-
var uniqueResolved = (files) => [...new Set(files.map((file) => path16.resolve(file)))].sort();
|
|
3179
|
-
|
|
3180
|
-
// pkgs/@akanjs/devkit/frontendBuild/devGeneratedIndexSync.ts
|
|
3181
|
-
import { mkdir as mkdir4, readdir as readdir2, readFile as readFile2, rm as rm2, stat as stat2, writeFile as writeFile2 } from "fs/promises";
|
|
3182
|
-
import path17 from "path";
|
|
3183
|
-
var BARREL_FACETS2 = new Set(["common", "srvkit", "ui", "webkit", "plugin"]);
|
|
3184
|
-
var FACET_SOURCE_FILE_RE = /\.(ts|tsx)$/;
|
|
3185
|
-
var FACET_EXCLUDED_FILE_RE = /(^index\.tsx?$|\.d\.ts$|\.(test|spec)\.(ts|tsx)$|\.css$|\.scss$|\.sass$)/;
|
|
3186
|
-
var FACET_PASCAL_CASE_RE = /^[A-Z][A-Za-z0-9]*$/;
|
|
3187
|
-
var FACET_CAMEL_CASE_RE = /^[a-z][A-Za-z0-9]*$/;
|
|
3188
|
-
var MODULE_UI_TYPES = ["Template", "Unit", "Util", "View", "Zone"];
|
|
3189
|
-
var SERVICE_UI_TYPES = ["Util", "Zone"];
|
|
3190
|
-
var SCALAR_UI_TYPES = ["Template", "Unit"];
|
|
3191
|
-
|
|
3192
|
-
class DevGeneratedIndexSync {
|
|
3193
|
-
#workspaceRoot;
|
|
3194
|
-
constructor({ workspaceRoot }) {
|
|
3195
|
-
this.#workspaceRoot = path17.resolve(workspaceRoot);
|
|
3196
|
-
}
|
|
3197
|
-
async syncForBatch(files) {
|
|
3198
|
-
const indexPaths = new Set;
|
|
3199
|
-
const errors = [];
|
|
3200
|
-
for (const file of files) {
|
|
3201
|
-
const facetIndex = this.#facetIndexFor(file);
|
|
3202
|
-
if (facetIndex)
|
|
3203
|
-
indexPaths.add(facetIndex);
|
|
3204
|
-
const moduleIndex = await this.#moduleIndexForDirectoryEvent(file).catch((err) => {
|
|
3205
|
-
errors.push(`[generated-index] module detection failed for ${file}: ${formatError2(err)}`);
|
|
3206
|
-
return null;
|
|
3207
|
-
});
|
|
3208
|
-
if (moduleIndex)
|
|
3209
|
-
indexPaths.add(moduleIndex);
|
|
3210
|
-
}
|
|
3211
|
-
const changedFiles = [];
|
|
3212
|
-
for (const indexPath of [...indexPaths].sort()) {
|
|
3213
|
-
try {
|
|
3214
|
-
const changed = await this.#syncIndex(indexPath);
|
|
3215
|
-
if (changed)
|
|
3216
|
-
changedFiles.push(indexPath);
|
|
3217
|
-
} catch (err) {
|
|
3218
|
-
errors.push(`[generated-index] sync failed for ${indexPath}: ${formatError2(err)}`);
|
|
3219
|
-
}
|
|
3220
|
-
}
|
|
3221
|
-
return { changedFiles, errors };
|
|
3222
|
-
}
|
|
3223
|
-
#facetIndexFor(file) {
|
|
3224
|
-
const abs = path17.resolve(file);
|
|
3225
|
-
const rel = path17.relative(this.#workspaceRoot, abs);
|
|
3226
|
-
if (rel.startsWith("..") || path17.isAbsolute(rel))
|
|
3227
|
-
return null;
|
|
3228
|
-
const parts = rel.split(path17.sep).filter(Boolean);
|
|
3229
|
-
if (parts.length < 4)
|
|
3230
|
-
return null;
|
|
3231
|
-
const [scope, project, facet, child] = parts;
|
|
3232
|
-
if (scope !== "apps" && scope !== "libs" || !project || !facet || !BARREL_FACETS2.has(facet))
|
|
3233
|
-
return null;
|
|
3234
|
-
if (!child || child.startsWith(".") || child === "index.ts" || child === "index.tsx")
|
|
3235
|
-
return null;
|
|
3236
|
-
return path17.join(this.#workspaceRoot, scope, project, facet, "index.ts");
|
|
3237
|
-
}
|
|
3238
|
-
async#moduleIndexForDirectoryEvent(file) {
|
|
3239
|
-
const abs = path17.resolve(file);
|
|
3240
|
-
const rel = path17.relative(this.#workspaceRoot, abs);
|
|
3241
|
-
if (rel.startsWith("..") || path17.isAbsolute(rel))
|
|
3242
|
-
return null;
|
|
3243
|
-
const parts = rel.split(path17.sep).filter(Boolean);
|
|
3244
|
-
if (parts.length !== 4 && parts.length !== 5)
|
|
3245
|
-
return null;
|
|
3246
|
-
const [scope, project, libSegment, moduleSegment, scalarSegment] = parts;
|
|
3247
|
-
if (scope !== "apps" && scope !== "libs" || !project || libSegment !== "lib")
|
|
3248
|
-
return null;
|
|
3249
|
-
const isExistingDirectory = await stat2(abs).then((s) => s.isDirectory()).catch(() => false);
|
|
3250
|
-
const looksLikeDeletedDirectory = !path17.extname(abs);
|
|
3251
|
-
if (!isExistingDirectory && !looksLikeDeletedDirectory)
|
|
3252
|
-
return null;
|
|
3253
|
-
if (moduleSegment === "__scalar" && scalarSegment) {
|
|
3254
|
-
return path17.join(this.#workspaceRoot, scope, project, "lib", "__scalar", scalarSegment, "index.ts");
|
|
3255
|
-
}
|
|
3256
|
-
if (!moduleSegment || moduleSegment === "__scalar")
|
|
3257
|
-
return null;
|
|
3258
|
-
return path17.join(this.#workspaceRoot, scope, project, "lib", moduleSegment, "index.ts");
|
|
3259
|
-
}
|
|
3260
|
-
async#syncIndex(indexPath) {
|
|
3261
|
-
const dir = path17.dirname(indexPath);
|
|
3262
|
-
const content = await this.#contentForIndex(indexPath);
|
|
3263
|
-
if (content === null) {
|
|
3264
|
-
if (!await exists(indexPath))
|
|
3265
|
-
return false;
|
|
3266
|
-
await rm2(indexPath, { force: true });
|
|
3267
|
-
return true;
|
|
3268
|
-
}
|
|
3269
|
-
const current = await readFile2(indexPath, "utf8").catch(() => null);
|
|
3270
|
-
if (current === content)
|
|
3271
|
-
return false;
|
|
3272
|
-
await mkdir4(dir, { recursive: true });
|
|
3273
|
-
await writeFile2(indexPath, content);
|
|
3274
|
-
return true;
|
|
3275
|
-
}
|
|
3276
|
-
async#contentForIndex(indexPath) {
|
|
3277
|
-
const parts = path17.relative(this.#workspaceRoot, indexPath).split(path17.sep).filter(Boolean);
|
|
3278
|
-
const facet = parts.at(-2);
|
|
3279
|
-
if (facet && BARREL_FACETS2.has(facet))
|
|
3280
|
-
return this.#facetContent(path17.dirname(indexPath));
|
|
3281
|
-
return this.#moduleContent(path17.dirname(indexPath));
|
|
3282
|
-
}
|
|
3283
|
-
async#facetContent(dir) {
|
|
3284
|
-
const nameCasePattern = path17.basename(dir) === "ui" ? FACET_PASCAL_CASE_RE : FACET_CAMEL_CASE_RE;
|
|
3285
|
-
const entries = await readdir2(dir, { withFileTypes: true }).catch(() => []);
|
|
3286
|
-
const exportNames = entries.flatMap((entry) => {
|
|
3287
|
-
const name = entry.name;
|
|
3288
|
-
if (name.startsWith("."))
|
|
3289
|
-
return [];
|
|
3290
|
-
if (entry.isDirectory())
|
|
3291
|
-
return nameCasePattern.test(name) ? [name] : [];
|
|
3292
|
-
if (!entry.isFile())
|
|
3293
|
-
return [];
|
|
3294
|
-
if (!FACET_SOURCE_FILE_RE.test(name) || FACET_EXCLUDED_FILE_RE.test(name))
|
|
3295
|
-
return [];
|
|
3296
|
-
const exportName = name.replace(FACET_SOURCE_FILE_RE, "");
|
|
3297
|
-
return nameCasePattern.test(exportName) ? [exportName] : [];
|
|
3298
|
-
}).sort();
|
|
3299
|
-
if (exportNames.length === 0)
|
|
3300
|
-
return null;
|
|
3301
|
-
return `${exportNames.map((name) => `export * from "./${name}";`).join(`
|
|
3302
|
-
`)}
|
|
3303
|
-
`;
|
|
3304
|
-
}
|
|
3305
|
-
async#moduleContent(dir) {
|
|
3306
|
-
const rel = path17.relative(this.#workspaceRoot, dir);
|
|
3307
|
-
const parts = rel.split(path17.sep).filter(Boolean);
|
|
3308
|
-
const moduleSegment = parts.at(-1);
|
|
3309
|
-
if (!moduleSegment)
|
|
3310
|
-
return null;
|
|
3311
|
-
const isScalar = parts.at(-2) === "__scalar";
|
|
3312
|
-
const rawModel = moduleSegment.startsWith("_") ? moduleSegment.slice(1) : moduleSegment;
|
|
3313
|
-
if (!rawModel)
|
|
3314
|
-
return null;
|
|
3315
|
-
const modelName = capitalize(rawModel);
|
|
3316
|
-
const allowedTypes = isScalar ? SCALAR_UI_TYPES : moduleSegment.startsWith("_") ? SERVICE_UI_TYPES : MODULE_UI_TYPES;
|
|
3317
|
-
const fileTypes = [];
|
|
3318
|
-
for (const type of allowedTypes) {
|
|
3319
|
-
if (await exists(path17.join(dir, `${modelName}.${type}.tsx`)))
|
|
3320
|
-
fileTypes.push(type);
|
|
3321
|
-
}
|
|
3322
|
-
if (fileTypes.length === 0)
|
|
3323
|
-
return null;
|
|
3324
|
-
return `
|
|
3325
|
-
${fileTypes.map((type) => `import * as ${type} from "./${modelName}.${type}";`).join(`
|
|
3326
|
-
`)}
|
|
3327
|
-
|
|
3328
|
-
export const ${modelName} = { ${fileTypes.join(", ")} };`;
|
|
3329
|
-
}
|
|
3330
|
-
}
|
|
3331
|
-
var exists = async (file) => stat2(file).then(() => true).catch(() => false);
|
|
3332
|
-
var capitalize = (value) => `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
3333
|
-
var formatError2 = (err) => err instanceof Error ? err.message : String(err);
|
|
3334
|
-
|
|
3335
|
-
// pkgs/@akanjs/devkit/frontendBuild/fontOptimizer.ts
|
|
3336
|
-
import { mkdir as mkdir5, stat as stat3 } from "fs/promises";
|
|
3337
|
-
import path18 from "path";
|
|
3338
|
-
import ts4 from "typescript";
|
|
3339
|
-
var FONT_URL_PREFIX = "/_akan/fonts";
|
|
3340
|
-
var DEFAULT_FONT_SUBSETS = ["latin"];
|
|
3341
|
-
|
|
3342
|
-
class FontOptimizer {
|
|
3343
|
-
#app;
|
|
3344
|
-
#command;
|
|
3345
|
-
#artifactRoot;
|
|
3346
|
-
#files = [];
|
|
3347
|
-
#cssParts = [];
|
|
3348
|
-
#woff2Ready = null;
|
|
3349
|
-
static #ksX1001Text = null;
|
|
3350
|
-
static #cacheVersion = 1;
|
|
3351
|
-
constructor(app, command = "start") {
|
|
3352
|
-
this.#app = app;
|
|
3353
|
-
this.#command = command;
|
|
3354
|
-
this.#artifactRoot = path18.join(command === "build" ? app.dist.cwdPath : app.cwdPath, ".akan/artifact");
|
|
3355
|
-
}
|
|
3356
|
-
async optimize() {
|
|
3357
|
-
const fonts = await this.discoverFonts();
|
|
3358
|
-
const cacheKey = await this.#buildCacheKey(fonts);
|
|
3359
|
-
const cached = cacheKey ? await this.#readCache(cacheKey) : null;
|
|
3360
|
-
if (cached) {
|
|
3361
|
-
this.#app.verbose(`[font] reused ${cached.files.length} cached file(s); skipped subsetting`);
|
|
3362
|
-
return { css: cached.css, fonts, files: cached.files };
|
|
3363
|
-
}
|
|
3364
|
-
for (const font of fonts) {
|
|
3365
|
-
if (!this.#isFontOptimizationEnabled(font))
|
|
3366
|
-
continue;
|
|
3367
|
-
await this.#optimizeFont(font);
|
|
3368
|
-
}
|
|
3369
|
-
const fontUtilityCss = this.#buildFontUtilityRules(fonts);
|
|
3370
|
-
if (fontUtilityCss)
|
|
3371
|
-
this.#cssParts.push(fontUtilityCss);
|
|
3372
|
-
const result = { css: this.#cssParts.join(`
|
|
3373
|
-
`), fonts, files: this.#files };
|
|
3374
|
-
if (cacheKey)
|
|
3375
|
-
await this.#writeCache(cacheKey, result);
|
|
3376
|
-
return result;
|
|
3377
|
-
}
|
|
3378
|
-
get #cachePath() {
|
|
3379
|
-
return path18.join(this.#artifactRoot, "fontCache.json");
|
|
3380
|
-
}
|
|
3381
|
-
async#buildCacheKey(fonts) {
|
|
3382
|
-
const sources = [];
|
|
3383
|
-
for (const font of fonts) {
|
|
3384
|
-
if (!this.#isFontOptimizationEnabled(font))
|
|
3385
|
-
continue;
|
|
3386
|
-
for (const face of this.#getFontFaces(font)) {
|
|
3387
|
-
const sourcePath = await this.#resolveFontSourcePath(face.src);
|
|
3388
|
-
const stamp = sourcePath ? await this.#fileStamp(sourcePath) : null;
|
|
3389
|
-
if (!stamp)
|
|
3390
|
-
return null;
|
|
3391
|
-
sources.push({ optimizedSrc: face.optimizedSrc, ...stamp });
|
|
3392
|
-
}
|
|
3393
|
-
for (const filePath of font.subsetFiles ?? []) {
|
|
3394
|
-
const abs = path18.isAbsolute(filePath) ? filePath : path18.join(this.#app.cwdPath, filePath);
|
|
3395
|
-
const stamp = await this.#fileStamp(abs);
|
|
3396
|
-
if (!stamp)
|
|
3397
|
-
return null;
|
|
3398
|
-
sources.push({ subsetFile: filePath, ...stamp });
|
|
3399
|
-
}
|
|
3400
|
-
if (this.#getFontSubsets(font).includes("auto"))
|
|
3401
|
-
sources.push({ autoSubsetText: this.#hashFontConfig(await this.#collectAutoSubsetText()) });
|
|
3402
|
-
}
|
|
3403
|
-
return this.#hashFontConfig({ version: FontOptimizer.#cacheVersion, fonts, sources });
|
|
3404
|
-
}
|
|
3405
|
-
async#fileStamp(filePath) {
|
|
3406
|
-
try {
|
|
3407
|
-
const stats = await stat3(filePath);
|
|
3408
|
-
return { mtimeMs: Math.round(stats.mtimeMs), size: stats.size };
|
|
3409
|
-
} catch {
|
|
3410
|
-
return null;
|
|
3411
|
-
}
|
|
3412
|
-
}
|
|
3413
|
-
async#readCache(key) {
|
|
3414
|
-
const cache = await Bun.file(this.#cachePath).json().catch(() => null);
|
|
3415
|
-
if (cache?.version !== FontOptimizer.#cacheVersion || cache.key !== key)
|
|
3416
|
-
return null;
|
|
3417
|
-
const files = cache.files.map((relativePath) => path18.join(this.#artifactRoot, relativePath));
|
|
3418
|
-
for (const filePath of files) {
|
|
3419
|
-
if (!await Bun.file(filePath).exists())
|
|
3420
|
-
return null;
|
|
3421
|
-
}
|
|
3422
|
-
return { css: cache.css, files };
|
|
3423
|
-
}
|
|
3424
|
-
async#writeCache(key, result) {
|
|
3425
|
-
const cache = {
|
|
3426
|
-
version: FontOptimizer.#cacheVersion,
|
|
3427
|
-
key,
|
|
3428
|
-
css: result.css,
|
|
3429
|
-
files: result.files.map((filePath) => path18.relative(this.#artifactRoot, filePath))
|
|
3430
|
-
};
|
|
3431
|
-
await Bun.write(this.#cachePath, JSON.stringify(cache));
|
|
3432
|
-
}
|
|
3433
|
-
async discoverFonts() {
|
|
3434
|
-
const pageKeys = await this.#app.getPageKeys();
|
|
3435
|
-
const fonts = [];
|
|
3436
|
-
await Promise.all(pageKeys.map(async (key) => {
|
|
3437
|
-
const filePath = path18.resolve(this.#app.cwdPath, "page", key);
|
|
3438
|
-
const file = Bun.file(filePath);
|
|
3439
|
-
if (!await file.exists())
|
|
3440
|
-
return;
|
|
3441
|
-
const source = await file.text();
|
|
3442
|
-
if (!source.includes("fonts"))
|
|
3443
|
-
return;
|
|
3444
|
-
fonts.push(...this.#extractFontsExport(source, filePath));
|
|
3445
|
-
}));
|
|
3446
|
-
return this.#dedupeFonts(fonts);
|
|
3447
|
-
}
|
|
3448
|
-
async#optimizeFont(font) {
|
|
3449
|
-
const faceCss = [];
|
|
3450
|
-
for (const face of this.#getFontFaces(font)) {
|
|
3451
|
-
const sourcePath = await this.#resolveFontSourcePath(face.src);
|
|
3452
|
-
if (!sourcePath) {
|
|
3453
|
-
this.#app.logger.warn(`[font] source not found: ${face.src}`);
|
|
3454
|
-
continue;
|
|
3455
|
-
}
|
|
3456
|
-
const outputPath = path18.join(this.#artifactRoot, face.optimizedSrc.replace(/^\/_akan\//, ""));
|
|
3457
|
-
await mkdir5(path18.dirname(outputPath), { recursive: true });
|
|
3458
|
-
const sourceBuffer = Buffer.from(await Bun.file(sourcePath).arrayBuffer());
|
|
3459
|
-
const outputBuffer = await this.#buildFontBuffer(font, sourceBuffer, sourcePath);
|
|
3460
|
-
await Bun.write(outputPath, outputBuffer);
|
|
3461
|
-
this.#files.push(outputPath);
|
|
3462
|
-
faceCss.push(this.#buildOptimizedFontFaceRule(font, face));
|
|
3463
|
-
const fallbackCss = await this.#buildFontaineFallbackCss(font, face, outputPath);
|
|
3464
|
-
if (fallbackCss)
|
|
3465
|
-
faceCss.push(fallbackCss);
|
|
3466
|
-
}
|
|
3467
|
-
if (faceCss.length > 0)
|
|
3468
|
-
this.#cssParts.push(...faceCss, this.#buildRootVariableRule(font));
|
|
3469
|
-
}
|
|
3470
|
-
#extractFontsExport(source, filePath) {
|
|
3471
|
-
const sourceFile = ts4.createSourceFile(filePath, source, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TSX);
|
|
3472
|
-
const fonts = [];
|
|
3473
|
-
for (const statement of sourceFile.statements) {
|
|
3474
|
-
if (!ts4.isVariableStatement(statement))
|
|
3475
|
-
continue;
|
|
3476
|
-
const modifiers = ts4.canHaveModifiers(statement) ? ts4.getModifiers(statement) : undefined;
|
|
3477
|
-
const isExported = modifiers?.some((modifier) => modifier.kind === ts4.SyntaxKind.ExportKeyword) ?? false;
|
|
3478
|
-
if (!isExported)
|
|
3479
|
-
continue;
|
|
3480
|
-
for (const declaration of statement.declarationList.declarations) {
|
|
3481
|
-
if (!ts4.isIdentifier(declaration.name) || declaration.name.text !== "fonts")
|
|
3482
|
-
continue;
|
|
3483
|
-
const value = declaration.initializer ? this.#literalToValue(declaration.initializer) : null;
|
|
3484
|
-
if (Array.isArray(value)) {
|
|
3485
|
-
fonts.push(...value.map((font) => this.#withFontDefaults(font)));
|
|
3486
|
-
}
|
|
3487
|
-
}
|
|
3488
|
-
}
|
|
3489
|
-
return fonts;
|
|
3490
|
-
}
|
|
3491
|
-
#literalToValue(node) {
|
|
3492
|
-
if (ts4.isStringLiteralLike(node))
|
|
3493
|
-
return node.text;
|
|
3494
|
-
if (ts4.isNumericLiteral(node))
|
|
3495
|
-
return Number(node.text);
|
|
3496
|
-
if (node.kind === ts4.SyntaxKind.TrueKeyword)
|
|
3497
|
-
return true;
|
|
3498
|
-
if (node.kind === ts4.SyntaxKind.FalseKeyword)
|
|
3499
|
-
return false;
|
|
3500
|
-
if (ts4.isArrayLiteralExpression(node))
|
|
3501
|
-
return node.elements.map((element) => this.#literalToValue(element));
|
|
3502
|
-
if (ts4.isObjectLiteralExpression(node)) {
|
|
3503
|
-
const obj = {};
|
|
3504
|
-
for (const prop of node.properties) {
|
|
3505
|
-
if (!ts4.isPropertyAssignment(prop))
|
|
3506
|
-
continue;
|
|
3507
|
-
const name = this.#getPropertyName(prop.name);
|
|
3508
|
-
if (!name)
|
|
3509
|
-
continue;
|
|
3510
|
-
obj[name] = this.#literalToValue(prop.initializer);
|
|
3511
|
-
}
|
|
3512
|
-
return obj;
|
|
3513
|
-
}
|
|
3514
|
-
if (ts4.isAsExpression(node) || ts4.isSatisfiesExpression(node) || ts4.isParenthesizedExpression(node)) {
|
|
3515
|
-
return this.#literalToValue(node.expression);
|
|
3516
|
-
}
|
|
3517
|
-
return;
|
|
3518
|
-
}
|
|
3519
|
-
#getPropertyName(name) {
|
|
3520
|
-
if (ts4.isIdentifier(name) || ts4.isStringLiteral(name) || ts4.isNumericLiteral(name))
|
|
3521
|
-
return name.text;
|
|
3522
|
-
return null;
|
|
3523
|
-
}
|
|
3524
|
-
#dedupeFonts(fonts) {
|
|
3525
|
-
const map = new Map;
|
|
3526
|
-
for (const font of fonts)
|
|
3527
|
-
map.set(JSON.stringify(font), font);
|
|
3528
|
-
return [...map.values()];
|
|
3529
|
-
}
|
|
3530
|
-
#withFontDefaults(font) {
|
|
3531
|
-
return { ...font, subsets: font.subsets ?? [...DEFAULT_FONT_SUBSETS] };
|
|
3532
|
-
}
|
|
3533
|
-
#getFontSubsets(font) {
|
|
3534
|
-
return font.subsets ?? DEFAULT_FONT_SUBSETS;
|
|
3535
|
-
}
|
|
3536
|
-
#getFontVariableName(font) {
|
|
3537
|
-
return font.variable ?? `--font-${font.name}`;
|
|
3538
|
-
}
|
|
3539
|
-
#getFontFallbackName(font) {
|
|
3540
|
-
return font.fallbackName ?? `${font.name} fallback`;
|
|
3541
|
-
}
|
|
3542
|
-
#isFontOptimizationEnabled(font) {
|
|
3543
|
-
return font.optimize !== false;
|
|
3544
|
-
}
|
|
3545
|
-
#getFontStyles(font) {
|
|
3546
|
-
return font.styles?.length ? font.styles : ["normal"];
|
|
3547
|
-
}
|
|
3548
|
-
#getFontFaces(font) {
|
|
3549
|
-
const enabledStyles = new Set(this.#getFontStyles(font));
|
|
3550
|
-
return font.paths.map((fontPath) => {
|
|
3551
|
-
const style = fontPath.style ?? "normal";
|
|
3552
|
-
return {
|
|
3553
|
-
font,
|
|
3554
|
-
path: fontPath,
|
|
3555
|
-
src: fontPath.src,
|
|
3556
|
-
weight: fontPath.weight,
|
|
3557
|
-
style,
|
|
3558
|
-
optimizedSrc: this.#getOptimizedFontSrc(font, fontPath)
|
|
3559
|
-
};
|
|
3560
|
-
}).filter((face) => enabledStyles.has(face.style));
|
|
3561
|
-
}
|
|
3562
|
-
#getOptimizedFontSrc(font, fontPath) {
|
|
3563
|
-
const style = fontPath.style ?? "normal";
|
|
3564
|
-
const hash = this.#hashFontConfig({
|
|
3565
|
-
name: font.name,
|
|
3566
|
-
src: fontPath.src,
|
|
3567
|
-
weight: fontPath.weight,
|
|
3568
|
-
style,
|
|
3569
|
-
display: font.display,
|
|
3570
|
-
subset: font.subset,
|
|
3571
|
-
subsets: this.#getFontSubsets(font),
|
|
3572
|
-
subsetText: font.subsetText,
|
|
3573
|
-
subsetFiles: font.subsetFiles,
|
|
3574
|
-
declarations: [...font.declarations ?? [], ...fontPath.declarations ?? []]
|
|
3575
|
-
});
|
|
3576
|
-
return `${FONT_URL_PREFIX}/${this.#slugFontPart(font.name)}-${this.#slugFontPart(String(fontPath.weight))}-${style}-${hash}.woff2`;
|
|
3577
|
-
}
|
|
3578
|
-
#hashFontConfig(value) {
|
|
3579
|
-
const input = this.#stableStringify(value);
|
|
3580
|
-
let hash = 2166136261;
|
|
3581
|
-
for (let i = 0;i < input.length; i++) {
|
|
3582
|
-
hash ^= input.charCodeAt(i);
|
|
3583
|
-
hash = Math.imul(hash, 16777619);
|
|
3584
|
-
}
|
|
3585
|
-
return (hash >>> 0).toString(36);
|
|
3586
|
-
}
|
|
3587
|
-
#stableStringify(value) {
|
|
3588
|
-
if (Array.isArray(value))
|
|
3589
|
-
return `[${value.map((item) => this.#stableStringify(item)).join(",")}]`;
|
|
3590
|
-
if (value && typeof value === "object") {
|
|
3591
|
-
const entries = Object.entries(value).filter(([, v]) => v !== undefined).sort(([a], [b]) => a.localeCompare(b));
|
|
3592
|
-
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${this.#stableStringify(v)}`).join(",")}}`;
|
|
3593
|
-
}
|
|
3594
|
-
return JSON.stringify(value);
|
|
3595
|
-
}
|
|
3596
|
-
#slugFontPart(value) {
|
|
3597
|
-
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "font";
|
|
3598
|
-
}
|
|
3599
|
-
async#resolveFontSourcePath(src) {
|
|
3600
|
-
if (!src.startsWith("/"))
|
|
3601
|
-
return null;
|
|
3602
|
-
const rel = src.replace(/^\//, "");
|
|
3603
|
-
const candidates = [
|
|
3604
|
-
this.#command === "build" ? path18.join(this.#app.dist.cwdPath, "public", rel) : null,
|
|
3605
|
-
path18.join(this.#app.cwdPath, "public", rel),
|
|
3606
|
-
this.#resolveWorkspacePublicPath(rel)
|
|
3607
|
-
].filter(Boolean);
|
|
3608
|
-
for (const candidate of candidates) {
|
|
3609
|
-
if (await Bun.file(candidate).exists())
|
|
3610
|
-
return candidate;
|
|
3611
|
-
}
|
|
3612
|
-
return null;
|
|
3613
|
-
}
|
|
3614
|
-
async#buildFontBuffer(font, sourceBuffer, sourcePath) {
|
|
3615
|
-
if (font.subset === false)
|
|
3616
|
-
return this.#convertToWoff2(sourceBuffer, sourcePath);
|
|
3617
|
-
const { default: subsetFont } = await import("subset-font");
|
|
3618
|
-
return subsetFont(sourceBuffer, await this.#getSubsetText(font), { targetFormat: "woff2" });
|
|
3619
|
-
}
|
|
3620
|
-
async#convertToWoff2(buffer, sourcePath) {
|
|
3621
|
-
const { createFont } = await import("fonteditor-core");
|
|
3622
|
-
await this.#initWoff2();
|
|
3623
|
-
const font = createFont(buffer, { type: this.#getFontType(sourcePath, buffer) });
|
|
3624
|
-
return font.write({ type: "woff2", toBuffer: true });
|
|
3625
|
-
}
|
|
3626
|
-
async#initWoff2() {
|
|
3627
|
-
this.#woff2Ready ??= import("fonteditor-core").then(({ woff2 }) => woff2.init()).then(() => {
|
|
3628
|
-
return;
|
|
3629
|
-
});
|
|
3630
|
-
return this.#woff2Ready;
|
|
3631
|
-
}
|
|
3632
|
-
#getFontType(sourcePath, buffer) {
|
|
3633
|
-
const signature = buffer.toString("ascii", 0, 4);
|
|
3634
|
-
if (signature === "wOFF")
|
|
3635
|
-
return "woff";
|
|
3636
|
-
if (signature === "wOF2")
|
|
3637
|
-
return "woff2";
|
|
3638
|
-
if (signature === "OTTO")
|
|
3639
|
-
return "otf";
|
|
3640
|
-
const ext = path18.extname(sourcePath).slice(1).toLowerCase();
|
|
3641
|
-
if (ext === "otf" || ext === "woff" || ext === "woff2")
|
|
3642
|
-
return ext;
|
|
3643
|
-
return "ttf";
|
|
3644
|
-
}
|
|
3645
|
-
#resolveWorkspacePublicPath(rel) {
|
|
3646
|
-
const [root, dep, ...rest] = rel.split("/");
|
|
3647
|
-
if (root !== "libs" || !dep || rest.length === 0)
|
|
3648
|
-
return null;
|
|
3649
|
-
return path18.join(this.#app.workspace.workspaceRoot, "libs", dep, "public", ...rest);
|
|
3650
|
-
}
|
|
3651
|
-
async#getSubsetText(font) {
|
|
3652
|
-
const parts = new Set;
|
|
3653
|
-
const subsets = this.#getFontSubsets(font);
|
|
3654
|
-
for (const subset of subsets)
|
|
3655
|
-
parts.add(await this.#getSubsetPresetText(subset));
|
|
3656
|
-
if (font.subsetText)
|
|
3657
|
-
parts.add(font.subsetText);
|
|
3658
|
-
for (const filePath of font.subsetFiles ?? []) {
|
|
3659
|
-
const abs = path18.isAbsolute(filePath) ? filePath : path18.join(this.#app.cwdPath, filePath);
|
|
3660
|
-
const file = Bun.file(abs);
|
|
3661
|
-
if (await file.exists())
|
|
3662
|
-
parts.add(await file.text());
|
|
3663
|
-
}
|
|
3664
|
-
return [...parts].join("");
|
|
3665
|
-
}
|
|
3666
|
-
async#getSubsetPresetText(subset) {
|
|
3667
|
-
if (subset === "latin")
|
|
3668
|
-
return this.#rangeText(32, 126);
|
|
3669
|
-
if (subset === "latin-ext")
|
|
3670
|
-
return `${this.#rangeText(32, 126)}${this.#rangeText(160, 591)}`;
|
|
3671
|
-
if (subset === "ks-x-1001")
|
|
3672
|
-
return FontOptimizer.#getKsX1001Text();
|
|
3673
|
-
if (subset === "auto")
|
|
3674
|
-
return this.#collectAutoSubsetText();
|
|
3675
|
-
return "";
|
|
3676
|
-
}
|
|
3677
|
-
async#collectAutoSubsetText() {
|
|
3678
|
-
const roots = ["page", "ui"].map((dir) => path18.join(this.#app.cwdPath, dir));
|
|
3679
|
-
const glob = new Bun.Glob("**/*.{ts,tsx,js,jsx,html,md}");
|
|
3680
|
-
const parts = [];
|
|
3681
|
-
await Promise.all(roots.map(async (root) => {
|
|
3682
|
-
if (!await Bun.file(root).exists())
|
|
3683
|
-
return;
|
|
3684
|
-
for await (const filePath of glob.scan({ cwd: root, absolute: true })) {
|
|
3685
|
-
parts.push(await Bun.file(filePath).text());
|
|
3686
|
-
}
|
|
3687
|
-
}));
|
|
3688
|
-
return parts.join("");
|
|
3689
|
-
}
|
|
3690
|
-
#rangeText(start, end) {
|
|
3691
|
-
let text = "";
|
|
3692
|
-
for (let code = start;code <= end; code++)
|
|
3693
|
-
text += String.fromCodePoint(code);
|
|
3694
|
-
return text;
|
|
3695
|
-
}
|
|
3696
|
-
static #getKsX1001Text() {
|
|
3697
|
-
if (FontOptimizer.#ksX1001Text)
|
|
3698
|
-
return FontOptimizer.#ksX1001Text;
|
|
3699
|
-
try {
|
|
3700
|
-
const decoder = new TextDecoder("euc-kr");
|
|
3701
|
-
const chars = new Set;
|
|
3702
|
-
for (let lead = 161;lead <= 254; lead++) {
|
|
3703
|
-
for (let trail = 161;trail <= 254; trail++) {
|
|
3704
|
-
const char = decoder.decode(Uint8Array.of(lead, trail));
|
|
3705
|
-
if (char && char !== "\uFFFD")
|
|
3706
|
-
chars.add(char);
|
|
3707
|
-
}
|
|
3708
|
-
}
|
|
3709
|
-
FontOptimizer.#ksX1001Text = [...chars].join("");
|
|
3710
|
-
} catch {
|
|
3711
|
-
FontOptimizer.#ksX1001Text = FontOptimizer.#rangeTextStatic(44032, 55203);
|
|
3712
|
-
}
|
|
3713
|
-
return FontOptimizer.#ksX1001Text;
|
|
3714
|
-
}
|
|
3715
|
-
static #rangeTextStatic(start, end) {
|
|
3716
|
-
let text = "";
|
|
3717
|
-
for (let code = start;code <= end; code++)
|
|
3718
|
-
text += String.fromCodePoint(code);
|
|
3719
|
-
return text;
|
|
3720
|
-
}
|
|
3721
|
-
#buildOptimizedFontFaceRule(font, face) {
|
|
3722
|
-
const declarations = [
|
|
3723
|
-
["font-family", this.#quote(font.name)],
|
|
3724
|
-
["src", `url(${this.#quote(face.optimizedSrc)}) format("woff2")`],
|
|
3725
|
-
["font-weight", String(face.weight)],
|
|
3726
|
-
["font-style", face.style],
|
|
3727
|
-
["font-display", font.display ?? "swap"],
|
|
3728
|
-
...this.#toDeclarationEntries(font.declarations),
|
|
3729
|
-
...this.#toDeclarationEntries(face.path.declarations)
|
|
3730
|
-
];
|
|
3731
|
-
return `@font-face {
|
|
3732
|
-
${declarations.map(([prop, value]) => ` ${prop}: ${value};`).join(`
|
|
3733
|
-
`)}
|
|
3734
|
-
}`;
|
|
3735
|
-
}
|
|
3736
|
-
async#buildFontaineFallbackCss(font, face, outputPath) {
|
|
3737
|
-
if (font.adjustFontFallback === false)
|
|
3738
|
-
return "";
|
|
3739
|
-
const { generateFontFace, getMetricsForFamily, readMetrics, resolveCategoryFallbacks } = await import("fontaine");
|
|
3740
|
-
const metrics = await readMetrics(outputPath).catch(() => null);
|
|
3741
|
-
if (!metrics)
|
|
3742
|
-
return "";
|
|
3743
|
-
const fallbacks = resolveCategoryFallbacks({
|
|
3744
|
-
fontFamily: font.name,
|
|
3745
|
-
fallbacks: font.fallbacks ?? {},
|
|
3746
|
-
metrics: { ...metrics, category: font.category }
|
|
3747
|
-
});
|
|
3748
|
-
const css = [];
|
|
3749
|
-
for (let i = fallbacks.length - 1;i >= 0; i--) {
|
|
3750
|
-
const fallback = fallbacks[i];
|
|
3751
|
-
const fallbackMetrics = await getMetricsForFamily(fallback);
|
|
3752
|
-
if (!fallbackMetrics)
|
|
3753
|
-
continue;
|
|
3754
|
-
css.push(generateFontFace({ ...metrics, category: font.category }, {
|
|
3755
|
-
name: this.#getFontFallbackName(font),
|
|
3756
|
-
font: fallback,
|
|
3757
|
-
metrics: fallbackMetrics,
|
|
3758
|
-
"font-weight": String(face.weight),
|
|
3759
|
-
"font-style": face.style
|
|
3760
|
-
}));
|
|
3761
|
-
}
|
|
3762
|
-
return css.join("");
|
|
3763
|
-
}
|
|
3764
|
-
#buildRootVariableRule(font) {
|
|
3765
|
-
return `:root { ${this.#getFontVariableName(font)}: ${this.#quote(font.name)}, ${this.#quote(this.#getFontFallbackName(font))}; }`;
|
|
3766
|
-
}
|
|
3767
|
-
#buildFontUtilityRules(fonts) {
|
|
3768
|
-
const rules = [];
|
|
3769
|
-
const seen = new Set;
|
|
3770
|
-
for (const font of fonts) {
|
|
3771
|
-
const className = font.className || `font-${font.name}`;
|
|
3772
|
-
const selector = `.${this.#escapeCssClassName(className)}`;
|
|
3773
|
-
const rule = `${selector} { font-family: var(${this.#getFontVariableName(font)}); }`;
|
|
3774
|
-
if (seen.has(rule))
|
|
3775
|
-
continue;
|
|
3776
|
-
seen.add(rule);
|
|
3777
|
-
rules.push(rule);
|
|
3778
|
-
}
|
|
3779
|
-
return rules.join(`
|
|
3780
|
-
`);
|
|
3781
|
-
}
|
|
3782
|
-
#escapeCssClassName(value) {
|
|
3783
|
-
return value.replace(/^-?\d|[^a-zA-Z0-9_-]/g, (part) => [...part].map((char) => `\\${char.codePointAt(0)?.toString(16)} `).join(""));
|
|
3784
|
-
}
|
|
3785
|
-
#toDeclarationEntries(declarations = []) {
|
|
3786
|
-
return declarations.map((declaration) => [declaration.prop, declaration.value]);
|
|
3787
|
-
}
|
|
3788
|
-
#quote(value) {
|
|
3789
|
-
return JSON.stringify(value);
|
|
3790
|
-
}
|
|
3791
|
-
}
|
|
3792
|
-
|
|
3793
|
-
// pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
|
|
3794
|
-
import path20 from "path";
|
|
3795
|
-
|
|
3796
|
-
// pkgs/@akanjs/devkit/transforms/externalizeFrameworkPlugin.ts
|
|
3797
|
-
import path19 from "path";
|
|
3798
|
-
var DEFAULT_INCLUDE = ["akanjs/", "@apps/", "@libs/"];
|
|
3799
|
-
var DEFAULT_EXCLUDE_EXACT = new Set(["akanjs/webkit", "@akanjs/cli", "@akanjs/devkit"]);
|
|
3800
|
-
var DEFAULT_EXCLUDE_PREFIX = ["@akanjs/cli/", "@akanjs/devkit/"];
|
|
3801
|
-
var OPTIONAL_BACKEND_EXTERNAL_EXACT = new Set([
|
|
3802
|
-
"@libsql/client",
|
|
3803
|
-
"bullmq",
|
|
3804
|
-
"croner",
|
|
3805
|
-
"ioredis",
|
|
3806
|
-
"postgres",
|
|
3807
|
-
"protobufjs"
|
|
3808
|
-
]);
|
|
3809
|
-
var RUNTIME_EXTERNAL_EXACT = new Set([
|
|
3810
|
-
"react",
|
|
3811
|
-
"react-dom",
|
|
3812
|
-
"react/jsx-runtime",
|
|
3813
|
-
"react/jsx-dev-runtime",
|
|
3814
|
-
"react-server-dom-webpack",
|
|
3815
|
-
"react-server-dom-webpack/server.node",
|
|
3816
|
-
"react-server-dom-webpack/client.node",
|
|
3817
|
-
"react-server-dom-webpack/client.browser"
|
|
3818
|
-
]);
|
|
3819
|
-
var RUNTIME_EXTERNAL_PREFIX = ["react-dom/", "react-server-dom-webpack/"];
|
|
3820
|
-
var CANDIDATE_EXTS3 = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
|
|
3821
|
-
async function createExternalizeFrameworkPlugin(options) {
|
|
3822
|
-
const tsconfig = await options.app.getTsConfig();
|
|
3823
|
-
const includePrefixes = options.include ?? DEFAULT_INCLUDE;
|
|
3824
|
-
const extraExact = new Set(options.extra ?? []);
|
|
3825
|
-
const workspaceRoot = options.app.workspace.workspaceRoot;
|
|
3826
|
-
const tsconfigPaths = tsconfig.compilerOptions.paths ?? {};
|
|
3827
|
-
const rootEntries = Object.entries(tsconfigPaths).filter(([k]) => !k.endsWith("/*")).map(([k, v]) => ({ pkg: k, entryFile: v[0] ?? null })).filter((e) => e.entryFile !== null);
|
|
3828
|
-
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);
|
|
3829
|
-
async function resolveWorkspaceSubpath(spec) {
|
|
3830
|
-
if (!workspaceRoot)
|
|
3831
|
-
return null;
|
|
3832
|
-
for (const { prefix, replacements } of wildcardEntries) {
|
|
3833
|
-
if (!spec.startsWith(prefix))
|
|
3834
|
-
continue;
|
|
3835
|
-
const suffix = spec.slice(prefix.length);
|
|
3836
|
-
for (const repl of replacements) {
|
|
3837
|
-
const replPath = repl?.endsWith("/*") ? repl.slice(0, -1) : repl ?? "";
|
|
3838
|
-
if (!replPath)
|
|
3839
|
-
continue;
|
|
3840
|
-
const candidate = path19.resolve(workspaceRoot, replPath + suffix);
|
|
3841
|
-
const hit = await firstExisting(candidate);
|
|
3842
|
-
if (hit)
|
|
3843
|
-
return hit;
|
|
3844
|
-
}
|
|
3845
|
-
}
|
|
3846
|
-
for (const { pkg, entryFile } of rootEntries) {
|
|
3847
|
-
if (spec !== pkg && !spec.startsWith(`${pkg}/`))
|
|
3848
|
-
continue;
|
|
3849
|
-
if (spec === pkg)
|
|
3850
|
-
continue;
|
|
3851
|
-
const suffix = spec.slice(pkg.length + 1);
|
|
3852
|
-
const pkgDir = path19.dirname(path19.resolve(workspaceRoot, entryFile));
|
|
3853
|
-
const candidate = path19.join(pkgDir, suffix);
|
|
3854
|
-
const hit = await firstExisting(candidate);
|
|
3855
|
-
if (hit)
|
|
3856
|
-
return hit;
|
|
3857
|
-
}
|
|
3858
|
-
return null;
|
|
3859
|
-
}
|
|
3860
|
-
return {
|
|
3861
|
-
name: "akan-externalize-framework",
|
|
3862
|
-
setup(build) {
|
|
3863
|
-
build.onResolve({ filter: /.*/ }, async (args) => {
|
|
3864
|
-
const spec = args.path;
|
|
3865
|
-
if (spec === "." || spec === ".." || spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/"))
|
|
3866
|
-
return;
|
|
3867
|
-
if (extraExact.has(spec) || DEFAULT_EXCLUDE_EXACT.has(spec))
|
|
3868
|
-
return { path: spec, external: true };
|
|
3869
|
-
for (const prefix of DEFAULT_EXCLUDE_PREFIX) {
|
|
3870
|
-
if (spec.startsWith(prefix))
|
|
3871
|
-
return { path: spec, external: true };
|
|
3872
|
-
}
|
|
3873
|
-
if (OPTIONAL_BACKEND_EXTERNAL_EXACT.has(spec))
|
|
3874
|
-
return { path: spec, external: true };
|
|
3875
|
-
if (RUNTIME_EXTERNAL_EXACT.has(spec))
|
|
3876
|
-
return { path: spec, external: true };
|
|
3877
|
-
for (const prefix of RUNTIME_EXTERNAL_PREFIX) {
|
|
3878
|
-
if (spec.startsWith(prefix))
|
|
3879
|
-
return { path: spec, external: true };
|
|
3880
|
-
}
|
|
3881
|
-
for (const prefix of includePrefixes) {
|
|
3882
|
-
if (!spec.startsWith(prefix))
|
|
3883
|
-
continue;
|
|
3884
|
-
const resolved = await resolveWorkspaceSubpath(spec);
|
|
3885
|
-
if (resolved)
|
|
3886
|
-
return { path: resolved };
|
|
3887
|
-
return;
|
|
3888
|
-
}
|
|
3889
|
-
return;
|
|
3890
|
-
});
|
|
3891
|
-
}
|
|
3892
|
-
};
|
|
3893
|
-
}
|
|
3894
|
-
async function firstExisting(basePath) {
|
|
3895
|
-
if (await Bun.file(basePath).exists())
|
|
3896
|
-
return basePath;
|
|
3897
|
-
for (const ext of CANDIDATE_EXTS3) {
|
|
3898
|
-
const candidate = `${basePath}${ext}`;
|
|
3899
|
-
if (await Bun.file(candidate).exists())
|
|
3900
|
-
return candidate;
|
|
3901
|
-
}
|
|
3902
|
-
for (const ext of CANDIDATE_EXTS3) {
|
|
3903
|
-
const candidate = path19.join(basePath, `index${ext}`);
|
|
3904
|
-
if (await Bun.file(candidate).exists())
|
|
3905
|
-
return candidate;
|
|
3906
|
-
}
|
|
3907
|
-
return null;
|
|
3908
|
-
}
|
|
3909
|
-
|
|
3910
|
-
// pkgs/@akanjs/devkit/transforms/useClientBundlePlugin.ts
|
|
3911
|
-
function createUseClientBundlePlugin(options = {}) {
|
|
3912
|
-
return {
|
|
3913
|
-
name: "akan-use-client-bundle",
|
|
3914
|
-
setup(build) {
|
|
3915
|
-
build.onLoad({ filter: /\.(tsx|ts|jsx|js)$/ }, async (args) => {
|
|
3916
|
-
if (args.path.includes("/node_modules/"))
|
|
3917
|
-
return;
|
|
3918
|
-
let source;
|
|
3919
|
-
try {
|
|
3920
|
-
source = await Bun.file(args.path).text();
|
|
3921
|
-
} catch {
|
|
3922
|
-
return;
|
|
3923
|
-
}
|
|
3924
|
-
const stubbed = transformUseClient(source, {
|
|
3925
|
-
path: args.path,
|
|
3926
|
-
workspaceRoot: options.workspaceRoot
|
|
3927
|
-
});
|
|
3928
|
-
if (stubbed === null)
|
|
3929
|
-
return;
|
|
3930
|
-
return { contents: stubbed, loader: loaderFor3(args.path) };
|
|
3931
|
-
});
|
|
3932
|
-
}
|
|
3933
|
-
};
|
|
3934
|
-
}
|
|
3935
|
-
function loaderFor3(absPath) {
|
|
3936
|
-
if (absPath.endsWith(".tsx"))
|
|
3937
|
-
return "tsx";
|
|
3938
|
-
if (absPath.endsWith(".jsx"))
|
|
3939
|
-
return "jsx";
|
|
3940
|
-
if (absPath.endsWith(".ts"))
|
|
3941
|
-
return "ts";
|
|
3942
|
-
return "js";
|
|
3943
|
-
}
|
|
3944
|
-
|
|
3945
|
-
// pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
|
|
3946
|
-
var VIRTUAL_PAGES_ENTRY = "akan-pages-entry";
|
|
3947
|
-
|
|
3948
|
-
class PagesBundleBuilder {
|
|
3949
|
-
#app;
|
|
3950
|
-
#command;
|
|
3951
|
-
#pageEntries;
|
|
3952
|
-
#started = Date.now();
|
|
3953
|
-
constructor(app, command = "start", pageEntries) {
|
|
3954
|
-
this.#app = app;
|
|
3955
|
-
this.#command = command;
|
|
3956
|
-
this.#pageEntries = pageEntries;
|
|
3957
|
-
}
|
|
3958
|
-
async build() {
|
|
3959
|
-
const akanConfig = await this.#app.getConfig();
|
|
3960
|
-
const resolvedEntries = this.#pageEntries ?? await resolveSsrPageEntriesForApp(this.#app, await this.#app.getPageKeys());
|
|
3961
|
-
const entrySource = PagesEntrySourceGenerator.generate(resolvedEntries);
|
|
3962
|
-
const workspaceRoot = this.#app.workspace.workspaceRoot;
|
|
3963
|
-
const result = await Bun.build({
|
|
3964
|
-
entrypoints: [VIRTUAL_PAGES_ENTRY],
|
|
3965
|
-
outdir: `${this.#artifactDir}/server`,
|
|
3966
|
-
target: "bun",
|
|
3967
|
-
format: "esm",
|
|
3968
|
-
splitting: this.#splitting,
|
|
3969
|
-
minify: this.#command === "build",
|
|
3970
|
-
naming: {
|
|
3971
|
-
entry: "pages-[hash].[ext]",
|
|
3972
|
-
chunk: "chunks/[name]-[hash].[ext]",
|
|
3973
|
-
asset: "assets/[name]-[hash].[ext]"
|
|
3974
|
-
},
|
|
3975
|
-
define: this.#define(),
|
|
3976
|
-
plugins: [
|
|
3977
|
-
PagesBundleBuilder.createPagesEntryPlugin(entrySource),
|
|
3978
|
-
PagesBundleBuilder.createServerCssStubPlugin(),
|
|
3979
|
-
PagesBundleBuilder.createServerUseClientFetchPlugin(),
|
|
3980
|
-
await createExternalizeFrameworkPlugin({ app: this.#app, extra: akanConfig.externalLibs }),
|
|
3981
|
-
akanConfig.barrelImports.length > 0 ? await createBarrelImportsPlugin(this.#app, {
|
|
3982
|
-
pipeAfter: (source, args) => transformUseClient(source, {
|
|
3983
|
-
path: args.path,
|
|
3984
|
-
workspaceRoot
|
|
3985
|
-
})
|
|
3986
|
-
}) : createUseClientBundlePlugin({ workspaceRoot })
|
|
3987
|
-
]
|
|
3988
|
-
});
|
|
3989
|
-
if (!result.success)
|
|
3990
|
-
throw new AggregateError(result.logs, "[PagesBundleBuilder] Bun.build failed");
|
|
3991
|
-
const entryArtifact = result.outputs.find((a) => a.kind === "entry-point");
|
|
3992
|
-
if (!entryArtifact)
|
|
3993
|
-
throw new Error("[PagesBundleBuilder] Bun.build emitted no entry-point artifact");
|
|
3994
|
-
const bundlePath = path20.resolve(entryArtifact.path);
|
|
3995
|
-
const buildId = Date.now();
|
|
3996
|
-
const outputBytes = result.outputs.reduce((sum, output) => sum + output.size, 0);
|
|
3997
|
-
const chunkCount = result.outputs.filter((output) => output.kind === "chunk").length;
|
|
3998
|
-
this.#app.verbose(`[PagesBundleBuilder] ${path20.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`);
|
|
3999
|
-
return {
|
|
4000
|
-
bundlePath,
|
|
4001
|
-
buildId,
|
|
4002
|
-
splitting: this.#splitting,
|
|
4003
|
-
entryBytes: entryArtifact.size,
|
|
4004
|
-
outputBytes,
|
|
4005
|
-
outputCount: result.outputs.length,
|
|
4006
|
-
chunkCount
|
|
4007
|
-
};
|
|
4008
|
-
}
|
|
4009
|
-
get #artifactDir() {
|
|
4010
|
-
return `${this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath}/.akan/artifact`;
|
|
4011
|
-
}
|
|
4012
|
-
get #splitting() {
|
|
4013
|
-
return process.env.AKAN_SERVER_PAGES_SPLITTING === "1";
|
|
4014
|
-
}
|
|
4015
|
-
#define() {
|
|
4016
|
-
const nodeEnv = this.#command === "build" ? "production" : "development";
|
|
4017
|
-
return {
|
|
4018
|
-
"process.env.NODE_ENV": JSON.stringify(nodeEnv),
|
|
4019
|
-
"process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("ssr"),
|
|
4020
|
-
...Object.fromEntries(Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]))
|
|
4021
|
-
};
|
|
4022
|
-
}
|
|
4023
|
-
static createPagesEntryPlugin(source) {
|
|
4024
|
-
return {
|
|
4025
|
-
name: "akan-pages-entry",
|
|
4026
|
-
setup(build) {
|
|
4027
|
-
build.onResolve({ filter: /^akan-pages-entry$/ }, () => ({
|
|
4028
|
-
path: VIRTUAL_PAGES_ENTRY,
|
|
4029
|
-
namespace: "akan-virtual"
|
|
4030
|
-
}));
|
|
4031
|
-
build.onLoad({ filter: /^akan-pages-entry$/, namespace: "akan-virtual" }, () => ({
|
|
4032
|
-
contents: source,
|
|
4033
|
-
loader: "tsx"
|
|
4034
|
-
}));
|
|
4035
|
-
}
|
|
4036
|
-
};
|
|
4037
|
-
}
|
|
4038
|
-
static createServerCssStubPlugin() {
|
|
4039
|
-
return {
|
|
4040
|
-
name: "akan-server-css-stub",
|
|
4041
|
-
setup(build) {
|
|
4042
|
-
build.onLoad({ filter: /\.css$/ }, () => ({
|
|
4043
|
-
contents: "",
|
|
4044
|
-
loader: "js"
|
|
4045
|
-
}));
|
|
4046
|
-
}
|
|
4047
|
-
};
|
|
4048
|
-
}
|
|
4049
|
-
static createServerUseClientFetchPlugin() {
|
|
4050
|
-
return {
|
|
4051
|
-
name: "akan-server-use-client-fetch",
|
|
4052
|
-
setup(build) {
|
|
4053
|
-
build.onLoad({ filter: /[/\\]lib[/\\]useClient\.(ts|tsx|js|jsx)$/ }, async (args) => {
|
|
4054
|
-
const source = await Bun.file(args.path).text();
|
|
4055
|
-
const transformed = PagesBundleBuilder.transformServerUseClientFetchSource(source);
|
|
4056
|
-
if (transformed === source)
|
|
4057
|
-
return;
|
|
4058
|
-
return { contents: transformed, loader: loaderFor4(args.path) };
|
|
4059
|
-
});
|
|
4060
|
-
}
|
|
4061
|
-
};
|
|
4062
|
-
}
|
|
4063
|
-
static transformServerUseClientFetchSource(source) {
|
|
4064
|
-
if (!source.includes(`with { type: "macro" }`) || !source.includes("FetchClient.build"))
|
|
4065
|
-
return source;
|
|
4066
|
-
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 });");
|
|
4067
|
-
}
|
|
4068
|
-
}
|
|
4069
|
-
function loaderFor4(absPath) {
|
|
4070
|
-
if (absPath.endsWith(".tsx"))
|
|
4071
|
-
return "tsx";
|
|
4072
|
-
if (absPath.endsWith(".jsx"))
|
|
4073
|
-
return "jsx";
|
|
4074
|
-
if (absPath.endsWith(".ts"))
|
|
4075
|
-
return "ts";
|
|
4076
|
-
return "js";
|
|
4077
|
-
}
|
|
4078
|
-
|
|
4079
|
-
// pkgs/@akanjs/devkit/frontendBuild/precompressArtifacts.ts
|
|
4080
|
-
import fs3 from "fs";
|
|
4081
|
-
import path21 from "path";
|
|
4082
|
-
var COMPRESSIBLE_EXTS = new Set([".css", ".html", ".js", ".json", ".svg"]);
|
|
4083
|
-
var MIN_COMPRESS_BYTES = 1024;
|
|
4084
|
-
async function precompressArtifacts(app) {
|
|
4085
|
-
const roots = [path21.join(app.dist.cwdPath, ".akan/artifact/client")];
|
|
4086
|
-
const result = { files: 0, inputBytes: 0, outputBytes: 0 };
|
|
4087
|
-
await Promise.all(roots.map((root) => precompressRoot(root, result)));
|
|
4088
|
-
if (result.files > 0) {
|
|
4089
|
-
app.verbose(`[precompress] wrote ${result.files} gzip sidecars (${formatBytes(result.inputBytes)} -> ${formatBytes(result.outputBytes)})`);
|
|
4090
|
-
}
|
|
4091
|
-
return result;
|
|
4092
|
-
}
|
|
4093
|
-
async function precompressRoot(root, result) {
|
|
4094
|
-
if (!fs3.existsSync(root) || !fs3.statSync(root).isDirectory())
|
|
4095
|
-
return;
|
|
4096
|
-
const glob = new Bun.Glob("**/*");
|
|
4097
|
-
for await (const filePath of glob.scan({ cwd: root, absolute: true })) {
|
|
4098
|
-
if (!await shouldPrecompress(filePath))
|
|
4099
|
-
continue;
|
|
4100
|
-
const bytes = await Bun.file(filePath).bytes();
|
|
4101
|
-
const gz = Bun.gzipSync(toArrayBuffer(bytes));
|
|
4102
|
-
await Bun.write(`${filePath}.gz`, gz);
|
|
4103
|
-
result.files += 1;
|
|
4104
|
-
result.inputBytes += bytes.byteLength;
|
|
4105
|
-
result.outputBytes += gz.byteLength;
|
|
4106
|
-
}
|
|
4107
|
-
}
|
|
4108
|
-
async function shouldPrecompress(filePath) {
|
|
4109
|
-
if (filePath.endsWith(".gz"))
|
|
4110
|
-
return false;
|
|
4111
|
-
if (!COMPRESSIBLE_EXTS.has(path21.extname(filePath).toLowerCase()))
|
|
4112
|
-
return false;
|
|
4113
|
-
const file = Bun.file(filePath);
|
|
4114
|
-
if (!await file.exists())
|
|
4115
|
-
return false;
|
|
4116
|
-
return file.size >= MIN_COMPRESS_BYTES;
|
|
4117
|
-
}
|
|
4118
|
-
function toArrayBuffer(bytes) {
|
|
4119
|
-
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
4120
|
-
}
|
|
4121
|
-
function formatBytes(bytes) {
|
|
4122
|
-
if (bytes < 1024)
|
|
4123
|
-
return `${bytes}B`;
|
|
4124
|
-
if (bytes < 1024 * 1024)
|
|
4125
|
-
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
4126
|
-
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
4127
|
-
}
|
|
4128
|
-
|
|
4129
|
-
// pkgs/@akanjs/devkit/frontendBuild/ssrBaseArtifactBuilder.ts
|
|
4130
|
-
import path22 from "path";
|
|
4131
|
-
import { optimize } from "@tailwindcss/node";
|
|
4132
|
-
function prepareCssAsset(command, basePath, cssText) {
|
|
4133
|
-
return optimize(cssText, { file: `${basePath || "root"}.css`, minify: command === "build" }).code;
|
|
4134
|
-
}
|
|
4135
|
-
|
|
4136
|
-
class SsrBaseArtifactBuilder {
|
|
4137
|
-
#app;
|
|
4138
|
-
#command;
|
|
4139
|
-
#artifactDir;
|
|
4140
|
-
#absArtifactDir;
|
|
4141
|
-
#started = Date.now();
|
|
4142
|
-
constructor(app, command = "start") {
|
|
4143
|
-
this.#app = app;
|
|
4144
|
-
this.#command = command;
|
|
4145
|
-
this.#artifactDir = `${command === "build" ? app.dist.cwdPath : app.cwdPath}/.akan/artifact`;
|
|
4146
|
-
this.#absArtifactDir = path22.resolve(this.#artifactDir);
|
|
4147
|
-
}
|
|
4148
|
-
async build() {
|
|
4149
|
-
const akanConfig = await this.#app.getConfig();
|
|
4150
|
-
const { rscClientUrl, rscRuntimeClientManifest, rscRuntimeSsrManifest, vendorMap } = await this.#buildRuntimeClientEntries();
|
|
4151
|
-
const pageKeys = await this.#app.getPageKeys();
|
|
4152
|
-
this.#app.verbose(`[base-artifact] discovered ${pageKeys.length} route files under ${this.#app.cwdPath}/page`);
|
|
4153
|
-
const pageEntries = await resolveSsrPageEntriesForApp(this.#app, pageKeys);
|
|
4154
|
-
const { cssCompiler, optimizedFonts, cssAssets } = await this.#buildStyleAssets();
|
|
4155
|
-
const pagesBundle = await new PagesBundleBuilder(this.#app, this.#command, pageEntries).build();
|
|
4156
|
-
this.#app.verbose(`[base-artifact] pages bundle -> ${pagesBundle.bundlePath} (buildId=${pagesBundle.buildId}, splitting=${pagesBundle.splitting}, entry=${pagesBundle.entryBytes} bytes, outputs=${pagesBundle.outputCount}, chunks=${pagesBundle.chunkCount}, total=${pagesBundle.outputBytes} bytes)`);
|
|
4157
|
-
const seedIndex = computeRouteSeedIndex(pageEntries);
|
|
4158
|
-
const seedIndexPath = await saveRouteSeedIndex(this.#absArtifactDir, seedIndex, {
|
|
4159
|
-
production: this.#command === "build"
|
|
4160
|
-
});
|
|
4161
|
-
this.#app.verbose(`[base-artifact] route seed index -> ${seedIndexPath} entries=${seedIndex.entries.length} globalLayouts=${seedIndex.globalLayoutFiles.length}`);
|
|
4162
|
-
const artifact = {
|
|
4163
|
-
rscClientUrl,
|
|
4164
|
-
rscRuntimeClientManifest,
|
|
4165
|
-
rscRuntimeSsrManifest,
|
|
4166
|
-
vendorMap,
|
|
4167
|
-
cssAssets,
|
|
4168
|
-
pagesBundlePath: this.#command === "build" ? path22.relative(this.#absArtifactDir, pagesBundle.bundlePath) : pagesBundle.bundlePath,
|
|
4169
|
-
pagesBundleBuildId: pagesBundle.buildId,
|
|
4170
|
-
domains: [...akanConfig.domains],
|
|
4171
|
-
subRoutes: Object.fromEntries(Array.from(akanConfig.subRoutes.entries()).map(([basePath, domains]) => [basePath, [...domains]])),
|
|
4172
|
-
basePaths: [...akanConfig.basePaths],
|
|
4173
|
-
branches: [...akanConfig.branches],
|
|
4174
|
-
i18n: akanConfig.i18n,
|
|
4175
|
-
imageConfig: akanConfig.images,
|
|
4176
|
-
deepLinkAssociations: Object.values(akanConfig.mobile.targets).filter((target) => (target.deepLinks?.domains?.length ?? 0) > 0).map((target) => ({
|
|
4177
|
-
targetName: target.name,
|
|
4178
|
-
appId: target.appId,
|
|
4179
|
-
domains: target.deepLinks?.domains ?? [],
|
|
4180
|
-
iosTeamId: target.deepLinks?.ios?.teamId,
|
|
4181
|
-
androidSha256CertFingerprints: target.deepLinks?.android?.sha256CertFingerprints
|
|
4182
|
-
}))
|
|
4183
|
-
};
|
|
4184
|
-
await Bun.write(path22.join(this.#absArtifactDir, "base-artifact.json"), `${JSON.stringify(artifact, null, 2)}
|
|
4185
|
-
`);
|
|
4186
|
-
this.#app.verbose(`[base-artifact] complete in ${Date.now() - this.#started}ms`);
|
|
4187
|
-
return { artifact, seedIndex, cssCompiler, optimizedFonts };
|
|
4188
|
-
}
|
|
4189
|
-
async#buildRuntimeClientEntries() {
|
|
4190
|
-
const akanServerPath = await this.#resolveAkanServerPath();
|
|
4191
|
-
const rscClientEntry = path22.resolve(akanServerPath, "rscClient.tsx");
|
|
4192
|
-
const rscSegmentOutletEntry = path22.resolve(akanServerPath, "rscSegmentOutlet.tsx");
|
|
4193
|
-
const vendorEntries = VENDOR_SPECIFIERS.map((specifier) => ({
|
|
4194
|
-
specifier,
|
|
4195
|
-
absPath: path22.resolve(akanServerPath, "vendor", `${specifier.replaceAll("/", "-").replaceAll(".", "-")}.ts`)
|
|
4196
|
-
}));
|
|
4197
|
-
const entries = [rscClientEntry, rscSegmentOutletEntry, ...vendorEntries.map((v) => v.absPath)];
|
|
4198
|
-
const clientBundle = await new ClientEntriesBundler({ app: this.#app, entries, command: this.#command }).bundle();
|
|
4199
|
-
const ssrBundle = await new ClientEntriesBundler({
|
|
4200
|
-
app: this.#app,
|
|
4201
|
-
entries: [rscSegmentOutletEntry],
|
|
4202
|
-
...RouteClientBuilder.resolveSsrClientExternalOptions(this.#command),
|
|
4203
|
-
outputSubdir: "client-ssr",
|
|
4204
|
-
command: this.#command
|
|
4205
|
-
}).bundle();
|
|
4206
|
-
const rscClientUrl = clientBundle.entryUrlsByAbsPath.get(rscClientEntry) ?? "";
|
|
4207
|
-
const rscRuntimeSsrManifest = {
|
|
4208
|
-
moduleLoading: null,
|
|
4209
|
-
moduleMap: Object.fromEntries(Object.entries(clientBundle.manifest).map(([key, row]) => {
|
|
4210
|
-
const ssrOutput = ssrBundle.entryOutputAbsByAbsPath.get(rscSegmentOutletEntry);
|
|
4211
|
-
if (!ssrOutput || key !== `${clientBundle.clientReferenceIdByAbsPath.get(rscSegmentOutletEntry)}#${row.name}`) {
|
|
4212
|
-
return null;
|
|
4213
|
-
}
|
|
4214
|
-
return [
|
|
4215
|
-
row.id,
|
|
4216
|
-
{ [row.name]: { id: ssrOutput, chunks: [ssrOutput, ssrOutput], name: row.name, async: true } }
|
|
4217
|
-
];
|
|
4218
|
-
}).filter((entry) => Boolean(entry)))
|
|
4219
|
-
};
|
|
4220
|
-
const vendorMap = Object.fromEntries(vendorEntries.map(({ specifier, absPath }) => [specifier, clientBundle.entryUrlsByAbsPath.get(absPath) ?? ""]));
|
|
4221
|
-
this.#app.verbose(`[base-artifact] rscClientUrl=${rscClientUrl} vendors=${Object.keys(vendorMap).length}`);
|
|
4222
|
-
return {
|
|
4223
|
-
rscClientUrl,
|
|
4224
|
-
rscRuntimeClientManifest: clientBundle.manifest,
|
|
4225
|
-
rscRuntimeSsrManifest,
|
|
4226
|
-
vendorMap
|
|
4227
|
-
};
|
|
4228
|
-
}
|
|
4229
|
-
async#resolveAkanServerPath() {
|
|
4230
|
-
const candidates = [];
|
|
4231
|
-
try {
|
|
4232
|
-
candidates.push(path22.dirname(Bun.resolveSync("akanjs/server", this.#app.workspace.workspaceRoot)));
|
|
4233
|
-
} catch {}
|
|
4234
|
-
candidates.push(path22.join(this.#app.workspace.workspaceRoot, "pkgs/akanjs/server"), path22.join(this.#app.workspace.workspaceRoot, "node_modules/akanjs/server"));
|
|
4235
|
-
try {
|
|
4236
|
-
candidates.push(path22.dirname(Bun.resolveSync("akanjs/server", path22.dirname(Bun.main))));
|
|
4237
|
-
} catch {}
|
|
4238
|
-
candidates.push(path22.join(path22.dirname(Bun.main), "node_modules/akanjs/server"), path22.join(path22.dirname(Bun.main), "../../akanjs/server"), path22.resolve(import.meta.dir, "../../server"), path22.resolve(import.meta.dir, "../server"));
|
|
4239
|
-
for (const candidate of candidates) {
|
|
4240
|
-
if (await Bun.file(path22.join(candidate, "rscClient.tsx")).exists())
|
|
4241
|
-
return candidate;
|
|
4242
|
-
}
|
|
4243
|
-
throw new Error(`[base-artifact] failed to locate akanjs/server; looked in: ${candidates.join(", ")}`);
|
|
4244
|
-
}
|
|
4245
|
-
async#buildStyleAssets() {
|
|
4246
|
-
const cssCompiler = new CssCompiler(this.#app);
|
|
4247
|
-
const cssByBasePath = await cssCompiler.getCssByBasePath();
|
|
4248
|
-
const optimizedFonts = await new FontOptimizer(this.#app, this.#command).optimize();
|
|
4249
|
-
const cssAssets = Object.fromEntries(await Promise.all(Object.entries(cssByBasePath).flatMap(([basePath, baseCssText]) => {
|
|
4250
|
-
const cssText = [baseCssText, optimizedFonts.css].filter(Boolean).join(`
|
|
4251
|
-
`);
|
|
4252
|
-
if (!cssText)
|
|
4253
|
-
return [];
|
|
4254
|
-
return [this.#writeCssAsset(basePath, cssText)];
|
|
4255
|
-
})));
|
|
4256
|
-
if (optimizedFonts.files.length > 0)
|
|
4257
|
-
this.#app.verbose(`[base-artifact] optimized ${optimizedFonts.files.length} font files`);
|
|
4258
|
-
return { cssCompiler, optimizedFonts, cssAssets };
|
|
4259
|
-
}
|
|
4260
|
-
async#writeCssAsset(basePath, cssText) {
|
|
4261
|
-
const cssAssetName = basePath || "root";
|
|
4262
|
-
const preparedCssText = await prepareCssAsset(this.#command, basePath, cssText);
|
|
4263
|
-
const cssHash = Bun.hash(`${basePath}
|
|
4264
|
-
${preparedCssText}`).toString(36);
|
|
4265
|
-
const [cssRelPath, cssUrl] = [
|
|
4266
|
-
`styles/${cssAssetName}-${cssHash}.css`,
|
|
4267
|
-
`/_akan/styles/${cssAssetName}-${cssHash}.css`
|
|
4268
|
-
];
|
|
4269
|
-
await Bun.write(path22.join(this.#absArtifactDir, cssRelPath), preparedCssText);
|
|
4270
|
-
this.#app.verbose(`[base-artifact] wrote ${preparedCssText.length} bytes of CSS for ${basePath} -> ${cssRelPath}`);
|
|
4271
|
-
return [basePath, { cssUrl, cssRelPath }];
|
|
4272
|
-
}
|
|
4273
|
-
}
|
|
4274
|
-
export { GraphClientEntryDiscovery, RouteClientBuilder, AllRoutesBuilder, AutoImportSync, CsrArtifactBuilder, CssCompiler, DevChangePlanner, DevGeneratedIndexSync, FontOptimizer, PagesBundleBuilder, precompressArtifacts, SsrBaseArtifactBuilder };
|