Skip to content

Commit 8b32f4a

Browse files
committed
feat: GeneratorAPI: addImports & addRootOptions
1 parent 581abd3 commit 8b32f4a

File tree

8 files changed

+153
-40
lines changed

8 files changed

+153
-40
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
11
const generateWithPlugin = require('@vue/cli-test-utils/generateWithPlugin')
22

33
test('inject import statement for service worker', async () => {
4-
const mockMain = (
5-
`import Vue from 'vue'\n` +
6-
`import Bar from './Bar.vue'`
7-
)
84
const { files } = await generateWithPlugin([
95
{
10-
id: 'files',
11-
apply: api => {
12-
api.render(files => {
13-
files['src/main.js'] = mockMain
14-
})
15-
},
6+
id: 'core',
7+
apply: require('@vue/cli-service/generator'),
168
options: {}
179
},
1810
{
@@ -22,5 +14,27 @@ test('inject import statement for service worker', async () => {
2214
}
2315
])
2416

25-
expect(files['src/main.js']).toMatch(`${mockMain}\nimport './registerServiceWorker'`)
17+
expect(files['src/main.js']).toMatch(`import './registerServiceWorker'`)
18+
})
19+
20+
test('inject import statement for service worker (with TS)', async () => {
21+
const { files } = await generateWithPlugin([
22+
{
23+
id: 'core',
24+
apply: require('@vue/cli-service/generator'),
25+
options: {}
26+
},
27+
{
28+
id: 'typescript',
29+
apply: require('@vue/cli-plugin-typescript/generator'),
30+
options: {}
31+
},
32+
{
33+
id: 'pwa',
34+
apply: require('../generator'),
35+
options: {}
36+
}
37+
])
38+
39+
expect(files['src/main.ts']).toMatch(`import './registerServiceWorker'`)
2640
})

packages/@vue/cli-plugin-pwa/generator/index.js

+1-16
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,6 @@ module.exports = api => {
44
'register-service-worker': '^1.0.0'
55
}
66
})
7-
7+
api.injectImports(`src/main.js`, `import './registerServiceWorker'`)
88
api.render('./template')
9-
10-
api.postProcessFiles(files => {
11-
const isTS = 'src/main.ts' in files
12-
const file = isTS
13-
? 'src/main.ts'
14-
: 'src/main.js'
15-
const main = files[file]
16-
if (main) {
17-
// inject import for registerServiceWorker script into main.js
18-
const lines = main.split(/\r?\n/g).reverse()
19-
const lastImportIndex = lines.findIndex(line => line.match(/^import/))
20-
lines[lastImportIndex] += `\nimport './registerServiceWorker'`
21-
files[file] = lines.reverse().join('\n')
22-
}
23-
})
249
}

packages/@vue/cli-service/generator/index.js

+4
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ module.exports = (api, options) => {
3131
})
3232

