diff --git a/packages/@vue/cli/__tests__/Generator.spec.js b/packages/@vue/cli/__tests__/Generator.spec.js index 5091601959..7e4e0cb274 100644 --- a/packages/@vue/cli/__tests__/Generator.spec.js +++ b/packages/@vue/cli/__tests__/Generator.spec.js @@ -22,6 +22,7 @@ new Vue({ render: h => h(App) }).$mount('#app') `.trim()) +fs.writeFileSync(path.resolve(templateDir, 'empty-entry.js'), `;`) // replace stubs fs.writeFileSync(path.resolve(templateDir, 'replace.js'), ` @@ -465,10 +466,28 @@ test('api: addEntryImport & addEntryInjection', async () => { ] }) await generator.generate() - expect(fs.readFileSync('/main.js', 'utf-8')).toMatch(/import foo from 'foo'\s+import bar from 'bar'/) + expect(fs.readFileSync('/main.js', 'utf-8')).toMatch(/import foo from 'foo'\r?\nimport bar from 'bar'/) expect(fs.readFileSync('/main.js', 'utf-8')).toMatch(/new Vue\({\s+p: p\(\),\s+baz,\s+foo,\s+bar,\s+render: h => h\(App\)\s+}\)/) }) +test('api: injectImports to empty file', async () => { + const generator = new Generator('/', { plugins: [ + { + id: 'test', + apply: api => { + api.injectImports('main.js', `import foo from 'foo'`) + api.injectImports('main.js', `import bar from 'bar'`) + api.render({ + 'main.js': path.join(templateDir, 'empty-entry.js') + }) + } + } + ] }) + + await generator.generate() + expect(fs.readFileSync('/main.js', 'utf-8')).toMatch(/import foo from 'foo'\r?\nimport bar from 'bar'/) +}) + test('api: addEntryDuplicateImport', async () => { const generator = new Generator('/', { plugins: [ { diff --git a/packages/@vue/cli/lib/util/injectImportsAndOptions.js b/packages/@vue/cli/lib/util/injectImportsAndOptions.js index 68e69958aa..36a94622c0 100644 --- a/packages/@vue/cli/lib/util/injectImportsAndOptions.js +++ b/packages/@vue/cli/lib/util/injectImportsAndOptions.js @@ -9,67 +9,56 @@ module.exports = function injectImportsAndOptions (source, imports, injections) return source } - const recast = require('recast') - const ast = recast.parse(source) + const j = require('jscodeshift') + const root = j(source) if (hasImports) { - const toImport = i => recast.parse(`${i}\n`).program.body[0] - const importDeclarations = [] - let lastImportIndex = -1 - - recast.types.visit(ast, { - visitImportDeclaration ({ node }) { - lastImportIndex = ast.program.body.findIndex(n => n === node) - importDeclarations.push(node) - return false - } + 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 }) - // avoid blank line after the previous import - if (lastImportIndex !== -1) { - delete ast.program.body[lastImportIndex].loc - } - const nonDuplicates = i => { - return !importDeclarations.some(node => { - const result = node.source.raw === i.source.raw && node.specifiers.length === i.specifiers.length + const declarations = root.find(j.ImportDeclaration) + const importSet = new Set(declarations.nodes().map(toImportHash)) + const nonDuplicates = node => !importSet.has(toImportHash(node)) - return result && node.specifiers.every((item, index) => { - return i.specifiers[index].local.name === item.local.name - }) - }) - } + const importASTNodes = imports.map(toImportAST).filter(nonDuplicates) - const newImports = imports.map(toImport).filter(nonDuplicates) - ast.program.body.splice(lastImportIndex + 1, 0, ...newImports) + 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 toProperty = i => { - return recast.parse(`({${i}})`).program.body[0].expression.properties + const toPropertyAST = i => { + return j(`({${i}})`).nodes()[0].program.body[0].expression.properties[0] } - recast.types.visit(ast, { - visitNewExpression ({ node }) { - if (node.callee.name === 'Vue') { - const options = node.arguments[0] - if (options && options.type === 'ObjectExpression') { - const nonDuplicates = i => { - return !options.properties.slice(0, -1).some(p => { - return p.key.name === i[0].key.name && - recast.print(p.value).code === recast.print(i[0].value).code - }) - } - // inject at index length - 1 as it's usually the render fn - options.properties = [ - ...options.properties.slice(0, -1), - ...([].concat(...injections.map(toProperty).filter(nonDuplicates))), - ...options.properties.slice(-1) - ] - } - } - return false - } - }) + + 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 recast.print(ast).code + return root.toSource() } diff --git a/packages/@vue/cli/package.json b/packages/@vue/cli/package.json index ab348dfeec..3685055423 100644 --- a/packages/@vue/cli/package.json +++ b/packages/@vue/cli/package.json @@ -45,6 +45,7 @@ "isbinaryfile": "^3.0.2", "javascript-stringify": "^1.6.0", "js-yaml": "^3.13.1", + "jscodeshift": "^0.6.4", "lodash.clonedeep": "^4.5.0", "minimist": "^1.2.0", "recast": "^0.17.6",