feat: GeneratorAPI: addImports & addRootOptions

This commit is contained in:
Evan You
2018-05-21 14:25:49 -04:00
parent 581abd34ef
commit 8b32f4a60f
8 changed files with 153 additions and 40 deletions

View File

@@ -1,18 +1,10 @@
const generateWithPlugin = require('@vue/cli-test-utils/generateWithPlugin')
test('inject import statement for service worker', async () => {
const mockMain = (
`import Vue from 'vue'\n` +
`import Bar from './Bar.vue'`
)
const { files } = await generateWithPlugin([
{
id: 'files',
apply: api => {
api.render(files => {
files['src/main.js'] = mockMain
})
},
id: 'core',
apply: require('@vue/cli-service/generator'),
options: {}
},
{
@@ -22,5 +14,27 @@ test('inject import statement for service worker', async () => {
}
])
expect(files['src/main.js']).toMatch(`${mockMain}\nimport './registerServiceWorker'`)
expect(files['src/main.js']).toMatch(`import './registerServiceWorker'`)
})
test('inject import statement for service worker (with TS)', async () => {
const { files } = await generateWithPlugin([
{
id: 'core',
apply: require('@vue/cli-service/generator'),
options: {}
},
{
id: 'typescript',
apply: require('@vue/cli-plugin-typescript/generator'),
options: {}
},
{
id: 'pwa',
apply: require('../generator'),
options: {}
}
])
expect(files['src/main.ts']).toMatch(`import './registerServiceWorker'`)
})

View File

@@ -4,21 +4,6 @@ module.exports = api => {
'register-service-worker': '^1.0.0'
}
})
api.injectImports(`src/main.js`, `import './registerServiceWorker'`)
api.render('./template')
api.postProcessFiles(files => {
const isTS = 'src/main.ts' in files
const file = isTS
? 'src/main.ts'
: 'src/main.js'
const main = files[file]
if (main) {
// inject import for registerServiceWorker script into main.js
const lines = main.split(/\r?\n/g).reverse()
const lastImportIndex = lines.findIndex(line => line.match(/^import/))
lines[lastImportIndex] += `\nimport './registerServiceWorker'`
files[file] = lines.reverse().join('\n')
}
})
}

View File