3333
if (options.router) {
34+
api.injectImports(`src/main.js`, `import router from './router'`)
35+
api.injectRootOptions(`src/main.js`, `router`)
3436
api.extendPackage({
3537
dependencies: {
3638
'vue-router': '^3.0.1'
@@ -39,6 +41,8 @@ module.exports = (api, options) => {
3941
}
4042

4143
if (options.vuex) {
44+
api.injectImports(`import store from './store'`)
45+
api.injectRootOptions(`store`)
4246
api.extendPackage({
4347
dependencies: {
4448
vuex: '^3.0.1'
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,8 @@
11
import Vue from 'vue'
22
import App from './App.vue'
3-
<%_ if (rootOptions.router) { _%>
4-
import router from './router'
5-
<%_ } _%>
6-
<%_ if (rootOptions.vuex) { _%>
7-
import store from './store'
8-
<%_ } _%>
93

104
Vue.config.productionTip = false
115

126
new Vue({
13-
<%_ if (rootOptions.router) { _%>
14-
router,
15-
<%_ } _%>
16-
<%_ if (rootOptions.vuex) { _%>
17-
store,
18-
<%_ } _%>
197
render: h => h(App)
208
}).$mount('#app')

packages/@vue/cli/__tests__/Generator.spec.js

+32
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ fs.ensureDirSync(templateDir)
1212
fs.writeFileSync(path.resolve(templateDir, 'foo.js'), 'foo(<%- options.n %>)')
1313
fs.ensureDirSync(path.resolve(templateDir, 'bar'))
1414
fs.writeFileSync(path.resolve(templateDir, 'bar/bar.js'), 'bar(<%- m %>)')
15+
fs.writeFileSync(path.resolve(templateDir, 'entry.js'), `
16+
import foo from 'foo'
17+
18+
new Vue({
19+
render: h => h(App)
20+
}).$mount('#app')
21+
`.trim())
1522

1623
fs.writeFileSync(path.resolve(templateDir, 'replace.js'), `
1724
---
@@ -431,6 +438,31 @@ test('api: resolve', () => {
431438
] })
432439
})
433440

441+
test('api: addEntryImport & addEntryInjection', async () => {
442+
const generator = new Generator('/', { plugins: [
443+
{
444+
id: 'test',
445+
apply: api => {
446+
api.injectImports('main.js', `import bar from 'bar'`)
447+
api.injectRootOptions('main.js', ['foo', 'bar'])
448+
api.render({
449+
'main.js': path.join(templateDir, 'entry.js')
450+
})
451+
}
452+
}
453+
] })
454+
455+
await generator.generate()
456+
expect(fs.readFileSync('/main.js', 'utf-8')).toMatch(`import foo from 'foo'\nimport bar from 'bar'`)
457+
expect(fs.readFileSync('/main.js', 'utf-8')).toMatch(
458+
`new Vue({
459+
foo,
460+
bar,
461+
render: h => h(App)
462+
})`
463+
)
464+
})
465+
434466
test('extract config files', async () => {
435467
const configs = {
436468
vue: {

packages/@vue/cli/lib/Generator.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const GeneratorAPI = require('./GeneratorAPI')
55
const sortObject = require('./util/sortObject')
66
const writeFileTree = require('./util/writeFileTree')
77
const configTransforms = require('./util/configTransforms')
8+
const injectImportsAndOptions = require('./util/injectImportsAndOptions')
89
const { toShortPluginId, matchesPluginId } = require('@vue/cli-shared-utils')
910

1011
const logger = require('@vue/cli-shared-utils/lib/logger')
@@ -27,6 +28,8 @@ module.exports = class Generator {
2728
this.plugins = plugins
2829
this.originalPkg = pkg
2930
this.pkg = Object.assign({}, pkg)
31+
this.imports = {}
32+
this.rootOptions = {}
3033
this.completeCbs = completeCbs
3134

3235
// for conflict resolution
@@ -136,13 +139,19 @@ module.exports = class Generator {
136139
for (const middleware of this.fileMiddlewares) {
137140
await middleware(files, ejs.render)
138141
}
139-
// normalize paths
140142
Object.keys(files).forEach(file => {
143+
// normalize paths
141144
const normalized = slash(file)
142145
if (file !== normalized) {
143146
files[normalized] = files[file]
144147
delete files[file]
145148
}
149+
// handle imports and root option injections
150+
files[normalized] = injectImportsAndOptions(
151+
files[normalized],
152+
this.imports[normalized],
153+
this.rootOptions[normalized]
154+
)
146155
})
147156
for (const postProcess of this.postProcessFilesCbs) {
148157
await postProcess(files)

packages/@vue/cli/lib/GeneratorAPI.js

+26
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,32 @@ class GeneratorAPI {
199199
genJSConfig (value) {
200200
return `module.exports = ${stringifyJS(value, null, 2)}`
201201
}
202+
203+
/**
204+
* Add import statements to a file.
205+
*/
206+
injectImports (file, imports) {
207+
const _imports = (
208+
this.generator.imports[file] ||
209+
(this.generator.imports[file] = new Set())
210+
)
211+
;(Array.isArray(imports) ? imports : [imports]).forEach(imp => {
212+
_imports.add(imp)
213+
})
214+
}
215+
216+
/**
217+
* Add options to the root Vue instance (detected by `new Vue`).
218+
*/
219+
injectRootOptions (file, options) {
220+
const _options = (
221+
this.generator.rootOptions[file] ||
222+
(this.generator.rootOptions[file] = new Set())
223+
)
224+
;(Array.isArray(options) ? options : [options]).forEach(opt => {
225+
_options.add(opt)
226+
})
227+
}
202228
}
203229

204230
function extractCallDir () {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
module.exports = function injectImportsAndOptions (source, imports, injections) {
2+
imports = imports instanceof Set ? Array.from(imports) : imports
3+
injections = injections instanceof Set ? Array.from(injections) : injections
4+
5+
const hasImports = imports && imports.length > 0
6+
const hasInjections = injections && injections.length > 0
7+
8+
if (!hasImports && !hasInjections) {
9+
return source
10+
}
11+
12+
const recast = require('recast')
13+
const ast = recast.parse(source)
14+
15+
if (hasImports) {
16+
const toImport = i => recast.parse(`${i}\n`).program.body[0]
17+
let lastImportIndex = -1
18+
recast.types.visit(ast, {
19+
visitImportDeclaration ({ node }) {
20+
lastImportIndex = ast.program.body.findIndex(n => n === node)
21+
return false
22+
}
23+
})
24+
// avoid blank line after the previous import
25+
delete ast.program.body[lastImportIndex].loc
26+
27+
const newImports = imports.map(toImport)
28+
ast.program.body.splice(lastImportIndex + 1, 0, ...newImports)
29+
}
30+
31+
if (hasInjections) {
32+
const toProperty = i => {
33+
return recast.parse(`({${i}})`).program.body[0].expression.properties
34+
}
35+
recast.types.visit(ast, {
36+
visitNewExpression ({ node }) {
37+
if (node.callee.name === 'Vue') {
38+
const options = node.arguments[0]
39+
if (options && options.type === 'ObjectExpression') {
40+
const props = options.properties
41+
// inject at index length - 1 as it's usually the render fn
42+
options.properties = [
43+
...props.slice(0, props.length - 1),
44+
...([].concat(...injections.map(toProperty))),
45+
...props.slice(props.length - 1)
46+
]
47+
}
48+
}
49+
return false
50+
}
51+
})
52+
}
53+
54+
return recast.print(ast).code
55+
}

0 commit comments

Comments
 (0)