Skip to content

Commit

Permalink
feat: default esm SSR build, simplified externalization
Browse files Browse the repository at this point in the history
  • Loading branch information
patak-dev committed May 26, 2022
1 parent 689adc0 commit 3b08d34
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 46 deletions.
2 changes: 1 addition & 1 deletion docs/config/ssr-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Prevent listed dependencies from being externalized for SSR. If `true`, no depen

## ssr.target

- **Type:** `'node' | 'webworker'`
- **Type:** `'node' | 'webworker' | 'node-cjs'`
- **Default:** `node`

Build target for the SSR server.
94 changes: 64 additions & 30 deletions packages/vite/src/node/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { manifestPlugin } from './plugins/manifest'
import type { Logger } from './logger'
import { dataURIPlugin } from './plugins/dataUri'
import { buildImportAnalysisPlugin } from './plugins/importAnalysisBuild'
import { resolveSSRExternal, shouldExternalizeForSSR } from './ssr/ssrExternal'
import { cjsShouldExternalizeForSSR, cjsSsrResolveExternals, shouldExternalizeForSSR } from './ssr/ssrExternal'
import { ssrManifestPlugin } from './ssr/ssrManifestPlugin'
import type { DepOptimizationMetadata } from './optimizer'
import { findKnownImports, getDepsCacheDir } from './optimizer'
Expand Down Expand Up @@ -367,27 +367,10 @@ async function doBuild(
ssr ? config.plugins.map((p) => injectSsrFlagToHooks(p)) : config.plugins
) as Plugin[]

// inject ssrExternal if present
const userExternal = options.rollupOptions?.external
let external = userExternal
if (ssr) {
// see if we have cached deps data available
let knownImports: string[] | undefined
const dataPath = path.join(getDepsCacheDir(config), '_metadata.json')
try {
const data = JSON.parse(
fs.readFileSync(dataPath, 'utf-8')
) as DepOptimizationMetadata
knownImports = Object.keys(data.optimized)
} catch (e) {}
if (!knownImports) {
// no dev deps optimization data, do a fresh scan
knownImports = await findKnownImports(config)
}
external = resolveExternal(
resolveSSRExternal(config, knownImports),
userExternal
)
external = await ssrResolveExternal(config, userExternal)
}

