thrunium 1.2.0-rc.2 → 1.2.0-rc.4
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 +14 -3
- package/dist/cli.mjs +373 -94
- package/dist/thrunium-global.mjs +1 -0
- package/dist/thrunium.js +1320 -67
- package/dist/thrunium.min.js +3 -2
- package/dist/thrunium.min.js.map +4 -4
- package/dist/types/button.d.ts +60 -0
- package/dist/types/collider.d.ts +4 -4
- package/dist/types/debug.d.ts +80 -0
- package/dist/types/font-string.d.ts +12 -0
- package/dist/types/fonts.d.ts +35 -0
- package/dist/types/game.d.ts +336 -15
- package/dist/types/input.d.ts +127 -0
- package/dist/types/internals.d.ts +14 -0
- package/dist/types/namespace.d.ts +2 -0
- package/dist/types/reference.d.ts +7 -1
- package/dist/types/sprite.d.ts +22 -0
- package/dist/types/timers.d.ts +18 -0
- package/package.json +1 -1
- package/templates/js/src/game.js +35 -0
- package/templates/ts/src/game.ts +35 -0
- package/templates/js/game.js +0 -17
- package/templates/ts/game.ts +0 -17
package/dist/cli.mjs
CHANGED
|
@@ -117,6 +117,112 @@ var init_engine = __esm({
|
|
|
117
117
|
}
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
+
// cli/fonts.ts
|
|
121
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
122
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "node:fs";
|
|
123
|
+
import { join as join2 } from "node:path";
|
|
124
|
+
function findGoogleFontUrls(js) {
|
|
125
|
+
const urls = /* @__PURE__ */ new Set();
|
|
126
|
+
for (const m of js.matchAll(/(["'`])(https:\/\/fonts\.googleapis\.com\/css2?\?[^"'`\\\s]*)\1(?=\s*(\+)?)/g)) {
|
|
127
|
+
const url = m[2];
|
|
128
|
+
const concatenated = m[3] === "+";
|
|
129
|
+
const unfinished = /[=@&?]$/.test(url) || url.includes("${");
|
|
130
|
+
if (!concatenated && !unfinished) urls.add(url);
|
|
131
|
+
}
|
|
132
|
+
return [...urls];
|
|
133
|
+
}
|
|
134
|
+
function parseFontFaces(css) {
|
|
135
|
+
const faces = [];
|
|
136
|
+
for (const [, body] of css.matchAll(/@font-face\s*\{([^}]*)\}/g)) {
|
|
137
|
+
const prop = (name) => new RegExp(`(?:^|;)\\s*${name}\\s*:\\s*([^;]+)`, "i").exec(body)?.[1].trim();
|
|
138
|
+
const family = prop("font-family");
|
|
139
|
+
const src = /url\(\s*([^)]+?)\s*\)/.exec(prop("src") ?? "")?.[1];
|
|
140
|
+
if (!family || !src) continue;
|
|
141
|
+
const face = { family: unquote(family), src: unquote(src) };
|
|
142
|
+
const weight = prop("font-weight");
|
|
143
|
+
const style = prop("font-style");
|
|
144
|
+
const unicodeRange = prop("unicode-range");
|
|
145
|
+
if (weight) face.weight = weight;
|
|
146
|
+
if (style) face.style = style;
|
|
147
|
+
if (unicodeRange) face.unicodeRange = unicodeRange;
|
|
148
|
+
faces.push(face);
|
|
149
|
+
}
|
|
150
|
+
return faces;
|
|
151
|
+
}
|
|
152
|
+
function fontsCacheDir(root) {
|
|
153
|
+
return join2(root, "node_modules/.cache/thrunium/fonts");
|
|
154
|
+
}
|
|
155
|
+
function manifestScript(manifest) {
|
|
156
|
+
return `<script>window.THRUNIUM_FONTS = ${JSON.stringify(manifest).replace(/</g, "\\u003c")};</script>`;
|
|
157
|
+
}
|
|
158
|
+
function writeAtomic(file, data) {
|
|
159
|
+
writeFileSync(file + ".tmp", data);
|
|
160
|
+
renameSync(file + ".tmp", file);
|
|
161
|
+
}
|
|
162
|
+
async function download(url, fetchImpl) {
|
|
163
|
+
const res = await fetchImpl(url, { headers: { "user-agent": USER_AGENT }, signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
164
|
+
if (!res.ok) throw new Error(`${url} answered ${res.status}`);
|
|
165
|
+
return res;
|
|
166
|
+
}
|
|
167
|
+
function readCache(dir) {
|
|
168
|
+
try {
|
|
169
|
+
const stored = JSON.parse(readFileSync2(join2(dir, INDEX_FILE), "utf8"));
|
|
170
|
+
const fonts = stored.map((f) => ({ ...f, file: join2(dir, f.file) }));
|
|
171
|
+
return fonts.every((f) => existsSync2(f.file)) ? fonts : null;
|
|
172
|
+
} catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function fetchOne(url, dir, fetchImpl) {
|
|
177
|
+
const cached = readCache(dir);
|
|
178
|
+
if (cached) return cached;
|
|
179
|
+
try {
|
|
180
|
+
const faces = parseFontFaces(await (await download(url, fetchImpl)).text());
|
|
181
|
+
if (faces.length === 0) throw new Error("the stylesheet has no @font-face blocks");
|
|
182
|
+
const names = /* @__PURE__ */ new Map();
|
|
183
|
+
for (const face of faces) {
|
|
184
|
+
const abs = new URL(face.src, url).href;
|
|
185
|
+
if (names.has(abs)) continue;
|
|
186
|
+
let name = (new URL(abs).pathname.split("/").pop() || "font").replace(/[^\w.-]/g, "_").replace(/^\./, "_");
|
|
187
|
+
if ([...names.values()].includes(name)) name = `${names.size}-${name}`;
|
|
188
|
+
names.set(abs, name);
|
|
189
|
+
}
|
|
190
|
+
mkdirSync(dir, { recursive: true });
|
|
191
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
192
|
+
await Promise.all(
|
|
193
|
+
[...names].map(async ([abs, name]) => {
|
|
194
|
+
const bytes = new Uint8Array(await (await download(abs, fetchImpl)).arrayBuffer());
|
|
195
|
+
writeAtomic(join2(dir, name), bytes);
|
|
196
|
+
hashes.set(name, sha256(bytes));
|
|
197
|
+
})
|
|
198
|
+
);
|
|
199
|
+
const stored = faces.map((face) => {
|
|
200
|
+
const name = names.get(new URL(face.src, url).href);
|
|
201
|
+
return { ...face, file: name, hash: hashes.get(name) };
|
|
202
|
+
});
|
|
203
|
+
writeAtomic(join2(dir, INDEX_FILE), JSON.stringify(stored));
|
|
204
|
+
return stored.map((f) => ({ ...f, file: join2(dir, f.file) }));
|
|
205
|
+
} catch (err) {
|
|
206
|
+
throw new UserError(`Could not download the Google Font ${url}: ${err instanceof Error ? err.message : String(err)}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function fetchFonts(urls, cacheDir, fetchImpl) {
|
|
210
|
+
const results = await Promise.all(urls.map((url) => fetchOne(url, join2(cacheDir, sha256(url).slice(0, 16)), fetchImpl)));
|
|
211
|
+
return new Map(urls.map((url, i) => [url, results[i]]));
|
|
212
|
+
}
|
|
213
|
+
var USER_AGENT, TIMEOUT_MS, INDEX_FILE, unquote, sha256;
|
|
214
|
+
var init_fonts = __esm({
|
|
215
|
+
"cli/fonts.ts"() {
|
|
216
|
+
"use strict";
|
|
217
|
+
init_errors();
|
|
218
|
+
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36";
|
|
219
|
+
TIMEOUT_MS = 3e4;
|
|
220
|
+
INDEX_FILE = "faces.json";
|
|
221
|
+
unquote = (s) => s.trim().replace(/^(["'])(.*)\1$/, "$2");
|
|
222
|
+
sha256 = (data) => createHash2("sha256").update(data).digest("hex");
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
120
226
|
// cli/html.ts
|
|
121
227
|
function parseAttributes(tag) {
|
|
122
228
|
const attrs = /* @__PURE__ */ new Map();
|
|
@@ -155,20 +261,28 @@ function hasEngineTag(html) {
|
|
|
155
261
|
function escapeHtml(text) {
|
|
156
262
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
157
263
|
}
|
|
158
|
-
function generatePage(title, entry) {
|
|
264
|
+
function generatePage(title, entry, opts = {}) {
|
|
265
|
+
const head = (opts.headTags ?? []).map((tag) => ` ${tag}
|
|
266
|
+
`).join("") + (opts.headExtra ? `${HEAD_PLACEHOLDER}
|
|
267
|
+
` : "");
|
|
268
|
+
const body = opts.bodyExtra ? `${BODY_PLACEHOLDER}
|
|
269
|
+
` : "";
|
|
159
270
|
return `<!DOCTYPE html>
|
|
160
|
-
<html lang="en">
|
|
271
|
+
<html lang="${escapeHtml(opts.lang ?? "en")}">
|
|
161
272
|
<head>
|
|
162
273
|
<meta charset="utf-8">
|
|
163
274
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
164
275
|
<title>${escapeHtml(title)}</title>
|
|
165
|
-
</head>
|
|
276
|
+
${head}</head>
|
|
166
277
|
<body style="margin: 0; padding: 0;">
|
|
167
|
-
<script type="module" src="${escapeHtml(entry)}"></script>
|
|
278
|
+
${body} <script type="module" src="${escapeHtml(entry)}"></script>
|
|
168
279
|
</body>
|
|
169
280
|
</html>
|
|
170
281
|
`;
|
|
171
282
|
}
|
|
283
|
+
function fillExtras(html, extras) {
|
|
284
|
+
return html.replace(HEAD_PLACEHOLDER, () => extras.head).replace(BODY_PLACEHOLDER, () => extras.body);
|
|
285
|
+
}
|
|
172
286
|
function transformHtml(html, opts) {
|
|
173
287
|
let out = html;
|
|
174
288
|
const hasHead = /<\/head\s*>/i.test(html);
|
|
@@ -198,26 +312,101 @@ function transformHtml(html, opts) {
|
|
|
198
312
|
}
|
|
199
313
|
return out;
|
|
200
314
|
}
|
|
201
|
-
var SCRIPT_TAG, ATTRIBUTE, ENGINE_SRC;
|
|
315
|
+
var SCRIPT_TAG, ATTRIBUTE, ENGINE_SRC, HEAD_PLACEHOLDER, BODY_PLACEHOLDER;
|
|
202
316
|
var init_html = __esm({
|
|
203
317
|
"cli/html.ts"() {
|
|
204
318
|
"use strict";
|
|
205
319
|
SCRIPT_TAG = /<script\b[^>]*>/gi;
|
|
206
320
|
ATTRIBUTE = /([^\s"'<>\/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
|
207
321
|
ENGINE_SRC = /(^|\/)thrunium(\.min)?\.js([?#]|$)/;
|
|
322
|
+
HEAD_PLACEHOLDER = "<!--thrunium:head-->";
|
|
323
|
+
BODY_PLACEHOLDER = "<!--thrunium:body-->";
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// cli/page.ts
|
|
328
|
+
import { existsSync as existsSync3, statSync } from "node:fs";
|
|
329
|
+
import { isAbsolute, join as join3, normalize } from "node:path";
|
|
330
|
+
function checkAsset(root, field, value) {
|
|
331
|
+
if (HTTP_URL.test(value)) return value;
|
|
332
|
+
const problem = (why) => new UserError(`"page.${field}" in game.json must be an http(s) URL or a relative path to a file in the project (${why}): ${value}`);
|
|
333
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(value) || value.startsWith("//")) throw problem("unsupported address");
|
|
334
|
+
if (/[?#]/.test(value)) throw problem("no ? or # suffix");
|
|
335
|
+
if (isAbsolute(value) || normalize(value).split(/[\\/]/).includes("..")) throw problem("outside the project");
|
|
336
|
+
const path = join3(root, value);
|
|
337
|
+
if (!existsSync3(path) || !statSync(path).isFile()) throw problem("file not found");
|
|
338
|
+
return value.replace(/^(\.\/)+/, "");
|
|
339
|
+
}
|
|
340
|
+
function readPageSettings(root, value) {
|
|
341
|
+
if (value === void 0) return {};
|
|
342
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
343
|
+
throw new UserError('"page" in game.json must be an object.');
|
|
344
|
+
}
|
|
345
|
+
const raw = value;
|
|
346
|
+
const settings = {};
|
|
347
|
+
for (const field of FIELDS) {
|
|
348
|
+
const v = raw[field];
|
|
349
|
+
if (v === void 0) continue;
|
|
350
|
+
if (typeof v !== "string") throw new UserError(`"page.${field}" in game.json must be a string.`);
|
|
351
|
+
if (v !== "") settings[field] = v;
|
|
352
|
+
}
|
|
353
|
+
if (settings.icon !== void 0) settings.icon = checkAsset(root, "icon", settings.icon);
|
|
354
|
+
if (settings.image !== void 0) settings.image = checkAsset(root, "image", settings.image);
|
|
355
|
+
if (settings.url !== void 0 && !(HTTP_URL.test(settings.url) && URL.canParse(settings.url))) {
|
|
356
|
+
throw new UserError(`"page.url" in game.json must be an absolute http(s) URL: ${settings.url}`);
|
|
357
|
+
}
|
|
358
|
+
return settings;
|
|
359
|
+
}
|
|
360
|
+
function pageHeadTags(settings, title) {
|
|
361
|
+
const { description, icon, url } = settings;
|
|
362
|
+
const image = settings.image !== void 0 && url !== void 0 && !HTTP_URL.test(settings.image) ? new URL(settings.image, url).href : settings.image;
|
|
363
|
+
const attr = escapeHtml;
|
|
364
|
+
const tags = [];
|
|
365
|
+
if (description !== void 0) tags.push(`<meta name="description" content="${attr(description)}">`);
|
|
366
|
+
if (url !== void 0) tags.push(`<link rel="canonical" href="${attr(url)}">`);
|
|
367
|
+
if (icon !== void 0) {
|
|
368
|
+
const ext = /\.[^./?#]+$/.exec(icon)?.[0].toLowerCase() ?? "";
|
|
369
|
+
const type = ICON_TYPES[ext];
|
|
370
|
+
tags.push(`<link rel="icon" href="${attr(icon)}"${type ? ` type="${type}"` : ""}>`);
|
|
371
|
+
}
|
|
372
|
+
if (description !== void 0 || image !== void 0 || url !== void 0) {
|
|
373
|
+
tags.push('<meta property="og:type" content="website">');
|
|
374
|
+
tags.push(`<meta property="og:title" content="${attr(title)}">`);
|
|
375
|
+
if (description !== void 0) tags.push(`<meta property="og:description" content="${attr(description)}">`);
|
|
376
|
+
if (image !== void 0) tags.push(`<meta property="og:image" content="${attr(image)}">`);
|
|
377
|
+
if (url !== void 0) tags.push(`<meta property="og:url" content="${attr(url)}">`);
|
|
378
|
+
tags.push(`<meta name="twitter:card" content="${image !== void 0 ? "summary_large_image" : "summary"}">`);
|
|
379
|
+
}
|
|
380
|
+
return tags;
|
|
381
|
+
}
|
|
382
|
+
var FIELDS, HTTP_URL, ICON_TYPES;
|
|
383
|
+
var init_page = __esm({
|
|
384
|
+
"cli/page.ts"() {
|
|
385
|
+
"use strict";
|
|
386
|
+
init_errors();
|
|
387
|
+
init_html();
|
|
388
|
+
FIELDS = ["title", "description", "icon", "image", "url", "lang"];
|
|
389
|
+
HTTP_URL = /^https?:\/\//i;
|
|
390
|
+
ICON_TYPES = {
|
|
391
|
+
".png": "image/png",
|
|
392
|
+
".svg": "image/svg+xml",
|
|
393
|
+
".ico": "image/x-icon",
|
|
394
|
+
".gif": "image/gif",
|
|
395
|
+
".webp": "image/webp"
|
|
396
|
+
};
|
|
208
397
|
}
|
|
209
398
|
});
|
|
210
399
|
|
|
211
400
|
// cli/project.ts
|
|
212
|
-
import { existsSync as
|
|
213
|
-
import { basename, isAbsolute, join as
|
|
401
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
|
|
402
|
+
import { basename, isAbsolute as isAbsolute2, join as join4, normalize as normalize2, resolve } from "node:path";
|
|
214
403
|
function resolveEngineMode(root, flag) {
|
|
215
404
|
if (flag) return flag;
|
|
216
|
-
const manifest =
|
|
217
|
-
if (!
|
|
405
|
+
const manifest = join4(root, "package.json");
|
|
406
|
+
if (!existsSync4(manifest)) return "cdn";
|
|
218
407
|
let pkg;
|
|
219
408
|
try {
|
|
220
|
-
pkg = JSON.parse(
|
|
409
|
+
pkg = JSON.parse(readFileSync3(manifest, "utf8"));
|
|
221
410
|
} catch {
|
|
222
411
|
throw new UserError("package.json is not valid JSON.");
|
|
223
412
|
}
|
|
@@ -227,65 +416,91 @@ function resolveEngineMode(root, flag) {
|
|
|
227
416
|
throw new UserError(`Invalid "thrunium.engine" in package.json: ${JSON.stringify(value)}. Use "cdn" or "bundle".`);
|
|
228
417
|
}
|
|
229
418
|
function isFile(path) {
|
|
230
|
-
return
|
|
419
|
+
return existsSync4(path) && statSync2(path).isFile();
|
|
231
420
|
}
|
|
232
421
|
function readGameJson(root) {
|
|
233
|
-
const file =
|
|
234
|
-
if (!
|
|
422
|
+
const file = join4(root, "game.json");
|
|
423
|
+
if (!existsSync4(file)) return null;
|
|
235
424
|
let json;
|
|
236
425
|
try {
|
|
237
|
-
json = JSON.parse(
|
|
426
|
+
json = JSON.parse(readFileSync3(file, "utf8"));
|
|
238
427
|
} catch {
|
|
239
428
|
throw new UserError("game.json is not valid JSON.");
|
|
240
429
|
}
|
|
241
430
|
if (typeof json !== "object" || json === null || Array.isArray(json)) {
|
|
242
431
|
throw new UserError("game.json must contain a JSON object.");
|
|
243
432
|
}
|
|
244
|
-
const { name, entry } = json;
|
|
433
|
+
const { name, entry, page } = json;
|
|
245
434
|
if (name !== void 0 && typeof name !== "string") throw new UserError('"name" in game.json must be a string.');
|
|
246
435
|
if (entry !== void 0 && typeof entry !== "string") throw new UserError('"entry" in game.json must be a string.');
|
|
247
|
-
return { name, entry };
|
|
436
|
+
return { name, entry, page };
|
|
248
437
|
}
|
|
249
438
|
function validateEntry(root, entry) {
|
|
250
|
-
const inside = !
|
|
439
|
+
const inside = !isAbsolute2(entry) && !normalize2(entry).split(/[\\/]/).includes("..");
|
|
251
440
|
if (!inside) throw new UserError(`"entry" in game.json must be a relative path inside the project: ${entry}`);
|
|
252
441
|
if (!ENTRY_EXTENSIONS.test(entry)) {
|
|
253
442
|
throw new UserError(`"entry" in game.json must end in .ts, .js, .mts or .mjs: ${entry}`);
|
|
254
443
|
}
|
|
255
|
-
if (!isFile(
|
|
444
|
+
if (!isFile(join4(root, entry))) throw new UserError(`"entry" in game.json points to ${entry}, which does not exist.`);
|
|
256
445
|
}
|
|
257
|
-
function
|
|
446
|
+
function hasKey(file, key) {
|
|
258
447
|
try {
|
|
259
|
-
const json = JSON.parse(
|
|
260
|
-
return typeof json === "object" && json !== null && !Array.isArray(json) &&
|
|
448
|
+
const json = JSON.parse(readFileSync3(file, "utf8"));
|
|
449
|
+
return typeof json === "object" && json !== null && !Array.isArray(json) && key in json;
|
|
261
450
|
} catch {
|
|
262
451
|
return false;
|
|
263
452
|
}
|
|
264
453
|
}
|
|
454
|
+
function readExtra(root, file) {
|
|
455
|
+
const path = join4(root, file);
|
|
456
|
+
return isFile(path) ? readFileSync3(path, "utf8").replace(/^\uFEFF/, "").trimEnd() : "";
|
|
457
|
+
}
|
|
265
458
|
function resolvePage(root) {
|
|
266
|
-
const indexPath =
|
|
267
|
-
if (
|
|
268
|
-
|
|
459
|
+
const indexPath = join4(root, "index.html");
|
|
460
|
+
if (existsSync4(indexPath)) {
|
|
461
|
+
const gameJson = join4(root, "game.json");
|
|
462
|
+
if (hasKey(gameJson, "entry")) {
|
|
269
463
|
throw new UserError('game.json has "entry", but index.html also exists and is used instead. Remove one of them.');
|
|
270
464
|
}
|
|
271
|
-
|
|
465
|
+
if (hasKey(gameJson, "page")) {
|
|
466
|
+
throw new UserError(
|
|
467
|
+
'game.json has "page", but index.html also exists and is used instead. Move the settings into index.html or delete it.'
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
for (const file of ["head.html", "body.html"]) {
|
|
471
|
+
if (isFile(join4(root, file))) {
|
|
472
|
+
throw new UserError(
|
|
473
|
+
`${file} exists, but index.html also exists and is used instead. Move its contents into index.html or delete it.`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return { html: readFileSync3(indexPath, "utf8"), htmlSource: "file", extras: { head: "", body: "" } };
|
|
272
478
|
}
|
|
273
479
|
const game = readGameJson(root);
|
|
274
480
|
let entry = game?.entry;
|
|
275
481
|
if (entry !== void 0) validateEntry(root, entry);
|
|
276
|
-
else entry = DEFAULT_ENTRIES.find((candidate) => isFile(
|
|
482
|
+
else entry = DEFAULT_ENTRIES.find((candidate) => isFile(join4(root, candidate)));
|
|
277
483
|
if (entry === void 0) {
|
|
278
484
|
throw new UserError(
|
|
279
485
|
`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
486
|
);
|
|
281
487
|
}
|
|
282
|
-
|
|
488
|
+
const settings = readPageSettings(root, game?.page);
|
|
489
|
+
const title = settings.title ?? game?.name ?? basename(resolve(root));
|
|
490
|
+
const extras = { head: readExtra(root, "head.html"), body: readExtra(root, "body.html") };
|
|
491
|
+
const html = generatePage(title, entry, {
|
|
492
|
+
lang: settings.lang,
|
|
493
|
+
headTags: pageHeadTags(settings, title),
|
|
494
|
+
headExtra: extras.head !== "",
|
|
495
|
+
bodyExtra: extras.body !== ""
|
|
496
|
+
});
|
|
497
|
+
return { html, htmlSource: "generated", extras };
|
|
283
498
|
}
|
|
284
499
|
function loadProject(root, engineFlag) {
|
|
285
|
-
const { html, htmlSource } = resolvePage(root);
|
|
500
|
+
const { html, htmlSource, extras } = resolvePage(root);
|
|
286
501
|
const entries = findModuleEntries(html);
|
|
287
502
|
for (const entry of entries) {
|
|
288
|
-
if (!
|
|
503
|
+
if (!existsSync4(join4(root, entry))) {
|
|
289
504
|
throw new UserError(`Entry file ${entry} (from ${htmlSource === "file" ? "index.html" : "game.json"}) does not exist.`);
|
|
290
505
|
}
|
|
291
506
|
}
|
|
@@ -293,7 +508,7 @@ function loadProject(root, engineFlag) {
|
|
|
293
508
|
if (usesModules && hasEngineTag(html)) {
|
|
294
509
|
throw new UserError("Remove the Thrunium <script> tag from index.html; thrunium adds it for you.");
|
|
295
510
|
}
|
|
296
|
-
return { root, html, htmlSource, entries, usesModules, engineMode: resolveEngineMode(root, engineFlag) };
|
|
511
|
+
return { root, html, htmlSource, extras, entries, usesModules, engineMode: resolveEngineMode(root, engineFlag) };
|
|
297
512
|
}
|
|
298
513
|
function thruniumPlugin() {
|
|
299
514
|
return {
|
|
@@ -334,6 +549,7 @@ var init_project = __esm({
|
|
|
334
549
|
init_engine();
|
|
335
550
|
init_errors();
|
|
336
551
|
init_html();
|
|
552
|
+
init_page();
|
|
337
553
|
DEFAULT_ENTRIES = ["game.ts", "game.js", "src/game.ts", "src/game.js"];
|
|
338
554
|
ENTRY_EXTENSIONS = /\.(ts|js|mts|mjs)$/;
|
|
339
555
|
ASSET_EXTENSIONS = [".woff", ".woff2", ".ttf", ".otf", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"];
|
|
@@ -346,6 +562,7 @@ var init_reload_client = __esm({
|
|
|
346
562
|
"cli/reload-client.ts"() {
|
|
347
563
|
"use strict";
|
|
348
564
|
RELOAD_CLIENT = `<script>
|
|
565
|
+
window.THRUNIUM_DEV = true;
|
|
349
566
|
(function () {
|
|
350
567
|
var overlay = null;
|
|
351
568
|
function clearOverlay() {
|
|
@@ -381,11 +598,11 @@ __export(dev_exports, {
|
|
|
381
598
|
startDev: () => startDev
|
|
382
599
|
});
|
|
383
600
|
import { spawn } from "node:child_process";
|
|
384
|
-
import { existsSync as
|
|
601
|
+
import { existsSync as existsSync5, readdirSync, realpathSync, statSync as statSync3, watch } from "node:fs";
|
|
385
602
|
import { readFile } from "node:fs/promises";
|
|
386
603
|
import { createServer } from "node:http";
|
|
387
604
|
import { isIP } from "node:net";
|
|
388
|
-
import { basename as basename2, extname, isAbsolute as
|
|
605
|
+
import { basename as basename2, dirname as dirname2, extname, isAbsolute as isAbsolute3, join as join5, normalize as normalize3, relative, resolve as resolve2, sep } from "node:path";
|
|
389
606
|
import { parseArgs } from "node:util";
|
|
390
607
|
import { context as esbuildContext, formatMessages } from "esbuild";
|
|
391
608
|
function openBrowser(url) {
|
|
@@ -423,10 +640,10 @@ async function listen(server, port, host) {
|
|
|
423
640
|
async function startDev(opts) {
|
|
424
641
|
const root = resolve2(opts.root);
|
|
425
642
|
const host = opts.host ?? "localhost";
|
|
426
|
-
const outDir =
|
|
643
|
+
const outDir = join5(root, "dist");
|
|
427
644
|
const realRoot = realpathSync(root);
|
|
428
645
|
const outTmp = outDir + ".tmp";
|
|
429
|
-
const virtualOut =
|
|
646
|
+
const virtualOut = join5(root, ".thrunium-dev");
|
|
430
647
|
loadProject(root);
|
|
431
648
|
const clients = /* @__PURE__ */ new Set();
|
|
432
649
|
let lastError = null;
|
|
@@ -447,6 +664,25 @@ data: ${JSON.stringify(data)}
|
|
|
447
664
|
let owned = /* @__PURE__ */ new Set();
|
|
448
665
|
let entries = [];
|
|
449
666
|
let ctx = null;
|
|
667
|
+
let fonts = /* @__PURE__ */ new Map();
|
|
668
|
+
const failedFonts = /* @__PURE__ */ new Set();
|
|
669
|
+
const fontUrl = (font) => `${FONTS_PREFIX}${basename2(dirname2(font.file))}/${basename2(font.file)}`;
|
|
670
|
+
async function refreshFonts(outputs) {
|
|
671
|
+
const urls = [...new Set([...outputs].flatMap(([name, contents]) => name.endsWith(".js") ? findGoogleFontUrls(Buffer.from(contents).toString("utf8")) : []))];
|
|
672
|
+
const next = /* @__PURE__ */ new Map();
|
|
673
|
+
await Promise.all(
|
|
674
|
+
urls.map(async (url2) => {
|
|
675
|
+
try {
|
|
676
|
+
next.set(url2, (await fetchFonts([url2], fontsCacheDir(root), opts.fetchImpl ?? fetch)).get(url2));
|
|
677
|
+
failedFonts.delete(url2);
|
|
678
|
+
} catch (err) {
|
|
679
|
+
if (!failedFonts.has(url2)) console.warn(`thrunium: ${err instanceof Error ? err.message : String(err)}. It loads from Google instead.`);
|
|
680
|
+
failedFonts.add(url2);
|
|
681
|
+
}
|
|
682
|
+
})
|
|
683
|
+
);
|
|
684
|
+
fonts = new Map(urls.flatMap((url2) => next.has(url2) ? [[url2, next.get(url2)]] : []));
|
|
685
|
+
}
|
|
450
686
|
const formatErrors = async (errors) => (await formatMessages(errors, { kind: "error", color: false })).join("\n").trim();
|
|
451
687
|
async function startBundler(newEntries) {
|
|
452
688
|
await ctx?.dispose();
|
|
@@ -454,6 +690,7 @@ data: ${JSON.stringify(data)}
|
|
|
454
690
|
entries = newEntries;
|
|
455
691
|
buildError = null;
|
|
456
692
|
bundles.clear();
|
|
693
|
+
fonts = /* @__PURE__ */ new Map();
|
|
457
694
|
owned = new Set(entries.map((e) => resolve2(root, e)));
|
|
458
695
|
if (entries.length === 0) return;
|
|
459
696
|
let first = true;
|
|
@@ -486,6 +723,7 @@ data: ${JSON.stringify(data)}
|
|
|
486
723
|
const changed = next.size !== bundles.size || [...next].some(([k, v]) => !Buffer.from(v).equals(bundles.get(k) ?? new Uint8Array()));
|
|
487
724
|
bundles.clear();
|
|
488
725
|
for (const [k, v] of next) bundles.set(k, v);
|
|
726
|
+
await refreshFonts(next);
|
|
489
727
|
owned = new Set(Object.keys(result.metafile?.inputs ?? {}).map((p) => resolve2(root, p)));
|
|
490
728
|
for (const e of entries) owned.add(resolve2(root, e));
|
|
491
729
|
if (!wasFirst && (changed || lastError !== null)) broadcastReload();
|
|
@@ -541,12 +779,23 @@ data: ${JSON.stringify(data)}
|
|
|
541
779
|
const project = loadProject(root);
|
|
542
780
|
const entryMap = new Map(project.entries.map((e, i) => [e, `${BUNDLE_PREFIX}${i}.js`]));
|
|
543
781
|
const styles = project.entries.flatMap((_, i) => bundles.has(`${i}.css`) ? [`${BUNDLE_PREFIX}${i}.css`] : []);
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
782
|
+
const manifest = {};
|
|
783
|
+
for (const [googleUrl, files] of fonts) {
|
|
784
|
+
manifest[googleUrl] = files.map((font) => {
|
|
785
|
+
const { file: _file, hash: _hash, ...face } = font;
|
|
786
|
+
return { ...face, src: fontUrl(font) };
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
const engineTag = (fonts.size > 0 ? manifestScript(manifest) : "") + ENGINE_TAG;
|
|
790
|
+
const html = fillExtras(
|
|
791
|
+
transformHtml(project.html, {
|
|
792
|
+
engineTag: project.usesModules ? engineTag : null,
|
|
793
|
+
entryMap,
|
|
794
|
+
inject: RELOAD_CLIENT,
|
|
795
|
+
styles
|
|
796
|
+
}),
|
|
797
|
+
project.extras
|
|
798
|
+
);
|
|
550
799
|
res.writeHead(200, { ...noCache, "content-type": MIME[".html"] }).end(html);
|
|
551
800
|
return;
|
|
552
801
|
}
|
|
@@ -565,15 +814,25 @@ data: ${JSON.stringify(data)}
|
|
|
565
814
|
res.writeHead(200, { ...noCache, "content-type": type }).end(contents);
|
|
566
815
|
return;
|
|
567
816
|
}
|
|
568
|
-
|
|
817
|
+
if (pathname.startsWith(FONTS_PREFIX)) {
|
|
818
|
+
const font = [...fonts.values()].flat().find((f) => fontUrl(f) === pathname);
|
|
819
|
+
if (!font) {
|
|
820
|
+
res.writeHead(404).end();
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
const type = MIME[extname(font.file).toLowerCase()] ?? "application/octet-stream";
|
|
824
|
+
res.writeHead(200, { ...noCache, "content-type": type }).end(await readFile(font.file));
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
const file = normalize3(join5(root, pathname));
|
|
569
828
|
const rel = relative(root, file);
|
|
570
829
|
if (/[\\\0]/.test(pathname) || // backslashes act as separators on Windows; NUL is never valid
|
|
571
|
-
rel.split(/[\\/]/).some((part) => part.startsWith(".")) || rel === "" || rel === ".." || rel.startsWith(".." + sep) ||
|
|
830
|
+
rel.split(/[\\/]/).some((part) => part.startsWith(".")) || rel === "" || rel === ".." || rel.startsWith(".." + sep) || isAbsolute3(rel) || !existsSync5(file) || !statSync3(file).isFile()) {
|
|
572
831
|
res.writeHead(404).end();
|
|
573
832
|
return;
|
|
574
833
|
}
|
|
575
834
|
const realRel = relative(realRoot, realpathSync(file));
|
|
576
|
-
if (realRel === ".." || realRel.startsWith(".." + sep) ||
|
|
835
|
+
if (realRel === ".." || realRel.startsWith(".." + sep) || isAbsolute3(realRel)) {
|
|
577
836
|
res.writeHead(404).end();
|
|
578
837
|
return;
|
|
579
838
|
}
|
|
@@ -585,7 +844,7 @@ data: ${JSON.stringify(data)}
|
|
|
585
844
|
for (const dir of [outDir, outTmp]) if (abs === dir || abs.startsWith(dir + sep)) return true;
|
|
586
845
|
if (owned.has(abs)) return true;
|
|
587
846
|
try {
|
|
588
|
-
return
|
|
847
|
+
return statSync3(abs).isDirectory();
|
|
589
848
|
} catch {
|
|
590
849
|
return false;
|
|
591
850
|
}
|
|
@@ -593,15 +852,10 @@ data: ${JSON.stringify(data)}
|
|
|
593
852
|
let timer;
|
|
594
853
|
let closed = false;
|
|
595
854
|
let pending = Promise.resolve();
|
|
596
|
-
let indexChanged = false;
|
|
597
855
|
const flush = async () => {
|
|
598
|
-
const checkEntries = indexChanged;
|
|
599
|
-
indexChanged = false;
|
|
600
856
|
try {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
if (next.join("\n") !== entries.join("\n")) await startBundler(next);
|
|
604
|
-
}
|
|
857
|
+
const next = findModuleEntries(loadProject(root).html);
|
|
858
|
+
if (next.join("\n") !== entries.join("\n")) await startBundler(next);
|
|
605
859
|
} catch (err) {
|
|
606
860
|
broadcastError(err instanceof Error ? err.message : String(err));
|
|
607
861
|
return;
|
|
@@ -620,20 +874,20 @@ data: ${JSON.stringify(data)}
|
|
|
620
874
|
};
|
|
621
875
|
const watchers = /* @__PURE__ */ new Map();
|
|
622
876
|
const skippedTop = (name) => {
|
|
623
|
-
const abs =
|
|
877
|
+
const abs = join5(root, name);
|
|
624
878
|
return name === "node_modules" || name.startsWith(".") || abs === outDir || abs === outTmp;
|
|
625
879
|
};
|
|
626
880
|
const onWatchError = (path) => (err) => {
|
|
627
881
|
watchers.get(path)?.close();
|
|
628
882
|
watchers.delete(path);
|
|
629
|
-
if (
|
|
883
|
+
if (existsSync5(path)) console.error(`thrunium: file watching stopped for ${path}: ${err.message}`);
|
|
630
884
|
};
|
|
631
885
|
const rescan = () => {
|
|
632
886
|
if (closed) return;
|
|
633
887
|
const dirs = /* @__PURE__ */ new Set();
|
|
634
888
|
try {
|
|
635
889
|
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
636
|
-
if (entry.isDirectory() && !skippedTop(entry.name)) dirs.add(
|
|
890
|
+
if (entry.isDirectory() && !skippedTop(entry.name)) dirs.add(join5(root, entry.name));
|
|
637
891
|
}
|
|
638
892
|
} catch {
|
|
639
893
|
return;
|
|
@@ -650,7 +904,7 @@ data: ${JSON.stringify(data)}
|
|
|
650
904
|
const w = watch(dir, { recursive: true }, (_event, filename) => {
|
|
651
905
|
if (!filename) return schedule();
|
|
652
906
|
const name = filename.toString();
|
|
653
|
-
if (name === basename2(dir) && !
|
|
907
|
+
if (name === basename2(dir) && !existsSync5(join5(dir, name))) return;
|
|
654
908
|
if (!ignored(resolve2(dir, name))) schedule();
|
|
655
909
|
});
|
|
656
910
|
w.on("error", onWatchError(dir));
|
|
@@ -662,9 +916,8 @@ data: ${JSON.stringify(data)}
|
|
|
662
916
|
const rootWatcher = watch(root, (event, filename) => {
|
|
663
917
|
if (event === "rename") rescan();
|
|
664
918
|
const abs = filename ? resolve2(root, filename.toString()) : root;
|
|
665
|
-
const pageInput = !filename ||
|
|
919
|
+
const pageInput = !filename || PAGE_INPUTS.some((file) => abs === join5(root, file));
|
|
666
920
|
if (!pageInput && ignored(abs)) return;
|
|
667
|
-
if (pageInput) indexChanged = true;
|
|
668
921
|
schedule();
|
|
669
922
|
});
|
|
670
923
|
rootWatcher.on("error", onWatchError(root));
|
|
@@ -741,12 +994,13 @@ ${USAGE}`);
|
|
|
741
994
|
});
|
|
742
995
|
await server.close();
|
|
743
996
|
}
|
|
744
|
-
var USAGE, MIME, ENGINE_TAG, BUNDLE_PREFIX, WATCH_DEBOUNCE_MS, PORT_ATTEMPTS;
|
|
997
|
+
var USAGE, MIME, ENGINE_TAG, BUNDLE_PREFIX, FONTS_PREFIX, PAGE_INPUTS, WATCH_DEBOUNCE_MS, PORT_ATTEMPTS;
|
|
745
998
|
var init_dev = __esm({
|
|
746
999
|
"cli/dev.ts"() {
|
|
747
1000
|
"use strict";
|
|
748
1001
|
init_engine();
|
|
749
1002
|
init_errors();
|
|
1003
|
+
init_fonts();
|
|
750
1004
|
init_html();
|
|
751
1005
|
init_project();
|
|
752
1006
|
init_reload_client();
|
|
@@ -781,6 +1035,8 @@ Options:
|
|
|
781
1035
|
};
|
|
782
1036
|
ENGINE_TAG = '<script src="/__thrunium/engine/thrunium.js"></script>';
|
|
783
1037
|
BUNDLE_PREFIX = "/__thrunium/bundle/";
|
|
1038
|
+
FONTS_PREFIX = "/__thrunium/fonts/";
|
|
1039
|
+
PAGE_INPUTS = ["index.html", "game.json", "head.html", "body.html"];
|
|
784
1040
|
WATCH_DEBOUNCE_MS = 50;
|
|
785
1041
|
PORT_ATTEMPTS = 11;
|
|
786
1042
|
}
|
|
@@ -792,14 +1048,14 @@ __export(build_exports, {
|
|
|
792
1048
|
build: () => build,
|
|
793
1049
|
runBuild: () => runBuild
|
|
794
1050
|
});
|
|
795
|
-
import { existsSync as
|
|
796
|
-
import { dirname as
|
|
1051
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync4, renameSync as renameSync2, rmSync, statSync as statSync4, writeFileSync as writeFileSync2, copyFileSync } from "node:fs";
|
|
1052
|
+
import { basename as basename3, dirname as dirname3, extname as extname2, isAbsolute as isAbsolute4, join as join6, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
|
797
1053
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
798
1054
|
import { build as esbuild, formatMessages as formatMessages2 } from "esbuild";
|
|
799
1055
|
function listFiles(root, dir, skipDirs) {
|
|
800
1056
|
const files = [];
|
|
801
1057
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
802
|
-
const abs =
|
|
1058
|
+
const abs = join6(dir, entry.name);
|
|
803
1059
|
if (entry.name.startsWith(".") || entry.name === "node_modules" || skipDirs.has(abs)) continue;
|
|
804
1060
|
if (entry.isDirectory()) files.push(...listFiles(root, abs, skipDirs));
|
|
805
1061
|
else if (entry.isFile() && !SKIPPED_NAMES.test(entry.name)) files.push(relative2(root, abs));
|
|
@@ -810,7 +1066,7 @@ async function runBuild(opts) {
|
|
|
810
1066
|
const root = resolve3(opts.root);
|
|
811
1067
|
const outDir = resolve3(root, opts.outDir ?? "dist");
|
|
812
1068
|
const rel = relative2(root, outDir);
|
|
813
|
-
if (rel === "" || rel === ".." || rel.startsWith(".." + sep2) ||
|
|
1069
|
+
if (rel === "" || rel === ".." || rel.startsWith(".." + sep2) || isAbsolute4(rel)) {
|
|
814
1070
|
throw new UserError(`The output folder must be inside the project and not the project itself: ${outDir}`);
|
|
815
1071
|
}
|
|
816
1072
|
const project = loadProject(root, opts.engine);
|
|
@@ -827,7 +1083,7 @@ async function runBuild(opts) {
|
|
|
827
1083
|
const entryMap = /* @__PURE__ */ new Map();
|
|
828
1084
|
const styles = [];
|
|
829
1085
|
if (project.usesModules) {
|
|
830
|
-
const outAbs =
|
|
1086
|
+
const outAbs = join6(root, ".thrunium-out");
|
|
831
1087
|
const result = await esbuild({
|
|
832
1088
|
...baseBuildOptions(root),
|
|
833
1089
|
entryPoints: project.entries,
|
|
@@ -862,43 +1118,57 @@ async function runBuild(opts) {
|
|
|
862
1118
|
if (project.usesModules) {
|
|
863
1119
|
if (bundled) {
|
|
864
1120
|
engineTag = '<script src="thrunium.min.js"></script>';
|
|
865
|
-
written.set("thrunium.min.js",
|
|
1121
|
+
written.set("thrunium.min.js", readFileSync4(engineFile("thrunium.min.js")));
|
|
866
1122
|
const map = engineFile("thrunium.min.js") + ".map";
|
|
867
|
-
if (
|
|
1123
|
+
if (existsSync6(map)) written.set("thrunium.min.js.map", readFileSync4(map));
|
|
868
1124
|
} else {
|
|
869
1125
|
engineTag = cdnScriptTag(engineVersion(), integrity);
|
|
870
1126
|
}
|
|
1127
|
+
const urls = [...new Set([...written].flatMap(([file, contents]) => file.endsWith(".js") ? findGoogleFontUrls(Buffer.from(contents).toString("utf8")) : []))];
|
|
1128
|
+
if (urls.length > 0) {
|
|
1129
|
+
const manifest = {};
|
|
1130
|
+
for (const [url, fonts] of await fetchFonts(urls, fontsCacheDir(root), opts.fetchImpl ?? fetch)) {
|
|
1131
|
+
manifest[url] = fonts.map(({ file, hash, ...face }) => {
|
|
1132
|
+
const ext = extname2(file);
|
|
1133
|
+
const src = `fonts/${basename3(file, ext)}-${hash.slice(0, 8)}${ext}`;
|
|
1134
|
+
written.set(src, readFileSync4(file));
|
|
1135
|
+
return { ...face, src };
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
engineTag = manifestScript(manifest) + engineTag;
|
|
1139
|
+
}
|
|
871
1140
|
}
|
|
872
1141
|
const tmp = outDir + ".tmp";
|
|
873
|
-
written.set("index.html", transformHtml(project.html, { engineTag, entryMap, inject: null, styles }));
|
|
1142
|
+
written.set("index.html", fillExtras(transformHtml(project.html, { engineTag, entryMap, inject: null, styles }), project.extras));
|
|
874
1143
|
rmSync(tmp, { recursive: true, force: true });
|
|
875
1144
|
try {
|
|
876
1145
|
const skipDirs = /* @__PURE__ */ new Set([outDir, tmp]);
|
|
877
1146
|
for (const file of listFiles(root, root, skipDirs)) {
|
|
878
1147
|
const key = file.split(sep2).join("/");
|
|
879
1148
|
if (written.has(key) || consumed.has(resolve3(root, file))) continue;
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1149
|
+
if (project.htmlSource === "generated" && PAGE_PARTS.has(key)) continue;
|
|
1150
|
+
const dest = join6(tmp, file);
|
|
1151
|
+
mkdirSync2(dirname3(dest), { recursive: true });
|
|
1152
|
+
copyFileSync(join6(root, file), dest);
|
|
883
1153
|
}
|
|
884
1154
|
for (const [file, contents] of written) {
|
|
885
|
-
const dest =
|
|
886
|
-
|
|
887
|
-
|
|
1155
|
+
const dest = join6(tmp, file);
|
|
1156
|
+
mkdirSync2(dirname3(dest), { recursive: true });
|
|
1157
|
+
writeFileSync2(dest, contents);
|
|
888
1158
|
}
|
|
889
1159
|
rmSync(outDir, { recursive: true, force: true });
|
|
890
|
-
|
|
1160
|
+
renameSync2(tmp, outDir);
|
|
891
1161
|
} catch (err) {
|
|
892
1162
|
rmSync(tmp, { recursive: true, force: true });
|
|
893
1163
|
throw err;
|
|
894
1164
|
}
|
|
895
1165
|
const files = listAll(outDir, outDir).sort();
|
|
896
|
-
for (const f of files) console.log(` ${f} ${formatSize(
|
|
1166
|
+
for (const f of files) console.log(` ${f} ${formatSize(statSync4(join6(outDir, f)).size)}`);
|
|
897
1167
|
return { outDir, files };
|
|
898
1168
|
}
|
|
899
1169
|
function listAll(base, dir) {
|
|
900
1170
|
return readdirSync2(dir, { withFileTypes: true }).flatMap(
|
|
901
|
-
(e) => e.isDirectory() ? listAll(base,
|
|
1171
|
+
(e) => e.isDirectory() ? listAll(base, join6(dir, e.name)) : [relative2(base, join6(dir, e.name)).split(sep2).join("/")]
|
|
902
1172
|
);
|
|
903
1173
|
}
|
|
904
1174
|
function formatSize(bytes) {
|
|
@@ -930,12 +1200,13 @@ ${USAGE2}`);
|
|
|
930
1200
|
}
|
|
931
1201
|
await runBuild({ root: process.cwd(), outDir: values.out, engine: values.engine });
|
|
932
1202
|
}
|
|
933
|
-
var USAGE2, SKIPPED_NAMES;
|
|
1203
|
+
var USAGE2, SKIPPED_NAMES, PAGE_PARTS;
|
|
934
1204
|
var init_build = __esm({
|
|
935
1205
|
"cli/build.ts"() {
|
|
936
1206
|
"use strict";
|
|
937
1207
|
init_engine();
|
|
938
1208
|
init_errors();
|
|
1209
|
+
init_fonts();
|
|
939
1210
|
init_html();
|
|
940
1211
|
init_project();
|
|
941
1212
|
USAGE2 = `Usage: thrunium build [options]
|
|
@@ -946,6 +1217,7 @@ Options:
|
|
|
946
1217
|
--help, -h Show this help
|
|
947
1218
|
`;
|
|
948
1219
|
SKIPPED_NAMES = /^(package\.json|package-lock\.json|npm-shrinkwrap\.json|pnpm-lock\.yaml|pnpm-workspace\.yaml|yarn\.lock|bun\.lockb?|bunfig\.toml|tsconfig.*\.json|.*\.ts)$/;
|
|
1220
|
+
PAGE_PARTS = /* @__PURE__ */ new Set(["head.html", "body.html"]);
|
|
949
1221
|
}
|
|
950
1222
|
});
|
|
951
1223
|
|
|
@@ -960,8 +1232,8 @@ __export(init_exports, {
|
|
|
960
1232
|
spawnInstall: () => spawnInstall
|
|
961
1233
|
});
|
|
962
1234
|
import { spawn as spawn2 } from "node:child_process";
|
|
963
|
-
import { cpSync, existsSync as
|
|
964
|
-
import { basename as
|
|
1235
|
+
import { cpSync, existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync5, renameSync as renameSync3, rmdirSync, statSync as statSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1236
|
+
import { basename as basename4, join as join7, resolve as resolve4 } from "node:path";
|
|
965
1237
|
import { parseArgs as parseArgs3 } from "node:util";
|
|
966
1238
|
function detectPackageManager(userAgent) {
|
|
967
1239
|
const [name, version] = (userAgent?.split(" ")[0] ?? "").split("/");
|
|
@@ -983,31 +1255,36 @@ function spawnInstall(cmd, args, cwd) {
|
|
|
983
1255
|
}
|
|
984
1256
|
async function runInit(opts) {
|
|
985
1257
|
const dir = resolve4(opts.dir);
|
|
986
|
-
if (
|
|
987
|
-
if (!
|
|
1258
|
+
if (existsSync7(dir)) {
|
|
1259
|
+
if (!statSync5(dir).isDirectory() || readdirSync3(dir).length > 0) {
|
|
988
1260
|
throw new UserError(`${dir} already exists and is not an empty folder.`);
|
|
989
1261
|
}
|
|
990
1262
|
} else {
|
|
991
|
-
|
|
1263
|
+
mkdirSync3(dir, { recursive: true });
|
|
992
1264
|
}
|
|
993
|
-
cpSync(
|
|
994
|
-
if (opts.pm.name === "pnpm") cpSync(
|
|
995
|
-
|
|
996
|
-
const name =
|
|
1265
|
+
cpSync(join7(packageRoot(), "templates", opts.template), dir, { recursive: true });
|
|
1266
|
+
if (opts.pm.name === "pnpm") cpSync(join7(packageRoot(), "templates", "pnpm"), dir, { recursive: true });
|
|
1267
|
+
renameSync3(join7(dir, "gitignore"), join7(dir, ".gitignore"));
|
|
1268
|
+
const name = basename4(dir).toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
997
1269
|
for (const file of TEXT_FILES) {
|
|
998
|
-
const path =
|
|
999
|
-
const text =
|
|
1000
|
-
|
|
1270
|
+
const path = join7(dir, file);
|
|
1271
|
+
const text = readFileSync5(path, "utf8").replace(/\{\{name\}\}/g, name).replace(/\{\{version\}\}/g, engineVersion());
|
|
1272
|
+
writeFileSync3(path, text);
|
|
1001
1273
|
}
|
|
1002
1274
|
const pinPnpm = opts.pm.name === "pnpm" && opts.pm.version;
|
|
1003
1275
|
if (pinPnpm || opts.pm.name === "npm") {
|
|
1004
|
-
const path =
|
|
1005
|
-
const pkg = JSON.parse(
|
|
1276
|
+
const path = join7(dir, "package.json");
|
|
1277
|
+
const pkg = JSON.parse(readFileSync5(path, "utf8"));
|
|
1006
1278
|
if (pinPnpm) pkg.packageManager = `pnpm@${opts.pm.version}`;
|
|
1007
1279
|
if (opts.pm.name === "npm") pkg.allowScripts = { esbuild: true };
|
|
1008
|
-
|
|
1280
|
+
writeFileSync3(path, `${JSON.stringify(pkg, null, 2)}
|
|
1009
1281
|
`);
|
|
1010
1282
|
}
|
|
1283
|
+
if (opts.src === false) {
|
|
1284
|
+
const file = opts.template === "ts" ? "game.ts" : "game.js";
|
|
1285
|
+
renameSync3(join7(dir, "src", file), join7(dir, file));
|
|
1286
|
+
rmdirSync(join7(dir, "src"));
|
|
1287
|
+
}
|
|
1011
1288
|
if (!opts.install) return;
|
|
1012
1289
|
const { cmd, args } = installCommand(opts.pm.name);
|
|
1013
1290
|
const ok = await (opts.runInstall ?? spawnInstall)(cmd, args, dir);
|
|
@@ -1024,6 +1301,7 @@ async function init(args) {
|
|
|
1024
1301
|
js: { type: "boolean" },
|
|
1025
1302
|
pm: { type: "string" },
|
|
1026
1303
|
install: { type: "boolean", default: true },
|
|
1304
|
+
src: { type: "boolean", default: true },
|
|
1027
1305
|
help: { type: "boolean", short: "h" }
|
|
1028
1306
|
},
|
|
1029
1307
|
allowPositionals: true,
|
|
@@ -1052,7 +1330,7 @@ ${USAGE3}`);
|
|
|
1052
1330
|
}
|
|
1053
1331
|
pm = values.pm === pm.name ? pm : { name: values.pm, version: null };
|
|
1054
1332
|
}
|
|
1055
|
-
await runInit({ dir, template: values.js ? "js" : "ts", pm, install: values.install });
|
|
1333
|
+
await runInit({ dir, template: values.js ? "js" : "ts", pm, install: values.install, src: values.src });
|
|
1056
1334
|
console.log(`Created ${dir === "." ? "the project" : dir}. Next:
|
|
1057
1335
|
|
|
1058
1336
|
${dir === "." ? "" : ` cd ${dir}
|
|
@@ -1074,6 +1352,7 @@ Options:
|
|
|
1074
1352
|
--pm <name> Package manager: npm, pnpm, yarn or bun
|
|
1075
1353
|
(default: the one that ran this command, else npm)
|
|
1076
1354
|
--no-install Do not install dependencies
|
|
1355
|
+
--no-src Put game.ts (or game.js) in the project root instead of src/
|
|
1077
1356
|
--help, -h Show this help
|
|
1078
1357
|
`;
|
|
1079
1358
|
PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
|
|
@@ -1083,7 +1362,7 @@ Options:
|
|
|
1083
1362
|
|
|
1084
1363
|
// cli/main.ts
|
|
1085
1364
|
init_errors();
|
|
1086
|
-
import { readFileSync as
|
|
1365
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
1087
1366
|
import { parseArgs as parseArgs4 } from "node:util";
|
|
1088
1367
|
var USAGE4 = `Usage: thrunium <command>
|
|
1089
1368
|
|
|
@@ -1098,7 +1377,7 @@ Options:
|
|
|
1098
1377
|
`;
|
|
1099
1378
|
function packageVersion() {
|
|
1100
1379
|
const url = new URL("../package.json", import.meta.url);
|
|
1101
|
-
return JSON.parse(
|
|
1380
|
+
return JSON.parse(readFileSync6(url, "utf8")).version;
|
|
1102
1381
|
}
|
|
1103
1382
|
async function main(argv) {
|
|
1104
1383
|
try {
|