Nuxt/packages/kit/src/module/utils.ts

360 lines
9.8 KiB
TypeScript
Raw Normal View History

import { existsSync, promises as fsp } from 'fs'
import { basename, extname, parse, resolve } from 'pathe'
import lodashTemplate from 'lodash.template'
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'
2021-04-15 18:49:29 +00:00
/**
* Renders given template using lodash template during build into the project buildDir
2021-04-15 18:49:29 +00:00
*/
export function addTemplate (_template: NuxtTemplate | string) {
const nuxt = useNuxt()
// Noprmalize template
const template = normalizeTemplate(_template)
// Remove any existing template with the same filename
nuxt.options.build.templates = nuxt.options.build.templates
.filter(p => normalizeTemplate(p).filename !== template.filename)
// Add to templates array
nuxt.options.build.templates.push(template)
return template
}
/**
* Normalize a nuxt template object
*/
export function normalizeTemplate (template: NuxtTemplate | string): NuxtTemplate {
if (!template) {
throw new Error('Invalid template: ' + JSON.stringify(template))
}
// Normalize
if (typeof template === 'string') {
template = { src: template }
} else {
template = { ...template }
}
// Use src if provided
if (template.src) {
if (!existsSync(template.src)) {
throw new Error('Template not found: ' + template.src)
}
if (!template.filename) {
const srcPath = parse(template.src)
template.filename = template.fileName ||
`${basename(srcPath.dir)}.${srcPath.name}.${hash(template.src)}${srcPath.ext}`
}
}
if (!template.src && !template.getContents) {
throw new Error('Invalid template. Either getContents or src options should be provided: ' + JSON.stringify(template))
}
if (!template.filename) {
throw new Error('Invalid template. Either filename should be provided: ' + JSON.stringify(template))
}
// Resolve dst
if (!template.dst) {
const nuxt = useNuxt()
template.dst = resolve(nuxt.options.buildDir, template.filename)
}
return template
}
/**
* Normalize a nuxt plugin object
*/
export function normalizePlugin (plugin: NuxtPlugin | string): NuxtPlugin {
// Normalize src
if (typeof plugin === 'string') {
plugin = { src: plugin }
} else {
plugin = { ...plugin }
}
if (!plugin.src) {
throw new Error('Invalid plugin. src option is required: ' + JSON.stringify(plugin))
}
// Normalize mode
if (plugin.ssr) {
plugin.mode = 'server'
}
if (!plugin.mode) {
const [, mode = 'all'] = plugin.src.match(/\.(server|client)(\.\w+)*$/) || []
plugin.mode = mode as 'all' | 'client' | 'server'
}
return plugin
}
2021-04-15 18:49:29 +00:00
/**
* Registers a nuxt plugin and to the plugins array.
2021-04-15 18:49:29 +00:00
*
* Note: You can use mode or .client and .server modifiers with fileName option
* to use plugin only in client or server side.
*
* Note: By default plugin is prepended to the plugins array. You can use second argument to append (push) instead.
2021-04-15 18:49:29 +00:00
*
* @example
* ```js
* addPlugin({
* src: path.resolve(__dirname, 'templates/foo.js'),
* filename: 'foo.server.js' // [optional] only include in server bundle
2021-04-15 18:49:29 +00:00
* })
* ```
*/
export interface AddPluginOptions { append?: Boolean }
export function addPlugin (_plugin: NuxtPlugin | string, opts: AddPluginOptions = {}) {
const nuxt = useNuxt()
// Normalize plugin
const plugin = normalizePlugin(_plugin)
// Remove any existing plugin with the same src
nuxt.options.plugins = nuxt.options.plugins.filter(p => normalizePlugin(p).src !== plugin.src)
// Prepend to array by default to be before user provided plugins since is usually used by modules
nuxt.options.plugins[opts.append ? 'push' : 'unshift'](plugin)
return plugin
}
2021-04-15 18:49:29 +00:00
/**
* Adds a template and registers as a nuxt plugin.
2021-04-15 18:49:29 +00:00
*/
export function addPluginTemplate (plugin: NuxtPluginTemplate | string, opts: AddPluginOptions = {}): NuxtPluginTemplate {
if (typeof plugin === 'string') {
plugin = { src: plugin }
}
// Update plugin src to template destination
plugin.src = addTemplate(plugin).dst
return addPlugin(plugin, opts)
}
2021-04-15 18:49:29 +00:00
/** Adds a new server middleware to the end of the server middleware array. */
export function addServerMiddleware (middleware) {
useNuxt().options.serverMiddleware.push(middleware)
}
export interface ExtendConfigOptions {
/**
* Install plugin on dev
*
* @default true
*/
dev?: boolean
/**
* Install plugin on build
*
* @default true
*/
build?: boolean
}
export interface ExtendWebpackConfigOptions extends ExtendConfigOptions {
/**
* Install plugin on server side
*
* @default true
*/
server?: boolean
/**
* Install plugin on client side
*
* @default true
*/
client?: boolean
}
export interface ExtendViteConfigOptions extends ExtendConfigOptions {}
/**
* Extend Webpack config
*
* The fallback function might be called multiple times
* when applying to both client and server builds.
*/
export function extendWebpackConfig (
fn: ((config: WebpackConfig)=> void),
options: ExtendWebpackConfigOptions = {}
) {
const nuxt = useNuxt()
if (options.dev === false && nuxt.options.dev) {
return
}
if (options.build === false && nuxt.options.build) {
return
}
nuxt.hook('webpack:config', (configs: WebpackConfig[]) => {
if (options.server !== false) {
const config = configs.find(i => i.name === 'server')
if (config) {
fn(config)
}
}
if (options.client !== false) {
const config = configs.find(i => i.name === 'client')
if (config) {
fn(config)
}
}
})
}
/**
* Extend Vite config
*/
export function extendViteConfig (
fn: ((config: ViteConfig) => void),
options: ExtendViteConfigOptions = {}
) {
const nuxt = useNuxt()
if (options.dev === false && nuxt.options.dev) {
return
}
if (options.build === false && nuxt.options.build) {
return
}
nuxt.hook('vite:extend', ({ config }) => fn(config))
}
/**
* Append Webpack plugin to the config.
*/
export function addWebpackPlugin (plugin: WebpackPluginInstance, options?: ExtendWebpackConfigOptions) {
extendWebpackConfig((config) => {
config.plugins = config.plugins || []
config.plugins.push(plugin)
}, options)
}
/**
* Append Vite plugin to the config.
*/
export function addVitePlugin (plugin: VitePlugin, options?: ExtendViteConfigOptions) {
extendViteConfig((config) => {
config.plugins = config.plugins || []
config.plugins.push(plugin)
}, options)
}
2021-09-05 21:21:33 +00:00
export async function compileTemplate (template: NuxtTemplate, ctx: any) {
const data = { ...ctx, ...template.options }
if (template.src) {
try {
const srcContents = await fsp.readFile(template.src, 'utf-8')
return lodashTemplate(srcContents, {})(data)
} catch (err) {
console.error('Error compiling template: ', template)
throw err
}
}
if (template.getContents) {
return template.getContents(data)
}
throw new Error('Invalid template: ' + JSON.stringify(template))
}
const serialize = data => JSON.stringify(data, null, 2).replace(/"{(.+)}"/g, '$1')
const importName = (src: string) => `${camelCase(basename(src, extname(src))).replace(/[^a-zA-Z?\d\s:]/g, '')}_${hash(src)}`
const importSources = (sources: string | string[], { lazy = false } = {}) => {
if (!Array.isArray(sources)) {
sources = [sources]
}
return sources.map((src) => {
if (lazy) {
return `const ${importName(src)} = () => import('${src}' /* webpackChunkName: '${src}' */)`
}
return `import ${importName(src)} from '${src}'`
}).join('\n')
}
export const templateUtils = {
serialize,
importName,
importSources
}
/**
* Check if current nuxt instance is version 2 legacy
*/
export function isNuxt2 (nuxt: Nuxt = useNuxt()) {
return getNuxtVersion(nuxt).startsWith('2.')
}
/**
2021-10-05 17:40:20 +00:00
* Check if current nuxt instance is version 3
*/
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
}