@ankhorage/doctor 0.4.0 → 0.6.0
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 +12 -0
- package/README.md +8 -3
- package/dist/analysis.d.ts +1 -1
- package/dist/analysis.d.ts.map +1 -1
- package/dist/analysis.js.map +1 -1
- package/dist/cliLayoutAnalysis.d.ts.map +1 -1
- package/dist/cliLayoutAnalysis.js +284 -15
- package/dist/cliLayoutAnalysis.js.map +1 -1
- package/dist/commands.d.ts +3 -3
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +5 -5
- package/dist/commands.js.map +1 -1
- package/dist/diagnostics.d.ts +3 -3
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/manifestAnalysis.d.ts +6 -0
- package/dist/manifestAnalysis.d.ts.map +1 -0
- package/dist/manifestAnalysis.js +322 -0
- package/dist/manifestAnalysis.js.map +1 -0
- package/dist/readme-usage.js +6 -1
- package/dist/readme-usage.js.map +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +1 -1
- package/src/cliLayoutAnalysis.ts +369 -20
- package/src/commands.ts +7 -6
- package/src/diagnostics.ts +27 -1
- package/src/index.ts +5 -0
- package/src/manifestAnalysis.ts +458 -0
- package/src/readme-usage.ts +6 -1
package/src/cliLayoutAnalysis.ts
CHANGED
|
@@ -7,24 +7,89 @@ import {
|
|
|
7
7
|
type DoctorAnalysisResult,
|
|
8
8
|
} from './analysis.js';
|
|
9
9
|
import type { DoctorDiagnostic, DoctorPolicyProfile } from './diagnostics.js';
|
|
10
|
+
import { analyzeAppManifestTarget } from './manifestAnalysis.js';
|
|
10
11
|
|
|
11
12
|
const LEGACY_ROOT_CLI_SOURCE = path.join('src', 'cli.ts');
|
|
12
13
|
const CANONICAL_CLI_INDEX_SOURCE = path.join('src', 'cli', 'index.ts');
|
|
14
|
+
const ACTIVE_SOURCE_ROOTS = ['src', 'app', 'apps', 'packages', 'scripts'] as const;
|
|
15
|
+
const ACTIVE_SOURCE_EXTENSIONS = new Set([
|
|
16
|
+
'.cjs',
|
|
17
|
+
'.cts',
|
|
18
|
+
'.js',
|
|
19
|
+
'.jsx',
|
|
20
|
+
'.mjs',
|
|
21
|
+
'.mts',
|
|
22
|
+
'.ts',
|
|
23
|
+
'.tsx',
|
|
24
|
+
]);
|
|
25
|
+
const IGNORED_SOURCE_DIRECTORIES = new Set([
|
|
26
|
+
'.expo',
|
|
27
|
+
'.git',
|
|
28
|
+
'.next',
|
|
29
|
+
'__fixtures__',
|
|
30
|
+
'__tests__',
|
|
31
|
+
'build',
|
|
32
|
+
'coverage',
|
|
33
|
+
'dist',
|
|
34
|
+
'docs',
|
|
35
|
+
'fixtures',
|
|
36
|
+
'node_modules',
|
|
37
|
+
'paradox',
|
|
38
|
+
'test',
|
|
39
|
+
'tests',
|
|
40
|
+
]);
|
|
41
|
+
const COMPATIBILITY_PACKAGE_PREFIX = '@ankh/';
|
|
42
|
+
const STUDIO_PACKAGE_NAME = '@ankhorage/studio';
|
|
43
|
+
const STUDIO_DND_PACKAGE_NAME = '@ankhorage/react-native-reanimated-dnd-web';
|
|
44
|
+
const STUDIO_RUNTIME_PACKAGE_NAME = '@ankhorage/runtime';
|
|
45
|
+
const DEPENDENCY_FIELD_NAMES = [
|
|
46
|
+
'dependencies',
|
|
47
|
+
'devDependencies',
|
|
48
|
+
'optionalDependencies',
|
|
49
|
+
'peerDependencies',
|
|
50
|
+
] as const;
|
|
51
|
+
|
|
52
|
+
interface ActiveSourceImport {
|
|
53
|
+
readonly filePath: string;
|
|
54
|
+
readonly specifier: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface DependencyEntry {
|
|
58
|
+
readonly fieldName: (typeof DEPENDENCY_FIELD_NAMES)[number];
|
|
59
|
+
readonly packageName: string;
|
|
60
|
+
readonly version: string;
|
|
61
|
+
}
|
|
13
62
|
|
|
14
63
|
export async function analyzeDoctorTargetWithCliLayout(
|
|
15
64
|
request: DoctorAnalysisRequest,
|
|
16
65
|
): Promise<DoctorAnalysisResult> {
|
|
66
|
+
const manifestResult = await analyzeAppManifestTarget(request);
|
|
67
|
+
if (manifestResult !== null) {
|
|
68
|
+
return manifestResult;
|
|
69
|
+
}
|
|
70
|
+
|
|
17
71
|
const result = await analyzeDoctorTarget(request);
|
|
18
72
|
if (!result.hasPackageJson) {
|
|
19
73
|
return result;
|
|
20
74
|
}
|
|
21
75
|
|
|
22
|
-
const
|
|
23
|
-
|
|
76
|
+
const packageJsonPath = path.join(result.targetPath, 'package.json');
|
|
77
|
+
const packageJson = await readPackageJson(packageJsonPath);
|
|
78
|
+
if (packageJson === null) {
|
|
24
79
|
return result;
|
|
25
80
|
}
|
|
26
81
|
|
|
27
|
-
const
|
|
82
|
+
const architectureDiagnostics = await analyzeTargetArchitecture({
|
|
83
|
+
packageJson,
|
|
84
|
+
packageJsonPath,
|
|
85
|
+
profile: result.profile,
|
|
86
|
+
targetPath: result.targetPath,
|
|
87
|
+
});
|
|
88
|
+
if (architectureDiagnostics.length === 0) {
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const diagnostics = [...result.diagnostics, ...architectureDiagnostics];
|
|
28
93
|
|
|
29
94
|
return {
|
|
30
95
|
...result,
|
|
@@ -33,49 +98,333 @@ export async function analyzeDoctorTargetWithCliLayout(
|
|
|
33
98
|
};
|
|
34
99
|
}
|
|
35
100
|
|
|
36
|
-
async function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
101
|
+
async function analyzeTargetArchitecture(request: {
|
|
102
|
+
readonly packageJson: Record<string, unknown>;
|
|
103
|
+
readonly packageJsonPath: string;
|
|
104
|
+
readonly profile: DoctorPolicyProfile;
|
|
105
|
+
readonly targetPath: string;
|
|
106
|
+
}): Promise<DoctorDiagnostic[]> {
|
|
40
107
|
const diagnostics: DoctorDiagnostic[] = [];
|
|
41
|
-
const legacyRootCliPath = path.join(targetPath, LEGACY_ROOT_CLI_SOURCE);
|
|
42
|
-
const canonicalCliIndexPath = path.join(targetPath, CANONICAL_CLI_INDEX_SOURCE);
|
|
108
|
+
const legacyRootCliPath = path.join(request.targetPath, LEGACY_ROOT_CLI_SOURCE);
|
|
109
|
+
const canonicalCliIndexPath = path.join(request.targetPath, CANONICAL_CLI_INDEX_SOURCE);
|
|
110
|
+
const exportsField = isRecord(request.packageJson.exports) ? request.packageJson.exports : null;
|
|
111
|
+
const cliCapable =
|
|
112
|
+
hasAnkhProvider(request.packageJson) ||
|
|
113
|
+
exportsField?.['./cli'] !== undefined ||
|
|
114
|
+
(await pathExists(canonicalCliIndexPath));
|
|
43
115
|
|
|
44
116
|
if (await pathExists(legacyRootCliPath)) {
|
|
45
117
|
diagnostics.push({
|
|
46
118
|
code: 'field-invalid',
|
|
47
119
|
message: 'Package CLI code must live under src/cli/; root src/cli.ts is not allowed.',
|
|
48
120
|
path: legacyRootCliPath,
|
|
49
|
-
profile,
|
|
121
|
+
profile: request.profile,
|
|
50
122
|
ruleId: 'package.cli.root-file.disallowed',
|
|
51
123
|
severity: 'error',
|
|
52
124
|
});
|
|
53
125
|
}
|
|
54
126
|
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
packageJson !== null &&
|
|
58
|
-
exposesCliProvider(packageJson) &&
|
|
59
|
-
!(await pathExists(canonicalCliIndexPath))
|
|
60
|
-
) {
|
|
127
|
+
if (cliCapable && !(await pathExists(canonicalCliIndexPath))) {
|
|
61
128
|
diagnostics.push({
|
|
62
129
|
code: 'missing-path',
|
|
63
130
|
message: 'CLI-capable packages must expose their provider from src/cli/index.ts.',
|
|
64
131
|
path: canonicalCliIndexPath,
|
|
65
|
-
profile,
|
|
132
|
+
profile: request.profile,
|
|
66
133
|
ruleId: 'package.cli.index.required',
|
|
67
134
|
severity: 'error',
|
|
68
135
|
});
|
|
69
136
|
}
|
|
70
137
|
|
|
138
|
+
if (cliCapable && exportsField?.['./cli'] === undefined) {
|
|
139
|
+
diagnostics.push({
|
|
140
|
+
code: 'field-missing',
|
|
141
|
+
message:
|
|
142
|
+
'CLI-capable packages must export "./cli" from package.json and point it at the owning src/cli/index.ts build output.',
|
|
143
|
+
path: request.packageJsonPath,
|
|
144
|
+
profile: request.profile,
|
|
145
|
+
ruleId: 'package.cli.export.required',
|
|
146
|
+
severity: 'error',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const dependencyEntries = collectDependencyEntries(request.packageJson);
|
|
151
|
+
diagnostics.push(
|
|
152
|
+
...validateDependencyArchitecture({
|
|
153
|
+
dependencyEntries,
|
|
154
|
+
packageJsonPath: request.packageJsonPath,
|
|
155
|
+
profile: request.profile,
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const activeSourceImports = await collectActiveSourceImports(request.targetPath);
|
|
160
|
+
diagnostics.push(
|
|
161
|
+
...validateActiveSourceImports({
|
|
162
|
+
activeSourceImports,
|
|
163
|
+
profile: request.profile,
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (request.packageJson.name === STUDIO_PACKAGE_NAME) {
|
|
168
|
+
diagnostics.push(
|
|
169
|
+
...validateStudioOwnership({
|
|
170
|
+
activeSourceImports,
|
|
171
|
+
dependencyEntries,
|
|
172
|
+
packageJsonPath: request.packageJsonPath,
|
|
173
|
+
profile: request.profile,
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
71
178
|
return diagnostics;
|
|
72
179
|
}
|
|
73
180
|
|
|
74
|
-
function
|
|
181
|
+
function validateDependencyArchitecture(request: {
|
|
182
|
+
readonly dependencyEntries: readonly DependencyEntry[];
|
|
183
|
+
readonly packageJsonPath: string;
|
|
184
|
+
readonly profile: DoctorPolicyProfile;
|
|
185
|
+
}): DoctorDiagnostic[] {
|
|
186
|
+
const diagnostics: DoctorDiagnostic[] = [];
|
|
187
|
+
|
|
188
|
+
for (const dependency of request.dependencyEntries) {
|
|
189
|
+
if (dependency.packageName.startsWith(COMPATIBILITY_PACKAGE_PREFIX)) {
|
|
190
|
+
diagnostics.push({
|
|
191
|
+
code: 'field-invalid',
|
|
192
|
+
message: `Dependency "${dependency.packageName}" in package.json.${dependency.fieldName} is an old ankhorage4 workspace alias. Depend on "${toOwningPackageSpecifier(dependency.packageName)}" directly.`,
|
|
193
|
+
path: request.packageJsonPath,
|
|
194
|
+
profile: request.profile,
|
|
195
|
+
ruleId: 'package.dependencies.ankh-workspace-alias.disallowed',
|
|
196
|
+
severity: 'error',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (
|
|
201
|
+
dependency.packageName.includes('ankhorage4') ||
|
|
202
|
+
dependency.version.includes('ankhorage4')
|
|
203
|
+
) {
|
|
204
|
+
diagnostics.push({
|
|
205
|
+
code: 'field-invalid',
|
|
206
|
+
message: `Dependency "${dependency.packageName}" references ankhorage4 as active source. Depend on the extracted owning @ankhorage/* package instead.`,
|
|
207
|
+
path: request.packageJsonPath,
|
|
208
|
+
profile: request.profile,
|
|
209
|
+
ruleId: 'package.dependencies.ankhorage4-source.disallowed',
|
|
210
|
+
severity: 'error',
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return diagnostics;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function validateActiveSourceImports(request: {
|
|
219
|
+
readonly activeSourceImports: readonly ActiveSourceImport[];
|
|
220
|
+
readonly profile: DoctorPolicyProfile;
|
|
221
|
+
}): DoctorDiagnostic[] {
|
|
222
|
+
const diagnostics: DoctorDiagnostic[] = [];
|
|
223
|
+
|
|
224
|
+
for (const sourceImport of request.activeSourceImports) {
|
|
225
|
+
if (sourceImport.specifier.startsWith(COMPATIBILITY_PACKAGE_PREFIX)) {
|
|
226
|
+
diagnostics.push({
|
|
227
|
+
code: 'field-invalid',
|
|
228
|
+
message: `Import "${sourceImport.specifier}" is an old ankhorage4 compatibility boundary. Import "${toOwningPackageSpecifier(sourceImport.specifier)}" directly from the owning package.`,
|
|
229
|
+
path: sourceImport.filePath,
|
|
230
|
+
profile: request.profile,
|
|
231
|
+
ruleId: 'package.imports.ankh-workspace-alias.disallowed',
|
|
232
|
+
severity: 'error',
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (sourceImport.specifier.includes('ankhorage4')) {
|
|
237
|
+
diagnostics.push({
|
|
238
|
+
code: 'field-invalid',
|
|
239
|
+
message: `Import "${sourceImport.specifier}" references ankhorage4 as active source. Import the extracted owning @ankhorage/* package instead.`,
|
|
240
|
+
path: sourceImport.filePath,
|
|
241
|
+
profile: request.profile,
|
|
242
|
+
ruleId: 'package.imports.ankhorage4-source.disallowed',
|
|
243
|
+
severity: 'error',
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return diagnostics;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function validateStudioOwnership(request: {
|
|
252
|
+
readonly activeSourceImports: readonly ActiveSourceImport[];
|
|
253
|
+
readonly dependencyEntries: readonly DependencyEntry[];
|
|
254
|
+
readonly packageJsonPath: string;
|
|
255
|
+
readonly profile: DoctorPolicyProfile;
|
|
256
|
+
}): DoctorDiagnostic[] {
|
|
257
|
+
const diagnostics: DoctorDiagnostic[] = [];
|
|
258
|
+
const directDependencies = new Set(
|
|
259
|
+
request.dependencyEntries
|
|
260
|
+
.filter((dependency) => dependency.fieldName === 'dependencies')
|
|
261
|
+
.map((dependency) => dependency.packageName),
|
|
262
|
+
);
|
|
263
|
+
const importedSpecifiers = request.activeSourceImports.map(
|
|
264
|
+
(sourceImport) => sourceImport.specifier,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
if (!directDependencies.has(STUDIO_DND_PACKAGE_NAME)) {
|
|
268
|
+
diagnostics.push({
|
|
269
|
+
code: 'missing-dependency',
|
|
270
|
+
message: `@ankhorage/studio must depend directly on "${STUDIO_DND_PACKAGE_NAME}" for Studio drag-and-drop ownership.`,
|
|
271
|
+
path: request.packageJsonPath,
|
|
272
|
+
profile: request.profile,
|
|
273
|
+
ruleId: 'studio.dependencies.dnd.required',
|
|
274
|
+
severity: 'error',
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!directDependencies.has(STUDIO_RUNTIME_PACKAGE_NAME)) {
|
|
279
|
+
diagnostics.push({
|
|
280
|
+
code: 'missing-dependency',
|
|
281
|
+
message: `@ankhorage/studio must depend directly on "${STUDIO_RUNTIME_PACKAGE_NAME}" for runtime ownership.`,
|
|
282
|
+
path: request.packageJsonPath,
|
|
283
|
+
profile: request.profile,
|
|
284
|
+
ruleId: 'studio.dependencies.runtime.required',
|
|
285
|
+
severity: 'error',
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (
|
|
290
|
+
!importedSpecifiers.some((specifier) => isPackageImport(specifier, STUDIO_DND_PACKAGE_NAME))
|
|
291
|
+
) {
|
|
292
|
+
diagnostics.push({
|
|
293
|
+
code: 'field-missing',
|
|
294
|
+
message: `@ankhorage/studio must import "${STUDIO_DND_PACKAGE_NAME}" directly from its Studio DnD boundary.`,
|
|
295
|
+
path: request.packageJsonPath,
|
|
296
|
+
profile: request.profile,
|
|
297
|
+
ruleId: 'studio.imports.dnd.required',
|
|
298
|
+
severity: 'error',
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (
|
|
303
|
+
!importedSpecifiers.some((specifier) => isPackageImport(specifier, STUDIO_RUNTIME_PACKAGE_NAME))
|
|
304
|
+
) {
|
|
305
|
+
diagnostics.push({
|
|
306
|
+
code: 'field-missing',
|
|
307
|
+
message: `@ankhorage/studio must import "${STUDIO_RUNTIME_PACKAGE_NAME}" directly from the owning runtime package.`,
|
|
308
|
+
path: request.packageJsonPath,
|
|
309
|
+
profile: request.profile,
|
|
310
|
+
ruleId: 'studio.imports.runtime.required',
|
|
311
|
+
severity: 'error',
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return diagnostics;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function collectActiveSourceImports(targetPath: string): Promise<ActiveSourceImport[]> {
|
|
319
|
+
const imports: ActiveSourceImport[] = [];
|
|
320
|
+
|
|
321
|
+
for (const sourceRoot of ACTIVE_SOURCE_ROOTS) {
|
|
322
|
+
const sourceRootPath = path.join(targetPath, sourceRoot);
|
|
323
|
+
if (!(await pathExists(sourceRootPath))) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
for (const filePath of await listActiveSourceFiles(sourceRootPath)) {
|
|
328
|
+
const source = await fs.readFile(filePath, 'utf8');
|
|
329
|
+
for (const specifier of extractImportSpecifiers(source)) {
|
|
330
|
+
imports.push({ filePath, specifier });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return imports;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function listActiveSourceFiles(rootPath: string): Promise<string[]> {
|
|
339
|
+
const files: string[] = [];
|
|
340
|
+
await visit(rootPath);
|
|
341
|
+
return files.sort((left, right) => left.localeCompare(right));
|
|
342
|
+
|
|
343
|
+
async function visit(currentPath: string): Promise<void> {
|
|
344
|
+
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
345
|
+
|
|
346
|
+
for (const entry of entries) {
|
|
347
|
+
if (entry.isDirectory()) {
|
|
348
|
+
if (!IGNORED_SOURCE_DIRECTORIES.has(entry.name)) {
|
|
349
|
+
await visit(path.join(currentPath, entry.name));
|
|
350
|
+
}
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (!entry.isFile()) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const filePath = path.join(currentPath, entry.name);
|
|
359
|
+
if (isActiveSourceFile(filePath)) {
|
|
360
|
+
files.push(filePath);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function isActiveSourceFile(filePath: string): boolean {
|
|
367
|
+
if (!ACTIVE_SOURCE_EXTENSIONS.has(path.extname(filePath))) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return !/(?:^|\.)(?:spec|test)\.[cm]?[jt]sx?$/.test(path.basename(filePath));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function extractImportSpecifiers(source: string): string[] {
|
|
375
|
+
const specifiers = new Set<string>();
|
|
376
|
+
const patterns = [
|
|
377
|
+
/\b(?:import|export)\s+(?:type\s+)?(?:[^;]*?\sfrom\s*)?['"]([^'"]+)['"]/g,
|
|
378
|
+
/\b(?:import|require)\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
379
|
+
];
|
|
380
|
+
|
|
381
|
+
for (const pattern of patterns) {
|
|
382
|
+
for (const match of source.matchAll(pattern)) {
|
|
383
|
+
const [, specifier] = match;
|
|
384
|
+
if (specifier !== undefined) {
|
|
385
|
+
specifiers.add(specifier);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return [...specifiers].sort((left, right) => left.localeCompare(right));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function collectDependencyEntries(packageJson: Record<string, unknown>): DependencyEntry[] {
|
|
394
|
+
const entries: DependencyEntry[] = [];
|
|
395
|
+
|
|
396
|
+
for (const fieldName of DEPENDENCY_FIELD_NAMES) {
|
|
397
|
+
const dependencies = isRecord(packageJson[fieldName]) ? packageJson[fieldName] : null;
|
|
398
|
+
if (dependencies === null) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
for (const [packageName, version] of Object.entries(dependencies)) {
|
|
403
|
+
if (isNonEmptyString(version)) {
|
|
404
|
+
entries.push({ fieldName, packageName, version });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return entries.sort((left, right) => {
|
|
410
|
+
const fieldComparison = left.fieldName.localeCompare(right.fieldName);
|
|
411
|
+
return fieldComparison !== 0
|
|
412
|
+
? fieldComparison
|
|
413
|
+
: left.packageName.localeCompare(right.packageName);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function hasAnkhProvider(packageJson: Record<string, unknown>): boolean {
|
|
75
418
|
const ankh = isRecord(packageJson.ankh) ? packageJson.ankh : null;
|
|
76
|
-
|
|
419
|
+
return isNonEmptyString(ankh?.provider);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function isPackageImport(specifier: string, packageName: string): boolean {
|
|
423
|
+
return specifier === packageName || specifier.startsWith(`${packageName}/`);
|
|
424
|
+
}
|
|
77
425
|
|
|
78
|
-
|
|
426
|
+
function toOwningPackageSpecifier(specifier: string): string {
|
|
427
|
+
return `@ankhorage/${specifier.slice(COMPATIBILITY_PACKAGE_PREFIX.length)}`;
|
|
79
428
|
}
|
|
80
429
|
|
|
81
430
|
async function readPackageJson(packageJsonPath: string): Promise<Record<string, unknown> | null> {
|
package/src/commands.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AnkhCapabilityId, AnkhCommandDescriptor } from '@ankhorage/contracts/cli';
|
|
2
2
|
|
|
3
3
|
import type { DoctorAnalysisResult, DoctorPlannedChange, DoctorTargetMode } from './analysis.js';
|
|
4
|
-
import {
|
|
4
|
+
import { analyzeDoctorTargetWithCliLayout } from './cliLayoutAnalysis.js';
|
|
5
5
|
import type { DoctorCommandContext, DoctorCommandRunResult } from './commandContext.js';
|
|
6
6
|
import {
|
|
7
7
|
countErrorDiagnostics,
|
|
@@ -38,7 +38,7 @@ export interface DoctorCommandInvocation {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export interface DoctorCommandServices {
|
|
41
|
-
readonly analyzeTarget: typeof
|
|
41
|
+
readonly analyzeTarget: typeof analyzeDoctorTargetWithCliLayout;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export interface RunDoctorCommandOptions {
|
|
@@ -64,7 +64,8 @@ export const DOCTOR_COMMANDS = [
|
|
|
64
64
|
path: ['validate'],
|
|
65
65
|
capability: COMMAND_CAPABILITIES.validate,
|
|
66
66
|
mode: 'validate',
|
|
67
|
-
summary:
|
|
67
|
+
summary:
|
|
68
|
+
'Validate a repo, package, or app-manifest JSON path with the applicable Doctor profile.',
|
|
68
69
|
run: runValidateCommand,
|
|
69
70
|
},
|
|
70
71
|
{
|
|
@@ -147,7 +148,7 @@ export function renderRootHelp(version: string): string {
|
|
|
147
148
|
commandLines,
|
|
148
149
|
'',
|
|
149
150
|
'Path resolution:',
|
|
150
|
-
' Pass
|
|
151
|
+
' Pass a repo/package directory or app-manifest JSON file, or omit [path] to inspect the current directory.',
|
|
151
152
|
'',
|
|
152
153
|
].join('\n');
|
|
153
154
|
}
|
|
@@ -165,7 +166,7 @@ function renderCommandHelp(command: DoctorCommandDefinition): string {
|
|
|
165
166
|
` ankh ${DOCTOR_COMMAND_CATEGORY} ${command.path.join(' ')} [path]`,
|
|
166
167
|
'',
|
|
167
168
|
'Path resolution:',
|
|
168
|
-
' Pass
|
|
169
|
+
' Pass a repo/package directory or app-manifest JSON file, or omit [path] to inspect the current directory.',
|
|
169
170
|
'',
|
|
170
171
|
].join('\n');
|
|
171
172
|
}
|
|
@@ -174,7 +175,7 @@ function createDoctorCommandServices(
|
|
|
174
175
|
overrides: Partial<DoctorCommandServices> = {},
|
|
175
176
|
): DoctorCommandServices {
|
|
176
177
|
return {
|
|
177
|
-
analyzeTarget: overrides.analyzeTarget ??
|
|
178
|
+
analyzeTarget: overrides.analyzeTarget ?? analyzeDoctorTargetWithCliLayout,
|
|
178
179
|
};
|
|
179
180
|
}
|
|
180
181
|
|
package/src/diagnostics.ts
CHANGED
|
@@ -2,6 +2,7 @@ export type DoctorDiagnosticCode =
|
|
|
2
2
|
| 'field-invalid'
|
|
3
3
|
| 'field-missing'
|
|
4
4
|
| 'invalid-ankh-metadata'
|
|
5
|
+
| 'invalid-app-manifest-json'
|
|
5
6
|
| 'invalid-package-json'
|
|
6
7
|
| 'missing-dependency'
|
|
7
8
|
| 'missing-path'
|
|
@@ -13,23 +14,44 @@ export type DoctorDiagnosticCode =
|
|
|
13
14
|
| 'unsupported-target';
|
|
14
15
|
|
|
15
16
|
export type DoctorDiagnosticSeverity = 'error' | 'warning';
|
|
16
|
-
export type DoctorPolicyProfile =
|
|
17
|
+
export type DoctorPolicyProfile =
|
|
18
|
+
'app-manifest' | 'integration-monorepo' | 'public-package' | 'unknown';
|
|
17
19
|
export type DoctorRuleId =
|
|
18
20
|
| 'integration.package.ankh.disallowed'
|
|
19
21
|
| 'integration.package.private.required'
|
|
20
22
|
| 'integration.package.workspaces.required'
|
|
23
|
+
| 'manifest.auth.authorization.engine.valid'
|
|
24
|
+
| 'manifest.auth.authorization.kind.valid'
|
|
25
|
+
| 'manifest.auth.authorization.valid-shape'
|
|
26
|
+
| 'manifest.auth.flow.post-sign-in.distinct'
|
|
27
|
+
| 'manifest.auth.flow.route.required'
|
|
28
|
+
| 'manifest.auth.flow.route.unique'
|
|
29
|
+
| 'manifest.auth.flow.route.valid'
|
|
30
|
+
| 'manifest.auth.flow.unauthorized-route.valid'
|
|
31
|
+
| 'manifest.auth.flow.valid-shape'
|
|
32
|
+
| 'manifest.auth.valid-shape'
|
|
33
|
+
| 'manifest.infra.valid-shape'
|
|
34
|
+
| 'manifest.json.readable'
|
|
35
|
+
| 'manifest.json.valid'
|
|
36
|
+
| 'manifest.root.valid-shape'
|
|
37
|
+
| 'manifest.settings.auth-flow.removed'
|
|
21
38
|
| 'package.ankh.capabilities.match-provider'
|
|
22
39
|
| 'package.ankh.present.valid-shape'
|
|
23
40
|
| 'package.ankh.provider-path.required'
|
|
24
41
|
| 'package.ankh.required-for-provider'
|
|
42
|
+
| 'package.cli.export.required'
|
|
25
43
|
| 'package.cli.index.required'
|
|
26
44
|
| 'package.cli.root-file.disallowed'
|
|
45
|
+
| 'package.dependencies.ankh-workspace-alias.disallowed'
|
|
46
|
+
| 'package.dependencies.ankhorage4-source.disallowed'
|
|
27
47
|
| 'package.dependencies.changesets.required'
|
|
28
48
|
| 'package.dependencies.devtools.required'
|
|
29
49
|
| 'package.dependencies.paradox.required'
|
|
30
50
|
| 'package.dependencies.types-bun.required'
|
|
31
51
|
| 'package.dependencies.types-node.required'
|
|
32
52
|
| 'package.dependencies.typescript.required'
|
|
53
|
+
| 'package.imports.ankh-workspace-alias.disallowed'
|
|
54
|
+
| 'package.imports.ankhorage4-source.disallowed'
|
|
33
55
|
| 'package.json.bugs.required'
|
|
34
56
|
| 'package.json.description.required'
|
|
35
57
|
| 'package.json.exports.required'
|
|
@@ -68,6 +90,10 @@ export type DoctorRuleId =
|
|
|
68
90
|
| 'repo.license.required'
|
|
69
91
|
| 'repo.readme.required'
|
|
70
92
|
| 'repo.workflows.required'
|
|
93
|
+
| 'studio.dependencies.dnd.required'
|
|
94
|
+
| 'studio.dependencies.runtime.required'
|
|
95
|
+
| 'studio.imports.dnd.required'
|
|
96
|
+
| 'studio.imports.runtime.required'
|
|
71
97
|
| 'target.package-json.required'
|
|
72
98
|
| 'target.path.directory'
|
|
73
99
|
| 'target.path.exists'
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,11 @@ export type {
|
|
|
23
23
|
DoctorPolicyProfile,
|
|
24
24
|
DoctorRuleId,
|
|
25
25
|
} from './diagnostics.js';
|
|
26
|
+
export {
|
|
27
|
+
analyzeAppManifest,
|
|
28
|
+
analyzeAppManifestFile,
|
|
29
|
+
analyzeAppManifestTarget,
|
|
30
|
+
} from './manifestAnalysis.js';
|
|
26
31
|
export {
|
|
27
32
|
DOCTOR_CAPABILITIES,
|
|
28
33
|
DOCTOR_COMMAND_CATEGORY,
|