@ringozz/godot 4.7.2-661 → 4.7.2-664
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 +24 -5
- package/package.json +5 -4
- package/src/assets.d.ts +10 -1
- package/src/bake.ts +469 -0
- package/src/dump-deps.gd +140 -66
- package/src/editor.ts +1 -1
- package/src/formats.ts +78 -0
- package/src/load.ts +153 -17
- package/src/pack.ts +281 -220
- package/src/preload.ts +165 -211
- package/src/serve.ts +67 -0
package/src/preload.ts
CHANGED
|
@@ -1,211 +1,165 @@
|
|
|
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
|
|
9
|
-
import type
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (kind === 'text') {
|
|
167
|
-
map.push(` ${JSON.stringify(resPath)}: { content: ${JSON.stringify(readFileSync(absP, 'utf8'))} },`);
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
const v = `f${fi++}`;
|
|
171
|
-
imports.push(`import ${v} from ${JSON.stringify(absP)} with { type: 'file' };`);
|
|
172
|
-
map.push(` ${JSON.stringify(resPath)}: { path: ${v} },`);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const filesVar = files.size ? `const files = {\n${map.join('\n')}\n};` : `const files = {};`;
|
|
176
|
-
const loadAsset = `const { materialize, load } = loadAsset(${JSON.stringify(res)}, ${cls}, files, import.meta.hot.data, ${ready});`;
|
|
177
|
-
return imports.join('\n') + '\n' + filesVar + '\n' + loadAsset + '\n' + `export { materialize, load as default };` + '\n';
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const assetPlugin: BunPlugin = {
|
|
181
|
-
name: 'godot-assets',
|
|
182
|
-
setup(build) {
|
|
183
|
-
build.onResolve({ filter: ASSET_RE }, (args) => {
|
|
184
|
-
// Only JS module imports of Godot source assets become `godot` modules.
|
|
185
|
-
// `Bun.build` reports JS imports as `import-statement` (and
|
|
186
|
-
// `dynamic-import`/`require-*`) but HTML asset references (<link>/<img>)
|
|
187
|
-
// and CSS url() refs as `internal`/`url-token` — those fall through so
|
|
188
|
-
// Bun copies/hashes the file normally. The runtime preload (bun run /
|
|
189
|
-
// bun test) reports JS imports with no kind (`undefined`), so treat that
|
|
190
|
-
// as a JS import too.
|
|
191
|
-
if (
|
|
192
|
-
args.kind !== undefined &&
|
|
193
|
-
args.kind !== 'import-statement' &&
|
|
194
|
-
args.kind !== 'dynamic-import' &&
|
|
195
|
-
args.kind !== 'require-call' &&
|
|
196
|
-
args.kind !== 'require-resolve'
|
|
197
|
-
) {
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
const base = args.importer ? dirname(args.importer) : (args.resolveDir ?? ROOT);
|
|
201
|
-
return { path: resolve(base, args.path), namespace: NS };
|
|
202
|
-
});
|
|
203
|
-
build.onLoad({ filter: /.*/, namespace: NS }, (args) => ({
|
|
204
|
-
contents: ensureModule(args.path),
|
|
205
|
-
loader: 'js',
|
|
206
|
-
}));
|
|
207
|
-
},
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
export default assetPlugin;
|
|
211
|
-
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, { version?: string }>>(resolve(DIR, RELEASE));
|
|
59
|
+
for (const [pack, { version }] of Object.entries(release ?? {})) {
|
|
60
|
+
if (!version) continue;
|
|
61
|
+
const entries = packEntries(readJson<PackDoc>(resolve(DIR, pack, version, PACK_DOC))?.exports);
|
|
62
|
+
if (entries.length) packs.set(pack, entries);
|
|
63
|
+
}
|
|
64
|
+
return packs;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The loose texture sources one asset stages: what the bake's manifest recorded for it, or itself when
|
|
69
|
+
* it *is* a loose texture — a source with an `.import` sidecar is excluded from the pack, so a texture
|
|
70
|
+
* imported on its own has no packed source to name it (the same rule `dump-deps.gd` classifies by).
|
|
71
|
+
*/
|
|
72
|
+
function needsOf(abs: string): string[] {
|
|
73
|
+
const res = resOf(abs);
|
|
74
|
+
const baked = MANIFEST?.needs[res];
|
|
75
|
+
if (baked) return baked;
|
|
76
|
+
return existsSync(abs + '.import') ? [res] : [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A loose file's staging descriptor: the bake's own (its manifest carries every loose file the app's
|
|
81
|
+
* pack owns), or its sidecar when the bake never saw it — which is what keeps the plugin and a content
|
|
82
|
+
* pack's runtime staging on the same rule, `descriptorOf`.
|
|
83
|
+
*/
|
|
84
|
+
function descriptorFor(loose: string): { path: string; import: string } | null {
|
|
85
|
+
const baked = MANIFEST?.stage[loose];
|
|
86
|
+
if (baked) return baked;
|
|
87
|
+
const sidecar = resolve(ROOT, loose.slice('res://'.length)) + '.import';
|
|
88
|
+
return existsSync(sidecar) ? descriptorOf(readFileSync(sidecar, 'utf8')) : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* One source's module, from the bake's own records: the loose textures the bake recorded for it (`needs`
|
|
93
|
+
* / `stage`, or its sidecar when the bake never saw it), each as the absolute path the bundler imports
|
|
94
|
+
* plus the two keys it stages under. Every other decision is the module text's (`serve.ts`).
|
|
95
|
+
*/
|
|
96
|
+
function servedAsset(abs: string): ServedAsset {
|
|
97
|
+
return {
|
|
98
|
+
res: resOf(abs),
|
|
99
|
+
// The filter is `moduleExtensions`, so a path that reached here has a class.
|
|
100
|
+
cls: classOf(abs)!,
|
|
101
|
+
boot: BOOT,
|
|
102
|
+
files: needsOf(abs).flatMap((loose) => {
|
|
103
|
+
const descriptor = descriptorFor(loose);
|
|
104
|
+
if (!descriptor) return [];
|
|
105
|
+
const { sidecar, product } = stagedKeys(loose, descriptor);
|
|
106
|
+
return [
|
|
107
|
+
{
|
|
108
|
+
specifier: resolve(ROOT, descriptor.path.replace(/^res:\/\//, '')),
|
|
109
|
+
sidecar,
|
|
110
|
+
content: descriptor.import,
|
|
111
|
+
product,
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
}),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Exactly the pack names the bake declares — nothing else is touched. A wider filter would shadow
|
|
120
|
+
* resolutions Bun makes itself (it injects a `react-refresh` import in dev, and that import must fall
|
|
121
|
+
* through to it, not to a plugin that declines).
|
|
122
|
+
*/
|
|
123
|
+
const PACK_RE = PACK_ENTRIES.size
|
|
124
|
+
? new RegExp(`^(${[...PACK_ENTRIES.keys()].map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})$`)
|
|
125
|
+
: null;
|
|
126
|
+
|
|
127
|
+
const assetPlugin: BunPlugin = {
|
|
128
|
+
name: 'godot-assets',
|
|
129
|
+
setup(build) {
|
|
130
|
+
build.onResolve({ filter: ASSET_RE }, (args) => {
|
|
131
|
+
// Only JS module imports of Godot source assets become `godot` modules.
|
|
132
|
+
// `Bun.build` reports JS imports as `import-statement` (and
|
|
133
|
+
// `dynamic-import`/`require-*`) but HTML asset references (<link>/<img>)
|
|
134
|
+
// and CSS url() refs as `internal`/`url-token` — those fall through so
|
|
135
|
+
// Bun copies/hashes the file normally. The runtime preload (bun run /
|
|
136
|
+
// bun test) reports JS imports with no kind (`undefined`), so treat that
|
|
137
|
+
// as a JS import too.
|
|
138
|
+
if (
|
|
139
|
+
args.kind !== undefined &&
|
|
140
|
+
args.kind !== 'import-statement' &&
|
|
141
|
+
args.kind !== 'dynamic-import' &&
|
|
142
|
+
args.kind !== 'require-call' &&
|
|
143
|
+
args.kind !== 'require-resolve'
|
|
144
|
+
) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const base = args.importer ? dirname(args.importer) : (args.resolveDir ?? ROOT);
|
|
148
|
+
return { path: resolve(base, args.path), namespace: NS };
|
|
149
|
+
});
|
|
150
|
+
// A pack's own module (the pack carries no entry file, so the plugin serves it, built from the
|
|
151
|
+
// bake's manifests). The filter names the packs exactly: anything else resolves as usual.
|
|
152
|
+
if (PACK_RE) {
|
|
153
|
+
build.onResolve({ filter: PACK_RE }, (args) => ({ path: args.path, namespace: NS }));
|
|
154
|
+
}
|
|
155
|
+
build.onLoad({ filter: /.*/, namespace: NS }, (args) => ({
|
|
156
|
+
contents: PACK_ENTRIES.has(args.path)
|
|
157
|
+
? packModule(args.path, PACK_ENTRIES.get(args.path)!)
|
|
158
|
+
: assetModule(servedAsset(args.path)),
|
|
159
|
+
loader: 'js',
|
|
160
|
+
}));
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export default assetPlugin;
|
|
165
|
+
plugin(assetPlugin);
|
package/src/serve.ts
ADDED
|
@@ -0,0 +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
|
+
}
|