@akanjs/cli 3.0.0-alpha.75 → 3.0.0-alpha.76
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-z0cam80t.js → agent.command-j4744bct.js} +6 -6
- package/{application.command-2dct57za.js → application.command-1sq0zwqx.js} +5 -5
- package/{applicationBuildRunner-ds8acdfz.js → applicationBuildRunner-8tqhxjd0.js} +2 -2
- package/buildBatch.proc.js +3 -3
- package/{capacitorApp-md7dnt25.js → capacitorApp-nqnvsbv2.js} +3 -3
- package/{cloud.command-8j7xrxsm.js → cloud.command-ajkhg234.js} +8 -8
- package/{context.command-xjf4y7em.js → context.command-jxga9cc9.js} +13 -13
- package/{guideline.command-yzm0qevv.js → guideline.command-2fts4m5x.js} +3 -3
- package/incrementalBuilder.proc.js +2 -2
- package/{index-h9cybjep.js → index-004ah39s.js} +1 -1
- package/{index-eed7vc38.js → index-0zabybjx.js} +10 -10
- package/{index-a341yqdh.js → index-120pzxb2.js} +3 -3
- package/{index-61x7qthy.js → index-25qfm3at.js} +1123 -1172
- package/{index-9gqzgfsd.js → index-2j5xrm1c.js} +2 -2
- package/{index-hvbdre8z.js → index-3qa4f4j7.js} +2 -2
- package/{index-te6r1b79.js → index-5yxz0zzc.js} +3 -3
- package/{index-akz5xvyw.js → index-6wsjpt7b.js} +1 -1
- package/{index-116w1dap.js → index-85rdqkfx.js} +3 -3
- package/{index-bmz9crym.js → index-aczwndks.js} +2 -2
- package/{index-q0s2dp3y.js → index-aevn8b0n.js} +1 -1
- package/{index-f39512t6.js → index-dp66vwr6.js} +117 -24
- package/{index-q13s3e01.js → index-j8rp2kdv.js} +3 -3
- package/{index-763h7h9g.js → index-mk1x6sad.js} +1 -1
- package/{index-ym9g9ggs.js → index-mpka7dq4.js} +1 -1
- package/{index-z52nhkjs.js → index-peg7zphh.js} +4 -4
- package/{index-y3wyqcsp.js → index-qgzzz44h.js} +5 -5
- package/{index-qge1hc70.js → index-qhy7zdmz.js} +2 -2
- package/{index-vrra4ttx.js → index-v6p94mz1.js} +4 -4
- package/{index-5r4e65hb.js → index-yzsp0jgb.js} +5 -5
- package/index.js +19 -19
- package/{library.command-w8b53vtt.js → library.command-tfkfhbcb.js} +4 -4
- package/{localRegistry.command-ymrgda7e.js → localRegistry.command-rhn2mh9j.js} +7 -7
- package/{module.command-7pje37dv.js → module.command-29xg1kzr.js} +6 -6
- package/{package.command-jg4qk8ts.js → package.command-j6yn5c7h.js} +4 -4
- package/package.json +2 -2
- package/{page.command-abbpj96j.js → page.command-vgxetvga.js} +4 -4
- package/{primitive.command-jw4yv918.js → primitive.command-k85cvm2z.js} +7 -7
- package/{quality.command-06j6qy6n.js → quality.command-9dsv2jw7.js} +3 -3
- package/{repair.command-x6resn10.js → repair.command-mzdh4yby.js} +6 -6
- package/{scalar.command-mj2vx6rj.js → scalar.command-0cgq46dj.js} +5 -5
- package/{workflow.command-pmw2tere.js → workflow.command-2yxpvpg3.js} +10 -10
- package/{workspace.command-x32k2vma.js → workspace.command-42yrde58.js} +19 -19
|
@@ -11,8 +11,9 @@ import {
|
|
|
11
11
|
transformUseClient
|
|
12
12
|
} from "./index-jcm6zr20.js";
|
|
13
13
|
import {
|
|
14
|
+
AsyncDefaultExportDetector,
|
|
14
15
|
resolveSsrPageEntriesForApp
|
|
15
|
-
} from "./index-
|
|
16
|
+
} from "./index-dp66vwr6.js";
|
|
16
17
|
import {
|
|
17
18
|
__require
|
|
18
19
|
} from "./index-yb2wsc7f.js";
|
|
@@ -227,334 +228,8 @@ class AllRoutesBuilder {
|
|
|
227
228
|
}
|
|
228
229
|
}
|
|
229
230
|
|
|
230
|
-
// pkgs/@akanjs/devkit/frontendBuild/csrArtifactBuilder.ts
|
|
231
|
-
import { mkdir, rm, unlink } from "fs/promises";
|
|
232
|
-
import path5 from "path";
|
|
233
|
-
|
|
234
|
-
// pkgs/@akanjs/devkit/frontendBuild/pagesEntrySourceGenerator.ts
|
|
235
|
-
import fs from "fs";
|
|
236
|
-
import path4 from "path";
|
|
237
|
-
import ts from "typescript";
|
|
238
|
-
|
|
239
|
-
class PagesEntrySourceGenerator {
|
|
240
|
-
#pageEntries;
|
|
241
|
-
constructor(pageEntries) {
|
|
242
|
-
this.#pageEntries = pageEntries;
|
|
243
|
-
}
|
|
244
|
-
static generate(pageEntries) {
|
|
245
|
-
return new PagesEntrySourceGenerator(pageEntries).generate();
|
|
246
|
-
}
|
|
247
|
-
generate() {
|
|
248
|
-
const lines = this.#pageEntries.map(({ key, moduleAbsPath }) => {
|
|
249
|
-
const specifier = PagesEntrySourceGenerator.#toImportSpecifier(moduleAbsPath);
|
|
250
|
-
return ` ${JSON.stringify(key)}: () => import(${JSON.stringify(specifier)}),`;
|
|
251
|
-
});
|
|
252
|
-
return `export const pages = {
|
|
253
|
-
${lines.join(`
|
|
254
|
-
`)}
|
|
255
|
-
};
|
|
256
|
-
`;
|
|
257
|
-
}
|
|
258
|
-
static generateStatic(pageEntries) {
|
|
259
|
-
return new PagesEntrySourceGenerator(pageEntries).generateStatic();
|
|
260
|
-
}
|
|
261
|
-
generateStatic() {
|
|
262
|
-
const imports = this.#pageEntries.map(({ moduleAbsPath }, index) => {
|
|
263
|
-
const specifier = PagesEntrySourceGenerator.#toImportSpecifier(moduleAbsPath);
|
|
264
|
-
return `import * as page${index} from ${JSON.stringify(specifier)};`;
|
|
265
|
-
});
|
|
266
|
-
const entries = this.#pageEntries.map(({ key, moduleAbsPath }, index) => {
|
|
267
|
-
const isAsyncDefault = PagesEntrySourceGenerator.#hasAsyncDefaultExport(moduleAbsPath);
|
|
268
|
-
return ` ${JSON.stringify(key)}: { loader: async () => page${index}, isAsyncDefault: ${isAsyncDefault} },`;
|
|
269
|
-
});
|
|
270
|
-
return `${imports.join(`
|
|
271
|
-
`)}
|
|
272
|
-
export const pages = {
|
|
273
|
-
${entries.join(`
|
|
274
|
-
`)}
|
|
275
|
-
};
|
|
276
|
-
`;
|
|
277
|
-
}
|
|
278
|
-
static #toImportSpecifier(moduleAbsPath) {
|
|
279
|
-
return path4.resolve(moduleAbsPath).split(path4.sep).join("/");
|
|
280
|
-
}
|
|
281
|
-
static #hasAsyncDefaultExport(moduleAbsPath) {
|
|
282
|
-
try {
|
|
283
|
-
const source = fs.readFileSync(path4.resolve(moduleAbsPath), "utf8");
|
|
284
|
-
const sourceFile = ts.createSourceFile(moduleAbsPath, source, ts.ScriptTarget.Latest, true, PagesEntrySourceGenerator.#scriptKind(moduleAbsPath));
|
|
285
|
-
return PagesEntrySourceGenerator.#sourceFileHasAsyncDefaultExport(sourceFile);
|
|
286
|
-
} catch {
|
|
287
|
-
return false;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
static #sourceFileHasAsyncDefaultExport(sourceFile) {
|
|
291
|
-
const asyncBindings = new Map;
|
|
292
|
-
let defaultIdentifier = null;
|
|
293
|
-
for (const statement of sourceFile.statements) {
|
|
294
|
-
if (ts.isFunctionDeclaration(statement)) {
|
|
295
|
-
if (PagesEntrySourceGenerator.#hasModifier(statement, ts.SyntaxKind.DefaultKeyword)) {
|
|
296
|
-
return PagesEntrySourceGenerator.#hasModifier(statement, ts.SyntaxKind.AsyncKeyword);
|
|
297
|
-
}
|
|
298
|
-
if (statement.name) {
|
|
299
|
-
asyncBindings.set(statement.name.text, PagesEntrySourceGenerator.#hasModifier(statement, ts.SyntaxKind.AsyncKeyword));
|
|
300
|
-
}
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
if (ts.isVariableStatement(statement)) {
|
|
304
|
-
for (const declaration of statement.declarationList.declarations) {
|
|
305
|
-
if (!ts.isIdentifier(declaration.name))
|
|
306
|
-
continue;
|
|
307
|
-
asyncBindings.set(declaration.name.text, PagesEntrySourceGenerator.#isAsyncFunctionExpression(declaration.initializer));
|
|
308
|
-
}
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
311
|
-
if (ts.isExportAssignment(statement)) {
|
|
312
|
-
if (PagesEntrySourceGenerator.#isAsyncFunctionExpression(statement.expression))
|
|
313
|
-
return true;
|
|
314
|
-
if (ts.isIdentifier(statement.expression))
|
|
315
|
-
defaultIdentifier = statement.expression.text;
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
319
|
-
const exportClause = statement.exportClause;
|
|
320
|
-
for (const specifier of exportClause.elements) {
|
|
321
|
-
if (specifier.name.text !== "default")
|
|
322
|
-
continue;
|
|
323
|
-
defaultIdentifier = specifier.propertyName?.text ?? specifier.name.text;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
return defaultIdentifier ? asyncBindings.get(defaultIdentifier) === true : false;
|
|
328
|
-
}
|
|
329
|
-
static #hasModifier(node, kind) {
|
|
330
|
-
return ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some((modifier) => modifier.kind === kind) ?? false);
|
|
331
|
-
}
|
|
332
|
-
static #isAsyncFunctionExpression(node) {
|
|
333
|
-
return Boolean(node && (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && PagesEntrySourceGenerator.#hasModifier(node, ts.SyntaxKind.AsyncKeyword));
|
|
334
|
-
}
|
|
335
|
-
static #scriptKind(moduleAbsPath) {
|
|
336
|
-
return moduleAbsPath.endsWith(".tsx") || moduleAbsPath.endsWith(".jsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// pkgs/@akanjs/devkit/frontendBuild/csrArtifactBuilder.ts
|
|
341
|
-
class CsrArtifactBuilder {
|
|
342
|
-
#app;
|
|
343
|
-
#command;
|
|
344
|
-
#lang;
|
|
345
|
-
constructor(app, command = "start", lang = "en") {
|
|
346
|
-
this.#app = app;
|
|
347
|
-
this.#command = command;
|
|
348
|
-
this.#lang = lang;
|
|
349
|
-
}
|
|
350
|
-
async build() {
|
|
351
|
-
const pageKeys = await this.#app.getPageKeys();
|
|
352
|
-
if (pageKeys.length === 0) {
|
|
353
|
-
this.#app.log(`[cli] no route files under ${this.#app.cwdPath}/page \u2014 skipping CSR build`);
|
|
354
|
-
return null;
|
|
355
|
-
}
|
|
356
|
-
const pageEntries = await resolveSsrPageEntriesForApp(this.#app, pageKeys);
|
|
357
|
-
const akanConfig = await this.#app.getConfig();
|
|
358
|
-
const artifact = await this.#loadCsrArtifact();
|
|
359
|
-
const csrBasePaths = [...akanConfig.basePaths];
|
|
360
|
-
const htmlEntries = csrBasePaths.length > 0 ? csrBasePaths : ["index"];
|
|
361
|
-
await rm(this.#outputDir, { recursive: true, force: true });
|
|
362
|
-
await mkdir(path5.join(this.#app.cwdPath, ".akan/generated/csr"), { recursive: true });
|
|
363
|
-
const generatedHtmlFiles = Object.fromEntries(htmlEntries.map((basePath) => this.#createHtmlFile(basePath)));
|
|
364
|
-
const result = await Bun.build({
|
|
365
|
-
target: "browser",
|
|
366
|
-
entrypoints: Object.keys(generatedHtmlFiles),
|
|
367
|
-
files: {
|
|
368
|
-
...generatedHtmlFiles,
|
|
369
|
-
[`${this.#app.cwdPath}/.akan/generated/csr/csr.tsx`]: `
|
|
370
|
-
import { bootCsr } from "akanjs/webkit";
|
|
371
|
-
${PagesEntrySourceGenerator.generateStatic(pageEntries)}
|
|
372
|
-
void bootCsr(pages);
|
|
373
|
-
`
|
|
374
|
-
},
|
|
375
|
-
root: `${this.#app.cwdPath}/.akan/generated/csr`,
|
|
376
|
-
outdir: this.#outputDir,
|
|
377
|
-
splitting: false,
|
|
378
|
-
minify: true,
|
|
379
|
-
env: "AKAN_PUBLIC_*",
|
|
380
|
-
define: this.#define(),
|
|
381
|
-
optimizeImports: akanConfig.optimizeImports
|
|
382
|
-
});
|
|
383
|
-
if (!result.success) {
|
|
384
|
-
const logs = result.logs.map((log) => log.message).join(`
|
|
385
|
-
`);
|
|
386
|
-
throw new Error(`[csr-build] failed${logs ? `
|
|
387
|
-
${logs}` : ""}`);
|
|
388
|
-
}
|
|
389
|
-
await this.#inlineCsrArtifacts(artifact.cssAssets ?? {});
|
|
390
|
-
this.#app.verbose(`[csr-build] output -> ${this.#outputDir}`);
|
|
391
|
-
return { outputDir: this.#outputDir };
|
|
392
|
-
}
|
|
393
|
-
get #outputDir() {
|
|
394
|
-
return path5.join(this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath, this.#command === "build" ? "csr" : ".akan/artifact/csr");
|
|
395
|
-
}
|
|
396
|
-
#define() {
|
|
397
|
-
const nodeEnv = this.#command === "build" ? "production" : "development";
|
|
398
|
-
return {
|
|
399
|
-
"process.env.NODE_ENV": JSON.stringify(nodeEnv),
|
|
400
|
-
"process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("csr"),
|
|
401
|
-
...Object.fromEntries(Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]))
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
#createHtmlFile(basePath) {
|
|
405
|
-
const filename = `${basePath}.html`;
|
|
406
|
-
return [
|
|
407
|
-
`${this.#app.cwdPath}/.akan/generated/csr/${filename}`,
|
|
408
|
-
`<!doctype html>
|
|
409
|
-
<html lang="${this.#lang}">
|
|
410
|
-
<head>
|
|
411
|
-
<meta charset="utf-8" />
|
|
412
|
-
<title>${this.#app.name}</title>
|
|
413
|
-
<base href="/" />
|
|
414
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
415
|
-
</head>
|
|
416
|
-
<body>
|
|
417
|
-
<div id="root"></div>
|
|
418
|
-
<script type="module" src="./csr.tsx"></script>
|
|
419
|
-
</body>
|
|
420
|
-
</html>
|
|
421
|
-
`
|
|
422
|
-
];
|
|
423
|
-
}
|
|
424
|
-
async#loadCsrArtifact() {
|
|
425
|
-
const artifactDir = path5.join(this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath, ".akan/artifact");
|
|
426
|
-
const artifactFile = Bun.file(path5.join(artifactDir, "base-artifact.json"));
|
|
427
|
-
if (!await artifactFile.exists())
|
|
428
|
-
return { cssAssets: {} };
|
|
429
|
-
const artifact = await artifactFile.json();
|
|
430
|
-
return { cssAssets: artifact.cssAssets ?? {} };
|
|
431
|
-
}
|
|
432
|
-
async#inlineCsrArtifacts(cssAssets) {
|
|
433
|
-
const jsFiles = new Set;
|
|
434
|
-
const cssFiles = new Set;
|
|
435
|
-
for (const htmlPath of await this.#htmlOutputPaths()) {
|
|
436
|
-
const htmlFile = Bun.file(htmlPath);
|
|
437
|
-
if (!await htmlFile.exists())
|
|
438
|
-
continue;
|
|
439
|
-
const basePath = path5.basename(htmlPath, ".html") === "index" ? "" : path5.basename(htmlPath, ".html");
|
|
440
|
-
const inlined = await this.#inlineHtmlAssets(await htmlFile.text(), htmlPath, cssAssets[basePath]);
|
|
441
|
-
for (const filePath of inlined.jsFiles)
|
|
442
|
-
jsFiles.add(filePath);
|
|
443
|
-
for (const filePath of inlined.cssFiles)
|
|
444
|
-
cssFiles.add(filePath);
|
|
445
|
-
await Bun.write(htmlPath, inlined.html);
|
|
446
|
-
}
|
|
447
|
-
for (const filePath of jsFiles)
|
|
448
|
-
await unlink(filePath).catch(() => {
|
|
449
|
-
return;
|
|
450
|
-
});
|
|
451
|
-
for (const filePath of cssFiles)
|
|
452
|
-
await unlink(filePath).catch(() => {
|
|
453
|
-
return;
|
|
454
|
-
});
|
|
455
|
-
const remainingJs = await this.#listOutputFiles((filePath) => filePath.endsWith(".js"));
|
|
456
|
-
const remainingCss = await this.#listOutputFiles((filePath) => filePath.endsWith(".css"));
|
|
457
|
-
const remainingAssets = [...remainingJs, ...remainingCss];
|
|
458
|
-
if (remainingAssets.length > 0) {
|
|
459
|
-
throw new Error(`[csr-build] expected single-file HTML, but CSR assets remain:
|
|
460
|
-
${remainingAssets.join(`
|
|
461
|
-
`)}`);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
async#inlineHtmlAssets(html, htmlPath, cssAsset) {
|
|
465
|
-
const jsFiles = [];
|
|
466
|
-
const cssFiles = CsrArtifactBuilder.collectStylesheetHrefs(html).map((href) => CsrArtifactBuilder.resolveHtmlAssetPath(htmlPath, href));
|
|
467
|
-
let next = CsrArtifactBuilder.stripBundledStylesheetLinks(html);
|
|
468
|
-
next = await CsrArtifactBuilder.replaceModuleScriptSrc(next, async (src) => {
|
|
469
|
-
const jsPath = CsrArtifactBuilder.resolveHtmlAssetPath(htmlPath, src);
|
|
470
|
-
jsFiles.push(jsPath);
|
|
471
|
-
return await Bun.file(jsPath).text();
|
|
472
|
-
});
|
|
473
|
-
const bundledCss = (await Promise.all(cssFiles.map((cssFile) => Bun.file(cssFile).text().catch(() => "")))).filter(Boolean).join(`
|
|
474
|
-
`);
|
|
475
|
-
if (bundledCss) {
|
|
476
|
-
const style = CsrArtifactBuilder.createInlineStyle(bundledCss);
|
|
477
|
-
if (!next.includes(style))
|
|
478
|
-
next = CsrArtifactBuilder.injectBeforeHeadEnd(next, style);
|
|
479
|
-
}
|
|
480
|
-
if (cssAsset) {
|
|
481
|
-
const cssPath = path5.join(this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath, ".akan/artifact", cssAsset.cssRelPath);
|
|
482
|
-
const css = await Bun.file(cssPath).text();
|
|
483
|
-
const style = CsrArtifactBuilder.createInlineStyle(css);
|
|
484
|
-
if (!next.includes(style))
|
|
485
|
-
next = CsrArtifactBuilder.injectBeforeHeadEnd(next, style);
|
|
486
|
-
}
|
|
487
|
-
return { html: next, jsFiles, cssFiles };
|
|
488
|
-
}
|
|
489
|
-
async#htmlOutputPaths() {
|
|
490
|
-
return await this.#listOutputFiles((filePath) => filePath.endsWith(".html"));
|
|
491
|
-
}
|
|
492
|
-
async#listOutputFiles(predicate) {
|
|
493
|
-
const glob = new Bun.Glob("**/*");
|
|
494
|
-
const files = [];
|
|
495
|
-
for await (const filePath of glob.scan({ cwd: this.#outputDir, absolute: true })) {
|
|
496
|
-
if (predicate(filePath))
|
|
497
|
-
files.push(filePath);
|
|
498
|
-
}
|
|
499
|
-
return files.sort();
|
|
500
|
-
}
|
|
501
|
-
static injectBeforeHeadEnd(html, snippet) {
|
|
502
|
-
const matches = [...html.matchAll(/<\/head\s*>/gi)];
|
|
503
|
-
const bodyStart = html.search(/<body(?:\s|>)/i);
|
|
504
|
-
const headEnd = matches.filter((match) => match.index !== undefined && (bodyStart === -1 || match.index < bodyStart)).at(-1);
|
|
505
|
-
if (!headEnd || headEnd.index === undefined)
|
|
506
|
-
return `${snippet}
|
|
507
|
-
${html}`;
|
|
508
|
-
return `${html.slice(0, headEnd.index)}${snippet}
|
|
509
|
-
${html.slice(headEnd.index)}`;
|
|
510
|
-
}
|
|
511
|
-
static stripBundledStylesheetLinks(html) {
|
|
512
|
-
return html.replace(/<link\b(?=[^>]*\brel=["']stylesheet["'])[^>]*>\s*/gi, "");
|
|
513
|
-
}
|
|
514
|
-
static collectStylesheetHrefs(html) {
|
|
515
|
-
const linkRe = /<link\b(?=[^>]*\brel=["']stylesheet["'])(?=[^>]*\bhref=["']([^"']+)["'])[^>]*>/gi;
|
|
516
|
-
return [...html.matchAll(linkRe)].map((match) => match[1]).filter((href) => !!href);
|
|
517
|
-
}
|
|
518
|
-
static createInlineStyle(css) {
|
|
519
|
-
return `<style data-akan-css="active">
|
|
520
|
-
${css.replace(/<\/style/gi, "<\\/style")}
|
|
521
|
-
</style>`;
|
|
522
|
-
}
|
|
523
|
-
static async replaceModuleScriptSrc(html, loadScript) {
|
|
524
|
-
const scriptRe = /<script\b(?=[^>]*\btype=["']module["'])(?=[^>]*\bsrc=["']([^"']+)["'])[^>]*>\s*<\/script>/gi;
|
|
525
|
-
let result = "";
|
|
526
|
-
let lastIndex = 0;
|
|
527
|
-
let matched = false;
|
|
528
|
-
for (const match of html.matchAll(scriptRe)) {
|
|
529
|
-
const full = match[0];
|
|
530
|
-
const src = match[1];
|
|
531
|
-
if (match.index === undefined || !src)
|
|
532
|
-
continue;
|
|
533
|
-
matched = true;
|
|
534
|
-
result += html.slice(lastIndex, match.index);
|
|
535
|
-
result += `<script type="module">
|
|
536
|
-
${CsrArtifactBuilder.escapeInlineScript(await loadScript(src))}
|
|
537
|
-
</script>`;
|
|
538
|
-
lastIndex = match.index + full.length;
|
|
539
|
-
}
|
|
540
|
-
if (!matched)
|
|
541
|
-
return html;
|
|
542
|
-
return result + html.slice(lastIndex);
|
|
543
|
-
}
|
|
544
|
-
static escapeInlineScript(source) {
|
|
545
|
-
return source.replace(/<\/script/gi, "<\\/script");
|
|
546
|
-
}
|
|
547
|
-
static resolveHtmlAssetPath(htmlPath, src) {
|
|
548
|
-
if (/^[a-z][a-z0-9+.-]*:/i.test(src) || src.startsWith("//")) {
|
|
549
|
-
throw new Error(`[csr-build] cannot inline external script: ${src}`);
|
|
550
|
-
}
|
|
551
|
-
const normalized = src.startsWith("/") ? src.slice(1) : src;
|
|
552
|
-
return path5.resolve(path5.dirname(htmlPath), normalized);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
231
|
// pkgs/@akanjs/devkit/frontendBuild/cssCompiler.ts
|
|
557
|
-
import
|
|
232
|
+
import path5 from "path";
|
|
558
233
|
import { Logger } from "akanjs/common";
|
|
559
234
|
import { compile } from "tailwindcss";
|
|
560
235
|
|
|
@@ -625,7 +300,7 @@ class CssCandidateCache {
|
|
|
625
300
|
}
|
|
626
301
|
|
|
627
302
|
// pkgs/@akanjs/devkit/frontendBuild/cssImportResolver.ts
|
|
628
|
-
import
|
|
303
|
+
import path4 from "path";
|
|
629
304
|
var CSS_IMPORT_EXTS = ["", ".css", "/styles.css", "/index.css"];
|
|
630
305
|
|
|
631
306
|
class CssImportResolver {
|
|
@@ -678,7 +353,7 @@ class CssImportResolver {
|
|
|
678
353
|
const exact = this.#paths[id];
|
|
679
354
|
if (exact) {
|
|
680
355
|
for (const repl of exact) {
|
|
681
|
-
const resolved = await this.#firstExisting(
|
|
356
|
+
const resolved = await this.#firstExisting(path4.resolve(this.#workspaceRoot, repl));
|
|
682
357
|
if (resolved)
|
|
683
358
|
return resolved;
|
|
684
359
|
}
|
|
@@ -689,7 +364,7 @@ class CssImportResolver {
|
|
|
689
364
|
const suffix = id.slice(prefix.length);
|
|
690
365
|
for (const repl of replacements) {
|
|
691
366
|
const replPath = repl.endsWith("/*") ? repl.slice(0, -1) : repl;
|
|
692
|
-
const resolved = await this.#firstExisting(
|
|
367
|
+
const resolved = await this.#firstExisting(path4.resolve(this.#workspaceRoot, replPath + suffix));
|
|
693
368
|
if (resolved)
|
|
694
369
|
return resolved;
|
|
695
370
|
}
|
|
@@ -719,29 +394,29 @@ class CssImportResolver {
|
|
|
719
394
|
try {
|
|
720
395
|
if (!await Bun.file(pkgPath).exists())
|
|
721
396
|
return null;
|
|
722
|
-
const pkgDir =
|
|
397
|
+
const pkgDir = path4.dirname(pkgPath);
|
|
723
398
|
const pkg = await Bun.file(pkgPath).json();
|
|
724
399
|
const subpath = id === pkgName ? "." : `.${id.slice(pkgName.length)}`;
|
|
725
400
|
const exportValue = pkg.exports?.[subpath];
|
|
726
401
|
const exportedEntry = typeof exportValue === "string" ? exportValue : exportValue?.style || exportValue?.import || exportValue?.default;
|
|
727
402
|
if (exportedEntry)
|
|
728
|
-
return await this.#firstExisting(
|
|
403
|
+
return await this.#firstExisting(path4.resolve(pkgDir, exportedEntry));
|
|
729
404
|
if (subpath !== ".")
|
|
730
|
-
return await this.#firstExisting(
|
|
731
|
-
return await this.#firstExisting(
|
|
405
|
+
return await this.#firstExisting(path4.resolve(pkgDir, subpath));
|
|
406
|
+
return await this.#firstExisting(path4.resolve(pkgDir, pkg.exports?.["."]?.style || pkg.style || "index.css"));
|
|
732
407
|
} catch {
|
|
733
408
|
return null;
|
|
734
409
|
}
|
|
735
410
|
}
|
|
736
411
|
#resolutionBases(fromBase) {
|
|
737
|
-
return [fromBase, this.#workspaceRoot,
|
|
412
|
+
return [fromBase, this.#workspaceRoot, path4.dirname(Bun.main), path4.resolve(path4.dirname(Bun.main), "../..")];
|
|
738
413
|
}
|
|
739
414
|
#packageJsonCandidates(pkgName) {
|
|
740
415
|
return [
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
416
|
+
path4.join(this.#workspaceRoot, "pkgs", pkgName, "package.json"),
|
|
417
|
+
path4.join(this.#workspaceRoot, "node_modules", pkgName, "package.json"),
|
|
418
|
+
path4.join(path4.dirname(Bun.main), "node_modules", pkgName, "package.json"),
|
|
419
|
+
path4.join(path4.dirname(Bun.main), "../../", pkgName, "package.json")
|
|
745
420
|
];
|
|
746
421
|
}
|
|
747
422
|
async#firstExisting(basePath) {
|
|
@@ -759,7 +434,7 @@ class CssImportResolver {
|
|
|
759
434
|
return parts[0] ?? null;
|
|
760
435
|
}
|
|
761
436
|
static isCssFile(filePath) {
|
|
762
|
-
return
|
|
437
|
+
return path4.extname(filePath) === ".css";
|
|
763
438
|
}
|
|
764
439
|
}
|
|
765
440
|
|
|
@@ -778,7 +453,7 @@ class CssCompiler {
|
|
|
778
453
|
this.#app = app;
|
|
779
454
|
}
|
|
780
455
|
get #candidateCachePath() {
|
|
781
|
-
return
|
|
456
|
+
return path5.join(this.#app.cwdPath, ".akan/cache/cssCandidates.json");
|
|
782
457
|
}
|
|
783
458
|
#cssText = null;
|
|
784
459
|
#cssTextByBasePath = null;
|
|
@@ -808,7 +483,7 @@ class CssCompiler {
|
|
|
808
483
|
return filePath;
|
|
809
484
|
}
|
|
810
485
|
for (const ext of SOURCE_EXTS) {
|
|
811
|
-
const filePath =
|
|
486
|
+
const filePath = path5.join(absPathNoExt, `index${ext}`);
|
|
812
487
|
if (await this.#fileExists(filePath))
|
|
813
488
|
return filePath;
|
|
814
489
|
}
|
|
@@ -866,14 +541,14 @@ class CssCompiler {
|
|
|
866
541
|
return this.#cssTextByBasePath;
|
|
867
542
|
}
|
|
868
543
|
async#warnUnreachableStylesheets() {
|
|
869
|
-
const pageDir =
|
|
544
|
+
const pageDir = path5.join(this.#app.cwdPath, "page");
|
|
870
545
|
const glob = new Bun.Glob("**/*.css");
|
|
871
546
|
for await (const cssPath of glob.scan({ cwd: pageDir, absolute: true })) {
|
|
872
|
-
if (cssPath.includes(`${
|
|
547
|
+
if (cssPath.includes(`${path5.sep}(libs)${path5.sep}`))
|
|
873
548
|
continue;
|
|
874
549
|
if (this.#discoveredCssPaths.has(cssPath))
|
|
875
550
|
continue;
|
|
876
|
-
this.#logger.warn(`css ${
|
|
551
|
+
this.#logger.warn(`css ${path5.relative(this.#app.cwdPath, cssPath)} is imported by no route and never compiled`);
|
|
877
552
|
}
|
|
878
553
|
}
|
|
879
554
|
async discoverCss({ refresh } = {}) {
|
|
@@ -885,7 +560,7 @@ class CssCompiler {
|
|
|
885
560
|
pageKeys
|
|
886
561
|
} = {}) {
|
|
887
562
|
pageKeys ??= await this.#app.getPageKeys({ refresh });
|
|
888
|
-
const seeds = pageKeys.map((key) =>
|
|
563
|
+
const seeds = pageKeys.map((key) => path5.resolve(this.#app.cwdPath, "page", key));
|
|
889
564
|
const cssFiles = new Set;
|
|
890
565
|
const sourceFiles = new Set;
|
|
891
566
|
const queue = [...seeds];
|
|
@@ -917,7 +592,7 @@ class CssCompiler {
|
|
|
917
592
|
} catch {
|
|
918
593
|
continue;
|
|
919
594
|
}
|
|
920
|
-
const importerDir =
|
|
595
|
+
const importerDir = path5.dirname(filePath);
|
|
921
596
|
for (const imp of imports) {
|
|
922
597
|
const spec = imp.path;
|
|
923
598
|
if (!spec)
|
|
@@ -942,18 +617,18 @@ class CssCompiler {
|
|
|
942
617
|
return { cssPaths, sourcePaths: [...sourceFiles] };
|
|
943
618
|
}
|
|
944
619
|
async#libTokenStylesheets(sourceFiles) {
|
|
945
|
-
const libsRoot =
|
|
620
|
+
const libsRoot = path5.join(this.#app.workspace.workspaceRoot, "libs");
|
|
946
621
|
const libNames = new Set;
|
|
947
622
|
for (const filePath of sourceFiles) {
|
|
948
|
-
const relPath =
|
|
949
|
-
if (relPath.startsWith("..") ||
|
|
623
|
+
const relPath = path5.relative(libsRoot, filePath);
|
|
624
|
+
if (relPath.startsWith("..") || path5.isAbsolute(relPath))
|
|
950
625
|
continue;
|
|
951
|
-
const [libName] = relPath.split(
|
|
626
|
+
const [libName] = relPath.split(path5.sep);
|
|
952
627
|
if (libName)
|
|
953
628
|
libNames.add(libName);
|
|
954
629
|
}
|
|
955
630
|
const tokenPaths = await Promise.all([...libNames].sort().map(async (libName) => {
|
|
956
|
-
const tokensPath =
|
|
631
|
+
const tokensPath = path5.join(libsRoot, libName, "ui/tokens.css");
|
|
957
632
|
return await this.#fileExists(tokensPath) ? tokensPath : null;
|
|
958
633
|
}));
|
|
959
634
|
return tokenPaths.filter((tokensPath) => !!tokensPath);
|
|
@@ -968,7 +643,7 @@ class CssCompiler {
|
|
|
968
643
|
const compileStarted = Date.now();
|
|
969
644
|
const compilers = await Promise.all(cssPaths.map(async (cssPath) => {
|
|
970
645
|
const css = await Bun.file(cssPath).text();
|
|
971
|
-
const base =
|
|
646
|
+
const base = path5.dirname(cssPath);
|
|
972
647
|
const imported2 = new Map;
|
|
973
648
|
const compiler = await compile(css, {
|
|
974
649
|
base,
|
|
@@ -1012,11 +687,11 @@ class CssCompiler {
|
|
|
1012
687
|
const content = await Bun.file(p).text();
|
|
1013
688
|
imported?.set(p, content);
|
|
1014
689
|
this.#logger.verbose(`css import "${id}" from ${fromBase} -> ${p} (${content.length} bytes)`);
|
|
1015
|
-
return { path: p, base:
|
|
690
|
+
return { path: p, base: path5.dirname(p), content };
|
|
1016
691
|
}
|
|
1017
692
|
async#resolveCssImport(id, fromBase) {
|
|
1018
693
|
if (id.startsWith(".") || id.startsWith("/")) {
|
|
1019
|
-
const filePath =
|
|
694
|
+
const filePath = path5.resolve(fromBase, id);
|
|
1020
695
|
if (await this.#fileExists(filePath))
|
|
1021
696
|
return filePath;
|
|
1022
697
|
throw new Error(`[css] failed to resolve stylesheet import "${id}" from ${fromBase} (no file at ${filePath})`);
|
|
@@ -1036,7 +711,7 @@ class CssCompiler {
|
|
|
1036
711
|
async#loadModule(id, fromBase) {
|
|
1037
712
|
const p = __require.resolve(id, { paths: [fromBase] });
|
|
1038
713
|
const mod = await import(p);
|
|
1039
|
-
return { path: p, base:
|
|
714
|
+
return { path: p, base: path5.dirname(p), module: mod.default ?? mod };
|
|
1040
715
|
}
|
|
1041
716
|
#resolveSourceImport(id, fromBase, resolvePackage) {
|
|
1042
717
|
const cacheKey = `${fromBase}\x00${id}`;
|
|
@@ -1049,7 +724,7 @@ class CssCompiler {
|
|
|
1049
724
|
}
|
|
1050
725
|
async#resolveSourceImportUncached(id, fromBase, resolvePackage) {
|
|
1051
726
|
if (id.startsWith(".") || id.startsWith("/")) {
|
|
1052
|
-
const abs = id.startsWith("/") ? id :
|
|
727
|
+
const abs = id.startsWith("/") ? id : path5.resolve(fromBase, id);
|
|
1053
728
|
return this.#resolveSourceFileCandidate(abs);
|
|
1054
729
|
}
|
|
1055
730
|
const pkg = await resolvePackage(id);
|
|
@@ -1100,7 +775,7 @@ function resolveSourceWithRequire(id, fromBase) {
|
|
|
1100
775
|
}
|
|
1101
776
|
}
|
|
1102
777
|
function isSourceFile(filePath) {
|
|
1103
|
-
return SOURCE_EXTS.includes(
|
|
778
|
+
return SOURCE_EXTS.includes(path5.extname(filePath));
|
|
1104
779
|
}
|
|
1105
780
|
function isIgnoredNodeModuleSource(filePath) {
|
|
1106
781
|
return NODE_MODULES_RE.test(filePath) && !AKANJS_NODE_MODULE_RE.test(filePath);
|
|
@@ -1110,928 +785,1204 @@ function declaredCustomProperties(css) {
|
|
|
1110
785
|
return [...new Set([...withoutThemeBlocks.matchAll(/(?:^|[\s;{])(--[\w-]+)\s*:/g)].map(([, name]) => name))].filter((name) => !!name);
|
|
1111
786
|
}
|
|
1112
787
|
function getPageKeyBasePath(pageKey, basePaths) {
|
|
1113
|
-
const normalized = pageKey.split(
|
|
788
|
+
const normalized = pageKey.split(path5.sep).join("/").replace(/^\.\//, "");
|
|
1114
789
|
const segments = normalized.split("/");
|
|
1115
790
|
const firstPublicSegment = segments.find((segment) => segment !== "[lang]" && !/^\(.+\)$/.test(segment));
|
|
1116
791
|
return firstPublicSegment && basePaths.includes(firstPublicSegment) ? firstPublicSegment : null;
|
|
1117
792
|
}
|
|
1118
793
|
|
|
1119
|
-
// pkgs/@akanjs/devkit/frontendBuild/
|
|
1120
|
-
import { mkdir as mkdir2, stat as stat2 } from "fs/promises";
|
|
794
|
+
// pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
|
|
1121
795
|
import path8 from "path";
|
|
1122
|
-
import ts2 from "typescript";
|
|
1123
|
-
var FONT_URL_PREFIX = "/_akan/fonts";
|
|
1124
|
-
var DEFAULT_FONT_SUBSETS = ["latin"];
|
|
1125
796
|
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
797
|
+
// pkgs/@akanjs/devkit/transforms/externalizeFrameworkPlugin.ts
|
|
798
|
+
import path6 from "path";
|
|
799
|
+
var DEFAULT_INCLUDE = ["akanjs/", "@apps/", "@libs/"];
|
|
800
|
+
var DEFAULT_EXCLUDE_EXACT = new Set(["akanjs/webkit", "@akanjs/cli", "@akanjs/devkit"]);
|
|
801
|
+
var DEFAULT_EXCLUDE_PREFIX = ["@akanjs/cli/", "@akanjs/devkit/"];
|
|
802
|
+
var OPTIONAL_BACKEND_EXTERNAL_EXACT = new Set([
|
|
803
|
+
"@libsql/client",
|
|
804
|
+
"bullmq",
|
|
805
|
+
"ioredis",
|
|
806
|
+
"postgres",
|
|
807
|
+
"protobufjs"
|
|
808
|
+
]);
|
|
809
|
+
var RUNTIME_EXTERNAL_EXACT = new Set([
|
|
810
|
+
"react",
|
|
811
|
+
"react-dom",
|
|
812
|
+
"react/jsx-runtime",
|
|
813
|
+
"react/jsx-dev-runtime",
|
|
814
|
+
"react-server-dom-webpack",
|
|
815
|
+
"react-server-dom-webpack/server.node",
|
|
816
|
+
"react-server-dom-webpack/client.node",
|
|
817
|
+
"react-server-dom-webpack/client.browser"
|
|
818
|
+
]);
|
|
819
|
+
var RUNTIME_EXTERNAL_PREFIX = ["react-dom/", "react-server-dom-webpack/"];
|
|
820
|
+
var CANDIDATE_EXTS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
|
|
821
|
+
async function createExternalizeFrameworkPlugin(options) {
|
|
822
|
+
const tsconfig = await options.app.getTsConfig();
|
|
823
|
+
const includePrefixes = options.include ?? DEFAULT_INCLUDE;
|
|
824
|
+
const extraExact = new Set(options.extra ?? []);
|
|
825
|
+
const workspaceRoot = options.app.workspace.workspaceRoot;
|
|
826
|
+
const tsconfigPaths = tsconfig.compilerOptions.paths ?? {};
|
|
827
|
+
const rootEntries = Object.entries(tsconfigPaths).filter(([k]) => !k.endsWith("/*")).map(([k, v]) => ({ pkg: k, entryFile: v[0] ?? null })).filter((e) => e.entryFile !== null);
|
|
828
|
+
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);
|
|
829
|
+
async function resolveWorkspaceSubpath(spec) {
|
|
830
|
+
if (!workspaceRoot)
|
|
831
|
+
return null;
|
|
832
|
+
for (const { prefix, replacements } of wildcardEntries) {
|
|
833
|
+
if (!spec.startsWith(prefix))
|
|
834
|
+
continue;
|
|
835
|
+
const suffix = spec.slice(prefix.length);
|
|
836
|
+
for (const repl of replacements) {
|
|
837
|
+
const replPath = repl?.endsWith("/*") ? repl.slice(0, -1) : repl ?? "";
|
|
838
|
+
if (!replPath)
|
|
839
|
+
continue;
|
|
840
|
+
const candidate = path6.resolve(workspaceRoot, replPath + suffix);
|
|
841
|
+
const hit = await firstExisting(candidate);
|
|
842
|
+
if (hit)
|
|
843
|
+
return hit;
|
|
844
|
+
}
|
|
1147
845
|
}
|
|
1148
|
-
for (const
|
|
1149
|
-
if (!
|
|
846
|
+
for (const { pkg, entryFile } of rootEntries) {
|
|
847
|
+
if (spec !== pkg && !spec.startsWith(`${pkg}/`))
|
|
1150
848
|
continue;
|
|
1151
|
-
|
|
849
|
+
if (spec === pkg)
|
|
850
|
+
continue;
|
|
851
|
+
const suffix = spec.slice(pkg.length + 1);
|
|
852
|
+
const pkgDir = path6.dirname(path6.resolve(workspaceRoot, entryFile));
|
|
853
|
+
const candidate = path6.join(pkgDir, suffix);
|
|
854
|
+
const hit = await firstExisting(candidate);
|
|
855
|
+
if (hit)
|
|
856
|
+
return hit;
|
|
1152
857
|
}
|
|
1153
|
-
|
|
1154
|
-
if (fontUtilityCss)
|
|
1155
|
-
this.#cssParts.push(fontUtilityCss);
|
|
1156
|
-
const result = { css: this.#cssParts.join(`
|
|
1157
|
-
`), fonts, files: this.#files };
|
|
1158
|
-
if (cacheKey)
|
|
1159
|
-
await this.#writeCache(cacheKey, result);
|
|
1160
|
-
return result;
|
|
1161
|
-
}
|
|
1162
|
-
get #cachePath() {
|
|
1163
|
-
return path8.join(this.#artifactRoot, "fontCache.json");
|
|
858
|
+
return null;
|
|
1164
859
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
if (
|
|
1181
|
-
return
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
860
|
+
return {
|
|
861
|
+
name: "akan-externalize-framework",
|
|
862
|
+
setup(build) {
|
|
863
|
+
build.onResolve({ filter: /.*/ }, async (args) => {
|
|
864
|
+
const spec = args.path;
|
|
865
|
+
if (spec === "." || spec === ".." || spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/"))
|
|
866
|
+
return;
|
|
867
|
+
if (extraExact.has(spec) || DEFAULT_EXCLUDE_EXACT.has(spec))
|
|
868
|
+
return { path: spec, external: true };
|
|
869
|
+
for (const prefix of DEFAULT_EXCLUDE_PREFIX) {
|
|
870
|
+
if (spec.startsWith(prefix))
|
|
871
|
+
return { path: spec, external: true };
|
|
872
|
+
}
|
|
873
|
+
if (OPTIONAL_BACKEND_EXTERNAL_EXACT.has(spec))
|
|
874
|
+
return { path: spec, external: true };
|
|
875
|
+
if (RUNTIME_EXTERNAL_EXACT.has(spec))
|
|
876
|
+
return { path: spec, external: true };
|
|
877
|
+
for (const prefix of RUNTIME_EXTERNAL_PREFIX) {
|
|
878
|
+
if (spec.startsWith(prefix))
|
|
879
|
+
return { path: spec, external: true };
|
|
880
|
+
}
|
|
881
|
+
for (const prefix of includePrefixes) {
|
|
882
|
+
if (!spec.startsWith(prefix))
|
|
883
|
+
continue;
|
|
884
|
+
const resolved = await resolveWorkspaceSubpath(spec);
|
|
885
|
+
if (resolved)
|
|
886
|
+
return { path: resolved };
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
return;
|
|
890
|
+
});
|
|
1186
891
|
}
|
|
1187
|
-
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
async function firstExisting(basePath) {
|
|
895
|
+
if (await Bun.file(basePath).exists())
|
|
896
|
+
return basePath;
|
|
897
|
+
for (const ext of CANDIDATE_EXTS) {
|
|
898
|
+
const candidate = `${basePath}${ext}`;
|
|
899
|
+
if (await Bun.file(candidate).exists())
|
|
900
|
+
return candidate;
|
|
1188
901
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
902
|
+
for (const ext of CANDIDATE_EXTS) {
|
|
903
|
+
const candidate = path6.join(basePath, `index${ext}`);
|
|
904
|
+
if (await Bun.file(candidate).exists())
|
|
905
|
+
return candidate;
|
|
1191
906
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
907
|
+
return null;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// pkgs/@akanjs/devkit/transforms/useClientBundlePlugin.ts
|
|
911
|
+
function createUseClientBundlePlugin(options = {}) {
|
|
912
|
+
return {
|
|
913
|
+
name: "akan-use-client-bundle",
|
|
914
|
+
setup(build) {
|
|
915
|
+
build.onLoad({ filter: /\.(tsx|ts|jsx|js)$/ }, async (args) => {
|
|
916
|
+
if (args.path.includes("/node_modules/"))
|
|
917
|
+
return;
|
|
918
|
+
let source;
|
|
919
|
+
try {
|
|
920
|
+
source = await Bun.file(args.path).text();
|
|
921
|
+
} catch {
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
const stubbed = transformUseClient(source, {
|
|
925
|
+
path: args.path,
|
|
926
|
+
workspaceRoot: options.workspaceRoot
|
|
927
|
+
});
|
|
928
|
+
if (stubbed === null)
|
|
929
|
+
return;
|
|
930
|
+
return { contents: stubbed, loader: loaderFor(args.path) };
|
|
931
|
+
});
|
|
1198
932
|
}
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
function loaderFor(absPath) {
|
|
936
|
+
if (absPath.endsWith(".tsx"))
|
|
937
|
+
return "tsx";
|
|
938
|
+
if (absPath.endsWith(".jsx"))
|
|
939
|
+
return "jsx";
|
|
940
|
+
if (absPath.endsWith(".ts"))
|
|
941
|
+
return "ts";
|
|
942
|
+
return "js";
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// pkgs/@akanjs/devkit/frontendBuild/pagesEntrySourceGenerator.ts
|
|
946
|
+
import path7 from "path";
|
|
947
|
+
class PagesEntrySourceGenerator {
|
|
948
|
+
#pageEntries;
|
|
949
|
+
constructor(pageEntries) {
|
|
950
|
+
this.#pageEntries = pageEntries;
|
|
1199
951
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
if (cache?.version !== FontOptimizer.#cacheVersion || cache.key !== key)
|
|
1203
|
-
return null;
|
|
1204
|
-
const files = cache.files.map((relativePath) => path8.join(this.#artifactRoot, relativePath));
|
|
1205
|
-
for (const filePath of files) {
|
|
1206
|
-
if (!await Bun.file(filePath).exists())
|
|
1207
|
-
return null;
|
|
1208
|
-
}
|
|
1209
|
-
return { css: cache.css, files };
|
|
952
|
+
static generate(pageEntries) {
|
|
953
|
+
return new PagesEntrySourceGenerator(pageEntries).generate();
|
|
1210
954
|
}
|
|
1211
|
-
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1214
|
-
key
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
955
|
+
generate() {
|
|
956
|
+
const lines = this.#pageEntries.map(({ key, moduleAbsPath }) => {
|
|
957
|
+
const specifier = PagesEntrySourceGenerator.#toImportSpecifier(moduleAbsPath);
|
|
958
|
+
return ` ${JSON.stringify(key)}: () => import(${JSON.stringify(specifier)}),`;
|
|
959
|
+
});
|
|
960
|
+
return `export const pages = {
|
|
961
|
+
${lines.join(`
|
|
962
|
+
`)}
|
|
963
|
+
};
|
|
964
|
+
`;
|
|
1219
965
|
}
|
|
1220
|
-
async
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
const
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
fonts.push(...this.#extractFontsExport(source, filePath));
|
|
966
|
+
static async generateStatic(pageEntries) {
|
|
967
|
+
return await new PagesEntrySourceGenerator(pageEntries).generateStatic();
|
|
968
|
+
}
|
|
969
|
+
async generateStatic() {
|
|
970
|
+
const imports = this.#pageEntries.map(({ moduleAbsPath }, index) => {
|
|
971
|
+
const specifier = PagesEntrySourceGenerator.#toImportSpecifier(moduleAbsPath);
|
|
972
|
+
return `import * as page${index} from ${JSON.stringify(specifier)};`;
|
|
973
|
+
});
|
|
974
|
+
const entries = await Promise.all(this.#pageEntries.map(async ({ key, moduleAbsPath }, index) => {
|
|
975
|
+
const isAsyncDefault = await AsyncDefaultExportDetector.detect(moduleAbsPath);
|
|
976
|
+
return ` ${JSON.stringify(key)}: { loader: async () => page${index}, isAsyncDefault: ${isAsyncDefault} },`;
|
|
1232
977
|
}));
|
|
1233
|
-
return
|
|
978
|
+
return `${imports.join(`
|
|
979
|
+
`)}
|
|
980
|
+
export const pages = {
|
|
981
|
+
${entries.join(`
|
|
982
|
+
`)}
|
|
983
|
+
};
|
|
984
|
+
`;
|
|
1234
985
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
for (const face of this.#getFontFaces(font)) {
|
|
1238
|
-
const sourcePath = await this.#resolveFontSourcePath(face.src);
|
|
1239
|
-
if (!sourcePath) {
|
|
1240
|
-
this.#app.logger.warn(`[font] source not found: ${face.src}`);
|
|
1241
|
-
continue;
|
|
1242
|
-
}
|
|
1243
|
-
const outputPath = path8.join(this.#artifactRoot, face.optimizedSrc.replace(/^\/_akan\//, ""));
|
|
1244
|
-
await mkdir2(path8.dirname(outputPath), { recursive: true });
|
|
1245
|
-
const sourceBuffer = Buffer.from(await Bun.file(sourcePath).arrayBuffer());
|
|
1246
|
-
const outputBuffer = await this.#buildFontBuffer(font, sourceBuffer, sourcePath);
|
|
1247
|
-
await Bun.write(outputPath, outputBuffer);
|
|
1248
|
-
this.#files.push(outputPath);
|
|
1249
|
-
faceCss.push(this.#buildOptimizedFontFaceRule(font, face));
|
|
1250
|
-
const fallbackCss = await this.#buildFontaineFallbackCss(font, face, outputPath);
|
|
1251
|
-
if (fallbackCss)
|
|
1252
|
-
faceCss.push(fallbackCss);
|
|
1253
|
-
}
|
|
1254
|
-
if (faceCss.length > 0)
|
|
1255
|
-
this.#cssParts.push(...faceCss, this.#buildRootVariableRule(font));
|
|
986
|
+
static #toImportSpecifier(moduleAbsPath) {
|
|
987
|
+
return path7.resolve(moduleAbsPath).split(path7.sep).join("/");
|
|
1256
988
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
|
|
992
|
+
var VIRTUAL_PAGES_ENTRY = "akan-pages-entry";
|
|
993
|
+
|
|
994
|
+
class PagesBundleBuilder {
|
|
995
|
+
#app;
|
|
996
|
+
#command;
|
|
997
|
+
#pageEntries;
|
|
998
|
+
#started = Date.now();
|
|
999
|
+
constructor(app, command = "start", pageEntries) {
|
|
1000
|
+
this.#app = app;
|
|
1001
|
+
this.#command = command;
|
|
1002
|
+
this.#pageEntries = pageEntries;
|
|
1003
|
+
}
|
|
1004
|
+
async build() {
|
|
1005
|
+
const akanConfig = await this.#app.getConfig();
|
|
1006
|
+
const resolvedEntries = this.#pageEntries ?? await resolveSsrPageEntriesForApp(this.#app, await this.#app.getPageKeys());
|
|
1007
|
+
const entrySource = PagesEntrySourceGenerator.generate(resolvedEntries);
|
|
1008
|
+
const workspaceRoot = this.#app.workspace.workspaceRoot;
|
|
1009
|
+
const result = await Bun.build({
|
|
1010
|
+
entrypoints: [VIRTUAL_PAGES_ENTRY],
|
|
1011
|
+
outdir: `${this.#artifactDir}/server`,
|
|
1012
|
+
target: "bun",
|
|
1013
|
+
format: "esm",
|
|
1014
|
+
splitting: this.#splitting,
|
|
1015
|
+
minify: this.#command === "build",
|
|
1016
|
+
naming: {
|
|
1017
|
+
entry: "pages-[hash].[ext]",
|
|
1018
|
+
chunk: "chunks/[name]-[hash].[ext]",
|
|
1019
|
+
asset: "assets/[name]-[hash].[ext]"
|
|
1020
|
+
},
|
|
1021
|
+
define: this.#define(),
|
|
1022
|
+
plugins: [
|
|
1023
|
+
PagesBundleBuilder.createPagesEntryPlugin(entrySource),
|
|
1024
|
+
PagesBundleBuilder.createCssStubPlugin(),
|
|
1025
|
+
PagesBundleBuilder.createServerUseClientFetchPlugin(),
|
|
1026
|
+
await createExternalizeFrameworkPlugin({ app: this.#app, extra: akanConfig.externalLibs }),
|
|
1027
|
+
akanConfig.barrelImports.length > 0 ? await createBarrelImportsPlugin(this.#app, {
|
|
1028
|
+
pipeAfter: (source, args) => transformUseClient(source, {
|
|
1029
|
+
path: args.path,
|
|
1030
|
+
workspaceRoot
|
|
1031
|
+
})
|
|
1032
|
+
}) : createUseClientBundlePlugin({ workspaceRoot })
|
|
1033
|
+
]
|
|
1034
|
+
});
|
|
1035
|
+
if (!result.success)
|
|
1036
|
+
throw new AggregateError(result.logs, "[PagesBundleBuilder] Bun.build failed");
|
|
1037
|
+
const entryArtifact = result.outputs.find((a) => a.kind === "entry-point");
|
|
1038
|
+
if (!entryArtifact)
|
|
1039
|
+
throw new Error("[PagesBundleBuilder] Bun.build emitted no entry-point artifact");
|
|
1040
|
+
const bundlePath = path8.resolve(entryArtifact.path);
|
|
1041
|
+
const buildId = Date.now();
|
|
1042
|
+
const outputBytes = result.outputs.reduce((sum, output) => sum + output.size, 0);
|
|
1043
|
+
const chunkCount = result.outputs.filter((output) => output.kind === "chunk").length;
|
|
1044
|
+
this.#app.verbose(`[PagesBundleBuilder] ${path8.basename(bundlePath)} emitted in ${Date.now() - this.#started}ms splitting=${this.#splitting} entry=${entryArtifact.size} bytes outputs=${result.outputs.length} chunks=${chunkCount} total=${outputBytes} bytes`);
|
|
1045
|
+
return {
|
|
1046
|
+
bundlePath,
|
|
1047
|
+
buildId,
|
|
1048
|
+
splitting: this.#splitting,
|
|
1049
|
+
entryBytes: entryArtifact.size,
|
|
1050
|
+
outputBytes,
|
|
1051
|
+
outputCount: result.outputs.length,
|
|
1052
|
+
chunkCount
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
get #artifactDir() {
|
|
1056
|
+
return `${this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath}/.akan/artifact`;
|
|
1057
|
+
}
|
|
1058
|
+
get #splitting() {
|
|
1059
|
+
return process.env.AKAN_SERVER_PAGES_SPLITTING === "1";
|
|
1060
|
+
}
|
|
1061
|
+
#define() {
|
|
1062
|
+
const nodeEnv = this.#command === "build" ? "production" : "development";
|
|
1063
|
+
return {
|
|
1064
|
+
"process.env.NODE_ENV": JSON.stringify(nodeEnv),
|
|
1065
|
+
"process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("ssr"),
|
|
1066
|
+
...Object.fromEntries(Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]))
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
static createPagesEntryPlugin(source) {
|
|
1070
|
+
return {
|
|
1071
|
+
name: "akan-pages-entry",
|
|
1072
|
+
setup(build) {
|
|
1073
|
+
build.onResolve({ filter: /^akan-pages-entry$/ }, () => ({
|
|
1074
|
+
path: VIRTUAL_PAGES_ENTRY,
|
|
1075
|
+
namespace: "akan-virtual"
|
|
1076
|
+
}));
|
|
1077
|
+
build.onLoad({ filter: /^akan-pages-entry$/, namespace: "akan-virtual" }, () => ({
|
|
1078
|
+
contents: source,
|
|
1079
|
+
loader: "tsx"
|
|
1080
|
+
}));
|
|
1274
1081
|
}
|
|
1275
|
-
}
|
|
1276
|
-
return fonts;
|
|
1082
|
+
};
|
|
1277
1083
|
}
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
return false;
|
|
1287
|
-
if (ts2.isArrayLiteralExpression(node))
|
|
1288
|
-
return node.elements.map((element) => this.#literalToValue(element));
|
|
1289
|
-
if (ts2.isObjectLiteralExpression(node)) {
|
|
1290
|
-
const obj = {};
|
|
1291
|
-
for (const prop of node.properties) {
|
|
1292
|
-
if (!ts2.isPropertyAssignment(prop))
|
|
1293
|
-
continue;
|
|
1294
|
-
const name = this.#getPropertyName(prop.name);
|
|
1295
|
-
if (!name)
|
|
1296
|
-
continue;
|
|
1297
|
-
obj[name] = this.#literalToValue(prop.initializer);
|
|
1084
|
+
static createCssStubPlugin() {
|
|
1085
|
+
return {
|
|
1086
|
+
name: "akan-css-stub",
|
|
1087
|
+
setup(build) {
|
|
1088
|
+
build.onLoad({ filter: /\.css$/ }, () => ({
|
|
1089
|
+
contents: "",
|
|
1090
|
+
loader: "js"
|
|
1091
|
+
}));
|
|
1298
1092
|
}
|
|
1299
|
-
|
|
1300
|
-
}
|
|
1301
|
-
if (ts2.isAsExpression(node) || ts2.isSatisfiesExpression(node) || ts2.isParenthesizedExpression(node)) {
|
|
1302
|
-
return this.#literalToValue(node.expression);
|
|
1303
|
-
}
|
|
1304
|
-
return;
|
|
1093
|
+
};
|
|
1305
1094
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1095
|
+
static createServerUseClientFetchPlugin() {
|
|
1096
|
+
return {
|
|
1097
|
+
name: "akan-server-use-client-fetch",
|
|
1098
|
+
setup(build) {
|
|
1099
|
+
build.onLoad({ filter: /[/\\]lib[/\\]useClient\.(ts|tsx|js|jsx)$/ }, async (args) => {
|
|
1100
|
+
const source = await Bun.file(args.path).text();
|
|
1101
|
+
const transformed = PagesBundleBuilder.transformServerUseClientFetchSource(source);
|
|
1102
|
+
if (transformed === source)
|
|
1103
|
+
return;
|
|
1104
|
+
return { contents: transformed, loader: loaderFor2(args.path) };
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1310
1108
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
return [...map.values()];
|
|
1109
|
+
static transformServerUseClientFetchSource(source) {
|
|
1110
|
+
if (!source.includes(`with { type: "macro" }`) || !source.includes("FetchClient.build"))
|
|
1111
|
+
return source;
|
|
1112
|
+
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 });");
|
|
1316
1113
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1114
|
+
}
|
|
1115
|
+
function loaderFor2(absPath) {
|
|
1116
|
+
if (absPath.endsWith(".tsx"))
|
|
1117
|
+
return "tsx";
|
|
1118
|
+
if (absPath.endsWith(".jsx"))
|
|
1119
|
+
return "jsx";
|
|
1120
|
+
if (absPath.endsWith(".ts"))
|
|
1121
|
+
return "ts";
|
|
1122
|
+
return "js";
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// pkgs/@akanjs/devkit/frontendBuild/csrArtifactBuilder.ts
|
|
1126
|
+
import { mkdir, rm, unlink } from "fs/promises";
|
|
1127
|
+
import path9 from "path";
|
|
1128
|
+
class CsrArtifactBuilder {
|
|
1129
|
+
#app;
|
|
1130
|
+
#command;
|
|
1131
|
+
#lang;
|
|
1132
|
+
constructor(app, command = "start", lang = "en") {
|
|
1133
|
+
this.#app = app;
|
|
1134
|
+
this.#command = command;
|
|
1135
|
+
this.#lang = lang;
|
|
1319
1136
|
}
|
|
1320
|
-
|
|
1321
|
-
|
|
1137
|
+
async build() {
|
|
1138
|
+
const pageKeys = await this.#app.getPageKeys();
|
|
1139
|
+
if (pageKeys.length === 0) {
|
|
1140
|
+
this.#app.log(`[cli] no route files under ${this.#app.cwdPath}/page \u2014 skipping CSR build`);
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1143
|
+
const pageEntries = await resolveSsrPageEntriesForApp(this.#app, pageKeys);
|
|
1144
|
+
const akanConfig = await this.#app.getConfig();
|
|
1145
|
+
const cssAssets = await this.#loadCssAssets();
|
|
1146
|
+
const basePaths = [...akanConfig.basePaths];
|
|
1147
|
+
const htmlBasePaths = basePaths.length > 0 ? basePaths : [""];
|
|
1148
|
+
await rm(this.#outputDir, { recursive: true, force: true });
|
|
1149
|
+
await mkdir(this.#generatedDir, { recursive: true });
|
|
1150
|
+
const generatedFiles = Object.fromEntries((await Promise.all(htmlBasePaths.map(async (basePath) => [
|
|
1151
|
+
this.#createHtmlFile(basePath),
|
|
1152
|
+
await this.#createEntryFile(basePath, CsrArtifactBuilder.pageEntriesForBasePath(pageEntries, basePath, basePaths))
|
|
1153
|
+
]))).flat());
|
|
1154
|
+
const result = await Bun.build({
|
|
1155
|
+
target: "browser",
|
|
1156
|
+
entrypoints: htmlBasePaths.map((basePath) => this.#generatedPath(CsrArtifactBuilder.htmlFilename(basePath))),
|
|
1157
|
+
files: generatedFiles,
|
|
1158
|
+
root: this.#generatedDir,
|
|
1159
|
+
outdir: this.#outputDir,
|
|
1160
|
+
splitting: false,
|
|
1161
|
+
minify: true,
|
|
1162
|
+
env: "AKAN_PUBLIC_*",
|
|
1163
|
+
define: this.#define(),
|
|
1164
|
+
optimizeImports: akanConfig.optimizeImports,
|
|
1165
|
+
plugins: [PagesBundleBuilder.createCssStubPlugin()]
|
|
1166
|
+
});
|
|
1167
|
+
if (!result.success) {
|
|
1168
|
+
const logs = result.logs.map((log) => log.message).join(`
|
|
1169
|
+
`);
|
|
1170
|
+
throw new Error(`[csr-build] failed${logs ? `
|
|
1171
|
+
${logs}` : ""}`);
|
|
1172
|
+
}
|
|
1173
|
+
await this.#inlineCsrArtifacts(cssAssets);
|
|
1174
|
+
this.#app.verbose(`[csr-build] output -> ${this.#outputDir}`);
|
|
1175
|
+
return { outputDir: this.#outputDir };
|
|
1322
1176
|
}
|
|
1323
|
-
|
|
1324
|
-
return
|
|
1177
|
+
static pageEntriesForBasePath(pageEntries, basePath, basePaths) {
|
|
1178
|
+
return pageEntries.filter((entry) => {
|
|
1179
|
+
const entryBasePath = getPageKeyBasePath(entry.key, basePaths);
|
|
1180
|
+
return entryBasePath === null || entryBasePath === basePath;
|
|
1181
|
+
});
|
|
1325
1182
|
}
|
|
1326
|
-
|
|
1327
|
-
return
|
|
1183
|
+
static htmlFilename(basePath) {
|
|
1184
|
+
return `${basePath || "index"}.html`;
|
|
1328
1185
|
}
|
|
1329
|
-
|
|
1330
|
-
return
|
|
1186
|
+
static entryFilename(basePath) {
|
|
1187
|
+
return `${basePath || "index"}.csr.tsx`;
|
|
1331
1188
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1189
|
+
static basePathOfHtml(htmlPath) {
|
|
1190
|
+
const name = path9.basename(htmlPath, ".html");
|
|
1191
|
+
return name === "index" ? "" : name;
|
|
1334
1192
|
}
|
|
1335
|
-
#
|
|
1336
|
-
|
|
1337
|
-
return font.paths.map((fontPath) => {
|
|
1338
|
-
const style = fontPath.style ?? "normal";
|
|
1339
|
-
return {
|
|
1340
|
-
font,
|
|
1341
|
-
path: fontPath,
|
|
1342
|
-
src: fontPath.src,
|
|
1343
|
-
weight: fontPath.weight,
|
|
1344
|
-
style,
|
|
1345
|
-
optimizedSrc: this.#getOptimizedFontSrc(font, fontPath)
|
|
1346
|
-
};
|
|
1347
|
-
}).filter((face) => enabledStyles.has(face.style));
|
|
1193
|
+
get #outputDir() {
|
|
1194
|
+
return path9.join(this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath, this.#command === "build" ? "csr" : ".akan/artifact/csr");
|
|
1348
1195
|
}
|
|
1349
|
-
#
|
|
1350
|
-
|
|
1351
|
-
const hash = this.#hashFontConfig({
|
|
1352
|
-
name: font.name,
|
|
1353
|
-
src: fontPath.src,
|
|
1354
|
-
weight: fontPath.weight,
|
|
1355
|
-
style,
|
|
1356
|
-
display: font.display,
|
|
1357
|
-
subset: font.subset,
|
|
1358
|
-
subsets: this.#getFontSubsets(font),
|
|
1359
|
-
subsetText: font.subsetText,
|
|
1360
|
-
subsetFiles: font.subsetFiles,
|
|
1361
|
-
declarations: [...font.declarations ?? [], ...fontPath.declarations ?? []]
|
|
1362
|
-
});
|
|
1363
|
-
return `${FONT_URL_PREFIX}/${this.#slugFontPart(font.name)}-${this.#slugFontPart(String(fontPath.weight))}-${style}-${hash}.woff2`;
|
|
1196
|
+
get #generatedDir() {
|
|
1197
|
+
return path9.join(this.#app.cwdPath, ".akan/generated/csr");
|
|
1364
1198
|
}
|
|
1365
|
-
#
|
|
1366
|
-
|
|
1367
|
-
let hash = 2166136261;
|
|
1368
|
-
for (let i = 0;i < input.length; i++) {
|
|
1369
|
-
hash ^= input.charCodeAt(i);
|
|
1370
|
-
hash = Math.imul(hash, 16777619);
|
|
1371
|
-
}
|
|
1372
|
-
return (hash >>> 0).toString(36);
|
|
1199
|
+
get #artifactDir() {
|
|
1200
|
+
return path9.join(this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath, ".akan/artifact");
|
|
1373
1201
|
}
|
|
1374
|
-
#
|
|
1375
|
-
|
|
1376
|
-
return `[${value.map((item) => this.#stableStringify(item)).join(",")}]`;
|
|
1377
|
-
if (value && typeof value === "object") {
|
|
1378
|
-
const entries = Object.entries(value).filter(([, v]) => v !== undefined).sort(([a], [b]) => a.localeCompare(b));
|
|
1379
|
-
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${this.#stableStringify(v)}`).join(",")}}`;
|
|
1380
|
-
}
|
|
1381
|
-
return JSON.stringify(value);
|
|
1382
|
-
}
|
|
1383
|
-
#slugFontPart(value) {
|
|
1384
|
-
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "font";
|
|
1385
|
-
}
|
|
1386
|
-
async#resolveFontSourcePath(src) {
|
|
1387
|
-
if (!src.startsWith("/"))
|
|
1388
|
-
return null;
|
|
1389
|
-
const rel = src.replace(/^\//, "");
|
|
1390
|
-
const candidates = [
|
|
1391
|
-
this.#command === "build" ? path8.join(this.#app.dist.cwdPath, "public", rel) : null,
|
|
1392
|
-
path8.join(this.#app.cwdPath, "public", rel),
|
|
1393
|
-
this.#resolveWorkspacePublicPath(rel)
|
|
1394
|
-
].filter(Boolean);
|
|
1395
|
-
for (const candidate of candidates) {
|
|
1396
|
-
if (await Bun.file(candidate).exists())
|
|
1397
|
-
return candidate;
|
|
1398
|
-
}
|
|
1399
|
-
return null;
|
|
1400
|
-
}
|
|
1401
|
-
async#buildFontBuffer(font, sourceBuffer, sourcePath) {
|
|
1402
|
-
if (font.subset === false)
|
|
1403
|
-
return this.#convertToWoff2(sourceBuffer, sourcePath);
|
|
1404
|
-
const { default: subsetFont } = await import("subset-font");
|
|
1405
|
-
return subsetFont(sourceBuffer, await this.#getSubsetText(font), { targetFormat: "woff2" });
|
|
1202
|
+
#generatedPath(filename) {
|
|
1203
|
+
return path9.join(this.#generatedDir, filename);
|
|
1406
1204
|
}
|
|
1407
|
-
|
|
1408
|
-
const
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1205
|
+
#define() {
|
|
1206
|
+
const nodeEnv = this.#command === "build" ? "production" : "development";
|
|
1207
|
+
return {
|
|
1208
|
+
"process.env.NODE_ENV": JSON.stringify(nodeEnv),
|
|
1209
|
+
"process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("csr"),
|
|
1210
|
+
...Object.fromEntries(Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]))
|
|
1211
|
+
};
|
|
1412
1212
|
}
|
|
1413
|
-
async#
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1213
|
+
async#createEntryFile(basePath, pageEntries) {
|
|
1214
|
+
return [
|
|
1215
|
+
this.#generatedPath(CsrArtifactBuilder.entryFilename(basePath)),
|
|
1216
|
+
`import { bootCsr } from "akanjs/webkit";
|
|
1217
|
+
${await PagesEntrySourceGenerator.generateStatic(pageEntries)}
|
|
1218
|
+
void bootCsr(pages);
|
|
1219
|
+
`
|
|
1220
|
+
];
|
|
1418
1221
|
}
|
|
1419
|
-
#
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1222
|
+
#createHtmlFile(basePath) {
|
|
1223
|
+
return [
|
|
1224
|
+
this.#generatedPath(CsrArtifactBuilder.htmlFilename(basePath)),
|
|
1225
|
+
`<!doctype html>
|
|
1226
|
+
<html lang="${this.#lang}">
|
|
1227
|
+
<head>
|
|
1228
|
+
<meta charset="utf-8" />
|
|
1229
|
+
<title>${this.#app.name}</title>
|
|
1230
|
+
<base href="/" />
|
|
1231
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1232
|
+
</head>
|
|
1233
|
+
<body>
|
|
1234
|
+
<div id="root"></div>
|
|
1235
|
+
<script type="module" src="./${CsrArtifactBuilder.entryFilename(basePath)}"></script>
|
|
1236
|
+
</body>
|
|
1237
|
+
</html>
|
|
1238
|
+
`
|
|
1239
|
+
];
|
|
1431
1240
|
}
|
|
1432
|
-
#
|
|
1433
|
-
const
|
|
1434
|
-
if (
|
|
1435
|
-
return
|
|
1436
|
-
|
|
1241
|
+
async#loadCssAssets() {
|
|
1242
|
+
const artifactFile = Bun.file(path9.join(this.#artifactDir, "base-artifact.json"));
|
|
1243
|
+
if (!await artifactFile.exists())
|
|
1244
|
+
return {};
|
|
1245
|
+
const artifact = await artifactFile.json();
|
|
1246
|
+
return artifact.cssAssets ?? {};
|
|
1437
1247
|
}
|
|
1438
|
-
async#
|
|
1439
|
-
const
|
|
1440
|
-
const
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1248
|
+
async#inlineCsrArtifacts(cssAssets) {
|
|
1249
|
+
const jsFiles = new Set;
|
|
1250
|
+
for (const htmlPath of await this.#htmlOutputPaths()) {
|
|
1251
|
+
const htmlFile = Bun.file(htmlPath);
|
|
1252
|
+
if (!await htmlFile.exists())
|
|
1253
|
+
continue;
|
|
1254
|
+
const basePath = CsrArtifactBuilder.basePathOfHtml(htmlPath);
|
|
1255
|
+
const cssAsset = cssAssets[basePath];
|
|
1256
|
+
if (!cssAsset) {
|
|
1257
|
+
this.#app.logger.warn(`[csr-build] base-artifact.json has no compiled stylesheet for ${basePath || "root"}; ${path9.basename(htmlPath)} ships without CSS`);
|
|
1258
|
+
}
|
|
1259
|
+
const inlined = await this.#inlineHtmlAssets(await htmlFile.text(), htmlPath, cssAsset);
|
|
1260
|
+
for (const filePath of inlined.jsFiles)
|
|
1261
|
+
jsFiles.add(filePath);
|
|
1262
|
+
await Bun.write(htmlPath, inlined.html);
|
|
1263
|
+
}
|
|
1264
|
+
for (const filePath of jsFiles)
|
|
1265
|
+
await unlink(filePath).catch(() => {
|
|
1266
|
+
return;
|
|
1267
|
+
});
|
|
1268
|
+
const remainingAssets = await this.#listOutputFiles((filePath) => filePath.endsWith(".js") || filePath.endsWith(".css"));
|
|
1269
|
+
if (remainingAssets.length > 0) {
|
|
1270
|
+
throw new Error(`[csr-build] expected single-file HTML, but CSR assets remain:
|
|
1271
|
+
${remainingAssets.join(`
|
|
1272
|
+
`)}`);
|
|
1450
1273
|
}
|
|
1451
|
-
return [...parts].join("");
|
|
1452
|
-
}
|
|
1453
|
-
async#getSubsetPresetText(subset) {
|
|
1454
|
-
if (subset === "latin")
|
|
1455
|
-
return this.#rangeText(32, 126);
|
|
1456
|
-
if (subset === "latin-ext")
|
|
1457
|
-
return `${this.#rangeText(32, 126)}${this.#rangeText(160, 591)}`;
|
|
1458
|
-
if (subset === "ks-x-1001")
|
|
1459
|
-
return FontOptimizer.#getKsX1001Text();
|
|
1460
|
-
if (subset === "auto")
|
|
1461
|
-
return this.#collectAutoSubsetText();
|
|
1462
|
-
return "";
|
|
1463
1274
|
}
|
|
1464
|
-
async#
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
for (const root of [...new Set(roots)].sort()) {
|
|
1470
|
-
const isDir = await stat2(root).then((entry) => entry.isDirectory(), () => false);
|
|
1471
|
-
if (!isDir)
|
|
1472
|
-
continue;
|
|
1473
|
-
const filePaths = [];
|
|
1474
|
-
for await (const filePath of glob.scan({ cwd: root, absolute: true }))
|
|
1475
|
-
filePaths.push(filePath);
|
|
1476
|
-
for (const filePath of filePaths.sort())
|
|
1477
|
-
parts.push(await Bun.file(filePath).text());
|
|
1275
|
+
async#inlineHtmlAssets(html, htmlPath, cssAsset) {
|
|
1276
|
+
let next = html;
|
|
1277
|
+
if (cssAsset) {
|
|
1278
|
+
const css = await Bun.file(path9.join(this.#artifactDir, cssAsset.cssRelPath)).text();
|
|
1279
|
+
next = CsrArtifactBuilder.injectBeforeHeadEnd(next, CsrArtifactBuilder.createInlineStyle(css));
|
|
1478
1280
|
}
|
|
1479
|
-
|
|
1281
|
+
const jsFiles = [];
|
|
1282
|
+
next = await CsrArtifactBuilder.replaceModuleScriptSrc(next, async (src) => {
|
|
1283
|
+
const jsPath = CsrArtifactBuilder.resolveHtmlAssetPath(htmlPath, src);
|
|
1284
|
+
jsFiles.push(jsPath);
|
|
1285
|
+
return await Bun.file(jsPath).text();
|
|
1286
|
+
});
|
|
1287
|
+
return { html: next, jsFiles };
|
|
1480
1288
|
}
|
|
1481
|
-
#
|
|
1482
|
-
|
|
1483
|
-
for (let code = start;code <= end; code++)
|
|
1484
|
-
text += String.fromCodePoint(code);
|
|
1485
|
-
return text;
|
|
1289
|
+
async#htmlOutputPaths() {
|
|
1290
|
+
return await this.#listOutputFiles((filePath) => filePath.endsWith(".html"));
|
|
1486
1291
|
}
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
for (let lead = 161;lead <= 254; lead++) {
|
|
1494
|
-
for (let trail = 161;trail <= 254; trail++) {
|
|
1495
|
-
const char = decoder.decode(Uint8Array.of(lead, trail));
|
|
1496
|
-
if (char && char !== "\uFFFD")
|
|
1497
|
-
chars.add(char);
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
FontOptimizer.#ksX1001Text = [...chars].join("");
|
|
1501
|
-
} catch {
|
|
1502
|
-
FontOptimizer.#ksX1001Text = FontOptimizer.#rangeTextStatic(44032, 55203);
|
|
1292
|
+
async#listOutputFiles(predicate) {
|
|
1293
|
+
const glob = new Bun.Glob("**/*");
|
|
1294
|
+
const files = [];
|
|
1295
|
+
for await (const filePath of glob.scan({ cwd: this.#outputDir, absolute: true })) {
|
|
1296
|
+
if (predicate(filePath))
|
|
1297
|
+
files.push(filePath);
|
|
1503
1298
|
}
|
|
1504
|
-
return
|
|
1299
|
+
return files.sort();
|
|
1505
1300
|
}
|
|
1506
|
-
static
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1301
|
+
static injectBeforeHeadEnd(html, snippet) {
|
|
1302
|
+
const scannable = CsrArtifactBuilder.blankEmbeddedContent(html);
|
|
1303
|
+
const headEnd = scannable.search(/<\/head\s*>/i);
|
|
1304
|
+
if (headEnd !== -1)
|
|
1305
|
+
return `${html.slice(0, headEnd)}${snippet}
|
|
1306
|
+
${html.slice(headEnd)}`;
|
|
1307
|
+
const bodyStart = scannable.search(/<body(?:\s|>)/i);
|
|
1308
|
+
if (bodyStart !== -1)
|
|
1309
|
+
return `${html.slice(0, bodyStart)}${snippet}
|
|
1310
|
+
${html.slice(bodyStart)}`;
|
|
1311
|
+
return `${html}
|
|
1312
|
+
${snippet}`;
|
|
1313
|
+
}
|
|
1314
|
+
static blankEmbeddedContent(html) {
|
|
1315
|
+
return html.replace(/<script\b[^>]*>[\s\S]*?<\/script\s*>|<style\b[^>]*>[\s\S]*?<\/style\s*>|<!--[\s\S]*?-->/gi, (match) => " ".repeat(match.length));
|
|
1511
1316
|
}
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
["font-weight", String(face.weight)],
|
|
1517
|
-
["font-style", face.style],
|
|
1518
|
-
["font-display", font.display ?? "swap"],
|
|
1519
|
-
...this.#toDeclarationEntries(font.declarations),
|
|
1520
|
-
...this.#toDeclarationEntries(face.path.declarations)
|
|
1521
|
-
];
|
|
1522
|
-
return `@font-face {
|
|
1523
|
-
${declarations.map(([prop, value]) => ` ${prop}: ${value};`).join(`
|
|
1524
|
-
`)}
|
|
1525
|
-
}`;
|
|
1317
|
+
static createInlineStyle(css) {
|
|
1318
|
+
return `<style data-akan-css="active">
|
|
1319
|
+
${css.replace(/<\/style/gi, "<\\/style")}
|
|
1320
|
+
</style>`;
|
|
1526
1321
|
}
|
|
1527
|
-
async
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
fallbacks: font.fallbacks ?? {},
|
|
1537
|
-
metrics: { ...metrics, category: font.category }
|
|
1538
|
-
});
|
|
1539
|
-
const css = [];
|
|
1540
|
-
for (let i = fallbacks.length - 1;i >= 0; i--) {
|
|
1541
|
-
const fallback = fallbacks[i];
|
|
1542
|
-
const fallbackMetrics = await getMetricsForFamily(fallback);
|
|
1543
|
-
if (!fallbackMetrics)
|
|
1322
|
+
static async replaceModuleScriptSrc(html, loadScript) {
|
|
1323
|
+
const scriptRe = /<script\b(?=[^>]*\btype=["']module["'])(?=[^>]*\bsrc=["']([^"']+)["'])[^>]*>\s*<\/script>/gi;
|
|
1324
|
+
let result = "";
|
|
1325
|
+
let lastIndex = 0;
|
|
1326
|
+
let matched = false;
|
|
1327
|
+
for (const match of html.matchAll(scriptRe)) {
|
|
1328
|
+
const full = match[0];
|
|
1329
|
+
const src = match[1];
|
|
1330
|
+
if (match.index === undefined || !src)
|
|
1544
1331
|
continue;
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
}));
|
|
1332
|
+
matched = true;
|
|
1333
|
+
result += html.slice(lastIndex, match.index);
|
|
1334
|
+
result += `<script type="module">
|
|
1335
|
+
${CsrArtifactBuilder.escapeInlineScript(await loadScript(src))}
|
|
1336
|
+
</script>`;
|
|
1337
|
+
lastIndex = match.index + full.length;
|
|
1552
1338
|
}
|
|
1553
|
-
|
|
1339
|
+
if (!matched)
|
|
1340
|
+
return html;
|
|
1341
|
+
return result + html.slice(lastIndex);
|
|
1554
1342
|
}
|
|
1555
|
-
|
|
1556
|
-
return
|
|
1343
|
+
static escapeInlineScript(source) {
|
|
1344
|
+
return source.replace(/<\/script/gi, "<\\/script");
|
|
1557
1345
|
}
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
for (const font of fonts) {
|
|
1562
|
-
const className = font.className || `font-${font.name}`;
|
|
1563
|
-
const selector = `.${this.#escapeCssClassName(className)}`;
|
|
1564
|
-
const rule = `${selector} { font-family: var(${this.#getFontVariableName(font)}); }`;
|
|
1565
|
-
if (seen.has(rule))
|
|
1566
|
-
continue;
|
|
1567
|
-
seen.add(rule);
|
|
1568
|
-
rules.push(rule);
|
|
1346
|
+
static resolveHtmlAssetPath(htmlPath, src) {
|
|
1347
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(src) || src.startsWith("//")) {
|
|
1348
|
+
throw new Error(`[csr-build] cannot inline external script: ${src}`);
|
|
1569
1349
|
}
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
}
|
|
1573
|
-
#escapeCssClassName(value) {
|
|
1574
|
-
return value.replace(/^-?\d|[^a-zA-Z0-9_-]/g, (part) => [...part].map((char) => `\\${char.codePointAt(0)?.toString(16)} `).join(""));
|
|
1575
|
-
}
|
|
1576
|
-
#toDeclarationEntries(declarations = []) {
|
|
1577
|
-
return declarations.map((declaration) => [declaration.prop, declaration.value]);
|
|
1578
|
-
}
|
|
1579
|
-
#quote(value) {
|
|
1580
|
-
return JSON.stringify(value);
|
|
1350
|
+
const normalized = src.startsWith("/") ? src.slice(1) : src;
|
|
1351
|
+
return path9.resolve(path9.dirname(htmlPath), normalized);
|
|
1581
1352
|
}
|
|
1582
1353
|
}
|
|
1583
1354
|
|
|
1584
|
-
// pkgs/@akanjs/devkit/frontendBuild/
|
|
1585
|
-
import {
|
|
1586
|
-
import
|
|
1587
|
-
import
|
|
1588
|
-
var
|
|
1589
|
-
var
|
|
1355
|
+
// pkgs/@akanjs/devkit/frontendBuild/fontOptimizer.ts
|
|
1356
|
+
import { mkdir as mkdir2, stat as stat2 } from "fs/promises";
|
|
1357
|
+
import path10 from "path";
|
|
1358
|
+
import ts from "typescript";
|
|
1359
|
+
var FONT_URL_PREFIX = "/_akan/fonts";
|
|
1360
|
+
var DEFAULT_FONT_SUBSETS = ["latin"];
|
|
1590
1361
|
|
|
1591
|
-
class
|
|
1362
|
+
class FontOptimizer {
|
|
1592
1363
|
#app;
|
|
1593
|
-
#
|
|
1594
|
-
#publicRoot;
|
|
1364
|
+
#command;
|
|
1595
1365
|
#artifactRoot;
|
|
1596
|
-
|
|
1366
|
+
#files = [];
|
|
1367
|
+
#cssParts = [];
|
|
1368
|
+
#woff2Ready = null;
|
|
1369
|
+
static #ksX1001Text = null;
|
|
1370
|
+
static #cacheVersion = 2;
|
|
1371
|
+
constructor(app, command = "start") {
|
|
1597
1372
|
this.#app = app;
|
|
1598
|
-
this.#
|
|
1599
|
-
this.#
|
|
1600
|
-
this.#artifactRoot = path9.join(app.dist.cwdPath, ".akan/artifact");
|
|
1373
|
+
this.#command = command;
|
|
1374
|
+
this.#artifactRoot = path10.join(command === "build" ? app.dist.cwdPath : app.cwdPath, ".akan/artifact");
|
|
1601
1375
|
}
|
|
1602
|
-
async
|
|
1603
|
-
const
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
return
|
|
1609
|
-
const fonts = await new FontOptimizer(this.#app, "build").discoverFonts();
|
|
1610
|
-
const unoptimizedSrcs = new Set;
|
|
1611
|
-
const optimizedSrcs = new Set;
|
|
1612
|
-
for (const font of fonts) {
|
|
1613
|
-
const target = isFontOptimizationEnabled(font) ? optimizedSrcs : unoptimizedSrcs;
|
|
1614
|
-
for (const fontPath of font.paths)
|
|
1615
|
-
target.add(path9.posix.basename(fontPath.src));
|
|
1616
|
-
}
|
|
1617
|
-
const result = { removed: [], kept: [], freedBytes: 0 };
|
|
1618
|
-
const undecided = [];
|
|
1619
|
-
for (const candidate of candidates) {
|
|
1620
|
-
const reason = this.#staticKeepReason(candidate, unoptimizedSrcs);
|
|
1621
|
-
if (reason)
|
|
1622
|
-
result.kept.push({ file: candidate.rel, bytes: candidate.bytes, reason });
|
|
1623
|
-
else
|
|
1624
|
-
undecided.push(candidate);
|
|
1376
|
+
async optimize() {
|
|
1377
|
+
const fonts = await this.discoverFonts();
|
|
1378
|
+
const cacheKey = await this.#buildCacheKey(fonts);
|
|
1379
|
+
const cached = cacheKey ? await this.#readCache(cacheKey) : null;
|
|
1380
|
+
if (cached) {
|
|
1381
|
+
this.#app.verbose(`[font] reused ${cached.files.length} cached file(s); skipped subsetting`);
|
|
1382
|
+
return { css: cached.css, fonts, files: cached.files };
|
|
1625
1383
|
}
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
if (referrer)
|
|
1631
|
-
result.kept.push({ file: candidate.rel, bytes: candidate.bytes, reason: { kind: "referenced", referrer } });
|
|
1632
|
-
else
|
|
1633
|
-
result.removed.push({ file: candidate.rel, bytes: candidate.bytes });
|
|
1634
|
-
}
|
|
1384
|
+
for (const font of fonts) {
|
|
1385
|
+
if (!this.#isFontOptimizationEnabled(font))
|
|
1386
|
+
continue;
|
|
1387
|
+
await this.#optimizeFont(font);
|
|
1635
1388
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1389
|
+
const fontUtilityCss = this.#buildFontUtilityRules(fonts);
|
|
1390
|
+
if (fontUtilityCss)
|
|
1391
|
+
this.#cssParts.push(fontUtilityCss);
|
|
1392
|
+
const result = { css: this.#cssParts.join(`
|
|
1393
|
+
`), fonts, files: this.#files };
|
|
1394
|
+
if (cacheKey)
|
|
1395
|
+
await this.#writeCache(cacheKey, result);
|
|
1639
1396
|
return result;
|
|
1640
1397
|
}
|
|
1641
|
-
#
|
|
1642
|
-
|
|
1643
|
-
return { kind: "unoptimized" };
|
|
1644
|
-
for (const glob of this.#keepGlobs) {
|
|
1645
|
-
if (new Bun.Glob(glob).match(candidate.rel))
|
|
1646
|
-
return { kind: "declared", glob };
|
|
1647
|
-
}
|
|
1648
|
-
return null;
|
|
1398
|
+
get #cachePath() {
|
|
1399
|
+
return path10.join(this.#artifactRoot, "fontCache.json");
|
|
1649
1400
|
}
|
|
1650
|
-
async#
|
|
1651
|
-
const
|
|
1652
|
-
for
|
|
1653
|
-
|
|
1654
|
-
if (!entry?.isFile())
|
|
1401
|
+
async#buildCacheKey(fonts) {
|
|
1402
|
+
const sources = [];
|
|
1403
|
+
for (const font of fonts) {
|
|
1404
|
+
if (!this.#isFontOptimizationEnabled(font))
|
|
1655
1405
|
continue;
|
|
1656
|
-
|
|
1406
|
+
for (const face of this.#getFontFaces(font)) {
|
|
1407
|
+
const sourcePath = await this.#resolveFontSourcePath(face.src);
|
|
1408
|
+
const stamp = sourcePath ? await this.#fileStamp(sourcePath) : null;
|
|
1409
|
+
if (!stamp)
|
|
1410
|
+
return null;
|
|
1411
|
+
sources.push({ optimizedSrc: face.optimizedSrc, ...stamp });
|
|
1412
|
+
}
|
|
1413
|
+
for (const filePath of font.subsetFiles ?? []) {
|
|
1414
|
+
const abs = path10.isAbsolute(filePath) ? filePath : path10.join(this.#app.cwdPath, filePath);
|
|
1415
|
+
const stamp = await this.#fileStamp(abs);
|
|
1416
|
+
if (!stamp)
|
|
1417
|
+
return null;
|
|
1418
|
+
sources.push({ subsetFile: filePath, ...stamp });
|
|
1419
|
+
}
|
|
1420
|
+
if (this.#getFontSubsets(font).includes("auto"))
|
|
1421
|
+
sources.push({ autoSubsetText: this.#cacheDigest(await this.#collectAutoSubsetText()) });
|
|
1657
1422
|
}
|
|
1658
|
-
return
|
|
1423
|
+
return this.#cacheDigest({ version: FontOptimizer.#cacheVersion, fonts, sources });
|
|
1659
1424
|
}
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
{
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
{ dir: path9.join(this.#artifactRoot, "server"), label: ".akan/artifact/server", needles: bundled },
|
|
1670
|
-
{ dir: path9.join(this.#app.dist.cwdPath, "csr"), label: "csr", needles: bundled }
|
|
1671
|
-
];
|
|
1672
|
-
for (const root of roots) {
|
|
1673
|
-
const pending = root.needles.filter((candidate) => !referrers.has(candidate.rel));
|
|
1674
|
-
if (!pending.length || !await this.#isDirectory(root.dir))
|
|
1675
|
-
continue;
|
|
1676
|
-
await this.#scanRoot(root, pending, referrers);
|
|
1425
|
+
#cacheDigest(value) {
|
|
1426
|
+
return new Bun.CryptoHasher("sha256").update(this.#stableStringify(value)).digest("hex");
|
|
1427
|
+
}
|
|
1428
|
+
async#fileStamp(filePath) {
|
|
1429
|
+
try {
|
|
1430
|
+
const stats = await stat2(filePath);
|
|
1431
|
+
return { mtimeMs: Math.round(stats.mtimeMs), size: stats.size };
|
|
1432
|
+
} catch {
|
|
1433
|
+
return null;
|
|
1677
1434
|
}
|
|
1678
|
-
return referrers;
|
|
1679
1435
|
}
|
|
1680
|
-
async#
|
|
1681
|
-
const
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
for
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
return;
|
|
1689
|
-
const text = await Bun.file(path9.join(root.dir, rel)).text().catch(() => "");
|
|
1690
|
-
if (!text)
|
|
1691
|
-
continue;
|
|
1692
|
-
for (const needle of remaining) {
|
|
1693
|
-
if (needle.forms.some((form) => text.includes(form)))
|
|
1694
|
-
referrers.set(needle.rel, `${root.label}/${rel.split(path9.sep).join("/")}`);
|
|
1695
|
-
}
|
|
1436
|
+
async#readCache(key) {
|
|
1437
|
+
const cache = await Bun.file(this.#cachePath).json().catch(() => null);
|
|
1438
|
+
if (cache?.version !== FontOptimizer.#cacheVersion || cache.key !== key)
|
|
1439
|
+
return null;
|
|
1440
|
+
const files = cache.files.map((relativePath) => path10.join(this.#artifactRoot, relativePath));
|
|
1441
|
+
for (const filePath of files) {
|
|
1442
|
+
if (!await Bun.file(filePath).exists())
|
|
1443
|
+
return null;
|
|
1696
1444
|
}
|
|
1445
|
+
return { css: cache.css, files };
|
|
1697
1446
|
}
|
|
1698
|
-
async#
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1447
|
+
async#writeCache(key, result) {
|
|
1448
|
+
const cache = {
|
|
1449
|
+
version: FontOptimizer.#cacheVersion,
|
|
1450
|
+
key,
|
|
1451
|
+
css: result.css,
|
|
1452
|
+
files: result.files.map((filePath) => path10.relative(this.#artifactRoot, filePath))
|
|
1453
|
+
};
|
|
1454
|
+
await Bun.write(this.#cachePath, JSON.stringify(cache));
|
|
1455
|
+
}
|
|
1456
|
+
async discoverFonts() {
|
|
1457
|
+
const pageKeys = await this.#app.getPageKeys();
|
|
1458
|
+
const fonts = [];
|
|
1459
|
+
await Promise.all(pageKeys.map(async (key) => {
|
|
1460
|
+
const filePath = path10.resolve(this.#app.cwdPath, "page", key);
|
|
1461
|
+
const file = Bun.file(filePath);
|
|
1462
|
+
if (!await file.exists())
|
|
1703
1463
|
return;
|
|
1704
|
-
|
|
1464
|
+
const source = await file.text();
|
|
1465
|
+
if (!source.includes("fonts"))
|
|
1466
|
+
return;
|
|
1467
|
+
fonts.push(...this.#extractFontsExport(source, filePath));
|
|
1468
|
+
}));
|
|
1469
|
+
return this.#dedupeFonts(fonts);
|
|
1705
1470
|
}
|
|
1706
|
-
#
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
this.#app.verbose(`[font-prune] kept public/${file} \u2014 ${FontPruner.describe(reason)}`);
|
|
1713
|
-
const byReferrer = new Map;
|
|
1714
|
-
for (const { bytes, reason } of result.kept) {
|
|
1715
|
-
if (reason.kind !== "referenced")
|
|
1471
|
+
async#optimizeFont(font) {
|
|
1472
|
+
const faceCss = [];
|
|
1473
|
+
for (const face of this.#getFontFaces(font)) {
|
|
1474
|
+
const sourcePath = await this.#resolveFontSourcePath(face.src);
|
|
1475
|
+
if (!sourcePath) {
|
|
1476
|
+
this.#app.logger.warn(`[font] source not found: ${face.src}`);
|
|
1716
1477
|
continue;
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1478
|
+
}
|
|
1479
|
+
const outputPath = path10.join(this.#artifactRoot, face.optimizedSrc.replace(/^\/_akan\//, ""));
|
|
1480
|
+
await mkdir2(path10.dirname(outputPath), { recursive: true });
|
|
1481
|
+
const sourceBuffer = Buffer.from(await Bun.file(sourcePath).arrayBuffer());
|
|
1482
|
+
const outputBuffer = await this.#buildFontBuffer(font, sourceBuffer, sourcePath);
|
|
1483
|
+
await Bun.write(outputPath, outputBuffer);
|
|
1484
|
+
this.#files.push(outputPath);
|
|
1485
|
+
faceCss.push(this.#buildOptimizedFontFaceRule(font, face));
|
|
1486
|
+
const fallbackCss = await this.#buildFontaineFallbackCss(font, face, outputPath);
|
|
1487
|
+
if (fallbackCss)
|
|
1488
|
+
faceCss.push(fallbackCss);
|
|
1721
1489
|
}
|
|
1722
|
-
if (
|
|
1723
|
-
|
|
1724
|
-
const ranked = [...byReferrer.entries()].sort(([, a], [, b]) => b.bytes - a.bytes);
|
|
1725
|
-
const named = ranked.slice(0, 3).map(([referrer, { files, bytes }]) => `${referrer} (${files}, ${FontPruner.formatBytes(bytes)})`);
|
|
1726
|
-
const rest = ranked.length - named.length;
|
|
1727
|
-
this.#app.logger.info(`[font-prune] kept ${FontPruner.formatBytes(ranked.reduce((total, [, entry]) => total + entry.bytes, 0))} of fonts in public/ because these reference them: ${named.join(", ")}${rest > 0 ? ` and ${rest} more` : ""}`);
|
|
1490
|
+
if (faceCss.length > 0)
|
|
1491
|
+
this.#cssParts.push(...faceCss, this.#buildRootVariableRule(font));
|
|
1728
1492
|
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1493
|
+
#extractFontsExport(source, filePath) {
|
|
1494
|
+
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
1495
|
+
const fonts = [];
|
|
1496
|
+
for (const statement of sourceFile.statements) {
|
|
1497
|
+
if (!ts.isVariableStatement(statement))
|
|
1498
|
+
continue;
|
|
1499
|
+
const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : undefined;
|
|
1500
|
+
const isExported = modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
1501
|
+
if (!isExported)
|
|
1502
|
+
continue;
|
|
1503
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1504
|
+
if (!ts.isIdentifier(declaration.name) || declaration.name.text !== "fonts")
|
|
1505
|
+
continue;
|
|
1506
|
+
const value = declaration.initializer ? this.#literalToValue(declaration.initializer) : null;
|
|
1507
|
+
if (Array.isArray(value)) {
|
|
1508
|
+
fonts.push(...value.map((font) => this.#withFontDefaults(font)));
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
return fonts;
|
|
1735
1513
|
}
|
|
1736
|
-
|
|
1737
|
-
if (
|
|
1738
|
-
return
|
|
1739
|
-
if (
|
|
1740
|
-
return
|
|
1741
|
-
|
|
1514
|
+
#literalToValue(node) {
|
|
1515
|
+
if (ts.isStringLiteralLike(node))
|
|
1516
|
+
return node.text;
|
|
1517
|
+
if (ts.isNumericLiteral(node))
|
|
1518
|
+
return Number(node.text);
|
|
1519
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword)
|
|
1520
|
+
return true;
|
|
1521
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword)
|
|
1522
|
+
return false;
|
|
1523
|
+
if (ts.isArrayLiteralExpression(node))
|
|
1524
|
+
return node.elements.map((element) => this.#literalToValue(element));
|
|
1525
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
1526
|
+
const obj = {};
|
|
1527
|
+
for (const prop of node.properties) {
|
|
1528
|
+
if (!ts.isPropertyAssignment(prop))
|
|
1529
|
+
continue;
|
|
1530
|
+
const name = this.#getPropertyName(prop.name);
|
|
1531
|
+
if (!name)
|
|
1532
|
+
continue;
|
|
1533
|
+
obj[name] = this.#literalToValue(prop.initializer);
|
|
1534
|
+
}
|
|
1535
|
+
return obj;
|
|
1536
|
+
}
|
|
1537
|
+
if (ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isParenthesizedExpression(node)) {
|
|
1538
|
+
return this.#literalToValue(node.expression);
|
|
1539
|
+
}
|
|
1540
|
+
return;
|
|
1742
1541
|
}
|
|
1743
|
-
|
|
1744
|
-
|
|
1542
|
+
#getPropertyName(name) {
|
|
1543
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name))
|
|
1544
|
+
return name.text;
|
|
1545
|
+
return null;
|
|
1745
1546
|
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1547
|
+
#dedupeFonts(fonts) {
|
|
1548
|
+
const map = new Map;
|
|
1549
|
+
for (const font of fonts)
|
|
1550
|
+
map.set(JSON.stringify(font), font);
|
|
1551
|
+
return [...map.values()];
|
|
1552
|
+
}
|
|
1553
|
+
#withFontDefaults(font) {
|
|
1554
|
+
return { ...font, subsets: font.subsets ?? [...DEFAULT_FONT_SUBSETS] };
|
|
1555
|
+
}
|
|
1556
|
+
#getFontSubsets(font) {
|
|
1557
|
+
return font.subsets ?? DEFAULT_FONT_SUBSETS;
|
|
1558
|
+
}
|
|
1559
|
+
#getFontVariableName(font) {
|
|
1560
|
+
return font.variable ?? `--font-${font.name}`;
|
|
1561
|
+
}
|
|
1562
|
+
#getFontFallbackName(font) {
|
|
1563
|
+
return font.fallbackName ?? `${font.name} fallback`;
|
|
1564
|
+
}
|
|
1565
|
+
#isFontOptimizationEnabled(font) {
|
|
1566
|
+
return font.optimize !== false;
|
|
1567
|
+
}
|
|
1568
|
+
#getFontStyles(font) {
|
|
1569
|
+
return font.styles?.length ? font.styles : ["normal"];
|
|
1570
|
+
}
|
|
1571
|
+
#getFontFaces(font) {
|
|
1572
|
+
const enabledStyles = new Set(this.#getFontStyles(font));
|
|
1573
|
+
return font.paths.map((fontPath) => {
|
|
1574
|
+
const style = fontPath.style ?? "normal";
|
|
1575
|
+
return {
|
|
1576
|
+
font,
|
|
1577
|
+
path: fontPath,
|
|
1578
|
+
src: fontPath.src,
|
|
1579
|
+
weight: fontPath.weight,
|
|
1580
|
+
style,
|
|
1581
|
+
optimizedSrc: this.#getOptimizedFontSrc(font, fontPath)
|
|
1582
|
+
};
|
|
1583
|
+
}).filter((face) => enabledStyles.has(face.style));
|
|
1584
|
+
}
|
|
1585
|
+
#getOptimizedFontSrc(font, fontPath) {
|
|
1586
|
+
const style = fontPath.style ?? "normal";
|
|
1587
|
+
const hash = this.#hashFontConfig({
|
|
1588
|
+
name: font.name,
|
|
1589
|
+
src: fontPath.src,
|
|
1590
|
+
weight: fontPath.weight,
|
|
1591
|
+
style,
|
|
1592
|
+
display: font.display,
|
|
1593
|
+
subset: font.subset,
|
|
1594
|
+
subsets: this.#getFontSubsets(font),
|
|
1595
|
+
subsetText: font.subsetText,
|
|
1596
|
+
subsetFiles: font.subsetFiles,
|
|
1597
|
+
declarations: [...font.declarations ?? [], ...fontPath.declarations ?? []]
|
|
1598
|
+
});
|
|
1599
|
+
return `${FONT_URL_PREFIX}/${this.#slugFontPart(font.name)}-${this.#slugFontPart(String(fontPath.weight))}-${style}-${hash}.woff2`;
|
|
1600
|
+
}
|
|
1601
|
+
#hashFontConfig(value) {
|
|
1602
|
+
const input = this.#stableStringify(value);
|
|
1603
|
+
let hash = 2166136261;
|
|
1604
|
+
for (let i = 0;i < input.length; i++) {
|
|
1605
|
+
hash ^= input.charCodeAt(i);
|
|
1606
|
+
hash = Math.imul(hash, 16777619);
|
|
1607
|
+
}
|
|
1608
|
+
return (hash >>> 0).toString(36);
|
|
1609
|
+
}
|
|
1610
|
+
#stableStringify(value) {
|
|
1611
|
+
if (Array.isArray(value))
|
|
1612
|
+
return `[${value.map((item) => this.#stableStringify(item)).join(",")}]`;
|
|
1613
|
+
if (value && typeof value === "object") {
|
|
1614
|
+
const entries = Object.entries(value).filter(([, v]) => v !== undefined).sort(([a], [b]) => a.localeCompare(b));
|
|
1615
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${this.#stableStringify(v)}`).join(",")}}`;
|
|
1616
|
+
}
|
|
1617
|
+
return JSON.stringify(value);
|
|
1618
|
+
}
|
|
1619
|
+
#slugFontPart(value) {
|
|
1620
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "font";
|
|
1621
|
+
}
|
|
1622
|
+
async#resolveFontSourcePath(src) {
|
|
1623
|
+
if (!src.startsWith("/"))
|
|
1785
1624
|
return null;
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
const hit = await firstExisting(candidate);
|
|
1796
|
-
if (hit)
|
|
1797
|
-
return hit;
|
|
1798
|
-
}
|
|
1625
|
+
const rel = src.replace(/^\//, "");
|
|
1626
|
+
const candidates = [
|
|
1627
|
+
this.#command === "build" ? path10.join(this.#app.dist.cwdPath, "public", rel) : null,
|
|
1628
|
+
path10.join(this.#app.cwdPath, "public", rel),
|
|
1629
|
+
this.#resolveWorkspacePublicPath(rel)
|
|
1630
|
+
].filter(Boolean);
|
|
1631
|
+
for (const candidate of candidates) {
|
|
1632
|
+
if (await Bun.file(candidate).exists())
|
|
1633
|
+
return candidate;
|
|
1799
1634
|
}
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1635
|
+
return null;
|
|
1636
|
+
}
|
|
1637
|
+
async#buildFontBuffer(font, sourceBuffer, sourcePath) {
|
|
1638
|
+
if (font.subset === false)
|
|
1639
|
+
return this.#convertToWoff2(sourceBuffer, sourcePath);
|
|
1640
|
+
const { default: subsetFont } = await import("subset-font");
|
|
1641
|
+
return subsetFont(sourceBuffer, await this.#getSubsetText(font), { targetFormat: "woff2" });
|
|
1642
|
+
}
|
|
1643
|
+
async#convertToWoff2(buffer, sourcePath) {
|
|
1644
|
+
const { createFont } = await import("fonteditor-core");
|
|
1645
|
+
await this.#initWoff2();
|
|
1646
|
+
const font = createFont(buffer, { type: this.#getFontType(sourcePath, buffer) });
|
|
1647
|
+
return font.write({ type: "woff2", toBuffer: true });
|
|
1648
|
+
}
|
|
1649
|
+
async#initWoff2() {
|
|
1650
|
+
this.#woff2Ready ??= import("fonteditor-core").then(({ woff2 }) => woff2.init()).then(() => {
|
|
1651
|
+
return;
|
|
1652
|
+
});
|
|
1653
|
+
return this.#woff2Ready;
|
|
1654
|
+
}
|
|
1655
|
+
#getFontType(sourcePath, buffer) {
|
|
1656
|
+
const signature = buffer.toString("ascii", 0, 4);
|
|
1657
|
+
if (signature === "wOFF")
|
|
1658
|
+
return "woff";
|
|
1659
|
+
if (signature === "wOF2")
|
|
1660
|
+
return "woff2";
|
|
1661
|
+
if (signature === "OTTO")
|
|
1662
|
+
return "otf";
|
|
1663
|
+
const ext = path10.extname(sourcePath).slice(1).toLowerCase();
|
|
1664
|
+
if (ext === "otf" || ext === "woff" || ext === "woff2")
|
|
1665
|
+
return ext;
|
|
1666
|
+
return "ttf";
|
|
1667
|
+
}
|
|
1668
|
+
#resolveWorkspacePublicPath(rel) {
|
|
1669
|
+
const [root, dep, ...rest] = rel.split("/");
|
|
1670
|
+
if (root !== "libs" || !dep || rest.length === 0)
|
|
1671
|
+
return null;
|
|
1672
|
+
return path10.join(this.#app.workspace.workspaceRoot, "libs", dep, "public", ...rest);
|
|
1673
|
+
}
|
|
1674
|
+
async#getSubsetText(font) {
|
|
1675
|
+
const parts = new Set;
|
|
1676
|
+
const subsets = this.#getFontSubsets(font);
|
|
1677
|
+
for (const subset of subsets)
|
|
1678
|
+
parts.add(await this.#getSubsetPresetText(subset));
|
|
1679
|
+
if (font.subsetText)
|
|
1680
|
+
parts.add(font.subsetText);
|
|
1681
|
+
for (const filePath of font.subsetFiles ?? []) {
|
|
1682
|
+
const abs = path10.isAbsolute(filePath) ? filePath : path10.join(this.#app.cwdPath, filePath);
|
|
1683
|
+
const file = Bun.file(abs);
|
|
1684
|
+
if (await file.exists())
|
|
1685
|
+
parts.add(await file.text());
|
|
1686
|
+
}
|
|
1687
|
+
return [...parts].join("");
|
|
1688
|
+
}
|
|
1689
|
+
async#getSubsetPresetText(subset) {
|
|
1690
|
+
if (subset === "latin")
|
|
1691
|
+
return this.#rangeText(32, 126);
|
|
1692
|
+
if (subset === "latin-ext")
|
|
1693
|
+
return `${this.#rangeText(32, 126)}${this.#rangeText(160, 591)}`;
|
|
1694
|
+
if (subset === "ks-x-1001")
|
|
1695
|
+
return FontOptimizer.#getKsX1001Text();
|
|
1696
|
+
if (subset === "auto")
|
|
1697
|
+
return this.#collectAutoSubsetText();
|
|
1698
|
+
return "";
|
|
1699
|
+
}
|
|
1700
|
+
async#collectAutoSubsetText() {
|
|
1701
|
+
const libPageRoots = (await this.#app.getPageRoots()).filter((root) => root.keyPrefix).map((root) => root.dir);
|
|
1702
|
+
const roots = [...["page", "ui", "lib"].map((dir) => path10.join(this.#app.cwdPath, dir)), ...libPageRoots];
|
|
1703
|
+
const glob = new Bun.Glob("**/*.{ts,tsx,js,jsx,html,md}");
|
|
1704
|
+
const parts = [];
|
|
1705
|
+
for (const root of [...new Set(roots)].sort()) {
|
|
1706
|
+
const isDir = await stat2(root).then((entry) => entry.isDirectory(), () => false);
|
|
1707
|
+
if (!isDir)
|
|
1804
1708
|
continue;
|
|
1805
|
-
const
|
|
1806
|
-
const
|
|
1807
|
-
|
|
1808
|
-
const
|
|
1809
|
-
|
|
1810
|
-
return hit;
|
|
1709
|
+
const filePaths = [];
|
|
1710
|
+
for await (const filePath of glob.scan({ cwd: root, absolute: true }))
|
|
1711
|
+
filePaths.push(filePath);
|
|
1712
|
+
for (const filePath of filePaths.sort())
|
|
1713
|
+
parts.push(await Bun.file(filePath).text());
|
|
1811
1714
|
}
|
|
1812
|
-
return
|
|
1715
|
+
return parts.join("");
|
|
1813
1716
|
}
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
for (const prefix of RUNTIME_EXTERNAL_PREFIX) {
|
|
1832
|
-
if (spec.startsWith(prefix))
|
|
1833
|
-
return { path: spec, external: true };
|
|
1834
|
-
}
|
|
1835
|
-
for (const prefix of includePrefixes) {
|
|
1836
|
-
if (!spec.startsWith(prefix))
|
|
1837
|
-
continue;
|
|
1838
|
-
const resolved = await resolveWorkspaceSubpath(spec);
|
|
1839
|
-
if (resolved)
|
|
1840
|
-
return { path: resolved };
|
|
1841
|
-
return;
|
|
1717
|
+
#rangeText(start, end) {
|
|
1718
|
+
let text = "";
|
|
1719
|
+
for (let code = start;code <= end; code++)
|
|
1720
|
+
text += String.fromCodePoint(code);
|
|
1721
|
+
return text;
|
|
1722
|
+
}
|
|
1723
|
+
static #getKsX1001Text() {
|
|
1724
|
+
if (FontOptimizer.#ksX1001Text)
|
|
1725
|
+
return FontOptimizer.#ksX1001Text;
|
|
1726
|
+
try {
|
|
1727
|
+
const decoder = new TextDecoder("euc-kr");
|
|
1728
|
+
const chars = new Set;
|
|
1729
|
+
for (let lead = 161;lead <= 254; lead++) {
|
|
1730
|
+
for (let trail = 161;trail <= 254; trail++) {
|
|
1731
|
+
const char = decoder.decode(Uint8Array.of(lead, trail));
|
|
1732
|
+
if (char && char !== "\uFFFD")
|
|
1733
|
+
chars.add(char);
|
|
1842
1734
|
}
|
|
1843
|
-
|
|
1844
|
-
|
|
1735
|
+
}
|
|
1736
|
+
FontOptimizer.#ksX1001Text = [...chars].join("");
|
|
1737
|
+
} catch {
|
|
1738
|
+
FontOptimizer.#ksX1001Text = FontOptimizer.#rangeTextStatic(44032, 55203);
|
|
1845
1739
|
}
|
|
1846
|
-
|
|
1847
|
-
}
|
|
1848
|
-
async function firstExisting(basePath) {
|
|
1849
|
-
if (await Bun.file(basePath).exists())
|
|
1850
|
-
return basePath;
|
|
1851
|
-
for (const ext of CANDIDATE_EXTS) {
|
|
1852
|
-
const candidate = `${basePath}${ext}`;
|
|
1853
|
-
if (await Bun.file(candidate).exists())
|
|
1854
|
-
return candidate;
|
|
1740
|
+
return FontOptimizer.#ksX1001Text;
|
|
1855
1741
|
}
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1742
|
+
static #rangeTextStatic(start, end) {
|
|
1743
|
+
let text = "";
|
|
1744
|
+
for (let code = start;code <= end; code++)
|
|
1745
|
+
text += String.fromCodePoint(code);
|
|
1746
|
+
return text;
|
|
1860
1747
|
}
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
}
|
|
1748
|
+
#buildOptimizedFontFaceRule(font, face) {
|
|
1749
|
+
const declarations = [
|
|
1750
|
+
["font-family", this.#quote(font.name)],
|
|
1751
|
+
["src", `url(${this.#quote(face.optimizedSrc)}) format("woff2")`],
|
|
1752
|
+
["font-weight", String(face.weight)],
|
|
1753
|
+
["font-style", face.style],
|
|
1754
|
+
["font-display", font.display ?? "swap"],
|
|
1755
|
+
...this.#toDeclarationEntries(font.declarations),
|
|
1756
|
+
...this.#toDeclarationEntries(face.path.declarations)
|
|
1757
|
+
];
|
|
1758
|
+
return `@font-face {
|
|
1759
|
+
${declarations.map(([prop, value]) => ` ${prop}: ${value};`).join(`
|
|
1760
|
+
`)}
|
|
1761
|
+
}`;
|
|
1762
|
+
}
|
|
1763
|
+
async#buildFontaineFallbackCss(font, face, outputPath) {
|
|
1764
|
+
if (font.adjustFontFallback === false)
|
|
1765
|
+
return "";
|
|
1766
|
+
const { generateFontFace, getMetricsForFamily, readMetrics, resolveCategoryFallbacks } = await import("fontaine");
|
|
1767
|
+
const metrics = await readMetrics(outputPath).catch(() => null);
|
|
1768
|
+
if (!metrics)
|
|
1769
|
+
return "";
|
|
1770
|
+
const fallbacks = resolveCategoryFallbacks({
|
|
1771
|
+
fontFamily: font.name,
|
|
1772
|
+
fallbacks: font.fallbacks ?? {},
|
|
1773
|
+
metrics: { ...metrics, category: font.category }
|
|
1774
|
+
});
|
|
1775
|
+
const css = [];
|
|
1776
|
+
for (let i = fallbacks.length - 1;i >= 0; i--) {
|
|
1777
|
+
const fallback = fallbacks[i];
|
|
1778
|
+
const fallbackMetrics = await getMetricsForFamily(fallback);
|
|
1779
|
+
if (!fallbackMetrics)
|
|
1780
|
+
continue;
|
|
1781
|
+
css.push(generateFontFace({ ...metrics, category: font.category }, {
|
|
1782
|
+
name: this.#getFontFallbackName(font),
|
|
1783
|
+
font: fallback,
|
|
1784
|
+
metrics: fallbackMetrics,
|
|
1785
|
+
"font-weight": String(face.weight),
|
|
1786
|
+
"font-style": face.style
|
|
1787
|
+
}));
|
|
1886
1788
|
}
|
|
1887
|
-
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1789
|
+
return css.join("");
|
|
1790
|
+
}
|
|
1791
|
+
#buildRootVariableRule(font) {
|
|
1792
|
+
return `:root { ${this.#getFontVariableName(font)}: ${this.#quote(font.name)}, ${this.#quote(this.#getFontFallbackName(font))}; }`;
|
|
1793
|
+
}
|
|
1794
|
+
#buildFontUtilityRules(fonts) {
|
|
1795
|
+
const rules = [];
|
|
1796
|
+
const seen = new Set;
|
|
1797
|
+
for (const font of fonts) {
|
|
1798
|
+
const className = font.className || `font-${font.name}`;
|
|
1799
|
+
const selector = `.${this.#escapeCssClassName(className)}`;
|
|
1800
|
+
const rule = `${selector} { font-family: var(${this.#getFontVariableName(font)}); }`;
|
|
1801
|
+
if (seen.has(rule))
|
|
1802
|
+
continue;
|
|
1803
|
+
seen.add(rule);
|
|
1804
|
+
rules.push(rule);
|
|
1805
|
+
}
|
|
1806
|
+
return rules.join(`
|
|
1807
|
+
`);
|
|
1808
|
+
}
|
|
1809
|
+
#escapeCssClassName(value) {
|
|
1810
|
+
return value.replace(/^-?\d|[^a-zA-Z0-9_-]/g, (part) => [...part].map((char) => `\\${char.codePointAt(0)?.toString(16)} `).join(""));
|
|
1811
|
+
}
|
|
1812
|
+
#toDeclarationEntries(declarations = []) {
|
|
1813
|
+
return declarations.map((declaration) => [declaration.prop, declaration.value]);
|
|
1814
|
+
}
|
|
1815
|
+
#quote(value) {
|
|
1816
|
+
return JSON.stringify(value);
|
|
1817
|
+
}
|
|
1897
1818
|
}
|
|
1898
1819
|
|
|
1899
|
-
// pkgs/@akanjs/devkit/frontendBuild/
|
|
1900
|
-
|
|
1820
|
+
// pkgs/@akanjs/devkit/frontendBuild/fontPruner.ts
|
|
1821
|
+
import { rm as rm2, rmdir, stat as stat3 } from "fs/promises";
|
|
1822
|
+
import path11 from "path";
|
|
1823
|
+
import { isFontOptimizationEnabled } from "akanjs/client";
|
|
1824
|
+
var FONT_GLOB = "**/*.{woff2,woff,ttf,otf,ttc,eot}";
|
|
1825
|
+
var REFERRER_GLOB = "**/*.{css,js,mjs,cjs,json,html,htm,svg,xml,webmanifest}";
|
|
1901
1826
|
|
|
1902
|
-
class
|
|
1827
|
+
class FontPruner {
|
|
1903
1828
|
#app;
|
|
1904
|
-
#
|
|
1905
|
-
#
|
|
1906
|
-
#
|
|
1907
|
-
constructor(app,
|
|
1829
|
+
#keepGlobs;
|
|
1830
|
+
#publicRoot;
|
|
1831
|
+
#artifactRoot;
|
|
1832
|
+
constructor(app, { keepFonts = [] } = {}) {
|
|
1908
1833
|
this.#app = app;
|
|
1909
|
-
this.#
|
|
1910
|
-
this.#
|
|
1834
|
+
this.#keepGlobs = keepFonts;
|
|
1835
|
+
this.#publicRoot = path11.join(app.dist.cwdPath, "public");
|
|
1836
|
+
this.#artifactRoot = path11.join(app.dist.cwdPath, ".akan/artifact");
|
|
1911
1837
|
}
|
|
1912
|
-
async
|
|
1913
|
-
const
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
const
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
const outputBytes = result.outputs.reduce((sum, output) => sum + output.size, 0);
|
|
1951
|
-
const chunkCount = result.outputs.filter((output) => output.kind === "chunk").length;
|
|
1952
|
-
this.#app.verbose(`[PagesBundleBuilder] ${path11.basename(bundlePath)} emitted in ${Date.now() - this.#started}ms splitting=${this.#splitting} entry=${entryArtifact.size} bytes outputs=${result.outputs.length} chunks=${chunkCount} total=${outputBytes} bytes`);
|
|
1953
|
-
return {
|
|
1954
|
-
bundlePath,
|
|
1955
|
-
buildId,
|
|
1956
|
-
splitting: this.#splitting,
|
|
1957
|
-
entryBytes: entryArtifact.size,
|
|
1958
|
-
outputBytes,
|
|
1959
|
-
outputCount: result.outputs.length,
|
|
1960
|
-
chunkCount
|
|
1961
|
-
};
|
|
1838
|
+
async prune() {
|
|
1839
|
+
const empty = { removed: [], kept: [], freedBytes: 0 };
|
|
1840
|
+
if (!await this.#isDirectory(this.#publicRoot))
|
|
1841
|
+
return empty;
|
|
1842
|
+
const candidates = await this.#collectCandidates();
|
|
1843
|
+
if (!candidates.length)
|
|
1844
|
+
return empty;
|
|
1845
|
+
const fonts = await new FontOptimizer(this.#app, "build").discoverFonts();
|
|
1846
|
+
const unoptimizedSrcs = new Set;
|
|
1847
|
+
const optimizedSrcs = new Set;
|
|
1848
|
+
for (const font of fonts) {
|
|
1849
|
+
const target = isFontOptimizationEnabled(font) ? optimizedSrcs : unoptimizedSrcs;
|
|
1850
|
+
for (const fontPath of font.paths)
|
|
1851
|
+
target.add(path11.posix.basename(fontPath.src));
|
|
1852
|
+
}
|
|
1853
|
+
const result = { removed: [], kept: [], freedBytes: 0 };
|
|
1854
|
+
const undecided = [];
|
|
1855
|
+
for (const candidate of candidates) {
|
|
1856
|
+
const reason = this.#staticKeepReason(candidate, unoptimizedSrcs);
|
|
1857
|
+
if (reason)
|
|
1858
|
+
result.kept.push({ file: candidate.rel, bytes: candidate.bytes, reason });
|
|
1859
|
+
else
|
|
1860
|
+
undecided.push(candidate);
|
|
1861
|
+
}
|
|
1862
|
+
if (undecided.length) {
|
|
1863
|
+
const referrers = await this.#findReferrers(undecided, optimizedSrcs);
|
|
1864
|
+
for (const candidate of undecided) {
|
|
1865
|
+
const referrer = referrers.get(candidate.rel);
|
|
1866
|
+
if (referrer)
|
|
1867
|
+
result.kept.push({ file: candidate.rel, bytes: candidate.bytes, reason: { kind: "referenced", referrer } });
|
|
1868
|
+
else
|
|
1869
|
+
result.removed.push({ file: candidate.rel, bytes: candidate.bytes });
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
await this.#remove(result.removed.map(({ file }) => file));
|
|
1873
|
+
result.freedBytes = result.removed.reduce((total, { bytes }) => total + bytes, 0);
|
|
1874
|
+
this.#report(result);
|
|
1875
|
+
return result;
|
|
1962
1876
|
}
|
|
1963
|
-
|
|
1964
|
-
|
|
1877
|
+
#staticKeepReason(candidate, unoptimizedSrcs) {
|
|
1878
|
+
if (unoptimizedSrcs.has(candidate.basename))
|
|
1879
|
+
return { kind: "unoptimized" };
|
|
1880
|
+
for (const glob of this.#keepGlobs) {
|
|
1881
|
+
if (new Bun.Glob(glob).match(candidate.rel))
|
|
1882
|
+
return { kind: "declared", glob };
|
|
1883
|
+
}
|
|
1884
|
+
return null;
|
|
1965
1885
|
}
|
|
1966
|
-
|
|
1967
|
-
|
|
1886
|
+
async#collectCandidates() {
|
|
1887
|
+
const candidates = [];
|
|
1888
|
+
for await (const rel of new Bun.Glob(FONT_GLOB).scan({ cwd: this.#publicRoot, dot: false })) {
|
|
1889
|
+
const entry = await stat3(path11.join(this.#publicRoot, rel)).catch(() => null);
|
|
1890
|
+
if (!entry?.isFile())
|
|
1891
|
+
continue;
|
|
1892
|
+
candidates.push({ rel: rel.split(path11.sep).join("/"), basename: path11.basename(rel), bytes: entry.size });
|
|
1893
|
+
}
|
|
1894
|
+
return candidates;
|
|
1968
1895
|
}
|
|
1969
|
-
#
|
|
1970
|
-
const
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1896
|
+
async#findReferrers(candidates, optimizedSrcs) {
|
|
1897
|
+
const referrers = new Map;
|
|
1898
|
+
const authoritative = candidates;
|
|
1899
|
+
const bundled = candidates.filter((candidate) => !optimizedSrcs.has(candidate.basename));
|
|
1900
|
+
const roots = [
|
|
1901
|
+
{ dir: this.#publicRoot, label: "public", needles: authoritative },
|
|
1902
|
+
{ dir: path11.join(this.#artifactRoot, "styles"), label: ".akan/artifact/styles", needles: authoritative },
|
|
1903
|
+
{ dir: path11.join(this.#artifactRoot, "client"), label: ".akan/artifact/client", needles: bundled },
|
|
1904
|
+
{ dir: path11.join(this.#artifactRoot, "client-ssr"), label: ".akan/artifact/client-ssr", needles: bundled },
|
|
1905
|
+
{ dir: path11.join(this.#artifactRoot, "server"), label: ".akan/artifact/server", needles: bundled },
|
|
1906
|
+
{ dir: path11.join(this.#app.dist.cwdPath, "csr"), label: "csr", needles: bundled }
|
|
1907
|
+
];
|
|
1908
|
+
for (const root of roots) {
|
|
1909
|
+
const pending = root.needles.filter((candidate) => !referrers.has(candidate.rel));
|
|
1910
|
+
if (!pending.length || !await this.#isDirectory(root.dir))
|
|
1911
|
+
continue;
|
|
1912
|
+
await this.#scanRoot(root, pending, referrers);
|
|
1913
|
+
}
|
|
1914
|
+
return referrers;
|
|
1976
1915
|
}
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1916
|
+
async#scanRoot(root, pending, referrers) {
|
|
1917
|
+
const needles = pending.map((candidate) => ({
|
|
1918
|
+
rel: candidate.rel,
|
|
1919
|
+
forms: [...new Set([candidate.basename, encodeURIComponent(candidate.basename)])]
|
|
1920
|
+
}));
|
|
1921
|
+
for await (const rel of new Bun.Glob(REFERRER_GLOB).scan({ cwd: root.dir, dot: false })) {
|
|
1922
|
+
const remaining = needles.filter((needle) => !referrers.has(needle.rel));
|
|
1923
|
+
if (!remaining.length)
|
|
1924
|
+
return;
|
|
1925
|
+
const text = await Bun.file(path11.join(root.dir, rel)).text().catch(() => "");
|
|
1926
|
+
if (!text)
|
|
1927
|
+
continue;
|
|
1928
|
+
for (const needle of remaining) {
|
|
1929
|
+
if (needle.forms.some((form) => text.includes(form)))
|
|
1930
|
+
referrers.set(needle.rel, `${root.label}/${rel.split(path11.sep).join("/")}`);
|
|
1989
1931
|
}
|
|
1990
|
-
}
|
|
1932
|
+
}
|
|
1991
1933
|
}
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
}));
|
|
2000
|
-
}
|
|
2001
|
-
};
|
|
1934
|
+
async#remove(files) {
|
|
1935
|
+
await Promise.all(files.map((file) => rm2(path11.join(this.#publicRoot, file), { force: true })));
|
|
1936
|
+
const dirs = [...new Set(files.map((file) => path11.posix.dirname(file)))].filter((dir) => dir !== "." && dir !== "/").sort((a, b) => b.length - a.length);
|
|
1937
|
+
for (const dir of dirs)
|
|
1938
|
+
await rmdir(path11.join(this.#publicRoot, dir)).catch(() => {
|
|
1939
|
+
return;
|
|
1940
|
+
});
|
|
2002
1941
|
}
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
}
|
|
2015
|
-
|
|
1942
|
+
#report(result) {
|
|
1943
|
+
if (!result.removed.length && !result.kept.length)
|
|
1944
|
+
return;
|
|
1945
|
+
for (const { file, bytes } of result.removed)
|
|
1946
|
+
this.#app.verbose(`[font-prune] dropped public/${file} (${FontPruner.formatBytes(bytes)})`);
|
|
1947
|
+
for (const { file, reason } of result.kept)
|
|
1948
|
+
this.#app.verbose(`[font-prune] kept public/${file} \u2014 ${FontPruner.describe(reason)}`);
|
|
1949
|
+
const byReferrer = new Map;
|
|
1950
|
+
for (const { bytes, reason } of result.kept) {
|
|
1951
|
+
if (reason.kind !== "referenced")
|
|
1952
|
+
continue;
|
|
1953
|
+
const entry = byReferrer.get(reason.referrer) ?? { files: 0, bytes: 0 };
|
|
1954
|
+
entry.files += 1;
|
|
1955
|
+
entry.bytes += bytes;
|
|
1956
|
+
byReferrer.set(reason.referrer, entry);
|
|
1957
|
+
}
|
|
1958
|
+
if (!byReferrer.size)
|
|
1959
|
+
return;
|
|
1960
|
+
const ranked = [...byReferrer.entries()].sort(([, a], [, b]) => b.bytes - a.bytes);
|
|
1961
|
+
const named = ranked.slice(0, 3).map(([referrer, { files, bytes }]) => `${referrer} (${files}, ${FontPruner.formatBytes(bytes)})`);
|
|
1962
|
+
const rest = ranked.length - named.length;
|
|
1963
|
+
this.#app.logger.info(`[font-prune] kept ${FontPruner.formatBytes(ranked.reduce((total, [, entry]) => total + entry.bytes, 0))} of fonts in public/ because these reference them: ${named.join(", ")}${rest > 0 ? ` and ${rest} more` : ""}`);
|
|
2016
1964
|
}
|
|
2017
|
-
static
|
|
2018
|
-
if (
|
|
2019
|
-
return
|
|
2020
|
-
|
|
1965
|
+
static describe(reason) {
|
|
1966
|
+
if (reason.kind === "unoptimized")
|
|
1967
|
+
return "declared with optimize: false, so the runtime CSS serves it";
|
|
1968
|
+
if (reason.kind === "declared")
|
|
1969
|
+
return `kept by assets.keepFonts "${reason.glob}"`;
|
|
1970
|
+
return `referenced by ${reason.referrer}`;
|
|
1971
|
+
}
|
|
1972
|
+
static formatBytes(bytes) {
|
|
1973
|
+
if (bytes < 1024)
|
|
1974
|
+
return `${bytes}B`;
|
|
1975
|
+
if (bytes < 1024 * 1024)
|
|
1976
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
1977
|
+
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
1978
|
+
}
|
|
1979
|
+
async#isDirectory(dir) {
|
|
1980
|
+
return await stat3(dir).then((entry) => entry.isDirectory(), () => false);
|
|
2021
1981
|
}
|
|
2022
|
-
}
|
|
2023
|
-
function loaderFor2(absPath) {
|
|
2024
|
-
if (absPath.endsWith(".tsx"))
|
|
2025
|
-
return "tsx";
|
|
2026
|
-
if (absPath.endsWith(".jsx"))
|
|
2027
|
-
return "jsx";
|
|
2028
|
-
if (absPath.endsWith(".ts"))
|
|
2029
|
-
return "ts";
|
|
2030
|
-
return "js";
|
|
2031
1982
|
}
|
|
2032
1983
|
|
|
2033
1984
|
// pkgs/@akanjs/devkit/frontendBuild/precompressArtifacts.ts
|
|
2034
|
-
import
|
|
1985
|
+
import fs from "fs";
|
|
2035
1986
|
import path12 from "path";
|
|
2036
1987
|
import zlib from "zlib";
|
|
2037
1988
|
var COMPRESSIBLE_EXTS = new Set([".css", ".html", ".js", ".json", ".svg"]);
|
|
@@ -2052,7 +2003,7 @@ async function precompressArtifacts(app) {
|
|
|
2052
2003
|
return result;
|
|
2053
2004
|
}
|
|
2054
2005
|
async function precompressRoot(root, result) {
|
|
2055
|
-
if (!
|
|
2006
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory())
|
|
2056
2007
|
return;
|
|
2057
2008
|
const glob = new Bun.Glob("**/*");
|
|
2058
2009
|
for await (const filePath of glob.scan({ cwd: root, absolute: true })) {
|
|
@@ -2253,4 +2204,4 @@ ${preparedCssText}`).toString(36);
|
|
|
2253
2204
|
}
|
|
2254
2205
|
}
|
|
2255
2206
|
}
|
|
2256
|
-
export { AllRoutesBuilder,
|
|
2207
|
+
export { AllRoutesBuilder, CssCompiler, PagesBundleBuilder, CsrArtifactBuilder, FontOptimizer, FontPruner, precompressArtifacts, SsrBaseArtifactBuilder };
|