ts-axi 0.1.2 → 0.1.4
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/README.md +16 -1
- package/dist/src/commands/common.js +83 -16
- package/dist/src/commands/def.js +6 -6
- package/dist/src/commands/home.js +35 -43
- package/dist/src/commands/impact.js +15 -23
- package/dist/src/commands/refs.js +95 -15
- package/dist/src/impact.js +5 -27
- package/dist/src/importers.js +182 -0
- package/dist/src/project.js +110 -73
- package/dist/src/scope.js +119 -0
- package/dist/src/symbols.js +324 -20
- package/dist/src/walk.js +96 -0
- package/dist/src/workspace.js +297 -0
- package/package.json +1 -1
package/dist/src/project.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
// Project discovery and the two engine paths (ADR 0001): a syntactic index
|
|
2
2
|
// over `ts.createSourceFile`, and a LanguageService for semantic queries.
|
|
3
|
-
import { existsSync,
|
|
3
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import ts from "typescript";
|
|
6
6
|
import { usageError } from "./errors.js";
|
|
7
7
|
import { classifyReferenceGroups } from "./references.js";
|
|
8
8
|
import { collectDeclarations } from "./symbols.js";
|
|
9
|
+
import { byDepthThenPath, listFiles } from "./walk.js";
|
|
10
|
+
import { detectWorkspace, hasMultiProjectSignal, isMultiProject } from "./workspace.js";
|
|
9
11
|
export const SOURCE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs"];
|
|
10
|
-
|
|
12
|
+
/** An implicit project above this many files is analysed by importer subgraph, not as a whole. */
|
|
13
|
+
export const IMPLICIT_SCOPE_THRESHOLD = 3_000;
|
|
11
14
|
export const IMPLICIT_WARNING = "no tsconfig.json found - implicit project";
|
|
12
15
|
function real(path) {
|
|
13
16
|
try {
|
|
@@ -65,29 +68,9 @@ export function discoverTsconfig(startDir, options = {}) {
|
|
|
65
68
|
function isSourceFile(name) {
|
|
66
69
|
return SOURCE_EXTENSIONS.some((ext) => name.endsWith(ext)) && !/\.d\.[mc]?ts$/.test(name);
|
|
67
70
|
}
|
|
68
|
-
/** Every source file below `root
|
|
71
|
+
/** Every source file below `root` from the shared listing (`src/walk.ts`: git, else fs walk), minus `.d.ts`. */
|
|
69
72
|
export function walkSourceFiles(root) {
|
|
70
|
-
|
|
71
|
-
const visit = (dir) => {
|
|
72
|
-
let entries;
|
|
73
|
-
try {
|
|
74
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
for (const entry of entries) {
|
|
80
|
-
if (entry.isDirectory()) {
|
|
81
|
-
if (!SKIP_DIRS.has(entry.name))
|
|
82
|
-
visit(join(dir, entry.name));
|
|
83
|
-
}
|
|
84
|
-
else if (entry.isFile() && isSourceFile(entry.name)) {
|
|
85
|
-
files.push(join(dir, entry.name));
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
visit(root);
|
|
90
|
-
return files.sort();
|
|
73
|
+
return listFiles(root, isSourceFile).files;
|
|
91
74
|
}
|
|
92
75
|
export const IMPLICIT_COMPILER_OPTIONS = {
|
|
93
76
|
allowJs: true,
|
|
@@ -101,6 +84,52 @@ export const IMPLICIT_COMPILER_OPTIONS = {
|
|
|
101
84
|
jsx: ts.JsxEmit.Preserve,
|
|
102
85
|
resolveJsonModule: true,
|
|
103
86
|
};
|
|
87
|
+
function readTsconfig(path) {
|
|
88
|
+
const read = ts.readConfigFile(path, ts.sys.readFile);
|
|
89
|
+
if (read.error)
|
|
90
|
+
return undefined;
|
|
91
|
+
return ts.parseJsonConfigFileContent(read.config, ts.sys, dirname(path), undefined, path);
|
|
92
|
+
}
|
|
93
|
+
function sourceNames(parsed) {
|
|
94
|
+
return parsed.fileNames
|
|
95
|
+
.map((f) => resolve(f))
|
|
96
|
+
.filter((f) => !f.split(sep).includes("node_modules") && SOURCE_EXTENSIONS.some((ext) => f.endsWith(ext)));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The configs a tsconfig stands for: itself, or - when it has `references` and no files of its
|
|
100
|
+
* own (an Nx project shell) - its referenced configs, recursively and deduplicated.
|
|
101
|
+
*/
|
|
102
|
+
export function expandTsconfig(path) {
|
|
103
|
+
const result = [];
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
const visit = (file) => {
|
|
106
|
+
const abs = real(file);
|
|
107
|
+
if (seen.has(abs))
|
|
108
|
+
return;
|
|
109
|
+
seen.add(abs);
|
|
110
|
+
const parsed = readTsconfig(abs);
|
|
111
|
+
if (!parsed)
|
|
112
|
+
return;
|
|
113
|
+
const fileNames = sourceNames(parsed);
|
|
114
|
+
const refs = parsed.projectReferences ?? [];
|
|
115
|
+
if (fileNames.length === 0 && refs.length > 0) {
|
|
116
|
+
for (const ref of refs) {
|
|
117
|
+
const target = ts.resolveProjectReferencePath(ref);
|
|
118
|
+
if (existsSync(target))
|
|
119
|
+
visit(target);
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
result.push({ path: abs, fileNames, options: parsed.options });
|
|
124
|
+
};
|
|
125
|
+
visit(path);
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
/** One config's options for a union: the first config's, with every config's `types` merged. */
|
|
129
|
+
function unionOptions(configs) {
|
|
130
|
+
const types = configs.some((c) => c.options.types === undefined) ? undefined : [...new Set(configs.flatMap((c) => c.options.types ?? []))];
|
|
131
|
+
return { ...configs[0].options, ...(types ? { types } : {}) };
|
|
132
|
+
}
|
|
104
133
|
/** Resolve the Project's file list and compiler options. Cheap: reads directories, parses no source. */
|
|
105
134
|
export function loadProject(root, location) {
|
|
106
135
|
if (!location.path) {
|
|
@@ -110,11 +139,14 @@ export function loadProject(root, location) {
|
|
|
110
139
|
if (read.error) {
|
|
111
140
|
throw usageError(`cannot read ${location.path} - ${ts.flattenDiagnosticMessageText(read.error.messageText, " ")}`);
|
|
112
141
|
}
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
142
|
+
const configs = expandTsconfig(location.path);
|
|
143
|
+
const self = configs.length === 1 && configs[0].path === real(location.path);
|
|
144
|
+
if (self || configs.length === 0) {
|
|
145
|
+
const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, dirname(location.path), undefined, location.path);
|
|
146
|
+
return { root, tsconfig: location.path, implicit: false, fileNames: sourceNames(parsed), options: { ...parsed.options, noEmit: true } };
|
|
147
|
+
}
|
|
148
|
+
const fileNames = [...new Set(configs.flatMap((c) => c.fileNames))].sort();
|
|
149
|
+
return { root, tsconfig: location.path, implicit: false, fileNames, options: { ...unionOptions(configs), noEmit: true }, referenced: configs.length };
|
|
118
150
|
}
|
|
119
151
|
function scriptKind(file) {
|
|
120
152
|
if (file.endsWith(".tsx"))
|
|
@@ -148,47 +180,25 @@ const OUTSIDE_WALK_CAP = 20_000;
|
|
|
148
180
|
const OUTSIDE_WALK_MS = 600;
|
|
149
181
|
/**
|
|
150
182
|
* Declarations matching `query` in source files below the repo root that are not in the Project
|
|
151
|
-
* (syntactic
|
|
183
|
+
* (syntactic; the fs fallback is bounded by file count and time; at most `max` hits, shallowest paths first).
|
|
152
184
|
*/
|
|
153
185
|
export function findOutsideProject(index, query, max = 3) {
|
|
154
186
|
const inProject = new Set(index.files);
|
|
155
|
-
const
|
|
187
|
+
const { files } = listFiles(index.root, isSourceFile, { maxFiles: OUTSIDE_WALK_CAP, deadline: Date.now() + OUTSIDE_WALK_MS });
|
|
156
188
|
const hits = [];
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
while (stack.length > 0 && hits.length < max && visited < OUTSIDE_WALK_CAP && Date.now() <= deadline) {
|
|
160
|
-
const dir = stack.pop();
|
|
161
|
-
let entries;
|
|
162
|
-
try {
|
|
163
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
164
|
-
}
|
|
165
|
-
catch {
|
|
189
|
+
for (const path of files.sort((a, b) => byDepthThenPath(index.rel(a), index.rel(b)))) {
|
|
190
|
+
if (inProject.has(path))
|
|
166
191
|
continue;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (!entry.isFile() || !isSourceFile(entry.name) || inProject.has(path))
|
|
176
|
-
continue;
|
|
177
|
-
visited++;
|
|
178
|
-
const text = index.text(path);
|
|
179
|
-
if (text === undefined || !text.includes(query.name))
|
|
180
|
-
continue;
|
|
181
|
-
for (const decl of index.declarations(path)) {
|
|
182
|
-
if (declMatches(decl, query))
|
|
183
|
-
hits.push(decl);
|
|
184
|
-
if (hits.length >= max)
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
187
|
-
if (hits.length >= max)
|
|
188
|
-
break;
|
|
189
|
-
}
|
|
192
|
+
const text = index.text(path);
|
|
193
|
+
if (text === undefined || !text.includes(query.name))
|
|
194
|
+
continue;
|
|
195
|
+
for (const decl of index.declarations(path))
|
|
196
|
+
if (declMatches(decl, query))
|
|
197
|
+
hits.push(decl);
|
|
198
|
+
if (hits.length >= max)
|
|
199
|
+
break;
|
|
190
200
|
}
|
|
191
|
-
return hits.sort((a, b) => a.file
|
|
201
|
+
return hits.slice(0, max).sort((a, b) => byDepthThenPath(a.file, b.file) || a.line - b.line);
|
|
192
202
|
}
|
|
193
203
|
/** Nearest `tsconfig.json` from the directory of `abs` up to `root` (inclusive). */
|
|
194
204
|
export function nearestTsconfig(abs, root) {
|
|
@@ -268,19 +278,21 @@ export function createSyntacticIndex(root, project) {
|
|
|
268
278
|
return index;
|
|
269
279
|
}
|
|
270
280
|
const FULL_DIAGNOSTICS_LIMIT = 300;
|
|
271
|
-
export function createSemanticProject(index) {
|
|
281
|
+
export function createSemanticProject(index, scope) {
|
|
272
282
|
const started = performance.now();
|
|
273
283
|
const { project } = index;
|
|
274
|
-
const fileNames = project.fileNames;
|
|
284
|
+
const fileNames = scope?.rootNames ?? project.fileNames;
|
|
285
|
+
const options = scope?.options ?? project.options;
|
|
275
286
|
const snapshots = new Map();
|
|
276
|
-
const currentDirectory = project.tsconfig ? dirname(project.tsconfig) : index.root;
|
|
287
|
+
const currentDirectory = scope?.currentDirectory ?? (project.tsconfig ? dirname(project.tsconfig) : index.root);
|
|
288
|
+
const indexed = new Set(index.files);
|
|
277
289
|
const host = {
|
|
278
|
-
getCompilationSettings: () =>
|
|
290
|
+
getCompilationSettings: () => options,
|
|
279
291
|
getScriptFileNames: () => fileNames,
|
|
280
292
|
getScriptVersion: () => "1",
|
|
281
293
|
getScriptSnapshot(file) {
|
|
282
294
|
if (!snapshots.has(file)) {
|
|
283
|
-
const text =
|
|
295
|
+
const text = indexed.has(file) ? index.text(file) : ts.sys.readFile(file);
|
|
284
296
|
snapshots.set(file, text === undefined ? undefined : ts.ScriptSnapshot.fromString(text));
|
|
285
297
|
}
|
|
286
298
|
return snapshots.get(file);
|
|
@@ -513,7 +525,8 @@ export function createSemanticProject(index) {
|
|
|
513
525
|
return service.getQuickInfoAtPosition(absFile, pos);
|
|
514
526
|
},
|
|
515
527
|
diagnosticsCount(touched = []) {
|
|
516
|
-
|
|
528
|
+
// A subgraph program mixes projects under one set of options: count the touched files only.
|
|
529
|
+
const files = !scope && projectFiles.length <= FULL_DIAGNOSTICS_LIMIT ? projectFiles : touched.map((f) => index.abs(f));
|
|
517
530
|
let count = 0;
|
|
518
531
|
for (const file of files) {
|
|
519
532
|
const sf = program.getSourceFile(file);
|
|
@@ -526,7 +539,21 @@ export function createSemanticProject(index) {
|
|
|
526
539
|
elapsedMs: () => Math.round(performance.now() - started),
|
|
527
540
|
};
|
|
528
541
|
}
|
|
529
|
-
/**
|
|
542
|
+
/** The workspace as one file set; each semantic command scopes its own program from it. */
|
|
543
|
+
function scopedProject(root, workspace, implicit) {
|
|
544
|
+
return {
|
|
545
|
+
root,
|
|
546
|
+
tsconfig: null,
|
|
547
|
+
implicit,
|
|
548
|
+
fileNames: workspace.sourceFiles,
|
|
549
|
+
options: IMPLICIT_COMPILER_OPTIONS,
|
|
550
|
+
scope: { workspace, kind: implicit ? "implicit" : workspace.kind },
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Repo root + tsconfig discovery + syntactic index in one call. Without `-P` / `.ts-axi.json`
|
|
555
|
+
* `project`, a multi-project workspace (or a large implicit project) becomes one scoped file set.
|
|
556
|
+
*/
|
|
530
557
|
export function openProject(cwd, options = {}) {
|
|
531
558
|
const root = findRepoRoot(cwd);
|
|
532
559
|
const location = discoverTsconfig(options.startDir ?? cwd, {
|
|
@@ -536,6 +563,16 @@ export function openProject(cwd, options = {}) {
|
|
|
536
563
|
settingsRoot: options.settingsRoot ?? root,
|
|
537
564
|
...(existsSync(join(root, ".git")) ? { stopAt: root } : {}),
|
|
538
565
|
});
|
|
539
|
-
const
|
|
540
|
-
|
|
566
|
+
const explicit = options.override !== undefined || options.settingsProject !== undefined;
|
|
567
|
+
let project;
|
|
568
|
+
if (!explicit && (location.implicit || hasMultiProjectSignal(root))) {
|
|
569
|
+
const workspace = detectWorkspace(root);
|
|
570
|
+
if (isMultiProject(workspace.kind))
|
|
571
|
+
project = scopedProject(root, workspace, false);
|
|
572
|
+
else if (location.implicit && workspace.sourceFiles.length > IMPLICIT_SCOPE_THRESHOLD)
|
|
573
|
+
project = scopedProject(root, workspace, true);
|
|
574
|
+
}
|
|
575
|
+
project ??= loadProject(root, location);
|
|
576
|
+
const index = createSyntacticIndex(root, project);
|
|
577
|
+
return { cwd, root, location, index, ...(project.implicit ? { warning: IMPLICIT_WARNING } : {}) };
|
|
541
578
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Scoped program for semantic commands in a workspace: seed files + their importers (syntactic
|
|
2
|
+
// importer index), compiled with the seed's own tsconfig plus the workspace `paths`.
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import ts from "typescript";
|
|
6
|
+
import { buildImporterIndex, createResolver, importerClosure } from "./importers.js";
|
|
7
|
+
import { createSemanticProject, expandTsconfig, IMPLICIT_COMPILER_OPTIONS, nearestTsconfig, } from "./project.js";
|
|
8
|
+
import { IMPORTER_MAX_FILES, IMPORTER_MAX_MS } from "./importers.js";
|
|
9
|
+
import { kindLabel, readJsonc } from "./workspace.js";
|
|
10
|
+
/** Importer hops: refs/callers need the declaring file's importers (barrels free), impact two levels. */
|
|
11
|
+
export const REFS_HOPS = 1;
|
|
12
|
+
export const IMPACT_HOPS = 2;
|
|
13
|
+
const indexes = new WeakMap();
|
|
14
|
+
/** The workspace importer index (in memory for this process only); `extra` = paths that resolve although absent (deleted files). */
|
|
15
|
+
export function importerIndexFor(index, workspace, extra = []) {
|
|
16
|
+
const key = [...extra].sort().join("\n");
|
|
17
|
+
let byKey = indexes.get(workspace);
|
|
18
|
+
if (!byKey)
|
|
19
|
+
indexes.set(workspace, (byKey = new Map()));
|
|
20
|
+
let built = byKey.get(key);
|
|
21
|
+
if (!built) {
|
|
22
|
+
const resolveSpec = createResolver(workspace, new Set([...workspace.sourceFiles, ...extra]));
|
|
23
|
+
built = buildImporterIndex(workspace.sourceFiles, (f) => index.text(f), resolveSpec, { maxFiles: IMPORTER_MAX_FILES, maxMs: IMPORTER_MAX_MS });
|
|
24
|
+
byKey.set(key, built);
|
|
25
|
+
}
|
|
26
|
+
return built;
|
|
27
|
+
}
|
|
28
|
+
/** Options that would make a subgraph program error on files outside one project's layout. */
|
|
29
|
+
const DROPPED_OPTIONS = ["composite", "incremental", "tsBuildInfoFile", "rootDir", "rootDirs", "outDir", "outFile", "declaration", "declarationMap", "emitDeclarationOnly", "sourceMap"];
|
|
30
|
+
/** The seed's nearest tsconfig (its referenced config that includes the seed, for a references-only shell) + workspace `paths`. */
|
|
31
|
+
export function compilerOptionsFor(seed, workspace) {
|
|
32
|
+
let options;
|
|
33
|
+
const tsconfig = nearestTsconfig(seed, workspace.root);
|
|
34
|
+
if (tsconfig) {
|
|
35
|
+
const configs = expandTsconfig(tsconfig);
|
|
36
|
+
const picked = (configs.find((c) => c.fileNames.includes(seed)) ?? configs[0])?.options;
|
|
37
|
+
// Importers include spec files: give them the spec config's globals too.
|
|
38
|
+
const types = configs.some((c) => c.options.types === undefined) ? undefined : [...new Set(configs.flatMap((c) => c.options.types ?? []))];
|
|
39
|
+
options = picked && types ? { ...picked, types } : picked;
|
|
40
|
+
}
|
|
41
|
+
if (!options) {
|
|
42
|
+
const base = join(workspace.root, "tsconfig.base.json");
|
|
43
|
+
const raw = existsSync(base) ? readJsonc(base)?.["compilerOptions"] : undefined;
|
|
44
|
+
options = raw ? { ...IMPLICIT_COMPILER_OPTIONS, ...ts.convertCompilerOptionsFromJson(raw, dirname(base), base).options } : { ...IMPLICIT_COMPILER_OPTIONS };
|
|
45
|
+
}
|
|
46
|
+
const merged = { ...options, noEmit: true, skipLibCheck: true };
|
|
47
|
+
for (const key of DROPPED_OPTIONS)
|
|
48
|
+
delete merged[key];
|
|
49
|
+
if (!merged.paths && workspace.paths) {
|
|
50
|
+
merged.paths = workspace.paths.paths;
|
|
51
|
+
// Internal option TypeScript sets from the tsconfig that declares `paths`; avoids the deprecated baseUrl.
|
|
52
|
+
merged["pathsBasePath"] = workspace.paths.base;
|
|
53
|
+
}
|
|
54
|
+
// Workspace packages resolve to their sources, not to a built `dist` behind a node_modules link.
|
|
55
|
+
const resolveSpec = createResolver({ packages: workspace.packages }, new Set(workspace.sourceFiles));
|
|
56
|
+
const packagePaths = {};
|
|
57
|
+
for (const pkg of workspace.packages) {
|
|
58
|
+
for (const sub of Object.keys(pkg.exports)) {
|
|
59
|
+
if (sub.includes("*"))
|
|
60
|
+
continue;
|
|
61
|
+
const spec = sub === "." ? pkg.name : `${pkg.name}/${sub.slice(2)}`;
|
|
62
|
+
if (merged.paths?.[spec])
|
|
63
|
+
continue;
|
|
64
|
+
const target = resolveSpec(spec, workspace.root);
|
|
65
|
+
if (target)
|
|
66
|
+
packagePaths[spec] = [target];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (Object.keys(packagePaths).length > 0) {
|
|
70
|
+
if (!merged.paths)
|
|
71
|
+
merged["pathsBasePath"] = workspace.root;
|
|
72
|
+
merged.paths = { ...packagePaths, ...merged.paths };
|
|
73
|
+
}
|
|
74
|
+
return merged;
|
|
75
|
+
}
|
|
76
|
+
/** Root files and options of the subgraph program around `seeds`. */
|
|
77
|
+
export function planScope(index, seeds, hops, extra = []) {
|
|
78
|
+
const scope = index.project.scope;
|
|
79
|
+
const { workspace } = scope;
|
|
80
|
+
const importers = hops > 0 ? importerIndexFor(index, workspace, extra) : undefined;
|
|
81
|
+
const closure = importers ? importerClosure(importers, [...seeds, ...extra], hops) : { importers: [], barrels: [] };
|
|
82
|
+
const seedFiles = seeds.filter((s) => existsSync(s));
|
|
83
|
+
const rootNames = [...new Set([...seedFiles, ...closure.importers])];
|
|
84
|
+
const options = compilerOptionsFor(seedFiles[0] ?? seeds[0] ?? join(workspace.root, "index.ts"), workspace);
|
|
85
|
+
if (rootNames.some((f) => /\.[mc]?jsx?$/.test(f))) {
|
|
86
|
+
options.allowJs = true;
|
|
87
|
+
options.checkJs = false;
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
program: { rootNames, options, currentDirectory: workspace.root },
|
|
91
|
+
info: {
|
|
92
|
+
line: `${kindLabel(scope.kind)} - ${rootNames.length} of ${workspace.sourceFiles.length} files (seeds ${seedFiles.length}; importers ${closure.importers.length})`,
|
|
93
|
+
partial: importers?.partial ?? false,
|
|
94
|
+
scanned: importers?.scanned ?? 0,
|
|
95
|
+
total: workspace.sourceFiles.length,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** A LanguageService over the Project, or over the importer subgraph of `seeds` when the Project is scoped. */
|
|
100
|
+
export function semanticFor(ctx, seeds, hops, extra = []) {
|
|
101
|
+
if (!ctx.index.project.scope)
|
|
102
|
+
return { semantic: createSemanticProject(ctx.index) };
|
|
103
|
+
const { program, info } = planScope(ctx.index, seeds, hops, extra);
|
|
104
|
+
return { semantic: createSemanticProject(ctx.index, program), scope: info };
|
|
105
|
+
}
|
|
106
|
+
/** `scope` / `scope_partial` fields for a semantic command's output. */
|
|
107
|
+
export function scopeFields(scope) {
|
|
108
|
+
if (!scope)
|
|
109
|
+
return {};
|
|
110
|
+
return { scope: scope.line, ...(scope.partial ? { scope_partial: true } : {}) };
|
|
111
|
+
}
|
|
112
|
+
/** Help line when the importer scan stopped early. */
|
|
113
|
+
export function scopeHelp(scope) {
|
|
114
|
+
if (!scope?.partial)
|
|
115
|
+
return [];
|
|
116
|
+
return [
|
|
117
|
+
`Importer scan stopped after ${scope.scanned} of ${scope.total} files (cap ${IMPORTER_MAX_FILES} files or ${IMPORTER_MAX_MS / 1000} s) - importers in the rest are missing; run with -P <tsconfig> for one whole project`,
|
|
118
|
+
];
|
|
119
|
+
}
|