@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
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { c, s } from "../val.config";
|
|
2
|
+
|
|
3
|
+
export default c.define(
|
|
4
|
+
"/content/basic-gallery-remote-existing.val.ts",
|
|
5
|
+
s.images({
|
|
6
|
+
directory: "/public/val/images-remote2",
|
|
7
|
+
accept: "image/*",
|
|
8
|
+
remote: true,
|
|
9
|
+
}),
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
"https://remote.val.build/file/p/proj123/b/01/v/1.0.0/h/abc1/f/def4/p/public/val/images-remote2/image.png":
|
|
13
|
+
{
|
|
14
|
+
width: 1,
|
|
15
|
+
height: 1,
|
|
16
|
+
mimeType: "image/png",
|
|
17
|
+
alt: null,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { c, s } from "../val.config";
|
|
2
|
+
|
|
3
|
+
export default c.define(
|
|
4
|
+
"/content/basic-gallery-remote.val.ts",
|
|
5
|
+
s.images({
|
|
6
|
+
directory: "/public/val/images-remote",
|
|
7
|
+
accept: "image/*",
|
|
8
|
+
remote: true,
|
|
9
|
+
}),
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
"/public/val/images-remote/image.png": {
|
|
13
|
+
width: 1,
|
|
14
|
+
height: 1,
|
|
15
|
+
mimeType: "image/png",
|
|
16
|
+
alt: null,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
);
|
|
Binary file
|
|
Binary file
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { initVal } from "@valbuild/core";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// NOTE: initVal() must be called with an options object for `config` to be
|
|
4
|
+
// defined - the CLI requires val.config to export a config object.
|
|
5
|
+
const { s, c, config } = initVal({});
|
|
4
6
|
|
|
5
7
|
export { s, c, config };
|
|
@@ -13,4 +13,6 @@ export default modules(config, [
|
|
|
13
13
|
{ def: () => import("./content/basic-gallery-fail-on-non-unique-dir.val") },
|
|
14
14
|
{ def: () => import("./content/basic-gallery-missing-tracked.val") },
|
|
15
15
|
{ def: () => import("./content/basic-gallery-wrong-metadata.val") },
|
|
16
|
+
{ def: () => import("./content/basic-gallery-remote.val") },
|
|
17
|
+
{ def: () => import("./content/basic-gallery-remote-existing.val") },
|
|
16
18
|
]);
|
package/src/cli.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { getVersions } from "./getVersions";
|
|
|
6
6
|
import { connect } from "./connect";
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
import { login } from "./login";
|
|
9
|
+
import { lsp } from "./lsp";
|
|
9
10
|
import { debug } from "./debug";
|
|
10
11
|
import { deleteUnappliablePatches } from "./deleteUnappliablePatches";
|
|
11
12
|
|
|
@@ -24,6 +25,7 @@ async function main(): Promise<void> {
|
|
|
24
25
|
files
|
|
25
26
|
connect
|
|
26
27
|
versions
|
|
28
|
+
lsp
|
|
27
29
|
debug
|
|
28
30
|
delete-unappliable-patches
|
|
29
31
|
|
|
@@ -53,6 +55,13 @@ async function main(): Promise<void> {
|
|
|
53
55
|
Options:
|
|
54
56
|
--root [root], -r [root] Set project root directory (default process.cwd())
|
|
55
57
|
|
|
58
|
+
Command: lsp
|
|
59
|
+
Description: start the Val language server (used by editor integrations)
|
|
60
|
+
Options:
|
|
61
|
+
--stdio Communicate over stdin/stdout
|
|
62
|
+
--node-ipc Communicate over Node IPC
|
|
63
|
+
--socket=[port] Communicate over a socket
|
|
64
|
+
|
|
56
65
|
Command: debug
|
|
57
66
|
Description: Create a self-contained snapshot (the pending patches plus the modules they
|
|
58
67
|
touch or reference) that Val developers can replay to reproduce validation and save
|
|
@@ -83,11 +92,6 @@ async function main(): Promise<void> {
|
|
|
83
92
|
`,
|
|
84
93
|
{
|
|
85
94
|
flags: {
|
|
86
|
-
port: {
|
|
87
|
-
type: "number",
|
|
88
|
-
alias: "p",
|
|
89
|
-
default: 4123,
|
|
90
|
-
},
|
|
91
95
|
root: {
|
|
92
96
|
type: "string",
|
|
93
97
|
alias: "r",
|
|
@@ -155,6 +159,8 @@ async function main(): Promise<void> {
|
|
|
155
159
|
});
|
|
156
160
|
case "versions":
|
|
157
161
|
return versions();
|
|
162
|
+
case "lsp":
|
|
163
|
+
return lsp();
|
|
158
164
|
case "debug":
|
|
159
165
|
return debug({
|
|
160
166
|
root: flags.root,
|
package/src/connect.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import pc from "picocolors";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import {
|
|
4
|
+
import { findAndEvalValConfigFile } from "./utils/evalValConfigFile";
|
|
5
5
|
|
|
6
6
|
const host = process.env.VAL_BUILD_URL || "https://admin.val.build";
|
|
7
7
|
|
|
@@ -39,9 +39,7 @@ async function tryGetProject(projectRoot: string): Promise<{
|
|
|
39
39
|
orgName: string;
|
|
40
40
|
projectName: string;
|
|
41
41
|
} | null> {
|
|
42
|
-
const valConfigFile =
|
|
43
|
-
(await evalValConfigFile(projectRoot, "val.config.ts")) ||
|
|
44
|
-
(await evalValConfigFile(projectRoot, "val.config.js"));
|
|
42
|
+
const valConfigFile = await findAndEvalValConfigFile(projectRoot);
|
|
45
43
|
|
|
46
44
|
if (valConfigFile && valConfigFile.project) {
|
|
47
45
|
const parts = valConfigFile.project.split("/");
|
package/src/debug/context.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
ValOpsFS,
|
|
10
10
|
ValOpsHttp,
|
|
11
11
|
} from "@valbuild/server";
|
|
12
|
-
import {
|
|
12
|
+
import { findAndEvalValConfigFile } from "../utils/evalValConfigFile";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Everything the debug commands need in order to talk to the same ops the
|
|
@@ -56,9 +56,7 @@ export async function createDebugContext(options: {
|
|
|
56
56
|
if (!fs.existsSync(projectRoot)) {
|
|
57
57
|
throw new DebugContextError(`Project root does not exist: ${projectRoot}`);
|
|
58
58
|
}
|
|
59
|
-
const config =
|
|
60
|
-
(await evalValConfigFile(projectRoot, "val.config.ts")) ||
|
|
61
|
-
(await evalValConfigFile(projectRoot, "val.config.js"));
|
|
59
|
+
const config = await findAndEvalValConfigFile(projectRoot);
|
|
62
60
|
if (!config) {
|
|
63
61
|
throw new DebugContextError(
|
|
64
62
|
`Could not find val.config.ts nor val.config.js in: ${projectRoot}`,
|
package/src/listUnusedFiles.ts
CHANGED
|
@@ -8,14 +8,12 @@ import {
|
|
|
8
8
|
import { createService } from "@valbuild/server";
|
|
9
9
|
import { glob } from "fast-glob";
|
|
10
10
|
import path from "path";
|
|
11
|
-
import {
|
|
11
|
+
import { findAndEvalValConfigFile } from "./utils/evalValConfigFile";
|
|
12
12
|
|
|
13
13
|
export async function listUnusedFiles({ root }: { root?: string }) {
|
|
14
14
|
const projectRoot = root ? path.resolve(root) : process.cwd();
|
|
15
15
|
|
|
16
|
-
const valConfigFile =
|
|
17
|
-
(await evalValConfigFile(projectRoot, "val.config.ts")) ||
|
|
18
|
-
(await evalValConfigFile(projectRoot, "val.config.js"));
|
|
16
|
+
const valConfigFile = await findAndEvalValConfigFile(projectRoot);
|
|
19
17
|
// Strip the leading "/" so it is relative to the project root (e.g. "public/val").
|
|
20
18
|
const managedDir = (valConfigFile?.files?.directory ?? "/public/val").replace(
|
|
21
19
|
/^\//,
|
package/src/login.ts
CHANGED
|
@@ -1,56 +1,44 @@
|
|
|
1
1
|
import pc from "picocolors";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import {
|
|
3
|
+
awaitValLoginConfirmation,
|
|
4
|
+
persistPersonalAccessToken,
|
|
5
|
+
startValLogin,
|
|
6
|
+
ValLoginError,
|
|
7
|
+
} from "@valbuild/server";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Terminal wrapper around the login device flow in `@valbuild/server`.
|
|
11
|
+
* The flow itself (polling, token persistence) is shared with
|
|
12
|
+
* `@valbuild/language-server`.
|
|
13
|
+
*/
|
|
8
14
|
export async function login(options: { root?: string }) {
|
|
9
15
|
try {
|
|
10
16
|
console.log(pc.cyan("\nStarting login process...\n"));
|
|
11
17
|
|
|
12
|
-
|
|
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). ",
|
|
26
|
-
),
|
|
27
|
-
text
|
|
28
|
-
? `Server response: ${text} (status: ${response.status})`
|
|
29
|
-
: `Status: ${response.status}`,
|
|
30
|
-
);
|
|
31
|
-
process.exit(1);
|
|
32
|
-
}
|
|
33
|
-
const json = await response.json();
|
|
34
|
-
if (json) {
|
|
35
|
-
token = json.nonce;
|
|
36
|
-
url = json.url;
|
|
37
|
-
}
|
|
38
|
-
if (!token || !url) {
|
|
39
|
-
console.error(pc.red("Unexpected response from the server."), json);
|
|
40
|
-
process.exit(1);
|
|
41
|
-
}
|
|
18
|
+
const { nonce, url } = await startValLogin();
|
|
42
19
|
|
|
43
20
|
console.log(pc.green("Open the following URL in your browser to log in:"));
|
|
44
21
|
console.log(pc.underline(pc.blue(url)));
|
|
45
22
|
console.log(pc.dim("\nWaiting for login confirmation...\n"));
|
|
46
23
|
|
|
47
|
-
|
|
48
|
-
const result = await pollForConfirmation(token);
|
|
24
|
+
const result = await awaitValLoginConfirmation(nonce);
|
|
49
25
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
26
|
+
const filePath = persistPersonalAccessToken(
|
|
27
|
+
options.root || process.cwd(),
|
|
28
|
+
result,
|
|
29
|
+
);
|
|
30
|
+
console.log(
|
|
31
|
+
pc.green(
|
|
32
|
+
`Token for ${pc.cyan(
|
|
33
|
+
result.profile.email,
|
|
34
|
+
)} saved to ${pc.cyan(filePath)}`,
|
|
35
|
+
),
|
|
36
|
+
);
|
|
53
37
|
} catch (error) {
|
|
38
|
+
if (error instanceof ValLoginError) {
|
|
39
|
+
console.error(pc.red(error.message), error.details ?? "");
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
54
42
|
console.error(
|
|
55
43
|
pc.red(
|
|
56
44
|
"An error occurred during the login process. Check your internet connection. Details:",
|
|
@@ -60,56 +48,3 @@ export async function login(options: { root?: string }) {
|
|
|
60
48
|
process.exit(1);
|
|
61
49
|
}
|
|
62
50
|
}
|
|
63
|
-
|
|
64
|
-
const MAX_DURATION = 5 * 60 * 1000; // 5 minutes
|
|
65
|
-
async function pollForConfirmation(token: string): Promise<{
|
|
66
|
-
profile: { email: string };
|
|
67
|
-
pat: string;
|
|
68
|
-
}> {
|
|
69
|
-
const start = Date.now();
|
|
70
|
-
while (Date.now() - start < MAX_DURATION) {
|
|
71
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
72
|
-
const response = await fetch(
|
|
73
|
-
`${host}/api/login?token=${token}&consume=true`,
|
|
74
|
-
{ method: "POST" },
|
|
75
|
-
);
|
|
76
|
-
if (response.status === 500) {
|
|
77
|
-
console.error(pc.red("An error occurred on the server."));
|
|
78
|
-
process.exit(1);
|
|
79
|
-
}
|
|
80
|
-
if (response.status === 200) {
|
|
81
|
-
const json = await response.json();
|
|
82
|
-
if (json) {
|
|
83
|
-
if (
|
|
84
|
-
typeof json.profile.email === "string" &&
|
|
85
|
-
typeof json.pat === "string"
|
|
86
|
-
) {
|
|
87
|
-
return json;
|
|
88
|
-
} else {
|
|
89
|
-
console.error(pc.red("Unexpected response from the server."));
|
|
90
|
-
process.exit(1);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
console.error(pc.red("Login confirmation timed out."));
|
|
96
|
-
process.exit(1);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function saveToken(
|
|
100
|
-
result: {
|
|
101
|
-
profile: { email: string };
|
|
102
|
-
pat: string;
|
|
103
|
-
},
|
|
104
|
-
filePath: string,
|
|
105
|
-
) {
|
|
106
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
107
|
-
fs.writeFileSync(filePath, JSON.stringify(result, null, 2));
|
|
108
|
-
console.log(
|
|
109
|
-
pc.green(
|
|
110
|
-
`Token for ${pc.cyan(
|
|
111
|
-
result.profile.email,
|
|
112
|
-
)} saved to ${pc.cyan(filePath)}`,
|
|
113
|
-
),
|
|
114
|
-
);
|
|
115
|
-
}
|
package/src/lsp.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Start the Val language server.
|
|
3
|
+
*
|
|
4
|
+
* Editors normally resolve `@valbuild/language-server` from the project's
|
|
5
|
+
* `node_modules` and launch its `val-language-server` binary directly. This
|
|
6
|
+
* subcommand exists so the server is discoverable from the CLI and easy to
|
|
7
|
+
* start by hand when debugging an editor integration:
|
|
8
|
+
*
|
|
9
|
+
* npx val lsp --stdio
|
|
10
|
+
*
|
|
11
|
+
* The import is dynamic so that `vscode-languageserver` is not loaded on every
|
|
12
|
+
* `val` invocation.
|
|
13
|
+
*/
|
|
14
|
+
export async function lsp(): Promise<void> {
|
|
15
|
+
const { main } = await import("@valbuild/language-server");
|
|
16
|
+
// Transport (`--stdio`, `--node-ipc`, `--socket=`) is read from argv by the
|
|
17
|
+
// server itself, so the flags pass straight through.
|
|
18
|
+
main();
|
|
19
|
+
}
|
|
@@ -383,4 +383,105 @@ describe("runValidation", () => {
|
|
|
383
383
|
service.dispose();
|
|
384
384
|
}
|
|
385
385
|
});
|
|
386
|
+
|
|
387
|
+
test("reports upload-remote error for local entry in a remote gallery", async () => {
|
|
388
|
+
const events: ValidationEvent[] = [];
|
|
389
|
+
|
|
390
|
+
for await (const event of runValidation({
|
|
391
|
+
root: tmpDir,
|
|
392
|
+
fix: false,
|
|
393
|
+
valFiles: ["content/basic-gallery-remote.val.ts"],
|
|
394
|
+
project: undefined,
|
|
395
|
+
remote: mockRemote,
|
|
396
|
+
fs: createDefaultValFSHost(),
|
|
397
|
+
})) {
|
|
398
|
+
events.push(event);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
expect(events.at(-1)).toEqual({
|
|
402
|
+
type: "summary-errors",
|
|
403
|
+
count: expect.any(Number),
|
|
404
|
+
});
|
|
405
|
+
const errors = events.filter((e) => e.type === "validation-error");
|
|
406
|
+
expect(
|
|
407
|
+
errors.some(
|
|
408
|
+
(e) =>
|
|
409
|
+
"message" in e &&
|
|
410
|
+
(e.message as string).includes("needs to be uploaded"),
|
|
411
|
+
),
|
|
412
|
+
).toBe(true);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("uploads local entry in a remote gallery and rewrites the key when fix is true", async () => {
|
|
416
|
+
const uploadRemote: IValRemote = {
|
|
417
|
+
remoteHost: DEFAULT_VAL_REMOTE_HOST,
|
|
418
|
+
getSettings: async () => ({
|
|
419
|
+
success: true,
|
|
420
|
+
data: {
|
|
421
|
+
publicProjectId: "pubproj",
|
|
422
|
+
remoteFileBuckets: [{ bucket: "01" }],
|
|
423
|
+
},
|
|
424
|
+
}),
|
|
425
|
+
uploadFile: async () => ({ success: true }),
|
|
426
|
+
};
|
|
427
|
+
// Upload requires a personal access token on disk.
|
|
428
|
+
fs.mkdirSync(path.join(tmpDir, ".val"), { recursive: true });
|
|
429
|
+
fs.writeFileSync(
|
|
430
|
+
path.join(tmpDir, ".val", "pat.json"),
|
|
431
|
+
JSON.stringify({ pat: "test-pat" }),
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
const events: ValidationEvent[] = [];
|
|
435
|
+
for await (const event of runValidation({
|
|
436
|
+
root: tmpDir,
|
|
437
|
+
fix: true,
|
|
438
|
+
valFiles: ["content/basic-gallery-remote.val.ts"],
|
|
439
|
+
project: "test/project",
|
|
440
|
+
remote: uploadRemote,
|
|
441
|
+
fs: createDefaultValFSHost(),
|
|
442
|
+
})) {
|
|
443
|
+
events.push(event);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
expect(events.some((e) => e.type === "remote-uploaded")).toBe(true);
|
|
447
|
+
expect(events.some((e) => e.type === "fix-applied")).toBe(true);
|
|
448
|
+
|
|
449
|
+
const service = await createService(tmpDir, createDefaultValFSHost());
|
|
450
|
+
try {
|
|
451
|
+
const result = await service.get(
|
|
452
|
+
"/content/basic-gallery-remote.val.ts" as ModuleFilePath,
|
|
453
|
+
"" as ModulePath,
|
|
454
|
+
{ validate: false },
|
|
455
|
+
);
|
|
456
|
+
const source = result.source as Record<string, unknown>;
|
|
457
|
+
// The local-path key is replaced by a remote URL key (file kept on disk).
|
|
458
|
+
expect(source).not.toHaveProperty("/public/val/images-remote/image.png");
|
|
459
|
+
const keys = Object.keys(source);
|
|
460
|
+
expect(keys).toHaveLength(1);
|
|
461
|
+
expect(keys[0]).toContain("pubproj");
|
|
462
|
+
expect(keys[0]).toContain("public/val/images-remote/image.png");
|
|
463
|
+
} finally {
|
|
464
|
+
service.dispose();
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("does not flag a kept-on-disk file behind a remote gallery key", async () => {
|
|
469
|
+
const events: ValidationEvent[] = [];
|
|
470
|
+
|
|
471
|
+
for await (const event of runValidation({
|
|
472
|
+
root: tmpDir,
|
|
473
|
+
fix: false,
|
|
474
|
+
valFiles: ["content/basic-gallery-remote-existing.val.ts"],
|
|
475
|
+
project: undefined,
|
|
476
|
+
remote: mockRemote,
|
|
477
|
+
fs: createDefaultValFSHost(),
|
|
478
|
+
})) {
|
|
479
|
+
events.push(event);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// The file kept on disk under the remote URL key must not be reported as
|
|
483
|
+
// untracked, and the remote URL key must not be reported as missing.
|
|
484
|
+
expect(events.at(-1)).toEqual({ type: "summary-success" });
|
|
485
|
+
expect(events.filter((e) => e.type === "validation-error")).toHaveLength(0);
|
|
486
|
+
});
|
|
386
487
|
});
|