@sdeverywhere/build 0.3.17 → 0.3.18

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,1015 +1,940 @@
1
- // src/build/build.ts
2
- import { join as joinPath6 } from "path";
3
- import { err as err3, ok as ok3 } from "neverthrow";
4
-
5
- // src/config/config-loader.ts
6
- import { existsSync, lstatSync, mkdirSync } from "fs";
7
- import { dirname, isAbsolute, join as joinPath, relative, resolve as resolvePath } from "path";
8
- import { fileURLToPath } from "url";
1
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
9
2
  import { err, ok } from "neverthrow";
3
+ import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
4
+ import { fileURLToPath } from "url";
5
+ import pico from "picocolors";
6
+ import { copyFile, readFile, readdir, writeFile } from "fs/promises";
7
+ import { canonicalVarId } from "@sdeverywhere/parse";
8
+ import { spawn } from "cross-spawn";
9
+ import { createHash } from "node:crypto";
10
+ import { createReadStream } from "node:fs";
11
+ import { basename as basename$1, join as join$1 } from "node:path";
12
+ import { pipeline } from "node:stream/promises";
13
+ import { glob, globSync, isDynamicPattern } from "tinyglobby";
14
+ import chokidar from "chokidar";
15
+ //#region src/config/config-loader.ts
16
+ /**
17
+ * Load a user-defined config file or a given `UserConfig` object. This validates
18
+ * all paths and if successful, this returns a `ResolvedConfig`, otherwise returns
19
+ * an error result.
20
+ *
21
+ * @param mode The build mode.
22
+ * @param config The path to a config file, or a `UserConfig` object; if undefined,
23
+ * this will look for a `sde.config.js` file in the current directory.
24
+ * @param sdeDir Temporary (the path to the `@sdeverywhere/cli` package).
25
+ * @param sdeCmdPath Temporary (the path to the `sde` command).
26
+ * @return An `ok` result with the `ResolvedConfig`, otherwise an `err` result.
27
+ */
10
28
  async function loadConfig(mode, config, sdeDir, sdeCmdPath) {
11
- let userConfig;
12
- if (typeof config === "object") {
13
- userConfig = config;
14
- } else {
15
- let configPath;
16
- if (typeof config === "string") {
17
- configPath = config;
18
- } else {
19
- configPath = joinPath(process.cwd(), "sde.config.js");
20
- }
21
- try {
22
- if (!existsSync(configPath)) {
23
- return err(new Error(`Cannot find config file '${configPath}'`));
24
- }
25
- const configRelPath = relativeToSourcePath(configPath);
26
- const configModule = await import(configRelPath);
27
- userConfig = await configModule.config();
28
- } catch (e) {
29
- return err(new Error(`Failed to load config file '${configPath}': ${e.message}`));
30
- }
31
- }
32
- try {
33
- const resolvedConfig = resolveUserConfig(userConfig, mode, sdeDir, sdeCmdPath);
34
- return ok({
35
- userConfig,
36
- resolvedConfig
37
- });
38
- } catch (e) {
39
- return err(e);
40
- }
29
+ let userConfig;
30
+ if (typeof config === "object") userConfig = config;
31
+ else {
32
+ let configPath;
33
+ if (typeof config === "string") configPath = config;
34
+ else configPath = join(process.cwd(), "sde.config.js");
35
+ try {
36
+ if (!existsSync(configPath)) return err(/* @__PURE__ */ new Error(`Cannot find config file '${configPath}'`));
37
+ userConfig = await (await import(relativeToSourcePath(configPath))).config();
38
+ } catch (e) {
39
+ return err(/* @__PURE__ */ new Error(`Failed to load config file '${configPath}': ${e.message}`));
40
+ }
41
+ }
42
+ try {
43
+ const resolvedConfig = resolveUserConfig(userConfig, mode, sdeDir, sdeCmdPath);
44
+ return ok({
45
+ userConfig,
46
+ resolvedConfig
47
+ });
48
+ } catch (e) {
49
+ return err(e);
50
+ }
41
51
  }
52
+ /**
53
+ * Resolve the given user configuration by resolving all paths. This will create
54
+ * the prep directory if it does not already exist. This will throw an error if
55
+ * any other paths are invalid or do not exist.
56
+ *
57
+ * @param userConfig The user-defined configuration.
58
+ * @param mode The active build mode.
59
+ * @param sdeDir Temporary (the path to the `@sdeverywhere/cli` package).
60
+ * @param sdeCmdPath Temporary (the path to the `sde` command).
61
+ * @return The resolved configuration.
62
+ */
42
63
  function resolveUserConfig(userConfig, mode, sdeDir, sdeCmdPath) {
43
- function expectDirectory(propName, path) {
44
- if (!existsSync(path)) {
45
- throw new Error(`The configured ${propName} (${path}) does not exist`);
46
- } else if (!lstatSync(path).isDirectory()) {
47
- throw new Error(`The configured ${propName} (${path}) is not a directory`);
48
- }
49
- }
50
- let rootDir;
51
- if (userConfig.rootDir) {
52
- rootDir = resolvePath(userConfig.rootDir);
53
- expectDirectory("rootDir", rootDir);
54
- } else {
55
- rootDir = process.cwd();
56
- }
57
- let prepDir;
58
- if (userConfig.prepDir) {
59
- prepDir = resolvePath(userConfig.prepDir);
60
- } else {
61
- prepDir = resolvePath(rootDir, "sde-prep");
62
- }
63
- mkdirSync(prepDir, { recursive: true });
64
- const userModelFiles = userConfig.modelFiles;
65
- const modelFiles = [];
66
- for (const userModelFile of userModelFiles) {
67
- const modelFile = resolvePath(userModelFile);
68
- if (!existsSync(modelFile)) {
69
- throw new Error(`The configured model file (${modelFile}) does not exist`);
70
- }
71
- modelFiles.push(modelFile);
72
- }
73
- let modelInputPaths;
74
- if (userConfig.modelInputPaths && userConfig.modelInputPaths.length > 0) {
75
- modelInputPaths = userConfig.modelInputPaths;
76
- } else {
77
- modelInputPaths = modelFiles;
78
- }
79
- let watchPaths2;
80
- if (userConfig.watchPaths && userConfig.watchPaths.length > 0) {
81
- watchPaths2 = userConfig.watchPaths;
82
- } else {
83
- watchPaths2 = modelFiles;
84
- }
85
- const rawGenFormat = userConfig.genFormat || "js";
86
- let genFormat;
87
- switch (rawGenFormat) {
88
- case "js":
89
- genFormat = "js";
90
- break;
91
- case "c":
92
- genFormat = "c";
93
- break;
94
- default:
95
- throw new Error(`The configured genFormat value is invalid; must be either 'js' or 'c'`);
96
- }
97
- let outListingFile;
98
- if (userConfig.outListingFile) {
99
- if (isAbsolute(userConfig.outListingFile)) {
100
- outListingFile = userConfig.outListingFile;
101
- } else {
102
- outListingFile = resolvePath(rootDir, userConfig.outListingFile);
103
- }
104
- }
105
- return {
106
- mode,
107
- rootDir,
108
- prepDir,
109
- modelFiles,
110
- modelInputPaths,
111
- watchPaths: watchPaths2,
112
- genFormat,
113
- outListingFile,
114
- sdeDir,
115
- sdeCmdPath
116
- };
64
+ function expectDirectory(propName, path) {
65
+ if (!existsSync(path)) throw new Error(`The configured ${propName} (${path}) does not exist`);
66
+ else if (!lstatSync(path).isDirectory()) throw new Error(`The configured ${propName} (${path}) is not a directory`);
67
+ }
68
+ let rootDir;
69
+ if (userConfig.rootDir) {
70
+ rootDir = resolve(userConfig.rootDir);
71
+ expectDirectory("rootDir", rootDir);
72
+ } else rootDir = process.cwd();
73
+ let prepDir;
74
+ if (userConfig.prepDir) prepDir = resolve(userConfig.prepDir);
75
+ else prepDir = resolve(rootDir, "sde-prep");
76
+ mkdirSync(prepDir, { recursive: true });
77
+ const userModelFiles = userConfig.modelFiles;
78
+ const modelFiles = [];
79
+ for (const userModelFile of userModelFiles) {
80
+ const modelFile = resolve(userModelFile);
81
+ if (!existsSync(modelFile)) throw new Error(`The configured model file (${modelFile}) does not exist`);
82
+ modelFiles.push(modelFile);
83
+ }
84
+ let modelInputPaths;
85
+ if (userConfig.modelInputPaths && userConfig.modelInputPaths.length > 0) modelInputPaths = userConfig.modelInputPaths;
86
+ else modelInputPaths = modelFiles;
87
+ let watchPaths;
88
+ if (userConfig.watchPaths && userConfig.watchPaths.length > 0) watchPaths = userConfig.watchPaths;
89
+ else watchPaths = modelFiles;
90
+ const rawGenFormat = userConfig.genFormat || "js";
91
+ let genFormat;
92
+ switch (rawGenFormat) {
93
+ case "js":
94
+ genFormat = "js";
95
+ break;
96
+ case "c":
97
+ genFormat = "c";
98
+ break;
99
+ default: throw new Error(`The configured genFormat value is invalid; must be either 'js' or 'c'`);
100
+ }
101
+ let outListingFile;
102
+ if (userConfig.outListingFile) {
103
+ if (isAbsolute(userConfig.outListingFile)) outListingFile = userConfig.outListingFile;
104
+ else outListingFile = resolve(rootDir, userConfig.outListingFile);
105
+ }
106
+ return {
107
+ mode,
108
+ rootDir,
109
+ prepDir,
110
+ modelFiles,
111
+ modelInputPaths,
112
+ watchPaths,
113
+ genFormat,
114
+ outListingFile,
115
+ sdeDir,
116
+ sdeCmdPath
117
+ };
117
118
  }
