@weig_3078/cli-demo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +1 -0
- package/.eslintrc +73 -0
- package/README.md +1 -0
- package/bin/mvc.js +14 -0
- package/dist/mvc.js +135771 -0
- package/ejs-demo.js +29 -0
- package/lib/ConfigTransform.js +40 -0
- package/lib/Creator.js +29 -0
- package/lib/Generator.js +307 -0
- package/lib/PromptModuleAPI.js +13 -0
- package/lib/create.js +328 -0
- package/lib/generator/babel/index.js +15 -0
- package/lib/generator/linter/index.js +55 -0
- package/lib/generator/linter/template/.eslintrc.js +30 -0
- package/lib/generator/router/index.js +17 -0
- package/lib/generator/router/template/src/App.vue +33 -0
- package/lib/generator/router/template/src/router/index.js +30 -0
- package/lib/generator/router/template/src/views/About.vue +5 -0
- package/lib/generator/router/template/src/views/Home.vue +18 -0
- package/lib/generator/vue/index.js +20 -0
- package/lib/generator/vue/template/public/favicon.ico +0 -0
- package/lib/generator/vue/template/public/index.html +19 -0
- package/lib/generator/vue/template/src/App.vue +29 -0
- package/lib/generator/vue/template/src/assets/logo.png +0 -0
- package/lib/generator/vue/template/src/components/HelloWorld.vue +110 -0
- package/lib/generator/vue/template/src/main.js +8 -0
- package/lib/generator/vuex/index.js +13 -0
- package/lib/generator/vuex/template/src/store/index.js +15 -0
- package/lib/generator/webpack/index.js +27 -0
- package/lib/generator/webpack/template/build/base.config.js +68 -0
- package/lib/generator/webpack/template/build/dev.config.js +19 -0
- package/lib/generator/webpack/template/build/pro.config.js +16 -0
- package/lib/promptModules/babel.js +10 -0
- package/lib/promptModules/linter.js +48 -0
- package/lib/promptModules/router.js +19 -0
- package/lib/promptModules/vuex.js +8 -0
- package/lib/utils/clearConsole.js +13 -0
- package/lib/utils/codemods/injectImports.js +31 -0
- package/lib/utils/codemods/injectOptions.js +27 -0
- package/lib/utils/configTransforms.js +49 -0
- package/lib/utils/executeCommand.js +33 -0
- package/lib/utils/normalizeFilePaths.js +11 -0
- package/lib/utils/sortObject.js +32 -0
- package/lib/utils/stringifyJS.js +12 -0
- package/lib/utils/writeFileTree.js +12 -0
- package/package.json +53 -0
- package/rollup.config.cjs +43 -0
- package/scripts/verify-commit.js +24 -0
package/ejs-demo.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
let ejs
|
|
2
|
+
try {
|
|
3
|
+
ejs = require('ejs')
|
|
4
|
+
} catch (e) {
|
|
5
|
+
console.error('缺少依赖 ejs,请先安装:npm i ejs')
|
|
6
|
+
process.exit(1)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const template = `
|
|
10
|
+
<h1><%= title %></h1>
|
|
11
|
+
<%_ if (isProd) { _%>
|
|
12
|
+
<script>console.log('prod')</script>
|
|
13
|
+
<%_ } else { _%>
|
|
14
|
+
<script>console.log('dev')</script>
|
|
15
|
+
<%_ } _%>
|
|
16
|
+
<ul>
|
|
17
|
+
<%_ items.forEach(i => { _%>
|
|
18
|
+
<li><%= i %></li>
|
|
19
|
+
<%_ }) _%>
|
|
20
|
+
</ul>
|
|
21
|
+
`.trim()
|
|
22
|
+
|
|
23
|
+
const html = ejs.render(template, {
|
|
24
|
+
title: 'EJS Demo',
|
|
25
|
+
isProd: process.env.NODE_ENV === 'dev',
|
|
26
|
+
items: ['a', 'b', 'c'],
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
console.log(html)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const transforms = require('./utils/configTransforms')
|
|
2
|
+
|
|
3
|
+
class ConfigTransform {
|
|
4
|
+
constructor(options) {
|
|
5
|
+
this.fileDescriptor = options.file
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
transform(value, context) {
|
|
9
|
+
let file
|
|
10
|
+
|
|
11
|
+
if (!file) {
|
|
12
|
+
file = this.getDefaultFile()
|
|
13
|
+
}
|
|
14
|
+
const { type, filename } = file
|
|
15
|
+
|
|
16
|
+
const transform = transforms[type]
|
|
17
|
+
|
|
18
|
+
let source
|
|
19
|
+
|
|
20
|
+
const content = transform.write({
|
|
21
|
+
source,
|
|
22
|
+
filename,
|
|
23
|
+
context,
|
|
24
|
+
value,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
filename,
|
|
29
|
+
content,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getDefaultFile() {
|
|
34
|
+
const [type] = Object.keys(this.fileDescriptor)
|
|
35
|
+
const [filename] = this.fileDescriptor[type]
|
|
36
|
+
return { type, filename }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = ConfigTransform
|
package/lib/Creator.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
class Creator {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.featurePrompt = {
|
|
4
|
+
name: 'features',
|
|
5
|
+
message: 'Check the features needed for your project:',
|
|
6
|
+
pageSize: 10,
|
|
7
|
+
type: 'checkbox',
|
|
8
|
+
choices: [],
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
this.injectedPrompts = []
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getFinalPrompts() {
|
|
15
|
+
this.injectedPrompts.forEach(prompt => {
|
|
16
|
+
const originalWhen = prompt.when || (() => true)
|
|
17
|
+
prompt.when = answers => originalWhen(answers)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const prompts = [
|
|
21
|
+
this.featurePrompt,
|
|
22
|
+
...this.injectedPrompts,
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
return prompts
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = Creator
|
package/lib/Generator.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
const fs = require('fs-extra')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const ejs = require('ejs')
|
|
4
|
+
const sortObject = require('./utils/sortObject')
|
|
5
|
+
const normalizeFilePaths = require('./utils/normalizeFilePaths')
|
|
6
|
+
const { runTransformation } = require('vue-codemod')
|
|
7
|
+
const writeFileTree = require('./utils/writeFileTree')
|
|
8
|
+
const { isBinaryFileSync } = require('isbinaryfile')
|
|
9
|
+
const isObject = (val) => val && typeof val === 'object'
|
|
10
|
+
const ConfigTransform = require('./ConfigTransform')
|
|
11
|
+
|
|
12
|
+
const defaultConfigTransforms = {
|
|
13
|
+
babel: new ConfigTransform({
|
|
14
|
+
file: {
|
|
15
|
+
js: ['babel.config.js'],
|
|
16
|
+
},
|
|
17
|
+
}),
|
|
18
|
+
postcss: new ConfigTransform({
|
|
19
|
+
file: {
|
|
20
|
+
js: ['postcss.config.js'],
|
|
21
|
+
json: ['.postcssrc.json', '.postcssrc'],
|
|
22
|
+
yaml: ['.postcssrc.yaml', '.postcssrc.yml'],
|
|
23
|
+
},
|
|
24
|
+
}),
|
|
25
|
+
eslintConfig: new ConfigTransform({
|
|
26
|
+
file: {
|
|
27
|
+
js: ['.eslintrc.js'],
|
|
28
|
+
json: ['.eslintrc', '.eslintrc.json'],
|
|
29
|
+
yaml: ['.eslintrc.yaml', '.eslintrc.yml'],
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
jest: new ConfigTransform({
|
|
33
|
+
file: {
|
|
34
|
+
js: ['jest.config.js'],
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
browserslist: new ConfigTransform({
|
|
38
|
+
file: {
|
|
39
|
+
lines: ['.browserslistrc'],
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const reservedConfigTransforms = {
|
|
45
|
+
vue: new ConfigTransform({
|
|
46
|
+
file: {
|
|
47
|
+
js: ['vue.config.js'],
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const ensureEOL = str => {
|
|
53
|
+
if (str.charAt(str.length - 1) !== '\n') {
|
|
54
|
+
return str + '\n'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return str
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
class Generator {
|
|
61
|
+
constructor(pkg, context) {
|
|
62
|
+
this.pkg = pkg
|
|
63
|
+
this.rootOptions = {}
|
|
64
|
+
this.imports = {}
|
|
65
|
+
this.files = {}
|
|
66
|
+
this.entryFile = `src/main.js`
|
|
67
|
+
this.fileMiddlewares = []
|
|
68
|
+
this.context = context
|
|
69
|
+
this.configTransforms = {}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
extendPackage(fields) {
|
|
73
|
+
const pkg = this.pkg
|
|
74
|
+
for (const key in fields) {
|
|
75
|
+
const value = fields[key]
|
|
76
|
+
const existing = pkg[key]
|
|
77
|
+
if (isObject(value) && (key === 'dependencies' || key === 'devDependencies' || key === 'scripts')) {
|
|
78
|
+
pkg[key] = Object.assign(existing || {}, value)
|
|
79
|
+
} else {
|
|
80
|
+
pkg[key] = value
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async generate(options = {}) {
|
|
86
|
+
// 从 package.json 中提取文件
|
|
87
|
+
this.extractConfigFiles()
|
|
88
|
+
// 解析文件内容
|
|
89
|
+
await this.resolveFiles()
|
|
90
|
+
// 将 package.json 中的字段排序
|
|
91
|
+
this.sortPkg()
|
|
92
|
+
this.files['package.json'] = JSON.stringify(this.pkg, null, 2) + '\n'
|
|
93
|
+
// 将所有文件写入到用户要创建的目录
|
|
94
|
+
await writeFileTree(this.context, this.files, options)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 按照下面的顺序对 package.json 中的 key 进行排序
|
|
98
|
+
sortPkg() {
|
|
99
|
+
// ensure package.json keys has readable order
|
|
100
|
+
this.pkg.dependencies = sortObject(this.pkg.dependencies)
|
|
101
|
+
this.pkg.devDependencies = sortObject(this.pkg.devDependencies)
|
|
102
|
+
this.pkg.scripts = sortObject(this.pkg.scripts, [
|
|
103
|
+
'dev',
|
|
104
|
+
'build',
|
|
105
|
+
'test:unit',
|
|
106
|
+
'test:e2e',
|
|
107
|
+
'lint',
|
|
108
|
+
'deploy',
|
|
109
|
+
])
|
|
110
|
+
|
|
111
|
+
this.pkg = sortObject(this.pkg, [
|
|
112
|
+
'name',
|
|
113
|
+
'version',
|
|
114
|
+
'private',
|
|
115
|
+
'description',
|
|
116
|
+
'author',
|
|
117
|
+
'scripts',
|
|
118
|
+
'husky',
|
|
119
|
+
'lint-staged',
|
|
120
|
+
'main',
|
|
121
|
+
'module',
|
|
122
|
+
'browser',
|
|
123
|
+
'jsDelivr',
|
|
124
|
+
'unpkg',
|
|
125
|
+
'files',
|
|
126
|
+
'dependencies',
|
|
127
|
+
'devDependencies',
|
|
128
|
+
'peerDependencies',
|
|
129
|
+
'vue',
|
|
130
|
+
'babel',
|
|
131
|
+
'eslintConfig',
|
|
132
|
+
'prettier',
|
|
133
|
+
'postcss',
|
|
134
|
+
'browserslist',
|
|
135
|
+
'jest',
|
|
136
|
+
])
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 使用 ejs 解析 lib\generator\xx\template 中的文件
|
|
140
|
+
async resolveFiles() {
|
|
141
|
+
const files = this.files
|
|
142
|
+
for (const middleware of this.fileMiddlewares) {
|
|
143
|
+
await middleware(files, ejs.render)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// normalize file paths on windows
|
|
147
|
+
// all paths are converted to use / instead of \
|
|
148
|
+
// 将反斜杠 \ 转换为正斜杠 /
|
|
149
|
+
normalizeFilePaths(files)
|
|
150
|
+
|
|
151
|
+
// 处理 import 语句的导入和 new Vue() 选项的注入
|
|
152
|
+
// vue-codemod 库,对代码进行解析得到 AST,再将 import 语句和根选项注入
|
|
153
|
+
Object.keys(files).forEach(file => {
|
|
154
|
+
let imports = this.imports[file]
|
|
155
|
+
imports = imports instanceof Set ? Array.from(imports) : imports
|
|
156
|
+
if (imports && imports.length > 0) {
|
|
157
|
+
files[file] = runTransformation(
|
|
158
|
+
{ path: file, source: files[file] },
|
|
159
|
+
require('./utils/codemods/injectImports'),
|
|
160
|
+
{ imports },
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let injections = this.rootOptions[file]
|
|
165
|
+
injections = injections instanceof Set ? Array.from(injections) : injections
|
|
166
|
+
if (injections && injections.length > 0) {
|
|
167
|
+
files[file] = runTransformation(
|
|
168
|
+
{ path: file, source: files[file] },
|
|
169
|
+
require('./utils/codemods/injectOptions'),
|
|
170
|
+
{ injections },
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 将 package.json 中的配置提取出来,生成单独的文件
|
|
177
|
+
// 例如将 package.json 中的
|
|
178
|
+
// babel: {
|
|
179
|
+
// presets: ['@babel/preset-env']
|
|
180
|
+
// },
|
|
181
|
+
// 提取出来变成 babel.config.js 文件
|
|
182
|
+
extractConfigFiles() {
|
|
183
|
+
const configTransforms = {
|
|
184
|
+
...defaultConfigTransforms,
|
|
185
|
+
...this.configTransforms,
|
|
186
|
+
...reservedConfigTransforms,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const extract = key => {
|
|
190
|
+
if (configTransforms[key] && this.pkg[key]) {
|
|
191
|
+
const value = this.pkg[key]
|
|
192
|
+
const configTransform = configTransforms[key]
|
|
193
|
+
const res = configTransform.transform(
|
|
194
|
+
value,
|
|
195
|
+
this.files,
|
|
196
|
+
this.context,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
const { content, filename } = res
|
|
200
|
+
// 如果文件不是以 \n 结尾,则补上 \n
|
|
201
|
+
this.files[filename] = ensureEOL(content)
|
|
202
|
+
delete this.pkg[key]
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
extract('vue')
|
|
207
|
+
extract('babel')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
render(source, additionalData = {}, ejsOptions = {}) {
|
|
211
|
+
// 获取调用 generator.render() 函数的文件的父目录路径
|
|
212
|
+
const baseDir = extractCallDir()
|
|
213
|
+
source = path.resolve(baseDir, source)
|
|
214
|
+
this._injectFileMiddleware(async (files) => {
|
|
215
|
+
const data = this._resolveData(additionalData)
|
|
216
|
+
// https://github.com/sindresorhus/globby
|
|
217
|
+
const globby = require('globby')
|
|
218
|
+
// 读取目录中所有的文件
|
|
219
|
+
const _files = await globby(['**/*'], { cwd: source, dot: true })
|
|
220
|
+
for (const rawPath of _files) {
|
|
221
|
+
const sourcePath = path.resolve(source, rawPath)
|
|
222
|
+
// 解析文件内容
|
|
223
|
+
const content = this.renderFile(sourcePath, data, ejsOptions)
|
|
224
|
+
// only set file if it's not all whitespace, or is a Buffer (binary files)
|
|
225
|
+
if (Buffer.isBuffer(content) || /[^\s]/.test(content)) {
|
|
226
|
+
files[rawPath] = content
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
_injectFileMiddleware(middleware) {
|
|
233
|
+
this.fileMiddlewares.push(middleware)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 合并选项
|
|
237
|
+
_resolveData(additionalData) {
|
|
238
|
+
return {
|
|
239
|
+
options: this.options,
|
|
240
|
+
rootOptions: this.rootOptions,
|
|
241
|
+
...additionalData,
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
renderFile(name, data, ejsOptions) {
|
|
246
|
+
// 如果是二进制文件,直接将读取结果返回
|
|
247
|
+
if (isBinaryFileSync(name)) {
|
|
248
|
+
return fs.readFileSync(name) // return buffer
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 返回文件内容
|
|
252
|
+
const template = fs.readFileSync(name, 'utf-8')
|
|
253
|
+
return ejs.render(template, data, ejsOptions)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Add import statements to a file.
|
|
258
|
+
*/
|
|
259
|
+
injectImports(file, imports) {
|
|
260
|
+
const _imports = (
|
|
261
|
+
this.imports[file]
|
|
262
|
+
|| (this.imports[file] = new Set())
|
|
263
|
+
);
|
|
264
|
+
(Array.isArray(imports) ? imports : [imports]).forEach(imp => {
|
|
265
|
+
_imports.add(imp)
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Add options to the root Vue instance (detected by `new Vue`).
|
|
271
|
+
*/
|
|
272
|
+
injectRootOptions(file, options) {
|
|
273
|
+
const _options = (
|
|
274
|
+
this.rootOptions[file]
|
|
275
|
+
|| (this.rootOptions[file] = new Set())
|
|
276
|
+
);
|
|
277
|
+
(Array.isArray(options) ? options : [options]).forEach(opt => {
|
|
278
|
+
_options.add(opt)
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// http://blog.shaochuancs.com/about-error-capturestacktrace/
|
|
284
|
+
// 获取调用栈信息
|
|
285
|
+
function extractCallDir() {
|
|
286
|
+
const obj = {}
|
|
287
|
+
Error.captureStackTrace(obj)
|
|
288
|
+
// 在 lib\generator\xx 等各个模块中 调用 generator.render()
|
|
289
|
+
// 将会排在调用栈中的第四个,也就是 obj.stack.split('\n')[3]
|
|
290
|
+
const callSite = obj.stack.split('\n')[3]
|
|
291
|
+
|
|
292
|
+
// the regexp for the stack when called inside a named function
|
|
293
|
+
const namedStackRegExp = /\s\((.*):\d+:\d+\)$/
|
|
294
|
+
// the regexp for the stack when called inside an anonymous
|
|
295
|
+
const anonymousStackRegExp = /at (.*):\d+:\d+$/
|
|
296
|
+
|
|
297
|
+
let matchResult = callSite.match(namedStackRegExp)
|
|
298
|
+
if (!matchResult) {
|
|
299
|
+
matchResult = callSite.match(anonymousStackRegExp)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const fileName = matchResult[1]
|
|
303
|
+
// 获取对应文件的目录
|
|
304
|
+
return path.dirname(fileName)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
module.exports = Generator
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module.exports = class PromptModuleAPI {
|
|
2
|
+
constructor(creator) {
|
|
3
|
+
this.creator = creator
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
injectFeature(feature) {
|
|
7
|
+
this.creator.featurePrompt.choices.push(feature)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
injectPrompt(prompt) {
|
|
11
|
+
this.creator.injectedPrompts.push(prompt)
|
|
12
|
+
}
|
|
13
|
+
}
|