@pix3/vite-plugin 0.0.0-stage → 2.0.0-alpha.0

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.
Files changed (85) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +38 -3
  3. package/dist/build/bundle-hooks.d.ts +23 -0
  4. package/dist/build/bundle-hooks.js +79 -0
  5. package/dist/build/classic-script.d.ts +11 -0
  6. package/dist/build/classic-script.js +46 -0
  7. package/dist/build/compress.d.ts +29 -0
  8. package/dist/build/compress.js +63 -0
  9. package/dist/build/dependency-imports.d.ts +53 -0
  10. package/dist/build/dependency-imports.js +239 -0
  11. package/dist/build/dev-manifest.d.ts +10 -0
  12. package/dist/build/dev-manifest.js +25 -0
  13. package/dist/build/editor-flush.d.ts +42 -0
  14. package/dist/build/editor-flush.js +91 -0
  15. package/dist/build/finish.d.ts +11 -0
  16. package/dist/build/finish.js +126 -0
  17. package/dist/build/output-plugins.d.ts +26 -0
  18. package/dist/build/output-plugins.js +88 -0
  19. package/dist/build/plan.d.ts +75 -0
  20. package/dist/build/plan.js +162 -0
  21. package/dist/build/player-modules.d.ts +99 -0
  22. package/dist/build/player-modules.js +204 -0
  23. package/dist/build/project-manifest.d.ts +57 -0
  24. package/dist/build/project-manifest.js +169 -0
  25. package/dist/build/record.d.ts +25 -0
  26. package/dist/build/record.js +14 -0
  27. package/dist/build/report.d.ts +114 -0
  28. package/dist/build/report.js +102 -0
  29. package/dist/build/scan.d.ts +84 -0
  30. package/dist/build/scan.js +375 -0
  31. package/dist/build/strip-decision.d.ts +27 -0
  32. package/dist/build/strip-decision.js +82 -0
  33. package/dist/build/strippable-runtime-modules.d.ts +85 -0
  34. package/dist/build/strippable-runtime-modules.js +309 -0
  35. package/dist/build/zip.d.ts +6 -0
  36. package/dist/build/zip.js +28 -0
  37. package/dist/client/host-files.d.ts +96 -0
  38. package/dist/client/host-files.js +169 -0
  39. package/dist/client/index.d.ts +218 -0
  40. package/dist/client/index.js +381 -0
  41. package/dist/dev-info.d.ts +55 -0
  42. package/dist/dev-info.js +116 -0
  43. package/dist/dev-server.d.ts +28 -0
  44. package/dist/dev-server.js +139 -0
  45. package/dist/editor-page.d.ts +31 -0
  46. package/dist/editor-page.js +74 -0
  47. package/dist/files/changeset.d.ts +84 -0
  48. package/dist/files/changeset.js +321 -0
  49. package/dist/files/content-type.d.ts +1 -0
  50. package/dist/files/content-type.js +56 -0
  51. package/dist/files/history.d.ts +67 -0
  52. package/dist/files/history.js +162 -0
  53. package/dist/files/paths.d.ts +41 -0
  54. package/dist/files/paths.js +144 -0
  55. package/dist/files/project-files.d.ts +208 -0
  56. package/dist/files/project-files.js +1062 -0
  57. package/dist/files/scan.d.ts +71 -0
  58. package/dist/files/scan.js +252 -0
  59. package/dist/index.d.ts +56 -0
  60. package/dist/index.js +278 -0
  61. package/dist/module-paths.d.ts +13 -0
  62. package/dist/module-paths.js +32 -0
  63. package/dist/player/index.d.ts +31 -0
  64. package/dist/player/index.js +102 -0
  65. package/dist/server/editor-socket.d.ts +53 -0
  66. package/dist/server/editor-socket.js +134 -0
  67. package/dist/server/guard.d.ts +61 -0
  68. package/dist/server/guard.js +165 -0
  69. package/dist/server/http.d.ts +24 -0
  70. package/dist/server/http.js +70 -0
  71. package/dist/server/image-keys.d.ts +46 -0
  72. package/dist/server/image-keys.js +118 -0
  73. package/dist/server/image-proxy.d.ts +32 -0
  74. package/dist/server/image-proxy.js +124 -0
  75. package/dist/server/router.d.ts +54 -0
  76. package/dist/server/router.js +147 -0
  77. package/dist/sync/barrier.d.ts +69 -0
  78. package/dist/sync/barrier.js +240 -0
  79. package/dist/sync/hot-update.d.ts +16 -0
  80. package/dist/sync/hot-update.js +58 -0
  81. package/dist/sync/script-graph.d.ts +79 -0
  82. package/dist/sync/script-graph.js +214 -0
  83. package/dist/vite-config.d.ts +12 -0
  84. package/dist/vite-config.js +62 -0
  85. package/package.json +60 -6
