@homebound/truss 2.29.9 → 2.29.11

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.
@@ -1,3 +1,5 @@
1
+ import { PluginContext } from 'rollup';
2
+
1
3
  /** The shape of the Css.json mapping file consumed by the Vite plugin. */
2
4
  interface TrussMapping {
3
5
  increment: number;
@@ -91,15 +93,18 @@ interface TrussPluginOptions {
91
93
  mapping: string;
92
94
  /** Paths to pre-compiled truss.css files from libraries to merge into the app's CSS. */
93
95
  libraries?: string[];
96
+ /** Unsupported patterns fail builds by default; dev transforms warn and keep serving the page. */
97
+ unsupportedPattern?: "error" | "warn";
94
98
  }
99
+ type DiagnosticContext = Pick<PluginContext, "warn">;
95
100
  interface TrussVitePlugin {
96
101
  name: string;
97
102
  enforce?: "pre" | "post";
98
103
  configResolved?: (config: any) => void;
99
104
  buildStart?: () => void;
100
105
  resolveId?: (source: string, importer: string | undefined) => string | null;
101
- load?: (id: string) => string | null;
102
- transform?: (code: string, id: string) => {
106
+ load?: (this: DiagnosticContext, id: string) => string | null;
107
+ transform?: (this: DiagnosticContext, code: string, id: string) => {
103
108
  code: string;
104
109
  map: any;
105
110
  } | null;
@@ -330,11 +330,44 @@ function resetConditionContext(context) {
330
330
  Object.assign(context, emptyConditionContext());
331
331
  }
332
332
 
333
+ // src/plugin/unknown-abbreviation.ts
334
+ var UnknownAbbreviationError = class extends UnsupportedPatternError {
335
+ constructor(abbreviation, candidates) {
336
+ const suggestion = closestAbbreviation(abbreviation, candidates);
337
+ super(`Unknown abbreviation "${abbreviation}"${suggestion ? `. Did you mean "${suggestion}"?` : ""}`);
338
+ }
339
+ };
340
+ function closestAbbreviation(abbreviation, candidates) {
341
+ let closest;
342
+ let bestDistance = 3;
343
+ for (const candidate of candidates) {
344
+ if (Math.abs(candidate.length - abbreviation.length) >= bestDistance) continue;
345
+ let previous = Array.from({ length: candidate.length + 1 }, (_, index) => index);
346
+ for (let i = 1; i <= abbreviation.length; i++) {
347
+ const current = [i];
348
+ for (let j = 1; j <= candidate.length; j++) {
349
+ current[j] = Math.min(
350
+ current[j - 1] + 1,
351
+ previous[j] + 1,
352
+ previous[j - 1] + (abbreviation[i - 1] === candidate[j - 1] ? 0 : 1)
353
+ );
354
+ }
355
+ previous = current;
356
+ }
357
+ const distance = previous[candidate.length];
358
+ if (distance < bestDistance) {
359
+ closest = candidate;
360
+ bestDistance = distance;
361
+ }
362
+ }
363
+ return closest;
364
+ }
365
+
333
366
  // src/plugin/resolve-entry.ts
334
367
  function requireEntry(mapping, abbr) {
335
368
  const entry = mapping.abbreviations[abbr];
336
369
  if (!entry) {
337
- throw new UnsupportedPatternError(`Unknown abbreviation "${abbr}"`);
370
+ throw new UnknownAbbreviationError(abbr, Object.keys(mapping.abbreviations));
338
371
  }
339
372
  return entry;
340
373
  }
@@ -2553,16 +2586,18 @@ function parseTrussCss(cssText) {
2553
2586
  }
2554
2587
  return { rules, properties, arbitraryCssBlocks };
2555
2588
  }
2556
- function serializeTrussCss(css) {
2589
+ function serializeTrussCss(css, annotate = true) {
2557
2590
  const lines = [];
2558
2591
  for (const rule of css.rules) {
2559
- lines.push(`/* @truss p:${rule.priority} c:${rule.className} */`, rule.cssText);
2592
+ if (annotate) lines.push(`/* @truss p:${rule.priority} c:${rule.className} */`);
2593
+ lines.push(rule.cssText);
2560
2594
  }
2561
2595
  for (const prop of css.properties) {
2562
- lines.push(`/* @truss @property */`, prop.cssText);
2596
+ if (annotate) lines.push(`/* @truss @property */`);
2597
+ lines.push(prop.cssText);
2563
2598
  }
2564
2599
  for (const block of css.arbitraryCssBlocks) {
2565
- lines.push(annotateArbitraryCssBlock(block.cssText));
2600
+ lines.push(annotate ? annotateArbitraryCssBlock(block.cssText) : block.cssText.trim());
2566
2601
  }
2567
2602
  return lines.join("\n");
2568
2603
  }
@@ -2733,12 +2768,28 @@ function getTopLevelVariableDeclaration(node) {
2733
2768
  return null;
2734
2769
  }
2735
2770
 
2771
+ // src/plugin/diagnostic.ts
2772
+ var Diagnostic = class extends Error {
2773
+ id;
2774
+ loc;
2775
+ constructor(message, filename, node) {
2776
+ const start = node.loc?.start;
2777
+ const location = start ? `${filename}:${start.line}:${start.column + 1}` : filename;
2778
+ super(`${location}: ${message}`);
2779
+ this.id = filename;
2780
+ this.loc = start ? { file: filename, line: start.line, column: start.column } : void 0;
2781
+ }
2782
+ };
2783
+
2736
2784
  // src/plugin/transform-css.ts
2737
- function transformCssTs(code, filename, mapping) {
2785
+ function transformCssTs(code, filename, mapping, options = {}) {
2738
2786
  const ast = parseModule(code, filename);
2739
2787
  const cssBindingName = findCssImportBinding(ast);
2740
2788
  const cssExport = findNamedCssExportObject(ast);
2741
2789
  if (!cssExport) {
2790
+ options.onDiagnostic?.(
2791
+ new Diagnostic("expected `export const css = { ... }` with an object literal", filename, ast.program)
2792
+ );
2742
2793
  return `/* [truss] ${filename}: expected \`export const css = { ... }\` with an object literal */
2743
2794
  `;
2744
2795
  }
@@ -2746,16 +2797,16 @@ function transformCssTs(code, filename, mapping) {
2746
2797
  const stringBindings = collectStaticStringBindings(ast);
2747
2798
  for (const prop of cssExport.properties) {
2748
2799
  if (t12.isSpreadElement(prop)) {
2749
- rules.push(`/* [truss] unsupported: spread elements in css.ts export */`);
2800
+ rules.push(unsupported("spread elements in css.ts export", prop));
2750
2801
  continue;
2751
2802
  }
2752
2803
  if (!t12.isObjectProperty(prop)) {
2753
- rules.push(`/* [truss] unsupported: non-property in css.ts export */`);
2804
+ rules.push(unsupported("non-property in css.ts export", prop));
2754
2805
  continue;
2755
2806
  }
2756
2807
  const selector = objectPropertyStringKey(prop, stringBindings);
2757
2808
  if (selector === null) {
2758
- rules.push(`/* [truss] unsupported: non-string-literal key in css.ts export */`);
2809
+ rules.push(unsupported("non-string-literal key in css.ts export", prop));
2759
2810
  continue;
2760
2811
  }
2761
2812
  const valueNode = prop.value;
@@ -2765,21 +2816,25 @@ function transformCssTs(code, filename, mapping) {
2765
2816
  continue;
2766
2817
  }
2767
2818
  if (!t12.isExpression(valueNode)) {
2768
- rules.push(`/* [truss] unsupported: "${selector}" value is not an expression */`);
2819
+ rules.push(unsupported(`"${selector}" value is not an expression`, valueNode));
2769
2820
  continue;
2770
2821
  }
2771
2822
  if (!cssBindingName) {
2772
- rules.push(`/* [truss] unsupported: "${selector}" \u2014 Css.*.$ chain requires a Css import */`);
2823
+ rules.push(unsupported(`"${selector}" \u2014 Css.*.$ chain requires a Css import`, valueNode));
2773
2824
  continue;
2774
2825
  }
2775
- const cssResult = resolveCssExpression(valueNode, cssBindingName, mapping, filename);
2826
+ const cssResult = resolveCssExpression(valueNode, cssBindingName, mapping);
2776
2827
  if ("error" in cssResult) {
2777
- rules.push(`/* [truss] unsupported: "${selector}" \u2014 ${cssResult.error} */`);
2828
+ rules.push(unsupported(`"${selector}" \u2014 ${cssResult.error}`, valueNode));
2778
2829
  continue;
2779
2830
  }
2780
2831
  rules.push(formatCssRule(selector, cssResult.declarations));
2781
2832
  }
2782
2833
  return rules.join("\n\n") + "\n";
2834
+ function unsupported(message, node) {
2835
+ options.onDiagnostic?.(new Diagnostic(message, filename, node));
2836
+ return `/* [truss] unsupported: ${message} */`;
2837
+ }
2783
2838
  }
2784
2839
  function findNamedCssExportObject(ast) {
2785
2840
  for (const node of ast.program.body) {
@@ -2809,7 +2864,7 @@ function extractStaticStringValue(node, cssBindingName) {
2809
2864
  }
2810
2865
  return null;
2811
2866
  }
2812
- function resolveCssExpression(node, cssBindingName, mapping, filename) {
2867
+ function resolveCssExpression(node, cssBindingName, mapping) {
2813
2868
  const chain = extractDollarChain(node, cssBindingName);
2814
2869
  if (!chain) {
2815
2870
  return { error: "value must be a Css.*.$ expression" };
@@ -3375,10 +3430,18 @@ function transformTruss(code, filename, mapping, options = {}) {
3375
3430
  return;
3376
3431
  }
3377
3432
  const resolveCssChainReference2 = buildCssChainReferenceResolver(path, cssBindingName);
3378
- const resolvedChain = resolveFullChain({ mapping, cssBindingName, resolveCssChainReference: resolveCssChainReference2 }, chain);
3433
+ const resolvedChain = resolveFullChain(
3434
+ {
3435
+ mapping,
3436
+ cssBindingName,
3437
+ resolveCssChainReference: resolveCssChainReference2
3438
+ },
3439
+ chain
3440
+ );
3379
3441
  sites.push({ path, resolvedChain });
3380
3442
  const line = path.node.loc?.start.line ?? null;
3381
3443
  for (const err of resolvedChain.errors) {
3444
+ options.onDiagnostic?.(new Diagnostic(err, filename, path.node));
3382
3445
  errorMessages.push({ message: err, line });
3383
3446
  }
3384
3447
  },
@@ -3628,9 +3691,9 @@ function createTrussTransformSession(options) {
3628
3691
  arbitraryCssRegistry.clear();
3629
3692
  libraryCache = null;
3630
3693
  }
3631
- function updateArbitraryCssRegistry(sourcePath, sourceCode) {
3694
+ function updateArbitraryCssRegistry(sourcePath, sourceCode, diagnostics = {}) {
3632
3695
  sourcePath = resolve2(sourcePath).replace(/\\/g, "/");
3633
- const css = transformCssTs(sourceCode, sourcePath, ensureMapping()).trim();
3696
+ const css = transformCssTs(sourceCode, sourcePath, ensureMapping(), diagnostics).trim();
3634
3697
  if (css.length > 0) {
3635
3698
  const prev = arbitraryCssRegistry.get(sourcePath);
3636
3699
  arbitraryCssRegistry.set(sourcePath, css);
@@ -3656,13 +3719,13 @@ function createTrussTransformSession(options) {
3656
3719
  }
3657
3720
  return result;
3658
3721
  }
3659
- function collectCss() {
3722
+ function collectCss(annotate = true) {
3660
3723
  const mapping2 = ensureMapping();
3661
3724
  const appCss = generateCssData(cssRegistry);
3662
3725
  const allArbitrary = Array.from(arbitraryCssRegistry.entries()).sort((a, b) => compareClassNames(a[0], b[0])).map((entry) => entry[1]).join("\n\n");
3663
3726
  if (allArbitrary.length > 0) appCss.arbitraryCssBlocks.push({ cssText: allArbitrary });
3664
3727
  const libs = loadLibraries();
3665
- const body = serializeTrussCss(libs.length === 0 ? appCss : mergeTrussCssData([...libs, appCss]));
3728
+ const body = serializeTrussCss(libs.length === 0 ? appCss : mergeTrussCssData([...libs, appCss]), annotate);
3666
3729
  if (body.length === 0) return "";
3667
3730
  return `${rootSpacingPreludeCss(mapping2.increment)}
3668
3731
  ${body}`;
@@ -3695,6 +3758,11 @@ import * as t16 from "@babel/types";
3695
3758
  import { readFileSync as readFileSync3, writeFileSync, mkdirSync } from "fs";
3696
3759
  import { resolve as resolve3, join } from "path";
3697
3760
  function trussEsbuildPlugin(opts) {
3761
+ const diagnostics = {
3762
+ onDiagnostic(error) {
3763
+ throw error;
3764
+ }
3765
+ };
3698
3766
  const session = createTrussTransformSession({
3699
3767
  mappingPath() {
3700
3768
  return resolve3(process.cwd(), opts.mapping);
@@ -3710,11 +3778,11 @@ function trussEsbuildPlugin(opts) {
3710
3778
  build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, (args) => {
3711
3779
  const code = readFileSync3(args.path, "utf8");
3712
3780
  if (args.path.endsWith(".css.ts")) {
3713
- session.updateArbitraryCssRegistry(args.path, code);
3781
+ session.updateArbitraryCssRegistry(args.path, code, diagnostics);
3714
3782
  return { contents: code, loader: loaderForPath(args.path) };
3715
3783
  }
3716
3784
  if (!code.includes("Css") && !code.includes("css=")) return void 0;
3717
- const result = session.transformCode(code, args.path);
3785
+ const result = session.transformCode(code, args.path, diagnostics);
3718
3786
  if (!result) return void 0;
3719
3787
  return { contents: result.code, loader: loaderForPath(args.path) };
3720
3788
  });
@@ -3753,6 +3821,8 @@ function trussPlugin(opts) {
3753
3821
  let debug = false;
3754
3822
  let isTest = false;
3755
3823
  let isBuild = false;
3824
+ let annotate = true;
3825
+ let devSocket;
3756
3826
  const libraryPaths = opts.libraries ?? [];
3757
3827
  let emittedCssFileName = null;
3758
3828
  let cssVersion = 0;
@@ -3760,6 +3830,11 @@ function trussPlugin(opts) {
3760
3830
  function mappingPath() {
3761
3831
  return resolve4(projectRoot || process.cwd(), opts.mapping);
3762
3832
  }
3833
+ function diagnostics(context) {
3834
+ return {
3835
+ onDiagnostic: (error) => reportDiagnostic(context, error)
3836
+ };
3837
+ }
3763
3838
  const session = createTrussTransformSession({
3764
3839
  mappingPath,
3765
3840
  projectRoot: () => projectRoot || process.cwd(),
@@ -3776,6 +3851,7 @@ function trussPlugin(opts) {
3776
3851
  debug = config.command === "serve" || config.mode === "development" || config.mode === "test";
3777
3852
  isTest = config.mode === "test";
3778
3853
  isBuild = config.command === "build";
3854
+ annotate = !isBuild || Boolean(config.build?.lib);
3779
3855
  },
3780
3856
  buildStart() {
3781
3857
  session.ensureMapping();
@@ -3786,9 +3862,10 @@ function trussPlugin(opts) {
3786
3862
  // -- Dev mode HMR --
3787
3863
  configureServer(server) {
3788
3864
  if (isTest) return;
3865
+ devSocket = server.ws;
3789
3866
  server.middlewares.use((req, res, next) => {
3790
3867
  if (req.url !== VIRTUAL_CSS_ENDPOINT) return next();
3791
- const css = session.collectCss();
3868
+ const css = session.collectCss(annotate);
3792
3869
  res.setHeader("Content-Type", "text/css");
3793
3870
  res.setHeader("Cache-Control", "no-store");
3794
3871
  res.end(css);
@@ -3878,7 +3955,7 @@ __injectTrussCSS(${JSON.stringify(payload)});
3878
3955
  }
3879
3956
  if (id.startsWith(VIRTUAL_TEST_CSS_PREFIX)) {
3880
3957
  const sourcePath2 = canonicalSourcePath(id.slice(VIRTUAL_TEST_CSS_PREFIX.length));
3881
- session.updateArbitraryCssRegistry(sourcePath2, readFileSync4(sourcePath2, "utf8"));
3958
+ session.updateArbitraryCssRegistry(sourcePath2, readFileSync4(sourcePath2, "utf8"), diagnostics(this));
3882
3959
  const payload = {
3883
3960
  arbitraryRules: splitArbitraryCss(session.getArbitraryCss(sourcePath2)),
3884
3961
  source: sourcePath2
@@ -3893,7 +3970,7 @@ __injectTrussCSS(${JSON.stringify(payload)});
3893
3970
  if (!id.startsWith(VIRTUAL_CSS_PREFIX)) return null;
3894
3971
  const sourcePath = id.slice(VIRTUAL_CSS_PREFIX.length) + ".ts";
3895
3972
  const sourceCode = readFileSync4(sourcePath, "utf8");
3896
- session.updateArbitraryCssRegistry(sourcePath, sourceCode);
3973
+ session.updateArbitraryCssRegistry(sourcePath, sourceCode, diagnostics(this));
3897
3974
  return `/* [truss] ${sourcePath} \u2014 included via truss.css */`;
3898
3975
  },
3899
3976
  transform(code, id) {
@@ -3907,7 +3984,7 @@ __injectTrussCSS(${JSON.stringify(payload)});
3907
3984
  import "${VIRTUAL_TEST_CSS_ID}";` : rewrittenImports.code;
3908
3985
  const importsOnlyResult = rewrittenImports.changed || shouldBootstrapTestCss ? { code: transformedCode, map: null } : null;
3909
3986
  if (fileId.endsWith(".css.ts")) {
3910
- session.updateArbitraryCssRegistry(fileId, code);
3987
+ session.updateArbitraryCssRegistry(fileId, code, diagnostics(this));
3911
3988
  if (isTest) {
3912
3989
  const css = session.getArbitraryCss(fileId);
3913
3990
  return { code: appendTestCssInjection(transformedCode, fileId, css), map: null };
@@ -3916,13 +3993,13 @@ import "${VIRTUAL_TEST_CSS_ID}";` : rewrittenImports.code;
3916
3993
  }
3917
3994
  const hasCssDsl = rewrittenImports.code.includes("Css") || rewrittenImports.code.includes("css=");
3918
3995
  if (!hasCssDsl) return importsOnlyResult;
3919
- const result = session.transformCode(transformedCode, fileId, { debug, injectCss: isTest });
3996
+ const result = session.transformCode(transformedCode, fileId, { debug, injectCss: isTest, ...diagnostics(this) });
3920
3997
  return result ? { code: result.code, map: result.map } : importsOnlyResult;
3921
3998
  },
3922
3999
  // -- Production CSS emission --
3923
4000
  generateBundle(_options, _bundle) {
3924
4001
  if (!isBuild) return;
3925
- const css = session.collectCss();
4002
+ const css = session.collectCss(annotate);
3926
4003
  if (!css) return;
3927
4004
  const hash = createHash("sha256").update(css).digest("hex").slice(0, 8);
3928
4005
  const fileName = `assets/truss-${hash}.css`;
@@ -3947,6 +4024,16 @@ import "${VIRTUAL_TEST_CSS_ID}";` : rewrittenImports.code;
3947
4024
  }
3948
4025
  }
3949
4026
  };
4027
+ function reportDiagnostic(context, error) {
4028
+ if ((opts.unsupportedPattern ?? (isBuild ? "error" : "warn")) === "error") {
4029
+ throw error;
4030
+ }
4031
+ context.warn(error);
4032
+ devSocket?.send({
4033
+ type: "error",
4034
+ err: { message: error.message, stack: "", id: error.id, loc: error.loc, plugin: "truss" }
4035
+ });
4036
+ }
3950
4037
  }
3951
4038
  function resolveImportPath(source, importer, projectRoot) {
3952
4039
  if (isAbsolute(source)) {