Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Persistent cache #4120

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion packages/vite/src/node/__tests__/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { injectQuery, isWindows } from '../utils'
import { asyncPool, injectQuery, isWindows } from '../utils'

if (isWindows) {
// this test will work incorrectly on unix systems
Expand Down Expand Up @@ -38,3 +38,19 @@ test('path with unicode, space, and %', () => {
'/usr/vite/東京 %20 hello?direct'
)
})

test('asyncPool', async () => {
const finished: number[] = []
await asyncPool({
concurrency: 3,
items: [1, 6, 4, 2, 5, 3],
fn: (nb) =>
new Promise<void>((resolve) =>
setTimeout(() => {
finished.push(nb)
resolve()
}, nb * 10)
)
})
expect(finished).toEqual([1, 2, 4, 6, 3, 5])
})
32 changes: 19 additions & 13 deletions packages/vite/src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
isObject,
lookupFile,
normalizePath,
dynamicImport
dynamicImport,
getShortHash
} from './utils'
import { resolvePlugins } from './plugins'
import colors from 'picocolors'
Expand Down Expand Up @@ -239,6 +240,7 @@ export type ResolvedConfig = Readonly<
'plugins' | 'alias' | 'dedupe' | 'assetsInclude' | 'optimizeDeps' | 'worker'
> & {
configFile: string | undefined
configFileHash: string | undefined
configFileDependencies: string[]
inlineConfig: InlineConfig
root: string
Expand Down Expand Up @@ -278,6 +280,7 @@ export async function resolveConfig(
defaultMode = 'development'
): Promise<ResolvedConfig> {
let config = inlineConfig
let configFileHash: string | undefined
let configFileDependencies: string[] = []
let mode = inlineConfig.mode || defaultMode

Expand All @@ -304,6 +307,7 @@ export async function resolveConfig(
if (loadResult) {
config = mergeConfig(loadResult.config, config)
configFile = loadResult.path
configFileHash = loadResult.hash
configFileDependencies = loadResult.dependencies
}
}
Expand Down Expand Up @@ -467,6 +471,7 @@ export async function resolveConfig(
const resolved: ResolvedConfig = {
...config,
configFile: configFile ? normalizePath(configFile) : undefined,
configFileHash,
configFileDependencies,
inlineConfig,
root: resolvedRoot,
Expand Down Expand Up @@ -823,14 +828,14 @@ export async function loadConfigFromFile(
path: string
config: UserConfig
dependencies: string[]
hash: string
} | null> {
const start = performance.now()
const getTime = () => `${(performance.now() - start).toFixed(2)}ms`

let resolvedPath: string | undefined
let isTS = false
let isESM = false
let dependencies: string[] = []

// check package.json for type: "module" and set `isMjs` to true
try {
Expand Down Expand Up @@ -887,18 +892,18 @@ export async function loadConfigFromFile(
}

try {
let userConfig: UserConfigExport | undefined
let bundledConfig: { code: string; dependencies: string[] }
let userConfig: UserConfigExport

if (isESM) {
const fileUrl = require('url').pathToFileURL(resolvedPath)
const bundled = await bundleConfigFile(resolvedPath, true)
dependencies = bundled.dependencies
bundledConfig = await bundleConfigFile(resolvedPath, true)
if (isTS) {
// before we can register loaders without requiring users to run node
// with --experimental-loader themselves, we have to do a hack here:
// bundle the config file w/ ts transforms first, write it to disk,
// load it with native Node ESM, then delete the file.
fs.writeFileSync(resolvedPath + '.js', bundled.code)
fs.writeFileSync(resolvedPath + '.js', bundledConfig.code)
userConfig = (await dynamicImport(`${fileUrl}.js?t=${Date.now()}`))
.default
fs.unlinkSync(resolvedPath + '.js')
Expand All @@ -910,13 +915,13 @@ export async function loadConfigFromFile(
userConfig = (await dynamicImport(`${fileUrl}?t=${Date.now()}`)).default
debug(`native esm config loaded in ${getTime()}`, fileUrl)
}
}

if (!userConfig) {
} else {
// Bundle config file and transpile it to cjs using esbuild.
const bundled = await bundleConfigFile(resolvedPath)
dependencies = bundled.dependencies
userConfig = await loadConfigFromBundledFile(resolvedPath, bundled.code)
bundledConfig = await bundleConfigFile(resolvedPath)
userConfig = await loadConfigFromBundledFile(
resolvedPath,
bundledConfig.code
)
debug(`bundled config file loaded in ${getTime()}`)
}

Expand All @@ -929,7 +934,8 @@ export async function loadConfigFromFile(
return {
path: normalizePath(resolvedPath),
config,
dependencies
hash: getShortHash(bundledConfig.code),
dependencies: bundledConfig.dependencies
}
} catch (e) {
createLogger(logLevel).error(
Expand Down
49 changes: 17 additions & 32 deletions packages/vite/src/node/optimizer/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import fs from 'fs'
import path from 'path'
import colors from 'picocolors'
import { createHash } from 'crypto'
import type { BuildOptions as EsbuildBuildOptions } from 'esbuild'
import { build } from 'esbuild'
import type { ResolvedConfig } from '../config'
import {
createDebugger,
emptyDir,
lookupFile,
normalizePath,
writeFile,
flattenId,
normalizeId
normalizeId,
getLockAndConfigHash,
getShortHash
} from '../utils'
import { esbuildDepPlugin } from './esbuildDepPlugin'
import { init, parse } from 'es-module-lexer'
Expand Down Expand Up @@ -115,7 +115,7 @@ export async function optimizeDeps(
command: 'build'
}

const { root, logger, cacheDir } = config
const { logger, cacheDir } = config
const log = asCommand ? logger.info : debug

if (!cacheDir) {
Expand All @@ -124,7 +124,7 @@ export async function optimizeDeps(
}

const dataPath = path.join(cacheDir, '_metadata.json')
const mainHash = getDepHash(root, config)
const mainHash = getDepHash(config)
const data: DepOptimizationMetadata = {
hash: mainHash,
browserHash: mainHash,
Expand Down Expand Up @@ -164,10 +164,7 @@ export async function optimizeDeps(
}

// update browser hash
data.browserHash = createHash('sha256')
.update(data.hash + JSON.stringify(deps))
.digest('hex')
.substring(0, 8)
data.browserHash = getShortHash(data.hash + JSON.stringify(deps))

const missingIds = Object.keys(missing)
if (missingIds.length) {
Expand Down Expand Up @@ -382,30 +379,18 @@ function isSingleDefaultExport(exports: readonly string[]) {
return exports.length === 1 && exports[0] === 'default'
}

const lockfileFormats = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml']

function getDepHash(root: string, config: ResolvedConfig): string {
let content = lookupFile(root, lockfileFormats) || ''
function getDepHash(config: ResolvedConfig): string {
// also take config into account
// only a subset of config options that can affect dep optimization
content += JSON.stringify(
{
mode: config.mode,
root: config.root,
resolve: config.resolve,
assetsInclude: config.assetsInclude,
plugins: config.plugins.map((p) => p.name),
optimizeDeps: {
include: config.optimizeDeps?.include,
exclude: config.optimizeDeps?.exclude
}
},
(_, value) => {
if (typeof value === 'function' || value instanceof RegExp) {
return value.toString()
}
return value
return getLockAndConfigHash(config.root, {
mode: config.mode,
root: config.root,
resolve: config.resolve,
assetsInclude: config.assetsInclude,
plugins: config.plugins.map((p) => p.name),
optimizeDeps: {
include: config.optimizeDeps?.include,
exclude: config.optimizeDeps?.exclude
}
)
return createHash('sha256').update(content).digest('hex').substring(0, 8)
})
}
5 changes: 2 additions & 3 deletions packages/vite/src/node/plugins/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ import fs, { promises as fsp } from 'fs'
import * as mrmime from 'mrmime'
import type { Plugin } from '../plugin'
import type { ResolvedConfig } from '../config'
import { cleanUrl } from '../utils'
import { cleanUrl, getShortHash } from '../utils'
import { FS_PREFIX } from '../constants'
import type { OutputOptions, PluginContext, RenderedChunk } from 'rollup'
import MagicString from 'magic-string'
import { createHash } from 'crypto'
import { normalizePath } from '../utils'

export const assetUrlRE = /__VITE_ASSET__([a-z\d]{8})__(?:\$_(.*?)__)?/g
Expand Down Expand Up @@ -343,7 +342,7 @@ async function fileToBuiltUrl(
}

export function getAssetHash(content: Buffer): string {
return createHash('sha256').update(content).digest('hex').slice(0, 8)
return getShortHash(content)
}

export async function urlToBuiltUrl(
Expand Down
45 changes: 26 additions & 19 deletions packages/vite/src/node/server/__tests__/pluginContainer.spec.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import { watch } from 'chokidar'
import type { UserConfig } from '../../config'
import { resolveConfig } from '../../config'
import type { Plugin } from '../../plugin'
import { ModuleGraph } from '../moduleGraph'
import type { PluginContainer } from '../pluginContainer'
import { createPluginContainer } from '../pluginContainer'

let resolveId: (id: string) => any
let moduleGraph: ModuleGraph

describe('plugin container', () => {
describe('getModuleInfo', () => {
beforeEach(() => {
moduleGraph = new ModuleGraph((id) => resolveId(id))
})

it('can pass metadata between hooks', async () => {
const entryUrl = '/x.js'

Expand Down Expand Up @@ -51,9 +45,8 @@ describe('plugin container', () => {
}
}

const container = await getPluginContainer({
plugins: [plugin]
})
const { moduleGraph, container, close } =
await getPluginContainerAndModuleGraph({ plugins: [plugin] })

const entryModule = await moduleGraph.ensureEntryFromUrl(entryUrl, false)
expect(entryModule.meta).toEqual({ x: 1 })
Expand All @@ -62,7 +55,7 @@ describe('plugin container', () => {
expect(loadResult?.meta).toEqual({ x: 2 })

await container.transform(loadResult.code, entryUrl)
await container.close()
await close()

expect(metaArray).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }])
})
Expand Down Expand Up @@ -90,21 +83,19 @@ describe('plugin container', () => {
}
}

const container = await getPluginContainer({
plugins: [plugin1, plugin2]
})
const { moduleGraph, container, close } =
await getPluginContainerAndModuleGraph({ plugins: [plugin1, plugin2] })

await moduleGraph.ensureEntryFromUrl(entryUrl, false)
await container.load(entryUrl)
await close()

expect.assertions(1)
})
})
})

async function getPluginContainer(
inlineConfig?: UserConfig
): Promise<PluginContainer> {
async function getPluginContainerAndModuleGraph(inlineConfig?: UserConfig) {
const config = await resolveConfig(
{ configFile: false, ...inlineConfig },
'serve'
Expand All @@ -113,7 +104,23 @@ async function getPluginContainer(
// @ts-ignore: This plugin requires a ViteDevServer instance.
config.plugins = config.plugins.filter((p) => !/pre-alias/.test(p.name))

resolveId = (id) => container.resolveId(id)
const watcher = watch(__dirname, { ignoreInitial: true })

const moduleGraph = new ModuleGraph(
(url) => container.resolveId(url),
(url) => container.load(url),
config,
watcher
)
const container = await createPluginContainer(config, moduleGraph)
return container
return {
moduleGraph,
container,
close: () =>
Promise.all([
container.close(),
moduleGraph.close(false),
watcher.close()
])
}
}
9 changes: 7 additions & 2 deletions packages/vite/src/node/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,12 +328,16 @@ export async function createServer(
...watchOptions
}) as FSWatcher

const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) =>
container.resolveId(url, undefined, { ssr })
const moduleGraph: ModuleGraph = new ModuleGraph(
(url, ssr) => container.resolveId(url, undefined, { ssr }),
(url) => container.load(url),
config,
watcher
)

const container = await createPluginContainer(config, moduleGraph, watcher)
const closeHttpServer = createServerCloseFn(httpServer)
await moduleGraph.loadCache()

// eslint-disable-next-line prefer-const
let exitProcess: () => void
Expand Down Expand Up @@ -387,6 +391,7 @@ export async function createServer(
watcher.close(),
ws.close(),
container.close(),
moduleGraph.close(),
closeHttpServer()
])
},
Expand Down
Loading