@colixsystems/widget-sdk 0.36.0 → 0.38.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 +16 -2
- package/dist/contract.cjs +107 -1
- package/dist/contract.js +107 -1
- package/dist/hooks.js +33 -5
- package/dist/linter.cjs +152 -3
- package/dist/linter.js +181 -18
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
26
26
|
| **CORE** | `useFill()` | `boolean` | `ctx.fill` — no scope. `true` when the host sized this widget to fill its page-grid tile's reserved height (containers + media fill by default; the author can override per tile). Media-style widgets switch to a `flex: 1` / `height: "100%"` layout; others ignore it. Defaults `false`. |
|
|
27
27
|
| **CORE** | `useClipboard()` | `{ copy, paste, hasContent }` | platform clipboard (web `navigator.clipboard` / native `expo-clipboard`); rejects with `ClipboardError` — no scope |
|
|
28
28
|
| **CORE** | `useToast()` | `{ showToast }` | `ctx.toast.showToast` (falls back to a CustomEvent / console) — no scope |
|
|
29
|
-
| **CORE** | `useI18n()` | `{ t, locale }` | `ctx.i18n` — no scope. `t(key)` resolves the widget-namespaced key (`widget.<id>.<key>`, declared in `manifest.translations`) then
|
|
29
|
+
| **CORE** | `useI18n()` | `{ t, locale }` | `ctx.i18n` — no scope. `t(key)` resolves the widget-namespaced key (`widget.<id>.<key>`, declared in `manifest.translations`) first, then a **predefined shared key** (`shared.<key>`) when `key` is one of the standard strings (`submit`, `cancel`, `save`, `loading`, …), then the raw key. Use a shared key for an identical default string so it translates once and any per-instance `widget.<id>.<key>` override still wins. |
|
|
30
30
|
| **DATASTORE** (`ctx.datastore`) | `useDatastoreQuery(table, options?)` | `{ data, loading, error, refetch }` | `records(table).list` (unwraps `{ data, meta }` to `data: []`) — `datastore.read:*` |
|
|
31
31
|
| **DATASTORE** | `useDatastoreRecord(table, id)` | `{ data, loading, error, refetch }` | `records(table).get` — `datastore.read:<table>` |
|
|
32
32
|
| **DATASTORE** | `useDatastoreSchema(tableId)` | `{ schema, loading, error, refetch }` | `schema(tableId)` — `datastore.read:<table>` |
|
|
@@ -47,7 +47,21 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
47
47
|
|
|
48
48
|
## Status
|
|
49
49
|
|
|
50
|
-
`v0.
|
|
50
|
+
`v0.38.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**.
|
|
51
|
+
|
|
52
|
+
### What's new in 0.38.0
|
|
53
|
+
|
|
54
|
+
**Predefined SHARED translation keys (REQ-L10N-SHARED).** The standard strings the default widgets repeat ("Submit", "Cancel", "Save", "Loading…", …) now have a tenant-wide shared namespace `shared.<key>` so an identical string is translated **once** and every widget that uses it inherits the translation.
|
|
55
|
+
|
|
56
|
+
- **New contract field `CONTRACT.sharedTranslationKeys`** — the predefined map (`{ <key>: { en } }`) and the single source the host seeder reads.
|
|
57
|
+
- **New exported helpers** `sharedTranslationPrefix()` / `sharedTranslationKey(key)` / `isSharedTranslationKey(key)` (from `@colixsystems/widget-sdk/contract`).
|
|
58
|
+
- **`useI18n().t(key)` resolution is now three-step**: the per-widget key (`widget.<id>.<key>`) first, then the shared key (`shared.<key>`) when `key` is one of the predefined shared keys, then the raw key / fallback. So a default widget that calls `t("submit")` picks up the shared translation with no manual key entry, and an author who sets `widget.<id>.submit` in the Translations admin still overrides **that instance only**. An author-invented bare key is never silently shared.
|
|
59
|
+
- **The host auto-registers the shared keys** for a tenant at workspace-content seed time and whenever a marketplace or AI-generated widget is added — idempotent and non-destructive (an admin edit is never overwritten). Authors manage / translate them in the Studio Translations screen like any other key.
|
|
60
|
+
- **`CONTRACT.version` → `1.28.0`** (additive: one new contract field + three helper exports + the `useI18n` shared-key step). No existing export changed signature.
|
|
61
|
+
|
|
62
|
+
### What's new in 0.37.0
|
|
63
|
+
|
|
64
|
+
**`react/jsx-runtime` + `react/jsx-dev-runtime` are now vetted imports.** A widget bundle compiled with React's *automatic* JSX runtime (Vite/esbuild's default) emits `import { jsx, jsxs, Fragment } from "react/jsx-runtime"` — code the author never writes by hand. The runtime already treated these as host-provided (the web loader shims both, the AI-agent sandbox stubs them, and the Developer guide documents them as externalized), but the linter's vetted list did not list them, so such a bundle failed publish static analysis with `import-not-vetted` on `react/jsx-runtime`. Both are now on `CONTRACT.vettedImports` as `core` subpaths of the already-vetted `react`. **`CONTRACT.version` → `1.27.0`** (additive: two vetted core subpaths). No existing entry changed shape.
|
|
51
65
|
|
|
52
66
|
### What's new in 0.36.0
|
|
53
67
|
|
package/dist/contract.cjs
CHANGED
|
@@ -1057,6 +1057,20 @@ const VETTED_IMPORTS = [
|
|
|
1057
1057
|
category: "core",
|
|
1058
1058
|
description: "React. Hooks, JSX, lifecycle. Unchanged.",
|
|
1059
1059
|
},
|
|
1060
|
+
{
|
|
1061
|
+
specifier: "react/jsx-runtime",
|
|
1062
|
+
platforms: ["web", "native"],
|
|
1063
|
+
category: "core",
|
|
1064
|
+
description:
|
|
1065
|
+
"React's automatic JSX runtime (jsx/jsxs/Fragment). Not hand-written — the JSX transform injects this import when a bundle is compiled with the automatic runtime. Host-provided on both platforms (the web loader shims it; Metro resolves the real subpath of react), so it is externalized exactly like react.",
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
specifier: "react/jsx-dev-runtime",
|
|
1069
|
+
platforms: ["web", "native"],
|
|
1070
|
+
category: "core",
|
|
1071
|
+
description:
|
|
1072
|
+
"React's automatic JSX dev runtime (jsxDEV/Fragment). The development-mode counterpart to react/jsx-runtime, injected by the JSX transform in dev builds. Host-provided on both platforms, externalized like react.",
|
|
1073
|
+
},
|
|
1060
1074
|
{
|
|
1061
1075
|
specifier: "@colixsystems/widget-sdk",
|
|
1062
1076
|
platforms: ["web", "native"],
|
|
@@ -1271,6 +1285,65 @@ function widgetTranslationKey(id, key) {
|
|
|
1271
1285
|
return `widget.${id}.${key}`;
|
|
1272
1286
|
}
|
|
1273
1287
|
|
|
1288
|
+
// REQ-L10N-SHARED — predefined SHARED translation keys for the standard
|
|
1289
|
+
// strings the default widgets repeat ("Submit", "Cancel", "Loading…", …).
|
|
1290
|
+
// They live in ONE tenant-wide namespace (`shared.<key>`) rather than the
|
|
1291
|
+
// per-widget namespace, so identical strings translate ONCE and every widget
|
|
1292
|
+
// that calls `t("<sharedKey>")` resolves the same dictionary value. An author
|
|
1293
|
+
// still overrides any instance by setting `widget.<id>.<key>` in the
|
|
1294
|
+
// Translations admin — `useI18n` resolves the per-widget key FIRST, then the
|
|
1295
|
+
// shared key, then the raw key, so the override is per-instance.
|
|
1296
|
+
//
|
|
1297
|
+
// This is the analogue of `widget.<id>.` for cross-widget reuse: one prefix,
|
|
1298
|
+
// one definition (the hook reads + the backend seeder writes both call these),
|
|
1299
|
+
// so the wire key and the lookup key can never drift.
|
|
1300
|
+
const SHARED_TRANSLATION_PREFIX = "shared.";
|
|
1301
|
+
function sharedTranslationPrefix() {
|
|
1302
|
+
return SHARED_TRANSLATION_PREFIX;
|
|
1303
|
+
}
|
|
1304
|
+
function sharedTranslationKey(key) {
|
|
1305
|
+
return `${SHARED_TRANSLATION_PREFIX}${key}`;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// The predefined set. Map of bare key -> { en } (English default only; the
|
|
1309
|
+
// tenant adds target-language values once in the Translations admin and every
|
|
1310
|
+
// widget inherits them). Adding/removing a key is a CONTRACT change (it grows
|
|
1311
|
+
// the dictionary every tenant gets), so the list lives with the contract and
|
|
1312
|
+
// the seeder reads it from here — never a second copy.
|
|
1313
|
+
const SHARED_TRANSLATION_KEYS = Object.freeze({
|
|
1314
|
+
submit: { en: "Submit" },
|
|
1315
|
+
cancel: { en: "Cancel" },
|
|
1316
|
+
save: { en: "Save" },
|
|
1317
|
+
delete: { en: "Delete" },
|
|
1318
|
+
edit: { en: "Edit" },
|
|
1319
|
+
close: { en: "Close" },
|
|
1320
|
+
confirm: { en: "Confirm" },
|
|
1321
|
+
back: { en: "Back" },
|
|
1322
|
+
next: { en: "Next" },
|
|
1323
|
+
previous: { en: "Previous" },
|
|
1324
|
+
yes: { en: "Yes" },
|
|
1325
|
+
no: { en: "No" },
|
|
1326
|
+
ok: { en: "OK" },
|
|
1327
|
+
loading: { en: "Loading…" },
|
|
1328
|
+
search: { en: "Search" },
|
|
1329
|
+
no_results: { en: "No results" },
|
|
1330
|
+
required: { en: "Required" },
|
|
1331
|
+
retry: { en: "Retry" },
|
|
1332
|
+
add: { en: "Add" },
|
|
1333
|
+
remove: { en: "Remove" },
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
// True when `key` is one of the predefined shared keys — the discriminator
|
|
1337
|
+
// `useI18n` uses to decide whether to try the `shared.<key>` namespace. A key
|
|
1338
|
+
// the author invents is NOT shared (it stays per-widget) so authors can't
|
|
1339
|
+
// accidentally collide with another widget by reusing a bare name.
|
|
1340
|
+
function isSharedTranslationKey(key) {
|
|
1341
|
+
return (
|
|
1342
|
+
typeof key === "string" &&
|
|
1343
|
+
Object.prototype.hasOwnProperty.call(SHARED_TRANSLATION_KEYS, key)
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1274
1347
|
const CONTRACT = deepFreeze({
|
|
1275
1348
|
// REQ-WSDK-PLATFORM bump:
|
|
1276
1349
|
// - `vettedImports` is a new field (rich allowlist with platforms +
|
|
@@ -1459,7 +1532,36 @@ const CONTRACT = deepFreeze({
|
|
|
1459
1532
|
// `users.write:*`. New linter rule `scope-required-for-user-delete`
|
|
1460
1533
|
// flags a widget that calls `.remove()` without declaring the scope.
|
|
1461
1534
|
// No hook signature changed; `useUsers().remove` is unchanged.
|
|
1462
|
-
|
|
1535
|
+
//
|
|
1536
|
+
// 1.27.0: additive — the vetted import allowlist gains `react/jsx-runtime`
|
|
1537
|
+
// and `react/jsx-dev-runtime`. These are subpaths of the already-vetted
|
|
1538
|
+
// `react`, injected automatically by the JSX transform when a bundle is
|
|
1539
|
+
// compiled with the automatic JSX runtime (Vite/esbuild's default). The
|
|
1540
|
+
// runtime already treats them as host-provided (the web loader shims both
|
|
1541
|
+
// in widgetLoader.js, the AI-agent sandbox stubs them, and the Developer
|
|
1542
|
+
// guide documents them as externalized), but the linter's vetted list did
|
|
1543
|
+
// not list them — so a first-party/marketplace bundle built with the
|
|
1544
|
+
// automatic runtime failed publish static analysis with `import-not-vetted`
|
|
1545
|
+
// on `react/jsx-runtime`. Adding them converges the linter onto the same
|
|
1546
|
+
// contract every other surface already honoured (CLAUDE.md §3). No
|
|
1547
|
+
// existing entry changed shape — minor bump on the pre-1.0 channel.
|
|
1548
|
+
//
|
|
1549
|
+
// 1.28.0: additive (REQ-L10N-SHARED) — predefined SHARED translation keys.
|
|
1550
|
+
// A new tenant-wide namespace `shared.<key>` carries the standard strings
|
|
1551
|
+
// the default widgets repeat ("Submit", "Cancel", "Loading…", …) so an
|
|
1552
|
+
// identical string is translated ONCE and every widget that calls
|
|
1553
|
+
// `t("<sharedKey>")` inherits it. New contract field
|
|
1554
|
+
// `sharedTranslationKeys` (the predefined map, the single source the
|
|
1555
|
+
// backend seeder reads) + new exported helpers `sharedTranslationPrefix()`
|
|
1556
|
+
// / `sharedTranslationKey(key)` / `isSharedTranslationKey(key)`.
|
|
1557
|
+
// `useI18n().t(key)` now resolves the per-widget key first, THEN the
|
|
1558
|
+
// shared key (when `key` is one of the predefined shared keys), then the
|
|
1559
|
+
// raw key — so a default widget picks up the shared translation with no
|
|
1560
|
+
// manual key entry, and a `widget.<id>.<key>` override still wins per
|
|
1561
|
+
// instance. No existing hook, primitive, manifest field, or token changed
|
|
1562
|
+
// shape — minor bump on the pre-1.0 channel.
|
|
1563
|
+
version: "1.28.0",
|
|
1564
|
+
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
1463
1565
|
hooks: HOOKS,
|
|
1464
1566
|
primitives: PRIMITIVES,
|
|
1465
1567
|
manifestSchema: MANIFEST_SCHEMA,
|
|
@@ -1497,4 +1599,8 @@ module.exports = {
|
|
|
1497
1599
|
requiredContextKeys,
|
|
1498
1600
|
widgetTranslationPrefix,
|
|
1499
1601
|
widgetTranslationKey,
|
|
1602
|
+
sharedTranslationPrefix,
|
|
1603
|
+
sharedTranslationKey,
|
|
1604
|
+
isSharedTranslationKey,
|
|
1605
|
+
SHARED_TRANSLATION_KEYS,
|
|
1500
1606
|
};
|
package/dist/contract.js
CHANGED
|
@@ -1057,6 +1057,20 @@ const VETTED_IMPORTS = [
|
|
|
1057
1057
|
category: "core",
|
|
1058
1058
|
description: "React. Hooks, JSX, lifecycle. Unchanged.",
|
|
1059
1059
|
},
|
|
1060
|
+
{
|
|
1061
|
+
specifier: "react/jsx-runtime",
|
|
1062
|
+
platforms: ["web", "native"],
|
|
1063
|
+
category: "core",
|
|
1064
|
+
description:
|
|
1065
|
+
"React's automatic JSX runtime (jsx/jsxs/Fragment). Not hand-written — the JSX transform injects this import when a bundle is compiled with the automatic runtime. Host-provided on both platforms (the web loader shims it; Metro resolves the real subpath of react), so it is externalized exactly like react.",
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
specifier: "react/jsx-dev-runtime",
|
|
1069
|
+
platforms: ["web", "native"],
|
|
1070
|
+
category: "core",
|
|
1071
|
+
description:
|
|
1072
|
+
"React's automatic JSX dev runtime (jsxDEV/Fragment). The development-mode counterpart to react/jsx-runtime, injected by the JSX transform in dev builds. Host-provided on both platforms, externalized like react.",
|
|
1073
|
+
},
|
|
1060
1074
|
{
|
|
1061
1075
|
specifier: "@colixsystems/widget-sdk",
|
|
1062
1076
|
platforms: ["web", "native"],
|
|
@@ -1271,6 +1285,65 @@ function widgetTranslationKey(id, key) {
|
|
|
1271
1285
|
return `widget.${id}.${key}`;
|
|
1272
1286
|
}
|
|
1273
1287
|
|
|
1288
|
+
// REQ-L10N-SHARED — predefined SHARED translation keys for the standard
|
|
1289
|
+
// strings the default widgets repeat ("Submit", "Cancel", "Loading…", …).
|
|
1290
|
+
// They live in ONE tenant-wide namespace (`shared.<key>`) rather than the
|
|
1291
|
+
// per-widget namespace, so identical strings translate ONCE and every widget
|
|
1292
|
+
// that calls `t("<sharedKey>")` resolves the same dictionary value. An author
|
|
1293
|
+
// still overrides any instance by setting `widget.<id>.<key>` in the
|
|
1294
|
+
// Translations admin — `useI18n` resolves the per-widget key FIRST, then the
|
|
1295
|
+
// shared key, then the raw key, so the override is per-instance.
|
|
1296
|
+
//
|
|
1297
|
+
// This is the analogue of `widget.<id>.` for cross-widget reuse: one prefix,
|
|
1298
|
+
// one definition (the hook reads + the backend seeder writes both call these),
|
|
1299
|
+
// so the wire key and the lookup key can never drift.
|
|
1300
|
+
const SHARED_TRANSLATION_PREFIX = "shared.";
|
|
1301
|
+
function sharedTranslationPrefix() {
|
|
1302
|
+
return SHARED_TRANSLATION_PREFIX;
|
|
1303
|
+
}
|
|
1304
|
+
function sharedTranslationKey(key) {
|
|
1305
|
+
return `${SHARED_TRANSLATION_PREFIX}${key}`;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// The predefined set. Map of bare key -> { en } (English default only; the
|
|
1309
|
+
// tenant adds target-language values once in the Translations admin and every
|
|
1310
|
+
// widget inherits them). Adding/removing a key is a CONTRACT change (it grows
|
|
1311
|
+
// the dictionary every tenant gets), so the list lives with the contract and
|
|
1312
|
+
// the seeder reads it from here — never a second copy.
|
|
1313
|
+
const SHARED_TRANSLATION_KEYS = Object.freeze({
|
|
1314
|
+
submit: { en: "Submit" },
|
|
1315
|
+
cancel: { en: "Cancel" },
|
|
1316
|
+
save: { en: "Save" },
|
|
1317
|
+
delete: { en: "Delete" },
|
|
1318
|
+
edit: { en: "Edit" },
|
|
1319
|
+
close: { en: "Close" },
|
|
1320
|
+
confirm: { en: "Confirm" },
|
|
1321
|
+
back: { en: "Back" },
|
|
1322
|
+
next: { en: "Next" },
|
|
1323
|
+
previous: { en: "Previous" },
|
|
1324
|
+
yes: { en: "Yes" },
|
|
1325
|
+
no: { en: "No" },
|
|
1326
|
+
ok: { en: "OK" },
|
|
1327
|
+
loading: { en: "Loading…" },
|
|
1328
|
+
search: { en: "Search" },
|
|
1329
|
+
no_results: { en: "No results" },
|
|
1330
|
+
required: { en: "Required" },
|
|
1331
|
+
retry: { en: "Retry" },
|
|
1332
|
+
add: { en: "Add" },
|
|
1333
|
+
remove: { en: "Remove" },
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
// True when `key` is one of the predefined shared keys — the discriminator
|
|
1337
|
+
// `useI18n` uses to decide whether to try the `shared.<key>` namespace. A key
|
|
1338
|
+
// the author invents is NOT shared (it stays per-widget) so authors can't
|
|
1339
|
+
// accidentally collide with another widget by reusing a bare name.
|
|
1340
|
+
function isSharedTranslationKey(key) {
|
|
1341
|
+
return (
|
|
1342
|
+
typeof key === "string" &&
|
|
1343
|
+
Object.prototype.hasOwnProperty.call(SHARED_TRANSLATION_KEYS, key)
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1274
1347
|
const CONTRACT = deepFreeze({
|
|
1275
1348
|
// REQ-WSDK-PLATFORM bump:
|
|
1276
1349
|
// - `vettedImports` is a new field (rich allowlist with platforms +
|
|
@@ -1459,7 +1532,36 @@ const CONTRACT = deepFreeze({
|
|
|
1459
1532
|
// `users.write:*`. New linter rule `scope-required-for-user-delete`
|
|
1460
1533
|
// flags a widget that calls `.remove()` without declaring the scope.
|
|
1461
1534
|
// No hook signature changed; `useUsers().remove` is unchanged.
|
|
1462
|
-
|
|
1535
|
+
//
|
|
1536
|
+
// 1.27.0: additive — the vetted import allowlist gains `react/jsx-runtime`
|
|
1537
|
+
// and `react/jsx-dev-runtime`. These are subpaths of the already-vetted
|
|
1538
|
+
// `react`, injected automatically by the JSX transform when a bundle is
|
|
1539
|
+
// compiled with the automatic JSX runtime (Vite/esbuild's default). The
|
|
1540
|
+
// runtime already treats them as host-provided (the web loader shims both
|
|
1541
|
+
// in widgetLoader.js, the AI-agent sandbox stubs them, and the Developer
|
|
1542
|
+
// guide documents them as externalized), but the linter's vetted list did
|
|
1543
|
+
// not list them — so a first-party/marketplace bundle built with the
|
|
1544
|
+
// automatic runtime failed publish static analysis with `import-not-vetted`
|
|
1545
|
+
// on `react/jsx-runtime`. Adding them converges the linter onto the same
|
|
1546
|
+
// contract every other surface already honoured (CLAUDE.md §3). No
|
|
1547
|
+
// existing entry changed shape — minor bump on the pre-1.0 channel.
|
|
1548
|
+
//
|
|
1549
|
+
// 1.28.0: additive (REQ-L10N-SHARED) — predefined SHARED translation keys.
|
|
1550
|
+
// A new tenant-wide namespace `shared.<key>` carries the standard strings
|
|
1551
|
+
// the default widgets repeat ("Submit", "Cancel", "Loading…", …) so an
|
|
1552
|
+
// identical string is translated ONCE and every widget that calls
|
|
1553
|
+
// `t("<sharedKey>")` inherits it. New contract field
|
|
1554
|
+
// `sharedTranslationKeys` (the predefined map, the single source the
|
|
1555
|
+
// backend seeder reads) + new exported helpers `sharedTranslationPrefix()`
|
|
1556
|
+
// / `sharedTranslationKey(key)` / `isSharedTranslationKey(key)`.
|
|
1557
|
+
// `useI18n().t(key)` now resolves the per-widget key first, THEN the
|
|
1558
|
+
// shared key (when `key` is one of the predefined shared keys), then the
|
|
1559
|
+
// raw key — so a default widget picks up the shared translation with no
|
|
1560
|
+
// manual key entry, and a `widget.<id>.<key>` override still wins per
|
|
1561
|
+
// instance. No existing hook, primitive, manifest field, or token changed
|
|
1562
|
+
// shape — minor bump on the pre-1.0 channel.
|
|
1563
|
+
version: "1.28.0",
|
|
1564
|
+
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
1463
1565
|
hooks: HOOKS,
|
|
1464
1566
|
primitives: PRIMITIVES,
|
|
1465
1567
|
manifestSchema: MANIFEST_SCHEMA,
|
|
@@ -1497,4 +1599,8 @@ export {
|
|
|
1497
1599
|
requiredContextKeys,
|
|
1498
1600
|
widgetTranslationPrefix,
|
|
1499
1601
|
widgetTranslationKey,
|
|
1602
|
+
sharedTranslationPrefix,
|
|
1603
|
+
sharedTranslationKey,
|
|
1604
|
+
isSharedTranslationKey,
|
|
1605
|
+
SHARED_TRANSLATION_KEYS,
|
|
1500
1606
|
};
|
package/dist/hooks.js
CHANGED
|
@@ -26,10 +26,18 @@ import React, {
|
|
|
26
26
|
useRef,
|
|
27
27
|
useState,
|
|
28
28
|
} from "react";
|
|
29
|
-
// REQ-L10N-WIDGET: the
|
|
30
|
-
// (contract.js). useI18n (lookup) and the backend seeder (write) both
|
|
31
|
-
// so the namespaced
|
|
32
|
-
|
|
29
|
+
// REQ-L10N-WIDGET / REQ-L10N-SHARED: the translation key formats live in ONE
|
|
30
|
+
// place (contract.js). useI18n (lookup) and the backend seeder (write) both
|
|
31
|
+
// call them so the namespaced keys can never drift between the two sides.
|
|
32
|
+
// `widgetTranslationKey` builds the per-widget key (`widget.<id>.<key>`);
|
|
33
|
+
// `sharedTranslationKey` builds the tenant-wide predefined key
|
|
34
|
+
// (`shared.<key>`); `isSharedTranslationKey` tells the hook whether a bare key
|
|
35
|
+
// is one of the predefined shared keys and so should try the shared namespace.
|
|
36
|
+
import {
|
|
37
|
+
widgetTranslationKey,
|
|
38
|
+
sharedTranslationKey,
|
|
39
|
+
isSharedTranslationKey,
|
|
40
|
+
} from "./contract.js";
|
|
33
41
|
|
|
34
42
|
/** @internal — host-injected context value of shape WidgetContext (see index.d.ts). */
|
|
35
43
|
const HostWidgetContext = createContext(null);
|
|
@@ -240,7 +248,27 @@ export function useI18n() {
|
|
|
240
248
|
return scoped;
|
|
241
249
|
}
|
|
242
250
|
}
|
|
243
|
-
// 2)
|
|
251
|
+
// 2) REQ-L10N-SHARED: for a predefined SHARED key, try the tenant-wide
|
|
252
|
+
// `shared.<key>` namespace next. This is what lets identical
|
|
253
|
+
// default-widget strings ("Submit", "Cancel", …) translate ONCE: a
|
|
254
|
+
// widget that has no per-instance `widget.<id>.<key>` override
|
|
255
|
+
// inherits the shared value. The per-widget step above runs FIRST,
|
|
256
|
+
// so an author override of a single instance still wins for that
|
|
257
|
+
// instance. Only the predefined keys are tried here — an
|
|
258
|
+
// author-invented bare key is never silently shared.
|
|
259
|
+
if (isSharedTranslationKey(key)) {
|
|
260
|
+
const shared = sharedTranslationKey(key);
|
|
261
|
+
const scoped = hostT(shared);
|
|
262
|
+
if (
|
|
263
|
+
typeof scoped === "string" &&
|
|
264
|
+
scoped.length > 0 &&
|
|
265
|
+
scoped !== shared &&
|
|
266
|
+
!scoped.startsWith("{{t:")
|
|
267
|
+
) {
|
|
268
|
+
return scoped;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// 3) Fall back to the raw key (shared app keys + widgets with no
|
|
244
272
|
// manifest translations — unchanged from pre-1.10 behaviour).
|
|
245
273
|
const out = hostT(key, fallback);
|
|
246
274
|
if (typeof out === "string" && out.length > 0 && out !== key) {
|
package/dist/linter.cjs
CHANGED
|
@@ -54,6 +54,149 @@ const CONTRACT_RULES = CONTRACT.bannedApis.map((b) =>
|
|
|
54
54
|
_ruleForIdentifier(b.identifier, b.reason),
|
|
55
55
|
);
|
|
56
56
|
|
|
57
|
+
// Replace the *content* of comments and string / template literals with
|
|
58
|
+
// spaces so the banned-identifier scan only ever sees executable code. A
|
|
59
|
+
// banned host-escape identifier (`window`, `document`, `eval`, `process`, …)
|
|
60
|
+
// is only dangerous as a real identifier reference — never as prose in a
|
|
61
|
+
// `//` comment or as character data inside a string — so matching the bare
|
|
62
|
+
// word there is a false positive that blocks an otherwise-clean widget (a
|
|
63
|
+
// comment that reads "the hour window the grid renders" must not trip
|
|
64
|
+
// `no-window`).
|
|
65
|
+
//
|
|
66
|
+
// Newlines are preserved verbatim so reported line numbers still line up
|
|
67
|
+
// with the original source. Template-literal `${ … }` expression holes are
|
|
68
|
+
// left intact: real code lives there and must still be scanned (`${window}`
|
|
69
|
+
// is a genuine escape). Backslash escapes inside strings/templates are
|
|
70
|
+
// consumed so an escaped quote (`"\""`) doesn't end the literal early.
|
|
71
|
+
function _stripNonCode(source) {
|
|
72
|
+
let out = "";
|
|
73
|
+
const n = source.length;
|
|
74
|
+
let mode = "code"; // code | line | block | sq | dq | tmpl
|
|
75
|
+
// Brace depth, plus a stack of the depths at which an enclosing template
|
|
76
|
+
// literal resumes — lets a `${ … }` hole (which may itself contain `{}`,
|
|
77
|
+
// strings, or nested templates) be told apart from the literal text.
|
|
78
|
+
let braceDepth = 0;
|
|
79
|
+
const tmplStack = [];
|
|
80
|
+
const keep = (ch) => {
|
|
81
|
+
out += ch;
|
|
82
|
+
};
|
|
83
|
+
const blank = (ch) => {
|
|
84
|
+
out += ch === "\n" || ch === "\r" ? ch : " ";
|
|
85
|
+
};
|
|
86
|
+
let i = 0;
|
|
87
|
+
while (i < n) {
|
|
88
|
+
const ch = source[i];
|
|
89
|
+
const nx = source[i + 1];
|
|
90
|
+
if (mode === "code") {
|
|
91
|
+
if (ch === "/" && nx === "/") {
|
|
92
|
+
mode = "line";
|
|
93
|
+
blank(ch);
|
|
94
|
+
blank(nx);
|
|
95
|
+
i += 2;
|
|
96
|
+
} else if (ch === "/" && nx === "*") {
|
|
97
|
+
mode = "block";
|
|
98
|
+
blank(ch);
|
|
99
|
+
blank(nx);
|
|
100
|
+
i += 2;
|
|
101
|
+
} else if (ch === "'") {
|
|
102
|
+
mode = "sq";
|
|
103
|
+
blank(ch);
|
|
104
|
+
i += 1;
|
|
105
|
+
} else if (ch === '"') {
|
|
106
|
+
mode = "dq";
|
|
107
|
+
blank(ch);
|
|
108
|
+
i += 1;
|
|
109
|
+
} else if (ch === "`") {
|
|
110
|
+
mode = "tmpl";
|
|
111
|
+
blank(ch);
|
|
112
|
+
i += 1;
|
|
113
|
+
} else if (ch === "{") {
|
|
114
|
+
braceDepth += 1;
|
|
115
|
+
keep(ch);
|
|
116
|
+
i += 1;
|
|
117
|
+
} else if (ch === "}") {
|
|
118
|
+
braceDepth -= 1;
|
|
119
|
+
if (
|
|
120
|
+
tmplStack.length > 0 &&
|
|
121
|
+
tmplStack[tmplStack.length - 1] === braceDepth
|
|
122
|
+
) {
|
|
123
|
+
tmplStack.pop();
|
|
124
|
+
mode = "tmpl";
|
|
125
|
+
blank(ch);
|
|
126
|
+
} else {
|
|
127
|
+
keep(ch);
|
|
128
|
+
}
|
|
129
|
+
i += 1;
|
|
130
|
+
} else {
|
|
131
|
+
keep(ch);
|
|
132
|
+
i += 1;
|
|
133
|
+
}
|
|
134
|
+
} else if (mode === "line") {
|
|
135
|
+
if (ch === "\n") {
|
|
136
|
+
mode = "code";
|
|
137
|
+
keep(ch);
|
|
138
|
+
} else {
|
|
139
|
+
blank(ch);
|
|
140
|
+
}
|
|
141
|
+
i += 1;
|
|
142
|
+
} else if (mode === "block") {
|
|
143
|
+
if (ch === "*" && nx === "/") {
|
|
144
|
+
mode = "code";
|
|
145
|
+
blank(ch);
|
|
146
|
+
blank(nx);
|
|
147
|
+
i += 2;
|
|
148
|
+
} else {
|
|
149
|
+
blank(ch);
|
|
150
|
+
i += 1;
|
|
151
|
+
}
|
|
152
|
+
} else if (mode === "sq" || mode === "dq") {
|
|
153
|
+
const quote = mode === "sq" ? "'" : '"';
|
|
154
|
+
if (ch === "\\") {
|
|
155
|
+
blank(ch);
|
|
156
|
+
if (i + 1 < n) blank(nx);
|
|
157
|
+
i += 2;
|
|
158
|
+
} else if (ch === quote) {
|
|
159
|
+
mode = "code";
|
|
160
|
+
blank(ch);
|
|
161
|
+
i += 1;
|
|
162
|
+
} else if (ch === "\n") {
|
|
163
|
+
// A bare newline terminates an unterminated string in JS; bail back
|
|
164
|
+
// to code so malformed input can't blank the rest of the file.
|
|
165
|
+
mode = "code";
|
|
166
|
+
keep(ch);
|
|
167
|
+
i += 1;
|
|
168
|
+
} else {
|
|
169
|
+
blank(ch);
|
|
170
|
+
i += 1;
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
// mode === "tmpl"
|
|
174
|
+
if (ch === "\\") {
|
|
175
|
+
blank(ch);
|
|
176
|
+
if (i + 1 < n) blank(nx);
|
|
177
|
+
i += 2;
|
|
178
|
+
} else if (ch === "`") {
|
|
179
|
+
mode = "code";
|
|
180
|
+
blank(ch);
|
|
181
|
+
i += 1;
|
|
182
|
+
} else if (ch === "$" && nx === "{") {
|
|
183
|
+
// Enter an expression hole. Remember the brace depth the template
|
|
184
|
+
// resumes at, then count the `{` so its matching `}` is recognised.
|
|
185
|
+
tmplStack.push(braceDepth);
|
|
186
|
+
braceDepth += 1;
|
|
187
|
+
mode = "code";
|
|
188
|
+
keep(ch);
|
|
189
|
+
keep(nx);
|
|
190
|
+
i += 2;
|
|
191
|
+
} else {
|
|
192
|
+
blank(ch);
|
|
193
|
+
i += 1;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
|
|
57
200
|
// REQ-WSDK-PLATFORM: `no-axios-import` is GONE. axios is on the vetted
|
|
58
201
|
// import list now (`CONTRACT.vettedImports`). See linter.js for the
|
|
59
202
|
// source-of-truth comment.
|
|
@@ -387,16 +530,22 @@ function lintSource(source, options) {
|
|
|
387
530
|
}
|
|
388
531
|
const findings = [];
|
|
389
532
|
const lines = source.split(/\r?\n/);
|
|
533
|
+
// Scan code with comments + string/template text blanked out so a banned
|
|
534
|
+
// identifier only fires on an actual code reference, not on the same word
|
|
535
|
+
// appearing in prose or string data. `codeLines` lines up 1:1 with `lines`
|
|
536
|
+
// (masking preserves newlines), so the reported snippet still comes from
|
|
537
|
+
// the original source.
|
|
538
|
+
const codeLines = _stripNonCode(source).split(/\r?\n/);
|
|
390
539
|
for (let i = 0; i < lines.length; i++) {
|
|
391
|
-
const
|
|
540
|
+
const codeLine = codeLines[i];
|
|
392
541
|
for (const rule of RULES) {
|
|
393
|
-
if (rule.pattern.test(
|
|
542
|
+
if (rule.pattern.test(codeLine)) {
|
|
394
543
|
findings.push({
|
|
395
544
|
rule: rule.id,
|
|
396
545
|
severity: "error",
|
|
397
546
|
label: rule.label,
|
|
398
547
|
line: i + 1,
|
|
399
|
-
snippet:
|
|
548
|
+
snippet: lines[i].trim().slice(0, 200),
|
|
400
549
|
});
|
|
401
550
|
}
|
|
402
551
|
}
|
package/dist/linter.js
CHANGED
|
@@ -55,6 +55,149 @@ const CONTRACT_RULES = CONTRACT.bannedApis.map((b) =>
|
|
|
55
55
|
_ruleForIdentifier(b.identifier, b.reason),
|
|
56
56
|
);
|
|
57
57
|
|
|
58
|
+
// Replace the *content* of comments and string / template literals with
|
|
59
|
+
// spaces so the banned-identifier scan only ever sees executable code. A
|
|
60
|
+
// banned host-escape identifier (`window`, `document`, `eval`, `process`, …)
|
|
61
|
+
// is only dangerous as a real identifier reference — never as prose in a
|
|
62
|
+
// `//` comment or as character data inside a string — so matching the bare
|
|
63
|
+
// word there is a false positive that blocks an otherwise-clean widget (a
|
|
64
|
+
// comment that reads "the hour window the grid renders" must not trip
|
|
65
|
+
// `no-window`).
|
|
66
|
+
//
|
|
67
|
+
// Newlines are preserved verbatim so reported line numbers still line up
|
|
68
|
+
// with the original source. Template-literal `${ … }` expression holes are
|
|
69
|
+
// left intact: real code lives there and must still be scanned (`${window}`
|
|
70
|
+
// is a genuine escape). Backslash escapes inside strings/templates are
|
|
71
|
+
// consumed so an escaped quote (`"\""`) doesn't end the literal early.
|
|
72
|
+
function _stripNonCode(source) {
|
|
73
|
+
let out = "";
|
|
74
|
+
const n = source.length;
|
|
75
|
+
let mode = "code"; // code | line | block | sq | dq | tmpl
|
|
76
|
+
// Brace depth, plus a stack of the depths at which an enclosing template
|
|
77
|
+
// literal resumes — lets a `${ … }` hole (which may itself contain `{}`,
|
|
78
|
+
// strings, or nested templates) be told apart from the literal text.
|
|
79
|
+
let braceDepth = 0;
|
|
80
|
+
const tmplStack = [];
|
|
81
|
+
const keep = (ch) => {
|
|
82
|
+
out += ch;
|
|
83
|
+
};
|
|
84
|
+
const blank = (ch) => {
|
|
85
|
+
out += ch === "\n" || ch === "\r" ? ch : " ";
|
|
86
|
+
};
|
|
87
|
+
let i = 0;
|
|
88
|
+
while (i < n) {
|
|
89
|
+
const ch = source[i];
|
|
90
|
+
const nx = source[i + 1];
|
|
91
|
+
if (mode === "code") {
|
|
92
|
+
if (ch === "/" && nx === "/") {
|
|
93
|
+
mode = "line";
|
|
94
|
+
blank(ch);
|
|
95
|
+
blank(nx);
|
|
96
|
+
i += 2;
|
|
97
|
+
} else if (ch === "/" && nx === "*") {
|
|
98
|
+
mode = "block";
|
|
99
|
+
blank(ch);
|
|
100
|
+
blank(nx);
|
|
101
|
+
i += 2;
|
|
102
|
+
} else if (ch === "'") {
|
|
103
|
+
mode = "sq";
|
|
104
|
+
blank(ch);
|
|
105
|
+
i += 1;
|
|
106
|
+
} else if (ch === '"') {
|
|
107
|
+
mode = "dq";
|
|
108
|
+
blank(ch);
|
|
109
|
+
i += 1;
|
|
110
|
+
} else if (ch === "`") {
|
|
111
|
+
mode = "tmpl";
|
|
112
|
+
blank(ch);
|
|
113
|
+
i += 1;
|
|
114
|
+
} else if (ch === "{") {
|
|
115
|
+
braceDepth += 1;
|
|
116
|
+
keep(ch);
|
|
117
|
+
i += 1;
|
|
118
|
+
} else if (ch === "}") {
|
|
119
|
+
braceDepth -= 1;
|
|
120
|
+
if (
|
|
121
|
+
tmplStack.length > 0 &&
|
|
122
|
+
tmplStack[tmplStack.length - 1] === braceDepth
|
|
123
|
+
) {
|
|
124
|
+
tmplStack.pop();
|
|
125
|
+
mode = "tmpl";
|
|
126
|
+
blank(ch);
|
|
127
|
+
} else {
|
|
128
|
+
keep(ch);
|
|
129
|
+
}
|
|
130
|
+
i += 1;
|
|
131
|
+
} else {
|
|
132
|
+
keep(ch);
|
|
133
|
+
i += 1;
|
|
134
|
+
}
|
|
135
|
+
} else if (mode === "line") {
|
|
136
|
+
if (ch === "\n") {
|
|
137
|
+
mode = "code";
|
|
138
|
+
keep(ch);
|
|
139
|
+
} else {
|
|
140
|
+
blank(ch);
|
|
141
|
+
}
|
|
142
|
+
i += 1;
|
|
143
|
+
} else if (mode === "block") {
|
|
144
|
+
if (ch === "*" && nx === "/") {
|
|
145
|
+
mode = "code";
|
|
146
|
+
blank(ch);
|
|
147
|
+
blank(nx);
|
|
148
|
+
i += 2;
|
|
149
|
+
} else {
|
|
150
|
+
blank(ch);
|
|
151
|
+
i += 1;
|
|
152
|
+
}
|
|
153
|
+
} else if (mode === "sq" || mode === "dq") {
|
|
154
|
+
const quote = mode === "sq" ? "'" : '"';
|
|
155
|
+
if (ch === "\\") {
|
|
156
|
+
blank(ch);
|
|
157
|
+
if (i + 1 < n) blank(nx);
|
|
158
|
+
i += 2;
|
|
159
|
+
} else if (ch === quote) {
|
|
160
|
+
mode = "code";
|
|
161
|
+
blank(ch);
|
|
162
|
+
i += 1;
|
|
163
|
+
} else if (ch === "\n") {
|
|
164
|
+
// A bare newline terminates an unterminated string in JS; bail back
|
|
165
|
+
// to code so malformed input can't blank the rest of the file.
|
|
166
|
+
mode = "code";
|
|
167
|
+
keep(ch);
|
|
168
|
+
i += 1;
|
|
169
|
+
} else {
|
|
170
|
+
blank(ch);
|
|
171
|
+
i += 1;
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
// mode === "tmpl"
|
|
175
|
+
if (ch === "\\") {
|
|
176
|
+
blank(ch);
|
|
177
|
+
if (i + 1 < n) blank(nx);
|
|
178
|
+
i += 2;
|
|
179
|
+
} else if (ch === "`") {
|
|
180
|
+
mode = "code";
|
|
181
|
+
blank(ch);
|
|
182
|
+
i += 1;
|
|
183
|
+
} else if (ch === "$" && nx === "{") {
|
|
184
|
+
// Enter an expression hole. Remember the brace depth the template
|
|
185
|
+
// resumes at, then count the `{` so its matching `}` is recognised.
|
|
186
|
+
tmplStack.push(braceDepth);
|
|
187
|
+
braceDepth += 1;
|
|
188
|
+
mode = "code";
|
|
189
|
+
keep(ch);
|
|
190
|
+
keep(nx);
|
|
191
|
+
i += 2;
|
|
192
|
+
} else {
|
|
193
|
+
blank(ch);
|
|
194
|
+
i += 1;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
|
|
58
201
|
// Extra rules that don't map 1:1 to a banned identifier in the contract:
|
|
59
202
|
// host-internal imports that widgets must never touch.
|
|
60
203
|
//
|
|
@@ -124,9 +267,7 @@ function _classifySpecifier(spec) {
|
|
|
124
267
|
|
|
125
268
|
function _importRules(source, manifest) {
|
|
126
269
|
const findings = [];
|
|
127
|
-
const allowed = new Map(
|
|
128
|
-
CONTRACT.vettedImports.map((v) => [v.specifier, v]),
|
|
129
|
-
);
|
|
270
|
+
const allowed = new Map(CONTRACT.vettedImports.map((v) => [v.specifier, v]));
|
|
130
271
|
// Track declared `supportedPlatforms` so a widget that claims "web only"
|
|
131
272
|
// doesn't import a native-only package (and vice versa) without the
|
|
132
273
|
// marketplace listing being honest about which platforms ship.
|
|
@@ -258,7 +399,12 @@ const USER_MUTATION_METHODS = ["invite", "deactivate", "reactivate"];
|
|
|
258
399
|
// useUsers().remove() must declare `users.delete:*` so the static contract
|
|
259
400
|
// matches the backend gate on DELETE /api/v1/app/users/:userId.
|
|
260
401
|
const USER_DELETE_METHODS = ["remove"];
|
|
261
|
-
const GROUP_MUTATION_METHODS = [
|
|
402
|
+
const GROUP_MUTATION_METHODS = [
|
|
403
|
+
"create",
|
|
404
|
+
"remove",
|
|
405
|
+
"addMember",
|
|
406
|
+
"removeMember",
|
|
407
|
+
];
|
|
262
408
|
const SKIP_COMMENT = /\/\/[^\n]*@appstudio-skip-scope-check/;
|
|
263
409
|
|
|
264
410
|
function _scopeRules(source, manifest) {
|
|
@@ -277,8 +423,7 @@ function _scopeRules(source, manifest) {
|
|
|
277
423
|
if (!reads) {
|
|
278
424
|
findings.push({
|
|
279
425
|
rule: "scope-required-for-useUsers",
|
|
280
|
-
label:
|
|
281
|
-
"useUsers() requires `users.read:*` in manifest.requestedScopes",
|
|
426
|
+
label: "useUsers() requires `users.read:*` in manifest.requestedScopes",
|
|
282
427
|
line: 0,
|
|
283
428
|
snippet: "",
|
|
284
429
|
});
|
|
@@ -308,10 +453,7 @@ function _scopeRules(source, manifest) {
|
|
|
308
453
|
for (const m of USER_MUTATION_METHODS) {
|
|
309
454
|
const re = new RegExp(`\\.${m}\\s*\\(`);
|
|
310
455
|
if (re.test(line)) {
|
|
311
|
-
if (
|
|
312
|
-
!declared.has("users.write:*") &&
|
|
313
|
-
!declared.has("users.write")
|
|
314
|
-
) {
|
|
456
|
+
if (!declared.has("users.write:*") && !declared.has("users.write")) {
|
|
315
457
|
findings.push({
|
|
316
458
|
rule: "scope-required-for-user-mutation",
|
|
317
459
|
label: `useUsers().${m}() requires \`users.write:*\` in manifest.requestedScopes`,
|
|
@@ -400,7 +542,13 @@ function _manifestActionRules(manifest) {
|
|
|
400
542
|
const validTriggers = new Set(CONTRACT.actionTriggerTypes);
|
|
401
543
|
const maxBytes = CONTRACT.actionScriptMaxBytes;
|
|
402
544
|
const push = (label) =>
|
|
403
|
-
findings.push({
|
|
545
|
+
findings.push({
|
|
546
|
+
rule: "manifest-action",
|
|
547
|
+
severity: "error",
|
|
548
|
+
label,
|
|
549
|
+
line: 0,
|
|
550
|
+
snippet: "",
|
|
551
|
+
});
|
|
404
552
|
if (!Array.isArray(manifest.actions)) {
|
|
405
553
|
push("manifest.actions must be an array (omit it or use [] for none)");
|
|
406
554
|
return findings;
|
|
@@ -422,9 +570,16 @@ function _manifestActionRules(manifest) {
|
|
|
422
570
|
push("manifest.actions[].name must be a non-empty string");
|
|
423
571
|
}
|
|
424
572
|
if (!validTriggers.has(a.triggerType)) {
|
|
425
|
-
push(
|
|
426
|
-
|
|
427
|
-
|
|
573
|
+
push(
|
|
574
|
+
`manifest.actions[].triggerType must be one of ${[...validTriggers].join(", ")}`,
|
|
575
|
+
);
|
|
576
|
+
} else if (
|
|
577
|
+
a.triggerType === "schedule" &&
|
|
578
|
+
(typeof a.scheduleCron !== "string" || !a.scheduleCron)
|
|
579
|
+
) {
|
|
580
|
+
push(
|
|
581
|
+
"manifest.actions[].scheduleCron is required when triggerType is 'schedule'",
|
|
582
|
+
);
|
|
428
583
|
}
|
|
429
584
|
if (typeof a.scriptSource !== "string" || a.scriptSource.length === 0) {
|
|
430
585
|
push("manifest.actions[].scriptSource must be a non-empty string");
|
|
@@ -438,7 +593,9 @@ function _manifestActionRules(manifest) {
|
|
|
438
593
|
}
|
|
439
594
|
}
|
|
440
595
|
if (a.triggerTableId !== undefined || a.apiKeyId !== undefined) {
|
|
441
|
-
push(
|
|
596
|
+
push(
|
|
597
|
+
"manifest.actions[] must not include triggerTableId or apiKeyId — those are tenant-local and bound after install",
|
|
598
|
+
);
|
|
442
599
|
}
|
|
443
600
|
}
|
|
444
601
|
return findings;
|
|
@@ -461,16 +618,22 @@ export function lintSource(source, options) {
|
|
|
461
618
|
}
|
|
462
619
|
const findings = [];
|
|
463
620
|
const lines = source.split(/\r?\n/);
|
|
621
|
+
// Scan code with comments + string/template text blanked out so a banned
|
|
622
|
+
// identifier only fires on an actual code reference, not on the same word
|
|
623
|
+
// appearing in prose or string data. `codeLines` lines up 1:1 with `lines`
|
|
624
|
+
// (masking preserves newlines), so the reported snippet still comes from
|
|
625
|
+
// the original source.
|
|
626
|
+
const codeLines = _stripNonCode(source).split(/\r?\n/);
|
|
464
627
|
for (let i = 0; i < lines.length; i++) {
|
|
465
|
-
const
|
|
628
|
+
const codeLine = codeLines[i];
|
|
466
629
|
for (const rule of RULES) {
|
|
467
|
-
if (rule.pattern.test(
|
|
630
|
+
if (rule.pattern.test(codeLine)) {
|
|
468
631
|
findings.push({
|
|
469
632
|
rule: rule.id,
|
|
470
633
|
severity: "error",
|
|
471
634
|
label: rule.label,
|
|
472
635
|
line: i + 1,
|
|
473
|
-
snippet:
|
|
636
|
+
snippet: lines[i].trim().slice(0, 200),
|
|
474
637
|
});
|
|
475
638
|
}
|
|
476
639
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
],
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "node scripts/build.js",
|
|
38
|
-
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-subscription.test.js src/__tests__/linter-users-scope.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/devserver.test.js"
|
|
38
|
+
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-subscription.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/devserver.test.js"
|
|
39
39
|
},
|
|
40
40
|
"engines": {
|
|
41
41
|
"node": ">=18"
|