119
+ /**
120
+ * Return a Unix-style path (e.g. '../../foo.js') that is relative to the directory of
121
+ * the current source file. This can be used to construct a path that is safe for
122
+ * dynamic import on either Unix or Windows.
123
+ *
124
+ * @param filePath The path to make relative.
125
+ */
118
126
  function relativeToSourcePath(filePath) {
119
- const srcDir = dirname(fileURLToPath(import.meta.url));
120
- const relPath = relative(srcDir, filePath);
121
- return relPath.replaceAll("\\", "/");
127
+ const srcDir = dirname(fileURLToPath(import.meta.url));
128
+ return relative(srcDir, filePath).replaceAll("\\", "/");
122
129
  }
123
-
124
- // src/_shared/log.ts
125
- import { writeFileSync } from "fs";
126
- import pico from "picocolors";
127
- var activeLevels = /* @__PURE__ */ new Set(["error", "info"]);
128
- var overlayFile;
129
- var overlayEnabled = false;
130
- var overlayHtml = "";
130
+ //#endregion
131
+ //#region src/_shared/log.ts
132
+ const activeLevels = /* @__PURE__ */ new Set(["error", "info"]);
133
+ let overlayFile;
134
+ let overlayEnabled = false;
135
+ let overlayHtml = "";
136
+ /**
137
+ * Set the active logging levels. By default, only 'error' and 'info'
138
+ * messages are emitted.
139
+ *
140
+ * @param logLevels The logging levels to include.
141
+ */
131
142
  function setActiveLevels(logLevels) {
132
- activeLevels.clear();
133
- for (const level of logLevels) {
134
- activeLevels.add(level);
135
- }
143
+ activeLevels.clear();
144
+ for (const level of logLevels) activeLevels.add(level);
136
145
  }
146
+ /**
147
+ * Set the path to the `messages.html` file where overlay messages will be written.
148
+ *
149
+ * @param file The absolute path to the HTML file where messages will be written.
150
+ * @param enabled Whether to write messages to the file; if false, the file will be
151
+ * emptied and no further messages will be written.
152
+ */
137
153
  function setOverlayFile(file, enabled) {
138
- overlayFile = file;
139
- overlayEnabled = enabled;
140
- writeFileSync(overlayFile, "");
154
+ overlayFile = file;
155
+ overlayEnabled = enabled;
156
+ writeFileSync(overlayFile, "");
141
157
  }
158
+ /**
159
+ * Log a message to the console and/or overlay.
160
+ *
161
+ * @param level The logging level.
162
+ * @param msg The message to emit.
163
+ */
142
164
  function log(level, msg) {
143
- if (activeLevels.has(level)) {
144
- if (level === "error") {
145
- console.error(pico.red(msg));
146
- logToOverlay(msg);
147
- } else {
148
- console.log(msg);
149
- logToOverlay(msg);
150
- }
151
- }
165
+ if (activeLevels.has(level)) {
166
+ if (level === "error") {
167
+ console.error(pico.red(msg));
168
+ logToOverlay(msg);
169
+ } else {
170
+ console.log(msg);
171
+ logToOverlay(msg);
172
+ }
173
+ }
152
174
  }
175
+ /**
176
+ * Log an error to the console and/or overlay.
177
+ *
178
+ * @param e The error to log.
179
+ */
153
180
  function logError(e) {
154
- const stack = e.stack || "";
155
- const stackLines = stack.split("\n").filter((s) => s.match(/^\s+at/));
156
- const trace = stackLines.slice(0, 3).join("\n");
157
- console.error(pico.red(`
158
- ERROR: ${e.message}`));
159
- console.error(pico.dim(pico.red(`${trace}
160
- `)));
161
- logToOverlay(`
162
- ERROR: ${e.message}`, true);
163
- logToOverlay(`${trace}
164
- `, true);
181
+ const trace = (e.stack || "").split("\n").filter((s) => s.match(/^\s+at/)).slice(0, 3).join("\n");
182
+ console.error(pico.red(`\nERROR: ${e.message}`));
183
+ console.error(pico.dim(pico.red(`${trace}\n`)));
184
+ logToOverlay(`\nERROR: ${e.message}`, true);
185
+ logToOverlay(`${trace}\n`, true);
165
186
  }
166
187
  function writeOverlayFiles() {
167
- writeFileSync(overlayFile, overlayHtml);
188
+ writeFileSync(overlayFile, overlayHtml);
168
189
  }
169
190
  function clearOverlay() {
170
- if (!overlayEnabled) {
171
- return;
172
- }
173
- overlayHtml = "";
174
- writeOverlayFiles();
191
+ if (!overlayEnabled) return;
192
+ overlayHtml = "";
193
+ writeOverlayFiles();
175
194
  }
176
- var indent = " ".repeat(4);
195
+ const indent = " ".repeat(4);
177
196
  function logToOverlay(msg, error = false) {
178
- if (!overlayEnabled) {
179
- return;
180
- }
181
- if (error) {
182
- msg = `<span class="overlay-error">${msg}</span>`;
183
- }
184
- const msgHtml = msg.replace(/\n/g, "\n<br/>").replace(/\s{2}/g, indent);
185
- if (overlayHtml) {
186
- overlayHtml += `<br/>${msgHtml}`;
187
- } else {
188
- overlayHtml = `${msgHtml}`;
189
- }
190
- writeOverlayFiles();
197
+ if (!overlayEnabled) return;
198
+ if (error) msg = `<span class="overlay-error">${msg}</span>`;
199
+ const msgHtml = msg.replace(/\n/g, "\n<br/>").replace(/\s{2}/g, indent);
200
+ if (overlayHtml) overlayHtml += `<br/>${msgHtml}`;
201
+ else overlayHtml = `${msgHtml}`;
202
+ writeOverlayFiles();
191
203
  }
192
-
193
- // src/build/impl/build-once.ts
194
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
195
- import { writeFile as writeFile2 } from "fs/promises";
196
- import { join as joinPath5 } from "path";
197
- import { err as err2, ok as ok2 } from "neverthrow";
198
-
199
- // src/context/context.ts
200
- import { canonicalVarId } from "@sdeverywhere/parse";
201
-
202
- // src/context/spawn-child.ts
203
- import { spawn } from "cross-spawn";
204
+ //#endregion
205
+ //#region src/context/spawn-child.ts
206
+ /**
207
+ * Spawn a child process that runs the given command.
208
+ *
209
+ * @param cwd The directory in which the command will be executed.
210
+ * @param command The command to execute.
211
+ * @param args The arguments to pass to the command.
212
+ * @param abortSignal The signal used to abort the process.
213
+ * @param opts Additional options to configure the process.
214
+ * @returns The output of the process.
215
+ */
204
216
  function spawnChild(cwd, command, args, abortSignal, opts) {
205
- return new Promise((resolve, reject) => {
206
- if (abortSignal?.aborted) {
207
- reject(new Error("ABORT"));
208
- return;
209
- }
210
- let childProc;
211
- const localLog = (s, err4 = false) => {
212
- if (childProc === void 0) {
213
- return;
214
- }
215
- log(err4 ? "error" : "info", s);
216
- };
217
- const abortHandler = () => {
218
- if (childProc) {
219
- log("info", "Killing existing build process...");
220
- childProc.kill("SIGKILL");
221
- childProc = void 0;
222
- }
223
- reject(new Error("ABORT"));
224
- };
225
- abortSignal?.addEventListener("abort", abortHandler, { once: true });
226
- const stdoutMessages = [];
227
- const stderrMessages = [];
228
- const logMessage = (msg, err4) => {
229
- let includeMessage = true;
230
- if (opts?.ignoredMessageFilter && msg.trim().startsWith(opts.ignoredMessageFilter)) {
231
- includeMessage = false;
232
- }
233
- if (includeMessage) {
234
- const lines = msg.trim().split("\n");
235
- for (const line of lines) {
236
- localLog(` ${line}`, err4);
237
- }
238
- }
239
- };
240
- childProc = spawn(command, args, {
241
- cwd
242
- });
243
- childProc.stdout.on("data", (data) => {
244
- const msg = data.toString();
245
- if (opts?.captureOutput === true) {
246
- stdoutMessages.push(msg);
247
- }
248
- if (opts?.logOutput !== false) {
249
- logMessage(msg, false);
250
- }
251
- });
252
- childProc.stderr.on("data", (data) => {
253
- const msg = data.toString();
254
- if (opts?.captureOutput === true) {
255
- stderrMessages.push(msg);
256
- }
257
- if (opts?.logOutput !== false) {
258
- logMessage(msg, true);
259
- }
260
- });
261
- childProc.on("error", (err4) => {
262
- localLog(`Process error: ${err4}`, true);
263
- });
264
- childProc.on("close", (code, signal) => {
265
- abortSignal?.removeEventListener("abort", abortHandler);
266
- childProc = void 0;
267
- if (signal) {
268
- return;
269
- }
270
- const processOutput = {
271
- exitCode: code,
272
- stdoutMessages,
273
- stderrMessages
274
- };
275
- if (code === 0) {
276
- resolve(processOutput);
277
- } else if (!signal) {
278
- if (opts?.ignoreError === true) {
279
- resolve(processOutput);
280
- } else {
281
- reject(new Error(`Child process failed (code=${code})`));
282
- }
283
- }
284
- });
285
- });
217
+ return new Promise((resolve, reject) => {
218
+ if (abortSignal?.aborted) {
219
+ reject(/* @__PURE__ */ new Error("ABORT"));
220
+ return;
221
+ }
222
+ let childProc;
223
+ const localLog = (s, err = false) => {
224
+ if (childProc === void 0) return;
225
+ log(err ? "error" : "info", s);
226
+ };
227
+ const abortHandler = () => {
228
+ if (childProc) {
229
+ log("info", "Killing existing build process...");
230
+ childProc.kill("SIGKILL");
231
+ childProc = void 0;
232
+ }
233
+ reject(/* @__PURE__ */ new Error("ABORT"));
234
+ };
235
+ abortSignal?.addEventListener("abort", abortHandler, { once: true });
236
+ const stdoutMessages = [];
237
+ const stderrMessages = [];
238
+ const logMessage = (msg, err) => {
239
+ let includeMessage = true;
240
+ if (opts?.ignoredMessageFilter && msg.trim().startsWith(opts.ignoredMessageFilter)) includeMessage = false;
241
+ if (includeMessage) {
242
+ const lines = msg.trim().split("\n");
243
+ for (const line of lines) localLog(` ${line}`, err);
244
+ }
245
+ };
246
+ childProc = spawn(command, args, { cwd });
247
+ childProc.stdout.on("data", (data) => {
248
+ const msg = data.toString();
249
+ if (opts?.captureOutput === true) stdoutMessages.push(msg);
250
+ if (opts?.logOutput !== false) logMessage(msg, false);
251
+ });
252
+ childProc.stderr.on("data", (data) => {
253
+ const msg = data.toString();
254
+ if (opts?.captureOutput === true) stderrMessages.push(msg);
255
+ if (opts?.logOutput !== false) logMessage(msg, true);
256
+ });
257
+ childProc.on("error", (err) => {
258
+ localLog(`Process error: ${err}`, true);
259
+ });
260
+ childProc.on("close", (code, signal) => {
261
+ abortSignal?.removeEventListener("abort", abortHandler);
262
+ childProc = void 0;
263
+ if (signal) return;
264
+ const processOutput = {
265
+ exitCode: code,
266
+ stdoutMessages,
267
+ stderrMessages
268
+ };
269
+ if (code === 0) resolve(processOutput);
270
+ else if (!signal) {
271
+ if (opts?.ignoreError === true) resolve(processOutput);
272
+ else reject(/* @__PURE__ */ new Error(`Child process failed (code=${code})`));
273
+ }
274
+ });
275
+ });
286
276
  }
