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

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.
@@ -2636,1555 +2636,1553 @@ ${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
- `;
2911
- }
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(", ")} };`;
2936
- }
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");
2969
- }
2970
- async optimize() {
2971
- const fonts = await this.discoverFonts();
2972
- for (const font of fonts) {
2973
- if (!this.#isFontOptimizationEnabled(font))
2974
- 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}`);
3001
- continue;
3002
- }
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)
3026
- continue;
3027
- for (const declaration of statement.declarationList.declarations) {
3028
- if (!ts4.isIdentifier(declaration.name) || declaration.name.text !== "fonts")
3029
- 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
- }
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))
3053
- continue;
3054
- const name = this.#getPropertyName(prop.name);
3055
- if (!name)
3056
- continue;
3057
- obj[name] = this.#literalToValue(prop.initializer);
3058
- }
3059
- return obj;
3060
- }
3061
- if (ts4.isAsExpression(node) || ts4.isSatisfiesExpression(node) || ts4.isParenthesizedExpression(node)) {
3062
- return this.#literalToValue(node.expression);
3063
- }
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`;
3124
- }
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);
3131
- }
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(",")}}`;
3140
- }
3141
- return JSON.stringify(value);
3142
- }
3143
- #slugFontPart(value) {
3144
- return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "font";
3145
- }
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;
3158
- }
3159
- return null;
3160
- }
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 });
3165
- }
3166
- async#initWoff2() {
3167
- this.#woff2Ready ??= woff2.init().then(() => {
3168
- return;
3169
- });
3170
- return this.#woff2Ready;
3171
- }
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";
3184
- }
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);
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;
3190
2801
  }
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("");
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;
3205
2832
  }
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 "";
2833
+ async discoverCss({ refresh } = {}) {
2834
+ const { cssPaths } = await this.discoverCssAndSources({ refresh });
2835
+ return cssPaths;
3216
2836
  }
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());
2837
+ async discoverCssAndSources({
2838
+ refresh,
2839
+ pageKeys
2840
+ } = {}) {
2841
+ pageKeys ??= await this.#app.getPageKeys({ refresh });
2842
+ const seeds = pageKeys.map((key) => path15.resolve(this.#app.cwdPath, "page", key));
2843
+ const cssFiles = new Set;
2844
+ const sourceFiles = new Set;
2845
+ const queue = [...seeds];
2846
+ const resolvePackage = await createTsconfigPackageResolver(this.#app);
2847
+ const analyzer = new BarrelAnalyzer({ resolvePackage });
2848
+ const akanConfig = await this.#app.getConfig({ refresh });
2849
+ while (queue.length > 0) {
2850
+ const filePath = queue.shift();
2851
+ if (!filePath || sourceFiles.has(filePath) || isIgnoredNodeModuleSource(filePath))
2852
+ continue;
2853
+ sourceFiles.add(filePath);
2854
+ let content;
2855
+ try {
2856
+ content = await Bun.file(filePath).text();
2857
+ } catch {
2858
+ continue;
3226
2859
  }
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);
2860
+ let source = content;
2861
+ if (akanConfig.barrelImports.length > 0) {
2862
+ try {
2863
+ const rewritten = await rewriteBarrelImports(content, akanConfig.barrelImports, analyzer);
2864
+ if (rewritten !== null)
2865
+ source = rewritten;
2866
+ } catch {}
2867
+ }
2868
+ let imports;
2869
+ try {
2870
+ imports = this.#transpiler.scanImports(source);
2871
+ } catch {
2872
+ continue;
2873
+ }
2874
+ const importerDir = path15.dirname(filePath);
2875
+ for (const imp of imports) {
2876
+ const spec = imp.path;
2877
+ if (!spec)
2878
+ continue;
2879
+ if (spec.endsWith(".css")) {
2880
+ const cssPath = await this.#resolveCssImport(spec, importerDir);
2881
+ cssFiles.add(cssPath);
2882
+ continue;
3247
2883
  }
2884
+ if (NON_SOURCE_EXT_RE2.test(spec))
2885
+ continue;
2886
+ const resolved = await this.#resolveSourceImport(spec, importerDir, resolvePackage);
2887
+ if (!resolved || sourceFiles.has(resolved) || isIgnoredNodeModuleSource(resolved))
2888
+ continue;
2889
+ queue.push(resolved);
3248
2890
  }
3249
- FontOptimizer.#ksX1001Text = [...chars].join("");
3250
- } catch {
3251
- FontOptimizer.#ksX1001Text = FontOptimizer.#rangeTextStatic(44032, 55203);
3252
2891
  }
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
- }`;
2892
+ return { cssPaths: [...cssFiles], sourcePaths: [...sourceFiles] };
3275
2893
  }
3276
- async#buildFontaineFallbackCss(font, face, outputPath) {
3277
- if (font.adjustFontFallback === false)
3278
- return "";
3279
- const metrics = await readMetrics(outputPath).catch(() => null);
3280
- if (!metrics)
2894
+ async compileCss(cssPaths, sourcePaths) {
2895
+ if (cssPaths.length === 0)
3281
2896
  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)
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)
3292
2911
  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
- }));
2912
+ for (const s of entry.compiler.sources)
2913
+ sourceDirs.add(s.base);
3300
2914
  }
3301
- return css.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));
2923
+ }
2924
+ this.#logger.verbose(`css compiled paths=${cssPaths.length} candidates=${candidates.length} in ${Date.now() - compileStarted}ms`);
2925
+ return parts.join(`
2926
+ `);
2927
+ }
2928
+ async#loadStylesheet(id, fromBase) {
2929
+ const p = await this.#resolveCssImport(id, fromBase);
2930
+ const content = await Bun.file(p).text();
2931
+ return { path: p, base: path15.dirname(p), content };
2932
+ }
2933
+ async#resolveCssImport(id, fromBase) {
2934
+ if (id.startsWith(".") || id.startsWith("/"))
2935
+ return path15.resolve(fromBase, id);
2936
+ const resolver = await this.#getCssImportResolver();
2937
+ const resolved = await resolver.resolve(id, fromBase);
2938
+ if (resolved)
2939
+ return resolved;
2940
+ throw new Error(`[css] failed to resolve stylesheet import "${id}" from ${fromBase}`);
2941
+ }
2942
+ async#getCssImportResolver() {
2943
+ if (this.#cssImportResolver)
2944
+ return this.#cssImportResolver;
2945
+ this.#cssImportResolver = await CssImportResolver.create(this.#app);
2946
+ return this.#cssImportResolver;
3302
2947
  }
3303
- #buildRootVariableRule(font) {
3304
- return `:root { ${this.#getFontVariableName(font)}: ${this.#quote(font.name)}, ${this.#quote(this.#getFontFallbackName(font))}; }`;
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 };
3305
2952
  }
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);
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);
3317
2957
  }
3318
- return rules.join(`
3319
- `);
2958
+ const pkg = await resolvePackage(id);
2959
+ if (pkg)
2960
+ return pkg.entryFile;
2961
+ for (const resolve of [() => resolveSourceWithBun(id, fromBase), () => resolveSourceWithRequire(id, fromBase)]) {
2962
+ const resolved = await resolve();
2963
+ if (resolved)
2964
+ return resolved;
2965
+ }
2966
+ return null;
3320
2967
  }
3321
- #escapeCssClassName(value) {
3322
- return value.replace(/^-?\d|[^a-zA-Z0-9_-]/g, (part) => [...part].map((char) => `\\${char.codePointAt(0)?.toString(16)} `).join(""));
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];
3323
2986
  }
3324
- #toDeclarationEntries(declarations = []) {
3325
- return declarations.map((declaration) => [declaration.prop, declaration.value]);
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;
3326
2995
  }
3327
- #quote(value) {
3328
- return JSON.stringify(value);
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;
3329
3000
  }
3001
+ return null;
3330
3002
  }
3331
-
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
3337
- 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";
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;
3355
3009
  }
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;
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;
3369
3017
  }
