@downdraft/cli 0.4.2 → 0.4.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@downdraft/cli",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/knackstedt/downdraft.git",
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@aws-sdk/client-s3": "^3.700.0",
15
- "@downdraft/engine": "^0.4.2",
15
+ "@downdraft/engine": "^0.4.3",
16
16
  "eta": "^3.5.0",
17
17
  "jimp": "^1.6.1",
18
18
  "resedit": "3.1.0"
@@ -10,7 +10,7 @@
10
10
  // ============================================================================
11
11
 
12
12
  import { execFileSync } from "node:child_process";
13
- import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
13
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
14
14
  import { tmpdir } from "node:os";
15
15
  import { dirname, join } from "node:path";
16
16
 
@@ -31,7 +31,14 @@ export async function resolvePlatLibDir(platKey, candidates, cacheRoot, version)
31
31
 
32
32
  const pkgName = `@downdraft/native-${platKey}`;
33
33
  const cacheDir = join(cacheRoot, "downdraft-native", platKey, "lib");
34
- if (existsSync(cacheDir) && readdirSync(cacheDir).length > 0) return cacheDir;
34
+ const marker = join(cacheRoot, "downdraft-native", platKey, ".version");
35
+ // Cache hit is only valid if it holds the lockstep version — a versionless
36
+ // cache would silently ship stale cdylibs after an engine bump. No marker
37
+ // (pre-versioning cache) → refetch.
38
+ if (existsSync(cacheDir) && readdirSync(cacheDir).length > 0) {
39
+ const cached = existsSync(marker) ? readFileSync(marker, "utf8").trim() : null;
40
+ if (!version || cached === version) return cacheDir;
41
+ }
35
42
 