287
-
288
- // src/context/context.ts
277
+ //#endregion
278
+ //#region src/context/context.ts
279
+ /**
280
+ * Provides access to common functionality that is needed during the build process.
281
+ * This is passed to most plugin functions.
282
+ */
289
283
  var BuildContext = class {
290
- /**
291
- * @param config The resolved configuration.
292
- * @hidden
293
- */
294
- constructor(config, stagedFiles, abortSignal) {
295
- this.config = config;
296
- this.stagedFiles = stagedFiles;
297
- this.abortSignal = abortSignal;
298
- }
299
- /**
300
- * Log a message to the console and/or the in-browser overlay panel.
301
- *
302
- * @param level The log level (verbose, info, error).
303
- * @param msg The message.
304
- */
305
- log(level, msg) {
306
- log(level, msg);
307
- }
308
- /**
309
- * Prepare for writing a file to the staged directory.
310
- *
311
- * This will add the path to the array of tracked files and will create the
312
- * staged directory if needed.
313
- *
314
- * @param srcDir The directory underneath the configured `staged` directory where
315
- * the file will be written (this must be a relative path).
316
- * @param srcFile The name of the file as written to the `staged` directory.
317
- * @param dstDir The absolute path to the destination directory where the staged
318
- * file will be copied when the build has completed.
319
- * @param dstFile The name of the file as written to the destination directory.
320
- * @return The absolute path to the staged file.
321
- */
322
- prepareStagedFile(srcDir, srcFile, dstDir, dstFile) {
323
- return this.stagedFiles.prepareStagedFile(srcDir, srcFile, dstDir, dstFile);
324
- }
325
- /**
326
- * Write a file to the staged directory.
327
- *
328
- * This file will be copied (along with other staged files) into the destination
329
- * directory only after the build process has completed. Copying all staged files
330
- * at once helps improve the local development experience by making it so that
331
- * live reloading tools only need to refresh once instead of every time a build
332
- * file is written.
333
- *
334
- * @param srcDir The directory underneath the configured `staged` directory where
335
- * the file will be written (this must be a relative path).
336
- * @param dstDir The absolute path to the destination directory where the staged
337
- * file will be copied when the build has completed.
338
- * @param filename The name of the file.
339
- * @param content The file content.
340
- */
341
- writeStagedFile(srcDir, dstDir, filename, content) {
342
- this.stagedFiles.writeStagedFile(srcDir, dstDir, filename, content);
343
- }
344
- /**
345
- * Spawn a child process that runs the given command.
346
- *
347
- * @param cwd The directory in which the command will be executed.
348
- * @param command The command to execute.
349
- * @param args The arguments to pass to the command.
350
- * @param opts Additional options to configure the process.
351
- * @returns The output of the process.
352
- */
353
- spawnChild(cwd, command, args, opts) {
354
- return spawnChild(cwd, command, args, this.abortSignal, opts);
355
- }
356
- /**
357
- * Format a (subscripted or non-subscripted) model variable name into a canonical
358
- * identifier (with special characters converted to underscore, and subscript/dimension
359
- * parts separated by commas).
360
- *
361
- * @param name The name of the variable in the source model, e.g., `Variable name[DimA, B2]`.
362
- * @returns The canonical identifier for the given name, e.g., `_variable_name[_dima,_b2]`.
363
- */
364
- canonicalVarId(name) {
365
- return canonicalVarId(name);
366
- }
284
+ /**
285
+ * @param config The resolved configuration.
286
+ * @hidden
287
+ */
288
+ constructor(config, stagedFiles, abortSignal) {
289
+ this.config = config;
290
+ this.stagedFiles = stagedFiles;
291
+ this.abortSignal = abortSignal;
292
+ }
293
+ /**
294
+ * Log a message to the console and/or the in-browser overlay panel.
295
+ *
296
+ * @param level The log level (verbose, info, error).
297
+ * @param msg The message.
298
+ */
299
+ log(level, msg) {
300
+ log(level, msg);
301
+ }
302
+ /**
303
+ * Prepare for writing a file to the staged directory.
304
+ *
305
+ * This will add the path to the array of tracked files and will create the
306
+ * staged directory if needed.
307
+ *
308
+ * @param srcDir The directory underneath the configured `staged` directory where
309
+ * the file will be written (this must be a relative path).
310
+ * @param srcFile The name of the file as written to the `staged` directory.
311
+ * @param dstDir The absolute path to the destination directory where the staged
312
+ * file will be copied when the build has completed.
313
+ * @param dstFile The name of the file as written to the destination directory.
314
+ * @return The absolute path to the staged file.
315
+ */
316
+ prepareStagedFile(srcDir, srcFile, dstDir, dstFile) {
317
+ return this.stagedFiles.prepareStagedFile(srcDir, srcFile, dstDir, dstFile);
318
+ }
319
+ /**
320
+ * Write a file to the staged directory.
321
+ *
322
+ * This file will be copied (along with other staged files) into the destination
323
+ * directory only after the build process has completed. Copying all staged files
324
+ * at once helps improve the local development experience by making it so that
325
+ * live reloading tools only need to refresh once instead of every time a build
326
+ * file is written.
327
+ *
328
+ * @param srcDir The directory underneath the configured `staged` directory where
329
+ * the file will be written (this must be a relative path).
330
+ * @param dstDir The absolute path to the destination directory where the staged
331
+ * file will be copied when the build has completed.
332
+ * @param filename The name of the file.
333
+ * @param content The file content.
334
+ */
335
+ writeStagedFile(srcDir, dstDir, filename, content) {
336
+ this.stagedFiles.writeStagedFile(srcDir, dstDir, filename, content);
337
+ }
338
+ /**
339
+ * Spawn a child process that runs the given command.
340
+ *
341
+ * @param cwd The directory in which the command will be executed.
342
+ * @param command The command to execute.
343
+ * @param args The arguments to pass to the command.
344
+ * @param opts Additional options to configure the process.
345
+ * @returns The output of the process.
346
+ */
347
+ spawnChild(cwd, command, args, opts) {
348
+ return spawnChild(cwd, command, args, this.abortSignal, opts);
349
+ }
350
+ /**
351
+ * Format a (subscripted or non-subscripted) model variable name into a canonical
352
+ * identifier (with special characters converted to underscore, and subscript/dimension
353
+ * parts separated by commas).
354
+ *
355
+ * @param name The name of the variable in the source model, e.g., `Variable name[DimA, B2]`.
356
+ * @returns The canonical identifier for the given name, e.g., `_variable_name[_dima,_b2]`.
357
+ */
358
+ canonicalVarId(name) {
359
+ return canonicalVarId(name);
360
+ }
367
361
  };