const rollupOptions: RollupOptions = {
Expand Down Expand Up @@ -421,10 +404,12 @@ async function doBuild(

try {
const buildOutputOptions = (output: OutputOptions = {}): OutputOptions => {
const cjsSsrBuild = ssr && config.ssr?.target === 'node-cjs'
return {
dir: outDir,
format: ssr ? 'cjs' : 'es',
exports: ssr ? 'named' : 'auto',
// Default format is 'es' for regular and for SSR builds
format: cjsSsrBuild ? 'cjs' : 'es',
exports: cjsSsrBuild ? 'named' : 'auto',
sourcemap: options.sourcemap,
name: libOptions ? libOptions.name : undefined,
generatedCode: 'es2015',
Expand Down Expand Up @@ -686,26 +671,75 @@ export function onRollupWarning(
}
}

function resolveExternal(
async function ssrResolveExternal(
config: ResolvedConfig,
user: ExternalOption | undefined
): Promise<ExternalOption> {
if( config.ssr?.target !== 'node-cjs') {
return esmSsrResolveExternal(config, user)
}
else {
// see if we have cached deps data available
let knownImports: string[] | undefined
const dataPath = path.join(getDepsCacheDir(config), '_metadata.json')
try {
const data = JSON.parse(
fs.readFileSync(dataPath, 'utf-8')
) as DepOptimizationMetadata
knownImports = Object.keys(data.optimized)
} catch (e) {}
if (!knownImports) {
// no dev deps optimization data, do a fresh scan
knownImports = await findKnownImports(config)
}
return cjsSsrResolveExternal(
cjsSsrResolveExternals(config, knownImports),
user
)
}
}

function esmSsrResolveExternal(
config: ResolvedConfig,
user: ExternalOption | undefined
): ExternalOption {
return (id, parentId, isResolved) => {
if( user ) {
const isUserExternal = resolveUserExternal(user, id, parentId, isResolved)
if( typeof isUserExternal === 'boolean' ) {
return isUserExternal
}
}
return shouldExternalizeForSSR(id, config)
}
}

// When ssr.format is node-cjs, this function reverts back to the 2.9 logic for externalization
function cjsSsrResolveExternal(
ssrExternals: string[],
user: ExternalOption | undefined
): ExternalOption {
return (id, parentId, isResolved) => {
if (shouldExternalizeForSSR(id, ssrExternals)) {
const isExternal = cjsShouldExternalizeForSSR(id, ssrExternals)
if (isExternal) {
return true
}
if (user) {
if (typeof user === 'function') {
return user(id, parentId, isResolved)
} else if (Array.isArray(user)) {
return user.some((test) => isExternal(id, test))
} else {
return isExternal(id, user)
}
return resolveUserExternal(user, id, parentId, isResolved)
}
}
}

function resolveUserExternal(user: ExternalOption, id: string, parentId: string | undefined, isResolved: boolean) {
if (typeof user === 'function') {
return user(id, parentId, isResolved)
} else if (Array.isArray(user)) {
return user.some((test) => isExternal(id, test))
} else {
return isExternal(id, user)
}
}

function isExternal(id: string, test: string | RegExp) {
if (typeof test === 'string') {
return id === test
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ export interface ExperimentalOptions {
importGlobRestoreExtension?: boolean
}

export type SSRTarget = 'node' | 'webworker'
export type SSRTarget = 'node' | 'webworker' | 'node-cjs'

export interface SSROptions {
external?: string[]
Expand Down
13 changes: 8 additions & 5 deletions packages/vite/src/node/plugins/importAnalysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {
} from '../utils'
import type { ResolvedConfig } from '../config'
import type { Plugin } from '../plugin'
import { shouldExternalizeForSSR } from '../ssr/ssrExternal'
import { cjsShouldExternalizeForSSR, shouldExternalizeForSSR } from '../ssr/ssrExternal'
import { transformRequest } from '../server/transformRequest'
import {
getDepsCacheDir,
Expand Down Expand Up @@ -363,10 +363,13 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
}
// skip ssr external
if (ssr) {
if (
server._ssrExternals &&
shouldExternalizeForSSR(specifier, server._ssrExternals)
) {
if (config.ssr?.target === 'node-cjs') {
if (cjsShouldExternalizeForSSR(specifier, server._ssrExternals)
) {
continue
}
}
else if (shouldExternalizeForSSR(specifier, config)) {
continue
}
if (isBuiltin(specifier)) {
Expand Down
4 changes: 2 additions & 2 deletions packages/vite/src/node/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
resolveHostname
} from '../utils'
import { ssrLoadModule } from '../ssr/ssrModuleLoader'
import { resolveSSRExternal } from '../ssr/ssrExternal'
import { cjsSsrResolveExternals } from '../ssr/ssrExternal'
import {
rebindErrorStacktrace,
ssrRewriteStacktrace
Expand Down Expand Up @@ -339,7 +339,7 @@ export async function createServer(
...Object.keys(optimizedDeps.metadata.discovered)
]
}
server._ssrExternals = resolveSSRExternal(config, knownImports)
server._ssrExternals = cjsSsrResolveExternals(config, knownImports)
}
return ssrLoadModule(
url,
Expand Down
97 changes: 90 additions & 7 deletions packages/vite/src/node/ssr/ssrExternal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { createFilter } from '@rollup/pluginutils'
import type { InternalResolveOptions } from '../plugins/resolve'
import { tryNodeResolve } from '../plugins/resolve'
import {
bareImportRE,
createDebugger,
isBuiltin,
isDefined,
lookupFile,
normalizePath,
Expand All @@ -29,7 +31,7 @@ export function stripNesting(packages: string[]) {
* Heuristics for determining whether a dependency should be externalized for
* server-side rendering.
*/
export function resolveSSRExternal(
export function cjsSsrResolveExternals(
config: ResolvedConfig,
knownImports: string[]
): string[] {
Expand All @@ -49,7 +51,7 @@ export function resolveSSRExternal(
seen.add(id)
})

collectExternals(
cjsSsrCollectExternals(
config.root,
config.resolve.preserveSymlinks,
ssrExternals,
Expand Down Expand Up @@ -86,8 +88,86 @@ const CJS_CONTENT_RE =
// TODO: use import()
const _require = createRequire(import.meta.url)

// do we need to do this ahead of time or could we do it lazily?
function collectExternals(
const isSsrExternalCache = new WeakMap<ResolvedConfig, (id: string) => boolean | undefined>()

export function shouldExternalizeForSSR(
id: string,
config: ResolvedConfig
): boolean | undefined {
let isSsrExternal = isSsrExternalCache.get(config)
if( !isSsrExternal ) {
isSsrExternal = createIsSsrExternal(config)
isSsrExternalCache.set(config, isSsrExternal)
}
return isSsrExternal(id)
}

function createIsSsrExternal(config: ResolvedConfig): (id: string) => boolean | undefined {
const processedIds = new Map<string, boolean | undefined>()

const { ssr, root } = config

const noExternal = ssr?.noExternal
const noExternalFilter = noExternal !== 'undefined' && typeof noExternal !== 'boolean' && createFilter(undefined, noExternal, { resolve: false })

const isConfiguredAsExternal = (id: string) => {
const { ssr } = config
if ( !ssr || ssr.external?.includes(id)) {
return true
}
if ( typeof noExternal === 'boolean' ) {
return !noExternal
}
if ( noExternalFilter ) {
return noExternalFilter(id)
}
return true
}

const resolveOptions: InternalResolveOptions = {
root,
preserveSymlinks: config.resolve.preserveSymlinks,
isProduction: false,
isBuild: true
}

const isPackageEntry = (id: string) => {
if (!bareImportRE.test(id) || id.includes('\0')) {
return false
}
if( tryNodeResolve(
id,
undefined,
resolveOptions,
ssr?.target === 'webworker',
undefined,
true
)) {
return true
}
try {
// no main entry, but deep imports may be allowed
const pkgPath = resolveFrom(`${id}/package.json`, root)
if (pkgPath.includes('node_modules')) {
return true
}
} catch {}
return false
}

return (id: string) => {
if( processedIds.has(id) ) {
return processedIds.get(id)
}
const external = isBuiltin(id) || (isPackageEntry(id) && isConfiguredAsExternal(id))
processedIds.set(id, external)
return external
}
}

// When ssr.format is 'node-cjs', this function is used reverting to the Vite 2.9 era
// SSR externalize heuristics
function cjsSsrCollectExternals(
root: string,
preserveSymlinks: boolean | undefined,
ssrExternals: Set<string>,
Expand Down Expand Up @@ -192,14 +272,17 @@ function collectExternals(
}

for (const depRoot of depsToTrace) {
collectExternals(depRoot, preserveSymlinks, ssrExternals, seen, logger)
cjsSsrCollectExternals(depRoot, preserveSymlinks, ssrExternals, seen, logger)
}
}

export function shouldExternalizeForSSR(
export function cjsShouldExternalizeForSSR(
id: string,
externals: string[]
externals: string[] | null
): boolean {
if( !externals ) {
return false
}
const should = externals.some((e) => {
if (id === e) {
return true
Expand Down

0 comments on commit 3b08d34

Please sign in to comment.