fix(nuxt): apply more import protections for nitro runtime (#25162)

This commit is contained in:
Pooya Parsa 2024-01-12 12:22:01 +01:00 committed by GitHub
parent 58f0627e0b
commit c4b6560abc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 54 additions and 25 deletions

View File

@ -17,7 +17,7 @@ import { template as defaultSpaLoadingTemplate } from '@nuxt/ui-templates/templa
import { version as nuxtVersion } from '../../package.json' import { version as nuxtVersion } from '../../package.json'
import { distDir } from '../dirs' import { distDir } from '../dirs'
import { toArray } from '../utils' 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 }) { export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
// Resolve config // Resolve config
@ -339,10 +339,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
nitroConfig.rollupConfig!.plugins!.push( nitroConfig.rollupConfig!.plugins!.push(
ImportProtectionPlugin.rollup({ ImportProtectionPlugin.rollup({
rootDir: nuxt.options.rootDir, rootDir: nuxt.options.rootDir,
patterns: [ patterns: nuxtImportProtections(nuxt, { isNitro: true }),
...['#app', /^#build(\/|$)/]
.map(p => [p, 'Vue app aliases are not allowed in server routes.']) as [RegExp | string, string][]
],
exclude: [/core[\\/]runtime[\\/]nitro[\\/]renderer/] exclude: [/core[\\/]runtime[\\/]nitro[\\/]renderer/]
}) })
) )

View File

@ -16,7 +16,7 @@ import importsModule from '../imports/module'
/* eslint-enable */ /* eslint-enable */
import { distDir, pkgDir } from '../dirs' import { distDir, pkgDir } from '../dirs'
import { version } from '../../package.json' 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 type { UnctxTransformPluginOptions } from './plugins/unctx'
import { UnctxTransformPlugin } from './plugins/unctx' import { UnctxTransformPlugin } from './plugins/unctx'
import type { TreeShakeComposablesPluginOptions } from './plugins/tree-shake' import type { TreeShakeComposablesPluginOptions } from './plugins/tree-shake'
@ -89,7 +89,7 @@ async function initNuxt (nuxt: Nuxt) {
rootDir: nuxt.options.rootDir, rootDir: nuxt.options.rootDir,
// Exclude top-level resolutions by plugins // Exclude top-level resolutions by plugins
exclude: [join(nuxt.options.rootDir, 'index.html')], exclude: [join(nuxt.options.rootDir, 'index.html')],
patterns: vueAppPatterns(nuxt) patterns: nuxtImportProtections(nuxt)
} }
addVitePlugin(() => ImportProtectionPlugin.vite(config)) addVitePlugin(() => ImportProtectionPlugin.vite(config))
addWebpackPlugin(() => ImportProtectionPlugin.webpack(config)) addWebpackPlugin(() => ImportProtectionPlugin.webpack(config))

View File

@ -1,9 +1,9 @@
import { createRequire } from 'node:module' import { createRequire } from 'node:module'
import { createUnplugin } from 'unplugin' import { createUnplugin } from 'unplugin'
import { logger } from '@nuxt/kit' 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 escapeRE from 'escape-string-regexp'
import type { Nuxt } from 'nuxt/schema' import type { NuxtOptions } from 'nuxt/schema'
const _require = createRequire(import.meta.url) const _require = createRequire(import.meta.url)
@ -13,16 +13,47 @@ interface ImportProtectionOptions {
exclude?: Array<RegExp | string> exclude?: Array<RegExp | string>
} }
export const vueAppPatterns = (nuxt: Nuxt) => [ export const nuxtImportProtections = (nuxt: { options: NuxtOptions }, options: { isNitro?: boolean } = {}) => {
[/^(nuxt|nuxt3|nuxt-nightly)$/, '`nuxt`/`nuxt3`/`nuxt-nightly` cannot be imported directly. Instead, import runtime Nuxt composables from `#app` or `#imports`.'], const patterns: ImportProtectionOptions['patterns'] = []
[/^((|~|~~|@|@@)\/)?nuxt\.config(\.|$)/, 'Importing directly from a `nuxt.config` file is not allowed. Instead, use runtime config or a module.'],
[/(^|node_modules\/)@vue\/composition-api/], patterns.push([
...nuxt.options.modules.filter(m => typeof m === 'string').map((m: any) => /^(nuxt|nuxt3|nuxt-nightly)$/,
[new RegExp(`^${escapeRE(m as string)}$`), 'Importing directly from module entry points is not allowed.']), '`nuxt`, `nuxt3` or `nuxt-nightly` cannot be imported directly.' + (options.isNitro ? '' : ' Instead, import runtime Nuxt composables from `#app` or `#imports`.')
...[/(^|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.'] patterns.push([
] as ImportProtectionOptions['patterns'] /^((|~|~~|@|@@)\/)?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) { export const ImportProtectionPlugin = createUnplugin(function (options: ImportProtectionOptions) {
const cache: Record<string, Map<string | RegExp, boolean>> = {} const cache: Record<string, Map<string | RegExp, boolean>> = {}

View File

@ -1,6 +1,7 @@
import { normalize } from 'pathe' import { normalize } from 'pathe'
import { describe, expect, it } from 'vitest' 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 = [ const testsToTriggerOn = [
['~/nuxt.config', 'app.vue', true], ['~/nuxt.config', 'app.vue', true],
@ -39,13 +40,13 @@ describe('import protection', () => {
const transformWithImportProtection = (id: string, importer: string) => { const transformWithImportProtection = (id: string, importer: string) => {
const plugin = ImportProtectionPlugin.rollup({ const plugin = ImportProtectionPlugin.rollup({
rootDir: '/root', rootDir: '/root',
patterns: vueAppPatterns({ patterns: nuxtImportProtections({
options: { options: {
modules: ['some-nuxt-module'], modules: ['some-nuxt-module'],
srcDir: 'src/', srcDir: '/root/src/',
dir: { server: 'server' } serverDir: '/root/src/server'
} } satisfies Partial<NuxtOptions> as NuxtOptions
} as any) })
}) })
return (plugin as any).resolveId(id, importer) return (plugin as any).resolveId(id, importer)