@colixsystems/widget-sdk 0.36.0 → 0.37.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/README.md CHANGED
@@ -47,7 +47,11 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
47
47
 
48
48
  ## Status
49
49
 
50
- `v0.36.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
50
+ `v0.37.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
51
+
52
+ ### What's new in 0.37.0
53
+
54
+ **`react/jsx-runtime` + `react/jsx-dev-runtime` are now vetted imports.** A widget bundle compiled with React's *automatic* JSX runtime (Vite/esbuild's default) emits `import { jsx, jsxs, Fragment } from "react/jsx-runtime"` — code the author never writes by hand. The runtime already treated these as host-provided (the web loader shims both, the AI-agent sandbox stubs them, and the Developer guide documents them as externalized), but the linter's vetted list did not list them, so such a bundle failed publish static analysis with `import-not-vetted` on `react/jsx-runtime`. Both are now on `CONTRACT.vettedImports` as `core` subpaths of the already-vetted `react`. **`CONTRACT.version` → `1.27.0`** (additive: two vetted core subpaths). No existing entry changed shape.
51
55
 
52
56
  ### What's new in 0.36.0
53
57
 
package/dist/contract.cjs CHANGED
@@ -1057,6 +1057,20 @@ const VETTED_IMPORTS = [
1057
1057
  category: "core",
1058
1058
  description: "React. Hooks, JSX, lifecycle. Unchanged.",
1059
1059
  },
