From c4b6560abc15f9944f2b4228fc050ed90c9eb3eb Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Fri, 12 Jan 2024 12:22:01 +0100 Subject: [PATCH] fix(nuxt): apply more import protections for nitro runtime (#25162) --- packages/nuxt/src/core/nitro.ts | 7 +-- packages/nuxt/src/core/nuxt.ts | 4 +- .../src/core/plugins/import-protection.ts | 55 +++++++++++++++---- packages/nuxt/test/import-protection.test.ts | 13 +++-- 4 files changed, 54 insertions(+), 25 deletions(-) diff --git a/packages/nuxt/src/core/nitro.ts b/packages/nuxt/src/core/nitro.ts index a294eb335d..cfda990c80 100644 --- a/packages/nuxt/src/core/nitro.ts +++ b/packages/nuxt/src/core/nitro.ts @@ -17,7 +17,7 @@ import { template as defaultSpaLoadingTemplate } from '@nuxt/ui-templates/templa import { version as nuxtVersion } from '../../package.json' import { distDir } from '../dirs' import { toArray } from '../utils' -import { ImportProtectionPlugin } from './plugins/import-protection' +import { ImportProtectionPlugin, nuxtImportProtections } from './plugins/import-protection' export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { // Resolve config @@ -339,10 +339,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { nitroConfig.rollupConfig!.plugins!.push( ImportProtectionPlugin.rollup({ rootDir: nuxt.options.rootDir, - patterns: [ - ...['#app', /^#build(\/|$)/] - .map(p => [p, 'Vue app aliases are not allowed in server routes.']) as [RegExp | string, string][] - ], + patterns: nuxtImportProtections(nuxt, { isNitro: true }), exclude: [/core[\\/]runtime[\\/]nitro[\\/]renderer/] }) ) diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index 2ce4f00db3..adc2aac1fa 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -16,7 +16,7 @@ import importsModule from '../imports/module' /* eslint-enable */ import { distDir, pkgDir } from '../dirs' import { version } from '../../package.json' -import { ImportProtectionPlugin, vueAppPatterns } from './plugins/import-protection' +import { ImportProtectionPlugin, nuxtImportProtections } from './plugins/import-protection' import type { UnctxTransformPluginOptions } from './plugins/unctx' import { UnctxTransformPlugin } from './plugins/unctx' import type { TreeShakeComposablesPluginOptions } from './plugins/tree-shake' @@ -89,7 +89,7 @@ async function initNuxt (nuxt: Nuxt) { rootDir: nuxt.options.rootDir, // Exclude top-level resolutions by plugins exclude: [join(nuxt.options.rootDir, 'index.html')], - patterns: vueAppPatterns(nuxt) + patterns: nuxtImportProtections(nuxt) } addVitePlugin(() => ImportProtectionPlugin.vite(config)) addWebpackPlugin(() => ImportProtectionPlugin.webpack(config)) diff --git a/packages/nuxt/src/core/plugins/import-protection.ts b/packages/nuxt/src/core/plugins/import-protection.ts index 4c00981199..25c6eaa66a 100644 --- a/packages/nuxt/src/core/plugins/import-protection.ts +++ b/packages/nuxt/src/core/plugins/import-protection.ts @@ -1,9 +1,9 @@ import { createRequire } from 'node:module' import { createUnplugin } from 'unplugin' import { logger } from '@nuxt/kit' -import { isAbsolute, join, relative } from 'pathe' +import { isAbsolute, join, relative, resolve } from 'pathe' import escapeRE from 'escape-string-regexp' -import type { Nuxt } from 'nuxt/schema' +import type { NuxtOptions } from 'nuxt/schema' const _require = createRequire(import.meta.url) @@ -13,16 +13,47 @@ interface ImportProtectionOptions { exclude?: Array } -export const vueAppPatterns = (nuxt: Nuxt) => [ - [/^(nuxt|nuxt3|nuxt-nightly)$/, '`nuxt`/`nuxt3`/`nuxt-nightly` cannot be imported directly. Instead, import runtime Nuxt composables from `#app` or `#imports`.'], - [/^((|~|~~|@|@@)\/)?nuxt\.config(\.|$)/, 'Importing directly from a `nuxt.config` file is not allowed. Instead, use runtime config or a module.'], - [/(^|node_modules\/)@vue\/composition-api/], - ...nuxt.options.modules.filter(m => typeof m === 'string').map((m: any) => - [new RegExp(`^${escapeRE(m as string)}$`), 'Importing directly from module entry points is not allowed.']), - ...[/(^|node_modules\/)@nuxt\/kit/, /(^|node_modules\/)nuxt\/(config|kit|schema)/, /^nitropack/] - .map(i => [i, 'This module cannot be imported in the Vue part of your app.']), - [new RegExp(escapeRE(join(nuxt.options.srcDir, (nuxt.options.dir as any).server || 'server')) + '\\/(api|routes|middleware|plugins)\\/'), 'Importing from server is not allowed in the Vue part of your app.'] -] as ImportProtectionOptions['patterns'] +export const nuxtImportProtections = (nuxt: { options: NuxtOptions }, options: { isNitro?: boolean } = {}) => { + const patterns: ImportProtectionOptions['patterns'] = [] + + patterns.push([ + /^(nuxt|nuxt3|nuxt-nightly)$/, + '`nuxt`, `nuxt3` or `nuxt-nightly` cannot be imported directly.' + (options.isNitro ? '' : ' Instead, import runtime Nuxt composables from `#app` or `#imports`.') + ]) + + patterns.push([ + /^((|~|~~|@|@@)\/)?nuxt\.config(\.|$)/, + 'Importing directly from a `nuxt.config` file is not allowed. Instead, use runtime config or a module.' + ]) + + patterns.push([/(^|node_modules\/)@vue\/composition-api/]) + + for (const mod of nuxt.options.modules.filter(m => typeof m === 'string')) { + patterns.push([ + new RegExp(`^${escapeRE(mod as string)}$`), + 'Importing directly from module entry-points is not allowed.' + ]) + } + + for (const i of [/(^|node_modules\/)@nuxt\/(kit|test-utils)/, /(^|node_modules\/)nuxi/, /(^|node_modules\/)nuxt\/(config|kit|schema)/, 'nitropack']) { + patterns.push([i, 'This module cannot be imported' + (options.isNitro ? 'in server runtime.' : ' in the Vue part of your app.')]) + } + + if (options.isNitro) { + for (const i of ['#app', /^#build(\/|$)/]) { + patterns.push([i, 'Vue app aliases are not allowed in server runtime.']) + } + } + + if (!options.isNitro) { + patterns.push([ + new RegExp(escapeRE(relative(nuxt.options.srcDir, resolve(nuxt.options.srcDir, nuxt.options.serverDir || 'server'))) + '\\/(api|routes|middleware|plugins)\\/'), + 'Importing from server is not allowed in the Vue part of your app.' + ]) + } + + return patterns +} export const ImportProtectionPlugin = createUnplugin(function (options: ImportProtectionOptions) { const cache: Record> = {} diff --git a/packages/nuxt/test/import-protection.test.ts b/packages/nuxt/test/import-protection.test.ts index 8d2e76e175..13b2548584 100644 --- a/packages/nuxt/test/import-protection.test.ts +++ b/packages/nuxt/test/import-protection.test.ts @@ -1,6 +1,7 @@ import { normalize } from 'pathe' import { describe, expect, it } from 'vitest' -import { ImportProtectionPlugin, vueAppPatterns } from '../src/core/plugins/import-protection' +import { ImportProtectionPlugin, nuxtImportProtections } from '../src/core/plugins/import-protection' +import type { NuxtOptions } from '../schema' const testsToTriggerOn = [ ['~/nuxt.config', 'app.vue', true], @@ -39,13 +40,13 @@ describe('import protection', () => { const transformWithImportProtection = (id: string, importer: string) => { const plugin = ImportProtectionPlugin.rollup({ rootDir: '/root', - patterns: vueAppPatterns({ + patterns: nuxtImportProtections({ options: { modules: ['some-nuxt-module'], - srcDir: 'src/', - dir: { server: 'server' } - } - } as any) + srcDir: '/root/src/', + serverDir: '/root/src/server' + } satisfies Partial as NuxtOptions + }) }) return (plugin as any).resolveId(id, importer)