@@ -31,6 +31,8 @@ module.exports = (api, options) => {
})
if (options.router) {
api.injectImports(`src/main.js`, `import router from './router'`)
api.injectRootOptions(`src/main.js`, `router`)
api.extendPackage({
dependencies: {
'vue-router': '^3.0.1'
@@ -39,6 +41,8 @@ module.exports = (api, options) => {
}
if (options.vuex) {
api.injectImports(`import store from './store'`)
api.injectRootOptions(`store`)
api.extendPackage({
dependencies: {
vuex: '^3.0.1'

View File

@@ -1,20 +1,8 @@
import Vue from 'vue'
import App from './App.vue'
<%_ if (rootOptions.router) { _%>
import router from './router'
<%_ } _%>
<%_ if (rootOptions.vuex) { _%>
import store from './store'
<%_ } _%>
Vue.config.productionTip = false
new Vue({
<%_ if (rootOptions.router) { _%>
router,
<%_ } _%>
<%_ if (rootOptions.vuex) { _%>
store,
<%_ } _%>
render: h => h(App)
}).$mount('#app')

View File

@@ -12,6 +12,13 @@ fs.ensureDirSync(templateDir)
fs.writeFileSync(path.resolve(templateDir, 'foo.js'), 'foo(<%- options.n %>)')
fs.ensureDirSync(path.resolve(templateDir, 'bar'))
fs.writeFileSync(path.resolve(templateDir, 'bar/bar.js'), 'bar(<%- m %>)')
fs.writeFileSync(path.resolve(templateDir, 'entry.js'), `
import foo from 'foo'
new Vue({
render: h => h(App)
}).$mount('#app')
`.trim())
fs.writeFileSync(path.resolve(templateDir, 'replace.js'), `
---
@@ -431,6 +438,31 @@ test('api: resolve', () => {
] })
})
test('api: addEntryImport & addEntryInjection', async () => {
const generator = new Generator('/', { plugins: [
{
id: 'test',
apply: api => {
api.injectImports('main.js', `import bar from 'bar'`)
api.injectRootOptions('main.js', ['foo', 'bar'])
api.render({
'main.js': path.join(templateDir, 'entry.js')
})
}
}
] })
await generator.generate()
expect(fs.readFileSync('/main.js', 'utf-8')).toMatch(`import foo from 'foo'\nimport bar from 'bar'`)
expect(fs.readFileSync('/main.js', 'utf-8')).toMatch(
`new Vue({
foo,
bar,
render: h => h(App)
})`
)
})
test('extract config files', async () => {
const configs = {
vue: {

View File

@@ -5,6 +5,7 @@ const GeneratorAPI = require('./GeneratorAPI')
const sortObject = require('./util/sortObject')
const writeFileTree = require('./util/writeFileTree')
const configTransforms = require('./util/configTransforms')
const injectImportsAndOptions = require('./util/injectImportsAndOptions')
const { toShortPluginId, matchesPluginId } = require('@vue/cli-shared-utils')
const logger = require('@vue/cli-shared-utils/lib/logger')
@@ -27,6 +28,8 @@ module.exports = class Generator {
this.plugins = plugins
this.originalPkg = pkg
this.pkg = Object.assign({}, pkg)
this.imports = {}
this.rootOptions = {}
this.completeCbs = completeCbs
// for conflict resolution
@@ -136,13 +139,19 @@ module.exports = class Generator {
for (const middleware of this.fileMiddlewares) {
await middleware(files, ejs.render)
}
// normalize paths
Object.keys(files).forEach(file => {
// normalize paths
const normalized = slash(file)
if (file !== normalized) {
files[normalized] = files[file]
delete files[file]
}
// handle imports and root option injections
files[normalized] = injectImportsAndOptions(
files[normalized],
this.imports[normalized],
this.rootOptions[normalized]
)
})
for (const postProcess of this.postProcessFilesCbs) {
await postProcess(files)

View File

@@ -199,6 +199,32 @@ class GeneratorAPI {
genJSConfig (value) {
return `module.exports = ${stringifyJS(value, null, 2)}`
}
/**
* Add import statements to a file.
*/
injectImports (file, imports) {
const _imports = (
this.generator.imports[file] ||
(this.generator.imports[file] = new Set())
)
;(Array.isArray(imports) ? imports : [imports]).forEach(imp => {
_imports.add(imp)
})
}
/**
* Add options to the root Vue instance (detected by `new Vue`).
*/
injectRootOptions (file, options) {
const _options = (
this.generator.rootOptions[file] ||
(this.generator.rootOptions[file] = new Set())
)
;(Array.isArray(options) ? options : [options]).forEach(opt => {
_options.add(opt)
})
}
}
function extractCallDir () {

View File

@@ -0,0 +1,55 @@
module.exports = function injectImportsAndOptions (source, imports, injections) {
imports = imports instanceof Set ? Array.from(imports) : imports
injections = injections instanceof Set ? Array.from(injections) : injections
const hasImports = imports && imports.length > 0
const hasInjections = injections && injections.length > 0
if (!hasImports && !hasInjections) {
return source
}
const recast = require('recast')
const ast = recast.parse(source)
if (hasImports) {
const toImport = i => recast.parse(`${i}\n`).program.body[0]
let lastImportIndex = -1
recast.types.visit(ast, {
visitImportDeclaration ({ node }) {
lastImportIndex = ast.program.body.findIndex(n => n === node)
return false
}
})
// avoid blank line after the previous import
delete ast.program.body[lastImportIndex].loc
const newImports = imports.map(toImport)
ast.program.body.splice(lastImportIndex + 1, 0, ...newImports)
}
if (hasInjections) {
const toProperty = i => {
return recast.parse(`({${i}})`).program.body[0].expression.properties
}
recast.types.visit(ast, {
visitNewExpression ({ node }) {
if (node.callee.name === 'Vue') {
const options = node.arguments[0]
if (options && options.type === 'ObjectExpression') {
const props = options.properties
// inject at index length - 1 as it's usually the render fn
options.properties = [
...props.slice(0, props.length - 1),
...([].concat(...injections.map(toProperty))),
...props.slice(props.length - 1)
]
}
}
return false
}
})
}
return recast.print(ast).code
}