36
43
  try {
37
44
  const meta = await (await fetch(
@@ -51,6 +58,7 @@ export async function resolvePlatLibDir(platKey, candidates, cacheRoot, version)
51
58
  mkdirSync(dirname(cacheDir), { recursive: true });
52
59
  // cpSync not renameSync: TMPDIR may be a different filesystem.
53
60
  cpSync(libDir, cacheDir, { recursive: true });
61
+ writeFileSync(marker, ver);
54
62
  console.log(`[package] fetched ${pkgName}@${ver} from npm → ${cacheDir}`);
55
63
  return cacheDir;
56
64
  } finally {
@@ -184,6 +184,14 @@ const META_DIR_RE = new RegExp(IMPORT_META.source + String.raw`\.(?:dir|dirname)
184
184
  const META_URL_RE = new RegExp(IMPORT_META.source + String.raw`\.url\b`, "g");
185
185
  const REQUIRE_GLOB_RE = /require\(\s*["'`]@downdraft\/engine\/platform\/glob-polyfill["'`]\s*\)/g;
186
186
  const GLOB_CALLEE_RE = new RegExp(IMPORT_META.source + String.raw`\.glob`);
187
+ // globAssets(pattern, import.meta, opts?) — the meta OBJECT escapes the
188
+ // META_DIR_RE/META_URL_RE token rewrites (its .dir/.url are read inside
189
+ // the engine helper). For emit runtimes the real import.meta points into
190
+ // the bundle tree, not dd-assets; for bun-compiled binaries it flattens
191
+ // to /$bunfs/root — either way every asset glob misses. The meta argument
192
+ // is swapped for a synthetic object bound to the module's staged dir.
193
+ const GLOB_ASSETS_META_RE = new RegExp(
194
+ `\\bglobAssets\\(\\s*(["'\`][^"'\`]*["'\`])\\s*,\\s*${IMPORT_META.source}(\\s*,)?`, "g");
187
195
  const SCRIPT_EXT_RE = /\.(ts|tsx|js|jsx|mts|cts|cjs)$/i; // .mjs staged as an asset (execArgv loaders)
188
196
  const DYN_IMPORT_RE = /import\(\s*([a-zA-Z_$][\w$]*)\s*\)/g;
189
197
 
@@ -433,6 +441,12 @@ const makeEmitPlugin = (collectWorkers) => ({
433
441
  needsGlob = true;
434
442
  out = out.replace(REQUIRE_GLOB_RE, "({ createGlob: __ddCreateGlob })");
435
443
  }
444
+ // 3c. globAssets(pattern, import.meta, ...) — bind the meta object to
445
+ // the staged dir so caller-relative globs resolve under dd-assets.
446
+ out = out.replace(GLOB_ASSETS_META_RE, (m, p1, comma) => {
447
+ needsGlob = true;
448
+ return `globAssets(${p1}, { dir: __ddModDir, url: __ddModUrl, glob: __ddGlob }${comma ?? ""}`;
449
+ });
436
450
  // 4. import.meta.dir/.url/__dirname → module's staged dir under
437
451
  // dd-assets (real FS).
438
452
  const metaOut = out
@@ -660,6 +674,8 @@ if (runtime === "bun") {
660
674
  `SELF="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")"\n` +
661
675
  `DIR="$(dirname "$SELF")"\n` +
662
676
  `export DD_APP_ID="${appId}"\n` +
677
+ `export DOWNDRAFT_PACKAGED=1\n` +
678
+ `export DOWNDRAFT_BUNDLE_DIR="$DIR"\n` +
663
679
  (iconPath ? `export DD_APP_ICON="$DIR/icon.png"\n` : "") +
664
680
  `exec "$DIR/${runtime}" ${rtArgs} "$DIR/bundle/index.js" "$@"\n`);
665
681
  chmodSync(join(appdir, exeName), 0o755);
@@ -713,9 +729,13 @@ if (runtime !== "bun") {
713
729
  }
714
730
 
715
731
  // build.files — electron-builder-style extra files into the appdir root.
732
+ // `dir/**/*` entries resolve to the dir itself (matched files keep their
733
+ // repo-relative structure under <appdir>/dir/).
716
734
  for (const f of (Array.isArray(build.files) ? build.files : []).values()) {
717
- const src = resolve(gameDir, typeof f === "string" ? f : f.from);
718
- const dest = join(appdir, typeof f === "string" ? basename(f) : (f.to ?? basename(f.from)));
735
+ const from = typeof f === "string" ? f : f.from;
736
+ const rel = from.replace(/\/\*\*?\/\*?$/, "").replace(/\/+$/, "");
737
+ const src = resolve(gameDir, rel);
738
+ const dest = join(appdir, typeof f === "string" ? rel : (f.to ?? rel));
719
739
  if (!existsSync(src)) { console.warn(`[package-desktop] files entry missing: ${src}`); continue; }
720
740
  cpSync(src, dest, { recursive: true });
721
741
  }
@@ -163,6 +163,13 @@ const META_DIR_RE = new RegExp(IMPORT_META.source + String.raw`\.(?:dir|dirname)
163
163
  const META_URL_RE = new RegExp(IMPORT_META.source + String.raw`\.url\b`, "g");
164
164
  const REQUIRE_GLOB_RE = /require\(\s*["'`]@downdraft\/engine\/platform\/glob-polyfill["'`]\s*\)/g;
165
165
  const GLOB_CALLEE_RE = new RegExp(IMPORT_META.source + String.raw`\.glob`);
166
+ // globAssets(pattern, import.meta, opts?) — the meta OBJECT escapes the
167
+ // META_DIR_RE/META_URL_RE token rewrites (its .dir/.url are read inside
168
+ // the engine helper); the real import.meta points into the bundle tree,
169
+ // not dd-assets, so every asset glob would miss. The meta argument is
170
+ // swapped for a synthetic object bound to the module's staged dir.
171
+ const GLOB_ASSETS_META_RE = new RegExp(
172
+ `\\bglobAssets\\(\\s*(["'\`][^"'\`]*["'\`])\\s*,\\s*${IMPORT_META.source}(\\s*,)?`, "g");
166
173
  const SCRIPT_EXT_RE = /\.(ts|tsx|js|jsx|mts|cts|cjs)$/i; // .mjs staged as an asset (execArgv loaders)
167
174
  const DYN_IMPORT_RE = /import\(\s*([a-zA-Z_$][\w$]*)\s*\)/g;
168
175
 
@@ -410,6 +417,12 @@ const makePlugin = (collectWorkers) => ({
410
417
  needsGlob = true;
411
418
  out = out.replace(REQUIRE_GLOB_RE, "({ createGlob: __ddCreateGlob })");
412
419
  }
420
+ // 3c. globAssets(pattern, import.meta, ...) — bind the meta object to
421
+ // the staged dir so caller-relative globs resolve under dd-assets.
422
+ out = out.replace(GLOB_ASSETS_META_RE, (m, p1, comma) => {
423
+ needsGlob = true;
424
+ return `globAssets(${p1}, { dir: __ddModDir, url: __ddModUrl, glob: __ddGlob }${comma ?? ""}`;
425
+ });
413
426
  // 4. import.meta.dir/.url/__dirname → module's staged dir under
414
427
  // dd-assets (real FS after extraction).
415
428
  const metaOut = out
@@ -107,6 +107,13 @@ let outfile = outfileArg;
107
107
  if (target.exe && !outfile.endsWith(".exe")) outfile += ".exe";
108
108
 
109
109
  const repoRoot = process.cwd();
110
+ // Bun embeds compile-mode modules keyed relative to the common ancestor of
111
+ // all ENTRYPOINT paths — which is NOT necessarily cwd (a game inside games/
112
+ // whose deps resolve under its own dir gets game-dir keys even when this
113
+ // script runs from the monorepo root). Baked $bunfs specifiers must be
114
+ // relative to that same root or workers/dynamic imports miss the embedded FS.
115
+ // Set after the worker-discovery fixpoint, before the compile pass.
116
+ let embedRoot = repoRoot;
110
117
  const engineConds = engineConditions(argv, resolve(entry));
111
118
  const QUERY_RE = /\?(raw|url|json)$/;
112
119
  const WORKER_RE = /new\s+Worker\(\s*new\s+URL\(\s*["'`]([^"'`]+)["'`]\s*,\s*import\.meta\.url\s*\)/g;
@@ -130,6 +137,14 @@ const META_URL_RE = new RegExp(IMPORT_META.source + String.raw`\.url\b`, "g");
130
137
  // injected __ddCreateGlob binding.
131
138
  const REQUIRE_GLOB_RE = /require\(\s*["'`]@downdraft\/engine\/platform\/glob-polyfill["'`]\s*\)/g;
132
139
  const GLOB_CALLEE_RE = new RegExp(IMPORT_META.source + String.raw`\.glob`);
140
+ // globAssets(pattern, import.meta, opts?) — the meta OBJECT escapes the
141
+ // META_DIR_RE/META_URL_RE token rewrites (its .dir/.url are read inside
142
+ // the engine helper), and in compiled binaries import.meta.dir flattens
143
+ // to /$bunfs/root for every module — the helper would glob a nonexistent
144
+ // root and return {} for every asset query. The meta argument is swapped
145
+ // for a synthetic object bound to the module's staged dir.
146
+ const GLOB_ASSETS_META_RE = new RegExp(
147
+ `\\bglobAssets\\(\\s*(["'\`][^"'\`]*["'\`])\\s*,\\s*${IMPORT_META.source}(\\s*,)?`, "g");
133
148
  const SCRIPT_EXT_RE = /\.(ts|tsx|js|jsx|mts|mjs|cts|cjs)$/i;
134
149
  const DYN_IMPORT_RE = /import\(\s*([a-zA-Z_$][\w$]*)\s*\)/g;
135
150
 
@@ -146,7 +161,7 @@ function resolveSpec(spec, importer) {
146
161
 
147
162
  /** repo-rel path of a module file as embedded in the binary (.js suffix). */
148
163
  function bunfsPath(absPath) {
149
- const rel = relative(repoRoot, absPath).replace(/\\/g, "/").replace(/\.[jt]sx?$/, ".js");
164
+ const rel = relative(embedRoot, absPath).replace(/\\/g, "/").replace(/\.[jt]sx?$/, ".js");
150
165
  // Emitted inside a quoted literal for dynamic import() — Windows needs the
151
166
  // B:\~BUN drive path (with escaped separators); /$bunfs doesn't resolve.
152
167
  return isWin
@@ -338,7 +353,7 @@ const makePlugin = (collectWorkers) => ({
338
353
  let out = src;
339
354
  // 1. Worker URLs first — before import.meta.url is rewritten.
340
355
  out = out.replace(WORKER_RE, (m, spec) => {
341
- const rel = relative(repoRoot, resolve(modDir, spec))
356
+ const rel = relative(embedRoot, resolve(modDir, spec))
342
357
  .replace(/\\/g, "/").replace(/\.[jt]sx?$/, ".js");
343
358
  // Lands inside new URL(...) — Windows wants a file: URL into the
344
359
  // B:\~BUN embedded root; the /$bunfs POSIX form misses it.
@@ -368,6 +383,12 @@ const makePlugin = (collectWorkers) => ({
368
383
  needsGlob = true;
369
384
  out = out.replace(REQUIRE_GLOB_RE, "({ createGlob: __ddCreateGlob })");
370
385
  }
386
+ // 3c. globAssets(pattern, import.meta, ...) — bind the meta object to
387
+ // the staged dir so caller-relative globs resolve under dd-assets.
388
+ out = out.replace(GLOB_ASSETS_META_RE, (m, p1, comma) => {
389
+ needsGlob = true;
390
+ return `globAssets(${p1}, { dir: __ddModDir, url: __ddModUrl, glob: __ddGlob }${comma ?? ""}`;
391
+ });
371
392
  // 4. import.meta.dir / .url / bare __dirname → the module's location
372
393
  // inside the staged dd-assets tree (real FS — sibling asset reads,
373
394
  // glob enumeration, and wasm/binary loads all resolve there).
@@ -428,6 +449,17 @@ while (workerEntries.size !== prevCount) {
428
449
  }
429
450
 
430
451
  const entrypoints = [resolve(entry), ...workerEntries];
452
+ // Bun's embedded-FS root is the common ancestor of the input paths —
453
+ // reproduce it so baked module paths land under the same keys.
454
+ embedRoot = entrypoints
455
+ .map((p) => dirname(p))
456
+ .reduce((lca, dir) => {
457
+ if (!lca) return dir;
458
+ const a = lca.split("/"), b = dir.split("/");
459
+ let i = 0;
460
+ while (i < a.length && a[i] === b[i]) i++;
461
+ return a.slice(0, i).join("/") || "/";
462
+ }, null);
431
463
  if (workerEntries.size) {
432
464
  console.log(`workers: ${[...workerEntries].map((p) => p.split("/").pop()).join(", ")}`);
433
465
  }
package/src/release.ts CHANGED
@@ -256,7 +256,12 @@ function packageNative(
256
256
  // appimage, flatpak). Formats come from --format → build.linux.target
257
257
  // → "dir".
258
258
  const format = opts.format
259
- ?? (Array.isArray(info.build.linux?.target) ? info.build.linux.target.join(",") : info.build.linux?.target)
259
+ ?? (Array.isArray(info.build.linux?.target)
260
+ ? info.build.linux.target
261
+ .map((t: unknown) => (typeof t === "string" ? t : (t as { target?: string }).target ?? ""))
262
+ .filter(Boolean)
263
+ .join(",")
264
+ : info.build.linux?.target)
260
265
  ?? "dir";
261
266
  const argv = [
262
267
  scriptDesktop,