@colixsystems/widget-sdk 0.187.0 → 0.188.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
@@ -75,7 +75,32 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
75
75
 
76
76
  ## Status
77
77
 
78
- `v0.187.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**.
78
+ `v0.188.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**.
79
+
80
+ ### What's new in 0.188.0 (contract 1.149.0)
81
+
82
+ **New linter rules for Leaflet maps: `leaflet-css-missing` and `leaflet-default-marker-icon`.** A widget's web entry is bundled with its own copy of Leaflet, and nothing else loads Leaflet's stylesheet. A map that skipped `import "leaflet/dist/leaflet.css"` drew a small patch of tiles inside an otherwise empty box, with its panes out of place and its markers invisible. Even with the stylesheet, Leaflet looks up its default marker image relative to the page, which 404s inside a widget, so a `<Marker>` with no `icon` showed no pin.
83
+
84
+ - **`leaflet-css-missing` (severity `error`).** A file that imports `leaflet` or `react-leaflet` code (an image-only import does not count) must also import `"leaflet/dist/leaflet.css"`. The bundler turns that import into an injected `<style>`. Import it in every file that uses Leaflet; the bundler includes it once.
85
+ - **`leaflet-default-marker-icon` (severity `warning`).** A react-leaflet `<Marker>` with no `icon` prop. Import the marker PNGs (the bundler inlines them as data URLs), build the icon once at module scope and pass it in:
86
+
87
+ ```jsx
88
+ import L from "leaflet";
89
+ import "leaflet/dist/leaflet.css";
90
+ import markerIcon from "leaflet/dist/images/marker-icon.png";
91
+ import markerIcon2x from "leaflet/dist/images/marker-icon-2x.png";
92
+ import markerShadow from "leaflet/dist/images/marker-shadow.png";
93
+
94
+ const pin = L.icon({
95
+ iconUrl: markerIcon, iconRetinaUrl: markerIcon2x, shadowUrl: markerShadow,
96
+ iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41],
97
+ });
98
+ // <Marker position={[lat, lng]} icon={pin} />
99
+ ```
100
+
101
+ It is a warning because a widget that configures `L.Icon.Default` itself is fine, and is not flagged. `<CircleMarker>` needs no icon.
102
+
103
+ The `leaflet` and `react-leaflet` entries in `CONTRACT.vettedImports` now say the same. Comments are not scanned, so documenting the pattern is safe.
79
104
 
80
105
  ### What's new in 0.187.0 (contract 1.148.0)
81
106
 
package/dist/contract.cjs CHANGED
@@ -3296,13 +3296,16 @@ const VETTED_IMPORTS = [
3296
3296
  platforms: ["web"],
3297
3297
  category: "geo",
3298
3298
  description:
3299
- "Web-only mapping library. Use alongside react-leaflet in widget.web.jsx as the web counterpart to react-native-maps.",
3299
+ "Web-only mapping library. Use alongside react-leaflet in widget.web.jsx as the web counterpart to react-native-maps. " +
3300
+ 'Every file that imports it must also `import "leaflet/dist/leaflet.css"` (the linter enforces it), and a marker needs an ' +
3301
+ "explicit `L.icon` built from the imported `leaflet/dist/images/*.png`: Leaflet's default icon cannot resolve its image inside a bundle.",
3300
3302
  },
3301
3303
  {
3302
3304
  specifier: "react-leaflet",
3303
3305
  platforms: ["web"],
3304
3306
  category: "geo",
3305
- description: "React bindings for leaflet. Web-only.",
3307
+ description:
3308
+ 'React bindings for leaflet. Web-only. Import "leaflet/dist/leaflet.css" alongside it and pass `icon` to every <Marker>.',
3306
3309
  },
