@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.
- package/LICENSE +201 -0
- package/README.md +38 -3
- 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 +99 -0
- package/dist/build/player-modules.js +204 -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 +278 -0
- package/dist/module-paths.d.ts +13 -0
- package/dist/module-paths.js +32 -0
- package/dist/player/index.d.ts +31 -0
- package/dist/player/index.js +102 -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 +62 -0
- package/package.json +60 -6
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Importers whose references do not pin anything, because they cannot survive in a player bundle
|
|
3
|
+
* on their own:
|
|
4
|
+
*
|
|
5
|
+
* - `core/SceneLoader` — the `switch` over every node type; a case whose type never appears in a
|
|
6
|
+
* shipped scene is dead code that only constructs the stub.
|
|
7
|
+
* - `index` — the package barrel; the bundler drops re-exports nobody imports.
|
|
8
|
+
* - `behaviors/register-behaviors` — registers classes by id into a `Map`; a stub registers fine
|
|
9
|
+
* and `ScriptRegistry.createComponent` reports the throw.
|
|
10
|
+
* - `core/SceneSaver` — never constructed by a player (see `SceneManager`'s optional saver), so it
|
|
11
|
+
* tree-shakes out with everything it imports.
|
|
12
|
+
*
|
|
13
|
+
* @internal The import-graph guard (`strippable-runtime-modules.spec.ts`) reads it.
|
|
14
|
+
*/
|
|
15
|
+
export const NEUTRALISED_IMPORTERS = [
|
|
16
|
+
'core/SceneLoader',
|
|
17
|
+
'index',
|
|
18
|
+
'behaviors/register-behaviors',
|
|
19
|
+
'core/SceneSaver',
|
|
20
|
+
];
|
|
21
|
+
export const STRIPPABLE_RUNTIME_MODULES = [
|
|
22
|
+
// --- script components (`core:` behaviours) ---
|
|
23
|
+
{ modulePath: 'behaviors/RotateBehavior', keepWhenMentioned: ['RotateBehavior', 'core:Rotate'] },
|
|
24
|
+
{
|
|
25
|
+
modulePath: 'behaviors/SimpleMoveBehavior',
|
|
26
|
+
keepWhenMentioned: ['SimpleMoveBehavior', 'core:SimpleMove'],
|
|
27
|
+
},
|
|
28
|
+
{ modulePath: 'behaviors/SineBehavior', keepWhenMentioned: ['SineBehavior', 'core:Sine'] },
|
|
29
|
+
{
|
|
30
|
+
modulePath: 'behaviors/PinToNodeBehavior',
|
|
31
|
+
keepWhenMentioned: ['PinToNodeBehavior', 'core:PinToNode'],
|
|
32
|
+
},
|
|
33
|
+
{ modulePath: 'behaviors/FollowBehavior', keepWhenMentioned: ['FollowBehavior', 'core:Follow'] },
|
|
34
|
+
{ modulePath: 'behaviors/FadeBehavior', keepWhenMentioned: ['FadeBehavior', 'core:Fade'] },
|
|
35
|
+
{
|
|
36
|
+
modulePath: 'behaviors/RadialProgressBehavior',
|
|
37
|
+
keepWhenMentioned: ['RadialProgressBehavior', 'core:RadialProgress'],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
modulePath: 'behaviors/PlaySoundBehavior',
|
|
41
|
+
keepWhenMentioned: ['PlaySoundBehavior', 'core:PlaySound'],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
modulePath: 'behaviors/SfxOnSignalBehavior',
|
|
45
|
+
keepWhenMentioned: ['SfxOnSignalBehavior', 'core:SfxOnSignal'],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
modulePath: 'behaviors/BurstOnSignalBehavior',
|
|
49
|
+
keepWhenMentioned: ['BurstOnSignalBehavior', 'core:BurstOnSignal'],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
modulePath: 'behaviors/FreeOnSignalBehavior',
|
|
53
|
+
keepWhenMentioned: ['FreeOnSignalBehavior', 'core:FreeOnSignal'],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
modulePath: 'behaviors/Hitbox2DBehavior',
|
|
57
|
+
keepWhenMentioned: ['Hitbox2DBehavior', 'core:Hitbox2D'],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
modulePath: 'behaviors/PointAttachmentBehavior',
|
|
61
|
+
keepWhenMentioned: ['PointAttachmentBehavior', 'core:PointAttachment'],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
modulePath: 'behaviors/CharacterVisual2DBehavior',
|
|
65
|
+
keepWhenMentioned: ['CharacterVisual2DBehavior', 'core:CharacterVisual2D'],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
modulePath: 'behaviors/ReplicatedTransformBehavior',
|
|
69
|
+
keepWhenMentioned: ['ReplicatedTransformBehavior', 'core:ReplicatedTransform'],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
modulePath: 'behaviors/NetworkedNodeBehavior',
|
|
73
|
+
keepWhenMentioned: ['NetworkedNodeBehavior', 'core:NetworkedNode'],
|
|
74
|
+
importers: ['behaviors/ReplicatedTransformBehavior'],
|
|
75
|
+
},
|
|
76
|
+
// --- node types ---
|
|
77
|
+
{
|
|
78
|
+
modulePath: 'nodes/2D/ColorRect2D',
|
|
79
|
+
keepWhenMentioned: ['ColorRect2D'],
|
|
80
|
+
// `UIControl2D.findPointerTarget` only tests `instanceof ColorRect2D`; against the stub that is
|
|
81
|
+
// false for every node, which is exactly right when no scene places one.
|
|
82
|
+
lazyValueImporters: ['nodes/2D/UI/UIControl2D'],
|
|
83
|
+
},
|
|
84
|
+
{ modulePath: 'nodes/2D/CanvasLayer2D', keepWhenMentioned: ['CanvasLayer2D'] },
|
|
85
|
+
{ modulePath: 'nodes/2D/TiledSprite2D', keepWhenMentioned: ['TiledSprite2D'] },
|
|
86
|
+
{ modulePath: 'nodes/2D/SpineSkeleton2D', keepWhenMentioned: ['SpineSkeleton2D'] },
|
|
87
|
+
{
|
|
88
|
+
modulePath: 'nodes/2D/AnimatedSprite2D',
|
|
89
|
+
keepWhenMentioned: ['AnimatedSprite2D'],
|
|
90
|
+
importers: ['behaviors/PointAttachmentBehavior', 'behaviors/CharacterVisual2DBehavior'],
|
|
91
|
+
},
|
|
92
|
+
{ modulePath: 'nodes/2D/UI/Bar2D', keepWhenMentioned: ['Bar2D'] },
|
|
93
|
+
{ modulePath: 'nodes/2D/UI/Checkbox2D', keepWhenMentioned: ['Checkbox2D'] },
|
|
94
|
+
{ modulePath: 'nodes/2D/UI/InventorySlot2D', keepWhenMentioned: ['InventorySlot2D'] },
|
|
95
|
+
{ modulePath: 'nodes/2D/UI/Joystick2D', keepWhenMentioned: ['Joystick2D'] },
|
|
96
|
+
{ modulePath: 'nodes/2D/UI/Slider2D', keepWhenMentioned: ['Slider2D'] },
|
|
97
|
+
{ modulePath: 'nodes/3D/AmbientLightNode', keepWhenMentioned: ['AmbientLightNode'] },
|
|
98
|
+
{ modulePath: 'nodes/3D/DirectionalLightNode', keepWhenMentioned: ['DirectionalLightNode'] },
|
|
99
|
+
{ modulePath: 'nodes/3D/HemisphereLightNode', keepWhenMentioned: ['HemisphereLightNode'] },
|
|
100
|
+
{ modulePath: 'nodes/3D/PointLightNode', keepWhenMentioned: ['PointLightNode'] },
|
|
101
|
+
{ modulePath: 'nodes/3D/SpotLightNode', keepWhenMentioned: ['SpotLightNode'] },
|
|
102
|
+
// --- 2D collision and physics ---
|
|
103
|
+
{
|
|
104
|
+
// The polygon math: winding, convex decomposition, SAT, the world transform. It has no
|
|
105
|
+
// keep-names of its own — it is exactly as needed as the two services that use it, and the
|
|
106
|
+
// fixpoint below works that out.
|
|
107
|
+
modulePath: 'core/collision-shapes-2d',
|
|
108
|
+
keepWhenMentioned: [],
|
|
109
|
+
importers: ['core/Collision2DService', 'core/Physics2DService', 'core/physics-2d-narrowphase'],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
modulePath: 'core/world-transform-2d',
|
|
113
|
+
keepWhenMentioned: [],
|
|
114
|
+
importers: ['core/Collision2DService', 'core/Physics2DService'],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
modulePath: 'core/collision-polygon-config',
|
|
118
|
+
keepWhenMentioned: [],
|
|
119
|
+
importers: ['behaviors/Hitbox2DBehavior', 'behaviors/Collider2DBehavior'],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
// Query-only 2D collision. `SceneService.collision2d` constructs it lazily, and a project that
|
|
123
|
+
// never writes the word cannot reach that getter — the NetworkService precedent, one line down.
|
|
124
|
+
modulePath: 'core/Collision2DService',
|
|
125
|
+
keepWhenMentioned: ['collision2d', 'Collision2D', 'Hitbox2D', 'core:Hitbox2D'],
|
|
126
|
+
// Hitbox2DBehavior imports only its TYPES, but reaches the service through the lazy
|
|
127
|
+
// `scene.collision2d` getter: a kept behaviour (placed by a scene, or constructed by a script
|
|
128
|
+
// as `new Hitbox2DBehavior()`) must keep the service — so it is an importer for the fixpoint.
|
|
129
|
+
importers: ['behaviors/Hitbox2DBehavior'],
|
|
130
|
+
lazyValueImporters: ['core/SceneService'],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
modulePath: 'core/physics-2d-narrowphase',
|
|
134
|
+
keepWhenMentioned: [],
|
|
135
|
+
importers: ['core/Physics2DService'],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
// ~20 KiB of solver. Same lazy-getter argument as `collision2d`; and `SceneRunner` reaches it
|
|
139
|
+
// through `SceneService.stepPhysics2D`, so it adds no value import of its own.
|
|
140
|
+
modulePath: 'core/Physics2DService',
|
|
141
|
+
keepWhenMentioned: [
|
|
142
|
+
'physics2d',
|
|
143
|
+
'Physics2D',
|
|
144
|
+
'PhysicsBody2D',
|
|
145
|
+
'core:PhysicsBody2D',
|
|
146
|
+
'Collider2D',
|
|
147
|
+
'core:Collider2D',
|
|
148
|
+
'core:PhysicsWorld2D',
|
|
149
|
+
'core:RevoluteJoint2D',
|
|
150
|
+
'RevoluteJoint2D',
|
|
151
|
+
],
|
|
152
|
+
// The four behaviours import only types from here but reach the service through the lazy
|
|
153
|
+
// `scene.physics2d` getter (same as Hitbox2D): each one kept keeps the solver.
|
|
154
|
+
importers: [
|
|
155
|
+
'behaviors/PhysicsBody2DBehavior',
|
|
156
|
+
'behaviors/Collider2DBehavior',
|
|
157
|
+
'behaviors/PhysicsWorld2DBehavior',
|
|
158
|
+
'behaviors/RevoluteJoint2DBehavior',
|
|
159
|
+
],
|
|
160
|
+
lazyValueImporters: ['core/SceneService'],
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
modulePath: 'behaviors/PhysicsBody2DBehavior',
|
|
164
|
+
keepWhenMentioned: ['PhysicsBody2DBehavior', 'core:PhysicsBody2D'],
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
modulePath: 'behaviors/Collider2DBehavior',
|
|
168
|
+
keepWhenMentioned: ['Collider2DBehavior', 'core:Collider2D'],
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
modulePath: 'behaviors/PhysicsWorld2DBehavior',
|
|
172
|
+
keepWhenMentioned: ['PhysicsWorld2DBehavior', 'core:PhysicsWorld2D'],
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
modulePath: 'behaviors/RevoluteJoint2DBehavior',
|
|
176
|
+
keepWhenMentioned: ['RevoluteJoint2DBehavior', 'core:RevoluteJoint2D'],
|
|
177
|
+
},
|
|
178
|
+
// --- tweens and trails ---
|
|
179
|
+
{
|
|
180
|
+
// `SceneService.tween` constructs it lazily, and the per-frame `updateTweens` hook is a null
|
|
181
|
+
// check on an instance nobody made — so a project that never writes the word cannot reach the
|
|
182
|
+
// getter (the `collision2d` / `network` precedent above).
|
|
183
|
+
modulePath: 'core/TweenApi',
|
|
184
|
+
keepWhenMentioned: ['tween', 'Tween', 'TweenApi'],
|
|
185
|
+
lazyValueImporters: ['core/SceneService'],
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
// The motion ribbon. `JuiceApi.trail()` is its only construction site, and nothing else in
|
|
189
|
+
// the runtime reaches it.
|
|
190
|
+
modulePath: 'core/trail-2d',
|
|
191
|
+
keepWhenMentioned: ['trail', 'Trail2D'],
|
|
192
|
+
lazyValueImporters: ['core/JuiceApi'],
|
|
193
|
+
},
|
|
194
|
+
// --- multiplayer ---
|
|
195
|
+
{
|
|
196
|
+
// ~59 KiB with its protocol tree. `SceneService.network` is a getter that constructs one
|
|
197
|
+
// lazily "so single-player scripts can touch it freely" — but a project that never writes the
|
|
198
|
+
// word `network` anywhere cannot reach that getter, and any project that does keeps the real
|
|
199
|
+
// service (see `NETWORK_MENTION_NAMES` in `scan.ts`).
|
|
200
|
+
modulePath: 'net/NetworkService',
|
|
201
|
+
keepWhenMentioned: [
|
|
202
|
+
'network',
|
|
203
|
+
'Network',
|
|
204
|
+
'NetworkService',
|
|
205
|
+
'setNetworkPrefabTable',
|
|
206
|
+
'NetworkedNode',
|
|
207
|
+
'ReplicatedTransform',
|
|
208
|
+
// `SceneService.netNodes` builds a binder on `this.network`: the same lazy construction.
|
|
209
|
+
'netNodes',
|
|
210
|
+
],
|
|
211
|
+
importers: ['behaviors/NetworkedNodeBehavior', 'behaviors/ReplicatedTransformBehavior'],
|
|
212
|
+
// `NetworkNodeBinder` reads `host.network`, but only `SceneService.netNodes` constructs it
|
|
213
|
+
// (a keep-name above), so it reaches the service only where the project said so.
|
|
214
|
+
lazyValueImporters: ['core/SceneService', 'core/NetworkNodeBinder'],
|
|
215
|
+
},
|
|
216
|
+
];
|
|
217
|
+
/**
|
|
218
|
+
* The subset of {@link STRIPPABLE_RUNTIME_MODULES} that can leave a build in which
|
|
219
|
+
* `mentions(name)` is false for every name the entry (and, transitively, every entry that imports
|
|
220
|
+
* it) is kept by.
|
|
221
|
+
*/
|
|
222
|
+
export const resolveStrippableRuntimeModules = (mentions,
|
|
223
|
+
/** The module's value exports (from its real source); each one keeps it like a keep-name. */
|
|
224
|
+
exportsOf = () => []) => {
|
|
225
|
+
const byPath = new Map(STRIPPABLE_RUNTIME_MODULES.map(entry => [entry.modulePath, entry]));
|
|
226
|
+
const stripped = new Set(STRIPPABLE_RUNTIME_MODULES.filter(entry => !entry.keepWhenMentioned.some(mentions) && !exportsOf(entry.modulePath).some(mentions)).map(entry => entry.modulePath));
|
|
227
|
+
// A module survives as long as anything that imports it survives. Iterate until that settles:
|
|
228
|
+
// un-stripping ReplicatedTransformBehavior, say, has to also rescue NetworkedNodeBehavior and
|
|
229
|
+
// through it NetworkService.
|
|
230
|
+
for (let changed = true; changed;) {
|
|
231
|
+
changed = false;
|
|
232
|
+
for (const modulePath of [...stripped]) {
|
|
233
|
+
const importers = byPath.get(modulePath)?.importers ?? [];
|
|
234
|
+
if (importers.some(importer => !stripped.has(importer))) {
|
|
235
|
+
stripped.delete(modulePath);
|
|
236
|
+
changed = true;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return STRIPPABLE_RUNTIME_MODULES.filter(entry => stripped.has(entry.modulePath));
|
|
241
|
+
};
|
|
242
|
+
/** The value exports of a runtime module's source, by kind (what the stub has to re-declare). */
|
|
243
|
+
const valueExportsOf = (moduleSource) => {
|
|
244
|
+
const classNames = new Set();
|
|
245
|
+
const functionNames = new Set();
|
|
246
|
+
const otherNames = new Set();
|
|
247
|
+
const declarationPattern = /export\s+(?:declare\s+)?(?:abstract\s+)?(class|const|function|async\s+function|let|var|enum)\s+([A-Za-z0-9_$]+)/g;
|
|
248
|
+
for (const match of moduleSource.matchAll(declarationPattern)) {
|
|
249
|
+
if (match[1] === 'class') {
|
|
250
|
+
classNames.add(match[2]);
|
|
251
|
+
}
|
|
252
|
+
else if (match[1].endsWith('function')) {
|
|
253
|
+
functionNames.add(match[2]);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
otherNames.add(match[2]);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// `export { a, b as c }` re-export lists (rare in node modules, but cheap to cover).
|
|
260
|
+
const listPattern = /export\s*\{([^}]*)\}(?!\s*from)/g;
|
|
261
|
+
for (const match of moduleSource.matchAll(listPattern)) {
|
|
262
|
+
for (const clause of match[1].split(',')) {
|
|
263
|
+
const name = clause
|
|
264
|
+
.trim()
|
|
265
|
+
.split(/\s+as\s+/)
|
|
266
|
+
.pop()
|
|
267
|
+
?.trim();
|
|
268
|
+
if (name &&
|
|
269
|
+
/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) &&
|
|
270
|
+
!classNames.has(name) &&
|
|
271
|
+
!functionNames.has(name)) {
|
|
272
|
+
otherNames.add(name);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return { classNames, functionNames, otherNames };
|
|
277
|
+
};
|
|
278
|
+
/** Every value export name of a module's source (the names that keep it besides the table's). */
|
|
279
|
+
export const valueExportNames = (moduleSource) => {
|
|
280
|
+
const { classNames, functionNames, otherNames } = valueExportsOf(moduleSource);
|
|
281
|
+
return [...classNames, ...functionNames, ...otherNames];
|
|
282
|
+
};
|
|
283
|
+
/**
|
|
284
|
+
* A drop-in replacement for a runtime module's source: same value exports, but every class throws
|
|
285
|
+
* if anything ever constructs it. Derived from the real source rather than hardcoded, so a renamed
|
|
286
|
+
* export cannot silently drift — and if a name is missed, the bundler fails the build with "no
|
|
287
|
+
* matching export" instead of shipping something broken.
|
|
288
|
+
*
|
|
289
|
+
* Types and interfaces need no stubs: the bundler erases type-only usages, so the importing module
|
|
290
|
+
* no longer references those names at all.
|
|
291
|
+
*/
|
|
292
|
+
export const buildStrippedModuleSource = (moduleSource, modulePath) => {
|
|
293
|
+
const { classNames, functionNames, otherNames } = valueExportsOf(moduleSource);
|
|
294
|
+
const strippedError = (name) => JSON.stringify(`[Pix3] ${name} was stripped from this build because nothing in the project referenced it at build time.`);
|
|
295
|
+
const lines = [`// Stripped from this build: nothing in the project mentions ${modulePath}.`];
|
|
296
|
+
for (const name of classNames) {
|
|
297
|
+
lines.push(`export class ${name} {`, ` static getPropertySchema() { return []; }`, ` constructor() { throw new Error(${strippedError(name)}); }`, `}`);
|
|
298
|
+
}
|
|
299
|
+
for (const name of functionNames) {
|
|
300
|
+
lines.push(`export function ${name}() { throw new Error(${strippedError(name)}); }`);
|
|
301
|
+
}
|
|
302
|
+
for (const name of otherNames) {
|
|
303
|
+
lines.push(`export const ${name} = undefined;`);
|
|
304
|
+
}
|
|
305
|
+
if (classNames.size === 0 && functionNames.size === 0 && otherNames.size === 0) {
|
|
306
|
+
lines.push('export {};');
|
|
307
|
+
}
|
|
308
|
+
return lines.join('\n') + '\n';
|
|
309
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `build: 'zip'` (plan §B.6): nothing is inlined — the build's `index.html`, chunks and the
|
|
3
|
+
* assets emitted beside them go into one `fflate` archive at `<outDir>/<name>.zip`, with paths
|
|
4
|
+
* relative to `outDir` (unpack anywhere; the player's `ResourceManager('./')` is relative).
|
|
5
|
+
*/
|
|
6
|
+
export declare const zipDirectory: (outDir: string, zipName: string) => string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, relative, sep } from 'node:path';
|
|
3
|
+
import { zipSync } from 'fflate';
|
|
4
|
+
/**
|
|
5
|
+
* `build: 'zip'` (plan §B.6): nothing is inlined — the build's `index.html`, chunks and the
|
|
6
|
+
* assets emitted beside them go into one `fflate` archive at `<outDir>/<name>.zip`, with paths
|
|
7
|
+
* relative to `outDir` (unpack anywhere; the player's `ResourceManager('./')` is relative).
|
|
8
|
+
*/
|
|
9
|
+
export const zipDirectory = (outDir, zipName) => {
|
|
10
|
+
const files = {};
|
|
11
|
+
const walk = (dir) => {
|
|
12
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
13
|
+
const absolute = join(dir, entry.name);
|
|
14
|
+
if (entry.isDirectory())
|
|
15
|
+
walk(absolute);
|
|
16
|
+
else if (entry.isFile()) {
|
|
17
|
+
const rel = relative(outDir, absolute).split(sep).join('/');
|
|
18
|
+
if (rel === zipName)
|
|
19
|
+
continue;
|
|
20
|
+
files[rel] = new Uint8Array(readFileSync(absolute));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
walk(outDir);
|
|
25
|
+
const zipPath = join(outDir, zipName);
|
|
26
|
+
writeFileSync(zipPath, zipSync(files, { level: 6 }));
|
|
27
|
+
return zipPath;
|
|
28
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `EditorHost.files` over `/__pix3/api/*` (`.plans/editor-core-port.md` §1.2). A failed call
|
|
3
|
+
* rejects with an `Error` carrying `failure: {code, status, message, currentHash?}` — the shape
|
|
4
|
+
* `@pix3/editor-core`'s `hostFailureOf` reads.
|
|
5
|
+
*/
|
|
6
|
+
export type HostFileErrorCode = 'not_found' | 'base_mismatch' | 'exists' | 'writer_superseded' | 'reserved_path' | 'bad_path' | 'not_a_file' | 'not_empty' | 'forbidden' | 'network' | 'other';
|
|
7
|
+
export interface HostFileFailure {
|
|
8
|
+
readonly code: HostFileErrorCode;
|
|
9
|
+
readonly status: number;
|
|
10
|
+
/** The file a changeset failed on. */
|
|
11
|
+
readonly path?: string;
|
|
12
|
+
readonly currentHash?: string | null;
|
|
13
|
+
readonly message: string;
|
|
14
|
+
}
|
|
15
|
+
export interface HostManifestEntry {
|
|
16
|
+
readonly path: string;
|
|
17
|
+
readonly kind: 'file' | 'dir';
|
|
18
|
+
readonly size: number;
|
|
19
|
+
readonly mtime: number;
|
|
20
|
+
readonly sha256?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface HostWriteOptions {
|
|
23
|
+
readonly ifMatch?: string;
|
|
24
|
+
readonly createOnly?: boolean;
|
|
25
|
+
readonly mutationId?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface HostChangesetEntry {
|
|
28
|
+
readonly path: string;
|
|
29
|
+
readonly data: Uint8Array | string;
|
|
30
|
+
readonly ifMatch?: string;
|
|
31
|
+
readonly createOnly?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface HostWriteResult {
|
|
34
|
+
readonly path: string;
|
|
35
|
+
readonly sha256: string;
|
|
36
|
+
readonly size: number;
|
|
37
|
+
readonly seq: number;
|
|
38
|
+
readonly mtime?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface HostChangesetResult {
|
|
41
|
+
readonly seq: number;
|
|
42
|
+
readonly files: readonly HostWriteResult[];
|
|
43
|
+
}
|
|
44
|
+
/** What the files client needs from the connection. */
|
|
45
|
+
export interface ApiCaller {
|
|
46
|
+
readonly base: string;
|
|
47
|
+
api(route: string, init?: RequestInit): Promise<Response>;
|
|
48
|
+
}
|
|
49
|
+
export declare class HostFileError extends Error {
|
|
50
|
+
readonly failure: HostFileFailure;
|
|
51
|
+
constructor(failure: HostFileFailure);
|
|
52
|
+
}
|
|
53
|
+
export declare const failureOf: (response: Response) => Promise<HostFileError>;
|
|
54
|
+
export declare const networkError: (error: unknown) => HostFileError;
|
|
55
|
+
export declare class HostFilesClient {
|
|
56
|
+
private readonly caller;
|
|
57
|
+
constructor(caller: ApiCaller);
|
|
58
|
+
private call;
|
|
59
|
+
private json;
|
|
60
|
+
read(path: string): Promise<{
|
|
61
|
+
bytes: Uint8Array;
|
|
62
|
+
sha256: string;
|
|
63
|
+
} | null>;
|
|
64
|
+
readText(path: string): Promise<string | null>;
|
|
65
|
+
head(path: string): Promise<{
|
|
66
|
+
sha256: string;
|
|
67
|
+
size: number;
|
|
68
|
+
} | null>;
|
|
69
|
+
write(path: string, data: Uint8Array | string, options?: HostWriteOptions): Promise<{
|
|
70
|
+
path: string;
|
|
71
|
+
sha256: string;
|
|
72
|
+
size: number;
|
|
73
|
+
seq: number;
|
|
74
|
+
}>;
|
|
75
|
+
/**
|
|
76
|
+
* Several files as one transaction (plan §C.4). A refusal names the file in `failure.path`;
|
|
77
|
+
* nothing was written then.
|
|
78
|
+
*/
|
|
79
|
+
writeChangeset(entries: readonly HostChangesetEntry[], options?: {
|
|
80
|
+
mutationId?: string;
|
|
81
|
+
}): Promise<HostChangesetResult>;
|
|
82
|
+
mkdir(path: string): Promise<void>;
|
|
83
|
+
delete(path: string, options?: {
|
|
84
|
+
recursive?: boolean;
|
|
85
|
+
}): Promise<void>;
|
|
86
|
+
move(from: string, to: string, options?: {
|
|
87
|
+
overwrite?: boolean;
|
|
88
|
+
}): Promise<void>;
|
|
89
|
+
manifest(): Promise<{
|
|
90
|
+
revision: string;
|
|
91
|
+
seq: number;
|
|
92
|
+
files: HostManifestEntry[];
|
|
93
|
+
}>;
|
|
94
|
+
hash(paths: readonly string[]): Promise<Record<string, string | null>>;
|
|
95
|
+
url(path: string): string;
|
|
96
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `EditorHost.files` over `/__pix3/api/*` (`.plans/editor-core-port.md` §1.2). A failed call
|
|
3
|
+
* rejects with an `Error` carrying `failure: {code, status, message, currentHash?}` — the shape
|
|
4
|
+
* `@pix3/editor-core`'s `hostFailureOf` reads.
|
|
5
|
+
*/
|
|
6
|
+
const KNOWN_CODES = new Set([
|
|
7
|
+
'not_found',
|
|
8
|
+
'base_mismatch',
|
|
9
|
+
'exists',
|
|
10
|
+
'writer_superseded',
|
|
11
|
+
'reserved_path',
|
|
12
|
+
'bad_path',
|
|
13
|
+
'not_a_file',
|
|
14
|
+
'not_empty',
|
|
15
|
+
]);
|
|
16
|
+
export class HostFileError extends Error {
|
|
17
|
+
failure;
|
|
18
|
+
constructor(failure) {
|
|
19
|
+
super(failure.message);
|
|
20
|
+
this.name = 'HostFileError';
|
|
21
|
+
this.failure = failure;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export const failureOf = async (response) => {
|
|
25
|
+
let body = {};
|
|
26
|
+
try {
|
|
27
|
+
body = (await response.json());
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// not JSON
|
|
31
|
+
}
|
|
32
|
+
const raw = typeof body.error === 'string' ? body.error : '';
|
|
33
|
+
const code = KNOWN_CODES.has(raw)
|
|
34
|
+
? raw
|
|
35
|
+
: response.status === 403
|
|
36
|
+
? 'forbidden'
|
|
37
|
+
: response.status === 404
|
|
38
|
+
? 'not_found'
|
|
39
|
+
: 'other';
|
|
40
|
+
return new HostFileError({
|
|
41
|
+
code,
|
|
42
|
+
status: response.status,
|
|
43
|
+
message: typeof body.message === 'string' ? body.message : `HTTP ${response.status}`,
|
|
44
|
+
...(typeof body.path === 'string' ? { path: body.path } : {}),
|
|
45
|
+
...('currentHash' in body ? { currentHash: body.currentHash ?? null } : {}),
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
export const networkError = (error) => new HostFileError({
|
|
49
|
+
code: 'network',
|
|
50
|
+
status: 0,
|
|
51
|
+
message: `The dev server did not answer: ${error instanceof Error ? error.message : String(error)}`,
|
|
52
|
+
});
|
|
53
|
+
/** Bytes as base64 without `Buffer` (this runs in the page); chunked to stay off the arg limit. */
|
|
54
|
+
const toBase64 = (bytes) => {
|
|
55
|
+
let binary = '';
|
|
56
|
+
for (let i = 0; i < bytes.length; i += 0x8000) {
|
|
57
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
58
|
+
}
|
|
59
|
+
return btoa(binary);
|
|
60
|
+
};
|
|
61
|
+
const etagOf = (response) => (response.headers.get('etag') ?? '').replace(/^W\//, '').replace(/^"|"$/g, '');
|
|
62
|
+
const q = (path) => `file?path=${encodeURIComponent(path)}`;
|
|
63
|
+
export class HostFilesClient {
|
|
64
|
+
caller;
|
|
65
|
+
constructor(caller) {
|
|
66
|
+
this.caller = caller;
|
|
67
|
+
}
|
|
68
|
+
async call(route, init) {
|
|
69
|
+
try {
|
|
70
|
+
return await this.caller.api(route, init);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
throw networkError(error);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async json(route, body) {
|
|
77
|
+
const response = await this.call(route, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: { 'Content-Type': 'application/json' },
|
|
80
|
+
body: JSON.stringify(body),
|
|
81
|
+
});
|
|
82
|
+
if (!response.ok)
|
|
83
|
+
throw await failureOf(response);
|
|
84
|
+
return (await response.json());
|
|
85
|
+
}
|
|
86
|
+
async read(path) {
|
|
87
|
+
const response = await this.call(q(path));
|
|
88
|
+
if (response.status === 404)
|
|
89
|
+
return null;
|
|
90
|
+
if (!response.ok)
|
|
91
|
+
throw await failureOf(response);
|
|
92
|
+
return { bytes: new Uint8Array(await response.arrayBuffer()), sha256: etagOf(response) };
|
|
93
|
+
}
|
|
94
|
+
async readText(path) {
|
|
95
|
+
const read = await this.read(path);
|
|
96
|
+
return read ? new TextDecoder().decode(read.bytes) : null;
|
|
97
|
+
}
|
|
98
|
+
async head(path) {
|
|
99
|
+
const response = await this.call(q(path), { method: 'HEAD' });
|
|
100
|
+
if (response.status === 404)
|
|
101
|
+
return null;
|
|
102
|
+
if (!response.ok)
|
|
103
|
+
throw await failureOf(response);
|
|
104
|
+
return { sha256: etagOf(response), size: Number(response.headers.get('content-length') ?? 0) };
|
|
105
|
+
}
|
|
106
|
+
async write(path, data, options = {}) {
|
|
107
|
+
const headers = { 'Content-Type': 'application/octet-stream' };
|
|
108
|
+
if (options.ifMatch)
|
|
109
|
+
headers['If-Match'] = options.ifMatch === '*' ? '*' : `"${options.ifMatch}"`;
|
|
110
|
+
if (options.createOnly)
|
|
111
|
+
headers['If-None-Match'] = '*';
|
|
112
|
+
if (options.mutationId)
|
|
113
|
+
headers['X-Mutation-Id'] = options.mutationId;
|
|
114
|
+
const response = await this.call(q(path), {
|
|
115
|
+
method: 'PUT',
|
|
116
|
+
headers,
|
|
117
|
+
body: typeof data === 'string' ? data : data,
|
|
118
|
+
});
|
|
119
|
+
if (!response.ok)
|
|
120
|
+
throw await failureOf(response);
|
|
121
|
+
const body = (await response.json());
|
|
122
|
+
return body;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Several files as one transaction (plan §C.4). A refusal names the file in `failure.path`;
|
|
126
|
+
* nothing was written then.
|
|
127
|
+
*/
|
|
128
|
+
async writeChangeset(entries, options = {}) {
|
|
129
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
130
|
+
if (options.mutationId)
|
|
131
|
+
headers['X-Mutation-Id'] = options.mutationId;
|
|
132
|
+
const files = entries.map(entry => ({
|
|
133
|
+
path: entry.path,
|
|
134
|
+
...(typeof entry.data === 'string' ? { text: entry.data } : { base64: toBase64(entry.data) }),
|
|
135
|
+
...(entry.ifMatch ? { ifMatch: entry.ifMatch } : {}),
|
|
136
|
+
...(entry.createOnly ? { createOnly: true } : {}),
|
|
137
|
+
}));
|
|
138
|
+
const response = await this.call('changeset', {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers,
|
|
141
|
+
body: JSON.stringify({ files }),
|
|
142
|
+
});
|
|
143
|
+
if (!response.ok)
|
|
144
|
+
throw await failureOf(response);
|
|
145
|
+
return (await response.json());
|
|
146
|
+
}
|
|
147
|
+
async mkdir(path) {
|
|
148
|
+
await this.json('mkdir', { path });
|
|
149
|
+
}
|
|
150
|
+
async delete(path, options = {}) {
|
|
151
|
+
await this.json('delete', { path, recursive: options.recursive === true });
|
|
152
|
+
}
|
|
153
|
+
async move(from, to, options = {}) {
|
|
154
|
+
await this.json('move', { from, to, overwrite: options.overwrite === true });
|
|
155
|
+
}
|
|
156
|
+
async manifest() {
|
|
157
|
+
const response = await this.call('manifest');
|
|
158
|
+
if (!response.ok)
|
|
159
|
+
throw await failureOf(response);
|
|
160
|
+
return (await response.json());
|
|
161
|
+
}
|
|
162
|
+
async hash(paths) {
|
|
163
|
+
const body = await this.json('hash', { paths });
|
|
164
|
+
return body.hashes;
|
|
165
|
+
}
|
|
166
|
+
url(path) {
|
|
167
|
+
return `${this.caller.base}${path.split('/').map(encodeURIComponent).join('/')}`;
|
|
168
|
+
}
|
|
169
|
+
}
|