@ringozz/godot 4.7.2-664 → 4.7.2-676
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 +6 -2
- package/package.json +4 -4
- package/src/bake.ts +143 -25
- package/src/editor.ts +5 -0
- package/src/export_presets.example.cfg +3 -3
- package/src/load.ts +42 -26
- package/src/pack.ts +0 -5
- package/src/preload.ts +164 -165
- package/src/serve.ts +67 -67
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ await import('./app.ts'); // may now import @ringozz/godot / react-godot freely
|
|
|
54
54
|
| `@ringozz/godot/boot` | Platform boot: `getNativeModule()`; web `preloadGodot()` (awaited before importing the app; no-op on desktop) |
|
|
55
55
|
| `@ringozz/godot/load` | Async resource loading: `loadResourceAsync`, `loadAsset`, `loadAssetPack` (native `.pck` packs), `configurePacks`/`loadPackEntry` (content packs) |
|
|
56
56
|
| `@ringozz/godot/bake` | `buildAssetPacks(opts, run?)` + the `godot pack` subcommand: bake the project's boot `.pck` plus one per declared content pack, with their loose-texture documents. `opts.cwd` picks the project root and `run` replaces the editor, so a test can bake a temp project |
|
|
57
|
-
| `@ringozz/godot/pack` | The pack protocol the bake writes and the plugin/runtime read: `PACK_DOC`/`RELEASE`/`FILES_DIR` and the boot's names, `parseDoc`/`serializeDoc`/`parseRelease`, `packEntries`/`godotEntry`/`entryId`/`
|
|
57
|
+
| `@ringozz/godot/pack` | The pack protocol the bake writes and the plugin/runtime read: `PACK_DOC`/`RELEASE`/`FILES_DIR` and the boot's names, `parseDoc`/`serializeDoc`/`parseRelease`, `packEntries`/`godotEntry`/`entryId`/`pckOf`, and `resolveEntry` (what to mount and stage for one entry) |
|
|
58
58
|
| `@ringozz/godot/debug` | `initDebug()`, `dumpStr`, `snapshot`, `inspect`, `click`, `find`, `gc` |
|
|
59
59
|
|
|
60
60
|
Class-scoped enums use bare names (import `ProcessMode` from `@ringozz/godot/Node`, not `NodeProcessMode`).
|
|
@@ -111,7 +111,11 @@ A content pack is deployed apart from the app, so nothing about it is in the bun
|
|
|
111
111
|
|
|
112
112
|
```ts
|
|
113
113
|
import { configurePacks } from '@ringozz/godot/load';
|
|
114
|
-
|
|
114
|
+
// The host owns the base: a pack's name, its version and one of its files, addressed its own way.
|
|
115
|
+
configurePacks({
|
|
116
|
+
at: (name, version, file) => `https://cdn.example/packs/${name}@${version}/${file}`,
|
|
117
|
+
release: 'https://cdn.example/packs/release.json', // or the map itself: `{ '@tiv/beach': { version, deps } }`
|
|
118
|
+
});
|
|
115
119
|
```
|
|
116
120
|
|
|
117
121
|
The boot pack — the app's own sources, which ship beside it — needs no config: the bake renders it into a module (`.godot/packs/boot-pack.js`: its `.pck`, plus a bundled URL per loose product it owns), which every imported asset pulls in, and `configurePacks` merges, so the app's `at` and the boot's config may land in either order. A pack declares its entries (`exports` → the `godot` condition, the `res://` path) and ships **no entry module**: the preload plugin serves one, built from the bake's documents, with one named export per entry. So importing a pack is an ordinary import — and the bake emits the types it resolves to (`package.d.ts`, named by the pack's `types`):
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ringozz/godot",
|
|
3
3
|
"author": "Vladimir Davidovich",
|
|
4
|
-
"version": "4.7.2-
|
|
4
|
+
"version": "4.7.2-676",
|
|
5
5
|
"description": "Node-API bindings for Godot Engine — call Godot classes from JavaScript",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -67,9 +67,9 @@
|
|
|
67
67
|
"precision": "single"
|
|
68
68
|
},
|
|
69
69
|
"optionalDependencies": {
|
|
70
|
-
"@ringozz/godot-macos-arm64": "^4.7.2-
|
|
71
|
-
"@ringozz/godot-windows-x86_64": "^4.7.2-
|
|
72
|
-
"@ringozz/godot-web-wasm32": "^4.7.2-
|
|
70
|
+
"@ringozz/godot-macos-arm64": "^4.7.2-676",
|
|
71
|
+
"@ringozz/godot-windows-x86_64": "^4.7.2-676",
|
|
72
|
+
"@ringozz/godot-web-wasm32": "^4.7.2-676"
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
75
|
"@types/bun": "*"
|
package/src/bake.ts
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
***********************************************************************/
|
|
4
4
|
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { createHash, type Hash } from 'node:crypto';
|
|
6
7
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
7
|
-
import { dirname, join, resolve } from 'node:path';
|
|
8
|
+
import { dirname, extname, join, resolve } from 'node:path';
|
|
8
9
|
import { fileURLToPath } from 'node:url';
|
|
9
10
|
import { resolveEditor } from './editor.ts';
|
|
10
11
|
import { packedExtensions, scannedExtensions } from './formats.ts';
|
|
@@ -50,6 +51,27 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
50
51
|
/** The dump's one build-time output, rewritten by every bake. */
|
|
51
52
|
const DUMP = '.godot/pack-dump.json';
|
|
52
53
|
|
|
54
|
+
/** The digest of every bake input, beside the packs: written by a bake that finished, read by the next. */
|
|
55
|
+
const STAMP = '.stamp.json';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Removes everything a bake derives — the import cache, the documents, the packs — and never a source.
|
|
59
|
+
* `godot clean` is the answer to the two states a bake cannot repair by itself: orphaned products left
|
|
60
|
+
* by sources that moved or were renamed, and a derived file whose import was never forced.
|
|
61
|
+
*/
|
|
62
|
+
export function cleanProject(cwd = process.cwd()): void {
|
|
63
|
+
rmSync(resolve(cwd, '.godot'), { recursive: true, force: true });
|
|
64
|
+
console.log('[pack] cleaned .godot');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Source the engine never imports or packs — the app's own code, sitting in the same tree as its
|
|
69
|
+
* assets. Editing it cannot change a pack, and it is the most common edit there is, so the digest
|
|
70
|
+
* skips it. The list is a deny-list on purpose: an unknown extension re-bakes (harmless) rather than
|
|
71
|
+
* going stale (a pack that silently lags its sources).
|
|
72
|
+
*/
|
|
73
|
+
const SOURCE = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.md', '.yaml', '.yml', '.css']);
|
|
74
|
+
|
|
53
75
|
export interface BuildAssetPackOptions {
|
|
54
76
|
/** The project root the bake reads and writes (default: `process.cwd()`). */
|
|
55
77
|
cwd?: string;
|
|
@@ -63,6 +85,8 @@ export interface BuildAssetPackOptions {
|
|
|
63
85
|
preset?: string;
|
|
64
86
|
/** Editor executable override (default: `resolveEditor()`). */
|
|
65
87
|
godot?: string;
|
|
88
|
+
/** Bake even when nothing the bake reads has changed since the last one (default: false). */
|
|
89
|
+
force?: boolean;
|
|
66
90
|
}
|
|
67
91
|
|
|
68
92
|
/** A content pack: a directory that declares itself with a `package.json`. */
|
|
@@ -96,19 +120,37 @@ export async function buildAssetPacks(opts: BuildAssetPackOptions = {}, run?: Ru
|
|
|
96
120
|
const bootRoots = opts.roots ?? ['res://src'];
|
|
97
121
|
const out = opts.out ?? PACK_DIR;
|
|
98
122
|
const preset = opts.preset ?? 'Pack';
|
|
99
|
-
const
|
|
123
|
+
const editor = opts.godot ?? (run ? null : await resolveEditor());
|
|
124
|
+
const exec = run ?? runner(editor!, cwd);
|
|
100
125
|
mkdirSync(resolve(cwd, out), { recursive: true });
|
|
101
126
|
|
|
127
|
+
const dlc = rel(opts.dlc ?? 'res://dlc');
|
|
128
|
+
const packs = discoverPacks(cwd, dlc);
|
|
129
|
+
|
|
130
|
+
const presets = resolve(cwd, 'export_presets.cfg');
|
|
131
|
+
const cfg = readFileSync(presets, 'utf8');
|
|
132
|
+
|
|
133
|
+
// Nothing the bake reads has changed since the last one that finished: an import scan, a dump and
|
|
134
|
+
// one export per pack are the whole cost of a dev server start, and none of it is worth repeating
|
|
135
|
+
// for a project nobody touched. The stamp is the digest of every input, not a timestamp of the
|
|
136
|
+
// outputs — a pack's `.pck` is rewritten on every export, so it can't answer this.
|
|
137
|
+
const stamp = resolve(cwd, out, STAMP);
|
|
138
|
+
const inputs = inputsOf({ cwd, dirs: [...bootRoots.map(rel), ...packs.map((pack) => pack.dir)], cfg, editor });
|
|
139
|
+
if (!opts.force && readStamp(stamp) === inputs) {
|
|
140
|
+
console.log(`[pack] up to date (${out})`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
102
144
|
// The editor's import scan must not walk the installed dependency tree: a workspace-linked pack
|
|
103
145
|
// would be imported a second time through the link, at a depth its relative paths (a terrain
|
|
104
146
|
// `.webparray`'s layer refs) do not survive. `.gdignore` is the editor's own skip marker; the
|
|
105
147
|
// bundler never looks at it, and the sources are imported once at their real `res://` paths.
|
|
106
|
-
|
|
107
|
-
|
|
148
|
+
// A pack that declares another pack gets that link *inside itself* (`node_modules` beside its
|
|
149
|
+
// `package.json`), so each pack's tree needs the marker as well as the root one.
|
|
150
|
+
const installed = [resolve(cwd, 'node_modules'), ...packs.map((pack) => resolve(cwd, pack.dir, 'node_modules'))];
|
|
151
|
+
for (const dir of installed.filter((dir) => existsSync(dir))) writeFileSync(join(dir, '.gdignore'), '');
|
|
108
152
|
exec(['--headless', '--path', '.', '--import']);
|
|
109
153
|
|
|
110
|
-
const dlc = rel(opts.dlc ?? 'res://dlc');
|
|
111
|
-
const packs = discoverPacks(cwd, dlc);
|
|
112
154
|
assertScopes(bootRoots.map(rel), packs);
|
|
113
155
|
const dirOf = new Map(packs.map((pack) => [pack.name, pack.dir]));
|
|
114
156
|
const release = releaseOf(packs);
|
|
@@ -117,8 +159,6 @@ export async function buildAssetPacks(opts: BuildAssetPackOptions = {}, run?: Ru
|
|
|
117
159
|
const bootDirs = bootRoots.map(rel);
|
|
118
160
|
const all = [...bootDirs, ...packs.map((pack) => pack.dir)];
|
|
119
161
|
|
|
120
|
-
const presets = resolve(cwd, 'export_presets.cfg');
|
|
121
|
-
const cfg = readFileSync(presets, 'utf8');
|
|
122
162
|
// A plan's routes are relative to the converter's output root, which is where the packs live.
|
|
123
163
|
const ctx: Bake = { run: exec, cwd, cfg, preset, base: excludeFilter(cfg), all, planBase: resPath(dlc) };
|
|
124
164
|
try {
|
|
@@ -126,18 +166,22 @@ export async function buildAssetPacks(opts: BuildAssetPackOptions = {}, run?: Ru
|
|
|
126
166
|
// that *owns* it, whoever needs it). One dump is also one editor run instead of one per pack.
|
|
127
167
|
const dump = runDump(ctx, [...bootRoots, ...packs.map((pack) => resPath(pack.dir))]);
|
|
128
168
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
169
|
+
if (bootDirs.length) {
|
|
170
|
+
const boot = manifestOf(dump, cwd, out, bootDirs);
|
|
171
|
+
assertRefs('the boot pack', boot.external, bootRoots);
|
|
172
|
+
exportPack(ctx, bootDirs, `${out}/${BOOT_PCK}`);
|
|
173
|
+
writeFileSync(resolve(cwd, out, BOOT_MANIFEST), serializeDoc({ needs: boot.needs, stage: boot.stage }));
|
|
174
|
+
writeFileSync(resolve(cwd, out, BOOT_MODULE), bootModule(boot));
|
|
175
|
+
console.log(`[pack] ${BOOT_PCK} over ${Object.keys(boot.needs).length} sources`);
|
|
176
|
+
}
|
|
135
177
|
|
|
136
178
|
for (const pack of packs) {
|
|
137
179
|
const version = release[pack.name].version;
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
180
|
+
// One directory per pack, holding exactly what its npm package holds: the `.pck`, its
|
|
181
|
+
// document, the loose products, the types and a manifest. So a bake's output and an installed
|
|
182
|
+
// pack have the same layout, and a host serves either with one static root. The version is
|
|
183
|
+
// the release's (`release.json`), not a directory's — a bake builds one per pack.
|
|
184
|
+
const dir = `${out}/${pack.name}`;
|
|
141
185
|
mkdirSync(resolve(cwd, dir), { recursive: true });
|
|
142
186
|
const own = manifestOf(dump, cwd, dir, [pack.dir]);
|
|
143
187
|
const entries = packEntries(pack.exports);
|
|
@@ -158,20 +202,43 @@ export async function buildAssetPacks(opts: BuildAssetPackOptions = {}, run?: Ru
|
|
|
158
202
|
stage: own.stage,
|
|
159
203
|
}),
|
|
160
204
|
);
|
|
161
|
-
// The pack's types,
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
writeFileSync(
|
|
205
|
+
// The pack's types, beside its own `package.json` (whose `types` names them) and in its
|
|
206
|
+
// output directory: a client installs the pack by name, so the types have to travel with it.
|
|
207
|
+
const types = entries.length ? packTypes(pack.exports) : null;
|
|
208
|
+
if (types) {
|
|
209
|
+
writeFileSync(resolve(cwd, pack.dir, 'package.d.ts'), types);
|
|
210
|
+
writeFileSync(resolve(cwd, dir, 'package.d.ts'), types);
|
|
167
211
|
} else {
|
|
168
|
-
rmSync(
|
|
212
|
+
rmSync(resolve(cwd, pack.dir, 'package.d.ts'), { force: true });
|
|
169
213
|
}
|
|
214
|
+
// The output directory *is* the deployable artifact: `npm publish` of it ships the `.pck`,
|
|
215
|
+
// its document, the loose products and the types, and its declared dependencies — so a
|
|
216
|
+
// level pulls its shared pack in on install, and nothing is assembled later.
|
|
217
|
+
writeFileSync(
|
|
218
|
+
resolve(cwd, dir, 'package.json'),
|
|
219
|
+
`${JSON.stringify(
|
|
220
|
+
{
|
|
221
|
+
name: pack.name,
|
|
222
|
+
version,
|
|
223
|
+
dependencies: Object.keys(pack.dependencies).length ? pack.dependencies : undefined,
|
|
224
|
+
types: types ? './package.d.ts' : undefined,
|
|
225
|
+
files: [PACK_DOC, pckOf(pack.name), `${FILES_DIR}/**`, ...(types ? ['package.d.ts'] : [])],
|
|
226
|
+
},
|
|
227
|
+
null,
|
|
228
|
+
'\t',
|
|
229
|
+
)}\n`,
|
|
230
|
+
);
|
|
170
231
|
console.log(`[pack] ${pack.name}@${version} over ${Object.keys(own.needs).length} sources`);
|
|
171
232
|
}
|
|
172
233
|
if (packs.length) {
|
|
173
234
|
writeFileSync(resolve(cwd, out, RELEASE), serializeDoc(release));
|
|
235
|
+
} else {
|
|
236
|
+
// Nothing to release: a release document left by a previous bake would name packs this
|
|
237
|
+
// project no longer builds.
|
|
238
|
+
rmSync(resolve(cwd, out, RELEASE), { force: true });
|
|
174
239
|
}
|
|
240
|
+
// Only a bake that got this far can say the project is up to date next time.
|
|
241
|
+
writeFileSync(stamp, `${inputs}\n`);
|
|
175
242
|
} finally {
|
|
176
243
|
// The preset is scoped per pack, so it is rewritten per export and put back once.
|
|
177
244
|
writeFileSync(presets, cfg);
|
|
@@ -453,6 +520,56 @@ function withExcludes(cfg: string, preset: string, exclude: string[]): string {
|
|
|
453
520
|
return `${cfg.slice(0, start)}exclude_filter=${JSON.stringify(exclude.join(','))}${cfg.slice(start + line[0].length)}`;
|
|
454
521
|
}
|
|
455
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Everything a bake reads, as one digest: each resource tree's files (`path/size/mtime` — content is the
|
|
525
|
+
* editor's to read, and a checkout that only moves timestamps re-bakes, which is the safe direction),
|
|
526
|
+
* the preset, the dump script, the tool, and the editor it runs.
|
|
527
|
+
*/
|
|
528
|
+
function inputsOf({ cwd, dirs, cfg, editor }: {
|
|
529
|
+
cwd: string;
|
|
530
|
+
dirs: string[];
|
|
531
|
+
cfg: string;
|
|
532
|
+
editor: string | null;
|
|
533
|
+
}): string {
|
|
534
|
+
const hash = createHash('sha1')
|
|
535
|
+
.update(cfg)
|
|
536
|
+
.update(readFileSync(join(HERE, 'dump-deps.gd')))
|
|
537
|
+
.update(readFileSync(join(HERE, '..', 'package.json')));
|
|
538
|
+
// The executable itself, not its `--version`: asking would spawn it, which is the cost the stamp
|
|
539
|
+
// exists to avoid.
|
|
540
|
+
hash.update(editor ? `${statSync(editor).size}:${statSync(editor).mtimeMs}` : 'injected');
|
|
541
|
+
for (const dir of dirs) digestTree(resolve(cwd, dir), '', hash);
|
|
542
|
+
return hash.digest('hex');
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** Feeds a tree's files into `hash`: the bake's own outputs and the app's own sources are not inputs. */
|
|
546
|
+
function digestTree(abs: string, rel: string, hash: Hash): void {
|
|
547
|
+
let names: string[];
|
|
548
|
+
try {
|
|
549
|
+
names = readdirSync(abs);
|
|
550
|
+
} catch {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
for (const name of names.sort()) {
|
|
554
|
+
// The bake's own output beside a pack's manifest, the app's own sources, and dependencies are
|
|
555
|
+
// not inputs: editing them cannot change a pack.
|
|
556
|
+
if (name === 'node_modules' || name.startsWith('.') || name === 'package.d.ts' || SOURCE.has(extname(name))) continue;
|
|
557
|
+
const child = join(abs, name);
|
|
558
|
+
const stat = statSync(child);
|
|
559
|
+
if (stat.isDirectory()) digestTree(child, `${rel}/${name}`, hash);
|
|
560
|
+
else hash.update(`${rel}/${name}:${stat.size}:${stat.mtimeMs}\n`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** The stamp a bake left, or `null` — a stamp this can't read is a project that gets baked. */
|
|
565
|
+
function readStamp(file: string): string | null {
|
|
566
|
+
try {
|
|
567
|
+
return readFileSync(file, 'utf8').trim();
|
|
568
|
+
} catch {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
456
573
|
/** Parses `godot pack` CLI args into {@link BuildAssetPackOptions}. */
|
|
457
574
|
export function parsePackArgs(args: string[]): BuildAssetPackOptions {
|
|
458
575
|
const opt = (flag: string): string | undefined => {
|
|
@@ -460,10 +577,11 @@ export function parsePackArgs(args: string[]): BuildAssetPackOptions {
|
|
|
460
577
|
return i >= 0 ? args[i + 1] : undefined;
|
|
461
578
|
};
|
|
462
579
|
return {
|
|
463
|
-
roots: opt('--roots')?.split(',').filter(Boolean),
|
|
580
|
+
roots: args.includes('--no-boot') ? [] : opt('--roots')?.split(',').filter(Boolean),
|
|
464
581
|
dlc: opt('--dlc'),
|
|
465
582
|
out: opt('--out'),
|
|
466
583
|
preset: opt('--preset'),
|
|
467
584
|
godot: opt('--godot'),
|
|
585
|
+
force: args.includes('--force'),
|
|
468
586
|
};
|
|
469
587
|
}
|
package/src/editor.ts
CHANGED
|
@@ -111,6 +111,11 @@ if (import.meta.main) {
|
|
|
111
111
|
await buildAssetPacks(parsePackArgs(process.argv.slice(3)));
|
|
112
112
|
process.exit(0);
|
|
113
113
|
}
|
|
114
|
+
if (command === 'clean') {
|
|
115
|
+
const { cleanProject } = await import('./bake.ts');
|
|
116
|
+
cleanProject();
|
|
117
|
+
process.exit(0);
|
|
118
|
+
}
|
|
114
119
|
const exe = await resolveEditor();
|
|
115
120
|
const result = spawnSync(exe, process.argv.slice(2), { stdio: 'inherit', env: { ...process.env, GODOT_NODE_WRAPPER: '1' } });
|
|
116
121
|
process.exit(result.status ?? 1);
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
; (see `dump-deps.gd`), not read from the pack. `export_filter="all_resources"`
|
|
8
8
|
; ignores `include_filter` — scope with `exclude_filter` only.
|
|
9
9
|
;
|
|
10
|
-
; The bake
|
|
11
|
-
;
|
|
12
|
-
;
|
|
10
|
+
; The bake rewrites this preset's `exclude_filter` once per pack (the boot pack, then one per
|
|
11
|
+
; content pack) and puts the file back when it finishes, so the export carries exactly one scope.
|
|
12
|
+
; Don't write those scopes by hand.
|
|
13
13
|
|
|
14
14
|
[preset.0]
|
|
15
15
|
|
package/src/load.ts
CHANGED
|
@@ -13,9 +13,7 @@ import { transcoder } from '@ringozz/godot/transcoder';
|
|
|
13
13
|
import {
|
|
14
14
|
FILES_DIR,
|
|
15
15
|
PACK_DOC,
|
|
16
|
-
RELEASE,
|
|
17
16
|
mountClosure,
|
|
18
|
-
packPath,
|
|
19
17
|
parseDoc,
|
|
20
18
|
parseRelease,
|
|
21
19
|
pckOf,
|
|
@@ -368,12 +366,23 @@ export interface MountedPack {
|
|
|
368
366
|
url: (file: string) => string;
|
|
369
367
|
}
|
|
370
368
|
|
|
371
|
-
/**
|
|
369
|
+
/**
|
|
370
|
+
* How the runtime reads content packs: a URL per file, and the release that names the packs (or where
|
|
371
|
+
* to read one). The host owns its base and the way a version is encoded in it, so the protocol never
|
|
372
|
+
* builds a release-relative path — it knows a pack's name and its files (`pack.json`, `<name>.pck`,
|
|
373
|
+
* `files/<product>`), and asks.
|
|
374
|
+
*/
|
|
372
375
|
export interface PackSource {
|
|
373
|
-
/**
|
|
374
|
-
at?: (
|
|
376
|
+
/** A pack's file, as a fetchable URL or a local path — how the host addresses a version is its own. */
|
|
377
|
+
at?: (pack: string, version: string, file: string) => string;
|
|
375
378
|
/** The app's own pack, configured by the bake's own code: always mounted, its bytes bundled. */
|
|
376
379
|
boot?: MountedPack;
|
|
380
|
+
/**
|
|
381
|
+
* The packs to mount. The app's own dependencies (pinned by its lockfile) are one way: there is no
|
|
382
|
+
* release document to fetch, and the versions the app was built and tested with are the ones it
|
|
383
|
+
* mounts. A URL is the other: mount what a release document names — a bake's `release.json`, a CDN's.
|
|
384
|
+
*/
|
|
385
|
+
release?: Release | string;
|
|
377
386
|
}
|
|
378
387
|
|
|
379
388
|
let packSource: PackSource = {};
|
|
@@ -386,11 +395,8 @@ export function configurePacks(source: PackSource): void {
|
|
|
386
395
|
packSource = { ...packSource, ...source };
|
|
387
396
|
}
|
|
388
397
|
|
|
389
|
-
/** Reads a
|
|
390
|
-
async function
|
|
391
|
-
const at = packSource.at;
|
|
392
|
-
if (!at) throw new Error('godot-assets: no pack source — call configurePacks({ at }) first.');
|
|
393
|
-
const url = at(path);
|
|
398
|
+
/** Reads a document: a URL over the network, anything else off the filesystem. */
|
|
399
|
+
async function readText(url: string): Promise<string> {
|
|
394
400
|
if (/^https?:/i.test(url)) {
|
|
395
401
|
const res = await fetch(url);
|
|
396
402
|
if (!res.ok) throw new Error(`godot-assets: ${url} (${res.status})`);
|
|
@@ -402,24 +408,27 @@ async function fetchPack(path: string): Promise<string> {
|
|
|
402
408
|
return text;
|
|
403
409
|
}
|
|
404
410
|
|
|
411
|
+
/** The release to mount: the packs the app pinned, or the document `configurePacks` was pointed at. */
|
|
412
|
+
async function mountedRelease(): Promise<Release> {
|
|
413
|
+
const pinned = packSource.release;
|
|
414
|
+
if (!pinned) throw new Error('godot-assets: no release — configurePacks({ release }) with the packs to mount or its URL.');
|
|
415
|
+
return typeof pinned === 'string' ? parseRelease(await readText(pinned)) : pinned;
|
|
416
|
+
}
|
|
417
|
+
|
|
405
418
|
/**
|
|
406
419
|
* A resolved entry's files, as the loader stages them: the app's own pack addresses a product from its
|
|
407
|
-
* bundled URLs, a
|
|
408
|
-
*
|
|
409
|
-
* file names rather than URLs.
|
|
420
|
+
* bundled URLs, a mounted pack from the host's addressing. This is the addressing the protocol
|
|
421
|
+
* deliberately does not know — which is why a resolution returns packs and file names, not URLs.
|
|
410
422
|
*/
|
|
411
423
|
function stagedFiles(
|
|
412
424
|
plan: ResolvedEntry,
|
|
413
|
-
release: Release,
|
|
414
425
|
boot: MountedPack | undefined,
|
|
415
|
-
|
|
426
|
+
fileUrl: (pack: string, file: string) => string,
|
|
416
427
|
): StagedFiles {
|
|
417
428
|
const files: StagedFiles = {};
|
|
418
429
|
for (const { sidecar, content, product, pack, file } of plan.files) {
|
|
419
430
|
files[sidecar] = { content };
|
|
420
|
-
files[product] = {
|
|
421
|
-
path: pack === null ? boot!.url(file) : at(packPath(pack, release[pack].version, `${FILES_DIR}/${file}`)),
|
|
422
|
-
};
|
|
431
|
+
files[product] = { path: pack === null ? boot!.url(file) : fileUrl(pack, `${FILES_DIR}/${file}`) };
|
|
423
432
|
}
|
|
424
433
|
return files;
|
|
425
434
|
}
|
|
@@ -436,24 +445,31 @@ export async function loadPackEntry<C extends ResourceConstructor>(
|
|
|
436
445
|
): Promise<InstanceType<C>> {
|
|
437
446
|
const at = packSource.at;
|
|
438
447
|
if (!at) throw new Error('godot-assets: no pack source — call configurePacks({ at }) first.');
|
|
448
|
+
const release = await mountedRelease();
|
|
439
449
|
// Mounting needs only the release — a pack's `.pck` is `<name>.pck` by convention — so the closure's
|
|
440
|
-
// packs are mounted while their documents are
|
|
441
|
-
const release = parseRelease(await fetchPack(RELEASE));
|
|
450
|
+
// packs are mounted while their documents are read, and only staging waits on those.
|
|
442
451
|
const names = mountClosure(release, pack);
|
|
443
|
-
const
|
|
452
|
+
const url = (name: string, file: string) => at(name, release[name].version, file);
|
|
444
453
|
const boot = packSource.boot;
|
|
445
|
-
const mounts = [...(boot ? [boot.pck] : []), ...names.map(
|
|
454
|
+
const mounts = [...(boot ? [boot.pck] : []), ...names.map((name) => url(name, pckOf(name)))].map((pck) =>
|
|
455
|
+
loadAssetPack(pck),
|
|
456
|
+
);
|
|
446
457
|
const docs = Object.fromEntries(
|
|
447
458
|
await Promise.all(
|
|
448
459
|
names.map(async (name): Promise<[string, PackDoc]> => {
|
|
449
|
-
const
|
|
450
|
-
|
|
460
|
+
const doc = parseDoc(await readText(url(name, PACK_DOC)));
|
|
461
|
+
// A base that serves a version it wasn't asked for is the one thing a client cannot see:
|
|
462
|
+
// production's URLs are immutable, and a dev server pointed at a local bake serves content
|
|
463
|
+
// at another version on purpose. Say so; the load still works.
|
|
464
|
+
if (doc.version !== release[name].version) {
|
|
465
|
+
console.warn(`godot-assets: ${name} ${release[name].version} asked, ${doc.version ?? 'no version'} served`);
|
|
466
|
+
}
|
|
467
|
+
return [name, doc];
|
|
451
468
|
}),
|
|
452
469
|
),
|
|
453
470
|
);
|
|
454
471
|
const plan = resolveEntry(release, docs, pack, entry, boot?.doc);
|
|
455
472
|
// The boot is a mounted pack like any other, so an entry borrows its loose files too.
|
|
456
|
-
const files = stagedFiles(plan, release, boot, at);
|
|
457
473
|
await Promise.all(mounts);
|
|
458
|
-
return loadAsset(plan.res, cls,
|
|
474
|
+
return loadAsset(plan.res, cls, stagedFiles(plan, boot, url)).load;
|
|
459
475
|
}
|
package/src/pack.ts
CHANGED
|
@@ -87,11 +87,6 @@ export function mountClosure(release: Release, entry: string): string[] {
|
|
|
87
87
|
return out;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
/** A pack's file, relative to a release base: `<name>/<version>/<file>` (version-addressed, immutable). */
|
|
91
|
-
export function packPath(name: string, version: string, file: string): string {
|
|
92
|
-
return `${name}/${version}/${file}`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
90
|
/** The `.pck` beside a pack's document: its name's last segment (a scoped name stays flat). The one
|
|
96
91
|
* convention the bake's writer and the runtime's reader share — mounting resolves it from the release
|
|
97
92
|
* alone, so it never waits on the document. */
|
package/src/preload.ts
CHANGED
|
@@ -1,165 +1,164 @@
|
|
|
1
|
-
/**********************************************************************
|
|
2
|
-
Copyright (c) Vladimir Davidovich. All rights reserved.
|
|
3
|
-
***********************************************************************/
|
|
4
|
-
|
|
5
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
-
import { dirname, resolve } from 'node:path';
|
|
7
|
-
import { plugin } from 'bun';
|
|
8
|
-
import { classOf, moduleExtensions } from './formats.ts';
|
|
9
|
-
import { BOOT_MANIFEST, BOOT_MODULE, PACK_DIR, PACK_DIR_ENV, PACK_DOC, RELEASE, descriptorOf, packEntries, stagedKeys, type PackDoc, type PackEntry } from './pack.ts';
|
|
10
|
-
import { assetModule, packModule, type ServedAsset } from './serve.ts';
|
|
11
|
-
import type { BunPlugin } from 'bun';
|
|
12
|
-
|
|
13
|
-
// Registers the godot-assets plugin: importing a Godot source asset becomes a module that stages what
|
|
14
|
-
// the source needs and loads it (default export = the load promise, `materialize` = its files staged).
|
|
15
|
-
//
|
|
16
|
-
// What a source needs comes from the bake (`@ringozz/godot/pack`), never from scanning source text:
|
|
17
|
-
// `dump-deps.gd` walked the project at bake time and wrote, per source, the loose (WebP-backed)
|
|
18
|
-
// texture sources the wasm engine cannot decode — and the descriptor of every loose file the app's
|
|
19
|
-
// pack owns. A packed source resolves inside the mounted `.pck`; only its loose textures are staged
|
|
20
|
-
// per file, each with the `.import` text and product `descriptorOf` reads from the same sidecar the
|
|
21
|
-
// bake read.
|
|
22
|
-
//
|
|
23
|
-
// The bake's own pack is the module it generated beside the packs (`boot-pack.js`): its `.pck`, the
|
|
24
|
-
// config the runtime stages content packs from, and the mount every asset module waits for. So
|
|
25
|
-
// importing any asset is what mounts the app's pack, and the config an app still supplies
|
|
26
|
-
// (`configurePacks({ at })`, where a content release is read) merges with it in either order.
|
|
27
|
-
//
|
|
28
|
-
// The load promise persists on `import.meta.hot.data` (carried across HMR re-evaluations) so `use()`
|
|
29
|
-
// never re-suspends; a rejected load is evicted so the next evaluation retries. The default export is
|
|
30
|
-
// the single plugin — registered globally via bunfig preload and loaded by `[serve.static] plugins`.
|
|
31
|
-
|
|
32
|
-
const ROOT = process.cwd();
|
|
33
|
-
const NS = 'godot';
|
|
34
|
-
|
|
35
|
-
const ASSET_RE = new RegExp(`\\.(${moduleExtensions.join('|')})$`, 'i');
|
|
36
|
-
|
|
37
|
-
const resOf = (abs: string) => 'res://' + abs.slice(ROOT.length + 1).replaceAll('\\', '/');
|
|
38
|
-
|
|
39
|
-
const readJson = <T>(file: string): T | null =>
|
|
40
|
-
existsSync(file) ? (JSON.parse(readFileSync(file, 'utf8')) as T) : null;
|
|
41
|
-
|
|
42
|
-
/** Where the boot pack was baked — `godot pack`'s `--out` ({@link PACK_DIR_ENV} overrides for a host). */
|
|
43
|
-
const DIR = resolve(ROOT, process.env[PACK_DIR_ENV] ?? PACK_DIR);
|
|
44
|
-
const MANIFEST = readJson<PackDoc>(resolve(DIR, BOOT_MANIFEST));
|
|
45
|
-
/** The bake's generated boot module (`{ mounting }` + its config); null when unbaked. */
|
|
46
|
-
const BOOT = MANIFEST ? resolve(DIR, BOOT_MODULE) : null;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* A content pack's entries, from the bake's own documents — `release.json` and each pack's emitted
|
|
50
|
-
* manifest, the same shapes the runtime fetches. A pack carries no entry module: the plugin serves one
|
|
51
|
-
* built from these, so an entry, its `res://` path and its class come from one declaration — the rule
|
|
52
|
-
* the bake emits the pack's types from as well (`packEntries`).
|
|
53
|
-
*/
|
|
54
|
-
const PACK_ENTRIES = readPacks();
|
|
55
|
-
|
|
56
|
-
function readPacks(): Map<string, PackEntry[]> {
|
|
57
|
-
const packs = new Map<string, PackEntry[]>();
|
|
58
|
-
const release = readJson<Record<string,
|
|
59
|
-
for (const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
*
|
|
81
|
-
* pack
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
//
|
|
132
|
-
// `
|
|
133
|
-
// `
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
args.kind !==
|
|
140
|
-
args.kind !== 'import
|
|
141
|
-
args.kind !== '
|
|
142
|
-
args.kind !== 'require-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
plugin(assetPlugin);
|
|
1
|
+
/**********************************************************************
|
|
2
|
+
Copyright (c) Vladimir Davidovich. All rights reserved.
|
|
3
|
+
***********************************************************************/
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, resolve } from 'node:path';
|
|
7
|
+
import { plugin } from 'bun';
|
|
8
|
+
import { classOf, moduleExtensions } from './formats.ts';
|
|
9
|
+
import { BOOT_MANIFEST, BOOT_MODULE, PACK_DIR, PACK_DIR_ENV, PACK_DOC, RELEASE, descriptorOf, packEntries, stagedKeys, type PackDoc, type PackEntry } from './pack.ts';
|
|
10
|
+
import { assetModule, packModule, type ServedAsset } from './serve.ts';
|
|
11
|
+
import type { BunPlugin } from 'bun';
|
|
12
|
+
|
|
13
|
+
// Registers the godot-assets plugin: importing a Godot source asset becomes a module that stages what
|
|
14
|
+
// the source needs and loads it (default export = the load promise, `materialize` = its files staged).
|
|
15
|
+
//
|
|
16
|
+
// What a source needs comes from the bake (`@ringozz/godot/pack`), never from scanning source text:
|
|
17
|
+
// `dump-deps.gd` walked the project at bake time and wrote, per source, the loose (WebP-backed)
|
|
18
|
+
// texture sources the wasm engine cannot decode — and the descriptor of every loose file the app's
|
|
19
|
+
// pack owns. A packed source resolves inside the mounted `.pck`; only its loose textures are staged
|
|
20
|
+
// per file, each with the `.import` text and product `descriptorOf` reads from the same sidecar the
|
|
21
|
+
// bake read.
|
|
22
|
+
//
|
|
23
|
+
// The bake's own pack is the module it generated beside the packs (`boot-pack.js`): its `.pck`, the
|
|
24
|
+
// config the runtime stages content packs from, and the mount every asset module waits for. So
|
|
25
|
+
// importing any asset is what mounts the app's pack, and the config an app still supplies
|
|
26
|
+
// (`configurePacks({ at })`, where a content release is read) merges with it in either order.
|
|
27
|
+
//
|
|
28
|
+
// The load promise persists on `import.meta.hot.data` (carried across HMR re-evaluations) so `use()`
|
|
29
|
+
// never re-suspends; a rejected load is evicted so the next evaluation retries. The default export is
|
|
30
|
+
// the single plugin — registered globally via bunfig preload and loaded by `[serve.static] plugins`.
|
|
31
|
+
|
|
32
|
+
const ROOT = process.cwd();
|
|
33
|
+
const NS = 'godot';
|
|
34
|
+
|
|
35
|
+
const ASSET_RE = new RegExp(`\\.(${moduleExtensions.join('|')})$`, 'i');
|
|
36
|
+
|
|
37
|
+
const resOf = (abs: string) => 'res://' + abs.slice(ROOT.length + 1).replaceAll('\\', '/');
|
|
38
|
+
|
|
39
|
+
const readJson = <T>(file: string): T | null =>
|
|
40
|
+
existsSync(file) ? (JSON.parse(readFileSync(file, 'utf8')) as T) : null;
|
|
41
|
+
|
|
42
|
+
/** Where the boot pack was baked — `godot pack`'s `--out` ({@link PACK_DIR_ENV} overrides for a host). */
|
|
43
|
+
const DIR = resolve(ROOT, process.env[PACK_DIR_ENV] ?? PACK_DIR);
|
|
44
|
+
const MANIFEST = readJson<PackDoc>(resolve(DIR, BOOT_MANIFEST));
|
|
45
|
+
/** The bake's generated boot module (`{ mounting }` + its config); null when unbaked. */
|
|
46
|
+
const BOOT = MANIFEST ? resolve(DIR, BOOT_MODULE) : null;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A content pack's entries, from the bake's own documents — `release.json` and each pack's emitted
|
|
50
|
+
* manifest, the same shapes the runtime fetches. A pack carries no entry module: the plugin serves one
|
|
51
|
+
* built from these, so an entry, its `res://` path and its class come from one declaration — the rule
|
|
52
|
+
* the bake emits the pack's types from as well (`packEntries`).
|
|
53
|
+
*/
|
|
54
|
+
const PACK_ENTRIES = readPacks();
|
|
55
|
+
|
|
56
|
+
function readPacks(): Map<string, PackEntry[]> {
|
|
57
|
+
const packs = new Map<string, PackEntry[]>();
|
|
58
|
+
const release = readJson<Record<string, unknown>>(resolve(DIR, RELEASE));
|
|
59
|
+
for (const pack of Object.keys(release ?? {})) {
|
|
60
|
+
const entries = packEntries(readJson<PackDoc>(resolve(DIR, pack, PACK_DOC))?.exports);
|
|
61
|
+
if (entries.length) packs.set(pack, entries);
|
|
62
|
+
}
|
|
63
|
+
return packs;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The loose texture sources one asset stages: what the bake's manifest recorded for it, or itself when
|
|
68
|
+
* it *is* a loose texture — a source with an `.import` sidecar is excluded from the pack, so a texture
|
|
69
|
+
* imported on its own has no packed source to name it (the same rule `dump-deps.gd` classifies by).
|
|
70
|
+
*/
|
|
71
|
+
function needsOf(abs: string): string[] {
|
|
72
|
+
const res = resOf(abs);
|
|
73
|
+
const baked = MANIFEST?.needs[res];
|
|
74
|
+
if (baked) return baked;
|
|
75
|
+
return existsSync(abs + '.import') ? [res] : [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A loose file's staging descriptor: the bake's own (its manifest carries every loose file the app's
|
|
80
|
+
* pack owns), or its sidecar when the bake never saw it — which is what keeps the plugin and a content
|
|
81
|
+
* pack's runtime staging on the same rule, `descriptorOf`.
|
|
82
|
+
*/
|
|
83
|
+
function descriptorFor(loose: string): { path: string; import: string } | null {
|
|
84
|
+
const baked = MANIFEST?.stage[loose];
|
|
85
|
+
if (baked) return baked;
|
|
86
|
+
const sidecar = resolve(ROOT, loose.slice('res://'.length)) + '.import';
|
|
87
|
+
return existsSync(sidecar) ? descriptorOf(readFileSync(sidecar, 'utf8')) : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* One source's module, from the bake's own records: the loose textures the bake recorded for it (`needs`
|
|
92
|
+
* / `stage`, or its sidecar when the bake never saw it), each as the absolute path the bundler imports
|
|
93
|
+
* plus the two keys it stages under. Every other decision is the module text's (`serve.ts`).
|
|
94
|
+
*/
|
|
95
|
+
function servedAsset(abs: string): ServedAsset {
|
|
96
|
+
return {
|
|
97
|
+
res: resOf(abs),
|
|
98
|
+
// The filter is `moduleExtensions`, so a path that reached here has a class.
|
|
99
|
+
cls: classOf(abs)!,
|
|
100
|
+
boot: BOOT,
|
|
101
|
+
files: needsOf(abs).flatMap((loose) => {
|
|
102
|
+
const descriptor = descriptorFor(loose);
|
|
103
|
+
if (!descriptor) return [];
|
|
104
|
+
const { sidecar, product } = stagedKeys(loose, descriptor);
|
|
105
|
+
return [
|
|
106
|
+
{
|
|
107
|
+
specifier: resolve(ROOT, descriptor.path.replace(/^res:\/\//, '')),
|
|
108
|
+
sidecar,
|
|
109
|
+
content: descriptor.import,
|
|
110
|
+
product,
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Exactly the pack names the bake declares — nothing else is touched. A wider filter would shadow
|
|
119
|
+
* resolutions Bun makes itself (it injects a `react-refresh` import in dev, and that import must fall
|
|
120
|
+
* through to it, not to a plugin that declines).
|
|
121
|
+
*/
|
|
122
|
+
const PACK_RE = PACK_ENTRIES.size
|
|
123
|
+
? new RegExp(`^(${[...PACK_ENTRIES.keys()].map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})$`)
|
|
124
|
+
: null;
|
|
125
|
+
|
|
126
|
+
const assetPlugin: BunPlugin = {
|
|
127
|
+
name: 'godot-assets',
|
|
128
|
+
setup(build) {
|
|
129
|
+
build.onResolve({ filter: ASSET_RE }, (args) => {
|
|
130
|
+
// Only JS module imports of Godot source assets become `godot` modules.
|
|
131
|
+
// `Bun.build` reports JS imports as `import-statement` (and
|
|
132
|
+
// `dynamic-import`/`require-*`) but HTML asset references (<link>/<img>)
|
|
133
|
+
// and CSS url() refs as `internal`/`url-token` — those fall through so
|
|
134
|
+
// Bun copies/hashes the file normally. The runtime preload (bun run /
|
|
135
|
+
// bun test) reports JS imports with no kind (`undefined`), so treat that
|
|
136
|
+
// as a JS import too.
|
|
137
|
+
if (
|
|
138
|
+
args.kind !== undefined &&
|
|
139
|
+
args.kind !== 'import-statement' &&
|
|
140
|
+
args.kind !== 'dynamic-import' &&
|
|
141
|
+
args.kind !== 'require-call' &&
|
|
142
|
+
args.kind !== 'require-resolve'
|
|
143
|
+
) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const base = args.importer ? dirname(args.importer) : (args.resolveDir ?? ROOT);
|
|
147
|
+
return { path: resolve(base, args.path), namespace: NS };
|
|
148
|
+
});
|
|
149
|
+
// A pack's own module (the pack carries no entry file, so the plugin serves it, built from the
|
|
150
|
+
// bake's manifests). The filter names the packs exactly: anything else resolves as usual.
|
|
151
|
+
if (PACK_RE) {
|
|
152
|
+
build.onResolve({ filter: PACK_RE }, (args) => ({ path: args.path, namespace: NS }));
|
|
153
|
+
}
|
|
154
|
+
build.onLoad({ filter: /.*/, namespace: NS }, (args) => ({
|
|
155
|
+
contents: PACK_ENTRIES.has(args.path)
|
|
156
|
+
? packModule(args.path, PACK_ENTRIES.get(args.path)!)
|
|
157
|
+
: assetModule(servedAsset(args.path)),
|
|
158
|
+
loader: 'js',
|
|
159
|
+
}));
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export default assetPlugin;
|
|
164
|
+
plugin(assetPlugin);
|
package/src/serve.ts
CHANGED
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
/**********************************************************************
|
|
2
|
-
Copyright (c) Vladimir Davidovich. All rights reserved.
|
|
3
|
-
***********************************************************************/
|
|
4
|
-
|
|
5
|
-
// The source text the preload plugin serves, as a virtual module per import: one for a Godot source
|
|
6
|
-
// asset (the loose textures it stages, then the load), one for a content pack (a named export per entry
|
|
7
|
-
// it declares). Pure — a module's text is a function of the facts the plugin resolved — which is what
|
|
8
|
-
// makes the one part of the plugin that decides what an import becomes reachable without a bake, a
|
|
9
|
-
// bundler or an engine. The plugin resolves those facts (`preload.ts`); nothing here touches a disk.
|
|
10
|
-
|
|
11
|
-
import { entryId, type PackEntry } from './pack.ts';
|
|
12
|
-
|
|
13
|
-
/** The facts the plugin resolved for one source asset, as the module it serves reads them. */
|
|
14
|
-
export interface ServedAsset {
|
|
15
|
-
/** The source's `res://` path — what the module loads. */
|
|
16
|
-
res: string;
|
|
17
|
-
/** The class it loads as. */
|
|
18
|
-
cls: string;
|
|
19
|
-
/**
|
|
20
|
-
* One entry per loose texture the source needs: the file to bundle (an absolute path, so the host can
|
|
21
|
-
* transcode it), and the two keys it stages under. The sidecar text is inlined rather than imported
|
|
22
|
-
* as text — Bun's `with { type: 'text' }` resolves to a null/default under the dev server's HMR
|
|
23
|
-
* wrapper for a module's own path.
|
|
24
|
-
*/
|
|
25
|
-
files: Array<{ specifier: string; sidecar: string; content: string; product: string }>;
|
|
26
|
-
/** The boot module every asset module waits for (`{ mounting }`); `null` when the project is unbaked. */
|
|
27
|
-
boot: string | null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* One source's module: its loose textures as bundled files, and the load. The boot module supplies the
|
|
32
|
-
* mount to wait for, so the pack's remaps are in place before anything resolves — and importing it is
|
|
33
|
-
* also what mounts the boot pack.
|
|
34
|
-
*/
|
|
35
|
-
export function assetModule({ res, cls, files, boot }: ServedAsset): string {
|
|
36
|
-
const imports = [
|
|
37
|
-
`import { loadAsset } from '@ringozz/godot/load';`,
|
|
38
|
-
`import { ${cls} } from '@ringozz/godot/${cls}';`,
|
|
39
|
-
];
|
|
40
|
-
const map: string[] = [];
|
|
41
|
-
files.forEach(({ specifier, sidecar, content, product }, n) => {
|
|
42
|
-
imports.push(`import f${n} from ${JSON.stringify(specifier)} with { type: 'file' };`);
|
|
43
|
-
map.push(`\t${JSON.stringify(sidecar)}: { content: ${JSON.stringify(content)} },`);
|
|
44
|
-
map.push(`\t${JSON.stringify(product)}: { path: f${n} },`);
|
|
45
|
-
});
|
|
46
|
-
if (boot) imports.push(`import { mounting } from ${JSON.stringify(boot)};`);
|
|
47
|
-
const staged = map.length ? `const files = {\n${map.join('\n')}\n};` : 'const files = {};';
|
|
48
|
-
const load = `const { materialize, load } = loadAsset(${JSON.stringify(res)}, ${cls}, files, import.meta.hot.data, ${boot ? 'mounting' : 'undefined'});`;
|
|
49
|
-
return `${imports.join('\n')}\n${staged}\n${load}\nexport { materialize, load as default };\n`;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* A content pack's module: one named export per entry its document declares, each loading through
|
|
54
|
-
* `loadPackEntry`, so a client resolves the entry by name from the document it fetched. Nothing about
|
|
55
|
-
* the pack is baked in — no entry file, no path — and the class is the same format-table rule every
|
|
56
|
-
* asset import uses.
|
|
57
|
-
*/
|
|
58
|
-
export function packModule(pack: string, entries: PackEntry[]): string {
|
|
59
|
-
const imports = ["import { loadPackEntry } from '@ringozz/godot/load';"];
|
|
60
|
-
for (const cls of new Set(entries.map((e) => e.cls))) {
|
|
61
|
-
imports.push(`import { ${cls} } from '@ringozz/godot/${cls}';`);
|
|
62
|
-
}
|
|
63
|
-
const lines = entries.map(
|
|
64
|
-
(e) => `export const ${entryId(e.entry)} = loadPackEntry(${JSON.stringify(pack)}, ${JSON.stringify(e.entry)}, ${e.cls});`,
|
|
65
|
-
);
|
|
66
|
-
return `${imports.join('\n')}\n\n${lines.join('\n')}\n`;
|
|
67
|
-
}
|
|
1
|
+
/**********************************************************************
|
|
2
|
+
Copyright (c) Vladimir Davidovich. All rights reserved.
|
|
3
|
+
***********************************************************************/
|
|
4
|
+
|
|
5
|
+
// The source text the preload plugin serves, as a virtual module per import: one for a Godot source
|
|
6
|
+
// asset (the loose textures it stages, then the load), one for a content pack (a named export per entry
|
|
7
|
+
// it declares). Pure — a module's text is a function of the facts the plugin resolved — which is what
|
|
8
|
+
// makes the one part of the plugin that decides what an import becomes reachable without a bake, a
|
|
9
|
+
// bundler or an engine. The plugin resolves those facts (`preload.ts`); nothing here touches a disk.
|
|
10
|
+
|
|
11
|
+
import { entryId, type PackEntry } from './pack.ts';
|
|
12
|
+
|
|
13
|
+
/** The facts the plugin resolved for one source asset, as the module it serves reads them. */
|
|
14
|
+
export interface ServedAsset {
|
|
15
|
+
/** The source's `res://` path — what the module loads. */
|
|
16
|
+
res: string;
|
|
17
|
+
/** The class it loads as. */
|
|
18
|
+
cls: string;
|
|
19
|
+
/**
|
|
20
|
+
* One entry per loose texture the source needs: the file to bundle (an absolute path, so the host can
|
|
21
|
+
* transcode it), and the two keys it stages under. The sidecar text is inlined rather than imported
|
|
22
|
+
* as text — Bun's `with { type: 'text' }` resolves to a null/default under the dev server's HMR
|
|
23
|
+
* wrapper for a module's own path.
|
|
24
|
+
*/
|
|
25
|
+
files: Array<{ specifier: string; sidecar: string; content: string; product: string }>;
|
|
26
|
+
/** The boot module every asset module waits for (`{ mounting }`); `null` when the project is unbaked. */
|
|
27
|
+
boot: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* One source's module: its loose textures as bundled files, and the load. The boot module supplies the
|
|
32
|
+
* mount to wait for, so the pack's remaps are in place before anything resolves — and importing it is
|
|
33
|
+
* also what mounts the boot pack.
|
|
34
|
+
*/
|
|
35
|
+
export function assetModule({ res, cls, files, boot }: ServedAsset): string {
|
|
36
|
+
const imports = [
|
|
37
|
+
`import { loadAsset } from '@ringozz/godot/load';`,
|
|
38
|
+
`import { ${cls} } from '@ringozz/godot/${cls}';`,
|
|
39
|
+
];
|
|
40
|
+
const map: string[] = [];
|
|
41
|
+
files.forEach(({ specifier, sidecar, content, product }, n) => {
|
|
42
|
+
imports.push(`import f${n} from ${JSON.stringify(specifier)} with { type: 'file' };`);
|
|
43
|
+
map.push(`\t${JSON.stringify(sidecar)}: { content: ${JSON.stringify(content)} },`);
|
|
44
|
+
map.push(`\t${JSON.stringify(product)}: { path: f${n} },`);
|
|
45
|
+
});
|
|
46
|
+
if (boot) imports.push(`import { mounting } from ${JSON.stringify(boot)};`);
|
|
47
|
+
const staged = map.length ? `const files = {\n${map.join('\n')}\n};` : 'const files = {};';
|
|
48
|
+
const load = `const { materialize, load } = loadAsset(${JSON.stringify(res)}, ${cls}, files, import.meta.hot.data, ${boot ? 'mounting' : 'undefined'});`;
|
|
49
|
+
return `${imports.join('\n')}\n${staged}\n${load}\nexport { materialize, load as default };\n`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A content pack's module: one named export per entry its document declares, each loading through
|
|
54
|
+
* `loadPackEntry`, so a client resolves the entry by name from the document it fetched. Nothing about
|
|
55
|
+
* the pack is baked in — no entry file, no path — and the class is the same format-table rule every
|
|
56
|
+
* asset import uses.
|
|
57
|
+
*/
|
|
58
|
+
export function packModule(pack: string, entries: PackEntry[]): string {
|
|
59
|
+
const imports = ["import { loadPackEntry } from '@ringozz/godot/load';"];
|
|
60
|
+
for (const cls of new Set(entries.map((e) => e.cls))) {
|
|
61
|
+
imports.push(`import { ${cls} } from '@ringozz/godot/${cls}';`);
|
|
62
|
+
}
|
|
63
|
+
const lines = entries.map(
|
|
64
|
+
(e) => `export const ${entryId(e.entry)} = loadPackEntry(${JSON.stringify(pack)}, ${JSON.stringify(e.entry)}, ${e.cls});`,
|
|
65
|
+
);
|
|
66
|
+
return `${imports.join('\n')}\n\n${lines.join('\n')}\n`;
|
|
67
|
+
}
|