@valbuild/server 0.97.2 → 0.97.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.
@@ -2,21 +2,21 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var quickjsEmscripten = require('quickjs-emscripten');
6
5
  var ts = require('typescript');
7
6
  var fp = require('@valbuild/core/fp');
8
7
  var core = require('@valbuild/core');
9
8
  var patch = require('@valbuild/core/patch');
10
9
  var path = require('path');
11
10
  var fs = require('fs');
12
- var sucrase = require('sucrase');
13
- var ui = require('@valbuild/ui');
11
+ var vm = require('node:vm');
12
+ var node_module = require('node:module');
14
13
  var internal = require('@valbuild/shared/internal');
15
14
  var server = require('@valbuild/ui/server');
16
15
  var crypto$1 = require('crypto');
17
16
  var zod = require('zod');
18
17
  var sizeOf = require('image-size');
19
18
  var zodValidationError = require('zod-validation-error');
19
+ var sucrase = require('sucrase');
20
20
  var http = require('http');
21
21
  var https = require('https');
22
22
 
@@ -43,6 +43,7 @@ function _interopNamespace(e) {
43
43
  var ts__default = /*#__PURE__*/_interopDefault(ts);
44
44
  var path__namespace = /*#__PURE__*/_interopNamespace(path);
45
45
  var fs__default = /*#__PURE__*/_interopDefault(fs);
46
+ var vm__default = /*#__PURE__*/_interopDefault(vm);
46
47
  var crypto__default = /*#__PURE__*/_interopDefault(crypto$1);
47
48
  var sizeOf__default = /*#__PURE__*/_interopDefault(sizeOf);
48
49
  var http__default = /*#__PURE__*/_interopDefault(http);
@@ -829,7 +830,7 @@ const ops = new TSOps(document => {
829
830
  });
830
831
 
831
832
  // TODO: rename to patchValFiles since we may write multiple files
832
- const patchValFile = async (id, rootDir, patch$1, sourceFileHandler, runtime) => {
833
+ const patchValFile = async (id, rootDir, patch$1, sourceFileHandler) => {
833
834
  // const timeId = randomUUID();
834
835
  // console.time("patchValFile" + timeId);
835
836
  const filePath = sourceFileHandler.resolveSourceModulePath(getSyntheticContainingPath(rootDir), `.${id.replace(".val.ts", ".val").replace(".val.js", ".val").replace(".val.jsx", ".val").replace(".val.tsx", ".val")}`);
@@ -880,143 +881,6 @@ const patchSourceFile = (sourceFile, patch$1) => {
880
881
  return patch.applyPatch(sourceFile, ops, patch$1);
881
882
  };
882
883
 
883
- const readValFile = async (moduleFilePath, rootDirPath, runtime, options) => {
884
- const context = runtime.newContext();
885
-
886
- // avoid failures when console.log is called
887
- const logHandle = context.newFunction("log", () => {
888
- // do nothing
889
- });
890
- const consoleHandle = context.newObject();
891
- context.setProp(consoleHandle, "log", logHandle);
892
- context.setProp(context.global, "console", consoleHandle);
893
- consoleHandle.dispose();
894
- logHandle.dispose();
895
-
896
- // avoid failures when process.env is called
897
- const envHandle = context.newObject();
898
- const processHandle = context.newObject();
899
- context.setProp(processHandle, "env", envHandle);
900
- context.setProp(context.global, "process", processHandle);
901
- const optionsHandle = context.newObject();
902
- if (options) {
903
- if (options.validate !== undefined) {
904
- context.setProp(optionsHandle, "validate", context.newNumber(+options.validate));
905
- }
906
- if (options.source !== undefined) {
907
- context.setProp(optionsHandle, "source", context.newNumber(+options.source));
908
- }
909
- if (options.schema !== undefined) {
910
- context.setProp(optionsHandle, "schema", context.newNumber(+options.schema));
911
- }
912
- }
913
- context.setProp(context.global, "__VAL_OPTIONS__", optionsHandle);
914
- envHandle.dispose();
915
- processHandle.dispose();
916
- optionsHandle.dispose();
917
- try {
918
- const modulePath = `.${moduleFilePath.replace(".val.js", ".val").replace(".val.ts", ".val").replace(".val.tsx", ".val").replace(".val.jsx", ".val")}`;
919
- const code = `import * as valModule from ${JSON.stringify(modulePath)};
920
- import { Internal } from "@valbuild/core";
921
-
922
- globalThis.valModule = {
923
- path: valModule?.default && Internal.getValPath(valModule?.default),
924
- schema: !!globalThis['__VAL_OPTIONS__'].schema ? valModule?.default && Internal.getSchema(valModule?.default)?.["executeSerialize"]() : undefined,
925
- source: !!globalThis['__VAL_OPTIONS__'].source ? valModule?.default && Internal.getSource(valModule?.default) : undefined,
926
- validation: !!globalThis['__VAL_OPTIONS__'].validate ? valModule?.default && (Internal.validate ? Internal.validate(valModule.default, Internal.getValPath(valModule?.default) || "/",
927
- Internal.getSource(valModule?.default)) : Internal.getSchema(valModule?.default)?.validate(
928
- Internal.getValPath(valModule?.default) || "/",
929
- Internal.getSource(valModule?.default)
930
- )) : undefined,
931
- defaultExport: !!valModule?.default,
932
- };
933
- `;
934
- const result = context.evalCode(code, getSyntheticContainingPath(rootDirPath));
935
- const fatalErrors = [];
936
- if (result.error) {
937
- const error = result.error.consume(context.dump);
938
- console.error(`Fatal error reading val file: ${moduleFilePath}. Error: ${error.message}\n`, error.stack);
939
- return {
940
- path: moduleFilePath,
941
- errors: {
942
- invalidModulePath: moduleFilePath,
943
- fatal: [{
944
- message: `${error.name || "Unknown error"}: ${error.message || "<no message>"}`,
945
- stack: error.stack
946
- }]
947
- }
948
- };
949
- } else {
950
- result.value.dispose();
951
- const valModule = context.getProp(context.global, "valModule").consume(context.dump);
952
- if (
953
- // if one of these are set it is a Val module, so must validate
954
- (valModule === null || valModule === void 0 ? void 0 : valModule.path) !== undefined || (valModule === null || valModule === void 0 ? void 0 : valModule.schema) !== undefined || (valModule === null || valModule === void 0 ? void 0 : valModule.source) !== undefined) {
955
- if (valModule.path !== moduleFilePath) {
956
- fatalErrors.push(`Wrong c.define path! Expected: '${moduleFilePath}', found: '${valModule.path}'`);
957
- } else if ((valModule === null || valModule === void 0 ? void 0 : valModule.schema) === undefined && options.schema) {
958
- fatalErrors.push(`Expected val path: '${moduleFilePath}' to have a schema`);
959
- } else if ((valModule === null || valModule === void 0 ? void 0 : valModule.source) === undefined && options.source) {
960
- fatalErrors.push(`Expected val path: '${moduleFilePath}' to have a source`);
961
- }
962
- }
963
- let errors = false;
964
- if (fatalErrors.length > 0) {
965
- errors = {
966
- invalidModulePath: valModule.path !== moduleFilePath ? moduleFilePath : undefined,
967
- fatal: fatalErrors.map(message => ({
968
- message
969
- }))
970
- };
971
- }
972
- if (valModule !== null && valModule !== void 0 && valModule.validation) {
973
- errors = {
974
- ...(errors ? errors : {}),
975
- validation: valModule.validation
976
- };
977
- }
978
- return {
979
- path: valModule.path || moduleFilePath,
980
- // NOTE: we use path here, since SerializedModuleContent (maybe bad name?) can be used for whole modules as well as subparts of modules
981
- source: valModule.source,
982
- schema: valModule.schema,
983
- errors
984
- };
985
- }
986
- } finally {
987
- context.dispose();
988
- }
989
- };
990
-
991
- const getCompilerOptions = (rootDir, parseConfigHost) => {
992
- const tsConfigPath = path__namespace["default"].resolve(rootDir, "tsconfig.json");
993
- const jsConfigPath = path__namespace["default"].resolve(rootDir, "jsconfig.json");
994
- let configFilePath;
995
- if (parseConfigHost.fileExists(jsConfigPath)) {
996
- configFilePath = jsConfigPath;
997
- } else if (parseConfigHost.fileExists(tsConfigPath)) {
998
- configFilePath = tsConfigPath;
999
- } else {
1000
- throw Error(`Could not read config from: "${tsConfigPath}" nor "${jsConfigPath}". Root dir: "${rootDir}"`);
1001
- }
1002
- const {
1003
- config,
1004
- error
1005
- } = ts__default["default"].readConfigFile(configFilePath, parseConfigHost.readFile.bind(parseConfigHost));
1006
- if (error) {
1007
- if (typeof error.messageText === "string") {
1008
- throw Error(`Could not parse config file: ${configFilePath}. Error: ${error.messageText}`);
1009
- }
1010
- throw Error(`Could not parse config file: ${configFilePath}. Error: ${error.messageText.messageText}`);
1011
- }
1012
- const optionsOverrides = undefined;
1013
- const parsedConfigFile = ts__default["default"].parseJsonConfigFileContent(config, parseConfigHost, rootDir, optionsOverrides, configFilePath);
1014
- if (parsedConfigFile.errors.length > 0) {
1015
- throw Error(`Could not parse config file: ${configFilePath}. Errors: ${parsedConfigFile.errors.map(e => e.messageText).join("\n")}`);
1016
- }
1017
- return parsedConfigFile.options;
1018
- };
1019
-
1020
884
  class ValSourceFileHandler {
1021
885
  constructor(projectRoot, compilerOptions, host = {
1022
886
  ...ts__default["default"].sys,
@@ -1064,326 +928,204 @@ class ValSourceFileHandler {
1064
928
  }
1065
929
  }
1066
930
 
1067
- const JsFileLookupMapping = [
1068
- // NOTE: first one matching will be used
1069
- [".cjs.d.ts", [".esm.js", ".mjs.js"]], [".cjs.js", [".esm.js", ".mjs.js"]], [".cjs", [".mjs"]], [".d.ts", [".js", ".esm.js", ".mjs.js"]]];
1070
- const MAX_CACHE_SIZE = 100 * 1024 * 1024; // 100 mb
1071
- const MAX_OBJECT_KEY_SIZE = 2 ** 27; // https://stackoverflow.com/questions/13367391/is-there-a-limit-on-length-of-the-key-string-in-js-object
1072
-
1073
- class ValModuleLoader {
1074
- constructor(projectRoot, compilerOptions,
1075
- // TODO: remove this?
1076
- sourceFileHandler, host = {
1077
- ...ts__default["default"].sys,
1078
- writeFile: (fileName, data, encoding) => {
1079
- fs__default["default"].mkdirSync(path__namespace["default"].dirname(fileName), {
1080
- recursive: true
1081
- });
1082
- fs__default["default"].writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
1083
- },
1084
- rmFile: fs__default["default"].rmSync,
1085
- readBuffer: fileName => {
1086
- try {
1087
- return fs__default["default"].readFileSync(fileName);
1088
- } catch {
1089
- return undefined;
1090
- }
1091
- }
1092
- }, disableCache = false) {
1093
- this.projectRoot = projectRoot;
1094
- this.compilerOptions = compilerOptions;
1095
- this.sourceFileHandler = sourceFileHandler;
1096
- this.host = host;
1097
- this.disableCache = disableCache;
1098
- this.cache = {};
1099
- this.cacheSize = 0;
931
+ const getCompilerOptions = (rootDir, parseConfigHost) => {
932
+ const tsConfigPath = path__namespace["default"].resolve(rootDir, "tsconfig.json");
933
+ const jsConfigPath = path__namespace["default"].resolve(rootDir, "jsconfig.json");
934
+ let configFilePath;
935
+ if (parseConfigHost.fileExists(jsConfigPath)) {
936
+ configFilePath = jsConfigPath;
937
+ } else if (parseConfigHost.fileExists(tsConfigPath)) {
938
+ configFilePath = tsConfigPath;
939
+ } else {
940
+ throw Error(`Could not read config from: "${tsConfigPath}" nor "${jsConfigPath}". Root dir: "${rootDir}"`);
1100
941
  }
1101
- getModule(modulePath) {
1102
- if (!modulePath) {
1103
- throw Error(`Illegal module path: "${modulePath}"`);
1104
- }
1105
- const code = this.host.readFile(modulePath);
1106
- if (!code) {
1107
- throw Error(`Could not read file "${modulePath}"`);
1108
- }
1109
- let compiledCode;
1110
- if (this.cache[code] && !this.disableCache) {
1111
- // TODO: use hash instead of code as key
1112
- compiledCode = this.cache[code];
1113
- } else {
1114
- compiledCode = sucrase.transform(code, {
1115
- filePath: modulePath,
1116
- disableESTransforms: true,
1117
- transforms: ["typescript"]
1118
- }).code;
1119
- if (!this.disableCache) {
1120
- if (this.cacheSize > MAX_CACHE_SIZE) {
1121
- console.warn("Cache size exceeded, clearing cache");
1122
- this.cache = {};
1123
- this.cacheSize = 0;
1124
- }
1125
- if (code.length < MAX_OBJECT_KEY_SIZE) {
1126
- this.cache[code] = compiledCode;
1127
- this.cacheSize += code.length + compiledCode.length; // code is mostly ASCII so 1 byte per char
1128
- }
1129
- }
942
+ const {
943
+ config,
944
+ error
945
+ } = ts__default["default"].readConfigFile(configFilePath, parseConfigHost.readFile.bind(parseConfigHost));
946
+ if (error) {
947
+ if (typeof error.messageText === "string") {
948
+ throw Error(`Could not parse config file: ${configFilePath}. Error: ${error.messageText}`);
1130
949
  }
1131
- return compiledCode;
950
+ throw Error(`Could not parse config file: ${configFilePath}. Error: ${error.messageText.messageText}`);
1132
951
  }
1133
- resolveModulePath(containingFilePath, requestedModuleName) {
1134
- var _this$host$realpath, _this$host;
1135
- let sourceFileName = this.sourceFileHandler.resolveSourceModulePath(containingFilePath, requestedModuleName);
1136
- if (requestedModuleName === "@vercel/stega") {
1137
- sourceFileName = this.sourceFileHandler.resolveSourceModulePath(containingFilePath, "@vercel/stega").replace("stega/dist", "stega/dist/esm");
1138
- }
1139
- const matches = this.findMatchingJsFile(sourceFileName);
1140
- if (matches.match === false) {
1141
- let debugInfo = "";
1142
- if (sourceFileName.includes("val.config")) {
1143
- debugInfo = `\n@valbuild directory scan:\n${this.host.readDirectory("/", ["js", "ts", "json"], [], ["**/@valbuild/*"]).join("\n")}`;
1144
- }
1145
- throw Error(`Could not find matching js file for module "${requestedModuleName}" requested by: "${containingFilePath}". Tried:\n${matches.tried.join("\n")}${debugInfo}`);
1146
- }
1147
- const filePath = matches.match;
1148
- // resolve all symlinks (preconstruct for example symlinks the dist folder)
1149
- const followedPath = ((_this$host$realpath = (_this$host = this.host).realpath) === null || _this$host$realpath === void 0 ? void 0 : _this$host$realpath.call(_this$host, filePath)) ?? filePath;
1150
- if (!followedPath) {
1151
- throw Error(`File path was empty: "${filePath}", containing file: "${containingFilePath}", requested module: "${requestedModuleName}"`);
1152
- }
1153
- return followedPath;
952
+ const optionsOverrides = undefined;
953
+ const parsedConfigFile = ts__default["default"].parseJsonConfigFileContent(config, parseConfigHost, rootDir, optionsOverrides, configFilePath);
954
+ if (parsedConfigFile.errors.length > 0) {
955
+ throw Error(`Could not parse config file: ${configFilePath}. Errors: ${parsedConfigFile.errors.map(e => e.messageText).join("\n")}`);
1154
956
  }
1155
- findMatchingJsFile(filePath) {
1156
- let requiresReplacements = false;
1157
- for (const [currentEnding] of JsFileLookupMapping) {
1158
- if (filePath.endsWith(currentEnding)) {
1159
- requiresReplacements = true;
1160
- break;
1161
- }
1162
- }
1163
- // avoid unnecessary calls to fileExists if we don't need to replace anything
1164
- if (!requiresReplacements) {
1165
- if (this.host.fileExists(filePath)) {
1166
- return {
1167
- match: filePath
1168
- };
1169
- }
1170
- }
1171
- const tried = [];
1172
- for (const [currentEnding, replacements] of JsFileLookupMapping) {
1173
- if (filePath.endsWith(currentEnding)) {
1174
- for (const replacement of replacements) {
1175
- const newFilePath = filePath.slice(0, -currentEnding.length) + replacement;
1176
- if (this.host.fileExists(newFilePath)) {
1177
- return {
1178
- match: newFilePath
1179
- };
1180
- } else {
1181
- tried.push(newFilePath);
1182
- }
1183
- }
1184
- }
957
+ return parsedConfigFile.options;
958
+ };
959
+
960
+ /**
961
+ * Loads the project's root `val.modules.ts` (or `.js`) using Node's `vm`
962
+ * module and returns its default export (a `ValModules` registry).
963
+ *
964
+ * This is a recursive CommonJS loader: the root modules file and every
965
+ * relative `*.val.ts` / `val.config.ts` it (dynamically) imports are
966
+ * transpiled to CommonJS and evaluated in a `vm` sandbox. Bare specifiers
967
+ * (e.g. `@valbuild/core`) are resolved with the real Node `require` so the
968
+ * user modules share the exact same `@valbuild/core` instance that
969
+ * `extractValModules` uses.
970
+ *
971
+ * Mirrors the pattern already used by the CLI's `evalValConfigFile`.
972
+ *
973
+ * SECURITY: The `vm` context is NOT a security sandbox. It deliberately exposes
974
+ * `process` and a `require` that falls back to the real Node resolver (so user
975
+ * modules share the same `@valbuild/core` instance). This loader must therefore
976
+ * only ever be used to evaluate the project's own first-party, trusted files
977
+ * (`val.modules` and the local `*.val.ts`/`val.config.ts` it imports) i.e. the
978
+ * same trust level as running the project's build. It must never be used to
979
+ * evaluate untrusted or third-party modules.
980
+ */
981
+ function loadValModules(projectRoot) {
982
+ const valModulesPath = findValModulesPath(projectRoot);
983
+ if (!valModulesPath) {
984
+ throw Error(`Could not find 'val.modules.ts' nor 'val.modules.js' in project root: '${projectRoot}'`);
985
+ }
986
+ const compilerOptions = getCompilerOptions(projectRoot, ts__default["default"].sys);
987
+ const cache = {};
988
+ const loaded = loadModule(valModulesPath, cache, compilerOptions);
989
+ const valModules = loaded.exports.default;
990
+ if (!valModules) {
991
+ throw Error(`Val modules file at path: '${valModulesPath}' must have a default export. Got: ${valModules}`);
992
+ }
993
+ return valModules;
994
+ }
995
+ function findValModulesPath(projectRoot) {
996
+ for (const fileName of ["val.modules.ts", "val.modules.js"]) {
997
+ const candidate = path__namespace["default"].join(projectRoot, fileName);
998
+ if (fs__default["default"].existsSync(candidate)) {
999
+ return candidate;
1185
1000
  }
1186
- return {
1187
- match: false,
1188
- tried: tried.concat(filePath)
1189
- };
1190
1001
  }
1002
+ return null;
1191
1003
  }
1004
+ const RESOLVE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".cjs", ".mjs"];
1192
1005
 
1193
- async function newValQuickJSRuntime(quickJSModule, moduleLoader, {
1194
- maxStackSize = 1024 * 20,
1195
- // maximum stack size that works: 1024 * 640 * 8
1196
- memoryLimit = 1024 * 640 // 640 mbs
1197
- } = {}) {
1198
- const runtime = quickJSModule.newRuntime();
1199
- runtime.setMaxStackSize(maxStackSize);
1200
- runtime.setMemoryLimit(memoryLimit);
1201
- runtime.setModuleLoader(modulePath => {
1202
- try {
1203
- // Special cases to avoid loading the React packages since currently React does not have a ESM build:
1204
- // TODO: this is not stable, find a better way to do this
1205
- if (modulePath === "@valbuild/react") {
1206
- return {
1207
- value: "export const useVal = () => { throw Error(`Cannot use 'useVal' in this type of file`) }; export function ValProvider() { throw Error(`Cannot use 'ValProvider' in this type of file`) }; export function ValRichText() { throw Error(`Cannot use 'ValRichText' in this type of file`)};"
1208
- };
1209
- }
1210
- if (modulePath === "@valbuild/react/internal") {
1211
- return {
1212
- value: `
1213
- const useVal = () => { throw Error('Cannot use \\'useVal\\' in this type of file') };
1214
- export function ValProvider() { throw Error('Cannot use \\'ValProvider\\' in this type of file') };
1215
- export function ValRichText() { throw Error('Cannot use \\'ValRichText\\' in this type of file')};`
1216
- };
1217
- }
1218
- if (modulePath === "@valbuild/ui") {
1219
- return {
1220
- value: `
1221
- export const ValOverlay = () => {
1222
- throw Error("Cannot use 'ValOverlay' in this type of file")
1223
- };
1224
- export const VAL_CSS_PATH = "${ui.VAL_CSS_PATH}";
1225
- export const VAL_APP_PATH = "${ui.VAL_CSS_PATH}";
1226
- export const VAL_APP_ID = "${ui.VAL_APP_ID}";
1227
- export const VAL_OVERLAY_ID = "${ui.VAL_OVERLAY_ID}";
1228
- export const IS_DEV = false;
1229
- export const VERSION = "0.0.0";
1230
- `
1231
- };
1232
- }
1233
- if (modulePath === "@valbuild/react/stega") {
1234
- return {
1235
- value: "export const useVal = () => { throw Error(`Cannot use 'useVal' in this type of file`) };export const fetchVal = () => { throw Error(`Cannot use 'fetchVal' in this type of file`) }; export const autoTagJSX = () => { /* ignore */ }; export const stegaClean = () => { throw Error(`Cannot use 'stegaClean' in this type of file`) }; export const stegaDecodeStrings = () => { throw Error(`Cannot use 'stegaDecodeStrings' in this type of file`) }; export const stegaEncode = () => { throw Error(`Cannot use 'stegaEncode' in this type of file`) }; export const raw = () => { throw Error(`Cannot use 'raw' in this type of file`) }; export const attrs = () => { throw Error(`Cannot use 'attrs' in this type of file`) }; "
1236
- };
1006
+ // Specifiers that user val files must not actually use. We stub them so that
1007
+ // importing is fine, but using a value throws a clear error. Real @valbuild
1008
+ // packages are resolved via the real require, so when they (legitimately)
1009
+ // import react/next internally those go through Node, not this stub.
1010
+ function isStubbedSpecifier(spec) {
1011
+ return spec === "react" || spec.startsWith("react/") || spec === "next" || spec.startsWith("next/") || spec === "@valbuild/ui" || spec === "@valbuild/react" || spec.startsWith("@valbuild/react/");
1012
+ }
1013
+ function makeStub(spec) {
1014
+ const throwing = prop => () => {
1015
+ throw Error(`Cannot use '${prop}' from '${spec}' in this type of file`);
1016
+ };
1017
+ const handler = {
1018
+ get(_target, prop) {
1019
+ if (prop === "__esModule") {
1020
+ return true;
1237
1021
  }
1238
- if (modulePath.startsWith("next/navigation")) {
1239
- return {
1240
- value: "export const usePathname = () => { throw Error(`Cannot use 'usePathname' in this type of file`) }; export const useRouter = () => { throw Error(`Cannot use 'useRouter' in this type of file`) }; export default new Proxy({}, { get() { return () => { throw new Error(`Cannot import 'next' in this file`) } } } );"
1241
- };
1022
+ if (typeof prop === "symbol") {
1023
+ return undefined;
1242
1024
  }
1243
- if (modulePath.startsWith("next")) {
1244
- return {
1245
- value: "export default new Proxy({}, { get() { return () => { throw new Error(`Cannot import 'next' in this file`) } } } );"
1246
- };
1025
+ // React.createContext is sometimes called at module top-level; return a
1026
+ // proxy-returning function so evaluation does not crash on import.
1027
+ if (prop === "createContext") {
1028
+ return () => new Proxy({}, handler);
1247
1029
  }
1248
- if (modulePath.startsWith("react/jsx-runtime")) {
1249
- return {
1250
- value: "export const jsx = () => { throw Error(`Cannot use 'jsx' in this type of file`) }; export const Fragment = () => { throw Error(`Cannot use 'Fragment' in this type of file`) }; export default new Proxy({}, { get() { return () => { throw new Error(`Cannot import 'react' in this file`) } } } ); export const jsxs = () => { throw Error(`Cannot use 'jsxs' in this type of file`) };"
1251
- };
1030
+ if (prop === "default") {
1031
+ return stub;
1252
1032
  }
1253
- if (modulePath.startsWith("react")) {
1254
- return {
1255
- value: `
1256
- export const createContext = () => new Proxy({}, { get() { return () => { throw new Error('Cannot use \\'createContext\\' in this file') } } } );
1257
- export const useTransition = () => { throw Error('Cannot use \\'useTransition\\' in this type of file') };
1258
-
1259
- export default new Proxy({}, {
1260
- get(target, props) {
1261
- // React.createContext might be called on top-level
1262
- if (props === 'createContext') {
1263
- return createContext;
1264
- }
1265
- return () => {
1266
- throw new Error('Cannot import \\'react\\' in this file');
1033
+ return throwing(prop);
1267
1034
  }
1035
+ };
1036
+ const stub = new Proxy({}, handler);
1037
+ return stub;
1038
+ }
1039
+ function loadModule(absPath, cache, compilerOptions) {
1040
+ const cached = cache[absPath];
1041
+ if (cached) {
1042
+ return cached;
1043
+ }
1044
+ const code = fs__default["default"].readFileSync(absPath, "utf-8");
1045
+ const transpiled = ts__default["default"].transpileModule(code, {
1046
+ compilerOptions: {
1047
+ target: ts__default["default"].ScriptTarget.ES2020,
1048
+ module: ts__default["default"].ModuleKind.CommonJS,
1049
+ esModuleInterop: true,
1050
+ jsx: ts__default["default"].JsxEmit.ReactJSX
1051
+ },
1052
+ fileName: absPath
1053
+ });
1054
+ const moduleObj = {
1055
+ exports: {}
1056
+ };
1057
+ // Insert into the cache before evaluating so cyclic imports resolve.
1058
+ cache[absPath] = moduleObj;
1059
+ const dirName = path__namespace["default"].dirname(absPath);
1060
+ const realRequire = node_module.Module.createRequire(absPath);
1061
+ const customRequire = spec => {
1062
+ var _ts$resolveModuleName;
1063
+ if (isStubbedSpecifier(spec)) {
1064
+ return makeStub(spec);
1065
+ }
1066
+ if (spec.startsWith(".") || path__namespace["default"].isAbsolute(spec)) {
1067
+ const resolved = resolveRelative(dirName, spec);
1068
+ if (!resolved) {
1069
+ throw Error(`Could not resolve module '${spec}' from '${absPath}'`);
1070
+ }
1071
+ return loadModule(resolved, cache, compilerOptions).exports;
1072
+ }
1073
+ // Non-relative specifier: it might be a tsconfig path alias (e.g. "_/val.config")
1074
+ // pointing at a local source file, or an actual node_modules package.
1075
+ const tsResolved = (_ts$resolveModuleName = ts__default["default"].resolveModuleName(spec, absPath, compilerOptions, ts__default["default"].sys).resolvedModule) === null || _ts$resolveModuleName === void 0 ? void 0 : _ts$resolveModuleName.resolvedFileName;
1076
+ if (tsResolved && !tsResolved.includes("/node_modules/") && !tsResolved.endsWith(".d.ts")) {
1077
+ return loadModule(tsResolved, cache, compilerOptions).exports;
1078
+ }
1079
+ // Real node_modules package – use the real require so user modules share
1080
+ // the same @valbuild/core instance as extractValModules.
1081
+ return realRequire(spec);
1082
+ };
1083
+
1084
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1085
+ const sandbox = {
1086
+ exports: moduleObj.exports,
1087
+ module: moduleObj,
1088
+ require: customRequire,
1089
+ __filename: absPath,
1090
+ __dirname: dirName,
1091
+ console,
1092
+ process
1093
+ };
1094
+ sandbox.global = sandbox;
1095
+ sandbox.globalThis = sandbox;
1096
+ const context = vm__default["default"].createContext(sandbox);
1097
+ const script = new vm__default["default"].Script(transpiled.outputText, {
1098
+ filename: absPath
1099
+ });
1100
+ script.runInContext(context);
1101
+ return moduleObj;
1102
+ }
1103
+ function resolveRelative(dirName, spec) {
1104
+ const base = path__namespace["default"].resolve(dirName, spec);
1105
+ // Exact file (with extension)
1106
+ if (fs__default["default"].existsSync(base) && fs__default["default"].statSync(base).isFile()) {
1107
+ return base;
1268
1108
  }
1269
- })`
1270
- };
1271
- }
1272
- if (modulePath.includes("/ValNextProvider")) {
1273
- return {
1274
- value: "export const ValNextProvider = new Proxy({}, { get() { return () => { throw new Error(`Cannot import 'ValNextProvider' in this file`) } } } )"
1275
- };
1276
- }
1277
- if (modulePath.includes("/ValContext")) {
1278
- return {
1279
- value: "export const useValEvents = () => { throw Error(`Cannot use 'useValEvents' in this type of file`) }; export const ValContext = new Proxy({}, { get() { return () => { throw new Error(`Cannot import 'ValContext' in this file`) } } } ) export const ValEvents = new Proxy({}, { get() { return () => { throw new Error(`Cannot import 'ValEvents' in this file`) } } } )"
1280
- };
1281
- }
1282
- if (modulePath.includes("/ValImage")) {
1283
- return {
1284
- value: "export const ValImage = new Proxy({}, { get() { return () => { throw new Error(`Cannot import 'ValImage' in this file`) } } } )"
1285
- };
1286
- }
1287
- if (modulePath.includes("/ValApp")) {
1288
- return {
1289
- value: "export const ValApp = new Proxy({}, { get() { return () => { throw new Error(`Cannot import 'ValApp' in this file`) } } } )"
1290
- };
1291
- }
1292
- if (modulePath.includes("/ValModulesClient")) {
1293
- return {
1294
- value: "export const ValModulesClient = new Proxy({}, { get() { return () => { throw new Error(`Cannot import 'ValModulesClient' in this file`) } } } ); export const useRegisterValModules = () => { throw new Error(`Cannot use 'useRegisterValModules' in this type of file`) };"
1295
- };
1296
- }
1297
- return {
1298
- value: moduleLoader.getModule(modulePath)
1299
- };
1300
- } catch {
1301
- return {
1302
- error: Error(`Could not resolve module: '${modulePath}'`)
1303
- };
1109
+ // Probe extensions (handles `./x.val` -> `./x.val.ts`)
1110
+ for (const ext of RESOLVE_EXTENSIONS) {
1111
+ const candidate = base + ext;
1112
+ if (fs__default["default"].existsSync(candidate)) {
1113
+ return candidate;
1304
1114
  }
1305
- }, (baseModuleName, requestedName) => {
1306
- try {
1307
- if (requestedName === "@valbuild/react") {
1308
- return {
1309
- value: requestedName
1310
- };
1311
- }
1312
- if (requestedName === "@valbuild/react/stega") {
1313
- return {
1314
- value: requestedName
1315
- };
1316
- }
1317
- if (requestedName === "@valbuild/react/internal") {
1318
- return {
1319
- value: requestedName
1320
- };
1321
- }
1322
- if (requestedName === "@valbuild/ui") {
1323
- return {
1324
- value: requestedName
1325
- };
1326
- }
1327
- if (requestedName.startsWith("next/navigation")) {
1328
- return {
1329
- value: requestedName
1330
- };
1331
- }
1332
- if (requestedName.startsWith("next")) {
1333
- return {
1334
- value: requestedName
1335
- };
1336
- }
1337
- if (requestedName.startsWith("react/jsx-runtime")) {
1338
- return {
1339
- value: requestedName
1340
- };
1341
- }
1342
- if (requestedName.startsWith("react")) {
1343
- return {
1344
- value: requestedName
1345
- };
1346
- }
1347
- if (requestedName.includes("/ValNextProvider")) {
1348
- return {
1349
- value: requestedName
1350
- };
1351
- }
1352
- if (requestedName.includes("/ValContext")) {
1353
- return {
1354
- value: requestedName
1355
- };
1356
- }
1357
- if (requestedName.includes("/ValImage")) {
1358
- return {
1359
- value: requestedName
1360
- };
1361
- }
1362
- if (requestedName.includes("/ValApp")) {
1363
- return {
1364
- value: requestedName
1365
- };
1366
- }
1367
- if (requestedName.includes("/ValModulesClient")) {
1368
- return {
1369
- value: requestedName
1370
- };
1115
+ }
1116
+ // Directory index
1117
+ if (fs__default["default"].existsSync(base) && fs__default["default"].statSync(base).isDirectory()) {
1118
+ for (const ext of RESOLVE_EXTENSIONS) {
1119
+ const candidate = path__namespace["default"].join(base, "index" + ext);
1120
+ if (fs__default["default"].existsSync(candidate)) {
1121
+ return candidate;
1371
1122
  }
1372
- const modulePath = moduleLoader.resolveModulePath(baseModuleName, requestedName);
1373
- return {
1374
- value: modulePath
1375
- };
1376
- } catch (e) {
1377
- console.debug(`Could not resolve ${requestedName} in ${baseModuleName}`, e);
1378
- return {
1379
- value: requestedName
1380
- };
1381
1123
  }
1382
- });
1383
- return runtime;
1124
+ }
1125
+ return null;
1384
1126
  }
1385
1127
 
1386
- async function createService(projectRoot, opts, host = {
1128
+ async function createService(projectRoot, host = {
1387
1129
  ...ts__default["default"].sys,
1388
1130
  writeFile: (fileName, data, encoding) => {
1389
1131
  fs__default["default"].mkdirSync(path__namespace["default"].dirname(fileName), {
@@ -1399,48 +1141,73 @@ async function createService(projectRoot, opts, host = {
1399
1141
  return undefined;
1400
1142
  }
1401
1143
  }
1402
- }, loader) {
1144
+ }) {
1403
1145
  const compilerOptions = getCompilerOptions(projectRoot, host);
1404
1146
  const sourceFileHandler = new ValSourceFileHandler(projectRoot, compilerOptions, host);
1405
- const module = await quickjsEmscripten.newQuickJSWASMModule();
1406
- const runtime = await newValQuickJSRuntime(module, loader || new ValModuleLoader(projectRoot, compilerOptions, sourceFileHandler, host, opts.disableCache === undefined ? true : opts.disableCache));
1407
- return new Service(projectRoot, sourceFileHandler, runtime);
1147
+ const valModules = loadValModules(projectRoot);
1148
+ const extracted = await core.extractValModules(valModules);
1149
+ return new Service(projectRoot, sourceFileHandler, extracted);
1408
1150
  }
1409
1151
  class Service {
1410
- constructor(projectRoot, sourceFileHandler, runtime) {
1152
+ constructor(projectRoot, sourceFileHandler, extracted) {
1411
1153
  this.sourceFileHandler = sourceFileHandler;
1412
- this.runtime = runtime;
1154
+ this.extracted = extracted;
1413
1155
  this.projectRoot = projectRoot;
1414
1156
  }
1157
+
1158
+ /**
1159
+ * The module file paths that are registered in the project's val.modules.
1160
+ */
1161
+ getModuleFilePaths() {
1162
+ return Object.keys(this.extracted.sources);
1163
+ }
1415
1164
  async get(moduleFilePath, modulePath, options) {
1416
- const valModule = await readValFile(moduleFilePath, this.projectRoot, this.runtime, options ?? {
1417
- validate: true,
1418
- source: true,
1419
- schema: true
1420
- });
1421
- if (valModule.source && valModule.schema) {
1422
- const resolved = core.Internal.resolvePath(modulePath, valModule.source, valModule.schema);
1423
- const sourcePath = resolved.path ? [moduleFilePath, resolved.path].join(".") : moduleFilePath;
1165
+ const opts = options ?? {
1166
+ validate: true
1167
+ };
1168
+ const source = this.extracted.sources[moduleFilePath];
1169
+ const schema = this.extracted.schemas[moduleFilePath];
1170
+ const serializedSchema = this.extracted.serializedSchemas[moduleFilePath];
1171
+ const moduleError = this.extracted.moduleErrors.find(e => e.path === moduleFilePath);
1172
+ if (source === undefined || schema === undefined || serializedSchema === undefined) {
1173
+ return {
1174
+ path: moduleFilePath,
1175
+ errors: {
1176
+ invalidModulePath: moduleFilePath,
1177
+ fatal: [{
1178
+ message: (moduleError === null || moduleError === void 0 ? void 0 : moduleError.message) ?? `Module '${moduleFilePath}' was not found in val.modules`
1179
+ }]
1180
+ }
1181
+ };
1182
+ }
1183
+ const validation = opts.validate ? schema["executeValidate"](moduleFilePath, source) : false;
1184
+ const resolved = core.Internal.resolvePath(modulePath, source, serializedSchema);
1185
+ const sourcePath = resolved.path ? [moduleFilePath, resolved.path].join(".") : moduleFilePath;
1186
+ if (!validation && !moduleError) {
1424
1187
  return {
1425
1188
  path: sourcePath,
1426
- schema: resolved.schema instanceof core.Schema ? resolved.schema["executeSerialize"]() : resolved.schema,
1427
1189
  source: resolved.source,
1428
- errors: valModule.errors && valModule.errors.validation ? {
1429
- validation: valModule.errors.validation || undefined,
1430
- fatal: valModule.errors.fatal || undefined
1431
- } : valModule.errors ? {
1432
- fatal: valModule.errors.fatal || undefined
1433
- } : false
1190
+ schema: resolved.schema,
1191
+ errors: false
1434
1192
  };
1435
- } else {
1436
- return valModule;
1437
1193
  }
1194
+ return {
1195
+ path: sourcePath,
1196
+ source: resolved.source,
1197
+ schema: resolved.schema,
1198
+ errors: {
1199
+ validation: validation || undefined,
1200
+ fatal: moduleError ? [{
1201
+ message: moduleError.message
1202
+ }] : undefined
1203
+ }
1204
+ };
1438
1205
  }
1439
1206
  async patch(moduleFilePath, patch) {
1440
- await patchValFile(moduleFilePath, this.projectRoot, patch, this.sourceFileHandler, this.runtime);
1207
+ await patchValFile(moduleFilePath, this.projectRoot, patch, this.sourceFileHandler);
1441
1208
  }
1442
1209
  dispose() {
1443
- this.runtime.dispose();
1210
+ // No-op: the vm-based loader holds no disposable resources.
1444
1211
  }
1445
1212
  }
1446
1213
 
@@ -1590,6 +1357,14 @@ class ValOps {
1590
1357
  async getSchemas() {
1591
1358
  return this.initSources().then(result => result.schemas);
1592
1359
  }
1360
+ async getSerializedSchemas() {
1361
+ const schemas = await this.getSchemas();
1362
+ const serialized = {};
1363
+ for (const [moduleFilePathS, schema] of Object.entries(schemas)) {
1364
+ serialized[moduleFilePathS] = schema["executeSerialize"]();
1365
+ }
1366
+ return serialized;
1367
+ }
1593
1368
  async getModuleErrors() {
1594
1369
  return this.initSources().then(result => result.moduleErrors);
1595
1370
  }
@@ -1614,6 +1389,7 @@ class ValOps {
1614
1389
  if (patch.appliedAt) {
1615
1390
  continue;
1616
1391
  }
1392
+ let hasSourceFileOps = false;
1617
1393
  for (const op of patch.patch) {
1618
1394
  if (op.op === "file") {
1619
1395
  const filePath = op.filePath;
@@ -1624,6 +1400,13 @@ class ValOps {
1624
1400
  };
1625
1401
  continue;
1626
1402
  }
1403
+ hasSourceFileOps = true;
1404
+ }
1405
+ // Once per patch, NOT once per op: prepare() re-looks-up the patch by id
1406
+ // and applies the whole thing for every entry, so a patch with two source
1407
+ // ops used to be applied twice. Idempotent for "replace", destructive for
1408
+ // array add/remove/move.
1409
+ if (hasSourceFileOps) {
1627
1410
  const path = patch.path;
1628
1411
  if (!patchesByModule[path]) {
1629
1412
  patchesByModule[path] = [];
@@ -1743,6 +1526,26 @@ class ValOps {
1743
1526
  };
1744
1527
  }
1745
1528
 
1529
+ /**
1530
+ * Every module's source, with the pending patches applied.
1531
+ *
1532
+ * `getSources(analysis)` returns ONLY the modules that had patches, which is
1533
+ * not enough to validate with: cross-module checks (keyOf, router routes)
1534
+ * resolve against other modules' sources and report spurious errors when they
1535
+ * are absent. `/sources/~` overlays the two for exactly this reason.
1536
+ */
1537
+ async getSourcesWithPatchesApplied(analysis) {
1538
+ const unpatched = await this.getSources();
1539
+ const patched = await this.getSources(analysis);
1540
+ return {
1541
+ sources: {
1542
+ ...unpatched.sources,
1543
+ ...patched.sources
1544
+ },
1545
+ errors: patched.errors
1546
+ };
1547
+ }
1548
+
1746
1549
  // #region validateSources
1747
1550
  async validateSources(schemas, sources, patchesByModule) {
1748
1551
  const errors = {};
@@ -2018,13 +1821,28 @@ class ValOps {
2018
1821
  }
2019
1822
 
2020
1823
  // #region prepareCommit
2021
- async prepare(patchAnalysis) {
1824
+ /**
1825
+ * Applies the pending patches to the source files so they can be committed.
1826
+ *
1827
+ * @param options.continueOnError Diagnosis only. By default a patch that
1828
+ * cannot be applied aborts the rest of that module's chain, which is what
1829
+ * /save requires: the commit is refused and nothing is written. With this
1830
+ * flag the failing patch is recorded in `unappliablePatches` and the chain
1831
+ * continues on the unchanged source file, so a single run reports *every*
1832
+ * unappliable patch instead of only the first one per module. The commit is
1833
+ * still refused (`hasErrors` stays true) - this only makes the report
1834
+ * complete.
1835
+ */
1836
+ async prepare(patchAnalysis, options) {
1837
+ const continueOnError = (options === null || options === void 0 ? void 0 : options.continueOnError) ?? false;
2022
1838
  const {
2023
1839
  patchesByModule,
2024
1840
  fileLastUpdatedByPatchId
2025
1841
  } = patchAnalysis;
2026
1842
  const patchedSourceFiles = {};
2027
1843
  const previousSourceFiles = {};
1844
+ const partiallyPatchedSourceFiles = {};
1845
+ const unappliablePatches = {};
2028
1846
  const applySourceFilePatches = async (path, patches) => {
2029
1847
  const sourceFileRes = await this.getSourceFile(path);
2030
1848
  const errors = [];
@@ -2049,9 +1867,18 @@ class ValOps {
2049
1867
  } of patches) {
2050
1868
  const patchData = patchAnalysis.patches.find(p => p.patchId === patchId);
2051
1869
  if (!patchData) {
1870
+ const message = `Analysis required non-existing patch: ${patchId}`;
2052
1871
  errors.push({
2053
- message: `Analysis required non-existing patch: ${patchId}`
1872
+ message
2054
1873
  });
1874
+ unappliablePatches[patchId] = {
1875
+ moduleFilePath: path,
1876
+ message
1877
+ };
1878
+ triedPatches.push(patchId);
1879
+ if (continueOnError) {
1880
+ continue;
1881
+ }
2055
1882
  break;
2056
1883
  }
2057
1884
  const patch$1 = patchData.patch;
@@ -2077,12 +1904,29 @@ class ValOps {
2077
1904
  }, null, 2));
2078
1905
  errors.push(patchRes.error);
2079
1906
  }
1907
+ unappliablePatches[patchId] = {
1908
+ moduleFilePath: path,
1909
+ message: formatPatchSourceError(patchRes.error)
1910
+ };
2080
1911
  triedPatches.push(patchId);
1912
+ if (continueOnError) {
1913
+ // Continue from the unchanged source file: the failing patch made
1914
+ // no change, so the rest of the chain applies on top of the state
1915
+ // it had before it. This mirrors what the client already does in
1916
+ // ValSyncEngine.getPatchedSource.
1917
+ continue;
1918
+ }
2081
1919
  break;
2082
1920
  }
2083
1921
  appliedPatches.push(patchId);
2084
1922
  tsSourceFile = patchRes.value;
2085
1923
  }
1924
+ if (errors.length > 0 && continueOnError) {
1925
+ // Diagnosis: expose what the source file would look like with the
1926
+ // appliable patches applied, so a caller can diff it even though the
1927
+ // commit is (correctly) refused.
1928
+ partiallyPatchedSourceFiles[path] = unescape(tsSourceFile.getText(tsSourceFile).replace(/\\u/g, "%u"));
1929
+ }
2086
1930
  if (errors.length === 0) {
2087
1931
  var _this$options;
2088
1932
  // https://github.com/microsoft/TypeScript/issues/36174
@@ -2168,8 +2012,10 @@ class ValOps {
2168
2012
  hasErrors,
2169
2013
  sourceFilePatchErrors,
2170
2014
  binaryFilePatchErrors,
2015
+ unappliablePatches,
2171
2016
  patchedSourceFiles,
2172
2017
  previousSourceFiles,
2018
+ partiallyPatchedSourceFiles,
2173
2019
  patchedBinaryFilesDescriptors,
2174
2020
  appliedPatches,
2175
2021
  skippedPatches,
@@ -2178,6 +2024,18 @@ class ValOps {
2178
2024
  return res;
2179
2025
  }
2180
2026
 
2027
+ /**
2028
+ * Reads a project file as text at whatever revision this ops instance points
2029
+ * at: the deployed commit in http mode, the working tree in fs mode.
2030
+ *
2031
+ * Public counterpart of `getSourceFile`, for the CLI's debug snapshot. The
2032
+ * snapshot has to capture the exact text `prepare` patches, which in http mode
2033
+ * is NOT the local working copy.
2034
+ */
2035
+ async readProjectFile(path) {
2036
+ return this.getSourceFile(path);
2037
+ }
2038
+
2181
2039
  // #region createPatch
2182
2040
  async createPatch(path, patch, patchId, parentRef, sessionId, authorId) {
2183
2041
  const saveRes = await this.saveSourceFilePatch(path, patch, patchId, parentRef, authorId, sessionId);
@@ -2214,6 +2072,16 @@ function isFileSource(value) {
2214
2072
  }
2215
2073
  return false;
2216
2074
  }
2075
+ function formatPatchSourceError(error) {
2076
+ if ("message" in error) {
2077
+ return error.message;
2078
+ } else if (Array.isArray(error)) {
2079
+ return error.map(formatPatchSourceError).join("\n");
2080
+ } else {
2081
+ const _exhaustiveCheck = error;
2082
+ return "Unknown patch source error: " + JSON.stringify(_exhaustiveCheck);
2083
+ }
2084
+ }
2217
2085
  function getFieldsForType(type) {
2218
2086
  if (type === "file") {
2219
2087
  return ["mimeType"];
@@ -3491,14 +3359,22 @@ const NonceResponse = zod.z.object({
3491
3359
  class ValOpsHttp extends ValOps {
3492
3360
  constructor(contentUrl, project, commitSha,
3493
3361
  // TODO: CommitSha
3494
- branch, apiKey, valModules, options) {
3362
+ branch,
3363
+ /**
3364
+ * An api key (how the app itself authenticates) or a personal access token
3365
+ * (how the CLI authenticates after `val login`). Same two shapes as
3366
+ * getSettings / uploadRemoteFile / getPresignedAuthNonce.
3367
+ */
3368
+ auth, valModules, options) {
3495
3369
  super(valModules, options);
3496
3370
  this.contentUrl = contentUrl;
3497
3371
  this.project = project;
3498
3372
  this.commitSha = commitSha;
3499
3373
  this.branch = branch;
3500
- this.authHeaders = {
3501
- Authorization: `Bearer ${apiKey}`
3374
+ this.authHeaders = "pat" in auth ? {
3375
+ "x-val-pat": auth.pat
3376
+ } : {
3377
+ Authorization: `Bearer ${auth.apiKey}`
3502
3378
  };
3503
3379
  this.root = (options === null || options === void 0 ? void 0 : options.root) ?? "";
3504
3380
  }
@@ -3824,8 +3700,31 @@ class ValOpsHttp extends ValOps {
3824
3700
  allErrors.push(...res.errors);
3825
3701
  }
3826
3702
  }
3703
+ // Chunking is a query-string-length workaround, NOT a filter: the content
3704
+ // api returns every applicable patch per request regardless of which
3705
+ // patch_ids we ask for. Concatenating the chunks therefore repeats the
3706
+ // whole chain once per chunk, and prepare() applies each patch that many
3707
+ // times - which corrupts arrays (a "remove" runs N times) and fails the
3708
+ // commit with "Array index out of bounds". Only bites above chunkSize
3709
+ // pending patches, so it stays invisible until a project accumulates them.
3710
+ //
3711
+ // Dedupe by patch id, keeping first occurrence so the api's ordering is
3712
+ // preserved, and keep only what was asked for. Correct whether or not the
3713
+ // api filters on its side.
3714
+ const requestedPatchIds = new Set(patchIds);
3715
+ const seenPatchIds = new Set();
3716
+ const patches = allPatches.filter(patch => {
3717
+ if (!requestedPatchIds.has(patch.patchId)) {
3718
+ return false;
3719
+ }
3720
+ if (seenPatchIds.has(patch.patchId)) {
3721
+ return false;
3722
+ }
3723
+ seenPatchIds.add(patch.patchId);
3724
+ return true;
3725
+ });
3827
3726
  return {
3828
- patches: allPatches,
3727
+ patches,
3829
3728
  errors: Object.keys(allErrors).length > 0 ? allErrors : undefined
3830
3729
  };
3831
3730
  }
@@ -3920,7 +3819,7 @@ class ValOpsHttp extends ValOps {
3920
3819
  return {
3921
3820
  patches,
3922
3821
  error: {
3923
- message: "Could not your changes. It is most likely due to a network issue. Check your network connection and please try again."
3822
+ message: "Could not get your changes. It is most likely due to a network issue. Check your network connection and please try again."
3924
3823
  }
3925
3824
  };
3926
3825
  } catch (err) {
@@ -4547,7 +4446,9 @@ const ValServer = (valModules, options, callbacks) => {
4547
4446
  config: options.config
4548
4447
  });
4549
4448
  } else if (options.mode === "http") {
4550
- serverOps = new ValOpsHttp(options.valContentUrl, options.project, options.commit, options.branch, options.apiKey, valModules, {
4449
+ serverOps = new ValOpsHttp(options.valContentUrl, options.project, options.commit, options.branch, {
4450
+ apiKey: options.apiKey
4451
+ }, valModules, {
4551
4452
  formatter: options.formatter,
4552
4453
  root: options.root,
4553
4454
  config: options.config
@@ -6771,16 +6672,6 @@ const ValServer = (valModules, options, callbacks) => {
6771
6672
  }
6772
6673
  };
6773
6674
  };
6774
- function formatPatchSourceError(error) {
6775
- if ("message" in error) {
6776
- return error.message;
6777
- } else if (Array.isArray(error)) {
6778
- return error.map(formatPatchSourceError).join("\n");
6779
- } else {
6780
- const _exhaustiveCheck = error;
6781
- return "Unknown patch source error: " + JSON.stringify(_exhaustiveCheck);
6782
- }
6783
- }
6784
6675
  function verifyCallbackReq(stateCookie, queryParams) {
6785
6676
  if (typeof stateCookie !== "string") {
6786
6677
  return {
@@ -7234,8 +7125,28 @@ async function readCommit(gitDir, branchName) {
7234
7125
  }
7235
7126
  function createValApiRouter(route, valServerPromise, convert) {
7236
7127
  const uiRequestHandler = server.createUIRequestHandler();
7128
+ // valServerPromise is created at module-eval time, but only awaited per request.
7129
+ // Without this no-op catch, a config error (e.g. proxy mode without a project)
7130
+ // rejects with no handler attached, which becomes an unhandledRejection and kills
7131
+ // the dev server. The error is still reported per request by the try/catch below.
7132
+ valServerPromise.catch(() => {
7133
+ // handled below
7134
+ });
7237
7135
  return async req => {
7238
- const valServer = await valServerPromise;
7136
+ let valServer;
7137
+ try {
7138
+ valServer = await valServerPromise;
7139
+ } catch (err) {
7140
+ const error = {
7141
+ message: "Val: could not start the Val server",
7142
+ details: err instanceof Error ? err.message : String(err)
7143
+ };
7144
+ console.error(error.message + ": " + error.details);
7145
+ return convert({
7146
+ status: 500,
7147
+ json: error
7148
+ });
7149
+ }
7239
7150
  const url = new URL(req.url);
7240
7151
  if (!url.pathname.startsWith(route)) {
7241
7152
  const error = {
@@ -7448,6 +7359,132 @@ function getCookies(req, cookiesDef) {
7448
7359
  return zod.z.object(cookiesDef).safeParse(input);
7449
7360
  }
7450
7361
 
7362
+ const JsFileLookupMapping = [
7363
+ // NOTE: first one matching will be used
7364
+ [".cjs.d.ts", [".esm.js", ".mjs.js"]], [".cjs.js", [".esm.js", ".mjs.js"]], [".cjs", [".mjs"]], [".d.ts", [".js", ".esm.js", ".mjs.js"]]];
7365
+ const MAX_CACHE_SIZE = 100 * 1024 * 1024; // 100 mb
7366
+ const MAX_OBJECT_KEY_SIZE = 2 ** 27; // https://stackoverflow.com/questions/13367391/is-there-a-limit-on-length-of-the-key-string-in-js-object
7367
+
7368
+ class ValModuleLoader {
7369
+ constructor(projectRoot, compilerOptions,
7370
+ // TODO: remove this?
7371
+ sourceFileHandler, host = {
7372
+ ...ts__default["default"].sys,
7373
+ writeFile: (fileName, data, encoding) => {
7374
+ fs__default["default"].mkdirSync(path__namespace["default"].dirname(fileName), {
7375
+ recursive: true
7376
+ });
7377
+ fs__default["default"].writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
7378
+ },
7379
+ rmFile: fs__default["default"].rmSync,
7380
+ readBuffer: fileName => {
7381
+ try {
7382
+ return fs__default["default"].readFileSync(fileName);
7383
+ } catch {
7384
+ return undefined;
7385
+ }
7386
+ }
7387
+ }, disableCache = false) {
7388
+ this.projectRoot = projectRoot;
7389
+ this.compilerOptions = compilerOptions;
7390
+ this.sourceFileHandler = sourceFileHandler;
7391
+ this.host = host;
7392
+ this.disableCache = disableCache;
7393
+ this.cache = {};
7394
+ this.cacheSize = 0;
7395
+ }
7396
+ getModule(modulePath) {
7397
+ if (!modulePath) {
7398
+ throw Error(`Illegal module path: "${modulePath}"`);
7399
+ }
7400
+ const code = this.host.readFile(modulePath);
7401
+ if (!code) {
7402
+ throw Error(`Could not read file "${modulePath}"`);
7403
+ }
7404
+ let compiledCode;
7405
+ if (this.cache[code] && !this.disableCache) {
7406
+ // TODO: use hash instead of code as key
7407
+ compiledCode = this.cache[code];
7408
+ } else {
7409
+ compiledCode = sucrase.transform(code, {
7410
+ filePath: modulePath,
7411
+ disableESTransforms: true,
7412
+ transforms: ["typescript"]
7413
+ }).code;
7414
+ if (!this.disableCache) {
7415
+ if (this.cacheSize > MAX_CACHE_SIZE) {
7416
+ console.warn("Cache size exceeded, clearing cache");
7417
+ this.cache = {};
7418
+ this.cacheSize = 0;
7419
+ }
7420
+ if (code.length < MAX_OBJECT_KEY_SIZE) {
7421
+ this.cache[code] = compiledCode;
7422
+ this.cacheSize += code.length + compiledCode.length; // code is mostly ASCII so 1 byte per char
7423
+ }
7424
+ }
7425
+ }
7426
+ return compiledCode;
7427
+ }
7428
+ resolveModulePath(containingFilePath, requestedModuleName) {
7429
+ var _this$host$realpath, _this$host;
7430
+ let sourceFileName = this.sourceFileHandler.resolveSourceModulePath(containingFilePath, requestedModuleName);
7431
+ if (requestedModuleName === "@vercel/stega") {
7432
+ sourceFileName = this.sourceFileHandler.resolveSourceModulePath(containingFilePath, "@vercel/stega").replace("stega/dist", "stega/dist/esm");
7433
+ }
7434
+ const matches = this.findMatchingJsFile(sourceFileName);
7435
+ if (matches.match === false) {
7436
+ let debugInfo = "";
7437
+ if (sourceFileName.includes("val.config")) {
7438
+ debugInfo = `\n@valbuild directory scan:\n${this.host.readDirectory("/", ["js", "ts", "json"], [], ["**/@valbuild/*"]).join("\n")}`;
7439
+ }
7440
+ throw Error(`Could not find matching js file for module "${requestedModuleName}" requested by: "${containingFilePath}". Tried:\n${matches.tried.join("\n")}${debugInfo}`);
7441
+ }
7442
+ const filePath = matches.match;
7443
+ // resolve all symlinks (preconstruct for example symlinks the dist folder)
7444
+ const followedPath = ((_this$host$realpath = (_this$host = this.host).realpath) === null || _this$host$realpath === void 0 ? void 0 : _this$host$realpath.call(_this$host, filePath)) ?? filePath;
7445
+ if (!followedPath) {
7446
+ throw Error(`File path was empty: "${filePath}", containing file: "${containingFilePath}", requested module: "${requestedModuleName}"`);
7447
+ }
7448
+ return followedPath;
7449
+ }
7450
+ findMatchingJsFile(filePath) {
7451
+ let requiresReplacements = false;
7452
+ for (const [currentEnding] of JsFileLookupMapping) {
7453
+ if (filePath.endsWith(currentEnding)) {
7454
+ requiresReplacements = true;
7455
+ break;
7456
+ }
7457
+ }
7458
+ // avoid unnecessary calls to fileExists if we don't need to replace anything
7459
+ if (!requiresReplacements) {
7460
+ if (this.host.fileExists(filePath)) {
7461
+ return {
7462
+ match: filePath
7463
+ };
7464
+ }
7465
+ }
7466
+ const tried = [];
7467
+ for (const [currentEnding, replacements] of JsFileLookupMapping) {
7468
+ if (filePath.endsWith(currentEnding)) {
7469
+ for (const replacement of replacements) {
7470
+ const newFilePath = filePath.slice(0, -currentEnding.length) + replacement;
7471
+ if (this.host.fileExists(newFilePath)) {
7472
+ return {
7473
+ match: newFilePath
7474
+ };
7475
+ } else {
7476
+ tried.push(newFilePath);
7477
+ }
7478
+ }
7479
+ }
7480
+ }
7481
+ return {
7482
+ match: false,
7483
+ tried: tried.concat(filePath)
7484
+ };
7485
+ }
7486
+ }
7487
+
7451
7488
  /**
7452
7489
  * An implementation of methods in the various ts.*Host interfaces
7453
7490
  * that uses ValFS to resolve modules and read/write files.
@@ -7562,10 +7599,10 @@ async function checkRemoteRef(remoteHost, ref, projectRoot, schema, metadata) {
7562
7599
  error: `File path is missing in remote ref: ${ref}`
7563
7600
  };
7564
7601
  }
7565
- if (!relativeFilePath.startsWith("public/val/")) {
7602
+ if (!relativeFilePath.startsWith("public/")) {
7566
7603
  return {
7567
7604
  status: "error",
7568
- error: `File path must be within the public/val/ directory (e.g. public/val/path/to/file.txt). Got: ${relativeFilePath}`
7605
+ error: `File path must be within the public/ directory (e.g. public/path/to/file.txt). Got: ${relativeFilePath}`
7569
7606
  };
7570
7607
  }
7571
7608
  const coreVersion = core.Internal.VERSION.core || "unknown";
@@ -7718,6 +7755,11 @@ async function downloadFileFromRemote(ref, filePath) {
7718
7755
  });
7719
7756
  }
7720
7757
 
7758
+ // A remaining error may optionally carry a more specific `sourcePath` than the
7759
+ // one the fix was created from. This is used by gallery checks, where a single
7760
+ // record-level fix expands into per-entry errors that should point at the
7761
+ // individual entry (e.g. `?p="/public/val/logo.png"`) rather than the record.
7762
+
7721
7763
  // TODO: find a better name? transformFixesToPatch?
7722
7764
  async function createFixPatch(config, apply, sourcePath, validationError, remoteFiles, moduleSource, moduleSchema) {
7723
7765
  const remainingErrors = [];
@@ -7924,10 +7966,10 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
7924
7966
  });
7925
7967
  continue;
7926
7968
  }
7927
- if (!filePath.startsWith("public/val/")) {
7969
+ if (!filePath.startsWith("public/")) {
7928
7970
  remainingErrors.push({
7929
7971
  ...validationError,
7930
- message: "Unexpected error while downloading remote (invalid file path - must start with public/val/)",
7972
+ message: "Unexpected error while downloading remote (invalid file path - must start with public/)",
7931
7973
  fixes: undefined
7932
7974
  });
7933
7975
  continue;
@@ -8012,7 +8054,11 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
8012
8054
  } else {
8013
8055
  remainingErrors.push({
8014
8056
  ...validationError,
8015
- message: `Image metadata for '${entryKey}' is incorrect (width: ${stored.width ?? "<empty>"} vs ${actualMetadata.width}, height: ${stored.height ?? "<empty>"} vs ${actualMetadata.height}, mimeType: ${stored.mimeType ?? "<empty>"} vs ${actualMetadata.mimeType}). Use --fix to update.`
8057
+ message: `Image metadata for '${entryKey}' is incorrect (width: ${stored.width ?? "<empty>"} vs ${actualMetadata.width}, height: ${stored.height ?? "<empty>"} vs ${actualMetadata.height}, mimeType: ${stored.mimeType ?? "<empty>"} vs ${actualMetadata.mimeType}). Use --fix to update.`,
8058
+ sourcePath: core.Internal.createValPathOfItem(sourcePath, entryKey),
8059
+ // Gallery entries are keyed by their file path; surface the
8060
+ // error on the key rather than the derived metadata value.
8061
+ keyError: true
8016
8062
  });
8017
8063
  }
8018
8064
  }
@@ -8033,7 +8079,11 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
8033
8079
  } else {
8034
8080
  remainingErrors.push({
8035
8081
  ...validationError,
8036
- message: `File metadata for '${entryKey}' has incorrect mimeType: '${stored.mimeType ?? "<empty>"}' vs '${actualMetadata.mimeType}'. Use --fix to update.`
8082
+ message: `File metadata for '${entryKey}' has incorrect mimeType: '${stored.mimeType ?? "<empty>"}' vs '${actualMetadata.mimeType}'. Use --fix to update.`,
8083
+ sourcePath: core.Internal.createValPathOfItem(sourcePath, entryKey),
8084
+ // Gallery entries are keyed by their file path; surface the
8085
+ // error on the key rather than the derived metadata value.
8086
+ keyError: true
8037
8087
  });
8038
8088
  }
8039
8089
  }
@@ -8191,22 +8241,289 @@ async function getFileMetadata(projectRoot, validationError) {
8191
8241
  return extractFileMetadata(fileRef);
8192
8242
  }
8193
8243
 
8244
+ function getModulePathRange(modulePath, modulePathMap,
8245
+ // Which part of an object/record member to point at. For an object property
8246
+ // the resolved node's own range is the *key* (property name); the *value*
8247
+ // range is stored under `children.val`. Array elements, leaf literals and
8248
+ // `c.image`/`c.file` `_ref`/`metadata` nodes have no `val` child, so "value"
8249
+ // falls back to the node's own range for those. Defaults to "key" to preserve
8250
+ // existing callers.
8251
+ target = "key") {
8252
+ var _range$children;
8253
+ // Handle empty or invalid module paths gracefully
8254
+ if (!modulePath || typeof modulePath !== "string") {
8255
+ return undefined;
8256
+ }
8257
+ let segments;
8258
+ try {
8259
+ // Quote-aware splitter that correctly handles keys containing dots
8260
+ // (e.g. file refs like `"/public/val/images/logo.png"`), unlike a naive
8261
+ // split on ".". Throws on malformed input (e.g. unbalanced quotes).
8262
+ segments = core.Internal.splitModulePath(modulePath);
8263
+ } catch {
8264
+ // Return undefined if the module path is malformed. This can happen when
8265
+ // there are upstream errors in schema serialization.
8266
+ return undefined;
8267
+ }
8268
+ if (segments.length === 0) {
8269
+ return undefined;
8270
+ }
8271
+ let range = modulePathMap[segments[0]];
8272
+ for (const pathSegment of segments.slice(1)) {
8273
+ var _range;
8274
+ if (!range) {
8275
+ break;
8276
+ }
8277
+ range = (_range = range) === null || _range === void 0 || (_range = _range.children) === null || _range === void 0 ? void 0 : _range[pathSegment];
8278
+ }
8279
+ if (!range) {
8280
+ return undefined;
8281
+ }
8282
+ const valueRange = target === "value" ? (_range$children = range.children) === null || _range$children === void 0 ? void 0 : _range$children.val : undefined;
8283
+ const resolved = valueRange ?? range;
8284
+ return resolved.start && resolved.end && {
8285
+ start: resolved.start,
8286
+ end: resolved.end
8287
+ };
8288
+ }
8289
+
8290
+ /**
8291
+ * The line/character range of `node`'s own text (leading trivia excluded).
8292
+ *
8293
+ * NOTE: do not compute the start as `end.character - node.getWidth()`. That
8294
+ * identity only holds while the node stays on a single line - for a multi-line
8295
+ * node (an object inside an array, a `c.image` metadata argument, ...) it
8296
+ * reports the *closing* line and a negative character.
8297
+ */
8298
+ function rangeOf(node, sourceFile) {
8299
+ return {
8300
+ start: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)),
8301
+ end: sourceFile.getLineAndCharacterOfPosition(node.getEnd())
8302
+ };
8303
+ }
8304
+ function createModulePathMap(sourceFile) {
8305
+ for (const child of sourceFile.getChildren().flatMap(child => child.getChildren())) {
8306
+ if (ts__default["default"].isExportAssignment(child)) {
8307
+ const contentNode = child.expression && ts__default["default"].isCallExpression(child.expression) && child.expression.arguments[2];
8308
+ if (contentNode) {
8309
+ return traverse(contentNode, sourceFile);
8310
+ }
8311
+ }
8312
+ }
8313
+ }
8314
+ function traverse(node, sourceFile) {
8315
+ if (ts__default["default"].isStringLiteral(node) || ts__default["default"].isNumericLiteral(node)) {
8316
+ return {
8317
+ "": {
8318
+ children: {},
8319
+ ...rangeOf(node, sourceFile)
8320
+ }
8321
+ };
8322
+ }
8323
+ if (ts__default["default"].isObjectLiteralExpression(node)) {
8324
+ return traverseObjectLiteral(node, sourceFile);
8325
+ }
8326
+ if (ts__default["default"].isArrayLiteralExpression(node)) {
8327
+ return traverseArrayLiteral(node, sourceFile);
8328
+ }
8329
+ if (ts__default["default"].isCallExpression(node)) {
8330
+ return traverseCallExpression(node, sourceFile);
8331
+ }
8332
+ }
8333
+ function traverseCallExpression(node, sourceFile) {
8334
+ if (ts__default["default"].isPropertyAccessExpression(node.expression)) {
8335
+ if (node.expression.expression.getText(sourceFile) === "c" && (node.expression.name.getText(sourceFile) === "file" || node.expression.name.getText(sourceFile) === "image")) {
8336
+ const val = {
8337
+ children: {},
8338
+ ...rangeOf(node, sourceFile)
8339
+ };
8340
+ if (node.arguments[0]) {
8341
+ const _ref = {
8342
+ children: {},
8343
+ ...rangeOf(node.arguments[0], sourceFile)
8344
+ };
8345
+ if (!node.arguments[1]) {
8346
+ return {
8347
+ val,
8348
+ _ref
8349
+ };
8350
+ }
8351
+ return {
8352
+ val,
8353
+ _ref,
8354
+ metadata: {
8355
+ children: {},
8356
+ ...rangeOf(node.arguments[1], sourceFile)
8357
+ }
8358
+ };
8359
+ }
8360
+ }
8361
+ }
8362
+ }
8363
+ function traverseArrayLiteral(node, sourceFile) {
8364
+ return node.elements.reduce((acc, element, index) => {
8365
+ if (ts__default["default"].isExpression(element)) {
8366
+ return {
8367
+ ...acc,
8368
+ [index]: {
8369
+ children: traverse(element, sourceFile),
8370
+ ...rangeOf(element, sourceFile)
8371
+ }
8372
+ };
8373
+ }
8374
+ return acc;
8375
+ }, {});
8376
+ }
8377
+ function traverseObjectLiteral(node, sourceFile) {
8378
+ return node.properties.reduce((acc, property) => {
8379
+ if (ts__default["default"].isPropertyAssignment(property)) {
8380
+ const key = property.name && (ts__default["default"].isIdentifier(property.name) || ts__default["default"].isStringLiteral(property.name)) && property.name.text;
8381
+ const value = property.initializer;
8382
+ if (key) {
8383
+ const val = {
8384
+ children: {},
8385
+ ...rangeOf(property.initializer, sourceFile)
8386
+ };
8387
+ return {
8388
+ ...acc,
8389
+ [key]: {
8390
+ children: {
8391
+ val,
8392
+ ...traverse(value, sourceFile)
8393
+ },
8394
+ ...rangeOf(property.name, sourceFile)
8395
+ }
8396
+ };
8397
+ }
8398
+ }
8399
+ return acc;
8400
+ }, {});
8401
+ }
8402
+
8403
+ /**
8404
+ * Replays a `val debug` snapshot: applies its patches the way /save does and
8405
+ * validates the result.
8406
+ *
8407
+ * A snapshot is a minimal Val project (the modules the patches touch plus the
8408
+ * ones they reference, a generated val.modules.ts, and the patch chain under
8409
+ * .val/patches), so replaying it is just a ValOpsFS pointed at the directory -
8410
+ * no snapshot-specific code paths, which is the point: if the replay reproduces
8411
+ * the bug, the bug is in the ordinary code.
8412
+ */
8413
+
8414
+ async function replaySnapshot(snapshotDir) {
8415
+ const root = path__namespace["default"].resolve(snapshotDir);
8416
+ if (!fs__default["default"].existsSync(path__namespace["default"].join(root, "val.modules.ts"))) {
8417
+ throw new Error(`Not a Val debug snapshot: no val.modules.ts in ${root}. ` + `Unzip the snapshot first.`);
8418
+ }
8419
+ const valModules = loadValModules(root);
8420
+ const serverOps = new ValOpsFS(process.env.VAL_CONTENT_URL || core.DEFAULT_CONTENT_HOST, root, valModules,
8421
+ // The snapshot's own val.config, as evaluated by loadValModules - no need to
8422
+ // re-read it, and this way the replay uses exactly the config the snapshot
8423
+ // carries (files.directory in particular).
8424
+ {
8425
+ config: valModules.config
8426
+ });
8427
+ const patchesRes = await serverOps.fetchPatches({
8428
+ patchIds: undefined,
8429
+ excludePatchOps: false
8430
+ });
8431
+ if (patchesRes.error) {
8432
+ throw new Error(`Could not read the snapshot's patches: ${patchesRes.error.message}`);
8433
+ }
8434
+ if (patchesRes.errors && patchesRes.errors.length > 0) {
8435
+ for (const err of patchesRes.errors) {
8436
+ console.error(`Snapshot patch could not be read: ${err.message}`);
8437
+ }
8438
+ }
8439
+ const analysis = {
8440
+ ...serverOps.analyzePatches(patchesRes.patches),
8441
+ ...patchesRes
8442
+ };
8443
+ const prepared = await serverOps.prepare(analysis, {
8444
+ continueOnError: true
8445
+ });
8446
+ const sources = await serverOps.getSourcesWithPatchesApplied(analysis);
8447
+ const schemas = await serverOps.getSchemas();
8448
+ const validation = await serverOps.validateSources(schemas, sources.sources, analysis.patchesByModule);
8449
+ const patchedSourceFiles = {};
8450
+ for (const [moduleFilePath, contents] of Object.entries(prepared.patchedSourceFiles)) {
8451
+ if (contents !== null) {
8452
+ patchedSourceFiles[moduleFilePath] = contents;
8453
+ }
8454
+ }
8455
+ Object.assign(patchedSourceFiles, prepared.partiallyPatchedSourceFiles);
8456
+ return {
8457
+ patches: patchesRes.patches.map(patch => {
8458
+ var _prepared$unappliable;
8459
+ return {
8460
+ patchId: patch.patchId,
8461
+ moduleFilePath: patch.path,
8462
+ createdAt: patch.createdAt,
8463
+ authorId: patch.authorId,
8464
+ error: (_prepared$unappliable = prepared.unappliablePatches[patch.patchId]) === null || _prepared$unappliable === void 0 ? void 0 : _prepared$unappliable.message
8465
+ };
8466
+ }),
8467
+ unappliablePatches: prepared.unappliablePatches,
8468
+ sourceFilePatchErrors: Object.fromEntries(Object.entries(prepared.sourceFilePatchErrors).map(([key, errors]) => [key, errors.map(formatPatchSourceError)])),
8469
+ binaryFilePatchErrors: prepared.binaryFilePatchErrors,
8470
+ validationErrors: validation.errors,
8471
+ patchedSourceFiles,
8472
+ hasErrors: prepared.hasErrors
8473
+ };
8474
+ }
8475
+
8476
+ /**
8477
+ * Compares a replay against the report captured when the snapshot was taken, so
8478
+ * "reproduced the customer's bug" is distinguishable from "behaves differently
8479
+ * on this version".
8480
+ */
8481
+ function compareWithCapturedReport(result, capturedReport) {
8482
+ const capturedIds = Object.keys(capturedReport.unappliablePatches ?? {});
8483
+ const nowIds = Object.keys(result.unappliablePatches);
8484
+ const stillFailing = capturedIds.filter(patchId => nowIds.includes(patchId));
8485
+ const nowApplying = capturedIds.filter(patchId => !nowIds.includes(patchId));
8486
+ const newlyFailing = nowIds.filter(patchId => !capturedIds.includes(patchId));
8487
+ return {
8488
+ stillFailing,
8489
+ nowApplying,
8490
+ newlyFailing,
8491
+ reproduced: capturedIds.length > 0 && nowApplying.length === 0 && newlyFailing.length === 0
8492
+ };
8493
+ }
8494
+ function readCapturedReport(snapshotDir) {
8495
+ const reportPath = path__namespace["default"].join(path__namespace["default"].resolve(snapshotDir), "report.json");
8496
+ if (!fs__default["default"].existsSync(reportPath)) {
8497
+ return null;
8498
+ }
8499
+ return JSON.parse(fs__default["default"].readFileSync(reportPath, "utf-8"));
8500
+ }
8501
+
8194
8502
  exports.Service = Service;
8195
8503
  exports.ValFSHost = ValFSHost;
8196
8504
  exports.ValModuleLoader = ValModuleLoader;
8505
+ exports.ValOpsFS = ValOpsFS;
8506
+ exports.ValOpsHttp = ValOpsHttp;
8197
8507
  exports.ValSourceFileHandler = ValSourceFileHandler;
8508
+ exports.compareWithCapturedReport = compareWithCapturedReport;
8198
8509
  exports.createFixPatch = createFixPatch;
8510
+ exports.createModulePathMap = createModulePathMap;
8199
8511
  exports.createService = createService;
8200
8512
  exports.createValApiRouter = createValApiRouter;
8201
8513
  exports.createValServer = createValServer;
8202
8514
  exports.decodeJwt = decodeJwt;
8203
8515
  exports.encodeJwt = encodeJwt;
8516
+ exports.formatPatchSourceError = formatPatchSourceError;
8204
8517
  exports.formatSyntaxErrorTree = formatSyntaxErrorTree;
8205
8518
  exports.getCompilerOptions = getCompilerOptions;
8206
8519
  exports.getExpire = getExpire;
8520
+ exports.getModulePathRange = getModulePathRange;
8207
8521
  exports.getPersonalAccessTokenPath = getPersonalAccessTokenPath;
8208
8522
  exports.getSettings = getSettings;
8523
+ exports.loadValModules = loadValModules;
8209
8524
  exports.parsePersonalAccessTokenFile = parsePersonalAccessTokenFile;
8210
8525
  exports.patchSourceFile = patchSourceFile;
8526
+ exports.readCapturedReport = readCapturedReport;
8527
+ exports.replaySnapshot = replaySnapshot;
8211
8528
  exports.safeReadGit = safeReadGit;
8212
8529
  exports.uploadRemoteFile = uploadRemoteFile;