diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index ab22d11e5..dbe32a6ee 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -1391,6 +1391,7 @@ const composeBundlelessExternalConfig = ( cssModulesAuto: CssLoaderOptionsAuto, bundle: boolean, outBase: string | null, + pkgJson?: PkgJson, ): { config: EnvironmentConfig; resolvedJsRedirect?: DeepRequired; @@ -1405,6 +1406,60 @@ const composeBundlelessExternalConfig = ( const assetRedirectExtension = redirect.asset?.extension ?? true; let resolver: RspackResolver | undefined; + const devDependencyNames = new Set( + Object.keys(pkgJson?.devDependencies ?? {}), + ); + const warnedDevDependencies = new Set(); + + const devDependencyList = Array.from(devDependencyNames); + + const matchDevDependency = (request: string): string | undefined => { + if (!request || devDependencyList.length === 0) { + return undefined; + } + + for (const name of devDependencyList) { + if (request === name || request.startsWith(`${name}/`)) { + return name; + } + } + + return undefined; + }; + + const warnDevDependency = (request: string, issuer?: string) => { + const matched = matchDevDependency(request); + if (!matched || warnedDevDependencies.has(matched)) { + return; + } + + // Only warn when we have an issuer and outBase is a string and the issuer + // is located inside the outBase (root) directory. Otherwise skip warning. + if (!issuer || typeof outBase !== 'string') { + return; + } + + const normalizedIssuer = normalizeSlash(issuer); + const normalizedOutBase = normalizeSlash(outBase); + const isIssuerInsideOutBase = + normalizedIssuer === normalizedOutBase || + normalizedIssuer.startsWith(`${normalizedOutBase}/`); + + if (!isIssuerInsideOutBase) { + return; + } + + warnedDevDependencies.add(matched); + + const relativeSource = normalizeSlash(path.relative(outBase, issuer)); + const sourceInfo = relativeSource + ? ` from ${color.green(relativeSource)}` + : ''; + + logger.warn( + `The externalized request ${color.green(`"${request}"`)}${sourceInfo} is declared in ${color.blue('"devDependencies"')} in package.json. Bundleless mode does not include devDependencies in the output, consider moving it to ${color.blue('"dependencies"')} or ${color.blue('"peerDependencies"')}.`, + ); + }; return { resolvedJsRedirect: { @@ -1478,6 +1533,7 @@ const composeBundlelessExternalConfig = ( // Prevent from externalizing entry modules here. if (issuer) { let resolvedRequest: string = request; + warnDevDependency(request, issuer); const redirectedPath = await redirectPath(resolvedRequest); const cssExternal = await cssExternalHandler( @@ -1778,6 +1834,7 @@ async function composeLibRsbuildConfig( cssModulesAuto, bundle, outBase, + pkgJson, ); const { config: targetConfig, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f842c2e6..f27f2b7a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -894,6 +894,15 @@ importers: tests/integration/externals/browser: {} + tests/integration/externals/dev-dependency-warning: + devDependencies: + left-pad: + specifier: 1.0.0 + version: 1.0.0 + normalize.css: + specifier: 8.0.1 + version: 8.0.1 + tests/integration/externals/module-import-warn: {} tests/integration/externals/node: {} @@ -5286,6 +5295,10 @@ packages: leac@0.6.0: resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} + left-pad@1.0.0: + resolution: {integrity: sha512-Xvly4CWfzT+7TGZRB2Qd7ub2dmJDomZCNf3BuT3XXfrNerQHtta82NNhbvToU4Qiz7IHrg21YB3UpL/6npCtmg==} + deprecated: use String.prototype.padStart() + lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} @@ -5808,6 +5821,9 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + normalize.css@8.0.1: + resolution: {integrity: sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==} + npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -12114,6 +12130,8 @@ snapshots: leac@0.6.0: {} + left-pad@1.0.0: {} + lightningcss-android-arm64@1.30.2: optional: true @@ -12845,6 +12863,8 @@ snapshots: normalize-path@3.0.0: {} + normalize.css@8.0.1: {} + npm-run-path@4.0.1: dependencies: path-key: 3.1.1 diff --git a/tests/integration/externals/dev-dependency-warning/package.json b/tests/integration/externals/dev-dependency-warning/package.json new file mode 100644 index 000000000..a52f815d5 --- /dev/null +++ b/tests/integration/externals/dev-dependency-warning/package.json @@ -0,0 +1,10 @@ +{ + "name": "bundleless-dev-dependency-warning", + "version": "0.0.0", + "private": true, + "type": "module", + "devDependencies": { + "left-pad": "1.0.0", + "normalize.css": "8.0.1" + } +} diff --git a/tests/integration/externals/dev-dependency-warning/rslib.config.ts b/tests/integration/externals/dev-dependency-warning/rslib.config.ts new file mode 100644 index 000000000..32199cd09 --- /dev/null +++ b/tests/integration/externals/dev-dependency-warning/rslib.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from '@rslib/core'; +import { generateBundleEsmConfig } from 'test-helper'; + +export default defineConfig({ + lib: [ + generateBundleEsmConfig({ + bundle: false, + source: { + entry: { + index: 'src/index.ts', + }, + }, + output: { + distPath: 'dist', + }, + }), + ], +}); diff --git a/tests/integration/externals/dev-dependency-warning/src/index.ts b/tests/integration/externals/dev-dependency-warning/src/index.ts new file mode 100644 index 000000000..713b791e4 --- /dev/null +++ b/tests/integration/externals/dev-dependency-warning/src/index.ts @@ -0,0 +1,6 @@ +import leftPad from 'left-pad'; +import leftPadLib from 'left-pad/lib'; +import 'normalize.css'; + +export const primary = leftPad('foo', 5, ' '); +export const secondary = leftPadLib; diff --git a/tests/integration/externals/index.test.ts b/tests/integration/externals/index.test.ts index 7d18cf2ea..3050caba1 100644 --- a/tests/integration/externals/index.test.ts +++ b/tests/integration/externals/index.test.ts @@ -112,3 +112,24 @@ test('user externals', async () => { " `); }); + +test('warn when bundleless external depends on devDependencies', async () => { + const fixturePath = join(__dirname, 'dev-dependency-warning'); + const { logs, restore } = proxyConsole(); + + await buildAndGetResults({ fixturePath }); + + restore(); + const warnLogs = logs.map((log) => stripAnsi(String(log))); + console.log(warnLogs); + const jsMatchingLog = warnLogs.filter( + (log) => + log.includes('The externalized request "left-pad/lib" from index.ts is declared in "devDependencies" in package.json. Bundleless mode does not include devDependencies in the output, consider moving it to "dependencies" or "peerDependencies".') + ); + expect(jsMatchingLog.length).toBe(1); + const cssMatchingLog = warnLogs.filter( + (log) => + log.includes('The externalized request "normalize.css" from index.ts is declared in "devDependencies" in package.json. Bundleless mode does not include devDependencies in the output, consider moving it to "dependencies" or "peerDependencies".') + ); + expect(cssMatchingLog.length).toBe(1); +});