From 8b32f4a60f4c1dff4d7f995643fffa90a82be35b Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 21 May 2018 14:25:49 -0400 Subject: [PATCH] feat: GeneratorAPI: addImports & addRootOptions --- .../__tests__/pwaGenerator.spec.js | 36 ++++++++---- .../@vue/cli-plugin-pwa/generator/index.js | 17 +----- packages/@vue/cli-service/generator/index.js | 4 ++ .../generator/template/src/main.js | 12 ---- packages/@vue/cli/__tests__/Generator.spec.js | 32 +++++++++++ packages/@vue/cli/lib/Generator.js | 11 +++- packages/@vue/cli/lib/GeneratorAPI.js | 26 +++++++++ .../cli/lib/util/injectImportsAndOptions.js | 55 +++++++++++++++++++ 8 files changed, 153 insertions(+), 40 deletions(-) create mode 100644 packages/@vue/cli/lib/util/injectImportsAndOptions.js diff --git a/packages/@vue/cli-plugin-pwa/__tests__/pwaGenerator.spec.js b/packages/@vue/cli-plugin-pwa/__tests__/pwaGenerator.spec.js index c2bb995d3..222f5f0c7 100644 --- a/packages/@vue/cli-plugin-pwa/__tests__/pwaGenerator.spec.js +++ b/packages/@vue/cli-plugin-pwa/__tests__/pwaGenerator.spec.js @@ -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'`) }) diff --git a/packages/@vue/cli-plugin-pwa/generator/index.js b/packages/@vue/cli-plugin-pwa/generator/index.js index 4cd065d13..433ac8397 100644 --- a/packages/@vue/cli-plugin-pwa/generator/index.js +++ b/packages/@vue/cli-plugin-pwa/generator/index.js @@ -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') - } - }) } diff --git a/packages/@vue/cli-service/generator/index.js b/packages/@vue/cli-service/generator/index.js index 977afe188..e51d289dc 100644 --- a/packages/@vue/cli-service/generator/index.js +++ b/packages/@vue/cli-service/generator/index.js @@ -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' diff --git a/packages/@vue/cli-service/generator/template/src/main.js b/packages/@vue/cli-service/generator/template/src/main.js index a659a87ff..fca74cfc9 100644 --- a/packages/@vue/cli-service/generator/template/src/main.js +++ b/packages/@vue/cli-service/generator/template/src/main.js @@ -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') diff --git a/packages/@vue/cli/__tests__/Generator.spec.js b/packages/@vue/cli/__tests__/Generator.spec.js index 00789ecd4..5033cafc3 100644 --- a/packages/@vue/cli/__tests__/Generator.spec.js +++ b/packages/@vue/cli/__tests__/Generator.spec.js @@ -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: { diff --git a/packages/@vue/cli/lib/Generator.js b/packages/@vue/cli/lib/Generator.js index b1c2e2432..70a147195 100644 --- a/packages/@vue/cli/lib/Generator.js +++ b/packages/@vue/cli/lib/Generator.js @@ -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) diff --git a/packages/@vue/cli/lib/GeneratorAPI.js b/packages/@vue/cli/lib/GeneratorAPI.js index 386040ebe..736306573 100644 --- a/packages/@vue/cli/lib/GeneratorAPI.js +++ b/packages/@vue/cli/lib/GeneratorAPI.js @@ -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 () { diff --git a/packages/@vue/cli/lib/util/injectImportsAndOptions.js b/packages/@vue/cli/lib/util/injectImportsAndOptions.js new file mode 100644 index 000000000..57c152a8b --- /dev/null +++ b/packages/@vue/cli/lib/util/injectImportsAndOptions.js @@ -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 +}