@sdeverywhere/build 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +19 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +0 -4
- package/dist/index.js +15 -25
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -2,22 +2,8 @@ var __create = Object.create;
|
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
6
5
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
6
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
9
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
10
|
-
var __spreadValues = (a, b) => {
|
|
11
|
-
for (var prop in b || (b = {}))
|
|
12
|
-
if (__hasOwnProp.call(b, prop))
|
|
13
|
-
__defNormalProp(a, prop, b[prop]);
|
|
14
|
-
if (__getOwnPropSymbols)
|
|
15
|
-
for (var prop of __getOwnPropSymbols(b)) {
|
|
16
|
-
if (__propIsEnum.call(b, prop))
|
|
17
|
-
__defNormalProp(a, prop, b[prop]);
|
|
18
|
-
}
|
|
19
|
-
return a;
|
|
20
|
-
};
|
|
21
7
|
var __export = (target, all) => {
|
|
22
8
|
for (var name in all)
|
|
23
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -30,7 +16,10 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
30
16
|
}
|
|
31
17
|
return to;
|
|
32
18
|
};
|
|
33
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
21
|
+
mod
|
|
22
|
+
));
|
|
34
23
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
35
24
|
|
|
36
25
|
// src/index.ts
|
|
@@ -408,9 +397,13 @@ function filesDiffer(aPath, bPath) {
|
|
|
408
397
|
var import_promises = require("fs/promises");
|
|
409
398
|
var import_path3 = require("path");
|
|
410
399
|
async function generateModel(context, plugins) {
|
|
400
|
+
const config = context.config;
|
|
401
|
+
if (config.modelFiles.length === 0) {
|
|
402
|
+
log("info", "No model input files specified, skipping model generation steps");
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
411
405
|
log("info", "Generating model...");
|
|
412
406
|
const t0 = performance.now();
|
|
413
|
-
const config = context.config;
|
|
414
407
|
const prepDir = config.prepDir;
|
|
415
408
|
const sdeCmdPath = config.sdeCmdPath;
|
|
416
409
|
for (const plugin of plugins) {
|
|
@@ -418,9 +411,7 @@ async function generateModel(context, plugins) {
|
|
|
418
411
|
await plugin.preProcessMdl(context);
|
|
419
412
|
}
|
|
420
413
|
}
|
|
421
|
-
if (config.modelFiles.length ===
|
|
422
|
-
throw new Error("No model input files specified");
|
|
423
|
-
} else if (config.modelFiles.length === 1) {
|
|
414
|
+
if (config.modelFiles.length === 1) {
|
|
424
415
|
await preprocessMdl(context, sdeCmdPath, prepDir, config.modelFiles[0]);
|
|
425
416
|
} else {
|
|
426
417
|
await flattenMdls(context, sdeCmdPath, prepDir, config.modelFiles);
|
|
@@ -558,11 +549,12 @@ async function buildOnce(config, userConfig, plugins, options) {
|
|
|
558
549
|
plugin.preGenerate(context, modelSpec);
|
|
559
550
|
}
|
|
560
551
|
}
|
|
561
|
-
const specJson =
|
|
552
|
+
const specJson = {
|
|
562
553
|
inputVarNames: modelSpec.inputs.map((input) => input.varName),
|
|
563
554
|
outputVarNames: modelSpec.outputs.map((output) => output.varName),
|
|
564
|
-
externalDatfiles: modelSpec.datFiles
|
|
565
|
-
|
|
555
|
+
externalDatfiles: modelSpec.datFiles,
|
|
556
|
+
...modelSpec.options
|
|
557
|
+
};
|
|
566
558
|
const specPath = (0, import_path5.join)(config.prepDir, "spec.json");
|
|
567
559
|
await (0, import_promises2.writeFile)(specPath, JSON.stringify(specJson, null, 2));
|
|
568
560
|
const modelHashPath = (0, import_path5.join)(config.prepDir, "model-hash.txt");
|
|
@@ -644,7 +636,11 @@ function watch(config, userConfig, plugins) {
|
|
|
644
636
|
const buildOptions = {
|
|
645
637
|
abortSignal: currentBuildState.abortController.signal
|
|
646
638
|
};
|
|
647
|
-
buildOnce(config, userConfig, plugins, buildOptions).
|
|
639
|
+
buildOnce(config, userConfig, plugins, buildOptions).then((result) => {
|
|
640
|
+
if (result.isErr()) {
|
|
641
|
+
logError(result.error);
|
|
642
|
+
}
|
|
643
|
+
}).catch((e) => {
|
|
648
644
|
logError(e);
|
|
649
645
|
}).finally(() => {
|
|
650
646
|
currentBuildState = void 0;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/build/build.ts","../src/config/config-loader.ts","../src/_shared/log.ts","../src/build/impl/build-once.ts","../src/context/spawn-child.ts","../src/context/context.ts","../src/context/staged-files.ts","../src/build/impl/gen-model.ts","../src/build/impl/hash-files.ts","../src/build/impl/watch.ts"],"sourcesContent":["// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\n//\n// Note that exports are ordered here according to the dependency chain,\n// i.e., lower items depend on the ones above.\n//\n\nexport type { LogLevel } from './_shared/log'\nexport type { BuildMode } from './_shared/mode'\nexport type { InputSpec, ModelSpec, OutputSpec } from './_shared/model-spec'\nexport type { ResolvedConfig } from './_shared/resolved-config'\n\nexport type { BuildContext } from './context/context'\n\nexport type { Plugin } from './plugin/plugin'\n\nexport type { UserConfig } from './config/user-config'\n\nexport type { BuildOptions, BuildResult } from './build/build'\nexport { build } from './build/build'\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { join as joinPath } from 'path'\n\nimport type { Result } from 'neverthrow'\nimport { err, ok } from 'neverthrow'\n\nimport type { BuildMode } from '../_shared/mode'\n\nimport { loadConfig } from '../config/config-loader'\nimport type { UserConfig } from '../config/user-config'\nimport type { LogLevel } from '../_shared/log'\nimport { setOverlayFile, setActiveLevels } from '../_shared/log'\nimport { buildOnce } from './impl/build-once'\nimport { watch } from './impl/watch'\n\nexport interface BuildOptions {\n /** The path to an `sde.config.js` file, or a `UserConfig` object. */\n config?: string | UserConfig\n\n /**\n * The log levels to include. If undefined, the default 'info' and 'error' levels\n * will be active.\n */\n logLevels?: LogLevel[]\n\n /**\n * The path to the `@sdeverywhere/cli` package. This is currently only used to get\n * access to the files in the `src/c` directory.\n * @hidden This should be removed once we have tighter integration with the `cli` package.\n */\n sdeDir: string\n\n /**\n * The path to the `sde` command.\n * @hidden This should be removed once we have tighter integration with the `cli` package.\n */\n sdeCmdPath: string\n}\n\nexport interface BuildResult {\n /**\n * The exit code that should be set by the process. This will be undefined\n * if `mode` is 'development', indicating that the process should be kept alive.\n */\n exitCode?: number\n}\n\n/**\n * Initiate the build process, which can either be a single build if `mode` is\n * 'production', or a live development environment if `mode` is 'development'.\n *\n * @param mode The build mode.\n * @param options The build options.\n * @return An `ok` result if the build completed, otherwise an `err` result.\n */\nexport async function build(mode: BuildMode, options: BuildOptions): Promise<Result<BuildResult, Error>> {\n // Load the config\n const configResult = await loadConfig(mode, options.config, options.sdeDir, options.sdeCmdPath)\n if (configResult.isErr()) {\n return err(configResult.error)\n }\n const { userConfig, resolvedConfig } = configResult.value\n\n // Configure logging level\n if (options.logLevels !== undefined) {\n setActiveLevels(options.logLevels)\n }\n\n // Configure the overlay `messages.html` file, which is written under the\n // `sde-prep` directory. For production builds, this file will remain empty.\n const messagesPath = joinPath(resolvedConfig.prepDir, 'messages.html')\n const overlayEnabled = mode === 'development'\n setOverlayFile(messagesPath, overlayEnabled)\n\n // Initialize plugins\n const plugins = userConfig.plugins || []\n for (const plugin of plugins) {\n if (plugin.init) {\n await plugin.init(resolvedConfig)\n }\n }\n\n try {\n const plugins = userConfig.plugins || []\n\n if (mode === 'development') {\n // Enable dev mode (which will rebuild when watched files are changed).\n // First run an initial build so that we have a baseline, and then\n // once that is complete, enable file watchers.\n const buildResult = await buildOnce(resolvedConfig, userConfig, plugins, {})\n // TODO: We should trap errors here and keep the dev process\n // running if the initial build fails\n if (buildResult.isErr()) {\n return err(buildResult.error)\n }\n\n // Allow plugins to set up watchers after the initial build completes\n for (const plugin of plugins) {\n if (plugin.watch) {\n await plugin.watch(resolvedConfig)\n }\n }\n\n // Watch for changes to the source/model/test files\n watch(resolvedConfig, userConfig, plugins)\n\n // Return a build result with undefined exit code, indicating that the\n // process should be kept alive\n return ok({})\n } else {\n // Run a single build\n const buildResult = await buildOnce(resolvedConfig, userConfig, plugins, {})\n if (buildResult.isErr()) {\n return err(buildResult.error)\n }\n\n // Configure the exit code depending on whether any plugins failed.\n // Currently we use the following exit code values:\n // 0 == build succeeded, AND all plugins succeeded\n // 1 == build failed (or a plugin reported a \"hard\" error)\n // 2 == build succeeded, BUT one or more plugins failed\n const allPluginsSucceeded = buildResult.value\n const exitCode = allPluginsSucceeded ? 0 : 2\n return ok({ exitCode })\n }\n } catch (e) {\n return err(e)\n }\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { existsSync, lstatSync, mkdirSync } from 'fs'\nimport { dirname, join as joinPath, relative, resolve as resolvePath } from 'path'\nimport { fileURLToPath } from 'url'\n\nimport type { Result } from 'neverthrow'\nimport { err, ok } from 'neverthrow'\n\nimport type { BuildMode } from '../_shared/mode'\nimport type { ResolvedConfig } from '../_shared/resolved-config'\n\nimport type { UserConfig } from './user-config'\n\nexport interface ConfigResult {\n userConfig: UserConfig\n resolvedConfig: ResolvedConfig\n}\n\n/**\n * Load a user-defined config file or a given `UserConfig` object. This validates\n * all paths and if successful, this returns a `ResolvedConfig`, otherwise returns\n * an error result.\n *\n * @param mode The build mode.\n * @param config The path to a config file, or a `UserConfig` object; if undefined,\n * this will look for a `sde.config.js` file in the current directory.\n * @param sdeDir Temporary (the path to the `@sdeverywhere/cli` package).\n * @param sdeCmdPath Temporary (the path to the `sde` command).\n * @return An `ok` result with the `ResolvedConfig`, otherwise an `err` result.\n */\nexport async function loadConfig(\n mode: BuildMode,\n config: string | UserConfig | undefined,\n sdeDir: string,\n sdeCmdPath: string\n): Promise<Result<ConfigResult, Error>> {\n let userConfig: UserConfig\n if (typeof config === 'object') {\n // Use the given `UserConfig` object\n userConfig = config\n } else {\n // Load the project-specific config. If no `--config` arg was specified\n // on the command line, look for a `sde.config.js` file in the current\n // directory, and failing that, use a default config.\n // TODO: Create a default config if no file found; for now, we just fail\n let configPath: string\n if (typeof config === 'string') {\n configPath = config\n } else {\n configPath = joinPath(process.cwd(), 'sde.config.js')\n }\n try {\n if (!existsSync(configPath)) {\n return err(new Error(`Cannot find config file '${configPath}'`))\n }\n const configRelPath = relativeToSourcePath(configPath)\n const configModule = await import(configRelPath)\n userConfig = await configModule.config()\n } catch (e) {\n return err(new Error(`Failed to load config file '${configPath}': ${e.message}`))\n }\n }\n\n // Resolve the config\n try {\n const resolvedConfig = resolveUserConfig(userConfig, mode, sdeDir, sdeCmdPath)\n return ok({\n userConfig,\n resolvedConfig\n })\n } catch (e) {\n return err(e)\n }\n}\n\n/**\n * Resolve the given user configuration by resolving all paths. This will create\n * the prep directory if it does not already exist. This will throw an error if\n * any other paths are invalid or do not exist.\n *\n * @param userConfig The user-defined configuration.\n * @param mode The active build mode.\n * @param sdeDir Temporary (the path to the `@sdeverywhere/cli` package).\n * @param sdeCmdPath Temporary (the path to the `sde` command).\n * @return The resolved configuration.\n */\nfunction resolveUserConfig(\n userConfig: UserConfig,\n mode: BuildMode,\n sdeDir: string,\n sdeCmdPath: string\n): ResolvedConfig {\n function expectDirectory(propName: string, path: string): void {\n if (!existsSync(path)) {\n throw new Error(`The configured ${propName} (${path}) does not exist`)\n } else if (!lstatSync(path).isDirectory()) {\n throw new Error(`The configured ${propName} (${path}) is not a directory`)\n }\n }\n\n // function expectFile(propName: string, path: string): void {\n // if (!existsSync(path)) {\n // throw new Error(`The configured ${propName} (${path}) does not exist`)\n // // TODO: Don't include the \"is file\" check for now; need to update this to\n // // handle symlinks\n // // } else if (!lstatSync(path).isFile()) {\n // // throw new Error(`The configured ${propName} (${path}) is not a file`)\n // }\n // }\n\n // Validate the root directory\n let rootDir: string\n if (userConfig.rootDir) {\n // Verify that the configured root directory exists\n rootDir = resolvePath(userConfig.rootDir)\n expectDirectory('rootDir', rootDir)\n } else {\n // Use the current working directory as the project root\n rootDir = process.cwd()\n }\n\n // Validate the prep directory\n let prepDir: string\n if (userConfig.prepDir) {\n // Resolve the configured prep directory\n prepDir = resolvePath(userConfig.prepDir)\n } else {\n // Create an 'sde-prep' directory under the configured root directory\n prepDir = resolvePath(rootDir, 'sde-prep')\n }\n mkdirSync(prepDir, { recursive: true })\n\n // Validate the model files\n const userModelFiles = userConfig.modelFiles\n const modelFiles: string[] = []\n for (const userModelFile of userModelFiles) {\n const modelFile = resolvePath(userModelFile)\n if (!existsSync(modelFile)) {\n throw new Error(`The configured model file (${modelFile}) does not exist`)\n }\n modelFiles.push(modelFile)\n }\n\n // TODO: Validate the watch paths; these are allowed to be globs, so need to\n // figure out the best way to resolve them\n let modelInputPaths: string[]\n if (userConfig.modelInputPaths && userConfig.modelInputPaths.length > 0) {\n modelInputPaths = userConfig.modelInputPaths\n } else {\n modelInputPaths = modelFiles\n }\n let watchPaths: string[]\n if (userConfig.watchPaths && userConfig.watchPaths.length > 0) {\n watchPaths = userConfig.watchPaths\n } else {\n watchPaths = modelFiles\n }\n\n return {\n mode,\n rootDir,\n prepDir,\n modelFiles,\n modelInputPaths,\n watchPaths,\n sdeDir,\n sdeCmdPath\n }\n}\n\n/**\n * Return a Unix-style path (e.g. '../../foo.js') that is relative to the directory of\n * the current source file. This can be used to construct a path that is safe for\n * dynamic import on either Unix or Windows.\n *\n * @param filePath The path to make relative.\n */\nfunction relativeToSourcePath(filePath: string): string {\n const srcDir = dirname(fileURLToPath(import.meta.url))\n const relPath = relative(srcDir, filePath)\n return relPath.replaceAll('\\\\', '/')\n}\n","// Copyright (c) 2021-2022 Climate Interactive / New Venture Fund\n\nimport { writeFileSync } from 'fs'\nimport pico from 'picocolors'\n\nexport type LogLevel = 'error' | 'info' | 'verbose'\n\nconst activeLevels: Set<LogLevel> = new Set(['error', 'info'])\n\nlet overlayFile: string\nlet overlayEnabled = false\nlet overlayHtml = ''\n\n/**\n * Set the active logging levels. By default, only 'error' and 'info'\n * messages are emitted.\n *\n * @param logLevels The logging levels to include.\n */\nexport function setActiveLevels(logLevels: LogLevel[]): void {\n activeLevels.clear()\n for (const level of logLevels) {\n activeLevels.add(level)\n }\n}\n\n/**\n * Set the path to the `messages.html` file where overlay messages will be written.\n *\n * @param file The absolute path to the HTML file where messages will be written.\n * @param enabled Whether to write messages to the file; if false, the file will be\n * emptied and no further messages will be written.\n */\nexport function setOverlayFile(file: string, enabled: boolean): void {\n overlayFile = file\n overlayEnabled = enabled\n\n // Write an empty file by default; this will ensure that messages from a previous\n // build aren't included in the current build\n writeFileSync(overlayFile, '')\n}\n\n/**\n * Log a message to the console and/or overlay.\n *\n * @param level The logging level.\n * @param msg The message to emit.\n */\nexport function log(level: LogLevel, msg: string): void {\n if (activeLevels.has(level)) {\n if (level === 'error') {\n console.error(pico.red(msg))\n logToOverlay(msg)\n } else {\n console.log(msg)\n logToOverlay(msg)\n }\n }\n}\n\n/**\n * Log an error to the console and/or overlay.\n *\n * @param e The error to log.\n */\nexport function logError(e: Error): void {\n // Remove the first part of the stack trace (which contains the message)\n // so that we can control the formatting of the message separately, then\n // only include up to 3 lines of the stack to keep the log cleaner\n const stack = e.stack || ''\n const stackLines = stack.split('\\n').filter(s => s.match(/^\\s+at/))\n const trace = stackLines.slice(0, 3).join('\\n')\n\n // Log the error message followed by the stack trace\n console.error(pico.red(`\\nERROR: ${e.message}`))\n console.error(pico.dim(pico.red(`${trace}\\n`)))\n logToOverlay(`\\nERROR: ${e.message}`, true)\n logToOverlay(`${trace}\\n`, true)\n}\n\nfunction writeOverlayFiles(): void {\n writeFileSync(overlayFile, overlayHtml)\n}\n\nexport function clearOverlay(): void {\n if (!overlayEnabled) {\n return\n }\n\n overlayHtml = ''\n writeOverlayFiles()\n}\n\nconst indent = ' '.repeat(4)\n\nexport function logToOverlay(msg: string, error = false): void {\n if (!overlayEnabled) {\n return\n }\n\n if (error) {\n msg = `<span class=\"overlay-error\">${msg}</span>`\n }\n const msgHtml = msg.replace(/\\n/g, '\\n<br/>').replace(/\\s{2}/g, indent)\n if (overlayHtml) {\n overlayHtml += `<br/>${msgHtml}`\n } else {\n overlayHtml = `${msgHtml}`\n }\n writeOverlayFiles()\n}\n\nexport function clearConsole(): void {\n // TODO: This is disabled for now; maybe re-enable it under an optional flag\n // console.log('\\x1Bc')\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { existsSync, readFileSync, writeFileSync } from 'fs'\nimport { writeFile } from 'fs/promises'\nimport { join as joinPath } from 'path'\n\nimport type { Result } from 'neverthrow'\nimport { err, ok } from 'neverthrow'\n\nimport { clearOverlay, log } from '../../_shared/log'\nimport type { ResolvedConfig } from '../../_shared/resolved-config'\n\nimport type { UserConfig } from '../../config/user-config'\nimport { BuildContext } from '../../context/context'\nimport { StagedFiles } from '../../context/staged-files'\nimport type { Plugin } from '../../plugin/plugin'\n\nimport { generateModel } from './gen-model'\nimport { computeInputFilesHash } from './hash-files'\nimport type { ModelSpec } from '../../_shared/model-spec'\n\nexport interface BuildOnceOptions {\n forceModelGen?: boolean\n abortSignal?: AbortSignal\n}\n\n/**\n * Perform a single build.\n *\n * This will return an error if a build failure occurred, or if a plugin encounters\n * an error. Otherwise, it will return true if the build and all plugins\n * succeeded, or false if a plugin wants to report a \"soft\" failure (for example,\n * if the model check plugin reports failing checks).\n *\n * @param config The resolved build configuration.\n * @param plugins The configured plugins.\n * @param options Options specific to the build process.\n * @return An `ok` result with true if the build and all plugins succeeded, or false if\n * one or more plugins failed; otherwise, an `err` result if there was a hard error.\n */\nexport async function buildOnce(\n config: ResolvedConfig,\n userConfig: UserConfig,\n plugins: Plugin[],\n options: BuildOnceOptions\n): Promise<Result<boolean, Error>> {\n // Create the build context\n const stagedFiles = new StagedFiles(config.prepDir)\n const context = new BuildContext(config, stagedFiles, options.abortSignal)\n\n // Get the model spec from the config\n let modelSpec: ModelSpec\n try {\n modelSpec = await userConfig.modelSpec(context)\n if (modelSpec === undefined) {\n return err(new Error('The model spec must be defined'))\n }\n } catch (e) {\n return err(e)\n }\n\n // Run plugins that implement `preGenerate`\n for (const plugin of plugins) {\n if (plugin.preGenerate) {\n plugin.preGenerate(context, modelSpec)\n }\n }\n\n // Write the spec file\n const specJson = {\n inputVarNames: modelSpec.inputs.map(input => input.varName),\n outputVarNames: modelSpec.outputs.map(output => output.varName),\n externalDatfiles: modelSpec.datFiles,\n ...modelSpec.options\n }\n const specPath = joinPath(config.prepDir, 'spec.json')\n await writeFile(specPath, JSON.stringify(specJson, null, 2))\n\n // Read the hash from the last successful model build, if available\n const modelHashPath = joinPath(config.prepDir, 'model-hash.txt')\n let previousModelHash: string\n if (existsSync(modelHashPath)) {\n previousModelHash = readFileSync(modelHashPath, 'utf8')\n } else {\n previousModelHash = 'NONE'\n }\n\n // The code gen and Wasm build steps are time consuming, so we avoid rebuilding\n // it if the build input files are unchanged since the last successful build\n const inputFilesHash = await computeInputFilesHash(config)\n let needModelGen: boolean\n if (options.forceModelGen === true) {\n needModelGen = true\n } else {\n const hashMismatch = inputFilesHash !== previousModelHash\n needModelGen = hashMismatch\n }\n\n let succeeded = true\n try {\n if (needModelGen) {\n // Generate the model\n await generateModel(context, plugins)\n\n // Save the hash of the input files, which can be used to determine if\n // we need to rebuild the model the next time\n writeFileSync(modelHashPath, inputFilesHash)\n } else {\n // Skip code generation\n log('info', 'Skipping model code generation; already up-to-date')\n }\n\n // Run plugins that implement `postGenerate`\n // TODO: For now we run all plugins even if one or more of them returns\n // false, which allows (in theory) for there to be multiple plugins that\n // run tests or checks as a post-build step. We should eventually make\n // this configurable so that a plugin can halt the build if it fails.\n // (Technically this is already possible if the plugin throws an error\n // instead of returning false, but maybe it should be more configurable.)\n for (const plugin of plugins) {\n if (plugin.postGenerate) {\n const pluginSucceeded = await plugin.postGenerate(context, modelSpec)\n if (!pluginSucceeded) {\n succeeded = false\n }\n }\n }\n\n // Copy staged files to their destination; this will only copy the staged\n // files if they are different than the existing destination files. We\n // copy the files in a batch like this so that hot module reload is only\n // triggered once at the end of the whole build process.\n stagedFiles.copyChangedFiles()\n\n // Run plugins that implement `postBuild` (this is specified to run after\n // the \"copy staged files\" step)\n for (const plugin of plugins) {\n if (plugin.postBuild) {\n const pluginSucceeded = await plugin.postBuild(context, modelSpec)\n if (!pluginSucceeded) {\n succeeded = false\n }\n }\n }\n\n if (config.mode === 'development') {\n // Hide the \"rebuilding\" message in the dev overlay in the app if the\n // build succeeded; otherwise keep the error message visible\n log('info', 'Waiting for changes...\\n')\n clearOverlay()\n }\n } catch (e) {\n // When a build is aborted, the error will have \"ABORT\" as the message,\n // in which case we can swallow the error; for actual errors, rethrow\n if (e.message !== 'ABORT') {\n // Clear the hash so that the model is rebuilt next time\n writeFileSync(modelHashPath, '')\n\n // Return an error result\n return err(e)\n }\n }\n\n return ok(succeeded)\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport type { ChildProcess } from 'child_process'\n\nimport { spawn } from 'cross-spawn'\n\nimport { log } from '../_shared/log'\n\n/**\n * @hidden This isn't ready to be included in the public API just yet.\n */\nexport interface ProcessOptions {\n logOutput?: boolean\n ignoredMessageFilter?: string\n captureOutput?: boolean\n ignoreError?: boolean\n}\n\n/**\n * @hidden This isn't ready to be included in the public API just yet.\n */\nexport interface ProcessOutput {\n exitCode: number\n stdoutMessages: string[]\n stderrMessages: string[]\n}\n\n/**\n * Spawn a child process that runs the given command.\n *\n * @param cwd The directory in which the command will be executed.\n * @param command The command to execute.\n * @param args The arguments to pass to the command.\n * @param abortSignal The signal used to abort the process.\n * @param opts Additional options to configure the process.\n * @returns The output of the process.\n */\nexport function spawnChild(\n cwd: string,\n command: string,\n args: string[],\n abortSignal?: AbortSignal,\n opts?: ProcessOptions\n): Promise<ProcessOutput> {\n return new Promise((resolve, reject) => {\n if (abortSignal?.aborted) {\n reject(new Error('ABORT'))\n return\n }\n\n let childProc: ChildProcess\n\n const localLog = (s: string, err = false) => {\n // Don't log anything after the process has been killed\n if (childProc === undefined) {\n return\n }\n log(err ? 'error' : 'info', s)\n }\n\n const abortHandler = () => {\n if (childProc) {\n log('info', 'Killing existing build process...')\n childProc.kill('SIGKILL')\n childProc = undefined\n }\n reject(new Error('ABORT'))\n }\n\n // Kill the process if abort is requested\n abortSignal?.addEventListener('abort', abortHandler, { once: true })\n\n // Prepare for capturing output, if requested\n const stdoutMessages: string[] = []\n const stderrMessages: string[] = []\n const logMessage = (msg: string, err: boolean) => {\n let includeMessage = true\n if (opts?.ignoredMessageFilter && msg.trim().startsWith(opts.ignoredMessageFilter)) {\n includeMessage = false\n }\n if (includeMessage) {\n const lines = msg.trim().split('\\n')\n for (const line of lines) {\n localLog(` ${line}`, err)\n }\n }\n }\n\n // Spawn the (asynchronous) child process. Note that we are using `spawn`\n // from the `cross-spawn` package as an alternative to the built-in\n // `child_process` module, which doesn't handle spaces in command path\n // on Windows.\n childProc = spawn(command, args, {\n cwd\n })\n\n childProc.stdout.on('data', (data: Buffer) => {\n const msg = data.toString()\n if (opts?.captureOutput === true) {\n stdoutMessages.push(msg)\n }\n if (opts?.logOutput !== false) {\n logMessage(msg, false)\n }\n })\n childProc.stderr.on('data', (data: Buffer) => {\n const msg = data.toString()\n if (opts?.captureOutput === true) {\n stderrMessages.push(msg)\n }\n if (opts?.logOutput !== false) {\n logMessage(msg, true)\n }\n })\n childProc.on('error', err => {\n localLog(`Process error: ${err}`, true)\n })\n childProc.on('close', (code, signal) => {\n // Stop listening for abort events\n abortSignal?.removeEventListener('abort', abortHandler)\n childProc = undefined\n\n if (signal) {\n // The process was killed by a signal, so we don't need to print anything\n return\n }\n\n const processOutput: ProcessOutput = {\n exitCode: code,\n stdoutMessages,\n stderrMessages\n }\n\n if (code === 0) {\n // The process exited cleanly; resolve the promise\n resolve(processOutput)\n } else if (!signal) {\n // The process failed\n if (opts?.ignoreError === true) {\n // Resolve the promise (but with a non-zero exit code)\n resolve(processOutput)\n } else {\n // Reject the promise\n reject(new Error(`Child process failed (code=${code})`))\n }\n }\n })\n })\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport type { LogLevel } from '../_shared/log'\nimport { log } from '../_shared/log'\n\nimport type { ResolvedConfig } from '../_shared/resolved-config'\n\nimport type { ProcessOptions, ProcessOutput } from './spawn-child'\nimport { spawnChild } from './spawn-child'\nimport type { StagedFiles } from './staged-files'\n\n/**\n * Provides access to common functionality that is needed during the build process.\n * This is passed to most plugin functions.\n */\nexport class BuildContext {\n /**\n * @param config The resolved configuration.\n * @hidden\n */\n constructor(\n public readonly config: ResolvedConfig,\n private readonly stagedFiles: StagedFiles,\n private readonly abortSignal: AbortSignal | undefined\n ) {}\n\n /**\n * Log a message to the console and/or the in-browser overlay panel.\n *\n * @param level The log level (verbose, info, error).\n * @param msg The message.\n */\n log(level: LogLevel, msg: string): void {\n log(level, msg)\n }\n\n /**\n * Prepare for writing a file to the staged directory.\n *\n * This will add the path to the array of tracked files and will create the\n * staged directory if needed.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file will be written (this must be a relative path).\n * @param srcFile The name of the file as written to the `staged` directory.\n * @param dstDir The absolute path to the destination directory where the staged\n * file will be copied when the build has completed.\n * @param dstFile The name of the file as written to the destination directory.\n * @return The absolute path to the staged file.\n */\n prepareStagedFile(srcDir: string, srcFile: string, dstDir: string, dstFile: string): string {\n return this.stagedFiles.prepareStagedFile(srcDir, srcFile, dstDir, dstFile)\n }\n\n /**\n * Write a file to the staged directory.\n *\n * This file will be copied (along with other staged files) into the destination\n * directory only after the build process has completed. Copying all staged files\n * at once helps improve the local development experience by making it so that\n * live reloading tools only need to refresh once instead of every time a build\n * file is written.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file will be written (this must be a relative path).\n * @param dstDir The absolute path to the destination directory where the staged\n * file will be copied when the build has completed.\n * @param filename The name of the file.\n * @param content The file content.\n */\n writeStagedFile(srcDir: string, dstDir: string, filename: string, content: string): void {\n this.stagedFiles.writeStagedFile(srcDir, dstDir, filename, content)\n }\n\n /**\n * Spawn a child process that runs the given command.\n *\n * @param cwd The directory in which the command will be executed.\n * @param command The command to execute.\n * @param args The arguments to pass to the command.\n * @param opts Additional options to configure the process.\n * @returns The output of the process.\n */\n spawnChild(cwd: string, command: string, args: string[], opts?: ProcessOptions): Promise<ProcessOutput> {\n return spawnChild(cwd, command, args, this.abortSignal, opts)\n }\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'fs'\nimport { join as joinPath } from 'path'\n\nimport { log } from '../_shared/log'\n\ninterface StagedFile {\n srcDir: string\n srcFile: string\n dstDir: string\n dstFile: string\n}\n\nexport class StagedFiles {\n private readonly baseStagedDir: string\n private readonly stagedFiles: StagedFile[] = []\n\n constructor(prepDir: string) {\n this.baseStagedDir = joinPath(prepDir, 'staged')\n }\n\n /**\n * Prepare for writing a file to the staged directory.\n *\n * This will add the path to the array of tracked files and will create the\n * staged directory if needed.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file will be written (this must be a relative path).\n * @param srcFile The name of the file as written to the `staged` directory.\n * @param dstDir The absolute path to the destination directory where the staged\n * file will be copied when the build has completed.\n * @param dstFile The name of the file as written to the destination directory.\n * @return The absolute path to the staged file.\n */\n prepareStagedFile(srcDir: string, srcFile: string, dstDir: string, dstFile: string): string {\n // Add an entry to the array of staged files (only if an entry does not already\n // exist) so that we can copy the file to the destination directory when the build\n // process has completed\n const stagedFile = {\n srcDir,\n srcFile,\n dstDir,\n dstFile\n }\n // TODO: We allow there to be more than one entry for each source path to\n // support the case where a single source file gets copied to multiple\n // destination paths. But we should throw an error if the same destination\n // path is configured for different source paths.\n if (this.stagedFiles.indexOf(stagedFile) < 0) {\n this.stagedFiles.push(stagedFile)\n }\n\n // Create the directory underneath the staged directory if needed\n const stagedDir = joinPath(this.baseStagedDir, srcDir)\n if (!existsSync(stagedDir)) {\n mkdirSync(stagedDir, { recursive: true })\n }\n\n return joinPath(stagedDir, srcFile)\n }\n\n /**\n * Write a file to the staged directory.\n *\n * This file will be copied (along with other staged files) into the destination\n * directory only after the build process has completed. Copying all staged files\n * at once helps improve the local development experience by making it so that\n * live reloading tools only need to refresh once instead of every time a build\n * file is written.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file will be written (this must be a relative path).\n * @param dstDir The absolute path to the destination directory where the staged\n * file will be copied when the build has completed.\n * @param filename The name of the file.\n * @param content The file content.\n */\n writeStagedFile(srcDir: string, dstDir: string, filename: string, content: string): void {\n // Add an entry to track the file and create the staged directory if needed\n const stagedFilePath = this.prepareStagedFile(srcDir, filename, dstDir, filename)\n\n // Write the file to the staged directory\n writeFileSync(stagedFilePath, content)\n }\n\n /**\n * Return the absolute path to the staged file for the given source directory and file name.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file would be written initially (this must be a relative path).\n * @param srcFile The name of the file.\n */\n getStagedFilePath(srcDir: string, srcFile: string): string {\n return joinPath(this.baseStagedDir, srcDir, srcFile)\n }\n\n /**\n * Return true if the staged file exists for the given source directory and file name.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file would be written initially (this must be a relative path).\n * @param srcFile The name of the file.\n */\n stagedFileExists(srcDir: string, srcFile: string): boolean {\n const fullSrcPath = this.getStagedFilePath(srcDir, srcFile)\n return existsSync(fullSrcPath)\n }\n\n /**\n * Return true if the destination file exists for the given source directory and file name.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file would be written initially (this must be a relative path).\n * @param srcFile The name of the file.\n */\n destinationFileExists(srcDir: string, srcFile: string): boolean {\n const f = this.stagedFiles.find(f => f.srcDir === srcDir && f.srcFile === srcFile)\n if (f === undefined) {\n return false\n }\n const fullDstPath = joinPath(f.dstDir, f.dstFile)\n return existsSync(fullDstPath)\n }\n\n /**\n * Copy staged files to their destination; this will only copy the staged\n * files if they are different than the existing destination files. We\n * copy the files in a batch like this so that hot module reload is only\n * triggered once at the end of the whole build process.\n */\n copyChangedFiles(): void {\n log('info', 'Copying changed files into place...')\n\n for (const f of this.stagedFiles) {\n this.copyStagedFile(f)\n }\n\n log('info', 'Done copying files')\n }\n\n /**\n * Copy a file from the `staged` directory to its destination. If the file already\n * exists in the destination directory and has the same contents as the source file,\n * the file will not be copied and this function will return false.\n *\n * @param f The staged file entry.\n */\n private copyStagedFile(f: StagedFile): boolean {\n // Create the destination directory, if needed\n if (!existsSync(f.dstDir)) {\n mkdirSync(f.dstDir, { recursive: true })\n }\n\n // If the destination file already exists and has the same contents as the source\n // file, we can skip copying it\n const fullSrcPath = this.getStagedFilePath(f.srcDir, f.srcFile)\n const fullDstPath = joinPath(f.dstDir, f.dstFile)\n const needsCopy = filesDiffer(fullSrcPath, fullDstPath)\n if (needsCopy) {\n log('verbose', ` Copying ${f.srcFile} to ${fullDstPath}`)\n copyFileSync(fullSrcPath, fullDstPath)\n }\n return needsCopy\n }\n}\n\n/**\n * Return true if both files exist at the given paths and have the same contents, false otherwise.\n */\nfunction filesDiffer(aPath: string, bPath: string): boolean {\n if (existsSync(aPath) && existsSync(bPath)) {\n // The files exist; see if they are different\n const aSize = statSync(aPath).size\n const bSize = statSync(bPath).size\n if (aSize !== bSize) {\n // The sizes are different, so the contents must be different\n return true\n } else {\n // The sizes are the same, so check the contents\n const aBuf = readFileSync(aPath)\n const bBuf = readFileSync(bPath)\n return !aBuf.equals(bBuf)\n }\n } else {\n // One or both files do not exist\n return true\n }\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { copyFile, readdir, readFile, writeFile } from 'fs/promises'\nimport { join as joinPath } from 'path'\n\nimport { log } from '../../_shared/log'\n\nimport type { BuildContext } from '../../context/context'\n\nimport type { Plugin } from '../../plugin/plugin'\n\n/**\n * Generate the model. This will run the core SDEverywhere code generation steps\n * and will also invoke the following plugin functions:\n * - `preProcessMdl`\n * - `postProcessMdl`\n * - `preGenerateC`\n * - `postGenerateC`\n */\nexport async function generateModel(context: BuildContext, plugins: Plugin[]): Promise<void> {\n log('info', 'Generating model...')\n\n const t0 = performance.now()\n\n // Use the defined prep directory\n const config = context.config\n const prepDir = config.prepDir\n\n // TODO: For now we assume the path is to the `main.js` file in the cli package;\n // this seems to work on both Unix and Windows, but we may need to revisit this\n // as part of removing the `sdeCmdPath` config hack\n const sdeCmdPath = config.sdeCmdPath\n\n // Process the mdl file(s)\n for (const plugin of plugins) {\n if (plugin.preProcessMdl) {\n await plugin.preProcessMdl(context)\n }\n }\n if (config.modelFiles.length === 0) {\n // Require at least one input file\n throw new Error('No model input files specified')\n } else if (config.modelFiles.length === 1) {\n // Preprocess the single mdl file\n await preprocessMdl(context, sdeCmdPath, prepDir, config.modelFiles[0])\n } else {\n // Flatten and preprocess the multiple mdl files into a single mdl file\n await flattenMdls(context, sdeCmdPath, prepDir, config.modelFiles)\n }\n for (const plugin of plugins) {\n if (plugin.postProcessMdl) {\n const mdlPath = joinPath(prepDir, 'processed.mdl')\n let mdlContent = await readFile(mdlPath, 'utf8')\n mdlContent = await plugin.postProcessMdl(context, mdlContent)\n await writeFile(mdlPath, mdlContent)\n }\n }\n\n // Generate the C file\n for (const plugin of plugins) {\n if (plugin.preGenerateC) {\n await plugin.preGenerateC(context)\n }\n }\n await generateC(context, config.sdeDir, sdeCmdPath, prepDir)\n for (const plugin of plugins) {\n if (plugin.postGenerateC) {\n const cPath = joinPath(prepDir, 'build', 'processed.c')\n let cContent = await readFile(cPath, 'utf8')\n cContent = await plugin.postGenerateC(context, cContent)\n await writeFile(cPath, cContent)\n }\n }\n\n const t1 = performance.now()\n const elapsed = ((t1 - t0) / 1000).toFixed(1)\n log('info', `Done generating model (${elapsed}s)`)\n}\n\n/**\n * Preprocess a single mdl file and copy the resulting `processed.mdl` to the prep directory.\n */\nasync function preprocessMdl(\n context: BuildContext,\n sdeCmdPath: string,\n prepDir: string,\n modelFile: string\n): Promise<void> {\n log('verbose', ' Preprocessing mdl file')\n\n // Copy the source file to the prep directory to make the next steps easier\n await copyFile(modelFile, joinPath(prepDir, 'processed.mdl'))\n\n // Use SDE to preprocess the model to strip anything that's not needed to build it\n const command = sdeCmdPath\n const args = ['generate', '--preprocess', 'processed.mdl']\n await context.spawnChild(prepDir, command, args)\n\n // Copy the processed file back to the prep directory\n await copyFile(joinPath(prepDir, 'build', 'processed.mdl'), joinPath(prepDir, 'processed.mdl'))\n}\n\n/**\n * Flatten multiple mdl files and copy the resulting `processed.mdl` to the prep directory.\n */\nasync function flattenMdls(\n context: BuildContext,\n sdeCmdPath: string,\n prepDir: string,\n modelFiles: string[]\n): Promise<void> {\n log('verbose', ' Flattening and preprocessing mdl files')\n\n // Use SDE to flatten the parent model and submodels into a single mdl file,\n // then preprocess to strip anything that's not needed to build the model\n const command = sdeCmdPath\n const args: string[] = []\n args.push('flatten')\n args.push('processed.mdl')\n args.push('--inputs')\n for (const path of modelFiles) {\n args.push(path)\n }\n\n // Disable logging by default; this will suppress flatten warnings, which are\n // sometimes unavoidable and not helpful\n const output = await context.spawnChild(prepDir, command, args, {\n logOutput: false,\n captureOutput: true,\n ignoreError: true\n })\n\n // Check for flattening errors\n let flattenErrors = false\n for (const msg of output.stderrMessages) {\n if (msg.includes('ERROR')) {\n flattenErrors = true\n break\n }\n }\n if (flattenErrors) {\n log('error', 'There were errors reported when flattening the model:')\n for (const msg of output.stderrMessages) {\n const lines = msg.split('\\n')\n for (const line of lines) {\n log('error', ` ${line}`)\n }\n }\n throw new Error(`Flatten command failed (code=${output.exitCode})`)\n } else if (output.exitCode !== 0) {\n throw new Error(`Flatten command failed (code=${output.exitCode})`)\n }\n\n // Copy the processed file back to the prep directory\n await copyFile(joinPath(prepDir, 'build', 'processed.mdl'), joinPath(prepDir, 'processed.mdl'))\n}\n\n/**\n * Generate a C file from the `processed.mdl` file.\n */\nasync function generateC(context: BuildContext, sdeDir: string, sdeCmdPath: string, prepDir: string): Promise<void> {\n log('verbose', ' Generating C code')\n\n // Use SDE to generate a C version of the model\n const command = sdeCmdPath\n const args = ['generate', '--genc', '--spec', 'spec.json', 'processed']\n await context.spawnChild(prepDir, command, args, {\n // By default, ignore lines that start with \"WARNING: Data for\" since these are often harmless\n // TODO: Don't filter by default, but make it configurable\n // ignoredMessageFilter: 'WARNING: Data for'\n })\n\n // Copy SDE's supporting C files into the build directory\n const buildDir = joinPath(prepDir, 'build')\n const sdeCDir = joinPath(sdeDir, 'src', 'c')\n const files = await readdir(sdeCDir)\n const copyOps = []\n for (const file of files) {\n if (file.endsWith('.c') || file.endsWith('.h')) {\n copyOps.push(copyFile(joinPath(sdeCDir, file), joinPath(buildDir, file)))\n }\n }\n await Promise.all(copyOps)\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { join as joinPath } from 'path'\nimport { hashElement } from 'folder-hash'\nimport glob from 'tiny-glob'\n\nimport type { ResolvedConfig } from '../../_shared/resolved-config'\n\n/**\n * Asynchronously compute the hash of the files that are inputs to the model\n * build process.\n */\nexport async function computeInputFilesHash(config: ResolvedConfig): Promise<string> {\n const inputFiles: string[] = []\n\n // Always include the `spec.json` file, since that is a primary input\n // to the model build process\n const specFile = joinPath(config.prepDir, 'spec.json')\n inputFiles.push(specFile)\n\n if (config.modelInputPaths && config.modelInputPaths.length > 0) {\n // Include the files that match the glob patterns in the config file.\n // Note that the folder-hash package supports glob patterns, but its\n // configuration is complicated, so it is easier if we resolve the\n // glob patterns here and pass each individual file to the\n // `hashElement` function.\n for (const globPath of config.modelInputPaths) {\n const paths = await glob(globPath, {\n cwd: config.rootDir,\n absolute: true,\n filesOnly: true\n })\n inputFiles.push(...paths)\n }\n } else {\n // Only use the mdl files to compute the hash\n inputFiles.push(...config.modelFiles)\n }\n\n // Compute the hash of each input file and concatenate into a single string\n let hash = ''\n for (const inputFile of inputFiles) {\n const result = await hashElement(inputFile)\n hash += result.hash\n }\n\n return hash\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { basename } from 'path'\n\nimport chokidar from 'chokidar'\n\nimport { clearOverlay, log, logError } from '../../_shared/log'\nimport type { ResolvedConfig } from '../../_shared/resolved-config'\n\nimport type { UserConfig } from '../../config/user-config'\nimport type { Plugin } from '../../plugin/plugin'\n\nimport type { BuildOnceOptions } from './build-once'\nimport { buildOnce } from './build-once'\n\nclass BuildState {\n readonly abortController = new AbortController()\n}\n\nexport function watch(config: ResolvedConfig, userConfig: UserConfig, plugins: Plugin[]): void {\n // Add a small delay so that if multiple files are changed at once (as is\n // often the case when switching branches), we batch them up and start the\n // the build after things settle down\n const delay = 150\n const changedPaths: Set<string> = new Set()\n\n // Keep track of the current build so that we only have one active at a time\n let currentBuildState: BuildState\n\n function performBuild() {\n clearOverlay()\n\n // Log the input files that have changed\n for (const path of changedPaths) {\n log('info', `Input file ${basename(path)} has been changed`)\n }\n\n // Clear the set of pending files\n changedPaths.clear()\n\n // Keep track of builds; if one is already in progress, abort it\n // before starting another one\n if (currentBuildState) {\n currentBuildState.abortController.abort()\n // TODO: Prevent aborted build from logging?\n currentBuildState = undefined\n }\n\n // Generate files and build the model\n currentBuildState = new BuildState()\n const buildOptions: BuildOnceOptions = {\n abortSignal: currentBuildState.abortController.signal\n }\n buildOnce(config, userConfig, plugins, buildOptions)\n .catch(e => {\n logError(e)\n // log('info', 'Waiting for changes...')\n })\n .finally(() => {\n currentBuildState = undefined\n })\n }\n\n function scheduleBuild(changedPath: string) {\n // Only schedule the build if the set is currently empty\n const schedule = changedPaths.size === 0\n\n // Add the path to the set of changed files\n changedPaths.add(changedPath)\n\n if (schedule) {\n // Schedule the build to start after a delay\n setTimeout(() => {\n performBuild()\n }, delay)\n }\n }\n\n let watchPaths: string[]\n if (config.watchPaths && config.watchPaths.length > 0) {\n // Watch the configured files\n watchPaths = config.watchPaths\n } else {\n // Only watch the mdl files\n watchPaths = config.modelFiles\n }\n\n // Watch the config and model files; if changes are detected, generate the specs\n // and rebuild the model if needed\n const watcher = chokidar.watch(watchPaths, {\n // Watch paths are resolved relative to the project root directory\n cwd: config.rootDir,\n // XXX: Include a delay, otherwise on macOS we sometimes get multiple\n // change events when the csv file is saved just once\n awaitWriteFinish: {\n stabilityThreshold: 200\n }\n })\n watcher.on('change', path => {\n scheduleBuild(path)\n })\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/build/build.ts","../src/config/config-loader.ts","../src/_shared/log.ts","../src/build/impl/build-once.ts","../src/context/spawn-child.ts","../src/context/context.ts","../src/context/staged-files.ts","../src/build/impl/gen-model.ts","../src/build/impl/hash-files.ts","../src/build/impl/watch.ts"],"sourcesContent":["// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\n//\n// Note that exports are ordered here according to the dependency chain,\n// i.e., lower items depend on the ones above.\n//\n\nexport type { LogLevel } from './_shared/log'\nexport type { BuildMode } from './_shared/mode'\nexport type { InputSpec, ModelSpec, OutputSpec } from './_shared/model-spec'\nexport type { ResolvedConfig } from './_shared/resolved-config'\n\nexport type { BuildContext } from './context/context'\n\nexport type { Plugin } from './plugin/plugin'\n\nexport type { UserConfig } from './config/user-config'\n\nexport type { BuildOptions, BuildResult } from './build/build'\nexport { build } from './build/build'\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { join as joinPath } from 'path'\n\nimport type { Result } from 'neverthrow'\nimport { err, ok } from 'neverthrow'\n\nimport type { BuildMode } from '../_shared/mode'\n\nimport { loadConfig } from '../config/config-loader'\nimport type { UserConfig } from '../config/user-config'\nimport type { LogLevel } from '../_shared/log'\nimport { setOverlayFile, setActiveLevels } from '../_shared/log'\nimport { buildOnce } from './impl/build-once'\nimport { watch } from './impl/watch'\n\nexport interface BuildOptions {\n /** The path to an `sde.config.js` file, or a `UserConfig` object. */\n config?: string | UserConfig\n\n /**\n * The log levels to include. If undefined, the default 'info' and 'error' levels\n * will be active.\n */\n logLevels?: LogLevel[]\n\n /**\n * The path to the `@sdeverywhere/cli` package. This is currently only used to get\n * access to the files in the `src/c` directory.\n * @hidden This should be removed once we have tighter integration with the `cli` package.\n */\n sdeDir: string\n\n /**\n * The path to the `sde` command.\n * @hidden This should be removed once we have tighter integration with the `cli` package.\n */\n sdeCmdPath: string\n}\n\nexport interface BuildResult {\n /**\n * The exit code that should be set by the process. This will be undefined\n * if `mode` is 'development', indicating that the process should be kept alive.\n */\n exitCode?: number\n}\n\n/**\n * Initiate the build process, which can either be a single build if `mode` is\n * 'production', or a live development environment if `mode` is 'development'.\n *\n * @param mode The build mode.\n * @param options The build options.\n * @return An `ok` result if the build completed, otherwise an `err` result.\n */\nexport async function build(mode: BuildMode, options: BuildOptions): Promise<Result<BuildResult, Error>> {\n // Load the config\n const configResult = await loadConfig(mode, options.config, options.sdeDir, options.sdeCmdPath)\n if (configResult.isErr()) {\n return err(configResult.error)\n }\n const { userConfig, resolvedConfig } = configResult.value\n\n // Configure logging level\n if (options.logLevels !== undefined) {\n setActiveLevels(options.logLevels)\n }\n\n // Configure the overlay `messages.html` file, which is written under the\n // `sde-prep` directory. For production builds, this file will remain empty.\n const messagesPath = joinPath(resolvedConfig.prepDir, 'messages.html')\n const overlayEnabled = mode === 'development'\n setOverlayFile(messagesPath, overlayEnabled)\n\n // Initialize plugins\n const plugins = userConfig.plugins || []\n for (const plugin of plugins) {\n if (plugin.init) {\n await plugin.init(resolvedConfig)\n }\n }\n\n try {\n const plugins = userConfig.plugins || []\n\n if (mode === 'development') {\n // Enable dev mode (which will rebuild when watched files are changed).\n // First run an initial build so that we have a baseline, and then\n // once that is complete, enable file watchers.\n const buildResult = await buildOnce(resolvedConfig, userConfig, plugins, {})\n // TODO: We should trap errors here and keep the dev process\n // running if the initial build fails\n if (buildResult.isErr()) {\n return err(buildResult.error)\n }\n\n // Allow plugins to set up watchers after the initial build completes\n for (const plugin of plugins) {\n if (plugin.watch) {\n await plugin.watch(resolvedConfig)\n }\n }\n\n // Watch for changes to the source/model/test files\n watch(resolvedConfig, userConfig, plugins)\n\n // Return a build result with undefined exit code, indicating that the\n // process should be kept alive\n return ok({})\n } else {\n // Run a single build\n const buildResult = await buildOnce(resolvedConfig, userConfig, plugins, {})\n if (buildResult.isErr()) {\n return err(buildResult.error)\n }\n\n // Configure the exit code depending on whether any plugins failed.\n // Currently we use the following exit code values:\n // 0 == build succeeded, AND all plugins succeeded\n // 1 == build failed (or a plugin reported a \"hard\" error)\n // 2 == build succeeded, BUT one or more plugins failed\n const allPluginsSucceeded = buildResult.value\n const exitCode = allPluginsSucceeded ? 0 : 2\n return ok({ exitCode })\n }\n } catch (e) {\n return err(e)\n }\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { existsSync, lstatSync, mkdirSync } from 'fs'\nimport { dirname, join as joinPath, relative, resolve as resolvePath } from 'path'\nimport { fileURLToPath } from 'url'\n\nimport type { Result } from 'neverthrow'\nimport { err, ok } from 'neverthrow'\n\nimport type { BuildMode } from '../_shared/mode'\nimport type { ResolvedConfig } from '../_shared/resolved-config'\n\nimport type { UserConfig } from './user-config'\n\nexport interface ConfigResult {\n userConfig: UserConfig\n resolvedConfig: ResolvedConfig\n}\n\n/**\n * Load a user-defined config file or a given `UserConfig` object. This validates\n * all paths and if successful, this returns a `ResolvedConfig`, otherwise returns\n * an error result.\n *\n * @param mode The build mode.\n * @param config The path to a config file, or a `UserConfig` object; if undefined,\n * this will look for a `sde.config.js` file in the current directory.\n * @param sdeDir Temporary (the path to the `@sdeverywhere/cli` package).\n * @param sdeCmdPath Temporary (the path to the `sde` command).\n * @return An `ok` result with the `ResolvedConfig`, otherwise an `err` result.\n */\nexport async function loadConfig(\n mode: BuildMode,\n config: string | UserConfig | undefined,\n sdeDir: string,\n sdeCmdPath: string\n): Promise<Result<ConfigResult, Error>> {\n let userConfig: UserConfig\n if (typeof config === 'object') {\n // Use the given `UserConfig` object\n userConfig = config\n } else {\n // Load the project-specific config. If no `--config` arg was specified\n // on the command line, look for a `sde.config.js` file in the current\n // directory, and failing that, use a default config.\n // TODO: Create a default config if no file found; for now, we just fail\n let configPath: string\n if (typeof config === 'string') {\n configPath = config\n } else {\n configPath = joinPath(process.cwd(), 'sde.config.js')\n }\n try {\n if (!existsSync(configPath)) {\n return err(new Error(`Cannot find config file '${configPath}'`))\n }\n const configRelPath = relativeToSourcePath(configPath)\n const configModule = await import(configRelPath)\n userConfig = await configModule.config()\n } catch (e) {\n return err(new Error(`Failed to load config file '${configPath}': ${e.message}`))\n }\n }\n\n // Resolve the config\n try {\n const resolvedConfig = resolveUserConfig(userConfig, mode, sdeDir, sdeCmdPath)\n return ok({\n userConfig,\n resolvedConfig\n })\n } catch (e) {\n return err(e)\n }\n}\n\n/**\n * Resolve the given user configuration by resolving all paths. This will create\n * the prep directory if it does not already exist. This will throw an error if\n * any other paths are invalid or do not exist.\n *\n * @param userConfig The user-defined configuration.\n * @param mode The active build mode.\n * @param sdeDir Temporary (the path to the `@sdeverywhere/cli` package).\n * @param sdeCmdPath Temporary (the path to the `sde` command).\n * @return The resolved configuration.\n */\nfunction resolveUserConfig(\n userConfig: UserConfig,\n mode: BuildMode,\n sdeDir: string,\n sdeCmdPath: string\n): ResolvedConfig {\n function expectDirectory(propName: string, path: string): void {\n if (!existsSync(path)) {\n throw new Error(`The configured ${propName} (${path}) does not exist`)\n } else if (!lstatSync(path).isDirectory()) {\n throw new Error(`The configured ${propName} (${path}) is not a directory`)\n }\n }\n\n // function expectFile(propName: string, path: string): void {\n // if (!existsSync(path)) {\n // throw new Error(`The configured ${propName} (${path}) does not exist`)\n // // TODO: Don't include the \"is file\" check for now; need to update this to\n // // handle symlinks\n // // } else if (!lstatSync(path).isFile()) {\n // // throw new Error(`The configured ${propName} (${path}) is not a file`)\n // }\n // }\n\n // Validate the root directory\n let rootDir: string\n if (userConfig.rootDir) {\n // Verify that the configured root directory exists\n rootDir = resolvePath(userConfig.rootDir)\n expectDirectory('rootDir', rootDir)\n } else {\n // Use the current working directory as the project root\n rootDir = process.cwd()\n }\n\n // Validate the prep directory\n let prepDir: string\n if (userConfig.prepDir) {\n // Resolve the configured prep directory\n prepDir = resolvePath(userConfig.prepDir)\n } else {\n // Create an 'sde-prep' directory under the configured root directory\n prepDir = resolvePath(rootDir, 'sde-prep')\n }\n mkdirSync(prepDir, { recursive: true })\n\n // Validate the model files\n const userModelFiles = userConfig.modelFiles\n const modelFiles: string[] = []\n for (const userModelFile of userModelFiles) {\n const modelFile = resolvePath(userModelFile)\n if (!existsSync(modelFile)) {\n throw new Error(`The configured model file (${modelFile}) does not exist`)\n }\n modelFiles.push(modelFile)\n }\n\n // TODO: Validate the watch paths; these are allowed to be globs, so need to\n // figure out the best way to resolve them\n let modelInputPaths: string[]\n if (userConfig.modelInputPaths && userConfig.modelInputPaths.length > 0) {\n modelInputPaths = userConfig.modelInputPaths\n } else {\n modelInputPaths = modelFiles\n }\n let watchPaths: string[]\n if (userConfig.watchPaths && userConfig.watchPaths.length > 0) {\n watchPaths = userConfig.watchPaths\n } else {\n watchPaths = modelFiles\n }\n\n return {\n mode,\n rootDir,\n prepDir,\n modelFiles,\n modelInputPaths,\n watchPaths,\n sdeDir,\n sdeCmdPath\n }\n}\n\n/**\n * Return a Unix-style path (e.g. '../../foo.js') that is relative to the directory of\n * the current source file. This can be used to construct a path that is safe for\n * dynamic import on either Unix or Windows.\n *\n * @param filePath The path to make relative.\n */\nfunction relativeToSourcePath(filePath: string): string {\n const srcDir = dirname(fileURLToPath(import.meta.url))\n const relPath = relative(srcDir, filePath)\n return relPath.replaceAll('\\\\', '/')\n}\n","// Copyright (c) 2021-2022 Climate Interactive / New Venture Fund\n\nimport { writeFileSync } from 'fs'\nimport pico from 'picocolors'\n\nexport type LogLevel = 'error' | 'info' | 'verbose'\n\nconst activeLevels: Set<LogLevel> = new Set(['error', 'info'])\n\nlet overlayFile: string\nlet overlayEnabled = false\nlet overlayHtml = ''\n\n/**\n * Set the active logging levels. By default, only 'error' and 'info'\n * messages are emitted.\n *\n * @param logLevels The logging levels to include.\n */\nexport function setActiveLevels(logLevels: LogLevel[]): void {\n activeLevels.clear()\n for (const level of logLevels) {\n activeLevels.add(level)\n }\n}\n\n/**\n * Set the path to the `messages.html` file where overlay messages will be written.\n *\n * @param file The absolute path to the HTML file where messages will be written.\n * @param enabled Whether to write messages to the file; if false, the file will be\n * emptied and no further messages will be written.\n */\nexport function setOverlayFile(file: string, enabled: boolean): void {\n overlayFile = file\n overlayEnabled = enabled\n\n // Write an empty file by default; this will ensure that messages from a previous\n // build aren't included in the current build\n writeFileSync(overlayFile, '')\n}\n\n/**\n * Log a message to the console and/or overlay.\n *\n * @param level The logging level.\n * @param msg The message to emit.\n */\nexport function log(level: LogLevel, msg: string): void {\n if (activeLevels.has(level)) {\n if (level === 'error') {\n console.error(pico.red(msg))\n logToOverlay(msg)\n } else {\n console.log(msg)\n logToOverlay(msg)\n }\n }\n}\n\n/**\n * Log an error to the console and/or overlay.\n *\n * @param e The error to log.\n */\nexport function logError(e: Error): void {\n // Remove the first part of the stack trace (which contains the message)\n // so that we can control the formatting of the message separately, then\n // only include up to 3 lines of the stack to keep the log cleaner\n const stack = e.stack || ''\n const stackLines = stack.split('\\n').filter(s => s.match(/^\\s+at/))\n const trace = stackLines.slice(0, 3).join('\\n')\n\n // Log the error message followed by the stack trace\n console.error(pico.red(`\\nERROR: ${e.message}`))\n console.error(pico.dim(pico.red(`${trace}\\n`)))\n logToOverlay(`\\nERROR: ${e.message}`, true)\n logToOverlay(`${trace}\\n`, true)\n}\n\nfunction writeOverlayFiles(): void {\n writeFileSync(overlayFile, overlayHtml)\n}\n\nexport function clearOverlay(): void {\n if (!overlayEnabled) {\n return\n }\n\n overlayHtml = ''\n writeOverlayFiles()\n}\n\nconst indent = ' '.repeat(4)\n\nexport function logToOverlay(msg: string, error = false): void {\n if (!overlayEnabled) {\n return\n }\n\n if (error) {\n msg = `<span class=\"overlay-error\">${msg}</span>`\n }\n const msgHtml = msg.replace(/\\n/g, '\\n<br/>').replace(/\\s{2}/g, indent)\n if (overlayHtml) {\n overlayHtml += `<br/>${msgHtml}`\n } else {\n overlayHtml = `${msgHtml}`\n }\n writeOverlayFiles()\n}\n\nexport function clearConsole(): void {\n // TODO: This is disabled for now; maybe re-enable it under an optional flag\n // console.log('\\x1Bc')\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { existsSync, readFileSync, writeFileSync } from 'fs'\nimport { writeFile } from 'fs/promises'\nimport { join as joinPath } from 'path'\n\nimport type { Result } from 'neverthrow'\nimport { err, ok } from 'neverthrow'\n\nimport { clearOverlay, log } from '../../_shared/log'\nimport type { ResolvedConfig } from '../../_shared/resolved-config'\n\nimport type { UserConfig } from '../../config/user-config'\nimport { BuildContext } from '../../context/context'\nimport { StagedFiles } from '../../context/staged-files'\nimport type { Plugin } from '../../plugin/plugin'\n\nimport { generateModel } from './gen-model'\nimport { computeInputFilesHash } from './hash-files'\nimport type { ModelSpec } from '../../_shared/model-spec'\n\nexport interface BuildOnceOptions {\n forceModelGen?: boolean\n abortSignal?: AbortSignal\n}\n\n/**\n * Perform a single build.\n *\n * This will return an error if a build failure occurred, or if a plugin encounters\n * an error. Otherwise, it will return true if the build and all plugins\n * succeeded, or false if a plugin wants to report a \"soft\" failure (for example,\n * if the model check plugin reports failing checks).\n *\n * @param config The resolved build configuration.\n * @param plugins The configured plugins.\n * @param options Options specific to the build process.\n * @return An `ok` result with true if the build and all plugins succeeded, or false if\n * one or more plugins failed; otherwise, an `err` result if there was a hard error.\n */\nexport async function buildOnce(\n config: ResolvedConfig,\n userConfig: UserConfig,\n plugins: Plugin[],\n options: BuildOnceOptions\n): Promise<Result<boolean, Error>> {\n // Create the build context\n const stagedFiles = new StagedFiles(config.prepDir)\n const context = new BuildContext(config, stagedFiles, options.abortSignal)\n\n // Get the model spec from the config\n let modelSpec: ModelSpec\n try {\n modelSpec = await userConfig.modelSpec(context)\n if (modelSpec === undefined) {\n return err(new Error('The model spec must be defined'))\n }\n } catch (e) {\n return err(e)\n }\n\n // Run plugins that implement `preGenerate`\n for (const plugin of plugins) {\n if (plugin.preGenerate) {\n plugin.preGenerate(context, modelSpec)\n }\n }\n\n // Write the spec file\n const specJson = {\n inputVarNames: modelSpec.inputs.map(input => input.varName),\n outputVarNames: modelSpec.outputs.map(output => output.varName),\n externalDatfiles: modelSpec.datFiles,\n ...modelSpec.options\n }\n const specPath = joinPath(config.prepDir, 'spec.json')\n await writeFile(specPath, JSON.stringify(specJson, null, 2))\n\n // Read the hash from the last successful model build, if available\n const modelHashPath = joinPath(config.prepDir, 'model-hash.txt')\n let previousModelHash: string\n if (existsSync(modelHashPath)) {\n previousModelHash = readFileSync(modelHashPath, 'utf8')\n } else {\n previousModelHash = 'NONE'\n }\n\n // The code gen and Wasm build steps are time consuming, so we avoid rebuilding\n // it if the build input files are unchanged since the last successful build\n const inputFilesHash = await computeInputFilesHash(config)\n let needModelGen: boolean\n if (options.forceModelGen === true) {\n needModelGen = true\n } else {\n const hashMismatch = inputFilesHash !== previousModelHash\n needModelGen = hashMismatch\n }\n\n let succeeded = true\n try {\n if (needModelGen) {\n // Generate the model\n await generateModel(context, plugins)\n\n // Save the hash of the input files, which can be used to determine if\n // we need to rebuild the model the next time\n writeFileSync(modelHashPath, inputFilesHash)\n } else {\n // Skip code generation\n log('info', 'Skipping model code generation; already up-to-date')\n }\n\n // Run plugins that implement `postGenerate`\n // TODO: For now we run all plugins even if one or more of them returns\n // false, which allows (in theory) for there to be multiple plugins that\n // run tests or checks as a post-build step. We should eventually make\n // this configurable so that a plugin can halt the build if it fails.\n // (Technically this is already possible if the plugin throws an error\n // instead of returning false, but maybe it should be more configurable.)\n for (const plugin of plugins) {\n if (plugin.postGenerate) {\n const pluginSucceeded = await plugin.postGenerate(context, modelSpec)\n if (!pluginSucceeded) {\n succeeded = false\n }\n }\n }\n\n // Copy staged files to their destination; this will only copy the staged\n // files if they are different than the existing destination files. We\n // copy the files in a batch like this so that hot module reload is only\n // triggered once at the end of the whole build process.\n stagedFiles.copyChangedFiles()\n\n // Run plugins that implement `postBuild` (this is specified to run after\n // the \"copy staged files\" step)\n for (const plugin of plugins) {\n if (plugin.postBuild) {\n const pluginSucceeded = await plugin.postBuild(context, modelSpec)\n if (!pluginSucceeded) {\n succeeded = false\n }\n }\n }\n\n if (config.mode === 'development') {\n // Hide the \"rebuilding\" message in the dev overlay in the app if the\n // build succeeded; otherwise keep the error message visible\n log('info', 'Waiting for changes...\\n')\n clearOverlay()\n }\n } catch (e) {\n // When a build is aborted, the error will have \"ABORT\" as the message,\n // in which case we can swallow the error; for actual errors, rethrow\n if (e.message !== 'ABORT') {\n // Clear the hash so that the model is rebuilt next time\n writeFileSync(modelHashPath, '')\n\n // Return an error result\n return err(e)\n }\n }\n\n return ok(succeeded)\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport type { ChildProcess } from 'child_process'\n\nimport { spawn } from 'cross-spawn'\n\nimport { log } from '../_shared/log'\n\n/**\n * @hidden This isn't ready to be included in the public API just yet.\n */\nexport interface ProcessOptions {\n logOutput?: boolean\n ignoredMessageFilter?: string\n captureOutput?: boolean\n ignoreError?: boolean\n}\n\n/**\n * @hidden This isn't ready to be included in the public API just yet.\n */\nexport interface ProcessOutput {\n exitCode: number\n stdoutMessages: string[]\n stderrMessages: string[]\n}\n\n/**\n * Spawn a child process that runs the given command.\n *\n * @param cwd The directory in which the command will be executed.\n * @param command The command to execute.\n * @param args The arguments to pass to the command.\n * @param abortSignal The signal used to abort the process.\n * @param opts Additional options to configure the process.\n * @returns The output of the process.\n */\nexport function spawnChild(\n cwd: string,\n command: string,\n args: string[],\n abortSignal?: AbortSignal,\n opts?: ProcessOptions\n): Promise<ProcessOutput> {\n return new Promise((resolve, reject) => {\n if (abortSignal?.aborted) {\n reject(new Error('ABORT'))\n return\n }\n\n let childProc: ChildProcess\n\n const localLog = (s: string, err = false) => {\n // Don't log anything after the process has been killed\n if (childProc === undefined) {\n return\n }\n log(err ? 'error' : 'info', s)\n }\n\n const abortHandler = () => {\n if (childProc) {\n log('info', 'Killing existing build process...')\n childProc.kill('SIGKILL')\n childProc = undefined\n }\n reject(new Error('ABORT'))\n }\n\n // Kill the process if abort is requested\n abortSignal?.addEventListener('abort', abortHandler, { once: true })\n\n // Prepare for capturing output, if requested\n const stdoutMessages: string[] = []\n const stderrMessages: string[] = []\n const logMessage = (msg: string, err: boolean) => {\n let includeMessage = true\n if (opts?.ignoredMessageFilter && msg.trim().startsWith(opts.ignoredMessageFilter)) {\n includeMessage = false\n }\n if (includeMessage) {\n const lines = msg.trim().split('\\n')\n for (const line of lines) {\n localLog(` ${line}`, err)\n }\n }\n }\n\n // Spawn the (asynchronous) child process. Note that we are using `spawn`\n // from the `cross-spawn` package as an alternative to the built-in\n // `child_process` module, which doesn't handle spaces in command path\n // on Windows.\n childProc = spawn(command, args, {\n cwd\n })\n\n childProc.stdout.on('data', (data: Buffer) => {\n const msg = data.toString()\n if (opts?.captureOutput === true) {\n stdoutMessages.push(msg)\n }\n if (opts?.logOutput !== false) {\n logMessage(msg, false)\n }\n })\n childProc.stderr.on('data', (data: Buffer) => {\n const msg = data.toString()\n if (opts?.captureOutput === true) {\n stderrMessages.push(msg)\n }\n if (opts?.logOutput !== false) {\n logMessage(msg, true)\n }\n })\n childProc.on('error', err => {\n localLog(`Process error: ${err}`, true)\n })\n childProc.on('close', (code, signal) => {\n // Stop listening for abort events\n abortSignal?.removeEventListener('abort', abortHandler)\n childProc = undefined\n\n if (signal) {\n // The process was killed by a signal, so we don't need to print anything\n return\n }\n\n const processOutput: ProcessOutput = {\n exitCode: code,\n stdoutMessages,\n stderrMessages\n }\n\n if (code === 0) {\n // The process exited cleanly; resolve the promise\n resolve(processOutput)\n } else if (!signal) {\n // The process failed\n if (opts?.ignoreError === true) {\n // Resolve the promise (but with a non-zero exit code)\n resolve(processOutput)\n } else {\n // Reject the promise\n reject(new Error(`Child process failed (code=${code})`))\n }\n }\n })\n })\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport type { LogLevel } from '../_shared/log'\nimport { log } from '../_shared/log'\n\nimport type { ResolvedConfig } from '../_shared/resolved-config'\n\nimport type { ProcessOptions, ProcessOutput } from './spawn-child'\nimport { spawnChild } from './spawn-child'\nimport type { StagedFiles } from './staged-files'\n\n/**\n * Provides access to common functionality that is needed during the build process.\n * This is passed to most plugin functions.\n */\nexport class BuildContext {\n /**\n * @param config The resolved configuration.\n * @hidden\n */\n constructor(\n public readonly config: ResolvedConfig,\n private readonly stagedFiles: StagedFiles,\n private readonly abortSignal: AbortSignal | undefined\n ) {}\n\n /**\n * Log a message to the console and/or the in-browser overlay panel.\n *\n * @param level The log level (verbose, info, error).\n * @param msg The message.\n */\n log(level: LogLevel, msg: string): void {\n log(level, msg)\n }\n\n /**\n * Prepare for writing a file to the staged directory.\n *\n * This will add the path to the array of tracked files and will create the\n * staged directory if needed.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file will be written (this must be a relative path).\n * @param srcFile The name of the file as written to the `staged` directory.\n * @param dstDir The absolute path to the destination directory where the staged\n * file will be copied when the build has completed.\n * @param dstFile The name of the file as written to the destination directory.\n * @return The absolute path to the staged file.\n */\n prepareStagedFile(srcDir: string, srcFile: string, dstDir: string, dstFile: string): string {\n return this.stagedFiles.prepareStagedFile(srcDir, srcFile, dstDir, dstFile)\n }\n\n /**\n * Write a file to the staged directory.\n *\n * This file will be copied (along with other staged files) into the destination\n * directory only after the build process has completed. Copying all staged files\n * at once helps improve the local development experience by making it so that\n * live reloading tools only need to refresh once instead of every time a build\n * file is written.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file will be written (this must be a relative path).\n * @param dstDir The absolute path to the destination directory where the staged\n * file will be copied when the build has completed.\n * @param filename The name of the file.\n * @param content The file content.\n */\n writeStagedFile(srcDir: string, dstDir: string, filename: string, content: string): void {\n this.stagedFiles.writeStagedFile(srcDir, dstDir, filename, content)\n }\n\n /**\n * Spawn a child process that runs the given command.\n *\n * @param cwd The directory in which the command will be executed.\n * @param command The command to execute.\n * @param args The arguments to pass to the command.\n * @param opts Additional options to configure the process.\n * @returns The output of the process.\n */\n spawnChild(cwd: string, command: string, args: string[], opts?: ProcessOptions): Promise<ProcessOutput> {\n return spawnChild(cwd, command, args, this.abortSignal, opts)\n }\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'fs'\nimport { join as joinPath } from 'path'\n\nimport { log } from '../_shared/log'\n\ninterface StagedFile {\n srcDir: string\n srcFile: string\n dstDir: string\n dstFile: string\n}\n\nexport class StagedFiles {\n private readonly baseStagedDir: string\n private readonly stagedFiles: StagedFile[] = []\n\n constructor(prepDir: string) {\n this.baseStagedDir = joinPath(prepDir, 'staged')\n }\n\n /**\n * Prepare for writing a file to the staged directory.\n *\n * This will add the path to the array of tracked files and will create the\n * staged directory if needed.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file will be written (this must be a relative path).\n * @param srcFile The name of the file as written to the `staged` directory.\n * @param dstDir The absolute path to the destination directory where the staged\n * file will be copied when the build has completed.\n * @param dstFile The name of the file as written to the destination directory.\n * @return The absolute path to the staged file.\n */\n prepareStagedFile(srcDir: string, srcFile: string, dstDir: string, dstFile: string): string {\n // Add an entry to the array of staged files (only if an entry does not already\n // exist) so that we can copy the file to the destination directory when the build\n // process has completed\n const stagedFile = {\n srcDir,\n srcFile,\n dstDir,\n dstFile\n }\n // TODO: We allow there to be more than one entry for each source path to\n // support the case where a single source file gets copied to multiple\n // destination paths. But we should throw an error if the same destination\n // path is configured for different source paths.\n if (this.stagedFiles.indexOf(stagedFile) < 0) {\n this.stagedFiles.push(stagedFile)\n }\n\n // Create the directory underneath the staged directory if needed\n const stagedDir = joinPath(this.baseStagedDir, srcDir)\n if (!existsSync(stagedDir)) {\n mkdirSync(stagedDir, { recursive: true })\n }\n\n return joinPath(stagedDir, srcFile)\n }\n\n /**\n * Write a file to the staged directory.\n *\n * This file will be copied (along with other staged files) into the destination\n * directory only after the build process has completed. Copying all staged files\n * at once helps improve the local development experience by making it so that\n * live reloading tools only need to refresh once instead of every time a build\n * file is written.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file will be written (this must be a relative path).\n * @param dstDir The absolute path to the destination directory where the staged\n * file will be copied when the build has completed.\n * @param filename The name of the file.\n * @param content The file content.\n */\n writeStagedFile(srcDir: string, dstDir: string, filename: string, content: string): void {\n // Add an entry to track the file and create the staged directory if needed\n const stagedFilePath = this.prepareStagedFile(srcDir, filename, dstDir, filename)\n\n // Write the file to the staged directory\n writeFileSync(stagedFilePath, content)\n }\n\n /**\n * Return the absolute path to the staged file for the given source directory and file name.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file would be written initially (this must be a relative path).\n * @param srcFile The name of the file.\n */\n getStagedFilePath(srcDir: string, srcFile: string): string {\n return joinPath(this.baseStagedDir, srcDir, srcFile)\n }\n\n /**\n * Return true if the staged file exists for the given source directory and file name.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file would be written initially (this must be a relative path).\n * @param srcFile The name of the file.\n */\n stagedFileExists(srcDir: string, srcFile: string): boolean {\n const fullSrcPath = this.getStagedFilePath(srcDir, srcFile)\n return existsSync(fullSrcPath)\n }\n\n /**\n * Return true if the destination file exists for the given source directory and file name.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file would be written initially (this must be a relative path).\n * @param srcFile The name of the file.\n */\n destinationFileExists(srcDir: string, srcFile: string): boolean {\n const f = this.stagedFiles.find(f => f.srcDir === srcDir && f.srcFile === srcFile)\n if (f === undefined) {\n return false\n }\n const fullDstPath = joinPath(f.dstDir, f.dstFile)\n return existsSync(fullDstPath)\n }\n\n /**\n * Copy staged files to their destination; this will only copy the staged\n * files if they are different than the existing destination files. We\n * copy the files in a batch like this so that hot module reload is only\n * triggered once at the end of the whole build process.\n */\n copyChangedFiles(): void {\n log('info', 'Copying changed files into place...')\n\n for (const f of this.stagedFiles) {\n this.copyStagedFile(f)\n }\n\n log('info', 'Done copying files')\n }\n\n /**\n * Copy a file from the `staged` directory to its destination. If the file already\n * exists in the destination directory and has the same contents as the source file,\n * the file will not be copied and this function will return false.\n *\n * @param f The staged file entry.\n */\n private copyStagedFile(f: StagedFile): boolean {\n // Create the destination directory, if needed\n if (!existsSync(f.dstDir)) {\n mkdirSync(f.dstDir, { recursive: true })\n }\n\n // If the destination file already exists and has the same contents as the source\n // file, we can skip copying it\n const fullSrcPath = this.getStagedFilePath(f.srcDir, f.srcFile)\n const fullDstPath = joinPath(f.dstDir, f.dstFile)\n const needsCopy = filesDiffer(fullSrcPath, fullDstPath)\n if (needsCopy) {\n log('verbose', ` Copying ${f.srcFile} to ${fullDstPath}`)\n copyFileSync(fullSrcPath, fullDstPath)\n }\n return needsCopy\n }\n}\n\n/**\n * Return true if both files exist at the given paths and have the same contents, false otherwise.\n */\nfunction filesDiffer(aPath: string, bPath: string): boolean {\n if (existsSync(aPath) && existsSync(bPath)) {\n // The files exist; see if they are different\n const aSize = statSync(aPath).size\n const bSize = statSync(bPath).size\n if (aSize !== bSize) {\n // The sizes are different, so the contents must be different\n return true\n } else {\n // The sizes are the same, so check the contents\n const aBuf = readFileSync(aPath)\n const bBuf = readFileSync(bPath)\n return !aBuf.equals(bBuf)\n }\n } else {\n // One or both files do not exist\n return true\n }\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { copyFile, readdir, readFile, writeFile } from 'fs/promises'\nimport { join as joinPath } from 'path'\n\nimport { log } from '../../_shared/log'\n\nimport type { BuildContext } from '../../context/context'\n\nimport type { Plugin } from '../../plugin/plugin'\n\n/**\n * Generate the model. This will run the core SDEverywhere code generation steps\n * and will also invoke the following plugin functions:\n * - `preProcessMdl`\n * - `postProcessMdl`\n * - `preGenerateC`\n * - `postGenerateC`\n */\nexport async function generateModel(context: BuildContext, plugins: Plugin[]): Promise<void> {\n const config = context.config\n if (config.modelFiles.length === 0) {\n log('info', 'No model input files specified, skipping model generation steps')\n return\n }\n\n log('info', 'Generating model...')\n\n const t0 = performance.now()\n\n // Use the defined prep directory\n const prepDir = config.prepDir\n\n // TODO: For now we assume the path is to the `main.js` file in the cli package;\n // this seems to work on both Unix and Windows, but we may need to revisit this\n // as part of removing the `sdeCmdPath` config hack\n const sdeCmdPath = config.sdeCmdPath\n\n // Process the mdl file(s)\n for (const plugin of plugins) {\n if (plugin.preProcessMdl) {\n await plugin.preProcessMdl(context)\n }\n }\n if (config.modelFiles.length === 1) {\n // Preprocess the single mdl file\n await preprocessMdl(context, sdeCmdPath, prepDir, config.modelFiles[0])\n } else {\n // Flatten and preprocess the multiple mdl files into a single mdl file\n await flattenMdls(context, sdeCmdPath, prepDir, config.modelFiles)\n }\n for (const plugin of plugins) {\n if (plugin.postProcessMdl) {\n const mdlPath = joinPath(prepDir, 'processed.mdl')\n let mdlContent = await readFile(mdlPath, 'utf8')\n mdlContent = await plugin.postProcessMdl(context, mdlContent)\n await writeFile(mdlPath, mdlContent)\n }\n }\n\n // Generate the C file\n for (const plugin of plugins) {\n if (plugin.preGenerateC) {\n await plugin.preGenerateC(context)\n }\n }\n await generateC(context, config.sdeDir, sdeCmdPath, prepDir)\n for (const plugin of plugins) {\n if (plugin.postGenerateC) {\n const cPath = joinPath(prepDir, 'build', 'processed.c')\n let cContent = await readFile(cPath, 'utf8')\n cContent = await plugin.postGenerateC(context, cContent)\n await writeFile(cPath, cContent)\n }\n }\n\n const t1 = performance.now()\n const elapsed = ((t1 - t0) / 1000).toFixed(1)\n log('info', `Done generating model (${elapsed}s)`)\n}\n\n/**\n * Preprocess a single mdl file and copy the resulting `processed.mdl` to the prep directory.\n */\nasync function preprocessMdl(\n context: BuildContext,\n sdeCmdPath: string,\n prepDir: string,\n modelFile: string\n): Promise<void> {\n log('verbose', ' Preprocessing mdl file')\n\n // Copy the source file to the prep directory to make the next steps easier\n await copyFile(modelFile, joinPath(prepDir, 'processed.mdl'))\n\n // Use SDE to preprocess the model to strip anything that's not needed to build it\n const command = sdeCmdPath\n const args = ['generate', '--preprocess', 'processed.mdl']\n await context.spawnChild(prepDir, command, args)\n\n // Copy the processed file back to the prep directory\n await copyFile(joinPath(prepDir, 'build', 'processed.mdl'), joinPath(prepDir, 'processed.mdl'))\n}\n\n/**\n * Flatten multiple mdl files and copy the resulting `processed.mdl` to the prep directory.\n */\nasync function flattenMdls(\n context: BuildContext,\n sdeCmdPath: string,\n prepDir: string,\n modelFiles: string[]\n): Promise<void> {\n log('verbose', ' Flattening and preprocessing mdl files')\n\n // Use SDE to flatten the parent model and submodels into a single mdl file,\n // then preprocess to strip anything that's not needed to build the model\n const command = sdeCmdPath\n const args: string[] = []\n args.push('flatten')\n args.push('processed.mdl')\n args.push('--inputs')\n for (const path of modelFiles) {\n args.push(path)\n }\n\n // Disable logging by default; this will suppress flatten warnings, which are\n // sometimes unavoidable and not helpful\n const output = await context.spawnChild(prepDir, command, args, {\n logOutput: false,\n captureOutput: true,\n ignoreError: true\n })\n\n // Check for flattening errors\n let flattenErrors = false\n for (const msg of output.stderrMessages) {\n if (msg.includes('ERROR')) {\n flattenErrors = true\n break\n }\n }\n if (flattenErrors) {\n log('error', 'There were errors reported when flattening the model:')\n for (const msg of output.stderrMessages) {\n const lines = msg.split('\\n')\n for (const line of lines) {\n log('error', ` ${line}`)\n }\n }\n throw new Error(`Flatten command failed (code=${output.exitCode})`)\n } else if (output.exitCode !== 0) {\n throw new Error(`Flatten command failed (code=${output.exitCode})`)\n }\n\n // Copy the processed file back to the prep directory\n await copyFile(joinPath(prepDir, 'build', 'processed.mdl'), joinPath(prepDir, 'processed.mdl'))\n}\n\n/**\n * Generate a C file from the `processed.mdl` file.\n */\nasync function generateC(context: BuildContext, sdeDir: string, sdeCmdPath: string, prepDir: string): Promise<void> {\n log('verbose', ' Generating C code')\n\n // Use SDE to generate a C version of the model\n const command = sdeCmdPath\n const args = ['generate', '--genc', '--spec', 'spec.json', 'processed']\n await context.spawnChild(prepDir, command, args, {\n // By default, ignore lines that start with \"WARNING: Data for\" since these are often harmless\n // TODO: Don't filter by default, but make it configurable\n // ignoredMessageFilter: 'WARNING: Data for'\n })\n\n // Copy SDE's supporting C files into the build directory\n const buildDir = joinPath(prepDir, 'build')\n const sdeCDir = joinPath(sdeDir, 'src', 'c')\n const files = await readdir(sdeCDir)\n const copyOps = []\n for (const file of files) {\n if (file.endsWith('.c') || file.endsWith('.h')) {\n copyOps.push(copyFile(joinPath(sdeCDir, file), joinPath(buildDir, file)))\n }\n }\n await Promise.all(copyOps)\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { join as joinPath } from 'path'\nimport { hashElement } from 'folder-hash'\nimport glob from 'tiny-glob'\n\nimport type { ResolvedConfig } from '../../_shared/resolved-config'\n\n/**\n * Asynchronously compute the hash of the files that are inputs to the model\n * build process.\n */\nexport async function computeInputFilesHash(config: ResolvedConfig): Promise<string> {\n const inputFiles: string[] = []\n\n // Always include the `spec.json` file, since that is a primary input\n // to the model build process\n const specFile = joinPath(config.prepDir, 'spec.json')\n inputFiles.push(specFile)\n\n if (config.modelInputPaths && config.modelInputPaths.length > 0) {\n // Include the files that match the glob patterns in the config file.\n // Note that the folder-hash package supports glob patterns, but its\n // configuration is complicated, so it is easier if we resolve the\n // glob patterns here and pass each individual file to the\n // `hashElement` function.\n for (const globPath of config.modelInputPaths) {\n const paths = await glob(globPath, {\n cwd: config.rootDir,\n absolute: true,\n filesOnly: true\n })\n inputFiles.push(...paths)\n }\n } else {\n // Only use the mdl files to compute the hash\n inputFiles.push(...config.modelFiles)\n }\n\n // Compute the hash of each input file and concatenate into a single string\n let hash = ''\n for (const inputFile of inputFiles) {\n const result = await hashElement(inputFile)\n hash += result.hash\n }\n\n return hash\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { basename } from 'path'\n\nimport chokidar from 'chokidar'\n\nimport { clearOverlay, log, logError } from '../../_shared/log'\nimport type { ResolvedConfig } from '../../_shared/resolved-config'\n\nimport type { UserConfig } from '../../config/user-config'\nimport type { Plugin } from '../../plugin/plugin'\n\nimport type { BuildOnceOptions } from './build-once'\nimport { buildOnce } from './build-once'\n\nclass BuildState {\n readonly abortController = new AbortController()\n}\n\nexport function watch(config: ResolvedConfig, userConfig: UserConfig, plugins: Plugin[]): void {\n // Add a small delay so that if multiple files are changed at once (as is\n // often the case when switching branches), we batch them up and start the\n // the build after things settle down\n const delay = 150\n const changedPaths: Set<string> = new Set()\n\n // Keep track of the current build so that we only have one active at a time\n let currentBuildState: BuildState\n\n function performBuild() {\n clearOverlay()\n\n // Log the input files that have changed\n for (const path of changedPaths) {\n log('info', `Input file ${basename(path)} has been changed`)\n }\n\n // Clear the set of pending files\n changedPaths.clear()\n\n // Keep track of builds; if one is already in progress, abort it\n // before starting another one\n if (currentBuildState) {\n currentBuildState.abortController.abort()\n // TODO: Prevent aborted build from logging?\n currentBuildState = undefined\n }\n\n // Generate files and build the model\n currentBuildState = new BuildState()\n const buildOptions: BuildOnceOptions = {\n abortSignal: currentBuildState.abortController.signal\n }\n buildOnce(config, userConfig, plugins, buildOptions)\n .then(result => {\n // Log the error message in case of error result\n if (result.isErr()) {\n logError(result.error)\n }\n })\n .catch(e => {\n // Also catch thrown errors that may have not already been\n // handled by `buildOnce`\n logError(e)\n // log('info', 'Waiting for changes...')\n })\n .finally(() => {\n currentBuildState = undefined\n })\n }\n\n function scheduleBuild(changedPath: string) {\n // Only schedule the build if the set is currently empty\n const schedule = changedPaths.size === 0\n\n // Add the path to the set of changed files\n changedPaths.add(changedPath)\n\n if (schedule) {\n // Schedule the build to start after a delay\n setTimeout(() => {\n performBuild()\n }, delay)\n }\n }\n\n let watchPaths: string[]\n if (config.watchPaths && config.watchPaths.length > 0) {\n // Watch the configured files\n watchPaths = config.watchPaths\n } else {\n // Only watch the mdl files\n watchPaths = config.modelFiles\n }\n\n // Watch the config and model files; if changes are detected, generate the specs\n // and rebuild the model if needed\n const watcher = chokidar.watch(watchPaths, {\n // Watch paths are resolved relative to the project root directory\n cwd: config.rootDir,\n // XXX: Include a delay, otherwise on macOS we sometimes get multiple\n // change events when the csv file is saved just once\n awaitWriteFinish: {\n stabilityThreshold: 200\n }\n })\n watcher.on('change', path => {\n scheduleBuild(path)\n })\n}\n"],"mappings":"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","names":["import_path","import_neverthrow","joinPath","resolvePath","import_fs","pico","import_fs","import_promises","import_path","import_neverthrow","err","import_fs","import_path","joinPath","f","import_path","joinPath","import_path","joinPath","glob","joinPath","import_path","chokidar","joinPath","overlayEnabled","plugins"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -33,10 +33,6 @@ interface OutputSpec {
|
|
|
33
33
|
* that should be included in the model generated by SDE.
|
|
34
34
|
*/
|
|
35
35
|
interface ModelSpec {
|
|
36
|
-
/** The start time (year) for the model (typically the same as `INITIAL TIME`). */
|
|
37
|
-
startTime: number;
|
|
38
|
-
/** The end time (year) for the model (typically the same as `FINAL TIME`). */
|
|
39
|
-
endTime: number;
|
|
40
36
|
/** The input variable specs. */
|
|
41
37
|
inputs: InputSpec[];
|
|
42
38
|
/** The output variable specs. */
|
package/dist/index.js
CHANGED
|
@@ -1,20 +1,3 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
3
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
4
|
-
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
5
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
6
|
-
var __spreadValues = (a, b) => {
|
|
7
|
-
for (var prop in b || (b = {}))
|
|
8
|
-
if (__hasOwnProp.call(b, prop))
|
|
9
|
-
__defNormalProp(a, prop, b[prop]);
|
|
10
|
-
if (__getOwnPropSymbols)
|
|
11
|
-
for (var prop of __getOwnPropSymbols(b)) {
|
|
12
|
-
if (__propIsEnum.call(b, prop))
|
|
13
|
-
__defNormalProp(a, prop, b[prop]);
|
|
14
|
-
}
|
|
15
|
-
return a;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
1
|
// src/build/build.ts
|
|
19
2
|
import { join as joinPath6 } from "path";
|
|
20
3
|
import { err as err3, ok as ok3 } from "neverthrow";
|
|
@@ -382,9 +365,13 @@ function filesDiffer(aPath, bPath) {
|
|
|
382
365
|
import { copyFile, readdir, readFile, writeFile } from "fs/promises";
|
|
383
366
|
import { join as joinPath3 } from "path";
|
|
384
367
|
async function generateModel(context, plugins) {
|
|
368
|
+
const config = context.config;
|
|
369
|
+
if (config.modelFiles.length === 0) {
|
|
370
|
+
log("info", "No model input files specified, skipping model generation steps");
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
385
373
|
log("info", "Generating model...");
|
|
386
374
|
const t0 = performance.now();
|
|
387
|
-
const config = context.config;
|
|
388
375
|
const prepDir = config.prepDir;
|
|
389
376
|
const sdeCmdPath = config.sdeCmdPath;
|
|
390
377
|
for (const plugin of plugins) {
|
|
@@ -392,9 +379,7 @@ async function generateModel(context, plugins) {
|
|
|
392
379
|
await plugin.preProcessMdl(context);
|
|
393
380
|
}
|
|
394
381
|
}
|
|
395
|
-
if (config.modelFiles.length ===
|
|
396
|
-
throw new Error("No model input files specified");
|
|
397
|
-
} else if (config.modelFiles.length === 1) {
|
|
382
|
+
if (config.modelFiles.length === 1) {
|
|
398
383
|
await preprocessMdl(context, sdeCmdPath, prepDir, config.modelFiles[0]);
|
|
399
384
|
} else {
|
|
400
385
|
await flattenMdls(context, sdeCmdPath, prepDir, config.modelFiles);
|
|
@@ -532,11 +517,12 @@ async function buildOnce(config, userConfig, plugins, options) {
|
|
|
532
517
|
plugin.preGenerate(context, modelSpec);
|
|
533
518
|
}
|
|
534
519
|
}
|
|
535
|
-
const specJson =
|
|
520
|
+
const specJson = {
|
|
536
521
|
inputVarNames: modelSpec.inputs.map((input) => input.varName),
|
|
537
522
|
outputVarNames: modelSpec.outputs.map((output) => output.varName),
|
|
538
|
-
externalDatfiles: modelSpec.datFiles
|
|
539
|
-
|
|
523
|
+
externalDatfiles: modelSpec.datFiles,
|
|
524
|
+
...modelSpec.options
|
|
525
|
+
};
|
|
540
526
|
const specPath = joinPath5(config.prepDir, "spec.json");
|
|
541
527
|
await writeFile2(specPath, JSON.stringify(specJson, null, 2));
|
|
542
528
|
const modelHashPath = joinPath5(config.prepDir, "model-hash.txt");
|
|
@@ -618,7 +604,11 @@ function watch(config, userConfig, plugins) {
|
|
|
618
604
|
const buildOptions = {
|
|
619
605
|
abortSignal: currentBuildState.abortController.signal
|
|
620
606
|
};
|
|
621
|
-
buildOnce(config, userConfig, plugins, buildOptions).
|
|
607
|
+
buildOnce(config, userConfig, plugins, buildOptions).then((result) => {
|
|
608
|
+
if (result.isErr()) {
|
|
609
|
+
logError(result.error);
|
|
610
|
+
}
|
|
611
|
+
}).catch((e) => {
|
|
622
612
|
logError(e);
|
|
623
613
|
}).finally(() => {
|
|
624
614
|
currentBuildState = void 0;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/build/build.ts","../src/config/config-loader.ts","../src/_shared/log.ts","../src/build/impl/build-once.ts","../src/context/spawn-child.ts","../src/context/context.ts","../src/context/staged-files.ts","../src/build/impl/gen-model.ts","../src/build/impl/hash-files.ts","../src/build/impl/watch.ts"],"sourcesContent":["// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { join as joinPath } from 'path'\n\nimport type { Result } from 'neverthrow'\nimport { err, ok } from 'neverthrow'\n\nimport type { BuildMode } from '../_shared/mode'\n\nimport { loadConfig } from '../config/config-loader'\nimport type { UserConfig } from '../config/user-config'\nimport type { LogLevel } from '../_shared/log'\nimport { setOverlayFile, setActiveLevels } from '../_shared/log'\nimport { buildOnce } from './impl/build-once'\nimport { watch } from './impl/watch'\n\nexport interface BuildOptions {\n /** The path to an `sde.config.js` file, or a `UserConfig` object. */\n config?: string | UserConfig\n\n /**\n * The log levels to include. If undefined, the default 'info' and 'error' levels\n * will be active.\n */\n logLevels?: LogLevel[]\n\n /**\n * The path to the `@sdeverywhere/cli` package. This is currently only used to get\n * access to the files in the `src/c` directory.\n * @hidden This should be removed once we have tighter integration with the `cli` package.\n */\n sdeDir: string\n\n /**\n * The path to the `sde` command.\n * @hidden This should be removed once we have tighter integration with the `cli` package.\n */\n sdeCmdPath: string\n}\n\nexport interface BuildResult {\n /**\n * The exit code that should be set by the process. This will be undefined\n * if `mode` is 'development', indicating that the process should be kept alive.\n */\n exitCode?: number\n}\n\n/**\n * Initiate the build process, which can either be a single build if `mode` is\n * 'production', or a live development environment if `mode` is 'development'.\n *\n * @param mode The build mode.\n * @param options The build options.\n * @return An `ok` result if the build completed, otherwise an `err` result.\n */\nexport async function build(mode: BuildMode, options: BuildOptions): Promise<Result<BuildResult, Error>> {\n // Load the config\n const configResult = await loadConfig(mode, options.config, options.sdeDir, options.sdeCmdPath)\n if (configResult.isErr()) {\n return err(configResult.error)\n }\n const { userConfig, resolvedConfig } = configResult.value\n\n // Configure logging level\n if (options.logLevels !== undefined) {\n setActiveLevels(options.logLevels)\n }\n\n // Configure the overlay `messages.html` file, which is written under the\n // `sde-prep` directory. For production builds, this file will remain empty.\n const messagesPath = joinPath(resolvedConfig.prepDir, 'messages.html')\n const overlayEnabled = mode === 'development'\n setOverlayFile(messagesPath, overlayEnabled)\n\n // Initialize plugins\n const plugins = userConfig.plugins || []\n for (const plugin of plugins) {\n if (plugin.init) {\n await plugin.init(resolvedConfig)\n }\n }\n\n try {\n const plugins = userConfig.plugins || []\n\n if (mode === 'development') {\n // Enable dev mode (which will rebuild when watched files are changed).\n // First run an initial build so that we have a baseline, and then\n // once that is complete, enable file watchers.\n const buildResult = await buildOnce(resolvedConfig, userConfig, plugins, {})\n // TODO: We should trap errors here and keep the dev process\n // running if the initial build fails\n if (buildResult.isErr()) {\n return err(buildResult.error)\n }\n\n // Allow plugins to set up watchers after the initial build completes\n for (const plugin of plugins) {\n if (plugin.watch) {\n await plugin.watch(resolvedConfig)\n }\n }\n\n // Watch for changes to the source/model/test files\n watch(resolvedConfig, userConfig, plugins)\n\n // Return a build result with undefined exit code, indicating that the\n // process should be kept alive\n return ok({})\n } else {\n // Run a single build\n const buildResult = await buildOnce(resolvedConfig, userConfig, plugins, {})\n if (buildResult.isErr()) {\n return err(buildResult.error)\n }\n\n // Configure the exit code depending on whether any plugins failed.\n // Currently we use the following exit code values:\n // 0 == build succeeded, AND all plugins succeeded\n // 1 == build failed (or a plugin reported a \"hard\" error)\n // 2 == build succeeded, BUT one or more plugins failed\n const allPluginsSucceeded = buildResult.value\n const exitCode = allPluginsSucceeded ? 0 : 2\n return ok({ exitCode })\n }\n } catch (e) {\n return err(e)\n }\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { existsSync, lstatSync, mkdirSync } from 'fs'\nimport { dirname, join as joinPath, relative, resolve as resolvePath } from 'path'\nimport { fileURLToPath } from 'url'\n\nimport type { Result } from 'neverthrow'\nimport { err, ok } from 'neverthrow'\n\nimport type { BuildMode } from '../_shared/mode'\nimport type { ResolvedConfig } from '../_shared/resolved-config'\n\nimport type { UserConfig } from './user-config'\n\nexport interface ConfigResult {\n userConfig: UserConfig\n resolvedConfig: ResolvedConfig\n}\n\n/**\n * Load a user-defined config file or a given `UserConfig` object. This validates\n * all paths and if successful, this returns a `ResolvedConfig`, otherwise returns\n * an error result.\n *\n * @param mode The build mode.\n * @param config The path to a config file, or a `UserConfig` object; if undefined,\n * this will look for a `sde.config.js` file in the current directory.\n * @param sdeDir Temporary (the path to the `@sdeverywhere/cli` package).\n * @param sdeCmdPath Temporary (the path to the `sde` command).\n * @return An `ok` result with the `ResolvedConfig`, otherwise an `err` result.\n */\nexport async function loadConfig(\n mode: BuildMode,\n config: string | UserConfig | undefined,\n sdeDir: string,\n sdeCmdPath: string\n): Promise<Result<ConfigResult, Error>> {\n let userConfig: UserConfig\n if (typeof config === 'object') {\n // Use the given `UserConfig` object\n userConfig = config\n } else {\n // Load the project-specific config. If no `--config` arg was specified\n // on the command line, look for a `sde.config.js` file in the current\n // directory, and failing that, use a default config.\n // TODO: Create a default config if no file found; for now, we just fail\n let configPath: string\n if (typeof config === 'string') {\n configPath = config\n } else {\n configPath = joinPath(process.cwd(), 'sde.config.js')\n }\n try {\n if (!existsSync(configPath)) {\n return err(new Error(`Cannot find config file '${configPath}'`))\n }\n const configRelPath = relativeToSourcePath(configPath)\n const configModule = await import(configRelPath)\n userConfig = await configModule.config()\n } catch (e) {\n return err(new Error(`Failed to load config file '${configPath}': ${e.message}`))\n }\n }\n\n // Resolve the config\n try {\n const resolvedConfig = resolveUserConfig(userConfig, mode, sdeDir, sdeCmdPath)\n return ok({\n userConfig,\n resolvedConfig\n })\n } catch (e) {\n return err(e)\n }\n}\n\n/**\n * Resolve the given user configuration by resolving all paths. This will create\n * the prep directory if it does not already exist. This will throw an error if\n * any other paths are invalid or do not exist.\n *\n * @param userConfig The user-defined configuration.\n * @param mode The active build mode.\n * @param sdeDir Temporary (the path to the `@sdeverywhere/cli` package).\n * @param sdeCmdPath Temporary (the path to the `sde` command).\n * @return The resolved configuration.\n */\nfunction resolveUserConfig(\n userConfig: UserConfig,\n mode: BuildMode,\n sdeDir: string,\n sdeCmdPath: string\n): ResolvedConfig {\n function expectDirectory(propName: string, path: string): void {\n if (!existsSync(path)) {\n throw new Error(`The configured ${propName} (${path}) does not exist`)\n } else if (!lstatSync(path).isDirectory()) {\n throw new Error(`The configured ${propName} (${path}) is not a directory`)\n }\n }\n\n // function expectFile(propName: string, path: string): void {\n // if (!existsSync(path)) {\n // throw new Error(`The configured ${propName} (${path}) does not exist`)\n // // TODO: Don't include the \"is file\" check for now; need to update this to\n // // handle symlinks\n // // } else if (!lstatSync(path).isFile()) {\n // // throw new Error(`The configured ${propName} (${path}) is not a file`)\n // }\n // }\n\n // Validate the root directory\n let rootDir: string\n if (userConfig.rootDir) {\n // Verify that the configured root directory exists\n rootDir = resolvePath(userConfig.rootDir)\n expectDirectory('rootDir', rootDir)\n } else {\n // Use the current working directory as the project root\n rootDir = process.cwd()\n }\n\n // Validate the prep directory\n let prepDir: string\n if (userConfig.prepDir) {\n // Resolve the configured prep directory\n prepDir = resolvePath(userConfig.prepDir)\n } else {\n // Create an 'sde-prep' directory under the configured root directory\n prepDir = resolvePath(rootDir, 'sde-prep')\n }\n mkdirSync(prepDir, { recursive: true })\n\n // Validate the model files\n const userModelFiles = userConfig.modelFiles\n const modelFiles: string[] = []\n for (const userModelFile of userModelFiles) {\n const modelFile = resolvePath(userModelFile)\n if (!existsSync(modelFile)) {\n throw new Error(`The configured model file (${modelFile}) does not exist`)\n }\n modelFiles.push(modelFile)\n }\n\n // TODO: Validate the watch paths; these are allowed to be globs, so need to\n // figure out the best way to resolve them\n let modelInputPaths: string[]\n if (userConfig.modelInputPaths && userConfig.modelInputPaths.length > 0) {\n modelInputPaths = userConfig.modelInputPaths\n } else {\n modelInputPaths = modelFiles\n }\n let watchPaths: string[]\n if (userConfig.watchPaths && userConfig.watchPaths.length > 0) {\n watchPaths = userConfig.watchPaths\n } else {\n watchPaths = modelFiles\n }\n\n return {\n mode,\n rootDir,\n prepDir,\n modelFiles,\n modelInputPaths,\n watchPaths,\n sdeDir,\n sdeCmdPath\n }\n}\n\n/**\n * Return a Unix-style path (e.g. '../../foo.js') that is relative to the directory of\n * the current source file. This can be used to construct a path that is safe for\n * dynamic import on either Unix or Windows.\n *\n * @param filePath The path to make relative.\n */\nfunction relativeToSourcePath(filePath: string): string {\n const srcDir = dirname(fileURLToPath(import.meta.url))\n const relPath = relative(srcDir, filePath)\n return relPath.replaceAll('\\\\', '/')\n}\n","// Copyright (c) 2021-2022 Climate Interactive / New Venture Fund\n\nimport { writeFileSync } from 'fs'\nimport pico from 'picocolors'\n\nexport type LogLevel = 'error' | 'info' | 'verbose'\n\nconst activeLevels: Set<LogLevel> = new Set(['error', 'info'])\n\nlet overlayFile: string\nlet overlayEnabled = false\nlet overlayHtml = ''\n\n/**\n * Set the active logging levels. By default, only 'error' and 'info'\n * messages are emitted.\n *\n * @param logLevels The logging levels to include.\n */\nexport function setActiveLevels(logLevels: LogLevel[]): void {\n activeLevels.clear()\n for (const level of logLevels) {\n activeLevels.add(level)\n }\n}\n\n/**\n * Set the path to the `messages.html` file where overlay messages will be written.\n *\n * @param file The absolute path to the HTML file where messages will be written.\n * @param enabled Whether to write messages to the file; if false, the file will be\n * emptied and no further messages will be written.\n */\nexport function setOverlayFile(file: string, enabled: boolean): void {\n overlayFile = file\n overlayEnabled = enabled\n\n // Write an empty file by default; this will ensure that messages from a previous\n // build aren't included in the current build\n writeFileSync(overlayFile, '')\n}\n\n/**\n * Log a message to the console and/or overlay.\n *\n * @param level The logging level.\n * @param msg The message to emit.\n */\nexport function log(level: LogLevel, msg: string): void {\n if (activeLevels.has(level)) {\n if (level === 'error') {\n console.error(pico.red(msg))\n logToOverlay(msg)\n } else {\n console.log(msg)\n logToOverlay(msg)\n }\n }\n}\n\n/**\n * Log an error to the console and/or overlay.\n *\n * @param e The error to log.\n */\nexport function logError(e: Error): void {\n // Remove the first part of the stack trace (which contains the message)\n // so that we can control the formatting of the message separately, then\n // only include up to 3 lines of the stack to keep the log cleaner\n const stack = e.stack || ''\n const stackLines = stack.split('\\n').filter(s => s.match(/^\\s+at/))\n const trace = stackLines.slice(0, 3).join('\\n')\n\n // Log the error message followed by the stack trace\n console.error(pico.red(`\\nERROR: ${e.message}`))\n console.error(pico.dim(pico.red(`${trace}\\n`)))\n logToOverlay(`\\nERROR: ${e.message}`, true)\n logToOverlay(`${trace}\\n`, true)\n}\n\nfunction writeOverlayFiles(): void {\n writeFileSync(overlayFile, overlayHtml)\n}\n\nexport function clearOverlay(): void {\n if (!overlayEnabled) {\n return\n }\n\n overlayHtml = ''\n writeOverlayFiles()\n}\n\nconst indent = ' '.repeat(4)\n\nexport function logToOverlay(msg: string, error = false): void {\n if (!overlayEnabled) {\n return\n }\n\n if (error) {\n msg = `<span class=\"overlay-error\">${msg}</span>`\n }\n const msgHtml = msg.replace(/\\n/g, '\\n<br/>').replace(/\\s{2}/g, indent)\n if (overlayHtml) {\n overlayHtml += `<br/>${msgHtml}`\n } else {\n overlayHtml = `${msgHtml}`\n }\n writeOverlayFiles()\n}\n\nexport function clearConsole(): void {\n // TODO: This is disabled for now; maybe re-enable it under an optional flag\n // console.log('\\x1Bc')\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { existsSync, readFileSync, writeFileSync } from 'fs'\nimport { writeFile } from 'fs/promises'\nimport { join as joinPath } from 'path'\n\nimport type { Result } from 'neverthrow'\nimport { err, ok } from 'neverthrow'\n\nimport { clearOverlay, log } from '../../_shared/log'\nimport type { ResolvedConfig } from '../../_shared/resolved-config'\n\nimport type { UserConfig } from '../../config/user-config'\nimport { BuildContext } from '../../context/context'\nimport { StagedFiles } from '../../context/staged-files'\nimport type { Plugin } from '../../plugin/plugin'\n\nimport { generateModel } from './gen-model'\nimport { computeInputFilesHash } from './hash-files'\nimport type { ModelSpec } from '../../_shared/model-spec'\n\nexport interface BuildOnceOptions {\n forceModelGen?: boolean\n abortSignal?: AbortSignal\n}\n\n/**\n * Perform a single build.\n *\n * This will return an error if a build failure occurred, or if a plugin encounters\n * an error. Otherwise, it will return true if the build and all plugins\n * succeeded, or false if a plugin wants to report a \"soft\" failure (for example,\n * if the model check plugin reports failing checks).\n *\n * @param config The resolved build configuration.\n * @param plugins The configured plugins.\n * @param options Options specific to the build process.\n * @return An `ok` result with true if the build and all plugins succeeded, or false if\n * one or more plugins failed; otherwise, an `err` result if there was a hard error.\n */\nexport async function buildOnce(\n config: ResolvedConfig,\n userConfig: UserConfig,\n plugins: Plugin[],\n options: BuildOnceOptions\n): Promise<Result<boolean, Error>> {\n // Create the build context\n const stagedFiles = new StagedFiles(config.prepDir)\n const context = new BuildContext(config, stagedFiles, options.abortSignal)\n\n // Get the model spec from the config\n let modelSpec: ModelSpec\n try {\n modelSpec = await userConfig.modelSpec(context)\n if (modelSpec === undefined) {\n return err(new Error('The model spec must be defined'))\n }\n } catch (e) {\n return err(e)\n }\n\n // Run plugins that implement `preGenerate`\n for (const plugin of plugins) {\n if (plugin.preGenerate) {\n plugin.preGenerate(context, modelSpec)\n }\n }\n\n // Write the spec file\n const specJson = {\n inputVarNames: modelSpec.inputs.map(input => input.varName),\n outputVarNames: modelSpec.outputs.map(output => output.varName),\n externalDatfiles: modelSpec.datFiles,\n ...modelSpec.options\n }\n const specPath = joinPath(config.prepDir, 'spec.json')\n await writeFile(specPath, JSON.stringify(specJson, null, 2))\n\n // Read the hash from the last successful model build, if available\n const modelHashPath = joinPath(config.prepDir, 'model-hash.txt')\n let previousModelHash: string\n if (existsSync(modelHashPath)) {\n previousModelHash = readFileSync(modelHashPath, 'utf8')\n } else {\n previousModelHash = 'NONE'\n }\n\n // The code gen and Wasm build steps are time consuming, so we avoid rebuilding\n // it if the build input files are unchanged since the last successful build\n const inputFilesHash = await computeInputFilesHash(config)\n let needModelGen: boolean\n if (options.forceModelGen === true) {\n needModelGen = true\n } else {\n const hashMismatch = inputFilesHash !== previousModelHash\n needModelGen = hashMismatch\n }\n\n let succeeded = true\n try {\n if (needModelGen) {\n // Generate the model\n await generateModel(context, plugins)\n\n // Save the hash of the input files, which can be used to determine if\n // we need to rebuild the model the next time\n writeFileSync(modelHashPath, inputFilesHash)\n } else {\n // Skip code generation\n log('info', 'Skipping model code generation; already up-to-date')\n }\n\n // Run plugins that implement `postGenerate`\n // TODO: For now we run all plugins even if one or more of them returns\n // false, which allows (in theory) for there to be multiple plugins that\n // run tests or checks as a post-build step. We should eventually make\n // this configurable so that a plugin can halt the build if it fails.\n // (Technically this is already possible if the plugin throws an error\n // instead of returning false, but maybe it should be more configurable.)\n for (const plugin of plugins) {\n if (plugin.postGenerate) {\n const pluginSucceeded = await plugin.postGenerate(context, modelSpec)\n if (!pluginSucceeded) {\n succeeded = false\n }\n }\n }\n\n // Copy staged files to their destination; this will only copy the staged\n // files if they are different than the existing destination files. We\n // copy the files in a batch like this so that hot module reload is only\n // triggered once at the end of the whole build process.\n stagedFiles.copyChangedFiles()\n\n // Run plugins that implement `postBuild` (this is specified to run after\n // the \"copy staged files\" step)\n for (const plugin of plugins) {\n if (plugin.postBuild) {\n const pluginSucceeded = await plugin.postBuild(context, modelSpec)\n if (!pluginSucceeded) {\n succeeded = false\n }\n }\n }\n\n if (config.mode === 'development') {\n // Hide the \"rebuilding\" message in the dev overlay in the app if the\n // build succeeded; otherwise keep the error message visible\n log('info', 'Waiting for changes...\\n')\n clearOverlay()\n }\n } catch (e) {\n // When a build is aborted, the error will have \"ABORT\" as the message,\n // in which case we can swallow the error; for actual errors, rethrow\n if (e.message !== 'ABORT') {\n // Clear the hash so that the model is rebuilt next time\n writeFileSync(modelHashPath, '')\n\n // Return an error result\n return err(e)\n }\n }\n\n return ok(succeeded)\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport type { ChildProcess } from 'child_process'\n\nimport { spawn } from 'cross-spawn'\n\nimport { log } from '../_shared/log'\n\n/**\n * @hidden This isn't ready to be included in the public API just yet.\n */\nexport interface ProcessOptions {\n logOutput?: boolean\n ignoredMessageFilter?: string\n captureOutput?: boolean\n ignoreError?: boolean\n}\n\n/**\n * @hidden This isn't ready to be included in the public API just yet.\n */\nexport interface ProcessOutput {\n exitCode: number\n stdoutMessages: string[]\n stderrMessages: string[]\n}\n\n/**\n * Spawn a child process that runs the given command.\n *\n * @param cwd The directory in which the command will be executed.\n * @param command The command to execute.\n * @param args The arguments to pass to the command.\n * @param abortSignal The signal used to abort the process.\n * @param opts Additional options to configure the process.\n * @returns The output of the process.\n */\nexport function spawnChild(\n cwd: string,\n command: string,\n args: string[],\n abortSignal?: AbortSignal,\n opts?: ProcessOptions\n): Promise<ProcessOutput> {\n return new Promise((resolve, reject) => {\n if (abortSignal?.aborted) {\n reject(new Error('ABORT'))\n return\n }\n\n let childProc: ChildProcess\n\n const localLog = (s: string, err = false) => {\n // Don't log anything after the process has been killed\n if (childProc === undefined) {\n return\n }\n log(err ? 'error' : 'info', s)\n }\n\n const abortHandler = () => {\n if (childProc) {\n log('info', 'Killing existing build process...')\n childProc.kill('SIGKILL')\n childProc = undefined\n }\n reject(new Error('ABORT'))\n }\n\n // Kill the process if abort is requested\n abortSignal?.addEventListener('abort', abortHandler, { once: true })\n\n // Prepare for capturing output, if requested\n const stdoutMessages: string[] = []\n const stderrMessages: string[] = []\n const logMessage = (msg: string, err: boolean) => {\n let includeMessage = true\n if (opts?.ignoredMessageFilter && msg.trim().startsWith(opts.ignoredMessageFilter)) {\n includeMessage = false\n }\n if (includeMessage) {\n const lines = msg.trim().split('\\n')\n for (const line of lines) {\n localLog(` ${line}`, err)\n }\n }\n }\n\n // Spawn the (asynchronous) child process. Note that we are using `spawn`\n // from the `cross-spawn` package as an alternative to the built-in\n // `child_process` module, which doesn't handle spaces in command path\n // on Windows.\n childProc = spawn(command, args, {\n cwd\n })\n\n childProc.stdout.on('data', (data: Buffer) => {\n const msg = data.toString()\n if (opts?.captureOutput === true) {\n stdoutMessages.push(msg)\n }\n if (opts?.logOutput !== false) {\n logMessage(msg, false)\n }\n })\n childProc.stderr.on('data', (data: Buffer) => {\n const msg = data.toString()\n if (opts?.captureOutput === true) {\n stderrMessages.push(msg)\n }\n if (opts?.logOutput !== false) {\n logMessage(msg, true)\n }\n })\n childProc.on('error', err => {\n localLog(`Process error: ${err}`, true)\n })\n childProc.on('close', (code, signal) => {\n // Stop listening for abort events\n abortSignal?.removeEventListener('abort', abortHandler)\n childProc = undefined\n\n if (signal) {\n // The process was killed by a signal, so we don't need to print anything\n return\n }\n\n const processOutput: ProcessOutput = {\n exitCode: code,\n stdoutMessages,\n stderrMessages\n }\n\n if (code === 0) {\n // The process exited cleanly; resolve the promise\n resolve(processOutput)\n } else if (!signal) {\n // The process failed\n if (opts?.ignoreError === true) {\n // Resolve the promise (but with a non-zero exit code)\n resolve(processOutput)\n } else {\n // Reject the promise\n reject(new Error(`Child process failed (code=${code})`))\n }\n }\n })\n })\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport type { LogLevel } from '../_shared/log'\nimport { log } from '../_shared/log'\n\nimport type { ResolvedConfig } from '../_shared/resolved-config'\n\nimport type { ProcessOptions, ProcessOutput } from './spawn-child'\nimport { spawnChild } from './spawn-child'\nimport type { StagedFiles } from './staged-files'\n\n/**\n * Provides access to common functionality that is needed during the build process.\n * This is passed to most plugin functions.\n */\nexport class BuildContext {\n /**\n * @param config The resolved configuration.\n * @hidden\n */\n constructor(\n public readonly config: ResolvedConfig,\n private readonly stagedFiles: StagedFiles,\n private readonly abortSignal: AbortSignal | undefined\n ) {}\n\n /**\n * Log a message to the console and/or the in-browser overlay panel.\n *\n * @param level The log level (verbose, info, error).\n * @param msg The message.\n */\n log(level: LogLevel, msg: string): void {\n log(level, msg)\n }\n\n /**\n * Prepare for writing a file to the staged directory.\n *\n * This will add the path to the array of tracked files and will create the\n * staged directory if needed.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file will be written (this must be a relative path).\n * @param srcFile The name of the file as written to the `staged` directory.\n * @param dstDir The absolute path to the destination directory where the staged\n * file will be copied when the build has completed.\n * @param dstFile The name of the file as written to the destination directory.\n * @return The absolute path to the staged file.\n */\n prepareStagedFile(srcDir: string, srcFile: string, dstDir: string, dstFile: string): string {\n return this.stagedFiles.prepareStagedFile(srcDir, srcFile, dstDir, dstFile)\n }\n\n /**\n * Write a file to the staged directory.\n *\n * This file will be copied (along with other staged files) into the destination\n * directory only after the build process has completed. Copying all staged files\n * at once helps improve the local development experience by making it so that\n * live reloading tools only need to refresh once instead of every time a build\n * file is written.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file will be written (this must be a relative path).\n * @param dstDir The absolute path to the destination directory where the staged\n * file will be copied when the build has completed.\n * @param filename The name of the file.\n * @param content The file content.\n */\n writeStagedFile(srcDir: string, dstDir: string, filename: string, content: string): void {\n this.stagedFiles.writeStagedFile(srcDir, dstDir, filename, content)\n }\n\n /**\n * Spawn a child process that runs the given command.\n *\n * @param cwd The directory in which the command will be executed.\n * @param command The command to execute.\n * @param args The arguments to pass to the command.\n * @param opts Additional options to configure the process.\n * @returns The output of the process.\n */\n spawnChild(cwd: string, command: string, args: string[], opts?: ProcessOptions): Promise<ProcessOutput> {\n return spawnChild(cwd, command, args, this.abortSignal, opts)\n }\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'fs'\nimport { join as joinPath } from 'path'\n\nimport { log } from '../_shared/log'\n\ninterface StagedFile {\n srcDir: string\n srcFile: string\n dstDir: string\n dstFile: string\n}\n\nexport class StagedFiles {\n private readonly baseStagedDir: string\n private readonly stagedFiles: StagedFile[] = []\n\n constructor(prepDir: string) {\n this.baseStagedDir = joinPath(prepDir, 'staged')\n }\n\n /**\n * Prepare for writing a file to the staged directory.\n *\n * This will add the path to the array of tracked files and will create the\n * staged directory if needed.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file will be written (this must be a relative path).\n * @param srcFile The name of the file as written to the `staged` directory.\n * @param dstDir The absolute path to the destination directory where the staged\n * file will be copied when the build has completed.\n * @param dstFile The name of the file as written to the destination directory.\n * @return The absolute path to the staged file.\n */\n prepareStagedFile(srcDir: string, srcFile: string, dstDir: string, dstFile: string): string {\n // Add an entry to the array of staged files (only if an entry does not already\n // exist) so that we can copy the file to the destination directory when the build\n // process has completed\n const stagedFile = {\n srcDir,\n srcFile,\n dstDir,\n dstFile\n }\n // TODO: We allow there to be more than one entry for each source path to\n // support the case where a single source file gets copied to multiple\n // destination paths. But we should throw an error if the same destination\n // path is configured for different source paths.\n if (this.stagedFiles.indexOf(stagedFile) < 0) {\n this.stagedFiles.push(stagedFile)\n }\n\n // Create the directory underneath the staged directory if needed\n const stagedDir = joinPath(this.baseStagedDir, srcDir)\n if (!existsSync(stagedDir)) {\n mkdirSync(stagedDir, { recursive: true })\n }\n\n return joinPath(stagedDir, srcFile)\n }\n\n /**\n * Write a file to the staged directory.\n *\n * This file will be copied (along with other staged files) into the destination\n * directory only after the build process has completed. Copying all staged files\n * at once helps improve the local development experience by making it so that\n * live reloading tools only need to refresh once instead of every time a build\n * file is written.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file will be written (this must be a relative path).\n * @param dstDir The absolute path to the destination directory where the staged\n * file will be copied when the build has completed.\n * @param filename The name of the file.\n * @param content The file content.\n */\n writeStagedFile(srcDir: string, dstDir: string, filename: string, content: string): void {\n // Add an entry to track the file and create the staged directory if needed\n const stagedFilePath = this.prepareStagedFile(srcDir, filename, dstDir, filename)\n\n // Write the file to the staged directory\n writeFileSync(stagedFilePath, content)\n }\n\n /**\n * Return the absolute path to the staged file for the given source directory and file name.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file would be written initially (this must be a relative path).\n * @param srcFile The name of the file.\n */\n getStagedFilePath(srcDir: string, srcFile: string): string {\n return joinPath(this.baseStagedDir, srcDir, srcFile)\n }\n\n /**\n * Return true if the staged file exists for the given source directory and file name.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file would be written initially (this must be a relative path).\n * @param srcFile The name of the file.\n */\n stagedFileExists(srcDir: string, srcFile: string): boolean {\n const fullSrcPath = this.getStagedFilePath(srcDir, srcFile)\n return existsSync(fullSrcPath)\n }\n\n /**\n * Return true if the destination file exists for the given source directory and file name.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file would be written initially (this must be a relative path).\n * @param srcFile The name of the file.\n */\n destinationFileExists(srcDir: string, srcFile: string): boolean {\n const f = this.stagedFiles.find(f => f.srcDir === srcDir && f.srcFile === srcFile)\n if (f === undefined) {\n return false\n }\n const fullDstPath = joinPath(f.dstDir, f.dstFile)\n return existsSync(fullDstPath)\n }\n\n /**\n * Copy staged files to their destination; this will only copy the staged\n * files if they are different than the existing destination files. We\n * copy the files in a batch like this so that hot module reload is only\n * triggered once at the end of the whole build process.\n */\n copyChangedFiles(): void {\n log('info', 'Copying changed files into place...')\n\n for (const f of this.stagedFiles) {\n this.copyStagedFile(f)\n }\n\n log('info', 'Done copying files')\n }\n\n /**\n * Copy a file from the `staged` directory to its destination. If the file already\n * exists in the destination directory and has the same contents as the source file,\n * the file will not be copied and this function will return false.\n *\n * @param f The staged file entry.\n */\n private copyStagedFile(f: StagedFile): boolean {\n // Create the destination directory, if needed\n if (!existsSync(f.dstDir)) {\n mkdirSync(f.dstDir, { recursive: true })\n }\n\n // If the destination file already exists and has the same contents as the source\n // file, we can skip copying it\n const fullSrcPath = this.getStagedFilePath(f.srcDir, f.srcFile)\n const fullDstPath = joinPath(f.dstDir, f.dstFile)\n const needsCopy = filesDiffer(fullSrcPath, fullDstPath)\n if (needsCopy) {\n log('verbose', ` Copying ${f.srcFile} to ${fullDstPath}`)\n copyFileSync(fullSrcPath, fullDstPath)\n }\n return needsCopy\n }\n}\n\n/**\n * Return true if both files exist at the given paths and have the same contents, false otherwise.\n */\nfunction filesDiffer(aPath: string, bPath: string): boolean {\n if (existsSync(aPath) && existsSync(bPath)) {\n // The files exist; see if they are different\n const aSize = statSync(aPath).size\n const bSize = statSync(bPath).size\n if (aSize !== bSize) {\n // The sizes are different, so the contents must be different\n return true\n } else {\n // The sizes are the same, so check the contents\n const aBuf = readFileSync(aPath)\n const bBuf = readFileSync(bPath)\n return !aBuf.equals(bBuf)\n }\n } else {\n // One or both files do not exist\n return true\n }\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { copyFile, readdir, readFile, writeFile } from 'fs/promises'\nimport { join as joinPath } from 'path'\n\nimport { log } from '../../_shared/log'\n\nimport type { BuildContext } from '../../context/context'\n\nimport type { Plugin } from '../../plugin/plugin'\n\n/**\n * Generate the model. This will run the core SDEverywhere code generation steps\n * and will also invoke the following plugin functions:\n * - `preProcessMdl`\n * - `postProcessMdl`\n * - `preGenerateC`\n * - `postGenerateC`\n */\nexport async function generateModel(context: BuildContext, plugins: Plugin[]): Promise<void> {\n log('info', 'Generating model...')\n\n const t0 = performance.now()\n\n // Use the defined prep directory\n const config = context.config\n const prepDir = config.prepDir\n\n // TODO: For now we assume the path is to the `main.js` file in the cli package;\n // this seems to work on both Unix and Windows, but we may need to revisit this\n // as part of removing the `sdeCmdPath` config hack\n const sdeCmdPath = config.sdeCmdPath\n\n // Process the mdl file(s)\n for (const plugin of plugins) {\n if (plugin.preProcessMdl) {\n await plugin.preProcessMdl(context)\n }\n }\n if (config.modelFiles.length === 0) {\n // Require at least one input file\n throw new Error('No model input files specified')\n } else if (config.modelFiles.length === 1) {\n // Preprocess the single mdl file\n await preprocessMdl(context, sdeCmdPath, prepDir, config.modelFiles[0])\n } else {\n // Flatten and preprocess the multiple mdl files into a single mdl file\n await flattenMdls(context, sdeCmdPath, prepDir, config.modelFiles)\n }\n for (const plugin of plugins) {\n if (plugin.postProcessMdl) {\n const mdlPath = joinPath(prepDir, 'processed.mdl')\n let mdlContent = await readFile(mdlPath, 'utf8')\n mdlContent = await plugin.postProcessMdl(context, mdlContent)\n await writeFile(mdlPath, mdlContent)\n }\n }\n\n // Generate the C file\n for (const plugin of plugins) {\n if (plugin.preGenerateC) {\n await plugin.preGenerateC(context)\n }\n }\n await generateC(context, config.sdeDir, sdeCmdPath, prepDir)\n for (const plugin of plugins) {\n if (plugin.postGenerateC) {\n const cPath = joinPath(prepDir, 'build', 'processed.c')\n let cContent = await readFile(cPath, 'utf8')\n cContent = await plugin.postGenerateC(context, cContent)\n await writeFile(cPath, cContent)\n }\n }\n\n const t1 = performance.now()\n const elapsed = ((t1 - t0) / 1000).toFixed(1)\n log('info', `Done generating model (${elapsed}s)`)\n}\n\n/**\n * Preprocess a single mdl file and copy the resulting `processed.mdl` to the prep directory.\n */\nasync function preprocessMdl(\n context: BuildContext,\n sdeCmdPath: string,\n prepDir: string,\n modelFile: string\n): Promise<void> {\n log('verbose', ' Preprocessing mdl file')\n\n // Copy the source file to the prep directory to make the next steps easier\n await copyFile(modelFile, joinPath(prepDir, 'processed.mdl'))\n\n // Use SDE to preprocess the model to strip anything that's not needed to build it\n const command = sdeCmdPath\n const args = ['generate', '--preprocess', 'processed.mdl']\n await context.spawnChild(prepDir, command, args)\n\n // Copy the processed file back to the prep directory\n await copyFile(joinPath(prepDir, 'build', 'processed.mdl'), joinPath(prepDir, 'processed.mdl'))\n}\n\n/**\n * Flatten multiple mdl files and copy the resulting `processed.mdl` to the prep directory.\n */\nasync function flattenMdls(\n context: BuildContext,\n sdeCmdPath: string,\n prepDir: string,\n modelFiles: string[]\n): Promise<void> {\n log('verbose', ' Flattening and preprocessing mdl files')\n\n // Use SDE to flatten the parent model and submodels into a single mdl file,\n // then preprocess to strip anything that's not needed to build the model\n const command = sdeCmdPath\n const args: string[] = []\n args.push('flatten')\n args.push('processed.mdl')\n args.push('--inputs')\n for (const path of modelFiles) {\n args.push(path)\n }\n\n // Disable logging by default; this will suppress flatten warnings, which are\n // sometimes unavoidable and not helpful\n const output = await context.spawnChild(prepDir, command, args, {\n logOutput: false,\n captureOutput: true,\n ignoreError: true\n })\n\n // Check for flattening errors\n let flattenErrors = false\n for (const msg of output.stderrMessages) {\n if (msg.includes('ERROR')) {\n flattenErrors = true\n break\n }\n }\n if (flattenErrors) {\n log('error', 'There were errors reported when flattening the model:')\n for (const msg of output.stderrMessages) {\n const lines = msg.split('\\n')\n for (const line of lines) {\n log('error', ` ${line}`)\n }\n }\n throw new Error(`Flatten command failed (code=${output.exitCode})`)\n } else if (output.exitCode !== 0) {\n throw new Error(`Flatten command failed (code=${output.exitCode})`)\n }\n\n // Copy the processed file back to the prep directory\n await copyFile(joinPath(prepDir, 'build', 'processed.mdl'), joinPath(prepDir, 'processed.mdl'))\n}\n\n/**\n * Generate a C file from the `processed.mdl` file.\n */\nasync function generateC(context: BuildContext, sdeDir: string, sdeCmdPath: string, prepDir: string): Promise<void> {\n log('verbose', ' Generating C code')\n\n // Use SDE to generate a C version of the model\n const command = sdeCmdPath\n const args = ['generate', '--genc', '--spec', 'spec.json', 'processed']\n await context.spawnChild(prepDir, command, args, {\n // By default, ignore lines that start with \"WARNING: Data for\" since these are often harmless\n // TODO: Don't filter by default, but make it configurable\n // ignoredMessageFilter: 'WARNING: Data for'\n })\n\n // Copy SDE's supporting C files into the build directory\n const buildDir = joinPath(prepDir, 'build')\n const sdeCDir = joinPath(sdeDir, 'src', 'c')\n const files = await readdir(sdeCDir)\n const copyOps = []\n for (const file of files) {\n if (file.endsWith('.c') || file.endsWith('.h')) {\n copyOps.push(copyFile(joinPath(sdeCDir, file), joinPath(buildDir, file)))\n }\n }\n await Promise.all(copyOps)\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { join as joinPath } from 'path'\nimport { hashElement } from 'folder-hash'\nimport glob from 'tiny-glob'\n\nimport type { ResolvedConfig } from '../../_shared/resolved-config'\n\n/**\n * Asynchronously compute the hash of the files that are inputs to the model\n * build process.\n */\nexport async function computeInputFilesHash(config: ResolvedConfig): Promise<string> {\n const inputFiles: string[] = []\n\n // Always include the `spec.json` file, since that is a primary input\n // to the model build process\n const specFile = joinPath(config.prepDir, 'spec.json')\n inputFiles.push(specFile)\n\n if (config.modelInputPaths && config.modelInputPaths.length > 0) {\n // Include the files that match the glob patterns in the config file.\n // Note that the folder-hash package supports glob patterns, but its\n // configuration is complicated, so it is easier if we resolve the\n // glob patterns here and pass each individual file to the\n // `hashElement` function.\n for (const globPath of config.modelInputPaths) {\n const paths = await glob(globPath, {\n cwd: config.rootDir,\n absolute: true,\n filesOnly: true\n })\n inputFiles.push(...paths)\n }\n } else {\n // Only use the mdl files to compute the hash\n inputFiles.push(...config.modelFiles)\n }\n\n // Compute the hash of each input file and concatenate into a single string\n let hash = ''\n for (const inputFile of inputFiles) {\n const result = await hashElement(inputFile)\n hash += result.hash\n }\n\n return hash\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { basename } from 'path'\n\nimport chokidar from 'chokidar'\n\nimport { clearOverlay, log, logError } from '../../_shared/log'\nimport type { ResolvedConfig } from '../../_shared/resolved-config'\n\nimport type { UserConfig } from '../../config/user-config'\nimport type { Plugin } from '../../plugin/plugin'\n\nimport type { BuildOnceOptions } from './build-once'\nimport { buildOnce } from './build-once'\n\nclass BuildState {\n readonly abortController = new AbortController()\n}\n\nexport function watch(config: ResolvedConfig, userConfig: UserConfig, plugins: Plugin[]): void {\n // Add a small delay so that if multiple files are changed at once (as is\n // often the case when switching branches), we batch them up and start the\n // the build after things settle down\n const delay = 150\n const changedPaths: Set<string> = new Set()\n\n // Keep track of the current build so that we only have one active at a time\n let currentBuildState: BuildState\n\n function performBuild() {\n clearOverlay()\n\n // Log the input files that have changed\n for (const path of changedPaths) {\n log('info', `Input file ${basename(path)} has been changed`)\n }\n\n // Clear the set of pending files\n changedPaths.clear()\n\n // Keep track of builds; if one is already in progress, abort it\n // before starting another one\n if (currentBuildState) {\n currentBuildState.abortController.abort()\n // TODO: Prevent aborted build from logging?\n currentBuildState = undefined\n }\n\n // Generate files and build the model\n currentBuildState = new BuildState()\n const buildOptions: BuildOnceOptions = {\n abortSignal: currentBuildState.abortController.signal\n }\n buildOnce(config, userConfig, plugins, buildOptions)\n .catch(e => {\n logError(e)\n // log('info', 'Waiting for changes...')\n })\n .finally(() => {\n currentBuildState = undefined\n })\n }\n\n function scheduleBuild(changedPath: string) {\n // Only schedule the build if the set is currently empty\n const schedule = changedPaths.size === 0\n\n // Add the path to the set of changed files\n changedPaths.add(changedPath)\n\n if (schedule) {\n // Schedule the build to start after a delay\n setTimeout(() => {\n performBuild()\n }, delay)\n }\n }\n\n let watchPaths: string[]\n if (config.watchPaths && config.watchPaths.length > 0) {\n // Watch the configured files\n watchPaths = config.watchPaths\n } else {\n // Only watch the mdl files\n watchPaths = config.modelFiles\n }\n\n // Watch the config and model files; if changes are detected, generate the specs\n // and rebuild the model if needed\n const watcher = chokidar.watch(watchPaths, {\n // Watch paths are resolved relative to the project root directory\n cwd: config.rootDir,\n // XXX: Include a delay, otherwise on macOS we sometimes get multiple\n // change events when the csv file is saved just once\n awaitWriteFinish: {\n stabilityThreshold: 200\n }\n })\n watcher.on('change', path => {\n scheduleBuild(path)\n })\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/build/build.ts","../src/config/config-loader.ts","../src/_shared/log.ts","../src/build/impl/build-once.ts","../src/context/spawn-child.ts","../src/context/context.ts","../src/context/staged-files.ts","../src/build/impl/gen-model.ts","../src/build/impl/hash-files.ts","../src/build/impl/watch.ts"],"sourcesContent":["// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { join as joinPath } from 'path'\n\nimport type { Result } from 'neverthrow'\nimport { err, ok } from 'neverthrow'\n\nimport type { BuildMode } from '../_shared/mode'\n\nimport { loadConfig } from '../config/config-loader'\nimport type { UserConfig } from '../config/user-config'\nimport type { LogLevel } from '../_shared/log'\nimport { setOverlayFile, setActiveLevels } from '../_shared/log'\nimport { buildOnce } from './impl/build-once'\nimport { watch } from './impl/watch'\n\nexport interface BuildOptions {\n /** The path to an `sde.config.js` file, or a `UserConfig` object. */\n config?: string | UserConfig\n\n /**\n * The log levels to include. If undefined, the default 'info' and 'error' levels\n * will be active.\n */\n logLevels?: LogLevel[]\n\n /**\n * The path to the `@sdeverywhere/cli` package. This is currently only used to get\n * access to the files in the `src/c` directory.\n * @hidden This should be removed once we have tighter integration with the `cli` package.\n */\n sdeDir: string\n\n /**\n * The path to the `sde` command.\n * @hidden This should be removed once we have tighter integration with the `cli` package.\n */\n sdeCmdPath: string\n}\n\nexport interface BuildResult {\n /**\n * The exit code that should be set by the process. This will be undefined\n * if `mode` is 'development', indicating that the process should be kept alive.\n */\n exitCode?: number\n}\n\n/**\n * Initiate the build process, which can either be a single build if `mode` is\n * 'production', or a live development environment if `mode` is 'development'.\n *\n * @param mode The build mode.\n * @param options The build options.\n * @return An `ok` result if the build completed, otherwise an `err` result.\n */\nexport async function build(mode: BuildMode, options: BuildOptions): Promise<Result<BuildResult, Error>> {\n // Load the config\n const configResult = await loadConfig(mode, options.config, options.sdeDir, options.sdeCmdPath)\n if (configResult.isErr()) {\n return err(configResult.error)\n }\n const { userConfig, resolvedConfig } = configResult.value\n\n // Configure logging level\n if (options.logLevels !== undefined) {\n setActiveLevels(options.logLevels)\n }\n\n // Configure the overlay `messages.html` file, which is written under the\n // `sde-prep` directory. For production builds, this file will remain empty.\n const messagesPath = joinPath(resolvedConfig.prepDir, 'messages.html')\n const overlayEnabled = mode === 'development'\n setOverlayFile(messagesPath, overlayEnabled)\n\n // Initialize plugins\n const plugins = userConfig.plugins || []\n for (const plugin of plugins) {\n if (plugin.init) {\n await plugin.init(resolvedConfig)\n }\n }\n\n try {\n const plugins = userConfig.plugins || []\n\n if (mode === 'development') {\n // Enable dev mode (which will rebuild when watched files are changed).\n // First run an initial build so that we have a baseline, and then\n // once that is complete, enable file watchers.\n const buildResult = await buildOnce(resolvedConfig, userConfig, plugins, {})\n // TODO: We should trap errors here and keep the dev process\n // running if the initial build fails\n if (buildResult.isErr()) {\n return err(buildResult.error)\n }\n\n // Allow plugins to set up watchers after the initial build completes\n for (const plugin of plugins) {\n if (plugin.watch) {\n await plugin.watch(resolvedConfig)\n }\n }\n\n // Watch for changes to the source/model/test files\n watch(resolvedConfig, userConfig, plugins)\n\n // Return a build result with undefined exit code, indicating that the\n // process should be kept alive\n return ok({})\n } else {\n // Run a single build\n const buildResult = await buildOnce(resolvedConfig, userConfig, plugins, {})\n if (buildResult.isErr()) {\n return err(buildResult.error)\n }\n\n // Configure the exit code depending on whether any plugins failed.\n // Currently we use the following exit code values:\n // 0 == build succeeded, AND all plugins succeeded\n // 1 == build failed (or a plugin reported a \"hard\" error)\n // 2 == build succeeded, BUT one or more plugins failed\n const allPluginsSucceeded = buildResult.value\n const exitCode = allPluginsSucceeded ? 0 : 2\n return ok({ exitCode })\n }\n } catch (e) {\n return err(e)\n }\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { existsSync, lstatSync, mkdirSync } from 'fs'\nimport { dirname, join as joinPath, relative, resolve as resolvePath } from 'path'\nimport { fileURLToPath } from 'url'\n\nimport type { Result } from 'neverthrow'\nimport { err, ok } from 'neverthrow'\n\nimport type { BuildMode } from '../_shared/mode'\nimport type { ResolvedConfig } from '../_shared/resolved-config'\n\nimport type { UserConfig } from './user-config'\n\nexport interface ConfigResult {\n userConfig: UserConfig\n resolvedConfig: ResolvedConfig\n}\n\n/**\n * Load a user-defined config file or a given `UserConfig` object. This validates\n * all paths and if successful, this returns a `ResolvedConfig`, otherwise returns\n * an error result.\n *\n * @param mode The build mode.\n * @param config The path to a config file, or a `UserConfig` object; if undefined,\n * this will look for a `sde.config.js` file in the current directory.\n * @param sdeDir Temporary (the path to the `@sdeverywhere/cli` package).\n * @param sdeCmdPath Temporary (the path to the `sde` command).\n * @return An `ok` result with the `ResolvedConfig`, otherwise an `err` result.\n */\nexport async function loadConfig(\n mode: BuildMode,\n config: string | UserConfig | undefined,\n sdeDir: string,\n sdeCmdPath: string\n): Promise<Result<ConfigResult, Error>> {\n let userConfig: UserConfig\n if (typeof config === 'object') {\n // Use the given `UserConfig` object\n userConfig = config\n } else {\n // Load the project-specific config. If no `--config` arg was specified\n // on the command line, look for a `sde.config.js` file in the current\n // directory, and failing that, use a default config.\n // TODO: Create a default config if no file found; for now, we just fail\n let configPath: string\n if (typeof config === 'string') {\n configPath = config\n } else {\n configPath = joinPath(process.cwd(), 'sde.config.js')\n }\n try {\n if (!existsSync(configPath)) {\n return err(new Error(`Cannot find config file '${configPath}'`))\n }\n const configRelPath = relativeToSourcePath(configPath)\n const configModule = await import(configRelPath)\n userConfig = await configModule.config()\n } catch (e) {\n return err(new Error(`Failed to load config file '${configPath}': ${e.message}`))\n }\n }\n\n // Resolve the config\n try {\n const resolvedConfig = resolveUserConfig(userConfig, mode, sdeDir, sdeCmdPath)\n return ok({\n userConfig,\n resolvedConfig\n })\n } catch (e) {\n return err(e)\n }\n}\n\n/**\n * Resolve the given user configuration by resolving all paths. This will create\n * the prep directory if it does not already exist. This will throw an error if\n * any other paths are invalid or do not exist.\n *\n * @param userConfig The user-defined configuration.\n * @param mode The active build mode.\n * @param sdeDir Temporary (the path to the `@sdeverywhere/cli` package).\n * @param sdeCmdPath Temporary (the path to the `sde` command).\n * @return The resolved configuration.\n */\nfunction resolveUserConfig(\n userConfig: UserConfig,\n mode: BuildMode,\n sdeDir: string,\n sdeCmdPath: string\n): ResolvedConfig {\n function expectDirectory(propName: string, path: string): void {\n if (!existsSync(path)) {\n throw new Error(`The configured ${propName} (${path}) does not exist`)\n } else if (!lstatSync(path).isDirectory()) {\n throw new Error(`The configured ${propName} (${path}) is not a directory`)\n }\n }\n\n // function expectFile(propName: string, path: string): void {\n // if (!existsSync(path)) {\n // throw new Error(`The configured ${propName} (${path}) does not exist`)\n // // TODO: Don't include the \"is file\" check for now; need to update this to\n // // handle symlinks\n // // } else if (!lstatSync(path).isFile()) {\n // // throw new Error(`The configured ${propName} (${path}) is not a file`)\n // }\n // }\n\n // Validate the root directory\n let rootDir: string\n if (userConfig.rootDir) {\n // Verify that the configured root directory exists\n rootDir = resolvePath(userConfig.rootDir)\n expectDirectory('rootDir', rootDir)\n } else {\n // Use the current working directory as the project root\n rootDir = process.cwd()\n }\n\n // Validate the prep directory\n let prepDir: string\n if (userConfig.prepDir) {\n // Resolve the configured prep directory\n prepDir = resolvePath(userConfig.prepDir)\n } else {\n // Create an 'sde-prep' directory under the configured root directory\n prepDir = resolvePath(rootDir, 'sde-prep')\n }\n mkdirSync(prepDir, { recursive: true })\n\n // Validate the model files\n const userModelFiles = userConfig.modelFiles\n const modelFiles: string[] = []\n for (const userModelFile of userModelFiles) {\n const modelFile = resolvePath(userModelFile)\n if (!existsSync(modelFile)) {\n throw new Error(`The configured model file (${modelFile}) does not exist`)\n }\n modelFiles.push(modelFile)\n }\n\n // TODO: Validate the watch paths; these are allowed to be globs, so need to\n // figure out the best way to resolve them\n let modelInputPaths: string[]\n if (userConfig.modelInputPaths && userConfig.modelInputPaths.length > 0) {\n modelInputPaths = userConfig.modelInputPaths\n } else {\n modelInputPaths = modelFiles\n }\n let watchPaths: string[]\n if (userConfig.watchPaths && userConfig.watchPaths.length > 0) {\n watchPaths = userConfig.watchPaths\n } else {\n watchPaths = modelFiles\n }\n\n return {\n mode,\n rootDir,\n prepDir,\n modelFiles,\n modelInputPaths,\n watchPaths,\n sdeDir,\n sdeCmdPath\n }\n}\n\n/**\n * Return a Unix-style path (e.g. '../../foo.js') that is relative to the directory of\n * the current source file. This can be used to construct a path that is safe for\n * dynamic import on either Unix or Windows.\n *\n * @param filePath The path to make relative.\n */\nfunction relativeToSourcePath(filePath: string): string {\n const srcDir = dirname(fileURLToPath(import.meta.url))\n const relPath = relative(srcDir, filePath)\n return relPath.replaceAll('\\\\', '/')\n}\n","// Copyright (c) 2021-2022 Climate Interactive / New Venture Fund\n\nimport { writeFileSync } from 'fs'\nimport pico from 'picocolors'\n\nexport type LogLevel = 'error' | 'info' | 'verbose'\n\nconst activeLevels: Set<LogLevel> = new Set(['error', 'info'])\n\nlet overlayFile: string\nlet overlayEnabled = false\nlet overlayHtml = ''\n\n/**\n * Set the active logging levels. By default, only 'error' and 'info'\n * messages are emitted.\n *\n * @param logLevels The logging levels to include.\n */\nexport function setActiveLevels(logLevels: LogLevel[]): void {\n activeLevels.clear()\n for (const level of logLevels) {\n activeLevels.add(level)\n }\n}\n\n/**\n * Set the path to the `messages.html` file where overlay messages will be written.\n *\n * @param file The absolute path to the HTML file where messages will be written.\n * @param enabled Whether to write messages to the file; if false, the file will be\n * emptied and no further messages will be written.\n */\nexport function setOverlayFile(file: string, enabled: boolean): void {\n overlayFile = file\n overlayEnabled = enabled\n\n // Write an empty file by default; this will ensure that messages from a previous\n // build aren't included in the current build\n writeFileSync(overlayFile, '')\n}\n\n/**\n * Log a message to the console and/or overlay.\n *\n * @param level The logging level.\n * @param msg The message to emit.\n */\nexport function log(level: LogLevel, msg: string): void {\n if (activeLevels.has(level)) {\n if (level === 'error') {\n console.error(pico.red(msg))\n logToOverlay(msg)\n } else {\n console.log(msg)\n logToOverlay(msg)\n }\n }\n}\n\n/**\n * Log an error to the console and/or overlay.\n *\n * @param e The error to log.\n */\nexport function logError(e: Error): void {\n // Remove the first part of the stack trace (which contains the message)\n // so that we can control the formatting of the message separately, then\n // only include up to 3 lines of the stack to keep the log cleaner\n const stack = e.stack || ''\n const stackLines = stack.split('\\n').filter(s => s.match(/^\\s+at/))\n const trace = stackLines.slice(0, 3).join('\\n')\n\n // Log the error message followed by the stack trace\n console.error(pico.red(`\\nERROR: ${e.message}`))\n console.error(pico.dim(pico.red(`${trace}\\n`)))\n logToOverlay(`\\nERROR: ${e.message}`, true)\n logToOverlay(`${trace}\\n`, true)\n}\n\nfunction writeOverlayFiles(): void {\n writeFileSync(overlayFile, overlayHtml)\n}\n\nexport function clearOverlay(): void {\n if (!overlayEnabled) {\n return\n }\n\n overlayHtml = ''\n writeOverlayFiles()\n}\n\nconst indent = ' '.repeat(4)\n\nexport function logToOverlay(msg: string, error = false): void {\n if (!overlayEnabled) {\n return\n }\n\n if (error) {\n msg = `<span class=\"overlay-error\">${msg}</span>`\n }\n const msgHtml = msg.replace(/\\n/g, '\\n<br/>').replace(/\\s{2}/g, indent)\n if (overlayHtml) {\n overlayHtml += `<br/>${msgHtml}`\n } else {\n overlayHtml = `${msgHtml}`\n }\n writeOverlayFiles()\n}\n\nexport function clearConsole(): void {\n // TODO: This is disabled for now; maybe re-enable it under an optional flag\n // console.log('\\x1Bc')\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { existsSync, readFileSync, writeFileSync } from 'fs'\nimport { writeFile } from 'fs/promises'\nimport { join as joinPath } from 'path'\n\nimport type { Result } from 'neverthrow'\nimport { err, ok } from 'neverthrow'\n\nimport { clearOverlay, log } from '../../_shared/log'\nimport type { ResolvedConfig } from '../../_shared/resolved-config'\n\nimport type { UserConfig } from '../../config/user-config'\nimport { BuildContext } from '../../context/context'\nimport { StagedFiles } from '../../context/staged-files'\nimport type { Plugin } from '../../plugin/plugin'\n\nimport { generateModel } from './gen-model'\nimport { computeInputFilesHash } from './hash-files'\nimport type { ModelSpec } from '../../_shared/model-spec'\n\nexport interface BuildOnceOptions {\n forceModelGen?: boolean\n abortSignal?: AbortSignal\n}\n\n/**\n * Perform a single build.\n *\n * This will return an error if a build failure occurred, or if a plugin encounters\n * an error. Otherwise, it will return true if the build and all plugins\n * succeeded, or false if a plugin wants to report a \"soft\" failure (for example,\n * if the model check plugin reports failing checks).\n *\n * @param config The resolved build configuration.\n * @param plugins The configured plugins.\n * @param options Options specific to the build process.\n * @return An `ok` result with true if the build and all plugins succeeded, or false if\n * one or more plugins failed; otherwise, an `err` result if there was a hard error.\n */\nexport async function buildOnce(\n config: ResolvedConfig,\n userConfig: UserConfig,\n plugins: Plugin[],\n options: BuildOnceOptions\n): Promise<Result<boolean, Error>> {\n // Create the build context\n const stagedFiles = new StagedFiles(config.prepDir)\n const context = new BuildContext(config, stagedFiles, options.abortSignal)\n\n // Get the model spec from the config\n let modelSpec: ModelSpec\n try {\n modelSpec = await userConfig.modelSpec(context)\n if (modelSpec === undefined) {\n return err(new Error('The model spec must be defined'))\n }\n } catch (e) {\n return err(e)\n }\n\n // Run plugins that implement `preGenerate`\n for (const plugin of plugins) {\n if (plugin.preGenerate) {\n plugin.preGenerate(context, modelSpec)\n }\n }\n\n // Write the spec file\n const specJson = {\n inputVarNames: modelSpec.inputs.map(input => input.varName),\n outputVarNames: modelSpec.outputs.map(output => output.varName),\n externalDatfiles: modelSpec.datFiles,\n ...modelSpec.options\n }\n const specPath = joinPath(config.prepDir, 'spec.json')\n await writeFile(specPath, JSON.stringify(specJson, null, 2))\n\n // Read the hash from the last successful model build, if available\n const modelHashPath = joinPath(config.prepDir, 'model-hash.txt')\n let previousModelHash: string\n if (existsSync(modelHashPath)) {\n previousModelHash = readFileSync(modelHashPath, 'utf8')\n } else {\n previousModelHash = 'NONE'\n }\n\n // The code gen and Wasm build steps are time consuming, so we avoid rebuilding\n // it if the build input files are unchanged since the last successful build\n const inputFilesHash = await computeInputFilesHash(config)\n let needModelGen: boolean\n if (options.forceModelGen === true) {\n needModelGen = true\n } else {\n const hashMismatch = inputFilesHash !== previousModelHash\n needModelGen = hashMismatch\n }\n\n let succeeded = true\n try {\n if (needModelGen) {\n // Generate the model\n await generateModel(context, plugins)\n\n // Save the hash of the input files, which can be used to determine if\n // we need to rebuild the model the next time\n writeFileSync(modelHashPath, inputFilesHash)\n } else {\n // Skip code generation\n log('info', 'Skipping model code generation; already up-to-date')\n }\n\n // Run plugins that implement `postGenerate`\n // TODO: For now we run all plugins even if one or more of them returns\n // false, which allows (in theory) for there to be multiple plugins that\n // run tests or checks as a post-build step. We should eventually make\n // this configurable so that a plugin can halt the build if it fails.\n // (Technically this is already possible if the plugin throws an error\n // instead of returning false, but maybe it should be more configurable.)\n for (const plugin of plugins) {\n if (plugin.postGenerate) {\n const pluginSucceeded = await plugin.postGenerate(context, modelSpec)\n if (!pluginSucceeded) {\n succeeded = false\n }\n }\n }\n\n // Copy staged files to their destination; this will only copy the staged\n // files if they are different than the existing destination files. We\n // copy the files in a batch like this so that hot module reload is only\n // triggered once at the end of the whole build process.\n stagedFiles.copyChangedFiles()\n\n // Run plugins that implement `postBuild` (this is specified to run after\n // the \"copy staged files\" step)\n for (const plugin of plugins) {\n if (plugin.postBuild) {\n const pluginSucceeded = await plugin.postBuild(context, modelSpec)\n if (!pluginSucceeded) {\n succeeded = false\n }\n }\n }\n\n if (config.mode === 'development') {\n // Hide the \"rebuilding\" message in the dev overlay in the app if the\n // build succeeded; otherwise keep the error message visible\n log('info', 'Waiting for changes...\\n')\n clearOverlay()\n }\n } catch (e) {\n // When a build is aborted, the error will have \"ABORT\" as the message,\n // in which case we can swallow the error; for actual errors, rethrow\n if (e.message !== 'ABORT') {\n // Clear the hash so that the model is rebuilt next time\n writeFileSync(modelHashPath, '')\n\n // Return an error result\n return err(e)\n }\n }\n\n return ok(succeeded)\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport type { ChildProcess } from 'child_process'\n\nimport { spawn } from 'cross-spawn'\n\nimport { log } from '../_shared/log'\n\n/**\n * @hidden This isn't ready to be included in the public API just yet.\n */\nexport interface ProcessOptions {\n logOutput?: boolean\n ignoredMessageFilter?: string\n captureOutput?: boolean\n ignoreError?: boolean\n}\n\n/**\n * @hidden This isn't ready to be included in the public API just yet.\n */\nexport interface ProcessOutput {\n exitCode: number\n stdoutMessages: string[]\n stderrMessages: string[]\n}\n\n/**\n * Spawn a child process that runs the given command.\n *\n * @param cwd The directory in which the command will be executed.\n * @param command The command to execute.\n * @param args The arguments to pass to the command.\n * @param abortSignal The signal used to abort the process.\n * @param opts Additional options to configure the process.\n * @returns The output of the process.\n */\nexport function spawnChild(\n cwd: string,\n command: string,\n args: string[],\n abortSignal?: AbortSignal,\n opts?: ProcessOptions\n): Promise<ProcessOutput> {\n return new Promise((resolve, reject) => {\n if (abortSignal?.aborted) {\n reject(new Error('ABORT'))\n return\n }\n\n let childProc: ChildProcess\n\n const localLog = (s: string, err = false) => {\n // Don't log anything after the process has been killed\n if (childProc === undefined) {\n return\n }\n log(err ? 'error' : 'info', s)\n }\n\n const abortHandler = () => {\n if (childProc) {\n log('info', 'Killing existing build process...')\n childProc.kill('SIGKILL')\n childProc = undefined\n }\n reject(new Error('ABORT'))\n }\n\n // Kill the process if abort is requested\n abortSignal?.addEventListener('abort', abortHandler, { once: true })\n\n // Prepare for capturing output, if requested\n const stdoutMessages: string[] = []\n const stderrMessages: string[] = []\n const logMessage = (msg: string, err: boolean) => {\n let includeMessage = true\n if (opts?.ignoredMessageFilter && msg.trim().startsWith(opts.ignoredMessageFilter)) {\n includeMessage = false\n }\n if (includeMessage) {\n const lines = msg.trim().split('\\n')\n for (const line of lines) {\n localLog(` ${line}`, err)\n }\n }\n }\n\n // Spawn the (asynchronous) child process. Note that we are using `spawn`\n // from the `cross-spawn` package as an alternative to the built-in\n // `child_process` module, which doesn't handle spaces in command path\n // on Windows.\n childProc = spawn(command, args, {\n cwd\n })\n\n childProc.stdout.on('data', (data: Buffer) => {\n const msg = data.toString()\n if (opts?.captureOutput === true) {\n stdoutMessages.push(msg)\n }\n if (opts?.logOutput !== false) {\n logMessage(msg, false)\n }\n })\n childProc.stderr.on('data', (data: Buffer) => {\n const msg = data.toString()\n if (opts?.captureOutput === true) {\n stderrMessages.push(msg)\n }\n if (opts?.logOutput !== false) {\n logMessage(msg, true)\n }\n })\n childProc.on('error', err => {\n localLog(`Process error: ${err}`, true)\n })\n childProc.on('close', (code, signal) => {\n // Stop listening for abort events\n abortSignal?.removeEventListener('abort', abortHandler)\n childProc = undefined\n\n if (signal) {\n // The process was killed by a signal, so we don't need to print anything\n return\n }\n\n const processOutput: ProcessOutput = {\n exitCode: code,\n stdoutMessages,\n stderrMessages\n }\n\n if (code === 0) {\n // The process exited cleanly; resolve the promise\n resolve(processOutput)\n } else if (!signal) {\n // The process failed\n if (opts?.ignoreError === true) {\n // Resolve the promise (but with a non-zero exit code)\n resolve(processOutput)\n } else {\n // Reject the promise\n reject(new Error(`Child process failed (code=${code})`))\n }\n }\n })\n })\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport type { LogLevel } from '../_shared/log'\nimport { log } from '../_shared/log'\n\nimport type { ResolvedConfig } from '../_shared/resolved-config'\n\nimport type { ProcessOptions, ProcessOutput } from './spawn-child'\nimport { spawnChild } from './spawn-child'\nimport type { StagedFiles } from './staged-files'\n\n/**\n * Provides access to common functionality that is needed during the build process.\n * This is passed to most plugin functions.\n */\nexport class BuildContext {\n /**\n * @param config The resolved configuration.\n * @hidden\n */\n constructor(\n public readonly config: ResolvedConfig,\n private readonly stagedFiles: StagedFiles,\n private readonly abortSignal: AbortSignal | undefined\n ) {}\n\n /**\n * Log a message to the console and/or the in-browser overlay panel.\n *\n * @param level The log level (verbose, info, error).\n * @param msg The message.\n */\n log(level: LogLevel, msg: string): void {\n log(level, msg)\n }\n\n /**\n * Prepare for writing a file to the staged directory.\n *\n * This will add the path to the array of tracked files and will create the\n * staged directory if needed.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file will be written (this must be a relative path).\n * @param srcFile The name of the file as written to the `staged` directory.\n * @param dstDir The absolute path to the destination directory where the staged\n * file will be copied when the build has completed.\n * @param dstFile The name of the file as written to the destination directory.\n * @return The absolute path to the staged file.\n */\n prepareStagedFile(srcDir: string, srcFile: string, dstDir: string, dstFile: string): string {\n return this.stagedFiles.prepareStagedFile(srcDir, srcFile, dstDir, dstFile)\n }\n\n /**\n * Write a file to the staged directory.\n *\n * This file will be copied (along with other staged files) into the destination\n * directory only after the build process has completed. Copying all staged files\n * at once helps improve the local development experience by making it so that\n * live reloading tools only need to refresh once instead of every time a build\n * file is written.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file will be written (this must be a relative path).\n * @param dstDir The absolute path to the destination directory where the staged\n * file will be copied when the build has completed.\n * @param filename The name of the file.\n * @param content The file content.\n */\n writeStagedFile(srcDir: string, dstDir: string, filename: string, content: string): void {\n this.stagedFiles.writeStagedFile(srcDir, dstDir, filename, content)\n }\n\n /**\n * Spawn a child process that runs the given command.\n *\n * @param cwd The directory in which the command will be executed.\n * @param command The command to execute.\n * @param args The arguments to pass to the command.\n * @param opts Additional options to configure the process.\n * @returns The output of the process.\n */\n spawnChild(cwd: string, command: string, args: string[], opts?: ProcessOptions): Promise<ProcessOutput> {\n return spawnChild(cwd, command, args, this.abortSignal, opts)\n }\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'fs'\nimport { join as joinPath } from 'path'\n\nimport { log } from '../_shared/log'\n\ninterface StagedFile {\n srcDir: string\n srcFile: string\n dstDir: string\n dstFile: string\n}\n\nexport class StagedFiles {\n private readonly baseStagedDir: string\n private readonly stagedFiles: StagedFile[] = []\n\n constructor(prepDir: string) {\n this.baseStagedDir = joinPath(prepDir, 'staged')\n }\n\n /**\n * Prepare for writing a file to the staged directory.\n *\n * This will add the path to the array of tracked files and will create the\n * staged directory if needed.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file will be written (this must be a relative path).\n * @param srcFile The name of the file as written to the `staged` directory.\n * @param dstDir The absolute path to the destination directory where the staged\n * file will be copied when the build has completed.\n * @param dstFile The name of the file as written to the destination directory.\n * @return The absolute path to the staged file.\n */\n prepareStagedFile(srcDir: string, srcFile: string, dstDir: string, dstFile: string): string {\n // Add an entry to the array of staged files (only if an entry does not already\n // exist) so that we can copy the file to the destination directory when the build\n // process has completed\n const stagedFile = {\n srcDir,\n srcFile,\n dstDir,\n dstFile\n }\n // TODO: We allow there to be more than one entry for each source path to\n // support the case where a single source file gets copied to multiple\n // destination paths. But we should throw an error if the same destination\n // path is configured for different source paths.\n if (this.stagedFiles.indexOf(stagedFile) < 0) {\n this.stagedFiles.push(stagedFile)\n }\n\n // Create the directory underneath the staged directory if needed\n const stagedDir = joinPath(this.baseStagedDir, srcDir)\n if (!existsSync(stagedDir)) {\n mkdirSync(stagedDir, { recursive: true })\n }\n\n return joinPath(stagedDir, srcFile)\n }\n\n /**\n * Write a file to the staged directory.\n *\n * This file will be copied (along with other staged files) into the destination\n * directory only after the build process has completed. Copying all staged files\n * at once helps improve the local development experience by making it so that\n * live reloading tools only need to refresh once instead of every time a build\n * file is written.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file will be written (this must be a relative path).\n * @param dstDir The absolute path to the destination directory where the staged\n * file will be copied when the build has completed.\n * @param filename The name of the file.\n * @param content The file content.\n */\n writeStagedFile(srcDir: string, dstDir: string, filename: string, content: string): void {\n // Add an entry to track the file and create the staged directory if needed\n const stagedFilePath = this.prepareStagedFile(srcDir, filename, dstDir, filename)\n\n // Write the file to the staged directory\n writeFileSync(stagedFilePath, content)\n }\n\n /**\n * Return the absolute path to the staged file for the given source directory and file name.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file would be written initially (this must be a relative path).\n * @param srcFile The name of the file.\n */\n getStagedFilePath(srcDir: string, srcFile: string): string {\n return joinPath(this.baseStagedDir, srcDir, srcFile)\n }\n\n /**\n * Return true if the staged file exists for the given source directory and file name.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file would be written initially (this must be a relative path).\n * @param srcFile The name of the file.\n */\n stagedFileExists(srcDir: string, srcFile: string): boolean {\n const fullSrcPath = this.getStagedFilePath(srcDir, srcFile)\n return existsSync(fullSrcPath)\n }\n\n /**\n * Return true if the destination file exists for the given source directory and file name.\n *\n * @param srcDir The directory underneath the configured `staged` directory where\n * the file would be written initially (this must be a relative path).\n * @param srcFile The name of the file.\n */\n destinationFileExists(srcDir: string, srcFile: string): boolean {\n const f = this.stagedFiles.find(f => f.srcDir === srcDir && f.srcFile === srcFile)\n if (f === undefined) {\n return false\n }\n const fullDstPath = joinPath(f.dstDir, f.dstFile)\n return existsSync(fullDstPath)\n }\n\n /**\n * Copy staged files to their destination; this will only copy the staged\n * files if they are different than the existing destination files. We\n * copy the files in a batch like this so that hot module reload is only\n * triggered once at the end of the whole build process.\n */\n copyChangedFiles(): void {\n log('info', 'Copying changed files into place...')\n\n for (const f of this.stagedFiles) {\n this.copyStagedFile(f)\n }\n\n log('info', 'Done copying files')\n }\n\n /**\n * Copy a file from the `staged` directory to its destination. If the file already\n * exists in the destination directory and has the same contents as the source file,\n * the file will not be copied and this function will return false.\n *\n * @param f The staged file entry.\n */\n private copyStagedFile(f: StagedFile): boolean {\n // Create the destination directory, if needed\n if (!existsSync(f.dstDir)) {\n mkdirSync(f.dstDir, { recursive: true })\n }\n\n // If the destination file already exists and has the same contents as the source\n // file, we can skip copying it\n const fullSrcPath = this.getStagedFilePath(f.srcDir, f.srcFile)\n const fullDstPath = joinPath(f.dstDir, f.dstFile)\n const needsCopy = filesDiffer(fullSrcPath, fullDstPath)\n if (needsCopy) {\n log('verbose', ` Copying ${f.srcFile} to ${fullDstPath}`)\n copyFileSync(fullSrcPath, fullDstPath)\n }\n return needsCopy\n }\n}\n\n/**\n * Return true if both files exist at the given paths and have the same contents, false otherwise.\n */\nfunction filesDiffer(aPath: string, bPath: string): boolean {\n if (existsSync(aPath) && existsSync(bPath)) {\n // The files exist; see if they are different\n const aSize = statSync(aPath).size\n const bSize = statSync(bPath).size\n if (aSize !== bSize) {\n // The sizes are different, so the contents must be different\n return true\n } else {\n // The sizes are the same, so check the contents\n const aBuf = readFileSync(aPath)\n const bBuf = readFileSync(bPath)\n return !aBuf.equals(bBuf)\n }\n } else {\n // One or both files do not exist\n return true\n }\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { copyFile, readdir, readFile, writeFile } from 'fs/promises'\nimport { join as joinPath } from 'path'\n\nimport { log } from '../../_shared/log'\n\nimport type { BuildContext } from '../../context/context'\n\nimport type { Plugin } from '../../plugin/plugin'\n\n/**\n * Generate the model. This will run the core SDEverywhere code generation steps\n * and will also invoke the following plugin functions:\n * - `preProcessMdl`\n * - `postProcessMdl`\n * - `preGenerateC`\n * - `postGenerateC`\n */\nexport async function generateModel(context: BuildContext, plugins: Plugin[]): Promise<void> {\n const config = context.config\n if (config.modelFiles.length === 0) {\n log('info', 'No model input files specified, skipping model generation steps')\n return\n }\n\n log('info', 'Generating model...')\n\n const t0 = performance.now()\n\n // Use the defined prep directory\n const prepDir = config.prepDir\n\n // TODO: For now we assume the path is to the `main.js` file in the cli package;\n // this seems to work on both Unix and Windows, but we may need to revisit this\n // as part of removing the `sdeCmdPath` config hack\n const sdeCmdPath = config.sdeCmdPath\n\n // Process the mdl file(s)\n for (const plugin of plugins) {\n if (plugin.preProcessMdl) {\n await plugin.preProcessMdl(context)\n }\n }\n if (config.modelFiles.length === 1) {\n // Preprocess the single mdl file\n await preprocessMdl(context, sdeCmdPath, prepDir, config.modelFiles[0])\n } else {\n // Flatten and preprocess the multiple mdl files into a single mdl file\n await flattenMdls(context, sdeCmdPath, prepDir, config.modelFiles)\n }\n for (const plugin of plugins) {\n if (plugin.postProcessMdl) {\n const mdlPath = joinPath(prepDir, 'processed.mdl')\n let mdlContent = await readFile(mdlPath, 'utf8')\n mdlContent = await plugin.postProcessMdl(context, mdlContent)\n await writeFile(mdlPath, mdlContent)\n }\n }\n\n // Generate the C file\n for (const plugin of plugins) {\n if (plugin.preGenerateC) {\n await plugin.preGenerateC(context)\n }\n }\n await generateC(context, config.sdeDir, sdeCmdPath, prepDir)\n for (const plugin of plugins) {\n if (plugin.postGenerateC) {\n const cPath = joinPath(prepDir, 'build', 'processed.c')\n let cContent = await readFile(cPath, 'utf8')\n cContent = await plugin.postGenerateC(context, cContent)\n await writeFile(cPath, cContent)\n }\n }\n\n const t1 = performance.now()\n const elapsed = ((t1 - t0) / 1000).toFixed(1)\n log('info', `Done generating model (${elapsed}s)`)\n}\n\n/**\n * Preprocess a single mdl file and copy the resulting `processed.mdl` to the prep directory.\n */\nasync function preprocessMdl(\n context: BuildContext,\n sdeCmdPath: string,\n prepDir: string,\n modelFile: string\n): Promise<void> {\n log('verbose', ' Preprocessing mdl file')\n\n // Copy the source file to the prep directory to make the next steps easier\n await copyFile(modelFile, joinPath(prepDir, 'processed.mdl'))\n\n // Use SDE to preprocess the model to strip anything that's not needed to build it\n const command = sdeCmdPath\n const args = ['generate', '--preprocess', 'processed.mdl']\n await context.spawnChild(prepDir, command, args)\n\n // Copy the processed file back to the prep directory\n await copyFile(joinPath(prepDir, 'build', 'processed.mdl'), joinPath(prepDir, 'processed.mdl'))\n}\n\n/**\n * Flatten multiple mdl files and copy the resulting `processed.mdl` to the prep directory.\n */\nasync function flattenMdls(\n context: BuildContext,\n sdeCmdPath: string,\n prepDir: string,\n modelFiles: string[]\n): Promise<void> {\n log('verbose', ' Flattening and preprocessing mdl files')\n\n // Use SDE to flatten the parent model and submodels into a single mdl file,\n // then preprocess to strip anything that's not needed to build the model\n const command = sdeCmdPath\n const args: string[] = []\n args.push('flatten')\n args.push('processed.mdl')\n args.push('--inputs')\n for (const path of modelFiles) {\n args.push(path)\n }\n\n // Disable logging by default; this will suppress flatten warnings, which are\n // sometimes unavoidable and not helpful\n const output = await context.spawnChild(prepDir, command, args, {\n logOutput: false,\n captureOutput: true,\n ignoreError: true\n })\n\n // Check for flattening errors\n let flattenErrors = false\n for (const msg of output.stderrMessages) {\n if (msg.includes('ERROR')) {\n flattenErrors = true\n break\n }\n }\n if (flattenErrors) {\n log('error', 'There were errors reported when flattening the model:')\n for (const msg of output.stderrMessages) {\n const lines = msg.split('\\n')\n for (const line of lines) {\n log('error', ` ${line}`)\n }\n }\n throw new Error(`Flatten command failed (code=${output.exitCode})`)\n } else if (output.exitCode !== 0) {\n throw new Error(`Flatten command failed (code=${output.exitCode})`)\n }\n\n // Copy the processed file back to the prep directory\n await copyFile(joinPath(prepDir, 'build', 'processed.mdl'), joinPath(prepDir, 'processed.mdl'))\n}\n\n/**\n * Generate a C file from the `processed.mdl` file.\n */\nasync function generateC(context: BuildContext, sdeDir: string, sdeCmdPath: string, prepDir: string): Promise<void> {\n log('verbose', ' Generating C code')\n\n // Use SDE to generate a C version of the model\n const command = sdeCmdPath\n const args = ['generate', '--genc', '--spec', 'spec.json', 'processed']\n await context.spawnChild(prepDir, command, args, {\n // By default, ignore lines that start with \"WARNING: Data for\" since these are often harmless\n // TODO: Don't filter by default, but make it configurable\n // ignoredMessageFilter: 'WARNING: Data for'\n })\n\n // Copy SDE's supporting C files into the build directory\n const buildDir = joinPath(prepDir, 'build')\n const sdeCDir = joinPath(sdeDir, 'src', 'c')\n const files = await readdir(sdeCDir)\n const copyOps = []\n for (const file of files) {\n if (file.endsWith('.c') || file.endsWith('.h')) {\n copyOps.push(copyFile(joinPath(sdeCDir, file), joinPath(buildDir, file)))\n }\n }\n await Promise.all(copyOps)\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { join as joinPath } from 'path'\nimport { hashElement } from 'folder-hash'\nimport glob from 'tiny-glob'\n\nimport type { ResolvedConfig } from '../../_shared/resolved-config'\n\n/**\n * Asynchronously compute the hash of the files that are inputs to the model\n * build process.\n */\nexport async function computeInputFilesHash(config: ResolvedConfig): Promise<string> {\n const inputFiles: string[] = []\n\n // Always include the `spec.json` file, since that is a primary input\n // to the model build process\n const specFile = joinPath(config.prepDir, 'spec.json')\n inputFiles.push(specFile)\n\n if (config.modelInputPaths && config.modelInputPaths.length > 0) {\n // Include the files that match the glob patterns in the config file.\n // Note that the folder-hash package supports glob patterns, but its\n // configuration is complicated, so it is easier if we resolve the\n // glob patterns here and pass each individual file to the\n // `hashElement` function.\n for (const globPath of config.modelInputPaths) {\n const paths = await glob(globPath, {\n cwd: config.rootDir,\n absolute: true,\n filesOnly: true\n })\n inputFiles.push(...paths)\n }\n } else {\n // Only use the mdl files to compute the hash\n inputFiles.push(...config.modelFiles)\n }\n\n // Compute the hash of each input file and concatenate into a single string\n let hash = ''\n for (const inputFile of inputFiles) {\n const result = await hashElement(inputFile)\n hash += result.hash\n }\n\n return hash\n}\n","// Copyright (c) 2022 Climate Interactive / New Venture Fund\n\nimport { basename } from 'path'\n\nimport chokidar from 'chokidar'\n\nimport { clearOverlay, log, logError } from '../../_shared/log'\nimport type { ResolvedConfig } from '../../_shared/resolved-config'\n\nimport type { UserConfig } from '../../config/user-config'\nimport type { Plugin } from '../../plugin/plugin'\n\nimport type { BuildOnceOptions } from './build-once'\nimport { buildOnce } from './build-once'\n\nclass BuildState {\n readonly abortController = new AbortController()\n}\n\nexport function watch(config: ResolvedConfig, userConfig: UserConfig, plugins: Plugin[]): void {\n // Add a small delay so that if multiple files are changed at once (as is\n // often the case when switching branches), we batch them up and start the\n // the build after things settle down\n const delay = 150\n const changedPaths: Set<string> = new Set()\n\n // Keep track of the current build so that we only have one active at a time\n let currentBuildState: BuildState\n\n function performBuild() {\n clearOverlay()\n\n // Log the input files that have changed\n for (const path of changedPaths) {\n log('info', `Input file ${basename(path)} has been changed`)\n }\n\n // Clear the set of pending files\n changedPaths.clear()\n\n // Keep track of builds; if one is already in progress, abort it\n // before starting another one\n if (currentBuildState) {\n currentBuildState.abortController.abort()\n // TODO: Prevent aborted build from logging?\n currentBuildState = undefined\n }\n\n // Generate files and build the model\n currentBuildState = new BuildState()\n const buildOptions: BuildOnceOptions = {\n abortSignal: currentBuildState.abortController.signal\n }\n buildOnce(config, userConfig, plugins, buildOptions)\n .then(result => {\n // Log the error message in case of error result\n if (result.isErr()) {\n logError(result.error)\n }\n })\n .catch(e => {\n // Also catch thrown errors that may have not already been\n // handled by `buildOnce`\n logError(e)\n // log('info', 'Waiting for changes...')\n })\n .finally(() => {\n currentBuildState = undefined\n })\n }\n\n function scheduleBuild(changedPath: string) {\n // Only schedule the build if the set is currently empty\n const schedule = changedPaths.size === 0\n\n // Add the path to the set of changed files\n changedPaths.add(changedPath)\n\n if (schedule) {\n // Schedule the build to start after a delay\n setTimeout(() => {\n performBuild()\n }, delay)\n }\n }\n\n let watchPaths: string[]\n if (config.watchPaths && config.watchPaths.length > 0) {\n // Watch the configured files\n watchPaths = config.watchPaths\n } else {\n // Only watch the mdl files\n watchPaths = config.modelFiles\n }\n\n // Watch the config and model files; if changes are detected, generate the specs\n // and rebuild the model if needed\n const watcher = chokidar.watch(watchPaths, {\n // Watch paths are resolved relative to the project root directory\n cwd: config.rootDir,\n // XXX: Include a delay, otherwise on macOS we sometimes get multiple\n // change events when the csv file is saved just once\n awaitWriteFinish: {\n stabilityThreshold: 200\n }\n })\n watcher.on('change', path => {\n scheduleBuild(path)\n })\n}\n"],"mappings":"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","names":["joinPath","err","ok","existsSync","readFileSync","writeFileSync","writeFile","joinPath","err","ok","err","existsSync","mkdirSync","writeFileSync","joinPath","joinPath","existsSync","mkdirSync","writeFileSync","f","joinPath","joinPath","joinPath","err","joinPath","writeFile","existsSync","readFileSync","writeFileSync","ok","err","joinPath","overlayEnabled","plugins","ok"]}
|