@cplieger/deadset-ts 7.2.0-dev.3 → 8.0.0-dev.1

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 (153) hide show
  1. package/dist/bin/mapper-connection.js +7 -4
  2. package/dist/src/analysis.js +116 -16
  3. package/dist/src/augmentations.js +21 -8
  4. package/dist/src/automatic-types.js +189 -0
  5. package/dist/src/base-url.js +18 -0
  6. package/dist/src/component-files.js +96 -31
  7. package/dist/src/components.js +7 -6
  8. package/dist/src/config.js +1 -0
  9. package/dist/src/configuration-files.js +54 -15
  10. package/dist/src/configured-declarations.js +24 -2
  11. package/dist/src/conformance-record.js +2 -2
  12. package/dist/src/contextual-uses.js +26 -26
  13. package/dist/src/convention-rows.js +75 -0
  14. package/dist/src/conventions.js +101 -5
  15. package/dist/src/decorator.js +21 -18
  16. package/dist/src/dependencies.js +185 -30
  17. package/dist/src/destructuring.js +77 -2
  18. package/dist/src/discover.js +188 -85
  19. package/dist/src/edges.js +1 -1
  20. package/dist/src/entry-point-gaps.js +7 -10
  21. package/dist/src/entry-points.js +112 -11
  22. package/dist/src/findings/completion.js +116 -21
  23. package/dist/src/findings/dependencies-and-module-machinery.js +71 -25
  24. package/dist/src/findings/emitters.js +1 -1
  25. package/dist/src/findings/intra-function.js +44 -13
  26. package/dist/src/findings/non-code-artifacts.js +66 -33
  27. package/dist/src/findings/unused-declarations.js +21 -1
  28. package/dist/src/findings/visibility-narrowing.js +22 -3
  29. package/dist/src/generated-file.js +11 -0
  30. package/dist/src/git-ignore.js +86 -0
  31. package/dist/src/graph.js +4 -1
  32. package/dist/src/import-bindings.js +97 -0
  33. package/dist/src/indexed-access.js +130 -28
  34. package/dist/src/interface-satisfaction.js +30 -12
  35. package/dist/src/intra-function-parts.js +313 -50
  36. package/dist/src/inventory.js +63 -8
  37. package/dist/src/json-document.js +5 -5
  38. package/dist/src/kinds.js +13 -13
  39. package/dist/src/matrix.js +18 -9
  40. package/dist/src/mdx-entries.js +197 -0
  41. package/dist/src/module-declarations.js +9 -13
  42. package/dist/src/namespace-objects.js +28 -18
  43. package/dist/src/overrides.js +45 -9
  44. package/dist/src/paths.js +9 -0
  45. package/dist/src/print.js +1 -0
  46. package/dist/src/ref.js +10 -0
  47. package/dist/src/references.js +29 -13
  48. package/dist/src/reflective-lookup.js +8 -6
  49. package/dist/src/report.js +4 -2
  50. package/dist/src/reporters/template.js +22 -13
  51. package/dist/src/resolve.js +2 -0
  52. package/dist/src/roots.js +110 -38
  53. package/dist/src/schema.js +1 -0
  54. package/dist/src/serialization-contract.js +97 -20
  55. package/dist/src/session.js +26 -1
  56. package/dist/src/source-files.js +39 -14
  57. package/dist/src/spelled-names.js +58 -0
  58. package/dist/src/suppress-file.js +12 -4
  59. package/dist/src/suppress-inline.js +21 -3
  60. package/dist/src/suppress.js +7 -5
  61. package/dist/src/sweep.js +27 -10
  62. package/dist/src/symbol-keys.js +103 -2
  63. package/dist/src/template-field.js +9 -6
  64. package/dist/src/test-support.js +121 -17
  65. package/dist/src/type-errors.js +116 -35
  66. package/dist/src/type-parameter-reads.js +172 -0
  67. package/dist/src/value-reach.js +17 -5
  68. package/dist/src/verbs/analyze.js +19 -1
  69. package/dist/src/verbs/explain.js +4 -2
  70. package/dist/src/version-range.js +10 -5
  71. package/dist/src/version.js +1 -1
  72. package/dist/src/workflow-steps.js +119 -22
  73. package/dist/src/workspace-programs.js +7 -3
  74. package/dist/src/workspace-resolution.js +72 -13
  75. package/dist/src/workspace.js +1 -1
  76. package/jsr.json +1 -1
  77. package/package.json +2 -2
  78. package/src/analysis.ts +175 -16
  79. package/src/augmentations.ts +21 -10
  80. package/src/automatic-types.ts +246 -0
  81. package/src/base-url.ts +24 -0
  82. package/src/component-files.ts +116 -31
  83. package/src/components.ts +7 -6
  84. package/src/config.ts +3 -0
  85. package/src/configuration-files.ts +80 -18
  86. package/src/configured-declarations.ts +33 -4
  87. package/src/conformance-record.ts +2 -2
  88. package/src/contextual-uses.ts +27 -27
  89. package/src/convention-rows.ts +103 -0
  90. package/src/conventions.ts +121 -7
  91. package/src/decorator.ts +28 -18
  92. package/src/dependencies.ts +249 -43
  93. package/src/destructuring.ts +93 -2
  94. package/src/discover.ts +234 -86
  95. package/src/edges.ts +1 -1
  96. package/src/entry-point-gaps.ts +8 -11
  97. package/src/entry-points.ts +133 -11
  98. package/src/finding.ts +10 -0
  99. package/src/findings/completion.ts +147 -24
  100. package/src/findings/dependencies-and-module-machinery.ts +90 -28
  101. package/src/findings/emitters.ts +1 -1
  102. package/src/findings/intra-function.ts +56 -14
  103. package/src/findings/non-code-artifacts.ts +81 -41
  104. package/src/findings/unused-declarations.ts +25 -1
  105. package/src/findings/visibility-narrowing.ts +32 -3
  106. package/src/generated-file.ts +16 -0
  107. package/src/git-ignore.ts +98 -0
  108. package/src/graph.ts +6 -0
  109. package/src/host.ts +5 -9
  110. package/src/import-bindings.ts +128 -0
  111. package/src/indexed-access.ts +152 -30
  112. package/src/interface-satisfaction.ts +33 -13
  113. package/src/intra-function-parts.ts +415 -64
  114. package/src/inventory.ts +83 -7
  115. package/src/json-document.ts +7 -7
  116. package/src/kinds.ts +13 -13
  117. package/src/matrix.ts +32 -14
  118. package/src/mdx-entries.ts +241 -0
  119. package/src/module-declarations.ts +11 -13
  120. package/src/namespace-objects.ts +34 -21
  121. package/src/overrides.ts +56 -9
  122. package/src/paths.ts +10 -0
  123. package/src/print.ts +1 -0
  124. package/src/ref.ts +12 -0
  125. package/src/references.ts +38 -17
  126. package/src/reflective-lookup.ts +9 -7
  127. package/src/report.ts +16 -3
  128. package/src/reporters/template.ts +21 -13
  129. package/src/resolve.ts +3 -0
  130. package/src/roots.ts +163 -45
  131. package/src/schema.ts +1 -0
  132. package/src/serialization-contract.ts +108 -17
  133. package/src/session.ts +29 -1
  134. package/src/source-files.ts +66 -16
  135. package/src/spelled-names.ts +102 -0
  136. package/src/suppress-file.ts +14 -3
  137. package/src/suppress-inline.ts +21 -2
  138. package/src/suppress.ts +9 -5
  139. package/src/sweep.ts +32 -11
  140. package/src/symbol-keys.ts +129 -2
  141. package/src/template-field.ts +9 -6
  142. package/src/test-support.ts +149 -16
  143. package/src/type-errors.ts +132 -41
  144. package/src/type-parameter-reads.ts +227 -0
  145. package/src/value-reach.ts +19 -6
  146. package/src/verbs/analyze.ts +28 -1
  147. package/src/verbs/explain.ts +5 -2
  148. package/src/version-range.ts +10 -5
  149. package/src/version.ts +1 -1
  150. package/src/workflow-steps.ts +127 -22
  151. package/src/workspace-programs.ts +7 -3
  152. package/src/workspace-resolution.ts +86 -14
  153. package/src/workspace.ts +1 -1
