@akanjs/cli 3.0.0-beta.22 → 3.0.0-beta.24

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.
@@ -866,6 +866,7 @@ function createUseClientBundlePlugin(options = {}) {
866
866
  import path6 from "path";
867
867
  class PagesEntrySourceGenerator {
868
868
  #pageEntries;
869
+ #asyncDefaults = new Map;
869
870
  constructor(pageEntries) {
870
871
  this.#pageEntries = pageEntries;
871
872
  }
@@ -883,16 +884,16 @@ ${lines.join(`
883
884
  };
884
885
  `;
885
886
  }
886
- static async generateStatic(pageEntries) {
887
- return await new PagesEntrySourceGenerator(pageEntries).generateStatic();
887
+ static async generateStatic(pageEntries, { fromDir } = {}) {
888
+ return await new PagesEntrySourceGenerator(pageEntries).generateStatic({ fromDir });
888
889
  }
889
- async generateStatic() {
890
+ async generateStatic({ fromDir } = {}) {
890
891
  const imports = this.#pageEntries.map(({ moduleAbsPath }, index) => {
891
- const specifier = PagesEntrySourceGenerator.#toImportSpecifier(moduleAbsPath);
892
+ const specifier = fromDir ? PagesEntrySourceGenerator.#toRelativeSpecifier(fromDir, moduleAbsPath) : PagesEntrySourceGenerator.#toImportSpecifier(moduleAbsPath);
892
893
  return `import * as page${index} from ${JSON.stringify(specifier)};`;
893
894
  });
894
895
  const entries = await Promise.all(this.#pageEntries.map(async ({ key, moduleAbsPath }, index) => {
895
- const isAsyncDefault = await AsyncDefaultExportDetector.detect(moduleAbsPath);
896
+ const isAsyncDefault = await this.#isAsyncDefault(moduleAbsPath);
896
897
  return ` ${JSON.stringify(key)}: { loader: async () => page${index}, isAsyncDefault: ${isAsyncDefault} },`;
897
898
  }));
898
899
  return `${imports.join(`
@@ -903,9 +904,40 @@ ${entries.join(`
903
904
  };
904
905
  `;
905
906
  }
907
+ async generateHotReplace({
908
+ fromDir,
909
+ ownerId,
910
+ moduleIds
911
+ }) {
912
+ const entries = await Promise.all(this.#pageEntries.map(async ({ key, moduleAbsPath }) => {
913
+ const specifier = PagesEntrySourceGenerator.#toRelativeSpecifier(fromDir, moduleAbsPath);
914
+ const isAsyncDefault = await this.#isAsyncDefault(moduleAbsPath);
915
+ return ` ${JSON.stringify(key)}: { loader: async () => require(${JSON.stringify(specifier)}), isAsyncDefault: ${isAsyncDefault} },`;
916
+ }));
917
+ return `__akan.accept(${JSON.stringify(ownerId)}, ${JSON.stringify(moduleIds)}, async () => {
918
+ const replaced = await akanWebkit.replacePages({
919
+ ${entries.join(`
920
+ `)}
921
+ });
922
+ if (!replaced) throw new Error("the route table no longer matches the pages it was built from");
923
+ });
924
+ `;
925
+ }
926
+ #isAsyncDefault(moduleAbsPath) {
927
+ const known = this.#asyncDefaults.get(moduleAbsPath);
928
+ if (known)
929
+ return known;
930
+ const detected = AsyncDefaultExportDetector.detect(moduleAbsPath);
931
+ this.#asyncDefaults.set(moduleAbsPath, detected);
932
+ return detected;
933
+ }
906
934
  static #toImportSpecifier(moduleAbsPath) {
907
935
  return path6.resolve(moduleAbsPath).split(path6.sep).join("/");
908
936
  }
937
+ static #toRelativeSpecifier(fromDir, moduleAbsPath) {
938
+ const relative = path6.relative(fromDir, path6.resolve(moduleAbsPath)).split(path6.sep).join("/");
939
+ return relative.startsWith(".") ? relative : `./${relative}`;
940
+ }
909
941
  }
910
942
 
911
943
  // pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
@@ -1244,9 +1276,774 @@ ${CsrArtifactBuilder.escapeInlineScript(await loadScript(src))}
1244
1276
  }
1245
1277
  }
1246
1278
 
1247
- // pkgs/@akanjs/devkit/frontendBuild/fontOptimizer.ts
1248
- import { mkdir as mkdir2, stat as stat2 } from "fs/promises";
1279
+ // pkgs/@akanjs/devkit/frontendBuild/csrDevBundler.ts
1280
+ import fs3 from "fs";
1281
+ import { mkdir as mkdir3 } from "fs/promises";
1282
+ import path13 from "path";
1283
+ import { CSR_DEV_DIRNAME, CSR_DEV_MANIFEST_FILE as CSR_DEV_MANIFEST_FILE2 } from "akanjs/server/hmr/csrDevManifest";
1284
+ import { isAkanRuntimeMetadataFile } from "akanjs/server/hmr/runtimeMetadataFile";
1285
+
1286
+ // pkgs/@akanjs/devkit/frontendBuild/csrDevArtifactWriter.ts
1287
+ import { mkdir as mkdir2, readdir, rename, rm as rm2 } from "fs/promises";
1288
+ import path10 from "path";
1289
+ import {
1290
+ CSR_DEV_APP_FILE,
1291
+ CSR_DEV_MANIFEST_FILE,
1292
+ CSR_DEV_ROUTE_PREFIX,
1293
+ csrDevModuleFile
1294
+ } from "akanjs/server/hmr/csrDevManifest";
1295
+
1296
+ // pkgs/@akanjs/devkit/frontendBuild/csrDevPaths.ts
1297
+ import fs from "fs";
1249
1298
  import path9 from "path";