368
-
369
- // src/context/staged-files.ts
370
- import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, statSync, writeFileSync as writeFileSync2 } from "fs";
371
- import { join as joinPath2 } from "path";
362
+ //#endregion
363
+ //#region src/context/staged-files.ts
372
364
  var StagedFiles = class {
373
- constructor(prepDir) {
374
- this.stagedFiles = [];
375
- this.baseStagedDir = joinPath2(prepDir, "staged");
376
- }
377
- /**
378
- * Prepare for writing a file to the staged directory.
379
- *
380
- * This will add the path to the array of tracked files and will create the
381
- * staged directory if needed.
382
- *
383
- * @param srcDir The directory underneath the configured `staged` directory where
384
- * the file will be written (this must be a relative path).
385
- * @param srcFile The name of the file as written to the `staged` directory.
386
- * @param dstDir The absolute path to the destination directory where the staged
387
- * file will be copied when the build has completed.
388
- * @param dstFile The name of the file as written to the destination directory.
389
- * @return The absolute path to the staged file.
390
- */
391
- prepareStagedFile(srcDir, srcFile, dstDir, dstFile) {
392
- const stagedFile = {
393
- srcDir,
394
- srcFile,
395
- dstDir,
396
- dstFile
397
- };
398
- if (this.stagedFiles.indexOf(stagedFile) < 0) {
399
- this.stagedFiles.push(stagedFile);
400
- }
401
- const stagedDir = joinPath2(this.baseStagedDir, srcDir);
402
- if (!existsSync2(stagedDir)) {
403
- mkdirSync2(stagedDir, { recursive: true });
404
- }
405
- return joinPath2(stagedDir, srcFile);
406
- }
407
- /**
408
- * Write a file to the staged directory.
409
- *
410
- * This file will be copied (along with other staged files) into the destination
411
- * directory only after the build process has completed. Copying all staged files
412
- * at once helps improve the local development experience by making it so that
413
- * live reloading tools only need to refresh once instead of every time a build
414
- * file is written.
415
- *
416
- * @param srcDir The directory underneath the configured `staged` directory where
417
- * the file will be written (this must be a relative path).
418
- * @param dstDir The absolute path to the destination directory where the staged
419
- * file will be copied when the build has completed.
420
- * @param filename The name of the file.
421
- * @param content The file content.
422
- */
423
- writeStagedFile(srcDir, dstDir, filename, content) {
424
- const stagedFilePath = this.prepareStagedFile(srcDir, filename, dstDir, filename);
425
- writeFileSync2(stagedFilePath, content);
426
- }
427
- /**
428
- * Return the absolute path to the staged file for the given source directory and file name.
429
- *
430
- * @param srcDir The directory underneath the configured `staged` directory where
431
- * the file would be written initially (this must be a relative path).
432
- * @param srcFile The name of the file.
433
- */
434
- getStagedFilePath(srcDir, srcFile) {
435
- return joinPath2(this.baseStagedDir, srcDir, srcFile);
436
- }
437
- /**
438
- * Return true if the staged file exists for the given source directory and file name.
439
- *
440
- * @param srcDir The directory underneath the configured `staged` directory where
441
- * the file would be written initially (this must be a relative path).
442
- * @param srcFile The name of the file.
443
- */
444
- stagedFileExists(srcDir, srcFile) {
445
- const fullSrcPath = this.getStagedFilePath(srcDir, srcFile);
446
- return existsSync2(fullSrcPath);
447
- }
448
- /**
449
- * Return true if the destination file exists for the given source directory and file name.
450
- *
451
- * @param srcDir The directory underneath the configured `staged` directory where
452
- * the file would be written initially (this must be a relative path).
453
- * @param srcFile The name of the file.
454
- */
455
- destinationFileExists(srcDir, srcFile) {
456
- const f = this.stagedFiles.find((f2) => f2.srcDir === srcDir && f2.srcFile === srcFile);
457
- if (f === void 0) {
458
- return false;
459
- }
460
- const fullDstPath = joinPath2(f.dstDir, f.dstFile);
461
- return existsSync2(fullDstPath);
462
- }
463
- /**
464
- * Copy staged files to their destination; this will only copy the staged
465
- * files if they are different than the existing destination files. We
466
- * copy the files in a batch like this so that hot module reload is only
467
- * triggered once at the end of the whole build process.
468
- */
469
- copyChangedFiles() {
470
- log("info", "Copying changed files into place...");
471
- for (const f of this.stagedFiles) {
472
- this.copyStagedFile(f);
473
- }
474
- log("info", "Done copying files");
475
- }
476
- /**
477
- * Copy a file from the `staged` directory to its destination. If the file already
478
- * exists in the destination directory and has the same contents as the source file,
479
- * the file will not be copied and this function will return false.
480
- *
481
- * @param f The staged file entry.
482
- */
483
- copyStagedFile(f) {
484
- if (!existsSync2(f.dstDir)) {
485
- mkdirSync2(f.dstDir, { recursive: true });
486
- }
487
- const fullSrcPath = this.getStagedFilePath(f.srcDir, f.srcFile);
488
- const fullDstPath = joinPath2(f.dstDir, f.dstFile);
489
- const needsCopy = filesDiffer(fullSrcPath, fullDstPath);
490
- if (needsCopy) {
491
- log("verbose", ` Copying ${f.srcFile} to ${fullDstPath}`);
492
- copyFileSync(fullSrcPath, fullDstPath);
493
- }
494
- return needsCopy;
495
- }
365
+ constructor(prepDir) {
366
+ this.stagedFiles = [];
367
+ this.baseStagedDir = join(prepDir, "staged");
368
+ }
369
+ /**
370
+ * Prepare for writing a file to the staged directory.
371
+ *
372
+ * This will add the path to the array of tracked files and will create the
373
+ * staged directory if needed.
374
+ *
375
+ * @param srcDir The directory underneath the configured `staged` directory where
376
+ * the file will be written (this must be a relative path).
377
+ * @param srcFile The name of the file as written to the `staged` directory.
378
+ * @param dstDir The absolute path to the destination directory where the staged
379
+ * file will be copied when the build has completed.
380
+ * @param dstFile The name of the file as written to the destination directory.
381
+ * @return The absolute path to the staged file.
382
+ */
383
+ prepareStagedFile(srcDir, srcFile, dstDir, dstFile) {
384
+ const stagedFile = {
385
+ srcDir,
386
+ srcFile,
387
+ dstDir,
388
+ dstFile
389
+ };
390
+ if (this.stagedFiles.indexOf(stagedFile) < 0) this.stagedFiles.push(stagedFile);
391
+ const stagedDir = join(this.baseStagedDir, srcDir);
392
+ if (!existsSync(stagedDir)) mkdirSync(stagedDir, { recursive: true });
393
+ return join(stagedDir, srcFile);
394
+ }
395
+ /**
396
+ * Write a file to the staged directory.
397
+ *
398
+ * This file will be copied (along with other staged files) into the destination
399
+ * directory only after the build process has completed. Copying all staged files
400
+ * at once helps improve the local development experience by making it so that
401
+ * live reloading tools only need to refresh once instead of every time a build
402
+ * file is written.
403
+ *
404
+ * @param srcDir The directory underneath the configured `staged` directory where
405
+ * the file will be written (this must be a relative path).
406
+ * @param dstDir The absolute path to the destination directory where the staged
407
+ * file will be copied when the build has completed.
408
+ * @param filename The name of the file.
409
+ * @param content The file content.
410
+ */
411
+ writeStagedFile(srcDir, dstDir, filename, content) {
412
+ const stagedFilePath = this.prepareStagedFile(srcDir, filename, dstDir, filename);
413
+ writeFileSync(stagedFilePath, content);
414
+ }
415
+ /**
416
+ * Return the absolute path to the staged file for the given source directory and file name.
417
+ *
418
+ * @param srcDir The directory underneath the configured `staged` directory where
419
+ * the file would be written initially (this must be a relative path).
420
+ * @param srcFile The name of the file.
421
+ */
422
+ getStagedFilePath(srcDir, srcFile) {
423
+ return join(this.baseStagedDir, srcDir, srcFile);
424
+ }
425
+ /**
426
+ * Return true if the staged file exists for the given source directory and file name.
427
+ *
428
+ * @param srcDir The directory underneath the configured `staged` directory where
429
+ * the file would be written initially (this must be a relative path).
430
+ * @param srcFile The name of the file.
431
+ */
432
+ stagedFileExists(srcDir, srcFile) {
433
+ const fullSrcPath = this.getStagedFilePath(srcDir, srcFile);
434
+ return existsSync(fullSrcPath);
435
+ }
436
+ /**
437
+ * Return true if the destination file exists for the given source directory and file name.
438
+ *
439
+ * @param srcDir The directory underneath the configured `staged` directory where
440
+ * the file would be written initially (this must be a relative path).
441
+ * @param srcFile The name of the file.
442
+ */
443
+ destinationFileExists(srcDir, srcFile) {
444
+ const f = this.stagedFiles.find((f) => f.srcDir === srcDir && f.srcFile === srcFile);
445
+ if (f === void 0) return false;
446
+ const fullDstPath = join(f.dstDir, f.dstFile);
447
+ return existsSync(fullDstPath);
448
+ }
449
+ /**
450
+ * Copy staged files to their destination; this will only copy the staged
451
+ * files if they are different than the existing destination files. We
452
+ * copy the files in a batch like this so that hot module reload is only
453
+ * triggered once at the end of the whole build process.
454
+ */
455
+ copyChangedFiles() {
456
+ log("info", "Copying changed files into place...");
457
+ for (const f of this.stagedFiles) this.copyStagedFile(f);
458
+ log("info", "Done copying files");
459
+ }
460
+ /**
461
+ * Copy a file from the `staged` directory to its destination. If the file already
462
+ * exists in the destination directory and has the same contents as the source file,
463
+ * the file will not be copied and this function will return false.
464
+ *
465
+ * @param f The staged file entry.
466
+ */
467
+ copyStagedFile(f) {
468
+ if (!existsSync(f.dstDir)) mkdirSync(f.dstDir, { recursive: true });
469
+ const fullSrcPath = this.getStagedFilePath(f.srcDir, f.srcFile);
470
+ const fullDstPath = join(f.dstDir, f.dstFile);
471
+ const needsCopy = filesDiffer(fullSrcPath, fullDstPath);
472
+ if (needsCopy) {
473
+ log("verbose", ` Copying ${f.srcFile} to ${fullDstPath}`);
474
+ copyFileSync(fullSrcPath, fullDstPath);
475
+ }
476
+ return needsCopy;
477
+ }
496
478
  };
479
+ /**
480
+ * Return true if both files exist at the given paths and have the same contents, false otherwise.
481
+ */
497
482
  function filesDiffer(aPath, bPath) {
498
- if (existsSync2(aPath) && existsSync2(bPath)) {
499
- const aSize = statSync(aPath).size;
500
- const bSize = statSync(bPath).size;
501
- if (aSize !== bSize) {
502
- return true;
503
- } else {
504
- const aBuf = readFileSync(aPath);
505
- const bBuf = readFileSync(bPath);
506
- return !aBuf.equals(bBuf);
507
- }
508
- } else {
509
- return true;
510
- }
483
+ if (existsSync(aPath) && existsSync(bPath)) {
484
+ if (statSync(aPath).size !== statSync(bPath).size) return true;
485
+ else {
486
+ const aBuf = readFileSync(aPath);
487
+ const bBuf = readFileSync(bPath);
488
+ return !aBuf.equals(bBuf);
489
+ }
490
+ } else return true;
511
491
  }
