@ontrails/trails 1.0.0-beta.30 → 1.0.0-beta.32

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/CHANGELOG.md CHANGED
@@ -1,5 +1,94 @@
1
1
  # trails
2
2
 
3
+ ## 1.0.0-beta.32
4
+
5
+ ### Patch Changes
6
+
7
+ - f3c4fef: Export a shared `escapeRegExp` helper from core and migrate first-party callers off local copies.
8
+ - cb0a9d8: Export shared workspace package discovery helpers from core and migrate first-party discovery callers.
9
+ - 8db145e: Move activation report derivation into Topographer and keep the Trails app
10
+ consuming the owner-held activation facts through a compatibility re-export.
11
+ - 8e2603c: Rename read-only topo helper exports from `build*` to `derive*`.
12
+ - 7a3a25f: Rename version lifecycle result paths from `file` to `filePath`.
13
+ - fe72b84: Fold remaining Regrade and Warden scan-target surfaces onto the shared path-scope vocabulary.
14
+ - Updated dependencies [3e5c0fc]
15
+ - Updated dependencies [f3c4fef]
16
+ - Updated dependencies [cb0a9d8]
17
+ - Updated dependencies [8db145e]
18
+ - Updated dependencies [2b819f4]
19
+ - Updated dependencies [21c6dda]
20
+ - Updated dependencies [860ef32]
21
+ - Updated dependencies [fe72b84]
22
+ - @ontrails/adapter-kit@1.0.0-beta.32
23
+ - @ontrails/core@1.0.0-beta.32
24
+ - @ontrails/permits@1.0.0-beta.32
25
+ - @ontrails/warden@1.0.0-beta.32
26
+ - @ontrails/regrade@1.0.0-beta.32
27
+ - @ontrails/topographer@1.0.0-beta.32
28
+ - @ontrails/config@1.0.0-beta.32
29
+ - @ontrails/cli@1.0.0-beta.32
30
+ - @ontrails/commander@1.0.0-beta.32
31
+ - @ontrails/http@1.0.0-beta.32
32
+ - @ontrails/mcp@1.0.0-beta.32
33
+ - @ontrails/observe@1.0.0-beta.32
34
+ - @ontrails/tracing@1.0.0-beta.32
35
+ - @ontrails/wayfinder@1.0.0-beta.32
36
+
37
+ ## 1.0.0-beta.31
38
+
39
+ ### Patch Changes
40
+
41
+ - e2f3d23: Default Regrade reports to actionable entries, add skip counts grouped by
42
+ reason, and expose an `includeEntries` option for full report inventories.
43
+ - 9be2b7e: Load project-local Warden term-rewrite rules from the Regrade root so repo-owned
44
+ migration classes can run through `trails regrade`.
45
+ - 47f782c: Add occurrence-level vocabulary regrade reports with plan, ledger,
46
+ and completion-gate facts. The Trails `regrade` operator command now supports
47
+ positional `<from> <to>` regrade runs and exposes the same capability through
48
+ the curated MCP surface.
49
+ - ee9f3ae: Let Warden fix capabilities declare downstream scan targets and have Regrade
50
+ honor those targets for Warden-backed term-rewrite classes.
51
+
52
+ Dogfood the first safe facet-to-trailhead prose rewrite through project-local
53
+ Warden rules and Regrade.
54
+
55
+ - 982a4d7: Add Regrade path-scope exclusion globs for vocabulary runs and expose them
56
+ through the `trails regrade` CLI/MCP contract.
57
+ - 1540233: Add Regrade scan inventory summaries that group matched files by extension and
58
+ top-level path, with occurrence counts for vocabulary regrade reports.
59
+ - de878bd: Let Trails project config provide default Regrade vocabulary scope settings,
60
+ with explicit CLI or MCP plan inputs overriding those defaults.
61
+ - a0126d9: Add Warden `scope.exclude` globs through project config and the Trails CLI
62
+ wrapper so governance runs can exclude local notes, scratch space, and generated
63
+ state without dropping durable skills or plugin assets from scope.
64
+ - a079073: Rename Regrade path-scope scan controls from `ignore` to `exclude` across CLI, MCP, and project config.
65
+ - 6a26a08: Rename Warden governance scope controls from jurisdiction ignore settings to `scope.exclude` across config, CLI, and Trails surfaces.
66
+ - Updated dependencies [e2f3d23]
67
+ - Updated dependencies [9be2b7e]
68
+ - Updated dependencies [47f782c]
69
+ - Updated dependencies [ee9f3ae]
70
+ - Updated dependencies [982a4d7]
71
+ - Updated dependencies [1540233]
72
+ - Updated dependencies [a0126d9]
73
+ - Updated dependencies [4cd5d4e]
74
+ - Updated dependencies [a079073]
75
+ - Updated dependencies [6a26a08]
76
+ - Updated dependencies [38907cc]
77
+ - @ontrails/regrade@1.0.0-beta.31
78
+ - @ontrails/warden@1.0.0-beta.31
79
+ - @ontrails/mcp@1.0.0-beta.31
80
+ - @ontrails/wayfinder@1.0.0-beta.31
81
+ - @ontrails/core@1.0.0-beta.31
82
+ - @ontrails/commander@1.0.0-beta.31
83
+ - @ontrails/adapter-kit@1.0.0-beta.31
84
+ - @ontrails/cli@1.0.0-beta.31
85
+ - @ontrails/config@1.0.0-beta.31
86
+ - @ontrails/http@1.0.0-beta.31
87
+ - @ontrails/observe@1.0.0-beta.31
88
+ - @ontrails/permits@1.0.0-beta.31
89
+ - @ontrails/topographer@1.0.0-beta.31
90
+ - @ontrails/tracing@1.0.0-beta.31
91
+
3
92
  ## 1.0.0-beta.30
