@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.
Files changed (101) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +39 -2
  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 +123 -0
  22. package/dist/build/player-modules.js +254 -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 +290 -0
  61. package/dist/module-paths.d.ts +13 -0
  62. package/dist/module-paths.js +32 -0
  63. package/dist/player/debug/context.d.ts +9 -0
  64. package/dist/player/debug/context.js +1 -0
  65. package/dist/player/debug/device-info.d.ts +15 -0
  66. package/dist/player/debug/device-info.js +36 -0
  67. package/dist/player/debug/frame-stats.d.ts +79 -0
  68. package/dist/player/debug/frame-stats.js +159 -0
  69. package/dist/player/debug/index.d.ts +10 -0
  70. package/dist/player/debug/index.js +20 -0
  71. package/dist/player/debug/long-frames.d.ts +46 -0
  72. package/dist/player/debug/long-frames.js +79 -0
  73. package/dist/player/debug/overlay.d.ts +25 -0
  74. package/dist/player/debug/overlay.js +320 -0
  75. package/dist/player/debug/session.d.ts +92 -0
  76. package/dist/player/debug/session.js +156 -0
  77. package/dist/player/dev/editor-link.d.ts +16 -0
  78. package/dist/player/dev/editor-link.js +125 -0
  79. package/dist/player/index.d.ts +38 -0
  80. package/dist/player/index.js +119 -0
  81. package/dist/server/editor-socket.d.ts +53 -0
  82. package/dist/server/editor-socket.js +134 -0
  83. package/dist/server/guard.d.ts +61 -0
  84. package/dist/server/guard.js +165 -0
  85. package/dist/server/http.d.ts +24 -0
  86. package/dist/server/http.js +70 -0
  87. package/dist/server/image-keys.d.ts +46 -0
  88. package/dist/server/image-keys.js +118 -0
  89. package/dist/server/image-proxy.d.ts +32 -0
  90. package/dist/server/image-proxy.js +124 -0
  91. package/dist/server/router.d.ts +54 -0
  92. package/dist/server/router.js +147 -0
  93. package/dist/sync/barrier.d.ts +69 -0
  94. package/dist/sync/barrier.js +240 -0
  95. package/dist/sync/hot-update.d.ts +16 -0
  96. package/dist/sync/hot-update.js +58 -0
  97. package/dist/sync/script-graph.d.ts +79 -0
  98. package/dist/sync/script-graph.js +214 -0
  99. package/dist/vite-config.d.ts +12 -0
  100. package/dist/vite-config.js +65 -0
  101. package/package.json +58 -4
