@valbuild/server 0.97.3 → 0.97.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
@@ -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,
|
|
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
|
|
10
|
-
import {
|
|
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
|
|
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
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
if (
|
|
1076
|
-
throw Error(`Could not
|
|
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
|
-
|
|
918
|
+
throw Error(`Could not parse config file: ${configFilePath}. Error: ${error.messageText.messageText}`);
|
|
1101
919
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
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
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
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 (
|
|
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
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
1242
|
-
|
|
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
|
-
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
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
|
|
1092
|
+
}
|
|
1093
|
+
return null;
|
|
1353
1094
|
}
|
|
1354
1095
|
|
|
1355
|
-
async function createService(projectRoot,
|
|
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
|
-
}
|
|
1112
|
+
}) {
|
|
1372
1113
|
const compilerOptions = getCompilerOptions(projectRoot, host);
|
|
1373
1114
|
const sourceFileHandler = new ValSourceFileHandler(projectRoot, compilerOptions, host);
|
|
1374
|
-
const
|
|
1375
|
-
const
|
|
1376
|
-
return new Service(projectRoot, sourceFileHandler,
|
|
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,
|
|
1120
|
+
constructor(projectRoot, sourceFileHandler, extracted) {
|
|
1380
1121
|
this.sourceFileHandler = sourceFileHandler;
|
|
1381
|
-
this.
|
|
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
|
|
1386
|
-
validate: true
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
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
|
-
|
|
1398
|
-
|
|
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
|
|
1175
|
+
await patchValFile(moduleFilePath, this.projectRoot, patch, this.sourceFileHandler);
|
|
1410
1176
|
}
|
|
1411
1177
|
dispose() {
|
|
1412
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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/
|
|
7570
|
+
if (!relativeFilePath.startsWith("public/")) {
|
|
7535
7571
|
return {
|
|
7536
7572
|
status: "error",
|
|
7537
|
-
error: `File path must be within the public/
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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 };
|