@sdeverywhere/plugin-check 0.3.36 → 0.3.38

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/dist/index.js CHANGED
@@ -1,938 +1,699 @@
1
- // src/plugin.ts
2
- import { existsSync as existsSync3 } from "fs";
3
- import { copyFile, mkdir as mkdir2 } from "fs/promises";
4
- import { dirname as dirname5, join as joinPath5, relative as relative5 } from "path";
5
- import { fileURLToPath as fileURLToPath5 } from "url";
1
+ import { existsSync, mkdirSync } from "node:fs";
2
+ import { copyFile, mkdir, readFile, readdir, stat, utimes, writeFile } from "node:fs/promises";
3
+ import { dirname, join, relative, resolve, sep } from "node:path";
4
+ import { fileURLToPath } from "url";
6
5
  import { build, createServer } from "vite";
7
- import { createConfig } from "@sdeverywhere/check-core";
8
-
9
- // src/bundle-file-ops.ts
10
- import { mkdir, readFile, stat, utimes, writeFile } from "fs/promises";
11
- import { dirname, join as joinPath } from "path";
6
+ import { createConfig, datasetMessage, encodeImplVars, predicateMessage, runSuite, scenarioMessage, suiteSummaryFromReport } from "@sdeverywhere/check-core";
7
+ import { performance } from "perf_hooks";
8
+ import pico from "picocolors";
9
+ import { existsSync as existsSync$1, readFileSync, statSync } from "fs";
10
+ import { dirname as dirname$1, join as join$1, relative as relative$1, resolve as resolve$1 } from "path";
11
+ import { fileURLToPath as fileURLToPath$1, pathToFileURL } from "node:url";
12
+ import chokidar from "chokidar";
13
+ //#region src/bundle-file-ops.ts
14
+ /**
15
+ * Download a bundle from a remote URL and save it to the local bundles directory.
16
+ *
17
+ * @param url The remote URL to download the bundle from.
18
+ * @param name The bundle name (may contain slashes for subdirectories).
19
+ * @param lastModified The last modified timestamp from the remote bundle.
20
+ * @param bundlesDir The bundles directory path.
21
+ * @param fetchRemoteBundle Optional custom function for fetching remote bundle files.
22
+ * @returns The file path where the bundle was saved.
23
+ */
12
24
  async function downloadBundle(url, name, lastModified, bundlesDir, fetchRemoteBundle) {
13
- const fullUrl = `${url}?cb=${Date.now()}`;
14
- let bundleContent;
15
- if (fetchRemoteBundle) {
16
- bundleContent = await fetchRemoteBundle(fullUrl);
17
- } else {
18
- const response = await fetch(fullUrl);
19
- if (!response.ok) {
20
- throw new Error(`Failed to fetch bundle: HTTP ${response.status} ${response.statusText}`);
21
- }
22
- bundleContent = await response.text();
23
- }
24
- const nameParts = name.split("/");
25
- const filePath = joinPath(bundlesDir, ...nameParts) + ".js";
26
- await mkdir(dirname(filePath), { recursive: true });
27
- await writeFile(filePath, bundleContent, "utf8");
28
- if (lastModified) {
29
- const mtime = new Date(lastModified);
30
- await utimes(filePath, mtime, mtime);
31
- }
32
- return filePath;
25
+ const fullUrl = `${url}?cb=${Date.now()}`;
26
+ let bundleContent;
27
+ if (fetchRemoteBundle) bundleContent = await fetchRemoteBundle(fullUrl);
28
+ else {
29
+ const response = await fetch(fullUrl);
30
+ if (!response.ok) throw new Error(`Failed to fetch bundle: HTTP ${response.status} ${response.statusText}`);
31
+ bundleContent = await response.text();
32
+ }
33
+ const nameParts = name.split("/");
34
+ const filePath = join(bundlesDir, ...nameParts) + ".js";
35
+ await mkdir(dirname(filePath), { recursive: true });
36
+ await writeFile(filePath, bundleContent, "utf8");
37
+ if (lastModified) {
38
+ const mtime = new Date(lastModified);
39
+ await utimes(filePath, mtime, mtime);
40
+ }
41
+ return filePath;
33
42
  }
43
+ /**
44
+ * Copy a bundle from a source URL and save it with a new name to the local bundles directory.
45
+ *
46
+ * @param url The source URL to copy the bundle from (can be 'current' or file:// URLs).
47
+ * @param newName The new bundle name (may contain slashes for subdirectories).
48
+ * @param bundlesDir The bundles directory path.
49
+ * @returns The file path where the bundle was saved.
50
+ */
34
51
  async function copyBundle(url, newName, bundlesDir) {
35
- let srcPath;
36
- if (url === "current") {
37
- const sdePrepDir = joinPath(bundlesDir, "..", "sde-prep");
38
- srcPath = joinPath(sdePrepDir, "check-bundle.js");
39
- } else if (url.startsWith("file://")) {
40
- srcPath = new URL(url).pathname;
41
- } else {
42
- throw new Error(`Cannot copy bundle with URL: ${url}`);
43
- }
44
- const bundleContent = await readFile(srcPath, "utf8");
45
- const stats = await stat(srcPath);
46
- const sourceLastModified = stats.mtime;
47
- const nameParts = newName.split("/");
48
- const filePath = joinPath(bundlesDir, ...nameParts) + ".js";
49
- await mkdir(dirname(filePath), { recursive: true });
50
- await writeFile(filePath, bundleContent, "utf8");
51
- if (sourceLastModified) {
52
- await utimes(filePath, sourceLastModified, sourceLastModified);
53
- }
54
- return filePath;
52
+ let srcPath;
53
+ if (url === "current") {
54
+ const sdePrepDir = join(bundlesDir, "..", "sde-prep");
55
+ srcPath = join(sdePrepDir, "check-bundle.js");
56
+ } else if (url.startsWith("file://")) srcPath = new URL(url).pathname;
57
+ else throw new Error(`Cannot copy bundle with URL: ${url}`);
58
+ const bundleContent = await readFile(srcPath, "utf8");
59
+ const sourceLastModified = (await stat(srcPath)).mtime;
60
+ const nameParts = newName.split("/");
61
+ const filePath = join(bundlesDir, ...nameParts) + ".js";
62
+ await mkdir(dirname(filePath), { recursive: true });
63
+ await writeFile(filePath, bundleContent, "utf8");
64
+ if (sourceLastModified) await utimes(filePath, sourceLastModified, sourceLastModified);
65
+ return filePath;
55
66
  }
56
-
57
- // src/run-suite.ts
58
- import { performance } from "perf_hooks";
59
- import pico from "picocolors";
60
- import {
61
- datasetMessage,
62
- predicateMessage,
63
- runSuite,
64
- scenarioMessage,
65
- suiteSummaryFromReport
66
- } from "@sdeverywhere/check-core";
67
+ //#endregion
68
+ //#region src/run-suite.ts
69
+ /**
70
+ * Runs the test suite.
71
+ */
67
72
  async function runTestSuite(context, config, verbose) {
68
- return new Promise((resolve, reject) => {
69
- const t0 = performance.now();
70
- let lastPctByInc;
71
- const callbacks = {
72
- onProgress: (progress) => {
73
- const pct = Math.round(progress * 100);
74
- const pctByInc = Math.floor(pct / 5) * 5;
75
- if (lastPctByInc === void 0 || pctByInc > lastPctByInc) {
76
- lastPctByInc = pctByInc;
77
- context.log("info", `${pctByInc}%`);
78
- }
79
- },
80
- onComplete: (report) => {
81
- try {
82
- const t1 = performance.now();
83
- const elapsedMillis = t1 - t0;
84
- const elapsedSeconds = (elapsedMillis / 1e3).toFixed(1);
85
- context.log("info", `
86
- Test suite completed in ${elapsedSeconds}s`);
87
- const allChecksPassed = printCheckSummary(context, report.checkReport, verbose);
88
- if (report.comparisonReport) {
89
- printPerfStats(context, config.comparison, report.comparisonReport);
90
- }
91
- const suiteSummary = suiteSummaryFromReport(report, elapsedMillis);
92
- resolve({
93
- allChecksPassed,
94
- suiteSummary
95
- });
96
- } catch (e) {
97
- reject(e);
98
- }
99
- },
100
- onError: (error) => {
101
- reject(error);
102
- }
103
- };
104
- runSuite(config, callbacks);
105
- });
73
+ return new Promise((resolve, reject) => {
74
+ const t0 = performance.now();
75
+ let lastPctByInc;
76
+ runSuite(config, {
77
+ onProgress: (progress) => {
78
+ const pct = Math.round(progress * 100);
79
+ const pctByInc = Math.floor(pct / 5) * 5;
80
+ if (lastPctByInc === void 0 || pctByInc > lastPctByInc) {
81
+ lastPctByInc = pctByInc;
82
+ context.log("info", `${pctByInc}%`);
83
+ }
84
+ },
85
+ onComplete: (report) => {
86
+ try {
87
+ const elapsedMillis = performance.now() - t0;
88
+ const elapsedSeconds = (elapsedMillis / 1e3).toFixed(1);
89
+ context.log("info", `\nTest suite completed in ${elapsedSeconds}s`);
90
+ const allChecksPassed = printCheckSummary(context, report.checkReport, verbose);
91
+ if (report.comparisonReport) printPerfStats(context, config.comparison, report.comparisonReport);
92
+ resolve({
93
+ allChecksPassed,
94
+ suiteSummary: suiteSummaryFromReport(report, elapsedMillis)
95
+ });
96
+ } catch (e) {
97
+ reject(e);
98
+ }
99
+ },
100
+ onError: (error) => {
101
+ reject(error);
102
+ }
103
+ });
104
+ });
106
105
  }
