Skip to content

Commit c2a20e5

Browse files
committed
fix(kit,nuxt): protect against resolved nuxt module subpath
1 parent 1ec99e9 commit c2a20e5

File tree

4 files changed

+42
-31
lines changed

4 files changed

+42
-31
lines changed

packages/kit/src/module/install.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { ModuleMeta, ModuleOptions, Nuxt, NuxtConfig, NuxtModule, NuxtOptio
44
import { dirname, isAbsolute, join, resolve } from 'pathe'
55
import { defu } from 'defu'
66
import { createJiti } from 'jiti'
7-
import { parseNodeModulePath } from 'mlly'
7+
import { lookupNodeModuleSubpath, parseNodeModulePath } from 'mlly'
88
import { resolveModulePath, resolveModuleURL } from 'exsolve'
99
import { isRelative } from 'ufo'
1010
import { readPackageJSON, resolvePackageJSON } from 'pkg-types'
@@ -322,8 +322,13 @@ async function callModule (nuxtModule: NuxtModule<any, Partial<any>, false>, met
322322
}
323323

324324
const modulePath = resolvedModulePath || moduleToInstall
325+
let entryPath: string | undefined
325326
if (typeof modulePath === 'string') {
326327
const parsed = parseNodeModulePath(modulePath)
328+
if (parsed.name) {
329+
const subpath = await lookupNodeModuleSubpath(modulePath) || '.'
330+
entryPath = join(parsed.name, subpath === './' ? '.' : subpath)
331+
}
327332
const moduleRoot = parsed.dir
328333
? parsed.dir + parsed.name
329334
: await resolvePackageJSON(modulePath, { try: true }).then(r => r ? dirname(r) : modulePath)
@@ -335,7 +340,7 @@ async function callModule (nuxtModule: NuxtModule<any, Partial<any>, false>, met
335340
}
336341

337342
nuxt.options._installedModules ||= []
338-
const entryPath = typeof moduleToInstall === 'string' ? resolveAlias(moduleToInstall, nuxt.options.alias) : undefined
343+
entryPath ||= typeof moduleToInstall === 'string' ? moduleToInstall : undefined
339344

340345
if (typeof moduleToInstall === 'string' && entryPath !== moduleToInstall) {
341346
buildTimeModuleMeta.rawPath = moduleToInstall

packages/nuxt/src/core/nuxt.ts

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -327,29 +327,6 @@ async function initNuxt (nuxt: Nuxt) {
327327
composables: nuxt.options.optimization.keyedComposables,
328328
}))
329329

330-
// shared folder import protection
331-
const sharedDir = withTrailingSlash(resolve(nuxt.options.rootDir, nuxt.options.dir.shared))
332-
const relativeSharedDir = withTrailingSlash(relative(nuxt.options.rootDir, resolve(nuxt.options.rootDir, nuxt.options.dir.shared)))
333-
const sharedPatterns = [/^#shared\//, new RegExp('^' + escapeRE(sharedDir)), new RegExp('^' + escapeRE(relativeSharedDir))]
334-
const sharedProtectionConfig = {
335-
cwd: nuxt.options.rootDir,
336-
include: sharedPatterns,
337-
patterns: createImportProtectionPatterns(nuxt, { context: 'shared' }),
338-
}
339-
addVitePlugin(() => ImpoundPlugin.vite(sharedProtectionConfig), { server: false })
340-
addWebpackPlugin(() => ImpoundPlugin.webpack(sharedProtectionConfig), { server: false })
341-
342-
// Add import protection
343-
const nuxtProtectionConfig = {
344-
cwd: nuxt.options.rootDir,
345-
// Exclude top-level resolutions by plugins
346-
exclude: [relative(nuxt.options.rootDir, join(nuxt.options.srcDir, 'index.html')), ...sharedPatterns],
347-
patterns: createImportProtectionPatterns(nuxt, { context: 'nuxt-app' }),
348-
}
349-
addVitePlugin(() => Object.assign(ImpoundPlugin.vite({ ...nuxtProtectionConfig, error: false }), { name: 'nuxt:import-protection' }), { client: false })
350-
addVitePlugin(() => Object.assign(ImpoundPlugin.vite({ ...nuxtProtectionConfig, error: true }), { name: 'nuxt:import-protection' }), { server: false })
351-
addWebpackPlugin(() => ImpoundPlugin.webpack(nuxtProtectionConfig))
352-
353330
// add resolver for modules used in virtual files
354331
addVitePlugin(() => ResolveDeepImportsPlugin(nuxt))
355332

@@ -392,6 +369,29 @@ async function initNuxt (nuxt: Nuxt) {
392369
composables: nuxt.options.optimization.treeShake.composables.client,
393370
}), { server: false })
394371
}
372+
373+
// shared folder import protection
374+
const sharedDir = withTrailingSlash(resolve(nuxt.options.rootDir, nuxt.options.dir.shared))
375+
const relativeSharedDir = withTrailingSlash(relative(nuxt.options.rootDir, resolve(nuxt.options.rootDir, nuxt.options.dir.shared)))
376+
const sharedPatterns = [/^#shared\//, new RegExp('^' + escapeRE(sharedDir)), new RegExp('^' + escapeRE(relativeSharedDir))]
377+
const sharedProtectionConfig = {
378+
cwd: nuxt.options.rootDir,
379+
include: sharedPatterns,
380+
patterns: createImportProtectionPatterns(nuxt, { context: 'shared' }),
381+
}
382+
addVitePlugin(() => ImpoundPlugin.vite(sharedProtectionConfig), { server: false })
383+
addWebpackPlugin(() => ImpoundPlugin.webpack(sharedProtectionConfig), { server: false })
384+
385+
// Add import protection
386+
const nuxtProtectionConfig = {
387+
cwd: nuxt.options.rootDir,
388+
// Exclude top-level resolutions by plugins
389+
exclude: [relative(nuxt.options.rootDir, join(nuxt.options.srcDir, 'index.html')), ...sharedPatterns],
390+
patterns: createImportProtectionPatterns(nuxt, { context: 'nuxt-app' }),
391+
}
392+
addVitePlugin(() => Object.assign(ImpoundPlugin.vite({ ...nuxtProtectionConfig, error: false }), { name: 'nuxt:import-protection' }), { client: false })
393+
addVitePlugin(() => Object.assign(ImpoundPlugin.vite({ ...nuxtProtectionConfig, error: true }), { name: 'nuxt:import-protection' }), { server: false })
394+
addWebpackPlugin(() => ImpoundPlugin.webpack(nuxtProtectionConfig))
395395
})
396396

