From 1b7d1bdd5ffe42f8b03d0675fe75e8981bac8231 Mon Sep 17 00:00:00 2001 From: Haoqun Jiang Date: Wed, 19 Jun 2019 17:07:35 +0800 Subject: [PATCH 1/2] feat: make `injectImports` & `injectRootOptions` work for `.vue` files --- packages/@vue/cli/lib/Generator.js | 26 ++++++-- .../cli/lib/util/codemods/injectImports.js | 29 +++++++++ .../cli/lib/util/codemods/injectOptions.js | 27 ++++++++ .../cli/lib/util/injectImportsAndOptions.js | 64 ------------------- packages/@vue/cli/lib/util/runCodemod.js | 6 ++ packages/@vue/cli/package.json | 1 + 6 files changed, 83 insertions(+), 70 deletions(-) create mode 100644 packages/@vue/cli/lib/util/codemods/injectImports.js create mode 100644 packages/@vue/cli/lib/util/codemods/injectOptions.js delete mode 100644 packages/@vue/cli/lib/util/injectImportsAndOptions.js create mode 100644 packages/@vue/cli/lib/util/runCodemod.js diff --git a/packages/@vue/cli/lib/Generator.js b/packages/@vue/cli/lib/Generator.js index 30f6ff8873..87386bea07 100644 --- a/packages/@vue/cli/lib/Generator.js +++ b/packages/@vue/cli/lib/Generator.js @@ -5,7 +5,7 @@ const sortObject = require('./util/sortObject') const writeFileTree = require('./util/writeFileTree') const inferRootOptions = require('./util/inferRootOptions') const normalizeFilePaths = require('./util/normalizeFilePaths') -const injectImportsAndOptions = require('./util/injectImportsAndOptions') +const runCodemod = require('./util/runCodemod') const { toShortPluginId, matchesPluginId } = require('@vue/cli-shared-utils') const ConfigTransform = require('./ConfigTransform') @@ -215,11 +215,25 @@ module.exports = class Generator { // handle imports and root option injections Object.keys(files).forEach(file => { - files[file] = injectImportsAndOptions( - files[file], - this.imports[file], - this.rootOptions[file] - ) + let imports = this.imports[file] + imports = imports instanceof Set ? Array.from(imports) : imports + if (imports && imports.length > 0) { + files[file] = runCodemod( + require('./util/codemods/injectImports'), + { path: file, source: files[file] }, + { imports } + ) + } + + let injections = this.rootOptions[file] + injections = injections instanceof Set ? Array.from(injections) : injections + if (injections && injections.length > 0) { + files[file] = runCodemod( + require('./util/codemods/injectOptions'), + { path: file, source: files[file] }, + { injections } + ) + } }) for (const postProcess of this.postProcessFilesCbs) { diff --git a/packages/@vue/cli/lib/util/codemods/injectImports.js b/packages/@vue/cli/lib/util/codemods/injectImports.js new file mode 100644 index 0000000000..fe95bd7f78 --- /dev/null +++ b/packages/@vue/cli/lib/util/codemods/injectImports.js @@ -0,0 +1,29 @@ +module.exports = function injectImports (fileInfo, api, { imports }) { + const j = api.jscodeshift + const root = j(fileInfo.source) + + const toImportAST = i => j(`${i}\n`).nodes()[0].program.body[0] + const toImportHash = node => JSON.stringify({ + specifiers: node.specifiers.map(s => s.local.name), + source: node.source.raw + }) + + const declarations = root.find(j.ImportDeclaration) + const importSet = new Set(declarations.nodes().map(toImportHash)) + const nonDuplicates = node => !importSet.has(toImportHash(node)) + + const importASTNodes = imports.map(toImportAST).filter(nonDuplicates) + + if (declarations.length) { + declarations + .at(-1) + // a tricky way to avoid blank line after the previous import + .forEach(({ node }) => delete node.loc) + .insertAfter(importASTNodes) + } else { + // no pre-existing import declarations + root.get().node.program.body.unshift(...importASTNodes) + } + + return root.toSource() +} diff --git a/packages/@vue/cli/lib/util/codemods/injectOptions.js b/packages/@vue/cli/lib/util/codemods/injectOptions.js new file mode 100644 index 0000000000..246244244c --- /dev/null +++ b/packages/@vue/cli/lib/util/codemods/injectOptions.js @@ -0,0 +1,27 @@ +module.exports = function injectOptions (fileInfo, api, { injections }) { + const j = api.jscodeshift + const root = j(fileInfo.source) + + const toPropertyAST = i => { + return j(`({${i}})`).nodes()[0].program.body[0].expression.properties[0] + } + + const properties = root + .find(j.NewExpression, { + callee: { name: 'Vue' }, + arguments: [{ type: 'ObjectExpression' }] + }) + .map(path => path.get('arguments', 0)) + .get() + .node + .properties + + const toPropertyHash = p => `${p.key.name}: ${j(p.value).toSource()}` + const propertySet = new Set(properties.map(toPropertyHash)) + const nonDuplicates = p => !propertySet.has(toPropertyHash(p)) + + // inject at index length - 1 as it's usually the render fn + properties.splice(-1, 0, ...injections.map(toPropertyAST).filter(nonDuplicates)) + + return root.toSource() +} diff --git a/packages/@vue/cli/lib/util/injectImportsAndOptions.js b/packages/@vue/cli/lib/util/injectImportsAndOptions.js deleted file mode 100644 index 36a94622c0..0000000000 --- a/packages/@vue/cli/lib/util/injectImportsAndOptions.js +++ /dev/null @@ -1,64 +0,0 @@ -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 j = require('jscodeshift') - const root = j(source) - - if (hasImports) { - const toImportAST = i => j(`${i}\n`).nodes()[0].program.body[0] - const toImportHash = node => JSON.stringify({ - specifiers: node.specifiers.map(s => s.local.name), - source: node.source.raw - }) - - const declarations = root.find(j.ImportDeclaration) - const importSet = new Set(declarations.nodes().map(toImportHash)) - const nonDuplicates = node => !importSet.has(toImportHash(node)) - - const importASTNodes = imports.map(toImportAST).filter(nonDuplicates) - - if (declarations.length) { - declarations - .at(-1) - // a tricky way to avoid blank line after the previous import - .forEach(({ node }) => delete node.loc) - .insertAfter(importASTNodes) - } else { - // no pre-existing import declarations - root.get().node.program.body.unshift(...importASTNodes) - } - } - - if (hasInjections) { - const toPropertyAST = i => { - return j(`({${i}})`).nodes()[0].program.body[0].expression.properties[0] - } - - const properties = root - .find(j.NewExpression, { - callee: { name: 'Vue' }, - arguments: [{ type: 'ObjectExpression' }] - }) - .map(path => path.get('arguments', 0)) - .get() - .node - .properties - - const toPropertyHash = p => `${p.key.name}: ${j(p.value).toSource()}` - const propertySet = new Set(properties.map(toPropertyHash)) - const nonDuplicates = p => !propertySet.has(toPropertyHash(p)) - - // inject at index length - 1 as it's usually the render fn - properties.splice(-1, 0, ...injections.map(toPropertyAST).filter(nonDuplicates)) - } - - return root.toSource() -} diff --git a/packages/@vue/cli/lib/util/runCodemod.js b/packages/@vue/cli/lib/util/runCodemod.js new file mode 100644 index 0000000000..22fbc82cf6 --- /dev/null +++ b/packages/@vue/cli/lib/util/runCodemod.js @@ -0,0 +1,6 @@ +const jscodeshift = require('jscodeshift') +const adapt = require('vue-jscodeshift-adapter') + +module.exports = function runCodemod (transform, fileInfo, options) { + return adapt(transform)(fileInfo, { jscodeshift }, options || {}) +} diff --git a/packages/@vue/cli/package.json b/packages/@vue/cli/package.json index b609233d19..42d02451ed 100644 --- a/packages/@vue/cli/package.json +++ b/packages/@vue/cli/package.json @@ -56,6 +56,7 @@ "shortid": "^2.2.11", "slash": "^3.0.0", "validate-npm-package-name": "^3.0.0", + "vue-jscodeshift-adapter": "2.0.2", "yaml-front-matter": "^3.4.1" }, "engines": { From 77c7c66922fe2d007ed2094db5441ebc18ca825d Mon Sep 17 00:00:00 2001 From: Haoqun Jiang Date: Wed, 19 Jun 2019 20:07:25 +0800 Subject: [PATCH 2/2] test: add .vue test case --- packages/@vue/cli/__tests__/Generator.spec.js | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/@vue/cli/__tests__/Generator.spec.js b/packages/@vue/cli/__tests__/Generator.spec.js index 7e4e0cb274..6da53c4368 100644 --- a/packages/@vue/cli/__tests__/Generator.spec.js +++ b/packages/@vue/cli/__tests__/Generator.spec.js @@ -23,6 +23,19 @@ new Vue({ }).$mount('#app') `.trim()) fs.writeFileSync(path.resolve(templateDir, 'empty-entry.js'), `;`) +fs.writeFileSync(path.resolve(templateDir, 'hello.vue'), ` + + +`) // replace stubs fs.writeFileSync(path.resolve(templateDir, 'replace.js'), ` @@ -505,6 +518,25 @@ test('api: addEntryDuplicateImport', async () => { expect(fs.readFileSync('/main.js', 'utf-8')).toMatch(/^import foo from 'foo'\s+new Vue/) }) +test('api: injectImport for .vue files', async () => { + const generator = new Generator('/', { plugins: [ + { + id: 'test', + apply: api => { + api.injectImports('hello.vue', `import foo from 'foo'`) + api.render({ + 'hello.vue': path.join(templateDir, 'hello.vue') + }) + } + } + ] }) + + await generator.generate() + const content = fs.readFileSync('/hello.vue', 'utf-8') + expect(content).toMatch(/import foo from 'foo'/) + expect(content).toMatch(/