@colixsystems/widget-sdk 0.188.0 → 0.189.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 +12 -1
- package/dist/cli.js +14 -3
- package/dist/contract.cjs +55 -1
- package/dist/contract.js +55 -1
- package/dist/dev-shims.js +6 -69
- package/dist/index.d.ts +4 -0
- package/dist/linter.cjs +129 -57
- package/dist/linter.js +130 -67
- package/package.json +3 -2
- package/src/contract.js +5317 -0
- package/src/dev-shims.js +6 -69
package/README.md
CHANGED
|
@@ -75,7 +75,18 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
75
75
|
|
|
76
76
|
## Status
|
|
77
77
|
|
|
78
|
-
`v0.
|
|
78
|
+
`v0.189.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.189.0 (contract 1.150.0)
|
|
81
|
+
|
|
82
|
+
**The import check now catches two shapes that passed lint and then failed to load in the web Player.** This narrows `import-not-vetted`:
|
|
83
|
+
|
|
84
|
+
- **Imports wrapped over several lines.** The check read one line at a time, so a Prettier-wrapped import of a package that is not on the vetted list was never seen. It now reads the whole file and reports the finding on the `from` line. `export { … } from "pkg"` and `export * from "pkg"` are checked too, and so are forms written without spaces (`import{a}from"pkg"`) or inside an expression (`x = require("pkg")`).
|
|
85
|
+
- **Subpaths of packages the web Player provides.** The web Player supplies some packages itself and loads them by their exact name only. `CONTRACT.hostExternalSpecifiers` lists them; today they are `react` (and its JSX runtimes), `react-dom`, `react-native`, `@colixsystems/widget-sdk`, `lucide-react-native`, `react-native-svg`, `react-native-qrcode-svg`, `date-fns`, `decimal.js`, `d3-scale`, `d3-shape`, `d3-array` and `simple-statistics`. A subpath of one of them, such as `date-fns/format` or `react-native/Libraries/…`, cannot load on web. It is now `import-not-vetted` in any file that ships to web. Import from the package itself: `import { format } from "date-fns"`.
|
|
86
|
+
|
|
87
|
+
A subpath of a package your bundle includes is still allowed, for example `leaflet/dist/leaflet.css`, a Leaflet marker image or `react-native-vector-icons/MaterialIcons`. A file that ships only to native (`widget.native.jsx`, or a manifest whose `supportedPlatforms` is `["native"]`) keeps every vetted subpath, because the native build resolves them.
|
|
88
|
+
|
|
89
|
+
`CONTRACT.subpathImportRoots` lists the vetted packages whose subpaths load on web, and the new `subpathImportRootsFor(manifest)` export of `@colixsystems/widget-sdk/linter` returns the roots that load for a file with that manifest. `appstudio-widget lint --manifest` now checks `widget.web.jsx` and `widget.native.jsx` against the platform each one ships to, as the submission check already did.
|
|
79
90
|
|
|
80
91
|
### What's new in 0.188.0 (contract 1.149.0)
|
|
81
92
|
|
package/dist/cli.js
CHANGED
|
@@ -5,10 +5,15 @@
|
|
|
5
5
|
// — REQ-WSDK-DEVKIT local dev server
|
|
6
6
|
|
|
7
7
|
import { readFileSync } from "node:fs";
|
|
8
|
-
import { resolve } from "node:path";
|
|
8
|
+
import { basename, resolve } from "node:path";
|
|
9
9
|
import { pathToFileURL } from "node:url";
|
|
10
10
|
import { argv, exit, stderr, stdout } from "node:process";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
lintSource,
|
|
13
|
+
lintStyleWiring,
|
|
14
|
+
lintEditability,
|
|
15
|
+
narrowManifestForFile,
|
|
16
|
+
} from "./linter.js";
|
|
12
17
|
import { startDevServer } from "./devserver.js";
|
|
13
18
|
|
|
14
19
|
function usage() {
|
|
@@ -99,9 +104,15 @@ async function runLint(rest) {
|
|
|
99
104
|
}
|
|
100
105
|
}
|
|
101
106
|
const label = positionals.join(", ");
|
|
107
|
+
// Narrow per split-impl file, as the submission gate does.
|
|
102
108
|
const perFile = Object.entries(files).map(([filePath, source]) => [
|
|
103
109
|
filePath,
|
|
104
|
-
lintSource(
|
|
110
|
+
lintSource(
|
|
111
|
+
source,
|
|
112
|
+
manifest
|
|
113
|
+
? { manifest: narrowManifestForFile(manifest, basename(filePath)) }
|
|
114
|
+
: undefined,
|
|
115
|
+
),
|
|
105
116
|
]);
|
|
106
117
|
const wiring = manifest
|
|
107
118
|
? lintStyleWiring(manifest, files)
|
package/dist/contract.cjs
CHANGED
|
@@ -3505,6 +3505,49 @@ const ALLOWED_BARE_IMPORTS = [
|
|
|
3505
3505
|
...CORE_INFRA_BARE_IMPORTS,
|
|
3506
3506
|
];
|
|
3507
3507
|
|
|
3508
|
+
// sc-10502: the bare specifiers the web host RESOLVES for a loaded widget
|
|
3509
|
+
// instead of letting its bundle inline a copy. The web-entry bundler marks
|
|
3510
|
+
// exactly these `external` and the Studio loader shims each by EXACT name, so
|
|
3511
|
+
// a subpath of one (`date-fns/format`) cannot load in the web Player.
|
|
3512
|
+
// `dev-shims.hostExternalSpecifiers()` returns this list; the loader's
|
|
3513
|
+
// module-load guard (frontend widgetLoader.js) throws if its shims drift.
|
|
3514
|
+
//
|
|
3515
|
+
// Why each entry is host-resolved, not bundled:
|
|
3516
|
+
// * react + its JSX runtimes: one React instance (hooks, context).
|
|
3517
|
+
// * react-dom, react-dom/client: react-leaflet portals through react-dom;
|
|
3518
|
+
// a second renderer beside the host's React crashes (sc-1064).
|
|
3519
|
+
// * @colixsystems/widget-sdk: the host-built SDK carries datastore, theme
|
|
3520
|
+
// and navigation.
|
|
3521
|
+
// * react-native, lucide-react-native, react-native-svg,
|
|
3522
|
+
// react-native-qrcode-svg: share the host's react-native-web instance
|
|
3523
|
+
// (sc-1265, sc-6105).
|
|
3524
|
+
// * date-fns and the maths set: an agent widget is transpiled, not
|
|
3525
|
+
// bundled, so the host must resolve its bare imports (sc-7191).
|
|
3526
|
+
const HOST_EXTERNAL_SPECIFIERS = [
|
|
3527
|
+
"react",
|
|
3528
|
+
"react/jsx-runtime",
|
|
3529
|
+
"react/jsx-dev-runtime",
|
|
3530
|
+
"react-dom",
|
|
3531
|
+
"react-dom/client",
|
|
3532
|
+
"@colixsystems/widget-sdk",
|
|
3533
|
+
"lucide-react-native",
|
|
3534
|
+
"react-native-svg",
|
|
3535
|
+
"date-fns",
|
|
3536
|
+
"react-native-qrcode-svg",
|
|
3537
|
+
"react-native",
|
|
3538
|
+
"decimal.js",
|
|
3539
|
+
"d3-scale",
|
|
3540
|
+
"d3-shape",
|
|
3541
|
+
"d3-array",
|
|
3542
|
+
"simple-statistics",
|
|
3543
|
+
];
|
|
3544
|
+
|
|
3545
|
+
// Vetted roots whose subpaths load on web (`leaflet/dist/leaflet.css`): the
|
|
3546
|
+
// bundle inlines them, so no exact-name host shim stands in the way.
|
|
3547
|
+
const SUBPATH_IMPORT_ROOTS = ALLOWED_BARE_IMPORTS.filter(
|
|
3548
|
+
(spec) => !HOST_EXTERNAL_SPECIFIERS.includes(spec),
|
|
3549
|
+
);
|
|
3550
|
+
|
|
3508
3551
|
// REQ-WSDK-PLATFORM §3.5: host-API URL patterns the linter scans for as a
|
|
3509
3552
|
// soft warning. None of these block the lint by themselves — they prompt
|
|
3510
3553
|
// the marketplace reviewer to spot-check, and they help authors learn the
|
|
@@ -4871,7 +4914,16 @@ const CONTRACT = deepFreeze({
|
|
|
4871
4914
|
// (error): a file importing leaflet / react-leaflet must import
|
|
4872
4915
|
// "leaflet/dist/leaflet.css". `leaflet-default-marker-icon` (warning)
|
|
4873
4916
|
// flags a react-leaflet <Marker> with no `icon`.
|
|
4874
|
-
|
|
4917
|
+
// 1.150.0: narrowing (sc-10502) — the linter scans imports across the whole
|
|
4918
|
+
// file, so a wrapped `import { … } from` (or an `export … from`) of an
|
|
4919
|
+
// unvetted package is `import-not-vetted` on its `from` line. A file
|
|
4920
|
+
// that ships to web may import a SUBPATH only of a bundled vetted
|
|
4921
|
+
// package (`subpathImportRoots`, e.g. "leaflet/dist/leaflet.css"); a
|
|
4922
|
+
// host-provided package's subpath ("date-fns/format") is flagged. A
|
|
4923
|
+
// native-only file keeps every vetted subpath (Metro resolves them).
|
|
4924
|
+
// New `hostExternalSpecifiers` / `subpathImportRoots` lists and the
|
|
4925
|
+
// linter export `subpathImportRootsFor(manifest)`.
|
|
4926
|
+
version: "1.150.0",
|
|
4875
4927
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
4876
4928
|
hooks: HOOKS,
|
|
4877
4929
|
primitives: PRIMITIVES,
|
|
@@ -4898,6 +4950,8 @@ const CONTRACT = deepFreeze({
|
|
|
4898
4950
|
bannedApis: BANNED_APIS,
|
|
4899
4951
|
vettedImports: VETTED_IMPORTS,
|
|
4900
4952
|
allowedBareImports: ALLOWED_BARE_IMPORTS,
|
|
4953
|
+
hostExternalSpecifiers: HOST_EXTERNAL_SPECIFIERS,
|
|
4954
|
+
subpathImportRoots: SUBPATH_IMPORT_ROOTS,
|
|
4901
4955
|
hostApiUrlPatterns: HOST_API_URL_PATTERNS,
|
|
4902
4956
|
payloadValueTypes: PAYLOAD_VALUE_TYPES,
|
|
4903
4957
|
chargeCurrency: CHARGE_CURRENCY,
|
package/dist/contract.js
CHANGED
|
@@ -3501,6 +3501,49 @@ const ALLOWED_BARE_IMPORTS = [
|
|
|
3501
3501
|
...CORE_INFRA_BARE_IMPORTS,
|
|
3502
3502
|
];
|
|
3503
3503
|
|
|
3504
|
+
// sc-10502: the bare specifiers the web host RESOLVES for a loaded widget
|
|
3505
|
+
// instead of letting its bundle inline a copy. The web-entry bundler marks
|
|
3506
|
+
// exactly these `external` and the Studio loader shims each by EXACT name, so
|
|
3507
|
+
// a subpath of one (`date-fns/format`) cannot load in the web Player.
|
|
3508
|
+
// `dev-shims.hostExternalSpecifiers()` returns this list; the loader's
|
|
3509
|
+
// module-load guard (frontend widgetLoader.js) throws if its shims drift.
|
|
3510
|
+
//
|
|
3511
|
+
// Why each entry is host-resolved, not bundled:
|
|
3512
|
+
// * react + its JSX runtimes: one React instance (hooks, context).
|
|
3513
|
+
// * react-dom, react-dom/client: react-leaflet portals through react-dom;
|
|
3514
|
+
// a second renderer beside the host's React crashes (sc-1064).
|
|
3515
|
+
// * @colixsystems/widget-sdk: the host-built SDK carries datastore, theme
|
|
3516
|
+
// and navigation.
|
|
3517
|
+
// * react-native, lucide-react-native, react-native-svg,
|
|
3518
|
+
// react-native-qrcode-svg: share the host's react-native-web instance
|
|
3519
|
+
// (sc-1265, sc-6105).
|
|
3520
|
+
// * date-fns and the maths set: an agent widget is transpiled, not
|
|
3521
|
+
// bundled, so the host must resolve its bare imports (sc-7191).
|
|
3522
|
+
const HOST_EXTERNAL_SPECIFIERS = [
|
|
3523
|
+
"react",
|
|
3524
|
+
"react/jsx-runtime",
|
|
3525
|
+
"react/jsx-dev-runtime",
|
|
3526
|
+
"react-dom",
|
|
3527
|
+
"react-dom/client",
|
|
3528
|
+
"@colixsystems/widget-sdk",
|
|
3529
|
+
"lucide-react-native",
|
|
3530
|
+
"react-native-svg",
|
|
3531
|
+
"date-fns",
|
|
3532
|
+
"react-native-qrcode-svg",
|
|
3533
|
+
"react-native",
|
|
3534
|
+
"decimal.js",
|
|
3535
|
+
"d3-scale",
|
|
3536
|
+
"d3-shape",
|
|
3537
|
+
"d3-array",
|
|
3538
|
+
"simple-statistics",
|
|
3539
|
+
];
|
|
3540
|
+
|
|
3541
|
+
// Vetted roots whose subpaths load on web (`leaflet/dist/leaflet.css`): the
|
|
3542
|
+
// bundle inlines them, so no exact-name host shim stands in the way.
|
|
3543
|
+
const SUBPATH_IMPORT_ROOTS = ALLOWED_BARE_IMPORTS.filter(
|
|
3544
|
+
(spec) => !HOST_EXTERNAL_SPECIFIERS.includes(spec),
|
|
3545
|
+
);
|
|
3546
|
+
|
|
3504
3547
|
// REQ-WSDK-PLATFORM §3.5: host-API URL patterns the linter scans for as a
|
|
3505
3548
|
// soft warning. None of these block the lint by themselves — they prompt
|
|
3506
3549
|
// the marketplace reviewer to spot-check, and they help authors learn the
|
|
@@ -4867,7 +4910,16 @@ const CONTRACT = deepFreeze({
|
|
|
4867
4910
|
// (error): a file importing leaflet / react-leaflet must import
|
|
4868
4911
|
// "leaflet/dist/leaflet.css". `leaflet-default-marker-icon` (warning)
|
|
4869
4912
|
// flags a react-leaflet <Marker> with no `icon`.
|
|
4870
|
-
|
|
4913
|
+
// 1.150.0: narrowing (sc-10502) — the linter scans imports across the whole
|
|
4914
|
+
// file, so a wrapped `import { … } from` (or an `export … from`) of an
|
|
4915
|
+
// unvetted package is `import-not-vetted` on its `from` line. A file
|
|
4916
|
+
// that ships to web may import a SUBPATH only of a bundled vetted
|
|
4917
|
+
// package (`subpathImportRoots`, e.g. "leaflet/dist/leaflet.css"); a
|
|
4918
|
+
// host-provided package's subpath ("date-fns/format") is flagged. A
|
|
4919
|
+
// native-only file keeps every vetted subpath (Metro resolves them).
|
|
4920
|
+
// New `hostExternalSpecifiers` / `subpathImportRoots` lists and the
|
|
4921
|
+
// linter export `subpathImportRootsFor(manifest)`.
|
|
4922
|
+
version: "1.150.0",
|
|
4871
4923
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
4872
4924
|
hooks: HOOKS,
|
|
4873
4925
|
primitives: PRIMITIVES,
|
|
@@ -4894,6 +4946,8 @@ const CONTRACT = deepFreeze({
|
|
|
4894
4946
|
bannedApis: BANNED_APIS,
|
|
4895
4947
|
vettedImports: VETTED_IMPORTS,
|
|
4896
4948
|
allowedBareImports: ALLOWED_BARE_IMPORTS,
|
|
4949
|
+
hostExternalSpecifiers: HOST_EXTERNAL_SPECIFIERS,
|
|
4950
|
+
subpathImportRoots: SUBPATH_IMPORT_ROOTS,
|
|
4897
4951
|
hostApiUrlPatterns: HOST_API_URL_PATTERNS,
|
|
4898
4952
|
payloadValueTypes: PAYLOAD_VALUE_TYPES,
|
|
4899
4953
|
chargeCurrency: CHARGE_CURRENCY,
|
package/dist/dev-shims.js
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
// runtime-enumerates the host's namespace). Imports of these packages from a
|
|
20
20
|
// sibling .jsx file return `422` from the dev server with a clear message.
|
|
21
21
|
|
|
22
|
+
import { CONTRACT } from "./contract.js";
|
|
23
|
+
|
|
22
24
|
// React named exports the Studio re-exposes through the shim. Mirrors the
|
|
23
25
|
// browser's `react` named-export surface. Adding a new hook in a React minor
|
|
24
26
|
// release means appending it here.
|
|
@@ -61,81 +63,16 @@ export const REACT_NAMED_EXPORTS = [
|
|
|
61
63
|
// React's automatic JSX runtime: jsx / jsxs / Fragment, plus the dev variant.
|
|
62
64
|
export const REACT_JSX_RUNTIME_EXPORTS = ["jsx", "jsxs", "Fragment", "jsxDEV"];
|
|
63
65
|
|
|
64
|
-
// REQ-WSDK-PLATFORM bundle (sc-1064): the canonical list of bare specifiers the
|
|
65
|
-
// runtime web host RESOLVES for a loaded widget — i.e. the ones the Studio's
|
|
66
|
-
// `widgetLoader.buildShims()` re-exports from `globalThis.__appstudioWidgetHost__`
|
|
67
|
-
// rather than letting a bundle inline a second copy. The web-entry bundler
|
|
68
|
-
// (`webbundle.bundleWebEntry`, used by both the publish packer and the dev
|
|
69
|
-
// server) marks EXACTLY these `external`, so everything else the entry imports
|
|
70
|
-
// (`react-leaflet`, `leaflet`, CSS, PNG assets, …) gets inlined.
|
|
71
|
-
//
|
|
72
|
-
// This is the SINGLE source of truth (CLAUDE.md §3). It MUST stay in lockstep
|
|
73
|
-
// with the frontend's `widgetLoader._hostVettedModules()` + the core React/SDK
|
|
74
|
-
// shims it builds. Two backstops enforce the equality: (1) this list's
|
|
75
|
-
// contents are pinned by the SDK-side importable test
|
|
76
|
-
// (`packages/widget-sdk/src/__tests__/host-externals.test.js`), and (2) the
|
|
77
|
-
// loader's own module-load guard in `frontend/src/services/widgetLoader.js`
|
|
78
|
-
// throws at import time if `hostShimmedSpecifiers()` drifts from
|
|
79
|
-
// `hostExternalSpecifiers()` — the loader's shimmed set can't be asserted from
|
|
80
|
-
// a Node test (it imports `react-dom` etc.), so that equality is enforced by
|
|
81
|
-
// the load-time guard rather than a separate frontend test.
|
|
82
|
-
//
|
|
83
|
-
// Why each entry is host-resolved (NOT bundled):
|
|
84
|
-
// * `react`, `react/jsx-runtime`, `react/jsx-dev-runtime` — the widget MUST
|
|
85
|
-
// share the host's ONE React instance (hooks + context); a bundled second
|
|
86
|
-
// React breaks both.
|
|
87
|
-
// * `react-dom`, `react-dom/client` — `react-leaflet`'s Popup/Pane render
|
|
88
|
-
// through `createPortal` from `react-dom`. A bundled second react-dom
|
|
89
|
-
// paired with the host's external React is a mismatched renderer and
|
|
90
|
-
// crashes. The Studio is a React-DOM app, so it shims its own react-dom.
|
|
91
|
-
// On React 19 the two specifiers expose DIFFERENT surfaces — createPortal /
|
|
92
|
-
// flushSync / version live on `react-dom`, createRoot / hydrateRoot on
|
|
93
|
-
// `react-dom/client` — so the loader stashes + shims each from its own real
|
|
94
|
-
// host namespace (sc-1064).
|
|
95
|
-
// * `@colixsystems/widget-sdk` — the widget must route hooks through the SAME
|
|
96
|
-
// SDK instance the host built (datastore/theme/navigation live there).
|
|
97
|
-
// * `lucide-react-native`, `react-native-svg`, `date-fns`,
|
|
98
|
-
// `react-native-qrcode-svg` — vetted packages the host already bundles +
|
|
99
|
-
// shims (`_hostVettedModules`); sharing the host copy keeps icons/SVG on
|
|
100
|
-
// the host's react-native-web instance. qrcode-svg draws through
|
|
101
|
-
// react-native-svg, so externalising it keeps the QR on that same
|
|
102
|
-
// instance instead of a bundle inlining a second one (sc-6105).
|
|
103
|
-
const HOST_EXTERNAL_SPECIFIERS = [
|
|
104
|
-
"react",
|
|
105
|
-
"react/jsx-runtime",
|
|
106
|
-
"react/jsx-dev-runtime",
|
|
107
|
-
"react-dom",
|
|
108
|
-
"react-dom/client",
|
|
109
|
-
"@colixsystems/widget-sdk",
|
|
110
|
-
"lucide-react-native",
|
|
111
|
-
"react-native-svg",
|
|
112
|
-
"date-fns",
|
|
113
|
-
"react-native-qrcode-svg",
|
|
114
|
-
// sc-1265: a bundled widget (and any vetted RN package it inlines, e.g.
|
|
115
|
-
// react-native-gesture-handler) imports `react-native`; the host resolves it
|
|
116
|
-
// to its OWN single react-native-web instance through a blob shim, so the
|
|
117
|
-
// bundle shares one RN-web (StyleSheet/context) instead of inlining a second
|
|
118
|
-
// copy. On native, Metro resolves the real react-native in the export.
|
|
119
|
-
"react-native",
|
|
120
|
-
// sc-7191: the vetted pure-JS maths set. Externalised for the same reason as
|
|
121
|
-
// date-fns — an AI-agent widget is transpiled, never bundled, so the host
|
|
122
|
-
// must resolve the bare import at runtime.
|
|
123
|
-
"decimal.js",
|
|
124
|
-
"d3-scale",
|
|
125
|
-
"d3-shape",
|
|
126
|
-
"d3-array",
|
|
127
|
-
"simple-statistics",
|
|
128
|
-
];
|
|
129
|
-
|
|
130
66
|
/**
|
|
131
67
|
* The canonical "host-resolved at runtime" bare-specifier set — the externals
|
|
132
|
-
* the web-entry bundler must NOT inline.
|
|
133
|
-
*
|
|
68
|
+
* the web-entry bundler must NOT inline. Sourced from
|
|
69
|
+
* `CONTRACT.hostExternalSpecifiers`, which documents each entry. Returns a
|
|
70
|
+
* fresh array each call so a caller can't mutate the source of truth.
|
|
134
71
|
*
|
|
135
72
|
* @returns {string[]}
|
|
136
73
|
*/
|
|
137
74
|
export function hostExternalSpecifiers() {
|
|
138
|
-
return [...
|
|
75
|
+
return [...CONTRACT.hostExternalSpecifiers];
|
|
139
76
|
}
|
|
140
77
|
|
|
141
78
|
function reactShimSrc() {
|
package/dist/index.d.ts
CHANGED
|
@@ -3380,6 +3380,10 @@ export interface AiWidgetContract {
|
|
|
3380
3380
|
readonly bundleExportContract: ReadonlyArray<ContractBundleShape>;
|
|
3381
3381
|
readonly bannedApis: ReadonlyArray<ContractBannedApi>;
|
|
3382
3382
|
readonly allowedBareImports: ReadonlyArray<string>;
|
|
3383
|
+
/** Bare specifiers the web host provides by exact name; a widget bundle never inlines them. */
|
|
3384
|
+
readonly hostExternalSpecifiers: ReadonlyArray<string>;
|
|
3385
|
+
/** Bundled vetted package roots: the only roots whose subpaths load in the web Player. */
|
|
3386
|
+
readonly subpathImportRoots: ReadonlyArray<string>;
|
|
3383
3387
|
/** sc-4686 — the render-time fallback currency when the host supplies none. */
|
|
3384
3388
|
readonly chargeCurrency: string;
|
|
3385
3389
|
/** sc-4686 — how each currency is written; read by `formatMoney`, never Intl. */
|
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
|
};
|