From 2c09b57a32f717e23e2d2c9bacf1be5c556325e5 Mon Sep 17 00:00:00 2001
From: Marcel Mundl <3000678+Haringat@users.noreply.github.com>
Date: Mon, 23 Sep 2024 01:45:13 +0200
Subject: [PATCH] feat(typescript): add transformers factory. (#1668)
feat(typescript): add transformers factory
---
packages/typescript/README.md | 44 +++++++-
packages/typescript/src/moduleResolution.ts | 4 +-
packages/typescript/src/options/tsconfig.ts | 7 +-
packages/typescript/src/watchProgram.ts | 32 ++++--
packages/typescript/test/test.js | 113 ++++++++++++++++++++
packages/typescript/types/index.d.ts | 2 +-
6 files changed, 188 insertions(+), 14 deletions(-)
diff --git a/packages/typescript/README.md b/packages/typescript/README.md
index 8350f9aa8..340f5cd3b 100644
--- a/packages/typescript/README.md
+++ b/packages/typescript/README.md
@@ -125,7 +125,7 @@ typescript({
### `transformers`
-Type: `{ [before | after | afterDeclarations]: TransformerFactory[] }`
+Type: `{ [before | after | afterDeclarations]: TransformerFactory[] } | ((program: ts.Program) => ts.CustomTransformers)`
Default: `undefined`
Allows registration of TypeScript custom transformers at any of the supported stages:
@@ -199,6 +199,48 @@ typescript({
});
```
+Alternatively, the transformers can be created inside a factory.
+
+Supported transformer factories:
+
+- all **built-in** TypeScript custom transformer factories:
+
+ - `import('typescript').TransformerFactory` annotated **TransformerFactory** bellow
+ - `import('typescript').CustomTransformerFactory` annotated **CustomTransformerFactory** bellow
+
+The example above could be written like this:
+
+```js
+typescript({
+ transformers: function (program) {
+ return {
+ before: [
+ ProgramRequiringTransformerFactory(program),
+ TypeCheckerRequiringTransformerFactory(program.getTypeChecker())
+ ],
+ after: [
+ // You can use normal transformers directly
+ require('custom-transformer-based-on-Context')
+ ],
+ afterDeclarations: [
+ // Or even define in place
+ function fixDeclarationFactory(context) {
+ return function fixDeclaration(source) {
+ function visitor(node) {
+ // Do real work here
+
+ return ts.visitEachChild(node, visitor, context);
+ }
+
+ return ts.visitEachChild(source, visitor, context);
+ };
+ }
+ ]
+ };
+ }
+});
+```
+
### `cacheDir`
Type: `String`
diff --git a/packages/typescript/src/moduleResolution.ts b/packages/typescript/src/moduleResolution.ts
index 97aa01e79..779e0304a 100644
--- a/packages/typescript/src/moduleResolution.ts
+++ b/packages/typescript/src/moduleResolution.ts
@@ -20,7 +20,9 @@ export type Resolver = (
/**
* Create a helper for resolving modules using Typescript.
- * @param host Typescript host that extends `ModuleResolutionHost`
+ * @param ts custom typescript implementation
+ * @param host Typescript host that extends {@link ModuleResolutionHost}
+ * @param filter
* with methods for sanitizing filenames and getting compiler options.
*/
export default function createModuleResolver(
diff --git a/packages/typescript/src/options/tsconfig.ts b/packages/typescript/src/options/tsconfig.ts
index 74d04ee9c..4ebc12675 100644
--- a/packages/typescript/src/options/tsconfig.ts
+++ b/packages/typescript/src/options/tsconfig.ts
@@ -45,6 +45,7 @@ function makeForcedCompilerOptions(noForceEmit: boolean) {
/**
* Finds the path to the tsconfig file relative to the current working directory.
+ * @param ts Custom typescript implementation
* @param relativePath Relative tsconfig path given by the user.
* If `false` is passed, then a null path is returned.
* @returns The absolute path, or null if the file does not exist.
@@ -69,9 +70,8 @@ function getTsConfigPath(ts: typeof typescript, relativePath?: string | false) {
/**
* Tries to read the tsconfig file at `tsConfigPath`.
+ * @param ts Custom typescript implementation
* @param tsConfigPath Absolute path to tsconfig JSON file.
- * @param explicitPath If true, the path was set by the plugin user.
- * If false, the path was computed automatically.
*/
function readTsConfigFile(ts: typeof typescript, tsConfigPath: string) {
const { config, error } = ts.readConfigFile(tsConfigPath, (path) => readFileSync(path, 'utf8'));
@@ -122,13 +122,14 @@ function setModuleResolutionKind(parsedConfig: ParsedCommandLine): ParsedCommand
};
}
-const configCache = new Map() as typescript.Map;
+const configCache = new Map() as typescript.ESMap;
/**
* Parse the Typescript config to use with the plugin.
* @param ts Typescript library instance.
* @param tsconfig Path to the tsconfig file, or `false` to ignore the file.
* @param compilerOptions Options passed to the plugin directly for Typescript.
+ * @param noForceEmit Whether to respect emit options from {@link tsconfig}
*
* @returns Parsed tsconfig.json file with some important properties:
* - `options`: Parsed compiler options.
diff --git a/packages/typescript/src/watchProgram.ts b/packages/typescript/src/watchProgram.ts
index 07bf25d18..9ad50f647 100644
--- a/packages/typescript/src/watchProgram.ts
+++ b/packages/typescript/src/watchProgram.ts
@@ -1,9 +1,11 @@
import type { PluginContext } from 'rollup';
import typescript from 'typescript';
import type {
+ CustomTransformers,
Diagnostic,
EmitAndSemanticDiagnosticsBuilderProgram,
ParsedCommandLine,
+ Program,
WatchCompilerHostOfFilesAndCompilerOptions,
WatchStatusReporter,
WriteFileCallback
@@ -39,7 +41,7 @@ interface CreateProgramOptions {
/** Function to resolve a module location */
resolveModule: Resolver;
/** Custom TypeScript transformers */
- transformers?: CustomTransformerFactories;
+ transformers?: CustomTransformerFactories | ((program: Program) => CustomTransformers);
}
type DeferredResolve = ((value: boolean | PromiseLike) => void) | (() => void);
@@ -155,22 +157,36 @@ function createWatchHost(
parsedOptions.projectReferences
);
+ let createdTransformers: CustomTransformers | undefined;
return {
...baseHost,
/** Override the created program so an in-memory emit is used */
afterProgramCreate(program) {
const origEmit = program.emit;
// eslint-disable-next-line no-param-reassign
- program.emit = (targetSourceFile, _, ...args) =>
- origEmit(
+ program.emit = (
+ targetSourceFile,
+ _,
+ cancellationToken,
+ emitOnlyDtsFiles,
+ customTransformers
+ ) => {
+ createdTransformers ??=
+ typeof transformers === 'function'
+ ? transformers(program.getProgram())
+ : mergeTransformers(
+ program,
+ transformers,
+ customTransformers as CustomTransformerFactories
+ );
+ return origEmit(
targetSourceFile,
writeFile,
- // cancellationToken
- args[0],
- // emitOnlyDtsFiles
- args[1],
- mergeTransformers(program, transformers, args[2] as CustomTransformerFactories)
+ cancellationToken,
+ emitOnlyDtsFiles,
+ createdTransformers
);
+ };
return baseHost.afterProgramCreate!(program);
},
diff --git a/packages/typescript/test/test.js b/packages/typescript/test/test.js
index d5ac91086..efed9b397 100644
--- a/packages/typescript/test/test.js
+++ b/packages/typescript/test/test.js
@@ -1264,6 +1264,119 @@ test('supports custom transformers', async (t) => {
);
});
+test('supports passing a custom transformers factory', async (t) => {
+ const warnings = [];
+
+ let program = null;
+ let typeChecker = null;
+
+ const bundle = await rollup({
+ input: 'fixtures/transformers/main.ts',
+ plugins: [
+ typescript({
+ tsconfig: 'fixtures/transformers/tsconfig.json',
+ outDir: 'fixtures/transformers/dist',
+ declaration: true,
+ transformers: (p) => {
+ program = p;
+ typeChecker = p.getTypeChecker();
+ return {
+ before: [
+ function removeOneParameterFactory(context) {
+ return function removeOneParameter(source) {
+ function visitor(node) {
+ if (ts.isArrowFunction(node)) {
+ return ts.factory.createArrowFunction(
+ node.modifiers,
+ node.typeParameters,
+ [node.parameters[0]],
+ node.type,
+ node.equalsGreaterThanToken,
+ node.body
+ );
+ }
+
+ return ts.visitEachChild(node, visitor, context);
+ }
+
+ return ts.visitEachChild(source, visitor, context);
+ };
+ }
+ ],
+ after: [
+ // Enforce a constant numeric output
+ function enforceConstantReturnFactory(context) {
+ return function enforceConstantReturn(source) {
+ function visitor(node) {
+ if (ts.isReturnStatement(node)) {
+ return ts.factory.createReturnStatement(ts.factory.createNumericLiteral('1'));
+ }
+
+ return ts.visitEachChild(node, visitor, context);
+ }
+
+ return ts.visitEachChild(source, visitor, context);
+ };
+ }
+ ],
+ afterDeclarations: [
+ // Change the return type to numeric
+ function fixDeclarationFactory(context) {
+ return function fixDeclaration(source) {
+ function visitor(node) {
+ if (ts.isFunctionTypeNode(node)) {
+ return ts.factory.createFunctionTypeNode(
+ node.typeParameters,
+ [node.parameters[0]],
+ ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)
+ );
+ }
+
+ return ts.visitEachChild(node, visitor, context);
+ }
+
+ return ts.visitEachChild(source, visitor, context);
+ };
+ }
+ ]
+ };
+ }
+ })
+ ],
+ onwarn(warning) {
+ warnings.push(warning);
+ }
+ });
+
+ const output = await getCode(bundle, { format: 'esm', dir: 'fixtures/transformers' }, true);
+
+ t.is(warnings.length, 0);
+ t.deepEqual(
+ output.map((out) => out.fileName),
+ ['main.js', 'dist/main.d.ts']
+ );
+
+ // Expect the function to have one less arguments from before transformer and return 1 from after transformer
+ t.true(output[0].code.includes('var HashFn = function (val) { return 1; };'), output[0].code);
+
+ // Expect the definition file to reflect the resulting function type after transformer modifications
+ t.true(
+ output[1].source.includes('export declare const HashFn: (val: string) => number;'),
+ output[1].source
+ );
+
+ // Expect a Program to have been forwarded for transformers with custom factories requesting one
+ t.deepEqual(program && program.emit && typeof program.emit === 'function', true);
+
+ // Expect a TypeChecker to have been forwarded for transformers with custom factories requesting one
+ t.deepEqual(
+ typeChecker &&
+ typeChecker.getTypeAtLocation &&
+ typeof typeChecker.getTypeAtLocation === 'function',
+ true
+ );
+});
+
// This test randomly fails with a segfault directly at the first "await waitForWatcherEvent" before any event occurred.
// Skipping it until we can figure out what the cause is.
test.serial.skip('picks up on newly included typescript files in watch mode', async (t) => {
diff --git a/packages/typescript/types/index.d.ts b/packages/typescript/types/index.d.ts
index 6d4a30f00..4db1c5d06 100644
--- a/packages/typescript/types/index.d.ts
+++ b/packages/typescript/types/index.d.ts
@@ -75,7 +75,7 @@ export interface RollupTypescriptPluginOptions {
/**
* TypeScript custom transformers
*/
- transformers?: CustomTransformerFactories;
+ transformers?: CustomTransformerFactories | ((program: Program) => CustomTransformers);
/**
* When set to false, force non-cached files to always be emitted in the output directory.output
* If not set, will default to true with a warning.