@colixsystems/widget-sdk 0.197.0 → 0.199.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 +148 -3
- package/dist/avatar-color.js +35 -0
- 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 +225 -26
- package/dist/contract.js +225 -26
- package/dist/detail-parts-view.js +9 -4
- package/dist/devserver.js +68 -10
- package/dist/hooks.js +549 -28
- package/dist/index.d.ts +247 -5
- package/dist/index.js +14 -0
- package/dist/index.native.js +14 -0
- package/dist/initials.js +3 -2
- package/dist/linter.cjs +28 -0
- package/dist/linter.js +28 -0
- package/dist/manifest.cjs +3 -1
- package/dist/manifest.js +3 -1
- package/dist/overlay-view.js +2 -5
- package/dist/overlay.js +12 -5
- package/dist/primitives.js +2 -0
- package/dist/primitives.native.js +2 -0
- package/dist/video.js +51 -0
- package/dist/video.native.js +61 -0
- package/dist/web-modal.js +139 -0
- package/package.json +11 -1
- package/src/contract.js +225 -26
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// The files a widget package ships in its assets/ directory: which names and
|
|
2
|
+
// types are allowed. Pure and filesystem-free, so the CLI, the dev server, the
|
|
3
|
+
// first-party packer and the backend all apply one rule set.
|
|
4
|
+
// Keep bundled-assets.cjs in lockstep.
|
|
5
|
+
|
|
6
|
+
import { CONTRACT } from "./contract.js";
|
|
7
|
+
|
|
8
|
+
const RULES = CONTRACT.bundledAssets;
|
|
9
|
+
const BUNDLED_ASSET_DIRECTORY = RULES.directory;
|
|
10
|
+
const ALLOWED_EXTENSIONS = Object.values(RULES.types).flatMap(
|
|
11
|
+
(type) => type.extensions,
|
|
12
|
+
);
|
|
13
|
+
const NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
14
|
+
|
|
15
|
+
const PNG = { kind: "image", mimeType: "image/png", extensions: ["png"] };
|
|
16
|
+
const JPEG = { kind: "image", mimeType: "image/jpeg", extensions: ["jpg", "jpeg"] };
|
|
17
|
+
const GIF = { kind: "image", mimeType: "image/gif", extensions: ["gif"] };
|
|
18
|
+
const WEBP = { kind: "image", mimeType: "image/webp", extensions: ["webp"] };
|
|
19
|
+
const WAV = { kind: "audio", mimeType: "audio/wav", extensions: ["wav"] };
|
|
20
|
+
const MP3 = { kind: "audio", mimeType: "audio/mpeg", extensions: ["mp3"] };
|
|
21
|
+
const M4A = { kind: "audio", mimeType: "audio/mp4", extensions: ["m4a"] };
|
|
22
|
+
|
|
23
|
+
const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
24
|
+
const JPEG_SIGNATURE = [0xff, 0xd8, 0xff];
|
|
25
|
+
const M4A_BRANDS = ["M4A ", "M4B ", "mp42", "isom", "mp41"];
|
|
26
|
+
|
|
27
|
+
function _hasBytes(bytes, signature, offset) {
|
|
28
|
+
if (bytes.length < offset + signature.length) return false;
|
|
29
|
+
return signature.every((byte, i) => bytes[offset + i] === byte);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function _hasText(bytes, text, offset) {
|
|
33
|
+
return _hasBytes(bytes, Array.from(text, (ch) => ch.charCodeAt(0)), offset);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// kbps by bitrate index; index 0 (free format) and 15 are never accepted.
|
|
37
|
+
const MPEG_BITRATES = {
|
|
38
|
+
v1Layer1: [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448],
|
|
39
|
+
v1Layer2: [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384],
|
|
40
|
+
v1Layer3: [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320],
|
|
41
|
+
v2Layer1: [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256],
|
|
42
|
+
v2Layer2or3: [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160],
|
|
43
|
+
};
|
|
44
|
+
// Hz by the header's version bits (3 = MPEG-1, 2 = MPEG-2, 0 = MPEG-2.5).
|
|
45
|
+
const MPEG_SAMPLE_RATES = {
|
|
46
|
+
3: [44100, 48000, 32000],
|
|
47
|
+
2: [22050, 24000, 16000],
|
|
48
|
+
0: [11025, 12000, 8000],
|
|
49
|
+
};
|
|
50
|
+
const MPEG_LAYER_1 = 3;
|
|
51
|
+
const MPEG_LAYER_3 = 1;
|
|
52
|
+
|
|
53
|
+
function _mpegBitrateTable(isVersion1, layer) {
|
|
54
|
+
if (layer === MPEG_LAYER_1) return isVersion1 ? MPEG_BITRATES.v1Layer1 : MPEG_BITRATES.v2Layer1;
|
|
55
|
+
if (!isVersion1) return MPEG_BITRATES.v2Layer2or3;
|
|
56
|
+
return layer === MPEG_LAYER_3 ? MPEG_BITRATES.v1Layer3 : MPEG_BITRATES.v1Layer2;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Byte length of the MPEG audio frame whose header starts at `offset`, or 0. */
|
|
60
|
+
function _mpegFrameLength(bytes, offset) {
|
|
61
|
+
if (bytes.length < offset + 4) return 0;
|
|
62
|
+
if (bytes[offset] !== 0xff || (bytes[offset + 1] & 0xe0) !== 0xe0) return 0;
|
|
63
|
+
const version = (bytes[offset + 1] >> 3) & 3;
|
|
64
|
+
const layer = (bytes[offset + 1] >> 1) & 3;
|
|
65
|
+
const bitrateIndex = bytes[offset + 2] >> 4;
|
|
66
|
+
const sampleRateIndex = (bytes[offset + 2] >> 2) & 3;
|
|
67
|
+
const padding = (bytes[offset + 2] >> 1) & 1;
|
|
68
|
+
if (version === 1 || layer === 0 || sampleRateIndex === 3) return 0;
|
|
69
|
+
if (bitrateIndex === 0 || bitrateIndex === 15) return 0;
|
|
70
|
+
const isVersion1 = version === 3;
|
|
71
|
+
const bitrate = _mpegBitrateTable(isVersion1, layer)[bitrateIndex] * 1000;
|
|
72
|
+
const sampleRate = MPEG_SAMPLE_RATES[version][sampleRateIndex];
|
|
73
|
+
if (layer === MPEG_LAYER_1) return (Math.floor((12 * bitrate) / sampleRate) + padding) * 4;
|
|
74
|
+
const samplesPerByte = layer === MPEG_LAYER_3 && !isVersion1 ? 72 : 144;
|
|
75
|
+
return Math.floor((samplesPerByte * bitrate) / sampleRate) + padding;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// A two-byte sync alone matches a UTF-16 BOM, so a second frame must follow.
|
|
79
|
+
function _isMpegAudioAt(bytes, offset) {
|
|
80
|
+
const length = _mpegFrameLength(bytes, offset);
|
|
81
|
+
if (length === 0) return false;
|
|
82
|
+
const next = offset + length;
|
|
83
|
+
return next === bytes.length || _mpegFrameLength(bytes, next) > 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const ID3_HEADER_BYTES = 10;
|
|
87
|
+
|
|
88
|
+
function _isMp3(bytes) {
|
|
89
|
+
if (!_hasText(bytes, "ID3", 0)) return _isMpegAudioAt(bytes, 0);
|
|
90
|
+
if (bytes.length < ID3_HEADER_BYTES || bytes[3] === 0xff || bytes[4] === 0xff) return false;
|
|
91
|
+
const size = [bytes[6], bytes[7], bytes[8], bytes[9]];
|
|
92
|
+
if (size.some((byte) => byte >= 0x80)) return false;
|
|
93
|
+
const tagBytes = (size[0] << 21) | (size[1] << 14) | (size[2] << 7) | size[3];
|
|
94
|
+
const footerBytes = bytes[5] & 0x10 ? ID3_HEADER_BYTES : 0;
|
|
95
|
+
return _isMpegAudioAt(bytes, ID3_HEADER_BYTES + tagBytes + footerBytes);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function _describe(type) {
|
|
99
|
+
return { kind: type.kind, mimeType: type.mimeType, extensions: [...type.extensions] };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function _sniff(bytes) {
|
|
103
|
+
if (_hasBytes(bytes, PNG_SIGNATURE, 0)) return PNG;
|
|
104
|
+
if (_hasBytes(bytes, JPEG_SIGNATURE, 0)) return JPEG;
|
|
105
|
+
if (_hasText(bytes, "GIF87a", 0) || _hasText(bytes, "GIF89a", 0)) return GIF;
|
|
106
|
+
if (_hasText(bytes, "RIFF", 0)) {
|
|
107
|
+
if (_hasText(bytes, "WEBP", 8)) return WEBP;
|
|
108
|
+
if (_hasText(bytes, "WAVE", 8)) return WAV;
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
if (_hasText(bytes, "ftyp", 4)) {
|
|
112
|
+
return M4A_BRANDS.some((brand) => _hasText(bytes, brand, 8)) ? M4A : null;
|
|
113
|
+
}
|
|
114
|
+
if (_isMp3(bytes)) return MP3;
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Identify a bundled file from its leading bytes alone.
|
|
120
|
+
* Returns `{ kind: "image" | "audio", mimeType, extensions }`, or `null` when
|
|
121
|
+
* the bytes are not one of the bundled types.
|
|
122
|
+
*/
|
|
123
|
+
function sniffBundledAssetType(bytes) {
|
|
124
|
+
if (!bytes || typeof bytes.length !== "number") return null;
|
|
125
|
+
const type = _sniff(bytes);
|
|
126
|
+
return type ? _describe(type) : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function _extensionOf(name) {
|
|
130
|
+
const dot = name.lastIndexOf(".");
|
|
131
|
+
return dot === -1 ? "" : name.slice(dot + 1).toLowerCase();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function _isWellFormedName(name) {
|
|
135
|
+
return (
|
|
136
|
+
typeof name === "string" &&
|
|
137
|
+
name.length <= RULES.maxNameLength &&
|
|
138
|
+
NAME_RE.test(name) &&
|
|
139
|
+
!name.includes("..")
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** True when `name` may be used as a file name inside a package's assets/. */
|
|
144
|
+
function isValidBundledAssetName(name) {
|
|
145
|
+
return _isWellFormedName(name) && ALLOWED_EXTENSIONS.includes(_extensionOf(name));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function _extensionList() {
|
|
149
|
+
const last = ALLOWED_EXTENSIONS[ALLOWED_EXTENSIONS.length - 1];
|
|
150
|
+
return `${ALLOWED_EXTENSIONS.slice(0, -1).join(", ")} and ${last}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function _error(code, name, rule) {
|
|
154
|
+
const where = `${BUNDLED_ASSET_DIRECTORY}/${name === null ? "" : name}`;
|
|
155
|
+
return { code, name, message: `${where}: ${rule}` };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function _fileError(file, seenNames) {
|
|
159
|
+
const { name, bytes } = file;
|
|
160
|
+
if (!_isWellFormedName(name)) {
|
|
161
|
+
return _error(
|
|
162
|
+
"ASSET_NAME_INVALID",
|
|
163
|
+
typeof name === "string" ? name : String(name),
|
|
164
|
+
"a bundled file name starts with a letter or digit, uses only letters, " +
|
|
165
|
+
`digits, dots, hyphens and underscores, and is at most ${RULES.maxNameLength} ` +
|
|
166
|
+
"characters long. Subdirectories are not allowed.",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const extension = _extensionOf(name);
|
|
170
|
+
if (!ALLOWED_EXTENSIONS.includes(extension)) {
|
|
171
|
+
return _error(
|
|
172
|
+
"ASSET_TYPE_NOT_ALLOWED",
|
|
173
|
+
name,
|
|
174
|
+
`only ${_extensionList()} files can be bundled.`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (seenNames.has(name.toLowerCase())) {
|
|
178
|
+
return _error(
|
|
179
|
+
"ASSET_NAME_DUPLICATE",
|
|
180
|
+
name,
|
|
181
|
+
"another bundled file has the same name when upper and lower case are ignored.",
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
seenNames.add(name.toLowerCase());
|
|
185
|
+
if (!bytes || !bytes.length) {
|
|
186
|
+
return _error("ASSET_EMPTY", name, "the file is empty.");
|
|
187
|
+
}
|
|
188
|
+
const sniffed = _sniff(bytes);
|
|
189
|
+
if (!sniffed || !sniffed.extensions.includes(extension)) {
|
|
190
|
+
return _error(
|
|
191
|
+
"ASSET_CONTENT_MISMATCH",
|
|
192
|
+
name,
|
|
193
|
+
`the file's content is not a valid .${extension} file.`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Validate the files of a package's assets/ directory.
|
|
201
|
+
* `files` is `[{ name, bytes }]`. Returns `{ ok, errors, assets }` where each
|
|
202
|
+
* error is `{ code, name, message }` and each accepted asset is
|
|
203
|
+
* `{ name, kind, mimeType, sizeBytes }`, sorted by name.
|
|
204
|
+
* Error codes: ASSET_NAME_INVALID, ASSET_TYPE_NOT_ALLOWED, ASSET_EMPTY,
|
|
205
|
+
* ASSET_CONTENT_MISMATCH, ASSET_NAME_DUPLICATE, ASSET_TOO_MANY.
|
|
206
|
+
*/
|
|
207
|
+
function validateBundledAssets(files) {
|
|
208
|
+
const list = Array.isArray(files) ? files : [];
|
|
209
|
+
const errors = [];
|
|
210
|
+
const assets = [];
|
|
211
|
+
const seenNames = new Set();
|
|
212
|
+
if (list.length > RULES.maxFiles) {
|
|
213
|
+
errors.push(
|
|
214
|
+
_error(
|
|
215
|
+
"ASSET_TOO_MANY",
|
|
216
|
+
null,
|
|
217
|
+
`a package can bundle at most ${RULES.maxFiles} files; this one has ${list.length}.`,
|
|
218
|
+
),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
for (const file of list) {
|
|
222
|
+
const error = _fileError(file || {}, seenNames);
|
|
223
|
+
if (error) {
|
|
224
|
+
errors.push(error);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const { kind, mimeType } = _sniff(file.bytes);
|
|
228
|
+
assets.push({ name: file.name, kind, mimeType, sizeBytes: file.bytes.length });
|
|
229
|
+
}
|
|
230
|
+
assets.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
231
|
+
return { ok: errors.length === 0, errors, assets };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export {
|
|
235
|
+
BUNDLED_ASSET_DIRECTORY,
|
|
236
|
+
isValidBundledAssetName,
|
|
237
|
+
sniffBundledAssetType,
|
|
238
|
+
validateBundledAssets,
|
|
239
|
+
};
|
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` +
|