Skip to content
Open
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
3 changes: 2 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"sharp": "catalog:",
"vitest": "catalog:",
"ipx": "catalog:",
"ipx-v3": "npm:ipx@^3.1.1",
"storybook": "catalog:",
"storybook-addon-rslib": "catalog:",
"storybook-react-rsbuild": "catalog:"
Expand All @@ -96,7 +97,7 @@
"react": ">=16.9.0",
"react-dom": ">=16.9.0",
"sharp": ">=0.33.5",
"ipx": ">=3.0.3"
"ipx": "^3.0.3 || ^4.0.0-0"
},
"peerDependenciesMeta": {
"sharp": {
Expand Down
39 changes: 35 additions & 4 deletions packages/core/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { IncomingMessage, ServerResponse } from 'node:http';
import path from 'node:path';
import type { RsbuildPlugin, RsbuildPluginAPI, Rspack } from '@rsbuild/core';
import type { IPXOptions, IPXStorage } from 'ipx';
import type { IPX, IPXOptions, IPXStorage } from 'ipx';
import { withoutBase } from 'ufo';
import type { LoaderOptions } from './loader';
import { logger } from './logger';
Expand Down Expand Up @@ -61,6 +62,33 @@ async function loadIPXModule() {
}
}

type IPXNodeMiddleware = (req: IncomingMessage, res: ServerResponse) => unknown;

type IPXNodeMiddlewareFactory = (ipx: IPX) => IPXNodeMiddleware;

/**
* Create a Node.js request handler for the IPX instance.
* Uses `createIPXNodeHandler` from ipx 4 and falls back to
* `createIPXNodeServer` from ipx 3, which was removed in ipx 4.
*/
function createIPXNodeMiddleware(mod: object, ipx: IPX): IPXNodeMiddleware {
if (
'createIPXNodeHandler' in mod &&
typeof mod.createIPXNodeHandler === 'function'
) {
return (mod.createIPXNodeHandler as IPXNodeMiddlewareFactory)(ipx);
}
if (
'createIPXNodeServer' in mod &&
typeof mod.createIPXNodeServer === 'function'
) {
return (mod.createIPXNodeServer as IPXNodeMiddlewareFactory)(ipx);
}
throw new Error(
'Unsupported ipx version: expected `createIPXNodeHandler` (ipx 4) or `createIPXNodeServer` (ipx 3) to be exported.',
);
}

function createBundlerStorage(compiler: Rspack.Compiler): IPXStorage {
const useOutputFileSystem = () => {
if (!compiler.outputFileSystem) {
Expand Down Expand Up @@ -164,7 +192,7 @@ export const pluginImage = (
// Panic while leave both `ipx` & `loader` empty,
if (!ipx && !options?.loader) throw new LoaderOrIPXRequiredError();
if (!ipx) return;
const { createIPX, createIPXNodeServer } = await loadIPXModule();
const ipxModule = await loadIPXModule();
const { assetPrefix = DEFAULT_IPX_BASENAME, ...ipxOptions } = ipx;

return mergeRsbuildConfig(config, {
Expand All @@ -185,11 +213,14 @@ export const pluginImage = (

const { storage = createBundlerStorage(compiler), ...rest } =
ipxOptions;
const ipx = createIPX({ storage, ...rest });
const ipx = ipxModule.createIPX({ storage, ...rest });
logger.debug(`Created IPX with local storage from ${distPath}`);
logger.debug(`Created IPX with assetPrefix ${assetPrefix}`);

const originalMiddleware = createIPXNodeServer(ipx);
const originalMiddleware = createIPXNodeMiddleware(
ipxModule,
ipx,
);
middlewares.unshift((req, res, _next) => {
const next = () => {
logger.debug(`IPX middleware incoming request: ${req.url}`);
Expand Down
3 changes: 3 additions & 0 deletions packages/core/tests/fixtures/ipx-entry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import firefox from './images/firefox.png';

console.log(firefox);
72 changes: 72 additions & 0 deletions packages/core/tests/ipx-middleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// @vitest-environment node
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { type RsbuildDevServer, createRsbuild } from '@rsbuild/core';
import { imageSize } from 'image-size';
import { afterEach, describe, expect, it, vi } from 'vitest';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

const variants = [
{
name: 'ipx 4',
handler: 'createIPXNodeHandler',
load: () => import('ipx'),
},
{
name: 'ipx 3',
handler: 'createIPXNodeServer',
load: () => import('ipx-v3'),
},
];

describe.each(variants)(
'IPX dev middleware with $name',
({ handler, load }) => {
let server: RsbuildDevServer | undefined;

afterEach(async () => {
await server?.close();
server = undefined;
vi.doUnmock('ipx');
vi.resetModules();
});

async function startDevServer() {
vi.resetModules();
vi.doMock('ipx', load);
// Make sure the plugin sees the intended ipx major.
expect(await import('ipx')).toHaveProperty(handler);
const { pluginImage } = await import('../src/plugin');
const rsbuild = await createRsbuild({
cwd: __dirname,
rsbuildConfig: {
plugins: [pluginImage({ ipx: {} })],
source: {
entry: { index: path.resolve(__dirname, 'fixtures/ipx-entry.js') },
},
output: { filenameHash: false },
dev: { hmr: false, liveReload: false },
server: { port: 38_000, printUrls: false },
},
});
server = await rsbuild.createDevServer();
await server.listen();
await server.environments.web.getStats();
return `http://localhost:${server.port}`;
}

it('serves optimized images under the asset prefix', async () => {
const origin = await startDevServer();

const res = await fetch(
`${origin}/_rsbuild/ipx/f_webp,w_48/static/image/firefox.png`,
);

expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe('image/webp');
const size = imageSize(new Uint8Array(await res.arrayBuffer()));
expect(size).toMatchObject({ type: 'webp', width: 48 });
});
},
);
2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
},
"peerDependencies": {
"@rsbuild-image/core": "*",
"ipx": ">=3.0.3",
"ipx": "^3.0.3 || ^4.0.0-0",
"react": ">=16.9.0",
"react-dom": ">=16.9.0",
"sharp": ">=0.33.5"
Expand Down
Loading