3370
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
+ }
3371
3031
 
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;
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);
3389
3046
  }
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}`);
3047
+ plan({ generation, files, kinds, generatedFiles = [] }) {
3048
+ const fileList = uniqueResolved([...files, ...generatedFiles]);
3049
+ const generatedSet = new Set(generatedFiles.map((file) => path16.resolve(file)));
3050
+ const kindSet = new Set(kinds);
3051
+ const roles = new Set;
3052
+ const actions = new Set;
3053
+ const reasonByFile = {};
3054
+ for (const kind of kindSet) {
3055
+ if (kind === "css") {
3056
+ roles.add("css");
3057
+ actions.add("rebuild-css");
3058
+ }
3059
+ if (kind === "config") {
3060
+ roles.add("config");
3061
+ actions.add("restart-dev-host");
3403
3062
  }
3404
3063
  }
3064
+ for (const file of fileList) {
3065
+ const reasons = new Set;
3066
+ const fileRoles = this.#rolesForFile(file, { isGenerated: generatedSet.has(path16.resolve(file)), reasons });
3067
+ for (const role of fileRoles)
3068
+ roles.add(role);
3069
+ if (reasons.has("runtime-metadata"))
3070
+ actions.add("restart-builder");
3071
+ if (reasons.size > 0)
3072
+ reasonByFile[path16.resolve(file)] = [...reasons].sort();
3073
+ }
3074
+ if (roles.has("barrel"))
3075
+ actions.add("sync-generated");
3076
+ if (roles.has("server") || roles.has("shared"))
3077
+ actions.add("restart-backend");
3078
+ if (roles.has("client") || roles.has("shared"))
3079
+ actions.add("rebuild-client");
3080
+ if (roles.has("css"))
3081
+ actions.add("rebuild-css");
3082
+ return {
3083
+ generation,
3084
+ files: fileList,
3085
+ generatedFiles: uniqueResolved(generatedFiles),
3086
+ roles: [...roles].sort(),
3087
+ actions: [...actions].sort(),
3088
+ reasonByFile
3089
+ };
3405
3090
  }
3406
- stop() {
3407
- this.#stopped = true;
3408
- if (this.#timer)
3409
- clearTimeout(this.#timer);
3410
- for (const w of this.#watchers) {
3411
- try {
3412
- w.close();
3413
- } catch {}
3091
+ #rolesForFile(file, { isGenerated, reasons }) {
3092
+ const roles = new Set;
3093
+ const abs = path16.resolve(file);
3094
+ const base = path16.basename(abs);
3095
+ const ext = path16.extname(abs).toLowerCase();
3096
+ const isSource = SOURCE_EXTS3.has(ext);
3097
+ const rel = path16.relative(this.#workspaceRoot, abs);
3098
+ const parts = rel.split(path16.sep).filter(Boolean);
3099
+ if (CONFIG_BASENAMES.has(base)) {
3100
+ roles.add("config");
3101
+ reasons.add("config-file");
3102
+ }
3103
+ if (ext === ".css") {
3104
+ roles.add("css");
3105
+ reasons.add("css-file");
3106
+ }
3107
+ if (isGenerated || isSource && this.#isBarrelFacetChild(parts)) {
3108
+ roles.add("barrel");
3109
+ reasons.add(isGenerated ? "generated-index" : "barrel-facet-child");
3110
+ }
3111
+ if (isSource && this.#isServerBiased(abs, parts)) {
3112
+ roles.add("server");
3113
+ reasons.add("server-path");
3114
+ }
3115
+ if (isSource && this.#isClientBiased(abs, parts)) {
3116
+ roles.add("client");
3117
+ reasons.add("client-path");
3118
+ }
3119
+ if (isSource && this.#isSharedBiased(abs, parts)) {
3120
+ roles.add("shared");
3121
+ reasons.add("shared-path");
3122
+ }
3123
+ if (isSource && this.#isRuntimeMetadataFile(parts, base)) {
3124
+ reasons.add("runtime-metadata");
3125
+ }
3126
+ if (roles.has("server") && roles.has("client")) {
3127
+ roles.delete("server");
3128
+ roles.delete("client");
3129
+ roles.add("shared");
3130
+ reasons.add("server-client-overlap");
3131
+ }
3132
+ if (roles.size === 0 && SOURCE_EXTS3.has(ext) && this.#isWorkspaceSource(rel)) {
3133
+ roles.add("shared");
3134
+ reasons.add("workspace-source-fallback");
3414
3135
  }
3136
+ return roles;
3415
3137
  }
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);
3138
+ #isServerBiased(abs, parts) {
3139
+ const base = path16.basename(abs);
3140
+ return parts.includes("srvkit") || SERVER_SUFFIXES.some((suffix) => base.endsWith(suffix)) || base === "main.ts" || base === "server.ts";
3141
+ }
3142
+ #isClientBiased(abs, parts) {
3143
+ const base = path16.basename(abs);
3144
+ return parts.includes("ui") || parts.includes("webkit") || parts.includes("page") || CLIENT_SUFFIXES.some((suffix) => base.endsWith(suffix));
3426
3145
  }
3427
- #flush() {
3428
- this.#timer = null;
3429
- if (this.#stopped || this.#pending.size === 0 || this.#flushing)
3430
- return;
3431
- this.#drain();
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);
3432
3149
  }
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
- }
3445
- }
3446
- } finally {
3447
- this.#flushing = false;
3448
- if (!this.#stopped && this.#pending.size > 0)
3449
- this.#timer = setTimeout(() => this.#flush(), this.#debounceMs);
3450
- }
3150
+ #isRuntimeMetadataFile(parts, base) {
3151
+ const parent = parts.at(-2);
3152
+ if (parent === "lib" && RUNTIME_METADATA_BASENAMES.has(base))
3153
+ return true;
3154
+ const libIndex = parts.lastIndexOf("lib");
3155
+ if (libIndex < 0 || parts.length <= libIndex + 1)
3156
+ return false;
3157
+ return base.endsWith(".dictionary.ts") || base.endsWith(".signal.ts");
3158
+ }
3159
+ #isBarrelFacetChild(parts) {
3160
+ if (parts.length < 4)
3161
+ return false;
3162
+ const [scope, , facet, child] = parts;
3163
+ if (scope !== "apps" && scope !== "libs")
3164
+ return false;
3165
+ if (!facet || !BARREL_FACETS.has(facet))
3166
+ return false;
3167
+ if (!child || child.startsWith(".") || child === "index.ts" || child === "index.tsx")
3168
+ return false;
3169
+ return true;
3170
+ }
3171
+ #isWorkspaceSource(rel) {
3172
+ if (!rel || rel.startsWith("..") || path16.isAbsolute(rel))
3173
+ return false;
3174
+ const [scope] = rel.split(path16.sep);
3175
+ return scope === "apps" || scope === "libs" || scope === "pkgs";
3451
3176
  }
3452
3177
  }
3178
+ var uniqueResolved = (files) => [...new Set(files.map((file) => path16.resolve(file)))].sort();
3453
3179
 
3454
- // pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
3455
- import path20 from "path";
3180
+ // pkgs/@akanjs/devkit/frontendBuild/devGeneratedIndexSync.ts
3181
+ import { mkdir as mkdir4, readdir as readdir2, readFile as readFile2, rm as rm2, stat as stat2, writeFile as writeFile2 } from "fs/promises";
3182
+ import path17 from "path";
3183
+ var BARREL_FACETS2 = new Set(["common", "srvkit", "ui", "webkit", "plugin"]);
3184
+ var FACET_SOURCE_FILE_RE = /\.(ts|tsx)$/;
3185
+ var FACET_EXCLUDED_FILE_RE = /(^index\.tsx?$|\.d\.ts$|\.(test|spec)\.(ts|tsx)$|\.css$|\.scss$|\.sass$)/;
3186
+ var FACET_PASCAL_CASE_RE = /^[A-Z][A-Za-z0-9]*$/;
3187
+ var FACET_CAMEL_CASE_RE = /^[a-z][A-Za-z0-9]*$/;
3188
+ var MODULE_UI_TYPES = ["Template", "Unit", "Util", "View", "Zone"];
3189
+ var SERVICE_UI_TYPES = ["Util", "Zone"];
3190
+ var SCALAR_UI_TYPES = ["Template", "Unit"];
3456
3191
 
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)
3492
- 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
- }
3192
+ class DevGeneratedIndexSync {
3193
+ #workspaceRoot;
3194
+ constructor({ workspaceRoot }) {
3195
+ this.#workspaceRoot = path17.resolve(workspaceRoot);
3196
+ }
3197
+ async syncForBatch(files) {
3198
+ const indexPaths = new Set;
3199
+ const errors = [];
3200
+ for (const file of files) {
3201
+ const facetIndex = this.#facetIndexFor(file);
3202
+ if (facetIndex)
3203
+ indexPaths.add(facetIndex);
3204
+ const moduleIndex = await this.#moduleIndexForDirectoryEvent(file).catch((err) => {
3205
+ errors.push(`[generated-index] module detection failed for ${file}: ${formatError2(err)}`);
3206
+ return null;
3207
+ });
3208
+ if (moduleIndex)
3209
+ indexPaths.add(moduleIndex);
3506
3210
  }
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;
3211
+ const changedFiles = [];
3212
+ for (const indexPath of [...indexPaths].sort()) {
3213
+ try {
3214
+ const changed = await this.#syncIndex(indexPath);
3215
+ if (changed)
3216
+ changedFiles.push(indexPath);
3217
+ } catch (err) {
3218
+ errors.push(`[generated-index] sync failed for ${indexPath}: ${formatError2(err)}`);
3219
+ }
3518
3220
  }
3519
- return null;
3221
+ return { changedFiles, errors };
3520
3222
  }
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
- });
3223
+ #facetIndexFor(file) {
3224
+ const abs = path17.resolve(file);
3225
+ const rel = path17.relative(this.#workspaceRoot, abs);
3226
+ if (rel.startsWith("..") || path17.isAbsolute(rel))
3227
+ return null;
3228
+ const parts = rel.split(path17.sep).filter(Boolean);
3229
+ if (parts.length < 4)
3230
+ return null;
3231
+ const [scope, project, facet, child] = parts;
3232
+ if (scope !== "apps" && scope !== "libs" || !project || !facet || !BARREL_FACETS2.has(facet))
3233
+ return null;
3234
+ if (!child || child.startsWith(".") || child === "index.ts" || child === "index.tsx")
3235
+ return null;
3236
+ return path17.join(this.#workspaceRoot, scope, project, facet, "index.ts");
3237
+ }
3238
+ async#moduleIndexForDirectoryEvent(file) {
3239
+ const abs = path17.resolve(file);
3240
+ const rel = path17.relative(this.#workspaceRoot, abs);
3241
+ if (rel.startsWith("..") || path17.isAbsolute(rel))
3242
+ return null;
3243
+ const parts = rel.split(path17.sep).filter(Boolean);
3244
+ if (parts.length !== 4 && parts.length !== 5)
3245
+ return null;
3246
+ const [scope, project, libSegment, moduleSegment, scalarSegment] = parts;
3247
+ if (scope !== "apps" && scope !== "libs" || !project || libSegment !== "lib")
3248
+ return null;
3249
+ const isExistingDirectory = await stat2(abs).then((s) => s.isDirectory()).catch(() => false);
3250
+ const looksLikeDeletedDirectory = !path17.extname(abs);
3251
+ if (!isExistingDirectory && !looksLikeDeletedDirectory)
3252
+ return null;
3253
+ if (moduleSegment === "__scalar" && scalarSegment) {
3254
+ return path17.join(this.#workspaceRoot, scope, project, "lib", "__scalar", scalarSegment, "index.ts");
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 } from "fs/promises";
3337
+ import path18 from "path";
3338
+ import {
3339
+ generateFontFace,
3340
+ getMetricsForFamily,
3341
+ readMetrics,
3342
+ resolveCategoryFallbacks
3343
+ } from "fontaine";
3344
+ import { createFont, woff2 } from "fonteditor-core";
3345
+ import subsetFont from "subset-font";
3346
+ import ts4 from "typescript";
3347
+ var FONT_URL_PREFIX = "/_akan/fonts";
3348
+ var DEFAULT_FONT_SUBSETS = ["latin"];
3608
3349
 
3609
- class PagesBundleBuilder {
3350
+ class FontOptimizer {
3610
3351
  #app;
3611
3352
  #command;
3612
- #pageEntries;
3613
- #started = Date.now();
3614
- constructor(app, command = "start", pageEntries) {
3353
+ #artifactRoot;
3354
+ #files = [];
3355
+ #cssParts = [];
3356
+ #woff2Ready = null;
3357
+ static #ksX1001Text = null;
3358
+ constructor(app, command = "start") {
3615
3359
  this.#app = app;
3616
3360
  this.#command = command;
3617
- this.#pageEntries = pageEntries;
3361
+ this.#artifactRoot = path18.join(command === "build" ? app.dist.cwdPath : app.cwdPath, ".akan/artifact");
3618
3362
  }
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
3668
- };
3363
+ async optimize() {
3364
+ const fonts = await this.discoverFonts();
3365
+ for (const font of fonts) {
3366
+ if (!this.#isFontOptimizationEnabled(font))
3367
+ continue;
3368
+ await this.#optimizeFont(font);
3369
+ }
3370
+ const fontUtilityCss = this.#buildFontUtilityRules(fonts);
3371
+ if (fontUtilityCss)
3372
+ this.#cssParts.push(fontUtilityCss);
3373
+ return { css: this.#cssParts.join(`
3374
+ `), fonts, files: this.#files };
3669
3375
  }
3670
- get #artifactDir() {
3671
- return `${this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath}/.akan/artifact`;
3376
+ async discoverFonts() {
3377
+ const pageKeys = await this.#app.getPageKeys();
3378
+ const fonts = [];
3379
+ await Promise.all(pageKeys.map(async (key) => {
3380
+ const filePath = path18.resolve(this.#app.cwdPath, "page", key);
3381
+ const file = Bun.file(filePath);
3382
+ if (!await file.exists())
3383
+ return;
3384
+ fonts.push(...this.#extractFontsExport(await file.text(), filePath));
3385
+ }));
3386
+ return this.#dedupeFonts(fonts);
3672
3387
  }
3673
- get #splitting() {
3674
- return process.env.AKAN_SERVER_PAGES_SPLITTING === "1";
3388
+ async#optimizeFont(font) {
3389
+ const faceCss = [];
3390
+ for (const face of this.#getFontFaces(font)) {
3391
+ const sourcePath = await this.#resolveFontSourcePath(face.src);
3392
+ if (!sourcePath) {
3393
+ this.#app.logger.warn(`[font] source not found: ${face.src}`);
3394
+ continue;
3395
+ }
3396
+ const outputPath = path18.join(this.#artifactRoot, face.optimizedSrc.replace(/^\/_akan\//, ""));
3397
+ await mkdir5(path18.dirname(outputPath), { recursive: true });
3398
+ const sourceBuffer = Buffer.from(await Bun.file(sourcePath).arrayBuffer());
3399
+ const outputBuffer = font.subset === false ? await this.#convertToWoff2(sourceBuffer, sourcePath) : await subsetFont(sourceBuffer, await this.#getSubsetText(font), { targetFormat: "woff2" });
3400
+ await Bun.write(outputPath, outputBuffer);
3401
+ this.#files.push(outputPath);
3402
+ faceCss.push(this.#buildOptimizedFontFaceRule(font, face));
3403
+ const fallbackCss = await this.#buildFontaineFallbackCss(font, face, outputPath);
3404
+ if (fallbackCss)
3405
+ faceCss.push(fallbackCss);
3406
+ }
3407
+ if (faceCss.length > 0)
3408
+ this.#cssParts.push(...faceCss, this.#buildRootVariableRule(font));
3675
3409
  }
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
- };
3410
+ #extractFontsExport(source, filePath) {
3411
+ const sourceFile = ts4.createSourceFile(filePath, source, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TSX);
3412
+ const fonts = [];
3413
+ for (const statement of sourceFile.statements) {
3414
+ if (!ts4.isVariableStatement(statement))
3415
+ continue;
3416
+ const modifiers = ts4.canHaveModifiers(statement) ? ts4.getModifiers(statement) : undefined;
3417
+ const isExported = modifiers?.some((modifier) => modifier.kind === ts4.SyntaxKind.ExportKeyword) ?? false;
3418
+ if (!isExported)
3419
+ continue;
3420
+ for (const declaration of statement.declarationList.declarations) {
3421
+ if (!ts4.isIdentifier(declaration.name) || declaration.name.text !== "fonts")
3422
+ continue;
3423
+ const value = declaration.initializer ? this.#literalToValue(declaration.initializer) : null;
3424
+ if (Array.isArray(value)) {
3425
+ fonts.push(...value.map((font) => this.#withFontDefaults(font)));
3426
+ }
3427
+ }
3428
+ }
3429
+ return fonts;
3683
3430
  }
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
- }));
3431
+ #literalToValue(node) {
3432
+ if (ts4.isStringLiteralLike(node))
3433
+ return node.text;
3434
+ if (ts4.isNumericLiteral(node))
3435
+ return Number(node.text);
3436
+ if (node.kind === ts4.SyntaxKind.TrueKeyword)
3437
+ return true;
3438
+ if (node.kind === ts4.SyntaxKind.FalseKeyword)
3439
+ return false;
3440
+ if (ts4.isArrayLiteralExpression(node))
3441
+ return node.elements.map((element) => this.#literalToValue(element));
3442
+ if (ts4.isObjectLiteralExpression(node)) {
3443
+ const obj = {};
3444
+ for (const prop of node.properties) {
3445
+ if (!ts4.isPropertyAssignment(prop))
3446
+ continue;
3447
+ const name = this.#getPropertyName(prop.name);
3448
+ if (!name)
3449
+ continue;
3450
+ obj[name] = this.#literalToValue(prop.initializer);
3696
3451
  }
3697
- };
3452
+ return obj;
3453
+ }
3454
+ if (ts4.isAsExpression(node) || ts4.isSatisfiesExpression(node) || ts4.isParenthesizedExpression(node)) {
3455
+ return this.#literalToValue(node.expression);
3456
+ }
3457
+ return;
3458
+ }
3459
+ #getPropertyName(name) {
3460
+ if (ts4.isIdentifier(name) || ts4.isStringLiteral(name) || ts4.isNumericLiteral(name))
3461
+ return name.text;
3462
+ return null;
3463
+ }
3464
+ #dedupeFonts(fonts) {
3465
+ const map = new Map;
3466
+ for (const font of fonts)
3467
+ map.set(JSON.stringify(font), font);
3468
+ return [...map.values()];
3469
+ }
3470
+ #withFontDefaults(font) {
3471
+ return { ...font, subsets: font.subsets ?? [...DEFAULT_FONT_SUBSETS] };
3698
3472
  }
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
- };
3473
+ #getFontSubsets(font) {
3474
+ return font.subsets ?? DEFAULT_FONT_SUBSETS;
3709
3475
  }
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
- };
3476
+ #getFontVariableName(font) {
3477
+ return font.variable ?? `--font-${font.name}`;
3723
3478
  }
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 });");
3479
+ #getFontFallbackName(font) {
3480
+ return font.fallbackName ?? `${font.name} fallback`;
3728
3481
  }
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)})`);
3482
+ #isFontOptimizationEnabled(font) {
3483
+ return font.optimize !== false;
3751
3484
  }
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;
3485
+ #getFontStyles(font) {
3486
+ return font.styles?.length ? font.styles : ["normal"];
3767
3487
  }
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);
3488
+ #getFontFaces(font) {
3489
+ const enabledStyles = new Set(this.#getFontStyles(font));
3490
+ return font.paths.map((fontPath) => {
3491
+ const style = fontPath.style ?? "normal";
3492
+ return {
3493
+ font,
3494
+ path: fontPath,
3495
+ src: fontPath.src,
3496
+ weight: fontPath.weight,
3497
+ style,
3498
+ optimizedSrc: this.#getOptimizedFontSrc(font, fontPath)
3499
+ };
3500
+ }).filter((face) => enabledStyles.has(face.style));
3811
3501
  }
