@valbuild/cli 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/cli/dist/valbuild-cli-cli.cjs.dev.js +222 -222
- package/cli/dist/valbuild-cli-cli.cjs.prod.js +222 -222
- package/cli/dist/valbuild-cli-cli.esm.js +221 -219
- package/package.json +5 -4
- package/src/__fixtures__/basic/content/basic-gallery-remote-existing.val.ts +20 -0
- package/src/__fixtures__/basic/content/basic-gallery-remote.val.ts +19 -0
- package/src/__fixtures__/basic/public/val/images-remote/image.png +0 -0
- package/src/__fixtures__/basic/public/val/images-remote2/image.png +0 -0
- package/src/__fixtures__/basic/val.config.ts +3 -1
- package/src/__fixtures__/basic/val.modules.ts +2 -0
- package/src/cli.ts +11 -5
- package/src/connect.ts +2 -4
- package/src/debug/context.ts +2 -4
- package/src/listUnusedFiles.ts +2 -4
- package/src/login.ts +29 -94
- package/src/lsp.ts +19 -0
- package/src/runValidation.test.ts +101 -0
- package/src/runValidation.ts +200 -73
- package/src/utils/evalValConfigFile.ts +6 -100
- package/src/validate.ts +2 -4
|
@@ -2,16 +2,13 @@ import meow from 'meow';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import pc from 'picocolors';
|
|
5
|
-
import fs from 'fs/promises';
|
|
5
|
+
import fs$1 from 'fs/promises';
|
|
6
6
|
import { glob } from 'fast-glob';
|
|
7
7
|
import { Internal, FILE_REF_PROP, DEFAULT_VAL_REMOTE_HOST, DEFAULT_CONTENT_HOST, VAL_EXTENSION } from '@valbuild/core';
|
|
8
|
-
import { createService, createFixPatch, getPersonalAccessTokenPath, parsePersonalAccessTokenFile, getModulePathRange, createModulePathMap, getSettings, uploadRemoteFile, loadValModules, safeReadGit, ValOpsHttp, ValOpsFS, formatPatchSourceError } from '@valbuild/server';
|
|
9
|
-
import vm from 'node:vm';
|
|
10
|
-
import ts from 'typescript';
|
|
11
|
-
import z from 'zod';
|
|
12
|
-
import { createRequire } from 'node:module';
|
|
8
|
+
import { createService, createFixPatch, getPersonalAccessTokenPath, parsePersonalAccessTokenFile, getModulePathRange, createModulePathMap, findAndEvalValConfigFile, getSettings, uploadRemoteFile, startValLogin, awaitValLoginConfirmation, persistPersonalAccessToken, ValLoginError, loadValModules, safeReadGit, ValOpsHttp, ValOpsFS, formatPatchSourceError } from '@valbuild/server';
|
|
13
9
|
import { resolveSchemaSourceFixes } from '@valbuild/shared/internal';
|
|
14
|
-
import
|
|
10
|
+
import ts from 'typescript';
|
|
11
|
+
import fs from 'fs';
|
|
15
12
|
import JSZip from 'jszip';
|
|
16
13
|
import readline from 'readline';
|
|
17
14
|
|
|
@@ -30,76 +27,6 @@ function info(message, opts = {}) {
|
|
|
30
27
|
console.log(chalk.blue("️ℹ️ : ") + message);
|
|
31
28
|
}
|
|
32
29
|
|
|
33
|
-
const ValConfigSchema = z.object({
|
|
34
|
-
project: z.string().optional(),
|
|
35
|
-
root: z.string().optional(),
|
|
36
|
-
files: z.object({
|
|
37
|
-
directory: z.string().refine(val => (val === "/public" || val.startsWith("/public/") && !val.endsWith("/")) &&
|
|
38
|
-
// Reject path traversal so the directory cannot escape /public
|
|
39
|
-
!val.split("/").some(segment => segment === "." || segment === ".."), {
|
|
40
|
-
message: "files.directory must start with '/public', must not end with '/' and must not contain '.' or '..' segments"
|
|
41
|
-
})
|
|
42
|
-
}).optional(),
|
|
43
|
-
gitCommit: z.string().optional(),
|
|
44
|
-
gitBranch: z.string().optional(),
|
|
45
|
-
defaultTheme: z.union([z.literal("light"), z.literal("dark")]).optional(),
|
|
46
|
-
ai: z.object({
|
|
47
|
-
commitMessages: z.object({
|
|
48
|
-
disabled: z.boolean().optional()
|
|
49
|
-
}).optional()
|
|
50
|
-
}).optional()
|
|
51
|
-
});
|
|
52
|
-
async function evalValConfigFile(projectRoot, configFileName) {
|
|
53
|
-
const valConfigPath = path.join(projectRoot, configFileName);
|
|
54
|
-
let code = null;
|
|
55
|
-
try {
|
|
56
|
-
code = await fs.readFile(valConfigPath, "utf-8");
|
|
57
|
-
} catch {
|
|
58
|
-
//
|
|
59
|
-
}
|
|
60
|
-
if (!code) {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
const transpiled = ts.transpileModule(code, {
|
|
64
|
-
compilerOptions: {
|
|
65
|
-
target: ts.ScriptTarget.ES2020,
|
|
66
|
-
module: ts.ModuleKind.CommonJS,
|
|
67
|
-
esModuleInterop: true
|
|
68
|
-
},
|
|
69
|
-
fileName: valConfigPath
|
|
70
|
-
});
|
|
71
|
-
const projectRootRequire = createRequire(valConfigPath);
|
|
72
|
-
const exportsObj = {};
|
|
73
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
74
|
-
const sandbox = {
|
|
75
|
-
exports: exportsObj,
|
|
76
|
-
module: {
|
|
77
|
-
exports: exportsObj
|
|
78
|
-
},
|
|
79
|
-
require: projectRootRequire,
|
|
80
|
-
// NOTE: this is a security risk, but this code is running in the users own environment at the CLI level
|
|
81
|
-
__filename: valConfigPath,
|
|
82
|
-
__dirname: projectRoot,
|
|
83
|
-
console,
|
|
84
|
-
process
|
|
85
|
-
};
|
|
86
|
-
sandbox.global = sandbox;
|
|
87
|
-
const context = vm.createContext(sandbox);
|
|
88
|
-
const script = new vm.Script(transpiled.outputText, {
|
|
89
|
-
filename: valConfigPath
|
|
90
|
-
});
|
|
91
|
-
script.runInContext(context);
|
|
92
|
-
const valConfig = sandbox.module.exports.config;
|
|
93
|
-
if (!valConfig) {
|
|
94
|
-
throw Error(`Val config file at path: '${valConfigPath}' must export a config object. Got: ${valConfig}`);
|
|
95
|
-
}
|
|
96
|
-
const result = ValConfigSchema.safeParse(valConfig);
|
|
97
|
-
if (!result.success) {
|
|
98
|
-
throw Error(`Val config file at path: '${valConfigPath}' has invalid schema: ${result.error.message}`);
|
|
99
|
-
}
|
|
100
|
-
return result.data;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
30
|
function getFileExt(filePath) {
|
|
104
31
|
// NOTE: We do not import the path module. This code is copied in different projects. We want the same implementation and which means that this might running in browser where path is not available).
|
|
105
32
|
return filePath.split(".").pop() || "";
|
|
@@ -141,32 +68,13 @@ async function handleFileMetadata(ctx) {
|
|
|
141
68
|
shouldApplyPatch: true
|
|
142
69
|
};
|
|
143
70
|
}
|
|
144
|
-
async function handleRemoteFileUpload(ctx) {
|
|
145
|
-
var _resolvedRemoteFileAt;
|
|
146
|
-
if (!ctx.fix) {
|
|
147
|
-
return {
|
|
148
|
-
success: false,
|
|
149
|
-
errorMessage: `Remote file ${ctx.sourcePath} needs to be uploaded (use --fix to upload)`
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
const [, modulePath] = Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
|
|
153
|
-
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
154
|
-
return {
|
|
155
|
-
success: false,
|
|
156
|
-
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
const resolvedRemoteFileAtSourcePath = Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
|
|
160
71
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
const filePath = path.join(ctx.projectRoot, fileRefProp);
|
|
72
|
+
// Shared upload core used by both the single-field (handleRemoteFileUpload)
|
|
73
|
+
// and gallery (handleRemoteGalleryFileUpload) handlers. The two differ only in
|
|
74
|
+
// how they derive the local file ref, metadata and serialized image/file
|
|
75
|
+
// schema; everything from auth through upload is identical.
|
|
76
|
+
async function uploadRemoteFileCore(ctx, fileRef, metadata, schema) {
|
|
77
|
+
const filePath = path.join(ctx.projectRoot, fileRef);
|
|
170
78
|
if (!ctx.fs.fileExists(filePath)) {
|
|
171
79
|
return {
|
|
172
80
|
success: false,
|
|
@@ -206,21 +114,6 @@ async function handleRemoteFileUpload(ctx) {
|
|
|
206
114
|
}]
|
|
207
115
|
};
|
|
208
116
|
}
|
|
209
|
-
if (!resolvedRemoteFileAtSourcePath.schema) {
|
|
210
|
-
return {
|
|
211
|
-
success: false,
|
|
212
|
-
errorMessage: `Cannot upload remote file: schema not found for ${ctx.sourcePath}`
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
const actualRemoteFileSource = resolvedRemoteFileAtSourcePath.source;
|
|
216
|
-
const fileSourceMetadata = Internal.isFile(actualRemoteFileSource) ? actualRemoteFileSource.metadata : undefined;
|
|
217
|
-
const resolveRemoteFileSchema = resolvedRemoteFileAtSourcePath.schema;
|
|
218
|
-
if (!resolveRemoteFileSchema) {
|
|
219
|
-
return {
|
|
220
|
-
success: false,
|
|
221
|
-
errorMessage: `Could not resolve schema for remote file: ${ctx.sourcePath}`
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
117
|
const projectName = ctx.project;
|
|
225
118
|
let publicProjectId = ctx.publicProjectId;
|
|
226
119
|
let remoteFileBuckets = ctx.remoteFileBuckets;
|
|
@@ -256,12 +149,6 @@ async function handleRemoteFileUpload(ctx) {
|
|
|
256
149
|
errorMessage: `Could not get project. Check that your val.config has the 'project' field set, or set it using the VAL_PROJECT environment variable`
|
|
257
150
|
};
|
|
258
151
|
}
|
|
259
|
-
if (resolveRemoteFileSchema.type !== "image" && resolveRemoteFileSchema.type !== "file") {
|
|
260
|
-
return {
|
|
261
|
-
success: false,
|
|
262
|
-
errorMessage: `The schema is the remote is neither image nor file: ${ctx.sourcePath}`
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
152
|
remoteFilesCounter += 1;
|
|
266
153
|
const bucket = remoteFileBuckets[remoteFilesCounter % remoteFileBuckets.length];
|
|
267
154
|
if (!bucket) {
|
|
@@ -287,8 +174,6 @@ async function handleRemoteFileUpload(ctx) {
|
|
|
287
174
|
const fileHash = Internal.remote.getFileHash(fileBuffer);
|
|
288
175
|
const coreVersion = Internal.VERSION.core || "unknown";
|
|
289
176
|
const fileExt = getFileExt(filePath);
|
|
290
|
-
const schema = resolveRemoteFileSchema;
|
|
291
|
-
const metadata = fileSourceMetadata;
|
|
292
177
|
const ref = Internal.remote.createRemoteRef(ctx.remote.remoteHost, {
|
|
293
178
|
publicProjectId,
|
|
294
179
|
coreVersion,
|
|
@@ -308,7 +193,7 @@ async function handleRemoteFileUpload(ctx) {
|
|
|
308
193
|
}
|
|
309
194
|
ctx.remoteFiles[ctx.sourcePath] = {
|
|
310
195
|
ref,
|
|
311
|
-
metadata
|
|
196
|
+
metadata
|
|
312
197
|
};
|
|
313
198
|
return {
|
|
314
199
|
success: true,
|
|
@@ -325,6 +210,131 @@ async function handleRemoteFileUpload(ctx) {
|
|
|
325
210
|
}]
|
|
326
211
|
};
|
|
327
212
|
}
|
|
213
|
+
async function handleRemoteFileUpload(ctx) {
|
|
214
|
+
var _resolvedRemoteFileAt;
|
|
215
|
+
if (!ctx.fix) {
|
|
216
|
+
return {
|
|
217
|
+
success: false,
|
|
218
|
+
// No sourcePath in the message: the reported location already points at it.
|
|
219
|
+
errorMessage: `Remote file needs to be uploaded (use --fix to upload)`
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const [, modulePath] = Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
|
|
223
|
+
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
224
|
+
return {
|
|
225
|
+
success: false,
|
|
226
|
+
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
const resolvedRemoteFileAtSourcePath = Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
|
|
230
|
+
|
|
231
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
232
|
+
const fileRefProp = (_resolvedRemoteFileAt = resolvedRemoteFileAtSourcePath.source) === null || _resolvedRemoteFileAt === void 0 ? void 0 : _resolvedRemoteFileAt[FILE_REF_PROP];
|
|
233
|
+
if (!fileRefProp) {
|
|
234
|
+
return {
|
|
235
|
+
success: false,
|
|
236
|
+
errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const resolveRemoteFileSchema = resolvedRemoteFileAtSourcePath.schema;
|
|
240
|
+
if (!resolveRemoteFileSchema) {
|
|
241
|
+
return {
|
|
242
|
+
success: false,
|
|
243
|
+
errorMessage: `Cannot upload remote file: schema not found for ${ctx.sourcePath}`
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
if (resolveRemoteFileSchema.type !== "image" && resolveRemoteFileSchema.type !== "file") {
|
|
247
|
+
return {
|
|
248
|
+
success: false,
|
|
249
|
+
errorMessage: `The schema is the remote is neither image nor file: ${ctx.sourcePath}`
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const actualRemoteFileSource = resolvedRemoteFileAtSourcePath.source;
|
|
253
|
+
const fileSourceMetadata = Internal.isFile(actualRemoteFileSource) ? actualRemoteFileSource.metadata : undefined;
|
|
254
|
+
return uploadRemoteFileCore(ctx, fileRefProp, fileSourceMetadata, resolveRemoteFileSchema);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Gallery (s.images({ remote: true }) / s.files({ remote: true })) upload.
|
|
258
|
+
// Unlike a single image/file field, a gallery entry is keyed by its local file
|
|
259
|
+
// path and the value is bare metadata (no FileSource), so we derive the file
|
|
260
|
+
// ref from the key and synthesize the image/file schema from the record's
|
|
261
|
+
// media options.
|
|
262
|
+
async function handleRemoteGalleryFileUpload(ctx) {
|
|
263
|
+
var _ctx$validationError$;
|
|
264
|
+
if (!ctx.fix) {
|
|
265
|
+
return {
|
|
266
|
+
success: false,
|
|
267
|
+
// No sourcePath in the message: the reported location already points at it.
|
|
268
|
+
errorMessage: `Remote file needs to be uploaded (use --fix to upload)`
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const fix = (_ctx$validationError$ = ctx.validationError.fixes) === null || _ctx$validationError$ === void 0 ? void 0 : _ctx$validationError$[0];
|
|
272
|
+
const mediaType = fix === "files:upload-remote" ? "file" : "image";
|
|
273
|
+
|
|
274
|
+
// The gallery entry is keyed by its local file path; validateMediaKey carries
|
|
275
|
+
// that key as the error value.
|
|
276
|
+
const fileRef = ctx.validationError.value;
|
|
277
|
+
if (typeof fileRef !== "string") {
|
|
278
|
+
return {
|
|
279
|
+
success: false,
|
|
280
|
+
errorMessage: `Expected a local file path for gallery entry at ${ctx.sourcePath}`
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const [, modulePath] = Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
|
|
284
|
+
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
285
|
+
return {
|
|
286
|
+
success: false,
|
|
287
|
+
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
const resolved = Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
|
|
291
|
+
const entrySource = resolved.source;
|
|
292
|
+
const metadata = entrySource && typeof entrySource === "object" && !Array.isArray(entrySource) ? entrySource : undefined;
|
|
293
|
+
|
|
294
|
+
// The gallery item schema is an ObjectSchema, not an image/file schema, so
|
|
295
|
+
// synthesize the serialized image/file schema (matching how single fields
|
|
296
|
+
// serialize) for the remote ref's validation hash.
|
|
297
|
+
//
|
|
298
|
+
// `accept`/`directory` come from the RECORD that holds the entry, resolved
|
|
299
|
+
// from the entry's PARENT path - not from the module root, which is only the
|
|
300
|
+
// record when the gallery is the whole module. A nested gallery
|
|
301
|
+
// (s.object({ gallery: s.images(...) })) would otherwise synthesize a schema
|
|
302
|
+
// with no options and bake a validation hash into the remote ref that can
|
|
303
|
+
// never validate, so a mismatch fails fast instead of uploading.
|
|
304
|
+
const [, parentModulePath] = Internal.splitModuleFilePathAndModulePath(Internal.parentOfSourcePath(ctx.sourcePath));
|
|
305
|
+
const recordSchema = Internal.resolvePath(parentModulePath, ctx.valModule.source, ctx.valModule.schema).schema;
|
|
306
|
+
if ((recordSchema === null || recordSchema === void 0 ? void 0 : recordSchema.type) !== "record") {
|
|
307
|
+
return {
|
|
308
|
+
success: false,
|
|
309
|
+
errorMessage: `Expected a gallery record at the parent of ${ctx.sourcePath}, got ${(recordSchema === null || recordSchema === void 0 ? void 0 : recordSchema.type) ?? "nothing"}`
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const {
|
|
313
|
+
accept,
|
|
314
|
+
directory
|
|
315
|
+
} = recordSchema;
|
|
316
|
+
const schema = mediaType === "image" ? {
|
|
317
|
+
type: "image",
|
|
318
|
+
opt: false,
|
|
319
|
+
options: {
|
|
320
|
+
...(accept ? {
|
|
321
|
+
accept
|
|
322
|
+
} : {}),
|
|
323
|
+
...(directory ? {
|
|
324
|
+
directory
|
|
325
|
+
} : {})
|
|
326
|
+
}
|
|
327
|
+
} : {
|
|
328
|
+
type: "file",
|
|
329
|
+
opt: false,
|
|
330
|
+
options: {
|
|
331
|
+
...(accept ? {
|
|
332
|
+
accept
|
|
333
|
+
} : {})
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
return uploadRemoteFileCore(ctx, fileRef, metadata, schema);
|
|
337
|
+
}
|
|
328
338
|
async function handleRemoteFileDownload(ctx) {
|
|
329
339
|
if (ctx.fix) {
|
|
330
340
|
return {
|
|
@@ -382,6 +392,17 @@ async function handleUniqueFolderCheck(ctx) {
|
|
|
382
392
|
success: true
|
|
383
393
|
};
|
|
384
394
|
}
|
|
395
|
+
|
|
396
|
+
// Maps a gallery key to its on-disk local path. Remote galleries key uploaded
|
|
397
|
+
// entries by a remote URL while keeping the file on disk; everything else is
|
|
398
|
+
// already a local path and is returned unchanged.
|
|
399
|
+
function remoteKeyToLocalPath(key) {
|
|
400
|
+
const remoteRefRes = Internal.remote.splitRemoteRef(key);
|
|
401
|
+
if (remoteRefRes.status === "success") {
|
|
402
|
+
return `/${remoteRefRes.filePath}`;
|
|
403
|
+
}
|
|
404
|
+
return key;
|
|
405
|
+
}
|
|
385
406
|
async function handleCheckAllFiles(ctx) {
|
|
386
407
|
const value = ctx.validationError.value;
|
|
387
408
|
if (!value || typeof value.directory !== "string") {
|
|
@@ -400,7 +421,10 @@ async function handleCheckAllFiles(ctx) {
|
|
|
400
421
|
errorMessage: `Could not get source for ${ctx.sourcePath}`
|
|
401
422
|
};
|
|
402
423
|
}
|
|
403
|
-
|
|
424
|
+
// Gallery entries are keyed by their file path. Remote galleries key uploaded
|
|
425
|
+
// entries by a remote URL, but the file is kept on disk at its local path, so
|
|
426
|
+
// normalize remote-URL keys back to that local path for the on-disk checks.
|
|
427
|
+
const trackedFiles = new Set(Object.keys(source).map(remoteKeyToLocalPath));
|
|
404
428
|
|
|
405
429
|
// Check that all tracked files exist on disk
|
|
406
430
|
const missingTrackedFiles = [...trackedFiles].filter(f => {
|
|
@@ -455,6 +479,8 @@ const currentFixHandlers = {
|
|
|
455
479
|
"file:add-metadata": handleFileMetadata,
|
|
456
480
|
"image:upload-remote": handleRemoteFileUpload,
|
|
457
481
|
"file:upload-remote": handleRemoteFileUpload,
|
|
482
|
+
"images:upload-remote": handleRemoteGalleryFileUpload,
|
|
483
|
+
"files:upload-remote": handleRemoteGalleryFileUpload,
|
|
458
484
|
"image:download-remote": handleRemoteFileDownload,
|
|
459
485
|
"file:download-remote": handleRemoteFileDownload,
|
|
460
486
|
"image:check-remote": handleRemoteFileCheck,
|
|
@@ -477,15 +503,15 @@ function createDefaultValFSHost() {
|
|
|
477
503
|
return {
|
|
478
504
|
...ts.sys,
|
|
479
505
|
writeFile: (fileName, data, encoding) => {
|
|
480
|
-
fs
|
|
506
|
+
fs.mkdirSync(path.dirname(fileName), {
|
|
481
507
|
recursive: true
|
|
482
508
|
});
|
|
483
|
-
fs
|
|
509
|
+
fs.writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
|
|
484
510
|
},
|
|
485
|
-
rmFile: fs
|
|
511
|
+
rmFile: fs.rmSync,
|
|
486
512
|
readBuffer: fileName => {
|
|
487
513
|
try {
|
|
488
|
-
return fs
|
|
514
|
+
return fs.readFileSync(fileName);
|
|
489
515
|
} catch {
|
|
490
516
|
return undefined;
|
|
491
517
|
}
|
|
@@ -826,7 +852,7 @@ function getCachedFile(moduleFilePath, projectRoot, cache) {
|
|
|
826
852
|
const filePath = path.join(projectRoot, moduleFilePath);
|
|
827
853
|
let fileContent;
|
|
828
854
|
try {
|
|
829
|
-
fileContent = fs
|
|
855
|
+
fileContent = fs.readFileSync(filePath, "utf-8");
|
|
830
856
|
} catch {
|
|
831
857
|
cache.set(moduleFilePath, null);
|
|
832
858
|
return null;
|
|
@@ -857,7 +883,7 @@ async function validate({
|
|
|
857
883
|
// errors found. Re-reads config and val files each call so it always reflects
|
|
858
884
|
// the latest state on disk (used both for one-shot and watch mode).
|
|
859
885
|
async function runOnce() {
|
|
860
|
-
const valConfigFile =
|
|
886
|
+
const valConfigFile = await findAndEvalValConfigFile(projectRoot);
|
|
861
887
|
const resolvedValConfigFile = valConfigFile ? {
|
|
862
888
|
...valConfigFile,
|
|
863
889
|
project: process.env.VAL_PROJECT || valConfigFile.project
|
|
@@ -1037,11 +1063,11 @@ async function validate({
|
|
|
1037
1063
|
if (prettier) {
|
|
1038
1064
|
for (const file of fixedFiles) {
|
|
1039
1065
|
const filePath = path.join(projectRoot, file);
|
|
1040
|
-
const fileContent = await fs.readFile(filePath, "utf-8");
|
|
1066
|
+
const fileContent = await fs$1.readFile(filePath, "utf-8");
|
|
1041
1067
|
const formattedContent = await prettier.format(fileContent, {
|
|
1042
1068
|
filepath: filePath
|
|
1043
1069
|
});
|
|
1044
|
-
await fs.writeFile(filePath, formattedContent);
|
|
1070
|
+
await fs$1.writeFile(filePath, formattedContent);
|
|
1045
1071
|
}
|
|
1046
1072
|
}
|
|
1047
1073
|
|
|
@@ -1162,7 +1188,7 @@ async function listUnusedFiles({
|
|
|
1162
1188
|
}) {
|
|
1163
1189
|
var _valConfigFile$files;
|
|
1164
1190
|
const projectRoot = root ? path.resolve(root) : process.cwd();
|
|
1165
|
-
const valConfigFile =
|
|
1191
|
+
const valConfigFile = await findAndEvalValConfigFile(projectRoot);
|
|
1166
1192
|
// Strip the leading "/" so it is relative to the project root (e.g. "public/val").
|
|
1167
1193
|
const managedDir = ((valConfigFile === null || valConfigFile === void 0 || (_valConfigFile$files = valConfigFile.files) === null || _valConfigFile$files === void 0 ? void 0 : _valConfigFile$files.directory) ?? "/public/val").replace(/^\//, "");
|
|
1168
1194
|
const service = await createService(projectRoot);
|
|
@@ -1251,7 +1277,7 @@ const getVersions = () => {
|
|
|
1251
1277
|
};
|
|
1252
1278
|
};
|
|
1253
1279
|
|
|
1254
|
-
const host
|
|
1280
|
+
const host = process.env.VAL_BUILD_URL || "https://admin.val.build";
|
|
1255
1281
|
async function connect(options) {
|
|
1256
1282
|
const {
|
|
1257
1283
|
root
|
|
@@ -1269,11 +1295,11 @@ async function connect(options) {
|
|
|
1269
1295
|
if (maybeGitOwnerAndRepo) {
|
|
1270
1296
|
params.set("github_repo", [maybeGitOwnerAndRepo.owner, maybeGitOwnerAndRepo.repo].join("/"));
|
|
1271
1297
|
}
|
|
1272
|
-
const url = `${host
|
|
1298
|
+
const url = `${host}/connect?${params.toString()}`;
|
|
1273
1299
|
console.log(pc.dim(`\nFollow the instructions in your browser to complete the setup:\n${pc.cyan(url)}\n`));
|
|
1274
1300
|
}
|
|
1275
1301
|
async function tryGetProject(projectRoot) {
|
|
1276
|
-
const valConfigFile =
|
|
1302
|
+
const valConfigFile = await findAndEvalValConfigFile(projectRoot);
|
|
1277
1303
|
if (valConfigFile && valConfigFile.project) {
|
|
1278
1304
|
const parts = valConfigFile.project.split("/");
|
|
1279
1305
|
if (parts.length === 2) {
|
|
@@ -1326,8 +1352,8 @@ function tryGetGitConfig(root) {
|
|
|
1326
1352
|
let lastDir = null;
|
|
1327
1353
|
while (currentDir !== lastDir) {
|
|
1328
1354
|
const gitConfigPath = path.join(currentDir, ".git", "config");
|
|
1329
|
-
if (fs
|
|
1330
|
-
return fs
|
|
1355
|
+
if (fs.existsSync(gitConfigPath)) {
|
|
1356
|
+
return fs.readFileSync(gitConfigPath, "utf-8");
|
|
1331
1357
|
}
|
|
1332
1358
|
lastDir = currentDir;
|
|
1333
1359
|
currentDir = path.dirname(currentDir);
|
|
@@ -1338,83 +1364,54 @@ function tryGetGitConfig(root) {
|
|
|
1338
1364
|
return null;
|
|
1339
1365
|
}
|
|
1340
1366
|
|
|
1341
|
-
|
|
1367
|
+
/**
|
|
1368
|
+
* Terminal wrapper around the login device flow in `@valbuild/server`.
|
|
1369
|
+
* The flow itself (polling, token persistence) is shared with
|
|
1370
|
+
* `@valbuild/language-server`.
|
|
1371
|
+
*/
|
|
1342
1372
|
async function login(options) {
|
|
1343
1373
|
try {
|
|
1344
|
-
var _response$headers$get;
|
|
1345
1374
|
console.log(pc.cyan("\nStarting login process...\n"));
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
headers: {
|
|
1351
|
-
"Content-Type": "application/json"
|
|
1352
|
-
}
|
|
1353
|
-
});
|
|
1354
|
-
let token;
|
|
1355
|
-
let url;
|
|
1356
|
-
if (!((_response$headers$get = response.headers.get("content-type")) !== null && _response$headers$get !== void 0 && _response$headers$get.includes("application/json"))) {
|
|
1357
|
-
const text = await response.text();
|
|
1358
|
-
console.error(pc.red("Unexpected failure while trying to login (content type was not JSON). "), text ? `Server response: ${text} (status: ${response.status})` : `Status: ${response.status}`);
|
|
1359
|
-
process.exit(1);
|
|
1360
|
-
}
|
|
1361
|
-
const json = await response.json();
|
|
1362
|
-
if (json) {
|
|
1363
|
-
token = json.nonce;
|
|
1364
|
-
url = json.url;
|
|
1365
|
-
}
|
|
1366
|
-
if (!token || !url) {
|
|
1367
|
-
console.error(pc.red("Unexpected response from the server."), json);
|
|
1368
|
-
process.exit(1);
|
|
1369
|
-
}
|
|
1375
|
+
const {
|
|
1376
|
+
nonce,
|
|
1377
|
+
url
|
|
1378
|
+
} = await startValLogin();
|
|
1370
1379
|
console.log(pc.green("Open the following URL in your browser to log in:"));
|
|
1371
1380
|
console.log(pc.underline(pc.blue(url)));
|
|
1372
1381
|
console.log(pc.dim("\nWaiting for login confirmation...\n"));
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
// Step 3: Save the token
|
|
1378
|
-
const filePath = getPersonalAccessTokenPath(options.root || process.cwd());
|
|
1379
|
-
saveToken(result, filePath);
|
|
1382
|
+
const result = await awaitValLoginConfirmation(nonce);
|
|
1383
|
+
const filePath = persistPersonalAccessToken(options.root || process.cwd(), result);
|
|
1384
|
+
console.log(pc.green(`Token for ${pc.cyan(result.profile.email)} saved to ${pc.cyan(filePath)}`));
|
|
1380
1385
|
} catch (error) {
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
const MAX_DURATION = 5 * 60 * 1000; // 5 minutes
|
|
1386
|
-
async function pollForConfirmation(token) {
|
|
1387
|
-
const start = Date.now();
|
|
1388
|
-
while (Date.now() - start < MAX_DURATION) {
|
|
1389
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1390
|
-
const response = await fetch(`${host}/api/login?token=${token}&consume=true`, {
|
|
1391
|
-
method: "POST"
|
|
1392
|
-
});
|
|
1393
|
-
if (response.status === 500) {
|
|
1394
|
-
console.error(pc.red("An error occurred on the server."));
|
|
1386
|
+
if (error instanceof ValLoginError) {
|
|
1387
|
+
console.error(pc.red(error.message), error.details ?? "");
|
|
1395
1388
|
process.exit(1);
|
|
1396
1389
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
if (json) {
|
|
1400
|
-
if (typeof json.profile.email === "string" && typeof json.pat === "string") {
|
|
1401
|
-
return json;
|
|
1402
|
-
} else {
|
|
1403
|
-
console.error(pc.red("Unexpected response from the server."));
|
|
1404
|
-
process.exit(1);
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1390
|
+
console.error(pc.red("An error occurred during the login process. Check your internet connection. Details:"), error instanceof Error ? error.message : JSON.stringify(error, null, 2));
|
|
1391
|
+
process.exit(1);
|
|
1408
1392
|
}
|
|
1409
|
-
console.error(pc.red("Login confirmation timed out."));
|
|
1410
|
-
process.exit(1);
|
|
1411
1393
|
}
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1394
|
+
|
|
1395
|
+
/**
|
|
1396
|
+
* Start the Val language server.
|
|
1397
|
+
*
|
|
1398
|
+
* Editors normally resolve `@valbuild/language-server` from the project's
|
|
1399
|
+
* `node_modules` and launch its `val-language-server` binary directly. This
|
|
1400
|
+
* subcommand exists so the server is discoverable from the CLI and easy to
|
|
1401
|
+
* start by hand when debugging an editor integration:
|
|
1402
|
+
*
|
|
1403
|
+
* npx val lsp --stdio
|
|
1404
|
+
*
|
|
1405
|
+
* The import is dynamic so that `vscode-languageserver` is not loaded on every
|
|
1406
|
+
* `val` invocation.
|
|
1407
|
+
*/
|
|
1408
|
+
async function lsp() {
|
|
1409
|
+
const {
|
|
1410
|
+
main
|
|
1411
|
+
} = await import('@valbuild/language-server');
|
|
1412
|
+
// Transport (`--stdio`, `--node-ipc`, `--socket=`) is read from argv by the
|
|
1413
|
+
// server itself, so the flags pass straight through.
|
|
1414
|
+
main();
|
|
1418
1415
|
}
|
|
1419
1416
|
|
|
1420
1417
|
/**
|
|
@@ -1431,10 +1428,10 @@ class DebugContextError extends Error {}
|
|
|
1431
1428
|
async function createDebugContext(options) {
|
|
1432
1429
|
var _config$files;
|
|
1433
1430
|
const projectRoot = options.root ? path.resolve(options.root) : process.cwd();
|
|
1434
|
-
if (!fs
|
|
1431
|
+
if (!fs.existsSync(projectRoot)) {
|
|
1435
1432
|
throw new DebugContextError(`Project root does not exist: ${projectRoot}`);
|
|
1436
1433
|
}
|
|
1437
|
-
const config =
|
|
1434
|
+
const config = await findAndEvalValConfigFile(projectRoot);
|
|
1438
1435
|
if (!config) {
|
|
1439
1436
|
throw new DebugContextError(`Could not find val.config.ts nor val.config.js in: ${projectRoot}`);
|
|
1440
1437
|
}
|
|
@@ -1501,8 +1498,8 @@ async function createDebugContext(options) {
|
|
|
1501
1498
|
*/
|
|
1502
1499
|
function readAuth(projectRoot) {
|
|
1503
1500
|
const patFile = getPersonalAccessTokenPath(projectRoot);
|
|
1504
|
-
if (fs
|
|
1505
|
-
const contents = fs
|
|
1501
|
+
if (fs.existsSync(patFile)) {
|
|
1502
|
+
const contents = fs.readFileSync(patFile, "utf-8");
|
|
1506
1503
|
const parsed = parsePersonalAccessTokenFile(contents);
|
|
1507
1504
|
if (parsed.success) {
|
|
1508
1505
|
return {
|
|
@@ -2049,7 +2046,7 @@ function readProjectValVersions(projectRoot) {
|
|
|
2049
2046
|
function readLocalFile(projectRoot, projectRelativePath) {
|
|
2050
2047
|
const absPath = path.join(projectRoot, projectRelativePath);
|
|
2051
2048
|
try {
|
|
2052
|
-
return fs
|
|
2049
|
+
return fs.readFileSync(absPath, "utf-8");
|
|
2053
2050
|
} catch {
|
|
2054
2051
|
return null;
|
|
2055
2052
|
}
|
|
@@ -2191,10 +2188,10 @@ async function writeZip(outPath, entries) {
|
|
|
2191
2188
|
type: "nodebuffer",
|
|
2192
2189
|
compression: "DEFLATE"
|
|
2193
2190
|
});
|
|
2194
|
-
fs
|
|
2191
|
+
fs.mkdirSync(path.dirname(outPath), {
|
|
2195
2192
|
recursive: true
|
|
2196
2193
|
});
|
|
2197
|
-
fs
|
|
2194
|
+
fs.writeFileSync(outPath, buffer);
|
|
2198
2195
|
}
|
|
2199
2196
|
function timestamp() {
|
|
2200
2197
|
return new Date().toISOString().replace(/[:.]/g, "-").replace(/Z$/, "");
|
|
@@ -2331,6 +2328,7 @@ async function main() {
|
|
|
2331
2328
|
files
|
|
2332
2329
|
connect
|
|
2333
2330
|
versions
|
|
2331
|
+
lsp
|
|
2334
2332
|
debug
|
|
2335
2333
|
delete-unappliable-patches
|
|
2336
2334
|
|
|
@@ -2360,6 +2358,13 @@ async function main() {
|
|
|
2360
2358
|
Options:
|
|
2361
2359
|
--root [root], -r [root] Set project root directory (default process.cwd())
|
|
2362
2360
|
|
|
2361
|
+
Command: lsp
|
|
2362
|
+
Description: start the Val language server (used by editor integrations)
|
|
2363
|
+
Options:
|
|
2364
|
+
--stdio Communicate over stdin/stdout
|
|
2365
|
+
--node-ipc Communicate over Node IPC
|
|
2366
|
+
--socket=[port] Communicate over a socket
|
|
2367
|
+
|
|
2363
2368
|
Command: debug
|
|
2364
2369
|
Description: Create a self-contained snapshot (the pending patches plus the modules they
|
|
2365
2370
|
touch or reference) that Val developers can replay to reproduce validation and save
|
|
@@ -2389,11 +2394,6 @@ async function main() {
|
|
|
2389
2394
|
--verbose List every pending patch, not just the failing ones
|
|
2390
2395
|
`, {
|
|
2391
2396
|
flags: {
|
|
2392
|
-
port: {
|
|
2393
|
-
type: "number",
|
|
2394
|
-
alias: "p",
|
|
2395
|
-
default: 4123
|
|
2396
|
-
},
|
|
2397
2397
|
root: {
|
|
2398
2398
|
type: "string",
|
|
2399
2399
|
alias: "r"
|
|
@@ -2455,6 +2455,8 @@ async function main() {
|
|
|
2455
2455
|
});
|
|
2456
2456
|
case "versions":
|
|
2457
2457
|
return versions();
|
|
2458
|
+
case "lsp":
|
|
2459
|
+
return lsp();
|
|
2458
2460
|
case "debug":
|
|
2459
2461
|
return debug({
|
|
2460
2462
|
root: flags.root,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valbuild/cli",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.99.0",
|
|
5
5
|
"description": "Val CLI tools",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -27,10 +27,11 @@
|
|
|
27
27
|
"open": "^9.1.0",
|
|
28
28
|
"picocolors": "^1.1.1",
|
|
29
29
|
"zod": "^4.3.5",
|
|
30
|
-
"@valbuild/core": "0.
|
|
30
|
+
"@valbuild/core": "0.99.0",
|
|
31
31
|
"@valbuild/eslint-plugin": "0.93.0",
|
|
32
|
-
"@valbuild/server": "0.
|
|
33
|
-
"@valbuild/
|
|
32
|
+
"@valbuild/language-server": "0.99.0",
|
|
33
|
+
"@valbuild/server": "0.99.0",
|
|
34
|
+
"@valbuild/shared": "0.99.0"
|
|
34
35
|
},
|
|
35
36
|
"peerDependencies": {
|
|
36
37
|
"prettier": "*",
|