From 188f1881eb9c280eaf34103253f1e6fea20bc274 Mon Sep 17 00:00:00 2001 From: patak Date: Mon, 4 Jul 2022 10:43:11 +0200 Subject: [PATCH] feat: improved cold start using deps scanner (#8869) --- docs/guide/migration.md | 5 - packages/vite/src/node/config.ts | 8 - packages/vite/src/node/optimizer/index.ts | 37 +- packages/vite/src/node/optimizer/optimizer.ts | 429 +++++++++++------ .../vite/src/node/plugins/importAnalysis.ts | 5 +- packages/vite/src/node/plugins/index.ts | 2 +- packages/vite/src/node/plugins/preAlias.ts | 40 +- packages/vite/src/node/plugins/resolve.ts | 4 + packages/vite/src/node/server/index.ts | 48 +- .../vite/src/node/server/transformRequest.ts | 5 +- .../ssr/__tests__/ssrModuleLoader.spec.ts | 32 -- playground/alias/index.html | 4 + playground/alias/package.json | 3 +- playground/alias/vite.config.js | 5 + .../css-sourcemap/__tests__/build.spec.ts | 7 - .../__tests__/css-sourcemap.spec.ts | 443 ++++++++++++++++++ .../css-sourcemap/__tests__/serve.spec.ts | 219 --------- .../postcss-plugins-different-dir.spec.ts | 2 +- .../postcss-plugins-different-dir/serve.ts | 10 + playground/css/postcss-caching/serve.ts | 10 + .../__tests__/dynamic-import.spec.ts | 6 +- playground/dynamic-import/{ => files}/mxd.js | 0 .../dynamic-import/{ => files}/mxd.json | 0 playground/dynamic-import/nested/index.js | 12 +- playground/dynamic-import/{ => views}/qux.js | 0 playground/dynamic-import/vite.config.js | 14 +- .../js-sourcemap/__tests__/build.spec.ts | 7 - .../{serve.spec.ts => js-sourcemap.spec.ts} | 6 + playground/preload/dep-a/index.js | 1 + playground/preload/dep-a/package.json | 7 + playground/preload/dep-including-a/index.js | 3 + .../preload/dep-including-a/package.json | 10 + playground/preload/index.html | 2 + playground/preload/package.json | 4 +- playground/preload/src/components/About.vue | 8 +- .../ssr-deps/__tests__/ssr-deps.spec.ts | 14 + playground/ssr-deps/nested-external/index.js | 9 + .../ssr-deps/nested-external/package.json | 7 + .../index.js | 3 + .../package.json | 10 + .../index.js | 5 + .../package.json | 8 + .../optimized-with-nested-external/index.js | 5 + .../package.json | 10 + playground/ssr-deps/package.json | 5 +- playground/ssr-deps/server.js | 12 +- playground/ssr-deps/src/app.js | 15 + .../__tests__/fixtures/ssrModuleLoader-bad.js | 0 playground/ssr-vue/__tests__/serve.ts | 4 + playground/ssr-vue/__tests__/ssr-vue.spec.ts | 34 +- .../__tests__/serve.spec.ts | 7 - ...ild.spec.ts => tailwind-sourcemap.spec.ts} | 6 + .../tailwind-sourcemap/postcss.config.js | 2 + playground/vitestSetup.ts | 7 +- ...pec.ts.snap => vue-sourcemap.spec.ts.snap} | 24 +- .../vue-sourcemap/__tests__/build.spec.ts | 7 - .../{serve.spec.ts => vue-sourcemap.spec.ts} | 42 +- pnpm-lock.yaml | 77 +++ 58 files changed, 1158 insertions(+), 553 deletions(-) delete mode 100644 packages/vite/src/node/ssr/__tests__/ssrModuleLoader.spec.ts delete mode 100644 playground/css-sourcemap/__tests__/build.spec.ts create mode 100644 playground/css-sourcemap/__tests__/css-sourcemap.spec.ts delete mode 100644 playground/css-sourcemap/__tests__/serve.spec.ts rename playground/css/__tests__/{ => postcss-plugins-different-dir}/postcss-plugins-different-dir.spec.ts (93%) create mode 100644 playground/css/__tests__/postcss-plugins-different-dir/serve.ts create mode 100644 playground/css/postcss-caching/serve.ts rename playground/dynamic-import/{ => files}/mxd.js (100%) rename playground/dynamic-import/{ => files}/mxd.json (100%) rename playground/dynamic-import/{ => views}/qux.js (100%) delete mode 100644 playground/js-sourcemap/__tests__/build.spec.ts rename playground/js-sourcemap/__tests__/{serve.spec.ts => js-sourcemap.spec.ts} (86%) create mode 100644 playground/preload/dep-a/index.js create mode 100644 playground/preload/dep-a/package.json create mode 100644 playground/preload/dep-including-a/index.js create mode 100644 playground/preload/dep-including-a/package.json create mode 100644 playground/ssr-deps/nested-external/index.js create mode 100644 playground/ssr-deps/nested-external/package.json create mode 100644 playground/ssr-deps/non-optimized-with-nested-external/index.js create mode 100644 playground/ssr-deps/non-optimized-with-nested-external/package.json create mode 100644 playground/ssr-deps/optimized-cjs-with-nested-external/index.js create mode 100644 playground/ssr-deps/optimized-cjs-with-nested-external/package.json create mode 100644 playground/ssr-deps/optimized-with-nested-external/index.js create mode 100644 playground/ssr-deps/optimized-with-nested-external/package.json rename {packages/vite/src/node/ssr => playground/ssr-vue}/__tests__/fixtures/ssrModuleLoader-bad.js (100%) delete mode 100644 playground/tailwind-sourcemap/__tests__/serve.spec.ts rename playground/tailwind-sourcemap/__tests__/{build.spec.ts => tailwind-sourcemap.spec.ts} (53%) rename playground/vue-sourcemap/__tests__/__snapshots__/{serve.spec.ts.snap => vue-sourcemap.spec.ts.snap} (85%) delete mode 100644 playground/vue-sourcemap/__tests__/build.spec.ts rename playground/vue-sourcemap/__tests__/{serve.spec.ts => vue-sourcemap.spec.ts} (66%) diff --git a/docs/guide/migration.md b/docs/guide/migration.md index c8bd2778b2526f..acf6460f93cf9a 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -34,7 +34,6 @@ This section describes the biggest architecture changes in Vite v3. To allow pro :::warning These options are marked as experimental and deprecated. They may be removed in a future v3 minor without respecting semver. Please pin the Vite version when using them. -- `legacy.devDepsScanner` - `legacy.buildRollupPluginCommonjs` - `legacy.buildSsrCjsExternalHeuristics` @@ -46,10 +45,6 @@ Vite's default dev server port is now 5173. You can use [`server.port`](../confi Vite's default dev server host is now `localhost`. You can use [`server.host`](../config/server-options.md#server-host) to set it to `127.0.0.1`. -Vite optimizes dependencies with esbuild to both convert CJS-only deps to ESM and to reduce the number of modules the browser needs to request. In v3, the default strategy to discover and batch dependencies has changed. Vite no longer pre-scans user code with esbuild to get an initial list of dependencies on cold start. Instead, it delays the first dependency optimization run until every imported user module on load is processed. - -To get back the v2 strategy, you can use `legacy.devDepsScanner`. - ### Build Changes In v3, Vite uses esbuild to optimize dependencies by default. Doing so, it removes one of the most significant differences between dev and prod present in v2. Because esbuild converts CJS-only dependencies to ESM, [`@rollupjs/plugin-commonjs`](https://github.com/rollup/plugins/tree/master/packages/commonjs) is no longer used. diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 7325a736644b00..403a15a0631742 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -278,14 +278,6 @@ export interface ExperimentalOptions { } export interface LegacyOptions { - /** - * Revert vite dev to the v2.9 strategy. Enable esbuild based deps scanner. - * - * @experimental - * @deprecated - * @default false - */ - devDepsScanner?: boolean /** * Revert vite build to the v2.9 strategy. Disable esbuild deps optimization and adds `@rollup/plugin-commonjs` * diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 11d9e83d6a0f90..142a9519bb4b59 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -63,7 +63,6 @@ export interface DepsOptimizer { isOptimizedDepFile: (id: string) => boolean isOptimizedDepUrl: (url: string) => boolean getOptimizedDepId: (depInfo: OptimizedDepInfo) => string - delayDepsOptimizerUntil: (id: string, done: () => Promise) => void registerWorkersSource: (id: string) => void resetRegisteredIds: () => void @@ -232,11 +231,15 @@ export async function optimizeDeps( if (cachedMetadata) { return cachedMetadata } - const depsInfo = await discoverProjectDependencies(config) + const deps = await discoverProjectDependencies(config) - const depsString = depsLogString(Object.keys(depsInfo)) + const depsString = depsLogString(Object.keys(deps)) log(colors.green(`Optimizing dependencies:\n ${depsString}`)) + await addManuallyIncludedOptimizeDeps(deps, config) + + const depsInfo = toDiscoveredDependencies(config, deps, !!config.build.ssr) + const result = await runOptimizeDeps(config, depsInfo) await result.commit() @@ -365,9 +368,8 @@ export function loadCachedDepOptimizationMetadata( * find deps to pre-bundle and include user hard-coded dependencies */ export async function discoverProjectDependencies( - config: ResolvedConfig, - timestamp?: string -): Promise> { + config: ResolvedConfig +): Promise> { const { deps, missing } = await scanImports(config) const missingIds = Object.keys(missing) @@ -384,24 +386,7 @@ export async function discoverProjectDependencies( ) } - return initialProjectDependencies(config, timestamp, deps) -} - -/** - * Create the initial discovered deps list. At build time we only - * have the manually included deps. During dev, a scan phase is - * performed and knownDeps is the list of discovered deps - */ -export async function initialProjectDependencies( - config: ResolvedConfig, - timestamp?: string, - knownDeps?: Record -): Promise> { - const deps: Record = knownDeps ?? {} - - await addManuallyIncludedOptimizeDeps(deps, config) - - return toDiscoveredDependencies(config, deps, !!config.build.ssr, timestamp) + return deps } export function toDiscoveredDependencies( @@ -431,7 +416,7 @@ export function toDiscoveredDependencies( export function depsLogString(qualifiedIds: string[]): string { if (isDebugEnabled) { - return colors.yellow(qualifiedIds.join(`\n `)) + return colors.yellow(qualifiedIds.join(`, `)) } else { const total = qualifiedIds.length const maxListed = 5 @@ -656,7 +641,7 @@ export async function findKnownImports( return Object.keys(deps) } -async function addManuallyIncludedOptimizeDeps( +export async function addManuallyIncludedOptimizeDeps( deps: Record, config: ResolvedConfig, extra: string[] = [], diff --git a/packages/vite/src/node/optimizer/optimizer.ts b/packages/vite/src/node/optimizer/optimizer.ts index def39f2e8c8361..5432f9493a7348 100644 --- a/packages/vite/src/node/optimizer/optimizer.ts +++ b/packages/vite/src/node/optimizer/optimizer.ts @@ -1,12 +1,9 @@ -import path from 'node:path' import colors from 'picocolors' import _debug from 'debug' -import glob from 'fast-glob' -import { FS_PREFIX } from '../constants' import { getHash } from '../utils' -import { transformRequest } from '../server/transformRequest' import type { ResolvedConfig, ViteDevServer } from '..' import { + addManuallyIncludedOptimizeDeps, addOptimizedDepInfo, createIsOptimizedDepUrl, debuggerViteDeps as debug, @@ -16,15 +13,16 @@ import { extractExportsData, getOptimizedDepPath, initDepsOptimizerMetadata, - initialProjectDependencies, isOptimizedDepFile, loadCachedDepOptimizationMetadata, newDepOptimizationProcessing, optimizeServerSsrDeps, - runOptimizeDeps + runOptimizeDeps, + toDiscoveredDependencies } from '.' import type { DepOptimizationProcessing, + DepOptimizationResult, DepsOptimizer, OptimizedDepInfo } from '.' @@ -55,13 +53,35 @@ export async function initDepsOptimizer( config: ResolvedConfig, server?: ViteDevServer ): Promise { - await createDepsOptimizer(config, server) + if (!getDepsOptimizer(config, { ssr: false })) { + await createDepsOptimizer(config, server) + } } +let creatingDevSsrOptimizer: Promise | undefined export async function initDevSsrDepsOptimizer( - config: ResolvedConfig + config: ResolvedConfig, + server: ViteDevServer ): Promise { - await createDevSsrDepsOptimizer(config) + if (getDepsOptimizer(config, { ssr: true })) { + return + } + if (creatingDevSsrOptimizer) { + return creatingDevSsrOptimizer + } + creatingDevSsrOptimizer = (async function () { + // Important: scanning needs to be done before starting the SSR dev optimizer + // If ssrLoadModule is called before server.listen(), the main deps optimizer + // will not be yet created + if (!getDepsOptimizer(config, { ssr: false })) { + await initDepsOptimizer(config, server) + } + await getDepsOptimizer(config, { ssr: false })!.scanProcessing + + await createDevSsrDepsOptimizer(config) + creatingDevSsrOptimizer = undefined + })() + return await creatingDevSsrOptimizer } async function createDepsOptimizer( @@ -71,8 +91,6 @@ async function createDepsOptimizer( const { logger } = config const isBuild = config.command === 'build' - const scan = config.command !== 'build' && config.legacy?.devDepsScanner - const sessionTimestamp = Date.now().toString() const cachedMetadata = loadCachedDepOptimizationMetadata(config) @@ -130,87 +148,100 @@ async function createDepsOptimizer( let enqueuedRerun: (() => void) | undefined let currentlyProcessing = false - // Only pretransform optimizeDeps.entries on cold start - let optimizeDepsEntriesVisited = !!cachedMetadata - // If there wasn't a cache or it is outdated, we need to prepare a first run let firstRunCalled = !!cachedMetadata - if (!cachedMetadata) { - if (!scan) { - // Initialize discovered deps with manually added optimizeDeps.include info - const discovered = await initialProjectDependencies( - config, - sessionTimestamp - ) - for (const depInfo of Object.values(discovered)) { - addOptimizedDepInfo(metadata, 'discovered', { - ...depInfo, - processing: depOptimizationProcessing.promise - }) - } - } else { - // Perform a esbuild base scan of user code to discover dependencies - currentlyProcessing = true + let postScanOptimizationResult: Promise | undefined - const scanPhaseProcessing = newDepOptimizationProcessing() - depsOptimizer.scanProcessing = scanPhaseProcessing.promise + if (!cachedMetadata) { + // Enter processing state until crawl of static imports ends + currentlyProcessing = true - setTimeout(async () => { - try { - debug(colors.green(`scanning for dependencies...`)) + // Initialize discovered deps with manually added optimizeDeps.include info - const discovered = await discoverProjectDependencies( - config, - sessionTimestamp - ) + const deps: Record = {} + await addManuallyIncludedOptimizeDeps(deps, config) - // Respect the scan phase discover order to improve reproducibility - for (const depInfo of Object.values(discovered)) { - addOptimizedDepInfo(metadata, 'discovered', { - ...depInfo, - processing: depOptimizationProcessing.promise - }) - } + const discovered = await toDiscoveredDependencies( + config, + deps, + !!config.build.ssr, + sessionTimestamp + ) - debug( - colors.green( - `dependencies found: ${depsLogString(Object.keys(discovered))}` - ) - ) + for (const depInfo of Object.values(discovered)) { + addOptimizedDepInfo(metadata, 'discovered', { + ...depInfo, + processing: depOptimizationProcessing.promise + }) + newDepsDiscovered = true + } - scanPhaseProcessing.resolve() - depsOptimizer.scanProcessing = undefined + // TODO: We need the scan during build time, until preAliasPlugin + // is refactored to work without the scanned deps. We could skip + // this for build later. - await runOptimizer() - } catch (e) { - logger.error(e.message) - if (depsOptimizer.scanProcessing) { - scanPhaseProcessing.resolve() - depsOptimizer.scanProcessing = undefined - } - } - }, 0) - } + runScanner() } - async function runOptimizer() { - const isRerun = firstRunCalled - firstRunCalled = true + async function runScanner() { + const scanPhaseProcessing = newDepOptimizationProcessing() + depsOptimizer.scanProcessing = scanPhaseProcessing.promise - // Ensure that rerun is called sequentially - enqueuedRerun = undefined + try { + debug(colors.green(`scanning for dependencies...`)) - // Ensure that a rerun will not be issued for current discovered deps - if (handle) clearTimeout(handle) + const deps = await discoverProjectDependencies(config) - if (Object.keys(metadata.discovered).length === 0) { - currentlyProcessing = false - return + debug( + colors.green( + Object.keys(deps).length > 0 + ? `dependencies found by scanner: ${depsLogString( + Object.keys(deps) + )}` + : `no dependencies found by scanner` + ) + ) + + // Add these dependencies to the discovered list, as these are currently + // used by the preAliasPlugin to support aliased and optimized deps. + // This is also used by the CJS externalization heuristics in legacy mode + for (const id of Object.keys(deps)) { + if (!metadata.discovered[id]) { + addMissingDep(id, deps[id]) + } + } + + if (!isBuild) { + const knownDeps = prepareKnownDeps() + + // For dev, we run the scanner and the first optimization + // run on the background, but we wait until crawling has ended + // to decide if we send this result to the browser or we need to + // do another optimize step + postScanOptimizationResult = runOptimizeDeps(config, knownDeps) + } + } catch (e) { + logger.error(e.message) + } finally { + scanPhaseProcessing.resolve() + depsOptimizer.scanProcessing = undefined } + } - currentlyProcessing = true + async function startNextDiscoveredBatch() { + newDepsDiscovered = false + // Add the current depOptimizationProcessing to the queue, these + // promises are going to be resolved once a rerun is committed + depOptimizationProcessingQueue.push(depOptimizationProcessing) + + // Create a new promise for the next rerun, discovered missing + // dependencies will be asigned this promise from this point + depOptimizationProcessing = newDepOptimizationProcessing() + } + + async function optimizeNewDeps() { // a succesful completion of the optimizeDeps rerun will end up // creating new bundled version of all current and discovered deps // in the cache dir and a new metadata info object assigned @@ -223,48 +254,53 @@ async function createDepsOptimizer( // All deps, previous known and newly discovered are rebundled, // respect insertion order to keep the metadata file stable - const newDeps: Record = {} + const knownDeps = prepareKnownDeps() + + startNextDiscoveredBatch() + + return await runOptimizeDeps(config, knownDeps) + } + function prepareKnownDeps() { + const knownDeps: Record = {} // Clone optimized info objects, fileHash, browserHash may be changed for them for (const dep of Object.keys(metadata.optimized)) { - newDeps[dep] = { ...metadata.optimized[dep] } + knownDeps[dep] = { ...metadata.optimized[dep] } } for (const dep of Object.keys(metadata.discovered)) { // Clone the discovered info discarding its processing promise const { processing, ...info } = metadata.discovered[dep] - newDeps[dep] = info + knownDeps[dep] = info } + return knownDeps + } - newDepsDiscovered = false + async function runOptimizer(preRunResult?: DepOptimizationResult) { + const isRerun = firstRunCalled + firstRunCalled = true - // Add the current depOptimizationProcessing to the queue, these - // promises are going to be resolved once a rerun is committed - depOptimizationProcessingQueue.push(depOptimizationProcessing) + // Ensure that rerun is called sequentially + enqueuedRerun = undefined - // Create a new promise for the next rerun, discovered missing - // dependencies will be asigned this promise from this point - depOptimizationProcessing = newDepOptimizationProcessing() + // Ensure that a rerun will not be issued for current discovered deps + if (handle) clearTimeout(handle) + + if (Object.keys(metadata.discovered).length === 0) { + currentlyProcessing = false + return + } + + currentlyProcessing = true try { - const processingResult = await runOptimizeDeps(config, newDeps) + const processingResult = preRunResult ?? (await optimizeNewDeps()) const newData = processingResult.metadata - const needsInteropMismatch = [] - for (const dep in metadata.discovered) { - const discoveredDepInfo = metadata.discovered[dep] - const depInfo = newData.optimized[dep] - if (depInfo) { - if ( - discoveredDepInfo.needsInterop !== undefined && - depInfo.needsInterop !== discoveredDepInfo.needsInterop - ) { - // This only happens when a discovered dependency has mixed ESM and CJS syntax - // and it hasn't been manually added to optimizeDeps.needsInterop - needsInteropMismatch.push(dep) - } - } - } + const needsInteropMismatch = findInteropMismatches( + metadata.discovered, + newData.optimized + ) // After a re-optimization, if the internal bundled chunks change a full page reload // is required. If the files are stable, we can avoid the reload that is expensive @@ -339,7 +375,15 @@ async function createDepsOptimizer( logNewlyDiscoveredDeps() }, 2 * debounceMs) } else { - debug(colors.green(`✨ optimized dependencies unchanged`)) + debug( + colors.green( + `✨ ${ + !isRerun + ? `dependencies optimized` + : `optimized dependencies unchanged` + }` + ) + ) } } else { if (newDepsDiscovered) { @@ -440,14 +484,9 @@ async function createDepsOptimizer( resolved: string, ssr?: boolean ): OptimizedDepInfo { - if (depsOptimizer.scanProcessing) { - config.logger.error( - 'Vite internal error: registering missing import before initial scanning is over' - ) - } if (!isBuild && ssr) { config.logger.error( - `Error: ${id} is a missing dependency in SSR dev server, it needs to be added to optimizeDeps.include` + 'Vite internal error: ssr dep registerd as a browser dep' ) } const optimized = metadata.optimized[id] @@ -464,8 +503,29 @@ async function createDepsOptimizer( // It will be processed in the next rerun call return missing } + + missing = addMissingDep(id, resolved, ssr) + + // Until the first optimize run is called, avoid triggering processing + // We'll wait until the user codebase is eagerly processed by Vite so + // we can get a list of every missing dependency before giving to the + // browser a dependency that may be outdated, thus avoiding full page reloads + + if (firstRunCalled) { + // Debounced rerun, let other missing dependencies be discovered before + // the running next optimizeDeps + debouncedProcessing() + } + + // Return the path for the optimized bundle, this path is known before + // esbuild is run to generate the pre-bundle + return missing + } + + function addMissingDep(id: string, resolved: string, ssr?: boolean) { newDepsDiscovered = true - missing = addOptimizedDepInfo(metadata, 'discovered', { + + return addOptimizedDepInfo(metadata, 'discovered', { id, file: getOptimizedDepPath(id, config, ssr), src: resolved, @@ -483,24 +543,12 @@ async function createDepsOptimizer( processing: depOptimizationProcessing.promise, exportsData: extractExportsData(resolved, config) }) - - // Until the first optimize run is called, avoid triggering processing - // We'll wait until the user codebase is eagerly processed by Vite so - // we can get a list of every missing dependency before giving to the - // browser a dependency that may be outdated, thus avoiding full page reloads - - if (scan || firstRunCalled) { - // Debounced rerun, let other missing dependencies be discovered before - // the running next optimizeDeps - debouncedProcessing() - } - - // Return the path for the optimized bundle, this path is known before - // esbuild is run to generate the pre-bundle - return missing } function debouncedProcessing(timeout = debounceMs) { + if (!newDepsDiscovered) { + return + } // Debounced rerun, let other missing dependencies be discovered before // the running next optimizeDeps enqueuedRerun = undefined @@ -516,6 +564,88 @@ async function createDepsOptimizer( }, timeout) } + async function onCrawlEnd() { + debug(colors.green(`✨ static imports crawl ended`)) + if (firstRunCalled) { + return + } + + currentlyProcessing = false + + 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 + + if (!isBuild && postScanOptimizationResult) { + const result = await postScanOptimizationResult + postScanOptimizationResult = undefined + + const scanDeps = Object.keys(result.metadata.optimized) + + if (scanDeps.length === 0 && crawlDeps.length === 0) { + debug( + colors.green( + `✨ no dependencies found by the scanner or crawling static imports` + ) + ) + result.cancel() + firstRunCalled = true + return + } + + const needsInteropMismatch = findInteropMismatches( + metadata.discovered, + result.metadata.optimized + ) + const scannerMissedDeps = crawlDeps.some((dep) => !scanDeps.includes(dep)) + const outdatedResult = + needsInteropMismatch.length > 0 || scannerMissedDeps + + if (outdatedResult) { + // Drop this scan result, and perform a new optimization to avoid a full reload + result.cancel() + + // Add deps found by the scanner to the discovered deps while crawling + for (const dep of scanDeps) { + if (!crawlDeps.includes(dep)) { + addMissingDep(dep, result.metadata.optimized[dep].src!) + } + } + if (scannerMissedDeps) { + debug( + colors.yellow( + `✨ new dependencies were found while crawling that weren't detected by the scanner` + ) + ) + } + debug(colors.green(`✨ re-running optimizer`)) + debouncedProcessing(0) + } else { + debug( + colors.green( + `✨ using post-scan optimizer result, the scanner found every used dependency` + ) + ) + startNextDiscoveredBatch() + runOptimizer(result) + } + } else { + if (crawlDeps.length === 0) { + debug( + colors.green( + `✨ no dependencies found while crawling the static imports` + ) + ) + firstRunCalled = true + } else { + // queue the first optimizer run + debouncedProcessing(0) + } + } + } + const runOptimizerIfIdleAfterMs = 100 let registeredIds: { id: string; done: () => Promise }[] = [] @@ -538,8 +668,8 @@ async function createDepsOptimizer( function ensureFirstRun() { if (!firstRunEnsured && !firstRunCalled && registeredIds.length === 0) { setTimeout(() => { - if (!firstRunCalled && registeredIds.length === 0) { - debouncedProcessing(0) // queue the optimizer run + if (registeredIds.length === 0) { + onCrawlEnd() } }, runOptimizerIfIdleAfterMs) } @@ -563,10 +693,6 @@ async function createDepsOptimizer( registeredIds.push({ id, done }) runOptimizerWhenIdle() } - if (server && !optimizeDepsEntriesVisited) { - optimizeDepsEntriesVisited = true - preTransformOptimizeDepsEntries(server) - } } function runOptimizerWhenIdle() { @@ -576,11 +702,11 @@ async function createDepsOptimizer( waitingOn = next.id const afterLoad = () => { waitingOn = undefined - if (!firstRunCalled && !workersSources.has(next.id)) { + if (!workersSources.has(next.id)) { if (registeredIds.length > 0) { runOptimizerWhenIdle() } else { - debouncedProcessing(0) // queue the optimizer run + onCrawlEnd() } } } @@ -602,6 +728,7 @@ async function createDevSsrDepsOptimizer( config: ResolvedConfig ): Promise { const metadata = await optimizeServerSsrDeps(config) + const depsOptimizer = { metadata, isOptimizedDepFile: (id: string) => isOptimizedDepFile(id, config), @@ -626,27 +753,25 @@ async function createDevSsrDepsOptimizer( devSsrDepsOptimizerMap.set(config, depsOptimizer) } -export async function preTransformOptimizeDepsEntries( - server: ViteDevServer -): Promise { - const { config } = server - const { root } = config - const { entries } = config.optimizeDeps - if (entries) { - const explicitEntries = await glob(entries, { - cwd: root, - ignore: ['**/node_modules/**', `**/${config.build.outDir}/**`], - absolute: true - }) - // TODO: should we restrict the entries to JS and HTML like the - // scanner did? I think we can let the user chose any entry - for (const entry of explicitEntries) { - const url = entry.startsWith(root + '/') - ? entry.slice(root.length) - : path.posix.join(FS_PREFIX + entry) - transformRequest(url, server, { ssr: false }).catch((e) => { - config.logger.error(e.message) - }) +function findInteropMismatches( + discovered: Record, + optimized: Record +) { + const needsInteropMismatch = [] + for (const dep in discovered) { + const discoveredDepInfo = discovered[dep] + const depInfo = optimized[dep] + if (depInfo) { + if ( + discoveredDepInfo.needsInterop !== undefined && + depInfo.needsInterop !== discoveredDepInfo.needsInterop + ) { + // This only happens when a discovered dependency has mixed ESM and CJS syntax + // and it hasn't been manually added to optimizeDeps.needsInterop + needsInteropMismatch.push(dep) + debug(colors.cyan(`✨ needsInterop mismatch detected for ${dep}`)) + } } } + return needsInteropMismatch } diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index 001a21dab43530..5af046908ff0fa 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -691,9 +691,8 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { ) // pre-transform known direct imports - // TODO: should we also crawl dynamic imports? or the experience is good enough to allow - // users to chose their tradeoffs by explicitily setting optimizeDeps.entries for the - // most common dynamic imports + // These requests will also be registered in transformRequest to be awaited + // by the deps optimizer if (config.server.preTransformRequests && staticImportedUrls.size) { staticImportedUrls.forEach(({ url, id }) => { url = unwrapId(removeImportQuery(url)).replace( diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index 9e01808139c4af..0bab69f50eeb98 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -41,7 +41,7 @@ export async function resolvePlugins( return [ isWatch ? ensureWatchPlugin() : null, isBuild ? metadataPlugin() : null, - isBuild ? null : preAliasPlugin(config), + preAliasPlugin(config), aliasPlugin({ entries: config.resolve.alias }), ...prePlugins, config.build.polyfillModulePreload diff --git a/packages/vite/src/node/plugins/preAlias.ts b/packages/vite/src/node/plugins/preAlias.ts index a99d9b02aafc02..2623a9d4ec97bc 100644 --- a/packages/vite/src/node/plugins/preAlias.ts +++ b/packages/vite/src/node/plugins/preAlias.ts @@ -1,4 +1,4 @@ -import type { ResolvedConfig } from '..' +import type { Alias, AliasOptions, ResolvedConfig } from '..' import type { Plugin } from '../plugin' import { bareImportRE } from '../utils' import { getDepsOptimizer } from '../optimizer' @@ -8,14 +8,48 @@ import { tryOptimizedResolve } from './resolve' * A plugin to avoid an aliased AND optimized dep from being aliased in src */ export function preAliasPlugin(config: ResolvedConfig): Plugin { + const findPatterns = getAliasPatterns(config.resolve.alias) return { name: 'vite:pre-alias', async resolveId(id, importer, options) { const ssr = options?.ssr === true const depsOptimizer = getDepsOptimizer(config, { ssr }) - if (depsOptimizer && bareImportRE.test(id) && !options?.scan) { - return await tryOptimizedResolve(depsOptimizer, id, importer) + if ( + importer && + depsOptimizer && + bareImportRE.test(id) && + !options?.scan + ) { + if (findPatterns.find((pattern) => matches(pattern, id))) { + return await tryOptimizedResolve(depsOptimizer, id, importer) + } } } } } + +// In sync with rollup plugin alias logic +function matches(pattern: string | RegExp, importee: string) { + if (pattern instanceof RegExp) { + return pattern.test(importee) + } + if (importee.length < pattern.length) { + return false + } + if (importee === pattern) { + return true + } + return importee.startsWith(pattern + '/') +} + +function getAliasPatterns( + entries: (AliasOptions | undefined) & Alias[] +): (string | RegExp)[] { + if (!entries) { + return [] + } + if (Array.isArray(entries)) { + return entries.map((entry) => entry.find) + } + return Object.entries(entries).map(([find]) => find) +} diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index 4967b7975c9a54..a18a30d72ee073 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -719,6 +719,10 @@ export async function tryOptimizedResolve( id: string, importer?: string ): Promise { + // TODO: we need to wait until scanning is done here as this function + // is used in the preAliasPlugin to decide if an aliased dep is optimized, + // and avoid replacing the bare import with the resolved path. + // We should be able to remove this in the future await depsOptimizer.scanProcessing const metadata = depsOptimizer.metadata diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 055e49d89a19e3..9c722ed667a4e7 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -311,8 +311,6 @@ export async function createServer( let exitProcess: () => void - let creatingDevSsrOptimizer: Promise | null = null - const server: ViteDevServer = { config, middlewares, @@ -331,12 +329,8 @@ export async function createServer( }, transformIndexHtml: null!, // to be immediately set async ssrLoadModule(url, opts?: { fixStacktrace?: boolean }) { - if (!getDepsOptimizer(config, { ssr: true })) { - if (!creatingDevSsrOptimizer) { - creatingDevSsrOptimizer = initDevSsrDepsOptimizer(config) - } - await creatingDevSsrOptimizer - creatingDevSsrOptimizer = null + if (isDepsOptimizerEnabled(config)) { + await initDevSsrDepsOptimizer(config, server) } await updateCjsSsrExternals(server) return ssrLoadModule( @@ -534,32 +528,40 @@ export async function createServer( // error handler middlewares.use(errorMiddleware(server, middlewareMode)) - const initOptimizer = async () => { - if (isDepsOptimizerEnabled(config)) { - await initDepsOptimizer(config, server) + let initingServer: Promise | undefined + let serverInited = false + const initServer = async () => { + if (serverInited) { + return + } + if (initingServer) { + return initingServer } + initingServer = (async function () { + await container.buildStart({}) + if (isDepsOptimizerEnabled(config)) { + await initDepsOptimizer(config, server) + } + initingServer = undefined + serverInited = true + })() + return initingServer } if (!middlewareMode && httpServer) { - let isOptimized = false // overwrite listen to init optimizer before server start const listen = httpServer.listen.bind(httpServer) httpServer.listen = (async (port: number, ...args: any[]) => { - if (!isOptimized) { - try { - await container.buildStart({}) - await initOptimizer() - isOptimized = true - } catch (e) { - httpServer.emit('error', e) - return - } + try { + await initServer() + } catch (e) { + httpServer.emit('error', e) + return } return listen(port, ...args) }) as any } else { - await container.buildStart({}) - await initOptimizer() + await initServer() } return server diff --git a/packages/vite/src/node/server/transformRequest.ts b/packages/vite/src/node/server/transformRequest.ts index 782ad3ef1e1725..574e69b27524a5 100644 --- a/packages/vite/src/node/server/transformRequest.ts +++ b/packages/vite/src/node/server/transformRequest.ts @@ -145,10 +145,7 @@ async function doTransform( const result = loadAndTransform(id, url, server, options, timestamp) - const depsOptimizer = getDepsOptimizer(config, { ssr }) - if (depsOptimizer && !config.legacy?.devDepsScanner) { - depsOptimizer.delayDepsOptimizerUntil(id, () => result) - } + getDepsOptimizer(config, { ssr })?.delayDepsOptimizerUntil(id, () => result) return result } diff --git a/packages/vite/src/node/ssr/__tests__/ssrModuleLoader.spec.ts b/packages/vite/src/node/ssr/__tests__/ssrModuleLoader.spec.ts deleted file mode 100644 index 8451be5b3af745..00000000000000 --- a/packages/vite/src/node/ssr/__tests__/ssrModuleLoader.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { resolve } from 'node:path' -import { fileURLToPath } from 'node:url' -import { expect, test, vi } from 'vitest' -import { createServer } from '../../index' - -const __filename = fileURLToPath(import.meta.url) -const badjs = resolve(__filename, '../fixtures/ssrModuleLoader-bad.js') -const THROW_MESSAGE = 'it is an expected error' - -test('always throw error when evaluating an wrong SSR module', async () => { - const viteServer = await createServer() - const spy = vi.spyOn(console, 'error').mockImplementation(() => {}) - const expectedErrors = [] - for (const _ of [0, 1]) { - try { - await viteServer.ssrLoadModule(badjs, { fixStacktrace: true }) - } catch (e) { - expectedErrors.push(e) - } - } - await viteServer.close() - expect(expectedErrors).toHaveLength(2) - expect(expectedErrors[0]).toBe(expectedErrors[1]) - expectedErrors.forEach((error) => { - expect(error?.message).toContain(THROW_MESSAGE) - }) - expect(spy).toBeCalledTimes(1) - const [firstParameter] = spy.mock.calls[0] - expect(firstParameter).toContain('Error when evaluating SSR module') - expect(firstParameter).toContain(THROW_MESSAGE) - spy.mockClear() -}) diff --git a/playground/alias/index.html b/playground/alias/index.html index 947db06127ed41..909aaf6ef5411e 100644 --- a/playground/alias/index.html +++ b/playground/alias/index.html @@ -42,6 +42,10 @@

Alias

} } }).mount('.optimized') + + // aliased to an absolute URL in CJS, should be optimized + import { isFunction } from '@vue/shared' + console.log(isFunction(() => {})) diff --git a/playground/alias/package.json b/playground/alias/package.json index ee345bf1f9ce13..4b49ca8d346ebf 100644 --- a/playground/alias/package.json +++ b/playground/alias/package.json @@ -10,7 +10,8 @@ }, "dependencies": { "aliased-module": "file:./dir/module", - "vue": "^3.2.37" + "vue": "^3.2.37", + "@vue/shared": "^3.2.37" }, "devDependencies": { "resolve-linked": "workspace:*" diff --git a/playground/alias/vite.config.js b/playground/alias/vite.config.js index 530fe86a3de2cb..634871877a0f56 100644 --- a/playground/alias/vite.config.js +++ b/playground/alias/vite.config.js @@ -16,6 +16,11 @@ module.exports = { { find: '/@', replacement: path.resolve(__dirname, 'dir') }, // aliasing an optimized dep { find: 'vue', replacement: 'vue/dist/vue.esm-bundler.js' }, + // aliasing an optimized dep to absolute URL + { + find: '@vue/shared', + replacement: require.resolve('@vue/shared/dist/shared.cjs.prod.js') + }, // aliasing one unoptimized dep to an optimized dep { find: 'foo', replacement: 'vue' }, { diff --git a/playground/css-sourcemap/__tests__/build.spec.ts b/playground/css-sourcemap/__tests__/build.spec.ts deleted file mode 100644 index b30284731a76d9..00000000000000 --- a/playground/css-sourcemap/__tests__/build.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { isBuild, serverLogs } from '~utils' - -test.runIf(isBuild)('should not output sourcemap warning (#4939)', () => { - serverLogs.forEach((log) => { - expect(log).not.toMatch('Sourcemap is likely to be incorrect') - }) -}) diff --git a/playground/css-sourcemap/__tests__/css-sourcemap.spec.ts b/playground/css-sourcemap/__tests__/css-sourcemap.spec.ts new file mode 100644 index 00000000000000..f63ef8946eba40 --- /dev/null +++ b/playground/css-sourcemap/__tests__/css-sourcemap.spec.ts @@ -0,0 +1,443 @@ +import { URL } from 'node:url' +import { + extractSourcemap, + formatSourcemapForSnapshot, + isBuild, + isServe, + page, + serverLogs +} from '~utils' + +test.runIf(isBuild)('should not output sourcemap warning (#4939)', () => { + serverLogs.forEach((log) => { + expect(log).not.toMatch('Sourcemap is likely to be incorrect') + }) +}) + +describe.runIf(isServe)('serve', () => { + const getStyleTagContentIncluding = async (content: string) => { + const styles = await page.$$('style') + for (const style of styles) { + const text = await style.textContent() + if (text.includes(content)) { + return text + } + } + throw new Error('Not found') + } + + test('linked css', async () => { + const res = await page.request.get( + new URL('./linked.css', page.url()).href, + { + headers: { + accept: 'text/css' + } + } + ) + const css = await res.text() + const map = extractSourcemap(css) + expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` + { + "mappings": "AAAA,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACT,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACb,CAAC;", + "sources": [ + "/root/linked.css", + ], + "sourcesContent": [ + ".linked { + color: red; + } + ", + ], + "version": 3, + } + `) + }) + + test('linked css with import', async () => { + const res = await page.request.get( + new URL('./linked-with-import.css', page.url()).href, + { + headers: { + accept: 'text/css' + } + } + ) + const css = await res.text() + const map = extractSourcemap(css) + expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` + { + "mappings": "AAAA;EACE,UAAU;AACZ;;ACAA;EACE,UAAU;AACZ", + "sources": [ + "/root/be-imported.css", + "/root/linked-with-import.css", + ], + "sourcesContent": [ + ".be-imported { + color: red; + } + ", + "@import '@/be-imported.css'; + + .linked-with-import { + color: red; + } + ", + ], + "version": 3, + } + `) + }) + + test('imported css', async () => { + const css = await getStyleTagContentIncluding('.imported ') + const map = extractSourcemap(css) + expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` + { + "mappings": "AAAA,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACX,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACb,CAAC;", + "sources": [ + "/root/imported.css", + ], + "sourcesContent": [ + ".imported { + color: red; + } + ", + ], + "version": 3, + } + `) + }) + + test('imported css with import', async () => { + const css = await getStyleTagContentIncluding('.imported-with-import ') + const map = extractSourcemap(css) + expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` + { + "mappings": "AAAA;EACE,UAAU;AACZ;;ACAA;EACE,UAAU;AACZ", + "sources": [ + "/root/be-imported.css", + "/root/imported-with-import.css", + ], + "sourcesContent": [ + ".be-imported { + color: red; + } + ", + "@import '@/be-imported.css'; + + .imported-with-import { + color: red; + } + ", + ], + "version": 3, + } + `) + }) + + test('imported sass', async () => { + const css = await getStyleTagContentIncluding('.imported-sass ') + const map = extractSourcemap(css) + expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` + { + "mappings": "AACE;EACE", + "sources": [ + "/root/imported.sass", + ], + "sourcesContent": [ + ".imported + &-sass + color: red + ", + ], + "version": 3, + } + `) + }) + + test('imported sass module', async () => { + const css = await getStyleTagContentIncluding('._imported-sass-module_') + const map = extractSourcemap(css) + expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` + { + "mappings": "AACE;EACE", + "sources": [ + "/root/imported.module.sass", + ], + "sourcesContent": [ + ".imported + &-sass-module + color: red + ", + ], + "version": 3, + } + `) + }) + + test('imported less', async () => { + const css = await getStyleTagContentIncluding('.imported-less ') + const map = extractSourcemap(css) + expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` + { + "mappings": "AACE;EACE", + "sources": [ + "/root/imported.less", + ], + "sourcesContent": [ + ".imported { + &-less { + color: @color; + } + } + ", + ], + "version": 3, + } + `) + }) + + test('imported stylus', async () => { + const css = await getStyleTagContentIncluding('.imported-stylus ') + const map = extractSourcemap(css) + expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` + { + "mappings": "AACE;EACE,cAAM", + "sources": [ + "/root/imported.styl", + ], + "sourcesContent": [ + ".imported + &-stylus + color blue-red-mixed + ", + ], + "version": 3, + } + `) + }) + + test('should not output missing source file warning', () => { + serverLogs.forEach((log) => { + expect(log).not.toMatch(/Sourcemap for .+ points to missing source files/) + }) + }) +}) + +describe.runIf(isServe)('serve', () => { + const getStyleTagContentIncluding = async (content: string) => { + const styles = await page.$$('style') + for (const style of styles) { + const text = await style.textContent() + if (text.includes(content)) { + return text + } + } + throw new Error('Not found') + } + + test('linked css', async () => { + const res = await page.request.get( + new URL('./linked.css', page.url()).href, + { + headers: { + accept: 'text/css' + } + } + ) + const css = await res.text() + const map = extractSourcemap(css) + expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` + { + "mappings": "AAAA,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACT,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACb,CAAC;", + "sources": [ + "/root/linked.css", + ], + "sourcesContent": [ + ".linked { + color: red; + } + ", + ], + "version": 3, + } + `) + }) + + test('linked css with import', async () => { + const res = await page.request.get( + new URL('./linked-with-import.css', page.url()).href, + { + headers: { + accept: 'text/css' + } + } + ) + const css = await res.text() + const map = extractSourcemap(css) + expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` + { + "mappings": "AAAA;EACE,UAAU;AACZ;;ACAA;EACE,UAAU;AACZ", + "sources": [ + "/root/be-imported.css", + "/root/linked-with-import.css", + ], + "sourcesContent": [ + ".be-imported { + color: red; + } + ", + "@import '@/be-imported.css'; + + .linked-with-import { + color: red; + } + ", + ], + "version": 3, + } + `) + }) + + test('imported css', async () => { + const css = await getStyleTagContentIncluding('.imported ') + const map = extractSourcemap(css) + expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` + { + "mappings": "AAAA,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACX,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACb,CAAC;", + "sources": [ + "/root/imported.css", + ], + "sourcesContent": [ + ".imported { + color: red; + } + ", + ], + "version": 3, + } + `) + }) + + test('imported css with import', async () => { + const css = await getStyleTagContentIncluding('.imported-with-import ') + const map = extractSourcemap(css) + expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` + { + "mappings": "AAAA;EACE,UAAU;AACZ;;ACAA;EACE,UAAU;AACZ", + "sources": [ + "/root/be-imported.css", + "/root/imported-with-import.css", + ], + "sourcesContent": [ + ".be-imported { + color: red; + } + ", + "@import '@/be-imported.css'; + + .imported-with-import { + color: red; + } + ", + ], + "version": 3, + } + `) + }) + + test('imported sass', async () => { + const css = await getStyleTagContentIncluding('.imported-sass ') + const map = extractSourcemap(css) + expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` + { + "mappings": "AACE;EACE", + "sources": [ + "/root/imported.sass", + ], + "sourcesContent": [ + ".imported + &-sass + color: red + ", + ], + "version": 3, + } + `) + }) + + test('imported sass module', async () => { + const css = await getStyleTagContentIncluding('._imported-sass-module_') + const map = extractSourcemap(css) + expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` + { + "mappings": "AACE;EACE", + "sources": [ + "/root/imported.module.sass", + ], + "sourcesContent": [ + ".imported + &-sass-module + color: red + ", + ], + "version": 3, + } + `) + }) + + test('imported less', async () => { + const css = await getStyleTagContentIncluding('.imported-less ') + const map = extractSourcemap(css) + expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` + { + "mappings": "AACE;EACE", + "sources": [ + "/root/imported.less", + ], + "sourcesContent": [ + ".imported { + &-less { + color: @color; + } + } + ", + ], + "version": 3, + } + `) + }) + + test('imported stylus', async () => { + const css = await getStyleTagContentIncluding('.imported-stylus ') + const map = extractSourcemap(css) + expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` + { + "mappings": "AACE;EACE,cAAM", + "sources": [ + "/root/imported.styl", + ], + "sourcesContent": [ + ".imported + &-stylus + color blue-red-mixed + ", + ], + "version": 3, + } + `) + }) + + test('should not output missing source file warning', () => { + serverLogs.forEach((log) => { + expect(log).not.toMatch(/Sourcemap for .+ points to missing source files/) + }) + }) +}) + +test.runIf(isBuild)('should not output sourcemap warning (#4939)', () => { + serverLogs.forEach((log) => { + expect(log).not.toMatch('Sourcemap is likely to be incorrect') + }) +}) diff --git a/playground/css-sourcemap/__tests__/serve.spec.ts b/playground/css-sourcemap/__tests__/serve.spec.ts deleted file mode 100644 index 6e7392952c3caa..00000000000000 --- a/playground/css-sourcemap/__tests__/serve.spec.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { URL } from 'node:url' -import { - extractSourcemap, - formatSourcemapForSnapshot, - isServe, - page, - serverLogs -} from '~utils' - -describe.runIf(isServe)('serve', () => { - const getStyleTagContentIncluding = async (content: string) => { - const styles = await page.$$('style') - for (const style of styles) { - const text = await style.textContent() - if (text.includes(content)) { - return text - } - } - throw new Error('Not found') - } - - test('linked css', async () => { - const res = await page.request.get( - new URL('./linked.css', page.url()).href, - { - headers: { - accept: 'text/css' - } - } - ) - const css = await res.text() - const map = extractSourcemap(css) - expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` - { - "mappings": "AAAA,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACT,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACb,CAAC;", - "sources": [ - "/root/linked.css", - ], - "sourcesContent": [ - ".linked { - color: red; - } - ", - ], - "version": 3, - } - `) - }) - - test('linked css with import', async () => { - const res = await page.request.get( - new URL('./linked-with-import.css', page.url()).href, - { - headers: { - accept: 'text/css' - } - } - ) - const css = await res.text() - const map = extractSourcemap(css) - expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` - { - "mappings": "AAAA;EACE,UAAU;AACZ;;ACAA;EACE,UAAU;AACZ", - "sources": [ - "/root/be-imported.css", - "/root/linked-with-import.css", - ], - "sourcesContent": [ - ".be-imported { - color: red; - } - ", - "@import '@/be-imported.css'; - - .linked-with-import { - color: red; - } - ", - ], - "version": 3, - } - `) - }) - - test('imported css', async () => { - const css = await getStyleTagContentIncluding('.imported ') - const map = extractSourcemap(css) - expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` - { - "mappings": "AAAA,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACX,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACb,CAAC;", - "sources": [ - "/root/imported.css", - ], - "sourcesContent": [ - ".imported { - color: red; - } - ", - ], - "version": 3, - } - `) - }) - - test('imported css with import', async () => { - const css = await getStyleTagContentIncluding('.imported-with-import ') - const map = extractSourcemap(css) - expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` - { - "mappings": "AAAA;EACE,UAAU;AACZ;;ACAA;EACE,UAAU;AACZ", - "sources": [ - "/root/be-imported.css", - "/root/imported-with-import.css", - ], - "sourcesContent": [ - ".be-imported { - color: red; - } - ", - "@import '@/be-imported.css'; - - .imported-with-import { - color: red; - } - ", - ], - "version": 3, - } - `) - }) - - test('imported sass', async () => { - const css = await getStyleTagContentIncluding('.imported-sass ') - const map = extractSourcemap(css) - expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` - { - "mappings": "AACE;EACE", - "sources": [ - "/root/imported.sass", - ], - "sourcesContent": [ - ".imported - &-sass - color: red - ", - ], - "version": 3, - } - `) - }) - - test('imported sass module', async () => { - const css = await getStyleTagContentIncluding('._imported-sass-module_') - const map = extractSourcemap(css) - expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` - { - "mappings": "AACE;EACE", - "sources": [ - "/root/imported.module.sass", - ], - "sourcesContent": [ - ".imported - &-sass-module - color: red - ", - ], - "version": 3, - } - `) - }) - - test('imported less', async () => { - const css = await getStyleTagContentIncluding('.imported-less ') - const map = extractSourcemap(css) - expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` - { - "mappings": "AACE;EACE", - "sources": [ - "/root/imported.less", - ], - "sourcesContent": [ - ".imported { - &-less { - color: @color; - } - } - ", - ], - "version": 3, - } - `) - }) - - test('imported stylus', async () => { - const css = await getStyleTagContentIncluding('.imported-stylus ') - const map = extractSourcemap(css) - expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` - { - "mappings": "AACE;EACE,cAAM", - "sources": [ - "/root/imported.styl", - ], - "sourcesContent": [ - ".imported - &-stylus - color blue-red-mixed - ", - ], - "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/css/__tests__/postcss-plugins-different-dir.spec.ts b/playground/css/__tests__/postcss-plugins-different-dir/postcss-plugins-different-dir.spec.ts similarity index 93% rename from playground/css/__tests__/postcss-plugins-different-dir.spec.ts rename to playground/css/__tests__/postcss-plugins-different-dir/postcss-plugins-different-dir.spec.ts index a1e0b4810c8ee5..346309f7fcc32d 100644 --- a/playground/css/__tests__/postcss-plugins-different-dir.spec.ts +++ b/playground/css/__tests__/postcss-plugins-different-dir/postcss-plugins-different-dir.spec.ts @@ -6,7 +6,7 @@ import { getBgColor, getColor, page, ports } from '~utils' test('postcss plugins in different dir', async () => { const port = ports['css/postcss-plugins-different-dir'] const server = await createServer({ - root: path.join(__dirname, '..', '..', 'tailwind'), + root: path.join(__dirname, '..', '..', '..', 'tailwind'), logLevel: 'silent', server: { port, diff --git a/playground/css/__tests__/postcss-plugins-different-dir/serve.ts b/playground/css/__tests__/postcss-plugins-different-dir/serve.ts new file mode 100644 index 00000000000000..16e7d655f5ec15 --- /dev/null +++ b/playground/css/__tests__/postcss-plugins-different-dir/serve.ts @@ -0,0 +1,10 @@ +// this is automatically detected by playground/vitestSetup.ts and will replace +// the default e2e test serve behavior + +// The server is started in the test, so we need to have a custom serve +// function or a default server will be created +export async function serve(): Promise<{ close(): Promise }> { + return { + close: () => Promise.resolve() + } +} diff --git a/playground/css/postcss-caching/serve.ts b/playground/css/postcss-caching/serve.ts new file mode 100644 index 00000000000000..16e7d655f5ec15 --- /dev/null +++ b/playground/css/postcss-caching/serve.ts @@ -0,0 +1,10 @@ +// this is automatically detected by playground/vitestSetup.ts and will replace +// the default e2e test serve behavior + +// The server is started in the test, so we need to have a custom serve +// function or a default server will be created +export async function serve(): Promise<{ close(): Promise }> { + return { + close: () => Promise.resolve() + } +} diff --git a/playground/dynamic-import/__tests__/dynamic-import.spec.ts b/playground/dynamic-import/__tests__/dynamic-import.spec.ts index 1eecd801f200c9..86c42a4d79a7e5 100644 --- a/playground/dynamic-import/__tests__/dynamic-import.spec.ts +++ b/playground/dynamic-import/__tests__/dynamic-import.spec.ts @@ -24,18 +24,18 @@ test('should load data URL of `data:`', async () => { await untilUpdated(() => page.textContent('.view'), 'data', true) }) -test('should have same reference on static and dynamic js import', async () => { +test('should have same reference on static and dynamic js import, .mxd', async () => { await page.click('.mxd') await untilUpdated(() => page.textContent('.view'), 'true', true) }) // in this case, it is not possible to detect the correct module -test('should have same reference on static and dynamic js import', async () => { +test('should have same reference on static and dynamic js import, .mxd2', async () => { await page.click('.mxd2') await untilUpdated(() => page.textContent('.view'), 'false', true) }) -test('should have same reference on static and dynamic js import', async () => { +test('should have same reference on static and dynamic js import, .mxdjson', async () => { await page.click('.mxdjson') await untilUpdated(() => page.textContent('.view'), 'true', true) }) diff --git a/playground/dynamic-import/mxd.js b/playground/dynamic-import/files/mxd.js similarity index 100% rename from playground/dynamic-import/mxd.js rename to playground/dynamic-import/files/mxd.js diff --git a/playground/dynamic-import/mxd.json b/playground/dynamic-import/files/mxd.json similarity index 100% rename from playground/dynamic-import/mxd.json rename to playground/dynamic-import/files/mxd.json diff --git a/playground/dynamic-import/nested/index.js b/playground/dynamic-import/nested/index.js index 31dcd07560836f..0b229055b32b7c 100644 --- a/playground/dynamic-import/nested/index.js +++ b/playground/dynamic-import/nested/index.js @@ -1,5 +1,5 @@ -import mxdStatic from '../mxd' -import mxdStaticJSON from '../mxd.json' +import mxdStatic from '../files/mxd' +import mxdStaticJSON from '../files/mxd.json' async function setView(view) { const { msg } = await import(`../views/${view}.js`) @@ -18,7 +18,7 @@ document.querySelector('.baz').addEventListener('click', async () => { // full dynamic const arr = ['qux.js'] -const view = `/${arr[0]}` +const view = `/views/${arr[0]}` document.querySelector('.qux').addEventListener('click', async () => { const { msg } = await import(/*@vite-ignore*/ view) text('.view', msg) @@ -27,12 +27,12 @@ document.querySelector('.qux').addEventListener('click', async () => { // mixed static and dynamic document.querySelector('.mxd').addEventListener('click', async () => { const view = 'mxd' - const { default: mxdDynamic } = await import(`../${view}.js`) + const { default: mxdDynamic } = await import(`../files/${view}.js`) text('.view', mxdStatic === mxdDynamic) }) document.querySelector('.mxd2').addEventListener('click', async () => { - const test = { jss: '../mxd.js' } + const test = { jss: '../files/mxd.js' } const ttest = test const view = 'mxd' const { default: mxdDynamic } = await import(/*@vite-ignore*/ test.jss) @@ -41,7 +41,7 @@ document.querySelector('.mxd2').addEventListener('click', async () => { document.querySelector('.mxdjson').addEventListener('click', async () => { const view = 'mxd' - const { default: mxdDynamicJSON } = await import(`../${view}.json`) + const { default: mxdDynamicJSON } = await import(`../files/${view}.json`) text('.view', mxdStaticJSON === mxdDynamicJSON) }) diff --git a/playground/dynamic-import/qux.js b/playground/dynamic-import/views/qux.js similarity index 100% rename from playground/dynamic-import/qux.js rename to playground/dynamic-import/views/qux.js diff --git a/playground/dynamic-import/vite.config.js b/playground/dynamic-import/vite.config.js index b12d8347287a2f..96baaa74798686 100644 --- a/playground/dynamic-import/vite.config.js +++ b/playground/dynamic-import/vite.config.js @@ -7,17 +7,19 @@ module.exports = vite.defineConfig({ { name: 'copy', writeBundle() { + fs.mkdirSync(path.resolve(__dirname, 'dist/views')) + fs.mkdirSync(path.resolve(__dirname, 'dist/files')) fs.copyFileSync( - path.resolve(__dirname, 'qux.js'), - path.resolve(__dirname, 'dist/qux.js') + path.resolve(__dirname, 'views/qux.js'), + path.resolve(__dirname, 'dist/views/qux.js') ) fs.copyFileSync( - path.resolve(__dirname, 'mxd.js'), - path.resolve(__dirname, 'dist/mxd.js') + path.resolve(__dirname, 'files/mxd.js'), + path.resolve(__dirname, 'dist/files/mxd.js') ) fs.copyFileSync( - path.resolve(__dirname, 'mxd.json'), - path.resolve(__dirname, 'dist/mxd.json') + path.resolve(__dirname, 'files/mxd.json'), + path.resolve(__dirname, 'dist/files/mxd.json') ) } } diff --git a/playground/js-sourcemap/__tests__/build.spec.ts b/playground/js-sourcemap/__tests__/build.spec.ts deleted file mode 100644 index b30284731a76d9..00000000000000 --- a/playground/js-sourcemap/__tests__/build.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { isBuild, serverLogs } from '~utils' - -test.runIf(isBuild)('should not output sourcemap warning (#4939)', () => { - serverLogs.forEach((log) => { - expect(log).not.toMatch('Sourcemap is likely to be incorrect') - }) -}) diff --git a/playground/js-sourcemap/__tests__/serve.spec.ts b/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts similarity index 86% rename from playground/js-sourcemap/__tests__/serve.spec.ts rename to playground/js-sourcemap/__tests__/js-sourcemap.spec.ts index 5cd5c0e448b72f..a7ecbdf145af4a 100644 --- a/playground/js-sourcemap/__tests__/serve.spec.ts +++ b/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts @@ -44,3 +44,9 @@ if (!isBuild) { expect(true).toBe(true) }) } + +test.runIf(isBuild)('should not output sourcemap warning (#4939)', () => { + serverLogs.forEach((log) => { + expect(log).not.toMatch('Sourcemap is likely to be incorrect') + }) +}) diff --git a/playground/preload/dep-a/index.js b/playground/preload/dep-a/index.js new file mode 100644 index 00000000000000..42c97c2d826ec6 --- /dev/null +++ b/playground/preload/dep-a/index.js @@ -0,0 +1 @@ +export const msgFromA = 'From dep-a' diff --git a/playground/preload/dep-a/package.json b/playground/preload/dep-a/package.json new file mode 100644 index 00000000000000..d45aab10cf9379 --- /dev/null +++ b/playground/preload/dep-a/package.json @@ -0,0 +1,7 @@ +{ + "name": "dep-a", + "private": true, + "version": "0.0.0", + "type": "module", + "main": "index.js" +} diff --git a/playground/preload/dep-including-a/index.js b/playground/preload/dep-including-a/index.js new file mode 100644 index 00000000000000..4fd1e37a11dd09 --- /dev/null +++ b/playground/preload/dep-including-a/index.js @@ -0,0 +1,3 @@ +export { msgFromA } from 'dep-a' + +export const msg = 'From dep-including-a' diff --git a/playground/preload/dep-including-a/package.json b/playground/preload/dep-including-a/package.json new file mode 100644 index 00000000000000..0b6febf47c6266 --- /dev/null +++ b/playground/preload/dep-including-a/package.json @@ -0,0 +1,10 @@ +{ + "name": "dep-including-a", + "private": true, + "version": "0.0.0", + "type": "module", + "main": "index.js", + "dependencies": { + "dep-a": "file:../dep-a" + } +} diff --git a/playground/preload/index.html b/playground/preload/index.html index affed21f9791cf..5b6acc4190a6d9 100644 --- a/playground/preload/index.html +++ b/playground/preload/index.html @@ -3,6 +3,8 @@ import { createApp } from 'vue' import router from './router.js' import App from './src/App.vue' + import { msgFromA } from 'dep-a' + console.log(msgFromA) createApp(App).use(router).mount('#app') diff --git a/playground/preload/package.json b/playground/preload/package.json index 2b76252c1e04ff..c72b346dece591 100644 --- a/playground/preload/package.json +++ b/playground/preload/package.json @@ -14,6 +14,8 @@ }, "devDependencies": { "@vitejs/plugin-vue": "workspace:*", - "terser": "^5.14.1" + "terser": "^5.14.1", + "dep-a": "file:./dep-a", + "dep-including-a": "file:./dep-including-a" } } diff --git a/playground/preload/src/components/About.vue b/playground/preload/src/components/About.vue index 2e73e80099446b..b19a3e3f196b5e 100644 --- a/playground/preload/src/components/About.vue +++ b/playground/preload/src/components/About.vue @@ -1,9 +1,13 @@ + + - - diff --git a/playground/ssr-deps/__tests__/ssr-deps.spec.ts b/playground/ssr-deps/__tests__/ssr-deps.spec.ts index dbb169647fc603..1e81b5fc689486 100644 --- a/playground/ssr-deps/__tests__/ssr-deps.spec.ts +++ b/playground/ssr-deps/__tests__/ssr-deps.spec.ts @@ -77,3 +77,17 @@ test('msg from no external cjs', async () => { await page.goto(url) expect(await page.textContent('.no-external-cjs-msg')).toMatch('Hello World!') }) + +test('msg from optimized with nested external', async () => { + await page.goto(url) + expect(await page.textContent('.optimized-with-nested-external')).toMatch( + 'Hello World!' + ) +}) + +test('msg from optimized cjs with nested external', async () => { + await page.goto(url) + expect(await page.textContent('.optimized-cjs-with-nested-external')).toMatch( + 'Hello World!' + ) +}) diff --git a/playground/ssr-deps/nested-external/index.js b/playground/ssr-deps/nested-external/index.js new file mode 100644 index 00000000000000..1e211c295b64ef --- /dev/null +++ b/playground/ssr-deps/nested-external/index.js @@ -0,0 +1,9 @@ +// Module with state, to check that it is properly externalized and +// not bundled in the optimized deps +let msg +export function setMessage(externalMsg) { + msg = externalMsg +} +export default function getMessage() { + return msg +} diff --git a/playground/ssr-deps/nested-external/package.json b/playground/ssr-deps/nested-external/package.json new file mode 100644 index 00000000000000..40d1fd957af246 --- /dev/null +++ b/playground/ssr-deps/nested-external/package.json @@ -0,0 +1,7 @@ +{ + "name": "nested-external", + "private": true, + "version": "0.0.0", + "main": "index.js", + "type": "module" +} diff --git a/playground/ssr-deps/non-optimized-with-nested-external/index.js b/playground/ssr-deps/non-optimized-with-nested-external/index.js new file mode 100644 index 00000000000000..cab1f9715fa51e --- /dev/null +++ b/playground/ssr-deps/non-optimized-with-nested-external/index.js @@ -0,0 +1,3 @@ +import { setMessage } from 'nested-external' + +setMessage('Hello World!') diff --git a/playground/ssr-deps/non-optimized-with-nested-external/package.json b/playground/ssr-deps/non-optimized-with-nested-external/package.json new file mode 100644 index 00000000000000..dc212bfb46caec --- /dev/null +++ b/playground/ssr-deps/non-optimized-with-nested-external/package.json @@ -0,0 +1,10 @@ +{ + "name": "non-optimized-with-nested-external", + "private": true, + "version": "0.0.0", + "type": "module", + "main": "index.js", + "dependencies": { + "nested-external": "file:../nested-external" + } +} diff --git a/playground/ssr-deps/optimized-cjs-with-nested-external/index.js b/playground/ssr-deps/optimized-cjs-with-nested-external/index.js new file mode 100644 index 00000000000000..adbbb819ed31ea --- /dev/null +++ b/playground/ssr-deps/optimized-cjs-with-nested-external/index.js @@ -0,0 +1,5 @@ +const getMessage = require('nested-external') + +module.exports = { + hello: getMessage +} diff --git a/playground/ssr-deps/optimized-cjs-with-nested-external/package.json b/playground/ssr-deps/optimized-cjs-with-nested-external/package.json new file mode 100644 index 00000000000000..d2b17b82d7cdea --- /dev/null +++ b/playground/ssr-deps/optimized-cjs-with-nested-external/package.json @@ -0,0 +1,8 @@ +{ + "name": "optimized-cjs-with-nested-external", + "private": true, + "version": "0.0.0", + "dependencies": { + "nested-external": "file:../nested-external" + } +} diff --git a/playground/ssr-deps/optimized-with-nested-external/index.js b/playground/ssr-deps/optimized-with-nested-external/index.js new file mode 100644 index 00000000000000..3b0d9135b3e5b1 --- /dev/null +++ b/playground/ssr-deps/optimized-with-nested-external/index.js @@ -0,0 +1,5 @@ +import getMessage from 'nested-external' + +export function hello() { + return getMessage() +} diff --git a/playground/ssr-deps/optimized-with-nested-external/package.json b/playground/ssr-deps/optimized-with-nested-external/package.json new file mode 100644 index 00000000000000..a1596220e04091 --- /dev/null +++ b/playground/ssr-deps/optimized-with-nested-external/package.json @@ -0,0 +1,10 @@ +{ + "name": "optimized-with-nested-external", + "private": true, + "version": "0.0.0", + "type": "module", + "main": "index.js", + "dependencies": { + "nested-external": "file:../nested-external" + } +} diff --git a/playground/ssr-deps/package.json b/playground/ssr-deps/package.json index 81068e24d3474d..20efa9753bebc9 100644 --- a/playground/ssr-deps/package.json +++ b/playground/ssr-deps/package.json @@ -21,7 +21,10 @@ "ts-transpiled-exports": "file:./ts-transpiled-exports", "no-external-cjs": "file:./no-external-cjs", "import-builtin-cjs": "file:./import-builtin-cjs", - "no-external-css": "file:./no-external-css" + "no-external-css": "file:./no-external-css", + "non-optimized-with-nested-external": "file:./non-optimized-with-nested-external", + "optimized-with-nested-external": "file:./optimized-with-nested-external", + "optimized-cjs-with-nested-external": "file:./optimized-with-nested-external" }, "devDependencies": { "cross-env": "^7.0.3", diff --git a/playground/ssr-deps/server.js b/playground/ssr-deps/server.js index 432ff69ca3b7d8..7075cbcacc4d7a 100644 --- a/playground/ssr-deps/server.js +++ b/playground/ssr-deps/server.js @@ -35,7 +35,17 @@ export async function createServer(root = process.cwd(), hmrPort) { }, appType: 'custom', ssr: { - noExternal: ['no-external-cjs', 'import-builtin-cjs', 'no-external-css'] + noExternal: ['no-external-cjs', 'import-builtin-cjs', 'no-external-css'], + external: ['nested-external'] + }, + optimizeDeps: { + include: [ + 'no-external-cjs', + 'import-builtin-cjs', + 'optimized-with-nested-external', + 'optimized-cjs-with-nested-external' + ], + exclude: ['nested-external'] } }) // use vite's connect instance as middleware diff --git a/playground/ssr-deps/src/app.js b/playground/ssr-deps/src/app.js index f5b0ad810421fb..26df58bde7439d 100644 --- a/playground/ssr-deps/src/app.js +++ b/playground/ssr-deps/src/app.js @@ -12,6 +12,14 @@ import requireAbsolute from 'require-absolute' import noExternalCjs from 'no-external-cjs' import importBuiltinCjs from 'import-builtin-cjs' +// This import will set a 'Hello World!" message in the nested-external non-entry dependency +import 'non-optimized-with-nested-external' + +// These two are optimized and get the message from nested-external, if the dependency is +// not properly externalized and ends up bundled, the message will be undefined +import optimizedWithNestedExternal from 'optimized-with-nested-external' +import optimizedCjsWithNestedExternal from 'optimized-cjs-with-nested-external' + export async function render(url, rootDir) { let html = '' @@ -53,5 +61,12 @@ export async function render(url, rootDir) { const importBuiltinCjsMessage = importBuiltinCjs.hello() html += `\n