512
-
513
- // src/build/impl/gen-model.ts
514
- import { copyFile, readdir, readFile, writeFile } from "fs/promises";
515
- import { basename, dirname as dirname2, join as joinPath3 } from "path";
492
+ //#endregion
493
+ //#region src/build/impl/gen-model.ts
494
+ /**
495
+ * Generate the model. This will run the core SDEverywhere code generation steps
496
+ * and will also invoke the following plugin functions:
497
+ * - `preProcessMdl`
498
+ * - `postProcessMdl`
499
+ * - `preGenerateC`
500
+ * - `postGenerateC`
501
+ */
516
502
  async function generateModel(context, plugins) {
517
- const config = context.config;
518
- if (config.modelFiles.length === 0) {
519
- log("info", "No model input files specified, skipping model generation steps");
520
- return;
521
- }
522
- log("info", "Generating model...");
523
- const t0 = performance.now();
524
- const prepDir = config.prepDir;
525
- const sdeCmdPath = config.sdeCmdPath;
526
- for (const plugin of plugins) {
527
- if (plugin.preProcessMdl) {
528
- await plugin.preProcessMdl(context);
529
- }
530
- }
531
- if (config.modelFiles.length === 1) {
532
- await preprocessMdl(context, sdeCmdPath, prepDir, config.modelFiles[0]);
533
- } else {
534
- await flattenMdls(context, sdeCmdPath, prepDir, config.modelFiles);
535
- }
536
- for (const plugin of plugins) {
537
- if (plugin.postProcessMdl) {
538
- const mdlPath = joinPath3(prepDir, "processed.mdl");
539
- let mdlContent = await readFile(mdlPath, "utf8");
540
- mdlContent = await plugin.postProcessMdl(context, mdlContent);
541
- await writeFile(mdlPath, mdlContent);
542
- }
543
- }
544
- for (const plugin of plugins) {
545
- if (plugin.preGenerateCode) {
546
- await plugin.preGenerateCode(context, config.genFormat);
547
- }
548
- }
549
- await generateCode(context, config.sdeDir, sdeCmdPath, prepDir);
550
- const generatedCodeFile = `processed.${config.genFormat}`;
551
- const generatedCodePath = joinPath3(prepDir, "build", generatedCodeFile);
552
- for (const plugin of plugins) {
553
- if (plugin.postGenerateCode) {
554
- let generatedCodeContent = await readFile(generatedCodePath, "utf8");
555
- generatedCodeContent = await plugin.postGenerateCode(context, config.genFormat, generatedCodeContent);
556
- await writeFile(generatedCodePath, generatedCodeContent);
557
- }
558
- }
559
- if (config.genFormat === "js") {
560
- const outputJsFile = "generated-model.js";
561
- const stagedOutputJsPath = context.prepareStagedFile("model", outputJsFile, prepDir, outputJsFile);
562
- await copyFile(generatedCodePath, stagedOutputJsPath);
563
- }
564
- if (config.outListingFile) {
565
- const srcListingJsonPath = joinPath3(prepDir, "build", "processed.json");
566
- const stagedDir = "model";
567
- const stagedFile = "listing.json";
568
- const dstDir = dirname2(config.outListingFile);
569
- const dstFile = basename(config.outListingFile);
570
- const stagedListingJsonPath = context.prepareStagedFile(stagedDir, stagedFile, dstDir, dstFile);
571
- await copyFile(srcListingJsonPath, stagedListingJsonPath);
572
- }
573
- const t1 = performance.now();
574
- const elapsed = ((t1 - t0) / 1e3).toFixed(1);
575
- log("info", `Done generating model (${elapsed}s)`);
503
+ const config = context.config;
504
+ if (config.modelFiles.length === 0) {
505
+ log("info", "No model input files specified, skipping model generation steps");
506
+ return;
507
+ }
508
+ log("info", "Generating model...");
509
+ const t0 = performance.now();
510
+ const prepDir = config.prepDir;
511
+ const sdeCmdPath = config.sdeCmdPath;
512
+ for (const plugin of plugins) if (plugin.preProcessMdl) await plugin.preProcessMdl(context);
513
+ if (config.modelFiles.length === 1) await preprocessMdl(context, sdeCmdPath, prepDir, config.modelFiles[0]);
514
+ else await flattenMdls(context, sdeCmdPath, prepDir, config.modelFiles);
515
+ for (const plugin of plugins) if (plugin.postProcessMdl) {
516
+ const mdlPath = join(prepDir, "processed.mdl");
517
+ let mdlContent = await readFile(mdlPath, "utf8");
518
+ mdlContent = await plugin.postProcessMdl(context, mdlContent);
519
+ await writeFile(mdlPath, mdlContent);
520
+ }
521
+ for (const plugin of plugins) if (plugin.preGenerateCode) await plugin.preGenerateCode(context, config.genFormat);
522
+ await generateCode(context, config.sdeDir, sdeCmdPath, prepDir);
523
+ const generatedCodeFile = `processed.${config.genFormat}`;
524
+ const generatedCodePath = join(prepDir, "build", generatedCodeFile);
525
+ for (const plugin of plugins) if (plugin.postGenerateCode) {
526
+ let generatedCodeContent = await readFile(generatedCodePath, "utf8");
527
+ generatedCodeContent = await plugin.postGenerateCode(context, config.genFormat, generatedCodeContent);
528
+ await writeFile(generatedCodePath, generatedCodeContent);
529
+ }
530
+ if (config.genFormat === "js") {
531
+ const outputJsFile = "generated-model.js";
532
+ const stagedOutputJsPath = context.prepareStagedFile("model", outputJsFile, prepDir, outputJsFile);
533
+ await copyFile(generatedCodePath, stagedOutputJsPath);
534
+ }
535
+ if (config.outListingFile) {
536
+ const srcListingJsonPath = join(prepDir, "build", "processed.json");
537
+ const stagedDir = "model";
538
+ const stagedFile = "listing.json";
539
+ const dstDir = dirname(config.outListingFile);
540
+ const dstFile = basename(config.outListingFile);
541
+ const stagedListingJsonPath = context.prepareStagedFile(stagedDir, stagedFile, dstDir, dstFile);
542
+ await copyFile(srcListingJsonPath, stagedListingJsonPath);
543
+ }
544
+ log("info", `Done generating model (${((performance.now() - t0) / 1e3).toFixed(1)}s)`);
576
545
  }
546
+ /**
547
+ * Preprocess a single mdl file and copy the resulting `processed.mdl` to the prep directory.
548
+ */
577
549
  async function preprocessMdl(context, sdeCmdPath, prepDir, modelFile) {
578
- log("verbose", " Preprocessing mdl file");
579
- await copyFile(modelFile, joinPath3(prepDir, "processed.mdl"));
580
- const command = sdeCmdPath;
581
- const args = ["generate", "--preprocess", "processed.mdl"];
582
- const ppOutput = await context.spawnChild(prepDir, command, args, {
583
- // The default error message from `spawnChild` is not very informative, so the
584
- // following allows us to throw our own error
585
- ignoreError: true
586
- });
587
- if (ppOutput.exitCode !== 0) {
588
- throw new Error(`Failed to preprocess mdl file: 'sde generate' command failed (code=${ppOutput.exitCode})`);
589
- }
590
- await copyFile(joinPath3(prepDir, "build", "processed.mdl"), joinPath3(prepDir, "processed.mdl"));
550
+ log("verbose", " Preprocessing mdl file");
551
+ await copyFile(modelFile, join(prepDir, "processed.mdl"));
552
+ const command = sdeCmdPath;
553
+ const ppOutput = await context.spawnChild(prepDir, command, [
554
+ "generate",
555
+ "--preprocess",
556
+ "processed.mdl"
557
+ ], { ignoreError: true });
558
+ if (ppOutput.exitCode !== 0) throw new Error(`Failed to preprocess mdl file: 'sde generate' command failed (code=${ppOutput.exitCode})`);
559
+ await copyFile(join(prepDir, "build", "processed.mdl"), join(prepDir, "processed.mdl"));
591
560
  }
561
+ /**
562
+ * Flatten multiple mdl files and copy the resulting `processed.mdl` to the prep directory.
563
+ */
592
564
  async function flattenMdls(context, sdeCmdPath, prepDir, modelFiles) {
593
- log("verbose", " Flattening and preprocessing mdl files");
594
- const command = sdeCmdPath;
595
- const args = [];
596
- args.push("flatten");
597
- args.push("processed.mdl");
598
- args.push("--inputs");
599
- for (const path of modelFiles) {
600
- args.push(path);
601
- }
602
- const output = await context.spawnChild(prepDir, command, args, {
603
- logOutput: false,
604
- captureOutput: true,
605
- ignoreError: true
606
- });
607
- let flattenErrors = false;
608
- for (const msg of output.stderrMessages) {
609
- if (msg.includes("ERROR")) {
610
- flattenErrors = true;
611
- break;
612
- }
613
- }
614
- if (flattenErrors) {
615
- log("error", "There were errors reported when flattening the model:");
616
- for (const msg of output.stderrMessages) {
617
- const lines = msg.split("\n");
618
- for (const line of lines) {
619
- log("error", ` ${line}`);
620
- }
621
- }
622
- throw new Error(`Failed to flatten mdl files: 'sde flatten' command failed (code=${output.exitCode})`);
623
- } else if (output.exitCode !== 0) {
624
- throw new Error(`Failed to flatten mdl files: 'sde flatten' command failed (code=${output.exitCode})`);
625
- }
626
- await copyFile(joinPath3(prepDir, "build", "processed.mdl"), joinPath3(prepDir, "processed.mdl"));
565
+ log("verbose", " Flattening and preprocessing mdl files");
566
+ const command = sdeCmdPath;
567
+ const args = [];
568
+ args.push("flatten");
569
+ args.push("processed.mdl");
570
+ args.push("--inputs");
571
+ for (const path of modelFiles) args.push(path);
572
+ const output = await context.spawnChild(prepDir, command, args, {
573
+ logOutput: false,
574
+ captureOutput: true,
575
+ ignoreError: true
576
+ });
577
+ let flattenErrors = false;
578
+ for (const msg of output.stderrMessages) if (msg.includes("ERROR")) {
579
+ flattenErrors = true;
580
+ break;
581
+ }
582
+ if (flattenErrors) {
583
+ log("error", "There were errors reported when flattening the model:");
584
+ for (const msg of output.stderrMessages) {
585
+ const lines = msg.split("\n");
586
+ for (const line of lines) log("error", ` ${line}`);
587
+ }
588
+ throw new Error(`Failed to flatten mdl files: 'sde flatten' command failed (code=${output.exitCode})`);
589
+ } else if (output.exitCode !== 0) throw new Error(`Failed to flatten mdl files: 'sde flatten' command failed (code=${output.exitCode})`);
590
+ await copyFile(join(prepDir, "build", "processed.mdl"), join(prepDir, "processed.mdl"));
627
591
  }
