@akanjs/cli 2.4.1-rc.2 → 2.4.1-rc.4

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 (44) hide show
  1. package/.build-stamp +1 -0
  2. package/{agent.command-h4afc69n.js → agent.command-t4346579.js} +6 -6
  3. package/{application.command-47mj9qsy.js → application.command-zkmeemdt.js} +5 -4
  4. package/{applicationBuildRunner-yz144508.js → applicationBuildRunner-5w8j9yp7.js} +2 -1
  5. package/buildBatch.proc.js +176 -0
  6. package/{capacitorApp-y0h6cgft.js → capacitorApp-wnxzrppx.js} +2 -2
  7. package/{cloud.command-hxcgsf80.js → cloud.command-5ztqxppm.js} +9 -8
  8. package/{context.command-nqbtak4f.js → context.command-smwxwgwf.js} +14 -14
  9. package/{guideline.command-ya0dh44f.js → guideline.command-1r0esdjs.js} +3 -3
  10. package/incrementalBuilder.proc.js +281 -160
  11. package/index-0fn1r7gg.js +466 -0
  12. package/{index-qhtr07v8.js → index-0jwvs8vp.js} +19 -19
  13. package/{index-a6sbyy0b.js → index-0t7pwff2.js} +3 -0
  14. package/{index-h6ca6qg0.js → index-2v10yr8g.js} +1 -1
  15. package/{index-73pr2cmy.js → index-2v6a4wd5.js} +4 -4
  16. package/{index-45aj5ry0.js → index-3sd2vape.js} +3 -3
  17. package/{index-y3hdhy4p.js → index-ahpcr9ss.js} +5 -5
  18. package/{index-8rc0bm04.js → index-eay4t1te.js} +2 -2
  19. package/{index-qaq13qk3.js → index-eqxmxan6.js} +1 -1
  20. package/{index-pmm9e2jf.js → index-fh990y67.js} +2 -2
  21. package/{index-sgmas1fc.js → index-gr4cjv99.js} +5 -5
  22. package/{index-1xdrsbry.js → index-hk58kr1m.js} +1 -1
  23. package/{index-85msc0wg.js → index-jbtn8h1y.js} +3 -3
  24. package/{index-a5rmdgy4.js → index-khzzttv1.js} +1362 -1447
  25. package/{index-77crfweb.js → index-n5y2gbf2.js} +478 -20
  26. package/{index-n6h3482q.js → index-nv7et4t3.js} +2 -2
  27. package/{index-hwzpw9c1.js → index-pya1h7wx.js} +3 -3
  28. package/{index-hdqztm58.js → index-qpz4csbs.js} +1 -1
  29. package/{index-mq6ns0f9.js → index-s9yajzkj.js} +9 -9
  30. package/{index-8pkbzj26.js → index-wh201a76.js} +7 -7
  31. package/{index-swf4bmbg.js → index-y4kg682s.js} +1 -1
  32. package/index.js +18 -18
  33. package/{library.command-r15zdqvp.js → library.command-jrew04m6.js} +3 -3
  34. package/{localRegistry.command-6z4s13mj.js → localRegistry.command-m08dkm1b.js} +8 -7
  35. package/{module.command-qrj3kmyz.js → module.command-p98t7522.js} +6 -6
  36. package/{package.command-5x5m0ej1.js → package.command-57dyrfmn.js} +3 -3
  37. package/package.json +2 -2
  38. package/{page.command-c6xdx0xm.js → page.command-tas6f3na.js} +3 -3
  39. package/{primitive.command-pv9ssmtf.js → primitive.command-bjrbsakw.js} +7 -7
  40. package/{quality.command-es67wvdp.js → quality.command-zavaafbw.js} +2 -2
  41. package/{repair.command-677675vw.js → repair.command-je57wx67.js} +5 -5
  42. package/{scalar.command-kabkd6wd.js → scalar.command-gqqyy1kr.js} +5 -5
  43. package/{workflow.command-64r6cw0w.js → workflow.command-d6mrc47s.js} +10 -10
  44. package/{workspace.command-875aj35r.js → workspace.command-bbg96z8k.js} +24 -23
@@ -2636,1555 +2636,1499 @@ ${CsrArtifactBuilder.escapeInlineScript(await loadScript(src))}
2636
2636
  }
2637
2637
  }
2638
2638
 
2639
- // pkgs/@akanjs/devkit/frontendBuild/devChangePlanner.ts
2639
+ // pkgs/@akanjs/devkit/frontendBuild/cssCompiler.ts
2640
+ import path15 from "path";
2641
+ import { Logger as Logger2 } from "akanjs/common";
2642
+ import { compile } from "tailwindcss";
2643
+
2644
+ // pkgs/@akanjs/devkit/frontendBuild/cssImportResolver.ts
2640
2645
  import path14 from "path";
2641
- var SOURCE_EXTS2 = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
2642
- var CONFIG_BASENAMES = new Set(["akan.config.ts", "bunfig.toml", "tsconfig.json", "package.json"]);
2643
- var BARREL_FACETS = new Set(["common", "srvkit", "ui", "webkit", "plugin"]);
2644
- var CLIENT_SUFFIXES = [".Template.tsx", ".Unit.tsx", ".Util.tsx", ".View.tsx", ".Zone.tsx", ".store.ts"];
2645
- var SHARED_SUFFIXES = [".constant.ts", ".dictionary.ts", ".signal.ts"];
2646
- var SERVER_SUFFIXES = [".service.ts", ".document.ts"];
2647
- var RUNTIME_METADATA_BASENAMES = new Set(["dict.ts", "sig.ts", "useClient.ts", "useServer.ts"]);
2646
+ var CSS_IMPORT_EXTS = ["", ".css", "/styles.css", "/index.css"];
2648
2647
 
