Skip to content

Commit f8c92d1

Browse files
authored
feat: default esm SSR build, simplified externalization (#8348)
1 parent 5161ecd commit f8c92d1

34 files changed

+344
-156
lines changed

docs/config/ssr-options.md

+8
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,11 @@ Prevent listed dependencies from being externalized for SSR. If `true`, no depen
2424
- **Default:** `node`
2525

2626
Build target for the SSR server.
27+
28+
## ssr.format
29+
30+
- **Type:** `'esm' | 'cjs'`
31+
- **Default:** `esm`
32+
- **Experimental**
33+
34+
Build format for the SSR server. Since Vite v3 the SSR build generates ESM by default. `'cjs'` can be selected to generate a CJS build, but it isn't recommended. The option is left marked as experimental to give users more time to update to ESM. CJS builds requires complex externalization heuristics that aren't present in the ESM format.

docs/vite.config.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { defineConfig } from 'vite'
2+
3+
export default defineConfig({
4+
ssr: {
5+
format: 'cjs'
6+
}
7+
})

packages/vite/src/node/build.ts

+50-34
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ import { manifestPlugin } from './plugins/manifest'
3131
import type { Logger } from './logger'
3232
import { dataURIPlugin } from './plugins/dataUri'
3333
import { buildImportAnalysisPlugin } from './plugins/importAnalysisBuild'
34-
import { resolveSSRExternal, shouldExternalizeForSSR } from './ssr/ssrExternal'
34+
import {
35+
cjsShouldExternalizeForSSR,
36+
cjsSsrResolveExternals
37+
} from './ssr/ssrExternal'
3538
import { ssrManifestPlugin } from './ssr/ssrManifestPlugin'
3639
import type { DepOptimizationMetadata } from './optimizer'
3740
import {
@@ -342,7 +345,6 @@ async function doBuild(
342345
const config = await resolveConfig(inlineConfig, 'build', 'production')
343346
const options = config.build
344347
const ssr = !!options.ssr
345-
const esm = config.ssr?.format === 'es' || !ssr
346348
const libOptions = options.lib
347349

348350
config.logger.info(
@@ -374,27 +376,14 @@ async function doBuild(
374376
ssr ? config.plugins.map((p) => injectSsrFlagToHooks(p)) : config.plugins
375377
) as Plugin[]
376378

377-
// inject ssrExternal if present
378379
const userExternal = options.rollupOptions?.external
379380
let external = userExternal
380-
if (ssr) {
381-
// see if we have cached deps data available
382-
let knownImports: string[] | undefined
383-
const dataPath = path.join(getDepsCacheDir(config), '_metadata.json')
384-
try {
385-
const data = JSON.parse(
386-
fs.readFileSync(dataPath, 'utf-8')
387-
) as DepOptimizationMetadata
388-
knownImports = Object.keys(data.optimized)
389-
} catch (e) {}
390-
if (!knownImports) {
391-
// no dev deps optimization data, do a fresh scan
392-
knownImports = await findKnownImports(config)
393-
}
394-
external = resolveExternal(
395-
resolveSSRExternal(config, knownImports),
396-
userExternal
397-
)
381+
382+
// In CJS, we can pass the externals to rollup as is. In ESM, we need to
383+
// do it in the resolve plugin so we can add the resolved extension for
384+
// deep node_modules imports
385+
if (ssr && config.ssr?.format === 'cjs') {
386+
external = await cjsSsrResolveExternal(config, userExternal)
398387
}
399388

400389
if (isDepsOptimizerEnabled(config) && !ssr) {
@@ -432,10 +421,12 @@ async function doBuild(
432421

433422
try {
434423
const buildOutputOptions = (output: OutputOptions = {}): OutputOptions => {
424+
const cjsSsrBuild = ssr && config.ssr?.format === 'cjs'
435425
return {
436426
dir: outDir,
437-
format: esm ? 'es' : 'cjs',
438-
exports: esm ? 'auto' : 'named',
427+
// Default format is 'es' for regular and for SSR builds
428+
format: cjsSsrBuild ? 'cjs' : 'es',
429+
exports: cjsSsrBuild ? 'named' : 'auto',
439430
sourcemap: options.sourcemap,
440431
name: libOptions ? libOptions.name : undefined,
441432
generatedCode: 'es2015',
@@ -697,26 +688,51 @@ export function onRollupWarning(
697688
}
698689
}
699690

700-
function resolveExternal(
701-
ssrExternals: string[],
691+
async function cjsSsrResolveExternal(
692+
config: ResolvedConfig,
702693
user: ExternalOption | undefined
703-
): ExternalOption {
694+
): Promise<ExternalOption> {
695+
// see if we have cached deps data available
696+
let knownImports: string[] | undefined
697+
const dataPath = path.join(getDepsCacheDir(config), '_metadata.json')
698+
try {
699+
const data = JSON.parse(
700+
fs.readFileSync(dataPath, 'utf-8')
701+
) as DepOptimizationMetadata
702+
knownImports = Object.keys(data.optimized)
703+
} catch (e) {}
704+
if (!knownImports) {
705+
// no dev deps optimization data, do a fresh scan
706+
knownImports = await findKnownImports(config)
707+
}
708+
const ssrExternals = cjsSsrResolveExternals(config, knownImports)
709+
704710
return (id, parentId, isResolved) => {
705-
if (shouldExternalizeForSSR(id, ssrExternals)) {
711+
const isExternal = cjsShouldExternalizeForSSR(id, ssrExternals)
712+
if (isExternal) {
706713
return true
707714
}
708715
if (user) {
709-
if (typeof user === 'function') {
710-
return user(id, parentId, isResolved)
711-
} else if (Array.isArray(user)) {
712-
return user.some((test) => isExternal(id, test))
713-
} else {
714-
return isExternal(id, user)
715-
}
716+
return resolveUserExternal(user, id, parentId, isResolved)
716717
}
717718
}
718719
}
719720

721+
function resolveUserExternal(
722+
user: ExternalOption,
723+
id: string,
724+
parentId: string | undefined,
725+
isResolved: boolean
726+
) {
727+
if (typeof user === 'function') {
728+
return user(id, parentId, isResolved)
729+
} else if (Array.isArray(user)) {
730+
return user.some((test) => isExternal(id, test))
731+
} else {
732+
return isExternal(id, user)
733+
}
734+
}
735+
720736
function isExternal(id: string, test: string | RegExp) {
721737
if (typeof test === 'string') {
722738
return id === test

packages/vite/src/node/config.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,8 @@ export interface ExperimentalOptions {
230230

231231
export type SSRTarget = 'node' | 'webworker'
232232

233+
export type SSRFormat = 'esm' | 'cjs'
234+
233235
export interface SSROptions {
234236
external?: string[]
235237
noExternal?: string | RegExp | (string | RegExp)[] | true
@@ -239,12 +241,14 @@ export interface SSROptions {
239241
* Default: 'node'
240242
*/
241243
target?: SSRTarget
242-
243244
/**
244-
* Define the module format for the ssr build.
245-
* Default: 'cjs'
245+
* Define the format for the ssr build. Since Vite v3 the SSR build generates ESM by default.
246+
* `'cjs'` can be selected to generate a CJS build, but it isn't recommended. This option is
247+
* left marked as experimental to give users more time to update to ESM. CJS builds requires
248+
* complex externalization heuristics that aren't present in the ESM format.
249+
* @experimental
246250
*/
247-
format?: 'es' | 'cjs'
251+
format?: SSRFormat
248252
}
249253

250254
export interface ResolveWorkerOptions {

packages/vite/src/node/plugins/importAnalysis.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ import {
4141
} from '../utils'
4242
import type { ResolvedConfig } from '../config'
4343
import type { Plugin } from '../plugin'
44-
import { shouldExternalizeForSSR } from '../ssr/ssrExternal'
44+
import {
45+
cjsShouldExternalizeForSSR,
46+
shouldExternalizeForSSR
47+
} from '../ssr/ssrExternal'
4548
import { transformRequest } from '../server/transformRequest'
4649
import {
4750
getDepsCacheDir,
@@ -362,10 +365,11 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
362365
}
363366
// skip ssr external
364367
if (ssr) {
365-
if (
366-
server._ssrExternals &&
367-
shouldExternalizeForSSR(specifier, server._ssrExternals)
368-
) {
368+
if (config.ssr?.format === 'cjs') {
369+
if (cjsShouldExternalizeForSSR(specifier, server._ssrExternals)) {
370+
continue
371+
}
372+
} else if (shouldExternalizeForSSR(specifier, config)) {
369373
continue
370374
}
371375
if (isBuiltin(specifier)) {

packages/vite/src/node/plugins/index.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ResolvedConfig } from '../config'
33
import { isDepsOptimizerEnabled } from '../config'
44
import type { Plugin } from '../plugin'
55
import { getDepsOptimizer } from '../optimizer'
6+
import { shouldExternalizeForSSR } from '../ssr/ssrExternal'
67
import { jsonPlugin } from './json'
78
import { resolvePlugin } from './resolve'
89
import { optimizedDepsBuildPlugin, optimizedDepsPlugin } from './optimizedDeps'
@@ -61,7 +62,11 @@ export async function resolvePlugins(
6162
packageCache: config.packageCache,
6263
ssrConfig: config.ssr,
6364
asSrc: true,
64-
getDepsOptimizer: () => getDepsOptimizer(config)
65+
getDepsOptimizer: () => getDepsOptimizer(config),
66+
shouldExternalize:
67+
isBuild && config.build.ssr && config.ssr?.format !== 'cjs'
68+
? (id) => shouldExternalizeForSSR(id, config)
69+
: undefined
6570
}),
6671
htmlInlineProxyPlugin(config),
6772
cssPlugin(config),

packages/vite/src/node/plugins/resolve.ts

+35-9
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export interface InternalResolveOptions extends ResolveOptions {
8383
scan?: boolean
8484
// Resolve using esbuild deps optimization
8585
getDepsOptimizer?: () => DepsOptimizer | undefined
86+
shouldExternalize?: (id: string) => boolean | undefined
8687
}
8788

8889
export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
@@ -105,6 +106,7 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
105106
const depsOptimizer = baseOptions.getDepsOptimizer?.()
106107

107108
const ssr = resolveOpts?.ssr === true
109+
108110
if (id.startsWith(browserExternalId)) {
109111
return id
110112
}
@@ -258,7 +260,10 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
258260

259261
// bare package imports, perform node resolve
260262
if (bareImportRE.test(id)) {
263+
const external = options.shouldExternalize?.(id)
264+
261265
if (
266+
!external &&
262267
asSrc &&
263268
depsOptimizer &&
264269
!ssr &&
@@ -270,7 +275,13 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
270275

271276
if (
272277
targetWeb &&
273-
(res = tryResolveBrowserMapping(id, importer, options, false))
278+
(res = tryResolveBrowserMapping(
279+
id,
280+
importer,
281+
options,
282+
false,
283+
external
284+
))
274285
) {
275286
return res
276287
}
@@ -282,7 +293,8 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
282293
options,
283294
targetWeb,
284295
depsOptimizer,
285-
ssr
296+
ssr,
297+
external
286298
))
287299
) {
288300
return res
@@ -523,7 +535,8 @@ export function tryNodeResolve(
523535
options: InternalResolveOptions,
524536
targetWeb: boolean,
525537
depsOptimizer?: DepsOptimizer,
526-
ssr?: boolean
538+
ssr?: boolean,
539+
externalize?: boolean
527540
): PartialResolvedId | undefined {
528541
const { root, dedupe, isBuild, preserveSymlinks, packageCache } = options
529542

@@ -591,7 +604,8 @@ export function tryNodeResolve(
591604

592605
let resolveId = resolvePackageEntry
593606
let unresolvedId = pkgId
594-
if (unresolvedId !== nestedPath) {
607+
const isDeepImport = unresolvedId !== nestedPath
608+
if (isDeepImport) {
595609
resolveId = resolveDeepImport
596610
unresolvedId = '.' + nestedPath.slice(pkgId.length)
597611
}
@@ -616,15 +630,25 @@ export function tryNodeResolve(
616630
return
617631
}
618632

633+
const processResult = (resolved: PartialResolvedId) => {
634+
if (!externalize) {
635+
return resolved
636+
}
637+
const resolvedExt = path.extname(resolved.id)
638+
const resolvedId =
639+
isDeepImport && path.extname(id) !== resolvedExt ? id + resolvedExt : id
640+
return { ...resolved, id: resolvedId, external: true }
641+
}
642+
619643
// link id to pkg for browser field mapping check
620644
idToPkgMap.set(resolved, pkg)
621-
if (isBuild && !depsOptimizer) {
645+
if ((isBuild && !depsOptimizer) || externalize) {
622646
// Resolve package side effects for build so that rollup can better
623647
// perform tree-shaking
624-
return {
648+
return processResult({
625649
id: resolved,
626650
moduleSideEffects: pkg.hasSideEffects(resolved)
627-
}
651+
})
628652
}
629653

630654
if (
@@ -940,7 +964,8 @@ function tryResolveBrowserMapping(
940964
id: string,
941965
importer: string | undefined,
942966
options: InternalResolveOptions,
943-
isFilePath: boolean
967+
isFilePath: boolean,
968+
externalize?: boolean
944969
) {
945970
let res: string | undefined
946971
const pkg = importer && idToPkgMap.get(importer)
@@ -953,10 +978,11 @@ function tryResolveBrowserMapping(
953978
isDebug &&
954979
debug(`[browser mapped] ${colors.cyan(id)} -> ${colors.dim(res)}`)
955980
idToPkgMap.set(res, pkg)
956-
return {
981+
const result = {
957982
id: res,
958983
moduleSideEffects: pkg.hasSideEffects(res)
959984
}
985+
return externalize ? { ...result, external: true } : result
960986
}
961987
} else if (browserMappedPath === false) {
962988
return browserExternalId

packages/vite/src/node/plugins/ssrRequireHook.ts

+2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import { arraify } from '../utils'
1212
export function ssrRequireHookPlugin(config: ResolvedConfig): Plugin | null {
1313
if (
1414
config.command !== 'build' ||
15+
!config.build.ssr ||
1516
!config.resolve.dedupe?.length ||
1617
config.ssr?.noExternal === true ||
18+
config.ssr?.format !== 'cjs' ||
1719
isBuildOutputEsm(config)
1820
) {
1921
return null

packages/vite/src/node/server/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
resolveHostname
2323
} from '../utils'
2424
import { ssrLoadModule } from '../ssr/ssrModuleLoader'
25-
import { resolveSSRExternal } from '../ssr/ssrExternal'
25+
import { cjsSsrResolveExternals } from '../ssr/ssrExternal'
2626
import {
2727
rebindErrorStacktrace,
2828
ssrRewriteStacktrace
@@ -330,7 +330,7 @@ export async function createServer(
330330
...Object.keys(depsOptimizer.metadata.discovered)
331331
]
332332
}
333-
server._ssrExternals = resolveSSRExternal(config, knownImports)
333+
server._ssrExternals = cjsSsrResolveExternals(config, knownImports)
334334
}
335335
return ssrLoadModule(
336336
url,

0 commit comments

Comments
 (0)