fix(js): enhance TypeScript transformer loading to support function-based plugins (#31443)
## Current Behavior TypeScript transformer loading in the js package was limited to standard Nx/TypeScript transformer plugins and didnt handle different exports ## Expected Behavior TypeScript transformer loading should support various function-based transformer formats in a generic way ## Related Issue(s) Fixes #31411 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Coly010 <Coly010@users.noreply.github.com>
This commit is contained in:
parent
55f33c582d
commit
61eb47f0d3
@ -0,0 +1,3 @@
|
|||||||
|
export const afterDeclarations = (options: any, program: any) => {
|
||||||
|
return () => {}; // Mock transformer factory
|
||||||
|
};
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export const after = (options: any, program: any) => {
|
||||||
|
return () => {}; // Mock transformer factory
|
||||||
|
};
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export default (options, program) => {
|
||||||
|
return () => {};
|
||||||
|
};
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
export const before = (options: any, program: any) => {
|
||||||
|
return () => {}; // Mock transformer factory
|
||||||
|
};
|
||||||
|
|
||||||
|
export const after = (options: any, program: any) => {
|
||||||
|
return () => {}; // Mock transformer factory
|
||||||
|
};
|
||||||
|
|
||||||
|
export const afterDeclarations = (options: any, program: any) => {
|
||||||
|
return () => {}; // Mock transformer factory
|
||||||
|
};
|
||||||
@ -1 +1,3 @@
|
|||||||
export const before = () => {};
|
export const before = (options: any, program: any) => {
|
||||||
|
return () => {}; // Mock transformer factory
|
||||||
|
};
|
||||||
|
|||||||
@ -1 +1,3 @@
|
|||||||
export const after = () => {};
|
export const after = (options: any, program: any) => {
|
||||||
|
return () => {}; // Mock transformer factory
|
||||||
|
};
|
||||||
|
|||||||
@ -2,6 +2,10 @@ import { loadTsTransformers } from './load-ts-transformers';
|
|||||||
|
|
||||||
jest.mock('plugin-a');
|
jest.mock('plugin-a');
|
||||||
jest.mock('plugin-b');
|
jest.mock('plugin-b');
|
||||||
|
jest.mock('function-after-plugin');
|
||||||
|
jest.mock('function-after-declarations-plugin');
|
||||||
|
jest.mock('function-direct-export');
|
||||||
|
jest.mock('function-multiple-hooks');
|
||||||
const mockRequireResolve = jest.fn((path) => path);
|
const mockRequireResolve = jest.fn((path) => path);
|
||||||
|
|
||||||
describe('loadTsTransformers', () => {
|
describe('loadTsTransformers', () => {
|
||||||
@ -29,6 +33,62 @@ describe('loadTsTransformers', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle function-based after transformers', () => {
|
||||||
|
const result = loadTsTransformers(
|
||||||
|
['function-after-plugin'],
|
||||||
|
mockRequireResolve as any
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.hasPlugin).toEqual(true);
|
||||||
|
expect(result.compilerPluginHooks).toEqual({
|
||||||
|
beforeHooks: [],
|
||||||
|
afterHooks: [expect.any(Function)],
|
||||||
|
afterDeclarationsHooks: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle function-based afterDeclarations transformers', () => {
|
||||||
|
const result = loadTsTransformers(
|
||||||
|
['function-after-declarations-plugin'],
|
||||||
|
mockRequireResolve as any
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.hasPlugin).toEqual(true);
|
||||||
|
expect(result.compilerPluginHooks).toEqual({
|
||||||
|
beforeHooks: [],
|
||||||
|
afterHooks: [],
|
||||||
|
afterDeclarationsHooks: [expect.any(Function)],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle direct function export transformers', () => {
|
||||||
|
const result = loadTsTransformers(
|
||||||
|
['function-direct-export'],
|
||||||
|
mockRequireResolve as any
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.hasPlugin).toEqual(true);
|
||||||
|
expect(result.compilerPluginHooks).toEqual({
|
||||||
|
beforeHooks: [expect.any(Function)],
|
||||||
|
afterHooks: [],
|
||||||
|
afterDeclarationsHooks: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle function-based transformers with multiple hooks', () => {
|
||||||
|
const result = loadTsTransformers(
|
||||||
|
['function-multiple-hooks'],
|
||||||
|
mockRequireResolve as any
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.hasPlugin).toEqual(true);
|
||||||
|
expect(result.compilerPluginHooks).toEqual({
|
||||||
|
beforeHooks: [expect.any(Function)],
|
||||||
|
afterHooks: [expect.any(Function)],
|
||||||
|
afterDeclarationsHooks: [expect.any(Function)],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function assertEmptyResult(result: ReturnType<typeof loadTsTransformers>) {
|
function assertEmptyResult(result: ReturnType<typeof loadTsTransformers>) {
|
||||||
expect(result.hasPlugin).toEqual(false);
|
expect(result.hasPlugin).toEqual(false);
|
||||||
expect(result.compilerPluginHooks).toEqual({
|
expect(result.compilerPluginHooks).toEqual({
|
||||||
|
|||||||
@ -7,6 +7,76 @@ import {
|
|||||||
TransformerPlugin,
|
TransformerPlugin,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
|
enum TransformerFormat {
|
||||||
|
STANDARD, // Standard TypeScript transformer API: { before, after, afterDeclarations }
|
||||||
|
FUNCTION_EXPORT, // Function-based: exports a function or { before: Function }
|
||||||
|
UNKNOWN, // Unknown format
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectTransformerFormat(plugin: any): TransformerFormat {
|
||||||
|
// Check if it's a standard Nx/TypeScript transformer plugin
|
||||||
|
if (plugin && (plugin.before || plugin.after || plugin.afterDeclarations)) {
|
||||||
|
return TransformerFormat.STANDARD;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a function-based transformer (exports a function directly)
|
||||||
|
if (typeof plugin === 'function') {
|
||||||
|
return TransformerFormat.FUNCTION_EXPORT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it has a function export (function-based plugin pattern)
|
||||||
|
if (
|
||||||
|
plugin &&
|
||||||
|
(typeof plugin.before === 'function' ||
|
||||||
|
typeof plugin.after === 'function' ||
|
||||||
|
typeof plugin.afterDeclarations === 'function')
|
||||||
|
) {
|
||||||
|
return TransformerFormat.FUNCTION_EXPORT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TransformerFormat.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
function adaptFunctionBasedTransformer(
|
||||||
|
plugin: any,
|
||||||
|
pluginOptions: Record<string, unknown>
|
||||||
|
) {
|
||||||
|
// Handle direct function export
|
||||||
|
if (typeof plugin === 'function') {
|
||||||
|
return {
|
||||||
|
before: (options: Record<string, unknown>, program: any) =>
|
||||||
|
plugin(options, program),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle object with function exports - adapt all available hooks
|
||||||
|
if (plugin && typeof plugin === 'object') {
|
||||||
|
const adapted: any = {};
|
||||||
|
|
||||||
|
if (typeof plugin.before === 'function') {
|
||||||
|
adapted.before = (options: Record<string, unknown>, program: any) =>
|
||||||
|
plugin.before(options, program);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof plugin.after === 'function') {
|
||||||
|
adapted.after = (options: Record<string, unknown>, program: any) =>
|
||||||
|
plugin.after(options, program);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof plugin.afterDeclarations === 'function') {
|
||||||
|
adapted.afterDeclarations = (
|
||||||
|
options: Record<string, unknown>,
|
||||||
|
program: any
|
||||||
|
) => plugin.afterDeclarations(options, program);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return adapted hooks if any were found, otherwise return original plugin
|
||||||
|
return Object.keys(adapted).length > 0 ? adapted : plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
|
||||||
export function loadTsTransformers(
|
export function loadTsTransformers(
|
||||||
plugins: TransformerEntry[],
|
plugins: TransformerEntry[],
|
||||||
moduleResolver: typeof require.resolve = require.resolve
|
moduleResolver: typeof require.resolve = require.resolve
|
||||||
@ -43,7 +113,18 @@ export function loadTsTransformers(
|
|||||||
const binaryPath = moduleResolver(name, {
|
const binaryPath = moduleResolver(name, {
|
||||||
paths: nodeModulePaths,
|
paths: nodeModulePaths,
|
||||||
});
|
});
|
||||||
return require(binaryPath);
|
const loadedPlugin = require(binaryPath);
|
||||||
|
// Check if main export already has transformer hooks
|
||||||
|
if (
|
||||||
|
loadedPlugin &&
|
||||||
|
(loadedPlugin.before ||
|
||||||
|
loadedPlugin.after ||
|
||||||
|
loadedPlugin.afterDeclarations)
|
||||||
|
) {
|
||||||
|
return loadedPlugin;
|
||||||
|
}
|
||||||
|
// Only fall back to .default if main export lacks transformer hooks
|
||||||
|
return loadedPlugin?.default ?? loadedPlugin;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn(`"${name}" plugin could not be found!`);
|
logger.warn(`"${name}" plugin could not be found!`);
|
||||||
return {};
|
return {};
|
||||||
@ -52,26 +133,71 @@ export function loadTsTransformers(
|
|||||||
|
|
||||||
for (let i = 0; i < pluginRefs.length; i++) {
|
for (let i = 0; i < pluginRefs.length; i++) {
|
||||||
const { name: pluginName, options: pluginOptions } = normalizedPlugins[i];
|
const { name: pluginName, options: pluginOptions } = normalizedPlugins[i];
|
||||||
const { before, after, afterDeclarations } = pluginRefs[i];
|
let plugin = pluginRefs[i];
|
||||||
if (!before && !after && !afterDeclarations) {
|
|
||||||
|
// Skip empty plugins (failed to load)
|
||||||
|
if (
|
||||||
|
!plugin ||
|
||||||
|
(typeof plugin !== 'function' && Object.keys(plugin).length === 0)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const format = detectTransformerFormat(plugin);
|
||||||
|
|
||||||
|
// Adapt function-based transformers to standard format
|
||||||
|
if (format === TransformerFormat.FUNCTION_EXPORT) {
|
||||||
|
logger.debug(`Adapting function-based transformer: ${pluginName}`);
|
||||||
|
plugin = adaptFunctionBasedTransformer(plugin, pluginOptions);
|
||||||
|
} else if (format === TransformerFormat.UNKNOWN) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`${pluginName} is not a Transformer Plugin. It does not provide neither before(), after(), nor afterDeclarations()`
|
`${pluginName} is not a recognized Transformer Plugin format. It should export ` +
|
||||||
|
`{ before?, after?, afterDeclarations? } functions or be a function-based transformer.`
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { before, after, afterDeclarations } = plugin;
|
||||||
|
|
||||||
|
// Validate that at least one hook is available
|
||||||
|
if (!before && !after && !afterDeclarations) {
|
||||||
|
logger.warn(
|
||||||
|
`${pluginName} does not provide any transformer hooks (before, after, or afterDeclarations).`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add hooks with proper error handling
|
||||||
if (before) {
|
if (before) {
|
||||||
beforeHooks.push(before.bind(before, pluginOptions));
|
try {
|
||||||
|
beforeHooks.push((program) => before(pluginOptions, program));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to register 'before' transformer for ${pluginName}: ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (after) {
|
if (after) {
|
||||||
afterHooks.push(after.bind(after, pluginOptions));
|
try {
|
||||||
|
afterHooks.push((program) => after(pluginOptions, program));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to register 'after' transformer for ${pluginName}: ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (afterDeclarations) {
|
if (afterDeclarations) {
|
||||||
afterDeclarationsHooks.push(
|
try {
|
||||||
afterDeclarations.bind(afterDeclarations, pluginOptions)
|
afterDeclarationsHooks.push((program) =>
|
||||||
);
|
afterDeclarations(pluginOptions, program)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to register 'afterDeclarations' transformer for ${pluginName}: ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -31,6 +31,35 @@ export interface CompilerPlugin {
|
|||||||
) => TransformerFactory;
|
) => TransformerFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended plugin interface to support different transformer API formats
|
||||||
|
* including function-based transformers and direct function exports
|
||||||
|
*/
|
||||||
|
export type AnyCompilerPlugin =
|
||||||
|
| CompilerPlugin
|
||||||
|
| ((
|
||||||
|
options?: Record<string, unknown>,
|
||||||
|
program?: Program
|
||||||
|
) => TransformerFactory)
|
||||||
|
| {
|
||||||
|
before: (
|
||||||
|
options?: Record<string, unknown>,
|
||||||
|
program?: Program
|
||||||
|
) => TransformerFactory;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
after: (
|
||||||
|
options?: Record<string, unknown>,
|
||||||
|
program?: Program
|
||||||
|
) => TransformerFactory;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
afterDeclarations: (
|
||||||
|
options?: Record<string, unknown>,
|
||||||
|
program?: Program
|
||||||
|
) => TransformerFactory;
|
||||||
|
};
|
||||||
|
|
||||||
export interface CompilerPluginHooks {
|
export interface CompilerPluginHooks {
|
||||||
beforeHooks: Array<(program?: Program) => TransformerFactory>;
|
beforeHooks: Array<(program?: Program) => TransformerFactory>;
|
||||||
afterHooks: Array<(program?: Program) => TransformerFactory>;
|
afterHooks: Array<(program?: Program) => TransformerFactory>;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user