From 83f3fc47cadc56d18101d9b71df849ddbda24a1c Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 6 Mar 2025 10:51:31 +0000 Subject: [PATCH] fix(nuxt): ensure externals are resolved first (#31235) --- packages/nuxt/src/core/nuxt.ts | 14 +++--- .../src/core/plugins/resolve-deep-imports.ts | 41 +++--------------- .../src/core/plugins/resolved-externals.ts | 43 +++++++++++++++++++ packages/vite/src/server.ts | 1 + test/bundle.test.ts | 4 +- 5 files changed, 59 insertions(+), 44 deletions(-) create mode 100644 packages/nuxt/src/core/plugins/resolved-externals.ts diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index 01d035adc7..65dc280627 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -24,7 +24,7 @@ import defu from 'defu' import { gt, satisfies } from 'semver' import { hasTTY, isCI } from 'std-env' import { genImport } from 'knitwork' -import { resolveModulePath, resolveModuleURL } from 'exsolve' +import { resolveModulePath } from 'exsolve' import pagesModule from '../pages/module' import metaModule from '../head/module' @@ -47,7 +47,8 @@ import schemaModule from './schema' import { RemovePluginMetadataPlugin } from './plugins/plugin-metadata' import { AsyncContextInjectionPlugin } from './plugins/async-context' import { ComposableKeysPlugin } from './plugins/composable-keys' -import { resolveDeepImportsPlugin } from './plugins/resolve-deep-imports' +import { ResolveDeepImportsPlugin } from './plugins/resolve-deep-imports' +import { ResolveExternalsPlugin } from './plugins/resolved-externals' import { PrehydrateTransformPlugin } from './plugins/prehydrate' import { VirtualFSPlugin } from './plugins/virtual' @@ -373,8 +374,10 @@ async function initNuxt (nuxt: Nuxt) { addWebpackPlugin(() => ImpoundPlugin.webpack(nuxtProtectionConfig)) // add resolver for modules used in virtual files - addVitePlugin(() => resolveDeepImportsPlugin(nuxt), { client: false }) - addVitePlugin(() => resolveDeepImportsPlugin(nuxt), { server: false }) + addVitePlugin(() => ResolveDeepImportsPlugin(nuxt), { client: false }) + addVitePlugin(() => ResolveDeepImportsPlugin(nuxt), { server: false }) + + addVitePlugin(() => ResolveExternalsPlugin(nuxt), { client: false, prepend: true }) // Add transform for `onPrehydrate` lifecycle hook addBuildPlugin(PrehydrateTransformPlugin({ sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client })) @@ -391,13 +394,12 @@ async function initNuxt (nuxt: Nuxt) { } nuxt.hook('modules:done', () => { - const importPaths = nuxt.options.modulesDir.map(dir => directoryToURL((dir))) // Add unctx transform addBuildPlugin(UnctxTransformPlugin({ sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client, transformerOptions: { ...nuxt.options.optimization.asyncTransforms, - helperModule: resolveModuleURL('unctx', { try: true, from: importPaths }) ?? 'unctx', + helperModule: 'unctx', }, })) diff --git a/packages/nuxt/src/core/plugins/resolve-deep-imports.ts b/packages/nuxt/src/core/plugins/resolve-deep-imports.ts index 2af0062c7c..95b9b71412 100644 --- a/packages/nuxt/src/core/plugins/resolve-deep-imports.ts +++ b/packages/nuxt/src/core/plugins/resolve-deep-imports.ts @@ -2,25 +2,22 @@ import { parseNodeModulePath } from 'mlly' import { resolveModulePath } from 'exsolve' import { isAbsolute, normalize, resolve } from 'pathe' import type { Plugin } from 'vite' -import { directoryToURL, resolveAlias, tryImportModule } from '@nuxt/kit' +import { directoryToURL, resolveAlias } from '@nuxt/kit' import type { Nuxt } from '@nuxt/schema' -import type { Nitro } from 'nitropack' -import type { PackageJson } from 'pkg-types' import { pkgDir } from '../../dirs' import { logger } from '../../utils' const VIRTUAL_RE = /^\0?virtual:(?:nuxt:)?/ -export function resolveDeepImportsPlugin (nuxt: Nuxt): Plugin { +export function ResolveDeepImportsPlugin (nuxt: Nuxt): Plugin { const exclude: string[] = ['virtual:', '\0virtual:', '/__skip_vite', '@vitest/'] let conditions: string[] - let external: Set return { name: 'nuxt:resolve-bare-imports', enforce: 'post', - async configResolved (config) { + configResolved (config) { const resolvedConditions = new Set([nuxt.options.dev ? 'development' : 'production', ...config.resolve.conditions]) if (resolvedConditions.has('browser')) { resolvedConditions.add('web') @@ -33,27 +30,12 @@ export function resolveDeepImportsPlugin (nuxt: Nuxt): Plugin { resolvedConditions.add('require') } conditions = [...resolvedConditions] - - const runtimeDependencies = await tryImportModule('nitropack/package.json', { - url: new URL(import.meta.url), - })?.then(r => r?.dependencies ? Object.keys(r.dependencies) : []).catch(() => []) || [] - - external = new Set([ - // explicit dependencies we use in our ssr renderer - these can be inlined (if necessary) in the nitro build - 'unhead', '@unhead/vue', 'unctx', 'h3', 'devalue', '@nuxt/devalue', 'radix3', 'rou3', 'unstorage', 'hookable', - // ensure we only have one version of vue if nitro is going to inline anyway - ...((nuxt as any)._nitro as Nitro).options.inlineDynamicImports ? ['vue', '@vue/server-renderer', '@unhead/vue'] : [], - // dependencies we might share with nitro - these can be inlined (if necessary) in the nitro build - ...runtimeDependencies, - ]) }, async resolveId (id, importer) { if (!importer || isAbsolute(id) || (!isAbsolute(importer) && !VIRTUAL_RE.test(importer)) || exclude.some(e => id.startsWith(e))) { return } - const overrides = external.has(id) ? { external: 'absolute' } as const : {} - const normalisedId = resolveAlias(normalize(id), nuxt.options.alias) const isNuxtTemplate = importer.startsWith('virtual:nuxt') const normalisedImporter = (isNuxtTemplate ? decodeURIComponent(importer) : importer).replace(VIRTUAL_RE, '') @@ -63,10 +45,7 @@ export function resolveDeepImportsPlugin (nuxt: Nuxt): Plugin { if (template?._path) { const res = await this.resolve?.(normalisedId, template._path, { skipSelf: true }) if (res !== undefined && res !== null) { - return { - ...res, - ...overrides, - } + return res } } } @@ -75,10 +54,7 @@ export function resolveDeepImportsPlugin (nuxt: Nuxt): Plugin { const res = await this.resolve?.(normalisedId, dir, { skipSelf: true }) if (res !== undefined && res !== null) { - return { - ...res, - ...overrides, - } + return res } const path = resolveModulePath(id, { @@ -93,13 +69,6 @@ export function resolveDeepImportsPlugin (nuxt: Nuxt): Plugin { return null } - if (external.has(id)) { - return { - id: normalize(path), - external: 'absolute', - } - } - return normalize(path) }, } diff --git a/packages/nuxt/src/core/plugins/resolved-externals.ts b/packages/nuxt/src/core/plugins/resolved-externals.ts new file mode 100644 index 0000000000..c81824aed3 --- /dev/null +++ b/packages/nuxt/src/core/plugins/resolved-externals.ts @@ -0,0 +1,43 @@ +import type { Plugin } from 'vite' +import { tryImportModule } from '@nuxt/kit' +import type { Nuxt } from '@nuxt/schema' +import type { Nitro } from 'nitropack' +import type { PackageJson } from 'pkg-types' + +export function ResolveExternalsPlugin (nuxt: Nuxt): Plugin { + let external: Set = new Set() + + return { + name: 'nuxt:resolve-externals', + enforce: 'pre', + async configResolved () { + if (!nuxt.options.dev) { + const runtimeDependencies = await tryImportModule('nitropack/package.json', { + url: new URL(import.meta.url), + })?.then(r => r?.dependencies ? Object.keys(r.dependencies) : []).catch(() => []) || [] + + external = new Set([ + // explicit dependencies we use in our ssr renderer - these can be inlined (if necessary) in the nitro build + 'unhead', '@unhead/vue', 'unctx', 'h3', 'devalue', '@nuxt/devalue', 'radix3', 'rou3', 'unstorage', 'hookable', + // ensure we only have one version of vue if nitro is going to inline anyway + ...((nuxt as any)._nitro as Nitro).options.inlineDynamicImports ? ['vue', '@vue/server-renderer', '@unhead/vue'] : [], + // dependencies we might share with nitro - these can be inlined (if necessary) in the nitro build + ...runtimeDependencies, + ]) + } + }, + async resolveId (id, importer) { + if (!external.has(id)) { + return + } + + const res = await this.resolve?.(id, importer, { skipSelf: true }) + if (res !== undefined && res !== null) { + return { + ...res, + external: 'absolute', + } + } + }, + } +} diff --git a/packages/vite/src/server.ts b/packages/vite/src/server.ts index 76b3fce2a4..ea534626f7 100644 --- a/packages/vite/src/server.ts +++ b/packages/vite/src/server.ts @@ -89,6 +89,7 @@ export async function buildServer (ctx: ViteBuildContext) { new RegExp('^' + escapeStringRegexp(withTrailingSlash(resolve(ctx.nuxt.options.rootDir, ctx.nuxt.options.dir.shared)))), ], output: { + preserveModules: true, entryFileNames: '[name].mjs', format: 'module', generatedCode: { diff --git a/test/bundle.test.ts b/test/bundle.test.ts index 0cd70b986d..4e692329c7 100644 --- a/test/bundle.test.ts +++ b/test/bundle.test.ts @@ -37,7 +37,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM const serverDir = join(rootDir, '.output/server') const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir) - expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"209k"`) + expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"193k"`) const modules = await analyzeSizes(['node_modules/**/*'], serverDir) expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1384k"`) @@ -74,7 +74,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM const serverDir = join(rootDir, '.output-inline/server') const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir) - expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"559k"`) + expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"544k"`) const modules = await analyzeSizes(['node_modules/**/*'], serverDir) expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"77.8k"`)