@pix3/vite-plugin 0.0.0-stage → 2.0.0-alpha.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/LICENSE +201 -0
- package/README.md +39 -2
- package/dist/build/bundle-hooks.d.ts +23 -0
- package/dist/build/bundle-hooks.js +79 -0
- package/dist/build/classic-script.d.ts +11 -0
- package/dist/build/classic-script.js +46 -0
- package/dist/build/compress.d.ts +29 -0
- package/dist/build/compress.js +63 -0
- package/dist/build/dependency-imports.d.ts +53 -0
- package/dist/build/dependency-imports.js +239 -0
- package/dist/build/dev-manifest.d.ts +10 -0
- package/dist/build/dev-manifest.js +25 -0
- package/dist/build/editor-flush.d.ts +42 -0
- package/dist/build/editor-flush.js +91 -0
- package/dist/build/finish.d.ts +11 -0
- package/dist/build/finish.js +126 -0
- package/dist/build/output-plugins.d.ts +26 -0
- package/dist/build/output-plugins.js +88 -0
- package/dist/build/plan.d.ts +75 -0
- package/dist/build/plan.js +162 -0
- package/dist/build/player-modules.d.ts +123 -0
- package/dist/build/player-modules.js +254 -0
- package/dist/build/project-manifest.d.ts +57 -0
- package/dist/build/project-manifest.js +169 -0
- package/dist/build/record.d.ts +25 -0
- package/dist/build/record.js +14 -0
- package/dist/build/report.d.ts +114 -0
- package/dist/build/report.js +102 -0
- package/dist/build/scan.d.ts +84 -0
- package/dist/build/scan.js +375 -0
- package/dist/build/strip-decision.d.ts +27 -0
- package/dist/build/strip-decision.js +82 -0
- package/dist/build/strippable-runtime-modules.d.ts +85 -0
- package/dist/build/strippable-runtime-modules.js +309 -0
- package/dist/build/zip.d.ts +6 -0
- package/dist/build/zip.js +28 -0
- package/dist/client/host-files.d.ts +96 -0
- package/dist/client/host-files.js +169 -0
- package/dist/client/index.d.ts +218 -0
- package/dist/client/index.js +381 -0
- package/dist/dev-info.d.ts +55 -0
- package/dist/dev-info.js +116 -0
- package/dist/dev-server.d.ts +28 -0
- package/dist/dev-server.js +139 -0
- package/dist/editor-page.d.ts +31 -0
- package/dist/editor-page.js +74 -0
- package/dist/files/changeset.d.ts +84 -0
- package/dist/files/changeset.js +321 -0
- package/dist/files/content-type.d.ts +1 -0
- package/dist/files/content-type.js +56 -0
- package/dist/files/history.d.ts +67 -0
- package/dist/files/history.js +162 -0
- package/dist/files/paths.d.ts +41 -0
- package/dist/files/paths.js +144 -0
- package/dist/files/project-files.d.ts +208 -0
- package/dist/files/project-files.js +1062 -0
- package/dist/files/scan.d.ts +71 -0
- package/dist/files/scan.js +252 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +290 -0
- package/dist/module-paths.d.ts +13 -0
- package/dist/module-paths.js +32 -0
- package/dist/player/debug/context.d.ts +9 -0
- package/dist/player/debug/context.js +1 -0
- package/dist/player/debug/device-info.d.ts +15 -0
- package/dist/player/debug/device-info.js +36 -0
- package/dist/player/debug/frame-stats.d.ts +79 -0
- package/dist/player/debug/frame-stats.js +159 -0
- package/dist/player/debug/index.d.ts +10 -0
- package/dist/player/debug/index.js +20 -0
- package/dist/player/debug/long-frames.d.ts +46 -0
- package/dist/player/debug/long-frames.js +79 -0
- package/dist/player/debug/overlay.d.ts +25 -0
- package/dist/player/debug/overlay.js +320 -0
- package/dist/player/debug/session.d.ts +92 -0
- package/dist/player/debug/session.js +156 -0
- package/dist/player/dev/editor-link.d.ts +16 -0
- package/dist/player/dev/editor-link.js +125 -0
- package/dist/player/index.d.ts +38 -0
- package/dist/player/index.js +119 -0
- package/dist/server/editor-socket.d.ts +53 -0
- package/dist/server/editor-socket.js +134 -0
- package/dist/server/guard.d.ts +61 -0
- package/dist/server/guard.js +165 -0
- package/dist/server/http.d.ts +24 -0
- package/dist/server/http.js +70 -0
- package/dist/server/image-keys.d.ts +46 -0
- package/dist/server/image-keys.js +118 -0
- package/dist/server/image-proxy.d.ts +32 -0
- package/dist/server/image-proxy.js +124 -0
- package/dist/server/router.d.ts +54 -0
- package/dist/server/router.js +147 -0
- package/dist/sync/barrier.d.ts +69 -0
- package/dist/sync/barrier.js +240 -0
- package/dist/sync/hot-update.d.ts +16 -0
- package/dist/sync/hot-update.js +58 -0
- package/dist/sync/script-graph.d.ts +79 -0
- package/dist/sync/script-graph.js +214 -0
- package/dist/vite-config.d.ts +12 -0
- package/dist/vite-config.js +65 -0
- package/package.json +58 -4
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { NON_SHIPPABLE_DIRECTORIES } from "../files/scan.js";
|
|
4
|
+
import { RESERVED_ROOT_DIR } from "../files/paths.js";
|
|
5
|
+
import { normalizeResRoot, resToAbsolute, stripRes, wireToRes } from "./project-manifest.js";
|
|
6
|
+
const TEXT_SOURCE_EXTENSIONS = new Set([
|
|
7
|
+
'pix3scene',
|
|
8
|
+
'prefab',
|
|
9
|
+
'pix3anim',
|
|
10
|
+
'ts',
|
|
11
|
+
'tsx',
|
|
12
|
+
'mts',
|
|
13
|
+
'cts',
|
|
14
|
+
'js',
|
|
15
|
+
'jsx',
|
|
16
|
+
'mjs',
|
|
17
|
+
'cjs',
|
|
18
|
+
'json',
|
|
19
|
+
'yaml',
|
|
20
|
+
'yml',
|
|
21
|
+
'html',
|
|
22
|
+
'css',
|
|
23
|
+
'atlas',
|
|
24
|
+
// Single-file components: a game may hang its HUD on a framework.
|
|
25
|
+
'vue',
|
|
26
|
+
'svelte',
|
|
27
|
+
'astro',
|
|
28
|
+
]);
|
|
29
|
+
/** Text sources whose `import`s are imports (not a `package.json`'s dependency list). */
|
|
30
|
+
const CODE_EXTENSIONS = new Set([
|
|
31
|
+
'ts',
|
|
32
|
+
'tsx',
|
|
33
|
+
'mts',
|
|
34
|
+
'cts',
|
|
35
|
+
'js',
|
|
36
|
+
'jsx',
|
|
37
|
+
'mjs',
|
|
38
|
+
'cjs',
|
|
39
|
+
'html',
|
|
40
|
+
'vue',
|
|
41
|
+
'svelte',
|
|
42
|
+
'astro',
|
|
43
|
+
]);
|
|
44
|
+
/** Tests and declarations: their mentions would only keep modules the game never runs. */
|
|
45
|
+
const NOT_A_SOURCE = /\.(?:spec|test)\.[cm]?[jt]sx?$|\.d\.[cm]?ts$|^package-lock\.json$/;
|
|
46
|
+
const SCENE_LIKE = /\.(pix3scene|prefab)$/i;
|
|
47
|
+
const SCANNABLE_RESOURCE = /\.(pix3scene|prefab|pix3anim)$/i;
|
|
48
|
+
const RESOURCE_PATH_PATTERN = /res:\/\/([^\s"'`\])]+)/g;
|
|
49
|
+
const MENTIONED_NAME_PATTERN = /[A-Za-z_][A-Za-z0-9_]*(?::[A-Za-z_][A-Za-z0-9_]*)*/g;
|
|
50
|
+
const IMPORT_SPECIFIER_PATTERN = /\b(?:from|import)\s*\(?\s*['"`]([^'"`\s]+)['"`]|\brequire\s*\(\s*['"`]([^'"`\s]+)['"`]/g;
|
|
51
|
+
/** Every identifier-like token of a text (the strip's mention superset) into `names`. */
|
|
52
|
+
export const collectMentionedNames = (text, names) => {
|
|
53
|
+
for (const match of text.matchAll(MENTIONED_NAME_PATTERN))
|
|
54
|
+
names.add(match[0]);
|
|
55
|
+
};
|
|
56
|
+
/** Every module specifier a code text imports, statically or not, into `specifiers`. */
|
|
57
|
+
export const collectImportedSpecifiers = (text, specifiers) => {
|
|
58
|
+
for (const match of text.matchAll(IMPORT_SPECIFIER_PATTERN)) {
|
|
59
|
+
specifiers.add(match[1] ?? match[2]);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
/** Whether a set of specifiers imports `name` itself or a subpath of it. */
|
|
63
|
+
const importsPackage = (specifiers, name) => [...specifiers].some(specifier => specifier === name || specifier.startsWith(`${name}/`));
|
|
64
|
+
/** Names that mean "this project talks to the multiplayer stack". */
|
|
65
|
+
export const NETWORK_MENTION_NAMES = [
|
|
66
|
+
'network',
|
|
67
|
+
'netNodes',
|
|
68
|
+
'Network',
|
|
69
|
+
'NetworkService',
|
|
70
|
+
'NetworkedNode',
|
|
71
|
+
'ReplicatedTransform',
|
|
72
|
+
'NetworkedNodeBehavior',
|
|
73
|
+
'ReplicatedTransformBehavior',
|
|
74
|
+
'core:NetworkedNode',
|
|
75
|
+
'core:ReplicatedTransform',
|
|
76
|
+
];
|
|
77
|
+
const SPINE_PAGE_IMAGE_PATTERN = /\.(png|jpg|jpeg|webp|ktx2|basis|bmp)$/i;
|
|
78
|
+
/**
|
|
79
|
+
* A `.pix3scene` that is instantiated rather than navigated to: `prefabs/`, `.prefab`, and
|
|
80
|
+
* `scenes/ui/` overlays (the conventions of the 1.x exporter and `pix3 smoke`).
|
|
81
|
+
*/
|
|
82
|
+
export const isPrefabPath = (path) => /(^|\/)prefabs\//i.test(path) || /\.prefab$/i.test(path) || /(^|\/)scenes\/ui\//i.test(path);
|
|
83
|
+
/** A scene the player can boot into or `changeScene` to: a `.pix3scene` that is not a prefab. */
|
|
84
|
+
export const isPlayableScenePath = (path) => path.toLowerCase().endsWith('.pix3scene') && !isPrefabPath(path);
|
|
85
|
+
const hasFileExtension = (path) => /\.[a-zA-Z0-9]+$/.test(path);
|
|
86
|
+
const extensionOf = (name) => {
|
|
87
|
+
const dot = name.lastIndexOf('.');
|
|
88
|
+
return dot <= 0 ? '' : name.slice(dot + 1).toLowerCase();
|
|
89
|
+
};
|
|
90
|
+
/** Page image names declared inside an `.atlas` file (port of `parseSpineAtlasPageNames`). */
|
|
91
|
+
export const parseSpineAtlasPageNames = (atlasText) => {
|
|
92
|
+
const pages = [];
|
|
93
|
+
let expectPage = true;
|
|
94
|
+
for (const rawLine of atlasText.split(/\r?\n/)) {
|
|
95
|
+
if (rawLine.trim().length === 0) {
|
|
96
|
+
expectPage = true;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (expectPage) {
|
|
100
|
+
const candidate = rawLine.trim();
|
|
101
|
+
if (SPINE_PAGE_IMAGE_PATTERN.test(candidate))
|
|
102
|
+
pages.push(candidate);
|
|
103
|
+
expectPage = false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return pages;
|
|
107
|
+
};
|
|
108
|
+
const resolveSpinePagePath = (atlasPath, pageName) => {
|
|
109
|
+
const name = pageName.trim().replace(/^\.\//, '');
|
|
110
|
+
if (/^[a-z]+[a-z0-9+.-]*:\/\//i.test(name) || name.startsWith('/'))
|
|
111
|
+
return stripRes(name);
|
|
112
|
+
const separator = atlasPath.lastIndexOf('/');
|
|
113
|
+
return separator < 0 ? name : `${atlasPath.slice(0, separator + 1)}${name}`;
|
|
114
|
+
};
|
|
115
|
+
/** Every file under `root` as a wire path, excluding tooling, build output and `.pix3/`. */
|
|
116
|
+
export const listProjectFiles = async (root) => {
|
|
117
|
+
const out = [];
|
|
118
|
+
const walk = async (wireDir) => {
|
|
119
|
+
const absolute = wireDir ? join(root, ...wireDir.split('/')) : root;
|
|
120
|
+
let entries;
|
|
121
|
+
try {
|
|
122
|
+
entries = await readdir(absolute, { withFileTypes: true });
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
const wirePath = wireDir ? `${wireDir}/${entry.name}` : entry.name;
|
|
130
|
+
if (entry.isDirectory()) {
|
|
131
|
+
if (NON_SHIPPABLE_DIRECTORIES.has(entry.name))
|
|
132
|
+
continue;
|
|
133
|
+
if (!wireDir && entry.name === RESERVED_ROOT_DIR)
|
|
134
|
+
continue;
|
|
135
|
+
await walk(wirePath);
|
|
136
|
+
}
|
|
137
|
+
else if (entry.isFile()) {
|
|
138
|
+
out.push(wirePath);
|
|
139
|
+
}
|
|
140
|
+
// Symlinks are neither listed nor followed (the file API's rule, `files/scan.ts`).
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
await walk('');
|
|
144
|
+
return out;
|
|
145
|
+
};
|
|
146
|
+
const isTextSource = (wirePath) => {
|
|
147
|
+
const name = wirePath.slice(wirePath.lastIndexOf('/') + 1);
|
|
148
|
+
return TEXT_SOURCE_EXTENSIONS.has(extensionOf(name)) && !NOT_A_SOURCE.test(name);
|
|
149
|
+
};
|
|
150
|
+
/** The static directory prefix of an interpolated `res://` path, or null (port of 1.x). */
|
|
151
|
+
const staticDirectoryPrefix = (resourcePath) => {
|
|
152
|
+
const staticPrefix = resourcePath.split('${')[0] ?? '';
|
|
153
|
+
const lastSlash = staticPrefix.lastIndexOf('/');
|
|
154
|
+
if (lastSlash <= 0)
|
|
155
|
+
return null;
|
|
156
|
+
const directory = staticPrefix.slice(0, lastSlash);
|
|
157
|
+
return directory.length > 0 ? directory : null;
|
|
158
|
+
};
|
|
159
|
+
export const scanProject = async (options) => {
|
|
160
|
+
const { root, manifest } = options;
|
|
161
|
+
const resRoot = normalizeResRoot(options.resRoot);
|
|
162
|
+
const warnings = [];
|
|
163
|
+
const mentionedNames = new Set();
|
|
164
|
+
const importedSpecifiers = new Set();
|
|
165
|
+
const resAbsolute = (resPath) => resToAbsolute(root, resRoot, resPath);
|
|
166
|
+
const wirePaths = await listProjectFiles(root);
|
|
167
|
+
const resFiles = new Set();
|
|
168
|
+
for (const wirePath of wirePaths) {
|
|
169
|
+
const resPath = wireToRes(wirePath, resRoot);
|
|
170
|
+
if (resPath !== null)
|
|
171
|
+
resFiles.add(resPath);
|
|
172
|
+
}
|
|
173
|
+
// One pass over every text source: mentions and `res://` references.
|
|
174
|
+
const assets = new Set();
|
|
175
|
+
const directoryQueue = [];
|
|
176
|
+
const queuedDirectories = new Set();
|
|
177
|
+
const scanQueue = [];
|
|
178
|
+
const scanned = new Set();
|
|
179
|
+
const addResource = (raw) => {
|
|
180
|
+
const resourcePath = stripRes(raw.trim());
|
|
181
|
+
if (!resourcePath)
|
|
182
|
+
return;
|
|
183
|
+
if (!hasFileExtension(resourcePath)) {
|
|
184
|
+
if (!queuedDirectories.has(resourcePath)) {
|
|
185
|
+
queuedDirectories.add(resourcePath);
|
|
186
|
+
directoryQueue.push(resourcePath);
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (assets.has(resourcePath))
|
|
191
|
+
return;
|
|
192
|
+
assets.add(resourcePath);
|
|
193
|
+
if (SCANNABLE_RESOURCE.test(resourcePath) && !scanned.has(resourcePath)) {
|
|
194
|
+
scanQueue.push(resourcePath);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
const collectFromText = (contents) => {
|
|
198
|
+
collectMentionedNames(contents, mentionedNames);
|
|
199
|
+
for (const match of contents.matchAll(RESOURCE_PATH_PATTERN)) {
|
|
200
|
+
const resourcePath = (match[1] ?? '').trim();
|
|
201
|
+
if (resourcePath.includes('${') || resourcePath.includes('`')) {
|
|
202
|
+
const prefix = staticDirectoryPrefix(resourcePath);
|
|
203
|
+
if (prefix)
|
|
204
|
+
addResource(prefix);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
addResource(resourcePath);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
let textSourceCount = 0;
|
|
212
|
+
for (const wirePath of wirePaths) {
|
|
213
|
+
if (!isTextSource(wirePath))
|
|
214
|
+
continue;
|
|
215
|
+
let contents;
|
|
216
|
+
try {
|
|
217
|
+
contents = await readFile(join(root, ...wirePath.split('/')), 'utf8');
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
warnings.push(`Could not read ${wirePath}; its references were not scanned.`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
textSourceCount++;
|
|
224
|
+
collectFromText(contents);
|
|
225
|
+
if (CODE_EXTENSIONS.has(extensionOf(wirePath))) {
|
|
226
|
+
collectImportedSpecifiers(contents, importedSpecifiers);
|
|
227
|
+
}
|
|
228
|
+
const resPath = wireToRes(wirePath, resRoot);
|
|
229
|
+
if (resPath !== null && SCANNABLE_RESOURCE.test(resPath))
|
|
230
|
+
scanned.add(resPath);
|
|
231
|
+
}
|
|
232
|
+
// Every scene and prefab ships: a game may load any of them at run time.
|
|
233
|
+
const allSceneLike = [...resFiles].filter(path => SCENE_LIKE.test(path)).sort();
|
|
234
|
+
for (const scenePath of allSceneLike)
|
|
235
|
+
addResource(scenePath);
|
|
236
|
+
// Transitive: a resource reached only through a reference (a `.pix3anim` outside the text
|
|
237
|
+
// walk is impossible here — the walk read everything — but a referenced directory can yield
|
|
238
|
+
// scannable files, and those name more).
|
|
239
|
+
const drain = async () => {
|
|
240
|
+
while (scanQueue.length > 0 || directoryQueue.length > 0) {
|
|
241
|
+
const scannable = scanQueue.shift();
|
|
242
|
+
if (scannable !== undefined) {
|
|
243
|
+
if (scanned.has(scannable))
|
|
244
|
+
continue;
|
|
245
|
+
scanned.add(scannable);
|
|
246
|
+
try {
|
|
247
|
+
collectFromText(await readFile(resAbsolute(scannable), 'utf8'));
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// Missing: reported below, when the asset set is verified against the disk.
|
|
251
|
+
}
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const directory = directoryQueue.shift();
|
|
255
|
+
if (directory === undefined)
|
|
256
|
+
continue;
|
|
257
|
+
const prefix = `${directory}/`;
|
|
258
|
+
for (const resPath of resFiles) {
|
|
259
|
+
if (resPath.startsWith(prefix))
|
|
260
|
+
addResource(resPath);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
await drain();
|
|
265
|
+
// Fonts are named only by the manifest, never by a `res://` in a scene.
|
|
266
|
+
for (const face of manifest.fonts)
|
|
267
|
+
addResource(face.path);
|
|
268
|
+
// Locale tables: only declared (or discovered) locales ship, plus their sprites.
|
|
269
|
+
const localization = manifest.localization ?? discoverLocalization(resFiles);
|
|
270
|
+
if (localization) {
|
|
271
|
+
const tables = new Set([localization.defaultLocale, localization.fallbackLocale ?? '', ...localization.locales]
|
|
272
|
+
.filter(locale => locale.length > 0)
|
|
273
|
+
.map(locale => `locales/${locale}.json`));
|
|
274
|
+
for (const table of tables) {
|
|
275
|
+
if (!resFiles.has(table))
|
|
276
|
+
continue;
|
|
277
|
+
addResource(table);
|
|
278
|
+
try {
|
|
279
|
+
const parsed = JSON.parse(await readFile(resAbsolute(table), 'utf8'));
|
|
280
|
+
for (const value of Object.values(parsed.sprites ?? {})) {
|
|
281
|
+
if (typeof value === 'string' && value.trim())
|
|
282
|
+
addResource(value);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
warnings.push(`Failed to scan locale table for sprite references: ${table}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Spine atlas pages live inside the `.atlas` text, invisible to the `res://` scan.
|
|
291
|
+
for (const atlasPath of [...assets].filter(path => /\.atlas$/i.test(path))) {
|
|
292
|
+
try {
|
|
293
|
+
const text = await readFile(resAbsolute(atlasPath), 'utf8');
|
|
294
|
+
for (const page of parseSpineAtlasPageNames(text)) {
|
|
295
|
+
addResource(resolveSpinePagePath(atlasPath, page));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
warnings.push(`Failed to scan Spine atlas for page images: ${atlasPath}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
await drain();
|
|
303
|
+
// Only files that exist ship; a reference to nothing is reported, not embedded as a miss.
|
|
304
|
+
const assetPaths = [];
|
|
305
|
+
for (const resPath of [...assets].sort()) {
|
|
306
|
+
if (resFiles.has(resPath)) {
|
|
307
|
+
assetPaths.push(resPath);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
if ((await stat(resAbsolute(resPath))).isFile()) {
|
|
312
|
+
assetPaths.push(resPath);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
// fall through
|
|
318
|
+
}
|
|
319
|
+
warnings.push(`Referenced resource not found: res://${resPath}`);
|
|
320
|
+
}
|
|
321
|
+
const dependencies = options.dependencies;
|
|
322
|
+
const mentions = (name) => mentionedNames.has(name) || (dependencies?.mentionedNames.has(name) ?? false);
|
|
323
|
+
const imports = (name) => importsPackage(importedSpecifiers, name) ||
|
|
324
|
+
(dependencies ? importsPackage(dependencies.importedSpecifiers, name) : false);
|
|
325
|
+
const scenePaths = allSceneLike.filter(isPlayableScenePath);
|
|
326
|
+
const entryScenePath = resolveEntryScene(scenePaths, manifest, options.entryScene, warnings);
|
|
327
|
+
return {
|
|
328
|
+
scenePaths,
|
|
329
|
+
entryScenePath,
|
|
330
|
+
assetPaths,
|
|
331
|
+
mentionedNames,
|
|
332
|
+
importedSpecifiers,
|
|
333
|
+
usesSpine: mentions('SpineSkeleton2D'),
|
|
334
|
+
// A PostProcess node, or a script / library that imports the package itself: then the
|
|
335
|
+
// runtime's own import shares the one real copy instead of a stub beside it.
|
|
336
|
+
usesPostProcessing: mentions('PostProcess') || imports('postprocessing'),
|
|
337
|
+
usesNetwork: NETWORK_MENTION_NAMES.some(mentions),
|
|
338
|
+
localization,
|
|
339
|
+
netKindPrefabs: assetPaths
|
|
340
|
+
.filter(path => SCENE_LIKE.test(path) && isPrefabPath(path))
|
|
341
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)),
|
|
342
|
+
warnings,
|
|
343
|
+
textSourceCount,
|
|
344
|
+
};
|
|
345
|
+
};
|
|
346
|
+
/** Zero-config localization: `locales/*.json` → `en` if present, else the first (editor rule). */
|
|
347
|
+
export const discoverLocalization = (resFiles) => {
|
|
348
|
+
const ids = [...resFiles]
|
|
349
|
+
.map(path => /^locales\/([^/]+)\.json$/.exec(path)?.[1] ?? null)
|
|
350
|
+
.filter((id) => id !== null)
|
|
351
|
+
.sort();
|
|
352
|
+
if (ids.length === 0)
|
|
353
|
+
return null;
|
|
354
|
+
return { defaultLocale: ids.includes('en') ? 'en' : ids[0], locales: ids };
|
|
355
|
+
};
|
|
356
|
+
/**
|
|
357
|
+
* The scene the player boots into, the same in dev and in a build: `pix3({ entryScene })`, else
|
|
358
|
+
* `pix3project.yaml` `defaultExportScenePath`, else `scenes/main.pix3scene`, else the first scene.
|
|
359
|
+
* A named scene that is not one of `scenePaths` is skipped with a warning.
|
|
360
|
+
*/
|
|
361
|
+
export const resolveEntryScene = (scenePaths, manifest, requested, warnings) => {
|
|
362
|
+
const wanted = requested ? stripRes(requested) : '';
|
|
363
|
+
if (wanted && scenePaths.includes(wanted))
|
|
364
|
+
return wanted;
|
|
365
|
+
if (wanted)
|
|
366
|
+
warnings.push(`Requested entry scene is not a project scene: ${wanted}`);
|
|
367
|
+
const configured = manifest.defaultScenePath ?? '';
|
|
368
|
+
if (configured && scenePaths.includes(configured))
|
|
369
|
+
return configured;
|
|
370
|
+
if (configured)
|
|
371
|
+
warnings.push(`defaultExportScenePath is not a project scene: ${configured}`);
|
|
372
|
+
if (scenePaths.includes('scenes/main.pix3scene'))
|
|
373
|
+
return 'scenes/main.pix3scene';
|
|
374
|
+
return scenePaths[0] ?? '';
|
|
375
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a build may strip unmentioned runtime modules (plan §B.6 item 2, N11). On by default.
|
|
3
|
+
* A dependency that declares `@pix3/runtime` is parsed first (`dependency-imports.ts`): its
|
|
4
|
+
* named imports keep their modules, and an import the build cannot follow (`import * as`, a
|
|
5
|
+
* dynamic import, a parse error) turns strip **off** with a message naming
|
|
6
|
+
* `pix3({ strip: { keep } })` — the owner names the runtime classes the library uses and strip
|
|
7
|
+
* stays on. `strip: false` switches it off for foreign entries; `strip: true` forces it on.
|
|
8
|
+
*/
|
|
9
|
+
export type StripOption = boolean | {
|
|
10
|
+
readonly keep?: readonly string[];
|
|
11
|
+
};
|
|
12
|
+
export interface StripDecision {
|
|
13
|
+
readonly enabled: boolean;
|
|
14
|
+
/** Why it is off (or on against findings), for the build log; null when nothing to say. */
|
|
15
|
+
readonly reason: string | null;
|
|
16
|
+
/** Names kept by `pix3({ strip: { keep } })`. */
|
|
17
|
+
readonly keep: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
/** The fix a message names when a dependency's imports cannot be followed. */
|
|
20
|
+
export declare const KEEP_HINT = "pix3({ strip: { keep: ['<RuntimeClass>', \u2026] } })";
|
|
21
|
+
/** Project dependencies that themselves depend on `@pix3/runtime`. */
|
|
22
|
+
export declare const runtimeDependents: (root: string) => string[];
|
|
23
|
+
export interface StripFindings {
|
|
24
|
+
/** Imports of `@pix3/runtime` the build could not follow (from `scanDependencyImports`). */
|
|
25
|
+
readonly opaque: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
export declare const decideStrip: (option: StripOption | undefined, findings?: StripFindings) => StripDecision;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { findPackageDir } from "../dev-info.js";
|
|
4
|
+
const OWN_PACKAGES = new Set([
|
|
5
|
+
'@pix3/runtime',
|
|
6
|
+
'@pix3/vite-plugin',
|
|
7
|
+
'@pix3/editor-core',
|
|
8
|
+
'@pix3/cli',
|
|
9
|
+
]);
|
|
10
|
+
/** The fix a message names when a dependency's imports cannot be followed. */
|
|
11
|
+
export const KEEP_HINT = "pix3({ strip: { keep: ['<RuntimeClass>', …] } })";
|
|
12
|
+
const readPackage = (dir) => {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
15
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const namesOf = (pkg, ...sections) => {
|
|
22
|
+
const out = [];
|
|
23
|
+
for (const section of sections) {
|
|
24
|
+
const value = pkg[section];
|
|
25
|
+
if (value && typeof value === 'object')
|
|
26
|
+
out.push(...Object.keys(value));
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
};
|
|
30
|
+
/** Project dependencies that themselves depend on `@pix3/runtime`. */
|
|
31
|
+
export const runtimeDependents = (root) => {
|
|
32
|
+
const pkg = readPackage(root);
|
|
33
|
+
if (!pkg)
|
|
34
|
+
return [];
|
|
35
|
+
const dependents = [];
|
|
36
|
+
const names = new Set(namesOf(pkg, 'dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'));
|
|
37
|
+
for (const name of names) {
|
|
38
|
+
if (OWN_PACKAGES.has(name))
|
|
39
|
+
continue;
|
|
40
|
+
const dir = findPackageDir(root, name);
|
|
41
|
+
const dependency = dir ? readPackage(dir) : null;
|
|
42
|
+
if (!dependency)
|
|
43
|
+
continue;
|
|
44
|
+
if (namesOf(dependency, 'dependencies', 'peerDependencies').includes('@pix3/runtime')) {
|
|
45
|
+
dependents.push(name);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return dependents.sort();
|
|
49
|
+
};
|
|
50
|
+
export const decideStrip = (option, findings = { opaque: [] }) => {
|
|
51
|
+
if (option === false)
|
|
52
|
+
return { enabled: false, reason: 'pix3({ strip: false })', keep: [] };
|
|
53
|
+
if (option === true) {
|
|
54
|
+
return { enabled: true, reason: 'forced by pix3({ strip: true })', keep: [] };
|
|
55
|
+
}
|
|
56
|
+
const keep = option && typeof option === 'object' ? [...(option.keep ?? [])] : [];
|
|
57
|
+
const opaque = findings.opaque;
|
|
58
|
+
if (opaque.length === 0) {
|
|
59
|
+
return {
|
|
60
|
+
enabled: true,
|
|
61
|
+
reason: keep.length > 0 ? `keeping ${keep.join(', ')} by pix3({ strip: { keep } })` : null,
|
|
62
|
+
keep,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const first = opaque[0] + (opaque.length > 1 ? ` (+${opaque.length - 1} more)` : '');
|
|
66
|
+
if (option && typeof option === 'object') {
|
|
67
|
+
return {
|
|
68
|
+
enabled: true,
|
|
69
|
+
reason: `${first} — the build cannot see which runtime modules it reaches; stripping anyway ` +
|
|
70
|
+
`because pix3({ strip: { keep } }) names what it uses` +
|
|
71
|
+
(keep.length > 0 ? ` (${keep.join(', ')})` : ' (nothing listed!)'),
|
|
72
|
+
keep,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
enabled: false,
|
|
77
|
+
reason: `${first} — the build cannot see which runtime modules it reaches, so nothing is ` +
|
|
78
|
+
`stripped. Name the runtime classes it uses with ${KEEP_HINT} to strip the rest, ` +
|
|
79
|
+
`or pass strip: true to strip regardless`,
|
|
80
|
+
keep,
|
|
81
|
+
};
|
|
82
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime modules a playable build may replace with a throwing stub when nothing in the project
|
|
3
|
+
* mentions them (plan §B.6 item 2; port of the 1.x `strippable-runtime-modules.ts`).
|
|
4
|
+
*
|
|
5
|
+
* Why a stub and not a runtime refactor: the plugin already owns the `load` hook, so "leave this
|
|
6
|
+
* out" costs one stub and changes no `@pix3/runtime` API — which matters because the package is
|
|
7
|
+
* consumed by external projects. See `../pix3/.plans/done/playable-export-size.md` §2 Р3/Р4.
|
|
8
|
+
*
|
|
9
|
+
* The table is only safe because of two invariants, both enforced by
|
|
10
|
+
* `strippable-runtime-modules.spec.ts` against the real import graph of `packages/runtime/src`:
|
|
11
|
+
*
|
|
12
|
+
* 1. Every **value** importer of a listed module — and every module that reaches it through the
|
|
13
|
+
* lazy getter that constructs it (`SceneService.physics2d`, `JuiceApi.trail`) — is either
|
|
14
|
+
* itself listed ({@link StrippableRuntimeModule.importers}), neutralised for this purpose
|
|
15
|
+
* ({@link NEUTRALISED_IMPORTERS}), or explicitly excused as a lazy construction site
|
|
16
|
+
* ({@link StrippableRuntimeModule.lazyValueImporters}); and that getter's name is a keep-name.
|
|
17
|
+
* 2. Behaviour ids match `behaviors/register-behaviors.ts`.
|
|
18
|
+
*
|
|
19
|
+
* Besides its keep-names, a module is kept by any mention of one of its **value exports** (the
|
|
20
|
+
* build reads them from the real source: {@link valueExportNames}) — the barrel re-exports every
|
|
21
|
+
* one, so `import { pointInPolygon } from '@pix3/runtime'` must keep `collision-shapes-2d` rather
|
|
22
|
+
* than hand the game a stub that throws (or a constant that reads `undefined`).
|
|
23
|
+
*
|
|
24
|
+
* If the runtime grows a new importer of a listed module, the spec fails and the entry has to be
|
|
25
|
+
* re-justified instead of quietly shipping a broken build.
|
|
26
|
+
*/
|
|
27
|
+
export interface StrippableRuntimeModule {
|
|
28
|
+
/** Path under `packages/runtime/src`, without extension. */
|
|
29
|
+
readonly modulePath: string;
|
|
30
|
+
/**
|
|
31
|
+
* Names that keep the module in the bundle when the project mentions any of them — the class
|
|
32
|
+
* name plus, for script components, the registry id as it appears in scene YAML
|
|
33
|
+
* (`type: core:Follow`).
|
|
34
|
+
*/
|
|
35
|
+
readonly keepWhenMentioned: readonly string[];
|
|
36
|
+
/**
|
|
37
|
+
* Entries of this table that keep this module alive: value importers, and modules that reach
|
|
38
|
+
* it through a lazy getter of a module that is always kept (`this.scene?.physics2d` in a
|
|
39
|
+
* behaviour that imports only the service's types). Such a module can only be stripped once all
|
|
40
|
+
* of them are — the build runs that fixpoint.
|
|
41
|
+
*/
|
|
42
|
+
readonly importers?: readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* Value importers that are NOT strippable, excused because they only construct the module
|
|
45
|
+
* lazily on a path unreachable for a project that never mentions it. Each one needs a comment
|
|
46
|
+
* saying why.
|
|
47
|
+
*/
|
|
48
|
+
readonly lazyValueImporters?: readonly string[];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Importers whose references do not pin anything, because they cannot survive in a player bundle
|
|
52
|
+
* on their own:
|
|
53
|
+
*
|
|
54
|
+
* - `core/SceneLoader` — the `switch` over every node type; a case whose type never appears in a
|
|
55
|
+
* shipped scene is dead code that only constructs the stub.
|
|
56
|
+
* - `index` — the package barrel; the bundler drops re-exports nobody imports.
|
|
57
|
+
* - `behaviors/register-behaviors` — registers classes by id into a `Map`; a stub registers fine
|
|
58
|
+
* and `ScriptRegistry.createComponent` reports the throw.
|
|
59
|
+
* - `core/SceneSaver` — never constructed by a player (see `SceneManager`'s optional saver), so it
|
|
60
|
+
* tree-shakes out with everything it imports.
|
|
61
|
+
*
|
|
62
|
+
* @internal The import-graph guard (`strippable-runtime-modules.spec.ts`) reads it.
|
|
63
|
+
*/
|
|
64
|
+
export declare const NEUTRALISED_IMPORTERS: readonly ["core/SceneLoader", "index", "behaviors/register-behaviors", "core/SceneSaver"];
|
|
65
|
+
export declare const STRIPPABLE_RUNTIME_MODULES: readonly StrippableRuntimeModule[];
|
|
66
|
+
/**
|
|
67
|
+
* The subset of {@link STRIPPABLE_RUNTIME_MODULES} that can leave a build in which
|
|
68
|
+
* `mentions(name)` is false for every name the entry (and, transitively, every entry that imports
|
|
69
|
+
* it) is kept by.
|
|
70
|
+
*/
|
|
71
|
+
export declare const resolveStrippableRuntimeModules: (mentions: (name: string) => boolean,
|
|
72
|
+
/** The module's value exports (from its real source); each one keeps it like a keep-name. */
|
|
73
|
+
exportsOf?: (modulePath: string) => readonly string[]) => readonly StrippableRuntimeModule[];
|
|
74
|
+
/** Every value export name of a module's source (the names that keep it besides the table's). */
|
|
75
|
+
export declare const valueExportNames: (moduleSource: string) => string[];
|
|
76
|
+
/**
|
|
77
|
+
* A drop-in replacement for a runtime module's source: same value exports, but every class throws
|
|
78
|
+
* if anything ever constructs it. Derived from the real source rather than hardcoded, so a renamed
|
|
79
|
+
* export cannot silently drift — and if a name is missed, the bundler fails the build with "no
|
|
80
|
+
* matching export" instead of shipping something broken.
|
|
81
|
+
*
|
|
82
|
+
* Types and interfaces need no stubs: the bundler erases type-only usages, so the importing module
|
|
83
|
+
* no longer references those names at all.
|
|
84
|
+
*/
|
|
85
|
+
export declare const buildStrippedModuleSource: (moduleSource: string, modulePath: string) => string;
|