Generator.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. const ejs = require('ejs')
  2. const debug = require('debug')
  3. const GeneratorAPI = require('./GeneratorAPI')
  4. const PackageManager = require('./util/ProjectPackageManager')
  5. const sortObject = require('./util/sortObject')
  6. const writeFileTree = require('./util/writeFileTree')
  7. const inferRootOptions = require('./util/inferRootOptions')
  8. const normalizeFilePaths = require('./util/normalizeFilePaths')
  9. const runCodemod = require('./util/runCodemod')
  10. const {
  11. semver,
  12. isPlugin,
  13. toShortPluginId,
  14. matchesPluginId,
  15. loadModule
  16. } = require('@vue/cli-shared-utils')
  17. const ConfigTransform = require('./ConfigTransform')
  18. const logger = require('@vue/cli-shared-utils/lib/logger')
  19. const logTypes = {
  20. log: logger.log,
  21. info: logger.info,
  22. done: logger.done,
  23. warn: logger.warn,
  24. error: logger.error
  25. }
  26. const defaultConfigTransforms = {
  27. babel: new ConfigTransform({
  28. file: {
  29. js: ['babel.config.js']
  30. }
  31. }),
  32. postcss: new ConfigTransform({
  33. file: {
  34. js: ['postcss.config.js'],
  35. json: ['.postcssrc.json', '.postcssrc'],
  36. yaml: ['.postcssrc.yaml', '.postcssrc.yml']
  37. }
  38. }),
  39. eslintConfig: new ConfigTransform({
  40. file: {
  41. js: ['.eslintrc.js'],
  42. json: ['.eslintrc', '.eslintrc.json'],
  43. yaml: ['.eslintrc.yaml', '.eslintrc.yml']
  44. }
  45. }),
  46. jest: new ConfigTransform({
  47. file: {
  48. js: ['jest.config.js']
  49. }
  50. }),
  51. browserslist: new ConfigTransform({
  52. file: {
  53. lines: ['.browserslistrc']
  54. }
  55. })
  56. }
  57. const reservedConfigTransforms = {
  58. vue: new ConfigTransform({
  59. file: {
  60. js: ['vue.config.js']
  61. }
  62. })
  63. }
  64. const ensureEOL = str => {
  65. if (str.charAt(str.length - 1) !== '\n') {
  66. return str + '\n'
  67. }
  68. return str
  69. }
  70. module.exports = class Generator {
  71. constructor (context, {
  72. pkg = {},
  73. plugins = [],
  74. afterInvokeCbs = [],
  75. afterAnyInvokeCbs = [],
  76. files = {},
  77. invoking = false
  78. } = {}) {
  79. this.context = context
  80. this.plugins = plugins
  81. this.originalPkg = pkg
  82. this.pkg = Object.assign({}, pkg)
  83. this.pm = new PackageManager({ context })
  84. this.imports = {}
  85. this.rootOptions = {}
  86. // we don't load the passed afterInvokes yet because we want to ignore them from other plugins
  87. this.passedAfterInvokeCbs = afterInvokeCbs
  88. this.afterInvokeCbs = []
  89. this.afterAnyInvokeCbs = afterAnyInvokeCbs
  90. this.configTransforms = {}
  91. this.defaultConfigTransforms = defaultConfigTransforms
  92. this.reservedConfigTransforms = reservedConfigTransforms
  93. this.invoking = invoking
  94. // for conflict resolution
  95. this.depSources = {}
  96. // virtual file tree
  97. this.files = files
  98. this.fileMiddlewares = []
  99. this.postProcessFilesCbs = []
  100. // exit messages
  101. this.exitLogs = []
  102. // load all the other plugins
  103. this.allPluginIds = Object.keys(this.pkg.dependencies || {})
  104. .concat(Object.keys(this.pkg.devDependencies || {}))
  105. .filter(isPlugin)
  106. const cliService = plugins.find(p => p.id === '@vue/cli-service')
  107. const rootOptions = cliService
  108. ? cliService.options
  109. : inferRootOptions(pkg)
  110. this.rootOptions = rootOptions
  111. }
  112. async initPlugins () {
  113. const { rootOptions, invoking } = this
  114. const pluginIds = this.plugins.map(p => p.id)
  115. // apply hooks from all plugins
  116. for (const id of this.allPluginIds) {
  117. const api = new GeneratorAPI(id, this, {}, rootOptions)
  118. const pluginGenerator = loadModule(`${id}/generator`, this.context)
  119. if (pluginGenerator && pluginGenerator.hooks) {
  120. await pluginGenerator.hooks(api, {}, rootOptions, pluginIds)
  121. }
  122. }
  123. // We are doing save/load to make the hook order deterministic
  124. // save "any" hooks
  125. const afterAnyInvokeCbsFromPlugins = this.afterAnyInvokeCbs
  126. // reset hooks
  127. this.afterInvokeCbs = this.passedAfterInvokeCbs
  128. this.afterAnyInvokeCbs = []
  129. this.postProcessFilesCbs = []
  130. // apply generators from plugins
  131. for (const plugin of this.plugins) {
  132. const { id, apply, options } = plugin
  133. const api = new GeneratorAPI(id, this, options, rootOptions)
  134. await apply(api, options, rootOptions, invoking)
  135. if (apply.hooks) {
  136. // while we execute the entire `hooks` function,
  137. // only the `afterInvoke` hook is respected
  138. // because `afterAnyHooks` is already determined by the `allPluginIds` loop above
  139. await apply.hooks(api, options, rootOptions, pluginIds)
  140. }
  141. // restore "any" hooks
  142. this.afterAnyInvokeCbs = afterAnyInvokeCbsFromPlugins
  143. }
  144. }
  145. async generate ({
  146. extractConfigFiles = false,
  147. checkExisting = false
  148. } = {}) {
  149. await this.initPlugins()
  150. // save the file system before applying plugin for comparison
  151. const initialFiles = Object.assign({}, this.files)
  152. // extract configs from package.json into dedicated files.
  153. this.extractConfigFiles(extractConfigFiles, checkExisting)
  154. // wait for file resolve
  155. await this.resolveFiles()
  156. // set package.json
  157. this.sortPkg()
  158. this.files['package.json'] = JSON.stringify(this.pkg, null, 2) + '\n'
  159. // write/update file tree to disk
  160. await writeFileTree(this.context, this.files, initialFiles)
  161. }
  162. extractConfigFiles (extractAll, checkExisting) {
  163. const configTransforms = Object.assign({},
  164. defaultConfigTransforms,
  165. this.configTransforms,
  166. reservedConfigTransforms
  167. )
  168. const extract = key => {
  169. if (
  170. configTransforms[key] &&
  171. this.pkg[key] &&
  172. // do not extract if the field exists in original package.json
  173. !this.originalPkg[key]
  174. ) {
  175. const value = this.pkg[key]
  176. const configTransform = configTransforms[key]
  177. const res = configTransform.transform(
  178. value,
  179. checkExisting,
  180. this.files,
  181. this.context
  182. )
  183. const { content, filename } = res
  184. this.files[filename] = ensureEOL(content)
  185. delete this.pkg[key]
  186. }
  187. }
  188. if (extractAll) {
  189. for (const key in this.pkg) {
  190. extract(key)
  191. }
  192. } else {
  193. if (!process.env.VUE_CLI_TEST) {
  194. // by default, always extract vue.config.js
  195. extract('vue')
  196. }
  197. // always extract babel.config.js as this is the only way to apply
  198. // project-wide configuration even to dependencies.
  199. // TODO: this can be removed when Babel supports root: true in package.json
  200. extract('babel')
  201. }
  202. }
  203. sortPkg () {
  204. // ensure package.json keys has readable order
  205. this.pkg.dependencies = sortObject(this.pkg.dependencies)
  206. this.pkg.devDependencies = sortObject(this.pkg.devDependencies)
  207. this.pkg.scripts = sortObject(this.pkg.scripts, [
  208. 'serve',
  209. 'build',
  210. 'test:unit',
  211. 'test:e2e',
  212. 'lint',
  213. 'deploy'
  214. ])
  215. this.pkg = sortObject(this.pkg, [
  216. 'name',
  217. 'version',
  218. 'private',
  219. 'description',
  220. 'author',
  221. 'scripts',
  222. 'main',
  223. 'module',
  224. 'browser',
  225. 'jsDelivr',
  226. 'unpkg',
  227. 'files',
  228. 'dependencies',
  229. 'devDependencies',
  230. 'peerDependencies',
  231. 'vue',
  232. 'babel',
  233. 'eslintConfig',
  234. 'prettier',
  235. 'postcss',
  236. 'browserslist',
  237. 'jest'
  238. ])
  239. debug('vue:cli-pkg')(this.pkg)
  240. }
  241. async resolveFiles () {
  242. const files = this.files
  243. for (const middleware of this.fileMiddlewares) {
  244. await middleware(files, ejs.render)
  245. }
  246. // normalize file paths on windows
  247. // all paths are converted to use / instead of \
  248. normalizeFilePaths(files)
  249. // handle imports and root option injections
  250. Object.keys(files).forEach(file => {
  251. let imports = this.imports[file]
  252. imports = imports instanceof Set ? Array.from(imports) : imports
  253. if (imports && imports.length > 0) {
  254. files[file] = runCodemod(
  255. require('./util/codemods/injectImports'),
  256. { path: file, source: files[file] },
  257. { imports }
  258. )
  259. }
  260. let injections = this.rootOptions[file]
  261. injections = injections instanceof Set ? Array.from(injections) : injections
  262. if (injections && injections.length > 0) {
  263. files[file] = runCodemod(
  264. require('./util/codemods/injectOptions'),
  265. { path: file, source: files[file] },
  266. { injections }
  267. )
  268. }
  269. })
  270. for (const postProcess of this.postProcessFilesCbs) {
  271. await postProcess(files)
  272. }
  273. debug('vue:cli-files')(this.files)
  274. }
  275. hasPlugin (_id, _version) {
  276. return [
  277. ...this.plugins.map(p => p.id),
  278. ...this.allPluginIds
  279. ].some(id => {
  280. if (!matchesPluginId(_id, id)) {
  281. return false
  282. }
  283. if (!_version) {
  284. return true
  285. }
  286. const version = this.pm.getInstalledVersion(id)
  287. return semver.satisfies(version, _version)
  288. })
  289. }
  290. printExitLogs () {
  291. if (this.exitLogs.length) {
  292. this.exitLogs.forEach(({ id, msg, type }) => {
  293. const shortId = toShortPluginId(id)
  294. const logFn = logTypes[type]
  295. if (!logFn) {
  296. logger.error(`Invalid api.exitLog type '${type}'.`, shortId)
  297. } else {
  298. logFn(msg, msg && shortId)
  299. }
  300. })
  301. logger.log()
  302. }
  303. }
  304. }