thrunium 1.2.0-rc.1 → 1.2.0-rc.2
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 +10 -3
- package/dist/cli.mjs +236 -60
- package/dist/thrunium.js +15 -5
- package/dist/thrunium.min.js +2 -2
- package/dist/thrunium.min.js.map +2 -2
- package/dist/types/game.d.ts +29 -12
- package/dist/types/reference.d.ts +1 -1
- package/package.json +1 -1
- package/templates/ts/tsconfig.json +1 -1
- package/templates/js/index.html +0 -6
- package/templates/ts/index.html +0 -6
- package/templates/ts/pnpm-workspace.yaml +0 -4
- /package/templates/js/{src/main.js → game.js} +0 -0
- /package/templates/{js → pnpm}/pnpm-workspace.yaml +0 -0
- /package/templates/ts/{src/main.ts → game.ts} +0 -0
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ The engine is also on npm as `thrunium` (Node 24 or newer):
|
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
37
|
npx thrunium init my-game # new TypeScript project; add --js for JavaScript
|
|
38
|
-
cd my-game &&
|
|
38
|
+
cd my-game && npm run dev # init installs for you; use pnpm dev, yarn dev or bun run dev
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
```ts
|
|
@@ -57,10 +57,17 @@ Commands (`thrunium --help`, `--version`):
|
|
|
57
57
|
|---|---|
|
|
58
58
|
| `thrunium dev` | `--port <n>` (default 3000, tries the next 10 ports if busy), `--host <name>` (default `localhost`), `--no-open` |
|
|
59
59
|
| `thrunium build` | `--out <dir>` (default `dist`), `--engine cdn\|bundle` |
|
|
60
|
-
| `thrunium init [dir]` | `--js` (TypeScript by default)
|
|
60
|
+
| `thrunium init [dir]` | `--js` (TypeScript by default), `--pm npm\|pnpm\|yarn\|bun` (default: the one that ran it, else npm), `--no-install`; the folder must be empty. Installs by default; if that fails the project is kept and the command is printed |
|
|
61
|
+
|
|
62
|
+
A project needs no `index.html`: without one, `dev` and `build` generate the page
|
|
63
|
+
from `game.json` (`entry`, else the first of `game.ts`, `game.js`, `src/game.ts`,
|
|
64
|
+
`src/game.js`; the title is `name`, else the folder name). A hand-written
|
|
65
|
+
`index.html` is used as written, and having both it and `entry` is an error.
|
|
66
|
+
`import './style.css'` from game code is bundled and linked, with `url()` fonts and
|
|
67
|
+
images; remote `@import` stays remote.
|
|
61
68
|
|
|
62
69
|
`dev` never uses the CDN. For a project with a module entry
|
|
63
|
-
(`<script type="module" src=...>` importing `thrunium`), `build` has two engine
|
|
70
|
+
(a generated page, or `<script type="module" src=...>` importing `thrunium`), `build` has two engine
|
|
64
71
|
modes. `cdn` (the default) adds a script tag for
|
|
65
72
|
`https://cdn.thrungames.com/thrunium/v<version>/thrunium.min.js` with a `sha384`
|
|
66
73
|
integrity hash, pinned to the installed version. It fails, and leaves the output
|
package/dist/cli.mjs
CHANGED
|
@@ -152,8 +152,27 @@ function findModuleEntries(html) {
|
|
|
152
152
|
function hasEngineTag(html) {
|
|
153
153
|
return scriptTags(html).some((t) => ENGINE_SRC.test(t.attrs.get("src") ?? ""));
|
|
154
154
|
}
|
|
155
|
+
function escapeHtml(text) {
|
|
156
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
157
|
+
}
|
|
158
|
+
function generatePage(title, entry) {
|
|
159
|
+
return `<!DOCTYPE html>
|
|
160
|
+
<html lang="en">
|
|
161
|
+
<head>
|
|
162
|
+
<meta charset="utf-8">
|
|
163
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
164
|
+
<title>${escapeHtml(title)}</title>
|
|
165
|
+
</head>
|
|
166
|
+
<body style="margin: 0; padding: 0;">
|
|
167
|
+
<script type="module" src="${escapeHtml(entry)}"></script>
|
|
168
|
+
</body>
|
|
169
|
+
</html>
|
|
170
|
+
`;
|
|
171
|
+
}
|
|
155
172
|
function transformHtml(html, opts) {
|
|
156
173
|
let out = html;
|
|
174
|
+
const hasHead = /<\/head\s*>/i.test(html);
|
|
175
|
+
const styleLinks = opts.styles.map((href) => `<link rel="stylesheet" href="${escapeHtml(href)}">`).join("");
|
|
157
176
|
const tags = localModuleTags(html);
|
|
158
177
|
for (let i = tags.length - 1; i >= 0; i--) {
|
|
159
178
|
const tag = tags[i];
|
|
@@ -163,9 +182,16 @@ function transformHtml(html, opts) {
|
|
|
163
182
|
if (mapped !== void 0) {
|
|
164
183
|
text = text.replace(/(\ssrc\s*=\s*)(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+)/i, (_m, pre) => `${pre}"${mapped}"`);
|
|
165
184
|
}
|
|
166
|
-
if (i === 0
|
|
185
|
+
if (i === 0) {
|
|
186
|
+
if (opts.engineTag !== null) text = opts.engineTag + text;
|
|
187
|
+
if (!hasHead) text = styleLinks + text;
|
|
188
|
+
}
|
|
167
189
|
out = out.slice(0, tag.index) + text + out.slice(tag.index + tag.text.length);
|
|
168
190
|
}
|
|
191
|
+
if (hasHead && styleLinks !== "") {
|
|
192
|
+
const at = /<\/head\s*>/i.exec(out).index;
|
|
193
|
+
out = out.slice(0, at) + styleLinks + out.slice(at);
|
|
194
|
+
}
|
|
169
195
|
if (opts.inject !== null) {
|
|
170
196
|
const at = [/<\/body\s*>/i, /<\/html\s*>/i].map((re) => re.exec(out)?.index).find((i) => i !== void 0);
|
|
171
197
|
out = at === void 0 ? out + opts.inject : out.slice(0, at) + opts.inject + out.slice(at);
|
|
@@ -183,8 +209,8 @@ var init_html = __esm({
|
|
|
183
209
|
});
|
|
184
210
|
|
|
185
211
|
// cli/project.ts
|
|
186
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
187
|
-
import { join as join2 } from "node:path";
|
|
212
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
213
|
+
import { basename, isAbsolute, join as join2, normalize, resolve } from "node:path";
|
|
188
214
|
function resolveEngineMode(root, flag) {
|
|
189
215
|
if (flag) return flag;
|
|
190
216
|
const manifest = join2(root, "package.json");
|
|
@@ -200,25 +226,91 @@ function resolveEngineMode(root, flag) {
|
|
|
200
226
|
if (value === "cdn" || value === "bundle") return value;
|
|
201
227
|
throw new UserError(`Invalid "thrunium.engine" in package.json: ${JSON.stringify(value)}. Use "cdn" or "bundle".`);
|
|
202
228
|
}
|
|
203
|
-
function
|
|
229
|
+
function isFile(path) {
|
|
230
|
+
return existsSync2(path) && statSync(path).isFile();
|
|
231
|
+
}
|
|
232
|
+
function readGameJson(root) {
|
|
233
|
+
const file = join2(root, "game.json");
|
|
234
|
+
if (!existsSync2(file)) return null;
|
|
235
|
+
let json;
|
|
236
|
+
try {
|
|
237
|
+
json = JSON.parse(readFileSync2(file, "utf8"));
|
|
238
|
+
} catch {
|
|
239
|
+
throw new UserError("game.json is not valid JSON.");
|
|
240
|
+
}
|
|
241
|
+
if (typeof json !== "object" || json === null || Array.isArray(json)) {
|
|
242
|
+
throw new UserError("game.json must contain a JSON object.");
|
|
243
|
+
}
|
|
244
|
+
const { name, entry } = json;
|
|
245
|
+
if (name !== void 0 && typeof name !== "string") throw new UserError('"name" in game.json must be a string.');
|
|
246
|
+
if (entry !== void 0 && typeof entry !== "string") throw new UserError('"entry" in game.json must be a string.');
|
|
247
|
+
return { name, entry };
|
|
248
|
+
}
|
|
249
|
+
function validateEntry(root, entry) {
|
|
250
|
+
const inside = !isAbsolute(entry) && !normalize(entry).split(/[\\/]/).includes("..");
|
|
251
|
+
if (!inside) throw new UserError(`"entry" in game.json must be a relative path inside the project: ${entry}`);
|
|
252
|
+
if (!ENTRY_EXTENSIONS.test(entry)) {
|
|
253
|
+
throw new UserError(`"entry" in game.json must end in .ts, .js, .mts or .mjs: ${entry}`);
|
|
254
|
+
}
|
|
255
|
+
if (!isFile(join2(root, entry))) throw new UserError(`"entry" in game.json points to ${entry}, which does not exist.`);
|
|
256
|
+
}
|
|
257
|
+
function hasEntryKey(file) {
|
|
258
|
+
try {
|
|
259
|
+
const json = JSON.parse(readFileSync2(file, "utf8"));
|
|
260
|
+
return typeof json === "object" && json !== null && !Array.isArray(json) && "entry" in json;
|
|
261
|
+
} catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function resolvePage(root) {
|
|
204
266
|
const indexPath = join2(root, "index.html");
|
|
205
|
-
if (
|
|
206
|
-
|
|
267
|
+
if (existsSync2(indexPath)) {
|
|
268
|
+
if (hasEntryKey(join2(root, "game.json"))) {
|
|
269
|
+
throw new UserError('game.json has "entry", but index.html also exists and is used instead. Remove one of them.');
|
|
270
|
+
}
|
|
271
|
+
return { html: readFileSync2(indexPath, "utf8"), htmlSource: "file" };
|
|
272
|
+
}
|
|
273
|
+
const game = readGameJson(root);
|
|
274
|
+
let entry = game?.entry;
|
|
275
|
+
if (entry !== void 0) validateEntry(root, entry);
|
|
276
|
+
else entry = DEFAULT_ENTRIES.find((candidate) => isFile(join2(root, candidate)));
|
|
277
|
+
if (entry === void 0) {
|
|
278
|
+
throw new UserError(
|
|
279
|
+
`No game found: add game.ts or game.js (or set "entry" in game.json), or an index.html. Looked for: ${DEFAULT_ENTRIES.join(", ")}.`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
return { html: generatePage(game?.name ?? basename(resolve(root)), entry), htmlSource: "generated" };
|
|
283
|
+
}
|
|
284
|
+
function loadProject(root, engineFlag) {
|
|
285
|
+
const { html, htmlSource } = resolvePage(root);
|
|
207
286
|
const entries = findModuleEntries(html);
|
|
208
287
|
for (const entry of entries) {
|
|
209
|
-
if (!existsSync2(join2(root, entry)))
|
|
288
|
+
if (!existsSync2(join2(root, entry))) {
|
|
289
|
+
throw new UserError(`Entry file ${entry} (from ${htmlSource === "file" ? "index.html" : "game.json"}) does not exist.`);
|
|
290
|
+
}
|
|
210
291
|
}
|
|
211
292
|
const usesModules = entries.length > 0;
|
|
212
293
|
if (usesModules && hasEngineTag(html)) {
|
|
213
294
|
throw new UserError("Remove the Thrunium <script> tag from index.html; thrunium adds it for you.");
|
|
214
295
|
}
|
|
215
|
-
return { root, html, entries, usesModules, engineMode: resolveEngineMode(root, engineFlag) };
|
|
296
|
+
return { root, html, htmlSource, entries, usesModules, engineMode: resolveEngineMode(root, engineFlag) };
|
|
216
297
|
}
|
|
217
298
|
function thruniumPlugin() {
|
|
218
299
|
return {
|
|
219
300
|
name: "thrunium",
|
|
220
301
|
setup(build2) {
|
|
221
302
|
build2.onResolve({ filter: /^thrunium$/ }, () => ({ path: globalShimFile() }));
|
|
303
|
+
const assetFilter = new RegExp(`(${ASSET_EXTENSIONS.map((e) => e.replace(".", "\\.")).join("|")})$`, "i");
|
|
304
|
+
build2.onResolve({ filter: assetFilter }, (args) => {
|
|
305
|
+
if (args.kind === "url-token" || args.kind === "import-rule") return void 0;
|
|
306
|
+
return {
|
|
307
|
+
errors: [
|
|
308
|
+
{
|
|
309
|
+
text: `Cannot import ${args.path} from game code. Load the file by path instead, for example with game.assets.`
|
|
310
|
+
}
|
|
311
|
+
]
|
|
312
|
+
};
|
|
313
|
+
});
|
|
222
314
|
}
|
|
223
315
|
};
|
|
224
316
|
}
|
|
@@ -229,15 +321,22 @@ function baseBuildOptions(root) {
|
|
|
229
321
|
format: "esm",
|
|
230
322
|
target: "es2017",
|
|
231
323
|
plugins: [thruniumPlugin()],
|
|
324
|
+
// CSS imported from game code is bundled; fonts and images it references
|
|
325
|
+
// are emitted as files (hashed names in build) rather than inlined.
|
|
326
|
+
loader: Object.fromEntries(ASSET_EXTENSIONS.map((ext) => [ext, "file"])),
|
|
232
327
|
logLevel: "silent"
|
|
233
328
|
};
|
|
234
329
|
}
|
|
330
|
+
var DEFAULT_ENTRIES, ENTRY_EXTENSIONS, ASSET_EXTENSIONS;
|
|
235
331
|
var init_project = __esm({
|
|
236
332
|
"cli/project.ts"() {
|
|
237
333
|
"use strict";
|
|
238
334
|
init_engine();
|
|
239
335
|
init_errors();
|
|
240
336
|
init_html();
|
|
337
|
+
DEFAULT_ENTRIES = ["game.ts", "game.js", "src/game.ts", "src/game.js"];
|
|
338
|
+
ENTRY_EXTENSIONS = /\.(ts|js|mts|mjs)$/;
|
|
339
|
+
ASSET_EXTENSIONS = [".woff", ".woff2", ".ttf", ".otf", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"];
|
|
241
340
|
}
|
|
242
341
|
});
|
|
243
342
|
|
|
@@ -282,11 +381,11 @@ __export(dev_exports, {
|
|
|
282
381
|
startDev: () => startDev
|
|
283
382
|
});
|
|
284
383
|
import { spawn } from "node:child_process";
|
|
285
|
-
import { existsSync as existsSync3, readdirSync, realpathSync, statSync, watch } from "node:fs";
|
|
384
|
+
import { existsSync as existsSync3, readdirSync, realpathSync, statSync as statSync2, watch } from "node:fs";
|
|
286
385
|
import { readFile } from "node:fs/promises";
|
|
287
386
|
import { createServer } from "node:http";
|
|
288
387
|
import { isIP } from "node:net";
|
|
289
|
-
import { basename, extname, isAbsolute, join as join3, normalize, relative, resolve, sep } from "node:path";
|
|
388
|
+
import { basename as basename2, extname, isAbsolute as isAbsolute2, join as join3, normalize as normalize2, relative, resolve as resolve2, sep } from "node:path";
|
|
290
389
|
import { parseArgs } from "node:util";
|
|
291
390
|
import { context as esbuildContext, formatMessages } from "esbuild";
|
|
292
391
|
function openBrowser(url) {
|
|
@@ -322,7 +421,7 @@ async function listen(server, port, host) {
|
|
|
322
421
|
throw new UserError(`Ports ${port}-${last} are all in use. Pass a different --port.`);
|
|
323
422
|
}
|
|
324
423
|
async function startDev(opts) {
|
|
325
|
-
const root =
|
|
424
|
+
const root = resolve2(opts.root);
|
|
326
425
|
const host = opts.host ?? "localhost";
|
|
327
426
|
const outDir = join3(root, "dist");
|
|
328
427
|
const realRoot = realpathSync(root);
|
|
@@ -355,7 +454,7 @@ data: ${JSON.stringify(data)}
|
|
|
355
454
|
entries = newEntries;
|
|
356
455
|
buildError = null;
|
|
357
456
|
bundles.clear();
|
|
358
|
-
owned = new Set(entries.map((e) =>
|
|
457
|
+
owned = new Set(entries.map((e) => resolve2(root, e)));
|
|
359
458
|
if (entries.length === 0) return;
|
|
360
459
|
let first = true;
|
|
361
460
|
let firstDone;
|
|
@@ -383,12 +482,12 @@ data: ${JSON.stringify(data)}
|
|
|
383
482
|
} else {
|
|
384
483
|
buildError = null;
|
|
385
484
|
const next = /* @__PURE__ */ new Map();
|
|
386
|
-
for (const f of result.outputFiles ?? []) next.set(f.path.slice(virtualOut.length + 1), f.contents);
|
|
485
|
+
for (const f of result.outputFiles ?? []) next.set(f.path.slice(virtualOut.length + 1).split(sep).join("/"), f.contents);
|
|
387
486
|
const changed = next.size !== bundles.size || [...next].some(([k, v]) => !Buffer.from(v).equals(bundles.get(k) ?? new Uint8Array()));
|
|
388
487
|
bundles.clear();
|
|
389
488
|
for (const [k, v] of next) bundles.set(k, v);
|
|
390
|
-
owned = new Set(Object.keys(result.metafile?.inputs ?? {}).map((p) =>
|
|
391
|
-
for (const e of entries) owned.add(
|
|
489
|
+
owned = new Set(Object.keys(result.metafile?.inputs ?? {}).map((p) => resolve2(root, p)));
|
|
490
|
+
for (const e of entries) owned.add(resolve2(root, e));
|
|
392
491
|
if (!wasFirst && (changed || lastError !== null)) broadcastReload();
|
|
393
492
|
}
|
|
394
493
|
if (wasFirst) firstDone();
|
|
@@ -440,11 +539,13 @@ data: ${JSON.stringify(data)}
|
|
|
440
539
|
}
|
|
441
540
|
if (pathname === "/" || pathname === "/index.html") {
|
|
442
541
|
const project = loadProject(root);
|
|
443
|
-
const entryMap = new Map(project.entries.map((e, i) => [e,
|
|
542
|
+
const entryMap = new Map(project.entries.map((e, i) => [e, `${BUNDLE_PREFIX}${i}.js`]));
|
|
543
|
+
const styles = project.entries.flatMap((_, i) => bundles.has(`${i}.css`) ? [`${BUNDLE_PREFIX}${i}.css`] : []);
|
|
444
544
|
const html = transformHtml(project.html, {
|
|
445
545
|
engineTag: project.usesModules ? ENGINE_TAG : null,
|
|
446
546
|
entryMap,
|
|
447
|
-
inject: RELOAD_CLIENT
|
|
547
|
+
inject: RELOAD_CLIENT,
|
|
548
|
+
styles
|
|
448
549
|
});
|
|
449
550
|
res.writeHead(200, { ...noCache, "content-type": MIME[".html"] }).end(html);
|
|
450
551
|
return;
|
|
@@ -453,25 +554,26 @@ data: ${JSON.stringify(data)}
|
|
|
453
554
|
res.writeHead(200, { ...noCache, "content-type": MIME[".js"] }).end(await readFile(engineFile("thrunium.js")));
|
|
454
555
|
return;
|
|
455
556
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
const contents = bundles.get(
|
|
557
|
+
if (pathname.startsWith(BUNDLE_PREFIX)) {
|
|
558
|
+
const name = pathname.slice(BUNDLE_PREFIX.length);
|
|
559
|
+
const contents = bundles.get(name);
|
|
459
560
|
if (!contents) {
|
|
460
561
|
res.writeHead(404).end();
|
|
461
562
|
return;
|
|
462
563
|
}
|
|
463
|
-
|
|
564
|
+
const type = MIME[extname(name).toLowerCase()] ?? "application/octet-stream";
|
|
565
|
+
res.writeHead(200, { ...noCache, "content-type": type }).end(contents);
|
|
464
566
|
return;
|
|
465
567
|
}
|
|
466
|
-
const file =
|
|
568
|
+
const file = normalize2(join3(root, pathname));
|
|
467
569
|
const rel = relative(root, file);
|
|
468
570
|
if (/[\\\0]/.test(pathname) || // backslashes act as separators on Windows; NUL is never valid
|
|
469
|
-
rel.split(/[\\/]/).some((part) => part.startsWith(".")) || rel === "" || rel === ".." || rel.startsWith(".." + sep) ||
|
|
571
|
+
rel.split(/[\\/]/).some((part) => part.startsWith(".")) || rel === "" || rel === ".." || rel.startsWith(".." + sep) || isAbsolute2(rel) || !existsSync3(file) || !statSync2(file).isFile()) {
|
|
470
572
|
res.writeHead(404).end();
|
|
471
573
|
return;
|
|
472
574
|
}
|
|
473
575
|
const realRel = relative(realRoot, realpathSync(file));
|
|
474
|
-
if (realRel === ".." || realRel.startsWith(".." + sep) ||
|
|
576
|
+
if (realRel === ".." || realRel.startsWith(".." + sep) || isAbsolute2(realRel)) {
|
|
475
577
|
res.writeHead(404).end();
|
|
476
578
|
return;
|
|
477
579
|
}
|
|
@@ -483,7 +585,7 @@ data: ${JSON.stringify(data)}
|
|
|
483
585
|
for (const dir of [outDir, outTmp]) if (abs === dir || abs.startsWith(dir + sep)) return true;
|
|
484
586
|
if (owned.has(abs)) return true;
|
|
485
587
|
try {
|
|
486
|
-
return
|
|
588
|
+
return statSync2(abs).isDirectory();
|
|
487
589
|
} catch {
|
|
488
590
|
return false;
|
|
489
591
|
}
|
|
@@ -548,8 +650,8 @@ data: ${JSON.stringify(data)}
|
|
|
548
650
|
const w = watch(dir, { recursive: true }, (_event, filename) => {
|
|
549
651
|
if (!filename) return schedule();
|
|
550
652
|
const name = filename.toString();
|
|
551
|
-
if (name ===
|
|
552
|
-
if (!ignored(
|
|
653
|
+
if (name === basename2(dir) && !existsSync3(join3(dir, name))) return;
|
|
654
|
+
if (!ignored(resolve2(dir, name))) schedule();
|
|
553
655
|
});
|
|
554
656
|
w.on("error", onWatchError(dir));
|
|
555
657
|
watchers.set(dir, w);
|
|
@@ -559,9 +661,10 @@ data: ${JSON.stringify(data)}
|
|
|
559
661
|
};
|
|
560
662
|
const rootWatcher = watch(root, (event, filename) => {
|
|
561
663
|
if (event === "rename") rescan();
|
|
562
|
-
const abs = filename ?
|
|
563
|
-
|
|
564
|
-
if (!
|
|
664
|
+
const abs = filename ? resolve2(root, filename.toString()) : root;
|
|
665
|
+
const pageInput = !filename || abs === join3(root, "index.html") || abs === join3(root, "game.json");
|
|
666
|
+
if (!pageInput && ignored(abs)) return;
|
|
667
|
+
if (pageInput) indexChanged = true;
|
|
565
668
|
schedule();
|
|
566
669
|
});
|
|
567
670
|
rootWatcher.on("error", onWatchError(root));
|
|
@@ -638,7 +741,7 @@ ${USAGE}`);
|
|
|
638
741
|
});
|
|
639
742
|
await server.close();
|
|
640
743
|
}
|
|
641
|
-
var USAGE, MIME, ENGINE_TAG,
|
|
744
|
+
var USAGE, MIME, ENGINE_TAG, BUNDLE_PREFIX, WATCH_DEBOUNCE_MS, PORT_ATTEMPTS;
|
|
642
745
|
var init_dev = __esm({
|
|
643
746
|
"cli/dev.ts"() {
|
|
644
747
|
"use strict";
|
|
@@ -669,10 +772,15 @@ Options:
|
|
|
669
772
|
".ogg": "audio/ogg",
|
|
670
773
|
".css": "text/css",
|
|
671
774
|
".ts": "text/javascript",
|
|
672
|
-
".wasm": "application/wasm"
|
|
775
|
+
".wasm": "application/wasm",
|
|
776
|
+
".woff": "font/woff",
|
|
777
|
+
".woff2": "font/woff2",
|
|
778
|
+
".ttf": "font/ttf",
|
|
779
|
+
".otf": "font/otf",
|
|
780
|
+
".webp": "image/webp"
|
|
673
781
|
};
|
|
674
782
|
ENGINE_TAG = '<script src="/__thrunium/engine/thrunium.js"></script>';
|
|
675
|
-
|
|
783
|
+
BUNDLE_PREFIX = "/__thrunium/bundle/";
|
|
676
784
|
WATCH_DEBOUNCE_MS = 50;
|
|
677
785
|
PORT_ATTEMPTS = 11;
|
|
678
786
|
}
|
|
@@ -684,8 +792,8 @@ __export(build_exports, {
|
|
|
684
792
|
build: () => build,
|
|
685
793
|
runBuild: () => runBuild
|
|
686
794
|
});
|
|
687
|
-
import { existsSync as existsSync4, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync3, renameSync, rmSync, statSync as
|
|
688
|
-
import { dirname as dirname2, isAbsolute as
|
|
795
|
+
import { existsSync as existsSync4, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync3, renameSync, rmSync, statSync as statSync3, writeFileSync, copyFileSync } from "node:fs";
|
|
796
|
+
import { dirname as dirname2, isAbsolute as isAbsolute3, join as join4, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
|
689
797
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
690
798
|
import { build as esbuild, formatMessages as formatMessages2 } from "esbuild";
|
|
691
799
|
function listFiles(root, dir, skipDirs) {
|
|
@@ -699,10 +807,10 @@ function listFiles(root, dir, skipDirs) {
|
|
|
699
807
|
return files;
|
|
700
808
|
}
|
|
701
809
|
async function runBuild(opts) {
|
|
702
|
-
const root =
|
|
703
|
-
const outDir =
|
|
810
|
+
const root = resolve3(opts.root);
|
|
811
|
+
const outDir = resolve3(root, opts.outDir ?? "dist");
|
|
704
812
|
const rel = relative2(root, outDir);
|
|
705
|
-
if (rel === "" || rel === ".." || rel.startsWith(".." + sep2) ||
|
|
813
|
+
if (rel === "" || rel === ".." || rel.startsWith(".." + sep2) || isAbsolute3(rel)) {
|
|
706
814
|
throw new UserError(`The output folder must be inside the project and not the project itself: ${outDir}`);
|
|
707
815
|
}
|
|
708
816
|
const project = loadProject(root, opts.engine);
|
|
@@ -717,6 +825,7 @@ async function runBuild(opts) {
|
|
|
717
825
|
const written = /* @__PURE__ */ new Map();
|
|
718
826
|
const consumed = /* @__PURE__ */ new Set();
|
|
719
827
|
const entryMap = /* @__PURE__ */ new Map();
|
|
828
|
+
const styles = [];
|
|
720
829
|
if (project.usesModules) {
|
|
721
830
|
const outAbs = join4(root, ".thrunium-out");
|
|
722
831
|
const result = await esbuild({
|
|
@@ -726,6 +835,8 @@ async function runBuild(opts) {
|
|
|
726
835
|
sourcemap: "linked",
|
|
727
836
|
outdir: outAbs,
|
|
728
837
|
entryNames: "assets/[name]-[hash]",
|
|
838
|
+
assetNames: "assets/[name]-[hash]",
|
|
839
|
+
// fonts/images referenced by CSS or code
|
|
729
840
|
metafile: true,
|
|
730
841
|
write: false
|
|
731
842
|
}).catch(async (err) => {
|
|
@@ -736,9 +847,16 @@ async function runBuild(opts) {
|
|
|
736
847
|
});
|
|
737
848
|
for (const f of result.outputFiles) written.set(relative2(outAbs, f.path).split(sep2).join("/"), f.contents);
|
|
738
849
|
for (const [file, meta] of Object.entries(result.metafile.outputs)) {
|
|
739
|
-
if (meta.entryPoint) entryMap.set(meta.entryPoint, relative2(outAbs,
|
|
850
|
+
if (meta.entryPoint) entryMap.set(meta.entryPoint, relative2(outAbs, resolve3(root, file)).split(sep2).join("/"));
|
|
851
|
+
}
|
|
852
|
+
for (const entry of project.entries) {
|
|
853
|
+
const output = Object.entries(result.metafile.outputs).find(([, meta]) => meta.entryPoint === entry);
|
|
854
|
+
const cssBundle = output?.[1].cssBundle;
|
|
855
|
+
if (cssBundle) styles.push(relative2(outAbs, resolve3(root, cssBundle)).split(sep2).join("/"));
|
|
856
|
+
}
|
|
857
|
+
for (const input of Object.keys(result.metafile.inputs)) {
|
|
858
|
+
if (!ASSET_EXTENSIONS.some((ext) => input.toLowerCase().endsWith(ext))) consumed.add(resolve3(root, input));
|
|
740
859
|
}
|
|
741
|
-
for (const input of Object.keys(result.metafile.inputs)) consumed.add(resolve2(root, input));
|
|
742
860
|
}
|
|
743
861
|
let engineTag = null;
|
|
744
862
|
if (project.usesModules) {
|
|
@@ -752,13 +870,13 @@ async function runBuild(opts) {
|
|
|
752
870
|
}
|
|
753
871
|
}
|
|
754
872
|
const tmp = outDir + ".tmp";
|
|
755
|
-
written.set("index.html", transformHtml(project.html, { engineTag, entryMap, inject: null }));
|
|
873
|
+
written.set("index.html", transformHtml(project.html, { engineTag, entryMap, inject: null, styles }));
|
|
756
874
|
rmSync(tmp, { recursive: true, force: true });
|
|
757
875
|
try {
|
|
758
876
|
const skipDirs = /* @__PURE__ */ new Set([outDir, tmp]);
|
|
759
877
|
for (const file of listFiles(root, root, skipDirs)) {
|
|
760
878
|
const key = file.split(sep2).join("/");
|
|
761
|
-
if (written.has(key) || consumed.has(
|
|
879
|
+
if (written.has(key) || consumed.has(resolve3(root, file))) continue;
|
|
762
880
|
const dest = join4(tmp, file);
|
|
763
881
|
mkdirSync(dirname2(dest), { recursive: true });
|
|
764
882
|
copyFileSync(join4(root, file), dest);
|
|
@@ -775,7 +893,7 @@ async function runBuild(opts) {
|
|
|
775
893
|
throw err;
|
|
776
894
|
}
|
|
777
895
|
const files = listAll(outDir, outDir).sort();
|
|
778
|
-
for (const f of files) console.log(` ${f} ${formatSize(
|
|
896
|
+
for (const f of files) console.log(` ${f} ${formatSize(statSync3(join4(outDir, f)).size)}`);
|
|
779
897
|
return { outDir, files };
|
|
780
898
|
}
|
|
781
899
|
function listAll(base, dir) {
|
|
@@ -834,37 +952,82 @@ Options:
|
|
|
834
952
|
// cli/init.ts
|
|
835
953
|
var init_exports = {};
|
|
836
954
|
__export(init_exports, {
|
|
955
|
+
detectPackageManager: () => detectPackageManager,
|
|
956
|
+
devCommand: () => devCommand,
|
|
837
957
|
init: () => init,
|
|
838
|
-
|
|
958
|
+
installCommand: () => installCommand,
|
|
959
|
+
runInit: () => runInit,
|
|
960
|
+
spawnInstall: () => spawnInstall
|
|
839
961
|
});
|
|
840
|
-
import {
|
|
841
|
-
import {
|
|
962
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
963
|
+
import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync4, renameSync as renameSync2, statSync as statSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
964
|
+
import { basename as basename3, join as join5, resolve as resolve4 } from "node:path";
|
|
842
965
|
import { parseArgs as parseArgs3 } from "node:util";
|
|
843
|
-
function
|
|
844
|
-
const
|
|
966
|
+
function detectPackageManager(userAgent) {
|
|
967
|
+
const [name, version] = (userAgent?.split(" ")[0] ?? "").split("/");
|
|
968
|
+
if (!PACKAGE_MANAGERS.includes(name)) return { name: "npm", version: null };
|
|
969
|
+
return { name, version: version && version !== "?" ? version : null };
|
|
970
|
+
}
|
|
971
|
+
function installCommand(pm) {
|
|
972
|
+
return { cmd: pm, args: pm === "yarn" ? [] : ["install"] };
|
|
973
|
+
}
|
|
974
|
+
function devCommand(pm) {
|
|
975
|
+
return pm === "npm" || pm === "bun" ? `${pm} run dev` : `${pm} dev`;
|
|
976
|
+
}
|
|
977
|
+
function spawnInstall(cmd, args, cwd) {
|
|
978
|
+
return new Promise((done) => {
|
|
979
|
+
const child = process.platform === "win32" ? spawn2([cmd, ...args].join(" "), { cwd, stdio: "inherit", shell: true }) : spawn2(cmd, args, { cwd, stdio: "inherit" });
|
|
980
|
+
child.on("error", () => done(false));
|
|
981
|
+
child.on("close", (code) => done(code === 0));
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
async function runInit(opts) {
|
|
985
|
+
const dir = resolve4(opts.dir);
|
|
845
986
|
if (existsSync5(dir)) {
|
|
846
|
-
if (!
|
|
987
|
+
if (!statSync4(dir).isDirectory() || readdirSync3(dir).length > 0) {
|
|
847
988
|
throw new UserError(`${dir} already exists and is not an empty folder.`);
|
|
848
989
|
}
|
|
849
990
|
} else {
|
|
850
991
|
mkdirSync2(dir, { recursive: true });
|
|
851
992
|
}
|
|
852
993
|
cpSync(join5(packageRoot(), "templates", opts.template), dir, { recursive: true });
|
|
994
|
+
if (opts.pm.name === "pnpm") cpSync(join5(packageRoot(), "templates", "pnpm"), dir, { recursive: true });
|
|
853
995
|
renameSync2(join5(dir, "gitignore"), join5(dir, ".gitignore"));
|
|
854
|
-
const name =
|
|
996
|
+
const name = basename3(dir).toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
855
997
|
for (const file of TEXT_FILES) {
|
|
856
998
|
const path = join5(dir, file);
|
|
857
999
|
const text = readFileSync4(path, "utf8").replace(/\{\{name\}\}/g, name).replace(/\{\{version\}\}/g, engineVersion());
|
|
858
1000
|
writeFileSync2(path, text);
|
|
859
1001
|
}
|
|
1002
|
+
const pinPnpm = opts.pm.name === "pnpm" && opts.pm.version;
|
|
1003
|
+
if (pinPnpm || opts.pm.name === "npm") {
|
|
1004
|
+
const path = join5(dir, "package.json");
|
|
1005
|
+
const pkg = JSON.parse(readFileSync4(path, "utf8"));
|
|
1006
|
+
if (pinPnpm) pkg.packageManager = `pnpm@${opts.pm.version}`;
|
|
1007
|
+
if (opts.pm.name === "npm") pkg.allowScripts = { esbuild: true };
|
|
1008
|
+
writeFileSync2(path, `${JSON.stringify(pkg, null, 2)}
|
|
1009
|
+
`);
|
|
1010
|
+
}
|
|
1011
|
+
if (!opts.install) return;
|
|
1012
|
+
const { cmd, args } = installCommand(opts.pm.name);
|
|
1013
|
+
const ok = await (opts.runInstall ?? spawnInstall)(cmd, args, dir);
|
|
1014
|
+
if (!ok) {
|
|
1015
|
+
throw new UserError(`The project was created, but "${[cmd, ...args].join(" ")}" failed. Run it in the project folder to finish.`);
|
|
1016
|
+
}
|
|
860
1017
|
}
|
|
861
1018
|
async function init(args) {
|
|
862
1019
|
let parsed;
|
|
863
1020
|
try {
|
|
864
1021
|
parsed = parseArgs3({
|
|
865
1022
|
args,
|
|
866
|
-
options: {
|
|
867
|
-
|
|
1023
|
+
options: {
|
|
1024
|
+
js: { type: "boolean" },
|
|
1025
|
+
pm: { type: "string" },
|
|
1026
|
+
install: { type: "boolean", default: true },
|
|
1027
|
+
help: { type: "boolean", short: "h" }
|
|
1028
|
+
},
|
|
1029
|
+
allowPositionals: true,
|
|
1030
|
+
allowNegative: true
|
|
868
1031
|
});
|
|
869
1032
|
} catch (err) {
|
|
870
1033
|
throw new UserError(`${err instanceof Error ? err.message : String(err)}
|
|
@@ -880,14 +1043,22 @@ ${USAGE3}`);
|
|
|
880
1043
|
|
|
881
1044
|
${USAGE3}`);
|
|
882
1045
|
const dir = positionals[0] ?? ".";
|
|
883
|
-
|
|
884
|
-
|
|
1046
|
+
let pm = detectPackageManager(process.env.npm_config_user_agent);
|
|
1047
|
+
if (values.pm !== void 0) {
|
|
1048
|
+
if (!PACKAGE_MANAGERS.includes(values.pm)) {
|
|
1049
|
+
throw new UserError(`Unknown package manager "${values.pm}". Use one of: ${PACKAGE_MANAGERS.join(", ")}.
|
|
885
1050
|
|
|
886
|
-
|
|
1051
|
+
${USAGE3}`);
|
|
1052
|
+
}
|
|
1053
|
+
pm = values.pm === pm.name ? pm : { name: values.pm, version: null };
|
|
1054
|
+
}
|
|
1055
|
+
await runInit({ dir, template: values.js ? "js" : "ts", pm, install: values.install });
|
|
1056
|
+
console.log(`Created ${dir === "." ? "the project" : dir}. Next:
|
|
887
1057
|
|
|
888
|
-
|
|
1058
|
+
${dir === "." ? "" : ` cd ${dir}
|
|
1059
|
+
`} ${devCommand(pm.name)}`);
|
|
889
1060
|
}
|
|
890
|
-
var USAGE3, TEXT_FILES;
|
|
1061
|
+
var USAGE3, PACKAGE_MANAGERS, TEXT_FILES;
|
|
891
1062
|
var init_init = __esm({
|
|
892
1063
|
"cli/init.ts"() {
|
|
893
1064
|
"use strict";
|
|
@@ -895,12 +1066,17 @@ var init_init = __esm({
|
|
|
895
1066
|
init_errors();
|
|
896
1067
|
USAGE3 = `Usage: thrunium init [dir] [options]
|
|
897
1068
|
|
|
898
|
-
Creates a new game project in <dir> (default: the current folder, which must be empty)
|
|
1069
|
+
Creates a new game project in <dir> (default: the current folder, which must be empty)
|
|
1070
|
+
and installs its dependencies.
|
|
899
1071
|
|
|
900
1072
|
Options:
|
|
901
|
-
--js
|
|
902
|
-
--
|
|
1073
|
+
--js Use JavaScript instead of TypeScript
|
|
1074
|
+
--pm <name> Package manager: npm, pnpm, yarn or bun
|
|
1075
|
+
(default: the one that ran this command, else npm)
|
|
1076
|
+
--no-install Do not install dependencies
|
|
1077
|
+
--help, -h Show this help
|
|
903
1078
|
`;
|
|
1079
|
+
PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
|
|
904
1080
|
TEXT_FILES = ["package.json", "game.json"];
|
|
905
1081
|
}
|
|
906
1082
|
});
|