@homebound/truss 2.29.8 → 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;
@@ -200,11 +200,9 @@ function toImportSpecifier(entry) {
200
200
  }
201
201
 
202
202
  // src/plugin/babel-utils.ts
203
- import _generate from "@babel/generator";
203
+ import generate from "@babel/generator";
204
204
  import { parse } from "@babel/parser";
205
- import _traverse from "@babel/traverse";
206
- var generate = _generate.default ?? _generate;
207
- var traverse = _traverse.default ?? _traverse;
205
+ import traverse from "@babel/traverse";
208
206
  function parseModule(code, filename) {
209
207
  return parse(code, {
210
208
  sourceType: "module",
@@ -332,11 +330,44 @@ function resetConditionContext(context) {
332
330
  Object.assign(context, emptyConditionContext());
333
331
  }
334
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
+
335
366
  // src/plugin/resolve-entry.ts
336
367
  function requireEntry(mapping, abbr) {
337
368
  const entry = mapping.abbreviations[abbr];
338
369
  if (!entry) {
339
- throw new UnsupportedPatternError(`Unknown abbreviation "${abbr}"`);
370
+ throw new UnknownAbbreviationError(abbr, Object.keys(mapping.abbreviations));
340
371
  }
341
372
  return entry;
342
373
  }
@@ -2735,12 +2766,28 @@ function getTopLevelVariableDeclaration(node) {
2735
2766
  return null;
2736
2767
  }
2737
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
+
2738
2782
  // src/plugin/transform-css.ts
2739
- function transformCssTs(code, filename, mapping) {
2783
+ function transformCssTs(code, filename, mapping, options = {}) {
2740
2784
  const ast = parseModule(code, filename);
2741
2785
  const cssBindingName = findCssImportBinding(ast);
2742
2786
  const cssExport = findNamedCssExportObject(ast);
2743
2787
  if (!cssExport) {
2788
+ options.onDiagnostic?.(
2789
+ new Diagnostic("expected `export const css = { ... }` with an object literal", filename, ast.program)
2790
+ );
2744
2791
  return `/* [truss] ${filename}: expected \`export const css = { ... }\` with an object literal */
2745
2792
  `;
2746
2793
  }
@@ -2748,16 +2795,16 @@ function transformCssTs(code, filename, mapping) {
2748
2795
  const stringBindings = collectStaticStringBindings(ast);
2749
2796
  for (const prop of cssExport.properties) {
2750
2797
  if (t12.isSpreadElement(prop)) {
2751
- rules.push(`/* [truss] unsupported: spread elements in css.ts export */`);
2798
+ rules.push(unsupported("spread elements in css.ts export", prop));
2752
2799
  continue;
2753
2800
  }
2754
2801
  if (!t12.isObjectProperty(prop)) {
2755
- rules.push(`/* [truss] unsupported: non-property in css.ts export */`);
2802
+ rules.push(unsupported("non-property in css.ts export", prop));
2756
2803
  continue;
2757
2804
  }
2758
2805
  const selector = objectPropertyStringKey(prop, stringBindings);
2759
2806
  if (selector === null) {
2760
- 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));
2761
2808
  continue;
2762
2809
  }
2763
2810
  const valueNode = prop.value;
@@ -2767,21 +2814,25 @@ function transformCssTs(code, filename, mapping) {
2767
2814
  continue;
2768
2815
  }
2769
2816
  if (!t12.isExpression(valueNode)) {
2770
- rules.push(`/* [truss] unsupported: "${selector}" value is not an expression */`);
2817
+ rules.push(unsupported(`"${selector}" value is not an expression`, valueNode));
2771
2818
  continue;
2772
2819
  }
2773
2820
  if (!cssBindingName) {
2774
- 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));
2775
2822
  continue;
2776
2823
  }
2777
- const cssResult = resolveCssExpression(valueNode, cssBindingName, mapping, filename);
2824
+ const cssResult = resolveCssExpression(valueNode, cssBindingName, mapping);
2778
2825
  if ("error" in cssResult) {
2779
- rules.push(`/* [truss] unsupported: "${selector}" \u2014 ${cssResult.error} */`);
2826
+ rules.push(unsupported(`"${selector}" \u2014 ${cssResult.error}`, valueNode));
2780
2827
  continue;
2781
2828
  }
2782
2829
  rules.push(formatCssRule(selector, cssResult.declarations));
2783
2830
  }
2784
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
+ }
2785
2836
  }
