@valbuild/cli 0.102.0 → 0.103.1
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 +26 -566
- package/cli/dist/valbuild-cli-cli.cjs.prod.js +26 -566
- package/cli/dist/valbuild-cli-cli.esm.js +25 -565
- package/package.json +5 -5
- package/src/__fixtures__/basic/content/basic-image-from-galleries.val.ts +2 -2
- package/src/__fixtures__/basic/content/basic-image-from-gallery.val.ts +1 -1
- package/src/__fixtures__/basic/content/basic-image.val.ts +3 -5
- package/src/__fixtures__/debug-snapshot/.val/patches/11111111-1111-4111-8111-111111111111/patch.json +6 -9
- package/src/__fixtures__/debug-snapshot/.val/patches/22222222-2222-4222-8222-222222222222/patch.json +5 -10
- package/src/__fixtures__/debug-snapshot/.val/patches/33333333-3333-4333-8333-333333333333/patch.json +16 -0
- package/src/__fixtures__/debug-snapshot/.val/patches/patches.log +4 -0
- package/src/debug/snapshot.ts +23 -18
- package/src/debug.test.ts +38 -17
- package/src/listUnusedFiles.ts +15 -22
- package/src/runValidation.test.ts +5 -5
- package/src/runValidation.ts +25 -771
- package/src/utils/sourcePathToFileLocation.ts +1 -1
- package/src/__fixtures__/debug-snapshot/.val/patches/head/patch.json +0 -20
- package/src/utils/getFileExt.ts +0 -4
|
@@ -4,11 +4,11 @@ import path from 'path';
|
|
|
4
4
|
import pc from 'picocolors';
|
|
5
5
|
import fs$1 from 'fs/promises';
|
|
6
6
|
import { glob } from 'fast-glob';
|
|
7
|
-
import { Internal,
|
|
8
|
-
import { createService,
|
|
7
|
+
import { Internal, DEFAULT_VAL_REMOTE_HOST, DEFAULT_CONTENT_HOST } from '@valbuild/core';
|
|
8
|
+
import { createService, fixHandlers, createFixPatch, getModulePathRange, findJsonEntryFilePath, createJsonEntryPathMap, createModulePathMap, findAndEvalValConfigFile, getSettings, uploadRemoteFile, createDefaultValFSHost, startValLogin, awaitValLoginConfirmation, persistPersonalAccessToken, ValLoginError, loadValModules, safeReadGit, ValOpsHttp, ValOpsFS, getPersonalAccessTokenPath, parsePersonalAccessTokenFile, formatPatchSourceError } from '@valbuild/server';
|
|
9
9
|
import { resolveSchemaSourceFixes } from '@valbuild/shared/internal';
|
|
10
|
-
import ts from 'typescript';
|
|
11
10
|
import fs from 'fs';
|
|
11
|
+
import ts from 'typescript';
|
|
12
12
|
import JSZip from 'jszip';
|
|
13
13
|
import readline from 'readline';
|
|
14
14
|
|
|
@@ -27,543 +27,6 @@ function info(message, opts = {}) {
|
|
|
27
27
|
console.log(chalk.blue("️ℹ️ : ") + message);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function getFileExt(filePath) {
|
|
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).
|
|
32
|
-
return filePath.split(".").pop() || "";
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const textEncoder = new TextEncoder();
|
|
36
|
-
|
|
37
|
-
// Types for handler system
|
|
38
|
-
|
|
39
|
-
// Handler functions
|
|
40
|
-
async function handleFileMetadata(ctx) {
|
|
41
|
-
var _fileSource$source;
|
|
42
|
-
const [, modulePath] = Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
|
|
43
|
-
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
44
|
-
return {
|
|
45
|
-
success: false,
|
|
46
|
-
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
const fileSource = Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
|
|
50
|
-
|
|
51
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
|
-
const fileRefProp = (_fileSource$source = fileSource.source) === null || _fileSource$source === void 0 ? void 0 : _fileSource$source[FILE_REF_PROP];
|
|
53
|
-
if (!fileRefProp) {
|
|
54
|
-
return {
|
|
55
|
-
success: false,
|
|
56
|
-
errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
const filePath = path.join(ctx.projectRoot, fileRefProp);
|
|
60
|
-
if (!ctx.fs.fileExists(filePath)) {
|
|
61
|
-
return {
|
|
62
|
-
success: false,
|
|
63
|
-
errorMessage: `File ${filePath} does not exist`
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
return {
|
|
67
|
-
success: true,
|
|
68
|
-
shouldApplyPatch: true
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
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);
|
|
78
|
-
if (!ctx.fs.fileExists(filePath)) {
|
|
79
|
-
return {
|
|
80
|
-
success: false,
|
|
81
|
-
errorMessage: `File ${filePath} does not exist`
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
const patFile = getPersonalAccessTokenPath(ctx.projectRoot);
|
|
85
|
-
if (!ctx.fs.fileExists(patFile)) {
|
|
86
|
-
return {
|
|
87
|
-
success: false,
|
|
88
|
-
errorMessage: `File: ${path.join(ctx.projectRoot, ctx.file)} has remote images that are not uploaded and you are not logged in.\n\nFix this error by logging in:\n\t"npx val login"\n`
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
const patFileContent = ctx.fs.readFile(patFile);
|
|
92
|
-
if (patFileContent === undefined) {
|
|
93
|
-
return {
|
|
94
|
-
success: false,
|
|
95
|
-
errorMessage: `Could not read personal access token file at ${patFile}`
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
const parsedPatFile = parsePersonalAccessTokenFile(patFileContent);
|
|
99
|
-
if (!parsedPatFile.success) {
|
|
100
|
-
return {
|
|
101
|
-
success: false,
|
|
102
|
-
errorMessage: `Error parsing personal access token file: ${parsedPatFile.error}. You need to login again.`
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
const {
|
|
106
|
-
pat
|
|
107
|
-
} = parsedPatFile.data;
|
|
108
|
-
if (ctx.remoteFiles[ctx.sourcePath]) {
|
|
109
|
-
return {
|
|
110
|
-
success: true,
|
|
111
|
-
events: [{
|
|
112
|
-
type: "remote-already-uploaded",
|
|
113
|
-
filePath
|
|
114
|
-
}]
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
const projectName = ctx.project;
|
|
118
|
-
let publicProjectId = ctx.publicProjectId;
|
|
119
|
-
let remoteFileBuckets = ctx.remoteFileBuckets;
|
|
120
|
-
let remoteFilesCounter = ctx.remoteFilesCounter;
|
|
121
|
-
if (!publicProjectId || !remoteFileBuckets) {
|
|
122
|
-
if (!projectName) {
|
|
123
|
-
return {
|
|
124
|
-
success: false,
|
|
125
|
-
errorMessage: "Project name not found. Add project name to val.config or set the VAL_PROJECT environment variable"
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
const settingsRes = await ctx.remote.getSettings(projectName, {
|
|
129
|
-
pat
|
|
130
|
-
});
|
|
131
|
-
if (!settingsRes.success) {
|
|
132
|
-
return {
|
|
133
|
-
success: false,
|
|
134
|
-
errorMessage: `Could not get public project id: ${settingsRes.message}.`
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
publicProjectId = settingsRes.data.publicProjectId;
|
|
138
|
-
remoteFileBuckets = settingsRes.data.remoteFileBuckets.map(b => b.bucket);
|
|
139
|
-
}
|
|
140
|
-
if (!publicProjectId) {
|
|
141
|
-
return {
|
|
142
|
-
success: false,
|
|
143
|
-
errorMessage: "Could not get public project id"
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
if (!projectName) {
|
|
147
|
-
return {
|
|
148
|
-
success: false,
|
|
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`
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
remoteFilesCounter += 1;
|
|
153
|
-
const bucket = remoteFileBuckets[remoteFilesCounter % remoteFileBuckets.length];
|
|
154
|
-
if (!bucket) {
|
|
155
|
-
return {
|
|
156
|
-
success: false,
|
|
157
|
-
errorMessage: `Internal error: could not allocate a bucket for the remote file located at ${ctx.sourcePath}`
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
const fileBuffer = ctx.fs.readBuffer(filePath);
|
|
161
|
-
if (fileBuffer === undefined) {
|
|
162
|
-
return {
|
|
163
|
-
success: false,
|
|
164
|
-
errorMessage: `Error reading file: ${filePath}`
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
const relativeFilePath = path.relative(ctx.projectRoot, filePath).split(path.sep).join("/");
|
|
168
|
-
if (!relativeFilePath.startsWith("public/")) {
|
|
169
|
-
return {
|
|
170
|
-
success: false,
|
|
171
|
-
errorMessage: `File path must be within the public/ directory (e.g. public/path/to/file.txt). Got: ${relativeFilePath}`
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
const fileHash = Internal.remote.getFileHash(fileBuffer);
|
|
175
|
-
const coreVersion = Internal.VERSION.core || "unknown";
|
|
176
|
-
const fileExt = getFileExt(filePath);
|
|
177
|
-
const ref = Internal.remote.createRemoteRef(ctx.remote.remoteHost, {
|
|
178
|
-
publicProjectId,
|
|
179
|
-
coreVersion,
|
|
180
|
-
bucket,
|
|
181
|
-
validationHash: Internal.remote.getValidationHash(coreVersion, schema, fileExt, metadata, fileHash, textEncoder),
|
|
182
|
-
fileHash,
|
|
183
|
-
filePath: relativeFilePath
|
|
184
|
-
});
|
|
185
|
-
const remoteFileUpload = await ctx.remote.uploadFile(projectName, bucket, fileHash, fileExt, fileBuffer, {
|
|
186
|
-
pat
|
|
187
|
-
});
|
|
188
|
-
if (!remoteFileUpload.success) {
|
|
189
|
-
return {
|
|
190
|
-
success: false,
|
|
191
|
-
errorMessage: `Could not upload remote file: '${ref}'. Error: ${remoteFileUpload.error}`
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
ctx.remoteFiles[ctx.sourcePath] = {
|
|
195
|
-
ref,
|
|
196
|
-
metadata
|
|
197
|
-
};
|
|
198
|
-
return {
|
|
199
|
-
success: true,
|
|
200
|
-
shouldApplyPatch: true,
|
|
201
|
-
publicProjectId,
|
|
202
|
-
remoteFileBuckets,
|
|
203
|
-
remoteFilesCounter,
|
|
204
|
-
events: [{
|
|
205
|
-
type: "remote-uploading",
|
|
206
|
-
ref
|
|
207
|
-
}, {
|
|
208
|
-
type: "remote-uploaded",
|
|
209
|
-
ref
|
|
210
|
-
}]
|
|
211
|
-
};
|
|
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
|
-
}
|
|
338
|
-
async function handleRemoteFileDownload(ctx) {
|
|
339
|
-
if (ctx.fix) {
|
|
340
|
-
return {
|
|
341
|
-
success: true,
|
|
342
|
-
shouldApplyPatch: true,
|
|
343
|
-
events: [{
|
|
344
|
-
type: "remote-downloading",
|
|
345
|
-
sourcePath: ctx.sourcePath
|
|
346
|
-
}]
|
|
347
|
-
};
|
|
348
|
-
} else {
|
|
349
|
-
return {
|
|
350
|
-
success: false,
|
|
351
|
-
errorMessage: `Remote file ${ctx.sourcePath} needs to be downloaded (use --fix to download)`
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
async function handleRemoteFileCheck() {
|
|
356
|
-
// Skip - no action needed
|
|
357
|
-
return {
|
|
358
|
-
success: true,
|
|
359
|
-
shouldApplyPatch: true
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
async function handleUniqueFolderCheck(ctx) {
|
|
363
|
-
const value = ctx.validationError.value;
|
|
364
|
-
if (!value || typeof value.directory !== "string") {
|
|
365
|
-
return {
|
|
366
|
-
success: false,
|
|
367
|
-
errorMessage: `Unexpected value in unique folder check for ${ctx.sourcePath}`
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
const {
|
|
371
|
-
directory
|
|
372
|
-
} = value;
|
|
373
|
-
const conflicts = [];
|
|
374
|
-
for (const file of ctx.valFiles) {
|
|
375
|
-
const otherModuleFilePath = `/${file}`;
|
|
376
|
-
if (otherModuleFilePath === ctx.moduleFilePath) continue;
|
|
377
|
-
const otherModule = await ctx.service.get(otherModuleFilePath, "", {
|
|
378
|
-
validate: false
|
|
379
|
-
});
|
|
380
|
-
const schema = otherModule.schema;
|
|
381
|
-
if ((schema === null || schema === void 0 ? void 0 : schema.type) === "record" && schema.directory === directory && schema.mediaType) {
|
|
382
|
-
conflicts.push(otherModuleFilePath);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
if (conflicts.length > 0) {
|
|
386
|
-
return {
|
|
387
|
-
success: false,
|
|
388
|
-
errorMessage: `Gallery directory '${directory}' in ${ctx.moduleFilePath} is also used by: ${conflicts.join(", ")}. Each gallery must use a unique directory.`
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
return {
|
|
392
|
-
success: true
|
|
393
|
-
};
|
|
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
|
-
}
|
|
406
|
-
async function handleCheckAllFiles(ctx) {
|
|
407
|
-
const value = ctx.validationError.value;
|
|
408
|
-
if (!value || typeof value.directory !== "string") {
|
|
409
|
-
return {
|
|
410
|
-
success: false,
|
|
411
|
-
errorMessage: `Unexpected value in check-all-files for ${ctx.sourcePath}`
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
const {
|
|
415
|
-
directory
|
|
416
|
-
} = value;
|
|
417
|
-
const source = ctx.valModule.source;
|
|
418
|
-
if (!source || typeof source !== "object" || Array.isArray(source)) {
|
|
419
|
-
return {
|
|
420
|
-
success: false,
|
|
421
|
-
errorMessage: `Could not get source for ${ctx.sourcePath}`
|
|
422
|
-
};
|
|
423
|
-
}
|
|
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));
|
|
428
|
-
|
|
429
|
-
// Check that all tracked files exist on disk
|
|
430
|
-
const missingTrackedFiles = [...trackedFiles].filter(f => {
|
|
431
|
-
return !ctx.fs.fileExists(path.join(ctx.projectRoot, f));
|
|
432
|
-
});
|
|
433
|
-
if (missingTrackedFiles.length > 0) {
|
|
434
|
-
if (!ctx.fix) {
|
|
435
|
-
return {
|
|
436
|
-
success: false,
|
|
437
|
-
errorMessage: `Gallery in ${ctx.moduleFilePath} has tracked files that do not exist on disk: ${missingTrackedFiles.join(", ")}. Add the files or remove them from the gallery.`
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
// fix: true — let createFixPatch remove the missing entries
|
|
441
|
-
return {
|
|
442
|
-
success: true,
|
|
443
|
-
shouldApplyPatch: true
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
const dirPath = path.join(ctx.projectRoot, directory);
|
|
447
|
-
const filesInDir = [];
|
|
448
|
-
try {
|
|
449
|
-
const entries = ctx.fs.readDirectory(dirPath, undefined, undefined, ["**/*"]);
|
|
450
|
-
for (const entry of entries) {
|
|
451
|
-
const relPath = "/" + path.relative(ctx.projectRoot, entry).split(path.sep).join("/");
|
|
452
|
-
filesInDir.push(relPath);
|
|
453
|
-
}
|
|
454
|
-
} catch {
|
|
455
|
-
// directory doesn't exist — no untracked files possible
|
|
456
|
-
}
|
|
457
|
-
const untrackedFiles = filesInDir.filter(f => !trackedFiles.has(f));
|
|
458
|
-
if (untrackedFiles.length > 0) {
|
|
459
|
-
return {
|
|
460
|
-
success: false,
|
|
461
|
-
errorMessage: `Gallery in ${ctx.moduleFilePath} has files not tracked: ${untrackedFiles.join(", ")}. Add these files to the gallery or remove them from the directory.`
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// All files accounted for — trigger metadata verification via createFixPatch
|
|
466
|
-
return {
|
|
467
|
-
success: true,
|
|
468
|
-
shouldApplyPatch: true
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
async function handleJsonValuesExtractEntry(ctx) {
|
|
472
|
-
const [, modulePath] = Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
|
|
473
|
-
const parts = Internal.splitModulePath(modulePath);
|
|
474
|
-
// Root-only by contract (see findNestedJsonValuesRecords): the entry is a
|
|
475
|
-
// direct child of the module's root record/router, so the path is one segment.
|
|
476
|
-
if (parts.length !== 1) {
|
|
477
|
-
return {
|
|
478
|
-
success: false,
|
|
479
|
-
errorMessage: `Cannot extract .jsonValues() entry at ${ctx.sourcePath}: expected a root record entry`
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
const entryKey = parts[0];
|
|
483
|
-
const source = ctx.valModule.source;
|
|
484
|
-
if (source === null || typeof source !== "object" || Array.isArray(source)) {
|
|
485
|
-
return {
|
|
486
|
-
success: false,
|
|
487
|
-
errorMessage: `Could not get source for ${ctx.moduleFilePath}`
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
const content = source[entryKey];
|
|
491
|
-
if (content === undefined) {
|
|
492
|
-
return {
|
|
493
|
-
success: false,
|
|
494
|
-
errorMessage: `Could not find .jsonValues() entry '${entryKey}' in ${ctx.moduleFilePath}`
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
if (!ctx.fix) {
|
|
498
|
-
return {
|
|
499
|
-
success: true,
|
|
500
|
-
fixableErrorMessage: ctx.validationError.message
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
try {
|
|
504
|
-
extractJsonValuesEntry(ctx.moduleFilePath, ctx.projectRoot, entryKey, content, ctx.service.sourceFileHandler);
|
|
505
|
-
} catch (err) {
|
|
506
|
-
return {
|
|
507
|
-
success: false,
|
|
508
|
-
errorMessage: err instanceof Error ? err.message : String(err)
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
return {
|
|
512
|
-
success: true,
|
|
513
|
-
appliedFix: true
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// Fix handler registry. `keyof:check-keys` and `router:check-route` are
|
|
518
|
-
// resolved upfront by the shared resolveSchemaSourceFixes — they never reach
|
|
519
|
-
// this registry, so they're excluded from the key set.
|
|
520
|
-
const currentFixHandlers = {
|
|
521
|
-
"image:check-metadata": handleFileMetadata,
|
|
522
|
-
"image:add-metadata": handleFileMetadata,
|
|
523
|
-
"file:check-metadata": handleFileMetadata,
|
|
524
|
-
"file:add-metadata": handleFileMetadata,
|
|
525
|
-
"image:upload-remote": handleRemoteFileUpload,
|
|
526
|
-
"file:upload-remote": handleRemoteFileUpload,
|
|
527
|
-
"images:upload-remote": handleRemoteGalleryFileUpload,
|
|
528
|
-
"files:upload-remote": handleRemoteGalleryFileUpload,
|
|
529
|
-
"image:download-remote": handleRemoteFileDownload,
|
|
530
|
-
"file:download-remote": handleRemoteFileDownload,
|
|
531
|
-
"image:check-remote": handleRemoteFileCheck,
|
|
532
|
-
"images:check-remote": handleRemoteFileCheck,
|
|
533
|
-
"file:check-remote": handleRemoteFileCheck,
|
|
534
|
-
"files:check-remote": handleRemoteFileCheck,
|
|
535
|
-
"images:check-unique-folder": handleUniqueFolderCheck,
|
|
536
|
-
"files:check-unique-folder": handleUniqueFolderCheck,
|
|
537
|
-
"images:check-all-files": handleCheckAllFiles,
|
|
538
|
-
"files:check-all-files": handleCheckAllFiles,
|
|
539
|
-
"jsonValues:extract-entry": handleJsonValuesExtractEntry
|
|
540
|
-
};
|
|
541
|
-
const deprecatedFixHandlers = {
|
|
542
|
-
"image:replace-metadata": handleFileMetadata
|
|
543
|
-
};
|
|
544
|
-
const fixHandlers = {
|
|
545
|
-
...deprecatedFixHandlers,
|
|
546
|
-
...currentFixHandlers
|
|
547
|
-
};
|
|
548
|
-
function createDefaultValFSHost() {
|
|
549
|
-
return {
|
|
550
|
-
...ts.sys,
|
|
551
|
-
writeFile: (fileName, data, encoding) => {
|
|
552
|
-
fs.mkdirSync(path.dirname(fileName), {
|
|
553
|
-
recursive: true
|
|
554
|
-
});
|
|
555
|
-
fs.writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
|
|
556
|
-
},
|
|
557
|
-
rmFile: fs.rmSync,
|
|
558
|
-
readBuffer: fileName => {
|
|
559
|
-
try {
|
|
560
|
-
return fs.readFileSync(fileName);
|
|
561
|
-
} catch {
|
|
562
|
-
return undefined;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
30
|
async function* runValidation({
|
|
568
31
|
root,
|
|
569
32
|
fix,
|
|
@@ -1381,7 +844,7 @@ async function listUnusedFiles({
|
|
|
1381
844
|
for (const error of validationError) {
|
|
1382
845
|
const value = error.value;
|
|
1383
846
|
if (isFileRef(value)) {
|
|
1384
|
-
const absoluteFilePathUsedByVal = path.join(projectRoot, ...value
|
|
847
|
+
const absoluteFilePathUsedByVal = path.join(projectRoot, ...value.path.split("/"));
|
|
1385
848
|
filesUsedByVal.push(absoluteFilePathUsedByVal);
|
|
1386
849
|
}
|
|
1387
850
|
}
|
|
@@ -1406,15 +869,15 @@ async function listUnusedFiles({
|
|
|
1406
869
|
service.dispose();
|
|
1407
870
|
return;
|
|
1408
871
|
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Whether a flagged validation value names a file.
|
|
875
|
+
*
|
|
876
|
+
* A `ValidationError` carries no schema, so the shape is all there is to go on —
|
|
877
|
+
* the same heuristic this whole function documents as a TODO.
|
|
878
|
+
*/
|
|
1409
879
|
function isFileRef(value) {
|
|
1410
|
-
|
|
1411
|
-
if (typeof value !== "object") return false;
|
|
1412
|
-
if (FILE_REF_PROP in value && VAL_EXTENSION in value) {
|
|
1413
|
-
if (value[VAL_EXTENSION] === "file" && typeof value[FILE_REF_PROP] === "string") {
|
|
1414
|
-
return true;
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
return false;
|
|
880
|
+
return !!value && typeof value === "object" && "path" in value && typeof value.path === "string";
|
|
1418
881
|
}
|
|
1419
882
|
|
|
1420
883
|
const getVersions = () => {
|
|
@@ -2005,8 +1468,11 @@ async function buildSnapshot(ctx, options = {}) {
|
|
|
2005
1468
|
path: p
|
|
2006
1469
|
});
|
|
2007
1470
|
}
|
|
2008
|
-
entries[`.val/patches/${patch.
|
|
1471
|
+
entries[`.val/patches/${patch.patchId}/patch.json`] = JSON.stringify(toFsPatch(patch), null, 2);
|
|
2009
1472
|
}
|
|
1473
|
+
// The order lives in the log, not in the records, so it has to be written too
|
|
1474
|
+
// or the unzipped snapshot is a store with no order at all.
|
|
1475
|
+
entries[".val/patches/patches.log"] = ["val-patch-log v1", ...patches.map(patch => `${patch.patchId} ${patch.createdAt} ${patch.path}`), ""].join("\n");
|
|
2010
1476
|
let includesBinaryFiles = false;
|
|
2011
1477
|
if (options.includeFiles) {
|
|
2012
1478
|
includesBinaryFiles = await writeBinaryFiles(ctx, analysis.fileLastUpdatedByPatchId, patches, entries);
|
|
@@ -2073,34 +1539,29 @@ async function validateSnapshotSources(ctx, analysis) {
|
|
|
2073
1539
|
return validationRes.errors;
|
|
2074
1540
|
}
|
|
2075
1541
|
function toSnapshotPatches(orderedPatches) {
|
|
2076
|
-
return orderedPatches.map(
|
|
1542
|
+
return orderedPatches.map(patch => ({
|
|
2077
1543
|
patchId: patch.patchId,
|
|
2078
1544
|
path: patch.path,
|
|
2079
1545
|
createdAt: patch.createdAt,
|
|
2080
1546
|
authorId: patch.authorId,
|
|
2081
1547
|
baseSha: patch.baseSha,
|
|
2082
1548
|
appliedAt: patch.appliedAt,
|
|
2083
|
-
parentPatchId: i === 0 ? null : orderedPatches[i - 1].patchId,
|
|
2084
1549
|
patch: patch.patch
|
|
2085
1550
|
}));
|
|
2086
1551
|
}
|
|
2087
1552
|
|
|
2088
1553
|
/**
|
|
2089
1554
|
* The shape ValOpsFS writes per patch, so an unzipped snapshot is a patch store
|
|
2090
|
-
* a plain ValOpsFS can read.
|
|
2091
|
-
*
|
|
1555
|
+
* a plain ValOpsFS can read.
|
|
1556
|
+
*
|
|
1557
|
+
* The directory is named after the patch itself and the record points at
|
|
1558
|
+
* nothing: the order is in `patches.log` beside it. A record that named its
|
|
1559
|
+
* parent is how a snapshot could carry a chain that had already lost a link.
|
|
2092
1560
|
*/
|
|
2093
1561
|
function toFsPatch(patch) {
|
|
2094
1562
|
return {
|
|
2095
1563
|
patch: patch.patch,
|
|
2096
1564
|
patchId: patch.patchId,
|
|
2097
|
-
parentRef: patch.parentPatchId === null ? {
|
|
2098
|
-
type: "head",
|
|
2099
|
-
headBaseSha: patch.baseSha
|
|
2100
|
-
} : {
|
|
2101
|
-
type: "patch",
|
|
2102
|
-
patchId: patch.parentPatchId
|
|
2103
|
-
},
|
|
2104
1565
|
path: patch.path,
|
|
2105
1566
|
authorId: patch.authorId,
|
|
2106
1567
|
sessionId: null,
|
|
@@ -2110,14 +1571,13 @@ function toFsPatch(patch) {
|
|
|
2110
1571
|
};
|
|
2111
1572
|
}
|
|
2112
1573
|
async function writeBinaryFiles(ctx, fileLastUpdatedByPatchId, patches, entries) {
|
|
2113
|
-
const
|
|
1574
|
+
const known = new Set(patches.map(p => p.patchId));
|
|
2114
1575
|
let wrote = false;
|
|
2115
1576
|
for (const [filePath, data] of Object.entries(fileLastUpdatedByPatchId)) {
|
|
2116
1577
|
if (data.isDelete) {
|
|
2117
1578
|
continue;
|
|
2118
1579
|
}
|
|
2119
|
-
|
|
2120
|
-
if (parentPatchId === undefined) {
|
|
1580
|
+
if (!known.has(data.patchId)) {
|
|
2121
1581
|
continue;
|
|
2122
1582
|
}
|
|
2123
1583
|
const buffer = await ctx.serverOps.getBase64EncodedBinaryFileFromPatch(filePath, data.patchId, data.remote);
|
|
@@ -2125,7 +1585,7 @@ async function writeBinaryFiles(ctx, fileLastUpdatedByPatchId, patches, entries)
|
|
|
2125
1585
|
continue;
|
|
2126
1586
|
}
|
|
2127
1587
|
// Base64 so the snapshot stays a text-only entry map; the replay decodes it.
|
|
2128
|
-
entries[`.val/patches/${
|
|
1588
|
+
entries[`.val/patches/${data.patchId}/files${filePath}/${path.posix.basename(filePath)}.base64`] = buffer.toString("base64");
|
|
2129
1589
|
wrote = true;
|
|
2130
1590
|
}
|
|
2131
1591
|
return wrote;
|
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.103.1",
|
|
5
5
|
"description": "Val CLI tools",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -27,11 +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.103.0",
|
|
31
31
|
"@valbuild/eslint-plugin": "0.93.0",
|
|
32
|
-
"@valbuild/language-server": "0.
|
|
33
|
-
"@valbuild/server": "0.
|
|
34
|
-
"@valbuild/shared": "0.
|
|
32
|
+
"@valbuild/language-server": "0.103.1",
|
|
33
|
+
"@valbuild/server": "0.103.1",
|
|
34
|
+
"@valbuild/shared": "0.103.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"prettier": "*",
|