4
93
 
5
94
  ### Patch Changes
package/README.md CHANGED
@@ -17,8 +17,8 @@ Common workflows:
17
17
  - `trails validate` checks root `trails.lock` for drift.
18
18
  - `trails wayfind`, `trails wayfind --trails --intent read`, `trails wayfind <id> --contract`, `trails wayfind <id> --deps`, `trails wayfind <id> --impact`, `trails wayfind pattern "wayfind.*"`, `trails wayfind query "release drift"`, `trails wayfind file <file> --outline`, and `trails wayfind diff ...` read graph artifacts and source outlines through Wayfinder for local navigation.
19
19
  - `trails schema <command...>` shows accepted CLI routes, aliases, flags, and schemas for an operator command.
20
- - `trails warden` runs Trails governance checks for contract and architecture drift.
21
- - `trails regrade --root-dir <path> --json` dry-runs downstream migration checks; add `--apply` only to write safe rewrites.
20
+ - `trails warden` runs Trails governance checks for contract and architecture drift. Use `--scope-exclude <glob>` or project `warden.scope.exclude` config when local notes, scratch space, or generated state should not be governed by Warden.
21
+ - `trails regrade --root-dir <path> --json` dry-runs downstream migration checks; add `--apply` only to write safe rewrites. Use `trails regrade <from> <to> --root-dir <path> --json` for occurrence-level vocabulary regrade reports that include the authored plan, observed ledger, completion gate, and scan inventory by file type and top-level path. Add `--exclude <glob>` to keep migration scope away from local notes, scratch space, generated state, or other paths that should not be scanned for that transition; project config can provide `regrade.scope` defaults and explicit plan inputs override them. The same `regrade` trail is exposed through MCP for agent-driven migration checks.
22
22
  - `trails guide` remains available for compatibility; prefer `trails wayfind --source live --module <app-module>` or saved-artifact Wayfinder reads for agent navigation.
23
23
 
24
24
  Trails is contract-first: define trails once with typed input, Result output, examples, and meta; the framework derives CLI, MCP, HTTP, and future surfaces from the same contracts.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ontrails/trails",
3
- "version": "1.0.0-beta.30",
3
+ "version": "1.0.0-beta.32",
4
4
  "bin": {
5
5
  "trails": "./bin/trails.ts"
6
6
  },