2649
- class DevChangePlanner {
2648
+ class CssImportResolver {
2650
2649
  #workspaceRoot;
2651
- constructor({ workspaceRoot }) {
2652
- this.#workspaceRoot = path14.resolve(workspaceRoot);
2650
+ #paths;
2651
+ #wildcardEntries;
2652
+ constructor(workspaceRoot, paths = {}) {
2653
+ this.#workspaceRoot = workspaceRoot;
2654
+ this.#paths = paths;
2655
+ this.#wildcardEntries = Object.entries(paths).filter(([key]) => key.endsWith("/*")).map(([key, replacements]) => ({ prefix: key.slice(0, -1), replacements })).sort((a, b) => b.prefix.length - a.prefix.length);
2653
2656
  }
2654
- plan({ generation, files, kinds, generatedFiles = [] }) {
2655
- const fileList = uniqueResolved([...files, ...generatedFiles]);
2656
- const generatedSet = new Set(generatedFiles.map((file) => path14.resolve(file)));
2657
- const kindSet = new Set(kinds);
2658
- const roles = new Set;
2659
- const actions = new Set;
2660
- const reasonByFile = {};
2661
- for (const kind of kindSet) {
2662
- if (kind === "css") {
2663
- roles.add("css");
2664
- actions.add("rebuild-css");
2665
- }
2666
- if (kind === "config") {
2667
- roles.add("config");
2668
- actions.add("restart-dev-host");
2669
- }
2670
- }
2671
- for (const file of fileList) {
2672
- const reasons = new Set;
2673
- const fileRoles = this.#rolesForFile(file, { isGenerated: generatedSet.has(path14.resolve(file)), reasons });
2674
- for (const role of fileRoles)
2675
- roles.add(role);
2676
- if (reasons.has("runtime-metadata"))
2677
- actions.add("restart-builder");
2678
- if (reasons.size > 0)
2679
- reasonByFile[path14.resolve(file)] = [...reasons].sort();
2680
- }
2681
- if (roles.has("barrel"))
2682
- actions.add("sync-generated");
2683
- if (roles.has("server") || roles.has("shared"))
2684
- actions.add("restart-backend");
2685
- if (roles.has("client") || roles.has("shared"))
2686
- actions.add("rebuild-client");
2687
- if (roles.has("css"))
2688
- actions.add("rebuild-css");
2689
- return {
2690
- generation,
2691
- files: fileList,
2692
- generatedFiles: uniqueResolved(generatedFiles),
2693
- roles: [...roles].sort(),
2694
- actions: [...actions].sort(),
2695
- reasonByFile
2696
- };
2657
+ static async create(app) {
2658
+ const tsconfig = await app.getTsConfig();
2659
+ return new CssImportResolver(app.workspace.workspaceRoot, tsconfig.compilerOptions.paths ?? {});
2697
2660
  }
2698
- #rolesForFile(file, { isGenerated, reasons }) {
2699
- const roles = new Set;
2700
- const abs = path14.resolve(file);
2701
- const base = path14.basename(abs);
2702
- const ext = path14.extname(abs).toLowerCase();
2703
- const isSource = SOURCE_EXTS2.has(ext);
2704
- const rel = path14.relative(this.#workspaceRoot, abs);
2705
- const parts = rel.split(path14.sep).filter(Boolean);
2706
- if (CONFIG_BASENAMES.has(base)) {
2707
- roles.add("config");
2708
- reasons.add("config-file");
2709
- }
2710
- if (ext === ".css") {
2711
- roles.add("css");
2712
- reasons.add("css-file");
2713
- }
2714
- if (isGenerated || isSource && this.#isBarrelFacetChild(parts)) {
2715
- roles.add("barrel");
2716
- reasons.add(isGenerated ? "generated-index" : "barrel-facet-child");
2661
+ async resolve(id, fromBase) {
2662
+ for (const resolve of [
2663
+ () => this.#resolveWithTsconfig(id),
2664
+ () => this.#resolveWithBun(id, fromBase),
2665
+ () => this.#resolveWithRequire(id, fromBase),
2666
+ () => this.#resolvePackageStyle(id, fromBase)
2667
+ ]) {
2668
+ const resolved = await resolve();
2669
+ if (resolved)
2670
+ return resolved;
2717
2671
  }
2718
- if (isSource && this.#isServerBiased(abs, parts)) {
2719
- roles.add("server");
2720
- reasons.add("server-path");
2672
+ return null;
2673
+ }
2674
+ #resolveWithBun(id, fromBase) {
2675
+ for (const base of this.#resolutionBases(fromBase)) {
2676
+ try {
2677
+ const resolved = Bun.resolveSync(id, base);
2678
+ if (CssImportResolver.isCssFile(resolved))
2679
+ return resolved;
2680
+ } catch {}
2721
2681
  }
2722
- if (isSource && this.#isClientBiased(abs, parts)) {
2723
- roles.add("client");
2724
- reasons.add("client-path");
2682
+ return null;
2683
+ }
2684
+ #resolveWithRequire(id, fromBase) {
2685
+ for (const base of this.#resolutionBases(fromBase)) {
2686
+ try {
2687
+ const resolved = __require.resolve(id, { paths: [base] });
2688
+ if (CssImportResolver.isCssFile(resolved))
2689
+ return resolved;
2690
+ } catch {}
2725
2691
  }
2726
- if (isSource && this.#isSharedBiased(abs, parts)) {
2727
- roles.add("shared");
2728
- reasons.add("shared-path");
2692
+ return null;
2693
+ }
2694
+ async#resolveWithTsconfig(id) {
2695
+ const exact = this.#paths[id];
2696
+ if (exact) {
2697
+ for (const repl of exact) {
2698
+ const resolved = await this.#firstExisting(path14.resolve(this.#workspaceRoot, repl));
2699
+ if (resolved)
2700
+ return resolved;
2701
+ }
2729
2702
  }
2730
- if (isSource && this.#isRuntimeMetadataFile(parts, base)) {
2731
- reasons.add("runtime-metadata");
2703
+ for (const { prefix, replacements } of this.#wildcardEntries) {
2704
+ if (!id.startsWith(prefix))
2705
+ continue;
2706
+ const suffix = id.slice(prefix.length);
2707
+ for (const repl of replacements) {
2708
+ const replPath = repl.endsWith("/*") ? repl.slice(0, -1) : repl;
2709
+ const resolved = await this.#firstExisting(path14.resolve(this.#workspaceRoot, replPath + suffix));
2710
+ if (resolved)
2711
+ return resolved;
2712
+ }
2732
2713
  }
2733
- if (roles.has("server") && roles.has("client")) {
2734
- roles.delete("server");
2735
- roles.delete("client");
2736
- roles.add("shared");
2737
- reasons.add("server-client-overlap");
2714
+ return null;
2715
+ }
2716
+ async#resolvePackageStyle(id, fromBase) {
2717
+ const pkgName = CssImportResolver.getPackageName(id);
2718
+ if (!pkgName)
2719
+ return null;
2720
+ for (const base of this.#resolutionBases(fromBase)) {
2721
+ try {
2722
+ const pkgPath = __require.resolve(`${pkgName}/package.json`, { paths: [base] });
2723
+ const resolved = await this.#resolvePackageStyleFromPackageJson(id, pkgName, pkgPath);
2724
+ if (resolved)
2725
+ return resolved;
2726
+ } catch {}
2738
2727
  }
2739
- if (roles.size === 0 && SOURCE_EXTS2.has(ext) && this.#isWorkspaceSource(rel)) {
2740
- roles.add("shared");
2741
- reasons.add("workspace-source-fallback");
2728
+ for (const pkgPath of this.#packageJsonCandidates(pkgName)) {
2729
+ const resolved = await this.#resolvePackageStyleFromPackageJson(id, pkgName, pkgPath);
2730
+ if (resolved)
2731
+ return resolved;
2742
2732
  }
2743
- return roles;
2733
+ return null;
2744
2734
  }
2745
- #isServerBiased(abs, parts) {
2746
- const base = path14.basename(abs);
2747
- return parts.includes("srvkit") || SERVER_SUFFIXES.some((suffix) => base.endsWith(suffix)) || base === "main.ts" || base === "server.ts";
2735
+ async#resolvePackageStyleFromPackageJson(id, pkgName, pkgPath) {
2736
+ try {
2737
+ if (!await Bun.file(pkgPath).exists())
2738
+ return null;
2739
+ const pkgDir = path14.dirname(pkgPath);
2740
+ const pkg = await Bun.file(pkgPath).json();
2741
+ const subpath = id === pkgName ? "." : `.${id.slice(pkgName.length)}`;
2742
+ const exportValue = pkg.exports?.[subpath];
2743
+ const styleEntry = (typeof exportValue === "string" ? exportValue : exportValue?.style || exportValue?.import || exportValue?.default) || pkg.exports?.["."]?.style || pkg.style || "index.css";
2744
+ return await this.#firstExisting(path14.resolve(pkgDir, styleEntry));
2745
+ } catch {
2746
+ return null;
2747
+ }
2748
2748
  }
2749
- #isClientBiased(abs, parts) {
2750
- const base = path14.basename(abs);
2751
- return parts.includes("ui") || parts.includes("webkit") || parts.includes("page") || CLIENT_SUFFIXES.some((suffix) => base.endsWith(suffix));
2749
+ #resolutionBases(fromBase) {
2750
+ return [fromBase, this.#workspaceRoot, path14.dirname(Bun.main), path14.resolve(path14.dirname(Bun.main), "../..")];
2752
2751
  }
2753
- #isSharedBiased(abs, parts) {
2754
- const base = path14.basename(abs);
2755
- return parts.includes("common") || SHARED_SUFFIXES.some((suffix) => base.endsWith(suffix)) || RUNTIME_METADATA_BASENAMES.has(base);
2752
+ #packageJsonCandidates(pkgName) {
2753
+ return [
2754
+ path14.join(this.#workspaceRoot, "pkgs", pkgName, "package.json"),
2755
+ path14.join(this.#workspaceRoot, "node_modules", pkgName, "package.json"),
2756
+ path14.join(path14.dirname(Bun.main), "node_modules", pkgName, "package.json"),
2757
+ path14.join(path14.dirname(Bun.main), "../../", pkgName, "package.json")
2758
+ ];
2756
2759
  }
2757
- #isRuntimeMetadataFile(parts, base) {
2758
- const parent = parts.at(-2);
2759
- if (parent === "lib" && RUNTIME_METADATA_BASENAMES.has(base))
2760
- return true;
2761
- const libIndex = parts.lastIndexOf("lib");
2762
- if (libIndex < 0 || parts.length <= libIndex + 1)
2763
- return false;
2764
- return base.endsWith(".dictionary.ts") || base.endsWith(".signal.ts");
2760
+ async#firstExisting(basePath) {
2761
+ for (const suffix of CSS_IMPORT_EXTS) {
2762
+ const candidate = `${basePath}${suffix}`;
2763
+ if (await Bun.file(candidate).exists())
2764
+ return candidate;
2765
+ }
2766
+ return null;
2765
2767
  }
2766
- #isBarrelFacetChild(parts) {
2767
- if (parts.length < 4)
2768
- return false;
2769
- const [scope, , facet, child] = parts;
2770
- if (scope !== "apps" && scope !== "libs")
2771
- return false;
2772
- if (!facet || !BARREL_FACETS.has(facet))
2773
- return false;
2774
- if (!child || child.startsWith(".") || child === "index.ts" || child === "index.tsx")
2775
- return false;
2776
- return true;
2768
+ static getPackageName(id) {
2769
+ const parts = id.split("/");
2770
+ if (id.startsWith("@"))
2771
+ return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
2772
+ return parts[0] ?? null;
2777
2773
  }
2778
- #isWorkspaceSource(rel) {
2779
- if (!rel || rel.startsWith("..") || path14.isAbsolute(rel))
2780
- return false;
2781
- const [scope] = rel.split(path14.sep);
2782
- return scope === "apps" || scope === "libs" || scope === "pkgs";
2774
+ static isCssFile(filePath) {
2775
+ return path14.extname(filePath) === ".css";
2783
2776
  }
2784
2777
  }
2785
- var uniqueResolved = (files) => [...new Set(files.map((file) => path14.resolve(file)))].sort();
2786
2778
 
2787
- // pkgs/@akanjs/devkit/frontendBuild/devGeneratedIndexSync.ts
2788
- import { mkdir as mkdir4, readdir as readdir2, readFile as readFile2, rm as rm2, stat as stat2, writeFile as writeFile2 } from "fs/promises";
2789
- import path15 from "path";
2790
- var BARREL_FACETS2 = new Set(["common", "srvkit", "ui", "webkit", "plugin"]);
2791
- var FACET_SOURCE_FILE_RE = /\.(ts|tsx)$/;
2792
- var FACET_EXCLUDED_FILE_RE = /(^index\.tsx?$|\.d\.ts$|\.(test|spec)\.(ts|tsx)$|\.css$|\.scss$|\.sass$)/;
2793
- var FACET_PASCAL_CASE_RE = /^[A-Z][A-Za-z0-9]*$/;
2794
- var FACET_CAMEL_CASE_RE = /^[a-z][A-Za-z0-9]*$/;
2795
- var MODULE_UI_TYPES = ["Template", "Unit", "Util", "View", "Zone"];
2796
- var SERVICE_UI_TYPES = ["Util", "Zone"];
2797
- var SCALAR_UI_TYPES = ["Template", "Unit"];
2779
+ // pkgs/@akanjs/devkit/frontendBuild/cssCompiler.ts
2780
+ var SOURCE_EXTS2 = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
2781
+ var NON_SOURCE_EXT_RE2 = /\.(json|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp3|mp4|wav)$/i;
2782
+ var NODE_MODULES_RE3 = /[\\/]node_modules[\\/]/;
2783
+ var AKANJS_NODE_MODULE_RE3 = /[\\/]node_modules[\\/]akanjs[\\/]/;
2798
2784
 
2799
- class DevGeneratedIndexSync {
2800
- #workspaceRoot;
2801
- constructor({ workspaceRoot }) {
2802
- this.#workspaceRoot = path15.resolve(workspaceRoot);
2785
+ class CssCompiler {
2786
+ #logger = new Logger2("CssCompiler");
2787
+ #transpiler = new Bun.Transpiler({ loader: "tsx" });
2788
+ #app;
2789
+ #cssImportResolver = null;
2790
+ constructor(app) {
2791
+ this.#app = app;
2803
2792
  }
2804
- async syncForBatch(files) {
2805
- const indexPaths = new Set;
2806
- const errors = [];
2807
- for (const file of files) {
2808
- const facetIndex = this.#facetIndexFor(file);
2809
- if (facetIndex)
2810
- indexPaths.add(facetIndex);
2811
- const moduleIndex = await this.#moduleIndexForDirectoryEvent(file).catch((err) => {
2812
- errors.push(`[generated-index] module detection failed for ${file}: ${formatError2(err)}`);
2813
- return null;
2814
- });
2815
- if (moduleIndex)
2816
- indexPaths.add(moduleIndex);
2817
- }
2818
- const changedFiles = [];
2819
- for (const indexPath of [...indexPaths].sort()) {
2820
- try {
2821
- const changed = await this.#syncIndex(indexPath);
2822
- if (changed)
2823
- changedFiles.push(indexPath);
2824
- } catch (err) {
2825
- errors.push(`[generated-index] sync failed for ${indexPath}: ${formatError2(err)}`);
2826
- }
2827
- }
2828
- return { changedFiles, errors };
2829
- }
2830
- #facetIndexFor(file) {
2831
- const abs = path15.resolve(file);
2832
- const rel = path15.relative(this.#workspaceRoot, abs);
2833
- if (rel.startsWith("..") || path15.isAbsolute(rel))
2834
- return null;
2835
- const parts = rel.split(path15.sep).filter(Boolean);
2836
- if (parts.length < 4)
2837
- return null;
2838
- const [scope, project, facet, child] = parts;
2839
- if (scope !== "apps" && scope !== "libs" || !project || !facet || !BARREL_FACETS2.has(facet))
2840
- return null;
2841
- if (!child || child.startsWith(".") || child === "index.ts" || child === "index.tsx")
2842
- return null;
2843
- return path15.join(this.#workspaceRoot, scope, project, facet, "index.ts");
2844
- }
2845
- async#moduleIndexForDirectoryEvent(file) {
2846
- const abs = path15.resolve(file);
2847
- const rel = path15.relative(this.#workspaceRoot, abs);
2848
- if (rel.startsWith("..") || path15.isAbsolute(rel))
2849
- return null;
2850
- const parts = rel.split(path15.sep).filter(Boolean);
2851
- if (parts.length !== 4 && parts.length !== 5)
2852
- return null;
2853
- const [scope, project, libSegment, moduleSegment, scalarSegment] = parts;
2854
- if (scope !== "apps" && scope !== "libs" || !project || libSegment !== "lib")
2855
- return null;
2856
- const isExistingDirectory = await stat2(abs).then((s) => s.isDirectory()).catch(() => false);
2857
- const looksLikeDeletedDirectory = !path15.extname(abs);
2858
- if (!isExistingDirectory && !looksLikeDeletedDirectory)
2859
- return null;
2860
- if (moduleSegment === "__scalar" && scalarSegment) {
2861
- return path15.join(this.#workspaceRoot, scope, project, "lib", "__scalar", scalarSegment, "index.ts");
2862
- }
2863
- if (!moduleSegment || moduleSegment === "__scalar")
2864
- return null;
2865
- return path15.join(this.#workspaceRoot, scope, project, "lib", moduleSegment, "index.ts");
2866
- }
2867
- async#syncIndex(indexPath) {
2868
- const dir = path15.dirname(indexPath);
2869
- const content = await this.#contentForIndex(indexPath);
2870
- if (content === null) {
2871
- if (!await exists(indexPath))
2872
- return false;
2873
- await rm2(indexPath, { force: true });
2874
- return true;
2875
- }
2876
- const current = await readFile2(indexPath, "utf8").catch(() => null);
2877
- if (current === content)
2878
- return false;
2879
- await mkdir4(dir, { recursive: true });
2880
- await writeFile2(indexPath, content);
2881
- return true;
2882
- }
2883
- async#contentForIndex(indexPath) {
2884
- const parts = path15.relative(this.#workspaceRoot, indexPath).split(path15.sep).filter(Boolean);
2885
- const facet = parts.at(-2);
2886
- if (facet && BARREL_FACETS2.has(facet))
2887
- return this.#facetContent(path15.dirname(indexPath));
2888
- return this.#moduleContent(path15.dirname(indexPath));
2889
- }
2890
- async#facetContent(dir) {
2891
- const nameCasePattern = path15.basename(dir) === "ui" ? FACET_PASCAL_CASE_RE : FACET_CAMEL_CASE_RE;
2892
- const entries = await readdir2(dir, { withFileTypes: true }).catch(() => []);
2893
- const exportNames = entries.flatMap((entry) => {
2894
- const name = entry.name;
2895
- if (name.startsWith("."))
2896
- return [];
2897
- if (entry.isDirectory())
2898
- return nameCasePattern.test(name) ? [name] : [];
2899
- if (!entry.isFile())
2900
- return [];
2901
- if (!FACET_SOURCE_FILE_RE.test(name) || FACET_EXCLUDED_FILE_RE.test(name))
2902
- return [];
2903
- const exportName = name.replace(FACET_SOURCE_FILE_RE, "");
2904
- return nameCasePattern.test(exportName) ? [exportName] : [];
2905
- }).sort();
2906
- if (exportNames.length === 0)
2907
- return null;
2908
- return `${exportNames.map((name) => `export * from "./${name}";`).join(`
2909
- `)}
2910
- `;
2793
+ #cssText = null;
2794
+ #cssTextByBasePath = null;
2795
+ async getCss({ refresh } = {}) {
2796
+ if (this.#cssText !== null && !refresh)
2797
+ return this.#cssText;
2798
+ const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh });
2799
+ this.#cssText = await this.compileCss(cssPaths, sourcePaths);
2800
+ return this.#cssText;
2911
2801
  }
2912
- async#moduleContent(dir) {
2913
- const rel = path15.relative(this.#workspaceRoot, dir);
2914
- const parts = rel.split(path15.sep).filter(Boolean);
2915
- const moduleSegment = parts.at(-1);
2916
- if (!moduleSegment)
2917
- return null;
2918
- const isScalar = parts.at(-2) === "__scalar";
2919
- const rawModel = moduleSegment.startsWith("_") ? moduleSegment.slice(1) : moduleSegment;
2920
- if (!rawModel)
2921
- return null;
2922
- const modelName = capitalize(rawModel);
2923
- const allowedTypes = isScalar ? SCALAR_UI_TYPES : moduleSegment.startsWith("_") ? SERVICE_UI_TYPES : MODULE_UI_TYPES;
2924
- const fileTypes = [];
2925
- for (const type of allowedTypes) {
2926
- if (await exists(path15.join(dir, `${modelName}.${type}.tsx`)))
2927
- fileTypes.push(type);
2928
- }
2929
- if (fileTypes.length === 0)
2930
- return null;
2931
- return `
2932
- ${fileTypes.map((type) => `import * as ${type} from "./${modelName}.${type}";`).join(`
2933
- `)}
2934
-
2935
- export const ${modelName} = { ${fileTypes.join(", ")} };`;
2802
+ async getCssByBasePath({ refresh } = {}) {
2803
+ if (this.#cssTextByBasePath !== null && !refresh)
2804
+ return this.#cssTextByBasePath;
2805
+ const akanConfig = await this.#app.getConfig({ refresh });
2806
+ const pageKeys = await this.#app.getPageKeys({ refresh });
2807
+ const basePaths = [...akanConfig.basePaths];
2808
+ const rootPageKeys = pageKeys.filter((pageKey) => getPageKeyBasePath(pageKey, basePaths) === null);
2809
+ const cssEntries = await Promise.all([
2810
+ (async () => {
2811
+ if (rootPageKeys.length === 0)
2812
+ return ["", ""];
2813
+ const started = Date.now();
2814
+ const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: rootPageKeys });
2815
+ const css = await this.compileCss(cssPaths, sourcePaths);
2816
+ this.#logger.verbose(`css base=root paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`);
2817
+ return ["", css];
2818
+ })(),
2819
+ ...basePaths.map(async (basePath) => {
2820
+ const basePathPageKeys = pageKeys.filter((pageKey) => getPageKeyBasePath(pageKey, basePaths) === basePath);
2821
+ if (basePathPageKeys.length === 0)
2822
+ return [basePath, ""];
2823
+ const started = Date.now();
2824
+ const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: basePathPageKeys });
2825
+ const css = await this.compileCss(cssPaths, sourcePaths);
2826
+ this.#logger.verbose(`css base=${basePath} paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`);
2827
+ return [basePath, css];
2828
+ })
2829
+ ]);
2830
+ this.#cssTextByBasePath = Object.fromEntries(cssEntries);
2831
+ return this.#cssTextByBasePath;
2936
2832
  }
2937
- }
2938
- var exists = async (file) => stat2(file).then(() => true).catch(() => false);
2939
- var capitalize = (value) => `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
2940
- var formatError2 = (err) => err instanceof Error ? err.message : String(err);
2941
-
2942
- // pkgs/@akanjs/devkit/frontendBuild/fontOptimizer.ts
2943
- import { mkdir as mkdir5 } from "fs/promises";
2944
- import path16 from "path";
2945
- import {
2946
- generateFontFace,
2947
- getMetricsForFamily,
2948
- readMetrics,
2949
- resolveCategoryFallbacks
2950
- } from "fontaine";
2951
- import { createFont, woff2 } from "fonteditor-core";
2952
- import subsetFont from "subset-font";
2953
- import ts4 from "typescript";
2954
- var FONT_URL_PREFIX = "/_akan/fonts";
2955
- var DEFAULT_FONT_SUBSETS = ["latin"];
2956
-
2957
- class FontOptimizer {
2958
- #app;
2959
- #command;
2960
- #artifactRoot;
2961
- #files = [];
2962
- #cssParts = [];
2963
- #woff2Ready = null;
2964
- static #ksX1001Text = null;
2965
- constructor(app, command = "start") {
2966
- this.#app = app;
2967
- this.#command = command;
2968
- this.#artifactRoot = path16.join(command === "build" ? app.dist.cwdPath : app.cwdPath, ".akan/artifact");
2833
+ async discoverCss({ refresh } = {}) {
2834
+ const { cssPaths } = await this.discoverCssAndSources({ refresh });
2835
+ return cssPaths;
2969
2836
  }
2970
- async optimize() {
2971
- const fonts = await this.discoverFonts();
2972
- for (const font of fonts) {
2973
- if (!this.#isFontOptimizationEnabled(font))
2837
+ async discoverCssAndSources({
2838
+ refresh,
2839
+ pageKeys
2840
+ } = {}) {
2841
+ pageKeys ??= await this.#app.getPageKeys({ refresh });
2842
+ const seeds = pageKeys.map((key) => path15.resolve(this.#app.cwdPath, "page", key));
2843
+ const cssFiles = new Set;
2844
+ const sourceFiles = new Set;
2845
+ const queue = [...seeds];
2846
+ const resolvePackage = await createTsconfigPackageResolver(this.#app);
2847
+ const analyzer = new BarrelAnalyzer({ resolvePackage });
2848
+ const akanConfig = await this.#app.getConfig({ refresh });
2849
+ while (queue.length > 0) {
2850
+ const filePath = queue.shift();
2851
+ if (!filePath || sourceFiles.has(filePath) || isIgnoredNodeModuleSource(filePath))
2974
2852
  continue;
2975
- await this.#optimizeFont(font);
2976
- }
2977
- const fontUtilityCss = this.#buildFontUtilityRules(fonts);
2978
- if (fontUtilityCss)
2979
- this.#cssParts.push(fontUtilityCss);
2980
- return { css: this.#cssParts.join(`
2981
- `), fonts, files: this.#files };
2982
- }
2983
- async discoverFonts() {
2984
- const pageKeys = await this.#app.getPageKeys();
2985
- const fonts = [];
2986
- await Promise.all(pageKeys.map(async (key) => {
2987
- const filePath = path16.resolve(this.#app.cwdPath, "page", key);
2988
- const file = Bun.file(filePath);
2989
- if (!await file.exists())
2990
- return;
2991
- fonts.push(...this.#extractFontsExport(await file.text(), filePath));
2992
- }));
2993
- return this.#dedupeFonts(fonts);
2994
- }
2995
- async#optimizeFont(font) {
2996
- const faceCss = [];
2997
- for (const face of this.#getFontFaces(font)) {
2998
- const sourcePath = await this.#resolveFontSourcePath(face.src);
2999
- if (!sourcePath) {
3000
- this.#app.logger.warn(`[font] source not found: ${face.src}`);
2853
+ sourceFiles.add(filePath);
2854
+ let content;
2855
+ try {
2856
+ content = await Bun.file(filePath).text();
2857
+ } catch {
3001
2858
  continue;
3002
2859
  }
3003
- const outputPath = path16.join(this.#artifactRoot, face.optimizedSrc.replace(/^\/_akan\//, ""));
3004
- await mkdir5(path16.dirname(outputPath), { recursive: true });
3005
- const sourceBuffer = Buffer.from(await Bun.file(sourcePath).arrayBuffer());
3006
- const outputBuffer = font.subset === false ? await this.#convertToWoff2(sourceBuffer, sourcePath) : await subsetFont(sourceBuffer, await this.#getSubsetText(font), { targetFormat: "woff2" });
3007
- await Bun.write(outputPath, outputBuffer);
3008
- this.#files.push(outputPath);
3009
- faceCss.push(this.#buildOptimizedFontFaceRule(font, face));
3010
- const fallbackCss = await this.#buildFontaineFallbackCss(font, face, outputPath);
3011
- if (fallbackCss)
3012
- faceCss.push(fallbackCss);
3013
- }
3014
- if (faceCss.length > 0)
3015
- this.#cssParts.push(...faceCss, this.#buildRootVariableRule(font));
3016
- }
3017
- #extractFontsExport(source, filePath) {
3018
- const sourceFile = ts4.createSourceFile(filePath, source, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TSX);
3019
- const fonts = [];
3020
- for (const statement of sourceFile.statements) {
3021
- if (!ts4.isVariableStatement(statement))
3022
- continue;
3023
- const modifiers = ts4.canHaveModifiers(statement) ? ts4.getModifiers(statement) : undefined;
3024
- const isExported = modifiers?.some((modifier) => modifier.kind === ts4.SyntaxKind.ExportKeyword) ?? false;
3025
- if (!isExported)
2860
+ let source = content;
2861
+ if (akanConfig.barrelImports.length > 0) {
2862
+ try {
2863
+ const rewritten = await rewriteBarrelImports(content, akanConfig.barrelImports, analyzer);
2864
+ if (rewritten !== null)
2865
+ source = rewritten;
2866
+ } catch {}
2867
+ }
2868
+ let imports;
2869
+ try {
2870
+ imports = this.#transpiler.scanImports(source);
2871
+ } catch {
3026
2872
  continue;
3027
- for (const declaration of statement.declarationList.declarations) {
3028
- if (!ts4.isIdentifier(declaration.name) || declaration.name.text !== "fonts")
2873
+ }
2874
+ const importerDir = path15.dirname(filePath);
2875
+ for (const imp of imports) {
2876
+ const spec = imp.path;
2877
+ if (!spec)
2878
+ continue;
2879
+ if (spec.endsWith(".css")) {
2880
+ const cssPath = await this.#resolveCssImport(spec, importerDir);
2881
+ cssFiles.add(cssPath);
3029
2882
  continue;
3030
- const value = declaration.initializer ? this.#literalToValue(declaration.initializer) : null;
3031
- if (Array.isArray(value)) {
3032
- fonts.push(...value.map((font) => this.#withFontDefaults(font)));
3033
2883
  }
3034
- }
3035
- }
3036
- return fonts;
3037
- }
3038
- #literalToValue(node) {
3039
- if (ts4.isStringLiteralLike(node))
3040
- return node.text;
3041
- if (ts4.isNumericLiteral(node))
3042
- return Number(node.text);
3043
- if (node.kind === ts4.SyntaxKind.TrueKeyword)
3044
- return true;
3045
- if (node.kind === ts4.SyntaxKind.FalseKeyword)
3046
- return false;
3047
- if (ts4.isArrayLiteralExpression(node))
3048
- return node.elements.map((element) => this.#literalToValue(element));
3049
- if (ts4.isObjectLiteralExpression(node)) {
3050
- const obj = {};
3051
- for (const prop of node.properties) {
3052
- if (!ts4.isPropertyAssignment(prop))
2884
+ if (NON_SOURCE_EXT_RE2.test(spec))
3053
2885
  continue;
3054
- const name = this.#getPropertyName(prop.name);
3055
- if (!name)
2886
+ const resolved = await this.#resolveSourceImport(spec, importerDir, resolvePackage);
2887
+ if (!resolved || sourceFiles.has(resolved) || isIgnoredNodeModuleSource(resolved))
3056
2888
  continue;
3057
- obj[name] = this.#literalToValue(prop.initializer);
2889
+ queue.push(resolved);
3058
2890
  }
3059
- return obj;
3060
- }
3061
- if (ts4.isAsExpression(node) || ts4.isSatisfiesExpression(node) || ts4.isParenthesizedExpression(node)) {
3062
- return this.#literalToValue(node.expression);
3063
2891
  }
3064
- return;
3065
- }
3066
- #getPropertyName(name) {
3067
- if (ts4.isIdentifier(name) || ts4.isStringLiteral(name) || ts4.isNumericLiteral(name))
3068
- return name.text;
3069
- return null;
3070
- }
3071
- #dedupeFonts(fonts) {
3072
- const map = new Map;
3073
- for (const font of fonts)
3074
- map.set(JSON.stringify(font), font);
3075
- return [...map.values()];
3076
- }
3077
- #withFontDefaults(font) {
3078
- return { ...font, subsets: font.subsets ?? [...DEFAULT_FONT_SUBSETS] };
3079
- }
3080
- #getFontSubsets(font) {
3081
- return font.subsets ?? DEFAULT_FONT_SUBSETS;
3082
- }
3083
- #getFontVariableName(font) {
3084
- return font.variable ?? `--font-${font.name}`;
3085
- }
3086
- #getFontFallbackName(font) {
3087
- return font.fallbackName ?? `${font.name} fallback`;
3088
- }
3089
- #isFontOptimizationEnabled(font) {
3090
- return font.optimize !== false;
3091
- }
3092
- #getFontStyles(font) {
3093
- return font.styles?.length ? font.styles : ["normal"];
3094
- }
3095
- #getFontFaces(font) {
3096
- const enabledStyles = new Set(this.#getFontStyles(font));
3097
- return font.paths.map((fontPath) => {
3098
- const style = fontPath.style ?? "normal";
3099
- return {
3100
- font,
3101
- path: fontPath,
3102
- src: fontPath.src,
3103
- weight: fontPath.weight,
3104
- style,
3105
- optimizedSrc: this.#getOptimizedFontSrc(font, fontPath)
3106
- };
3107
- }).filter((face) => enabledStyles.has(face.style));
3108
- }
3109
- #getOptimizedFontSrc(font, fontPath) {
3110
- const style = fontPath.style ?? "normal";
3111
- const hash = this.#hashFontConfig({
3112
- name: font.name,
3113
- src: fontPath.src,
3114
- weight: fontPath.weight,
3115
- style,
3116
- display: font.display,
3117
- subset: font.subset,
3118
- subsets: this.#getFontSubsets(font),
3119
- subsetText: font.subsetText,
3120
- subsetFiles: font.subsetFiles,
3121
- declarations: [...font.declarations ?? [], ...fontPath.declarations ?? []]
3122
- });
3123
- return `${FONT_URL_PREFIX}/${this.#slugFontPart(font.name)}-${this.#slugFontPart(String(fontPath.weight))}-${style}-${hash}.woff2`;
2892
+ return { cssPaths: [...cssFiles], sourcePaths: [...sourceFiles] };
3124
2893
  }
3125
- #hashFontConfig(value) {
3126
- const input = this.#stableStringify(value);
3127
- let hash = 2166136261;
3128
- for (let i = 0;i < input.length; i++) {
3129
- hash ^= input.charCodeAt(i);
3130
- hash = Math.imul(hash, 16777619);
2894
+ async compileCss(cssPaths, sourcePaths) {
2895
+ if (cssPaths.length === 0)
2896
+ return "";
2897
+ const compileStarted = Date.now();
2898
+ const compilers = await Promise.all(cssPaths.map(async (cssPath) => {
2899
+ const css = await Bun.file(cssPath).text();
2900
+ const base = path15.dirname(cssPath);
2901
+ const compiler = await compile(css, {
2902
+ base,
2903
+ loadStylesheet: (id, fromBase) => this.#loadStylesheet(id, fromBase),
2904
+ loadModule: (id, fromBase) => this.#loadModule(id, fromBase)
2905
+ });
2906
+ return { cssPath, compiler };
2907
+ }));
2908
+ const sourceDirs = new Set;
2909
+ for (const entry of compilers) {
2910
+ if (!entry)
2911
+ continue;
2912
+ for (const s of entry.compiler.sources)
2913
+ sourceDirs.add(s.base);
3131
2914
  }
3132
- return (hash >>> 0).toString(36);
3133
- }
3134
- #stableStringify(value) {
3135
- if (Array.isArray(value))
3136
- return `[${value.map((item) => this.#stableStringify(item)).join(",")}]`;
3137
- if (value && typeof value === "object") {
3138
- const entries = Object.entries(value).filter(([, v]) => v !== undefined).sort(([a], [b]) => a.localeCompare(b));
3139
- return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${this.#stableStringify(v)}`).join(",")}}`;
2915
+ const scanStarted = Date.now();
2916
+ const candidates = await this.#scanCandidates(sourcePaths, [...sourceDirs]);
2917
+ this.#logger.verbose(`css candidates scanned count=${candidates.length} sources=${sourcePaths.length} dirs=${sourceDirs.size} in ${Date.now() - scanStarted}ms`);
2918
+ const parts = [];
2919
+ for (const entry of compilers) {
2920
+ if (!entry)
2921
+ continue;
2922
+ parts.push(entry.compiler.build(candidates));
3140
2923
  }
3141
- return JSON.stringify(value);
2924
+ this.#logger.verbose(`css compiled paths=${cssPaths.length} candidates=${candidates.length} in ${Date.now() - compileStarted}ms`);
2925
+ return parts.join(`
2926
+ `);
3142
2927
  }
3143
- #slugFontPart(value) {
3144
- return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "font";
2928
+ async#loadStylesheet(id, fromBase) {
2929
+ const p = await this.#resolveCssImport(id, fromBase);
2930
+ const content = await Bun.file(p).text();
2931
+ return { path: p, base: path15.dirname(p), content };
3145
2932
  }
3146
- async#resolveFontSourcePath(src) {
3147
- if (!src.startsWith("/"))
3148
- return null;
3149
- const rel = src.replace(/^\//, "");
3150
- const candidates = [
3151
- this.#command === "build" ? path16.join(this.#app.dist.cwdPath, "public", rel) : null,
3152
- path16.join(this.#app.cwdPath, "public", rel),
3153
- this.#resolveWorkspacePublicPath(rel)
3154
- ].filter(Boolean);
3155
- for (const candidate of candidates) {
3156
- if (await Bun.file(candidate).exists())
3157
- return candidate;
2933
+ async#resolveCssImport(id, fromBase) {
2934
+ if (id.startsWith(".") || id.startsWith("/"))
2935
+ return path15.resolve(fromBase, id);
2936
+ const resolver = await this.#getCssImportResolver();
2937
+ const resolved = await resolver.resolve(id, fromBase);
2938
+ if (resolved)
2939
+ return resolved;
2940
+ throw new Error(`[css] failed to resolve stylesheet import "${id}" from ${fromBase}`);
2941
+ }
2942
+ async#getCssImportResolver() {
2943
+ if (this.#cssImportResolver)
2944
+ return this.#cssImportResolver;
2945
+ this.#cssImportResolver = await CssImportResolver.create(this.#app);
2946
+ return this.#cssImportResolver;
2947
+ }
2948
+ async#loadModule(id, fromBase) {
2949
+ const p = __require.resolve(id, { paths: [fromBase] });
2950
+ const mod = await import(p);
2951
+ return { path: p, base: path15.dirname(p), module: mod.default ?? mod };
2952
+ }
2953
+ async#resolveSourceImport(id, fromBase, resolvePackage) {
2954
+ if (id.startsWith(".") || id.startsWith("/")) {
2955
+ const abs = id.startsWith("/") ? id : path15.resolve(fromBase, id);
2956
+ return resolveSourceFileCandidate(abs);
2957
+ }
2958
+ const pkg = await resolvePackage(id);
2959
+ if (pkg)
2960
+ return pkg.entryFile;
2961
+ for (const resolve of [() => resolveSourceWithBun(id, fromBase), () => resolveSourceWithRequire(id, fromBase)]) {
2962
+ const resolved = await resolve();
2963
+ if (resolved)
2964
+ return resolved;
3158
2965
  }
3159
2966
  return null;
3160
2967
  }
3161
- async#convertToWoff2(buffer, sourcePath) {
3162
- await this.#initWoff2();
3163
- const font = createFont(buffer, { type: this.#getFontType(sourcePath, buffer) });
3164
- return font.write({ type: "woff2", toBuffer: true });
2968
+ async#scanCandidates(sourcePaths, dirs) {
2969
+ const CANDIDATE_RE = /-?[\w@][\w:/.-]*(?:\[[^\]]+\][\w:/.-]*)*/g;
2970
+ const candidates = new Set;
2971
+ const glob = new Bun.Glob("**/*.{tsx,ts,jsx,js,html}");
2972
+ const files = new Set(sourcePaths);
2973
+ await Promise.all(dirs.map(async (dir) => {
2974
+ for await (const file of glob.scan({ cwd: dir, absolute: true })) {
2975
+ if (isIgnoredNodeModuleSource(file))
2976
+ continue;
2977
+ files.add(file);
2978
+ }
2979
+ }));
2980
+ await Promise.all([...files].map(async (file) => {
2981
+ const content = await Bun.file(file).text();
2982
+ for (const m of content.matchAll(CANDIDATE_RE))
2983
+ candidates.add(m[0]);
2984
+ }));
2985
+ return [...candidates];
3165
2986
  }
3166
- async#initWoff2() {
3167
- this.#woff2Ready ??= woff2.init().then(() => {
3168
- return;
3169
- });
3170
- return this.#woff2Ready;
2987
+ }
2988
+ async function resolveSourceFileCandidate(absPathNoExt) {
2989
+ if (await Bun.file(absPathNoExt).exists())
2990
+ return isSourceFile(absPathNoExt) ? absPathNoExt : null;
2991
+ for (const ext of SOURCE_EXTS2) {
2992
+ const filePath = `${absPathNoExt}${ext}`;
2993
+ if (await Bun.file(filePath).exists())
2994
+ return filePath;
3171
2995
  }
3172
- #getFontType(sourcePath, buffer) {
3173
- const signature = buffer.toString("ascii", 0, 4);
3174
- if (signature === "wOFF")
3175
- return "woff";
3176
- if (signature === "wOF2")
3177
- return "woff2";
3178
- if (signature === "OTTO")
3179
- return "otf";
3180
- const ext = path16.extname(sourcePath).slice(1).toLowerCase();
3181
- if (ext === "otf" || ext === "woff" || ext === "woff2")
3182
- return ext;
3183
- return "ttf";
2996
+ for (const ext of SOURCE_EXTS2) {
2997
+ const filePath = path15.join(absPathNoExt, `index${ext}`);
2998
+ if (await Bun.file(filePath).exists())
2999
+ return filePath;
3184
3000
  }
3185
- #resolveWorkspacePublicPath(rel) {
3186
- const [root, dep, ...rest] = rel.split("/");
3187
- if (root !== "libs" || !dep || rest.length === 0)
3188
- return null;
3189
- return path16.join(this.#app.workspace.workspaceRoot, "libs", dep, "public", ...rest);
3001
+ return null;
3002
+ }
3003
+ function resolveSourceWithBun(id, fromBase) {
3004
+ try {
3005
+ const resolved = Bun.resolveSync(id, fromBase);
3006
+ return isSourceFile(resolved) ? resolved : null;
3007
+ } catch {
3008
+ return null;
3190
3009
  }
3191
- async#getSubsetText(font) {
3192
- const parts = new Set;
3193
- const subsets = this.#getFontSubsets(font);
3194
- for (const subset of subsets)
3195
- parts.add(await this.#getSubsetPresetText(subset));
3196
- if (font.subsetText)
3197
- parts.add(font.subsetText);
3198
- for (const filePath of font.subsetFiles ?? []) {
3199
- const abs = path16.isAbsolute(filePath) ? filePath : path16.join(this.#app.cwdPath, filePath);
3200
- const file = Bun.file(abs);
3201
- if (await file.exists())
3202
- parts.add(await file.text());
3203
- }
3204
- return [...parts].join("");
3010
+ }
3011
+ function resolveSourceWithRequire(id, fromBase) {
3012
+ try {
3013
+ const resolved = __require.resolve(id, { paths: [fromBase] });
3014
+ return isSourceFile(resolved) ? resolved : null;
3015
+ } catch {
3016
+ return null;
3205
3017
  }
3206
- async#getSubsetPresetText(subset) {
3207
- if (subset === "latin")
3208
- return this.#rangeText(32, 126);
3209
- if (subset === "latin-ext")
3210
- return `${this.#rangeText(32, 126)}${this.#rangeText(160, 591)}`;
3211
- if (subset === "ks-x-1001")
3212
- return FontOptimizer.#getKsX1001Text();
3213
- if (subset === "auto")
3214
- return this.#collectAutoSubsetText();
3215
- return "";
3018
+ }
3019
+ function isSourceFile(filePath) {
3020
+ return SOURCE_EXTS2.includes(path15.extname(filePath));
3021
+ }
3022
+ function isIgnoredNodeModuleSource(filePath) {
3023
+ return NODE_MODULES_RE3.test(filePath) && !AKANJS_NODE_MODULE_RE3.test(filePath);
3024
+ }
3025
+ function getPageKeyBasePath(pageKey, basePaths) {
3026
+ const normalized = pageKey.split(path15.sep).join("/").replace(/^\.\//, "");
3027
+ const segments = normalized.split("/");
3028
+ const firstPublicSegment = segments.find((segment) => segment !== "[lang]" && !/^\(.+\)$/.test(segment));
3029
+ return firstPublicSegment && basePaths.includes(firstPublicSegment) ? firstPublicSegment : null;
3030
+ }
3031
+
3032
+ // pkgs/@akanjs/devkit/frontendBuild/devChangePlanner.ts
3033
+ import path16 from "path";
3034
+ var SOURCE_EXTS3 = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
3035
+ var CONFIG_BASENAMES = new Set(["akan.config.ts", "bunfig.toml", "tsconfig.json", "package.json"]);
3036
+ var BARREL_FACETS = new Set(["common", "srvkit", "ui", "webkit", "plugin"]);
3037
+ var CLIENT_SUFFIXES = [".Template.tsx", ".Unit.tsx", ".Util.tsx", ".View.tsx", ".Zone.tsx", ".store.ts"];
3038
+ var SHARED_SUFFIXES = [".constant.ts", ".dictionary.ts", ".signal.ts"];
3039
+ var SERVER_SUFFIXES = [".service.ts", ".document.ts"];
3040
+ var RUNTIME_METADATA_BASENAMES = new Set(["dict.ts", "sig.ts", "useClient.ts", "useServer.ts"]);
3041
+
3042
+ class DevChangePlanner {
3043
+ #workspaceRoot;
3044
+ constructor({ workspaceRoot }) {
3045
+ this.#workspaceRoot = path16.resolve(workspaceRoot);
3216
3046
  }
3217
- async#collectAutoSubsetText() {
3218
- const roots = ["page", "ui"].map((dir) => path16.join(this.#app.cwdPath, dir));
3219
- const glob = new Bun.Glob("**/*.{ts,tsx,js,jsx,html,md}");
3220
- const parts = [];
3221
- await Promise.all(roots.map(async (root) => {
3222
- if (!await Bun.file(root).exists())
3223
- return;
3224
- for await (const filePath of glob.scan({ cwd: root, absolute: true })) {
3225
- parts.push(await Bun.file(filePath).text());
3047
+ plan({ generation, files, kinds, generatedFiles = [] }) {
3048
+ const fileList = uniqueResolved([...files, ...generatedFiles]);
3049
+ const generatedSet = new Set(generatedFiles.map((file) => path16.resolve(file)));
3050
+ const kindSet = new Set(kinds);
3051
+ const roles = new Set;
3052
+ const actions = new Set;
3053
+ const reasonByFile = {};
3054
+ for (const kind of kindSet) {
3055
+ if (kind === "css") {
3056
+ roles.add("css");
3057
+ actions.add("rebuild-css");
3226
3058
  }
3227
- }));
3228
- return parts.join("");
3229
- }
3230
- #rangeText(start, end) {
3231
- let text = "";
3232
- for (let code = start;code <= end; code++)
3233
- text += String.fromCodePoint(code);
3234
- return text;
3235
- }
3236
- static #getKsX1001Text() {
3237
- if (FontOptimizer.#ksX1001Text)
3238
- return FontOptimizer.#ksX1001Text;
3239
- try {
3240
- const decoder = new TextDecoder("euc-kr");
3241
- const chars = new Set;
3242
- for (let lead = 161;lead <= 254; lead++) {
3243
- for (let trail = 161;trail <= 254; trail++) {
3244
- const char = decoder.decode(Uint8Array.of(lead, trail));
3245
- if (char && char !== "\uFFFD")
3246
- chars.add(char);
3247
- }
3059
+ if (kind === "config") {
3060
+ roles.add("config");
3061
+ actions.add("restart-dev-host");
3248
3062
  }
3249
- FontOptimizer.#ksX1001Text = [...chars].join("");
3250
- } catch {
3251
- FontOptimizer.#ksX1001Text = FontOptimizer.#rangeTextStatic(44032, 55203);
3252
3063
  }
3253
- return FontOptimizer.#ksX1001Text;
3254
- }
3255
- static #rangeTextStatic(start, end) {
3256
- let text = "";
3257
- for (let code = start;code <= end; code++)
3258
- text += String.fromCodePoint(code);
3259
- return text;
3260
- }
3261
- #buildOptimizedFontFaceRule(font, face) {
3262
- const declarations = [
3263
- ["font-family", this.#quote(font.name)],
3264
- ["src", `url(${this.#quote(face.optimizedSrc)}) format("woff2")`],
3265
- ["font-weight", String(face.weight)],
3266
- ["font-style", face.style],
3267
- ["font-display", font.display ?? "swap"],
3268
- ...this.#toDeclarationEntries(font.declarations),
3269
- ...this.#toDeclarationEntries(face.path.declarations)
3270
- ];
3271
- return `@font-face {
3272
- ${declarations.map(([prop, value]) => ` ${prop}: ${value};`).join(`
3273
- `)}
3274
- }`;
3064
+ for (const file of fileList) {
3065
+ const reasons = new Set;
3066
+ const fileRoles = this.#rolesForFile(file, { isGenerated: generatedSet.has(path16.resolve(file)), reasons });
3067
+ for (const role of fileRoles)
3068
+ roles.add(role);
3069
+ if (reasons.has("runtime-metadata"))
3070
+ actions.add("restart-builder");
3071
+ if (reasons.size > 0)
3072
+ reasonByFile[path16.resolve(file)] = [...reasons].sort();
3073
+ }
3074
+ if (roles.has("barrel"))
3075
+ actions.add("sync-generated");
3076
+ if (roles.has("server") || roles.has("shared"))
3077
+ actions.add("restart-backend");
3078
+ if (roles.has("client") || roles.has("shared"))
3079
+ actions.add("rebuild-client");
3080
+ if (roles.has("css"))
3081
+ actions.add("rebuild-css");
3082
+ return {
3083
+ generation,
3084
+ files: fileList,
3085
+ generatedFiles: uniqueResolved(generatedFiles),
3086
+ roles: [...roles].sort(),
3087
+ actions: [...actions].sort(),
3088
+ reasonByFile
3089
+ };
3275
3090
  }
3276
- async#buildFontaineFallbackCss(font, face, outputPath) {
3277
- if (font.adjustFontFallback === false)
3278
- return "";
3279
- const metrics = await readMetrics(outputPath).catch(() => null);
3280
- if (!metrics)
3281
- return "";
3282
- const fallbacks = resolveCategoryFallbacks({
3283
- fontFamily: font.name,
3284
- fallbacks: font.fallbacks ?? {},
3285
- metrics: { ...metrics, category: font.category }
3286
- });
3287
- const css = [];
3288
- for (let i = fallbacks.length - 1;i >= 0; i--) {
3289
- const fallback = fallbacks[i];
3290
- const fallbackMetrics = await getMetricsForFamily(fallback);
3291
- if (!fallbackMetrics)
3292
- continue;
3293
- css.push(generateFontFace({ ...metrics, category: font.category }, {
3294
- name: this.#getFontFallbackName(font),
3295
- font: fallback,
3296
- metrics: fallbackMetrics,
3297
- "font-weight": String(face.weight),
3298
- "font-style": face.style
3299
- }));
3091
+ #rolesForFile(file, { isGenerated, reasons }) {
3092
+ const roles = new Set;
3093
+ const abs = path16.resolve(file);
3094
+ const base = path16.basename(abs);
3095
+ const ext = path16.extname(abs).toLowerCase();
3096
+ const isSource = SOURCE_EXTS3.has(ext);
3097
+ const rel = path16.relative(this.#workspaceRoot, abs);
3098
+ const parts = rel.split(path16.sep).filter(Boolean);
3099
+ if (CONFIG_BASENAMES.has(base)) {
3100
+ roles.add("config");
3101
+ reasons.add("config-file");
3102
+ }
3103
+ if (ext === ".css") {
3104
+ roles.add("css");
3105
+ reasons.add("css-file");
3106
+ }
3107
+ if (isGenerated || isSource && this.#isBarrelFacetChild(parts)) {
3108
+ roles.add("barrel");
3109
+ reasons.add(isGenerated ? "generated-index" : "barrel-facet-child");
3110
+ }
3111
+ if (isSource && this.#isServerBiased(abs, parts)) {
3112
+ roles.add("server");
3113
+ reasons.add("server-path");
3300
3114
  }
3301
- return css.join("");
3115
+ if (isSource && this.#isClientBiased(abs, parts)) {
3116
+ roles.add("client");
3117
+ reasons.add("client-path");
3118
+ }
3119
+ if (isSource && this.#isSharedBiased(abs, parts)) {
3120
+ roles.add("shared");
3121
+ reasons.add("shared-path");
3122
+ }
3123
+ if (isSource && this.#isRuntimeMetadataFile(parts, base)) {
3124
+ reasons.add("runtime-metadata");
3125
+ }
3126
+ if (roles.has("server") && roles.has("client")) {
3127
+ roles.delete("server");
3128
+ roles.delete("client");
3129
+ roles.add("shared");
3130
+ reasons.add("server-client-overlap");
3131
+ }
3132
+ if (roles.size === 0 && SOURCE_EXTS3.has(ext) && this.#isWorkspaceSource(rel)) {
3133
+ roles.add("shared");
3134
+ reasons.add("workspace-source-fallback");
3135
+ }
3136
+ return roles;
3302
3137
  }
3303
- #buildRootVariableRule(font) {
3304
- return `:root { ${this.#getFontVariableName(font)}: ${this.#quote(font.name)}, ${this.#quote(this.#getFontFallbackName(font))}; }`;
3138
+ #isServerBiased(abs, parts) {
3139
+ const base = path16.basename(abs);
3140
+ return parts.includes("srvkit") || SERVER_SUFFIXES.some((suffix) => base.endsWith(suffix)) || base === "main.ts" || base === "server.ts";
3305
3141
  }
3306
- #buildFontUtilityRules(fonts) {
3307
- const rules = [];
3308
- const seen = new Set;
3309
- for (const font of fonts) {
3310
- const className = font.className || `font-${font.name}`;
3311
- const selector = `.${this.#escapeCssClassName(className)}`;
3312
- const rule = `${selector} { font-family: var(${this.#getFontVariableName(font)}); }`;
3313
- if (seen.has(rule))
3314
- continue;
3315
- seen.add(rule);
3316
- rules.push(rule);
3317
- }
3318
- return rules.join(`
3319
- `);
3142
+ #isClientBiased(abs, parts) {
3143
+ const base = path16.basename(abs);
3144
+ return parts.includes("ui") || parts.includes("webkit") || parts.includes("page") || CLIENT_SUFFIXES.some((suffix) => base.endsWith(suffix));
3320
3145
  }
3321
- #escapeCssClassName(value) {
3322
- return value.replace(/^-?\d|[^a-zA-Z0-9_-]/g, (part) => [...part].map((char) => `\\${char.codePointAt(0)?.toString(16)} `).join(""));
3146
+ #isSharedBiased(abs, parts) {
3147
+ const base = path16.basename(abs);
3148
+ return parts.includes("common") || SHARED_SUFFIXES.some((suffix) => base.endsWith(suffix)) || RUNTIME_METADATA_BASENAMES.has(base);
3323
3149
  }
3324
- #toDeclarationEntries(declarations = []) {
3325
- return declarations.map((declaration) => [declaration.prop, declaration.value]);
3150
+ #isRuntimeMetadataFile(parts, base) {
3151
+ const parent = parts.at(-2);
3152
+ if (parent === "lib" && RUNTIME_METADATA_BASENAMES.has(base))
3153
+ return true;
3154
+ const libIndex = parts.lastIndexOf("lib");
3155
+ if (libIndex < 0 || parts.length <= libIndex + 1)
3156
+ return false;
3157
+ return base.endsWith(".dictionary.ts") || base.endsWith(".signal.ts");
3326
3158
  }
3327
- #quote(value) {
3328
- return JSON.stringify(value);
3159
+ #isBarrelFacetChild(parts) {
3160
+ if (parts.length < 4)
3161
+ return false;
3162
+ const [scope, , facet, child] = parts;
3163
+ if (scope !== "apps" && scope !== "libs")
3164
+ return false;
3165
+ if (!facet || !BARREL_FACETS.has(facet))
3166
+ return false;
3167
+ if (!child || child.startsWith(".") || child === "index.ts" || child === "index.tsx")
3168
+ return false;
3169
+ return true;
3170
+ }
3171
+ #isWorkspaceSource(rel) {
3172
+ if (!rel || rel.startsWith("..") || path16.isAbsolute(rel))
3173
+ return false;
3174
+ const [scope] = rel.split(path16.sep);
3175
+ return scope === "apps" || scope === "libs" || scope === "pkgs";
3329
3176
  }
3330
3177
  }
3178
+ var uniqueResolved = (files) => [...new Set(files.map((file) => path16.resolve(file)))].sort();
3331
3179
 
3332
- // pkgs/@akanjs/devkit/frontendBuild/hmrWatcher.ts
3333
- import fs3 from "fs";
3334
- import path18 from "path";
3335
-
3336
- // pkgs/@akanjs/devkit/frontendBuild/hmrChangeClassifier.ts
3180
+ // pkgs/@akanjs/devkit/frontendBuild/devGeneratedIndexSync.ts
3181
+ import { mkdir as mkdir4, readdir as readdir2, readFile as readFile2, rm as rm2, stat as stat2, writeFile as writeFile2 } from "fs/promises";
3337
3182
  import path17 from "path";
3338
- var SOURCE_EXTS3 = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
3339
- var CSS_EXTS = new Set([".css"]);
3340
- var CONFIG_BASENAMES2 = new Set(["akan.config.ts", "bunfig.toml", "tsconfig.json", "package.json"]);
3341
-
3342
- class HmrChangeClassifier {
3343
- classify(abs) {
3344
- if (this.#isUninteresting(abs))
3345
- return "ignore";
3346
- const base = path17.basename(abs);
3347
- if (CONFIG_BASENAMES2.has(base))
3348
- return "config";
3349
- const ext = path17.extname(abs).toLowerCase();
3350
- if (CSS_EXTS.has(ext))
3351
- return "css";
3352
- if (SOURCE_EXTS3.has(ext))
3353
- return "code";
3354
- return "ignore";
3355
- }
3356
- #isUninteresting(abs) {
3357
- const base = path17.basename(abs);
3358
- if (!base)
3359
- return true;
3360
- if (base.startsWith("."))
3361
- return true;
3362
- if (base.endsWith("~") || base.endsWith(".swp") || base.endsWith(".swx") || base.endsWith(".tmp"))
3363
- return true;
3364
- if (abs.includes(`${path17.sep}node_modules${path17.sep}`))
3365
- return true;
3366
- if (abs.includes(`${path17.sep}.akan${path17.sep}`))
3367
- return true;
3368
- return false;
3369
- }
3370
- }
3183
+ var BARREL_FACETS2 = new Set(["common", "srvkit", "ui", "webkit", "plugin"]);
3184
+ var FACET_SOURCE_FILE_RE = /\.(ts|tsx)$/;
3185
+ var FACET_EXCLUDED_FILE_RE = /(^index\.tsx?$|\.d\.ts$|\.(test|spec)\.(ts|tsx)$|\.css$|\.scss$|\.sass$)/;
3186
+ var FACET_PASCAL_CASE_RE = /^[A-Z][A-Za-z0-9]*$/;
3187
+ var FACET_CAMEL_CASE_RE = /^[a-z][A-Za-z0-9]*$/;
3188
+ var MODULE_UI_TYPES = ["Template", "Unit", "Util", "View", "Zone"];
3189
+ var SERVICE_UI_TYPES = ["Util", "Zone"];
3190
+ var SCALAR_UI_TYPES = ["Template", "Unit"];
3371
3191
 
3372
- // pkgs/@akanjs/devkit/frontendBuild/hmrWatcher.ts
3373
- class HmrWatcher {
3374
- #roots;
3375
- #debounceMs;
3376
- #onBatch;
3377
- #logger;
3378
- #watchers = [];
3379
- #pending = new Map;
3380
- #classifier = new HmrChangeClassifier;
3381
- #timer = null;
3382
- #stopped = false;
3383
- #flushing = false;
3384
- constructor(opts) {
3385
- this.#roots = [...new Set(opts.roots.map((r) => path18.resolve(r)))];
3386
- this.#debounceMs = opts.debounceMs ?? 80;
3387
- this.#onBatch = opts.onBatch;
3388
- this.#logger = opts.logger;
3192
+ class DevGeneratedIndexSync {
3193
+ #workspaceRoot;
3194
+ constructor({ workspaceRoot }) {
3195
+ this.#workspaceRoot = path17.resolve(workspaceRoot);
3389
3196
  }
3390
- start() {
3391
- for (const root of this.#roots) {
3392
- try {
3393
- const w = fs3.watch(root, { recursive: true, persistent: false }, (_event, filename) => {
3394
- if (!filename)
3395
- return;
3396
- const abs = path18.resolve(root, filename.toString());
3397
- this.#queue(abs);
3398
- });
3399
- this.#watchers.push(w);
3400
- this.#logger.verbose(`[hmr] watching ${root}`);
3401
- } catch (err) {
3402
- this.#logger.error(`[hmr] failed to watch ${root}: ${err.message}`);
3403
- }
3197
+ async syncForBatch(files) {
3198
+ const indexPaths = new Set;
3199
+ const errors = [];
3200
+ for (const file of files) {
3201
+ const facetIndex = this.#facetIndexFor(file);
3202
+ if (facetIndex)
3203
+ indexPaths.add(facetIndex);
3204
+ const moduleIndex = await this.#moduleIndexForDirectoryEvent(file).catch((err) => {
3205
+ errors.push(`[generated-index] module detection failed for ${file}: ${formatError2(err)}`);
3206
+ return null;
3207
+ });
3208
+ if (moduleIndex)
3209
+ indexPaths.add(moduleIndex);
3404
3210
  }
3405
- }
3406
- stop() {
3407
- this.#stopped = true;
3408
- if (this.#timer)
3409
- clearTimeout(this.#timer);
3410
- for (const w of this.#watchers) {
3211
+ const changedFiles = [];
3212
+ for (const indexPath of [...indexPaths].sort()) {
3411
3213
  try {
3412
- w.close();
3413
- } catch {}
3414
- }
3415
- }
3416
- #queue(abs) {
3417
- const kind = this.#classifier.classify(abs);
3418
- if (kind === "ignore")
3419
- return;
3420
- this.#pending.set(abs, kind);
3421
- if (this.#flushing)
3422
- return;
3423
- if (this.#timer)
3424
- clearTimeout(this.#timer);
3425
- this.#timer = setTimeout(() => this.#flush(), this.#debounceMs);
3426
- }
3427
- #flush() {
3428
- this.#timer = null;
3429
- if (this.#stopped || this.#pending.size === 0 || this.#flushing)
3430
- return;
3431
- this.#drain();
3432
- }
3433
- async#drain() {
3434
- this.#flushing = true;
3435
- try {
3436
- while (!this.#stopped && this.#pending.size > 0) {
3437
- const files = Array.from(this.#pending.keys());
3438
- const kinds = new Set(this.#pending.values());
3439
- this.#pending.clear();
3440
- try {
3441
- await this.#onBatch({ files, kinds });
3442
- } catch (e) {
3443
- this.#logger.error(`[hmr] onBatch error: ${e.message}`);
3444
- }
3214
+ const changed = await this.#syncIndex(indexPath);
3215
+ if (changed)
3216
+ changedFiles.push(indexPath);
3217
+ } catch (err) {
3218
+ errors.push(`[generated-index] sync failed for ${indexPath}: ${formatError2(err)}`);
3445
3219
  }
3446
- } finally {
3447
- this.#flushing = false;
3448
- if (!this.#stopped && this.#pending.size > 0)
3449
- this.#timer = setTimeout(() => this.#flush(), this.#debounceMs);
3450
3220
  }
3221
+ return { changedFiles, errors };
3451
3222
  }
3452
- }
3453
-
3454
- // pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
3455
- import path20 from "path";
3456
-
3457
- // pkgs/@akanjs/devkit/transforms/externalizeFrameworkPlugin.ts
3458
- import path19 from "path";
3459
- var DEFAULT_INCLUDE = ["akanjs/", "@apps/", "@libs/"];
3460
- var DEFAULT_EXCLUDE_EXACT = new Set(["akanjs/webkit", "@akanjs/cli", "@akanjs/devkit"]);
3461
- var DEFAULT_EXCLUDE_PREFIX = ["@akanjs/cli/", "@akanjs/devkit/"];
3462
- var OPTIONAL_BACKEND_EXTERNAL_EXACT = new Set([
3463
- "@libsql/client",
3464
- "bullmq",
3465
- "croner",
3466
- "ioredis",
3467
- "postgres",
3468
- "protobufjs"
3469
- ]);
3470
- var RUNTIME_EXTERNAL_EXACT = new Set([
3471
- "react",
3472
- "react-dom",
3473
- "react/jsx-runtime",
3474
- "react/jsx-dev-runtime",
3475
- "react-server-dom-webpack",
3476
- "react-server-dom-webpack/server.node",
3477
- "react-server-dom-webpack/client.node",
3478
- "react-server-dom-webpack/client.browser"
3479
- ]);
3480
- var RUNTIME_EXTERNAL_PREFIX = ["react-dom/", "react-server-dom-webpack/"];
3481
- var CANDIDATE_EXTS3 = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
3482
- async function createExternalizeFrameworkPlugin(options) {
3483
- const tsconfig = await options.app.getTsConfig();
3484
- const includePrefixes = options.include ?? DEFAULT_INCLUDE;
3485
- const extraExact = new Set(options.extra ?? []);
3486
- const workspaceRoot = options.app.workspace.workspaceRoot;
3487
- const tsconfigPaths = tsconfig.compilerOptions.paths ?? {};
3488
- const rootEntries = Object.entries(tsconfigPaths).filter(([k]) => !k.endsWith("/*")).map(([k, v]) => ({ pkg: k, entryFile: v[0] ?? null })).filter((e) => e.entryFile !== null);
3489
- 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);
3490
- async function resolveWorkspaceSubpath(spec) {
3491
- if (!workspaceRoot)
3223
+ #facetIndexFor(file) {
3224
+ const abs = path17.resolve(file);
3225
+ const rel = path17.relative(this.#workspaceRoot, abs);
3226
+ if (rel.startsWith("..") || path17.isAbsolute(rel))
3492
3227
  return null;
3493
- for (const { prefix, replacements } of wildcardEntries) {
3494
- if (!spec.startsWith(prefix))
3495
- continue;
3496
- const suffix = spec.slice(prefix.length);
3497
- for (const repl of replacements) {
3498
- const replPath = repl?.endsWith("/*") ? repl.slice(0, -1) : repl ?? "";
3499
- if (!replPath)
3500
- continue;
3501
- const candidate = path19.resolve(workspaceRoot, replPath + suffix);
3502
- const hit = await firstExisting(candidate);
3503
- if (hit)
3504
- return hit;
3505
- }
3506
- }
3507
- for (const { pkg, entryFile } of rootEntries) {
3508
- if (spec !== pkg && !spec.startsWith(`${pkg}/`))
3509
- continue;
3510
- if (spec === pkg)
3511
- continue;
3512
- const suffix = spec.slice(pkg.length + 1);
3513
- const pkgDir = path19.dirname(path19.resolve(workspaceRoot, entryFile));
3514
- const candidate = path19.join(pkgDir, suffix);
3515
- const hit = await firstExisting(candidate);
3516
- if (hit)
3517
- return hit;
3518
- }
3519
- return null;
3228
+ const parts = rel.split(path17.sep).filter(Boolean);
3229
+ if (parts.length < 4)
3230
+ return null;
3231
+ const [scope, project, facet, child] = parts;
3232
+ if (scope !== "apps" && scope !== "libs" || !project || !facet || !BARREL_FACETS2.has(facet))
3233
+ return null;
3234
+ if (!child || child.startsWith(".") || child === "index.ts" || child === "index.tsx")
3235
+ return null;
3236
+ return path17.join(this.#workspaceRoot, scope, project, facet, "index.ts");
3520
3237
  }
3521
- return {
3522
- name: "akan-externalize-framework",
3523
- setup(build) {
3524
- build.onResolve({ filter: /.*/ }, async (args) => {
3525
- const spec = args.path;
3526
- if (spec === "." || spec === ".." || spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/"))
3527
- return;
3528
- if (extraExact.has(spec) || DEFAULT_EXCLUDE_EXACT.has(spec))
3529
- return { path: spec, external: true };
3530
- for (const prefix of DEFAULT_EXCLUDE_PREFIX) {
3531
- if (spec.startsWith(prefix))
3532
- return { path: spec, external: true };
3533
- }
3534
- if (OPTIONAL_BACKEND_EXTERNAL_EXACT.has(spec))
3535
- return { path: spec, external: true };
3536
- if (RUNTIME_EXTERNAL_EXACT.has(spec))
3537
- return { path: spec, external: true };
3538
- for (const prefix of RUNTIME_EXTERNAL_PREFIX) {
3539
- if (spec.startsWith(prefix))
3540
- return { path: spec, external: true };
3541
- }
3542
- for (const prefix of includePrefixes) {
3543
- if (!spec.startsWith(prefix))
3544
- continue;
3545
- const resolved = await resolveWorkspaceSubpath(spec);
3546
- if (resolved)
3547
- return { path: resolved };
3548
- return;
3549
- }
3550
- return;
3551
- });
3238
+ async#moduleIndexForDirectoryEvent(file) {
3239
+ const abs = path17.resolve(file);
3240
+ const rel = path17.relative(this.#workspaceRoot, abs);
3241
+ if (rel.startsWith("..") || path17.isAbsolute(rel))
3242
+ return null;
3243
+ const parts = rel.split(path17.sep).filter(Boolean);
3244
+ if (parts.length !== 4 && parts.length !== 5)
3245
+ return null;
3246
+ const [scope, project, libSegment, moduleSegment, scalarSegment] = parts;
3247
+ if (scope !== "apps" && scope !== "libs" || !project || libSegment !== "lib")
3248
+ return null;
3249
+ const isExistingDirectory = await stat2(abs).then((s) => s.isDirectory()).catch(() => false);
3250
+ const looksLikeDeletedDirectory = !path17.extname(abs);
3251
+ if (!isExistingDirectory && !looksLikeDeletedDirectory)
3252
+ return null;
3253
+ if (moduleSegment === "__scalar" && scalarSegment) {
3254
+ return path17.join(this.#workspaceRoot, scope, project, "lib", "__scalar", scalarSegment, "index.ts");
3552
3255
  }
3553
- };
3554
- }
3555
- async function firstExisting(basePath) {
3556
- if (await Bun.file(basePath).exists())
3557
- return basePath;
3558
- for (const ext of CANDIDATE_EXTS3) {
3559
- const candidate = `${basePath}${ext}`;
3560
- if (await Bun.file(candidate).exists())
3561
- return candidate;
3256
+ if (!moduleSegment || moduleSegment === "__scalar")
3257
+ return null;
3258
+ return path17.join(this.#workspaceRoot, scope, project, "lib", moduleSegment, "index.ts");
3562
3259
  }
3563
- for (const ext of CANDIDATE_EXTS3) {
3564
- const candidate = path19.join(basePath, `index${ext}`);
3565
- if (await Bun.file(candidate).exists())
3566
- return candidate;
3260
+ async#syncIndex(indexPath) {
3261
+ const dir = path17.dirname(indexPath);
3262
+ const content = await this.#contentForIndex(indexPath);
3263
+ if (content === null) {
3264
+ if (!await exists(indexPath))
3265
+ return false;
3266
+ await rm2(indexPath, { force: true });
3267
+ return true;
3268
+ }
3269
+ const current = await readFile2(indexPath, "utf8").catch(() => null);
3270
+ if (current === content)
3271
+ return false;
3272
+ await mkdir4(dir, { recursive: true });
3273
+ await writeFile2(indexPath, content);
3274
+ return true;
3567
3275
  }
3568
- return null;
3569
- }
3570
-
3571
- // pkgs/@akanjs/devkit/transforms/useClientBundlePlugin.ts
3572
- function createUseClientBundlePlugin(options = {}) {
3573
- return {
3574
- name: "akan-use-client-bundle",
3575
- setup(build) {
3576
- build.onLoad({ filter: /\.(tsx|ts|jsx|js)$/ }, async (args) => {
3577
- if (args.path.includes("/node_modules/"))
3578
- return;
3579
- let source;
3580
- try {
3581
- source = await Bun.file(args.path).text();
3582
- } catch {
3583
- return;
3584
- }
3585
- const stubbed = transformUseClient(source, {
3586
- path: args.path,
3587
- workspaceRoot: options.workspaceRoot
3588
- });
3589
- if (stubbed === null)
3590
- return;
3591
- return { contents: stubbed, loader: loaderFor3(args.path) };
3592
- });
3276
+ async#contentForIndex(indexPath) {
3277
+ const parts = path17.relative(this.#workspaceRoot, indexPath).split(path17.sep).filter(Boolean);
3278
+ const facet = parts.at(-2);
3279
+ if (facet && BARREL_FACETS2.has(facet))
3280
+ return this.#facetContent(path17.dirname(indexPath));
3281
+ return this.#moduleContent(path17.dirname(indexPath));
3282
+ }
3283
+ async#facetContent(dir) {
3284
+ const nameCasePattern = path17.basename(dir) === "ui" ? FACET_PASCAL_CASE_RE : FACET_CAMEL_CASE_RE;
3285
+ const entries = await readdir2(dir, { withFileTypes: true }).catch(() => []);
3286
+ const exportNames = entries.flatMap((entry) => {
3287
+ const name = entry.name;
3288
+ if (name.startsWith("."))
3289
+ return [];
3290
+ if (entry.isDirectory())
3291
+ return nameCasePattern.test(name) ? [name] : [];
3292
+ if (!entry.isFile())
3293
+ return [];
3294
+ if (!FACET_SOURCE_FILE_RE.test(name) || FACET_EXCLUDED_FILE_RE.test(name))
3295
+ return [];
3296
+ const exportName = name.replace(FACET_SOURCE_FILE_RE, "");
3297
+ return nameCasePattern.test(exportName) ? [exportName] : [];
3298
+ }).sort();
3299
+ if (exportNames.length === 0)
3300
+ return null;
3301
+ return `${exportNames.map((name) => `export * from "./${name}";`).join(`
3302
+ `)}
3303
+ `;
3304
+ }
3305
+ async#moduleContent(dir) {
3306
+ const rel = path17.relative(this.#workspaceRoot, dir);
3307
+ const parts = rel.split(path17.sep).filter(Boolean);
3308
+ const moduleSegment = parts.at(-1);
3309
+ if (!moduleSegment)
3310
+ return null;
3311
+ const isScalar = parts.at(-2) === "__scalar";
3312
+ const rawModel = moduleSegment.startsWith("_") ? moduleSegment.slice(1) : moduleSegment;
3313
+ if (!rawModel)
3314
+ return null;
3315
+ const modelName = capitalize(rawModel);
3316
+ const allowedTypes = isScalar ? SCALAR_UI_TYPES : moduleSegment.startsWith("_") ? SERVICE_UI_TYPES : MODULE_UI_TYPES;
3317
+ const fileTypes = [];
3318
+ for (const type of allowedTypes) {
3319
+ if (await exists(path17.join(dir, `${modelName}.${type}.tsx`)))
3320
+ fileTypes.push(type);
3593
3321
  }
3594
- };
3595
- }
3596
- function loaderFor3(absPath) {
3597
- if (absPath.endsWith(".tsx"))
3598
- return "tsx";
3599
- if (absPath.endsWith(".jsx"))
3600
- return "jsx";
3601
- if (absPath.endsWith(".ts"))
3602
- return "ts";
3603
- return "js";
3322
+ if (fileTypes.length === 0)
3323
+ return null;
3324
+ return `
3325
+ ${fileTypes.map((type) => `import * as ${type} from "./${modelName}.${type}";`).join(`
3326
+ `)}
3327
+
3328
+ export const ${modelName} = { ${fileTypes.join(", ")} };`;
3329
+ }
3604
3330
  }
3331
+ var exists = async (file) => stat2(file).then(() => true).catch(() => false);
3332
+ var capitalize = (value) => `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
3333
+ var formatError2 = (err) => err instanceof Error ? err.message : String(err);
3605
3334
 
3606
- // pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
3607
- var VIRTUAL_PAGES_ENTRY = "akan-pages-entry";
3335
+ // pkgs/@akanjs/devkit/frontendBuild/fontOptimizer.ts
3336
+ import { mkdir as mkdir5, stat as stat3 } from "fs/promises";
3337
+ import path18 from "path";
3338
+ import ts4 from "typescript";
3339
+ var FONT_URL_PREFIX = "/_akan/fonts";
3340
+ var DEFAULT_FONT_SUBSETS = ["latin"];
3608
3341
 
3609
- class PagesBundleBuilder {
3342
+ class FontOptimizer {
3610
3343
  #app;
3611
3344
  #command;
3612
- #pageEntries;
3613
- #started = Date.now();
3614
- constructor(app, command = "start", pageEntries) {
3345
+ #artifactRoot;
3346
+ #files = [];
3347
+ #cssParts = [];
3348
+ #woff2Ready = null;
3349
+ static #ksX1001Text = null;
3350
+ static #cacheVersion = 1;
3351
+ constructor(app, command = "start") {
3615
3352
  this.#app = app;
3616
3353
  this.#command = command;
3617
- this.#pageEntries = pageEntries;
3354
+ this.#artifactRoot = path18.join(command === "build" ? app.dist.cwdPath : app.cwdPath, ".akan/artifact");
3618
3355
  }
3619
- async build() {
3620
- const akanConfig = await this.#app.getConfig();
3621
- const resolvedEntries = this.#pageEntries ?? await resolveSsrPageEntriesForApp(this.#app, await this.#app.getPageKeys());
3622
- const entrySource = PagesEntrySourceGenerator.generate(resolvedEntries);
3623
- const workspaceRoot = this.#app.workspace.workspaceRoot;
3624
- const result = await Bun.build({
3625
- entrypoints: [VIRTUAL_PAGES_ENTRY],
3626
- outdir: `${this.#artifactDir}/server`,
3627
- target: "bun",
3628
- format: "esm",
3629
- splitting: this.#splitting,
3630
- minify: this.#command === "build",
3631
- naming: {
3632
- entry: "pages-[hash].[ext]",
3633
- chunk: "chunks/[name]-[hash].[ext]",
3634
- asset: "assets/[name]-[hash].[ext]"
3635
- },
3636
- define: this.#define(),
3637
- plugins: [
3638
- PagesBundleBuilder.createPagesEntryPlugin(entrySource),
3639
- PagesBundleBuilder.createServerCssStubPlugin(),
3640
- PagesBundleBuilder.createServerUseClientFetchPlugin(),
3641
- await createExternalizeFrameworkPlugin({ app: this.#app, extra: akanConfig.externalLibs }),
3642
- akanConfig.barrelImports.length > 0 ? await createBarrelImportsPlugin(this.#app, {
3643
- pipeAfter: (source, args) => transformUseClient(source, {
3644
- path: args.path,
3645
- workspaceRoot
3646
- })
3647
- }) : createUseClientBundlePlugin({ workspaceRoot })
3648
- ]
3649
- });
3650
- if (!result.success)
3651
- throw new AggregateError(result.logs, "[PagesBundleBuilder] Bun.build failed");
3652
- const entryArtifact = result.outputs.find((a) => a.kind === "entry-point");
3653
- if (!entryArtifact)
3654
- throw new Error("[PagesBundleBuilder] Bun.build emitted no entry-point artifact");
3655
- const bundlePath = path20.resolve(entryArtifact.path);
3656
- const buildId = Date.now();
3657
- const outputBytes = result.outputs.reduce((sum, output) => sum + output.size, 0);
3658
- const chunkCount = result.outputs.filter((output) => output.kind === "chunk").length;
3659
- this.#app.verbose(`[PagesBundleBuilder] ${path20.basename(bundlePath)} emitted in ${Date.now() - this.#started}ms splitting=${this.#splitting} entry=${entryArtifact.size} bytes outputs=${result.outputs.length} chunks=${chunkCount} total=${outputBytes} bytes`);
3660
- return {
3661
- bundlePath,
3662
- buildId,
3663
- splitting: this.#splitting,
3664
- entryBytes: entryArtifact.size,
3665
- outputBytes,
3666
- outputCount: result.outputs.length,
3667
- chunkCount
3356
+ async optimize() {
3357
+ const fonts = await this.discoverFonts();
3358
+ const cacheKey = await this.#buildCacheKey(fonts);
3359
+ const cached = cacheKey ? await this.#readCache(cacheKey) : null;
3360
+ if (cached) {
3361
+ this.#app.verbose(`[font] reused ${cached.files.length} cached file(s); skipped subsetting`);
3362
+ return { css: cached.css, fonts, files: cached.files };
3363
+ }
3364
+ for (const font of fonts) {
3365
+ if (!this.#isFontOptimizationEnabled(font))
3366
+ continue;
3367
+ await this.#optimizeFont(font);
3368
+ }
3369
+ const fontUtilityCss = this.#buildFontUtilityRules(fonts);
3370
+ if (fontUtilityCss)
3371
+ this.#cssParts.push(fontUtilityCss);
3372
+ const result = { css: this.#cssParts.join(`
3373
+ `), fonts, files: this.#files };
3374
+ if (cacheKey)
3375
+ await this.#writeCache(cacheKey, result);
3376
+ return result;
3377
+ }
3378
+ get #cachePath() {
3379
+ return path18.join(this.#artifactRoot, "fontCache.json");
3380
+ }
3381
+ async#buildCacheKey(fonts) {
3382
+ const sources = [];
3383
+ for (const font of fonts) {
3384
+ if (!this.#isFontOptimizationEnabled(font))
3385
+ continue;
3386
+ for (const face of this.#getFontFaces(font)) {
3387
+ const sourcePath = await this.#resolveFontSourcePath(face.src);
3388
+ const stamp = sourcePath ? await this.#fileStamp(sourcePath) : null;
3389
+ if (!stamp)
3390
+ return null;
3391
+ sources.push({ optimizedSrc: face.optimizedSrc, ...stamp });
3392
+ }
3393
+ for (const filePath of font.subsetFiles ?? []) {
3394
+ const abs = path18.isAbsolute(filePath) ? filePath : path18.join(this.#app.cwdPath, filePath);
3395
+ const stamp = await this.#fileStamp(abs);
3396
+ if (!stamp)
3397
+ return null;
3398
+ sources.push({ subsetFile: filePath, ...stamp });
3399
+ }
3400
+ if (this.#getFontSubsets(font).includes("auto"))
3401
+ sources.push({ autoSubsetText: this.#hashFontConfig(await this.#collectAutoSubsetText()) });
3402
+ }
3403
+ return this.#hashFontConfig({ version: FontOptimizer.#cacheVersion, fonts, sources });
3404
+ }
3405
+ async#fileStamp(filePath) {
3406
+ try {
3407
+ const stats = await stat3(filePath);
3408
+ return { mtimeMs: Math.round(stats.mtimeMs), size: stats.size };
3409
+ } catch {
3410
+ return null;
3411
+ }
3412
+ }
3413
+ async#readCache(key) {
3414
+ const cache = await Bun.file(this.#cachePath).json().catch(() => null);
3415
+ if (cache?.version !== FontOptimizer.#cacheVersion || cache.key !== key)
3416
+ return null;
3417
+ const files = cache.files.map((relativePath) => path18.join(this.#artifactRoot, relativePath));
3418
+ for (const filePath of files) {
3419
+ if (!await Bun.file(filePath).exists())
3420
+ return null;
3421
+ }
3422
+ return { css: cache.css, files };
3423
+ }
3424
+ async#writeCache(key, result) {
3425
+ const cache = {
3426
+ version: FontOptimizer.#cacheVersion,
3427
+ key,
3428
+ css: result.css,
3429
+ files: result.files.map((filePath) => path18.relative(this.#artifactRoot, filePath))
3668
3430
  };
3431
+ await Bun.write(this.#cachePath, JSON.stringify(cache));
3432
+ }
3433
+ async discoverFonts() {
3434
+ const pageKeys = await this.#app.getPageKeys();
3435
+ const fonts = [];
3436
+ await Promise.all(pageKeys.map(async (key) => {
3437
+ const filePath = path18.resolve(this.#app.cwdPath, "page", key);
3438
+ const file = Bun.file(filePath);
3439
+ if (!await file.exists())
3440
+ return;
3441
+ const source = await file.text();
3442
+ if (!source.includes("fonts"))
3443
+ return;
3444
+ fonts.push(...this.#extractFontsExport(source, filePath));
3445
+ }));
3446
+ return this.#dedupeFonts(fonts);
3447
+ }
3448
+ async#optimizeFont(font) {
3449
+ const faceCss = [];
3450
+ for (const face of this.#getFontFaces(font)) {
3451
+ const sourcePath = await this.#resolveFontSourcePath(face.src);
3452
+ if (!sourcePath) {
3453
+ this.#app.logger.warn(`[font] source not found: ${face.src}`);
3454
+ continue;
3455
+ }
3456
+ const outputPath = path18.join(this.#artifactRoot, face.optimizedSrc.replace(/^\/_akan\//, ""));
3457
+ await mkdir5(path18.dirname(outputPath), { recursive: true });
3458
+ const sourceBuffer = Buffer.from(await Bun.file(sourcePath).arrayBuffer());
3459
+ const outputBuffer = await this.#buildFontBuffer(font, sourceBuffer, sourcePath);
3460
+ await Bun.write(outputPath, outputBuffer);
3461
+ this.#files.push(outputPath);
3462
+ faceCss.push(this.#buildOptimizedFontFaceRule(font, face));
3463
+ const fallbackCss = await this.#buildFontaineFallbackCss(font, face, outputPath);
3464
+ if (fallbackCss)
3465
+ faceCss.push(fallbackCss);
3466
+ }
3467
+ if (faceCss.length > 0)
3468
+ this.#cssParts.push(...faceCss, this.#buildRootVariableRule(font));
3469
+ }
3470
+ #extractFontsExport(source, filePath) {
3471
+ const sourceFile = ts4.createSourceFile(filePath, source, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TSX);
3472
+ const fonts = [];
3473
+ for (const statement of sourceFile.statements) {
3474
+ if (!ts4.isVariableStatement(statement))
3475
+ continue;
3476
+ const modifiers = ts4.canHaveModifiers(statement) ? ts4.getModifiers(statement) : undefined;
3477
+ const isExported = modifiers?.some((modifier) => modifier.kind === ts4.SyntaxKind.ExportKeyword) ?? false;
3478
+ if (!isExported)
3479
+ continue;
3480
+ for (const declaration of statement.declarationList.declarations) {
3481
+ if (!ts4.isIdentifier(declaration.name) || declaration.name.text !== "fonts")
3482
+ continue;
3483
+ const value = declaration.initializer ? this.#literalToValue(declaration.initializer) : null;
3484
+ if (Array.isArray(value)) {
3485
+ fonts.push(...value.map((font) => this.#withFontDefaults(font)));
3486
+ }
3487
+ }
3488
+ }
3489
+ return fonts;
3669
3490
  }
3670
- get #artifactDir() {
3671
- return `${this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath}/.akan/artifact`;
3491
+ #literalToValue(node) {
3492
+ if (ts4.isStringLiteralLike(node))
3493
+ return node.text;
3494
+ if (ts4.isNumericLiteral(node))
3495
+ return Number(node.text);
3496
+ if (node.kind === ts4.SyntaxKind.TrueKeyword)
3497
+ return true;
3498
+ if (node.kind === ts4.SyntaxKind.FalseKeyword)
3499
+ return false;
3500
+ if (ts4.isArrayLiteralExpression(node))
3501
+ return node.elements.map((element) => this.#literalToValue(element));
3502
+ if (ts4.isObjectLiteralExpression(node)) {
3503
+ const obj = {};
3504
+ for (const prop of node.properties) {
3505
+ if (!ts4.isPropertyAssignment(prop))
3506
+ continue;
3507
+ const name = this.#getPropertyName(prop.name);
3508
+ if (!name)
3509
+ continue;
3510
+ obj[name] = this.#literalToValue(prop.initializer);
3511
+ }
3512
+ return obj;
3513
+ }
3514
+ if (ts4.isAsExpression(node) || ts4.isSatisfiesExpression(node) || ts4.isParenthesizedExpression(node)) {
3515
+ return this.#literalToValue(node.expression);
3516
+ }
3517
+ return;
3672
3518
  }
3673
- get #splitting() {
3674
- return process.env.AKAN_SERVER_PAGES_SPLITTING === "1";
3519
+ #getPropertyName(name) {
3520
+ if (ts4.isIdentifier(name) || ts4.isStringLiteral(name) || ts4.isNumericLiteral(name))
3521
+ return name.text;
3522
+ return null;
3675
3523
  }
3676
- #define() {
3677
- const nodeEnv = this.#command === "build" ? "production" : "development";
3678
- return {
3679
- "process.env.NODE_ENV": JSON.stringify(nodeEnv),
3680
- "process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("ssr"),
3681
- ...Object.fromEntries(Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]))
3682
- };
3524
+ #dedupeFonts(fonts) {
3525
+ const map = new Map;
3526
+ for (const font of fonts)
3527
+ map.set(JSON.stringify(font), font);
3528
+ return [...map.values()];
3683
3529
  }
3684
- static createPagesEntryPlugin(source) {
3685
- return {
3686
- name: "akan-pages-entry",
3687
- setup(build) {
3688
- build.onResolve({ filter: /^akan-pages-entry$/ }, () => ({
3689
- path: VIRTUAL_PAGES_ENTRY,
3690
- namespace: "akan-virtual"
3691
- }));
3692
- build.onLoad({ filter: /^akan-pages-entry$/, namespace: "akan-virtual" }, () => ({
3693
- contents: source,
3694
- loader: "tsx"
3695
- }));
3696
- }
3697
- };
3530
+ #withFontDefaults(font) {
3531
+ return { ...font, subsets: font.subsets ?? [...DEFAULT_FONT_SUBSETS] };
3698
3532
  }
3699
- static createServerCssStubPlugin() {
3700
- return {
3701
- name: "akan-server-css-stub",
3702
- setup(build) {
3703
- build.onLoad({ filter: /\.css$/ }, () => ({
3704
- contents: "",
3705
- loader: "js"
3706
- }));
3707
- }
3708
- };
3533
+ #getFontSubsets(font) {
3534
+ return font.subsets ?? DEFAULT_FONT_SUBSETS;
3709
3535
  }
3710
- static createServerUseClientFetchPlugin() {
3711
- return {
3712
- name: "akan-server-use-client-fetch",
3713
- setup(build) {
3714
- build.onLoad({ filter: /[/\\]lib[/\\]useClient\.(ts|tsx|js|jsx)$/ }, async (args) => {
3715
- const source = await Bun.file(args.path).text();
3716
- const transformed = PagesBundleBuilder.transformServerUseClientFetchSource(source);
3717
- if (transformed === source)
3718
- return;
3719
- return { contents: transformed, loader: loaderFor4(args.path) };
3720
- });
3721
- }
3722
- };
3536
+ #getFontVariableName(font) {
3537
+ return font.variable ?? `--font-${font.name}`;
3723
3538
  }
3724
- static transformServerUseClientFetchSource(source) {
3725
- if (!source.includes(`with { type: "macro" }`) || !source.includes("FetchClient.build"))
3726
- return source;
3727
- 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 });");
3539
+ #getFontFallbackName(font) {
3540
+ return font.fallbackName ?? `${font.name} fallback`;
3728
3541
  }
3729
- }
3730
- function loaderFor4(absPath) {
3731
- if (absPath.endsWith(".tsx"))
3732
- return "tsx";
3733
- if (absPath.endsWith(".jsx"))
3734
- return "jsx";
3735
- if (absPath.endsWith(".ts"))
3736
- return "ts";
3737
- return "js";
3738
- }
3739
-
3740
- // pkgs/@akanjs/devkit/frontendBuild/precompressArtifacts.ts
3741
- import fs4 from "fs";
3742
- import path21 from "path";
3743
- var COMPRESSIBLE_EXTS = new Set([".css", ".html", ".js", ".json", ".svg"]);
3744
- var MIN_COMPRESS_BYTES = 1024;
3745
- async function precompressArtifacts(app) {
3746
- const roots = [path21.join(app.dist.cwdPath, ".akan/artifact/client")];
3747
- const result = { files: 0, inputBytes: 0, outputBytes: 0 };
3748
- await Promise.all(roots.map((root) => precompressRoot(root, result)));
3749
- if (result.files > 0) {
3750
- app.verbose(`[precompress] wrote ${result.files} gzip sidecars (${formatBytes(result.inputBytes)} -> ${formatBytes(result.outputBytes)})`);
3542
+ #isFontOptimizationEnabled(font) {
3543
+ return font.optimize !== false;
3751
3544
  }
3752
- return result;
3753
- }
3754
- async function precompressRoot(root, result) {
3755
- if (!fs4.existsSync(root) || !fs4.statSync(root).isDirectory())
3756
- return;
3757
- const glob = new Bun.Glob("**/*");
3758
- for await (const filePath of glob.scan({ cwd: root, absolute: true })) {
3759
- if (!await shouldPrecompress(filePath))
3760
- continue;
3761
- const bytes = await Bun.file(filePath).bytes();
3762
- const gz = Bun.gzipSync(toArrayBuffer(bytes));
3763
- await Bun.write(`${filePath}.gz`, gz);
3764
- result.files += 1;
3765
- result.inputBytes += bytes.byteLength;
3766
- result.outputBytes += gz.byteLength;
3545
+ #getFontStyles(font) {
3546
+ return font.styles?.length ? font.styles : ["normal"];
3767
3547
  }
3768
- }
3769
- async function shouldPrecompress(filePath) {
3770
- if (filePath.endsWith(".gz"))
3771
- return false;
3772
- if (!COMPRESSIBLE_EXTS.has(path21.extname(filePath).toLowerCase()))
3773
- return false;
3774
- const file = Bun.file(filePath);
3775
- if (!await file.exists())
3776
- return false;
3777
- return file.size >= MIN_COMPRESS_BYTES;
3778
- }
3779
- function toArrayBuffer(bytes) {
3780
- return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
3781
- }
3782
- function formatBytes(bytes) {
3783
- if (bytes < 1024)
3784
- return `${bytes}B`;
3785
- if (bytes < 1024 * 1024)
3786
- return `${(bytes / 1024).toFixed(1)}KB`;
3787
- return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
3788
- }
3789
-
3790
- // pkgs/@akanjs/devkit/frontendBuild/ssrBaseArtifactBuilder.ts
3791
- import path24 from "path";
3792
- import { optimize } from "@tailwindcss/node";
3793
-
3794
- // pkgs/@akanjs/devkit/frontendBuild/cssCompiler.ts
3795
- import path23 from "path";
3796
- import { Logger as Logger2 } from "akanjs/common";
3797
- import { compile } from "tailwindcss";
3798
-
3799
- // pkgs/@akanjs/devkit/frontendBuild/cssImportResolver.ts
3800
- import path22 from "path";
3801
- var CSS_IMPORT_EXTS = ["", ".css", "/styles.css", "/index.css"];
3802
-
3803
- class CssImportResolver {
3804
- #workspaceRoot;
3805
- #paths;
3806
- #wildcardEntries;
3807
- constructor(workspaceRoot, paths = {}) {
3808
- this.#workspaceRoot = workspaceRoot;
3809
- this.#paths = paths;
3810
- this.#wildcardEntries = Object.entries(paths).filter(([key]) => key.endsWith("/*")).map(([key, replacements]) => ({ prefix: key.slice(0, -1), replacements })).sort((a, b) => b.prefix.length - a.prefix.length);
3548
+ #getFontFaces(font) {
3549
+ const enabledStyles = new Set(this.#getFontStyles(font));
3550
+ return font.paths.map((fontPath) => {
3551
+ const style = fontPath.style ?? "normal";
3552
+ return {
3553
+ font,
3554
+ path: fontPath,
3555
+ src: fontPath.src,
3556
+ weight: fontPath.weight,
3557
+ style,
3558
+ optimizedSrc: this.#getOptimizedFontSrc(font, fontPath)
3559
+ };
3560
+ }).filter((face) => enabledStyles.has(face.style));
3811
3561
  }
3812
- static async create(app) {
3813
- const tsconfig = await app.getTsConfig();
3814
- return new CssImportResolver(app.workspace.workspaceRoot, tsconfig.compilerOptions.paths ?? {});
3562
+ #getOptimizedFontSrc(font, fontPath) {
3563
+ const style = fontPath.style ?? "normal";
3564
+ const hash = this.#hashFontConfig({
3565
+ name: font.name,
3566
+ src: fontPath.src,
3567
+ weight: fontPath.weight,
3568
+ style,
3569
+ display: font.display,
3570
+ subset: font.subset,
3571
+ subsets: this.#getFontSubsets(font),
3572
+ subsetText: font.subsetText,
3573
+ subsetFiles: font.subsetFiles,
3574
+ declarations: [...font.declarations ?? [], ...fontPath.declarations ?? []]
3575
+ });
3576
+ return `${FONT_URL_PREFIX}/${this.#slugFontPart(font.name)}-${this.#slugFontPart(String(fontPath.weight))}-${style}-${hash}.woff2`;
3815
3577
  }
3816
- async resolve(id, fromBase) {
3817
- for (const resolve of [
3818
- () => this.#resolveWithTsconfig(id),
3819
- () => this.#resolveWithBun(id, fromBase),
3820
- () => this.#resolveWithRequire(id, fromBase),
3821
- () => this.#resolvePackageStyle(id, fromBase)
3822
- ]) {
3823
- const resolved = await resolve();
3824
- if (resolved)
3825
- return resolved;
3578
+ #hashFontConfig(value) {
3579
+ const input = this.#stableStringify(value);
3580
+ let hash = 2166136261;
3581
+ for (let i = 0;i < input.length; i++) {
3582
+ hash ^= input.charCodeAt(i);
3583
+ hash = Math.imul(hash, 16777619);
3584
+ }
3585
+ return (hash >>> 0).toString(36);
3586
+ }
3587
+ #stableStringify(value) {
3588
+ if (Array.isArray(value))
3589
+ return `[${value.map((item) => this.#stableStringify(item)).join(",")}]`;
3590
+ if (value && typeof value === "object") {
3591
+ const entries = Object.entries(value).filter(([, v]) => v !== undefined).sort(([a], [b]) => a.localeCompare(b));
3592
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${this.#stableStringify(v)}`).join(",")}}`;
3593
+ }
3594
+ return JSON.stringify(value);
3595
+ }
3596
+ #slugFontPart(value) {
3597
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "font";
3598
+ }
3599
+ async#resolveFontSourcePath(src) {
3600
+ if (!src.startsWith("/"))
3601
+ return null;
3602
+ const rel = src.replace(/^\//, "");
3603
+ const candidates = [
3604
+ this.#command === "build" ? path18.join(this.#app.dist.cwdPath, "public", rel) : null,
3605
+ path18.join(this.#app.cwdPath, "public", rel),
3606
+ this.#resolveWorkspacePublicPath(rel)
3607
+ ].filter(Boolean);
3608
+ for (const candidate of candidates) {
3609
+ if (await Bun.file(candidate).exists())
3610
+ return candidate;
3826
3611
  }
3827
3612
  return null;
3828
3613
  }
3829
- #resolveWithBun(id, fromBase) {
3830
- for (const base of this.#resolutionBases(fromBase)) {
3831
- try {
3832
- const resolved = Bun.resolveSync(id, base);
3833
- if (CssImportResolver.isCssFile(resolved))
3834
- return resolved;
3835
- } catch {}
3836
- }
3837
- return null;
3614
+ async#buildFontBuffer(font, sourceBuffer, sourcePath) {
3615
+ if (font.subset === false)
3616
+ return this.#convertToWoff2(sourceBuffer, sourcePath);
3617
+ const { default: subsetFont } = await import("subset-font");
3618
+ return subsetFont(sourceBuffer, await this.#getSubsetText(font), { targetFormat: "woff2" });
3838
3619
  }
3839
- #resolveWithRequire(id, fromBase) {
3840
- for (const base of this.#resolutionBases(fromBase)) {
3841
- try {
3842
- const resolved = __require.resolve(id, { paths: [base] });
3843
- if (CssImportResolver.isCssFile(resolved))
3844
- return resolved;
3845
- } catch {}
3620
+ async#convertToWoff2(buffer, sourcePath) {
3621
+ const { createFont } = await import("fonteditor-core");
3622
+ await this.#initWoff2();
3623
+ const font = createFont(buffer, { type: this.#getFontType(sourcePath, buffer) });
3624
+ return font.write({ type: "woff2", toBuffer: true });
3625
+ }
3626
+ async#initWoff2() {
3627
+ this.#woff2Ready ??= import("fonteditor-core").then(({ woff2 }) => woff2.init()).then(() => {
3628
+ return;
3629
+ });
3630
+ return this.#woff2Ready;
3631
+ }
3632
+ #getFontType(sourcePath, buffer) {
3633
+ const signature = buffer.toString("ascii", 0, 4);
3634
+ if (signature === "wOFF")
3635
+ return "woff";
3636
+ if (signature === "wOF2")
3637
+ return "woff2";
3638
+ if (signature === "OTTO")
3639
+ return "otf";
3640
+ const ext = path18.extname(sourcePath).slice(1).toLowerCase();
3641
+ if (ext === "otf" || ext === "woff" || ext === "woff2")
3642
+ return ext;
3643
+ return "ttf";
3644
+ }
3645
+ #resolveWorkspacePublicPath(rel) {
3646
+ const [root, dep, ...rest] = rel.split("/");
3647
+ if (root !== "libs" || !dep || rest.length === 0)
3648
+ return null;
3649
+ return path18.join(this.#app.workspace.workspaceRoot, "libs", dep, "public", ...rest);
3650
+ }
3651
+ async#getSubsetText(font) {
3652
+ const parts = new Set;
3653
+ const subsets = this.#getFontSubsets(font);
3654
+ for (const subset of subsets)
3655
+ parts.add(await this.#getSubsetPresetText(subset));
3656
+ if (font.subsetText)
3657
+ parts.add(font.subsetText);
3658
+ for (const filePath of font.subsetFiles ?? []) {
3659
+ const abs = path18.isAbsolute(filePath) ? filePath : path18.join(this.#app.cwdPath, filePath);
3660
+ const file = Bun.file(abs);
3661
+ if (await file.exists())
3662
+ parts.add(await file.text());
3846
3663
  }
3847
- return null;
3664
+ return [...parts].join("");
3848
3665
  }
3849
- async#resolveWithTsconfig(id) {
3850
- const exact = this.#paths[id];
3851
- if (exact) {
3852
- for (const repl of exact) {
3853
- const resolved = await this.#firstExisting(path22.resolve(this.#workspaceRoot, repl));
3854
- if (resolved)
3855
- return resolved;
3666
+ async#getSubsetPresetText(subset) {
3667
+ if (subset === "latin")
3668
+ return this.#rangeText(32, 126);
3669
+ if (subset === "latin-ext")
3670
+ return `${this.#rangeText(32, 126)}${this.#rangeText(160, 591)}`;
3671
+ if (subset === "ks-x-1001")
3672
+ return FontOptimizer.#getKsX1001Text();
3673
+ if (subset === "auto")
3674
+ return this.#collectAutoSubsetText();
3675
+ return "";
3676
+ }
3677
+ async#collectAutoSubsetText() {
3678
+ const roots = ["page", "ui"].map((dir) => path18.join(this.#app.cwdPath, dir));
3679
+ const glob = new Bun.Glob("**/*.{ts,tsx,js,jsx,html,md}");
3680
+ const parts = [];
3681
+ await Promise.all(roots.map(async (root) => {
3682
+ if (!await Bun.file(root).exists())
3683
+ return;
3684
+ for await (const filePath of glob.scan({ cwd: root, absolute: true })) {
3685
+ parts.push(await Bun.file(filePath).text());
3856
3686
  }
3857
- }
3858
- for (const { prefix, replacements } of this.#wildcardEntries) {
3859
- if (!id.startsWith(prefix))
3860
- continue;
3861
- const suffix = id.slice(prefix.length);
3862
- for (const repl of replacements) {
3863
- const replPath = repl.endsWith("/*") ? repl.slice(0, -1) : repl;
3864
- const resolved = await this.#firstExisting(path22.resolve(this.#workspaceRoot, replPath + suffix));
3865
- if (resolved)
3866
- return resolved;
3687
+ }));
3688
+ return parts.join("");
3689
+ }
3690
+ #rangeText(start, end) {
3691
+ let text = "";
3692
+ for (let code = start;code <= end; code++)
3693
+ text += String.fromCodePoint(code);
3694
+ return text;
3695
+ }
3696
+ static #getKsX1001Text() {
3697
+ if (FontOptimizer.#ksX1001Text)
3698
+ return FontOptimizer.#ksX1001Text;
3699
+ try {
3700
+ const decoder = new TextDecoder("euc-kr");
3701
+ const chars = new Set;
3702
+ for (let lead = 161;lead <= 254; lead++) {
3703
+ for (let trail = 161;trail <= 254; trail++) {
3704
+ const char = decoder.decode(Uint8Array.of(lead, trail));
3705
+ if (char && char !== "\uFFFD")
3706
+ chars.add(char);
3707
+ }
3867
3708
  }
3709
+ FontOptimizer.#ksX1001Text = [...chars].join("");
3710
+ } catch {
3711
+ FontOptimizer.#ksX1001Text = FontOptimizer.#rangeTextStatic(44032, 55203);
3868
3712
  }
3869
- return null;
3713
+ return FontOptimizer.#ksX1001Text;
3870
3714
  }
3871
- async#resolvePackageStyle(id, fromBase) {
3872
- const pkgName = CssImportResolver.getPackageName(id);
3873
- if (!pkgName)
3874
- return null;
3875
- for (const base of this.#resolutionBases(fromBase)) {
3876
- try {
3877
- const pkgPath = __require.resolve(`${pkgName}/package.json`, { paths: [base] });
3878
- const resolved = await this.#resolvePackageStyleFromPackageJson(id, pkgName, pkgPath);
3879
- if (resolved)
3880
- return resolved;
3881
- } catch {}
3715
+ static #rangeTextStatic(start, end) {
3716
+ let text = "";
3717
+ for (let code = start;code <= end; code++)
3718
+ text += String.fromCodePoint(code);
3719
+ return text;
3720
+ }
3721
+ #buildOptimizedFontFaceRule(font, face) {
3722
+ const declarations = [
3723
+ ["font-family", this.#quote(font.name)],
3724
+ ["src", `url(${this.#quote(face.optimizedSrc)}) format("woff2")`],
3725
+ ["font-weight", String(face.weight)],
3726
+ ["font-style", face.style],
3727
+ ["font-display", font.display ?? "swap"],
3728
+ ...this.#toDeclarationEntries(font.declarations),
3729
+ ...this.#toDeclarationEntries(face.path.declarations)
3730
+ ];
3731
+ return `@font-face {
3732
+ ${declarations.map(([prop, value]) => ` ${prop}: ${value};`).join(`
3733
+ `)}
3734
+ }`;
3735
+ }
3736
+ async#buildFontaineFallbackCss(font, face, outputPath) {
3737
+ if (font.adjustFontFallback === false)
3738
+ return "";
3739
+ const { generateFontFace, getMetricsForFamily, readMetrics, resolveCategoryFallbacks } = await import("fontaine");
3740
+ const metrics = await readMetrics(outputPath).catch(() => null);
3741
+ if (!metrics)
3742
+ return "";
3743
+ const fallbacks = resolveCategoryFallbacks({
3744
+ fontFamily: font.name,
3745
+ fallbacks: font.fallbacks ?? {},
3746
+ metrics: { ...metrics, category: font.category }
3747
+ });
3748
+ const css = [];
3749
+ for (let i = fallbacks.length - 1;i >= 0; i--) {
3750
+ const fallback = fallbacks[i];
3751
+ const fallbackMetrics = await getMetricsForFamily(fallback);
3752
+ if (!fallbackMetrics)
3753
+ continue;
3754
+ css.push(generateFontFace({ ...metrics, category: font.category }, {
3755
+ name: this.#getFontFallbackName(font),
3756
+ font: fallback,
3757
+ metrics: fallbackMetrics,
3758
+ "font-weight": String(face.weight),
3759
+ "font-style": face.style
3760
+ }));
3882
3761
  }
3883
- for (const pkgPath of this.#packageJsonCandidates(pkgName)) {
3884
- const resolved = await this.#resolvePackageStyleFromPackageJson(id, pkgName, pkgPath);
3885
- if (resolved)
3886
- return resolved;
3762
+ return css.join("");
3763
+ }
3764
+ #buildRootVariableRule(font) {
3765
+ return `:root { ${this.#getFontVariableName(font)}: ${this.#quote(font.name)}, ${this.#quote(this.#getFontFallbackName(font))}; }`;
3766
+ }
3767
+ #buildFontUtilityRules(fonts) {
3768
+ const rules = [];
3769
+ const seen = new Set;
3770
+ for (const font of fonts) {
3771
+ const className = font.className || `font-${font.name}`;
3772
+ const selector = `.${this.#escapeCssClassName(className)}`;
3773
+ const rule = `${selector} { font-family: var(${this.#getFontVariableName(font)}); }`;
3774
+ if (seen.has(rule))
3775
+ continue;
3776
+ seen.add(rule);
3777
+ rules.push(rule);
3887
3778
  }
3888
- return null;
3779
+ return rules.join(`
3780
+ `);
3889
3781
  }
3890
- async#resolvePackageStyleFromPackageJson(id, pkgName, pkgPath) {
3891
- try {
3892
- if (!await Bun.file(pkgPath).exists())
3893
- return null;
3894
- const pkgDir = path22.dirname(pkgPath);
3895
- const pkg = await Bun.file(pkgPath).json();
3896
- const subpath = id === pkgName ? "." : `.${id.slice(pkgName.length)}`;
3897
- const exportValue = pkg.exports?.[subpath];
3898
- const styleEntry = (typeof exportValue === "string" ? exportValue : exportValue?.style || exportValue?.import || exportValue?.default) || pkg.exports?.["."]?.style || pkg.style || "index.css";
3899
- return await this.#firstExisting(path22.resolve(pkgDir, styleEntry));
3900
- } catch {
3782
+ #escapeCssClassName(value) {
3783
+ return value.replace(/^-?\d|[^a-zA-Z0-9_-]/g, (part) => [...part].map((char) => `\\${char.codePointAt(0)?.toString(16)} `).join(""));
3784
+ }
3785
+ #toDeclarationEntries(declarations = []) {
3786
+ return declarations.map((declaration) => [declaration.prop, declaration.value]);
3787
+ }
3788
+ #quote(value) {
3789
+ return JSON.stringify(value);
3790
+ }
3791
+ }
3792
+
3793
+ // pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
3794
+ import path20 from "path";
3795
+
3796
+ // pkgs/@akanjs/devkit/transforms/externalizeFrameworkPlugin.ts
3797
+ import path19 from "path";
3798
+ var DEFAULT_INCLUDE = ["akanjs/", "@apps/", "@libs/"];
3799
+ var DEFAULT_EXCLUDE_EXACT = new Set(["akanjs/webkit", "@akanjs/cli", "@akanjs/devkit"]);
3800
+ var DEFAULT_EXCLUDE_PREFIX = ["@akanjs/cli/", "@akanjs/devkit/"];
3801
+ var OPTIONAL_BACKEND_EXTERNAL_EXACT = new Set([
3802
+ "@libsql/client",
3803
+ "bullmq",
3804
+ "croner",
3805
+ "ioredis",
3806
+ "postgres",
3807
+ "protobufjs"
3808
+ ]);
3809
+ var RUNTIME_EXTERNAL_EXACT = new Set([
3810
+ "react",
3811
+ "react-dom",
3812
+ "react/jsx-runtime",
3813
+ "react/jsx-dev-runtime",
3814
+ "react-server-dom-webpack",
3815
+ "react-server-dom-webpack/server.node",
3816
+ "react-server-dom-webpack/client.node",
3817
+ "react-server-dom-webpack/client.browser"
3818
+ ]);
3819
+ var RUNTIME_EXTERNAL_PREFIX = ["react-dom/", "react-server-dom-webpack/"];
3820
+ var CANDIDATE_EXTS3 = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
3821
+ async function createExternalizeFrameworkPlugin(options) {
3822
+ const tsconfig = await options.app.getTsConfig();
3823
+ const includePrefixes = options.include ?? DEFAULT_INCLUDE;
3824
+ const extraExact = new Set(options.extra ?? []);
3825
+ const workspaceRoot = options.app.workspace.workspaceRoot;
3826
+ const tsconfigPaths = tsconfig.compilerOptions.paths ?? {};
3827
+ const rootEntries = Object.entries(tsconfigPaths).filter(([k]) => !k.endsWith("/*")).map(([k, v]) => ({ pkg: k, entryFile: v[0] ?? null })).filter((e) => e.entryFile !== null);
3828
+ const wildcardEntries = Object.entries(tsconfigPaths).filter(([k]) => k.endsWith("/*")).map(([k, v]) => ({ prefix: k.slice(0, -1), replacements: v })).sort((a, b) => b.prefix.length - a.prefix.length);
3829
+ async function resolveWorkspaceSubpath(spec) {
3830
+ if (!workspaceRoot)
3901
3831
  return null;
3832
+ for (const { prefix, replacements } of wildcardEntries) {
3833
+ if (!spec.startsWith(prefix))
3834
+ continue;
3835
+ const suffix = spec.slice(prefix.length);
3836
+ for (const repl of replacements) {
3837
+ const replPath = repl?.endsWith("/*") ? repl.slice(0, -1) : repl ?? "";
3838
+ if (!replPath)
3839
+ continue;
3840
+ const candidate = path19.resolve(workspaceRoot, replPath + suffix);
3841
+ const hit = await firstExisting(candidate);
3842
+ if (hit)
3843
+ return hit;
3844
+ }
3902
3845
  }
3903
- }
3904
- #resolutionBases(fromBase) {
3905
- return [fromBase, this.#workspaceRoot, path22.dirname(Bun.main), path22.resolve(path22.dirname(Bun.main), "../..")];
3906
- }
3907
- #packageJsonCandidates(pkgName) {
3908
- return [
3909
- path22.join(this.#workspaceRoot, "pkgs", pkgName, "package.json"),
3910
- path22.join(this.#workspaceRoot, "node_modules", pkgName, "package.json"),
3911
- path22.join(path22.dirname(Bun.main), "node_modules", pkgName, "package.json"),
3912
- path22.join(path22.dirname(Bun.main), "../../", pkgName, "package.json")
3913
- ];
3914
- }
3915
- async#firstExisting(basePath) {
3916
- for (const suffix of CSS_IMPORT_EXTS) {
3917
- const candidate = `${basePath}${suffix}`;
3918
- if (await Bun.file(candidate).exists())
3919
- return candidate;
3846
+ for (const { pkg, entryFile } of rootEntries) {
3847
+ if (spec !== pkg && !spec.startsWith(`${pkg}/`))
3848
+ continue;
3849
+ if (spec === pkg)
3850
+ continue;
3851
+ const suffix = spec.slice(pkg.length + 1);
3852
+ const pkgDir = path19.dirname(path19.resolve(workspaceRoot, entryFile));
3853
+ const candidate = path19.join(pkgDir, suffix);
3854
+ const hit = await firstExisting(candidate);
3855
+ if (hit)
3856
+ return hit;
3920
3857
  }
3921
3858
  return null;
3922
3859
  }
3923
- static getPackageName(id) {
3924
- const parts = id.split("/");
3925
- if (id.startsWith("@"))
3926
- return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
3927
- return parts[0] ?? null;
3860
+ return {
3861
+ name: "akan-externalize-framework",
3862
+ setup(build) {
3863
+ build.onResolve({ filter: /.*/ }, async (args) => {
3864
+ const spec = args.path;
3865
+ if (spec === "." || spec === ".." || spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/"))
3866
+ return;
3867
+ if (extraExact.has(spec) || DEFAULT_EXCLUDE_EXACT.has(spec))
3868
+ return { path: spec, external: true };
3869
+ for (const prefix of DEFAULT_EXCLUDE_PREFIX) {
3870
+ if (spec.startsWith(prefix))
3871
+ return { path: spec, external: true };
3872
+ }
3873
+ if (OPTIONAL_BACKEND_EXTERNAL_EXACT.has(spec))
3874
+ return { path: spec, external: true };
3875
+ if (RUNTIME_EXTERNAL_EXACT.has(spec))
3876
+ return { path: spec, external: true };
3877
+ for (const prefix of RUNTIME_EXTERNAL_PREFIX) {
3878
+ if (spec.startsWith(prefix))
3879
+ return { path: spec, external: true };
3880
+ }
3881
+ for (const prefix of includePrefixes) {
3882
+ if (!spec.startsWith(prefix))
3883
+ continue;
3884
+ const resolved = await resolveWorkspaceSubpath(spec);
3885
+ if (resolved)
3886
+ return { path: resolved };
3887
+ return;
3888
+ }
3889
+ return;
3890
+ });
3891
+ }
3892
+ };
3893
+ }
3894
+ async function firstExisting(basePath) {
3895
+ if (await Bun.file(basePath).exists())
3896
+ return basePath;
3897
+ for (const ext of CANDIDATE_EXTS3) {
3898
+ const candidate = `${basePath}${ext}`;
3899
+ if (await Bun.file(candidate).exists())
3900
+ return candidate;
3928
3901
  }
3929
- static isCssFile(filePath) {
3930
- return path22.extname(filePath) === ".css";
3902
+ for (const ext of CANDIDATE_EXTS3) {
3903
+ const candidate = path19.join(basePath, `index${ext}`);
3904
+ if (await Bun.file(candidate).exists())
3905
+ return candidate;
3931
3906
  }
3907
+ return null;
3932
3908
  }
3933
3909
 
3934
- // pkgs/@akanjs/devkit/frontendBuild/cssCompiler.ts
3935
- var SOURCE_EXTS4 = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
3936
- var NON_SOURCE_EXT_RE2 = /\.(json|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp3|mp4|wav)$/i;
3937
- var NODE_MODULES_RE3 = /[\\/]node_modules[\\/]/;
3938
- var AKANJS_NODE_MODULE_RE3 = /[\\/]node_modules[\\/]akanjs[\\/]/;
3939
-
3940
- class CssCompiler {
3941
- #logger = new Logger2("CssCompiler");
3942
- #transpiler = new Bun.Transpiler({ loader: "tsx" });
3943
- #app;
3944
- #cssImportResolver = null;
3945
- constructor(app) {
3946
- this.#app = app;
3947
- }
3948
- #cssText = null;
3949
- #cssTextByBasePath = null;
3950
- async getCss({ refresh } = {}) {
3951
- if (this.#cssText !== null && !refresh)
3952
- return this.#cssText;
3953
- const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh });
3954
- this.#cssText = await this.compileCss(cssPaths, sourcePaths);
3955
- return this.#cssText;
3956
- }
3957
- async getCssByBasePath({ refresh } = {}) {
3958
- if (this.#cssTextByBasePath !== null && !refresh)
3959
- return this.#cssTextByBasePath;
3960
- const akanConfig = await this.#app.getConfig({ refresh });
3961
- const pageKeys = await this.#app.getPageKeys({ refresh });
3962
- const basePaths = [...akanConfig.basePaths];
3963
- const rootPageKeys = pageKeys.filter((pageKey) => getPageKeyBasePath(pageKey, basePaths) === null);
3964
- const cssEntries = await Promise.all([
3965
- (async () => {
3966
- if (rootPageKeys.length === 0)
3967
- return ["", ""];
3968
- const started = Date.now();
3969
- const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: rootPageKeys });
3970
- const css = await this.compileCss(cssPaths, sourcePaths);
3971
- this.#logger.verbose(`css base=root paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`);
3972
- return ["", css];
3973
- })(),
3974
- ...basePaths.map(async (basePath) => {
3975
- const basePathPageKeys = pageKeys.filter((pageKey) => getPageKeyBasePath(pageKey, basePaths) === basePath);
3976
- if (basePathPageKeys.length === 0)
3977
- return [basePath, ""];
3978
- const started = Date.now();
3979
- const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: basePathPageKeys });
3980
- const css = await this.compileCss(cssPaths, sourcePaths);
3981
- this.#logger.verbose(`css base=${basePath} paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`);
3982
- return [basePath, css];
3983
- })
3984
- ]);
3985
- this.#cssTextByBasePath = Object.fromEntries(cssEntries);
3986
- return this.#cssTextByBasePath;
3987
- }
3988
- async discoverCss({ refresh } = {}) {
3989
- const { cssPaths } = await this.discoverCssAndSources({ refresh });
3990
- return cssPaths;
3991
- }
3992
- async discoverCssAndSources({
3993
- refresh,
3994
- pageKeys
3995
- } = {}) {
3996
- pageKeys ??= await this.#app.getPageKeys({ refresh });
3997
- const seeds = pageKeys.map((key) => path23.resolve(this.#app.cwdPath, "page", key));
3998
- const cssFiles = new Set;
3999
- const sourceFiles = new Set;
4000
- const queue = [...seeds];
4001
- const resolvePackage = await createTsconfigPackageResolver(this.#app);
4002
- const analyzer = new BarrelAnalyzer({ resolvePackage });
4003
- const akanConfig = await this.#app.getConfig({ refresh });
4004
- while (queue.length > 0) {
4005
- const filePath = queue.shift();
4006
- if (!filePath || sourceFiles.has(filePath) || isIgnoredNodeModuleSource(filePath))
4007
- continue;
4008
- sourceFiles.add(filePath);
4009
- let content;
4010
- try {
4011
- content = await Bun.file(filePath).text();
4012
- } catch {
4013
- continue;
4014
- }
4015
- let source = content;
4016
- if (akanConfig.barrelImports.length > 0) {
3910
+ // pkgs/@akanjs/devkit/transforms/useClientBundlePlugin.ts
3911
+ function createUseClientBundlePlugin(options = {}) {
3912
+ return {
3913
+ name: "akan-use-client-bundle",
3914
+ setup(build) {
3915
+ build.onLoad({ filter: /\.(tsx|ts|jsx|js)$/ }, async (args) => {
3916
+ if (args.path.includes("/node_modules/"))
3917
+ return;
3918
+ let source;
4017
3919
  try {
4018
- const rewritten = await rewriteBarrelImports(content, akanConfig.barrelImports, analyzer);
4019
- if (rewritten !== null)
4020
- source = rewritten;
4021
- } catch {}
4022
- }
4023
- let imports;
4024
- try {
4025
- imports = this.#transpiler.scanImports(source);
4026
- } catch {
4027
- continue;
4028
- }
4029
- const importerDir = path23.dirname(filePath);
4030
- for (const imp of imports) {
4031
- const spec = imp.path;
4032
- if (!spec)
4033
- continue;
4034
- if (spec.endsWith(".css")) {
4035
- const cssPath = await this.#resolveCssImport(spec, importerDir);
4036
- cssFiles.add(cssPath);
4037
- continue;
3920
+ source = await Bun.file(args.path).text();
3921
+ } catch {
3922
+ return;
4038
3923
  }
4039
- if (NON_SOURCE_EXT_RE2.test(spec))
4040
- continue;
4041
- const resolved = await this.#resolveSourceImport(spec, importerDir, resolvePackage);
4042
- if (!resolved || sourceFiles.has(resolved) || isIgnoredNodeModuleSource(resolved))
4043
- continue;
4044
- queue.push(resolved);
4045
- }
4046
- }
4047
- return { cssPaths: [...cssFiles], sourcePaths: [...sourceFiles] };
4048
- }
4049
- async compileCss(cssPaths, sourcePaths) {
4050
- if (cssPaths.length === 0)
4051
- return "";
4052
- const compileStarted = Date.now();
4053
- const compilers = await Promise.all(cssPaths.map(async (cssPath) => {
4054
- const css = await Bun.file(cssPath).text();
4055
- const base = path23.dirname(cssPath);
4056
- const compiler = await compile(css, {
4057
- base,
4058
- loadStylesheet: (id, fromBase) => this.#loadStylesheet(id, fromBase),
4059
- loadModule: (id, fromBase) => this.#loadModule(id, fromBase)
3924
+ const stubbed = transformUseClient(source, {
3925
+ path: args.path,
3926
+ workspaceRoot: options.workspaceRoot
3927
+ });
3928
+ if (stubbed === null)
3929
+ return;
3930
+ return { contents: stubbed, loader: loaderFor3(args.path) };
4060
3931
  });
4061
- return { cssPath, compiler };
4062
- }));
4063
- const sourceDirs = new Set;
4064
- for (const entry of compilers) {
4065
- if (!entry)
4066
- continue;
4067
- for (const s of entry.compiler.sources)
4068
- sourceDirs.add(s.base);
4069
- }
4070
- const scanStarted = Date.now();
4071
- const candidates = await this.#scanCandidates(sourcePaths, [...sourceDirs]);
4072
- this.#logger.verbose(`css candidates scanned count=${candidates.length} sources=${sourcePaths.length} dirs=${sourceDirs.size} in ${Date.now() - scanStarted}ms`);
4073
- const parts = [];
4074
- for (const entry of compilers) {
4075
- if (!entry)
4076
- continue;
4077
- parts.push(entry.compiler.build(candidates));
4078
3932
  }
4079
- this.#logger.verbose(`css compiled paths=${cssPaths.length} candidates=${candidates.length} in ${Date.now() - compileStarted}ms`);
4080
- return parts.join(`
4081
- `);
3933
+ };
3934
+ }
3935
+ function loaderFor3(absPath) {
3936
+ if (absPath.endsWith(".tsx"))
3937
+ return "tsx";
3938
+ if (absPath.endsWith(".jsx"))
3939
+ return "jsx";
3940
+ if (absPath.endsWith(".ts"))
3941
+ return "ts";
3942
+ return "js";
3943
+ }
3944
+
3945
+ // pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
3946
+ var VIRTUAL_PAGES_ENTRY = "akan-pages-entry";
3947
+
3948
+ class PagesBundleBuilder {
3949
+ #app;
3950
+ #command;
3951
+ #pageEntries;
3952
+ #started = Date.now();
3953
+ constructor(app, command = "start", pageEntries) {
3954
+ this.#app = app;
3955
+ this.#command = command;
3956
+ this.#pageEntries = pageEntries;
4082
3957
  }
4083
- async#loadStylesheet(id, fromBase) {
4084
- const p = await this.#resolveCssImport(id, fromBase);
4085
- const content = await Bun.file(p).text();
4086
- return { path: p, base: path23.dirname(p), content };
3958
+ async build() {
3959
+ const akanConfig = await this.#app.getConfig();
3960
+ const resolvedEntries = this.#pageEntries ?? await resolveSsrPageEntriesForApp(this.#app, await this.#app.getPageKeys());
3961
+ const entrySource = PagesEntrySourceGenerator.generate(resolvedEntries);
3962
+ const workspaceRoot = this.#app.workspace.workspaceRoot;
3963
+ const result = await Bun.build({
3964
+ entrypoints: [VIRTUAL_PAGES_ENTRY],
3965
+ outdir: `${this.#artifactDir}/server`,
3966
+ target: "bun",
3967
+ format: "esm",
3968
+ splitting: this.#splitting,
3969
+ minify: this.#command === "build",
3970
+ naming: {
3971
+ entry: "pages-[hash].[ext]",
3972
+ chunk: "chunks/[name]-[hash].[ext]",
3973
+ asset: "assets/[name]-[hash].[ext]"
3974
+ },
3975
+ define: this.#define(),
3976
+ plugins: [
3977
+ PagesBundleBuilder.createPagesEntryPlugin(entrySource),
3978
+ PagesBundleBuilder.createServerCssStubPlugin(),
3979
+ PagesBundleBuilder.createServerUseClientFetchPlugin(),
3980
+ await createExternalizeFrameworkPlugin({ app: this.#app, extra: akanConfig.externalLibs }),
3981
+ akanConfig.barrelImports.length > 0 ? await createBarrelImportsPlugin(this.#app, {
3982
+ pipeAfter: (source, args) => transformUseClient(source, {
3983
+ path: args.path,
3984
+ workspaceRoot
3985
+ })
3986
+ }) : createUseClientBundlePlugin({ workspaceRoot })
3987
+ ]
3988
+ });
3989
+ if (!result.success)
3990
+ throw new AggregateError(result.logs, "[PagesBundleBuilder] Bun.build failed");
3991
+ const entryArtifact = result.outputs.find((a) => a.kind === "entry-point");
3992
+ if (!entryArtifact)
3993
+ throw new Error("[PagesBundleBuilder] Bun.build emitted no entry-point artifact");
3994
+ const bundlePath = path20.resolve(entryArtifact.path);
3995
+ const buildId = Date.now();
3996
+ const outputBytes = result.outputs.reduce((sum, output) => sum + output.size, 0);
3997
+ const chunkCount = result.outputs.filter((output) => output.kind === "chunk").length;
3998
+ this.#app.verbose(`[PagesBundleBuilder] ${path20.basename(bundlePath)} emitted in ${Date.now() - this.#started}ms splitting=${this.#splitting} entry=${entryArtifact.size} bytes outputs=${result.outputs.length} chunks=${chunkCount} total=${outputBytes} bytes`);
3999
+ return {
4000
+ bundlePath,
4001
+ buildId,
4002
+ splitting: this.#splitting,
4003
+ entryBytes: entryArtifact.size,
4004
+ outputBytes,
4005
+ outputCount: result.outputs.length,
4006
+ chunkCount
4007
+ };
4087
4008
  }
4088
- async#resolveCssImport(id, fromBase) {
4089
- if (id.startsWith(".") || id.startsWith("/"))
4090
- return path23.resolve(fromBase, id);
4091
- const resolver = await this.#getCssImportResolver();
4092
- const resolved = await resolver.resolve(id, fromBase);
4093
- if (resolved)
4094
- return resolved;
4095
- throw new Error(`[css] failed to resolve stylesheet import "${id}" from ${fromBase}`);
4009
+ get #artifactDir() {
4010
+ return `${this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath}/.akan/artifact`;
4096
4011
  }
4097
- async#getCssImportResolver() {
4098
- if (this.#cssImportResolver)
4099
- return this.#cssImportResolver;
4100
- this.#cssImportResolver = await CssImportResolver.create(this.#app);
4101
- return this.#cssImportResolver;
4012
+ get #splitting() {
4013
+ return process.env.AKAN_SERVER_PAGES_SPLITTING === "1";
4102
4014
  }
4103
- async#loadModule(id, fromBase) {
4104
- const p = __require.resolve(id, { paths: [fromBase] });
4105
- const mod = await import(p);
4106
- return { path: p, base: path23.dirname(p), module: mod.default ?? mod };
4015
+ #define() {
4016
+ const nodeEnv = this.#command === "build" ? "production" : "development";
4017
+ return {
4018
+ "process.env.NODE_ENV": JSON.stringify(nodeEnv),
4019
+ "process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("ssr"),
4020
+ ...Object.fromEntries(Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]))
4021
+ };
4107
4022
  }
4108
- async#resolveSourceImport(id, fromBase, resolvePackage) {
4109
- if (id.startsWith(".") || id.startsWith("/")) {
4110
- const abs = id.startsWith("/") ? id : path23.resolve(fromBase, id);
4111
- return resolveSourceFileCandidate(abs);
4112
- }
4113
- const pkg = await resolvePackage(id);
4114
- if (pkg)
4115
- return pkg.entryFile;
4116
- for (const resolve of [() => resolveSourceWithBun(id, fromBase), () => resolveSourceWithRequire(id, fromBase)]) {
4117
- const resolved = await resolve();
4118
- if (resolved)
4119
- return resolved;
4120
- }
4121
- return null;
4023
+ static createPagesEntryPlugin(source) {
4024
+ return {
4025
+ name: "akan-pages-entry",
4026
+ setup(build) {
4027
+ build.onResolve({ filter: /^akan-pages-entry$/ }, () => ({
4028
+ path: VIRTUAL_PAGES_ENTRY,
4029
+ namespace: "akan-virtual"
4030
+ }));
4031
+ build.onLoad({ filter: /^akan-pages-entry$/, namespace: "akan-virtual" }, () => ({
4032
+ contents: source,
4033
+ loader: "tsx"
4034
+ }));
4035
+ }
4036
+ };
4122
4037
  }
4123
- async#scanCandidates(sourcePaths, dirs) {
4124
- const CANDIDATE_RE = /-?[\w@][\w:/.-]*(?:\[[^\]]+\][\w:/.-]*)*/g;
4125
- const candidates = new Set;
4126
- const glob = new Bun.Glob("**/*.{tsx,ts,jsx,js,html}");
4127
- const files = new Set(sourcePaths);
4128
- await Promise.all(dirs.map(async (dir) => {
4129
- for await (const file of glob.scan({ cwd: dir, absolute: true })) {
4130
- if (isIgnoredNodeModuleSource(file))
4131
- continue;
4132
- files.add(file);
4038
+ static createServerCssStubPlugin() {
4039
+ return {
4040
+ name: "akan-server-css-stub",
4041
+ setup(build) {
4042
+ build.onLoad({ filter: /\.css$/ }, () => ({
4043
+ contents: "",
4044
+ loader: "js"
4045
+ }));
4133
4046
  }
4134
- }));
4135
- await Promise.all([...files].map(async (file) => {
4136
- const content = await Bun.file(file).text();
4137
- for (const m of content.matchAll(CANDIDATE_RE))
4138
- candidates.add(m[0]);
4139
- }));
4140
- return [...candidates];
4047
+ };
4141
4048
  }
4142
- }
4143
- async function resolveSourceFileCandidate(absPathNoExt) {
4144
- if (await Bun.file(absPathNoExt).exists())
4145
- return isSourceFile(absPathNoExt) ? absPathNoExt : null;
4146
- for (const ext of SOURCE_EXTS4) {
4147
- const filePath = `${absPathNoExt}${ext}`;
4148
- if (await Bun.file(filePath).exists())
4149
- return filePath;
4049
+ static createServerUseClientFetchPlugin() {
4050
+ return {
4051
+ name: "akan-server-use-client-fetch",
4052
+ setup(build) {
4053
+ build.onLoad({ filter: /[/\\]lib[/\\]useClient\.(ts|tsx|js|jsx)$/ }, async (args) => {
4054
+ const source = await Bun.file(args.path).text();
4055
+ const transformed = PagesBundleBuilder.transformServerUseClientFetchSource(source);
4056
+ if (transformed === source)
4057
+ return;
4058
+ return { contents: transformed, loader: loaderFor4(args.path) };
4059
+ });
4060
+ }
4061
+ };
4150
4062
  }
4151
- for (const ext of SOURCE_EXTS4) {
4152
- const filePath = path23.join(absPathNoExt, `index${ext}`);
4153
- if (await Bun.file(filePath).exists())
4154
- return filePath;
4063
+ static transformServerUseClientFetchSource(source) {
4064
+ if (!source.includes(`with { type: "macro" }`) || !source.includes("FetchClient.build"))
4065
+ return source;
4066
+ return source.replace(/import\s+\{\s*getSerializedSignal\s*\}\s+from\s+["']\.\/sig["']\s+with\s+\{\s*type\s*:\s*["']macro["']\s*\};/, `import { fetch as serverFetch } from "./sig";`).replace(/const\s+fetchProto\s*=\s*FetchClient\.build<[^;]+;/, "const fetchProto = FetchClient.build<typeof signal>(cnst, serverFetch.serializedSignal, { Err: pageProto.Err, base: serverFetch });");
4155
4067
  }
4156
- return null;
4157
4068
  }
4158
- function resolveSourceWithBun(id, fromBase) {
4159
- try {
4160
- const resolved = Bun.resolveSync(id, fromBase);
4161
- return isSourceFile(resolved) ? resolved : null;
4162
- } catch {
4163
- return null;
4069
+ function loaderFor4(absPath) {
4070
+ if (absPath.endsWith(".tsx"))
4071
+ return "tsx";
4072
+ if (absPath.endsWith(".jsx"))
4073
+ return "jsx";
4074
+ if (absPath.endsWith(".ts"))
4075
+ return "ts";
4076
+ return "js";
4077
+ }
4078
+
4079
+ // pkgs/@akanjs/devkit/frontendBuild/precompressArtifacts.ts
4080
+ import fs3 from "fs";
4081
+ import path21 from "path";
4082
+ var COMPRESSIBLE_EXTS = new Set([".css", ".html", ".js", ".json", ".svg"]);
4083
+ var MIN_COMPRESS_BYTES = 1024;
4084
+ async function precompressArtifacts(app) {
4085
+ const roots = [path21.join(app.dist.cwdPath, ".akan/artifact/client")];
4086
+ const result = { files: 0, inputBytes: 0, outputBytes: 0 };
4087
+ await Promise.all(roots.map((root) => precompressRoot(root, result)));
4088
+ if (result.files > 0) {
4089
+ app.verbose(`[precompress] wrote ${result.files} gzip sidecars (${formatBytes(result.inputBytes)} -> ${formatBytes(result.outputBytes)})`);
4164
4090
  }
4091
+ return result;
4165
4092
  }
4166
- function resolveSourceWithRequire(id, fromBase) {
4167
- try {
4168
- const resolved = __require.resolve(id, { paths: [fromBase] });
4169
- return isSourceFile(resolved) ? resolved : null;
4170
- } catch {
4171
- return null;
4093
+ async function precompressRoot(root, result) {
4094
+ if (!fs3.existsSync(root) || !fs3.statSync(root).isDirectory())
4095
+ return;
4096
+ const glob = new Bun.Glob("**/*");
4097
+ for await (const filePath of glob.scan({ cwd: root, absolute: true })) {
4098
+ if (!await shouldPrecompress(filePath))
4099
+ continue;
4100
+ const bytes = await Bun.file(filePath).bytes();
4101
+ const gz = Bun.gzipSync(toArrayBuffer(bytes));
4102
+ await Bun.write(`${filePath}.gz`, gz);
4103
+ result.files += 1;
4104
+ result.inputBytes += bytes.byteLength;
4105
+ result.outputBytes += gz.byteLength;
4172
4106
  }
4173
4107
  }
4174
- function isSourceFile(filePath) {
4175
- return SOURCE_EXTS4.includes(path23.extname(filePath));
4108
+ async function shouldPrecompress(filePath) {
4109
+ if (filePath.endsWith(".gz"))
4110
+ return false;
4111
+ if (!COMPRESSIBLE_EXTS.has(path21.extname(filePath).toLowerCase()))
4112
+ return false;
4113
+ const file = Bun.file(filePath);
4114
+ if (!await file.exists())
4115
+ return false;
4116
+ return file.size >= MIN_COMPRESS_BYTES;
4176
4117
  }
4177
- function isIgnoredNodeModuleSource(filePath) {
4178
- return NODE_MODULES_RE3.test(filePath) && !AKANJS_NODE_MODULE_RE3.test(filePath);
4118
+ function toArrayBuffer(bytes) {
4119
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
4179
4120
  }
4180
- function getPageKeyBasePath(pageKey, basePaths) {
4181
- const normalized = pageKey.split(path23.sep).join("/").replace(/^\.\//, "");
4182
- const segments = normalized.split("/");
4183
- const firstPublicSegment = segments.find((segment) => segment !== "[lang]" && !/^\(.+\)$/.test(segment));
4184
- return firstPublicSegment && basePaths.includes(firstPublicSegment) ? firstPublicSegment : null;
4121
+ function formatBytes(bytes) {
4122
+ if (bytes < 1024)
4123
+ return `${bytes}B`;
4124
+ if (bytes < 1024 * 1024)
4125
+ return `${(bytes / 1024).toFixed(1)}KB`;
4126
+ return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
4185
4127
  }
4186
4128
 
4187
4129
  // pkgs/@akanjs/devkit/frontendBuild/ssrBaseArtifactBuilder.ts
4130
+ import path22 from "path";
4131
+ import { optimize } from "@tailwindcss/node";
4188
4132
  function prepareCssAsset(command, basePath, cssText) {
4189
4133
  return optimize(cssText, { file: `${basePath || "root"}.css`, minify: command === "build" }).code;
4190
4134
  }
@@ -4199,7 +4143,7 @@ class SsrBaseArtifactBuilder {
4199
4143
  this.#app = app;
4200
4144
  this.#command = command;
4201
4145
  this.#artifactDir = `${command === "build" ? app.dist.cwdPath : app.cwdPath}/.akan/artifact`;
4202
- this.#absArtifactDir = path24.resolve(this.#artifactDir);
4146
+ this.#absArtifactDir = path22.resolve(this.#artifactDir);
4203
4147
  }
4204
4148
  async build() {
4205
4149
  const akanConfig = await this.#app.getConfig();
@@ -4221,7 +4165,7 @@ class SsrBaseArtifactBuilder {
4221
4165
  rscRuntimeSsrManifest,
4222
4166
  vendorMap,
4223
4167
  cssAssets,
4224
- pagesBundlePath: this.#command === "build" ? path24.relative(this.#absArtifactDir, pagesBundle.bundlePath) : pagesBundle.bundlePath,
4168
+ pagesBundlePath: this.#command === "build" ? path22.relative(this.#absArtifactDir, pagesBundle.bundlePath) : pagesBundle.bundlePath,
4225
4169
  pagesBundleBuildId: pagesBundle.buildId,
4226
4170
  domains: [...akanConfig.domains],
4227
4171
  subRoutes: Object.fromEntries(Array.from(akanConfig.subRoutes.entries()).map(([basePath, domains]) => [basePath, [...domains]])),
@@ -4237,18 +4181,18 @@ class SsrBaseArtifactBuilder {
4237
4181
  androidSha256CertFingerprints: target.deepLinks?.android?.sha256CertFingerprints
4238
4182
  }))
4239
4183
  };
4240
- await Bun.write(path24.join(this.#absArtifactDir, "base-artifact.json"), `${JSON.stringify(artifact, null, 2)}
4184
+ await Bun.write(path22.join(this.#absArtifactDir, "base-artifact.json"), `${JSON.stringify(artifact, null, 2)}
4241
4185
  `);
4242
4186
  this.#app.verbose(`[base-artifact] complete in ${Date.now() - this.#started}ms`);
4243
4187
  return { artifact, seedIndex, cssCompiler, optimizedFonts };
4244
4188
  }
4245
4189
  async#buildRuntimeClientEntries() {
4246
4190
  const akanServerPath = await this.#resolveAkanServerPath();
4247
- const rscClientEntry = path24.resolve(akanServerPath, "rscClient.tsx");
4248
- const rscSegmentOutletEntry = path24.resolve(akanServerPath, "rscSegmentOutlet.tsx");
4191
+ const rscClientEntry = path22.resolve(akanServerPath, "rscClient.tsx");
4192
+ const rscSegmentOutletEntry = path22.resolve(akanServerPath, "rscSegmentOutlet.tsx");
4249
4193
  const vendorEntries = VENDOR_SPECIFIERS.map((specifier) => ({
4250
4194
  specifier,
4251
- absPath: path24.resolve(akanServerPath, "vendor", `${specifier.replaceAll("/", "-").replaceAll(".", "-")}.ts`)
4195
+ absPath: path22.resolve(akanServerPath, "vendor", `${specifier.replaceAll("/", "-").replaceAll(".", "-")}.ts`)
4252
4196
  }));
4253
4197
  const entries = [rscClientEntry, rscSegmentOutletEntry, ...vendorEntries.map((v) => v.absPath)];
4254
4198
  const clientBundle = await new ClientEntriesBundler({ app: this.#app, entries, command: this.#command }).bundle();
@@ -4285,15 +4229,15 @@ class SsrBaseArtifactBuilder {
4285
4229
  async#resolveAkanServerPath() {
4286
4230
  const candidates = [];
4287
4231
  try {
4288
- candidates.push(path24.dirname(Bun.resolveSync("akanjs/server", this.#app.workspace.workspaceRoot)));
4232
+ candidates.push(path22.dirname(Bun.resolveSync("akanjs/server", this.#app.workspace.workspaceRoot)));
4289
4233
  } catch {}
4290
- candidates.push(path24.join(this.#app.workspace.workspaceRoot, "pkgs/akanjs/server"), path24.join(this.#app.workspace.workspaceRoot, "node_modules/akanjs/server"));
4234
+ candidates.push(path22.join(this.#app.workspace.workspaceRoot, "pkgs/akanjs/server"), path22.join(this.#app.workspace.workspaceRoot, "node_modules/akanjs/server"));
4291
4235
  try {
4292
- candidates.push(path24.dirname(Bun.resolveSync("akanjs/server", path24.dirname(Bun.main))));
4236
+ candidates.push(path22.dirname(Bun.resolveSync("akanjs/server", path22.dirname(Bun.main))));
4293
4237
  } catch {}
4294
- candidates.push(path24.join(path24.dirname(Bun.main), "node_modules/akanjs/server"), path24.join(path24.dirname(Bun.main), "../../akanjs/server"), path24.resolve(import.meta.dir, "../../server"), path24.resolve(import.meta.dir, "../server"));
4238
+ candidates.push(path22.join(path22.dirname(Bun.main), "node_modules/akanjs/server"), path22.join(path22.dirname(Bun.main), "../../akanjs/server"), path22.resolve(import.meta.dir, "../../server"), path22.resolve(import.meta.dir, "../server"));
4295
4239
  for (const candidate of candidates) {
4296
- if (await Bun.file(path24.join(candidate, "rscClient.tsx")).exists())
4240
+ if (await Bun.file(path22.join(candidate, "rscClient.tsx")).exists())
4297
4241
  return candidate;
4298
4242
  }
4299
4243
  throw new Error(`[base-artifact] failed to locate akanjs/server; looked in: ${candidates.join(", ")}`);
@@ -4322,38 +4266,9 @@ ${preparedCssText}`).toString(36);
4322
4266
  `styles/${cssAssetName}-${cssHash}.css`,
4323
4267
  `/_akan/styles/${cssAssetName}-${cssHash}.css`
4324
4268
  ];
4325
- await Bun.write(path24.join(this.#absArtifactDir, cssRelPath), preparedCssText);
4269
+ await Bun.write(path22.join(this.#absArtifactDir, cssRelPath), preparedCssText);
4326
4270
  this.#app.verbose(`[base-artifact] wrote ${preparedCssText.length} bytes of CSS for ${basePath} -> ${cssRelPath}`);
4327
4271
  return [basePath, { cssUrl, cssRelPath }];
4328
4272
  }
4329
4273
  }
4330
-
4331
- // pkgs/@akanjs/devkit/frontendBuild/watchRootResolver.ts
4332
- import fs5 from "fs";
4333
- import path25 from "path";
4334
-
4335
- class WatchRootResolver {
4336
- #app;
4337
- constructor(app) {
4338
- this.#app = app;
4339
- }
4340
- async resolve() {
4341
- const tsconfig = await this.#app.getTsConfig();
4342
- const set = new Set;
4343
- set.add(path25.resolve(`${this.#app.cwdPath}/page`));
4344
- for (const targets of Object.values(tsconfig.compilerOptions.paths ?? {})) {
4345
- for (const target of targets) {
4346
- if (!target)
4347
- continue;
4348
- if (path25.isAbsolute(target))
4349
- continue;
4350
- const cleaned = target.replace(/\/?\*+.*$/, "").replace(/\/[^/]+\.[^/]+$/, "");
4351
- const resolved = path25.resolve(this.#app.workspace.workspaceRoot, cleaned);
4352
- if (fs5.existsSync(resolved))
4353
- set.add(resolved);
4354
- }
4355
- }
4356
- return [...set];
4357
- }
4358
- }
4359
- export { GraphClientEntryDiscovery, RouteClientBuilder, AllRoutesBuilder, AutoImportSync, CsrArtifactBuilder, DevChangePlanner, DevGeneratedIndexSync, FontOptimizer, HmrWatcher, PagesBundleBuilder, precompressArtifacts, SsrBaseArtifactBuilder, WatchRootResolver };
4274
+ export { GraphClientEntryDiscovery, RouteClientBuilder, AllRoutesBuilder, AutoImportSync, CsrArtifactBuilder, CssCompiler, DevChangePlanner, DevGeneratedIndexSync, FontOptimizer, PagesBundleBuilder, precompressArtifacts, SsrBaseArtifactBuilder };