1299
+
1300
+ class CsrDevPaths {
1301
+ static modulePrefix = "akan-module:";
1302
+ static stubPrefix = "stub:";
1303
+ root;
1304
+ constructor(workspaceRoot) {
1305
+ this.root = CsrDevPaths.realpath(workspaceRoot);
1306
+ }
1307
+ idOf(file) {
1308
+ const real = CsrDevPaths.realpath(file);
1309
+ const relative = path9.relative(this.root, real);
1310
+ const id = relative.startsWith("..") || path9.isAbsolute(relative) ? real : relative;
1311
+ return id.split(path9.sep).join("/");
1312
+ }
1313
+ fileOf(id) {
1314
+ return path9.isAbsolute(id) ? id : path9.join(this.root, id);
1315
+ }
1316
+ static isStub(target) {
1317
+ return target.startsWith(CsrDevPaths.stubPrefix);
1318
+ }
1319
+ static isVendorFile(file) {
1320
+ return file.includes(`${path9.sep}node_modules${path9.sep}`);
1321
+ }
1322
+ static isScript(file) {
1323
+ return /\.[cm]?[jt]sx?$/.test(file);
1324
+ }
1325
+ static mtimeOf(file) {
1326
+ return fs.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? -1;
1327
+ }
1328
+ static async hashOf(file) {
1329
+ const source = Bun.file(file);
1330
+ return await source.exists() ? Bun.hash(await source.arrayBuffer()).toString(36) : "";
1331
+ }
1332
+ static realpath(file) {
1333
+ return fs.existsSync(file) ? fs.realpathSync(file) : path9.resolve(file);
1334
+ }
1335
+ static tryResolve(specifier, from) {
1336
+ try {
1337
+ return Bun.resolveSync(specifier, from);
1338
+ } catch {
1339
+ return null;
1340
+ }
1341
+ }
1342
+ static lineCount(text) {
1343
+ let count = 0;
1344
+ for (let index = text.indexOf(`
1345
+ `);index !== -1; index = text.indexOf(`
1346
+ `, index + 1))
1347
+ count += 1;
1348
+ return count;
1349
+ }
1350
+ }
1351
+
1352
+ // pkgs/@akanjs/devkit/frontendBuild/csrDevArtifactWriter.ts
1353
+ class CsrDevArtifactWriter {
1354
+ static #keptPatches = 40;
1355
+ static #keptVendors = 3;
1356
+ #outDir;
1357
+ constructor(outDir) {
1358
+ this.#outDir = outDir;
1359
+ }
1360
+ async reset() {
1361
+ await rm2(this.#outDir, { recursive: true, force: true });
1362
+ await mkdir2(path10.join(this.#outDir, "modules"), { recursive: true });
1363
+ }
1364
+ async readJson(name) {
1365
+ const file = Bun.file(path10.join(this.#outDir, name));
1366
+ if (!await file.exists())
1367
+ return null;
1368
+ return await file.json().catch(() => null);
1369
+ }
1370
+ async writeJson(name, value) {
1371
+ await this.#writeAtomic(name, JSON.stringify(value));
1372
+ }
1373
+ async writeModules(compiled) {
1374
+ await mkdir2(path10.join(this.#outDir, "modules"), { recursive: true });
1375
+ const helpers = new Map(compiled.flatMap((module) => module.helpers ? [[module.helpers.hash, module.helpers]] : []));
1376
+ await Promise.all([
1377
+ ...compiled.flatMap((module) => [
1378
+ Bun.write(this.#modulePath(module.id, ".js"), module.factory),
1379
+ ...module.sourceMap ? [Bun.write(this.#modulePath(module.id, ".js.map"), module.sourceMap)] : []
1380
+ ]),
1381
+ ...[...helpers.values()].map((shared) => Bun.write(this.#helpersPath(shared.hash), shared.definition))
1382
+ ]);
1383
+ }
1384
+ async forgetModules(ids) {
1385
+ await Promise.all(ids.flatMap((id) => [
1386
+ rm2(this.#modulePath(id, ".js"), { force: true }),
1387
+ rm2(this.#modulePath(id, ".js.map"), { force: true })
1388
+ ]));
1389
+ }
1390
+ async writeVendor(graph) {
1391
+ const ids = Object.keys(graph.modules).filter((id) => graph.modules[id]?.vendor).sort();
1392
+ const stubs = new Set(Object.values(graph.modules).flatMap((module) => module.deps.filter(CsrDevPaths.isStub)));
1393
+ const lines = [await this.#helperDefinitions(graph, ids), ...await this.#defineLines(ids)];
1394
+ for (const stub of [...stubs].sort()) {
1395
+ const message = `[akan-csr] ${stub.slice(CsrDevPaths.stubPrefix.length)} is a Node built-in the browser does not have`;
1396
+ lines.push(`__akan.define(${JSON.stringify(stub)}, function () {
1397
+ throw new Error(${JSON.stringify(message)});
1398
+ });
1399
+ `);
1400
+ }
1401
+ const code = lines.join("");
1402
+ const vendorFile = `vendor-${Bun.hash(code).toString(36)}.js`;
1403
+ await this.#writeAtomic(vendorFile, code);
1404
+ return vendorFile;
1405
+ }
1406
+ async writeApp(graph, generation) {
1407
+ const ids = Object.keys(graph.modules).filter((id) => !graph.modules[id]?.vendor).sort();
1408
+ const lines = await this.#defineLines(ids);
1409
+ const blocks = ids.map((id, index) => [id, lines[index] ?? ""]);
1410
+ const start = `__akan.start(${JSON.stringify({ generation, refresh: graph.refresh })});
1411
+ `;
1412
+ await this.#writeWithSourceMap(CSR_DEV_APP_FILE, await this.#helperDefinitions(graph, ids), blocks, start);
1413
+ }
1414
+ async writePatch(generation, modules) {
1415
+ const patchFile = `patch-${generation}.js`;
1416
+ const blocks = modules.map((module, index) => [module.id, `${JSON.stringify(module.id)}: ${module.factory}${index < modules.length - 1 ? `,
1417
+ ` : `
1418
+ `}`]);
1419
+ const helpers = new Map(modules.flatMap((module) => module.helpers ? [[module.helpers.hash, module.helpers]] : []));
1420
+ const header = `${[...helpers.values()].map((shared) => shared.definition).join("")}__akan.update(${generation}, {
1421
+ `;
1422
+ await this.#writeWithSourceMap(patchFile, header, blocks, `});
1423
+ `);
1424
+ return `${CSR_DEV_ROUTE_PREFIX}${patchFile}`;
1425
+ }
1426
+ async writeState(graph, manifest) {
1427
+ await this.writeJson("graph.json", graph);
1428
+ await this.writeJson(CSR_DEV_MANIFEST_FILE, manifest);
1429
+ }
1430
+ async prune(vendorFile, generation) {
1431
+ const names = await readdir(this.#outDir);
1432
+ const stale = names.filter((name) => {
1433
+ const patch = /^patch-(\d+)\.js(?:\.layout\.json)?$/.exec(name);
1434
+ return patch ? Number(patch[1]) <= generation - CsrDevArtifactWriter.#keptPatches : false;
1435
+ });
1436
+ const vendors = names.filter((name) => /^vendor-[\w-]+\.js$/.test(name) && name !== vendorFile).map((name) => ({ name, mtimeMs: CsrDevPaths.mtimeOf(path10.join(this.#outDir, name)) })).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(CsrDevArtifactWriter.#keptVendors - 1).map(({ name }) => name);
1437
+ await Promise.all([...stale, ...vendors].map((name) => rm2(path10.join(this.#outDir, name), { force: true })));
1438
+ }
1439
+ async#helperDefinitions(graph, ids) {
1440
+ const hashes = [...new Set(ids.flatMap((id) => graph.modules[id]?.helpers ?? []))].sort();
1441
+ return (await Promise.all(hashes.map((hash) => Bun.file(this.#helpersPath(hash)).text()))).join("");
1442
+ }
1443
+ async#defineLines(ids) {
1444
+ return await Promise.all(ids.map(async (id) => `__akan.define(${JSON.stringify(id)}, ${await Bun.file(this.#modulePath(id, ".js")).text()});
1445
+ `));
1446
+ }
1447
+ async#writeWithSourceMap(name, header, blocks, footer) {
1448
+ const layout = { lineCount: 0, modules: [] };
1449
+ let line = CsrDevPaths.lineCount(header);
1450
+ for (const [id, text] of blocks) {
1451
+ layout.modules.push([id, line + 1]);
1452
+ line += CsrDevPaths.lineCount(text);
1453
+ }
1454
+ const code = `${header}${blocks.map(([, text]) => text).join("")}${footer}//# sourceMappingURL=${name}.map
1455
+ `;
1456
+ layout.lineCount = CsrDevPaths.lineCount(code);
1457
+ await this.writeJson(`${name}.layout.json`, layout);
1458
+ await this.#writeAtomic(name, code);
1459
+ }
1460
+ async#writeAtomic(name, content) {
1461
+ const target = path10.join(this.#outDir, name);
1462
+ const temp = `${target}.${process.pid}.tmp`;
1463
+ await Bun.write(temp, content);
1464
+ await rename(temp, target);
1465
+ }
1466
+ #modulePath(id, extension) {
1467
+ return path10.join(this.#outDir, csrDevModuleFile(id, extension));
1468
+ }
1469
+ #helpersPath(hash) {
1470
+ return path10.join(this.#outDir, "modules", `helpers-${hash}.js`);
1471
+ }
1472
+ }
1473
+
1474
+ // pkgs/@akanjs/devkit/frontendBuild/csrDevModuleCompiler.ts
1475
+ import path12 from "path";
1476
+ import { CSR_DEV_ROUTE_PREFIX as CSR_DEV_ROUTE_PREFIX2 } from "akanjs/server/hmr/csrDevManifest";
1477
+
1478
+ // pkgs/@akanjs/devkit/frontendBuild/csrDevResolver.ts
1479
+ import fs2 from "fs";
1480
+ import path11 from "path";
1481
+ class CsrDevResolver {
1482
+ static inline = "inline";
1483
+ #paths;
1484
+ #context;
1485
+ #entryFiles;
1486
+ #resolution = new Map;
1487
+ #fallbacks = new Set;
1488
+ #prepassDone = false;
1489
+ constructor({ paths, context, entryFiles, resolution = {} }) {
1490
+ this.#paths = paths;
1491
+ this.#context = context;
1492
+ this.#entryFiles = entryFiles;
1493
+ for (const [importer, bySpecifier] of Object.entries(resolution))
1494
+ this.#resolution.set(paths.fileOf(importer), new Map(Object.entries(bySpecifier).map(([specifier, target]) => [
1495
+ specifier,
1496
+ CsrDevPaths.isStub(target) ? target : paths.fileOf(target)
1497
+ ])));
1498
+ }
1499
+ get prepassDone() {
1500
+ return this.#prepassDone;
1501
+ }
1502
+ get fallbacks() {
1503
+ return this.#fallbacks;
1504
+ }
1505
+ async prepass() {
1506
+ const result = await Bun.build({
1507
+ entrypoints: [...this.#entryFiles, this.#context.refreshFile],
1508
+ target: "browser",
1509
+ metafile: true,
1510
+ env: "AKAN_PUBLIC_*",
1511
+ define: this.#context.define,
1512
+ optimizeImports: this.#context.optimizeImports,
1513
+ plugins: [PagesBundleBuilder.createCssStubPlugin()]
1514
+ });
1515
+ const { inputs } = result.metafile;
1516
+ for (const [input, { imports }] of Object.entries(inputs)) {
1517
+ const importer = CsrDevPaths.realpath(path11.resolve(input));
1518
+ for (const record of imports) {
1519
+ if (!record.original)
1520
+ continue;
1521
+ if (record.external) {
1522
+ if (record.original.startsWith("node:"))
1523
+ this.#remember(importer, record.original, `${CsrDevPaths.stubPrefix}${record.original}`);
1524
+ continue;
1525
+ }
1526
+ const target = path11.resolve(record.path);
1527
+ if (fs2.existsSync(target))
1528
+ this.#remember(importer, record.original, CsrDevPaths.realpath(target));
1529
+ }
1530
+ }
1531
+ this.#prepassDone = true;
1532
+ }
1533
+ resolve(importer, specifier) {
1534
+ if (this.#isInlineSpecifier(specifier))
1535
+ return CsrDevResolver.inline;
1536
+ const known = this.#resolution.get(importer)?.get(specifier);
1537
+ const usable = known && (CsrDevPaths.isStub(known) || fs2.existsSync(known)) ? known : null;
1538
+ const target = usable ?? this.#resolveUnknown(importer, specifier);
1539
+ if (target === null || CsrDevPaths.isStub(target))
1540
+ return target;
1541
+ if (!CsrDevPaths.isScript(target))
1542
+ return CsrDevResolver.inline;
1543
+ if (!usable)
1544
+ this.#remember(importer, specifier, target);
1545
+ return target;
1546
+ }
1547
+ isRegistryModule(file) {
1548
+ if (!CsrDevPaths.isScript(file))
1549
+ return false;
1550
+ const packageName = CsrDevResolver.#packageNameOf(file);
1551
+ if (packageName === null)
1552
+ return true;
1553
+ return !this.#context.optimizeImports.some((pattern) => {
1554
+ const base = CsrDevResolver.#patternBase(pattern);
1555
+ return base === packageName || base.startsWith(`${packageName}/`);
1556
+ });
1557
+ }
1558
+ serialize() {
1559
+ return Object.fromEntries([...this.#resolution].map(([importer, bySpecifier]) => [
1560
+ this.#paths.idOf(importer),
1561
+ Object.fromEntries([...bySpecifier].map(([specifier, target]) => [
1562
+ specifier,
1563
+ CsrDevPaths.isStub(target) ? target : this.#paths.idOf(target)
1564
+ ]))
1565
+ ]));
1566
+ }
1567
+ #resolveUnknown(importer, specifier) {
1568
+ if (specifier.startsWith("node:"))
1569
+ return `${CsrDevPaths.stubPrefix}${specifier}`;
1570
+ const relative = specifier.startsWith(".") || path11.isAbsolute(specifier);
1571
+ if (!relative && !this.#prepassDone)
1572
+ return null;
1573
+ if (!relative) {
1574
+ const sibling = this.#resolvedBySibling(importer, specifier);
1575
+ if (sibling)
1576
+ return sibling;
1577
+ }
1578
+ const resolved = CsrDevPaths.tryResolve(specifier, path11.dirname(importer));
1579
+ if (!resolved)
1580
+ throw new Error(`[csr-dev] cannot resolve "${specifier}" from ${this.#paths.idOf(importer)}`);
1581
+ if (!path11.isAbsolute(resolved))
1582
+ return `${CsrDevPaths.stubPrefix}${specifier}`;
1583
+ if (!relative)
1584
+ this.#fallbacks.add(specifier);
1585
+ return CsrDevPaths.realpath(resolved);
1586
+ }
1587
+ #resolvedBySibling(importer, specifier) {
1588
+ const scope = CsrDevResolver.#packageScopeOf(importer);
1589
+ for (const [other, bySpecifier] of this.#resolution) {
1590
+ const target = bySpecifier.get(specifier);
1591
+ if (target && CsrDevResolver.#packageScopeOf(other) === scope)
1592
+ return target;
1593
+ }
1594
+ return null;
1595
+ }
1596
+ #remember(importer, specifier, target) {
1597
+ const byImporter = this.#resolution.get(importer) ?? new Map;
1598
+ this.#resolution.set(importer, byImporter.set(specifier, target));
1599
+ }
1600
+ #isInlineSpecifier(specifier) {
1601
+ if (specifier.startsWith(".") || path11.isAbsolute(specifier) || specifier.startsWith("node:"))
1602
+ return false;
1603
+ return this.#context.optimizeImports.some((pattern) => {
1604
+ const base = CsrDevResolver.#patternBase(pattern);
1605
+ return specifier === base || specifier.startsWith(`${base}/`);
1606
+ });
1607
+ }
1608
+ static #patternBase(pattern) {
1609
+ return pattern.endsWith("/*") ? pattern.slice(0, -2) : pattern;
1610
+ }
1611
+ static #packageNameOf(file) {
1612
+ const marker = `${path11.sep}node_modules${path11.sep}`;
1613
+ const index = file.lastIndexOf(marker);
1614
+ if (index < 0)
1615
+ return null;
1616
+ const [scope, name] = file.slice(index + marker.length).split(path11.sep);
1617
+ return (scope?.startsWith("@") ? `${scope}/${name}` : scope) || null;
1618
+ }
1619
+ static #packageScopeOf(file) {
1620
+ const marker = `${path11.sep}node_modules${path11.sep}`;
1621
+ const index = file.lastIndexOf(marker);
1622
+ if (index < 0)
1623
+ return "";
1624
+ const [scope, name] = file.slice(index + marker.length).split(path11.sep);
1625
+ return file.slice(0, index + marker.length) + (scope?.startsWith("@") ? `${scope}${path11.sep}${name}` : scope);
1626
+ }
1627
+ }
1628
+
1629
+ // pkgs/@akanjs/devkit/frontendBuild/csrDevModuleCompiler.ts
1630
+ class CsrDevModuleCompiler {
1631
+ static #storeRoot = /(?:^|\/)lib\/st\.ts$/;
1632
+ static #factoryArgument = /(?<![\w$.])(?:require|module|exports|__akanImport|\$RefreshReg\$|\$RefreshSig\$)(?![\w$]|\s*:)/;
1633
+ static #helperPatches = [
1634
+ ["__toESM", "(mod, isNodeMode, target)", "toESM"],
1635
+ ["__reExport", "(target, mod, secondTarget)", "reExport"]
1636
+ ];
1637
+ #app;
1638
+ #paths;
1639
+ #resolver;
1640
+ #context;
1641
+ #outDir;
1642
+ constructor({ app, paths, resolver, context, outDir }) {
1643
+ this.#app = app;
1644
+ this.#paths = paths;
1645
+ this.#resolver = resolver;
1646
+ this.#context = context;
1647
+ this.#outDir = outDir;
1648
+ }
1649
+ async compile(startFiles, known) {
1650
+ const compiled = new Map;
1651
+ const seen = new Set([...known, ...startFiles]);
1652
+ let frontier = [...new Set(startFiles)];
1653
+ while (frontier.length > 0) {
1654
+ const round = await this.#compileRound(frontier);
1655
+ if (round.misses.length > 0 && !this.#resolver.prepassDone) {
1656
+ this.#app.verbose(`[csr-dev] ${round.misses.length} unresolved import(s); rerunning the resolution build`);
1657
+ await this.#resolver.prepass();
1658
+ continue;
1659
+ }
1660
+ const next = [];
1661
+ for (const module of round.modules) {
1662
+ compiled.set(module.file, module);
1663
+ for (const dep of module.deps) {
1664
+ if (CsrDevPaths.isStub(dep) || seen.has(dep))
1665
+ continue;
1666
+ seen.add(dep);
1667
+ next.push(dep);
1668
+ }
1669
+ }
1670
+ frontier = next;
1671
+ }
1672
+ const { fallbacks } = this.#resolver;
1673
+ if (fallbacks.size > 0)
1674
+ this.#app.verbose(`[csr-dev] ${fallbacks.size} import(s) outside the browser build's graph resolved with Bun's runtime resolver: ${[...fallbacks].slice(0, 5).join(", ")}${fallbacks.size > 5 ? ", ..." : ""}`);
1675
+ return [...compiled.values()];
1676
+ }
1677
+ async#compileRound(files) {
1678
+ const appFiles = files.filter((file) => !CsrDevPaths.isVendorFile(file));
1679
+ const vendorFiles = files.filter((file) => CsrDevPaths.isVendorFile(file));
1680
+ const rounds = await Promise.all([
1681
+ appFiles.length > 0 ? this.#bunBuild(appFiles, false) : null,
1682
+ vendorFiles.length > 0 ? this.#bunBuild(vendorFiles, true) : null
1683
+ ]);
1684
+ const modules = rounds.flatMap((round) => round?.modules ?? []);
1685
+ const misses = rounds.flatMap((round) => round?.misses ?? []);
1686
+ for (const file of rounds.flatMap((round) => round?.tangled ?? [])) {
1687
+ const single = await this.#bunBuild([file], CsrDevPaths.isVendorFile(file));
1688
+ if (single.tangled.length > 0)
1689
+ throw new Error(`[csr-dev] ${this.#paths.idOf(file)} kept inlining another module`);
1690
+ modules.push(...single.modules);
1691
+ misses.push(...single.misses);
1692
+ }
1693
+ return { modules, misses };
1694
+ }
1695
+ async#bunBuild(files, vendor) {
1696
+ const mtimes = new Map(files.map((file) => [file, CsrDevPaths.mtimeOf(file)]));
1697
+ const hashes = new Map(await Promise.all(files.map(async (file) => [file, vendor ? "" : await CsrDevPaths.hashOf(file)])));
1698
+ const misses = [];
1699
+ const result = await Bun.build({
1700
+ entrypoints: files,
1701
+ root: this.#paths.root,
1702
+ target: "browser",
1703
+ format: "cjs",
1704
+ reactFastRefresh: !vendor,
1705
+ sourcemap: vendor ? "none" : "external",
1706
+ naming: { entry: "[dir]/[name].[ext]", asset: "assets/[name]-[hash].[ext]" },
1707
+ publicPath: CSR_DEV_ROUTE_PREFIX2,
1708
+ metafile: true,
1709
+ env: "AKAN_PUBLIC_*",
1710
+ define: this.#context.define,
1711
+ optimizeImports: this.#context.optimizeImports,
1712
+ plugins: [this.#registryPlugin(vendor, misses)]
1713
+ });
1714
+ const { outputs } = result.metafile;
1715
+ const sourceMaps = new Map(result.outputs.filter((artifact) => artifact.kind === "sourcemap").map((artifact) => [artifact.path, artifact]));
1716
+ const modules = [];
1717
+ const tangled = [];
1718
+ for (const artifact of result.outputs) {
1719
+ if (artifact.kind === "asset") {
1720
+ await Bun.write(path12.join(this.#outDir, artifact.path), artifact);
1721
+ continue;
1722
+ }
1723
+ if (artifact.kind !== "entry-point")
1724
+ continue;
1725
+ const meta = outputs[artifact.path];
1726
+ if (!meta?.entryPoint)
1727
+ throw new Error(`[csr-dev] no entry point recorded for ${artifact.path}`);
1728
+ const file = CsrDevPaths.realpath(path12.resolve(meta.entryPoint));
1729
+ const inputs = Object.keys(meta.inputs).map((input) => CsrDevPaths.realpath(path12.resolve(input)));
1730
+ if (inputs.some((input) => input !== file && this.#resolver.isRegistryModule(input))) {
1731
+ tangled.push(file);
1732
+ continue;
1733
+ }
1734
+ const code = await artifact.text();
1735
+ const id = this.#paths.idOf(file);
1736
+ const sourceMap = sourceMaps.get(`${artifact.path}.map`);
1737
+ modules.push({
1738
+ id,
1739
+ file,
1740
+ vendor,
1741
+ ...CsrDevModuleCompiler.#factory(id, code),
1742
+ sourceMap: sourceMap ? this.#rebaseSourceMap(await sourceMap.text()) : undefined,
1743
+ deps: this.#emittedDeps(code),
1744
+ mtimeMs: mtimes.get(file) ?? CsrDevPaths.mtimeOf(file),
1745
+ hash: hashes.get(file) ?? ""
1746
+ });
1747
+ }
1748
+ return { modules, misses, tangled };
1749
+ }
1750
+ #registryPlugin(vendor, misses) {
1751
+ return {
1752
+ name: "akan-csr-registry",
1753
+ setup: (build) => {
1754
+ build.onLoad({ filter: /\.css$/ }, () => ({ contents: "", loader: "js" }));
1755
+ if (!vendor)
1756
+ build.onLoad({ filter: /\.tsx$/ }, async (args) => {
1757
+ if (CsrDevPaths.isVendorFile(args.path))
1758
+ return;
1759
+ const normalized = RouteClientBuilder.normalizeNamedDefaultFunctionForFastRefresh(await Bun.file(args.path).text());
1760
+ return normalized ? { contents: normalized, loader: "tsx" } : undefined;
1761
+ });
1762
+ build.onResolve({ filter: /.*/ }, (args) => {
1763
+ if (args.kind === "entry-point-build" || !args.importer)
1764
+ return;
1765
+ const importer = CsrDevPaths.realpath(args.importer);
1766
+ const target = this.#resolver.resolve(importer, args.path);
1767
+ if (target === CsrDevResolver.inline)
1768
+ return;
1769
+ if (target === null) {
1770
+ misses.push(`${args.path} from ${this.#paths.idOf(importer)}`);
1771
+ return { path: `akan-miss:${args.path}`, external: true };
1772
+ }
1773
+ const external = CsrDevPaths.isStub(target) ? target : `${CsrDevPaths.modulePrefix}${this.#paths.idOf(target)}`;
1774
+ return { path: external, external: true };
1775
+ });
1776
+ }
1777
+ };
1778
+ }
1779
+ #rebaseSourceMap(text) {
1780
+ const map = JSON.parse(text);
1781
+ const files = map.sources.map((source) => path12.resolve(source));
1782
+ map.sources = files.map((file) => `/${this.#paths.idOf(file)}`);
1783
+ map.sourcesContent = files.map((file, index) => CsrDevPaths.isVendorFile(file) ? null : map.sourcesContent?.[index] ?? null);
1784
+ return JSON.stringify(map);
1785
+ }
1786
+ #emittedDeps(code) {
1787
+ const deps = new Set;
1788
+ const references = /\b(?:require|import|__akanImport)\("((?:akan-module|stub):[^"]+)"\)/g;
1789
+ for (const [, reference] of code.matchAll(references)) {
1790
+ if (!reference)
1791
+ continue;
1792
+ deps.add(CsrDevPaths.isStub(reference) ? reference : this.#paths.fileOf(reference.slice(CsrDevPaths.modulePrefix.length)));
1793
+ }
1794
+ return [...deps];
1795
+ }
1796
+ static #factory(id, code) {
1797
+ const prefix = CsrDevPaths.modulePrefix;
1798
+ let patched = code.replaceAll(`import("${prefix}`, `__akanImport("${prefix}`).replace(/^\/\/# debugId=.*$/m, "");
1799
+ for (const [name, params, method] of CsrDevModuleCompiler.#helperPatches) {
1800
+ const definition = `var ${name} = ${params} => {`;
1801
+ if (patched.includes(definition))
1802
+ patched = patched.replace(definition, `var ${name} = __akan.${method}, __bun${name} = ${params} => {`);
1803
+ else if (patched.includes(`${name}(`))
1804
+ throw new Error(`[csr-dev] ${id}: Bun's ${name} helper no longer reads "${definition}"; CsrDevModuleCompiler must follow`);
1805
+ }
1806
+ if (CsrDevModuleCompiler.#storeRoot.test(id))
1807
+ patched += `
1808
+ module.hot.accept();`;
1809
+ const shared = CsrDevModuleCompiler.#shareHelpers(patched);
1810
+ return {
1811
+ factory: `function (require, module, exports, $RefreshReg$, $RefreshSig$, __akanImport) {
1812
+ ${shared?.code ?? patched}
1813
+ }`,
1814
+ helpers: shared?.helpers ?? null
1815
+ };
1816
+ }
1817
+ static #shareHelpers(code) {
1818
+ const marker = code.search(/^\/\/ /m);
1819
+ if (marker <= 0)
1820
+ return null;
1821
+ const preamble = code.slice(0, marker);
1822
+ const names = [...preamble.matchAll(/^(?:var|let|const|function)\s+([\w$]+)/gm)].map(([, name]) => name).filter((name) => !!name);
1823
+ if (names.length === 0 || CsrDevModuleCompiler.#factoryArgument.test(preamble))
1824
+ return null;
1825
+ const hash = Bun.hash(preamble).toString(36);
1826
+ const binding = `var { ${names.join(", ")} } = __akan.helpers(${JSON.stringify(hash)});`;
1827
+ return {
1828
+ code: `${binding}${`
1829
+ `.repeat(CsrDevPaths.lineCount(preamble))}${code.slice(marker)}`,
1830
+ helpers: {
1831
+ hash,
1832
+ definition: `__akan.defineHelpers(${JSON.stringify(hash)}, function () {
1833
+ ${preamble}return { ${names.join(", ")} };
1834
+ });
1835
+ `
1836
+ }
1837
+ };
1838
+ }
1839
+ }
1840
+
1841
+ // pkgs/@akanjs/devkit/frontendBuild/csrDevBundler.ts
1842
+ class CsrDevBundler {
1843
+ static #formatVersion = 3;
1844
+ #app;
1845
+ #paths;
1846
+ #outDir;
1847
+ #entryDir;
1848
+ #writer;
1849
+ constructor(app) {
1850
+ this.#app = app;
1851
+ this.#paths = new CsrDevPaths(app.workspace.workspaceRoot);
1852
+ this.#outDir = path13.join(app.cwdPath, ".akan/artifact", CSR_DEV_DIRNAME);
1853
+ this.#entryDir = path13.join(app.cwdPath, ".akan/generated", CSR_DEV_DIRNAME);
1854
+ this.#writer = new CsrDevArtifactWriter(this.#outDir);
1855
+ }
1856
+ async update(changedFiles = []) {
1857
+ const context = await this.#context();
1858
+ if (context.pageEntries.length === 0)
1859
+ return null;
1860
+ const manifest = await this.#writer.readJson(CSR_DEV_MANIFEST_FILE2);
1861
+ const graph = await this.#writer.readJson("graph.json");
1862
+ const generation = (manifest?.generation ?? 0) + 1;
1863
+ if (!manifest || !graph || graph.version !== 1 || graph.configKey !== context.configKey)
1864
+ return await this.#fullBuild(context, generation, manifest ? "the dev bundle config changed" : "first build");
1865
+ const metadataFile = changedFiles.find(isAkanRuntimeMetadataFile);
1866
+ if (metadataFile)
1867
+ return await this.#fullBuild(context, generation, `${path13.basename(metadataFile)} changed`);
1868
+ return await this.#incrementalBuild(context, graph, manifest, changedFiles, generation);
1869
+ }
1870
+ async#context() {
1871
+ const akanConfig = await this.#app.getConfig();
1872
+ const pageEntries = await resolveSsrPageEntriesForApp(this.#app, await this.#app.getPageKeys());
1873
+ const basePaths = [...akanConfig.basePaths];
1874
+ const htmlBasePaths = basePaths.length > 0 ? basePaths : [""];
1875
+ const define = bundleDefine(this.#app, "start", "csr");
1876
+ const optimizeImports = [...akanConfig.optimizeImports];
1877
+ const configKey = Bun.hash(JSON.stringify([CsrDevBundler.#formatVersion, Bun.version, define, optimizeImports, htmlBasePaths])).toString(36);
1878
+ return {
1879
+ pageEntries,
1880
+ basePaths,
1881
+ htmlBasePaths,
1882
+ define,
1883
+ optimizeImports,
1884
+ configKey,
1885
+ refreshFile: this.#resolveRefreshRuntime()
1886
+ };
1887
+ }
1888
+ #resolveRefreshRuntime() {
1889
+ const akanWebkit = CsrDevPaths.tryResolve("akanjs/webkit", this.#app.cwdPath);
1890
+ const bases = [
1891
+ this.#app.cwdPath,
1892
+ this.#paths.root,
1893
+ ...akanWebkit ? [path13.dirname(path13.dirname(akanWebkit))] : []
1894
+ ];
1895
+ for (const base of bases) {
1896
+ const resolved = CsrDevPaths.tryResolve("react-refresh/runtime", base);
1897
+ if (resolved)
1898
+ return CsrDevPaths.realpath(resolved);
1899
+ }
1900
+ throw new Error(`[csr-dev] react-refresh/runtime is not resolvable from ${bases.join(", ")}`);
1901
+ }
1902
+ async#fullBuild(context, generation, reason) {
1903
+ await this.#writer.reset();
1904
+ const entries = await this.#writeEntries(context);
1905
+ const entryFiles = Object.values(entries.files);
1906
+ const resolver = new CsrDevResolver({ paths: this.#paths, context, entryFiles });
1907
+ await resolver.prepass();
1908
+ const compiled = await this.#compiler(context, resolver).compile([...entryFiles, context.refreshFile], new Set);
1909
+ const graph = {
1910
+ version: 1,
1911
+ configKey: context.configKey,
1912
+ refresh: this.#paths.idOf(context.refreshFile),
1913
+ entries: this.#entryIds(entries.files),
1914
+ modules: {},
1915
+ resolution: {},
1916
+ pending: []
1917
+ };
1918
+ this.#merge(graph, resolver, compiled);
1919
+ await this.#writer.writeModules(compiled);
1920
+ const vendorFile = await this.#writer.writeVendor(graph);
1921
+ await this.#writer.writeApp(graph, generation);
1922
+ await this.#writer.writeState(graph, { version: 1, generation, vendorFile, entries: graph.entries });
1923
+ await this.#writer.prune(vendorFile, generation);
1924
+ return { generation, reload: true, reason, changedIds: [], moduleCount: Object.keys(graph.modules).length };
1925
+ }
1926
+ async#incrementalBuild(context, graph, manifest, changedFiles, generation) {
1927
+ const entries = await this.#writeEntries(context);
1928
+ const entryIds = this.#entryIds(entries.files);
1929
+ await this.#forgetDeletedModules(graph);
1930
+ const changed = await this.#changedModules(graph, changedFiles);
1931
+ for (const file of entries.changed)
1932
+ changed.add(this.#paths.idOf(file));
1933
+ const routesMoved = entries.changed.length > 0 || JSON.stringify(entryIds) !== JSON.stringify(graph.entries);
1934
+ if (changed.size === 0)
1935
+ return null;
1936
+ const resolver = new CsrDevResolver({
1937
+ paths: this.#paths,
1938
+ context,
1939
+ entryFiles: Object.values(entries.files),
1940
+ resolution: graph.resolution
1941
+ });
1942
+ const known = new Set(Object.keys(graph.modules).map((id) => this.#paths.fileOf(id)));
1943
+ let compiled;
1944
+ try {
1945
+ compiled = await this.#compiler(context, resolver).compile([...changed].map((id) => this.#paths.fileOf(id)), known);
1946
+ } catch (error) {
1947
+ graph.pending = [...changed];
1948
+ await this.#writer.writeJson("graph.json", graph);
1949
+ throw error;
1950
+ }
1951
+ const vendorJoined = compiled.some((module) => module.vendor && !graph.modules[module.id]);
1952
+ this.#merge(graph, resolver, compiled);
1953
+ graph.entries = entryIds;
1954
+ await this.#writer.writeModules(compiled);
1955
+ const vendorFile = vendorJoined ? await this.#writer.writeVendor(graph) : manifest.vendorFile;
1956
+ await this.#writer.writeApp(graph, generation);
1957
+ const changedIds = compiled.filter((module) => !module.vendor).map((module) => module.id);
1958
+ const constantId = changedIds.find((id) => id.endsWith(".constant.ts"));
1959
+ const reason = vendorJoined ? "an npm module joined the graph" : routesMoved ? "the route table changed" : constantId ? `${path13.basename(constantId)} changed` : undefined;
1960
+ const patchUrl = reason ? undefined : await this.#writer.writePatch(generation, compiled.filter((module) => !module.vendor));
1961
+ await this.#writer.writeState(graph, { ...manifest, generation, vendorFile, entries: entryIds });
1962
+ await this.#writer.prune(vendorFile, generation);
1963
+ return {
1964
+ generation,
1965
+ reload: !!reason,
1966
+ reason,
1967
+ patchUrl,
1968
+ changedIds,
1969
+ moduleCount: Object.keys(graph.modules).length
1970
+ };
1971
+ }
1972
+ async#changedModules(graph, changedFiles) {
1973
+ const changed = new Set(graph.pending.filter((id) => graph.modules[id]));
1974
+ for (const file of changedFiles) {
1975
+ const id = this.#paths.idOf(file);
1976
+ if (graph.modules[id])
1977
+ changed.add(id);
1978
+ }
1979
+ for (const [id, module] of Object.entries(graph.modules)) {
1980
+ if (module.vendor || changed.has(id))
1981
+ continue;
1982
+ const file = this.#paths.fileOf(id);
1983
+ if (CsrDevPaths.mtimeOf(file) === module.mtimeMs)
1984
+ continue;
1985
+ if (await CsrDevPaths.hashOf(file) !== module.hash)
1986
+ changed.add(id);
1987
+ }
1988
+ return changed;
1989
+ }
1990
+ async#forgetDeletedModules(graph) {
1991
+ const deleted = Object.keys(graph.modules).filter((id) => !graph.modules[id]?.vendor && !fs3.existsSync(this.#paths.fileOf(id)));
1992
+ for (const id of deleted)
1993
+ delete graph.modules[id];
1994
+ await this.#writer.forgetModules(deleted);
1995
+ }
1996
+ async#writeEntries(context) {
1997
+ await mkdir3(this.#entryDir, { recursive: true });
1998
+ const files = {};
1999
+ const changed = [];
2000
+ for (const basePath of context.htmlBasePaths) {
2001
+ const file = path13.join(this.#entryDir, CsrArtifactBuilder.entryFilename(basePath));
2002
+ const entryPages = CsrArtifactBuilder.pageEntriesForBasePath(context.pageEntries, basePath, context.basePaths);
2003
+ const generator = new PagesEntrySourceGenerator(entryPages);
2004
+ const pages = await generator.generateStatic({ fromDir: this.#entryDir });
2005
+ const hot = await generator.generateHotReplace({
2006
+ fromDir: this.#entryDir,
2007
+ ownerId: this.#paths.idOf(file),
2008
+ moduleIds: entryPages.map(({ moduleAbsPath }) => this.#paths.idOf(moduleAbsPath))
2009
+ });
2010
+ const source = `import * as akanWebkit from "akanjs/webkit";
2011
+ ${pages}
2012
+ void akanWebkit.bootCsr(pages);
2013
+ ${hot}`;
2014
+ const unchanged = await Bun.file(file).exists() && await Bun.file(file).text() === source;
2015
+ if (!unchanged)
2016
+ await Bun.write(file, source);
2017
+ const entryFile = CsrDevPaths.realpath(file);
2018
+ files[basePath] = entryFile;
2019
+ if (!unchanged)
2020
+ changed.push(entryFile);
2021
+ }
2022
+ return { files, changed };
2023
+ }
2024
+ #compiler(context, resolver) {
2025
+ return new CsrDevModuleCompiler({ app: this.#app, paths: this.#paths, resolver, context, outDir: this.#outDir });
2026
+ }
2027
+ #merge(graph, resolver, compiled) {
2028
+ for (const module of compiled)
2029
+ graph.modules[module.id] = {
2030
+ vendor: module.vendor,
2031
+ mtimeMs: module.mtimeMs,
2032
+ hash: module.hash,
2033
+ deps: module.deps.map((dep) => CsrDevPaths.isStub(dep) ? dep : this.#paths.idOf(dep)),
2034
+ ...module.helpers ? { helpers: module.helpers.hash } : {}
2035
+ };
2036
+ graph.pending = [];
2037
+ graph.resolution = resolver.serialize();
2038
+ }
2039
+ #entryIds(files) {
2040
+ return Object.fromEntries(Object.entries(files).map(([basePath, file]) => [basePath, this.#paths.idOf(file)]));
2041
+ }
2042
+ }
2043
+
2044
+ // pkgs/@akanjs/devkit/frontendBuild/fontOptimizer.ts
2045
+ import { mkdir as mkdir4, stat as stat2 } from "fs/promises";
2046
+ import path14 from "path";
1250
2047
  import ts from "typescript";
1251
2048
  var FONT_URL_PREFIX = "/_akan/fonts";
1252
2049
  var DEFAULT_FONT_SUBSETS = ["latin"];
@@ -1263,7 +2060,7 @@ class FontOptimizer {
1263
2060
  constructor(app, command = "start") {
1264
2061
  this.#app = app;
1265
2062
  this.#command = command;
1266
- this.#artifactRoot = path9.join(command === "build" ? app.dist.cwdPath : app.cwdPath, ".akan/artifact");
2063
+ this.#artifactRoot = path14.join(command === "build" ? app.dist.cwdPath : app.cwdPath, ".akan/artifact");
1267
2064
  }
1268
2065
  async optimize() {
1269
2066
  const fonts = await this.discoverFonts();
@@ -1288,7 +2085,7 @@ class FontOptimizer {
1288
2085
  return result;
1289
2086
  }
1290
2087
  get #cachePath() {
1291
- return path9.join(this.#artifactRoot, "fontCache.json");
2088
+ return path14.join(this.#artifactRoot, "fontCache.json");
1292
2089
  }
1293
2090
  async#buildCacheKey(fonts) {
1294
2091
  const sources = [];
@@ -1303,7 +2100,7 @@ class FontOptimizer {
1303
2100
  sources.push({ optimizedSrc: face.optimizedSrc, ...stamp });
1304
2101
  }
1305
2102
  for (const filePath of font.subsetFiles ?? []) {
1306
- const abs = path9.isAbsolute(filePath) ? filePath : path9.join(this.#app.cwdPath, filePath);
2103
+ const abs = path14.isAbsolute(filePath) ? filePath : path14.join(this.#app.cwdPath, filePath);
1307
2104
  const stamp = await this.#fileStamp(abs);
1308
2105
  if (!stamp)
1309
2106
  return null;
@@ -1329,7 +2126,7 @@ class FontOptimizer {
1329
2126
  const cache = await Bun.file(this.#cachePath).json().catch(() => null);
1330
2127
  if (cache?.version !== FontOptimizer.#cacheVersion || cache.key !== key)
1331
2128
  return null;
1332
- const files = cache.files.map((relativePath) => path9.join(this.#artifactRoot, relativePath));
2129
+ const files = cache.files.map((relativePath) => path14.join(this.#artifactRoot, relativePath));
1333
2130
  for (const filePath of files) {
1334
2131
  if (!await Bun.file(filePath).exists())
1335
2132
  return null;
@@ -1341,7 +2138,7 @@ class FontOptimizer {
1341
2138
  version: FontOptimizer.#cacheVersion,
1342
2139
  key,
1343
2140
  css: result.css,
1344
- files: result.files.map((filePath) => path9.relative(this.#artifactRoot, filePath))
2141
+ files: result.files.map((filePath) => path14.relative(this.#artifactRoot, filePath))
1345
2142
  };
1346
2143
  await Bun.write(this.#cachePath, JSON.stringify(cache));
1347
2144
  }
@@ -1349,7 +2146,7 @@ class FontOptimizer {
1349
2146
  const pageKeys = await this.#app.getPageKeys();
1350
2147
  const fonts = [];
1351
2148
  await Promise.all(pageKeys.map(async (key) => {
1352
- const filePath = path9.resolve(this.#app.cwdPath, "page", key);
2149
+ const filePath = path14.resolve(this.#app.cwdPath, "page", key);
1353
2150
  const file = Bun.file(filePath);
1354
2151
  if (!await file.exists())
1355
2152
  return;
@@ -1368,8 +2165,8 @@ class FontOptimizer {
1368
2165
  this.#app.logger.warn(`[font] source not found: ${face.src}`);
1369
2166
  continue;
1370
2167
  }
1371
- const outputPath = path9.join(this.#artifactRoot, face.optimizedSrc.replace(/^\/_akan\//, ""));
1372
- await mkdir2(path9.dirname(outputPath), { recursive: true });
2168
+ const outputPath = path14.join(this.#artifactRoot, face.optimizedSrc.replace(/^\/_akan\//, ""));
2169
+ await mkdir4(path14.dirname(outputPath), { recursive: true });
1373
2170
  const sourceBuffer = Buffer.from(await Bun.file(sourcePath).arrayBuffer());
1374
2171
  const outputBuffer = await this.#buildFontBuffer(font, sourceBuffer, sourcePath);
1375
2172
  await Bun.write(outputPath, outputBuffer);
@@ -1391,7 +2188,7 @@ class FontOptimizer {
1391
2188
  const entries = Array.isArray(value) ? value : [];
1392
2189
  const fonts = entries.filter((entry) => this.#isReadableFont(entry));
1393
2190
  if (fonts.length !== entries.length || !Array.isArray(value))
1394
- this.#app.logger.warn(`[font] ${path9.relative(this.#app.cwdPath, filePath)} declares fonts the build cannot read without evaluating the module \u2014 write the list inline, or nothing is subset for it and every /_akan/fonts request 404s`);
2191
+ this.#app.logger.warn(`[font] ${path14.relative(this.#app.cwdPath, filePath)} declares fonts the build cannot read without evaluating the module \u2014 write the list inline, or nothing is subset for it and every /_akan/fonts request 404s`);
1395
2192
  return fonts.map((font) => ({ ...font, subsets: font.subsets ?? [...DEFAULT_FONT_SUBSETS] }));
1396
2193
  }
1397
2194
  #isReadableFont(value) {
@@ -1549,8 +2346,8 @@ class FontOptimizer {
1549
2346
  return null;
1550
2347
  const rel = src.replace(/^\//, "");
1551
2348
  const candidates = [
1552
- this.#command === "build" ? path9.join(this.#app.dist.cwdPath, "public", rel) : null,
1553
- path9.join(this.#app.cwdPath, "public", rel),
2349
+ this.#command === "build" ? path14.join(this.#app.dist.cwdPath, "public", rel) : null,
2350
+ path14.join(this.#app.cwdPath, "public", rel),
1554
2351
  this.#resolveWorkspacePublicPath(rel)
1555
2352
  ].filter(Boolean);
1556
2353
  for (const candidate of candidates) {
@@ -1582,7 +2379,7 @@ class FontOptimizer {
1582
2379
  return "woff2";
1583
2380
  if (signature === "OTTO")
1584
2381
  return "otf";
1585
- const ext = path9.extname(sourcePath).slice(1).toLowerCase();
2382
+ const ext = path14.extname(sourcePath).slice(1).toLowerCase();
1586
2383
  if (ext === "otf" || ext === "woff" || ext === "woff2")
1587
2384
  return ext;
1588
2385
  return "ttf";
@@ -1591,7 +2388,7 @@ class FontOptimizer {
1591
2388
  const [root, dep, ...rest] = rel.split("/");
1592
2389
  if (root !== "libs" || !dep || rest.length === 0)
1593
2390
  return null;
1594
- return path9.join(this.#app.workspace.workspaceRoot, "libs", dep, "public", ...rest);
2391
+ return path14.join(this.#app.workspace.workspaceRoot, "libs", dep, "public", ...rest);
1595
2392
  }
1596
2393
  async#getSubsetText(font) {
1597
2394
  const parts = new Set;
@@ -1601,7 +2398,7 @@ class FontOptimizer {
1601
2398
  if (font.subsetText)
1602
2399
  parts.add(font.subsetText);
1603
2400
  for (const filePath of font.subsetFiles ?? []) {
1604
- const abs = path9.isAbsolute(filePath) ? filePath : path9.join(this.#app.cwdPath, filePath);
2401
+ const abs = path14.isAbsolute(filePath) ? filePath : path14.join(this.#app.cwdPath, filePath);
1605
2402
  const file = Bun.file(abs);
1606
2403
  if (await file.exists())
1607
2404
  parts.add(await file.text());
@@ -1621,7 +2418,7 @@ class FontOptimizer {
1621
2418
  }
1622
2419
  async#collectAutoSubsetText() {
1623
2420
  const libPageRoots = (await this.#app.getPageRoots()).filter((root) => root.keyPrefix).map((root) => root.dir);
1624
- const roots = [...["page", "ui", "lib"].map((dir) => path9.join(this.#app.cwdPath, dir)), ...libPageRoots];
2421
+ const roots = [...["page", "ui", "lib"].map((dir) => path14.join(this.#app.cwdPath, dir)), ...libPageRoots];
1625
2422
  const glob = new Bun.Glob("**/*.{ts,tsx,js,jsx,html,md}");
1626
2423
  const parts = [];
1627
2424
  for (const root of [...new Set(roots)].sort()) {
@@ -1734,8 +2531,8 @@ ${declarations.map(([prop, value]) => ` ${prop}: ${value};`).join(`
1734
2531
  }
1735
2532
 
1736
2533
  // pkgs/@akanjs/devkit/frontendBuild/fontPruner.ts
1737
- import { rm as rm2, rmdir, stat as stat3 } from "fs/promises";
1738
- import path10 from "path";
2534
+ import { rm as rm3, rmdir, stat as stat3 } from "fs/promises";
2535
+ import path15 from "path";
1739
2536
  import { isFontOptimizationEnabled } from "akanjs/client";
1740
2537
  var FONT_GLOB = "**/*.{woff2,woff,ttf,otf,ttc,eot}";
1741
2538
  var REFERRER_GLOB = "**/*.{css,js,mjs,cjs,json,html,htm,svg,xml,webmanifest}";
@@ -1748,8 +2545,8 @@ class FontPruner {
1748
2545
  constructor(app, { keepFonts = [] } = {}) {
1749
2546
  this.#app = app;
1750
2547
  this.#keepGlobs = keepFonts;
1751
- this.#publicRoot = path10.join(app.dist.cwdPath, "public");
1752
- this.#artifactRoot = path10.join(app.dist.cwdPath, ".akan/artifact");
2548
+ this.#publicRoot = path15.join(app.dist.cwdPath, "public");
2549
+ this.#artifactRoot = path15.join(app.dist.cwdPath, ".akan/artifact");
1753
2550
  }
1754
2551
  async prune() {
1755
2552
  const empty = { removed: [], kept: [], freedBytes: 0 };
@@ -1764,7 +2561,7 @@ class FontPruner {
1764
2561
  for (const font of fonts) {
1765
2562
  const target = isFontOptimizationEnabled(font) ? optimizedSrcs : unoptimizedSrcs;
1766
2563
  for (const fontPath of font.paths)
1767
- target.add(path10.posix.basename(fontPath.src));
2564
+ target.add(path15.posix.basename(fontPath.src));
1768
2565
  }
1769
2566
  const result = { removed: [], kept: [], freedBytes: 0 };
1770
2567
  const undecided = [];
@@ -1802,10 +2599,10 @@ class FontPruner {
1802
2599
  async#collectCandidates() {
1803
2600
  const candidates = [];
1804
2601
  for await (const rel of new Bun.Glob(FONT_GLOB).scan({ cwd: this.#publicRoot, dot: false })) {
1805
- const entry = await stat3(path10.join(this.#publicRoot, rel)).catch(() => null);
2602
+ const entry = await stat3(path15.join(this.#publicRoot, rel)).catch(() => null);
1806
2603
  if (!entry?.isFile())
1807
2604
  continue;
1808
- candidates.push({ rel: rel.split(path10.sep).join("/"), basename: path10.basename(rel), bytes: entry.size });
2605
+ candidates.push({ rel: rel.split(path15.sep).join("/"), basename: path15.basename(rel), bytes: entry.size });
1809
2606
  }
1810
2607
  return candidates;
1811
2608
  }
@@ -1815,11 +2612,11 @@ class FontPruner {
1815
2612
  const bundled = candidates.filter((candidate) => !optimizedSrcs.has(candidate.basename));
1816
2613
  const roots = [
1817
2614
  { dir: this.#publicRoot, label: "public", needles: authoritative },
1818
- { dir: path10.join(this.#artifactRoot, "styles"), label: ".akan/artifact/styles", needles: authoritative },
1819
- { dir: path10.join(this.#artifactRoot, "client"), label: ".akan/artifact/client", needles: bundled },
1820
- { dir: path10.join(this.#artifactRoot, "client-ssr"), label: ".akan/artifact/client-ssr", needles: bundled },
1821
- { dir: path10.join(this.#artifactRoot, "server"), label: ".akan/artifact/server", needles: bundled },
1822
- { dir: path10.join(this.#app.dist.cwdPath, "csr"), label: "csr", needles: bundled }
2615
+ { dir: path15.join(this.#artifactRoot, "styles"), label: ".akan/artifact/styles", needles: authoritative },
2616
+ { dir: path15.join(this.#artifactRoot, "client"), label: ".akan/artifact/client", needles: bundled },
2617
+ { dir: path15.join(this.#artifactRoot, "client-ssr"), label: ".akan/artifact/client-ssr", needles: bundled },
2618
+ { dir: path15.join(this.#artifactRoot, "server"), label: ".akan/artifact/server", needles: bundled },
2619
+ { dir: path15.join(this.#app.dist.cwdPath, "csr"), label: "csr", needles: bundled }
1823
2620
  ];
1824
2621
  for (const root of roots) {
1825
2622
  const pending = root.needles.filter((candidate) => !referrers.has(candidate.rel));
@@ -1838,20 +2635,20 @@ class FontPruner {
1838
2635
  const remaining = needles.filter((needle) => !referrers.has(needle.rel));
1839
2636
  if (!remaining.length)
1840
2637
  return;
1841
- const text = await Bun.file(path10.join(root.dir, rel)).text().catch(() => "");
2638
+ const text = await Bun.file(path15.join(root.dir, rel)).text().catch(() => "");
1842
2639
  if (!text)
1843
2640
  continue;
1844
2641
  for (const needle of remaining) {
1845
2642
  if (needle.forms.some((form) => text.includes(form)))
1846
- referrers.set(needle.rel, `${root.label}/${rel.split(path10.sep).join("/")}`);
2643
+ referrers.set(needle.rel, `${root.label}/${rel.split(path15.sep).join("/")}`);
1847
2644
  }
1848
2645
  }
1849
2646
  }
1850
2647
  async#remove(files) {
1851
- await Promise.all(files.map((file) => rm2(path10.join(this.#publicRoot, file), { force: true })));
1852
- const dirs = [...new Set(files.map((file) => path10.posix.dirname(file)))].filter((dir) => dir !== "." && dir !== "/").sort((a, b) => b.length - a.length);
2648
+ await Promise.all(files.map((file) => rm3(path15.join(this.#publicRoot, file), { force: true })));
2649
+ const dirs = [...new Set(files.map((file) => path15.posix.dirname(file)))].filter((dir) => dir !== "." && dir !== "/").sort((a, b) => b.length - a.length);
1853
2650
  for (const dir of dirs)
1854
- await rmdir(path10.join(this.#publicRoot, dir)).catch(() => {
2651
+ await rmdir(path15.join(this.#publicRoot, dir)).catch(() => {
1855
2652
  return;
1856
2653
  });
1857
2654
  }
@@ -1898,8 +2695,8 @@ class FontPruner {
1898
2695
  }
1899
2696
 
1900
2697
  // pkgs/@akanjs/devkit/frontendBuild/precompressArtifacts.ts
1901
- import fs from "fs";
1902
- import path11 from "path";
2698
+ import fs4 from "fs";
2699
+ import path16 from "path";
1903
2700
  import zlib from "zlib";
1904
2701
  var COMPRESSIBLE_EXTS = new Set([".css", ".html", ".js", ".json", ".svg"]);
1905
2702
  var MIN_COMPRESS_BYTES = 1024;
@@ -1909,8 +2706,8 @@ var GZIP_LEVEL = 9;
1909
2706
  var { formatBytes } = FontPruner;
1910
2707
  async function precompressArtifacts(app) {
1911
2708
  const roots = [
1912
- path11.join(app.dist.cwdPath, ".akan/artifact/client"),
1913
- path11.join(app.dist.cwdPath, ".akan/artifact/styles")
2709
+ path16.join(app.dist.cwdPath, ".akan/artifact/client"),
2710
+ path16.join(app.dist.cwdPath, ".akan/artifact/styles")
1914
2711
  ];
1915
2712
  const result = { files: 0, inputBytes: 0, outputBytes: 0, brotliBytes: 0 };
1916
2713
  await Promise.all(roots.map((root) => precompressRoot(root, result)));
@@ -1920,7 +2717,7 @@ async function precompressArtifacts(app) {
1920
2717
  return result;
1921
2718
  }
1922
2719
  async function precompressRoot(root, result) {
1923
- if (!fs.existsSync(root) || !fs.statSync(root).isDirectory())
2720
+ if (!fs4.existsSync(root) || !fs4.statSync(root).isDirectory())
1924
2721
  return;
1925
2722
  const glob = new Bun.Glob("**/*");
1926
2723
  for await (const filePath of glob.scan({ cwd: root, absolute: true })) {
@@ -1929,7 +2726,7 @@ async function precompressRoot(root, result) {
1929
2726
  const bytes = await Bun.file(filePath).bytes();
1930
2727
  const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
1931
2728
  const gz = Bun.gzipSync(buffer, { level: GZIP_LEVEL });
1932
- const br = brotliCompress(buffer, path11.extname(filePath).toLowerCase());
2729
+ const br = brotliCompress(buffer, path16.extname(filePath).toLowerCase());
1933
2730
  await Promise.all([Bun.write(`${filePath}.gz`, gz), Bun.write(`${filePath}.br`, br)]);
1934
2731
  result.files += 1;
1935
2732
  result.inputBytes += bytes.byteLength;
@@ -1949,7 +2746,7 @@ function brotliCompress(buffer, ext) {
1949
2746
  async function shouldPrecompress(filePath) {
1950
2747
  if (filePath.endsWith(".gz") || filePath.endsWith(".br"))
1951
2748
  return false;
1952
- if (!COMPRESSIBLE_EXTS.has(path11.extname(filePath).toLowerCase()))
2749
+ if (!COMPRESSIBLE_EXTS.has(path16.extname(filePath).toLowerCase()))
1953
2750
  return false;
1954
2751
  const file = Bun.file(filePath);
1955
2752
  if (!await file.exists())
@@ -1958,7 +2755,7 @@ async function shouldPrecompress(filePath) {
1958
2755
  }
1959
2756
 
1960
2757
  // pkgs/@akanjs/devkit/frontendBuild/ssrBaseArtifactBuilder.ts
1961
- import path12 from "path";
2758
+ import path17 from "path";
1962
2759
  import { optimize } from "@tailwindcss/node";
1963
2760
  function prepareCssAsset(command, basePath, cssText) {
1964
2761
  return optimize(cssText, { file: `${basePath || "root"}.css`, minify: command === "build" }).code;
@@ -1974,7 +2771,10 @@ class SsrBaseArtifactBuilder {
1974
2771
  this.#app = app;
1975
2772
  this.#command = command;
1976
2773
  this.#artifactDir = `${command === "build" ? app.dist.cwdPath : app.cwdPath}/.akan/artifact`;
1977
- this.#absArtifactDir = path12.resolve(this.#artifactDir);
2774
+ this.#absArtifactDir = path17.resolve(this.#artifactDir);
2775
+ }
2776
+ static servedWeb(command, web) {
2777
+ return command === "start" ? { ssr: true, csr: true } : web;
1978
2778
  }
1979
2779
  async build() {
1980
2780
  const akanConfig = await this.#app.getConfig();
@@ -1996,7 +2796,7 @@ class SsrBaseArtifactBuilder {
1996
2796
  rscRuntimeSsrManifest,
1997
2797
  vendorMap,
1998
2798
  cssAssets,
1999
- pagesBundlePath: this.#command === "build" ? path12.relative(this.#absArtifactDir, pagesBundle.bundlePath) : pagesBundle.bundlePath,
2799
+ pagesBundlePath: this.#command === "build" ? path17.relative(this.#absArtifactDir, pagesBundle.bundlePath) : pagesBundle.bundlePath,
2000
2800
  pagesBundleBuildId: pagesBundle.buildId,
2001
2801
  domains: [...akanConfig.domains],
2002
2802
  subRoutes: Object.fromEntries(Array.from(akanConfig.subRoutes.entries()).map(([basePath, domains]) => [basePath, [...domains]])),
@@ -2004,7 +2804,7 @@ class SsrBaseArtifactBuilder {
2004
2804
  branches: [...akanConfig.branches],
2005
2805
  i18n: akanConfig.i18n,
2006
2806
  imageConfig: akanConfig.images,
2007
- web: akanConfig.web,
2807
+ web: SsrBaseArtifactBuilder.servedWeb(this.#command, akanConfig.web),
2008
2808
  deepLinkAssociations: Object.values(akanConfig.mobile.targets).filter((target) => (target.deepLinks?.domains?.length ?? 0) > 0).map((target) => ({
2009
2809
  targetName: target.name,
2010
2810
  appId: target.appId,
@@ -2013,18 +2813,18 @@ class SsrBaseArtifactBuilder {
2013
2813
  androidSha256CertFingerprints: target.deepLinks?.android?.sha256CertFingerprints
2014
2814
  }))
2015
2815
  };
2016
- await Bun.write(path12.join(this.#absArtifactDir, "base-artifact.json"), `${JSON.stringify(artifact, null, 2)}
2816
+ await Bun.write(path17.join(this.#absArtifactDir, "base-artifact.json"), `${JSON.stringify(artifact, null, 2)}
2017
2817
  `);
2018
2818
  this.#app.verbose(`[base-artifact] complete in ${Date.now() - this.#started}ms`);
2019
2819
  return { artifact, seedIndex, cssCompiler, optimizedFonts };
2020
2820
  }
2021
2821
  async#buildRuntimeClientEntries() {
2022
2822
  const akanServerPath = await this.#resolveAkanServerPath();
2023
- const rscClientEntry = path12.resolve(akanServerPath, "rscClient.tsx");
2024
- const rscSegmentOutletEntry = path12.resolve(akanServerPath, "rscSegmentOutlet.tsx");
2823
+ const rscClientEntry = path17.resolve(akanServerPath, "rscClient.tsx");
2824
+ const rscSegmentOutletEntry = path17.resolve(akanServerPath, "rscSegmentOutlet.tsx");
2025
2825
  const vendorEntries = VENDOR_SPECIFIERS.map((specifier) => ({
2026
2826
  specifier,
2027
- absPath: path12.resolve(akanServerPath, "vendor", `${specifier.replaceAll("/", "-").replaceAll(".", "-")}.ts`)
2827
+ absPath: path17.resolve(akanServerPath, "vendor", `${specifier.replaceAll("/", "-").replaceAll(".", "-")}.ts`)
2028
2828
  }));
2029
2829
  const entries = [rscClientEntry, rscSegmentOutletEntry, ...vendorEntries.map((v) => v.absPath)];
2030
2830
  const clientBundle = await new ClientEntriesBundler({ app: this.#app, entries, command: this.#command }).bundle();
@@ -2061,15 +2861,15 @@ class SsrBaseArtifactBuilder {
2061
2861
  async#resolveAkanServerPath() {
2062
2862
  const candidates = [];
2063
2863
  try {
2064
- candidates.push(path12.dirname(Bun.resolveSync("akanjs/server", this.#app.workspace.workspaceRoot)));
2864
+ candidates.push(path17.dirname(Bun.resolveSync("akanjs/server", this.#app.workspace.workspaceRoot)));
2065
2865
  } catch {}
2066
- candidates.push(path12.join(this.#app.workspace.workspaceRoot, "pkgs/akanjs/server"), path12.join(this.#app.workspace.workspaceRoot, "node_modules/akanjs/server"));
2866
+ candidates.push(path17.join(this.#app.workspace.workspaceRoot, "pkgs/akanjs/server"), path17.join(this.#app.workspace.workspaceRoot, "node_modules/akanjs/server"));
2067
2867
  try {
2068
- candidates.push(path12.dirname(Bun.resolveSync("akanjs/server", path12.dirname(Bun.main))));
2868
+ candidates.push(path17.dirname(Bun.resolveSync("akanjs/server", path17.dirname(Bun.main))));
2069
2869
  } catch {}
2070
- candidates.push(path12.join(path12.dirname(Bun.main), "node_modules/akanjs/server"), path12.join(path12.dirname(Bun.main), "../../akanjs/server"), path12.resolve(import.meta.dir, "../../server"), path12.resolve(import.meta.dir, "../server"));
2870
+ candidates.push(path17.join(path17.dirname(Bun.main), "node_modules/akanjs/server"), path17.join(path17.dirname(Bun.main), "../../akanjs/server"), path17.resolve(import.meta.dir, "../../server"), path17.resolve(import.meta.dir, "../server"));
2071
2871
  for (const candidate of candidates) {
2072
- if (await Bun.file(path12.join(candidate, "rscClient.tsx")).exists())
2872
+ if (await Bun.file(path17.join(candidate, "rscClient.tsx")).exists())
2073
2873
  return candidate;
2074
2874
  }
2075
2875
  throw new Error(`[base-artifact] failed to locate akanjs/server; looked in: ${candidates.join(", ")}`);
@@ -2098,7 +2898,7 @@ ${preparedCssText}`).toString(36);
2098
2898
  `styles/${cssAssetName}-${cssHash}.css`,
2099
2899
  `/_akan/styles/${cssAssetName}-${cssHash}.css`
2100
2900
  ];
2101
- await Bun.write(path12.join(this.#absArtifactDir, cssRelPath), preparedCssText);
2901
+ await Bun.write(path17.join(this.#absArtifactDir, cssRelPath), preparedCssText);
2102
2902
  SsrBaseArtifactBuilder.#warnDroppedImports(this.#app, cssRelPath, preparedCssText, imported);
2103
2903
  this.#app.verbose(`[base-artifact] wrote ${preparedCssText.length} bytes of CSS for ${basePath} -> ${cssRelPath}`);
2104
2904
  return [basePath, { cssUrl, cssRelPath }];
@@ -2111,4 +2911,4 @@ ${preparedCssText}`).toString(36);
2111
2911
  }
2112
2912
  }
2113
2913
  }
2114
- export { computeRouteSeedIndex, saveRouteSeedIndex, AllRoutesBuilder, CssCompiler, PagesBundleBuilder, CsrArtifactBuilder, FontOptimizer, FontPruner, precompressArtifacts, SsrBaseArtifactBuilder };
2914
+ export { computeRouteSeedIndex, saveRouteSeedIndex, AllRoutesBuilder, CssCompiler, PagesBundleBuilder, CsrArtifactBuilder, CsrDevBundler, FontOptimizer, FontPruner, precompressArtifacts, SsrBaseArtifactBuilder };