3812
- static async create(app) {
3813
- const tsconfig = await app.getTsConfig();
3814
- return new CssImportResolver(app.workspace.workspaceRoot, tsconfig.compilerOptions.paths ?? {});
3502
+ #getOptimizedFontSrc(font, fontPath) {
3503
+ const style = fontPath.style ?? "normal";
3504
+ const hash = this.#hashFontConfig({
3505
+ name: font.name,
3506
+ src: fontPath.src,
3507
+ weight: fontPath.weight,
3508
+ style,
3509
+ display: font.display,
3510
+ subset: font.subset,
3511
+ subsets: this.#getFontSubsets(font),
3512
+ subsetText: font.subsetText,
3513
+ subsetFiles: font.subsetFiles,
3514
+ declarations: [...font.declarations ?? [], ...fontPath.declarations ?? []]
3515
+ });
3516
+ return `${FONT_URL_PREFIX}/${this.#slugFontPart(font.name)}-${this.#slugFontPart(String(fontPath.weight))}-${style}-${hash}.woff2`;
3815
3517
  }
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;
3518
+ #hashFontConfig(value) {
3519
+ const input = this.#stableStringify(value);
3520
+ let hash = 2166136261;
3521
+ for (let i = 0;i < input.length; i++) {
3522
+ hash ^= input.charCodeAt(i);
3523
+ hash = Math.imul(hash, 16777619);
3826
3524
  }
