@esportsplus/typescript 0.24.2 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cli/tsc.js +58 -68
- package/build/compiler/coordinator.d.ts +12 -0
- package/build/compiler/coordinator.js +100 -0
- package/build/compiler/index.d.ts +1 -0
- package/build/compiler/index.js +1 -0
- package/build/compiler/plugins/index.d.ts +3 -5
- package/build/compiler/plugins/tsc.d.ts +4 -4
- package/build/compiler/plugins/tsc.js +4 -6
- package/build/compiler/plugins/vite.d.ts +3 -4
- package/build/compiler/plugins/vite.js +4 -4
- package/build/compiler/types.d.ts +25 -9
- package/package.json +1 -1
- package/src/cli/tsc.ts +83 -108
- package/src/compiler/coordinator.ts +184 -0
- package/src/compiler/index.ts +8 -7
- package/src/compiler/plugins/index.ts +3 -3
- package/src/compiler/plugins/tsc.ts +19 -10
- package/src/compiler/plugins/vite.ts +10 -13
- package/src/compiler/types.ts +59 -12
package/build/cli/tsc.js
CHANGED
|
@@ -3,9 +3,10 @@ import { createRequire } from 'module';
|
|
|
3
3
|
import { pathToFileURL } from 'url';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import ts from 'typescript';
|
|
6
|
+
import coordinator from '../compiler/coordinator.js';
|
|
6
7
|
const BACKSLASH_REGEX = /\\/g;
|
|
7
8
|
let require = createRequire(import.meta.url), skipFlags = new Set(['--help', '--init', '--noEmit', '--showConfig', '--version', '-h', '-noEmit', '-v']);
|
|
8
|
-
async function build(config, tsconfig,
|
|
9
|
+
async function build(config, tsconfig, pluginConfigs) {
|
|
9
10
|
let root = path.dirname(path.resolve(tsconfig)), parsed = ts.parseJsonConfigFileContent(config, ts.sys, root);
|
|
10
11
|
if (parsed.errors.length > 0) {
|
|
11
12
|
for (let i = 0, n = parsed.errors.length; i < n; i++) {
|
|
@@ -13,63 +14,50 @@ async function build(config, tsconfig, plugins) {
|
|
|
13
14
|
}
|
|
14
15
|
process.exit(1);
|
|
15
16
|
}
|
|
16
|
-
await loadPlugins(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
for (let j = 0, m = instances.length; j < m; j++) {
|
|
25
|
-
instances[j].analyze?.(sourceFile);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
let transformers = instances.map(i => i.transform);
|
|
29
|
-
for (let i = 0, n = parsed.fileNames.length; i < n; i++) {
|
|
30
|
-
let fileName = parsed.fileNames[i], sourceFile = program.getSourceFile(fileName);
|
|
31
|
-
if (!sourceFile) {
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
let result = ts.transform(sourceFile, transformers), transformed = result.transformed[0];
|
|
35
|
-
if (transformed !== sourceFile) {
|
|
36
|
-
transformedFiles.set(normalizePath(fileName), printer.printFile(transformed));
|
|
37
|
-
}
|
|
38
|
-
result.dispose();
|
|
17
|
+
let plugins = await loadPlugins(pluginConfigs, root);
|
|
18
|
+
let program = ts.createProgram(parsed.fileNames, parsed.options), shared = new Map(), transformedFiles = new Map();
|
|
19
|
+
for (let i = 0, n = parsed.fileNames.length; i < n; i++) {
|
|
20
|
+
let fileName = parsed.fileNames[i], sourceFile = program.getSourceFile(fileName);
|
|
21
|
+
if (!sourceFile) {
|
|
22
|
+
continue;
|
|
39
23
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
let transformed = transformedFiles.get(normalizePath(fileName));
|
|
44
|
-
if (transformed) {
|
|
45
|
-
return ts.createSourceFile(fileName, transformed, languageVersion, true);
|
|
46
|
-
}
|
|
47
|
-
return originalGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
|
|
48
|
-
};
|
|
49
|
-
customHost.readFile = (fileName) => {
|
|
50
|
-
return transformedFiles.get(normalizePath(fileName)) ?? originalReadFile(fileName);
|
|
51
|
-
};
|
|
52
|
-
program = ts.createProgram(parsed.fileNames, parsed.options, customHost);
|
|
24
|
+
let result = coordinator.transform(plugins, sourceFile.getFullText(), sourceFile, program, shared);
|
|
25
|
+
if (result.changed) {
|
|
26
|
+
transformedFiles.set(normalizePath(fileName), result.code);
|
|
53
27
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
28
|
+
}
|
|
29
|
+
if (transformedFiles.size > 0) {
|
|
30
|
+
let customHost = ts.createCompilerHost(parsed.options), originalGetSourceFile = customHost.getSourceFile.bind(customHost), originalReadFile = customHost.readFile.bind(customHost);
|
|
31
|
+
customHost.getSourceFile = (fileName, languageVersion, onError, shouldCreateNewSourceFile) => {
|
|
32
|
+
let transformed = transformedFiles.get(normalizePath(fileName));
|
|
33
|
+
if (transformed) {
|
|
34
|
+
return ts.createSourceFile(fileName, transformed, languageVersion, true);
|
|
35
|
+
}
|
|
36
|
+
return originalGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
|
|
37
|
+
};
|
|
38
|
+
customHost.readFile = (fileName) => {
|
|
39
|
+
return transformedFiles.get(normalizePath(fileName)) ?? originalReadFile(fileName);
|
|
40
|
+
};
|
|
41
|
+
program = ts.createProgram(parsed.fileNames, parsed.options, customHost);
|
|
42
|
+
}
|
|
43
|
+
let { diagnostics, emitSkipped } = program.emit();
|
|
44
|
+
diagnostics = ts.getPreEmitDiagnostics(program).concat(diagnostics);
|
|
45
|
+
if (diagnostics.length > 0) {
|
|
46
|
+
console.error(ts.formatDiagnosticsWithColorAndContext(diagnostics, {
|
|
47
|
+
getCanonicalFileName: (fileName) => fileName,
|
|
48
|
+
getCurrentDirectory: () => root,
|
|
49
|
+
getNewLine: () => '\n'
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
if (emitSkipped) {
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
return runTscAlias(process.argv.slice(2)).then((code) => process.exit(code));
|
|
68
56
|
}
|
|
69
|
-
async function loadPlugins(
|
|
70
|
-
let
|
|
71
|
-
for (let i = 0, n =
|
|
72
|
-
let
|
|
57
|
+
async function loadPlugins(configs, root) {
|
|
58
|
+
let plugins = [], promises = [];
|
|
59
|
+
for (let i = 0, n = configs.length; i < n; i++) {
|
|
60
|
+
let config = configs[i], pluginPath = config.transform;
|
|
73
61
|
if (pluginPath.startsWith('.')) {
|
|
74
62
|
pluginPath = pathToFileURL(path.resolve(root, pluginPath)).href;
|
|
75
63
|
}
|
|
@@ -77,20 +65,19 @@ async function loadPlugins(plugins, root) {
|
|
|
77
65
|
pluginPath = pathToFileURL(require.resolve(pluginPath, { paths: [root] })).href;
|
|
78
66
|
}
|
|
79
67
|
promises.push(import(pluginPath).then((module) => {
|
|
80
|
-
let
|
|
81
|
-
if (typeof
|
|
82
|
-
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
if (plugin.after) {
|
|
86
|
-
after.push(factory);
|
|
68
|
+
let plugin = module.default ?? module;
|
|
69
|
+
if (typeof plugin === 'function') {
|
|
70
|
+
plugin = plugin();
|
|
87
71
|
}
|
|
88
|
-
|
|
89
|
-
|
|
72
|
+
if (!plugin || typeof plugin.transform !== 'function') {
|
|
73
|
+
console.error(`Plugin ${config.transform}: invalid plugin format, expected { transform: Function }`);
|
|
74
|
+
return;
|
|
90
75
|
}
|
|
76
|
+
plugins.push(plugin);
|
|
91
77
|
}));
|
|
92
78
|
}
|
|
93
|
-
|
|
79
|
+
await Promise.all(promises);
|
|
80
|
+
return plugins;
|
|
94
81
|
}
|
|
95
82
|
function main() {
|
|
96
83
|
let tsconfig = ts.findConfigFile(process.cwd(), ts.sys.fileExists, 'tsconfig.json');
|
|
@@ -101,9 +88,12 @@ function main() {
|
|
|
101
88
|
if (error) {
|
|
102
89
|
return passthrough();
|
|
103
90
|
}
|
|
104
|
-
let
|
|
105
|
-
|
|
106
|
-
|
|
91
|
+
let pluginConfigs = config?.compilerOptions?.plugins?.filter((p) => typeof p === 'object' && p !== null && 'transform' in p) ?? [];
|
|
92
|
+
if (pluginConfigs.length === 0) {
|
|
93
|
+
return passthrough();
|
|
94
|
+
}
|
|
95
|
+
console.log(`Found ${pluginConfigs.length} transformer plugin(s), using coordinated build...`);
|
|
96
|
+
build(config, tsconfig, pluginConfigs).catch((err) => {
|
|
107
97
|
console.error(err);
|
|
108
98
|
process.exit(1);
|
|
109
99
|
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Plugin, SharedContext } from './types.js';
|
|
2
|
+
import { ts } from '../index.js';
|
|
3
|
+
type CoordinatorResult = {
|
|
4
|
+
changed: boolean;
|
|
5
|
+
code: string;
|
|
6
|
+
sourceFile: ts.SourceFile;
|
|
7
|
+
};
|
|
8
|
+
declare const _default: {
|
|
9
|
+
transform: (plugins: Plugin[], sourceCode: string, sourceFile: ts.SourceFile, program: ts.Program, shared: SharedContext) => CoordinatorResult;
|
|
10
|
+
};
|
|
11
|
+
export default _default;
|
|
12
|
+
export type { CoordinatorResult };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { ts } from '../index.js';
|
|
2
|
+
import code from './code.js';
|
|
3
|
+
import imports from './imports.js';
|
|
4
|
+
function applyImports(sourceCode, sourceFile, intents) {
|
|
5
|
+
let result = sourceCode;
|
|
6
|
+
for (let i = 0, n = intents.length; i < n; i++) {
|
|
7
|
+
let intent = intents[i];
|
|
8
|
+
result = imports.modify(result, sourceFile, intent.package, {
|
|
9
|
+
add: intent.add,
|
|
10
|
+
namespace: intent.namespace,
|
|
11
|
+
remove: intent.remove
|
|
12
|
+
});
|
|
13
|
+
if (i < n - 1) {
|
|
14
|
+
sourceFile = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
function applyIntents(sourceCode, sourceFile, intents) {
|
|
20
|
+
if (intents.length === 0) {
|
|
21
|
+
return sourceCode;
|
|
22
|
+
}
|
|
23
|
+
let replacements = intents.map(intent => ({
|
|
24
|
+
end: intent.node.end,
|
|
25
|
+
newText: intent.generate(sourceFile),
|
|
26
|
+
start: intent.node.getStart(sourceFile)
|
|
27
|
+
}));
|
|
28
|
+
return code.replaceReverse(sourceCode, replacements);
|
|
29
|
+
}
|
|
30
|
+
function applyPrepend(sourceCode, sourceFile, prepend) {
|
|
31
|
+
if (prepend.length === 0) {
|
|
32
|
+
return sourceCode;
|
|
33
|
+
}
|
|
34
|
+
let insertPos = findLastImportEnd(sourceFile), prependText = prepend.join('\n') + '\n';
|
|
35
|
+
if (insertPos === 0) {
|
|
36
|
+
return prependText + sourceCode;
|
|
37
|
+
}
|
|
38
|
+
return sourceCode.slice(0, insertPos) + '\n' + prependText + sourceCode.slice(insertPos);
|
|
39
|
+
}
|
|
40
|
+
function findLastImportEnd(sourceFile) {
|
|
41
|
+
let lastEnd = 0;
|
|
42
|
+
for (let i = 0, n = sourceFile.statements.length; i < n; i++) {
|
|
43
|
+
let stmt = sourceFile.statements[i];
|
|
44
|
+
if (ts.isImportDeclaration(stmt)) {
|
|
45
|
+
lastEnd = stmt.end;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return lastEnd;
|
|
52
|
+
}
|
|
53
|
+
function hasPattern(sourceCode, patterns) {
|
|
54
|
+
for (let i = 0, n = patterns.length; i < n; i++) {
|
|
55
|
+
if (sourceCode.indexOf(patterns[i]) !== -1) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const transform = (plugins, sourceCode, sourceFile, program, shared) => {
|
|
62
|
+
if (plugins.length === 0) {
|
|
63
|
+
return { changed: false, code: sourceCode, sourceFile };
|
|
64
|
+
}
|
|
65
|
+
let changed = false, currentCode = sourceCode, currentSourceFile = sourceFile;
|
|
66
|
+
for (let i = 0, n = plugins.length; i < n; i++) {
|
|
67
|
+
let plugin = plugins[i];
|
|
68
|
+
if (plugin.patterns && !hasPattern(currentCode, plugin.patterns)) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
let result = plugin.transform({
|
|
72
|
+
checker: program.getTypeChecker(),
|
|
73
|
+
code: currentCode,
|
|
74
|
+
program,
|
|
75
|
+
shared,
|
|
76
|
+
sourceFile: currentSourceFile
|
|
77
|
+
});
|
|
78
|
+
let hasChanges = (result.imports && result.imports.length > 0) ||
|
|
79
|
+
(result.prepend && result.prepend.length > 0) ||
|
|
80
|
+
(result.replacements && result.replacements.length > 0);
|
|
81
|
+
if (!hasChanges) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
changed = true;
|
|
85
|
+
if (result.replacements && result.replacements.length > 0) {
|
|
86
|
+
currentCode = applyIntents(currentCode, currentSourceFile, result.replacements);
|
|
87
|
+
currentSourceFile = ts.createSourceFile(currentSourceFile.fileName, currentCode, currentSourceFile.languageVersion, true);
|
|
88
|
+
}
|
|
89
|
+
if (result.prepend && result.prepend.length > 0) {
|
|
90
|
+
currentCode = applyPrepend(currentCode, currentSourceFile, result.prepend);
|
|
91
|
+
currentSourceFile = ts.createSourceFile(currentSourceFile.fileName, currentCode, currentSourceFile.languageVersion, true);
|
|
92
|
+
}
|
|
93
|
+
if (result.imports && result.imports.length > 0) {
|
|
94
|
+
currentCode = applyImports(currentCode, currentSourceFile, result.imports);
|
|
95
|
+
currentSourceFile = ts.createSourceFile(currentSourceFile.fileName, currentCode, currentSourceFile.languageVersion, true);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { changed, code: currentCode, sourceFile: currentSourceFile };
|
|
99
|
+
};
|
|
100
|
+
export default { transform };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * as ast from './ast/index.js';
|
|
2
2
|
export { default as code } from './code.js';
|
|
3
|
+
export { default as coordinator } from './coordinator.js';
|
|
3
4
|
export { default as imports } from './imports.js';
|
|
4
5
|
export { default as plugin } from './plugins/index.js';
|
|
5
6
|
export { default as program } from './program.js';
|
package/build/compiler/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * as ast from './ast/index.js';
|
|
2
2
|
export { default as code } from './code.js';
|
|
3
|
+
export { default as coordinator } from './coordinator.js';
|
|
3
4
|
export { default as imports } from './imports.js';
|
|
4
5
|
export { default as plugin } from './plugins/index.js';
|
|
5
6
|
export { default as program } from './program.js';
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
declare const _default: {
|
|
2
|
-
tsc: (
|
|
3
|
-
analyze: ((sourceFile: import("typescript").SourceFile) => void) | undefined;
|
|
2
|
+
tsc: (plugins: import("../types.js").Plugin[]) => (program: import("typescript").Program, shared: import("../types.js").SharedContext) => {
|
|
4
3
|
transform: import("typescript").TransformerFactory<import("typescript").SourceFile>;
|
|
5
4
|
};
|
|
6
|
-
vite: ({
|
|
7
|
-
analyze?: import("../index.js").AnalyzeFn;
|
|
5
|
+
vite: ({ name, onWatchChange, plugins }: {
|
|
8
6
|
name: string;
|
|
9
7
|
onWatchChange?: () => void;
|
|
10
|
-
|
|
8
|
+
plugins: import("../types.js").Plugin[];
|
|
11
9
|
}) => ({ root }?: {
|
|
12
10
|
root?: string;
|
|
13
11
|
}) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
analyze: ((sourceFile: ts.SourceFile) => void) | undefined;
|
|
1
|
+
import type { Plugin, SharedContext } from '../types.js';
|
|
2
|
+
import type ts from 'typescript';
|
|
3
|
+
type PluginInstance = {
|
|
5
4
|
transform: ts.TransformerFactory<ts.SourceFile>;
|
|
6
5
|
};
|
|
6
|
+
declare const _default: (plugins: Plugin[]) => (program: ts.Program, shared: SharedContext) => PluginInstance;
|
|
7
7
|
export default _default;
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import coordinator from '../coordinator.js';
|
|
2
|
+
export default (plugins) => {
|
|
3
|
+
return (program, shared) => {
|
|
3
4
|
return {
|
|
4
|
-
analyze: analyze
|
|
5
|
-
? (sourceFile) => analyze(sourceFile, program, context)
|
|
6
|
-
: undefined,
|
|
7
5
|
transform: (() => {
|
|
8
6
|
return (sourceFile) => {
|
|
9
|
-
let result = transform(sourceFile, program,
|
|
7
|
+
let result = coordinator.transform(plugins, sourceFile.getFullText(), sourceFile, program, shared);
|
|
10
8
|
return result.changed ? result.sourceFile : sourceFile;
|
|
11
9
|
};
|
|
12
10
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Plugin } from '../types.js';
|
|
2
2
|
type VitePlugin = {
|
|
3
3
|
configResolved: (config: unknown) => void;
|
|
4
4
|
enforce: 'pre';
|
|
@@ -10,12 +10,11 @@ type VitePlugin = {
|
|
|
10
10
|
watchChange: (id: string) => void;
|
|
11
11
|
};
|
|
12
12
|
type VitePluginOptions = {
|
|
13
|
-
analyze?: AnalyzeFn;
|
|
14
13
|
name: string;
|
|
15
14
|
onWatchChange?: () => void;
|
|
16
|
-
|
|
15
|
+
plugins: Plugin[];
|
|
17
16
|
};
|
|
18
|
-
declare const _default: ({
|
|
17
|
+
declare const _default: ({ name, onWatchChange, plugins }: VitePluginOptions) => ({ root }?: {
|
|
19
18
|
root?: string;
|
|
20
19
|
}) => VitePlugin;
|
|
21
20
|
export default _default;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { ts } from '../../index.js';
|
|
2
|
+
import coordinator from '../coordinator.js';
|
|
2
3
|
import program from '../program.js';
|
|
3
4
|
const FILE_REGEX = /\.[tj]sx?$/;
|
|
4
5
|
let contexts = new Map();
|
|
5
|
-
export default ({
|
|
6
|
+
export default ({ name, onWatchChange, plugins }) => {
|
|
6
7
|
return ({ root } = {}) => {
|
|
7
8
|
return {
|
|
8
9
|
configResolved(config) {
|
|
@@ -15,9 +16,8 @@ export default ({ analyze, name, onWatchChange, transform }) => {
|
|
|
15
16
|
return null;
|
|
16
17
|
}
|
|
17
18
|
try {
|
|
18
|
-
let
|
|
19
|
-
|
|
20
|
-
let result = transform(sourceFile, prog, context);
|
|
19
|
+
let prog = program.get(root || ''), shared = contexts.get(root || '') ?? contexts.set(root || '', new Map()).get(root || ''), sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true);
|
|
20
|
+
let result = coordinator.transform(plugins, code, sourceFile, prog, shared);
|
|
21
21
|
if (!result.changed) {
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import type ts from 'typescript';
|
|
2
|
-
type
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
type ImportIntent = {
|
|
3
|
+
add?: string[];
|
|
4
|
+
namespace?: string;
|
|
5
|
+
package: string;
|
|
6
|
+
remove?: string[];
|
|
7
7
|
};
|
|
8
|
+
type Plugin = {
|
|
9
|
+
patterns?: string[];
|
|
10
|
+
transform: (ctx: TransformContext) => TransformResult;
|
|
11
|
+
};
|
|
12
|
+
type PluginFactory = (options?: Record<string, unknown>) => Plugin;
|
|
8
13
|
type QuickCheckPattern = {
|
|
9
14
|
patterns?: string[];
|
|
10
15
|
regex?: RegExp;
|
|
@@ -16,10 +21,21 @@ type Range = {
|
|
|
16
21
|
type Replacement = Range & {
|
|
17
22
|
newText: string;
|
|
18
23
|
};
|
|
19
|
-
type
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
type ReplacementIntent = {
|
|
25
|
+
generate: (sourceFile: ts.SourceFile) => string;
|
|
26
|
+
node: ts.Node;
|
|
27
|
+
};
|
|
28
|
+
type SharedContext = Map<string, unknown>;
|
|
29
|
+
type TransformContext = {
|
|
30
|
+
checker: ts.TypeChecker;
|
|
22
31
|
code: string;
|
|
32
|
+
program: ts.Program;
|
|
33
|
+
shared: SharedContext;
|
|
23
34
|
sourceFile: ts.SourceFile;
|
|
24
35
|
};
|
|
25
|
-
|
|
36
|
+
type TransformResult = {
|
|
37
|
+
imports?: ImportIntent[];
|
|
38
|
+
prepend?: string[];
|
|
39
|
+
replacements?: ReplacementIntent[];
|
|
40
|
+
};
|
|
41
|
+
export type { ImportIntent, Plugin, PluginFactory, QuickCheckPattern, Range, Replacement, ReplacementIntent, SharedContext, TransformContext, TransformResult };
|
package/package.json
CHANGED
package/src/cli/tsc.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import { createRequire } from 'module';
|
|
3
3
|
import { pathToFileURL } from 'url';
|
|
4
|
-
|
|
5
4
|
import path from 'path';
|
|
6
5
|
import ts from 'typescript';
|
|
6
|
+
import coordinator from '~/compiler/coordinator';
|
|
7
|
+
import type { Plugin, SharedContext } from '~/compiler/types';
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
type PluginConfig = {
|
|
@@ -11,15 +12,6 @@ type PluginConfig = {
|
|
|
11
12
|
transform: string;
|
|
12
13
|
};
|
|
13
14
|
|
|
14
|
-
type PluginContext = Map<string, unknown>;
|
|
15
|
-
|
|
16
|
-
type PluginInstance = {
|
|
17
|
-
analyze?: (sourceFile: ts.SourceFile) => void;
|
|
18
|
-
transform: ts.TransformerFactory<ts.SourceFile>;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
type PluginFactory = (program: ts.Program, context: PluginContext) => PluginInstance;
|
|
22
|
-
|
|
23
15
|
|
|
24
16
|
const BACKSLASH_REGEX = /\\/g;
|
|
25
17
|
|
|
@@ -28,7 +20,7 @@ let require = createRequire(import.meta.url),
|
|
|
28
20
|
skipFlags = new Set(['--help', '--init', '--noEmit', '--showConfig', '--version', '-h', '-noEmit', '-v']);
|
|
29
21
|
|
|
30
22
|
|
|
31
|
-
async function build(config: object, tsconfig: string,
|
|
23
|
+
async function build(config: object, tsconfig: string, pluginConfigs: PluginConfig[]): Promise<void> {
|
|
32
24
|
let root = path.dirname(path.resolve(tsconfig)),
|
|
33
25
|
parsed = ts.parseJsonConfigFileContent(config, ts.sys, root);
|
|
34
26
|
|
|
@@ -42,110 +34,88 @@ async function build(config: object, tsconfig: string, plugins: PluginConfig[]):
|
|
|
42
34
|
process.exit(1);
|
|
43
35
|
}
|
|
44
36
|
|
|
45
|
-
await loadPlugins(
|
|
46
|
-
let context: PluginContext = new Map(),
|
|
47
|
-
printer = ts.createPrinter(),
|
|
48
|
-
program = ts.createProgram(parsed.fileNames, parsed.options),
|
|
49
|
-
transformedFiles = new Map<string, string>();
|
|
37
|
+
let plugins = await loadPlugins(pluginConfigs, root);
|
|
50
38
|
|
|
51
|
-
|
|
52
|
-
|
|
39
|
+
let program = ts.createProgram(parsed.fileNames, parsed.options),
|
|
40
|
+
shared: SharedContext = new Map(),
|
|
41
|
+
transformedFiles = new Map<string, string>();
|
|
53
42
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
sourceFile = program.getSourceFile(fileName);
|
|
43
|
+
for (let i = 0, n = parsed.fileNames.length; i < n; i++) {
|
|
44
|
+
let fileName = parsed.fileNames[i],
|
|
45
|
+
sourceFile = program.getSourceFile(fileName);
|
|
58
46
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
for (let j = 0, m = instances.length; j < m; j++) {
|
|
64
|
-
instances[j].analyze?.(sourceFile);
|
|
65
|
-
}
|
|
47
|
+
if (!sourceFile) {
|
|
48
|
+
continue;
|
|
66
49
|
}
|
|
67
50
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (!sourceFile) {
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
let result = ts.transform(sourceFile, transformers),
|
|
80
|
-
transformed = result.transformed[0];
|
|
81
|
-
|
|
82
|
-
if (transformed !== sourceFile) {
|
|
83
|
-
transformedFiles.set(normalizePath(fileName), printer.printFile(transformed));
|
|
84
|
-
}
|
|
51
|
+
let result = coordinator.transform(
|
|
52
|
+
plugins,
|
|
53
|
+
sourceFile.getFullText(),
|
|
54
|
+
sourceFile,
|
|
55
|
+
program,
|
|
56
|
+
shared
|
|
57
|
+
);
|
|
85
58
|
|
|
86
|
-
|
|
59
|
+
if (result.changed) {
|
|
60
|
+
transformedFiles.set(normalizePath(fileName), result.code);
|
|
87
61
|
}
|
|
62
|
+
}
|
|
88
63
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
64
|
+
if (transformedFiles.size > 0) {
|
|
65
|
+
let customHost = ts.createCompilerHost(parsed.options),
|
|
66
|
+
originalGetSourceFile = customHost.getSourceFile.bind(customHost),
|
|
67
|
+
originalReadFile = customHost.readFile.bind(customHost);
|
|
68
|
+
|
|
69
|
+
customHost.getSourceFile = (
|
|
70
|
+
fileName: string,
|
|
71
|
+
languageVersion: ts.ScriptTarget,
|
|
72
|
+
onError?: (message: string) => void,
|
|
73
|
+
shouldCreateNewSourceFile?: boolean
|
|
74
|
+
): ts.SourceFile | undefined => {
|
|
75
|
+
let transformed = transformedFiles.get(normalizePath(fileName));
|
|
76
|
+
|
|
77
|
+
if (transformed) {
|
|
78
|
+
return ts.createSourceFile(fileName, transformed, languageVersion, true);
|
|
79
|
+
}
|
|
105
80
|
|
|
106
|
-
|
|
107
|
-
|
|
81
|
+
return originalGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
|
|
82
|
+
};
|
|
108
83
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
84
|
+
customHost.readFile = (fileName: string): string | undefined => {
|
|
85
|
+
return transformedFiles.get(normalizePath(fileName)) ?? originalReadFile(fileName);
|
|
86
|
+
};
|
|
112
87
|
|
|
113
|
-
|
|
114
|
-
|
|
88
|
+
program = ts.createProgram(parsed.fileNames, parsed.options, customHost);
|
|
89
|
+
}
|
|
115
90
|
|
|
116
|
-
|
|
91
|
+
let { diagnostics, emitSkipped } = program.emit();
|
|
117
92
|
|
|
118
|
-
|
|
93
|
+
diagnostics = ts.getPreEmitDiagnostics(program).concat(diagnostics);
|
|
119
94
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
95
|
+
if (diagnostics.length > 0) {
|
|
96
|
+
console.error(
|
|
97
|
+
ts.formatDiagnosticsWithColorAndContext(diagnostics, {
|
|
98
|
+
getCanonicalFileName: (fileName) => fileName,
|
|
99
|
+
getCurrentDirectory: () => root,
|
|
100
|
+
getNewLine: () => '\n'
|
|
101
|
+
})
|
|
102
|
+
);
|
|
103
|
+
}
|
|
129
104
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
105
|
+
if (emitSkipped) {
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
133
108
|
|
|
134
|
-
|
|
135
|
-
});
|
|
109
|
+
return runTscAlias(process.argv.slice(2)).then((code) => process.exit(code));
|
|
136
110
|
}
|
|
137
111
|
|
|
138
|
-
async function loadPlugins(
|
|
139
|
-
|
|
140
|
-
before: PluginFactory[];
|
|
141
|
-
}> {
|
|
142
|
-
let after: PluginFactory[] = [],
|
|
143
|
-
before: PluginFactory[] = [],
|
|
112
|
+
async function loadPlugins(configs: PluginConfig[], root: string): Promise<Plugin[]> {
|
|
113
|
+
let plugins: Plugin[] = [],
|
|
144
114
|
promises: Promise<void>[] = [];
|
|
145
115
|
|
|
146
|
-
for (let i = 0, n =
|
|
147
|
-
let
|
|
148
|
-
pluginPath =
|
|
116
|
+
for (let i = 0, n = configs.length; i < n; i++) {
|
|
117
|
+
let config = configs[i],
|
|
118
|
+
pluginPath = config.transform;
|
|
149
119
|
|
|
150
120
|
if (pluginPath.startsWith('.')) {
|
|
151
121
|
pluginPath = pathToFileURL(path.resolve(root, pluginPath)).href;
|
|
@@ -156,24 +126,25 @@ async function loadPlugins(plugins: PluginConfig[], root: string): Promise<{
|
|
|
156
126
|
|
|
157
127
|
promises.push(
|
|
158
128
|
import(pluginPath).then((module) => {
|
|
159
|
-
let
|
|
129
|
+
let plugin = module.default ?? module;
|
|
160
130
|
|
|
161
|
-
if (typeof
|
|
162
|
-
|
|
163
|
-
return;
|
|
131
|
+
if (typeof plugin === 'function') {
|
|
132
|
+
plugin = plugin();
|
|
164
133
|
}
|
|
165
134
|
|
|
166
|
-
if (plugin.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
else {
|
|
170
|
-
before.push(factory);
|
|
135
|
+
if (!plugin || typeof plugin.transform !== 'function') {
|
|
136
|
+
console.error(`Plugin ${config.transform}: invalid plugin format, expected { transform: Function }`);
|
|
137
|
+
return;
|
|
171
138
|
}
|
|
139
|
+
|
|
140
|
+
plugins.push(plugin);
|
|
172
141
|
})
|
|
173
142
|
);
|
|
174
143
|
}
|
|
175
144
|
|
|
176
|
-
|
|
145
|
+
await Promise.all(promises);
|
|
146
|
+
|
|
147
|
+
return plugins;
|
|
177
148
|
}
|
|
178
149
|
|
|
179
150
|
function main(): void {
|
|
@@ -189,13 +160,17 @@ function main(): void {
|
|
|
189
160
|
return passthrough();
|
|
190
161
|
}
|
|
191
162
|
|
|
192
|
-
let
|
|
163
|
+
let pluginConfigs = config?.compilerOptions?.plugins?.filter(
|
|
193
164
|
(p: unknown) => typeof p === 'object' && p !== null && 'transform' in p
|
|
194
165
|
) ?? [];
|
|
195
166
|
|
|
196
|
-
|
|
167
|
+
if (pluginConfigs.length === 0) {
|
|
168
|
+
return passthrough();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log(`Found ${pluginConfigs.length} transformer plugin(s), using coordinated build...`);
|
|
197
172
|
|
|
198
|
-
build(config, tsconfig,
|
|
173
|
+
build(config, tsconfig, pluginConfigs).catch((err) => {
|
|
199
174
|
console.error(err);
|
|
200
175
|
process.exit(1);
|
|
201
176
|
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { ImportIntent, Plugin, ReplacementIntent, SharedContext } from './types.js';
|
|
2
|
+
import { ts } from '~/index.js';
|
|
3
|
+
import code from './code.js';
|
|
4
|
+
import imports from './imports.js';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
type CoordinatorResult = {
|
|
8
|
+
changed: boolean;
|
|
9
|
+
code: string;
|
|
10
|
+
sourceFile: ts.SourceFile;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
function applyImports(
|
|
15
|
+
sourceCode: string,
|
|
16
|
+
sourceFile: ts.SourceFile,
|
|
17
|
+
intents: ImportIntent[]
|
|
18
|
+
): string {
|
|
19
|
+
let result = sourceCode;
|
|
20
|
+
|
|
21
|
+
for (let i = 0, n = intents.length; i < n; i++) {
|
|
22
|
+
let intent = intents[i];
|
|
23
|
+
|
|
24
|
+
result = imports.modify(result, sourceFile, intent.package, {
|
|
25
|
+
add: intent.add,
|
|
26
|
+
namespace: intent.namespace,
|
|
27
|
+
remove: intent.remove
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (i < n - 1) {
|
|
31
|
+
sourceFile = ts.createSourceFile(
|
|
32
|
+
sourceFile.fileName,
|
|
33
|
+
result,
|
|
34
|
+
sourceFile.languageVersion,
|
|
35
|
+
true
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function applyIntents(
|
|
44
|
+
sourceCode: string,
|
|
45
|
+
sourceFile: ts.SourceFile,
|
|
46
|
+
intents: ReplacementIntent[]
|
|
47
|
+
): string {
|
|
48
|
+
if (intents.length === 0) {
|
|
49
|
+
return sourceCode;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let replacements = intents.map(intent => ({
|
|
53
|
+
end: intent.node.end,
|
|
54
|
+
newText: intent.generate(sourceFile),
|
|
55
|
+
start: intent.node.getStart(sourceFile)
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
return code.replaceReverse(sourceCode, replacements);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function applyPrepend(sourceCode: string, sourceFile: ts.SourceFile, prepend: string[]): string {
|
|
62
|
+
if (prepend.length === 0) {
|
|
63
|
+
return sourceCode;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let insertPos = findLastImportEnd(sourceFile),
|
|
67
|
+
prependText = prepend.join('\n') + '\n';
|
|
68
|
+
|
|
69
|
+
if (insertPos === 0) {
|
|
70
|
+
return prependText + sourceCode;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return sourceCode.slice(0, insertPos) + '\n' + prependText + sourceCode.slice(insertPos);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function findLastImportEnd(sourceFile: ts.SourceFile): number {
|
|
77
|
+
let lastEnd = 0;
|
|
78
|
+
|
|
79
|
+
for (let i = 0, n = sourceFile.statements.length; i < n; i++) {
|
|
80
|
+
let stmt = sourceFile.statements[i];
|
|
81
|
+
|
|
82
|
+
if (ts.isImportDeclaration(stmt)) {
|
|
83
|
+
lastEnd = stmt.end;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return lastEnd;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function hasPattern(sourceCode: string, patterns: string[]): boolean {
|
|
94
|
+
for (let i = 0, n = patterns.length; i < n; i++) {
|
|
95
|
+
if (sourceCode.indexOf(patterns[i]) !== -1) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Transform source through all plugins sequentially.
|
|
106
|
+
* Each plugin receives fresh AST with accurate positions.
|
|
107
|
+
*/
|
|
108
|
+
const transform = (
|
|
109
|
+
plugins: Plugin[],
|
|
110
|
+
sourceCode: string,
|
|
111
|
+
sourceFile: ts.SourceFile,
|
|
112
|
+
program: ts.Program,
|
|
113
|
+
shared: SharedContext
|
|
114
|
+
): CoordinatorResult => {
|
|
115
|
+
if (plugins.length === 0) {
|
|
116
|
+
return { changed: false, code: sourceCode, sourceFile };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let changed = false,
|
|
120
|
+
currentCode = sourceCode,
|
|
121
|
+
currentSourceFile = sourceFile;
|
|
122
|
+
|
|
123
|
+
for (let i = 0, n = plugins.length; i < n; i++) {
|
|
124
|
+
let plugin = plugins[i];
|
|
125
|
+
|
|
126
|
+
if (plugin.patterns && !hasPattern(currentCode, plugin.patterns)) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let result = plugin.transform({
|
|
131
|
+
checker: program.getTypeChecker(),
|
|
132
|
+
code: currentCode,
|
|
133
|
+
program,
|
|
134
|
+
shared,
|
|
135
|
+
sourceFile: currentSourceFile
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
let hasChanges = (result.imports && result.imports.length > 0) ||
|
|
139
|
+
(result.prepend && result.prepend.length > 0) ||
|
|
140
|
+
(result.replacements && result.replacements.length > 0);
|
|
141
|
+
|
|
142
|
+
if (!hasChanges) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
changed = true;
|
|
147
|
+
|
|
148
|
+
if (result.replacements && result.replacements.length > 0) {
|
|
149
|
+
currentCode = applyIntents(currentCode, currentSourceFile, result.replacements);
|
|
150
|
+
currentSourceFile = ts.createSourceFile(
|
|
151
|
+
currentSourceFile.fileName,
|
|
152
|
+
currentCode,
|
|
153
|
+
currentSourceFile.languageVersion,
|
|
154
|
+
true
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (result.prepend && result.prepend.length > 0) {
|
|
159
|
+
currentCode = applyPrepend(currentCode, currentSourceFile, result.prepend);
|
|
160
|
+
currentSourceFile = ts.createSourceFile(
|
|
161
|
+
currentSourceFile.fileName,
|
|
162
|
+
currentCode,
|
|
163
|
+
currentSourceFile.languageVersion,
|
|
164
|
+
true
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (result.imports && result.imports.length > 0) {
|
|
169
|
+
currentCode = applyImports(currentCode, currentSourceFile, result.imports);
|
|
170
|
+
currentSourceFile = ts.createSourceFile(
|
|
171
|
+
currentSourceFile.fileName,
|
|
172
|
+
currentCode,
|
|
173
|
+
currentSourceFile.languageVersion,
|
|
174
|
+
true
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { changed, code: currentCode, sourceFile: currentSourceFile };
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
export default { transform };
|
|
184
|
+
export type { CoordinatorResult };
|
package/src/compiler/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
export * as ast from './ast';
|
|
2
|
-
export { default as code } from './code';
|
|
3
|
-
export { default as
|
|
4
|
-
export { default as
|
|
5
|
-
export { default as
|
|
6
|
-
export { default as
|
|
7
|
-
export
|
|
1
|
+
export * as ast from './ast/index.js';
|
|
2
|
+
export { default as code } from './code.js';
|
|
3
|
+
export { default as coordinator } from './coordinator.js';
|
|
4
|
+
export { default as imports } from './imports.js';
|
|
5
|
+
export { default as plugin } from './plugins/index.js';
|
|
6
|
+
export { default as program } from './program.js';
|
|
7
|
+
export { default as uid } from './uid.js';
|
|
8
|
+
export type * from './types.js';
|
|
@@ -1,20 +1,29 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import
|
|
1
|
+
import type { Plugin, SharedContext } from '../types.js';
|
|
2
|
+
import type ts from 'typescript';
|
|
3
|
+
import coordinator from '../coordinator.js';
|
|
3
4
|
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
type PluginInstance = {
|
|
7
|
+
transform: ts.TransformerFactory<ts.SourceFile>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export default (plugins: Plugin[]) => {
|
|
12
|
+
return (program: ts.Program, shared: SharedContext): PluginInstance => {
|
|
7
13
|
return {
|
|
8
|
-
analyze: analyze
|
|
9
|
-
? (sourceFile: ts.SourceFile) => analyze(sourceFile, program, context)
|
|
10
|
-
: undefined,
|
|
11
14
|
transform: (() => {
|
|
12
|
-
return (sourceFile) => {
|
|
13
|
-
let result = transform(
|
|
15
|
+
return (sourceFile: ts.SourceFile) => {
|
|
16
|
+
let result = coordinator.transform(
|
|
17
|
+
plugins,
|
|
18
|
+
sourceFile.getFullText(),
|
|
19
|
+
sourceFile,
|
|
20
|
+
program,
|
|
21
|
+
shared
|
|
22
|
+
);
|
|
14
23
|
|
|
15
24
|
return result.changed ? result.sourceFile : sourceFile;
|
|
16
25
|
};
|
|
17
26
|
}) as ts.TransformerFactory<ts.SourceFile>
|
|
18
27
|
};
|
|
19
28
|
};
|
|
20
|
-
}
|
|
29
|
+
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ResolvedConfig } from 'vite';
|
|
2
|
-
import type {
|
|
3
|
-
import { ts } from '
|
|
4
|
-
import
|
|
2
|
+
import type { Plugin, SharedContext } from '../types.js';
|
|
3
|
+
import { ts } from '~/index.js';
|
|
4
|
+
import coordinator from '../coordinator.js';
|
|
5
|
+
import program from '../program.js';
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
type VitePlugin = {
|
|
@@ -13,20 +14,19 @@ type VitePlugin = {
|
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
type VitePluginOptions = {
|
|
16
|
-
analyze?: AnalyzeFn;
|
|
17
17
|
name: string;
|
|
18
18
|
onWatchChange?: () => void;
|
|
19
|
-
|
|
19
|
+
plugins: Plugin[];
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
const FILE_REGEX = /\.[tj]sx?$/;
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
let contexts = new Map<string,
|
|
26
|
+
let contexts = new Map<string, SharedContext>();
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
export default ({
|
|
29
|
+
export default ({ name, onWatchChange, plugins }: VitePluginOptions) => {
|
|
30
30
|
return ({ root }: { root?: string } = {}): VitePlugin => {
|
|
31
31
|
return {
|
|
32
32
|
configResolved(config: unknown) {
|
|
@@ -40,13 +40,11 @@ export default ({ analyze, name, onWatchChange, transform }: VitePluginOptions)
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
try {
|
|
43
|
-
let
|
|
44
|
-
|
|
43
|
+
let prog = program.get(root || ''),
|
|
44
|
+
shared = contexts.get(root || '') ?? contexts.set(root || '', new Map()).get(root || '')!,
|
|
45
45
|
sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true);
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
let result = transform(sourceFile, prog, context);
|
|
47
|
+
let result = coordinator.transform(plugins, code, sourceFile, prog, shared);
|
|
50
48
|
|
|
51
49
|
if (!result.changed) {
|
|
52
50
|
return null;
|
|
@@ -62,7 +60,6 @@ export default ({ analyze, name, onWatchChange, transform }: VitePluginOptions)
|
|
|
62
60
|
watchChange(id: string) {
|
|
63
61
|
if (FILE_REGEX.test(id)) {
|
|
64
62
|
onWatchChange?.();
|
|
65
|
-
|
|
66
63
|
contexts.delete(root || '');
|
|
67
64
|
program.delete(root || '');
|
|
68
65
|
}
|
package/src/compiler/types.ts
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
import type ts from 'typescript';
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
type
|
|
4
|
+
type ImportIntent = {
|
|
5
|
+
add?: string[];
|
|
6
|
+
namespace?: string;
|
|
7
|
+
package: string;
|
|
8
|
+
remove?: string[];
|
|
9
|
+
};
|
|
5
10
|
|
|
6
|
-
type
|
|
11
|
+
type Plugin = {
|
|
12
|
+
/**
|
|
13
|
+
* Optional patterns for quick-check optimization.
|
|
14
|
+
* If provided, transform() is only called when source contains at least one pattern.
|
|
15
|
+
*/
|
|
16
|
+
patterns?: string[];
|
|
7
17
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Transform a source file, returning replacement intents.
|
|
20
|
+
* Called with fresh AST - positions are always accurate.
|
|
21
|
+
*/
|
|
22
|
+
transform: (ctx: TransformContext) => TransformResult;
|
|
11
23
|
};
|
|
12
24
|
|
|
25
|
+
type PluginFactory = (options?: Record<string, unknown>) => Plugin;
|
|
26
|
+
|
|
13
27
|
type QuickCheckPattern = {
|
|
14
28
|
patterns?: string[];
|
|
15
29
|
regex?: RegExp;
|
|
@@ -24,19 +38,52 @@ type Replacement = Range & {
|
|
|
24
38
|
newText: string;
|
|
25
39
|
};
|
|
26
40
|
|
|
27
|
-
type
|
|
41
|
+
type ReplacementIntent = {
|
|
42
|
+
/**
|
|
43
|
+
* Generator function that produces the replacement text.
|
|
44
|
+
* Called at apply-time with current sourceFile for accurate positions.
|
|
45
|
+
*/
|
|
46
|
+
generate: (sourceFile: ts.SourceFile) => string;
|
|
28
47
|
|
|
29
|
-
|
|
30
|
-
|
|
48
|
+
/**
|
|
49
|
+
* AST node to replace. Position resolved at apply-time.
|
|
50
|
+
*/
|
|
51
|
+
node: ts.Node;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type SharedContext = Map<string, unknown>;
|
|
55
|
+
|
|
56
|
+
type TransformContext = {
|
|
57
|
+
checker: ts.TypeChecker;
|
|
31
58
|
code: string;
|
|
59
|
+
program: ts.Program;
|
|
60
|
+
shared: SharedContext;
|
|
32
61
|
sourceFile: ts.SourceFile;
|
|
33
62
|
};
|
|
34
63
|
|
|
64
|
+
type TransformResult = {
|
|
65
|
+
/**
|
|
66
|
+
* Import modifications to apply after replacements.
|
|
67
|
+
*/
|
|
68
|
+
imports?: ImportIntent[];
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Code to prepend after imports (e.g., generated classes, template factories).
|
|
72
|
+
*/
|
|
73
|
+
prepend?: string[];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Replacement intents - node references with generator functions.
|
|
77
|
+
*/
|
|
78
|
+
replacements?: ReplacementIntent[];
|
|
79
|
+
};
|
|
80
|
+
|
|
35
81
|
|
|
36
82
|
export type {
|
|
37
|
-
|
|
38
|
-
|
|
83
|
+
ImportIntent,
|
|
84
|
+
Plugin, PluginFactory,
|
|
39
85
|
QuickCheckPattern,
|
|
40
|
-
Range, Replacement,
|
|
41
|
-
|
|
86
|
+
Range, Replacement, ReplacementIntent,
|
|
87
|
+
SharedContext,
|
|
88
|
+
TransformContext, TransformResult
|
|
42
89
|
};
|