@valbuild/cli 0.97.3 → 0.97.4
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 +1471 -118
- package/cli/dist/valbuild-cli-cli.cjs.prod.js +1471 -118
- package/cli/dist/valbuild-cli-cli.esm.js +1469 -118
- package/package.json +6 -4
- package/src/__fixtures__/basic/val.config.ts +2 -2
- package/src/__fixtures__/basic/val.modules.ts +16 -0
- package/src/__fixtures__/debug-snapshot/.val/patches/11111111-1111-4111-8111-111111111111/patch.json +19 -0
- package/src/__fixtures__/debug-snapshot/.val/patches/22222222-2222-4222-8222-222222222222/patch.json +20 -0
- package/src/__fixtures__/debug-snapshot/.val/patches/head/patch.json +20 -0
- package/src/__fixtures__/debug-snapshot/content/projects.val.ts +23 -0
- package/src/__fixtures__/debug-snapshot/content/summary.ts +6 -0
- package/src/__fixtures__/debug-snapshot/content/tags.val.ts +10 -0
- package/src/__fixtures__/debug-snapshot/content/unrelated.val.ts +5 -0
- package/src/__fixtures__/debug-snapshot/tsconfig.json +12 -0
- package/src/__fixtures__/debug-snapshot/val.config.ts +5 -0
- package/src/__fixtures__/debug-snapshot/val.modules.ts +8 -0
- package/src/cli.ts +89 -2
- package/src/debug/context.ts +173 -0
- package/src/debug/importGraph.ts +126 -0
- package/src/debug/moduleClosure.ts +167 -0
- package/src/debug/report.ts +80 -0
- package/src/debug/snapshot.ts +497 -0
- package/src/debug/snapshotRoundTrip.test.ts +95 -0
- package/src/debug.test.ts +107 -0
- package/src/debug.ts +120 -0
- package/src/deleteUnappliablePatches.ts +139 -0
- package/src/listUnusedFiles.ts +16 -4
- package/src/runValidation.test.ts +6 -6
- package/src/runValidation.ts +40 -15
- package/src/utils/evalValConfigFile.ts +13 -5
- package/src/utils/sourcePathToFileLocation.ts +184 -0
- package/src/validate.ts +415 -154
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valbuild/cli",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.97.
|
|
4
|
+
"version": "0.97.4",
|
|
5
5
|
"description": "Val CLI tools",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -19,16 +19,18 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"chalk": "^5.6.2",
|
|
22
|
+
"chokidar": "^5.0.0",
|
|
22
23
|
"cors": "^2.8.5",
|
|
23
24
|
"fast-glob": "^3.3.3",
|
|
25
|
+
"jszip": "^3.10.1",
|
|
24
26
|
"meow": "^9.0.0",
|
|
25
27
|
"open": "^9.1.0",
|
|
26
28
|
"picocolors": "^1.1.1",
|
|
27
29
|
"zod": "^4.3.5",
|
|
28
|
-
"@valbuild/core": "0.97.
|
|
30
|
+
"@valbuild/core": "0.97.4",
|
|
29
31
|
"@valbuild/eslint-plugin": "0.93.0",
|
|
30
|
-
"@valbuild/server": "0.97.
|
|
31
|
-
"@valbuild/shared": "0.97.
|
|
32
|
+
"@valbuild/server": "0.97.4",
|
|
33
|
+
"@valbuild/shared": "0.97.4"
|
|
32
34
|
},
|
|
33
35
|
"peerDependencies": {
|
|
34
36
|
"prettier": "*",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { modules } from "@valbuild/core";
|
|
2
|
+
import { config } from "./val.config";
|
|
3
|
+
|
|
4
|
+
export default modules(config, [
|
|
5
|
+
{ def: () => import("./content/basic-valid.val") },
|
|
6
|
+
{ def: () => import("./content/basic-errors.val") },
|
|
7
|
+
{ def: () => import("./content/basic-files.val") },
|
|
8
|
+
{ def: () => import("./content/basic-image.val") },
|
|
9
|
+
{ def: () => import("./content/basic-image-from-gallery.val") },
|
|
10
|
+
{ def: () => import("./content/basic-image-from-galleries.val") },
|
|
11
|
+
{ def: () => import("./content/basic-gallery.val") },
|
|
12
|
+
{ def: () => import("./content/basic-gallery-2.val") },
|
|
13
|
+
{ def: () => import("./content/basic-gallery-fail-on-non-unique-dir.val") },
|
|
14
|
+
{ def: () => import("./content/basic-gallery-missing-tracked.val") },
|
|
15
|
+
{ def: () => import("./content/basic-gallery-wrong-metadata.val") },
|
|
16
|
+
]);
|
package/src/__fixtures__/debug-snapshot/.val/patches/11111111-1111-4111-8111-111111111111/patch.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"patch": [
|
|
3
|
+
{
|
|
4
|
+
"op": "remove",
|
|
5
|
+
"path": ["BBL", "summary", "services", "2"]
|
|
6
|
+
}
|
|
7
|
+
],
|
|
8
|
+
"patchId": "22222222-2222-4222-8222-222222222222",
|
|
9
|
+
"parentRef": {
|
|
10
|
+
"type": "patch",
|
|
11
|
+
"patchId": "11111111-1111-4111-8111-111111111111"
|
|
12
|
+
},
|
|
13
|
+
"path": "/content/projects.val.ts",
|
|
14
|
+
"authorId": "author-b",
|
|
15
|
+
"sessionId": null,
|
|
16
|
+
"baseSha": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
17
|
+
"coreVersion": null,
|
|
18
|
+
"createdAt": "2026-08-11T09:00:00.000Z"
|
|
19
|
+
}
|
package/src/__fixtures__/debug-snapshot/.val/patches/22222222-2222-4222-8222-222222222222/patch.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"patch": [
|
|
3
|
+
{
|
|
4
|
+
"op": "replace",
|
|
5
|
+
"path": ["BBL", "summary", "services", "2"],
|
|
6
|
+
"value": "Development U"
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"patchId": "33333333-3333-4333-8333-333333333333",
|
|
10
|
+
"parentRef": {
|
|
11
|
+
"type": "patch",
|
|
12
|
+
"patchId": "22222222-2222-4222-8222-222222222222"
|
|
13
|
+
},
|
|
14
|
+
"path": "/content/projects.val.ts",
|
|
15
|
+
"authorId": "author-a",
|
|
16
|
+
"sessionId": null,
|
|
17
|
+
"baseSha": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
18
|
+
"coreVersion": null,
|
|
19
|
+
"createdAt": "2026-08-11T09:00:05.000Z"
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"patch": [
|
|
3
|
+
{
|
|
4
|
+
"op": "replace",
|
|
5
|
+
"path": ["BBL", "title"],
|
|
6
|
+
"value": "BBL Housing"
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"patchId": "11111111-1111-4111-8111-111111111111",
|
|
10
|
+
"parentRef": {
|
|
11
|
+
"type": "head",
|
|
12
|
+
"headBaseSha": "0000000000000000000000000000000000000000000000000000000000000000"
|
|
13
|
+
},
|
|
14
|
+
"path": "/content/projects.val.ts",
|
|
15
|
+
"authorId": "author-a",
|
|
16
|
+
"sessionId": null,
|
|
17
|
+
"baseSha": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
18
|
+
"coreVersion": null,
|
|
19
|
+
"createdAt": "2026-08-10T12:00:00.000Z"
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { c, s } from "../val.config";
|
|
2
|
+
import tagsVal from "./tags.val";
|
|
3
|
+
import { summarySchema } from "./summary";
|
|
4
|
+
|
|
5
|
+
export default c.define(
|
|
6
|
+
"/content/projects.val.ts",
|
|
7
|
+
s.record(
|
|
8
|
+
s.object({
|
|
9
|
+
title: s.string(),
|
|
10
|
+
tag: s.keyOf(tagsVal),
|
|
11
|
+
summary: summarySchema,
|
|
12
|
+
}),
|
|
13
|
+
),
|
|
14
|
+
{
|
|
15
|
+
BBL: {
|
|
16
|
+
title: "BBL",
|
|
17
|
+
tag: "design",
|
|
18
|
+
summary: {
|
|
19
|
+
services: ["Strategy", "Design", "Development"],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { modules } from "@valbuild/core";
|
|
2
|
+
import { config } from "./val.config";
|
|
3
|
+
|
|
4
|
+
export default modules(config, [
|
|
5
|
+
{ def: () => import("./content/projects.val") },
|
|
6
|
+
{ def: () => import("./content/tags.val") },
|
|
7
|
+
{ def: () => import("./content/unrelated.val") },
|
|
8
|
+
]);
|
package/src/cli.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { getVersions } from "./getVersions";
|
|
|
6
6
|
import { connect } from "./connect";
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
import { login } from "./login";
|
|
9
|
+
import { debug } from "./debug";
|
|
10
|
+
import { deleteUnappliablePatches } from "./deleteUnappliablePatches";
|
|
9
11
|
|
|
10
12
|
async function main(): Promise<void> {
|
|
11
13
|
const { input, flags, showHelp } = meow(
|
|
@@ -22,12 +24,15 @@ async function main(): Promise<void> {
|
|
|
22
24
|
files
|
|
23
25
|
connect
|
|
24
26
|
versions
|
|
25
|
-
|
|
27
|
+
debug
|
|
28
|
+
delete-unappliable-patches
|
|
29
|
+
|
|
26
30
|
Command: validate
|
|
27
31
|
Description: val-idate val modules
|
|
28
32
|
Options:
|
|
29
33
|
--root [root], -r [root] Set project root directory (default process.cwd())
|
|
30
34
|
--fix [fix] Attempt to fix validation errors
|
|
35
|
+
--watch, -w Re-validate on changes to val.config, val.modules and *.val files
|
|
31
36
|
|
|
32
37
|
|
|
33
38
|
Command: login
|
|
@@ -43,10 +48,38 @@ async function main(): Promise<void> {
|
|
|
43
48
|
|
|
44
49
|
Command: list-unused-files
|
|
45
50
|
Description: EXPERIMENTAL.
|
|
46
|
-
List files that are in public/val but not in use by any Val module.
|
|
51
|
+
List files that are in the configured files directory (files.directory, default public/val) but not in use by any Val module.
|
|
47
52
|
This is useful for cleaning up unused files.
|
|
48
53
|
Options:
|
|
49
54
|
--root [root], -r [root] Set project root directory (default process.cwd())
|
|
55
|
+
|
|
56
|
+
Command: debug
|
|
57
|
+
Description: Create a self-contained snapshot (the pending patches plus the modules they
|
|
58
|
+
touch or reference) that Val developers can replay to reproduce validation and save
|
|
59
|
+
errors. Read-only.
|
|
60
|
+
Options:
|
|
61
|
+
--root [root], -r [root] Set project root directory (default process.cwd())
|
|
62
|
+
--out [file] Output zip (default ./val-debug-<branch>-<commit>-<timestamp>.zip)
|
|
63
|
+
--remote Read the patches from the hosted project instead of <root>/.val
|
|
64
|
+
(requires "val login"; implied by VAL_API_KEY)
|
|
65
|
+
--commit [sha] Commit to read module sources at (default VAL_GIT_COMMIT, else git HEAD)
|
|
66
|
+
--branch [name] Branch (default VAL_GIT_BRANCH, else the current git branch)
|
|
67
|
+
--include-files Also download the binary files the patches reference
|
|
68
|
+
--verbose List every pending patch, not just the failing ones
|
|
69
|
+
|
|
70
|
+
Command: delete-unappliable-patches
|
|
71
|
+
Description: Delete the pending patches that cannot be applied. This is what unblocks a
|
|
72
|
+
publish failing with "Failed to create commit". Capture a "val debug" snapshot first:
|
|
73
|
+
deleting discards the changes those patches contain.
|
|
74
|
+
Options:
|
|
75
|
+
--root [root], -r [root] Set project root directory (default process.cwd())
|
|
76
|
+
--remote Operate on the hosted project instead of <root>/.val
|
|
77
|
+
(requires "val login"; implied by VAL_API_KEY)
|
|
78
|
+
--commit [sha] Commit to read module sources at (default VAL_GIT_COMMIT, else git HEAD)
|
|
79
|
+
--branch [name] Branch (default VAL_GIT_BRANCH, else the current git branch)
|
|
80
|
+
--dry-run Only list what would be deleted
|
|
81
|
+
--yes Do not ask for confirmation
|
|
82
|
+
--verbose List every pending patch, not just the failing ones
|
|
50
83
|
`,
|
|
51
84
|
{
|
|
52
85
|
flags: {
|
|
@@ -62,12 +95,40 @@ async function main(): Promise<void> {
|
|
|
62
95
|
fix: {
|
|
63
96
|
type: "boolean",
|
|
64
97
|
},
|
|
98
|
+
watch: {
|
|
99
|
+
type: "boolean",
|
|
100
|
+
alias: "w",
|
|
101
|
+
},
|
|
65
102
|
noEslint: {
|
|
66
103
|
type: "boolean",
|
|
67
104
|
},
|
|
68
105
|
managedDir: {
|
|
69
106
|
type: "string",
|
|
70
107
|
},
|
|
108
|
+
out: {
|
|
109
|
+
type: "string",
|
|
110
|
+
},
|
|
111
|
+
commit: {
|
|
112
|
+
type: "string",
|
|
113
|
+
},
|
|
114
|
+
branch: {
|
|
115
|
+
type: "string",
|
|
116
|
+
},
|
|
117
|
+
includeFiles: {
|
|
118
|
+
type: "boolean",
|
|
119
|
+
},
|
|
120
|
+
remote: {
|
|
121
|
+
type: "boolean",
|
|
122
|
+
},
|
|
123
|
+
dryRun: {
|
|
124
|
+
type: "boolean",
|
|
125
|
+
},
|
|
126
|
+
yes: {
|
|
127
|
+
type: "boolean",
|
|
128
|
+
},
|
|
129
|
+
verbose: {
|
|
130
|
+
type: "boolean",
|
|
131
|
+
},
|
|
71
132
|
},
|
|
72
133
|
hardRejection: false,
|
|
73
134
|
},
|
|
@@ -94,6 +155,26 @@ async function main(): Promise<void> {
|
|
|
94
155
|
});
|
|
95
156
|
case "versions":
|
|
96
157
|
return versions();
|
|
158
|
+
case "debug":
|
|
159
|
+
return debug({
|
|
160
|
+
root: flags.root,
|
|
161
|
+
out: flags.out,
|
|
162
|
+
commit: flags.commit,
|
|
163
|
+
branch: flags.branch,
|
|
164
|
+
remote: flags.remote,
|
|
165
|
+
includeFiles: flags.includeFiles,
|
|
166
|
+
verbose: flags.verbose,
|
|
167
|
+
});
|
|
168
|
+
case "delete-unappliable-patches":
|
|
169
|
+
return deleteUnappliablePatches({
|
|
170
|
+
root: flags.root,
|
|
171
|
+
commit: flags.commit,
|
|
172
|
+
branch: flags.branch,
|
|
173
|
+
remote: flags.remote,
|
|
174
|
+
dryRun: flags.dryRun,
|
|
175
|
+
yes: flags.yes,
|
|
176
|
+
verbose: flags.verbose,
|
|
177
|
+
});
|
|
97
178
|
case "login":
|
|
98
179
|
return login({
|
|
99
180
|
root: flags.root,
|
|
@@ -112,9 +193,15 @@ async function main(): Promise<void> {
|
|
|
112
193
|
if (flags.managedDir) {
|
|
113
194
|
return error(`Command "validate" does not support --managedDir flag`);
|
|
114
195
|
}
|
|
196
|
+
if (flags.watch && flags.fix) {
|
|
197
|
+
return error(
|
|
198
|
+
`Command "validate" does not support --watch together with --fix`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
115
201
|
return validate({
|
|
116
202
|
root: flags.root,
|
|
117
203
|
fix: flags.fix,
|
|
204
|
+
watch: flags.watch,
|
|
118
205
|
});
|
|
119
206
|
default:
|
|
120
207
|
return error(`Unknown command "${input.join(" ")}"`);
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { DEFAULT_CONTENT_HOST, ValConfig, ValModules } from "@valbuild/core";
|
|
4
|
+
import {
|
|
5
|
+
getPersonalAccessTokenPath,
|
|
6
|
+
loadValModules,
|
|
7
|
+
parsePersonalAccessTokenFile,
|
|
8
|
+
safeReadGit,
|
|
9
|
+
ValOpsFS,
|
|
10
|
+
ValOpsHttp,
|
|
11
|
+
} from "@valbuild/server";
|
|
12
|
+
import { evalValConfigFile } from "../utils/evalValConfigFile";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Everything the debug commands need in order to talk to the same ops the
|
|
16
|
+
* running app talks to.
|
|
17
|
+
*
|
|
18
|
+
* Mirrors how the app itself decides between fs and http mode in
|
|
19
|
+
* `initHandlerOptions` (packages/server/src/ValRouter.ts): a project name plus
|
|
20
|
+
* credentials means the patches live in the hosted content service, otherwise
|
|
21
|
+
* they are on disk under `<root>/.val`.
|
|
22
|
+
*/
|
|
23
|
+
export type DebugContext = {
|
|
24
|
+
projectRoot: string;
|
|
25
|
+
valModules: ValModules;
|
|
26
|
+
config: ValConfig;
|
|
27
|
+
mode: "fs" | "http";
|
|
28
|
+
project: string | null;
|
|
29
|
+
branch: string | null;
|
|
30
|
+
commit: string | null;
|
|
31
|
+
/** How we authenticated, for the snapshot manifest. Never the secret itself. */
|
|
32
|
+
authKind: "pat" | "api-key" | "none";
|
|
33
|
+
contentUrl: string;
|
|
34
|
+
filesDirectory: string;
|
|
35
|
+
serverOps: ValOpsFS | ValOpsHttp;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export class DebugContextError extends Error {}
|
|
39
|
+
|
|
40
|
+
export async function createDebugContext(options: {
|
|
41
|
+
root?: string;
|
|
42
|
+
commit?: string;
|
|
43
|
+
branch?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Read patches from the hosted content service rather than `<root>/.val`.
|
|
46
|
+
*
|
|
47
|
+
* Has to be explicit: whether a project's patches live remotely depends on
|
|
48
|
+
* how the *deployed* app is configured (proxy mode), which a local checkout
|
|
49
|
+
* cannot know. A `val login` token says nothing about it either - it is a user
|
|
50
|
+
* credential, not a signal. We default to fs mode and let VAL_API_KEY (the
|
|
51
|
+
* same thing that puts the app in proxy mode) opt in automatically.
|
|
52
|
+
*/
|
|
53
|
+
remote?: boolean;
|
|
54
|
+
}): Promise<DebugContext> {
|
|
55
|
+
const projectRoot = options.root ? path.resolve(options.root) : process.cwd();
|
|
56
|
+
if (!fs.existsSync(projectRoot)) {
|
|
57
|
+
throw new DebugContextError(`Project root does not exist: ${projectRoot}`);
|
|
58
|
+
}
|
|
59
|
+
const config =
|
|
60
|
+
(await evalValConfigFile(projectRoot, "val.config.ts")) ||
|
|
61
|
+
(await evalValConfigFile(projectRoot, "val.config.js"));
|
|
62
|
+
if (!config) {
|
|
63
|
+
throw new DebugContextError(
|
|
64
|
+
`Could not find val.config.ts nor val.config.js in: ${projectRoot}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const valModules = loadValModules(projectRoot);
|
|
68
|
+
const contentUrl = process.env.VAL_CONTENT_URL || DEFAULT_CONTENT_HOST;
|
|
69
|
+
const filesDirectory = config.files?.directory ?? "/public/val";
|
|
70
|
+
const project = config.project || process.env.VAL_PROJECT || null;
|
|
71
|
+
const git = await safeReadGit(projectRoot);
|
|
72
|
+
const branch =
|
|
73
|
+
options.branch ||
|
|
74
|
+
config.gitBranch ||
|
|
75
|
+
process.env.VAL_GIT_BRANCH ||
|
|
76
|
+
git.branch ||
|
|
77
|
+
null;
|
|
78
|
+
const commit =
|
|
79
|
+
options.commit ||
|
|
80
|
+
config.gitCommit ||
|
|
81
|
+
process.env.VAL_GIT_COMMIT ||
|
|
82
|
+
git.commit ||
|
|
83
|
+
null;
|
|
84
|
+
|
|
85
|
+
const wantsRemote = options.remote || !!process.env.VAL_API_KEY;
|
|
86
|
+
const auth = wantsRemote ? readAuth(projectRoot) : null;
|
|
87
|
+
if (wantsRemote) {
|
|
88
|
+
if (!project) {
|
|
89
|
+
throw new DebugContextError(
|
|
90
|
+
"Cannot read remote patches: no project is configured.\n" +
|
|
91
|
+
"Set 'project' in val.config, or the VAL_PROJECT env var.",
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (!auth) {
|
|
95
|
+
throw new DebugContextError(
|
|
96
|
+
"Cannot read remote patches: you are not logged in.\n\n\tnpx val login\n\n" +
|
|
97
|
+
"(or set the VAL_API_KEY env var)",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (!branch) {
|
|
101
|
+
throw new DebugContextError(
|
|
102
|
+
"Could not determine the branch. Pass --branch, or set VAL_GIT_BRANCH.",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (!commit) {
|
|
106
|
+
throw new DebugContextError(
|
|
107
|
+
"Could not determine the commit. Pass --commit, or set VAL_GIT_COMMIT.\n" +
|
|
108
|
+
"This must be the commit the app was deployed from: it is the commit the module sources are read at.",
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
projectRoot,
|
|
113
|
+
valModules,
|
|
114
|
+
config,
|
|
115
|
+
mode: "http",
|
|
116
|
+
project,
|
|
117
|
+
branch,
|
|
118
|
+
commit,
|
|
119
|
+
authKind: "pat" in auth ? "pat" : "api-key",
|
|
120
|
+
contentUrl,
|
|
121
|
+
filesDirectory,
|
|
122
|
+
serverOps: new ValOpsHttp(
|
|
123
|
+
contentUrl,
|
|
124
|
+
project,
|
|
125
|
+
commit,
|
|
126
|
+
branch,
|
|
127
|
+
auth,
|
|
128
|
+
valModules,
|
|
129
|
+
{ root: config.root, config },
|
|
130
|
+
),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
projectRoot,
|
|
135
|
+
valModules,
|
|
136
|
+
config,
|
|
137
|
+
mode: "fs",
|
|
138
|
+
project,
|
|
139
|
+
branch,
|
|
140
|
+
commit,
|
|
141
|
+
authKind: "none",
|
|
142
|
+
contentUrl,
|
|
143
|
+
filesDirectory,
|
|
144
|
+
serverOps: new ValOpsFS(contentUrl, projectRoot, valModules, { config }),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The personal access token written by `val login`, falling back to the api key
|
|
150
|
+
* the app itself uses. Reading the pat file is the same dance as
|
|
151
|
+
* `resolveRemoteFile` in runValidation.ts.
|
|
152
|
+
*/
|
|
153
|
+
function readAuth(
|
|
154
|
+
projectRoot: string,
|
|
155
|
+
): { pat: string } | { apiKey: string } | null {
|
|
156
|
+
const patFile = getPersonalAccessTokenPath(projectRoot);
|
|
157
|
+
if (fs.existsSync(patFile)) {
|
|
158
|
+
const contents = fs.readFileSync(patFile, "utf-8");
|
|
159
|
+
const parsed = parsePersonalAccessTokenFile(contents);
|
|
160
|
+
if (parsed.success) {
|
|
161
|
+
return { pat: parsed.data.pat };
|
|
162
|
+
}
|
|
163
|
+
throw new DebugContextError(
|
|
164
|
+
`Could not parse the personal access token at ${patFile}: ${parsed.error}.\n` +
|
|
165
|
+
`Log in again:\n\n\tnpx val login`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
const apiKey = process.env.VAL_API_KEY;
|
|
169
|
+
if (apiKey) {
|
|
170
|
+
return { apiKey };
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
|
|
4
|
+
const RESOLVE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".cjs", ".mjs"];
|
|
5
|
+
|
|
6
|
+
export type ImportGraphResult = {
|
|
7
|
+
/** Project-relative path (leading "/") -> file contents. */
|
|
8
|
+
files: Record<string, string>;
|
|
9
|
+
/**
|
|
10
|
+
* Specifiers we could not resolve to a project file: bare packages (fine,
|
|
11
|
+
* they come from node_modules) and tsconfig path aliases (NOT fine - the
|
|
12
|
+
* snapshot will not evaluate). Recorded so the manifest can say so out loud
|
|
13
|
+
* rather than shipping a bundle that mysteriously fails to load.
|
|
14
|
+
*/
|
|
15
|
+
unresolved: { from: string; specifier: string }[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Walks the relative-import graph of `entryFiles` and reads every project file
|
|
20
|
+
* it reaches, using `readFile` so the contents come from the same revision as
|
|
21
|
+
* the modules themselves (the deployed commit in http mode).
|
|
22
|
+
*
|
|
23
|
+
* `loadValModules` evaluates val.modules.ts and everything it imports in a vm,
|
|
24
|
+
* so a snapshot that is missing an imported file (a shared schema fragment, or
|
|
25
|
+
* val.config itself) cannot be replayed at all.
|
|
26
|
+
*/
|
|
27
|
+
export async function collectImportedProjectFiles(
|
|
28
|
+
entryFiles: { path: string; contents: string }[],
|
|
29
|
+
readFile: (projectRelativePath: string) => Promise<string | null>,
|
|
30
|
+
): Promise<ImportGraphResult> {
|
|
31
|
+
const files: Record<string, string> = {};
|
|
32
|
+
const unresolved: { from: string; specifier: string }[] = [];
|
|
33
|
+
const seen = new Set<string>();
|
|
34
|
+
const queue: { path: string; contents: string }[] = [];
|
|
35
|
+
|
|
36
|
+
for (const entry of entryFiles) {
|
|
37
|
+
seen.add(entry.path);
|
|
38
|
+
queue.push(entry);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
while (queue.length > 0) {
|
|
42
|
+
const current = queue.shift();
|
|
43
|
+
if (current === undefined) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const dir = path.posix.dirname(current.path);
|
|
47
|
+
for (const specifier of readImportSpecifiers(
|
|
48
|
+
current.path,
|
|
49
|
+
current.contents,
|
|
50
|
+
)) {
|
|
51
|
+
if (!specifier.startsWith(".")) {
|
|
52
|
+
unresolved.push({ from: current.path, specifier });
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const base = path.posix.resolve(dir, specifier);
|
|
56
|
+
const resolved = await resolveProjectFile(base, readFile);
|
|
57
|
+
if (!resolved) {
|
|
58
|
+
unresolved.push({ from: current.path, specifier });
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (seen.has(resolved.path)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
seen.add(resolved.path);
|
|
65
|
+
files[resolved.path] = resolved.contents;
|
|
66
|
+
queue.push(resolved);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { files, unresolved };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function resolveProjectFile(
|
|
74
|
+
base: string,
|
|
75
|
+
readFile: (projectRelativePath: string) => Promise<string | null>,
|
|
76
|
+
): Promise<{ path: string; contents: string } | null> {
|
|
77
|
+
const candidates = [
|
|
78
|
+
base,
|
|
79
|
+
...RESOLVE_EXTENSIONS.map((ext) => base + ext),
|
|
80
|
+
...RESOLVE_EXTENSIONS.map((ext) => path.posix.join(base, "index" + ext)),
|
|
81
|
+
];
|
|
82
|
+
for (const candidate of candidates) {
|
|
83
|
+
const contents = await readFile(candidate);
|
|
84
|
+
if (contents !== null) {
|
|
85
|
+
return { path: candidate, contents };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Every module specifier in the file: static imports/exports and the dynamic
|
|
93
|
+
* `import()` calls val.modules.ts is built out of.
|
|
94
|
+
*/
|
|
95
|
+
function readImportSpecifiers(filePath: string, contents: string): string[] {
|
|
96
|
+
const sourceFile = ts.createSourceFile(
|
|
97
|
+
filePath,
|
|
98
|
+
contents,
|
|
99
|
+
ts.ScriptTarget.ES2020,
|
|
100
|
+
true,
|
|
101
|
+
);
|
|
102
|
+
const specifiers: string[] = [];
|
|
103
|
+
const visit = (node: ts.Node) => {
|
|
104
|
+
if (
|
|
105
|
+
(ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
|
|
106
|
+
node.moduleSpecifier &&
|
|
107
|
+
ts.isStringLiteral(node.moduleSpecifier)
|
|
108
|
+
) {
|
|
109
|
+
specifiers.push(node.moduleSpecifier.text);
|
|
110
|
+
} else if (
|
|
111
|
+
ts.isCallExpression(node) &&
|
|
112
|
+
(node.expression.kind === ts.SyntaxKind.ImportKeyword ||
|
|
113
|
+
(ts.isIdentifier(node.expression) &&
|
|
114
|
+
node.expression.text === "require")) &&
|
|
115
|
+
node.arguments.length > 0
|
|
116
|
+
) {
|
|
117
|
+
const arg = node.arguments[0];
|
|
118
|
+
if (ts.isStringLiteral(arg)) {
|
|
119
|
+
specifiers.push(arg.text);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
ts.forEachChild(node, visit);
|
|
123
|
+
};
|
|
124
|
+
visit(sourceFile);
|
|
125
|
+
return specifiers;
|
|
126
|
+
}
|