feat(kit): module compatibility utils (#21246)

This commit is contained in:
Harlan Wilton 2023-06-07 06:36:35 +08:00 committed by GitHub
parent a31899af65
commit c0b3d26b00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 119 additions and 14 deletions

View File

@ -2,6 +2,10 @@ import satisfies from 'semver/functions/satisfies.js' // npm/node-semver#381
import type { Nuxt, NuxtCompatibility, NuxtCompatibilityIssues } from '@nuxt/schema' import type { Nuxt, NuxtCompatibility, NuxtCompatibilityIssues } from '@nuxt/schema'
import { useNuxt } from './context' import { useNuxt } from './context'
export function normalizeSemanticVersion (version: string) {
return version.replace(/-[0-9]+\.[0-9a-f]+/, '') // Remove edge prefix
}
/** /**
* Check version constraints and return incompatibility issues as an array * Check version constraints and return incompatibility issues as an array
*/ */
@ -11,9 +15,7 @@ export async function checkNuxtCompatibility (constraints: NuxtCompatibility, nu
// Nuxt version check // Nuxt version check
if (constraints.nuxt) { if (constraints.nuxt) {
const nuxtVersion = getNuxtVersion(nuxt) const nuxtVersion = getNuxtVersion(nuxt)
const nuxtSemanticVersion = nuxtVersion if (!satisfies(normalizeSemanticVersion(nuxtVersion), constraints.nuxt, { includePrerelease: true })) {
.replace(/-[0-9]+\.[0-9a-f]+/, '') // Remove edge prefix
if (!satisfies(nuxtSemanticVersion, constraints.nuxt, { includePrerelease: true })) {
issues.push({ issues.push({
name: 'nuxt', name: 'nuxt',
message: `Nuxt version \`${constraints.nuxt}\` is required but currently using \`${nuxtVersion}\`` message: `Nuxt version \`${constraints.nuxt}\` is required but currently using \`${nuxtVersion}\``

View File

@ -1,6 +1,7 @@
// Module // Module
export * from './module/define' export * from './module/define'
export * from './module/install' export * from './module/install'
export * from './module/compatibility'
// Loader // Loader
export * from './loader/config' export * from './loader/config'

View File

@ -0,0 +1,45 @@
import { describe, expect, it } from 'vitest'
import { loadNuxt } from '../loader/nuxt'
import { getNuxtModuleVersion, hasNuxtModule, hasNuxtModuleCompatibility } from './compatibility'
import { defineNuxtModule } from './define'
describe('nuxt module compatibility', () => {
it('check module installed', async () => {
const nuxt = await loadNuxt({
overrides: {
modules: [
defineNuxtModule({
meta: {
name: 'nuxt-module-foo'
}
})
]
}
})
expect(hasNuxtModule('nuxt-module-foo', nuxt)).toStrictEqual(true)
await nuxt.close()
})
it('can retrieve module version from module instance', async () => {
const nuxt = await loadNuxt({})
const module = defineNuxtModule({
meta: {
name: 'nuxt-module-foo',
version: '1.0.0'
}
})
expect(await getNuxtModuleVersion(module, nuxt)).toEqual('1.0.0')
await nuxt.close()
})
it('check module instance version compatibility', async () => {
const nuxt = await loadNuxt({})
const module = defineNuxtModule({
meta: {
name: 'nuxt-module-foo',
version: '1.0.0'
}
})
expect(await hasNuxtModuleCompatibility(module, '^1.0.0', nuxt)).toStrictEqual(true)
expect(await hasNuxtModuleCompatibility(module, '^2.0.0', nuxt)).toStrictEqual(false)
await nuxt.close()
})
})

View File

@ -0,0 +1,54 @@
import satisfies from 'semver/functions/satisfies.js' // npm/node-semver#381
import type { Nuxt, NuxtModule } from '@nuxt/schema'
import { useNuxt } from '../context'
import { normalizeSemanticVersion } from '../compatibility'
import { loadNuxtModuleInstance } from './install'
/**
* Check if a Nuxt module is installed by name.
*
* This will check both the installed modules and the modules to be installed. Note
* that it cannot detect if a module is _going to be_ installed programmatically by another module.
*/
export function hasNuxtModule (moduleName: string, nuxt: Nuxt = useNuxt()) : boolean {
return nuxt.options._installedModules.some(({ meta }) => meta.name === moduleName) ||
nuxt.options.modules.includes(moduleName)
}
/**
* Checks if a Nuxt Module is compatible with a given semver version.
*/
export async function hasNuxtModuleCompatibility (module: string | NuxtModule, semverVersion: string, nuxt: Nuxt = useNuxt()): Promise<boolean> {
const version = await getNuxtModuleVersion(module, nuxt)
if (!version) {
return false
}
return satisfies(normalizeSemanticVersion(version), semverVersion, {
includePrerelease: true
})
}
/**
* Get the version of a Nuxt module.
*
* Scans installed modules for the version, if it's not found it will attempt to load the module instance and get the version from there.
*/
export async function getNuxtModuleVersion (module: string | NuxtModule, nuxt: Nuxt | any = useNuxt()): Promise<string | false> {
const moduleMeta = (typeof module === 'string' ? { name: module } : await module.getMeta?.()) || {}
if (moduleMeta.version) { return moduleMeta.version }
// need a name from here
if (!moduleMeta.name) { return false }
// maybe the version got attached within the installed module instance?
const version = nuxt.options._installedModules
// @ts-expect-error _installedModules is not typed
.filter(m => m.meta.name === moduleMeta.name).map(m => m.meta.version)?.[0]
if (version) {
return version
}
// it's possible that the module will be installed, it just hasn't been done yet, preemptively load the instance
if (typeof module !== 'string' && nuxt.options.modules.includes(moduleMeta.name)) {
const { buildTimeModuleMeta } = await loadNuxtModuleInstance(moduleMeta.name, nuxt)
return buildTimeModuleMeta.version || false
}
return false
}

View File

@ -1,6 +1,7 @@
import { lstatSync } from 'node:fs' import { existsSync, promises as fsp, lstatSync } from 'node:fs'
import type { Nuxt, NuxtModule } from '@nuxt/schema' import type { ModuleMeta, Nuxt, NuxtModule } from '@nuxt/schema'
import { dirname, isAbsolute } from 'pathe' import { dirname, isAbsolute, join } from 'pathe'
import { defu } from 'defu'
import { isNuxt2 } from '../compatibility' import { isNuxt2 } from '../compatibility'
import { useNuxt } from '../context' import { useNuxt } from '../context'
import { requireModule } from '../internal/cjs' import { requireModule } from '../internal/cjs'
@ -8,9 +9,8 @@ import { importModule } from '../internal/esm'
import { resolveAlias, resolvePath } from '../resolve' import { resolveAlias, resolvePath } from '../resolve'
/** Installs a module on a Nuxt instance. */ /** Installs a module on a Nuxt instance. */
export async function installModule (moduleToInstall: string | NuxtModule, _inlineOptions?: any, _nuxt?: Nuxt) { export async function installModule (moduleToInstall: string | NuxtModule, inlineOptions?: any, nuxt: Nuxt = useNuxt()) {
const nuxt = useNuxt() const { nuxtModule, buildTimeModuleMeta } = await loadNuxtModuleInstance(moduleToInstall, nuxt)
const { nuxtModule, inlineOptions } = await normalizeModule(moduleToInstall, _inlineOptions)
// Call module // Call module
const res = ( const res = (
@ -29,7 +29,7 @@ export async function installModule (moduleToInstall: string | NuxtModule, _inli
nuxt.options._installedModules = nuxt.options._installedModules || [] nuxt.options._installedModules = nuxt.options._installedModules || []
nuxt.options._installedModules.push({ nuxt.options._installedModules.push({
meta: await nuxtModule.getMeta?.(), meta: defu(await nuxtModule.getMeta?.(), buildTimeModuleMeta),
timings: res.timings, timings: res.timings,
entryPath: typeof moduleToInstall === 'string' ? resolveAlias(moduleToInstall) : undefined entryPath: typeof moduleToInstall === 'string' ? resolveAlias(moduleToInstall) : undefined
}) })
@ -48,9 +48,8 @@ export const normalizeModuleTranspilePath = (p: string) => {
return p.split('node_modules/').pop() as string return p.split('node_modules/').pop() as string
} }
async function normalizeModule (nuxtModule: string | NuxtModule, inlineOptions?: any) { export async function loadNuxtModuleInstance (nuxtModule: string | NuxtModule, nuxt: Nuxt = useNuxt()) {
const nuxt = useNuxt() let buildTimeModuleMeta: ModuleMeta = {}
// Import if input is string // Import if input is string
if (typeof nuxtModule === 'string') { if (typeof nuxtModule === 'string') {
const src = await resolvePath(nuxtModule) const src = await resolvePath(nuxtModule)
@ -61,6 +60,10 @@ async function normalizeModule (nuxtModule: string | NuxtModule, inlineOptions?:
console.error(`Error while requiring module \`${nuxtModule}\`: ${error}`) console.error(`Error while requiring module \`${nuxtModule}\`: ${error}`)
throw error throw error
} }
// nuxt-module-builder generates a module.json with metadata including the version
if (existsSync(join(dirname(src), 'module.json'))) {
buildTimeModuleMeta = JSON.parse(await fsp.readFile(join(dirname(src), 'module.json'), 'utf-8'))
}
} }
// Throw error if input is not a function // Throw error if input is not a function
@ -68,5 +71,5 @@ async function normalizeModule (nuxtModule: string | NuxtModule, inlineOptions?:
throw new TypeError('Nuxt module should be a function: ' + nuxtModule) throw new TypeError('Nuxt module should be a function: ' + nuxtModule)
} }
return { nuxtModule, inlineOptions } as { nuxtModule: NuxtModule<any>, inlineOptions: undefined | Record<string, any> } return { nuxtModule, buildTimeModuleMeta } as { nuxtModule: NuxtModule<any>, buildTimeModuleMeta: ModuleMeta }
} }