107
106
  function printCheckSummary(context, checkReport, verbose) {
108
- function printResult(indent, status, text) {
109
- if (!verbose && status === "passed" && indent > 1) {
110
- return;
111
- }
112
- let statusChar;
113
- switch (status) {
114
- case "passed":
115
- statusChar = "\u2713";
116
- break;
117
- case "failed":
118
- statusChar = "\u2717";
119
- break;
120
- case "error":
121
- statusChar = "\u203C";
122
- break;
123
- case "skipped":
124
- statusChar = "\u2013";
125
- break;
126
- default:
127
- statusChar = "";
128
- break;
129
- }
130
- const msg = `${" ".repeat(indent)}${statusChar} ${text}`;
131
- context.log("info", status === "passed" ? pico.green(msg) : pico.red(msg));
132
- }
133
- function bold(s) {
134
- return pico.bold(s);
135
- }
136
- function printTest(test) {
137
- const msg = `${test.name}${verbose || test.status !== "passed" ? ":" : ""}`;
138
- printResult(1, test.status, msg);
139
- }
140
- let allPassed = true;
141
- context.log("info", "\nCheck results:");
142
- for (const group of checkReport.groups) {
143
- context.log("info", `
144
- ${group.name}`);
145
- for (const test of group.tests) {
146
- if (test.status !== "passed") {
147
- allPassed = false;
148
- }
149
- printTest(test);
150
- for (const scenario of test.scenarios) {
151
- printResult(3, scenario.status, scenarioMessage(scenario, bold));
152
- for (const dataset of scenario.datasets) {
153
- printResult(5, dataset.status, datasetMessage(dataset, bold));
154
- for (const predicate of dataset.predicates) {
155
- printResult(7, predicate.result.status, predicateMessage(predicate, bold));
156
- }
157
- }
158
- }
159
- }
160
- }
161
- context.log("info", "");
162
- return allPassed;
107
+ function printResult(indent, status, text) {
108
+ if (!verbose && status === "passed" && indent > 1) return;
109
+ let statusChar;
110
+ switch (status) {
111
+ case "passed":
112
+ statusChar = "✓";
113
+ break;
114
+ case "failed":
115
+ statusChar = "✗";
116
+ break;
117
+ case "error":
118
+ statusChar = "‼";
119
+ break;
120
+ case "skipped":
121
+ statusChar = "–";
122
+ break;
123
+ default: statusChar = "";
124
+ }
125
+ const msg = `${" ".repeat(indent)}${statusChar} ${text}`;
126
+ context.log("info", status === "passed" ? pico.green(msg) : pico.red(msg));
127
+ }
128
+ function bold(s) {
129
+ return pico.bold(s);
130
+ }
131
+ function printTest(test) {
132
+ const msg = `${test.name}${verbose || test.status !== "passed" ? ":" : ""}`;
133
+ printResult(1, test.status, msg);
134
+ }
135
+ let allPassed = true;
136
+ context.log("info", "\nCheck results:");
137
+ for (const group of checkReport.groups) {
138
+ context.log("info", `\n${group.name}`);
139
+ for (const test of group.tests) {
140
+ if (test.status !== "passed") allPassed = false;
141
+ printTest(test);
142
+ for (const scenario of test.scenarios) {
143
+ printResult(3, scenario.status, scenarioMessage(scenario, bold));
144
+ for (const dataset of scenario.datasets) {
145
+ printResult(5, dataset.status, datasetMessage(dataset, bold));
146
+ for (const predicate of dataset.predicates) printResult(7, predicate.result.status, predicateMessage(predicate, bold));
147
+ }
148
+ }
149
+ }
150
+ }
151
+ context.log("info", "");
152
+ return allPassed;
163
153
  }
