@sdeverywhere/build 0.3.2 → 0.3.4

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 CHANGED
@@ -567,7 +567,14 @@ async function preprocessMdl(context, sdeCmdPath, prepDir, modelFile) {
567
567
  await (0, import_promises.copyFile)(modelFile, (0, import_path3.join)(prepDir, "processed.mdl"));
568
568
  const command = sdeCmdPath;
569
569
  const args = ["generate", "--preprocess", "processed.mdl"];
570
- await context.spawnChild(prepDir, command, args);
570
+ const ppOutput = await context.spawnChild(prepDir, command, args, {
571
+ // The default error message from `spawnChild` is not very informative, so the
572
+ // following allows us to throw our own error
573
+ ignoreError: true
574
+ });
575
+ if (ppOutput.exitCode !== 0) {
576
+ throw new Error(`Failed to preprocess mdl file: 'sde generate' command failed (code=${ppOutput.exitCode})`);
577
+ }
571
578
  await (0, import_promises.copyFile)((0, import_path3.join)(prepDir, "build", "processed.mdl"), (0, import_path3.join)(prepDir, "processed.mdl"));
572
579
  }
573
580
  async function flattenMdls(context, sdeCmdPath, prepDir, modelFiles) {
@@ -600,23 +607,27 @@ async function flattenMdls(context, sdeCmdPath, prepDir, modelFiles) {
600
607
  log("error", ` ${line}`);
601
608
  }
602
609
  }
603
- throw new Error(`Flatten command failed (code=${output.exitCode})`);
610
+ throw new Error(`Failed to flatten mdl files: 'sde flatten' command failed (code=${output.exitCode})`);
604
611
  } else if (output.exitCode !== 0) {
605
- throw new Error(`Flatten command failed (code=${output.exitCode})`);
612
+ throw new Error(`Failed to flatten mdl files: 'sde flatten' command failed (code=${output.exitCode})`);
606
613
  }
607
614
  await (0, import_promises.copyFile)((0, import_path3.join)(prepDir, "build", "processed.mdl"), (0, import_path3.join)(prepDir, "processed.mdl"));
608
615
  }
