@valbuild/cli 0.102.0 → 0.103.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 +13 -550
- package/cli/dist/valbuild-cli-cli.cjs.prod.js +13 -550
- package/cli/dist/valbuild-cli-cli.esm.js +12 -549
- 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/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/utils/getFileExt.ts +0 -4
|
@@ -9,8 +9,8 @@ var fastGlob = require('fast-glob');
|
|
|
9
9
|
var core = require('@valbuild/core');
|
|
10
10
|
var server = require('@valbuild/server');
|
|
11
11
|
var internal = require('@valbuild/shared/internal');
|
|
12
|
-
var ts = require('typescript');
|
|
13
12
|
var fs = require('fs');
|
|
13
|
+
var ts = require('typescript');
|
|
14
14
|
var JSZip = require('jszip');
|
|
15
15
|
var readline = require('readline');
|
|
16
16
|
|
|
@@ -39,8 +39,8 @@ var chalk__default = /*#__PURE__*/_interopDefault(chalk);
|
|
|
39
39
|
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
40
40
|
var pc__default = /*#__PURE__*/_interopDefault(pc);
|
|
41
41
|
var fs__default$1 = /*#__PURE__*/_interopDefault(fs$1);
|
|
42
|
-
var ts__default = /*#__PURE__*/_interopDefault(ts);
|
|
43
42
|
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
43
|
+
var ts__default = /*#__PURE__*/_interopDefault(ts);
|
|
44
44
|
var JSZip__default = /*#__PURE__*/_interopDefault(JSZip);
|
|
45
45
|
var readline__default = /*#__PURE__*/_interopDefault(readline);
|
|
46
46
|
|
|
@@ -59,543 +59,6 @@ function info(message, opts = {}) {
|
|
|
59
59
|
console.log(chalk__default["default"].blue("️ℹ️ : ") + message);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
function getFileExt(filePath) {
|
|
63
|
-
// 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).
|
|
64
|
-
return filePath.split(".").pop() || "";
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const textEncoder = new TextEncoder();
|
|
68
|
-
|
|
69
|
-
// Types for handler system
|
|
70
|
-
|
|
71
|
-
// Handler functions
|
|
72
|
-
async function handleFileMetadata(ctx) {
|
|
73
|
-
var _fileSource$source;
|
|
74
|
-
const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
|
|
75
|
-
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
76
|
-
return {
|
|
77
|
-
success: false,
|
|
78
|
-
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
const fileSource = core.Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
|
|
82
|
-
|
|
83
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
84
|
-
const fileRefProp = (_fileSource$source = fileSource.source) === null || _fileSource$source === void 0 ? void 0 : _fileSource$source[core.FILE_REF_PROP];
|
|
85
|
-
if (!fileRefProp) {
|
|
86
|
-
return {
|
|
87
|
-
success: false,
|
|
88
|
-
errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
const filePath = path__default["default"].join(ctx.projectRoot, fileRefProp);
|
|
92
|
-
if (!ctx.fs.fileExists(filePath)) {
|
|
93
|
-
return {
|
|
94
|
-
success: false,
|
|
95
|
-
errorMessage: `File ${filePath} does not exist`
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
return {
|
|
99
|
-
success: true,
|
|
100
|
-
shouldApplyPatch: true
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Shared upload core used by both the single-field (handleRemoteFileUpload)
|
|
105
|
-
// and gallery (handleRemoteGalleryFileUpload) handlers. The two differ only in
|
|
106
|
-
// how they derive the local file ref, metadata and serialized image/file
|
|
107
|
-
// schema; everything from auth through upload is identical.
|
|
108
|
-
async function uploadRemoteFileCore(ctx, fileRef, metadata, schema) {
|
|
109
|
-
const filePath = path__default["default"].join(ctx.projectRoot, fileRef);
|
|
110
|
-
if (!ctx.fs.fileExists(filePath)) {
|
|
111
|
-
return {
|
|
112
|
-
success: false,
|
|
113
|
-
errorMessage: `File ${filePath} does not exist`
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
const patFile = server.getPersonalAccessTokenPath(ctx.projectRoot);
|
|
117
|
-
if (!ctx.fs.fileExists(patFile)) {
|
|
118
|
-
return {
|
|
119
|
-
success: false,
|
|
120
|
-
errorMessage: `File: ${path__default["default"].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`
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
const patFileContent = ctx.fs.readFile(patFile);
|
|
124
|
-
if (patFileContent === undefined) {
|
|
125
|
-
return {
|
|
126
|
-
success: false,
|
|
127
|
-
errorMessage: `Could not read personal access token file at ${patFile}`
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
const parsedPatFile = server.parsePersonalAccessTokenFile(patFileContent);
|
|
131
|
-
if (!parsedPatFile.success) {
|
|
132
|
-
return {
|
|
133
|
-
success: false,
|
|
134
|
-
errorMessage: `Error parsing personal access token file: ${parsedPatFile.error}. You need to login again.`
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
const {
|
|
138
|
-
pat
|
|
139
|
-
} = parsedPatFile.data;
|
|
140
|
-
if (ctx.remoteFiles[ctx.sourcePath]) {
|
|
141
|
-
return {
|
|
142
|
-
success: true,
|
|
143
|
-
events: [{
|
|
144
|
-
type: "remote-already-uploaded",
|
|
145
|
-
filePath
|
|
146
|
-
}]
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
const projectName = ctx.project;
|
|
150
|
-
let publicProjectId = ctx.publicProjectId;
|
|
151
|
-
let remoteFileBuckets = ctx.remoteFileBuckets;
|
|
152
|
-
let remoteFilesCounter = ctx.remoteFilesCounter;
|
|
153
|
-
if (!publicProjectId || !remoteFileBuckets) {
|
|
154
|
-
if (!projectName) {
|
|
155
|
-
return {
|
|
156
|
-
success: false,
|
|
157
|
-
errorMessage: "Project name not found. Add project name to val.config or set the VAL_PROJECT environment variable"
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
const settingsRes = await ctx.remote.getSettings(projectName, {
|
|
161
|
-
pat
|
|
162
|
-
});
|
|
163
|
-
if (!settingsRes.success) {
|
|
164
|
-
return {
|
|
165
|
-
success: false,
|
|
166
|
-
errorMessage: `Could not get public project id: ${settingsRes.message}.`
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
publicProjectId = settingsRes.data.publicProjectId;
|
|
170
|
-
remoteFileBuckets = settingsRes.data.remoteFileBuckets.map(b => b.bucket);
|
|
171
|
-
}
|
|
172
|
-
if (!publicProjectId) {
|
|
173
|
-
return {
|
|
174
|
-
success: false,
|
|
175
|
-
errorMessage: "Could not get public project id"
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
if (!projectName) {
|
|
179
|
-
return {
|
|
180
|
-
success: false,
|
|
181
|
-
errorMessage: `Could not get project. Check that your val.config has the 'project' field set, or set it using the VAL_PROJECT environment variable`
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
remoteFilesCounter += 1;
|
|
185
|
-
const bucket = remoteFileBuckets[remoteFilesCounter % remoteFileBuckets.length];
|
|
186
|
-
if (!bucket) {
|
|
187
|
-
return {
|
|
188
|
-
success: false,
|
|
189
|
-
errorMessage: `Internal error: could not allocate a bucket for the remote file located at ${ctx.sourcePath}`
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
const fileBuffer = ctx.fs.readBuffer(filePath);
|
|
193
|
-
if (fileBuffer === undefined) {
|
|
194
|
-
return {
|
|
195
|
-
success: false,
|
|
196
|
-
errorMessage: `Error reading file: ${filePath}`
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
const relativeFilePath = path__default["default"].relative(ctx.projectRoot, filePath).split(path__default["default"].sep).join("/");
|
|
200
|
-
if (!relativeFilePath.startsWith("public/")) {
|
|
201
|
-
return {
|
|
202
|
-
success: false,
|
|
203
|
-
errorMessage: `File path must be within the public/ directory (e.g. public/path/to/file.txt). Got: ${relativeFilePath}`
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
const fileHash = core.Internal.remote.getFileHash(fileBuffer);
|
|
207
|
-
const coreVersion = core.Internal.VERSION.core || "unknown";
|
|
208
|
-
const fileExt = getFileExt(filePath);
|
|
209
|
-
const ref = core.Internal.remote.createRemoteRef(ctx.remote.remoteHost, {
|
|
210
|
-
publicProjectId,
|
|
211
|
-
coreVersion,
|
|
212
|
-
bucket,
|
|
213
|
-
validationHash: core.Internal.remote.getValidationHash(coreVersion, schema, fileExt, metadata, fileHash, textEncoder),
|
|
214
|
-
fileHash,
|
|
215
|
-
filePath: relativeFilePath
|
|
216
|
-
});
|
|
217
|
-
const remoteFileUpload = await ctx.remote.uploadFile(projectName, bucket, fileHash, fileExt, fileBuffer, {
|
|
218
|
-
pat
|
|
219
|
-
});
|
|
220
|
-
if (!remoteFileUpload.success) {
|
|
221
|
-
return {
|
|
222
|
-
success: false,
|
|
223
|
-
errorMessage: `Could not upload remote file: '${ref}'. Error: ${remoteFileUpload.error}`
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
ctx.remoteFiles[ctx.sourcePath] = {
|
|
227
|
-
ref,
|
|
228
|
-
metadata
|
|
229
|
-
};
|
|
230
|
-
return {
|
|
231
|
-
success: true,
|
|
232
|
-
shouldApplyPatch: true,
|
|
233
|
-
publicProjectId,
|
|
234
|
-
remoteFileBuckets,
|
|
235
|
-
remoteFilesCounter,
|
|
236
|
-
events: [{
|
|
237
|
-
type: "remote-uploading",
|
|
238
|
-
ref
|
|
239
|
-
}, {
|
|
240
|
-
type: "remote-uploaded",
|
|
241
|
-
ref
|
|
242
|
-
}]
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
async function handleRemoteFileUpload(ctx) {
|
|
246
|
-
var _resolvedRemoteFileAt;
|
|
247
|
-
if (!ctx.fix) {
|
|
248
|
-
return {
|
|
249
|
-
success: false,
|
|
250
|
-
// No sourcePath in the message: the reported location already points at it.
|
|
251
|
-
errorMessage: `Remote file needs to be uploaded (use --fix to upload)`
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
|
|
255
|
-
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
256
|
-
return {
|
|
257
|
-
success: false,
|
|
258
|
-
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
const resolvedRemoteFileAtSourcePath = core.Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
|
|
262
|
-
|
|
263
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
264
|
-
const fileRefProp = (_resolvedRemoteFileAt = resolvedRemoteFileAtSourcePath.source) === null || _resolvedRemoteFileAt === void 0 ? void 0 : _resolvedRemoteFileAt[core.FILE_REF_PROP];
|
|
265
|
-
if (!fileRefProp) {
|
|
266
|
-
return {
|
|
267
|
-
success: false,
|
|
268
|
-
errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
const resolveRemoteFileSchema = resolvedRemoteFileAtSourcePath.schema;
|
|
272
|
-
if (!resolveRemoteFileSchema) {
|
|
273
|
-
return {
|
|
274
|
-
success: false,
|
|
275
|
-
errorMessage: `Cannot upload remote file: schema not found for ${ctx.sourcePath}`
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
if (resolveRemoteFileSchema.type !== "image" && resolveRemoteFileSchema.type !== "file") {
|
|
279
|
-
return {
|
|
280
|
-
success: false,
|
|
281
|
-
errorMessage: `The schema is the remote is neither image nor file: ${ctx.sourcePath}`
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
const actualRemoteFileSource = resolvedRemoteFileAtSourcePath.source;
|
|
285
|
-
const fileSourceMetadata = core.Internal.isFile(actualRemoteFileSource) ? actualRemoteFileSource.metadata : undefined;
|
|
286
|
-
return uploadRemoteFileCore(ctx, fileRefProp, fileSourceMetadata, resolveRemoteFileSchema);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Gallery (s.images({ remote: true }) / s.files({ remote: true })) upload.
|
|
290
|
-
// Unlike a single image/file field, a gallery entry is keyed by its local file
|
|
291
|
-
// path and the value is bare metadata (no FileSource), so we derive the file
|
|
292
|
-
// ref from the key and synthesize the image/file schema from the record's
|
|
293
|
-
// media options.
|
|
294
|
-
async function handleRemoteGalleryFileUpload(ctx) {
|
|
295
|
-
var _ctx$validationError$;
|
|
296
|
-
if (!ctx.fix) {
|
|
297
|
-
return {
|
|
298
|
-
success: false,
|
|
299
|
-
// No sourcePath in the message: the reported location already points at it.
|
|
300
|
-
errorMessage: `Remote file needs to be uploaded (use --fix to upload)`
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
const fix = (_ctx$validationError$ = ctx.validationError.fixes) === null || _ctx$validationError$ === void 0 ? void 0 : _ctx$validationError$[0];
|
|
304
|
-
const mediaType = fix === "files:upload-remote" ? "file" : "image";
|
|
305
|
-
|
|
306
|
-
// The gallery entry is keyed by its local file path; validateMediaKey carries
|
|
307
|
-
// that key as the error value.
|
|
308
|
-
const fileRef = ctx.validationError.value;
|
|
309
|
-
if (typeof fileRef !== "string") {
|
|
310
|
-
return {
|
|
311
|
-
success: false,
|
|
312
|
-
errorMessage: `Expected a local file path for gallery entry at ${ctx.sourcePath}`
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
|
|
316
|
-
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
317
|
-
return {
|
|
318
|
-
success: false,
|
|
319
|
-
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
const resolved = core.Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
|
|
323
|
-
const entrySource = resolved.source;
|
|
324
|
-
const metadata = entrySource && typeof entrySource === "object" && !Array.isArray(entrySource) ? entrySource : undefined;
|
|
325
|
-
|
|
326
|
-
// The gallery item schema is an ObjectSchema, not an image/file schema, so
|
|
327
|
-
// synthesize the serialized image/file schema (matching how single fields
|
|
328
|
-
// serialize) for the remote ref's validation hash.
|
|
329
|
-
//
|
|
330
|
-
// `accept`/`directory` come from the RECORD that holds the entry, resolved
|
|
331
|
-
// from the entry's PARENT path - not from the module root, which is only the
|
|
332
|
-
// record when the gallery is the whole module. A nested gallery
|
|
333
|
-
// (s.object({ gallery: s.images(...) })) would otherwise synthesize a schema
|
|
334
|
-
// with no options and bake a validation hash into the remote ref that can
|
|
335
|
-
// never validate, so a mismatch fails fast instead of uploading.
|
|
336
|
-
const [, parentModulePath] = core.Internal.splitModuleFilePathAndModulePath(core.Internal.parentOfSourcePath(ctx.sourcePath));
|
|
337
|
-
const recordSchema = core.Internal.resolvePath(parentModulePath, ctx.valModule.source, ctx.valModule.schema).schema;
|
|
338
|
-
if ((recordSchema === null || recordSchema === void 0 ? void 0 : recordSchema.type) !== "record") {
|
|
339
|
-
return {
|
|
340
|
-
success: false,
|
|
341
|
-
errorMessage: `Expected a gallery record at the parent of ${ctx.sourcePath}, got ${(recordSchema === null || recordSchema === void 0 ? void 0 : recordSchema.type) ?? "nothing"}`
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
const {
|
|
345
|
-
accept,
|
|
346
|
-
directory
|
|
347
|
-
} = recordSchema;
|
|
348
|
-
const schema = mediaType === "image" ? {
|
|
349
|
-
type: "image",
|
|
350
|
-
opt: false,
|
|
351
|
-
options: {
|
|
352
|
-
...(accept ? {
|
|
353
|
-
accept
|
|
354
|
-
} : {}),
|
|
355
|
-
...(directory ? {
|
|
356
|
-
directory
|
|
357
|
-
} : {})
|
|
358
|
-
}
|
|
359
|
-
} : {
|
|
360
|
-
type: "file",
|
|
361
|
-
opt: false,
|
|
362
|
-
options: {
|
|
363
|
-
...(accept ? {
|
|
364
|
-
accept
|
|
365
|
-
} : {})
|
|
366
|
-
}
|
|
367
|
-
};
|
|
368
|
-
return uploadRemoteFileCore(ctx, fileRef, metadata, schema);
|
|
369
|
-
}
|
|
370
|
-
async function handleRemoteFileDownload(ctx) {
|
|
371
|
-
if (ctx.fix) {
|
|
372
|
-
return {
|
|
373
|
-
success: true,
|
|
374
|
-
shouldApplyPatch: true,
|
|
375
|
-
events: [{
|
|
376
|
-
type: "remote-downloading",
|
|
377
|
-
sourcePath: ctx.sourcePath
|
|
378
|
-
}]
|
|
379
|
-
};
|
|
380
|
-
} else {
|
|
381
|
-
return {
|
|
382
|
-
success: false,
|
|
383
|
-
errorMessage: `Remote file ${ctx.sourcePath} needs to be downloaded (use --fix to download)`
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
async function handleRemoteFileCheck() {
|
|
388
|
-
// Skip - no action needed
|
|
389
|
-
return {
|
|
390
|
-
success: true,
|
|
391
|
-
shouldApplyPatch: true
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
async function handleUniqueFolderCheck(ctx) {
|
|
395
|
-
const value = ctx.validationError.value;
|
|
396
|
-
if (!value || typeof value.directory !== "string") {
|
|
397
|
-
return {
|
|
398
|
-
success: false,
|
|
399
|
-
errorMessage: `Unexpected value in unique folder check for ${ctx.sourcePath}`
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
const {
|
|
403
|
-
directory
|
|
404
|
-
} = value;
|
|
405
|
-
const conflicts = [];
|
|
406
|
-
for (const file of ctx.valFiles) {
|
|
407
|
-
const otherModuleFilePath = `/${file}`;
|
|
408
|
-
if (otherModuleFilePath === ctx.moduleFilePath) continue;
|
|
409
|
-
const otherModule = await ctx.service.get(otherModuleFilePath, "", {
|
|
410
|
-
validate: false
|
|
411
|
-
});
|
|
412
|
-
const schema = otherModule.schema;
|
|
413
|
-
if ((schema === null || schema === void 0 ? void 0 : schema.type) === "record" && schema.directory === directory && schema.mediaType) {
|
|
414
|
-
conflicts.push(otherModuleFilePath);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
if (conflicts.length > 0) {
|
|
418
|
-
return {
|
|
419
|
-
success: false,
|
|
420
|
-
errorMessage: `Gallery directory '${directory}' in ${ctx.moduleFilePath} is also used by: ${conflicts.join(", ")}. Each gallery must use a unique directory.`
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
return {
|
|
424
|
-
success: true
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Maps a gallery key to its on-disk local path. Remote galleries key uploaded
|
|
429
|
-
// entries by a remote URL while keeping the file on disk; everything else is
|
|
430
|
-
// already a local path and is returned unchanged.
|
|
431
|
-
function remoteKeyToLocalPath(key) {
|
|
432
|
-
const remoteRefRes = core.Internal.remote.splitRemoteRef(key);
|
|
433
|
-
if (remoteRefRes.status === "success") {
|
|
434
|
-
return `/${remoteRefRes.filePath}`;
|
|
435
|
-
}
|
|
436
|
-
return key;
|
|
437
|
-
}
|
|
438
|
-
async function handleCheckAllFiles(ctx) {
|
|
439
|
-
const value = ctx.validationError.value;
|
|
440
|
-
if (!value || typeof value.directory !== "string") {
|
|
441
|
-
return {
|
|
442
|
-
success: false,
|
|
443
|
-
errorMessage: `Unexpected value in check-all-files for ${ctx.sourcePath}`
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
const {
|
|
447
|
-
directory
|
|
448
|
-
} = value;
|
|
449
|
-
const source = ctx.valModule.source;
|
|
450
|
-
if (!source || typeof source !== "object" || Array.isArray(source)) {
|
|
451
|
-
return {
|
|
452
|
-
success: false,
|
|
453
|
-
errorMessage: `Could not get source for ${ctx.sourcePath}`
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
// Gallery entries are keyed by their file path. Remote galleries key uploaded
|
|
457
|
-
// entries by a remote URL, but the file is kept on disk at its local path, so
|
|
458
|
-
// normalize remote-URL keys back to that local path for the on-disk checks.
|
|
459
|
-
const trackedFiles = new Set(Object.keys(source).map(remoteKeyToLocalPath));
|
|
460
|
-
|
|
461
|
-
// Check that all tracked files exist on disk
|
|
462
|
-
const missingTrackedFiles = [...trackedFiles].filter(f => {
|
|
463
|
-
return !ctx.fs.fileExists(path__default["default"].join(ctx.projectRoot, f));
|
|
464
|
-
});
|
|
465
|
-
if (missingTrackedFiles.length > 0) {
|
|
466
|
-
if (!ctx.fix) {
|
|
467
|
-
return {
|
|
468
|
-
success: false,
|
|
469
|
-
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.`
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
// fix: true — let createFixPatch remove the missing entries
|
|
473
|
-
return {
|
|
474
|
-
success: true,
|
|
475
|
-
shouldApplyPatch: true
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
const dirPath = path__default["default"].join(ctx.projectRoot, directory);
|
|
479
|
-
const filesInDir = [];
|
|
480
|
-
try {
|
|
481
|
-
const entries = ctx.fs.readDirectory(dirPath, undefined, undefined, ["**/*"]);
|
|
482
|
-
for (const entry of entries) {
|
|
483
|
-
const relPath = "/" + path__default["default"].relative(ctx.projectRoot, entry).split(path__default["default"].sep).join("/");
|
|
484
|
-
filesInDir.push(relPath);
|
|
485
|
-
}
|
|
486
|
-
} catch {
|
|
487
|
-
// directory doesn't exist — no untracked files possible
|
|
488
|
-
}
|
|
489
|
-
const untrackedFiles = filesInDir.filter(f => !trackedFiles.has(f));
|
|
490
|
-
if (untrackedFiles.length > 0) {
|
|
491
|
-
return {
|
|
492
|
-
success: false,
|
|
493
|
-
errorMessage: `Gallery in ${ctx.moduleFilePath} has files not tracked: ${untrackedFiles.join(", ")}. Add these files to the gallery or remove them from the directory.`
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// All files accounted for — trigger metadata verification via createFixPatch
|
|
498
|
-
return {
|
|
499
|
-
success: true,
|
|
500
|
-
shouldApplyPatch: true
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
async function handleJsonValuesExtractEntry(ctx) {
|
|
504
|
-
const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
|
|
505
|
-
const parts = core.Internal.splitModulePath(modulePath);
|
|
506
|
-
// Root-only by contract (see findNestedJsonValuesRecords): the entry is a
|
|
507
|
-
// direct child of the module's root record/router, so the path is one segment.
|
|
508
|
-
if (parts.length !== 1) {
|
|
509
|
-
return {
|
|
510
|
-
success: false,
|
|
511
|
-
errorMessage: `Cannot extract .jsonValues() entry at ${ctx.sourcePath}: expected a root record entry`
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
const entryKey = parts[0];
|
|
515
|
-
const source = ctx.valModule.source;
|
|
516
|
-
if (source === null || typeof source !== "object" || Array.isArray(source)) {
|
|
517
|
-
return {
|
|
518
|
-
success: false,
|
|
519
|
-
errorMessage: `Could not get source for ${ctx.moduleFilePath}`
|
|
520
|
-
};
|
|
521
|
-
}
|
|
522
|
-
const content = source[entryKey];
|
|
523
|
-
if (content === undefined) {
|
|
524
|
-
return {
|
|
525
|
-
success: false,
|
|
526
|
-
errorMessage: `Could not find .jsonValues() entry '${entryKey}' in ${ctx.moduleFilePath}`
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
if (!ctx.fix) {
|
|
530
|
-
return {
|
|
531
|
-
success: true,
|
|
532
|
-
fixableErrorMessage: ctx.validationError.message
|
|
533
|
-
};
|
|
534
|
-
}
|
|
535
|
-
try {
|
|
536
|
-
server.extractJsonValuesEntry(ctx.moduleFilePath, ctx.projectRoot, entryKey, content, ctx.service.sourceFileHandler);
|
|
537
|
-
} catch (err) {
|
|
538
|
-
return {
|
|
539
|
-
success: false,
|
|
540
|
-
errorMessage: err instanceof Error ? err.message : String(err)
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
return {
|
|
544
|
-
success: true,
|
|
545
|
-
appliedFix: true
|
|
546
|
-
};
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// Fix handler registry. `keyof:check-keys` and `router:check-route` are
|
|
550
|
-
// resolved upfront by the shared resolveSchemaSourceFixes — they never reach
|
|
551
|
-
// this registry, so they're excluded from the key set.
|
|
552
|
-
const currentFixHandlers = {
|
|
553
|
-
"image:check-metadata": handleFileMetadata,
|
|
554
|
-
"image:add-metadata": handleFileMetadata,
|
|
555
|
-
"file:check-metadata": handleFileMetadata,
|
|
556
|
-
"file:add-metadata": handleFileMetadata,
|
|
557
|
-
"image:upload-remote": handleRemoteFileUpload,
|
|
558
|
-
"file:upload-remote": handleRemoteFileUpload,
|
|
559
|
-
"images:upload-remote": handleRemoteGalleryFileUpload,
|
|
560
|
-
"files:upload-remote": handleRemoteGalleryFileUpload,
|
|
561
|
-
"image:download-remote": handleRemoteFileDownload,
|
|
562
|
-
"file:download-remote": handleRemoteFileDownload,
|
|
563
|
-
"image:check-remote": handleRemoteFileCheck,
|
|
564
|
-
"images:check-remote": handleRemoteFileCheck,
|
|
565
|
-
"file:check-remote": handleRemoteFileCheck,
|
|
566
|
-
"files:check-remote": handleRemoteFileCheck,
|
|
567
|
-
"images:check-unique-folder": handleUniqueFolderCheck,
|
|
568
|
-
"files:check-unique-folder": handleUniqueFolderCheck,
|
|
569
|
-
"images:check-all-files": handleCheckAllFiles,
|
|
570
|
-
"files:check-all-files": handleCheckAllFiles,
|
|
571
|
-
"jsonValues:extract-entry": handleJsonValuesExtractEntry
|
|
572
|
-
};
|
|
573
|
-
const deprecatedFixHandlers = {
|
|
574
|
-
"image:replace-metadata": handleFileMetadata
|
|
575
|
-
};
|
|
576
|
-
const fixHandlers = {
|
|
577
|
-
...deprecatedFixHandlers,
|
|
578
|
-
...currentFixHandlers
|
|
579
|
-
};
|
|
580
|
-
function createDefaultValFSHost() {
|
|
581
|
-
return {
|
|
582
|
-
...ts__default["default"].sys,
|
|
583
|
-
writeFile: (fileName, data, encoding) => {
|
|
584
|
-
fs__default["default"].mkdirSync(path__default["default"].dirname(fileName), {
|
|
585
|
-
recursive: true
|
|
586
|
-
});
|
|
587
|
-
fs__default["default"].writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
|
|
588
|
-
},
|
|
589
|
-
rmFile: fs__default["default"].rmSync,
|
|
590
|
-
readBuffer: fileName => {
|
|
591
|
-
try {
|
|
592
|
-
return fs__default["default"].readFileSync(fileName);
|
|
593
|
-
} catch {
|
|
594
|
-
return undefined;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
};
|
|
598
|
-
}
|
|
599
62
|
async function* runValidation({
|
|
600
63
|
root,
|
|
601
64
|
fix,
|
|
@@ -682,7 +145,7 @@ async function* runValidation({
|
|
|
682
145
|
|
|
683
146
|
// Find and execute appropriate handler
|
|
684
147
|
const fixType = v.fixes[0]; // Take first fix
|
|
685
|
-
const handler = fixHandlers[fixType];
|
|
148
|
+
const handler = server.fixHandlers[fixType];
|
|
686
149
|
if (!handler) {
|
|
687
150
|
yield {
|
|
688
151
|
type: "unknown-fix",
|
|
@@ -1136,7 +599,7 @@ async function validate({
|
|
|
1136
599
|
getSettings: (projectName, options) => server.getSettings(projectName, options),
|
|
1137
600
|
uploadFile: (project, bucket, fileHash, fileExt, fileBuffer, options) => server.uploadRemoteFile(process.env.VAL_CONTENT_URL || core.DEFAULT_CONTENT_HOST, project, bucket, fileHash, fileExt ?? "", fileBuffer, options)
|
|
1138
601
|
},
|
|
1139
|
-
fs: createDefaultValFSHost()
|
|
602
|
+
fs: server.createDefaultValFSHost()
|
|
1140
603
|
})) {
|
|
1141
604
|
switch (event.type) {
|
|
1142
605
|
case "file-valid":
|
|
@@ -1413,7 +876,7 @@ async function listUnusedFiles({
|
|
|
1413
876
|
for (const error of validationError) {
|
|
1414
877
|
const value = error.value;
|
|
1415
878
|
if (isFileRef(value)) {
|
|
1416
|
-
const absoluteFilePathUsedByVal = path__default["default"].join(projectRoot, ...value
|
|
879
|
+
const absoluteFilePathUsedByVal = path__default["default"].join(projectRoot, ...value.path.split("/"));
|
|
1417
880
|
filesUsedByVal.push(absoluteFilePathUsedByVal);
|
|
1418
881
|
}
|
|
1419
882
|
}
|
|
@@ -1438,15 +901,15 @@ async function listUnusedFiles({
|
|
|
1438
901
|
service.dispose();
|
|
1439
902
|
return;
|
|
1440
903
|
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Whether a flagged validation value names a file.
|
|
907
|
+
*
|
|
908
|
+
* A `ValidationError` carries no schema, so the shape is all there is to go on —
|
|
909
|
+
* the same heuristic this whole function documents as a TODO.
|
|
910
|
+
*/
|
|
1441
911
|
function isFileRef(value) {
|
|
1442
|
-
|
|
1443
|
-
if (typeof value !== "object") return false;
|
|
1444
|
-
if (core.FILE_REF_PROP in value && core.VAL_EXTENSION in value) {
|
|
1445
|
-
if (value[core.VAL_EXTENSION] === "file" && typeof value[core.FILE_REF_PROP] === "string") {
|
|
1446
|
-
return true;
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
|
-
return false;
|
|
912
|
+
return !!value && typeof value === "object" && "path" in value && typeof value.path === "string";
|
|
1450
913
|
}
|
|
1451
914
|
|
|
1452
915
|
const getVersions = () => {
|