164
- function stat2(label, n) {
165
- return `${label}=${n.toFixed(1)}ms`;
154
+ function stat$1(label, n) {
155
+ return `${label}=${n.toFixed(1)}ms`;
166
156
  }
167
157
  function printPerfReportLine(context, perfReport) {
168
- const avg = stat2("avg", perfReport.avgTime);
169
- const min = stat2("min", perfReport.minTime);
170
- const max = stat2("max", perfReport.maxTime);
171
- context.log("info", ` ${avg} ${min} ${max}`);
158
+ const avg = stat$1("avg", perfReport.avgTime);
159
+ const min = stat$1("min", perfReport.minTime);
160
+ const max = stat$1("max", perfReport.maxTime);
161
+ context.log("info", ` ${avg} ${min} ${max}`);
172
162
  }
173
163
  function printPerfStats(context, comparisonConfig, report) {
174
- context.log("info", "\nPerformance stats:");
175
- context.log("info", ` ${comparisonConfig.bundleL.name}:`);
176
- printPerfReportLine(context, report.perfReportL);
177
- context.log("info", ` ${comparisonConfig.bundleR.name}:`);
178
- printPerfReportLine(context, report.perfReportR);
179
- context.log("info", "");
164
+ context.log("info", "\nPerformance stats:");
165
+ context.log("info", ` ${comparisonConfig.bundleL.name}:`);
166
+ printPerfReportLine(context, report.perfReportL);
167
+ context.log("info", ` ${comparisonConfig.bundleR.name}:`);
168
+ printPerfReportLine(context, report.perfReportR);
169
+ context.log("info", "");
180
170
  }
181
-
182
- // src/vite-config-for-bundle.ts
183
- import { existsSync, readFileSync, statSync } from "fs";
184
- import { basename, dirname as dirname2, join as joinPath2, relative, resolve as resolvePath } from "path";
185
- import { fileURLToPath } from "url";
186
- import { nodeResolve } from "@rollup/plugin-node-resolve";
187
- import { encodeImplVars } from "@sdeverywhere/check-core";
188
- var __filename2 = fileURLToPath(import.meta.url);
189
- var __dirname2 = dirname2(__filename2);
171
+ //#endregion
172
+ //#region src/vite-config-for-bundle.ts
173
+ const __filename$2 = fileURLToPath(import.meta.url);
174
+ const __dirname$2 = dirname$1(__filename$2);
175
+ /**
176
+ * This is a virtual module plugin used to inject model-specific configuration
177
+ * values into the generated worker bundle.
178
+ *
179
+ * This follows the "Virtual Modules Convention" described here:
180
+ * https://vitejs.dev/guide/api-plugin.html#virtual-modules-convention
181
+ *
182
+ * TODO: This could be simplified by using `vite-plugin-virtual` but that
183
+ * doesn't seem to be working correctly in an ESM setting
184
+ */
190
185
  function injectModelSpec(context, modelSpec) {
191
- const prepDir = context.config.prepDir;
192
- const inputSpecs = [];
193
- for (const modelInputSpec of modelSpec.inputs) {
194
- if (modelInputSpec.defaultValue === void 0 || modelInputSpec.minValue === void 0 || modelInputSpec.maxValue === void 0) {
195
- let msg = "";
196
- msg += `WARNING: The {defaultValue,minValue,maxValue} properties are required by plugin-check, `;
197
- msg += `but are undefined in the InputSpec for '${modelInputSpec.varName}'. `;
198
- msg += `This input variable will be excluded from the model-check bundle until those properties `;
199
- msg += `are defined.`;
200
- console.warn(msg);
201
- continue;
202
- }
203
- const varId = context.canonicalVarId(modelInputSpec.varName);
204
- const inputId = modelInputSpec.inputId || varId;
205
- inputSpecs.push({
206
- inputId,
207
- varId,
208
- ...modelInputSpec
209
- });
210
- }
211
- const outputSpecs = modelSpec.outputs.map((o) => {
212
- return {
213
- varId: context.canonicalVarId(o.varName),
214
- ...o
215
- };
216
- });
217
- function readJsonListing() {
218
- const path = joinPath2(prepDir, "build", "processed.json");
219
- if (existsSync(path)) {
220
- const json = readFileSync(path, "utf8");
221
- return JSON.parse(json);
222
- } else {
223
- return {};
224
- }
225
- }
226
- const listing = readJsonListing();
227
- const varInstances = listing.varInstances || {};
228
- const encodedImplVars = encodeImplVars(varInstances);
229
- function stagedFileSize(filename) {
230
- const path = joinPath2(prepDir, "staged", "model", filename);
231
- if (existsSync(path)) {
232
- return statSync(path).size;
233
- } else {
234
- return 0;
235
- }
236
- }
237
- const modelSizeInBytes = stagedFileSize("generated-model.js");
238
- const dataSizeInBytes = stagedFileSize("static-data.ts");
239
- const moduleSrc = `
186
+ const prepDir = context.config.prepDir;
187
+ const inputSpecs = [];
188
+ for (const modelInputSpec of modelSpec.inputs) {
189
+ if (modelInputSpec.defaultValue === void 0 || modelInputSpec.minValue === void 0 || modelInputSpec.maxValue === void 0) {
190
+ let msg = "";
191
+ msg += `WARNING: The {defaultValue,minValue,maxValue} properties are required by plugin-check, `;
192
+ msg += `but are undefined in the InputSpec for '${modelInputSpec.varName}'. `;
193
+ msg += `This input variable will be excluded from the model-check bundle until those properties `;
194
+ msg += `are defined.`;
195
+ console.warn(msg);
196
+ continue;
197
+ }
198
+ const varId = context.canonicalVarId(modelInputSpec.varName);
199
+ const inputId = modelInputSpec.inputId || varId;
200
+ inputSpecs.push({
201
+ inputId,
202
+ varId,
203
+ ...modelInputSpec
204
+ });
205
+ }
206
+ const outputSpecs = modelSpec.outputs.map((o) => {
207
+ return {
208
+ varId: context.canonicalVarId(o.varName),
209
+ ...o
210
+ };
211
+ });
212
+ function readJsonListing() {
213
+ const path = join$1(prepDir, "build", "processed.json");
214
+ if (existsSync$1(path)) {
215
+ const json = readFileSync(path, "utf8");
216
+ return JSON.parse(json);
217
+ } else return {};
218
+ }
219
+ const varInstances = readJsonListing().varInstances || {};
220
+ const encodedImplVars = encodeImplVars(varInstances);
221
+ function stagedFileSize(filename) {
222
+ const path = join$1(prepDir, "staged", "model", filename);
223
+ if (existsSync$1(path)) return statSync(path).size;
224
+ else return 0;
225
+ }
226
+ const modelSizeInBytes = stagedFileSize("generated-model.js");
227
+ const dataSizeInBytes = stagedFileSize("static-data.ts");
228
+ const moduleSrc = `
240
229
  export const inputSpecs = ${JSON.stringify(inputSpecs)};
241
230
  export const outputSpecs = ${JSON.stringify(outputSpecs)};
242
231
  export const encodedImplVars = ${JSON.stringify(encodedImplVars)};
243
232
  export const modelSizeInBytes = ${modelSizeInBytes};
244
233
  export const dataSizeInBytes = ${dataSizeInBytes};
245
234
  `;
246
- const virtualModuleId = "virtual:model-spec";
247
- const resolvedVirtualModuleId = "\0" + virtualModuleId;
248
- return {
249
- name: "vite-plugin-virtual-custom",
250
- resolveId(id) {
251
- if (id === virtualModuleId) {
252
- return resolvedVirtualModuleId;
253
- }
254
- },
255
- load(id) {
256
- if (id === resolvedVirtualModuleId) {
257
- return moduleSrc;
258
- }
259
- }
260
- };
261
- }
262
- function overrideViteResolvePlugin(viteConfig) {
263
- const resolvePlugin = viteConfig.plugins.find((p) => p.name === "vite:resolve");
264
- if (resolvePlugin === void 0) {
265
- throw new Error("Failed to locate the built-in vite:resolve plugin");
266
- }
267
- const originalResolveId = resolvePlugin.resolveId;
268
- resolvePlugin.resolveId = async function resolveId(id, importer, options) {
269
- if (id.startsWith("./implementation") && importer.includes("threads/dist-esm")) {
270
- const idFileName = id.replace("./", "");
271
- const importerFileName = basename(importer);
272
- const resolvedId = importer.replace(importerFileName, `${idFileName}.js`);
273
- return {
274
- id: resolvedId,
275
- moduleSideEffects: false
276
- };
277
- }
278
- return await originalResolveId.handler.call(this, id, importer, options);
279
- };
235
+ const virtualModuleId = "virtual:model-spec";
236
+ const resolvedVirtualModuleId = "\0" + virtualModuleId;
237
+ return {
238
+ name: "vite-plugin-virtual-custom",
239
+ resolveId(id) {
240
+ if (id === virtualModuleId) return resolvedVirtualModuleId;
241
+ },
242
+ load(id) {
243
+ if (id === resolvedVirtualModuleId) return moduleSrc;
244
+ }
245
+ };
280
246
  }
281
247
  async function createViteConfigForBundle(context, modelSpec) {
282
- const root = resolvePath(__dirname2, "..", "template-bundle");
283
- const prepDir = context.config.prepDir;
284
- const outDir = relative(root, prepDir);
285
- const modelWorkerPath = joinPath2(prepDir, "staged", "model", "worker.js?raw");
286
- return {
287
- // Don't use an external config file
288
- configFile: false,
289
- // Use the root directory configured above
290
- root,
291
- // Don't clear the screen in dev mode so that we can see builder output
292
- clearScreen: false,
293
- // TODO: Disable vite output by default?
294
- // logLevel: 'silent',
295
- // Configure path aliases
296
- resolve: {
297
- alias: [
298
- // Inject the configured model worker
299
- {
300
- find: "@_model_worker_",
301
- replacement: modelWorkerPath
302
- },
303
- // XXX: Prevent Vite from using the `browser` section of `threads/package.json`
304
- // since we want to force the use of the general module (under dist-esm) that chooses
305
- // the correct implementation (Web Worker vs worker_threads) at runtime. Currently
306
- // Vite's library mode is browser focused and generally chooses the right imports,
307
- // except in the case of the threads package where we want to use the generic
308
- // `implementation.js` that chooses between Web Worker and worker_threads at runtime.
309
- // Note that we could in theory set `resolve.browserField` to false, but that would
310
- // make Vite not use the browser field for all other packages, and there is not
311
- // currently a way to tell Vite to use the browser field on a case-by-case basis.
312
- // So for now we need this workaround here to make it resolve to `dist-esm`, and then
313
- // a second workaround in `overrideViteResolvePlugin` to prevent the resolver from
314
- // using the browser field when resolving the threads package.
315
- {
316
- find: "threads",
317
- replacement: "threads",
318
- customResolver: async function(source, importer, options) {
319
- const customResolver = nodeResolve({ browser: false });
320
- const resolveIdHook = customResolver.resolveId;
321
- const resolveIdFn = typeof resolveIdHook === "function" ? resolveIdHook : resolveIdHook.handler;
322
- const resolved = await resolveIdFn.call(this, source, importer, options);
323
- if (source === "threads/worker") {
324
- return resolved.id.replace("worker.mjs", "dist-esm/worker/index.js");
325
- } else {
326
- return resolved.id.replace("index.mjs", "dist-esm/index.js");
327
- }
328
- }
329
- }
330
- ]
331
- },
332
- plugins: [
333
- // Use a virtual module plugin to inject the model spec values
334
- injectModelSpec(context, modelSpec),
335
- // XXX: Install a wrapper around the built-in `vite:resolve` plugin so that we can
336
- // override the default resolver behavior that tries to resolve the `browser` section
337
- // of the `package.json` for the threads package.
338
- {
339
- name: "vite-plugin-override-resolve",
340
- configResolved(viteConfig) {
341
- overrideViteResolvePlugin(viteConfig);
342
- }
343
- }
344
- ],
345
- build: {
346
- // Write output files to the configured directory (instead of the default `dist`);
347
- // note that this must be relative to the project `root`
348
- outDir,
349
- emptyOutDir: false,
350
- // Uncomment for debugging purposes
351
- // minify: false,
352
- lib: {
353
- entry: "./src/index.ts",
354
- formats: ["es"],
355
- fileName: () => "check-bundle.js"
356
- },
357
- rollupOptions: {
358
- // Don't transform Node imports used by threads.js
359
- external: ["events", "os", "path", "url"],
360
- // XXX: Insert custom code at the top of the generated bundle that defines
361
- // the special `__non_webpack_require__` function that is used by threads.js
362
- // in its Node implementation. This import ensures that threads.js uses
363
- // the native `worker_threads` implementation when using the bundle in a
364
- // Node environment. When importing the bundle for use in the browser,
365
- // Vite will transform this import into an empty module due to the empty
366
- // polyfill that is configured in `vite-config-for-report.ts`.
367
- output: {
368
- banner: `
369
- import * as worker_threads from 'worker_threads'
370
- let __non_webpack_require__ = () => {
371
- return worker_threads;
372
- };
373
- `
374
- },
375
- onwarn: (warning, warn) => {
376
- if (warning.code !== "EVAL") {
377
- warn(warning);
378
- }
379
- }
380
- }
381
- }
382
- };
248
+ const root = resolve$1(__dirname$2, "..", "template-bundle");
249
+ const prepDir = context.config.prepDir;
250
+ const outDir = relative$1(root, prepDir);
251
+ return {
252
+ configFile: false,
253
+ root,
254
+ clearScreen: false,
255
+ resolve: { alias: [{
256
+ find: "@_model_worker_",
257
+ replacement: join$1(prepDir, "staged", "model", "worker.js?raw")
258
+ }] },
259
+ plugins: [injectModelSpec(context, modelSpec)],
260
+ build: {
261
+ outDir,
262
+ emptyOutDir: false,
263
+ lib: {
264
+ entry: "./src/index.ts",
265
+ formats: ["es"],
266
+ fileName: () => "check-bundle.js"
267
+ }
268
+ }
269
+ };
383
270
  }
384
-
385
- // src/vite-config-for-report.ts
386
- import { existsSync as existsSync2, mkdirSync } from "fs";
387
- import { dirname as dirname3, relative as relative3, join as joinPath4, resolve as resolvePath2 } from "path";
388
- import { fileURLToPath as fileURLToPath3 } from "url";
389
- import replace from "@rollup/plugin-replace";
390
-
391
- // src/vite-local-bundles-plugin.ts
392
- import { readdir, readFile as readFile2, stat as stat3 } from "fs/promises";
393
- import { join as joinPath3, relative as relative2, sep } from "path";
394
- import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
395
- import chokidar from "chokidar";
271
+ //#endregion
272
+ //#region src/vite-local-bundles-plugin.ts
273
+ /**
274
+ * Vite plugin that provides a bridge to the model-check report app to allow access
275
+ * to the local bundles directory when running in local development mode.
276
+ *
277
+ * This plugin adds an HMR (Hot Module Replacement) event handler that listens for
278
+ * 'list-bundles' and 'download-bundle' events from the client.
279
+ *
280
+ * Note that when a request fails, the handlers below log only the error message (along
281
+ * with the bundle name and URL) instead of the raw error object, since the latter can
282
+ * produce a long and unhelpful stack trace in the console. The full message is sent
283
+ * back to the client, which reports it in the browser console.
284
+ *
285
+ * @param bundlesDir The absolute path to the bundles directory.
286
+ * @param currentBundlePath The absolute path to the current bundle file.
287
+ * @param fetchRemoteBundle Optional function for fetching remote bundle files.
288
+ */
396
289
  function localBundlesPlugin(bundlesDir, currentBundlePath, fetchRemoteBundle) {
397
- return {
398
- name: "sde-local-bundles",
399
- configureServer(server) {
400
- const watcher = chokidar.watch(bundlesDir, {
401
- // Don't send initial "file added" events
402
- ignoreInitial: true,
403
- // XXX: Include a delay, otherwise on macOS we sometimes get multiple
404
- // change events when a file is saved just once
405
- awaitWriteFinish: {
406
- stabilityThreshold: 200
407
- },
408
- // Watch up to 10 levels deep
409
- depth: 10
410
- });
411
- watcher.on("all", (event) => {
412
- if (event === "add" || event === "unlink") {
413
- server.ws.send("bundles-changed", {});
414
- }
415
- });
416
- server.httpServer?.on("close", () => {
417
- watcher.close();
418
- });
419
- server.ws.on("list-bundles", async (_, client) => {
420
- try {
421
- const bundles = await scanBundlesRecursively(bundlesDir, bundlesDir);
422
- const currentBundleStats = await stat3(currentBundlePath);
423
- bundles.push({
424
- name: "current",
425
- url: "current",
426
- lastModified: currentBundleStats.mtime.toISOString()
427
- });
428
- client.send("list-bundles-success", { bundles });
429
- } catch (error) {
430
- console.error(`[sde-local-bundles] Failed to list bundles:`, error);
431
- client.send("list-bundles-error", { error: error.message });
432
- }
433
- });
434
- server.ws.on("load-bundle", async (data, client) => {
435
- const { url, name } = data;
436
- try {
437
- let sourceCode;
438
- if (url.startsWith("file://")) {
439
- const filePath = fileURLToPath2(url);
440
- sourceCode = await readFile2(filePath, "utf-8");
441
- } else if (url.startsWith("https://") || url.startsWith("http://")) {
442
- const fullUrl = `${url}?cb=${Date.now()}`;
443
- if (fetchRemoteBundle) {
444
- sourceCode = await fetchRemoteBundle(fullUrl);
445
- } else {
446
- const response = await fetch(fullUrl);
447
- if (!response.ok) {
448
- throw new Error(`Failed to fetch bundle: ${response.status} ${response.statusText}`);
449
- }
450
- sourceCode = await response.text();
451
- }
452
- } else {
453
- throw new Error(`Unsupported URL scheme: ${url}`);
454
- }
455
- client.send("load-bundle-success", { name, url, sourceCode });
456
- } catch (error) {
457
- console.error(`[sde-local-bundles] Failed to load bundle:`, error);
458
- client.send("load-bundle-error", { name, url, error: error.message });
459
- }
460
- });
461
- server.ws.on("download-bundle", async (data, client) => {
462
- const { url, name, lastModified } = data;
463
- try {
464
- console.log(`[sde-local-bundles] Downloading bundle: name=${name} url=${url}`);
465
- const filePath = await downloadBundle(url, name, lastModified, bundlesDir, fetchRemoteBundle);
466
- console.log(`[sde-local-bundles] Downloaded bundle to ${filePath}`);
467
- client.send("download-bundle-success", { name, filePath: `${name}.js` });
468
- } catch (error) {
469
- console.error(`[sde-local-bundles] Failed to download bundle:`, error);
470
- client.send("download-bundle-error", { name, error: error.message });
471
- }
472
- });
473
- server.ws.on("copy-bundle", async (data, client) => {
474
- const { url, name, newName } = data;
475
- try {
476
- console.log(`[sde-local-bundles] Copying bundle: src=${name} dst=${newName}`);
477
- const filePath = await copyBundle(url, newName, bundlesDir);
478
- console.log(`[sde-local-bundles] Copied bundle to ${filePath}`);
479
- client.send("copy-bundle-success", { name: newName, filePath: `${newName}.js` });
480
- } catch (error) {
481
- console.error(`[sde-local-bundles] Failed to copy bundle:`, error);
482
- client.send("copy-bundle-error", { name, error: error.message });
483
- }
484
- });
485
- }
486
- };
290
+ return {
291
+ name: "sde-local-bundles",
292
+ configureServer(server) {
293
+ const watcher = chokidar.watch(bundlesDir, {
294
+ ignoreInitial: true,
295
+ awaitWriteFinish: { stabilityThreshold: 200 },
296
+ depth: 10
297
+ });
298
+ watcher.on("all", (event) => {
299
+ if (event === "add" || event === "unlink") server.ws.send("bundles-changed", {});
300
+ });
301
+ server.httpServer?.on("close", () => {
302
+ watcher.close();
303
+ });
304
+ server.ws.on("list-bundles", async (_, client) => {
305
+ try {
306
+ const bundles = await scanBundlesRecursively(bundlesDir, bundlesDir);
307
+ const currentBundleStats = await stat(currentBundlePath);
308
+ bundles.push({
309
+ name: "current",
310
+ url: "current",
311
+ lastModified: currentBundleStats.mtime.toISOString()
312
+ });
313
+ client.send("list-bundles-success", { bundles });
314
+ } catch (error) {
315
+ console.error(`[sde-local-bundles] Failed to list bundles in ${bundlesDir}: ${error.message}`);
316
+ client.send("list-bundles-error", { error: error.message });
317
+ }
318
+ });
319
+ server.ws.on("load-bundle", async (data, client) => {
320
+ const { url, name } = data;
321
+ try {
322
+ let sourceCode;
323
+ if (url.startsWith("file://")) {
324
+ const filePath = fileURLToPath$1(url);
325
+ sourceCode = await readFile(filePath, "utf-8");
326
+ } else if (url.startsWith("https://") || url.startsWith("http://")) {
327
+ const fullUrl = `${url}?cb=${Date.now()}`;
328
+ if (fetchRemoteBundle) sourceCode = await fetchRemoteBundle(fullUrl);
329
+ else {
330
+ const response = await fetch(fullUrl);
331
+ if (!response.ok) throw new Error(`Failed to fetch bundle: ${response.status} ${response.statusText}`);
332
+ sourceCode = await response.text();
333
+ }
334
+ } else throw new Error(`Unsupported URL scheme: ${url}`);
335
+ client.send("load-bundle-success", {
336
+ name,
337
+ url,
338
+ sourceCode
339
+ });
340
+ } catch (error) {
341
+ console.error(`[sde-local-bundles] Failed to load bundle '${name}' from ${url}: ${error.message}`);
342
+ client.send("load-bundle-error", {
343
+ name,
344
+ url,
345
+ error: error.message
346
+ });
347
+ }
348
+ });
349
+ server.ws.on("download-bundle", async (data, client) => {
350
+ const { url, name, lastModified } = data;
351
+ try {
352
+ console.log(`[sde-local-bundles] Downloading bundle: name=${name} url=${url}`);
353
+ const filePath = await downloadBundle(url, name, lastModified, bundlesDir, fetchRemoteBundle);
354
+ console.log(`[sde-local-bundles] Downloaded bundle to ${filePath}`);
355
+ client.send("download-bundle-success", {
356
+ name,
357
+ filePath: `${name}.js`
358
+ });
359
+ } catch (error) {
360
+ console.error(`[sde-local-bundles] Failed to download bundle '${name}' from ${url}: ${error.message}`);
361
+ client.send("download-bundle-error", {
362
+ name,
363
+ error: error.message
364
+ });
365
+ }
366
+ });
367
+ server.ws.on("copy-bundle", async (data, client) => {
368
+ const { url, name, newName } = data;
369
+ try {
370
+ console.log(`[sde-local-bundles] Copying bundle: src=${name} dst=${newName}`);
371
+ const filePath = await copyBundle(url, newName, bundlesDir);
372
+ console.log(`[sde-local-bundles] Copied bundle to ${filePath}`);
373
+ client.send("copy-bundle-success", {
374
+ name: newName,
375
+ filePath: `${newName}.js`
376
+ });
377
+ } catch (error) {
378
+ console.error(`[sde-local-bundles] Failed to copy bundle '${name}' to '${newName}': ${error.message}`);
379
+ client.send("copy-bundle-error", {
380
+ name,
381
+ error: error.message
382
+ });
383
+ }
384
+ });
385
+ }
386
+ };
487
387
  }
388
+ /**
389
+ * Recursively scan a directory for .js files.
390
+ *
391
+ * @param dir The directory to scan.
392
+ * @param baseDir The base directory (used for calculating relative paths).
393
+ * @returns An array of bundle information.
394
+ */
488
395
  async function scanBundlesRecursively(dir, baseDir) {
489
- const bundles = [];
490
- const entries = await readdir(dir, { withFileTypes: true });
491
- for (const entry of entries) {
492
- const fullPath = joinPath3(dir, entry.name);
493
- if (entry.isDirectory()) {
494
- const subBundles = await scanBundlesRecursively(fullPath, baseDir);
495
- bundles.push(...subBundles);
496
- } else if (entry.isFile() && entry.name.endsWith(".js")) {
497
- const stats = await stat3(fullPath);
498
- const relativePath = relative2(baseDir, fullPath);
499
- const name = relativePath.replace(/\.js$/, "").split(sep).join("/");
500
- bundles.push({
501
- name,
502
- url: pathToFileURL(fullPath).toString(),
503
- lastModified: stats.mtime.toISOString()
504
- });
505
- }
506
- }
507
- return bundles;
396
+ const bundles = [];
397
+ const entries = await readdir(dir, { withFileTypes: true });
398
+ for (const entry of entries) {
399
+ const fullPath = join(dir, entry.name);
400
+ if (entry.isDirectory()) {
401
+ const subBundles = await scanBundlesRecursively(fullPath, baseDir);
402
+ bundles.push(...subBundles);
403
+ } else if (entry.isFile() && entry.name.endsWith(".js")) {
404
+ const stats = await stat(fullPath);
405
+ const name = relative(baseDir, fullPath).replace(/\.js$/, "").split(sep).join("/");
406
+ bundles.push({
407
+ name,
408
+ url: pathToFileURL(fullPath).toString(),
409
+ lastModified: stats.mtime.toISOString()
410
+ });
411
+ }
412
+ }
413
+ return bundles;
508
414
  }
509
-
510
- // src/vite-config-for-report.ts
511
- var __filename3 = fileURLToPath3(import.meta.url);
512
- var __dirname3 = dirname3(__filename3);
415
+ //#endregion
416
+ //#region src/vite-config-for-report.ts
417
+ const __filename$1 = fileURLToPath$1(import.meta.url);
418
+ const __dirname$1 = dirname(__filename$1);
419
+ /**
420
+ * NOTE: This function currently only supports creating a Vite config for the
421
+ * model-check report when the current/baseline bundles are local files. If
422
+ * you want to use remote bundles, you must first download them to the local
423
+ * `bundles` directory and then pass `LocalBundleSpec` instances that include
424
+ * the local bundle file paths.
425
+ */
513
426
  function createViteConfigForReport(mode, options, projDir, prepDir, currentBundleSpec, baselineBundleSpec, testConfigPath, suiteSummary) {
514
- const root = resolvePath2(__dirname3, "..", "template-report");
515
- const bundlesDir = resolvePath2(projDir, "bundles");
516
- if (!existsSync2(bundlesDir)) {
517
- mkdirSync(bundlesDir, { recursive: true });
518
- }
519
- const templateSrcDir = resolvePath2(root, "src");
520
- const relProjDir = relative3(templateSrcDir, projDir);
521
- const relProjDirPath = relProjDir.replaceAll("\\", "/");
522
- const bundlesPath = `${relProjDirPath}/bundles/**/*.js`;
523
- let reportPath;
524
- if (options?.reportPath) {
525
- reportPath = options.reportPath;
526
- } else {
527
- reportPath = joinPath4(prepDir, "check-report");
528
- }
529
- const outDir = relative3(root, reportPath);
530
- const suiteSummaryJson = suiteSummary ? JSON.stringify(suiteSummary) : "";
531
- const alias = (find, replacement) => {
532
- return {
533
- find,
534
- replacement
535
- };
536
- };
537
- const noopPolyfillAlias = (find) => {
538
- return {
539
- find,
540
- replacement: "/polyfills/noop-polyfills.ts"
541
- };
542
- };
543
- return {
544
- // Don't use an external config file
545
- configFile: false,
546
- // Use the root directory configured above
547
- root,
548
- // Use `.` as the base directory (instead of the default `/`); this controls
549
- // how the path to the js/css files are generated in `index.html`
550
- base: "",
551
- // Use a custom cache directory under `prepDir`, as otherwise Vite will use
552
- // `packages/plugin-check/template-report/node_modules/.vite`, and we want to
553
- // avoid generating files in `template-report` (which should be read-only)
554
- cacheDir: joinPath4(prepDir, ".vite-check-report"),
555
- // Load static files from `static` (instead of the default `public`)
556
- // publicDir: 'static',
557
- // Don't clear the screen in dev mode so that we can see builder output
558
- clearScreen: false,
559
- // TODO
560
- // logLevel: 'silent',
561
- optimizeDeps: {
562
- // Prevent Vite from examining other html files when scanning entrypoints
563
- // for dependency optimization
564
- entries: ["index.html"],
565
- // XXX: When plugin-check is installed via pnpm, the Vite dev server seems
566
- // to have no trouble resolving other dependencies using the optimizeDeps
567
- // mechanism. However, this fails when the package is installed via yarn
568
- // or npm (probably due to the fact that the `template-report` directory
569
- // is located under the top-level `node_modules` directory); in the browser,
570
- // there will be "import not found" errors for the packages referenced below.
571
- // As a terrible workaround, explicitly include the direct dependencies so
572
- // that Vite optimizes them; this works for pnpm, yarn, and npm. We should
573
- // find a less fragile solution.
574
- include: [
575
- // from check-core
576
- "@sdeverywhere/check-core > assert-never",
577
- "@sdeverywhere/check-core > ajv",
578
- "@sdeverywhere/check-core > neverthrow",
579
- "@sdeverywhere/check-core > yaml",
580
- // from check-ui-shell
581
- "@sdeverywhere/check-ui-shell > fontfaceobserver",
582
- "@sdeverywhere/check-ui-shell > copy-text-to-clipboard",
583
- "@sdeverywhere/check-ui-shell > chart.js"
584
- ],
585
- exclude: [
586
- // XXX: The threads.js implementation references `tiny-worker` as an optional
587
- // dependency, but it doesn't get used at runtime, so we can just exclude it
588
- // so that Vite doesn't complain in dev mode
589
- "tiny-worker"
590
- // XXX: Similarly, chart.js treats `moment` as an optional dependency, but we
591
- // don't use it at runtime; we need to exclude it here, otherwise Vite will
592
- // complain about missing dependencies in dev mode
593
- // 'moment'
594
- ]
595
- },
596
- // Configure path aliases
597
- resolve: {
598
- alias: [
599
- // Use the configured "baseline" bundle if defined, otherwise use the "empty" bundle
600
- // (which will cause comparison tests to be skipped)
601
- alias("@_baseline_bundle_", baselineBundleSpec?.path || "/src/empty-bundle.ts"),
602
- // Use the configured "current" bundle
603
- alias("@_current_bundle_", currentBundleSpec.path),
604
- // Use the configured test config file
605
- alias("@_test_config_", testConfigPath),
606
- // Make the overlay use the `messages.html` file that is written to the prep directory
607
- alias("@_prep_", prepDir),
608
- // XXX: Include no-op polyfills for these modules that are used in the Node-specific
609
- // implementation of threads.js; this allows us to use one bundle that works in both
610
- // Node and browser environments
611
- noopPolyfillAlias("events"),
612
- noopPolyfillAlias("fs"),
613
- noopPolyfillAlias("os"),
614
- noopPolyfillAlias("path"),
615
- noopPolyfillAlias("url"),
616
- noopPolyfillAlias("worker_threads")
617
- ]
618
- },
619
- // Inject special values into the generated JS
620
- define: {
621
- // Inject the summary JSON into the build
622
- __SUITE_SUMMARY_JSON__: JSON.stringify(suiteSummaryJson),
623
- // Inject the baseline bundle name
624
- __BASELINE_NAME__: JSON.stringify(baselineBundleSpec?.name || ""),
625
- // Inject the current bundle name
626
- __CURRENT_NAME__: JSON.stringify(currentBundleSpec.name),
627
- // Inject the remote bundles URL
628
- __REMOTE_BUNDLES_URL__: JSON.stringify(options?.remoteBundlesUrl || "")
629
- },
630
- plugins: [
631
- // Inject special values into the generated JS
632
- // TODO: We currently have to use `@rollup/plugin-replace` instead of Vite's
633
- // built-in `define` feature because the latter does not seem to run before
634
- // the glob handler (which requires the glob to be injected as a literal)
635
- replace({
636
- preventAssignment: true,
637
- delimiters: ["", ""],
638
- values: {
639
- // Inject the path for baseline bundles
640
- // XXX: Note that we use './bundles/**/*.txt' instead of something special
641
- // like './__BASELINE_BUNDLES_PATH__' because sometimes Vite's dependency
642
- // scanner sees the latter (instead of the injected path) and reports
643
- // an error since the path does not exist. As a workaround, we use
644
- // './bundles/**/*.txt', which gets interpreted as the valid path
645
- // '.../template-report/src/bundles/**/*.txt' (see `bundles/unused.txt`).
646
- "./bundles/**/*.txt": bundlesPath
647
- }
648
- }),
649
- // When local development mode is active, enable the local bundles plugin that
650
- // allows the report app to access the local bundles directory
651
- ...mode === "watch" ? [localBundlesPlugin(bundlesDir, currentBundleSpec.path, options?.fetchRemoteBundle)] : []
652
- ],
653
- build: {
654
- // Write output files to the configured directory (instead of the default `dist`);
655
- // note that this must be relative to the project `root`
656
- outDir,
657
- // Write js/css files to `public` (instead of the default `<outDir>/assets`)
658
- assetsDir: "",
659
- rollupOptions: {
660
- output: {
661
- // XXX: Prevent vite from creating a separate `vendor.js` file
662
- manualChunks: void 0
663
- },
664
- onwarn: (warning, warn) => {
665
- if (warning.code !== "EVAL") {
666
- warn(warning);
667
- }
668
- }
669
- }
670
- },
671
- server: {
672
- // Run the dev server at `localhost:8081` by default
673
- port: options?.serverPort || 8081,
674
- // Open the app in the browser by default
675
- open: "/index.html",
676
- // XXX: Add a small delay, otherwise on macOS we sometimes get multiple
677
- // change events when a file is saved just once. That is a relatively
678
- // harmless issue except that it causes redundant messages in the console
679
- // and can cause extra churn when refreshing the app.
680
- watch: {
681
- awaitWriteFinish: {
682
- stabilityThreshold: 100
683
- }
684
- }
685
- }
686
- };
427
+ const root = resolve(__dirname$1, "..", "template-report");
428
+ const bundlesDir = resolve(projDir, "bundles");
429
+ if (!existsSync(bundlesDir)) mkdirSync(bundlesDir, { recursive: true });
430
+ let reportPath;
431
+ if (options?.reportPath) reportPath = options.reportPath;
432
+ else reportPath = join(prepDir, "check-report");
433
+ const outDir = relative(root, reportPath);
434
+ const suiteSummaryJson = suiteSummary ? JSON.stringify(suiteSummary) : "";
435
+ const alias = (find, replacement) => {
436
+ return {
437
+ find,
438
+ replacement
439
+ };
440
+ };
441
+ const noopPolyfillAlias = (find) => {
442
+ return {
443
+ find,
444
+ replacement: "/polyfills/noop-polyfills.ts"
445
+ };
446
+ };
447
+ return {
448
+ configFile: false,
449
+ root,
450
+ base: "",
451
+ cacheDir: join(prepDir, ".vite-check-report"),
452
+ clearScreen: false,
453
+ optimizeDeps: {
454
+ entries: ["index.html"],
455
+ include: [
456
+ "@sdeverywhere/check-core > assert-never",
457
+ "@sdeverywhere/check-core > ajv",
458
+ "@sdeverywhere/check-core > neverthrow",
459
+ "@sdeverywhere/check-core > yaml",
460
+ "@sdeverywhere/check-ui-shell > fontfaceobserver",
461
+ "@sdeverywhere/check-ui-shell > copy-text-to-clipboard",
462
+ "@sdeverywhere/check-ui-shell > chart.js"
463
+ ],
464
+ exclude: []
465
+ },
466
+ resolve: { alias: [
467
+ alias("@_baseline_bundle_", baselineBundleSpec?.path || "/src/empty-bundle.ts"),
468
+ alias("@_current_bundle_", currentBundleSpec.path),
469
+ alias("@_test_config_", testConfigPath),
470
+ alias("@_prep_", prepDir),
471
+ noopPolyfillAlias("events"),
472
+ noopPolyfillAlias("fs"),
473
+ noopPolyfillAlias("os"),
474
+ noopPolyfillAlias("path"),
475
+ noopPolyfillAlias("url"),
476
+ noopPolyfillAlias("worker_threads"),
477
+ noopPolyfillAlias("tiny-worker")
478
+ ] },
479
+ define: {
480
+ __SUITE_SUMMARY_JSON__: JSON.stringify(suiteSummaryJson),
481
+ __BASELINE_NAME__: JSON.stringify(baselineBundleSpec?.name || ""),
482
+ __CURRENT_NAME__: JSON.stringify(currentBundleSpec.name),
483
+ __REMOTE_BUNDLES_URL__: JSON.stringify(options?.remoteBundlesUrl || "")
484
+ },
485
+ plugins: [...mode === "watch" ? [localBundlesPlugin(bundlesDir, currentBundleSpec.path, options?.fetchRemoteBundle)] : []],
486
+ build: {
487
+ outDir,
488
+ assetsDir: "",
489
+ rolldownOptions: { checks: { eval: false } }
490
+ },
491
+ server: {
492
+ port: options?.serverPort || 8081,
493
+ open: "/index.html",
494
+ watch: { awaitWriteFinish: { stabilityThreshold: 100 } }
495
+ }
496
+ };
687
497
  }
688
-
689
- // src/vite-config-for-tests.ts
690
- import { dirname as dirname4, relative as relative4, resolve as resolvePath3 } from "path";
691
- import { fileURLToPath as fileURLToPath4 } from "url";
692
- import replace2 from "@rollup/plugin-replace";
693
- var __filename4 = fileURLToPath4(import.meta.url);
694
- var __dirname4 = dirname4(__filename4);
498
+ //#endregion
499
+ //#region src/vite-inject-literals-plugin.ts
500
+ /**
501
+ * Return a Vite plugin that replaces literal strings in source files before
502
+ * Vite's own transforms run.
503
+ *
504
+ * This is a minimal replacement for `@rollup/plugin-replace` (as previously
505
+ * configured with empty delimiters, i.e., plain string substitution). We use
506
+ * this instead of Vite's built-in `define` feature because `define`
507
+ * replacements are not applied before Vite's `import.meta.glob` handler runs,
508
+ * and the glob handler requires the glob pattern to appear as a literal in
509
+ * the source.
510
+ *
511
+ * @param values A map of literal search strings to their replacement strings.
512
+ */
513
+ function injectLiteralsPlugin(values) {
514
+ const entries = Object.entries(values);
515
+ return {
516
+ name: "vite-plugin-inject-literals",
517
+ transform(code) {
518
+ let transformed = code;
519
+ let changed = false;
520
+ for (const [find, replacement] of entries) if (transformed.includes(find)) {
521
+ transformed = transformed.replaceAll(find, replacement);
522
+ changed = true;
523
+ }
524
+ if (changed) return {
525
+ code: transformed,
526
+ map: null
527
+ };
528
+ else return;
529
+ }
530
+ };
531
+ }
532
+ //#endregion
533
+ //#region src/vite-config-for-tests.ts
534
+ const __filename = fileURLToPath(import.meta.url);
535
+ const __dirname = dirname$1(__filename);
695
536
  function createViteConfigForTests(mode, projDir, prepDir) {
696
- const root = resolvePath3(__dirname4, "..", "template-tests");
697
- const templateSrcDir = resolvePath3(root, "src");
698
- const relProjDir = relative4(templateSrcDir, projDir);
699
- const relProjDirPath = relProjDir.replaceAll("\\", "/");
700
- const yamlCheckGlobPatterns = `['${relProjDirPath}/**/checks/*.yaml', '${relProjDirPath}/**/*.check.yaml']`;
701
- const yamlComparisonGlobPatterns = `['${relProjDirPath}/**/comparisons/*.yaml']`;
702
- const outDir = relative4(root, prepDir);
703
- return {
704
- // Don't use an external config file
705
- configFile: false,
706
- // Use the root directory configured above
707
- root,
708
- // Don't clear the screen in dev mode so that we can see builder output
709
- clearScreen: false,
710
- // TODO: Disable vite output by default?
711
- // logLevel: 'silent',
712
- plugins: [
713
- // Inject special values into the generated JS
714
- // TODO: We currently have to use `@rollup/plugin-replace` instead of Vite's
715
- // built-in `define` feature because the latter does not seem to run before
716
- // the glob handler (which requires the glob to be injected as a literal)
717
- replace2({
718
- preventAssignment: true,
719
- delimiters: ["", ""],
720
- values: {
721
- // Inject the glob patterns for matching model check yaml files
722
- '"./__YAML_CHECK_GLOB_PATTERNS__"': yamlCheckGlobPatterns,
723
- // Inject the glob patterns for matching model comparison yaml files
724
- '"./__YAML_COMPARISON_GLOB_PATTERNS__"': yamlComparisonGlobPatterns
725
- }
726
- })
727
- ],
728
- build: {
729
- // Write output files to the configured directory (instead of the default `dist`);
730
- // note that this must be relative to the project `root`
731
- outDir,
732
- emptyOutDir: false,
733
- lib: {
734
- entry: "./src/index.ts",
735
- formats: ["es"],
736
- fileName: () => "check-tests.js"
737
- },
738
- // Enable watch mode if requested
739
- watch: mode === "watch" && {},
740
- rollupOptions: {
741
- // Prevent dependencies from being included in packaged library
742
- // TODO: For now we include check-core in the packaged library so that its
743
- // dependencies are correctly resolved at runtime. Ideally this would only
744
- // include a couple functions that are used for defining tests, but Vite 2.x
745
- // does not implement tree shaking for ES libraries, which means the generated
746
- // library is much larger than it needs to be. Once we upgrade to Vite 3.x,
747
- // the generated library should be smaller; see related fix:
748
- // https://github.com/vitejs/vite/pull/8737
749
- // external: Object.keys(pkg.dependencies)
750
- }
751
- }
752
- };
537
+ const root = resolve$1(__dirname, "..", "template-tests");
538
+ const templateSrcDir = resolve$1(root, "src");
539
+ const relProjDirPath = relative$1(templateSrcDir, projDir).replaceAll("\\", "/");
540
+ const yamlCheckGlobPatterns = `['${relProjDirPath}/**/checks/*.yaml', '${relProjDirPath}/**/*.check.yaml']`;
541
+ const yamlComparisonGlobPatterns = `['${relProjDirPath}/**/comparisons/*.yaml']`;
542
+ const outDir = relative$1(root, prepDir);
543
+ return {
544
+ configFile: false,
545
+ root,
546
+ clearScreen: false,
547
+ plugins: [injectLiteralsPlugin({
548
+ "\"./__YAML_CHECK_GLOB_PATTERNS__\"": yamlCheckGlobPatterns,
549
+ "\"./__YAML_COMPARISON_GLOB_PATTERNS__\"": yamlComparisonGlobPatterns
550
+ })],
551
+ build: {
552
+ outDir,
553
+ emptyOutDir: false,
554
+ lib: {
555
+ entry: "./src/index.ts",
556
+ formats: ["es"],
557
+ fileName: () => "check-tests.js"
558
+ },
559
+ watch: mode === "watch" && {}
560
+ }
561
+ };
753
562
  }
754
-
755
- // src/plugin.ts
563
+ //#endregion
564
+ //#region src/plugin.ts
756
565
  function checkPlugin(options) {
757
- return new CheckPlugin(options);
566
+ return new CheckPlugin(options);
758
567
  }
759
568
  var CheckPlugin = class {
760
- constructor(options) {
761
- this.options = options;
762
- this.firstBuild = true;
763
- }
764
- async watch(config) {
765
- if (this.options?.testConfigPath === void 0) {
766
- await this.genTestConfig("watch", config);
767
- }
768
- const testOptions = await this.resolveTestOptions("watch", config);
769
- const viteConfig = await this.createViteConfigForReport("watch", config, testOptions, void 0);
770
- const server = await createServer(viteConfig);
771
- await server.listen();
772
- }
773
- // TODO: Note that this plugin runs as a `postBuild` step because it currently
774
- // needs to run after other plugins, and those plugins need to run after the
775
- // staged files are copied to their final destination(s). We should probably
776
- // make it configurable so that it can either be run as a `postGenerate` or a
777
- // `postBuild` step.
778
- async postBuild(context, modelSpec) {
779
- const firstBuild = this.firstBuild;
780
- this.firstBuild = false;
781
- if (this.options?.current?.path === void 0 && this.options?.current?.url === void 0) {
782
- if (context.config.mode === "development") {
783
- await this.copyPreviousBundle(context.config);
784
- }
785
- context.log("info", "Generating model check bundle...");
786
- await this.genCurrentBundle(context, modelSpec);
787
- }
788
- if (this.options?.testConfigPath === void 0) {
789
- if (context.config.mode === "production" || firstBuild) {
790
- context.log("info", "Generating model check test configuration...");
791
- await this.genTestConfig("bundle", context.config);
792
- }
793
- }
794
- if (context.config.mode === "production") {
795
- const testOptions = await this.resolveTestOptions("bundle", context.config);
796
- return this.runChecks(context, testOptions);
797
- } else {
798
- return true;
799
- }
800
- }
801
- async copyPreviousBundle(config) {
802
- const currentBundleFile = joinPath5(config.prepDir, "check-bundle.js");
803
- if (existsSync3(currentBundleFile)) {
804
- const bundlesDir = joinPath5(config.rootDir, "bundles");
805
- if (!existsSync3(bundlesDir)) {
806
- await mkdir2(bundlesDir, { recursive: true });
807
- }
808
- const previousBundleFile = joinPath5(bundlesDir, "previous.js");
809
- await copyFile(currentBundleFile, previousBundleFile);
810
- }
811
- }
812
- async genCurrentBundle(context, modelSpec) {
813
- const viteConfig = await createViteConfigForBundle(context, modelSpec);
814
- await build(viteConfig);
815
- }
816
- async genTestConfig(mode, config) {
817
- const rootDir = config.rootDir;
818
- const prepDir = config.prepDir;
819
- const viteConfig = createViteConfigForTests(mode, rootDir, prepDir);
820
- await build(viteConfig);
821
- }
822
- async runChecks(context, testOptions) {
823
- context.log("info", "Running model checks...");
824
- async function importBundleModule(bundleSpec) {
825
- return import(relativeToSourcePath(bundleSpec.path));
826
- }
827
- const moduleR = await importBundleModule(testOptions.currentBundleSpec);
828
- const bundleR = moduleR.createBundle();
829
- const bundleNameR = testOptions.currentBundleSpec.name;
830
- let bundleL;
831
- let bundleNameL;
832
- if (testOptions.baselineBundleSpec !== void 0) {
833
- const moduleL = await importBundleModule(testOptions.baselineBundleSpec);
834
- const rawBundleL = moduleL.createBundle();
835
- if (rawBundleL.version === bundleR.version) {
836
- bundleL = rawBundleL;
837
- bundleNameL = testOptions.baselineBundleSpec.name || "base";
838
- } else {
839
- console.warn(
840
- `WARNING: Bundle version mismatch (baseline=${rawBundleL.version} current=${bundleR.version}); check tests will be run but comparisons will be skipped`
841
- );
842
- }
843
- }
844
- const testConfigModule = await import(relativeToSourcePath(testOptions.testConfigPath));
845
- const configInitOptions = {
846
- bundleNameL,
847
- bundleNameR
848
- };
849
- const configOptions = await testConfigModule.getConfigOptions(bundleL, bundleR, configInitOptions);
850
- const checkConfig = await createConfig(configOptions);
851
- const result = await runTestSuite(
852
- context,
853
- checkConfig,
854
- /*verbose=*/
855
- false
856
- );
857
- context.log("info", "Building model check report");
858
- const viteConfig = await this.createViteConfigForReport("bundle", context.config, testOptions, result.suiteSummary);
859
- await build(viteConfig);
860
- return result.allChecksPassed;
861
- }
862
- async resolveTestOptions(mode, config) {
863
- const fetchRemoteBundle = this.options?.fetchRemoteBundle;
864
- async function resolveBundle(bundle) {
865
- if (bundle?.url !== void 0) {
866
- const localBundlePath = await downloadBundle(
867
- bundle.url,
868
- bundle.name,
869
- // TODO: We don't know the last modified time of the remote bundle here, so we use
870
- // undefined (which means the local file will be created with the current timestamp)
871
- void 0,
872
- joinPath5(config.rootDir, "bundles"),
873
- fetchRemoteBundle
874
- );
875
- return {
876
- name: bundle.name,
877
- path: localBundlePath
878
- };
879
- } else if (bundle?.path !== void 0) {
880
- return {
881
- name: bundle.name,
882
- path: bundle.path
883
- };
884
- } else {
885
- return {
886
- name: bundle?.name || "current",
887
- path: joinPath5(config.prepDir, "check-bundle.js")
888
- };
889
- }
890
- }
891
- const currentBundleSpec = await resolveBundle(this.options?.current);
892
- let baselineBundleSpec;
893
- if (mode === "bundle" && this.options?.baseline) {
894
- try {
895
- baselineBundleSpec = await resolveBundle(this.options.baseline);
896
- } catch (e) {
897
- const name = this.options.baseline.name;
898
- const loc = this.options.baseline.url || this.options.baseline.path;
899
- console.warn(
900
- `WARNING: Failed to load '${name}' bundle from '${loc}'; check tests will be run but comparisons will be skipped. Cause:`,
901
- e
902
- );
903
- }
904
- }
905
- let testConfigPath;
906
- if (this.options?.testConfigPath === void 0) {
907
- testConfigPath = joinPath5(config.prepDir, "check-tests.js");
908
- } else {
909
- testConfigPath = this.options.testConfigPath;
910
- }
911
- return {
912
- currentBundleSpec,
913
- baselineBundleSpec,
914
- testConfigPath
915
- };
916
- }
917
- async createViteConfigForReport(mode, config, testOptions, suiteSummary) {
918
- return createViteConfigForReport(
919
- mode,
920
- this.options,
921
- config.rootDir,
922
- config.prepDir,
923
- testOptions.currentBundleSpec,
924
- testOptions.baselineBundleSpec,
925
- testOptions.testConfigPath,
926
- suiteSummary
927
- );
928
- }
569
+ constructor(options) {
570
+ this.options = options;
571
+ this.firstBuild = true;
572
+ }
573
+ async watch(config) {
574
+ if (this.options?.testConfigPath === void 0) await this.genTestConfig("watch", config);
575
+ const testOptions = await this.resolveTestOptions("watch", config);
576
+ const viteConfig = await this.createViteConfigForReport("watch", config, testOptions, void 0);
577
+ await (await createServer(viteConfig)).listen();
578
+ }
579
+ async postBuild(context, modelSpec) {
580
+ const firstBuild = this.firstBuild;
581
+ this.firstBuild = false;
582
+ if (this.options?.current?.path === void 0 && this.options?.current?.url === void 0) {
583
+ if (context.config.mode === "development") await this.copyPreviousBundle(context.config);
584
+ context.log("info", "Generating model check bundle...");
585
+ await this.genCurrentBundle(context, modelSpec);
586
+ }
587
+ if (this.options?.testConfigPath === void 0) {
588
+ if (context.config.mode === "production" || firstBuild) {
589
+ context.log("info", "Generating model check test configuration...");
590
+ await this.genTestConfig("bundle", context.config);
591
+ }
592
+ }
593
+ if (context.config.mode === "production") {
594
+ const testOptions = await this.resolveTestOptions("bundle", context.config);
595
+ return this.runChecks(context, testOptions);
596
+ } else return true;
597
+ }
598
+ async copyPreviousBundle(config) {
599
+ const currentBundleFile = join(config.prepDir, "check-bundle.js");
600
+ if (existsSync(currentBundleFile)) {
601
+ const bundlesDir = join(config.rootDir, "bundles");
602
+ if (!existsSync(bundlesDir)) await mkdir(bundlesDir, { recursive: true });
603
+ const previousBundleFile = join(bundlesDir, "previous.js");
604
+ await copyFile(currentBundleFile, previousBundleFile);
605
+ }
606
+ }
607
+ async genCurrentBundle(context, modelSpec) {
608
+ const viteConfig = await createViteConfigForBundle(context, modelSpec);
609
+ await build(viteConfig);
610
+ }
611
+ async genTestConfig(mode, config) {
612
+ const rootDir = config.rootDir;
613
+ const prepDir = config.prepDir;
614
+ const viteConfig = createViteConfigForTests(mode, rootDir, prepDir);
615
+ await build(viteConfig);
616
+ }
617
+ async runChecks(context, testOptions) {
618
+ context.log("info", "Running model checks...");
619
+ async function importBundleModule(bundleSpec) {
620
+ return import(relativeToSourcePath(bundleSpec.path));
621
+ }
622
+ const bundleR = (await importBundleModule(testOptions.currentBundleSpec)).createBundle();
623
+ const bundleNameR = testOptions.currentBundleSpec.name;
624
+ let bundleL;
625
+ let bundleNameL;
626
+ if (testOptions.baselineBundleSpec !== void 0) {
627
+ const rawBundleL = (await importBundleModule(testOptions.baselineBundleSpec)).createBundle();
628
+ if (rawBundleL.version === bundleR.version) {
629
+ bundleL = rawBundleL;
630
+ bundleNameL = testOptions.baselineBundleSpec.name || "base";
631
+ } else console.warn(`WARNING: Bundle version mismatch (baseline=${rawBundleL.version} current=${bundleR.version}); check tests will be run but comparisons will be skipped`);
632
+ }
633
+ const testConfigModule = await import(relativeToSourcePath(testOptions.testConfigPath));
634
+ const configInitOptions = {
635
+ bundleNameL,
636
+ bundleNameR
637
+ };
638
+ const configOptions = await testConfigModule.getConfigOptions(bundleL, bundleR, configInitOptions);
639
+ const result = await runTestSuite(context, await createConfig(configOptions), false);
640
+ context.log("info", "Building model check report");
641
+ const viteConfig = await this.createViteConfigForReport("bundle", context.config, testOptions, result.suiteSummary);
642
+ await build(viteConfig);
643
+ return result.allChecksPassed;
644
+ }
645
+ async resolveTestOptions(mode, config) {
646
+ const fetchRemoteBundle = this.options?.fetchRemoteBundle;
647
+ async function resolveBundle(bundle) {
648
+ if (bundle?.url !== void 0) {
649
+ const localBundlePath = await downloadBundle(bundle.url, bundle.name, void 0, join(config.rootDir, "bundles"), fetchRemoteBundle);
650
+ return {
651
+ name: bundle.name,
652
+ path: localBundlePath
653
+ };
654
+ } else if (bundle?.path !== void 0) return {
655
+ name: bundle.name,
656
+ path: bundle.path
657
+ };
658
+ else return {
659
+ name: bundle?.name || "current",
660
+ path: join(config.prepDir, "check-bundle.js")
661
+ };
662
+ }
663
+ const currentBundleSpec = await resolveBundle(this.options?.current);
664
+ let baselineBundleSpec;
665
+ if (mode === "bundle" && this.options?.baseline) try {
666
+ baselineBundleSpec = await resolveBundle(this.options.baseline);
667
+ } catch (e) {
668
+ const name = this.options.baseline.name;
669
+ const loc = this.options.baseline.url || this.options.baseline.path;
670
+ console.warn(`WARNING: Failed to load '${name}' bundle from '${loc}'; check tests will be run but comparisons will be skipped. Cause:`, e);
671
+ }
672
+ let testConfigPath;
673
+ if (this.options?.testConfigPath === void 0) testConfigPath = join(config.prepDir, "check-tests.js");
674
+ else testConfigPath = this.options.testConfigPath;
675
+ return {
676
+ currentBundleSpec,
677
+ baselineBundleSpec,
678
+ testConfigPath
679
+ };
680
+ }
681
+ async createViteConfigForReport(mode, config, testOptions, suiteSummary) {
682
+ return createViteConfigForReport(mode, this.options, config.rootDir, config.prepDir, testOptions.currentBundleSpec, testOptions.baselineBundleSpec, testOptions.testConfigPath, suiteSummary);
683
+ }
929
684
  };
685
+ /**
686
+ * Return a Unix-style path (e.g. '../../foo.js') that is relative to the directory of
687
+ * the current source file. This can be used to construct a path that is safe for
688
+ * dynamic import on either Unix or Windows.
689
+ *
690
+ * @param filePath The path to make relative.
691
+ */
930
692
  function relativeToSourcePath(filePath) {
931
- const srcDir = dirname5(fileURLToPath5(import.meta.url));
932
- const relPath = relative5(srcDir, filePath);
933
- return relPath.replaceAll("\\", "/");
693
+ const srcDir = dirname(fileURLToPath(import.meta.url));
694
+ return relative(srcDir, filePath).replaceAll("\\", "/");
934
695
  }
935
- export {
936
- checkPlugin
937
- };
696
+ //#endregion
697
+ export { checkPlugin };
698
+
938
699
  //# sourceMappingURL=index.js.map