@@ -66,10 +66,12 @@ function partialOf(text: string): Partial | undefined {
66
66
  parts.push(Number(part));
67
67
  }
68
68
  const prerelease = prereleaseOf(match[4]);
69
- if (prerelease === undefined || (prerelease.length > 0 && wild)) {
69
+ if (prerelease === undefined) {
70
70
  return undefined;
71
71
  }
72
- return { major: parts[0], minor: parts[1], patch: parts[2], prerelease };
72
+ // node-semver drops the prerelease of a version with a wildcard part (`1.2.x-beta` is `1.2.x`):
73
+ // https://github.com/npm/node-semver/blob/v7.8.5/classes/range.js#L409-L430
74
+ return { major: parts[0], minor: parts[1], patch: parts[2], prerelease: wild ? [] : prerelease };
73
75
  }
74
76
 
75
77
  /** One exact version, or `undefined` where the text is not one. */
@@ -152,11 +154,14 @@ function passes(version: Version, comparator: Comparator): boolean {
152
154
  }
153
155
  }
154
156
 
155
- /** The comparators a version with missing parts stands for: any value of each. */
157
+ /**
158
+ * The comparators a version with missing parts stands for: any value of each. A wildcard
159
+ * major is no comparator, so it never refuses a 0.0.0 prerelease another comparator admits.
160
+ */
156
161
  function anyOf(partial: Partial): Comparator[] {
157
162
  const { major, minor } = partial;
158
163
  if (major === undefined) {
159
- return [{ operator: ">=", version: exact(0, 0, 0) }];
164
+ return [];
160
165
  }
161
166
  if (minor === undefined) {
162
167
  return [
@@ -210,7 +215,7 @@ function primitive(operator: "<" | "<=" | ">" | ">=", partial: Partial): Compara
210
215
  if (major === undefined) {
211
216
  return operator === "<" || operator === ">"
212
217
  ? [{ operator: "<", version: floorOf(0, 0, 0) }]
213
- : [{ operator: ">=", version: exact(0, 0, 0) }];
218
+ : [];
214
219
  }
215
220
  if (minor !== undefined && patch !== undefined) {
216
221
  return [{ operator, version: lowest(partial) }];
package/src/version.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * independently of the analyzer's own version, which is the one the host answers
5
5
  * from the package the analyzer was installed from.
6
6
  */
7
- export const CONTRACT_VERSION = "7.2.0";
7
+ export const CONTRACT_VERSION = "8.0.0";
@@ -1,12 +1,13 @@
1
1
  /**
2
- * The source files a CI workflow's `run:` steps name by their path from the target root.
3
- * A workflow file is read as text, line by line, with no YAML parsed.
2
+ * The source files a CI workflow's `run:` steps and the repository's git hooks name by their
3
+ * path from the target root. A workflow or hook file is read as text, line by line, with no
4
+ * YAML parsed.
4
5
  */
5
6
 
6
7
  import type { SourceFile } from "@typescript/native/ast";
7
8
  import type { Host } from "./host.ts";
8
9
  import { scriptTokens } from "./manifest.ts";
9
- import { joinPath, resolvePath } from "./paths.ts";
10
+ import { joinPath, relativePath, resolvePath } from "./paths.ts";
10
11
  import type { SourceFiles } from "./source-files.ts";
11
12
 
12
13
  /** One file a workflow step names. */
@@ -21,6 +22,23 @@ const WORKFLOWS = ".github/workflows";
21
22
  /** A line holding a run command: indentation and one sequence marker before `run:`. */
22
23
  const RUN_LINE = /^(\s*(?:- )?)run:(.*)$/u;
23
24
 
25
+ /** The directories whose every file is a git hook script, run as the shell runs it. */
26
+ const HOOK_DIRS = [".husky", ".githooks"] as const;
27
+
28
+ /** The hook manager files whose commands are written under a key, with that key. */
29
+ const HOOK_MANAGERS: readonly (readonly [string, RegExp])[] = [
30
+ [".pre-commit-config.yaml", /^(\s*(?:- )?)entry:(.*)$/u],
31
+ ["lefthook.yml", RUN_LINE],
32
+ ["lefthook.yaml", RUN_LINE],
33
+ [".lefthook.yml", RUN_LINE],
34
+ ];
35
+
36
+ /** The names GNU make reads a makefile under, in the order it tries them. */
37
+ const MAKEFILES = ["GNUmakefile", "makefile", "Makefile"] as const;
38
+
39
+ /** The operators that end one shell command of a recipe line. */
40
+ const COMMAND_SEPARATORS = /&&|\|\||;|\|/u;
41
+
24
42
  const BLOCK_SCALAR = /^[|>][+-]?$/u;
25
43
 
26
44
  const QUOTED = /^(["'])(.*)\1$/su;
@@ -30,7 +48,8 @@ const COMPUTED = /[${*?[]/u;
30
48
 
31
49
  const INDENT = /^\s*/u;
32
50
 
33
- function workflowFiles(host: Host, dir: string): readonly string[] {
51
+ /** The files directly in one directory whose name `accepts`, in name order. */
52
+ function filesIn(host: Host, dir: string, accepts: (name: string) => boolean): readonly string[] {
34
53
  let entries;
35
54
  try {
36
55
  entries = host.readDirectory(dir);
@@ -38,17 +57,25 @@ function workflowFiles(host: Host, dir: string): readonly string[] {
38
57
  return [];
39
58
  }
40
59
  return entries
41
- .filter((entry) => !entry.directory && /\.ya?ml$/u.test(entry.name))
60
+ .filter((entry) => !entry.directory && accepts(entry.name))
42
61
  .map((entry) => joinPath(dir, entry.name))
43
62
  .sort();
44
63
  }
45
64
 
46
- /** Every command line one workflow file's `run:` steps hold. */
47
- export function runCommands(text: string): readonly string[] {
65
+ function readText(host: Host, path: string): string | undefined {
66
+ try {
67
+ return host.readFile(path);
68
+ } catch {
69
+ return undefined;
70
+ }
71
+ }
72
+
73
+ /** Every command line one workflow file's `run:` steps, or the steps under `key`, hold. */
74
+ export function runCommands(text: string, key: RegExp = RUN_LINE): readonly string[] {
48
75
  const lines = text.split(/\r?\n/u);
49
76
  const found: string[] = [];
50
77
  for (let at = 0; at < lines.length; at += 1) {
51
- const match = RUN_LINE.exec(lines[at] ?? "");
78
+ const match = key.exec(lines[at] ?? "");
52
79
  if (match === null) {
53
80
  continue;
54
81
  }
@@ -69,28 +96,95 @@ export function runCommands(text: string): readonly string[] {
69
96
  return found;
70
97
  }
71
98
 
72
- /** Every token the `run:` steps of the workflow files in the target root's `.github/workflows` hold. */
73
- export function workflowTokens(host: Host, targetRoot: string): readonly string[] {
74
- const dir = joinPath(resolvePath(host.workingDirectory(), targetRoot), WORKFLOWS);
75
- const found: string[] = [];
76
- for (const path of workflowFiles(host, dir)) {
77
- let text: string;
78
- try {
79
- text = host.readFile(path);
80
- } catch {
99
+ /**
100
+ * Every token the recipe lines of the target root's makefile hold, each with the directory
101
+ * below the root it is read against: a recipe line is one that starts with a tab, joined
102
+ * with the lines a trailing `\` continues it onto, and make runs each in a shell of its
103
+ * own, so a `cd` moves the directory of the later commands of its line alone.
104
+ */
105
+ function recipeTokens(host: Host, root: string): readonly { token: string; dir: string }[] {
106
+ const path = MAKEFILES.map((name) => joinPath(root, name)).find(
107
+ (candidate) => host.kindOf(candidate) === "file",
108
+ );
109
+ const text = path === undefined ? undefined : readText(host, path);
110
+ if (text === undefined) {
111
+ return [];
112
+ }
113
+ const found: { token: string; dir: string }[] = [];
114
+ for (const line of text.replace(/\\\r?\n/gu, " ").split(/\r?\n/u)) {
115
+ if (!line.startsWith("\t")) {
81
116
  continue;
82
117
  }
83
- for (const command of runCommands(text)) {
84
- found.push(...scriptTokens(command));
118
+ let dir = root;
119
+ for (const command of withoutComment(line.slice(1).replace(/^[@+-]+/u, "")).split(
120
+ COMMAND_SEPARATORS,
121
+ )) {
122
+ const tokens = scriptTokens(command);
123
+ const [first, target, ...rest] = tokens;
124
+ if (first === "cd" && target !== undefined && rest.length === 0 && !COMPUTED.test(target)) {
125
+ dir = resolvePath(dir, target);
126
+ continue;
127
+ }
128
+ for (const token of tokens) {
129
+ found.push({ token, dir: relativePath(root, dir) ?? "" });
130
+ }
85
131
  }
86
132
  }
87
133
  return found;
88
134
  }
89
135
 
136
+ /** One shell line up to its comment: a `#` that starts a word outside quotes. */
137
+ function withoutComment(line: string): string {
138
+ let quote = "";
139
+ for (let at = 0; at < line.length; at += 1) {
140
+ const char = line[at];
141
+ if (quote !== "") {
142
+ if (char === quote) {
143
+ quote = "";
144
+ } else if (char === "\\" && quote === '"') {
145
+ at += 1;
146
+ }
147
+ } else if (char === "'" || char === '"') {
148
+ quote = char;
149
+ } else if (char === "\\") {
150
+ at += 1;
151
+ } else if (char === "#" && (at === 0 || /\s/u.test(line[at - 1] ?? ""))) {
152
+ return line.slice(0, at);
153
+ }
154
+ }
155
+ return line;
156
+ }
157
+
90
158
  /**
91
- * The own files the `run:` steps of every workflow file in the target root's
92
- * `.github/workflows` name: a token equal to a file's path below the target root, with or
93
- * without a leading `./`, whatever directory the step runs in.
159
+ * Every token the target root's workflow steps, git hooks and makefile hold: the `run:`
160
+ * steps of the workflow files in `.github/workflows`, every line of each file in `.husky`
161
+ * and `.githooks`, the `entry:` commands of `.pre-commit-config.yaml`, the `run:` commands
162
+ * of a lefthook file and the makefile's recipe lines.
163
+ */
164
+ export function workflowTokens(host: Host, targetRoot: string): readonly string[] {
165
+ const root = resolvePath(host.workingDirectory(), targetRoot);
166
+ const commands: string[] = [];
167
+ for (const path of filesIn(host, joinPath(root, WORKFLOWS), (name) => /\.ya?ml$/u.test(name))) {
168
+ commands.push(...runCommands(readText(host, path) ?? ""));
169
+ }
170
+ for (const dir of HOOK_DIRS) {
171
+ for (const path of filesIn(host, joinPath(root, dir), () => true)) {
172
+ commands.push(...(readText(host, path) ?? "").split(/\r?\n/u).map(withoutComment));
173
+ }
174
+ }
175
+ for (const [name, key] of HOOK_MANAGERS) {
176
+ commands.push(...runCommands(readText(host, joinPath(root, name)) ?? "", key));
177
+ }
178
+ return [
179
+ ...commands.flatMap((command) => scriptTokens(command)),
180
+ ...recipeTokens(host, root).map(({ token }) => token),
181
+ ];
182
+ }
183
+
184
+ /**
185
+ * The own files the target root's workflow steps and git hooks name, as
186
+ * {@link workflowTokens} reads them: a token equal to a file's path below the target root,
187
+ * with or without a leading `./`, whatever directory the step runs in.
94
188
  */
95
189
  export function workflowEntries(
96
190
  host: Host,
@@ -107,5 +201,16 @@ export function workflowEntries(
107
201
  found.push({ file, source: token });
108
202
  }
109
203
  }
204
+ const root = resolvePath(host.workingDirectory(), targetRoot);
205
+ for (const { token, dir } of recipeTokens(host, root)) {
206
+ const below =
207
+ COMPUTED.test(token) || dir === ""
208
+ ? undefined
209
+ : relativePath(root, resolvePath(joinPath(root, dir), token));
210
+ const file = below === undefined ? undefined : files.byPath.get(below);
211
+ if (file !== undefined) {
212
+ found.push({ file, source: token });
213
+ }
214
+ }
110
215
  return found;
111
216
  }
@@ -16,6 +16,7 @@ import type {
16
16
  } from "@typescript/native/sync";
17
17
  import { isStringLiteralLikeNode } from "@typescript/native/ast";
18
18
  import type { StringLiteralLikeNode } from "@typescript/native/ast";
19
+ import { baseUrlPaths } from "./base-url.ts";
19
20
  import type { Host } from "./host.ts";
20
21
  import { dirnamePath, resolvePath } from "./paths.ts";
21
22
  import type { Engine } from "./session.ts";
@@ -62,7 +63,8 @@ const BATCH = 2048;
62
63
 
63
64
  /**
64
65
  * The options a configuration's program is built with: its own, less the ones that
65
- * lay out an emitted program's inputs and outputs. The analysis emits nothing, and a
66
+ * lay out an emitted program's inputs and outputs, and with `paths` read against the
67
+ * effective `baseUrl` as {@link baseUrlPaths} states. The analysis emits nothing, and a
66
68
  * member's source is outside the importer's root directory by construction, which
67
69
  * the compiler refuses under such a layout.
68
70
  */
@@ -75,7 +77,9 @@ function analysisOptions(parsed: Parsed): CreateSnapshotProgramParams["compilerO
75
77
  tsBuildInfoFile: _tsBuildInfoFile,
76
78
  ...kept
77
79
  } = parsed.options;
78
- return composite === true ? { ...kept, declaration: true } : kept;
80
+ const paths = baseUrlPaths(parsed.options);
81
+ const options = paths === undefined ? kept : { ...kept, paths };
82
+ return composite === true ? { ...options, declaration: true } : options;
79
83
  }
80
84
 
81
85
  /** The conditions an `exports` lookup under one configuration and one resolution mode reads. */
@@ -152,7 +156,7 @@ function configurationOf(
152
156
  const raw: unknown = (options as Record<string, unknown>)["pathsBasePath"];
153
157
  const base = typeof raw === "string" ? raw : dirnamePath(reading.configFile);
154
158
  const paths: Record<string, string[]> = {};
155
- for (const [key, targets] of Object.entries(options.paths ?? {})) {
159
+ for (const [key, targets] of Object.entries(baseUrlPaths(options) ?? options.paths ?? {})) {
156
160
  paths[key] = targets.map((target) => resolvePath(base, target));
157
161
  }
158
162
  for (const entry of reading.entries) {
@@ -1,13 +1,13 @@
1
1
  /**
2
- * Which file an import of a workspace package's name is read from. The package is the
3
- * one `node_modules` links the importer to or, with no link, the one the importer's
4
- * manifest depends on. A source file the compiler reaches stands; any other file is read
5
- * back to the source it is compiled from or to a manifest target that is source, a file
6
- * the package's configurations neither compile nor write is read as installed, and an
7
- * import nothing answers for is unresolved.
2
+ * Which file an import of a workspace package's name is read from: the package
3
+ * `node_modules` links the importer to or, with no link, the one its manifest depends on.
4
+ * A source file the compiler reaches stands; any other file reads back to its source or to
5
+ * a manifest target that is source, an ignored file to the source its path mirrors, a file
6
+ * the package neither compiles nor writes is read as installed, and the rest is unresolved.
8
7
  */
9
8
 
10
9
  import { emittedFrom } from "./emit-map.ts";
10
+ import { gitIgnored } from "./git-ignore.ts";
11
11
  import type { Host } from "./host.ts";
12
12
  import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
13
13
  import { satisfies } from "./version-range.ts";
@@ -45,6 +45,7 @@ const DEPENDENCY_MEMBERS = [
45
45
  ] as const;
46
46
 
47
47
  const SOURCE = /(?<!\.d)\.(?:[mc]?ts|tsx)$/u;
48
+ const OUTPUT = /(?:\.d\.[mc]?ts|\.[mc]?js|\.jsx)$/u;
48
49
 
49
50
  function isRecord(value: unknown): value is Record<string, unknown> {
50
51
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -77,10 +78,17 @@ function exportsEntry(
77
78
  exportsField: unknown,
78
79
  subpath: string,
79
80
  ): { readonly value: unknown; readonly match?: string } | undefined {
80
- const map =
81
- isRecord(exportsField) && Object.keys(exportsField).some((key) => key.startsWith("."))
82
- ? exportsField
83
- : { ".": exportsField };
81
+ const keys = isRecord(exportsField) ? Object.keys(exportsField) : [];
82
+ const dotted = keys.filter((key) => key.startsWith(".")).length;
83
+ // An object mixing subpath keys with others is invalid to Node; the compiler still
84
+ // answers `.` from its `"."` key and no other subpath:
85
+ // https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/module/resolver.go#L673-L695
86
+ if (isRecord(exportsField) && dotted > 0 && dotted < keys.length) {
87
+ return subpath === "." && Object.hasOwn(exportsField, ".")
88
+ ? { value: exportsField["."] }
89
+ : undefined;
90
+ }
91
+ const map = isRecord(exportsField) && dotted > 0 ? exportsField : { ".": exportsField };
84
92
  if (Object.hasOwn(map, subpath)) {
85
93
  return { value: map[subpath] };
86
94
  }
@@ -224,6 +232,32 @@ export function workspaceResolver(host: Host, workspace: Workspace): WorkspaceRe
224
232
  return found;
225
233
  };
226
234
 
235
+ const root = workspace.members[0]?.dir;
236
+ const ignored = root === undefined ? () => false : gitIgnored(host, root);
237
+ const manifests = new Map<
238
+ string,
239
+ { dir: string; manifest: Record<string, unknown> } | undefined
240
+ >();
241
+ /** The nearest manifest at or above one directory, up to the workspace's root. */
242
+ const nearestManifest = (
243
+ dir: string,
244
+ ): { dir: string; manifest: Record<string, unknown> } | undefined => {
245
+ if (manifests.has(dir)) {
246
+ return manifests.get(dir);
247
+ }
248
+ let found: { dir: string; manifest: Record<string, unknown> } | undefined;
249
+ try {
250
+ const value: unknown = JSON.parse(host.readFile(joinPath(dir, "package.json")));
251
+ found = isRecord(value) ? { dir, manifest: value } : undefined;
252
+ } catch {
253
+ const up = dirnamePath(dir);
254
+ found =
255
+ up === dir || root === undefined || !inside(root, up) ? undefined : nearestManifest(up);
256
+ }
257
+ manifests.set(dir, found);
258
+ return found;
259
+ };
260
+
227
261
  const linkOf = (candidates: readonly WorkspaceMember[], fromDir: string): Linked => {
228
262
  const name = candidates[0]?.name ?? "";
229
263
  const linked = linkedFrom(name, fromDir);
@@ -235,14 +269,16 @@ export function workspaceResolver(host: Host, workspace: Workspace): WorkspaceRe
235
269
  if (scope?.name === name) {
236
270
  return { kind: "member", member: scope };
237
271
  }
238
- const manifest = scope?.manifest;
272
+ // A package below the root that no workspace lists depends on a member as a member does.
273
+ const owner = scope ?? nearestManifest(fromDir);
274
+ const manifest = owner?.manifest;
239
275
  const declared = DEPENDENCY_MEMBERS.flatMap((field) => {
240
276
  const held = manifest?.[field];
241
277
  const specifier = isRecord(held) ? held[name] : undefined;
242
278
  return typeof specifier === "string" ? [specifier] : [];
243
279
  });
244
280
  const matched = candidates.filter((member) =>
245
- declared.some((specifier) => dependsOn(specifier, member, scope?.dir ?? fromDir)),
281
+ declared.some((specifier) => dependsOn(specifier, member, owner?.dir ?? fromDir)),
246
282
  );
247
283
  const [one, two] = matched;
248
284
  if (one === undefined) {
@@ -257,6 +293,25 @@ export function workspaceResolver(host: Host, workspace: Workspace): WorkspaceRe
257
293
  const compiled = (member: WorkspaceMember, path: string): boolean =>
258
294
  member.configurations.some((configuration) => configuration.fileNames.has(path));
259
295
 
296
+ /**
297
+ * The member's source an ignored output path mirrors: the path below one of its
298
+ * directories, the deepest first, read below the member's own directory as an output
299
+ * directory's tree mirrors its root directory's, which a build leaves in an ignored
300
+ * directory however its configuration writes it.
301
+ */
302
+ const mirrored = (member: WorkspaceMember, path: string): string | undefined => {
303
+ for (let dir = dirnamePath(path); inside(member.dir, dir); dir = dirnamePath(dir)) {
304
+ const found = emittedFrom(path, { outDir: dir, sourceDirs: [member.dir] }).find(
305
+ (candidate) =>
306
+ SOURCE.test(candidate) && !ignored(candidate) && host.kindOf(candidate) === "file",
307
+ );
308
+ if (found !== undefined) {
309
+ return found;
310
+ }
311
+ }
312
+ return undefined;
313
+ };
314
+
260
315
  /**
261
316
  * The member's source one of its paths is, or is compiled from: a TypeScript source
262
317
  * file, the source an output path maps back to, or a file one of the member's
@@ -278,6 +333,9 @@ export function workspaceResolver(host: Host, workspace: Workspace): WorkspaceRe
278
333
  if (found !== undefined) {
279
334
  return found;
280
335
  }
336
+ if (ignored(path)) {
337
+ return mirrored(member, path);
338
+ }
281
339
  return compiled(member, path) && host.kindOf(path) === "file" ? path : undefined;
282
340
  };
283
341
 
@@ -344,10 +402,12 @@ export function workspaceResolver(host: Host, workspace: Workspace): WorkspaceRe
344
402
  }
345
403
  const { member } = linked;
346
404
  // A member no compiler configuration builds is read as the package manager
347
- // installed it, wherever the compiler's resolution found it.
405
+ // installed it, wherever the compiler's resolution found it, unless what it found is
406
+ // a file the tree ignores, which no install wrote.
348
407
  if (
349
408
  answered !== undefined &&
350
- (member.configurations.length === 0 || !inside(member.dir, answered.resolvedFileName))
409
+ ((member.configurations.length === 0 && !ignored(answered.resolvedFileName)) ||
410
+ !inside(member.dir, answered.resolvedFileName))
351
411
  ) {
352
412
  return { kind: "default" };
353
413
  }
@@ -371,6 +431,18 @@ export function workspaceResolver(host: Host, workspace: Workspace): WorkspaceRe
371
431
  if (answered !== undefined) {
372
432
  return { kind: "default" };
373
433
  }
434
+ // A stylesheet or another asset is no module with a source, so the import of one
435
+ // keeps the compiler's resolution.
436
+ if (
437
+ targets.some(
438
+ (target) =>
439
+ !SOURCE.test(target) &&
440
+ !OUTPUT.test(target) &&
441
+ host.kindOf(joinPath(member.dir, target)) === "file",
442
+ )
443
+ ) {
444
+ return { kind: "default" };
445
+ }
374
446
  const named = targets.length === 0 ? "no target" : targets.join(", ");
375
447
  const built = targets.some((target) => host.kindOf(joinPath(member.dir, target)) === "file");
376
448
  return {
package/src/workspace.ts CHANGED
@@ -274,7 +274,7 @@ function memberDirectories(
274
274
  /** Whether a workspace's patterns name one directory below its root, read without listing any. */
275
275
  function namesDirectory(root: string, patterns: readonly string[], dir: string): boolean {
276
276
  const below = relativePath(root, dir);
277
- if (below === undefined || below.startsWith("..")) {
277
+ if (below === undefined) {
278
278
  return false;
279
279
  }
280
280
  const path = below.split("/");