From d16af1d6636a8ac4cf01c78b36629770331faa4a Mon Sep 17 00:00:00 2001 From: Timeless0911 <50201324+Timeless0911@users.noreply.github.com> Date: Tue, 11 Feb 2025 18:01:56 +0800 Subject: [PATCH] feat: support DTS redirect (#742) --- packages/core/src/config.ts | 3 +- packages/core/src/types/config.ts | 14 +- packages/plugin-dts/README.md | 71 ++++++ packages/plugin-dts/package.json | 4 +- packages/plugin-dts/src/dts.ts | 6 + packages/plugin-dts/src/index.ts | 10 +- packages/plugin-dts/src/tsc.ts | 31 ++- packages/plugin-dts/src/utils.ts | 222 +++++++++++++++++- pnpm-lock.yaml | 120 ++++++++++ tests/integration/redirect/dts.test.ts | 166 +++++++++++++ .../redirect/dts/compile/rslog/index.d.ts | 4 + .../redirect/dts/compile/rslog/index.js | 3 + .../redirect/dts/compile/rslog/package.json | 4 + tests/integration/redirect/dts/package.json | 14 ++ .../integration/redirect/dts/rslib.config.ts | 60 +++++ tests/integration/redirect/dts/src/foo/foo.ts | 5 + tests/integration/redirect/dts/src/index.ts | 12 + tests/integration/redirect/dts/src/logger.ts | 25 ++ tests/integration/redirect/dts/src/types.ts | 17 ++ tests/integration/redirect/dts/tsconfig.json | 12 + website/docs/en/config/lib/redirect.mdx | 151 +++++++++--- .../en/guide/migration/modernjs-module.mdx | 4 +- website/docs/zh/config/lib/redirect.mdx | 151 +++++++++--- website/docs/zh/guide/basic/typescript.mdx | 2 +- .../zh/guide/migration/modernjs-module.mdx | 4 +- 25 files changed, 1025 insertions(+), 90 deletions(-) create mode 100644 tests/integration/redirect/dts.test.ts create mode 100644 tests/integration/redirect/dts/compile/rslog/index.d.ts create mode 100644 tests/integration/redirect/dts/compile/rslog/index.js create mode 100644 tests/integration/redirect/dts/compile/rslog/package.json create mode 100644 tests/integration/redirect/dts/package.json create mode 100644 tests/integration/redirect/dts/rslib.config.ts create mode 100644 tests/integration/redirect/dts/src/foo/foo.ts create mode 100644 tests/integration/redirect/dts/src/index.ts create mode 100644 tests/integration/redirect/dts/src/logger.ts create mode 100644 tests/integration/redirect/dts/src/types.ts create mode 100644 tests/integration/redirect/dts/tsconfig.json diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index c82dff358..96f8f5cd8 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -1217,7 +1217,7 @@ const composeDtsConfig = async ( libConfig: LibConfig, dtsExtension: string, ): Promise => { - const { format, autoExternal, banner, footer } = libConfig; + const { format, autoExternal, banner, footer, redirect } = libConfig; let { dts } = libConfig; @@ -1243,6 +1243,7 @@ const composeDtsConfig = async ( autoExternal: getAutoExternalDefaultValue(format!, autoExternal), banner: banner?.dts, footer: footer?.dts, + redirect: redirect?.dts, }), ], }; diff --git a/packages/core/src/types/config.ts b/packages/core/src/types/config.ts index f183458d8..030f31620 100644 --- a/packages/core/src/types/config.ts +++ b/packages/core/src/types/config.ts @@ -177,9 +177,16 @@ export type StyleRedirect = { extension?: boolean; }; -// @ts-expect-error TODO: support dts redirect in the future -type DtsRedirect = { +export type DtsRedirect = { + /** + * Whether to automatically redirect the import paths of TypeScript declaration output files. + * @defaultValue `true` + */ path?: boolean; + /** + * Whether to automatically redirect the file extension to import paths based on the TypeScript declaration output files. + * @defaultValue `false` + */ extension?: boolean; }; @@ -190,7 +197,8 @@ export type Redirect = { style?: StyleRedirect; /** Controls the redirect of the import paths of output asset files. */ asset?: boolean; - // dts?: DtsRedirect; + /** Controls the redirect of the import paths of output TypeScript declaration files. */ + dts?: DtsRedirect; }; export interface LibConfig extends EnvironmentConfig { diff --git a/packages/plugin-dts/README.md b/packages/plugin-dts/README.md index 8da144229..0991d1074 100644 --- a/packages/plugin-dts/README.md +++ b/packages/plugin-dts/README.md @@ -163,6 +163,77 @@ pluginDts({ }); ``` +### redirect + +- **Type:** + +```ts +type DtsRedirect = { + path?: boolean; + extension?: boolean; +}; +``` + +- **Default:** + +```ts +const defaultRedirect = { + path: true, + extension: false, +}; +``` + +Controls the redirect of the import paths of output TypeScript declaration files. + +```js +pluginDts({ + redirect: { + path: true, + extension: false, + }, +}); +``` + +#### redirect.path + +- **Type:** `boolean` +- **Default:** `true` + +Whether to automatically redirect the import paths of TypeScript declaration output files. + +- When set to `true`, Rslib will redirect the import path in the DTS output file to the corresponding relative path based on the [compilerOptions.paths](https://typescriptlang.org/tsconfig#paths) configured in `tsconfig.json`. + +```ts +// `compilerOptions.paths` is set to `{ "@/*": ["src/*"] }` +import { foo } from '@/foo'; // source code of './src/bar.ts' ↓ +import { foo } from './foo'; // expected output of './dist/bar.d.ts' + +import { foo } from '@/foo'; // source code of './src/utils/index.ts' ↓ +import { foo } from '../foo'; // expected output './dist/utils/index.d.ts' +``` + +- When set to `false`, the original import path will remain unchanged. + +#### redirect.extension + +- **Type:** `boolean` +- **Default:** `false` + +Whether to automatically redirect the file extension to import paths based on the TypeScript declaration output files. + +- When set to `true`, the import paths in DTS files will be redirected to the corresponding JavaScript extension which can be resolved to corresponding DTS file. The extension of the DTS output file is related to the `dtsExtension` configuration. + +```ts +// `dtsExtension` is set to `.d.mts` +import { foo } from './foo'; // source code of './src/bar.ts' ↓ +import { foo } from './foo.mjs'; // expected output of './dist/bar.d.mts' + +import { foo } from './foo.ts'; // source code of './src/utils/index.ts' ↓ +import { foo } from './foo.mjs'; // expected output './dist/utils/index.d.mts' +``` + +- When set to `false`, the file extension will remain unchanged from the original import path in the rewritten import path of the output file (regardless of whether it is specified or specified as any value). + ## Contributing Please read the [Contributing Guide](https://github.com/web-infra-dev/rslib/blob/main/CONTRIBUTING.md). diff --git a/packages/plugin-dts/package.json b/packages/plugin-dts/package.json index 74181636b..ff4be6f77 100644 --- a/packages/plugin-dts/package.json +++ b/packages/plugin-dts/package.json @@ -29,9 +29,11 @@ "dev": "rslib build --watch" }, "dependencies": { + "@ast-grep/napi": "^0.34.4", "magic-string": "^0.30.17", "picocolors": "1.1.1", - "tinyglobby": "^0.2.10" + "tinyglobby": "^0.2.10", + "tsconfig-paths": "^4.2.0" }, "devDependencies": { "@microsoft/api-extractor": "^7.49.2", diff --git a/packages/plugin-dts/src/dts.ts b/packages/plugin-dts/src/dts.ts index 4cecde1af..c62f69ce0 100644 --- a/packages/plugin-dts/src/dts.ts +++ b/packages/plugin-dts/src/dts.ts @@ -129,6 +129,10 @@ export async function generateDts(data: DtsGenOptions): Promise { userExternals, banner, footer, + redirect = { + path: true, + extension: false, + }, } = data; logger.start(`Generating DTS... ${color.gray(`(${name})`)}`); @@ -252,6 +256,8 @@ export async function generateDts(data: DtsGenOptions): Promise { tsConfigResult, declarationDir, dtsExtension, + redirect, + rootDir, banner, footer, }, diff --git a/packages/plugin-dts/src/index.ts b/packages/plugin-dts/src/index.ts index 30680142a..001aea64d 100644 --- a/packages/plugin-dts/src/index.ts +++ b/packages/plugin-dts/src/index.ts @@ -9,6 +9,11 @@ import { loadTsconfig, processSourceEntry } from './utils'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); +export type DtsRedirect = { + path?: boolean; + extension?: boolean; +}; + export type PluginDtsOptions = { bundle?: boolean; distPath?: string; @@ -24,6 +29,7 @@ export type PluginDtsOptions = { }; banner?: string; footer?: string; + redirect?: DtsRedirect; }; export type DtsEntry = { @@ -53,7 +59,6 @@ export const PLUGIN_DTS_NAME = 'rsbuild:dts'; // use ts compiler API to generate bundleless dts // use ts compiler API and api-extractor to generate dts bundle -// TODO: deal alias in dts export const pluginDts = (options: PluginDtsOptions = {}): RsbuildPlugin => ({ name: PLUGIN_DTS_NAME, @@ -61,6 +66,9 @@ export const pluginDts = (options: PluginDtsOptions = {}): RsbuildPlugin => ({ options.bundle = options.bundle ?? false; options.abortOnError = options.abortOnError ?? true; options.build = options.build ?? false; + options.redirect = options.redirect ?? {}; + options.redirect.path = options.redirect.path ?? true; + options.redirect.extension = options.redirect.extension ?? false; const dtsPromises: Promise[] = []; let promisesResult: TaskResult[] = []; diff --git a/packages/plugin-dts/src/tsc.ts b/packages/plugin-dts/src/tsc.ts index e3e81237c..ec00e931c 100644 --- a/packages/plugin-dts/src/tsc.ts +++ b/packages/plugin-dts/src/tsc.ts @@ -1,6 +1,7 @@ import { logger } from '@rsbuild/core'; import color from 'picocolors'; import ts from 'typescript'; +import type { DtsRedirect } from './index'; import { getFileLoc, getTimeCost, processDtsFiles } from './utils'; export type EmitDtsOptions = { @@ -10,6 +11,8 @@ export type EmitDtsOptions = { tsConfigResult: ts.ParsedCommandLine; declarationDir: string; dtsExtension: string; + rootDir: string; + redirect: DtsRedirect; banner?: string; footer?: string; }; @@ -21,6 +24,8 @@ async function handleDiagnosticsAndProcessFiles( bundle: boolean, declarationDir: string, dtsExtension: string, + redirect: DtsRedirect, + rootDir: string, banner?: string, footer?: string, name?: string, @@ -36,7 +41,16 @@ async function handleDiagnosticsAndProcessFiles( diagnosticMessages.push(message); } - await processDtsFiles(bundle, declarationDir, dtsExtension, banner, footer); + await processDtsFiles( + bundle, + declarationDir, + dtsExtension, + redirect, + configPath, + rootDir, + banner, + footer, + ); if (diagnosticMessages.length) { logger.error( @@ -65,8 +79,10 @@ export async function emitDts( declarationDir, name, dtsExtension, + rootDir, banner, footer, + redirect, } = options; const { options: rawCompilerOptions, @@ -131,6 +147,9 @@ export async function emitDts( bundle, declarationDir, dtsExtension, + redirect, + configPath, + rootDir, banner, footer, ); @@ -143,6 +162,9 @@ export async function emitDts( bundle, declarationDir, dtsExtension, + redirect, + configPath, + rootDir, banner, footer, ); @@ -179,6 +201,8 @@ export async function emitDts( bundle, declarationDir, dtsExtension, + redirect, + rootDir, banner, footer, name, @@ -211,6 +235,8 @@ export async function emitDts( bundle, declarationDir, dtsExtension, + redirect, + rootDir, banner, footer, name, @@ -243,6 +269,9 @@ export async function emitDts( bundle, declarationDir, dtsExtension, + redirect, + configPath, + rootDir, banner, footer, ); diff --git a/packages/plugin-dts/src/utils.ts b/packages/plugin-dts/src/utils.ts index ff93bfa25..3f1b395ef 100644 --- a/packages/plugin-dts/src/utils.ts +++ b/packages/plugin-dts/src/utils.ts @@ -1,13 +1,42 @@ import fs from 'node:fs'; import fsP from 'node:fs/promises'; import { platform } from 'node:os'; -import path, { basename, dirname, join, relative, resolve } from 'node:path'; +import path, { + basename, + dirname, + extname, + join, + normalize, + relative, + resolve, +} from 'node:path'; +import { type NapiConfig, parseAsync } from '@ast-grep/napi'; import { type RsbuildConfig, logger } from '@rsbuild/core'; import MagicString from 'magic-string'; import color from 'picocolors'; import { convertPathToPattern, glob } from 'tinyglobby'; +import { type MatchPath, createMatchPath, loadConfig } from 'tsconfig-paths'; import ts from 'typescript'; -import type { DtsEntry } from './index'; +import type { DtsEntry, DtsRedirect } from './index'; + +const JS_EXTENSIONS: string[] = [ + 'js', + 'mjs', + 'jsx', + '(? { - if (!banner && !footer) { - return; - } - - const content = await fsP.readFile(file, 'utf-8'); + const content = await fsP.readFile(dtsFile, 'utf-8'); const code = new MagicString(content); if (banner && !content.trimStart().startsWith(banner.trim())) { @@ -135,7 +160,148 @@ export async function addBannerAndFooter( } if (code.hasChanged()) { - await fsP.writeFile(file, code.toString()); + await fsP.writeFile(dtsFile, code.toString()); + } +} + +export async function redirectDtsImports( + dtsFile: string, + dtsExtension: string, + redirect: DtsRedirect, + matchPath: MatchPath, + outDir: string, + rootDir: string, +): Promise { + const content = await fsP.readFile(dtsFile, 'utf-8'); + const code = new MagicString(content); + const sgNode = (await parseAsync('typescript', content)).root(); + const matcher: NapiConfig = { + rule: { + kind: 'string_fragment', + any: [ + { + inside: { + stopBy: 'end', + kind: 'import_statement', + field: 'source', + }, + }, + { + inside: { + stopBy: 'end', + kind: 'export_statement', + field: 'source', + }, + }, + { + inside: { + kind: 'string', + inside: { + kind: 'arguments', + inside: { + kind: 'call_expression', + has: { + field: 'function', + regex: '^(import|require)$', + }, + }, + }, + }, + }, + ], + }, + }; + const matchModule = sgNode.findAll(matcher).map((matchNode) => { + return { + n: matchNode.text(), + s: matchNode.range().start.index, + e: matchNode.range().end.index, + }; + }); + const extensions = dtsExtension + .replace(/\.d\.ts$/, '.js') + .replace(/\.d\.cts$/, '.cjs') + .replace(/\.d\.mts$/, '.mjs'); + + for (const imp of matchModule) { + const { n: importPath, s: start, e: end } = imp; + + if (!importPath) continue; + + try { + const absoluteImportPath = matchPath(importPath, undefined, undefined, [ + '.jsx', + '.tsx', + '.js', + '.ts', + '.mjs', + '.mts', + '.cjs', + '.cts', + ]); + + let redirectImportPath = importPath; + + if (absoluteImportPath && redirect.path) { + const isOutsideRootdir = !absoluteImportPath.startsWith( + rootDir + path.sep, + ); + + if (isOutsideRootdir) { + const relativePath = relative(dirname(dtsFile), absoluteImportPath); + redirectImportPath = relativePath.startsWith('..') + ? relativePath + : `./${relativePath}`; + } else { + const originalFilePath = resolve(rootDir, relative(outDir, dtsFile)); + const originalSourceDir = dirname(originalFilePath); + const relativePath = relative(originalSourceDir, absoluteImportPath); + redirectImportPath = relativePath.startsWith('..') + ? relativePath + : `./${relativePath}`; + } + } + + const ext = extname(redirectImportPath); + + if (ext) { + if (JS_EXTENSIONS_PATTERN.test(redirectImportPath)) { + if (redirect.extension) { + redirectImportPath = redirectImportPath.replace( + /\.[^.]+$/, + extensions, + ); + } + } + } else { + if ( + absoluteImportPath && + normalize(absoluteImportPath).startsWith(normalize(rootDir)) + ) { + if (redirect.extension) { + redirectImportPath = `${redirectImportPath}${extensions}`; + } + } + + if (!absoluteImportPath && importPath.startsWith('.')) { + if (redirect.extension) { + redirectImportPath = `${redirectImportPath}${extensions}`; + } + } + } + + const normalizedRedirectImportPath = redirectImportPath + .split(path.sep) + .join('/'); + + code.overwrite(start, end, normalizedRedirectImportPath); + } catch (err) { + logger.debug(err); + } + } + + if (code.hasChanged()) { + await fsP.writeFile(dtsFile, code.toString()); } } @@ -143,6 +309,9 @@ export async function processDtsFiles( bundle: boolean, dir: string, dtsExtension: string, + redirect: DtsRedirect, + tsconfigPath: string, + rootDir: string, banner?: string, footer?: string, ): Promise { @@ -150,13 +319,46 @@ export async function processDtsFiles( return; } + let matchPath: MatchPath | undefined; + + if (redirect.path || redirect.extension) { + const result = loadConfig(tsconfigPath); + + if (result.resultType === 'failed') { + logger.error(result.message); + return; + } + + const { absoluteBaseUrl, paths, mainFields, addMatchAll } = result; + matchPath = createMatchPath( + absoluteBaseUrl, + paths, + mainFields, + addMatchAll, + ); + } + const dtsFiles = await glob(convertPath(join(dir, '/**/*.d.ts')), { absolute: true, }); for (const file of dtsFiles) { try { - await addBannerAndFooter(file, banner, footer); + if (banner || footer) { + await addBannerAndFooter(file, banner, footer); + } + + if ((redirect.path || redirect.extension) && matchPath) { + await redirectDtsImports( + file, + dtsExtension, + redirect, + matchPath, + dir, + rootDir, + ); + } + const newFile = file.replace('.d.ts', dtsExtension); fs.renameSync(file, newFile); } catch (error) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6b5878e1..c53bc5570 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -350,6 +350,9 @@ importers: packages/plugin-dts: dependencies: + '@ast-grep/napi': + specifier: ^0.34.4 + version: 0.34.4 magic-string: specifier: ^0.30.17 version: 0.30.17 @@ -359,6 +362,9 @@ importers: tinyglobby: specifier: ^0.2.10 version: 0.2.10 + tsconfig-paths: + specifier: ^4.2.0 + version: 4.2.0 devDependencies: '@microsoft/api-extractor': specifier: ^7.49.2 @@ -769,6 +775,23 @@ importers: tests/integration/redirect/asset: {} + tests/integration/redirect/dts: + devDependencies: + '@rslib/core': + specifier: workspace:* + version: link:../../../../packages/core + '@types/express': + specifier: ^5.0.0 + version: 5.0.0 + express: + specifier: ^4.21.2 + version: 4.21.2 + typescript: + specifier: ^5.7.3 + version: 5.7.3 + + tests/integration/redirect/dts/compile/rslog: {} + tests/integration/redirect/js: devDependencies: '@types/lodash': @@ -983,6 +1006,64 @@ packages: react: '>=16' react-dom: '>=16' + '@ast-grep/napi-darwin-arm64@0.34.4': + resolution: {integrity: sha512-igpZwfWZeG/iwa2m7/3RKMg6UnVOt5tPCPKPMfjX2qiD1oHgcNKwTBmUmXhGCyNgwBlW8z06iXnwNIA0i+rJrw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@ast-grep/napi-darwin-x64@0.34.4': + resolution: {integrity: sha512-OqLjev/+IUW7lpLirDx7wb3iAhQkinMx9El8NWV0AipcJsbfJhQmUy2ppDqkglJVcdlh0NIaoTgWOXWya8yXzQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@ast-grep/napi-linux-arm64-gnu@0.34.4': + resolution: {integrity: sha512-PtdV6ll45O4iLKsChDLtAdyvG5R+Y/sCsatxT5T4JTptQ5rsbOeA4ZDTU8ZJJ6qBlVkhlpKM6aBBdP3HUU8HLQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@ast-grep/napi-linux-arm64-musl@0.34.4': + resolution: {integrity: sha512-Vm+xWUUWkGmv22ov9/D9i0JLds6+46Wj9G6tdByLhlB/R3jJEcWfg2qxnLihEFwJXuk+ic3PdtR0XmLRIx66Rw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@ast-grep/napi-linux-x64-gnu@0.34.4': + resolution: {integrity: sha512-O6NMaYIzVFq5uSmo/wPIAZkTqfOERr9biECjoMpMVWbPP1T9NfSoWeuYQu+W/zF/ucZDEDWGjsE/ZDNAG5SZgQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@ast-grep/napi-linux-x64-musl@0.34.4': + resolution: {integrity: sha512-QAwcB716F84fPXcVjYjKL8sQ0iQONOZ9CkdoEmmY5YW/yxR8uEA6s0bLwWPqHl6QeUuDIlA3+hgHwarWoUrPrA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@ast-grep/napi-win32-arm64-msvc@0.34.4': + resolution: {integrity: sha512-dYvmAPo+p/jH6hBIxKd+T2u0STHp02cz4O6cuFFwQG13cIysXYRb4BnF1/wtKM0gFxeSsVZCE/ANoin1O3TJTQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@ast-grep/napi-win32-ia32-msvc@0.34.4': + resolution: {integrity: sha512-iYk6L8Jxfk/AlUi2Ups2PHkKDwFP3Cl1DrScu9dL+nePNBIZVmyjN122PHdFtgmRFWjff6hGHWINHsESCGwsXg==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@ast-grep/napi-win32-x64-msvc@0.34.4': + resolution: {integrity: sha512-Filq6uuYVHJPPpoNeGgYtbpzt1ElmQvK3CP7OoEoICX3zN3mff6s9fLhMIsO8wm4+PZpT8FrOQTB9XWPLjU4zw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@ast-grep/napi@0.34.4': + resolution: {integrity: sha512-QesKWYy+DXiRQll5Wi2hDRPQ1fOWp2gdF1TFyvuZQZoPu2kVVTOOGX0ajpMm4D2mS9N5n0NB4fsG14ukGOnAlg==} + engines: {node: '>= 10'} + '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -6659,6 +6740,45 @@ snapshots: transitivePeerDependencies: - '@types/react' + '@ast-grep/napi-darwin-arm64@0.34.4': + optional: true + + '@ast-grep/napi-darwin-x64@0.34.4': + optional: true + + '@ast-grep/napi-linux-arm64-gnu@0.34.4': + optional: true + + '@ast-grep/napi-linux-arm64-musl@0.34.4': + optional: true + + '@ast-grep/napi-linux-x64-gnu@0.34.4': + optional: true + + '@ast-grep/napi-linux-x64-musl@0.34.4': + optional: true + + '@ast-grep/napi-win32-arm64-msvc@0.34.4': + optional: true + + '@ast-grep/napi-win32-ia32-msvc@0.34.4': + optional: true + + '@ast-grep/napi-win32-x64-msvc@0.34.4': + optional: true + + '@ast-grep/napi@0.34.4': + optionalDependencies: + '@ast-grep/napi-darwin-arm64': 0.34.4 + '@ast-grep/napi-darwin-x64': 0.34.4 + '@ast-grep/napi-linux-arm64-gnu': 0.34.4 + '@ast-grep/napi-linux-arm64-musl': 0.34.4 + '@ast-grep/napi-linux-x64-gnu': 0.34.4 + '@ast-grep/napi-linux-x64-musl': 0.34.4 + '@ast-grep/napi-win32-arm64-msvc': 0.34.4 + '@ast-grep/napi-win32-ia32-msvc': 0.34.4 + '@ast-grep/napi-win32-x64-msvc': 0.34.4 + '@babel/code-frame@7.26.2': dependencies: '@babel/helper-validator-identifier': 7.25.9 diff --git a/tests/integration/redirect/dts.test.ts b/tests/integration/redirect/dts.test.ts new file mode 100644 index 000000000..6633c1c30 --- /dev/null +++ b/tests/integration/redirect/dts.test.ts @@ -0,0 +1,166 @@ +import path from 'node:path'; +import { buildAndGetResults } from 'test-helper'; +import { beforeAll, expect, test } from 'vitest'; + +let contents: Awaited>['contents']; + +beforeAll(async () => { + const fixturePath = path.resolve(__dirname, './dts'); + contents = (await buildAndGetResults({ fixturePath, type: 'dts' })).contents; +}); + +test('redirect.dts default', async () => { + expect(contents.esm0).toMatchInlineSnapshot(` + { + "/tests/integration/redirect/dts/dist/default/esm/foo/foo.d.ts": "import { logRequest } from '../logger'; + import { logger } from '../../../../compile/rslog'; + import { logRequest as logRequest2 } from '../logger'; + export { logRequest, logRequest2, logger }; + ", + "/tests/integration/redirect/dts/dist/default/esm/index.d.ts": "import { logRequest } from './logger'; + import { logger } from '../../../compile/rslog'; + import type { LoggerOptions } from './types'; + import { defaultOptions } from './types.js'; + export { logRequest, logger, type LoggerOptions, defaultOptions }; + export type { Foo } from './types'; + export type { Bar } from './types'; + export * from './types'; + export * from '../../../compile/rslog'; + export * from './logger'; + ", + "/tests/integration/redirect/dts/dist/default/esm/logger.d.ts": "import type { Request } from 'express'; + import type { LoggerOptions } from './types'; + export declare function logRequest(req: Request, options: LoggerOptions): void; + ", + "/tests/integration/redirect/dts/dist/default/esm/types.d.ts": "export interface LoggerOptions { + logLevel: 'info' | 'debug' | 'warn' | 'error'; + logBody: boolean; + } + export declare const defaultOptions: LoggerOptions; + export interface Foo { + foo: string; + } + export interface Bar { + bar: string; + } + ", + } + `); +}); + +test('redirect.dts.path false', async () => { + expect(contents.esm1).toMatchInlineSnapshot(` + { + "/tests/integration/redirect/dts/dist/path-false/esm/foo/foo.d.ts": "import { logRequest } from '@src/logger'; + import { logger } from 'rslog'; + import { logRequest as logRequest2 } from '../logger'; + export { logRequest, logRequest2, logger }; + ", + "/tests/integration/redirect/dts/dist/path-false/esm/index.d.ts": "import { logRequest } from '@src/logger'; + import { logger } from 'rslog'; + import type { LoggerOptions } from './types'; + import { defaultOptions } from './types.js'; + export { logRequest, logger, type LoggerOptions, defaultOptions }; + export type { Foo } from '@src/types'; + export type { Bar } from 'types'; + export * from './types'; + export * from 'rslog'; + export * from '@src/logger'; + ", + "/tests/integration/redirect/dts/dist/path-false/esm/logger.d.ts": "import type { Request } from 'express'; + import type { LoggerOptions } from './types'; + export declare function logRequest(req: Request, options: LoggerOptions): void; + ", + "/tests/integration/redirect/dts/dist/path-false/esm/types.d.ts": "export interface LoggerOptions { + logLevel: 'info' | 'debug' | 'warn' | 'error'; + logBody: boolean; + } + export declare const defaultOptions: LoggerOptions; + export interface Foo { + foo: string; + } + export interface Bar { + bar: string; + } + ", + } + `); +}); + +test('redirect.dts.extension true', async () => { + expect(contents.esm2).toMatchInlineSnapshot(` + { + "/tests/integration/redirect/dts/dist/extension-true/esm/foo/foo.d.ts": "import { logRequest } from '../logger.js'; + import { logger } from '../../../../compile/rslog'; + import { logRequest as logRequest2 } from '../logger.js'; + export { logRequest, logRequest2, logger }; + ", + "/tests/integration/redirect/dts/dist/extension-true/esm/index.d.ts": "import { logRequest } from './logger.js'; + import { logger } from '../../../compile/rslog'; + import type { LoggerOptions } from './types.js'; + import { defaultOptions } from './types.js'; + export { logRequest, logger, type LoggerOptions, defaultOptions }; + export type { Foo } from './types.js'; + export type { Bar } from './types.js'; + export * from './types.js'; + export * from '../../../compile/rslog'; + export * from './logger.js'; + ", + "/tests/integration/redirect/dts/dist/extension-true/esm/logger.d.ts": "import type { Request } from 'express'; + import type { LoggerOptions } from './types.js'; + export declare function logRequest(req: Request, options: LoggerOptions): void; + ", + "/tests/integration/redirect/dts/dist/extension-true/esm/types.d.ts": "export interface LoggerOptions { + logLevel: 'info' | 'debug' | 'warn' | 'error'; + logBody: boolean; + } + export declare const defaultOptions: LoggerOptions; + export interface Foo { + foo: string; + } + export interface Bar { + bar: string; + } + ", + } + `); +}); + +test('redirect.dts.extension true with dts.autoExtension true', async () => { + expect(contents.esm3).toMatchInlineSnapshot(` + { + "/tests/integration/redirect/dts/dist/auto-extension-true/esm/foo/foo.d.mts": "import { logRequest } from '../logger.mjs'; + import { logger } from '../../../../compile/rslog'; + import { logRequest as logRequest2 } from '../logger.mjs'; + export { logRequest, logRequest2, logger }; + ", + "/tests/integration/redirect/dts/dist/auto-extension-true/esm/index.d.mts": "import { logRequest } from './logger.mjs'; + import { logger } from '../../../compile/rslog'; + import type { LoggerOptions } from './types.mjs'; + import { defaultOptions } from './types.mjs'; + export { logRequest, logger, type LoggerOptions, defaultOptions }; + export type { Foo } from './types.mjs'; + export type { Bar } from './types.mjs'; + export * from './types.mjs'; + export * from '../../../compile/rslog'; + export * from './logger.mjs'; + ", + "/tests/integration/redirect/dts/dist/auto-extension-true/esm/logger.d.mts": "import type { Request } from 'express'; + import type { LoggerOptions } from './types.mjs'; + export declare function logRequest(req: Request, options: LoggerOptions): void; + ", + "/tests/integration/redirect/dts/dist/auto-extension-true/esm/types.d.mts": "export interface LoggerOptions { + logLevel: 'info' | 'debug' | 'warn' | 'error'; + logBody: boolean; + } + export declare const defaultOptions: LoggerOptions; + export interface Foo { + foo: string; + } + export interface Bar { + bar: string; + } + ", + } + `); +}); diff --git a/tests/integration/redirect/dts/compile/rslog/index.d.ts b/tests/integration/redirect/dts/compile/rslog/index.d.ts new file mode 100644 index 000000000..4c82305cf --- /dev/null +++ b/tests/integration/redirect/dts/compile/rslog/index.d.ts @@ -0,0 +1,4 @@ +export declare function logger(): { + (...data: any[]): void; + (message?: any, ...optionalParams: any[]): void; +}; diff --git a/tests/integration/redirect/dts/compile/rslog/index.js b/tests/integration/redirect/dts/compile/rslog/index.js new file mode 100644 index 000000000..55cdb9deb --- /dev/null +++ b/tests/integration/redirect/dts/compile/rslog/index.js @@ -0,0 +1,3 @@ +export function logger() { + return console.log; +} diff --git a/tests/integration/redirect/dts/compile/rslog/package.json b/tests/integration/redirect/dts/compile/rslog/package.json new file mode 100644 index 000000000..13d2bffee --- /dev/null +++ b/tests/integration/redirect/dts/compile/rslog/package.json @@ -0,0 +1,4 @@ +{ + "name": "rslog", + "version": "1.2.3" +} diff --git a/tests/integration/redirect/dts/package.json b/tests/integration/redirect/dts/package.json new file mode 100644 index 000000000..027cc6d46 --- /dev/null +++ b/tests/integration/redirect/dts/package.json @@ -0,0 +1,14 @@ +{ + "name": "redirect-dts-test", + "version": "1.0.0", + "private": true, + "devDependencies": { + "@rslib/core": "workspace:*", + "@types/express": "^5.0.0", + "express": "^4.21.2", + "typescript": "^5.7.3" + }, + "peerDependencies": { + "express": "^4" + } +} diff --git a/tests/integration/redirect/dts/rslib.config.ts b/tests/integration/redirect/dts/rslib.config.ts new file mode 100644 index 000000000..443a8e7f1 --- /dev/null +++ b/tests/integration/redirect/dts/rslib.config.ts @@ -0,0 +1,60 @@ +import { defineConfig } from '@rslib/core'; +import { generateBundleEsmConfig } from 'test-helper'; + +export default defineConfig({ + lib: [ + // 0 - default + generateBundleEsmConfig({ + dts: true, + output: { + distPath: { + root: './dist/default/esm', + }, + }, + }), + // 1 - path: false + generateBundleEsmConfig({ + dts: true, + output: { + distPath: { + root: './dist/path-false/esm', + }, + }, + redirect: { + dts: { + path: false, + }, + }, + }), + // 2 - extension: true + generateBundleEsmConfig({ + dts: true, + output: { + distPath: { + root: './dist/extension-true/esm', + }, + }, + redirect: { + dts: { + extension: true, + }, + }, + }), + // 3 - extension: true with dts.autoExtension true + generateBundleEsmConfig({ + dts: { + autoExtension: true, + }, + output: { + distPath: { + root: './dist/auto-extension-true/esm', + }, + }, + redirect: { + dts: { + extension: true, + }, + }, + }), + ], +}); diff --git a/tests/integration/redirect/dts/src/foo/foo.ts b/tests/integration/redirect/dts/src/foo/foo.ts new file mode 100644 index 000000000..7ebaf0898 --- /dev/null +++ b/tests/integration/redirect/dts/src/foo/foo.ts @@ -0,0 +1,5 @@ +import { logRequest } from '@src/logger'; +import { logger } from 'rslog'; +import { logRequest as logRequest2 } from '../logger'; + +export { logRequest, logRequest2, logger }; diff --git a/tests/integration/redirect/dts/src/index.ts b/tests/integration/redirect/dts/src/index.ts new file mode 100644 index 000000000..a0a31097b --- /dev/null +++ b/tests/integration/redirect/dts/src/index.ts @@ -0,0 +1,12 @@ +import { logRequest } from '@src/logger'; +import { logger } from 'rslog'; +import type { LoggerOptions } from './types'; +import { defaultOptions } from './types.js'; + +export { logRequest, logger, type LoggerOptions, defaultOptions }; + +export type { Foo } from '@src/types'; +export type { Bar } from 'types'; +export * from './types'; +export * from 'rslog'; +export * from '@src/logger'; diff --git a/tests/integration/redirect/dts/src/logger.ts b/tests/integration/redirect/dts/src/logger.ts new file mode 100644 index 000000000..3157cc284 --- /dev/null +++ b/tests/integration/redirect/dts/src/logger.ts @@ -0,0 +1,25 @@ +import type { Request } from 'express'; +import type { LoggerOptions } from './types'; + +export function logRequest(req: Request, options: LoggerOptions): void { + const { method, url } = req; + const logMessage = `${method} ${url}`; + + switch (options.logLevel) { + case 'debug': + console.debug(logMessage); + break; + case 'warn': + console.warn(logMessage); + break; + case 'error': + console.error(logMessage); + break; + default: + console.log(logMessage); + } + + if (options.logBody && req.body) { + console.log('Request body:', req.body); + } +} diff --git a/tests/integration/redirect/dts/src/types.ts b/tests/integration/redirect/dts/src/types.ts new file mode 100644 index 000000000..cc9ccac52 --- /dev/null +++ b/tests/integration/redirect/dts/src/types.ts @@ -0,0 +1,17 @@ +export interface LoggerOptions { + logLevel: 'info' | 'debug' | 'warn' | 'error'; + logBody: boolean; +} + +export const defaultOptions: LoggerOptions = { + logLevel: 'info', + logBody: false, +}; + +export interface Foo { + foo: string; +} + +export interface Bar { + bar: string; +} diff --git a/tests/integration/redirect/dts/tsconfig.json b/tests/integration/redirect/dts/tsconfig.json new file mode 100644 index 000000000..c86ca44d8 --- /dev/null +++ b/tests/integration/redirect/dts/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "strict": true, + "skipLibCheck": true, + "paths": { + "*": ["./src/*"], + "@src/*": ["./src/*"], + "rslog": ["./compile/rslog"] + } + }, + "include": ["src/**/*"] +} diff --git a/website/docs/en/config/lib/redirect.mdx b/website/docs/en/config/lib/redirect.mdx index 9f33aee3f..627470c2d 100644 --- a/website/docs/en/config/lib/redirect.mdx +++ b/website/docs/en/config/lib/redirect.mdx @@ -6,9 +6,7 @@ overviewHeaders: [2, 3] :::info -`redirect` is the unique configuration for bundleless mode (set [lib.bundle](/config/lib/bundle) to `false`). It will not take effect in bundle mode where all output files are packaged into a single file, eliminating the need for import path redirection. - -As bundleless mode is still under development, additional redirect configurations will be introduced in the future. +`redirect` is the unique configuration for [bundleless mode](/guide/basic/output-structure#bundle--bundleless). It will not take effect in bundle mode where all output files are bundled into a single file, eliminating the need for import path redirection. ::: @@ -25,10 +23,16 @@ type StyleRedirect = { extension?: boolean; }; +type DtsRedirect = { + path?: boolean; + extension?: boolean; +}; + type Redirect = { js?: JsRedirect; style?: StyleRedirect; asset?: boolean; + dts?: DtsRedirect; }; ``` @@ -44,43 +48,26 @@ const defaultRedirect = { path: true, extension: true, }, + asset: true, + dts: { + path: true, + extension: false, + }, }; ``` -Configure the redirect for import paths in output files. In bundleless mode, there are often needs such as using aliases or automatically appending suffixes for ESM products. The `redirect` configuration is designed to address these issues. - -Common scenarios that require redirect: - -- Automatically convert `compilerOptions.paths` in tsconfig.json to correct relative path - - For example, set `compilerOptions.paths` to `{ "@/*": ["src/*"] }` in tsconfig.json, the output file will be redirected to the correct relative path: +Configure the redirect for import paths in output files. - ```ts - import { foo } from '@/foo'; // source code of './src/bar.ts' ↓ - import { foo } from './foo.js'; // expected output of './dist/bar.js' - - import { foo } from '@/foo'; // source code of './src/utils/index.ts' ↓ - import { foo } from '../foo.js'; // expected output './dist/utils/index.js' - ``` - -- Automatically append file suffix - - For ESM products that run in Node.js, you must specify the exact full path for the module import to load correctly. Rslib will automatically add the suffix based on the output file. - - ```ts - import { foo } from './foo'; // source code of './src/bar.ts' ↓ - import { foo } from './foo.mjs'; // expected output of './dist/bar.js' - - import { foo } from './foo.ts'; // source code of './src/utils/index.ts' ↓ - import { foo } from './foo.mjs'; // expected output './dist/utils/index.js' - ``` +In bundleless mode, there are often needs such as using aliases or automatically appending suffixes for ESM outputs. The `redirect` configuration is designed to address these issues. ## redirect.js Controls the redirect of the import paths of output JavaScript files. :::warning + When [output.externals](/config/rsbuild/output#outputexternals) is configured and a request is matched, neither `redirect.js.path` nor `redirect.js.extension` will take effect, and the final rewritten request path will be entirely controlled by [output.externals](/config/rsbuild/output#outputexternals). + ::: ### redirect.js.path @@ -90,9 +77,21 @@ Whether to automatically redirect the import paths of JavaScript output files. - **Type:** `boolean` - **Default:** `true` -When set to `true`, [resolve.alias](/config/rsbuild/resolve#resolvealias) and [resolve.aliasStrategy](/config/rsbuild/resolve#aliasstrategy) will take effect and applied in the rewritten import path of the output file. For TypeScript projects, you only need to configure [compilerOptions.paths](https://typescriptlang.org/tsconfig#paths) in the tsconfig.json file. +When set to `true`, [resolve.alias](/config/rsbuild/resolve#resolvealias) and [resolve.aliasStrategy](/config/rsbuild/resolve#aliasstrategy) will take effect and applied in the rewritten import path of the output file. For TypeScript projects, just configure [compilerOptions.paths](https://typescriptlang.org/tsconfig#paths) in the `tsconfig.json` file. -When set to `false`, the import path will not be effected by [resolve.alias](/config/rsbuild/resolve#resolvealias), [resolve.aliasStrategy](/config/rsbuild/resolve#aliasstrategy) and tsconfig.json. +When set to `false`, the import path will not be effected by [resolve.alias](/config/rsbuild/resolve#resolvealias), [resolve.aliasStrategy](/config/rsbuild/resolve#aliasstrategy) and `tsconfig.json`. + +- **Example:** + +When set `compilerOptions.paths` to `{ "@/*": ["src/*"] }` in `tsconfig.json`, the output file will be redirected to the correct relative path: + +```ts +import { foo } from '@/foo'; // source code of './src/bar.ts' ↓ +import { foo } from './foo.js'; // expected output of './dist/bar.js' + +import { foo } from '@/foo'; // source code of './src/utils/index.ts' ↓ +import { foo } from '../foo.js'; // expected output './dist/utils/index.js' +``` ### redirect.js.extension @@ -105,17 +104,37 @@ When set to `true`, the file extension will automatically be added to the rewrit When set to `false`, the file extension will remain unchanged from the original import path in the rewritten import path of the output file (regardless of whether it is specified or specified as any value). +:::note +The extension of the JavaScript output file is related to the [autoExtension](/config/lib/auto-extension#libautoextension) configuration. +::: + +- **Example:** + +For ESM outputs running in Node.js, the full extension to the module import path must be specified to load correctly. Rslib will automatically add corresponding file extensions based on the actual output JavaScript file. + +```ts +import { foo } from './foo'; // source code of './src/bar.ts' ↓ +import { foo } from './foo.mjs'; // expected output of './dist/bar.mjs' + +import { foo } from './foo.ts'; // source code of './src/utils/index.ts' ↓ +import { foo } from './foo.mjs'; // expected output './dist/utils/index.mjs' +``` + ## redirect.style Controls the redirect of the import paths of output style files. ### redirect.style.path -Whether to automatically redirect the import paths of style output files, the rules are the same as [redirect.js.path](/config/lib/redirect#redirectjspath). +Whether to automatically redirect the import paths of style output files. - **Type:** `boolean` - **Default:** `true` +When set to `true`, the relevant redirect rules are the same as [redirect.js.path](/config/lib/redirect#redirectjspath). + +When set to `false`, the original import path will remain unchanged. + - **Example:** When importing normal style files: @@ -145,7 +164,10 @@ Whether to automatically redirect the file extension to import paths based on th - **Type:** `boolean` - **Default:** `true` -When set to `true`, the file extension of importing a normal style file will be rewritten to `.css`. When importing [CSS Modules](/config/rsbuild/output#outputcssmodules), the path will be rewritten to the corresponding JavaScript output file. +When set to `true`: + +- When importing a normal style file, the path will be rewritten to `.css`. +- When importing [CSS Modules](/config/rsbuild/output#outputcssmodules), the path will be rewritten to the corresponding JavaScript output file. When set to `false`, the file extension will remain unchanged from the original import path. @@ -163,11 +185,72 @@ import styles from './index.module.mjs'; // expected output ## redirect.asset -When set to `true`, the paths of imported resource files will be rewritten to the corresponding JavaScript product file. +Controls the redirect of the import paths of output asset files. + +- **Type:** `boolean` +- **Default:** `true` + +When set to `true`, the paths of imported asset files will be redirected to the corresponding JavaScript output file. When set to `false`, the file extension will remain unchanged from the original import path. +- **Example:** + ```ts import url from './assets/logo.svg'; // source code ↓ import url from './assets/logo.mjs'; // expected output ``` + +## redirect.dts + +Controls the redirect of the import paths of output TypeScript declaration files. + +### redirect.dts.path + +Whether to automatically redirect the import paths of TypeScript declaration output files. + +- **Type:** `boolean` +- **Default:** `true` + +When set to `true`, Rslib will redirect the import path in the DTS output file to the corresponding relative path based on the [compilerOptions.paths](https://typescriptlang.org/tsconfig#paths) configured in `tsconfig.json`. + +When set to `false`, the original import path will remain unchanged. + +- **Example:** + +When `compilerOptions.paths` is set to `{ "@/*": ["src/*"] }` in `tsconfig.json`, the DTS output file will be redirected to the correct relative path: + +```ts +import { foo } from '@/foo'; // source code of './src/bar.ts' ↓ +import { foo } from './foo'; // expected output of './dist/bar.d.ts' + +import { foo } from '@/foo'; // source code of './src/utils/index.ts' ↓ +import { foo } from '../foo'; // expected output './dist/utils/index.d.ts' +``` + +### redirect.dts.extension + +Whether to automatically redirect the file extension to import paths based on the TypeScript declaration output files. + +- **Type:** `boolean` +- **Default:** `false` + +When set to `true`, the import paths in DTS files will be redirected to the corresponding JavaScript extension which can be resolved to corresponding DTS file. + +When set to `false`, the file extension will remain unchanged from the original import path in the rewritten import path of the output file (regardless of whether it is specified or specified as any value). + +:::note +The extension of the TypeScript declaration file is related to the [dts.autoExtension](/config/lib/dts#dtsautoextension) configuration. +::: + +- **Example:** + +For the `.d.mts` declaration file, in some scenarios, the full extension of the module import path is needed to load correctly. + +```ts +import { foo } from './foo'; // source code of './src/bar.ts' ↓ +import { foo } from './foo.mjs'; // expected output of './dist/bar.d.mts' + +import { foo } from './foo.ts'; // source code of './src/utils/index.ts' ↓ +import { foo } from './foo.mjs'; // expected output './dist/utils/index.d.mts' +``` diff --git a/website/docs/en/guide/migration/modernjs-module.mdx b/website/docs/en/guide/migration/modernjs-module.mdx index 1d324ec9f..b7d7b6a22 100644 --- a/website/docs/en/guide/migration/modernjs-module.mdx +++ b/website/docs/en/guide/migration/modernjs-module.mdx @@ -89,7 +89,7 @@ export default defineConfig({ ## TypeScript declaration -If you use Typescript in your `Modern.js Module` and need to generate declaration files, add the following changes: +If you use TypeScript in your `Modern.js Module` and need to generate declaration files, add the following changes: ```js title="rslib.config.ts" import { defineConfig } from '@rslib/core'; @@ -146,7 +146,7 @@ In addition, you have to install the `@rsbuild/plugin-sass` package as `devDepen -If you run Typescript together with Sass, you might run into DTS generation errors. This can be resolved by adding a `env.d.ts` file in your `/src` directory. +If you run TypeScript together with Sass, you might run into DTS generation errors. This can be resolved by adding a `env.d.ts` file in your `/src` directory. ```ts title="src/env.d.ts" declare module '*.scss' { diff --git a/website/docs/zh/config/lib/redirect.mdx b/website/docs/zh/config/lib/redirect.mdx index 3df8ae680..5caac8f4d 100644 --- a/website/docs/zh/config/lib/redirect.mdx +++ b/website/docs/zh/config/lib/redirect.mdx @@ -6,9 +6,7 @@ overviewHeaders: [2, 3] :::info -`redirect` 是 bundleless 模式(将 [lib.bundle](/config/lib/bundle) 设置为 `false`)的特定配置。在 bundle 模式下不会生效,因为所有产物文件都被打包成一个文件。所以,导入路径不存在因而不需要重定向。 - -由于 bundleless 模式仍在开发中,未来将引入更多的重定向配置。 +`redirect` 是 [bundleless 模式](/guide/basic/output-structure#bundle--bundleless) 的特定配置。该配置在 bundle 模式下不会生效,因为所有产物文件都被打包成一个文件,不需要进行文件导入路径的重定向。 ::: @@ -25,9 +23,16 @@ type StyleRedirect = { extension?: boolean; }; +type DtsRedirect = { + path?: boolean; + extension?: boolean; +}; + type Redirect = { js?: JsRedirect; style?: StyleRedirect; + asset?: boolean; + dts?: DtsRedirect; }; ``` @@ -44,43 +49,25 @@ const defaultRedirect = { extension: true, }, asset: true, + dts: { + path: true, + extension: false, + }, }; ``` -配置产物文件中导入路径的重定向。在 bundleless 模式下,通常需要使用别名或自动添加 ESM 产物的后缀。`redirect` 配置旨在解决这些问题。 - -常见的需要 redirect 的场景: - -- 自动将 tsconfig.json 中 `compilerOptions.paths` 转换为正确的相对路径 +配置产物文件中导入路径的重定向。 - 例如,在 tsconfig.json 中将 `compilerOptions.paths` 设置为 `{ "@/*": ["src/*"] }`,产物文件将被重定向到正确的相对路径: - - ```ts - import { foo } from '@/foo'; // './src/bar.ts' 的源码 ↓ - import { foo } from './foo.js'; // './dist/bar.js' 预期生成的代码 - - import { foo } from '@/foo'; // './src/utils/index.ts' 的源码 ↓ - import { foo } from '../foo.js'; // './dist/utils/index.js' 预期生成的代码 - ``` - -- 自动添加文件后缀 - - 对于在 Node.js 中运行的 ESM 产物,必须指定模块导入的完整路径才能正确加载。Rslib 将根产物文件自动添加后缀。 - - ```ts - import { foo } from './foo'; // './src/bar.ts' 的源码 ↓ - import { foo } from './foo.mjs'; // './dist/bar.js' 预期生成的代码 - - import { foo } from './foo.ts'; // './src/utils/index.ts' 的源码 ↓ - import { foo } from './foo.mjs'; // './dist/utils/index.js' 预期生成的代码 - ``` +在 bundleless 模式下,通常需要使用别名或自动添加 ESM 产物的后缀。`redirect` 配置旨在解决这些问题。 ## redirect.js 控制 JavaScript 产物文件导入路径的重定向。 :::warning + 当 [output.externals](/config/rsbuild/output#outputexternals) 被配置且请求被匹配时,`redirect.js.path` 和 `redirect.js.extension` 都不会生效,最终重写的请求路径将完全由 [output.externals](/config/rsbuild/output#outputexternals) 控制。 + ::: ### redirect.js.path @@ -90,9 +77,21 @@ const defaultRedirect = { - **类型:** `boolean` - **默认值:** `true` -当设置为 `true` 时,[resolve.alias](/config/rsbuild/resolve#resolvealias) 和 [resolve.aliasStrategy](/config/rsbuild/resolve#aliasstrategy) 将生效并应用于产物文件的重写导入路径。对于 TypeScript 项目,您只需在 tsconfig.json 文件中配置 [compilerOptions.paths](https://typescriptlang.org/tsconfig#paths)。 +当设置为 `true` 时,[resolve.alias](/config/rsbuild/resolve#resolvealias) 和 [resolve.aliasStrategy](/config/rsbuild/resolve#aliasstrategy) 将生效并应用于产物文件的重写导入路径。对于 TypeScript 项目,在 `tsconfig.json` 文件中配置 [compilerOptions.paths](https://typescriptlang.org/tsconfig#paths) 即可。 + +当设置为 `false` 时,导入路径将不受 [resolve.alias](/config/rsbuild/resolve#resolvealias)、[resolve.aliasStrategy](/config/rsbuild/resolve#aliasstrategy) 和 `tsconfig.json` 的影响。 -当设置为 `false` 时,导入路径将不受 [resolve.alias](/config/rsbuild/resolve#resolvealias)、[resolve.aliasStrategy](/config/rsbuild/resolve#aliasstrategy) 和 tsconfig.json 的影响。 +- 示例: + +在 `tsconfig.json` 中将 `compilerOptions.paths` 设置为 `{ "@/*": ["src/*"] }` 时,产物文件将被重定向到正确的相对路径: + +```ts +import { foo } from '@/foo'; // './src/bar.ts' 的源码 ↓ +import { foo } from './foo.js'; // './dist/bar.js' 预期生成的代码 + +import { foo } from '@/foo'; // './src/utils/index.ts' 的源码 ↓ +import { foo } from '../foo.js'; // './dist/utils/index.js' 预期生成的代码 +``` ### redirect.js.extension @@ -103,7 +102,23 @@ const defaultRedirect = { 当设置为 `true` 时,无论原始扩展名或导入路径中是否指定,文件扩展名都将自动添加到产物文件的重写导入路径中。 -当设置为 `false` 时,文件扩展名将保持原始导入路径(无论是否指定或指定为任意值)。 +当设置为 `false` 时,文件扩展名将保持原始导入路径不变(无论是否指定或指定为任意值)。 + +:::note +JavaScript 产物文件的扩展名与 [autoExtension](/config/lib/auto-extension#libautoextension) 配置有关。 +::: + +- **示例:** + +对于在 Node.js 中运行的 ESM 产物,必须要指定模块导入路径的完整扩展名才能正确加载。Rslib 将根据实际的 JavaScript 产物文件自动添加对应的文件扩展名。 + +```ts +import { foo } from './foo'; // './src/bar.ts' 的源码 ↓ +import { foo } from './foo.mjs'; // './dist/bar.mjs' 预期生成的代码 + +import { foo } from './foo.ts'; // './src/utils/index.ts' 的源码 ↓ +import { foo } from './foo.mjs'; // './dist/utils/index.mjs' 预期生成的代码 +``` ## redirect.style @@ -111,11 +126,15 @@ const defaultRedirect = { ### redirect.style.path -是否自动重定向样式产物文件的导入路径,规则与 [redirect.js.path](/config/lib/redirect#redirectjspath) 相同。 +是否自动重定向样式产物文件的导入路径。 - **类型:** `boolean` - **默认值:** `true` +当设置为 `true` 时,相关重定向规则与 [redirect.js.path](/config/lib/redirect#redirectjspath) 相同。 + +当设置为 `false` 时,将保持原始导入路径不变。 + - **示例:** 导入普通样式文件时: @@ -145,7 +164,10 @@ import styles from '../foo.css'; // './dist/utils/index.js' 预期生成的代 - **类型:** `boolean` - **默认值:** `true` -当设置为 `true` 时,导入普通样式文件的文件扩展名将被重写为 `.css`,导入 [CSS Modules](/config/rsbuild/output#outputcssmodules) 时,路径将被重写为到对应的 JavaScript 产物文件。 +当设置为 `true` 时: + +- 导入普通样式文件的文件扩展名将被重写为 `.css`。 +- 导入 [CSS Modules](/config/rsbuild/output#outputcssmodules) 时,路径将被重写为到对应的 JavaScript 产物文件。 当设置为 `false` 时,文件扩展名将保持原始导入路径。 @@ -163,11 +185,72 @@ import styles from './index.module.mjs'; // 预期生成的代码 ## redirect.asset +控制资源文件导入路径的重定向。 + +- **类型:** `boolean` +- **默认值:** `true` + 当设置为 `true` 时,导入资源文件的路径将被重写到对应的 JavaScript 产物文件。 当设置为 `false` 时,文件扩展名将保持原始导入路径。 +- **示例:** + ```ts import url from './assets/logo.svg'; // 源码 ↓ import url from './assets/logo.mjs'; // 预期生成的代码 ``` + +## redirect.dts + +控制 TypeScript 类型文件中导入路径的重定向。 + +### redirect.dts.path + +是否自动重定向 TypeScript 类型文件中的导入路径。 + +- **类型:** `boolean` +- **默认值:** `true` + +当设置为 `true` 时,Rslib 会根据 `tsconfig.json` 文件中配置的 [compilerOptions.paths](https://typescriptlang.org/tsconfig#paths),将 DTS 产物文件中的导入路径重定向为对应的相对路径。 + +当设置为 `false` 时,将保持原始导入路径不变。 + +- **示例:** + +在 `tsconfig.json` 中将 `compilerOptions.paths` 设置为 `{ "@/*": ["src/*"] }` 时,DTS 产物文件将被重定向到正确的相对路径: + +```ts +import { foo } from '@/foo'; // './src/bar.ts' 的源码 ↓ +import { foo } from './foo'; // './dist/bar.d.ts' 预期生成的代码 + +import { foo } from '@/foo'; // './src/utils/index.ts' 的源码 ↓ +import { foo } from '../foo'; // './dist/utils/index.d.ts' 预期生成的代码 +``` + +### redirect.dts.extension + +是否根据 TypeScript 类型文件自动重定向文件扩展名到导入路径。 + +- **类型:** `boolean` +- **默认值:** `false` + +当设置为 `true` 时,DTS 文件中的引入路径会被重定向到对应的可以解析到相应 DTS 文件的 JavaScript 文件扩展名。 + +当设置为 `false` 时,文件扩展名将保持原始导入路径不变(无论是否指定或指定为任意值)。 + +:::note +TypeScript 类型文件的扩展名与 [dts.autoExtension](/config/lib/dts#dtsautoextension) 配置有关。 +::: + +- **示例:** + +对于 `.d.mts` 类型文件,在一些场景下需要指定模块导入路径的完整扩展名才能正确加载。 + +```ts +import { foo } from './foo'; // './src/bar.ts' 的源码 ↓ +import { foo } from './foo.mjs'; // './dist/bar.d.mts' 预期生成的代码 + +import { foo } from './foo.ts'; // './src/utils/index.ts' 的源码 ↓ +import { foo } from './foo.mjs'; // './dist/utils/index.d.mts' 预期生成的代码 +``` diff --git a/website/docs/zh/guide/basic/typescript.mdx b/website/docs/zh/guide/basic/typescript.mdx index 545c09307..3a7193de7 100644 --- a/website/docs/zh/guide/basic/typescript.mdx +++ b/website/docs/zh/guide/basic/typescript.mdx @@ -1,4 +1,4 @@ -# 使用 Typescript +# 使用 TypeScript Rslib 默认支持 TypeScript,你可以直接在项目中使用 `.ts` 和 `.tsx` 文件。 diff --git a/website/docs/zh/guide/migration/modernjs-module.mdx b/website/docs/zh/guide/migration/modernjs-module.mdx index 9debcc4ad..291559b3a 100644 --- a/website/docs/zh/guide/migration/modernjs-module.mdx +++ b/website/docs/zh/guide/migration/modernjs-module.mdx @@ -89,7 +89,7 @@ export default defineConfig({ ## TypeScript 类型定义 -如果你在 `Modern.js Module` 中使用 Typescript 并需要生成类型定义文件,请添加以下更改: +如果你在 `Modern.js Module` 中使用 TypeScript 并需要生成类型定义文件,请添加以下更改: ```js title="rslib.config.ts" import { defineConfig } from '@rslib/core'; @@ -146,7 +146,7 @@ export default defineConfig({ -如果你在运行 Typescript 和 Sass,你可能会遇到 DTS 生成错误。这可以通过在 `/src` 目录中添加一个 `env.d.ts` 文件来解决。 +如果你在运行 TypeScript 和 Sass,你可能会遇到 DTS 生成错误。这可以通过在 `/src` 目录中添加一个 `env.d.ts` 文件来解决。 ```ts title="src/env.d.ts" declare module '*.scss' {