diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index b760f30b6a6b0c..06b222736667c8 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -7,6 +7,7 @@ import colors from 'picocolors' import type { Alias, AliasOptions } from 'types/alias' import aliasPlugin from '@rollup/plugin-alias' import { build } from 'esbuild' +import type { Plugin as ESBuildPlugin } from 'esbuild' import type { RollupOptions } from 'rollup' import type { Plugin } from './plugin' import type { @@ -584,6 +585,7 @@ export async function resolveConfig( const middlewareMode = config?.server?.middlewareMode + config = mergeConfig(config, externalConfigCompat(config, configEnv)) const optimizeDeps = config.optimizeDeps || {} const BASE_URL = resolvedBase @@ -1029,3 +1031,83 @@ export function isDepsOptimizerEnabled(config: ResolvedConfig): boolean { (command === 'serve' && optimizeDeps.disabled === 'dev') ) } + +// esbuild doesn't transpile `require('foo')` into `import` statements if 'foo' is externalized +// https://github.com/evanw/esbuild/issues/566#issuecomment-735551834 +function esbuildCjsExternalPlugin(externals: string[]): ESBuildPlugin { + return { + name: 'cjs-external', + setup(build) { + const escape = (text: string) => + `^${text.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}$` + const filter = new RegExp(externals.map(escape).join('|')) + + build.onResolve({ filter: /.*/, namespace: 'external' }, (args) => ({ + path: args.path, + external: true + })) + + build.onResolve({ filter }, (args) => ({ + path: args.path, + namespace: 'external' + })) + + build.onLoad({ filter: /.*/, namespace: 'external' }, (args) => ({ + contents: `export * from ${JSON.stringify(args.path)}` + })) + } + } +} + +// Support `rollupOptions.external` when `legacy.buildRollupPluginCommonjs` is disabled +function externalConfigCompat(config: UserConfig, { command }: ConfigEnv) { + // Only affects the build command + if (command !== 'build') { + return {} + } + + // Skip if using Rollup CommonJS plugin + if ( + config.legacy?.buildRollupPluginCommonjs || + config.optimizeDeps?.disabled === 'build' + ) { + return {} + } + + // Skip if no `external` configured + const external = config?.build?.rollupOptions?.external + if (!external) { + return {} + } + + let normalizedExternal = external + if (typeof external === 'string') { + normalizedExternal = [external] + } + + // TODO: decide whether to support RegExp and function options + // They're not supported yet because `optimizeDeps.exclude` currently only accepts strings + if ( + !Array.isArray(normalizedExternal) || + normalizedExternal.some((ext) => typeof ext !== 'string') + ) { + throw new Error( + `[vite] 'build.rollupOptions.external' can only be an array of strings or a string.\n` + + `You can turn on 'legacy.buildRollupPluginCommonjs' to support more advanced options.` + ) + } + + const additionalConfig: UserConfig = { + optimizeDeps: { + exclude: normalizedExternal as string[], + esbuildOptions: { + plugins: [ + // TODO: maybe it can be added globally/unconditionally? + esbuildCjsExternalPlugin(normalizedExternal as string[]) + ] + } + } + } + + return additionalConfig +} diff --git a/playground/external/__tests__/external.spec.ts b/playground/external/__tests__/external.spec.ts new file mode 100644 index 00000000000000..ed1d1872181c02 --- /dev/null +++ b/playground/external/__tests__/external.spec.ts @@ -0,0 +1,13 @@ +import { isBuild, page } from '~utils' + +describe.runIf(isBuild)('build', () => { + test('should externalize imported packages', async () => { + // If `vue` is successfully externalized, the page should use the version from the import map + expect(await page.textContent('#imported-vue-version')).toBe('3.2.0') + }) + + test('should externalize required packages', async () => { + // If `vue` is successfully externalized, the page should use the version from the import map + expect(await page.textContent('#required-vue-version')).toBe('3.2.0') + }) +}) diff --git a/playground/external/dep-that-imports-vue/index.js b/playground/external/dep-that-imports-vue/index.js new file mode 100644 index 00000000000000..aa79957e9b3b49 --- /dev/null +++ b/playground/external/dep-that-imports-vue/index.js @@ -0,0 +1,3 @@ +import { version } from 'vue' + +document.querySelector('#imported-vue-version').textContent = version diff --git a/playground/external/dep-that-imports-vue/package.json b/playground/external/dep-that-imports-vue/package.json new file mode 100644 index 00000000000000..2c088c73cbb035 --- /dev/null +++ b/playground/external/dep-that-imports-vue/package.json @@ -0,0 +1,7 @@ +{ + "name": "@vitejs/dep-that-imports-vue", + "version": "0.0.1", + "dependencies": { + "vue": "^3.2.37" + } +} diff --git a/playground/external/dep-that-requires-vue/index.js b/playground/external/dep-that-requires-vue/index.js new file mode 100644 index 00000000000000..0b573021a00b57 --- /dev/null +++ b/playground/external/dep-that-requires-vue/index.js @@ -0,0 +1,3 @@ +const { version } = require('vue') + +document.querySelector('#required-vue-version').textContent = version diff --git a/playground/external/dep-that-requires-vue/package.json b/playground/external/dep-that-requires-vue/package.json new file mode 100644 index 00000000000000..a12c43b40fb282 --- /dev/null +++ b/playground/external/dep-that-requires-vue/package.json @@ -0,0 +1,7 @@ +{ + "name": "@vitejs/dep-that-requires-vue", + "version": "0.0.1", + "dependencies": { + "vue": "^3.2.37" + } +} diff --git a/playground/external/index.html b/playground/external/index.html new file mode 100644 index 00000000000000..66a16c329c1fc5 --- /dev/null +++ b/playground/external/index.html @@ -0,0 +1,20 @@ + + + + + + Vite App + + + +

Imported Vue version:

+

Required Vue version:

+ + + diff --git a/playground/external/package.json b/playground/external/package.json new file mode 100644 index 00000000000000..94fdc429ce2b0b --- /dev/null +++ b/playground/external/package.json @@ -0,0 +1,19 @@ +{ + "name": "external-test", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "debug": "node --inspect-brk ../../packages/vite/bin/vite", + "preview": "vite preview" + }, + "dependencies": { + "@vitejs/dep-that-imports-vue": "file:./dep-that-imports-vue", + "@vitejs/dep-that-requires-vue": "file:./dep-that-requires-vue" + }, + "devDependencies": { + "vite": "workspace:*", + "vue": "^3.2.37" + } +} diff --git a/playground/external/src/main.js b/playground/external/src/main.js new file mode 100644 index 00000000000000..0e1d3bece640e9 --- /dev/null +++ b/playground/external/src/main.js @@ -0,0 +1,2 @@ +import '@vitejs/dep-that-imports-vue' +import '@vitejs/dep-that-requires-vue' diff --git a/playground/external/vite.config.js b/playground/external/vite.config.js new file mode 100644 index 00000000000000..f6126b069cf49d --- /dev/null +++ b/playground/external/vite.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' + +export default defineConfig({ + build: { + minify: false, + rollupOptions: { + external: ['vue'] + } + } +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f3a9c1313adfb..9817093816f686 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -457,6 +457,31 @@ importers: dependencies: vue: 3.2.37 + playground/external: + specifiers: + '@vitejs/dep-that-imports-vue': file:./dep-that-imports-vue + '@vitejs/dep-that-requires-vue': file:./dep-that-requires-vue + vite: workspace:* + vue: ^3.2.37 + dependencies: + '@vitejs/dep-that-imports-vue': file:playground/external/dep-that-imports-vue + '@vitejs/dep-that-requires-vue': file:playground/external/dep-that-requires-vue + devDependencies: + vite: link:../../packages/vite + vue: 3.2.37 + + playground/external/dep-that-imports-vue: + specifiers: + vue: ^3.2.37 + dependencies: + vue: 3.2.37 + + playground/external/dep-that-requires-vue: + specifiers: + vue: ^3.2.37 + dependencies: + vue: 3.2.37 + playground/file-delete-restore: specifiers: '@vitejs/plugin-react': workspace:* @@ -8694,6 +8719,22 @@ packages: version: 1.0.0 dev: false + file:playground/external/dep-that-imports-vue: + resolution: {directory: playground/external/dep-that-imports-vue, type: directory} + name: '@vitejs/dep-that-imports-vue' + version: 0.0.1 + dependencies: + vue: 3.2.37 + dev: false + + file:playground/external/dep-that-requires-vue: + resolution: {directory: playground/external/dep-that-requires-vue, type: directory} + name: '@vitejs/dep-that-requires-vue' + version: 0.0.1 + dependencies: + vue: 3.2.37 + dev: false + file:playground/json/json-module: resolution: {directory: playground/json/json-module, type: directory} name: json-module