@homebound/truss 2.14.0 → 2.15.1

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.
@@ -41,7 +41,8 @@ interface TrussEsbuildPluginOptions {
41
41
  outputCss?: string;
42
42
  }
43
43
  /**
44
- * esbuild plugin that transforms `Css.*.$` expressions and emits a `truss.css` file.
44
+ * esbuild plugin that transforms `Css.*.$` expressions, collects `.css.ts` blocks,
45
+ * and emits a `truss.css` file.
45
46
  *
46
47
  * Designed for library builds using tsup/esbuild. Transforms source files
47
48
  * during the build and writes an annotated `truss.css` alongside the output
@@ -101,6 +102,7 @@ interface TrussVitePlugin {
101
102
  transformIndexHtml?: (html: string) => string;
102
103
  handleHotUpdate?: (ctx: any) => void;
103
104
  generateBundle?: (options: any, bundle: any) => void;
105
+ writeBundle?: (options: any, bundle: any) => void;
104
106
  }
105
107
  /**
106
108
  * Vite plugin that transforms `Css.*.$` expressions from truss's CssBuilder DSL
@@ -111,7 +113,7 @@ interface TrussVitePlugin {
111
113
  * while imports are supplemented with a virtual CSS side-effect module.
112
114
  *
113
115
  * In dev mode, serves CSS via a virtual endpoint that the injected runtime keeps in sync.
114
- * In production, emits a single `truss.css` asset with all atomic rules.
116
+ * In production, emits a content-hashed CSS asset (e.g. `assets/truss-abc123.css`) for long-term caching.
115
117
  */
116
118
  declare function trussPlugin(opts: TrussPluginOptions): TrussVitePlugin;
117
119
  /** Load a truss mapping file synchronously (for tests). */
@@ -1,6 +1,7 @@
1
1
  // src/plugin/index.ts
2
- import { readFileSync as readFileSync3, existsSync } from "fs";
3
- import { resolve as resolve2, dirname, isAbsolute } from "path";
2
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync, readdirSync } from "fs";
3
+ import { resolve as resolve2, dirname, isAbsolute, join as join2 } from "path";
4
+ import { createHash } from "crypto";
4
5
 
5
6
  // src/plugin/emit-truss.ts
6
7
  import * as t from "@babel/types";
