@esportsplus/typescript 0.24.1 → 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 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, plugins) {
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(plugins, root).then((factories) => {
17
- let context = new Map(), printer = ts.createPrinter(), program = ts.createProgram(parsed.fileNames, parsed.options), transformedFiles = new Map();
18
- let instances = factories.before.map(f => f(program, context));
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;
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
- if (transformedFiles.size > 0) {
41
- let customHost = ts.createCompilerHost(parsed.options), originalGetSourceFile = customHost.getSourceFile.bind(customHost), originalReadFile = customHost.readFile.bind(customHost);
42
- customHost.getSourceFile = (fileName, languageVersion, onError, shouldCreateNewSourceFile) => {
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
- let { diagnostics, emitSkipped } = program.emit();
55
- diagnostics = ts.getPreEmitDiagnostics(program).concat(diagnostics);
56
- if (diagnostics.length > 0) {
57
- console.error(ts.formatDiagnosticsWithColorAndContext(diagnostics, {
58
- getCanonicalFileName: (fileName) => fileName,
59
- getCurrentDirectory: () => root,
60
- getNewLine: () => '\n'
61
- }));
62
- }
63
- if (emitSkipped) {
64
- process.exit(1);
65
- }
66
- return runTscAlias(process.argv.slice(2)).then((code) => process.exit(code));
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(plugins, root) {
70
- let after = [], before = [], promises = [];
71
- for (let i = 0, n = plugins.length; i < n; i++) {
72
- let plugin = plugins[i], pluginPath = plugin.transform;
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 factory = module.default ?? module.createTransformer ?? module;
81
- if (typeof factory !== 'function') {
82
- console.error(`Plugin ${plugin.transform}: no transformer factory found`);
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
- else {
89
- before.push(factory);
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
- return Promise.all(promises).then(() => ({ after, before }));
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 plugins = config?.compilerOptions?.plugins?.filter((p) => typeof p === 'object' && p !== null && 'transform' in p) ?? [];
105
- console.log(`Found ${plugins.length} transformer plugin(s), using programmatic build...`);
106
- build(config, tsconfig, plugins).catch((err) => {
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 };
@@ -18,7 +18,7 @@ const find = (sourceFile, packageName) => {
18
18
  specifiers.set(propertyName, name);
19
19
  }
20
20
  }
21
- imports.push({ end: stmt.end, specifiers, start: stmt.getStart() });
21
+ imports.push({ end: stmt.end, specifiers, start: stmt.getStart(sourceFile) });
22
22
  }
23
23
  return imports;
24
24
  };
@@ -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,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,19 +1,22 @@
1
1
  declare const _default: {
2
- tsc: ({ analyze, transform }: import("../index.js").PluginDefinition) => (program: import("typescript").Program, context: import("../index.js").PluginContext) => {
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: ({ analyze, name, onWatchChange, transform }: import("../index.js").VitePluginOptions) => ({ root }?: {
5
+ vite: ({ name, onWatchChange, plugins }: {
6
+ name: string;
7
+ onWatchChange?: () => void;
8
+ plugins: import("../types.js").Plugin[];
9
+ }) => ({ root }?: {
7
10
  root?: string;
8
11
  }) => {
9
- configResolved(config: import("vite").ResolvedConfig): void;
10
- enforce: string;
12
+ configResolved: (config: unknown) => void;
13
+ enforce: "pre";
11
14
  name: string;
12
- transform(code: string, id: string): {
15
+ transform: (code: string, id: string) => {
13
16
  code: string;
14
17
  map: null;
15
18
  } | null;
16
- watchChange(id: string): void;
19
+ watchChange: (id: string) => void;
17
20
  };
18
21
  };
19
22
  export default _default;
@@ -1,7 +1,7 @@
1
- import type { PluginContext, PluginDefinition } from '../types.js';
2
- import { ts } from '../../index.js';
3
- declare const _default: ({ analyze, transform }: PluginDefinition) => (program: ts.Program, context: PluginContext) => {
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
- export default ({ analyze, transform }) => {
2
- return (program, context) => {
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, context);
7
+ let result = coordinator.transform(plugins, sourceFile.getFullText(), sourceFile, program, shared);
10
8
  return result.changed ? result.sourceFile : sourceFile;
11
9
  };
12
10
  })
@@ -1,15 +1,20 @@
1
- import type { ResolvedConfig } from 'vite';
2
- import type { VitePluginOptions } from '../types.js';
3
- declare const _default: ({ analyze, name, onWatchChange, transform }: VitePluginOptions) => ({ root }?: {
4
- root?: string;
5
- }) => {
6
- configResolved(config: ResolvedConfig): void;
7
- enforce: string;
1
+ import type { Plugin } from '../types.js';
2
+ type VitePlugin = {
3
+ configResolved: (config: unknown) => void;
4
+ enforce: 'pre';
8
5
  name: string;
9
- transform(code: string, id: string): {
6
+ transform: (code: string, id: string) => {
10
7
  code: string;
11
8
  map: null;
12
9
  } | null;
13
- watchChange(id: string): void;
10
+ watchChange: (id: string) => void;
11
+ };
12
+ type VitePluginOptions = {
13
+ name: string;
14
+ onWatchChange?: () => void;
15
+ plugins: Plugin[];
14
16
  };
17
+ declare const _default: ({ name, onWatchChange, plugins }: VitePluginOptions) => ({ root }?: {
18
+ root?: string;
19
+ }) => VitePlugin;
15
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 ({ analyze, name, onWatchChange, transform }) => {
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 context = contexts.get(root || '') ?? contexts.set(root || '', new Map()).get(root || ''), prog = program.get(root || ''), sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true);
19
- analyze?.(sourceFile, prog, context);
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 AnalyzeFn = (sourceFile: ts.SourceFile, program: ts.Program, context: PluginContext) => void;
3
- type PluginContext = Map<string, unknown>;
4
- type PluginDefinition = {
5
- analyze?: AnalyzeFn;
6
- transform: TransformFn;
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,16 +21,21 @@ type Range = {
16
21
  type Replacement = Range & {
17
22
  newText: string;
18
23
  };
19
- type TransformFn = (sourceFile: ts.SourceFile, program: ts.Program, context?: PluginContext) => TransformResult;
20
- type TransformResult = {
21
- changed: boolean;
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
- type VitePluginOptions = {
26
- analyze?: AnalyzeFn;
27
- name: string;
28
- onWatchChange?: () => void;
29
- transform: TransformFn;
36
+ type TransformResult = {
37
+ imports?: ImportIntent[];
38
+ prepend?: string[];
39
+ replacements?: ReplacementIntent[];
30
40
  };
31
- export type { AnalyzeFn, PluginContext, PluginDefinition, QuickCheckPattern, Range, Replacement, TransformFn, TransformResult, VitePluginOptions };
41
+ export type { ImportIntent, Plugin, PluginFactory, QuickCheckPattern, Range, Replacement, ReplacementIntent, SharedContext, TransformContext, TransformResult };
package/package.json CHANGED
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "type": "module",
39
39
  "types": "build/index.d.ts",
40
- "version": "0.24.1",
40
+ "version": "0.25.0",
41
41
  "scripts": {
42
42
  "build": "tsc && tsc-alias",
43
43
  "-": "-"
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, plugins: PluginConfig[]): Promise<void> {
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(plugins, root).then((factories) => {
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
- // Create plugin instances with shared context
52
- let instances = factories.before.map(f => f(program, context));
39
+ let program = ts.createProgram(parsed.fileNames, parsed.options),
40
+ shared: SharedContext = new Map(),
41
+ transformedFiles = new Map<string, string>();
53
42
 
54
- // Phase 1: Analyze - all plugins analyze all files first
55
- for (let i = 0, n = parsed.fileNames.length; i < n; i++) {
56
- let fileName = parsed.fileNames[i],
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
- if (!sourceFile) {
60
- continue;
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
- // Phase 2: Transform - all plugins transform all files
69
- let transformers = instances.map(i => i.transform);
70
-
71
- for (let i = 0, n = parsed.fileNames.length; i < n; i++) {
72
- let fileName = parsed.fileNames[i],
73
- sourceFile = program.getSourceFile(fileName);
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
- result.dispose();
59
+ if (result.changed) {
60
+ transformedFiles.set(normalizePath(fileName), result.code);
87
61
  }
62
+ }
88
63
 
89
- if (transformedFiles.size > 0) {
90
- let customHost = ts.createCompilerHost(parsed.options),
91
- originalGetSourceFile = customHost.getSourceFile.bind(customHost),
92
- originalReadFile = customHost.readFile.bind(customHost);
93
-
94
- customHost.getSourceFile = (
95
- fileName: string,
96
- languageVersion: ts.ScriptTarget,
97
- onError?: (message: string) => void,
98
- shouldCreateNewSourceFile?: boolean
99
- ): ts.SourceFile | undefined => {
100
- let transformed = transformedFiles.get(normalizePath(fileName));
101
-
102
- if (transformed) {
103
- return ts.createSourceFile(fileName, transformed, languageVersion, true);
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
- return originalGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
107
- };
81
+ return originalGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
82
+ };
108
83
 
109
- customHost.readFile = (fileName: string): string | undefined => {
110
- return transformedFiles.get(normalizePath(fileName)) ?? originalReadFile(fileName);
111
- };
84
+ customHost.readFile = (fileName: string): string | undefined => {
85
+ return transformedFiles.get(normalizePath(fileName)) ?? originalReadFile(fileName);
86
+ };
112
87
 
113
- program = ts.createProgram(parsed.fileNames, parsed.options, customHost);
114
- }
88
+ program = ts.createProgram(parsed.fileNames, parsed.options, customHost);
89
+ }
115
90
 
116
- let { diagnostics, emitSkipped } = program.emit();
91
+ let { diagnostics, emitSkipped } = program.emit();
117
92
 
118
- diagnostics = ts.getPreEmitDiagnostics(program).concat(diagnostics);
93
+ diagnostics = ts.getPreEmitDiagnostics(program).concat(diagnostics);
119
94
 
120
- if (diagnostics.length > 0) {
121
- console.error(
122
- ts.formatDiagnosticsWithColorAndContext(diagnostics, {
123
- getCanonicalFileName: (fileName) => fileName,
124
- getCurrentDirectory: () => root,
125
- getNewLine: () => '\n'
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
- if (emitSkipped) {
131
- process.exit(1);
132
- }
105
+ if (emitSkipped) {
106
+ process.exit(1);
107
+ }
133
108
 
134
- return runTscAlias(process.argv.slice(2)).then((code) => process.exit(code));
135
- });
109
+ return runTscAlias(process.argv.slice(2)).then((code) => process.exit(code));
136
110
  }
137
111
 
138
- async function loadPlugins(plugins: PluginConfig[], root: string): Promise<{
139
- after: PluginFactory[];
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 = plugins.length; i < n; i++) {
147
- let plugin = plugins[i],
148
- pluginPath = plugin.transform;
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 factory = module.default ?? module.createTransformer ?? module;
129
+ let plugin = module.default ?? module;
160
130
 
161
- if (typeof factory !== 'function') {
162
- console.error(`Plugin ${plugin.transform}: no transformer factory found`);
163
- return;
131
+ if (typeof plugin === 'function') {
132
+ plugin = plugin();
164
133
  }
165
134
 
166
- if (plugin.after) {
167
- after.push(factory);
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
- return Promise.all(promises).then(() => ({ after, before }));
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 plugins = config?.compilerOptions?.plugins?.filter(
163
+ let pluginConfigs = config?.compilerOptions?.plugins?.filter(
193
164
  (p: unknown) => typeof p === 'object' && p !== null && 'transform' in p
194
165
  ) ?? [];
195
166
 
196
- console.log(`Found ${plugins.length} transformer plugin(s), using programmatic build...`);
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, plugins).catch((err) => {
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 };
@@ -46,7 +46,7 @@ const find = (sourceFile: ts.SourceFile, packageName: string): ImportInfo[] => {
46
46
  }
47
47
  }
48
48
 
49
- imports.push({ end: stmt.end, specifiers, start: stmt.getStart() });
49
+ imports.push({ end: stmt.end, specifiers, start: stmt.getStart(sourceFile) });
50
50
  }
51
51
 
52
52
  return imports;
@@ -1,7 +1,8 @@
1
- export * as ast from './ast';
2
- export { default as code } from './code';
3
- export { default as imports } from './imports';
4
- export { default as plugin } from './plugins';
5
- export { default as program } from './program';
6
- export { default as uid } from './uid';
7
- export type * from './types';
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,5 +1,5 @@
1
- import tsc from './tsc';
2
- import vite from './vite';
1
+ import tsc from './tsc.js';
2
+ import vite from './vite.js';
3
3
 
4
4
 
5
- export default { tsc, vite };
5
+ export default { tsc, vite };
@@ -1,20 +1,29 @@
1
- import type { PluginContext, PluginDefinition } from '../types';
2
- import { ts } from '../..';
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
- export default ({ analyze, transform }: PluginDefinition) => {
6
- return (program: ts.Program, context: PluginContext) => {
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(sourceFile, program, context);
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,20 +1,36 @@
1
1
  import type { ResolvedConfig } from 'vite';
2
- import type { PluginContext, VitePluginOptions } from '../types';
3
- import { ts } from '../..';
4
- import program from '../program';
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';
6
+
7
+
8
+ type VitePlugin = {
9
+ configResolved: (config: unknown) => void;
10
+ enforce: 'pre';
11
+ name: string;
12
+ transform: (code: string, id: string) => { code: string; map: null } | null;
13
+ watchChange: (id: string) => void;
14
+ };
15
+
16
+ type VitePluginOptions = {
17
+ name: string;
18
+ onWatchChange?: () => void;
19
+ plugins: Plugin[];
20
+ };
5
21
 
6
22
 
7
23
  const FILE_REGEX = /\.[tj]sx?$/;
8
24
 
9
25
 
10
- let contexts = new Map<string, PluginContext>();
26
+ let contexts = new Map<string, SharedContext>();
11
27
 
12
28
 
13
- export default ({ analyze, name, onWatchChange, transform }: VitePluginOptions) => {
14
- return ({ root }: { root?: string } = {}) => {
29
+ export default ({ name, onWatchChange, plugins }: VitePluginOptions) => {
30
+ return ({ root }: { root?: string } = {}): VitePlugin => {
15
31
  return {
16
- configResolved(config: ResolvedConfig) {
17
- root ??= config.root;
32
+ configResolved(config: unknown) {
33
+ root ??= (config as ResolvedConfig).root;
18
34
  },
19
35
  enforce: 'pre',
20
36
  name: `${name}/plugin-vite`,
@@ -24,13 +40,11 @@ export default ({ analyze, name, onWatchChange, transform }: VitePluginOptions)
24
40
  }
25
41
 
26
42
  try {
27
- let context = contexts.get(root || '') ?? contexts.set(root || '', new Map()).get(root || '')!,
28
- prog = program.get(root || ''),
43
+ let prog = program.get(root || ''),
44
+ shared = contexts.get(root || '') ?? contexts.set(root || '', new Map()).get(root || '')!,
29
45
  sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true);
30
46
 
31
- analyze?.(sourceFile, prog, context);
32
-
33
- let result = transform(sourceFile, prog, context);
47
+ let result = coordinator.transform(plugins, code, sourceFile, prog, shared);
34
48
 
35
49
  if (!result.changed) {
36
50
  return null;
@@ -46,7 +60,6 @@ export default ({ analyze, name, onWatchChange, transform }: VitePluginOptions)
46
60
  watchChange(id: string) {
47
61
  if (FILE_REGEX.test(id)) {
48
62
  onWatchChange?.();
49
-
50
63
  contexts.delete(root || '');
51
64
  program.delete(root || '');
52
65
  }
@@ -1,15 +1,29 @@
1
1
  import type ts from 'typescript';
2
2
 
3
3
 
4
- type AnalyzeFn = (sourceFile: ts.SourceFile, program: ts.Program, context: PluginContext) => void;
4
+ type ImportIntent = {
5
+ add?: string[];
6
+ namespace?: string;
7
+ package: string;
8
+ remove?: string[];
9
+ };
5
10
 
6
- type PluginContext = Map<string, unknown>;
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
- type PluginDefinition = {
9
- analyze?: AnalyzeFn;
10
- transform: TransformFn;
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,27 +38,52 @@ type Replacement = Range & {
24
38
  newText: string;
25
39
  };
26
40
 
27
- type TransformFn = (sourceFile: ts.SourceFile, program: ts.Program, context?: PluginContext) => TransformResult;
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
- type TransformResult = {
30
- changed: boolean;
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
 
35
- type VitePluginOptions = {
36
- analyze?: AnalyzeFn;
37
- name: string;
38
- onWatchChange?: () => void;
39
- transform: TransformFn;
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[];
40
79
  };
41
80
 
42
81
 
43
82
  export type {
44
- AnalyzeFn,
45
- PluginContext, PluginDefinition,
83
+ ImportIntent,
84
+ Plugin, PluginFactory,
46
85
  QuickCheckPattern,
47
- Range, Replacement,
48
- TransformFn, TransformResult,
49
- VitePluginOptions
86
+ Range, Replacement, ReplacementIntent,
87
+ SharedContext,
88
+ TransformContext, TransformResult
50
89
  };