@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.
- package/dist/declarations/src/Service.d.ts +10 -16
- package/dist/declarations/src/ValOps.d.ts +349 -0
- package/dist/declarations/src/ValOpsFS.d.ts +143 -0
- package/dist/declarations/src/ValOpsHttp.d.ts +147 -0
- package/dist/declarations/src/createFixPatch.d.ts +4 -1
- package/dist/declarations/src/debug/replaySnapshot.d.ts +54 -0
- package/dist/declarations/src/index.d.ts +9 -1
- package/dist/declarations/src/loadValModules.d.ts +23 -0
- package/dist/declarations/src/modulePathMap.d.ts +25 -0
- package/dist/declarations/src/patchValFile.d.ts +1 -2
- package/dist/valbuild-server.cjs.dev.js +809 -492
- package/dist/valbuild-server.cjs.prod.js +809 -492
- package/dist/valbuild-server.esm.js +801 -494
- package/package.json +4 -5
|
@@ -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
|
|
13
|
-
var
|
|
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
|
|
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
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
if (
|
|
1107
|
-
throw Error(`Could not
|
|
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
|
-
|
|
950
|
+
throw Error(`Could not parse config file: ${configFilePath}. Error: ${error.messageText.messageText}`);
|
|
1132
951
|
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
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
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
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
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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 (
|
|
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
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
1273
|
-
|
|
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
|
-
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
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
|
|
1124
|
+
}
|
|
1125
|
+
return null;
|
|
1384
1126
|
}
|
|
1385
1127
|
|
|
1386
|
-
async function createService(projectRoot,
|
|
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
|
-
}
|
|
1144
|
+
}) {
|
|
1403
1145
|
const compilerOptions = getCompilerOptions(projectRoot, host);
|
|
1404
1146
|
const sourceFileHandler = new ValSourceFileHandler(projectRoot, compilerOptions, host);
|
|
1405
|
-
const
|
|
1406
|
-
const
|
|
1407
|
-
return new Service(projectRoot, sourceFileHandler,
|
|
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,
|
|
1152
|
+
constructor(projectRoot, sourceFileHandler, extracted) {
|
|
1411
1153
|
this.sourceFileHandler = sourceFileHandler;
|
|
1412
|
-
this.
|
|
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
|
|
1417
|
-
validate: true
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
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
|
-
|
|
1429
|
-
|
|
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
|
|
1207
|
+
await patchValFile(moduleFilePath, this.projectRoot, patch, this.sourceFileHandler);
|
|
1441
1208
|
}
|
|
1442
1209
|
dispose() {
|
|
1443
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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/
|
|
7602
|
+
if (!relativeFilePath.startsWith("public/")) {
|
|
7566
7603
|
return {
|
|
7567
7604
|
status: "error",
|
|
7568
|
-
error: `File path must be within the public/
|
|
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/
|
|
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/
|
|
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;
|