From 9b21f69405271f1b864fa934a96adcb0e1a2bc4d Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Wed, 23 Oct 2024 07:15:56 +0100 Subject: [PATCH 1/4] feat: enable dependencies discovery and pre-bundling in ssr environments (#18358) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 翠 / green --- packages/vite/src/node/external.ts | 1 - packages/vite/src/node/optimizer/scan.ts | 20 +++- .../vite/src/node/plugins/importAnalysis.ts | 2 +- packages/vite/src/node/plugins/resolve.ts | 16 +--- packages/vite/src/node/server/environment.ts | 8 +- packages/vite/src/node/ssr/fetchModule.ts | 42 ++++----- packages/vite/src/node/ssr/index.ts | 1 - .../__tests__/basic.spec.ts | 9 -- .../__tests__/environment-react-ssr.spec.ts | 92 +++++++++++++++++++ playground/environment-react-ssr/package.json | 2 + .../environment-react-ssr/vite.config.ts | 8 ++ pnpm-lock.yaml | 14 ++- 12 files changed, 147 insertions(+), 68 deletions(-) delete mode 100644 playground/environment-react-ssr/__tests__/basic.spec.ts create mode 100644 playground/environment-react-ssr/__tests__/environment-react-ssr.spec.ts diff --git a/packages/vite/src/node/external.ts b/packages/vite/src/node/external.ts index 1fef5113a..e55eabb7f 100644 --- a/packages/vite/src/node/external.ts +++ b/packages/vite/src/node/external.ts @@ -87,7 +87,6 @@ export function createIsConfiguredAsExternal( config.command === 'build' ? undefined : importer, resolveOptions, undefined, - true, // try to externalize, will return undefined or an object without // a external flag if it isn't externalizable true, diff --git a/packages/vite/src/node/optimizer/scan.ts b/packages/vite/src/node/optimizer/scan.ts index 4eb6dd883..c8a3020d2 100644 --- a/packages/vite/src/node/optimizer/scan.ts +++ b/packages/vite/src/node/optimizer/scan.ts @@ -253,13 +253,25 @@ async function computeEntries(environment: ScanEnvironment) { if (explicitEntryPatterns) { entries = await globEntries(explicitEntryPatterns, environment) } else if (buildInput) { - const resolvePath = (p: string) => path.resolve(environment.config.root, p) + const resolvePath = async (p: string) => { + const id = ( + await environment.pluginContainer.resolveId(p, undefined, { + scan: true, + }) + )?.id + if (id === undefined) { + throw new Error( + `failed to resolve rollupOptions.input value: ${JSON.stringify(p)}.`, + ) + } + return id + } if (typeof buildInput === 'string') { - entries = [resolvePath(buildInput)] + entries = [await resolvePath(buildInput)] } else if (Array.isArray(buildInput)) { - entries = buildInput.map(resolvePath) + entries = await Promise.all(buildInput.map(resolvePath)) } else if (isObject(buildInput)) { - entries = Object.values(buildInput).map(resolvePath) + entries = await Promise.all(Object.values(buildInput).map(resolvePath)) } else { throw new Error('invalid rollupOptions.input value.') } diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index bf74811cd..a7a77968e 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -512,7 +512,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { if (isExternalUrl(specifier) || isDataUrl(specifier)) { return } - // skip ssr external + // skip ssr externals and builtins if (ssr && !matchAlias(specifier)) { if (shouldExternalize(environment, specifier, importer)) { return diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index a51b7c20a..f43bda309 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -23,7 +23,6 @@ import { isBuiltin, isDataUrl, isExternalUrl, - isFilePathESM, isInNodeModules, isNonDriveRelativeAbsolutePath, isObject, @@ -444,7 +443,6 @@ export function resolvePlugin( importer, options, depsOptimizer, - ssr, external, undefined, depsOptimizerOptions, @@ -746,7 +744,6 @@ export function tryNodeResolve( importer: string | null | undefined, options: InternalResolveOptionsWithOverrideConditions, depsOptimizer?: DepsOptimizer, - ssr: boolean = false, externalize?: boolean, allowLinkedExternal: boolean = true, depsOptimizerOptions?: DepOptimizationOptions, @@ -880,11 +877,9 @@ export function tryNodeResolve( : OPTIMIZABLE_ENTRY_RE.test(resolved) let exclude = depsOptimizer?.options.exclude - let include = depsOptimizer?.options.include if (options.ssrOptimizeCheck) { // we don't have the depsOptimizer exclude = depsOptimizerOptions?.exclude - include = depsOptimizerOptions?.include } const skipOptimization = @@ -893,15 +888,7 @@ export function tryNodeResolve( (importer && isInNodeModules(importer)) || exclude?.includes(pkgId) || exclude?.includes(id) || - SPECIAL_QUERY_RE.test(resolved) || - // During dev SSR, we don't have a way to reload the module graph if - // a non-optimized dep is found. So we need to skip optimization here. - // The only optimized deps are the ones explicitly listed in the config. - (!options.ssrOptimizeCheck && !isBuild && ssr) || - // Only optimize non-external CJS deps during SSR by default - (ssr && - isFilePathESM(resolved, options.packageCache) && - !(include?.includes(pkgId) || include?.includes(id))) + SPECIAL_QUERY_RE.test(resolved) if (options.ssrOptimizeCheck) { return { @@ -1222,7 +1209,6 @@ function tryResolveBrowserMapping( undefined, undefined, undefined, - undefined, depsOptimizerOptions, )?.id : tryFsResolve(path.join(pkg.dir, browserMappedPath), options)) diff --git a/packages/vite/src/node/server/environment.ts b/packages/vite/src/node/server/environment.ts index 8b7da8bb6..722cf963c 100644 --- a/packages/vite/src/node/server/environment.ts +++ b/packages/vite/src/node/server/environment.ts @@ -137,14 +137,8 @@ export class DevEnvironment extends BaseEnvironment { } else if (isDepOptimizationDisabled(optimizeDeps)) { this.depsOptimizer = undefined } else { - // We only support auto-discovery for the client environment, for all other - // environments `noDiscovery` has no effect and a simpler explicit deps - // optimizer is used that only optimizes explicitly included dependencies - // so it doesn't need to reload the environment. Now that we have proper HMR - // and full reload for general environments, we can enable auto-discovery for - // them in the future this.depsOptimizer = ( - optimizeDeps.noDiscovery || options.consumer !== 'client' + optimizeDeps.noDiscovery ? createExplicitDepsOptimizer : createDepsOptimizer )(this) diff --git a/packages/vite/src/node/ssr/fetchModule.ts b/packages/vite/src/node/ssr/fetchModule.ts index 70b866ed1..b517a0feb 100644 --- a/packages/vite/src/node/ssr/fetchModule.ts +++ b/packages/vite/src/node/ssr/fetchModule.ts @@ -44,32 +44,22 @@ export async function fetchModule( const { externalConditions, dedupe, preserveSymlinks } = environment.config.resolve - const resolved = tryNodeResolve( - url, - importer, - { - mainFields: ['main'], - conditions: [], - externalConditions, - external: [], - noExternal: [], - overrideConditions: [ - ...externalConditions, - 'production', - 'development', - ], - extensions: ['.js', '.cjs', '.json'], - dedupe, - preserveSymlinks, - isBuild: false, - isProduction, - root, - packageCache: environment.config.packageCache, - webCompatible: environment.config.webCompatible, - }, - undefined, - true, - ) + const resolved = tryNodeResolve(url, importer, { + mainFields: ['main'], + conditions: [], + externalConditions, + external: [], + noExternal: [], + overrideConditions: [...externalConditions, 'production', 'development'], + extensions: ['.js', '.cjs', '.json'], + dedupe, + preserveSymlinks, + isBuild: false, + isProduction, + root, + packageCache: environment.config.packageCache, + webCompatible: environment.config.webCompatible, + }) if (!resolved) { const err: any = new Error( `Cannot find module '${url}' imported from '${importer}'`, diff --git a/packages/vite/src/node/ssr/index.ts b/packages/vite/src/node/ssr/index.ts index 975aa26aa..dc597ea41 100644 --- a/packages/vite/src/node/ssr/index.ts +++ b/packages/vite/src/node/ssr/index.ts @@ -62,7 +62,6 @@ export function resolveSSROptions( ...ssr, optimizeDeps: { ...optimizeDeps, - noDiscovery: true, // always true for ssr esbuildOptions: { preserveSymlinks, ...optimizeDeps.esbuildOptions, diff --git a/playground/environment-react-ssr/__tests__/basic.spec.ts b/playground/environment-react-ssr/__tests__/basic.spec.ts deleted file mode 100644 index 4b98b37a2..000000000 --- a/playground/environment-react-ssr/__tests__/basic.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { test } from 'vitest' -import { page } from '~utils' - -test('basic', async () => { - await page.getByText('hydrated: true').isVisible() - await page.getByText('Count: 0').isVisible() - await page.getByRole('button', { name: '+' }).click() - await page.getByText('Count: 1').isVisible() -}) diff --git a/playground/environment-react-ssr/__tests__/environment-react-ssr.spec.ts b/playground/environment-react-ssr/__tests__/environment-react-ssr.spec.ts new file mode 100644 index 000000000..bc9c716ff --- /dev/null +++ b/playground/environment-react-ssr/__tests__/environment-react-ssr.spec.ts @@ -0,0 +1,92 @@ +import fs from 'node:fs' +import path from 'node:path' +import { describe, expect, onTestFinished, test } from 'vitest' +import type { DepOptimizationMetadata } from 'vite' +import { + isBuild, + page, + readFile, + serverLogs, + testDir, + untilUpdated, +} from '~utils' + +test('basic', async () => { + await page.getByText('hydrated: true').isVisible() + await page.getByText('Count: 0').isVisible() + await page.getByRole('button', { name: '+' }).click() + await page.getByText('Count: 1').isVisible() +}) + +describe.runIf(!isBuild)('pre-bundling', () => { + test('client', async () => { + const meta = await readFile('node_modules/.vite/deps/_metadata.json') + const metaJson: DepOptimizationMetadata = JSON.parse(meta) + + expect(metaJson.optimized['react']).toBeTruthy() + expect(metaJson.optimized['react-dom/client']).toBeTruthy() + expect(metaJson.optimized['react/jsx-dev-runtime']).toBeTruthy() + + expect(metaJson.optimized['react-dom/server']).toBeFalsy() + }) + + test('ssr', async () => { + const meta = await readFile('node_modules/.vite/deps_ssr/_metadata.json') + const metaJson: DepOptimizationMetadata = JSON.parse(meta) + + expect(metaJson.optimized['react']).toBeTruthy() + expect(metaJson.optimized['react-dom/server']).toBeTruthy() + expect(metaJson.optimized['react/jsx-dev-runtime']).toBeTruthy() + + expect(metaJson.optimized['react-dom/client']).toBeFalsy() + }) + + test('deps reload', async () => { + const envs = ['client', 'server'] as const + + const getMeta = (env: (typeof envs)[number]): DepOptimizationMetadata => { + const meta = readFile( + `node_modules/.vite/deps${env === 'client' ? '' : '_ssr'}/_metadata.json`, + ) + return JSON.parse(meta) + } + + expect(getMeta('client').optimized['react-fake-client']).toBeFalsy() + expect(getMeta('client').optimized['react-fake-server']).toBeFalsy() + expect(getMeta('server').optimized['react-fake-server']).toBeFalsy() + expect(getMeta('server').optimized['react-fake-client']).toBeFalsy() + + envs.forEach((env) => { + const filePath = path.resolve(testDir, `src/entry-${env}.tsx`) + const originalContent = readFile(filePath) + fs.writeFileSync( + filePath, + `import 'react-fake-${env}'\n${originalContent}`, + 'utf-8', + ) + onTestFinished(() => { + fs.writeFileSync(filePath, originalContent, 'utf-8') + }) + }) + + await untilUpdated( + () => + serverLogs + .map( + (log) => + log + // eslint-disable-next-line no-control-regex + .replace(/\x1B\[\d+m/g, '') + .match(/new dependencies optimized: (react-fake-.*)/)?.[1], + ) + .filter(Boolean) + .join(', '), + 'react-fake-server, react-fake-client', + ) + + expect(getMeta('client').optimized['react-fake-client']).toBeTruthy() + expect(getMeta('client').optimized['react-fake-server']).toBeFalsy() + expect(getMeta('server').optimized['react-fake-server']).toBeTruthy() + expect(getMeta('server').optimized['react-fake-client']).toBeFalsy() + }) +}) diff --git a/playground/environment-react-ssr/package.json b/playground/environment-react-ssr/package.json index 44910cc40..14473dd31 100644 --- a/playground/environment-react-ssr/package.json +++ b/playground/environment-react-ssr/package.json @@ -12,6 +12,8 @@ "@types/react": "^18.3.11", "@types/react-dom": "^18.3.1", "react": "^18.3.1", + "react-fake-client": "npm:react@^18.3.1", + "react-fake-server": "npm:react@^18.3.1", "react-dom": "^18.3.1" } } diff --git a/playground/environment-react-ssr/vite.config.ts b/playground/environment-react-ssr/vite.config.ts index 05d667b50..0d14c778f 100644 --- a/playground/environment-react-ssr/vite.config.ts +++ b/playground/environment-react-ssr/vite.config.ts @@ -21,6 +21,9 @@ export default defineConfig((env) => ({ }, }, ], + resolve: { + noExternal: true, + }, environments: { client: { build: { @@ -30,6 +33,11 @@ export default defineConfig((env) => ({ }, }, ssr: { + dev: { + optimizeDeps: { + noDiscovery: false, + }, + }, build: { outDir: 'dist/server', // [feedback] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a0f1a57f3..31e590d93 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -698,6 +698,12 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + react-fake-client: + specifier: npm:react@^18.3.1 + version: react@18.3.1 + react-fake-server: + specifier: npm:react@^18.3.1 + version: react@18.3.1 playground/extensions: dependencies: @@ -7123,8 +7129,8 @@ packages: zimmerframe@1.0.0: resolution: {integrity: sha512-H6qQ6LtjP+kDQwDgol18fPi4OCo7F+73ZBYt2U9c1D3V74bIMKxXvyrN0x+1I7/RYh5YsausflQxQR/qwDLHPQ==} - zod@3.22.4: - resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -11301,7 +11307,7 @@ snapshots: workerd: 1.20241011.1 ws: 8.18.0 youch: 3.2.3 - zod: 3.22.4 + zod: 3.23.8 transitivePeerDependencies: - bufferutil - supports-color @@ -12908,6 +12914,6 @@ snapshots: zimmerframe@1.0.0: {} - zod@3.22.4: {} + zod@3.23.8: {} zwitch@2.0.4: {} From 3778c7acf95e2dec3c18f3954ba4a52a979f45c4 Mon Sep 17 00:00:00 2001 From: patak <583075+patak-dev@users.noreply.github.com> Date: Wed, 23 Oct 2024 08:20:12 +0200 Subject: [PATCH 2/4] docs: center last projects row in landing (#18424) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 翠 / green --- .../FrameworksSection.vue | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/docs/.vitepress/theme/components/landing/3. frameworks-section/FrameworksSection.vue b/docs/.vitepress/theme/components/landing/3. frameworks-section/FrameworksSection.vue index df59bfaf6..658b21e4a 100644 --- a/docs/.vitepress/theme/components/landing/3. frameworks-section/FrameworksSection.vue +++ b/docs/.vitepress/theme/components/landing/3. frameworks-section/FrameworksSection.vue @@ -244,19 +244,6 @@ const numFrameworksPerRow = computed( () => numBlocksPerRow.value - paddedBlocksPerSide.value * 2, ) -/** - * The indexes of the blocks on each row that support framework cards. - */ -const centerIndexes: ComputedRef<{ start: number; end: number }> = computed( - () => { - const startIndex = paddedBlocksPerSide.value - return { - start: startIndex, - end: numBlocksPerRow.value - paddedBlocksPerSide.value, - } - }, -) - /** * How many rows do we need to display all the frameworks? */ @@ -264,6 +251,41 @@ const numRows: ComputedRef = computed(() => { return Math.ceil(frameworks.length / numFrameworksPerRow.value) }) +/** + * The indexes of the blocks on each row that support framework cards. + * + * Note that the index of the returned array is 1-based. + */ +const centerIndexes: ComputedRef<{ start: number; end: number }[]> = computed( + () => { + const firstRowsStartIndex = paddedBlocksPerSide.value + const frameworksPerFirstRows = + numBlocksPerRow.value - 2 * paddedBlocksPerSide.value + const lastRowStartIndex = + paddedBlocksPerSide.value + + Math.floor( + (frameworksPerFirstRows - + (frameworks.length % frameworksPerFirstRows)) / + 2, + ) + return new Array(numRows.value + 1).fill(0).map((_, i) => { + return i < numRows.value || + frameworks.length % frameworksPerFirstRows === 0 + ? { + start: firstRowsStartIndex, + end: numBlocksPerRow.value - paddedBlocksPerSide.value, + } + : { + start: lastRowStartIndex, + end: + lastRowStartIndex + + (frameworks.length % frameworksPerFirstRows) + + 1, + } + }) + }, +) + /** * Generate CSS transformations for each row, to gracefully slide between horizontal positions. */ @@ -289,8 +311,8 @@ const rowStyle: ComputedRef<{ transform: string }> = computed(() => {