592
+ /**
593
+ * Generate a JS or C file from the `processed.mdl` file.
594
+ */
628
595
  async function generateCode(context, sdeDir, sdeCmdPath, prepDir) {
629
- const genFormat = context.config.genFormat;
630
- const genFormatName = genFormat.toUpperCase();
631
- log("verbose", ` Generating ${genFormatName} code`);
632
- const dataDir = dirname2(context.config.modelFiles[0]);
633
- const command = sdeCmdPath;
634
- const outFormat = `--outformat=${genFormat}`;
635
- const genCmdArgs = ["generate", outFormat, "--list", "--spec", "spec.json", "--datadir", dataDir, "processed"];
636
- const genCmdOutput = await context.spawnChild(prepDir, command, genCmdArgs, {
637
- // By default, ignore lines that start with "WARNING: Data for" since these are often harmless
638
- // TODO: Don't filter by default, but make it configurable
639
- // ignoredMessageFilter: 'WARNING: Data for'
640
- // The default error message from `spawnChild` is not very informative, so the
641
- // following allows us to throw our own error
642
- ignoreError: true
643
- });
644
- if (genCmdOutput.exitCode !== 0) {
645
- throw new Error(
646
- `Failed to generate ${genFormatName} code: 'sde generate' command failed (code=${genCmdOutput.exitCode})`
647
- );
648
- }
649
- if (genFormat === "c") {
650
- const buildDir = joinPath3(prepDir, "build");
651
- const sdeCDir = joinPath3(sdeDir, "src", "c");
652
- const files = await readdir(sdeCDir);
653
- const copyOps = [];
654
- for (const file of files) {
655
- if (file.endsWith(".c") || file.endsWith(".h")) {
656
- copyOps.push(copyFile(joinPath3(sdeCDir, file), joinPath3(buildDir, file)));
657
- }
658
- }
659
- await Promise.all(copyOps);
660
- }
596
+ const genFormat = context.config.genFormat;
597
+ const genFormatName = genFormat.toUpperCase();
598
+ log("verbose", ` Generating ${genFormatName} code`);
599
+ const dataDir = dirname(context.config.modelFiles[0]);
600
+ const command = sdeCmdPath;
601
+ const genCmdArgs = [
602
+ "generate",
603
+ `--outformat=${genFormat}`,
604
+ "--list",
605
+ "--spec",
606
+ "spec.json",
607
+ "--datadir",
608
+ dataDir,
609
+ "processed"
610
+ ];
611
+ const genCmdOutput = await context.spawnChild(prepDir, command, genCmdArgs, { ignoreError: true });
612
+ if (genCmdOutput.exitCode !== 0) throw new Error(`Failed to generate ${genFormatName} code: 'sde generate' command failed (code=${genCmdOutput.exitCode})`);
613
+ if (genFormat === "c") {
614
+ const buildDir = join(prepDir, "build");
615
+ const sdeCDir = join(sdeDir, "src", "c");
616
+ const files = await readdir(sdeCDir);
617
+ const copyOps = [];
618
+ for (const file of files) if (file.endsWith(".c") || file.endsWith(".h")) copyOps.push(copyFile(join(sdeCDir, file), join(buildDir, file)));
619
+ await Promise.all(copyOps);
620
+ }
661
621
  }
662
-
663
- // src/build/impl/hash-files.ts
664
- import { createHash } from "crypto";
665
- import { createReadStream } from "fs";
666
- import { basename as basename2, join as joinPath4 } from "path";
667
- import { pipeline } from "stream/promises";
668
- import { glob } from "tinyglobby";
622
+ //#endregion
623
+ //#region src/build/impl/hash-files.ts
624
+ /**
625
+ * Asynchronously compute the hash of the files that are inputs to the model
626
+ * build process.
627
+ */
669
628
  async function computeInputFilesHash(config) {
670
- const inputFiles = [];
671
- const specFile = joinPath4(config.prepDir, "spec.json");
672
- inputFiles.push(specFile);
673
- if (config.modelInputPaths && config.modelInputPaths.length > 0) {
674
- for (const globPath of config.modelInputPaths) {
675
- const paths = await glob(globPath, {
676
- cwd: config.rootDir,
677
- absolute: true,
678
- onlyFiles: true
679
- });
680
- inputFiles.push(...paths);
681
- }
682
- } else {
683
- inputFiles.push(...config.modelFiles);
684
- }
685
- let hash = "";
686
- for (const inputFile of inputFiles) {
687
- hash += await hashFile(inputFile);
688
- }
689
- return hash;
629
+ const inputFiles = [];
630
+ const specFile = join$1(config.prepDir, "spec.json");
631
+ inputFiles.push(specFile);
632
+ if (config.modelInputPaths && config.modelInputPaths.length > 0) for (const globPath of config.modelInputPaths) {
633
+ const paths = await glob(globPath, {
634
+ cwd: config.rootDir,
635
+ absolute: true,
636
+ onlyFiles: true
637
+ });
638
+ inputFiles.push(...paths);
639
+ }
640
+ else inputFiles.push(...config.modelFiles);
641
+ let hash = "";
642
+ for (const inputFile of inputFiles) hash += await hashFile(inputFile);
643
+ return hash;
690
644
  }
645
+ /**
646
+ * Asynchronously compute the hash of a single file. The returned hash covers the
647
+ * base name of the file followed by its contents, so that renaming a file changes
648
+ * the hash even if its contents are unchanged.
649
+ *
650
+ * @param file The absolute path of the file to hash.
651
+ * @returns The base64-encoded SHA-1 digest for the file.
652
+ */
691
653
  async function hashFile(file) {
692
- const hash = createHash("sha1");
693
- hash.update(basename2(file));
694
- await pipeline(createReadStream(file), hash);
695
- return hash.digest("base64");
654
+ const hash = createHash("sha1");
655
+ hash.update(basename$1(file));
656
+ await pipeline(createReadStream(file), hash);
657
+ return hash.digest("base64");
696
658
  }
697
-
698
- // src/build/impl/build-once.ts
659
+ //#endregion
660
+ //#region src/build/impl/build-once.ts
661
+ /**
662
+ * Perform a single build.
663
+ *
664
+ * This will return an error if a build failure occurred, or if a plugin encounters
665
+ * an error. Otherwise, it will return true if the build and all plugins
666
+ * succeeded, or false if a plugin wants to report a "soft" failure (for example,
667
+ * if the model check plugin reports failing checks).
668
+ *
669
+ * @param config The resolved build configuration.
670
+ * @param plugins The configured plugins.
671
+ * @param options Options specific to the build process.
672
+ * @return An `ok` result with true if the build and all plugins succeeded, or false if
673
+ * one or more plugins failed; otherwise, an `err` result if there was a hard error.
674
+ */
699
675
  async function buildOnce(config, userConfig, plugins, options) {
700
- const stagedFiles = new StagedFiles(config.prepDir);
701
- const context = new BuildContext(config, stagedFiles, options.abortSignal);
702
- const modelHashPath = joinPath5(config.prepDir, "model-hash.txt");
703
- let succeeded = true;
704
- try {
705
- const userModelSpec = await userConfig.modelSpec(context);
706
- if (userModelSpec === void 0) {
707
- return err2(new Error("The model spec must be defined"));
708
- }
709
- const modelSpec = resolveModelSpec(userModelSpec);
710
- for (const plugin of plugins) {
711
- if (plugin.preGenerate) {
712
- await plugin.preGenerate(context, modelSpec);
713
- }
714
- }
715
- const specJson = {
716
- inputVarNames: modelSpec.inputVarNames,
717
- outputVarNames: modelSpec.outputVarNames,
718
- datFiles: modelSpec.datFiles,
719
- bundleListing: modelSpec.bundleListing,
720
- customConstants: modelSpec.customConstants,
721
- customLookups: modelSpec.customLookups,
722
- customOutputs: modelSpec.customOutputs,
723
- directData: modelSpec.directData,
724
- dimensionFamilies: modelSpec.dimensionFamilies,
725
- specialSeparationDims: modelSpec.specialSeparationDims,
726
- separateAllVarsWithDims: modelSpec.separateAllVarsWithDims
727
- };
728
- const specPath = joinPath5(config.prepDir, "spec.json");
729
- await writeFile2(specPath, JSON.stringify(specJson, null, 2));
730
- let previousModelHash;
731
- if (existsSync3(modelHashPath)) {
732
- previousModelHash = readFileSync2(modelHashPath, "utf8");
733
- } else {
734
- previousModelHash = "NONE";
735
- }
736
- const inputFilesHash = await computeInputFilesHash(config);
737
- let needModelGen;
738
- if (options.forceModelGen === true) {
739
- needModelGen = true;
740
- } else {
741
- const hashMismatch = inputFilesHash !== previousModelHash;
742
- needModelGen = hashMismatch;
743
- }
744
- if (needModelGen) {
745
- await generateModel(context, plugins);
746
- writeFileSync3(modelHashPath, inputFilesHash);
747
- } else {
748
- log("info", "Skipping model code generation; already up-to-date");
749
- }
750
- for (const plugin of plugins) {
751
- if (plugin.postGenerate) {
752
- const pluginSucceeded = await plugin.postGenerate(context, modelSpec);
753
- if (!pluginSucceeded) {
754
- succeeded = false;
755
- }
756
- }
757
- }
758
- stagedFiles.copyChangedFiles();
759
- for (const plugin of plugins) {
760
- if (plugin.postBuild) {
761
- const pluginSucceeded = await plugin.postBuild(context, modelSpec);
762
- if (!pluginSucceeded) {
763
- succeeded = false;
764
- }
765
- }
766
- }
767
- if (config.mode === "development") {
768
- log("info", "Waiting for changes...\n");
769
- clearOverlay();
770
- }
771
- } catch (e) {
772
- if (e.message !== "ABORT") {
773
- writeFileSync3(modelHashPath, "");
774
- return err2(e);
775
- }
776
- }
777
- return ok2(succeeded);
676
+ const stagedFiles = new StagedFiles(config.prepDir);
677
+ const context = new BuildContext(config, stagedFiles, options.abortSignal);
678
+ const modelHashPath = join(config.prepDir, "model-hash.txt");
679
+ let succeeded = true;
680
+ try {
681
+ const userModelSpec = await userConfig.modelSpec(context);
682
+ if (userModelSpec === void 0) return err(/* @__PURE__ */ new Error("The model spec must be defined"));
683
+ const modelSpec = resolveModelSpec(userModelSpec);
684
+ for (const plugin of plugins) if (plugin.preGenerate) await plugin.preGenerate(context, modelSpec);
685
+ const specJson = {
686
+ inputVarNames: modelSpec.inputVarNames,
687
+ outputVarNames: modelSpec.outputVarNames,
688
+ datFiles: modelSpec.datFiles,
689
+ bundleListing: modelSpec.bundleListing,
690
+ customConstants: modelSpec.customConstants,
691
+ customLookups: modelSpec.customLookups,
692
+ customOutputs: modelSpec.customOutputs,
693
+ directData: modelSpec.directData,
694
+ dimensionFamilies: modelSpec.dimensionFamilies,
695
+ specialSeparationDims: modelSpec.specialSeparationDims,
696
+ separateAllVarsWithDims: modelSpec.separateAllVarsWithDims
697
+ };
698
+ const specPath = join(config.prepDir, "spec.json");
699
+ await writeFile(specPath, JSON.stringify(specJson, null, 2));
700
+ let previousModelHash;
701
+ if (existsSync(modelHashPath)) previousModelHash = readFileSync(modelHashPath, "utf8");
702
+ else previousModelHash = "NONE";
703
+ const inputFilesHash = await computeInputFilesHash(config);
704
+ let needModelGen;
705
+ if (options.forceModelGen === true) needModelGen = true;
706
+ else needModelGen = inputFilesHash !== previousModelHash;
707
+ if (needModelGen) {
708
+ await generateModel(context, plugins);
709
+ writeFileSync(modelHashPath, inputFilesHash);
710
+ } else log("info", "Skipping model code generation; already up-to-date");
711
+ for (const plugin of plugins) if (plugin.postGenerate) {
712
+ if (!await plugin.postGenerate(context, modelSpec)) succeeded = false;
713
+ }
714
+ stagedFiles.copyChangedFiles();
715
+ for (const plugin of plugins) if (plugin.postBuild) {
716
+ if (!await plugin.postBuild(context, modelSpec)) succeeded = false;
717
+ }
718
+ if (config.mode === "development") {
719
+ log("info", "Waiting for changes...\n");
720
+ clearOverlay();
721
+ }
722
+ } catch (e) {
723
+ if (e.message !== "ABORT") {
724
+ writeFileSync(modelHashPath, "");
725
+ return err(e);
726
+ }
727
+ }
728
+ return ok(succeeded);
778
729
  }
