diff --git a/packages/core/src/initPlugins.ts b/packages/core/src/initPlugins.ts index 756afd4f64..882b78597b 100644 --- a/packages/core/src/initPlugins.ts +++ b/packages/core/src/initPlugins.ts @@ -148,7 +148,7 @@ export function initPluginAPI({ }; let transformId = 0; - const transformer: Record = {}; + const transformer: Record> = {}; const processAssetsFns: { environment?: string; descriptor: ProcessAssetsDescriptor; diff --git a/packages/core/src/types/plugin.ts b/packages/core/src/types/plugin.ts index 1b6ef764c0..3a9a4af43a 100644 --- a/packages/core/src/types/plugin.ts +++ b/packages/core/src/types/plugin.ts @@ -304,11 +304,12 @@ type TransformResult = map?: string | Rspack.sources.RawSourceMap | null; }; -export type TransformContext = { +export type TransformContext = { /** * The code of the module. + * When raw is true, this will be a Buffer instead of a string. */ - code: string; + code: Raw extends true ? Buffer : string; /** * The directory path of the currently processed module, * which changes with the location of each processed module. @@ -370,8 +371,8 @@ export type TransformContext = { resolve: Rspack.LoaderContext['resolve']; }; -export type TransformHandler = ( - context: TransformContext, +export type TransformHandler = ( + context: TransformContext, ) => MaybePromise; export type TransformDescriptor = { @@ -449,9 +450,9 @@ export type TransformDescriptor = { order?: HookOrder; }; -export type TransformHook = ( - descriptor: TransformDescriptor, - handler: TransformHandler, +export type TransformHook = ( + descriptor: T, + handler: TransformHandler, ) => void; export type ProcessAssetsStage = diff --git a/packages/core/tests/transformRaw.test.ts b/packages/core/tests/transformRaw.test.ts new file mode 100644 index 0000000000..029df1f193 --- /dev/null +++ b/packages/core/tests/transformRaw.test.ts @@ -0,0 +1,115 @@ +import { createStubRsbuild } from '@scripts/test-helper'; + +describe('transform raw loader', () => { + it('should handle Buffer input correctly for raw transforms', async () => { + // This test verifies the typing works correctly + const rsbuild = await createStubRsbuild({ + plugins: [ + { + name: 'test-raw-transform', + setup(api) { + api.transform({ test: /\.node$/, raw: true }, ({ code }) => { + // The key test is that TypeScript allows this code to compile + // when TransformContext.code is typed as string | Buffer + const isBuffer = Buffer.isBuffer(code); + const isString = typeof code === 'string'; + + // This should work with both Buffer and string + return `export default "transformed-${isBuffer ? 'buffer' : isString ? 'string' : 'unknown'}";`; + }); + }, + }, + ], + rsbuildConfig: { + output: { + target: 'node', + }, + }, + }); + + const config = await rsbuild.unwrapConfig(); + + // Verify that the raw loader is being used + const rules = config.module?.rules || []; + const nodeRule = rules.find((rule: any) => + rule.test?.toString().includes('\\.node'), + ); + + expect(nodeRule).toBeDefined(); + expect(nodeRule.use[0].loader).toContain('transformRawLoader'); + }); + + it('should handle string input correctly for non-raw transforms', async () => { + // This test verifies that the typing works for non-raw transforms too + const rsbuild = await createStubRsbuild({ + plugins: [ + { + name: 'test-string-transform', + setup(api) { + api.transform({ test: /\.test\.js$/ }, ({ code }) => { + // The key test is that TypeScript allows this code to compile + // when TransformContext.code is typed as string | Buffer + const isBuffer = Buffer.isBuffer(code); + const isString = typeof code === 'string'; + + return `export default "transformed-${isString ? 'string' : isBuffer ? 'buffer' : 'unknown'}";`; + }); + }, + }, + ], + }); + + const config = await rsbuild.unwrapConfig(); + + // Verify that the regular loader is being used (not raw) + const rules = config.module?.rules || []; + const testRule = rules.find((rule: any) => + rule.test?.toString().includes('\\.test\\.js'), + ); + + expect(testRule).toBeDefined(); + expect(testRule.use[0].loader).toContain('transformLoader'); + expect(testRule.use[0].loader).not.toContain('transformRawLoader'); + }); + + it('should allow the nodeAddons plugin pattern to work with Buffer', () => { + // This test specifically validates the use case from the nodeAddons plugin + // where raw transforms expect Buffer input + const mockCode = Buffer.from('test binary data'); + const emitFileCalls: Array<{ name: string; content: Buffer }> = []; + + // Simulate the transform handler from nodeAddons plugin + const transformHandler = ({ + code, + emitFile, + }: { + code: string | Buffer; + emitFile: (name: string, content: Buffer) => void; + }) => { + // This should work without type errors now that code can be Buffer + emitFile('test.node', code as Buffer); + + return ` +try { +const path = require("path"); +process.dlopen(module, path.join(__dirname, "test.node")); +} catch (error) { +throw new Error('Failed to load Node.js addon: "test.node"\\n' + error); +} +`; + }; + + // This should not throw TypeScript errors + const result = transformHandler({ + code: mockCode, + emitFile: (name, content) => { + emitFileCalls.push({ name, content }); + }, + }); + + expect(emitFileCalls).toHaveLength(1); + expect(emitFileCalls[0].name).toBe('test.node'); + expect(emitFileCalls[0].content).toBe(mockCode); + expect(result).toContain('process.dlopen'); + }); +}); diff --git a/packages/core/tests/transformTyping.test.ts b/packages/core/tests/transformTyping.test.ts new file mode 100644 index 0000000000..2c3bbb00b6 --- /dev/null +++ b/packages/core/tests/transformTyping.test.ts @@ -0,0 +1,88 @@ +// Test demonstrating the improved conditional typing for TransformContext +import { createStubRsbuild } from '@scripts/test-helper'; + +describe('transform context conditional typing', () => { + it('should provide Buffer type for raw transforms', async () => { + // This test validates that when raw: true is used, the code parameter is typed as Buffer + const rsbuild = await createStubRsbuild({ + plugins: [ + { + name: 'test-buffer-typing', + setup(api) { + // This should compile without TypeScript errors and code should be typed as Buffer + api.transform( + { test: /\.buffer$/, raw: true }, + ({ code, emitFile }) => { + // Type assertion for testing - code should be Buffer when raw: true + const buffer: Buffer = code; // This should work without type errors + + // Verify it's actually a Buffer at runtime + expect(Buffer.isBuffer(code)).toBe(true); + expect(typeof code).not.toBe('string'); + + emitFile('test.bin', buffer); + return 'export default "buffer processed";'; + }, + ); + }, + }, + ], + }); + + // Verify the transform was registered + const config = await rsbuild.unwrapConfig(); + expect(config).toBeDefined(); + }); + + it('should provide string type for non-raw transforms', async () => { + // This test validates that when raw is not specified or false, the code parameter is typed as string + const rsbuild = await createStubRsbuild({ + plugins: [ + { + name: 'test-string-typing', + setup(api) { + // This should compile without TypeScript errors and code should be typed as string + api.transform({ test: /\.string$/ }, ({ code }) => { + // Type assertion for testing - code should be string when raw is not specified + const str: string = code; // This should work without type errors + + // Verify it's actually a string at runtime in normal usage + expect(typeof code).toBe('string'); + expect(Buffer.isBuffer(code)).toBe(false); + + return `${str}\n// processed`; + }); + }, + }, + ], + }); + + // Verify the transform was registered + const config = await rsbuild.unwrapConfig(); + expect(config).toBeDefined(); + }); + + it('should provide string type for explicitly non-raw transforms', async () => { + // This test validates that when raw: false is explicitly specified, code is typed as string + const rsbuild = await createStubRsbuild({ + plugins: [ + { + name: 'test-explicit-non-raw', + setup(api) { + // This should compile without TypeScript errors and code should be typed as string + api.transform({ test: /\.text$/, raw: false }, ({ code }) => { + // Type assertion for testing - code should be string when raw: false + const str: string = code; // This should work without type errors + + return `processed: ${str}`; + }); + }, + }, + ], + }); + + // Verify the transform was registered + const config = await rsbuild.unwrapConfig(); + expect(config).toBeDefined(); + }); +});