@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/src/config.js
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fx.yml` project configuration and PenguinMod config emission.
|
|
3
|
+
*
|
|
4
|
+
* PenguinMod stores per-project runtime settings (framerate, stage size, turbo,
|
|
5
|
+
* interpolation, ...) inside a stage comment whose final line ends with the
|
|
6
|
+
* magic string ` // _twconfig_` followed by JSON. We reproduce that format
|
|
7
|
+
* exactly so the settings round-trip through PenguinMod's own loader. The
|
|
8
|
+
* `_twconfig_` spelling is PenguinMod's on-disk format and must not change.
|
|
9
|
+
*/
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import * as yaml from "js-yaml";
|
|
13
|
+
import { configFilename, configFilenames, productName } from "./brand.js";
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_PROJECT = {
|
|
16
|
+
name: `${productName()} Project`,
|
|
17
|
+
framerate: 30,
|
|
18
|
+
width: 480,
|
|
19
|
+
height: 360,
|
|
20
|
+
turbo: false,
|
|
21
|
+
interpolation: false,
|
|
22
|
+
hq: false,
|
|
23
|
+
runtime: {},
|
|
24
|
+
extensions: [],
|
|
25
|
+
unsandboxed: [],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Fx-only settings that never reach PenguinMod's project runtime config.
|
|
30
|
+
* Kept in a separate top-level `fx:` block in fx.yml.
|
|
31
|
+
*/
|
|
32
|
+
export const DEFAULT_FX = {
|
|
33
|
+
partialSprites: false,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Packaging defaults. These never reach PenguinMod itself; they are handed to
|
|
38
|
+
* the PenguinMod Packager when `fx package` or `fx dev` runs.
|
|
39
|
+
*/
|
|
40
|
+
export const DEFAULT_PACKAGE = {
|
|
41
|
+
target: "html",
|
|
42
|
+
output: null,
|
|
43
|
+
extract: false,
|
|
44
|
+
name: null,
|
|
45
|
+
version: null,
|
|
46
|
+
icon: null,
|
|
47
|
+
custom: { js: "", css: "", html: "" },
|
|
48
|
+
options: {},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Friendly target names mapped onto the packager's environment identifiers. */
|
|
52
|
+
export const PACKAGE_TARGET_ALIASES = {
|
|
53
|
+
html: "html",
|
|
54
|
+
zip: "zip",
|
|
55
|
+
"zip-one-asset": "zip-one-asset",
|
|
56
|
+
windows: "electron-win64",
|
|
57
|
+
win: "electron-win64",
|
|
58
|
+
macos: "electron-mac",
|
|
59
|
+
mac: "electron-mac",
|
|
60
|
+
linux: "electron-linux64",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Top-level packager options we accept directly inside `package:`. Everything
|
|
65
|
+
* else belongs under `package.options`.
|
|
66
|
+
*/
|
|
67
|
+
const PACKAGE_OPTION_KEYS = new Set([
|
|
68
|
+
"turbo",
|
|
69
|
+
"interpolation",
|
|
70
|
+
"framerate",
|
|
71
|
+
"highQualityPen",
|
|
72
|
+
"maxClones",
|
|
73
|
+
"fencing",
|
|
74
|
+
"miscLimits",
|
|
75
|
+
"dangerousOptimizations",
|
|
76
|
+
"stageWidth",
|
|
77
|
+
"stageHeight",
|
|
78
|
+
"resizeMode",
|
|
79
|
+
"autoplay",
|
|
80
|
+
"username",
|
|
81
|
+
"closeWhenStopped",
|
|
82
|
+
"projectId",
|
|
83
|
+
"appearance",
|
|
84
|
+
"loadingScreen",
|
|
85
|
+
"controls",
|
|
86
|
+
"monitors",
|
|
87
|
+
"penguinmod",
|
|
88
|
+
"compiler",
|
|
89
|
+
"packagedRuntime",
|
|
90
|
+
"optimizeCode",
|
|
91
|
+
"removeDefaultFonts",
|
|
92
|
+
"app",
|
|
93
|
+
"chunks",
|
|
94
|
+
"cloudVariables",
|
|
95
|
+
"cursor",
|
|
96
|
+
"extensions",
|
|
97
|
+
"bakeExtensions",
|
|
98
|
+
"maxTextureDimension",
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
const PACKAGE_SHORTHANDS = {
|
|
102
|
+
fps: "framerate",
|
|
103
|
+
width: "stageWidth",
|
|
104
|
+
height: "stageHeight",
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const PACKAGE_SPECIAL_KEYS = new Set([
|
|
108
|
+
"target",
|
|
109
|
+
"output",
|
|
110
|
+
"extract",
|
|
111
|
+
"name",
|
|
112
|
+
"version",
|
|
113
|
+
"icon",
|
|
114
|
+
"custom",
|
|
115
|
+
"options",
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
function isPlainObject(value) {
|
|
119
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function mergePlain(target, source) {
|
|
123
|
+
for (const [key, value] of Object.entries(source || {})) {
|
|
124
|
+
if (isPlainObject(value) && isPlainObject(target[key]))
|
|
125
|
+
mergePlain(target[key], value);
|
|
126
|
+
else target[key] = value;
|
|
127
|
+
}
|
|
128
|
+
return target;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** @param {unknown} value @param {string} label */
|
|
132
|
+
function normalizeTextSource(value, label) {
|
|
133
|
+
if (value == null) return "";
|
|
134
|
+
if (typeof value === "string") return value;
|
|
135
|
+
if (isPlainObject(value) && typeof value.file === "string" && value.file)
|
|
136
|
+
return { file: value.file };
|
|
137
|
+
throw new Error(
|
|
138
|
+
`${configFilename()}: ${label} must be text or { file: "<path>" }`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {unknown} value
|
|
144
|
+
* @returns {string}
|
|
145
|
+
*/
|
|
146
|
+
export function normalizePackageTarget(value) {
|
|
147
|
+
if (value == null || value === "") return DEFAULT_PACKAGE.target;
|
|
148
|
+
if (typeof value !== "string")
|
|
149
|
+
throw new Error(`${configFilename()}: package.target must be a string`);
|
|
150
|
+
const key = value.toLowerCase();
|
|
151
|
+
return PACKAGE_TARGET_ALIASES[key] ?? key;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {object} raw parsed YAML `package` mapping
|
|
156
|
+
* @returns {typeof DEFAULT_PACKAGE}
|
|
157
|
+
*/
|
|
158
|
+
export function normalizePackage(raw = {}) {
|
|
159
|
+
if (!isPlainObject(raw))
|
|
160
|
+
throw new Error(`${configFilename()}: "package" must be a mapping`);
|
|
161
|
+
|
|
162
|
+
const unknown = Object.keys(raw).filter(
|
|
163
|
+
(key) =>
|
|
164
|
+
!PACKAGE_SPECIAL_KEYS.has(key) &&
|
|
165
|
+
!PACKAGE_OPTION_KEYS.has(key) &&
|
|
166
|
+
!Object.hasOwn(PACKAGE_SHORTHANDS, key),
|
|
167
|
+
);
|
|
168
|
+
if (unknown.length)
|
|
169
|
+
throw new Error(
|
|
170
|
+
`${configFilename()}: unknown package setting(s): ${unknown
|
|
171
|
+
.map((key) => `"${key}"`)
|
|
172
|
+
.join(", ")}. Put raw packager options under "package.options".`,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const pkg = {
|
|
176
|
+
...DEFAULT_PACKAGE,
|
|
177
|
+
custom: { ...DEFAULT_PACKAGE.custom },
|
|
178
|
+
options: {},
|
|
179
|
+
};
|
|
180
|
+
pkg.target = normalizePackageTarget(raw.target);
|
|
181
|
+
if (raw.output != null) pkg.output = String(raw.output);
|
|
182
|
+
if (raw.extract != null) pkg.extract = Boolean(raw.extract);
|
|
183
|
+
if (raw.name != null) pkg.name = String(raw.name);
|
|
184
|
+
if (raw.version != null) pkg.version = String(raw.version);
|
|
185
|
+
if (raw.icon != null) {
|
|
186
|
+
if (typeof raw.icon === "string" && raw.icon) pkg.icon = raw.icon;
|
|
187
|
+
else if (isPlainObject(raw.icon) && typeof raw.icon.file === "string")
|
|
188
|
+
pkg.icon = raw.icon.file;
|
|
189
|
+
else
|
|
190
|
+
throw new Error(
|
|
191
|
+
`${configFilename()}: package.icon must be a file path or { file: "<path>" }`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (raw.custom != null) {
|
|
196
|
+
if (!isPlainObject(raw.custom))
|
|
197
|
+
throw new Error(
|
|
198
|
+
`${configFilename()}: "package.custom" must be a mapping`,
|
|
199
|
+
);
|
|
200
|
+
const unknownCustom = Object.keys(raw.custom).filter(
|
|
201
|
+
(key) => !["js", "css", "html"].includes(key),
|
|
202
|
+
);
|
|
203
|
+
if (unknownCustom.length)
|
|
204
|
+
throw new Error(
|
|
205
|
+
`${configFilename()}: unknown package.custom setting(s): ${unknownCustom
|
|
206
|
+
.map((key) => `"${key}"`)
|
|
207
|
+
.join(", ")}`,
|
|
208
|
+
);
|
|
209
|
+
for (const key of ["js", "css", "html"])
|
|
210
|
+
pkg.custom[key] = normalizeTextSource(
|
|
211
|
+
raw.custom[key],
|
|
212
|
+
`package.custom.${key}`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (const [key, option] of Object.entries(PACKAGE_SHORTHANDS)) {
|
|
217
|
+
if (raw[key] !== undefined) pkg.options[option] = raw[key];
|
|
218
|
+
}
|
|
219
|
+
for (const key of PACKAGE_OPTION_KEYS) {
|
|
220
|
+
if (raw[key] === undefined) continue;
|
|
221
|
+
if (isPlainObject(raw[key]) && isPlainObject(pkg.options[key]))
|
|
222
|
+
mergePlain(pkg.options[key], raw[key]);
|
|
223
|
+
else pkg.options[key] = raw[key];
|
|
224
|
+
}
|
|
225
|
+
if (raw.options != null) {
|
|
226
|
+
if (!isPlainObject(raw.options))
|
|
227
|
+
throw new Error(
|
|
228
|
+
`${configFilename()}: "package.options" must be a mapping`,
|
|
229
|
+
);
|
|
230
|
+
mergePlain(pkg.options, raw.options);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return pkg;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** PenguinMod's stage-comment magic string (unchanged on disk). */
|
|
237
|
+
export const CONFIG_MAGIC = " // _twconfig_";
|
|
238
|
+
|
|
239
|
+
const COMMENT_PREAMBLE =
|
|
240
|
+
"Configuration for https://penguinmod.com/\n" +
|
|
241
|
+
"You can move, resize, and minimize this comment, but don't edit it by hand. " +
|
|
242
|
+
"This comment can be deleted to remove the stored settings.";
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* @param {object} raw parsed YAML `project` mapping
|
|
246
|
+
* @returns {typeof DEFAULT_PROJECT}
|
|
247
|
+
*/
|
|
248
|
+
export function normalizeProject(raw = {}) {
|
|
249
|
+
const project = { ...DEFAULT_PROJECT };
|
|
250
|
+
|
|
251
|
+
if (raw.name != null) project.name = String(raw.name);
|
|
252
|
+
if (raw.fps != null) project.framerate = Number(raw.fps);
|
|
253
|
+
else if (raw.framerate != null) project.framerate = Number(raw.framerate);
|
|
254
|
+
if (raw.width != null) project.width = Number(raw.width);
|
|
255
|
+
if (raw.height != null) project.height = Number(raw.height);
|
|
256
|
+
if (raw.turbo != null) project.turbo = Boolean(raw.turbo);
|
|
257
|
+
if (raw.interpolation != null)
|
|
258
|
+
project.interpolation = Boolean(raw.interpolation);
|
|
259
|
+
else if (raw.interpolate != null)
|
|
260
|
+
project.interpolation = Boolean(raw.interpolate);
|
|
261
|
+
if (raw.hq != null || raw.hqPen != null)
|
|
262
|
+
project.hq = Boolean(raw.hq ?? raw.hqPen);
|
|
263
|
+
if (raw.runtime && typeof raw.runtime === "object")
|
|
264
|
+
project.runtime = raw.runtime;
|
|
265
|
+
if (Array.isArray(raw.extensions))
|
|
266
|
+
project.extensions = raw.extensions.map(String);
|
|
267
|
+
if (Array.isArray(raw.unsandboxed))
|
|
268
|
+
project.unsandboxed = raw.unsandboxed.map(String);
|
|
269
|
+
else if (raw.unsandboxed != null)
|
|
270
|
+
project.unsandboxed = [String(raw.unsandboxed)];
|
|
271
|
+
|
|
272
|
+
return project;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* @param {object} raw parsed YAML `fx` mapping
|
|
277
|
+
* @returns {typeof DEFAULT_FX}
|
|
278
|
+
*/
|
|
279
|
+
export function normalizeFx(raw = {}) {
|
|
280
|
+
const fx = { ...DEFAULT_FX };
|
|
281
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
282
|
+
if (raw.partialSprites != null)
|
|
283
|
+
fx.partialSprites = Boolean(raw.partialSprites);
|
|
284
|
+
}
|
|
285
|
+
return fx;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Parse a YAML config string.
|
|
290
|
+
* @param {string} text
|
|
291
|
+
* @returns {{project: typeof DEFAULT_PROJECT, fx: typeof DEFAULT_FX, raw: object}}
|
|
292
|
+
*/
|
|
293
|
+
export function parseConfig(text) {
|
|
294
|
+
const raw = yaml.load(text) || {};
|
|
295
|
+
if (typeof raw !== "object" || Array.isArray(raw))
|
|
296
|
+
throw new Error(
|
|
297
|
+
`${configFilename()} must contain a mapping at the top level`,
|
|
298
|
+
);
|
|
299
|
+
if (raw.fx != null && (typeof raw.fx !== "object" || Array.isArray(raw.fx)))
|
|
300
|
+
throw new Error(`${configFilename()} must contain an "fx" mapping`);
|
|
301
|
+
return {
|
|
302
|
+
project: normalizeProject(raw.project || {}),
|
|
303
|
+
fx: normalizeFx(raw.fx || {}),
|
|
304
|
+
package: normalizePackage(raw.package || {}),
|
|
305
|
+
raw,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Find the nearest `fx.yml`/`fx.yaml`, walking up from `dir`.
|
|
311
|
+
* This lets a project keep its config at the repository root while sources
|
|
312
|
+
* live in `src/`.
|
|
313
|
+
* @param {string} dir
|
|
314
|
+
* @returns {string|null}
|
|
315
|
+
*/
|
|
316
|
+
export function findConfigPath(dir) {
|
|
317
|
+
let current = path.resolve(dir);
|
|
318
|
+
for (;;) {
|
|
319
|
+
for (const name of configFilenames()) {
|
|
320
|
+
const candidate = path.join(current, name);
|
|
321
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
322
|
+
}
|
|
323
|
+
const parent = path.dirname(current);
|
|
324
|
+
if (parent === current) return null;
|
|
325
|
+
current = parent;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Find and load `fx.yml`/`fx.yaml` from a directory (or any of
|
|
331
|
+
* its ancestors).
|
|
332
|
+
* @param {string} dir
|
|
333
|
+
* @returns {{project: typeof DEFAULT_PROJECT, fx: typeof DEFAULT_FX, package: typeof DEFAULT_PACKAGE, raw: object, path: string|null}}
|
|
334
|
+
*/
|
|
335
|
+
export function loadConfigFrom(dir) {
|
|
336
|
+
const found = findConfigPath(dir);
|
|
337
|
+
if (found) {
|
|
338
|
+
const {
|
|
339
|
+
project,
|
|
340
|
+
fx,
|
|
341
|
+
package: pkg,
|
|
342
|
+
raw,
|
|
343
|
+
} = parseConfig(fs.readFileSync(found, "utf8"));
|
|
344
|
+
return { project, fx, package: pkg, raw, path: found };
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
project: { ...DEFAULT_PROJECT },
|
|
348
|
+
fx: { ...DEFAULT_FX },
|
|
349
|
+
package: normalizePackage({}),
|
|
350
|
+
raw: {},
|
|
351
|
+
path: null,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Build the differing-from-default config payload. Matches the shape produced
|
|
357
|
+
* by PenguinMod's `runtime.storeProjectOptions()`.
|
|
358
|
+
* @param {typeof DEFAULT_PROJECT} project
|
|
359
|
+
* @returns {object}
|
|
360
|
+
*/
|
|
361
|
+
export function configPayload(project) {
|
|
362
|
+
const out = {};
|
|
363
|
+
if (project.framerate && project.framerate !== DEFAULT_PROJECT.framerate)
|
|
364
|
+
out.framerate = project.framerate;
|
|
365
|
+
if (project.turbo) out.turbo = true;
|
|
366
|
+
if (project.interpolation) out.interpolation = true;
|
|
367
|
+
if (project.hq) out.hq = true;
|
|
368
|
+
if (project.width && project.width !== DEFAULT_PROJECT.width)
|
|
369
|
+
out.width = project.width;
|
|
370
|
+
if (project.height && project.height !== DEFAULT_PROJECT.height)
|
|
371
|
+
out.height = project.height;
|
|
372
|
+
|
|
373
|
+
const defaultRuntime = { maxClones: 300, miscLimits: true, fencing: true };
|
|
374
|
+
const runtime = {};
|
|
375
|
+
for (const [key, value] of Object.entries(project.runtime || {})) {
|
|
376
|
+
if (value !== defaultRuntime[key]) runtime[key] = value;
|
|
377
|
+
}
|
|
378
|
+
if (Object.keys(runtime).length) out.runtimeOptions = runtime;
|
|
379
|
+
|
|
380
|
+
return out;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Full text for the stage config comment, or null when everything is at its
|
|
385
|
+
* default.
|
|
386
|
+
* @param {typeof DEFAULT_PROJECT} project
|
|
387
|
+
* @returns {string|null}
|
|
388
|
+
*/
|
|
389
|
+
export function configComment(project) {
|
|
390
|
+
const payload = configPayload(project);
|
|
391
|
+
if (Object.keys(payload).length === 0) return null;
|
|
392
|
+
return `${COMMENT_PREAMBLE}\n${JSON.stringify(payload)}${CONFIG_MAGIC}`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export {
|
|
396
|
+
addProjectExtension,
|
|
397
|
+
extensionKey,
|
|
398
|
+
readProjectExtensions,
|
|
399
|
+
removeProjectExtension,
|
|
400
|
+
} from "./config-edit.js";
|