animader-cli 0.0.0-stage → 0.1.1
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/LICENSE +121 -0
- package/adapters/build-node.mjs +265 -0
- package/adapters/devserver.mjs +481 -0
- package/adapters/devsession.mjs +619 -0
- package/adapters/export-node.mjs +603 -0
- package/adapters/export-run.mjs +126 -0
- package/adapters/generations.mjs +183 -0
- package/adapters/http.mjs +294 -0
- package/adapters/launch.mjs +83 -0
- package/adapters/node-fs.mjs +80 -0
- package/adapters/pngine.mjs +87 -0
- package/adapters/snapshot.mjs +143 -0
- package/adapters/target.mjs +104 -0
- package/adapters/watch.mjs +355 -0
- package/adapters/webaudio.mjs +102 -0
- package/bin/animader.mjs +31 -0
- package/hello/join.js +46 -0
- package/hello/run.js +259 -0
- package/lib/build.mjs +126 -0
- package/lib/buildloop.mjs +254 -0
- package/lib/contract.mjs +371 -0
- package/lib/errors.mjs +51 -0
- package/lib/export.mjs +445 -0
- package/lib/new.mjs +304 -0
- package/lib/pipeline.mjs +247 -0
- package/lib/receipt.mjs +331 -0
- package/lib/rmtree.mjs +49 -0
- package/lib/routes.mjs +170 -0
- package/lib/score.mjs +434 -0
- package/lib/show.mjs +458 -0
- package/lib/soundtrack.mjs +67 -0
- package/lib/vendor.mjs +99 -0
- package/lib/wav.mjs +197 -0
- package/package.json +47 -4
- package/player/boot.mjs +792 -0
- package/player/controller.mjs +366 -0
- package/player/devchannel.mjs +306 -0
- package/player/index.html +35 -0
- package/player/page.mjs +134 -0
- package/player/player.css +393 -0
- package/player/release.mjs +76 -0
- package/src/commands/build.mjs +201 -0
- package/src/commands/dev.mjs +214 -0
- package/src/commands/doctor.mjs +348 -0
- package/src/commands/export.mjs +158 -0
- package/src/commands/new.mjs +164 -0
- package/src/commands/project.mjs +46 -0
- package/src/config.mjs +110 -0
- package/src/router.mjs +149 -0
- package/src/toolchain.mjs +691 -0
- package/starters/circle/README.md +50 -0
- package/starters/circle/animader.json +4 -0
- package/starters/circle/gitignore +9 -0
- package/starters/circle/kernels/circle.wgsl +32 -0
- package/starters/circle/program.sjon +34 -0
- package/starters/circle/timeline.piece.sjon +26 -0
- package/starters/ember/bake/spans.bin +0 -0
- package/starters/ember/kernels/curl.wgsl +10 -0
- package/starters/ember/kernels/ember.wgsl +10 -0
- package/starters/ember/kernels/hash.wgsl +12 -0
- package/starters/ember/kernels/simplex.wgsl +27 -0
- package/starters/ember/program.sjon +328 -0
- package/starters/ember/timeline.piece.sjon +34 -0
- package/toolchain/licenses/animader.txt +121 -0
- package/toolchain/licenses/esbuild.txt +21 -0
- package/toolchain/licenses/pacer.txt +121 -0
- package/toolchain/licenses/pngine.txt +121 -0
- package/toolchain/manifest.json +212 -0
- package/toolchain/prepare.mjs +446 -0
- package/toolchain/uniform-type-name.js +184 -0
- package/README.md +0 -3
package/lib/contract.mjs
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// The demo's project contract and CLI argument grammar. This module is
|
|
2
|
+
// portable: it takes bytes and paths through injected functions and parses
|
|
3
|
+
// no guest language. The exact folder names are data, not a second reader
|
|
4
|
+
// of SJON; `pngine validate` and the payload bake are the program checks.
|
|
5
|
+
//
|
|
6
|
+
// Ownership: project input shape, host duration/port bounds, argument
|
|
7
|
+
// decisions. IO and native execution belong to host adapters.
|
|
8
|
+
|
|
9
|
+
import { projectError, usageError } from './errors.mjs';
|
|
10
|
+
import { SOUNDTRACK_NAMES, soundtrackNamesText } from './soundtrack.mjs';
|
|
11
|
+
|
|
12
|
+
export { SOUNDTRACK_NAMES, isSoundtrackName } from './soundtrack.mjs';
|
|
13
|
+
|
|
14
|
+
export const PROJECT_FILES = Object.freeze({
|
|
15
|
+
program: 'program.sjon',
|
|
16
|
+
timeline: 'timeline.piece.sjon',
|
|
17
|
+
soundtrack: 'soundtrack.wav',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const GENERATED_DIR = 'out';
|
|
21
|
+
export const BAKED_PAYLOAD = 'show.png';
|
|
22
|
+
export const PACKED_SCORE = 'score.pacb';
|
|
23
|
+
export const BUILD_RECEIPT = 'build-receipt.json';
|
|
24
|
+
export const SPLIT_PIECE = 'piece.sjon';
|
|
25
|
+
|
|
26
|
+
// A show is a host policy, not a pacer transport field. The upper bound is
|
|
27
|
+
// the demo's own known range; the lower bound is merely positive.
|
|
28
|
+
export const MAX_SECONDS = 600;
|
|
29
|
+
|
|
30
|
+
export const DEFAULT_LIMITS = Object.freeze({
|
|
31
|
+
program: 16 * 1024 * 1024,
|
|
32
|
+
timeline: 16 * 1024 * 1024,
|
|
33
|
+
soundtrack: 64 * 1024 * 1024,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const RANGE_WORDS = `a positive number at most ${MAX_SECONDS}`;
|
|
37
|
+
|
|
38
|
+
function inFolder(folder, name) {
|
|
39
|
+
return `${String(folder).replace(/[\\/]+$/, '')}/${name}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function folderLabel(folder) {
|
|
43
|
+
return folder === '' ? 'the current folder' : folder;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function parseSeconds(raw) {
|
|
47
|
+
if (raw === undefined) {
|
|
48
|
+
throw usageError(
|
|
49
|
+
'missing_seconds',
|
|
50
|
+
`--seconds is required and must be ${RANGE_WORDS}`,
|
|
51
|
+
'Pass the host show length, for example: node demo/show.mjs <folder> --seconds 8',
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const text = typeof raw === 'number' ? String(raw) : String(raw).trim();
|
|
55
|
+
const value = Number(text);
|
|
56
|
+
if (text === '' || !Number.isFinite(value) || value <= 0 || value > MAX_SECONDS) {
|
|
57
|
+
throw usageError(
|
|
58
|
+
'invalid_seconds',
|
|
59
|
+
`--seconds must be ${RANGE_WORDS}; got ${JSON.stringify(String(raw))}`,
|
|
60
|
+
'Pass a positive finite host show length, for example --seconds 8',
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function parsePort(raw) {
|
|
67
|
+
if (raw === undefined) return 0;
|
|
68
|
+
const text = String(raw).trim();
|
|
69
|
+
if (!/^\d+$/.test(text)) {
|
|
70
|
+
throw usageError(
|
|
71
|
+
'invalid_port',
|
|
72
|
+
`--port must be an integer from 0 through 65535; got ${JSON.stringify(String(raw))}`,
|
|
73
|
+
'Use --port 0 to let the operating system choose a free port',
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const value = Number(text);
|
|
77
|
+
if (value > 65535) {
|
|
78
|
+
throw usageError(
|
|
79
|
+
'invalid_port',
|
|
80
|
+
`--port must be an integer from 0 through 65535; got ${text}`,
|
|
81
|
+
'Use --port 0 to let the operating system choose a free port',
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function parseArgv(argv) {
|
|
88
|
+
if (!Array.isArray(argv)) {
|
|
89
|
+
throw usageError('invalid_argv', 'the CLI adapter must pass an argument array', 'Call node demo/show.mjs <folder> --seconds <n>');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let folder = null;
|
|
93
|
+
let secondsRaw;
|
|
94
|
+
let portRaw;
|
|
95
|
+
let open = true;
|
|
96
|
+
let preflightOnly = false;
|
|
97
|
+
let help = false;
|
|
98
|
+
let endOptions = false;
|
|
99
|
+
|
|
100
|
+
for (let index = 0; index < argv.length; index++) {
|
|
101
|
+
const argument = String(argv[index]);
|
|
102
|
+
if (!endOptions && argument === '--') {
|
|
103
|
+
endOptions = true;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (!endOptions && (argument === '--help' || argument === '-h')) {
|
|
107
|
+
help = true;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!endOptions && argument.startsWith('--')) {
|
|
111
|
+
const equals = argument.indexOf('=');
|
|
112
|
+
const name = equals === -1 ? argument : argument.slice(0, equals);
|
|
113
|
+
const inline = equals === -1 ? undefined : argument.slice(equals + 1);
|
|
114
|
+
const takeValue = () => {
|
|
115
|
+
if (inline !== undefined) return inline;
|
|
116
|
+
if (index + 1 >= argv.length) {
|
|
117
|
+
throw usageError('missing_option_value', `${name} needs a value`, `Pass ${name} <value>`);
|
|
118
|
+
}
|
|
119
|
+
index++;
|
|
120
|
+
return String(argv[index]);
|
|
121
|
+
};
|
|
122
|
+
if (name === '--seconds') {
|
|
123
|
+
secondsRaw = takeValue();
|
|
124
|
+
} else if (name === '--port') {
|
|
125
|
+
portRaw = takeValue();
|
|
126
|
+
} else if (name === '--no-open') {
|
|
127
|
+
if (inline !== undefined) {
|
|
128
|
+
throw usageError('unexpected_option_value', '--no-open takes no value', 'Use --no-open by itself');
|
|
129
|
+
}
|
|
130
|
+
open = false;
|
|
131
|
+
} else if (name === '--preflight-only') {
|
|
132
|
+
if (inline !== undefined) {
|
|
133
|
+
throw usageError('unexpected_option_value', '--preflight-only takes no value', 'Use --preflight-only by itself');
|
|
134
|
+
}
|
|
135
|
+
preflightOnly = true;
|
|
136
|
+
} else {
|
|
137
|
+
throw usageError(
|
|
138
|
+
'unknown_option',
|
|
139
|
+
`unknown option ${name}`,
|
|
140
|
+
'The supported options are --seconds, --port, --no-open, --preflight-only and --help',
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (!endOptions && argument.startsWith('-') && argument !== '-') {
|
|
146
|
+
throw usageError(
|
|
147
|
+
'unknown_option',
|
|
148
|
+
`unknown option ${argument}`,
|
|
149
|
+
'The supported options are --seconds, --port, --no-open, --preflight-only and --help',
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (folder !== null) {
|
|
153
|
+
throw usageError('extra_argument', `extra folder argument ${JSON.stringify(argument)}`, 'Pass exactly one project folder');
|
|
154
|
+
}
|
|
155
|
+
folder = argument;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (help) {
|
|
159
|
+
return { help: true, folder, seconds: undefined, port: 0, open, preflightOnly };
|
|
160
|
+
}
|
|
161
|
+
if (folder === null) {
|
|
162
|
+
throw usageError(
|
|
163
|
+
'missing_folder',
|
|
164
|
+
'exactly one project folder is required',
|
|
165
|
+
'Run: node demo/show.mjs <folder> --seconds 8 [--port N] [--no-open]',
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
help: false,
|
|
171
|
+
folder,
|
|
172
|
+
seconds: parseSeconds(secondsRaw),
|
|
173
|
+
port: parsePort(portRaw),
|
|
174
|
+
open,
|
|
175
|
+
preflightOnly,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function usageText() {
|
|
180
|
+
return [
|
|
181
|
+
'animader demo',
|
|
182
|
+
'',
|
|
183
|
+
'Usage:',
|
|
184
|
+
' node demo/show.mjs <folder> --seconds S [--port N] [--no-open]',
|
|
185
|
+
'',
|
|
186
|
+
'The folder carries exactly:',
|
|
187
|
+
` ${PROJECT_FILES.program}`,
|
|
188
|
+
` ${PROJECT_FILES.timeline} (inline-roots aggregate; a split ${SPLIT_PIECE} is refused)`,
|
|
189
|
+
` exactly one of ${soundtrackNamesText()}`,
|
|
190
|
+
' (.wav is browser-decodable WAV; .ogg is Ogg Vorbis or Ogg Opus;',
|
|
191
|
+
' .opus is Ogg Opus — the browser decodes the actual bytes)',
|
|
192
|
+
' plus any relative assets the program names.',
|
|
193
|
+
'',
|
|
194
|
+
'Options:',
|
|
195
|
+
` --seconds S required host show length, ${RANGE_WORDS}`,
|
|
196
|
+
' --port N 127.0.0.1 port; 0 (the default) chooses a free one',
|
|
197
|
+
' --no-open do not attempt to launch Chromium',
|
|
198
|
+
' --preflight-only validate and bake, print the receipt, then exit',
|
|
199
|
+
' -h, --help this text',
|
|
200
|
+
'',
|
|
201
|
+
`Generated: ${GENERATED_DIR}/${BAKED_PAYLOAD} and ${GENERATED_DIR}/${BUILD_RECEIPT}`,
|
|
202
|
+
].join('\n');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function requireFile(statFile, path, name, seat, limits, folder) {
|
|
206
|
+
const stats = await statFile(path);
|
|
207
|
+
if (stats === null) {
|
|
208
|
+
throw projectError(
|
|
209
|
+
'missing_input',
|
|
210
|
+
`${name} is missing in ${folderLabel(folder)}`,
|
|
211
|
+
`Place ${name} in the project folder. The fixed inputs are ${PROJECT_FILES.program} and ${PROJECT_FILES.timeline}, plus one of ${soundtrackNamesText()}.`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (!stats.isFile) {
|
|
215
|
+
throw projectError(
|
|
216
|
+
'input_not_file',
|
|
217
|
+
`${name} is not a regular file in ${folderLabel(folder)}`,
|
|
218
|
+
`Replace ${name} with a file.`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (stats.size === 0) {
|
|
222
|
+
throw projectError(
|
|
223
|
+
'empty_input',
|
|
224
|
+
`${name} is empty in ${folderLabel(folder)}`,
|
|
225
|
+
`Write ${name} or regenerate the fixture with: node demo/prepare.mjs`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (stats.size > limits[seat]) {
|
|
229
|
+
throw projectError(
|
|
230
|
+
'input_too_large',
|
|
231
|
+
`${name} is ${stats.size} bytes; the demo limit is ${limits[seat]} bytes`,
|
|
232
|
+
`Use a smaller ${name}; the limit is a preflight guard, not a format restriction.`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return stats;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The soundtrack seat's candidacy, decided on names exactly as the folder
|
|
240
|
+
* lists them: an entry is a candidate only under its exact spelling, never
|
|
241
|
+
* under what a case-folding filesystem (APFS is case-insensitive by
|
|
242
|
+
* default) resolves a fixed probe to. The allowlist order is stable.
|
|
243
|
+
*
|
|
244
|
+
* @param {string[]} entries the folder's own entry names
|
|
245
|
+
* @returns {string[]} the recognized candidates, in allowlist order
|
|
246
|
+
*/
|
|
247
|
+
export function soundtrackCandidates(entries) {
|
|
248
|
+
const listed = new Set(entries);
|
|
249
|
+
return SOUNDTRACK_NAMES.filter((name) => listed.has(name));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Resolve the soundtrack seat over the shared three-name allowlist. The
|
|
254
|
+
* folder is read once and candidacy comes from its listing by exact
|
|
255
|
+
* string equality; every candidate is then checked for file shape, size
|
|
256
|
+
* and emptiness: a directory or empty file with a recognized name is an
|
|
257
|
+
* input error and is never ignored in favor of another candidate. Among
|
|
258
|
+
* valid candidates exactly one must remain; the error names what is
|
|
259
|
+
* present and what is supported.
|
|
260
|
+
*/
|
|
261
|
+
async function resolveSoundtrack({ folder, io, limit }) {
|
|
262
|
+
const candidates = soundtrackCandidates(await io.listDir(folder));
|
|
263
|
+
const present = [];
|
|
264
|
+
for (const name of candidates) {
|
|
265
|
+
const stats = await io.statFile(inFolder(folder, name));
|
|
266
|
+
if (stats === null) continue;
|
|
267
|
+
if (!stats.isFile) {
|
|
268
|
+
throw projectError(
|
|
269
|
+
'input_not_file',
|
|
270
|
+
`${name} is not a regular file in ${folderLabel(folder)}`,
|
|
271
|
+
`Replace ${name} with a regular file; a directory with a recognized soundtrack name is an input error, not a skippable candidate.`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
if (stats.size === 0) {
|
|
275
|
+
throw projectError(
|
|
276
|
+
'empty_input',
|
|
277
|
+
`${name} is empty in ${folderLabel(folder)}`,
|
|
278
|
+
`Write audio bytes to ${name} or move it out of the project folder; exactly one recognized soundtrack is required.`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
if (stats.size > limit) {
|
|
282
|
+
throw projectError(
|
|
283
|
+
'input_too_large',
|
|
284
|
+
`${name} is ${stats.size} bytes; the demo limit is ${limit} bytes`,
|
|
285
|
+
`Use a smaller ${name}; the limit is a preflight guard, not a format restriction.`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
present.push(name);
|
|
289
|
+
}
|
|
290
|
+
if (present.length === 0) {
|
|
291
|
+
throw projectError(
|
|
292
|
+
'missing_input',
|
|
293
|
+
`no recognized soundtrack is present in ${folderLabel(folder)}: expected exactly one of ${soundtrackNamesText()}`,
|
|
294
|
+
`Place exactly one of ${soundtrackNamesText()} in the project folder; the browser decodes the actual bytes, not the extension.`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
if (present.length > 1) {
|
|
298
|
+
throw projectError(
|
|
299
|
+
'ambiguous_soundtrack',
|
|
300
|
+
`${present.length} recognized soundtracks are present in ${folderLabel(folder)}: ${present.join(', ')}; exactly one is required`,
|
|
301
|
+
`Keep exactly one of ${soundtrackNamesText()} and move or rename the rest outside the project root; the host never deletes author audio.`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
const name = present[0];
|
|
305
|
+
const bytes = await io.readFile(inFolder(folder, name));
|
|
306
|
+
return { name, bytes: bytes.byteLength, sha256: io.hashBytes(bytes) };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Inspect one project folder against the exact-name contract.
|
|
311
|
+
*
|
|
312
|
+
* @param {Object} options
|
|
313
|
+
* @param {string} options.folder
|
|
314
|
+
* @param {Object} options.io
|
|
315
|
+
* @param {(path: string) => Promise<null|{isFile: boolean, isDirectory: boolean, size: number}>} options.io.statFile
|
|
316
|
+
* @param {(path: string) => Promise<string[]>} options.io.listDir the folder's
|
|
317
|
+
* own entry names — the soundtrack seat's candidacy is decided on these,
|
|
318
|
+
* never on what a case-folding stat resolves
|
|
319
|
+
* @param {(path: string) => Promise<Uint8Array>} options.io.readFile
|
|
320
|
+
* @param {(bytes: Uint8Array) => string} options.io.hashBytes
|
|
321
|
+
* @param {Object} [options.limits]
|
|
322
|
+
* @returns {Promise<{folder: string, files: Object<string, {name: string, bytes: number, sha256: string}>}>}
|
|
323
|
+
*/
|
|
324
|
+
export async function inspectProject({ folder, io, limits }) {
|
|
325
|
+
if (typeof folder !== 'string' || folder === '') {
|
|
326
|
+
throw projectError('missing_folder', 'the project folder is empty', 'Pass the folder that holds the three exact input names.');
|
|
327
|
+
}
|
|
328
|
+
if (!io
|
|
329
|
+
|| typeof io.statFile !== 'function'
|
|
330
|
+
|| typeof io.readFile !== 'function'
|
|
331
|
+
|| typeof io.hashBytes !== 'function'
|
|
332
|
+
|| typeof io.listDir !== 'function') {
|
|
333
|
+
throw new TypeError('inspectProject needs io.statFile, io.listDir, io.readFile and io.hashBytes');
|
|
334
|
+
}
|
|
335
|
+
const effectiveLimits = limits ?? io.limits ?? DEFAULT_LIMITS;
|
|
336
|
+
|
|
337
|
+
const splitPath = inFolder(folder, SPLIT_PIECE);
|
|
338
|
+
const split = await io.statFile(splitPath);
|
|
339
|
+
if (split !== null) {
|
|
340
|
+
throw projectError(
|
|
341
|
+
'split_piece_unsupported',
|
|
342
|
+
`${SPLIT_PIECE} is a split piece and this demo has one derived seat`,
|
|
343
|
+
`Use ${PROJECT_FILES.timeline} with tempura's roots inline instead; a split piece has no authored fallback here.`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const outStats = await io.statFile(inFolder(folder, GENERATED_DIR));
|
|
348
|
+
if (outStats !== null && !outStats.isDirectory) {
|
|
349
|
+
throw projectError(
|
|
350
|
+
'out_not_directory',
|
|
351
|
+
`${GENERATED_DIR}/ exists but is not a directory`,
|
|
352
|
+
`${GENERATED_DIR}/ is generated output; move the file out of the way so the demo can create it.`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const files = {};
|
|
357
|
+
for (const seat of ['program', 'timeline']) {
|
|
358
|
+
const name = PROJECT_FILES[seat];
|
|
359
|
+
const path = inFolder(folder, name);
|
|
360
|
+
await requireFile(io.statFile, path, name, seat, effectiveLimits, folder);
|
|
361
|
+
const bytes = await io.readFile(path);
|
|
362
|
+
files[seat] = {
|
|
363
|
+
name,
|
|
364
|
+
bytes: bytes.byteLength,
|
|
365
|
+
sha256: io.hashBytes(bytes),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
files.soundtrack = await resolveSoundtrack({ folder, io, limit: effectiveLimits.soundtrack });
|
|
369
|
+
|
|
370
|
+
return { folder, files };
|
|
371
|
+
}
|
package/lib/errors.mjs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Named failures are the CLI's public vocabulary. This module is portable:
|
|
2
|
+
// it carries no `node:` import, no process state and no DOM. Host adapters
|
|
3
|
+
// add paths and causes; the decision and the exit class stay here.
|
|
4
|
+
|
|
5
|
+
export const EXIT_CODES = Object.freeze({
|
|
6
|
+
usage: 2,
|
|
7
|
+
project: 3,
|
|
8
|
+
build: 4,
|
|
9
|
+
server: 5,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export class DemoError extends Error {
|
|
13
|
+
constructor(code, message, { exitCode = 1, remedy = '', stage = '', cause } = {}) {
|
|
14
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
15
|
+
this.name = 'DemoError';
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.exitCode = exitCode;
|
|
18
|
+
this.remedy = remedy;
|
|
19
|
+
this.stage = stage;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function usageError(code, message, remedy = '') {
|
|
24
|
+
return new DemoError(code, message, { exitCode: EXIT_CODES.usage, remedy });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function projectError(code, message, remedy = '') {
|
|
28
|
+
return new DemoError(code, message, { exitCode: EXIT_CODES.project, remedy });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildError(code, message, remedy = '') {
|
|
32
|
+
return new DemoError(code, message, { exitCode: EXIT_CODES.build, remedy });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function serverError(code, message, remedy = '') {
|
|
36
|
+
return new DemoError(code, message, { exitCode: EXIT_CODES.server, remedy });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The exact stderr lines every demo entry point prints for a thrown error.
|
|
41
|
+
* The `animader` CLI bin shares this helper so the format has one owner;
|
|
42
|
+
* adding an export here changes no existing demo behaviour.
|
|
43
|
+
*
|
|
44
|
+
* @param {unknown} error
|
|
45
|
+
* @returns {string[]}
|
|
46
|
+
*/
|
|
47
|
+
export function errorLines(error) {
|
|
48
|
+
const lines = [`error: ${error.message}`];
|
|
49
|
+
if (error?.remedy) lines.push(`remedy: ${error.remedy}`);
|
|
50
|
+
return lines;
|
|
51
|
+
}
|