123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336 |
- 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 runCodemod = require('./util/runCodemod')
- const {
- semver,
- isPlugin,
- toShortPluginId,
- matchesPluginId,
- loadModule
- } = 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']
- }
- })
- }
- 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
- }
- module.exports = class Generator {
- constructor (context, {
- pkg = {},
- plugins = [],
- afterInvokeCbs = [],
- afterAnyInvokeCbs = [],
- files = {},
- invoking = false
- } = {}) {
- this.context = context
- this.plugins = plugins
- this.originalPkg = pkg
- this.pkg = Object.assign({}, pkg)
- this.pm = new PackageManager({ context })
- this.imports = {}
- this.rootOptions = {}
- // we don't load the passed afterInvokes yet because we want to ignore them from other plugins
- this.passedAfterInvokeCbs = afterInvokeCbs
- this.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 = files
- this.fileMiddlewares = []
- this.postProcessFilesCbs = []
- // exit messages
- this.exitLogs = []
- // load all the other plugins
- this.allPluginIds = Object.keys(this.pkg.dependencies || {})
- .concat(Object.keys(this.pkg.devDependencies || {}))
- .filter(isPlugin)
- 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)
- // apply hooks from all plugins
- for (const id of this.allPluginIds) {
- const api = new GeneratorAPI(id, this, {}, rootOptions)
- const pluginGenerator = loadModule(`${id}/generator`, this.context)
- if (pluginGenerator && pluginGenerator.hooks) {
- await pluginGenerator.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 = this.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 `allPluginIds` 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)
- }
- 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)
- }
- 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] = runCodemod(
- require('./util/codemods/injectImports'),
- { path: file, source: files[file] },
- { imports }
- )
- }
- let injections = this.rootOptions[file]
- injections = injections instanceof Set ? Array.from(injections) : injections
- if (injections && injections.length > 0) {
- files[file] = runCodemod(
- require('./util/codemods/injectOptions'),
- { path: file, source: files[file] },
- { injections }
- )
- }
- })
- for (const postProcess of this.postProcessFilesCbs) {
- await postProcess(files)
- }
- debug('vue:cli-files')(this.files)
- }
- hasPlugin (_id, _version) {
- return [
- ...this.plugins.map(p => p.id),
- ...this.allPluginIds
- ].some(id => {
- if (!matchesPluginId(_id, id)) {
- return false
- }
- if (!_version) {
- return true
- }
- const version = this.pm.getInstalledVersion(id)
- return semver.satisfies(version, _version)
- })
- }
- 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()
- }
- }
- }
|