From 1bc443cd93d125d4f4949cbef122bd4417cc8c7d Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Sun, 27 Jun 2021 19:22:31 -0400 Subject: [PATCH 1/3] fix: move package.json cache into ResolvedConfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …so changes to package.json invalidate the cache. This is useful for linked packages and when a dependency is upgraded. This commit also fixes a bug with `pkg.sideEffects` handling by including `preserveSymlinks` in the cache key. --- packages/vite/src/node/build.ts | 2 + packages/vite/src/node/config.ts | 4 + packages/vite/src/node/index.ts | 10 +- packages/vite/src/node/packages.ts | 164 ++++++++++++++++++++++ packages/vite/src/node/plugins/index.ts | 1 + packages/vite/src/node/plugins/resolve.ts | 99 +++---------- packages/vite/src/node/server/index.ts | 13 ++ 7 files changed, 205 insertions(+), 88 deletions(-) create mode 100644 packages/vite/src/node/packages.ts diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 3a72a666ff4752..a8eeda6ec97a93 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -40,6 +40,7 @@ import { DepOptimizationMetadata } from './optimizer' import { scanImports } from './optimizer/scan' import { assetImportMetaUrlPlugin } from './plugins/assetImportMetaUrl' import { loadFallbackPlugin } from './plugins/loadFallback' +import { watchPackageDataPlugin } from './packages' export interface BuildOptions { /** @@ -348,6 +349,7 @@ export function resolveBuildPlugins(config: ResolvedConfig): { const options = config.build return { pre: [ + watchPackageDataPlugin(config), buildHtmlPlugin(config), commonjsPlugin(options.commonjsOptions), dataURIPlugin(), diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index c627fc5c502886..73899d3f7eb8d8 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -47,6 +47,7 @@ import { import aliasPlugin from '@rollup/plugin-alias' import { build } from 'esbuild' import { performance } from 'perf_hooks' +import { PackageCache } from './packages' const debug = createDebugger('vite:config') @@ -239,6 +240,8 @@ export type ResolvedConfig = Readonly< logger: Logger createResolver: (options?: Partial) => ResolveFn optimizeDeps: Omit + /** @internal */ + packageCache: PackageCache } > @@ -458,6 +461,7 @@ export async function resolveConfig( return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file) }, logger, + packageCache: new Map(), createResolver, optimizeDeps: { ...config.optimizeDeps, diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 084aa66b03f141..5b92990f83a221 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -6,7 +6,8 @@ export { optimizeDeps } from './optimizer' export { send } from './server/send' export { createLogger, printHttpServerUrls } from './logger' export { transformWithEsbuild } from './plugins/esbuild' -export { resolvePackageData, resolvePackageEntry } from './plugins/resolve' +export { resolvePackageEntry } from './plugins/resolve' +export { resolvePackageData } from './packages' export { normalizePath } from './utils' // additional types @@ -34,6 +35,7 @@ export type { DepOptimizationOptions } from './optimizer' export type { Plugin } from './plugin' +export type { PackageCache, PackageData } from './packages' export type { Logger, LogOptions, @@ -60,11 +62,7 @@ export type { JsonOptions } from './plugins/json' export type { TransformOptions as EsbuildTransformOptions } from 'esbuild' export type { ESBuildOptions, ESBuildTransformResult } from './plugins/esbuild' export type { Manifest, ManifestChunk } from './plugins/manifest' -export type { - PackageData, - ResolveOptions, - InternalResolveOptions -} from './plugins/resolve' +export type { ResolveOptions, InternalResolveOptions } from './plugins/resolve' export type { WebSocketServer } from './server/ws' export type { PluginContainer } from './server/pluginContainer' export type { ModuleGraph, ModuleNode, ResolvedUrl } from './server/moduleGraph' diff --git a/packages/vite/src/node/packages.ts b/packages/vite/src/node/packages.ts new file mode 100644 index 00000000000000..e5d96aa09bab9f --- /dev/null +++ b/packages/vite/src/node/packages.ts @@ -0,0 +1,164 @@ +import fs from 'fs' +import path from 'path' +import { createFilter } from '@rollup/pluginutils' +import { createDebugger, resolveFrom } from './utils' +import { ResolvedConfig } from './config' +import { Plugin } from './plugin' + +const isDebug = process.env.DEBUG +const debug = createDebugger('vite:resolve-details', { + onlyWhenFocused: true +}) + +/** Cache for package.json resolution and package.json contents */ +export type PackageCache = Map + +export interface PackageData { + dir: string + hasSideEffects: (id: string) => boolean | 'no-treeshake' + webResolvedImports: Record + nodeResolvedImports: Record + setResolvedCache: (key: string, entry: string, targetWeb: boolean) => void + getResolvedCache: (key: string, targetWeb: boolean) => string | undefined + data: { + [field: string]: any + version: string + main: string + module: string + browser: string | Record + exports: string | Record | string[] + dependencies: Record + } +} + +export function invalidatePackageData( + packageCache: PackageCache, + pkgPath: string +) { + packageCache.delete(pkgPath) + const pkgDir = path.dirname(pkgPath) + packageCache.forEach((pkg, cacheKey) => { + if (pkg.dir === pkgDir) { + packageCache.delete(cacheKey) + } + }) +} + +export function resolvePackageData( + id: string, + basedir: string, + preserveSymlinks = false, + packageCache?: PackageCache +): PackageData | null { + let pkg: PackageData | undefined + let cacheKey: string | undefined + if (packageCache) { + cacheKey = `${id}&${basedir}&${preserveSymlinks}` + if ((pkg = packageCache.get(cacheKey))) { + return pkg + } + } + let pkgPath: string | undefined + try { + pkgPath = resolveFrom(`${id}/package.json`, basedir, preserveSymlinks) + pkg = loadPackageData(pkgPath, true, packageCache) + if (packageCache) { + packageCache.set(cacheKey!, pkg) + } + return pkg + } catch (e) { + if (e instanceof SyntaxError) { + isDebug && debug(`Parsing failed: ${pkgPath}`) + } + // Ignore error for missing package.json + else if (e.code !== 'MODULE_NOT_FOUND') { + throw e + } + } + return null +} + +export function loadPackageData( + pkgPath: string, + preserveSymlinks?: boolean, + packageCache?: PackageCache +): PackageData { + if (!preserveSymlinks) { + pkgPath = fs.realpathSync.native(pkgPath) + } + + let cached: PackageData | undefined + if ((cached = packageCache?.get(pkgPath))) { + return cached + } + + const data = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) + const pkgDir = path.dirname(pkgPath) + const { sideEffects } = data + let hasSideEffects: (id: string) => boolean + if (typeof sideEffects === 'boolean') { + hasSideEffects = () => sideEffects + } else if (Array.isArray(sideEffects)) { + hasSideEffects = createFilter(sideEffects, null, { resolve: pkgDir }) + } else { + hasSideEffects = () => true + } + + const pkg: PackageData = { + dir: pkgDir, + data, + hasSideEffects, + webResolvedImports: {}, + nodeResolvedImports: {}, + setResolvedCache(key: string, entry: string, targetWeb: boolean) { + if (targetWeb) { + pkg.webResolvedImports[key] = entry + } else { + pkg.nodeResolvedImports[key] = entry + } + }, + getResolvedCache(key: string, targetWeb: boolean) { + if (targetWeb) { + return pkg.webResolvedImports[key] + } else { + return pkg.nodeResolvedImports[key] + } + } + } + + packageCache?.set(pkgPath, pkg) + return pkg +} + +export function watchPackageDataPlugin(config: ResolvedConfig): Plugin { + const watchQueue = new Set() + let watchFile = (id: string) => { + watchQueue.add(id) + } + + const { packageCache } = config + const setPackageData = packageCache.set.bind(packageCache) + packageCache.set = (id, pkg) => { + if (id.endsWith('.json')) { + watchFile(id) + } + return setPackageData(id, pkg) + } + + return { + name: 'vite:watch-package-data', + buildStart() { + watchFile = this.addWatchFile + watchQueue.forEach(watchFile) + watchQueue.clear() + }, + buildEnd() { + watchFile = (id) => watchQueue.add(id) + }, + watchChange(id) { + if (id.endsWith('/package.json')) { + invalidatePackageData(packageCache, id) + } + } + } +} diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index 0452e4f19a73fb..54a7ae8589b2a6 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -40,6 +40,7 @@ export async function resolvePlugins( root: config.root, isProduction: config.isProduction, isBuild, + packageCache: config.packageCache, ssrConfig: config.ssr, asSrc: true }), diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index f80cfdeb18608b..da06b9397f17bc 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -30,9 +30,14 @@ import { getTsSrcPath } from '../utils' import { ViteDevServer, SSROptions } from '..' -import { createFilter } from '@rollup/pluginutils' import { PartialResolvedId } from 'rollup' import { resolve as _resolveExports } from 'resolve.exports' +import { + loadPackageData, + PackageCache, + PackageData, + resolvePackageData +} from '../packages' // special id for paths marked with browser: false // https://github.com/defunctzombie/package-browser-field-spec#ignore-a-module @@ -56,6 +61,7 @@ export interface InternalResolveOptions extends ResolveOptions { isBuild: boolean isProduction: boolean ssrConfig?: SSROptions + packageCache?: PackageCache /** * src code mode also attempts the following: * - resolving /xxx as URLs @@ -415,11 +421,15 @@ function tryResolveFile( } else if (tryIndex) { if (!skipPackageJson) { const pkgPath = file + '/package.json' - if (fs.existsSync(pkgPath)) { + try { // path points to a node package - const pkg = loadPackageData(pkgPath) + const pkg = loadPackageData(pkgPath, options.preserveSymlinks) const resolved = resolvePackageEntry(file, pkg, targetWeb, options) return resolved + } catch (e) { + if (e.code !== 'ENOENT') { + throw e + } } } const index = tryFsResolve(file + '/index', options) @@ -457,7 +467,7 @@ export function tryNodeResolve( server?: ViteDevServer, ssr?: boolean ): PartialResolvedId | undefined { - const { root, dedupe, isBuild } = options + const { root, dedupe, isBuild, preserveSymlinks, packageCache } = options // split id by last '>' for nested selected packages, for example: // 'foo > bar > baz' => 'foo > bar' & 'baz' @@ -508,12 +518,12 @@ export function tryNodeResolve( // nested node module, step-by-step resolve to the basedir of the nestedPath if (nestedRoot) { - basedir = nestedResolveFrom(nestedRoot, basedir, options.preserveSymlinks) + basedir = nestedResolveFrom(nestedRoot, basedir, preserveSymlinks) } - let pkg: PackageData | undefined + let pkg: PackageData | null = null const pkgId = possiblePkgIds.reverse().find((pkgId) => { - pkg = resolvePackageData(pkgId, basedir, options.preserveSymlinks) + pkg = resolvePackageData(pkgId, basedir, preserveSymlinks, packageCache) return pkg })! @@ -650,81 +660,6 @@ export function tryOptimizedResolve( } } -export interface PackageData { - dir: string - hasSideEffects: (id: string) => boolean - webResolvedImports: Record - nodeResolvedImports: Record - setResolvedCache: (key: string, entry: string, targetWeb: boolean) => void - getResolvedCache: (key: string, targetWeb: boolean) => string | undefined - data: { - [field: string]: any - version: string - main: string - module: string - browser: string | Record - exports: string | Record | string[] - dependencies: Record - } -} - -const packageCache = new Map() - -export function resolvePackageData( - id: string, - basedir: string, - preserveSymlinks = false -): PackageData | undefined { - const cacheKey = id + basedir - if (packageCache.has(cacheKey)) { - return packageCache.get(cacheKey) - } - try { - const pkgPath = resolveFrom(`${id}/package.json`, basedir, preserveSymlinks) - return loadPackageData(pkgPath, cacheKey) - } catch (e) { - isDebug && debug(`${chalk.red(`[failed loading package.json]`)} ${id}`) - } -} - -function loadPackageData(pkgPath: string, cacheKey = pkgPath) { - const data = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) - const pkgDir = path.dirname(pkgPath) - const { sideEffects } = data - let hasSideEffects: (id: string) => boolean - if (typeof sideEffects === 'boolean') { - hasSideEffects = () => sideEffects - } else if (Array.isArray(sideEffects)) { - hasSideEffects = createFilter(sideEffects, null, { resolve: pkgDir }) - } else { - hasSideEffects = () => true - } - - const pkg: PackageData = { - dir: pkgDir, - data, - hasSideEffects, - webResolvedImports: {}, - nodeResolvedImports: {}, - setResolvedCache(key: string, entry: string, targetWeb: boolean) { - if (targetWeb) { - pkg.webResolvedImports[key] = entry - } else { - pkg.nodeResolvedImports[key] = entry - } - }, - getResolvedCache(key: string, targetWeb: boolean) { - if (targetWeb) { - return pkg.webResolvedImports[key] - } else { - return pkg.nodeResolvedImports[key] - } - } - } - packageCache.set(cacheKey, pkg) - return pkg -} - export function resolvePackageEntry( id: string, { dir, data, setResolvedCache, getResolvedCache }: PackageData, diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index a9fe5431ee59ae..65db5964b43384 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -60,6 +60,7 @@ import { resolveHostname } from '../utils' import { searchForWorkspaceRoot } from './searchRoot' import { CLIENT_DIR } from '../constants' import { printCommonServerUrls } from '../logger' +import { invalidatePackageData } from '../packages' export { searchForWorkspaceRoot } from './searchRoot' @@ -390,8 +391,20 @@ export async function createServer( process.stdin.on('end', exitProcess) } + const { packageCache } = config + const setPackageData = packageCache.set.bind(packageCache) + packageCache.set = (id, pkg) => { + if (id.endsWith('.json')) { + watcher.add(id) + } + return setPackageData(id, pkg) + } + watcher.on('change', async (file) => { file = normalizePath(file) + if (file.endsWith('/package.json')) { + return invalidatePackageData(packageCache, file) + } // invalidate module graph cache on file change moduleGraph.onFileChange(file) if (serverConfig.hmr !== false) { From 0860a433f53fde3a48a2a7157922bb36bc1fcb6c Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Tue, 16 Nov 2021 22:14:31 -0500 Subject: [PATCH 2/3] chore: fix typescript error --- packages/vite/src/node/plugins/resolve.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index da06b9397f17bc..e68c62b8c0aea0 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -521,9 +521,9 @@ export function tryNodeResolve( basedir = nestedResolveFrom(nestedRoot, basedir, preserveSymlinks) } - let pkg: PackageData | null = null + let pkg: PackageData | undefined const pkgId = possiblePkgIds.reverse().find((pkgId) => { - pkg = resolvePackageData(pkgId, basedir, preserveSymlinks, packageCache) + pkg = resolvePackageData(pkgId, basedir, preserveSymlinks, packageCache)! return pkg })! From c310295b77de639d1c0a717390ada602581362e5 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Thu, 18 Nov 2021 14:34:24 -0500 Subject: [PATCH 3/3] Update packages/vite/src/node/packages.ts Co-authored-by: Shinigami --- packages/vite/src/node/packages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/packages.ts b/packages/vite/src/node/packages.ts index e5d96aa09bab9f..8b769020a05853 100644 --- a/packages/vite/src/node/packages.ts +++ b/packages/vite/src/node/packages.ts @@ -34,7 +34,7 @@ export interface PackageData { export function invalidatePackageData( packageCache: PackageCache, pkgPath: string -) { +): void { packageCache.delete(pkgPath) const pkgDir = path.dirname(pkgPath) packageCache.forEach((pkg, cacheKey) => {