@@ -27,25 +27,25 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@clack/prompts": "^1.1.0",
30
- "@ontrails/adapter-kit": "^1.0.0-beta.30",
31
- "@ontrails/cli": "^1.0.0-beta.30",
32
- "@ontrails/commander": "^1.0.0-beta.30",
33
- "@ontrails/config": "^1.0.0-beta.30",
34
- "@ontrails/core": "^1.0.0-beta.30",
35
- "@ontrails/http": "^1.0.0-beta.30",
36
- "@ontrails/mcp": "^1.0.0-beta.30",
37
- "@ontrails/observe": "^1.0.0-beta.30",
38
- "@ontrails/permits": "^1.0.0-beta.30",
39
- "@ontrails/regrade": "^1.0.0-beta.30",
40
- "@ontrails/topographer": "^1.0.0-beta.30",
41
- "@ontrails/tracing": "^1.0.0-beta.30",
42
- "@ontrails/warden": "^1.0.0-beta.30",
43
- "@ontrails/wayfinder": "^1.0.0-beta.30",
30
+ "@ontrails/adapter-kit": "^1.0.0-beta.32",
31
+ "@ontrails/cli": "^1.0.0-beta.32",
32
+ "@ontrails/commander": "^1.0.0-beta.32",
33
+ "@ontrails/config": "^1.0.0-beta.32",
34
+ "@ontrails/core": "^1.0.0-beta.32",
35
+ "@ontrails/http": "^1.0.0-beta.32",
36
+ "@ontrails/mcp": "^1.0.0-beta.32",
37
+ "@ontrails/observe": "^1.0.0-beta.32",
38
+ "@ontrails/permits": "^1.0.0-beta.32",
39
+ "@ontrails/regrade": "^1.0.0-beta.32",
40
+ "@ontrails/topographer": "^1.0.0-beta.32",
41
+ "@ontrails/tracing": "^1.0.0-beta.32",
42
+ "@ontrails/warden": "^1.0.0-beta.32",
43
+ "@ontrails/wayfinder": "^1.0.0-beta.32",
44
44
  "commander": "^14.0.3",
45
45
  "typescript": "^5.9.3",
46
46
  "zod": "^4.3.5"
47
47
  },
48
48
  "devDependencies": {
49
- "@ontrails/testing": "^1.0.0-beta.30"
49
+ "@ontrails/testing": "^1.0.0-beta.32"
50
50
  }
51
51
  }
package/src/cli.ts CHANGED
@@ -247,6 +247,7 @@ const wardenValueFlags = new Set([
247
247
  '--drafts',
248
248
  '--fail-on',
249
249
  '--format',
250
+ '--scope-exclude',
250
251
  '--lock',
251
252
  '--root-dir',
252
253
  ]);
