@shivaedev/quality 0.22.0 → 0.24.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 +13 -0
- package/README.md +4 -0
- package/dist/baseline/compare.d.ts +2 -2
- package/dist/baseline/compare.d.ts.map +1 -1
- package/dist/baseline/compare.js +5 -3
- package/dist/baseline/compare.js.map +1 -1
- package/dist/baseline/format.d.ts +5 -5
- package/dist/baseline/format.d.ts.map +1 -1
- package/dist/baseline/format.js +23 -13
- package/dist/baseline/format.js.map +1 -1
- package/dist/baseline/legacy.d.ts +2 -2
- package/dist/baseline/legacy.d.ts.map +1 -1
- package/dist/baseline/legacy.js +4 -4
- package/dist/baseline/legacy.js.map +1 -1
- package/dist/baseline/rewrite.d.ts +1 -1
- package/dist/baseline/rewrite.d.ts.map +1 -1
- package/dist/baseline/rewrite.js +2 -2
- package/dist/baseline/rewrite.js.map +1 -1
- package/dist/baseline/update.d.ts +2 -2
- package/dist/baseline/update.d.ts.map +1 -1
- package/dist/baseline/update.js +24 -18
- package/dist/baseline/update.js.map +1 -1
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +15 -9
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/baseline.d.ts +1 -1
- package/dist/cli/baseline.d.ts.map +1 -1
- package/dist/cli/baseline.js +10 -6
- package/dist/cli/baseline.js.map +1 -1
- package/dist/cli/fix.d.ts +2 -2
- package/dist/cli/fix.d.ts.map +1 -1
- package/dist/cli/fix.js +34 -40
- package/dist/cli/fix.js.map +1 -1
- package/dist/cli/lint.d.ts +1 -1
- package/dist/cli/lint.d.ts.map +1 -1
- package/dist/cli/lint.js +1 -1
- package/dist/cli/lint.js.map +1 -1
- package/dist/cli/migrate.d.ts.map +1 -1
- package/dist/cli/migrate.js +1 -1
- package/dist/cli/migrate.js.map +1 -1
- package/dist/cli/program.d.ts +9 -0
- package/dist/cli/program.d.ts.map +1 -0
- package/dist/cli/program.js +44 -0
- package/dist/cli/program.js.map +1 -0
- package/dist/cli/run-main.d.ts +5 -2
- package/dist/cli/run-main.d.ts.map +1 -1
- package/dist/cli/run-main.js +16 -12
- package/dist/cli/run-main.js.map +1 -1
- package/dist/cli/shrink.d.ts +2 -2
- package/dist/cli/shrink.d.ts.map +1 -1
- package/dist/cli/shrink.js +9 -3
- package/dist/cli/shrink.js.map +1 -1
- package/dist/cli.js +2 -49
- package/dist/cli.js.map +1 -1
- package/dist/config/decode.d.ts +1 -1
- package/dist/config/decode.d.ts.map +1 -1
- package/dist/config/decode.js +13 -9
- package/dist/config/decode.js.map +1 -1
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +26 -20
- package/dist/config/load.js.map +1 -1
- package/dist/config/resolve.d.ts +22 -2
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +18 -10
- package/dist/config/resolve.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -1
- package/dist/config.js.map +1 -1
- package/dist/decoded.d.ts +2 -2
- package/dist/decoded.d.ts.map +1 -1
- package/dist/decoded.js +7 -5
- package/dist/decoded.js.map +1 -1
- package/dist/engine/baseline-file.d.ts +1 -1
- package/dist/engine/baseline-file.d.ts.map +1 -1
- package/dist/engine/baseline-file.js +4 -2
- package/dist/engine/baseline-file.js.map +1 -1
- package/dist/engine/evaluate.d.ts +2 -2
- package/dist/engine/evaluate.d.ts.map +1 -1
- package/dist/engine/evaluate.js +5 -3
- package/dist/engine/evaluate.js.map +1 -1
- package/dist/engine/run-rules.d.ts +1 -1
- package/dist/engine/run-rules.d.ts.map +1 -1
- package/dist/engine/run-rules.js +24 -14
- package/dist/engine/run-rules.js.map +1 -1
- package/dist/engine/session.d.ts.map +1 -1
- package/dist/engine/session.js +8 -4
- package/dist/engine/session.js.map +1 -1
- package/dist/engine/violation.d.ts +7 -7
- package/dist/engine/violation.d.ts.map +1 -1
- package/dist/engine/violation.js +22 -10
- package/dist/engine/violation.js.map +1 -1
- package/dist/exceptions/registry.d.ts +2 -2
- package/dist/exceptions/registry.d.ts.map +1 -1
- package/dist/exceptions/registry.js +10 -6
- package/dist/exceptions/registry.js.map +1 -1
- package/dist/git/base.d.ts +1 -2
- package/dist/git/base.d.ts.map +1 -1
- package/dist/git/base.js +22 -10
- package/dist/git/base.js.map +1 -1
- package/dist/git/command.d.ts +2 -2
- package/dist/git/command.d.ts.map +1 -1
- package/dist/git/command.js +13 -9
- package/dist/git/command.js.map +1 -1
- package/dist/git/history.d.ts +1 -1
- package/dist/git/history.d.ts.map +1 -1
- package/dist/git/history.js +5 -3
- package/dist/git/history.js.map +1 -1
- package/dist/inventory/collect.d.ts +1 -1
- package/dist/inventory/collect.d.ts.map +1 -1
- package/dist/inventory/collect.js +7 -5
- package/dist/inventory/collect.js.map +1 -1
- package/dist/inventory/filesystem.d.ts +1 -1
- package/dist/inventory/filesystem.d.ts.map +1 -1
- package/dist/inventory/filesystem.js +11 -7
- package/dist/inventory/filesystem.js.map +1 -1
- package/dist/inventory/ignore-scope.d.ts +4 -4
- package/dist/inventory/ignore-scope.d.ts.map +1 -1
- package/dist/inventory/ignore-scope.js +18 -9
- package/dist/inventory/ignore-scope.js.map +1 -1
- package/dist/manifests/sorted.d.ts +1 -1
- package/dist/manifests/sorted.d.ts.map +1 -1
- package/dist/manifests/sorted.js +14 -16
- package/dist/manifests/sorted.js.map +1 -1
- package/dist/report/plural.d.ts +1 -1
- package/dist/report/plural.d.ts.map +1 -1
- package/dist/report/plural.js +3 -1
- package/dist/report/plural.js.map +1 -1
- package/dist/report/render.d.ts +1 -1
- package/dist/report/render.d.ts.map +1 -1
- package/dist/report/render.js +29 -21
- package/dist/report/render.js.map +1 -1
- package/dist/report/stale.d.ts +1 -1
- package/dist/report/stale.d.ts.map +1 -1
- package/dist/report/stale.js +18 -8
- package/dist/report/stale.js.map +1 -1
- package/dist/rule.d.ts +13 -13
- package/dist/rule.d.ts.map +1 -1
- package/dist/rule.js +10 -8
- package/dist/rule.js.map +1 -1
- package/dist/rules/built-in.d.ts +2 -2
- package/dist/rules/built-in.d.ts.map +1 -1
- package/dist/rules/built-in.js +6 -0
- package/dist/rules/built-in.js.map +1 -1
- package/dist/rules/comments/kinds.d.ts +4 -4
- package/dist/rules/comments/kinds.d.ts.map +1 -1
- package/dist/rules/comments/kinds.js +16 -8
- package/dist/rules/comments/kinds.js.map +1 -1
- package/dist/rules/comments/max-per-file.d.ts.map +1 -1
- package/dist/rules/comments/max-per-file.js +13 -9
- package/dist/rules/comments/max-per-file.js.map +1 -1
- package/dist/rules/comments/pattern-rule.d.ts +1 -1
- package/dist/rules/comments/pattern-rule.d.ts.map +1 -1
- package/dist/rules/comments/pattern-rule.js +12 -10
- package/dist/rules/comments/pattern-rule.js.map +1 -1
- package/dist/rules/comments/scan.d.ts +3 -3
- package/dist/rules/comments/scan.d.ts.map +1 -1
- package/dist/rules/comments/scan.js +23 -19
- package/dist/rules/comments/scan.js.map +1 -1
- package/dist/rules/max-lines.d.ts.map +1 -1
- package/dist/rules/max-lines.js +12 -10
- package/dist/rules/max-lines.js.map +1 -1
- package/dist/rules/maxDepth.d.ts +2 -0
- package/dist/rules/maxDepth.d.ts.map +1 -0
- package/dist/rules/maxDepth.js +20 -0
- package/dist/rules/maxDepth.js.map +1 -0
- package/dist/rules/noBarrel.d.ts +2 -0
- package/dist/rules/noBarrel.d.ts.map +1 -0
- package/dist/rules/noBarrel.js +14 -0
- package/dist/rules/noBarrel.js.map +1 -0
- package/dist/rules/stories/origins.d.ts +10 -0
- package/dist/rules/stories/origins.d.ts.map +1 -0
- package/dist/rules/stories/origins.js +129 -0
- package/dist/rules/stories/origins.js.map +1 -0
- package/dist/rules/stories/prerequisites.d.ts +4 -0
- package/dist/rules/stories/prerequisites.d.ts.map +1 -0
- package/dist/rules/stories/prerequisites.js +32 -0
- package/dist/rules/stories/prerequisites.js.map +1 -0
- package/dist/rules/stories/program.d.ts +5 -0
- package/dist/rules/stories/program.d.ts.map +1 -0
- package/dist/rules/stories/program.js +22 -0
- package/dist/rules/stories/program.js.map +1 -0
- package/dist/rules/stories/specs.d.ts +2 -0
- package/dist/rules/stories/specs.d.ts.map +1 -0
- package/dist/rules/stories/specs.js +80 -0
- package/dist/rules/stories/specs.js.map +1 -0
- package/dist/rules/suppressions/biome/configs.d.ts +1 -1
- package/dist/rules/suppressions/biome/configs.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/configs.js +23 -17
- package/dist/rules/suppressions/biome/configs.js.map +1 -1
- package/dist/rules/suppressions/biome/declarations.d.ts +4 -4
- package/dist/rules/suppressions/biome/declarations.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/declarations.js +15 -7
- package/dist/rules/suppressions/biome/declarations.js.map +1 -1
- package/dist/rules/suppressions/biome/json.d.ts +6 -6
- package/dist/rules/suppressions/biome/json.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/json.js +27 -13
- package/dist/rules/suppressions/biome/json.js.map +1 -1
- package/dist/rules/suppressions/biome/resolve.d.ts +1 -1
- package/dist/rules/suppressions/biome/resolve.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/resolve.js +23 -17
- package/dist/rules/suppressions/biome/resolve.js.map +1 -1
- package/dist/rules/suppressions/biome/weakenings.d.ts +1 -1
- package/dist/rules/suppressions/biome/weakenings.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/weakenings.js +78 -52
- package/dist/rules/suppressions/biome/weakenings.js.map +1 -1
- package/dist/rules/suppressions/biome-overrides.d.ts.map +1 -1
- package/dist/rules/suppressions/biome-overrides.js +45 -33
- package/dist/rules/suppressions/biome-overrides.js.map +1 -1
- package/dist/rules/suppressions/directives.d.ts +1 -1
- package/dist/rules/suppressions/directives.d.ts.map +1 -1
- package/dist/rules/suppressions/directives.js +5 -3
- package/dist/rules/suppressions/directives.js.map +1 -1
- package/dist/rules/suppressions/no-double-cast.d.ts.map +1 -1
- package/dist/rules/suppressions/no-double-cast.js +14 -10
- package/dist/rules/suppressions/no-double-cast.js.map +1 -1
- package/dist/rules/suppressions/no-inline.d.ts.map +1 -1
- package/dist/rules/suppressions/no-inline.js +8 -6
- package/dist/rules/suppressions/no-inline.js.map +1 -1
- package/dist/rules/suppressions/stylesheet-comments.d.ts +1 -1
- package/dist/rules/suppressions/stylesheet-comments.d.ts.map +1 -1
- package/dist/rules/suppressions/stylesheet-comments.js +11 -5
- package/dist/rules/suppressions/stylesheet-comments.js.map +1 -1
- package/dist/rules/syntax.d.ts +1 -1
- package/dist/rules/syntax.d.ts.map +1 -1
- package/dist/rules/syntax.js +2 -2
- package/dist/rules/syntax.js.map +1 -1
- package/dist/standard-schema.d.ts +6 -6
- package/dist/standard-schema.d.ts.map +1 -1
- package/docs/modules.md +19 -1
- package/docs/roadmap.md +1 -1
- package/docs/rules.md +9 -1
- package/package.json +2 -2
- package/src/baseline/compare.ts +5 -3
- package/src/baseline/format.ts +17 -10
- package/src/baseline/legacy.ts +4 -4
- package/src/baseline/rewrite.ts +2 -2
- package/src/baseline/update.ts +20 -16
- package/src/cli/args.ts +15 -10
- package/src/cli/baseline.ts +36 -35
- package/src/cli/fix.ts +39 -42
- package/src/cli/lint.ts +17 -19
- package/src/cli/migrate.ts +20 -21
- package/src/cli/program.ts +50 -0
- package/src/cli/run-main.ts +19 -13
- package/src/cli/shrink.ts +11 -8
- package/src/cli.ts +2 -51
- package/src/config/decode.ts +15 -10
- package/src/config/load.ts +44 -37
- package/src/config/resolve.ts +32 -27
- package/src/config.ts +3 -1
- package/src/decoded.ts +5 -4
- package/src/engine/baseline-file.ts +14 -12
- package/src/engine/evaluate.ts +6 -5
- package/src/engine/run-rules.ts +21 -13
- package/src/engine/session.ts +32 -25
- package/src/engine/violation.ts +22 -11
- package/src/exceptions/registry.ts +10 -8
- package/src/git/base.ts +45 -37
- package/src/git/command.ts +6 -4
- package/src/git/history.ts +5 -4
- package/src/inventory/collect.ts +39 -34
- package/src/inventory/filesystem.ts +38 -30
- package/src/inventory/ignore-scope.ts +20 -10
- package/src/manifests/sorted.ts +18 -18
- package/src/report/plural.ts +3 -1
- package/src/report/render.ts +23 -18
- package/src/report/stale.ts +33 -25
- package/src/rule.ts +25 -19
- package/src/rules/built-in.ts +6 -0
- package/src/rules/comments/kinds.ts +16 -9
- package/src/rules/comments/max-per-file.ts +15 -11
- package/src/rules/comments/pattern-rule.ts +5 -4
- package/src/rules/comments/scan.ts +18 -15
- package/src/rules/max-lines.ts +3 -2
- package/src/rules/maxDepth.ts +24 -0
- package/src/rules/noBarrel.ts +16 -0
- package/src/rules/stories/origins.ts +147 -0
- package/src/rules/stories/prerequisites.ts +37 -0
- package/src/rules/stories/program.ts +29 -0
- package/src/rules/stories/specs.ts +93 -0
- package/src/rules/suppressions/biome/configs.ts +23 -17
- package/src/rules/suppressions/biome/declarations.ts +15 -10
- package/src/rules/suppressions/biome/json.ts +25 -13
- package/src/rules/suppressions/biome/resolve.ts +23 -18
- package/src/rules/suppressions/biome/weakenings.ts +60 -41
- package/src/rules/suppressions/biome-overrides.ts +32 -22
- package/src/rules/suppressions/directives.ts +5 -4
- package/src/rules/suppressions/no-double-cast.ts +14 -10
- package/src/rules/suppressions/no-inline.ts +5 -4
- package/src/rules/suppressions/stylesheet-comments.ts +11 -6
- package/src/rules/syntax.ts +2 -2
- package/src/standard-schema.ts +4 -4
package/src/report/stale.ts
CHANGED
|
@@ -8,31 +8,39 @@ interface StaleFiles {
|
|
|
8
8
|
readonly registry: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
lines.length === 0
|
|
11
|
+
function section(label: string, file: string, guidance: string, lines: readonly string[]): readonly string[] {
|
|
12
|
+
return lines.length === 0
|
|
13
|
+
? []
|
|
14
|
+
: [[`${label} ${file}: ${plural(lines.length, "stale entry", "stale entries")}`, ` ${guidance}`, ...lines].join("\n")];
|
|
15
|
+
}
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
function baselineLine({ entry, problem }: StaleBaselineEntry): string {
|
|
18
|
+
return ` ${entry.rule} ${entry.file} ${problem}.`;
|
|
19
|
+
}
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
` ${entry.rule} ${entry.file}${entry.subject === undefined ? "" : ` (${entry.subject})`} ${problem}.`;
|
|
21
|
+
function registryLine({ entry, problem }: StaleRegistryEntry): string {
|
|
22
|
+
return ` ${entry.rule} ${entry.file}${entry.subject === undefined ? "" : ` (${entry.subject})`} ${problem}.`;
|
|
23
|
+
}
|
|
18
24
|
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
25
|
+
export function staleSections(outcome: Outcome, files: StaleFiles): readonly string[] {
|
|
26
|
+
return [
|
|
27
|
+
...section(
|
|
28
|
+
"error",
|
|
29
|
+
files.baseline,
|
|
30
|
+
"An entry for a rule that is off or unknown covers nothing. Run `quality baseline prune` to drop it.",
|
|
31
|
+
outcome.staleBaseline.map(baselineLine),
|
|
32
|
+
),
|
|
33
|
+
...section(
|
|
34
|
+
"note",
|
|
35
|
+
files.baseline,
|
|
36
|
+
"These pass, and `quality baseline tighten` lowers them when their files change. `quality baseline prune` lowers them all.",
|
|
37
|
+
outcome.looseBaseline.map(baselineLine),
|
|
38
|
+
),
|
|
39
|
+
...section(
|
|
40
|
+
"error",
|
|
41
|
+
files.registry,
|
|
42
|
+
"Every registered exception must still apply. Remove the entries whose exception is gone.",
|
|
43
|
+
outcome.staleRegistry.map(registryLine),
|
|
44
|
+
),
|
|
45
|
+
];
|
|
46
|
+
}
|
package/src/rule.ts
CHANGED
|
@@ -28,35 +28,35 @@ export interface RuleInputs {
|
|
|
28
28
|
readonly sources: readonly SourceFile[];
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export interface RuleContext<
|
|
32
|
-
readonly options:
|
|
31
|
+
export interface RuleContext<TOptions> extends RuleInputs {
|
|
32
|
+
readonly options: TOptions;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export type Configured =
|
|
36
36
|
| { readonly _tag: "Invalid"; readonly issues: readonly string[] }
|
|
37
37
|
| { readonly _tag: "Ready"; readonly check: (inputs: RuleInputs) => Promise<Findings> };
|
|
38
38
|
|
|
39
|
-
export interface Rule<
|
|
39
|
+
export interface Rule<TId extends string = string, TInput = unknown> {
|
|
40
40
|
// Bivariance keeps rules with different options in one list; configure validates its input at runtime.
|
|
41
|
-
configure: Bivariant<(options:
|
|
41
|
+
configure: Bivariant<(options: TInput | undefined) => Promise<Configured>>;
|
|
42
42
|
readonly description: string;
|
|
43
43
|
readonly family?: boolean;
|
|
44
|
-
readonly id:
|
|
44
|
+
readonly id: TId;
|
|
45
45
|
readonly registrable?: boolean;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
interface RuleDefinition<
|
|
49
|
-
readonly check: (context: RuleContext<
|
|
48
|
+
interface RuleDefinition<TId extends string, TOptions> {
|
|
49
|
+
readonly check: (context: RuleContext<TOptions>) => Findings | PromiseLike<Findings>;
|
|
50
50
|
readonly description: string;
|
|
51
|
-
readonly id:
|
|
51
|
+
readonly id: TId;
|
|
52
52
|
readonly registrable?: false;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
interface RuleDefinitionWithOptions<
|
|
56
|
-
readonly options: StandardSchemaV1<
|
|
55
|
+
interface RuleDefinitionWithOptions<TId extends string, TInput, TOptions> extends RuleDefinition<TId, TOptions> {
|
|
56
|
+
readonly options: StandardSchemaV1<TInput, TOptions>;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
interface RuleDefinitionWithoutOptions<
|
|
59
|
+
interface RuleDefinitionWithoutOptions<TId extends string> extends RuleDefinition<TId, undefined> {
|
|
60
60
|
readonly options?: undefined;
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -68,9 +68,12 @@ const noOptions: StandardSchemaV1<undefined, undefined> = {
|
|
|
68
68
|
},
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
function configureWith<TInput, TOptions>(
|
|
72
|
+
schema: StandardSchemaV1<TInput, TOptions>,
|
|
73
|
+
check: RuleDefinition<string, TOptions>["check"],
|
|
74
|
+
absent: unknown,
|
|
75
|
+
) {
|
|
76
|
+
return async (options: unknown): Promise<Configured> => {
|
|
74
77
|
const result = await schema["~standard"].validate(options ?? absent);
|
|
75
78
|
if (result.issues !== undefined) {
|
|
76
79
|
return { _tag: "Invalid", issues: result.issues.map(describeIssue) };
|
|
@@ -78,12 +81,15 @@ const configureWith =
|
|
|
78
81
|
const value = result.value;
|
|
79
82
|
return { _tag: "Ready", check: async (inputs) => check({ ...inputs, options: value }) };
|
|
80
83
|
};
|
|
84
|
+
}
|
|
81
85
|
|
|
82
|
-
export function defineRule<const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
export function defineRule<const TId extends string, TInput, TOptions>(
|
|
87
|
+
definition: RuleDefinitionWithOptions<TId, TInput, TOptions>,
|
|
88
|
+
): Rule<TId, TInput>;
|
|
89
|
+
export function defineRule<const TId extends string>(definition: RuleDefinitionWithoutOptions<TId>): Rule<TId, undefined>;
|
|
90
|
+
export function defineRule<const TId extends string, TInput, TOptions>(
|
|
91
|
+
definition: RuleDefinitionWithOptions<TId, TInput, TOptions> | RuleDefinitionWithoutOptions<TId>,
|
|
92
|
+
): Rule<TId, TInput> | Rule<TId, undefined> {
|
|
87
93
|
const configure =
|
|
88
94
|
definition.options === undefined
|
|
89
95
|
? configureWith(noOptions, definition.check, undefined)
|
package/src/rules/built-in.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { noTestCode } from "#rules/imports/noTestCode.ts";
|
|
|
17
17
|
import { importsResolvable } from "#rules/imports/resolvable.ts";
|
|
18
18
|
import { importsRuntime } from "#rules/imports/runtime.ts";
|
|
19
19
|
import { storySetup } from "#rules/stories/setup.ts";
|
|
20
|
+
import { storySpecs } from "#rules/stories/specs.ts";
|
|
20
21
|
import { biomeOverrides } from "#rules/suppressions/biome-overrides.ts";
|
|
21
22
|
import { biomeRecommended } from "#rules/suppressions/biomeRecommended.ts";
|
|
22
23
|
import { noDoubleCast } from "#rules/suppressions/no-double-cast.ts";
|
|
@@ -27,9 +28,13 @@ import { testsFollow } from "#rules/test-names/follow.ts";
|
|
|
27
28
|
import { biome } from "./biome.ts";
|
|
28
29
|
import { manifestsSorted } from "./manifests-sorted.ts";
|
|
29
30
|
import { maxLines } from "./max-lines.ts";
|
|
31
|
+
import { maxDepth } from "./maxDepth.ts";
|
|
32
|
+
import { noBarrel } from "./noBarrel.ts";
|
|
30
33
|
|
|
31
34
|
export const builtInRules = [
|
|
32
35
|
maxLines,
|
|
36
|
+
maxDepth,
|
|
37
|
+
noBarrel,
|
|
33
38
|
noJsdoc,
|
|
34
39
|
noLineReference,
|
|
35
40
|
noPrReference,
|
|
@@ -58,4 +63,5 @@ export const builtInRules = [
|
|
|
58
63
|
testsFollow,
|
|
59
64
|
testsColocated,
|
|
60
65
|
storySetup,
|
|
66
|
+
storySpecs,
|
|
61
67
|
] as const;
|
|
@@ -15,21 +15,28 @@ const DIRECTIVES: readonly RegExp[] = [
|
|
|
15
15
|
|
|
16
16
|
const TRIPLE_SLASH = /^\/\/\/\s*</u;
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
function contentOf(comment: SourceComment): readonly string[] {
|
|
19
|
+
return comment.body.filter((line) => line !== "");
|
|
20
|
+
}
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
tags.some((tag) => line === tag || line.startsWith(`${tag} `) || line.startsWith(`${tag}\t`));
|
|
22
|
+
function startsWithTag(line: string, tags: readonly string[]): boolean {
|
|
23
|
+
return tags.some((tag) => line === tag || line.startsWith(`${tag} `) || line.startsWith(`${tag}\t`));
|
|
24
|
+
}
|
|
22
25
|
|
|
23
|
-
export
|
|
26
|
+
export function textOf(comment: SourceComment): string {
|
|
27
|
+
return comment.body.join("\n");
|
|
28
|
+
}
|
|
24
29
|
|
|
25
|
-
export
|
|
30
|
+
export function isJsdoc(comment: SourceComment): boolean {
|
|
31
|
+
return comment.kind === "block" && comment.text.startsWith("/**") && comment.text !== "/**/";
|
|
32
|
+
}
|
|
26
33
|
|
|
27
|
-
export
|
|
34
|
+
export function isPragma(comment: SourceComment, pragmas: readonly string[]): boolean {
|
|
28
35
|
const content = contentOf(comment);
|
|
29
36
|
return content.length > 0 && content.every((line) => startsWithTag(line, pragmas));
|
|
30
|
-
}
|
|
37
|
+
}
|
|
31
38
|
|
|
32
|
-
export
|
|
39
|
+
export function isDirective(comment: SourceComment): boolean {
|
|
33
40
|
const first = contentOf(comment)[0] ?? "";
|
|
34
41
|
return TRIPLE_SLASH.test(comment.text) || DIRECTIVES.some((directive) => directive.test(first));
|
|
35
|
-
}
|
|
42
|
+
}
|
|
@@ -9,18 +9,22 @@ const MaxPerFileOptions = Schema.Struct({
|
|
|
9
9
|
max: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(2)).pipe(Schema.withDecodingDefaultKey(Effect.succeed(2))),
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
function continues(previous: SourceComment | undefined, comment: SourceComment): boolean {
|
|
13
|
+
return (
|
|
14
|
+
previous !== undefined
|
|
15
|
+
&& previous.kind === "line"
|
|
16
|
+
&& comment.kind === "line"
|
|
17
|
+
&& previous.ownLine
|
|
18
|
+
&& comment.ownLine
|
|
19
|
+
&& comment.line === previous.endLine + 1
|
|
20
|
+
);
|
|
21
|
+
}
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
comments.filter((comment, index) => !continues(comments[index - 1], comment));
|
|
23
|
+
function commentBlocks(comments: readonly SourceComment[]): readonly SourceComment[] {
|
|
24
|
+
return comments.filter((comment, index) => !continues(comments[index - 1], comment));
|
|
25
|
+
}
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
function overCap(file: SourceFile, max: number, allow: readonly string[]): readonly Finding[] {
|
|
24
28
|
const counted = commentsOf(file).filter((comment) => !(isDirective(comment) || isPragma(comment, allow)));
|
|
25
29
|
const blocks = commentBlocks(counted);
|
|
26
30
|
const first = blocks[max];
|
|
@@ -35,7 +39,7 @@ const overCap = (file: SourceFile, max: number, allow: readonly string[]): reado
|
|
|
35
39
|
threshold: max,
|
|
36
40
|
},
|
|
37
41
|
];
|
|
38
|
-
}
|
|
42
|
+
}
|
|
39
43
|
|
|
40
44
|
export const maxPerFile = defineRule({
|
|
41
45
|
check: ({ options, sources }) => sources.flatMap((file) => overCap(file, options.max, options.allow)),
|
|
@@ -9,13 +9,13 @@ interface PatternRule<Id extends string> {
|
|
|
9
9
|
readonly patterns: readonly RegExp[];
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
function firstMatch(comment: SourceComment, patterns: readonly RegExp[]): string | undefined {
|
|
13
13
|
const text = textOf(comment);
|
|
14
14
|
return patterns.map((pattern) => pattern.exec(text)?.[0]).find((match) => match !== undefined);
|
|
15
|
-
}
|
|
15
|
+
}
|
|
16
16
|
|
|
17
|
-
export
|
|
18
|
-
defineRule({
|
|
17
|
+
export function definePatternRule<const Id extends string>({ description, id, message, patterns }: PatternRule<Id>): Rule<Id, undefined> {
|
|
18
|
+
return defineRule({
|
|
19
19
|
check: ({ sources }) =>
|
|
20
20
|
sources.flatMap((file) =>
|
|
21
21
|
commentsOf(file).flatMap((comment): readonly Finding[] => {
|
|
@@ -26,3 +26,4 @@ export const definePatternRule = <const Id extends string>({ description, id, me
|
|
|
26
26
|
description,
|
|
27
27
|
id,
|
|
28
28
|
});
|
|
29
|
+
}
|
|
@@ -13,15 +13,15 @@ export interface SourceComment {
|
|
|
13
13
|
|
|
14
14
|
const DECLARATION = /\.d\.[cm]?ts$/u;
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
function commentRanges(source: ts.SourceFile): readonly ts.CommentRange[] {
|
|
17
17
|
const ranges = new Map<number, ts.CommentRange>();
|
|
18
18
|
const jsxText: ts.Node[] = [];
|
|
19
|
-
|
|
19
|
+
function add(found: readonly ts.CommentRange[] | undefined) {
|
|
20
20
|
for (const range of found ?? []) {
|
|
21
21
|
ranges.set(range.pos, range);
|
|
22
22
|
}
|
|
23
|
-
}
|
|
24
|
-
|
|
23
|
+
}
|
|
24
|
+
function visit(node: ts.Node) {
|
|
25
25
|
if (node.kind === ts.SyntaxKind.JsxText) {
|
|
26
26
|
jsxText.push(node);
|
|
27
27
|
}
|
|
@@ -30,22 +30,23 @@ const commentRanges = (source: ts.SourceFile): readonly ts.CommentRange[] => {
|
|
|
30
30
|
for (const child of node.getChildren(source)) {
|
|
31
31
|
visit(child);
|
|
32
32
|
}
|
|
33
|
-
}
|
|
33
|
+
}
|
|
34
34
|
visit(source);
|
|
35
35
|
return [...ranges.values()]
|
|
36
36
|
.filter((range) => !jsxText.some((text) => range.pos >= text.pos && range.pos < text.end))
|
|
37
37
|
.sort((left, right) => left.pos - right.pos);
|
|
38
|
-
}
|
|
38
|
+
}
|
|
39
39
|
|
|
40
|
-
export
|
|
41
|
-
kind === "line"
|
|
40
|
+
export function bodyOf(kind: SourceComment["kind"], text: string): readonly string[] {
|
|
41
|
+
return kind === "line"
|
|
42
42
|
? [text.slice(2).trim()]
|
|
43
43
|
: text
|
|
44
44
|
.slice(2, -2)
|
|
45
45
|
.split("\n")
|
|
46
46
|
.map((line) => line.trim().replace(/^\*(?=\s|$)\s*/u, ""));
|
|
47
|
+
}
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
function toComment(source: ts.SourceFile, range: ts.CommentRange): SourceComment {
|
|
49
50
|
const kind = range.kind === ts.SyntaxKind.SingleLineCommentTrivia ? "line" : "block";
|
|
50
51
|
const text = source.text.slice(range.pos, range.end);
|
|
51
52
|
const start = source.getLineAndCharacterOfPosition(range.pos);
|
|
@@ -58,16 +59,16 @@ const toComment = (source: ts.SourceFile, range: ts.CommentRange): SourceComment
|
|
|
58
59
|
ownLine: source.text.slice(lineStart, range.pos).trim() === "",
|
|
59
60
|
text,
|
|
60
61
|
};
|
|
61
|
-
}
|
|
62
|
+
}
|
|
62
63
|
|
|
63
64
|
const scanned = new WeakMap<SourceFile, readonly SourceComment[]>();
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
function scan(file: SourceFile): readonly SourceComment[] {
|
|
66
67
|
const source = parse(file);
|
|
67
68
|
return source === undefined ? [] : commentRanges(source).map((range) => toComment(source, range));
|
|
68
|
-
}
|
|
69
|
+
}
|
|
69
70
|
|
|
70
|
-
export
|
|
71
|
+
export function scanComments(file: SourceFile): readonly SourceComment[] {
|
|
71
72
|
const cached = scanned.get(file);
|
|
72
73
|
if (cached !== undefined) {
|
|
73
74
|
return cached;
|
|
@@ -75,6 +76,8 @@ export const scanComments = (file: SourceFile): readonly SourceComment[] => {
|
|
|
75
76
|
const comments = scan(file);
|
|
76
77
|
scanned.set(file, comments);
|
|
77
78
|
return comments;
|
|
78
|
-
}
|
|
79
|
+
}
|
|
79
80
|
|
|
80
|
-
export
|
|
81
|
+
export function commentsOf(file: SourceFile): readonly SourceComment[] {
|
|
82
|
+
return DECLARATION.test(file.path) ? [] : scanComments(file);
|
|
83
|
+
}
|
package/src/rules/max-lines.ts
CHANGED
|
@@ -11,8 +11,8 @@ const MaxLinesOptions = Schema.Struct({
|
|
|
11
11
|
test: Limit.check(Schema.isLessThanOrEqualTo(300)).pipe(Schema.withDecodingDefaultKey(Effect.succeed(300))),
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
file.lines.length <= limit
|
|
14
|
+
function oversized(file: SourceFile, limit: number): readonly Finding[] {
|
|
15
|
+
return file.lines.length <= limit
|
|
16
16
|
? []
|
|
17
17
|
: [
|
|
18
18
|
{
|
|
@@ -22,6 +22,7 @@ const oversized = (file: SourceFile, limit: number): readonly Finding[] =>
|
|
|
22
22
|
threshold: limit,
|
|
23
23
|
},
|
|
24
24
|
];
|
|
25
|
+
}
|
|
25
26
|
|
|
26
27
|
export const maxLines = defineRule({
|
|
27
28
|
check: ({ options, sources }) =>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineRule, type Finding, type SourceFile } from "#rule.ts";
|
|
2
|
+
|
|
3
|
+
const DECLARATION = /\.d\.[cm]?ts$/u;
|
|
4
|
+
const TOO_DEEP = /^\t{8,}/u;
|
|
5
|
+
|
|
6
|
+
function findingsOf(file: SourceFile): readonly Finding[] {
|
|
7
|
+
return file.lines.flatMap((text, index) =>
|
|
8
|
+
TOO_DEEP.test(text)
|
|
9
|
+
? [
|
|
10
|
+
{
|
|
11
|
+
file: file.path,
|
|
12
|
+
line: index + 1,
|
|
13
|
+
message: "Indentation is 8+ tabs deep. Extract a named function or component.",
|
|
14
|
+
},
|
|
15
|
+
]
|
|
16
|
+
: [],
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const maxDepth = defineRule({
|
|
21
|
+
check: ({ sources }) => sources.filter((file) => !DECLARATION.test(file.path)).flatMap(findingsOf),
|
|
22
|
+
description: "Extract a named function or component before indentation reaches eight tabs.",
|
|
23
|
+
id: "nesting/max-depth",
|
|
24
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { defineRule } from "#rule.ts";
|
|
3
|
+
|
|
4
|
+
const BARREL_FILE = /^index\.tsx?$/u;
|
|
5
|
+
|
|
6
|
+
export const noBarrel = defineRule({
|
|
7
|
+
check: ({ sources }) =>
|
|
8
|
+
sources
|
|
9
|
+
.filter((file) => BARREL_FILE.test(posix.basename(file.path)))
|
|
10
|
+
.map((file) => ({
|
|
11
|
+
file: file.path,
|
|
12
|
+
message: "index.ts and index.tsx modules are banned. Name the module after its purpose and import it directly.",
|
|
13
|
+
})),
|
|
14
|
+
description: "Name TypeScript modules after their purpose instead of index.ts or index.tsx.",
|
|
15
|
+
id: "structure/no-barrel",
|
|
16
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import type { ImportGraph } from "#imports/graph.ts";
|
|
4
|
+
import type { RuleInputs } from "#rule.ts";
|
|
5
|
+
import { storyProgram } from "./program.ts";
|
|
6
|
+
|
|
7
|
+
type Origin = "factory" | "kit" | "story" | "plain" | "suite" | "vitest" | "stories" | "function" | undefined;
|
|
8
|
+
|
|
9
|
+
function importFrom(node: ts.Node): string | undefined {
|
|
10
|
+
for (let current: ts.Node | undefined = node; current !== undefined; current = current.parent) {
|
|
11
|
+
if (ts.isImportDeclaration(current) && ts.isStringLiteral(current.moduleSpecifier)) {
|
|
12
|
+
return current.moduleSpecifier.text;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function moduleOrigin(from: string | undefined): Origin {
|
|
19
|
+
if (from === "vitest" || from === "@effect/vitest") {
|
|
20
|
+
return "vitest";
|
|
21
|
+
}
|
|
22
|
+
return from === "@shivaedev/test-story" || from === "@shivaedev/test-story/storyKit.ts" ? "stories" : undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function imported(name: string, from: string | undefined): Origin {
|
|
26
|
+
const namespace = moduleOrigin(from);
|
|
27
|
+
if (name === "storyKit" && namespace === "stories") {
|
|
28
|
+
return "factory";
|
|
29
|
+
}
|
|
30
|
+
if ((name === "describe" || name === "suite") && namespace === "vitest") {
|
|
31
|
+
return "suite";
|
|
32
|
+
}
|
|
33
|
+
return (name === "it" || name === "test") && namespace === "vitest" ? "plain" : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function storyOrigins(inputs: RuleInputs, graph: ImportGraph) {
|
|
37
|
+
const program = storyProgram(inputs, graph);
|
|
38
|
+
const checker = program.getTypeChecker();
|
|
39
|
+
const visiting = new Set<ts.Node>();
|
|
40
|
+
const known = new Map<ts.Node, Origin>();
|
|
41
|
+
|
|
42
|
+
function binding(node: ts.BindingElement): Origin {
|
|
43
|
+
if (!ts.isVariableDeclaration(node.parent.parent)) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const value = node.parent.parent.initializer;
|
|
47
|
+
const name = node.propertyName ?? node.name;
|
|
48
|
+
return value !== undefined && (ts.isIdentifier(name) || ts.isStringLiteral(name)) ? member(value, name.text) : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function declaration(node: ts.Node): Origin {
|
|
52
|
+
if (ts.isImportSpecifier(node)) {
|
|
53
|
+
return imported((node.propertyName ?? node.name).text, importFrom(node));
|
|
54
|
+
}
|
|
55
|
+
if (ts.isNamespaceImport(node)) {
|
|
56
|
+
return moduleOrigin(importFrom(node));
|
|
57
|
+
}
|
|
58
|
+
if (ts.isVariableDeclaration(node) || ts.isPropertyAssignment(node)) {
|
|
59
|
+
return node.initializer === undefined ? undefined : origin(node.initializer);
|
|
60
|
+
}
|
|
61
|
+
return localBinding(node);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function localBinding(node: ts.Node): Origin {
|
|
65
|
+
if (ts.isShorthandPropertyAssignment(node)) {
|
|
66
|
+
return symbol(checker.getShorthandAssignmentValueSymbol(node));
|
|
67
|
+
}
|
|
68
|
+
return ts.isBindingElement(node) ? binding(node) : undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function symbol(value: ts.Symbol | undefined): Origin {
|
|
72
|
+
if (value === undefined) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const direct = value.declarations?.map(visit).find((result) => result !== undefined);
|
|
76
|
+
return direct ?? (value.flags & ts.SymbolFlags.Alias ? symbol(checker.getAliasedSymbol(value)) : undefined);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function object(node: ts.ObjectLiteralExpression): Origin {
|
|
80
|
+
for (const property of [...node.properties].reverse()) {
|
|
81
|
+
if (property.name?.getText() === "it") {
|
|
82
|
+
return visit(property) === "story" ? "kit" : undefined;
|
|
83
|
+
}
|
|
84
|
+
if (ts.isSpreadAssignment(property) && visit(property.expression) === "kit") {
|
|
85
|
+
return "kit";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function member(value: ts.Expression, name: string): Origin {
|
|
92
|
+
const receiver = visit(value);
|
|
93
|
+
if (receiver === "kit" && name === "it") {
|
|
94
|
+
return "story";
|
|
95
|
+
}
|
|
96
|
+
if (receiver === "story" || receiver === "plain" || receiver === "suite") {
|
|
97
|
+
return receiver;
|
|
98
|
+
}
|
|
99
|
+
const from = receiver === "vitest" ? "vitest" : undefined;
|
|
100
|
+
return imported(name, receiver === "stories" ? "@shivaedev/test-story/storyKit.ts" : from);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function identifier(node: ts.Identifier): Origin {
|
|
104
|
+
if (node.text === "describe" || node.text === "suite") {
|
|
105
|
+
return symbol(checker.getSymbolAtLocation(node)) ?? "suite";
|
|
106
|
+
}
|
|
107
|
+
const value = checker.getSymbolAtLocation(node);
|
|
108
|
+
return symbol(value) ?? (value === undefined && (node.text === "it" || node.text === "test") ? "plain" : undefined);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function origin(node: ts.Node): Origin {
|
|
112
|
+
if (ts.isFunctionLike(node)) {
|
|
113
|
+
return "function";
|
|
114
|
+
}
|
|
115
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
116
|
+
return object(node);
|
|
117
|
+
}
|
|
118
|
+
return expression(node);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function expression(node: ts.Node): Origin {
|
|
122
|
+
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node)) {
|
|
123
|
+
return visit(node.expression);
|
|
124
|
+
}
|
|
125
|
+
if (ts.isCallExpression(node)) {
|
|
126
|
+
const callee = visit(node.expression);
|
|
127
|
+
return callee === "factory" ? "kit" : callee;
|
|
128
|
+
}
|
|
129
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
130
|
+
return member(node.expression, node.name.text) ?? symbol(checker.getSymbolAtLocation(node.name));
|
|
131
|
+
}
|
|
132
|
+
return ts.isIdentifier(node) ? identifier(node) : declaration(node);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function visit(node: ts.Node): Origin {
|
|
136
|
+
if (visiting.has(node) || known.has(node)) {
|
|
137
|
+
return known.get(node);
|
|
138
|
+
}
|
|
139
|
+
visiting.add(node);
|
|
140
|
+
const result = origin(node);
|
|
141
|
+
visiting.delete(node);
|
|
142
|
+
known.set(node, result);
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { origin: visit, source: (path: string) => program.getSourceFile(join(inputs.root, path)) };
|
|
147
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import type { WorkspacePackage } from "#imports/workspace.ts";
|
|
3
|
+
import type { RuleInputs } from "#rule.ts";
|
|
4
|
+
|
|
5
|
+
const DEPENDENCIES = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
|
|
6
|
+
|
|
7
|
+
function object(value: unknown): value is Record<string, unknown> {
|
|
8
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function manifest(text: string | undefined): Record<string, unknown> {
|
|
12
|
+
const value: unknown = JSON.parse(text ?? "{}");
|
|
13
|
+
return object(value) ? value : {};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function storyPrerequisites(inputs: RuleInputs, packages: readonly WorkspacePackage[]): Promise<ReadonlySet<string>> {
|
|
17
|
+
const root = manifest(await inputs.readText("package.json"));
|
|
18
|
+
const members = typeof root.name === "string" ? [...packages, { directory: "", name: root.name }] : packages;
|
|
19
|
+
const byName = new Map(members.map((member) => [member.name, member]));
|
|
20
|
+
const pending = ["@shivaedev/test-story"];
|
|
21
|
+
const directories = new Set<string>();
|
|
22
|
+
while (pending.length > 0) {
|
|
23
|
+
const current = byName.get(pending.pop() ?? "");
|
|
24
|
+
if (current === undefined || directories.has(current.directory)) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
directories.add(current.directory);
|
|
28
|
+
const value = manifest(await inputs.readText(posix.join(current.directory, "package.json")));
|
|
29
|
+
for (const key of DEPENDENCIES) {
|
|
30
|
+
const dependencies = value[key];
|
|
31
|
+
if (object(dependencies)) {
|
|
32
|
+
pending.push(...Object.keys(dependencies).filter((name) => byName.has(name)));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return directories;
|
|
37
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import type { ImportGraph } from "#imports/graph.ts";
|
|
4
|
+
import type { RuleInputs } from "#rule.ts";
|
|
5
|
+
|
|
6
|
+
export function storyProgram(inputs: RuleInputs, graph: ImportGraph): ts.Program {
|
|
7
|
+
const sources = new Map(inputs.sources.map((file) => [join(inputs.root, file.path), file.text]));
|
|
8
|
+
const targets = new Map(
|
|
9
|
+
graph.edges.flatMap((edge) =>
|
|
10
|
+
edge.to.kind === "file" ? [[`${join(inputs.root, edge.from)}:${edge.specifier}`, join(inputs.root, edge.to.path)] as const] : [],
|
|
11
|
+
),
|
|
12
|
+
);
|
|
13
|
+
const options: ts.CompilerOptions = { allowJs: true, noLib: true, types: [] };
|
|
14
|
+
const host: ts.CompilerHost = {
|
|
15
|
+
...ts.createCompilerHost(options),
|
|
16
|
+
fileExists: (path) => sources.has(path),
|
|
17
|
+
getSourceFile: (path) => {
|
|
18
|
+
const text = sources.get(path);
|
|
19
|
+
return text === undefined ? undefined : ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true);
|
|
20
|
+
},
|
|
21
|
+
readFile: (path) => sources.get(path),
|
|
22
|
+
resolveModuleNames: (names, from) =>
|
|
23
|
+
names.map((name) => {
|
|
24
|
+
const path = targets.get(`${from}:${name}`);
|
|
25
|
+
return path === undefined ? undefined : { extension: ts.Extension.Ts, resolvedFileName: path };
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
return ts.createProgram([...sources.keys()], options, host);
|
|
29
|
+
}
|