3827
- return null;
3525
+ return (hash >>> 0).toString(36);
3828
3526
  }
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 {}
3527
+ #stableStringify(value) {
3528
+ if (Array.isArray(value))
3529
+ return `[${value.map((item) => this.#stableStringify(item)).join(",")}]`;
3530
+ if (value && typeof value === "object") {
3531
+ const entries = Object.entries(value).filter(([, v]) => v !== undefined).sort(([a], [b]) => a.localeCompare(b));
3532
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${this.#stableStringify(v)}`).join(",")}}`;
3836
3533
  }
3837
- return null;
3534
+ return JSON.stringify(value);
3838
3535
  }
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 {}
3536
+ #slugFontPart(value) {
3537
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "font";
3538
+ }
3539
+ async#resolveFontSourcePath(src) {
3540
+ if (!src.startsWith("/"))
3541
+ return null;
3542
+ const rel = src.replace(/^\//, "");
3543
+ const candidates = [
3544
+ this.#command === "build" ? path18.join(this.#app.dist.cwdPath, "public", rel) : null,
3545
+ path18.join(this.#app.cwdPath, "public", rel),
3546
+ this.#resolveWorkspacePublicPath(rel)
3547
+ ].filter(Boolean);
3548
+ for (const candidate of candidates) {
3549
+ if (await Bun.file(candidate).exists())
3550
+ return candidate;
3846
3551
  }
3847
3552
  return null;
3848
3553
  }
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;
3856
- }
3554
+ async#convertToWoff2(buffer, sourcePath) {
3555
+ await this.#initWoff2();
3556
+ const font = createFont(buffer, { type: this.#getFontType(sourcePath, buffer) });
3557
+ return font.write({ type: "woff2", toBuffer: true });
3558
+ }
3559
+ async#initWoff2() {
3560
+ this.#woff2Ready ??= woff2.init().then(() => {
3561
+ return;
3562
+ });
3563
+ return this.#woff2Ready;
3564
+ }
3565
+ #getFontType(sourcePath, buffer) {
3566
+ const signature = buffer.toString("ascii", 0, 4);
3567
+ if (signature === "wOFF")
3568
+ return "woff";
3569
+ if (signature === "wOF2")
3570
+ return "woff2";
3571
+ if (signature === "OTTO")
3572
+ return "otf";
3573
+ const ext = path18.extname(sourcePath).slice(1).toLowerCase();
3574
+ if (ext === "otf" || ext === "woff" || ext === "woff2")
3575
+ return ext;
3576
+ return "ttf";
3577
+ }
3578
+ #resolveWorkspacePublicPath(rel) {
3579
+ const [root, dep, ...rest] = rel.split("/");
3580
+ if (root !== "libs" || !dep || rest.length === 0)
3581
+ return null;
3582
+ return path18.join(this.#app.workspace.workspaceRoot, "libs", dep, "public", ...rest);
3583
+ }
3584
+ async#getSubsetText(font) {
3585
+ const parts = new Set;
3586
+ const subsets = this.#getFontSubsets(font);
3587
+ for (const subset of subsets)
3588
+ parts.add(await this.#getSubsetPresetText(subset));
3589
+ if (font.subsetText)
3590
+ parts.add(font.subsetText);
3591
+ for (const filePath of font.subsetFiles ?? []) {
3592
+ const abs = path18.isAbsolute(filePath) ? filePath : path18.join(this.#app.cwdPath, filePath);
3593
+ const file = Bun.file(abs);
3594
+ if (await file.exists())
3595
+ parts.add(await file.text());
3857
3596
  }
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;
3597
+ return [...parts].join("");
3598
+ }
3599
+ async#getSubsetPresetText(subset) {
3600
+ if (subset === "latin")
3601
+ return this.#rangeText(32, 126);
3602
+ if (subset === "latin-ext")
3603
+ return `${this.#rangeText(32, 126)}${this.#rangeText(160, 591)}`;
3604
+ if (subset === "ks-x-1001")
3605
+ return FontOptimizer.#getKsX1001Text();
3606
+ if (subset === "auto")
3607
+ return this.#collectAutoSubsetText();
3608
+ return "";
3609
+ }
3610
+ async#collectAutoSubsetText() {
3611
+ const roots = ["page", "ui"].map((dir) => path18.join(this.#app.cwdPath, dir));
3612
+ const glob = new Bun.Glob("**/*.{ts,tsx,js,jsx,html,md}");
3613
+ const parts = [];
3614
+ await Promise.all(roots.map(async (root) => {
3615
+ if (!await Bun.file(root).exists())
3616
+ return;
3617
+ for await (const filePath of glob.scan({ cwd: root, absolute: true })) {
3618
+ parts.push(await Bun.file(filePath).text());
3867
3619
  }
3868
- }
3869
- return null;
3620
+ }));
3621
+ return parts.join("");
3870
3622
  }
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 {}
3882
- }
3883
- for (const pkgPath of this.#packageJsonCandidates(pkgName)) {
3884
- const resolved = await this.#resolvePackageStyleFromPackageJson(id, pkgName, pkgPath);
3885
- if (resolved)
3886
- return resolved;
3887
- }
3888
- return null;
3623
+ #rangeText(start, end) {
3624
+ let text = "";
3625
+ for (let code = start;code <= end; code++)
3626
+ text += String.fromCodePoint(code);
3627
+ return text;
3889
3628
  }
