Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 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
3 changes: 2 additions & 1 deletion packages/core/src/types/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,9 @@ type TransformResult =
export type TransformContext = {
/**
* The code of the module.
* When raw is true, this will be a Buffer instead of a string.
*/
code: string;
code: string | Buffer;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot can we ensure that code is string when raw option is not used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Implemented conditional typing for TransformContext! Now code is properly typed as string when raw is not used and Buffer when raw: true.

The typing now provides:

  • TransformContext<true>code: Buffer (raw transforms)
  • TransformContext<false>code: string (non-raw transforms)
  • TransformContext (default) → code: string (backward compatible)

This ensures type safety and eliminates the need for runtime type checking in transform handlers. Fixed in commit 79fbde4.

/**
* The directory path of the currently processed module,
* which changes with the location of each processed module.
Expand Down
115 changes: 115 additions & 0 deletions packages/core/tests/transformRaw.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});