@cplieger/deadset-ts 4.0.1 → 5.0.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.
Files changed (117) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/component-mapper.js +5 -0
  3. package/dist/bin/mapper-connection.js +101 -0
  4. package/dist/bin/node-host.js +24 -2
  5. package/dist/src/analysis.js +229 -36
  6. package/dist/src/component-files.js +612 -0
  7. package/dist/src/component-layer.js +132 -0
  8. package/dist/src/config.js +9 -2
  9. package/dist/src/configuration-files.js +186 -0
  10. package/dist/src/conformance-record.js +2 -2
  11. package/dist/src/convention-rows.js +383 -0
  12. package/dist/src/conventions.js +456 -0
  13. package/dist/src/dependencies.js +30 -3
  14. package/dist/src/discover.js +10 -3
  15. package/dist/src/emit-map.js +72 -0
  16. package/dist/src/entry-point-gaps.js +81 -35
  17. package/dist/src/entry-points.js +47 -32
  18. package/dist/src/exempt.js +11 -3
  19. package/dist/src/findings/completion.js +19 -29
  20. package/dist/src/findings/emitters.js +22 -1
  21. package/dist/src/findings/unused-declarations.js +7 -2
  22. package/dist/src/graph.js +13 -5
  23. package/dist/src/inventory.js +4 -2
  24. package/dist/src/kinds.js +2 -2
  25. package/dist/src/manifest.js +37 -11
  26. package/dist/src/matrix.js +21 -13
  27. package/dist/src/partial.js +3 -1
  28. package/dist/src/pnpm-workspace.js +120 -0
  29. package/dist/src/position.js +17 -2
  30. package/dist/src/print.js +2 -0
  31. package/dist/src/references.js +209 -30
  32. package/dist/src/report.js +26 -6
  33. package/dist/src/reporters/annotations.js +10 -3
  34. package/dist/src/reporters/template.js +3 -0
  35. package/dist/src/resolve.js +31 -6
  36. package/dist/src/roots.js +37 -8
  37. package/dist/src/run.js +13 -3
  38. package/dist/src/schema.js +2 -0
  39. package/dist/src/session.js +141 -34
  40. package/dist/src/setup-failure.js +23 -0
  41. package/dist/src/source-files.js +4 -61
  42. package/dist/src/sweep.js +4 -2
  43. package/dist/src/template-field.js +38 -6
  44. package/dist/src/test-support.js +59 -0
  45. package/dist/src/type-errors.js +252 -0
  46. package/dist/src/type-query-alias.js +92 -0
  47. package/dist/src/verbs/analyze.js +24 -4
  48. package/dist/src/verbs/explain.js +4 -1
  49. package/dist/src/verbs/print-projects.js +17 -56
  50. package/dist/src/verbs/print-retained.js +7 -2
  51. package/dist/src/verbs/print-roots.js +7 -2
  52. package/dist/src/version-range.js +255 -0
  53. package/dist/src/version.js +1 -1
  54. package/dist/src/workspace-manifests.js +68 -0
  55. package/dist/src/workspace-programs.js +342 -0
  56. package/dist/src/workspace-resolution.js +307 -0
  57. package/dist/src/workspace-run.js +75 -0
  58. package/dist/src/workspace.js +358 -0
  59. package/jsr.json +1 -1
  60. package/package.json +2 -1
  61. package/src/analysis.ts +394 -70
  62. package/src/component-files.ts +718 -0
  63. package/src/component-layer.ts +160 -0
  64. package/src/config.ts +11 -2
  65. package/src/configuration-files.ts +235 -0
  66. package/src/conformance-record.ts +2 -2
  67. package/src/convention-rows.ts +440 -0
  68. package/src/conventions.ts +640 -0
  69. package/src/dependencies.ts +36 -2
  70. package/src/discover.ts +10 -6
  71. package/src/emit-map.ts +93 -0
  72. package/src/entry-point-gaps.ts +92 -38
  73. package/src/entry-points.ts +66 -36
  74. package/src/exempt.ts +21 -3
  75. package/src/findings/completion.ts +24 -33
  76. package/src/findings/emitters.ts +32 -1
  77. package/src/findings/unused-declarations.ts +9 -2
  78. package/src/graph.ts +15 -4
  79. package/src/host.ts +23 -0
  80. package/src/index.ts +1 -1
  81. package/src/inventory.ts +13 -2
  82. package/src/kinds.ts +2 -2
  83. package/src/manifest.ts +47 -14
  84. package/src/matrix.ts +43 -15
  85. package/src/partial.ts +6 -1
  86. package/src/pnpm-workspace.ts +135 -0
  87. package/src/position.ts +23 -2
  88. package/src/print.ts +2 -0
  89. package/src/references.ts +227 -28
  90. package/src/report.ts +56 -6
  91. package/src/reporters/annotations.ts +12 -3
  92. package/src/reporters/template.ts +3 -0
  93. package/src/resolve.ts +59 -6
  94. package/src/roots.ts +60 -9
  95. package/src/run.ts +15 -5
  96. package/src/schema.ts +2 -0
  97. package/src/session.ts +218 -37
  98. package/src/setup-failure.ts +39 -0
  99. package/src/source-files.ts +4 -66
  100. package/src/sweep.ts +9 -2
  101. package/src/template-field.ts +42 -7
  102. package/src/test-support.ts +77 -0
  103. package/src/type-errors.ts +340 -0
  104. package/src/type-query-alias.ts +133 -0
  105. package/src/verbs/analyze.ts +38 -4
  106. package/src/verbs/explain.ts +4 -1
  107. package/src/verbs/print-projects.ts +17 -69
  108. package/src/verbs/print-retained.ts +7 -2
  109. package/src/verbs/print-roots.ts +7 -2
  110. package/src/verbs/verb.ts +6 -3
  111. package/src/version-range.ts +309 -0
  112. package/src/version.ts +1 -1
  113. package/src/workspace-manifests.ts +100 -0
  114. package/src/workspace-programs.ts +486 -0
  115. package/src/workspace-resolution.ts +393 -0
  116. package/src/workspace-run.ts +148 -0
  117. package/src/workspace.ts +463 -0
