feat(schema,nuxt): add shared/ folder and #shared alias (#28682)

This commit is contained in:
Estéban 2024-11-02 22:13:41 +01:00 committed by GitHub
parent a19391df43
commit be892629eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 102 additions and 32 deletions

View File

@ -17,7 +17,7 @@ import { version as nuxtVersion } from '../../package.json'
import { distDir } from '../dirs'
import { toArray } from '../utils'
import { template as defaultSpaLoadingTemplate } from '../../../ui-templates/dist/templates/spa-loading-icon'
import { nuxtImportProtections } from './plugins/import-protection'
import { createImportProtectionPatterns } from './plugins/import-protection'
import { EXTENSION_RE } from './utils'
const logLevelMapReverse = {
@ -49,6 +49,8 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
.map(m => m.entryPath!),
)
const isNuxtV4 = nuxt.options.future?.compatibilityVersion === 4
const nitroConfig: NitroConfig = defu(nuxt.options.nitro, {
debug: nuxt.options.debug,
rootDir: nuxt.options.rootDir,
@ -66,6 +68,12 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
},
imports: {
autoImport: nuxt.options.imports.autoImport as boolean,
dirs: isNuxtV4
? [
resolve(nuxt.options.rootDir, 'shared', 'utils'),
resolve(nuxt.options.rootDir, 'shared', 'types'),
]
: [],
imports: [
{
as: '__buildAssetsURL',
@ -362,11 +370,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,
patterns: nuxtImportProtections(nuxt, { isNitro: true }),
exclude: [/core[\\/]runtime[\\/]nitro[\\/]renderer/],
include: sharedPatterns,
patterns: createImportProtectionPatterns(nuxt, { context: 'shared' }),
}),
ImpoundPlugin.rollup({
cwd: nuxt.options.rootDir,
patterns: createImportProtectionPatterns(nuxt, { context: 'nitro-app' }),
exclude: [/core[\\/]runtime[\\/]nitro[\\/]renderer/, ...sharedPatterns],
}),
)

View File

@ -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'
@ -32,7 +31,7 @@ import { distDir, pkgDir } from '../dirs'
import { version } from '../../package.json'
import { scriptsStubsPreset } from '../imports/presets'
import { resolveTypePath } from './utils/types'
import { nuxtImportProtections } from './plugins/import-protection'
import { createImportProtectionPatterns } from './plugins/import-protection'
import { UnctxTransformPlugin } from './plugins/unctx'
import { TreeShakeComposablesPlugin } from './plugins/tree-shake'
import { DevOnlyPlugin } from './plugins/dev-only'
@ -249,16 +248,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')],
patterns: nuxtImportProtections(nuxt),
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 })

View File

@ -9,12 +9,17 @@ interface ImportProtectionOptions {
exclude?: Array<RegExp | string>
}
export const nuxtImportProtections = (nuxt: { options: NuxtOptions }, options: { isNitro?: boolean } = {}) => {
interface NuxtImportProtectionOptions {
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.isNitro ? '' : ' 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([
@ -26,27 +31,33 @@ export const nuxtImportProtections = (nuxt: { options: NuxtOptions }, options: {
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.isNitro ? ' in server runtime.' : ' in the Vue part of your app.')])
patterns.push([i, `This module cannot be imported in ${context}.`])
}
if (options.isNitro) {
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.isNitro) {
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

View File

@ -54,6 +54,8 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
await nuxt.callHook('imports:context', ctx)
const isNuxtV4 = nuxt.options.future?.compatibilityVersion === 4
// composables/ dirs from all layers
let composablesDirs: string[] = []
if (options.scan) {
@ -64,6 +66,12 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
}
composablesDirs.push(resolve(layer.config.srcDir, 'composables'))
composablesDirs.push(resolve(layer.config.srcDir, 'utils'))
if (isNuxtV4) {
composablesDirs.push(resolve(layer.config.rootDir, 'shared', 'utils'))
composablesDirs.push(resolve(layer.config.rootDir, 'shared', 'types'))
}
for (const dir of (layer.config.imports?.dirs ?? [])) {
if (!dir) {
continue

View File

@ -1,7 +1,7 @@
import { normalize } from 'pathe'
import { describe, expect, it } from 'vitest'
import { ImpoundPlugin } from 'impound'
import { nuxtImportProtections } from '../src/core/plugins/import-protection'
import { createImportProtectionPatterns } from '../src/core/plugins/import-protection'
import type { NuxtOptions } from '../schema'
const testsToTriggerOn = [
@ -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 {
@ -38,16 +38,16 @@ 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: nuxtImportProtections({
patterns: createImportProtectionPatterns({
options: {
modules: ['some-nuxt-module'],
srcDir: '/root/src/',
serverDir: '/root/src/server',
} satisfies Partial<NuxtOptions> as NuxtOptions,
}),
}, { context }),
})
return (plugin as any).resolveId.call({ error: () => {} }, id, importer)

View File

@ -355,6 +355,11 @@ export default defineUntypedSchema({
*/
plugins: 'plugins',
/**
* The shared directory. This directory is shared between the app and the server.
*/
shared: 'shared',
/**
* The directory containing your static files, which will be directly accessible via the Nuxt server
* and copied across into your `dist` folder when your app is generated.
@ -424,12 +429,13 @@ export default defineUntypedSchema({
*/
alias: {
$resolve: async (val: Record<string, string>, get): Promise<Record<string, string>> => {
const [srcDir, rootDir, assetsDir, publicDir, buildDir] = await Promise.all([get('srcDir'), get('rootDir'), get('dir.assets'), get('dir.public'), get('buildDir')]) as [string, string, string, string, string]
const [srcDir, rootDir, assetsDir, publicDir, buildDir, sharedDir] = await Promise.all([get('srcDir'), get('rootDir'), get('dir.assets'), get('dir.public'), get('buildDir'), get('dir.shared')]) as [string, string, string, string, string, string]
return {
'~': srcDir,
'@': srcDir,
'~~': rootDir,
'@@': rootDir,
'#shared': resolve(rootDir, sharedDir),
[basename(assetsDir)]: resolve(srcDir, assetsDir),
[basename(publicDir)]: resolve(srcDir, publicDir),
'#build': buildDir,

View File

@ -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'
@ -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',

View File

@ -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:/,
@ -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)
}

View File

@ -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) {

View File

@ -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'
@ -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')
}