@@ -14,6 +14,7 @@ export const trailsMcpIncludedTrails = [
14
14
  'doctor',
15
15
  'draft.promote',
16
16
  'guide',
17
+ 'regrade',
17
18
  'release.check',
18
19
  'release.smoke',
19
20
  'revise',
@@ -0,0 +1,152 @@
1
+ import { loadTrailsConfigValue } from '@ontrails/config';
2
+ import {
3
+ InternalError,
4
+ Result,
5
+ ValidationError,
6
+ pathScopeSchema,
7
+ } from '@ontrails/core';
8
+ import type { Result as TrailsResult } from '@ontrails/core';
9
+ import { z } from 'zod';
10
+
11
+ export const regradeConfigSchema = z
12
+ .object({
13
+ scope: pathScopeSchema.optional(),
14
+ })
15
+ .default({});
16
+
17
+ export type RegradeConfig = z.output<typeof regradeConfigSchema>;
18
+
19
+ interface RegradeConfigLoadResult {
20
+ readonly config?: RegradeConfig;
21
+ readonly configPath?: string;
22
+ }
23
+
24
+ interface ResolvableConfig {
25
+ readonly resolve: (options?: {
26
+ readonly cwd?: string;
27
+ readonly env?: Record<string, string | undefined>;
28
+ }) => unknown;
29
+ }
30
+
31
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
32
+ typeof value === 'object' && value !== null && !Array.isArray(value);
33
+
34
+ const isResultLike = (
35
+ value: unknown
36
+ ): value is {
37
+ readonly error?: unknown;
38
+ readonly isErr: () => boolean;
39
+ readonly isOk: () => boolean;
40
+ readonly value?: unknown;
41
+ } =>
42
+ isRecord(value) &&
43
+ typeof value['isOk'] === 'function' &&
44
+ typeof value['isErr'] === 'function';
45
+
46
+ const isResolvableConfig = (value: unknown): value is ResolvableConfig =>
47
+ isRecord(value) && typeof value['resolve'] === 'function';
48
+
49
+ const errorMessage = (error: unknown): string =>
50
+ error instanceof Error ? error.message : String(error);
51
+
52
+ const extractRegradeConfig = (
53
+ value: unknown
54
+ ): TrailsResult<RegradeConfig | undefined, ValidationError> => {
55
+ if (!(isRecord(value) && 'regrade' in value)) {
56
+ return Result.ok();
57
+ }
58
+
59
+ const parsed = regradeConfigSchema.safeParse(value['regrade']);
60
+ if (parsed.success) {
61
+ return Result.ok(parsed.data);
62
+ }
63
+
64
+ return Result.err(
65
+ new ValidationError('Invalid regrade config in Trails config file.', {
66
+ context: { issues: parsed.error.issues },
67
+ })
68
+ );
69
+ };
70
+
71
+ export const loadRegradeConfig = async ({
72
+ configPath,
73
+ env = {},
74
+ rootDir,
75
+ }: {
76
+ readonly configPath?: string | undefined;
77
+ readonly env?: Record<string, string | undefined> | undefined;
78
+ readonly rootDir: string;
79
+ }): Promise<
80
+ TrailsResult<RegradeConfigLoadResult, InternalError | ValidationError>
81
+ > => {
82
+ try {
83
+ const loaded = await loadTrailsConfigValue({
84
+ configPath,
85
+ rootDir,
86
+ });
87
+ const exported = loaded.value;
88
+ if (exported === undefined) {
89
+ return Result.ok({});
90
+ }
91
+
92
+ if (isResolvableConfig(exported)) {
93
+ const resolved = await exported.resolve({ cwd: rootDir, env });
94
+ if (isResultLike(resolved)) {
95
+ if (resolved.isErr()) {
96
+ return Result.err(
97
+ new InternalError(
98
+ `Failed to resolve regrade config: ${errorMessage(resolved.error)}`
99
+ )
100
+ );
101
+ }
102
+ const configResult = extractRegradeConfig(resolved.value);
103
+ if (configResult.isErr()) {
104
+ return configResult;
105
+ }
106
+ return Result.ok({
107
+ ...(configResult.value === undefined
108
+ ? {}
109
+ : { config: configResult.value }),
110
+ ...(loaded.configPath === undefined
111
+ ? {}
112
+ : { configPath: loaded.configPath }),
113
+ });
114
+ }
115
+
116
+ const configResult = extractRegradeConfig(resolved);
117
+ if (configResult.isErr()) {
118
+ return configResult;
119
+ }
120
+ return Result.ok({
121
+ ...(configResult.value === undefined
122
+ ? {}
123
+ : { config: configResult.value }),
124
+ ...(loaded.configPath === undefined
125
+ ? {}
126
+ : { configPath: loaded.configPath }),
127
+ });
128
+ }
129
+
130
+ const configResult = extractRegradeConfig(exported);
131
+ if (configResult.isErr()) {
132
+ return configResult;
133
+ }
134
+ return Result.ok({
135
+ ...(configResult.value === undefined
136
+ ? {}
137
+ : { config: configResult.value }),
138
+ ...(loaded.configPath === undefined
139
+ ? {}
140
+ : { configPath: loaded.configPath }),
141
+ });
142
+ } catch (error) {
143
+ return Result.err(
144
+ new InternalError(
145
+ `Failed to load regrade config: ${errorMessage(error)}`,
146
+ {
147
+ cause: error instanceof Error ? error : new Error(String(error)),
148
+ }
149
+ )
150
+ );
151
+ }
152
+ };
@@ -1,5 +1,6 @@
1
1
  import { readFile, readdir, writeFile } from 'node:fs/promises';
2
2
  import { join, relative } from 'node:path';
3
+ import { escapeRegExp } from '@ontrails/core';
3
4
 
4
5
  export interface ReleasePackCoherenceInput {
5
6
  readonly branchName?: string | undefined;
@@ -234,9 +235,6 @@ export const findLockfileWorkspaceMetadataMismatches = ({
234
235
  return mismatches;
235
236
  };
236
237
 
237
- const escapeRegExp = (text: string): string =>
238
- text.replaceAll(/[.*+?^${}()|[\]\\]/gu, '\\$&');
239
-
240
238
  const findWorkspaceBlockRange = (
241
239
  text: string,
242
240
  workspacePath: string
@@ -13,9 +13,16 @@ import type {
13
13
  AdapterTargetConformanceManifest,
14
14
  AdapterTargetPlacement,
15
15
  } from '@ontrails/adapter-kit';
16
- import { Result, trail, ValidationError } from '@ontrails/core';
17
- import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
18
- import { dirname, join, relative, resolve } from 'node:path';
16
+ import {
17
+ findWorkspacePackage,
18
+ listWorkspacePatterns,
19
+ Result,
20
+ trail,
21
+ ValidationError,
22
+ } from '@ontrails/core';
23
+ import type { WorkspaceRootManifest } from '@ontrails/core';
24
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
25
+ import { dirname, join, resolve } from 'node:path';
19
26
  import { z } from 'zod';
20
27
 
21
28
  import {
@@ -69,20 +76,11 @@ interface AdapterOperationPlan {
69
76
  readonly targetKey: string;
70
77
  }
71
78
 
72
- interface RootManifest {
73
- readonly workspaces?: unknown;
74
- }
75
-
76
79
  interface WorkspacePackageManifest {
77
80
  readonly exports?: unknown;
78
81
  readonly name?: unknown;
79
82
  }
80
83
 
81
- interface WorkspacePackageName {
82
- readonly name: string;
83
- readonly workspacePath: string;
84
- }
85
-
86
84
  interface LocalNamedReexport {
87
85
  readonly local: string;
88
86
  readonly specifier: string;
@@ -123,27 +121,6 @@ const readJson = <T>(path: string): T | undefined => {
123
121
  }
124
122
  };
125
123
 
126
- const workspacePatternsFromManifest = (
127
- manifest: RootManifest | undefined
128
- ): readonly string[] => {
129
- const { workspaces } = manifest ?? {};
130
- if (Array.isArray(workspaces)) {
131
- return workspaces.filter(
132
- (pattern): pattern is string => typeof pattern === 'string'
133
- );
134
- }
135
-
136
- const packages =
137
- workspaces && typeof workspaces === 'object' && !Array.isArray(workspaces)
138
- ? (workspaces as Record<string, unknown>)['packages']
139
- : undefined;
140
- return Array.isArray(packages)
141
- ? packages.filter(
142
- (pattern): pattern is string => typeof pattern === 'string'
143
- )
144
- : [];
145
- };
146
-
147
124
  const normalizeWorkspacePattern = (pattern: string): string =>
148
125
  pattern.replace(/^\.\//u, '').replace(/\/+$/u, '');
149
126
 
@@ -169,54 +146,14 @@ const rootWorkspaceIncludesPath = (
169
146
  rootDir: string,
170
147
  workspacePath: string
171
148
  ): boolean => {
172
- const rootManifest = readJson<RootManifest>(join(rootDir, 'package.json'));
173
- return workspacePatternsFromManifest(rootManifest).some((pattern) =>
149
+ const rootManifest = readJson<WorkspaceRootManifest>(
150
+ join(rootDir, 'package.json')
151
+ );
152
+ return listWorkspacePatterns(rootManifest).some((pattern) =>
174
153
  workspacePatternCoversPath(pattern, workspacePath)
175
154
  );
176
155
  };
177
156
 
178
- const workspaceDirsForPattern = (
179
- rootDir: string,
180
- pattern: string
181
- ): readonly string[] => {
182
- if (!pattern.endsWith('/*')) {
183
- const workspaceDir = join(rootDir, pattern);
184
- return existsSync(workspaceDir) ? [workspaceDir] : [];
185
- }
186
-
187
- const groupDir = join(rootDir, pattern.slice(0, -2));
188
- if (!existsSync(groupDir)) {
189
- return [];
190
- }
191
-
192
- return readdirSync(groupDir, { withFileTypes: true })
193
- .filter((entry) => entry.isDirectory())
194
- .map((entry) => join(groupDir, entry.name))
195
- .toSorted();
196
- };
197
-
198
- const findWorkspacePackageName = (
199
- rootDir: string,
200
- packageName: string
201
- ): WorkspacePackageName | undefined => {
202
- const rootManifest = readJson<RootManifest>(join(rootDir, 'package.json'));
203
- for (const pattern of workspacePatternsFromManifest(rootManifest)) {
204
- for (const workspaceDir of workspaceDirsForPattern(rootDir, pattern)) {
205
- const manifest = readJson<WorkspacePackageManifest>(
206
- join(workspaceDir, 'package.json')
207
- );
208
- if (manifest?.name === packageName) {
209
- return {
210
- name: packageName,
211
- workspacePath: relative(rootDir, workspaceDir),
212
- };
213
- }
214
- }
215
- }
216
-
217
- return undefined;
218
- };
219
-
220
157
  const resolvePhysicalRootDir = (
221
158
  rootDir: string
222
159
  ): Result<string, ValidationError> => {
@@ -882,10 +819,13 @@ const buildExtractedPlan = (
882
819
  if (!packageNamePattern.test(packageName)) {
883
820
  return fail(packageNameMessage);
884
821
  }
885
- const existingPackage = findWorkspacePackageName(rootDir, packageName);
822
+ const existingPackage = findWorkspacePackage<WorkspacePackageManifest>(
823
+ rootDir,
824
+ packageName
825
+ );
886
826
  if (existingPackage) {
887
827
  return fail(
888
- `Workspace package name "${existingPackage.name}" already exists at ${existingPackage.workspacePath}.`
828
+ `Workspace package name "${packageName}" already exists at ${existingPackage.workspacePath}.`
889
829
  );
890
830
  }
891
831
 
@@ -51,7 +51,7 @@ export const deprecateTrail = trail('deprecate', {
51
51
  }),
52
52
  intent: 'write',
53
53
  output: z.object({
54
- file: z.string(),
54
+ filePath: z.string(),
55
55
  trailId: z.string(),
56
56
  updated: z.boolean(),
57
57
  }),
@@ -10,8 +10,8 @@ import { z } from 'zod';
10
10
  import { withFreshOperatorApp } from './operator-context.js';
11
11
  import { trailDetailOutput } from './topo-output-schemas.js';
12
12
  import {
13
- buildCurrentGuideEntries,
14
- buildCurrentTopoDetail,
13
+ deriveCurrentGuideEntries,
14
+ deriveCurrentTopoDetail,
15
15
  readSurfaceLayerNamesFromContext,
16
16
  } from './topo-read-support.js';
17
17
  import { createIsolatedExampleInput } from './topo-support.js';
@@ -53,7 +53,7 @@ export const guideTrail = trail('guide', {
53
53
  blaze: async (input: GuideTrailInput, ctx) =>
54
54
  withFreshOperatorApp<GuideTrailOutput>(input, ctx, ({ lease, rootDir }) => {
55
55
  if (input.trailId) {
56
- const detail = buildCurrentTopoDetail(lease.app, input.trailId, {
56
+ const detail = deriveCurrentTopoDetail(lease.app, input.trailId, {
57
57
  cliAliases: lease.cliAliases,
58
58
  rootDir,
59
59
  surfaceLayerNames: readSurfaceLayerNamesFromContext(ctx),
@@ -70,7 +70,7 @@ export const guideTrail = trail('guide', {
70
70
  }
71
71
 
72
72
  return Result.ok({
73
- entries: buildCurrentGuideEntries(lease.app, {
73
+ entries: deriveCurrentGuideEntries(lease.app, {
74
74
  rootDir,
75
75
  }) as GuideEntry[],
76
76
  mode: 'list' as const,
@@ -19,6 +19,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
19
19
  import {
20
20
  deriveSafePath,
21
21
  isTrailsError,
22
+ listWorkspacePackages,
22
23
  PermissionError,
23
24
  Result,
24
25
  ValidationError,
@@ -278,6 +279,8 @@ interface PackageJson {
278
279
  readonly workspaces?: unknown;
279
280
  }
280
281
 
282
+ type NamedPackageJson = PackageJson & { readonly name: string };
283
+
281
284
  const readPackageJson = (packagePath: string): PackageJson | undefined => {
282
285
  try {
283
286
  return JSON.parse(readFileSync(packagePath, 'utf8')) as PackageJson;
@@ -327,20 +330,6 @@ const isPackageLocalImport = (
327
330
  const isScannableModule = (modulePath: string): boolean =>
328
331
  SCANNABLE_EXTENSIONS.has(extname(modulePath));
329
332
 
330
- const resolveImportedModulePath = (
331
- importerPath: string,
332
- importPath: string
333
- ): string => {
334
- const resolved = import.meta.resolve(
335
- importPath,
336
- pathToFileURL(importerPath).href
337
- );
338
- return resolveFilesystemModulePath(
339
- fileURLToPath(resolved),
340
- dirname(importerPath)
341
- );
342
- };
343
-
344
333
  const readDirectoryEntries = (directoryPath: string): readonly string[] => {
345
334
  try {
346
335
  return readdirSync(directoryPath);
@@ -359,25 +348,18 @@ const safeStat = (
359
348
  }
360
349
  };
361
350
 
362
- const readWorkspacePatterns = (cwd: string): readonly string[] => {
363
- const rootPackage = readPackageJson(join(cwd, 'package.json'));
364
- const workspaces = rootPackage?.workspaces;
365
- if (Array.isArray(workspaces)) {
366
- return workspaces.filter(
367
- (pattern): pattern is string => typeof pattern === 'string'
368
- );
369
- }
370
- if (
371
- typeof workspaces === 'object' &&
372
- workspaces !== null &&
373
- 'packages' in workspaces &&
374
- Array.isArray(workspaces.packages)
375
- ) {
376
- return workspaces.packages.filter(
377
- (pattern): pattern is string => typeof pattern === 'string'
378
- );
379
- }
380
- return [];
351
+ const resolveImportedModulePath = (
352
+ importerPath: string,
353
+ importPath: string
354
+ ): string => {
355
+ const resolved = import.meta.resolve(
356
+ importPath,
357
+ pathToFileURL(importerPath).href
358
+ );
359
+ return resolveFilesystemModulePath(
360
+ fileURLToPath(resolved),
361
+ dirname(importerPath)
362
+ );
381
363
  };
382
364
 
383
365
  interface WorkspacePackage {
@@ -386,45 +368,12 @@ interface WorkspacePackage {
386
368
  readonly packageRoot: string;
387
369
  }
388
370
 
389
- const listWorkspacePackageRoots = (cwd: string): readonly string[] => {
390
- const roots: string[] = [];
391
- for (const pattern of readWorkspacePatterns(cwd)) {
392
- if (pattern.endsWith('/*')) {
393
- const baseDir = resolve(cwd, pattern.slice(0, -2));
394
- for (const entry of readDirectoryEntries(baseDir)) {
395
- const entryPath = join(baseDir, entry);
396
- if (safeStat(entryPath)?.isDirectory()) {
397
- roots.push(entryPath);
398
- }
399
- }
400
- continue;
401
- }
402
-
403
- if (!pattern.includes('*')) {
404
- roots.push(resolve(cwd, pattern));
405
- }
406
- }
407
- return roots;
408
- };
409
-
410
- const readWorkspacePackages = (cwd: string): readonly WorkspacePackage[] => {
411
- const packages: WorkspacePackage[] = [];
412
- for (const packageRoot of listWorkspacePackageRoots(cwd)) {
413
- const packageJson = readPackageJson(join(packageRoot, 'package.json'));
414
- if (
415
- packageJson !== undefined &&
416
- typeof packageJson.name === 'string' &&
417
- packageJson.name.length > 0
418
- ) {
419
- packages.push({
420
- name: packageJson.name,
421
- packageJson,
422
- packageRoot,
423
- });
424
- }
425
- }
426
- return packages;
427
- };
371
+ const readWorkspacePackages = (cwd: string): readonly WorkspacePackage[] =>
372
+ listWorkspacePackages<NamedPackageJson>(cwd).map((workspacePackage) => ({
373
+ name: workspacePackage.manifest.name,
374
+ packageJson: workspacePackage.manifest,
375
+ packageRoot: workspacePackage.packageRoot,
376
+ }));
428
377
 
429
378
  const parsePackageSpecifier = (
430
379
  importPath: string