@valbuild/server 0.97.7 → 0.99.0
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/checkRemoteRef.d.ts +20 -0
- package/dist/declarations/src/evalValConfigFile.d.ts +17 -0
- package/dist/declarations/src/extractMetadata.d.ts +4 -0
- package/dist/declarations/src/getFileExt.d.ts +1 -0
- package/dist/declarations/src/getValidationErrorFileRef.d.ts +2 -0
- package/dist/declarations/src/hasRemoteFileSchema.d.ts +5 -0
- package/dist/declarations/src/index.d.ts +11 -0
- package/dist/declarations/src/loadValModules.d.ts +11 -1
- package/dist/declarations/src/login.d.ts +69 -0
- package/dist/declarations/src/modulePathMap.d.ts +4 -0
- package/dist/declarations/src/patch/ts/valModule.d.ts +8 -0
- package/dist/declarations/src/validateMetadata.d.ts +6 -0
- package/dist/valbuild-server.cjs.dev.js +630 -201
- package/dist/valbuild-server.cjs.prod.js +630 -201
- package/dist/valbuild-server.esm.js +452 -45
- package/package.json +4 -4
|
@@ -6,16 +6,17 @@ import * as path from 'path';
|
|
|
6
6
|
import path__default from 'path';
|
|
7
7
|
import fs, { promises } from 'fs';
|
|
8
8
|
import vm from 'node:vm';
|
|
9
|
-
import { Module } from 'node:module';
|
|
9
|
+
import { Module, createRequire } from 'node:module';
|
|
10
10
|
import { resolveSchemaSourceFixForError, getErrorMessageFromUnknownJson, Patch, ParentRef, VAL_ENABLE_COOKIE_NAME, VAL_STATE_COOKIE, VAL_SESSION_COOKIE, Api } from '@valbuild/shared/internal';
|
|
11
11
|
import { createUIRequestHandler } from '@valbuild/ui/server';
|
|
12
12
|
import crypto$1 from 'crypto';
|
|
13
|
-
import { z } from 'zod';
|
|
13
|
+
import z$1, { z } from 'zod';
|
|
14
14
|
import sizeOf from 'image-size';
|
|
15
15
|
import { fromError } from 'zod-validation-error';
|
|
16
16
|
import { transform } from 'sucrase';
|
|
17
17
|
import http from 'http';
|
|
18
18
|
import https from 'https';
|
|
19
|
+
import fs$1 from 'fs/promises';
|
|
19
20
|
|
|
20
21
|
class ValSyntaxError {
|
|
21
22
|
constructor(message, node) {
|
|
@@ -925,6 +926,15 @@ const getCompilerOptions = (rootDir, parseConfigHost) => {
|
|
|
925
926
|
return parsedConfigFile.options;
|
|
926
927
|
};
|
|
927
928
|
|
|
929
|
+
/**
|
|
930
|
+
* The filesystem seam `loadValModules` reads through.
|
|
931
|
+
*
|
|
932
|
+
* Defaults to `ts.sys`, i.e. the real filesystem. An editor integration (see
|
|
933
|
+
* `@valbuild/language-server`) passes a host that overlays unsaved buffers, so
|
|
934
|
+
* that evaluation sees what the user is looking at rather than what was last
|
|
935
|
+
* saved.
|
|
936
|
+
*/
|
|
937
|
+
|
|
928
938
|
/**
|
|
929
939
|
* Loads the project's root `val.modules.ts` (or `.js`) using Node's `vm`
|
|
930
940
|
* module and returns its default export (a `ValModules` registry).
|
|
@@ -946,24 +956,24 @@ const getCompilerOptions = (rootDir, parseConfigHost) => {
|
|
|
946
956
|
* same trust level as running the project's build. It must never be used to
|
|
947
957
|
* evaluate untrusted or third-party modules.
|
|
948
958
|
*/
|
|
949
|
-
function loadValModules(projectRoot) {
|
|
950
|
-
const valModulesPath = findValModulesPath(projectRoot);
|
|
959
|
+
function loadValModules(projectRoot, host = ts.sys) {
|
|
960
|
+
const valModulesPath = findValModulesPath(projectRoot, host);
|
|
951
961
|
if (!valModulesPath) {
|
|
952
962
|
throw Error(`Could not find 'val.modules.ts' nor 'val.modules.js' in project root: '${projectRoot}'`);
|
|
953
963
|
}
|
|
954
|
-
const compilerOptions = getCompilerOptions(projectRoot,
|
|
964
|
+
const compilerOptions = getCompilerOptions(projectRoot, host);
|
|
955
965
|
const cache = {};
|
|
956
|
-
const loaded = loadModule(valModulesPath, cache, compilerOptions);
|
|
966
|
+
const loaded = loadModule(valModulesPath, cache, compilerOptions, host);
|
|
957
967
|
const valModules = loaded.exports.default;
|
|
958
968
|
if (!valModules) {
|
|
959
969
|
throw Error(`Val modules file at path: '${valModulesPath}' must have a default export. Got: ${valModules}`);
|
|
960
970
|
}
|
|
961
971
|
return valModules;
|
|
962
972
|
}
|
|
963
|
-
function findValModulesPath(projectRoot) {
|
|
973
|
+
function findValModulesPath(projectRoot, host) {
|
|
964
974
|
for (const fileName of ["val.modules.ts", "val.modules.js"]) {
|
|
965
975
|
const candidate = path__default.join(projectRoot, fileName);
|
|
966
|
-
if (
|
|
976
|
+
if (host.fileExists(candidate)) {
|
|
967
977
|
return candidate;
|
|
968
978
|
}
|
|
969
979
|
}
|
|
@@ -1004,12 +1014,15 @@ function makeStub(spec) {
|
|
|
1004
1014
|
const stub = new Proxy({}, handler);
|
|
1005
1015
|
return stub;
|
|
1006
1016
|
}
|
|
1007
|
-
function loadModule(absPath, cache, compilerOptions) {
|
|
1017
|
+
function loadModule(absPath, cache, compilerOptions, host) {
|
|
1008
1018
|
const cached = cache[absPath];
|
|
1009
1019
|
if (cached) {
|
|
1010
1020
|
return cached;
|
|
1011
1021
|
}
|
|
1012
|
-
const code =
|
|
1022
|
+
const code = host.readFile(absPath);
|
|
1023
|
+
if (code === undefined) {
|
|
1024
|
+
throw Error(`Could not read Val module file: '${absPath}'`);
|
|
1025
|
+
}
|
|
1013
1026
|
const transpiled = ts.transpileModule(code, {
|
|
1014
1027
|
compilerOptions: {
|
|
1015
1028
|
target: ts.ScriptTarget.ES2020,
|
|
@@ -1032,17 +1045,17 @@ function loadModule(absPath, cache, compilerOptions) {
|
|
|
1032
1045
|
return makeStub(spec);
|
|
1033
1046
|
}
|
|
1034
1047
|
if (spec.startsWith(".") || path__default.isAbsolute(spec)) {
|
|
1035
|
-
const resolved = resolveRelative(dirName, spec);
|
|
1048
|
+
const resolved = resolveRelative(dirName, spec, host);
|
|
1036
1049
|
if (!resolved) {
|
|
1037
1050
|
throw Error(`Could not resolve module '${spec}' from '${absPath}'`);
|
|
1038
1051
|
}
|
|
1039
|
-
return loadModule(resolved, cache, compilerOptions).exports;
|
|
1052
|
+
return loadModule(resolved, cache, compilerOptions, host).exports;
|
|
1040
1053
|
}
|
|
1041
1054
|
// Non-relative specifier: it might be a tsconfig path alias (e.g. "_/val.config")
|
|
1042
1055
|
// pointing at a local source file, or an actual node_modules package.
|
|
1043
|
-
const tsResolved = (_ts$resolveModuleName = ts.resolveModuleName(spec, absPath, compilerOptions,
|
|
1056
|
+
const tsResolved = (_ts$resolveModuleName = ts.resolveModuleName(spec, absPath, compilerOptions, host).resolvedModule) === null || _ts$resolveModuleName === void 0 ? void 0 : _ts$resolveModuleName.resolvedFileName;
|
|
1044
1057
|
if (tsResolved && !tsResolved.includes("/node_modules/") && !tsResolved.endsWith(".d.ts")) {
|
|
1045
|
-
return loadModule(tsResolved, cache, compilerOptions).exports;
|
|
1058
|
+
return loadModule(tsResolved, cache, compilerOptions, host).exports;
|
|
1046
1059
|
}
|
|
1047
1060
|
// Real node_modules package – use the real require so user modules share
|
|
1048
1061
|
// the same @valbuild/core instance as extractValModules.
|
|
@@ -1068,24 +1081,25 @@ function loadModule(absPath, cache, compilerOptions) {
|
|
|
1068
1081
|
script.runInContext(context);
|
|
1069
1082
|
return moduleObj;
|
|
1070
1083
|
}
|
|
1071
|
-
function resolveRelative(dirName, spec) {
|
|
1084
|
+
function resolveRelative(dirName, spec, host) {
|
|
1085
|
+
var _host$directoryExists;
|
|
1072
1086
|
const base = path__default.resolve(dirName, spec);
|
|
1073
1087
|
// Exact file (with extension)
|
|
1074
|
-
if (
|
|
1088
|
+
if (host.fileExists(base)) {
|
|
1075
1089
|
return base;
|
|
1076
1090
|
}
|
|
1077
1091
|
// Probe extensions (handles `./x.val` -> `./x.val.ts`)
|
|
1078
1092
|
for (const ext of RESOLVE_EXTENSIONS) {
|
|
1079
1093
|
const candidate = base + ext;
|
|
1080
|
-
if (
|
|
1094
|
+
if (host.fileExists(candidate)) {
|
|
1081
1095
|
return candidate;
|
|
1082
1096
|
}
|
|
1083
1097
|
}
|
|
1084
1098
|
// Directory index
|
|
1085
|
-
if (
|
|
1099
|
+
if (((_host$directoryExists = host.directoryExists) === null || _host$directoryExists === void 0 ? void 0 : _host$directoryExists.call(host, base)) ?? fs.existsSync(base)) {
|
|
1086
1100
|
for (const ext of RESOLVE_EXTENSIONS) {
|
|
1087
1101
|
const candidate = path__default.join(base, "index" + ext);
|
|
1088
|
-
if (
|
|
1102
|
+
if (host.fileExists(candidate)) {
|
|
1089
1103
|
return candidate;
|
|
1090
1104
|
}
|
|
1091
1105
|
}
|
|
@@ -1112,7 +1126,9 @@ async function createService(projectRoot, host = {
|
|
|
1112
1126
|
}) {
|
|
1113
1127
|
const compilerOptions = getCompilerOptions(projectRoot, host);
|
|
1114
1128
|
const sourceFileHandler = new ValSourceFileHandler(projectRoot, compilerOptions, host);
|
|
1115
|
-
|
|
1129
|
+
// Read val.modules (and everything it imports) through the same host, so an
|
|
1130
|
+
// embedder that overlays unsaved editor buffers evaluates what the user sees.
|
|
1131
|
+
const valModules = loadValModules(projectRoot, host);
|
|
1116
1132
|
const extracted = await extractValModules(valModules);
|
|
1117
1133
|
return new Service(projectRoot, sourceFileHandler, extracted);
|
|
1118
1134
|
}
|
|
@@ -7728,6 +7744,33 @@ async function downloadFileFromRemote(ref, filePath) {
|
|
|
7728
7744
|
// record-level fix expands into per-entry errors that should point at the
|
|
7729
7745
|
// individual entry (e.g. `?p="/public/val/logo.png"`) rather than the record.
|
|
7730
7746
|
|
|
7747
|
+
// Gallery entries are keyed by their file path. Remote galleries key uploaded
|
|
7748
|
+
// entries by a remote URL while keeping the file on disk at its local path, so
|
|
7749
|
+
// normalize a remote-URL key back to that local path for on-disk reads.
|
|
7750
|
+
function galleryKeyToLocalPath(key) {
|
|
7751
|
+
const res = Internal.remote.splitRemoteRef(key);
|
|
7752
|
+
return res.status === "success" ? `/${res.filePath}` : key;
|
|
7753
|
+
}
|
|
7754
|
+
|
|
7755
|
+
// Patch path of the record that holds a media entry. Media keys are file paths
|
|
7756
|
+
// that can contain dots, which sourceToPatchPath cannot round-trip, so strip
|
|
7757
|
+
// the (JSON-encoded) key segment off the entry source path and convert the
|
|
7758
|
+
// remaining parent path — whose ancestors are plain object keys / array
|
|
7759
|
+
// indices — instead.
|
|
7760
|
+
function parentPatchPathOfMediaEntry(sourcePath, entryKey) {
|
|
7761
|
+
const jsonKey = JSON.stringify(entryKey);
|
|
7762
|
+
let parent = sourcePath;
|
|
7763
|
+
if (parent.endsWith(jsonKey)) {
|
|
7764
|
+
parent = parent.slice(0, parent.length - jsonKey.length);
|
|
7765
|
+
}
|
|
7766
|
+
if (parent.endsWith(".")) {
|
|
7767
|
+
parent = parent.slice(0, -1);
|
|
7768
|
+
} else if (parent.endsWith(Internal.ModuleFilePathSep)) {
|
|
7769
|
+
parent = parent.slice(0, parent.length - Internal.ModuleFilePathSep.length);
|
|
7770
|
+
}
|
|
7771
|
+
return sourceToPatchPath(parent);
|
|
7772
|
+
}
|
|
7773
|
+
|
|
7731
7774
|
// TODO: find a better name? transformFixesToPatch?
|
|
7732
7775
|
async function createFixPatch(config, apply, sourcePath, validationError, remoteFiles, moduleSource, moduleSchema) {
|
|
7733
7776
|
const remainingErrors = [];
|
|
@@ -7905,6 +7948,61 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
7905
7948
|
path: sourceToPatchPath(sourcePath)
|
|
7906
7949
|
});
|
|
7907
7950
|
}
|
|
7951
|
+
} else if (fix === "images:upload-remote" || fix === "files:upload-remote") {
|
|
7952
|
+
// Gallery entry: the record is keyed by the file path, so uploading to
|
|
7953
|
+
// remote means renaming the key from the local path to the remote URL
|
|
7954
|
+
// (remove the old key, add the new one with the same metadata).
|
|
7955
|
+
const remoteFile = remoteFiles[sourcePath];
|
|
7956
|
+
if (!remoteFile) {
|
|
7957
|
+
remainingErrors.push({
|
|
7958
|
+
...validationError,
|
|
7959
|
+
message: "Cannot fix local to remote gallery entry: remote file was not uploaded",
|
|
7960
|
+
fixes: undefined
|
|
7961
|
+
});
|
|
7962
|
+
continue;
|
|
7963
|
+
}
|
|
7964
|
+
const metadata = remoteFile.metadata;
|
|
7965
|
+
if (!metadata) {
|
|
7966
|
+
remainingErrors.push({
|
|
7967
|
+
...validationError,
|
|
7968
|
+
message: "Failed to get metadata for remote gallery file",
|
|
7969
|
+
fixes: undefined
|
|
7970
|
+
});
|
|
7971
|
+
continue;
|
|
7972
|
+
}
|
|
7973
|
+
// The entry is keyed by its (local) file path, which can contain dots
|
|
7974
|
+
// (e.g. image.png). sourceToPatchPath can't round-trip such keys, so
|
|
7975
|
+
// build the patch path from the parent record path plus the key (which
|
|
7976
|
+
// validateMediaKey carries as the error value), like the UI does.
|
|
7977
|
+
const entryKey = validationError.value;
|
|
7978
|
+
if (typeof entryKey !== "string") {
|
|
7979
|
+
remainingErrors.push({
|
|
7980
|
+
...validationError,
|
|
7981
|
+
message: "Cannot rewrite remote gallery entry: missing entry key",
|
|
7982
|
+
fixes: undefined
|
|
7983
|
+
});
|
|
7984
|
+
continue;
|
|
7985
|
+
}
|
|
7986
|
+
const parentPath = parentPatchPathOfMediaEntry(sourcePath, entryKey);
|
|
7987
|
+
const removePath = parentPath.concat(entryKey);
|
|
7988
|
+
const addPath = parentPath.concat(remoteFile.ref);
|
|
7989
|
+
if (!isNotRoot(removePath) || !isNotRoot(addPath)) {
|
|
7990
|
+
remainingErrors.push({
|
|
7991
|
+
...validationError,
|
|
7992
|
+
message: "Cannot rewrite remote gallery entry at root path",
|
|
7993
|
+
fixes: undefined
|
|
7994
|
+
});
|
|
7995
|
+
continue;
|
|
7996
|
+
}
|
|
7997
|
+
patch.push({
|
|
7998
|
+
op: "remove",
|
|
7999
|
+
path: removePath
|
|
8000
|
+
});
|
|
8001
|
+
patch.push({
|
|
8002
|
+
op: "add",
|
|
8003
|
+
path: addPath,
|
|
8004
|
+
value: metadata
|
|
8005
|
+
});
|
|
7908
8006
|
} else if (fix === "image:download-remote" || fix === "file:download-remote") {
|
|
7909
8007
|
const v = getRemoteValueFromValidationError(validationError);
|
|
7910
8008
|
if (!v.success) {
|
|
@@ -7981,7 +8079,7 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
7981
8079
|
}
|
|
7982
8080
|
const gallerySource = moduleSource;
|
|
7983
8081
|
for (const [entryKey, storedEntry] of Object.entries(gallerySource)) {
|
|
7984
|
-
const filename = path__default.join(config.projectRoot, entryKey);
|
|
8082
|
+
const filename = path__default.join(config.projectRoot, galleryKeyToLocalPath(entryKey));
|
|
7985
8083
|
let buffer;
|
|
7986
8084
|
try {
|
|
7987
8085
|
buffer = fs.readFileSync(filename);
|
|
@@ -8209,6 +8307,294 @@ async function getFileMetadata(projectRoot, validationError) {
|
|
|
8209
8307
|
return extractFileMetadata(fileRef);
|
|
8210
8308
|
}
|
|
8211
8309
|
|
|
8310
|
+
function validateMetadata(actualMetadata, expectedMetadata) {
|
|
8311
|
+
const missingMetadata = [];
|
|
8312
|
+
const erroneousMetadata = {};
|
|
8313
|
+
if (typeof actualMetadata !== "object" || actualMetadata === null) {
|
|
8314
|
+
return {
|
|
8315
|
+
globalErrors: ["Metadata is wrong type: must be an object."]
|
|
8316
|
+
};
|
|
8317
|
+
}
|
|
8318
|
+
if (Array.isArray(actualMetadata)) {
|
|
8319
|
+
return {
|
|
8320
|
+
globalErrors: ["Metadata is wrong type: cannot be an array."]
|
|
8321
|
+
};
|
|
8322
|
+
}
|
|
8323
|
+
const recordMetadata = actualMetadata;
|
|
8324
|
+
const globalErrors = [];
|
|
8325
|
+
for (const anyKey in expectedMetadata) {
|
|
8326
|
+
if (typeof anyKey !== "string") {
|
|
8327
|
+
globalErrors.push(`Expected metadata has key '${anyKey}' that is not typeof 'string', but: '${typeof anyKey}'. This is most likely a Val bug.`);
|
|
8328
|
+
} else {
|
|
8329
|
+
if (anyKey in actualMetadata) {
|
|
8330
|
+
const key = anyKey;
|
|
8331
|
+
if (expectedMetadata[key] !== recordMetadata[key]) {
|
|
8332
|
+
erroneousMetadata[key] = `Expected metadata '${key}' to be ${JSON.stringify(expectedMetadata[key])}, but got ${JSON.stringify(recordMetadata[key])}.`;
|
|
8333
|
+
}
|
|
8334
|
+
} else {
|
|
8335
|
+
missingMetadata.push(anyKey);
|
|
8336
|
+
}
|
|
8337
|
+
}
|
|
8338
|
+
}
|
|
8339
|
+
if (globalErrors.length === 0 && missingMetadata.length === 0 && Object.keys(erroneousMetadata).length === 0) {
|
|
8340
|
+
return false;
|
|
8341
|
+
}
|
|
8342
|
+
return {
|
|
8343
|
+
missingMetadata,
|
|
8344
|
+
erroneousMetadata
|
|
8345
|
+
};
|
|
8346
|
+
}
|
|
8347
|
+
|
|
8348
|
+
/**
|
|
8349
|
+
* NOTE: this is intentionally NOT `SharedValConfig` from `@valbuild/shared`.
|
|
8350
|
+
* That schema requires `files.directory` to be exactly `/public/val`, whereas
|
|
8351
|
+
* this one accepts any path beneath `/public`. Unifying them would change which
|
|
8352
|
+
* configs are accepted, so they are kept separate on purpose.
|
|
8353
|
+
*/
|
|
8354
|
+
const ValConfigSchema = z$1.object({
|
|
8355
|
+
project: z$1.string().optional(),
|
|
8356
|
+
root: z$1.string().optional(),
|
|
8357
|
+
files: z$1.object({
|
|
8358
|
+
directory: z$1.string().refine(val => (val === "/public" || val.startsWith("/public/") && !val.endsWith("/")) &&
|
|
8359
|
+
// Reject path traversal so the directory cannot escape /public
|
|
8360
|
+
!val.split("/").some(segment => segment === "." || segment === ".."), {
|
|
8361
|
+
message: "files.directory must start with '/public', must not end with '/' and must not contain '.' or '..' segments"
|
|
8362
|
+
})
|
|
8363
|
+
}).optional(),
|
|
8364
|
+
gitCommit: z$1.string().optional(),
|
|
8365
|
+
gitBranch: z$1.string().optional(),
|
|
8366
|
+
defaultTheme: z$1.union([z$1.literal("light"), z$1.literal("dark")]).optional(),
|
|
8367
|
+
ai: z$1.object({
|
|
8368
|
+
commitMessages: z$1.object({
|
|
8369
|
+
disabled: z$1.boolean().optional()
|
|
8370
|
+
}).optional()
|
|
8371
|
+
}).optional()
|
|
8372
|
+
});
|
|
8373
|
+
|
|
8374
|
+
/**
|
|
8375
|
+
* `ENOENT` (nothing at this path) and `ENOTDIR` (a path segment is a file) both
|
|
8376
|
+
* mean the config file simply is not there.
|
|
8377
|
+
*/
|
|
8378
|
+
function isFileNotFound(err) {
|
|
8379
|
+
const code = err && typeof err === "object" && "code" in err ? err.code : undefined;
|
|
8380
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
8381
|
+
}
|
|
8382
|
+
|
|
8383
|
+
/**
|
|
8384
|
+
* Read and evaluate a `val.config.{ts,js}` file from disk.
|
|
8385
|
+
*
|
|
8386
|
+
* Returns `null` if the file does not exist, and throws if it exists but does
|
|
8387
|
+
* not export a valid `config` object.
|
|
8388
|
+
*
|
|
8389
|
+
* Used by the Val CLI and by `@valbuild/language-server`, so that an editor
|
|
8390
|
+
* resolves project config exactly the way the CLI does.
|
|
8391
|
+
*/
|
|
8392
|
+
async function evalValConfigFile(projectRoot, configFileName) {
|
|
8393
|
+
const valConfigPath = path__default.join(projectRoot, configFileName);
|
|
8394
|
+
let code;
|
|
8395
|
+
try {
|
|
8396
|
+
code = await fs$1.readFile(valConfigPath, "utf-8");
|
|
8397
|
+
} catch (err) {
|
|
8398
|
+
// A missing file means "this project does not use this config file name",
|
|
8399
|
+
// which callers handle by trying the next candidate. Anything else (no read
|
|
8400
|
+
// permission, an IO error, a directory where a file was expected) is a real
|
|
8401
|
+
// problem and must not be reported as "no config".
|
|
8402
|
+
if (isFileNotFound(err)) {
|
|
8403
|
+
return null;
|
|
8404
|
+
}
|
|
8405
|
+
throw Error(`Could not read Val config file at path: '${valConfigPath}': ${err instanceof Error ? err.message : String(err)}`);
|
|
8406
|
+
}
|
|
8407
|
+
if (!code.trim()) {
|
|
8408
|
+
return null;
|
|
8409
|
+
}
|
|
8410
|
+
const transpiled = ts.transpileModule(code, {
|
|
8411
|
+
compilerOptions: {
|
|
8412
|
+
target: ts.ScriptTarget.ES2020,
|
|
8413
|
+
module: ts.ModuleKind.CommonJS,
|
|
8414
|
+
esModuleInterop: true
|
|
8415
|
+
},
|
|
8416
|
+
fileName: valConfigPath
|
|
8417
|
+
});
|
|
8418
|
+
const projectRootRequire = createRequire(valConfigPath);
|
|
8419
|
+
const exportsObj = {};
|
|
8420
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8421
|
+
const sandbox = {
|
|
8422
|
+
exports: exportsObj,
|
|
8423
|
+
module: {
|
|
8424
|
+
exports: exportsObj
|
|
8425
|
+
},
|
|
8426
|
+
require: projectRootRequire,
|
|
8427
|
+
// NOTE: this is a security risk, but this code is running in the users own environment at the CLI level
|
|
8428
|
+
__filename: valConfigPath,
|
|
8429
|
+
__dirname: projectRoot,
|
|
8430
|
+
console,
|
|
8431
|
+
process
|
|
8432
|
+
};
|
|
8433
|
+
sandbox.global = sandbox;
|
|
8434
|
+
const context = vm.createContext(sandbox);
|
|
8435
|
+
const script = new vm.Script(transpiled.outputText, {
|
|
8436
|
+
filename: valConfigPath
|
|
8437
|
+
});
|
|
8438
|
+
script.runInContext(context);
|
|
8439
|
+
const valConfig = sandbox.module.exports.config;
|
|
8440
|
+
if (!valConfig) {
|
|
8441
|
+
throw Error(`Val config file at path: '${valConfigPath}' must export a config object. Got: ${valConfig}`);
|
|
8442
|
+
}
|
|
8443
|
+
const result = ValConfigSchema.safeParse(valConfig);
|
|
8444
|
+
if (!result.success) {
|
|
8445
|
+
throw Error(`Val config file at path: '${valConfigPath}' has invalid schema: ${result.error.message}`);
|
|
8446
|
+
}
|
|
8447
|
+
return result.data;
|
|
8448
|
+
}
|
|
8449
|
+
|
|
8450
|
+
/**
|
|
8451
|
+
* Resolve the project's Val config, trying the TypeScript file first and
|
|
8452
|
+
* falling back to JavaScript. Mirrors what the CLI has always done at each of
|
|
8453
|
+
* its call sites.
|
|
8454
|
+
*/
|
|
8455
|
+
async function findAndEvalValConfigFile(projectRoot) {
|
|
8456
|
+
return (await evalValConfigFile(projectRoot, "val.config.ts")) || (await evalValConfigFile(projectRoot, "val.config.js"));
|
|
8457
|
+
}
|
|
8458
|
+
|
|
8459
|
+
/**
|
|
8460
|
+
* The Val login device flow, as reusable primitives.
|
|
8461
|
+
*
|
|
8462
|
+
* The CLI wraps these with terminal output, and `@valbuild/language-server`
|
|
8463
|
+
* wraps them with LSP `window/showDocument` and progress reporting. Neither the
|
|
8464
|
+
* polling nor the token handling is duplicated between them.
|
|
8465
|
+
*
|
|
8466
|
+
* Deliberately no `console` output and no `process.exit`: every failure is a
|
|
8467
|
+
* thrown {@link ValLoginError} so an embedder can decide how to surface it.
|
|
8468
|
+
*/
|
|
8469
|
+
|
|
8470
|
+
const DEFAULT_LOGIN_HOST = "https://admin.val.build";
|
|
8471
|
+
function defaultHost() {
|
|
8472
|
+
return process.env.VAL_BUILD_URL || DEFAULT_LOGIN_HOST;
|
|
8473
|
+
}
|
|
8474
|
+
class ValLoginError extends Error {
|
|
8475
|
+
constructor(code, message, details) {
|
|
8476
|
+
super(message);
|
|
8477
|
+
this.code = code;
|
|
8478
|
+
this.details = details;
|
|
8479
|
+
this.name = "ValLoginError";
|
|
8480
|
+
}
|
|
8481
|
+
}
|
|
8482
|
+
|
|
8483
|
+
/** A login attempt that is waiting for the user to confirm in a browser. */
|
|
8484
|
+
|
|
8485
|
+
/**
|
|
8486
|
+
* Begin a login attempt. The caller is responsible for getting
|
|
8487
|
+
* {@link ValLoginSession.url} in front of the user.
|
|
8488
|
+
*/
|
|
8489
|
+
async function startValLogin(options = {}) {
|
|
8490
|
+
var _response$headers$get;
|
|
8491
|
+
const host = options.host ?? defaultHost();
|
|
8492
|
+
const response = await fetch(`${host}/api/login`, {
|
|
8493
|
+
method: "POST",
|
|
8494
|
+
headers: {
|
|
8495
|
+
"Content-Type": "application/json"
|
|
8496
|
+
}
|
|
8497
|
+
});
|
|
8498
|
+
if (response.status >= 500) {
|
|
8499
|
+
const text = await response.text().catch(() => "");
|
|
8500
|
+
throw new ValLoginError("server-error", "An error occurred on the server.", text ? `Server response: ${text} (status: ${response.status})` : `Status: ${response.status}`);
|
|
8501
|
+
}
|
|
8502
|
+
if (!((_response$headers$get = response.headers.get("content-type")) !== null && _response$headers$get !== void 0 && _response$headers$get.includes("application/json"))) {
|
|
8503
|
+
const text = await response.text().catch(() => "");
|
|
8504
|
+
throw new ValLoginError("unexpected-content-type", "Unexpected failure while trying to login (content type was not JSON).", text ? `Server response: ${text} (status: ${response.status})` : `Status: ${response.status}`);
|
|
8505
|
+
}
|
|
8506
|
+
const json = await response.json();
|
|
8507
|
+
const nonce = json === null || json === void 0 ? void 0 : json.nonce;
|
|
8508
|
+
const url = json === null || json === void 0 ? void 0 : json.url;
|
|
8509
|
+
if (typeof nonce !== "string" || typeof url !== "string") {
|
|
8510
|
+
throw new ValLoginError("unexpected-response", "Unexpected response from the server.", JSON.stringify(json));
|
|
8511
|
+
}
|
|
8512
|
+
return {
|
|
8513
|
+
nonce,
|
|
8514
|
+
url
|
|
8515
|
+
};
|
|
8516
|
+
}
|
|
8517
|
+
const DEFAULT_LOGIN_MAX_DURATION = 5 * 60 * 1000; // 5 minutes
|
|
8518
|
+
const DEFAULT_LOGIN_POLL_INTERVAL = 1000;
|
|
8519
|
+
|
|
8520
|
+
/**
|
|
8521
|
+
* Poll until the user confirms the login in their browser.
|
|
8522
|
+
*
|
|
8523
|
+
* Accepts an `AbortSignal` so an editor can cancel the flow when the user
|
|
8524
|
+
* dismisses the prompt, instead of leaving a poll loop running for 5 minutes.
|
|
8525
|
+
*/
|
|
8526
|
+
async function awaitValLoginConfirmation(nonce, options = {}) {
|
|
8527
|
+
const host = options.host ?? defaultHost();
|
|
8528
|
+
const maxDuration = options.maxDurationMs ?? DEFAULT_LOGIN_MAX_DURATION;
|
|
8529
|
+
const pollInterval = options.pollIntervalMs ?? DEFAULT_LOGIN_POLL_INTERVAL;
|
|
8530
|
+
const now = options.now ?? (() => Date.now());
|
|
8531
|
+
const start = now();
|
|
8532
|
+
while (now() - start < maxDuration) {
|
|
8533
|
+
var _options$signal, _options$signal2;
|
|
8534
|
+
if ((_options$signal = options.signal) !== null && _options$signal !== void 0 && _options$signal.aborted) {
|
|
8535
|
+
throw new ValLoginError("aborted", "Login was cancelled.");
|
|
8536
|
+
}
|
|
8537
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
8538
|
+
if ((_options$signal2 = options.signal) !== null && _options$signal2 !== void 0 && _options$signal2.aborted) {
|
|
8539
|
+
throw new ValLoginError("aborted", "Login was cancelled.");
|
|
8540
|
+
}
|
|
8541
|
+
const response = await fetch(`${host}/api/login?token=${nonce}&consume=true`, {
|
|
8542
|
+
method: "POST"
|
|
8543
|
+
});
|
|
8544
|
+
if (response.status >= 500) {
|
|
8545
|
+
throw new ValLoginError("server-error", "An error occurred on the server.", `Status: ${response.status}`);
|
|
8546
|
+
}
|
|
8547
|
+
if (response.status === 200) {
|
|
8548
|
+
var _json$profile;
|
|
8549
|
+
const json = await response.json().catch(() => null);
|
|
8550
|
+
if (!json) {
|
|
8551
|
+
// A 200 with an empty body is not a confirmation; keep polling rather
|
|
8552
|
+
// than failing the login, which is what the CLI has always done.
|
|
8553
|
+
continue;
|
|
8554
|
+
}
|
|
8555
|
+
if (typeof ((_json$profile = json.profile) === null || _json$profile === void 0 ? void 0 : _json$profile.email) === "string" && typeof json.pat === "string") {
|
|
8556
|
+
return json;
|
|
8557
|
+
}
|
|
8558
|
+
throw new ValLoginError("unexpected-response", "Unexpected response from the server.", JSON.stringify(json));
|
|
8559
|
+
}
|
|
8560
|
+
}
|
|
8561
|
+
throw new ValLoginError("timeout", "Login confirmation timed out.");
|
|
8562
|
+
}
|
|
8563
|
+
|
|
8564
|
+
/**
|
|
8565
|
+
* Write a completed login to the project's personal access token file.
|
|
8566
|
+
*
|
|
8567
|
+
* @returns the path the token was written to.
|
|
8568
|
+
*/
|
|
8569
|
+
function persistPersonalAccessToken(projectRoot, result) {
|
|
8570
|
+
const filePath = getPersonalAccessTokenPath(projectRoot);
|
|
8571
|
+
// NOTE: no restrictive mode on the directory. This is the project's shared
|
|
8572
|
+
// `.val`, which also holds the pending patches the dev server reads and
|
|
8573
|
+
// writes; making it owner-only would lock out anyone who did not run the
|
|
8574
|
+
// login. Only the token file itself is tightened.
|
|
8575
|
+
fs.mkdirSync(path__default.dirname(filePath), {
|
|
8576
|
+
recursive: true
|
|
8577
|
+
});
|
|
8578
|
+
// The token is a credential: keep it owner-only rather than at the mercy of
|
|
8579
|
+
// the process umask. `mode` only applies when the file is created, so an
|
|
8580
|
+
// already existing file is chmod-ed explicitly.
|
|
8581
|
+
fs.writeFileSync(filePath, JSON.stringify(result, null, 2), {
|
|
8582
|
+
mode: 0o600
|
|
8583
|
+
});
|
|
8584
|
+
try {
|
|
8585
|
+
fs.chmodSync(filePath, 0o600);
|
|
8586
|
+
} catch {
|
|
8587
|
+
// Not all platforms / filesystems support POSIX modes (Windows, some
|
|
8588
|
+
// network mounts). Failing to tighten them must not fail the login.
|
|
8589
|
+
}
|
|
8590
|
+
return filePath;
|
|
8591
|
+
}
|
|
8592
|
+
|
|
8593
|
+
/**
|
|
8594
|
+
* NOTE: `@valbuild/language-server` has a near-identical `modulePathMap.ts`.
|
|
8595
|
+
* Fix traversal bugs in both, or fold them together.
|
|
8596
|
+
*/
|
|
8597
|
+
|
|
8212
8598
|
function getModulePathRange(modulePath, modulePathMap,
|
|
8213
8599
|
// Which part of an object/record member to point at. For an object property
|
|
8214
8600
|
// the resolved node's own range is the *key* (property name); the *value*
|
|
@@ -8218,31 +8604,39 @@ function getModulePathRange(modulePath, modulePathMap,
|
|
|
8218
8604
|
// existing callers.
|
|
8219
8605
|
target = "key") {
|
|
8220
8606
|
var _range$children;
|
|
8221
|
-
// Handle
|
|
8222
|
-
if (
|
|
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.
|
|
8607
|
+
// Handle invalid module paths gracefully
|
|
8608
|
+
if (typeof modulePath !== "string") {
|
|
8234
8609
|
return undefined;
|
|
8235
8610
|
}
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
8611
|
+
let range;
|
|
8612
|
+
if (modulePath === "") {
|
|
8613
|
+
// The empty module path is the module root, stored under the "" key: the
|
|
8614
|
+
// source argument of `c.define`. This is what module-level diagnostics
|
|
8615
|
+
// (which have no module path) resolve to.
|
|
8616
|
+
range = modulePathMap[""];
|
|
8617
|
+
} else {
|
|
8618
|
+
let segments;
|
|
8619
|
+
try {
|
|
8620
|
+
// Quote-aware splitter that correctly handles keys containing dots
|
|
8621
|
+
// (e.g. file refs like `"/public/val/images/logo.png"`), unlike a naive
|
|
8622
|
+
// split on ".". Throws on malformed input (e.g. unbalanced quotes).
|
|
8623
|
+
segments = Internal.splitModulePath(modulePath);
|
|
8624
|
+
} catch {
|
|
8625
|
+
// Return undefined if the module path is malformed. This can happen when
|
|
8626
|
+
// there are upstream errors in schema serialization.
|
|
8627
|
+
return undefined;
|
|
8628
|
+
}
|
|
8629
|
+
if (segments.length === 0) {
|
|
8630
|
+
return undefined;
|
|
8631
|
+
}
|
|
8632
|
+
range = modulePathMap[segments[0]];
|
|
8633
|
+
for (const pathSegment of segments.slice(1)) {
|
|
8634
|
+
var _range;
|
|
8635
|
+
if (!range) {
|
|
8636
|
+
break;
|
|
8637
|
+
}
|
|
8638
|
+
range = (_range = range) === null || _range === void 0 || (_range = _range.children) === null || _range === void 0 ? void 0 : _range[pathSegment];
|
|
8244
8639
|
}
|
|
8245
|
-
range = (_range = range) === null || _range === void 0 || (_range = _range.children) === null || _range === void 0 ? void 0 : _range[pathSegment];
|
|
8246
8640
|
}
|
|
8247
8641
|
if (!range) {
|
|
8248
8642
|
return undefined;
|
|
@@ -8274,7 +8668,20 @@ function createModulePathMap(sourceFile) {
|
|
|
8274
8668
|
if (ts.isExportAssignment(child)) {
|
|
8275
8669
|
const contentNode = child.expression && ts.isCallExpression(child.expression) && child.expression.arguments[2];
|
|
8276
8670
|
if (contentNode) {
|
|
8277
|
-
|
|
8671
|
+
const map = traverse(contentNode, sourceFile) ?? {};
|
|
8672
|
+
// Always expose the module root under the "" key so diagnostics without
|
|
8673
|
+
// a module path still resolve to a location. `traverse` already emits ""
|
|
8674
|
+
// for primitive roots (with a tighter range), so keep that if present.
|
|
8675
|
+
// Object/record keys are never "" (see traverseObjectLiteral), so this
|
|
8676
|
+
// cannot collide with a real key.
|
|
8677
|
+
return {
|
|
8678
|
+
...map,
|
|
8679
|
+
"": map[""] ?? {
|
|
8680
|
+
children: {},
|
|
8681
|
+
start: sourceFile.getLineAndCharacterOfPosition(contentNode.getStart(sourceFile)),
|
|
8682
|
+
end: sourceFile.getLineAndCharacterOfPosition(contentNode.getEnd())
|
|
8683
|
+
}
|
|
8684
|
+
};
|
|
8278
8685
|
}
|
|
8279
8686
|
}
|
|
8280
8687
|
}
|
|
@@ -8467,4 +8874,4 @@ function readCapturedReport(snapshotDir) {
|
|
|
8467
8874
|
return JSON.parse(fs.readFileSync(reportPath, "utf-8"));
|
|
8468
8875
|
}
|
|
8469
8876
|
|
|
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 };
|
|
8877
|
+
export { DEFAULT_LOGIN_HOST, DEFAULT_LOGIN_MAX_DURATION, DEFAULT_LOGIN_POLL_INTERVAL, Service, ValFSHost, ValLoginError, ValModuleLoader, ValOpsFS, ValOpsHttp, ValSourceFileHandler, analyzeValModule, awaitValLoginConfirmation, checkRemoteRef, compareWithCapturedReport, createFixPatch, createModulePathMap, createService, createValApiRouter, createValServer, decodeJwt, downloadFileFromRemote, encodeJwt, evalValConfigFile, extractFileMetadata, extractImageMetadata, findAndEvalValConfigFile, formatPatchSourceError, formatSyntaxErrorTree, getCachedRemoteFileDir, getCachedRemoteFilePath, getCompilerOptions, getExpire, getFileExt, getModulePathRange, getPersonalAccessTokenPath, getSettings, getValidationErrorFileRef, hasRemoteFileSchema, loadValModules, parsePersonalAccessTokenFile, patchSourceFile, persistPersonalAccessToken, readCapturedReport, replaySnapshot, safeReadGit, startValLogin, uploadRemoteFile, validateMetadata };
|
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"./package.json": "./package.json"
|
|
17
17
|
},
|
|
18
18
|
"types": "dist/valbuild-server.cjs.d.ts",
|
|
19
|
-
"version": "0.
|
|
19
|
+
"version": "0.99.0",
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@prettier/sync": "^0.6.1",
|
|
22
22
|
"@types/jest": "^30.0.0"
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"typescript": "^5.9.3",
|
|
30
30
|
"zod": "^4.3.5",
|
|
31
31
|
"zod-validation-error": "^5.0.0",
|
|
32
|
-
"@valbuild/core": "0.
|
|
33
|
-
"@valbuild/shared": "0.
|
|
34
|
-
"@valbuild/ui": "0.
|
|
32
|
+
"@valbuild/core": "0.99.0",
|
|
33
|
+
"@valbuild/shared": "0.99.0",
|
|
34
|
+
"@valbuild/ui": "0.99.0"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
37
37
|
"node": ">=18.17.0"
|