3307
3310
  {
3308
3311
  specifier: "expo-audio",
@@ -4864,7 +4867,11 @@ const CONTRACT = deepFreeze({
4864
4867
  // New `useUserPhoto()` changes it through the new OPTIONAL context
4865
4868
  // slice `account` ({ setPhoto, removePhoto }); a host without it reads
4866
4869
  // `canEdit: false`.
4867
- version: "1.148.0",
4870
+ // 1.149.0: narrowing (sc-10466) — new linter rule `leaflet-css-missing`
4871
+ // (error): a file importing leaflet / react-leaflet must import
4872
+ // "leaflet/dist/leaflet.css". `leaflet-default-marker-icon` (warning)
4873
+ // flags a react-leaflet <Marker> with no `icon`.
4874
+ version: "1.149.0",
4868
4875
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
4869
4876
  hooks: HOOKS,
4870
4877
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -3292,13 +3292,16 @@ const VETTED_IMPORTS = [
3292
3292
  platforms: ["web"],
3293
3293
  category: "geo",
3294
3294
  description:
3295
- "Web-only mapping library. Use alongside react-leaflet in widget.web.jsx as the web counterpart to react-native-maps.",
3295
+ "Web-only mapping library. Use alongside react-leaflet in widget.web.jsx as the web counterpart to react-native-maps. " +
3296
+ 'Every file that imports it must also `import "leaflet/dist/leaflet.css"` (the linter enforces it), and a marker needs an ' +
3297
+ "explicit `L.icon` built from the imported `leaflet/dist/images/*.png`: Leaflet's default icon cannot resolve its image inside a bundle.",
3296
3298
  },
3297
3299
  {
3298
3300
  specifier: "react-leaflet",
3299
3301
  platforms: ["web"],
3300
3302
  category: "geo",
3301
- description: "React bindings for leaflet. Web-only.",
3303
+ description:
3304
+ 'React bindings for leaflet. Web-only. Import "leaflet/dist/leaflet.css" alongside it and pass `icon` to every <Marker>.',
3302
3305
  },
3303
3306
  {
3304
3307
  specifier: "expo-audio",
@@ -4860,7 +4863,11 @@ const CONTRACT = deepFreeze({
4860
4863
  // New `useUserPhoto()` changes it through the new OPTIONAL context
4861
4864
  // slice `account` ({ setPhoto, removePhoto }); a host without it reads
4862
4865
  // `canEdit: false`.
4863
- version: "1.148.0",
4866
+ // 1.149.0: narrowing (sc-10466) — new linter rule `leaflet-css-missing`
4867
+ // (error): a file importing leaflet / react-leaflet must import
4868
+ // "leaflet/dist/leaflet.css". `leaflet-default-marker-icon` (warning)
4869
+ // flags a react-leaflet <Marker> with no `icon`.
4870
+ version: "1.149.0",
4864
4871
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
4865
4872
  hooks: HOOKS,
4866
4873
  primitives: PRIMITIVES,
package/dist/linter.cjs CHANGED
@@ -252,7 +252,9 @@ function _packageRoot(spec) {
252
252
  const parts = spec.split("/");
253
253
  // A traversal / empty segment is a non-canonical specifier — never resolve it
254
254
  // to a vetted root, or "leaflet/../evil" would pass the gate as "leaflet".
255
- if (parts.some((p) => p === "" || p === "." || p === "..")) return null;
255
+ if (parts.some((p) => p === "" || p === "." || p === ".." || p.includes("\\"))) {
256
+ return null;
257
+ }
256
258
  return spec.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
257
259
  }
258
260
 
@@ -336,6 +338,87 @@ function _importRules(source, manifest) {
336
338
  return findings;
337
339
  }
338
340
 
341
+ // sc-10466 — Leaflet is bundled per widget; no host supplies its stylesheet.
342
+ // Without `leaflet/dist/leaflet.css` the tiles, panes and markers are
343
+ // unpositioned: a patch of map in an empty box, and no visible pin. And once
344
+ // the CSS is injected inline its default marker image resolves against the
345
+ // page URL and 404s, so a `<Marker>` needs an explicit `icon`.
346
+ const _LEAFLET_ROOTS = new Set(["leaflet", "react-leaflet"]);
347
+ const _LEAFLET_CSS = "leaflet/dist/leaflet.css";
348
+ const _MARKER_TAG_RE = /<Marker\b/g;
349
+
350
+ const _ASSET_SPECIFIER_RE = /\.(css|png|jpe?g|gif)$/i;
351
+ const _ICON_DEFAULT_CONFIGURED_RE =
352
+ /\bIcon\s*\.\s*Default\s*(\.\s*mergeOptions\s*\(|\.\s*imagePath\s*=)/;
353
+
354
+ function _lineAt(code, index) {
355
+ return code.slice(0, index).split(/\r?\n/).length;
356
+ }
357
+
358
+ function _leafletRules(source) {
359
+ const code = _stripNonCode(source, { keepStrings: true });
360
+ const sourceLines = source.split(/\r?\n/);
361
+ let firstLeafletLine = 0;
362
+ let importsCss = false;
363
+ let importsReactLeaflet = false;
364
+ // Whole-source scan: a Prettier-wrapped import spans several lines.
365
+ IMPORT_SPECIFIER_RE.lastIndex = 0;
366
+ let m;
367
+ while ((m = IMPORT_SPECIFIER_RE.exec(code))) {
368
+ const spec = m[1];
369
+ if (spec === _LEAFLET_CSS) importsCss = true;
370
+ const root = _packageRoot(spec);
371
+ if (!_LEAFLET_ROOTS.has(root) || _ASSET_SPECIFIER_RE.test(spec)) continue;
372
+ if (root === "react-leaflet") importsReactLeaflet = true;
373
+ if (!firstLeafletLine) firstLeafletLine = _lineAt(code, m.index + m[0].length);
374
+ }
375
+ IMPORT_SPECIFIER_RE.lastIndex = 0;
376
+ if (!firstLeafletLine) return [];
377
+ const findings = [];
378
+ if (!importsCss) {
379
+ findings.push({
380
+ rule: "leaflet-css-missing",
381
+ severity: "error",
382
+ label:
383
+ `this file uses Leaflet but never imports its stylesheet — without ` +
384
+ `it the map draws a partial patch of tiles inside an empty box and ` +
385
+ `markers are invisible. Add \`import "${_LEAFLET_CSS}";\` to this file.`,
386
+ line: firstLeafletLine,
387
+ snippet: (sourceLines[firstLeafletLine - 1] || "").trim().slice(0, 200),
388
+ });
389
+ }
390
+ if (
391
+ !importsReactLeaflet ||
392
+ _ICON_DEFAULT_CONFIGURED_RE.test(_stripNonCode(source))
393
+ ) {
394
+ return findings;
395
+ }
396
+ _MARKER_TAG_RE.lastIndex = 0;
397
+ let tag;
398
+ while ((tag = _MARKER_TAG_RE.exec(code))) {
399
+ const end = _jsxOpenTagEnd(code, tag.index + tag[0].length);
400
+ if (/(^|\s)icon\s*=/.test(code.slice(tag.index, end))) continue;
401
+ const line = _lineAt(code, tag.index);
402
+ findings.push({
403
+ rule: "leaflet-default-marker-icon",
404
+ severity: "warning",
405
+ label:
406
+ `<Marker> uses Leaflet's default icon, whose image path cannot ` +
407
+ `resolve inside a widget bundle, so the pin does not show. Import the ` +
408
+ `images (\`import markerIcon from "leaflet/dist/images/marker-icon.png"\`, ` +
409
+ `plus marker-icon-2x.png and marker-shadow.png), build ` +
410
+ `\`L.icon({ iconUrl, iconRetinaUrl, shadowUrl, iconSize: [25, 41], ` +
411
+ `iconAnchor: [12, 41] })\` once at module scope, and pass it as ` +
412
+ `\`icon={…}\`. Warning only — a widget that sets \`L.Icon.Default\` ` +
413
+ `itself (\`mergeOptions\` / \`imagePath\`) is not flagged.`,
414
+ line,
415
+ snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
416
+ });
417
+ _MARKER_TAG_RE.lastIndex = end;
418
+ }
419
+ return findings;
420
+ }
421
+
339
422
  // Explicit opt-out. A widget legitimately calling a THIRD-PARTY API with a
340
423
  // relative path against an `axios` baseURL cannot avoid the substring match,
341
424
  // and post-sc-3589 an unavoidable finding burns AI repair turns on correct
@@ -1853,6 +1936,7 @@ function lintSource(source, options) {
1853
1936
  severity: f.severity || "error",
1854
1937
  })),
1855
1938
  );
1939
+ findings.push(..._leafletRules(source));
1856
1940
  findings.push(..._hostApiUrlRules(source));
1857
1941
  // sc-6970 — soft warning: HTML markup spliced into a content string.
1858
1942
  findings.push(..._htmlInContentRules(source));
package/dist/linter.js CHANGED
@@ -135,7 +135,9 @@ function _packageRoot(spec) {
135
135
  const parts = spec.split("/");
136
136
  // A traversal / empty segment is a non-canonical specifier — never resolve it
137
137
  // to a vetted root, or "leaflet/../evil" would pass the gate as "leaflet".
138
- if (parts.some((p) => p === "" || p === "." || p === "..")) return null;
138
+ if (parts.some((p) => p === "" || p === "." || p === ".." || p.includes("\\"))) {
139
+ return null;
140
+ }
139
141
  return spec.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
140
142
  }
141
143
 
@@ -229,6 +231,87 @@ function _importRules(source, manifest) {
229
231
  return findings;
230
232
  }
231
233
 
234
+ // sc-10466 — Leaflet is bundled per widget; no host supplies its stylesheet.
235
+ // Without `leaflet/dist/leaflet.css` the tiles, panes and markers are
236
+ // unpositioned: a patch of map in an empty box, and no visible pin. And once
237
+ // the CSS is injected inline its default marker image resolves against the
238
+ // page URL and 404s, so a `<Marker>` needs an explicit `icon`.
239
+ const _LEAFLET_ROOTS = new Set(["leaflet", "react-leaflet"]);
240
+ const _LEAFLET_CSS = "leaflet/dist/leaflet.css";
241
+ const _MARKER_TAG_RE = /<Marker\b/g;
242
+
243
+ const _ASSET_SPECIFIER_RE = /\.(css|png|jpe?g|gif)$/i;
244
+ const _ICON_DEFAULT_CONFIGURED_RE =
245
+ /\bIcon\s*\.\s*Default\s*(\.\s*mergeOptions\s*\(|\.\s*imagePath\s*=)/;
246
+
247
+ function _lineAt(code, index) {
248
+ return code.slice(0, index).split(/\r?\n/).length;
249
+ }
250
+
251
+ function _leafletRules(source) {
252
+ const code = _stripNonCode(source, { keepStrings: true });
253
+ const sourceLines = source.split(/\r?\n/);
254
+ let firstLeafletLine = 0;
255
+ let importsCss = false;
256
+ let importsReactLeaflet = false;
257
+ // Whole-source scan: a Prettier-wrapped import spans several lines.
258
+ IMPORT_SPECIFIER_RE.lastIndex = 0;
259
+ let m;
260
+ while ((m = IMPORT_SPECIFIER_RE.exec(code))) {
261
+ const spec = m[1];
262
+ if (spec === _LEAFLET_CSS) importsCss = true;
263
+ const root = _packageRoot(spec);
264
+ if (!_LEAFLET_ROOTS.has(root) || _ASSET_SPECIFIER_RE.test(spec)) continue;
265
+ if (root === "react-leaflet") importsReactLeaflet = true;
266
+ if (!firstLeafletLine) firstLeafletLine = _lineAt(code, m.index + m[0].length);
267
+ }
268
+ IMPORT_SPECIFIER_RE.lastIndex = 0;
269
+ if (!firstLeafletLine) return [];
270
+ const findings = [];
271
+ if (!importsCss) {
272
+ findings.push({
273
+ rule: "leaflet-css-missing",
274
+ severity: "error",
275
+ label:
276
+ `this file uses Leaflet but never imports its stylesheet — without ` +
277
+ `it the map draws a partial patch of tiles inside an empty box and ` +
278
+ `markers are invisible. Add \`import "${_LEAFLET_CSS}";\` to this file.`,
279
+ line: firstLeafletLine,
280
+ snippet: (sourceLines[firstLeafletLine - 1] || "").trim().slice(0, 200),
281
+ });
282
+ }
283
+ if (
284
+ !importsReactLeaflet ||
285
+ _ICON_DEFAULT_CONFIGURED_RE.test(_stripNonCode(source))
286
+ ) {
287
+ return findings;
288
+ }
289
+ _MARKER_TAG_RE.lastIndex = 0;
290
+ let tag;
291
+ while ((tag = _MARKER_TAG_RE.exec(code))) {
292
+ const end = _jsxOpenTagEnd(code, tag.index + tag[0].length);
293
+ if (/(^|\s)icon\s*=/.test(code.slice(tag.index, end))) continue;
294
+ const line = _lineAt(code, tag.index);
295
+ findings.push({
296
+ rule: "leaflet-default-marker-icon",
297
+ severity: "warning",
298
+ label:
299
+ `<Marker> uses Leaflet's default icon, whose image path cannot ` +
300
+ `resolve inside a widget bundle, so the pin does not show. Import the ` +
301
+ `images (\`import markerIcon from "leaflet/dist/images/marker-icon.png"\`, ` +
302
+ `plus marker-icon-2x.png and marker-shadow.png), build ` +
303
+ `\`L.icon({ iconUrl, iconRetinaUrl, shadowUrl, iconSize: [25, 41], ` +
304
+ `iconAnchor: [12, 41] })\` once at module scope, and pass it as ` +
305
+ `\`icon={…}\`. Warning only — a widget that sets \`L.Icon.Default\` ` +
306
+ `itself (\`mergeOptions\` / \`imagePath\`) is not flagged.`,
307
+ line,
308
+ snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
309
+ });
310
+ _MARKER_TAG_RE.lastIndex = end;
311
+ }
312
+ return findings;
313
+ }
314
+
232
315
  // REQ-WSDK-PLATFORM §3.5: soft warning when source contains host-API URL
233
316
  // substrings. `severity: "warning"` so a HUMAN-authored submission still
234
317
  // publishes and the marketplace review queue flags it for a human pass.
@@ -1925,6 +2008,7 @@ export function lintSource(source, options) {
1925
2008
  severity: f.severity || "error",
1926
2009
  })),
1927
2010
  );
2011
+ findings.push(..._leafletRules(source));
1928
2012
  // REQ-WSDK-PLATFORM §3.5: soft host-API URL warning (does not block).
1929
2013
  findings.push(..._hostApiUrlRules(source));
1930
2014
  // sc-6970 — soft warning: HTML markup spliced into a content string.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.187.0",
3
+ "version": "0.188.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",