@ontrails/warden 1.0.0-beta.2 → 1.0.0-beta.21
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 +497 -6
- package/README.md +77 -26
- package/bin/warden.ts +50 -0
- package/package.json +27 -5
- package/src/adapter-check.ts +136 -0
- package/src/ast.ts +28 -0
- package/src/cli.ts +1374 -103
- package/src/command.ts +953 -0
- package/src/config.ts +184 -0
- package/src/draft.ts +22 -0
- package/src/drift.ts +106 -22
- package/src/fix.ts +120 -0
- package/src/formatters.ts +79 -9
- package/src/guide.ts +245 -0
- package/src/index.ts +206 -14
- package/src/project-context.ts +163 -0
- package/src/resolve.ts +530 -0
- package/src/rules/activation-orphan.ts +97 -0
- package/src/rules/ast.ts +3176 -85
- package/src/rules/circular-refs.ts +154 -0
- package/src/rules/composes-declarations.ts +704 -0
- package/src/rules/context-no-surface-types.ts +68 -8
- package/src/rules/contour-exists.ts +251 -0
- package/src/rules/contour-ids.ts +15 -0
- package/src/rules/dead-internal-trail.ts +154 -0
- package/src/rules/draft-file-marking.ts +160 -0
- package/src/rules/draft-visible-debt.ts +87 -0
- package/src/rules/error-mapping-completeness.ts +288 -0
- package/src/rules/example-valid.ts +401 -0
- package/src/rules/fires-declarations.ts +758 -0
- package/src/rules/implementation-returns-result.ts +1265 -95
- package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
- package/src/rules/incomplete-crud.ts +580 -0
- package/src/rules/index.ts +219 -18
- package/src/rules/intent-propagation.ts +127 -0
- package/src/rules/layer-field-name-drift.ts +96 -0
- package/src/rules/metadata.ts +654 -0
- package/src/rules/missing-reconcile.ts +98 -0
- package/src/rules/missing-visibility.ts +110 -0
- package/src/rules/no-destructured-compose.ts +192 -0
- package/src/rules/no-dev-permit-in-source.ts +99 -0
- package/src/rules/no-direct-implementation-call.ts +7 -7
- package/src/rules/no-legacy-layer-imports.ts +211 -0
- package/src/rules/no-native-error-result.ts +111 -0
- package/src/rules/no-redundant-result-error-wrap.ts +331 -0
- package/src/rules/no-retired-cross-vocabulary.ts +194 -0
- package/src/rules/no-sync-result-assumption.ts +1134 -99
- package/src/rules/no-throw-in-detour-recover.ts +225 -0
- package/src/rules/no-throw-in-implementation.ts +10 -9
- package/src/rules/no-top-level-surface.ts +389 -0
- package/src/rules/on-references-exist.ts +194 -0
- package/src/rules/orphaned-signal.ts +150 -0
- package/src/rules/owner-projection-parity.ts +146 -0
- package/src/rules/permit-governance.ts +25 -0
- package/src/rules/public-export-example-coverage.ts +553 -0
- package/src/rules/public-internal-deep-imports.ts +517 -0
- package/src/rules/public-output-schema.ts +29 -0
- package/src/rules/public-union-output-discriminants.ts +150 -0
- package/src/rules/read-intent-fires.ts +187 -0
- package/src/rules/reference-exists.ts +98 -0
- package/src/rules/registry-names.ts +145 -0
- package/src/rules/resolved-import-boundary.ts +146 -0
- package/src/rules/resource-declarations.ts +704 -0
- package/src/rules/resource-exists.ts +179 -0
- package/src/rules/resource-id-grammar.ts +65 -0
- package/src/rules/resource-mock-coverage.ts +115 -0
- package/src/rules/scan.ts +38 -25
- package/src/rules/scheduled-destroy-intent.ts +44 -0
- package/src/rules/signal-graph-coaching.ts +191 -0
- package/src/rules/specs.ts +9 -5
- package/src/rules/static-resource-accessor-preference.ts +657 -0
- package/src/rules/surface-facet-coherence.ts +370 -0
- package/src/rules/trail-versioning-source.ts +1094 -0
- package/src/rules/trail-versioning-topo.ts +172 -0
- package/src/rules/types.ts +270 -6
- package/src/rules/unmaterialized-activation-source.ts +84 -0
- package/src/rules/unreachable-detour-shadowing.ts +344 -0
- package/src/rules/valid-describe-refs.ts +160 -32
- package/src/rules/valid-detour-contract.ts +78 -0
- package/src/rules/warden-export-symmetry.ts +533 -0
- package/src/rules/warden-rules-use-ast.ts +996 -0
- package/src/rules/webhook-route-collision.ts +243 -0
- package/src/trails/activation-orphan.trail.ts +84 -0
- package/src/trails/circular-refs.trail.ts +29 -0
- package/src/trails/composes-declarations.trail.ts +22 -0
- package/src/trails/context-no-surface-types.trail.ts +21 -0
- package/src/trails/contour-exists.trail.ts +21 -0
- package/src/trails/dead-internal-trail.trail.ts +26 -0
- package/src/trails/deprecation-without-guidance.trail.ts +21 -0
- package/src/trails/draft-file-marking.trail.ts +16 -0
- package/src/trails/draft-visible-debt.trail.ts +16 -0
- package/src/trails/error-mapping-completeness.trail.ts +29 -0
- package/src/trails/example-valid.trail.ts +25 -0
- package/src/trails/fires-declarations.trail.ts +23 -0
- package/src/trails/fork-without-preserved-blaze.trail.ts +31 -0
- package/src/trails/implementation-returns-result.trail.ts +20 -0
- package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
- package/src/trails/incomplete-crud.trail.ts +39 -0
- package/src/trails/index.ts +78 -0
- package/src/trails/intent-propagation.trail.ts +30 -0
- package/src/trails/layer-field-name-drift.trail.ts +39 -0
- package/src/trails/marker-schema-unsupported.trail.ts +23 -0
- package/src/trails/missing-reconcile.trail.ts +33 -0
- package/src/trails/missing-visibility.trail.ts +22 -0
- package/src/trails/no-destructured-compose.trail.ts +44 -0
- package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
- package/src/trails/no-direct-implementation-call.trail.ts +16 -0
- package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
- package/src/trails/no-native-error-result.trail.ts +18 -0
- package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
- package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
- package/src/trails/no-sync-result-assumption.trail.ts +19 -0
- package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
- package/src/trails/no-throw-in-implementation.trail.ts +20 -0
- package/src/trails/no-top-level-surface.trail.ts +43 -0
- package/src/trails/on-references-exist.trail.ts +21 -0
- package/src/trails/orphaned-signal.trail.ts +36 -0
- package/src/trails/owner-projection-parity.trail.ts +26 -0
- package/src/trails/pending-force.trail.ts +21 -0
- package/src/trails/permit-governance.trail.ts +51 -0
- package/src/trails/prefer-schema-inference.trail.ts +21 -0
- package/src/trails/public-export-example-coverage.trail.ts +16 -0
- package/src/trails/public-internal-deep-imports.trail.ts +94 -0
- package/src/trails/public-output-schema.trail.ts +55 -0
- package/src/trails/public-union-output-discriminants.trail.ts +33 -0
- package/src/trails/read-intent-fires.trail.ts +20 -0
- package/src/trails/reference-exists.trail.ts +25 -0
- package/src/trails/resolved-import-boundary.trail.ts +109 -0
- package/src/trails/resource-declarations.trail.ts +25 -0
- package/src/trails/resource-exists.trail.ts +27 -0
- package/src/trails/resource-id-grammar.trail.ts +39 -0
- package/src/trails/resource-mock-coverage.trail.ts +40 -0
- package/src/trails/run.ts +162 -0
- package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
- package/src/trails/schema.ts +194 -0
- package/src/trails/signal-graph-coaching.trail.ts +77 -0
- package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
- package/src/trails/surface-facet-coherence.trail.ts +25 -0
- package/src/trails/topo.ts +6 -0
- package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
- package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
- package/src/trails/valid-describe-refs.trail.ts +18 -0
- package/src/trails/valid-detour-contract.trail.ts +71 -0
- package/src/trails/version-gap.trail.ts +35 -0
- package/src/trails/version-pinned-compose.trail.ts +23 -0
- package/src/trails/version-without-examples.trail.ts +38 -0
- package/src/trails/warden-export-symmetry.trail.ts +16 -0
- package/src/trails/warden-rules-use-ast.trail.ts +45 -0
- package/src/trails/webhook-route-collision.trail.ts +50 -0
- package/src/trails/wrap-rule.ts +213 -0
- package/src/workspaces.ts +238 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/cli.d.ts +0 -46
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -221
- package/dist/cli.js.map +0 -1
- package/dist/drift.d.ts +0 -26
- package/dist/drift.d.ts.map +0 -1
- package/dist/drift.js +0 -27
- package/dist/drift.js.map +0 -1
- package/dist/formatters.d.ts +0 -29
- package/dist/formatters.d.ts.map +0 -1
- package/dist/formatters.js +0 -87
- package/dist/formatters.js.map +0 -1
- package/dist/index.d.ts +0 -26
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -26
- package/dist/index.js.map +0 -1
- package/dist/rules/ast.d.ts +0 -41
- package/dist/rules/ast.d.ts.map +0 -1
- package/dist/rules/ast.js +0 -163
- package/dist/rules/ast.js.map +0 -1
- package/dist/rules/context-no-surface-types.d.ts +0 -12
- package/dist/rules/context-no-surface-types.d.ts.map +0 -1
- package/dist/rules/context-no-surface-types.js +0 -96
- package/dist/rules/context-no-surface-types.js.map +0 -1
- package/dist/rules/implementation-returns-result.d.ts +0 -13
- package/dist/rules/implementation-returns-result.d.ts.map +0 -1
- package/dist/rules/implementation-returns-result.js +0 -231
- package/dist/rules/implementation-returns-result.js.map +0 -1
- package/dist/rules/index.d.ts +0 -22
- package/dist/rules/index.d.ts.map +0 -1
- package/dist/rules/index.js +0 -41
- package/dist/rules/index.js.map +0 -1
- package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
- package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
- package/dist/rules/no-direct-impl-in-route.js +0 -46
- package/dist/rules/no-direct-impl-in-route.js.map +0 -1
- package/dist/rules/no-direct-implementation-call.d.ts +0 -12
- package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
- package/dist/rules/no-direct-implementation-call.js +0 -39
- package/dist/rules/no-direct-implementation-call.js.map +0 -1
- package/dist/rules/no-sync-result-assumption.d.ts +0 -6
- package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
- package/dist/rules/no-sync-result-assumption.js +0 -98
- package/dist/rules/no-sync-result-assumption.js.map +0 -1
- package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
- package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
- package/dist/rules/no-throw-in-detour-target.js +0 -87
- package/dist/rules/no-throw-in-detour-target.js.map +0 -1
- package/dist/rules/no-throw-in-implementation.d.ts +0 -9
- package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
- package/dist/rules/no-throw-in-implementation.js +0 -34
- package/dist/rules/no-throw-in-implementation.js.map +0 -1
- package/dist/rules/prefer-schema-inference.d.ts +0 -7
- package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
- package/dist/rules/prefer-schema-inference.js +0 -86
- package/dist/rules/prefer-schema-inference.js.map +0 -1
- package/dist/rules/scan.d.ts +0 -8
- package/dist/rules/scan.d.ts.map +0 -1
- package/dist/rules/scan.js +0 -32
- package/dist/rules/scan.js.map +0 -1
- package/dist/rules/specs.d.ts +0 -29
- package/dist/rules/specs.d.ts.map +0 -1
- package/dist/rules/specs.js +0 -192
- package/dist/rules/specs.js.map +0 -1
- package/dist/rules/structure.d.ts +0 -13
- package/dist/rules/structure.d.ts.map +0 -1
- package/dist/rules/structure.js +0 -142
- package/dist/rules/structure.js.map +0 -1
- package/dist/rules/types.d.ts +0 -52
- package/dist/rules/types.d.ts.map +0 -1
- package/dist/rules/types.js +0 -2
- package/dist/rules/types.js.map +0 -1
- package/dist/rules/valid-describe-refs.d.ts +0 -7
- package/dist/rules/valid-describe-refs.d.ts.map +0 -1
- package/dist/rules/valid-describe-refs.js +0 -51
- package/dist/rules/valid-describe-refs.js.map +0 -1
- package/dist/rules/valid-detour-refs.d.ts +0 -6
- package/dist/rules/valid-detour-refs.d.ts.map +0 -1
- package/dist/rules/valid-detour-refs.js +0 -116
- package/dist/rules/valid-detour-refs.js.map +0 -1
- package/src/__tests__/cli.test.ts +0 -198
- package/src/__tests__/drift.test.ts +0 -74
- package/src/__tests__/formatters.test.ts +0 -157
- package/src/__tests__/implementation-returns-result.test.ts +0 -75
- package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
- package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
- package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
- package/src/__tests__/prefer-schema-inference.test.ts +0 -84
- package/src/__tests__/rules.test.ts +0 -188
- package/src/__tests__/valid-describe-refs.test.ts +0 -60
- package/src/rules/no-direct-impl-in-route.ts +0 -77
- package/src/rules/no-throw-in-detour-target.ts +0 -150
- package/src/rules/valid-detour-refs.ts +0 -187
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
package/src/resolve.ts
ADDED
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public Warden resolver helper surface.
|
|
3
|
+
*
|
|
4
|
+
* These helpers wrap `oxc-resolver` behind Warden-owned import-resolution
|
|
5
|
+
* facts so rules never depend on resolver binding internals directly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
9
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
10
|
+
|
|
11
|
+
import { ResolverFactory } from 'oxc-resolver';
|
|
12
|
+
import type { NapiResolveOptions, ResolveResult } from 'oxc-resolver';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
getStringValue,
|
|
16
|
+
isStringLiteral,
|
|
17
|
+
offsetToLine,
|
|
18
|
+
parse,
|
|
19
|
+
walk,
|
|
20
|
+
} from './rules/ast.js';
|
|
21
|
+
import type { AstNode } from './rules/ast.js';
|
|
22
|
+
|
|
23
|
+
export const wardenImportResolutionErrorKinds = [
|
|
24
|
+
'builtin',
|
|
25
|
+
'ignored',
|
|
26
|
+
'not-found',
|
|
27
|
+
'package-path-not-exported',
|
|
28
|
+
'other',
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
export type WardenImportResolutionErrorKind =
|
|
32
|
+
(typeof wardenImportResolutionErrorKinds)[number];
|
|
33
|
+
|
|
34
|
+
export interface WardenImportResolution {
|
|
35
|
+
readonly importerPath: string;
|
|
36
|
+
readonly importSource: string;
|
|
37
|
+
readonly line: number;
|
|
38
|
+
readonly resolvedPath?: string | undefined;
|
|
39
|
+
readonly packageName?: string | undefined;
|
|
40
|
+
readonly packageRoot?: string | undefined;
|
|
41
|
+
readonly crossesPackageBoundary: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* True when a bare package specifier resolves successfully while the target
|
|
44
|
+
* package declares an exports map. This is a coarse resolver fact; it does
|
|
45
|
+
* not prove the resolved file matched a specific export entry.
|
|
46
|
+
*/
|
|
47
|
+
readonly usesPublicExport: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* True when a resolved file lands inside an internal/private package path.
|
|
50
|
+
* Export-map-blocked internal specifiers do not have a resolved file path;
|
|
51
|
+
* combine this with errorKind/importSource checks when guarding specifiers.
|
|
52
|
+
*/
|
|
53
|
+
readonly isInternalTarget: boolean;
|
|
54
|
+
readonly errorKind?: WardenImportResolutionErrorKind | undefined;
|
|
55
|
+
readonly errorMessage?: string | undefined;
|
|
56
|
+
readonly builtinModule?: string | undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface WardenImportSpecifier {
|
|
60
|
+
readonly importSource: string;
|
|
61
|
+
readonly line: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface WardenResolverOptions {
|
|
65
|
+
readonly rootDir?: string | undefined;
|
|
66
|
+
readonly resolveOptions?: NapiResolveOptions | undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface WardenProjectResolver {
|
|
70
|
+
readonly rootDir: string;
|
|
71
|
+
readonly resolveOptions: NapiResolveOptions;
|
|
72
|
+
clearCache(): void;
|
|
73
|
+
resolveImport(
|
|
74
|
+
importerPath: string,
|
|
75
|
+
importSource: string,
|
|
76
|
+
line?: number
|
|
77
|
+
): WardenImportResolution;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface PackageInfo {
|
|
81
|
+
readonly name?: string | undefined;
|
|
82
|
+
readonly packageJsonPath: string;
|
|
83
|
+
readonly root: string;
|
|
84
|
+
readonly exports?: unknown;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const conditionNames = ['bun', 'node', 'import', 'default'] as const;
|
|
88
|
+
export const packagePathNotExportedErrorFragment =
|
|
89
|
+
'is not exported under the conditions';
|
|
90
|
+
|
|
91
|
+
export const defaultWardenResolveOptions = {
|
|
92
|
+
builtinModules: true,
|
|
93
|
+
conditionNames: [...conditionNames],
|
|
94
|
+
extensionAlias: {
|
|
95
|
+
'.cjs': ['.cts', '.cjs'],
|
|
96
|
+
'.js': ['.ts', '.tsx', '.js'],
|
|
97
|
+
'.mjs': ['.mts', '.mjs'],
|
|
98
|
+
},
|
|
99
|
+
extensions: [
|
|
100
|
+
'.ts',
|
|
101
|
+
'.tsx',
|
|
102
|
+
'.mts',
|
|
103
|
+
'.cts',
|
|
104
|
+
'.js',
|
|
105
|
+
'.jsx',
|
|
106
|
+
'.mjs',
|
|
107
|
+
'.cjs',
|
|
108
|
+
'.json',
|
|
109
|
+
],
|
|
110
|
+
moduleType: true,
|
|
111
|
+
symlinks: true,
|
|
112
|
+
tsconfig: 'auto',
|
|
113
|
+
} satisfies NapiResolveOptions;
|
|
114
|
+
|
|
115
|
+
export const normalizePath = (path: string): string =>
|
|
116
|
+
path.replaceAll('\\', '/');
|
|
117
|
+
|
|
118
|
+
const mergeStringLists = (
|
|
119
|
+
base: readonly string[] = [],
|
|
120
|
+
override: readonly string[] = []
|
|
121
|
+
): string[] => [...new Set([...base, ...override])];
|
|
122
|
+
|
|
123
|
+
const mergeExtensionAlias = (
|
|
124
|
+
base: NonNullable<NapiResolveOptions['extensionAlias']>,
|
|
125
|
+
override: NapiResolveOptions['extensionAlias'] | undefined
|
|
126
|
+
): NonNullable<NapiResolveOptions['extensionAlias']> => {
|
|
127
|
+
const merged: Record<string, string[]> = Object.fromEntries(
|
|
128
|
+
Object.entries(base).map(([extension, aliases]) => [
|
|
129
|
+
extension,
|
|
130
|
+
[...aliases],
|
|
131
|
+
])
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
for (const [extension, aliases] of Object.entries(override ?? {})) {
|
|
135
|
+
merged[extension] = mergeStringLists(merged[extension], aliases);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return merged;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const mergeWardenResolveOptions = (
|
|
142
|
+
overrides: NapiResolveOptions | undefined
|
|
143
|
+
): NapiResolveOptions => ({
|
|
144
|
+
...defaultWardenResolveOptions,
|
|
145
|
+
...overrides,
|
|
146
|
+
conditionNames: mergeStringLists(
|
|
147
|
+
defaultWardenResolveOptions.conditionNames,
|
|
148
|
+
overrides?.conditionNames
|
|
149
|
+
),
|
|
150
|
+
extensionAlias: mergeExtensionAlias(
|
|
151
|
+
defaultWardenResolveOptions.extensionAlias,
|
|
152
|
+
overrides?.extensionAlias
|
|
153
|
+
),
|
|
154
|
+
extensions: mergeStringLists(
|
|
155
|
+
defaultWardenResolveOptions.extensions,
|
|
156
|
+
overrides?.extensions
|
|
157
|
+
),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const normalizeRealPath = (path: string): string => {
|
|
161
|
+
try {
|
|
162
|
+
return normalizePath(realpathSync(path));
|
|
163
|
+
} catch {
|
|
164
|
+
return normalizePath(resolve(path));
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const readPackageJson = (
|
|
169
|
+
packageJsonPath: string
|
|
170
|
+
): Record<string, unknown> | null => {
|
|
171
|
+
try {
|
|
172
|
+
return JSON.parse(readFileSync(packageJsonPath, 'utf8')) as Record<
|
|
173
|
+
string,
|
|
174
|
+
unknown
|
|
175
|
+
>;
|
|
176
|
+
} catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const packageInfoFromPackageJson = (
|
|
182
|
+
packageJsonPath: string
|
|
183
|
+
): PackageInfo | null => {
|
|
184
|
+
if (!existsSync(packageJsonPath)) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const json = readPackageJson(packageJsonPath);
|
|
188
|
+
if (!json) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
const root = normalizeRealPath(dirname(packageJsonPath));
|
|
192
|
+
return {
|
|
193
|
+
...(json['exports'] === undefined ? {} : { exports: json['exports'] }),
|
|
194
|
+
...(typeof json['name'] === 'string' ? { name: json['name'] } : {}),
|
|
195
|
+
packageJsonPath: normalizeRealPath(packageJsonPath),
|
|
196
|
+
root,
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const findNearestPackageJson = (fromPath: string): string | null => {
|
|
201
|
+
let dir = dirname(resolve(fromPath));
|
|
202
|
+
while (true) {
|
|
203
|
+
const packageJsonPath = join(dir, 'package.json');
|
|
204
|
+
if (existsSync(packageJsonPath)) {
|
|
205
|
+
return packageJsonPath;
|
|
206
|
+
}
|
|
207
|
+
const parent = dirname(dir);
|
|
208
|
+
if (parent === dir) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
dir = parent;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const findPackageInfoForPath = (path: string): PackageInfo | null => {
|
|
216
|
+
const packageJsonPath = findNearestPackageJson(path);
|
|
217
|
+
return packageJsonPath ? packageInfoFromPackageJson(packageJsonPath) : null;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const parseBarePackageName = (specifier: string): string | null => {
|
|
221
|
+
if (
|
|
222
|
+
specifier.startsWith('.') ||
|
|
223
|
+
specifier.startsWith('/') ||
|
|
224
|
+
specifier.startsWith('#') ||
|
|
225
|
+
specifier.startsWith('node:') ||
|
|
226
|
+
/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(specifier)
|
|
227
|
+
) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const parts = specifier.split('/');
|
|
232
|
+
if (specifier.startsWith('@')) {
|
|
233
|
+
const [scope, name] = parts;
|
|
234
|
+
return scope && name ? `${scope}/${name}` : null;
|
|
235
|
+
}
|
|
236
|
+
return parts[0] ?? null;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const packageSpecifierParts = (
|
|
240
|
+
specifier: string
|
|
241
|
+
): { readonly packageName: string; readonly subpath: string } | null => {
|
|
242
|
+
const packageName = parseBarePackageName(specifier);
|
|
243
|
+
if (!packageName) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const suffix = specifier.slice(packageName.length);
|
|
247
|
+
return {
|
|
248
|
+
packageName,
|
|
249
|
+
subpath: suffix.length === 0 ? '.' : `.${suffix}`,
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const findNodeModulesPackageInfo = (
|
|
254
|
+
importerPath: string,
|
|
255
|
+
packageName: string
|
|
256
|
+
): PackageInfo | null => {
|
|
257
|
+
const packageSegments = packageName.split('/');
|
|
258
|
+
let dir = dirname(resolve(importerPath));
|
|
259
|
+
while (true) {
|
|
260
|
+
const packageJsonPath = join(
|
|
261
|
+
dir,
|
|
262
|
+
'node_modules',
|
|
263
|
+
...packageSegments,
|
|
264
|
+
'package.json'
|
|
265
|
+
);
|
|
266
|
+
const info = packageInfoFromPackageJson(packageJsonPath);
|
|
267
|
+
if (info) {
|
|
268
|
+
return info;
|
|
269
|
+
}
|
|
270
|
+
const parent = dirname(dir);
|
|
271
|
+
if (parent === dir) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
dir = parent;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const hasExportsMap = (info: PackageInfo | null): boolean =>
|
|
279
|
+
info?.exports !== undefined;
|
|
280
|
+
|
|
281
|
+
const classifyResolverError = (
|
|
282
|
+
error: string
|
|
283
|
+
): WardenImportResolutionErrorKind => {
|
|
284
|
+
if (error.includes(packagePathNotExportedErrorFragment)) {
|
|
285
|
+
return 'package-path-not-exported';
|
|
286
|
+
}
|
|
287
|
+
if (error.includes('ignored')) {
|
|
288
|
+
return 'ignored';
|
|
289
|
+
}
|
|
290
|
+
if (error.includes('not found') || error.includes('Cannot find')) {
|
|
291
|
+
return 'not-found';
|
|
292
|
+
}
|
|
293
|
+
return 'other';
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const isInternalTargetPath = (
|
|
297
|
+
packageRoot: string | undefined,
|
|
298
|
+
resolvedPath: string | undefined
|
|
299
|
+
): boolean => {
|
|
300
|
+
if (!packageRoot || !resolvedPath) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
const relativePath = normalizePath(relative(packageRoot, resolvedPath));
|
|
304
|
+
return /(?:^|\/)(?:src\/)?(?:internal|private|_internal|_private)(?:\/|$)/.test(
|
|
305
|
+
relativePath
|
|
306
|
+
);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const resolveResultPackageInfo = (
|
|
310
|
+
result: ResolveResult
|
|
311
|
+
): PackageInfo | null => {
|
|
312
|
+
if (result.packageJsonPath) {
|
|
313
|
+
return packageInfoFromPackageJson(result.packageJsonPath);
|
|
314
|
+
}
|
|
315
|
+
if (result.path) {
|
|
316
|
+
return findPackageInfoForPath(result.path);
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const resolveErrorKind = (
|
|
322
|
+
errorMessage: string | undefined,
|
|
323
|
+
builtinModule: string | undefined
|
|
324
|
+
): WardenImportResolutionErrorKind | undefined => {
|
|
325
|
+
if (errorMessage) {
|
|
326
|
+
return classifyResolverError(errorMessage);
|
|
327
|
+
}
|
|
328
|
+
return builtinModule ? 'builtin' : undefined;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const optionalResolutionFields = ({
|
|
332
|
+
builtinModule,
|
|
333
|
+
errorKind,
|
|
334
|
+
errorMessage,
|
|
335
|
+
packageName,
|
|
336
|
+
packageRoot,
|
|
337
|
+
resolvedPath,
|
|
338
|
+
}: {
|
|
339
|
+
readonly builtinModule: string | undefined;
|
|
340
|
+
readonly errorKind: WardenImportResolutionErrorKind | undefined;
|
|
341
|
+
readonly errorMessage: string | undefined;
|
|
342
|
+
readonly packageName: string | undefined;
|
|
343
|
+
readonly packageRoot: string | undefined;
|
|
344
|
+
readonly resolvedPath: string | undefined;
|
|
345
|
+
}): Partial<WardenImportResolution> => ({
|
|
346
|
+
...(builtinModule ? { builtinModule } : {}),
|
|
347
|
+
...(errorKind ? { errorKind } : {}),
|
|
348
|
+
...(errorMessage ? { errorMessage } : {}),
|
|
349
|
+
...(packageName ? { packageName } : {}),
|
|
350
|
+
...(packageRoot ? { packageRoot } : {}),
|
|
351
|
+
...(resolvedPath ? { resolvedPath } : {}),
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const buildResolution = ({
|
|
355
|
+
importSource,
|
|
356
|
+
importerPath,
|
|
357
|
+
line,
|
|
358
|
+
result,
|
|
359
|
+
}: {
|
|
360
|
+
readonly importSource: string;
|
|
361
|
+
readonly importerPath: string;
|
|
362
|
+
readonly line: number;
|
|
363
|
+
readonly result: ResolveResult;
|
|
364
|
+
}): WardenImportResolution => {
|
|
365
|
+
const normalizedImporter = normalizeRealPath(importerPath);
|
|
366
|
+
const importerInfo = findPackageInfoForPath(normalizedImporter);
|
|
367
|
+
const specifier = packageSpecifierParts(importSource);
|
|
368
|
+
const resolvedInfo = resolveResultPackageInfo(result);
|
|
369
|
+
const packageInfo =
|
|
370
|
+
resolvedInfo ??
|
|
371
|
+
(specifier
|
|
372
|
+
? findNodeModulesPackageInfo(normalizedImporter, specifier.packageName)
|
|
373
|
+
: null);
|
|
374
|
+
const resolvedPath = result.path ? normalizeRealPath(result.path) : undefined;
|
|
375
|
+
const packageRoot = packageInfo?.root;
|
|
376
|
+
const packageName = packageInfo?.name ?? specifier?.packageName;
|
|
377
|
+
const errorMessage = result.error;
|
|
378
|
+
const builtinModule = result.builtin?.resolved;
|
|
379
|
+
const errorKind = resolveErrorKind(errorMessage, builtinModule);
|
|
380
|
+
const crossesPackageBoundary = Boolean(
|
|
381
|
+
importerInfo?.root && packageRoot && importerInfo.root !== packageRoot
|
|
382
|
+
);
|
|
383
|
+
const usesPublicExport = Boolean(
|
|
384
|
+
!errorMessage && specifier && hasExportsMap(packageInfo)
|
|
385
|
+
);
|
|
386
|
+
const isInternalTarget = isInternalTargetPath(packageRoot, resolvedPath);
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
crossesPackageBoundary,
|
|
390
|
+
importSource,
|
|
391
|
+
importerPath: normalizedImporter,
|
|
392
|
+
isInternalTarget,
|
|
393
|
+
line,
|
|
394
|
+
usesPublicExport,
|
|
395
|
+
...optionalResolutionFields({
|
|
396
|
+
builtinModule,
|
|
397
|
+
errorKind,
|
|
398
|
+
errorMessage,
|
|
399
|
+
packageName,
|
|
400
|
+
packageRoot,
|
|
401
|
+
resolvedPath,
|
|
402
|
+
}),
|
|
403
|
+
};
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const getModuleSourceNode = (node: AstNode): AstNode | undefined =>
|
|
407
|
+
(node as unknown as { source?: AstNode }).source;
|
|
408
|
+
|
|
409
|
+
const isStaticImportNode = (node: AstNode): boolean =>
|
|
410
|
+
node.type === 'ImportDeclaration' ||
|
|
411
|
+
node.type === 'ExportNamedDeclaration' ||
|
|
412
|
+
node.type === 'ExportAllDeclaration';
|
|
413
|
+
|
|
414
|
+
const isDynamicImportExpression = (node: AstNode): boolean =>
|
|
415
|
+
node.type === 'ImportExpression';
|
|
416
|
+
|
|
417
|
+
const isTypeImportNode = (node: AstNode): boolean =>
|
|
418
|
+
node.type === 'TSImportType';
|
|
419
|
+
|
|
420
|
+
const isRequireCallExpression = (node: AstNode): boolean => {
|
|
421
|
+
if (node.type !== 'CallExpression') {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
const { callee } = node as unknown as { callee?: AstNode };
|
|
425
|
+
return (
|
|
426
|
+
callee?.type === 'Identifier' &&
|
|
427
|
+
(callee as unknown as { name?: string }).name === 'require'
|
|
428
|
+
);
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const getRequireSourceNode = (node: AstNode): AstNode | undefined =>
|
|
432
|
+
(node as unknown as { arguments?: readonly AstNode[] }).arguments?.[0];
|
|
433
|
+
|
|
434
|
+
export const collectImportSpecifiers = (
|
|
435
|
+
filePath: string,
|
|
436
|
+
sourceCode: string
|
|
437
|
+
): readonly WardenImportSpecifier[] => {
|
|
438
|
+
const ast = parse(filePath, sourceCode);
|
|
439
|
+
if (!ast) {
|
|
440
|
+
return [];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const specifiers: WardenImportSpecifier[] = [];
|
|
444
|
+
walk(ast, (node) => {
|
|
445
|
+
if (
|
|
446
|
+
!isStaticImportNode(node) &&
|
|
447
|
+
!isDynamicImportExpression(node) &&
|
|
448
|
+
!isTypeImportNode(node) &&
|
|
449
|
+
!isRequireCallExpression(node)
|
|
450
|
+
) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const source = isRequireCallExpression(node)
|
|
454
|
+
? getRequireSourceNode(node)
|
|
455
|
+
: getModuleSourceNode(node);
|
|
456
|
+
const importSource =
|
|
457
|
+
source && isStringLiteral(source) ? getStringValue(source) : null;
|
|
458
|
+
if (!importSource) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
specifiers.push({
|
|
462
|
+
importSource,
|
|
463
|
+
line: offsetToLine(sourceCode, node.start),
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
return specifiers;
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
export const createWardenResolver = (
|
|
470
|
+
options: WardenResolverOptions = {}
|
|
471
|
+
): WardenProjectResolver => {
|
|
472
|
+
const rootDir = normalizeRealPath(options.rootDir ?? process.cwd());
|
|
473
|
+
const resolveOptions = mergeWardenResolveOptions(options.resolveOptions);
|
|
474
|
+
const resolver = new ResolverFactory(resolveOptions);
|
|
475
|
+
const cache = new Map<string, ResolveResult>();
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
clearCache: () => {
|
|
479
|
+
cache.clear();
|
|
480
|
+
resolver.clearCache();
|
|
481
|
+
},
|
|
482
|
+
resolveImport: (
|
|
483
|
+
importerPath: string,
|
|
484
|
+
importSource: string,
|
|
485
|
+
line = 1
|
|
486
|
+
): WardenImportResolution => {
|
|
487
|
+
const absoluteImporterPath = isAbsolute(importerPath)
|
|
488
|
+
? importerPath
|
|
489
|
+
: resolve(rootDir, importerPath);
|
|
490
|
+
const normalizedImporterPath = normalizeRealPath(absoluteImporterPath);
|
|
491
|
+
const key = `${normalizedImporterPath}\0${importSource}`;
|
|
492
|
+
const cached = cache.get(key);
|
|
493
|
+
if (cached) {
|
|
494
|
+
return buildResolution({
|
|
495
|
+
importSource,
|
|
496
|
+
importerPath: normalizedImporterPath,
|
|
497
|
+
line,
|
|
498
|
+
result: cached,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
const result = resolver.resolveFileSync(
|
|
502
|
+
normalizedImporterPath,
|
|
503
|
+
importSource
|
|
504
|
+
);
|
|
505
|
+
const resolution = buildResolution({
|
|
506
|
+
importSource,
|
|
507
|
+
importerPath: normalizedImporterPath,
|
|
508
|
+
line,
|
|
509
|
+
result,
|
|
510
|
+
});
|
|
511
|
+
cache.set(key, result);
|
|
512
|
+
return resolution;
|
|
513
|
+
},
|
|
514
|
+
resolveOptions,
|
|
515
|
+
rootDir,
|
|
516
|
+
};
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
export const collectImportResolutionsForFile = ({
|
|
520
|
+
filePath,
|
|
521
|
+
resolver,
|
|
522
|
+
sourceCode,
|
|
523
|
+
}: {
|
|
524
|
+
readonly filePath: string;
|
|
525
|
+
readonly resolver: WardenProjectResolver;
|
|
526
|
+
readonly sourceCode: string;
|
|
527
|
+
}): readonly WardenImportResolution[] =>
|
|
528
|
+
collectImportSpecifiers(filePath, sourceCode).map((specifier) =>
|
|
529
|
+
resolver.resolveImport(filePath, specifier.importSource, specifier.line)
|
|
530
|
+
);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { Topo } from '@ontrails/core';
|
|
2
|
+
|
|
3
|
+
import type { TopoAwareWardenRule, WardenDiagnostic } from './types.js';
|
|
4
|
+
|
|
5
|
+
const RULE_NAME = 'activation-orphan';
|
|
6
|
+
const TOPO_FILE = '<topo>';
|
|
7
|
+
const DRAFT_ID_PREFIX = ['_draft', '.'].join('');
|
|
8
|
+
|
|
9
|
+
const isDraftSourceId = (id: string): boolean => id.startsWith(DRAFT_ID_PREFIX);
|
|
10
|
+
|
|
11
|
+
const sortedUnique = (values: Iterable<string>): readonly string[] =>
|
|
12
|
+
[...new Set(values)].toSorted();
|
|
13
|
+
|
|
14
|
+
const collectSignalProducerIds = (topo: Topo): ReadonlySet<string> => {
|
|
15
|
+
const producerIds = new Set<string>();
|
|
16
|
+
|
|
17
|
+
for (const signal of topo.listSignals()) {
|
|
18
|
+
if ((signal.from?.length ?? 0) > 0) {
|
|
19
|
+
producerIds.add(signal.id);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const resource of topo.resources.values()) {
|
|
24
|
+
for (const signal of resource.signals ?? []) {
|
|
25
|
+
producerIds.add(signal.id);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const trail of topo.list()) {
|
|
30
|
+
for (const signalId of trail.fires) {
|
|
31
|
+
producerIds.add(signalId);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return producerIds;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const collectKnownSignalIds = (topo: Topo): ReadonlySet<string> =>
|
|
39
|
+
new Set(topo.listSignals().map((signal) => signal.id));
|
|
40
|
+
|
|
41
|
+
const collectSignalConsumers = (
|
|
42
|
+
topo: Topo
|
|
43
|
+
): ReadonlyMap<string, readonly string[]> => {
|
|
44
|
+
const consumersBySignal = new Map<string, Set<string>>();
|
|
45
|
+
|
|
46
|
+
for (const trail of topo.list()) {
|
|
47
|
+
for (const activation of trail.activationSources) {
|
|
48
|
+
if (activation.source.kind !== 'signal') {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const consumers =
|
|
52
|
+
consumersBySignal.get(activation.source.id) ?? new Set<string>();
|
|
53
|
+
consumers.add(trail.id);
|
|
54
|
+
consumersBySignal.set(activation.source.id, consumers);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return new Map(
|
|
59
|
+
[...consumersBySignal.entries()].map(([signalId, consumers]) => [
|
|
60
|
+
signalId,
|
|
61
|
+
sortedUnique(consumers),
|
|
62
|
+
])
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const buildDiagnostic = (
|
|
67
|
+
signalId: string,
|
|
68
|
+
consumerIds: readonly string[]
|
|
69
|
+
): WardenDiagnostic => ({
|
|
70
|
+
filePath: TOPO_FILE,
|
|
71
|
+
line: 1,
|
|
72
|
+
message: `Signal activation source "${signalId}" activates trail${consumerIds.length === 1 ? '' : 's'} ${consumerIds.map((id) => `"${id}"`).join(', ')} but has no producer declaration in the topo. Add a trail fires: declaration, add signal from: producer metadata, or remove the unused activation source.`,
|
|
73
|
+
rule: RULE_NAME,
|
|
74
|
+
severity: 'warn',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export const activationOrphan: TopoAwareWardenRule = {
|
|
78
|
+
checkTopo: (topo) => {
|
|
79
|
+
const knownSignalIds = collectKnownSignalIds(topo);
|
|
80
|
+
const producerIds = collectSignalProducerIds(topo);
|
|
81
|
+
const consumersBySignal = collectSignalConsumers(topo);
|
|
82
|
+
|
|
83
|
+
return [...consumersBySignal.entries()]
|
|
84
|
+
.filter(
|
|
85
|
+
([signalId]) =>
|
|
86
|
+
!isDraftSourceId(signalId) &&
|
|
87
|
+
knownSignalIds.has(signalId) &&
|
|
88
|
+
!producerIds.has(signalId)
|
|
89
|
+
)
|
|
90
|
+
.toSorted(([a], [b]) => a.localeCompare(b))
|
|
91
|
+
.map(([signalId, consumerIds]) => buildDiagnostic(signalId, consumerIds));
|
|
92
|
+
},
|
|
93
|
+
description:
|
|
94
|
+
'Warn when signal activation consumers reference sources with no producer declaration in the topo.',
|
|
95
|
+
name: RULE_NAME,
|
|
96
|
+
severity: 'warn',
|
|
97
|
+
};
|