diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 30b6db02b..9b7d8383a 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -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, @@ -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 @@ -1457,7 +1468,7 @@ const composeBundlelessExternalConfig = ( resolvedRequest = `./${resolvedRequest}`; } return { - path: resolvedRequest, + path: `${resolvedRequest}${query}${fragment}`, isResolved: true, }; } @@ -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) @@ -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, + ); } } } diff --git a/packages/core/src/css/cssConfig.ts b/packages/core/src/css/cssConfig.ts index 48ed96fa7..9d1b962de 100644 --- a/packages/core/src/css/cssConfig.ts +++ b/packages/core/src/css/cssConfig.ts @@ -6,6 +6,7 @@ import { isCssFile, isCssModulesFile, } from './utils'; +import { parsePathQueryFragment, replacePathExtension } from '../utils/path'; const require = createRequire(import.meta.url); @@ -13,6 +14,13 @@ 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, @@ -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; } diff --git a/packages/core/src/css/utils.ts b/packages/core/src/css/utils.ts index 4de7e5570..e79bdf528 100644 --- a/packages/core/src/css/utils.ts +++ b/packages/core/src/css/utils.ts @@ -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 @@ -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; diff --git a/packages/core/src/utils/path.ts b/packages/core/src/utils/path.ts new file mode 100644 index 000000000..2a6f0a8ea --- /dev/null +++ b/packages/core/src/utils/path.ts @@ -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}`; +} diff --git a/tests/integration/redirect/js.test.ts b/tests/integration/redirect/js.test.ts index 0d3bed46f..6ca8cbdc0 100644 --- a/tests/integration/redirect/js.test.ts +++ b/tests/integration/redirect/js.test.ts @@ -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 () => { @@ -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 () => { @@ -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")'); }); diff --git a/tests/integration/redirect/js/src/query-target.ts b/tests/integration/redirect/js/src/query-target.ts new file mode 100644 index 000000000..93413f700 --- /dev/null +++ b/tests/integration/redirect/js/src/query-target.ts @@ -0,0 +1 @@ +export const queryTarget = 'query-target'; diff --git a/tests/integration/redirect/js/src/query.ts b/tests/integration/redirect/js/src/query.ts new file mode 100644 index 000000000..7896f96d1 --- /dev/null +++ b/tests/integration/redirect/js/src/query.ts @@ -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; diff --git a/tests/integration/redirect/style.test.ts b/tests/integration/redirect/style.test.ts index beb1feb3d..ae5ef9b1e 100644 --- a/tests/integration/redirect/style.test.ts +++ b/tests/integration/redirect/style.test.ts @@ -20,9 +20,11 @@ test('0. default', async () => { ); expect(cssIndexJs).toMatchInlineSnapshot(` "import "./index.css"; + import "./index.css?inline"; " `); expect(cssIndexCjs).toContain('require("./index.css");'); + expect(cssIndexCjs).toContain('require("./index.css?inline");'); const { content: cssModuleIndexJs } = queryContent( contents.esm0!, @@ -35,9 +37,15 @@ test('0. default', async () => { expect(cssModuleIndexJs).toContain( 'import index_module from "./index.module.js";', ); + expect(cssModuleIndexJs).toContain( + 'import index_moduleinline from "./index_module.css?inline";', + ); expect(cssModuleIndexCjs).toContain( 'const external_index_module_cjs_namespaceObject = require("./index.module.cjs");', ); + expect(cssModuleIndexCjs).toContain( + 'const external_index_moduleinline_namespaceObject = require("./index_module.css?inline");', + ); }); test('1. style.path: false', () => { @@ -51,9 +59,11 @@ test('1. style.path: false', () => { ); expect(cssIndexJs).toMatchInlineSnapshot(` "import "@/less/index.css"; + import "@/less/index.css?inline"; " `); expect(cssIndexCjs).toContain('require("@/less/index.css");'); + expect(cssIndexCjs).toContain('require("@/less/index.css?inline");'); const { content: cssModuleIndexJs } = queryContent( contents.esm1!, @@ -66,9 +76,15 @@ test('1. style.path: false', () => { expect(cssModuleIndexJs).toContain( 'import index_module from "@/module/index.module.js";', ); + expect(cssModuleIndexJs).toContain( + 'import index_moduleinline from "@/module/index_module.css?inline";', + ); expect(cssModuleIndexCjs).toContain( 'const index_module_cjs_namespaceObject = require("@/module/index.module.cjs");', ); + expect(cssModuleIndexCjs).toContain( + 'const index_moduleinline_namespaceObject = require("@/module/index_module.css?inline");', + ); }); test('2. style.extension: false', async () => { @@ -82,9 +98,11 @@ test('2. style.extension: false', async () => { ); expect(cssIndexJs).toMatchInlineSnapshot(` "import "./index.less"; + import "./index.less?inline"; " `); expect(cssIndexCjs).toContain('require("./index.less");'); + expect(cssIndexCjs).toContain('require("./index.less?inline");'); const { content: cssModuleIndexJs } = queryContent( contents.esm2!, @@ -97,9 +115,15 @@ test('2. style.extension: false', async () => { expect(cssModuleIndexJs).toContain( 'import index_module from "./index.module.less";', ); + expect(cssModuleIndexJs).toContain( + 'import index_moduleinline from "./index.module.less?inline";', + ); expect(cssModuleIndexCjs).toContain( 'const external_index_module_less_namespaceObject = require("./index.module.less");', ); + expect(cssModuleIndexCjs).toContain( + 'const external_index_moduleinline_namespaceObject = require("./index.module.less?inline");', + ); }); test('3. style.path: false, style.extension: false', async () => { @@ -113,9 +137,11 @@ test('3. style.path: false, style.extension: false', async () => { ); expect(cssIndexJs).toMatchInlineSnapshot(` "import "@/less/index.less"; + import "@/less/index.less?inline"; " `); expect(cssIndexCjs).toContain('require("@/less/index.css");'); + expect(cssIndexCjs).toContain('require("@/less/index.css?inline");'); const { content: cssModuleIndexJs } = queryContent( contents.esm3!, @@ -128,9 +154,15 @@ test('3. style.path: false, style.extension: false', async () => { expect(cssModuleIndexJs).toContain( 'import index_module from "@/module/index.module.less";', ); + expect(cssModuleIndexJs).toContain( + 'import index_moduleinline from "@/module/index.module.less?inline";', + ); expect(cssModuleIndexCjs).toContain( 'const index_module_cjs_namespaceObject = require("@/module/index.module.cjs");', ); + expect(cssModuleIndexCjs).toContain( + 'const index_moduleinline_namespaceObject = require("@/module/index_module.css?inline");', + ); }); test('should external 3rd packages CSS', async () => { @@ -144,9 +176,11 @@ test('should external 3rd packages CSS', async () => { ); expect(cssIndexJs).toMatchInlineSnapshot(` "import "./index.css"; + import "./index.css?inline"; " `); expect(cssIndexCjs).toContain('require("./index.css");'); + expect(cssIndexCjs).toContain('require("./index.css?inline");'); const { content: pkgIndexJs } = queryContent( contents.esm0!, diff --git a/tests/integration/redirect/style/src/less/index.ts b/tests/integration/redirect/style/src/less/index.ts index 1a7941cf3..eac87ccb3 100644 --- a/tests/integration/redirect/style/src/less/index.ts +++ b/tests/integration/redirect/style/src/less/index.ts @@ -1 +1,2 @@ import '@/less/index.less'; +import '@/less/index.less?inline'; diff --git a/tests/integration/redirect/style/src/module/index.ts b/tests/integration/redirect/style/src/module/index.ts index c87e89278..8f10154bf 100644 --- a/tests/integration/redirect/style/src/module/index.ts +++ b/tests/integration/redirect/style/src/module/index.ts @@ -1,4 +1,7 @@ // @ts-ignore env.d.ts import styles from '@/module/index.module.less'; +// @ts-ignore env.d.ts +import inlineStyles from '@/module/index.module.less?inline'; console.log('styles: ', styles); +console.log('inline styles: ', inlineStyles);