@vzn/vx 0.0.202 → 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 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 (13 hooks, `commands` included) | **shippable** |
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.202",
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.202",
21
- "@vzn/vx-linux-arm64": "0.0.202",
22
- "@vzn/vx-darwin-x64": "0.0.202",
23
- "@vzn/vx-darwin-arm64": "0.0.202"
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 { type CliLoadOptions, loadCliProjects, loadCliWorkspace } from './workspace-config.js'
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 listProjects(await loadWorkspace(root, reads))
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 listProjects(await loadWorkspace(root, reads))
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
@@ -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, listProjects, loadWorkspace } from '../workspace/index.js'
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(root, await listProjects(await loadWorkspace(root)), 'all', {
68
- noCreate: true,
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 listProjects(workspace)
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 listProjects(workspace)
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 { loadProjects, loadWorkspacePlugins, type LoadedProjects } from './projects.js'
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 listProjects(workspace)
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
@@ -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
+ }