Skip to content

refactor: replace recast with jscodeshift for injectImportsAndOptions #4003

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion packages/@vue/cli/__tests__/Generator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'), `
Expand Down Expand Up @@ -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: [
{
Expand Down
91 changes: 40 additions & 51 deletions packages/@vue/cli/lib/util/injectImportsAndOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
1 change: 1 addition & 0 deletions packages/@vue/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down