@esportsplus/typescript 0.31.0 → 0.31.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.
- package/README.md +86 -0
- package/bin/tsc-lsp +3 -0
- package/build/cli/diagnostics.d.ts +5 -1
- package/build/cli/diagnostics.js +13 -8
- package/build/cli/tsc.d.ts +3 -1
- package/build/cli/tsc.js +77 -87
- package/build/compiler/coordinator.d.ts +1 -2
- package/build/compiler/coordinator.js +33 -22
- package/build/compiler/imports.d.ts +2 -0
- package/build/compiler/imports.js +9 -2
- package/build/compiler/language-service.d.ts +15 -4
- package/build/compiler/language-service.js +56 -24
- package/build/compiler/plugins/vite.js +7 -5
- package/build/compiler/sourcemap.d.ts +3 -1
- package/build/compiler/sourcemap.js +23 -5
- package/build/jsonc.d.ts +2 -0
- package/build/jsonc.js +85 -0
- package/build/lsp/bin.d.ts +1 -0
- package/build/lsp/bin.js +2 -0
- package/build/lsp/diagnostics.d.ts +10 -0
- package/build/lsp/diagnostics.js +69 -0
- package/build/lsp/index.d.ts +2 -0
- package/build/lsp/index.js +2 -0
- package/build/lsp/server.d.ts +4 -0
- package/build/lsp/server.js +130 -0
- package/build/lsp/workspace.d.ts +14 -0
- package/build/lsp/workspace.js +46 -0
- package/build/probe/adapter.d.ts +14 -0
- package/build/probe/adapter.js +42 -0
- package/build/probe/async/channel.d.ts +4 -0
- package/build/probe/async/channel.js +560 -0
- package/build/probe/async/value.d.ts +9 -0
- package/build/probe/async/value.js +16 -0
- package/build/probe/channels.d.ts +4 -0
- package/build/probe/channels.js +16 -0
- package/build/probe/exceptions/channel.d.ts +5 -0
- package/build/probe/exceptions/channel.js +657 -0
- package/build/probe/exceptions/jsdoc.d.ts +8 -0
- package/build/probe/exceptions/jsdoc.js +34 -0
- package/build/probe/exceptions/value.d.ts +39 -0
- package/build/probe/exceptions/value.js +158 -0
- package/build/probe/kernel/analyze.d.ts +8 -0
- package/build/probe/kernel/analyze.js +68 -0
- package/build/probe/kernel/ast.d.ts +11 -0
- package/build/probe/kernel/ast.js +49 -0
- package/build/probe/kernel/config.d.ts +5 -0
- package/build/probe/kernel/config.js +209 -0
- package/build/probe/kernel/fixpoint.d.ts +6 -0
- package/build/probe/kernel/fixpoint.js +206 -0
- package/build/probe/kernel/format.d.ts +4 -0
- package/build/probe/kernel/format.js +32 -0
- package/build/probe/kernel/graph.d.ts +4 -0
- package/build/probe/kernel/graph.js +507 -0
- package/build/probe/kernel/ids.d.ts +8 -0
- package/build/probe/kernel/ids.js +66 -0
- package/build/probe/kernel/program.d.ts +8 -0
- package/build/probe/kernel/program.js +13 -0
- package/build/probe/kernel/types.d.ts +134 -0
- package/build/probe/kernel/types.js +1 -0
- package/build/probe/overlay/base/async.jsonc +13 -0
- package/build/probe/overlay/base/exceptions.jsonc +54 -0
- package/build/probe/overlay/base/resources.jsonc +57 -0
- package/build/probe/overlay/load.d.ts +18 -0
- package/build/probe/overlay/load.js +302 -0
- package/build/probe/overlay/presets/express.jsonc +25 -0
- package/build/probe/overlay/presets/node.jsonc +17 -0
- package/build/probe/resources/channel.d.ts +4 -0
- package/build/probe/resources/channel.js +798 -0
- package/build/probe/resources/value.d.ts +9 -0
- package/build/probe/resources/value.js +36 -0
- package/build/tsconfig.d.ts +2 -0
- package/build/tsconfig.js +150 -0
- package/package.json +10 -4
- package/tsconfig.base.json +19 -0
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ Extends the TypeScript compiler with a plugin architecture for custom AST transf
|
|
|
18
18
|
- Vite plugin for dev/build integration
|
|
19
19
|
- CLI wrapper for `tsc` with automatic plugin detection
|
|
20
20
|
- Language service caching for incremental compilation
|
|
21
|
+
- `analyze` throw-safety analysis on the `tsc` passthrough and over LSP
|
|
21
22
|
|
|
22
23
|
## Usage
|
|
23
24
|
|
|
@@ -74,6 +75,83 @@ tsc
|
|
|
74
75
|
|
|
75
76
|
The CLI detects plugins in `tsconfig.json` `compilerOptions.plugins`, loads them, runs coordinated compilation, and automatically calls `tsc-alias` afterward. The package installs `tsc`/`tsc-alias` bins (also under the unambiguous `esportsplus-tsc`/`esportsplus-tsc-alias` names).
|
|
76
77
|
|
|
78
|
+
## analyze
|
|
79
|
+
|
|
80
|
+
`analyze` is a static analyzer for effects TypeScript's types leave implicit. It runs a per-function summary fixpoint over the call graph — so a finding points at the *consumer*, the call site where a caller should be careful — and is organised into **channels**, each checking one class of effect:
|
|
81
|
+
|
|
82
|
+
- **`exceptions`** — calls that may throw with no `catch` on the path to a handler boundary, `@throws` under-declaration, and `catch` rethrows that drop the caught error's `cause`. Carries the throw origin as related information.
|
|
83
|
+
- **`resources`** — acquired resources (timers, event listeners, file handles, sockets, `Disposable`s, …) that can leak: not released, transferred, or `using`-bound on every path — including throwing paths, which it derives from the `exceptions` channel's summaries.
|
|
84
|
+
- **`async`** — unbounded `Promise.all(…)`-style fan-out, orphaned promises whose rejections go unhandled, and awaited cancellable calls that drop an `AbortSignal` the function holds.
|
|
85
|
+
|
|
86
|
+
Only `exceptions` is on by default; enable the others per project. It rides the `tsc` passthrough (build/CI), and ships an LSP server so editors can render the same findings.
|
|
87
|
+
|
|
88
|
+
### Enable
|
|
89
|
+
|
|
90
|
+
Add an `analyze` entry to `compilerOptions.plugins`. Analysis covers every file the tsconfig includes (ignoring excludes); no config beyond the entry is required.
|
|
91
|
+
|
|
92
|
+
```jsonc
|
|
93
|
+
{
|
|
94
|
+
"compilerOptions": {
|
|
95
|
+
"plugins": [
|
|
96
|
+
{
|
|
97
|
+
"name": "ts-probe",
|
|
98
|
+
// Editor squiggle color; "warn" opts down. CLI/build ignore this.
|
|
99
|
+
"severity": "error",
|
|
100
|
+
// Fail the tsc/build run when there are findings.
|
|
101
|
+
"failOnFindings": true,
|
|
102
|
+
// Per-channel config. `enabled` and `dispatch` are recognised on
|
|
103
|
+
// every channel; other keys are that channel's own options.
|
|
104
|
+
"channels": {
|
|
105
|
+
"exceptions": {
|
|
106
|
+
"enabled": true,
|
|
107
|
+
// "consumers" (default): uncaught calls only.
|
|
108
|
+
// "cross-module": only when the throwing callee is in another package.
|
|
109
|
+
// "all": throws AND uncaught calls.
|
|
110
|
+
"report": "consumers",
|
|
111
|
+
// Flag `throw`s inside `catch` that drop the caught error's cause.
|
|
112
|
+
"errorCause": true
|
|
113
|
+
},
|
|
114
|
+
"resources": {
|
|
115
|
+
"enabled": true,
|
|
116
|
+
// Untrackable handling when a resource escapes local analysis:
|
|
117
|
+
// "optimist" assumes transfer (silent), "pessimist" reports it.
|
|
118
|
+
"dispatch": "optimist",
|
|
119
|
+
// Calls that take ownership of a passed resource argument.
|
|
120
|
+
"ownership": [{ "callee": "registerCleanup", "params": [0] }]
|
|
121
|
+
},
|
|
122
|
+
"async": {
|
|
123
|
+
"enabled": true,
|
|
124
|
+
"fanOut": "warn", // "off" | "warn" | "error"
|
|
125
|
+
"fanOutAllowLiteralUpTo": 16, // inline array/tuple size that is fine
|
|
126
|
+
"poolFunctions": ["p-limit", "p-map"] // sanctioned concurrency wrappers
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
// Model third-party throw behavior: "node", "express".
|
|
130
|
+
"presets": ["node"]
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### CLI / build
|
|
138
|
+
|
|
139
|
+
The `tsc` passthrough runs analyze after a successful compile and prints findings to stderr. With `failOnFindings: true`, a run with findings exits non-zero — drop it into CI as a gate.
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
tsc # compiles, resolves aliases, then reports analyze findings
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Editor (LSP)
|
|
146
|
+
|
|
147
|
+
The package ships a standalone language server (`esportsplus-tsc-lsp` bin, or the `@esportsplus/typescript/lsp` export) that publishes analyze findings over LSP. A client spawns it beside the native TypeScript server and merges both diagnostic streams; `severity` drives the squiggle color. Analysis runs against saved files on open and save. Beyond diagnostics it serves **hovers** (the finding plus its origin→boundary chain) and **quick-fixes** — `void`/`await` an orphaned promise, forward an `AbortSignal`, or fix a leaked handle by converting it to `using` or wrapping the region in `try/finally`.
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { startServer } from '@esportsplus/typescript/lsp';
|
|
151
|
+
|
|
152
|
+
startServer(); // stdio LSP server
|
|
153
|
+
```
|
|
154
|
+
|
|
77
155
|
## API
|
|
78
156
|
|
|
79
157
|
### `@esportsplus/typescript`
|
|
@@ -91,6 +169,14 @@ No exports. Import the TypeScript compiler API directly from `typescript/unstabl
|
|
|
91
169
|
| `plugin` | Built-in plugins (`tsc`, `vite`) |
|
|
92
170
|
| `uid` | Unique identifier generation |
|
|
93
171
|
|
|
172
|
+
### `@esportsplus/typescript/lsp`
|
|
173
|
+
|
|
174
|
+
| Export | Description |
|
|
175
|
+
|---|---|
|
|
176
|
+
| `startServer` | Start the stdio analyze LSP server |
|
|
177
|
+
| `createServer` | Wire the server onto an existing JSON-RPC connection |
|
|
178
|
+
| `AnalyzeWorkspace` | Long-lived native session that re-runs the analysis |
|
|
179
|
+
|
|
94
180
|
### Types
|
|
95
181
|
|
|
96
182
|
```typescript
|
package/bin/tsc-lsp
ADDED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { type Diagnostic } from 'typescript/unstable/sync';
|
|
2
|
+
import type { PositionMapping } from '../compiler/sourcemap.js';
|
|
2
3
|
declare const flatten: (diagnostic: Diagnostic, indent?: number) => string;
|
|
3
|
-
declare const format: (diagnostics: readonly Diagnostic[], root: string
|
|
4
|
+
declare const format: (diagnostics: readonly Diagnostic[], root: string, transformed?: Map<string, {
|
|
5
|
+
code: string;
|
|
6
|
+
mapping: PositionMapping;
|
|
7
|
+
}>) => string;
|
|
4
8
|
export { flatten, format };
|
package/build/cli/diagnostics.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync } from 'fs';
|
|
|
2
2
|
import { computeLineStarts } from 'typescript/unstable/ast/scanner';
|
|
3
3
|
import { DiagnosticCategory } from 'typescript/unstable/sync';
|
|
4
4
|
import path from 'path';
|
|
5
|
+
import { resolveOffset } from '../compiler/sourcemap.js';
|
|
5
6
|
const ANSI_BLUE = '\x1b[94m';
|
|
6
7
|
const ANSI_CYAN = '\x1b[96m';
|
|
7
8
|
const ANSI_GREY = '\x1b[90m';
|
|
@@ -34,16 +35,18 @@ function categoryLabel(category) {
|
|
|
34
35
|
return 'message';
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
|
-
function formatOne(diagnostic, root) {
|
|
38
|
+
function formatOne(diagnostic, root, sources, transformed) {
|
|
38
39
|
let category = categoryLabel(diagnostic.category), code = `${ANSI_GREY}TS${diagnostic.code}${ANSI_RESET}`, color = categoryColor(diagnostic.category), message = flatten(diagnostic);
|
|
39
40
|
if (diagnostic.fileName === undefined) {
|
|
40
41
|
return `${color}${category}${ANSI_RESET} ${code}: ${message}`;
|
|
41
42
|
}
|
|
42
|
-
let location = `${ANSI_CYAN}${path.relative(root, diagnostic.fileName).replace(BACKSLASH_REGEX, '/')}${ANSI_RESET}`,
|
|
43
|
+
let location = `${ANSI_CYAN}${path.relative(root, diagnostic.fileName).replace(BACKSLASH_REGEX, '/')}${ANSI_RESET}`, entry = transformed?.get(diagnostic.fileName.replace(BACKSLASH_REGEX, '/')), end = entry ? resolveOffset(entry.mapping, diagnostic.end) : diagnostic.end, pos = entry ? resolveOffset(entry.mapping, diagnostic.pos) : diagnostic.pos, sourceInfo = sources.get(diagnostic.fileName) ?? readSourceInfo(diagnostic.fileName);
|
|
44
|
+
sources.set(diagnostic.fileName, sourceInfo);
|
|
45
|
+
const text = sourceInfo.text;
|
|
43
46
|
if (text === undefined) {
|
|
44
47
|
return `${location} - ${color}${category}${ANSI_RESET} ${code}: ${message}`;
|
|
45
48
|
}
|
|
46
|
-
let lineStarts =
|
|
49
|
+
let lineStarts = sourceInfo.lineStarts, line = lineOfPosition(lineStarts, pos), character = pos - lineStarts[line], lineEnd = line + 1 < lineStarts.length ? lineStarts[line + 1] : text.length, source = text.slice(lineStarts[line], lineEnd).replace(TRAILING_NEWLINE_REGEX, ''), width = Math.max(1, Math.min(end, lineEnd) - pos), underline = `${' '.repeat(character)}${color}${'~'.repeat(width)}${ANSI_RESET}`, header = `${location}:${ANSI_YELLOW}${line + 1}${ANSI_RESET}:${ANSI_YELLOW}${character + 1}${ANSI_RESET} - ${color}${category}${ANSI_RESET} ${code}: ${message}`;
|
|
47
50
|
return `${header}\n\n${source}\n${underline}`;
|
|
48
51
|
}
|
|
49
52
|
function lineOfPosition(lineStarts, position) {
|
|
@@ -59,12 +62,13 @@ function lineOfPosition(lineStarts, position) {
|
|
|
59
62
|
}
|
|
60
63
|
return high < 0 ? 0 : high;
|
|
61
64
|
}
|
|
62
|
-
function
|
|
65
|
+
function readSourceInfo(fileName) {
|
|
63
66
|
try {
|
|
64
|
-
|
|
67
|
+
const text = readFileSync(fileName, 'utf8');
|
|
68
|
+
return { lineStarts: computeLineStarts(text), text };
|
|
65
69
|
}
|
|
66
70
|
catch {
|
|
67
|
-
return undefined;
|
|
71
|
+
return { lineStarts: [], text: undefined };
|
|
68
72
|
}
|
|
69
73
|
}
|
|
70
74
|
const flatten = (diagnostic, indent = 0) => {
|
|
@@ -80,10 +84,11 @@ const flatten = (diagnostic, indent = 0) => {
|
|
|
80
84
|
}
|
|
81
85
|
return result;
|
|
82
86
|
};
|
|
83
|
-
const format = (diagnostics, root) => {
|
|
87
|
+
const format = (diagnostics, root, transformed) => {
|
|
84
88
|
let parts = [];
|
|
89
|
+
const sources = new Map();
|
|
85
90
|
for (let i = 0, n = diagnostics.length; i < n; i++) {
|
|
86
|
-
parts.push(formatOne(diagnostics[i], root));
|
|
91
|
+
parts.push(formatOne(diagnostics[i], root, sources, transformed));
|
|
87
92
|
}
|
|
88
93
|
return parts.join('\n\n');
|
|
89
94
|
};
|
package/build/cli/tsc.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Plugin } from '../compiler/types.js';
|
|
2
2
|
import { API } from 'typescript/unstable/sync';
|
|
3
|
+
import { stripJsonc } from '../jsonc.js';
|
|
3
4
|
type PluginConfig = {
|
|
4
5
|
transform: string;
|
|
5
6
|
};
|
|
@@ -13,6 +14,7 @@ declare function isPlugin(value: unknown): value is Plugin;
|
|
|
13
14
|
declare function loadPlugins(configs: PluginConfig[], root: string): Promise<Plugin[]>;
|
|
14
15
|
declare function main(): void;
|
|
15
16
|
declare function normalizePath(fileName: string): string;
|
|
17
|
+
declare function projectPath(args: string[]): string | null;
|
|
16
18
|
declare function resolvePluginConfigs(tsconfig: string): PluginConfig[];
|
|
17
19
|
declare function runTscAlias(args: string[]): Promise<number>;
|
|
18
|
-
export { build, classifyFlags, isPlugin, loadPlugins, main, normalizePath, resolvePluginConfigs, runTscAlias };
|
|
20
|
+
export { build, classifyFlags, isPlugin, loadPlugins, main, normalizePath, projectPath, resolvePluginConfigs, runTscAlias, stripJsonc };
|
package/build/cli/tsc.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DiagnosticCategory } from 'typescript/unstable/sync';
|
|
2
2
|
import { createRequire } from 'module';
|
|
3
3
|
import { format } from './diagnostics.js';
|
|
4
4
|
import { PACKAGE_NAME } from '../constants.js';
|
|
@@ -9,30 +9,36 @@ import fs from 'fs';
|
|
|
9
9
|
import languageService from '../compiler/language-service.js';
|
|
10
10
|
import path from 'path';
|
|
11
11
|
import sourcemap from '../compiler/sourcemap.js';
|
|
12
|
+
import { analyze, analyzeProgram } from '../probe/kernel/analyze.js';
|
|
13
|
+
import { formatDiagnostics } from '../probe/kernel/format.js';
|
|
14
|
+
import { loadConfigFromTsconfig } from '../probe/kernel/config.js';
|
|
15
|
+
import { stripJsonc } from '../jsonc.js';
|
|
16
|
+
import { readPlugins } from '../tsconfig.js';
|
|
12
17
|
const BACKSLASH_REGEX = /\\/g;
|
|
13
18
|
const INFORMATIONAL_FLAGS = new Set(['--help', '--init', '--showConfig', '--version', '-h', '-v']);
|
|
14
19
|
const NO_EMIT_FLAGS = new Set(['--noEmit', '-noEmit']);
|
|
15
20
|
const WATCH_FLAGS = new Set(['--watch', '-w']);
|
|
16
|
-
let require = createRequire(import.meta.url), skipFlags = new Set([
|
|
21
|
+
let require = createRequire(import.meta.url), skipFlags = new Set([...INFORMATIONAL_FLAGS, ...NO_EMIT_FLAGS]);
|
|
17
22
|
async function build(tsconfig, pluginConfigs, instance, noEmit = false) {
|
|
18
|
-
let root = path.dirname(path.resolve(tsconfig)), owned = instance === undefined, api =
|
|
23
|
+
let opened = instance === undefined ? languageService.open(tsconfig) : undefined, root = path.dirname(path.resolve(tsconfig)), owned = instance === undefined, api = opened?.api ?? instance, snapshot = opened?.snapshot ?? api.updateSnapshot({ openProjects: [tsconfig] }), project = opened?.project ?? snapshot.getProject(tsconfig);
|
|
19
24
|
if (!project) {
|
|
20
|
-
teardown(snapshot,
|
|
25
|
+
teardown(snapshot, tsconfig, owned);
|
|
21
26
|
throw new Error(`${PACKAGE_NAME}: project not found for ${tsconfig}`);
|
|
22
27
|
}
|
|
23
28
|
let configDiagnostics = project.program.getConfigFileParsingDiagnostics();
|
|
24
29
|
if (configDiagnostics.some((diagnostic) => diagnostic.category === DiagnosticCategory.Error)) {
|
|
25
30
|
console.error(format(configDiagnostics, root));
|
|
26
|
-
teardown(snapshot,
|
|
31
|
+
teardown(snapshot, tsconfig, owned);
|
|
27
32
|
process.exit(1);
|
|
28
33
|
}
|
|
29
34
|
let { fileNames, options } = api.parseConfigFile(tsconfig), plugins, shared = new Map(), transformedFiles = new Map();
|
|
35
|
+
runAnalyze(tsconfig, project.program, project.checker);
|
|
30
36
|
try {
|
|
31
37
|
plugins = await loadPlugins(pluginConfigs, root);
|
|
32
38
|
}
|
|
33
39
|
catch (error) {
|
|
34
40
|
console.error(error instanceof Error ? error.message : String(error));
|
|
35
|
-
teardown(snapshot,
|
|
41
|
+
teardown(snapshot, tsconfig, owned);
|
|
36
42
|
process.exit(1);
|
|
37
43
|
}
|
|
38
44
|
for (let i = 0, n = fileNames.length; i < n; i++) {
|
|
@@ -40,15 +46,14 @@ async function build(tsconfig, pluginConfigs, instance, noEmit = false) {
|
|
|
40
46
|
if (!sourceFile) {
|
|
41
47
|
continue;
|
|
42
48
|
}
|
|
43
|
-
let result = coordinator.transform(plugins, sourceFile.getFullText(), sourceFile, { checker: project.checker, program: project.program }, root, shared);
|
|
49
|
+
let result = coordinator.transform(plugins, sourceFile.getFullText(), sourceFile, { checker: project.checker, configPath: tsconfig, program: project.program }, root, shared);
|
|
44
50
|
if (result.changed) {
|
|
45
51
|
transformedFiles.set(normalizePath(fileName), { code: result.code, mapping: result.map });
|
|
46
52
|
}
|
|
47
53
|
}
|
|
48
|
-
let program =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
54
|
+
let program = transformedFiles.size === 0
|
|
55
|
+
? project.program
|
|
56
|
+
: languageService.updateMany(tsconfig, new Map([...transformedFiles].map(([fileName, entry]) => [fileName, entry.code]))).program;
|
|
52
57
|
let diagnostics = [
|
|
53
58
|
...program.getConfigFileParsingDiagnostics(),
|
|
54
59
|
...program.getSyntacticDiagnostics(),
|
|
@@ -58,18 +63,18 @@ async function build(tsconfig, pluginConfigs, instance, noEmit = false) {
|
|
|
58
63
|
...program.getProgramDiagnostics()
|
|
59
64
|
];
|
|
60
65
|
if (diagnostics.length > 0) {
|
|
61
|
-
console.error(format(diagnostics, root));
|
|
66
|
+
console.error(format(diagnostics, root, transformedFiles));
|
|
62
67
|
}
|
|
63
68
|
if (diagnostics.some((diagnostic) => diagnostic.category === DiagnosticCategory.Error)) {
|
|
64
|
-
teardown(snapshot,
|
|
69
|
+
teardown(snapshot, tsconfig, owned);
|
|
65
70
|
process.exit(1);
|
|
66
71
|
}
|
|
67
72
|
if (noEmit) {
|
|
68
|
-
teardown(snapshot,
|
|
73
|
+
teardown(snapshot, tsconfig, owned);
|
|
69
74
|
process.exit(0);
|
|
70
75
|
}
|
|
71
76
|
let code = await emit(tsconfig, fileNames, transformedFiles, root, options);
|
|
72
|
-
teardown(snapshot,
|
|
77
|
+
teardown(snapshot, tsconfig, owned);
|
|
73
78
|
if (code !== 0) {
|
|
74
79
|
process.exit(code);
|
|
75
80
|
}
|
|
@@ -187,36 +192,6 @@ async function emit(tsconfig, fileNames, transformedFiles, root, options) {
|
|
|
187
192
|
fs.rmSync(mirror, { force: true, recursive: true });
|
|
188
193
|
}
|
|
189
194
|
}
|
|
190
|
-
function extendsTarget(specifier, fromDir) {
|
|
191
|
-
if (typeof specifier !== 'string') {
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
if (specifier.startsWith('.')) {
|
|
195
|
-
let resolved = path.resolve(fromDir, specifier);
|
|
196
|
-
if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) {
|
|
197
|
-
return resolved;
|
|
198
|
-
}
|
|
199
|
-
if (fs.existsSync(resolved + '.json')) {
|
|
200
|
-
return resolved + '.json';
|
|
201
|
-
}
|
|
202
|
-
let nested = path.join(resolved, 'tsconfig.json');
|
|
203
|
-
if (fs.existsSync(nested)) {
|
|
204
|
-
return nested;
|
|
205
|
-
}
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
try {
|
|
209
|
-
return require.resolve(specifier, { paths: [fromDir] });
|
|
210
|
-
}
|
|
211
|
-
catch {
|
|
212
|
-
try {
|
|
213
|
-
return require.resolve(specifier + '/tsconfig.json', { paths: [fromDir] });
|
|
214
|
-
}
|
|
215
|
-
catch {
|
|
216
|
-
return null;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
195
|
function isPlugin(value) {
|
|
221
196
|
return typeof value === 'object' && value !== null && 'transform' in value && typeof value.transform === 'function';
|
|
222
197
|
}
|
|
@@ -254,22 +229,24 @@ async function loadPlugins(configs, root) {
|
|
|
254
229
|
return plugins;
|
|
255
230
|
}
|
|
256
231
|
function main() {
|
|
257
|
-
let tsconfig = languageService.findConfig(process.cwd());
|
|
232
|
+
let args = process.argv.slice(2), tsconfig = projectPath(args) ?? languageService.findConfig(process.cwd());
|
|
258
233
|
if (!tsconfig) {
|
|
259
234
|
return passthrough();
|
|
260
235
|
}
|
|
236
|
+
let flags = classifyFlags(args);
|
|
261
237
|
let pluginConfigs = resolvePluginConfigs(tsconfig);
|
|
262
238
|
if (pluginConfigs.length === 0) {
|
|
239
|
+
if (!flags.informational && !flags.watch) {
|
|
240
|
+
runAnalyze(tsconfig);
|
|
241
|
+
}
|
|
263
242
|
return passthrough();
|
|
264
243
|
}
|
|
265
|
-
let flags = classifyFlags(process.argv.slice(2));
|
|
266
244
|
if (flags.informational) {
|
|
267
245
|
return passthrough();
|
|
268
246
|
}
|
|
269
247
|
if (flags.watch) {
|
|
270
248
|
console.error(`${PACKAGE_NAME}: --watch is not supported on the transformer plugin path; run a one-shot build or use real tsc directly`);
|
|
271
249
|
process.exit(1);
|
|
272
|
-
return;
|
|
273
250
|
}
|
|
274
251
|
console.log(`${PACKAGE_NAME}: found ${pluginConfigs.length} transformer plugin(s), using coordinated build...`);
|
|
275
252
|
build(tsconfig, pluginConfigs, undefined, flags.noEmit).catch((err) => {
|
|
@@ -280,6 +257,49 @@ function main() {
|
|
|
280
257
|
function normalizePath(fileName) {
|
|
281
258
|
return path.resolve(fileName).replace(BACKSLASH_REGEX, '/');
|
|
282
259
|
}
|
|
260
|
+
function projectPath(args) {
|
|
261
|
+
for (let i = 0, n = args.length; i < n; i++) {
|
|
262
|
+
let arg = args[i], value;
|
|
263
|
+
if (arg === '-p' || arg === '--project') {
|
|
264
|
+
value = args[i + 1];
|
|
265
|
+
}
|
|
266
|
+
else if (arg.startsWith('--project=')) {
|
|
267
|
+
value = arg.slice('--project='.length);
|
|
268
|
+
}
|
|
269
|
+
if (value !== undefined) {
|
|
270
|
+
let resolved = path.resolve(value);
|
|
271
|
+
return fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()
|
|
272
|
+
? path.join(resolved, 'tsconfig.json')
|
|
273
|
+
: resolved;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
function runAnalyze(tsconfig, program, checker) {
|
|
279
|
+
let config;
|
|
280
|
+
try {
|
|
281
|
+
config = loadConfigFromTsconfig(tsconfig);
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
console.error(`${PACKAGE_NAME}: analyze config error: ${error instanceof Error ? error.message : String(error)}`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (!config) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
let result = program && checker ? analyzeProgram(program, checker, config) : analyze(config);
|
|
292
|
+
if (result.diagnostics.length > 0) {
|
|
293
|
+
console.error(formatDiagnostics(result.diagnostics, config.projectRoot));
|
|
294
|
+
if (config.failOnFindings) {
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
console.error(`${PACKAGE_NAME}: analyze failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
283
303
|
function passthrough() {
|
|
284
304
|
let args = process.argv.slice(2), tsDir = path.dirname(require.resolve('typescript/package.json'));
|
|
285
305
|
spawn(process.execPath, [path.join(tsDir, 'lib', 'tsc.js'), ...args], { stdio: 'inherit' })
|
|
@@ -290,39 +310,8 @@ function passthrough() {
|
|
|
290
310
|
process.exit(code ?? 0);
|
|
291
311
|
});
|
|
292
312
|
}
|
|
293
|
-
function readPlugins(tsconfig, seen) {
|
|
294
|
-
let id = path.resolve(tsconfig);
|
|
295
|
-
if (seen.has(id)) {
|
|
296
|
-
return undefined;
|
|
297
|
-
}
|
|
298
|
-
seen.add(id);
|
|
299
|
-
let config;
|
|
300
|
-
try {
|
|
301
|
-
config = JSON.parse(stripJsonc(fs.readFileSync(id, 'utf8')));
|
|
302
|
-
}
|
|
303
|
-
catch {
|
|
304
|
-
return undefined;
|
|
305
|
-
}
|
|
306
|
-
let plugins;
|
|
307
|
-
if (config?.extends !== undefined) {
|
|
308
|
-
let bases = Array.isArray(config.extends) ? config.extends : [config.extends];
|
|
309
|
-
for (let i = 0, n = bases.length; i < n; i++) {
|
|
310
|
-
let target = extendsTarget(bases[i], path.dirname(id));
|
|
311
|
-
if (target) {
|
|
312
|
-
let inherited = readPlugins(target, seen);
|
|
313
|
-
if (inherited !== undefined) {
|
|
314
|
-
plugins = inherited;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
if (Array.isArray(config?.compilerOptions?.plugins)) {
|
|
320
|
-
plugins = config.compilerOptions.plugins;
|
|
321
|
-
}
|
|
322
|
-
return plugins;
|
|
323
|
-
}
|
|
324
313
|
function resolvePluginConfigs(tsconfig) {
|
|
325
|
-
let plugins = readPlugins(tsconfig
|
|
314
|
+
let plugins = readPlugins(tsconfig);
|
|
326
315
|
if (!Array.isArray(plugins)) {
|
|
327
316
|
return [];
|
|
328
317
|
}
|
|
@@ -345,7 +334,7 @@ function spawnTsc(tscJs, args) {
|
|
|
345
334
|
child.on('exit', (code) => resolve(code ?? 0));
|
|
346
335
|
});
|
|
347
336
|
}
|
|
348
|
-
function
|
|
337
|
+
function legacyStripJsonc(text) {
|
|
349
338
|
let escaped = false, inBlockComment = false, inLineComment = false, inString = false, stripped = '';
|
|
350
339
|
for (let i = 0, n = text.length; i < n; i++) {
|
|
351
340
|
let char = text[i], next = text[i + 1];
|
|
@@ -429,16 +418,17 @@ function stripJsonc(text) {
|
|
|
429
418
|
}
|
|
430
419
|
return result;
|
|
431
420
|
}
|
|
432
|
-
|
|
421
|
+
void legacyStripJsonc;
|
|
422
|
+
function teardown(snapshot, configPath, owned) {
|
|
423
|
+
if (owned) {
|
|
424
|
+
languageService.dispose(configPath);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
433
427
|
if (!snapshot.isDisposed()) {
|
|
434
428
|
snapshot.dispose();
|
|
435
429
|
}
|
|
436
|
-
if (owned) {
|
|
437
|
-
api.close();
|
|
438
|
-
}
|
|
439
|
-
languageService.dispose(root);
|
|
440
430
|
}
|
|
441
431
|
if (process.env.VITEST === undefined) {
|
|
442
432
|
main();
|
|
443
433
|
}
|
|
444
|
-
export { build, classifyFlags, isPlugin, loadPlugins, main, normalizePath, resolvePluginConfigs, runTscAlias };
|
|
434
|
+
export { build, classifyFlags, isPlugin, loadPlugins, main, normalizePath, projectPath, resolvePluginConfigs, runTscAlias, stripJsonc };
|
|
@@ -6,10 +6,10 @@ type CoordinatorResult = {
|
|
|
6
6
|
changed: boolean;
|
|
7
7
|
code: string;
|
|
8
8
|
map: PositionMapping;
|
|
9
|
-
sourceFile: SourceFile;
|
|
10
9
|
};
|
|
11
10
|
declare const transform: (plugins: Plugin[], code: string, file: SourceFile, project: {
|
|
12
11
|
checker: Checker;
|
|
12
|
+
configPath?: string;
|
|
13
13
|
program: Program;
|
|
14
14
|
}, root: string, shared: SharedContext) => {
|
|
15
15
|
changed: boolean;
|
|
@@ -17,7 +17,6 @@ declare const transform: (plugins: Plugin[], code: string, file: SourceFile, pro
|
|
|
17
17
|
map: {
|
|
18
18
|
generations: OffsetAnchor[][];
|
|
19
19
|
};
|
|
20
|
-
sourceFile: SourceFile;
|
|
21
20
|
};
|
|
22
21
|
declare const _default: {
|
|
23
22
|
transform: typeof transform;
|
|
@@ -26,18 +26,15 @@ function applyImports(code, file, intents) {
|
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
let
|
|
29
|
+
let edits = [], keys = [...merged.keys()];
|
|
30
30
|
for (let i = 0, n = keys.length; i < n; i++) {
|
|
31
|
-
let
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
if (i < n - 1) {
|
|
37
|
-
file = languageService.parse(file.fileName, code);
|
|
38
|
-
}
|
|
31
|
+
let result = modify(code, file, keys[i], merged.get(keys[i]));
|
|
32
|
+
edits.push(...result.edits);
|
|
33
|
+
}
|
|
34
|
+
if (edits.length === 0) {
|
|
35
|
+
return { batches: [], code };
|
|
39
36
|
}
|
|
40
|
-
return { batches, code };
|
|
37
|
+
return { batches: [{ before: code, edits }], code: replaceReverse(code, edits) };
|
|
41
38
|
}
|
|
42
39
|
function applyIntents(code, file, intents) {
|
|
43
40
|
if (intents.length === 0) {
|
|
@@ -84,6 +81,9 @@ function modify(code, file, pkg, options) {
|
|
|
84
81
|
return { code, edits: [] };
|
|
85
82
|
}
|
|
86
83
|
let { namespace } = options, add = options.add ? new Set(options.add) : null, found = imports.all(file, pkg);
|
|
84
|
+
if (namespace && !add && !options.remove && found.some(info => info.namespace === namespace)) {
|
|
85
|
+
return { code, edits: [] };
|
|
86
|
+
}
|
|
87
87
|
if (found.length === 0) {
|
|
88
88
|
let statements = [];
|
|
89
89
|
if (namespace) {
|
|
@@ -98,7 +98,7 @@ function modify(code, file, pkg, options) {
|
|
|
98
98
|
let newText = statements.join('\n') + '\n';
|
|
99
99
|
return { code: newText + code, edits: [{ end: 0, newText, start: 0 }] };
|
|
100
100
|
}
|
|
101
|
-
let remove = options.remove ? new Set(options.remove) : null, specifiers = new Set();
|
|
101
|
+
let remove = options.remove ? new Set(options.remove) : null, specifiers = new Set(), nonNamespace = found.filter(info => !info.namespace), existingNamespace = found.some(info => info.namespace === namespace), defaultImport = nonNamespace.find(info => info.defaultName), target = defaultImport ?? nonNamespace[0];
|
|
102
102
|
for (let i = 0, n = found.length; i < n; i++) {
|
|
103
103
|
for (let [name, alias] of found[i].specifiers) {
|
|
104
104
|
if (!remove || (!remove.has(name) && !remove.has(alias))) {
|
|
@@ -113,20 +113,34 @@ function modify(code, file, pkg, options) {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
let statements = [];
|
|
116
|
-
if (namespace) {
|
|
116
|
+
if (namespace && !existingNamespace) {
|
|
117
117
|
statements.push(`import * as ${namespace} from '${pkg}';`);
|
|
118
118
|
}
|
|
119
|
-
if (
|
|
119
|
+
if (target) {
|
|
120
|
+
let named = specifiers.size > 0 ? `{ ${[...specifiers].sort().join(', ')} }` : '', defaultName = target.defaultName, clause = defaultName && named ? `${defaultName}, ${named}` : defaultName ?? named;
|
|
121
|
+
if (clause) {
|
|
122
|
+
statements.push(`import ${clause} from '${pkg}';`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else if (specifiers.size > 0) {
|
|
120
126
|
statements.push(`import { ${[...specifiers].sort().join(', ')} } from '${pkg}';`);
|
|
121
127
|
}
|
|
122
128
|
let replacements = [];
|
|
123
129
|
for (let i = 0, n = found.length; i < n; i++) {
|
|
130
|
+
let info = found[i];
|
|
131
|
+
if (info.namespace) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
124
134
|
replacements.push({
|
|
125
|
-
end:
|
|
126
|
-
newText:
|
|
127
|
-
start:
|
|
135
|
+
end: info.end,
|
|
136
|
+
newText: info === target ? statements.join('\n') : info.defaultName ? `import ${info.defaultName} from '${pkg}';` : '',
|
|
137
|
+
start: info.start
|
|
128
138
|
});
|
|
129
139
|
}
|
|
140
|
+
if (!target && statements.length > 0) {
|
|
141
|
+
let first = found[0];
|
|
142
|
+
replacements.push({ end: first.start, newText: statements.join('\n') + '\n', start: first.start });
|
|
143
|
+
}
|
|
130
144
|
return { code: replaceReverse(code, replacements), edits: replacements };
|
|
131
145
|
}
|
|
132
146
|
function replaceReverse(code, replacements) {
|
|
@@ -150,7 +164,7 @@ function replaceReverse(code, replacements) {
|
|
|
150
164
|
}
|
|
151
165
|
const transform = (plugins, code, file, project, root, shared) => {
|
|
152
166
|
if (plugins.length === 0) {
|
|
153
|
-
return { changed: false, code, map: { generations: [] }
|
|
167
|
+
return { changed: false, code, map: { generations: [] } };
|
|
154
168
|
}
|
|
155
169
|
uid.scope(root, file.fileName, code);
|
|
156
170
|
let changed = false, currentCode = code, currentFile = file, currentProject = project, fileName = file.fileName, generations = [], last = plugins.length - 1;
|
|
@@ -199,15 +213,12 @@ const transform = (plugins, code, file, project, root, shared) => {
|
|
|
199
213
|
if (pluginChanged) {
|
|
200
214
|
changed = true;
|
|
201
215
|
if (i < last) {
|
|
202
|
-
currentProject = languageService.update(root, fileName, currentCode);
|
|
216
|
+
currentProject = languageService.update(project.configPath ?? languageService.findConfig(root) ?? root, fileName, currentCode);
|
|
203
217
|
currentFile = currentProject.program.getSourceFile(fileName) ??
|
|
204
218
|
languageService.parse(fileName, currentCode);
|
|
205
219
|
}
|
|
206
|
-
else {
|
|
207
|
-
currentFile = languageService.parse(fileName, currentCode);
|
|
208
|
-
}
|
|
209
220
|
}
|
|
210
221
|
}
|
|
211
|
-
return { changed, code: currentCode, map: { generations }
|
|
222
|
+
return { changed, code: currentCode, map: { generations } };
|
|
212
223
|
};
|
|
213
224
|
export default { transform };
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { Node, SourceFile } from 'typescript/unstable/ast';
|
|
2
2
|
import type { Checker } from 'typescript/unstable/sync';
|
|
3
3
|
type ImportInfo = {
|
|
4
|
+
defaultName?: string;
|
|
4
5
|
end: number;
|
|
6
|
+
namespace?: string;
|
|
5
7
|
specifiers: Map<string, string>;
|
|
6
8
|
typeOnly: Set<string>;
|
|
7
9
|
start: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SyntaxKind } from 'typescript/unstable/ast';
|
|
2
|
-
import { isIdentifier, isImportDeclaration, isNamedImports, isStringLiteral } from 'typescript/unstable/ast/is';
|
|
2
|
+
import { isIdentifier, isImportDeclaration, isNamedImports, isNamespaceImport, isStringLiteral } from 'typescript/unstable/ast/is';
|
|
3
3
|
import { SymbolFlags } from 'typescript/unstable/sync';
|
|
4
4
|
let cache = new WeakMap();
|
|
5
5
|
function fileNameMatchesPackage(fileName, pkg) {
|
|
@@ -27,7 +27,14 @@ const all = (file, pkg) => {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
imports.push({
|
|
30
|
+
imports.push({
|
|
31
|
+
defaultName: stmt.importClause?.name?.text,
|
|
32
|
+
end: stmt.end,
|
|
33
|
+
namespace: bindings && isNamespaceImport(bindings) ? bindings.name.text : undefined,
|
|
34
|
+
specifiers,
|
|
35
|
+
start: stmt.getStart(file),
|
|
36
|
+
typeOnly
|
|
37
|
+
});
|
|
31
38
|
}
|
|
32
39
|
return imports;
|
|
33
40
|
};
|