3890
- async#resolvePackageStyleFromPackageJson(id, pkgName, pkgPath) {
3629
+ static #getKsX1001Text() {
3630
+ if (FontOptimizer.#ksX1001Text)
3631
+ return FontOptimizer.#ksX1001Text;
3891
3632
  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));
3633
+ const decoder = new TextDecoder("euc-kr");
3634
+ const chars = new Set;
3635
+ for (let lead = 161;lead <= 254; lead++) {
3636
+ for (let trail = 161;trail <= 254; trail++) {
3637
+ const char = decoder.decode(Uint8Array.of(lead, trail));
3638
+ if (char && char !== "\uFFFD")
3639
+ chars.add(char);
3640
+ }
3641
+ }
3642
+ FontOptimizer.#ksX1001Text = [...chars].join("");
3900
3643
  } catch {
3901
- return null;
3644
+ FontOptimizer.#ksX1001Text = FontOptimizer.#rangeTextStatic(44032, 55203);
3902
3645
  }
3646
+ return FontOptimizer.#ksX1001Text;
3903
3647
  }
3904
- #resolutionBases(fromBase) {
3905
- return [fromBase, this.#workspaceRoot, path22.dirname(Bun.main), path22.resolve(path22.dirname(Bun.main), "../..")];
3648
+ static #rangeTextStatic(start, end) {
3649
+ let text = "";
3650
+ for (let code = start;code <= end; code++)
3651
+ text += String.fromCodePoint(code);
3652
+ return text;
3906
3653
  }
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")
3654
+ #buildOptimizedFontFaceRule(font, face) {
3655
+ const declarations = [
3656
+ ["font-family", this.#quote(font.name)],
3657
+ ["src", `url(${this.#quote(face.optimizedSrc)}) format("woff2")`],
3658
+ ["font-weight", String(face.weight)],
3659
+ ["font-style", face.style],
3660
+ ["font-display", font.display ?? "swap"],
3661
+ ...this.#toDeclarationEntries(font.declarations),
3662
+ ...this.#toDeclarationEntries(face.path.declarations)
3913
3663
  ];
3664
+ return `@font-face {
3665
+ ${declarations.map(([prop, value]) => ` ${prop}: ${value};`).join(`
3666
+ `)}
3667
+ }`;
3914
3668
  }
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;
3669
+ async#buildFontaineFallbackCss(font, face, outputPath) {
3670
+ if (font.adjustFontFallback === false)
3671
+ return "";
3672
+ const metrics = await readMetrics(outputPath).catch(() => null);
3673
+ if (!metrics)
3674
+ return "";
3675
+ const fallbacks = resolveCategoryFallbacks({
3676
+ fontFamily: font.name,
3677
+ fallbacks: font.fallbacks ?? {},
3678
+ metrics: { ...metrics, category: font.category }
3679
+ });
3680
+ const css = [];
3681
+ for (let i = fallbacks.length - 1;i >= 0; i--) {
3682
+ const fallback = fallbacks[i];
3683
+ const fallbackMetrics = await getMetricsForFamily(fallback);
3684
+ if (!fallbackMetrics)
3685
+ continue;
3686
+ css.push(generateFontFace({ ...metrics, category: font.category }, {
3687
+ name: this.#getFontFallbackName(font),
3688
+ font: fallback,
3689
+ metrics: fallbackMetrics,
3690
+ "font-weight": String(face.weight),
3691
+ "font-style": face.style
3692
+ }));
3920
3693
  }
3921
- return null;
3694
+ return css.join("");
3922
3695
  }
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;
3696
+ #buildRootVariableRule(font) {
3697
+ return `:root { ${this.#getFontVariableName(font)}: ${this.#quote(font.name)}, ${this.#quote(this.#getFontFallbackName(font))}; }`;
3928
3698
  }
3929
- static isCssFile(filePath) {
3930
- return path22.extname(filePath) === ".css";
3699
+ #buildFontUtilityRules(fonts) {
3700
+ const rules = [];
3701
+ const seen = new Set;
3702
+ for (const font of fonts) {
3703
+ const className = font.className || `font-${font.name}`;
3704
+ const selector = `.${this.#escapeCssClassName(className)}`;
3705
+ const rule = `${selector} { font-family: var(${this.#getFontVariableName(font)}); }`;
3706
+ if (seen.has(rule))
3707
+ continue;
3708
+ seen.add(rule);
3709
+ rules.push(rule);
3710
+ }
3711
+ return rules.join(`
3712
+ `);
3713
+ }
3714
+ #escapeCssClassName(value) {
3715
+ return value.replace(/^-?\d|[^a-zA-Z0-9_-]/g, (part) => [...part].map((char) => `\\${char.codePointAt(0)?.toString(16)} `).join(""));
3716
+ }
3717
+ #toDeclarationEntries(declarations = []) {
3718
+ return declarations.map((declaration) => [declaration.prop, declaration.value]);
3719
+ }
3720
+ #quote(value) {
3721
+ return JSON.stringify(value);
3931
3722
  }
3932
3723
  }
