Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(typescript): add transformers factory. #1668

Merged
merged 1 commit into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion packages/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ typescript({

### `transformers`

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

Allows registration of TypeScript custom transformers at any of the supported stages:
Expand Down Expand Up @@ -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`<br>
Expand Down
4 changes: 3 additions & 1 deletion packages/typescript/src/moduleResolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
7 changes: 4 additions & 3 deletions packages/typescript/src/options/tsconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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'));
Expand Down Expand Up @@ -122,13 +122,14 @@ function setModuleResolutionKind(parsedConfig: ParsedCommandLine): ParsedCommand
};
}

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

/**
* 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.
Expand Down
32 changes: 24 additions & 8 deletions packages/typescript/src/watchProgram.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { PluginContext } from 'rollup';
import typescript from 'typescript';
import type {
CustomTransformers,
Diagnostic,
EmitAndSemanticDiagnosticsBuilderProgram,
ParsedCommandLine,
Program,
WatchCompilerHostOfFilesAndCompilerOptions,
WatchStatusReporter,
WriteFileCallback
Expand Down Expand Up @@ -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<boolean>) => void) | (() => void);
Expand Down Expand Up @@ -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);
},
Expand Down
113 changes: 113 additions & 0 deletions packages/typescript/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1236,6 +1236,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) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/typescript/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading