@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.
Files changed (32) hide show
  1. package/cli/dist/valbuild-cli-cli.cjs.dev.js +1471 -118
  2. package/cli/dist/valbuild-cli-cli.cjs.prod.js +1471 -118
  3. package/cli/dist/valbuild-cli-cli.esm.js +1469 -118
  4. package/package.json +6 -4
  5. package/src/__fixtures__/basic/val.config.ts +2 -2
  6. package/src/__fixtures__/basic/val.modules.ts +16 -0
  7. package/src/__fixtures__/debug-snapshot/.val/patches/11111111-1111-4111-8111-111111111111/patch.json +19 -0
  8. package/src/__fixtures__/debug-snapshot/.val/patches/22222222-2222-4222-8222-222222222222/patch.json +20 -0
  9. package/src/__fixtures__/debug-snapshot/.val/patches/head/patch.json +20 -0
  10. package/src/__fixtures__/debug-snapshot/content/projects.val.ts +23 -0
  11. package/src/__fixtures__/debug-snapshot/content/summary.ts +6 -0
  12. package/src/__fixtures__/debug-snapshot/content/tags.val.ts +10 -0
  13. package/src/__fixtures__/debug-snapshot/content/unrelated.val.ts +5 -0
  14. package/src/__fixtures__/debug-snapshot/tsconfig.json +12 -0
  15. package/src/__fixtures__/debug-snapshot/val.config.ts +5 -0
  16. package/src/__fixtures__/debug-snapshot/val.modules.ts +8 -0
  17. package/src/cli.ts +89 -2
  18. package/src/debug/context.ts +173 -0
  19. package/src/debug/importGraph.ts +126 -0
  20. package/src/debug/moduleClosure.ts +167 -0
  21. package/src/debug/report.ts +80 -0
  22. package/src/debug/snapshot.ts +497 -0
  23. package/src/debug/snapshotRoundTrip.test.ts +95 -0
  24. package/src/debug.test.ts +107 -0
  25. package/src/debug.ts +120 -0
  26. package/src/deleteUnappliablePatches.ts +139 -0
  27. package/src/listUnusedFiles.ts +16 -4
  28. package/src/runValidation.test.ts +6 -6
  29. package/src/runValidation.ts +40 -15
  30. package/src/utils/evalValConfigFile.ts +13 -5
  31. package/src/utils/sourcePathToFileLocation.ts +184 -0
  32. package/src/validate.ts +415 -154
