@replayablejs/build 0.1.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 +21 -0
- package/README.md +29 -0
- package/dist/index.d.mts +85 -0
- package/dist/index.mjs +1063 -0
- package/dist/index.mjs.map +1 -0
- package/dist/internal-scope-BxNTO0F3.mjs +55 -0
- package/dist/internal-scope-BxNTO0F3.mjs.map +1 -0
- package/dist/runtime/bindings/adapter.d.mts +6 -0
- package/dist/runtime/bindings/adapter.mjs +10 -0
- package/dist/runtime/bindings/adapter.mjs.map +1 -0
- package/dist/runtime/bindings/assets.d.mts +6 -0
- package/dist/runtime/bindings/assets.mjs +8 -0
- package/dist/runtime/bindings/assets.mjs.map +1 -0
- package/dist/runtime/bindings/definition.d.mts +6 -0
- package/dist/runtime/bindings/definition.mjs +8 -0
- package/dist/runtime/bindings/definition.mjs.map +1 -0
- package/dist/runtime/entries/assets.d.mts +1 -0
- package/dist/runtime/entries/assets.mjs +9 -0
- package/dist/runtime/entries/assets.mjs.map +1 -0
- package/dist/runtime/entries/config.d.mts +1 -0
- package/dist/runtime/entries/config.mjs +13 -0
- package/dist/runtime/entries/config.mjs.map +1 -0
- package/dist/runtime/entries/host.d.mts +1 -0
- package/dist/runtime/entries/host.mjs +9 -0
- package/dist/runtime/entries/host.mjs.map +1 -0
- package/package.json +67 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1063 @@
|
|
|
1
|
+
import { buildAssets } from "@replayablejs/assets";
|
|
2
|
+
import { createVariants, defineConfig } from "@replayablejs/config";
|
|
3
|
+
import { dirname, extname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { build, createServer, mergeConfig, normalizePath } from "vite";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { styleText } from "node:util";
|
|
7
|
+
import { toHtml } from "hast-util-to-html";
|
|
8
|
+
import { h } from "hastscript";
|
|
9
|
+
import { REPLAYABLE_CONTAINER_ID, REPLAYABLE_LOADING_INDICATOR_ID, REPLAYABLE_ROOT_ID } from "@replayablejs/runtime/shell";
|
|
10
|
+
import { rm, writeFile } from "node:fs/promises";
|
|
11
|
+
import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
12
|
+
//#region src/networks/google.ts
|
|
13
|
+
const EXIT_API_URL = "https://tpc.googlesyndication.com/pagead/gadgets/html5/api/exitapi.js";
|
|
14
|
+
/** Resolves the production document elements required by Google App Campaigns. */
|
|
15
|
+
function resolveGoogleHtmlHead(variant) {
|
|
16
|
+
return {
|
|
17
|
+
metaTags: [{
|
|
18
|
+
name: "ad.orientation",
|
|
19
|
+
content: resolveOrientation(variant.screen.orientations)
|
|
20
|
+
}],
|
|
21
|
+
scripts: [{ src: EXIT_API_URL }]
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Converts Replayable's enabled layouts into Google's orientation-tag value. */
|
|
25
|
+
function resolveOrientation(orientations) {
|
|
26
|
+
const { landscape, portrait } = orientations;
|
|
27
|
+
if (landscape.enabled && portrait.enabled) return "portrait,landscape";
|
|
28
|
+
return portrait.enabled ? "portrait" : "landscape";
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/networks/unity.ts
|
|
32
|
+
const unityHtmlHead = {
|
|
33
|
+
metaTags: [],
|
|
34
|
+
scripts: [{ src: "mraid.js" }]
|
|
35
|
+
};
|
|
36
|
+
/** Adds Unity's required MRAID SDK bootstrap before authored runtime code. */
|
|
37
|
+
function resolveUnityHtmlHead() {
|
|
38
|
+
return unityHtmlHead;
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/networks/network-profiles.ts
|
|
42
|
+
const fullScreenEndCard = {
|
|
43
|
+
animation: "continuous",
|
|
44
|
+
interaction: "full-screen"
|
|
45
|
+
};
|
|
46
|
+
const ctaOnlyEndCard = {
|
|
47
|
+
animation: "finite",
|
|
48
|
+
interaction: "cta-only"
|
|
49
|
+
};
|
|
50
|
+
const previewControls = { persistentCta: true };
|
|
51
|
+
const hiddenControls = { persistentCta: false };
|
|
52
|
+
const liftoffControls = { persistentCta: true };
|
|
53
|
+
const noCompileTimeDefinitions = {};
|
|
54
|
+
const emptyHtmlHead = {
|
|
55
|
+
metaTags: [],
|
|
56
|
+
scripts: []
|
|
57
|
+
};
|
|
58
|
+
const networkProfiles = {
|
|
59
|
+
preview: {
|
|
60
|
+
assetMode: "inline",
|
|
61
|
+
applicationMode: "single-module",
|
|
62
|
+
compileTimeDefinitions: noCompileTimeDefinitions,
|
|
63
|
+
completionDurationStart: "ready",
|
|
64
|
+
controls: previewControls,
|
|
65
|
+
endCard: fullScreenEndCard,
|
|
66
|
+
loadingIndicator: "replayable",
|
|
67
|
+
resolveHtmlHead: resolveEmptyHtmlHead,
|
|
68
|
+
runtime: "browser"
|
|
69
|
+
},
|
|
70
|
+
applovin: {
|
|
71
|
+
assetMode: "inline",
|
|
72
|
+
applicationMode: "single-module",
|
|
73
|
+
compileTimeDefinitions: noCompileTimeDefinitions,
|
|
74
|
+
completionDurationStart: "interaction",
|
|
75
|
+
controls: hiddenControls,
|
|
76
|
+
endCard: fullScreenEndCard,
|
|
77
|
+
loadingIndicator: "replayable",
|
|
78
|
+
resolveHtmlHead: resolveEmptyHtmlHead,
|
|
79
|
+
runtime: "applovin"
|
|
80
|
+
},
|
|
81
|
+
meta: {
|
|
82
|
+
assetMode: "inline",
|
|
83
|
+
applicationMode: "single-module",
|
|
84
|
+
compileTimeDefinitions: noCompileTimeDefinitions,
|
|
85
|
+
completionDurationStart: "ready",
|
|
86
|
+
controls: hiddenControls,
|
|
87
|
+
endCard: fullScreenEndCard,
|
|
88
|
+
loadingIndicator: "replayable",
|
|
89
|
+
resolveHtmlHead: resolveEmptyHtmlHead,
|
|
90
|
+
runtime: "meta"
|
|
91
|
+
},
|
|
92
|
+
google: {
|
|
93
|
+
assetMode: "resource",
|
|
94
|
+
applicationMode: "module-graph",
|
|
95
|
+
compileTimeDefinitions: noCompileTimeDefinitions,
|
|
96
|
+
completionDurationStart: "ready",
|
|
97
|
+
controls: hiddenControls,
|
|
98
|
+
endCard: ctaOnlyEndCard,
|
|
99
|
+
loadingIndicator: "replayable",
|
|
100
|
+
resolveHtmlHead: resolveGoogleHtmlHead,
|
|
101
|
+
runtime: "google"
|
|
102
|
+
},
|
|
103
|
+
liftoff: {
|
|
104
|
+
assetMode: "resource",
|
|
105
|
+
applicationMode: "module-graph",
|
|
106
|
+
compileTimeDefinitions: noCompileTimeDefinitions,
|
|
107
|
+
completionDurationStart: "ready",
|
|
108
|
+
controls: liftoffControls,
|
|
109
|
+
endCard: fullScreenEndCard,
|
|
110
|
+
loadingIndicator: "replayable",
|
|
111
|
+
resolveHtmlHead: resolveEmptyHtmlHead,
|
|
112
|
+
runtime: "liftoff"
|
|
113
|
+
},
|
|
114
|
+
mintegral: {
|
|
115
|
+
assetMode: "inline",
|
|
116
|
+
applicationMode: "single-module",
|
|
117
|
+
compileTimeDefinitions: noCompileTimeDefinitions,
|
|
118
|
+
completionDurationStart: "ready",
|
|
119
|
+
controls: hiddenControls,
|
|
120
|
+
endCard: fullScreenEndCard,
|
|
121
|
+
loadingIndicator: "host",
|
|
122
|
+
resolveHtmlHead: resolveEmptyHtmlHead,
|
|
123
|
+
runtime: "mintegral"
|
|
124
|
+
},
|
|
125
|
+
moloco: {
|
|
126
|
+
assetMode: "inline",
|
|
127
|
+
applicationMode: "single-module",
|
|
128
|
+
compileTimeDefinitions: { XMLHttpRequest: "undefined" },
|
|
129
|
+
completionDurationStart: "ready",
|
|
130
|
+
controls: hiddenControls,
|
|
131
|
+
endCard: fullScreenEndCard,
|
|
132
|
+
loadingIndicator: "replayable",
|
|
133
|
+
resolveHtmlHead: resolveEmptyHtmlHead,
|
|
134
|
+
runtime: "moloco"
|
|
135
|
+
},
|
|
136
|
+
unity: {
|
|
137
|
+
assetMode: "inline",
|
|
138
|
+
applicationMode: "single-module",
|
|
139
|
+
compileTimeDefinitions: noCompileTimeDefinitions,
|
|
140
|
+
completionDurationStart: "ready",
|
|
141
|
+
controls: hiddenControls,
|
|
142
|
+
endCard: fullScreenEndCard,
|
|
143
|
+
loadingIndicator: "replayable",
|
|
144
|
+
resolveHtmlHead: resolveUnityHtmlHead,
|
|
145
|
+
runtime: "unity"
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
/** Resolves the asset, document, and runtime policy owned by one variant's network. */
|
|
149
|
+
function resolvePlayableProfile(variant) {
|
|
150
|
+
const profile = networkProfiles[variant.network];
|
|
151
|
+
return {
|
|
152
|
+
assetMode: profile.assetMode,
|
|
153
|
+
applicationMode: profile.applicationMode,
|
|
154
|
+
compileTimeDefinitions: profile.compileTimeDefinitions,
|
|
155
|
+
completionDurationStart: profile.completionDurationStart,
|
|
156
|
+
controls: variant.network === "preview" ? variant.controls : profile.controls,
|
|
157
|
+
endCard: profile.endCard,
|
|
158
|
+
htmlHead: profile.resolveHtmlHead(variant),
|
|
159
|
+
loadingIndicator: profile.loadingIndicator,
|
|
160
|
+
runtime: profile.runtime
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** Returns no document additions for networks without production head requirements. */
|
|
164
|
+
function resolveEmptyHtmlHead(_variant) {
|
|
165
|
+
return emptyHtmlHead;
|
|
166
|
+
}
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region src/runtime/resolve-assets-module.ts
|
|
169
|
+
/** Resolves the generated assets module selected for one playable variant. */
|
|
170
|
+
function resolveAssetsModule(projectRoot, variant) {
|
|
171
|
+
return normalizePath(resolve(projectRoot, variant.assets.emit.assets));
|
|
172
|
+
}
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region src/vite/create-playable-vite-context.ts
|
|
175
|
+
/** Resolves every shared input needed before configuring Vite. */
|
|
176
|
+
function createPlayableViteContext(options) {
|
|
177
|
+
return {
|
|
178
|
+
entryFile: resolve(options.projectRoot, options.variant.entry),
|
|
179
|
+
assetsModule: resolveAssetsModule(options.projectRoot, options.variant),
|
|
180
|
+
profile: resolvePlayableProfile(options.variant),
|
|
181
|
+
projectRoot: options.projectRoot,
|
|
182
|
+
variant: options.variant
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
//#endregion
|
|
186
|
+
//#region src/pipeline/create-development-context.ts
|
|
187
|
+
const DEFAULT_HOST = "0.0.0.0";
|
|
188
|
+
const DEFAULT_PORT = 5173;
|
|
189
|
+
/** Resolves every path, profile, and server option needed by local development. */
|
|
190
|
+
function createDevelopmentContext(variant, options) {
|
|
191
|
+
const viteContext = createPlayableViteContext({
|
|
192
|
+
projectRoot: resolve(options.projectRoot),
|
|
193
|
+
variant
|
|
194
|
+
});
|
|
195
|
+
return {
|
|
196
|
+
...viteContext,
|
|
197
|
+
profile: {
|
|
198
|
+
...viteContext.profile,
|
|
199
|
+
assetMode: "resource"
|
|
200
|
+
},
|
|
201
|
+
host: options.host ?? DEFAULT_HOST,
|
|
202
|
+
open: options.open ?? false,
|
|
203
|
+
port: options.port ?? DEFAULT_PORT
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
//#endregion
|
|
207
|
+
//#region src/vite/create-base-vite-config.ts
|
|
208
|
+
/** Creates environment-independent Vite configuration for one Replayable project. */
|
|
209
|
+
function createBaseViteConfig(projectRoot) {
|
|
210
|
+
return {
|
|
211
|
+
configFile: false,
|
|
212
|
+
envDir: false,
|
|
213
|
+
logLevel: "warn",
|
|
214
|
+
publicDir: false,
|
|
215
|
+
root: projectRoot
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region src/vite/create-development-vite-config.ts
|
|
220
|
+
/** Combines the four production entry configurations into one HMR module graph. */
|
|
221
|
+
function createDevelopmentViteConfig(options) {
|
|
222
|
+
const { application, assets, config, host } = options.entries;
|
|
223
|
+
const entryConfig = [
|
|
224
|
+
host,
|
|
225
|
+
config,
|
|
226
|
+
assets,
|
|
227
|
+
application
|
|
228
|
+
].reduce((current, entry) => mergeConfig(current, entry.viteConfig), {});
|
|
229
|
+
return mergeConfig(createBaseViteConfig(options.projectRoot), mergeConfig(entryConfig, { plugins: [...options.plugins ?? []] }));
|
|
230
|
+
}
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region src/runtime/resolve-build-runtime-module.ts
|
|
233
|
+
const buildDirectory = dirname(fileURLToPath(import.meta.resolve("@replayablejs/build")));
|
|
234
|
+
/** Resolves one private browser module emitted beside the build package entry. */
|
|
235
|
+
function resolveBuildRuntimeModule(modulePath) {
|
|
236
|
+
return resolve(buildDirectory, "runtime", modulePath);
|
|
237
|
+
}
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/runtime/resolve-build-runtime-entry.ts
|
|
240
|
+
/** Resolves one private browser entry emitted with the build package. */
|
|
241
|
+
function resolveBuildRuntimeEntry(entry) {
|
|
242
|
+
return resolveBuildRuntimeModule(`entries/${entry}.mjs`);
|
|
243
|
+
}
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region src/runtime/resolve-devtools.ts
|
|
246
|
+
const devtoolsDirectory = dirname(fileURLToPath(import.meta.resolve("@replayablejs/devtools")));
|
|
247
|
+
/** Audio-disabled variants and production never load the sound control or its icons. */
|
|
248
|
+
function resolveSoundControl(command, enabled) {
|
|
249
|
+
return resolve(devtoolsDirectory, "sound-control", command === "serve" && enabled ? "enabled.js" : "disabled.js");
|
|
250
|
+
}
|
|
251
|
+
/** Production selects a dependency-free stub, regardless of the project's preference. */
|
|
252
|
+
function resolveEndCardTrigger(command, enabled) {
|
|
253
|
+
return resolve(devtoolsDirectory, "endcard-trigger", command === "serve" && enabled ? "enabled.js" : "disabled.js");
|
|
254
|
+
}
|
|
255
|
+
/** Selects stats before module loading; production never visits the enabled graph. */
|
|
256
|
+
function resolveDevtoolsStats(command, stats) {
|
|
257
|
+
const enabled = command === "serve" && stats !== false && Object.values(stats).some((value) => value === true);
|
|
258
|
+
return resolve(devtoolsDirectory, "stats", enabled ? "enabled.js" : "disabled.js");
|
|
259
|
+
}
|
|
260
|
+
/** Context registration is unnecessary unless a WebGL metric is requested in development. */
|
|
261
|
+
function resolveWebglStats(command, stats) {
|
|
262
|
+
const enabled = command === "serve" && stats !== false && (stats.drawCalls || stats.textureBinds || stats.programUses);
|
|
263
|
+
return resolve(devtoolsDirectory, "stats", "webgl", enabled ? "enabled.js" : "disabled.js");
|
|
264
|
+
}
|
|
265
|
+
//#endregion
|
|
266
|
+
//#region src/runtime/resolve-runtime-audio.ts
|
|
267
|
+
const runtimeDirectory$1 = dirname(fileURLToPath(import.meta.resolve("@replayablejs/runtime")));
|
|
268
|
+
/** Returns the enabled or no-op audio module selected for one concrete variant. */
|
|
269
|
+
function resolveRuntimeAudio(enabled) {
|
|
270
|
+
return resolve(runtimeDirectory$1, "audio", enabled ? "enabled.js" : "disabled.js");
|
|
271
|
+
}
|
|
272
|
+
//#endregion
|
|
273
|
+
//#region src/vite/resolve-physical-id.ts
|
|
274
|
+
/** Removes Vite query metadata and normalizes a module ID for comparison. */
|
|
275
|
+
function resolvePhysicalId(id) {
|
|
276
|
+
return normalizePath(id).split("?")[0] ?? id;
|
|
277
|
+
}
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region src/vite/plugins/generated-assets-boundary.ts
|
|
280
|
+
/** Prevents application code from importing the physical generated asset registry. */
|
|
281
|
+
function createGeneratedAssetsBoundaryPlugin(options) {
|
|
282
|
+
const allowedImporter = options.allowedImporter === void 0 ? void 0 : resolvePhysicalId(options.allowedImporter);
|
|
283
|
+
const generatedAssetsModule = resolvePhysicalId(options.assetsModule);
|
|
284
|
+
return {
|
|
285
|
+
name: "replayable:generated-assets-boundary",
|
|
286
|
+
enforce: "pre",
|
|
287
|
+
async resolveId(source, importer) {
|
|
288
|
+
if (importer === void 0) return;
|
|
289
|
+
const resolved = await this.resolve(source, importer, { skipSelf: true });
|
|
290
|
+
if (resolved === null || resolvePhysicalId(resolved.id) !== generatedAssetsModule) return resolved;
|
|
291
|
+
if (allowedImporter !== void 0 && resolvePhysicalId(importer) === allowedImporter) return resolved;
|
|
292
|
+
throw new Error(["The generated assets module is internal to the Replayable build.", "Consume assets through playable.loader instead of importing the generated module directly."].join(" "));
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
//#endregion
|
|
297
|
+
//#region src/vite/create-application-vite-config.ts
|
|
298
|
+
/** Creates Vite configuration for authored application and runtime code only. */
|
|
299
|
+
function createApplicationViteConfig(options, assetsEntry, command) {
|
|
300
|
+
return {
|
|
301
|
+
define: options.profile.compileTimeDefinitions,
|
|
302
|
+
plugins: [createGeneratedAssetsBoundaryPlugin({
|
|
303
|
+
allowedImporter: assetsEntry,
|
|
304
|
+
assetsModule: options.assetsModule
|
|
305
|
+
})],
|
|
306
|
+
resolve: { alias: {
|
|
307
|
+
"#adapter": resolveBuildRuntimeModule("bindings/adapter.mjs"),
|
|
308
|
+
"#assets": resolveBuildRuntimeModule("bindings/assets.mjs"),
|
|
309
|
+
"#audio": resolveRuntimeAudio(options.variant.audio),
|
|
310
|
+
"#definition": resolveBuildRuntimeModule("bindings/definition.mjs"),
|
|
311
|
+
"#stats": resolveDevtoolsStats(command, options.variant.devtools.stats),
|
|
312
|
+
"#endcard-trigger": resolveEndCardTrigger(command, options.variant.devtools.endCardTrigger),
|
|
313
|
+
"#sound-control": resolveSoundControl(command, options.variant.audio && options.variant.devtools.soundControl),
|
|
314
|
+
"#webgl-stats": resolveWebglStats(command, options.variant.devtools.stats)
|
|
315
|
+
} }
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
//#endregion
|
|
319
|
+
//#region src/vite/plugins/generated-assets.ts
|
|
320
|
+
const BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
321
|
+
".avif",
|
|
322
|
+
".glb",
|
|
323
|
+
".gltf",
|
|
324
|
+
".jpeg",
|
|
325
|
+
".jpg",
|
|
326
|
+
".m4a",
|
|
327
|
+
".mp3",
|
|
328
|
+
".ogg",
|
|
329
|
+
".png",
|
|
330
|
+
".skel",
|
|
331
|
+
".wav",
|
|
332
|
+
".webp",
|
|
333
|
+
".woff",
|
|
334
|
+
".woff2"
|
|
335
|
+
]);
|
|
336
|
+
const JSON_EXTENSION = ".json";
|
|
337
|
+
const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
338
|
+
".atlas",
|
|
339
|
+
".frag",
|
|
340
|
+
".glsl",
|
|
341
|
+
".vert"
|
|
342
|
+
]);
|
|
343
|
+
const INLINE_QUERY_BY_KIND = {
|
|
344
|
+
binary: "?url&inline",
|
|
345
|
+
json: void 0,
|
|
346
|
+
text: "?raw"
|
|
347
|
+
};
|
|
348
|
+
/**
|
|
349
|
+
* Applies a profile's asset policy through Vite's native import queries.
|
|
350
|
+
*
|
|
351
|
+
* The generated assets module stays independent of Vite and contains ordinary
|
|
352
|
+
* imports such as:
|
|
353
|
+
*
|
|
354
|
+
* ```ts
|
|
355
|
+
* import image from './resources/logo.webp';
|
|
356
|
+
* import shader from './resources/frag.glsl';
|
|
357
|
+
* import locale from './resources/translations.json';
|
|
358
|
+
* ```
|
|
359
|
+
*
|
|
360
|
+
* Before Vite resolves those imports, this plugin privately attaches the query
|
|
361
|
+
* that describes the required runtime representation:
|
|
362
|
+
*
|
|
363
|
+
* | Imported file | Inline mode | Resource mode |
|
|
364
|
+
* | ------------------- | ------------- | ---------------- |
|
|
365
|
+
* | Image/audio/font | `?url&inline` | `?url&no-inline` |
|
|
366
|
+
* | Binary SKEL | `?url&inline` | `?url&no-inline` |
|
|
367
|
+
* | GLSL/Spine atlas | `?raw` | `?url&no-inline` |
|
|
368
|
+
* | JSON | ordinary JSON | `?url&no-inline` |
|
|
369
|
+
*
|
|
370
|
+
* Only direct imports made by the generated assets module are rewritten.
|
|
371
|
+
* Authored application imports and unrelated JSON or text files retain Vite's
|
|
372
|
+
* normal behavior.
|
|
373
|
+
*/
|
|
374
|
+
function createGeneratedAssetsPlugin(options) {
|
|
375
|
+
const assetsModule = resolvePhysicalId(options.assetsModule);
|
|
376
|
+
return {
|
|
377
|
+
name: "replayable:generated-assets",
|
|
378
|
+
enforce: "pre",
|
|
379
|
+
async resolveId(source, importer) {
|
|
380
|
+
if (importer === void 0 || resolvePhysicalId(importer) !== assetsModule) return;
|
|
381
|
+
const importKind = classifyGeneratedImport(source);
|
|
382
|
+
if (importKind === void 0) return;
|
|
383
|
+
const query = resolveViteQuery(importKind, options.mode);
|
|
384
|
+
if (query === void 0) return;
|
|
385
|
+
return this.resolve(`${source}${query}`, importer, { skipSelf: true });
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
/** Classifies one physical import emitted by the generated assets module. */
|
|
390
|
+
function classifyGeneratedImport(source) {
|
|
391
|
+
const extension = extname(resolvePhysicalId(source)).toLowerCase();
|
|
392
|
+
if (BINARY_EXTENSIONS.has(extension)) return "binary";
|
|
393
|
+
if (TEXT_EXTENSIONS.has(extension)) return "text";
|
|
394
|
+
return extension === JSON_EXTENSION ? "json" : void 0;
|
|
395
|
+
}
|
|
396
|
+
/** Selects the documented Vite query for one asset kind and delivery mode. */
|
|
397
|
+
function resolveViteQuery(kind, mode) {
|
|
398
|
+
return mode === "resource" ? "?url&no-inline" : INLINE_QUERY_BY_KIND[kind];
|
|
399
|
+
}
|
|
400
|
+
//#endregion
|
|
401
|
+
//#region src/vite/create-assets-vite-config.ts
|
|
402
|
+
/** Creates Vite configuration for the generated assets registration only. */
|
|
403
|
+
function createAssetsViteConfig(options) {
|
|
404
|
+
return {
|
|
405
|
+
resolve: { alias: { "#generated-assets": options.assetsModule } },
|
|
406
|
+
plugins: [createGeneratedAssetsPlugin({
|
|
407
|
+
assetsModule: options.assetsModule,
|
|
408
|
+
mode: options.profile.assetMode
|
|
409
|
+
})]
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
//#endregion
|
|
413
|
+
//#region src/runtime/create-runtime-define.ts
|
|
414
|
+
const RUNTIME_DEFINITION = "REPLAYABLE_RUNTIME_DEFINITION";
|
|
415
|
+
/** Creates the platform-neutral value completed by the browser config entry. */
|
|
416
|
+
function createRuntimeDefine(variant, profile) {
|
|
417
|
+
const definition = {
|
|
418
|
+
assetMode: profile.assetMode,
|
|
419
|
+
config: {
|
|
420
|
+
audio: variant.audio,
|
|
421
|
+
backgroundColor: variant.backgroundColor,
|
|
422
|
+
completion: createRuntimeCompletionConfig(variant, profile),
|
|
423
|
+
controls: { persistentCta: profile.controls.persistentCta },
|
|
424
|
+
devtools: variant.devtools,
|
|
425
|
+
endCard: profile.endCard,
|
|
426
|
+
id: variant.id,
|
|
427
|
+
localization: variant.localization,
|
|
428
|
+
network: variant.network,
|
|
429
|
+
params: variant.params,
|
|
430
|
+
screen: variant.screen,
|
|
431
|
+
store: variant.store,
|
|
432
|
+
version: variant.version
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
return { [RUNTIME_DEFINITION]: JSON.stringify(definition) };
|
|
436
|
+
}
|
|
437
|
+
/** Removes disabled timers while adding the network-owned duration start policy. */
|
|
438
|
+
function createRuntimeCompletionConfig(variant, profile) {
|
|
439
|
+
const { duration, inactivity } = variant.completion;
|
|
440
|
+
return {
|
|
441
|
+
durationStart: profile.completionDurationStart,
|
|
442
|
+
...duration === void 0 ? {} : { duration },
|
|
443
|
+
...inactivity === void 0 ? {} : { inactivity }
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
//#endregion
|
|
447
|
+
//#region src/vite/create-config-vite-config.ts
|
|
448
|
+
/** Creates Vite configuration for the resolved runtime definition only. */
|
|
449
|
+
function createConfigViteConfig(options) {
|
|
450
|
+
return { define: createRuntimeDefine(options.variant, options.profile) };
|
|
451
|
+
}
|
|
452
|
+
//#endregion
|
|
453
|
+
//#region src/runtime/resolve-runtime-adapter.ts
|
|
454
|
+
const runtimeDirectory = dirname(fileURLToPath(import.meta.resolve("@replayablejs/runtime")));
|
|
455
|
+
/** Returns the private runtime adapter module selected by Replayable. */
|
|
456
|
+
function resolveRuntimeAdapter(host) {
|
|
457
|
+
return resolve(runtimeDirectory, "adapters", `${host}.js`);
|
|
458
|
+
}
|
|
459
|
+
//#endregion
|
|
460
|
+
//#region src/vite/create-host-vite-config.ts
|
|
461
|
+
/** Creates Vite configuration for the profile-selected network host only. */
|
|
462
|
+
function createHostViteConfig(options) {
|
|
463
|
+
return { resolve: { alias: { "#selected-adapter": resolveRuntimeAdapter(options.profile.runtime) } } };
|
|
464
|
+
}
|
|
465
|
+
//#endregion
|
|
466
|
+
//#region src/vite/create-playable-entries.ts
|
|
467
|
+
/**
|
|
468
|
+
* Describes the four private module entries shared by development and production.
|
|
469
|
+
*
|
|
470
|
+
* Production builds each descriptor independently. Development merges their Vite
|
|
471
|
+
* configuration into one live graph and serves the same inputs in execution order.
|
|
472
|
+
*/
|
|
473
|
+
function createPlayableEntries(context, command) {
|
|
474
|
+
const assetsEntry = resolveBuildRuntimeEntry("assets");
|
|
475
|
+
return {
|
|
476
|
+
host: {
|
|
477
|
+
input: resolveBuildRuntimeEntry("host"),
|
|
478
|
+
viteConfig: createHostViteConfig(context)
|
|
479
|
+
},
|
|
480
|
+
config: {
|
|
481
|
+
input: resolveBuildRuntimeEntry("config"),
|
|
482
|
+
viteConfig: createConfigViteConfig(context)
|
|
483
|
+
},
|
|
484
|
+
assets: {
|
|
485
|
+
input: assetsEntry,
|
|
486
|
+
viteConfig: createAssetsViteConfig(context)
|
|
487
|
+
},
|
|
488
|
+
application: {
|
|
489
|
+
input: context.entryFile,
|
|
490
|
+
viteConfig: createApplicationViteConfig(context, assetsEntry, command)
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
//#endregion
|
|
495
|
+
//#region src/development/plugins/development-reporter.ts
|
|
496
|
+
const UPDATE_LABELS = {
|
|
497
|
+
create: "Created",
|
|
498
|
+
delete: "Deleted",
|
|
499
|
+
update: "Updated"
|
|
500
|
+
};
|
|
501
|
+
const DUPLICATE_UPDATE_WINDOW = 100;
|
|
502
|
+
/** Reports Vite file updates without exposing Vite as the public development interface. */
|
|
503
|
+
function createDevelopmentReporterPlugin(projectRoot) {
|
|
504
|
+
const recentUpdates = /* @__PURE__ */ new Map();
|
|
505
|
+
return {
|
|
506
|
+
name: "replayable:development-reporter",
|
|
507
|
+
hotUpdate(options) {
|
|
508
|
+
if (this.environment.name !== "client") return;
|
|
509
|
+
const file = normalizePath(relative(projectRoot, options.file));
|
|
510
|
+
const startedAt = performance.now();
|
|
511
|
+
const previousUpdate = recentUpdates.get(file);
|
|
512
|
+
if (previousUpdate !== void 0 && startedAt - previousUpdate < DUPLICATE_UPDATE_WINDOW) return;
|
|
513
|
+
recentUpdates.set(file, startedAt);
|
|
514
|
+
setImmediate(() => {
|
|
515
|
+
const duration = Math.round(performance.now() - startedAt);
|
|
516
|
+
const label = styleText("green", UPDATE_LABELS[options.type].padEnd(9));
|
|
517
|
+
const formattedDuration = duration === 0 ? "<1ms" : `${duration}ms`;
|
|
518
|
+
const details = styleText("dim", `${file} in ${formattedDuration}`);
|
|
519
|
+
console.log(`${label} ${details}`);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
//#endregion
|
|
525
|
+
//#region src/html/playable-html.ts
|
|
526
|
+
/** Canonical entry document produced by every Replayable build. */
|
|
527
|
+
const PLAYABLE_HTML_FILE = "index.html";
|
|
528
|
+
//#endregion
|
|
529
|
+
//#region src/html/loading-indicator.scss?inline
|
|
530
|
+
var loading_indicator_default = "#replayable-loading-indicator{z-index:1;pointer-events:none;place-items:center;display:grid;position:absolute;inset:0}#replayable-loading-indicator:after{content:\"\";border:.125rem solid #7f7f7f73;border-top-color:#fff;border-radius:50%;width:1.5rem;height:1.5rem;animation:.8s linear infinite replayable-loading-spin}@keyframes replayable-loading-spin{to{transform:rotate(1turn)}}@media (prefers-reduced-motion:reduce){#replayable-loading-indicator:after{animation:none}}\n";
|
|
531
|
+
//#endregion
|
|
532
|
+
//#region src/html/playable-shell.scss?inline
|
|
533
|
+
var playable_shell_default = "html,body{overscroll-behavior:none;width:100%;height:100%;margin:0;overflow:hidden}body{background-color:inherit;position:fixed;inset:0}#replayable-root{background-color:inherit;-webkit-user-select:none;user-select:none;-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent;touch-action:none;position:fixed;inset:0;overflow:hidden}#replayable-container{background-color:inherit;position:absolute;overflow:hidden}#replayable-root:before{content:\"\";visibility:hidden;pointer-events:none;padding:max(env(safe-area-inset-top,0px), clamp(36px, 5svh, 44px)) max(env(safe-area-inset-right,0px), 12px) max(env(safe-area-inset-bottom,0px), clamp(24px, 4svh, 36px)) max(env(safe-area-inset-left,0px), 12px);position:absolute}#replayable-root[data-orientation=landscape]:before{padding:max(env(safe-area-inset-top,0px), 12px) max(env(safe-area-inset-right,0px), clamp(48px, 7svw, 64px)) max(env(safe-area-inset-bottom,0px), clamp(20px, 4svh, 32px)) max(env(safe-area-inset-left,0px), clamp(48px, 7svw, 64px))}\n";
|
|
534
|
+
//#endregion
|
|
535
|
+
//#region src/html/playable-shell.ts
|
|
536
|
+
/** Returns only the framework styles required by the active loading owner. */
|
|
537
|
+
function resolvePlayableShellStyles(loadingIndicator) {
|
|
538
|
+
if (loadingIndicator === "host") return playable_shell_default;
|
|
539
|
+
return `${playable_shell_default}${loading_indicator_default}`;
|
|
540
|
+
}
|
|
541
|
+
/** Renders the framework mount points shared by DOM and canvas playables. */
|
|
542
|
+
function renderPlayableShell(loadingIndicator) {
|
|
543
|
+
const children = [h("div", { id: REPLAYABLE_CONTAINER_ID })];
|
|
544
|
+
if (loadingIndicator === "replayable") children.unshift(h("div", {
|
|
545
|
+
id: REPLAYABLE_LOADING_INDICATOR_ID,
|
|
546
|
+
role: "status",
|
|
547
|
+
ariaLabel: "Loading"
|
|
548
|
+
}));
|
|
549
|
+
return h("div", { id: REPLAYABLE_ROOT_ID }, children);
|
|
550
|
+
}
|
|
551
|
+
//#endregion
|
|
552
|
+
//#region src/html/render-playable-html.ts
|
|
553
|
+
/** Renders the shared playable document used by development and production. */
|
|
554
|
+
function renderPlayableHtml(variant, resources) {
|
|
555
|
+
const networkMetaTags = resources.head.metaTags.map(({ name, content }) => h("meta", {
|
|
556
|
+
name,
|
|
557
|
+
content
|
|
558
|
+
}));
|
|
559
|
+
const stylesheets = resources.stylesheets.map((stylesheet) => h("link", {
|
|
560
|
+
href: stylesheet,
|
|
561
|
+
rel: "stylesheet"
|
|
562
|
+
}));
|
|
563
|
+
const networkScripts = resources.head.scripts.map(({ src }) => h("script", { src }));
|
|
564
|
+
const initializationScripts = renderInitializationScripts(resources.initializers);
|
|
565
|
+
const shellStyles = resolvePlayableShellStyles(resources.loadingIndicator);
|
|
566
|
+
const document = h(null, [{ type: "doctype" }, h("html", {
|
|
567
|
+
lang: variant.localization.language,
|
|
568
|
+
style: { "background-color": variant.backgroundColor }
|
|
569
|
+
}, [h("head", [
|
|
570
|
+
h("meta", { charSet: "UTF-8" }),
|
|
571
|
+
h("meta", {
|
|
572
|
+
name: "viewport",
|
|
573
|
+
content: "width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
|
|
574
|
+
}),
|
|
575
|
+
...networkMetaTags,
|
|
576
|
+
h("title", variant.projectName),
|
|
577
|
+
h("style", shellStyles),
|
|
578
|
+
...stylesheets,
|
|
579
|
+
...networkScripts
|
|
580
|
+
]), h("body", [
|
|
581
|
+
renderPlayableShell(resources.loadingIndicator),
|
|
582
|
+
...initializationScripts,
|
|
583
|
+
h("script", {
|
|
584
|
+
"data-replayable-entry": "application",
|
|
585
|
+
src: resources.entry,
|
|
586
|
+
type: "module"
|
|
587
|
+
})
|
|
588
|
+
])])]);
|
|
589
|
+
return `${toHtml(document)}\n`;
|
|
590
|
+
}
|
|
591
|
+
/** Renders scope initialization in its required execution order. */
|
|
592
|
+
function renderInitializationScripts(initializers) {
|
|
593
|
+
if (initializers === void 0) return [];
|
|
594
|
+
return [
|
|
595
|
+
h("script", {
|
|
596
|
+
"data-replayable-entry": "host",
|
|
597
|
+
src: initializers.host,
|
|
598
|
+
type: "module"
|
|
599
|
+
}),
|
|
600
|
+
h("script", {
|
|
601
|
+
"data-replayable-entry": "config",
|
|
602
|
+
src: initializers.config,
|
|
603
|
+
type: "module"
|
|
604
|
+
}),
|
|
605
|
+
h("script", {
|
|
606
|
+
"data-replayable-entry": "assets",
|
|
607
|
+
src: initializers.assets,
|
|
608
|
+
type: "module"
|
|
609
|
+
})
|
|
610
|
+
];
|
|
611
|
+
}
|
|
612
|
+
//#endregion
|
|
613
|
+
//#region src/development/plugins/playable-html.ts
|
|
614
|
+
/** Serves framework-owned HTML while leaving modules and HMR to Vite. */
|
|
615
|
+
function createPlayableHtmlPlugin(variant, resources) {
|
|
616
|
+
return {
|
|
617
|
+
name: "replayable:html",
|
|
618
|
+
configureServer(server) {
|
|
619
|
+
server.middlewares.use(async (request, response, next) => {
|
|
620
|
+
if (!isHtmlRequest(request.url)) {
|
|
621
|
+
next();
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
try {
|
|
625
|
+
const source = renderPlayableHtml(variant, resources);
|
|
626
|
+
const html = await server.transformIndexHtml(request.url ?? "/", source);
|
|
627
|
+
response.statusCode = 200;
|
|
628
|
+
response.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
629
|
+
response.end(html);
|
|
630
|
+
} catch (error) {
|
|
631
|
+
next(error);
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
/** Matches only the development document, never module or asset requests. */
|
|
638
|
+
function isHtmlRequest(requestUrl) {
|
|
639
|
+
if (requestUrl === void 0) return false;
|
|
640
|
+
const pathname = new URL(requestUrl, "http://replayable.local").pathname;
|
|
641
|
+
return pathname === "/" || pathname === `/index.html`;
|
|
642
|
+
}
|
|
643
|
+
//#endregion
|
|
644
|
+
//#region src/development/create-development-server.ts
|
|
645
|
+
/** Starts Vite for one resolved playable variant and its authored browser entry. */
|
|
646
|
+
async function createDevelopmentServer(context) {
|
|
647
|
+
const entries = createPlayableEntries(context, "serve");
|
|
648
|
+
const viteConfig = createDevelopmentViteConfig({
|
|
649
|
+
entries,
|
|
650
|
+
projectRoot: context.projectRoot,
|
|
651
|
+
plugins: [createPlayableHtmlPlugin(context.variant, {
|
|
652
|
+
entry: toDevelopmentModuleUrl(entries.application.input),
|
|
653
|
+
initializers: {
|
|
654
|
+
assets: toDevelopmentModuleUrl(entries.assets.input),
|
|
655
|
+
config: toDevelopmentModuleUrl(entries.config.input),
|
|
656
|
+
host: toDevelopmentModuleUrl(entries.host.input)
|
|
657
|
+
},
|
|
658
|
+
head: context.profile.htmlHead,
|
|
659
|
+
loadingIndicator: context.profile.loadingIndicator,
|
|
660
|
+
stylesheets: []
|
|
661
|
+
}), createDevelopmentReporterPlugin(context.projectRoot)]
|
|
662
|
+
});
|
|
663
|
+
const server = await createServer({
|
|
664
|
+
...viteConfig,
|
|
665
|
+
appType: "custom",
|
|
666
|
+
server: {
|
|
667
|
+
host: context.host,
|
|
668
|
+
open: context.open,
|
|
669
|
+
port: context.port
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
await server.listen();
|
|
673
|
+
return server;
|
|
674
|
+
}
|
|
675
|
+
/** Converts an absolute private entry path into Vite's browser-facing filesystem URL. */
|
|
676
|
+
function toDevelopmentModuleUrl(file) {
|
|
677
|
+
return `/@fs/${normalizePath(file)}`;
|
|
678
|
+
}
|
|
679
|
+
//#endregion
|
|
680
|
+
//#region src/development/select-development-variant.ts
|
|
681
|
+
/** Selects the preview variant that satisfies every explicit development choice. */
|
|
682
|
+
function selectDevelopmentVariant(variants, selection) {
|
|
683
|
+
const variant = variants.find((candidate) => candidate.network === "preview" && (selection.version === void 0 || candidate.version === selection.version) && (selection.language === void 0 || candidate.localization.language === selection.language));
|
|
684
|
+
if (variant === void 0) throw new Error(`No configured playable variant matches ${describeSelection(selection)}.`);
|
|
685
|
+
return variant;
|
|
686
|
+
}
|
|
687
|
+
/** Produces a concise, actionable description for an invalid selection. */
|
|
688
|
+
function describeSelection(selection) {
|
|
689
|
+
return [
|
|
690
|
+
`version=${selection.version ?? "*"}`,
|
|
691
|
+
"network=preview",
|
|
692
|
+
`language=${selection.language ?? "*"}`
|
|
693
|
+
].join(", ");
|
|
694
|
+
}
|
|
695
|
+
//#endregion
|
|
696
|
+
//#region src/development/serve-preview.ts
|
|
697
|
+
/** Builds one preview variant's assets and starts its local Vite server. */
|
|
698
|
+
async function servePreview(config, options) {
|
|
699
|
+
const validatedConfig = defineConfig(config);
|
|
700
|
+
const context = createDevelopmentContext(selectDevelopmentVariant(createVariants(validatedConfig), options), options);
|
|
701
|
+
await buildAssets(context.variant.assets, context.projectRoot);
|
|
702
|
+
const server = await createDevelopmentServer(context);
|
|
703
|
+
const resolvedUrls = server.resolvedUrls;
|
|
704
|
+
return {
|
|
705
|
+
close: () => server.close(),
|
|
706
|
+
localUrls: resolvedUrls?.local ?? [],
|
|
707
|
+
networkUrls: resolvedUrls?.network ?? [],
|
|
708
|
+
variantId: context.variant.id
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
//#endregion
|
|
712
|
+
//#region src/pipeline/resolve-output-directory.ts
|
|
713
|
+
/** Resolves an output path while preventing project-wide recursive deletion. */
|
|
714
|
+
function resolveOutputDirectory(projectRoot, configuredPath) {
|
|
715
|
+
const outputDirectory = resolve(projectRoot, configuredPath);
|
|
716
|
+
const projectRelativePath = relative(projectRoot, outputDirectory);
|
|
717
|
+
if (projectRelativePath === "" || isOutsideProject(projectRelativePath)) throw new Error("The build output must resolve to a directory inside the project root.");
|
|
718
|
+
const physicalRoot = realpathSync.native(projectRoot);
|
|
719
|
+
const physicalOutput = resolvePhysicalPath(outputDirectory);
|
|
720
|
+
const physicalRelativePath = relative(physicalRoot, physicalOutput);
|
|
721
|
+
if (physicalRelativePath === "" || isOutsideProject(physicalRelativePath)) throw new Error("The build output must resolve to a directory inside the project root.");
|
|
722
|
+
return outputDirectory;
|
|
723
|
+
}
|
|
724
|
+
/** Resolves symlinks in the nearest existing ancestor of a future output path. */
|
|
725
|
+
function resolvePhysicalPath(path) {
|
|
726
|
+
if (lstatSync(path, { throwIfNoEntry: false }) !== void 0) {
|
|
727
|
+
const physicalPath = realpathSync.native(path);
|
|
728
|
+
if (!statSync(physicalPath).isDirectory()) throw new Error(`ENOTDIR: build output ancestor is not a directory: ${path}`);
|
|
729
|
+
return physicalPath;
|
|
730
|
+
}
|
|
731
|
+
const parent = dirname(path);
|
|
732
|
+
return resolve(resolvePhysicalPath(parent), relative(parent, path));
|
|
733
|
+
}
|
|
734
|
+
/** Identifies relative paths that resolve beyond their intended parent directory. */
|
|
735
|
+
function isOutsideProject(relativePath) {
|
|
736
|
+
return relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath);
|
|
737
|
+
}
|
|
738
|
+
//#endregion
|
|
739
|
+
//#region src/html/emit-playable-html.ts
|
|
740
|
+
/** Writes the runnable HTML document into its prepared variant output. */
|
|
741
|
+
async function emitPlayableHtml(context, bundle) {
|
|
742
|
+
const source = renderPlayableHtml(context.variant, {
|
|
743
|
+
entry: `./${bundle.entries.application}`,
|
|
744
|
+
head: context.profile.htmlHead,
|
|
745
|
+
initializers: {
|
|
746
|
+
assets: `./${bundle.entries.assets}`,
|
|
747
|
+
config: `./${bundle.entries.config}`,
|
|
748
|
+
host: `./${bundle.entries.host}`
|
|
749
|
+
},
|
|
750
|
+
loadingIndicator: context.profile.loadingIndicator,
|
|
751
|
+
stylesheets: bundle.stylesheets.map((stylesheet) => `./${stylesheet}`)
|
|
752
|
+
});
|
|
753
|
+
await writeFile(context.htmlFile, source, "utf8");
|
|
754
|
+
}
|
|
755
|
+
//#endregion
|
|
756
|
+
//#region src/pipeline/create-build-context.ts
|
|
757
|
+
/** Resolves every authored and generated path needed to build one variant. */
|
|
758
|
+
function createBuildContext(variant, options) {
|
|
759
|
+
const projectRoot = resolve(options.projectRoot);
|
|
760
|
+
const outputDirectory = resolveOutputDirectory(projectRoot, options.outputDirectory);
|
|
761
|
+
return {
|
|
762
|
+
...createPlayableViteContext({
|
|
763
|
+
projectRoot,
|
|
764
|
+
variant
|
|
765
|
+
}),
|
|
766
|
+
outputDirectory,
|
|
767
|
+
htmlFile: join(outputDirectory, PLAYABLE_HTML_FILE)
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
//#endregion
|
|
771
|
+
//#region src/browser-targets.ts
|
|
772
|
+
/** Minimum browser engines supported by compiled playable output. */
|
|
773
|
+
const PLAYABLE_BROWSER_TARGETS = ["ios16.1", "chrome105"];
|
|
774
|
+
//#endregion
|
|
775
|
+
//#region src/production/create-bundle-output.ts
|
|
776
|
+
const STYLESHEET_EXTENSION = ".css";
|
|
777
|
+
const standaloneEntryRoles = [
|
|
778
|
+
"host",
|
|
779
|
+
"config",
|
|
780
|
+
"assets"
|
|
781
|
+
];
|
|
782
|
+
const entryRoles = [...standaloneEntryRoles, "application"];
|
|
783
|
+
/** Combines four role-specific Rolldown results into one HTML-facing description. */
|
|
784
|
+
function createBundleOutput(outputs, applicationMode) {
|
|
785
|
+
const entries = {
|
|
786
|
+
host: requireEntryFile(outputs.host, "host"),
|
|
787
|
+
config: requireEntryFile(outputs.config, "config"),
|
|
788
|
+
assets: requireEntryFile(outputs.assets, "assets"),
|
|
789
|
+
application: requireEntryFile(outputs.application, "application")
|
|
790
|
+
};
|
|
791
|
+
validateBundleContract(outputs, entries, applicationMode);
|
|
792
|
+
const emittedAssets = Object.values(outputs).flat().filter((chunkOrAsset) => chunkOrAsset.type === "asset");
|
|
793
|
+
return {
|
|
794
|
+
entries,
|
|
795
|
+
chunks: outputs.application.filter((chunkOrAsset) => chunkOrAsset.type === "chunk" && !chunkOrAsset.isEntry).map(({ fileName }) => fileName).sort(),
|
|
796
|
+
stylesheets: collectAssetFiles(emittedAssets, true),
|
|
797
|
+
resources: collectAssetFiles(emittedAssets, false)
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
/** Enforces the entry isolation required by the runtime registration protocol. */
|
|
801
|
+
function validateBundleContract(outputs, entries, applicationMode) {
|
|
802
|
+
const entryFiles = Object.values(entries);
|
|
803
|
+
if (new Set(entryFiles).size !== entryFiles.length) throw new Error(`Playable entry filenames must be unique; received ${entryFiles.join(", ")}.`);
|
|
804
|
+
for (const role of standaloneEntryRoles) assertStandaloneEntry(outputs[role], role);
|
|
805
|
+
if (applicationMode === "single-module") assertStandaloneEntry(outputs.application, "application");
|
|
806
|
+
for (const role of entryRoles) assertNoCrossEntryImports(outputs[role], role, entries);
|
|
807
|
+
}
|
|
808
|
+
/** Requires a role configured without code splitting to emit exactly one chunk. */
|
|
809
|
+
function assertStandaloneEntry(output, role) {
|
|
810
|
+
const chunks = output.filter((chunkOrAsset) => chunkOrAsset.type === "chunk");
|
|
811
|
+
if (chunks.length !== 1) throw new Error(`Expected standalone ${role} entry to emit one chunk, received ${chunks.length}.`);
|
|
812
|
+
}
|
|
813
|
+
/** Rejects dependencies on another private entry while permitting application chunks. */
|
|
814
|
+
function assertNoCrossEntryImports(output, role, entries) {
|
|
815
|
+
const forbiddenEntryFiles = new Set(Object.entries(entries).filter(([entryRole]) => entryRole !== role).map(([, entryFile]) => entryFile));
|
|
816
|
+
for (const chunk of output) {
|
|
817
|
+
if (chunk.type !== "chunk") continue;
|
|
818
|
+
const forbiddenImport = [...chunk.imports, ...chunk.dynamicImports].find((file) => forbiddenEntryFiles.has(file));
|
|
819
|
+
if (forbiddenImport !== void 0) throw new Error(`Playable ${role} entry imports private entry ${forbiddenImport}.`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
/** Returns the one entry emitted by a role-specific build. */
|
|
823
|
+
function requireEntryFile(output, role) {
|
|
824
|
+
const entryFiles = output.filter((chunkOrAsset) => chunkOrAsset.type === "chunk" && chunkOrAsset.isEntry).map(({ fileName }) => fileName);
|
|
825
|
+
const entryFile = entryFiles[0];
|
|
826
|
+
if (entryFile === void 0 || entryFiles.length !== 1) throw new Error(`Expected one ${role} entry, received ${entryFiles.length}.`);
|
|
827
|
+
return entryFile;
|
|
828
|
+
}
|
|
829
|
+
/** Collects unique stylesheet or non-stylesheet asset paths in stable order. */
|
|
830
|
+
function collectAssetFiles(assets, stylesheets) {
|
|
831
|
+
return [...new Set(assets.filter(({ fileName }) => fileName.endsWith(STYLESHEET_EXTENSION) === stylesheets).map(({ fileName }) => fileName))].sort();
|
|
832
|
+
}
|
|
833
|
+
//#endregion
|
|
834
|
+
//#region src/production/validate-application-module-graph.ts
|
|
835
|
+
/**
|
|
836
|
+
* Browser packages whose state or object identity cannot safely be duplicated.
|
|
837
|
+
*
|
|
838
|
+
* This list is intentionally local to `@replayablejs/build`: unlike the repository
|
|
839
|
+
* dependency check, this validator ships to consumers and must work independently.
|
|
840
|
+
*/
|
|
841
|
+
const SINGLETON_PACKAGES = [
|
|
842
|
+
"@esotericsoftware/spine-core",
|
|
843
|
+
"@esotericsoftware/spine-pixi-v8",
|
|
844
|
+
"@replayablejs/runtime",
|
|
845
|
+
"motion",
|
|
846
|
+
"motion-dom",
|
|
847
|
+
"pixi.js"
|
|
848
|
+
];
|
|
849
|
+
/**
|
|
850
|
+
* Rejects singleton packages bundled from more than one physical installation.
|
|
851
|
+
*
|
|
852
|
+
* Rolldown may include the same package in several chunks; those modules are
|
|
853
|
+
* valid when their nearest package.json resolves to the same real directory.
|
|
854
|
+
* Distinct directories indicate separate caches, class identities, or frame
|
|
855
|
+
* schedulers that cannot safely coexist inside one playable application.
|
|
856
|
+
*
|
|
857
|
+
* Only the application build belongs here. Replayable deliberately builds host,
|
|
858
|
+
* config, and assets as separate executable entries; combining all four module
|
|
859
|
+
* graphs would confuse that isolation with duplicate browser package instances.
|
|
860
|
+
*
|
|
861
|
+
* A protected package may be absent—for example, a DOM-only playable contains
|
|
862
|
+
* no PixiJS. Presence is enforced by authored imports; this function only rejects
|
|
863
|
+
* multiple installations of a package that actually entered the application.
|
|
864
|
+
*/
|
|
865
|
+
function validateApplicationModuleGraph(output) {
|
|
866
|
+
const packageInstances = /* @__PURE__ */ new Map();
|
|
867
|
+
const packageCache = /* @__PURE__ */ new Map();
|
|
868
|
+
for (const chunkOrAsset of output) {
|
|
869
|
+
if (chunkOrAsset.type !== "chunk") continue;
|
|
870
|
+
for (const moduleId of Object.keys(chunkOrAsset.modules)) {
|
|
871
|
+
const packageInstance = resolvePackageInstance(moduleId, packageCache);
|
|
872
|
+
if (!isSingletonPackage(packageInstance?.name)) continue;
|
|
873
|
+
const instances = packageInstances.get(packageInstance.name) ?? /* @__PURE__ */ new Set();
|
|
874
|
+
instances.add(packageInstance.directory);
|
|
875
|
+
packageInstances.set(packageInstance.name, instances);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
for (const packageName of SINGLETON_PACKAGES) {
|
|
879
|
+
const instances = packageInstances.get(packageName);
|
|
880
|
+
if (instances !== void 0 && instances.size > 1) throw new Error(`Playable application contains multiple ${packageName} instances: ${[...instances].join(", ")}.`);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
/** Resolves the nearest package.json that owns one filesystem-backed module. */
|
|
884
|
+
function resolvePackageInstance(moduleId, cache) {
|
|
885
|
+
const filePath = moduleId.replace(/[?#].*$/, "");
|
|
886
|
+
if (!isAbsolute(filePath)) return null;
|
|
887
|
+
let directory = dirname(filePath);
|
|
888
|
+
const visitedDirectories = [];
|
|
889
|
+
while (directory !== parse(directory).root) {
|
|
890
|
+
const cachedInstance = cache.get(directory);
|
|
891
|
+
if (cachedInstance !== void 0 || cache.has(directory)) {
|
|
892
|
+
cacheDirectories(visitedDirectories, cachedInstance ?? null, cache);
|
|
893
|
+
return cachedInstance ?? null;
|
|
894
|
+
}
|
|
895
|
+
visitedDirectories.push(directory);
|
|
896
|
+
const manifestPath = join(directory, "package.json");
|
|
897
|
+
if (existsSync(manifestPath)) {
|
|
898
|
+
const packageInstance = readPackageInstance(JSON.parse(readFileSync(manifestPath, "utf8")), directory);
|
|
899
|
+
cacheDirectories(visitedDirectories, packageInstance, cache);
|
|
900
|
+
return packageInstance;
|
|
901
|
+
}
|
|
902
|
+
directory = dirname(directory);
|
|
903
|
+
}
|
|
904
|
+
cacheDirectories(visitedDirectories, null, cache);
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Retains one package lookup for every traversed directory.
|
|
909
|
+
*
|
|
910
|
+
* Many modules share nested directories. Caching the whole walk prevents repeated
|
|
911
|
+
* filesystem traversal for every file in a large dependency such as PixiJS.
|
|
912
|
+
*/
|
|
913
|
+
function cacheDirectories(directories, packageInstance, cache) {
|
|
914
|
+
for (const directory of directories) cache.set(directory, packageInstance);
|
|
915
|
+
}
|
|
916
|
+
/** Reads only the package identity required by singleton validation. */
|
|
917
|
+
function readPackageInstance(manifest, directory) {
|
|
918
|
+
if (typeof manifest !== "object" || manifest === null || !("name" in manifest) || typeof manifest.name !== "string") return null;
|
|
919
|
+
return {
|
|
920
|
+
directory: realpathSync.native(directory),
|
|
921
|
+
name: manifest.name
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
/** Narrows an arbitrary package name to the protected singleton set. */
|
|
925
|
+
function isSingletonPackage(name) {
|
|
926
|
+
return name !== void 0 && SINGLETON_PACKAGES.some((packageName) => packageName === name);
|
|
927
|
+
}
|
|
928
|
+
//#endregion
|
|
929
|
+
//#region src/production/bundle-variant.ts
|
|
930
|
+
/** Builds independently executable host, config, assets, and application entries. */
|
|
931
|
+
async function bundleVariant(context) {
|
|
932
|
+
const entries = createPlayableEntries(context, "build");
|
|
933
|
+
const hostOutput = await buildEntry(context, entries.host, {
|
|
934
|
+
emptyOutputDirectory: true,
|
|
935
|
+
output: {
|
|
936
|
+
codeSplitting: false,
|
|
937
|
+
format: "es"
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
const configOutput = await buildEntry(context, entries.config, {
|
|
941
|
+
emptyOutputDirectory: false,
|
|
942
|
+
output: {
|
|
943
|
+
codeSplitting: false,
|
|
944
|
+
format: "es"
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
const assetsOutput = await buildEntry(context, entries.assets, {
|
|
948
|
+
emptyOutputDirectory: false,
|
|
949
|
+
output: {
|
|
950
|
+
codeSplitting: false,
|
|
951
|
+
format: "es"
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
const applicationOutput = await buildEntry(context, entries.application, {
|
|
955
|
+
emptyOutputDirectory: false,
|
|
956
|
+
output: {
|
|
957
|
+
codeSplitting: context.profile.applicationMode === "module-graph",
|
|
958
|
+
format: "es"
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
validateApplicationModuleGraph(applicationOutput);
|
|
962
|
+
return createBundleOutput({
|
|
963
|
+
host: hostOutput,
|
|
964
|
+
config: configOutput,
|
|
965
|
+
assets: assetsOutput,
|
|
966
|
+
application: applicationOutput
|
|
967
|
+
}, context.profile.applicationMode);
|
|
968
|
+
}
|
|
969
|
+
/** Runs one role-specific Vite build into the shared variant directory. */
|
|
970
|
+
async function buildEntry(context, entry, options) {
|
|
971
|
+
const buildResult = await build({
|
|
972
|
+
...createBaseViteConfig(context.projectRoot),
|
|
973
|
+
...entry.viteConfig,
|
|
974
|
+
base: "./",
|
|
975
|
+
build: {
|
|
976
|
+
assetsInlineLimit: context.profile.assetMode === "inline" ? Infinity : 0,
|
|
977
|
+
chunkSizeWarningLimit: Infinity,
|
|
978
|
+
cssTarget: [...PLAYABLE_BROWSER_TARGETS],
|
|
979
|
+
emptyOutDir: options.emptyOutputDirectory,
|
|
980
|
+
outDir: context.outputDirectory,
|
|
981
|
+
rolldownOptions: {
|
|
982
|
+
input: entry.input,
|
|
983
|
+
onLog(level, log, defaultHandler) {
|
|
984
|
+
defaultHandler(level === "warn" ? "error" : level, log);
|
|
985
|
+
},
|
|
986
|
+
output: options.output
|
|
987
|
+
},
|
|
988
|
+
target: [...PLAYABLE_BROWSER_TARGETS]
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
if (!("output" in buildResult)) throw new Error("Vite did not return a single playable build output.");
|
|
992
|
+
return buildResult.output;
|
|
993
|
+
}
|
|
994
|
+
//#endregion
|
|
995
|
+
//#region src/production/build-variant.ts
|
|
996
|
+
/**
|
|
997
|
+
* Produces one runnable playable from a concrete configuration variant.
|
|
998
|
+
*
|
|
999
|
+
* @param variant - Fully resolved version, network, and language combination.
|
|
1000
|
+
* @param options - Explicit project root and variant output directory.
|
|
1001
|
+
*/
|
|
1002
|
+
async function buildVariant(variant, options) {
|
|
1003
|
+
const context = createBuildContext(variant, options);
|
|
1004
|
+
const assets = await buildAssets(context.variant.assets, context.projectRoot);
|
|
1005
|
+
await emitPlayableHtml(context, await bundleVariant(context));
|
|
1006
|
+
return {
|
|
1007
|
+
assets,
|
|
1008
|
+
htmlFile: context.htmlFile,
|
|
1009
|
+
outputDirectory: context.outputDirectory,
|
|
1010
|
+
variantId: context.variant.id
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
//#endregion
|
|
1014
|
+
//#region src/production/build-project.ts
|
|
1015
|
+
/**
|
|
1016
|
+
* Builds every concrete variant configured by one Replayable project.
|
|
1017
|
+
*
|
|
1018
|
+
* Variants are built sequentially because their asset builds share generated
|
|
1019
|
+
* source paths. The project output is reset once so variants removed from the
|
|
1020
|
+
* configuration cannot leave stale playable directories behind.
|
|
1021
|
+
* The first variant is restored afterward as a deterministic baseline. This
|
|
1022
|
+
* does not isolate an active preview: build and development share asset paths.
|
|
1023
|
+
*
|
|
1024
|
+
* @param config - Human-authored or already validated Replayable configuration.
|
|
1025
|
+
* @param workingDirectory - Project directory used to resolve authored paths.
|
|
1026
|
+
*/
|
|
1027
|
+
async function buildProject(config, workingDirectory = process.cwd()) {
|
|
1028
|
+
const validatedConfig = defineConfig(config);
|
|
1029
|
+
const projectRoot = resolve(workingDirectory);
|
|
1030
|
+
const outputDirectory = resolveOutputDirectory(projectRoot, validatedConfig.build.outDir);
|
|
1031
|
+
const configuredVariants = createVariants(validatedConfig);
|
|
1032
|
+
const canonicalVariant = configuredVariants[0];
|
|
1033
|
+
const variants = [];
|
|
1034
|
+
const errors = [];
|
|
1035
|
+
if (canonicalVariant === void 0) throw new Error("A Replayable project must resolve at least one playable variant.");
|
|
1036
|
+
await rm(outputDirectory, {
|
|
1037
|
+
force: true,
|
|
1038
|
+
recursive: true
|
|
1039
|
+
});
|
|
1040
|
+
try {
|
|
1041
|
+
for (const variant of configuredVariants) variants.push(await buildVariant(variant, {
|
|
1042
|
+
outputDirectory: join(outputDirectory, variant.id),
|
|
1043
|
+
projectRoot
|
|
1044
|
+
}));
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
errors.push(error);
|
|
1047
|
+
}
|
|
1048
|
+
try {
|
|
1049
|
+
await buildAssets(canonicalVariant.assets, projectRoot);
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
errors.push(error);
|
|
1052
|
+
}
|
|
1053
|
+
if (errors.length === 1) throw errors[0];
|
|
1054
|
+
if (errors.length > 1) throw new AggregateError(errors, "Playable build and generated asset restoration both failed.");
|
|
1055
|
+
return {
|
|
1056
|
+
outputDirectory,
|
|
1057
|
+
variants
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
//#endregion
|
|
1061
|
+
export { buildProject, buildVariant, servePreview };
|
|
1062
|
+
|
|
1063
|
+
//# sourceMappingURL=index.mjs.map
|