diff --git a/packages/vite/client.d.ts b/packages/vite/client.d.ts index b73389ec373f1f..0f2c3db286d82f 100644 --- a/packages/vite/client.d.ts +++ b/packages/vite/client.d.ts @@ -221,7 +221,7 @@ declare module '*.txt' { // wasm?init declare module '*.wasm?init' { const initWasm: ( - options: WebAssembly.Imports, + options?: WebAssembly.Imports, ) => Promise export default initWasm } diff --git a/packages/vite/package.json b/packages/vite/package.json index f183caa6ddd103..56b1f7258a8e2c 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -28,6 +28,9 @@ "types": "./client.d.ts" }, "./dist/client/*": "./dist/client/*", + "./types/*": { + "types": "./types/*" + }, "./package.json": "./package.json" }, "files": [ diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index dfaab8362784c5..ec9173e18afed4 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -35,6 +35,7 @@ import { joinUrlSegments, normalizePath, requireResolveFromRootWithFallback, + withTrailingSlash, } from './utils' import { manifestPlugin } from './plugins/manifest' import type { Logger } from './logger' @@ -714,7 +715,7 @@ function prepareOutDir( for (const outDir of nonDuplicateDirs) { if ( fs.existsSync(outDir) && - !normalizePath(outDir).startsWith(config.root + '/') + !normalizePath(outDir).startsWith(withTrailingSlash(config.root)) ) { // warn if outDir is outside of root config.logger.warn( @@ -1240,5 +1241,9 @@ export const toOutputFilePathInHtml = toOutputFilePathWithoutRuntime function areSeparateFolders(a: string, b: string) { const na = normalizePath(a) const nb = normalizePath(b) - return na !== nb && !na.startsWith(nb + '/') && !nb.startsWith(na + '/') + return ( + na !== nb && + !na.startsWith(withTrailingSlash(nb)) && + !nb.startsWith(withTrailingSlash(na)) + ) } diff --git a/packages/vite/src/node/cli.ts b/packages/vite/src/node/cli.ts index df858f403f80c5..dbc8280a14876d 100644 --- a/packages/vite/src/node/cli.ts +++ b/packages/vite/src/node/cli.ts @@ -102,9 +102,31 @@ function cleanOptions( return ret } +/** + * host may be a number (like 0), should convert to string + */ +const convertHost = (v: any) => { + if (typeof v === 'number') { + return String(v) + } + return v +} + +/** + * base may be a number (like 0), should convert to empty string + */ +const convertBase = (v: any) => { + if (v === 0) { + return '' + } + return v +} + cli .option('-c, --config ', `[string] use specified config file`) - .option('--base ', `[string] public base path (default: /)`) + .option('--base ', `[string] public base path (default: /)`, { + type: [convertBase], + }) .option('-l, --logLevel ', `[string] info | warn | error | silent`) .option('--clearScreen', `[boolean] allow/disable clear screen when logging`) .option('-d, --debug [feat]', `[string | boolean] show debug logs`) @@ -116,7 +138,7 @@ cli .command('[root]', 'start dev server') // default command .alias('serve') // the command is called 'serve' in Vite's API .alias('dev') // alias to align with the script name - .option('--host [host]', `[string] specify hostname`) + .option('--host [host]', `[string] specify hostname`, { type: [convertHost] }) .option('--port ', `[number] specify port`) .option('--https', `[boolean] use TLS + HTTP/2`) .option('--open [path]', `[boolean | string] open browser on startup`) @@ -306,7 +328,7 @@ cli // preview cli .command('preview [root]', 'locally preview production build') - .option('--host [host]', `[string] specify hostname`) + .option('--host [host]', `[string] specify hostname`, { type: [convertHost] }) .option('--port ', `[number] specify port`) .option('--strictPort', `[boolean] exit if specified port is already in use`) .option('--https', `[boolean] use TLS + HTTP/2`) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index f73ae2ae703e1c..123e206ccb1e3f 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -33,12 +33,14 @@ import { dynamicImport, isBuiltin, isExternalUrl, + isNodeBuiltin, isObject, lookupFile, mergeAlias, mergeConfig, normalizeAlias, normalizePath, + withTrailingSlash, } from './utils' import { createPluginHookUtils, @@ -680,7 +682,7 @@ export async function resolveConfig( ), inlineConfig, root: resolvedRoot, - base: resolvedBase.endsWith('/') ? resolvedBase : resolvedBase + '/', + base: withTrailingSlash(resolvedBase), rawBase: resolvedBase, resolve: resolveOptions, publicDir: resolvedPublicDir, @@ -856,7 +858,7 @@ assetFileNames isn't equal for every build.rollupOptions.output. A single patter ) { resolved.logger.warn( colors.yellow(` -(!) Experimental legacy.buildSsrCjsExternalHeuristics and ssr.format: 'cjs' are going to be removed in Vite 5. +(!) Experimental legacy.buildSsrCjsExternalHeuristics and ssr.format: 'cjs' are going to be removed in Vite 5. Find more information and give feedback at https://github.com/vitejs/vite/discussions/13816. `), ) @@ -1081,13 +1083,15 @@ async function bundleConfigFile( if ( kind === 'entry-point' || path.isAbsolute(id) || - isBuiltin(id) + isNodeBuiltin(id) ) { return } - // partial deno support as `npm:` does not work with esbuild - if (id.startsWith('npm:')) { + // With the `isNodeBuiltin` check above, this check captures if the builtin is a + // non-node built-in, which esbuild doesn't know how to handle. In that case, we + // externalize it so the non-node runtime handles it instead. + if (isBuiltin(id)) { return { external: true } } diff --git a/packages/vite/src/node/optimizer/optimizer.ts b/packages/vite/src/node/optimizer/optimizer.ts index 84f1575e1ec755..edb4af5232b995 100644 --- a/packages/vite/src/node/optimizer/optimizer.ts +++ b/packages/vite/src/node/optimizer/optimizer.ts @@ -619,8 +619,6 @@ async function createDepsOptimizer( return } - const crawlDeps = Object.keys(metadata.discovered) - // Await for the scan+optimize step running in the background // It normally should be over by the time crawling of user code ended await depsOptimizer.scanProcessing @@ -630,6 +628,7 @@ async function createDepsOptimizer( optimizationResult = undefined currentlyProcessing = false + const crawlDeps = Object.keys(metadata.discovered) const scanDeps = Object.keys(result.metadata.optimized) if (scanDeps.length === 0 && crawlDeps.length === 0) { @@ -680,6 +679,7 @@ async function createDepsOptimizer( runOptimizer(result) } } else { + const crawlDeps = Object.keys(metadata.discovered) currentlyProcessing = false if (crawlDeps.length === 0) { diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 2c298a94c31f5a..d16130b58c01b9 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -23,6 +23,7 @@ import { joinUrlSegments, normalizePath, removeLeadingSlash, + withTrailingSlash, } from '../utils' import { FS_PREFIX } from '../constants' @@ -229,7 +230,11 @@ export function checkPublicFile( return } const publicFile = path.join(publicDir, cleanUrl(url)) - if (!publicFile.startsWith(publicDir)) { + if ( + !normalizePath(publicFile).startsWith( + withTrailingSlash(normalizePath(publicDir)), + ) + ) { // can happen if URL starts with '../' return } @@ -257,7 +262,7 @@ function fileToDevUrl(id: string, config: ResolvedConfig) { if (checkPublicFile(id, config)) { // in public dir, keep the url as-is rtn = id - } else if (id.startsWith(config.root)) { + } else if (id.startsWith(withTrailingSlash(config.root))) { // in project root, infer short public path rtn = '/' + path.posix.relative(config.root, id) } else { diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 2b8d40b2bb20bd..933cb507a3150c 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -406,7 +406,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:css-post', - buildStart() { + renderStart() { // Ensure new caches for every build (i.e. rebuilding in watch mode) pureCssChunks = new Set() outputToExtractedCSSMap = new Map() @@ -769,11 +769,15 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { // chunks instead. chunk.imports = chunk.imports.filter((file) => { if (pureCssChunkNames.includes(file)) { - const { importedCss } = (bundle[file] as OutputChunk) - .viteMetadata! + const { importedCss, importedAssets } = ( + bundle[file] as OutputChunk + ).viteMetadata! importedCss.forEach((file) => chunk.viteMetadata!.importedCss.add(file), ) + importedAssets.forEach((file) => + chunk.viteMetadata!.importedAssets.add(file), + ) return false } return true @@ -789,6 +793,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { pureCssChunkNames.forEach((fileName) => { removedPureCssFiles.set(fileName, bundle[fileName] as RenderedChunk) delete bundle[fileName] + delete bundle[`${fileName}.map`] }) } diff --git a/packages/vite/src/node/plugins/esbuild.ts b/packages/vite/src/node/plugins/esbuild.ts index d2bf848437a915..591e2883453fe3 100644 --- a/packages/vite/src/node/plugins/esbuild.ts +++ b/packages/vite/src/node/plugins/esbuild.ts @@ -28,10 +28,9 @@ import { searchForWorkspaceRoot } from '../server/searchRoot' const debug = createDebugger('vite:esbuild') -const INJECT_HELPERS_IIFE_RE = - /^(.*?)((?:const|var)\s+\S+\s*=\s*function\s*\([^)]*\)\s*\{\s*"use strict";)/s -const INJECT_HELPERS_UMD_RE = - /^(.*?)(\(function\([^)]*\)\s*\{.+?amd.+?function\([^)]*\)\s*\{\s*"use strict";)/s +// IIFE content looks like `var MyLib = function() {`. Spaces are removed when minified +const IIFE_BEGIN_RE = + /(const|var)\s+\S+\s*=\s*function\(\)\s*\{.*"use strict";/s const validExtensionRE = /\.\w+$/ const jsxExtensionsRE = /\.(?:j|t)sx\b/ @@ -333,22 +332,30 @@ export const buildEsbuildPlugin = (config: ResolvedConfig): Plugin => { if (config.build.lib) { // #7188, esbuild adds helpers out of the UMD and IIFE wrappers, and the // names are minified potentially causing collision with other globals. - // We use a regex to inject the helpers inside the wrappers. + // We inject the helpers inside the wrappers. + // e.g. turn: + // (function(){ /*actual content/* })() + // into: + // (function(){ /*actual content/* })() + // Not using regex because it's too hard to rule out performance issues like #8738 #8099 #10900 #14065 + // Instead, using plain string index manipulation (indexOf, slice) which is simple and performant // We don't need to create a MagicString here because both the helpers and // the headers don't modify the sourcemap - const injectHelpers = - opts.format === 'umd' - ? INJECT_HELPERS_UMD_RE - : opts.format === 'iife' - ? INJECT_HELPERS_IIFE_RE - : undefined - if (injectHelpers) { - res.code = res.code.replace( - injectHelpers, - (_, helpers, header) => header + helpers, - ) + const esbuildCode = res.code + const contentIndex = + opts.format === 'iife' + ? esbuildCode.match(IIFE_BEGIN_RE)?.index || 0 + : opts.format === 'umd' + ? esbuildCode.indexOf(`(function(`) // same for minified or not + : 0 + if (contentIndex > 0) { + const esbuildHelpers = esbuildCode.slice(0, contentIndex) + res.code = esbuildCode + .slice(contentIndex) + .replace(`"use strict";`, `"use strict";` + esbuildHelpers) } } + return res }, } diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index c33cd519fb4da7..45e8a38788353c 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -44,6 +44,7 @@ import { timeFrom, transformStableResult, unwrapId, + withTrailingSlash, wrapId, } from '../utils' import { getDepOptimizationConfig } from '../config' @@ -81,6 +82,8 @@ export const hasViteIgnoreRE = /\/\*\s*@vite-ignore\s*\*\// const cleanUpRawUrlRE = /\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm const urlIsStringRE = /^(?:'.*'|".*"|`.*`)$/ +const templateLiteralRE = /^\s*`(.*)`\s*$/ + interface UrlPosition { url: string start: number @@ -277,7 +280,6 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { let needQueryInjectHelper = false let s: MagicString | undefined const str = () => s || (s = new MagicString(source)) - const importedUrls = new Set() let isPartiallySelfAccepting = false const importedBindings = enablePartialAccept ? new Map>() @@ -335,7 +337,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // normalize all imports into resolved URLs // e.g. `import 'foo'` -> `import '/@fs/.../node_modules/foo/index.js'` - if (resolved.id.startsWith(root + '/')) { + if (resolved.id.startsWith(withTrailingSlash(root))) { // in root: infer short absolute path from root url = resolved.id.slice(root.length) } else if ( @@ -410,6 +412,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { return [url, resolved.id] } + const orderedImportedUrls = new Array(imports.length) const orderedAcceptedUrls = new Array | undefined>( imports.length, ) @@ -425,12 +428,13 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { ss: expStart, se: expEnd, d: dynamicIndex, - // #2083 User may use escape path, - // so use imports[index].n to get the unescaped string - n: specifier, a: assertIndex, } = importSpecifier + // #2083 User may use escape path, + // so use imports[index].n to get the unescaped string + let specifier = importSpecifier.n + const rawUrl = source.slice(start, end) // check import.meta usage @@ -468,6 +472,14 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { hasEnv = true } return + } else if (templateLiteralRE.test(rawUrl)) { + // If the import has backticks but isn't transformed as a glob import + // (as there's nothing to glob), check if it's simply a plain string. + // If so, we can replace the specifier as a plain string to prevent + // an incorrect "cannot be analyzed" warning. + if (!(rawUrl.includes('${') && rawUrl.includes('}'))) { + specifier = rawUrl.replace(templateLiteralRE, '$1') + } } const isDynamicImport = dynamicIndex > -1 @@ -639,7 +651,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { const hmrUrl = unwrapId(stripBase(url, base)) const isLocalImport = !isExternalUrl(hmrUrl) && !isDataUrl(hmrUrl) if (isLocalImport) { - importedUrls.add(hmrUrl) + orderedImportedUrls[index] = hmrUrl } if (enablePartialAccept && importedBindings) { @@ -672,7 +684,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { config.logger.error(e.message, { error: e }) }) } - } else if (!importer.startsWith(clientDir)) { + } else if (!importer.startsWith(withTrailingSlash(clientDir))) { if (!isInNodeModules(importer)) { // check @vite-ignore which suppresses dynamic import warning const hasViteIgnore = hasViteIgnoreRE.test( @@ -717,6 +729,9 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { }), ) + const importedUrls = new Set( + orderedImportedUrls.filter(Boolean) as string[], + ) const acceptedUrls = mergeAcceptedUrls(orderedAcceptedUrls) const acceptedExports = mergeAcceptedUrls(orderedAcceptedExports) @@ -850,16 +865,17 @@ export function interopNamedImports( se: expEnd, d: dynamicIndex, } = importSpecifier + const exp = source.slice(expStart, expEnd) if (dynamicIndex > -1) { // rewrite `import('package')` to expose the default directly str.overwrite( expStart, expEnd, - `import('${rewrittenUrl}').then(m => m.default && m.default.__esModule ? m.default : ({ ...m.default, default: m.default }))`, + `import('${rewrittenUrl}').then(m => m.default && m.default.__esModule ? m.default : ({ ...m.default, default: m.default }))` + + getLineBreaks(exp), { contentOnly: true }, ) } else { - const exp = source.slice(expStart, expEnd) const rawUrl = source.slice(start, end) const rewritten = transformCjsImport( exp, @@ -870,14 +886,28 @@ export function interopNamedImports( config, ) if (rewritten) { - str.overwrite(expStart, expEnd, rewritten, { contentOnly: true }) + str.overwrite(expStart, expEnd, rewritten + getLineBreaks(exp), { + contentOnly: true, + }) } else { // #1439 export * from '...' - str.overwrite(start, end, rewrittenUrl, { contentOnly: true }) + str.overwrite( + start, + end, + rewrittenUrl + getLineBreaks(source.slice(start, end)), + { + contentOnly: true, + }, + ) } } } +// get line breaks to preserve line count for not breaking source maps +function getLineBreaks(str: string) { + return str.includes('\n') ? '\n'.repeat(str.split('\n').length - 1) : '' +} + type ImportNameSpecifier = { importedName: string; localName: string } /** diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index 1d046eb49f581e..1889790463dae8 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -16,6 +16,7 @@ import { isInNodeModules, moduleListContains, numberToPos, + withTrailingSlash, } from '../utils' import type { Plugin } from '../plugin' import { getDepOptimizationConfig } from '../config' @@ -271,7 +272,7 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { // normalize all imports into resolved URLs // e.g. `import 'foo'` -> `import '/@fs/.../node_modules/foo/index.js'` - if (resolved.id.startsWith(root + '/')) { + if (resolved.id.startsWith(withTrailingSlash(root))) { // in root: infer short absolute path from root url = resolved.id.slice(root.length) } else { diff --git a/packages/vite/src/node/plugins/importMetaGlob.ts b/packages/vite/src/node/plugins/importMetaGlob.ts index 0daf6b04358b30..60172e0de3a143 100644 --- a/packages/vite/src/node/plugins/importMetaGlob.ts +++ b/packages/vite/src/node/plugins/importMetaGlob.ts @@ -58,8 +58,10 @@ export function getAffectedGlobModules( (!affirmed.length || affirmed.some((glob) => isMatch(file, glob))) && (!negated.length || negated.every((glob) => isMatch(file, glob))), ) - ) - modules.push(...(server.moduleGraph.getModulesByFile(id) || [])) + ) { + const mod = server.moduleGraph.getModuleById(id) + if (mod) modules.push(mod) + } } modules.forEach((i) => { if (i?.file) server.moduleGraph.onFileChange(i.file) diff --git a/packages/vite/src/node/plugins/preAlias.ts b/packages/vite/src/node/plugins/preAlias.ts index 931db79d231bf7..cd86bac770d823 100644 --- a/packages/vite/src/node/plugins/preAlias.ts +++ b/packages/vite/src/node/plugins/preAlias.ts @@ -14,6 +14,7 @@ import { isInNodeModules, isOptimizable, moduleListContains, + withTrailingSlash, } from '../utils' import { getDepsOptimizer } from '../optimizer' import { tryOptimizedResolve } from './resolve' @@ -114,7 +115,7 @@ function matches(pattern: string | RegExp, importee: string) { if (importee === pattern) { return true } - return importee.startsWith(pattern + '/') + return importee.startsWith(withTrailingSlash(pattern)) } function getAliasPatterns( diff --git a/packages/vite/src/node/plugins/reporter.ts b/packages/vite/src/node/plugins/reporter.ts index cbcb0409d1e3f0..74ffe11246e8ad 100644 --- a/packages/vite/src/node/plugins/reporter.ts +++ b/packages/vite/src/node/plugins/reporter.ts @@ -4,7 +4,12 @@ import { promisify } from 'node:util' import colors from 'picocolors' import type { Plugin } from 'rollup' import type { ResolvedConfig } from '../config' -import { isDefined, isInNodeModules, normalizePath } from '../utils' +import { + isDefined, + isInNodeModules, + normalizePath, + withTrailingSlash, +} from '../utils' import { LogLevels } from '../logger' const groups = [ @@ -243,9 +248,10 @@ export function buildReporterPlugin(config: ResolvedConfig): Plugin { group.name === 'JS' && entry.size / 1000 > chunkLimit if (isLarge) hasLargeChunks = true const sizeColor = isLarge ? colors.yellow : colors.dim - let log = colors.dim(relativeOutDir + '/') + let log = colors.dim(withTrailingSlash(relativeOutDir)) log += - !config.build.lib && entry.name.startsWith(assetsDir) + !config.build.lib && + entry.name.startsWith(withTrailingSlash(assetsDir)) ? colors.dim(assetsDir) + group.color( entry.name diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index 63841d398c5256..5932b720875bef 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -36,6 +36,7 @@ import { safeRealpathSync, slash, tryStatSync, + withTrailingSlash, } from '../utils' import { optimizedDepInfoFromFile, optimizedDepInfoFromId } from '../optimizer' import type { DepsOptimizer } from '../optimizer' @@ -228,7 +229,11 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { // URL // /foo -> /fs-root/foo - if (asSrc && id[0] === '/' && (rootInRoot || !id.startsWith(root))) { + if ( + asSrc && + id[0] === '/' && + (rootInRoot || !id.startsWith(withTrailingSlash(root))) + ) { const fsPath = path.resolve(root, id.slice(1)) if ((res = tryFsResolve(fsPath, options))) { debug?.(`[url] ${colors.cyan(id)} -> ${colors.dim(res)}`) @@ -725,9 +730,11 @@ export function tryNodeResolve( ) { const mainPkg = findNearestMainPackageData(basedir, packageCache)?.data if (mainPkg) { + const pkgName = getNpmPackageName(id) if ( - mainPkg.peerDependencies?.[id] && - mainPkg.peerDependenciesMeta?.[id]?.optional + pkgName != null && + mainPkg.peerDependencies?.[pkgName] && + mainPkg.peerDependenciesMeta?.[pkgName]?.optional ) { return { id: `${optionalPeerDepId}:${id}:${mainPkg.name}`, @@ -939,7 +946,7 @@ export async function tryOptimizedResolve( } // match by src to correctly identify if id belongs to nested dependency - if (optimizedData.src.startsWith(idPkgDir)) { + if (optimizedData.src.startsWith(withTrailingSlash(idPkgDir))) { return depsOptimizer.getOptimizedDepId(optimizedData) } } diff --git a/packages/vite/src/node/plugins/worker.ts b/packages/vite/src/node/plugins/worker.ts index 202c8723f45218..ef98a4f2eddb23 100644 --- a/packages/vite/src/node/plugins/worker.ts +++ b/packages/vite/src/node/plugins/worker.ts @@ -274,9 +274,15 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin { injectEnv = module?.transformResult?.code || '' } } - return { - code: injectEnv + raw, + if (injectEnv) { + const s = new MagicString(raw) + s.prepend(injectEnv) + return { + code: s.toString(), + map: s.generateMap({ hires: 'boundary' }), + } } + return } if ( query == null || diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index e72b1ebdf05338..f708b1114ed3cd 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -5,7 +5,13 @@ import colors from 'picocolors' import type { Update } from 'types/hmrPayload' import type { RollupError } from 'rollup' import { CLIENT_DIR } from '../constants' -import { createDebugger, normalizePath, unique, wrapId } from '../utils' +import { + createDebugger, + normalizePath, + unique, + withTrailingSlash, + wrapId, +} from '../utils' import type { ViteDevServer } from '..' import { isCSSRequest } from '../plugins/css' import { getAffectedGlobModules } from '../plugins/importMetaGlob' @@ -38,7 +44,9 @@ export interface HmrContext { } export function getShortName(file: string, root: string): string { - return file.startsWith(root + '/') ? path.posix.relative(root, file) : file + return file.startsWith(withTrailingSlash(root)) + ? path.posix.relative(root, file) + : file } export async function handleHMRUpdate( @@ -81,7 +89,7 @@ export async function handleHMRUpdate( debugHmr?.(`[file change] ${colors.dim(shortFile)}`) // (dev only) the client itself cannot be hot updated. - if (file.startsWith(normalizedClientDir)) { + if (file.startsWith(withTrailingSlash(normalizedClientDir))) { ws.send({ type: 'full-reload', path: '*', diff --git a/packages/vite/src/node/server/middlewares/base.ts b/packages/vite/src/node/server/middlewares/base.ts index cba4582486f8a9..c6af2302aef3d9 100644 --- a/packages/vite/src/node/server/middlewares/base.ts +++ b/packages/vite/src/node/server/middlewares/base.ts @@ -1,6 +1,6 @@ import type { Connect } from 'dep-types/connect' import type { ViteDevServer } from '..' -import { joinUrlSegments, stripBase } from '../../utils' +import { joinUrlSegments, stripBase, withTrailingSlash } from '../../utils' // this middleware is only active when (base !== '/') @@ -36,7 +36,7 @@ export function baseMiddleware({ } else if (req.headers.accept?.includes('text/html')) { // non-based page visit const redirectPath = - url + '/' !== base ? joinUrlSegments(base, url) : base + withTrailingSlash(url) !== base ? joinUrlSegments(base, url) : base res.writeHead(404, { 'Content-Type': 'text/html', }) diff --git a/packages/vite/src/node/server/middlewares/static.ts b/packages/vite/src/node/server/middlewares/static.ts index d1e7073179415b..8acc9e681b4e37 100644 --- a/packages/vite/src/node/server/middlewares/static.ts +++ b/packages/vite/src/node/server/middlewares/static.ts @@ -19,6 +19,7 @@ import { removeLeadingSlash, shouldServeFile, slash, + withTrailingSlash, } from '../../utils' const knownJavascriptExtensionRE = /\.[tj]sx?$/ @@ -118,7 +119,7 @@ export function serveStaticMiddleware( } if (redirectedPathname) { // dir is pre-normalized to posix style - if (redirectedPathname.startsWith(dir)) { + if (redirectedPathname.startsWith(withTrailingSlash(dir))) { redirectedPathname = redirectedPathname.slice(dir.length) } } @@ -129,7 +130,7 @@ export function serveStaticMiddleware( resolvedPathname[resolvedPathname.length - 1] === '/' && fileUrl[fileUrl.length - 1] !== '/' ) { - fileUrl = fileUrl + '/' + fileUrl = withTrailingSlash(fileUrl) } if (!ensureServingAccess(fileUrl, server, res, next)) { return diff --git a/packages/vite/src/node/server/middlewares/transform.ts b/packages/vite/src/node/server/middlewares/transform.ts index 737fb92f7d0fbc..f85b12a46202f4 100644 --- a/packages/vite/src/node/server/middlewares/transform.ts +++ b/packages/vite/src/node/server/middlewares/transform.ts @@ -16,6 +16,7 @@ import { removeImportQuery, removeTimestampQuery, unwrapId, + withTrailingSlash, } from '../../utils' import { send } from '../send' import { ERR_LOAD_URL, transformRequest } from '../transformRequest' @@ -129,10 +130,10 @@ export function transformMiddleware( // check if public dir is inside root dir const publicDir = normalizePath(server.config.publicDir) const rootDir = normalizePath(server.config.root) - if (publicDir.startsWith(rootDir)) { + if (publicDir.startsWith(withTrailingSlash(rootDir))) { const publicPath = `${publicDir.slice(rootDir.length)}/` // warn explicit public paths - if (url.startsWith(publicPath)) { + if (url.startsWith(withTrailingSlash(publicPath))) { let warning: string if (isImportRequest(url)) { diff --git a/packages/vite/src/node/server/searchRoot.ts b/packages/vite/src/node/server/searchRoot.ts index fb8c9c4cfc215a..edb7a76946266e 100644 --- a/packages/vite/src/node/server/searchRoot.ts +++ b/packages/vite/src/node/server/searchRoot.ts @@ -27,8 +27,12 @@ function hasWorkspacePackageJSON(root: string): boolean { if (!isFileReadable(path)) { return false } - const content = JSON.parse(fs.readFileSync(path, 'utf-8')) || {} - return !!content.workspaces + try { + const content = JSON.parse(fs.readFileSync(path, 'utf-8')) || {} + return !!content.workspaces + } catch { + return false + } } function hasRootFile(root: string): boolean { diff --git a/packages/vite/src/node/server/ws.ts b/packages/vite/src/node/server/ws.ts index 4a298cac1eebd2..7df59370f9e1d8 100644 --- a/packages/vite/src/node/server/ws.ts +++ b/packages/vite/src/node/server/ws.ts @@ -1,9 +1,10 @@ import path from 'node:path' -import type { Server } from 'node:http' +import type { IncomingMessage, Server } from 'node:http' import { STATUS_CODES, createServer as createHttpServer } from 'node:http' import type { ServerOptions as HttpsServerOptions } from 'node:https' import { createServer as createHttpsServer } from 'node:https' import type { Socket } from 'node:net' +import type { Duplex } from 'node:stream' import colors from 'picocolors' import type { WebSocket as WebSocketRaw } from 'ws' import { WebSocketServer as WebSocketServerRaw_ } from 'ws' @@ -104,6 +105,11 @@ export function createWebSocketServer( // TODO: the main server port may not have been chosen yet as it may use the next available const portsAreCompatible = !hmrPort || hmrPort === config.server.port const wsServer = hmrServer || (portsAreCompatible && server) + let hmrServerWsListener: ( + req: InstanceType, + socket: Duplex, + head: Buffer, + ) => void const customListeners = new Map>>() const clientsMap = new WeakMap() const port = hmrPort || 24678 @@ -116,7 +122,7 @@ export function createWebSocketServer( hmrBase = path.posix.join(hmrBase, hmrPath) } wss = new WebSocketServerRaw({ noServer: true }) - wsServer.on('upgrade', (req, socket, head) => { + hmrServerWsListener = (req, socket, head) => { if ( req.headers['sec-websocket-protocol'] === HMR_HEADER && req.url === hmrBase @@ -125,7 +131,8 @@ export function createWebSocketServer( wss.emit('connection', ws, req) }) } - }) + } + wsServer.on('upgrade', hmrServerWsListener) } else { // http server request handler keeps the same with // https://github.com/websockets/ws/blob/45e17acea791d865df6b255a55182e9c42e5877a/lib/websocket-server.js#L88-L96 @@ -273,6 +280,11 @@ export function createWebSocketServer( }, close() { + // should remove listener if hmr.server is set + // otherwise the old listener swallows all WebSocket connections + if (hmrServerWsListener && wsServer) { + wsServer.off('upgrade', hmrServerWsListener) + } return new Promise((resolve, reject) => { wss.clients.forEach((client) => { client.terminate() diff --git a/packages/vite/src/node/ssr/ssrExternal.ts b/packages/vite/src/node/ssr/ssrExternal.ts index 9a5bc28b39b9ee..99001f3614f330 100644 --- a/packages/vite/src/node/ssr/ssrExternal.ts +++ b/packages/vite/src/node/ssr/ssrExternal.ts @@ -13,6 +13,7 @@ import { isInNodeModules, lookupFile, normalizePath, + withTrailingSlash, } from '../utils' import type { Logger, ResolvedConfig } from '..' import { resolvePackageData } from '../packages' @@ -340,7 +341,10 @@ export function cjsShouldExternalizeForSSR( } // deep imports, check ext before externalizing - only externalize // extension-less imports and explicit .js imports - if (id.startsWith(e + '/') && (!path.extname(id) || id.endsWith('.js'))) { + if ( + id.startsWith(withTrailingSlash(e)) && + (!path.extname(id) || id.endsWith('.js')) + ) { return true } }) diff --git a/packages/vite/src/node/ssr/ssrModuleLoader.ts b/packages/vite/src/node/ssr/ssrModuleLoader.ts index 3e498809f32829..129d91b5f0a428 100644 --- a/packages/vite/src/node/ssr/ssrModuleLoader.ts +++ b/packages/vite/src/node/ssr/ssrModuleLoader.ts @@ -268,7 +268,7 @@ async function nodeImport( resolveOptions: InternalResolveOptionsWithOverrideConditions, ) { let url: string - if (id.startsWith('node:') || id.startsWith('data:') || isBuiltin(id)) { + if (id.startsWith('data:') || isBuiltin(id)) { url = id } else { const resolved = tryNodeResolve( diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 27a619947c0b0d..7b8912835d1f79 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -88,6 +88,12 @@ export const flattenId = (id: string): string => export const normalizeId = (id: string): string => id.replace(replaceNestedIdRE, ' > ') +// Supported by Node, Deno, Bun +const NODE_BUILTIN_NAMESPACE = 'node:' +// Supported by Deno +const NPM_BUILTIN_NAMESPACE = 'npm:' +// Supported by Bun +const BUN_BUILTIN_NAMESPACE = 'bun:' //TODO: revisit later to see if the edge case that "compiling using node v12 code to be run in node v16 in the server" is what we intend to support. const builtins = new Set([ ...builtinModules, @@ -105,14 +111,19 @@ const builtins = new Set([ 'util/types', 'wasi', ]) +// Some runtimes like Bun injects namespaced modules here, which is not a node builtin +const nodeBuiltins = [...builtins].filter((id) => !id.includes(':')) -const NODE_BUILTIN_NAMESPACE = 'node:' +// TODO: Use `isBuiltin` from `node:module`, but Deno doesn't support it export function isBuiltin(id: string): boolean { - return builtins.has( - id.startsWith(NODE_BUILTIN_NAMESPACE) - ? id.slice(NODE_BUILTIN_NAMESPACE.length) - : id, - ) + if (process.versions.deno && id.startsWith(NPM_BUILTIN_NAMESPACE)) return true + if (process.versions.bun && id.startsWith(BUN_BUILTIN_NAMESPACE)) return true + return isNodeBuiltin(id) +} + +export function isNodeBuiltin(id: string): boolean { + if (id.startsWith(NODE_BUILTIN_NAMESPACE)) return true + return nodeBuiltins.includes(id) } export function isInNodeModules(id: string): boolean { @@ -123,7 +134,9 @@ export function moduleListContains( moduleList: string[] | undefined, id: string, ): boolean | undefined { - return moduleList?.some((m) => m === id || id.startsWith(m + '/')) + return moduleList?.some( + (m) => m === id || id.startsWith(withTrailingSlash(m)), + ) } export function isOptimizable( @@ -221,6 +234,13 @@ export function fsPathFromUrl(url: string): string { return fsPathFromId(cleanUrl(url)) } +export function withTrailingSlash(path: string): string { + if (path[path.length - 1] !== '/') { + return `${path}/` + } + return path +} + /** * Check if dir is a parent of file * @@ -231,9 +251,7 @@ export function fsPathFromUrl(url: string): string { * @returns true if dir is a parent of file */ export function isParentDirectory(dir: string, file: string): boolean { - if (dir[dir.length - 1] !== '/') { - dir = `${dir}/` - } + dir = withTrailingSlash(dir) return ( file.startsWith(dir) || (isCaseInsensitiveFS && file.toLowerCase().startsWith(dir.toLowerCase())) @@ -644,7 +662,7 @@ export function ensureWatchedFile( if ( file && // only need to watch if out of root - !file.startsWith(root + '/') && + !file.startsWith(withTrailingSlash(root)) && // some rollup plugins use null bytes for private resolved Ids !file.includes('\0') && fs.existsSync(file) @@ -1222,7 +1240,7 @@ export function stripBase(path: string, base: string): string { if (path === base) { return '/' } - const devBase = base[base.length - 1] === '/' ? base : base + '/' + const devBase = withTrailingSlash(base) return path.startsWith(devBase) ? path.slice(devBase.length - 1) : path } diff --git a/playground/css-sourcemap/__tests__/lib-entry/lib-entry.spec.ts b/playground/css-sourcemap/__tests__/lib-entry/lib-entry.spec.ts new file mode 100644 index 00000000000000..d2fa5f7d2c31e7 --- /dev/null +++ b/playground/css-sourcemap/__tests__/lib-entry/lib-entry.spec.ts @@ -0,0 +1,8 @@ +import { describe, expect, test } from 'vitest' +import { findAssetFile, isBuild } from '~utils' + +describe.runIf(isBuild)('css lib entry', () => { + test('remove useless js sourcemap', async () => { + expect(findAssetFile('linked.js.map', 'lib-entry', './')).toBe('') + }) +}) diff --git a/playground/css-sourcemap/index.js b/playground/css-sourcemap/index.js new file mode 100644 index 00000000000000..c05f09558ddaf0 --- /dev/null +++ b/playground/css-sourcemap/index.js @@ -0,0 +1 @@ +export default 'hello' diff --git a/playground/css-sourcemap/vite.config-lib-entry.js b/playground/css-sourcemap/vite.config-lib-entry.js new file mode 100644 index 00000000000000..600b7414a48b75 --- /dev/null +++ b/playground/css-sourcemap/vite.config-lib-entry.js @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite' + +export default defineConfig({ + build: { + cssCodeSplit: true, + sourcemap: true, + outDir: 'dist/lib-entry', + lib: { + entry: ['./index.js', './linked.css'], + formats: ['es'], + }, + }, +}) diff --git a/playground/dynamic-import/index.html b/playground/dynamic-import/index.html index 31cf128ddd12d1..afc5c03f8b862a 100644 --- a/playground/dynamic-import/index.html +++ b/playground/dynamic-import/index.html @@ -35,6 +35,8 @@
+
+
diff --git a/playground/dynamic-import/nested/index.js b/playground/dynamic-import/nested/index.js index 180dd6b5ad3b6b..25a6426e3f7ee1 100644 --- a/playground/dynamic-import/nested/index.js +++ b/playground/dynamic-import/nested/index.js @@ -131,4 +131,8 @@ import(`../nested/nested/${base}.js`).then((mod) => { text('.dynamic-import-nested-self', mod.self) }) +import(`../nested/static.js`).then((mod) => { + text('.dynamic-import-static', mod.self) +}) + console.log('index.js') diff --git a/playground/dynamic-import/nested/static.js b/playground/dynamic-import/nested/static.js new file mode 100644 index 00000000000000..02dd476388a6e4 --- /dev/null +++ b/playground/dynamic-import/nested/static.js @@ -0,0 +1 @@ +export const self = 'dynamic-import-static' diff --git a/playground/glob-import/__tests__/glob-import.spec.ts b/playground/glob-import/__tests__/glob-import.spec.ts index 931edfd99138de..8e73892219e192 100644 --- a/playground/glob-import/__tests__/glob-import.spec.ts +++ b/playground/glob-import/__tests__/glob-import.spec.ts @@ -11,6 +11,7 @@ import { page, removeFile, untilBrowserLogAfter, + untilUpdated, viteTestUrl, withRetry, } from '~utils' @@ -131,6 +132,12 @@ test('unassigned import processes', async () => { ) }) +test('import glob in package', async () => { + expect(await page.textContent('.in-package')).toBe( + JSON.stringify(['/pkg-pages/foo.js']), + ) +}) + if (!isBuild) { test('hmr for adding/removing files', async () => { const resultElement = page.locator('.result') @@ -190,6 +197,22 @@ if (!isBuild) { response = await request.catch(() => ({ status: () => -1 })) expect(response.status()).toBe(-1) }) + + test('hmr for adding/removing files in package', async () => { + const resultElement = page.locator('.in-package') + + addFile('pkg-pages/bar.js', '// empty') + await untilUpdated( + () => resultElement.textContent(), + JSON.stringify(['/pkg-pages/foo.js', '/pkg-pages/bar.js'].sort()), + ) + + removeFile('pkg-pages/bar.js') + await untilUpdated( + () => resultElement.textContent(), + JSON.stringify(['/pkg-pages/foo.js']), + ) + }) } test('tree-shake eager css', async () => { diff --git a/playground/glob-import/import-meta-glob-pkg/index.js b/playground/glob-import/import-meta-glob-pkg/index.js new file mode 100644 index 00000000000000..44705cf18f9f22 --- /dev/null +++ b/playground/glob-import/import-meta-glob-pkg/index.js @@ -0,0 +1,4 @@ +export const g = import.meta.glob('/pkg-pages/*.js') +document.querySelector('.in-package').textContent = JSON.stringify( + Object.keys(g).sort(), +) diff --git a/playground/glob-import/import-meta-glob-pkg/package.json b/playground/glob-import/import-meta-glob-pkg/package.json new file mode 100644 index 00000000000000..7138de851543cf --- /dev/null +++ b/playground/glob-import/import-meta-glob-pkg/package.json @@ -0,0 +1,5 @@ +{ + "name": "@vitejs/test-import-meta-glob-pkg", + "type": "module", + "main": "./index.js" +} diff --git a/playground/glob-import/index.html b/playground/glob-import/index.html index b726965ff62067..3b72430668e14d 100644 --- a/playground/glob-import/index.html +++ b/playground/glob-import/index.html @@ -23,6 +23,8 @@

Escape alias glob


 

Sub imports


+

In package

+

 
 
 
 
+
+
 
diff --git a/playground/glob-import/package.json b/playground/glob-import/package.json
index 7d9104ab431c28..d71d01109270f1 100644
--- a/playground/glob-import/package.json
+++ b/playground/glob-import/package.json
@@ -11,5 +11,8 @@
     "build": "vite build",
     "debug": "node --inspect-brk ../../packages/vite/bin/vite",
     "preview": "vite preview"
+  },
+  "dependencies": {
+    "@vitejs/test-import-meta-glob-pkg": "file:./import-meta-glob-pkg"
   }
 }
diff --git a/playground/glob-import/pkg-pages/foo.js b/playground/glob-import/pkg-pages/foo.js
new file mode 100644
index 00000000000000..8b1a393741c96c
--- /dev/null
+++ b/playground/glob-import/pkg-pages/foo.js
@@ -0,0 +1 @@
+// empty
diff --git a/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts b/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts
index ce67f19e904521..2b8044f40fa32e 100644
--- a/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts
+++ b/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts
@@ -36,6 +36,32 @@ if (!isBuild) {
     `)
   })
 
+  test('multiline import', async () => {
+    const res = await page.request.get(
+      new URL('./with-multiline-import.ts', page.url()).href,
+    )
+    const multi = await res.text()
+    const map = extractSourcemap(multi)
+    expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(`
+      {
+        "mappings": "AACA;AAAA,EACE;AAAA,OACK;AAEP,QAAQ,IAAI,yBAAyB,GAAG;",
+        "sources": [
+          "with-multiline-import.ts",
+        ],
+        "sourcesContent": [
+          "// prettier-ignore
+      import {
+        foo
+      } from '@vitejs/test-importee-pkg'
+
+      console.log('with-multiline-import', foo)
+      ",
+        ],
+        "version": 3,
+      }
+    `)
+  })
+
   test('should not output missing source file warning', () => {
     serverLogs.forEach((log) => {
       expect(log).not.toMatch(/Sourcemap for .+ points to missing source files/)
diff --git a/playground/js-sourcemap/importee-pkg/index.js b/playground/js-sourcemap/importee-pkg/index.js
new file mode 100644
index 00000000000000..a96b15202fba44
--- /dev/null
+++ b/playground/js-sourcemap/importee-pkg/index.js
@@ -0,0 +1,2 @@
+// eslint-disable-next-line import/no-commonjs
+exports.foo = 'foo'
diff --git a/playground/js-sourcemap/importee-pkg/package.json b/playground/js-sourcemap/importee-pkg/package.json
new file mode 100644
index 00000000000000..2bc76d5bb50b39
--- /dev/null
+++ b/playground/js-sourcemap/importee-pkg/package.json
@@ -0,0 +1,6 @@
+{
+  "name": "@vitejs/test-importee-pkg",
+  "private": true,
+  "version": "0.0.0",
+  "main": "./index.js"
+}
diff --git a/playground/js-sourcemap/index.html b/playground/js-sourcemap/index.html
index bda409a5cc9693..f669bf4fc102aa 100644
--- a/playground/js-sourcemap/index.html
+++ b/playground/js-sourcemap/index.html
@@ -6,3 +6,4 @@ 

JS Sourcemap

+ diff --git a/playground/js-sourcemap/package.json b/playground/js-sourcemap/package.json index 6381b13c9b09e9..b002697756a24c 100644 --- a/playground/js-sourcemap/package.json +++ b/playground/js-sourcemap/package.json @@ -8,5 +8,8 @@ "build": "vite build", "debug": "node --inspect-brk ../../packages/vite/bin/vite", "preview": "vite preview" + }, + "dependencies": { + "@vitejs/test-importee-pkg": "file:importee-pkg" } } diff --git a/playground/js-sourcemap/with-multiline-import.ts b/playground/js-sourcemap/with-multiline-import.ts new file mode 100644 index 00000000000000..5bf8aa53214384 --- /dev/null +++ b/playground/js-sourcemap/with-multiline-import.ts @@ -0,0 +1,6 @@ +// prettier-ignore +import { + foo +} from '@vitejs/test-importee-pkg' + +console.log('with-multiline-import', foo) diff --git a/playground/lib/__tests__/lib.spec.ts b/playground/lib/__tests__/lib.spec.ts index b203535e0154da..8232a2fd100649 100644 --- a/playground/lib/__tests__/lib.spec.ts +++ b/playground/lib/__tests__/lib.spec.ts @@ -33,7 +33,9 @@ describe.runIf(isBuild)('build', () => { 'dist/nominify/my-lib-custom-filename.iife.js', ) // esbuild helpers are injected inside of the IIFE wrapper - expect(code).toMatch(/^var MyLib=function\(\)\{"use strict";/) + // esbuild has a bug that injects some statements before `"use strict"`: https://github.com/evanw/esbuild/issues/3322 + // remove the `.*?` part once it's fixed + expect(code).toMatch(/^var MyLib=function\(\)\{.*?"use strict";/) expect(noMinifyCode).toMatch( /^var MyLib\s*=\s*function\(\)\s*\{.*?"use strict";/s, ) diff --git a/playground/lib/__tests__/serve.ts b/playground/lib/__tests__/serve.ts index 44e1728ffd6086..e07efe526eaf00 100644 --- a/playground/lib/__tests__/serve.ts +++ b/playground/lib/__tests__/serve.ts @@ -61,6 +61,12 @@ export async function serve(): Promise<{ close(): Promise }> { configFile: path.resolve(__dirname, '../vite.dyimport.config.js'), }) + await build({ + root: rootDir, + logLevel: 'warn', // output esbuild warns + configFile: path.resolve(__dirname, '../vite.multiple-output.config.js'), + }) + await build({ root: rootDir, logLevel: 'warn', // output esbuild warns diff --git a/playground/lib/src/main-multiple-output.js b/playground/lib/src/main-multiple-output.js new file mode 100644 index 00000000000000..78e22283ccbc1b --- /dev/null +++ b/playground/lib/src/main-multiple-output.js @@ -0,0 +1,6 @@ +// import file to test css build handling +import './index.css' + +export default async function message(sel) { + document.querySelector(sel).textContent = 'success' +} diff --git a/playground/lib/src/main.js b/playground/lib/src/main.js index 59c8e897cb0789..8be8ec37e635ee 100644 --- a/playground/lib/src/main.js +++ b/playground/lib/src/main.js @@ -10,3 +10,6 @@ export default function myLib(sel) { // make sure umd helper has been moved to the right position console.log(`amd function(){ "use strict"; }`) } + +// For triggering unhandled global esbuild helpers in previous regex-based implementation for injection +;(function () {})()?.foo diff --git a/playground/lib/src/sub-multiple-output.js b/playground/lib/src/sub-multiple-output.js new file mode 100644 index 00000000000000..78e22283ccbc1b --- /dev/null +++ b/playground/lib/src/sub-multiple-output.js @@ -0,0 +1,6 @@ +// import file to test css build handling +import './index.css' + +export default async function message(sel) { + document.querySelector(sel).textContent = 'success' +} diff --git a/playground/lib/vite.config.js b/playground/lib/vite.config.js index 6b4395624dc27a..84612ba1f65306 100644 --- a/playground/lib/vite.config.js +++ b/playground/lib/vite.config.js @@ -7,6 +7,7 @@ export default defineConfig({ supported: { // Force esbuild inject helpers to test regex 'object-rest-spread': false, + 'optional-chain': false, }, }, build: { diff --git a/playground/lib/vite.multiple-output.config.js b/playground/lib/vite.multiple-output.config.js new file mode 100644 index 00000000000000..e986221ad6ca9b --- /dev/null +++ b/playground/lib/vite.multiple-output.config.js @@ -0,0 +1,39 @@ +import path from 'node:path' +import { defineConfig } from 'vite' + +const root = process.env.VITEST + ? path.resolve(__dirname, '../../playground-temp/lib') + : __dirname + +export default defineConfig({ + build: { + lib: { + // set multiple entrypoint to trigger css chunking + entry: { + main: path.resolve(__dirname, 'src/main-multiple-output.js'), + sub: path.resolve(__dirname, 'src/sub-multiple-output.js'), + }, + name: 'MyLib', + }, + outDir: 'dist/multiple-output', + rollupOptions: { + // due to playground-temp, the `dir` needs to be relative to the resolvedRoot + output: [ + { + dir: path.resolve(root, 'dist/multiple-output/es'), + format: 'es', + entryFileNames: 'index.mjs', + assetFileNames: 'assets/mylib.css', + }, + { + dir: path.resolve(root, 'dist/multiple-output/cjs'), + format: 'cjs', + entryFileNames: 'index.cjs', + assetFileNames: 'assets/mylib.css', + }, + ], + }, + cssCodeSplit: true, + }, + cacheDir: 'node_modules/.vite-multiple-output', +}) diff --git a/playground/module-graph/__tests__/module-graph.spec.ts b/playground/module-graph/__tests__/module-graph.spec.ts new file mode 100644 index 00000000000000..bfabd53f289724 --- /dev/null +++ b/playground/module-graph/__tests__/module-graph.spec.ts @@ -0,0 +1,12 @@ +import { expect, test } from 'vitest' +import { isServe, page, viteServer } from '~utils' + +test.runIf(isServe)('importedUrls order is preserved', async () => { + const el = page.locator('.imported-urls-order') + expect(await el.textContent()).toBe('[success]') + const mod = await viteServer.moduleGraph.getModuleByUrl( + '/imported-urls-order.js', + ) + const importedModuleIds = [...mod.importedModules].map((m) => m.url) + expect(importedModuleIds).toEqual(['\x00virtual:slow-module', '/empty.js']) +}) diff --git a/playground/module-graph/empty.js b/playground/module-graph/empty.js new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/playground/module-graph/imported-urls-order.js b/playground/module-graph/imported-urls-order.js new file mode 100644 index 00000000000000..9ccf4527e6665d --- /dev/null +++ b/playground/module-graph/imported-urls-order.js @@ -0,0 +1,7 @@ +import { msg } from 'virtual:slow-module' +import './empty.js' + +export default msg + +// This module tests that the import order is preserved in this module's `importedUrls` property +// as the imports can be processed in parallel diff --git a/playground/module-graph/index.html b/playground/module-graph/index.html new file mode 100644 index 00000000000000..663a7d7ed0066a --- /dev/null +++ b/playground/module-graph/index.html @@ -0,0 +1,10 @@ +
+ + diff --git a/playground/module-graph/package.json b/playground/module-graph/package.json new file mode 100644 index 00000000000000..35e0799c262738 --- /dev/null +++ b/playground/module-graph/package.json @@ -0,0 +1,12 @@ +{ + "name": "@vitejs/test-hmr", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "debug": "node --inspect-brk ../../packages/vite/bin/vite", + "preview": "vite preview" + } +} diff --git a/playground/module-graph/vite.config.ts b/playground/module-graph/vite.config.ts new file mode 100644 index 00000000000000..53e07ff3bfd483 --- /dev/null +++ b/playground/module-graph/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vite' +import type { Plugin } from 'vite' + +export default defineConfig({ + plugins: [slowModulePlugin()], +}) + +function slowModulePlugin(): Plugin { + return { + name: 'slow-module', + resolveId(id) { + if (id === 'virtual:slow-module') { + return '\0virtual:slow-module' + } + }, + async load(id) { + if (id === '\0virtual:slow-module') { + await new Promise((resolve) => setTimeout(resolve, 500)) + return `export const msg = '[success]'` + } + }, + } +} diff --git a/playground/optimize-deps/__tests__/optimize-deps.spec.ts b/playground/optimize-deps/__tests__/optimize-deps.spec.ts index 1e54aa1bd700b5..a42e8838288e9e 100644 --- a/playground/optimize-deps/__tests__/optimize-deps.spec.ts +++ b/playground/optimize-deps/__tests__/optimize-deps.spec.ts @@ -108,6 +108,19 @@ test('dep with optional peer dep', async () => { } }) +test('dep with optional peer dep submodule', async () => { + expect( + await page.textContent('.dep-with-optional-peer-dep-submodule'), + ).toMatch(`[success]`) + if (isServe) { + expect(browserErrors.map((error) => error.message)).toEqual( + expect.arrayContaining([ + 'Could not resolve "foobar/baz" imported by "@vitejs/test-dep-with-optional-peer-dep-submodule". Is it installed?', + ]), + ) + } +}) + test('dep with css import', async () => { expect(await getColor('.dep-linked-include')).toBe('red') }) diff --git a/playground/optimize-deps/dep-with-optional-peer-dep-submodule/index.js b/playground/optimize-deps/dep-with-optional-peer-dep-submodule/index.js new file mode 100644 index 00000000000000..d2ace777ebf930 --- /dev/null +++ b/playground/optimize-deps/dep-with-optional-peer-dep-submodule/index.js @@ -0,0 +1,7 @@ +export function callItself() { + return '[success]' +} + +export async function callPeerDepSubmodule() { + return await import('foobar/baz') +} diff --git a/playground/optimize-deps/dep-with-optional-peer-dep-submodule/package.json b/playground/optimize-deps/dep-with-optional-peer-dep-submodule/package.json new file mode 100644 index 00000000000000..82dcdff5dea262 --- /dev/null +++ b/playground/optimize-deps/dep-with-optional-peer-dep-submodule/package.json @@ -0,0 +1,15 @@ +{ + "name": "@vitejs/test-dep-with-optional-peer-dep-submodule", + "private": true, + "version": "0.0.0", + "main": "index.js", + "type": "module", + "peerDependencies": { + "foobar": "0.0.0" + }, + "peerDependenciesMeta": { + "foobar": { + "optional": true + } + } +} diff --git a/playground/optimize-deps/index.html b/playground/optimize-deps/index.html index 6cf34f1f4c3469..af31bcff8a4d29 100644 --- a/playground/optimize-deps/index.html +++ b/playground/optimize-deps/index.html @@ -65,6 +65,9 @@

Import from dependency with dynamic import

Import from dependency with optional peer dep

+

Import from dependency with optional peer dep submodule

+
+

Externalize known non-js files in optimize included dep

@@ -205,6 +208,16 @@

Pre bundle css modules require

callPeerDep() + +