message from import-builtin-cjs: ${importBuiltinCjsMessage}

` + const optimizedWithNestedExternalMessage = optimizedWithNestedExternal.hello() + html += `\n

message from optimized-with-nested-external: ${optimizedWithNestedExternalMessage}

` + + const optimizedCjsWithNestedExternalMessage = + optimizedCjsWithNestedExternal.hello() + html += `\n

message from optimized-cjs-with-nested-external: ${optimizedCjsWithNestedExternalMessage}

` + return html + '\n' } diff --git a/packages/vite/src/node/ssr/__tests__/fixtures/ssrModuleLoader-bad.js b/playground/ssr-vue/__tests__/fixtures/ssrModuleLoader-bad.js similarity index 100% rename from packages/vite/src/node/ssr/__tests__/fixtures/ssrModuleLoader-bad.js rename to playground/ssr-vue/__tests__/fixtures/ssrModuleLoader-bad.js diff --git a/playground/ssr-vue/__tests__/serve.ts b/playground/ssr-vue/__tests__/serve.ts index 99b02e10c94a19..e03810017bc04e 100644 --- a/playground/ssr-vue/__tests__/serve.ts +++ b/playground/ssr-vue/__tests__/serve.ts @@ -3,10 +3,13 @@ import path from 'node:path' import kill from 'kill-port' +import type { ViteDevServer } from 'vite' import { hmrPorts, isBuild, ports, rootDir } from '~utils' export const port = ports['ssr-vue'] +export let viteServer: ViteDevServer + export async function serve(): Promise<{ close(): Promise }> { if (isBuild) { // build first @@ -44,6 +47,7 @@ export async function serve(): Promise<{ close(): Promise }> { isBuild, hmrPorts['ssr-vue'] ) + viteServer = vite return new Promise((resolve, reject) => { try { diff --git a/playground/ssr-vue/__tests__/ssr-vue.spec.ts b/playground/ssr-vue/__tests__/ssr-vue.spec.ts index 7b61c2634f7c5b..7d4d1ec0f7ab87 100644 --- a/playground/ssr-vue/__tests__/ssr-vue.spec.ts +++ b/playground/ssr-vue/__tests__/ssr-vue.spec.ts @@ -1,4 +1,5 @@ import { resolve } from 'node:path' +import { fileURLToPath } from 'node:url' import fetch from 'node-fetch' import { port } from './serve' import { @@ -7,7 +8,8 @@ import { getColor, isBuild, page, - untilUpdated + untilUpdated, + viteServer } from '~utils' const url = `http://localhost:${port}/test/` @@ -202,3 +204,33 @@ test.runIf(isBuild)('dynamic css file should be preloaded', async () => { expect(homeHtml).toMatch(file) } }) + +test.runIf(!isBuild)( + 'always throw error when evaluating an wrong SSR module', + async () => { + const __filename = fileURLToPath(import.meta.url) + const badjs = resolve(__filename, '../fixtures/ssrModuleLoader-bad.js') + const THROW_MESSAGE = 'it is an expected error' + + const spy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const expectedErrors = [] + for (const _ of [0, 1]) { + try { + console.log(viteServer) + await viteServer.ssrLoadModule(badjs, { fixStacktrace: true }) + } catch (e) { + expectedErrors.push(e) + } + } + expect(expectedErrors).toHaveLength(2) + expect(expectedErrors[0]).toBe(expectedErrors[1]) + expectedErrors.forEach((error) => { + expect(error?.message).toContain(THROW_MESSAGE) + }) + expect(spy).toBeCalledTimes(1) + const [firstParameter] = spy.mock.calls[0] + expect(firstParameter).toContain('Error when evaluating SSR module') + expect(firstParameter).toContain(THROW_MESSAGE) + spy.mockClear() + } +) diff --git a/playground/tailwind-sourcemap/__tests__/serve.spec.ts b/playground/tailwind-sourcemap/__tests__/serve.spec.ts deleted file mode 100644 index 62e46cc7781482..00000000000000 --- a/playground/tailwind-sourcemap/__tests__/serve.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { isBuild, serverLogs } from '~utils' - -test.runIf(isBuild)('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/tailwind-sourcemap/__tests__/build.spec.ts b/playground/tailwind-sourcemap/__tests__/tailwind-sourcemap.spec.ts similarity index 53% rename from playground/tailwind-sourcemap/__tests__/build.spec.ts rename to playground/tailwind-sourcemap/__tests__/tailwind-sourcemap.spec.ts index b30284731a76d9..86238ab800f24e 100644 --- a/playground/tailwind-sourcemap/__tests__/build.spec.ts +++ b/playground/tailwind-sourcemap/__tests__/tailwind-sourcemap.spec.ts @@ -1,5 +1,11 @@ import { isBuild, serverLogs } from '~utils' +test.runIf(!isBuild)('should not output missing source file warning', () => { + serverLogs.forEach((log) => { + expect(log).not.toMatch(/Sourcemap for .+ points to missing source files/) + }) +}) + test.runIf(isBuild)('should not output sourcemap warning (#4939)', () => { serverLogs.forEach((log) => { expect(log).not.toMatch('Sourcemap is likely to be incorrect') diff --git a/playground/tailwind-sourcemap/postcss.config.js b/playground/tailwind-sourcemap/postcss.config.js index eab3760cbc7b42..ad6adc688915c1 100644 --- a/playground/tailwind-sourcemap/postcss.config.js +++ b/playground/tailwind-sourcemap/postcss.config.js @@ -1,3 +1,5 @@ +console.log(__dirname + '/tailwind.config.js') + module.exports = { plugins: { tailwindcss: { config: __dirname + '/tailwind.config.js' } diff --git a/playground/vitestSetup.ts b/playground/vitestSetup.ts index de0877302e5118..7b960811eb3f7e 100644 --- a/playground/vitestSetup.ts +++ b/playground/vitestSetup.ts @@ -35,6 +35,10 @@ export const viteBinPath = path.posix.join( let server: ViteDevServer | http.Server +/** + * Vite Dev Server when testing serve + */ +export let viteServer: ViteDevServer /** * Root of the Vite fixture */ @@ -146,6 +150,7 @@ beforeAll(async (s) => { } if (serve) { server = await serve() + viteServer = mod.viteServer return } } else { @@ -212,7 +217,7 @@ export async function startDefaultServe(): Promise { process.env.VITE_INLINE = 'inline-serve' const testConfig = mergeConfig(options, config || {}) viteConfig = testConfig - server = await (await createServer(testConfig)).listen() + viteServer = server = await (await createServer(testConfig)).listen() // use resolved port/base from server const devBase = server.config.base viteTestUrl = `http://localhost:${server.config.server.port}${ diff --git a/playground/vue-sourcemap/__tests__/__snapshots__/serve.spec.ts.snap b/playground/vue-sourcemap/__tests__/__snapshots__/vue-sourcemap.spec.ts.snap similarity index 85% rename from playground/vue-sourcemap/__tests__/__snapshots__/serve.spec.ts.snap rename to playground/vue-sourcemap/__tests__/__snapshots__/vue-sourcemap.spec.ts.snap index e91b6ce384c562..d2600ee6edccce 100644 --- a/playground/vue-sourcemap/__tests__/__snapshots__/serve.spec.ts.snap +++ b/playground/vue-sourcemap/__tests__/__snapshots__/vue-sourcemap.spec.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1 -exports[`serve:vue-sourcemap > css 1`] = ` +exports[`serve:vue-sourcemap > css > serve-css 1`] = ` { "mappings": ";AAQA;EACE,UAAU;AACZ", "sources": [ @@ -47,7 +47,7 @@ exports[`serve:vue-sourcemap > css 1`] = ` } `; -exports[`serve:vue-sourcemap > css module 1`] = ` +exports[`serve:vue-sourcemap > css module > serve-css-module 1`] = ` { "mappings": ";AAcA;EACE,UAAU;AACZ", "sources": [ @@ -94,7 +94,7 @@ exports[`serve:vue-sourcemap > css module 1`] = ` } `; -exports[`serve:vue-sourcemap > css scoped 1`] = ` +exports[`serve:vue-sourcemap > css scoped > serve-css-scoped 1`] = ` { "mappings": ";AAoBA;EACE,UAAU;AACZ", "sources": [ @@ -141,7 +141,7 @@ exports[`serve:vue-sourcemap > css scoped 1`] = ` } `; -exports[`serve:vue-sourcemap > js 1`] = ` +exports[`serve:vue-sourcemap > js > serve-js 1`] = ` { "mappings": "AAKA,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;;;;;;AAGP;AACd,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;;;;;;;;;;;wBARlB,oBAAiB,WAAd,MAAU", "sources": [ @@ -165,7 +165,7 @@ console.log('setup') } `; -exports[`serve:vue-sourcemap > less with additionalData 1`] = ` +exports[`serve:vue-sourcemap > less with additionalData > serve-less-with-additionalData 1`] = ` { "mappings": "AAKA;EACE", "sources": [ @@ -187,7 +187,7 @@ exports[`serve:vue-sourcemap > less with additionalData 1`] = ` } `; -exports[`serve:vue-sourcemap > no script 1`] = ` +exports[`serve:vue-sourcemap > no script > serve-no-script 1`] = ` { "mappings": ";;;wBACE,oBAAwB,WAArB,aAAiB", "sourceRoot": "", @@ -204,7 +204,7 @@ exports[`serve:vue-sourcemap > no script 1`] = ` } `; -exports[`serve:vue-sourcemap > no template 1`] = ` +exports[`serve:vue-sourcemap > no template > serve-no-template 1`] = ` { "mappings": "AACA,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;;;;;;AAGP;AACd,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;;;;;;;", "sources": [ @@ -224,7 +224,7 @@ console.log('setup') } `; -exports[`serve:vue-sourcemap > sass 1`] = ` +exports[`serve:vue-sourcemap > sass > serve-sass 1`] = ` { "mappings": "AAKA;EACE", "sources": [ @@ -245,7 +245,7 @@ exports[`serve:vue-sourcemap > sass 1`] = ` } `; -exports[`serve:vue-sourcemap > sass with import 1`] = ` +exports[`serve:vue-sourcemap > sass with import > serve-sass-with-import 1`] = ` { "mappings": "AAAA;EACE;;ACOF;EACE", "sources": [ @@ -273,7 +273,7 @@ exports[`serve:vue-sourcemap > sass with import 1`] = ` } `; -exports[`serve:vue-sourcemap > src imported 1`] = ` +exports[`serve:vue-sourcemap > src imported > serve-src-imported 1`] = ` { "mappings": "AAAA;EACE,UAAU;AACZ", "sources": [ @@ -289,7 +289,7 @@ exports[`serve:vue-sourcemap > src imported 1`] = ` } `; -exports[`serve:vue-sourcemap > src imported sass 1`] = ` +exports[`serve:vue-sourcemap > src imported sass > serve-src-imported-sass 1`] = ` { "mappings": "AAAA;EACE;;ACCF;EACE", "sources": [ @@ -310,7 +310,7 @@ exports[`serve:vue-sourcemap > src imported sass 1`] = ` } `; -exports[`serve:vue-sourcemap > ts 1`] = ` +exports[`serve:vue-sourcemap > ts > serve-ts 1`] = ` { "mappings": ";AAKA,QAAQ,IAAI,WAAW;;;;;AAIvB,YAAQ,IAAI,UAAU;;;;;;;;uBARpB,oBAAiB,WAAd,MAAU", "sources": [ diff --git a/playground/vue-sourcemap/__tests__/build.spec.ts b/playground/vue-sourcemap/__tests__/build.spec.ts deleted file mode 100644 index b30284731a76d9..00000000000000 --- a/playground/vue-sourcemap/__tests__/build.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { isBuild, serverLogs } from '~utils' - -test.runIf(isBuild)('should not output sourcemap warning (#4939)', () => { - serverLogs.forEach((log) => { - expect(log).not.toMatch('Sourcemap is likely to be incorrect') - }) -}) diff --git a/playground/vue-sourcemap/__tests__/serve.spec.ts b/playground/vue-sourcemap/__tests__/vue-sourcemap.spec.ts similarity index 66% rename from playground/vue-sourcemap/__tests__/serve.spec.ts rename to playground/vue-sourcemap/__tests__/vue-sourcemap.spec.ts index 1ace0d7d1f4306..2241af413eee94 100644 --- a/playground/vue-sourcemap/__tests__/serve.spec.ts +++ b/playground/vue-sourcemap/__tests__/vue-sourcemap.spec.ts @@ -2,8 +2,10 @@ import { URL } from 'node:url' import { extractSourcemap, formatSourcemapForSnapshot, + isBuild, isServe, - page + page, + serverLogs } from '~utils' describe.runIf(isServe)('serve:vue-sourcemap', () => { @@ -22,62 +24,70 @@ describe.runIf(isServe)('serve:vue-sourcemap', () => { const res = await page.request.get(new URL('./Js.vue', page.url()).href) const js = await res.text() const map = extractSourcemap(js) - expect(formatSourcemapForSnapshot(map)).toMatchSnapshot() + expect(formatSourcemapForSnapshot(map)).toMatchSnapshot('serve-js') }) test('ts', async () => { const res = await page.request.get(new URL('./Ts.vue', page.url()).href) const js = await res.text() const map = extractSourcemap(js) - expect(formatSourcemapForSnapshot(map)).toMatchSnapshot() + expect(formatSourcemapForSnapshot(map)).toMatchSnapshot('serve-ts') }) test('css', async () => { const css = await getStyleTagContentIncluding('.css ') const map = extractSourcemap(css) - expect(formatSourcemapForSnapshot(map)).toMatchSnapshot() + expect(formatSourcemapForSnapshot(map)).toMatchSnapshot('serve-css') }) test('css module', async () => { const css = await getStyleTagContentIncluding('._css-module_') const map = extractSourcemap(css) - expect(formatSourcemapForSnapshot(map)).toMatchSnapshot() + expect(formatSourcemapForSnapshot(map)).toMatchSnapshot('serve-css-module') }) test('css scoped', async () => { const css = await getStyleTagContentIncluding('.css-scoped[data-v-') const map = extractSourcemap(css) - expect(formatSourcemapForSnapshot(map)).toMatchSnapshot() + expect(formatSourcemapForSnapshot(map)).toMatchSnapshot('serve-css-scoped') }) test('sass', async () => { const css = await getStyleTagContentIncluding('.sass ') const map = extractSourcemap(css) - expect(formatSourcemapForSnapshot(map)).toMatchSnapshot() + expect(formatSourcemapForSnapshot(map)).toMatchSnapshot('serve-sass') }) test('sass with import', async () => { const css = await getStyleTagContentIncluding('.sass-with-import ') const map = extractSourcemap(css) - expect(formatSourcemapForSnapshot(map)).toMatchSnapshot() + expect(formatSourcemapForSnapshot(map)).toMatchSnapshot( + 'serve-sass-with-import' + ) }) test('less with additionalData', async () => { const css = await getStyleTagContentIncluding('.less ') const map = extractSourcemap(css) - expect(formatSourcemapForSnapshot(map)).toMatchSnapshot() + expect(formatSourcemapForSnapshot(map)).toMatchSnapshot( + 'serve-less-with-additionalData' + ) }) test('src imported', async () => { const css = await getStyleTagContentIncluding('.src-import[data-v-') const map = extractSourcemap(css) - expect(formatSourcemapForSnapshot(map)).toMatchSnapshot() + expect(formatSourcemapForSnapshot(map)).toMatchSnapshot( + 'serve-src-imported' + ) }) test('src imported sass', async () => { const css = await getStyleTagContentIncluding('.src-import-sass[data-v-') const map = extractSourcemap(css) - expect(formatSourcemapForSnapshot(map)).toMatchSnapshot() + expect(formatSourcemapForSnapshot(map)).toMatchSnapshot( + 'serve-src-imported-sass' + ) }) test('no script', async () => { @@ -86,7 +96,7 @@ describe.runIf(isServe)('serve:vue-sourcemap', () => { ) const js = await res.text() const map = extractSourcemap(js) - expect(formatSourcemapForSnapshot(map)).toMatchSnapshot() + expect(formatSourcemapForSnapshot(map)).toMatchSnapshot('serve-no-script') }) test('no template', async () => { @@ -95,6 +105,12 @@ describe.runIf(isServe)('serve:vue-sourcemap', () => { ) const js = await res.text() const map = extractSourcemap(js) - expect(formatSourcemapForSnapshot(map)).toMatchSnapshot() + expect(formatSourcemapForSnapshot(map)).toMatchSnapshot('serve-no-template') + }) +}) + +test.runIf(isBuild)('should not output sourcemap warning (#4939)', () => { + serverLogs.forEach((log) => { + expect(log).not.toMatch('Sourcemap is likely to be incorrect') }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d93a1cc430bcd3..d63fd7c1ce006c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -343,10 +343,12 @@ importers: playground/alias: specifiers: + '@vue/shared': ^3.2.37 aliased-module: file:./dir/module resolve-linked: workspace:* vue: ^3.2.37 dependencies: + '@vue/shared': 3.2.37 aliased-module: file:playground/alias/dir/module vue: 3.2.37 devDependencies: @@ -737,6 +739,8 @@ importers: playground/preload: specifiers: '@vitejs/plugin-vue': workspace:* + dep-a: file:./dep-a + dep-including-a: file:./dep-including-a terser: ^5.14.1 vue: ^3.2.37 vue-router: ^4.0.16 @@ -745,8 +749,19 @@ importers: vue-router: 4.0.16_vue@3.2.37 devDependencies: '@vitejs/plugin-vue': link:../../packages/plugin-vue + dep-a: file:playground/preload/dep-a + dep-including-a: file:playground/preload/dep-including-a terser: 5.14.1 + playground/preload/dep-a: + specifiers: {} + + playground/preload/dep-including-a: + specifiers: + dep-a: file:../dep-a + dependencies: + dep-a: file:playground/preload/dep-a + playground/preserve-symlinks: specifiers: '@symlinks/moduleA': link:./moduleA @@ -867,8 +882,11 @@ importers: import-builtin-cjs: file:./import-builtin-cjs no-external-cjs: file:./no-external-cjs no-external-css: file:./no-external-css + non-optimized-with-nested-external: file:./non-optimized-with-nested-external object-assigned-exports: file:./object-assigned-exports only-object-assigned-exports: file:./only-object-assigned-exports + optimized-cjs-with-nested-external: file:./optimized-with-nested-external + optimized-with-nested-external: file:./optimized-with-nested-external primitive-export: file:./primitive-export read-file-content: file:./read-file-content require-absolute: file:./require-absolute @@ -881,8 +899,11 @@ importers: import-builtin-cjs: file:playground/ssr-deps/import-builtin-cjs no-external-cjs: file:playground/ssr-deps/no-external-cjs no-external-css: file:playground/ssr-deps/no-external-css + non-optimized-with-nested-external: file:playground/ssr-deps/non-optimized-with-nested-external object-assigned-exports: file:playground/ssr-deps/object-assigned-exports only-object-assigned-exports: file:playground/ssr-deps/only-object-assigned-exports + optimized-cjs-with-nested-external: file:playground/ssr-deps/optimized-with-nested-external + optimized-with-nested-external: file:playground/ssr-deps/optimized-with-nested-external primitive-export: file:playground/ssr-deps/primitive-export read-file-content: file:playground/ssr-deps/read-file-content require-absolute: file:playground/ssr-deps/require-absolute @@ -906,18 +927,39 @@ importers: playground/ssr-deps/import-builtin-cjs: specifiers: {} + playground/ssr-deps/nested-external: + specifiers: {} + playground/ssr-deps/no-external-cjs: specifiers: {} playground/ssr-deps/no-external-css: specifiers: {} + playground/ssr-deps/non-optimized-with-nested-external: + specifiers: + nested-external: file:../nested-external + dependencies: + nested-external: file:playground/ssr-deps/nested-external + playground/ssr-deps/object-assigned-exports: specifiers: {} playground/ssr-deps/only-object-assigned-exports: specifiers: {} + playground/ssr-deps/optimized-cjs-with-nested-external: + specifiers: + nested-external: file:../nested-external + dependencies: + nested-external: file:playground/ssr-deps/nested-external + + playground/ssr-deps/optimized-with-nested-external: + specifiers: + nested-external: file:../nested-external + dependencies: + nested-external: file:playground/ssr-deps/nested-external + playground/ssr-deps/primitive-export: specifiers: {} @@ -8866,6 +8908,19 @@ packages: version: 0.0.0 dev: false + file:playground/preload/dep-a: + resolution: {directory: playground/preload/dep-a, type: directory} + name: dep-a + version: 0.0.0 + + file:playground/preload/dep-including-a: + resolution: {directory: playground/preload/dep-including-a, type: directory} + name: dep-including-a + version: 0.0.0 + dependencies: + dep-a: file:playground/preload/dep-a + dev: true + file:playground/react/jsx-entry: resolution: {directory: playground/react/jsx-entry, type: directory} name: jsx-entry @@ -8898,6 +8953,12 @@ packages: version: 0.0.0 dev: false + file:playground/ssr-deps/nested-external: + resolution: {directory: playground/ssr-deps/nested-external, type: directory} + name: nested-external + version: 0.0.0 + dev: false + file:playground/ssr-deps/no-external-cjs: resolution: {directory: playground/ssr-deps/no-external-cjs, type: directory} name: no-external-cjs @@ -8910,6 +8971,14 @@ packages: version: 0.0.0 dev: false + file:playground/ssr-deps/non-optimized-with-nested-external: + resolution: {directory: playground/ssr-deps/non-optimized-with-nested-external, type: directory} + name: non-optimized-with-nested-external + version: 0.0.0 + dependencies: + nested-external: file:playground/ssr-deps/nested-external + dev: false + file:playground/ssr-deps/object-assigned-exports: resolution: {directory: playground/ssr-deps/object-assigned-exports, type: directory} name: object-assigned-exports @@ -8922,6 +8991,14 @@ packages: version: 0.0.0 dev: false + file:playground/ssr-deps/optimized-with-nested-external: + resolution: {directory: playground/ssr-deps/optimized-with-nested-external, type: directory} + name: optimized-with-nested-external + version: 0.0.0 + dependencies: + nested-external: file:playground/ssr-deps/nested-external + dev: false + file:playground/ssr-deps/primitive-export: resolution: {directory: playground/ssr-deps/primitive-export, type: directory} name: primitive-export