@valbuild/server 0.97.3 → 0.97.5

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.
@@ -1,19 +1,19 @@
1
- import { newQuickJSWASMModule } from 'quickjs-emscripten';
2
1
  import ts from 'typescript';
3
2
  import { result, pipe } from '@valbuild/core/fp';
4
- import { FILE_REF_PROP, FILE_REF_SUBTYPE_TAG, VAL_EXTENSION, derefPatch, Internal, Schema, extractValModules, ImageSchema, DEFAULT_CONTENT_HOST } from '@valbuild/core';
3
+ import { FILE_REF_PROP, FILE_REF_SUBTYPE_TAG, VAL_EXTENSION, derefPatch, extractValModules, Internal, ImageSchema, DEFAULT_CONTENT_HOST } from '@valbuild/core';
5
4
  import { deepEqual, isNotRoot, PatchError, parseAndValidateArrayIndex, applyPatch, JSONOps, deepClone, sourceToPatchPath } from '@valbuild/core/patch';
6
5
  import * as path from 'path';
7
6
  import path__default from 'path';
8
7
  import fs, { promises } from 'fs';
9
- import { transform } from 'sucrase';
10
- import { VAL_CSS_PATH, VAL_APP_ID, VAL_OVERLAY_ID } from '@valbuild/ui';
8
+ import vm from 'node:vm';
9
+ import { Module } from 'node:module';
11
10
  import { resolveSchemaSourceFixForError, getErrorMessageFromUnknownJson, Patch, ParentRef, VAL_ENABLE_COOKIE_NAME, VAL_STATE_COOKIE, VAL_SESSION_COOKIE, Api } from '@valbuild/shared/internal';
12
11
  import { createUIRequestHandler } from '@valbuild/ui/server';
13
12
  import crypto$1 from 'crypto';
14
13
  import { z } from 'zod';
15
14
  import sizeOf from 'image-size';
16
15
  import { fromError } from 'zod-validation-error';
16
+ import { transform } from 'sucrase';
17
17
  import http from 'http';
18
18
  import https from 'https';
19
19
 
@@ -798,7 +798,7 @@ const ops = new TSOps(document => {
798
798
  });
799
799
 
800
800
  // TODO: rename to patchValFiles since we may write multiple files
