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.