609
616
  async function generateC(context, sdeDir, sdeCmdPath, prepDir) {
610
617
  log("verbose", " Generating C code");
611
618
  const command = sdeCmdPath;
612
- const gencArgs = ["generate", "--genc", "--spec", "spec.json", "processed"];
613
- await context.spawnChild(prepDir, command, gencArgs, {
619
+ const gencArgs = ["generate", "--genc", "--list", "--spec", "spec.json", "processed"];
620
+ const gencOutput = await context.spawnChild(prepDir, command, gencArgs, {
614
621
  // By default, ignore lines that start with "WARNING: Data for" since these are often harmless
615
622
  // TODO: Don't filter by default, but make it configurable
616
623
  // ignoredMessageFilter: 'WARNING: Data for'
624
+ // The default error message from `spawnChild` is not very informative, so the
625
+ // following allows us to throw our own error
626
+ ignoreError: true
617
627
  });
618
- const listArgs = ["generate", "--list", "--spec", "spec.json", "processed"];
619
- await context.spawnChild(prepDir, command, listArgs, {});
628
+ if (gencOutput.exitCode !== 0) {
629
+ throw new Error(`Failed to generate C code: 'sde generate' command failed (code=${gencOutput.exitCode})`);
630
+ }
620
631
  const buildDir = (0, import_path3.join)(prepDir, "build");
621
632
  const sdeCDir = (0, import_path3.join)(sdeDir, "src", "c");
622
633
  const files = await (0, import_promises.readdir)(sdeCDir);
@@ -661,45 +672,40 @@ async function computeInputFilesHash(config) {
661
672
  async function buildOnce(config, userConfig, plugins, options) {
662
673
  const stagedFiles = new StagedFiles(config.prepDir);
663
674
  const context = new BuildContext(config, stagedFiles, options.abortSignal);
664
- let modelSpec;
675
+ const modelHashPath = (0, import_path5.join)(config.prepDir, "model-hash.txt");
676
+ let succeeded = true;
665
677
  try {
666
- modelSpec = await userConfig.modelSpec(context);
678
+ const modelSpec = await userConfig.modelSpec(context);
667
679
  if (modelSpec === void 0) {
668
680
  return (0, import_neverthrow2.err)(new Error("The model spec must be defined"));
669
681
  }
670
- } catch (e) {
671
- return (0, import_neverthrow2.err)(e);
672
- }
673
- for (const plugin of plugins) {
674
- if (plugin.preGenerate) {
675
- plugin.preGenerate(context, modelSpec);
682
+ for (const plugin of plugins) {
683
+ if (plugin.preGenerate) {
684
+ await plugin.preGenerate(context, modelSpec);
685
+ }
686
+ }
687
+ const specJson = {
688
+ inputVarNames: modelSpec.inputs.map((input) => input.varName),
689
+ outputVarNames: modelSpec.outputs.map((output) => output.varName),
690
+ externalDatfiles: modelSpec.datFiles,
691
+ ...modelSpec.options
692
+ };
693
+ const specPath = (0, import_path5.join)(config.prepDir, "spec.json");
694
+ await (0, import_promises2.writeFile)(specPath, JSON.stringify(specJson, null, 2));
695
+ let previousModelHash;
696
+ if ((0, import_fs4.existsSync)(modelHashPath)) {
697
+ previousModelHash = (0, import_fs4.readFileSync)(modelHashPath, "utf8");
698
+ } else {
699
+ previousModelHash = "NONE";
700
+ }
701
+ const inputFilesHash = await computeInputFilesHash(config);
702
+ let needModelGen;
703
+ if (options.forceModelGen === true) {
704
+ needModelGen = true;
705
+ } else {
706
+ const hashMismatch = inputFilesHash !== previousModelHash;
707
+ needModelGen = hashMismatch;
676
708
  }
677
- }
678
- const specJson = {
679
- inputVarNames: modelSpec.inputs.map((input) => input.varName),
680
- outputVarNames: modelSpec.outputs.map((output) => output.varName),
681
- externalDatfiles: modelSpec.datFiles,
682
- ...modelSpec.options
683
- };
684
- const specPath = (0, import_path5.join)(config.prepDir, "spec.json");
685
- await (0, import_promises2.writeFile)(specPath, JSON.stringify(specJson, null, 2));
686
- const modelHashPath = (0, import_path5.join)(config.prepDir, "model-hash.txt");
687
- let previousModelHash;
688
- if ((0, import_fs4.existsSync)(modelHashPath)) {
689
- previousModelHash = (0, import_fs4.readFileSync)(modelHashPath, "utf8");
690
- } else {
691
- previousModelHash = "NONE";
692
- }
693
- const inputFilesHash = await computeInputFilesHash(config);
694
- let needModelGen;
695
- if (options.forceModelGen === true) {
696
- needModelGen = true;
697
- } else {
698
- const hashMismatch = inputFilesHash !== previousModelHash;
699
- needModelGen = hashMismatch;
700
- }
701
- let succeeded = true;
702
- try {
703
709
  if (needModelGen) {
704
710
  await generateModel(context, plugins);
705
711
  (0, import_fs4.writeFileSync)(modelHashPath, inputFilesHash);
@@ -814,28 +820,27 @@ async function build(mode, options) {
814
820
  const messagesPath = (0, import_path7.join)(resolvedConfig.prepDir, "messages.html");
815
821
  const overlayEnabled2 = mode === "development";
816
822
  setOverlayFile(messagesPath, overlayEnabled2);
817
- const plugins = userConfig.plugins || [];
818
- for (const plugin of plugins) {
819
- if (plugin.init) {
820
- await plugin.init(resolvedConfig);
821
- }
822
- }
823
823
  try {
824
- const plugins2 = userConfig.plugins || [];
824
+ const plugins = userConfig.plugins || [];
825
+ for (const plugin of plugins) {
826
+ if (plugin.init) {
827
+ await plugin.init(resolvedConfig);
828
+ }
829
+ }
825
830
  if (mode === "development") {
826
- const buildResult = await buildOnce(resolvedConfig, userConfig, plugins2, {});
831
+ const buildResult = await buildOnce(resolvedConfig, userConfig, plugins, {});
827
832
  if (buildResult.isErr()) {
828
833
  return (0, import_neverthrow3.err)(buildResult.error);
829
834
  }
830
- for (const plugin of plugins2) {
835
+ for (const plugin of plugins) {
831
836
  if (plugin.watch) {
832
837
  await plugin.watch(resolvedConfig);
833
838
  }
834
839
  }
835
- watch(resolvedConfig, userConfig, plugins2);
840
+ watch(resolvedConfig, userConfig, plugins);
836
841
  return (0, import_neverthrow3.ok)({});
837
842
  } else {
838
- const buildResult = await buildOnce(resolvedConfig, userConfig, plugins2, {});
843
+ const buildResult = await buildOnce(resolvedConfig, userConfig, plugins, {});
839
844
  if (buildResult.isErr()) {
840
845
  return (0, import_neverthrow3.err)(buildResult.error);
841
846
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../../../node_modules/.pnpm/tsup@7.2.0_typescript@5.2.2/node_modules/tsup/assets/cjs_shims.js","../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","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () =>\n typeof document === 'undefined'\n ? new URL('file:' + __filename).href\n : (document.currentScript && document.currentScript.src) ||\n new URL('main.js', document.baseURI).href\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\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 = '&nbsp;'.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 gencArgs = ['generate', '--genc', '--spec', 'spec.json', 'processed']\n await context.spawnChild(prepDir, command, gencArgs, {\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 // Use SDE to generate a JSON list of all model dimensions and variables\n // TODO: Allow --genc and --list in same command so that we only need to process once\n const listArgs = ['generate', '--list', '--spec', 'spec.json', 'processed']\n await context.spawnChild(prepDir, command, listArgs, {})\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"]}
1
+ {"version":3,"sources":["../src/index.ts","../../../node_modules/.pnpm/tsup@7.2.0_typescript@5.2.2/node_modules/tsup/assets/cjs_shims.js","../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","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () =>\n typeof document === 'undefined'\n ? new URL('file:' + __filename).href\n : (document.currentScript && document.currentScript.src) ||\n new URL('main.js', document.baseURI).href\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\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 try {\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 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 = '&nbsp;'.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'\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 const modelHashPath = joinPath(config.prepDir, 'model-hash.txt')\n\n // Note that the entire body of this function is wrapped in a try/catch. Any\n // errors that are thrown by plugin functions or the core `generateModel`\n // function will be caught and handled as appropriate.\n let succeeded = true\n try {\n // Get the model spec from the config\n const modelSpec = await userConfig.modelSpec(context)\n if (modelSpec === undefined) {\n return err(new Error('The model spec must be defined'))\n }\n\n // Run plugins that implement `preGenerate`\n for (const plugin of plugins) {\n if (plugin.preGenerate) {\n await 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 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 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 const ppOutput = await context.spawnChild(prepDir, command, args, {\n // The default error message from `spawnChild` is not very informative, so the\n // following allows us to throw our own error\n ignoreError: true\n })\n if (ppOutput.exitCode !== 0) {\n throw new Error(`Failed to preprocess mdl file: 'sde generate' command failed (code=${ppOutput.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 * 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(`Failed to flatten mdl files: 'sde flatten' command failed (code=${output.exitCode})`)\n } else if (output.exitCode !== 0) {\n throw new Error(`Failed to flatten mdl files: 'sde 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 both a C version of the model (`--genc`) AND a JSON list of all model\n // dimensions and variables (`--list`)\n const command = sdeCmdPath\n const gencArgs = ['generate', '--genc', '--list', '--spec', 'spec.json', 'processed']\n const gencOutput = await context.spawnChild(prepDir, command, gencArgs, {\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 // The default error message from `spawnChild` is not very informative, so the\n // following allows us to throw our own error\n ignoreError: true\n })\n if (gencOutput.exitCode !== 0) {\n throw new Error(`Failed to generate C code: 'sde generate' command failed (code=${gencOutput.exitCode})`)\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"]}
package/dist/index.js CHANGED
@@ -528,7 +528,14 @@ async function preprocessMdl(context, sdeCmdPath, prepDir, modelFile) {
528
528
  await copyFile(modelFile, joinPath3(prepDir, "processed.mdl"));
529
529
  const command = sdeCmdPath;
530
530
  const args = ["generate", "--preprocess", "processed.mdl"];
531
- await context.spawnChild(prepDir, command, args);
531
+ const ppOutput = await context.spawnChild(prepDir, command, args, {
532
+ // The default error message from `spawnChild` is not very informative, so the
533
+ // following allows us to throw our own error
534
+ ignoreError: true
535
+ });
536
+ if (ppOutput.exitCode !== 0) {
537
+ throw new Error(`Failed to preprocess mdl file: 'sde generate' command failed (code=${ppOutput.exitCode})`);
538
+ }
532
539
  await copyFile(joinPath3(prepDir, "build", "processed.mdl"), joinPath3(prepDir, "processed.mdl"));
533
540
  }
534
541
  async function flattenMdls(context, sdeCmdPath, prepDir, modelFiles) {
@@ -561,23 +568,27 @@ async function flattenMdls(context, sdeCmdPath, prepDir, modelFiles) {
561
568
  log("error", ` ${line}`);
562
569
  }
563
570
  }
564
- throw new Error(`Flatten command failed (code=${output.exitCode})`);
571
+ throw new Error(`Failed to flatten mdl files: 'sde flatten' command failed (code=${output.exitCode})`);
565
572
  } else if (output.exitCode !== 0) {
566
- throw new Error(`Flatten command failed (code=${output.exitCode})`);
573
+ throw new Error(`Failed to flatten mdl files: 'sde flatten' command failed (code=${output.exitCode})`);
567
574
  }
568
575
  await copyFile(joinPath3(prepDir, "build", "processed.mdl"), joinPath3(prepDir, "processed.mdl"));
569
576
  }
570
577
  async function generateC(context, sdeDir, sdeCmdPath, prepDir) {
571
578
  log("verbose", " Generating C code");
572
579
  const command = sdeCmdPath;
573
- const gencArgs = ["generate", "--genc", "--spec", "spec.json", "processed"];
574
- await context.spawnChild(prepDir, command, gencArgs, {
580
+ const gencArgs = ["generate", "--genc", "--list", "--spec", "spec.json", "processed"];
581
+ const gencOutput = await context.spawnChild(prepDir, command, gencArgs, {
575
582
  // By default, ignore lines that start with "WARNING: Data for" since these are often harmless
576
583
  // TODO: Don't filter by default, but make it configurable
577
584
  // ignoredMessageFilter: 'WARNING: Data for'
585
+ // The default error message from `spawnChild` is not very informative, so the
586
+ // following allows us to throw our own error
587
+ ignoreError: true
578
588
  });
579
- const listArgs = ["generate", "--list", "--spec", "spec.json", "processed"];
580
- await context.spawnChild(prepDir, command, listArgs, {});
589
+ if (gencOutput.exitCode !== 0) {
590
+ throw new Error(`Failed to generate C code: 'sde generate' command failed (code=${gencOutput.exitCode})`);
591
+ }
581
592
  const buildDir = joinPath3(prepDir, "build");
582
593
  const sdeCDir = joinPath3(sdeDir, "src", "c");
583
594
  const files = await readdir(sdeCDir);
@@ -622,45 +633,40 @@ async function computeInputFilesHash(config) {
622
633
  async function buildOnce(config, userConfig, plugins, options) {
623
634
  const stagedFiles = new StagedFiles(config.prepDir);
624
635
  const context = new BuildContext(config, stagedFiles, options.abortSignal);
625
- let modelSpec;
636
+ const modelHashPath = joinPath5(config.prepDir, "model-hash.txt");
637
+ let succeeded = true;
626
638
  try {
627
- modelSpec = await userConfig.modelSpec(context);
639
+ const modelSpec = await userConfig.modelSpec(context);
628
640
  if (modelSpec === void 0) {
629
641
  return err2(new Error("The model spec must be defined"));
630
642
  }
631
- } catch (e) {
632
- return err2(e);
633
- }
634
- for (const plugin of plugins) {
635
- if (plugin.preGenerate) {
636
- plugin.preGenerate(context, modelSpec);
643
+ for (const plugin of plugins) {
644
+ if (plugin.preGenerate) {
645
+ await plugin.preGenerate(context, modelSpec);
646
+ }
647
+ }
648
+ const specJson = {
649
+ inputVarNames: modelSpec.inputs.map((input) => input.varName),
650
+ outputVarNames: modelSpec.outputs.map((output) => output.varName),
651
+ externalDatfiles: modelSpec.datFiles,
652
+ ...modelSpec.options
653
+ };
654
+ const specPath = joinPath5(config.prepDir, "spec.json");
655
+ await writeFile2(specPath, JSON.stringify(specJson, null, 2));
656
+ let previousModelHash;
657
+ if (existsSync3(modelHashPath)) {
658
+ previousModelHash = readFileSync2(modelHashPath, "utf8");
659
+ } else {
660
+ previousModelHash = "NONE";
661
+ }
662
+ const inputFilesHash = await computeInputFilesHash(config);
663
+ let needModelGen;
664
+ if (options.forceModelGen === true) {
665
+ needModelGen = true;
666
+ } else {
667
+ const hashMismatch = inputFilesHash !== previousModelHash;
668
+ needModelGen = hashMismatch;
637
669
  }
638
- }
639
- const specJson = {
640
- inputVarNames: modelSpec.inputs.map((input) => input.varName),
641
- outputVarNames: modelSpec.outputs.map((output) => output.varName),
642
- externalDatfiles: modelSpec.datFiles,
643
- ...modelSpec.options
644
- };
645
- const specPath = joinPath5(config.prepDir, "spec.json");
646
- await writeFile2(specPath, JSON.stringify(specJson, null, 2));
647
- const modelHashPath = joinPath5(config.prepDir, "model-hash.txt");
648
- let previousModelHash;
649
- if (existsSync3(modelHashPath)) {
650
- previousModelHash = readFileSync2(modelHashPath, "utf8");
651
- } else {
652
- previousModelHash = "NONE";
653
- }
654
- const inputFilesHash = await computeInputFilesHash(config);
655
- let needModelGen;
656
- if (options.forceModelGen === true) {
657
- needModelGen = true;
658
- } else {
659
- const hashMismatch = inputFilesHash !== previousModelHash;
660
- needModelGen = hashMismatch;
661
- }
662
- let succeeded = true;
663
- try {
664
670
  if (needModelGen) {
665
671
  await generateModel(context, plugins);
666
672
  writeFileSync3(modelHashPath, inputFilesHash);
@@ -775,28 +781,27 @@ async function build(mode, options) {
775
781
  const messagesPath = joinPath6(resolvedConfig.prepDir, "messages.html");
776
782
  const overlayEnabled2 = mode === "development";
777
783
  setOverlayFile(messagesPath, overlayEnabled2);
778
- const plugins = userConfig.plugins || [];
779
- for (const plugin of plugins) {
780
- if (plugin.init) {
781
- await plugin.init(resolvedConfig);
782
- }
783
- }
784
784
  try {
785
- const plugins2 = userConfig.plugins || [];
785
+ const plugins = userConfig.plugins || [];
786
+ for (const plugin of plugins) {
787
+ if (plugin.init) {
788
+ await plugin.init(resolvedConfig);
789
+ }
790
+ }
786
791
  if (mode === "development") {
787
- const buildResult = await buildOnce(resolvedConfig, userConfig, plugins2, {});
792
+ const buildResult = await buildOnce(resolvedConfig, userConfig, plugins, {});
788
793
  if (buildResult.isErr()) {
789
794
  return err3(buildResult.error);
790
795
  }
791
- for (const plugin of plugins2) {
796
+ for (const plugin of plugins) {
792
797
  if (plugin.watch) {
793
798
  await plugin.watch(resolvedConfig);
794
799
  }
795
800
  }
796
- watch(resolvedConfig, userConfig, plugins2);
801
+ watch(resolvedConfig, userConfig, plugins);
797
802
  return ok3({});
798
803
  } else {
799
- const buildResult = await buildOnce(resolvedConfig, userConfig, plugins2, {});
804
+ const buildResult = await buildOnce(resolvedConfig, userConfig, plugins, {});
800
805
  if (buildResult.isErr()) {
801
806
  return err3(buildResult.error);
802
807
  }
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 = '&nbsp;'.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 gencArgs = ['generate', '--genc', '--spec', 'spec.json', 'processed']\n await context.spawnChild(prepDir, command, gencArgs, {\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 // Use SDE to generate a JSON list of all model dimensions and variables\n // TODO: Allow --genc and --list in same command so that we only need to process once\n const listArgs = ['generate', '--list', '--spec', 'spec.json', 'processed']\n await context.spawnChild(prepDir, command, listArgs, {})\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"]}
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 try {\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 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 = '&nbsp;'.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'\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 const modelHashPath = joinPath(config.prepDir, 'model-hash.txt')\n\n // Note that the entire body of this function is wrapped in a try/catch. Any\n // errors that are thrown by plugin functions or the core `generateModel`\n // function will be caught and handled as appropriate.\n let succeeded = true\n try {\n // Get the model spec from the config\n const modelSpec = await userConfig.modelSpec(context)\n if (modelSpec === undefined) {\n return err(new Error('The model spec must be defined'))\n }\n\n // Run plugins that implement `preGenerate`\n for (const plugin of plugins) {\n if (plugin.preGenerate) {\n await 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 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 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 const ppOutput = await context.spawnChild(prepDir, command, args, {\n // The default error message from `spawnChild` is not very informative, so the\n // following allows us to throw our own error\n ignoreError: true\n })\n if (ppOutput.exitCode !== 0) {\n throw new Error(`Failed to preprocess mdl file: 'sde generate' command failed (code=${ppOutput.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 * 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(`Failed to flatten mdl files: 'sde flatten' command failed (code=${output.exitCode})`)\n } else if (output.exitCode !== 0) {\n throw new Error(`Failed to flatten mdl files: 'sde 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 both a C version of the model (`--genc`) AND a JSON list of all model\n // dimensions and variables (`--list`)\n const command = sdeCmdPath\n const gencArgs = ['generate', '--genc', '--list', '--spec', 'spec.json', 'processed']\n const gencOutput = await context.spawnChild(prepDir, command, gencArgs, {\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 // The default error message from `spawnChild` is not very informative, so the\n // following allows us to throw our own error\n ignoreError: true\n })\n if (gencOutput.exitCode !== 0) {\n throw new Error(`Failed to generate C code: 'sde generate' command failed (code=${gencOutput.exitCode})`)\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","joinPath","err","writeFile","existsSync","readFileSync","writeFileSync","ok","err","joinPath","overlayEnabled","ok"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdeverywhere/build",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "files": [
5
5
  "dist/**"
6
6
  ],
@@ -47,7 +47,7 @@
47
47
  "precommit": "../../scripts/precommit",
48
48
  "test": "vitest run",
49
49
  "test:watch": "vitest",
50
- "test:ci": "vitest run",
50
+ "test:ci": "vitest --threads=false --test-timeout=10000 run",
51
51
  "type-check": "tsc --noEmit -p tsconfig-build.json",
52
52
  "build": "tsup",
53
53
  "docs": "../../scripts/gen-docs.js",