730
+ /**
731
+ * Convert a `ModelSpec` instance to a `ResolvedModelSpec` instance.
732
+ *
733
+ * @param userModelSpec The `ModelSpec` instance returned by the `UserConfig`.
734
+ */
779
735
  function resolveModelSpec(userModelSpec) {
780
- const { options, ...configuredProps } = userModelSpec;
781
- const modelSpec = { ...options, ...configuredProps };
782
- let inputVarNames;
783
- let inputSpecs;
784
- if (modelSpec.inputs.length > 0) {
785
- const item = modelSpec.inputs[0];
786
- if (typeof item === "string") {
787
- inputVarNames = modelSpec.inputs;
788
- inputSpecs = inputVarNames.map((varName) => {
789
- return {
790
- varName
791
- };
792
- });
793
- } else {
794
- inputSpecs = modelSpec.inputs;
795
- inputVarNames = inputSpecs.map((spec) => spec.varName);
796
- }
797
- } else {
798
- inputVarNames = [];
799
- inputSpecs = [];
800
- }
801
- let outputVarNames;
802
- let outputSpecs;
803
- if (modelSpec.outputs.length > 0) {
804
- const item = modelSpec.outputs[0];
805
- if (typeof item === "string") {
806
- outputVarNames = modelSpec.outputs;
807
- outputSpecs = outputVarNames.map((varName) => {
808
- return {
809
- varName
810
- };
811
- });
812
- } else {
813
- outputSpecs = modelSpec.outputs;
814
- outputVarNames = outputSpecs.map((spec) => spec.varName);
815
- }
816
- } else {
817
- outputVarNames = [];
818
- outputSpecs = [];
819
- }
820
- let customConstants;
821
- if (modelSpec.customConstants !== void 0) {
822
- customConstants = modelSpec.customConstants;
823
- } else {
824
- customConstants = false;
825
- }
826
- let customLookups;
827
- if (modelSpec.customLookups !== void 0) {
828
- customLookups = modelSpec.customLookups;
829
- } else {
830
- customLookups = false;
831
- }
832
- let customOutputs;
833
- if (modelSpec.customOutputs !== void 0) {
834
- customOutputs = modelSpec.customOutputs;
835
- } else {
836
- customOutputs = false;
837
- }
838
- return {
839
- // Carry through the properties (for example, `directData` and `specialSeparationDims`)
840
- // that are shared with the compile package and don't need to be resolved here
841
- ...modelSpec,
842
- inputVarNames,
843
- inputs: inputSpecs,
844
- outputVarNames,
845
- outputs: outputSpecs,
846
- datFiles: modelSpec.datFiles || [],
847
- bundleListing: modelSpec.bundleListing === true,
848
- customConstants,
849
- customLookups,
850
- customOutputs
851
- };
736
+ const { options, ...configuredProps } = userModelSpec;
737
+ const modelSpec = {
738
+ ...options,
739
+ ...configuredProps
740
+ };
741
+ let inputVarNames;
742
+ let inputSpecs;
743
+ if (modelSpec.inputs.length > 0) {
744
+ if (typeof modelSpec.inputs[0] === "string") {
745
+ inputVarNames = modelSpec.inputs;
746
+ inputSpecs = inputVarNames.map((varName) => {
747
+ return { varName };
748
+ });
749
+ } else {
750
+ inputSpecs = modelSpec.inputs;
751
+ inputVarNames = inputSpecs.map((spec) => spec.varName);
752
+ }
753
+ } else {
754
+ inputVarNames = [];
755
+ inputSpecs = [];
756
+ }
757
+ let outputVarNames;
758
+ let outputSpecs;
759
+ if (modelSpec.outputs.length > 0) {
760
+ if (typeof modelSpec.outputs[0] === "string") {
761
+ outputVarNames = modelSpec.outputs;
762
+ outputSpecs = outputVarNames.map((varName) => {
763
+ return { varName };
764
+ });
765
+ } else {
766
+ outputSpecs = modelSpec.outputs;
767
+ outputVarNames = outputSpecs.map((spec) => spec.varName);
768
+ }
769
+ } else {
770
+ outputVarNames = [];
771
+ outputSpecs = [];
772
+ }
773
+ let customConstants;
774
+ if (modelSpec.customConstants !== void 0) customConstants = modelSpec.customConstants;
775
+ else customConstants = false;
776
+ let customLookups;
777
+ if (modelSpec.customLookups !== void 0) customLookups = modelSpec.customLookups;
778
+ else customLookups = false;
779
+ let customOutputs;
780
+ if (modelSpec.customOutputs !== void 0) customOutputs = modelSpec.customOutputs;
781
+ else customOutputs = false;
782
+ return {
783
+ ...modelSpec,
784
+ inputVarNames,
785
+ inputs: inputSpecs,
786
+ outputVarNames,
787
+ outputs: outputSpecs,
788
+ datFiles: modelSpec.datFiles || [],
789
+ bundleListing: modelSpec.bundleListing === true,
790
+ customConstants,
791
+ customLookups,
792
+ customOutputs
793
+ };
852
794
  }
853
-
854
- // src/build/impl/watch.ts
855
- import { basename as basename3 } from "path";
856
-
857
- // src/build/impl/watch-paths.ts
858
- import chokidar from "chokidar";
859
- import { globSync, isDynamicPattern } from "tinyglobby";
795
+ //#endregion
796
+ //#region src/build/impl/watch-paths.ts
797
+ /**
798
+ * Resolve watch paths, expanding glob patterns into concrete file paths.
799
+ *
800
+ * This function doesn't attempt to expand directories. This is intentional
801
+ * as it allows chokidar to handle directory changes (added and removed files)
802
+ * instead of it being passed a specific set of files when the watch is first
803
+ * set up.
804
+ *
805
+ * @param patterns The watch paths to resolve (may include glob patterns).
806
+ * @param cwd The current working directory to resolve paths relative to.
807
+ * @returns An array of resolved paths. Non-glob paths are returned as-is,
808
+ * while glob patterns are expanded to absolute paths of matching files.
809
+ */
860
810
  function resolveWatchPaths(patterns, cwd) {
861
- const resolvedPaths = [];
862
- const regularPaths = [];
863
- const globPatterns = [];
864
- for (const pattern of patterns) {
865
- if (isDynamicPattern(pattern)) {
866
- globPatterns.push(pattern);
867
- } else {
868
- regularPaths.push(pattern);
869
- }
870
- }
871
- resolvedPaths.push(...regularPaths);
872
- if (globPatterns.length > 0) {
873
- const paths = globSync(globPatterns, {
874
- // Watch paths are resolved relative to the provided cwd
875
- cwd,
876
- // Resolve to absolute paths
877
- absolute: true
878
- });
879
- resolvedPaths.push(...paths);
880
- }
881
- return resolvedPaths;
811
+ const resolvedPaths = [];
812
+ const regularPaths = [];
813
+ const globPatterns = [];
814
+ for (const pattern of patterns) if (isDynamicPattern(pattern)) globPatterns.push(pattern);
815
+ else regularPaths.push(pattern);
816
+ resolvedPaths.push(...regularPaths);
817
+ if (globPatterns.length > 0) {
818
+ const paths = globSync(globPatterns, {
819
+ cwd,
820
+ absolute: true
821
+ });
822
+ resolvedPaths.push(...paths);
823
+ }
824
+ return resolvedPaths;
882
825
  }
