@downdraft/cli 0.0.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/package.json +36 -3
- package/scripts/default-icon.png +0 -0
- package/scripts/package-desktop.mjs +931 -0
- package/scripts/package-mobile.mjs +822 -0
- package/scripts/package-native.mjs +638 -0
- package/src/args.ts +333 -0
- package/src/assets.ts +523 -0
- package/src/blob-store-s3.ts +145 -0
- package/src/build-games.ts +45 -0
- package/src/build.ts +40 -0
- package/src/bun-env.d.ts +6 -0
- package/src/debug.ts +56 -0
- package/src/dev.ts +231 -0
- package/src/dist.ts +36 -0
- package/src/export.ts +38 -0
- package/src/index.ts +131 -0
- package/src/list-games.ts +37 -0
- package/src/mcp.ts +320 -0
- package/src/new.ts +63 -0
- package/src/paths.ts +101 -0
- package/src/plugin-command.ts +214 -0
- package/src/process-utils.ts +179 -0
- package/src/release.ts +432 -0
- package/src/scaffold-plugin.ts +280 -0
- package/src/scaffold.ts +207 -0
- package/src/test.ts +187 -0
- package/src/usage.ts +256 -0
- package/templates/_shared/ai-companion/devin-config.eta +11 -0
- package/templates/_shared/ai-companion/engine-prompt.eta +155 -0
- package/templates/_shared/ai-companion/logging-rule.eta +3 -0
- package/templates/_shared/ai-companion/loops-rule.eta +3 -0
- package/templates/_shared/partials/config-json-fields.eta +3 -0
- package/templates/_shared/partials/dev-deps.eta +8 -0
- package/templates/_shared/partials/package-json-fields.eta +12 -0
- package/templates/_shared/partials/tsconfig.eta +16 -0
- package/templates/_shared/partials/vscode-extensions.eta +11 -0
- package/templates/_shared/partials/vscode-settings.eta +29 -0
- package/templates/_shared/partials/vscode-tasks.eta +93 -0
- package/templates/full/.gitignore.eta +5 -0
- package/templates/full/.vscode/extensions.json.eta +1 -0
- package/templates/full/.vscode/settings.json.eta +1 -0
- package/templates/full/.vscode/tasks.json.eta +1 -0
- package/templates/full/README.md.eta +45 -0
- package/templates/full/assets/shaders/.eta +0 -0
- package/templates/full/downdraft.config.json.eta +10 -0
- package/templates/full/package.json.eta +9 -0
- package/templates/full/src/entities/.eta +0 -0
- package/templates/full/src/game-module.ts.eta +30 -0
- package/templates/full/src/native-entry.ts.eta +14 -0
- package/templates/full/src/sim-host.ts.eta +19 -0
- package/templates/full/src/sim-worker.ts.eta +99 -0
- package/templates/full/src/systems/.eta +0 -0
- package/templates/full/tsconfig.json.eta +1 -0
- package/templates/gamemodule/README.md.eta +32 -0
- package/templates/gamemodule/downdraft.config.json.eta +3 -0
- package/templates/gamemodule/package.json.eta +9 -0
- package/templates/gamemodule/src/game-module.ts.eta +63 -0
- package/templates/gamemodule/src/native-entry.ts.eta +14 -0
- package/templates/gamemodule/src/sim-host.ts.eta +25 -0
- package/templates/gamemodule/src/sim-worker.ts.eta +24 -0
- package/templates/gamemodule/tsconfig.json.eta +1 -0
- package/templates/minimal/.gitignore.eta +5 -0
- package/templates/minimal/.vscode/extensions.json.eta +1 -0
- package/templates/minimal/.vscode/settings.json.eta +1 -0
- package/templates/minimal/.vscode/tasks.json.eta +1 -0
- package/templates/minimal/README.md.eta +32 -0
- package/templates/minimal/downdraft.config.json.eta +3 -0
- package/templates/minimal/package.json.eta +9 -0
- package/templates/minimal/src/game-module.ts.eta +44 -0
- package/templates/minimal/src/native-entry.ts.eta +14 -0
- package/templates/minimal/tsconfig.json.eta +1 -0
- package/templates/physics/.gitignore.eta +5 -0
- package/templates/physics/.vscode/extensions.json.eta +1 -0
- package/templates/physics/.vscode/settings.json.eta +1 -0
- package/templates/physics/.vscode/tasks.json.eta +1 -0
- package/templates/physics/README.md.eta +36 -0
- package/templates/physics/downdraft.config.json.eta +6 -0
- package/templates/physics/package.json.eta +9 -0
- package/templates/physics/src/game-module.ts.eta +29 -0
- package/templates/physics/src/native-entry.ts.eta +14 -0
- package/templates/physics/src/sim-host.ts.eta +19 -0
- package/templates/physics/src/sim-worker.ts.eta +155 -0
- package/templates/physics/tsconfig.json.eta +1 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// package-native.mjs — compile a game's native entry into a standalone Bun
|
|
3
|
+
// binary plus a staged runtime tree:
|
|
4
|
+
//
|
|
5
|
+
// bun packages/cli/scripts/package-native.mjs games/mining-rpg/src/native-entry.ts /tmp/out/mining
|
|
6
|
+
// bun scripts/package-native.mjs --mcp <entry> <outfile> # retain the MCP endpoint
|
|
7
|
+
// --target=win|linux|mac cross-compile (default: host)
|
|
8
|
+
// --mode=dev|debug|prod prod minifies (default: prod)
|
|
9
|
+
// --product-name=<name> PE/branding product name (win target)
|
|
10
|
+
// --product-version=<v> PE/branding version (win target)
|
|
11
|
+
// --icon=<path> .ico/.png icon to stamp (win target)
|
|
12
|
+
//
|
|
13
|
+
// By default the in-process MCP automation endpoint is compiled OUT of
|
|
14
|
+
// packaged binaries (__DD_MCP_STRIP__ define → dead code). --mcp retains it:
|
|
15
|
+
// the endpoint still stays off at runtime unless DOWNDRAFT_MCP=1 is set
|
|
16
|
+
// (see native-host.ts).
|
|
17
|
+
//
|
|
18
|
+
// Produces:
|
|
19
|
+
// <outfile> compiled binary
|
|
20
|
+
// <outdir>/native/*.so platform lib + engine cdylibs (dlopen'd)
|
|
21
|
+
// <outdir>/dd-assets/<repo-rel> ?url imports + glob-matched asset files
|
|
22
|
+
//
|
|
23
|
+
// The runtime Bun.plugin() loaders in bun-preload.ts handle ?raw/.wgsl/?url
|
|
24
|
+
// for `bun run`, but Bun.build resolves modules on disk first — query
|
|
25
|
+
// suffixes fail resolution before onLoad fires. This plugin mirrors those
|
|
26
|
+
// loaders with the build-time contract (onResolve strips the suffix, onLoad
|
|
27
|
+
// returns contents + a real loader).
|
|
28
|
+
//
|
|
29
|
+
// Packaging transforms (compile pass only):
|
|
30
|
+
// - `new Worker(new URL("x.ts", import.meta.url))` — workers are embedded
|
|
31
|
+
// as extra entrypoints; the specifier is rewritten to the module's
|
|
32
|
+
// /$bunfs/root/<repo-rel>.js path so it resolves inside the binary.
|
|
33
|
+
// - `new URL("asset.png", import.meta.url)` (non-script) — rewritten to a
|
|
34
|
+
// file:// URL into the staged dd-assets tree.
|
|
35
|
+
// - `import.meta.glob(...)` — rewritten to createGlob(<repo-rel-dir>),
|
|
36
|
+
// which maps onto dd-assets in packaged builds.
|
|
37
|
+
// - `import.meta.dir` / `.dirname` / `.url` — rewritten to the module's
|
|
38
|
+
// /$bunfs/root/<repo-rel> path so glob/worker resolution behaves like
|
|
39
|
+
// dev. (In compiled binaries every module reports import.meta.dir as
|
|
40
|
+
// /$bunfs/root — the source directory must be baked in per module.)
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
import { createHash } from "node:crypto";
|
|
44
|
+
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
45
|
+
import { createRequire } from "node:module";
|
|
46
|
+
import { tmpdir } from "node:os";
|
|
47
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
48
|
+
import { fileURLToPath } from "node:url";
|
|
49
|
+
|
|
50
|
+
// CRATES is shared with the repo's scripts/native-crates.mjs when this file
|
|
51
|
+
// runs inside the monorepo. In a published @downdraft/cli install the repo
|
|
52
|
+
// scripts tree doesn't exist — fall back to the platform lib only (engine
|
|
53
|
+
// cdylibs are additionally probed inside the installed @downdraft/engine
|
|
54
|
+
// package below, so standalone packaging still finds them).
|
|
55
|
+
let CRATES = [];
|
|
56
|
+
let libFileName = (libName, nodePlat) =>
|
|
57
|
+
nodePlat.startsWith("win32") ? `${libName}.dll`
|
|
58
|
+
: nodePlat.startsWith("darwin") ? `lib${libName}.dylib` : `lib${libName}.so`;
|
|
59
|
+
try {
|
|
60
|
+
({ CRATES, libFileName } = await import(
|
|
61
|
+
fileURLToPath(new URL("../../../scripts/native-crates.mjs", import.meta.url))));
|
|
62
|
+
} catch { /* published package — engine crates probed via node resolution */ }
|
|
63
|
+
|
|
64
|
+
const argv = process.argv.slice(2);
|
|
65
|
+
// --mcp retains the MCP automation endpoint in the packaged binary (still
|
|
66
|
+
// gated at runtime by DOWNDRAFT_MCP=1). Default: strip it.
|
|
67
|
+
const retainMcp = argv.includes("--mcp");
|
|
68
|
+
// --target=<win|linux|mac> selects the Bun compile target and the platform
|
|
69
|
+
// arch used for native-lib staging. Default: the host platform.
|
|
70
|
+
const targetArg = argv.find((a) => a.startsWith("--target="))?.split("=")[1];
|
|
71
|
+
// --mode=<dev|debug|prod> — prod minifies; dev/debug leave the bundle readable.
|
|
72
|
+
const modeArg = argv.find((a) => a.startsWith("--mode="))?.split("=")[1] ?? "prod";
|
|
73
|
+
// Branding flags — value may itself contain '=' so split on the first only.
|
|
74
|
+
const flagValue = (name) => argv.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 2);
|
|
75
|
+
const productNameFlag = flagValue("product-name");
|
|
76
|
+
const productVersionFlag = flagValue("product-version");
|
|
77
|
+
const iconFlag = flagValue("icon");
|
|
78
|
+
const positional = argv.filter((a) => !a.startsWith("--"));
|
|
79
|
+
const [entry, outfileArg] = positional;
|
|
80
|
+
if (!entry || !outfileArg) {
|
|
81
|
+
console.error("usage: bun package-native.mjs [--mcp] [--target=win|linux|mac] [--mode=dev|debug|prod] <entry.ts> <outfile>");
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Target triple: release vocabulary (win|linux|mac) → bun compile target +
|
|
86
|
+
// node <platform>-<arch> key used for native lib staging. mac targets Apple
|
|
87
|
+
// Silicon by default (the dominant desktop arch); win/linux target x64.
|
|
88
|
+
const TARGETS = {
|
|
89
|
+
win: { plat: "win32-x64", bun: "bun-windows-x64", exe: true },
|
|
90
|
+
linux: { plat: "linux-x64", bun: "bun-linux-x64", exe: false },
|
|
91
|
+
mac: { plat: "darwin-arm64", bun: "bun-darwin-arm64", exe: false },
|
|
92
|
+
};
|
|
93
|
+
const hostKey = `${process.platform}-${process.arch}`;
|
|
94
|
+
if (targetArg && !TARGETS[targetArg]) {
|
|
95
|
+
console.error(`unknown --target "${targetArg}" — expected win, linux, or mac`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
const target = targetArg ? TARGETS[targetArg] : { plat: hostKey, bun: undefined, exe: process.platform === "win32" };
|
|
99
|
+
let outfile = outfileArg;
|
|
100
|
+
if (target.exe && !outfile.endsWith(".exe")) outfile += ".exe";
|
|
101
|
+
|
|
102
|
+
const repoRoot = process.cwd();
|
|
103
|
+
const QUERY_RE = /\?(raw|url|json)$/;
|
|
104
|
+
const WORKER_RE = /new\s+Worker\(\s*new\s+URL\(\s*["'`]([^"'`]+)["'`]\s*,\s*import\.meta\.url\s*\)/g;
|
|
105
|
+
const ASSET_URL_RE = /new\s+URL\(\s*["'`]([^"'`]+)["'`]\s*,\s*import\.meta\.url\s*\)/g;
|
|
106
|
+
// import.meta is frequently written as `(import.meta as any)` in this
|
|
107
|
+
// codebase — both the bare and parenthesized forms must match.
|
|
108
|
+
const IMPORT_META = /(?:\(\s*import\.meta\s+as\s+any\s*\)|import\.meta(?:\s+as\s+any)?)/;
|
|
109
|
+
const GLOB_RE = new RegExp(IMPORT_META.source + String.raw`\.glob\(\s*(["'\`])([^"'\`]+)\1`, "g");
|
|
110
|
+
// Staging scan covers all glob call forms: import.meta.glob("pat"), the
|
|
111
|
+
// common _glob("pat") alias, and createGlob(modDir)("pat").
|
|
112
|
+
const STAGE_GLOB_RE = new RegExp(
|
|
113
|
+
`(?:${IMPORT_META.source}\\.glob|_glob|globAssets)\\(\\s*(["'\`])([^"'\`]+)\\1|createGlob\\([^)]*\\)\\(\\s*(["'\`])([^"'\`]+)\\3`, "g");
|
|
114
|
+
// Module-relative file reads: resolve(__dirname, "./zstd.wasm"),
|
|
115
|
+
// join(import.meta.dir, "data.bin") — stage the referenced file.
|
|
116
|
+
const STAGE_READ_RE = new RegExp(
|
|
117
|
+
String.raw`(?:resolve|join)\(\s*(?:__dirname|${IMPORT_META.source}\.(?:dir|dirname|url))\s*,\s*(["'\`])([^"'\`]+)\1`, "g");
|
|
118
|
+
const META_DIR_RE = new RegExp(IMPORT_META.source + String.raw`\.(?:dir|dirname)\b`, "g");
|
|
119
|
+
const META_URL_RE = new RegExp(IMPORT_META.source + String.raw`\.url\b`, "g");
|
|
120
|
+
// require("@downdraft/engine/platform/glob-polyfill") — a runtime package
|
|
121
|
+
// specifier can't resolve inside a compiled binary; rewrite to the
|
|
122
|
+
// injected __ddCreateGlob binding.
|
|
123
|
+
const REQUIRE_GLOB_RE = /require\(\s*["'`]@downdraft\/engine\/platform\/glob-polyfill["'`]\s*\)/g;
|
|
124
|
+
const GLOB_CALLEE_RE = new RegExp(IMPORT_META.source + String.raw`\.glob`);
|
|
125
|
+
const SCRIPT_EXT_RE = /\.(ts|tsx|js|jsx|mts|mjs|cts|cjs)$/i;
|
|
126
|
+
const DYN_IMPORT_RE = /import\(\s*([a-zA-Z_$][\w$]*)\s*\)/g;
|
|
127
|
+
|
|
128
|
+
function escapeRe(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }
|
|
129
|
+
|
|
130
|
+
/** Resolve a module specifier to an absolute path (relative or bare). */
|
|
131
|
+
function resolveSpec(spec, importer) {
|
|
132
|
+
try {
|
|
133
|
+
return spec.startsWith(".") || spec.startsWith("/")
|
|
134
|
+
? resolve(dirname(importer), spec)
|
|
135
|
+
: Bun.resolveSync(spec, dirname(importer));
|
|
136
|
+
} catch { return null; }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** repo-rel path of a module file as embedded in the binary (.js suffix). */
|
|
140
|
+
function bunfsPath(absPath) {
|
|
141
|
+
return `/$bunfs/root/${relative(repoRoot, absPath).replace(/\\/g, "/").replace(/\.[jt]sx?$/, ".js")}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Worker files referenced via `new Worker(new URL("...", import.meta.url))`
|
|
145
|
+
// must be passed as extra entrypoints — Bun bundles each one separately into
|
|
146
|
+
// the binary. Discovered by scanning every module that enters the graph.
|
|
147
|
+
const workerEntries = new Set();
|
|
148
|
+
|
|
149
|
+
// Dynamic imports with a variable specifier (`const spec = "..."; import(spec)`)
|
|
150
|
+
// are invisible to the bundler — used to keep platform-native out of browser
|
|
151
|
+
// bundles. In packaged binaries they can only resolve to /$bunfs paths of
|
|
152
|
+
// modules embedded as extra entrypoints, so we resolve the bound literal,
|
|
153
|
+
// embed it, and rewrite the specifier. Keyed by `<file>:<ident>` → spec info.
|
|
154
|
+
const dynImports = new Map();
|
|
155
|
+
|
|
156
|
+
// Files staged under <outdir>/dd-assets/<rel>: ?url imports, asset
|
|
157
|
+
// new URL(...) refs, and import.meta.glob matches.
|
|
158
|
+
const assetFiles = new Set();
|
|
159
|
+
|
|
160
|
+
/** Repo-relative staging path for an absolute file path. */
|
|
161
|
+
function assetRel(absPath) {
|
|
162
|
+
const rel = relative(repoRoot, absPath).replace(/\\/g, "/");
|
|
163
|
+
if (!rel.startsWith("..")) return rel;
|
|
164
|
+
// Outside the repo (e.g. standalone node_modules) — hash the DIRECTORY so
|
|
165
|
+
// that a module's baked dirname and its sibling assets land together.
|
|
166
|
+
const h = createHash("sha1").update(dirname(absPath)).digest("hex").slice(0, 8);
|
|
167
|
+
return `external/${h}/${basename(absPath)}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function walkDir(dir, results = []) {
|
|
171
|
+
let entries;
|
|
172
|
+
try { entries = readdirSync(dir); } catch { return results; }
|
|
173
|
+
for (let _i = 0, _it = entries, _n = _it.length; _i < _n; _i++) { const e = _it[_i];
|
|
174
|
+
const full = `${dir}/${e}`;
|
|
175
|
+
let st;
|
|
176
|
+
try { st = statSync(full); } catch { continue; }
|
|
177
|
+
if (st.isDirectory()) walkDir(full, results);
|
|
178
|
+
else results.push(full);
|
|
179
|
+
}
|
|
180
|
+
return results;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const makePlugin = (collectWorkers) => ({
|
|
184
|
+
name: "downdraft-native-build",
|
|
185
|
+
setup(build) {
|
|
186
|
+
const resolveImport = (spec, importer) => {
|
|
187
|
+
// Relative → join on the importer; bare specifiers → Bun's resolver
|
|
188
|
+
// (workspace package specifiers like @downdraft/engine/... need it).
|
|
189
|
+
const base = spec.startsWith(".") || spec.startsWith("/")
|
|
190
|
+
? resolve(dirname(importer), spec)
|
|
191
|
+
: Bun.resolveSync(spec, dirname(importer));
|
|
192
|
+
// Engine convention: `./x.wgsl` may resolve to a TS module `x.wgsl.ts`.
|
|
193
|
+
return existsSync(base) ? base : (existsSync(`${base}.ts`) ? `${base}.ts` : base);
|
|
194
|
+
};
|
|
195
|
+
// Strip ?raw / ?url / ?json so the bundler resolves the real file.
|
|
196
|
+
build.onResolve({ filter: /\?(raw|url|json)$/ }, (args) => {
|
|
197
|
+
const clean = args.path.replace(QUERY_RE, "");
|
|
198
|
+
const kind = args.path.match(QUERY_RE)[1];
|
|
199
|
+
const resolved = resolveImport(clean, args.importer);
|
|
200
|
+
// Engine convention: `./x.wgsl` may resolve to a TS module x.wgsl.ts —
|
|
201
|
+
// those are real TS modules, so leave them in the default namespace.
|
|
202
|
+
if (resolved.endsWith(".ts")) return { path: resolved };
|
|
203
|
+
return { path: resolved, namespace: `dd-${kind}` };
|
|
204
|
+
});
|
|
205
|
+
// ?raw / bare .wgsl/.glsl → text default export (matches bun-preload).
|
|
206
|
+
build.onLoad({ filter: /.*/, namespace: "dd-raw" }, (args) => ({
|
|
207
|
+
contents: `export default ${JSON.stringify(readFileSync(args.path, "utf-8"))};`,
|
|
208
|
+
loader: "js",
|
|
209
|
+
}));
|
|
210
|
+
// ?json → parse at build time, embed as a JS literal.
|
|
211
|
+
build.onLoad({ filter: /.*/, namespace: "dd-json" }, (args) => ({
|
|
212
|
+
contents: `export default ${JSON.stringify(JSON.parse(readFileSync(args.path, "utf-8")))};`,
|
|
213
|
+
loader: "js",
|
|
214
|
+
}));
|
|
215
|
+
// ?url → file:// URL into the staged dd-assets tree (packaged) or the
|
|
216
|
+
// absolute path (scan pass — keeps dev-mode parity for non-compiled use).
|
|
217
|
+
build.onLoad({ filter: /.*/, namespace: "dd-url" }, (args) => {
|
|
218
|
+
assetFiles.add(args.path);
|
|
219
|
+
if (collectWorkers) {
|
|
220
|
+
return { contents: `export default ${JSON.stringify(args.path)};`, loader: "js" };
|
|
221
|
+
}
|
|
222
|
+
const rel = assetRel(args.path);
|
|
223
|
+
return {
|
|
224
|
+
contents:
|
|
225
|
+
`import { dirname as __ddDirname } from "node:path";` +
|
|
226
|
+
`import { pathToFileURL as __ddP2F } from "node:url";` +
|
|
227
|
+
`export default __ddP2F(__ddDirname(process.execPath) + "/dd-assets/" + ${JSON.stringify(rel)}).href;`,
|
|
228
|
+
loader: "js",
|
|
229
|
+
};
|
|
230
|
+
});
|
|
231
|
+
// Bare .wgsl/.glsl imports (no ?raw suffix) → text default export.
|
|
232
|
+
["wgsl", "glsl"].forEach((ext) => {
|
|
233
|
+
build.onResolve({ filter: new RegExp(`\\.${ext}$`) }, (args) => {
|
|
234
|
+
const resolved = resolveImport(args.path, args.importer);
|
|
235
|
+
if (resolved.endsWith(".ts")) return { path: resolved };
|
|
236
|
+
return { path: resolved, namespace: "dd-raw" };
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
// CSS — no DOM in native mode; discard like the runtime loader does.
|
|
240
|
+
// Resolve to an absolute path — a relative namespaced path can be
|
|
241
|
+
// re-serialized as "dd-css:./x.css" and rejected as a bogus package.
|
|
242
|
+
// Unresolvable package-rooted CSS (exports-map-hidden) still stubs out.
|
|
243
|
+
build.onResolve({ filter: /\.css$/ }, (args) => {
|
|
244
|
+
// A re-serialized "dd-css:<path>" specifier (Bun transpile cache replay)
|
|
245
|
+
// must be returned unchanged — resolving it as a package/relative path
|
|
246
|
+
// wraps it in another "dd-css:<dir>" layer each pass until ENAMETOOLONG.
|
|
247
|
+
if (args.path.startsWith("dd-css:")) {
|
|
248
|
+
return { path: args.path.slice("dd-css:".length), namespace: "dd-css" };
|
|
249
|
+
}
|
|
250
|
+
let path;
|
|
251
|
+
try { path = resolveImport(args.path, args.importer); }
|
|
252
|
+
catch { path = resolve(dirname(args.importer), args.path); }
|
|
253
|
+
return { path, namespace: "dd-css" };
|
|
254
|
+
});
|
|
255
|
+
build.onLoad({ filter: /.*/, namespace: "dd-css" }, () => ({
|
|
256
|
+
contents: `export default "";`, loader: "js",
|
|
257
|
+
}));
|
|
258
|
+
// Scan every real source file for Worker entrypoints, asset refs, and
|
|
259
|
+
// glob patterns. In the compile pass, rewrite module-local paths:
|
|
260
|
+
// inside a compiled binary import.meta flattens to /$bunfs/root, so
|
|
261
|
+
// each module's original repo-relative location is baked in.
|
|
262
|
+
build.onLoad({ filter: /\.[jt]sx?$/ }, (args) => {
|
|
263
|
+
const src = readFileSync(args.path, "utf-8");
|
|
264
|
+
const modDir = dirname(args.path);
|
|
265
|
+
const modRel = assetRel(args.path);
|
|
266
|
+
const modBase = basename(modRel).replace(/\.[jt]sx?$/, ".js");
|
|
267
|
+
const stagedDir = dirname(modRel);
|
|
268
|
+
|
|
269
|
+
// Worker entrypoints — needed in both passes (pass 1 collects).
|
|
270
|
+
for (const m of src.matchAll(WORKER_RE)) {
|
|
271
|
+
const p = resolve(modDir, m[1]);
|
|
272
|
+
if (existsSync(p)) workerEntries.add(p);
|
|
273
|
+
}
|
|
274
|
+
// Asset refs: new URL("x.png", import.meta.url) where x isn't a script.
|
|
275
|
+
for (const m of src.matchAll(ASSET_URL_RE)) {
|
|
276
|
+
if (SCRIPT_EXT_RE.test(m[1])) continue;
|
|
277
|
+
const p = resolve(modDir, m[1]);
|
|
278
|
+
if (existsSync(p) && statSync(p).isFile()) assetFiles.add(p);
|
|
279
|
+
}
|
|
280
|
+
// Dynamic imports through variable specifiers: `const s = "spec";
|
|
281
|
+
// await import(s)`. The bundler can't see them — resolve the bound
|
|
282
|
+
// literal, embed the module, and rewrite the specifier to its
|
|
283
|
+
// /$bunfs path in the compile pass.
|
|
284
|
+
for (const m of src.matchAll(DYN_IMPORT_RE)) {
|
|
285
|
+
const ident = m[1];
|
|
286
|
+
const bind = src.match(new RegExp("(?:const|let|var)\\s+" + escapeRe(ident) + "\\s*=\\s*([\"'`])([^\"'`]+)\\1"));
|
|
287
|
+
if (!bind) continue;
|
|
288
|
+
const spec = bind[2];
|
|
289
|
+
if (!spec || spec.startsWith("node:") || spec.startsWith("bun:")) continue;
|
|
290
|
+
const abs = resolveSpec(spec, args.path);
|
|
291
|
+
if (!abs || !existsSync(abs)) continue;
|
|
292
|
+
workerEntries.add(abs);
|
|
293
|
+
dynImports.set(`${args.path}:${ident}`, { ident, spec, abs });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Module-relative file reads — stage so the baked dirname resolves.
|
|
297
|
+
for (const m of src.matchAll(STAGE_READ_RE)) {
|
|
298
|
+
if (SCRIPT_EXT_RE.test(m[2])) continue;
|
|
299
|
+
const p = resolve(modDir, m[2]);
|
|
300
|
+
if (existsSync(p) && statSync(p).isFile()) assetFiles.add(p);
|
|
301
|
+
}
|
|
302
|
+
// Glob patterns — stage the entire subtree under the pattern's
|
|
303
|
+
// static prefix. Matched files routinely reference siblings the
|
|
304
|
+
// glob doesn't cover (GLB → ../Textures/x.png, mod.json →
|
|
305
|
+
// shaders/*.wgsl), so the whole asset root must ship. Covers
|
|
306
|
+
// import.meta.glob (rewritten below), the _glob alias, and
|
|
307
|
+
// createGlob(modDir)(...) — the latter two resolve through
|
|
308
|
+
// import.meta.dir/__dirname, fixed by the baked-dir rewrite.
|
|
309
|
+
for (const m of src.matchAll(STAGE_GLOB_RE)) {
|
|
310
|
+
const pattern = m[2] ?? m[4];
|
|
311
|
+
if (!pattern) continue;
|
|
312
|
+
const rootParts = [];
|
|
313
|
+
for (const p of pattern.split("/")) {
|
|
314
|
+
if (p.includes("*") || p.includes("{")) break;
|
|
315
|
+
rootParts.push(p);
|
|
316
|
+
}
|
|
317
|
+
const searchDir = resolve(modDir, ...rootParts);
|
|
318
|
+
if (existsSync(searchDir) && statSync(searchDir).isDirectory()) {
|
|
319
|
+
for (const f of walkDir(searchDir)) assetFiles.add(f);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (collectWorkers) return undefined;
|
|
324
|
+
|
|
325
|
+
let out = src;
|
|
326
|
+
// 1. Worker URLs first — before import.meta.url is rewritten.
|
|
327
|
+
out = out.replace(WORKER_RE, (m, spec) => {
|
|
328
|
+
const rel = relative(repoRoot, resolve(modDir, spec))
|
|
329
|
+
.replace(/\\/g, "/").replace(/\.[jt]sx?$/, ".js");
|
|
330
|
+
return m.replace(`"${spec}"`, `"/$bunfs/root/${rel}"`)
|
|
331
|
+
.replace(`'${spec}'`, `"/$bunfs/root/${rel}"`)
|
|
332
|
+
.replace(`\`${spec}\``, `"/$bunfs/root/${rel}"`);
|
|
333
|
+
});
|
|
334
|
+
// 2. Asset URL refs need no rewrite — import.meta.url already points
|
|
335
|
+
// at the staged dd-assets dir, so new URL("./x.png", import.meta.url)
|
|
336
|
+
// resolves there on its own.
|
|
337
|
+
// 2b. Variable-specifier dynamic imports → embedded $bunfs path.
|
|
338
|
+
for (const m of out.matchAll(DYN_IMPORT_RE)) {
|
|
339
|
+
const ident = m[1];
|
|
340
|
+
const info = dynImports.get(`${args.path}:${ident}`);
|
|
341
|
+
if (!info) continue;
|
|
342
|
+
const decl = new RegExp("((?:const|let|var)\\s+" + escapeRe(ident) + "\\s*=\\s*)([\"'`])" + escapeRe(info.spec) + "\\2");
|
|
343
|
+
out = out.replace(decl, `$1$2${bunfsPath(info.abs)}$2`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// 3. import.meta.glob → createGlob bound to the module's staged dir.
|
|
347
|
+
let needsGlob = false;
|
|
348
|
+
out = out.replace(GLOB_RE, (m) => { needsGlob = true; return m.replace(GLOB_CALLEE_RE, "__ddGlob"); });
|
|
349
|
+
// 3b. require() of the glob-polyfill package — unresolvable inside a
|
|
350
|
+
// compiled binary; swap in the injected import binding.
|
|
351
|
+
if (REQUIRE_GLOB_RE.test(out)) {
|
|
352
|
+
needsGlob = true;
|
|
353
|
+
out = out.replace(REQUIRE_GLOB_RE, "({ createGlob: __ddCreateGlob })");
|
|
354
|
+
}
|
|
355
|
+
// 4. import.meta.dir / .url / bare __dirname → the module's location
|
|
356
|
+
// inside the staged dd-assets tree (real FS — sibling asset reads,
|
|
357
|
+
// glob enumeration, and wasm/binary loads all resolve there).
|
|
358
|
+
// Bun flattens all of these to /$bunfs/root in compiled binaries.
|
|
359
|
+
// Apply unconditionally — a file that only reads import.meta.url for
|
|
360
|
+
// a `new URL("./x.png", ...)` still needs the rewrite even though no
|
|
361
|
+
// earlier step changed its source.
|
|
362
|
+
const metaOut = out
|
|
363
|
+
.replace(META_DIR_RE, "__ddModDir")
|
|
364
|
+
.replace(META_URL_RE, "__ddModUrl")
|
|
365
|
+
.replace(/(?<![.\w$])__dirname\b/g, "__ddModDir");
|
|
366
|
+
if (metaOut !== out || needsGlob) {
|
|
367
|
+
out =
|
|
368
|
+
`import { dirname as __ddDirname } from "node:path";\n` +
|
|
369
|
+
`import { pathToFileURL as __ddP2F } from "node:url";\n` +
|
|
370
|
+
`const __ddModDir = __ddDirname(process.execPath) + "/dd-assets/" + ${JSON.stringify(stagedDir === "." ? "" : stagedDir + "/")}.replace(/\\/$/, "");\n` +
|
|
371
|
+
`const __ddModUrl = __ddP2F(__ddModDir + "/" + ${JSON.stringify(modBase)}).href;\n` +
|
|
372
|
+
(needsGlob
|
|
373
|
+
? `import { createGlob as __ddCreateGlob } from "@downdraft/engine/platform/glob-polyfill";\n` +
|
|
374
|
+
// Lazy — eager construction runs at module-init time and can
|
|
375
|
+
// hit a TDZ on consts declared later in the same module
|
|
376
|
+
// (glob-polyfill.ts itself matches REQUIRE_GLOB_RE via a
|
|
377
|
+
// comment).
|
|
378
|
+
`const __ddGlob = (p, o) => __ddCreateGlob(__ddModDir)(p, o);\n`
|
|
379
|
+
: "") +
|
|
380
|
+
metaOut;
|
|
381
|
+
}
|
|
382
|
+
const loader = { ts: "ts", tsx: "tsx", js: "js", jsx: "jsx", mts: "ts", mjs: "js" }[args.path.split(".").pop()] || "ts";
|
|
383
|
+
return out === src ? undefined : { contents: out, loader };
|
|
384
|
+
});
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const scanOutdir = mkdtempSync(join(tmpdir(), "dd-scan-"));
|
|
389
|
+
|
|
390
|
+
// Pass 1: bundle without compile to discover Worker entrypoints. Worker
|
|
391
|
+
// files enter the module graph only once listed as entrypoints, so loop to
|
|
392
|
+
// a fixpoint — this also discovers workers spawned by workers and dynamic
|
|
393
|
+
// imports reachable only inside worker bundles.
|
|
394
|
+
let prevCount = -1;
|
|
395
|
+
while (workerEntries.size !== prevCount) {
|
|
396
|
+
prevCount = workerEntries.size;
|
|
397
|
+
const scan = await Bun.build({
|
|
398
|
+
entrypoints: [resolve(entry), ...workerEntries],
|
|
399
|
+
target: "bun",
|
|
400
|
+
plugins: [makePlugin(true)],
|
|
401
|
+
splitting: false,
|
|
402
|
+
sourcemap: "none",
|
|
403
|
+
minify: false,
|
|
404
|
+
define: retainMcp ? {} : { __DD_MCP_STRIP__: "true" },
|
|
405
|
+
outdir: scanOutdir,
|
|
406
|
+
});
|
|
407
|
+
if (!scan.success) {
|
|
408
|
+
scan.logs.forEach((msg) => { console.error(msg);; });
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const entrypoints = [resolve(entry), ...workerEntries];
|
|
414
|
+
if (workerEntries.size) {
|
|
415
|
+
console.log(`workers: ${[...workerEntries].map((p) => p.split("/").pop()).join(", ")}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const result = await Bun.build({
|
|
419
|
+
entrypoints,
|
|
420
|
+
compile: { outfile: resolve(outfile), ...(target.bun ? { target: target.bun } : {}) },
|
|
421
|
+
target: "bun",
|
|
422
|
+
plugins: [makePlugin(false)],
|
|
423
|
+
splitting: false,
|
|
424
|
+
sourcemap: "none",
|
|
425
|
+
minify: modeArg === "prod",
|
|
426
|
+
// Strip the MCP endpoint from distributed binaries unless --mcp was passed.
|
|
427
|
+
define: retainMcp ? {} : { __DD_MCP_STRIP__: "true" },
|
|
428
|
+
// koffi is the Node-only FFI fallback (dead under Bun); its native addon
|
|
429
|
+
// can't bundle, and the import is guarded by runtime detection anyway.
|
|
430
|
+
external: ["koffi"],
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
if (!result.success) {
|
|
434
|
+
result.logs.forEach((msg) => { console.error(msg);; });
|
|
435
|
+
process.exit(1);
|
|
436
|
+
}
|
|
437
|
+
result.outputs.forEach((out) => {
|
|
438
|
+
console.log(`${out.kind.padEnd(18)} ${out.path} ${(out.size / 1024 / 1024).toFixed(1)}MB`);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// ── Windows PE branding ──
|
|
442
|
+
// Stamp version info + icon into the compiled .exe so it doesn't identify
|
|
443
|
+
// itself as "Bun runtime". resedit is pure JS — no wine/rcedit needed.
|
|
444
|
+
// Metadata precedence: --product-name/--product-version/--icon flags, then
|
|
445
|
+
// the game's package.json (productName / build.productName, version,
|
|
446
|
+
// build.icon or icon.ico|icon.png in the game dir), then the game name.
|
|
447
|
+
const gameDir = dirname(dirname(resolve(entry))); // src/native-entry.ts → game root
|
|
448
|
+
if (target.exe) {
|
|
449
|
+
let pkg = {};
|
|
450
|
+
try { pkg = JSON.parse(readFileSync(join(gameDir, "package.json"), "utf-8")); } catch { /* standalone entry */ }
|
|
451
|
+
const titleize = (s) => String(s).replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
452
|
+
const productName = productNameFlag ?? pkg.productName ?? pkg.build?.productName
|
|
453
|
+
?? titleize(pkg.name ?? basename(outfile, ".exe"));
|
|
454
|
+
const version = productVersionFlag ?? pkg.version ?? "0.0.0";
|
|
455
|
+
const author = typeof pkg.author === "string" ? pkg.author : pkg.author?.name ?? "DownDraft Engine";
|
|
456
|
+
let iconPath = iconFlag
|
|
457
|
+
?? (pkg.build?.icon ? resolve(gameDir, pkg.build.icon) : undefined);
|
|
458
|
+
if (!iconPath) {
|
|
459
|
+
iconPath = ["icon.ico", "assets/icon.ico", "icon.png", "assets/icon.png"]
|
|
460
|
+
.map((p) => join(gameDir, p)).find((p) => existsSync(p));
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
const ResEdit = await import("resedit");
|
|
464
|
+
const exe = ResEdit.NtExecutable.from(readFileSync(resolve(outfile)));
|
|
465
|
+
// Bun appends a `.bun` trailer section after `.reloc`, which
|
|
466
|
+
// NtExecutableResource.from() rejects outright — it only tolerates
|
|
467
|
+
// .reloc after .rsrc. Hide .bun from the order check; resources are
|
|
468
|
+
// regenerated in place below (noGrow), so no section ever moves.
|
|
469
|
+
const allSections = exe.getAllSections.bind(exe);
|
|
470
|
+
let rsrcVa = -1;
|
|
471
|
+
let reloc;
|
|
472
|
+
for (const s of allSections()) {
|
|
473
|
+
if (s.info.name === ".rsrc") rsrcVa = s.info.virtualAddress;
|
|
474
|
+
if (s.info.name === ".reloc") reloc = s;
|
|
475
|
+
}
|
|
476
|
+
exe.getAllSections = () =>
|
|
477
|
+
allSections().filter((s) => s === reloc || s.info.virtualAddress <= rsrcVa || rsrcVa < 0);
|
|
478
|
+
const res = ResEdit.NtExecutableResource.from(exe);
|
|
479
|
+
exe.getAllSections = allSections;
|
|
480
|
+
const lang = { lang: 1033, codepage: 1200 };
|
|
481
|
+
const vi = ResEdit.Resource.VersionInfo.createEmpty();
|
|
482
|
+
// PE versions are numeric quads — strip any semver suffix/prefix.
|
|
483
|
+
const verNum = (version.match(/\d+(?:\.\d+)*/) ?? ["0.0.0"])[0];
|
|
484
|
+
vi.setFileVersion(verNum, lang.lang);
|
|
485
|
+
vi.setProductVersion(verNum, lang.lang);
|
|
486
|
+
vi.setStringValues(lang, {
|
|
487
|
+
CompanyName: author,
|
|
488
|
+
FileDescription: productName,
|
|
489
|
+
FileVersion: version,
|
|
490
|
+
InternalName: basename(outfile, ".exe"),
|
|
491
|
+
OriginalFilename: basename(outfile),
|
|
492
|
+
ProductName: productName,
|
|
493
|
+
ProductVersion: version,
|
|
494
|
+
}, /* addToAvailableLanguage */ true);
|
|
495
|
+
// Replace Bun's stock version resource entirely — Windows reads the
|
|
496
|
+
// first RT_VERSION it finds, so a leftover one can shadow ours.
|
|
497
|
+
res.entries = res.entries.filter((e) => e.type !== 16);
|
|
498
|
+
vi.outputToResourceEntries(res.entries);
|
|
499
|
+
for (const e of res.entries.filter((e) => e.type === 16)) e.lang = lang.lang;
|
|
500
|
+
|
|
501
|
+
if (iconPath && existsSync(iconPath)) {
|
|
502
|
+
const iconBin = readFileSync(iconPath);
|
|
503
|
+
let items;
|
|
504
|
+
if (iconPath.endsWith(".ico")) {
|
|
505
|
+
items = ResEdit.Data.IconFile.from(iconBin).icons;
|
|
506
|
+
} else if (iconPath.endsWith(".png")) {
|
|
507
|
+
// PNG-compressed icons are valid in PE resources (Vista+). Width and
|
|
508
|
+
// height come from the IHDR chunk (bytes 16–24).
|
|
509
|
+
const w = iconBin.readUInt32BE(16), h = iconBin.readUInt32BE(20);
|
|
510
|
+
items = [new ResEdit.Data.RawIconItem(iconBin, w, h, 32)];
|
|
511
|
+
}
|
|
512
|
+
if (items?.length) {
|
|
513
|
+
// Reuse an existing icon-group id when present (Bun embeds one);
|
|
514
|
+
// otherwise allocate 101 — Windows picks the lowest group anyway.
|
|
515
|
+
const existing = res.entries.filter((e) => e.type === 14); // RT_GROUP_ICON
|
|
516
|
+
const groupId = existing.length ? existing[0].id : 101;
|
|
517
|
+
ResEdit.Resource.IconGroupEntry.replaceIconsForResource(
|
|
518
|
+
res.entries, groupId, lang.lang, items);
|
|
519
|
+
}
|
|
520
|
+
} else {
|
|
521
|
+
// No game icon — drop Bun's ~270KB stock icon (RT_ICON + its
|
|
522
|
+
// RT_GROUP_ICON) so the version strings fit inside the existing .rsrc
|
|
523
|
+
// under noGrow below. The exe falls back to the generic Windows icon,
|
|
524
|
+
// which is better branding than shipping the Bun logo.
|
|
525
|
+
res.entries = res.entries.filter((e) => e.type !== 3 && e.type !== 14);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// noGrow: fit inside the existing .rsrc — the new data must not push
|
|
529
|
+
// .reloc/.bun because pe-library can't relocate arbitrary trailers.
|
|
530
|
+
// Bun's stock .rsrc is ~270KB, ample for version strings + an icon.
|
|
531
|
+
res.outputResource(exe, /* noGrow */ true);
|
|
532
|
+
writeFileSync(resolve(outfile), Buffer.from(exe.generate()));
|
|
533
|
+
console.log(`branding: ${productName} v${version}${iconPath ? ` + icon ${basename(iconPath)}` : ""}`);
|
|
534
|
+
} catch (e) {
|
|
535
|
+
// Cosmetic only — never fail the package over resource stamping.
|
|
536
|
+
console.warn(`warning: PE branding skipped (${e?.message ?? e})`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ── Stage runtime tree ──
|
|
541
|
+
const outdir = dirname(resolve(outfile));
|
|
542
|
+
const platKey = target.plat;
|
|
543
|
+
const libExt = platKey.startsWith("win32") ? ".dll" : platKey.startsWith("darwin") ? ".dylib" : ".so";
|
|
544
|
+
|
|
545
|
+
const nativeDir = join(outdir, "native");
|
|
546
|
+
mkdirSync(nativeDir, { recursive: true });
|
|
547
|
+
|
|
548
|
+
// Package resolution rooted at the game dir — works both inside the
|
|
549
|
+
// monorepo (workspace links) and in a standalone game repo (node_modules).
|
|
550
|
+
const gameRequire = createRequire(resolve(entry));
|
|
551
|
+
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
552
|
+
const monorepoRoot = resolve(scriptDir, "../../..");
|
|
553
|
+
|
|
554
|
+
function pkgRoot(name) {
|
|
555
|
+
try { return dirname(gameRequire.resolve(`${name}/package.json`)); }
|
|
556
|
+
catch { return null; }
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Platform lib — the unified libdowndraft_platform (Rust, statically links
|
|
560
|
+
// wgpu — no external deps) stages under native/<platform>-<arch>/. Probe
|
|
561
|
+
// order: monorepo dev staging, the installed platform-native package, the
|
|
562
|
+
// per-target @downdraft/native-<plat>-<arch> package.
|
|
563
|
+
const platLibCandidates = [...new Set([
|
|
564
|
+
join(monorepoRoot, "packages/platform-native/native", platKey),
|
|
565
|
+
pkgRoot("@downdraft/platform-native") && join(pkgRoot("@downdraft/platform-native"), "native", platKey),
|
|
566
|
+
pkgRoot(`@downdraft/native-${platKey}`) && join(pkgRoot(`@downdraft/native-${platKey}`), "lib"),
|
|
567
|
+
].filter(Boolean))];
|
|
568
|
+
const platStageDir = platLibCandidates.find((d) => existsSync(d));
|
|
569
|
+
if (platStageDir) {
|
|
570
|
+
for (const f of readdirSync(platStageDir)) {
|
|
571
|
+
if (f.endsWith(libExt)) copyFileSync(join(platStageDir, f), join(nativeDir, f));
|
|
572
|
+
}
|
|
573
|
+
} else {
|
|
574
|
+
console.error(`no libdowndraft_platform build found for ${platKey} — searched: ${platLibCandidates.join(", ")}`);
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Engine cdylibs — staged if a build exists: new dist/<plat>-<arch>/ and
|
|
579
|
+
// dist/ layouts (build-native.mjs), the installed @downdraft/engine package
|
|
580
|
+
// (standalone games), plus the legacy standalone-crate target/ + crate-dir
|
|
581
|
+
// layouts.
|
|
582
|
+
const engineCrates = CRATES.filter((c) => c.pkg !== "downdraft-platform");
|
|
583
|
+
const engineRoot = pkgRoot("@downdraft/engine");
|
|
584
|
+
const staged = new Set();
|
|
585
|
+
engineCrates.forEach((crate) => {
|
|
586
|
+
const file = libFileName(crate.lib, platKey);
|
|
587
|
+
// crate.dir is packages/engine/libraries/<name>/native — strip the
|
|
588
|
+
// package prefix to probe inside an installed @downdraft/engine.
|
|
589
|
+
const pkgRel = crate.dir.replace(/^packages\/engine\//, "");
|
|
590
|
+
const candidates = [
|
|
591
|
+
join(repoRoot, crate.dest, platKey, file),
|
|
592
|
+
join(repoRoot, crate.dest, file),
|
|
593
|
+
join(repoRoot, crate.dir, "target", "release", file),
|
|
594
|
+
join(repoRoot, crate.dir, file),
|
|
595
|
+
...(engineRoot ? [
|
|
596
|
+
join(engineRoot, pkgRel, "dist", platKey, file),
|
|
597
|
+
join(engineRoot, pkgRel, "dist", file),
|
|
598
|
+
] : []),
|
|
599
|
+
];
|
|
600
|
+
for (let _i = 0, _it = candidates, _n = _it.length; _i < _n; _i++) {
|
|
601
|
+
const p = _it[_i];
|
|
602
|
+
if (existsSync(p) && !staged.has(file)) {
|
|
603
|
+
copyFileSync(p, join(nativeDir, file));
|
|
604
|
+
staged.add(file);
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// Game-native libs — convention: <game>/native/dist/<platform>-<arch>/ holds
|
|
611
|
+
// artifacts the game built itself (cdylib .so/.dll/.dylib loaded via the FFI
|
|
612
|
+
// adapter, or .node napi addons loaded via createRequire/process.dlopen).
|
|
613
|
+
// Staged flat into native/ so resolveNativeLibrary's <execDir>/native lookup
|
|
614
|
+
// and execDir-relative .node resolution both hit.
|
|
615
|
+
const gameNativeDir = join(gameDir, "native", "dist", platKey);
|
|
616
|
+
if (existsSync(gameNativeDir)) {
|
|
617
|
+
let gameLibs = 0;
|
|
618
|
+
for (const f of readdirSync(gameNativeDir)) {
|
|
619
|
+
if (f.endsWith(libExt) || f.endsWith(".node")) {
|
|
620
|
+
copyFileSync(join(gameNativeDir, f), join(nativeDir, f));
|
|
621
|
+
staged.add(f);
|
|
622
|
+
gameLibs++;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (gameLibs) console.log(`staged ${gameLibs} game-native lib(s) from ${gameNativeDir}`);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Assets referenced via ?url / new URL / import.meta.glob.
|
|
629
|
+
const assetRoot = join(outdir, "dd-assets");
|
|
630
|
+
let stagedAssets = 0;
|
|
631
|
+
for (const f of assetFiles.values()) {
|
|
632
|
+
const dest = join(assetRoot, assetRel(f));
|
|
633
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
634
|
+
copyFileSync(f, dest);
|
|
635
|
+
stagedAssets++;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
console.log(`staged: ${readdirSync(nativeDir).filter((f) => f.endsWith(libExt)).length} native libs, ${stagedAssets} assets`);
|