@colixsystems/widget-sdk 0.198.0 → 0.200.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 +96 -3
- package/dist/bundled-assets-fs.js +60 -0
- package/dist/bundled-assets.cjs +239 -0
- package/dist/bundled-assets.js +239 -0
- package/dist/cli.js +52 -11
- package/dist/contract.cjs +42 -3
- package/dist/contract.js +42 -3
- package/dist/devserver.js +68 -10
- package/dist/emoji-picker.js +2 -1
- package/dist/hooks.js +14 -0
- package/dist/host.d.ts +6 -0
- package/dist/host.js +1 -0
- package/dist/index.d.ts +70 -1
- package/dist/index.js +6 -0
- package/dist/index.native.js +6 -0
- package/dist/linter.cjs +28 -0
- package/dist/linter.js +28 -0
- package/dist/manifest.cjs +1 -0
- package/dist/manifest.js +1 -0
- package/dist/markdown-input.js +6 -1
- package/dist/primitives.js +2 -1
- package/dist/text-field.js +2 -1
- package/dist/text-input-view.js +27 -0
- package/dist/theme-background.cjs +26 -3
- package/dist/theme-background.js +20 -2
- package/dist/touch-input-font-floor.js +22 -0
- package/package.json +7 -1
- package/src/contract.js +42 -3
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// — REQ-WSDK-DEVKIT local dev server
|
|
6
6
|
|
|
7
7
|
import { readFileSync } from "node:fs";
|
|
8
|
-
import { basename, resolve } from "node:path";
|
|
8
|
+
import { basename, dirname, resolve } from "node:path";
|
|
9
9
|
import { pathToFileURL } from "node:url";
|
|
10
10
|
import { argv, exit, stderr, stdout } from "node:process";
|
|
11
11
|
import {
|
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
narrowManifestForFile,
|
|
16
16
|
} from "./linter.js";
|
|
17
17
|
import { startDevServer } from "./devserver.js";
|
|
18
|
+
import {
|
|
19
|
+
bundledAssetDirectoryOf,
|
|
20
|
+
loadBundledAssets,
|
|
21
|
+
} from "./bundled-assets-fs.js";
|
|
18
22
|
|
|
19
23
|
function usage() {
|
|
20
24
|
stderr.write(
|
|
@@ -22,6 +26,8 @@ function usage() {
|
|
|
22
26
|
" appstudio-widget lint <file.jsx...> [--manifest <path>]\n" +
|
|
23
27
|
" Pass every file the widget ships plus --manifest to also check " +
|
|
24
28
|
"that each styleSchema field is actually read.\n" +
|
|
29
|
+
" An assets/ directory beside a file is checked too: every image " +
|
|
30
|
+
"and sound in it must be a type the package may bundle.\n" +
|
|
25
31
|
" appstudio-widget dev <entry.jsx|widget-dir> [--port <n>] [--manifest <path>]\n" +
|
|
26
32
|
" A directory containing widget.json runs in multi-file mode " +
|
|
27
33
|
"(REQ-WSDK-DEVKIT v2): the dev server reads the canonical web entry, " +
|
|
@@ -57,6 +63,28 @@ function parseFlags(args) {
|
|
|
57
63
|
return { flags, positionals };
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
// One read per assets/ directory, however many linted files sit beside it.
|
|
67
|
+
function loadAssetsBesideFiles(filePaths) {
|
|
68
|
+
const byDirectory = new Map();
|
|
69
|
+
for (const filePath of filePaths) {
|
|
70
|
+
const assetsDir = bundledAssetDirectoryOf(dirname(filePath));
|
|
71
|
+
if (!byDirectory.has(assetsDir)) {
|
|
72
|
+
byDirectory.set(assetsDir, loadBundledAssets(assetsDir));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return byDirectory;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function assetFindings(assets) {
|
|
79
|
+
return assets.errors.map((error) => ({
|
|
80
|
+
rule: error.code,
|
|
81
|
+
severity: "error",
|
|
82
|
+
label: error.message,
|
|
83
|
+
line: 0,
|
|
84
|
+
snippet: "",
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
|
|
60
88
|
async function loadManifest(path) {
|
|
61
89
|
const abs = resolve(path);
|
|
62
90
|
if (abs.endsWith(".json")) return JSON.parse(readFileSync(abs, "utf8"));
|
|
@@ -68,9 +96,8 @@ function writeFindings(findings, stream) {
|
|
|
68
96
|
for (const f of findings) {
|
|
69
97
|
const severity = f.severity === "warning" ? "warning" : "error";
|
|
70
98
|
const where = f.line ? ` line ${f.line}` : "";
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
);
|
|
99
|
+
const snippet = f.snippet ? ` ${f.snippet}\n` : "";
|
|
100
|
+
stream.write(` ${severity} [${f.rule}]${where}: ${f.label}\n${snippet}`);
|
|
74
101
|
}
|
|
75
102
|
}
|
|
76
103
|
|
|
@@ -104,16 +131,20 @@ async function runLint(rest) {
|
|
|
104
131
|
}
|
|
105
132
|
}
|
|
106
133
|
const label = positionals.join(", ");
|
|
134
|
+
const assetsByDirectory = loadAssetsBesideFiles(Object.keys(files));
|
|
135
|
+
const assetsBeside = (filePath) =>
|
|
136
|
+
assetsByDirectory.get(bundledAssetDirectoryOf(dirname(filePath)));
|
|
107
137
|
// Narrow per split-impl file, as the submission gate does.
|
|
108
138
|
const perFile = Object.entries(files).map(([filePath, source]) => [
|
|
109
139
|
filePath,
|
|
110
|
-
lintSource(
|
|
111
|
-
|
|
112
|
-
manifest
|
|
140
|
+
lintSource(source, {
|
|
141
|
+
assetFiles: assetsBeside(filePath).fileNames,
|
|
142
|
+
...(manifest
|
|
113
143
|
? { manifest: narrowManifestForFile(manifest, basename(filePath)) }
|
|
114
|
-
:
|
|
115
|
-
),
|
|
144
|
+
: {}),
|
|
145
|
+
}),
|
|
116
146
|
]);
|
|
147
|
+
const bundled = [...assetsByDirectory.values()].flatMap(assetFindings);
|
|
117
148
|
const wiring = manifest
|
|
118
149
|
? lintStyleWiring(manifest, files)
|
|
119
150
|
: { ok: true, findings: [] };
|
|
@@ -123,6 +154,7 @@ async function runLint(rest) {
|
|
|
123
154
|
: { ok: true, findings: [] };
|
|
124
155
|
const all = perFile
|
|
125
156
|
.flatMap(([, r]) => r.findings)
|
|
157
|
+
.concat(bundled)
|
|
126
158
|
.concat(wiring.findings)
|
|
127
159
|
.concat(editability.findings);
|
|
128
160
|
if (all.length === 0) {
|
|
@@ -132,7 +164,11 @@ async function runLint(rest) {
|
|
|
132
164
|
// sc-3493 — a warning-severity finding used to be swallowed: `ok` stays true
|
|
133
165
|
// for warnings, so the CLI printed "clean" and dropped them. A warning nobody
|
|
134
166
|
// sees is pointless. Report every finding; only errors change the exit code.
|
|
135
|
-
const ok =
|
|
167
|
+
const ok =
|
|
168
|
+
perFile.every(([, r]) => r.ok) &&
|
|
169
|
+
bundled.length === 0 &&
|
|
170
|
+
wiring.ok &&
|
|
171
|
+
editability.ok;
|
|
136
172
|
const errors = all.filter((f) => f.severity !== "warning").length;
|
|
137
173
|
const stream = ok ? stdout : stderr;
|
|
138
174
|
stream.write(
|
|
@@ -143,6 +179,10 @@ async function runLint(rest) {
|
|
|
143
179
|
if (perFile.length > 1) stream.write(`${filePath}:\n`);
|
|
144
180
|
writeFindings(r.findings, stream);
|
|
145
181
|
}
|
|
182
|
+
if (bundled.length > 0) {
|
|
183
|
+
stream.write("assets:\n");
|
|
184
|
+
writeFindings(bundled, stream);
|
|
185
|
+
}
|
|
146
186
|
if (wiring.findings.length > 0) {
|
|
147
187
|
stream.write(`manifest.styleSchema:\n`);
|
|
148
188
|
writeFindings(wiring.findings, stream);
|
|
@@ -185,7 +225,8 @@ async function runDev(rest) {
|
|
|
185
225
|
` manifest: ${handle.url}/manifest.json\n` +
|
|
186
226
|
` reload: ${handle.url}/__dev/events (SSE)\n` +
|
|
187
227
|
(handle.directoryMode
|
|
188
|
-
? ` files: ${handle.url}/file/<relpath>\n shims: ${handle.url}/shim/<slug>\n`
|
|
228
|
+
? ` files: ${handle.url}/file/<relpath>\n shims: ${handle.url}/shim/<slug>\n` +
|
|
229
|
+
` assets: ${handle.url}/assets/<name>\n`
|
|
189
230
|
: "") +
|
|
190
231
|
(handle.manifestId ? ` id: ${handle.manifestId}\n` : "") +
|
|
191
232
|
`\nIn the Studio Builder (dev mode), open the "Dev widgets" panel and add:\n` +
|
package/dist/contract.cjs
CHANGED
|
@@ -1171,6 +1171,20 @@ const HOOKS = [
|
|
|
1171
1171
|
requiredContextSlice: ["assets.list"],
|
|
1172
1172
|
scopes: null,
|
|
1173
1173
|
},
|
|
1174
|
+
{
|
|
1175
|
+
name: "useBundledAsset",
|
|
1176
|
+
signature: "useBundledAsset(fileName)",
|
|
1177
|
+
description:
|
|
1178
|
+
"Returns the URL of an image or sound file the widget package ships in " +
|
|
1179
|
+
"its assets/ directory, or null when the package has no file with that " +
|
|
1180
|
+
"name. Only an uploaded package can bundle files; a widget written in " +
|
|
1181
|
+
"the Studio has none.",
|
|
1182
|
+
returnShape: {
|
|
1183
|
+
"(returns)": "string | null",
|
|
1184
|
+
},
|
|
1185
|
+
requiredContextSlice: ["widget.assets"],
|
|
1186
|
+
scopes: null,
|
|
1187
|
+
},
|
|
1174
1188
|
{
|
|
1175
1189
|
name: "useFilestoreFiles",
|
|
1176
1190
|
signature: "useFilestoreFiles({ spaceType, recordId, folderId, q, type })",
|
|
@@ -2888,6 +2902,19 @@ const ACTION_SCRIPT_GLOBALS = [
|
|
|
2888
2902
|
// Mirrors action.service.js SCRIPT_MAX_BYTES.
|
|
2889
2903
|
const ACTION_SCRIPT_MAX_BYTES = 200 * 1024;
|
|
2890
2904
|
|
|
2905
|
+
// The one package size limit; the backend derives its upload caps from it.
|
|
2906
|
+
const PACKAGE_MAX_BYTES = 12 * 1024 * 1024;
|
|
2907
|
+
|
|
2908
|
+
const BUNDLED_ASSETS = {
|
|
2909
|
+
directory: "assets",
|
|
2910
|
+
maxFiles: 50,
|
|
2911
|
+
maxNameLength: 64,
|
|
2912
|
+
types: {
|
|
2913
|
+
image: { extensions: ["png", "jpg", "jpeg", "gif", "webp"] },
|
|
2914
|
+
audio: { extensions: ["mp3", "m4a", "wav"] },
|
|
2915
|
+
},
|
|
2916
|
+
};
|
|
2917
|
+
|
|
2891
2918
|
/**
|
|
2892
2919
|
* sc-4915 — the trigger set a manifest action declares, canonically ordered.
|
|
2893
2920
|
*
|
|
@@ -3071,9 +3098,15 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3071
3098
|
},
|
|
3072
3099
|
widget: {
|
|
3073
3100
|
description:
|
|
3074
|
-
"Identity of the currently rendered widget instance ({ id, version })
|
|
3101
|
+
"Identity of the currently rendered widget instance ({ id, version }) " +
|
|
3102
|
+
"and the files its package bundles.",
|
|
3075
3103
|
required: true,
|
|
3076
|
-
fields: {
|
|
3104
|
+
fields: {
|
|
3105
|
+
id: "manifest.id",
|
|
3106
|
+
version: "manifest.version",
|
|
3107
|
+
assets:
|
|
3108
|
+
"{ [fileName]: string } — bundled file name → absolute URL; {} when the package bundles nothing",
|
|
3109
|
+
},
|
|
3077
3110
|
},
|
|
3078
3111
|
// REQ-LAY-08 — optional host layout hint backing useFill(). True when the
|
|
3079
3112
|
// host sized this widget to fill its layout slot's available height (a
|
|
@@ -5268,7 +5301,11 @@ const CONTRACT = deepFreeze({
|
|
|
5268
5301
|
// asset carrying `durationMs`. New `useHardwareBack(handler)` walks a
|
|
5269
5302
|
// widget's own pages on Android's back button (optional
|
|
5270
5303
|
// `ctx.navigation.onHardwareBack`; unsupported on the web).
|
|
5271
|
-
|
|
5304
|
+
// 1.159.0: additive (sc-10721) — a package may ship images and sounds in a
|
|
5305
|
+
// flat `assets/` directory (`bundledAssets`, within `packageMaxBytes`).
|
|
5306
|
+
// New `useBundledAsset(fileName)` resolves one through the new
|
|
5307
|
+
// `widget.assets` map; linter rule `bundled-asset-missing` (error).
|
|
5308
|
+
version: "1.159.0",
|
|
5272
5309
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5273
5310
|
hooks: HOOKS,
|
|
5274
5311
|
primitives: PRIMITIVES,
|
|
@@ -5278,6 +5315,8 @@ const CONTRACT = deepFreeze({
|
|
|
5278
5315
|
actionTriggerTypes: ACTION_TRIGGER_TYPES,
|
|
5279
5316
|
actionScriptGlobals: ACTION_SCRIPT_GLOBALS,
|
|
5280
5317
|
actionScriptMaxBytes: ACTION_SCRIPT_MAX_BYTES,
|
|
5318
|
+
packageMaxBytes: PACKAGE_MAX_BYTES,
|
|
5319
|
+
bundledAssets: BUNDLED_ASSETS,
|
|
5281
5320
|
themeTokens: DEFAULT_THEME_TOKENS,
|
|
5282
5321
|
themeColorTokens: THEME_COLOR_TOKENS,
|
|
5283
5322
|
themeComponents: THEME_COMPONENTS,
|
package/dist/contract.js
CHANGED
|
@@ -1171,6 +1171,20 @@ const HOOKS = [
|
|
|
1171
1171
|
requiredContextSlice: ["assets.list"],
|
|
1172
1172
|
scopes: null,
|
|
1173
1173
|
},
|
|
1174
|
+
{
|
|
1175
|
+
name: "useBundledAsset",
|
|
1176
|
+
signature: "useBundledAsset(fileName)",
|
|
1177
|
+
description:
|
|
1178
|
+
"Returns the URL of an image or sound file the widget package ships in " +
|
|
1179
|
+
"its assets/ directory, or null when the package has no file with that " +
|
|
1180
|
+
"name. Only an uploaded package can bundle files; a widget written in " +
|
|
1181
|
+
"the Studio has none.",
|
|
1182
|
+
returnShape: {
|
|
1183
|
+
"(returns)": "string | null",
|
|
1184
|
+
},
|
|
1185
|
+
requiredContextSlice: ["widget.assets"],
|
|
1186
|
+
scopes: null,
|
|
1187
|
+
},
|
|
1174
1188
|
{
|
|
1175
1189
|
name: "useFilestoreFiles",
|
|
1176
1190
|
signature: "useFilestoreFiles({ spaceType, recordId, folderId, q, type })",
|
|
@@ -2884,6 +2898,19 @@ const ACTION_SCRIPT_GLOBALS = [
|
|
|
2884
2898
|
// Mirrors action.service.js SCRIPT_MAX_BYTES.
|
|
2885
2899
|
const ACTION_SCRIPT_MAX_BYTES = 200 * 1024;
|
|
2886
2900
|
|
|
2901
|
+
// The one package size limit; the backend derives its upload caps from it.
|
|
2902
|
+
const PACKAGE_MAX_BYTES = 12 * 1024 * 1024;
|
|
2903
|
+
|
|
2904
|
+
const BUNDLED_ASSETS = {
|
|
2905
|
+
directory: "assets",
|
|
2906
|
+
maxFiles: 50,
|
|
2907
|
+
maxNameLength: 64,
|
|
2908
|
+
types: {
|
|
2909
|
+
image: { extensions: ["png", "jpg", "jpeg", "gif", "webp"] },
|
|
2910
|
+
audio: { extensions: ["mp3", "m4a", "wav"] },
|
|
2911
|
+
},
|
|
2912
|
+
};
|
|
2913
|
+
|
|
2887
2914
|
/**
|
|
2888
2915
|
* sc-4915 — the trigger set a manifest action declares, canonically ordered.
|
|
2889
2916
|
*
|
|
@@ -3067,9 +3094,15 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3067
3094
|
},
|
|
3068
3095
|
widget: {
|
|
3069
3096
|
description:
|
|
3070
|
-
"Identity of the currently rendered widget instance ({ id, version })
|
|
3097
|
+
"Identity of the currently rendered widget instance ({ id, version }) " +
|
|
3098
|
+
"and the files its package bundles.",
|
|
3071
3099
|
required: true,
|
|
3072
|
-
fields: {
|
|
3100
|
+
fields: {
|
|
3101
|
+
id: "manifest.id",
|
|
3102
|
+
version: "manifest.version",
|
|
3103
|
+
assets:
|
|
3104
|
+
"{ [fileName]: string } — bundled file name → absolute URL; {} when the package bundles nothing",
|
|
3105
|
+
},
|
|
3073
3106
|
},
|
|
3074
3107
|
// REQ-LAY-08 — optional host layout hint backing useFill(). True when the
|
|
3075
3108
|
// host sized this widget to fill its layout slot's available height (a
|
|
@@ -5264,7 +5297,11 @@ const CONTRACT = deepFreeze({
|
|
|
5264
5297
|
// asset carrying `durationMs`. New `useHardwareBack(handler)` walks a
|
|
5265
5298
|
// widget's own pages on Android's back button (optional
|
|
5266
5299
|
// `ctx.navigation.onHardwareBack`; unsupported on the web).
|
|
5267
|
-
|
|
5300
|
+
// 1.159.0: additive (sc-10721) — a package may ship images and sounds in a
|
|
5301
|
+
// flat `assets/` directory (`bundledAssets`, within `packageMaxBytes`).
|
|
5302
|
+
// New `useBundledAsset(fileName)` resolves one through the new
|
|
5303
|
+
// `widget.assets` map; linter rule `bundled-asset-missing` (error).
|
|
5304
|
+
version: "1.159.0",
|
|
5268
5305
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5269
5306
|
hooks: HOOKS,
|
|
5270
5307
|
primitives: PRIMITIVES,
|
|
@@ -5274,6 +5311,8 @@ const CONTRACT = deepFreeze({
|
|
|
5274
5311
|
actionTriggerTypes: ACTION_TRIGGER_TYPES,
|
|
5275
5312
|
actionScriptGlobals: ACTION_SCRIPT_GLOBALS,
|
|
5276
5313
|
actionScriptMaxBytes: ACTION_SCRIPT_MAX_BYTES,
|
|
5314
|
+
packageMaxBytes: PACKAGE_MAX_BYTES,
|
|
5315
|
+
bundledAssets: BUNDLED_ASSETS,
|
|
5277
5316
|
themeTokens: DEFAULT_THEME_TOKENS,
|
|
5278
5317
|
themeColorTokens: THEME_COLOR_TOKENS,
|
|
5279
5318
|
themeComponents: THEME_COMPONENTS,
|
package/dist/devserver.js
CHANGED
|
@@ -24,8 +24,11 @@
|
|
|
24
24
|
// `globalThis.__appstudioWidgetHost__` — the same global the Studio's
|
|
25
25
|
// blob shims populate, so the dev path stays in lockstep with the
|
|
26
26
|
// published path;
|
|
27
|
+
// • serves the widget's bundled images and sounds at `/assets/<name>` and
|
|
28
|
+
// lists them at `/assets.json`;
|
|
27
29
|
// • watches the whole widget directory recursively for `.jsx`/`.json` and
|
|
28
|
-
// broadcasts `reload` to every connected
|
|
30
|
+
// anything under `assets/`, and broadcasts `reload` to every connected
|
|
31
|
+
// client.
|
|
29
32
|
//
|
|
30
33
|
// single-file mode (legacy v1 — passed a bare `.jsx` entry)
|
|
31
34
|
// • original REQ-WSDK-DEVKIT behaviour. Kept so existing tasks don't
|
|
@@ -60,6 +63,15 @@ import {
|
|
|
60
63
|
} from "node:fs";
|
|
61
64
|
import { resolve, dirname, join, sep, relative, posix } from "node:path";
|
|
62
65
|
import { lintSource } from "./linter.js";
|
|
66
|
+
import {
|
|
67
|
+
BUNDLED_ASSET_DIRECTORY,
|
|
68
|
+
isValidBundledAssetName,
|
|
69
|
+
sniffBundledAssetType,
|
|
70
|
+
} from "./bundled-assets.js";
|
|
71
|
+
import {
|
|
72
|
+
bundledAssetDirectoryOf,
|
|
73
|
+
loadBundledAssets,
|
|
74
|
+
} from "./bundled-assets-fs.js";
|
|
63
75
|
import {
|
|
64
76
|
getShimSource,
|
|
65
77
|
shimmableSpecifiersForSiblings,
|
|
@@ -414,6 +426,16 @@ function _isUnderReal(parentRealAbs, childAbs) {
|
|
|
414
426
|
}
|
|
415
427
|
}
|
|
416
428
|
|
|
429
|
+
const ASSET_ROUTE_PREFIX = `/${BUNDLED_ASSET_DIRECTORY}/`;
|
|
430
|
+
const ASSET_PATH_RE = new RegExp(`^${BUNDLED_ASSET_DIRECTORY}[\\\\/]`);
|
|
431
|
+
|
|
432
|
+
/** True when a change to `filename` (relative to the watch root) reloads. */
|
|
433
|
+
export function isReloadTrigger(filename) {
|
|
434
|
+
if (!filename) return true;
|
|
435
|
+
const path = String(filename);
|
|
436
|
+
return /\.(jsx?|json)$/.test(path) || ASSET_PATH_RE.test(path);
|
|
437
|
+
}
|
|
438
|
+
|
|
417
439
|
function setCors(res) {
|
|
418
440
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
419
441
|
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
@@ -545,6 +567,35 @@ export function createDevServer({
|
|
|
545
567
|
return { abs, rel: normalised };
|
|
546
568
|
}
|
|
547
569
|
|
|
570
|
+
// Single-file mode has no widget directory, so it bundles nothing.
|
|
571
|
+
const assetsDir = directoryMode ? bundledAssetDirectoryOf(watchRoot) : null;
|
|
572
|
+
const bundledAssets = () => loadBundledAssets(assetsDir);
|
|
573
|
+
|
|
574
|
+
function serveAssetList(res) {
|
|
575
|
+
const names = bundledAssets().assets.map((asset) => asset.name);
|
|
576
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
577
|
+
res.end(JSON.stringify({ assets: names }));
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function serveAsset(res, name) {
|
|
581
|
+
const resolved = isValidBundledAssetName(name)
|
|
582
|
+
? resolveFileRel(`${BUNDLED_ASSET_DIRECTORY}/${name}`)
|
|
583
|
+
: null;
|
|
584
|
+
const bytes = resolved ? readFileSync(resolved.abs) : null;
|
|
585
|
+
const type = bytes ? sniffBundledAssetType(bytes) : null;
|
|
586
|
+
if (!type) {
|
|
587
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
588
|
+
res.end("Bundled file not found");
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
res.writeHead(200, {
|
|
592
|
+
"Content-Type": type.mimeType,
|
|
593
|
+
"Content-Length": bytes.length,
|
|
594
|
+
"Cache-Control": "no-store",
|
|
595
|
+
});
|
|
596
|
+
res.end(bytes);
|
|
597
|
+
}
|
|
598
|
+
|
|
548
599
|
function transpileWithGuards(absPath) {
|
|
549
600
|
const source = readFileSync(absPath, "utf8");
|
|
550
601
|
try {
|
|
@@ -754,6 +805,10 @@ export function createDevServer({
|
|
|
754
805
|
);
|
|
755
806
|
return;
|
|
756
807
|
}
|
|
808
|
+
if (url === "/assets.json") return serveAssetList(res);
|
|
809
|
+
if (url.startsWith(ASSET_ROUTE_PREFIX)) {
|
|
810
|
+
return serveAsset(res, url.slice(ASSET_ROUTE_PREFIX.length));
|
|
811
|
+
}
|
|
757
812
|
if (directoryMode && url.startsWith("/file/")) {
|
|
758
813
|
return serveFile(req, res, url.slice("/file/".length));
|
|
759
814
|
}
|
|
@@ -782,15 +837,18 @@ export function createDevServer({
|
|
|
782
837
|
} catch {
|
|
783
838
|
return;
|
|
784
839
|
}
|
|
785
|
-
const
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
840
|
+
const assets = bundledAssets();
|
|
841
|
+
const { ok, findings } = lintSource(source, {
|
|
842
|
+
assetFiles: assets.fileNames,
|
|
843
|
+
});
|
|
844
|
+
if (!onLint) return;
|
|
845
|
+
if (ok && assets.ok) {
|
|
846
|
+
onLint("lint: clean");
|
|
847
|
+
return;
|
|
793
848
|
}
|
|
849
|
+
onLint(`lint: ${findings.length + assets.errors.length} finding(s)`);
|
|
850
|
+
for (const e of assets.errors) onLint(` [${e.code}] ${e.message}`);
|
|
851
|
+
for (const f of findings) onLint(` [${f.rule}] line ${f.line}: ${f.label}`);
|
|
794
852
|
}
|
|
795
853
|
|
|
796
854
|
return {
|
|
@@ -939,7 +997,7 @@ export async function startDevServer({
|
|
|
939
997
|
dev.watchRoot,
|
|
940
998
|
{ recursive: directoryMode },
|
|
941
999
|
(_event, filename) => {
|
|
942
|
-
if (
|
|
1000
|
+
if (!isReloadTrigger(filename)) return;
|
|
943
1001
|
if (timer) clearTimeout(timer);
|
|
944
1002
|
timer = setTimeout(() => {
|
|
945
1003
|
timer = null;
|
package/dist/emoji-picker.js
CHANGED
|
@@ -10,5 +10,6 @@
|
|
|
10
10
|
import * as ReactNative from "react-native-web";
|
|
11
11
|
import { makeEmojiPicker } from "./emoji-picker-view.js";
|
|
12
12
|
import { Overlay } from "./overlay.js";
|
|
13
|
+
import { TextInput } from "./primitives.js";
|
|
13
14
|
|
|
14
|
-
export const EmojiPicker = makeEmojiPicker({ ...ReactNative, Overlay });
|
|
15
|
+
export const EmojiPicker = makeEmojiPicker({ ...ReactNative, TextInput, Overlay });
|
package/dist/hooks.js
CHANGED
|
@@ -4334,6 +4334,20 @@ export function useAsset(assetId) {
|
|
|
4334
4334
|
return { url, asset, loading, error, refetch };
|
|
4335
4335
|
}
|
|
4336
4336
|
|
|
4337
|
+
/**
|
|
4338
|
+
* URL of a file the widget package ships in its `assets/` directory, or
|
|
4339
|
+
* `null` when the package bundles no file with that name.
|
|
4340
|
+
*/
|
|
4341
|
+
export function useBundledAsset(fileName) {
|
|
4342
|
+
const ctx = useWidgetContextOrThrow("useBundledAsset");
|
|
4343
|
+
// An older host publishes no `widget.assets`.
|
|
4344
|
+
const assets = ctx.widget && ctx.widget.assets;
|
|
4345
|
+
if (!fileName || !assets) return null;
|
|
4346
|
+
return Object.prototype.hasOwnProperty.call(assets, fileName)
|
|
4347
|
+
? assets[fileName]
|
|
4348
|
+
: null;
|
|
4349
|
+
}
|
|
4350
|
+
|
|
4337
4351
|
/**
|
|
4338
4352
|
* List every tenant asset carrying a given tag. Returns
|
|
4339
4353
|
* `{ assets, loading, error, refetch }`. The widget passes a single `tag`
|
package/dist/host.d.ts
CHANGED
|
@@ -460,6 +460,12 @@ export interface LoginDesign {
|
|
|
460
460
|
/** The sign-in screen's authored design, or null when nothing is configured. */
|
|
461
461
|
export function normaliseLoginDesign(raw: unknown): LoginDesign | null;
|
|
462
462
|
|
|
463
|
+
/** sc-10845 — the sign-in card's logo: the author's own, else the app logo. */
|
|
464
|
+
export function resolveLoginLogoUrl(
|
|
465
|
+
login: unknown,
|
|
466
|
+
appLogoUrl: string | null | undefined,
|
|
467
|
+
): string | null;
|
|
468
|
+
|
|
463
469
|
/** sc-7800 — true when a stored value is a `theme:<path>` colour binding. */
|
|
464
470
|
export function isThemeTokenRef(value: unknown): boolean;
|
|
465
471
|
|
package/dist/host.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -698,7 +698,13 @@ export interface RecordMuteWire {
|
|
|
698
698
|
|
|
699
699
|
export interface WidgetContext<TProps = unknown> {
|
|
700
700
|
props: TProps;
|
|
701
|
-
widget: {
|
|
701
|
+
widget: {
|
|
702
|
+
id: string;
|
|
703
|
+
instanceId: string;
|
|
704
|
+
version: string;
|
|
705
|
+
/** Bundled file name → absolute URL; `{}` when the package bundles nothing. */
|
|
706
|
+
assets: Record<string, string>;
|
|
707
|
+
};
|
|
702
708
|
/**
|
|
703
709
|
* REQ-LAY-08 — optional host layout hint backing `useFill()`. `true` when
|
|
704
710
|
* the host sized this widget to fill its layout slot's available height (a
|
|
@@ -1614,6 +1620,18 @@ export function useAsset(assetId: string | null | undefined): {
|
|
|
1614
1620
|
refetch(): Promise<void>;
|
|
1615
1621
|
};
|
|
1616
1622
|
|
|
1623
|
+
/**
|
|
1624
|
+
* URL of an image or sound file the widget package ships in its `assets/`
|
|
1625
|
+
* directory, or `null` when the package has no file with that name.
|
|
1626
|
+
*
|
|
1627
|
+
* ```jsx
|
|
1628
|
+
* const logo = useBundledAsset("logo.png");
|
|
1629
|
+
* const chime = useAudioPlayer(useBundledAsset("chime.mp3"));
|
|
1630
|
+
* return logo ? <Image source={{ uri: logo }} /> : null;
|
|
1631
|
+
* ```
|
|
1632
|
+
*/
|
|
1633
|
+
export function useBundledAsset(fileName: string | null | undefined): string | null;
|
|
1634
|
+
|
|
1617
1635
|
/**
|
|
1618
1636
|
* List every tenant asset carrying a given tag. Reads `ctx.assets.list({
|
|
1619
1637
|
* tag, type, limit })` and unwraps the `{ data, meta }` envelope.
|
|
@@ -3705,6 +3723,12 @@ export interface LintFinding {
|
|
|
3705
3723
|
}
|
|
3706
3724
|
export interface LintOptions {
|
|
3707
3725
|
manifest?: { requestedScopes?: string[]; supportedPlatforms?: string[] };
|
|
3726
|
+
/**
|
|
3727
|
+
* File names in the package's `assets/` directory. When given, a
|
|
3728
|
+
* `useBundledAsset("name")` literal naming a file outside the list is a
|
|
3729
|
+
* `bundled-asset-missing` error; omit it to skip that rule.
|
|
3730
|
+
*/
|
|
3731
|
+
assetFiles?: string[];
|
|
3708
3732
|
}
|
|
3709
3733
|
export function lintSource(
|
|
3710
3734
|
source: string,
|
|
@@ -3714,6 +3738,40 @@ export function lintSource(
|
|
|
3714
3738
|
findings: LintFinding[];
|
|
3715
3739
|
};
|
|
3716
3740
|
|
|
3741
|
+
// Bundled assets — the files a package ships in its `assets/` directory.
|
|
3742
|
+
export type BundledAssetKind = "image" | "audio";
|
|
3743
|
+
export type BundledAssetErrorCode =
|
|
3744
|
+
| "ASSET_NAME_INVALID"
|
|
3745
|
+
| "ASSET_TYPE_NOT_ALLOWED"
|
|
3746
|
+
| "ASSET_EMPTY"
|
|
3747
|
+
| "ASSET_CONTENT_MISMATCH"
|
|
3748
|
+
| "ASSET_NAME_DUPLICATE"
|
|
3749
|
+
| "ASSET_TOO_MANY";
|
|
3750
|
+
export interface BundledAssetError {
|
|
3751
|
+
code: BundledAssetErrorCode;
|
|
3752
|
+
/** `null` for an error about the directory as a whole (`ASSET_TOO_MANY`). */
|
|
3753
|
+
name: string | null;
|
|
3754
|
+
message: string;
|
|
3755
|
+
}
|
|
3756
|
+
export interface BundledAsset {
|
|
3757
|
+
name: string;
|
|
3758
|
+
kind: BundledAssetKind;
|
|
3759
|
+
mimeType: string;
|
|
3760
|
+
sizeBytes: number;
|
|
3761
|
+
}
|
|
3762
|
+
/** The package directory bundled files live in (`"assets"`); from the `./bundled-assets` entry. */
|
|
3763
|
+
export const BUNDLED_ASSET_DIRECTORY: string;
|
|
3764
|
+
/** True when `name` may be used as a file name inside `assets/`. */
|
|
3765
|
+
export function isValidBundledAssetName(name: unknown): boolean;
|
|
3766
|
+
/** Identifies a bundled file from its leading bytes; `null` when it is not an allowed type. */
|
|
3767
|
+
export function sniffBundledAssetType(
|
|
3768
|
+
bytes: Uint8Array,
|
|
3769
|
+
): { kind: BundledAssetKind; mimeType: string; extensions: string[] } | null;
|
|
3770
|
+
/** Validates the files of an `assets/` directory; `assets` is sorted by name. */
|
|
3771
|
+
export function validateBundledAssets(
|
|
3772
|
+
files: Array<{ name: string; bytes: Uint8Array }>,
|
|
3773
|
+
): { ok: boolean; errors: BundledAssetError[]; assets: BundledAsset[] };
|
|
3774
|
+
|
|
3717
3775
|
// --------------------------------------------------------------- CONTRACT
|
|
3718
3776
|
//
|
|
3719
3777
|
// Single-source-of-truth contract artefact. See docs/design/ai-widget-contract.md.
|
|
@@ -3768,6 +3826,17 @@ export interface AiWidgetContract {
|
|
|
3768
3826
|
readonly manifestSchema: Readonly<Record<string, ContractManifestField>>;
|
|
3769
3827
|
readonly manifestCategories: ReadonlyArray<string>;
|
|
3770
3828
|
readonly manifestPlatforms: ReadonlyArray<string>;
|
|
3829
|
+
/** Largest package a widget may ship, bundled files included. */
|
|
3830
|
+
readonly packageMaxBytes: number;
|
|
3831
|
+
/** What a package may ship in its `assets/` directory. */
|
|
3832
|
+
readonly bundledAssets: {
|
|
3833
|
+
readonly directory: string;
|
|
3834
|
+
readonly maxFiles: number;
|
|
3835
|
+
readonly maxNameLength: number;
|
|
3836
|
+
readonly types: Readonly<
|
|
3837
|
+
Record<BundledAssetKind, { readonly extensions: ReadonlyArray<string> }>
|
|
3838
|
+
>;
|
|
3839
|
+
};
|
|
3771
3840
|
readonly themeTokens: ThemeTokens;
|
|
3772
3841
|
/** sc-7800 — the brand colour tokens a `type: "color"` field may bind to. */
|
|
3773
3842
|
readonly themeColorTokens: ReadonlyArray<{ path: string; label: string }>;
|
package/dist/index.js
CHANGED
|
@@ -39,6 +39,7 @@ export {
|
|
|
39
39
|
useAgent,
|
|
40
40
|
useAsset,
|
|
41
41
|
useAssetsByTag,
|
|
42
|
+
useBundledAsset,
|
|
42
43
|
useFilestoreFiles,
|
|
43
44
|
useFilestoreFile,
|
|
44
45
|
useFilestoreUpload,
|
|
@@ -169,6 +170,11 @@ export {
|
|
|
169
170
|
Video,
|
|
170
171
|
} from "./primitives.js";
|
|
171
172
|
export { lintSource, bannedIdentifiers } from "./linter.js";
|
|
173
|
+
export {
|
|
174
|
+
validateBundledAssets,
|
|
175
|
+
sniffBundledAssetType,
|
|
176
|
+
isValidBundledAssetName,
|
|
177
|
+
} from "./bundled-assets.js";
|
|
172
178
|
// sc-3696: the trailing six are the theme-derivation helpers both hosts resolve
|
|
173
179
|
// `useTheme()` with (the Player through widgetTheme.js, the export through its
|
|
174
180
|
// generated theme module), plus the gradient angle projection `<Gradient>`
|
package/dist/index.native.js
CHANGED
|
@@ -39,6 +39,7 @@ export {
|
|
|
39
39
|
useAgent,
|
|
40
40
|
useAsset,
|
|
41
41
|
useAssetsByTag,
|
|
42
|
+
useBundledAsset,
|
|
42
43
|
useFilestoreFiles,
|
|
43
44
|
useFilestoreFile,
|
|
44
45
|
useFilestoreUpload,
|
|
@@ -162,6 +163,11 @@ export {
|
|
|
162
163
|
Video,
|
|
163
164
|
} from "./primitives.native.js";
|
|
164
165
|
export { lintSource, bannedIdentifiers } from "./linter.js";
|
|
166
|
+
export {
|
|
167
|
+
validateBundledAssets,
|
|
168
|
+
sniffBundledAssetType,
|
|
169
|
+
isValidBundledAssetName,
|
|
170
|
+
} from "./bundled-assets.js";
|
|
165
171
|
// sc-3696: the trailing six are the theme-derivation helpers both hosts resolve
|
|
166
172
|
// `useTheme()` with (the Player through widgetTheme.js, the export through its
|
|
167
173
|
// generated theme module), plus the gradient angle projection `<Gradient>`
|
package/dist/linter.cjs
CHANGED
|
@@ -1082,6 +1082,33 @@ function _jsxOpenTagEnd(source, from) {
|
|
|
1082
1082
|
return source.length;
|
|
1083
1083
|
}
|
|
1084
1084
|
|
|
1085
|
+
// sc-10721 — a useBundledAsset literal must name a file the package ships.
|
|
1086
|
+
const BUNDLED_ASSET_CALL_RE =
|
|
1087
|
+
/\buseBundledAsset\s*\(\s*(?:"([^"\n]*)"|'([^'\n]*)'|`([^`]*)`)\s*[,)]/g;
|
|
1088
|
+
|
|
1089
|
+
function _bundledAssetRules(source, assetFiles) {
|
|
1090
|
+
if (!Array.isArray(assetFiles)) return [];
|
|
1091
|
+
const findings = [];
|
|
1092
|
+
// Comments blanked, strings kept: the file name IS a string literal.
|
|
1093
|
+
const code = _stripNonCode(source, { keepStrings: true });
|
|
1094
|
+
const sourceLines = source.split(/\r?\n/);
|
|
1095
|
+
for (const match of code.matchAll(BUNDLED_ASSET_CALL_RE)) {
|
|
1096
|
+
const name = match[1] || match[2] || match[3] || "";
|
|
1097
|
+
if (name.includes("${") || assetFiles.includes(name)) continue;
|
|
1098
|
+
const line = code.slice(0, match.index).split(/\r?\n/).length;
|
|
1099
|
+
findings.push({
|
|
1100
|
+
rule: "bundled-asset-missing",
|
|
1101
|
+
severity: "error",
|
|
1102
|
+
label:
|
|
1103
|
+
`useBundledAsset("${name}") names a file that is not in the package's ` +
|
|
1104
|
+
"assets/ directory. Add the file there or correct the name.",
|
|
1105
|
+
line,
|
|
1106
|
+
snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
return findings;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1085
1112
|
// sc-4649 / sc-4686 — see linter.js for the rationale comments. The two
|
|
1086
1113
|
// files must stay in lockstep (a test asserts behaviour-equivalence).
|
|
1087
1114
|
const REQUEST_PAYMENT_CALL = "requestPayment(";
|
|
@@ -2015,6 +2042,7 @@ function lintSource(source, options) {
|
|
|
2015
2042
|
findings.push(..._translationApiRules(source));
|
|
2016
2043
|
findings.push(..._handBuiltPageUrlRules(source));
|
|
2017
2044
|
findings.push(..._lucideIconRules(source));
|
|
2045
|
+
findings.push(..._bundledAssetRules(source, options && options.assetFiles));
|
|
2018
2046
|
findings.push(..._selfContainedReferenceRules(source));
|
|
2019
2047
|
findings.push(..._imagePercentHeightRules(source));
|
|
2020
2048
|
// sc-7274 — soft warning: a percentage flexBasis sizes the main axis, so in a
|