@@ -0,0 +1,214 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+ import { isAbsolute, sep } from 'node:path';
4
+ import { normalizePath } from 'vite';
5
+ import { isExcludedPath } from "../files/scan.js";
6
+ import { relativeInside } from "../module-paths.js";
7
+ /**
8
+ * The scripts the editor executes, and the proof that it executed the bytes on disk (plan §B.2).
9
+ *
10
+ * - `virtual:pix3/editor-scripts` — eager `import.meta.glob` over `scripts/` and `src/scripts/`
11
+ * (no specs, tests or declarations), exporting `__pix3Revision`;
12
+ * - `virtual:pix3/bot-policies` — the same over `design/tests/bots/`;
13
+ * - every project source module the client environment transforms gets an **executed-content
14
+ * stamp**, `(globalThis.__pix3Executed ??= {})[path] = sha`, which runs only when that module
15
+ * body runs. S1 showed the root's own hashes prove nothing about nested modules; the stamp does.
16
+ */
17
+ export const EDITOR_SCRIPTS_ID = 'virtual:pix3/editor-scripts';
18
+ export const BOT_POLICIES_ID = 'virtual:pix3/bot-policies';
19
+ export const ROOT_IDS = [EDITOR_SCRIPTS_ID, BOT_POLICIES_ID];
20
+ export const resolvedId = (id) => `\0${id}`;
21
+ /**
22
+ * The project-script rule, mirrored from the runtime's `project-script-registration.ts`
23
+ * (`PROJECT_SCRIPT_DIRECTORIES`, `projectScriptGlobPatterns`, `isProjectScriptEntry`): the
24
+ * plugin runs in Node and cannot import the runtime's TypeScript sources, so `script-rule.spec.ts`
25
+ * pins these to the runtime's. A `.ts` under `scripts/` or `src/scripts/` (no specs, tests or
26
+ * declarations) is an entry — no `.js`, no textual gate.
27
+ */
28
+ export const SCRIPT_DIRS = ['scripts', 'src/scripts'];
29
+ export const BOT_POLICY_DIRS = ['design/tests/bots'];
30
+ const SOURCE_EXTENSION = /\.(?:[cm]?[jt]sx?)$/;
31
+ const NOT_A_SCRIPT = /\.(?:spec|test)\.ts$|\.d\.ts$/;
32
+ /** The glob patterns of a root over `dirs` (= the runtime's `projectScriptGlobPatterns`). */
33
+ export const globPatterns = (dirs) => dirs.flatMap(dir => [
34
+ `/${dir}/**/*.ts`,
35
+ `!/${dir}/**/*.spec.ts`,
36
+ `!/${dir}/**/*.test.ts`,
37
+ `!/${dir}/**/*.d.ts`,
38
+ ]);
39
+ /** Source of a glob root; `revision` is the file table's `seq` when Vite loads it. */
40
+ export const rootModuleSource = (id, revision) => {
41
+ const dirs = id === BOT_POLICIES_ID ? BOT_POLICY_DIRS : SCRIPT_DIRS;
42
+ return [
43
+ `export const __pix3Revision = ${revision};`,
44
+ `export const modules = import.meta.glob(${JSON.stringify(globPatterns(dirs))}, { eager: true });`,
45
+ '',
46
+ ].join('\n');
47
+ };
48
+ /** True for a wire path the editor-scripts or bot-policies glob picks up. */
49
+ export const isGlobbedScript = (wirePath) => wirePath.endsWith('.ts') &&
50
+ !NOT_A_SCRIPT.test(wirePath) &&
51
+ [...SCRIPT_DIRS, ...BOT_POLICY_DIRS].some(dir => wirePath.startsWith(`${dir}/`));
52
+ export const sha256Hex = (bytes) => createHash('sha256').update(bytes).digest('hex');
53
+ export class ScriptGraph {
54
+ root;
55
+ server;
56
+ /** Absolute directories inside the root that hold no project sources (Vite's `cacheDir`). */
57
+ ignored;
58
+ constructor(root, server, ignored = []) {
59
+ this.root = root;
60
+ this.server = server;
61
+ this.ignored = ignored.map(dir => normalizePath(dir).replace(/\/+$/, ''));
62
+ }
63
+ /** Module id (absolute path, maybe with a query) → wire path, when it is a project source. */
64
+ wirePathOf(id) {
65
+ const file = id.split('?')[0];
66
+ if (!isAbsolute(file) || !SOURCE_EXTENSION.test(file))
67
+ return null;
68
+ // A pre-bundled dependency in an in-project `cacheDir` is not the project's code.
69
+ if (this.ignored.some(dir => normalizePath(file).startsWith(`${dir}/`)))
70
+ return null;
71
+ const wirePath = relativeInside(this.root, file);
72
+ return wirePath === null || isExcludedPath(wirePath) ? null : wirePath;
73
+ }
74
+ absoluteOf(wirePath) {
75
+ return `${this.root}${sep}${wirePath.split('/').join(sep)}`;
76
+ }
77
+ /**
78
+ * `transform` hook body: append the executed-content stamp to a project source module. The sha
79
+ * is of the file on disk (what the barrier compares with), not of `code`.
80
+ */
81
+ stamp(code, id) {
82
+ const wirePath = this.wirePathOf(id);
83
+ if (wirePath === null)
84
+ return null;
85
+ let sha;
86
+ try {
87
+ sha = sha256Hex(readFileSync(id.split('?')[0]));
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ return (`${code}\n;(globalThis.__pix3Executed ??= {})[${JSON.stringify(wirePath)}] = ` +
93
+ `${JSON.stringify(sha)};\n`);
94
+ }
95
+ graph() {
96
+ return this.server()?.environments.client ?? null;
97
+ }
98
+ rootModules() {
99
+ const env = this.graph();
100
+ if (!env)
101
+ return [];
102
+ const out = [];
103
+ for (const id of ROOT_IDS) {
104
+ const mod = env.moduleGraph.getModuleById(resolvedId(id));
105
+ if (mod)
106
+ out.push(mod);
107
+ }
108
+ return out;
109
+ }
110
+ /**
111
+ * Vite ≥ 5.1 SOFT-invalidates static importers of a changed module: the cached transform is
112
+ * reused with re-stamped `?t=` and `load()` does not run again, so `__pix3Revision` would stay
113
+ * stale (S1, finding 1). The roots are hard-invalidated explicitly.
114
+ *
115
+ * Twice: as an HMR invalidation (`lastHMRTimestamp`, what the propagation stamps) and as a plain
116
+ * one, because only `lastInvalidationTimestamp` is what Vite checks a transform against. With
117
+ * the HMR call alone, a root request already in flight — the tab re-importing after a watcher
118
+ * `pix3:scripts`, its `load()` done at the old `seq` — is handed to the next request for the same
119
+ * URL (`transformRequest` dedupes on the URL without `?t=`), and its result is even cached after
120
+ * the invalidation. The sync's own re-import then executed the previous `__pix3Revision`
121
+ * (`.plans/scripts-vite.md` S15). Vite's own clock is used, so the invalidation is strictly later
122
+ * than every request that started before it.
123
+ */
124
+ hardInvalidateRoots() {
125
+ const env = this.graph();
126
+ if (!env)
127
+ return;
128
+ for (const mod of this.rootModules()) {
129
+ env.moduleGraph.invalidateModule(mod, new Set(), Date.now(), true, false);
130
+ env.moduleGraph.invalidateModule(mod, new Set(), undefined, false, false);
131
+ }
132
+ }
133
+ /** Client-environment nodes of a file (never Vite 8's mixed nodes — S1, finding 5). */
134
+ modulesOf(wirePath) {
135
+ const env = this.graph();
136
+ if (!env)
137
+ return [];
138
+ return [...(env.moduleGraph.getModulesByFile(normalizePath(this.absoluteOf(wirePath))) ?? [])];
139
+ }
140
+ /** True when the editor executes `wirePath`: globbed, or imported (transitively) by a root. */
141
+ isEditorModule(wirePath) {
142
+ if (isGlobbedScript(wirePath))
143
+ return true;
144
+ const roots = new Set(ROOT_IDS.map(resolvedId));
145
+ const seen = new Set();
146
+ const queue = this.modulesOf(wirePath);
147
+ while (queue.length > 0) {
148
+ const mod = queue.shift();
149
+ if (seen.has(mod))
150
+ continue;
151
+ seen.add(mod);
152
+ if (mod.id && roots.has(mod.id))
153
+ return true;
154
+ for (const importer of mod.importers)
155
+ queue.push(importer);
156
+ }
157
+ return false;
158
+ }
159
+ /**
160
+ * Project modules only the bot policies reach (`.plans/scripts-vite.md` S4/S11): reachable from
161
+ * `virtual:pix3/bot-policies` and not from `virtual:pix3/editor-scripts`, so the game the editor
162
+ * plays never runs them and a change to one need not wait for play to stop. Read from the client
163
+ * graph as it is now (after the barrier's propagation); a module the graph has not seen is not
164
+ * listed, which errs on the side of holding play.
165
+ */
166
+ policyOnlyModules() {
167
+ const env = this.graph();
168
+ if (!env)
169
+ return [];
170
+ const reach = (rootId) => {
171
+ const out = new Set();
172
+ const root = env.moduleGraph.getModuleById(resolvedId(rootId));
173
+ if (!root)
174
+ return out;
175
+ const seen = new Set([root]);
176
+ const queue = [...root.importedModules];
177
+ while (queue.length > 0) {
178
+ const mod = queue.shift();
179
+ if (seen.has(mod))
180
+ continue;
181
+ seen.add(mod);
182
+ const wirePath = mod.file ? this.wirePathOf(mod.file) : null;
183
+ if (wirePath !== null)
184
+ out.add(wirePath);
185
+ for (const imported of mod.importedModules)
186
+ queue.push(imported);
187
+ }
188
+ return out;
189
+ };
190
+ // No editor root in the graph yet: nothing can be proven policy-only.
191
+ if (!env.moduleGraph.getModuleById(resolvedId(EDITOR_SCRIPTS_ID)))
192
+ return [];
193
+ const game = reach(EDITOR_SCRIPTS_ID);
194
+ return [...reach(BOT_POLICIES_ID)].filter(path => !game.has(path)).sort();
195
+ }
196
+ /** HMR-propagate one file through the client graph (stamps `lastHMRTimestamp` up the chain). */
197
+ async reload(wirePath) {
198
+ const env = this.graph();
199
+ if (!env)
200
+ return 0;
201
+ const mods = this.modulesOf(wirePath);
202
+ for (const mod of mods)
203
+ await env.reloadModule(mod);
204
+ return mods.length;
205
+ }
206
+ /** Re-run the roots' globs (a script was added or removed). */
207
+ async reloadRoots() {
208
+ const env = this.graph();
209
+ if (!env)
210
+ return;
211
+ for (const mod of this.rootModules())
212
+ await env.reloadModule(mod);
213
+ }
214
+ }
@@ -0,0 +1,12 @@
1
+ import { type UserConfig } from 'vite';
2
+ /** The plugin's `config` hook: what it adds to the user's Vite config. */
3
+ export declare const pix3ViteConfig: (options: {
4
+ readonly user: UserConfig;
5
+ /** This package's directory (the page client and the player are served from it). */
6
+ readonly packageDir: string;
7
+ /** The playable build this command makes, or false (dev, `build: false`). */
8
+ readonly format: "html" | "zip" | false;
9
+ readonly compress: boolean;
10
+ /** Vite 8 bundles with rolldown (`this.meta.rolldownVersion`), Vite 7 with rollup. */
11
+ readonly rolldown: boolean;
12
+ }) => UserConfig;
@@ -0,0 +1,65 @@
1
+ import { readFileSync, realpathSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import { searchForWorkspaceRoot } from 'vite';
4
+ import { findPackageDir } from "./dev-info.js";
5
+ /** `dist/optimize-deps.json` of the installed editor, or nothing. */
6
+ const editorOptimizeDeps = (editorCoreDir) => {
7
+ if (!editorCoreDir)
8
+ return [];
9
+ try {
10
+ const list = JSON.parse(readFileSync(join(editorCoreDir, 'dist', 'optimize-deps.json'), 'utf8'));
11
+ return Array.isArray(list) ? list.filter((id) => typeof id === 'string') : [];
12
+ }
13
+ catch {
14
+ return [];
15
+ }
16
+ };
17
+ /** The plugin's `config` hook: what it adds to the user's Vite config. */
18
+ export const pix3ViteConfig = (options) => {
19
+ const { user, format } = options;
20
+ const projectRoot = resolve(user.root ?? process.cwd());
21
+ const linkedEditorCore = findPackageDir(projectRoot, '@pix3/editor-core');
22
+ // Vite checks `fs.allow` against real paths (`.native`: long names on Windows, as Vite resolves
23
+ // them); a workspace or pnpm link points elsewhere.
24
+ const editorCoreDir = linkedEditorCore ? realpathSync.native(linkedEditorCore) : null;
25
+ // An explicit `fs.allow` switches off Vite's workspace-root default; keep it when the user
26
+ // set none, and add this package (the page client and the player are served from it by
27
+ // `/@fs/`) and the prebuilt editor (served from wherever npm put it: hoisted, pnpm, a
28
+ // workspace link).
29
+ const own = [options.packageDir, ...(editorCoreDir ? [editorCoreDir] : [])];
30
+ const allow = user.server?.fs?.allow ? own : [searchForWorkspaceRoot(projectRoot), ...own];
31
+ // rolldown's output option is `codeSplitting`; rollup spells the same `inlineDynamicImports`.
32
+ const singleChunk = options.rolldown ? { codeSplitting: false } : { inlineDynamicImports: true };
33
+ // A compressed build is injected as script text: `iife`, so it declares nothing global.
34
+ const outputFormat = options.compress && format === 'html' ? { format: 'iife' } : {};
35
+ return {
36
+ resolve: { dedupe: ['three', '@pix3/runtime'] },
37
+ optimizeDeps: {
38
+ // One runtime and one three for the game and the editor (plan §B.2, S1), plus the bare
39
+ // subpaths the prebuilt editor imports (`dist/optimize-deps.json`).
40
+ include: ['@pix3/runtime', 'three', ...editorOptimizeDeps(editorCoreDir)],
41
+ // The installed player imports virtual modules resolved by this plugin's Vite hooks;
42
+ // the dependency optimizer cannot resolve them while prebundling node_modules.
43
+ exclude: ['@pix3/editor-core', '@pix3/vite-plugin/player'],
44
+ },
45
+ server: { fs: { allow } },
46
+ ...(format
47
+ ? {
48
+ // Relative URLs: the artifact runs from `file://`, a subfolder or a sandboxed frame.
49
+ base: './',
50
+ build: format === 'html'
51
+ ? {
52
+ // Everything inlined (the single-file plugin's recommended config, set here
53
+ // so the chunking flag follows the bundler, not Vite's version string).
54
+ assetsInlineLimit: () => true,
55
+ chunkSizeWarningLimit: 100_000_000,
56
+ cssCodeSplit: false,
57
+ assetsDir: '',
58
+ modulePreload: false,
59
+ rollupOptions: { output: { ...singleChunk, ...outputFormat } },
60
+ }
61
+ : {},
62
+ }
63
+ : {}),
64
+ };
65
+ };
package/package.json CHANGED
@@ -1,6 +1,60 @@
1
1
  {
2
2
  "name": "@pix3/vite-plugin",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "2.0.0-alpha.1",
4
+ "description": "Pix3 editor, file API, sync barrier and build as a Vite plugin.",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/pix3dev/pix3-core.git",
9
+ "directory": "packages/vite-plugin"
10
+ },
11
+ "homepage": "https://github.com/pix3dev/pix3-core/tree/main/packages/vite-plugin#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/pix3dev/pix3-core/issues"
14
+ },
15
+ "engines": {
16
+ "node": ">=20.19"
17
+ },
18
+ "main": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js"
24
+ },
25
+ "./player": {
26
+ "types": "./dist/player/index.d.ts",
27
+ "import": "./dist/player/index.js"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "README.md"
33
+ ],
34
+ "scripts": {
35
+ "build": "tsc -p tsconfig.build.json && tsc -p player/tsconfig.build.json",
36
+ "prepack": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && npm run build",
37
+ "type-check": "tsc --noEmit && tsc --noEmit -p tsconfig.build.json && tsc --noEmit -p player/tsconfig.json"
38
+ },
39
+ "dependencies": {
40
+ "fflate": "^0.8.3",
41
+ "vite-plugin-singlefile": "^2.3.3",
42
+ "ws": "^8.18.0",
43
+ "yaml": "^2.6.0"
44
+ },
45
+ "peerDependencies": {
46
+ "@pix3/runtime": "2.0.0-alpha.1",
47
+ "vite": "^7 || ^8"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^24.5.2",
51
+ "@types/ws": "^8.18.1",
52
+ "typescript": "~5.8.3",
53
+ "vite": "^8.3.3"
54
+ },
55
+ "license": "Apache-2.0",
56
+ "publishConfig": {
57
+ "access": "public",
58
+ "tag": "next"
59
+ }
60
+ }