2786
2837
  function findNamedCssExportObject(ast) {
2787
2838
  for (const node of ast.program.body) {
@@ -2811,7 +2862,7 @@ function extractStaticStringValue(node, cssBindingName) {
2811
2862
  }
2812
2863
  return null;
2813
2864
  }
2814
- function resolveCssExpression(node, cssBindingName, mapping, filename) {
2865
+ function resolveCssExpression(node, cssBindingName, mapping) {
2815
2866
  const chain = extractDollarChain(node, cssBindingName);
2816
2867
  if (!chain) {
2817
2868
  return { error: "value must be a Css.*.$ expression" };
@@ -3377,10 +3428,18 @@ function transformTruss(code, filename, mapping, options = {}) {
3377
3428
  return;
3378
3429
  }
3379
3430
  const resolveCssChainReference2 = buildCssChainReferenceResolver(path, cssBindingName);
3380
- const resolvedChain = resolveFullChain({ mapping, cssBindingName, resolveCssChainReference: resolveCssChainReference2 }, chain);
3431
+ const resolvedChain = resolveFullChain(
3432
+ {
3433
+ mapping,
3434
+ cssBindingName,
3435
+ resolveCssChainReference: resolveCssChainReference2
3436
+ },
3437
+ chain
3438
+ );
3381
3439
  sites.push({ path, resolvedChain });
3382
3440
  const line = path.node.loc?.start.line ?? null;
3383
3441
  for (const err of resolvedChain.errors) {
3442
+ options.onDiagnostic?.(new Diagnostic(err, filename, path.node));
3384
3443
  errorMessages.push({ message: err, line });
3385
3444
  }
3386
3445
  },
@@ -3630,9 +3689,9 @@ function createTrussTransformSession(options) {
3630
3689
  arbitraryCssRegistry.clear();
3631
3690
  libraryCache = null;
3632
3691
  }
3633
- function updateArbitraryCssRegistry(sourcePath, sourceCode) {
3692
+ function updateArbitraryCssRegistry(sourcePath, sourceCode, diagnostics = {}) {
3634
3693
  sourcePath = resolve2(sourcePath).replace(/\\/g, "/");
3635
- const css = transformCssTs(sourceCode, sourcePath, ensureMapping()).trim();
3694
+ const css = transformCssTs(sourceCode, sourcePath, ensureMapping(), diagnostics).trim();
3636
3695
  if (css.length > 0) {
3637
3696
  const prev = arbitraryCssRegistry.get(sourcePath);
3638
3697
  arbitraryCssRegistry.set(sourcePath, css);
@@ -3697,6 +3756,11 @@ import * as t16 from "@babel/types";
3697
3756
  import { readFileSync as readFileSync3, writeFileSync, mkdirSync } from "fs";
3698
3757
  import { resolve as resolve3, join } from "path";
3699
3758
  function trussEsbuildPlugin(opts) {
3759
+ const diagnostics = {
3760
+ onDiagnostic(error) {
3761
+ throw error;
3762
+ }
3763
+ };
3700
3764
  const session = createTrussTransformSession({
3701
3765
  mappingPath() {
3702
3766
  return resolve3(process.cwd(), opts.mapping);
@@ -3712,11 +3776,11 @@ function trussEsbuildPlugin(opts) {
3712
3776
  build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, (args) => {
3713
3777
  const code = readFileSync3(args.path, "utf8");
3714
3778
  if (args.path.endsWith(".css.ts")) {
3715
- session.updateArbitraryCssRegistry(args.path, code);
3779
+ session.updateArbitraryCssRegistry(args.path, code, diagnostics);
3716
3780
  return { contents: code, loader: loaderForPath(args.path) };
3717
3781
  }
3718
3782
  if (!code.includes("Css") && !code.includes("css=")) return void 0;
3719
- const result = session.transformCode(code, args.path);
3783
+ const result = session.transformCode(code, args.path, diagnostics);
3720
3784
  if (!result) return void 0;
3721
3785
  return { contents: result.code, loader: loaderForPath(args.path) };
3722
3786
  });
@@ -3755,6 +3819,7 @@ function trussPlugin(opts) {
3755
3819
  let debug = false;
3756
3820
  let isTest = false;
3757
3821
  let isBuild = false;
3822
+ let devSocket;
3758
3823
  const libraryPaths = opts.libraries ?? [];
3759
3824
  let emittedCssFileName = null;
3760
3825
  let cssVersion = 0;
@@ -3762,6 +3827,11 @@ function trussPlugin(opts) {
3762
3827
  function mappingPath() {
3763
3828
  return resolve4(projectRoot || process.cwd(), opts.mapping);
3764
3829
  }
3830
+ function diagnostics(context) {
3831
+ return {
3832
+ onDiagnostic: (error) => reportDiagnostic(context, error)
3833
+ };
3834
+ }
3765
3835
  const session = createTrussTransformSession({
3766
3836
  mappingPath,
3767
3837
  projectRoot: () => projectRoot || process.cwd(),
@@ -3788,6 +3858,7 @@ function trussPlugin(opts) {
3788
3858
  // -- Dev mode HMR --
3789
3859
  configureServer(server) {
3790
3860
  if (isTest) return;
3861
+ devSocket = server.ws;
3791
3862
  server.middlewares.use((req, res, next) => {
3792
3863
  if (req.url !== VIRTUAL_CSS_ENDPOINT) return next();
3793
3864
  const css = session.collectCss();
@@ -3880,7 +3951,7 @@ __injectTrussCSS(${JSON.stringify(payload)});
3880
3951
  }
3881
3952
  if (id.startsWith(VIRTUAL_TEST_CSS_PREFIX)) {
3882
3953
  const sourcePath2 = canonicalSourcePath(id.slice(VIRTUAL_TEST_CSS_PREFIX.length));
3883
- session.updateArbitraryCssRegistry(sourcePath2, readFileSync4(sourcePath2, "utf8"));
3954
+ session.updateArbitraryCssRegistry(sourcePath2, readFileSync4(sourcePath2, "utf8"), diagnostics(this));
3884
3955
  const payload = {
3885
3956
  arbitraryRules: splitArbitraryCss(session.getArbitraryCss(sourcePath2)),
3886
3957
  source: sourcePath2
@@ -3895,7 +3966,7 @@ __injectTrussCSS(${JSON.stringify(payload)});
3895
3966
  if (!id.startsWith(VIRTUAL_CSS_PREFIX)) return null;
3896
3967
  const sourcePath = id.slice(VIRTUAL_CSS_PREFIX.length) + ".ts";
3897
3968
  const sourceCode = readFileSync4(sourcePath, "utf8");
3898
- session.updateArbitraryCssRegistry(sourcePath, sourceCode);
3969
+ session.updateArbitraryCssRegistry(sourcePath, sourceCode, diagnostics(this));
3899
3970
  return `/* [truss] ${sourcePath} \u2014 included via truss.css */`;
3900
3971
  },
3901
3972
  transform(code, id) {
@@ -3909,7 +3980,7 @@ __injectTrussCSS(${JSON.stringify(payload)});
3909
3980
  import "${VIRTUAL_TEST_CSS_ID}";` : rewrittenImports.code;
3910
3981
  const importsOnlyResult = rewrittenImports.changed || shouldBootstrapTestCss ? { code: transformedCode, map: null } : null;
3911
3982
  if (fileId.endsWith(".css.ts")) {
3912
- session.updateArbitraryCssRegistry(fileId, code);
3983
+ session.updateArbitraryCssRegistry(fileId, code, diagnostics(this));
3913
3984
  if (isTest) {
3914
3985
  const css = session.getArbitraryCss(fileId);
3915
3986
  return { code: appendTestCssInjection(transformedCode, fileId, css), map: null };
@@ -3918,7 +3989,7 @@ import "${VIRTUAL_TEST_CSS_ID}";` : rewrittenImports.code;
3918
3989
  }
3919
3990
  const hasCssDsl = rewrittenImports.code.includes("Css") || rewrittenImports.code.includes("css=");
3920
3991
  if (!hasCssDsl) return importsOnlyResult;
3921
- const result = session.transformCode(transformedCode, fileId, { debug, injectCss: isTest });
3992
+ const result = session.transformCode(transformedCode, fileId, { debug, injectCss: isTest, ...diagnostics(this) });
3922
3993
  return result ? { code: result.code, map: result.map } : importsOnlyResult;
3923
3994
  },
3924
3995
  // -- Production CSS emission --
@@ -3949,6 +4020,16 @@ import "${VIRTUAL_TEST_CSS_ID}";` : rewrittenImports.code;
3949
4020
  }
3950
4021
  }
3951
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
+ }
3952
4033
  }
3953
4034
  function resolveImportPath(source, importer, projectRoot) {
3954
4035
  if (isAbsolute(source)) {