@cplieger/deadset-ts 8.0.0-dev.8 → 8.0.0-dev.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/src/analysis.js +84 -288
  2. package/dist/src/configuration-files.js +2 -9
  3. package/dist/src/consumers.js +1 -28
  4. package/dist/src/contract/contract.generated.js +20 -10
  5. package/dist/src/conventions.js +3 -8
  6. package/dist/src/failure.js +36 -0
  7. package/dist/src/fs/document.js +21 -28
  8. package/dist/src/fs/manifest.js +7 -0
  9. package/dist/src/load/classify.js +236 -0
  10. package/dist/src/load/diagnostics.js +79 -0
  11. package/dist/src/load/discover.js +230 -0
  12. package/dist/src/{emit-map.js → load/emit-map.js} +1 -1
  13. package/dist/src/load/expected-module.js +136 -0
  14. package/dist/src/load/load.js +22 -0
  15. package/dist/src/load/module-lookup.js +37 -0
  16. package/dist/src/load/plan.js +155 -0
  17. package/dist/src/{test-globals.js → load/test-globals.js} +51 -56
  18. package/dist/src/load/test-joins.js +82 -0
  19. package/dist/src/{workspace-manifests.js → load/workspace-manifests.js} +3 -3
  20. package/dist/src/{workspace-programs.js → load/workspace-programs.js} +37 -40
  21. package/dist/src/{workspace-resolution.js → load/workspace-resolution.js} +21 -59
  22. package/dist/src/{workspace-run.js → load/workspace-run.js} +4 -3
  23. package/dist/src/{workspace.js → load/workspace.js} +21 -17
  24. package/dist/src/outside-tests.js +1 -1
  25. package/dist/src/run.js +20 -26
  26. package/dist/src/scope.js +11 -19
  27. package/dist/src/session.js +22 -28
  28. package/dist/src/source-files.js +1 -1
  29. package/dist/src/type-errors.js +11 -216
  30. package/dist/src/verbs/analyze.js +90 -109
  31. package/dist/src/verbs/describe.js +5 -12
  32. package/dist/src/verbs/version.js +12 -2
  33. package/jsr.json +1 -1
  34. package/package.json +1 -1
  35. package/src/analysis.ts +68 -437
  36. package/src/configuration-files.ts +8 -10
  37. package/src/consumers.ts +2 -53
  38. package/src/contract/contract.generated.ts +20 -10
  39. package/src/conventions.ts +6 -12
  40. package/src/failure.ts +63 -0
  41. package/src/fs/document.ts +23 -27
  42. package/src/fs/manifest.ts +8 -0
  43. package/src/load/classify.ts +352 -0
  44. package/src/load/diagnostics.ts +141 -0
  45. package/src/load/discover.ts +378 -0
  46. package/src/{emit-map.ts → load/emit-map.ts} +1 -1
  47. package/src/load/expected-module.ts +188 -0
  48. package/src/load/load.ts +61 -0
  49. package/src/load/module-lookup.ts +50 -0
  50. package/src/load/plan.ts +258 -0
  51. package/src/{test-globals.ts → load/test-globals.ts} +59 -65
  52. package/src/load/test-joins.ts +98 -0
  53. package/src/{workspace-manifests.ts → load/workspace-manifests.ts} +4 -4
  54. package/src/{workspace-programs.ts → load/workspace-programs.ts} +59 -53
  55. package/src/{workspace-resolution.ts → load/workspace-resolution.ts} +31 -78
  56. package/src/{workspace-run.ts → load/workspace-run.ts} +14 -11
  57. package/src/{workspace.ts → load/workspace.ts} +28 -21
  58. package/src/outside-tests.ts +1 -1
  59. package/src/partial.ts +1 -1
  60. package/src/run.ts +26 -25
  61. package/src/scope.ts +15 -21
  62. package/src/session.ts +52 -53
  63. package/src/source-files.ts +1 -1
  64. package/src/type-errors.ts +18 -288
  65. package/src/verbs/analyze.ts +129 -136
  66. package/src/verbs/describe.ts +4 -11
  67. package/src/verbs/version.ts +13 -2
  68. package/dist/src/base-url.js +0 -18
  69. package/dist/src/discover.js +0 -374
  70. package/dist/src/setup-failure.js +0 -19
  71. package/dist/src/test-joins.js +0 -101
  72. package/src/base-url.ts +0 -24
  73. package/src/discover.ts +0 -548
  74. package/src/setup-failure.ts +0 -37
  75. package/src/test-joins.ts +0 -118
