@homebound/truss 2.3.5 → 2.5.0

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.
@@ -34,11 +34,57 @@ type TrussMappingEntry =
34
34
  chain: string[];
35
35
  };
36
36
 
37
+ interface TrussEsbuildPluginOptions {
38
+ /** Path to the Css.json mapping file (relative to cwd or absolute). */
39
+ mapping: string;
40
+ /** Output path for the generated truss.css (relative to outDir or absolute). Defaults to `truss.css`. */
41
+ outputCss?: string;
42
+ }
43
+ /**
44
+ * esbuild plugin that transforms `Css.*.$` expressions and emits a `truss.css` file.
45
+ *
46
+ * Designed for library builds using tsup/esbuild. Transforms source files
47
+ * during the build and writes an annotated `truss.css` alongside the output
48
+ * that consuming applications can merge via the Vite plugin's `libraries` option.
49
+ *
50
+ * Usage with tsup:
51
+ * ```ts
52
+ * import { trussEsbuildPlugin } from "@homebound/truss/plugin";
53
+ *
54
+ * export default defineConfig({
55
+ * esbuildPlugins: [trussEsbuildPlugin({ mapping: "./src/Css.json" })],
56
+ * });
57
+ * ```
58
+ */
59
+ declare function trussEsbuildPlugin(opts: TrussEsbuildPluginOptions): {
60
+ name: string;
61
+ setup(build: EsbuildPluginBuild): void;
62
+ };
63
+ /**
64
+ * Minimal esbuild plugin types so we don't need esbuild as a dependency.
65
+ *
66
+ * These match the subset of the esbuild Plugin API that we use.
67
+ */
68
+ interface EsbuildPluginBuild {
69
+ initialOptions: {
70
+ outdir?: string;
71
+ };
72
+ onLoad(options: {
73
+ filter: RegExp;
74
+ }, callback: (args: {
75
+ path: string;
76
+ }) => {
77
+ contents: string;
78
+ loader: string;
79
+ } | undefined): void;
80
+ onEnd(callback: () => void): void;
81
+ }
82
+
37
83
  interface TrussPluginOptions {
38
84
  /** Path to the Css.json mapping file used for transforming files (relative to project root or absolute). */
39
85
  mapping: string;
40
- /** Packages in `node_modules` that should also be transformed, all other `node_modules` files are skipped. */
41
- externalPackages?: string[];
86
+ /** Paths to pre-compiled truss.css files from libraries to merge into the app's CSS. */
87
+ libraries?: string[];
42
88
  }
43
89
  interface TrussVitePlugin {
44
90
  name: string;
@@ -72,4 +118,4 @@ declare function trussPlugin(opts: TrussPluginOptions): TrussVitePlugin;
72
118
  /** Load a truss mapping file synchronously (for tests). */
73
119
  declare function loadMapping(path: string): TrussMapping;
74
120
 
75
- export { type TrussMapping, type TrussMappingEntry, type TrussPluginOptions, type TrussVitePlugin, loadMapping, trussPlugin };
121
+ export { type TrussEsbuildPluginOptions, type TrussMapping, type TrussMappingEntry, type TrussPluginOptions, type TrussVitePlugin, loadMapping, trussEsbuildPlugin, trussPlugin };
@@ -1,6 +1,6 @@
1
1
  // src/plugin/index.ts
2
- import { readFileSync, writeFileSync, existsSync } from "fs";
3
- import { resolve, dirname, isAbsolute, join } from "path";
2
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync } from "fs";
3
+ import { resolve as resolve2, dirname, isAbsolute, join as join2 } from "path";
4
4
 
5
5
  // src/plugin/emit-truss.ts
6
6
  import * as t from "@babel/types";
