UNPKG

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