@colixsystems/widget-sdk 0.44.0 → 0.45.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 +55 -1
- package/dist/contract.cjs +11 -2
- package/dist/contract.js +11 -2
- package/dist/dev-shims.js +6 -0
- package/dist/linter.cjs +59 -0
- package/dist/linter.js +59 -0
- package/dist/lucideIconNames.cjs +1462 -0
- package/dist/lucideIconNames.js +1460 -0
- package/dist/webbundle.js +23 -0
- package/package.json +7 -4
- package/src/dev-shims.js +261 -0
- package/src/webbundle.js +148 -0
package/README.md
CHANGED
|
@@ -49,10 +49,24 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
49
49
|
|
|
50
50
|
## Status
|
|
51
51
|
|
|
52
|
-
`v0.
|
|
52
|
+
`v0.45.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**.
|
|
53
|
+
|
|
54
|
+
### What's new in 0.45.0
|
|
55
|
+
|
|
56
|
+
**New linter rule `lucide-unknown-icon` (sc-1366).** `lucide-react-native` is a vetted import the bundler **externalises**, so esbuild never checks the *named* imports — a widget that imports an icon name absent from the pinned `lucide-react-native@0.368.0` (e.g. `import { House } from "lucide-react-native"`, when that version only ships `Home` — `House` was added to lucide later) bundles and publishes cleanly, then fails only at **runtime load** ("does not provide an export named 'House'") and renders blank. The linter now rejects such imports at publish so the failure becomes a repair-loop finding instead of a broken shipped widget. The valid set is committed data (`src/lucideIconNames.{cjs,js}` — the 1451 base export names + the generated-from `LUCIDE_VERSION`) because the linter runs in zero-dependency contexts where `lucide-react-native` (and its `react-native` peer) is not installed; alias forms (`<Name>Icon`, `Lucide<Name>`) are normalised. Regenerate after a lucide bump with `node scripts/generate-lucide-icon-names.cjs`; a test asserts `LUCIDE_VERSION` matches the frontend pin so the set can't go stale. The `CONTRACT` object is unchanged (no new field), so `CONTRACT.version` stays at `1.32.0`. The author/agent-facing mirror of this rule is the "Icons" note in the AI agent's `DEFAULT_SYSTEM_PROMPT` (shipped in the companion sc-1273/sc-1362 prompt PR).
|
|
53
57
|
|
|
54
58
|
### What's new in 0.44.0
|
|
55
59
|
|
|
60
|
+
**AI-generated widgets are bundled at publish (sc-1265).** Until this release the AI agent's publish path served the validated LLM source **verbatim** (transpiled, not bundled), which meant an AI widget could only import the host-shimmed specifiers — a draft that imported, say, `react-native-gesture-handler` published cleanly but threw "unresolvable bare imports" at load. Marketplace widgets, in contrast, are esbuild-**bundled** by the publish packer and get the full vetted-import surface. This release closes that gap: every AI publish now ships through the **`@appstudio/widget-bundler`** sidecar (`services/widget-bundler` — internal HTTP service that wraps the SAME `bundleWebEntry` the marketplace packer uses), so the bundled output is byte-shape identical regardless of who published.
|
|
61
|
+
|
|
62
|
+
- **`react-native` is now host-resolved on web.** A bundled widget — AI or marketplace — that imports `react-native` (directly, or via a vetted RN package like `react-native-gesture-handler` that imports it internally) leaves the specifier as a bare import; the Studio loader shims it to the host's **single** `react-native-web` instance. That keeps the bundle small (gesture-handler probes dropped from 762 KB to ~248 KB) and avoids two RN-web copies in the same page (`StyleSheet` + context conflicts).
|
|
63
|
+
- **`hostExternalSpecifiers()` grew `react-native`.** The canonical "host-resolved at runtime" set the bundler externalises now lists react family + `react-dom` + `react-dom/client` + `@colixsystems/widget-sdk` + the vetted shimmed packages (lucide / svg / date-fns) + `react-native`.
|
|
64
|
+
- **`bundleWebEntry` resolves RN web builds.** `resolveExtensions` is `.web.js`-first, `mainFields` is `[browser, module, main]`, and the `browser` export condition is active — so esbuild picks the web build of an RN-shape package automatically. No alias step; `react-native` stays external.
|
|
65
|
+
- **Contract description fix.** The vetted-imports entry for `react-native` previously claimed "the host bundler aliases this to react-native-web" — that was never true and the misclaim leaked into the AI system prompt. The description now reads: "`react-native` is host-resolved to the host's single react-native-web instance (external, shimmed) — NOT aliased at bundle time".
|
|
66
|
+
- **AI publish storage shape.** Every AI publish now writes the `bundleFiles` JSONB column with the bundled web entry under `widget.web.jsx` and the transpiled RN source under `widget.native.jsx`. `bundleSource` (the legacy single-file column) stays NULL on AI rows; `resolveWebBundleSource` / `resolveNativeBundleSource` pick the right file at load time.
|
|
67
|
+
- **Bundle failure → repair loop.** A bundler error is now a `bundle.web` finding the existing publish repair loop folds into the next attempt's prompt, so the model can self-correct a draft whose imports the bundler refused.
|
|
68
|
+
- **`CONTRACT.version` → `1.32.0`** (additive: the `react-native` host-resolution behaviour + a corrected vetted-imports description; the SET of vetted imports is unchanged).
|
|
69
|
+
|
|
56
70
|
**Vetted `@shopify/react-native-skia` for canvas-style graphics & games (sc-1270).** Widgets that need true 2D/GPU canvas drawing (games, custom visualisations) can now `import` Skia. It is **native-only** (`platforms: ["native"]`, like `react-native-maps` / `lottie-react-native`): author it in `widget.native.jsx` and pair it with a web variant in `widget.web.jsx` — a browser `<canvas>` or `react-native-svg`. This closes the gap where a raw `<canvas>` rendered in the web Player but crashed the Expo export ("View config getter callback for component `canvas` … received undefined") because React Native has no `<canvas>`. The compiler pins `@shopify/react-native-skia` in the exported app's `package.json`; no host shim is added (native-only packages are never shimmed, and there is no Skia web build wired into the Player). Unified Skia-on-web (CanvasKit/WASM) is a documented follow-up. Additive — `CONTRACT.version` bumped to 1.31.0.
|
|
57
71
|
|
|
58
72
|
### What's new in 0.43.0
|
|
@@ -488,6 +502,46 @@ The manifest declares the matching scope:
|
|
|
488
502
|
|
|
489
503
|
The server-side gate is `canGrant` on the target record — Studio owners pass automatically; an APP_USER holds `canGrant` as the record's creator or via a delegated grant. A caller without `canGrant` receives `PermissionError { code: "FORBIDDEN" }`. The hook collapses to a stable no-op when `tableId` or `recordId` is null/empty — so a widget can render its picker first, then bind to the picked record without conditional hook tricks.
|
|
490
504
|
|
|
505
|
+
## Cross-platform widgets (single-file vs split)
|
|
506
|
+
|
|
507
|
+
Every widget runs in **both** the web Player and the exported native (Expo) app.
|
|
508
|
+
There are two ways to author one, and the **file set you ship decides
|
|
509
|
+
`supportedPlatforms`** — you don't hand-declare it, the platform derives it.
|
|
510
|
+
|
|
511
|
+
**1. Single file — `widget.jsx` (the default).** Works on web AND native. Pick
|
|
512
|
+
this when the widget needs only the SDK primitives + hooks, or only packages
|
|
513
|
+
that support both platforms (every `["web", "native"]` entry in
|
|
514
|
+
`CONTRACT.vettedImports` — `react-native`, `react-native-svg`, `date-fns`,
|
|
515
|
+
`react-native-paper`, `@shopify/flash-list`, `react-native-reanimated`,
|
|
516
|
+
`react-native-gesture-handler`, `expo-linear-gradient`, …). Most widgets land
|
|
517
|
+
here. Derives `supportedPlatforms: ["web", "native"]`.
|
|
518
|
+
|
|
519
|
+
**2. Split implementation — `widget.web.jsx` + `widget.native.jsx`.** Pick this
|
|
520
|
+
when the widget needs a package that only runs on ONE platform. Each file
|
|
521
|
+
targets its platform and imports the package that works there; shared logic
|
|
522
|
+
lives in a sibling `./helper.js` both files import. This is the **canonical
|
|
523
|
+
pattern** for graphics, media, and any platform-divergent library:
|
|
524
|
+
|
|
525
|
+
| Capability | `widget.native.jsx` | `widget.web.jsx` |
|
|
526
|
+
| ---------- | ------------------- | ---------------- |
|
|
527
|
+
| Maps | `react-native-maps` | `react-leaflet` / `leaflet` |
|
|
528
|
+
| Canvas / 2D-GPU drawing | `@shopify/react-native-skia` | `<canvas>` or `react-native-svg` |
|
|
529
|
+
| Video | `expo-video` | the browser `<video>` element |
|
|
530
|
+
| Audio | `expo-audio` | the browser `<audio>` element |
|
|
531
|
+
| Lottie animation | `lottie-react-native` | `lottie-react` |
|
|
532
|
+
| Embedded web content | `react-native-webview` | an `<iframe>` |
|
|
533
|
+
|
|
534
|
+
A few native-only packages (`@react-native-community/datetimepicker`,
|
|
535
|
+
`expo-clipboard`) are already wrapped by an SDK primitive/hook
|
|
536
|
+
(`<DateTimePicker>`, `useClipboard()`) that resolves the right implementation on
|
|
537
|
+
each platform — reach for those first and you stay single-file.
|
|
538
|
+
|
|
539
|
+
**The linter enforces honesty at publish.** A single-file widget that imports a
|
|
540
|
+
native-only package while claiming web (or vice-versa) fails the
|
|
541
|
+
`import-platform-mismatch` rule — move the import into the file that targets its
|
|
542
|
+
platform, or ship the split pair. So you can't accidentally publish a widget
|
|
543
|
+
that renders on one platform and blanks on the other.
|
|
544
|
+
|
|
491
545
|
## Linter
|
|
492
546
|
|
|
493
547
|
```sh
|
package/dist/contract.cjs
CHANGED
|
@@ -1140,7 +1140,7 @@ const VETTED_IMPORTS = [
|
|
|
1140
1140
|
platforms: ["web", "native"],
|
|
1141
1141
|
category: "primitive",
|
|
1142
1142
|
description:
|
|
1143
|
-
"Direct RN imports for APIs the SDK hasn't re-exported yet. On web
|
|
1143
|
+
"Direct RN imports for APIs the SDK hasn't re-exported yet. On web `react-native` is host-resolved to the host's single react-native-web instance (external, shimmed) — NOT aliased at bundle time — so a bundled widget shares the host's RN-web (one StyleSheet/context, no double-instance conflicts). On native Metro resolves the real library.",
|
|
1144
1144
|
},
|
|
1145
1145
|
{
|
|
1146
1146
|
specifier: "axios",
|
|
@@ -1672,7 +1672,16 @@ const CONTRACT = deepFreeze({
|
|
|
1672
1672
|
// variant in widget.web.jsx. Pinned in the compiler's export
|
|
1673
1673
|
// package.json; no host shim (native-only is never shimmed). No hook,
|
|
1674
1674
|
// primitive, manifest field, or token changed shape — minor bump.
|
|
1675
|
-
|
|
1675
|
+
//
|
|
1676
|
+
// 1.32.0: behaviour (sc-1265) — `react-native` is now host-resolved on web
|
|
1677
|
+
// as an EXTERNAL, loader-shimmed to the host's single react-native-web
|
|
1678
|
+
// instance, instead of being aliased at bundle time. AI publishes now
|
|
1679
|
+
// bundle through the widget-bundler sidecar (byte-shape identical to the
|
|
1680
|
+
// marketplace packer), so a bundled widget shares the host's one RN-web
|
|
1681
|
+
// instance — one StyleSheet/context, no double-instance conflicts. The
|
|
1682
|
+
// vetted-import SET is unchanged (react-native was already vetted); only
|
|
1683
|
+
// its web resolution + description changed — minor bump.
|
|
1684
|
+
version: "1.32.0",
|
|
1676
1685
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
1677
1686
|
hooks: HOOKS,
|
|
1678
1687
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -1140,7 +1140,7 @@ const VETTED_IMPORTS = [
|
|
|
1140
1140
|
platforms: ["web", "native"],
|
|
1141
1141
|
category: "primitive",
|
|
1142
1142
|
description:
|
|
1143
|
-
"Direct RN imports for APIs the SDK hasn't re-exported yet. On web
|
|
1143
|
+
"Direct RN imports for APIs the SDK hasn't re-exported yet. On web `react-native` is host-resolved to the host's single react-native-web instance (external, shimmed) — NOT aliased at bundle time — so a bundled widget shares the host's RN-web (one StyleSheet/context, no double-instance conflicts). On native Metro resolves the real library.",
|
|
1144
1144
|
},
|
|
1145
1145
|
{
|
|
1146
1146
|
specifier: "axios",
|
|
@@ -1672,7 +1672,16 @@ const CONTRACT = deepFreeze({
|
|
|
1672
1672
|
// variant in widget.web.jsx. Pinned in the compiler's export
|
|
1673
1673
|
// package.json; no host shim (native-only is never shimmed). No hook,
|
|
1674
1674
|
// primitive, manifest field, or token changed shape — minor bump.
|
|
1675
|
-
|
|
1675
|
+
//
|
|
1676
|
+
// 1.32.0: behaviour (sc-1265) — `react-native` is now host-resolved on web
|
|
1677
|
+
// as an EXTERNAL, loader-shimmed to the host's single react-native-web
|
|
1678
|
+
// instance, instead of being aliased at bundle time. AI publishes now
|
|
1679
|
+
// bundle through the widget-bundler sidecar (byte-shape identical to the
|
|
1680
|
+
// marketplace packer), so a bundled widget shares the host's one RN-web
|
|
1681
|
+
// instance — one StyleSheet/context, no double-instance conflicts. The
|
|
1682
|
+
// vetted-import SET is unchanged (react-native was already vetted); only
|
|
1683
|
+
// its web resolution + description changed — minor bump.
|
|
1684
|
+
version: "1.32.0",
|
|
1676
1685
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
1677
1686
|
hooks: HOOKS,
|
|
1678
1687
|
primitives: PRIMITIVES,
|
package/dist/dev-shims.js
CHANGED
|
@@ -107,6 +107,12 @@ const HOST_EXTERNAL_SPECIFIERS = [
|
|
|
107
107
|
"lucide-react-native",
|
|
108
108
|
"react-native-svg",
|
|
109
109
|
"date-fns",
|
|
110
|
+
// sc-1265: a bundled widget (and any vetted RN package it inlines, e.g.
|
|
111
|
+
// react-native-gesture-handler) imports `react-native`; the host resolves it
|
|
112
|
+
// to its OWN single react-native-web instance through a blob shim, so the
|
|
113
|
+
// bundle shares one RN-web (StyleSheet/context) instead of inlining a second
|
|
114
|
+
// copy. On native, Metro resolves the real react-native in the export.
|
|
115
|
+
"react-native",
|
|
110
116
|
];
|
|
111
117
|
|
|
112
118
|
/**
|
package/dist/linter.cjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"use strict";
|
|
12
12
|
|
|
13
13
|
const { CONTRACT } = require("./contract.cjs");
|
|
14
|
+
const { LUCIDE_ICON_NAMES, LUCIDE_VERSION } = require("./lucideIconNames.cjs");
|
|
14
15
|
|
|
15
16
|
function _ruleForIdentifier(identifier, reason) {
|
|
16
17
|
const id = identifier;
|
|
@@ -513,6 +514,63 @@ function _manifestActionRules(manifest) {
|
|
|
513
514
|
return findings;
|
|
514
515
|
}
|
|
515
516
|
|
|
517
|
+
// sc-1366 — invalid lucide icon import gate. lucide-react-native is a vetted
|
|
518
|
+
// import that the bundler EXTERNALISES, so esbuild never checks the named
|
|
519
|
+
// imports; a name that does not exist in the pinned version (`House` was added
|
|
520
|
+
// to lucide AFTER the pinned 0.368.0, which only has `Home`) bundles + publishes
|
|
521
|
+
// cleanly and fails only at runtime load ("does not provide an export named
|
|
522
|
+
// 'House'"), leaving the widget blank. This rule rejects such imports at publish
|
|
523
|
+
// so the agent's repair loop fixes them. The valid set is committed data
|
|
524
|
+
// (lucideIconNames.cjs) because the linter runs where lucide is not installed.
|
|
525
|
+
const LUCIDE_NAME_SET = new Set(LUCIDE_ICON_NAMES);
|
|
526
|
+
const LUCIDE_IMPORT_RE =
|
|
527
|
+
/import\s+(?:[A-Za-z0-9_$]+\s*,\s*)?\{([\s\S]*?)\}\s*from\s*["']lucide-react-native["']/g;
|
|
528
|
+
|
|
529
|
+
// lucide re-exports each base icon as `<Name>`, `<Name>Icon`, and `Lucide<Name>`;
|
|
530
|
+
// the committed set holds only the base names, so normalise the alias forms
|
|
531
|
+
// before the membership check. No base name ends in "Icon" or starts with
|
|
532
|
+
// "Lucide" (asserted by lucide-icon-names.test.js), so stripping is unambiguous.
|
|
533
|
+
function _isValidLucideName(name) {
|
|
534
|
+
if (LUCIDE_NAME_SET.has(name)) return true;
|
|
535
|
+
if (name.endsWith("Icon") && LUCIDE_NAME_SET.has(name.slice(0, -4))) return true;
|
|
536
|
+
if (name.startsWith("Lucide") && LUCIDE_NAME_SET.has(name.slice(6))) return true;
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function _lucideIconRules(source) {
|
|
541
|
+
const findings = [];
|
|
542
|
+
const sourceLines = source.split(/\r?\n/);
|
|
543
|
+
LUCIDE_IMPORT_RE.lastIndex = 0;
|
|
544
|
+
let m;
|
|
545
|
+
while ((m = LUCIDE_IMPORT_RE.exec(source))) {
|
|
546
|
+
const line = source.slice(0, m.index).split(/\r?\n/).length;
|
|
547
|
+
for (const raw of (m[1] || "").split(",")) {
|
|
548
|
+
// Strip any inline comment from the specifier token so a `{ Home, // x`
|
|
549
|
+
// comment can't glue onto and hide the next specifier (e.g. `House`).
|
|
550
|
+
const cleaned = raw
|
|
551
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
552
|
+
.replace(/\/\/[^\n]*/g, "");
|
|
553
|
+
const imported = cleaned.trim().split(/\s+as\s+/)[0].trim();
|
|
554
|
+
if (!imported || imported === "type") continue;
|
|
555
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(imported)) continue;
|
|
556
|
+
if (_isValidLucideName(imported)) continue;
|
|
557
|
+
findings.push({
|
|
558
|
+
rule: "lucide-unknown-icon",
|
|
559
|
+
severity: "error",
|
|
560
|
+
label:
|
|
561
|
+
`lucide-react-native has no export "${imported}" in the pinned ` +
|
|
562
|
+
`lucide-react-native@${LUCIDE_VERSION}. The widget bundles cleanly but FAILS at ` +
|
|
563
|
+
`runtime load ("does not provide an export named '${imported}'") and never ` +
|
|
564
|
+
`renders — replace it with an icon name that exists in that version (lucide ` +
|
|
565
|
+
`adds and renames icons across releases, so a newer name may not exist here).`,
|
|
566
|
+
line,
|
|
567
|
+
snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return findings;
|
|
572
|
+
}
|
|
573
|
+
|
|
516
574
|
function lintSource(source, options) {
|
|
517
575
|
if (typeof source !== "string") {
|
|
518
576
|
return {
|
|
@@ -557,6 +615,7 @@ function lintSource(source, options) {
|
|
|
557
615
|
})),
|
|
558
616
|
);
|
|
559
617
|
findings.push(..._hostApiUrlRules(source));
|
|
618
|
+
findings.push(..._lucideIconRules(source));
|
|
560
619
|
findings.push(
|
|
561
620
|
..._scopeRules(source, options && options.manifest).map((f) => ({
|
|
562
621
|
...f,
|
package/dist/linter.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// system prompt, the linter, and the runtime allowlist agree.
|
|
8
8
|
|
|
9
9
|
import { CONTRACT } from "./contract.js";
|
|
10
|
+
import { LUCIDE_ICON_NAMES, LUCIDE_VERSION } from "./lucideIconNames.js";
|
|
10
11
|
|
|
11
12
|
// Per-identifier match rule. Most banned identifiers compile to a
|
|
12
13
|
// whole-word match; a few have special syntax (`Function(`, `new Function`,
|
|
@@ -601,6 +602,63 @@ function _manifestActionRules(manifest) {
|
|
|
601
602
|
return findings;
|
|
602
603
|
}
|
|
603
604
|
|
|
605
|
+
// sc-1366 — invalid lucide icon import gate. lucide-react-native is a vetted
|
|
606
|
+
// import that the bundler EXTERNALISES, so esbuild never checks the named
|
|
607
|
+
// imports; a name that does not exist in the pinned version (`House` was added
|
|
608
|
+
// to lucide AFTER the pinned 0.368.0, which only has `Home`) bundles + publishes
|
|
609
|
+
// cleanly and fails only at runtime load ("does not provide an export named
|
|
610
|
+
// 'House'"), leaving the widget blank. This rule rejects such imports at publish
|
|
611
|
+
// so the agent's repair loop fixes them. The valid set is committed data
|
|
612
|
+
// (lucideIconNames.js) because the linter runs where lucide is not installed.
|
|
613
|
+
const LUCIDE_NAME_SET = new Set(LUCIDE_ICON_NAMES);
|
|
614
|
+
const LUCIDE_IMPORT_RE =
|
|
615
|
+
/import\s+(?:[A-Za-z0-9_$]+\s*,\s*)?\{([\s\S]*?)\}\s*from\s*["']lucide-react-native["']/g;
|
|
616
|
+
|
|
617
|
+
// lucide re-exports each base icon as `<Name>`, `<Name>Icon`, and `Lucide<Name>`;
|
|
618
|
+
// the committed set holds only the base names, so normalise the alias forms
|
|
619
|
+
// before the membership check. No base name ends in "Icon" or starts with
|
|
620
|
+
// "Lucide" (asserted by lucide-icon-names.test.js), so stripping is unambiguous.
|
|
621
|
+
function _isValidLucideName(name) {
|
|
622
|
+
if (LUCIDE_NAME_SET.has(name)) return true;
|
|
623
|
+
if (name.endsWith("Icon") && LUCIDE_NAME_SET.has(name.slice(0, -4))) return true;
|
|
624
|
+
if (name.startsWith("Lucide") && LUCIDE_NAME_SET.has(name.slice(6))) return true;
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function _lucideIconRules(source) {
|
|
629
|
+
const findings = [];
|
|
630
|
+
const sourceLines = source.split(/\r?\n/);
|
|
631
|
+
LUCIDE_IMPORT_RE.lastIndex = 0;
|
|
632
|
+
let m;
|
|
633
|
+
while ((m = LUCIDE_IMPORT_RE.exec(source))) {
|
|
634
|
+
const line = source.slice(0, m.index).split(/\r?\n/).length;
|
|
635
|
+
for (const raw of (m[1] || "").split(",")) {
|
|
636
|
+
// Strip any inline comment from the specifier token so a `{ Home, // x`
|
|
637
|
+
// comment can't glue onto and hide the next specifier (e.g. `House`).
|
|
638
|
+
const cleaned = raw
|
|
639
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
640
|
+
.replace(/\/\/[^\n]*/g, "");
|
|
641
|
+
const imported = cleaned.trim().split(/\s+as\s+/)[0].trim();
|
|
642
|
+
if (!imported || imported === "type") continue;
|
|
643
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(imported)) continue;
|
|
644
|
+
if (_isValidLucideName(imported)) continue;
|
|
645
|
+
findings.push({
|
|
646
|
+
rule: "lucide-unknown-icon",
|
|
647
|
+
severity: "error",
|
|
648
|
+
label:
|
|
649
|
+
`lucide-react-native has no export "${imported}" in the pinned ` +
|
|
650
|
+
`lucide-react-native@${LUCIDE_VERSION}. The widget bundles cleanly but FAILS at ` +
|
|
651
|
+
`runtime load ("does not provide an export named '${imported}'") and never ` +
|
|
652
|
+
`renders — replace it with an icon name that exists in that version (lucide ` +
|
|
653
|
+
`adds and renames icons across releases, so a newer name may not exist here).`,
|
|
654
|
+
line,
|
|
655
|
+
snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return findings;
|
|
660
|
+
}
|
|
661
|
+
|
|
604
662
|
export function lintSource(source, options) {
|
|
605
663
|
if (typeof source !== "string") {
|
|
606
664
|
return {
|
|
@@ -647,6 +705,7 @@ export function lintSource(source, options) {
|
|
|
647
705
|
);
|
|
648
706
|
// REQ-WSDK-PLATFORM §3.5: soft host-API URL warning (does not block).
|
|
649
707
|
findings.push(..._hostApiUrlRules(source));
|
|
708
|
+
findings.push(..._lucideIconRules(source));
|
|
650
709
|
// REQ-USERMGMT / REQ-ACL-SYS M3 — scope-aware rules. Run after the
|
|
651
710
|
// line-by-line scan so banned-identifier findings stay first in the
|
|
652
711
|
// output.
|