@colixsystems/widget-sdk 0.188.0 → 0.190.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/dist/linter.cjs CHANGED
@@ -221,8 +221,25 @@ function bannedIdentifiers() {
221
221
 
222
222
  // REQ-WSDK-PLATFORM §3.4, §8: import-specifier validation. See linter.js
223
223
  // for the source-of-truth comment.
224
- const IMPORT_SPECIFIER_RE =
225
- /(?:^|[\s;])(?:import(?:\s+[^"';]+from)?|require)\s*\(?\s*['"]([^'"]+)['"]/g;
224
+ // The import clause follows the grammar (default, `* as ns`, `{ … }`), so
225
+ // prose that merely says "import … from" rarely matches. A brace list stops
226
+ // at the next import/export keyword and no two quantifiers share whitespace,
227
+ // keeping the whole-source scan linear on hostile input.
228
+ const _ID_CHAR = "[\\p{ID_Continue}$\\\\]";
229
+ const _NAMED = `\\{(?:(?!\\b(?:import|export)\\b)(?:[^}"';]|"[^"\\n]*"|'[^'\\n]*'))*\\}`;
230
+ const _NAMESPACE = `\\*\\s*as\\s+${_ID_CHAR}+`;
231
+ const _IMPORT_CLAUSE =
232
+ `(?:${_ID_CHAR}+(?:\\s*,\\s*(?:${_NAMESPACE}|${_NAMED}))?` +
233
+ `|${_NAMESPACE}|${_NAMED})`;
234
+ const IMPORT_SPECIFIER_RE = new RegExp(
235
+ // Not part of an identifier, a member access, CSS `@import` or JSX text.
236
+ `(?<![\\p{ID_Continue}$.@#'"\`>])` +
237
+ `(?:import(?!${_ID_CHAR})\\s*(?:${_IMPORT_CLAUSE}\\s*from\\s*)?(?:\\(\\s*)?` +
238
+ `|export(?!${_ID_CHAR})\\s*(?:\\*(?:\\s*as\\s+${_ID_CHAR}+)?|${_NAMED})\\s*from\\s*` +
239
+ `|require(?!${_ID_CHAR})\\s*\\(\\s*)` +
240
+ `['"]([^'"\\s]+)['"]`,
241
+ "gu",
242
+ );
226
243
 
227
244
  function _classifySpecifier(spec) {
228
245
  if (
@@ -258,6 +275,24 @@ function _packageRoot(spec) {
258
275
  return spec.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
259
276
  }
260
277
 
278
+ /**
279
+ * The package roots whose SUBPATH imports load in a file whose manifest is
280
+ * narrowed to it (`narrowManifestForFile`). The web loader shims the packages
281
+ * the host provides by exact name, so a file that ships to web may import a
282
+ * subpath only of a bundled vetted package; Metro, on native, resolves any
283
+ * real subpath of a vetted package. Shared with the agent render sandbox.
284
+ */
285
+ function subpathImportRootsFor(manifest) {
286
+ const platforms =
287
+ manifest && Array.isArray(manifest.supportedPlatforms)
288
+ ? manifest.supportedPlatforms
289
+ : [];
290
+ const nativeOnly = platforms.length > 0 && !platforms.includes("web");
291
+ return nativeOnly
292
+ ? [...CONTRACT.allowedBareImports]
293
+ : [...CONTRACT.subpathImportRoots];
294
+ }
295
+
261
296
  function _importRules(source, manifest) {
262
297
  const findings = [];
263
298
  const allowed = new Map(
@@ -277,60 +312,86 @@ function _importRules(source, manifest) {
277
312
  manifest.supportedPlatforms.length > 0
278
313
  ? new Set(manifest.supportedPlatforms)
279
314
  : null;
315
+ const subpathRoots = new Set(subpathImportRootsFor(manifest));
280
316
 
281
- const lines = source.split(/\r?\n/);
282
- for (let i = 0; i < lines.length; i += 1) {
283
- const line = lines[i];
317
+ // Whole-source scan: a Prettier-wrapped import spans several lines. Both
318
+ // the raw and the comment-masked text are read, because the masker cannot
319
+ // see regex literals and a `/"/` would blank a real import out of it.
320
+ const specifiersByEnd = new Map();
321
+ for (const text of [source, _stripNonCode(source, { keepStrings: true })]) {
284
322
  IMPORT_SPECIFIER_RE.lastIndex = 0;
285
323
  let m;
286
- while ((m = IMPORT_SPECIFIER_RE.exec(line))) {
287
- const spec = m[1];
288
- const kind = _classifySpecifier(spec);
289
- if (kind === "relative-ok") continue;
290
- if (kind === "relative-escape") {
291
- findings.push({
292
- rule: "no-parent-relative-import",
293
- label: `relative import "${spec}" escapes the bundle (\`../\`) — only sibling-file imports (\`./foo.js\`) are allowed`,
294
- line: i + 1,
295
- snippet: line.trim().slice(0, 200),
296
- });
297
- continue;
298
- }
299
- if (kind === "absolute") {
300
- findings.push({
301
- rule: "no-absolute-import",
302
- label: `absolute import "${spec}" is not allowed — use a vetted bare specifier or a sibling-file (\`./foo.js\`) import`,
303
- line: i + 1,
304
- snippet: line.trim().slice(0, 200),
305
- });
306
- continue;
307
- }
308
- const entry = allowed.get(spec) || allowed.get(_packageRoot(spec));
309
- if (!entry) {
310
- findings.push({
311
- rule: "import-not-vetted",
312
- label:
313
- `import "${spec}" is not on the vetted package list — see ` +
314
- `CONTRACT.vettedImports for the current set or open a request to add it`,
315
- line: i + 1,
316
- snippet: line.trim().slice(0, 200),
317
- });
318
- continue;
319
- }
320
- if (declaredPlatforms) {
321
- for (const wanted of declaredPlatforms) {
322
- if (!entry.platforms.includes(wanted)) {
323
- findings.push({
324
- rule: "import-platform-mismatch",
325
- label:
326
- `"${spec}" supports [${entry.platforms.join(", ")}] but the ` +
327
- `manifest claims supportedPlatforms includes "${wanted}". ` +
328
- `Move this import into widget.${wanted === "web" ? "native" : "web"}.jsx, ` +
329
- `or drop "${wanted}" from manifest.supportedPlatforms.`,
330
- line: i + 1,
331
- snippet: line.trim().slice(0, 200),
332
- });
333
- }
324
+ while ((m = IMPORT_SPECIFIER_RE.exec(text))) {
325
+ specifiersByEnd.set(m.index + m[0].length, m[1]);
326
+ }
327
+ }
328
+ IMPORT_SPECIFIER_RE.lastIndex = 0;
329
+ const sourceLines = source.split(/\r?\n/);
330
+ const lineAt = _lineCounter(source);
331
+ const ordered = [...specifiersByEnd].sort((a, b) => a[0] - b[0]);
332
+ for (const [end, spec] of ordered) {
333
+ const line = lineAt(end);
334
+ const snippet = (sourceLines[line - 1] || "").trim().slice(0, 200);
335
+ const kind = _classifySpecifier(spec);
336
+ if (kind === "relative-ok") continue;
337
+ if (kind === "relative-escape") {
338
+ findings.push({
339
+ rule: "no-parent-relative-import",
340
+ label: `relative import "${spec}" escapes the bundle (\`../\`) — only sibling-file imports (\`./foo.js\`) are allowed`,
341
+ line,
342
+ snippet,
343
+ });
344
+ continue;
345
+ }
346
+ if (kind === "absolute") {
347
+ findings.push({
348
+ rule: "no-absolute-import",
349
+ label: `absolute import "${spec}" is not allowed — use a vetted bare specifier or a sibling-file (\`./foo.js\`) import`,
350
+ line,
351
+ snippet,
352
+ });
353
+ continue;
354
+ }
355
+ // Bare specifier — validate against the vetted list. A subpath/asset
356
+ // import (leaflet/dist/leaflet.css) resolves to its vetted package root.
357
+ let entry = allowed.get(spec);
358
+ const root = entry ? null : _packageRoot(spec);
359
+ if (root && subpathRoots.has(root)) entry = allowed.get(root);
360
+ if (!entry) {
361
+ findings.push({
362
+ rule: "import-not-vetted",
363
+ label:
364
+ root && allowed.has(root)
365
+ ? `import "${spec}" is a subpath of "${root}", which the web ` +
366
+ `host provides by its exact name only, so it cannot load in ` +
367
+ `the web Player — import from "${root}" itself (a file that ` +
368
+ `ships only to native may keep the subpath)`
369
+ : `import "${spec}" is not on the vetted package list — see ` +
370
+ `CONTRACT.vettedImports for the current set or open a request to add it`,
371
+ line,
372
+ snippet,
373
+ });
374
+ continue;
375
+ }
376
+ // If the manifest declares supportedPlatforms, every imported
377
+ // specifier must support every declared platform. A widget that
378
+ // imports react-native-maps (native-only) cannot honestly claim
379
+ // supportedPlatforms: ["web", "native"] from a single source file —
380
+ // it must either drop "web" from the manifest, OR move that import
381
+ // into widget.native.jsx so the web bundle doesn't see it.
382
+ if (declaredPlatforms) {
383
+ for (const wanted of declaredPlatforms) {
384
+ if (!entry.platforms.includes(wanted)) {
385
+ findings.push({
386
+ rule: "import-platform-mismatch",
387
+ label:
388
+ `"${spec}" supports [${entry.platforms.join(", ")}] but the ` +
389
+ `manifest claims supportedPlatforms includes "${wanted}". ` +
390
+ `Move this import into widget.${wanted === "web" ? "native" : "web"}.jsx, ` +
391
+ `or drop "${wanted}" from manifest.supportedPlatforms.`,
392
+ line,
393
+ snippet,
394
+ });
334
395
  }
335
396
  }
336
397
  }
@@ -351,8 +412,16 @@ const _ASSET_SPECIFIER_RE = /\.(css|png|jpe?g|gif)$/i;
351
412
  const _ICON_DEFAULT_CONFIGURED_RE =
352
413
  /\bIcon\s*\.\s*Default\s*(\.\s*mergeOptions\s*\(|\.\s*imagePath\s*=)/;
353
414
 
354
- function _lineAt(code, index) {
355
- return code.slice(0, index).split(/\r?\n/).length;
415
+ // Line of an index in `code`, counted forward: indexes must not decrease.
416
+ function _lineCounter(code) {
417
+ let line = 1;
418
+ let at = 0;
419
+ return (index) => {
420
+ for (; at < index; at += 1) {
421
+ if (code.charCodeAt(at) === 10) line += 1;
422
+ }
423
+ return line;
424
+ };
356
425
  }
357
426
 
358
427
  function _leafletRules(source) {
@@ -361,6 +430,7 @@ function _leafletRules(source) {
361
430
  let firstLeafletLine = 0;
362
431
  let importsCss = false;
363
432
  let importsReactLeaflet = false;
433
+ const importLineAt = _lineCounter(code);
364
434
  // Whole-source scan: a Prettier-wrapped import spans several lines.
365
435
  IMPORT_SPECIFIER_RE.lastIndex = 0;
366
436
  let m;
@@ -370,7 +440,7 @@ function _leafletRules(source) {
370
440
  const root = _packageRoot(spec);
371
441
  if (!_LEAFLET_ROOTS.has(root) || _ASSET_SPECIFIER_RE.test(spec)) continue;
372
442
  if (root === "react-leaflet") importsReactLeaflet = true;
373
- if (!firstLeafletLine) firstLeafletLine = _lineAt(code, m.index + m[0].length);
443
+ if (!firstLeafletLine) firstLeafletLine = importLineAt(m.index + m[0].length);
374
444
  }
375
445
  IMPORT_SPECIFIER_RE.lastIndex = 0;
376
446
  if (!firstLeafletLine) return [];
@@ -393,12 +463,13 @@ function _leafletRules(source) {
393
463
  ) {
394
464
  return findings;
395
465
  }
466
+ const markerLineAt = _lineCounter(code);
396
467
  _MARKER_TAG_RE.lastIndex = 0;
397
468
  let tag;
398
469
  while ((tag = _MARKER_TAG_RE.exec(code))) {
399
470
  const end = _jsxOpenTagEnd(code, tag.index + tag[0].length);
400
471
  if (/(^|\s)icon\s*=/.test(code.slice(tag.index, end))) continue;
401
- const line = _lineAt(code, tag.index);
472
+ const line = markerLineAt(tag.index);
402
473
  findings.push({
403
474
  rule: "leaflet-default-marker-icon",
404
475
  severity: "warning",
@@ -1980,4 +2051,5 @@ module.exports = {
1980
2051
  lintEditability,
1981
2052
  bannedIdentifiers,
1982
2053
  narrowManifestForFile,
2054
+ subpathImportRootsFor,
1983
2055
  };
package/dist/linter.js CHANGED
@@ -94,9 +94,10 @@ export function bannedIdentifiers() {
94
94
  //
95
95
  // Recognised forms (string-level, no AST):
96
96
  // import X from "spec"
97
- // import { … } from "spec"
97
+ // import { … } from "spec" (also wrapped over several lines)
98
98
  // import * as X from "spec"
99
99
  // import "spec"
100
+ // export { … } from "spec" / export * from "spec"
100
101
  // const X = require("spec") // CJS — for completeness
101
102
  //
102
103
  // Relative imports within the bundle (`./foo.js`, `./shared/util.js`) are
@@ -104,8 +105,25 @@ export function bannedIdentifiers() {
104
105
  // between `widget.web.jsx` and `widget.native.jsx`. Parent-directory
105
106
  // (`../`) and absolute (`/`, `http(s):`) paths are rejected to keep the
106
107
  // bundle scope sealed.
107
- const IMPORT_SPECIFIER_RE =
108
- /(?:^|[\s;])(?:import(?:\s+[^"';]+from)?|require)\s*\(?\s*['"]([^'"]+)['"]/g;
108
+ // The import clause follows the grammar (default, `* as ns`, `{ … }`), so
109
+ // prose that merely says "import … from" rarely matches. A brace list stops
110
+ // at the next import/export keyword and no two quantifiers share whitespace,
111
+ // keeping the whole-source scan linear on hostile input.
112
+ const _ID_CHAR = "[\\p{ID_Continue}$\\\\]";
113
+ const _NAMED = `\\{(?:(?!\\b(?:import|export)\\b)(?:[^}"';]|"[^"\\n]*"|'[^'\\n]*'))*\\}`;
114
+ const _NAMESPACE = `\\*\\s*as\\s+${_ID_CHAR}+`;
115
+ const _IMPORT_CLAUSE =
116
+ `(?:${_ID_CHAR}+(?:\\s*,\\s*(?:${_NAMESPACE}|${_NAMED}))?` +
117
+ `|${_NAMESPACE}|${_NAMED})`;
118
+ const IMPORT_SPECIFIER_RE = new RegExp(
119
+ // Not part of an identifier, a member access, CSS `@import` or JSX text.
120
+ `(?<![\\p{ID_Continue}$.@#'"\`>])` +
121
+ `(?:import(?!${_ID_CHAR})\\s*(?:${_IMPORT_CLAUSE}\\s*from\\s*)?(?:\\(\\s*)?` +
122
+ `|export(?!${_ID_CHAR})\\s*(?:\\*(?:\\s*as\\s+${_ID_CHAR}+)?|${_NAMED})\\s*from\\s*` +
123
+ `|require(?!${_ID_CHAR})\\s*\\(\\s*)` +
124
+ `['"]([^'"\\s]+)['"]`,
125
+ "gu",
126
+ );
109
127
 
110
128
  function _classifySpecifier(spec) {
111
129
  if (
@@ -141,6 +159,24 @@ function _packageRoot(spec) {
141
159
  return spec.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
142
160
  }
143
161
 
162
+ /**
163
+ * The package roots whose SUBPATH imports load in a file whose manifest is
164
+ * narrowed to it (`narrowManifestForFile`). The web loader shims the packages
165
+ * the host provides by exact name, so a file that ships to web may import a
166
+ * subpath only of a bundled vetted package; Metro, on native, resolves any
167
+ * real subpath of a vetted package. Shared with the agent render sandbox.
168
+ */
169
+ export function subpathImportRootsFor(manifest) {
170
+ const platforms =
171
+ manifest && Array.isArray(manifest.supportedPlatforms)
172
+ ? manifest.supportedPlatforms
173
+ : [];
174
+ const nativeOnly = platforms.length > 0 && !platforms.includes("web");
175
+ return nativeOnly
176
+ ? [...CONTRACT.allowedBareImports]
177
+ : [...CONTRACT.subpathImportRoots];
178
+ }
179
+
144
180
  function _importRules(source, manifest) {
145
181
  const findings = [];
146
182
  const allowed = new Map(CONTRACT.vettedImports.map((v) => [v.specifier, v]));
@@ -161,69 +197,86 @@ function _importRules(source, manifest) {
161
197
  manifest.supportedPlatforms.length > 0
162
198
  ? new Set(manifest.supportedPlatforms)
163
199
  : null;
200
+ const subpathRoots = new Set(subpathImportRootsFor(manifest));
164
201
 
165
- const lines = source.split(/\r?\n/);
166
- for (let i = 0; i < lines.length; i += 1) {
167
- const line = lines[i];
168
- // Reset lastIndex — the regex is /g and shared across lines.
202
+ // Whole-source scan: a Prettier-wrapped import spans several lines. Both
203
+ // the raw and the comment-masked text are read, because the masker cannot
204
+ // see regex literals and a `/"/` would blank a real import out of it.
205
+ const specifiersByEnd = new Map();
206
+ for (const text of [source, _stripNonCode(source, { keepStrings: true })]) {
169
207
  IMPORT_SPECIFIER_RE.lastIndex = 0;
170
208
  let m;
171
- while ((m = IMPORT_SPECIFIER_RE.exec(line))) {
172
- const spec = m[1];
173
- const kind = _classifySpecifier(spec);
174
- if (kind === "relative-ok") continue;
175
- if (kind === "relative-escape") {
176
- findings.push({
177
- rule: "no-parent-relative-import",
178
- label: `relative import "${spec}" escapes the bundle (\`../\`) — only sibling-file imports (\`./foo.js\`) are allowed`,
179
- line: i + 1,
180
- snippet: line.trim().slice(0, 200),
181
- });
182
- continue;
183
- }
184
- if (kind === "absolute") {
185
- findings.push({
186
- rule: "no-absolute-import",
187
- label: `absolute import "${spec}" is not allowed — use a vetted bare specifier or a sibling-file (\`./foo.js\`) import`,
188
- line: i + 1,
189
- snippet: line.trim().slice(0, 200),
190
- });
191
- continue;
192
- }
193
- // Bare specifier — validate against the vetted list. A subpath/asset
194
- // import (leaflet/dist/leaflet.css) resolves to its vetted package root.
195
- const entry = allowed.get(spec) || allowed.get(_packageRoot(spec));
196
- if (!entry) {
197
- findings.push({
198
- rule: "import-not-vetted",
199
- label:
200
- `import "${spec}" is not on the vetted package list — see ` +
201
- `CONTRACT.vettedImports for the current set or open a request to add it`,
202
- line: i + 1,
203
- snippet: line.trim().slice(0, 200),
204
- });
205
- continue;
206
- }
207
- // If the manifest declares supportedPlatforms, every imported
208
- // specifier must support every declared platform. A widget that
209
- // imports react-native-maps (native-only) cannot honestly claim
210
- // supportedPlatforms: ["web", "native"] from a single source file —
211
- // it must either drop "web" from the manifest, OR move that import
212
- // into widget.native.jsx so the web bundle doesn't see it.
213
- if (declaredPlatforms) {
214
- for (const wanted of declaredPlatforms) {
215
- if (!entry.platforms.includes(wanted)) {
216
- findings.push({
217
- rule: "import-platform-mismatch",
218
- label:
219
- `"${spec}" supports [${entry.platforms.join(", ")}] but the ` +
220
- `manifest claims supportedPlatforms includes "${wanted}". ` +
221
- `Move this import into widget.${wanted === "web" ? "native" : "web"}.jsx, ` +
222
- `or drop "${wanted}" from manifest.supportedPlatforms.`,
223
- line: i + 1,
224
- snippet: line.trim().slice(0, 200),
225
- });
226
- }
209
+ while ((m = IMPORT_SPECIFIER_RE.exec(text))) {
210
+ specifiersByEnd.set(m.index + m[0].length, m[1]);
211
+ }
212
+ }
213
+ IMPORT_SPECIFIER_RE.lastIndex = 0;
214
+ const sourceLines = source.split(/\r?\n/);
215
+ const lineAt = _lineCounter(source);
216
+ const ordered = [...specifiersByEnd].sort((a, b) => a[0] - b[0]);
217
+ for (const [end, spec] of ordered) {
218
+ const line = lineAt(end);
219
+ const snippet = (sourceLines[line - 1] || "").trim().slice(0, 200);
220
+ const kind = _classifySpecifier(spec);
221
+ if (kind === "relative-ok") continue;
222
+ if (kind === "relative-escape") {
223
+ findings.push({
224
+ rule: "no-parent-relative-import",
225
+ label: `relative import "${spec}" escapes the bundle (\`../\`) — only sibling-file imports (\`./foo.js\`) are allowed`,
226
+ line,
227
+ snippet,
228
+ });
229
+ continue;
230
+ }
231
+ if (kind === "absolute") {
232
+ findings.push({
233
+ rule: "no-absolute-import",
234
+ label: `absolute import "${spec}" is not allowed — use a vetted bare specifier or a sibling-file (\`./foo.js\`) import`,
235
+ line,
236
+ snippet,
237
+ });
238
+ continue;
239
+ }
240
+ // Bare specifier — validate against the vetted list. A subpath/asset
241
+ // import (leaflet/dist/leaflet.css) resolves to its vetted package root.
242
+ let entry = allowed.get(spec);
243
+ const root = entry ? null : _packageRoot(spec);
244
+ if (root && subpathRoots.has(root)) entry = allowed.get(root);
245
+ if (!entry) {
246
+ findings.push({
247
+ rule: "import-not-vetted",
248
+ label:
249
+ root && allowed.has(root)
250
+ ? `import "${spec}" is a subpath of "${root}", which the web ` +
251
+ `host provides by its exact name only, so it cannot load in ` +
252
+ `the web Player — import from "${root}" itself (a file that ` +
253
+ `ships only to native may keep the subpath)`
254
+ : `import "${spec}" is not on the vetted package list — see ` +
255
+ `CONTRACT.vettedImports for the current set or open a request to add it`,
256
+ line,
257
+ snippet,
258
+ });
259
+ continue;
260
+ }
261
+ // If the manifest declares supportedPlatforms, every imported
262
+ // specifier must support every declared platform. A widget that
263
+ // imports react-native-maps (native-only) cannot honestly claim
264
+ // supportedPlatforms: ["web", "native"] from a single source file —
265
+ // it must either drop "web" from the manifest, OR move that import
266
+ // into widget.native.jsx so the web bundle doesn't see it.
267
+ if (declaredPlatforms) {
268
+ for (const wanted of declaredPlatforms) {
269
+ if (!entry.platforms.includes(wanted)) {
270
+ findings.push({
271
+ rule: "import-platform-mismatch",
272
+ label:
273
+ `"${spec}" supports [${entry.platforms.join(", ")}] but the ` +
274
+ `manifest claims supportedPlatforms includes "${wanted}". ` +
275
+ `Move this import into widget.${wanted === "web" ? "native" : "web"}.jsx, ` +
276
+ `or drop "${wanted}" from manifest.supportedPlatforms.`,
277
+ line,
278
+ snippet,
279
+ });
227
280
  }
228
281
  }
229
282
  }
@@ -244,8 +297,16 @@ const _ASSET_SPECIFIER_RE = /\.(css|png|jpe?g|gif)$/i;
244
297
  const _ICON_DEFAULT_CONFIGURED_RE =
245
298
  /\bIcon\s*\.\s*Default\s*(\.\s*mergeOptions\s*\(|\.\s*imagePath\s*=)/;
246
299
 
247
- function _lineAt(code, index) {
248
- return code.slice(0, index).split(/\r?\n/).length;
300
+ // Line of an index in `code`, counted forward: indexes must not decrease.
301
+ function _lineCounter(code) {
302
+ let line = 1;
303
+ let at = 0;
304
+ return (index) => {
305
+ for (; at < index; at += 1) {
306
+ if (code.charCodeAt(at) === 10) line += 1;
307
+ }
308
+ return line;
309
+ };
249
310
  }
250
311
 
251
312
  function _leafletRules(source) {
@@ -254,6 +315,7 @@ function _leafletRules(source) {
254
315
  let firstLeafletLine = 0;
255
316
  let importsCss = false;
256
317
  let importsReactLeaflet = false;
318
+ const importLineAt = _lineCounter(code);
257
319
  // Whole-source scan: a Prettier-wrapped import spans several lines.
258
320
  IMPORT_SPECIFIER_RE.lastIndex = 0;
259
321
  let m;
@@ -263,7 +325,7 @@ function _leafletRules(source) {
263
325
  const root = _packageRoot(spec);
264
326
  if (!_LEAFLET_ROOTS.has(root) || _ASSET_SPECIFIER_RE.test(spec)) continue;
265
327
  if (root === "react-leaflet") importsReactLeaflet = true;
266
- if (!firstLeafletLine) firstLeafletLine = _lineAt(code, m.index + m[0].length);
328
+ if (!firstLeafletLine) firstLeafletLine = importLineAt(m.index + m[0].length);
267
329
  }
268
330
  IMPORT_SPECIFIER_RE.lastIndex = 0;
269
331
  if (!firstLeafletLine) return [];
@@ -286,12 +348,13 @@ function _leafletRules(source) {
286
348
  ) {
287
349
  return findings;
288
350
  }
351
+ const markerLineAt = _lineCounter(code);
289
352
  _MARKER_TAG_RE.lastIndex = 0;
290
353
  let tag;
291
354
  while ((tag = _MARKER_TAG_RE.exec(code))) {
292
355
  const end = _jsxOpenTagEnd(code, tag.index + tag[0].length);
293
356
  if (/(^|\s)icon\s*=/.test(code.slice(tag.index, end))) continue;
294
- const line = _lineAt(code, tag.index);
357
+ const line = markerLineAt(tag.index);
295
358
  findings.push({
296
359
  rule: "leaflet-default-marker-icon",
297
360
  severity: "warning",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.188.0",
3
+ "version": "0.190.0",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "homepage": "https://github.com/Colix-AB/AppStudio",
6
6
  "type": "module",
@@ -45,7 +45,8 @@
45
45
  "README.md",
46
46
  "LICENSE",
47
47
  "src/webbundle.js",
48
- "src/dev-shims.js"
48
+ "src/dev-shims.js",
49
+ "src/contract.js"
49
50
  ],
50
51
  "scripts": {
51
52
  "build": "node scripts/build.js",