3933
3724
 
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[\\/]/;
3725
+ // pkgs/@akanjs/devkit/frontendBuild/hmrWatcher.ts
3726
+ import fs3 from "fs";
3727
+ import path20 from "path";
3939
3728
 
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;
3729
+ // pkgs/@akanjs/devkit/frontendBuild/hmrChangeClassifier.ts
3730
+ import path19 from "path";
3731
+ var SOURCE_EXTS4 = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
3732
+ var CSS_EXTS = new Set([".css"]);
3733
+ var CONFIG_BASENAMES2 = new Set(["akan.config.ts", "bunfig.toml", "tsconfig.json", "package.json"]);
3734
+
3735
+ class HmrChangeClassifier {
3736
+ classify(abs) {
3737
+ if (this.#isUninteresting(abs))
3738
+ return "ignore";
3739
+ const base = path19.basename(abs);
3740
+ if (CONFIG_BASENAMES2.has(base))
3741
+ return "config";
3742
+ const ext = path19.extname(abs).toLowerCase();
3743
+ if (CSS_EXTS.has(ext))
3744
+ return "css";
3745
+ if (SOURCE_EXTS4.has(ext))
3746
+ return "code";
3747
+ return "ignore";
3987
3748
  }
3988
- async discoverCss({ refresh } = {}) {
3989
- const { cssPaths } = await this.discoverCssAndSources({ refresh });
3990
- return cssPaths;
3749
+ #isUninteresting(abs) {
3750
+ const base = path19.basename(abs);
3751
+ if (!base)
3752
+ return true;
3753
+ if (base.startsWith("."))
3754
+ return true;
3755
+ if (base.endsWith("~") || base.endsWith(".swp") || base.endsWith(".swx") || base.endsWith(".tmp"))
3756
+ return true;
3757
+ if (abs.includes(`${path19.sep}node_modules${path19.sep}`))
3758
+ return true;
3759
+ if (abs.includes(`${path19.sep}.akan${path19.sep}`))
3760
+ return true;
3761
+ return false;
3991
3762
  }
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;
3763
+ }
3764
+
3765
+ // pkgs/@akanjs/devkit/frontendBuild/hmrWatcher.ts
3766
+ class HmrWatcher {
3767
+ #roots;
3768
+ #debounceMs;
3769
+ #onBatch;
3770
+ #logger;
3771
+ #watchers = [];
3772
+ #pending = new Map;
3773
+ #classifier = new HmrChangeClassifier;
3774
+ #timer = null;
3775
+ #stopped = false;
3776
+ #flushing = false;
3777
+ constructor(opts) {
3778
+ this.#roots = [...new Set(opts.roots.map((r) => path20.resolve(r)))];
3779
+ this.#debounceMs = opts.debounceMs ?? 80;
3780
+ this.#onBatch = opts.onBatch;
3781
+ this.#logger = opts.logger;
3782
+ }
3783
+ start() {
3784
+ for (const root of this.#roots) {
4010
3785
  try {
4011
- content = await Bun.file(filePath).text();
4012
- } catch {
4013
- continue;
3786
+ const w = fs3.watch(root, { recursive: true, persistent: false }, (_event, filename) => {
3787
+ if (!filename)
3788
+ return;
3789
+ const abs = path20.resolve(root, filename.toString());
3790
+ this.#queue(abs);
3791
+ });
3792
+ this.#watchers.push(w);
3793
+ this.#logger.verbose(`[hmr] watching ${root}`);
3794
+ } catch (err) {
3795
+ this.#logger.error(`[hmr] failed to watch ${root}: ${err.message}`);
4014
3796
  }
4015
- let source = content;
4016
- if (akanConfig.barrelImports.length > 0) {
3797
+ }
3798
+ }
3799
+ stop() {
3800
+ this.#stopped = true;
3801
+ if (this.#timer)
3802
+ clearTimeout(this.#timer);
3803
+ for (const w of this.#watchers) {
3804
+ try {
3805
+ w.close();
3806
+ } catch {}
3807
+ }
3808
+ }
3809
+ #queue(abs) {
3810
+ const kind = this.#classifier.classify(abs);
3811
+ if (kind === "ignore")
3812
+ return;
3813
+ this.#pending.set(abs, kind);
3814
+ if (this.#flushing)
3815
+ return;
3816
+ if (this.#timer)
3817
+ clearTimeout(this.#timer);
3818
+ this.#timer = setTimeout(() => this.#flush(), this.#debounceMs);
3819
+ }
3820
+ #flush() {
3821
+ this.#timer = null;
3822
+ if (this.#stopped || this.#pending.size === 0 || this.#flushing)
3823
+ return;
3824
+ this.#drain();
3825
+ }
3826
+ async#drain() {
3827
+ this.#flushing = true;
3828
+ try {
3829
+ while (!this.#stopped && this.#pending.size > 0) {
3830
+ const files = Array.from(this.#pending.keys());
3831
+ const kinds = new Set(this.#pending.values());
3832
+ this.#pending.clear();
4017
3833
  try {
4018
- const rewritten = await rewriteBarrelImports(content, akanConfig.barrelImports, analyzer);
4019
- if (rewritten !== null)
4020
- source = rewritten;
4021
- } catch {}
3834
+ await this.#onBatch({ files, kinds });
3835
+ } catch (e) {
3836
+ this.#logger.error(`[hmr] onBatch error: ${e.message}`);
3837
+ }
4022
3838
  }
4023
- let imports;
4024
- try {
4025
- imports = this.#transpiler.scanImports(source);
4026
- } catch {
3839
+ } finally {
3840
+ this.#flushing = false;
3841
+ if (!this.#stopped && this.#pending.size > 0)
3842
+ this.#timer = setTimeout(() => this.#flush(), this.#debounceMs);
3843
+ }
3844
+ }
3845
+ }
3846
+
3847
+ // pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
3848
+ import path22 from "path";
3849
+
3850
+ // pkgs/@akanjs/devkit/transforms/externalizeFrameworkPlugin.ts
3851
+ import path21 from "path";
3852
+ var DEFAULT_INCLUDE = ["akanjs/", "@apps/", "@libs/"];
3853
+ var DEFAULT_EXCLUDE_EXACT = new Set(["akanjs/webkit", "@akanjs/cli", "@akanjs/devkit"]);
3854
+ var DEFAULT_EXCLUDE_PREFIX = ["@akanjs/cli/", "@akanjs/devkit/"];
3855
+ var OPTIONAL_BACKEND_EXTERNAL_EXACT = new Set([
3856
+ "@libsql/client",
3857
+ "bullmq",
3858
+ "croner",
3859
+ "ioredis",
3860
+ "postgres",
3861
+ "protobufjs"
3862
+ ]);
3863
+ var RUNTIME_EXTERNAL_EXACT = new Set([
3864
+ "react",
3865
+ "react-dom",
3866
+ "react/jsx-runtime",
3867
+ "react/jsx-dev-runtime",
3868
+ "react-server-dom-webpack",
3869
+ "react-server-dom-webpack/server.node",
3870
+ "react-server-dom-webpack/client.node",
3871
+ "react-server-dom-webpack/client.browser"
3872
+ ]);
3873
+ var RUNTIME_EXTERNAL_PREFIX = ["react-dom/", "react-server-dom-webpack/"];
3874
+ var CANDIDATE_EXTS3 = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
3875
+ async function createExternalizeFrameworkPlugin(options) {
3876
+ const tsconfig = await options.app.getTsConfig();
3877
+ const includePrefixes = options.include ?? DEFAULT_INCLUDE;
3878
+ const extraExact = new Set(options.extra ?? []);
3879
+ const workspaceRoot = options.app.workspace.workspaceRoot;
3880
+ const tsconfigPaths = tsconfig.compilerOptions.paths ?? {};
3881
+ const rootEntries = Object.entries(tsconfigPaths).filter(([k]) => !k.endsWith("/*")).map(([k, v]) => ({ pkg: k, entryFile: v[0] ?? null })).filter((e) => e.entryFile !== null);
3882
+ 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);
3883
+ async function resolveWorkspaceSubpath(spec) {
3884
+ if (!workspaceRoot)
3885
+ return null;
3886
+ for (const { prefix, replacements } of wildcardEntries) {
3887
+ if (!spec.startsWith(prefix))
4027
3888
  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;
4038
- }
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))
3889
+ const suffix = spec.slice(prefix.length);
3890
+ for (const repl of replacements) {
3891
+ const replPath = repl?.endsWith("/*") ? repl.slice(0, -1) : repl ?? "";
3892
+ if (!replPath)
4043
3893
  continue;
4044
- queue.push(resolved);
3894
+ const candidate = path21.resolve(workspaceRoot, replPath + suffix);
3895
+ const hit = await firstExisting(candidate);
3896
+ if (hit)
3897
+ return hit;
4045
3898
  }
4046
3899
  }
4047
- return { cssPaths: [...cssFiles], sourcePaths: [...sourceFiles] };
3900
+ for (const { pkg, entryFile } of rootEntries) {
3901
+ if (spec !== pkg && !spec.startsWith(`${pkg}/`))
3902
+ continue;
3903
+ if (spec === pkg)
3904
+ continue;
3905
+ const suffix = spec.slice(pkg.length + 1);
3906
+ const pkgDir = path21.dirname(path21.resolve(workspaceRoot, entryFile));
3907
+ const candidate = path21.join(pkgDir, suffix);
3908
+ const hit = await firstExisting(candidate);
3909
+ if (hit)
3910
+ return hit;
3911
+ }
3912
+ return null;
4048
3913
  }
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)
3914
+ return {
3915
+ name: "akan-externalize-framework",
3916
+ setup(build) {
3917
+ build.onResolve({ filter: /.*/ }, async (args) => {
3918
+ const spec = args.path;
3919
+ if (spec === "." || spec === ".." || spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/"))
3920
+ return;
3921
+ if (extraExact.has(spec) || DEFAULT_EXCLUDE_EXACT.has(spec))
3922
+ return { path: spec, external: true };
3923
+ for (const prefix of DEFAULT_EXCLUDE_PREFIX) {
3924
+ if (spec.startsWith(prefix))
3925
+ return { path: spec, external: true };
3926
+ }
3927
+ if (OPTIONAL_BACKEND_EXTERNAL_EXACT.has(spec))
3928
+ return { path: spec, external: true };
3929
+ if (RUNTIME_EXTERNAL_EXACT.has(spec))
3930
+ return { path: spec, external: true };
3931
+ for (const prefix of RUNTIME_EXTERNAL_PREFIX) {
3932
+ if (spec.startsWith(prefix))
3933
+ return { path: spec, external: true };
3934
+ }
3935
+ for (const prefix of includePrefixes) {
3936
+ if (!spec.startsWith(prefix))
3937
+ continue;
3938
+ const resolved = await resolveWorkspaceSubpath(spec);
3939
+ if (resolved)
3940
+ return { path: resolved };
3941
+ return;
3942
+ }
3943
+ return;
4060
3944
  });
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
3945
  }
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));
3946
+ };
3947
+ }
3948
+ async function firstExisting(basePath) {
3949
+ if (await Bun.file(basePath).exists())
3950
+ return basePath;
3951
+ for (const ext of CANDIDATE_EXTS3) {
3952
+ const candidate = `${basePath}${ext}`;
3953
+ if (await Bun.file(candidate).exists())
3954
+ return candidate;
3955
+ }
3956
+ for (const ext of CANDIDATE_EXTS3) {
3957
+ const candidate = path21.join(basePath, `index${ext}`);
3958
+ if (await Bun.file(candidate).exists())
3959
+ return candidate;
3960
+ }
3961
+ return null;
3962
+ }
3963
+
3964
+ // pkgs/@akanjs/devkit/transforms/useClientBundlePlugin.ts
3965
+ function createUseClientBundlePlugin(options = {}) {
3966
+ return {
3967
+ name: "akan-use-client-bundle",
3968
+ setup(build) {
3969
+ build.onLoad({ filter: /\.(tsx|ts|jsx|js)$/ }, async (args) => {
3970
+ if (args.path.includes("/node_modules/"))
3971
+ return;
3972
+ let source;
3973
+ try {
3974
+ source = await Bun.file(args.path).text();
3975
+ } catch {
3976
+ return;
3977
+ }
3978
+ const stubbed = transformUseClient(source, {
3979
+ path: args.path,
3980
+ workspaceRoot: options.workspaceRoot
3981
+ });
3982
+ if (stubbed === null)
3983
+ return;
3984
+ return { contents: stubbed, loader: loaderFor3(args.path) };
3985
+ });
4078
3986
  }
4079
- this.#logger.verbose(`css compiled paths=${cssPaths.length} candidates=${candidates.length} in ${Date.now() - compileStarted}ms`);
4080
- return parts.join(`
4081
- `);
3987
+ };
3988
+ }
3989
+ function loaderFor3(absPath) {
3990
+ if (absPath.endsWith(".tsx"))
3991
+ return "tsx";
3992
+ if (absPath.endsWith(".jsx"))
3993
+ return "jsx";
3994
+ if (absPath.endsWith(".ts"))
3995
+ return "ts";
3996
+ return "js";
3997
+ }
3998
+
3999
+ // pkgs/@akanjs/devkit/frontendBuild/pagesBundleBuilder.ts
4000
+ var VIRTUAL_PAGES_ENTRY = "akan-pages-entry";
4001
+
4002
+ class PagesBundleBuilder {
4003
+ #app;
4004
+ #command;
4005
+ #pageEntries;
4006
+ #started = Date.now();
4007
+ constructor(app, command = "start", pageEntries) {
4008
+ this.#app = app;
4009
+ this.#command = command;
4010
+ this.#pageEntries = pageEntries;
4082
4011
  }
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 };
4012
+ async build() {
4013
+ const akanConfig = await this.#app.getConfig();
4014
+ const resolvedEntries = this.#pageEntries ?? await resolveSsrPageEntriesForApp(this.#app, await this.#app.getPageKeys());
4015
+ const entrySource = PagesEntrySourceGenerator.generate(resolvedEntries);
4016
+ const workspaceRoot = this.#app.workspace.workspaceRoot;
4017
+ const result = await Bun.build({
4018
+ entrypoints: [VIRTUAL_PAGES_ENTRY],
4019
+ outdir: `${this.#artifactDir}/server`,
4020
+ target: "bun",
4021
+ format: "esm",
4022
+ splitting: this.#splitting,
4023
+ minify: this.#command === "build",
4024
+ naming: {
4025
+ entry: "pages-[hash].[ext]",
4026
+ chunk: "chunks/[name]-[hash].[ext]",
4027
+ asset: "assets/[name]-[hash].[ext]"
4028
+ },
4029
+ define: this.#define(),
4030
+ plugins: [
4031
+ PagesBundleBuilder.createPagesEntryPlugin(entrySource),
4032
+ PagesBundleBuilder.createServerCssStubPlugin(),
4033
+ PagesBundleBuilder.createServerUseClientFetchPlugin(),
4034
+ await createExternalizeFrameworkPlugin({ app: this.#app, extra: akanConfig.externalLibs }),
4035
+ akanConfig.barrelImports.length > 0 ? await createBarrelImportsPlugin(this.#app, {
4036
+ pipeAfter: (source, args) => transformUseClient(source, {
4037
+ path: args.path,
4038
+ workspaceRoot
4039
+ })
4040
+ }) : createUseClientBundlePlugin({ workspaceRoot })
4041
+ ]
4042
+ });
4043
+ if (!result.success)
4044
+ throw new AggregateError(result.logs, "[PagesBundleBuilder] Bun.build failed");
4045
+ const entryArtifact = result.outputs.find((a) => a.kind === "entry-point");
4046
+ if (!entryArtifact)
4047
+ throw new Error("[PagesBundleBuilder] Bun.build emitted no entry-point artifact");
4048
+ const bundlePath = path22.resolve(entryArtifact.path);
4049
+ const buildId = Date.now();
4050
+ const outputBytes = result.outputs.reduce((sum, output) => sum + output.size, 0);
4051
+ const chunkCount = result.outputs.filter((output) => output.kind === "chunk").length;
4052
+ this.#app.verbose(`[PagesBundleBuilder] ${path22.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`);
4053
+ return {
4054
+ bundlePath,
4055
+ buildId,
4056
+ splitting: this.#splitting,
4057
+ entryBytes: entryArtifact.size,
4058
+ outputBytes,
4059
+ outputCount: result.outputs.length,
4060
+ chunkCount
4061
+ };
4087
4062
  }
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}`);
4063
+ get #artifactDir() {
4064
+ return `${this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath}/.akan/artifact`;
4096
4065
  }
4097
- async#getCssImportResolver() {
4098
- if (this.#cssImportResolver)
4099
- return this.#cssImportResolver;
4100
- this.#cssImportResolver = await CssImportResolver.create(this.#app);
4101
- return this.#cssImportResolver;
4066
+ get #splitting() {
4067
+ return process.env.AKAN_SERVER_PAGES_SPLITTING === "1";
4102
4068
  }
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 };
4069
+ #define() {
4070
+ const nodeEnv = this.#command === "build" ? "production" : "development";
4071
+ return {
4072
+ "process.env.NODE_ENV": JSON.stringify(nodeEnv),
4073
+ "process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("ssr"),
4074
+ ...Object.fromEntries(Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]))
4075
+ };
4107
4076
  }
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;
4077
+ static createPagesEntryPlugin(source) {
4078
+ return {
4079
+ name: "akan-pages-entry",
4080
+ setup(build) {
4081
+ build.onResolve({ filter: /^akan-pages-entry$/ }, () => ({
4082
+ path: VIRTUAL_PAGES_ENTRY,
4083
+ namespace: "akan-virtual"
4084
+ }));
4085
+ build.onLoad({ filter: /^akan-pages-entry$/, namespace: "akan-virtual" }, () => ({
4086
+ contents: source,
4087
+ loader: "tsx"
4088
+ }));
4089
+ }
4090
+ };
4122
4091
  }
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);
4092
+ static createServerCssStubPlugin() {
4093
+ return {
4094
+ name: "akan-server-css-stub",
4095
+ setup(build) {
4096
+ build.onLoad({ filter: /\.css$/ }, () => ({
4097
+ contents: "",
4098
+ loader: "js"
4099
+ }));
4133
4100
  }
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];
4101
+ };
4141
4102
  }
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;
4103
+ static createServerUseClientFetchPlugin() {
4104
+ return {
4105
+ name: "akan-server-use-client-fetch",
4106
+ setup(build) {
4107
+ build.onLoad({ filter: /[/\\]lib[/\\]useClient\.(ts|tsx|js|jsx)$/ }, async (args) => {
4108
+ const source = await Bun.file(args.path).text();
4109
+ const transformed = PagesBundleBuilder.transformServerUseClientFetchSource(source);
4110
+ if (transformed === source)
4111
+ return;
4112
+ return { contents: transformed, loader: loaderFor4(args.path) };
4113
+ });
4114
+ }
4115
+ };
4150
4116
  }
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;
4117
+ static transformServerUseClientFetchSource(source) {
4118
+ if (!source.includes(`with { type: "macro" }`) || !source.includes("FetchClient.build"))
4119
+ return source;
4120
+ 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
4121
  }
4156
- return null;
4157
4122
  }
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;
4123
+ function loaderFor4(absPath) {
4124
+ if (absPath.endsWith(".tsx"))
4125
+ return "tsx";
4126
+ if (absPath.endsWith(".jsx"))
4127
+ return "jsx";
4128
+ if (absPath.endsWith(".ts"))
4129
+ return "ts";
4130
+ return "js";
4131
+ }
4132
+
4133
+ // pkgs/@akanjs/devkit/frontendBuild/precompressArtifacts.ts
4134
+ import fs4 from "fs";
4135
+ import path23 from "path";
4136
+ var COMPRESSIBLE_EXTS = new Set([".css", ".html", ".js", ".json", ".svg"]);
4137
+ var MIN_COMPRESS_BYTES = 1024;
4138
+ async function precompressArtifacts(app) {
4139
+ const roots = [path23.join(app.dist.cwdPath, ".akan/artifact/client")];
4140
+ const result = { files: 0, inputBytes: 0, outputBytes: 0 };
4141
+ await Promise.all(roots.map((root) => precompressRoot(root, result)));
4142
+ if (result.files > 0) {
4143
+ app.verbose(`[precompress] wrote ${result.files} gzip sidecars (${formatBytes(result.inputBytes)} -> ${formatBytes(result.outputBytes)})`);
4164
4144
  }
4145
+ return result;
4165
4146
  }
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;
4147
+ async function precompressRoot(root, result) {
4148
+ if (!fs4.existsSync(root) || !fs4.statSync(root).isDirectory())
4149
+ return;
4150
+ const glob = new Bun.Glob("**/*");
4151
+ for await (const filePath of glob.scan({ cwd: root, absolute: true })) {
4152
+ if (!await shouldPrecompress(filePath))
4153
+ continue;
4154
+ const bytes = await Bun.file(filePath).bytes();
4155
+ const gz = Bun.gzipSync(toArrayBuffer(bytes));
4156
+ await Bun.write(`${filePath}.gz`, gz);
4157
+ result.files += 1;
4158
+ result.inputBytes += bytes.byteLength;
4159
+ result.outputBytes += gz.byteLength;
4172
4160
  }
4173
4161
  }
4174
- function isSourceFile(filePath) {
4175
- return SOURCE_EXTS4.includes(path23.extname(filePath));
4162
+ async function shouldPrecompress(filePath) {
4163
+ if (filePath.endsWith(".gz"))
4164
+ return false;
4165
+ if (!COMPRESSIBLE_EXTS.has(path23.extname(filePath).toLowerCase()))
4166
+ return false;
4167
+ const file = Bun.file(filePath);
4168
+ if (!await file.exists())
4169
+ return false;
4170
+ return file.size >= MIN_COMPRESS_BYTES;
4176
4171
  }
4177
- function isIgnoredNodeModuleSource(filePath) {
4178
- return NODE_MODULES_RE3.test(filePath) && !AKANJS_NODE_MODULE_RE3.test(filePath);
4172
+ function toArrayBuffer(bytes) {
4173
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
4179
4174
  }
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;
4175
+ function formatBytes(bytes) {
4176
+ if (bytes < 1024)
4177
+ return `${bytes}B`;
4178
+ if (bytes < 1024 * 1024)
4179
+ return `${(bytes / 1024).toFixed(1)}KB`;
4180
+ return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
4185
4181
  }
4186
4182
 
4187
4183
  // pkgs/@akanjs/devkit/frontendBuild/ssrBaseArtifactBuilder.ts
4184
+ import path24 from "path";
4185
+ import { optimize } from "@tailwindcss/node";
4188
4186
  function prepareCssAsset(command, basePath, cssText) {
4189
4187
  return optimize(cssText, { file: `${basePath || "root"}.css`, minify: command === "build" }).code;
4190
4188
  }
@@ -4356,4 +4354,4 @@ class WatchRootResolver {
4356
4354
  return [...set];
4357
4355
  }
4358
4356
  }
4359
- export { GraphClientEntryDiscovery, RouteClientBuilder, AllRoutesBuilder, AutoImportSync, CsrArtifactBuilder, DevChangePlanner, DevGeneratedIndexSync, FontOptimizer, HmrWatcher, PagesBundleBuilder, precompressArtifacts, SsrBaseArtifactBuilder, WatchRootResolver };
4357
+ export { GraphClientEntryDiscovery, RouteClientBuilder, AllRoutesBuilder, AutoImportSync, CsrArtifactBuilder, CssCompiler, DevChangePlanner, DevGeneratedIndexSync, FontOptimizer, HmrWatcher, PagesBundleBuilder, precompressArtifacts, SsrBaseArtifactBuilder, WatchRootResolver };