826
+ /**
827
+ * Watch file paths and invoke callbacks when files are changed, added, or removed.
828
+ *
829
+ * This function sets up a file watcher using chokidar. Glob patterns in the paths
830
+ * are resolved before being passed to chokidar (since chokidar no longer supports
831
+ * glob patterns).
832
+ *
833
+ * @param patterns The paths to watch (may include glob patterns).
834
+ * @param cwd The current working directory to resolve paths relative to.
835
+ * @param onChange Callback invoked when a file is changed.
836
+ * @param onReady Optional callback invoked when the watcher is ready.
837
+ * @returns A cleanup function that closes the watcher.
838
+ */
883
839
  function watchPaths(patterns, cwd, onChange, onReady) {
884
- const resolvedPathsToWatch = resolveWatchPaths(patterns, cwd);
885
- const watcher = chokidar.watch(resolvedPathsToWatch, {
886
- // Watch paths are resolved relative to the provided cwd
887
- cwd,
888
- // Ignore the initial add events when the watcher is created
889
- ignoreInitial: true,
890
- // Include a delay, otherwise on macOS we sometimes get multiple
891
- // change events when the file is saved just once
892
- awaitWriteFinish: {
893
- stabilityThreshold: 200
894
- }
895
- });
896
- watcher.on("change", (path) => {
897
- onChange(path);
898
- });
899
- watcher.on("add", (path) => {
900
- onChange(path);
901
- });
902
- watcher.on("unlink", (path) => {
903
- onChange(path);
904
- });
905
- if (onReady) {
906
- watcher.on("ready", onReady);
907
- }
908
- return () => {
909
- watcher.close();
910
- };
840
+ const resolvedPathsToWatch = resolveWatchPaths(patterns, cwd);
841
+ const watcher = chokidar.watch(resolvedPathsToWatch, {
842
+ cwd,
843
+ ignoreInitial: true,
844
+ awaitWriteFinish: { stabilityThreshold: 200 }
845
+ });
846
+ watcher.on("change", (path) => {
847
+ onChange(path);
848
+ });
849
+ watcher.on("add", (path) => {
850
+ onChange(path);
851
+ });
852
+ watcher.on("unlink", (path) => {
853
+ onChange(path);
854
+ });
855
+ if (onReady) watcher.on("ready", onReady);
856
+ return () => {
857
+ watcher.close();
858
+ };
911
859
  }
912
-
913
- // src/build/impl/watch.ts
860
+ //#endregion
861
+ //#region src/build/impl/watch.ts
914
862
  var BuildState = class {
915
- constructor() {
916
- this.abortController = new AbortController();
917
- }
863
+ constructor() {
864
+ this.abortController = new AbortController();
865
+ }
918
866
  };
919
867
  function watch(config, userConfig, plugins) {
920
- const delay = 150;
921
- const changedPaths = /* @__PURE__ */ new Set();
922
- let currentBuildState;
923
- function performBuild() {
924
- clearOverlay();
925
- for (const path of changedPaths) {
926
- log("info", `Input file ${basename3(path)} has been changed`);
927
- }
928
- changedPaths.clear();
929
- if (currentBuildState) {
930
- currentBuildState.abortController.abort();
931
- currentBuildState = void 0;
932
- }
933
- currentBuildState = new BuildState();
934
- const buildOptions = {
935
- abortSignal: currentBuildState.abortController.signal
936
- };
937
- buildOnce(config, userConfig, plugins, buildOptions).then((result) => {
938
- if (result.isErr()) {
939
- logError(result.error);
940
- }
941
- }).catch((e) => {
942
- logError(e);
943
- }).finally(() => {
944
- currentBuildState = void 0;
945
- });
946
- }
947
- function scheduleBuild(changedPath) {
948
- const schedule = changedPaths.size === 0;
949
- changedPaths.add(changedPath);
950
- if (schedule) {
951
- setTimeout(() => {
952
- performBuild();
953
- }, delay);
954
- }
955
- }
956
- let watchPatterns;
957
- if (config.watchPaths && config.watchPaths.length > 0) {
958
- watchPatterns = config.watchPaths;
959
- } else {
960
- watchPatterns = config.modelFiles;
961
- }
962
- watchPaths(watchPatterns, config.rootDir, (path) => {
963
- scheduleBuild(path);
964
- });
868
+ const delay = 150;
869
+ const changedPaths = /* @__PURE__ */ new Set();
870
+ let currentBuildState;
871
+ function performBuild() {
872
+ clearOverlay();
873
+ for (const path of changedPaths) log("info", `Input file ${basename(path)} has been changed`);
874
+ changedPaths.clear();
875
+ if (currentBuildState) {
876
+ currentBuildState.abortController.abort();
877
+ currentBuildState = void 0;
878
+ }
879
+ currentBuildState = new BuildState();
880
+ buildOnce(config, userConfig, plugins, { abortSignal: currentBuildState.abortController.signal }).then((result) => {
881
+ if (result.isErr()) logError(result.error);
882
+ }).catch((e) => {
883
+ logError(e);
884
+ }).finally(() => {
885
+ currentBuildState = void 0;
886
+ });
887
+ }
888
+ function scheduleBuild(changedPath) {
889
+ const schedule = changedPaths.size === 0;
890
+ changedPaths.add(changedPath);
891
+ if (schedule) setTimeout(() => {
892
+ performBuild();
893
+ }, delay);
894
+ }
895
+ let watchPatterns;
896
+ if (config.watchPaths && config.watchPaths.length > 0) watchPatterns = config.watchPaths;
897
+ else watchPatterns = config.modelFiles;
898
+ watchPaths(watchPatterns, config.rootDir, (path) => {
899
+ scheduleBuild(path);
900
+ });
965
901
  }
966
-
967
- // src/build/build.ts
902
+ //#endregion
903
+ //#region src/build/build.ts
904
+ /**
905
+ * Initiate the build process, which can either be a single build if `mode` is
906
+ * 'production', or a live development environment if `mode` is 'development'.
907
+ *
908
+ * @param mode The build mode.
909
+ * @param options The build options.
910
+ * @return An `ok` result if the build completed, otherwise an `err` result.
911
+ */
968
912
  async function build(mode, options) {
969
- const configResult = await loadConfig(mode, options.config, options.sdeDir, options.sdeCmdPath);
970
- if (configResult.isErr()) {
971
- return err3(configResult.error);
972
- }
973
- const { userConfig, resolvedConfig } = configResult.value;
974
- if (options.logLevels !== void 0) {
975
- setActiveLevels(options.logLevels);
976
- }
977
- const messagesPath = joinPath6(resolvedConfig.prepDir, "messages.html");
978
- const overlayEnabled2 = mode === "development";
979
- setOverlayFile(messagesPath, overlayEnabled2);
980
- try {
981
- const plugins = userConfig.plugins || [];
982
- for (const plugin of plugins) {
983
- if (plugin.init) {
984
- await plugin.init(resolvedConfig);
985
- }
986
- }
987
- if (mode === "development") {
988
- const buildResult = await buildOnce(resolvedConfig, userConfig, plugins, {});
989
- if (buildResult.isErr()) {
990
- return err3(buildResult.error);
991
- }
992
- for (const plugin of plugins) {
993
- if (plugin.watch) {
994
- await plugin.watch(resolvedConfig);
995
- }
996
- }
997
- watch(resolvedConfig, userConfig, plugins);
998
- return ok3({});
999
- } else {
1000
- const buildResult = await buildOnce(resolvedConfig, userConfig, plugins, {});
1001
- if (buildResult.isErr()) {
1002
- return err3(buildResult.error);
1003
- }
1004
- const allPluginsSucceeded = buildResult.value;
1005
- const exitCode = allPluginsSucceeded ? 0 : 2;
1006
- return ok3({ exitCode });
1007
- }
1008
- } catch (e) {
1009
- return err3(e);
1010
- }
913
+ const configResult = await loadConfig(mode, options.config, options.sdeDir, options.sdeCmdPath);
914
+ if (configResult.isErr()) return err(configResult.error);
915
+ const { userConfig, resolvedConfig } = configResult.value;
916
+ if (options.logLevels !== void 0) setActiveLevels(options.logLevels);
917
+ setOverlayFile(join(resolvedConfig.prepDir, "messages.html"), mode === "development");
918
+ try {
919
+ const plugins = userConfig.plugins || [];
920
+ for (const plugin of plugins) if (plugin.init) await plugin.init(resolvedConfig);
921
+ if (mode === "development") {
922
+ const buildResult = await buildOnce(resolvedConfig, userConfig, plugins, {});
923
+ if (buildResult.isErr()) return err(buildResult.error);
924
+ for (const plugin of plugins) if (plugin.watch) await plugin.watch(resolvedConfig);
925
+ watch(resolvedConfig, userConfig, plugins);
926
+ return ok({});
927
+ } else {
928
+ const buildResult = await buildOnce(resolvedConfig, userConfig, plugins, {});
929
+ if (buildResult.isErr()) return err(buildResult.error);
930
+ const exitCode = buildResult.value ? 0 : 2;
931
+ return ok({ exitCode });
932
+ }
933
+ } catch (e) {
934
+ return err(e);
935
+ }
1011
936
  }
1012
- export {
1013
- build
1014
- };
937
+ //#endregion
938
+ export { build };
939
+
1015
940
  //# sourceMappingURL=index.js.map