@valbuild/cli 0.72.4 → 0.73.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 +333 -19
- package/cli/dist/valbuild-cli-cli.cjs.prod.js +333 -19
- package/cli/dist/valbuild-cli-cli.esm.js +331 -21
- package/package.json +6 -5
- package/src/cli.ts +13 -1
- package/src/getVersions.ts +4 -4
- package/src/login.ts +112 -0
- package/src/utils/evalValConfigFile.ts +90 -0
- package/src/utils/getFileExt.ts +4 -0
- package/src/utils/getValCoreVersion.ts +5 -0
- package/src/validate.ts +321 -25
package/src/login.ts
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
import pc from "picocolors";
|
2
|
+
import fs from "fs";
|
3
|
+
import path from "path";
|
4
|
+
import { getPersonalAccessTokenPath } from "@valbuild/server";
|
5
|
+
|
6
|
+
const host = process.env.VAL_BUILD_URL || "https://app.val.build";
|
7
|
+
|
8
|
+
export async function login(options: { root?: string }) {
|
9
|
+
try {
|
10
|
+
console.log(pc.cyan("\nStarting login process...\n"));
|
11
|
+
|
12
|
+
// Step 1: Initiate login and get token and URL
|
13
|
+
const response = await fetch(`${host}/api/login`, {
|
14
|
+
method: "POST",
|
15
|
+
headers: {
|
16
|
+
"Content-Type": "application/json",
|
17
|
+
},
|
18
|
+
});
|
19
|
+
let token;
|
20
|
+
let url;
|
21
|
+
if (!response.headers.get("content-type")?.includes("application/json")) {
|
22
|
+
const text = await response.text();
|
23
|
+
console.error(
|
24
|
+
pc.red(
|
25
|
+
"Unexpected failure while trying to login (content type was not JSON). Server response:",
|
26
|
+
),
|
27
|
+
text || "<empty>",
|
28
|
+
);
|
29
|
+
process.exit(1);
|
30
|
+
}
|
31
|
+
const json = await response.json();
|
32
|
+
if (json) {
|
33
|
+
token = json.nonce;
|
34
|
+
url = json.url;
|
35
|
+
}
|
36
|
+
if (!token || !url) {
|
37
|
+
console.error(pc.red("Unexpected response from the server."), json);
|
38
|
+
process.exit(1);
|
39
|
+
}
|
40
|
+
|
41
|
+
console.log(pc.green("Open the following URL in your browser to log in:"));
|
42
|
+
console.log(pc.underline(pc.blue(url)));
|
43
|
+
console.log(pc.dim("\nWaiting for login confirmation...\n"));
|
44
|
+
|
45
|
+
// Step 2: Poll for login confirmation
|
46
|
+
const result = await pollForConfirmation(token);
|
47
|
+
|
48
|
+
// Step 3: Save the token
|
49
|
+
const filePath = getPersonalAccessTokenPath(options.root || process.cwd());
|
50
|
+
saveToken(result, filePath);
|
51
|
+
} catch (error) {
|
52
|
+
console.error(
|
53
|
+
pc.red(
|
54
|
+
"An error occurred during the login process. Check your internet connection. Details:",
|
55
|
+
),
|
56
|
+
error instanceof Error ? error.message : JSON.stringify(error, null, 2),
|
57
|
+
);
|
58
|
+
process.exit(1);
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
const MAX_DURATION = 5 * 60 * 1000; // 5 minutes
|
63
|
+
async function pollForConfirmation(token: string): Promise<{
|
64
|
+
profile: { username: string };
|
65
|
+
pat: string;
|
66
|
+
}> {
|
67
|
+
const start = Date.now();
|
68
|
+
while (Date.now() - start < MAX_DURATION) {
|
69
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
70
|
+
const response = await fetch(
|
71
|
+
`${host}/api/login?token=${token}&consume=true`,
|
72
|
+
);
|
73
|
+
if (response.status === 500) {
|
74
|
+
console.error(pc.red("An error occurred on the server."));
|
75
|
+
process.exit(1);
|
76
|
+
}
|
77
|
+
if (response.status === 200) {
|
78
|
+
const json = await response.json();
|
79
|
+
if (json) {
|
80
|
+
if (
|
81
|
+
typeof json.profile.username === "string" &&
|
82
|
+
typeof json.pat === "string"
|
83
|
+
) {
|
84
|
+
return json;
|
85
|
+
} else {
|
86
|
+
console.error(pc.red("Unexpected response from the server."));
|
87
|
+
process.exit(1);
|
88
|
+
}
|
89
|
+
}
|
90
|
+
}
|
91
|
+
}
|
92
|
+
console.error(pc.red("Login confirmation timed out."));
|
93
|
+
process.exit(1);
|
94
|
+
}
|
95
|
+
|
96
|
+
function saveToken(
|
97
|
+
result: {
|
98
|
+
profile: { username: string };
|
99
|
+
pat: string;
|
100
|
+
},
|
101
|
+
filePath: string,
|
102
|
+
) {
|
103
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
104
|
+
fs.writeFileSync(filePath, JSON.stringify(result, null, 2));
|
105
|
+
console.log(
|
106
|
+
pc.green(
|
107
|
+
`Token for ${pc.cyan(
|
108
|
+
result.profile.username,
|
109
|
+
)} saved to ${pc.cyan(filePath)}`,
|
110
|
+
),
|
111
|
+
);
|
112
|
+
}
|
@@ -0,0 +1,90 @@
|
|
1
|
+
import path from "path";
|
2
|
+
import fs from "fs/promises";
|
3
|
+
import vm from "node:vm";
|
4
|
+
import ts from "typescript"; // TODO: make this dependency optional (only required if the file is val.config.ts not val.config.js)
|
5
|
+
import z from "zod";
|
6
|
+
import { ValConfig } from "@valbuild/core";
|
7
|
+
|
8
|
+
const ValConfigSchema = z.object({
|
9
|
+
project: z.string().optional(),
|
10
|
+
root: z.string().optional(),
|
11
|
+
files: z
|
12
|
+
.object({
|
13
|
+
directory: z
|
14
|
+
.string()
|
15
|
+
.refine((val): val is `/public/val` => val.startsWith("/public/val"), {
|
16
|
+
message: "files.directory must start with '/public/val'",
|
17
|
+
}),
|
18
|
+
})
|
19
|
+
.optional(),
|
20
|
+
gitCommit: z.string().optional(),
|
21
|
+
gitBranch: z.string().optional(),
|
22
|
+
defaultTheme: z.union([z.literal("light"), z.literal("dark")]).optional(),
|
23
|
+
ai: z
|
24
|
+
.object({
|
25
|
+
commitMessages: z
|
26
|
+
.object({
|
27
|
+
disabled: z.boolean().optional(),
|
28
|
+
})
|
29
|
+
.optional(),
|
30
|
+
})
|
31
|
+
.optional(),
|
32
|
+
});
|
33
|
+
|
34
|
+
export async function evalValConfigFile(
|
35
|
+
projectRoot: string,
|
36
|
+
configFileName: string,
|
37
|
+
): Promise<ValConfig | null> {
|
38
|
+
const valConfigPath = path.join(projectRoot, configFileName);
|
39
|
+
|
40
|
+
let code: string | null = null;
|
41
|
+
try {
|
42
|
+
code = await fs.readFile(valConfigPath, "utf-8");
|
43
|
+
} catch (err) {
|
44
|
+
//
|
45
|
+
}
|
46
|
+
if (!code) {
|
47
|
+
return null;
|
48
|
+
}
|
49
|
+
|
50
|
+
const transpiled = ts.transpileModule(code, {
|
51
|
+
compilerOptions: {
|
52
|
+
target: ts.ScriptTarget.ES2020,
|
53
|
+
module: ts.ModuleKind.CommonJS,
|
54
|
+
esModuleInterop: true,
|
55
|
+
},
|
56
|
+
fileName: valConfigPath,
|
57
|
+
});
|
58
|
+
|
59
|
+
const exportsObj = {};
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
61
|
+
const sandbox: Record<string, any> = {
|
62
|
+
exports: exportsObj,
|
63
|
+
module: { exports: exportsObj },
|
64
|
+
require, // NOTE: this is a security risk, but this code is running in the users own environment at the CLI level
|
65
|
+
__filename: valConfigPath,
|
66
|
+
__dirname: path.dirname(valConfigPath),
|
67
|
+
console,
|
68
|
+
process,
|
69
|
+
};
|
70
|
+
sandbox.global = sandbox;
|
71
|
+
|
72
|
+
const context = vm.createContext(sandbox);
|
73
|
+
const script = new vm.Script(transpiled.outputText, {
|
74
|
+
filename: valConfigPath,
|
75
|
+
});
|
76
|
+
script.runInContext(context);
|
77
|
+
const valConfig = sandbox.module.exports.config;
|
78
|
+
if (!valConfig) {
|
79
|
+
throw Error(
|
80
|
+
`Val config file at path: '${valConfigPath}' must export a config object. Got: ${valConfig}`,
|
81
|
+
);
|
82
|
+
}
|
83
|
+
const result = ValConfigSchema.safeParse(valConfig);
|
84
|
+
if (!result.success) {
|
85
|
+
throw Error(
|
86
|
+
`Val config file at path: '${valConfigPath}' has invalid schema: ${result.error.message}`,
|
87
|
+
);
|
88
|
+
}
|
89
|
+
return result.data;
|
90
|
+
}
|
@@ -0,0 +1,4 @@
|
|
1
|
+
export function getFileExt(filePath: string) {
|
2
|
+
// 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).
|
3
|
+
return filePath.split(".").pop() || "";
|
4
|
+
}
|
package/src/validate.ts
CHANGED
@@ -1,10 +1,20 @@
|
|
1
1
|
import path from "path";
|
2
|
-
import { createFixPatch, createService } from "@valbuild/server";
|
3
2
|
import {
|
3
|
+
createFixPatch,
|
4
|
+
createService,
|
5
|
+
getSettings,
|
6
|
+
getPersonalAccessTokenPath,
|
7
|
+
parsePersonalAccessTokenFile,
|
8
|
+
uploadRemoteFile,
|
9
|
+
} from "@valbuild/server";
|
10
|
+
import {
|
11
|
+
DEFAULT_VAL_REMOTE_HOST,
|
4
12
|
FILE_REF_PROP,
|
5
13
|
Internal,
|
6
14
|
ModuleFilePath,
|
7
15
|
ModulePath,
|
16
|
+
SerializedFileSchema,
|
17
|
+
SerializedImageSchema,
|
8
18
|
SourcePath,
|
9
19
|
ValidationFix,
|
10
20
|
} from "@valbuild/core";
|
@@ -12,6 +22,7 @@ import { glob } from "fast-glob";
|
|
12
22
|
import picocolors from "picocolors";
|
13
23
|
import { ESLint } from "eslint";
|
14
24
|
import fs from "fs/promises";
|
25
|
+
import { evalValConfigFile } from "./utils/evalValConfigFile";
|
15
26
|
|
16
27
|
export async function validate({
|
17
28
|
root,
|
@@ -22,11 +33,20 @@ export async function validate({
|
|
22
33
|
fix?: boolean;
|
23
34
|
noEslint?: boolean;
|
24
35
|
}) {
|
36
|
+
const valRemoteHost = process.env.VAL_REMOTE_HOST || DEFAULT_VAL_REMOTE_HOST;
|
25
37
|
const projectRoot = root ? path.resolve(root) : process.cwd();
|
26
38
|
const eslint = new ESLint({
|
27
39
|
cwd: projectRoot,
|
28
40
|
ignore: false,
|
29
41
|
});
|
42
|
+
const valConfigFile =
|
43
|
+
(await evalValConfigFile(projectRoot, "val.config.ts")) ||
|
44
|
+
(await evalValConfigFile(projectRoot, "val.config.js"));
|
45
|
+
console.log(
|
46
|
+
picocolors.greenBright(
|
47
|
+
`Validating project${valConfigFile?.project ? ` '${picocolors.inverse(valConfigFile?.project)}'` : ""}...`,
|
48
|
+
),
|
49
|
+
);
|
30
50
|
const service = await createService(projectRoot, {});
|
31
51
|
const checkKeyIsValid = async (
|
32
52
|
key: string,
|
@@ -119,8 +139,8 @@ export async function validate({
|
|
119
139
|
"files",
|
120
140
|
);
|
121
141
|
}
|
122
|
-
console.log(
|
123
|
-
|
142
|
+
console.log(picocolors.greenBright(`Found ${valFiles.length} files...`));
|
143
|
+
let publicProjectId: string | undefined;
|
124
144
|
let didFix = false; // TODO: ugly
|
125
145
|
async function validateFile(file: string): Promise<number> {
|
126
146
|
const moduleFilePath = `/${file}` as ModuleFilePath; // TODO: check if this always works? (Windows?)
|
@@ -135,6 +155,12 @@ export async function validate({
|
|
135
155
|
"utf-8",
|
136
156
|
);
|
137
157
|
const eslintResult = eslintResultsByFile?.[file];
|
158
|
+
const remoteFiles: Record<
|
159
|
+
SourcePath,
|
160
|
+
{ ref: string; metadata?: Record<string, unknown> }
|
161
|
+
> = {};
|
162
|
+
let remoteFileBuckets: string[] | null = null;
|
163
|
+
let remoteFilesCounter = 0;
|
138
164
|
eslintResult?.messages.forEach((m) => {
|
139
165
|
// display surrounding code
|
140
166
|
logEslintMessage(fileContent, moduleFilePath, m);
|
@@ -152,6 +178,7 @@ export async function validate({
|
|
152
178
|
(prev, m) => (m.severity >= 2 ? prev + 1 : prev),
|
153
179
|
0,
|
154
180
|
) || 0;
|
181
|
+
let fixedErrors = 0;
|
155
182
|
if (valModule.errors) {
|
156
183
|
if (valModule.errors.validation) {
|
157
184
|
for (const [sourcePath, validationErrors] of Object.entries(
|
@@ -178,24 +205,31 @@ export async function validate({
|
|
178
205
|
valModule.source,
|
179
206
|
valModule.schema,
|
180
207
|
);
|
181
|
-
|
182
|
-
projectRoot,
|
183
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
184
|
-
(fileSource.source as any)?.[FILE_REF_PROP],
|
185
|
-
);
|
208
|
+
let filePath: string | null = null;
|
186
209
|
try {
|
210
|
+
filePath = path.join(
|
211
|
+
projectRoot,
|
212
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
213
|
+
(fileSource.source as any)?.[FILE_REF_PROP],
|
214
|
+
);
|
187
215
|
await fs.access(filePath);
|
188
216
|
} catch {
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
217
|
+
if (filePath) {
|
218
|
+
console.log(
|
219
|
+
picocolors.red("✘"),
|
220
|
+
`File ${filePath} does not exist`,
|
221
|
+
);
|
222
|
+
} else {
|
223
|
+
console.log(
|
224
|
+
picocolors.red("✘"),
|
225
|
+
`Expected file to be defined at: ${sourcePath} but no file was found`,
|
226
|
+
);
|
227
|
+
}
|
193
228
|
errors += 1;
|
194
229
|
continue;
|
195
230
|
}
|
196
231
|
}
|
197
232
|
} else if (v.fixes.includes("keyof:check-keys")) {
|
198
|
-
const prevErrors = errors;
|
199
233
|
if (
|
200
234
|
v.value &&
|
201
235
|
typeof v.value === "object" &&
|
@@ -238,31 +272,274 @@ export async function validate({
|
|
238
272
|
);
|
239
273
|
errors += 1;
|
240
274
|
}
|
241
|
-
|
275
|
+
} else if (
|
276
|
+
v.fixes.includes("image:upload-remote") ||
|
277
|
+
v.fixes.includes("file:upload-remote")
|
278
|
+
) {
|
279
|
+
if (!fix) {
|
280
|
+
console.log(
|
281
|
+
picocolors.red("✘"),
|
282
|
+
`Remote file ${sourcePath} needs to be uploaded (use --fix to upload)`,
|
283
|
+
);
|
284
|
+
errors += 1;
|
285
|
+
continue;
|
286
|
+
}
|
287
|
+
const [, modulePath] =
|
288
|
+
Internal.splitModuleFilePathAndModulePath(
|
289
|
+
sourcePath as SourcePath,
|
290
|
+
);
|
291
|
+
if (valModule.source && valModule.schema) {
|
292
|
+
const resolvedRemoteFileAtSourcePath = Internal.resolvePath(
|
293
|
+
modulePath,
|
294
|
+
valModule.source,
|
295
|
+
valModule.schema,
|
296
|
+
);
|
297
|
+
let filePath: string | null = null;
|
298
|
+
try {
|
299
|
+
filePath = path.join(
|
300
|
+
projectRoot,
|
301
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
302
|
+
(resolvedRemoteFileAtSourcePath.source as any)?.[
|
303
|
+
FILE_REF_PROP
|
304
|
+
],
|
305
|
+
);
|
306
|
+
await fs.access(filePath);
|
307
|
+
} catch {
|
308
|
+
if (filePath) {
|
309
|
+
console.log(
|
310
|
+
picocolors.red("✘"),
|
311
|
+
`File ${filePath} does not exist`,
|
312
|
+
);
|
313
|
+
} else {
|
314
|
+
console.log(
|
315
|
+
picocolors.red("✘"),
|
316
|
+
`Expected file to be defined at: ${sourcePath} but no file was found`,
|
317
|
+
);
|
318
|
+
}
|
319
|
+
errors += 1;
|
320
|
+
continue;
|
321
|
+
}
|
322
|
+
const patFile = getPersonalAccessTokenPath(projectRoot);
|
323
|
+
try {
|
324
|
+
await fs.access(patFile);
|
325
|
+
} catch {
|
326
|
+
// TODO: display this error only once:
|
327
|
+
console.log(
|
328
|
+
picocolors.red("✘"),
|
329
|
+
`File: ${path.join(projectRoot, 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`,
|
330
|
+
);
|
331
|
+
errors += 1;
|
332
|
+
continue;
|
333
|
+
}
|
334
|
+
|
335
|
+
const parsedPatFile = parsePersonalAccessTokenFile(
|
336
|
+
await fs.readFile(patFile, "utf-8"),
|
337
|
+
);
|
338
|
+
if (!parsedPatFile.success) {
|
339
|
+
console.log(
|
340
|
+
picocolors.red("✘"),
|
341
|
+
`Error parsing personal access token file: ${parsedPatFile.error}. You need to login again.`,
|
342
|
+
);
|
343
|
+
errors += 1;
|
344
|
+
continue;
|
345
|
+
}
|
346
|
+
const { pat } = parsedPatFile.data;
|
347
|
+
|
348
|
+
if (remoteFiles[sourcePath as SourcePath]) {
|
349
|
+
console.log(
|
350
|
+
picocolors.yellow("⚠"),
|
351
|
+
`Remote file ${filePath} already uploaded`,
|
352
|
+
);
|
353
|
+
continue;
|
354
|
+
}
|
355
|
+
// TODO: parallelize this:
|
356
|
+
console.log(
|
357
|
+
picocolors.yellow("⚠"),
|
358
|
+
`Uploading remote file ${filePath}...`,
|
359
|
+
);
|
360
|
+
|
361
|
+
if (!resolvedRemoteFileAtSourcePath.schema) {
|
362
|
+
console.log(
|
363
|
+
picocolors.red("✘"),
|
364
|
+
`Cannot upload remote file: schema not found for ${sourcePath}`,
|
365
|
+
);
|
366
|
+
errors += 1;
|
367
|
+
continue;
|
368
|
+
}
|
369
|
+
|
370
|
+
const actualRemoteFileSource =
|
371
|
+
resolvedRemoteFileAtSourcePath.source;
|
372
|
+
const fileSourceMetadata = Internal.isFile(
|
373
|
+
actualRemoteFileSource,
|
374
|
+
)
|
375
|
+
? actualRemoteFileSource.metadata
|
376
|
+
: undefined;
|
377
|
+
const resolveRemoteFileSchema =
|
378
|
+
resolvedRemoteFileAtSourcePath.schema;
|
379
|
+
if (!resolveRemoteFileSchema) {
|
380
|
+
console.log(
|
381
|
+
picocolors.red("✘"),
|
382
|
+
`Could not resolve schema for remote file: ${sourcePath}`,
|
383
|
+
);
|
384
|
+
errors += 1;
|
385
|
+
continue;
|
386
|
+
}
|
387
|
+
if (!publicProjectId || !remoteFileBuckets) {
|
388
|
+
let projectName = process.env.VAL_PROJECT;
|
389
|
+
if (!projectName) {
|
390
|
+
projectName = valConfigFile?.project;
|
391
|
+
}
|
392
|
+
if (!projectName) {
|
393
|
+
console.log(
|
394
|
+
picocolors.red("✘"),
|
395
|
+
"Project name not found. Set VAL_PROJECT environment variable or add project name to val.config",
|
396
|
+
);
|
397
|
+
errors += 1;
|
398
|
+
continue;
|
399
|
+
}
|
400
|
+
const settingsRes = await getSettings(projectName, {
|
401
|
+
pat,
|
402
|
+
});
|
403
|
+
if (!settingsRes.success) {
|
404
|
+
console.log(
|
405
|
+
picocolors.red("✘"),
|
406
|
+
`Could not get public project id: ${settingsRes.message}.`,
|
407
|
+
);
|
408
|
+
errors += 1;
|
409
|
+
continue;
|
410
|
+
}
|
411
|
+
publicProjectId = settingsRes.data.publicProjectId;
|
412
|
+
remoteFileBuckets =
|
413
|
+
settingsRes.data.remoteFileBuckets.map((b) => b.bucket);
|
414
|
+
}
|
415
|
+
if (!publicProjectId) {
|
416
|
+
console.log(
|
417
|
+
picocolors.red("✘"),
|
418
|
+
"Could not get public project id",
|
419
|
+
);
|
420
|
+
errors += 1;
|
421
|
+
continue;
|
422
|
+
}
|
423
|
+
if (
|
424
|
+
resolveRemoteFileSchema.type !== "image" &&
|
425
|
+
resolveRemoteFileSchema.type !== "file"
|
426
|
+
) {
|
427
|
+
console.log(
|
428
|
+
picocolors.red("✘"),
|
429
|
+
`The schema is the remote is neither image nor file: ${sourcePath}`,
|
430
|
+
);
|
431
|
+
}
|
432
|
+
remoteFilesCounter += 1;
|
433
|
+
const bucket =
|
434
|
+
remoteFileBuckets[
|
435
|
+
remoteFilesCounter % remoteFileBuckets.length
|
436
|
+
];
|
437
|
+
if (!bucket) {
|
438
|
+
console.log(
|
439
|
+
picocolors.red("✘"),
|
440
|
+
`Internal error: could not allocate a bucket for the remote file located at ${sourcePath}`,
|
441
|
+
);
|
442
|
+
errors += 1;
|
443
|
+
continue;
|
444
|
+
}
|
445
|
+
let fileBuffer: Buffer;
|
446
|
+
try {
|
447
|
+
fileBuffer = await fs.readFile(filePath);
|
448
|
+
} catch (e) {
|
449
|
+
console.log(
|
450
|
+
picocolors.red("✘"),
|
451
|
+
`Error reading file: ${e}`,
|
452
|
+
);
|
453
|
+
errors += 1;
|
454
|
+
continue;
|
455
|
+
}
|
456
|
+
const relativeFilePath = path
|
457
|
+
.relative(projectRoot, filePath)
|
458
|
+
.split(path.sep)
|
459
|
+
.join("/") as `public/val/${string}`;
|
460
|
+
if (!relativeFilePath.startsWith("public/val/")) {
|
461
|
+
console.log(
|
462
|
+
picocolors.red("✘"),
|
463
|
+
`File path must be within the public/val/ directory (e.g. public/val/path/to/file.txt). Got: ${relativeFilePath}`,
|
464
|
+
);
|
465
|
+
errors += 1;
|
466
|
+
continue;
|
467
|
+
}
|
468
|
+
const remoteFileUpload = await uploadRemoteFile(
|
469
|
+
valRemoteHost,
|
470
|
+
fileBuffer,
|
471
|
+
publicProjectId,
|
472
|
+
bucket,
|
473
|
+
relativeFilePath,
|
474
|
+
resolveRemoteFileSchema as
|
475
|
+
| SerializedFileSchema
|
476
|
+
| SerializedImageSchema,
|
477
|
+
fileSourceMetadata,
|
478
|
+
{ pat },
|
479
|
+
);
|
480
|
+
if (!remoteFileUpload.success) {
|
481
|
+
console.log(
|
482
|
+
picocolors.red("✘"),
|
483
|
+
`Error uploading remote file: ${remoteFileUpload.error}`,
|
484
|
+
);
|
485
|
+
errors += 1;
|
486
|
+
continue;
|
487
|
+
}
|
488
|
+
console.log(
|
489
|
+
picocolors.yellow("⚠"),
|
490
|
+
`Uploaded remote file ${filePath}`,
|
491
|
+
);
|
492
|
+
remoteFiles[sourcePath as SourcePath] = {
|
493
|
+
ref: remoteFileUpload.ref,
|
494
|
+
metadata: fileSourceMetadata,
|
495
|
+
};
|
496
|
+
}
|
497
|
+
} else if (
|
498
|
+
v.fixes.includes("image:download-remote") ||
|
499
|
+
v.fixes.includes("file:download-remote")
|
500
|
+
) {
|
501
|
+
if (fix) {
|
502
|
+
console.log(
|
503
|
+
picocolors.yellow("⚠"),
|
504
|
+
`Downloading remote file in ${sourcePath}...`,
|
505
|
+
);
|
506
|
+
} else {
|
242
507
|
console.log(
|
243
508
|
picocolors.red("✘"),
|
244
|
-
|
245
|
-
`${sourcePath}`,
|
509
|
+
`Remote file ${sourcePath} needs to be downloaded (use --fix to download)`,
|
246
510
|
);
|
511
|
+
errors += 1;
|
512
|
+
continue;
|
247
513
|
}
|
514
|
+
} else if (
|
515
|
+
v.fixes.includes("image:check-remote") ||
|
516
|
+
v.fixes.includes("file:check-remote")
|
517
|
+
) {
|
518
|
+
// skip
|
248
519
|
} else {
|
249
520
|
console.log(
|
250
521
|
picocolors.red("✘"),
|
251
|
-
"
|
252
|
-
|
253
|
-
|
522
|
+
"Unknown fix",
|
523
|
+
v.fixes,
|
524
|
+
"for",
|
525
|
+
sourcePath,
|
254
526
|
);
|
255
527
|
errors += 1;
|
528
|
+
continue;
|
256
529
|
}
|
257
530
|
const fixPatch = await createFixPatch(
|
258
|
-
{ projectRoot },
|
531
|
+
{ projectRoot, remoteHost: valRemoteHost },
|
259
532
|
!!fix,
|
260
533
|
sourcePath as SourcePath,
|
261
534
|
v,
|
535
|
+
remoteFiles,
|
536
|
+
valModule.source,
|
537
|
+
valModule.schema,
|
262
538
|
);
|
263
539
|
if (fix && fixPatch?.patch && fixPatch?.patch.length > 0) {
|
264
540
|
await service.patch(moduleFilePath, fixPatch.patch);
|
265
541
|
didFix = true;
|
542
|
+
fixedErrors += 1;
|
266
543
|
console.log(
|
267
544
|
picocolors.yellow("⚠"),
|
268
545
|
"Applied fix for",
|
@@ -272,8 +549,10 @@ export async function validate({
|
|
272
549
|
fixPatch?.remainingErrors?.forEach((e) => {
|
273
550
|
errors += 1;
|
274
551
|
console.log(
|
275
|
-
|
276
|
-
|
552
|
+
e.fixes && e.fixes.length
|
553
|
+
? picocolors.yellow("⚠")
|
554
|
+
: picocolors.red("✘"),
|
555
|
+
`Got ${e.fixes && e.fixes.length ? "fixable " : ""}error in`,
|
277
556
|
`${sourcePath}:`,
|
278
557
|
e.message,
|
279
558
|
);
|
@@ -282,7 +561,7 @@ export async function validate({
|
|
282
561
|
errors += 1;
|
283
562
|
console.log(
|
284
563
|
picocolors.red("✘"),
|
285
|
-
"
|
564
|
+
"Got error in",
|
286
565
|
`${sourcePath}:`,
|
287
566
|
v.message,
|
288
567
|
);
|
@@ -290,6 +569,16 @@ export async function validate({
|
|
290
569
|
}
|
291
570
|
}
|
292
571
|
}
|
572
|
+
if (
|
573
|
+
fixedErrors === errors &&
|
574
|
+
(!valModule.errors.fatal || valModule.errors.fatal.length == 0)
|
575
|
+
) {
|
576
|
+
console.log(
|
577
|
+
picocolors.green("✔"),
|
578
|
+
moduleFilePath,
|
579
|
+
"is valid (" + (Date.now() - start) + "ms)",
|
580
|
+
);
|
581
|
+
}
|
293
582
|
for (const fatalError of valModule.errors.fatal || []) {
|
294
583
|
errors += 1;
|
295
584
|
console.log(
|
@@ -306,6 +595,13 @@ export async function validate({
|
|
306
595
|
"is valid (" + (Date.now() - start) + "ms)",
|
307
596
|
);
|
308
597
|
}
|
598
|
+
if (errors > 0) {
|
599
|
+
console.log(
|
600
|
+
picocolors.red("✘"),
|
601
|
+
`${`/${file}`} contains ${errors} error${errors > 1 ? "s" : ""}`,
|
602
|
+
" (" + (Date.now() - start) + "ms)",
|
603
|
+
);
|
604
|
+
}
|
309
605
|
return errors;
|
310
606
|
}
|
311
607
|
}
|
@@ -325,9 +621,9 @@ export async function validate({
|
|
325
621
|
if (errors > 0) {
|
326
622
|
console.log(
|
327
623
|
picocolors.red("✘"),
|
328
|
-
"
|
624
|
+
"Got",
|
329
625
|
errors,
|
330
|
-
"
|
626
|
+
"error" + (errors > 1 ? "s" : ""),
|
331
627
|
);
|
332
628
|
process.exit(1);
|
333
629
|
} else {
|