@@ -0,0 +1,160 @@
1
+ /**
2
+ * How a snapshot reads component files: a file-system layer that never reaches the disk gives
3
+ * every configuration the snapshot opens a `contentMappers` member naming this analyzer's own
4
+ * mapper, or none, in place of any the configuration declares, because a configuration's own
5
+ * member wins over one it extends. The mapper resolves as a package linked into the
6
+ * `node_modules` beside each configuration, to a directory holding only its manifest.
7
+ */
8
+
9
+ import type { CreateSnapshotParams } from "@typescript/native/unstable/sync";
10
+ import type { Host } from "./host.ts";
11
+ import { dirnamePath, joinPath } from "./paths.ts";
12
+
13
+ /** The name the mapper's manifest declares and every configuration's entry names. */
14
+ export const MAPPER_PACKAGE = "deadset-ts-component-files";
15
+
16
+ /** The file system one snapshot is created with. */
17
+ export type SnapshotFileSystem = NonNullable<CreateSnapshotParams["fileSystem"]>;
18
+
19
+ /** The manifest of the mapper's package, which runs `command`. */
20
+ export function mapperManifest(command: readonly string[]): string {
21
+ return `${JSON.stringify(
22
+ {
23
+ name: MAPPER_PACKAGE,
24
+ version: "0.0.0",
25
+ private: true,
26
+ typescript: { contentMapper: { exec: command } },
27
+ },
28
+ null,
29
+ 2,
30
+ )}\n`;
31
+ }
32
+
33
+ /** The offset past the white space and comments from `at`. */
34
+ function pastTrivia(text: string, from: number): number {
35
+ let at = from;
36
+ for (;;) {
37
+ if (/\s/u.test(text[at] ?? "x")) {
38
+ at += 1;
39
+ } else if (text.startsWith("//", at)) {
40
+ const end = text.slice(at).search(/[\n\r]/u);
41
+ at = end < 0 ? text.length : at + end;
42
+ } else if (text.startsWith("/*", at)) {
43
+ const end = text.indexOf("*/", at + 2);
44
+ at = end < 0 ? text.length : end + 2;
45
+ } else {
46
+ return at;
47
+ }
48
+ }
49
+ }
50
+
51
+ /** The offset past the string opening at `from`. */
52
+ function pastString(text: string, from: number): number {
53
+ let at = from + 1;
54
+ while (at < text.length && text[at] !== '"') {
55
+ at += text[at] === "\\" ? 2 : 1;
56
+ }
57
+ return at + 1;
58
+ }
59
+
60
+ /** The offset past the value starting at `from`: a string, a bracketed value, or a bare word. */
61
+ function pastValue(text: string, from: number): number {
62
+ const first = text[from];
63
+ if (first === '"') {
64
+ return pastString(text, from);
65
+ }
66
+ if (first !== "{" && first !== "[") {
67
+ let at = from;
68
+ while (at < text.length && /[^\s,}\]/]/u.test(text[at] ?? "")) {
69
+ at += 1;
70
+ }
71
+ return at;
72
+ }
73
+ let depth = 0;
74
+ let at = from;
75
+ while (at < text.length) {
76
+ at = pastTrivia(text, at);
77
+ const char = text[at];
78
+ if (char === '"') {
79
+ at = pastString(text, at);
80
+ continue;
81
+ }
82
+ if (char === "{" || char === "[") {
83
+ depth += 1;
84
+ } else if (char === "}" || char === "]") {
85
+ depth -= 1;
86
+ if (depth === 0) {
87
+ return at + 1;
88
+ }
89
+ }
90
+ at += 1;
91
+ }
92
+ return at;
93
+ }
94
+
95
+ /**
96
+ * One configuration's text with its top-level `contentMappers` member set to `mappers`,
97
+ * the member's own value replaced where it has one and the member written first
98
+ * otherwise. Every line keeps its number. Text that holds no object is returned as is,
99
+ * for the compiler to refuse.
100
+ */
101
+ export function withContentMappers(text: string, mappers: unknown): string {
102
+ const value = JSON.stringify(mappers);
103
+ const open = pastTrivia(text, 0);
104
+ if (text[open] !== "{") {
105
+ return text;
106
+ }
107
+ let at = pastTrivia(text, open + 1);
108
+ const empty = text[at] === "}";
109
+ while (at < text.length && text[at] === '"') {
110
+ const keyEnd = pastString(text, at);
111
+ const key = text.slice(at, keyEnd);
112
+ const valueStart = pastTrivia(text, pastTrivia(text, keyEnd) + 1);
113
+ const valueEnd = pastValue(text, valueStart);
114
+ if (key === '"contentMappers"') {
115
+ return text.slice(0, valueStart) + value + text.slice(valueEnd);
116
+ }
117
+ at = pastTrivia(text, valueEnd);
118
+ if (text[at] !== ",") {
119
+ break;
120
+ }
121
+ at = pastTrivia(text, at + 1);
122
+ }
123
+ return `${text.slice(0, open + 1)}"contentMappers":${value}${empty ? "" : ","}${text.slice(open + 1)}`;
124
+ }
125
+
126
+ /** This analyzer's mapper: the extensions it reads and the directory of its package. */
127
+ interface ComponentMapper {
128
+ readonly extensions: readonly string[];
129
+ readonly packageDirectory: string;
130
+ }
131
+
132
+ /**
133
+ * The layer one snapshot is created with: every configuration named carrying `mapper`, its
134
+ * package linked beside it, or no mapper at all where `mapper` is absent. A configuration
135
+ * `written` holds is that text, which exists nowhere else; any other is read through the host.
136
+ */
137
+ export function componentLayer(
138
+ host: Host,
139
+ configFiles: readonly string[],
140
+ mapper: ComponentMapper | undefined,
141
+ written: ReadonlyMap<string, string> = new Map(),
142
+ ): SnapshotFileSystem {
143
+ const mappers =
144
+ mapper === undefined ? [] : [{ package: MAPPER_PACKAGE, extensions: [...mapper.extensions] }];
145
+ const files: Record<string, string> = {};
146
+ const symlinks: Record<string, { target: string; host: boolean }> = {};
147
+ for (const configFile of configFiles) {
148
+ files[configFile] = withContentMappers(
149
+ written.get(configFile) ?? host.readFile(configFile),
150
+ mappers,
151
+ );
152
+ if (mapper !== undefined) {
153
+ symlinks[joinPath(dirnamePath(configFile), "node_modules", MAPPER_PACKAGE)] = {
154
+ target: mapper.packageDirectory,
155
+ host: true,
156
+ };
157
+ }
158
+ }
159
+ return { kind: "layer", files, symlinks };
160
+ }
package/src/config.ts CHANGED
@@ -162,6 +162,8 @@ export interface Providers {
162
162
  export interface TSSection {
163
163
  readonly testFiles: readonly string[];
164
164
  readonly entryFiles: readonly string[];
165
+ readonly componentExtensions: readonly string[];
166
+ readonly disabledConventions: readonly string[];
165
167
  readonly injectionRegistrations: readonly DeclarationEntry[];
166
168
  readonly lifecycleContracts: readonly LifecycleContract[];
167
169
  readonly serializers: readonly DeclarationEntry[];
@@ -202,7 +204,7 @@ export function defaultConfig(): Config {
202
204
  targetKind: "",
203
205
  analysis: {
204
206
  languages: [],
205
- minConfidence: "possible",
207
+ minConfidence: "probable",
206
208
  generatedFiles: "exclude",
207
209
  consumerTests: "test",
208
210
  configurations: [],
@@ -228,8 +230,15 @@ export function defaultConfig(): Config {
228
230
  ],
229
231
  },
230
232
  ts: {
231
- testFiles: ["**/*.test.{ts,tsx,mts,cts}"],
233
+ testFiles: [
234
+ "**/*.test.{ts,tsx,mts,cts}",
235
+ "**/*.spec.*",
236
+ "**/__tests__/**",
237
+ "**/__mocks__/**",
238
+ ],
232
239
  entryFiles: [],
240
+ componentExtensions: [".vue", ".svelte", ".astro"],
241
+ disabledConventions: [],
233
242
  injectionRegistrations: [],
234
243
  lifecycleContracts: [],
235
244
  serializers: [],
@@ -0,0 +1,235 @@
1
+ /**
2
+ * The configuration files a tool loads by name, and the strings they hold. A file in a
3
+ * directory that holds a `package.json` is a configuration module when its name is
4
+ * `<stem>.config.<ext>`, `<stem>.<qualifier>.config.<ext>` or `.<stem>rc.<ext>`, and a
5
+ * JSON configuration file when its name is the JSON form of one of them. A tool reads
6
+ * every string either holds as it likes, so each one that spells a declared dependency
7
+ * uses it and each relative one that names a source file of the program roots the file.
8
+ */
9
+
10
+ import {
11
+ isNoSubstitutionTemplateLiteral,
12
+ isStringLiteral,
13
+ type Node,
14
+ type SourceFile,
15
+ } from "@typescript/native/unstable/ast";
16
+ import type { Host } from "./host.ts";
17
+ import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
18
+ import type { SourceFiles } from "./source-files.ts";
19
+
20
+ const MANIFEST = "package.json";
21
+
22
+ /** A configuration module's name, each form with the spelling a root reports it by. */
23
+ const MODULE_FORMS: readonly { readonly name: RegExp; readonly form: string }[] = [
24
+ { name: /^[\w-]+\.config\.(?:js|mjs|cjs|ts|mts|cts)$/u, form: "<stem>.config.<ext>" },
25
+ {
26
+ name: /^[\w-]+\.[\w-]+\.config\.(?:js|mjs|cjs|ts|mts|cts)$/u,
27
+ form: "<stem>.<qualifier>.config.<ext>",
28
+ },
29
+ { name: /^\.[\w-]+rc\.(?:js|mjs|cjs)$/u, form: ".<stem>rc.<ext>" },
30
+ ];
31
+
32
+ /** A JSON configuration file's name. */
33
+ const DOCUMENT_NAME = /^(?:[\w-]+(?:\.[\w-]+)?\.config\.json|\.[\w-]+rc(?:\.json)?)$/u;
34
+
35
+ /** The manifest members a dependency is declared in. */
36
+ const DEPENDENCY_MEMBERS = [
37
+ "dependencies",
38
+ "devDependencies",
39
+ "peerDependencies",
40
+ "optionalDependencies",
41
+ ] as const;
42
+
43
+ /** The prefixes a string names a file with, relative to the file that writes it. */
44
+ const RELATIVE_PREFIXES = ["./", "../"] as const;
45
+
46
+ /** The extensions the program's module resolution appends to a specifier written without one. */
47
+ const RESOLVED_EXTENSIONS = [
48
+ ".ts",
49
+ ".tsx",
50
+ ".d.ts",
51
+ ".mts",
52
+ ".cts",
53
+ ".js",
54
+ ".jsx",
55
+ ".mjs",
56
+ ".cjs",
57
+ ] as const;
58
+
59
+ /** The file's name, the last segment of its path. */
60
+ function basename(path: string): string {
61
+ return path.slice(path.lastIndexOf("/") + 1);
62
+ }
63
+
64
+ function isRecord(value: unknown): value is Record<string, unknown> {
65
+ return typeof value === "object" && value !== null && !Array.isArray(value);
66
+ }
67
+
68
+ /** The form one file name is as a configuration module, or undefined where it is none. */
69
+ export function configurationModuleForm(name: string): string | undefined {
70
+ return MODULE_FORMS.find((one) => one.name.test(name))?.form;
71
+ }
72
+
73
+ /** Whether one file name is a JSON configuration file's. */
74
+ export function isConfigurationDocumentName(name: string): boolean {
75
+ return DOCUMENT_NAME.test(name);
76
+ }
77
+
78
+ /** One JSON configuration file, with every string value it holds. */
79
+ interface ConfigurationDocument {
80
+ /** The file, absolute. */
81
+ readonly path: string;
82
+ /** Every string value at any depth, in document order; a member's name is no value. */
83
+ readonly strings: readonly string[];
84
+ }
85
+
86
+ /** The configuration files one project holds and sits beside. */
87
+ interface ConfigurationFiles {
88
+ /** Every own file that is a configuration module, with the form its name has. */
89
+ readonly modules: readonly { readonly file: SourceFile; readonly form: string }[];
90
+ /** Every JSON configuration file in a directory of the project's files or above one. */
91
+ readonly documents: readonly ConfigurationDocument[];
92
+ }
93
+
94
+ /** Every string value one decoded JSON value holds at any depth. */
95
+ export function documentStrings(value: unknown): string[] {
96
+ if (typeof value === "string") {
97
+ return [value];
98
+ }
99
+ if (Array.isArray(value)) {
100
+ return value.flatMap(documentStrings);
101
+ }
102
+ return isRecord(value) ? Object.values(value).flatMap(documentStrings) : [];
103
+ }
104
+
105
+ /** Every string literal and every template literal with no substitution one file writes. */
106
+ export function moduleStrings(
107
+ file: SourceFile,
108
+ ): readonly { readonly node: Node; readonly text: string }[] {
109
+ const found: { node: Node; text: string }[] = [];
110
+ const visit = (node: Node): void => {
111
+ if (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node)) {
112
+ found.push({ node, text: node.text });
113
+ }
114
+ node.forEachChild(visit);
115
+ };
116
+ file.forEachChild(visit);
117
+ return found;
118
+ }
119
+
120
+ /**
121
+ * The configuration files of one project's own files below the target root. A module
122
+ * is an own file; a JSON configuration file is read from each directory below the
123
+ * target root, the root included, that holds an own file or is above one. Either sits
124
+ * in a directory that holds a manifest. A document that cannot be read or parsed as
125
+ * JSON holds no string.
126
+ */
127
+ export function configurationFiles(
128
+ host: Host,
129
+ targetRoot: string,
130
+ files: SourceFiles,
131
+ ): ConfigurationFiles {
132
+ const root = resolvePath(host.workingDirectory(), targetRoot);
133
+ const manifests = new Map<string, boolean>();
134
+ const holdsManifest = (dir: string): boolean => {
135
+ let held = manifests.get(dir);
136
+ if (held === undefined) {
137
+ held = host.kindOf(joinPath(dir, MANIFEST)) === "file";
138
+ manifests.set(dir, held);
139
+ }
140
+ return held;
141
+ };
142
+
143
+ const modules: { file: SourceFile; form: string }[] = [];
144
+ const dirs = new Set<string>([root]);
145
+ for (const file of files.byPath.values()) {
146
+ const form = configurationModuleForm(basename(file.fileName));
147
+ if (form !== undefined && holdsManifest(dirnamePath(file.fileName))) {
148
+ modules.push({ file, form });
149
+ }
150
+ for (
151
+ let dir = dirnamePath(file.fileName);
152
+ relativePath(root, dir) !== undefined && !dirs.has(dir);
153
+ dir = dirnamePath(dir)
154
+ ) {
155
+ dirs.add(dir);
156
+ }
157
+ }
158
+
159
+ const documents: ConfigurationDocument[] = [];
160
+ for (const dir of [...dirs].sort()) {
161
+ if (!holdsManifest(dir)) {
162
+ continue;
163
+ }
164
+ let names: readonly string[];
165
+ try {
166
+ names = host
167
+ .readDirectory(dir)
168
+ .filter((entry) => !entry.directory && isConfigurationDocumentName(entry.name))
169
+ .map((entry) => entry.name)
170
+ .sort();
171
+ } catch {
172
+ continue;
173
+ }
174
+ for (const name of names) {
175
+ const path = joinPath(dir, name);
176
+ let value: unknown;
177
+ try {
178
+ value = JSON.parse(host.readFile(path));
179
+ } catch {
180
+ continue;
181
+ }
182
+ documents.push({ path, strings: documentStrings(value) });
183
+ }
184
+ }
185
+ return { modules, documents };
186
+ }
187
+
188
+ /** Every dependency the manifest in one directory declares, or none where it holds none to read. */
189
+ export function declaredIn(host: Host, dir: string): readonly string[] {
190
+ let value: unknown;
191
+ try {
192
+ value = JSON.parse(host.readFile(joinPath(dir, MANIFEST)));
193
+ } catch {
194
+ return [];
195
+ }
196
+ if (!isRecord(value)) {
197
+ return [];
198
+ }
199
+ const names = new Set<string>();
200
+ for (const member of DEPENDENCY_MEMBERS) {
201
+ const section = value[member];
202
+ if (isRecord(section)) {
203
+ Object.keys(section).forEach((name) => names.add(name));
204
+ }
205
+ }
206
+ return [...names];
207
+ }
208
+
209
+ /** The dependencies one string uses: each it equals, or is followed by `/` and a subpath in. */
210
+ export function dependenciesNamed(text: string, declared: readonly string[]): readonly string[] {
211
+ return declared.filter((name) => text === name || text.startsWith(`${name}/`));
212
+ }
213
+
214
+ /**
215
+ * The own files one string names read against a directory: none unless it starts with
216
+ * `./` or `../`, else the file it names directly or through the emit mapping, or the
217
+ * file it names with an extension the module resolution appends.
218
+ */
219
+ export function filesNamedBy(files: SourceFiles, dir: string, text: string): readonly SourceFile[] {
220
+ if (!RELATIVE_PREFIXES.some((prefix) => text.startsWith(prefix)) || text.includes("*")) {
221
+ return [];
222
+ }
223
+ const path = resolvePath(dir, text);
224
+ const direct = files.named(path);
225
+ if (direct.length > 0) {
226
+ return direct;
227
+ }
228
+ for (const extension of RESOLVED_EXTENSIONS) {
229
+ const held = files.byName.get(path + extension);
230
+ if (held !== undefined) {
231
+ return [held];
232
+ }
233
+ }
234
+ return [];
235
+ }
@@ -2,9 +2,9 @@
2
2
  // Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
3
3
  export const RECORD = {
4
4
  conformance: {
5
- corpusVersion: "2.0.0",
5
+ corpusVersion: "2.1.1",
6
6
  result: "pass",
7
- digest: "sha256:1274315595d7b944d22eb0126081e24cf293c9fe53a236f4d263c714644f32ee",
7
+ digest: "sha256:e5e456bf3a706f1fa9f906ae71e10ea98a78da52d7d99a5ed59dca8a72bbe927",
8
8
  },
9
9
  gaps: [
10
10
  {