Files
vue-cli/packages/@vue/cli/lib/Generator.js
2021-06-07 16:49:44 +08:00

381 lines
10 KiB
JavaScript

const ejs = require('ejs')
const debug = require('debug')
const GeneratorAPI = require('./GeneratorAPI')
const PackageManager = require('./util/ProjectPackageManager')
const sortObject = require('./util/sortObject')
const writeFileTree = require('./util/writeFileTree')
const inferRootOptions = require('./util/inferRootOptions')
const normalizeFilePaths = require('./util/normalizeFilePaths')
const { runTransformation } = require('vue-codemod')
const {
semver,
isPlugin,
toShortPluginId,
matchesPluginId,
loadModule,
sortPlugins
} = require('@vue/cli-shared-utils')
const ConfigTransform = require('./ConfigTransform')
const logger = require('@vue/cli-shared-utils/lib/logger')
const logTypes = {
log: logger.log,
info: logger.info,
done: logger.done,
warn: logger.warn,
error: logger.error
}
const defaultConfigTransforms = {
babel: new ConfigTransform({
file: {
js: ['babel.config.js']
}
}),
postcss: new ConfigTransform({
file: {
js: ['postcss.config.js'],
json: ['.postcssrc.json', '.postcssrc'],
yaml: ['.postcssrc.yaml', '.postcssrc.yml']
}
}),
eslintConfig: new ConfigTransform({
file: {
js: ['.eslintrc.js'],
json: ['.eslintrc', '.eslintrc.json'],
yaml: ['.eslintrc.yaml', '.eslintrc.yml']
}
}),
jest: new ConfigTransform({
file: {
js: ['jest.config.js']
}
}),
browserslist: new ConfigTransform({
file: {
lines: ['.browserslistrc']
}
}),
'lint-staged': new ConfigTransform({
file: {
js: ['lint-staged.config.js'],
json: ['.lintstagedrc', '.lintstagedrc.json'],
yaml: ['.lintstagedrc.yaml', '.lintstagedrc.yml']
}
})
}
const reservedConfigTransforms = {
vue: new ConfigTransform({
file: {
js: ['vue.config.js']
}
})
}
const ensureEOL = str => {
if (str.charAt(str.length - 1) !== '\n') {
return str + '\n'
}
return str
}
/**
* Collect created/modified files into set
* @param {Record<string,string|Buffer>} files
* @param {Set<string>} set
*/
const watchFiles = (files, set) => {
return new Proxy(files, {
set (target, key, value, receiver) {
set.add(key)
return Reflect.set(target, key, value, receiver)
},
deleteProperty (target, key) {
set.delete(key)
return Reflect.deleteProperty(target, key)
}
})
}
module.exports = class Generator {
constructor (context, {
pkg = {},
plugins = [],
afterInvokeCbs = [],
afterAnyInvokeCbs = [],
files = {},
invoking = false
} = {}) {
this.context = context
this.plugins = sortPlugins(plugins)
this.originalPkg = pkg
this.pkg = Object.assign({}, pkg)
this.pm = new PackageManager({ context })
this.imports = {}
this.rootOptions = {}
this.afterInvokeCbs = afterInvokeCbs
this.afterAnyInvokeCbs = afterAnyInvokeCbs
this.configTransforms = {}
this.defaultConfigTransforms = defaultConfigTransforms
this.reservedConfigTransforms = reservedConfigTransforms
this.invoking = invoking
// for conflict resolution
this.depSources = {}
// virtual file tree
this.files = Object.keys(files).length
// when execute `vue add/invoke`, only created/modified files are written to disk
? watchFiles(files, this.filesModifyRecord = new Set())
// all files need to be written to disk
: files
this.fileMiddlewares = []
this.postProcessFilesCbs = []
// exit messages
this.exitLogs = []
// load all the other plugins
this.allPlugins = this.resolveAllPlugins()
const cliService = plugins.find(p => p.id === '@vue/cli-service')
const rootOptions = cliService
? cliService.options
: inferRootOptions(pkg)
this.rootOptions = rootOptions
}
async initPlugins () {
const { rootOptions, invoking } = this
const pluginIds = this.plugins.map(p => p.id)
// avoid modifying the passed afterInvokes, because we want to ignore them from other plugins
const passedAfterInvokeCbs = this.afterInvokeCbs
this.afterInvokeCbs = []
// apply hooks from all plugins to collect 'afterAnyHooks'
for (const plugin of this.allPlugins) {
const { id, apply } = plugin
const api = new GeneratorAPI(id, this, {}, rootOptions)
if (apply.hooks) {
await apply.hooks(api, {}, rootOptions, pluginIds)
}
}
// We are doing save/load to make the hook order deterministic
// save "any" hooks
const afterAnyInvokeCbsFromPlugins = this.afterAnyInvokeCbs
// reset hooks
this.afterInvokeCbs = passedAfterInvokeCbs
this.afterAnyInvokeCbs = []
this.postProcessFilesCbs = []
// apply generators from plugins
for (const plugin of this.plugins) {
const { id, apply, options } = plugin
const api = new GeneratorAPI(id, this, options, rootOptions)
await apply(api, options, rootOptions, invoking)
if (apply.hooks) {
// while we execute the entire `hooks` function,
// only the `afterInvoke` hook is respected
// because `afterAnyHooks` is already determined by the `allPlugins` loop above
await apply.hooks(api, options, rootOptions, pluginIds)
}
}
// restore "any" hooks
this.afterAnyInvokeCbs = afterAnyInvokeCbsFromPlugins
}
async generate ({
extractConfigFiles = false,
checkExisting = false
} = {}) {
await this.initPlugins()
// save the file system before applying plugin for comparison
const initialFiles = Object.assign({}, this.files)
// extract configs from package.json into dedicated files.
this.extractConfigFiles(extractConfigFiles, checkExisting)
// wait for file resolve
await this.resolveFiles()
// set package.json
this.sortPkg()
this.files['package.json'] = JSON.stringify(this.pkg, null, 2) + '\n'
// write/update file tree to disk
await writeFileTree(this.context, this.files, initialFiles, this.filesModifyRecord)
}
extractConfigFiles (extractAll, checkExisting) {
const configTransforms = Object.assign({},
defaultConfigTransforms,
this.configTransforms,
reservedConfigTransforms
)
const extract = key => {
if (
configTransforms[key] &&
this.pkg[key] &&
// do not extract if the field exists in original package.json
!this.originalPkg[key]
) {
const value = this.pkg[key]
const configTransform = configTransforms[key]
const res = configTransform.transform(
value,
checkExisting,
this.files,
this.context
)
const { content, filename } = res
this.files[filename] = ensureEOL(content)
delete this.pkg[key]
}
}
if (extractAll) {
for (const key in this.pkg) {
extract(key)
}
} else {
if (!process.env.VUE_CLI_TEST) {
// by default, always extract vue.config.js
extract('vue')
}
// always extract babel.config.js as this is the only way to apply
// project-wide configuration even to dependencies.
// TODO: this can be removed when Babel supports root: true in package.json
extract('babel')
}
}
sortPkg () {
// ensure package.json keys has readable order
this.pkg.dependencies = sortObject(this.pkg.dependencies)
this.pkg.devDependencies = sortObject(this.pkg.devDependencies)
this.pkg.scripts = sortObject(this.pkg.scripts, [
'serve',
'build',
'test:unit',
'test:e2e',
'lint',
'deploy'
])
this.pkg = sortObject(this.pkg, [
'name',
'version',
'private',
'description',
'author',
'scripts',
'main',
'module',
'browser',
'jsDelivr',
'unpkg',
'files',
'dependencies',
'devDependencies',
'peerDependencies',
'vue',
'babel',
'eslintConfig',
'prettier',
'postcss',
'browserslist',
'jest'
])
debug('vue:cli-pkg')(this.pkg)
}
resolveAllPlugins () {
const allPlugins = []
Object.keys(this.pkg.dependencies || {})
.concat(Object.keys(this.pkg.devDependencies || {}))
.forEach(id => {
if (!isPlugin(id)) return
const pluginGenerator = loadModule(`${id}/generator`, this.context)
if (!pluginGenerator) return
allPlugins.push({ id, apply: pluginGenerator })
})
return sortPlugins(allPlugins)
}
async resolveFiles () {
const files = this.files
for (const middleware of this.fileMiddlewares) {
await middleware(files, ejs.render)
}
// normalize file paths on windows
// all paths are converted to use / instead of \
normalizeFilePaths(files)
// handle imports and root option injections
Object.keys(files).forEach(file => {
let imports = this.imports[file]
imports = imports instanceof Set ? Array.from(imports) : imports
if (imports && imports.length > 0) {
files[file] = runTransformation(
{ path: file, source: files[file] },
require('./util/codemods/injectImports'),
{ imports }
)
}
let injections = this.rootOptions[file]
injections = injections instanceof Set ? Array.from(injections) : injections
if (injections && injections.length > 0) {
files[file] = runTransformation(
{ path: file, source: files[file] },
require('./util/codemods/injectOptions'),
{ injections }
)
}
})
for (const postProcess of this.postProcessFilesCbs) {
await postProcess(files)
}
debug('vue:cli-files')(this.files)
}
hasPlugin (id, versionRange) {
const pluginExists = [
...this.plugins.map(p => p.id),
...this.allPlugins.map(p => p.id)
].some(pid => matchesPluginId(id, pid))
if (!pluginExists) {
return false
}
if (!versionRange) {
return pluginExists
}
return semver.satisfies(
this.pm.getInstalledVersion(id),
versionRange
)
}
printExitLogs () {
if (this.exitLogs.length) {
this.exitLogs.forEach(({ id, msg, type }) => {
const shortId = toShortPluginId(id)
const logFn = logTypes[type]
if (!logFn) {
logger.error(`Invalid api.exitLog type '${type}'.`, shortId)
} else {
logFn(msg, msg && shortId)
}
})
logger.log()
}
}
}