@vzn/vx 0.0.203 → 0.0.204
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 +14 -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 +29 -1
- package/src/orchestrator/plugin.ts +21 -0
- package/src/orchestrator/prepare.ts +7 -3
- package/src/orchestrator/projects.ts +24 -2
- package/src/workspace/index.ts +2 -0
- package/src/workspace/workspace.ts +58 -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.204",
|
|
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.204",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.204",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.204",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.204"
|
|
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,14 @@ 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
|
+
loadWorkspaceConfig,
|
|
19
|
+
type Workspace,
|
|
18
20
|
type ProjectEntry,
|
|
19
21
|
type ProjectMeta,
|
|
20
22
|
readLockfile,
|
|
@@ -36,6 +38,17 @@ export async function loadCliWorkspace(workspaceRoot: string): Promise<CliWorksp
|
|
|
36
38
|
return { workspaceConfig, plugins, cacheDir: resolveCacheDir(workspaceRoot, workspaceConfig) }
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
/**
|
|
42
|
+
* The workspace's projects as a run discovers them: core's, then the
|
|
43
|
+
* plugin `discover` stage. Only the plugin list is read; the `config`
|
|
44
|
+
* stage cannot change it.
|
|
45
|
+
*/
|
|
46
|
+
export async function discoverCliProjects(workspace: Workspace): Promise<ProjectMeta[]> {
|
|
47
|
+
const plugins = ((await loadWorkspaceConfig(workspace.root))?.plugins ??
|
|
48
|
+
[]) as readonly VxPlugin[]
|
|
49
|
+
return await discoverProjects(workspace, plugins, warnToStderr)
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
/**
|
|
40
53
|
* `--cache-dir <path>` / `--cache-dir=<path>` for a verb that reads the
|
|
41
54
|
* 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,8 +113,8 @@ 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)
|
|
117
|
+
const metas = await discoverProjects(await loadWorkspace(root, reads), plugins, warn)
|
|
119
118
|
const cacheDir =
|
|
120
119
|
opts.cacheDir === undefined
|
|
121
120
|
? resolveCacheDir(root, workspaceConfig)
|
|
@@ -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,
|
|
@@ -19,11 +19,13 @@ import type {
|
|
|
19
19
|
FingerprintContext,
|
|
20
20
|
GraphHookContext,
|
|
21
21
|
KeyHookContext,
|
|
22
|
+
NamedProject,
|
|
22
23
|
ProjectHookContext,
|
|
23
24
|
ScheduleHookContext,
|
|
24
25
|
VxPlugin,
|
|
25
26
|
WorkspaceHookContext,
|
|
26
27
|
} from './plugin.js'
|
|
28
|
+
import type { ProjectMeta } from '../workspace/index.js'
|
|
27
29
|
|
|
28
30
|
/** `a string`, `an array`, `null`, `a number` — for a refusal that names what came back. */
|
|
29
31
|
function describeValue(v: unknown): string {
|
|
@@ -97,7 +99,7 @@ async function safe<T>(plugin: VxPlugin, hook: string, fn: () => T | Promise<T>)
|
|
|
97
99
|
|
|
98
100
|
export function hasHook(
|
|
99
101
|
plugins: readonly VxPlugin[],
|
|
100
|
-
hook: 'config' | 'project' | 'graph' | 'key' | 'schedule' | 'admit',
|
|
102
|
+
hook: 'config' | 'discover' | 'project' | 'graph' | 'key' | 'schedule' | 'admit',
|
|
101
103
|
): boolean {
|
|
102
104
|
for (const p of plugins) if (p[hook] !== undefined) return true
|
|
103
105
|
return false
|
|
@@ -118,6 +120,32 @@ export async function applyConfigHooks(
|
|
|
118
120
|
}
|
|
119
121
|
}
|
|
120
122
|
|
|
123
|
+
/**
|
|
124
|
+
* `discover` stage: each plugin names directories to add to `projects`,
|
|
125
|
+
* seeing the ones earlier plugins added. `add` turns one name into a meta
|
|
126
|
+
* (null: already there) or refuses it.
|
|
127
|
+
*/
|
|
128
|
+
export async function applyDiscoverHooks(
|
|
129
|
+
plugins: readonly VxPlugin[],
|
|
130
|
+
projects: ProjectMeta[],
|
|
131
|
+
ctx: WorkspaceHookContext,
|
|
132
|
+
add: (named: NamedProject, plugin: VxPlugin) => Promise<ProjectMeta | null>,
|
|
133
|
+
): Promise<void> {
|
|
134
|
+
for (const plugin of plugins) {
|
|
135
|
+
if (plugin.discover === undefined) continue
|
|
136
|
+
const named = await safe(plugin, 'discover', () => plugin.discover!({ ...ctx, projects }))
|
|
137
|
+
if (!Array.isArray(named)) {
|
|
138
|
+
throw new UserError(
|
|
139
|
+
`plugin '${plugin.name}' failed in discover: expected an array, got ${describeValue(named)}`,
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
for (const n of named as readonly NamedProject[]) {
|
|
143
|
+
const meta = await add(n, plugin)
|
|
144
|
+
if (meta !== null) projects.push(meta)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
121
149
|
/** `project` stage: every plugin edits one project's config in place. */
|
|
122
150
|
export async function applyProjectHooks(
|
|
123
151
|
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,16 @@ export interface WorkspaceHookContext {
|
|
|
243
254
|
warn(message: string): void
|
|
244
255
|
}
|
|
245
256
|
|
|
257
|
+
export interface DiscoverContext extends WorkspaceHookContext {
|
|
258
|
+
/** The projects found so far: core's, then earlier plugins'. */
|
|
259
|
+
readonly projects: readonly ProjectMeta[]
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export interface NamedProject {
|
|
263
|
+
readonly dir: string
|
|
264
|
+
readonly name: string
|
|
265
|
+
}
|
|
266
|
+
|
|
246
267
|
export interface ProjectHookContext extends BaseContext {
|
|
247
268
|
/** The package name (`package.json#name`). */
|
|
248
269
|
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'
|
|
@@ -187,7 +191,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
187
191
|
const projectMetas =
|
|
188
192
|
options.discovered?.root === workspaceRoot
|
|
189
193
|
? options.discovered.projects
|
|
190
|
-
: await
|
|
194
|
+
: await discoverProjects(workspace, plugins, (m) => log.status(m))
|
|
191
195
|
mark('discover projects')
|
|
192
196
|
|
|
193
197
|
// SCOPED config loading: configs are programs, and evaluating 1090
|
|
@@ -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,26 @@ 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
|
+
warn: (message: string) => void,
|
|
67
|
+
): Promise<ProjectMeta[]> {
|
|
68
|
+
const projects = await listProjects(workspace)
|
|
69
|
+
if (!hasHook(plugins, 'discover')) return projects
|
|
70
|
+
await applyDiscoverHooks(
|
|
71
|
+
plugins,
|
|
72
|
+
projects,
|
|
73
|
+
{ workspaceRoot: workspace.root, warn },
|
|
74
|
+
(named, plugin) => namedProject(workspace, projects, named, plugin.name),
|
|
75
|
+
)
|
|
76
|
+
return projects.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
|
|
77
|
+
}
|
|
78
|
+
|
|
57
79
|
export interface LoadProjectsArgs {
|
|
58
80
|
workspaceRoot: string
|
|
59
81
|
cacheDir: string
|
|
@@ -249,8 +271,8 @@ export async function loadResolvedProjects(
|
|
|
249
271
|
): Promise<Map<string, ProjectEntry>> {
|
|
250
272
|
const warn = opts.warn ?? ((): void => {})
|
|
251
273
|
const reads: LoadReads = new Map()
|
|
252
|
-
const metas = await listProjects(await loadWorkspace(workspaceRoot, reads))
|
|
253
274
|
const { workspaceConfig, plugins } = await loadWorkspacePlugins(workspaceRoot, warn)
|
|
275
|
+
const metas = await discoverProjects(await loadWorkspace(workspaceRoot, reads), plugins, warn)
|
|
254
276
|
const cacheDir = resolveCacheDir(workspaceRoot, workspaceConfig)
|
|
255
277
|
// Opened as `vx last` opens it: a reader makes no index where there is
|
|
256
278
|
// none and never resets an earlier schema's (C-5). One it cannot read
|
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,61 @@ 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 pkg = text === null ? { name: named.name } : parsePackageJson(text, pkgJsonPath)
|
|
650
|
+
if (pkg.name !== named.name) {
|
|
651
|
+
throw new UserError(
|
|
652
|
+
`${where} "${named.name}" at ${shown}: its package.json names it "${pkg.name ?? ''}"`,
|
|
653
|
+
)
|
|
654
|
+
}
|
|
655
|
+
return { name: named.name, dir, packageJson: pkg, configPath }
|
|
656
|
+
}
|