@akanjs/cli 3.0.0-alpha.74 → 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.
Files changed (43) hide show
  1. package/.build-stamp +1 -1
  2. package/{agent.command-z0cam80t.js → agent.command-j4744bct.js} +6 -6
  3. package/{application.command-2dct57za.js → application.command-1sq0zwqx.js} +5 -5
  4. package/{applicationBuildRunner-ds8acdfz.js → applicationBuildRunner-8tqhxjd0.js} +2 -2
  5. package/buildBatch.proc.js +3 -3
  6. package/{capacitorApp-md7dnt25.js → capacitorApp-nqnvsbv2.js} +3 -3
  7. package/{cloud.command-8j7xrxsm.js → cloud.command-ajkhg234.js} +8 -8
  8. package/{context.command-xjf4y7em.js → context.command-jxga9cc9.js} +13 -13
  9. package/{guideline.command-yzm0qevv.js → guideline.command-2fts4m5x.js} +3 -3
  10. package/incrementalBuilder.proc.js +2 -2
  11. package/{index-h9cybjep.js → index-004ah39s.js} +1 -1
  12. package/{index-eed7vc38.js → index-0zabybjx.js} +10 -10
  13. package/{index-a341yqdh.js → index-120pzxb2.js} +3 -3
  14. package/{index-61x7qthy.js → index-25qfm3at.js} +1123 -1172
  15. package/{index-9gqzgfsd.js → index-2j5xrm1c.js} +2 -2
  16. package/{index-hvbdre8z.js → index-3qa4f4j7.js} +2 -2
  17. package/{index-te6r1b79.js → index-5yxz0zzc.js} +3 -3
  18. package/{index-akz5xvyw.js → index-6wsjpt7b.js} +1 -1
  19. package/{index-116w1dap.js → index-85rdqkfx.js} +3 -3
  20. package/{index-bmz9crym.js → index-aczwndks.js} +2 -2
  21. package/{index-q0s2dp3y.js → index-aevn8b0n.js} +1 -1
  22. package/{index-f39512t6.js → index-dp66vwr6.js} +117 -24
  23. package/{index-q13s3e01.js → index-j8rp2kdv.js} +3 -3
  24. package/{index-763h7h9g.js → index-mk1x6sad.js} +1 -1
  25. package/{index-ym9g9ggs.js → index-mpka7dq4.js} +1 -1
  26. package/{index-z52nhkjs.js → index-peg7zphh.js} +4 -4
  27. package/{index-y3wyqcsp.js → index-qgzzz44h.js} +5 -5
  28. package/{index-qge1hc70.js → index-qhy7zdmz.js} +2 -2
  29. package/{index-vrra4ttx.js → index-v6p94mz1.js} +4 -4
  30. package/{index-5r4e65hb.js → index-yzsp0jgb.js} +5 -5
  31. package/index.js +19 -19
  32. package/{library.command-w8b53vtt.js → library.command-tfkfhbcb.js} +4 -4
  33. package/{localRegistry.command-ymrgda7e.js → localRegistry.command-rhn2mh9j.js} +7 -7
  34. package/{module.command-7pje37dv.js → module.command-29xg1kzr.js} +6 -6
  35. package/{package.command-jg4qk8ts.js → package.command-j6yn5c7h.js} +4 -4
  36. package/package.json +2 -2
  37. package/{page.command-abbpj96j.js → page.command-vgxetvga.js} +4 -4
  38. package/{primitive.command-jw4yv918.js → primitive.command-k85cvm2z.js} +7 -7
  39. package/{quality.command-06j6qy6n.js → quality.command-9dsv2jw7.js} +3 -3
  40. package/{repair.command-x6resn10.js → repair.command-mzdh4yby.js} +6 -6
  41. package/{scalar.command-mj2vx6rj.js → scalar.command-0cgq46dj.js} +5 -5
  42. package/{workflow.command-pmw2tere.js → workflow.command-2yxpvpg3.js} +10 -10
  43. 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-f39512t6.js";
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 path7 from "path";
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 path6 from "path";
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(path6.resolve(this.#workspaceRoot, repl));
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(path6.resolve(this.#workspaceRoot, replPath + suffix));
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 = path6.dirname(pkgPath);
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(path6.resolve(pkgDir, exportedEntry));
403
+ return await this.#firstExisting(path4.resolve(pkgDir, exportedEntry));
729
404
  if (subpath !== ".")
730
- return await this.#firstExisting(path6.resolve(pkgDir, subpath));
731
- return await this.#firstExisting(path6.resolve(pkgDir, pkg.exports?.["."]?.style || pkg.style || "index.css"));
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, path6.dirname(Bun.main), path6.resolve(path6.dirname(Bun.main), "../..")];
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
- path6.join(this.#workspaceRoot, "pkgs", pkgName, "package.json"),
742
- path6.join(this.#workspaceRoot, "node_modules", pkgName, "package.json"),
743
- path6.join(path6.dirname(Bun.main), "node_modules", pkgName, "package.json"),
744
- path6.join(path6.dirname(Bun.main), "../../", pkgName, "package.json")
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 path6.extname(filePath) === ".css";
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 path7.join(this.#app.cwdPath, ".akan/cache/cssCandidates.json");
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 = path7.join(absPathNoExt, `index${ext}`);
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 = path7.join(this.#app.cwdPath, "page");
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(`${path7.sep}(libs)${path7.sep}`))
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 ${path7.relative(this.#app.cwdPath, cssPath)} is imported by no route and never compiled`);
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) => path7.resolve(this.#app.cwdPath, "page", 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 = path7.dirname(filePath);
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 = path7.join(this.#app.workspace.workspaceRoot, "libs");
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 = path7.relative(libsRoot, filePath);
949
- if (relPath.startsWith("..") || path7.isAbsolute(relPath))
623
+ const relPath = path5.relative(libsRoot, filePath);
624
+ if (relPath.startsWith("..") || path5.isAbsolute(relPath))
950
625
  continue;
951
- const [libName] = relPath.split(path7.sep);
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 = path7.join(libsRoot, libName, "ui/tokens.css");
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 = path7.dirname(cssPath);
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: path7.dirname(p), content };
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 = path7.resolve(fromBase, id);
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: path7.dirname(p), module: mod.default ?? mod };
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 : path7.resolve(fromBase, 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(path7.extname(filePath));
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(path7.sep).join("/").replace(/^\.\//, "");
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/fontOptimizer.ts
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
- class FontOptimizer {
1127
- #app;
1128
- #command;
1129
- #artifactRoot;
1130
- #files = [];
1131
- #cssParts = [];
1132
- #woff2Ready = null;
1133
- static #ksX1001Text = null;
1134
- static #cacheVersion = 2;
1135
- constructor(app, command = "start") {
1136
- this.#app = app;
1137
- this.#command = command;
1138
- this.#artifactRoot = path8.join(command === "build" ? app.dist.cwdPath : app.cwdPath, ".akan/artifact");
1139
- }
1140
- async optimize() {
1141
- const fonts = await this.discoverFonts();
1142
- const cacheKey = await this.#buildCacheKey(fonts);
1143
- const cached = cacheKey ? await this.#readCache(cacheKey) : null;
1144
- if (cached) {
1145
- this.#app.verbose(`[font] reused ${cached.files.length} cached file(s); skipped subsetting`);
1146
- return { css: cached.css, fonts, files: cached.files };
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 font of fonts) {
1149
- if (!this.#isFontOptimizationEnabled(font))
846
+ for (const { pkg, entryFile } of rootEntries) {
847
+ if (spec !== pkg && !spec.startsWith(`${pkg}/`))
1150
848
  continue;
1151
- await this.#optimizeFont(font);
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
- const fontUtilityCss = this.#buildFontUtilityRules(fonts);
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
- async#buildCacheKey(fonts) {
1166
- const sources = [];
1167
- for (const font of fonts) {
1168
- if (!this.#isFontOptimizationEnabled(font))
1169
- continue;
1170
- for (const face of this.#getFontFaces(font)) {
1171
- const sourcePath = await this.#resolveFontSourcePath(face.src);
1172
- const stamp = sourcePath ? await this.#fileStamp(sourcePath) : null;
1173
- if (!stamp)
1174
- return null;
1175
- sources.push({ optimizedSrc: face.optimizedSrc, ...stamp });
1176
- }
1177
- for (const filePath of font.subsetFiles ?? []) {
1178
- const abs = path8.isAbsolute(filePath) ? filePath : path8.join(this.#app.cwdPath, filePath);
1179
- const stamp = await this.#fileStamp(abs);
1180
- if (!stamp)
1181
- return null;
1182
- sources.push({ subsetFile: filePath, ...stamp });
1183
- }
1184
- if (this.#getFontSubsets(font).includes("auto"))
1185
- sources.push({ autoSubsetText: this.#cacheDigest(await this.#collectAutoSubsetText()) });
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
- return this.#cacheDigest({ version: FontOptimizer.#cacheVersion, fonts, sources });
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
- #cacheDigest(value) {
1190
- return new Bun.CryptoHasher("sha256").update(this.#stableStringify(value)).digest("hex");
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
- async#fileStamp(filePath) {
1193
- try {
1194
- const stats = await stat2(filePath);
1195
- return { mtimeMs: Math.round(stats.mtimeMs), size: stats.size };
1196
- } catch {
1197
- return null;
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
- async#readCache(key) {
1201
- const cache = await Bun.file(this.#cachePath).json().catch(() => null);
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
- async#writeCache(key, result) {
1212
- const cache = {
1213
- version: FontOptimizer.#cacheVersion,
1214
- key,
1215
- css: result.css,
1216
- files: result.files.map((filePath) => path8.relative(this.#artifactRoot, filePath))
1217
- };
1218
- await Bun.write(this.#cachePath, JSON.stringify(cache));
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 discoverFonts() {
1221
- const pageKeys = await this.#app.getPageKeys();
1222
- const fonts = [];
1223
- await Promise.all(pageKeys.map(async (key) => {
1224
- const filePath = path8.resolve(this.#app.cwdPath, "page", key);
1225
- const file = Bun.file(filePath);
1226
- if (!await file.exists())
1227
- return;
1228
- const source = await file.text();
1229
- if (!source.includes("fonts"))
1230
- return;
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 this.#dedupeFonts(fonts);
978
+ return `${imports.join(`
979
+ `)}
980
+ export const pages = {
981
+ ${entries.join(`
982
+ `)}
983
+ };
984
+ `;
1234
985
  }
1235
- async#optimizeFont(font) {
1236
- const faceCss = [];
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
- #extractFontsExport(source, filePath) {
1258
- const sourceFile = ts2.createSourceFile(filePath, source, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TSX);
1259
- const fonts = [];
1260
- for (const statement of sourceFile.statements) {
1261
- if (!ts2.isVariableStatement(statement))
1262
- continue;
1263
- const modifiers = ts2.canHaveModifiers(statement) ? ts2.getModifiers(statement) : undefined;
1264
- const isExported = modifiers?.some((modifier) => modifier.kind === ts2.SyntaxKind.ExportKeyword) ?? false;
1265
- if (!isExported)
1266
- continue;
1267
- for (const declaration of statement.declarationList.declarations) {
1268
- if (!ts2.isIdentifier(declaration.name) || declaration.name.text !== "fonts")
1269
- continue;
1270
- const value = declaration.initializer ? this.#literalToValue(declaration.initializer) : null;
1271
- if (Array.isArray(value)) {
1272
- fonts.push(...value.map((font) => this.#withFontDefaults(font)));
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
- #literalToValue(node) {
1279
- if (ts2.isStringLiteralLike(node))
1280
- return node.text;
1281
- if (ts2.isNumericLiteral(node))
1282
- return Number(node.text);
1283
- if (node.kind === ts2.SyntaxKind.TrueKeyword)
1284
- return true;
1285
- if (node.kind === ts2.SyntaxKind.FalseKeyword)
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
- return obj;
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
- #getPropertyName(name) {
1307
- if (ts2.isIdentifier(name) || ts2.isStringLiteral(name) || ts2.isNumericLiteral(name))
1308
- return name.text;
1309
- return null;
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
- #dedupeFonts(fonts) {
1312
- const map = new Map;
1313
- for (const font of fonts)
1314
- map.set(JSON.stringify(font), font);
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
- #withFontDefaults(font) {
1318
- return { ...font, subsets: font.subsets ?? [...DEFAULT_FONT_SUBSETS] };
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
- #getFontSubsets(font) {
1321
- return font.subsets ?? DEFAULT_FONT_SUBSETS;
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
- #getFontVariableName(font) {
1324
- return font.variable ?? `--font-${font.name}`;
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
- #getFontFallbackName(font) {
1327
- return font.fallbackName ?? `${font.name} fallback`;
1183
+ static htmlFilename(basePath) {
1184
+ return `${basePath || "index"}.html`;
1328
1185
  }
1329
- #isFontOptimizationEnabled(font) {
1330
- return font.optimize !== false;
1186
+ static entryFilename(basePath) {
1187
+ return `${basePath || "index"}.csr.tsx`;
1331
1188
  }
1332
- #getFontStyles(font) {
1333
- return font.styles?.length ? font.styles : ["normal"];
1189
+ static basePathOfHtml(htmlPath) {
1190
+ const name = path9.basename(htmlPath, ".html");
1191
+ return name === "index" ? "" : name;
1334
1192
  }
1335
- #getFontFaces(font) {
1336
- const enabledStyles = new Set(this.#getFontStyles(font));
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
- #getOptimizedFontSrc(font, fontPath) {
1350
- const style = fontPath.style ?? "normal";
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
- #hashFontConfig(value) {
1366
- const input = this.#stableStringify(value);
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
- #stableStringify(value) {
1375
- if (Array.isArray(value))
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
- async#convertToWoff2(buffer, sourcePath) {
1408
- const { createFont } = await import("fonteditor-core");
1409
- await this.#initWoff2();
1410
- const font = createFont(buffer, { type: this.#getFontType(sourcePath, buffer) });
1411
- return font.write({ type: "woff2", toBuffer: true });
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#initWoff2() {
1414
- this.#woff2Ready ??= import("fonteditor-core").then(({ woff2 }) => woff2.init()).then(() => {
1415
- return;
1416
- });
1417
- return this.#woff2Ready;
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
- #getFontType(sourcePath, buffer) {
1420
- const signature = buffer.toString("ascii", 0, 4);
1421
- if (signature === "wOFF")
1422
- return "woff";
1423
- if (signature === "wOF2")
1424
- return "woff2";
1425
- if (signature === "OTTO")
1426
- return "otf";
1427
- const ext = path8.extname(sourcePath).slice(1).toLowerCase();
1428
- if (ext === "otf" || ext === "woff" || ext === "woff2")
1429
- return ext;
1430
- return "ttf";
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
- #resolveWorkspacePublicPath(rel) {
1433
- const [root, dep, ...rest] = rel.split("/");
1434
- if (root !== "libs" || !dep || rest.length === 0)
1435
- return null;
1436
- return path8.join(this.#app.workspace.workspaceRoot, "libs", dep, "public", ...rest);
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#getSubsetText(font) {
1439
- const parts = new Set;
1440
- const subsets = this.#getFontSubsets(font);
1441
- for (const subset of subsets)
1442
- parts.add(await this.#getSubsetPresetText(subset));
1443
- if (font.subsetText)
1444
- parts.add(font.subsetText);
1445
- for (const filePath of font.subsetFiles ?? []) {
1446
- const abs = path8.isAbsolute(filePath) ? filePath : path8.join(this.#app.cwdPath, filePath);
1447
- const file = Bun.file(abs);
1448
- if (await file.exists())
1449
- parts.add(await file.text());
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#collectAutoSubsetText() {
1465
- const libPageRoots = (await this.#app.getPageRoots()).filter((root) => root.keyPrefix).map((root) => root.dir);
1466
- const roots = [...["page", "ui", "lib"].map((dir) => path8.join(this.#app.cwdPath, dir)), ...libPageRoots];
1467
- const glob = new Bun.Glob("**/*.{ts,tsx,js,jsx,html,md}");
1468
- const parts = [];
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
- return parts.join("");
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
- #rangeText(start, end) {
1482
- let text = "";
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
- static #getKsX1001Text() {
1488
- if (FontOptimizer.#ksX1001Text)
1489
- return FontOptimizer.#ksX1001Text;
1490
- try {
1491
- const decoder = new TextDecoder("euc-kr");
1492
- const chars = new Set;
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 FontOptimizer.#ksX1001Text;
1299
+ return files.sort();
1505
1300
  }
1506
- static #rangeTextStatic(start, end) {
1507
- let text = "";
1508
- for (let code = start;code <= end; code++)
1509
- text += String.fromCodePoint(code);
1510
- return text;
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
- #buildOptimizedFontFaceRule(font, face) {
1513
- const declarations = [
1514
- ["font-family", this.#quote(font.name)],
1515
- ["src", `url(${this.#quote(face.optimizedSrc)}) format("woff2")`],
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#buildFontaineFallbackCss(font, face, outputPath) {
1528
- if (font.adjustFontFallback === false)
1529
- return "";
1530
- const { generateFontFace, getMetricsForFamily, readMetrics, resolveCategoryFallbacks } = await import("fontaine");
1531
- const metrics = await readMetrics(outputPath).catch(() => null);
1532
- if (!metrics)
1533
- return "";
1534
- const fallbacks = resolveCategoryFallbacks({
1535
- fontFamily: font.name,
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
- css.push(generateFontFace({ ...metrics, category: font.category }, {
1546
- name: this.#getFontFallbackName(font),
1547
- font: fallback,
1548
- metrics: fallbackMetrics,
1549
- "font-weight": String(face.weight),
1550
- "font-style": face.style
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
- return css.join("");
1339
+ if (!matched)
1340
+ return html;
1341
+ return result + html.slice(lastIndex);
1554
1342
  }
1555
- #buildRootVariableRule(font) {
1556
- return `:root { ${this.#getFontVariableName(font)}: ${this.#quote(font.name)}, ${this.#quote(this.#getFontFallbackName(font))}; }`;
1343
+ static escapeInlineScript(source) {
1344
+ return source.replace(/<\/script/gi, "<\\/script");
1557
1345
  }
1558
- #buildFontUtilityRules(fonts) {
1559
- const rules = [];
1560
- const seen = new Set;
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
- return rules.join(`
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/fontPruner.ts
1585
- import { rm as rm2, rmdir, stat as stat3 } from "fs/promises";
1586
- import path9 from "path";
1587
- import { isFontOptimizationEnabled } from "akanjs/client";
1588
- var FONT_GLOB = "**/*.{woff2,woff,ttf,otf,ttc,eot}";
1589
- var REFERRER_GLOB = "**/*.{css,js,mjs,cjs,json,html,htm,svg,xml,webmanifest}";
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 FontPruner {
1362
+ class FontOptimizer {
1592
1363
  #app;
1593
- #keepGlobs;
1594
- #publicRoot;
1364
+ #command;
1595
1365
  #artifactRoot;
1596
- constructor(app, { keepFonts = [] } = {}) {
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.#keepGlobs = keepFonts;
1599
- this.#publicRoot = path9.join(app.dist.cwdPath, "public");
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 prune() {
1603
- const empty = { removed: [], kept: [], freedBytes: 0 };
1604
- if (!await this.#isDirectory(this.#publicRoot))
1605
- return empty;
1606
- const candidates = await this.#collectCandidates();
1607
- if (!candidates.length)
1608
- return empty;
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
- if (undecided.length) {
1627
- const referrers = await this.#findReferrers(undecided, optimizedSrcs);
1628
- for (const candidate of undecided) {
1629
- const referrer = referrers.get(candidate.rel);
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
- await this.#remove(result.removed.map(({ file }) => file));
1637
- result.freedBytes = result.removed.reduce((total, { bytes }) => total + bytes, 0);
1638
- this.#report(result);
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
- #staticKeepReason(candidate, unoptimizedSrcs) {
1642
- if (unoptimizedSrcs.has(candidate.basename))
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#collectCandidates() {
1651
- const candidates = [];
1652
- for await (const rel of new Bun.Glob(FONT_GLOB).scan({ cwd: this.#publicRoot, dot: false })) {
1653
- const entry = await stat3(path9.join(this.#publicRoot, rel)).catch(() => null);
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
- candidates.push({ rel: rel.split(path9.sep).join("/"), basename: path9.basename(rel), bytes: entry.size });
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 candidates;
1423
+ return this.#cacheDigest({ version: FontOptimizer.#cacheVersion, fonts, sources });
1659
1424
  }
1660
- async#findReferrers(candidates, optimizedSrcs) {
1661
- const referrers = new Map;
1662
- const authoritative = candidates;
1663
- const bundled = candidates.filter((candidate) => !optimizedSrcs.has(candidate.basename));
1664
- const roots = [
1665
- { dir: this.#publicRoot, label: "public", needles: authoritative },
1666
- { dir: path9.join(this.#artifactRoot, "styles"), label: ".akan/artifact/styles", needles: authoritative },
1667
- { dir: path9.join(this.#artifactRoot, "client"), label: ".akan/artifact/client", needles: bundled },
1668
- { dir: path9.join(this.#artifactRoot, "client-ssr"), label: ".akan/artifact/client-ssr", needles: bundled },
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#scanRoot(root, pending, referrers) {
1681
- const needles = pending.map((candidate) => ({
1682
- rel: candidate.rel,
1683
- forms: [...new Set([candidate.basename, encodeURIComponent(candidate.basename)])]
1684
- }));
1685
- for await (const rel of new Bun.Glob(REFERRER_GLOB).scan({ cwd: root.dir, dot: false })) {
1686
- const remaining = needles.filter((needle) => !referrers.has(needle.rel));
1687
- if (!remaining.length)
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#remove(files) {
1699
- await Promise.all(files.map((file) => rm2(path9.join(this.#publicRoot, file), { force: true })));
1700
- const dirs = [...new Set(files.map((file) => path9.posix.dirname(file)))].filter((dir) => dir !== "." && dir !== "/").sort((a, b) => b.length - a.length);
1701
- for (const dir of dirs)
1702
- await rmdir(path9.join(this.#publicRoot, dir)).catch(() => {
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
- #report(result) {
1707
- if (!result.removed.length && !result.kept.length)
1708
- return;
1709
- for (const { file, bytes } of result.removed)
1710
- this.#app.verbose(`[font-prune] dropped public/${file} (${FontPruner.formatBytes(bytes)})`);
1711
- for (const { file, reason } of result.kept)
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
- const entry = byReferrer.get(reason.referrer) ?? { files: 0, bytes: 0 };
1718
- entry.files += 1;
1719
- entry.bytes += bytes;
1720
- byReferrer.set(reason.referrer, entry);
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 (!byReferrer.size)
1723
- return;
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
- static describe(reason) {
1730
- if (reason.kind === "unoptimized")
1731
- return "declared with optimize: false, so the runtime CSS serves it";
1732
- if (reason.kind === "declared")
1733
- return `kept by assets.keepFonts "${reason.glob}"`;
1734
- return `referenced by ${reason.referrer}`;
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
- static formatBytes(bytes) {
1737
- if (bytes < 1024)
1738
- return `${bytes}B`;
1739
- if (bytes < 1024 * 1024)
1740
- return `${(bytes / 1024).toFixed(1)}KB`;
1741
- return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
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
- async#isDirectory(dir) {
1744
- return await stat3(dir).then((entry) => entry.isDirectory(), () => false);
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
- // pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
1749
- import path11 from "path";
1750
-
1751
- // pkgs/@akanjs/devkit/transforms/externalizeFrameworkPlugin.ts
1752
- import path10 from "path";
1753
- var DEFAULT_INCLUDE = ["akanjs/", "@apps/", "@libs/"];
1754
- var DEFAULT_EXCLUDE_EXACT = new Set(["akanjs/webkit", "@akanjs/cli", "@akanjs/devkit"]);
1755
- var DEFAULT_EXCLUDE_PREFIX = ["@akanjs/cli/", "@akanjs/devkit/"];
1756
- var OPTIONAL_BACKEND_EXTERNAL_EXACT = new Set([
1757
- "@libsql/client",
1758
- "bullmq",
1759
- "ioredis",
1760
- "postgres",
1761
- "protobufjs"
1762
- ]);
1763
- var RUNTIME_EXTERNAL_EXACT = new Set([
1764
- "react",
1765
- "react-dom",
1766
- "react/jsx-runtime",
1767
- "react/jsx-dev-runtime",
1768
- "react-server-dom-webpack",
1769
- "react-server-dom-webpack/server.node",
1770
- "react-server-dom-webpack/client.node",
1771
- "react-server-dom-webpack/client.browser"
1772
- ]);
1773
- var RUNTIME_EXTERNAL_PREFIX = ["react-dom/", "react-server-dom-webpack/"];
1774
- var CANDIDATE_EXTS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
1775
- async function createExternalizeFrameworkPlugin(options) {
1776
- const tsconfig = await options.app.getTsConfig();
1777
- const includePrefixes = options.include ?? DEFAULT_INCLUDE;
1778
- const extraExact = new Set(options.extra ?? []);
1779
- const workspaceRoot = options.app.workspace.workspaceRoot;
1780
- const tsconfigPaths = tsconfig.compilerOptions.paths ?? {};
1781
- const rootEntries = Object.entries(tsconfigPaths).filter(([k]) => !k.endsWith("/*")).map(([k, v]) => ({ pkg: k, entryFile: v[0] ?? null })).filter((e) => e.entryFile !== null);
1782
- 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);
1783
- async function resolveWorkspaceSubpath(spec) {
1784
- if (!workspaceRoot)
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
- for (const { prefix, replacements } of wildcardEntries) {
1787
- if (!spec.startsWith(prefix))
1788
- continue;
1789
- const suffix = spec.slice(prefix.length);
1790
- for (const repl of replacements) {
1791
- const replPath = repl?.endsWith("/*") ? repl.slice(0, -1) : repl ?? "";
1792
- if (!replPath)
1793
- continue;
1794
- const candidate = path10.resolve(workspaceRoot, replPath + suffix);
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
- for (const { pkg, entryFile } of rootEntries) {
1801
- if (spec !== pkg && !spec.startsWith(`${pkg}/`))
1802
- continue;
1803
- if (spec === pkg)
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 suffix = spec.slice(pkg.length + 1);
1806
- const pkgDir = path10.dirname(path10.resolve(workspaceRoot, entryFile));
1807
- const candidate = path10.join(pkgDir, suffix);
1808
- const hit = await firstExisting(candidate);
1809
- if (hit)
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 null;
1715
+ return parts.join("");
1813
1716
  }
1814
- return {
1815
- name: "akan-externalize-framework",
1816
- setup(build) {
1817
- build.onResolve({ filter: /.*/ }, async (args) => {
1818
- const spec = args.path;
1819
- if (spec === "." || spec === ".." || spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/"))
1820
- return;
1821
- if (extraExact.has(spec) || DEFAULT_EXCLUDE_EXACT.has(spec))
1822
- return { path: spec, external: true };
1823
- for (const prefix of DEFAULT_EXCLUDE_PREFIX) {
1824
- if (spec.startsWith(prefix))
1825
- return { path: spec, external: true };
1826
- }
1827
- if (OPTIONAL_BACKEND_EXTERNAL_EXACT.has(spec))
1828
- return { path: spec, external: true };
1829
- if (RUNTIME_EXTERNAL_EXACT.has(spec))
1830
- return { path: spec, external: true };
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
- return;
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
- for (const ext of CANDIDATE_EXTS) {
1857
- const candidate = path10.join(basePath, `index${ext}`);
1858
- if (await Bun.file(candidate).exists())
1859
- return candidate;
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
- return null;
1862
- }
1863
-
1864
- // pkgs/@akanjs/devkit/transforms/useClientBundlePlugin.ts
1865
- function createUseClientBundlePlugin(options = {}) {
1866
- return {
1867
- name: "akan-use-client-bundle",
1868
- setup(build) {
1869
- build.onLoad({ filter: /\.(tsx|ts|jsx|js)$/ }, async (args) => {
1870
- if (args.path.includes("/node_modules/"))
1871
- return;
1872
- let source;
1873
- try {
1874
- source = await Bun.file(args.path).text();
1875
- } catch {
1876
- return;
1877
- }
1878
- const stubbed = transformUseClient(source, {
1879
- path: args.path,
1880
- workspaceRoot: options.workspaceRoot
1881
- });
1882
- if (stubbed === null)
1883
- return;
1884
- return { contents: stubbed, loader: loaderFor(args.path) };
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
- function loaderFor(absPath) {
1890
- if (absPath.endsWith(".tsx"))
1891
- return "tsx";
1892
- if (absPath.endsWith(".jsx"))
1893
- return "jsx";
1894
- if (absPath.endsWith(".ts"))
1895
- return "ts";
1896
- return "js";
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/pagesBundleBuilder.ts
1900
- var VIRTUAL_PAGES_ENTRY = "akan-pages-entry";
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 PagesBundleBuilder {
1827
+ class FontPruner {
1903
1828
  #app;
1904
- #command;
1905
- #pageEntries;
1906
- #started = Date.now();
1907
- constructor(app, command = "start", pageEntries) {
1829
+ #keepGlobs;
1830
+ #publicRoot;
1831
+ #artifactRoot;
1832
+ constructor(app, { keepFonts = [] } = {}) {
1908
1833
  this.#app = app;
1909
- this.#command = command;
1910
- this.#pageEntries = pageEntries;
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 build() {
1913
- const akanConfig = await this.#app.getConfig();
1914
- const resolvedEntries = this.#pageEntries ?? await resolveSsrPageEntriesForApp(this.#app, await this.#app.getPageKeys());
1915
- const entrySource = PagesEntrySourceGenerator.generate(resolvedEntries);
1916
- const workspaceRoot = this.#app.workspace.workspaceRoot;
1917
- const result = await Bun.build({
1918
- entrypoints: [VIRTUAL_PAGES_ENTRY],
1919
- outdir: `${this.#artifactDir}/server`,
1920
- target: "bun",
1921
- format: "esm",
1922
- splitting: this.#splitting,
1923
- minify: this.#command === "build",
1924
- naming: {
1925
- entry: "pages-[hash].[ext]",
1926
- chunk: "chunks/[name]-[hash].[ext]",
1927
- asset: "assets/[name]-[hash].[ext]"
1928
- },
1929
- define: this.#define(),
1930
- plugins: [
1931
- PagesBundleBuilder.createPagesEntryPlugin(entrySource),
1932
- PagesBundleBuilder.createServerCssStubPlugin(),
1933
- PagesBundleBuilder.createServerUseClientFetchPlugin(),
1934
- await createExternalizeFrameworkPlugin({ app: this.#app, extra: akanConfig.externalLibs }),
1935
- akanConfig.barrelImports.length > 0 ? await createBarrelImportsPlugin(this.#app, {
1936
- pipeAfter: (source, args) => transformUseClient(source, {
1937
- path: args.path,
1938
- workspaceRoot
1939
- })
1940
- }) : createUseClientBundlePlugin({ workspaceRoot })
1941
- ]
1942
- });
1943
- if (!result.success)
1944
- throw new AggregateError(result.logs, "[PagesBundleBuilder] Bun.build failed");
1945
- const entryArtifact = result.outputs.find((a) => a.kind === "entry-point");
1946
- if (!entryArtifact)
1947
- throw new Error("[PagesBundleBuilder] Bun.build emitted no entry-point artifact");
1948
- const bundlePath = path11.resolve(entryArtifact.path);
1949
- const buildId = Date.now();
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
- get #artifactDir() {
1964
- return `${this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath}/.akan/artifact`;
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
- get #splitting() {
1967
- return process.env.AKAN_SERVER_PAGES_SPLITTING === "1";
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
- #define() {
1970
- const nodeEnv = this.#command === "build" ? "production" : "development";
1971
- return {
1972
- "process.env.NODE_ENV": JSON.stringify(nodeEnv),
1973
- "process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("ssr"),
1974
- ...Object.fromEntries(Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]))
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
- static createPagesEntryPlugin(source) {
1978
- return {
1979
- name: "akan-pages-entry",
1980
- setup(build) {
1981
- build.onResolve({ filter: /^akan-pages-entry$/ }, () => ({
1982
- path: VIRTUAL_PAGES_ENTRY,
1983
- namespace: "akan-virtual"
1984
- }));
1985
- build.onLoad({ filter: /^akan-pages-entry$/, namespace: "akan-virtual" }, () => ({
1986
- contents: source,
1987
- loader: "tsx"
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
- static createServerCssStubPlugin() {
1993
- return {
1994
- name: "akan-server-css-stub",
1995
- setup(build) {
1996
- build.onLoad({ filter: /\.css$/ }, () => ({
1997
- contents: "",
1998
- loader: "js"
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
- static createServerUseClientFetchPlugin() {
2004
- return {
2005
- name: "akan-server-use-client-fetch",
2006
- setup(build) {
2007
- build.onLoad({ filter: /[/\\]lib[/\\]useClient\.(ts|tsx|js|jsx)$/ }, async (args) => {
2008
- const source = await Bun.file(args.path).text();
2009
- const transformed = PagesBundleBuilder.transformServerUseClientFetchSource(source);
2010
- if (transformed === source)
2011
- return;
2012
- return { contents: transformed, loader: loaderFor2(args.path) };
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 transformServerUseClientFetchSource(source) {
2018
- if (!source.includes(`with { type: "macro" }`) || !source.includes("FetchClient.build"))
2019
- return source;
2020
- 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 });");
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 fs2 from "fs";
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 (!fs2.existsSync(root) || !fs2.statSync(root).isDirectory())
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, CsrArtifactBuilder, CssCompiler, FontOptimizer, FontPruner, PagesBundleBuilder, precompressArtifacts, SsrBaseArtifactBuilder };
2207
+ export { AllRoutesBuilder, CssCompiler, PagesBundleBuilder, CsrArtifactBuilder, FontOptimizer, FontPruner, precompressArtifacts, SsrBaseArtifactBuilder };