diff --git a/rollup.config.js b/rollup.config.js index 8f1f85b9363..cb1c1d274d8 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -15,6 +15,12 @@ import alias from '@rollup/plugin-alias' import { entries } from './scripts/aliases.js' import { inlineEnums } from './scripts/inline-enums.js' import { minify as minifySwc } from '@swc/core' +import { + resolveCJSIgnores, + resolveDefines, + resolveEntryFile, + resolveExternal as resolveExternalShared, +} from './scripts/build-shared.js' /** * @template T @@ -32,7 +38,6 @@ const require = createRequire(import.meta.url) const __dirname = fileURLToPath(new URL('.', import.meta.url)) const masterVersion = require('./package.json').version -const consolidatePkg = require('@vue/consolidate/package.json') const privatePackages = fs.readdirSync('packages-private') const pkgBase = privatePackages.includes(process.env.TARGET) @@ -156,54 +161,19 @@ function createConfig(format, output, plugins = []) { output.name = packageOptions.name } - let entryFile = /runtime$/.test(format) ? `src/runtime.ts` : `src/index.ts` - - // the compat build needs both default AND named exports. This will cause - // Rollup to complain for non-ESM targets, so we use separate entries for - // esm vs. non-esm builds. - if (isCompatPackage && (isBrowserESMBuild || isBundlerESMBuild)) { - entryFile = /runtime$/.test(format) - ? `src/esm-runtime.ts` - : `src/esm-index.ts` - } + // Use shared function for entry file resolution + const entryFile = resolveEntryFile(format, isCompatPackage) function resolveDefine() { - /** @type {Record} */ - const replacements = { - __COMMIT__: `"${process.env.COMMIT}"`, - __VERSION__: `"${masterVersion}"`, - // this is only used during Vue's internal tests - __TEST__: `false`, - // If the build is expected to run directly in the browser (global / esm builds) - __BROWSER__: String(isBrowserBuild), - __GLOBAL__: String(isGlobalBuild), - __ESM_BUNDLER__: String(isBundlerESMBuild), - __ESM_BROWSER__: String(isBrowserESMBuild), - // is targeting Node (SSR)? - __CJS__: String(isCJSBuild), - // need SSR-specific branches? - __SSR__: String(!isGlobalBuild), - - // 2.x compat build - __COMPAT__: String(isCompatBuild), - - // feature flags - __FEATURE_SUSPENSE__: `true`, - __FEATURE_OPTIONS_API__: isBundlerESMBuild - ? `__VUE_OPTIONS_API__` - : `true`, - __FEATURE_PROD_DEVTOOLS__: isBundlerESMBuild - ? `__VUE_PROD_DEVTOOLS__` - : `false`, - __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__: isBundlerESMBuild - ? `__VUE_PROD_HYDRATION_MISMATCH_DETAILS__` - : `false`, - } - - if (!isBundlerESMBuild) { - // hard coded dev/prod builds - replacements.__DEV__ = String(!isProductionBuild) - } + // Use shared function for base defines + const replacements = resolveDefines({ + pkg, + format, + target: process.env.TARGET, + prod: isProductionBuild, + version: masterVersion, + commit: process.env.COMMIT || 'dev', + }) // allow inline overrides like //__RUNTIME_COMPILE__=true pnpm build runtime-core @@ -255,50 +225,21 @@ function createConfig(format, output, plugins = []) { } function resolveExternal() { - const treeShakenDeps = [ - 'source-map-js', - '@babel/parser', - 'estree-walker', - 'entities/lib/decode.js', - ] - - if (isGlobalBuild || isBrowserESMBuild || isCompatPackage) { - if (!packageOptions.enableNonBrowserBranches) { - // normal browser builds - non-browser only imports are tree-shaken, - // they are only listed here to suppress warnings. - return treeShakenDeps - } - } else { - // Node / esm-bundler builds. - // externalize all direct deps unless it's the compat build. - return [ - ...Object.keys(pkg.dependencies || {}), - ...Object.keys(pkg.peerDependencies || {}), - // for @vue/compiler-sfc / server-renderer - ...['path', 'url', 'stream'], - // somehow these throw warnings for runtime-* package builds - ...treeShakenDeps, - ] - } + // Use shared function for external resolution + return resolveExternalShared({ + pkg, + format, + target: process.env.TARGET, + isGlobalBuild, + isBrowserESMBuild, + isCompatPackage, + packageOptions, + }) } function resolveNodePlugins() { - // we are bundling forked consolidate.js in compiler-sfc which dynamically - // requires a ton of template engines which should be ignored. - /** @type {ReadonlyArray} */ - let cjsIgnores = [] - if (pkg.name === '@vue/compiler-sfc') { - cjsIgnores = [ - ...Object.keys(consolidatePkg.devDependencies), - 'vm', - 'crypto', - 'react-dom/server', - 'teacup/lib/express', - 'arc-templates/dist/es5', - 'then-pug', - 'then-jade', - ] - } + // Use shared function for CJS ignores + const cjsIgnores = resolveCJSIgnores(process.env.TARGET) const nodePlugins = (format === 'cjs' && Object.keys(pkg.devDependencies || {}).length) || diff --git a/scripts/build-shared.js b/scripts/build-shared.js new file mode 100644 index 00000000000..77fae01c9d1 --- /dev/null +++ b/scripts/build-shared.js @@ -0,0 +1,206 @@ +// @ts-check +import { createRequire } from 'node:module' + +const require = createRequire(import.meta.url) + +/** + * @param {Object} options + * @param {any} options.pkg - Package.json object + * @param {string} options.format - Build format + * @param {string|undefined} options.target - Target package name + * @param {boolean} options.isGlobalBuild - Whether this is a global build + * @param {boolean} options.isBrowserESMBuild - Whether this is a browser ESM build + * @param {boolean} options.isCompatPackage - Whether this is the compat package + * @param {any} options.packageOptions - Package build options + * @returns {string[]} + */ +export function resolveExternal({ + pkg, + format, + target = '', + isGlobalBuild = false, + isBrowserESMBuild = false, + isCompatPackage = false, + packageOptions = {}, +}) { + const treeShakenDeps = [ + 'source-map-js', + '@babel/parser', + 'estree-walker', + 'entities/lib/decode.js', + ] + + // Global and browser builds inline everything + if (isGlobalBuild || isBrowserESMBuild || isCompatPackage) { + if (!packageOptions.enableNonBrowserBranches) { + return treeShakenDeps + } + } + + // Base externals for Node/bundler builds + let external = [] + + // For CJS and ESM-bundler formats, externalize dependencies + if (format === 'cjs' || format.includes('esm-bundler')) { + external = [ + ...Object.keys(pkg.dependencies || {}), + ...Object.keys(pkg.peerDependencies || {}), + 'path', + 'url', + 'stream', + ] + } + + // Special handling for compiler-sfc + if (target === 'compiler-sfc') { + const consolidateDeps = getConsolidateDeps() + external = [ + ...external, + ...consolidateDeps, + 'fs', + 'vm', + 'crypto', + 'react-dom/server', + 'teacup/lib/express', + 'arc-templates/dist/es5', + 'then-pug', + 'then-jade', + ] + } + + // Add tree-shaken deps to suppress warnings + if (external.length) { + external = [...external, ...treeShakenDeps] + } + + return external +} + +/** + * Get consolidate package dependencies + * @returns {string[]} + */ +export function getConsolidateDeps() { + try { + const consolidatePkg = require('@vue/consolidate/package.json') + return Object.keys(consolidatePkg.devDependencies || {}) + } catch { + return [] + } +} + +/** + * Resolve compiler ignore list for CommonJS + * @param {string|undefined} target - Target package name + * @returns {string[]} + */ +export function resolveCJSIgnores(target) { + if (target === 'compiler-sfc') { + return [ + ...getConsolidateDeps(), + 'vm', + 'crypto', + 'react-dom/server', + 'teacup/lib/express', + 'arc-templates/dist/es5', + 'then-pug', + 'then-jade', + ] + } + return [] +} + +/** + * Resolve define/replace values for build + * @param {Object} options + * @param {any} options.pkg - Package.json object + * @param {string} options.format - Build format + * @param {string | undefined} options.target - Target package name + * @param {boolean} options.prod - Whether this is a production build + * @param {string} [options.version] - Version override + * @param {string} [options.commit] - Commit hash + * @returns {Record} + */ +export function resolveDefines({ + pkg, + format, + target = '', + prod, + version, + commit = 'dev', +}) { + const isBundlerESMBuild = format.includes('esm-bundler') + const isBrowserESMBuild = format.includes('esm-browser') + const isGlobalBuild = format.includes('global') + const isCJSBuild = format === 'cjs' + const isCompatBuild = target === 'vue-compat' || pkg.buildOptions?.compat + + // Determine if this is a browser build + const isBrowserBuild = + (isGlobalBuild || isBrowserESMBuild || isBundlerESMBuild) && + !pkg.buildOptions?.enableNonBrowserBranches + + const defines = { + __COMMIT__: `"${commit}"`, + __VERSION__: `"${version || pkg.version}"`, + __DEV__: prod ? `false` : `true`, + __TEST__: `false`, + __BROWSER__: String(isBrowserBuild), + __GLOBAL__: String(isGlobalBuild), + __ESM_BUNDLER__: String(isBundlerESMBuild), + __ESM_BROWSER__: String(isBrowserESMBuild), + __CJS__: String(isCJSBuild), + __SSR__: String(!isGlobalBuild), + __COMPAT__: String(isCompatBuild), + __FEATURE_SUSPENSE__: `true`, + __FEATURE_OPTIONS_API__: isBundlerESMBuild ? `__VUE_OPTIONS_API__` : `true`, + __FEATURE_PROD_DEVTOOLS__: isBundlerESMBuild + ? `__VUE_PROD_DEVTOOLS__` + : `false`, + __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__: isBundlerESMBuild + ? `__VUE_PROD_HYDRATION_MISMATCH_DETAILS__` + : `false`, + } + + return defines +} + +/** + * Resolve output format for different build types + * @param {string} format - Build format string + * @returns {'iife' | 'cjs' | 'esm'} + */ +export function resolveOutputFormat(format) { + if (format.startsWith('global')) return 'iife' + if (format === 'cjs') return 'cjs' + return 'esm' +} + +/** + * Resolve output file postfix + * @param {string} format - Build format string + * @returns {string} + */ +export function resolvePostfix(format) { + return format.endsWith('-runtime') + ? `runtime.${format.replace(/-runtime$/, '')}` + : format +} + +/** + * Resolve entry file based on format and package + * @param {string} format - Build format + * @param {boolean} isCompatPackage - Whether this is compat package + * @returns {string} + */ +export function resolveEntryFile(format, isCompatPackage = false) { + const isRuntime = /runtime$/.test(format) + const isBrowserESMBuild = format.includes('esm-browser') + const isBundlerESMBuild = format.includes('esm-bundler') + + if (isCompatPackage && (isBrowserESMBuild || isBundlerESMBuild)) { + return isRuntime ? `src/esm-runtime.ts` : `src/esm-index.ts` + } + + return isRuntime ? `src/runtime.ts` : `src/index.ts` +} diff --git a/scripts/dev.js b/scripts/dev.js index fb4d3873e8b..67ae8c0ab83 100644 --- a/scripts/dev.js +++ b/scripts/dev.js @@ -11,6 +11,12 @@ import { fileURLToPath } from 'node:url' import { createRequire } from 'node:module' import { parseArgs } from 'node:util' import { polyfillNode } from 'esbuild-plugin-polyfill-node' +import { + resolveDefines, + resolveExternal, + resolveOutputFormat, + resolvePostfix, +} from './build-shared.js' const require = createRequire(import.meta.url) const __dirname = dirname(fileURLToPath(import.meta.url)) @@ -42,16 +48,9 @@ const { const format = rawFormat || 'global' const targets = positionals.length ? positionals : ['vue'] -// resolve output -const outputFormat = format.startsWith('global') - ? 'iife' - : format === 'cjs' - ? 'cjs' - : 'esm' - -const postfix = format.endsWith('-runtime') - ? `runtime.${format.replace(/-runtime$/, '')}` - : format +// resolve output using shared functions +const outputFormat = resolveOutputFormat(format) +const postfix = resolvePostfix(format) const privatePackages = fs.readdirSync('packages-private') @@ -69,48 +68,19 @@ for (const target of targets) { ) const relativeOutfile = relative(process.cwd(), outfile) - // resolve externals - // TODO this logic is largely duplicated from rollup.config.js - /** @type {string[]} */ - let external = [] - if (!inlineDeps) { - // cjs & esm-bundler: external all deps - if (format === 'cjs' || format.includes('esm-bundler')) { - external = [ - ...external, - ...Object.keys(pkg.dependencies || {}), - ...Object.keys(pkg.peerDependencies || {}), - // for @vue/compiler-sfc / server-renderer - 'path', - 'url', - 'stream', - ] - } + // resolve externals using shared function + const external = inlineDeps + ? [] + : resolveExternal({ + pkg, + format, + target, + isGlobalBuild: format === 'global', + isBrowserESMBuild: format.includes('esm-browser'), + isCompatPackage: target === 'vue-compat', + packageOptions: pkg.buildOptions || {}, + }) - if (target === 'compiler-sfc') { - const consolidatePkgPath = require.resolve( - '@vue/consolidate/package.json', - { - paths: [resolve(__dirname, `../packages/${target}/`)], - }, - ) - const consolidateDeps = Object.keys( - require(consolidatePkgPath).devDependencies, - ) - external = [ - ...external, - ...consolidateDeps, - 'fs', - 'vm', - 'crypto', - 'react-dom/server', - 'teacup/lib/express', - 'arc-templates/dist/es5', - 'then-pug', - 'then-jade', - ] - } - } /** @type {Array} */ const plugins = [ { @@ -127,6 +97,15 @@ for (const target of targets) { plugins.push(polyfillNode()) } + // resolve defines using shared function + const defines = resolveDefines({ + pkg, + format, + target, + prod, + commit: 'dev', + }) + esbuild .context({ entryPoints: [resolve(__dirname, `${pkgBasePath}/src/index.ts`)], @@ -138,25 +117,7 @@ for (const target of targets) { globalName: pkg.buildOptions?.name, platform: format === 'cjs' ? 'node' : 'browser', plugins, - define: { - __COMMIT__: `"dev"`, - __VERSION__: `"${pkg.version}"`, - __DEV__: prod ? `false` : `true`, - __TEST__: `false`, - __BROWSER__: String( - format !== 'cjs' && !pkg.buildOptions?.enableNonBrowserBranches, - ), - __GLOBAL__: String(format === 'global'), - __ESM_BUNDLER__: String(format.includes('esm-bundler')), - __ESM_BROWSER__: String(format.includes('esm-browser')), - __CJS__: String(format === 'cjs'), - __SSR__: String(format !== 'global'), - __COMPAT__: String(target === 'vue-compat'), - __FEATURE_SUSPENSE__: `true`, - __FEATURE_OPTIONS_API__: `true`, - __FEATURE_PROD_DEVTOOLS__: `false`, - __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__: `true`, - }, + define: defines, }) .then(ctx => ctx.watch()) }