Skip to content
Draft
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
2 changes: 1 addition & 1 deletion packages/core/src/initPlugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export function initPluginAPI({
};

let transformId = 0;
const transformer: Record<string, TransformHandler> = {};
const transformer: Record<string, TransformHandler<boolean>> = {};
const processAssetsFns: {
environment?: string;
descriptor: ProcessAssetsDescriptor;
Expand Down
15 changes: 8 additions & 7 deletions packages/core/src/types/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,11 +304,12 @@ type TransformResult =
map?: string | Rspack.sources.RawSourceMap | null;
};

export type TransformContext = {
export type TransformContext<Raw extends boolean = false> = {
/**
* 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.
Expand Down Expand Up @@ -370,8 +371,8 @@ export type TransformContext = {
resolve: Rspack.LoaderContext['resolve'];
};

export type TransformHandler = (
context: TransformContext,
export type TransformHandler<Raw extends boolean = false> = (
context: TransformContext<Raw>,
) => MaybePromise<TransformResult>;

export type TransformDescriptor = {
Expand Down Expand Up @@ -449,9 +450,9 @@ export type TransformDescriptor = {
order?: HookOrder;
};

export type TransformHook = (
descriptor: TransformDescriptor,
handler: TransformHandler,
export type TransformHook = <T extends TransformDescriptor>(
descriptor: T,
handler: TransformHandler<T['raw'] extends true ? true : false>,
) => void;

export type ProcessAssetsStage =
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');
});
});
88 changes: 88 additions & 0 deletions packages/core/tests/transformTyping.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});