Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(schema,nuxt): add shared folder to alias and auto-imports #28682

Merged
merged 7 commits into from
Nov 2, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix: protect against imports in #shared dir + share chunks with nitro
  • Loading branch information
danielroe committed Oct 10, 2024
commit 8693d510470688ac4398e3dd50a07e218bfc651b
11 changes: 10 additions & 1 deletion packages/nuxt/src/core/nitro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,11 +366,20 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
// Register nuxt protection patterns
nitroConfig.rollupConfig!.plugins = await nitroConfig.rollupConfig!.plugins || []
nitroConfig.rollupConfig!.plugins = toArray(nitroConfig.rollupConfig!.plugins)

const sharedDir = withTrailingSlash(resolve(nuxt.options.rootDir, nuxt.options.dir.shared))
const relativeSharedDir = withTrailingSlash(relative(nuxt.options.rootDir, resolve(nuxt.options.rootDir, nuxt.options.dir.shared)))
const sharedPatterns = [/^#shared\//, new RegExp('^' + escapeRE(sharedDir)), new RegExp('^' + escapeRE(relativeSharedDir))]
nitroConfig.rollupConfig!.plugins!.push(
ImpoundPlugin.rollup({
cwd: nuxt.options.rootDir,
include: sharedPatterns,
patterns: createImportProtectionPatterns(nuxt, { context: 'shared' }),
}),
ImpoundPlugin.rollup({
cwd: nuxt.options.rootDir,
patterns: createImportProtectionPatterns(nuxt, { context: 'nitro-app' }),
exclude: [/core[\\/]runtime[\\/]nitro[\\/]renderer/],
exclude: [/core[\\/]runtime[\\/]nitro[\\/]renderer/, ...sharedPatterns],
}),
)

Expand Down
23 changes: 17 additions & 6 deletions packages/nuxt/src/core/nuxt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import type { DateString } from 'compatx'
import escapeRE from 'escape-string-regexp'
import { withTrailingSlash, withoutLeadingSlash } from 'ufo'
import { ImpoundPlugin } from 'impound'
import type { ImpoundOptions } from 'impound'
import defu from 'defu'
import { gt, satisfies } from 'semver'
import { hasTTY, isCI } from 'std-env'
Expand Down Expand Up @@ -248,16 +247,28 @@ async function initNuxt (nuxt: Nuxt) {
// Add plugin normalization plugin
addBuildPlugin(RemovePluginMetadataPlugin(nuxt))

// shared folder import protection
const sharedDir = withTrailingSlash(resolve(nuxt.options.rootDir, nuxt.options.dir.shared))
const relativeSharedDir = withTrailingSlash(relative(nuxt.options.rootDir, resolve(nuxt.options.rootDir, nuxt.options.dir.shared)))
const sharedPatterns = [/^#shared\//, new RegExp('^' + escapeRE(sharedDir)), new RegExp('^' + escapeRE(relativeSharedDir))]
const sharedProtectionConfig = {
cwd: nuxt.options.rootDir,
include: sharedPatterns,
patterns: createImportProtectionPatterns(nuxt, { context: 'shared' }),
}
addVitePlugin(() => ImpoundPlugin.vite(sharedProtectionConfig), { server: false })
addWebpackPlugin(() => ImpoundPlugin.webpack(sharedProtectionConfig), { server: false })

// Add import protection
const config: ImpoundOptions = {
const nuxtProtectionConfig = {
cwd: nuxt.options.rootDir,
// Exclude top-level resolutions by plugins
exclude: [join(nuxt.options.srcDir, 'index.html')],
exclude: [relative(nuxt.options.rootDir, join(nuxt.options.srcDir, 'index.html')), ...sharedPatterns],
patterns: createImportProtectionPatterns(nuxt, { context: 'nuxt-app' }),
}
addVitePlugin(() => Object.assign(ImpoundPlugin.vite({ ...config, error: false }), { name: 'nuxt:import-protection' }), { client: false })
addVitePlugin(() => Object.assign(ImpoundPlugin.vite({ ...config, error: true }), { name: 'nuxt:import-protection' }), { server: false })
addWebpackPlugin(() => ImpoundPlugin.webpack(config))
addVitePlugin(() => Object.assign(ImpoundPlugin.vite({ ...nuxtProtectionConfig, error: false }), { name: 'nuxt:import-protection' }), { client: false })
addVitePlugin(() => Object.assign(ImpoundPlugin.vite({ ...nuxtProtectionConfig, error: true }), { name: 'nuxt:import-protection' }), { server: false })
addWebpackPlugin(() => ImpoundPlugin.webpack(nuxtProtectionConfig))

// add resolver for modules used in virtual files
addVitePlugin(() => resolveDeepImportsPlugin(nuxt), { client: false })
Expand Down
23 changes: 15 additions & 8 deletions packages/nuxt/src/core/plugins/import-protection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ interface ImportProtectionOptions {
}

interface NuxtImportProtectionOptions {
context: 'nuxt-app' | 'nitro-app'
context: 'nuxt-app' | 'nitro-app' | 'shared'
}

export const createImportProtectionPatterns = (nuxt: { options: NuxtOptions }, options: NuxtImportProtectionOptions) => {
const patterns: ImportProtectionOptions['patterns'] = []
const context = contextFlags[options.context]

patterns.push([
/^(nuxt|nuxt3|nuxt-nightly)$/,
'`nuxt`, `nuxt3` or `nuxt-nightly` cannot be imported directly.' + (options.context === 'nitro-app' ? '' : ' Instead, import runtime Nuxt composables from `#app` or `#imports`.'),
`\`nuxt\`, or \`nuxt-nightly\` cannot be imported directly in ${context}.` + (options.context === 'nuxt-app' ? ' Instead, import runtime Nuxt composables from `#app` or `#imports`.' : ''),
])

patterns.push([
Expand All @@ -30,27 +31,33 @@ export const createImportProtectionPatterns = (nuxt: { options: NuxtOptions }, o

for (const mod of nuxt.options.modules.filter(m => typeof m === 'string')) {
patterns.push([
new RegExp(`^${escapeRE(mod as string)}$`),
new RegExp(`^${escapeRE(mod)}$`),
'Importing directly from module entry-points is not allowed.',
])
}

for (const i of [/(^|node_modules\/)@nuxt\/(kit|test-utils)/, /(^|node_modules\/)nuxi/, /(^|node_modules\/)nitro(?:pack)?(?:-nightly)?(?:$|\/)(?!(?:dist\/)?runtime|types)/, /(^|node_modules\/)nuxt\/(config|kit|schema)/]) {
patterns.push([i, 'This module cannot be imported' + (options.context === 'nitro-app' ? ' in server runtime.' : ' in the Vue part of your app.')])
patterns.push([i, `This module cannot be imported in ${context}.`])
}

if (options.context === 'nitro-app') {
if (options.context === 'nitro-app' || options.context === 'shared') {
for (const i of ['#app', /^#build(\/|$)/]) {
patterns.push([i, 'Vue app aliases are not allowed in server runtime.'])
patterns.push([i, `Vue app aliases are not allowed in ${context}.`])
}
}

if (options.context === 'nuxt-app') {
if (options.context === 'nuxt-app' || options.context === 'shared') {
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.',
`Importing from server is not allowed in ${context}.`,
])
}

return patterns
}

const contextFlags = {
'nitro-app': 'server runtime',
'nuxt-app': 'the Vue part of your app',
'shared': 'the #shared directory',
} as const
6 changes: 3 additions & 3 deletions packages/nuxt/test/import-protection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const testsToTriggerOn = [

describe('import protection', () => {
it.each(testsToTriggerOn)('should protect %s', async (id, importer, isProtected) => {
const result = await transformWithImportProtection(id, importer)
const result = await transformWithImportProtection(id, importer, 'nuxt-app')
if (!isProtected) {
expect(result).toBeNull()
} else {
Expand All @@ -38,7 +38,7 @@ describe('import protection', () => {
})
})

const transformWithImportProtection = (id: string, importer: string) => {
const transformWithImportProtection = (id: string, importer: string, context: 'nitro-app' | 'nuxt-app' | 'shared') => {
const plugin = ImpoundPlugin.rollup({
cwd: '/root',
patterns: createImportProtectionPatterns({
Expand All @@ -47,7 +47,7 @@ const transformWithImportProtection = (id: string, importer: string) => {
srcDir: '/root/src/',
serverDir: '/root/src/server',
} satisfies Partial<NuxtOptions> as NuxtOptions,
}, { context: 'nuxt-app' }),
}, { context }),
})

return (plugin as any).resolveId.call({ error: () => {} }, id, importer)
Expand Down
9 changes: 8 additions & 1 deletion packages/vite/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { joinURL, withTrailingSlash, withoutLeadingSlash } from 'ufo'
import type { ViteConfig } from '@nuxt/schema'
import defu from 'defu'
import type { Nitro } from 'nitro/types'
import escapeStringRegexp from 'escape-string-regexp'
import type { ViteBuildContext } from './vite'
import { createViteLogger } from './utils/logger'
import { initViteNodeServer } from './vite-node'
Expand Down Expand Up @@ -80,7 +81,13 @@ export async function buildServer (ctx: ViteBuildContext) {
ssr: true,
rollupOptions: {
input: { server: entry },
external: ['nitro/runtime', '#internal/nuxt/paths', '#internal/nuxt/app-config'],
external: [
'nitro/runtime',
'#internal/nuxt/paths',
'#internal/nuxt/app-config',
'#shared',
new RegExp('^' + escapeStringRegexp(withTrailingSlash(resolve(ctx.nuxt.options.rootDir, ctx.nuxt.options.dir.shared)))),
],
output: {
entryFileNames: '[name].mjs',
format: 'module',
Expand Down
12 changes: 9 additions & 3 deletions packages/vite/src/utils/external.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import type { ExternalsOptions } from 'externality'
import { ExternalsDefaults, isExternal } from 'externality'
import type { ViteDevServer } from 'vite'
import escapeStringRegexp from 'escape-string-regexp'
import { withTrailingSlash } from 'ufo'
import type { Nuxt } from 'nuxt/schema'
import { resolve } from 'pathe'
import { toArray } from '.'

export function createIsExternal (viteServer: ViteDevServer, rootDir: string, modulesDirs?: string[]) {
export function createIsExternal (viteServer: ViteDevServer, nuxt: Nuxt) {
const externalOpts: ExternalsOptions = {
inline: [
/virtual:/,
Expand All @@ -16,15 +20,17 @@ export function createIsExternal (viteServer: ViteDevServer, rootDir: string, mo
),
],
external: [
'#shared',
new RegExp('^' + escapeStringRegexp(withTrailingSlash(resolve(nuxt.options.rootDir, nuxt.options.dir.shared)))),
...(viteServer.config.ssr.external as string[]) || [],
/node_modules/,
],
resolve: {
modules: modulesDirs,
modules: nuxt.options.modulesDir,
type: 'module',
extensions: ['.ts', '.js', '.json', '.vue', '.mjs', '.jsx', '.tsx', '.wasm'],
},
}

return (id: string) => isExternal(id, rootDir, externalOpts)
return (id: string) => isExternal(id, nuxt.options.rootDir, externalOpts)
}
2 changes: 1 addition & 1 deletion packages/vite/src/vite-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ function createViteNodeApp (ctx: ViteBuildContext, invalidates: Set<string> = ne
},
})

const isExternal = createIsExternal(viteServer, ctx.nuxt.options.rootDir, ctx.nuxt.options.modulesDir)
const isExternal = createIsExternal(viteServer, ctx.nuxt)
node.shouldExternalize = async (id: string) => {
const result = await isExternal(id)
if (result?.external) {
Expand Down
8 changes: 6 additions & 2 deletions packages/webpack/src/configs/server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isAbsolute } from 'pathe'
import { isAbsolute, resolve } from 'pathe'
import ForkTSCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'
import { logger } from '@nuxt/kit'
import type { WebpackConfigContext } from '../utils/config'
Expand Down Expand Up @@ -53,7 +53,11 @@ function serverStandalone (ctx: WebpackConfigContext) {
'#',
...ctx.options.build.transpile,
]
const external = ['nitro/runtime']
const external = [
'nitro/runtime',
'#shared',
resolve(ctx.nuxt.options.rootDir, ctx.nuxt.options.dir.shared),
]
if (!ctx.nuxt.options.dev) {
external.push('#internal/nuxt/paths', '#internal/nuxt/app-config')
}
Expand Down
Loading