mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-13 09:33:54 +00:00
feat(schema,nuxt): add shared/
folder and #shared
alias (#28682)
This commit is contained in:
parent
9fdf90cbde
commit
18d547d5a5
@ -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],
|
||||
}),
|
||||
)
|
||||
|
||||
|
@ -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 })
|
||||
|
@ -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\/)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.')])
|
||||
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 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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -352,6 +352,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.
|
||||
@ -421,12 +426,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,
|
||||
|
@ -8,6 +8,7 @@ import type { ViteConfig } from '@nuxt/schema'
|
||||
import type { PackageJson } from 'pkg-types'
|
||||
import defu from 'defu'
|
||||
import type { Nitro } from 'nitropack'
|
||||
import escapeStringRegexp from 'escape-string-regexp'
|
||||
import type { ViteBuildContext } from './vite'
|
||||
import { createViteLogger } from './utils/logger'
|
||||
import { initViteNodeServer } from './vite-node'
|
||||
@ -81,7 +82,12 @@ export async function buildServer (ctx: ViteBuildContext) {
|
||||
ssr: true,
|
||||
rollupOptions: {
|
||||
input: { server: entry },
|
||||
external: ['#internal/nitro', '#internal/nuxt/paths'],
|
||||
external: [
|
||||
'#internal/nitro',
|
||||
'#internal/nuxt/paths',
|
||||
'#shared',
|
||||
new RegExp('^' + escapeStringRegexp(withTrailingSlash(resolve(ctx.nuxt.options.rootDir, ctx.nuxt.options.dir.shared)))),
|
||||
],
|
||||
output: {
|
||||
entryFileNames: '[name].mjs',
|
||||
format: 'module',
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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 = ['#internal/nitro']
|
||||
const external = [
|
||||
'#internal/nitro',
|
||||
'#shared',
|
||||
resolve(ctx.nuxt.options.rootDir, ctx.nuxt.options.dir.shared),
|
||||
]
|
||||
if (!ctx.nuxt.options.dev) {
|
||||
external.push('#internal/nuxt/paths')
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user