@triflare/fx 0.0.0-stage → 1.0.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 +201 -0
- package/README.md +73 -2
- package/bin/fx.js +1166 -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 +551 -0
- package/src/dev.js +390 -0
- package/src/errors.js +56 -0
- package/src/extensions.js +747 -0
- package/src/index.js +35 -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
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comment-preserving edits to `fx.yml`.
|
|
3
|
+
*
|
|
4
|
+
* Parsing and re-dumping YAML loses comments and reformats the whole file, so
|
|
5
|
+
* these helpers edit only the `project.extensions` and `project.unsandboxed`
|
|
6
|
+
* entries. Everything else stays byte for byte as written.
|
|
7
|
+
*/
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import * as yaml from "js-yaml";
|
|
10
|
+
|
|
11
|
+
function splitLines(text) {
|
|
12
|
+
return text.split(/(?<=\n)/);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function content(line) {
|
|
16
|
+
return line.replace(/\r?\n$/, "");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function ending(line) {
|
|
20
|
+
return /\r?\n$/.exec(line)?.[0] ?? "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function eolOf(text) {
|
|
24
|
+
return text.includes("\r\n") ? "\r\n" : "\n";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function topLevel(lines, key) {
|
|
28
|
+
const pattern = new RegExp(`^${key}[ \\t]*:[ \\t]*(.*)$`);
|
|
29
|
+
for (let index = 0; index < lines.length; index++) {
|
|
30
|
+
const match = pattern.exec(content(lines[index]));
|
|
31
|
+
if (match) return { index, rest: match[1] };
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sectionEnd(lines, start) {
|
|
37
|
+
let end = start + 1;
|
|
38
|
+
for (let index = start + 1; index < lines.length; index++) {
|
|
39
|
+
const text = content(lines[index]);
|
|
40
|
+
if (text.trim() === "") continue;
|
|
41
|
+
if (!/^[ \t]/.test(text)) break;
|
|
42
|
+
end = index + 1;
|
|
43
|
+
}
|
|
44
|
+
return end;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function childKey(lines, start, end, key) {
|
|
48
|
+
const pattern = new RegExp(`^([ \\t]+)${key}[ \\t]*:[ \\t]*(.*)$`);
|
|
49
|
+
for (let index = start + 1; index < end; index++) {
|
|
50
|
+
const match = pattern.exec(content(lines[index]));
|
|
51
|
+
if (match) return { index, indent: match[1], rest: match[2] };
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function childIndent(lines, start, end) {
|
|
57
|
+
for (let index = start + 1; index < end; index++) {
|
|
58
|
+
const match = /^([ \t]+)\S/.exec(content(lines[index]));
|
|
59
|
+
if (match) return match[1];
|
|
60
|
+
}
|
|
61
|
+
return " ";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function projectBounds(lines) {
|
|
65
|
+
const project = topLevel(lines, "project");
|
|
66
|
+
if (!project) return null;
|
|
67
|
+
if (project.rest.trim() !== "" && !project.rest.trim().startsWith("#"))
|
|
68
|
+
throw new Error("project must be a block mapping to edit it");
|
|
69
|
+
return { index: project.index, end: sectionEnd(lines, project.index) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Split `value # comment` at the first unquoted comment marker. */
|
|
73
|
+
function splitComment(rest) {
|
|
74
|
+
let quote = null;
|
|
75
|
+
for (let index = 0; index < rest.length; index++) {
|
|
76
|
+
const char = rest[index];
|
|
77
|
+
if (quote === '"') {
|
|
78
|
+
if (char === "\\") index++;
|
|
79
|
+
else if (char === '"') quote = null;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (quote === "'") {
|
|
83
|
+
if (char === "'" && rest[index + 1] === "'") index++;
|
|
84
|
+
else if (char === "'") quote = null;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (char === '"' || char === "'") {
|
|
88
|
+
quote = char;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (char === "#" && (index === 0 || /\s/.test(rest[index - 1]))) {
|
|
92
|
+
return {
|
|
93
|
+
value: rest.slice(0, index).trimEnd(),
|
|
94
|
+
comment: rest.slice(index).trimEnd(),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { value: rest.trimEnd(), comment: "" };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseValue(text) {
|
|
102
|
+
try {
|
|
103
|
+
return yaml.load(text);
|
|
104
|
+
} catch {
|
|
105
|
+
return text;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function quote(value) {
|
|
110
|
+
return JSON.stringify(String(value));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function readList(lines, key, start, end) {
|
|
114
|
+
const keyLine = childKey(lines, start, end, key);
|
|
115
|
+
if (!keyLine) return null;
|
|
116
|
+
const { value: raw, comment } = splitComment(keyLine.rest);
|
|
117
|
+
|
|
118
|
+
if (raw.startsWith("[")) {
|
|
119
|
+
if (!raw.trimEnd().endsWith("]"))
|
|
120
|
+
throw new Error(
|
|
121
|
+
`Multi-line ${key} lists are not supported; edit them by hand`,
|
|
122
|
+
);
|
|
123
|
+
const parsed = parseValue(raw);
|
|
124
|
+
return {
|
|
125
|
+
keyLine,
|
|
126
|
+
style: "inline",
|
|
127
|
+
values: Array.isArray(parsed) ? parsed.map(String) : [],
|
|
128
|
+
comment,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (raw !== "") {
|
|
133
|
+
const parsed = parseValue(raw);
|
|
134
|
+
return {
|
|
135
|
+
keyLine,
|
|
136
|
+
style: "inline",
|
|
137
|
+
values: parsed == null ? [] : [String(parsed)],
|
|
138
|
+
comment,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const items = [];
|
|
143
|
+
let itemIndent = null;
|
|
144
|
+
let lastLine = keyLine.index;
|
|
145
|
+
for (let index = keyLine.index + 1; index < end; index++) {
|
|
146
|
+
const text = content(lines[index]);
|
|
147
|
+
if (text.trim() === "") continue;
|
|
148
|
+
if (/^[ \t]*#/.test(text)) {
|
|
149
|
+
// A comment above the first item belongs to the list.
|
|
150
|
+
if (items.length === 0) lastLine = index;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const match = /^([ \t]*)-[ \t]*(.*)$/.exec(text);
|
|
154
|
+
if (!match || match[1].length <= keyLine.indent.length) break;
|
|
155
|
+
itemIndent ??= match[1];
|
|
156
|
+
if (match[1] !== itemIndent) break;
|
|
157
|
+
const value = splitComment(match[2]).value;
|
|
158
|
+
items.push({ value: String(parseValue(value) ?? ""), line: index });
|
|
159
|
+
lastLine = index;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
keyLine,
|
|
164
|
+
style: "block",
|
|
165
|
+
values: items.map((item) => item.value),
|
|
166
|
+
items,
|
|
167
|
+
itemIndent: itemIndent ?? `${keyLine.indent} `,
|
|
168
|
+
lastLine,
|
|
169
|
+
comment,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function insertAt(lines, index, added, eol) {
|
|
174
|
+
if (index > 0 && !lines[index - 1].endsWith("\n")) lines[index - 1] += eol;
|
|
175
|
+
lines.splice(index, 0, ...added);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function addValue(lines, key, value, eol) {
|
|
179
|
+
const bounds = projectBounds(lines);
|
|
180
|
+
if (!bounds) {
|
|
181
|
+
if (lines.length > 0 && !lines[lines.length - 1].endsWith("\n"))
|
|
182
|
+
lines[lines.length - 1] += eol;
|
|
183
|
+
lines.push(
|
|
184
|
+
`project:${eol}`,
|
|
185
|
+
` ${key}:${eol}`,
|
|
186
|
+
` - ${quote(value)}${eol}`,
|
|
187
|
+
);
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const list = readList(lines, key, bounds.index, bounds.end);
|
|
192
|
+
if (list && list.values.includes(value)) return false;
|
|
193
|
+
|
|
194
|
+
if (!list) {
|
|
195
|
+
const indent = childIndent(lines, bounds.index, bounds.end);
|
|
196
|
+
insertAt(
|
|
197
|
+
lines,
|
|
198
|
+
bounds.end,
|
|
199
|
+
[`${indent}${key}:${eol}`, `${indent} - ${quote(value)}${eol}`],
|
|
200
|
+
eol,
|
|
201
|
+
);
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (list.style === "block") {
|
|
206
|
+
insertAt(
|
|
207
|
+
lines,
|
|
208
|
+
list.lastLine + 1,
|
|
209
|
+
[`${list.itemIndent}- ${quote(value)}${eol}`],
|
|
210
|
+
eol,
|
|
211
|
+
);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const line = lines[list.keyLine.index];
|
|
216
|
+
lines[list.keyLine.index] =
|
|
217
|
+
`${list.keyLine.indent}${key}: [${[...list.values, value]
|
|
218
|
+
.map(quote)
|
|
219
|
+
.join(", ")}]` +
|
|
220
|
+
`${list.comment ? ` ${list.comment}` : ""}${ending(line) || eol}`;
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function removeValue(lines, key, matches) {
|
|
225
|
+
const bounds = projectBounds(lines);
|
|
226
|
+
if (!bounds) return [];
|
|
227
|
+
const list = readList(lines, key, bounds.index, bounds.end);
|
|
228
|
+
if (!list) return [];
|
|
229
|
+
|
|
230
|
+
const removed = list.values.filter((value) => matches(value));
|
|
231
|
+
if (removed.length === 0) return [];
|
|
232
|
+
|
|
233
|
+
if (list.style === "block") {
|
|
234
|
+
const doomed = list.items
|
|
235
|
+
.filter((item) => matches(item.value))
|
|
236
|
+
.map((item) => item.line)
|
|
237
|
+
.sort((a, b) => b - a);
|
|
238
|
+
for (const line of doomed) lines.splice(line, 1);
|
|
239
|
+
if (list.items.length === doomed.length) {
|
|
240
|
+
const line = lines[list.keyLine.index];
|
|
241
|
+
lines[list.keyLine.index] =
|
|
242
|
+
`${list.keyLine.indent}${key}: []` +
|
|
243
|
+
`${list.comment ? ` ${list.comment}` : ""}${ending(line) || "\n"}`;
|
|
244
|
+
}
|
|
245
|
+
return removed;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const kept = list.values.filter((value) => !matches(value));
|
|
249
|
+
const line = lines[list.keyLine.index];
|
|
250
|
+
lines[list.keyLine.index] =
|
|
251
|
+
`${list.keyLine.indent}${key}: [${kept.map(quote).join(", ")}]` +
|
|
252
|
+
`${list.comment ? ` ${list.comment}` : ""}${ending(line) || "\n"}`;
|
|
253
|
+
return removed;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function writeDocument(configPath, text) {
|
|
257
|
+
try {
|
|
258
|
+
yaml.load(text);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
throw new Error(`Refusing to write invalid YAML: ${error.message}`, {
|
|
261
|
+
cause: error,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
fs.writeFileSync(configPath, text);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function stringList(value) {
|
|
268
|
+
if (Array.isArray(value)) return value.map(String);
|
|
269
|
+
if (value != null) return [String(value)];
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Basename of an extension path or URL, without `.js`, used as its id. */
|
|
274
|
+
export function extensionKey(source) {
|
|
275
|
+
const clean = String(source).split(/[?#]/)[0];
|
|
276
|
+
const base = clean.split(/[\\/]/).pop() || "";
|
|
277
|
+
return base.replace(/\.js$/i, "");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Read the extension lists without normalising the rest of the config. */
|
|
281
|
+
export function readProjectExtensions(configPath) {
|
|
282
|
+
const raw = yaml.load(fs.readFileSync(configPath, "utf8")) ?? {};
|
|
283
|
+
if (typeof raw !== "object" || Array.isArray(raw))
|
|
284
|
+
throw new Error("fx.yml must contain a mapping at the top level");
|
|
285
|
+
const project =
|
|
286
|
+
raw.project && typeof raw.project === "object" ? raw.project : {};
|
|
287
|
+
return {
|
|
288
|
+
extensions: stringList(project.extensions),
|
|
289
|
+
unsandboxed: stringList(project.unsandboxed),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Add an extension source to `project.extensions`, and optionally its id to
|
|
295
|
+
* `project.unsandboxed`.
|
|
296
|
+
* @returns {{added: boolean, unsandboxedAdded: boolean}}
|
|
297
|
+
*/
|
|
298
|
+
export function addProjectExtension(configPath, source, { unsandboxed } = {}) {
|
|
299
|
+
const text = fs.readFileSync(configPath, "utf8");
|
|
300
|
+
const lines = splitLines(text);
|
|
301
|
+
const eol = eolOf(text);
|
|
302
|
+
|
|
303
|
+
const added = addValue(lines, "extensions", source, eol);
|
|
304
|
+
const unsandboxedAdded = unsandboxed
|
|
305
|
+
? addValue(lines, "unsandboxed", unsandboxed, eol)
|
|
306
|
+
: false;
|
|
307
|
+
if (added || unsandboxedAdded) writeDocument(configPath, lines.join(""));
|
|
308
|
+
return { added, unsandboxedAdded };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Remove an extension by its source, path/URL basename, or unsandboxed id.
|
|
313
|
+
* @returns {{extensions: string[], unsandboxedRemoved: boolean}}
|
|
314
|
+
*/
|
|
315
|
+
export function removeProjectExtension(configPath, target) {
|
|
316
|
+
const text = fs.readFileSync(configPath, "utf8");
|
|
317
|
+
const lines = splitLines(text);
|
|
318
|
+
|
|
319
|
+
const removed = removeValue(
|
|
320
|
+
lines,
|
|
321
|
+
"extensions",
|
|
322
|
+
(entry) => entry === target || extensionKey(entry) === target,
|
|
323
|
+
);
|
|
324
|
+
const removedKeys = new Set(removed.map(extensionKey));
|
|
325
|
+
const unsandboxedRemoved = removeValue(
|
|
326
|
+
lines,
|
|
327
|
+
"unsandboxed",
|
|
328
|
+
(id) => id === target || removedKeys.has(id),
|
|
329
|
+
);
|
|
330
|
+
if (removed.length === 0 && unsandboxedRemoved.length === 0)
|
|
331
|
+
throw new Error(`No extension matching ${JSON.stringify(target)}`);
|
|
332
|
+
|
|
333
|
+
writeDocument(configPath, lines.join(""));
|
|
334
|
+
return {
|
|
335
|
+
extensions: removed,
|
|
336
|
+
unsandboxedRemoved: unsandboxedRemoved.length > 0,
|
|
337
|
+
};
|
|
338
|
+
}
|