@@ -3357,11 +3358,14 @@ function toVirtualCssSpecifier(source) {
3357
3358
  import { readFileSync } from "fs";
3358
3359
  var RULE_ANNOTATION_RE = /^\/\* @truss p:([\d.]+) c:(\S+) \*\/$/;
3359
3360
  var PROPERTY_ANNOTATION_RE = /^\/\* @truss @property \*\/$/;
3361
+ var ARBITRARY_START_RE = /^\/\* @truss arbitrary:start \*\/$/;
3362
+ var ARBITRARY_END_RE = /^\/\* @truss arbitrary:end \*\/$/;
3360
3363
  var PROPERTY_VAR_RE = /^@property\s+(--\S+)/;
3361
3364
  function parseTrussCss(cssText) {
3362
3365
  const lines = cssText.split("\n");
3363
3366
  const rules = [];
3364
3367
  const properties = [];
3368
+ const arbitraryCssBlocks = [];
3365
3369
  let i = 0;
3366
3370
  while (i < lines.length) {
3367
3371
  const line = lines[i].trim();
@@ -3390,19 +3394,43 @@ function parseTrussCss(cssText) {
3390
3394
  i++;
3391
3395
  continue;
3392
3396
  }
3397
+ if (ARBITRARY_START_RE.test(line)) {
3398
+ i++;
3399
+ const blockLines = [];
3400
+ while (i < lines.length && !ARBITRARY_END_RE.test(lines[i].trim())) {
3401
+ blockLines.push(lines[i]);
3402
+ i++;
3403
+ }
3404
+ const blockText = blockLines.join("\n").trim();
3405
+ if (blockText.length > 0) {
3406
+ arbitraryCssBlocks.push({ cssText: blockText });
3407
+ }
3408
+ if (i < lines.length && ARBITRARY_END_RE.test(lines[i].trim())) {
3409
+ i++;
3410
+ }
3411
+ continue;
3412
+ }
3393
3413
  i++;
3394
3414
  }
3395
- return { rules, properties };
3415
+ return { rules, properties, arbitraryCssBlocks };
3396
3416
  }
3397
3417
  function readTrussCss(filePath) {
3398
3418
  const content = readFileSync(filePath, "utf8");
3399
3419
  return parseTrussCss(content);
3400
3420
  }
3421
+ function annotateArbitraryCssBlock(cssText) {
3422
+ const trimmed = cssText.trim();
3423
+ if (trimmed.length === 0) {
3424
+ return "";
3425
+ }
3426
+ return ["/* @truss arbitrary:start */", trimmed, "/* @truss arbitrary:end */"].join("\n");
3427
+ }
3401
3428
  function mergeTrussCss(sources) {
3402
3429
  const seenClasses = /* @__PURE__ */ new Set();
3403
3430
  const allRules = [];
3404
3431
  const seenProperties = /* @__PURE__ */ new Set();
3405
3432
  const allProperties = [];
3433
+ const allArbitraryCssBlocks = [];
3406
3434
  for (const source of sources) {
3407
3435
  for (const rule of source.rules) {
3408
3436
  if (!seenClasses.has(rule.className)) {
@@ -3416,6 +3444,7 @@ function mergeTrussCss(sources) {
3416
3444
  allProperties.push(prop);
3417
3445
  }
3418
3446
  }
3447
+ allArbitraryCssBlocks.push(...source.arbitraryCssBlocks ?? []);
3419
3448
  }
3420
3449
  allRules.sort((a, b) => {
3421
3450
  const diff = a.priority - b.priority;
@@ -3431,6 +3460,9 @@ function mergeTrussCss(sources) {
3431
3460
  lines.push(`/* @truss @property */`);
3432
3461
  lines.push(prop.cssText);
3433
3462
  }
3463
+ for (const block of allArbitraryCssBlocks) {
3464
+ lines.push(annotateArbitraryCssBlock(block.cssText));
3465
+ }
3434
3466
  return lines.join("\n");
3435
3467
  }
3436
3468
 
@@ -3439,6 +3471,7 @@ import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
3439
3471
  import { resolve, join } from "path";
3440
3472
  function trussEsbuildPlugin(opts) {
3441
3473
  const cssRegistry = /* @__PURE__ */ new Map();
3474
+ const arbitraryCssRegistry = /* @__PURE__ */ new Map();
3442
3475
  let mapping = null;
3443
3476
  let outDir;
3444
3477
  return {
@@ -3447,6 +3480,18 @@ function trussEsbuildPlugin(opts) {
3447
3480
  outDir = build.initialOptions.outdir ?? build.initialOptions.outdir;
3448
3481
  build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, (args) => {
3449
3482
  const code = readFileSync2(args.path, "utf8");
3483
+ if (args.path.endsWith(".css.ts")) {
3484
+ if (!mapping) {
3485
+ mapping = loadMapping(resolve(process.cwd(), opts.mapping));
3486
+ }
3487
+ const css = annotateArbitraryCssBlock(transformCssTs(code, args.path, mapping));
3488
+ if (css.length > 0) {
3489
+ arbitraryCssRegistry.set(args.path, css);
3490
+ } else {
3491
+ arbitraryCssRegistry.delete(args.path);
3492
+ }
3493
+ return { contents: code, loader: loaderForPath(args.path) };
3494
+ }
3450
3495
  if (!code.includes("Css") && !code.includes("css=")) return void 0;
3451
3496
  if (!mapping) {
3452
3497
  mapping = loadMapping(resolve(process.cwd(), opts.mapping));
@@ -3463,8 +3508,11 @@ function trussEsbuildPlugin(opts) {
3463
3508
  return { contents: result.code, loader: loaderForPath(args.path) };
3464
3509
  });
3465
3510
  build.onEnd(() => {
3466
- if (cssRegistry.size === 0) return;
3467
- const css = generateCssText(cssRegistry);
3511
+ if (cssRegistry.size === 0 && arbitraryCssRegistry.size === 0) return;
3512
+ const cssParts = [generateCssText(cssRegistry), ...arbitraryCssRegistry.values()].filter(
3513
+ (part) => part.length > 0
3514
+ );
3515
+ const css = cssParts.join("\n");
3468
3516
  const cssFileName = opts.outputCss ?? "truss.css";
3469
3517
  const cssPath = resolve(outDir ?? join(process.cwd(), "dist"), cssFileName);
3470
3518
  mkdirSync(resolve(cssPath, ".."), { recursive: true });
@@ -3483,6 +3531,7 @@ function loaderForPath(filePath) {
3483
3531
  // src/plugin/index.ts
3484
3532
  var VIRTUAL_CSS_PREFIX = "\0truss-css:";
3485
3533
  var CSS_TS_QUERY = "?truss-css";
3534
+ var TRUSS_CSS_PLACEHOLDER = "__TRUSS_CSS_HASH__";
3486
3535
  var VIRTUAL_CSS_ENDPOINT = "/virtual:truss.css";
3487
3536
  var VIRTUAL_RUNTIME_ID = "virtual:truss:runtime";
3488
3537
  var RESOLVED_VIRTUAL_RUNTIME_ID = "\0" + VIRTUAL_RUNTIME_ID;
@@ -3495,6 +3544,7 @@ function trussPlugin(opts) {
3495
3544
  let isTest = false;
3496
3545
  let isBuild = false;
3497
3546
  const libraryPaths = opts.libraries ?? [];
3547
+ let emittedCssFileName = null;
3498
3548
  const cssRegistry = /* @__PURE__ */ new Map();
3499
3549
  let cssVersion = 0;
3500
3550
  let lastSentVersion = 0;
@@ -3562,8 +3612,9 @@ function trussPlugin(opts) {
3562
3612
  },
3563
3613
  transformIndexHtml(html) {
3564
3614
  if (isBuild) {
3565
- const link = `<link rel="stylesheet" href="./truss.css">`;
3566
- return html.replace("</head>", ` ${link}
3615
+ const stripped = html.replace(/\s*<link[^>]*href=["'][^"']*virtual:truss\.css["'][^>]*\/?>/g, "").replace(/\s*<link[^>]*href=["'][^"']*__TRUSS_CSS_HASH__["'][^>]*\/?>/g, "").replace(/\s*<link[^>]*href=["'][^"']*\/assets\/truss-[0-9a-f]+\.css["'][^>]*\/?>/g, "");
3616
+ const link = `<link rel="stylesheet" href="${TRUSS_CSS_PLACEHOLDER}">`;
3617
+ return stripped.replace("</head>", ` ${link}
3567
3618
  </head>`);
3568
3619
  }
3569
3620
  const tag = `<script type="module" src="/${VIRTUAL_RUNTIME_ID}"></script>`;
@@ -3680,11 +3731,27 @@ __injectTrussCSS(${JSON.stringify(css)});
3680
3731
  if (!isBuild) return;
3681
3732
  const css = collectCss();
3682
3733
  if (!css) return;
3734
+ const hash = createHash("sha256").update(css).digest("hex").slice(0, 8);
3735
+ const fileName = `assets/truss-${hash}.css`;
3736
+ emittedCssFileName = fileName;
3683
3737
  this.emitFile({
3684
3738
  type: "asset",
3685
- fileName: "truss.css",
3739
+ fileName,
3686
3740
  source: css
3687
3741
  });
3742
+ },
3743
+ /** Patch HTML files on disk to replace the CSS placeholder with the hashed filename. */
3744
+ writeBundle(options, _bundle) {
3745
+ if (!emittedCssFileName) return;
3746
+ const outDir = options.dir || join2(projectRoot, "dist");
3747
+ for (const entry of readdirSync(outDir)) {
3748
+ if (!entry.endsWith(".html")) continue;
3749
+ const htmlPath = join2(outDir, entry);
3750
+ const html = readFileSync3(htmlPath, "utf8");
3751
+ if (html.includes(TRUSS_CSS_PLACEHOLDER)) {
3752
+ writeFileSync2(htmlPath, html.replace(TRUSS_CSS_PLACEHOLDER, `/${emittedCssFileName}`), "utf8");
3753
+ }
3754
+ }
3688
3755
  }
3689
3756
  };
3690
3757
  }