@triflare/fx 1.0.2 → 2.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/README.md +32 -0
- package/bin/fx.js +4 -1160
- package/package.json +9 -3
- package/product.yml +1 -1
- package/src/brand.js +9 -3
- package/src/build.js +2 -1
- package/src/catalog.js +117 -140
- package/src/cli/commands/build.js +98 -0
- package/src/cli/commands/catalog.js +42 -0
- package/src/cli/commands/check.js +44 -0
- package/src/cli/commands/completion.js +54 -0
- package/src/cli/commands/decompile.js +266 -0
- package/src/cli/commands/dev.js +179 -0
- package/src/cli/commands/extension.js +110 -0
- package/src/cli/commands/index.js +39 -0
- package/src/cli/commands/init.js +169 -0
- package/src/cli/commands/package.js +137 -0
- package/src/cli/commands/sprite/add.js +85 -0
- package/src/cli/commands/sprite/build.js +62 -0
- package/src/cli/commands/sprite/helpers.js +66 -0
- package/src/cli/commands/sprite/index.js +29 -0
- package/src/cli/commands/sprite/list.js +31 -0
- package/src/cli/commands/sprite/remove.js +51 -0
- package/src/cli/commands/vendor/add.js +98 -0
- package/src/cli/commands/vendor/helpers.js +50 -0
- package/src/cli/commands/vendor/index.js +39 -0
- package/src/cli/commands/vendor/install.js +64 -0
- package/src/cli/commands/vendor/list.js +45 -0
- package/src/cli/commands/vendor/migrate.js +47 -0
- package/src/cli/commands/vendor/outdated.js +51 -0
- package/src/cli/commands/vendor/remove.js +34 -0
- package/src/cli/commands/vendor/update.js +49 -0
- package/src/cli/commands/vendor/verify.js +44 -0
- package/src/cli/completion/bash.js +10 -0
- package/src/cli/completion/fish.js +9 -0
- package/src/cli/completion/index.js +97 -0
- package/src/cli/completion/zsh.js +11 -0
- package/src/cli/context.js +257 -0
- package/src/cli/define-command.js +140 -0
- package/src/cli/errors.js +111 -0
- package/src/cli/format.js +31 -0
- package/src/cli/prompts.js +183 -0
- package/src/cli/reporter.js +194 -0
- package/src/cli/run.js +248 -0
- package/src/cli/templates/index.js +112 -0
- package/src/cli/theme.js +84 -0
- package/src/compiler.js +32 -3
- package/src/config-edit.js +280 -1
- package/src/config.js +137 -3
- package/src/decompiler.js +12 -2
- package/src/extensions.js +311 -48
- package/src/index.js +21 -0
- package/src/vendor.js +1323 -0
- package/vendor/pm-packager-dist/.fx-build.json +1 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import {
|
|
4
|
+
addProjectExtension,
|
|
5
|
+
findConfigPath,
|
|
6
|
+
readProjectExtensions,
|
|
7
|
+
removeProjectExtension,
|
|
8
|
+
} from "../../config.js";
|
|
9
|
+
import { defineCommand } from "../define-command.js";
|
|
10
|
+
import { UsageError } from "../errors.js";
|
|
11
|
+
|
|
12
|
+
function requireConfigPath(cwd, product) {
|
|
13
|
+
const found = findConfigPath(cwd);
|
|
14
|
+
if (!found)
|
|
15
|
+
throw new UsageError("No project config found.", {
|
|
16
|
+
hint: `run "${product.command} init" first`,
|
|
17
|
+
});
|
|
18
|
+
return found;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function deprecation(reporter, product) {
|
|
22
|
+
reporter.warn(
|
|
23
|
+
`"${product.command} extension ..." is deprecated and will be removed in a future major version.`,
|
|
24
|
+
);
|
|
25
|
+
reporter.hint(
|
|
26
|
+
`run "${product.command} vendor migrate", then use "${product.command} vendor ..." commands`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const extensionAddCommand = {
|
|
31
|
+
name: "add",
|
|
32
|
+
summary: "Add a legacy project.extensions entry",
|
|
33
|
+
arguments: ["<source>"],
|
|
34
|
+
options: [
|
|
35
|
+
{
|
|
36
|
+
flags: "--unsandboxed <id>",
|
|
37
|
+
description: "Allow an extension id to run unsandboxed",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
run({ args, options, cwd, reporter, product }) {
|
|
41
|
+
deprecation(reporter, product);
|
|
42
|
+
const source = args[0];
|
|
43
|
+
const configPath = requireConfigPath(cwd, product);
|
|
44
|
+
const unsandboxed =
|
|
45
|
+
typeof options.unsandboxed === "string" ? options.unsandboxed : null;
|
|
46
|
+
const result = addProjectExtension(configPath, source, { unsandboxed });
|
|
47
|
+
if (result.added)
|
|
48
|
+
reporter.success(`Added ${source} to ${path.basename(configPath)}`);
|
|
49
|
+
else
|
|
50
|
+
reporter.info(
|
|
51
|
+
`${source} is already listed in ${path.basename(configPath)}`,
|
|
52
|
+
);
|
|
53
|
+
if (unsandboxed && result.unsandboxedAdded)
|
|
54
|
+
reporter.success(`Allowed ${unsandboxed} to run unsandboxed`);
|
|
55
|
+
return { source, config: configPath, ...result };
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const extensionRemoveCommand = {
|
|
60
|
+
name: "remove",
|
|
61
|
+
summary: "Remove a legacy project.extensions entry",
|
|
62
|
+
arguments: ["<source|id>"],
|
|
63
|
+
run({ args, cwd, reporter, product }) {
|
|
64
|
+
deprecation(reporter, product);
|
|
65
|
+
const target = args[0];
|
|
66
|
+
const configPath = requireConfigPath(cwd, product);
|
|
67
|
+
const result = removeProjectExtension(configPath, target);
|
|
68
|
+
if (result.extensions.length > 0)
|
|
69
|
+
reporter.success(
|
|
70
|
+
`Removed ${result.extensions.join(", ")} from ${path.basename(configPath)}`,
|
|
71
|
+
);
|
|
72
|
+
if (result.unsandboxedRemoved)
|
|
73
|
+
reporter.success(`Removed ${target} from the unsandboxed list`);
|
|
74
|
+
return { target, config: configPath, ...result };
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const extensionListCommand = {
|
|
79
|
+
name: "list",
|
|
80
|
+
summary: "List legacy project.extensions entries",
|
|
81
|
+
run({ cwd, reporter, product }) {
|
|
82
|
+
deprecation(reporter, product);
|
|
83
|
+
const configPath = requireConfigPath(cwd, product);
|
|
84
|
+
const { extensions, unsandboxed } = readProjectExtensions(configPath);
|
|
85
|
+
reporter.heading(
|
|
86
|
+
`${path.basename(configPath)}: ${extensions.length} extension(s)`,
|
|
87
|
+
);
|
|
88
|
+
for (const entry of extensions) reporter.item(entry, "", { width: 0 });
|
|
89
|
+
reporter.detail("Unsandboxed", unsandboxed.join(", ") || "(none)", {
|
|
90
|
+
width: 12,
|
|
91
|
+
});
|
|
92
|
+
return { extensions, unsandboxed };
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export function registerExtension(program, options) {
|
|
97
|
+
const extension = new Command("extension")
|
|
98
|
+
.description("Deprecated legacy project.extensions commands.")
|
|
99
|
+
.helpGroup("Legacy Commands:");
|
|
100
|
+
extension.copyInheritedSettings(program);
|
|
101
|
+
extension.action(() => extension.outputHelp());
|
|
102
|
+
program.addCommand(extension, { hidden: true });
|
|
103
|
+
for (const spec of [
|
|
104
|
+
extensionAddCommand,
|
|
105
|
+
extensionRemoveCommand,
|
|
106
|
+
extensionListCommand,
|
|
107
|
+
])
|
|
108
|
+
defineCommand(extension, spec, options);
|
|
109
|
+
return extension;
|
|
110
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { completeCommand, completionCommand } from "./completion.js";
|
|
2
|
+
import { buildCommand } from "./build.js";
|
|
3
|
+
import { catalogCommand } from "./catalog.js";
|
|
4
|
+
import { checkCommand } from "./check.js";
|
|
5
|
+
import { decompileCommand } from "./decompile.js";
|
|
6
|
+
import { devCommand } from "./dev.js";
|
|
7
|
+
import { registerExtension } from "./extension.js";
|
|
8
|
+
import { initCommand } from "./init.js";
|
|
9
|
+
import { packageCommand } from "./package.js";
|
|
10
|
+
import { registerSprite } from "./sprite/index.js";
|
|
11
|
+
import { registerVendor } from "./vendor/index.js";
|
|
12
|
+
import { defineCommand } from "../define-command.js";
|
|
13
|
+
|
|
14
|
+
const TOP_LEVEL = [
|
|
15
|
+
buildCommand,
|
|
16
|
+
devCommand,
|
|
17
|
+
packageCommand,
|
|
18
|
+
decompileCommand,
|
|
19
|
+
checkCommand,
|
|
20
|
+
catalogCommand,
|
|
21
|
+
initCommand,
|
|
22
|
+
completionCommand,
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Register every command on the root program.
|
|
27
|
+
*
|
|
28
|
+
* @param {import("commander").Command} program
|
|
29
|
+
* @param {{state: object, product: object}} options
|
|
30
|
+
*/
|
|
31
|
+
export function registerCommands(program, options) {
|
|
32
|
+
program.commandsGroup("Core Commands:");
|
|
33
|
+
for (const spec of TOP_LEVEL) defineCommand(program, spec, options);
|
|
34
|
+
registerVendor(program, options);
|
|
35
|
+
registerSprite(program, options);
|
|
36
|
+
defineCommand(program, completeCommand, options);
|
|
37
|
+
registerExtension(program, options);
|
|
38
|
+
return program;
|
|
39
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { normalizePackageTarget } from "../../config.js";
|
|
4
|
+
import { UsageError } from "../errors.js";
|
|
5
|
+
import {
|
|
6
|
+
initFiles,
|
|
7
|
+
STAGE_SIZES,
|
|
8
|
+
TARGETS,
|
|
9
|
+
TEMPLATES,
|
|
10
|
+
} from "../templates/index.js";
|
|
11
|
+
|
|
12
|
+
const TEMPLATE_VALUES = new Set(TEMPLATES.map((entry) => entry.value));
|
|
13
|
+
const TARGET_VALUES = new Set(TARGETS.map((entry) => entry.value));
|
|
14
|
+
|
|
15
|
+
function parseSize(value) {
|
|
16
|
+
const match = /^(\d+)x(\d+)$/i.exec(String(value).trim());
|
|
17
|
+
if (!match)
|
|
18
|
+
throw new UsageError(
|
|
19
|
+
`--size must look like 480x360, received ${JSON.stringify(value)}`,
|
|
20
|
+
);
|
|
21
|
+
return { width: Number(match[1]), height: Number(match[2]) };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function sizeValue(width, height) {
|
|
25
|
+
return `${width}x${height}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const initCommand = {
|
|
29
|
+
name: "init",
|
|
30
|
+
aliases: ["i"],
|
|
31
|
+
group: "Core Commands:",
|
|
32
|
+
summary: "Create a new project",
|
|
33
|
+
description: `Scaffold a project config, a source file, and optional starter
|
|
34
|
+
assets. On a terminal this opens a short wizard; use --yes for defaults.`,
|
|
35
|
+
arguments: ["[dir]"],
|
|
36
|
+
options: [
|
|
37
|
+
{ flags: "--name <name>", description: "Project name" },
|
|
38
|
+
{
|
|
39
|
+
flags: "--template <template>",
|
|
40
|
+
description: "Template: cat, empty, or blank",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
flags: "--target <target>",
|
|
44
|
+
description: "Package target: none, html, or zip",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
flags: "--size <WxH>",
|
|
48
|
+
description: "Stage size (default 480x360)",
|
|
49
|
+
},
|
|
50
|
+
{ flags: "--force", description: "Overwrite existing files" },
|
|
51
|
+
],
|
|
52
|
+
examples: [
|
|
53
|
+
"fx init",
|
|
54
|
+
"fx init examples/demo --yes",
|
|
55
|
+
"fx init --template blank --target zip",
|
|
56
|
+
],
|
|
57
|
+
async run({ args, options, cwd, reporter, prompts, product, yes }) {
|
|
58
|
+
const directory = path.resolve(cwd, args[0] || ".");
|
|
59
|
+
const defaultName = `My ${product.name} Project`;
|
|
60
|
+
const interactive = prompts.enabled && !yes;
|
|
61
|
+
|
|
62
|
+
if (interactive) prompts.intro("Create a new project");
|
|
63
|
+
|
|
64
|
+
const name =
|
|
65
|
+
options.name ??
|
|
66
|
+
(interactive
|
|
67
|
+
? await prompts.text({
|
|
68
|
+
message: "Project name",
|
|
69
|
+
defaultValue: defaultName,
|
|
70
|
+
})
|
|
71
|
+
: defaultName);
|
|
72
|
+
|
|
73
|
+
let template = options.template ?? "cat";
|
|
74
|
+
if (!TEMPLATE_VALUES.has(template))
|
|
75
|
+
throw new UsageError(
|
|
76
|
+
`--template must be one of: ${[...TEMPLATE_VALUES].join(", ")}`,
|
|
77
|
+
);
|
|
78
|
+
if (interactive && options.template === undefined)
|
|
79
|
+
template = await prompts.select({
|
|
80
|
+
message: "Template",
|
|
81
|
+
options: TEMPLATES,
|
|
82
|
+
initialValue: template,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
let size = options.size ?? "480x360";
|
|
86
|
+
if (interactive && options.size === undefined)
|
|
87
|
+
size = await prompts.select({
|
|
88
|
+
message: "Stage size",
|
|
89
|
+
options: STAGE_SIZES,
|
|
90
|
+
initialValue: "480x360",
|
|
91
|
+
});
|
|
92
|
+
const { width, height } = parseSize(size);
|
|
93
|
+
|
|
94
|
+
let target = options.target ?? "html";
|
|
95
|
+
if (!TARGET_VALUES.has(target)) {
|
|
96
|
+
try {
|
|
97
|
+
target = normalizePackageTarget(target);
|
|
98
|
+
} catch {
|
|
99
|
+
throw new UsageError(
|
|
100
|
+
`--target must be one of: ${[...TARGET_VALUES].join(", ")}`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (interactive && options.target === undefined)
|
|
105
|
+
target = await prompts.select({
|
|
106
|
+
message: "Package target",
|
|
107
|
+
options: TARGETS,
|
|
108
|
+
initialValue: target,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const files = initFiles({
|
|
112
|
+
product,
|
|
113
|
+
name,
|
|
114
|
+
template,
|
|
115
|
+
width,
|
|
116
|
+
height,
|
|
117
|
+
target,
|
|
118
|
+
});
|
|
119
|
+
const conflicts = Object.keys(files).filter((relative) =>
|
|
120
|
+
fs.existsSync(path.join(directory, relative)),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
let force = Boolean(options.force);
|
|
124
|
+
if (conflicts.length > 0 && !force) {
|
|
125
|
+
if (interactive) {
|
|
126
|
+
const overwrite = await prompts.confirm({
|
|
127
|
+
message: `Overwrite ${conflicts.length} existing file(s)?`,
|
|
128
|
+
initialValue: false,
|
|
129
|
+
});
|
|
130
|
+
force = Boolean(overwrite);
|
|
131
|
+
}
|
|
132
|
+
if (!force)
|
|
133
|
+
reporter.warn(
|
|
134
|
+
`Skipping ${conflicts.length} existing file(s); pass --force to overwrite`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const written = [];
|
|
139
|
+
for (const relative of Object.keys(files).sort()) {
|
|
140
|
+
const destination = path.join(directory, relative);
|
|
141
|
+
if (fs.existsSync(destination) && !force) {
|
|
142
|
+
written.push({ path: relative, status: "skipped" });
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
146
|
+
fs.writeFileSync(destination, files[relative]);
|
|
147
|
+
written.push({ path: relative, status: "written" });
|
|
148
|
+
reporter.bullet(relative);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const count = written.filter((entry) => entry.status === "written").length;
|
|
152
|
+
reporter.success(
|
|
153
|
+
`Created ${count} file(s) in ${path.relative(cwd, directory) || "."}`,
|
|
154
|
+
);
|
|
155
|
+
if (interactive)
|
|
156
|
+
prompts.outro(
|
|
157
|
+
`Run "${product.command} build" or "${product.command} dev" next.`,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
directory,
|
|
162
|
+
name,
|
|
163
|
+
template,
|
|
164
|
+
target,
|
|
165
|
+
size: sizeValue(width, height),
|
|
166
|
+
files: written,
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { build } from "../../build.js";
|
|
4
|
+
import { normalizePackageTarget } from "../../config.js";
|
|
5
|
+
import { packageProject } from "../../packager.js";
|
|
6
|
+
import {
|
|
7
|
+
extractArchive,
|
|
8
|
+
packageOutputPath,
|
|
9
|
+
resolvePackageConfig,
|
|
10
|
+
} from "../../package.js";
|
|
11
|
+
import { loadProjectState } from "../context.js";
|
|
12
|
+
import { UsageError } from "../errors.js";
|
|
13
|
+
import { formatBytes, formatDuration } from "../format.js";
|
|
14
|
+
|
|
15
|
+
function isArchive(file) {
|
|
16
|
+
return path.extname(file).toLowerCase() === ".pmp";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const packageCommand = {
|
|
20
|
+
name: "package",
|
|
21
|
+
aliases: ["p", "pack"],
|
|
22
|
+
group: "Core Commands:",
|
|
23
|
+
summary: "Package into HTML, ZIP, or a desktop build",
|
|
24
|
+
description: `Package a source, project archive, or directory into HTML, a ZIP
|
|
25
|
+
archive, or a desktop build (Windows/macOS/Linux use Electron).`,
|
|
26
|
+
arguments: ["[input.tfx|input.pmp|dir]"],
|
|
27
|
+
options: [
|
|
28
|
+
{ flags: "-o, --output <file>", description: "Output file or directory" },
|
|
29
|
+
{ flags: "--target <target>", description: "Packaging target" },
|
|
30
|
+
{ flags: "--extract", description: "Extract a packaged archive" },
|
|
31
|
+
],
|
|
32
|
+
examples: [
|
|
33
|
+
"fx package",
|
|
34
|
+
"fx package --target html",
|
|
35
|
+
"fx package build/game.pmp --target windows --extract",
|
|
36
|
+
],
|
|
37
|
+
async run({ args, options, cwd, reporter, prompts }) {
|
|
38
|
+
const started = performance.now();
|
|
39
|
+
const input = args[0];
|
|
40
|
+
const archiveInput = input ? isArchive(path.resolve(cwd, input)) : false;
|
|
41
|
+
const state = await loadProjectState({
|
|
42
|
+
input,
|
|
43
|
+
cwd,
|
|
44
|
+
options,
|
|
45
|
+
reporter,
|
|
46
|
+
prompts,
|
|
47
|
+
includeExtensions: !archiveInput,
|
|
48
|
+
});
|
|
49
|
+
const { target, loaded, searchDir } = state;
|
|
50
|
+
|
|
51
|
+
let sb3;
|
|
52
|
+
let projectName;
|
|
53
|
+
if (target.file && isArchive(target.file)) {
|
|
54
|
+
sb3 = fs.readFileSync(target.file);
|
|
55
|
+
projectName = path.basename(target.file, path.extname(target.file));
|
|
56
|
+
} else {
|
|
57
|
+
const result = build({
|
|
58
|
+
...target,
|
|
59
|
+
projectConfig: loaded.project,
|
|
60
|
+
fxConfig: loaded.fx,
|
|
61
|
+
catalog: state.catalog,
|
|
62
|
+
extensionUrls: state.extensionUrls,
|
|
63
|
+
useConfigFile: false,
|
|
64
|
+
});
|
|
65
|
+
for (const warning of result.warnings) reporter.warn(warning);
|
|
66
|
+
sb3 = result.sb3;
|
|
67
|
+
projectName = result.config.name;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const baseDir = loaded.path ? path.dirname(loaded.path) : searchDir;
|
|
71
|
+
const pkg = resolvePackageConfig(loaded.package, {
|
|
72
|
+
baseDir,
|
|
73
|
+
projectName,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (typeof options.target === "string")
|
|
77
|
+
pkg.target = normalizePackageTarget(options.target);
|
|
78
|
+
if (typeof options.output === "string")
|
|
79
|
+
pkg.output = path.resolve(cwd, options.output);
|
|
80
|
+
const extract = Boolean(options.extract) || pkg.extract;
|
|
81
|
+
if (extract && pkg.target === "html")
|
|
82
|
+
throw new UsageError("--extract only applies to zip and desktop targets");
|
|
83
|
+
|
|
84
|
+
const output = packageOutputPath({
|
|
85
|
+
target: pkg.target,
|
|
86
|
+
output: pkg.output,
|
|
87
|
+
name: pkg.name || projectName,
|
|
88
|
+
dir: baseDir,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const spinner = prompts.spinner();
|
|
92
|
+
spinner.start("Packaging project...");
|
|
93
|
+
let result;
|
|
94
|
+
try {
|
|
95
|
+
result = await packageProject({
|
|
96
|
+
sb3,
|
|
97
|
+
projectName,
|
|
98
|
+
packageConfig: pkg,
|
|
99
|
+
onProgress: (message) => spinner.message(String(message)),
|
|
100
|
+
});
|
|
101
|
+
} finally {
|
|
102
|
+
spinner.stop();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fs.mkdirSync(path.dirname(path.resolve(output)), { recursive: true });
|
|
106
|
+
fs.writeFileSync(output, result.data);
|
|
107
|
+
const durationMs = Math.round(performance.now() - started);
|
|
108
|
+
|
|
109
|
+
reporter.success(`Packaged ${output}`);
|
|
110
|
+
reporter.text("");
|
|
111
|
+
reporter.detail("Target", result.target);
|
|
112
|
+
reporter.detail("Type", result.type);
|
|
113
|
+
reporter.detail("Size", formatBytes(result.data.length));
|
|
114
|
+
reporter.detail("Time", formatDuration(durationMs));
|
|
115
|
+
|
|
116
|
+
let extracted = null;
|
|
117
|
+
if (extract) {
|
|
118
|
+
const extension = path.extname(output);
|
|
119
|
+
const extractDir = path.join(
|
|
120
|
+
path.dirname(output),
|
|
121
|
+
path.basename(output, extension),
|
|
122
|
+
);
|
|
123
|
+
const files = await extractArchive(result.data, extractDir);
|
|
124
|
+
extracted = { directory: extractDir, files };
|
|
125
|
+
reporter.success(`Extracted ${files.length} file(s) to ${extractDir}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
output,
|
|
130
|
+
target: result.target,
|
|
131
|
+
type: result.type,
|
|
132
|
+
size: result.data.length,
|
|
133
|
+
extracted,
|
|
134
|
+
durationMs,
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { findConfigPath } from "../../../config.js";
|
|
4
|
+
import { sourceExtension } from "../../../brand.js";
|
|
5
|
+
import { slugify } from "../../../package.js";
|
|
6
|
+
import { parse } from "../../../parser.js";
|
|
7
|
+
import { readSprite, spriteSource } from "../../../sprite.js";
|
|
8
|
+
import { CliError } from "../../errors.js";
|
|
9
|
+
import {
|
|
10
|
+
readInputBytes,
|
|
11
|
+
spriteNames,
|
|
12
|
+
spriteRoot,
|
|
13
|
+
spriteSourceDir,
|
|
14
|
+
} from "./helpers.js";
|
|
15
|
+
|
|
16
|
+
export const spriteAddCommand = {
|
|
17
|
+
name: "add",
|
|
18
|
+
summary: "Import a .sprite3/.pms sprite as source",
|
|
19
|
+
description: `Import a sprite archive, write its assets, and generate a .tfx
|
|
20
|
+
source file.`,
|
|
21
|
+
arguments: ["<source>"],
|
|
22
|
+
options: [
|
|
23
|
+
{ flags: "--name <name>", description: "Override the sprite name" },
|
|
24
|
+
{ flags: "--force", description: "Overwrite an existing source file" },
|
|
25
|
+
],
|
|
26
|
+
examples: [
|
|
27
|
+
"fx sprite add robot.sprite3",
|
|
28
|
+
"fx sprite add https://example.com/cat.pms --name Cat",
|
|
29
|
+
],
|
|
30
|
+
async run({ args, options, cwd, reporter, product }) {
|
|
31
|
+
const source = args[0];
|
|
32
|
+
const configPath = findConfigPath(cwd);
|
|
33
|
+
const rootDir = spriteRoot(cwd, configPath);
|
|
34
|
+
|
|
35
|
+
const { sprite, assets } = readSprite(await readInputBytes(source, cwd));
|
|
36
|
+
const name = typeof options.name === "string" ? options.name : sprite.name;
|
|
37
|
+
if (!name)
|
|
38
|
+
throw new CliError("sprite add could not read a sprite name", {
|
|
39
|
+
code: "SPRITE_NAME_MISSING",
|
|
40
|
+
hint: "pass --name <name>",
|
|
41
|
+
});
|
|
42
|
+
sprite.name = name;
|
|
43
|
+
|
|
44
|
+
const existing = spriteNames(rootDir).find((entry) => entry.name === name);
|
|
45
|
+
if (existing)
|
|
46
|
+
throw new CliError(
|
|
47
|
+
`A sprite named ${JSON.stringify(name)} already exists in ${existing.file}`,
|
|
48
|
+
{
|
|
49
|
+
code: "SPRITE_EXISTS",
|
|
50
|
+
hint: "remove it first, or choose another name with --name",
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const sourceDir = spriteSourceDir(rootDir, product);
|
|
55
|
+
const file = path.join(
|
|
56
|
+
sourceDir,
|
|
57
|
+
`${slugify(name)}${sourceExtension(product)}`,
|
|
58
|
+
);
|
|
59
|
+
if (fs.existsSync(file) && !options.force)
|
|
60
|
+
throw new CliError(`${file} already exists`, {
|
|
61
|
+
code: "SPRITE_FILE_EXISTS",
|
|
62
|
+
hint: "pass --force to overwrite",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const assetsDir = path.join(rootDir, "assets");
|
|
66
|
+
const relative = path.relative(sourceDir, assetsDir).replace(/\\/g, "/");
|
|
67
|
+
const text = spriteSource(sprite, {
|
|
68
|
+
assetPrefix: relative ? `${relative}/` : "",
|
|
69
|
+
});
|
|
70
|
+
parse(text, file);
|
|
71
|
+
|
|
72
|
+
fs.mkdirSync(assetsDir, { recursive: true });
|
|
73
|
+
for (const [assetName, data] of assets)
|
|
74
|
+
fs.writeFileSync(path.join(assetsDir, assetName), data);
|
|
75
|
+
fs.mkdirSync(sourceDir, { recursive: true });
|
|
76
|
+
fs.writeFileSync(file, text);
|
|
77
|
+
|
|
78
|
+
reporter.success(`Added sprite ${name} -> ${path.relative(rootDir, file)}`);
|
|
79
|
+
return {
|
|
80
|
+
name,
|
|
81
|
+
file,
|
|
82
|
+
assets: [...assets.keys()],
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { build } from "../../../build.js";
|
|
4
|
+
import { outputDirectory } from "../../../brand.js";
|
|
5
|
+
import { slugify } from "../../../package.js";
|
|
6
|
+
import { spriteArchive } from "../../../sprite.js";
|
|
7
|
+
import { loadProjectState } from "../../context.js";
|
|
8
|
+
import { CliError } from "../../errors.js";
|
|
9
|
+
|
|
10
|
+
export const spriteBuildCommand = {
|
|
11
|
+
name: "build",
|
|
12
|
+
summary: "Compile one sprite into a .pms archive",
|
|
13
|
+
description:
|
|
14
|
+
"Build a single sprite from the current project into a .pms file.",
|
|
15
|
+
arguments: ["<name>"],
|
|
16
|
+
options: [{ flags: "-o, --output <file>", description: "Output .pms path" }],
|
|
17
|
+
examples: ["fx sprite build Cat", "fx sprite build Cat -o cat.pms"],
|
|
18
|
+
async run({ args, options, cwd, reporter, prompts }) {
|
|
19
|
+
const name = args[0];
|
|
20
|
+
const state = await loadProjectState({
|
|
21
|
+
input: undefined,
|
|
22
|
+
cwd,
|
|
23
|
+
options,
|
|
24
|
+
reporter,
|
|
25
|
+
prompts,
|
|
26
|
+
});
|
|
27
|
+
const result = build({
|
|
28
|
+
...state.target,
|
|
29
|
+
projectConfig: state.loaded.project,
|
|
30
|
+
fxConfig: state.loaded.fx,
|
|
31
|
+
catalog: state.catalog,
|
|
32
|
+
extensionUrls: state.extensionUrls,
|
|
33
|
+
useConfigFile: false,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const sprite = result.project.targets.find(
|
|
37
|
+
(target) => !target.isStage && target.name === name,
|
|
38
|
+
);
|
|
39
|
+
if (!sprite) {
|
|
40
|
+
const available = result.project.targets
|
|
41
|
+
.filter((target) => !target.isStage)
|
|
42
|
+
.map((target) => target.name);
|
|
43
|
+
throw new CliError(
|
|
44
|
+
`No sprite named ${JSON.stringify(name)}. ` +
|
|
45
|
+
(available.length
|
|
46
|
+
? `Available: ${available.join(", ")}`
|
|
47
|
+
: "The project has no sprites."),
|
|
48
|
+
{ code: "SPRITE_NOT_FOUND" },
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const output =
|
|
53
|
+
options.output !== undefined
|
|
54
|
+
? path.resolve(cwd, options.output)
|
|
55
|
+
: path.join(cwd, outputDirectory(), `${slugify(name)}.pms`);
|
|
56
|
+
fs.mkdirSync(path.dirname(path.resolve(output)), { recursive: true });
|
|
57
|
+
fs.writeFileSync(output, spriteArchive(sprite, result.assets));
|
|
58
|
+
|
|
59
|
+
reporter.success(`Built sprite ${name} -> ${output}`);
|
|
60
|
+
return { name, output };
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parse } from "../../../parser.js";
|
|
4
|
+
import { discoverSources } from "../../../project.js";
|
|
5
|
+
import { sourceDirectory } from "../../../brand.js";
|
|
6
|
+
|
|
7
|
+
/** Read a local file or download a remote sprite archive. */
|
|
8
|
+
export async function readInputBytes(source, cwd = process.cwd()) {
|
|
9
|
+
if (/^https?:\/\//i.test(source)) {
|
|
10
|
+
const response = await fetch(source, {
|
|
11
|
+
signal: AbortSignal.timeout(15000),
|
|
12
|
+
});
|
|
13
|
+
if (!response.ok)
|
|
14
|
+
throw new Error(
|
|
15
|
+
`Could not download ${source}: ${response.status} ${response.statusText}`,
|
|
16
|
+
);
|
|
17
|
+
return Buffer.from(await response.arrayBuffer());
|
|
18
|
+
}
|
|
19
|
+
return fs.readFileSync(path.resolve(cwd, source));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Every sprite declaration under `dir`, in source order. */
|
|
23
|
+
export function spriteNames(dir) {
|
|
24
|
+
const sprites = [];
|
|
25
|
+
for (const file of discoverSources(dir)) {
|
|
26
|
+
const ast = parse(fs.readFileSync(file, "utf8"), file);
|
|
27
|
+
for (const sprite of ast.sprites) {
|
|
28
|
+
sprites.push({ name: sprite.name, file, partial: sprite.partial });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return sprites;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Group sprite declarations by name, tracking the base declaration. */
|
|
35
|
+
export function spriteGroups(dir) {
|
|
36
|
+
const groups = new Map();
|
|
37
|
+
for (const entry of spriteNames(dir)) {
|
|
38
|
+
let group = groups.get(entry.name);
|
|
39
|
+
if (!group) {
|
|
40
|
+
group = {
|
|
41
|
+
name: entry.name,
|
|
42
|
+
file: entry.file,
|
|
43
|
+
files: [],
|
|
44
|
+
partial: true,
|
|
45
|
+
};
|
|
46
|
+
groups.set(entry.name, group);
|
|
47
|
+
}
|
|
48
|
+
group.files.push(entry.file);
|
|
49
|
+
if (!entry.partial) {
|
|
50
|
+
group.file = entry.file;
|
|
51
|
+
group.partial = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const group of groups.values())
|
|
55
|
+
group.files = [...new Set([group.file, ...group.files])];
|
|
56
|
+
return [...groups.values()];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Root directory that owns sources and assets. */
|
|
60
|
+
export function spriteRoot(cwd, configPath) {
|
|
61
|
+
return configPath ? path.dirname(configPath) : cwd;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function spriteSourceDir(rootDir, product) {
|
|
65
|
+
return path.join(rootDir, sourceDirectory(product));
|
|
66
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { defineCommand } from "../../define-command.js";
|
|
3
|
+
import { spriteAddCommand } from "./add.js";
|
|
4
|
+
import { spriteBuildCommand } from "./build.js";
|
|
5
|
+
import { spriteListCommand } from "./list.js";
|
|
6
|
+
import { spriteRemoveCommand } from "./remove.js";
|
|
7
|
+
|
|
8
|
+
const SUBCOMMANDS = [
|
|
9
|
+
spriteAddCommand,
|
|
10
|
+
spriteBuildCommand,
|
|
11
|
+
spriteRemoveCommand,
|
|
12
|
+
spriteListCommand,
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export function registerSprite(program, options) {
|
|
16
|
+
const sprite = new Command("sprite")
|
|
17
|
+
.description("Import, build, list, and remove sprites.")
|
|
18
|
+
.helpGroup("Extension Commands:");
|
|
19
|
+
sprite.copyInheritedSettings(program);
|
|
20
|
+
sprite.addHelpText(
|
|
21
|
+
"after",
|
|
22
|
+
`\nExamples:\n fx sprite add robot.sprite3\n fx sprite build Cat\n fx sprite list --json\n`,
|
|
23
|
+
);
|
|
24
|
+
sprite.action(() => sprite.outputHelp());
|
|
25
|
+
program.addCommand(sprite);
|
|
26
|
+
|
|
27
|
+
for (const spec of SUBCOMMANDS) defineCommand(sprite, spec, options);
|
|
28
|
+
return sprite;
|
|
29
|
+
}
|