@vzn/vx 0.0.203 → 0.0.205
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/README.md +1 -1
- package/package.json +5 -5
- package/src/cli/select.ts +7 -3
- package/src/cli/show.ts +2 -3
- package/src/cli/task-verb.ts +10 -5
- package/src/cli/watch.ts +3 -4
- package/src/cli/workspace-config.ts +11 -1
- package/src/config.ts +2 -0
- package/src/index.ts +2 -0
- package/src/orchestrator/doctor.ts +2 -3
- package/src/orchestrator/index.ts +3 -0
- package/src/orchestrator/plugin-host.ts +30 -1
- package/src/orchestrator/plugin.ts +23 -0
- package/src/orchestrator/prepare.ts +13 -9
- package/src/orchestrator/projects.ts +30 -2
- package/src/workspace/index.ts +2 -0
- package/src/workspace/workspace.ts +60 -0
package/README.md
CHANGED
|
@@ -216,7 +216,7 @@ every commit. The schema may still change; what 1.0 will promise is in
|
|
|
216
216
|
| Surface | Maturity |
|
|
217
217
|
| -------------------------------------------------- | -------------------- |
|
|
218
218
|
| Core task runner + caching | **production-ready** |
|
|
219
|
-
| Plugin pipeline (
|
|
219
|
+
| Plugin pipeline (14 hooks, `commands` included) | **shippable** |
|
|
220
220
|
| `vx init` / `@vzn/vx-migrate` (scripts; Turbo, Nx) | **shippable** |
|
|
221
221
|
| First-party plugins (table above) | **shippable** |
|
|
222
222
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.205",
|
|
4
4
|
"description": "A fast, correct task runner and build cache for JavaScript monorepos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"node": ">=18"
|
|
18
18
|
},
|
|
19
19
|
"optionalDependencies": {
|
|
20
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
21
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
22
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
23
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
20
|
+
"@vzn/vx-linux-x64": "0.0.205",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.205",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.205",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.205"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cli/select.ts
CHANGED
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
type LoadReads,
|
|
16
16
|
type FingerprintClaims,
|
|
17
17
|
FROZEN_WITHOUT_LOCK,
|
|
18
|
-
listProjects,
|
|
19
18
|
loadProjectConfig,
|
|
20
19
|
loadWorkspace,
|
|
21
20
|
parseFilter,
|
|
@@ -28,7 +27,12 @@ import type { ProjectEntry } from '../workspace/index.js'
|
|
|
28
27
|
import { parseDependencySpec } from '../graph/index.js'
|
|
29
28
|
import { nearest, UserError } from '../util/index.js'
|
|
30
29
|
import { claimedAffected, fingerprintClaims } from '../orchestrator/index.js'
|
|
31
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
type CliLoadOptions,
|
|
32
|
+
discoverCliProjects,
|
|
33
|
+
loadCliProjects,
|
|
34
|
+
loadCliWorkspace,
|
|
35
|
+
} from './workspace-config.js'
|
|
32
36
|
|
|
33
37
|
/**
|
|
34
38
|
* The projects whose tasks declare a `cache.inputs.workspaceFiles` glob
|
|
@@ -110,7 +114,7 @@ async function workspaceFingerprintClaims(
|
|
|
110
114
|
async function loadWorkspaceProjects(cwd: string): Promise<ProjectMeta[]> {
|
|
111
115
|
const reads: LoadReads = new Map()
|
|
112
116
|
const root = await findWorkspaceRoot(cwd, reads)
|
|
113
|
-
return await
|
|
117
|
+
return await discoverCliProjects(await loadWorkspace(root, reads))
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
export async function findCwdProject(cwd: string): Promise<string | null> {
|
package/src/cli/show.ts
CHANGED
|
@@ -10,11 +10,10 @@ import type { ProjectConfig, TaskConfig } from '../config.js'
|
|
|
10
10
|
import { declaredTask } from '../graph/index.js'
|
|
11
11
|
import { flagHint, seeHelp } from './help.js'
|
|
12
12
|
import { nearMatches, relPosix, secretMask, UserError } from '../util/index.js'
|
|
13
|
-
import { loadCliProjects } from './workspace-config.js'
|
|
13
|
+
import { discoverCliProjects, loadCliProjects } from './workspace-config.js'
|
|
14
14
|
import {
|
|
15
15
|
findWorkspaceRoot,
|
|
16
16
|
type LoadReads,
|
|
17
|
-
listProjects,
|
|
18
17
|
loadWorkspace,
|
|
19
18
|
type ProjectEntry,
|
|
20
19
|
type ProjectMeta,
|
|
@@ -60,7 +59,7 @@ export async function showCmd(args: readonly string[]): Promise<number> {
|
|
|
60
59
|
}
|
|
61
60
|
const reads: LoadReads = new Map()
|
|
62
61
|
const root = await findWorkspaceRoot(process.cwd(), reads)
|
|
63
|
-
const metas = await
|
|
62
|
+
const metas = await discoverCliProjects(await loadWorkspace(root, reads))
|
|
64
63
|
const byName = new Map(metas.map((m) => [m.name, m]))
|
|
65
64
|
|
|
66
65
|
const hashAt = parsed.target?.indexOf('#') ?? -1
|
package/src/cli/task-verb.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
// name would change meaning the day a plugin declares that verb.
|
|
6
6
|
|
|
7
7
|
import { nxProjectTarget } from '../orchestrator/index.js'
|
|
8
|
-
import { findWorkspaceRoot,
|
|
8
|
+
import { findWorkspaceRoot, loadWorkspace } from '../workspace/index.js'
|
|
9
9
|
import { findCwdProject } from './select.js'
|
|
10
|
-
import { loadCliProjects } from './workspace-config.js'
|
|
10
|
+
import { discoverCliProjects, loadCliProjects } from './workspace-config.js'
|
|
11
11
|
|
|
12
12
|
/** The `vx run` a task typed as a verb means, or null when no project declares it. */
|
|
13
13
|
export async function taskVerbHint(
|
|
@@ -64,9 +64,14 @@ async function workspaceProjects(
|
|
|
64
64
|
): Promise<Awaited<ReturnType<typeof loadCliProjects>> | null> {
|
|
65
65
|
try {
|
|
66
66
|
const root = await findWorkspaceRoot(cwd)
|
|
67
|
-
return await loadCliProjects(
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
return await loadCliProjects(
|
|
68
|
+
root,
|
|
69
|
+
await discoverCliProjects(await loadWorkspace(root)),
|
|
70
|
+
'all',
|
|
71
|
+
{
|
|
72
|
+
noCreate: true,
|
|
73
|
+
},
|
|
74
|
+
)
|
|
70
75
|
} catch {
|
|
71
76
|
// No workspace, or a config that does not load: the caller's plain line stands.
|
|
72
77
|
return null
|
package/src/cli/watch.ts
CHANGED
|
@@ -26,13 +26,12 @@ import {
|
|
|
26
26
|
import {
|
|
27
27
|
findWorkspaceRoot,
|
|
28
28
|
type LoadReads,
|
|
29
|
-
listProjects,
|
|
30
29
|
loadWorkspace,
|
|
31
30
|
LOCKFILE_NAME,
|
|
32
31
|
memberBaseDirs,
|
|
33
32
|
type ProjectMeta,
|
|
34
33
|
} from '../workspace/index.js'
|
|
35
|
-
import { type CliLoadOptions, loadCliWorkspace } from './workspace-config.js'
|
|
34
|
+
import { type CliLoadOptions, discoverCliProjects, loadCliWorkspace } from './workspace-config.js'
|
|
36
35
|
import {
|
|
37
36
|
isIgnoredWatchPath,
|
|
38
37
|
isWorkspaceConfigFile,
|
|
@@ -148,7 +147,7 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
148
147
|
const reads: LoadReads = new Map()
|
|
149
148
|
const workspaceRoot = await findWorkspaceRoot(cwd, reads)
|
|
150
149
|
const workspace = await loadWorkspace(workspaceRoot, reads)
|
|
151
|
-
const allProjects = await
|
|
150
|
+
const allProjects = await discoverCliProjects(workspace)
|
|
152
151
|
const inScope = (all: readonly ProjectMeta[]): ProjectMeta[] =>
|
|
153
152
|
opts.projects === undefined ? [...all] : all.filter((p) => opts.projects!.includes(p.name))
|
|
154
153
|
const scope = inScope(allProjects)
|
|
@@ -226,7 +225,7 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
226
225
|
// dependency of it.
|
|
227
226
|
rediscover: async () => {
|
|
228
227
|
const workspace = await loadWorkspace(workspaceRoot)
|
|
229
|
-
const all = await
|
|
228
|
+
const all = await discoverCliProjects(workspace)
|
|
230
229
|
const sweep = await sweepConfigs(all, workspaceRoot, load, opts.tasks)
|
|
231
230
|
const now = await watchedProjects(workspaceRoot, all, inScope(all), load, sweep.staged)
|
|
232
231
|
return {
|
|
@@ -9,12 +9,13 @@ import path from 'node:path'
|
|
|
9
9
|
import type { WorkspaceConfig } from '../config.js'
|
|
10
10
|
import { UserError } from '../util/index.js'
|
|
11
11
|
import { Cache, noteSchemaReset } from '../cache/index.js'
|
|
12
|
-
import { loadProjects, loadWorkspacePlugins } from '../orchestrator/index.js'
|
|
12
|
+
import { discoverProjects, loadProjects, loadWorkspacePlugins } from '../orchestrator/index.js'
|
|
13
13
|
import type { VxPlugin } from '../orchestrator/index.js'
|
|
14
14
|
import {
|
|
15
15
|
buildPackageGraph,
|
|
16
16
|
computeWorkspaceFingerprint,
|
|
17
17
|
FROZEN_WITHOUT_LOCK,
|
|
18
|
+
type Workspace,
|
|
18
19
|
type ProjectEntry,
|
|
19
20
|
type ProjectMeta,
|
|
20
21
|
readLockfile,
|
|
@@ -36,6 +37,15 @@ export async function loadCliWorkspace(workspaceRoot: string): Promise<CliWorksp
|
|
|
36
37
|
return { workspaceConfig, plugins, cacheDir: resolveCacheDir(workspaceRoot, workspaceConfig) }
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The workspace's projects as a run discovers them: core's, then the
|
|
42
|
+
* plugin `discover` stage, with the cache dir the run uses.
|
|
43
|
+
*/
|
|
44
|
+
export async function discoverCliProjects(workspace: Workspace): Promise<ProjectMeta[]> {
|
|
45
|
+
const { plugins, cacheDir } = await loadCliWorkspace(workspace.root)
|
|
46
|
+
return await discoverProjects(workspace, plugins, cacheDir, warnToStderr)
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
/**
|
|
40
50
|
* `--cache-dir <path>` / `--cache-dir=<path>` for a verb that reads the
|
|
41
51
|
* cache a run wrote — `why`, `last`, `info`, `cache prune`. Same rules as
|
package/src/config.ts
CHANGED
|
@@ -56,6 +56,7 @@ export interface Plugin {
|
|
|
56
56
|
/** The package name — set by `definePlugin`, never by the plugin. */
|
|
57
57
|
readonly name: string
|
|
58
58
|
config?(workspace: unknown, ctx: unknown): unknown
|
|
59
|
+
discover?(ctx: unknown): unknown
|
|
59
60
|
project?(config: unknown, ctx: unknown): unknown
|
|
60
61
|
graph?(nodes: unknown, ctx: unknown): unknown
|
|
61
62
|
key?(task: unknown, ctx: unknown): unknown
|
|
@@ -80,6 +81,7 @@ export interface Plugin {
|
|
|
80
81
|
*/
|
|
81
82
|
export const PLUGIN_HOOKS = [
|
|
82
83
|
'config',
|
|
84
|
+
'discover',
|
|
83
85
|
'project',
|
|
84
86
|
'graph',
|
|
85
87
|
'key',
|
package/src/index.ts
CHANGED
|
@@ -161,6 +161,8 @@ export type {
|
|
|
161
161
|
ScheduleHookContext,
|
|
162
162
|
AdmitContext,
|
|
163
163
|
WorkspaceHookContext,
|
|
164
|
+
DiscoverContext,
|
|
165
|
+
NamedProject,
|
|
164
166
|
PluginSetupContext,
|
|
165
167
|
} from './orchestrator/index.js'
|
|
166
168
|
// The per-task execution contract a plugin's `executor` capability returns.
|
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
computeWorkspaceFingerprint,
|
|
25
25
|
findWorkspaceRoot,
|
|
26
26
|
type LoadReads,
|
|
27
|
-
listProjects,
|
|
28
27
|
loadProjectConfig,
|
|
29
28
|
loadWorkspace,
|
|
30
29
|
lockfilePath,
|
|
@@ -33,7 +32,7 @@ import {
|
|
|
33
32
|
} from '../workspace/index.js'
|
|
34
33
|
import { flakyTasks, type FlakyTask } from './failure-mode.js'
|
|
35
34
|
import type { VxPlugin } from './plugin.js'
|
|
36
|
-
import { loadProjects, loadWorkspacePlugins } from './projects.js'
|
|
35
|
+
import { discoverProjects, loadProjects, loadWorkspacePlugins } from './projects.js'
|
|
37
36
|
|
|
38
37
|
const warnToStderr = (message: string): void => {
|
|
39
38
|
process.stderr.write(`${message}\n`)
|
|
@@ -114,12 +113,12 @@ export async function collectInfo(cwd: string, opts: CollectInfoOptions = {}): P
|
|
|
114
113
|
const warn = opts.warn ?? warnToStderr
|
|
115
114
|
const reads: LoadReads = new Map()
|
|
116
115
|
const root = await findWorkspaceRoot(cwd, reads)
|
|
117
|
-
const metas = await listProjects(await loadWorkspace(root, reads))
|
|
118
116
|
const { workspaceConfig, plugins } = await loadWorkspacePlugins(root, warn)
|
|
119
117
|
const cacheDir =
|
|
120
118
|
opts.cacheDir === undefined
|
|
121
119
|
? resolveCacheDir(root, workspaceConfig)
|
|
122
120
|
: path.resolve(cwd, opts.cacheDir)
|
|
121
|
+
const metas = await discoverProjects(await loadWorkspace(root, reads), plugins, cacheDir, warn)
|
|
123
122
|
const cache = Cache.inspect(cacheDir)
|
|
124
123
|
noteSchemaReset(cache, warn)
|
|
125
124
|
let stats
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
export { run, planRun, nxProjectTarget } from './run.js'
|
|
5
5
|
export { prepareRun, type PreparedRun } from './prepare.js'
|
|
6
6
|
export {
|
|
7
|
+
discoverProjects,
|
|
7
8
|
loadProjects,
|
|
8
9
|
loadResolvedProjects,
|
|
9
10
|
loadWorkspacePlugins,
|
|
@@ -70,6 +71,8 @@ export {
|
|
|
70
71
|
type ScheduleHookContext,
|
|
71
72
|
type AdmitContext,
|
|
72
73
|
type WorkspaceHookContext,
|
|
74
|
+
type DiscoverContext,
|
|
75
|
+
type NamedProject,
|
|
73
76
|
type InstallPluginsArgs,
|
|
74
77
|
type Plugin,
|
|
75
78
|
type PluginContext,
|
|
@@ -14,16 +14,19 @@ import type { ProjectConfig, WorkspaceConfig } from '../config.js'
|
|
|
14
14
|
import { checkGraph, type TaskNode } from '../graph/index.js'
|
|
15
15
|
import type {
|
|
16
16
|
CacheContext,
|
|
17
|
+
DiscoverContext,
|
|
17
18
|
ExecutorContext,
|
|
18
19
|
FingerprintChange,
|
|
19
20
|
FingerprintContext,
|
|
20
21
|
GraphHookContext,
|
|
21
22
|
KeyHookContext,
|
|
23
|
+
NamedProject,
|
|
22
24
|
ProjectHookContext,
|
|
23
25
|
ScheduleHookContext,
|
|
24
26
|
VxPlugin,
|
|
25
27
|
WorkspaceHookContext,
|
|
26
28
|
} from './plugin.js'
|
|
29
|
+
import type { ProjectMeta } from '../workspace/index.js'
|
|
27
30
|
|
|
28
31
|
/** `a string`, `an array`, `null`, `a number` — for a refusal that names what came back. */
|
|
29
32
|
function describeValue(v: unknown): string {
|
|
@@ -97,7 +100,7 @@ async function safe<T>(plugin: VxPlugin, hook: string, fn: () => T | Promise<T>)
|
|
|
97
100
|
|
|
98
101
|
export function hasHook(
|
|
99
102
|
plugins: readonly VxPlugin[],
|
|
100
|
-
hook: 'config' | 'project' | 'graph' | 'key' | 'schedule' | 'admit',
|
|
103
|
+
hook: 'config' | 'discover' | 'project' | 'graph' | 'key' | 'schedule' | 'admit',
|
|
101
104
|
): boolean {
|
|
102
105
|
for (const p of plugins) if (p[hook] !== undefined) return true
|
|
103
106
|
return false
|
|
@@ -118,6 +121,32 @@ export async function applyConfigHooks(
|
|
|
118
121
|
}
|
|
119
122
|
}
|
|
120
123
|
|
|
124
|
+
/**
|
|
125
|
+
* `discover` stage: each plugin names directories to add to `projects`,
|
|
126
|
+
* seeing the ones earlier plugins added. `add` turns one name into a meta
|
|
127
|
+
* (null: already there) or refuses it.
|
|
128
|
+
*/
|
|
129
|
+
export async function applyDiscoverHooks(
|
|
130
|
+
plugins: readonly VxPlugin[],
|
|
131
|
+
projects: ProjectMeta[],
|
|
132
|
+
ctx: Omit<DiscoverContext, 'projects'>,
|
|
133
|
+
add: (named: NamedProject, plugin: VxPlugin) => Promise<ProjectMeta | null>,
|
|
134
|
+
): Promise<void> {
|
|
135
|
+
for (const plugin of plugins) {
|
|
136
|
+
if (plugin.discover === undefined) continue
|
|
137
|
+
const named = await safe(plugin, 'discover', () => plugin.discover!({ ...ctx, projects }))
|
|
138
|
+
if (!Array.isArray(named)) {
|
|
139
|
+
throw new UserError(
|
|
140
|
+
`plugin '${plugin.name}' failed in discover: expected an array, got ${describeValue(named)}`,
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
for (const n of named as readonly NamedProject[]) {
|
|
144
|
+
const meta = await add(n, plugin)
|
|
145
|
+
if (meta !== null) projects.push(meta)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
121
150
|
/** `project` stage: every plugin edits one project's config in place. */
|
|
122
151
|
export async function applyProjectHooks(
|
|
123
152
|
plugins: readonly VxPlugin[],
|
|
@@ -60,6 +60,17 @@ export interface VxPlugin {
|
|
|
60
60
|
*/
|
|
61
61
|
config?(workspace: WorkspaceConfig, ctx: WorkspaceHookContext): void | Promise<void>
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Directories to make projects beyond the package manager's members, as
|
|
65
|
+
* `{ dir, name }` (`dir` absolute or relative to the workspace root): a
|
|
66
|
+
* runner's root tasks, a project no member glob lists. Runs once per
|
|
67
|
+
* discovery, before any project config loads; each plugin sees core's
|
|
68
|
+
* projects and the ones earlier plugins named. A named directory keeps
|
|
69
|
+
* a `package.json` of the same name, if it has one; a name or directory
|
|
70
|
+
* already taken otherwise, or one outside the root, is refused.
|
|
71
|
+
*/
|
|
72
|
+
discover?(ctx: DiscoverContext): readonly NamedProject[] | Promise<readonly NamedProject[]>
|
|
73
|
+
|
|
63
74
|
/**
|
|
64
75
|
* One project's validated config, right after it loaded and before the
|
|
65
76
|
* graph is built. Add, remove or edit tasks. Runs for every loaded project
|
|
@@ -243,6 +254,18 @@ export interface WorkspaceHookContext {
|
|
|
243
254
|
warn(message: string): void
|
|
244
255
|
}
|
|
245
256
|
|
|
257
|
+
export interface DiscoverContext extends WorkspaceHookContext {
|
|
258
|
+
/** Where vx's cache lives: a plugin may keep what it read to answer here. */
|
|
259
|
+
readonly cacheDir: string
|
|
260
|
+
/** The projects found so far: core's, then earlier plugins'. */
|
|
261
|
+
readonly projects: readonly ProjectMeta[]
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export interface NamedProject {
|
|
265
|
+
readonly dir: string
|
|
266
|
+
readonly name: string
|
|
267
|
+
}
|
|
268
|
+
|
|
246
269
|
export interface ProjectHookContext extends BaseContext {
|
|
247
270
|
/** The package name (`package.json#name`). */
|
|
248
271
|
readonly name: string
|
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
computeWorkspaceFingerprints,
|
|
30
30
|
findWorkspaceRoot,
|
|
31
31
|
type LoadReads,
|
|
32
|
-
listProjects,
|
|
33
32
|
loadWorkspace,
|
|
34
33
|
FROZEN_WITHOUT_LOCK,
|
|
35
34
|
readLockfile,
|
|
@@ -53,7 +52,12 @@ import {
|
|
|
53
52
|
resolveCache,
|
|
54
53
|
teardownPlugins,
|
|
55
54
|
} from './plugin-host.js'
|
|
56
|
-
import {
|
|
55
|
+
import {
|
|
56
|
+
discoverProjects,
|
|
57
|
+
loadProjects,
|
|
58
|
+
loadWorkspacePlugins,
|
|
59
|
+
type LoadedProjects,
|
|
60
|
+
} from './projects.js'
|
|
57
61
|
import { keyExcludedDependencies } from './excluded-keys.js'
|
|
58
62
|
import { FingerprintWatch } from './fingerprint-watch.js'
|
|
59
63
|
import type { VxPlugin } from './plugin.js'
|
|
@@ -184,10 +188,15 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
184
188
|
log.status(m),
|
|
185
189
|
)
|
|
186
190
|
mark('workspace config')
|
|
191
|
+
// `--cache-dir <path>` (RunOptions.cacheDir) overrides the workspace
|
|
192
|
+
// `cacheDir` field + the `.vx/cache` default; resolved relative to cwd.
|
|
193
|
+
const cacheDir = options.cacheDir
|
|
194
|
+
? path.resolve(options.cwd, options.cacheDir)
|
|
195
|
+
: resolveCacheDir(workspaceRoot, workspaceConfig)
|
|
187
196
|
const projectMetas =
|
|
188
197
|
options.discovered?.root === workspaceRoot
|
|
189
198
|
? options.discovered.projects
|
|
190
|
-
: await
|
|
199
|
+
: await discoverProjects(workspace, plugins, cacheDir, (m) => log.status(m))
|
|
191
200
|
mark('discover projects')
|
|
192
201
|
|
|
193
202
|
// SCOPED config loading: configs are programs, and evaluating 1090
|
|
@@ -220,13 +229,8 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
220
229
|
mark('package graph')
|
|
221
230
|
|
|
222
231
|
// The local cache opens BEFORE the configs load: it is also where their
|
|
223
|
-
// cached evaluations live.
|
|
224
|
-
// overrides the workspace `cacheDir` field + the `.vx/cache` default;
|
|
225
|
-
// resolved relative to cwd.
|
|
232
|
+
// cached evaluations live.
|
|
226
233
|
const policy: CachePolicy = options.cache ?? FULL_CACHE_POLICY
|
|
227
|
-
const cacheDir = options.cacheDir
|
|
228
|
-
? path.resolve(options.cwd, options.cacheDir)
|
|
229
|
-
: resolveCacheDir(workspaceRoot, workspaceConfig)
|
|
230
234
|
const localCache = new Cache(
|
|
231
235
|
cacheDir,
|
|
232
236
|
{ read: policy.localRead, write: policy.localWrite },
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type PackageGraph,
|
|
16
16
|
type ProjectEntry,
|
|
17
17
|
type ProjectMeta,
|
|
18
|
+
type Workspace,
|
|
18
19
|
} from '../workspace/index.js'
|
|
19
20
|
import type { WorkspaceConfig } from '../config.js'
|
|
20
21
|
import { Cache } from '../cache/index.js'
|
|
@@ -25,11 +26,12 @@ import {
|
|
|
25
26
|
listProjects,
|
|
26
27
|
loadWorkspace,
|
|
27
28
|
loadWorkspaceConfig,
|
|
29
|
+
namedProject,
|
|
28
30
|
resolveCacheDir,
|
|
29
31
|
validateWorkspace,
|
|
30
32
|
} from '../workspace/index.js'
|
|
31
33
|
import { parseDependencySpec } from '../graph/index.js'
|
|
32
|
-
import { applyConfigHooks, applyProjectHooks, hasHook } from './plugin-host.js'
|
|
34
|
+
import { applyConfigHooks, applyDiscoverHooks, applyProjectHooks, hasHook } from './plugin-host.js'
|
|
33
35
|
import type { VxPlugin } from './plugin.js'
|
|
34
36
|
|
|
35
37
|
/**
|
|
@@ -54,6 +56,27 @@ export async function loadWorkspacePlugins(
|
|
|
54
56
|
return { workspaceConfig, plugins }
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The workspace's projects: core's discovery, then the plugin `discover`
|
|
61
|
+
* stage. With no plugin declaring it, `listProjects` alone.
|
|
62
|
+
*/
|
|
63
|
+
export async function discoverProjects(
|
|
64
|
+
workspace: Workspace,
|
|
65
|
+
plugins: readonly VxPlugin[],
|
|
66
|
+
cacheDir: string,
|
|
67
|
+
warn: (message: string) => void,
|
|
68
|
+
): Promise<ProjectMeta[]> {
|
|
69
|
+
const projects = await listProjects(workspace)
|
|
70
|
+
if (!hasHook(plugins, 'discover')) return projects
|
|
71
|
+
await applyDiscoverHooks(
|
|
72
|
+
plugins,
|
|
73
|
+
projects,
|
|
74
|
+
{ workspaceRoot: workspace.root, cacheDir, warn },
|
|
75
|
+
(named, plugin) => namedProject(workspace, projects, named, plugin.name),
|
|
76
|
+
)
|
|
77
|
+
return projects.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
|
|
78
|
+
}
|
|
79
|
+
|
|
57
80
|
export interface LoadProjectsArgs {
|
|
58
81
|
workspaceRoot: string
|
|
59
82
|
cacheDir: string
|
|
@@ -249,9 +272,14 @@ export async function loadResolvedProjects(
|
|
|
249
272
|
): Promise<Map<string, ProjectEntry>> {
|
|
250
273
|
const warn = opts.warn ?? ((): void => {})
|
|
251
274
|
const reads: LoadReads = new Map()
|
|
252
|
-
const metas = await listProjects(await loadWorkspace(workspaceRoot, reads))
|
|
253
275
|
const { workspaceConfig, plugins } = await loadWorkspacePlugins(workspaceRoot, warn)
|
|
254
276
|
const cacheDir = resolveCacheDir(workspaceRoot, workspaceConfig)
|
|
277
|
+
const metas = await discoverProjects(
|
|
278
|
+
await loadWorkspace(workspaceRoot, reads),
|
|
279
|
+
plugins,
|
|
280
|
+
cacheDir,
|
|
281
|
+
warn,
|
|
282
|
+
)
|
|
255
283
|
// Opened as `vx last` opens it: a reader makes no index where there is
|
|
256
284
|
// none and never resets an earlier schema's (C-5). One it cannot read
|
|
257
285
|
// serves nothing; the configs evaluate live.
|
package/src/workspace/index.ts
CHANGED
|
@@ -46,6 +46,7 @@ export {
|
|
|
46
46
|
export {
|
|
47
47
|
findWorkspaceRoot,
|
|
48
48
|
listProjects,
|
|
49
|
+
namedProject,
|
|
49
50
|
loadWorkspace,
|
|
50
51
|
unreachedHint,
|
|
51
52
|
unreachedPackages,
|
|
@@ -54,6 +55,7 @@ export {
|
|
|
54
55
|
resolveCacheDir,
|
|
55
56
|
type ProjectEntry,
|
|
56
57
|
type ProjectMeta,
|
|
58
|
+
type Workspace,
|
|
57
59
|
} from './workspace.js'
|
|
58
60
|
export type { LoadReads } from './load-reads.js'
|
|
59
61
|
// The migration seam (see migration.ts) and core's own mapper, the scripts one.
|
|
@@ -596,3 +596,63 @@ export async function listProjects(workspace: Workspace): Promise<ProjectMeta[]>
|
|
|
596
596
|
// all any consumer needs.
|
|
597
597
|
return projects.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
|
|
598
598
|
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* A directory a plugin's `discover` hook names, as a project meta; null
|
|
602
|
+
* when `known` already holds it under that name. `by` names the plugin in
|
|
603
|
+
* a refusal. A directory without a `package.json` is a project by the
|
|
604
|
+
* plugin's name alone.
|
|
605
|
+
*/
|
|
606
|
+
export async function namedProject(
|
|
607
|
+
workspace: Workspace,
|
|
608
|
+
known: readonly ProjectMeta[],
|
|
609
|
+
named: { readonly dir: string; readonly name: string },
|
|
610
|
+
by: string,
|
|
611
|
+
): Promise<ProjectMeta | null> {
|
|
612
|
+
const where = `plugin '${by}' named project`
|
|
613
|
+
if (typeof named?.name !== 'string' || named.name === '' || typeof named.dir !== 'string') {
|
|
614
|
+
throw new UserError(`${where} ${JSON.stringify(named)}: expected { dir: string, name: string }`)
|
|
615
|
+
}
|
|
616
|
+
const dir = path.resolve(workspace.root, named.dir)
|
|
617
|
+
const rel = relPosix(workspace.root, dir)
|
|
618
|
+
if (rel === '..' || rel.startsWith('../') || path.isAbsolute(rel)) {
|
|
619
|
+
throw new UserError(`${where} "${named.name}" at ${dir}: outside the workspace root`)
|
|
620
|
+
}
|
|
621
|
+
const shown = rel === '' ? '.' : rel
|
|
622
|
+
for (const p of known) {
|
|
623
|
+
if (p.dir === dir && p.name === named.name) return null
|
|
624
|
+
if (p.dir === dir) {
|
|
625
|
+
throw new UserError(`${where} "${named.name}" at ${shown}: already the project "${p.name}"`)
|
|
626
|
+
}
|
|
627
|
+
if (p.name === named.name) {
|
|
628
|
+
throw new UserError(
|
|
629
|
+
`${where} "${named.name}" at ${shown}: the name is taken by ${relPosix(workspace.root, p.dir) || '.'}`,
|
|
630
|
+
)
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
const pkgJsonPath = dir + path.sep + 'package.json'
|
|
634
|
+
const [configPath, text] = await Promise.all([
|
|
635
|
+
findConfigFile(dir),
|
|
636
|
+
Bun.file(pkgJsonPath)
|
|
637
|
+
.text()
|
|
638
|
+
.catch((err: unknown) => absent(err, null)),
|
|
639
|
+
])
|
|
640
|
+
if (
|
|
641
|
+
text === null &&
|
|
642
|
+
!(await stat(dir).then(
|
|
643
|
+
(st) => st.isDirectory(),
|
|
644
|
+
() => false,
|
|
645
|
+
))
|
|
646
|
+
) {
|
|
647
|
+
throw new UserError(`${where} "${named.name}" at ${shown}: no such directory`)
|
|
648
|
+
}
|
|
649
|
+
const parsed: Partial<PackageJson> = text === null ? {} : parsePackageJson(text, pkgJsonPath)
|
|
650
|
+
// A nameless manifest names nothing to disagree with (Nx's own projects).
|
|
651
|
+
const pkg: PackageJson = { ...parsed, name: parsed.name ?? named.name }
|
|
652
|
+
if (pkg.name !== named.name) {
|
|
653
|
+
throw new UserError(
|
|
654
|
+
`${where} "${named.name}" at ${shown}: its package.json names it "${pkg.name ?? ''}"`,
|
|
655
|
+
)
|
|
656
|
+
}
|
|
657
|
+
return { name: named.name, dir, packageJson: pkg, configPath }
|
|
658
|
+
}
|