@presolve/vite 0.2.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/package.json +25 -0
- package/src/index.js +610 -0
- package/test/smoke.mjs +357 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Presolve contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@presolve/vite",
|
|
3
|
+
"version": "0.2.0-beta.1",
|
|
4
|
+
"description": "Vite adapter boundary for compiler-owned Presolve products.",
|
|
5
|
+
"license": "MIT OR Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/fierstdev/presolve.git",
|
|
10
|
+
"directory": "packages/vite"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public",
|
|
14
|
+
"tag": "beta"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./src/index.js"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"vite": "^7.0.0"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "node test/smoke.mjs"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
/** V1 shape of the compiler manifest consumed by this external adapter. */
|
|
6
|
+
export const PRESOLVE_VITE_ADAPTER_SCHEMA_VERSION = 1;
|
|
7
|
+
export const PRESOLVE_APPLICATION_PUBLICATION_CONTRACT_V1 = "presolve-application-publication:1";
|
|
8
|
+
export const PRESOLVE_VIRTUAL_MODULE_SCHEMA_VERSION = 1;
|
|
9
|
+
export const PRESOLVE_VIRTUAL_MODULE_PREFIX = "virtual:presolve/v1/";
|
|
10
|
+
export const PRESOLVE_ENVIRONMENT_PUBLICATION_ARTIFACT = "environment.browser.json";
|
|
11
|
+
export const PRESOLVE_ENVIRONMENT_PUBLICATION_SCHEMA_VERSION = 1;
|
|
12
|
+
export const PRESOLVE_DEVELOPMENT_DIAGNOSTICS_SCHEMA_VERSION = 1;
|
|
13
|
+
export const PRESOLVE_VITE_PRODUCTION_SCHEMA_VERSION = 1;
|
|
14
|
+
export const PRESOLVE_HMR_UPDATE_SCHEMA_VERSION = 1;
|
|
15
|
+
export const PRESOLVE_HMR_EVENT = "presolve:hmr";
|
|
16
|
+
export const PRESOLVE_PRODUCTION_AUDIT_SCHEMA_VERSION = 1;
|
|
17
|
+
export const PRESOLVE_PRODUCTION_AUDIT_ARTIFACT = "production-audit.json";
|
|
18
|
+
export const PRESOLVE_SOURCE_MAP_TRANSLATION_SCHEMA_VERSION = 1;
|
|
19
|
+
|
|
20
|
+
const PRESOLVE_HMR_MESSAGE_CLASSES = new Set([
|
|
21
|
+
"template-update",
|
|
22
|
+
"action-update",
|
|
23
|
+
"computed-update",
|
|
24
|
+
"style-update",
|
|
25
|
+
"server-only-update",
|
|
26
|
+
"component-instance-reload",
|
|
27
|
+
"route-reload",
|
|
28
|
+
"full-reload",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates the Vite transport boundary over an already-produced compiler product.
|
|
33
|
+
*
|
|
34
|
+
* The compiler owns parsing, TypeScript semantics, lowering, and artifact
|
|
35
|
+
* contents. This package owns only Vite integration and its transport hooks.
|
|
36
|
+
*/
|
|
37
|
+
export function createPresolveVitePlugin({ compilerProduct, readArtifact, requestHost, hmr } = {}) {
|
|
38
|
+
const manifest = validateCompilerProduct(compilerProduct);
|
|
39
|
+
let hmrTransport;
|
|
40
|
+
const plugin = {
|
|
41
|
+
name: "presolve:compiler-products",
|
|
42
|
+
enforce: "pre",
|
|
43
|
+
api: Object.freeze({
|
|
44
|
+
schemaVersion: PRESOLVE_VITE_ADAPTER_SCHEMA_VERSION,
|
|
45
|
+
compilerContract: manifest.compiler_contract,
|
|
46
|
+
workspaceSnapshotId: manifest.workspace_snapshot_id,
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
if (readArtifact) {
|
|
50
|
+
const registry = createPresolveVirtualModuleRegistry({ compilerProduct, readArtifact });
|
|
51
|
+
plugin.resolveId = registry.resolveId;
|
|
52
|
+
plugin.load = async resolvedId => {
|
|
53
|
+
const code = await registry.load(resolvedId);
|
|
54
|
+
if (code === undefined) return undefined;
|
|
55
|
+
return {
|
|
56
|
+
code,
|
|
57
|
+
// This is a transport map for the virtual compiler artifact, not an
|
|
58
|
+
// authored-source map. Vite may retain it in physical output maps.
|
|
59
|
+
map: {
|
|
60
|
+
version: 3,
|
|
61
|
+
sources: [resolvedId.replace(/^\0/, "")],
|
|
62
|
+
sourcesContent: [code],
|
|
63
|
+
names: [],
|
|
64
|
+
mappings: "AAAA",
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (requestHost !== undefined && typeof requestHost !== "function") {
|
|
70
|
+
throw new TypeError("@presolve/vite requestHost must be a function");
|
|
71
|
+
}
|
|
72
|
+
if (hmr !== undefined && typeof hmr !== "function") {
|
|
73
|
+
throw new TypeError("@presolve/vite hmr must be a compiler-owned function");
|
|
74
|
+
}
|
|
75
|
+
if (hmr) {
|
|
76
|
+
plugin.handleHotUpdate = async context => {
|
|
77
|
+
if (!hmrTransport) {
|
|
78
|
+
throw new Error("Presolve HMR transport is unavailable before Vite configures the server");
|
|
79
|
+
}
|
|
80
|
+
return hmrTransport.publish(
|
|
81
|
+
await hmr(Object.freeze({ file: context.file, timestamp: context.timestamp })),
|
|
82
|
+
context.modules,
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (requestHost || hmr) {
|
|
87
|
+
plugin.configureServer = server => {
|
|
88
|
+
if (requestHost) {
|
|
89
|
+
server.middlewares.use((request, response, next) => {
|
|
90
|
+
Promise.resolve(requestHost(request)).then(hostResponse => {
|
|
91
|
+
if (hostResponse === undefined) {
|
|
92
|
+
next();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
writeHostResponse(response, hostResponse);
|
|
96
|
+
}).catch(next);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (hmr) {
|
|
100
|
+
hmrTransport = createPresolveHmrTransport({
|
|
101
|
+
workspaceSnapshotId: manifest.workspace_snapshot_id,
|
|
102
|
+
send: message => server.ws.send(message),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return Object.freeze(plugin);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Validates and transports one compiler-selected HMR update without deriving
|
|
112
|
+
* semantics from Vite modules, source text, or filenames.
|
|
113
|
+
*/
|
|
114
|
+
export function createPresolveHmrTransport({ workspaceSnapshotId, send } = {}) {
|
|
115
|
+
if (typeof workspaceSnapshotId !== "string" || !workspaceSnapshotId) {
|
|
116
|
+
throw new TypeError("Presolve HMR transport requires a workspaceSnapshotId");
|
|
117
|
+
}
|
|
118
|
+
if (typeof send !== "function") {
|
|
119
|
+
throw new TypeError("Presolve HMR transport requires a Vite send function");
|
|
120
|
+
}
|
|
121
|
+
return Object.freeze({
|
|
122
|
+
schemaVersion: PRESOLVE_HMR_UPDATE_SCHEMA_VERSION,
|
|
123
|
+
publish(update, viteModules = []) {
|
|
124
|
+
const canonical = validateHmrUpdate(update, workspaceSnapshotId);
|
|
125
|
+
if (!Array.isArray(viteModules)) {
|
|
126
|
+
throw new TypeError("Vite HMR modules must be an array");
|
|
127
|
+
}
|
|
128
|
+
if (canonical.messageClass === "style-update") return viteModules;
|
|
129
|
+
if (canonical.messageClass === "full-reload") {
|
|
130
|
+
send({ type: "full-reload", path: "*" });
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
send({ type: "custom", event: PRESOLVE_HMR_EVENT, data: canonical });
|
|
134
|
+
return [];
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Starts the Vite development server owned by the `presolve dev` boundary.
|
|
141
|
+
*
|
|
142
|
+
* The request host decides document, route, loader, and action routing from
|
|
143
|
+
* compiler products; returning `undefined` delegates JS, CSS, and assets to
|
|
144
|
+
* Vite middleware. Vite types are intentionally not exposed from this API.
|
|
145
|
+
*/
|
|
146
|
+
export async function startPresolveDevServer({
|
|
147
|
+
compilerProduct,
|
|
148
|
+
readArtifact,
|
|
149
|
+
requestHost,
|
|
150
|
+
hmr,
|
|
151
|
+
diagnostics = () => ({}),
|
|
152
|
+
vite = {},
|
|
153
|
+
} = {}) {
|
|
154
|
+
if (typeof requestHost !== "function") {
|
|
155
|
+
throw new TypeError("presolve dev requires a compiler-owned requestHost");
|
|
156
|
+
}
|
|
157
|
+
if (typeof diagnostics !== "function") {
|
|
158
|
+
throw new TypeError("presolve dev diagnostics must be a function");
|
|
159
|
+
}
|
|
160
|
+
const { createServer } = await import("vite");
|
|
161
|
+
const plugin = createPresolveVitePlugin({ compilerProduct, readArtifact, requestHost, hmr });
|
|
162
|
+
const configuredPlugins = vite.plugins === undefined
|
|
163
|
+
? []
|
|
164
|
+
: Array.isArray(vite.plugins) ? vite.plugins : [vite.plugins];
|
|
165
|
+
const server = await createServer({
|
|
166
|
+
...vite,
|
|
167
|
+
appType: "custom",
|
|
168
|
+
plugins: [...configuredPlugins, plugin],
|
|
169
|
+
});
|
|
170
|
+
let currentDiagnostics = composeDevelopmentDiagnostics(await diagnostics());
|
|
171
|
+
const publishDiagnostics = async () => {
|
|
172
|
+
currentDiagnostics = composeDevelopmentDiagnostics(await diagnostics());
|
|
173
|
+
server.ws.send({
|
|
174
|
+
type: "custom",
|
|
175
|
+
event: "presolve:diagnostics",
|
|
176
|
+
data: currentDiagnostics,
|
|
177
|
+
});
|
|
178
|
+
return currentDiagnostics;
|
|
179
|
+
};
|
|
180
|
+
await server.listen();
|
|
181
|
+
await publishDiagnostics();
|
|
182
|
+
return Object.freeze({
|
|
183
|
+
server,
|
|
184
|
+
diagnostics: () => currentDiagnostics,
|
|
185
|
+
publishDiagnostics,
|
|
186
|
+
close: () => server.close(),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Merges TypeScript and Presolve diagnostics without changing their meaning. */
|
|
191
|
+
export function composeDevelopmentDiagnostics({ typescript = [], presolve = [] } = {}) {
|
|
192
|
+
const diagnostics = [
|
|
193
|
+
...normalizeDiagnostics("typescript", typescript),
|
|
194
|
+
...normalizeDiagnostics("presolve", presolve),
|
|
195
|
+
].sort(compareDevelopmentDiagnostics);
|
|
196
|
+
return Object.freeze({
|
|
197
|
+
schemaVersion: PRESOLVE_DEVELOPMENT_DIAGNOSTICS_SCHEMA_VERSION,
|
|
198
|
+
diagnostics: Object.freeze(diagnostics),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Runs Vite's production build for one compiler-selected logical entry.
|
|
204
|
+
*
|
|
205
|
+
* The Vite manifest is read after physical output is written and then mapped
|
|
206
|
+
* back to the stable compiler entry-component ID. No Vite filename becomes a
|
|
207
|
+
* compiler semantic identity.
|
|
208
|
+
*/
|
|
209
|
+
export async function buildPresolveProduction({
|
|
210
|
+
compilerProduct,
|
|
211
|
+
readArtifact,
|
|
212
|
+
entryArtifactPath,
|
|
213
|
+
viteEntries = [],
|
|
214
|
+
vite = {},
|
|
215
|
+
} = {}) {
|
|
216
|
+
const manifest = validateCompilerProduct(compilerProduct);
|
|
217
|
+
if (typeof entryArtifactPath !== "string" || !entryArtifactPath) {
|
|
218
|
+
throw new TypeError("Presolve production build requires an entryArtifactPath");
|
|
219
|
+
}
|
|
220
|
+
if (!manifest.artifacts.some(artifact => artifact.path === entryArtifactPath)) {
|
|
221
|
+
throw new TypeError(`Presolve production entry is not a compiler artifact: ${entryArtifactPath}`);
|
|
222
|
+
}
|
|
223
|
+
if (typeof manifest.entry_component_id !== "string" || !manifest.entry_component_id) {
|
|
224
|
+
throw new TypeError("Presolve production build requires manifest.entry_component_id");
|
|
225
|
+
}
|
|
226
|
+
if (!Array.isArray(viteEntries)) {
|
|
227
|
+
throw new TypeError("Presolve Vite entries must be an array");
|
|
228
|
+
}
|
|
229
|
+
const outDir = vite.build?.outDir;
|
|
230
|
+
if (typeof outDir !== "string" || !outDir) {
|
|
231
|
+
throw new TypeError("Presolve production build requires an explicit Vite build.outDir");
|
|
232
|
+
}
|
|
233
|
+
const manifestName = "presolve-vite-manifest.json";
|
|
234
|
+
const virtualEntryId = `${PRESOLVE_VIRTUAL_MODULE_PREFIX}${entryArtifactPath}`;
|
|
235
|
+
const viteRoot = typeof vite.root === "string" && vite.root ? vite.root : process.cwd();
|
|
236
|
+
const additionalInputs = viteEntries
|
|
237
|
+
.map(validateViteEntry)
|
|
238
|
+
.map(entry => Object.freeze({ ...entry, path: resolve(viteRoot, entry.path) }));
|
|
239
|
+
if (new Set(additionalInputs.map(entry => entry.name)).size !== additionalInputs.length) {
|
|
240
|
+
throw new TypeError("Presolve Vite entry names must be unique");
|
|
241
|
+
}
|
|
242
|
+
const input = Object.fromEntries([
|
|
243
|
+
["presolve-compiler-entry", virtualEntryId],
|
|
244
|
+
...additionalInputs.map(entry => [entry.name, entry.path]),
|
|
245
|
+
]);
|
|
246
|
+
const plugin = createPresolveVitePlugin({ compilerProduct, readArtifact });
|
|
247
|
+
const configuredPlugins = vite.plugins === undefined
|
|
248
|
+
? []
|
|
249
|
+
: Array.isArray(vite.plugins) ? vite.plugins : [vite.plugins];
|
|
250
|
+
const { build } = await import("vite");
|
|
251
|
+
await build({
|
|
252
|
+
...vite,
|
|
253
|
+
configFile: false,
|
|
254
|
+
plugins: [...configuredPlugins, plugin],
|
|
255
|
+
build: {
|
|
256
|
+
...vite.build,
|
|
257
|
+
outDir,
|
|
258
|
+
emptyOutDir: false,
|
|
259
|
+
manifest: manifestName,
|
|
260
|
+
sourcemap: true,
|
|
261
|
+
rollupOptions: {
|
|
262
|
+
...vite.build?.rollupOptions,
|
|
263
|
+
input,
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
const viteManifestPath = join(outDir, manifestName);
|
|
268
|
+
const viteManifest = JSON.parse(await readFile(viteManifestPath, "utf8"));
|
|
269
|
+
const isCompilerEntry = input => input === virtualEntryId
|
|
270
|
+
|| input === "presolve-compiler-entry"
|
|
271
|
+
|| input.endsWith(`${PRESOLVE_VIRTUAL_MODULE_PREFIX}${entryArtifactPath}`);
|
|
272
|
+
const entries = Object.entries(viteManifest)
|
|
273
|
+
.filter(([, output]) => output.isEntry)
|
|
274
|
+
.map(([input, output]) => Object.freeze({
|
|
275
|
+
input,
|
|
276
|
+
file: output.file,
|
|
277
|
+
css: Object.freeze([...(output.css ?? [])].sort()),
|
|
278
|
+
assets: Object.freeze([...(output.assets ?? [])].sort()),
|
|
279
|
+
imports: Object.freeze([...(output.imports ?? [])].sort()),
|
|
280
|
+
compilerArtifactPath: isCompilerEntry(input)
|
|
281
|
+
? entryArtifactPath
|
|
282
|
+
: undefined,
|
|
283
|
+
componentId: isCompilerEntry(input)
|
|
284
|
+
? manifest.entry_component_id
|
|
285
|
+
: undefined,
|
|
286
|
+
}))
|
|
287
|
+
.sort((left, right) => left.input.localeCompare(right.input));
|
|
288
|
+
if (!entries.some(entry => entry.compilerArtifactPath === entryArtifactPath)) {
|
|
289
|
+
throw new Error("Vite production manifest did not retain the compiler-selected virtual entry");
|
|
290
|
+
}
|
|
291
|
+
const sourceMaps = await Promise.all(entries
|
|
292
|
+
.filter(entry => entry.compilerArtifactPath !== undefined)
|
|
293
|
+
.map(async entry => {
|
|
294
|
+
const mapPath = join(outDir, `${entry.file}.map`);
|
|
295
|
+
let map;
|
|
296
|
+
try {
|
|
297
|
+
map = JSON.parse(await readFile(mapPath, "utf8"));
|
|
298
|
+
} catch {
|
|
299
|
+
throw new Error(`Vite did not emit a source map for ${entry.file}`);
|
|
300
|
+
}
|
|
301
|
+
return Object.freeze({
|
|
302
|
+
file: entry.file,
|
|
303
|
+
mapPath,
|
|
304
|
+
translation: translatePresolveSourceMap({ compilerProduct, sourceMap: map }),
|
|
305
|
+
});
|
|
306
|
+
}));
|
|
307
|
+
return Object.freeze({
|
|
308
|
+
schemaVersion: PRESOLVE_VITE_PRODUCTION_SCHEMA_VERSION,
|
|
309
|
+
compilerContract: manifest.compiler_contract,
|
|
310
|
+
workspaceSnapshotId: manifest.workspace_snapshot_id,
|
|
311
|
+
entryComponentId: manifest.entry_component_id,
|
|
312
|
+
viteManifestPath,
|
|
313
|
+
entries: Object.freeze(entries),
|
|
314
|
+
sourceMaps: Object.freeze(sourceMaps),
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function validateViteEntry(entry) {
|
|
319
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)
|
|
320
|
+
|| typeof entry.name !== "string" || !entry.name
|
|
321
|
+
|| typeof entry.path !== "string" || !entry.path
|
|
322
|
+
|| entry.name === "presolve-compiler-entry") {
|
|
323
|
+
throw new TypeError("Presolve Vite entries require a non-empty name and path");
|
|
324
|
+
}
|
|
325
|
+
return Object.freeze({ name: entry.name, path: entry.path });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Associates Vite map sources with exact compiler publication artifacts. This
|
|
330
|
+
* does not decode mappings or fabricate authored locations: Vite owns map
|
|
331
|
+
* generation and the compiler manifest owns the logical source identity.
|
|
332
|
+
*/
|
|
333
|
+
export function translatePresolveSourceMap({ compilerProduct, sourceMap } = {}) {
|
|
334
|
+
const manifest = validateCompilerProduct(compilerProduct);
|
|
335
|
+
if (!sourceMap || typeof sourceMap !== "object" || !Array.isArray(sourceMap.sources)) {
|
|
336
|
+
throw new TypeError("Vite source map requires a sources array");
|
|
337
|
+
}
|
|
338
|
+
const artifactPaths = new Set(manifest.artifacts.map(artifact => {
|
|
339
|
+
validateArtifact(artifact);
|
|
340
|
+
return artifact.path;
|
|
341
|
+
}));
|
|
342
|
+
const sources = sourceMap.sources.map(source => {
|
|
343
|
+
if (typeof source !== "string" || !source) {
|
|
344
|
+
throw new TypeError("Vite source map sources must be non-empty strings");
|
|
345
|
+
}
|
|
346
|
+
const index = source.indexOf(PRESOLVE_VIRTUAL_MODULE_PREFIX);
|
|
347
|
+
const artifactPath = index < 0 ? undefined : source.slice(index + PRESOLVE_VIRTUAL_MODULE_PREFIX.length);
|
|
348
|
+
return Object.freeze({
|
|
349
|
+
viteSource: source,
|
|
350
|
+
compilerArtifactPath: artifactPaths.has(artifactPath) ? artifactPath : undefined,
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
return Object.freeze({
|
|
354
|
+
schemaVersion: PRESOLVE_SOURCE_MAP_TRANSLATION_SCHEMA_VERSION,
|
|
355
|
+
workspaceSnapshotId: manifest.workspace_snapshot_id,
|
|
356
|
+
sources: Object.freeze(sources),
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Reads the compiler-produced production audit after verifying its publication
|
|
362
|
+
* digest. The adapter validates transport shape only; audit policy remains in
|
|
363
|
+
* the compiler artifact.
|
|
364
|
+
*/
|
|
365
|
+
export async function readPresolveProductionAudit({ compilerProduct, readArtifact } = {}) {
|
|
366
|
+
const manifest = validateCompilerProduct(compilerProduct);
|
|
367
|
+
if (typeof readArtifact !== "function") {
|
|
368
|
+
throw new TypeError("Presolve production audit requires a readArtifact function");
|
|
369
|
+
}
|
|
370
|
+
const artifact = manifest.artifacts.find(candidate => candidate.path === PRESOLVE_PRODUCTION_AUDIT_ARTIFACT);
|
|
371
|
+
if (!artifact) {
|
|
372
|
+
throw new TypeError("compiler product does not publish a production audit artifact");
|
|
373
|
+
}
|
|
374
|
+
validateArtifact(artifact);
|
|
375
|
+
const bytes = toBytes(await readArtifact(artifact.path));
|
|
376
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
377
|
+
if (digest !== artifact.digest) {
|
|
378
|
+
throw new Error("compiler production audit digest mismatch");
|
|
379
|
+
}
|
|
380
|
+
let audit;
|
|
381
|
+
try {
|
|
382
|
+
audit = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
383
|
+
} catch {
|
|
384
|
+
throw new TypeError("compiler production audit must be UTF-8 JSON");
|
|
385
|
+
}
|
|
386
|
+
if (!audit || typeof audit !== "object" || Array.isArray(audit)
|
|
387
|
+
|| audit.schemaVersion !== PRESOLVE_PRODUCTION_AUDIT_SCHEMA_VERSION
|
|
388
|
+
|| audit.status !== "passed"
|
|
389
|
+
|| typeof audit.buildId !== "string" || !audit.buildId
|
|
390
|
+
|| !Array.isArray(audit.checks) || audit.checks.some(check => typeof check !== "string" || !check)) {
|
|
391
|
+
throw new TypeError("compiler production audit is not a passing schema-v1 product");
|
|
392
|
+
}
|
|
393
|
+
return Object.freeze({
|
|
394
|
+
schemaVersion: audit.schemaVersion,
|
|
395
|
+
buildId: audit.buildId,
|
|
396
|
+
optimizationReportSchemaVersion: audit.optimizationReportSchemaVersion,
|
|
397
|
+
runtimeCostReportSchemaVersion: audit.runtimeCostReportSchemaVersion,
|
|
398
|
+
runtimeTableCount: audit.runtimeTableCount,
|
|
399
|
+
authorityCount: audit.authorityCount,
|
|
400
|
+
invariantCount: audit.invariantCount,
|
|
401
|
+
checks: Object.freeze([...audit.checks]),
|
|
402
|
+
status: audit.status,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Builds versioned Vite module names from one digest-bound compiler product.
|
|
408
|
+
*
|
|
409
|
+
* `readArtifact` is a host bridge over exact compiler bytes. Content is
|
|
410
|
+
* re-digested before Vite sees it, so the manifest remains the source of truth.
|
|
411
|
+
*/
|
|
412
|
+
export function createPresolveVirtualModuleRegistry({ compilerProduct, readArtifact }) {
|
|
413
|
+
const manifest = validateCompilerProduct(compilerProduct);
|
|
414
|
+
if (typeof readArtifact !== "function") {
|
|
415
|
+
throw new TypeError("@presolve/vite virtual modules require a readArtifact function");
|
|
416
|
+
}
|
|
417
|
+
const modules = new Map();
|
|
418
|
+
for (const artifact of manifest.artifacts) {
|
|
419
|
+
validateArtifact(artifact);
|
|
420
|
+
const id = `${PRESOLVE_VIRTUAL_MODULE_PREFIX}${artifact.path}`;
|
|
421
|
+
if (modules.has(id)) {
|
|
422
|
+
throw new TypeError(`duplicate Presolve publication artifact ${artifact.path}`);
|
|
423
|
+
}
|
|
424
|
+
modules.set(id, Object.freeze({
|
|
425
|
+
id,
|
|
426
|
+
resolvedId: `\0${id}`,
|
|
427
|
+
artifactPath: artifact.path,
|
|
428
|
+
digest: artifact.digest,
|
|
429
|
+
}));
|
|
430
|
+
}
|
|
431
|
+
const entries = Object.freeze([...modules.values()].sort((left, right) => left.id.localeCompare(right.id)));
|
|
432
|
+
return Object.freeze({
|
|
433
|
+
schemaVersion: PRESOLVE_VIRTUAL_MODULE_SCHEMA_VERSION,
|
|
434
|
+
entries,
|
|
435
|
+
resolveId(id) {
|
|
436
|
+
return modules.get(id)?.resolvedId;
|
|
437
|
+
},
|
|
438
|
+
async load(resolvedId) {
|
|
439
|
+
const entry = entries.find(candidate => candidate.resolvedId === resolvedId);
|
|
440
|
+
if (!entry) return undefined;
|
|
441
|
+
const bytes = toBytes(await readArtifact(entry.artifactPath));
|
|
442
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
443
|
+
if (digest !== entry.digest) {
|
|
444
|
+
throw new Error(`compiler artifact digest mismatch for ${entry.artifactPath}`);
|
|
445
|
+
}
|
|
446
|
+
return virtualModuleSource(entry.artifactPath, new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function validateCompilerProduct(product) {
|
|
452
|
+
if (!product || typeof product !== "object") {
|
|
453
|
+
throw new TypeError("@presolve/vite requires a compiler product object");
|
|
454
|
+
}
|
|
455
|
+
const { manifest } = product;
|
|
456
|
+
if (!manifest || typeof manifest !== "object") {
|
|
457
|
+
throw new TypeError("@presolve/vite requires compilerProduct.manifest");
|
|
458
|
+
}
|
|
459
|
+
if (manifest.schema_version !== 1) {
|
|
460
|
+
throw new TypeError(`unsupported Presolve publication manifest schema ${manifest.schema_version}`);
|
|
461
|
+
}
|
|
462
|
+
if (manifest.compiler_contract !== PRESOLVE_APPLICATION_PUBLICATION_CONTRACT_V1) {
|
|
463
|
+
throw new TypeError(`unsupported Presolve compiler contract ${manifest.compiler_contract}`);
|
|
464
|
+
}
|
|
465
|
+
if (!Array.isArray(manifest.artifacts)) {
|
|
466
|
+
throw new TypeError("Presolve publication manifest must contain artifacts");
|
|
467
|
+
}
|
|
468
|
+
return manifest;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function validateHmrUpdate(update, workspaceSnapshotId) {
|
|
472
|
+
if (!update || typeof update !== "object" || Array.isArray(update)) {
|
|
473
|
+
throw new TypeError("Presolve HMR update must be an object");
|
|
474
|
+
}
|
|
475
|
+
if (update.schemaVersion !== PRESOLVE_HMR_UPDATE_SCHEMA_VERSION) {
|
|
476
|
+
throw new TypeError(`unsupported Presolve HMR update schema ${update.schemaVersion}`);
|
|
477
|
+
}
|
|
478
|
+
if (update.workspaceSnapshotId !== workspaceSnapshotId) {
|
|
479
|
+
throw new TypeError("Presolve HMR update workspace snapshot does not match the compiler product");
|
|
480
|
+
}
|
|
481
|
+
if (typeof update.updateId !== "string" || !update.updateId) {
|
|
482
|
+
throw new TypeError("Presolve HMR update requires a stable updateId");
|
|
483
|
+
}
|
|
484
|
+
if (!PRESOLVE_HMR_MESSAGE_CLASSES.has(update.messageClass)) {
|
|
485
|
+
throw new TypeError("Presolve HMR update has an unsupported messageClass");
|
|
486
|
+
}
|
|
487
|
+
if (!Array.isArray(update.affectedModuleIds)
|
|
488
|
+
|| update.affectedModuleIds.some(id => typeof id !== "string" || !id)
|
|
489
|
+
|| [...new Set(update.affectedModuleIds)].length !== update.affectedModuleIds.length
|
|
490
|
+
|| [...update.affectedModuleIds].sort().some((id, index) => id !== update.affectedModuleIds[index])) {
|
|
491
|
+
throw new TypeError("Presolve HMR update requires sorted unique affectedModuleIds");
|
|
492
|
+
}
|
|
493
|
+
if (!matchesStateCompatibility(update)) {
|
|
494
|
+
throw new TypeError("Presolve HMR state preservation must be explicitly compiler-proven");
|
|
495
|
+
}
|
|
496
|
+
return Object.freeze({
|
|
497
|
+
schemaVersion: update.schemaVersion,
|
|
498
|
+
workspaceSnapshotId: update.workspaceSnapshotId,
|
|
499
|
+
updateId: update.updateId,
|
|
500
|
+
messageClass: update.messageClass,
|
|
501
|
+
affectedModuleIds: Object.freeze([...update.affectedModuleIds]),
|
|
502
|
+
stateCompatibility: update.stateCompatibility,
|
|
503
|
+
preserveState: update.preserveState,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function matchesStateCompatibility(update) {
|
|
508
|
+
return (update.stateCompatibility === "proven-compatible" && update.preserveState === true)
|
|
509
|
+
|| (update.stateCompatibility === "reload-required" && update.preserveState === false);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function validateArtifact(artifact) {
|
|
513
|
+
if (!artifact || typeof artifact.path !== "string" || !artifact.path || artifact.path.startsWith("/")
|
|
514
|
+
|| artifact.path.split("/").includes("..")) {
|
|
515
|
+
throw new TypeError("Presolve publication artifact paths must be non-escaping relative paths");
|
|
516
|
+
}
|
|
517
|
+
if (!/^[a-f0-9]{64}$/.test(artifact.digest)) {
|
|
518
|
+
throw new TypeError(`Presolve publication artifact ${artifact.path} requires a SHA-256 digest`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function toBytes(value) {
|
|
523
|
+
if (value instanceof Uint8Array) return value;
|
|
524
|
+
if (typeof value === "string") return new TextEncoder().encode(value);
|
|
525
|
+
throw new TypeError("readArtifact must return a string or Uint8Array");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function virtualModuleSource(artifactPath, content) {
|
|
529
|
+
if (artifactPath === PRESOLVE_ENVIRONMENT_PUBLICATION_ARTIFACT) {
|
|
530
|
+
return environmentVirtualModuleSource(artifactPath, content);
|
|
531
|
+
}
|
|
532
|
+
return [
|
|
533
|
+
`export const artifactPath = ${JSON.stringify(artifactPath)};`,
|
|
534
|
+
`export const content = ${JSON.stringify(content)};`,
|
|
535
|
+
"export default content;",
|
|
536
|
+
"",
|
|
537
|
+
].join("\n");
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Projects the compiler-published browser environment artifact into one Vite
|
|
542
|
+
* virtual module. This validates only the immutable compiler product: it never
|
|
543
|
+
* reads dotenv files, process state, or Vite's environment object.
|
|
544
|
+
*/
|
|
545
|
+
function environmentVirtualModuleSource(artifactPath, content) {
|
|
546
|
+
let artifact;
|
|
547
|
+
try {
|
|
548
|
+
artifact = JSON.parse(content);
|
|
549
|
+
} catch {
|
|
550
|
+
throw new TypeError("Presolve browser environment artifact must be UTF-8 JSON");
|
|
551
|
+
}
|
|
552
|
+
if (!artifact || typeof artifact !== "object" || Array.isArray(artifact)
|
|
553
|
+
|| artifact.schemaVersion !== PRESOLVE_ENVIRONMENT_PUBLICATION_SCHEMA_VERSION
|
|
554
|
+
|| !artifact.browserValues || typeof artifact.browserValues !== "object"
|
|
555
|
+
|| Array.isArray(artifact.browserValues)) {
|
|
556
|
+
throw new TypeError("Presolve browser environment artifact must be a schema-v1 compiler product");
|
|
557
|
+
}
|
|
558
|
+
for (const [name, value] of Object.entries(artifact.browserValues)) {
|
|
559
|
+
if (!name.startsWith("PRESOLVE_PUBLIC_") || name.length === "PRESOLVE_PUBLIC_".length
|
|
560
|
+
|| typeof value !== "string" || value.includes("\0")) {
|
|
561
|
+
throw new TypeError("Presolve browser environment artifact contained an invalid public value");
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
const browserValues = Object.fromEntries(Object.entries(artifact.browserValues).sort(
|
|
565
|
+
([left], [right]) => left.localeCompare(right),
|
|
566
|
+
));
|
|
567
|
+
return [
|
|
568
|
+
`export const artifactPath = ${JSON.stringify(artifactPath)};`,
|
|
569
|
+
`export const schemaVersion = ${PRESOLVE_ENVIRONMENT_PUBLICATION_SCHEMA_VERSION};`,
|
|
570
|
+
`export const browserValues = Object.freeze(${JSON.stringify(browserValues)});`,
|
|
571
|
+
"export default browserValues;",
|
|
572
|
+
"",
|
|
573
|
+
].join("\n");
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function writeHostResponse(response, hostResponse) {
|
|
577
|
+
if (!hostResponse || typeof hostResponse !== "object") {
|
|
578
|
+
throw new TypeError("Presolve requestHost must return undefined or a response object");
|
|
579
|
+
}
|
|
580
|
+
const { status = 200, headers = {}, body = "" } = hostResponse;
|
|
581
|
+
if (!Number.isInteger(status) || status < 100 || status > 599) {
|
|
582
|
+
throw new TypeError("Presolve requestHost response status must be an HTTP status code");
|
|
583
|
+
}
|
|
584
|
+
if (!headers || typeof headers !== "object" || Array.isArray(headers)) {
|
|
585
|
+
throw new TypeError("Presolve requestHost response headers must be an object");
|
|
586
|
+
}
|
|
587
|
+
response.statusCode = status;
|
|
588
|
+
for (const [name, value] of Object.entries(headers)) response.setHeader(name, value);
|
|
589
|
+
response.end(toBytes(body));
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function normalizeDiagnostics(authority, diagnostics) {
|
|
593
|
+
if (!Array.isArray(diagnostics)) {
|
|
594
|
+
throw new TypeError(`${authority} diagnostics must be an array`);
|
|
595
|
+
}
|
|
596
|
+
return diagnostics.map(diagnostic => {
|
|
597
|
+
if (!diagnostic || typeof diagnostic !== "object" || diagnostic.code === undefined
|
|
598
|
+
|| typeof diagnostic.message !== "string") {
|
|
599
|
+
throw new TypeError(`${authority} diagnostics require code and message`);
|
|
600
|
+
}
|
|
601
|
+
return Object.freeze({ authority, ...diagnostic });
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function compareDevelopmentDiagnostics(left, right) {
|
|
606
|
+
return String(left.file ?? "").localeCompare(String(right.file ?? ""))
|
|
607
|
+
|| (left.start ?? -1) - (right.start ?? -1)
|
|
608
|
+
|| String(left.code).localeCompare(String(right.code))
|
|
609
|
+
|| left.authority.localeCompare(right.authority);
|
|
610
|
+
}
|
package/test/smoke.mjs
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
buildPresolveProduction,
|
|
7
|
+
createPresolveHmrTransport,
|
|
8
|
+
createPresolveVitePlugin,
|
|
9
|
+
createPresolveVirtualModuleRegistry,
|
|
10
|
+
composeDevelopmentDiagnostics,
|
|
11
|
+
PRESOLVE_HMR_EVENT,
|
|
12
|
+
PRESOLVE_ENVIRONMENT_PUBLICATION_ARTIFACT,
|
|
13
|
+
readPresolveProductionAudit,
|
|
14
|
+
translatePresolveSourceMap,
|
|
15
|
+
PRESOLVE_VITE_ADAPTER_SCHEMA_VERSION,
|
|
16
|
+
PRESOLVE_VIRTUAL_MODULE_PREFIX,
|
|
17
|
+
startPresolveDevServer,
|
|
18
|
+
} from "../src/index.js";
|
|
19
|
+
|
|
20
|
+
const runtime = "export const runtime = 1;\n";
|
|
21
|
+
const digest = createHash("sha256").update(runtime).digest("hex");
|
|
22
|
+
const environmentArtifact = JSON.stringify({
|
|
23
|
+
schemaVersion: 1,
|
|
24
|
+
browserValues: { PRESOLVE_PUBLIC_NAME: "Presolve" },
|
|
25
|
+
});
|
|
26
|
+
const environmentDigest = createHash("sha256").update(environmentArtifact).digest("hex");
|
|
27
|
+
const auditJson = JSON.stringify({
|
|
28
|
+
schemaVersion: 1,
|
|
29
|
+
buildId: "resume-build:fixture",
|
|
30
|
+
optimizationReportSchemaVersion: 1,
|
|
31
|
+
runtimeCostReportSchemaVersion: 1,
|
|
32
|
+
runtimeTableCount: 0,
|
|
33
|
+
authorityCount: 8,
|
|
34
|
+
invariantCount: 13,
|
|
35
|
+
checks: ["report-schema"],
|
|
36
|
+
status: "passed",
|
|
37
|
+
});
|
|
38
|
+
const auditDigest = createHash("sha256").update(auditJson).digest("hex");
|
|
39
|
+
const plugin = createPresolveVitePlugin({
|
|
40
|
+
compilerProduct: {
|
|
41
|
+
manifest: {
|
|
42
|
+
schema_version: 1,
|
|
43
|
+
compiler_contract: "presolve-application-publication:1",
|
|
44
|
+
workspace_snapshot_id: "fixture-snapshot",
|
|
45
|
+
artifacts: [{ path: "runtime.js", digest }],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (plugin.name !== "presolve:compiler-products" || plugin.enforce !== "pre") {
|
|
51
|
+
throw new Error("adapter must expose its stable Vite plugin identity");
|
|
52
|
+
}
|
|
53
|
+
if (plugin.api.schemaVersion !== PRESOLVE_VITE_ADAPTER_SCHEMA_VERSION) {
|
|
54
|
+
throw new Error("adapter must expose its schema version");
|
|
55
|
+
}
|
|
56
|
+
if ("resolveId" in plugin || "load" in plugin || "configureServer" in plugin) {
|
|
57
|
+
throw new Error("the skeleton must not publish virtual modules or dev-server hooks");
|
|
58
|
+
}
|
|
59
|
+
assertRejects(
|
|
60
|
+
() => createPresolveVitePlugin({ compilerProduct: { manifest: { schema_version: 2 } } }),
|
|
61
|
+
"unsupported manifest schemas must not enter Vite",
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const registry = createPresolveVirtualModuleRegistry({
|
|
65
|
+
compilerProduct: {
|
|
66
|
+
manifest: {
|
|
67
|
+
schema_version: 1,
|
|
68
|
+
compiler_contract: "presolve-application-publication:1",
|
|
69
|
+
workspace_snapshot_id: "fixture-snapshot",
|
|
70
|
+
artifacts: [{ path: "runtime.js", digest }],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
readArtifact: path => path === "runtime.js" ? runtime : undefined,
|
|
74
|
+
});
|
|
75
|
+
const virtualId = `${PRESOLVE_VIRTUAL_MODULE_PREFIX}runtime.js`;
|
|
76
|
+
const resolvedId = registry.resolveId(virtualId);
|
|
77
|
+
if (resolvedId !== `\0${virtualId}`) throw new Error("registry must resolve a versioned virtual module id");
|
|
78
|
+
const source = await registry.load(resolvedId);
|
|
79
|
+
const expected = [
|
|
80
|
+
"export const artifactPath = \"runtime.js\";",
|
|
81
|
+
"export const content = \"export const runtime = 1;\\n\";",
|
|
82
|
+
"export default content;",
|
|
83
|
+
"",
|
|
84
|
+
].join("\n");
|
|
85
|
+
if (source !== expected) throw new Error("registry must expose the golden compiler artifact content");
|
|
86
|
+
await assertAsyncRejects(
|
|
87
|
+
() => createPresolveVirtualModuleRegistry({
|
|
88
|
+
compilerProduct: { manifest: { schema_version: 1, compiler_contract: "presolve-application-publication:1", artifacts: [{ path: "runtime.js", digest: "0".repeat(64) }] } },
|
|
89
|
+
readArtifact: () => runtime,
|
|
90
|
+
}).load(`\0${virtualId}`),
|
|
91
|
+
"registry must reject artifact content that differs from its compiler digest",
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const environmentRegistry = createPresolveVirtualModuleRegistry({
|
|
95
|
+
compilerProduct: {
|
|
96
|
+
manifest: {
|
|
97
|
+
schema_version: 1,
|
|
98
|
+
compiler_contract: "presolve-application-publication:1",
|
|
99
|
+
workspace_snapshot_id: "fixture-snapshot",
|
|
100
|
+
artifacts: [{ path: PRESOLVE_ENVIRONMENT_PUBLICATION_ARTIFACT, digest: environmentDigest }],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
readArtifact: path => path === PRESOLVE_ENVIRONMENT_PUBLICATION_ARTIFACT ? environmentArtifact : undefined,
|
|
104
|
+
});
|
|
105
|
+
const environmentSource = await environmentRegistry.load(
|
|
106
|
+
environmentRegistry.resolveId(`${PRESOLVE_VIRTUAL_MODULE_PREFIX}${PRESOLVE_ENVIRONMENT_PUBLICATION_ARTIFACT}`),
|
|
107
|
+
);
|
|
108
|
+
if (!environmentSource.includes('export const browserValues = Object.freeze({"PRESOLVE_PUBLIC_NAME":"Presolve"});')
|
|
109
|
+
|| environmentSource.includes("process.env") || environmentSource.includes("import.meta.env")) {
|
|
110
|
+
throw new Error("Vite must expose only the compiler-published browser environment map");
|
|
111
|
+
}
|
|
112
|
+
await assertAsyncRejects(
|
|
113
|
+
() => createPresolveVirtualModuleRegistry({
|
|
114
|
+
compilerProduct: {
|
|
115
|
+
manifest: {
|
|
116
|
+
schema_version: 1,
|
|
117
|
+
compiler_contract: "presolve-application-publication:1",
|
|
118
|
+
workspace_snapshot_id: "fixture-snapshot",
|
|
119
|
+
artifacts: [{ path: PRESOLVE_ENVIRONMENT_PUBLICATION_ARTIFACT, digest: createHash("sha256").update('{"schemaVersion":1,"browserValues":{"DATABASE_URL":"secret"}}').digest("hex") }],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
readArtifact: () => '{"schemaVersion":1,"browserValues":{"DATABASE_URL":"secret"}}',
|
|
123
|
+
}).load(`\0${PRESOLVE_VIRTUAL_MODULE_PREFIX}${PRESOLVE_ENVIRONMENT_PUBLICATION_ARTIFACT}`),
|
|
124
|
+
"Vite must reject server-owned values in a compiler environment artifact",
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const audit = await readPresolveProductionAudit({
|
|
128
|
+
compilerProduct: {
|
|
129
|
+
manifest: {
|
|
130
|
+
schema_version: 1,
|
|
131
|
+
compiler_contract: "presolve-application-publication:1",
|
|
132
|
+
workspace_snapshot_id: "fixture-snapshot",
|
|
133
|
+
artifacts: [{ path: "production-audit.json", digest: auditDigest }],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
readArtifact: () => auditJson,
|
|
137
|
+
});
|
|
138
|
+
if (audit.status !== "passed" || audit.buildId !== "resume-build:fixture") {
|
|
139
|
+
throw new Error("Vite must expose the digest-verified compiler production audit");
|
|
140
|
+
}
|
|
141
|
+
await assertAsyncRejects(
|
|
142
|
+
() => readPresolveProductionAudit({
|
|
143
|
+
compilerProduct: {
|
|
144
|
+
manifest: {
|
|
145
|
+
schema_version: 1,
|
|
146
|
+
compiler_contract: "presolve-application-publication:1",
|
|
147
|
+
workspace_snapshot_id: "fixture-snapshot",
|
|
148
|
+
artifacts: [{ path: "production-audit.json", digest: "0".repeat(64) }],
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
readArtifact: () => auditJson,
|
|
152
|
+
}),
|
|
153
|
+
"Vite must reject a production audit whose bytes differ from the compiler manifest",
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const sourceMapTranslation = translatePresolveSourceMap({
|
|
157
|
+
compilerProduct: {
|
|
158
|
+
manifest: {
|
|
159
|
+
schema_version: 1,
|
|
160
|
+
compiler_contract: "presolve-application-publication:1",
|
|
161
|
+
workspace_snapshot_id: "fixture-snapshot",
|
|
162
|
+
artifacts: [{ path: "runtime.js", digest }],
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
sourceMap: { version: 3, sources: [`\0${PRESOLVE_VIRTUAL_MODULE_PREFIX}runtime.js`, "node_modules/dependency.js"] },
|
|
166
|
+
});
|
|
167
|
+
if (sourceMapTranslation.sources[0].compilerArtifactPath !== "runtime.js"
|
|
168
|
+
|| sourceMapTranslation.sources[1].compilerArtifactPath !== undefined) {
|
|
169
|
+
throw new Error("source-map translation must retain only manifest-bound compiler identities");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const diagnostics = composeDevelopmentDiagnostics({
|
|
173
|
+
typescript: [{ code: 2322, message: "Type mismatch", file: "src/App.tsx", start: 12 }],
|
|
174
|
+
presolve: [{ code: "PSV1001", message: "Unsupported construct", file: "src/App.tsx", start: 3 }],
|
|
175
|
+
});
|
|
176
|
+
if (diagnostics.diagnostics.map(diagnostic => diagnostic.authority).join(",") !== "presolve,typescript") {
|
|
177
|
+
throw new Error("development diagnostics must compose and order both authorities");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const hmrMessages = [];
|
|
181
|
+
const hmr = createPresolveHmrTransport({
|
|
182
|
+
workspaceSnapshotId: "fixture-snapshot",
|
|
183
|
+
send: message => hmrMessages.push(message),
|
|
184
|
+
});
|
|
185
|
+
const actionUpdate = {
|
|
186
|
+
schemaVersion: 1,
|
|
187
|
+
workspaceSnapshotId: "fixture-snapshot",
|
|
188
|
+
updateId: "hmr-action-1",
|
|
189
|
+
messageClass: "action-update",
|
|
190
|
+
affectedModuleIds: ["virtual:presolve/v1/runtime.js"],
|
|
191
|
+
stateCompatibility: "proven-compatible",
|
|
192
|
+
preserveState: true,
|
|
193
|
+
};
|
|
194
|
+
if (hmr.publish(actionUpdate, [{ id: "vite-runtime" }]).length !== 0) {
|
|
195
|
+
throw new Error("semantic HMR must suppress Vite module replacement");
|
|
196
|
+
}
|
|
197
|
+
if (hmrMessages.length !== 1 || hmrMessages[0].event !== PRESOLVE_HMR_EVENT
|
|
198
|
+
|| hmrMessages[0].data.preserveState !== true) {
|
|
199
|
+
throw new Error("semantic HMR must transport the compiler-selected update unchanged");
|
|
200
|
+
}
|
|
201
|
+
const viteModules = [{ id: "vite-style" }];
|
|
202
|
+
if (hmr.publish({ ...actionUpdate, updateId: "hmr-style-1", messageClass: "style-update" }, viteModules) !== viteModules) {
|
|
203
|
+
throw new Error("style updates must remain under Vite native CSS HMR");
|
|
204
|
+
}
|
|
205
|
+
hmr.publish({
|
|
206
|
+
...actionUpdate,
|
|
207
|
+
updateId: "hmr-full-1",
|
|
208
|
+
messageClass: "full-reload",
|
|
209
|
+
stateCompatibility: "reload-required",
|
|
210
|
+
preserveState: false,
|
|
211
|
+
});
|
|
212
|
+
if (hmrMessages.at(-1).type !== "full-reload") {
|
|
213
|
+
throw new Error("full reload must use Vite's native transport");
|
|
214
|
+
}
|
|
215
|
+
assertRejects(
|
|
216
|
+
() => hmr.publish({ ...actionUpdate, updateId: "hmr-unsafe-1", preserveState: false }),
|
|
217
|
+
"the adapter must reject state preservation that was not compiler-proven",
|
|
218
|
+
);
|
|
219
|
+
const hmrPlugin = createPresolveVitePlugin({
|
|
220
|
+
compilerProduct: {
|
|
221
|
+
manifest: {
|
|
222
|
+
schema_version: 1,
|
|
223
|
+
compiler_contract: "presolve-application-publication:1",
|
|
224
|
+
workspace_snapshot_id: "fixture-snapshot",
|
|
225
|
+
artifacts: [{ path: "runtime.js", digest }],
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
hmr: observation => ({ ...actionUpdate, updateId: `hmr-hook-${observation.timestamp}` }),
|
|
229
|
+
});
|
|
230
|
+
const hmrPluginMessages = [];
|
|
231
|
+
hmrPlugin.configureServer({ ws: { send: message => hmrPluginMessages.push(message) }, middlewares: { use() {} } });
|
|
232
|
+
const hmrResult = await hmrPlugin.handleHotUpdate({ file: "/work/runtime.ts", timestamp: 10, modules: viteModules });
|
|
233
|
+
if (hmrResult.length !== 0 || hmrPluginMessages[0].data.updateId !== "hmr-hook-10") {
|
|
234
|
+
throw new Error("Vite's hot-update hook must only forward the compiler HMR product");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const dev = await startPresolveDevServer({
|
|
238
|
+
compilerProduct: {
|
|
239
|
+
manifest: {
|
|
240
|
+
schema_version: 1,
|
|
241
|
+
compiler_contract: "presolve-application-publication:1",
|
|
242
|
+
workspace_snapshot_id: "fixture-snapshot",
|
|
243
|
+
artifacts: [{ path: "runtime.js", digest }],
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
readArtifact: () => runtime,
|
|
247
|
+
requestHost: request => request.url === "/route"
|
|
248
|
+
? { status: 200, headers: { "content-type": "text/plain" }, body: "Presolve route" }
|
|
249
|
+
: undefined,
|
|
250
|
+
diagnostics: () => ({ typescript: [{ code: 2322, message: "Type mismatch" }], presolve: [] }),
|
|
251
|
+
vite: { logLevel: "silent", server: { host: "127.0.0.1", port: 0 } },
|
|
252
|
+
});
|
|
253
|
+
try {
|
|
254
|
+
const address = dev.server.httpServer.address();
|
|
255
|
+
const response = await fetch(`http://127.0.0.1:${address.port}/route`, {
|
|
256
|
+
headers: { connection: "close" },
|
|
257
|
+
});
|
|
258
|
+
if (response.status !== 200 || await response.text() !== "Presolve route") {
|
|
259
|
+
throw new Error("presolve dev must route compiler-owned requests without restarting Vite");
|
|
260
|
+
}
|
|
261
|
+
const viteAsset = await fetch(`http://127.0.0.1:${address.port}/@vite/client`, {
|
|
262
|
+
headers: { connection: "close" },
|
|
263
|
+
});
|
|
264
|
+
if (viteAsset.status !== 200 || !(await viteAsset.text()).includes("createHotContext")) {
|
|
265
|
+
throw new Error("unclaimed JS assets must continue through Vite middleware");
|
|
266
|
+
}
|
|
267
|
+
const published = await dev.publishDiagnostics();
|
|
268
|
+
if (published.diagnostics.length !== 1 || published.diagnostics[0].authority !== "typescript") {
|
|
269
|
+
throw new Error("presolve dev must republish the composed diagnostics product");
|
|
270
|
+
}
|
|
271
|
+
} finally {
|
|
272
|
+
dev.server.httpServer.closeAllConnections?.();
|
|
273
|
+
await dev.close();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const outputDirectory = await mkdtemp(join(tmpdir(), "presolve-vite-build-"));
|
|
277
|
+
const applicationDirectory = await mkdtemp(join(tmpdir(), "presolve-vite-assets-"));
|
|
278
|
+
try {
|
|
279
|
+
await mkdir(join(applicationDirectory, "styles"));
|
|
280
|
+
await mkdir(join(applicationDirectory, "assets"));
|
|
281
|
+
await mkdir(join(applicationDirectory, "public"));
|
|
282
|
+
await writeFile(join(applicationDirectory, "assets", "mark.svg"), "<svg xmlns=\"http://www.w3.org/2000/svg\"/>\n");
|
|
283
|
+
await writeFile(join(applicationDirectory, "public", "robots.txt"), "User-agent: *\n");
|
|
284
|
+
await writeFile(join(applicationDirectory, "styles", "app.css"), ".app { background-image: url('../assets/mark.svg'); }\n");
|
|
285
|
+
await writeFile(join(applicationDirectory, "styles", "card.module.css"), ".card { --presolve-card-color: 43; color: var(--presolve-card-color); }\n");
|
|
286
|
+
await writeFile(
|
|
287
|
+
join(applicationDirectory, "ui-entry.js"),
|
|
288
|
+
"import './styles/app.css'; import styles from './styles/card.module.css'; document.documentElement.className = styles.card;\n",
|
|
289
|
+
);
|
|
290
|
+
const production = await buildPresolveProduction({
|
|
291
|
+
compilerProduct: {
|
|
292
|
+
manifest: {
|
|
293
|
+
schema_version: 1,
|
|
294
|
+
compiler_contract: "presolve-application-publication:1",
|
|
295
|
+
workspace_snapshot_id: "fixture-snapshot",
|
|
296
|
+
entry_component_id: "component:x-app",
|
|
297
|
+
artifacts: [{ path: "runtime.js", digest }],
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
readArtifact: () => runtime,
|
|
301
|
+
entryArtifactPath: "runtime.js",
|
|
302
|
+
viteEntries: [{ name: "application-ui", path: "ui-entry.js" }],
|
|
303
|
+
vite: {
|
|
304
|
+
root: applicationDirectory,
|
|
305
|
+
publicDir: "public",
|
|
306
|
+
logLevel: "silent",
|
|
307
|
+
build: { outDir: outputDirectory, assetsInlineLimit: 0 },
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
if (production.entryComponentId !== "component:x-app" || production.entries.length !== 2) {
|
|
311
|
+
throw new Error("production build must retain the compiler entry and explicit Vite entries");
|
|
312
|
+
}
|
|
313
|
+
if (production.sourceMaps.length !== 1 || !production.sourceMaps[0].mapPath.endsWith(".map")) {
|
|
314
|
+
throw new Error("production builds must emit and report Vite source maps");
|
|
315
|
+
}
|
|
316
|
+
const entry = production.entries.find(candidate => candidate.compilerArtifactPath === "runtime.js");
|
|
317
|
+
if (entry.compilerArtifactPath !== "runtime.js" || entry.componentId !== "component:x-app") {
|
|
318
|
+
throw new Error("production entry mapping must retain compiler identities");
|
|
319
|
+
}
|
|
320
|
+
const physicalManifest = JSON.parse(await readFile(production.viteManifestPath, "utf8"));
|
|
321
|
+
if (!Object.values(physicalManifest).some(output => output.file === entry.file)) {
|
|
322
|
+
throw new Error("production product must describe a file from Vite's written manifest");
|
|
323
|
+
}
|
|
324
|
+
const ui = production.entries.find(candidate => candidate.input.endsWith("ui-entry.js"));
|
|
325
|
+
if (!ui || ui.compilerArtifactPath !== undefined || ui.css.length === 0) {
|
|
326
|
+
throw new Error("Vite-owned CSS module entries must stay outside compiler identity mapping");
|
|
327
|
+
}
|
|
328
|
+
const css = (await Promise.all(ui.css.map(path => readFile(join(outputDirectory, path), "utf8")))).join("\n");
|
|
329
|
+
if (!css.includes("--presolve-card-color") || !css.includes("mark-")) {
|
|
330
|
+
throw new Error("Vite must package CSS modules and imported asset URLs");
|
|
331
|
+
}
|
|
332
|
+
if (await readFile(join(outputDirectory, "robots.txt"), "utf8") !== "User-agent: *\n") {
|
|
333
|
+
throw new Error("Vite must copy caller-declared public assets");
|
|
334
|
+
}
|
|
335
|
+
} finally {
|
|
336
|
+
await rm(outputDirectory, { recursive: true, force: true });
|
|
337
|
+
await rm(applicationDirectory, { recursive: true, force: true });
|
|
338
|
+
}
|
|
339
|
+
process.exit(0);
|
|
340
|
+
|
|
341
|
+
function assertRejects(action, message) {
|
|
342
|
+
try {
|
|
343
|
+
action();
|
|
344
|
+
} catch {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
throw new Error(message);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function assertAsyncRejects(action, message) {
|
|
351
|
+
try {
|
|
352
|
+
await action();
|
|
353
|
+
} catch {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
throw new Error(message);
|
|
357
|
+
}
|