@colixsystems/widget-sdk 0.117.0 → 0.119.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 +56 -2
- package/dist/contract.cjs +59 -6
- package/dist/contract.js +59 -6
- package/dist/devserver.js +92 -40
- package/dist/flatten-entry.js +817 -0
- package/dist/hooks.js +148 -9
- package/dist/host.d.ts +8 -4
- package/dist/index.d.ts +55 -0
- package/dist/linter.js +3 -146
- package/dist/source-mask.js +162 -0
- package/dist/theme-components.cjs +70 -10
- package/dist/theme-components.js +70 -10
- package/package.json +7 -3
package/dist/hooks.js
CHANGED
|
@@ -987,9 +987,55 @@ function toGeolocationError(err) {
|
|
|
987
987
|
return new GeolocationError(code, message, { cause: err });
|
|
988
988
|
}
|
|
989
989
|
|
|
990
|
+
/** Normalise a host position onto the hook's three numeric slots. */
|
|
991
|
+
function normalizeGeolocationPosition(pos) {
|
|
992
|
+
return {
|
|
993
|
+
latitude: pos && typeof pos.latitude === "number" ? pos.latitude : null,
|
|
994
|
+
longitude: pos && typeof pos.longitude === "number" ? pos.longitude : null,
|
|
995
|
+
accuracy: pos && typeof pos.accuracy === "number" ? pos.accuracy : null,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Ask the host whether a background watch is running. The watch outlives the
|
|
1001
|
+
* widget's mount, so the host — not the hook — owns this truth.
|
|
1002
|
+
*/
|
|
1003
|
+
function readBackgroundWatching(client) {
|
|
1004
|
+
if (!client || typeof client.isBackgroundWatching !== "function") return false;
|
|
1005
|
+
try {
|
|
1006
|
+
return Boolean(client.isBackgroundWatching());
|
|
1007
|
+
} catch {
|
|
1008
|
+
return false;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/** Whether this host offers the background watch. Never throws at render. */
|
|
1013
|
+
function readBackgroundSupported(client) {
|
|
1014
|
+
if (!client || typeof client.startBackgroundWatch !== "function") return false;
|
|
1015
|
+
if (typeof client.isBackgroundSupported !== "function") return true;
|
|
1016
|
+
try {
|
|
1017
|
+
return Boolean(client.isBackgroundSupported());
|
|
1018
|
+
} catch {
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/** Subscribe defensively; a host that throws simply yields no subscription. */
|
|
1024
|
+
function safeSubscribe(client, method, handler) {
|
|
1025
|
+
if (!client || typeof client[method] !== "function") return null;
|
|
1026
|
+
try {
|
|
1027
|
+
const unsubscribe = client[method](handler);
|
|
1028
|
+
return typeof unsubscribe === "function" ? unsubscribe : null;
|
|
1029
|
+
} catch {
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
990
1034
|
/**
|
|
991
1035
|
* Read the device's current position. Returns
|
|
992
|
-
* `{ latitude, longitude, accuracy, loading, error, getCurrentPosition
|
|
1036
|
+
* `{ latitude, longitude, accuracy, loading, error, getCurrentPosition,
|
|
1037
|
+
* backgroundSupported, backgroundWatching, startBackgroundWatch,
|
|
1038
|
+
* stopBackgroundWatch }`.
|
|
993
1039
|
*
|
|
994
1040
|
* Capture is IMPERATIVE — call `getCurrentPosition()` from a user gesture (a
|
|
995
1041
|
* tap on a button). Browsers and the mobile OS gate the permission prompt on a
|
|
@@ -1004,6 +1050,21 @@ function toGeolocationError(err) {
|
|
|
1004
1050
|
* Safe-by-default: on a host that does not inject `ctx.device.geolocation`,
|
|
1005
1051
|
* `getCurrentPosition()` rejects with `code: "UNSUPPORTED"` rather than
|
|
1006
1052
|
* throwing at render, so a widget can call the hook unconditionally.
|
|
1053
|
+
*
|
|
1054
|
+
* BACKGROUND WATCH (sc-6450) — `startBackgroundWatch(options?)` keeps positions
|
|
1055
|
+
* arriving while the app is BACKGROUNDED, which is what field-work apps
|
|
1056
|
+
* (delivery tracking, site visits, mileage logging) need. It is capability-
|
|
1057
|
+
* gated: check `backgroundSupported` before offering the control. The web
|
|
1058
|
+
* Player reports false — a browser tab cannot track in the background — and so
|
|
1059
|
+
* does an exported app whose workspace has not opted into background location
|
|
1060
|
+
* in Publishing Settings, because a build that never uses it must declare no
|
|
1061
|
+
* background mode and stay clear of the extra store review.
|
|
1062
|
+
*
|
|
1063
|
+
* The watch OUTLIVES the widget's mount by design, so it is released only by
|
|
1064
|
+
* `stopBackgroundWatch()`, never on unmount; `backgroundWatching` is seeded
|
|
1065
|
+
* from the host so a remounted widget reports a running watch honestly.
|
|
1066
|
+
* Delivered positions land in the SAME `latitude` / `longitude` / `accuracy`
|
|
1067
|
+
* slots as the foreground read.
|
|
1007
1068
|
*/
|
|
1008
1069
|
export function useGeolocation(options) {
|
|
1009
1070
|
const ctx = useWidgetContextOrThrow("useGeolocation");
|
|
@@ -1019,6 +1080,45 @@ export function useGeolocation(options) {
|
|
|
1019
1080
|
optionsRef.current = options;
|
|
1020
1081
|
const runRef = useRef(0);
|
|
1021
1082
|
|
|
1083
|
+
const client = ctx.device && ctx.device.geolocation;
|
|
1084
|
+
const backgroundSupported = readBackgroundSupported(client);
|
|
1085
|
+
const [backgroundWatching, setBackgroundWatching] = useState(() =>
|
|
1086
|
+
readBackgroundWatching(client),
|
|
1087
|
+
);
|
|
1088
|
+
|
|
1089
|
+
// Attach to the host's two background channels on MOUNT rather than inside
|
|
1090
|
+
// startBackgroundWatch: the watch survives unmount, so a remounted widget
|
|
1091
|
+
// must still receive its positions. Subscribing prompts for nothing and
|
|
1092
|
+
// starts no sensor — only startBackgroundWatch() does, from a user gesture.
|
|
1093
|
+
//
|
|
1094
|
+
// The host is the SINGLE source of `backgroundWatching`: the OS can end the
|
|
1095
|
+
// watch on its own (a permission downgrade, a killed service), the answer is
|
|
1096
|
+
// primed asynchronously after a cold relaunch, and a sibling widget may start
|
|
1097
|
+
// or stop it — none of which this hook could observe on its own. Keyed on the
|
|
1098
|
+
// client so a host that injects the slice late still gets wired up.
|
|
1099
|
+
useEffect(() => {
|
|
1100
|
+
if (!client) return undefined;
|
|
1101
|
+
setBackgroundWatching(readBackgroundWatching(client));
|
|
1102
|
+
const unsubscribers = [
|
|
1103
|
+
safeSubscribe(client, "subscribeBackgroundPositions", (pos) => {
|
|
1104
|
+
setCoords(normalizeGeolocationPosition(pos));
|
|
1105
|
+
}),
|
|
1106
|
+
safeSubscribe(client, "subscribeBackgroundWatchState", (watching) => {
|
|
1107
|
+
setBackgroundWatching(Boolean(watching));
|
|
1108
|
+
}),
|
|
1109
|
+
];
|
|
1110
|
+
return () => {
|
|
1111
|
+
for (const unsubscribe of unsubscribers) {
|
|
1112
|
+
if (!unsubscribe) continue;
|
|
1113
|
+
try {
|
|
1114
|
+
unsubscribe();
|
|
1115
|
+
} catch {
|
|
1116
|
+
/* the host already tore the subscription down */
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
}, [client]);
|
|
1121
|
+
|
|
1022
1122
|
const getCurrentPosition = useCallback(async () => {
|
|
1023
1123
|
const myRun = ++runRef.current;
|
|
1024
1124
|
const client = clientRef.current;
|
|
@@ -1037,14 +1137,7 @@ export function useGeolocation(options) {
|
|
|
1037
1137
|
setError(null);
|
|
1038
1138
|
try {
|
|
1039
1139
|
const pos = await client.getCurrentPosition(optionsRef.current);
|
|
1040
|
-
const next =
|
|
1041
|
-
latitude:
|
|
1042
|
-
pos && typeof pos.latitude === "number" ? pos.latitude : null,
|
|
1043
|
-
longitude:
|
|
1044
|
-
pos && typeof pos.longitude === "number" ? pos.longitude : null,
|
|
1045
|
-
accuracy:
|
|
1046
|
-
pos && typeof pos.accuracy === "number" ? pos.accuracy : null,
|
|
1047
|
-
};
|
|
1140
|
+
const next = normalizeGeolocationPosition(pos);
|
|
1048
1141
|
if (runRef.current !== myRun) return next;
|
|
1049
1142
|
setCoords(next);
|
|
1050
1143
|
setLoading(false);
|
|
@@ -1059,6 +1152,48 @@ export function useGeolocation(options) {
|
|
|
1059
1152
|
}
|
|
1060
1153
|
}, []);
|
|
1061
1154
|
|
|
1155
|
+
const startBackgroundWatch = useCallback(async (watchOptions) => {
|
|
1156
|
+
const client = clientRef.current;
|
|
1157
|
+
if (!client || typeof client.startBackgroundWatch !== "function") {
|
|
1158
|
+
const e = new GeolocationError(
|
|
1159
|
+
"UNSUPPORTED",
|
|
1160
|
+
"This host does not track location in the background.",
|
|
1161
|
+
);
|
|
1162
|
+
setError(e);
|
|
1163
|
+
throw e;
|
|
1164
|
+
}
|
|
1165
|
+
setError(null);
|
|
1166
|
+
try {
|
|
1167
|
+
await client.startBackgroundWatch(watchOptions);
|
|
1168
|
+
} catch (err) {
|
|
1169
|
+
const ge = toGeolocationError(err);
|
|
1170
|
+
setError(ge);
|
|
1171
|
+
throw ge;
|
|
1172
|
+
} finally {
|
|
1173
|
+
// Re-read rather than assume: the host knows whether the OS actually
|
|
1174
|
+
// took the subscription, and a failed start may still leave one.
|
|
1175
|
+
setBackgroundWatching(readBackgroundWatching(client));
|
|
1176
|
+
}
|
|
1177
|
+
}, []);
|
|
1178
|
+
|
|
1179
|
+
const stopBackgroundWatch = useCallback(async () => {
|
|
1180
|
+
const client = clientRef.current;
|
|
1181
|
+
if (!client || typeof client.stopBackgroundWatch !== "function") {
|
|
1182
|
+
setBackgroundWatching(false);
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
try {
|
|
1186
|
+
await client.stopBackgroundWatch();
|
|
1187
|
+
} catch (err) {
|
|
1188
|
+
const ge = toGeolocationError(err);
|
|
1189
|
+
setError(ge);
|
|
1190
|
+
throw ge;
|
|
1191
|
+
} finally {
|
|
1192
|
+
// A REFUSED stop leaves the watch running; only the host knows.
|
|
1193
|
+
setBackgroundWatching(readBackgroundWatching(client));
|
|
1194
|
+
}
|
|
1195
|
+
}, []);
|
|
1196
|
+
|
|
1062
1197
|
return {
|
|
1063
1198
|
latitude: coords ? coords.latitude : null,
|
|
1064
1199
|
longitude: coords ? coords.longitude : null,
|
|
@@ -1066,6 +1201,10 @@ export function useGeolocation(options) {
|
|
|
1066
1201
|
loading,
|
|
1067
1202
|
error,
|
|
1068
1203
|
getCurrentPosition,
|
|
1204
|
+
backgroundSupported,
|
|
1205
|
+
backgroundWatching,
|
|
1206
|
+
startBackgroundWatch,
|
|
1207
|
+
stopBackgroundWatch,
|
|
1069
1208
|
};
|
|
1070
1209
|
}
|
|
1071
1210
|
|
package/dist/host.d.ts
CHANGED
|
@@ -59,10 +59,14 @@ export function normaliseWidgetStyleFields(
|
|
|
59
59
|
export function normaliseWidgetStyles(raw: unknown): ThemeWidgetStyles;
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
* REQ-THEME-15 host render-boundary helper: folds the
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
62
|
+
* REQ-THEME-15 host render-boundary helper: folds the widget's declared
|
|
63
|
+
* `styleSchema` defaults and the theme's per-component tokens into a widget's
|
|
64
|
+
* props as `style` DEFAULTS, with the author's per-instance values winning.
|
|
65
|
+
*
|
|
66
|
+
* sc-6750 — precedence, weakest first: `styleSchema` default ->
|
|
67
|
+
* palette/`components.<scope>` -> `widgetStyles[manifestId]` -> per-instance
|
|
68
|
+
* `props.style`. Returns the same `props` reference when neither the theme nor
|
|
69
|
+
* the schema sets anything. Applied by the platform hosts, never by authors.
|
|
66
70
|
*/
|
|
67
71
|
export function applyThemeComponentStyle<T = Record<string, unknown>>(
|
|
68
72
|
manifestId: string,
|
package/dist/index.d.ts
CHANGED
|
@@ -645,6 +645,29 @@ export interface WidgetContext<TProps = unknown> {
|
|
|
645
645
|
longitude: number;
|
|
646
646
|
accuracy: number;
|
|
647
647
|
}>;
|
|
648
|
+
/** sc-6450 — false on web and on an export that did not opt in. */
|
|
649
|
+
isBackgroundSupported?(): boolean;
|
|
650
|
+
startBackgroundWatch?(
|
|
651
|
+
options?: BackgroundLocationOptions,
|
|
652
|
+
): Promise<void>;
|
|
653
|
+
stopBackgroundWatch?(): Promise<void>;
|
|
654
|
+
isBackgroundWatching?(): boolean;
|
|
655
|
+
/** Attach to the running watch; starts no sensor and prompts for nothing. */
|
|
656
|
+
subscribeBackgroundPositions?(
|
|
657
|
+
onPosition: (pos: {
|
|
658
|
+
latitude: number;
|
|
659
|
+
longitude: number;
|
|
660
|
+
accuracy: number;
|
|
661
|
+
}) => void,
|
|
662
|
+
): () => void;
|
|
663
|
+
/**
|
|
664
|
+
* sc-6450 — the host pushes whether a watch is running: it is primed
|
|
665
|
+
* asynchronously after a cold relaunch, the OS can end it on its own, and
|
|
666
|
+
* a sibling widget may start or stop it.
|
|
667
|
+
*/
|
|
668
|
+
subscribeBackgroundWatchState?(
|
|
669
|
+
onChange: (watching: boolean) => void,
|
|
670
|
+
): () => void;
|
|
648
671
|
};
|
|
649
672
|
};
|
|
650
673
|
}
|
|
@@ -1429,6 +1452,15 @@ export interface GeolocationOptions {
|
|
|
1429
1452
|
maximumAge?: number;
|
|
1430
1453
|
}
|
|
1431
1454
|
|
|
1455
|
+
/** sc-6450 — pass-through options for `startBackgroundWatch(...)`. */
|
|
1456
|
+
export interface BackgroundLocationOptions {
|
|
1457
|
+
enableHighAccuracy?: boolean;
|
|
1458
|
+
/** Report only after the device has moved this far, in metres. */
|
|
1459
|
+
distanceIntervalMeters?: number;
|
|
1460
|
+
/** Report no more often than this, in milliseconds. */
|
|
1461
|
+
timeIntervalMs?: number;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1432
1464
|
export interface GeolocationResult {
|
|
1433
1465
|
latitude: number | null;
|
|
1434
1466
|
longitude: number | null;
|
|
@@ -1446,6 +1478,26 @@ export interface GeolocationResult {
|
|
|
1446
1478
|
longitude: number;
|
|
1447
1479
|
accuracy: number;
|
|
1448
1480
|
}>;
|
|
1481
|
+
/**
|
|
1482
|
+
* sc-6450 — whether this host can track location while BACKGROUNDED. False
|
|
1483
|
+
* on the web Player and on an exported app whose workspace did not opt into
|
|
1484
|
+
* background location. Check it before rendering the control.
|
|
1485
|
+
*/
|
|
1486
|
+
backgroundSupported: boolean;
|
|
1487
|
+
/** Whether a background watch is currently running on this device. */
|
|
1488
|
+
backgroundWatching: boolean;
|
|
1489
|
+
/**
|
|
1490
|
+
* Start tracking while backgrounded — call from a user gesture. The watch
|
|
1491
|
+
* OUTLIVES the widget's mount; only `stopBackgroundWatch()` releases it.
|
|
1492
|
+
* Delivered positions land in the same `latitude`/`longitude`/`accuracy`
|
|
1493
|
+
* slots. Rejects with a `GeolocationError`.
|
|
1494
|
+
*
|
|
1495
|
+
* Tracking continues while the app RUNS in the background; it does not
|
|
1496
|
+
* survive the OS terminating the app.
|
|
1497
|
+
*/
|
|
1498
|
+
startBackgroundWatch(options?: BackgroundLocationOptions): Promise<void>;
|
|
1499
|
+
/** Release the OS subscription. */
|
|
1500
|
+
stopBackgroundWatch(): Promise<void>;
|
|
1449
1501
|
}
|
|
1450
1502
|
|
|
1451
1503
|
/**
|
|
@@ -1455,6 +1507,9 @@ export interface GeolocationResult {
|
|
|
1455
1507
|
* `navigator.geolocation`, the Expo export via `expo-location`. Safe to call on
|
|
1456
1508
|
* a host that doesn't broker geolocation: `getCurrentPosition()` then rejects
|
|
1457
1509
|
* with `code: "UNSUPPORTED"`.
|
|
1510
|
+
*
|
|
1511
|
+
* sc-6450 — the same hook also drives the native-only background watch; see
|
|
1512
|
+
* `backgroundSupported` / `startBackgroundWatch` on the result.
|
|
1458
1513
|
*/
|
|
1459
1514
|
export function useGeolocation(options?: GeolocationOptions): GeolocationResult;
|
|
1460
1515
|
|
package/dist/linter.js
CHANGED
|
@@ -56,152 +56,9 @@ const CONTRACT_RULES = CONTRACT.bannedApis.map((b) =>
|
|
|
56
56
|
_ruleForIdentifier(b.identifier, b.reason),
|
|
57
57
|
);
|
|
58
58
|
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
// is only dangerous as a real identifier reference — never as prose in a
|
|
63
|
-
// `//` comment or as character data inside a string — so matching the bare
|
|
64
|
-
// word there is a false positive that blocks an otherwise-clean widget (a
|
|
65
|
-
// comment that reads "the hour window the grid renders" must not trip
|
|
66
|
-
// `no-window`).
|
|
67
|
-
//
|
|
68
|
-
// Newlines are preserved verbatim so reported line numbers still line up
|
|
69
|
-
// with the original source. Template-literal `${ … }` expression holes are
|
|
70
|
-
// left intact: real code lives there and must still be scanned (`${window}`
|
|
71
|
-
// is a genuine escape). Backslash escapes inside strings/templates are
|
|
72
|
-
// consumed so an escaped quote (`"\""`) doesn't end the literal early.
|
|
73
|
-
function _stripNonCode(source, { keepStrings = false } = {}) {
|
|
74
|
-
let out = "";
|
|
75
|
-
const n = source.length;
|
|
76
|
-
let mode = "code"; // code | line | block | sq | dq | tmpl
|
|
77
|
-
// Brace depth, plus a stack of the depths at which an enclosing template
|
|
78
|
-
// literal resumes — lets a `${ … }` hole (which may itself contain `{}`,
|
|
79
|
-
// strings, or nested templates) be told apart from the literal text.
|
|
80
|
-
let braceDepth = 0;
|
|
81
|
-
const tmplStack = [];
|
|
82
|
-
const keep = (ch) => {
|
|
83
|
-
out += ch;
|
|
84
|
-
};
|
|
85
|
-
const blank = (ch) => {
|
|
86
|
-
out += ch === "\n" || ch === "\r" ? ch : " ";
|
|
87
|
-
};
|
|
88
|
-
// String / template CONTENT: blanked for the banned-identifier scan
|
|
89
|
-
// (prose must not trip `no-window`), kept for the host-API-URL scan,
|
|
90
|
-
// whose whole job is to find a URL literal.
|
|
91
|
-
const str = keepStrings ? keep : blank;
|
|
92
|
-
let i = 0;
|
|
93
|
-
while (i < n) {
|
|
94
|
-
const ch = source[i];
|
|
95
|
-
const nx = source[i + 1];
|
|
96
|
-
if (mode === "code") {
|
|
97
|
-
if (ch === "/" && nx === "/") {
|
|
98
|
-
mode = "line";
|
|
99
|
-
blank(ch);
|
|
100
|
-
blank(nx);
|
|
101
|
-
i += 2;
|
|
102
|
-
} else if (ch === "/" && nx === "*") {
|
|
103
|
-
mode = "block";
|
|
104
|
-
blank(ch);
|
|
105
|
-
blank(nx);
|
|
106
|
-
i += 2;
|
|
107
|
-
} else if (ch === "'") {
|
|
108
|
-
mode = "sq";
|
|
109
|
-
str(ch);
|
|
110
|
-
i += 1;
|
|
111
|
-
} else if (ch === '"') {
|
|
112
|
-
mode = "dq";
|
|
113
|
-
str(ch);
|
|
114
|
-
i += 1;
|
|
115
|
-
} else if (ch === "`") {
|
|
116
|
-
mode = "tmpl";
|
|
117
|
-
str(ch);
|
|
118
|
-
i += 1;
|
|
119
|
-
} else if (ch === "{") {
|
|
120
|
-
braceDepth += 1;
|
|
121
|
-
keep(ch);
|
|
122
|
-
i += 1;
|
|
123
|
-
} else if (ch === "}") {
|
|
124
|
-
braceDepth -= 1;
|
|
125
|
-
if (
|
|
126
|
-
tmplStack.length > 0 &&
|
|
127
|
-
tmplStack[tmplStack.length - 1] === braceDepth
|
|
128
|
-
) {
|
|
129
|
-
tmplStack.pop();
|
|
130
|
-
mode = "tmpl";
|
|
131
|
-
str(ch);
|
|
132
|
-
} else {
|
|
133
|
-
keep(ch);
|
|
134
|
-
}
|
|
135
|
-
i += 1;
|
|
136
|
-
} else {
|
|
137
|
-
keep(ch);
|
|
138
|
-
i += 1;
|
|
139
|
-
}
|
|
140
|
-
} else if (mode === "line") {
|
|
141
|
-
if (ch === "\n") {
|
|
142
|
-
mode = "code";
|
|
143
|
-
keep(ch);
|
|
144
|
-
} else {
|
|
145
|
-
blank(ch);
|
|
146
|
-
}
|
|
147
|
-
i += 1;
|
|
148
|
-
} else if (mode === "block") {
|
|
149
|
-
if (ch === "*" && nx === "/") {
|
|
150
|
-
mode = "code";
|
|
151
|
-
blank(ch);
|
|
152
|
-
blank(nx);
|
|
153
|
-
i += 2;
|
|
154
|
-
} else {
|
|
155
|
-
blank(ch);
|
|
156
|
-
i += 1;
|
|
157
|
-
}
|
|
158
|
-
} else if (mode === "sq" || mode === "dq") {
|
|
159
|
-
const quote = mode === "sq" ? "'" : '"';
|
|
160
|
-
if (ch === "\\") {
|
|
161
|
-
str(ch);
|
|
162
|
-
if (i + 1 < n) str(nx);
|
|
163
|
-
i += 2;
|
|
164
|
-
} else if (ch === quote) {
|
|
165
|
-
mode = "code";
|
|
166
|
-
str(ch);
|
|
167
|
-
i += 1;
|
|
168
|
-
} else if (ch === "\n") {
|
|
169
|
-
// A bare newline terminates an unterminated string in JS; bail back
|
|
170
|
-
// to code so malformed input can't blank the rest of the file.
|
|
171
|
-
mode = "code";
|
|
172
|
-
keep(ch);
|
|
173
|
-
i += 1;
|
|
174
|
-
} else {
|
|
175
|
-
str(ch);
|
|
176
|
-
i += 1;
|
|
177
|
-
}
|
|
178
|
-
} else {
|
|
179
|
-
// mode === "tmpl"
|
|
180
|
-
if (ch === "\\") {
|
|
181
|
-
str(ch);
|
|
182
|
-
if (i + 1 < n) str(nx);
|
|
183
|
-
i += 2;
|
|
184
|
-
} else if (ch === "`") {
|
|
185
|
-
mode = "code";
|
|
186
|
-
str(ch);
|
|
187
|
-
i += 1;
|
|
188
|
-
} else if (ch === "$" && nx === "{") {
|
|
189
|
-
// Enter an expression hole. Remember the brace depth the template
|
|
190
|
-
// resumes at, then count the `{` so its matching `}` is recognised.
|
|
191
|
-
tmplStack.push(braceDepth);
|
|
192
|
-
braceDepth += 1;
|
|
193
|
-
mode = "code";
|
|
194
|
-
keep(ch);
|
|
195
|
-
keep(nx);
|
|
196
|
-
i += 2;
|
|
197
|
-
} else {
|
|
198
|
-
str(ch);
|
|
199
|
-
i += 1;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return out;
|
|
204
|
-
}
|
|
59
|
+
// sc-6086: moved to source-mask.js so the packer's scanner masks source the
|
|
60
|
+
// same way this linter does. Alias kept — the rules below read _stripNonCode.
|
|
61
|
+
import { stripNonCode as _stripNonCode } from "./source-mask.js";
|
|
205
62
|
|
|
206
63
|
// Extra rules that don't map 1:1 to a banned identifier in the contract:
|
|
207
64
|
// host-internal imports that widgets must never touch.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// sc-6086: the shared source masker, extracted verbatim from linter.js so the
|
|
2
|
+
// linter, the packer's import scanner, and the dev-server guard all agree on
|
|
3
|
+
// what is code and what is a comment or string literal (CLAUDE.md §3).
|
|
4
|
+
//
|
|
5
|
+
// Length-preserving: every blanked character becomes a space and newlines are
|
|
6
|
+
// kept, so an index into the mask is the SAME index in the original source.
|
|
7
|
+
// That is what lets a caller match against the mask and splice the original.
|
|
8
|
+
|
|
9
|
+
// Replace the *content* of comments and string / template literals with
|
|
10
|
+
// spaces so the banned-identifier scan only ever sees executable code. A
|
|
11
|
+
// banned host-escape identifier (`window`, `document`, `eval`, `process`, …)
|
|
12
|
+
// is only dangerous as a real identifier reference — never as prose in a
|
|
13
|
+
// `//` comment or as character data inside a string — so matching the bare
|
|
14
|
+
// word there is a false positive that blocks an otherwise-clean widget (a
|
|
15
|
+
// comment that reads "the hour window the grid renders" must not trip
|
|
16
|
+
// `no-window`).
|
|
17
|
+
//
|
|
18
|
+
// Newlines are preserved verbatim so reported line numbers still line up
|
|
19
|
+
// with the original source. Template-literal `${ … }` expression holes are
|
|
20
|
+
// left intact: real code lives there and must still be scanned (`${window}`
|
|
21
|
+
// is a genuine escape). Backslash escapes inside strings/templates are
|
|
22
|
+
// consumed so an escaped quote (`"\""`) doesn't end the literal early.
|
|
23
|
+
export function stripNonCode(
|
|
24
|
+
source,
|
|
25
|
+
{ keepStrings = false, keepTemplates = keepStrings } = {},
|
|
26
|
+
) {
|
|
27
|
+
let out = "";
|
|
28
|
+
const n = source.length;
|
|
29
|
+
let mode = "code"; // code | line | block | sq | dq | tmpl
|
|
30
|
+
// Brace depth, plus a stack of the depths at which an enclosing template
|
|
31
|
+
// literal resumes — lets a `${ … }` hole (which may itself contain `{}`,
|
|
32
|
+
// strings, or nested templates) be told apart from the literal text.
|
|
33
|
+
let braceDepth = 0;
|
|
34
|
+
const tmplStack = [];
|
|
35
|
+
const keep = (ch) => {
|
|
36
|
+
out += ch;
|
|
37
|
+
};
|
|
38
|
+
const blank = (ch) => {
|
|
39
|
+
out += ch === "\n" || ch === "\r" ? ch : " ";
|
|
40
|
+
};
|
|
41
|
+
// String / template CONTENT: blanked for the banned-identifier scan
|
|
42
|
+
// (prose must not trip `no-window`), kept for the host-API-URL scan,
|
|
43
|
+
// whose whole job is to find a URL literal.
|
|
44
|
+
const str = keepStrings ? keep : blank;
|
|
45
|
+
// An import specifier is always a quoted string, never a template literal —
|
|
46
|
+
// so the import scanner keeps quoted content and blanks templates, which is
|
|
47
|
+
// what stops an `import … from "./x"` line inside a backtick block from
|
|
48
|
+
// reading as a real statement.
|
|
49
|
+
const tmplStr = keepTemplates ? keep : blank;
|
|
50
|
+
let i = 0;
|
|
51
|
+
while (i < n) {
|
|
52
|
+
const ch = source[i];
|
|
53
|
+
const nx = source[i + 1];
|
|
54
|
+
if (mode === "code") {
|
|
55
|
+
if (ch === "/" && nx === "/") {
|
|
56
|
+
mode = "line";
|
|
57
|
+
blank(ch);
|
|
58
|
+
blank(nx);
|
|
59
|
+
i += 2;
|
|
60
|
+
} else if (ch === "/" && nx === "*") {
|
|
61
|
+
mode = "block";
|
|
62
|
+
blank(ch);
|
|
63
|
+
blank(nx);
|
|
64
|
+
i += 2;
|
|
65
|
+
} else if (ch === "'") {
|
|
66
|
+
mode = "sq";
|
|
67
|
+
str(ch);
|
|
68
|
+
i += 1;
|
|
69
|
+
} else if (ch === '"') {
|
|
70
|
+
mode = "dq";
|
|
71
|
+
str(ch);
|
|
72
|
+
i += 1;
|
|
73
|
+
} else if (ch === "`") {
|
|
74
|
+
mode = "tmpl";
|
|
75
|
+
tmplStr(ch);
|
|
76
|
+
i += 1;
|
|
77
|
+
} else if (ch === "{") {
|
|
78
|
+
braceDepth += 1;
|
|
79
|
+
keep(ch);
|
|
80
|
+
i += 1;
|
|
81
|
+
} else if (ch === "}") {
|
|
82
|
+
braceDepth -= 1;
|
|
83
|
+
if (
|
|
84
|
+
tmplStack.length > 0 &&
|
|
85
|
+
tmplStack[tmplStack.length - 1] === braceDepth
|
|
86
|
+
) {
|
|
87
|
+
tmplStack.pop();
|
|
88
|
+
mode = "tmpl";
|
|
89
|
+
str(ch);
|
|
90
|
+
} else {
|
|
91
|
+
keep(ch);
|
|
92
|
+
}
|
|
93
|
+
i += 1;
|
|
94
|
+
} else {
|
|
95
|
+
keep(ch);
|
|
96
|
+
i += 1;
|
|
97
|
+
}
|
|
98
|
+
} else if (mode === "line") {
|
|
99
|
+
if (ch === "\n") {
|
|
100
|
+
mode = "code";
|
|
101
|
+
keep(ch);
|
|
102
|
+
} else {
|
|
103
|
+
blank(ch);
|
|
104
|
+
}
|
|
105
|
+
i += 1;
|
|
106
|
+
} else if (mode === "block") {
|
|
107
|
+
if (ch === "*" && nx === "/") {
|
|
108
|
+
mode = "code";
|
|
109
|
+
blank(ch);
|
|
110
|
+
blank(nx);
|
|
111
|
+
i += 2;
|
|
112
|
+
} else {
|
|
113
|
+
blank(ch);
|
|
114
|
+
i += 1;
|
|
115
|
+
}
|
|
116
|
+
} else if (mode === "sq" || mode === "dq") {
|
|
117
|
+
const quote = mode === "sq" ? "'" : '"';
|
|
118
|
+
if (ch === "\\") {
|
|
119
|
+
str(ch);
|
|
120
|
+
if (i + 1 < n) str(nx);
|
|
121
|
+
i += 2;
|
|
122
|
+
} else if (ch === quote) {
|
|
123
|
+
mode = "code";
|
|
124
|
+
str(ch);
|
|
125
|
+
i += 1;
|
|
126
|
+
} else if (ch === "\n") {
|
|
127
|
+
// A bare newline terminates an unterminated string in JS; bail back
|
|
128
|
+
// to code so malformed input can't blank the rest of the file.
|
|
129
|
+
mode = "code";
|
|
130
|
+
keep(ch);
|
|
131
|
+
i += 1;
|
|
132
|
+
} else {
|
|
133
|
+
str(ch);
|
|
134
|
+
i += 1;
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
// mode === "tmpl"
|
|
138
|
+
if (ch === "\\") {
|
|
139
|
+
tmplStr(ch);
|
|
140
|
+
if (i + 1 < n) tmplStr(nx);
|
|
141
|
+
i += 2;
|
|
142
|
+
} else if (ch === "`") {
|
|
143
|
+
mode = "code";
|
|
144
|
+
tmplStr(ch);
|
|
145
|
+
i += 1;
|
|
146
|
+
} else if (ch === "$" && nx === "{") {
|
|
147
|
+
// Enter an expression hole. Remember the brace depth the template
|
|
148
|
+
// resumes at, then count the `{` so its matching `}` is recognised.
|
|
149
|
+
tmplStack.push(braceDepth);
|
|
150
|
+
braceDepth += 1;
|
|
151
|
+
mode = "code";
|
|
152
|
+
keep(ch);
|
|
153
|
+
keep(nx);
|
|
154
|
+
i += 2;
|
|
155
|
+
} else {
|
|
156
|
+
tmplStr(ch);
|
|
157
|
+
i += 1;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|