397397
if (!nuxt.options.dev) {

packages/nuxt/src/core/plugins/import-protection.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@ export function createImportProtectionPatterns (nuxt: { options: NuxtOptions },
2929

3030
patterns.push([/(^|node_modules\/)@vue\/composition-api/])
3131

32-
for (const mod of nuxt.options.modules.filter(m => typeof m === 'string')) {
33-
patterns.push([
34-
new RegExp(`^${escapeRE(mod)}$`),
35-
'Importing directly from module entry-points is not allowed.',
36-
])
32+
for (const mod of nuxt.options._installedModules) {
33+
if (mod.entryPath) {
34+
patterns.push([
35+
new RegExp(`^${escapeRE(mod.entryPath)}$`),
36+
'Importing directly from module entry-points is not allowed.',
37+
])
38+
}
3739
}
3840

3941
for (const i of [/(^|node_modules\/)@nuxt\/(cli|kit|test-utils)/, /(^|node_modules\/)nuxi/, /(^|node_modules\/)nitro(?:pack)?(?:-nightly)?(?:$|\/)(?!(?:dist\/)?(?:node_modules|presets|runtime|types))/, /(^|node_modules\/)nuxt\/(config|kit|schema)/]) {

packages/nuxt/test/import-protection.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const testsToTriggerOn = [
2222
['nuxt/schema', 'components/Component.vue', true],
2323
['/root/node_modules/@nuxt/kit', 'components/Component.vue', true],
2424
['some-nuxt-module', 'components/Component.vue', true],
25+
['some-nuxt-module/runtime/something.vue', 'components/Component.vue', false],
2526
['/root/src/server/api/test.ts', 'components/Component.vue', true],
2627
['src/server/api/test.ts', 'components/Component.vue', true],
2728
['node_modules/nitropack/node_modules/crossws/dist/adapters/bun.mjs', 'node_modules/nitropack/dist/presets/bun/runtime/bun.mjs', false],
@@ -44,7 +45,10 @@ const transformWithImportProtection = (id: string, importer: string, context: 'n
4445
cwd: '/root',
4546
patterns: createImportProtectionPatterns({
4647
options: {
47-
modules: ['some-nuxt-module'],
48+
_installedModules: [
49+
// @ts-expect-error an incomplete module
50+
{ entryPath: 'some-nuxt-module' },
51+
],
4852
srcDir: '/root/src/',
4953
serverDir: '/root/src/server',
5054
} satisfies Partial<NuxtOptions> as NuxtOptions,

0 commit comments

Comments
 (0)