mirror of
https://github.com/vuejs/vue-cli.git
synced 2026-01-24 16:18:57 -06:00
feat: GeneratorAPI: addImports & addRootOptions
This commit is contained in:
@@ -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'`)
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
55
packages/@vue/cli/lib/util/injectImportsAndOptions.js
Normal file
55
packages/@vue/cli/lib/util/injectImportsAndOptions.js
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user