@@ -4,7 +4,7 @@
4
4
  * manifest or a document names is read back to its source through this one rule,
5
5
  * whether the path is the target's own or a workspace package's.
6
6
  */
7
- import { dirnamePath, joinPath, relativePath } from "./paths.js";
7
+ import { dirnamePath, joinPath, relativePath } from "../paths.js";
8
8
  /** The source extensions the compiler emits each output extension from. */
9
9
  const EMITTED = [
10
10
  { emitted: ".d.ts", sources: [".ts", ".tsx"] },
@@ -0,0 +1,136 @@
1
+ import { isNoSubstitutionTemplateLiteral, isStringLiteral, } from "@typescript/native/ast";
2
+ import { packageOfSpecifier } from "../dependencies.js";
3
+ import { MANIFEST } from "../fs/manifest.js";
4
+ import { entriesOf } from "../fs/tree.js";
5
+ import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "../paths.js";
6
+ import { dependencySpecifiers, installedPackage, manifestsAbove } from "./module-lookup.js";
7
+ /** The string literal holding one offset, innermost first, a doc comment's included. */
8
+ function literalAt(file, offset) {
9
+ let found;
10
+ const visit = (node) => {
11
+ if (offset < node.pos || offset >= node.end) {
12
+ return;
13
+ }
14
+ if (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node)) {
15
+ found = node.text;
16
+ }
17
+ // `forEachChild` does not enter a doc comment, where a JavaScript file imports types.
18
+ for (const doc of node.jsDoc ?? []) {
19
+ visit(doc);
20
+ }
21
+ node.forEachChild(visit);
22
+ };
23
+ file.forEachChild(visit);
24
+ return found;
25
+ }
26
+ /**
27
+ * What one `paths` key's `*` matches in one specifier, `""` for a key with no `*` that
28
+ * is the specifier, or `undefined` where the key does not match it.
29
+ */
30
+ function mappedBy(key, specifier) {
31
+ const star = key.indexOf("*");
32
+ if (star < 0) {
33
+ return key === specifier ? "" : undefined;
34
+ }
35
+ const prefix = key.slice(0, star);
36
+ const suffix = key.slice(star + 1);
37
+ return specifier.length >= prefix.length + suffix.length &&
38
+ specifier.startsWith(prefix) &&
39
+ specifier.endsWith(suffix)
40
+ ? specifier.slice(prefix.length, specifier.length - suffix.length)
41
+ : undefined;
42
+ }
43
+ /**
44
+ * One `paths` target, its first `*` replaced by `star` taken literally, as the compiler does:
45
+ * https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/module/resolver.go#L1282
46
+ */
47
+ function substituted(target, star) {
48
+ const at = target.indexOf("*");
49
+ return at < 0 ? target : target.slice(0, at) + star + target.slice(at + 1);
50
+ }
51
+ /**
52
+ * Whether a path names something on disk, as written or with any extension; under
53
+ * `fileOnly`, only a file counts.
54
+ */
55
+ function provided(host, path, fileOnly) {
56
+ const counts = (at) => fileOnly ? host.kindOf(at) === "file" : host.kindOf(at) !== "absent";
57
+ if (counts(path)) {
58
+ return true;
59
+ }
60
+ const dir = dirnamePath(path);
61
+ const stem = `${path.slice(dir.length + 1)}.`;
62
+ return entriesOf(host, dir).some((entry) => entry.name.startsWith(stem) && counts(joinPath(dir, entry.name)));
63
+ }
64
+ /** The manifest at or above `dir`, no higher than the target root, that declares `name`. */
65
+ function declaringManifest(host, dir, targetRoot, name) {
66
+ for (const { dir: at, manifest } of manifestsAbove(host, dir, targetRoot)) {
67
+ if (dependencySpecifiers(manifest, name).length > 0) {
68
+ return joinPath(at, MANIFEST);
69
+ }
70
+ }
71
+ return undefined;
72
+ }
73
+ /**
74
+ * Why one import that the compiler cannot resolve names a module the project expects,
75
+ * with the fix, or undefined where the module is one the project does not expect.
76
+ */
77
+ function expectedModule(context, importer, specifier) {
78
+ const { host, targetRoot } = context;
79
+ const fix = "run the generator or the build that writes it";
80
+ if (specifier.startsWith(".") || isAbsolutePath(specifier)) {
81
+ return provided(host, resolvePath(dirnamePath(importer), specifier), false)
82
+ ? undefined
83
+ : `which names no file of the target: ${fix}`;
84
+ }
85
+ if (specifier.startsWith("#")) {
86
+ return `which the manifest's imports map to no file: ${fix}`;
87
+ }
88
+ const mapped = context.paths.entries.flatMap(({ key, targets }) => {
89
+ const star = mappedBy(key, specifier);
90
+ return star === undefined
91
+ ? []
92
+ : targets.map((target) => resolvePath(context.paths.base, substituted(target, star)));
93
+ });
94
+ const name = packageOfSpecifier(specifier);
95
+ // The compiler looks up in `node_modules` what `paths` does not resolve, so the error
96
+ // of an installed package's import is the import's own, a subpath its exports omit:
97
+ // https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/module/resolver.go#L548-L575
98
+ if (name !== undefined && installedPackage(host, dirnamePath(importer), name) !== undefined) {
99
+ return undefined;
100
+ }
101
+ if (mapped.length > 0) {
102
+ return mapped.some((path) => provided(host, path, true))
103
+ ? undefined
104
+ : `which the compiler configuration's paths map to no file: ${fix}`;
105
+ }
106
+ const manifest = name === undefined
107
+ ? undefined
108
+ : declaringManifest(host, dirnamePath(importer), targetRoot, name);
109
+ // No run requires a nested package that is no workspace member to be installed.
110
+ if (manifest === undefined ||
111
+ (dirnamePath(manifest) !== targetRoot && !context.member(manifest))) {
112
+ return undefined;
113
+ }
114
+ const where = relativePath(targetRoot, manifest) ?? manifest;
115
+ return `a dependency ${where} declares that is not installed: run the package manager's install`;
116
+ }
117
+ /** The `paths` mapping of the configuration at `configFile` whose parse answered `options`. */
118
+ export function pathsOf(configFile, options) {
119
+ // The parse answers `pathsBasePath`, the directory of the configuration that writes
120
+ // `paths` (an extended one included), though the client's option type omits it:
121
+ // https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/tsoptions/tsconfigparsing.go#L1120
122
+ const declaredAt = options.pathsBasePath;
123
+ return {
124
+ base: typeof declaredAt === "string" ? declaredAt : dirnamePath(configFile),
125
+ entries: Object.entries(options.paths ?? {}).map(([key, targets]) => ({ key, targets })),
126
+ };
127
+ }
128
+ /**
129
+ * Why one import the compiler cannot resolve, at `offset` of `file`, names a module the project
130
+ * expects and nothing provides, with the fix; undefined where the project does not expect it.
131
+ */
132
+ export function missingModule(context, file, offset) {
133
+ const specifier = literalAt(file, offset);
134
+ const reason = specifier === undefined ? undefined : expectedModule(context, file.fileName, specifier);
135
+ return specifier === undefined || reason === undefined ? undefined : { specifier, reason };
136
+ }
@@ -0,0 +1,22 @@
1
+ import { resolvePath } from "../paths.js";
2
+ import { openSession } from "../session.js";
3
+ import { classify } from "./classify.js";
4
+ import { planRun, programBuilds } from "./plan.js";
5
+ /**
6
+ * Plans the run, opens every configuration in one snapshot of one client, classifies every
7
+ * project, and hands the analyzed and the consumer projects to `read` while the client answers.
8
+ * A run that cannot be analyzed ends with one {@link FailureExit} before `read` is called.
9
+ */
10
+ export function loadRun(engine, host, scope, config, read) {
11
+ const plan = planRun(engine, host, scope, config);
12
+ return openSession(engine, plan.plans.map((one) => one.configFile), { programs: programBuilds(host, plan.plans) }, (session, unanswered) => {
13
+ const classified = classify(host, plan, new Map(session.views.map((view) => [view.configFile, view])));
14
+ const runFiles = new Set(session.views.flatMap((view) => view.ownSourceFiles().map((file) => resolvePath(host.workingDirectory(), file.fileName))));
15
+ return {
16
+ plan,
17
+ classified,
18
+ result: read({ plan, classified, runFiles }),
19
+ unanswered: unanswered(),
20
+ };
21
+ });
22
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Where a package an import names is installed and which manifests above an importer declare
3
+ * it: the leaf both the expected-module reading and the workspace resolution read.
4
+ */
5
+ import { DEPENDENCY_SECTIONS, INSTALLED_PACKAGES, manifestAt } from "../fs/manifest.js";
6
+ import { isRecord } from "../json-document.js";
7
+ import { ancestors, joinPath } from "../paths.js";
8
+ /**
9
+ * The directory `node_modules/<name>` holds for an importer in `dir`: the nearest one at or
10
+ * above `dir`, above any target root too, that is a directory, a link to one included, as the
11
+ * compiler looks a package up. Undefined where no ancestor installs it.
12
+ */
13
+ export function installedPackage(host, dir, name) {
14
+ return ancestors(dir)
15
+ .map((at) => joinPath(at, INSTALLED_PACKAGES, name))
16
+ .find((candidate) => host.kindOf(candidate) === "directory");
17
+ }
18
+ /**
19
+ * The manifests at or above `dir` and no higher than `top`, nearest first; a directory outside
20
+ * `top` reads its own manifest alone.
21
+ */
22
+ export function* manifestsAbove(host, dir, top) {
23
+ const chain = ancestors(dir, top);
24
+ for (const at of chain.length > 0 ? chain : [dir]) {
25
+ const manifest = manifestAt(host, at);
26
+ if (manifest !== undefined) {
27
+ yield { dir: at, manifest };
28
+ }
29
+ }
30
+ }
31
+ /** The specifiers one manifest depends on `name` with, one per dependency section naming it. */
32
+ export function dependencySpecifiers(manifest, name) {
33
+ return DEPENDENCY_SECTIONS.flatMap((section) => {
34
+ const declared = manifest[section];
35
+ return isRecord(declared) && Object.hasOwn(declared, name) ? [declared[name]] : [];
36
+ });
37
+ }
@@ -0,0 +1,155 @@
1
+ import { readConventions } from "../conventions.js";
2
+ import { holdsManifest, inInstalledPackages, MANIFEST } from "../fs/manifest.js";
3
+ import { packageEntries, readManifest } from "../manifest.js";
4
+ import { ancestors, dirnamePath, normalizePath, relativePath, resolvePath } from "../paths.js";
5
+ import { bytewise } from "../text/compare.js";
6
+ import { globExpression } from "../text/glob.js";
7
+ import { discoverProjects, isTargetConfiguration, } from "./discover.js";
8
+ import { FailureExit } from "../failure.js";
9
+ import { joinedTestFiles, joinedTests } from "./test-joins.js";
10
+ import { testGlobals } from "./test-globals.js";
11
+ import { findWorkspace } from "./workspace.js";
12
+ import { writtenConfiguration } from "./workspace-programs.js";
13
+ import { workspaceRun } from "./workspace-run.js";
14
+ /**
15
+ * The `paths` one configuration's resolved options map under the compilers before version
16
+ * 7, each target absolute: a target is read against the effective `baseUrl`, and a specifier
17
+ * no other pattern maps is looked up below it. Undefined where no `baseUrl` is set, so the
18
+ * options' own `paths` stand. Version 7 reads no `baseUrl`:
19
+ * https://github.com/microsoft/TypeScript/issues/62207
20
+ */
21
+ export function baseUrlPaths(options) {
22
+ const baseUrl = Reflect.get(options, "baseUrl");
23
+ if (typeof baseUrl !== "string") {
24
+ return undefined;
25
+ }
26
+ return Object.fromEntries(Object.entries({ "*": ["*"], ...options.paths }).map(([key, targets]) => [
27
+ key,
28
+ targets.map((target) => resolvePath(baseUrl, target)),
29
+ ]));
30
+ }
31
+ /** One configuration, read through the `paths` its `baseUrl` gives. */
32
+ function planned(configuration) {
33
+ const { options } = configuration.parsed;
34
+ const paths = baseUrlPaths(options);
35
+ return {
36
+ ...configuration,
37
+ options: paths === undefined ? options : { ...options, paths },
38
+ build: paths === undefined
39
+ ? { kind: "project" }
40
+ : {
41
+ kind: "configuration",
42
+ value: { extends: configuration.configFile, compilerOptions: { paths } },
43
+ },
44
+ };
45
+ }
46
+ /**
47
+ * The nested packages of a run: each directory below the target root, in no `node_modules`
48
+ * directory and none of `known`, that holds a `package.json` and lies at or above one of
49
+ * `files`, in path order.
50
+ */
51
+ function nestedPackageDirs(host, targetRoot, files, known) {
52
+ const seen = new Set();
53
+ const found = [];
54
+ for (const file of files) {
55
+ for (const dir of ancestors(dirnamePath(file), targetRoot)) {
56
+ if (seen.has(dir) || relativePath(targetRoot, dir) === undefined) {
57
+ break;
58
+ }
59
+ seen.add(dir);
60
+ const below = relativePath(targetRoot, dir) ?? "";
61
+ if (!inInstalledPackages(below) && !known.has(dir) && holdsManifest(host, dir)) {
62
+ found.push(dir);
63
+ }
64
+ }
65
+ }
66
+ return found.sort(bytewise);
67
+ }
68
+ /** The manifests conventions are applied from: the target's, then each member's and nested package's. */
69
+ function conventionManifests(host, root, packages) {
70
+ const found = new Map();
71
+ if (holdsManifest(host, root)) {
72
+ found.set(root, { dir: root, path: MANIFEST });
73
+ }
74
+ for (const [dirs, nested] of [
75
+ [packages.members, false],
76
+ [packages.nested, true],
77
+ ]) {
78
+ for (const dir of dirs) {
79
+ const below = relativePath(root, dir);
80
+ if (below !== undefined && !found.has(dir)) {
81
+ found.set(dir, { dir, path: `${below}/${MANIFEST}`, nested });
82
+ }
83
+ }
84
+ }
85
+ return [...found.values()];
86
+ }
87
+ /** The workspace one consumer sits in, read from its own root, the target's reused. */
88
+ function consumerWorkspace(host, consumer, target) {
89
+ try {
90
+ return { workspace: findWorkspace(host, consumer.path, () => undefined, target) };
91
+ }
92
+ catch (error) {
93
+ if (!(error instanceof FailureExit)) {
94
+ throw error;
95
+ }
96
+ return { fault: error.failures[0].detail };
97
+ }
98
+ }
99
+ /**
100
+ * The plan of one run: the configurations discovery names, each built through the steps in
101
+ * order (the `baseUrl` paths, the workspace members read from source, the test runner's
102
+ * globals, the joined test files of a derived matrix), the package set and the convention
103
+ * rows applied per package. A nested package is one at or above a file a target
104
+ * configuration lists or a test file that joins it.
105
+ */
106
+ export function planRun(engine, host, scope, config) {
107
+ const discovery = discoverProjects(engine, host, scope, config.analysis.configurations);
108
+ const root = resolvePath(host.workingDirectory(), scope.target.path);
109
+ const parse = (fileName, text) => engine.parseSourceFile(fileName, text);
110
+ let plans = discovery.configurations.map(planned);
111
+ const targets = plans.filter(isTargetConfiguration);
112
+ const manifest = readManifest(host, scope.target.path);
113
+ const workspace = workspaceRun(engine, host, root, targets, manifest);
114
+ const consumerWorkspaces = new Map(discovery.consumers.map((consumer) => [
115
+ consumer.path,
116
+ consumerWorkspace(host, consumer, workspace?.workspace),
117
+ ]));
118
+ plans = plans.map((plan) => {
119
+ const build = workspace?.programs.get(plan.configFile);
120
+ return build === undefined ? plan : { ...plan, build };
121
+ });
122
+ plans = testGlobals(host, parse, plans);
123
+ const joins = discovery.mode === "derived"
124
+ ? joinedTestFiles(host, root, targets, config.ts.test_files.map(globExpression))
125
+ : new Map();
126
+ plans = joinedTests(plans, joins);
127
+ const members = workspace?.memberDirs ?? [];
128
+ const nested = nestedPackageDirs(host, normalizePath(root), targets.flatMap((plan) => [
129
+ plan.configFile,
130
+ ...plan.parsed.fileNames,
131
+ ...(joins.get(plan.configFile) ?? []),
132
+ ]), new Set(members));
133
+ const packages = {
134
+ manifest: workspace?.manifest ?? manifest,
135
+ members,
136
+ nested,
137
+ entries: [...(workspace?.entries ?? []), ...packageEntries(host, normalizePath(root), nested)],
138
+ published: new Set((workspace?.entries ?? []).filter((entry) => entry.published).map((entry) => entry.dir)),
139
+ };
140
+ const conventions = readConventions(host, parse, conventionManifests(host, root, packages), config.ts.disabled_conventions);
141
+ return { discovery, plans, packages, conventions, workspace, consumerWorkspaces, root };
142
+ }
143
+ /** How the snapshot builds each planned configuration other than by opening its project. */
144
+ export function programBuilds(host, plans) {
145
+ const builds = new Map();
146
+ for (const { configFile, build } of plans) {
147
+ if (build.kind === "program") {
148
+ builds.set(configFile, build);
149
+ }
150
+ else if (build.kind === "configuration") {
151
+ builds.set(configFile, writtenConfiguration(host, configFile, build.value));
152
+ }
153
+ }
154
+ return builds;
155
+ }
@@ -7,11 +7,12 @@
7
7
  * written for the runner, as one that imports it is.
8
8
  */
9
9
  import { isAsExpression, isBinaryExpression, isCallExpression, isExportAssignment, isExpressionStatement, isIdentifier, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, SyntaxKind, } from "@typescript/native/ast";
10
- import { ancestors, dirnamePath, joinPath } from "./paths.js";
11
- import { UNANSWERED } from "./query.js";
12
- import { writtenPath } from "./workspace-programs.js";
13
- import { filesIn } from "./fs/tree.js";
14
- import { holdsManifest, INSTALLED_PACKAGES } from "./fs/manifest.js";
10
+ import { isRecord } from "../json-document.js";
11
+ import { ancestors, dirnamePath, joinPath } from "../paths.js";
12
+ import { UNANSWERED } from "../query.js";
13
+ import { filesIn } from "../fs/tree.js";
14
+ import { holdsManifest, INSTALLED_PACKAGES } from "../fs/manifest.js";
15
+ import { isTargetConfiguration } from "./discover.js";
15
16
  /** The test runner whose configuration injects the globals, and the `types` entry declaring them. */
16
17
  export const GLOBALS_RUNNER = "vitest";
17
18
  const GLOBALS_TYPES = "vitest/globals";
@@ -117,67 +118,61 @@ function typesOf(value) {
117
118
  return Array.isArray(value) && value.every((one) => typeof one === "string") ? value : undefined;
118
119
  }
119
120
  /**
120
- * How the snapshot reads each configuration of `configFiles` whose nearest manifest's
121
- * runner configuration injects the globals and that can resolve their declarations: as
122
- * `programs` reads it, else as itself, with the globals' declarations added to its `types`.
123
- * A configuration the compiler cannot read is left for its project to refuse.
121
+ * The plans with the globals' declarations added to the `types` of each target configuration
122
+ * whose nearest manifest's runner configuration injects them and that can resolve them.
124
123
  */
125
- export function withTestGlobals(engine, host, parse, configFiles, programs) {
126
- const found = new Map(programs);
124
+ export function testGlobals(host, parse, plans) {
127
125
  const injects = new Map();
128
- for (const configFile of configFiles) {
129
- const dir = manifestDir(host, configFile);
130
- if (dir === undefined || !globalsInstalled(host, configFile)) {
131
- continue;
126
+ return plans.map((plan) => {
127
+ const dir = manifestDir(host, plan.configFile);
128
+ if (!isTargetConfiguration(plan) ||
129
+ dir === undefined ||
130
+ !globalsInstalled(host, plan.configFile)) {
131
+ return plan;
132
132
  }
133
133
  if (!injects.has(dir)) {
134
134
  injects.set(dir, injectsGlobals(host, parse, dir));
135
135
  }
136
+ const base = typesOf(plan.options.types) ?? [];
137
+ const { build } = plan;
136
138
  if (injects.get(dir) !== true) {
137
- continue;
138
- }
139
- let base;
140
- try {
141
- base = typesOf(engine.parseConfigFile(configFile).options.types) ?? [];
142
- }
143
- catch {
144
- continue;
145
- }
146
- const held = found.get(configFile);
147
- if (held?.kind === "program") {
148
- const listed = typesOf(held.params.compilerOptions.types) ?? base;
149
- if (!listed.includes(GLOBALS_TYPES)) {
150
- found.set(configFile, {
151
- kind: "program",
152
- params: {
153
- ...held.params,
154
- compilerOptions: { ...held.params.compilerOptions, types: [...listed, GLOBALS_TYPES] },
139
+ return plan;
140
+ }
141
+ if (build.kind === "program") {
142
+ const listed = typesOf(build.params.compilerOptions.types) ?? base;
143
+ return listed.includes(GLOBALS_TYPES)
144
+ ? plan
145
+ : {
146
+ ...plan,
147
+ build: {
148
+ kind: "program",
149
+ params: {
150
+ ...build.params,
151
+ compilerOptions: {
152
+ ...build.params.compilerOptions,
153
+ types: [...listed, GLOBALS_TYPES],
154
+ },
155
+ },
155
156
  },
156
- });
157
- }
158
- continue;
157
+ };
159
158
  }
160
- let written = { extends: configFile };
161
- if (held !== undefined) {
162
- try {
163
- written = JSON.parse(held.text);
164
- }
165
- catch {
166
- continue;
167
- }
168
- }
169
- const options = (written["compilerOptions"] ?? {});
159
+ const written = build.kind === "configuration" ? build.value : { extends: plan.configFile };
160
+ const held = written["compilerOptions"];
161
+ const options = isRecord(held) ? held : {};
170
162
  const listed = typesOf(options["types"]) ?? base;
171
- if (listed.includes(GLOBALS_TYPES)) {
172
- continue;
173
- }
174
- found.set(configFile, {
175
- kind: "configuration",
176
- file: held?.file ?? writtenPath(host, configFile),
177
- text: `${JSON.stringify({ ...written, compilerOptions: { ...options, types: [...listed, GLOBALS_TYPES] } }, null, 2)}\n`,
178
- });
179
- }
180
- return found;
163
+ return listed.includes(GLOBALS_TYPES)
164
+ ? plan
165
+ : {
166
+ ...plan,
167
+ build: {
168
+ kind: "configuration",
169
+ value: {
170
+ ...written,
171
+ compilerOptions: { ...options, types: [...listed, GLOBALS_TYPES] },
172
+ },
173
+ },
174
+ };
175
+ });
181
176
  }
182
177
  /**
183
178
  * Whether one file names a global the runner injects, which is how a file written for the
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The test files a derived matrix holds although no configuration's file list names them.
3
+ * The test runner compiles such a file with the options of the nearest compiler
4
+ * configuration at or above it, so the file joins that configuration's program as a file
5
+ * its list names, unless a `package.json` below that configuration's directory makes the
6
+ * file another package's, which its own test runner compiles.
7
+ */
8
+ import { sourcePaths } from "../file-facts.js";
9
+ import { ancestors, dirnamePath, joinPath } from "../paths.js";
10
+ import { bytewise } from "../text/compare.js";
11
+ import { holdsManifest } from "../fs/manifest.js";
12
+ const PREFERRED = "/tsconfig.json";
13
+ /** The configurations of one directory in the order a file joins them: `tsconfig.json` first. */
14
+ function byPreference(a, b) {
15
+ const first = a.endsWith(PREFERRED) ? 0 : 1;
16
+ const second = b.endsWith(PREFERRED) ? 0 : 1;
17
+ return first - second || bytewise(a, b);
18
+ }
19
+ /**
20
+ * Per configuration of `configurations`, the absolute paths of the test files that join it:
21
+ * each file below `root` a pattern of `patterns` names, that no configuration's file list
22
+ * holds, joins the configuration nearest at or above its directory when no `package.json`
23
+ * sits between them.
24
+ */
25
+ export function joinedTestFiles(host, root, configurations, patterns) {
26
+ const listed = new Set();
27
+ const byDir = new Map();
28
+ for (const { configFile, parsed } of configurations) {
29
+ parsed.fileNames.forEach((name) => listed.add(name));
30
+ const dir = dirnamePath(configFile);
31
+ byDir.set(dir, [...(byDir.get(dir) ?? []), configFile].sort(byPreference));
32
+ }
33
+ const joins = new Map();
34
+ for (const path of sourcePaths(host, root)) {
35
+ const file = joinPath(root, path);
36
+ if (listed.has(file) || !patterns.some((pattern) => pattern.test(path))) {
37
+ continue;
38
+ }
39
+ for (const dir of ancestors(dirnamePath(file), root)) {
40
+ const [nearest] = byDir.get(dir) ?? [];
41
+ if (nearest !== undefined) {
42
+ joins.set(nearest, [...(joins.get(nearest) ?? []), file]);
43
+ break;
44
+ }
45
+ if (holdsManifest(host, dir)) {
46
+ break;
47
+ }
48
+ }
49
+ }
50
+ return joins;
51
+ }
52
+ /**
53
+ * The plans with each joined test file added to the configuration it joins: to a described
54
+ * program's root files, or to the file list of a configuration only the snapshot holds, which
55
+ * lists every file the configuration lists and the joined ones.
56
+ */
57
+ export function joinedTests(plans, joins) {
58
+ return plans.map((plan) => {
59
+ const files = joins.get(plan.configFile);
60
+ const { build } = plan;
61
+ if (files === undefined) {
62
+ return plan;
63
+ }
64
+ if (build.kind === "program") {
65
+ return {
66
+ ...plan,
67
+ build: {
68
+ kind: "program",
69
+ params: { ...build.params, rootFiles: [...(build.params.rootFiles ?? []), ...files] },
70
+ },
71
+ };
72
+ }
73
+ const written = build.kind === "configuration" ? build.value : { extends: plan.configFile };
74
+ return {
75
+ ...plan,
76
+ build: {
77
+ kind: "configuration",
78
+ value: { ...written, files: [...plan.parsed.fileNames, ...files], include: [] },
79
+ },
80
+ };
81
+ });
82
+ }
@@ -6,9 +6,9 @@
6
6
  * workspace imports, so only its commands are entry points.
7
7
  */
8
8
  import { emittedFrom } from "./emit-map.js";
9
- import { readManifest } from "./manifest.js";
10
- import { joinPath, relativePath } from "./paths.js";
11
- import { MANIFEST } from "./fs/manifest.js";
9
+ import { readManifest } from "../manifest.js";
10
+ import { joinPath, relativePath } from "../paths.js";
11
+ import { MANIFEST } from "../fs/manifest.js";
12
12
  /** The one character a subpath pattern of a manifest's `exports` holds. */
13
13
  const SUBPATH_WILDCARD = "*";
14
14
  /** The paths one written target stands for in its member's sources, itself included. */