@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
package/src/dev.js
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Development server for `fx dev`.
|
|
3
|
+
*
|
|
4
|
+
* The server builds through a caller-provided callback, compares the finished
|
|
5
|
+
* HTML with the previous result, and only tells connected browsers to reload
|
|
6
|
+
* when the bytes actually changed. Build errors keep the last good page and
|
|
7
|
+
* are reported over Server-Sent Events.
|
|
8
|
+
*/
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import http from "node:http";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
|
|
14
|
+
const IGNORED_SEGMENTS = new Set([
|
|
15
|
+
"node_modules",
|
|
16
|
+
".git",
|
|
17
|
+
"build",
|
|
18
|
+
"dist",
|
|
19
|
+
"coverage",
|
|
20
|
+
".cache",
|
|
21
|
+
".smoke",
|
|
22
|
+
".vscode",
|
|
23
|
+
".vscode-test",
|
|
24
|
+
".vscode-test-web",
|
|
25
|
+
"vendor",
|
|
26
|
+
"reference",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const DEV_EVENTS_PATH = "/__fx/events";
|
|
30
|
+
const DEV_STATUS_PATH = "/__fx/status";
|
|
31
|
+
|
|
32
|
+
function isWildcardHost(host) {
|
|
33
|
+
return host === "0.0.0.0" || host === "::" || host === "[::]";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function urlHost(host) {
|
|
37
|
+
if (host.includes(":") && !host.startsWith("[")) return `[${host}]`;
|
|
38
|
+
return host;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ignoredFile(filename) {
|
|
42
|
+
if (!filename) return false;
|
|
43
|
+
return filename.split(/[\\/]/).some((segment) => {
|
|
44
|
+
if (!segment) return false;
|
|
45
|
+
if (IGNORED_SEGMENTS.has(segment)) return true;
|
|
46
|
+
return segment.startsWith(".") && segment !== ".";
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function escapeHtml(text) {
|
|
51
|
+
return String(text)
|
|
52
|
+
.replaceAll("&", "&")
|
|
53
|
+
.replaceAll("<", "<")
|
|
54
|
+
.replaceAll(">", ">")
|
|
55
|
+
.replaceAll('"', """);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {string} html
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
export function injectDevClient(html) {
|
|
63
|
+
if (html.includes("data-fx-dev")) return html;
|
|
64
|
+
const script = `<script data-fx-dev>
|
|
65
|
+
(() => {
|
|
66
|
+
if (window.__fxDevClient) return;
|
|
67
|
+
window.__fxDevClient = true;
|
|
68
|
+
let banner = null;
|
|
69
|
+
const showError = (message) => {
|
|
70
|
+
if (!banner) {
|
|
71
|
+
banner = document.createElement("pre");
|
|
72
|
+
banner.id = "__fx_dev_error";
|
|
73
|
+
banner.style.cssText = "position:fixed;left:0;right:0;bottom:0;max-height:45vh;overflow:auto;margin:0;padding:12px;background:#210b0b;color:#ffb4b4;border-top:1px solid #ff6b6b;font:12px/1.45 ui-monospace,monospace;white-space:pre-wrap;z-index:2147483647";
|
|
74
|
+
document.body.appendChild(banner);
|
|
75
|
+
}
|
|
76
|
+
banner.textContent = "Build failed\\n\\n" + message;
|
|
77
|
+
};
|
|
78
|
+
const source = new EventSource(${JSON.stringify(DEV_EVENTS_PATH)});
|
|
79
|
+
source.addEventListener("reload", () => location.reload());
|
|
80
|
+
source.addEventListener("build-error", (event) => showError(event.data));
|
|
81
|
+
source.addEventListener("connected", () => {
|
|
82
|
+
if (banner) banner.remove();
|
|
83
|
+
banner = null;
|
|
84
|
+
});
|
|
85
|
+
})();
|
|
86
|
+
</script>`;
|
|
87
|
+
if (/<\/body\s*>/i.test(html))
|
|
88
|
+
return html.replace(/<\/body\s*>/i, `${script}\n</body>`);
|
|
89
|
+
return `${html}${html.endsWith("\n") ? "" : "\n"}${script}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* URLs to print for a listening dev server.
|
|
94
|
+
* @param {{host: string, port: number}} options
|
|
95
|
+
* @returns {{local: string[], network: string[]}}
|
|
96
|
+
*/
|
|
97
|
+
export function formatDevUrls({ host, port }) {
|
|
98
|
+
if (!isWildcardHost(host)) {
|
|
99
|
+
return {
|
|
100
|
+
local: [`http://${urlHost(host)}:${port}/`],
|
|
101
|
+
network: [],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const local = [`http://localhost:${port}/`];
|
|
106
|
+
const network = [];
|
|
107
|
+
for (const addresses of Object.values(os.networkInterfaces())) {
|
|
108
|
+
for (const address of addresses || []) {
|
|
109
|
+
if (address.internal || address.family !== "IPv4") continue;
|
|
110
|
+
network.push(`http://${address.address}:${port}/`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { local, network: [...new Set(network)] };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeHtml(result) {
|
|
117
|
+
if (typeof result === "string") return result;
|
|
118
|
+
if (Buffer.isBuffer(result) || result instanceof Uint8Array)
|
|
119
|
+
return Buffer.from(result).toString("utf8");
|
|
120
|
+
if (result && typeof result.html === "string") return result.html;
|
|
121
|
+
throw new Error("fx dev build callback must return HTML text");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function writeEvent(response, event, data) {
|
|
125
|
+
response.write(`event: ${event}\n`);
|
|
126
|
+
for (const line of String(data).split("\n"))
|
|
127
|
+
response.write(`data: ${line}\n`);
|
|
128
|
+
response.write("\n");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function errorPage(error) {
|
|
132
|
+
const message = error?.stack || error?.message || "Unknown build error";
|
|
133
|
+
return `<!doctype html>
|
|
134
|
+
<html>
|
|
135
|
+
<head><meta charset="utf-8"><title>Build failed</title></head>
|
|
136
|
+
<body style="margin:0;padding:24px;background:#160d0d;color:#ffb4b4;font:14px/1.5 ui-monospace,monospace">
|
|
137
|
+
<h1 style="font-size:18px">Build failed</h1>
|
|
138
|
+
<pre style="white-space:pre-wrap">${escapeHtml(message)}</pre>
|
|
139
|
+
</body>
|
|
140
|
+
</html>`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function loadingPage() {
|
|
144
|
+
return `<!doctype html>
|
|
145
|
+
<html>
|
|
146
|
+
<head><meta charset="utf-8"><title>Building…</title></head>
|
|
147
|
+
<body style="margin:0;padding:24px;background:#0e0e12;color:#ddd;font:14px/1.5 ui-monospace,monospace">
|
|
148
|
+
Building…
|
|
149
|
+
</body>
|
|
150
|
+
</html>`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Start the dev server and watch a directory for changes.
|
|
155
|
+
*
|
|
156
|
+
* @param {object} options
|
|
157
|
+
* @param {string} options.watchDir Directory to watch recursively.
|
|
158
|
+
* @param {() => Promise<string|Buffer|{html: string}>} options.build Build callback.
|
|
159
|
+
* @param {string} [options.host="127.0.0.1"]
|
|
160
|
+
* @param {number} [options.port=3000] Port 0 asks the OS for a free port.
|
|
161
|
+
* @param {boolean} [options.strictPort=false]
|
|
162
|
+
* @param {number} [options.debounce=75]
|
|
163
|
+
* @param {object} [options.log] Logger with error/success/warn/info methods.
|
|
164
|
+
* @returns {Promise<object>}
|
|
165
|
+
*/
|
|
166
|
+
export async function startDevServer(options = {}) {
|
|
167
|
+
const watchDir = path.resolve(options.watchDir || process.cwd());
|
|
168
|
+
if (!fs.existsSync(watchDir))
|
|
169
|
+
throw new Error(`Watch directory does not exist: ${watchDir}`);
|
|
170
|
+
if (typeof options.build !== "function")
|
|
171
|
+
throw new Error("startDevServer requires a build callback");
|
|
172
|
+
|
|
173
|
+
const host =
|
|
174
|
+
options.host === true || options.host == null
|
|
175
|
+
? "127.0.0.1"
|
|
176
|
+
: String(options.host);
|
|
177
|
+
const requestedPort = options.port == null ? 3000 : Number(options.port);
|
|
178
|
+
if (
|
|
179
|
+
!Number.isInteger(requestedPort) ||
|
|
180
|
+
requestedPort < 0 ||
|
|
181
|
+
requestedPort > 65535
|
|
182
|
+
)
|
|
183
|
+
throw new Error(`Invalid port: ${options.port}`);
|
|
184
|
+
const strictPort = Boolean(options.strictPort);
|
|
185
|
+
const debounce = options.debounce ?? 75;
|
|
186
|
+
const log = options.log || {};
|
|
187
|
+
const clients = new Set();
|
|
188
|
+
|
|
189
|
+
let currentHtml = null;
|
|
190
|
+
let currentError = null;
|
|
191
|
+
let previousHtml = null;
|
|
192
|
+
let hasBuilt = false;
|
|
193
|
+
let closed = false;
|
|
194
|
+
let building = false;
|
|
195
|
+
let queued = false;
|
|
196
|
+
let timer = null;
|
|
197
|
+
let watcher = null;
|
|
198
|
+
|
|
199
|
+
function broadcast(event, data) {
|
|
200
|
+
for (const client of clients) {
|
|
201
|
+
try {
|
|
202
|
+
writeEvent(client, event, data);
|
|
203
|
+
} catch {
|
|
204
|
+
clients.delete(client);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function runBuild() {
|
|
210
|
+
if (closed) return;
|
|
211
|
+
if (building) {
|
|
212
|
+
queued = true;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
building = true;
|
|
216
|
+
const recovering = currentError != null;
|
|
217
|
+
try {
|
|
218
|
+
const html = normalizeHtml(await options.build());
|
|
219
|
+
const changed = html !== previousHtml;
|
|
220
|
+
previousHtml = html;
|
|
221
|
+
currentHtml = html;
|
|
222
|
+
if (currentError) currentError = null;
|
|
223
|
+
if (changed || recovering) {
|
|
224
|
+
broadcast("reload", String(Date.now()));
|
|
225
|
+
if (hasBuilt) log.success?.("Reloaded browser");
|
|
226
|
+
hasBuilt = true;
|
|
227
|
+
}
|
|
228
|
+
} catch (err) {
|
|
229
|
+
currentError = err instanceof Error ? err : new Error(String(err));
|
|
230
|
+
log.error?.(`Build failed: ${currentError.message}`);
|
|
231
|
+
broadcast("build-error", currentError.stack || currentError.message);
|
|
232
|
+
} finally {
|
|
233
|
+
building = false;
|
|
234
|
+
if (queued) {
|
|
235
|
+
queued = false;
|
|
236
|
+
schedule(0);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function schedule(delay = debounce) {
|
|
242
|
+
if (closed) return;
|
|
243
|
+
clearTimeout(timer);
|
|
244
|
+
timer = setTimeout(() => {
|
|
245
|
+
timer = null;
|
|
246
|
+
void runBuild();
|
|
247
|
+
}, delay);
|
|
248
|
+
timer.unref?.();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const server = http.createServer((request, response) => {
|
|
252
|
+
const url = new URL(request.url || "/", "http://localhost");
|
|
253
|
+
|
|
254
|
+
if (url.pathname === DEV_EVENTS_PATH) {
|
|
255
|
+
response.writeHead(200, {
|
|
256
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
257
|
+
"Cache-Control": "no-cache, no-transform",
|
|
258
|
+
Connection: "keep-alive",
|
|
259
|
+
"X-Accel-Buffering": "no",
|
|
260
|
+
});
|
|
261
|
+
response.write("retry: 500\n\n");
|
|
262
|
+
writeEvent(response, "connected", String(Date.now()));
|
|
263
|
+
clients.add(response);
|
|
264
|
+
response.on("close", () => clients.delete(response));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (url.pathname === DEV_STATUS_PATH) {
|
|
269
|
+
response.writeHead(200, {
|
|
270
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
271
|
+
"Cache-Control": "no-store",
|
|
272
|
+
});
|
|
273
|
+
response.end(
|
|
274
|
+
`${JSON.stringify({
|
|
275
|
+
ok: !currentError,
|
|
276
|
+
error: currentError ? currentError.message : null,
|
|
277
|
+
html: currentHtml != null,
|
|
278
|
+
})}\n`,
|
|
279
|
+
);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (url.pathname !== "/" && url.pathname !== "/index.html") {
|
|
284
|
+
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
285
|
+
response.end("Not found\n");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const html =
|
|
290
|
+
currentHtml ?? (currentError ? errorPage(currentError) : loadingPage());
|
|
291
|
+
response.writeHead(200, {
|
|
292
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
293
|
+
"Cache-Control": "no-store",
|
|
294
|
+
});
|
|
295
|
+
response.end(injectDevClient(html));
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
let port;
|
|
299
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
300
|
+
try {
|
|
301
|
+
port = await new Promise((resolve, reject) => {
|
|
302
|
+
const onError = (error) => {
|
|
303
|
+
server.off("listening", onListening);
|
|
304
|
+
reject(error);
|
|
305
|
+
};
|
|
306
|
+
const onListening = () => {
|
|
307
|
+
server.off("error", onError);
|
|
308
|
+
resolve(server.address().port);
|
|
309
|
+
};
|
|
310
|
+
server.once("error", onError);
|
|
311
|
+
server.once("listening", onListening);
|
|
312
|
+
server.listen(requestedPort === 0 ? 0 : requestedPort + attempt, host);
|
|
313
|
+
});
|
|
314
|
+
break;
|
|
315
|
+
} catch (error) {
|
|
316
|
+
if (
|
|
317
|
+
error.code !== "EADDRINUSE" ||
|
|
318
|
+
strictPort ||
|
|
319
|
+
requestedPort === 0 ||
|
|
320
|
+
attempt >= 20
|
|
321
|
+
)
|
|
322
|
+
throw error;
|
|
323
|
+
log.warn?.(
|
|
324
|
+
`Port ${requestedPort + attempt} is in use, trying ${
|
|
325
|
+
requestedPort + attempt + 1
|
|
326
|
+
}…`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
watcher = fs.watch(watchDir, { recursive: true }, (_event, filename) => {
|
|
333
|
+
if (!ignoredFile(filename)) schedule();
|
|
334
|
+
});
|
|
335
|
+
} catch (error) {
|
|
336
|
+
await new Promise((resolve) => server.close(resolve));
|
|
337
|
+
throw new Error(
|
|
338
|
+
`Could not watch ${watchDir}: ${error.message}. ` +
|
|
339
|
+
"Recursive file watching needs Node.js 22 or newer.",
|
|
340
|
+
{ cause: error },
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const keepAlive = setInterval(() => {
|
|
345
|
+
for (const client of clients) {
|
|
346
|
+
try {
|
|
347
|
+
client.write(": ping\n\n");
|
|
348
|
+
} catch {
|
|
349
|
+
clients.delete(client);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}, 25000);
|
|
353
|
+
keepAlive.unref();
|
|
354
|
+
|
|
355
|
+
const ready = runBuild();
|
|
356
|
+
const urls = formatDevUrls({ host, port });
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
server,
|
|
360
|
+
host,
|
|
361
|
+
port,
|
|
362
|
+
url: urls.local[0],
|
|
363
|
+
localUrls: urls.local,
|
|
364
|
+
networkUrls: urls.network,
|
|
365
|
+
ready,
|
|
366
|
+
get html() {
|
|
367
|
+
return currentHtml;
|
|
368
|
+
},
|
|
369
|
+
get error() {
|
|
370
|
+
return currentError;
|
|
371
|
+
},
|
|
372
|
+
async close() {
|
|
373
|
+
if (closed) return;
|
|
374
|
+
closed = true;
|
|
375
|
+
clearTimeout(timer);
|
|
376
|
+
clearInterval(keepAlive);
|
|
377
|
+
watcher?.close();
|
|
378
|
+
for (const client of clients) {
|
|
379
|
+
try {
|
|
380
|
+
client.end();
|
|
381
|
+
} catch {
|
|
382
|
+
// already closed
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
clients.clear();
|
|
386
|
+
await new Promise((resolve) => server.close(resolve));
|
|
387
|
+
server.closeAllConnections?.();
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared error types and a small diagnostic formatter for Triflare Fx.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class FxError extends Error {
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} message
|
|
8
|
+
* @param {{line?: number, column?: number, file?: string, source?: string}} [info]
|
|
9
|
+
*/
|
|
10
|
+
constructor(message, info = {}) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "FxError";
|
|
13
|
+
this.line = info.line;
|
|
14
|
+
this.column = info.column;
|
|
15
|
+
this.file = info.file;
|
|
16
|
+
this.source = info.source;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Human readable, single/multi-line diagnostic with a source caret. */
|
|
20
|
+
format() {
|
|
21
|
+
const where = this.file ? `${this.file}` : "<input>";
|
|
22
|
+
const loc =
|
|
23
|
+
this.line != null ? `${where}:${this.line}:${this.column ?? 0}` : where;
|
|
24
|
+
const head = `${this.name}: ${this.message}\n --> ${loc}`;
|
|
25
|
+
if (this.source && this.line != null) {
|
|
26
|
+
const lines = this.source.split(/\r?\n/);
|
|
27
|
+
const text = lines[this.line - 1];
|
|
28
|
+
if (text != null) {
|
|
29
|
+
const caret = " ".repeat(Math.max(0, (this.column ?? 1) - 1)) + "^";
|
|
30
|
+
return `${head}\n${this.line} | ${text}\n${" ".repeat(String(this.line).length)} | ${caret}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return head;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class LexError extends FxError {
|
|
38
|
+
constructor(message, info) {
|
|
39
|
+
super(message, info);
|
|
40
|
+
this.name = "LexError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class ParseError extends FxError {
|
|
45
|
+
constructor(message, info) {
|
|
46
|
+
super(message, info);
|
|
47
|
+
this.name = "ParseError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class CompileError extends FxError {
|
|
52
|
+
constructor(message, info) {
|
|
53
|
+
super(message, info);
|
|
54
|
+
this.name = "CompileError";
|
|
55
|
+
}
|
|
56
|
+
}
|