rsnext/packages/next/build/plugins/collect-plugins.ts
Tim Neutkens e125d905a0
Clean up render.tsx options (#13759)
Went through and removed a bunch of internal options which are just pass-through values of buildManifest

Closes #13851
2020-06-06 23:00:03 +00:00

251 lines
6.2 KiB
TypeScript

import findUp from 'next/dist/compiled/find-up'
import { promises } from 'fs'
import path from 'path'
import resolve from 'next/dist/compiled/resolve/index.js'
import { execOnce } from '../../next-server/lib/utils'
export type PluginMetaData = {
requiredEnv: string[]
middleware: string[]
pluginName: string
directory: string
pkgName: string
version: string
config?: { [name: string]: any }
}
// currently supported middleware
export const VALID_MIDDLEWARE = [
'document-head-tags-server',
'on-init-client',
'on-init-server',
'on-error-server',
'on-error-client',
'on-error-client',
'on-error-server',
'babel-preset-build',
'unstable-post-hydration',
'unstable-get-styles-server',
'unstable-enhance-app-server',
]
type ENV_OPTIONS = { [name: string]: string }
const exitWithError = (error: string) => {
console.error(error)
process.exit(1)
}
async function collectPluginMeta(
env: ENV_OPTIONS,
pluginPackagePath: string
): Promise<PluginMetaData> {
const pkgDir = path.dirname(pluginPackagePath)
const pluginPackageJson = require(pluginPackagePath)
const pluginMetaData: {
name: string
'required-env': string[]
} = pluginPackageJson.nextjs
if (!pluginMetaData) {
exitWithError('Next.js plugins need to have a "nextjs" key in package.json')
}
if (!pluginMetaData.name) {
exitWithError(
'Next.js plugins need to have a "nextjs.name" key in package.json'
)
}
// TODO: add err.sh explaining requirements
let middleware: string[] = []
try {
middleware = (
await promises.readdir(path.join(pkgDir, 'src'), { withFileTypes: true })
)
.filter((dirent) => dirent.isFile())
.map((file) => file.name)
} catch (err) {
if (err.code !== 'ENOENT') {
console.error(err)
}
exitWithError(
`Failed to read src/ directory for Next.js plugin: ${pluginMetaData.name}`
)
}
// remove the extension from the middleware
middleware = middleware.map((item) => {
const parts = item.split('.')
parts.pop()
return parts.join('.')
})
const invalidMiddleware: string[] = []
for (const item of middleware) {
if (!VALID_MIDDLEWARE.includes(item)) {
invalidMiddleware.push(item)
}
}
if (invalidMiddleware.length > 0) {
console.error(
`Next.js Plugin: ${
pluginMetaData.name
} listed invalid middleware ${invalidMiddleware.join(', ')}`
)
}
// TODO: investigate requiring plugins' env be prefixed
// somehow to prevent collision
if (!Array.isArray(pluginMetaData['required-env'])) {
exitWithError(
'Next.js plugins need to have a "nextjs.required-env" key in package.json'
)
}
const missingEnvFields: string[] = []
for (const field of pluginMetaData['required-env']) {
if (typeof env[field] === 'undefined') {
missingEnvFields.push(field)
}
}
if (missingEnvFields.length > 0) {
exitWithError(
`Next.js Plugin: ${
pluginMetaData.name
} required env ${missingEnvFields.join(
', '
)} but was missing in your \`next.config.js\``
)
}
return {
middleware,
directory: pkgDir.replace(/\\/g, '/'),
requiredEnv: pluginMetaData['required-env'],
version: pluginPackageJson.version,
pluginName: pluginMetaData.name,
pkgName: pluginPackageJson.name,
}
}
// clean package name so it can be used as variable
export const getPluginId = (pkg: string): string => {
pkg = pkg.replace(/\W/g, '')
if (pkg.match(/^[0-9]/)) {
pkg = `_${pkg}`
}
return pkg
}
type PluginConfig =
| string
| {
name: string
config: { [name: string]: any }
}
async function _collectPlugins(
dir: string,
env: ENV_OPTIONS,
pluginsConfig: PluginConfig[] | undefined
): Promise<PluginMetaData[]> {
let nextPluginNames: string[] = []
const skippedPluginNames: string[] = []
const hasPluginConfig = Array.isArray(pluginsConfig)
const nextPluginConfigNames = hasPluginConfig
? pluginsConfig!.map((config) =>
typeof config === 'string' ? config : config.name
)
: null
const rootPackageJsonPath = await findUp('package.json', { cwd: dir })
if (!rootPackageJsonPath && !nextPluginConfigNames) {
console.log('Failed to load plugins, no package.json')
return []
}
if (rootPackageJsonPath) {
const rootPackageJson = require(rootPackageJsonPath)
let dependencies: string[] = []
if (rootPackageJson.dependencies) {
dependencies = dependencies.concat(
Object.keys(rootPackageJson.dependencies)
)
}
if (rootPackageJson.devDependencies) {
dependencies = dependencies.concat(
Object.keys(rootPackageJson.devDependencies)
)
}
// find packages with the naming convention
// @scope/next-plugin-[name]
// @next/plugin-[name]
// next-plugin-[name]
const filteredDeps = dependencies.filter((name) => {
return name.match(/(^@next\/plugin|next-plugin-)/)
})
if (nextPluginConfigNames) {
for (const dep of filteredDeps) {
if (!nextPluginConfigNames.includes(dep)) {
skippedPluginNames.push(dep)
}
}
nextPluginNames = nextPluginConfigNames
} else {
nextPluginNames = filteredDeps
}
}
const nextPluginMetaData = await Promise.all(
nextPluginNames.map((name) =>
collectPluginMeta(
env,
resolve.sync(path.join(name, 'package.json'), {
basedir: dir,
preserveSymlinks: true,
})
)
)
)
for (const plugin of nextPluginMetaData) {
// Add plugin config from `next.config.js`
if (hasPluginConfig) {
const curPlugin = pluginsConfig!.find(
(config) =>
config && typeof config === 'object' && config.name === plugin.pkgName
)
if (curPlugin && typeof curPlugin === 'object') {
plugin.config = curPlugin.config
}
}
console.log(
`Loaded plugin: ${plugin.pkgName}${
plugin.version ? `@${plugin.version}` : ''
}`
)
}
if (skippedPluginNames.length) {
console.log(
`Plugins config used skipped loading: ${skippedPluginNames.join(', ')}`
)
}
console.log()
return nextPluginMetaData
}
// only execute it once between server/client configs
// since the plugins need to match
export const collectPlugins = execOnce(_collectPlugins)