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 { runTransformation } = require('vue-codemod')
|
10 | const {
|
11 | semver,
|
12 |
|
13 | isPlugin,
|
14 | toShortPluginId,
|
15 | matchesPluginId,
|
16 |
|
17 | loadModule
|
18 | } = require('@vue/cli-shared-utils')
|
19 | const ConfigTransform = require('./ConfigTransform')
|
20 |
|
21 | const logger = require('@vue/cli-shared-utils/lib/logger')
|
22 | const logTypes = {
|
23 | log: logger.log,
|
24 | info: logger.info,
|
25 | done: logger.done,
|
26 | warn: logger.warn,
|
27 | error: logger.error
|
28 | }
|
29 |
|
30 | const 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 |
|
62 | const reservedConfigTransforms = {
|
63 | vue: new ConfigTransform({
|
64 | file: {
|
65 | js: ['vue.config.js']
|
66 | }
|
67 | })
|
68 | }
|
69 |
|
70 | const ensureEOL = str => {
|
71 | if (str.charAt(str.length - 1) !== '\n') {
|
72 | return str + '\n'
|
73 | }
|
74 | return str
|
75 | }
|
76 |
|
77 | module.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 |
|
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 |
|
102 | this.depSources = {}
|
103 |
|
104 | this.files = files
|
105 | this.fileMiddlewares = []
|
106 | this.postProcessFilesCbs = []
|
107 |
|
108 | this.exitLogs = []
|
109 |
|
110 |
|
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 |
|
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 |
|
138 |
|
139 | const afterAnyInvokeCbsFromPlugins = this.afterAnyInvokeCbs
|
140 |
|
141 |
|
142 | this.afterInvokeCbs = this.passedAfterInvokeCbs
|
143 | this.afterAnyInvokeCbs = []
|
144 | this.postProcessFilesCbs = []
|
145 |
|
146 |
|
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 |
|
154 |
|
155 |
|
156 | await apply.hooks(api, options, rootOptions, pluginIds)
|
157 | }
|
158 |
|
159 |
|
160 | this.afterAnyInvokeCbs = afterAnyInvokeCbsFromPlugins
|
161 | }
|
162 | }
|
163 |
|
164 | async generate ({
|
165 | extractConfigFiles = false,
|
166 | checkExisting = false
|
167 | } = {}) {
|
168 | await this.initPlugins()
|
169 |
|
170 |
|
171 | const initialFiles = Object.assign({}, this.files)
|
172 |
|
173 | this.extractConfigFiles(extractConfigFiles, checkExisting)
|
174 |
|
175 | await this.resolveFiles()
|
176 |
|
177 | this.sortPkg()
|
178 | this.files['package.json'] = JSON.stringify(this.pkg, null, 2) + '\n'
|
179 |
|
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 |
|
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 |
|
216 | extract('vue')
|
217 | }
|
218 |
|
219 |
|
220 |
|
221 | extract('babel')
|
222 | }
|
223 | }
|
224 |
|
225 | sortPkg () {
|
226 |
|
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 |
|
272 |
|
273 | normalizeFilePaths(files)
|
274 |
|
275 |
|
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 | }
|