@ankhorage/doctor 0.11.7 → 0.11.9
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 +1 -1
- package/dist/analysis.d.ts.map +1 -1
- package/dist/analysis.js +62 -181
- package/dist/analysis.js.map +1 -1
- package/dist/cliLayoutAnalysis.d.ts.map +1 -1
- package/dist/cliLayoutAnalysis.js +26 -26
- package/dist/cliLayoutAnalysis.js.map +1 -1
- package/dist/sourceArchitectureAnalysis.d.ts.map +1 -1
- package/dist/sourceArchitectureAnalysis.js +35 -70
- package/dist/sourceArchitectureAnalysis.js.map +1 -1
- package/dist/utils/getArchitecturePolicyRule.d.ts +281 -0
- package/dist/utils/getArchitecturePolicyRule.d.ts.map +1 -0
- package/dist/utils/getArchitecturePolicyRule.js +12 -0
- package/dist/utils/getArchitecturePolicyRule.js.map +1 -0
- package/package.json +3 -3
- package/src/analysis.ts +83 -218
- package/src/cliLayoutAnalysis.ts +33 -26
- package/src/sourceArchitectureAnalysis.ts +59 -105
- package/src/utils/getArchitecturePolicyRule.ts +15 -0
package/src/analysis.ts
CHANGED
|
@@ -2,10 +2,12 @@ import { promises as fs } from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
|
|
5
|
+
import { ARCHITECTURE_POLICY } from '@ankhorage/policy/architecture';
|
|
5
6
|
import { uniqueSortedStrings } from '@ankhorage/utility/array';
|
|
6
7
|
|
|
7
8
|
import type { DoctorDiagnostic, DoctorPolicyProfile, DoctorRuleId } from './diagnostics.js';
|
|
8
9
|
import type { DoctorReadiness } from './readiness.js';
|
|
10
|
+
import { getArchitecturePolicyRule } from './utils/getArchitecturePolicyRule.js';
|
|
9
11
|
|
|
10
12
|
export type DoctorTargetCheck = 'manifest' | 'package' | 'repo';
|
|
11
13
|
export type DoctorTargetMode = 'fix' | 'package' | 'repo' | 'validate';
|
|
@@ -65,21 +67,7 @@ const REPO_MARKERS = [
|
|
|
65
67
|
{ label: 'yarn.lock', relativePath: 'yarn.lock' },
|
|
66
68
|
] as const;
|
|
67
69
|
|
|
68
|
-
const
|
|
69
|
-
'build',
|
|
70
|
-
'typecheck',
|
|
71
|
-
'lint',
|
|
72
|
-
'lint:fix',
|
|
73
|
-
'format',
|
|
74
|
-
'format:check',
|
|
75
|
-
'test',
|
|
76
|
-
'test:standalone',
|
|
77
|
-
'knip:check',
|
|
78
|
-
'docs',
|
|
79
|
-
'changeset',
|
|
80
|
-
'changeset:status',
|
|
81
|
-
'version-packages',
|
|
82
|
-
] as const;
|
|
70
|
+
const PUBLIC_PACKAGE_POLICY = ARCHITECTURE_POLICY.publicPackage;
|
|
83
71
|
|
|
84
72
|
const PROVIDER_SOURCE_CANDIDATES = [
|
|
85
73
|
path.join('src', 'ankh.provider.ts'),
|
|
@@ -245,46 +233,16 @@ async function analyzeRepoPolicy(request: {
|
|
|
245
233
|
const diagnostics: DoctorDiagnostic[] = [];
|
|
246
234
|
|
|
247
235
|
if (request.profile === 'public-package') {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
request.plannedChanges,
|
|
259
|
-
request.targetPath,
|
|
260
|
-
'CHANGELOG.md',
|
|
261
|
-
'repo.changelog.required',
|
|
262
|
-
'create-file',
|
|
263
|
-
);
|
|
264
|
-
await maybeRequirePath(
|
|
265
|
-
diagnostics,
|
|
266
|
-
request.plannedChanges,
|
|
267
|
-
request.targetPath,
|
|
268
|
-
'LICENSE',
|
|
269
|
-
'repo.license.required',
|
|
270
|
-
'create-file',
|
|
271
|
-
);
|
|
272
|
-
await maybeRequirePath(
|
|
273
|
-
diagnostics,
|
|
274
|
-
request.plannedChanges,
|
|
275
|
-
request.targetPath,
|
|
276
|
-
'.changeset',
|
|
277
|
-
'repo.changeset.required',
|
|
278
|
-
'create-directory',
|
|
279
|
-
);
|
|
280
|
-
await maybeRequirePath(
|
|
281
|
-
diagnostics,
|
|
282
|
-
request.plannedChanges,
|
|
283
|
-
request.targetPath,
|
|
284
|
-
path.join('.github', 'workflows'),
|
|
285
|
-
'repo.workflows.required',
|
|
286
|
-
'create-directory',
|
|
287
|
-
);
|
|
236
|
+
for (const artifact of PUBLIC_PACKAGE_POLICY.requiredRepoPaths) {
|
|
237
|
+
await maybeRequirePath(
|
|
238
|
+
diagnostics,
|
|
239
|
+
request.plannedChanges,
|
|
240
|
+
request.targetPath,
|
|
241
|
+
artifact.path,
|
|
242
|
+
artifact.ruleId,
|
|
243
|
+
artifact.kind === 'file' ? 'create-file' : 'create-directory',
|
|
244
|
+
);
|
|
245
|
+
}
|
|
288
246
|
}
|
|
289
247
|
|
|
290
248
|
if (request.profile === 'integration-monorepo') {
|
|
@@ -362,29 +320,11 @@ async function analyzePackagePolicy(request: {
|
|
|
362
320
|
const { packageJson } = request;
|
|
363
321
|
|
|
364
322
|
if (request.profile === 'public-package') {
|
|
365
|
-
|
|
323
|
+
requirePublicPackageFields({
|
|
366
324
|
diagnostics,
|
|
367
325
|
packageJson,
|
|
368
326
|
packageJsonPath: request.packageJsonPath,
|
|
369
327
|
profile: request.profile,
|
|
370
|
-
ruleId: 'package.json.name.required',
|
|
371
|
-
fieldName: 'name',
|
|
372
|
-
});
|
|
373
|
-
requireNonEmptyStringField({
|
|
374
|
-
diagnostics,
|
|
375
|
-
packageJson,
|
|
376
|
-
packageJsonPath: request.packageJsonPath,
|
|
377
|
-
profile: request.profile,
|
|
378
|
-
ruleId: 'package.json.version.required',
|
|
379
|
-
fieldName: 'version',
|
|
380
|
-
});
|
|
381
|
-
requireNonEmptyStringField({
|
|
382
|
-
diagnostics,
|
|
383
|
-
packageJson,
|
|
384
|
-
packageJsonPath: request.packageJsonPath,
|
|
385
|
-
profile: request.profile,
|
|
386
|
-
ruleId: 'package.json.type.required',
|
|
387
|
-
fieldName: 'type',
|
|
388
328
|
});
|
|
389
329
|
}
|
|
390
330
|
|
|
@@ -401,79 +341,6 @@ async function analyzePackagePolicy(request: {
|
|
|
401
341
|
);
|
|
402
342
|
}
|
|
403
343
|
|
|
404
|
-
requireNonEmptyStringField({
|
|
405
|
-
diagnostics,
|
|
406
|
-
packageJson,
|
|
407
|
-
packageJsonPath: request.packageJsonPath,
|
|
408
|
-
profile: request.profile,
|
|
409
|
-
ruleId: 'package.json.description.required',
|
|
410
|
-
fieldName: 'description',
|
|
411
|
-
});
|
|
412
|
-
requireRecordField({
|
|
413
|
-
diagnostics,
|
|
414
|
-
packageJson,
|
|
415
|
-
packageJsonPath: request.packageJsonPath,
|
|
416
|
-
profile: request.profile,
|
|
417
|
-
ruleId: 'package.json.repository.required',
|
|
418
|
-
fieldName: 'repository',
|
|
419
|
-
});
|
|
420
|
-
requireNonEmptyStringField({
|
|
421
|
-
diagnostics,
|
|
422
|
-
packageJson,
|
|
423
|
-
packageJsonPath: request.packageJsonPath,
|
|
424
|
-
profile: request.profile,
|
|
425
|
-
ruleId: 'package.json.homepage.required',
|
|
426
|
-
fieldName: 'homepage',
|
|
427
|
-
});
|
|
428
|
-
requireRecordField({
|
|
429
|
-
diagnostics,
|
|
430
|
-
packageJson,
|
|
431
|
-
packageJsonPath: request.packageJsonPath,
|
|
432
|
-
profile: request.profile,
|
|
433
|
-
ruleId: 'package.json.bugs.required',
|
|
434
|
-
fieldName: 'bugs',
|
|
435
|
-
});
|
|
436
|
-
requireNonEmptyStringField({
|
|
437
|
-
diagnostics,
|
|
438
|
-
packageJson,
|
|
439
|
-
packageJsonPath: request.packageJsonPath,
|
|
440
|
-
profile: request.profile,
|
|
441
|
-
ruleId: 'package.json.license.required',
|
|
442
|
-
fieldName: 'license',
|
|
443
|
-
});
|
|
444
|
-
requireStringArrayField({
|
|
445
|
-
diagnostics,
|
|
446
|
-
packageJson,
|
|
447
|
-
packageJsonPath: request.packageJsonPath,
|
|
448
|
-
profile: request.profile,
|
|
449
|
-
ruleId: 'package.json.keywords.required',
|
|
450
|
-
fieldName: 'keywords',
|
|
451
|
-
});
|
|
452
|
-
requireStringArrayField({
|
|
453
|
-
diagnostics,
|
|
454
|
-
packageJson,
|
|
455
|
-
packageJsonPath: request.packageJsonPath,
|
|
456
|
-
profile: request.profile,
|
|
457
|
-
ruleId: 'package.json.files.required',
|
|
458
|
-
fieldName: 'files',
|
|
459
|
-
});
|
|
460
|
-
requireRecordField({
|
|
461
|
-
diagnostics,
|
|
462
|
-
packageJson,
|
|
463
|
-
packageJsonPath: request.packageJsonPath,
|
|
464
|
-
profile: request.profile,
|
|
465
|
-
ruleId: 'package.json.exports.required',
|
|
466
|
-
fieldName: 'exports',
|
|
467
|
-
});
|
|
468
|
-
requireRecordField({
|
|
469
|
-
diagnostics,
|
|
470
|
-
packageJson,
|
|
471
|
-
packageJsonPath: request.packageJsonPath,
|
|
472
|
-
profile: request.profile,
|
|
473
|
-
ruleId: 'package.json.publish-config.required',
|
|
474
|
-
fieldName: 'publishConfig',
|
|
475
|
-
});
|
|
476
|
-
|
|
477
344
|
if (packageJson.private === true) {
|
|
478
345
|
diagnostics.push(
|
|
479
346
|
createDiagnostic({
|
|
@@ -481,35 +348,38 @@ async function analyzePackagePolicy(request: {
|
|
|
481
348
|
message: 'Public package profiles must not set private: true.',
|
|
482
349
|
path: request.packageJsonPath,
|
|
483
350
|
profile: request.profile,
|
|
484
|
-
|
|
485
|
-
severity: 'error',
|
|
351
|
+
...architecturePolicyRuleFields(PUBLIC_PACKAGE_POLICY.privateDisallowedRuleId),
|
|
486
352
|
}),
|
|
487
353
|
);
|
|
488
354
|
}
|
|
489
355
|
|
|
490
|
-
if (
|
|
356
|
+
if (
|
|
357
|
+
packageJson.type !== undefined &&
|
|
358
|
+
packageJson.type !== PUBLIC_PACKAGE_POLICY.packageType.value
|
|
359
|
+
) {
|
|
491
360
|
diagnostics.push(
|
|
492
361
|
createDiagnostic({
|
|
493
362
|
code: 'field-invalid',
|
|
494
|
-
message:
|
|
363
|
+
message: `package.json "type" must be "${PUBLIC_PACKAGE_POLICY.packageType.value}".`,
|
|
495
364
|
path: request.packageJsonPath,
|
|
496
365
|
profile: request.profile,
|
|
497
|
-
|
|
498
|
-
severity: 'error',
|
|
366
|
+
...architecturePolicyRuleFields(PUBLIC_PACKAGE_POLICY.packageType.ruleId),
|
|
499
367
|
}),
|
|
500
368
|
);
|
|
501
369
|
}
|
|
502
370
|
|
|
503
371
|
const { publishConfig } = packageJson;
|
|
504
|
-
if (
|
|
372
|
+
if (
|
|
373
|
+
isRecord(publishConfig) &&
|
|
374
|
+
publishConfig.access !== PUBLIC_PACKAGE_POLICY.publishAccess.value
|
|
375
|
+
) {
|
|
505
376
|
diagnostics.push(
|
|
506
377
|
createDiagnostic({
|
|
507
378
|
code: isNonEmptyString(publishConfig.access) ? 'field-invalid' : 'field-missing',
|
|
508
|
-
message:
|
|
379
|
+
message: `package.json publishConfig.access must exist and equal "${PUBLIC_PACKAGE_POLICY.publishAccess.value}".`,
|
|
509
380
|
path: request.packageJsonPath,
|
|
510
381
|
profile: request.profile,
|
|
511
|
-
|
|
512
|
-
severity: 'error',
|
|
382
|
+
...architecturePolicyRuleFields(PUBLIC_PACKAGE_POLICY.publishAccess.ruleId),
|
|
513
383
|
}),
|
|
514
384
|
);
|
|
515
385
|
}
|
|
@@ -521,8 +391,7 @@ async function analyzePackagePolicy(request: {
|
|
|
521
391
|
message: 'package.json must define a Bun-aligned packageManager field.',
|
|
522
392
|
path: request.packageJsonPath,
|
|
523
393
|
profile: request.profile,
|
|
524
|
-
|
|
525
|
-
severity: 'error',
|
|
394
|
+
...architecturePolicyRuleFields(PUBLIC_PACKAGE_POLICY.packageManager.requiredRuleId),
|
|
526
395
|
}),
|
|
527
396
|
);
|
|
528
397
|
request.plannedChanges.push(
|
|
@@ -530,7 +399,7 @@ async function analyzePackagePolicy(request: {
|
|
|
530
399
|
description: `Add packageManager: "bun@${Bun.version}" to package.json.`,
|
|
531
400
|
filePath: request.packageJsonPath,
|
|
532
401
|
kind: 'update-json',
|
|
533
|
-
ruleId:
|
|
402
|
+
ruleId: PUBLIC_PACKAGE_POLICY.packageManager.requiredRuleId,
|
|
534
403
|
}),
|
|
535
404
|
);
|
|
536
405
|
} else if (!packageJson.packageManager.startsWith('bun@')) {
|
|
@@ -540,23 +409,21 @@ async function analyzePackagePolicy(request: {
|
|
|
540
409
|
message: 'package.json packageManager must be Bun-aligned, for example "bun@1.x".',
|
|
541
410
|
path: request.packageJsonPath,
|
|
542
411
|
profile: request.profile,
|
|
543
|
-
|
|
544
|
-
severity: 'error',
|
|
412
|
+
...architecturePolicyRuleFields(PUBLIC_PACKAGE_POLICY.packageManager.bunRuleId),
|
|
545
413
|
}),
|
|
546
414
|
);
|
|
547
415
|
}
|
|
548
416
|
|
|
549
417
|
const scripts = isRecord(packageJson.scripts) ? packageJson.scripts : null;
|
|
550
|
-
for (const
|
|
551
|
-
if (scripts === null || !isNonEmptyString(scripts[
|
|
418
|
+
for (const requirement of PUBLIC_PACKAGE_POLICY.requiredScripts) {
|
|
419
|
+
if (scripts === null || !isNonEmptyString(scripts[requirement.name])) {
|
|
552
420
|
diagnostics.push(
|
|
553
421
|
createDiagnostic({
|
|
554
422
|
code: 'missing-script',
|
|
555
|
-
message: `Missing required package script: ${
|
|
423
|
+
message: `Missing required package script: ${requirement.name}`,
|
|
556
424
|
path: request.packageJsonPath,
|
|
557
425
|
profile: request.profile,
|
|
558
|
-
ruleId
|
|
559
|
-
severity: 'error',
|
|
426
|
+
...architecturePolicyRuleFields(requirement.ruleId),
|
|
560
427
|
}),
|
|
561
428
|
);
|
|
562
429
|
}
|
|
@@ -573,8 +440,7 @@ async function analyzePackagePolicy(request: {
|
|
|
573
440
|
message: 'Public package repos must declare TypeScript in dependencies or devDependencies.',
|
|
574
441
|
path: request.packageJsonPath,
|
|
575
442
|
profile: request.profile,
|
|
576
|
-
|
|
577
|
-
severity: 'error',
|
|
443
|
+
...architecturePolicyRuleFields(PUBLIC_PACKAGE_POLICY.dependencyRules.typescript),
|
|
578
444
|
}),
|
|
579
445
|
);
|
|
580
446
|
}
|
|
@@ -597,8 +463,7 @@ async function analyzePackagePolicy(request: {
|
|
|
597
463
|
message: 'Public package repos must declare @types/bun in devDependencies.',
|
|
598
464
|
path: request.packageJsonPath,
|
|
599
465
|
profile: request.profile,
|
|
600
|
-
|
|
601
|
-
severity: 'error',
|
|
466
|
+
...architecturePolicyRuleFields(PUBLIC_PACKAGE_POLICY.dependencyRules.bunTypes),
|
|
602
467
|
}),
|
|
603
468
|
);
|
|
604
469
|
}
|
|
@@ -610,8 +475,7 @@ async function analyzePackagePolicy(request: {
|
|
|
610
475
|
message: 'Public package repos must declare @types/node in devDependencies.',
|
|
611
476
|
path: request.packageJsonPath,
|
|
612
477
|
profile: request.profile,
|
|
613
|
-
|
|
614
|
-
severity: 'error',
|
|
478
|
+
...architecturePolicyRuleFields(PUBLIC_PACKAGE_POLICY.dependencyRules.nodeTypes),
|
|
615
479
|
}),
|
|
616
480
|
);
|
|
617
481
|
}
|
|
@@ -624,8 +488,7 @@ async function analyzePackagePolicy(request: {
|
|
|
624
488
|
'This package consumes shared Ankhorage lint/format/knip tooling and must declare @ankhorage/devtools.',
|
|
625
489
|
path: request.packageJsonPath,
|
|
626
490
|
profile: request.profile,
|
|
627
|
-
|
|
628
|
-
severity: 'error',
|
|
491
|
+
...architecturePolicyRuleFields(PUBLIC_PACKAGE_POLICY.dependencyRules.devtools),
|
|
629
492
|
}),
|
|
630
493
|
);
|
|
631
494
|
}
|
|
@@ -641,8 +504,7 @@ async function analyzePackagePolicy(request: {
|
|
|
641
504
|
message: 'This package owns Paradox docs generation and must declare @ankhorage/paradox.',
|
|
642
505
|
path: request.packageJsonPath,
|
|
643
506
|
profile: request.profile,
|
|
644
|
-
|
|
645
|
-
severity: 'error',
|
|
507
|
+
...architecturePolicyRuleFields(PUBLIC_PACKAGE_POLICY.dependencyRules.paradox),
|
|
646
508
|
}),
|
|
647
509
|
);
|
|
648
510
|
}
|
|
@@ -684,8 +546,7 @@ function createChangesetsDependencyDiagnostic(request: {
|
|
|
684
546
|
: 'Public package repos must not declare @changesets/cli directly; @ankhorage/devtools owns Changesets execution.',
|
|
685
547
|
path: request.packageJsonPath,
|
|
686
548
|
profile: request.profile,
|
|
687
|
-
|
|
688
|
-
severity: 'error',
|
|
549
|
+
...architecturePolicyRuleFields(PUBLIC_PACKAGE_POLICY.dependencyRules.changesets),
|
|
689
550
|
});
|
|
690
551
|
}
|
|
691
552
|
|
|
@@ -1027,6 +888,37 @@ async function isProviderPackage(targetPath: string): Promise<boolean> {
|
|
|
1027
888
|
return false;
|
|
1028
889
|
}
|
|
1029
890
|
|
|
891
|
+
/*** Validate every generic public-package metadata field from canonical Policy. */
|
|
892
|
+
function requirePublicPackageFields(request: {
|
|
893
|
+
readonly diagnostics: DoctorDiagnostic[];
|
|
894
|
+
readonly packageJson: Record<string, unknown>;
|
|
895
|
+
readonly packageJsonPath: string;
|
|
896
|
+
readonly profile: DoctorPolicyProfile;
|
|
897
|
+
}): void {
|
|
898
|
+
for (const requirement of PUBLIC_PACKAGE_POLICY.requiredFields) {
|
|
899
|
+
const input = {
|
|
900
|
+
diagnostics: request.diagnostics,
|
|
901
|
+
fieldName: requirement.name,
|
|
902
|
+
packageJson: request.packageJson,
|
|
903
|
+
packageJsonPath: request.packageJsonPath,
|
|
904
|
+
profile: request.profile,
|
|
905
|
+
ruleId: requirement.ruleId,
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
switch (requirement.kind) {
|
|
909
|
+
case 'non-empty-string':
|
|
910
|
+
requireNonEmptyStringField(input);
|
|
911
|
+
break;
|
|
912
|
+
case 'record':
|
|
913
|
+
requireRecordField(input);
|
|
914
|
+
break;
|
|
915
|
+
case 'string-array':
|
|
916
|
+
requireStringArrayField(input);
|
|
917
|
+
break;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
1030
922
|
async function maybeRequirePath(
|
|
1031
923
|
diagnostics: DoctorDiagnostic[],
|
|
1032
924
|
plannedChanges: DoctorPlannedChange[],
|
|
@@ -1051,8 +943,7 @@ async function maybeRequirePath(
|
|
|
1051
943
|
message: `Missing expected repo artifact: ${relativePath}`,
|
|
1052
944
|
path: absolutePath,
|
|
1053
945
|
profile: 'public-package',
|
|
1054
|
-
ruleId,
|
|
1055
|
-
severity: 'error',
|
|
946
|
+
...architecturePolicyRuleFields(ruleId),
|
|
1056
947
|
}),
|
|
1057
948
|
);
|
|
1058
949
|
plannedChanges.push(
|
|
@@ -1083,8 +974,7 @@ function requireNonEmptyStringField(request: {
|
|
|
1083
974
|
message: `package.json must define a non-empty "${request.fieldName}" field.`,
|
|
1084
975
|
path: request.packageJsonPath,
|
|
1085
976
|
profile: request.profile,
|
|
1086
|
-
|
|
1087
|
-
severity: 'error',
|
|
977
|
+
...architecturePolicyRuleFields(request.ruleId),
|
|
1088
978
|
}),
|
|
1089
979
|
);
|
|
1090
980
|
}
|
|
@@ -1107,8 +997,7 @@ function requireRecordField(request: {
|
|
|
1107
997
|
message: `package.json must define an object "${request.fieldName}" field.`,
|
|
1108
998
|
path: request.packageJsonPath,
|
|
1109
999
|
profile: request.profile,
|
|
1110
|
-
|
|
1111
|
-
severity: 'error',
|
|
1000
|
+
...architecturePolicyRuleFields(request.ruleId),
|
|
1112
1001
|
}),
|
|
1113
1002
|
);
|
|
1114
1003
|
}
|
|
@@ -1131,8 +1020,7 @@ function requireStringArrayField(request: {
|
|
|
1131
1020
|
message: `package.json must define a non-empty string array "${request.fieldName}" field.`,
|
|
1132
1021
|
path: request.packageJsonPath,
|
|
1133
1022
|
profile: request.profile,
|
|
1134
|
-
|
|
1135
|
-
severity: 'error',
|
|
1023
|
+
...architecturePolicyRuleFields(request.ruleId),
|
|
1136
1024
|
}),
|
|
1137
1025
|
);
|
|
1138
1026
|
}
|
|
@@ -1212,37 +1100,6 @@ function validateAnkhMetadataShape(value: unknown, providerPackage: boolean): st
|
|
|
1212
1100
|
return null;
|
|
1213
1101
|
}
|
|
1214
1102
|
|
|
1215
|
-
function mapScriptRule(scriptName: (typeof REQUIRED_PUBLIC_PACKAGE_SCRIPTS)[number]): DoctorRuleId {
|
|
1216
|
-
switch (scriptName) {
|
|
1217
|
-
case 'build':
|
|
1218
|
-
return 'package.scripts.build.required';
|
|
1219
|
-
case 'typecheck':
|
|
1220
|
-
return 'package.scripts.typecheck.required';
|
|
1221
|
-
case 'lint':
|
|
1222
|
-
return 'package.scripts.lint.required';
|
|
1223
|
-
case 'lint:fix':
|
|
1224
|
-
return 'package.scripts.lint-fix.required';
|
|
1225
|
-
case 'format':
|
|
1226
|
-
return 'package.scripts.format.required';
|
|
1227
|
-
case 'format:check':
|
|
1228
|
-
return 'package.scripts.format-check.required';
|
|
1229
|
-
case 'test':
|
|
1230
|
-
return 'package.scripts.test.required';
|
|
1231
|
-
case 'test:standalone':
|
|
1232
|
-
return 'package.scripts.standalone.required';
|
|
1233
|
-
case 'knip:check':
|
|
1234
|
-
return 'package.scripts.knip.required';
|
|
1235
|
-
case 'docs':
|
|
1236
|
-
return 'package.scripts.docs.required';
|
|
1237
|
-
case 'changeset':
|
|
1238
|
-
return 'package.scripts.changeset.required';
|
|
1239
|
-
case 'changeset:status':
|
|
1240
|
-
return 'package.scripts.changeset-status.required';
|
|
1241
|
-
case 'version-packages':
|
|
1242
|
-
return 'package.scripts.version-packages.required';
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
1103
|
function createEarlyResult(targetPath: string, diagnostic: DoctorDiagnostic): DoctorAnalysisResult {
|
|
1247
1104
|
return {
|
|
1248
1105
|
appliedChecks: [],
|
|
@@ -1256,6 +1113,14 @@ function createEarlyResult(targetPath: string, diagnostic: DoctorDiagnostic): Do
|
|
|
1256
1113
|
};
|
|
1257
1114
|
}
|
|
1258
1115
|
|
|
1116
|
+
/*** Return canonical rule id and severity for a Policy-owned generic package rule. */
|
|
1117
|
+
function architecturePolicyRuleFields(
|
|
1118
|
+
ruleId: DoctorRuleId,
|
|
1119
|
+
): Pick<DoctorDiagnostic, 'ruleId' | 'severity'> {
|
|
1120
|
+
const rule = getArchitecturePolicyRule(ruleId);
|
|
1121
|
+
return { ruleId: rule.id, severity: rule.severity };
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1259
1124
|
function createDiagnostic(diagnostic: DoctorDiagnostic): DoctorDiagnostic {
|
|
1260
1125
|
return diagnostic;
|
|
1261
1126
|
}
|
package/src/cliLayoutAnalysis.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
+
import { ARCHITECTURE_POLICY } from '@ankhorage/policy/architecture';
|
|
5
|
+
|
|
4
6
|
import {
|
|
5
7
|
analyzeDoctorTarget,
|
|
6
8
|
type DoctorAnalysisRequest,
|
|
@@ -9,8 +11,10 @@ import {
|
|
|
9
11
|
import type { DoctorDiagnostic, DoctorPolicyProfile } from './diagnostics.js';
|
|
10
12
|
import { analyzeAppManifestTarget } from './manifestAnalysis.js';
|
|
11
13
|
import { analyzeSourceArchitecture } from './sourceArchitectureAnalysis.js';
|
|
14
|
+
import { getArchitecturePolicyRule } from './utils/getArchitecturePolicyRule.js';
|
|
12
15
|
|
|
13
|
-
const
|
|
16
|
+
const CLI_POLICY = ARCHITECTURE_POLICY.cli;
|
|
17
|
+
const DEPENDENCY_POLICY = ARCHITECTURE_POLICY.dependencies;
|
|
14
18
|
const ACTIVE_SOURCE_ROOTS = ['src', 'app', 'apps', 'packages', 'scripts'] as const;
|
|
15
19
|
const ACTIVE_SOURCE_EXTENSIONS = new Set([
|
|
16
20
|
'.cjs',
|
|
@@ -38,7 +42,6 @@ const IGNORED_SOURCE_DIRECTORIES = new Set([
|
|
|
38
42
|
'test',
|
|
39
43
|
'tests',
|
|
40
44
|
]);
|
|
41
|
-
const COMPATIBILITY_PACKAGE_PREFIX = '@ankh/';
|
|
42
45
|
const STUDIO_PACKAGE_NAME = '@ankhorage/studio';
|
|
43
46
|
const STUDIO_DND_PACKAGE_NAME = '@ankhorage/react-native-reanimated-dnd-web';
|
|
44
47
|
const STUDIO_RUNTIME_PACKAGE_NAME = '@ankhorage/runtime';
|
|
@@ -105,9 +108,10 @@ async function analyzeTargetArchitecture(request: {
|
|
|
105
108
|
readonly targetPath: string;
|
|
106
109
|
}): Promise<DoctorDiagnostic[]> {
|
|
107
110
|
const diagnostics: DoctorDiagnostic[] = [];
|
|
108
|
-
const legacyRootCliPath = path.join(request.targetPath,
|
|
111
|
+
const legacyRootCliPath = path.join(request.targetPath, CLI_POLICY.legacyRootFile);
|
|
109
112
|
const exportsField = isRecord(request.packageJson.exports) ? request.packageJson.exports : null;
|
|
110
|
-
const cliCapable =
|
|
113
|
+
const cliCapable =
|
|
114
|
+
hasAnkhProvider(request.packageJson) || exportsField?.[CLI_POLICY.packageExport] !== undefined;
|
|
111
115
|
|
|
112
116
|
if (await pathExists(legacyRootCliPath)) {
|
|
113
117
|
diagnostics.push({
|
|
@@ -115,20 +119,18 @@ async function analyzeTargetArchitecture(request: {
|
|
|
115
119
|
message: 'Package CLI code must live under src/cli/; root src/cli.ts is not allowed.',
|
|
116
120
|
path: legacyRootCliPath,
|
|
117
121
|
profile: request.profile,
|
|
118
|
-
|
|
119
|
-
severity: 'error',
|
|
122
|
+
...architectureRuleFields(CLI_POLICY.legacyRootRuleId),
|
|
120
123
|
});
|
|
121
124
|
}
|
|
122
125
|
|
|
123
|
-
if (cliCapable && exportsField?.[
|
|
126
|
+
if (cliCapable && exportsField?.[CLI_POLICY.packageExport] === undefined) {
|
|
124
127
|
diagnostics.push({
|
|
125
128
|
code: 'field-missing',
|
|
126
129
|
message:
|
|
127
130
|
'CLI-capable packages must export "./cli" from package.json and point it at the metadata-declared provider build output.',
|
|
128
131
|
path: request.packageJsonPath,
|
|
129
132
|
profile: request.profile,
|
|
130
|
-
|
|
131
|
-
severity: 'error',
|
|
133
|
+
...architectureRuleFields(CLI_POLICY.exportRuleId),
|
|
132
134
|
});
|
|
133
135
|
}
|
|
134
136
|
|
|
@@ -178,42 +180,41 @@ function validateDependencyArchitecture(request: {
|
|
|
178
180
|
const diagnostics: DoctorDiagnostic[] = [];
|
|
179
181
|
|
|
180
182
|
for (const dependency of request.dependencyEntries) {
|
|
181
|
-
if (dependency.packageName.startsWith(
|
|
183
|
+
if (dependency.packageName.startsWith(DEPENDENCY_POLICY.compatibilityPackagePrefix)) {
|
|
182
184
|
diagnostics.push({
|
|
183
185
|
code: 'field-invalid',
|
|
184
186
|
message: `Dependency "${dependency.packageName}" in package.json.${dependency.fieldName} is an old ankhorage4 workspace alias. Depend on "${toOwningPackageSpecifier(dependency.packageName)}" directly.`,
|
|
185
187
|
path: request.packageJsonPath,
|
|
186
188
|
profile: request.profile,
|
|
187
|
-
|
|
188
|
-
severity: 'error',
|
|
189
|
+
...architectureRuleFields(DEPENDENCY_POLICY.rules.compatibilityDependency),
|
|
189
190
|
});
|
|
190
191
|
}
|
|
191
192
|
|
|
192
193
|
if (
|
|
193
194
|
request.profile === 'public-package' &&
|
|
194
|
-
|
|
195
|
+
DEPENDENCY_POLICY.localProtocolPrefixes.some((prefix) =>
|
|
196
|
+
dependency.version.startsWith(prefix),
|
|
197
|
+
)
|
|
195
198
|
) {
|
|
196
199
|
diagnostics.push({
|
|
197
200
|
code: 'field-invalid',
|
|
198
201
|
message: `Published dependency "${dependency.packageName}" must resolve from a registry version, not "${dependency.version}". Standalone packages cannot rely on local/workspace/Git package state.`,
|
|
199
202
|
path: request.packageJsonPath,
|
|
200
203
|
profile: request.profile,
|
|
201
|
-
|
|
202
|
-
severity: 'error',
|
|
204
|
+
...architectureRuleFields(DEPENDENCY_POLICY.rules.localProtocolDependency),
|
|
203
205
|
});
|
|
204
206
|
}
|
|
205
207
|
|
|
206
208
|
if (
|
|
207
|
-
dependency.packageName.includes(
|
|
208
|
-
dependency.version.includes(
|
|
209
|
+
dependency.packageName.includes(DEPENDENCY_POLICY.legacySourceMarker) ||
|
|
210
|
+
dependency.version.includes(DEPENDENCY_POLICY.legacySourceMarker)
|
|
209
211
|
) {
|
|
210
212
|
diagnostics.push({
|
|
211
213
|
code: 'field-invalid',
|
|
212
214
|
message: `Dependency "${dependency.packageName}" references ankhorage4 as active source. Depend on the extracted owning @ankhorage/* package instead.`,
|
|
213
215
|
path: request.packageJsonPath,
|
|
214
216
|
profile: request.profile,
|
|
215
|
-
|
|
216
|
-
severity: 'error',
|
|
217
|
+
...architectureRuleFields(DEPENDENCY_POLICY.rules.legacySourceDependency),
|
|
217
218
|
});
|
|
218
219
|
}
|
|
219
220
|
}
|
|
@@ -228,25 +229,23 @@ function validateActiveSourceImports(request: {
|
|
|
228
229
|
const diagnostics: DoctorDiagnostic[] = [];
|
|
229
230
|
|
|
230
231
|
for (const sourceImport of request.activeSourceImports) {
|
|
231
|
-
if (sourceImport.specifier.startsWith(
|
|
232
|
+
if (sourceImport.specifier.startsWith(DEPENDENCY_POLICY.compatibilityPackagePrefix)) {
|
|
232
233
|
diagnostics.push({
|
|
233
234
|
code: 'field-invalid',
|
|
234
235
|
message: `Import "${sourceImport.specifier}" is an old ankhorage4 compatibility boundary. Import "${toOwningPackageSpecifier(sourceImport.specifier)}" directly from the owning package.`,
|
|
235
236
|
path: sourceImport.filePath,
|
|
236
237
|
profile: request.profile,
|
|
237
|
-
|
|
238
|
-
severity: 'error',
|
|
238
|
+
...architectureRuleFields(DEPENDENCY_POLICY.rules.compatibilityImport),
|
|
239
239
|
});
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
if (sourceImport.specifier.includes(
|
|
242
|
+
if (sourceImport.specifier.includes(DEPENDENCY_POLICY.legacySourceMarker)) {
|
|
243
243
|
diagnostics.push({
|
|
244
244
|
code: 'field-invalid',
|
|
245
245
|
message: `Import "${sourceImport.specifier}" references ankhorage4 as active source. Import the extracted owning @ankhorage/* package instead.`,
|
|
246
246
|
path: sourceImport.filePath,
|
|
247
247
|
profile: request.profile,
|
|
248
|
-
|
|
249
|
-
severity: 'error',
|
|
248
|
+
...architectureRuleFields(DEPENDENCY_POLICY.rules.legacySourceImport),
|
|
250
249
|
});
|
|
251
250
|
}
|
|
252
251
|
}
|
|
@@ -429,8 +428,16 @@ function isPackageImport(specifier: string, packageName: string): boolean {
|
|
|
429
428
|
return specifier === packageName || specifier.startsWith(`${packageName}/`);
|
|
430
429
|
}
|
|
431
430
|
|
|
431
|
+
/*** Resolve Policy-owned rule metadata for generic architecture diagnostics. */
|
|
432
|
+
function architectureRuleFields(
|
|
433
|
+
ruleId: DoctorDiagnostic['ruleId'],
|
|
434
|
+
): Pick<DoctorDiagnostic, 'ruleId' | 'severity'> {
|
|
435
|
+
const rule = getArchitecturePolicyRule(ruleId);
|
|
436
|
+
return { ruleId: rule.id, severity: rule.severity };
|
|
437
|
+
}
|
|
438
|
+
|
|
432
439
|
function toOwningPackageSpecifier(specifier: string): string {
|
|
433
|
-
return `@ankhorage/${specifier.slice(
|
|
440
|
+
return `@ankhorage/${specifier.slice(DEPENDENCY_POLICY.compatibilityPackagePrefix.length)}`;
|
|
434
441
|
}
|
|
435
442
|
|
|
436
443
|
async function readPackageJson(packageJsonPath: string): Promise<Record<string, unknown> | null> {
|