@@ -838,13 +838,18 @@ function collectVariableRules(rules, seg, mapping) {
838
838
  function generateCssText(rules) {
839
839
  const allRules = Array.from(rules.values());
840
840
  sortRulesByPriority(allRules);
841
+ const priorities = allRules.map(computeRulePriority);
841
842
  const lines = [];
842
- for (const rule of allRules) {
843
+ for (let i = 0; i < allRules.length; i++) {
844
+ const rule = allRules[i];
845
+ const priority = priorities[i];
846
+ lines.push(`/* @truss p:${priority} c:${rule.className} */`);
843
847
  lines.push(formatRule(rule));
844
848
  }
845
849
  for (const rule of allRules) {
846
850
  for (const declaration of getRuleDeclarations(rule)) {
847
851
  if (declaration.cssVarName) {
852
+ lines.push(`/* @truss @property */`);
848
853
  lines.push(`@property ${declaration.cssVarName} { syntax: "*"; inherits: false; }`);
849
854
  }
850
855
  }
@@ -2791,6 +2796,133 @@ function toVirtualCssSpecifier(source) {
2791
2796
  return `${source}?truss-css`;
2792
2797
  }
2793
2798
 
2799
+ // src/plugin/merge-css.ts
2800
+ import { readFileSync } from "fs";
2801
+ var RULE_ANNOTATION_RE = /^\/\* @truss p:([\d.]+) c:(\S+) \*\/$/;
2802
+ var PROPERTY_ANNOTATION_RE = /^\/\* @truss @property \*\/$/;
2803
+ var PROPERTY_VAR_RE = /^@property\s+(--\S+)/;
2804
+ function parseTrussCss(cssText) {
2805
+ const lines = cssText.split("\n");
2806
+ const rules = [];
2807
+ const properties = [];
2808
+ let i = 0;
2809
+ while (i < lines.length) {
2810
+ const line = lines[i].trim();
2811
+ const ruleMatch = RULE_ANNOTATION_RE.exec(line);
2812
+ if (ruleMatch) {
2813
+ const priority = parseFloat(ruleMatch[1]);
2814
+ const className = ruleMatch[2];
2815
+ i++;
2816
+ while (i < lines.length && lines[i].trim() === "") i++;
2817
+ if (i < lines.length) {
2818
+ rules.push({ priority, className, cssText: lines[i].trim() });
2819
+ }
2820
+ i++;
2821
+ continue;
2822
+ }
2823
+ if (PROPERTY_ANNOTATION_RE.test(line)) {
2824
+ i++;
2825
+ while (i < lines.length && lines[i].trim() === "") i++;
2826
+ if (i < lines.length) {
2827
+ const propLine = lines[i].trim();
2828
+ const varMatch = PROPERTY_VAR_RE.exec(propLine);
2829
+ if (varMatch) {
2830
+ properties.push({ cssText: propLine, varName: varMatch[1] });
2831
+ }
2832
+ }
2833
+ i++;
2834
+ continue;
2835
+ }
2836
+ i++;
2837
+ }
2838
+ return { rules, properties };
2839
+ }
2840
+ function readTrussCss(filePath) {
2841
+ const content = readFileSync(filePath, "utf8");
2842
+ return parseTrussCss(content);
2843
+ }
2844
+ function mergeTrussCss(sources) {
2845
+ const seenClasses = /* @__PURE__ */ new Set();
2846
+ const allRules = [];
2847
+ const seenProperties = /* @__PURE__ */ new Set();
2848
+ const allProperties = [];
2849
+ for (const source of sources) {
2850
+ for (const rule of source.rules) {
2851
+ if (!seenClasses.has(rule.className)) {
2852
+ seenClasses.add(rule.className);
2853
+ allRules.push(rule);
2854
+ }
2855
+ }
2856
+ for (const prop of source.properties) {
2857
+ if (!seenProperties.has(prop.varName)) {
2858
+ seenProperties.add(prop.varName);
2859
+ allProperties.push(prop);
2860
+ }
2861
+ }
2862
+ }
2863
+ allRules.sort(function(a, b) {
2864
+ const diff = a.priority - b.priority;
2865
+ if (diff !== 0) return diff;
2866
+ return a.className < b.className ? -1 : a.className > b.className ? 1 : 0;
2867
+ });
2868
+ const lines = [];
2869
+ for (const rule of allRules) {
2870
+ lines.push(`/* @truss p:${rule.priority} c:${rule.className} */`);
2871
+ lines.push(rule.cssText);
2872
+ }
2873
+ for (const prop of allProperties) {
2874
+ lines.push(`/* @truss @property */`);
2875
+ lines.push(prop.cssText);
2876
+ }
2877
+ return lines.join("\n");
2878
+ }
2879
+
2880
+ // src/plugin/esbuild-plugin.ts
2881
+ import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
2882
+ import { resolve, join } from "path";
2883
+ function trussEsbuildPlugin(opts) {
2884
+ const cssRegistry = /* @__PURE__ */ new Map();
2885
+ let mapping = null;
2886
+ let outDir;
2887
+ return {
2888
+ name: "truss",
2889
+ setup(build) {
2890
+ outDir = build.initialOptions.outdir ?? build.initialOptions.outdir;
2891
+ build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, function(args) {
2892
+ const code = readFileSync2(args.path, "utf8");
2893
+ if (!code.includes("Css")) return void 0;
2894
+ if (!mapping) {
2895
+ mapping = loadMapping(resolve(process.cwd(), opts.mapping));
2896
+ }
2897
+ const result = transformTruss(code, args.path, mapping);
2898
+ if (!result) return void 0;
2899
+ if (result.rules) {
2900
+ for (const [className, rule] of result.rules) {
2901
+ if (!cssRegistry.has(className)) {
2902
+ cssRegistry.set(className, rule);
2903
+ }
2904
+ }
2905
+ }
2906
+ return { contents: result.code, loader: loaderForPath(args.path) };
2907
+ });
2908
+ build.onEnd(function() {
2909
+ if (cssRegistry.size === 0) return;
2910
+ const css = generateCssText(cssRegistry);
2911
+ const cssFileName = opts.outputCss ?? "truss.css";
2912
+ const cssPath = resolve(outDir ?? join(process.cwd(), "dist"), cssFileName);
2913
+ mkdirSync(resolve(cssPath, ".."), { recursive: true });
2914
+ writeFileSync(cssPath, css, "utf8");
2915
+ });
2916
+ }
2917
+ };
2918
+ }
2919
+ function loaderForPath(filePath) {
2920
+ if (filePath.endsWith(".tsx")) return "tsx";
2921
+ if (filePath.endsWith(".ts")) return "ts";
2922
+ if (filePath.endsWith(".jsx")) return "jsx";
2923
+ return "js";
2924
+ }
2925
+
2794
2926
  // src/plugin/index.ts
2795
2927
  var VIRTUAL_CSS_PREFIX = "\0truss-css:";
2796
2928
  var CSS_TS_QUERY = "?truss-css";
@@ -2803,12 +2935,12 @@ function trussPlugin(opts) {
2803
2935
  let debug = false;
2804
2936
  let isTest = false;
2805
2937
  let isBuild = false;
2806
- const externalPackages = opts.externalPackages ?? [];
2938
+ const libraryPaths = opts.libraries ?? [];
2807
2939
  const cssRegistry = /* @__PURE__ */ new Map();
2808
2940
  let cssVersion = 0;
2809
2941
  let lastSentVersion = 0;
2810
2942
  function mappingPath() {
2811
- return resolve(projectRoot || process.cwd(), opts.mapping);
2943
+ return resolve2(projectRoot || process.cwd(), opts.mapping);
2812
2944
  }
2813
2945
  function ensureMapping() {
2814
2946
  if (!mapping) {
@@ -2816,8 +2948,22 @@ function trussPlugin(opts) {
2816
2948
  }
2817
2949
  return mapping;
2818
2950
  }
2951
+ let libraryCache = null;
2952
+ function loadLibraries() {
2953
+ if (!libraryCache) {
2954
+ libraryCache = libraryPaths.map(function(libPath) {
2955
+ const resolved = resolve2(projectRoot || process.cwd(), libPath);
2956
+ return readTrussCss(resolved);
2957
+ });
2958
+ }
2959
+ return libraryCache;
2960
+ }
2819
2961
  function collectCss() {
2820
- return generateCssText(cssRegistry);
2962
+ const appCss = generateCssText(cssRegistry);
2963
+ const libs = loadLibraries();
2964
+ if (libs.length === 0) return appCss;
2965
+ const appParsed = parseTrussCss(appCss);
2966
+ return mergeTrussCss([...libs, appParsed]);
2821
2967
  }
2822
2968
  return {
2823
2969
  name: "truss",
@@ -2831,6 +2977,7 @@ function trussPlugin(opts) {
2831
2977
  buildStart() {
2832
2978
  ensureMapping();
2833
2979
  cssRegistry.clear();
2980
+ libraryCache = null;
2834
2981
  cssVersion = 0;
2835
2982
  lastSentVersion = 0;
2836
2983
  },
@@ -2907,7 +3054,7 @@ function trussPlugin(opts) {
2907
3054
  }
2908
3055
  if (!id.startsWith(VIRTUAL_CSS_PREFIX)) return null;
2909
3056
  const sourcePath = id.slice(VIRTUAL_CSS_PREFIX.length) + ".ts";
2910
- const sourceCode = readFileSync(sourcePath, "utf8");
3057
+ const sourceCode = readFileSync3(sourcePath, "utf8");
2911
3058
  return transformCssTs(sourceCode, sourcePath, ensureMapping());
2912
3059
  },
2913
3060
  transform(code, id) {
@@ -2917,7 +3064,7 @@ function trussPlugin(opts) {
2917
3064
  const hasCssDsl = rewrittenCode.includes("Css");
2918
3065
  if (!hasCssDsl && !rewrittenImports.changed) return null;
2919
3066
  const fileId = stripQueryAndHash(id);
2920
- if (isNodeModulesFile(fileId) && !isWhitelistedExternalPackageFile(fileId, externalPackages)) {
3067
+ if (isNodeModulesFile(fileId)) {
2921
3068
  return null;
2922
3069
  }
2923
3070
  if (fileId.endsWith(".css.ts")) {
@@ -2971,15 +3118,15 @@ function trussPlugin(opts) {
2971
3118
  if (!isBuild) return;
2972
3119
  const css = collectCss();
2973
3120
  if (!css) return;
2974
- const outDir = options.dir || join(projectRoot, "dist");
2975
- const trussPath = join(outDir, "truss.css");
3121
+ const outDir = options.dir || join2(projectRoot, "dist");
3122
+ const trussPath = join2(outDir, "truss.css");
2976
3123
  if (!existsSync(trussPath)) {
2977
3124
  const alreadyEmitted = Object.keys(bundle).some(function(key) {
2978
3125
  const asset = bundle[key];
2979
3126
  return asset.type === "asset" && key.endsWith(".css") && typeof asset.source === "string" && asset.source.includes(css);
2980
3127
  });
2981
3128
  if (!alreadyEmitted) {
2982
- writeFileSync(trussPath, css, "utf8");
3129
+ writeFileSync2(trussPath, css, "utf8");
2983
3130
  }
2984
3131
  }
2985
3132
  }
@@ -2990,9 +3137,9 @@ function resolveImportPath(source, importer, projectRoot) {
2990
3137
  return source;
2991
3138
  }
2992
3139
  if (importer) {
2993
- return resolve(dirname(importer), source);
3140
+ return resolve2(dirname(importer), source);
2994
3141
  }
2995
- return resolve(projectRoot || process.cwd(), source);
3142
+ return resolve2(projectRoot || process.cwd(), source);
2996
3143
  }
2997
3144
  function stripQueryAndHash(id) {
2998
3145
  const queryIndex = id.indexOf("?");
@@ -3007,23 +3154,15 @@ function stripQueryAndHash(id) {
3007
3154
  return cleanId;
3008
3155
  }
3009
3156
  function isNodeModulesFile(filePath) {
3010
- return normalizePath(filePath).includes("/node_modules/");
3011
- }
3012
- function isWhitelistedExternalPackageFile(filePath, externalPackages) {
3013
- const normalizedPath = normalizePath(filePath);
3014
- return externalPackages.some(function(pkg) {
3015
- return normalizedPath.includes(`/node_modules/${pkg}/`);
3016
- });
3017
- }
3018
- function normalizePath(path) {
3019
- return path.replace(/\\/g, "/");
3157
+ return filePath.replace(/\\/g, "/").includes("/node_modules/");
3020
3158
  }
3021
3159
  function loadMapping(path) {
3022
- const raw = readFileSync(path, "utf8");
3160
+ const raw = readFileSync3(path, "utf8");
3023
3161
  return JSON.parse(raw);
3024
3162
  }
3025
3163
  export {
3026
3164
  loadMapping,
3165
+ trussEsbuildPlugin,
3027
3166
  trussPlugin
3028
3167
  };
3029
3168
  //# sourceMappingURL=index.js.map