@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/README.md +27 -1
- package/dist/cli.js +14 -3
- package/dist/contract.cjs +93 -1
- package/dist/contract.js +93 -1
- package/dist/dev-shims.js +6 -69
- package/dist/hooks.js +65 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/dist/linter.cjs +129 -57
- package/dist/linter.js +130 -67
- package/package.json +3 -2
- package/src/contract.js +5355 -0
- package/src/dev-shims.js +6 -69
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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(
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
355
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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 =
|
|
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 =
|
|
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.
|
|
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",
|