Skip to content

Commit 2c09b57

Browse files
authored
feat(typescript): add transformers factory. (#1668)
feat(typescript): add transformers factory
1 parent 5afda37 commit 2c09b57

File tree

6 files changed

+188
-14
lines changed

6 files changed

+188
-14
lines changed

packages/typescript/README.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ typescript({
125125

126126
### `transformers`
127127

128-
Type: `{ [before | after | afterDeclarations]: TransformerFactory[] }`<br>
128+
Type: `{ [before | after | afterDeclarations]: TransformerFactory[] } | ((program: ts.Program) => ts.CustomTransformers)`<br>
129129
Default: `undefined`
130130

131131
Allows registration of TypeScript custom transformers at any of the supported stages:
@@ -199,6 +199,48 @@ typescript({
199199
});
200200
```
201201

202+
Alternatively, the transformers can be created inside a factory.
203+
204+
Supported transformer factories:
205+
206+
- all **built-in** TypeScript custom transformer factories:
207+
208+
- `import('typescript').TransformerFactory` annotated **TransformerFactory** bellow
209+
- `import('typescript').CustomTransformerFactory` annotated **CustomTransformerFactory** bellow
210+
211+
The example above could be written like this:
212+
213+
```js
214+
typescript({
215+
transformers: function (program) {
216+
return {
217+
before: [
218+
ProgramRequiringTransformerFactory(program),
219+
TypeCheckerRequiringTransformerFactory(program.getTypeChecker())
220+
],
221+
after: [
222+
// You can use normal transformers directly
223+
require('custom-transformer-based-on-Context')
224+
],
225+
afterDeclarations: [
226+
// Or even define in place
227+
function fixDeclarationFactory(context) {
228+
return function fixDeclaration(source) {
229+
function visitor(node) {
230+
// Do real work here
231+
232+
return ts.visitEachChild(node, visitor, context);
233+
}
234+
235+
return ts.visitEachChild(source, visitor, context);
236+
};
237+
}
238+
]
239+
};
240+
}
241+
});
242+
```
243+
202244
### `cacheDir`
203245

204246
Type: `String`<br>

packages/typescript/src/moduleResolution.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ export type Resolver = (
2020

2121
/**
2222
* Create a helper for resolving modules using Typescript.
23-
* @param host Typescript host that extends `ModuleResolutionHost`
23+
* @param ts custom typescript implementation
24+
* @param host Typescript host that extends {@link ModuleResolutionHost}
25+
* @param filter
2426
* with methods for sanitizing filenames and getting compiler options.
2527
*/
2628
export default function createModuleResolver(

packages/typescript/src/options/tsconfig.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ function makeForcedCompilerOptions(noForceEmit: boolean) {
4545

4646
/**
4747
* Finds the path to the tsconfig file relative to the current working directory.
48+
* @param ts Custom typescript implementation
4849
* @param relativePath Relative tsconfig path given by the user.
4950
* If `false` is passed, then a null path is returned.
5051
* @returns The absolute path, or null if the file does not exist.
@@ -69,9 +70,8 @@ function getTsConfigPath(ts: typeof typescript, relativePath?: string | false) {
6970

7071
/**
7172
* Tries to read the tsconfig file at `tsConfigPath`.
73+
* @param ts Custom typescript implementation
7274
* @param tsConfigPath Absolute path to tsconfig JSON file.
73-
* @param explicitPath If true, the path was set by the plugin user.
74-
* If false, the path was computed automatically.
7575
*/
7676
function readTsConfigFile(ts: typeof typescript, tsConfigPath: string) {
7777
const { config, error } = ts.readConfigFile(tsConfigPath, (path) => readFileSync(path, 'utf8'));
@@ -122,13 +122,14 @@ function setModuleResolutionKind(parsedConfig: ParsedCommandLine): ParsedCommand
122122
};
123123
}
124124

125-
const configCache = new Map() as typescript.Map<ExtendedConfigCacheEntry>;
125+
const configCache = new Map() as typescript.ESMap<string, ExtendedConfigCacheEntry>;
126126

127127
/**
128128
* Parse the Typescript config to use with the plugin.
129129
* @param ts Typescript library instance.
130130
* @param tsconfig Path to the tsconfig file, or `false` to ignore the file.
131131
* @param compilerOptions Options passed to the plugin directly for Typescript.
132+
* @param noForceEmit Whether to respect emit options from {@link tsconfig}
132133
*
133134
* @returns Parsed tsconfig.json file with some important properties:
134135
* - `options`: Parsed compiler options.

packages/typescript/src/watchProgram.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import type { PluginContext } from 'rollup';
22
import typescript from 'typescript';
33
import type {
4+
CustomTransformers,
45
Diagnostic,
56
EmitAndSemanticDiagnosticsBuilderProgram,
67
ParsedCommandLine,
8+
Program,
79
WatchCompilerHostOfFilesAndCompilerOptions,
810
WatchStatusReporter,
911
WriteFileCallback
@@ -39,7 +41,7 @@ interface CreateProgramOptions {
3941
/** Function to resolve a module location */
4042
resolveModule: Resolver;
4143
/** Custom TypeScript transformers */
42-
transformers?: CustomTransformerFactories;
44+
transformers?: CustomTransformerFactories | ((program: Program) => CustomTransformers);
4345
}
4446

4547
type DeferredResolve = ((value: boolean | PromiseLike<boolean>) => void) | (() => void);
@@ -155,22 +157,36 @@ function createWatchHost(
155157
parsedOptions.projectReferences
156158
);
157159

160+
let createdTransformers: CustomTransformers | undefined;
158161
return {
159162
...baseHost,
160163
/** Override the created program so an in-memory emit is used */
161164
afterProgramCreate(program) {
162165
const origEmit = program.emit;
163166
// eslint-disable-next-line no-param-reassign
164-
program.emit = (targetSourceFile, _, ...args) =>
165-
origEmit(
167+
program.emit = (
168+
targetSourceFile,
169+
_,
170+
cancellationToken,
171+
emitOnlyDtsFiles,
172+
customTransformers
173+
) => {
174+
createdTransformers ??=
175+
typeof transformers === 'function'
176+
? transformers(program.getProgram())
177+
: mergeTransformers(
178+
program,
179+
transformers,
180+
customTransformers as CustomTransformerFactories
181+
);
182+
return origEmit(
166183
targetSourceFile,
167184
writeFile,
168-
// cancellationToken
169-
args[0],
170-
// emitOnlyDtsFiles
171-
args[1],
172-
mergeTransformers(program, transformers, args[2] as CustomTransformerFactories)
185+
cancellationToken,
186+
emitOnlyDtsFiles,
187+
createdTransformers
173188
);
189+
};
174190

175191
return baseHost.afterProgramCreate!(program);
176192
},

packages/typescript/test/test.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1264,6 +1264,119 @@ test('supports custom transformers', async (t) => {
12641264
);
12651265
});
12661266

1267+
test('supports passing a custom transformers factory', async (t) => {
1268+
const warnings = [];
1269+
1270+
let program = null;
1271+
let typeChecker = null;
1272+
1273+
const bundle = await rollup({
1274+
input: 'fixtures/transformers/main.ts',
1275+
plugins: [
1276+
typescript({
1277+
tsconfig: 'fixtures/transformers/tsconfig.json',
1278+
outDir: 'fixtures/transformers/dist',
1279+
declaration: true,
1280+
transformers: (p) => {
1281+
program = p;
1282+
typeChecker = p.getTypeChecker();
1283+
return {
1284+
before: [
1285+
function removeOneParameterFactory(context) {
1286+
return function removeOneParameter(source) {
1287+
function visitor(node) {
1288+
if (ts.isArrowFunction(node)) {
1289+
return ts.factory.createArrowFunction(
1290+
node.modifiers,
1291+
node.typeParameters,
1292+
[node.parameters[0]],
1293+
node.type,
1294+
node.equalsGreaterThanToken,
1295+
node.body
1296+
);
1297+
}
1298+
1299+
return ts.visitEachChild(node, visitor, context);
1300+
}
1301+
1302+
return ts.visitEachChild(source, visitor, context);
1303+
};
1304+
}
1305+
],
1306+
after: [
1307+
// Enforce a constant numeric output
1308+
function enforceConstantReturnFactory(context) {
1309+
return function enforceConstantReturn(source) {
1310+
function visitor(node) {
1311+
if (ts.isReturnStatement(node)) {
1312+
return ts.factory.createReturnStatement(ts.factory.createNumericLiteral('1'));
1313+
}
1314+
1315+
return ts.visitEachChild(node, visitor, context);
1316+
}
1317+
1318+
return ts.visitEachChild(source, visitor, context);
1319+
};
1320+
}
1321+
],
1322+
afterDeclarations: [
1323+
// Change the return type to numeric
1324+
function fixDeclarationFactory(context) {
1325+
return function fixDeclaration(source) {
1326+
function visitor(node) {
1327+
if (ts.isFunctionTypeNode(node)) {
1328+
return ts.factory.createFunctionTypeNode(
1329+
node.typeParameters,
1330+
[node.parameters[0]],
1331+
ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)
1332+
);
1333+
}
1334+
1335+
return ts.visitEachChild(node, visitor, context);
1336+
}
1337+
1338+
return ts.visitEachChild(source, visitor, context);
1339+
};
1340+
}
1341+
]
1342+
};
1343+
}
1344+
})
1345+
],
1346+
onwarn(warning) {
1347+
warnings.push(warning);
1348+
}
1349+
});
1350+
1351+
const output = await getCode(bundle, { format: 'esm', dir: 'fixtures/transformers' }, true);
1352+
1353+
t.is(warnings.length, 0);
1354+
t.deepEqual(
1355+
output.map((out) => out.fileName),
1356+
['main.js', 'dist/main.d.ts']
1357+
);
1358+
1359+
// Expect the function to have one less arguments from before transformer and return 1 from after transformer
1360+
t.true(output[0].code.includes('var HashFn = function (val) { return 1; };'), output[0].code);
1361+
1362+
// Expect the definition file to reflect the resulting function type after transformer modifications
1363+
t.true(
1364+
output[1].source.includes('export declare const HashFn: (val: string) => number;'),
1365+
output[1].source
1366+
);
1367+
1368+
// Expect a Program to have been forwarded for transformers with custom factories requesting one
1369+
t.deepEqual(program && program.emit && typeof program.emit === 'function', true);
1370+
1371+
// Expect a TypeChecker to have been forwarded for transformers with custom factories requesting one
1372+
t.deepEqual(
1373+
typeChecker &&
1374+
typeChecker.getTypeAtLocation &&
1375+
typeof typeChecker.getTypeAtLocation === 'function',
1376+
true
1377+
);
1378+
});
1379+
12671380
// This test randomly fails with a segfault directly at the first "await waitForWatcherEvent" before any event occurred.
12681381
// Skipping it until we can figure out what the cause is.
12691382
test.serial.skip('picks up on newly included typescript files in watch mode', async (t) => {

packages/typescript/types/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export interface RollupTypescriptPluginOptions {
7575
/**
7676
* TypeScript custom transformers
7777
*/
78-
transformers?: CustomTransformerFactories;
78+
transformers?: CustomTransformerFactories | ((program: Program) => CustomTransformers);
7979
/**
8080
* When set to false, force non-cached files to always be emitted in the output directory.output
8181
* If not set, will default to true with a warning.

0 commit comments

Comments
 (0)