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/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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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 existsSync2, readFileSync as readFileSync2, statSync } from "node:fs";
213
- import { basename, isAbsolute, join as join2, normalize, resolve } from "node:path";
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 = join2(root, "package.json");
217
- if (!existsSync2(manifest)) return "cdn";
405
+ const manifest = join4(root, "package.json");
406
+ if (!existsSync4(manifest)) return "cdn";
218
407
  let pkg;
219
408
  try {
220
- pkg = JSON.parse(readFileSync2(manifest, "utf8"));
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 existsSync2(path) && statSync(path).isFile();
419
+ return existsSync4(path) && statSync2(path).isFile();
231
420
  }
232
421
  function readGameJson(root) {
233
- const file = join2(root, "game.json");
234
- if (!existsSync2(file)) return null;
422
+ const file = join4(root, "game.json");
423
+ if (!existsSync4(file)) return null;
235
424
  let json;
236
425
  try {
237
- json = JSON.parse(readFileSync2(file, "utf8"));
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 = !isAbsolute(entry) && !normalize(entry).split(/[\\/]/).includes("..");
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(join2(root, entry))) throw new UserError(`"entry" in game.json points to ${entry}, which does not exist.`);
444
+ if (!isFile(join4(root, entry))) throw new UserError(`"entry" in game.json points to ${entry}, which does not exist.`);
256
445
  }
257
- function hasEntryKey(file) {
446
+ function hasKey(file, key) {
258
447
  try {
259
- const json = JSON.parse(readFileSync2(file, "utf8"));
260
- return typeof json === "object" && json !== null && !Array.isArray(json) && "entry" in 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 = join2(root, "index.html");
267
- if (existsSync2(indexPath)) {
268
- if (hasEntryKey(join2(root, "game.json"))) {
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
- return { html: readFileSync2(indexPath, "utf8"), htmlSource: "file" };
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(join2(root, candidate)));
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
- return { html: generatePage(game?.name ?? basename(resolve(root)), entry), htmlSource: "generated" };
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 (!existsSync2(join2(root, entry))) {
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 existsSync3, readdirSync, realpathSync, statSync as statSync2, watch } from "node:fs";
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 isAbsolute2, join as join3, normalize as normalize2, relative, resolve as resolve2, sep } from "node:path";
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 = join3(root, "dist");
643
+ const outDir = join5(root, "dist");
427
644
  const realRoot = realpathSync(root);
428
645
  const outTmp = outDir + ".tmp";
429
- const virtualOut = join3(root, ".thrunium-dev");
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 html = transformHtml(project.html, {
545
- engineTag: project.usesModules ? ENGINE_TAG : null,
546
- entryMap,
547
- inject: RELOAD_CLIENT,
548
- styles
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
- const file = normalize2(join3(root, pathname));
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) || isAbsolute2(rel) || !existsSync3(file) || !statSync2(file).isFile()) {
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) || isAbsolute2(realRel)) {
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 statSync2(abs).isDirectory();
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
- if (checkEntries) {
602
- const next = findModuleEntries(loadProject(root).html);
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 = join3(root, name);
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 (existsSync3(path)) console.error(`thrunium: file watching stopped for ${path}: ${err.message}`);
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(join3(root, entry.name));
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) && !existsSync3(join3(dir, name))) return;
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 || abs === join3(root, "index.html") || abs === join3(root, "game.json");
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 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";
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 = join4(dir, entry.name);
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) || isAbsolute3(rel)) {
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 = join4(root, ".thrunium-out");
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", readFileSync3(engineFile("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 (existsSync4(map)) written.set("thrunium.min.js.map", readFileSync3(map));
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
- const dest = join4(tmp, file);
881
- mkdirSync(dirname2(dest), { recursive: true });
882
- copyFileSync(join4(root, file), dest);
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 = join4(tmp, file);
886
- mkdirSync(dirname2(dest), { recursive: true });
887
- writeFileSync(dest, contents);
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
- renameSync(tmp, outDir);
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(statSync3(join4(outDir, f)).size)}`);
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, join4(dir, e.name)) : [relative2(base, join4(dir, e.name)).split(sep2).join("/")]
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 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";
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 (existsSync5(dir)) {
987
- if (!statSync4(dir).isDirectory() || readdirSync3(dir).length > 0) {
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
- mkdirSync2(dir, { recursive: true });
1263
+ mkdirSync3(dir, { recursive: true });
992
1264
  }
993
- cpSync(join5(packageRoot(), "templates", opts.template), dir, { recursive: true });
994
- if (opts.pm.name === "pnpm") cpSync(join5(packageRoot(), "templates", "pnpm"), dir, { recursive: true });
995
- renameSync2(join5(dir, "gitignore"), join5(dir, ".gitignore"));
996
- const name = basename3(dir).toLowerCase().replace(/[^a-z0-9-]/g, "-");
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 = join5(dir, file);
999
- const text = readFileSync4(path, "utf8").replace(/\{\{name\}\}/g, name).replace(/\{\{version\}\}/g, engineVersion());
1000
- writeFileSync2(path, text);
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 = join5(dir, "package.json");
1005
- const pkg = JSON.parse(readFileSync4(path, "utf8"));
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
- writeFileSync2(path, `${JSON.stringify(pkg, null, 2)}
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 readFileSync5 } from "node:fs";
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(readFileSync5(url, "utf8")).version;
1380
+ return JSON.parse(readFileSync6(url, "utf8")).version;
1102
1381
  }
1103
1382
  async function main(argv) {
1104
1383
  try {