@homebound/truss 2.29.9 → 2.29.10

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
  }
@@ -2733,12 +2766,28 @@ function getTopLevelVariableDeclaration(node) {
2733
2766
  return null;
2734
2767
  }
2735
2768
 
2769
+ // src/plugin/diagnostic.ts
2770
+ var Diagnostic = class extends Error {
2771
+ id;
2772
+ loc;
2773
+ constructor(message, filename, node) {
2774
+ const start = node.loc?.start;
2775
+ const location = start ? `${filename}:${start.line}:${start.column + 1}` : filename;
2776
+ super(`${location}: ${message}`);
2777
+ this.id = filename;
2778
+ this.loc = start ? { file: filename, line: start.line, column: start.column } : void 0;
2779
+ }
2780
+ };
2781
+
2736
2782
  // src/plugin/transform-css.ts
2737
- function transformCssTs(code, filename, mapping) {
2783
+ function transformCssTs(code, filename, mapping, options = {}) {
2738
2784
  const ast = parseModule(code, filename);
2739
2785
  const cssBindingName = findCssImportBinding(ast);
2740
2786
  const cssExport = findNamedCssExportObject(ast);
2741
2787
  if (!cssExport) {
2788
+ options.onDiagnostic?.(
2789
+ new Diagnostic("expected `export const css = { ... }` with an object literal", filename, ast.program)
2790
+ );
2742
2791
  return `/* [truss] ${filename}: expected \`export const css = { ... }\` with an object literal */
2743
2792
  `;
2744
2793
  }
@@ -2746,16 +2795,16 @@ function transformCssTs(code, filename, mapping) {
2746
2795
  const stringBindings = collectStaticStringBindings(ast);
2747
2796
  for (const prop of cssExport.properties) {
2748
2797
  if (t12.isSpreadElement(prop)) {
2749
- rules.push(`/* [truss] unsupported: spread elements in css.ts export */`);
2798
+ rules.push(unsupported("spread elements in css.ts export", prop));
2750
2799
  continue;
2751
2800
  }
2752
2801
  if (!t12.isObjectProperty(prop)) {
2753
- rules.push(`/* [truss] unsupported: non-property in css.ts export */`);
2802
+ rules.push(unsupported("non-property in css.ts export", prop));
2754
2803
  continue;
2755
2804
  }
2756
2805
  const selector = objectPropertyStringKey(prop, stringBindings);
2757
2806
  if (selector === null) {
2758
- rules.push(`/* [truss] unsupported: non-string-literal key in css.ts export */`);
2807
+ rules.push(unsupported("non-string-literal key in css.ts export", prop));
2759
2808
  continue;
2760
2809
  }
2761
2810
  const valueNode = prop.value;
@@ -2765,21 +2814,25 @@ function transformCssTs(code, filename, mapping) {
2765
2814
  continue;
2766
2815
  }
2767
2816
  if (!t12.isExpression(valueNode)) {
2768
- rules.push(`/* [truss] unsupported: "${selector}" value is not an expression */`);
2817
+ rules.push(unsupported(`"${selector}" value is not an expression`, valueNode));
2769
2818
  continue;
2770
2819
  }
2771
2820
  if (!cssBindingName) {
2772
- rules.push(`/* [truss] unsupported: "${selector}" \u2014 Css.*.$ chain requires a Css import */`);
2821
+ rules.push(unsupported(`"${selector}" \u2014 Css.*.$ chain requires a Css import`, valueNode));
2773
2822
  continue;
2774
2823
  }
2775
- const cssResult = resolveCssExpression(valueNode, cssBindingName, mapping, filename);
2824
+ const cssResult = resolveCssExpression(valueNode, cssBindingName, mapping);
2776
2825
  if ("error" in cssResult) {
2777
- rules.push(`/* [truss] unsupported: "${selector}" \u2014 ${cssResult.error} */`);
2826
+ rules.push(unsupported(`"${selector}" \u2014 ${cssResult.error}`, valueNode));
2778
2827
  continue;
2779
2828
  }
2780
2829
  rules.push(formatCssRule(selector, cssResult.declarations));
2781
2830
  }
2782
2831
  return rules.join("\n\n") + "\n";
2832
+ function unsupported(message, node) {
2833
+ options.onDiagnostic?.(new Diagnostic(message, filename, node));
2834
+ return `/* [truss] unsupported: ${message} */`;
2835
+ }
2783
2836
  }
2784
2837
  function findNamedCssExportObject(ast) {
2785
2838
  for (const node of ast.program.body) {
@@ -2809,7 +2862,7 @@ function extractStaticStringValue(node, cssBindingName) {
2809
2862
  }
2810
2863
  return null;
2811
2864
  }
2812
- function resolveCssExpression(node, cssBindingName, mapping, filename) {
2865
+ function resolveCssExpression(node, cssBindingName, mapping) {
2813
2866
  const chain = extractDollarChain(node, cssBindingName);
2814
2867
  if (!chain) {
2815
2868
  return { error: "value must be a Css.*.$ expression" };
@@ -3375,10 +3428,18 @@ function transformTruss(code, filename, mapping, options = {}) {
3375
3428
  return;
3376
3429
  }
3377
3430
  const resolveCssChainReference2 = buildCssChainReferenceResolver(path, cssBindingName);
3378
- const resolvedChain = resolveFullChain({ mapping, cssBindingName, resolveCssChainReference: resolveCssChainReference2 }, chain);
3431
+ const resolvedChain = resolveFullChain(
3432
+ {
3433
+ mapping,
3434
+ cssBindingName,
3435
+ resolveCssChainReference: resolveCssChainReference2
3436
+ },
3437
+ chain
3438
+ );
3379
3439
  sites.push({ path, resolvedChain });
3380
3440
  const line = path.node.loc?.start.line ?? null;
3381
3441
  for (const err of resolvedChain.errors) {
3442
+ options.onDiagnostic?.(new Diagnostic(err, filename, path.node));
3382
3443
  errorMessages.push({ message: err, line });
3383
3444
  }
3384
3445
  },
@@ -3628,9 +3689,9 @@ function createTrussTransformSession(options) {
3628
3689
  arbitraryCssRegistry.clear();
3629
3690
  libraryCache = null;
3630
3691
  }
3631
- function updateArbitraryCssRegistry(sourcePath, sourceCode) {
3692
+ function updateArbitraryCssRegistry(sourcePath, sourceCode, diagnostics = {}) {
3632
3693
  sourcePath = resolve2(sourcePath).replace(/\\/g, "/");
3633
- const css = transformCssTs(sourceCode, sourcePath, ensureMapping()).trim();
3694
+ const css = transformCssTs(sourceCode, sourcePath, ensureMapping(), diagnostics).trim();
3634
3695
  if (css.length > 0) {
3635
3696
  const prev = arbitraryCssRegistry.get(sourcePath);
3636
3697
  arbitraryCssRegistry.set(sourcePath, css);
@@ -3695,6 +3756,11 @@ import * as t16 from "@babel/types";
3695
3756
  import { readFileSync as readFileSync3, writeFileSync, mkdirSync } from "fs";
3696
3757
  import { resolve as resolve3, join } from "path";
3697
3758
  function trussEsbuildPlugin(opts) {
3759
+ const diagnostics = {
3760
+ onDiagnostic(error) {
3761
+ throw error;
3762
+ }
3763
+ };
3698
3764
  const session = createTrussTransformSession({
3699
3765
  mappingPath() {
3700
3766
  return resolve3(process.cwd(), opts.mapping);
@@ -3710,11 +3776,11 @@ function trussEsbuildPlugin(opts) {
3710
3776
  build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, (args) => {
3711
3777
  const code = readFileSync3(args.path, "utf8");
3712
3778
  if (args.path.endsWith(".css.ts")) {
3713
- session.updateArbitraryCssRegistry(args.path, code);
3779
+ session.updateArbitraryCssRegistry(args.path, code, diagnostics);
3714
3780
  return { contents: code, loader: loaderForPath(args.path) };
3715
3781
  }
3716
3782
  if (!code.includes("Css") && !code.includes("css=")) return void 0;
3717
- const result = session.transformCode(code, args.path);
3783
+ const result = session.transformCode(code, args.path, diagnostics);
3718
3784
  if (!result) return void 0;
3719
3785
  return { contents: result.code, loader: loaderForPath(args.path) };
3720
3786
  });
@@ -3753,6 +3819,7 @@ function trussPlugin(opts) {
3753
3819
  let debug = false;
3754
3820
  let isTest = false;
3755
3821
  let isBuild = false;
3822
+ let devSocket;
3756
3823
  const libraryPaths = opts.libraries ?? [];
3757
3824
  let emittedCssFileName = null;
3758
3825
  let cssVersion = 0;
@@ -3760,6 +3827,11 @@ function trussPlugin(opts) {
3760
3827
  function mappingPath() {
3761
3828
  return resolve4(projectRoot || process.cwd(), opts.mapping);
3762
3829
  }
3830
+ function diagnostics(context) {
3831
+ return {
3832
+ onDiagnostic: (error) => reportDiagnostic(context, error)
3833
+ };
3834
+ }
3763
3835
  const session = createTrussTransformSession({
3764
3836
  mappingPath,
3765
3837
  projectRoot: () => projectRoot || process.cwd(),
@@ -3786,6 +3858,7 @@ function trussPlugin(opts) {
3786
3858
  // -- Dev mode HMR --
3787
3859
  configureServer(server) {
3788
3860
  if (isTest) return;
3861
+ devSocket = server.ws;
3789
3862
  server.middlewares.use((req, res, next) => {
3790
3863
  if (req.url !== VIRTUAL_CSS_ENDPOINT) return next();
3791
3864
  const css = session.collectCss();
@@ -3878,7 +3951,7 @@ __injectTrussCSS(${JSON.stringify(payload)});
3878
3951
  }
3879
3952
  if (id.startsWith(VIRTUAL_TEST_CSS_PREFIX)) {
3880
3953
  const sourcePath2 = canonicalSourcePath(id.slice(VIRTUAL_TEST_CSS_PREFIX.length));
3881
- session.updateArbitraryCssRegistry(sourcePath2, readFileSync4(sourcePath2, "utf8"));
3954
+ session.updateArbitraryCssRegistry(sourcePath2, readFileSync4(sourcePath2, "utf8"), diagnostics(this));
3882
3955
  const payload = {
3883
3956
  arbitraryRules: splitArbitraryCss(session.getArbitraryCss(sourcePath2)),
3884
3957
  source: sourcePath2
@@ -3893,7 +3966,7 @@ __injectTrussCSS(${JSON.stringify(payload)});
3893
3966
  if (!id.startsWith(VIRTUAL_CSS_PREFIX)) return null;
3894
3967
  const sourcePath = id.slice(VIRTUAL_CSS_PREFIX.length) + ".ts";
3895
3968
  const sourceCode = readFileSync4(sourcePath, "utf8");
3896
- session.updateArbitraryCssRegistry(sourcePath, sourceCode);
3969
+ session.updateArbitraryCssRegistry(sourcePath, sourceCode, diagnostics(this));
3897
3970
  return `/* [truss] ${sourcePath} \u2014 included via truss.css */`;
3898
3971
  },
3899
3972
  transform(code, id) {
@@ -3907,7 +3980,7 @@ __injectTrussCSS(${JSON.stringify(payload)});
3907
3980
  import "${VIRTUAL_TEST_CSS_ID}";` : rewrittenImports.code;
3908
3981
  const importsOnlyResult = rewrittenImports.changed || shouldBootstrapTestCss ? { code: transformedCode, map: null } : null;
3909
3982
  if (fileId.endsWith(".css.ts")) {
3910
- session.updateArbitraryCssRegistry(fileId, code);
3983
+ session.updateArbitraryCssRegistry(fileId, code, diagnostics(this));
3911
3984
  if (isTest) {
3912
3985
  const css = session.getArbitraryCss(fileId);
3913
3986
  return { code: appendTestCssInjection(transformedCode, fileId, css), map: null };
@@ -3916,7 +3989,7 @@ import "${VIRTUAL_TEST_CSS_ID}";` : rewrittenImports.code;
3916
3989
  }
3917
3990
  const hasCssDsl = rewrittenImports.code.includes("Css") || rewrittenImports.code.includes("css=");
3918
3991
  if (!hasCssDsl) return importsOnlyResult;
3919
- const result = session.transformCode(transformedCode, fileId, { debug, injectCss: isTest });
3992
+ const result = session.transformCode(transformedCode, fileId, { debug, injectCss: isTest, ...diagnostics(this) });
3920
3993
  return result ? { code: result.code, map: result.map } : importsOnlyResult;
3921
3994
  },
3922
3995
  // -- Production CSS emission --
@@ -3947,6 +4020,16 @@ import "${VIRTUAL_TEST_CSS_ID}";` : rewrittenImports.code;
3947
4020
  }
3948
4021
  }
3949
4022
  };
4023
+ function reportDiagnostic(context, error) {
4024
+ if ((opts.unsupportedPattern ?? (isBuild ? "error" : "warn")) === "error") {
4025
+ throw error;
4026
+ }
4027
+ context.warn(error);
4028
+ devSocket?.send({
4029
+ type: "error",
4030
+ err: { message: error.message, stack: "", id: error.id, loc: error.loc, plugin: "truss" }
4031
+ });
4032
+ }
3950
4033
  }
3951
4034
  function resolveImportPath(source, importer, projectRoot) {
3952
4035
  if (isAbsolute(source)) {