@terpjs/eslint-boundaries 0.22.0 → 0.23.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terpjs/eslint-boundaries",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "type": "module",
5
5
  "description": "Terp frontend boundary rules (as data) + the ESLint adapter: no cross-module imports, no package internals, design-token-only styling (no style/className/module stylesheets), token-styled components for raw HTML tags, router-only in-app links, generated-client-only, and browser XSS/navigation sink bans. Strict-only (no modes); governed opt-outs via terp-allow markers + the escape-hatch budget ratchet (terp-boundaries-budget).",
6
6
  "main": "./src/index.js",
@@ -25,7 +25,7 @@
25
25
  "typescript-eslint": "^8.69.0"
26
26
  },
27
27
  "devDependencies": {
28
- "@terpjs/spec": "0.34.0",
28
+ "@terpjs/spec": "0.35.0",
29
29
  "eslint": "^10.10.0",
30
30
  "vitest": "^5.0.0"
31
31
  },
package/src/i18n.test.js CHANGED
@@ -144,6 +144,18 @@ describe("locale catalog completeness", () => {
144
144
  expect(dynamicTrans.find((message) => message.ruleId === "terp/locale-catalogs-complete")?.message)
145
145
  .toContain("requires static non-empty id and message");
146
146
 
147
+ // A spread is still refused - the catalog is inventoried statically - but it is the
148
+ // one shape with a sanctioned alternative, so the refusal names it. Sharing one
149
+ // descriptor between two screens is ordinary; only its JSX-body spelling is not.
150
+ const spreadTrans = await lintWithCatalog(
151
+ { sourceLocale: "en", locales: { en: {}, nl: {} } },
152
+ 'export const W = () => <Trans {...TITLE} />;',
153
+ );
154
+ const spreadMessage = spreadTrans
155
+ .find((message) => message.ruleId === "terp/locale-catalogs-complete")?.message;
156
+ expect(spreadMessage).toContain("useUiText");
157
+ expect(spreadMessage).toContain("text(DESCRIPTOR)");
158
+
147
159
  const staleAllowlist = await lintWithCatalog(
148
160
  {
149
161
  sourceLocale: "en",
package/src/index.js CHANGED
@@ -788,9 +788,23 @@ const localeCatalogsComplete = {
788
788
  if (jsxName(node.name) !== "Trans") return;
789
789
  const descriptor = transDescriptor(node);
790
790
  if (descriptor === null) {
791
+ // A spread is the one shape with a sanctioned alternative, and the generic
792
+ // message sent authors looking for a way to make the spread work instead.
793
+ // One descriptor shared by two screens is ordinary; what cannot be shared is
794
+ // the JSX-body spelling of it, because the catalog is inventoried statically
795
+ // and `{...DESCRIPTOR}` carries no attributes to read. The resolver renders
796
+ // the same copy from the same constant, so name it rather than the refusal.
797
+ const spread = node.attributes.some(
798
+ (attribute) => attribute.type === "JSXSpreadAttribute",
799
+ );
791
800
  context.report({
792
801
  node,
793
- message: "<Trans> requires static non-empty id and message attributes.",
802
+ message: spread
803
+ ? "<Trans> reads its id and message as static attributes, so a shared UiText " +
804
+ "constant cannot be spread into it. Render the same constant through the " +
805
+ "resolver instead: const text = useUiText() in the component, then " +
806
+ "{text(DESCRIPTOR)} where the copy goes."
807
+ : "<Trans> requires static non-empty id and message attributes.",
794
808
  });
795
809
  return;
796
810
  }
package/src/spec.js CHANGED
@@ -16,7 +16,7 @@
16
16
  * NOT by this package's own suite, which certification runs against candidate spec releases
17
17
  * whose version is allowed to be newer).
18
18
  */
19
- export const SPEC_VERSION = "0.34.0";
19
+ export const SPEC_VERSION = "0.35.0";
20
20
 
21
21
  export const BOUNDARY_SPEC = {
22
22
  /** Every app-authored TypeScript source file whose user-facing copy must be cataloged. */