@@ -0,0 +1,167 @@
1
+ import {
2
+ Internal,
3
+ ModuleFilePath,
4
+ SerializedSchema,
5
+ SourcePath,
6
+ } from "@valbuild/core";
7
+
8
+ export type InclusionReason =
9
+ | { type: "patched" }
10
+ | { type: "keyOf"; from: ModuleFilePath }
11
+ | { type: "referencedModule"; from: ModuleFilePath }
12
+ | { type: "router" };
13
+
14
+ /**
15
+ * The set of modules a snapshot has to carry so that it both evaluates and
16
+ * validates the same way the customer's project does.
17
+ *
18
+ * Starts from the modules the pending patches touch and closes over the
19
+ * cross-module references a schema can hold:
20
+ * - `keyOf` points at another module through its `path` (a SourcePath)
21
+ * - `image`/`file` point at a gallery module through `referencedModule`
22
+ * - route validation cross-references *every* router module, so if any
23
+ * included module has a route or router we need all of them
24
+ */
25
+ export function resolveModuleClosure(
26
+ patchedModules: ModuleFilePath[],
27
+ serializedSchemas: Record<ModuleFilePath, SerializedSchema>,
28
+ ): Map<ModuleFilePath, InclusionReason[]> {
29
+ const included = new Map<ModuleFilePath, InclusionReason[]>();
30
+ const addReason = (
31
+ moduleFilePath: ModuleFilePath,
32
+ reason: InclusionReason,
33
+ ): boolean => {
34
+ const existing = included.get(moduleFilePath);
35
+ if (existing) {
36
+ existing.push(reason);
37
+ return false;
38
+ }
39
+ included.set(moduleFilePath, [reason]);
40
+ return true;
41
+ };
42
+
43
+ const queue: ModuleFilePath[] = [];
44
+ for (const moduleFilePath of patchedModules) {
45
+ if (addReason(moduleFilePath, { type: "patched" })) {
46
+ queue.push(moduleFilePath);
47
+ }
48
+ }
49
+
50
+ let needsAllRouterModules = false;
51
+ while (queue.length > 0) {
52
+ const moduleFilePath = queue.shift();
53
+ if (moduleFilePath === undefined) {
54
+ continue;
55
+ }
56
+ const schema = serializedSchemas[moduleFilePath];
57
+ if (!schema) {
58
+ continue;
59
+ }
60
+ const refs = collectSchemaReferences(schema);
61
+ if (refs.hasRouteOrRouter) {
62
+ needsAllRouterModules = true;
63
+ }
64
+ for (const keyOfPath of refs.keyOfSourcePaths) {
65
+ const [referenced] = Internal.splitModuleFilePathAndModulePath(keyOfPath);
66
+ if (addReason(referenced, { type: "keyOf", from: moduleFilePath })) {
67
+ queue.push(referenced);
68
+ }
69
+ }
70
+ for (const referencedModule of refs.referencedModules) {
71
+ // referencedModule is a module file path written in the schema, so it is
72
+ // only trustworthy insofar as it names a module we know about.
73
+ const referenced = Object.keys(serializedSchemas).find(
74
+ (candidate) => candidate === referencedModule,
75
+ );
76
+ if (referenced === undefined) {
77
+ continue;
78
+ }
79
+ if (
80
+ addReason(referenced as ModuleFilePath, {
81
+ type: "referencedModule",
82
+ from: moduleFilePath,
83
+ })
84
+ ) {
85
+ queue.push(referenced as ModuleFilePath);
86
+ }
87
+ }
88
+ }
89
+
90
+ if (needsAllRouterModules) {
91
+ for (const [moduleFilePathS, schema] of Object.entries(serializedSchemas)) {
92
+ if (collectSchemaReferences(schema).isRouterModule) {
93
+ addReason(moduleFilePathS as ModuleFilePath, { type: "router" });
94
+ }
95
+ }
96
+ }
97
+
98
+ return included;
99
+ }
100
+
101
+ type SchemaReferences = {
102
+ keyOfSourcePaths: SourcePath[];
103
+ referencedModules: string[];
104
+ /** This module has a route field or is a router module. */
105
+ hasRouteOrRouter: boolean;
106
+ isRouterModule: boolean;
107
+ };
108
+
109
+ function collectSchemaReferences(schema: SerializedSchema): SchemaReferences {
110
+ const refs: SchemaReferences = {
111
+ keyOfSourcePaths: [],
112
+ referencedModules: [],
113
+ hasRouteOrRouter: false,
114
+ isRouterModule: false,
115
+ };
116
+ const visit = (node: SerializedSchema, isRoot: boolean) => {
117
+ switch (node.type) {
118
+ case "keyOf":
119
+ refs.keyOfSourcePaths.push(node.path);
120
+ break;
121
+ case "route":
122
+ refs.hasRouteOrRouter = true;
123
+ break;
124
+ case "file":
125
+ case "image":
126
+ if (node.referencedModule) {
127
+ refs.referencedModules.push(node.referencedModule);
128
+ }
129
+ break;
130
+ case "record":
131
+ if (node.router) {
132
+ refs.hasRouteOrRouter = true;
133
+ if (isRoot) {
134
+ refs.isRouterModule = true;
135
+ }
136
+ }
137
+ visit(node.item, false);
138
+ if (node.key) {
139
+ visit(node.key, false);
140
+ }
141
+ if (node.alt) {
142
+ visit(node.alt, false);
143
+ }
144
+ break;
145
+ case "array":
146
+ visit(node.item, false);
147
+ break;
148
+ case "object":
149
+ for (const item of Object.values(node.items)) {
150
+ visit(item, false);
151
+ }
152
+ break;
153
+ case "union":
154
+ if (typeof node.key !== "string") {
155
+ visit(node.key, false);
156
+ }
157
+ for (const item of node.items) {
158
+ visit(item, false);
159
+ }
160
+ break;
161
+ default:
162
+ break;
163
+ }
164
+ };
165
+ visit(schema, true);
166
+ return refs;
167
+ }
@@ -0,0 +1,80 @@
1
+ import pc from "picocolors";
2
+ import { ModuleFilePath, PatchId } from "@valbuild/core";
3
+ import { PreparedCommit } from "@valbuild/server";
4
+
5
+ export type PatchMetadata = {
6
+ patchId: PatchId;
7
+ path: ModuleFilePath;
8
+ createdAt: string;
9
+ authorId: string | null;
10
+ };
11
+
12
+ /**
13
+ * Prints the pending patches grouped by module, marking the ones that could not
14
+ * be applied. This is the same information `/save` returns on a 400, except it
15
+ * lists all of them rather than the first per module.
16
+ */
17
+ export function printPatchReport(
18
+ patches: PatchMetadata[],
19
+ prepared: Pick<PreparedCommit, "unappliablePatches" | "appliedPatches">,
20
+ options: { verbose?: boolean } = {},
21
+ ): void {
22
+ const byModule = new Map<ModuleFilePath, PatchMetadata[]>();
23
+ for (const patch of patches) {
24
+ const existing = byModule.get(patch.path);
25
+ if (existing) {
26
+ existing.push(patch);
27
+ } else {
28
+ byModule.set(patch.path, [patch]);
29
+ }
30
+ }
31
+ const moduleFilePaths = Array.from(byModule.keys()).sort();
32
+ for (const moduleFilePath of moduleFilePaths) {
33
+ const modulePatches = byModule.get(moduleFilePath) ?? [];
34
+ const unappliableHere = modulePatches.filter(
35
+ (patch) => prepared.unappliablePatches[patch.patchId],
36
+ );
37
+ const header = `${moduleFilePath} ${pc.dim(
38
+ `(${modulePatches.length} patch${modulePatches.length === 1 ? "" : "es"})`,
39
+ )}`;
40
+ console.log(unappliableHere.length > 0 ? pc.red(header) : pc.green(header));
41
+ for (const patch of modulePatches) {
42
+ const failure = prepared.unappliablePatches[patch.patchId];
43
+ if (!failure && !options.verbose) {
44
+ continue;
45
+ }
46
+ const who = patch.authorId ?? "unknown author";
47
+ const line = ` ${patch.patchId} ${patch.createdAt} ${who}`;
48
+ if (failure) {
49
+ console.log(pc.red(line));
50
+ for (const messageLine of failure.message.split("\n")) {
51
+ console.log(pc.red(` ${messageLine}`));
52
+ }
53
+ } else {
54
+ console.log(pc.dim(line));
55
+ }
56
+ }
57
+ }
58
+
59
+ const unappliableCount = Object.keys(prepared.unappliablePatches).length;
60
+ console.log("");
61
+ if (unappliableCount === 0) {
62
+ console.log(
63
+ pc.green(
64
+ `${patches.length} pending patch${patches.length === 1 ? "" : "es"}, all appliable.`,
65
+ ),
66
+ );
67
+ } else {
68
+ console.log(
69
+ pc.red(
70
+ `${patches.length} pending patch${patches.length === 1 ? "" : "es"}, ` +
71
+ `${unappliableCount} of which cannot be applied across ${moduleFilePaths.length} module(s).`,
72
+ ),
73
+ );
74
+ console.log(
75
+ pc.dim(
76
+ "Publishing is blocked until these are removed. See: val delete-unappliable-patches --dry-run",
77
+ ),
78
+ );
79
+ }
80
+ }