@@ -0,0 +1,75 @@
1
+ import type { CompressedHtml } from './compress.ts';
2
+ import { type EmbeddedAssetsResult } from './player-modules.ts';
3
+ import { type ProjectManifestInfo } from './project-manifest.ts';
4
+ import type { ModuleClassifier, ModuleSize } from './report.ts';
5
+ import { type ProjectScan } from './scan.ts';
6
+ import { type StripOption } from './strip-decision.ts';
7
+ /** What `buildStart` learned about the project for this `vite build`, plus what the bundle told. */
8
+ export interface BuildState {
9
+ readonly format: 'html' | 'zip';
10
+ readonly manifest: ProjectManifestInfo;
11
+ readonly scan: ProjectScan;
12
+ /** Module paths (under the runtime's `src/`) replaced by stubs. */
13
+ readonly stripped: ReadonlySet<string>;
14
+ /** `name a dependency may import → the stripped module it keeps` (the transform net). */
15
+ readonly stubbedNames: ReadonlyMap<string, string>;
16
+ readonly stripReason: string | null;
17
+ readonly stripEnabled: boolean;
18
+ /** Whether `pix3({ strip })` was set at all (the net errors only when it was not). */
19
+ readonly stripConfigured: boolean;
20
+ readonly keep: readonly string[];
21
+ /** N11: what the dependencies that declare the runtime import from it, and how many files. */
22
+ readonly dependencyImports: ReadonlySet<string>;
23
+ readonly dependencyParsed: Record<string, number>;
24
+ /** Real path of the runtime's `src/`, or null when it did not resolve. */
25
+ readonly runtimeSrc: string | null;
26
+ /** How the report and the net name a module id. */
27
+ readonly classifier: ModuleClassifier;
28
+ readonly spineInstalled: boolean;
29
+ readonly postprocessingInstalled: boolean;
30
+ /** Scenes ship as JSON and the runtime's `yaml` is `JSON.parse`. */
31
+ readonly stubYaml: boolean;
32
+ /** No model in the project: `GLTFLoader` is a stub. */
33
+ readonly stubGltf: boolean;
34
+ /**
35
+ * Filled by the bundle hooks, for the report: what the hooks actually replaced (a runtime that
36
+ * resolves under another path than the one `buildStart` saw would leave these empty).
37
+ */
38
+ readonly applied: {
39
+ readonly modules: Set<string>;
40
+ yaml: boolean;
41
+ gltf: boolean;
42
+ postprocessing: boolean;
43
+ };
44
+ readonly moduleSizes: ModuleSize[];
45
+ bundleBytes: number;
46
+ assets: EmbeddedAssetsResult | null;
47
+ compressed: CompressedHtml | null;
48
+ }
49
+ export interface PlanBuildOptions {
50
+ readonly root: string;
51
+ readonly format: 'html' | 'zip';
52
+ readonly resRoot: string;
53
+ readonly entryScene: string | null;
54
+ readonly strip: StripOption | undefined;
55
+ readonly compress: boolean;
56
+ /** This package's directory (the player's modules are grouped as `player`). */
57
+ readonly packageDir: string;
58
+ /** The bundler's resolver from the project's `index.html`: the resolved id, or null. */
59
+ readonly resolve: (specifier: string) => Promise<string | null>;
60
+ /** The bundler's `this.parse`. */
61
+ readonly parse: (code: string) => unknown;
62
+ readonly log: (line: string) => void;
63
+ readonly warn: (line: string) => void;
64
+ }
65
+ /** Step 0: flush the editor that `.pix3/dev.json` points at; an unsynced editor fails the build. */
66
+ export declare const flushEditor: (root: string, log: (line: string) => void, fail: (message: string) => never) => Promise<void>;
67
+ /**
68
+ * Plan §B.6 items 1–2: one scan of every text source (the project's and, N11, every dependency
69
+ * that declares the runtime) decides what ships and what the stubs replace.
70
+ */
71
+ export declare const planBuild: (options: PlanBuildOptions) => Promise<BuildState>;
72
+ /** The module path under the runtime's `src/` of a `.ts` module id (`core/Foo`), or null. */
73
+ export declare const runtimeModulePath: (build: BuildState | null, id: string) => string | null;
74
+ /** The bytes an asset ships as: a scene or prefab as JSON when the runtime's `yaml` is a stub. */
75
+ export declare const shippedBytes: (build: BuildState, resPath: string, bytes: Buffer, warn: (line: string) => void) => Buffer;
@@ -0,0 +1,162 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileOf, realFileOf, relativeInside } from "../module-paths.js";
4
+ import { RUNTIME_SPECIFIER, scanDependencyImports } from "./dependency-imports.js";
5
+ import { EditorUnsyncedError, flushEditorBeforeBuild } from "./editor-flush.js";
6
+ import { EMBEDDED_ASSETS_ID, SCENE_LIKE_ASSET, sceneAsJson, } from "./player-modules.js";
7
+ import { readProjectManifest } from "./project-manifest.js";
8
+ import { scanProject } from "./scan.js";
9
+ import { decideStrip, runtimeDependents } from "./strip-decision.js";
10
+ import { resolveStrippableRuntimeModules, valueExportNames } from "./strippable-runtime-modules.js";
11
+ /** A `.glb`/`.gltf` (any case) or the loader by name. */
12
+ const MODEL_MENTION = /^(?:glb|gltf|GLTFLoader)$/i;
13
+ /** Step 0: flush the editor that `.pix3/dev.json` points at; an unsynced editor fails the build. */
14
+ export const flushEditor = async (root, log, fail) => {
15
+ try {
16
+ const flushed = await flushEditorBeforeBuild(root);
17
+ if (flushed.status === 'flushed')
18
+ log(`editor at ${flushed.url} flushed its unsaved scenes`);
19
+ else if (flushed.status === 'no-editor')
20
+ log(`dev server at ${flushed.url}: no editor tab open`);
21
+ else if (flushed.status === 'no-server' && flushed.url)
22
+ log(`no dev server at ${flushed.url} (${flushed.reason}): .pix3/dev.json is stale`);
23
+ }
24
+ catch (error) {
25
+ if (error instanceof EditorUnsyncedError)
26
+ fail(`${error.code}: ${error.message}`);
27
+ throw error;
28
+ }
29
+ };
30
+ /**
31
+ * Plan §B.6 items 1–2: one scan of every text source (the project's and, N11, every dependency
32
+ * that declares the runtime) decides what ships and what the stubs replace.
33
+ */
34
+ export const planBuild = async (options) => {
35
+ const { root, format, log, warn } = options;
36
+ const manifest = readProjectManifest(root);
37
+ const runtime = await options.resolve(RUNTIME_SPECIFIER);
38
+ const runtimeSrc = runtime ? dirname(realFileOf(runtime)) : null;
39
+ if (!runtimeSrc)
40
+ warn('@pix3/runtime did not resolve from the project; nothing is stripped');
41
+ const spineInstalled = (await options.resolve('@esotericsoftware/spine-threejs')) !== null;
42
+ const postprocessingInstalled = (await options.resolve('postprocessing')) !== null;
43
+ const dependencyScan = await scanDependencyImports({
44
+ root,
45
+ packages: options.strip === false ? [] : runtimeDependents(root),
46
+ parse: options.parse,
47
+ });
48
+ const scan = await scanProject({
49
+ root,
50
+ resRoot: options.resRoot,
51
+ manifest,
52
+ entryScene: options.entryScene,
53
+ dependencies: dependencyScan,
54
+ });
55
+ for (const warning of scan.warnings)
56
+ warn(warning);
57
+ if (!scan.entryScenePath)
58
+ warn('no .pix3scene found: the player will have nothing to boot');
59
+ if (scan.usesSpine && !spineInstalled)
60
+ warn('a scene places a SpineSkeleton2D but @esotericsoftware/spine-threejs is not installed');
61
+ const decision = decideStrip(options.strip, dependencyScan);
62
+ const nameSets = [
63
+ scan.mentionedNames,
64
+ dependencyScan.names,
65
+ dependencyScan.mentionedNames,
66
+ new Set(decision.keep),
67
+ ];
68
+ const mentions = (name) => nameSets.some(names => names.has(name));
69
+ const stripped = new Set();
70
+ const stubbedNames = new Map();
71
+ if (decision.enabled && runtimeSrc) {
72
+ // Every value export keeps its module too (the barrel re-exports them all): a mention of
73
+ // `pointInPolygon` keeps `collision-shapes-2d`, and the net knows every stubbed name.
74
+ const exportsOf = new Map();
75
+ const readExports = (modulePath) => {
76
+ let names = exportsOf.get(modulePath);
77
+ if (!names) {
78
+ try {
79
+ names = valueExportNames(readFileSync(join(runtimeSrc, `${modulePath}.ts`), 'utf8'));
80
+ }
81
+ catch {
82
+ names = [];
83
+ }
84
+ exportsOf.set(modulePath, names);
85
+ }
86
+ return names;
87
+ };
88
+ for (const entry of resolveStrippableRuntimeModules(mentions, readExports)) {
89
+ stripped.add(entry.modulePath);
90
+ for (const name of [...entry.keepWhenMentioned, ...readExports(entry.modulePath)])
91
+ stubbedNames.set(name, entry.modulePath);
92
+ }
93
+ }
94
+ // Case-insensitive, like the asset test: `crate.GLB` names a model as well.
95
+ const stubGltf = decision.enabled &&
96
+ runtimeSrc !== null &&
97
+ !nameSets.some(names => [...names].some(name => MODEL_MENTION.test(name))) &&
98
+ !scan.assetPaths.some(path => /\.(glb|gltf)$/i.test(path));
99
+ for (const [name, files] of Object.entries(dependencyScan.parsed)) {
100
+ log(`${name} depends on ${RUNTIME_SPECIFIER}: ${files} file(s) parsed` +
101
+ (dependencyScan.names.size > 0
102
+ ? `, keeping ${[...dependencyScan.names].sort().join(', ')}`
103
+ : ''));
104
+ }
105
+ log(`build ${format}${options.compress && format === 'html' ? ' (compressed)' : ''}: ` +
106
+ `entry ${scan.entryScenePath || '(none)'}, ${scan.scenePaths.length} scene(s), ` +
107
+ `${scan.assetPaths.length} asset(s), ${scan.textSourceCount} text source(s) scanned; ` +
108
+ (decision.enabled
109
+ ? `strip on (${stripped.size} module(s)${stubGltf ? ', GLTFLoader' : ''}, scenes as JSON)` +
110
+ (decision.reason ? ` — ${decision.reason}` : '')
111
+ : `strip off — ${decision.reason}`));
112
+ if (options.compress && format === 'zip')
113
+ warn('compress applies to build: "html" only; the zip is written as is');
114
+ return {
115
+ format,
116
+ manifest,
117
+ scan,
118
+ stripped,
119
+ stubbedNames,
120
+ stripReason: decision.reason,
121
+ stripEnabled: decision.enabled,
122
+ stripConfigured: options.strip !== undefined,
123
+ keep: decision.keep,
124
+ dependencyImports: dependencyScan.names,
125
+ dependencyParsed: dependencyScan.parsed,
126
+ runtimeSrc,
127
+ classifier: {
128
+ root,
129
+ runtimeSrc,
130
+ playerDir: options.packageDir,
131
+ embeddedAssetsId: EMBEDDED_ASSETS_ID,
132
+ },
133
+ spineInstalled,
134
+ postprocessingInstalled,
135
+ stubYaml: decision.enabled,
136
+ stubGltf,
137
+ applied: { modules: new Set(), yaml: false, gltf: false, postprocessing: false },
138
+ moduleSizes: [],
139
+ bundleBytes: 0,
140
+ assets: null,
141
+ compressed: null,
142
+ };
143
+ };
144
+ /** The module path under the runtime's `src/` of a `.ts` module id (`core/Foo`), or null. */
145
+ export const runtimeModulePath = (build, id) => {
146
+ if (!build?.runtimeSrc || !fileOf(id).endsWith('.ts'))
147
+ return null;
148
+ return relativeInside(build.runtimeSrc, realFileOf(id))?.replace(/\.ts$/, '') ?? null;
149
+ };
150
+ /** The bytes an asset ships as: a scene or prefab as JSON when the runtime's `yaml` is a stub. */
151
+ export const shippedBytes = (build, resPath, bytes, warn) => {
152
+ if (!build.stubYaml || !SCENE_LIKE_ASSET.test(resPath))
153
+ return bytes;
154
+ try {
155
+ return Buffer.from(sceneAsJson(bytes.toString('utf8')), 'utf8');
156
+ }
157
+ catch (error) {
158
+ // Not YAML the build can read: ship it as is; the runtime reports it when it loads.
159
+ warn(`${resPath} did not parse as YAML (${String(error)}); shipped as written`);
160
+ return bytes;
161
+ }
162
+ };
@@ -0,0 +1,99 @@
1
+ import { type LocalizationSettings, type ProjectManifestInfo } from './project-manifest.ts';
2
+ /**
3
+ * The `virtual:pix3/*` modules the player (`@pix3/vite-plugin/player`) imports (plan §B.5). Each
4
+ * is generated here from the project, in dev from `pix3project.yaml` and the scene list, in a
5
+ * build from the scan (`scan.ts`). The seams for the optional libraries are **static** imports
6
+ * inside the generated module — a dynamic import would become a chunk a single-file HTML can never
7
+ * fetch, and a bare specifier left unaliased is silently externalised (the 1.x `postprocessing`
8
+ * bug, CLAUDE.md «Playable export size»).
9
+ */
10
+ export declare const SCENE_MANIFEST_ID = "virtual:pix3/scene-manifest";
11
+ export declare const EMBEDDED_ASSETS_ID = "virtual:pix3/embedded-assets";
12
+ export declare const PROJECT_SCRIPTS_ID = "virtual:pix3/project-scripts";
13
+ export declare const SPINE_ID = "virtual:pix3/spine";
14
+ export declare const POSTPROCESSING_ID = "virtual:pix3/postprocessing";
15
+ export declare const NETWORK_ID = "virtual:pix3/network";
16
+ /** What `postprocessing` resolves to in a single-file build whose scenes place no `PostProcess`. */
17
+ export declare const POSTPROCESSING_STUB_ID = "virtual:pix3/postprocessing-stub";
18
+ /** What `yaml` resolves to for the runtime when the scenes ship as JSON. */
19
+ export declare const YAML_STUB_ID = "virtual:pix3/yaml-stub";
20
+ /** What `three/examples/jsm/loaders/GLTFLoader.js` resolves to when no model is shipped. */
21
+ export declare const GLTF_LOADER_STUB_ID = "virtual:pix3/gltf-loader-stub";
22
+ export declare const PLAYER_VIRTUAL_IDS: readonly string[];
23
+ export interface SceneManifestModel {
24
+ readonly scenePaths: readonly string[];
25
+ readonly entryScenePath: string;
26
+ readonly manifest: ProjectManifestInfo;
27
+ readonly localization: LocalizationSettings | null;
28
+ readonly netKindPrefabs: readonly string[];
29
+ /** `ResourceManager` base: `/<resRoot>/` in dev, `./` for a build. */
30
+ readonly resourceBase: string;
31
+ }
32
+ export declare const sceneManifestSource: (model: SceneManifestModel) => string;
33
+ /** The eager glob root of the project's scripts — the player's `register-project-scripts`. */
34
+ export declare const projectScriptsSource: () => string;
35
+ export interface EmbeddedAssetsSourceOptions {
36
+ readonly root: string;
37
+ readonly resRoot: string;
38
+ /** `res://`-relative paths to embed. */
39
+ readonly assetPaths: readonly string[];
40
+ /** The bytes that ship for a path (a scene as JSON, say); the file's own bytes by default. */
41
+ readonly shippedBytes?: (resPath: string, bytes: Buffer) => Buffer;
42
+ }
43
+ export interface EmbeddedAssetEntry {
44
+ readonly path: string;
45
+ readonly rawBytes: number;
46
+ readonly base64Bytes: number;
47
+ }
48
+ export interface EmbeddedAssetsResult {
49
+ readonly source: string;
50
+ readonly rawBytes: number;
51
+ readonly base64Bytes: number;
52
+ readonly count: number;
53
+ /** Largest first. */
54
+ readonly entries: readonly EmbeddedAssetEntry[];
55
+ }
56
+ /** Asset entries largest first, then by path. */
57
+ export declare const largestFirst: (a: EmbeddedAssetEntry, b: EmbeddedAssetEntry) => number;
58
+ /** `{ [resPath]: { base64, mimeType } }` of every shipped asset (single-file build). */
59
+ export declare const embeddedAssetsSource: (options: EmbeddedAssetsSourceOptions) => Promise<EmbeddedAssetsResult>;
60
+ /** Scene-shaped resources (`.pix3scene`, `.prefab`): the ones the runtime parses as YAML. */
61
+ export declare const SCENE_LIKE_ASSET: RegExp;
62
+ /**
63
+ * A scene's bytes for a build that stubs `yaml` ({@link yamlStubSource}): the same document as
64
+ * JSON. JSON is YAML, so a project script that reads the text with its own `yaml` import still
65
+ * parses it; the runtime's `parse` becomes `JSON.parse`. The 1.x export did the same.
66
+ */
67
+ export declare const sceneAsJson: (text: string) => string;
68
+ /**
69
+ * What `yaml` resolves to **for the runtime's own importers** (`SceneLoader`, `SceneSaver`) when
70
+ * every scene ships as JSON: `parse` is `JSON.parse`, `stringify` throws (a player never writes a
71
+ * scene — `SceneSaver` is not even constructed, so this tree-shakes out). A project module that
72
+ * imports `yaml` itself keeps the real parser; this stub is decided per importer.
73
+ */
74
+ export declare const yamlStubSource: () => string;
75
+ /** The specifier `AssetLoader` imports the loader by. */
76
+ export declare const GLTF_LOADER_SPECIFIER = "three/examples/jsm/loaders/GLTFLoader.js";
77
+ /**
78
+ * `GLTFLoader` (three addons, ~100 KiB rendered) is value-imported by `AssetLoader` for every
79
+ * build; a project with no `.glb`/`.gltf` asset and no script naming `GLTFLoader` gets a loader
80
+ * that fails the way a missing file would (the 1.x `gltf-loader-stub`).
81
+ */
82
+ export declare const gltfLoaderStubSource: () => string;
83
+ export declare const noEmbeddedAssetsSource: () => string;
84
+ /**
85
+ * `virtual:pix3/spine`. Dev: the lazy loader (`virtual:pix3/spine-loader` decides whether the
86
+ * package is installed). Build: a static import when a scene places a `SpineSkeleton2D` and the
87
+ * package is installed, nothing otherwise.
88
+ */
89
+ export declare const spineSource: (mode: "dev" | "static" | "none") => string;
90
+ /**
91
+ * `virtual:pix3/postprocessing`. The runtime's default is `import('postprocessing')`, which is
92
+ * right in dev and in a zip (a chunk can be fetched). A single-file build registers the module
93
+ * statically when a scene places a `PostProcess`; otherwise `postprocessing` itself resolves to
94
+ * {@link postprocessingStubSource}, so the inlined dynamic import costs nothing.
95
+ */
96
+ export declare const postprocessingSource: (mode: "default" | "static") => string;
97
+ export declare const postprocessingStubSource: () => string;
98
+ /** `virtual:pix3/network`: the real installer, or a no-op that keeps `net/**` out of the bundle. */
99
+ export declare const networkSource: (usesNetwork: boolean) => string;
@@ -0,0 +1,204 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { parse as parseYaml } from 'yaml';
3
+ import { contentTypeFor } from "../files/content-type.js";
4
+ import { globPatterns, SCRIPT_DIRS } from "../sync/script-graph.js";
5
+ import { resToAbsolute, } from "./project-manifest.js";
6
+ /**
7
+ * The `virtual:pix3/*` modules the player (`@pix3/vite-plugin/player`) imports (plan §B.5). Each
8
+ * is generated here from the project, in dev from `pix3project.yaml` and the scene list, in a
9
+ * build from the scan (`scan.ts`). The seams for the optional libraries are **static** imports
10
+ * inside the generated module — a dynamic import would become a chunk a single-file HTML can never
11
+ * fetch, and a bare specifier left unaliased is silently externalised (the 1.x `postprocessing`
12
+ * bug, CLAUDE.md «Playable export size»).
13
+ */
14
+ export const SCENE_MANIFEST_ID = 'virtual:pix3/scene-manifest';
15
+ export const EMBEDDED_ASSETS_ID = 'virtual:pix3/embedded-assets';
16
+ export const PROJECT_SCRIPTS_ID = 'virtual:pix3/project-scripts';
17
+ export const SPINE_ID = 'virtual:pix3/spine';
18
+ export const POSTPROCESSING_ID = 'virtual:pix3/postprocessing';
19
+ export const NETWORK_ID = 'virtual:pix3/network';
20
+ /** What `postprocessing` resolves to in a single-file build whose scenes place no `PostProcess`. */
21
+ export const POSTPROCESSING_STUB_ID = 'virtual:pix3/postprocessing-stub';
22
+ /** What `yaml` resolves to for the runtime when the scenes ship as JSON. */
23
+ export const YAML_STUB_ID = 'virtual:pix3/yaml-stub';
24
+ /** What `three/examples/jsm/loaders/GLTFLoader.js` resolves to when no model is shipped. */
25
+ export const GLTF_LOADER_STUB_ID = 'virtual:pix3/gltf-loader-stub';
26
+ export const PLAYER_VIRTUAL_IDS = [
27
+ SCENE_MANIFEST_ID,
28
+ EMBEDDED_ASSETS_ID,
29
+ PROJECT_SCRIPTS_ID,
30
+ SPINE_ID,
31
+ POSTPROCESSING_ID,
32
+ NETWORK_ID,
33
+ POSTPROCESSING_STUB_ID,
34
+ YAML_STUB_ID,
35
+ GLTF_LOADER_STUB_ID,
36
+ ];
37
+ const json = (value) => JSON.stringify(value, null, 2);
38
+ export const sceneManifestSource = (model) => [
39
+ `export const scenePaths = ${json(model.scenePaths)};`,
40
+ `export const activeScenePath = ${json(model.entryScenePath)};`,
41
+ `export const resourceBase = ${json(model.resourceBase)};`,
42
+ `export const projectName = ${json(model.manifest.projectName)};`,
43
+ `export const runtimeViewportBaseSize = ${json(model.manifest.viewportBaseSize)};`,
44
+ `export const runtimeQuality = ${json(model.manifest.quality)};`,
45
+ `export const runtimeFonts = ${json(model.manifest.fonts)};`,
46
+ `export const runtimeLocalization = ${json(model.localization)};`,
47
+ // Autoloads (`pix3project.yaml` `autoloads:`): the player hands them to the runner, which
48
+ // builds them from the project-scripts registry once per session.
49
+ `export const runtimeAutoloads = ${json(model.manifest.autoloads)};`,
50
+ // Multiplayer kind table (D6): the wire Kind is the index into `prefabs`; sorted by code
51
+ // point so every build of this project agrees with the room allowlist.
52
+ `export const netKindTable = ${json({ prefabs: model.netKindPrefabs, authored: [] })};`,
53
+ '',
54
+ ].join('\n');
55
+ /** The eager glob root of the project's scripts — the player's `register-project-scripts`. */
56
+ export const projectScriptsSource = () => `export const modules = import.meta.glob(${JSON.stringify(globPatterns(SCRIPT_DIRS))}, { eager: true });\n`;
57
+ /** Asset entries largest first, then by path. */
58
+ export const largestFirst = (a, b) => b.rawBytes - a.rawBytes || (a.path < b.path ? -1 : 1);
59
+ /** `{ [resPath]: { base64, mimeType } }` of every shipped asset (single-file build). */
60
+ export const embeddedAssetsSource = async (options) => {
61
+ const lines = [];
62
+ const entries = [];
63
+ let rawBytes = 0;
64
+ let base64Bytes = 0;
65
+ for (const resPath of options.assetPaths) {
66
+ const file = await readFile(resToAbsolute(options.root, options.resRoot, resPath));
67
+ const bytes = options.shippedBytes ? options.shippedBytes(resPath, file) : file;
68
+ const base64 = bytes.toString('base64');
69
+ rawBytes += bytes.byteLength;
70
+ base64Bytes += base64.length;
71
+ entries.push({ path: resPath, rawBytes: bytes.byteLength, base64Bytes: base64.length });
72
+ lines.push(`${JSON.stringify(resPath)}: { base64: ${JSON.stringify(base64)}, mimeType: ${JSON.stringify(contentTypeFor(resPath).split(';')[0])} }`);
73
+ }
74
+ entries.sort(largestFirst);
75
+ return {
76
+ source: `export const embeddedAssets = {\n${lines.join(',\n')}\n};\n`,
77
+ rawBytes,
78
+ base64Bytes,
79
+ count: options.assetPaths.length,
80
+ entries,
81
+ };
82
+ };
83
+ /** Scene-shaped resources (`.pix3scene`, `.prefab`): the ones the runtime parses as YAML. */
84
+ export const SCENE_LIKE_ASSET = /\.(pix3scene|prefab)$/i;
85
+ /**
86
+ * A scene's bytes for a build that stubs `yaml` ({@link yamlStubSource}): the same document as
87
+ * JSON. JSON is YAML, so a project script that reads the text with its own `yaml` import still
88
+ * parses it; the runtime's `parse` becomes `JSON.parse`. The 1.x export did the same.
89
+ */
90
+ export const sceneAsJson = (text) => JSON.stringify(parseYaml(text));
91
+ /**
92
+ * What `yaml` resolves to **for the runtime's own importers** (`SceneLoader`, `SceneSaver`) when
93
+ * every scene ships as JSON: `parse` is `JSON.parse`, `stringify` throws (a player never writes a
94
+ * scene — `SceneSaver` is not even constructed, so this tree-shakes out). A project module that
95
+ * imports `yaml` itself keeps the real parser; this stub is decided per importer.
96
+ */
97
+ export const yamlStubSource = () => [
98
+ '// Scenes and prefabs ship as JSON in this build: the YAML parser is not bundled.',
99
+ 'export const parse = text => JSON.parse(text);',
100
+ "export const stringify = () => { throw new Error('[Pix3] yaml.stringify was stripped from this build: a player never writes scenes.'); };",
101
+ 'export default { parse, stringify };',
102
+ '',
103
+ ].join('\n');
104
+ /** The specifier `AssetLoader` imports the loader by. */
105
+ export const GLTF_LOADER_SPECIFIER = 'three/examples/jsm/loaders/GLTFLoader.js';
106
+ /**
107
+ * `GLTFLoader` (three addons, ~100 KiB rendered) is value-imported by `AssetLoader` for every
108
+ * build; a project with no `.glb`/`.gltf` asset and no script naming `GLTFLoader` gets a loader
109
+ * that fails the way a missing file would (the 1.x `gltf-loader-stub`).
110
+ */
111
+ export const gltfLoaderStubSource = () => [
112
+ '// Stripped from this build: no scene or script names a .glb/.gltf model.',
113
+ "const stripped = () => new Error('[Pix3] GLTFLoader was stripped from this build because no scene or script names a .glb/.gltf model.');",
114
+ 'export class GLTFLoader {',
115
+ ' setPath() { return this; }',
116
+ ' setResourcePath() { return this; }',
117
+ ' setDRACOLoader() { return this; }',
118
+ ' setKTX2Loader() { return this; }',
119
+ ' setMeshoptDecoder() { return this; }',
120
+ ' register() { return this; }',
121
+ ' unregister() { return this; }',
122
+ ' load(_url, _onLoad, _onProgress, onError) { const error = stripped(); if (onError) onError(error); else throw error; }',
123
+ ' loadAsync() { return Promise.reject(stripped()); }',
124
+ ' parse(_data, _path, _onLoad, onError) { const error = stripped(); if (onError) onError(error); else throw error; }',
125
+ ' parseAsync() { return Promise.reject(stripped()); }',
126
+ '}',
127
+ '',
128
+ ].join('\n');
129
+ export const noEmbeddedAssetsSource = () => `export const embeddedAssets = {};\n`;
130
+ /**
131
+ * `virtual:pix3/spine`. Dev: the lazy loader (`virtual:pix3/spine-loader` decides whether the
132
+ * package is installed). Build: a static import when a scene places a `SpineSkeleton2D` and the
133
+ * package is installed, nothing otherwise.
134
+ */
135
+ export const spineSource = (mode) => {
136
+ if (mode === 'none') {
137
+ return '// No SpineSkeleton2D in this project: Spine is not bundled.\nexport {};\n';
138
+ }
139
+ if (mode === 'static') {
140
+ return [
141
+ `import * as spine from '@esotericsoftware/spine-threejs';`,
142
+ `import { setSpineModuleLoader } from '@pix3/runtime';`,
143
+ `setSpineModuleLoader(() => Promise.resolve(spine));`,
144
+ '',
145
+ ].join('\n');
146
+ }
147
+ return [
148
+ `import { loadSpine } from 'virtual:pix3/spine-loader';`,
149
+ `import { setSpineModuleLoader } from '@pix3/runtime';`,
150
+ `setSpineModuleLoader(async () => {`,
151
+ ` const spine = await loadSpine();`,
152
+ ` if (!spine) throw new Error('This scene uses Spine, but @esotericsoftware/spine-threejs is not installed in the project.');`,
153
+ ` return spine;`,
154
+ `});`,
155
+ '',
156
+ ].join('\n');
157
+ };
158
+ /**
159
+ * `virtual:pix3/postprocessing`. The runtime's default is `import('postprocessing')`, which is
160
+ * right in dev and in a zip (a chunk can be fetched). A single-file build registers the module
161
+ * statically when a scene places a `PostProcess`; otherwise `postprocessing` itself resolves to
162
+ * {@link postprocessingStubSource}, so the inlined dynamic import costs nothing.
163
+ */
164
+ export const postprocessingSource = (mode) => mode === 'static'
165
+ ? [
166
+ `import * as postprocessing from 'postprocessing';`,
167
+ `import { setPostprocessingModuleLoader } from '@pix3/runtime';`,
168
+ `setPostprocessingModuleLoader(() => Promise.resolve(postprocessing));`,
169
+ '',
170
+ ].join('\n')
171
+ : '// No PostProcess node in this project: the effect stack is not registered.\nexport {};\n';
172
+ export const postprocessingStubSource = () => [
173
+ '// Stripped from this build: no scene places a PostProcess node.',
174
+ `const stripped = () => { throw new Error('[Pix3] postprocessing was stripped from this build because no scene places a PostProcess node.'); };`,
175
+ 'export const EffectComposer = stripped;',
176
+ 'export const RenderPass = stripped;',
177
+ 'export const EffectPass = stripped;',
178
+ 'export const ClearPass = stripped;',
179
+ 'export const NormalPass = stripped;',
180
+ 'export const DepthDownsamplingPass = stripped;',
181
+ 'export const BloomEffect = stripped;',
182
+ 'export const VignetteEffect = stripped;',
183
+ 'export const ChromaticAberrationEffect = stripped;',
184
+ 'export const SSAOEffect = stripped;',
185
+ 'export const BlendFunction = {};',
186
+ 'export const KernelSize = {};',
187
+ '',
188
+ ].join('\n');
189
+ /** `virtual:pix3/network`: the real installer, or a no-op that keeps `net/**` out of the bundle. */
190
+ export const networkSource = (usesNetwork) => usesNetwork
191
+ ? [
192
+ `import { NetworkService, setNetworkPrefabTable } from '@pix3/runtime';`,
193
+ `import { netKindTable } from 'virtual:pix3/scene-manifest';`,
194
+ `export function installNetworkService(runner) {`,
195
+ ` setNetworkPrefabTable(netKindTable);`,
196
+ ` runner.setNetworkService(new NetworkService());`,
197
+ `}`,
198
+ '',
199
+ ].join('\n')
200
+ : [
201
+ '// Nothing in this project mentions multiplayer: `net/**` is not bundled.',
202
+ 'export function installNetworkService() {}',
203
+ '',
204
+ ].join('\n');
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The part of `pix3project.yaml` the player and the build read (plan §B.5 «scene-manifest из
3
+ * pix3project.yaml»). A Node-side subset of the editor's `normalizeProjectManifest`
4
+ * (`packages/editor-core/src/core/ProjectManifest.ts`): same defaults, same clamps, so a build and
5
+ * the editor's play mode agree on viewport, quality, fonts and locales.
6
+ */
7
+ export declare const PROJECT_MANIFEST_FILE = "pix3project.yaml";
8
+ export interface QualitySettings {
9
+ readonly antialias: boolean;
10
+ readonly shadows: boolean;
11
+ readonly maxPixelRatio: number;
12
+ }
13
+ export interface ProjectFontFace {
14
+ readonly family: string;
15
+ readonly path: string;
16
+ readonly weight?: number | string;
17
+ readonly style?: 'normal' | 'italic';
18
+ readonly unicodeRange?: string;
19
+ }
20
+ export interface LocalizationSettings {
21
+ readonly defaultLocale: string;
22
+ readonly fallbackLocale?: string;
23
+ readonly locales: readonly string[];
24
+ }
25
+ /** One `autoloads:` entry (the runtime's `AutoloadConfig`). */
26
+ export interface AutoloadEntry {
27
+ readonly singleton: string;
28
+ readonly scriptPath: string;
29
+ readonly enabled: boolean;
30
+ }
31
+ export interface ProjectManifestInfo {
32
+ readonly projectName: string;
33
+ /** `defaultExportScenePath` without `res://`, or null. */
34
+ readonly defaultScenePath: string | null;
35
+ readonly viewportBaseSize: {
36
+ readonly width: number;
37
+ readonly height: number;
38
+ };
39
+ readonly quality: QualitySettings;
40
+ readonly fonts: readonly ProjectFontFace[];
41
+ /** The manifest's `localization` block, normalised; null = inert. */
42
+ readonly localization: LocalizationSettings | null;
43
+ /** The `autoloads:` list, read as the runtime's `normalizeAutoloads` reads it. */
44
+ readonly autoloads: readonly AutoloadEntry[];
45
+ }
46
+ /** `res://x`, `./x`, `/x` → `x`. */
47
+ export declare const stripRes: (path: string) => string;
48
+ /** `pix3({ resRoot })` as the build and the server use it: no scheme, no trailing `/`, `'.'` for the root. */
49
+ export declare const normalizeResRoot: (resRoot: string) => string;
50
+ /** A project wire path → its `res://`-relative path, or null outside `resRoot`. */
51
+ export declare const wireToRes: (wirePath: string, resRoot: string) => string | null;
52
+ /** The absolute file of a `res://`-relative path. */
53
+ export declare const resToAbsolute: (root: string, resRoot: string, resPath: string) => string;
54
+ /** Parse the manifest text (an empty or malformed manifest yields the defaults). */
55
+ export declare const parseProjectManifest: (text: string, fallbackName: string) => ProjectManifestInfo;
56
+ /** The manifest of the project at `root` (defaults when there is none). */
57
+ export declare const readProjectManifest: (root: string) => ProjectManifestInfo;