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
49 changes: 35 additions & 14 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ import {
pick,
readPackageJson,
} from './utils/helper';
import {
appendPathExtension,
parsePathQueryFragment,
replacePathExtension,
} from './utils/path';
import { isDebug, logger } from './utils/logger';
import {
ESX_TO_BROWSERSLIST,
Expand Down Expand Up @@ -1418,33 +1423,39 @@ const composeBundlelessExternalConfig = (
return;
}
const { issuer } = contextInfo;
const originExtension = extname(request);
const originExtension = extname(
parsePathQueryFragment(request).path,
);

if (!resolver) {
resolver = getResolve() as RspackResolver;
}

async function redirectPath(
request: string,
): Promise<{ path?: string; isResolved: boolean }> {
async function redirectPath(request: string): Promise<{
path?: string;
isResolved: boolean;
}> {
try {
const { query, fragment } = parsePathQueryFragment(request);
let resolvedRequest = request;
// use resolver to resolve the request
resolvedRequest = await resolver!(context!, resolvedRequest);
const { path: resolvedPath } =
parsePathQueryFragment(resolvedRequest);
if (typeof outBase !== 'string') {
throw new Error(
`outBase expect to be a string in bundleless mode, but got ${outBase}`,
);
}

const isSubpath = normalizeSlash(resolvedRequest).startsWith(
const isSubpath = normalizeSlash(resolvedPath).startsWith(
`${normalizeSlash(outBase)}/`,
);

// only handle the request that within the root path
if (isSubpath) {
resolvedRequest = normalizeSlash(
path.relative(path.dirname(issuer), resolvedRequest),
path.relative(path.dirname(issuer), resolvedPath),
);
// Requests that fall through here cannot be matched by any other externals config ahead.
// Treat all these requests as relative import of source code. Node.js won't add the
Expand All @@ -1457,7 +1468,7 @@ const composeBundlelessExternalConfig = (
resolvedRequest = `./${resolvedRequest}`;
}
return {
path: resolvedRequest,
path: `${resolvedRequest}${query}${fragment}`,
isResolved: true,
};
}
Expand Down Expand Up @@ -1542,13 +1553,15 @@ const composeBundlelessExternalConfig = (
// This may result in a change in semantics,
// user should use copy to keep origin file or use another separate entry to deal this
if (resolvedRequest.startsWith('.') && isResolved) {
const ext = extname(resolvedRequest);
const requestResourcePath =
parsePathQueryFragment(resolvedRequest).path;
const ext = extname(requestResourcePath);

if (ext) {
// 1. js files hit JS_EXTENSIONS_PATTERN, ./foo.ts -> ./foo.mjs
if (JS_EXTENSIONS_PATTERN.test(resolvedRequest)) {
resolvedRequest = resolvedRequest.replace(
/\.[^.]+$/,
if (JS_EXTENSIONS_PATTERN.test(requestResourcePath)) {
resolvedRequest = replacePathExtension(
resolvedRequest,
jsRedirectExtension
? jsExtension
: JS_EXTENSIONS_PATTERN.test(originExtension)
Expand Down Expand Up @@ -1579,13 +1592,21 @@ const composeBundlelessExternalConfig = (
if (
!jsRedirectPath &&
(await isDirectory(
join(dirname(issuer), resolvedRequest),
join(
dirname(issuer),
parsePathQueryFragment(resolvedRequest).path,
),
))
) {
// This uses `/` instead of `path.join` here because `join` removes potential "./" prefixes
resolvedRequest = `${resolvedRequest.replace(/\/+$/, '')}/index`;
const { path, query, fragment } =
parsePathQueryFragment(resolvedRequest);
resolvedRequest = `${path.replace(/\/+$/, '')}/index${query}${fragment}`;
}
resolvedRequest = `${resolvedRequest}${jsExtension}`;
resolvedRequest = appendPathExtension(
resolvedRequest,
jsExtension,
);
}
}
}
Expand Down
18 changes: 16 additions & 2 deletions packages/core/src/css/cssConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,21 @@ import {
isCssFile,
isCssModulesFile,
} from './utils';
import { parsePathQueryFragment, replacePathExtension } from '../utils/path';

const require = createRequire(import.meta.url);

export const RSLIB_CSS_ENTRY_FLAG = '__rslib_css__';

type ExternalCallback = (arg0?: undefined, arg1?: string) => void;

const CSS_MODULE_EXTENSION_PATTERN = /\.module\.[^.]+$/i;

function replaceCssModulesCssExtension(filepath: string): string {
const { path, query, fragment } = parsePathQueryFragment(filepath);
return `${path.replace(CSS_MODULE_EXTENSION_PATTERN, '_module.css')}${query}${fragment}`;
}

export async function cssExternalHandler(
request: string,
callback: ExternalCallback,
Expand Down Expand Up @@ -53,10 +61,16 @@ export async function cssExternalHandler(
if (styleRedirectExtension) {
const isCssModulesRequest = isCssModulesFile(resolvedRequest, auto);
if (isCssModulesRequest) {
callback(undefined, resolvedRequest.replace(/\.[^.]+$/, jsExtension));
const { query, fragment } = parsePathQueryFragment(resolvedRequest);
callback(
undefined,
query || fragment
? replaceCssModulesCssExtension(resolvedRequest)
: replacePathExtension(resolvedRequest, jsExtension),
);
return;
}
callback(undefined, resolvedRequest.replace(/\.[^.]+$/, '.css'));
callback(undefined, replacePathExtension(resolvedRequest, '.css'));
return;
}

Expand Down
35 changes: 11 additions & 24 deletions packages/core/src/css/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import path from 'node:path';
import type { CSSLoaderOptions } from '@rsbuild/core';
import { CSS_EXTENSIONS_PATTERN } from '../constant';
import { parsePathQueryFragment } from '../utils/path';

// https://rsbuild.rs/config/output/css-modules#cssmodulesauto
export type CssLoaderOptionsAuto = CSSLoaderOptions['modules'] extends infer T
Expand Down Expand Up @@ -53,47 +54,33 @@ export function getUndoPath(
}

export function isCssFile(filepath: string): boolean {
return CSS_EXTENSIONS_PATTERN.test(filepath);
const { path } = parsePathQueryFragment(filepath);
return CSS_EXTENSIONS_PATTERN.test(path);
}

const CSS_MODULE_REG = /\.module\.\w+$/i;

/**
* This function is modified based on
* https://github.com/web-infra-dev/rspack/blob/7b80a45a1c58de7bc506dbb107fad6fda37d2a1f/packages/rspack/src/loader-runner/index.ts#L903
*/
const PATH_QUERY_FRAGMENT_REGEXP =
/^((?:\u200b.|[^?#\u200b])*)(\?(?:\u200b.|[^#\u200b])*)?(#.*)?$/;
export function parsePathQueryFragment(str: string): {
path: string;
query: string;
fragment: string;
} {
const match = PATH_QUERY_FRAGMENT_REGEXP.exec(str);
return {
path: match?.[1]?.replace(/\u200b(.)/g, '$1') || '',
query: match?.[2] ? match[2].replace(/\u200b(.)/g, '$1') : '',
fragment: match?.[3] || '',
};
}

export function isCssModulesFile(
filepath: string,
auto: CssLoaderOptionsAuto,
): boolean {
const filename = path.basename(filepath);
const {
path: resourcePath,
query,
fragment,
} = parsePathQueryFragment(filepath);
const filename = path.basename(resourcePath);
if (auto === true) {
return CSS_MODULE_REG.test(filename);
}

if (auto instanceof RegExp) {
return auto.test(filepath);
return auto.test(resourcePath);
}

if (typeof auto === 'function') {
const { path, query, fragment } = parsePathQueryFragment(filepath);
// this is a mock for loader
return auto(path, query, fragment);
return auto(resourcePath, query, fragment);
}

return false;
Expand Down
35 changes: 35 additions & 0 deletions packages/core/src/utils/path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* This function is modified based on
* https://github.com/web-infra-dev/rspack/blob/7b80a45a1c58de7bc506dbb107fad6fda37d2a1f/packages/rspack/src/loader-runner/index.ts#L903
*/
const PATH_QUERY_FRAGMENT_REGEXP =
/^((?:\u200b.|[^?#\u200b])*)(\?(?:\u200b.|[^#\u200b])*)?(#.*)?$/;

export function parsePathQueryFragment(str: string): {
path: string;
query: string;
fragment: string;
} {
const match = PATH_QUERY_FRAGMENT_REGEXP.exec(str);
return {
path: match?.[1]?.replace(/\u200b(.)/g, '$1') || '',
query: match?.[2] ? match[2].replace(/\u200b(.)/g, '$1') : '',
fragment: match?.[3] || '',
};
}

export function appendPathExtension(
filepath: string,
extension: string,
): string {
const { path, query, fragment } = parsePathQueryFragment(filepath);
return `${path}${extension}${query}${fragment}`;
}

export function replacePathExtension(
filepath: string,
extension: string,
): string {
const { path, query, fragment } = parsePathQueryFragment(filepath);
return `${path.replace(/\.[^.]+$/, extension)}${query}${fragment}`;
}
23 changes: 23 additions & 0 deletions tests/integration/redirect/js.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ test('redirect.js default', async () => {

expect(esmResult.default).toEqual(cjsResult.default);
expect(esmResult.default).toMatchInlineSnapshot(`"FOOBAR1FOOBAR1BAZSTRING"`); // cspell:disable-line

const { content: queryJs } = queryContent(contents.esm0!, /esm\/query\.js/);
const { content: queryCjs } = queryContent(contents.cjs0!, /cjs\/query\.cjs/);
expect(queryJs).toContain(
'import { queryTarget } from "./query-target.js?query";',
);
expect(queryCjs).toContain('require("./query-target.cjs?query")');
});

test('redirect.js.path false', async () => {
Expand Down Expand Up @@ -71,6 +78,13 @@ test('redirect.js.path false', async () => {
export default src;
"
`);

const { content: queryJs } = queryContent(contents.esm1!, /esm\/query\.js/);
const { content: queryCjs } = queryContent(contents.cjs1!, /cjs\/query\.cjs/);
expect(queryJs).toContain(
'import { queryTarget } from "./query-target.js?query";',
);
expect(queryCjs).toContain('require("./query-target.cjs?query")');
});

test('redirect.js.path with user override externals', async () => {
Expand Down Expand Up @@ -177,4 +191,13 @@ test('redirect.js.extension: false', async () => {
export default src;
"
`);

const { content: queryJs } = queryContent(contents.esm4!, /esm\/query\.js/);
const { content: queryCjs } = queryContent(contents.cjs4!, /cjs\/query\.cjs/);
expect(queryJs).toContain(
'import { queryTarget } from "./query-target?query";',
);
expect(queryJs).toContain('from "./query-target.ts?query"');
expect(queryCjs).toContain('require("./query-target?query")');
expect(queryCjs).toContain('require("./query-target.ts?query")');
});
1 change: 1 addition & 0 deletions tests/integration/redirect/js/src/query-target.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const queryTarget = 'query-target';
4 changes: 4 additions & 0 deletions tests/integration/redirect/js/src/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { queryTarget as extensionlessQueryTarget } from './query-target?query';
import { queryTarget as tsQueryTarget } from './query-target.ts?query';

export const query = extensionlessQueryTarget + tsQueryTarget;
Loading
Loading