1060
+ {
1061
+ specifier: "react/jsx-runtime",
1062
+ platforms: ["web", "native"],
1063
+ category: "core",
1064
+ description:
1065
+ "React's automatic JSX runtime (jsx/jsxs/Fragment). Not hand-written — the JSX transform injects this import when a bundle is compiled with the automatic runtime. Host-provided on both platforms (the web loader shims it; Metro resolves the real subpath of react), so it is externalized exactly like react.",
1066
+ },
1067
+ {
1068
+ specifier: "react/jsx-dev-runtime",
1069
+ platforms: ["web", "native"],
1070
+ category: "core",
1071
+ description:
1072
+ "React's automatic JSX dev runtime (jsxDEV/Fragment). The development-mode counterpart to react/jsx-runtime, injected by the JSX transform in dev builds. Host-provided on both platforms, externalized like react.",
1073
+ },
1060
1074
  {
1061
1075
  specifier: "@colixsystems/widget-sdk",
1062
1076
  platforms: ["web", "native"],
@@ -1459,7 +1473,20 @@ const CONTRACT = deepFreeze({
1459
1473
  // `users.write:*`. New linter rule `scope-required-for-user-delete`
1460
1474
  // flags a widget that calls `.remove()` without declaring the scope.
1461
1475
  // No hook signature changed; `useUsers().remove` is unchanged.
1462
- version: "1.26.0",
1476
+ //
1477
+ // 1.27.0: additive — the vetted import allowlist gains `react/jsx-runtime`
1478
+ // and `react/jsx-dev-runtime`. These are subpaths of the already-vetted
1479
+ // `react`, injected automatically by the JSX transform when a bundle is
1480
+ // compiled with the automatic JSX runtime (Vite/esbuild's default). The
1481
+ // runtime already treats them as host-provided (the web loader shims both
1482
+ // in widgetLoader.js, the AI-agent sandbox stubs them, and the Developer
1483
+ // guide documents them as externalized), but the linter's vetted list did
1484
+ // not list them — so a first-party/marketplace bundle built with the
1485
+ // automatic runtime failed publish static analysis with `import-not-vetted`
1486
+ // on `react/jsx-runtime`. Adding them converges the linter onto the same
1487
+ // contract every other surface already honoured (CLAUDE.md §3). No
1488
+ // existing entry changed shape — minor bump on the pre-1.0 channel.
1489
+ version: "1.27.0",
1463
1490
  hooks: HOOKS,
1464
1491
  primitives: PRIMITIVES,
1465
1492
  manifestSchema: MANIFEST_SCHEMA,
package/dist/contract.js CHANGED
@@ -1057,6 +1057,20 @@ const VETTED_IMPORTS = [
1057
1057
  category: "core",
1058
1058
  description: "React. Hooks, JSX, lifecycle. Unchanged.",
1059
1059
  },
1060
+ {
1061
+ specifier: "react/jsx-runtime",
1062
+ platforms: ["web", "native"],
1063
+ category: "core",
1064
+ description:
1065
+ "React's automatic JSX runtime (jsx/jsxs/Fragment). Not hand-written — the JSX transform injects this import when a bundle is compiled with the automatic runtime. Host-provided on both platforms (the web loader shims it; Metro resolves the real subpath of react), so it is externalized exactly like react.",
1066
+ },
1067
+ {
1068
+ specifier: "react/jsx-dev-runtime",
1069
+ platforms: ["web", "native"],
1070
+ category: "core",
1071
+ description:
1072
+ "React's automatic JSX dev runtime (jsxDEV/Fragment). The development-mode counterpart to react/jsx-runtime, injected by the JSX transform in dev builds. Host-provided on both platforms, externalized like react.",
1073
+ },
1060
1074
  {
1061
1075
  specifier: "@colixsystems/widget-sdk",
1062
1076
  platforms: ["web", "native"],
@@ -1459,7 +1473,20 @@ const CONTRACT = deepFreeze({
1459
1473
  // `users.write:*`. New linter rule `scope-required-for-user-delete`
1460
1474
  // flags a widget that calls `.remove()` without declaring the scope.
1461
1475
  // No hook signature changed; `useUsers().remove` is unchanged.
1462
- version: "1.26.0",
1476
+ //
1477
+ // 1.27.0: additive — the vetted import allowlist gains `react/jsx-runtime`
1478
+ // and `react/jsx-dev-runtime`. These are subpaths of the already-vetted
1479
+ // `react`, injected automatically by the JSX transform when a bundle is
1480
+ // compiled with the automatic JSX runtime (Vite/esbuild's default). The
1481
+ // runtime already treats them as host-provided (the web loader shims both
1482
+ // in widgetLoader.js, the AI-agent sandbox stubs them, and the Developer
1483
+ // guide documents them as externalized), but the linter's vetted list did
1484
+ // not list them — so a first-party/marketplace bundle built with the
1485
+ // automatic runtime failed publish static analysis with `import-not-vetted`
1486
+ // on `react/jsx-runtime`. Adding them converges the linter onto the same
1487
+ // contract every other surface already honoured (CLAUDE.md §3). No
1488
+ // existing entry changed shape — minor bump on the pre-1.0 channel.
1489
+ version: "1.27.0",
1463
1490
  hooks: HOOKS,
1464
1491
  primitives: PRIMITIVES,
1465
1492
  manifestSchema: MANIFEST_SCHEMA,
package/dist/linter.cjs CHANGED
@@ -54,6 +54,149 @@ const CONTRACT_RULES = CONTRACT.bannedApis.map((b) =>
54
54
  _ruleForIdentifier(b.identifier, b.reason),
55
55
  );
56
56
 
57
+ // Replace the *content* of comments and string / template literals with
58
+ // spaces so the banned-identifier scan only ever sees executable code. A
59
+ // banned host-escape identifier (`window`, `document`, `eval`, `process`, …)
60
+ // is only dangerous as a real identifier reference — never as prose in a
61
+ // `//` comment or as character data inside a string — so matching the bare
62
+ // word there is a false positive that blocks an otherwise-clean widget (a
63
+ // comment that reads "the hour window the grid renders" must not trip
64
+ // `no-window`).
65
+ //
66
+ // Newlines are preserved verbatim so reported line numbers still line up
67
+ // with the original source. Template-literal `${ … }` expression holes are
68
+ // left intact: real code lives there and must still be scanned (`${window}`
69
+ // is a genuine escape). Backslash escapes inside strings/templates are
70
+ // consumed so an escaped quote (`"\""`) doesn't end the literal early.
71
+ function _stripNonCode(source) {
72
+ let out = "";
73
+ const n = source.length;
74
+ let mode = "code"; // code | line | block | sq | dq | tmpl
75
+ // Brace depth, plus a stack of the depths at which an enclosing template
76
+ // literal resumes — lets a `${ … }` hole (which may itself contain `{}`,
77
+ // strings, or nested templates) be told apart from the literal text.
78
+ let braceDepth = 0;
79
+ const tmplStack = [];
80
+ const keep = (ch) => {
81
+ out += ch;
82
+ };
83
+ const blank = (ch) => {
84
+ out += ch === "\n" || ch === "\r" ? ch : " ";
85
+ };
86
+ let i = 0;
87
+ while (i < n) {
88
+ const ch = source[i];
89
+ const nx = source[i + 1];
90
+ if (mode === "code") {
91
+ if (ch === "/" && nx === "/") {
92
+ mode = "line";
93
+ blank(ch);
94
+ blank(nx);
95
+ i += 2;
96
+ } else if (ch === "/" && nx === "*") {
97
+ mode = "block";
98
+ blank(ch);
99
+ blank(nx);
100
+ i += 2;
101
+ } else if (ch === "'") {
102
+ mode = "sq";
103
+ blank(ch);
104
+ i += 1;
105
+ } else if (ch === '"') {
106
+ mode = "dq";
107
+ blank(ch);
108
+ i += 1;
109
+ } else if (ch === "`") {
110
+ mode = "tmpl";
111
+ blank(ch);
112
+ i += 1;
113
+ } else if (ch === "{") {
114
+ braceDepth += 1;
115
+ keep(ch);
116
+ i += 1;
117
+ } else if (ch === "}") {
118
+ braceDepth -= 1;
119
+ if (
120
+ tmplStack.length > 0 &&
121
+ tmplStack[tmplStack.length - 1] === braceDepth
122
+ ) {
123
+ tmplStack.pop();
124
+ mode = "tmpl";
125
+ blank(ch);
126
+ } else {
127
+ keep(ch);
128
+ }
129
+ i += 1;
130
+ } else {
131
+ keep(ch);
132
+ i += 1;
133
+ }
134
+ } else if (mode === "line") {
135
+ if (ch === "\n") {
136
+ mode = "code";
137
+ keep(ch);
138
+ } else {
139
+ blank(ch);
140
+ }
141
+ i += 1;
142
+ } else if (mode === "block") {
143
+ if (ch === "*" && nx === "/") {
144
+ mode = "code";
145
+ blank(ch);
146
+ blank(nx);
147
+ i += 2;
148
+ } else {
149
+ blank(ch);
150
+ i += 1;
151
+ }
152
+ } else if (mode === "sq" || mode === "dq") {
153
+ const quote = mode === "sq" ? "'" : '"';
154
+ if (ch === "\\") {
155
+ blank(ch);
156
+ if (i + 1 < n) blank(nx);
157
+ i += 2;
158
+ } else if (ch === quote) {
159
+ mode = "code";
160
+ blank(ch);
161
+ i += 1;
162
+ } else if (ch === "\n") {
163
+ // A bare newline terminates an unterminated string in JS; bail back
164
+ // to code so malformed input can't blank the rest of the file.
165
+ mode = "code";
166
+ keep(ch);
167
+ i += 1;
168
+ } else {
169
+ blank(ch);
170
+ i += 1;
171
+ }
172
+ } else {
173
+ // mode === "tmpl"
174
+ if (ch === "\\") {
175
+ blank(ch);
176
+ if (i + 1 < n) blank(nx);
177
+ i += 2;
178
+ } else if (ch === "`") {
179
+ mode = "code";
180
+ blank(ch);
181
+ i += 1;
182
+ } else if (ch === "$" && nx === "{") {
183
+ // Enter an expression hole. Remember the brace depth the template
184
+ // resumes at, then count the `{` so its matching `}` is recognised.
185
+ tmplStack.push(braceDepth);
186
+ braceDepth += 1;
187
+ mode = "code";
188
+ keep(ch);
189
+ keep(nx);
190
+ i += 2;
191
+ } else {
192
+ blank(ch);
193
+ i += 1;
194
+ }
195
+ }
196
+ }
197
+ return out;
198
+ }
199
+
57
200
  // REQ-WSDK-PLATFORM: `no-axios-import` is GONE. axios is on the vetted
58
201
  // import list now (`CONTRACT.vettedImports`). See linter.js for the
59
202
  // source-of-truth comment.
@@ -387,16 +530,22 @@ function lintSource(source, options) {
387
530
  }
388
531
  const findings = [];
389
532
  const lines = source.split(/\r?\n/);
533
+ // Scan code with comments + string/template text blanked out so a banned
534
+ // identifier only fires on an actual code reference, not on the same word
535
+ // appearing in prose or string data. `codeLines` lines up 1:1 with `lines`
536
+ // (masking preserves newlines), so the reported snippet still comes from
537
+ // the original source.
538
+ const codeLines = _stripNonCode(source).split(/\r?\n/);
390
539
  for (let i = 0; i < lines.length; i++) {
391
- const line = lines[i];
540
+ const codeLine = codeLines[i];
392
541
  for (const rule of RULES) {
393
- if (rule.pattern.test(line)) {
542
+ if (rule.pattern.test(codeLine)) {
394
543
  findings.push({
395
544
  rule: rule.id,
396
545
  severity: "error",
397
546
  label: rule.label,
398
547
  line: i + 1,
399
- snippet: line.trim().slice(0, 200),
548
+ snippet: lines[i].trim().slice(0, 200),
400
549
  });
401
550
  }
402
551
  }
package/dist/linter.js CHANGED
@@ -55,6 +55,149 @@ const CONTRACT_RULES = CONTRACT.bannedApis.map((b) =>
55
55
  _ruleForIdentifier(b.identifier, b.reason),
56
56
  );
57
57
 
58
+ // Replace the *content* of comments and string / template literals with
59
+ // spaces so the banned-identifier scan only ever sees executable code. A
60
+ // banned host-escape identifier (`window`, `document`, `eval`, `process`, …)
61
+ // is only dangerous as a real identifier reference — never as prose in a
62
+ // `//` comment or as character data inside a string — so matching the bare
63
+ // word there is a false positive that blocks an otherwise-clean widget (a
64
+ // comment that reads "the hour window the grid renders" must not trip
65
+ // `no-window`).
66
+ //
67
+ // Newlines are preserved verbatim so reported line numbers still line up
68
+ // with the original source. Template-literal `${ … }` expression holes are
69
+ // left intact: real code lives there and must still be scanned (`${window}`
70
+ // is a genuine escape). Backslash escapes inside strings/templates are
71
+ // consumed so an escaped quote (`"\""`) doesn't end the literal early.
72
+ function _stripNonCode(source) {
73
+ let out = "";
74
+ const n = source.length;
75
+ let mode = "code"; // code | line | block | sq | dq | tmpl
76
+ // Brace depth, plus a stack of the depths at which an enclosing template
77
+ // literal resumes — lets a `${ … }` hole (which may itself contain `{}`,
78
+ // strings, or nested templates) be told apart from the literal text.
79
+ let braceDepth = 0;
80
+ const tmplStack = [];
81
+ const keep = (ch) => {
82
+ out += ch;
83
+ };
84
+ const blank = (ch) => {
85
+ out += ch === "\n" || ch === "\r" ? ch : " ";
86
+ };
87
+ let i = 0;
88
+ while (i < n) {
89
+ const ch = source[i];
90
+ const nx = source[i + 1];
91
+ if (mode === "code") {
92
+ if (ch === "/" && nx === "/") {
93
+ mode = "line";
94
+ blank(ch);
95
+ blank(nx);
96
+ i += 2;
97
+ } else if (ch === "/" && nx === "*") {
98
+ mode = "block";
99
+ blank(ch);
100
+ blank(nx);
101
+ i += 2;
102
+ } else if (ch === "'") {
103
+ mode = "sq";
104
+ blank(ch);
105
+ i += 1;
106
+ } else if (ch === '"') {
107
+ mode = "dq";
108
+ blank(ch);
109
+ i += 1;
110
+ } else if (ch === "`") {
111
+ mode = "tmpl";
112
+ blank(ch);
113
+ i += 1;
114
+ } else if (ch === "{") {
115
+ braceDepth += 1;
116
+ keep(ch);
117
+ i += 1;
118
+ } else if (ch === "}") {
119
+ braceDepth -= 1;
120
+ if (
121
+ tmplStack.length > 0 &&
122
+ tmplStack[tmplStack.length - 1] === braceDepth
123
+ ) {
124
+ tmplStack.pop();
125
+ mode = "tmpl";
126
+ blank(ch);
127
+ } else {
128
+ keep(ch);
129
+ }
130
+ i += 1;
131
+ } else {
132
+ keep(ch);
133
+ i += 1;
134
+ }
135
+ } else if (mode === "line") {
136
+ if (ch === "\n") {
137
+ mode = "code";
138
+ keep(ch);
139
+ } else {
140
+ blank(ch);
141
+ }
142
+ i += 1;
143
+ } else if (mode === "block") {
144
+ if (ch === "*" && nx === "/") {
145
+ mode = "code";
146
+ blank(ch);
147
+ blank(nx);
148
+ i += 2;
149
+ } else {
150
+ blank(ch);
151
+ i += 1;
152
+ }
153
+ } else if (mode === "sq" || mode === "dq") {
154
+ const quote = mode === "sq" ? "'" : '"';
155
+ if (ch === "\\") {
156
+ blank(ch);
157
+ if (i + 1 < n) blank(nx);
158
+ i += 2;
159
+ } else if (ch === quote) {
160
+ mode = "code";
161
+ blank(ch);
162
+ i += 1;
163
+ } else if (ch === "\n") {
164
+ // A bare newline terminates an unterminated string in JS; bail back
165
+ // to code so malformed input can't blank the rest of the file.
166
+ mode = "code";
167
+ keep(ch);
168
+ i += 1;
169
+ } else {
170
+ blank(ch);
171
+ i += 1;
172
+ }
173
+ } else {
174
+ // mode === "tmpl"
175
+ if (ch === "\\") {
176
+ blank(ch);
177
+ if (i + 1 < n) blank(nx);
178
+ i += 2;
179
+ } else if (ch === "`") {
180
+ mode = "code";
181
+ blank(ch);
182
+ i += 1;
183
+ } else if (ch === "$" && nx === "{") {
184
+ // Enter an expression hole. Remember the brace depth the template
185
+ // resumes at, then count the `{` so its matching `}` is recognised.
186
+ tmplStack.push(braceDepth);
187
+ braceDepth += 1;
188
+ mode = "code";
189
+ keep(ch);
190
+ keep(nx);
191
+ i += 2;
192
+ } else {
193
+ blank(ch);
194
+ i += 1;
195
+ }
196
+ }
197
+ }
198
+ return out;
199
+ }
200
+
58
201
  // Extra rules that don't map 1:1 to a banned identifier in the contract:
59
202
  // host-internal imports that widgets must never touch.
60
203
  //
@@ -124,9 +267,7 @@ function _classifySpecifier(spec) {
124
267
 
125
268
  function _importRules(source, manifest) {
126
269
  const findings = [];
127
- const allowed = new Map(
128
- CONTRACT.vettedImports.map((v) => [v.specifier, v]),
129
- );
270
+ const allowed = new Map(CONTRACT.vettedImports.map((v) => [v.specifier, v]));
130
271
  // Track declared `supportedPlatforms` so a widget that claims "web only"
131
272
  // doesn't import a native-only package (and vice versa) without the
132
273
  // marketplace listing being honest about which platforms ship.
@@ -258,7 +399,12 @@ const USER_MUTATION_METHODS = ["invite", "deactivate", "reactivate"];
258
399
  // useUsers().remove() must declare `users.delete:*` so the static contract
259
400
  // matches the backend gate on DELETE /api/v1/app/users/:userId.
260
401
  const USER_DELETE_METHODS = ["remove"];
261
- const GROUP_MUTATION_METHODS = ["create", "remove", "addMember", "removeMember"];
402
+ const GROUP_MUTATION_METHODS = [
403
+ "create",
404
+ "remove",
405
+ "addMember",
406
+ "removeMember",
407
+ ];
262
408
  const SKIP_COMMENT = /\/\/[^\n]*@appstudio-skip-scope-check/;
263
409
 
264
410
  function _scopeRules(source, manifest) {
@@ -277,8 +423,7 @@ function _scopeRules(source, manifest) {
277
423
  if (!reads) {
278
424
  findings.push({
279
425
  rule: "scope-required-for-useUsers",
280
- label:
281
- "useUsers() requires `users.read:*` in manifest.requestedScopes",
426
+ label: "useUsers() requires `users.read:*` in manifest.requestedScopes",
282
427
  line: 0,
283
428
  snippet: "",
284
429
  });
@@ -308,10 +453,7 @@ function _scopeRules(source, manifest) {
308
453
  for (const m of USER_MUTATION_METHODS) {
309
454
  const re = new RegExp(`\\.${m}\\s*\\(`);
310
455
  if (re.test(line)) {
311
- if (
312
- !declared.has("users.write:*") &&
313
- !declared.has("users.write")
314
- ) {
456
+ if (!declared.has("users.write:*") && !declared.has("users.write")) {
315
457
  findings.push({
316
458
  rule: "scope-required-for-user-mutation",
317
459
  label: `useUsers().${m}() requires \`users.write:*\` in manifest.requestedScopes`,
@@ -400,7 +542,13 @@ function _manifestActionRules(manifest) {
400
542
  const validTriggers = new Set(CONTRACT.actionTriggerTypes);
401
543
  const maxBytes = CONTRACT.actionScriptMaxBytes;
402
544
  const push = (label) =>
403
- findings.push({ rule: "manifest-action", severity: "error", label, line: 0, snippet: "" });
545
+ findings.push({
546
+ rule: "manifest-action",
547
+ severity: "error",
548
+ label,
549
+ line: 0,
550
+ snippet: "",
551
+ });
404
552
  if (!Array.isArray(manifest.actions)) {
405
553
  push("manifest.actions must be an array (omit it or use [] for none)");
406
554
  return findings;
@@ -422,9 +570,16 @@ function _manifestActionRules(manifest) {
422
570
  push("manifest.actions[].name must be a non-empty string");
423
571
  }
424
572
  if (!validTriggers.has(a.triggerType)) {
425
- push(`manifest.actions[].triggerType must be one of ${[...validTriggers].join(", ")}`);
426
- } else if (a.triggerType === "schedule" && (typeof a.scheduleCron !== "string" || !a.scheduleCron)) {
427
- push("manifest.actions[].scheduleCron is required when triggerType is 'schedule'");
573
+ push(
574
+ `manifest.actions[].triggerType must be one of ${[...validTriggers].join(", ")}`,
575
+ );
576
+ } else if (
577
+ a.triggerType === "schedule" &&
578
+ (typeof a.scheduleCron !== "string" || !a.scheduleCron)
579
+ ) {
580
+ push(
581
+ "manifest.actions[].scheduleCron is required when triggerType is 'schedule'",
582
+ );
428
583
  }
429
584
  if (typeof a.scriptSource !== "string" || a.scriptSource.length === 0) {
430
585
  push("manifest.actions[].scriptSource must be a non-empty string");
@@ -438,7 +593,9 @@ function _manifestActionRules(manifest) {
438
593
  }
439
594
  }
440
595
  if (a.triggerTableId !== undefined || a.apiKeyId !== undefined) {
441
- push("manifest.actions[] must not include triggerTableId or apiKeyId — those are tenant-local and bound after install");
596
+ push(
597
+ "manifest.actions[] must not include triggerTableId or apiKeyId — those are tenant-local and bound after install",
598
+ );
442
599
  }
443
600
  }
444
601
  return findings;
@@ -461,16 +618,22 @@ export function lintSource(source, options) {
461
618
  }
462
619
  const findings = [];
463
620
  const lines = source.split(/\r?\n/);
621
+ // Scan code with comments + string/template text blanked out so a banned
622
+ // identifier only fires on an actual code reference, not on the same word
623
+ // appearing in prose or string data. `codeLines` lines up 1:1 with `lines`
624
+ // (masking preserves newlines), so the reported snippet still comes from
625
+ // the original source.
626
+ const codeLines = _stripNonCode(source).split(/\r?\n/);
464
627
  for (let i = 0; i < lines.length; i++) {
465
- const line = lines[i];
628
+ const codeLine = codeLines[i];
466
629
  for (const rule of RULES) {
467
- if (rule.pattern.test(line)) {
630
+ if (rule.pattern.test(codeLine)) {
468
631
  findings.push({
469
632
  rule: rule.id,
470
633
  severity: "error",
471
634
  label: rule.label,
472
635
  line: i + 1,
473
- snippet: line.trim().slice(0, 200),
636
+ snippet: lines[i].trim().slice(0, 200),
474
637
  });
475
638
  }
476
639
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.36.0",
3
+ "version": "0.37.0",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -35,7 +35,7 @@
35
35
  ],
36
36
  "scripts": {
37
37
  "build": "node scripts/build.js",
38
- "test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-subscription.test.js src/__tests__/linter-users-scope.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/devserver.test.js"
38
+ "test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-subscription.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/devserver.test.js"
39
39
  },
40
40
  "engines": {
41
41
  "node": ">=18"