diff --git a/packages/bridge/src/module.ts b/packages/bridge/src/module.ts index d45a4f585c..3eb3c14fec 100644 --- a/packages/bridge/src/module.ts +++ b/packages/bridge/src/module.ts @@ -1,5 +1,5 @@ import { createRequire } from 'module' -import { defineNuxtModule, installModule } from '@nuxt/kit' +import { defineNuxtModule, installModule, checkNuxtCompatibilityIssues } from '@nuxt/kit' import { setupNitroBridge } from './nitro' import { setupAppBridge } from './app' import { setupCAPIBridge } from './capi' @@ -15,6 +15,7 @@ export default defineNuxtModule({ app: {}, capi: {}, globalImports: true, + constraints: true, // TODO: Remove from 2.16 postcss8: true, swc: true, @@ -50,5 +51,18 @@ export default defineNuxtModule({ if (opts.resolve) { setupBetterResolve() } + if (opts.constraints) { + nuxt.hook('modules:done', (moduleContainer: any) => { + for (const [name, m] of Object.entries(moduleContainer.requiredModules || {})) { + const requires = (m as any)?.handler?.meta?.requires + if (requires) { + const issues = checkNuxtCompatibilityIssues(requires, nuxt) + if (issues.length) { + console.warn(`[bridge] Detected module incompatibility issues for \`${name}\`:\n` + issues.toString()) + } + } + } + }) + } } }) diff --git a/packages/kit/package.json b/packages/kit/package.json index bdde8b88f9..999da46a71 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -15,6 +15,7 @@ }, "devDependencies": { "@types/lodash.template": "^4", + "@types/semver": "^7", "unbuild": "latest" }, "dependencies": { @@ -30,6 +31,7 @@ "pathe": "^0.2.0", "rc9": "^1.2.0", "scule": "^0.2.1", + "semver": "^7.3.5", "std-env": "^2.3.1", "ufo": "^0.7.9", "unctx": "^1.0.2", diff --git a/packages/kit/src/module/define.ts b/packages/kit/src/module/define.ts index 71c1dd8650..c47f368d33 100644 --- a/packages/kit/src/module/define.ts +++ b/packages/kit/src/module/define.ts @@ -1,10 +1,11 @@ import { promises as fsp } from 'fs' import defu from 'defu' import { applyDefaults } from 'untyped' +import consola from 'consola' import { useNuxt, nuxtCtx } from '../nuxt' import type { Nuxt, NuxtTemplate } from '../types/nuxt' import type { NuxtModule, LegacyNuxtModule, ModuleOptions } from '../types/module' -import { compileTemplate, isNuxt2, templateUtils } from './utils' +import { checkNuxtCompatibilityIssues, compileTemplate, isNuxt2, templateUtils } from './utils' /** * Define a Nuxt module, automatically merging defaults with user provided options, installing @@ -34,6 +35,15 @@ export function defineNuxtModule (input: NuxtMod return } + // check nuxt version range + if (mod.requires) { + const issues = checkNuxtCompatibilityIssues(mod.requires, nuxt) + if (issues.length) { + consola.warn(`Module \`${mod.name}\` is disabled due to incompatibility issues:\n${issues.toString()}`) + return + } + } + // Resolve options const configKey = mod.configKey || mod.name const userOptions = defu(inlineOptions, nuxt.options[configKey]) as OptionsT diff --git a/packages/kit/src/module/utils.ts b/packages/kit/src/module/utils.ts index 6ebc9bef5d..0c3267efee 100644 --- a/packages/kit/src/module/utils.ts +++ b/packages/kit/src/module/utils.ts @@ -5,6 +5,9 @@ import hash from 'hash-sum' import type { WebpackPluginInstance, Configuration as WebpackConfig } from 'webpack' import type { Plugin as VitePlugin, UserConfig as ViteConfig } from 'vite' import { camelCase } from 'scule' +import semver from 'semver' +import { NuxtCompatibilityConstraints, NuxtCompatibilityIssues } from '../types/module' +import { Nuxt } from '../types/nuxt' import { useNuxt } from '../nuxt' import type { NuxtTemplate, NuxtPlugin, NuxtPluginTemplate } from '../types/nuxt' @@ -255,15 +258,6 @@ export function addVitePlugin (plugin: VitePlugin, options?: ExtendViteConfigOpt }, options) } -/** - * Check if current nuxt instance is version 2 legacy - */ -export function isNuxt2 (nuxt?: any) { - nuxt = nuxt || useNuxt() - const version = (nuxt?.version || nuxt?.constructor?.version || '').replace(/^v|-.*$/g, '') - return version.startsWith('2.') -} - export async function compileTemplate (template: NuxtTemplate, ctx: any) { const data = { ...ctx, ...template.options } if (template.src) { @@ -302,3 +296,64 @@ export const templateUtils = { importName, importSources } + +/** + * Check if current nuxt instance is version 2 legacy + */ +export function isNuxt2 (nuxt: Nuxt = useNuxt()) { + return getNuxtVersion(nuxt).startsWith('2.') +} + +/** + * Check if current nuxt instance is version 2 legacy + */ +export function isNuxt3 (nuxt: Nuxt = useNuxt()) { + return getNuxtVersion(nuxt).startsWith('3.') +} + +/** + * Get nuxt version + */ +export function getNuxtVersion (nuxt: Nuxt | any = useNuxt() /* TODO: LegacyNuxt */) { + const version = (nuxt?._version || nuxt?.version || nuxt?.constructor?.version || '').replace(/^v/g, '') + if (!version) { + throw new Error('Cannot determine nuxt version! Is currect instance passed?') + } + return version +} + +/** + * Check version constraints and return incompatibility issues as an array + */ +export function checkNuxtCompatibilityIssues (constraints: NuxtCompatibilityConstraints, nuxt: Nuxt = useNuxt()): NuxtCompatibilityIssues { + const issues: NuxtCompatibilityIssues = [] + if (constraints.nuxt) { + const nuxtVersion = getNuxtVersion(nuxt) + if (!semver.satisfies(nuxtVersion, constraints.nuxt)) { + issues.push({ + name: 'nuxt', + message: `Nuxt version \`${constraints.nuxt}\` is required but currently using \`${nuxtVersion}\`` + }) + } + } + issues.toString = () => issues.map(issue => ` - [${issue.name}] ${issue.message}`).join('\n') + return issues +} + +/** + * Check version constraints and throw a detailed error if has any, otherwise returns true + */ +export function ensureNuxtCompatibility (constraints: NuxtCompatibilityConstraints, nuxt: Nuxt = useNuxt()): true { + const issues = checkNuxtCompatibilityIssues(constraints, nuxt) + if (issues.length) { + throw new Error('Nuxt compatibility issues found:\n' + issues.toString()) + } + return true +} + +/** + * Check version constraints and return true if passed, otherwise returns false + */ +export function hasNuxtCompatibility (constraints: NuxtCompatibilityConstraints, nuxt: Nuxt = useNuxt()) { + return !checkNuxtCompatibilityIssues(constraints, nuxt).length +} diff --git a/packages/kit/src/types/module.ts b/packages/kit/src/types/module.ts index 648c469c2e..2ca39e702e 100644 --- a/packages/kit/src/types/module.ts +++ b/packages/kit/src/types/module.ts @@ -2,14 +2,43 @@ import type { ModuleContainer } from '../module/container' import { Nuxt } from './nuxt' import { NuxtHooks } from './hooks' +export interface NuxtCompatibilityConstraints { + /** + * Required nuxt version. for example, `^2.14.0` or `>=3.0.0-27219851.6e49637`. + */ + nuxt?: string +} + +export interface NuxtCompatibilityIssue { + name: string + message: string +} + +export interface NuxtCompatibilityIssues extends Array { + /** + * Return formatted error message + */ + toString(): string +} + export interface ModuleMeta { - /** The module name. */ + /** Module name */ name?: string + + /** Module version */ + version?: string + /** * The configuration key used within `nuxt.config` for this module's options. * For example, `@nuxtjs/axios` uses `axios`. */ configKey?: string + + /** + * Semver constraints for the versions of Nuxt or features this module are supported. + */ + requires?: NuxtCompatibilityConstraints + [key: string]: any } diff --git a/packages/kit/src/types/nuxt.ts b/packages/kit/src/types/nuxt.ts index 2f218d73a9..673dbb1720 100644 --- a/packages/kit/src/types/nuxt.ts +++ b/packages/kit/src/types/nuxt.ts @@ -3,9 +3,11 @@ import type { NuxtHooks } from './hooks' import type { NuxtOptions } from './config' export interface Nuxt { + // Private fields + _version: string + /** The resolved Nuxt configuration. */ options: NuxtOptions - hooks: Hookable hook: Nuxt['hooks']['hook'] callHook: Nuxt['hooks']['callHook'] diff --git a/packages/nuxt3/package.json b/packages/nuxt3/package.json index f578846cb3..6151f76029 100644 --- a/packages/nuxt3/package.json +++ b/packages/nuxt3/package.json @@ -1,6 +1,6 @@ { "name": "nuxt3", - "version": "0.10.0", + "version": "3.0.0", "repository": "nuxt/framework", "license": "MIT", "type": "module", diff --git a/packages/nuxt3/src/core/nuxt.ts b/packages/nuxt3/src/core/nuxt.ts index 6a36a1d31d..eb9944dd6f 100644 --- a/packages/nuxt3/src/core/nuxt.ts +++ b/packages/nuxt3/src/core/nuxt.ts @@ -6,12 +6,14 @@ import metaModule from '../meta/module' import componentsModule from '../components/module' import globalImportsModule from '../global-imports/module' import { distDir, pkgDir } from '../dirs' +import { version } from '../../package.json' import { initNitro } from './nitro' export function createNuxt (options: NuxtOptions): Nuxt { const hooks = createHooks() const nuxt: Nuxt = { + _version: version, options, hooks, callHook: hooks.callHook, diff --git a/yarn.lock b/yarn.lock index 0eabb3743c..569d446726 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1494,6 +1494,7 @@ __metadata: resolution: "@nuxt/kit@workspace:packages/kit" dependencies: "@types/lodash.template": ^4 + "@types/semver": ^7 consola: ^2.15.3 create-require: ^1.1.1 defu: ^5.0.0 @@ -1506,6 +1507,7 @@ __metadata: pathe: ^0.2.0 rc9: ^1.2.0 scule: ^0.2.1 + semver: ^7.3.5 std-env: ^2.3.1 ufo: ^0.7.9 unbuild: latest @@ -2408,6 +2410,13 @@ __metadata: languageName: node linkType: hard +"@types/semver@npm:^7": + version: 7.3.8 + resolution: "@types/semver@npm:7.3.8" + checksum: bc90f5a9d5430e36f766c08c898e3c28af88830ebc7736baef8ffc74783bad2efb32f29c40d450e85fc341847ee74e2dd97b76cfc7da407e4232ba9ecae4ff9c + languageName: node + linkType: hard + "@types/serve-static@npm:^1.13.10": version: 1.13.10 resolution: "@types/serve-static@npm:1.13.10"