801
- const patchValFile = async (id, rootDir, patch, sourceFileHandler, runtime) => {
801
+ const patchValFile = async (id, rootDir, patch, sourceFileHandler) => {
802
802
  // const timeId = randomUUID();
803
803
  // console.time("patchValFile" + timeId);
804
804
  const filePath = sourceFileHandler.resolveSourceModulePath(getSyntheticContainingPath(rootDir), `.${id.replace(".val.ts", ".val").replace(".val.js", ".val").replace(".val.jsx", ".val").replace(".val.tsx", ".val")}`);
@@ -849,143 +849,6 @@ const patchSourceFile = (sourceFile, patch) => {
849
849
  return applyPatch(sourceFile, ops, patch);
850
850
  };
851
851
 
852
- const readValFile = async (moduleFilePath, rootDirPath, runtime, options) => {
853
- const context = runtime.newContext();
854
-
855
- // avoid failures when console.log is called
856
- const logHandle = context.newFunction("log", () => {
857
- // do nothing
858
- });
859
- const consoleHandle = context.newObject();
860
- context.setProp(consoleHandle, "log", logHandle);
861
- context.setProp(context.global, "console", consoleHandle);
862
- consoleHandle.dispose();
863
- logHandle.dispose();
864
-
865
- // avoid failures when process.env is called
866
- const envHandle = context.newObject();
867
- const processHandle = context.newObject();
868
- context.setProp(processHandle, "env", envHandle);
869
- context.setProp(context.global, "process", processHandle);
870
- const optionsHandle = context.newObject();
871
- if (options) {
872
- if (options.validate !== undefined) {
873
- context.setProp(optionsHandle, "validate", context.newNumber(+options.validate));
874
- }
875
- if (options.source !== undefined) {
876
- context.setProp(optionsHandle, "source", context.newNumber(+options.source));
877
- }
878
- if (options.schema !== undefined) {
879
- context.setProp(optionsHandle, "schema", context.newNumber(+options.schema));
880
- }
881
- }
882
- context.setProp(context.global, "__VAL_OPTIONS__", optionsHandle);
883
- envHandle.dispose();
884
- processHandle.dispose();
885
- optionsHandle.dispose();
886
- try {
887
- const modulePath = `.${moduleFilePath.replace(".val.js", ".val").replace(".val.ts", ".val").replace(".val.tsx", ".val").replace(".val.jsx", ".val")}`;
888
- const code = `import * as valModule from ${JSON.stringify(modulePath)};
889
- import { Internal } from "@valbuild/core";
890
-
891
- globalThis.valModule = {
892
- path: valModule?.default && Internal.getValPath(valModule?.default),
893
- schema: !!globalThis['__VAL_OPTIONS__'].schema ? valModule?.default && Internal.getSchema(valModule?.default)?.["executeSerialize"]() : undefined,
894
- source: !!globalThis['__VAL_OPTIONS__'].source ? valModule?.default && Internal.getSource(valModule?.default) : undefined,
895
- validation: !!globalThis['__VAL_OPTIONS__'].validate ? valModule?.default && (Internal.validate ? Internal.validate(valModule.default, Internal.getValPath(valModule?.default) || "/",
896
- Internal.getSource(valModule?.default)) : Internal.getSchema(valModule?.default)?.validate(
897
- Internal.getValPath(valModule?.default) || "/",
898
- Internal.getSource(valModule?.default)
899
- )) : undefined,
900
- defaultExport: !!valModule?.default,
901
- };
902
- `;
903
- const result = context.evalCode(code, getSyntheticContainingPath(rootDirPath));
904
- const fatalErrors = [];
905
- if (result.error) {
906
- const error = result.error.consume(context.dump);
907
- console.error(`Fatal error reading val file: ${moduleFilePath}. Error: ${error.message}\n`, error.stack);
908
- return {
909
- path: moduleFilePath,
910
- errors: {
911
- invalidModulePath: moduleFilePath,
912
- fatal: [{
913
- message: `${error.name || "Unknown error"}: ${error.message || "<no message>"}`,
914
- stack: error.stack
915
- }]
916
- }
917
- };
918
- } else {
919
- result.value.dispose();
920
- const valModule = context.getProp(context.global, "valModule").consume(context.dump);
921
- if (
922
- // if one of these are set it is a Val module, so must validate
923
- (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) {
924
- if (valModule.path !== moduleFilePath) {
925
- fatalErrors.push(`Wrong c.define path! Expected: '${moduleFilePath}', found: '${valModule.path}'`);
926
- } else if ((valModule === null || valModule === void 0 ? void 0 : valModule.schema) === undefined && options.schema) {
927
- fatalErrors.push(`Expected val path: '${moduleFilePath}' to have a schema`);
928
- } else if ((valModule === null || valModule === void 0 ? void 0 : valModule.source) === undefined && options.source) {
929
- fatalErrors.push(`Expected val path: '${moduleFilePath}' to have a source`);
930
- }
931
- }
932
- let errors = false;
933
- if (fatalErrors.length > 0) {
934
- errors = {
935
- invalidModulePath: valModule.path !== moduleFilePath ? moduleFilePath : undefined,
936
- fatal: fatalErrors.map(message => ({
937
- message
938
- }))
939
- };
940
- }
941
- if (valModule !== null && valModule !== void 0 && valModule.validation) {
942
- errors = {
943
- ...(errors ? errors : {}),
944
- validation: valModule.validation
945
- };
946
- }
947
- return {
948
- path: valModule.path || moduleFilePath,
949
- // NOTE: we use path here, since SerializedModuleContent (maybe bad name?) can be used for whole modules as well as subparts of modules
950
- source: valModule.source,
951
- schema: valModule.schema,
952
- errors
953
- };
954
- }
955
- } finally {
956
- context.dispose();
957
- }
958
- };
959
-
960
- const getCompilerOptions = (rootDir, parseConfigHost) => {
961
- const tsConfigPath = path__default.resolve(rootDir, "tsconfig.json");
962
- const jsConfigPath = path__default.resolve(rootDir, "jsconfig.json");
963
- let configFilePath;
964
- if (parseConfigHost.fileExists(jsConfigPath)) {
965
- configFilePath = jsConfigPath;
966
- } else if (parseConfigHost.fileExists(tsConfigPath)) {
967
- configFilePath = tsConfigPath;
968
- } else {
969
- throw Error(`Could not read config from: "${tsConfigPath}" nor "${jsConfigPath}". Root dir: "${rootDir}"`);
970
- }
971
- const {
972
- config,
973
- error
974
- } = ts.readConfigFile(configFilePath, parseConfigHost.readFile.bind(parseConfigHost));
975
- if (error) {
976
- if (typeof error.messageText === "string") {
977
- throw Error(`Could not parse config file: ${configFilePath}. Error: ${error.messageText}`);
978
- }
979
- throw Error(`Could not parse config file: ${configFilePath}. Error: ${error.messageText.messageText}`);
980
- }
981
- const optionsOverrides = undefined;
982
- const parsedConfigFile = ts.parseJsonConfigFileContent(config, parseConfigHost, rootDir, optionsOverrides, configFilePath);
983
- if (parsedConfigFile.errors.length > 0) {
984
- throw Error(`Could not parse config file: ${configFilePath}. Errors: ${parsedConfigFile.errors.map(e => e.messageText).join("\n")}`);
985
- }
986
- return parsedConfigFile.options;
987
- };
988
-
989
852
  class ValSourceFileHandler {
990
853
  constructor(projectRoot, compilerOptions, host = {
991
854
  ...ts.sys,
@@ -1033,326 +896,204 @@ class ValSourceFileHandler {
1033
896
  }
1034
897
  }
1035
898
 
1036
- const JsFileLookupMapping = [
1037
- // NOTE: first one matching will be used
1038
- [".cjs.d.ts", [".esm.js", ".mjs.js"]], [".cjs.js", [".esm.js", ".mjs.js"]], [".cjs", [".mjs"]], [".d.ts", [".js", ".esm.js", ".mjs.js"]]];
1039
- const MAX_CACHE_SIZE = 100 * 1024 * 1024; // 100 mb
1040
- 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
1041
-
1042
- class ValModuleLoader {
1043
- constructor(projectRoot, compilerOptions,
1044
- // TODO: remove this?
1045
- sourceFileHandler, host = {
1046
- ...ts.sys,
1047
- writeFile: (fileName, data, encoding) => {
1048
- fs.mkdirSync(path__default.dirname(fileName), {
1049
- recursive: true
1050
- });
1051
- fs.writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
1052
- },
1053
- rmFile: fs.rmSync,
1054
- readBuffer: fileName => {
1055
- try {
1056
- return fs.readFileSync(fileName);
1057
- } catch {
1058
- return undefined;
1059
- }
1060
- }
1061
- }, disableCache = false) {
1062
- this.projectRoot = projectRoot;
1063
- this.compilerOptions = compilerOptions;
1064
- this.sourceFileHandler = sourceFileHandler;
1065
- this.host = host;
1066
- this.disableCache = disableCache;
1067
- this.cache = {};
1068
- this.cacheSize = 0;
899
+ const getCompilerOptions = (rootDir, parseConfigHost) => {
900
+ const tsConfigPath = path__default.resolve(rootDir, "tsconfig.json");
901
+ const jsConfigPath = path__default.resolve(rootDir, "jsconfig.json");
902
+ let configFilePath;
903
+ if (parseConfigHost.fileExists(jsConfigPath)) {
904
+ configFilePath = jsConfigPath;
905
+ } else if (parseConfigHost.fileExists(tsConfigPath)) {
906
+ configFilePath = tsConfigPath;
907
+ } else {
908
+ throw Error(`Could not read config from: "${tsConfigPath}" nor "${jsConfigPath}". Root dir: "${rootDir}"`);
1069
909
  }
1070
- getModule(modulePath) {
1071
- if (!modulePath) {
1072
- throw Error(`Illegal module path: "${modulePath}"`);
1073
- }
1074
- const code = this.host.readFile(modulePath);
1075
- if (!code) {
1076
- throw Error(`Could not read file "${modulePath}"`);
1077
- }
1078
- let compiledCode;
1079
- if (this.cache[code] && !this.disableCache) {
1080
- // TODO: use hash instead of code as key
1081
- compiledCode = this.cache[code];
1082
- } else {
1083
- compiledCode = transform(code, {
1084
- filePath: modulePath,
1085
- disableESTransforms: true,
1086
- transforms: ["typescript"]
1087
- }).code;
1088
- if (!this.disableCache) {
1089
- if (this.cacheSize > MAX_CACHE_SIZE) {
1090
- console.warn("Cache size exceeded, clearing cache");
1091
- this.cache = {};
1092
- this.cacheSize = 0;
1093
- }
1094
- if (code.length < MAX_OBJECT_KEY_SIZE) {
1095
- this.cache[code] = compiledCode;
1096
- this.cacheSize += code.length + compiledCode.length; // code is mostly ASCII so 1 byte per char
1097
- }
1098
- }
910
+ const {
911
+ config,
912
+ error
913
+ } = ts.readConfigFile(configFilePath, parseConfigHost.readFile.bind(parseConfigHost));
914
+ if (error) {
915
+ if (typeof error.messageText === "string") {
916
+ throw Error(`Could not parse config file: ${configFilePath}. Error: ${error.messageText}`);
1099
917
  }
1100
- return compiledCode;
918
+ throw Error(`Could not parse config file: ${configFilePath}. Error: ${error.messageText.messageText}`);
1101
919
  }
1102
- resolveModulePath(containingFilePath, requestedModuleName) {
1103
- var _this$host$realpath, _this$host;
1104
- let sourceFileName = this.sourceFileHandler.resolveSourceModulePath(containingFilePath, requestedModuleName);
1105
- if (requestedModuleName === "@vercel/stega") {
1106
- sourceFileName = this.sourceFileHandler.resolveSourceModulePath(containingFilePath, "@vercel/stega").replace("stega/dist", "stega/dist/esm");
1107
- }
1108
- const matches = this.findMatchingJsFile(sourceFileName);
1109
- if (matches.match === false) {
1110
- let debugInfo = "";
1111
- if (sourceFileName.includes("val.config")) {
1112
- debugInfo = `\n@valbuild directory scan:\n${this.host.readDirectory("/", ["js", "ts", "json"], [], ["**/@valbuild/*"]).join("\n")}`;
1113
- }
1114
- throw Error(`Could not find matching js file for module "${requestedModuleName}" requested by: "${containingFilePath}". Tried:\n${matches.tried.join("\n")}${debugInfo}`);
1115
- }
1116
- const filePath = matches.match;
1117
- // resolve all symlinks (preconstruct for example symlinks the dist folder)
1118
- 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;
1119
- if (!followedPath) {
1120
- throw Error(`File path was empty: "${filePath}", containing file: "${containingFilePath}", requested module: "${requestedModuleName}"`);
1121
- }
1122
- return followedPath;
920
+ const optionsOverrides = undefined;
921
+ const parsedConfigFile = ts.parseJsonConfigFileContent(config, parseConfigHost, rootDir, optionsOverrides, configFilePath);
922
+ if (parsedConfigFile.errors.length > 0) {
923
+ throw Error(`Could not parse config file: ${configFilePath}. Errors: ${parsedConfigFile.errors.map(e => e.messageText).join("\n")}`);
1123
924
  }
1124
- findMatchingJsFile(filePath) {
1125
- let requiresReplacements = false;
1126
- for (const [currentEnding] of JsFileLookupMapping) {
1127
- if (filePath.endsWith(currentEnding)) {
1128
- requiresReplacements = true;
1129
- break;
1130
- }
1131
- }
1132
- // avoid unnecessary calls to fileExists if we don't need to replace anything
1133
- if (!requiresReplacements) {
1134
- if (this.host.fileExists(filePath)) {
1135
- return {
1136
- match: filePath
1137
- };
1138
- }
1139
- }
1140
- const tried = [];
1141
- for (const [currentEnding, replacements] of JsFileLookupMapping) {
1142
- if (filePath.endsWith(currentEnding)) {
1143
- for (const replacement of replacements) {
1144
- const newFilePath = filePath.slice(0, -currentEnding.length) + replacement;
1145
- if (this.host.fileExists(newFilePath)) {
1146
- return {
1147
- match: newFilePath
1148
- };
1149
- } else {
1150
- tried.push(newFilePath);
1151
- }
1152
- }
1153
- }
925
+ return parsedConfigFile.options;
926
+ };
927
+
928
+ /**
929
+ * Loads the project's root `val.modules.ts` (or `.js`) using Node's `vm`
930
+ * module and returns its default export (a `ValModules` registry).
931
+ *
932
+ * This is a recursive CommonJS loader: the root modules file and every
933
+ * relative `*.val.ts` / `val.config.ts` it (dynamically) imports are
934
+ * transpiled to CommonJS and evaluated in a `vm` sandbox. Bare specifiers
935
+ * (e.g. `@valbuild/core`) are resolved with the real Node `require` so the
936
+ * user modules share the exact same `@valbuild/core` instance that
937
+ * `extractValModules` uses.
938
+ *
939
+ * Mirrors the pattern already used by the CLI's `evalValConfigFile`.
940
+ *
941
+ * SECURITY: The `vm` context is NOT a security sandbox. It deliberately exposes
942
+ * `process` and a `require` that falls back to the real Node resolver (so user
943
+ * modules share the same `@valbuild/core` instance). This loader must therefore
944
+ * only ever be used to evaluate the project's own first-party, trusted files
945
+ * (`val.modules` and the local `*.val.ts`/`val.config.ts` it imports) i.e. the
946
+ * same trust level as running the project's build. It must never be used to
947
+ * evaluate untrusted or third-party modules.
948
+ */
949
+ function loadValModules(projectRoot) {
950
+ const valModulesPath = findValModulesPath(projectRoot);
951
+ if (!valModulesPath) {
952
+ throw Error(`Could not find 'val.modules.ts' nor 'val.modules.js' in project root: '${projectRoot}'`);
953
+ }
954
+ const compilerOptions = getCompilerOptions(projectRoot, ts.sys);
955
+ const cache = {};
956
+ const loaded = loadModule(valModulesPath, cache, compilerOptions);
957
+ const valModules = loaded.exports.default;
958
+ if (!valModules) {
959
+ throw Error(`Val modules file at path: '${valModulesPath}' must have a default export. Got: ${valModules}`);
960
+ }
961
+ return valModules;
962
+ }
963
+ function findValModulesPath(projectRoot) {
964
+ for (const fileName of ["val.modules.ts", "val.modules.js"]) {
965
+ const candidate = path__default.join(projectRoot, fileName);
966
+ if (fs.existsSync(candidate)) {
967
+ return candidate;
1154
968
  }
1155
- return {
1156
- match: false,
1157
- tried: tried.concat(filePath)
1158
- };
1159
969
  }
970
+ return null;
1160
971
  }
972
+ const RESOLVE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".cjs", ".mjs"];
1161
973
 
1162
- async function newValQuickJSRuntime(quickJSModule, moduleLoader, {
1163
- maxStackSize = 1024 * 20,
1164
- // maximum stack size that works: 1024 * 640 * 8
1165
- memoryLimit = 1024 * 640 // 640 mbs
1166
- } = {}) {
1167
- const runtime = quickJSModule.newRuntime();
1168
- runtime.setMaxStackSize(maxStackSize);
1169
- runtime.setMemoryLimit(memoryLimit);
1170
- runtime.setModuleLoader(modulePath => {
1171
- try {
1172
- // Special cases to avoid loading the React packages since currently React does not have a ESM build:
1173
- // TODO: this is not stable, find a better way to do this
1174
- if (modulePath === "@valbuild/react") {
1175
- return {
1176
- 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`)};"
1177
- };
1178
- }
1179
- if (modulePath === "@valbuild/react/internal") {
1180
- return {
1181
- value: `
1182
- const useVal = () => { throw Error('Cannot use \\'useVal\\' in this type of file') };
1183
- export function ValProvider() { throw Error('Cannot use \\'ValProvider\\' in this type of file') };
1184
- export function ValRichText() { throw Error('Cannot use \\'ValRichText\\' in this type of file')};`
1185
- };
1186
- }
1187
- if (modulePath === "@valbuild/ui") {
1188
- return {
1189
- value: `
1190
- export const ValOverlay = () => {
1191
- throw Error("Cannot use 'ValOverlay' in this type of file")
1192
- };
1193
- export const VAL_CSS_PATH = "${VAL_CSS_PATH}";
1194
- export const VAL_APP_PATH = "${VAL_CSS_PATH}";
1195
- export const VAL_APP_ID = "${VAL_APP_ID}";
1196
- export const VAL_OVERLAY_ID = "${VAL_OVERLAY_ID}";
1197
- export const IS_DEV = false;
1198
- export const VERSION = "0.0.0";
1199
- `
1200
- };
1201
- }
1202
- if (modulePath === "@valbuild/react/stega") {
1203
- return {
1204
- 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`) }; "
1205
- };
974
+ // Specifiers that user val files must not actually use. We stub them so that
975
+ // importing is fine, but using a value throws a clear error. Real @valbuild
976
+ // packages are resolved via the real require, so when they (legitimately)
977
+ // import react/next internally those go through Node, not this stub.
978
+ function isStubbedSpecifier(spec) {
979
+ return spec === "react" || spec.startsWith("react/") || spec === "next" || spec.startsWith("next/") || spec === "@valbuild/ui" || spec === "@valbuild/react" || spec.startsWith("@valbuild/react/");
980
+ }
981
+ function makeStub(spec) {
982
+ const throwing = prop => () => {
983
+ throw Error(`Cannot use '${prop}' from '${spec}' in this type of file`);
984
+ };
985
+ const handler = {
986
+ get(_target, prop) {
987
+ if (prop === "__esModule") {
988
+ return true;
1206
989
  }
1207
- if (modulePath.startsWith("next/navigation")) {
1208
- return {
1209
- 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`) } } } );"
1210
- };
990
+ if (typeof prop === "symbol") {
991
+ return undefined;
1211
992
  }
1212
- if (modulePath.startsWith("next")) {
1213
- return {
1214
- value: "export default new Proxy({}, { get() { return () => { throw new Error(`Cannot import 'next' in this file`) } } } );"
1215
- };
993
+ // React.createContext is sometimes called at module top-level; return a
994
+ // proxy-returning function so evaluation does not crash on import.
995
+ if (prop === "createContext") {
996
+ return () => new Proxy({}, handler);
1216
997
  }
1217
- if (modulePath.startsWith("react/jsx-runtime")) {
1218
- return {
1219
- 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`) };"
1220
- };
998
+ if (prop === "default") {
999
+ return stub;
1221
1000
  }
1222
- if (modulePath.startsWith("react")) {
1223
- return {
1224
- value: `
1225
- export const createContext = () => new Proxy({}, { get() { return () => { throw new Error('Cannot use \\'createContext\\' in this file') } } } );
1226
- export const useTransition = () => { throw Error('Cannot use \\'useTransition\\' in this type of file') };
1227
-
1228
- export default new Proxy({}, {
1229
- get(target, props) {
1230
- // React.createContext might be called on top-level
1231
- if (props === 'createContext') {
1232
- return createContext;
1233
- }
1234
- return () => {
1235
- throw new Error('Cannot import \\'react\\' in this file');
1001
+ return throwing(prop);
1236
1002
  }
1003
+ };
1004
+ const stub = new Proxy({}, handler);
1005
+ return stub;
1006
+ }
1007
+ function loadModule(absPath, cache, compilerOptions) {
1008
+ const cached = cache[absPath];
1009
+ if (cached) {
1010
+ return cached;
1011
+ }
1012
+ const code = fs.readFileSync(absPath, "utf-8");
1013
+ const transpiled = ts.transpileModule(code, {
1014
+ compilerOptions: {
1015
+ target: ts.ScriptTarget.ES2020,
1016
+ module: ts.ModuleKind.CommonJS,
1017
+ esModuleInterop: true,
1018
+ jsx: ts.JsxEmit.ReactJSX
1019
+ },
1020
+ fileName: absPath
1021
+ });
1022
+ const moduleObj = {
1023
+ exports: {}
1024
+ };
1025
+ // Insert into the cache before evaluating so cyclic imports resolve.
1026
+ cache[absPath] = moduleObj;
1027
+ const dirName = path__default.dirname(absPath);
1028
+ const realRequire = Module.createRequire(absPath);
1029
+ const customRequire = spec => {
1030
+ var _ts$resolveModuleName;
1031
+ if (isStubbedSpecifier(spec)) {
1032
+ return makeStub(spec);
1033
+ }
1034
+ if (spec.startsWith(".") || path__default.isAbsolute(spec)) {
1035
+ const resolved = resolveRelative(dirName, spec);
1036
+ if (!resolved) {
1037
+ throw Error(`Could not resolve module '${spec}' from '${absPath}'`);
1038
+ }
1039
+ return loadModule(resolved, cache, compilerOptions).exports;
1040
+ }
1041
+ // Non-relative specifier: it might be a tsconfig path alias (e.g. "_/val.config")
1042
+ // pointing at a local source file, or an actual node_modules package.
1043
+ const tsResolved = (_ts$resolveModuleName = ts.resolveModuleName(spec, absPath, compilerOptions, ts.sys).resolvedModule) === null || _ts$resolveModuleName === void 0 ? void 0 : _ts$resolveModuleName.resolvedFileName;
1044
+ if (tsResolved && !tsResolved.includes("/node_modules/") && !tsResolved.endsWith(".d.ts")) {
1045
+ return loadModule(tsResolved, cache, compilerOptions).exports;
1046
+ }
1047
+ // Real node_modules package – use the real require so user modules share
1048
+ // the same @valbuild/core instance as extractValModules.
1049
+ return realRequire(spec);
1050
+ };
1051
+
1052
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1053
+ const sandbox = {
1054
+ exports: moduleObj.exports,
1055
+ module: moduleObj,
1056
+ require: customRequire,
1057
+ __filename: absPath,
1058
+ __dirname: dirName,
1059
+ console,
1060
+ process
1061
+ };
1062
+ sandbox.global = sandbox;
1063
+ sandbox.globalThis = sandbox;
1064
+ const context = vm.createContext(sandbox);
1065
+ const script = new vm.Script(transpiled.outputText, {
1066
+ filename: absPath
1067
+ });
1068
+ script.runInContext(context);
1069
+ return moduleObj;
1070
+ }
1071
+ function resolveRelative(dirName, spec) {
1072
+ const base = path__default.resolve(dirName, spec);
1073
+ // Exact file (with extension)
1074
+ if (fs.existsSync(base) && fs.statSync(base).isFile()) {
1075
+ return base;
1237
1076
  }
1238
- })`
1239
- };
1240
- }
1241
- if (modulePath.includes("/ValNextProvider")) {
1242
- return {
1243
- value: "export const ValNextProvider = new Proxy({}, { get() { return () => { throw new Error(`Cannot import 'ValNextProvider' in this file`) } } } )"
1244
- };
1245
- }
1246
- if (modulePath.includes("/ValContext")) {
1247
- return {
1248
- 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`) } } } )"
1249
- };
1250
- }
1251
- if (modulePath.includes("/ValImage")) {
1252
- return {
1253
- value: "export const ValImage = new Proxy({}, { get() { return () => { throw new Error(`Cannot import 'ValImage' in this file`) } } } )"
1254
- };
1255
- }
1256
- if (modulePath.includes("/ValApp")) {
1257
- return {
1258
- value: "export const ValApp = new Proxy({}, { get() { return () => { throw new Error(`Cannot import 'ValApp' in this file`) } } } )"
1259
- };
1260
- }
1261
- if (modulePath.includes("/ValModulesClient")) {
1262
- return {
1263
- 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`) };"
1264
- };
1265
- }
1266
- return {
1267
- value: moduleLoader.getModule(modulePath)
1268
- };
1269
- } catch {
1270
- return {
1271
- error: Error(`Could not resolve module: '${modulePath}'`)
1272
- };
1077
+ // Probe extensions (handles `./x.val` -> `./x.val.ts`)
1078
+ for (const ext of RESOLVE_EXTENSIONS) {
1079
+ const candidate = base + ext;
1080
+ if (fs.existsSync(candidate)) {
1081
+ return candidate;
1273
1082
  }
1274
- }, (baseModuleName, requestedName) => {
1275
- try {
1276
- if (requestedName === "@valbuild/react") {
1277
- return {
1278
- value: requestedName
1279
- };
1280
- }
1281
- if (requestedName === "@valbuild/react/stega") {
1282
- return {
1283
- value: requestedName
1284
- };
1285
- }
1286
- if (requestedName === "@valbuild/react/internal") {
1287
- return {
1288
- value: requestedName
1289
- };
1290
- }
1291
- if (requestedName === "@valbuild/ui") {
1292
- return {
1293
- value: requestedName
1294
- };
1295
- }
1296
- if (requestedName.startsWith("next/navigation")) {
1297
- return {
1298
- value: requestedName
1299
- };
1300
- }
1301
- if (requestedName.startsWith("next")) {
1302
- return {
1303
- value: requestedName
1304
- };
1305
- }
1306
- if (requestedName.startsWith("react/jsx-runtime")) {
1307
- return {
1308
- value: requestedName
1309
- };
1310
- }
1311
- if (requestedName.startsWith("react")) {
1312
- return {
1313
- value: requestedName
1314
- };
1315
- }
1316
- if (requestedName.includes("/ValNextProvider")) {
1317
- return {
1318
- value: requestedName
1319
- };
1320
- }
1321
- if (requestedName.includes("/ValContext")) {
1322
- return {
1323
- value: requestedName
1324
- };
1325
- }
1326
- if (requestedName.includes("/ValImage")) {
1327
- return {
1328
- value: requestedName
1329
- };
1330
- }
1331
- if (requestedName.includes("/ValApp")) {
1332
- return {
1333
- value: requestedName
1334
- };
1335
- }
1336
- if (requestedName.includes("/ValModulesClient")) {
1337
- return {
1338
- value: requestedName
1339
- };
1083
+ }
1084
+ // Directory index
1085
+ if (fs.existsSync(base) && fs.statSync(base).isDirectory()) {
1086
+ for (const ext of RESOLVE_EXTENSIONS) {
1087
+ const candidate = path__default.join(base, "index" + ext);
1088
+ if (fs.existsSync(candidate)) {
1089
+ return candidate;
1340
1090
  }
1341
- const modulePath = moduleLoader.resolveModulePath(baseModuleName, requestedName);
1342
- return {
1343
- value: modulePath
1344
- };
1345
- } catch (e) {
1346
- console.debug(`Could not resolve ${requestedName} in ${baseModuleName}`, e);
1347
- return {
1348
- value: requestedName
1349
- };
1350
1091
  }
1351
- });
1352
- return runtime;
1092
+ }
1093
+ return null;
1353
1094
  }
1354
1095
 
1355
- async function createService(projectRoot, opts, host = {
1096
+ async function createService(projectRoot, host = {
1356
1097
  ...ts.sys,
1357
1098
  writeFile: (fileName, data, encoding) => {
1358
1099
  fs.mkdirSync(path__default.dirname(fileName), {
@@ -1368,48 +1109,73 @@ async function createService(projectRoot, opts, host = {
1368
1109
  return undefined;
1369
1110
  }
1370
1111
  }
1371
- }, loader) {
1112
+ }) {
1372
1113
  const compilerOptions = getCompilerOptions(projectRoot, host);
1373
1114
  const sourceFileHandler = new ValSourceFileHandler(projectRoot, compilerOptions, host);
1374
- const module = await newQuickJSWASMModule();
1375
- const runtime = await newValQuickJSRuntime(module, loader || new ValModuleLoader(projectRoot, compilerOptions, sourceFileHandler, host, opts.disableCache === undefined ? process.env.NODE_ENV === "development" ? false : true : opts.disableCache));
1376
- return new Service(projectRoot, sourceFileHandler, runtime);
1115
+ const valModules = loadValModules(projectRoot);
1116
+ const extracted = await extractValModules(valModules);
1117
+ return new Service(projectRoot, sourceFileHandler, extracted);
1377
1118
  }
1378
1119
  class Service {
1379
- constructor(projectRoot, sourceFileHandler, runtime) {
1120
+ constructor(projectRoot, sourceFileHandler, extracted) {
1380
1121
  this.sourceFileHandler = sourceFileHandler;
1381
- this.runtime = runtime;
1122
+ this.extracted = extracted;
1382
1123
  this.projectRoot = projectRoot;
1383
1124
  }
1125
+
1126
+ /**
1127
+ * The module file paths that are registered in the project's val.modules.
1128
+ */
1129
+ getModuleFilePaths() {
1130
+ return Object.keys(this.extracted.sources);
1131
+ }
1384
1132
  async get(moduleFilePath, modulePath, options) {
1385
- const valModule = await readValFile(moduleFilePath, this.projectRoot, this.runtime, options ?? {
1386
- validate: true,
1387
- source: true,
1388
- schema: true
1389
- });
1390
- if (valModule.source && valModule.schema) {
1391
- const resolved = Internal.resolvePath(modulePath, valModule.source, valModule.schema);
1392
- const sourcePath = resolved.path ? [moduleFilePath, resolved.path].join(".") : moduleFilePath;
1133
+ const opts = options ?? {
1134
+ validate: true
1135
+ };
1136
+ const source = this.extracted.sources[moduleFilePath];
1137
+ const schema = this.extracted.schemas[moduleFilePath];
1138
+ const serializedSchema = this.extracted.serializedSchemas[moduleFilePath];
1139
+ const moduleError = this.extracted.moduleErrors.find(e => e.path === moduleFilePath);
1140
+ if (source === undefined || schema === undefined || serializedSchema === undefined) {
1141
+ return {
1142
+ path: moduleFilePath,
1143
+ errors: {
1144
+ invalidModulePath: moduleFilePath,
1145
+ fatal: [{
1146
+ message: (moduleError === null || moduleError === void 0 ? void 0 : moduleError.message) ?? `Module '${moduleFilePath}' was not found in val.modules`
1147
+ }]
1148
+ }
1149
+ };
1150
+ }
1151
+ const validation = opts.validate ? schema["executeValidate"](moduleFilePath, source) : false;
1152
+ const resolved = Internal.resolvePath(modulePath, source, serializedSchema);
1153
+ const sourcePath = resolved.path ? [moduleFilePath, resolved.path].join(".") : moduleFilePath;
1154
+ if (!validation && !moduleError) {
1393
1155
  return {
1394
1156
  path: sourcePath,
1395
- schema: resolved.schema instanceof Schema ? resolved.schema["executeSerialize"]() : resolved.schema,
1396
1157
  source: resolved.source,
1397
- errors: valModule.errors && valModule.errors.validation ? {
1398
- validation: valModule.errors.validation || undefined,
1399
- fatal: valModule.errors.fatal || undefined
1400
- } : valModule.errors ? {
1401
- fatal: valModule.errors.fatal || undefined
1402
- } : false
1158
+ schema: resolved.schema,
1159
+ errors: false
1403
1160
  };
1404
- } else {
1405
- return valModule;
1406
1161
  }
1162
+ return {
1163
+ path: sourcePath,
1164
+ source: resolved.source,
1165
+ schema: resolved.schema,
1166
+ errors: {
1167
+ validation: validation || undefined,
1168
+ fatal: moduleError ? [{
1169
+ message: moduleError.message
1170
+ }] : undefined
1171
+ }
1172
+ };
1407
1173
  }
1408
1174
  async patch(moduleFilePath, patch) {
1409
- await patchValFile(moduleFilePath, this.projectRoot, patch, this.sourceFileHandler, this.runtime);
1175
+ await patchValFile(moduleFilePath, this.projectRoot, patch, this.sourceFileHandler);
1410
1176
  }
1411
1177
  dispose() {
1412
- this.runtime.dispose();
1178
+ // No-op: the vm-based loader holds no disposable resources.
1413
1179
  }
1414
1180
  }
1415
1181
 
@@ -1559,6 +1325,14 @@ class ValOps {
1559
1325
  async getSchemas() {
1560
1326
  return this.initSources().then(result => result.schemas);
1561
1327
  }
1328
+ async getSerializedSchemas() {
1329
+ const schemas = await this.getSchemas();
1330
+ const serialized = {};
1331
+ for (const [moduleFilePathS, schema] of Object.entries(schemas)) {
1332
+ serialized[moduleFilePathS] = schema["executeSerialize"]();
1333
+ }
1334
+ return serialized;
1335
+ }
1562
1336
  async getModuleErrors() {
1563
1337
  return this.initSources().then(result => result.moduleErrors);
1564
1338
  }
@@ -1583,6 +1357,7 @@ class ValOps {
1583
1357
  if (patch.appliedAt) {
1584
1358
  continue;
1585
1359
  }
1360
+ let hasSourceFileOps = false;
1586
1361
  for (const op of patch.patch) {
1587
1362
  if (op.op === "file") {
1588
1363
  const filePath = op.filePath;
@@ -1593,6 +1368,13 @@ class ValOps {
1593
1368
  };
1594
1369
  continue;
1595
1370
  }
1371
+ hasSourceFileOps = true;
1372
+ }
1373
+ // Once per patch, NOT once per op: prepare() re-looks-up the patch by id
1374
+ // and applies the whole thing for every entry, so a patch with two source
1375
+ // ops used to be applied twice. Idempotent for "replace", destructive for
1376
+ // array add/remove/move.
1377
+ if (hasSourceFileOps) {
1596
1378
  const path = patch.path;
1597
1379
  if (!patchesByModule[path]) {
1598
1380
  patchesByModule[path] = [];
@@ -1712,6 +1494,26 @@ class ValOps {
1712
1494
  };
1713
1495
  }
1714
1496
 
1497
+ /**
1498
+ * Every module's source, with the pending patches applied.
1499
+ *
1500
+ * `getSources(analysis)` returns ONLY the modules that had patches, which is
1501
+ * not enough to validate with: cross-module checks (keyOf, router routes)
1502
+ * resolve against other modules' sources and report spurious errors when they
1503
+ * are absent. `/sources/~` overlays the two for exactly this reason.
1504
+ */
1505
+ async getSourcesWithPatchesApplied(analysis) {
1506
+ const unpatched = await this.getSources();
1507
+ const patched = await this.getSources(analysis);
1508
+ return {
1509
+ sources: {
1510
+ ...unpatched.sources,
1511
+ ...patched.sources
1512
+ },
1513
+ errors: patched.errors
1514
+ };
1515
+ }
1516
+
1715
1517
  // #region validateSources
1716
1518
  async validateSources(schemas, sources, patchesByModule) {
1717
1519
  const errors = {};
@@ -1987,13 +1789,28 @@ class ValOps {
1987
1789
  }
1988
1790
 
1989
1791
  // #region prepareCommit
1990
- async prepare(patchAnalysis) {
1792
+ /**
1793
+ * Applies the pending patches to the source files so they can be committed.
1794
+ *
1795
+ * @param options.continueOnError Diagnosis only. By default a patch that
1796
+ * cannot be applied aborts the rest of that module's chain, which is what
1797
+ * /save requires: the commit is refused and nothing is written. With this
1798
+ * flag the failing patch is recorded in `unappliablePatches` and the chain
1799
+ * continues on the unchanged source file, so a single run reports *every*
1800
+ * unappliable patch instead of only the first one per module. The commit is
1801
+ * still refused (`hasErrors` stays true) - this only makes the report
1802
+ * complete.
1803
+ */
1804
+ async prepare(patchAnalysis, options) {
1805
+ const continueOnError = (options === null || options === void 0 ? void 0 : options.continueOnError) ?? false;
1991
1806
  const {
1992
1807
  patchesByModule,
1993
1808
  fileLastUpdatedByPatchId
1994
1809
  } = patchAnalysis;
1995
1810
  const patchedSourceFiles = {};
1996
1811
  const previousSourceFiles = {};
1812
+ const partiallyPatchedSourceFiles = {};
1813
+ const unappliablePatches = {};
1997
1814
  const applySourceFilePatches = async (path, patches) => {
1998
1815
  const sourceFileRes = await this.getSourceFile(path);
1999
1816
  const errors = [];
@@ -2018,9 +1835,18 @@ class ValOps {
2018
1835
  } of patches) {
2019
1836
  const patchData = patchAnalysis.patches.find(p => p.patchId === patchId);
2020
1837
  if (!patchData) {
1838
+ const message = `Analysis required non-existing patch: ${patchId}`;
2021
1839
  errors.push({
2022
- message: `Analysis required non-existing patch: ${patchId}`
1840
+ message
2023
1841
  });
1842
+ unappliablePatches[patchId] = {
1843
+ moduleFilePath: path,
1844
+ message
1845
+ };
1846
+ triedPatches.push(patchId);
1847
+ if (continueOnError) {
1848
+ continue;
1849
+ }
2024
1850
  break;
2025
1851
  }
2026
1852
  const patch = patchData.patch;
@@ -2046,12 +1872,29 @@ class ValOps {
2046
1872
  }, null, 2));
2047
1873
  errors.push(patchRes.error);
2048
1874
  }
1875
+ unappliablePatches[patchId] = {
1876
+ moduleFilePath: path,
1877
+ message: formatPatchSourceError(patchRes.error)
1878
+ };
2049
1879
  triedPatches.push(patchId);
1880
+ if (continueOnError) {
1881
+ // Continue from the unchanged source file: the failing patch made
1882
+ // no change, so the rest of the chain applies on top of the state
1883
+ // it had before it. This mirrors what the client already does in
1884
+ // ValSyncEngine.getPatchedSource.
1885
+ continue;
1886
+ }
2050
1887
  break;
2051
1888
  }
2052
1889
  appliedPatches.push(patchId);
2053
1890
  tsSourceFile = patchRes.value;
2054
1891
  }
1892
+ if (errors.length > 0 && continueOnError) {
1893
+ // Diagnosis: expose what the source file would look like with the
1894
+ // appliable patches applied, so a caller can diff it even though the
1895
+ // commit is (correctly) refused.
1896
+ partiallyPatchedSourceFiles[path] = unescape(tsSourceFile.getText(tsSourceFile).replace(/\\u/g, "%u"));
1897
+ }
2055
1898
  if (errors.length === 0) {
2056
1899
  var _this$options;
2057
1900
  // https://github.com/microsoft/TypeScript/issues/36174
@@ -2137,8 +1980,10 @@ class ValOps {
2137
1980
  hasErrors,
2138
1981
  sourceFilePatchErrors,
2139
1982
  binaryFilePatchErrors,
1983
+ unappliablePatches,
2140
1984
  patchedSourceFiles,
2141
1985
  previousSourceFiles,
1986
+ partiallyPatchedSourceFiles,
2142
1987
  patchedBinaryFilesDescriptors,
2143
1988
  appliedPatches,
2144
1989
  skippedPatches,
@@ -2147,6 +1992,18 @@ class ValOps {
2147
1992
  return res;
2148
1993
  }
2149
1994
 
1995
+ /**
1996
+ * Reads a project file as text at whatever revision this ops instance points
1997
+ * at: the deployed commit in http mode, the working tree in fs mode.
1998
+ *
1999
+ * Public counterpart of `getSourceFile`, for the CLI's debug snapshot. The
2000
+ * snapshot has to capture the exact text `prepare` patches, which in http mode
2001
+ * is NOT the local working copy.
2002
+ */
2003
+ async readProjectFile(path) {
2004
+ return this.getSourceFile(path);
2005
+ }
2006
+
2150
2007
  // #region createPatch
2151
2008
  async createPatch(path, patch, patchId, parentRef, sessionId, authorId) {
2152
2009
  const saveRes = await this.saveSourceFilePatch(path, patch, patchId, parentRef, authorId, sessionId);
@@ -2183,6 +2040,16 @@ function isFileSource(value) {
2183
2040
  }
2184
2041
  return false;
2185
2042
  }
2043
+ function formatPatchSourceError(error) {
2044
+ if ("message" in error) {
2045
+ return error.message;
2046
+ } else if (Array.isArray(error)) {
2047
+ return error.map(formatPatchSourceError).join("\n");
2048
+ } else {
2049
+ const _exhaustiveCheck = error;
2050
+ return "Unknown patch source error: " + JSON.stringify(_exhaustiveCheck);
2051
+ }
2052
+ }
2186
2053
  function getFieldsForType(type) {
2187
2054
  if (type === "file") {
2188
2055
  return ["mimeType"];
@@ -3460,14 +3327,22 @@ const NonceResponse = z.object({
3460
3327
  class ValOpsHttp extends ValOps {
3461
3328
  constructor(contentUrl, project, commitSha,
3462
3329
  // TODO: CommitSha
3463
- branch, apiKey, valModules, options) {
3330
+ branch,
3331
+ /**
3332
+ * An api key (how the app itself authenticates) or a personal access token
3333
+ * (how the CLI authenticates after `val login`). Same two shapes as
3334
+ * getSettings / uploadRemoteFile / getPresignedAuthNonce.
3335
+ */
3336
+ auth, valModules, options) {
3464
3337
  super(valModules, options);
3465
3338
  this.contentUrl = contentUrl;
3466
3339
  this.project = project;
3467
3340
  this.commitSha = commitSha;
3468
3341
  this.branch = branch;
3469
- this.authHeaders = {
3470
- Authorization: `Bearer ${apiKey}`
3342
+ this.authHeaders = "pat" in auth ? {
3343
+ "x-val-pat": auth.pat
3344
+ } : {
3345
+ Authorization: `Bearer ${auth.apiKey}`
3471
3346
  };
3472
3347
  this.root = (options === null || options === void 0 ? void 0 : options.root) ?? "";
3473
3348
  }
@@ -3793,8 +3668,31 @@ class ValOpsHttp extends ValOps {
3793
3668
  allErrors.push(...res.errors);
3794
3669
  }
3795
3670
  }
3671
+ // Chunking is a query-string-length workaround, NOT a filter: the content
3672
+ // api returns every applicable patch per request regardless of which
3673
+ // patch_ids we ask for. Concatenating the chunks therefore repeats the
3674
+ // whole chain once per chunk, and prepare() applies each patch that many
3675
+ // times - which corrupts arrays (a "remove" runs N times) and fails the
3676
+ // commit with "Array index out of bounds". Only bites above chunkSize
3677
+ // pending patches, so it stays invisible until a project accumulates them.
3678
+ //
3679
+ // Dedupe by patch id, keeping first occurrence so the api's ordering is
3680
+ // preserved, and keep only what was asked for. Correct whether or not the
3681
+ // api filters on its side.
3682
+ const requestedPatchIds = new Set(patchIds);
3683
+ const seenPatchIds = new Set();
3684
+ const patches = allPatches.filter(patch => {
3685
+ if (!requestedPatchIds.has(patch.patchId)) {
3686
+ return false;
3687
+ }
3688
+ if (seenPatchIds.has(patch.patchId)) {
3689
+ return false;
3690
+ }
3691
+ seenPatchIds.add(patch.patchId);
3692
+ return true;
3693
+ });
3796
3694
  return {
3797
- patches: allPatches,
3695
+ patches,
3798
3696
  errors: Object.keys(allErrors).length > 0 ? allErrors : undefined
3799
3697
  };
3800
3698
  }
@@ -3889,7 +3787,7 @@ class ValOpsHttp extends ValOps {
3889
3787
  return {
3890
3788
  patches,
3891
3789
  error: {
3892
- message: "Could not your changes. It is most likely due to a network issue. Check your network connection and please try again."
3790
+ message: "Could not get your changes. It is most likely due to a network issue. Check your network connection and please try again."
3893
3791
  }
3894
3792
  };
3895
3793
  } catch (err) {
@@ -4516,7 +4414,9 @@ const ValServer = (valModules, options, callbacks) => {
4516
4414
  config: options.config
4517
4415
  });
4518
4416
  } else if (options.mode === "http") {
4519
- serverOps = new ValOpsHttp(options.valContentUrl, options.project, options.commit, options.branch, options.apiKey, valModules, {
4417
+ serverOps = new ValOpsHttp(options.valContentUrl, options.project, options.commit, options.branch, {
4418
+ apiKey: options.apiKey
4419
+ }, valModules, {
4520
4420
  formatter: options.formatter,
4521
4421
  root: options.root,
4522
4422
  config: options.config
@@ -6740,16 +6640,6 @@ const ValServer = (valModules, options, callbacks) => {
6740
6640
  }
6741
6641
  };
6742
6642
  };
6743
- function formatPatchSourceError(error) {
6744
- if ("message" in error) {
6745
- return error.message;
6746
- } else if (Array.isArray(error)) {
6747
- return error.map(formatPatchSourceError).join("\n");
6748
- } else {
6749
- const _exhaustiveCheck = error;
6750
- return "Unknown patch source error: " + JSON.stringify(_exhaustiveCheck);
6751
- }
6752
- }
6753
6643
  function verifyCallbackReq(stateCookie, queryParams) {
6754
6644
  if (typeof stateCookie !== "string") {
6755
6645
  return {
@@ -7203,8 +7093,28 @@ async function readCommit(gitDir, branchName) {
7203
7093
  }
7204
7094
  function createValApiRouter(route, valServerPromise, convert) {
7205
7095
  const uiRequestHandler = createUIRequestHandler();
7096
+ // valServerPromise is created at module-eval time, but only awaited per request.
7097
+ // Without this no-op catch, a config error (e.g. proxy mode without a project)
7098
+ // rejects with no handler attached, which becomes an unhandledRejection and kills
7099
+ // the dev server. The error is still reported per request by the try/catch below.
7100
+ valServerPromise.catch(() => {
7101
+ // handled below
7102
+ });
7206
7103
  return async req => {
7207
- const valServer = await valServerPromise;
7104
+ let valServer;
7105
+ try {
7106
+ valServer = await valServerPromise;
7107
+ } catch (err) {
7108
+ const error = {
7109
+ message: "Val: could not start the Val server",
7110
+ details: err instanceof Error ? err.message : String(err)
7111
+ };
7112
+ console.error(error.message + ": " + error.details);
7113
+ return convert({
7114
+ status: 500,
7115
+ json: error
7116
+ });
7117
+ }
7208
7118
  const url = new URL(req.url);
7209
7119
  if (!url.pathname.startsWith(route)) {
7210
7120
  const error = {
@@ -7417,6 +7327,132 @@ function getCookies(req, cookiesDef) {
7417
7327
  return z.object(cookiesDef).safeParse(input);
7418
7328
  }
7419
7329
 
7330
+ const JsFileLookupMapping = [
7331
+ // NOTE: first one matching will be used
7332
+ [".cjs.d.ts", [".esm.js", ".mjs.js"]], [".cjs.js", [".esm.js", ".mjs.js"]], [".cjs", [".mjs"]], [".d.ts", [".js", ".esm.js", ".mjs.js"]]];
7333
+ const MAX_CACHE_SIZE = 100 * 1024 * 1024; // 100 mb
7334
+ 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
7335
+
7336
+ class ValModuleLoader {
7337
+ constructor(projectRoot, compilerOptions,
7338
+ // TODO: remove this?
7339
+ sourceFileHandler, host = {
7340
+ ...ts.sys,
7341
+ writeFile: (fileName, data, encoding) => {
7342
+ fs.mkdirSync(path__default.dirname(fileName), {
7343
+ recursive: true
7344
+ });
7345
+ fs.writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
7346
+ },
7347
+ rmFile: fs.rmSync,
7348
+ readBuffer: fileName => {
7349
+ try {
7350
+ return fs.readFileSync(fileName);
7351
+ } catch {
7352
+ return undefined;
7353
+ }
7354
+ }
7355
+ }, disableCache = false) {
7356
+ this.projectRoot = projectRoot;
7357
+ this.compilerOptions = compilerOptions;
7358
+ this.sourceFileHandler = sourceFileHandler;
7359
+ this.host = host;
7360
+ this.disableCache = disableCache;
7361
+ this.cache = {};
7362
+ this.cacheSize = 0;
7363
+ }
7364
+ getModule(modulePath) {
7365
+ if (!modulePath) {
7366
+ throw Error(`Illegal module path: "${modulePath}"`);
7367
+ }
7368
+ const code = this.host.readFile(modulePath);
7369
+ if (!code) {
7370
+ throw Error(`Could not read file "${modulePath}"`);
7371
+ }
7372
+ let compiledCode;
7373
+ if (this.cache[code] && !this.disableCache) {
7374
+ // TODO: use hash instead of code as key
7375
+ compiledCode = this.cache[code];
7376
+ } else {
7377
+ compiledCode = transform(code, {
7378
+ filePath: modulePath,
7379
+ disableESTransforms: true,
7380
+ transforms: ["typescript"]
7381
+ }).code;
7382
+ if (!this.disableCache) {
7383
+ if (this.cacheSize > MAX_CACHE_SIZE) {
7384
+ console.warn("Cache size exceeded, clearing cache");
7385
+ this.cache = {};
7386
+ this.cacheSize = 0;
7387
+ }
7388
+ if (code.length < MAX_OBJECT_KEY_SIZE) {
7389
+ this.cache[code] = compiledCode;
7390
+ this.cacheSize += code.length + compiledCode.length; // code is mostly ASCII so 1 byte per char
7391
+ }
7392
+ }
7393
+ }
7394
+ return compiledCode;
7395
+ }
7396
+ resolveModulePath(containingFilePath, requestedModuleName) {
7397
+ var _this$host$realpath, _this$host;
7398
+ let sourceFileName = this.sourceFileHandler.resolveSourceModulePath(containingFilePath, requestedModuleName);
7399
+ if (requestedModuleName === "@vercel/stega") {
7400
+ sourceFileName = this.sourceFileHandler.resolveSourceModulePath(containingFilePath, "@vercel/stega").replace("stega/dist", "stega/dist/esm");
7401
+ }
7402
+ const matches = this.findMatchingJsFile(sourceFileName);
7403
+ if (matches.match === false) {
7404
+ let debugInfo = "";
7405
+ if (sourceFileName.includes("val.config")) {
7406
+ debugInfo = `\n@valbuild directory scan:\n${this.host.readDirectory("/", ["js", "ts", "json"], [], ["**/@valbuild/*"]).join("\n")}`;
7407
+ }
7408
+ throw Error(`Could not find matching js file for module "${requestedModuleName}" requested by: "${containingFilePath}". Tried:\n${matches.tried.join("\n")}${debugInfo}`);
7409
+ }
7410
+ const filePath = matches.match;
7411
+ // resolve all symlinks (preconstruct for example symlinks the dist folder)
7412
+ 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;
7413
+ if (!followedPath) {
7414
+ throw Error(`File path was empty: "${filePath}", containing file: "${containingFilePath}", requested module: "${requestedModuleName}"`);
7415
+ }
7416
+ return followedPath;
7417
+ }
7418
+ findMatchingJsFile(filePath) {
7419
+ let requiresReplacements = false;
7420
+ for (const [currentEnding] of JsFileLookupMapping) {
7421
+ if (filePath.endsWith(currentEnding)) {
7422
+ requiresReplacements = true;
7423
+ break;
7424
+ }
7425
+ }
7426
+ // avoid unnecessary calls to fileExists if we don't need to replace anything
7427
+ if (!requiresReplacements) {
7428
+ if (this.host.fileExists(filePath)) {
7429
+ return {
7430
+ match: filePath
7431
+ };
7432
+ }
7433
+ }
7434
+ const tried = [];
7435
+ for (const [currentEnding, replacements] of JsFileLookupMapping) {
7436
+ if (filePath.endsWith(currentEnding)) {
7437
+ for (const replacement of replacements) {
7438
+ const newFilePath = filePath.slice(0, -currentEnding.length) + replacement;
7439
+ if (this.host.fileExists(newFilePath)) {
7440
+ return {
7441
+ match: newFilePath
7442
+ };
7443
+ } else {
7444
+ tried.push(newFilePath);
7445
+ }
7446
+ }
7447
+ }
7448
+ }
7449
+ return {
7450
+ match: false,
7451
+ tried: tried.concat(filePath)
7452
+ };
7453
+ }
7454
+ }
7455
+
7420
7456
  /**
7421
7457
  * An implementation of methods in the various ts.*Host interfaces
7422
7458
  * that uses ValFS to resolve modules and read/write files.
@@ -7531,10 +7567,10 @@ async function checkRemoteRef(remoteHost, ref, projectRoot, schema, metadata) {
7531
7567
  error: `File path is missing in remote ref: ${ref}`
7532
7568
  };
7533
7569
  }
7534
- if (!relativeFilePath.startsWith("public/val/")) {
7570
+ if (!relativeFilePath.startsWith("public/")) {
7535
7571
  return {
7536
7572
  status: "error",
7537
- error: `File path must be within the public/val/ directory (e.g. public/val/path/to/file.txt). Got: ${relativeFilePath}`
7573
+ error: `File path must be within the public/ directory (e.g. public/path/to/file.txt). Got: ${relativeFilePath}`
7538
7574
  };
7539
7575
  }
7540
7576
  const coreVersion = Internal.VERSION.core || "unknown";
@@ -7687,6 +7723,11 @@ async function downloadFileFromRemote(ref, filePath) {
7687
7723
  });
7688
7724
  }
7689
7725
 
7726
+ // A remaining error may optionally carry a more specific `sourcePath` than the
7727
+ // one the fix was created from. This is used by gallery checks, where a single
7728
+ // record-level fix expands into per-entry errors that should point at the
7729
+ // individual entry (e.g. `?p="/public/val/logo.png"`) rather than the record.
7730
+
7690
7731
  // TODO: find a better name? transformFixesToPatch?
7691
7732
  async function createFixPatch(config, apply, sourcePath, validationError, remoteFiles, moduleSource, moduleSchema) {
7692
7733
  const remainingErrors = [];
@@ -7893,10 +7934,10 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
7893
7934
  });
7894
7935
  continue;
7895
7936
  }
7896
- if (!filePath.startsWith("public/val/")) {
7937
+ if (!filePath.startsWith("public/")) {
7897
7938
  remainingErrors.push({
7898
7939
  ...validationError,
7899
- message: "Unexpected error while downloading remote (invalid file path - must start with public/val/)",
7940
+ message: "Unexpected error while downloading remote (invalid file path - must start with public/)",
7900
7941
  fixes: undefined
7901
7942
  });
7902
7943
  continue;
@@ -7981,7 +8022,11 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
7981
8022
  } else {
7982
8023
  remainingErrors.push({
7983
8024
  ...validationError,
7984
- 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.`
8025
+ 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.`,
8026
+ sourcePath: Internal.createValPathOfItem(sourcePath, entryKey),
8027
+ // Gallery entries are keyed by their file path; surface the
8028
+ // error on the key rather than the derived metadata value.
8029
+ keyError: true
7985
8030
  });
7986
8031
  }
7987
8032
  }
@@ -8002,7 +8047,11 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
8002
8047
  } else {
8003
8048
  remainingErrors.push({
8004
8049
  ...validationError,
8005
- message: `File metadata for '${entryKey}' has incorrect mimeType: '${stored.mimeType ?? "<empty>"}' vs '${actualMetadata.mimeType}'. Use --fix to update.`
8050
+ message: `File metadata for '${entryKey}' has incorrect mimeType: '${stored.mimeType ?? "<empty>"}' vs '${actualMetadata.mimeType}'. Use --fix to update.`,
8051
+ sourcePath: Internal.createValPathOfItem(sourcePath, entryKey),
8052
+ // Gallery entries are keyed by their file path; surface the
8053
+ // error on the key rather than the derived metadata value.
8054
+ keyError: true
8006
8055
  });
8007
8056
  }
8008
8057
  }
@@ -8160,4 +8209,262 @@ async function getFileMetadata(projectRoot, validationError) {
8160
8209
  return extractFileMetadata(fileRef);
8161
8210
  }
8162
8211
 
8163
- export { Service, ValFSHost, ValModuleLoader, ValSourceFileHandler, createFixPatch, createService, createValApiRouter, createValServer, decodeJwt, encodeJwt, formatSyntaxErrorTree, getCompilerOptions, getExpire, getPersonalAccessTokenPath, getSettings, parsePersonalAccessTokenFile, patchSourceFile, safeReadGit, uploadRemoteFile };
8212
+ function getModulePathRange(modulePath, modulePathMap,
8213
+ // Which part of an object/record member to point at. For an object property
8214
+ // the resolved node's own range is the *key* (property name); the *value*
8215
+ // range is stored under `children.val`. Array elements, leaf literals and
8216
+ // `c.image`/`c.file` `_ref`/`metadata` nodes have no `val` child, so "value"
8217
+ // falls back to the node's own range for those. Defaults to "key" to preserve
8218
+ // existing callers.
8219
+ target = "key") {
8220
+ var _range$children;
8221
+ // Handle empty or invalid module paths gracefully
8222
+ if (!modulePath || typeof modulePath !== "string") {
8223
+ return undefined;
8224
+ }
8225
+ let segments;
8226
+ try {
8227
+ // Quote-aware splitter that correctly handles keys containing dots
8228
+ // (e.g. file refs like `"/public/val/images/logo.png"`), unlike a naive
8229
+ // split on ".". Throws on malformed input (e.g. unbalanced quotes).
8230
+ segments = Internal.splitModulePath(modulePath);
8231
+ } catch {
8232
+ // Return undefined if the module path is malformed. This can happen when
8233
+ // there are upstream errors in schema serialization.
8234
+ return undefined;
8235
+ }
8236
+ if (segments.length === 0) {
8237
+ return undefined;
8238
+ }
8239
+ let range = modulePathMap[segments[0]];
8240
+ for (const pathSegment of segments.slice(1)) {
8241
+ var _range;
8242
+ if (!range) {
8243
+ break;
8244
+ }
8245
+ range = (_range = range) === null || _range === void 0 || (_range = _range.children) === null || _range === void 0 ? void 0 : _range[pathSegment];
8246
+ }
8247
+ if (!range) {
8248
+ return undefined;
8249
+ }
8250
+ const valueRange = target === "value" ? (_range$children = range.children) === null || _range$children === void 0 ? void 0 : _range$children.val : undefined;
8251
+ const resolved = valueRange ?? range;
8252
+ return resolved.start && resolved.end && {
8253
+ start: resolved.start,
8254
+ end: resolved.end
8255
+ };
8256
+ }
8257
+
8258
+ /**
8259
+ * The line/character range of `node`'s own text (leading trivia excluded).
8260
+ *
8261
+ * NOTE: do not compute the start as `end.character - node.getWidth()`. That
8262
+ * identity only holds while the node stays on a single line - for a multi-line
8263
+ * node (an object inside an array, a `c.image` metadata argument, ...) it
8264
+ * reports the *closing* line and a negative character.
8265
+ */
8266
+ function rangeOf(node, sourceFile) {
8267
+ return {
8268
+ start: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)),
8269
+ end: sourceFile.getLineAndCharacterOfPosition(node.getEnd())
8270
+ };
8271
+ }
8272
+ function createModulePathMap(sourceFile) {
8273
+ for (const child of sourceFile.getChildren().flatMap(child => child.getChildren())) {
8274
+ if (ts.isExportAssignment(child)) {
8275
+ const contentNode = child.expression && ts.isCallExpression(child.expression) && child.expression.arguments[2];
8276
+ if (contentNode) {
8277
+ return traverse(contentNode, sourceFile);
8278
+ }
8279
+ }
8280
+ }
8281
+ }
8282
+ function traverse(node, sourceFile) {
8283
+ if (ts.isStringLiteral(node) || ts.isNumericLiteral(node)) {
8284
+ return {
8285
+ "": {
8286
+ children: {},
8287
+ ...rangeOf(node, sourceFile)
8288
+ }
8289
+ };
8290
+ }
8291
+ if (ts.isObjectLiteralExpression(node)) {
8292
+ return traverseObjectLiteral(node, sourceFile);
8293
+ }
8294
+ if (ts.isArrayLiteralExpression(node)) {
8295
+ return traverseArrayLiteral(node, sourceFile);
8296
+ }
8297
+ if (ts.isCallExpression(node)) {
8298
+ return traverseCallExpression(node, sourceFile);
8299
+ }
8300
+ }
8301
+ function traverseCallExpression(node, sourceFile) {
8302
+ if (ts.isPropertyAccessExpression(node.expression)) {
8303
+ if (node.expression.expression.getText(sourceFile) === "c" && (node.expression.name.getText(sourceFile) === "file" || node.expression.name.getText(sourceFile) === "image")) {
8304
+ const val = {
8305
+ children: {},
8306
+ ...rangeOf(node, sourceFile)
8307
+ };
8308
+ if (node.arguments[0]) {
8309
+ const _ref = {
8310
+ children: {},
8311
+ ...rangeOf(node.arguments[0], sourceFile)
8312
+ };
8313
+ if (!node.arguments[1]) {
8314
+ return {
8315
+ val,
8316
+ _ref
8317
+ };
8318
+ }
8319
+ return {
8320
+ val,
8321
+ _ref,
8322
+ metadata: {
8323
+ children: {},
8324
+ ...rangeOf(node.arguments[1], sourceFile)
8325
+ }
8326
+ };
8327
+ }
8328
+ }
8329
+ }
8330
+ }
8331
+ function traverseArrayLiteral(node, sourceFile) {
8332
+ return node.elements.reduce((acc, element, index) => {
8333
+ if (ts.isExpression(element)) {
8334
+ return {
8335
+ ...acc,
8336
+ [index]: {
8337
+ children: traverse(element, sourceFile),
8338
+ ...rangeOf(element, sourceFile)
8339
+ }
8340
+ };
8341
+ }
8342
+ return acc;
8343
+ }, {});
8344
+ }
8345
+ function traverseObjectLiteral(node, sourceFile) {
8346
+ return node.properties.reduce((acc, property) => {
8347
+ if (ts.isPropertyAssignment(property)) {
8348
+ const key = property.name && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) && property.name.text;
8349
+ const value = property.initializer;
8350
+ if (key) {
8351
+ const val = {
8352
+ children: {},
8353
+ ...rangeOf(property.initializer, sourceFile)
8354
+ };
8355
+ return {
8356
+ ...acc,
8357
+ [key]: {
8358
+ children: {
8359
+ val,
8360
+ ...traverse(value, sourceFile)
8361
+ },
8362
+ ...rangeOf(property.name, sourceFile)
8363
+ }
8364
+ };
8365
+ }
8366
+ }
8367
+ return acc;
8368
+ }, {});
8369
+ }
8370
+
8371
+ /**
8372
+ * Replays a `val debug` snapshot: applies its patches the way /save does and
8373
+ * validates the result.
8374
+ *
8375
+ * A snapshot is a minimal Val project (the modules the patches touch plus the
8376
+ * ones they reference, a generated val.modules.ts, and the patch chain under
8377
+ * .val/patches), so replaying it is just a ValOpsFS pointed at the directory -
8378
+ * no snapshot-specific code paths, which is the point: if the replay reproduces
8379
+ * the bug, the bug is in the ordinary code.
8380
+ */
8381
+
8382
+ async function replaySnapshot(snapshotDir) {
8383
+ const root = path__default.resolve(snapshotDir);
8384
+ if (!fs.existsSync(path__default.join(root, "val.modules.ts"))) {
8385
+ throw new Error(`Not a Val debug snapshot: no val.modules.ts in ${root}. ` + `Unzip the snapshot first.`);
8386
+ }
8387
+ const valModules = loadValModules(root);
8388
+ const serverOps = new ValOpsFS(process.env.VAL_CONTENT_URL || DEFAULT_CONTENT_HOST, root, valModules,
8389
+ // The snapshot's own val.config, as evaluated by loadValModules - no need to
8390
+ // re-read it, and this way the replay uses exactly the config the snapshot
8391
+ // carries (files.directory in particular).
8392
+ {
8393
+ config: valModules.config
8394
+ });
8395
+ const patchesRes = await serverOps.fetchPatches({
8396
+ patchIds: undefined,
8397
+ excludePatchOps: false
8398
+ });
8399
+ if (patchesRes.error) {
8400
+ throw new Error(`Could not read the snapshot's patches: ${patchesRes.error.message}`);
8401
+ }
8402
+ if (patchesRes.errors && patchesRes.errors.length > 0) {
8403
+ for (const err of patchesRes.errors) {
8404
+ console.error(`Snapshot patch could not be read: ${err.message}`);
8405
+ }
8406
+ }
8407
+ const analysis = {
8408
+ ...serverOps.analyzePatches(patchesRes.patches),
8409
+ ...patchesRes
8410
+ };
8411
+ const prepared = await serverOps.prepare(analysis, {
8412
+ continueOnError: true
8413
+ });
8414
+ const sources = await serverOps.getSourcesWithPatchesApplied(analysis);
8415
+ const schemas = await serverOps.getSchemas();
8416
+ const validation = await serverOps.validateSources(schemas, sources.sources, analysis.patchesByModule);
8417
+ const patchedSourceFiles = {};
8418
+ for (const [moduleFilePath, contents] of Object.entries(prepared.patchedSourceFiles)) {
8419
+ if (contents !== null) {
8420
+ patchedSourceFiles[moduleFilePath] = contents;
8421
+ }
8422
+ }
8423
+ Object.assign(patchedSourceFiles, prepared.partiallyPatchedSourceFiles);
8424
+ return {
8425
+ patches: patchesRes.patches.map(patch => {
8426
+ var _prepared$unappliable;
8427
+ return {
8428
+ patchId: patch.patchId,
8429
+ moduleFilePath: patch.path,
8430
+ createdAt: patch.createdAt,
8431
+ authorId: patch.authorId,
8432
+ error: (_prepared$unappliable = prepared.unappliablePatches[patch.patchId]) === null || _prepared$unappliable === void 0 ? void 0 : _prepared$unappliable.message
8433
+ };
8434
+ }),
8435
+ unappliablePatches: prepared.unappliablePatches,
8436
+ sourceFilePatchErrors: Object.fromEntries(Object.entries(prepared.sourceFilePatchErrors).map(([key, errors]) => [key, errors.map(formatPatchSourceError)])),
8437
+ binaryFilePatchErrors: prepared.binaryFilePatchErrors,
8438
+ validationErrors: validation.errors,
8439
+ patchedSourceFiles,
8440
+ hasErrors: prepared.hasErrors
8441
+ };
8442
+ }
8443
+
8444
+ /**
8445
+ * Compares a replay against the report captured when the snapshot was taken, so
8446
+ * "reproduced the customer's bug" is distinguishable from "behaves differently
8447
+ * on this version".
8448
+ */
8449
+ function compareWithCapturedReport(result, capturedReport) {
8450
+ const capturedIds = Object.keys(capturedReport.unappliablePatches ?? {});
8451
+ const nowIds = Object.keys(result.unappliablePatches);
8452
+ const stillFailing = capturedIds.filter(patchId => nowIds.includes(patchId));
8453
+ const nowApplying = capturedIds.filter(patchId => !nowIds.includes(patchId));
8454
+ const newlyFailing = nowIds.filter(patchId => !capturedIds.includes(patchId));
8455
+ return {
8456
+ stillFailing,
8457
+ nowApplying,
8458
+ newlyFailing,
8459
+ reproduced: capturedIds.length > 0 && nowApplying.length === 0 && newlyFailing.length === 0
8460
+ };
8461
+ }
8462
+ function readCapturedReport(snapshotDir) {
8463
+ const reportPath = path__default.join(path__default.resolve(snapshotDir), "report.json");
8464
+ if (!fs.existsSync(reportPath)) {
8465
+ return null;
8466
+ }
8467
+ return JSON.parse(fs.readFileSync(reportPath, "utf-8"));
8468
+ }
8469
+
8470
+ export { Service, ValFSHost, ValModuleLoader, ValOpsFS, ValOpsHttp, ValSourceFileHandler, compareWithCapturedReport, createFixPatch, createModulePathMap, createService, createValApiRouter, createValServer, decodeJwt, encodeJwt, formatPatchSourceError, formatSyntaxErrorTree, getCompilerOptions, getExpire, getModulePathRange, getPersonalAccessTokenPath, getSettings, loadValModules, parsePersonalAccessTokenFile, patchSourceFile, readCapturedReport, replaySnapshot, safeReadGit, uploadRemoteFile };