@triflare/fx 0.0.0-stage → 1.0.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/LICENSE +201 -0
- package/README.md +73 -2
- package/bin/fx.js +949 -0
- package/package.json +64 -4
- package/product.yml +28 -0
- package/src/brand.js +105 -0
- package/src/build.js +181 -0
- package/src/catalog.js +753 -0
- package/src/compiler.js +1658 -0
- package/src/config-edit.js +338 -0
- package/src/config.js +400 -0
- package/src/decompiler.js +504 -0
- package/src/dev.js +390 -0
- package/src/errors.js +56 -0
- package/src/extensions.js +707 -0
- package/src/index.js +31 -0
- package/src/language.js +12 -0
- package/src/lexer.js +202 -0
- package/src/log.js +70 -0
- package/src/package.js +202 -0
- package/src/packager.js +177 -0
- package/src/parser.js +608 -0
- package/src/product.js +27 -0
- package/src/project.js +156 -0
- package/src/sb3.js +340 -0
- package/src/sprite.js +81 -0
- package/vendor/pm-packager-dist/.fx-build.json +6 -0
- package/vendor/pm-packager-dist/LICENSE +202 -0
- package/vendor/pm-packager-dist/NOTICE +6 -0
- package/vendor/pm-packager-dist/dist/290e09e569a1cab8e61ba93b0d23863f.png +0 -0
- package/vendor/pm-packager-dist/dist/icns.js +1 -0
- package/vendor/pm-packager-dist/dist/packager.js +154 -0
- package/vendor/pm-packager-dist/dist/scaffolding/addons.js +2 -0
- package/vendor/pm-packager-dist/dist/scaffolding/extension worker.js +2 -0
- package/vendor/pm-packager-dist/dist/scaffolding/scaffolding-full.js +93 -0
- package/vendor/pm-packager-dist/dist/scaffolding/scaffolding-min.js +93 -0
- package/vendor/pm-packager-dist/dist/scaffolding/vendors~iframe-extension-worker.js +2 -0
- package/vendor/pm-packager-dist/dist/vendors~sha256.js +3 -0
- package/vendor/pm-packager-dist/package.json +3 -0
package/bin/fx.js
ADDED
|
@@ -0,0 +1,949 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Triflare Fx command line interface.
|
|
4
|
+
*
|
|
5
|
+
* fx build [input.tfx | dir] [-o out.pmp] [--no-config] (alias: b)
|
|
6
|
+
* fx decompile <input.pmp> [-o out.tfx] [--assets <dir>] (alias: d)
|
|
7
|
+
* fx check [input.tfx | dir] (alias: c)
|
|
8
|
+
* fx catalog [dir] [--json] (alias: l)
|
|
9
|
+
* fx init [dir] (alias: i)
|
|
10
|
+
*/
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import crypto from "node:crypto";
|
|
14
|
+
import process from "node:process";
|
|
15
|
+
import { build } from "../src/build.js";
|
|
16
|
+
import { parse } from "../src/parser.js";
|
|
17
|
+
import { discoverSources, mergePrograms } from "../src/project.js";
|
|
18
|
+
import { decompile } from "../src/decompiler.js";
|
|
19
|
+
import { unzipSync } from "../src/sb3.js";
|
|
20
|
+
import { CATALOG, blockNames } from "../src/catalog.js";
|
|
21
|
+
import {
|
|
22
|
+
addProjectExtension,
|
|
23
|
+
findConfigPath,
|
|
24
|
+
loadConfigFrom,
|
|
25
|
+
normalizePackageTarget,
|
|
26
|
+
readProjectExtensions,
|
|
27
|
+
removeProjectExtension,
|
|
28
|
+
} from "../src/config.js";
|
|
29
|
+
import { loadExtensions } from "../src/extensions.js";
|
|
30
|
+
import { readSprite, spriteArchive, spriteSource } from "../src/sprite.js";
|
|
31
|
+
import { packageProject } from "../src/packager.js";
|
|
32
|
+
import {
|
|
33
|
+
extractArchive,
|
|
34
|
+
packageOutputPath,
|
|
35
|
+
packageSignature,
|
|
36
|
+
resolvePackageConfig,
|
|
37
|
+
slugify,
|
|
38
|
+
} from "../src/package.js";
|
|
39
|
+
import { startDevServer } from "../src/dev.js";
|
|
40
|
+
import {
|
|
41
|
+
brandText,
|
|
42
|
+
configFilename,
|
|
43
|
+
outputDirectory,
|
|
44
|
+
sourceDirectory,
|
|
45
|
+
sourceExtension,
|
|
46
|
+
} from "../src/brand.js";
|
|
47
|
+
import { createLogger } from "../src/log.js";
|
|
48
|
+
import { loadProduct } from "../src/product.js";
|
|
49
|
+
|
|
50
|
+
const product = loadProduct();
|
|
51
|
+
const log = createLogger(product);
|
|
52
|
+
const EXTENSION = sourceExtension(product);
|
|
53
|
+
|
|
54
|
+
function parseArgs(argv) {
|
|
55
|
+
const args = { _: [], flags: {} };
|
|
56
|
+
const booleanFlags = new Set([
|
|
57
|
+
"no-config",
|
|
58
|
+
"json",
|
|
59
|
+
"help",
|
|
60
|
+
"h",
|
|
61
|
+
"version",
|
|
62
|
+
"v",
|
|
63
|
+
"force",
|
|
64
|
+
"extract",
|
|
65
|
+
"lan",
|
|
66
|
+
"strict-port",
|
|
67
|
+
]);
|
|
68
|
+
for (let i = 0; i < argv.length; i++) {
|
|
69
|
+
const token = argv[i];
|
|
70
|
+
if (token.startsWith("--")) {
|
|
71
|
+
const [key, inline] = token.slice(2).split("=");
|
|
72
|
+
if (inline !== undefined) args.flags[key] = inline;
|
|
73
|
+
else if (booleanFlags.has(key)) args.flags[key] = true;
|
|
74
|
+
else if (argv[i + 1] && !argv[i + 1].startsWith("-"))
|
|
75
|
+
args.flags[key] = argv[++i];
|
|
76
|
+
else args.flags[key] = true;
|
|
77
|
+
} else if (token.startsWith("-")) {
|
|
78
|
+
const key = token.slice(1);
|
|
79
|
+
if (booleanFlags.has(key)) args.flags[key] = true;
|
|
80
|
+
else if (argv[i + 1] && !argv[i + 1].startsWith("-"))
|
|
81
|
+
args.flags[key] = argv[++i];
|
|
82
|
+
else args.flags[key] = true;
|
|
83
|
+
} else {
|
|
84
|
+
args._.push(token);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return args;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function usage(stream = process.stdout) {
|
|
91
|
+
const command = brandText(product.command, stream);
|
|
92
|
+
const name = brandText(product.name, stream);
|
|
93
|
+
return `${name} ${product.version} — ${product.tagline}
|
|
94
|
+
|
|
95
|
+
Usage:
|
|
96
|
+
${command} build [<input${EXTENSION}>|<dir>] [-o <output.pmp>] [--no-config]
|
|
97
|
+
${command} package [<input${EXTENSION}>|<input.pmp>|<dir>] [-o <output>] [--target <target>] [--extract]
|
|
98
|
+
${command} dev [<input${EXTENSION}>|<dir>] [--host [host]] [--lan] [--port <port>] [--strict-port]
|
|
99
|
+
${command} decompile <input.pmp> [-o <output${EXTENSION}>] [--assets <dir>]
|
|
100
|
+
${command} check [<input${EXTENSION}>|<dir>]
|
|
101
|
+
${command} catalog [<dir>] [--json]
|
|
102
|
+
${command} init [dir]
|
|
103
|
+
|
|
104
|
+
Commands:
|
|
105
|
+
build (b) Compile a source file, or every ${EXTENSION} file in a directory
|
|
106
|
+
(defaults to the working directory) into one .pmp.
|
|
107
|
+
package (p) Package a source, project archive or directory into HTML or a ZIP
|
|
108
|
+
(Windows/macOS/Linux desktop builds use Electron).
|
|
109
|
+
dev Serve the packaged HTML and reload when the output changes.
|
|
110
|
+
decompile (d) Convert a .pmp back into ${product.name} source.
|
|
111
|
+
check (c) Parse a file (or every ${EXTENSION} file in a directory) for errors.
|
|
112
|
+
catalog (l) List ${product.name} blocks, including those from ${configFilename(product)}
|
|
113
|
+
extensions (paths or URLs).
|
|
114
|
+
init (i) Scaffold a new project.
|
|
115
|
+
|
|
116
|
+
Command groups:
|
|
117
|
+
extension add <source> [--unsandboxed <id>]
|
|
118
|
+
Add an extension (path or URL) to ${configFilename(product)}.
|
|
119
|
+
extension remove <source|id>
|
|
120
|
+
Remove an extension from ${configFilename(product)}.
|
|
121
|
+
extension list [--json]
|
|
122
|
+
List configured extensions.
|
|
123
|
+
sprite add <url|file> [--name <name>] [--force]
|
|
124
|
+
Import a .sprite3/.pms sprite as .tfx source.
|
|
125
|
+
sprite build <name> [-o <output.pms>]
|
|
126
|
+
Compile one sprite into a .pms archive.
|
|
127
|
+
sprite remove <name>
|
|
128
|
+
Delete a source file that declares only that sprite.
|
|
129
|
+
sprite list [<dir>] [--json]
|
|
130
|
+
List declared sprites.
|
|
131
|
+
|
|
132
|
+
Options:
|
|
133
|
+
-o, --output Output file path
|
|
134
|
+
--no-config Ignore ${configFilename(product)}
|
|
135
|
+
--assets <dir> Extract project assets into <dir> when decompiling
|
|
136
|
+
--target <t> Packaging target (html, zip, windows, macos, linux, …)
|
|
137
|
+
--extract Unzip a packaged desktop/zip archive
|
|
138
|
+
--host [host] Dev server host; with no value listen on all interfaces
|
|
139
|
+
--lan Alias for listening on all interfaces
|
|
140
|
+
--port <port> Dev server port (default 3000)
|
|
141
|
+
--strict-port Fail instead of picking the next free dev port
|
|
142
|
+
--json Machine readable output (catalog)
|
|
143
|
+
-v, --version Print the version
|
|
144
|
+
`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Long command name -> one-letter alias. */
|
|
148
|
+
const COMMAND_ALIASES = {
|
|
149
|
+
b: "build",
|
|
150
|
+
p: "package",
|
|
151
|
+
d: "decompile",
|
|
152
|
+
c: "check",
|
|
153
|
+
l: "catalog",
|
|
154
|
+
i: "init",
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
function fail(message) {
|
|
158
|
+
log.error(message);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function loadProjectExtensions(dir, config, { localFileWarning } = {}) {
|
|
163
|
+
const catalog = {};
|
|
164
|
+
const extensionUrls = {};
|
|
165
|
+
const unsandboxed = [];
|
|
166
|
+
if (!config.extensions || config.extensions.length === 0)
|
|
167
|
+
return { catalog, extensionUrls, unsandboxed };
|
|
168
|
+
const loaded = await loadExtensions(config.extensions, {
|
|
169
|
+
baseDir: dir,
|
|
170
|
+
expectUnsandboxed: config.unsandboxed,
|
|
171
|
+
localFileWarning,
|
|
172
|
+
});
|
|
173
|
+
Object.assign(catalog, loaded.catalog);
|
|
174
|
+
Object.assign(extensionUrls, loaded.extensionUrls);
|
|
175
|
+
unsandboxed.push(...loaded.unsandboxed);
|
|
176
|
+
for (const warning of loaded.warnings)
|
|
177
|
+
process.stderr.write(`warning: ${warning}\n`);
|
|
178
|
+
return { catalog, extensionUrls, unsandboxed };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Split a positional build/check argument into `{file}` or `{dir}`. */
|
|
182
|
+
function resolveInput(input) {
|
|
183
|
+
if (!input) return { dir: process.cwd() };
|
|
184
|
+
const resolved = path.resolve(input);
|
|
185
|
+
let stat;
|
|
186
|
+
try {
|
|
187
|
+
stat = fs.statSync(resolved);
|
|
188
|
+
} catch {
|
|
189
|
+
fail(`input not found: ${input}`);
|
|
190
|
+
}
|
|
191
|
+
return stat.isDirectory() ? { dir: resolved } : { file: resolved };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function cmdBuild(args) {
|
|
195
|
+
const target = resolveInput(args._[0]);
|
|
196
|
+
const searchDir = target.file ? path.dirname(target.file) : target.dir;
|
|
197
|
+
|
|
198
|
+
const loaded = args.flags["no-config"]
|
|
199
|
+
? { project: null, fx: null, path: null }
|
|
200
|
+
: loadConfigFrom(searchDir);
|
|
201
|
+
const config = loaded.project;
|
|
202
|
+
const extBase = loaded.path ? path.dirname(loaded.path) : searchDir;
|
|
203
|
+
const { catalog, extensionUrls, unsandboxed } = await loadProjectExtensions(
|
|
204
|
+
extBase,
|
|
205
|
+
config || {},
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const result = build({
|
|
209
|
+
...target,
|
|
210
|
+
projectConfig: config,
|
|
211
|
+
fxConfig: loaded.fx,
|
|
212
|
+
catalog,
|
|
213
|
+
extensionUrls,
|
|
214
|
+
useConfigFile: !args.flags["no-config"],
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const explicit = args.flags.o || args.flags.output;
|
|
218
|
+
const output =
|
|
219
|
+
explicit ||
|
|
220
|
+
(target.file
|
|
221
|
+
? target.file.toLowerCase().endsWith(EXTENSION.toLowerCase())
|
|
222
|
+
? `${target.file.slice(0, -EXTENSION.length)}.pmp`
|
|
223
|
+
: `${target.file}.pmp`
|
|
224
|
+
: path.join(
|
|
225
|
+
target.dir,
|
|
226
|
+
outputDirectory(product),
|
|
227
|
+
`${slugify(result.config.name || path.basename(target.dir))}.pmp`,
|
|
228
|
+
));
|
|
229
|
+
|
|
230
|
+
fs.mkdirSync(path.dirname(path.resolve(output)), { recursive: true });
|
|
231
|
+
fs.writeFileSync(output, result.sb3);
|
|
232
|
+
|
|
233
|
+
const blockCount = result.project.targets.reduce(
|
|
234
|
+
(sum, t) => sum + Object.keys(t.blocks).length,
|
|
235
|
+
0,
|
|
236
|
+
);
|
|
237
|
+
log.success(`Built ${output}`);
|
|
238
|
+
process.stdout.write(
|
|
239
|
+
` sources: ${result.files.length} file(s)\n` +
|
|
240
|
+
` targets: ${result.project.targets.length}\n` +
|
|
241
|
+
` blocks: ${blockCount}\n` +
|
|
242
|
+
` assets: ${result.assets.size}\n` +
|
|
243
|
+
` extensions: ${result.project.extensions.join(", ") || "(none)"}\n` +
|
|
244
|
+
(unsandboxed.length
|
|
245
|
+
? ` unsandboxed: ${unsandboxed.join(", ")} ` +
|
|
246
|
+
"(must be allowed to run unsandboxed in the editor)\n"
|
|
247
|
+
: ""),
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function formatBytes(bytes) {
|
|
252
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
253
|
+
const units = ["KB", "MB", "GB"];
|
|
254
|
+
let value = bytes / 1024;
|
|
255
|
+
let unit = units[0];
|
|
256
|
+
for (let i = 1; value >= 1024 && i < units.length; i += 1) {
|
|
257
|
+
value /= 1024;
|
|
258
|
+
unit = units[i];
|
|
259
|
+
}
|
|
260
|
+
return `${value.toFixed(value >= 10 ? 1 : 2)} ${unit}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Read a `.pmp` directly, or build `.tfx`/directory sources into one. */
|
|
264
|
+
async function readPackagingSource(target, loaded) {
|
|
265
|
+
const searchDir = target.file ? path.dirname(target.file) : target.dir;
|
|
266
|
+
if (target.file && path.extname(target.file).toLowerCase() === ".pmp") {
|
|
267
|
+
return {
|
|
268
|
+
sb3: fs.readFileSync(target.file),
|
|
269
|
+
projectName: path.basename(target.file, path.extname(target.file)),
|
|
270
|
+
searchDir,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const extBase = loaded.path ? path.dirname(loaded.path) : searchDir;
|
|
275
|
+
const { catalog, extensionUrls } = await loadProjectExtensions(
|
|
276
|
+
extBase,
|
|
277
|
+
loaded.project || {},
|
|
278
|
+
);
|
|
279
|
+
const result = build({
|
|
280
|
+
...target,
|
|
281
|
+
projectConfig: loaded.project,
|
|
282
|
+
fxConfig: loaded.fx,
|
|
283
|
+
catalog,
|
|
284
|
+
extensionUrls,
|
|
285
|
+
useConfigFile: false,
|
|
286
|
+
});
|
|
287
|
+
return {
|
|
288
|
+
sb3: result.sb3,
|
|
289
|
+
projectName: result.config.name,
|
|
290
|
+
searchDir,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function cmdPackage(args) {
|
|
295
|
+
const target = resolveInput(args._[0]);
|
|
296
|
+
const loaded = args.flags["no-config"]
|
|
297
|
+
? { project: null, fx: null, package: null, path: null }
|
|
298
|
+
: loadConfigFrom(target.file ? path.dirname(target.file) : target.dir);
|
|
299
|
+
const source = await readPackagingSource(target, loaded);
|
|
300
|
+
const baseDir = loaded.path ? path.dirname(loaded.path) : source.searchDir;
|
|
301
|
+
const pkg = resolvePackageConfig(loaded.package, {
|
|
302
|
+
baseDir,
|
|
303
|
+
projectName: source.projectName,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
if (args.flags.target !== undefined) {
|
|
307
|
+
if (typeof args.flags.target !== "string") fail("--target needs a value");
|
|
308
|
+
pkg.target = normalizePackageTarget(args.flags.target);
|
|
309
|
+
}
|
|
310
|
+
const explicit = args.flags.o || args.flags.output;
|
|
311
|
+
if (explicit !== undefined) {
|
|
312
|
+
if (typeof explicit !== "string") fail("-o/--output needs a value");
|
|
313
|
+
pkg.output = path.resolve(explicit);
|
|
314
|
+
}
|
|
315
|
+
const extract = Boolean(args.flags.extract) || pkg.extract;
|
|
316
|
+
if (extract && pkg.target === "html")
|
|
317
|
+
fail("--extract only applies to zip and desktop targets");
|
|
318
|
+
|
|
319
|
+
const output = packageOutputPath({
|
|
320
|
+
target: pkg.target,
|
|
321
|
+
output: pkg.output,
|
|
322
|
+
name: pkg.name || source.projectName,
|
|
323
|
+
dir: baseDir,
|
|
324
|
+
});
|
|
325
|
+
const result = await packageProject({
|
|
326
|
+
sb3: source.sb3,
|
|
327
|
+
projectName: source.projectName,
|
|
328
|
+
packageConfig: pkg,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
fs.mkdirSync(path.dirname(path.resolve(output)), { recursive: true });
|
|
332
|
+
fs.writeFileSync(output, result.data);
|
|
333
|
+
log.success(`Packaged ${output}`);
|
|
334
|
+
process.stdout.write(
|
|
335
|
+
` target: ${result.target}\n` +
|
|
336
|
+
` type: ${result.type}\n` +
|
|
337
|
+
` size: ${formatBytes(result.data.length)}\n`,
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
if (extract) {
|
|
341
|
+
const extension = path.extname(output);
|
|
342
|
+
const extractDir = path.join(
|
|
343
|
+
path.dirname(output),
|
|
344
|
+
path.basename(output, extension),
|
|
345
|
+
);
|
|
346
|
+
const files = await extractArchive(result.data, extractDir);
|
|
347
|
+
log.success(`Extracted ${files.length} file(s) to ${extractDir}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function cmdDev(args) {
|
|
352
|
+
const target = resolveInput(args._[0]);
|
|
353
|
+
if (target.file && path.extname(target.file).toLowerCase() === ".pmp")
|
|
354
|
+
fail("fx dev needs .tfx sources or a directory, not a .pmp archive");
|
|
355
|
+
|
|
356
|
+
const searchDir = target.file ? path.dirname(target.file) : target.dir;
|
|
357
|
+
const useConfigFile = !args.flags["no-config"];
|
|
358
|
+
const configPath = useConfigFile ? findConfigPath(searchDir) : null;
|
|
359
|
+
const baseDir = configPath ? path.dirname(configPath) : searchDir;
|
|
360
|
+
|
|
361
|
+
let host = "127.0.0.1";
|
|
362
|
+
if (args.flags.lan) host = "0.0.0.0";
|
|
363
|
+
else if (args.flags.host === true) host = "0.0.0.0";
|
|
364
|
+
else if (typeof args.flags.host === "string") host = args.flags.host;
|
|
365
|
+
|
|
366
|
+
if (args.flags.port === true) fail("--port needs a number");
|
|
367
|
+
const port = args.flags.port === undefined ? 3000 : Number(args.flags.port);
|
|
368
|
+
|
|
369
|
+
let lastSb3Hash = null;
|
|
370
|
+
let lastSignature = null;
|
|
371
|
+
let lastHtml = null;
|
|
372
|
+
|
|
373
|
+
const buildForDev = async () => {
|
|
374
|
+
const loaded = useConfigFile
|
|
375
|
+
? loadConfigFrom(searchDir)
|
|
376
|
+
: { project: null, fx: null, package: null, path: null };
|
|
377
|
+
const configBase = loaded.path ? path.dirname(loaded.path) : searchDir;
|
|
378
|
+
const { catalog, extensionUrls } = await loadProjectExtensions(
|
|
379
|
+
configBase,
|
|
380
|
+
loaded.project || {},
|
|
381
|
+
);
|
|
382
|
+
const result = build({
|
|
383
|
+
...target,
|
|
384
|
+
projectConfig: loaded.project,
|
|
385
|
+
fxConfig: loaded.fx,
|
|
386
|
+
catalog,
|
|
387
|
+
extensionUrls,
|
|
388
|
+
useConfigFile: false,
|
|
389
|
+
});
|
|
390
|
+
const pkg = resolvePackageConfig(loaded.package, {
|
|
391
|
+
baseDir: configBase,
|
|
392
|
+
projectName: result.config.name,
|
|
393
|
+
});
|
|
394
|
+
// Dev always serves HTML, no matter what the package block requests.
|
|
395
|
+
pkg.target = "html";
|
|
396
|
+
|
|
397
|
+
const signature = packageSignature(pkg);
|
|
398
|
+
const sb3Hash = crypto
|
|
399
|
+
.createHash("sha256")
|
|
400
|
+
.update(result.sb3)
|
|
401
|
+
.digest("hex");
|
|
402
|
+
if (
|
|
403
|
+
lastHtml != null &&
|
|
404
|
+
sb3Hash === lastSb3Hash &&
|
|
405
|
+
signature === lastSignature
|
|
406
|
+
)
|
|
407
|
+
return lastHtml;
|
|
408
|
+
|
|
409
|
+
const packaged = await packageProject({
|
|
410
|
+
sb3: result.sb3,
|
|
411
|
+
projectName: result.config.name,
|
|
412
|
+
packageConfig: pkg,
|
|
413
|
+
});
|
|
414
|
+
lastSb3Hash = sb3Hash;
|
|
415
|
+
lastSignature = signature;
|
|
416
|
+
lastHtml = packaged.html ?? packaged.data.toString("utf8");
|
|
417
|
+
return lastHtml;
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const server = await startDevServer({
|
|
421
|
+
watchDir: baseDir,
|
|
422
|
+
build: buildForDev,
|
|
423
|
+
host,
|
|
424
|
+
port,
|
|
425
|
+
strictPort: Boolean(args.flags["strict-port"]),
|
|
426
|
+
log,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
process.stdout.write(`\n ${brandText(product.name)} dev\n`);
|
|
430
|
+
for (const url of server.localUrls) log.bullet(`Local: ${url}`);
|
|
431
|
+
for (const url of server.networkUrls) log.bullet(`Network: ${url}`);
|
|
432
|
+
log.info(" Press Ctrl+C to stop.");
|
|
433
|
+
|
|
434
|
+
let stopping = false;
|
|
435
|
+
const shutdown = () => {
|
|
436
|
+
if (stopping) return;
|
|
437
|
+
stopping = true;
|
|
438
|
+
server.close().finally(() => process.exit(0));
|
|
439
|
+
};
|
|
440
|
+
process.once("SIGINT", shutdown);
|
|
441
|
+
process.once("SIGTERM", shutdown);
|
|
442
|
+
|
|
443
|
+
await server.ready;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function cmdCheck(args) {
|
|
447
|
+
const target = resolveInput(args._[0]);
|
|
448
|
+
const files = target.file ? [target.file] : discoverSources(target.dir);
|
|
449
|
+
if (files.length === 0)
|
|
450
|
+
fail(`No ${EXTENSION} files found under ${target.dir}`);
|
|
451
|
+
|
|
452
|
+
const searchDir = target.file ? path.dirname(target.file) : target.dir;
|
|
453
|
+
const loaded = args.flags["no-config"]
|
|
454
|
+
? { fx: null }
|
|
455
|
+
: loadConfigFrom(searchDir);
|
|
456
|
+
const entries = files.map((file) => ({
|
|
457
|
+
ast: parse(fs.readFileSync(file, "utf8"), file),
|
|
458
|
+
file,
|
|
459
|
+
}));
|
|
460
|
+
mergePrograms(entries, {
|
|
461
|
+
partialSprites: loaded.fx?.partialSprites ?? false,
|
|
462
|
+
});
|
|
463
|
+
log.success(`Checked ${files.length} file(s): OK`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function cmdCatalog(args) {
|
|
467
|
+
// Merge the built-in catalog with any extensions declared in fx.yml, so
|
|
468
|
+
// blocks from path- or URL-loaded extensions show up here too.
|
|
469
|
+
const loaded = args.flags["no-config"]
|
|
470
|
+
? { project: null, path: null }
|
|
471
|
+
: loadConfigFrom(args._[0] || process.cwd());
|
|
472
|
+
const config = loaded.project;
|
|
473
|
+
const extBase = loaded.path ? path.dirname(loaded.path) : process.cwd();
|
|
474
|
+
const { catalog } = await loadProjectExtensions(extBase, config || {}, {
|
|
475
|
+
localFileWarning: false,
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
const merged = { ...CATALOG, ...catalog };
|
|
479
|
+
const names = Object.keys(merged).sort();
|
|
480
|
+
|
|
481
|
+
if (args.flags.json) {
|
|
482
|
+
process.stdout.write(`${JSON.stringify(merged, null, 2)}\n`);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const builtIn = blockNames().length;
|
|
486
|
+
process.stdout.write(
|
|
487
|
+
`${product.name} catalog (${names.length} blocks` +
|
|
488
|
+
(names.length > builtIn
|
|
489
|
+
? `, ${names.length - builtIn} from extensions`
|
|
490
|
+
: "") +
|
|
491
|
+
`):\n`,
|
|
492
|
+
);
|
|
493
|
+
for (const name of names) {
|
|
494
|
+
const entry = merged[name];
|
|
495
|
+
const tag = entry.extension ? ` (ext ${entry.extension})` : "";
|
|
496
|
+
process.stdout.write(
|
|
497
|
+
` ${name.padEnd(32)} -> ${entry.opcode} [${entry.kind}]${tag}\n`,
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function cmdDecompile(args) {
|
|
503
|
+
const input = args._[0];
|
|
504
|
+
if (!input)
|
|
505
|
+
fail(`decompile requires an input .pmp file\n\n${usage(process.stderr)}`);
|
|
506
|
+
const data = fs.readFileSync(input);
|
|
507
|
+
const files = unzipSync(data);
|
|
508
|
+
|
|
509
|
+
const assetDir = args.flags.assets;
|
|
510
|
+
let assetPrefix = "";
|
|
511
|
+
if (assetDir) {
|
|
512
|
+
fs.mkdirSync(assetDir, { recursive: true });
|
|
513
|
+
const resolvedAssetDir = fs.realpathSync(assetDir);
|
|
514
|
+
for (const [name, buffer] of files) {
|
|
515
|
+
if (!/^[0-9a-f]{32}\.[a-z0-9]+(?![\s\S])/i.test(name)) continue;
|
|
516
|
+
const destination = path.resolve(resolvedAssetDir, name);
|
|
517
|
+
if (path.dirname(destination) !== resolvedAssetDir) continue;
|
|
518
|
+
// Existing symlinks could redirect an otherwise safe name outside the directory.
|
|
519
|
+
if (
|
|
520
|
+
fs.lstatSync(destination, { throwIfNoEntry: false })?.isSymbolicLink()
|
|
521
|
+
)
|
|
522
|
+
continue;
|
|
523
|
+
fs.writeFileSync(destination, buffer);
|
|
524
|
+
}
|
|
525
|
+
// Reference assets relative to the directory the source file will live in.
|
|
526
|
+
const outDir = path.dirname(
|
|
527
|
+
path.resolve(
|
|
528
|
+
args.flags.o ||
|
|
529
|
+
args.flags.output ||
|
|
530
|
+
path.join(process.cwd(), `out${EXTENSION}`),
|
|
531
|
+
),
|
|
532
|
+
);
|
|
533
|
+
const rel = path
|
|
534
|
+
.relative(outDir, path.resolve(assetDir))
|
|
535
|
+
.replace(/\\/g, "/");
|
|
536
|
+
assetPrefix = rel ? `${rel}/` : "";
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const source = decompile(data, { assetPrefix });
|
|
540
|
+
const output = args.flags.o || args.flags.output;
|
|
541
|
+
if (output) {
|
|
542
|
+
fs.writeFileSync(output, source);
|
|
543
|
+
log.success(`Decompiled ${input} -> ${output}`);
|
|
544
|
+
} else {
|
|
545
|
+
process.stdout.write(source);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function initFiles() {
|
|
550
|
+
return {
|
|
551
|
+
[configFilename(product)]: `project:
|
|
552
|
+
name: "My ${product.name} Project"
|
|
553
|
+
fps: 60
|
|
554
|
+
width: 480
|
|
555
|
+
height: 360
|
|
556
|
+
turbo: false
|
|
557
|
+
interpolation: true
|
|
558
|
+
fx:
|
|
559
|
+
partialSprites: false
|
|
560
|
+
package:
|
|
561
|
+
target: html
|
|
562
|
+
`,
|
|
563
|
+
[`${sourceDirectory(product)}/main${EXTENSION}`]: `sprite "Cat" {
|
|
564
|
+
costumes: {
|
|
565
|
+
cat: "../assets/cat.svg"
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
let score = 0;
|
|
569
|
+
|
|
570
|
+
when greenFlag {
|
|
571
|
+
motion.goToXY(0, 0);
|
|
572
|
+
control.forever {
|
|
573
|
+
motion.turnRight(1);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
when keyPressed("space") {
|
|
578
|
+
score += 1;
|
|
579
|
+
looks.sayForSecs(operators.join("Score: ", score), 1);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
`,
|
|
583
|
+
"assets/cat.svg": `<svg xmlns="http://www.w3.org/2000/svg" width="95" height="101" viewBox="0 0 95 101">
|
|
584
|
+
<ellipse cx="47" cy="60" rx="35" ry="35" fill="#ffab19" stroke="#cf8b17" stroke-width="4"/>
|
|
585
|
+
<circle cx="35" cy="50" r="6" fill="#000"/>
|
|
586
|
+
<circle cx="60" cy="50" r="6" fill="#000"/>
|
|
587
|
+
</svg>
|
|
588
|
+
`,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function cmdInit(args) {
|
|
593
|
+
const dir = args._[0] || ".";
|
|
594
|
+
for (const [rel, content] of Object.entries(initFiles())) {
|
|
595
|
+
const dest = path.join(dir, rel);
|
|
596
|
+
if (fs.existsSync(dest)) {
|
|
597
|
+
log.warn(`skip ${dest} (exists)`);
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
601
|
+
fs.writeFileSync(dest, content);
|
|
602
|
+
log.bullet(dest);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function requireConfigPath() {
|
|
607
|
+
const found = findConfigPath(process.cwd());
|
|
608
|
+
if (!found)
|
|
609
|
+
fail(
|
|
610
|
+
`No ${configFilename(product)} found. Run "${product.command} init" first.`,
|
|
611
|
+
);
|
|
612
|
+
return found;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function readInputBytes(source) {
|
|
616
|
+
if (/^https?:\/\//i.test(source)) {
|
|
617
|
+
const response = await fetch(source, {
|
|
618
|
+
signal: AbortSignal.timeout(15000),
|
|
619
|
+
});
|
|
620
|
+
if (!response.ok)
|
|
621
|
+
throw new Error(
|
|
622
|
+
`Could not download ${source}: ${response.status} ${response.statusText}`,
|
|
623
|
+
);
|
|
624
|
+
return Buffer.from(await response.arrayBuffer());
|
|
625
|
+
}
|
|
626
|
+
return fs.readFileSync(source);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** Every sprite declaration under `dir`, in source order. */
|
|
630
|
+
function spriteNames(dir) {
|
|
631
|
+
const sprites = [];
|
|
632
|
+
for (const file of discoverSources(dir)) {
|
|
633
|
+
const ast = parse(fs.readFileSync(file, "utf8"), file);
|
|
634
|
+
for (const sprite of ast.sprites) {
|
|
635
|
+
sprites.push({ name: sprite.name, file, partial: sprite.partial });
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return sprites;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** Group sprite declarations by name, tracking the base declaration. */
|
|
642
|
+
function spriteGroups(dir) {
|
|
643
|
+
const groups = new Map();
|
|
644
|
+
for (const entry of spriteNames(dir)) {
|
|
645
|
+
let group = groups.get(entry.name);
|
|
646
|
+
if (!group) {
|
|
647
|
+
group = {
|
|
648
|
+
name: entry.name,
|
|
649
|
+
file: entry.file,
|
|
650
|
+
files: [],
|
|
651
|
+
partial: true,
|
|
652
|
+
};
|
|
653
|
+
groups.set(entry.name, group);
|
|
654
|
+
}
|
|
655
|
+
group.files.push(entry.file);
|
|
656
|
+
if (!entry.partial) {
|
|
657
|
+
group.file = entry.file;
|
|
658
|
+
group.partial = false;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
for (const group of groups.values()) {
|
|
662
|
+
// Show the base file first, then fragments in discovery order.
|
|
663
|
+
group.files = [...new Set([group.file, ...group.files])];
|
|
664
|
+
}
|
|
665
|
+
return [...groups.values()];
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function cmdExtensionAdd(args) {
|
|
669
|
+
const source = args._[1];
|
|
670
|
+
if (!source) fail("extension add requires a path or URL");
|
|
671
|
+
const configPath = requireConfigPath();
|
|
672
|
+
let unsandboxed = null;
|
|
673
|
+
if (args.flags.unsandboxed !== undefined) {
|
|
674
|
+
if (typeof args.flags.unsandboxed !== "string")
|
|
675
|
+
fail("--unsandboxed needs an extension id");
|
|
676
|
+
unsandboxed = args.flags.unsandboxed;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const { added, unsandboxedAdded } = addProjectExtension(configPath, source, {
|
|
680
|
+
unsandboxed,
|
|
681
|
+
});
|
|
682
|
+
if (added) log.success(`Added ${source} to ${path.basename(configPath)}`);
|
|
683
|
+
else log.info(`${source} is already listed in ${path.basename(configPath)}`);
|
|
684
|
+
if (unsandboxed && unsandboxedAdded)
|
|
685
|
+
log.success(`Allowed ${unsandboxed} to run unsandboxed`);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function cmdExtensionRemove(args) {
|
|
689
|
+
const target = args._[1];
|
|
690
|
+
if (!target) fail("extension remove requires a path, URL or extension id");
|
|
691
|
+
const configPath = requireConfigPath();
|
|
692
|
+
const result = removeProjectExtension(configPath, target);
|
|
693
|
+
if (result.extensions.length > 0)
|
|
694
|
+
log.success(
|
|
695
|
+
`Removed ${result.extensions.join(", ")} from ${path.basename(configPath)}`,
|
|
696
|
+
);
|
|
697
|
+
if (result.unsandboxedRemoved)
|
|
698
|
+
log.success(`Removed ${target} from the unsandboxed list`);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function cmdExtensionList(args) {
|
|
702
|
+
const configPath = requireConfigPath();
|
|
703
|
+
const { extensions, unsandboxed } = readProjectExtensions(configPath);
|
|
704
|
+
if (args.flags.json) {
|
|
705
|
+
process.stdout.write(
|
|
706
|
+
`${JSON.stringify({ extensions, unsandboxed }, null, 2)}\n`,
|
|
707
|
+
);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
process.stdout.write(
|
|
711
|
+
`${path.basename(configPath)}: ${extensions.length} extension(s)\n`,
|
|
712
|
+
);
|
|
713
|
+
for (const entry of extensions) process.stdout.write(` ${entry}\n`);
|
|
714
|
+
process.stdout.write(`unsandboxed: ${unsandboxed.join(", ") || "(none)"}\n`);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function cmdExtension(args) {
|
|
718
|
+
switch (args._[0]) {
|
|
719
|
+
case "add":
|
|
720
|
+
return cmdExtensionAdd(args);
|
|
721
|
+
case "remove":
|
|
722
|
+
return cmdExtensionRemove(args);
|
|
723
|
+
case "list":
|
|
724
|
+
return cmdExtensionList(args);
|
|
725
|
+
default:
|
|
726
|
+
fail(`extension expects add, remove or list\n\n${usage(process.stderr)}`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
async function cmdSpriteAdd(args) {
|
|
731
|
+
const source = args._[1];
|
|
732
|
+
if (!source) fail("sprite add requires a .sprite3/.pms file or URL");
|
|
733
|
+
const configPath = findConfigPath(process.cwd());
|
|
734
|
+
const rootDir = configPath ? path.dirname(configPath) : process.cwd();
|
|
735
|
+
|
|
736
|
+
const { sprite, assets } = readSprite(await readInputBytes(source));
|
|
737
|
+
const name =
|
|
738
|
+
typeof args.flags.name === "string" ? args.flags.name : sprite.name;
|
|
739
|
+
if (!name) fail("sprite add could not read a sprite name; pass --name");
|
|
740
|
+
sprite.name = name;
|
|
741
|
+
|
|
742
|
+
const existing = spriteNames(rootDir).find((entry) => entry.name === name);
|
|
743
|
+
if (existing)
|
|
744
|
+
fail(
|
|
745
|
+
`A sprite named ${JSON.stringify(name)} already exists in ${existing.file}`,
|
|
746
|
+
);
|
|
747
|
+
|
|
748
|
+
const sourceDir = path.join(rootDir, sourceDirectory(product));
|
|
749
|
+
const file = path.join(sourceDir, `${slugify(name)}${EXTENSION}`);
|
|
750
|
+
if (fs.existsSync(file) && !args.flags.force)
|
|
751
|
+
fail(`${file} already exists; pass --force to overwrite`);
|
|
752
|
+
|
|
753
|
+
const assetsDir = path.join(rootDir, "assets");
|
|
754
|
+
const relative = path.relative(sourceDir, assetsDir).replace(/\\/g, "/");
|
|
755
|
+
const text = spriteSource(sprite, {
|
|
756
|
+
assetPrefix: relative ? `${relative}/` : "",
|
|
757
|
+
});
|
|
758
|
+
parse(text, file);
|
|
759
|
+
|
|
760
|
+
fs.mkdirSync(assetsDir, { recursive: true });
|
|
761
|
+
for (const [assetName, data] of assets)
|
|
762
|
+
fs.writeFileSync(path.join(assetsDir, assetName), data);
|
|
763
|
+
fs.mkdirSync(sourceDir, { recursive: true });
|
|
764
|
+
fs.writeFileSync(file, text);
|
|
765
|
+
log.success(`Added sprite ${name} -> ${path.relative(rootDir, file)}`);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
async function cmdSpriteBuild(args) {
|
|
769
|
+
const name = args._[1];
|
|
770
|
+
if (!name) fail("sprite build requires a sprite name");
|
|
771
|
+
const dir = process.cwd();
|
|
772
|
+
const loaded = args.flags["no-config"]
|
|
773
|
+
? { project: null, fx: null, path: null }
|
|
774
|
+
: loadConfigFrom(dir);
|
|
775
|
+
const config = loaded.project;
|
|
776
|
+
const extBase = loaded.path ? path.dirname(loaded.path) : dir;
|
|
777
|
+
const { catalog, extensionUrls } = await loadProjectExtensions(
|
|
778
|
+
extBase,
|
|
779
|
+
config || {},
|
|
780
|
+
);
|
|
781
|
+
const result = build({
|
|
782
|
+
dir,
|
|
783
|
+
projectConfig: config,
|
|
784
|
+
fxConfig: loaded.fx,
|
|
785
|
+
catalog,
|
|
786
|
+
extensionUrls,
|
|
787
|
+
useConfigFile: !args.flags["no-config"],
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
const sprite = result.project.targets.find(
|
|
791
|
+
(target) => !target.isStage && target.name === name,
|
|
792
|
+
);
|
|
793
|
+
if (!sprite) {
|
|
794
|
+
const available = result.project.targets
|
|
795
|
+
.filter((target) => !target.isStage)
|
|
796
|
+
.map((target) => target.name);
|
|
797
|
+
fail(
|
|
798
|
+
`No sprite named ${JSON.stringify(name)}. ` +
|
|
799
|
+
(available.length
|
|
800
|
+
? `Available: ${available.join(", ")}`
|
|
801
|
+
: "The project has no sprites."),
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const explicit = args.flags.o || args.flags.output;
|
|
806
|
+
const output =
|
|
807
|
+
explicit ||
|
|
808
|
+
path.join(dir, outputDirectory(product), `${slugify(name)}.pms`);
|
|
809
|
+
fs.mkdirSync(path.dirname(path.resolve(output)), { recursive: true });
|
|
810
|
+
fs.writeFileSync(output, spriteArchive(sprite, result.assets));
|
|
811
|
+
log.success(`Built sprite ${name} -> ${output}`);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function cmdSpriteRemove(args) {
|
|
815
|
+
const name = args._[1];
|
|
816
|
+
if (!name) fail("sprite remove requires a sprite name");
|
|
817
|
+
const dir = process.cwd();
|
|
818
|
+
const matches = spriteNames(dir).filter((entry) => entry.name === name);
|
|
819
|
+
if (matches.length === 0)
|
|
820
|
+
fail(`No sprite named ${JSON.stringify(name)} under ${dir}`);
|
|
821
|
+
|
|
822
|
+
const files = [...new Set(matches.map((entry) => entry.file))];
|
|
823
|
+
for (const file of files) {
|
|
824
|
+
const ast = parse(fs.readFileSync(file, "utf8"), file);
|
|
825
|
+
const onlyThisSprite =
|
|
826
|
+
ast.sprites.length > 0 &&
|
|
827
|
+
ast.sprites.every((sprite) => sprite.name === name) &&
|
|
828
|
+
ast.topItems.length === 0 &&
|
|
829
|
+
!ast.stage;
|
|
830
|
+
if (!onlyThisSprite)
|
|
831
|
+
fail(`${file} also declares other code; remove the sprite block by hand`);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
for (const file of files) fs.rmSync(file);
|
|
835
|
+
log.success(
|
|
836
|
+
`Removed sprite ${name} (${files
|
|
837
|
+
.map((file) => path.relative(dir, file) || file)
|
|
838
|
+
.join(", ")})`,
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function cmdSpriteList(args) {
|
|
843
|
+
const dir = path.resolve(args._[1] || process.cwd());
|
|
844
|
+
const sprites = spriteGroups(dir);
|
|
845
|
+
if (args.flags.json) {
|
|
846
|
+
process.stdout.write(
|
|
847
|
+
`${JSON.stringify(
|
|
848
|
+
sprites.map(({ name, file, files, partial }) => ({
|
|
849
|
+
name,
|
|
850
|
+
file: path.relative(dir, file),
|
|
851
|
+
files: files.map((entry) => path.relative(dir, entry)),
|
|
852
|
+
partial,
|
|
853
|
+
})),
|
|
854
|
+
null,
|
|
855
|
+
2,
|
|
856
|
+
)}\n`,
|
|
857
|
+
);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
if (sprites.length === 0) {
|
|
861
|
+
log.info(`No sprites under ${dir}`);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
for (const { name, files } of sprites)
|
|
865
|
+
process.stdout.write(
|
|
866
|
+
` ${name.padEnd(24)} ${files
|
|
867
|
+
.map((file) => path.relative(dir, file) || file)
|
|
868
|
+
.join(", ")}\n`,
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
async function cmdSprite(args) {
|
|
873
|
+
switch (args._[0]) {
|
|
874
|
+
case "add":
|
|
875
|
+
return cmdSpriteAdd(args);
|
|
876
|
+
case "build":
|
|
877
|
+
return cmdSpriteBuild(args);
|
|
878
|
+
case "remove":
|
|
879
|
+
return cmdSpriteRemove(args);
|
|
880
|
+
case "list":
|
|
881
|
+
return cmdSpriteList(args);
|
|
882
|
+
default:
|
|
883
|
+
fail(
|
|
884
|
+
`sprite expects add, build, remove or list\n\n${usage(process.stderr)}`,
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function printVersion() {
|
|
890
|
+
process.stdout.write(`${brandText(product.name)} ${product.version}\n`);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
async function main() {
|
|
894
|
+
const argv = process.argv.slice(2);
|
|
895
|
+
const rawCommand = argv.shift();
|
|
896
|
+
const command = COMMAND_ALIASES[rawCommand] || rawCommand;
|
|
897
|
+
const args = parseArgs(argv);
|
|
898
|
+
|
|
899
|
+
if (args.flags.version || args.flags.v) {
|
|
900
|
+
printVersion();
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
switch (command) {
|
|
905
|
+
case "build":
|
|
906
|
+
return cmdBuild(args);
|
|
907
|
+
case "package":
|
|
908
|
+
return cmdPackage(args);
|
|
909
|
+
case "dev":
|
|
910
|
+
return cmdDev(args);
|
|
911
|
+
case "decompile":
|
|
912
|
+
return cmdDecompile(args);
|
|
913
|
+
case "check":
|
|
914
|
+
return cmdCheck(args);
|
|
915
|
+
case "catalog":
|
|
916
|
+
return cmdCatalog(args);
|
|
917
|
+
case "init":
|
|
918
|
+
return cmdInit(args);
|
|
919
|
+
case "extension":
|
|
920
|
+
return cmdExtension(args);
|
|
921
|
+
case "sprite":
|
|
922
|
+
return cmdSprite(args);
|
|
923
|
+
case "--version":
|
|
924
|
+
case "-v":
|
|
925
|
+
printVersion();
|
|
926
|
+
return;
|
|
927
|
+
case undefined:
|
|
928
|
+
case "help":
|
|
929
|
+
case "--help":
|
|
930
|
+
case "-h":
|
|
931
|
+
process.stdout.write(usage());
|
|
932
|
+
return;
|
|
933
|
+
default:
|
|
934
|
+
log.error(
|
|
935
|
+
`We don't recognize that command: ${JSON.stringify(command)}. ` +
|
|
936
|
+
`Try '${product.command} help'.`,
|
|
937
|
+
);
|
|
938
|
+
process.stdout.write(usage());
|
|
939
|
+
process.exitCode = 1;
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
main().catch((err) => {
|
|
945
|
+
if (err && typeof err.format === "function")
|
|
946
|
+
process.stderr.write(`${err.format()}\n`);
|
|
947
|
+
else process.stderr.write(`error: ${err && err.stack ? err.stack : err}\n`);
|
|
948
|
+
process.exit(1);
|
|
949
|
+
});
|