refactor: replace recast with jscodeshift for injectImportsAndOptions (#4003)

fixes #3309

(cherry picked from commit 2e417b01d2)
This commit is contained in:
Haoqun Jiang
2019-05-17 14:58:31 +08:00
parent 844f75b6ab
commit 2a151b0e18
3 changed files with 63 additions and 54 deletions

View File

@@ -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: [
{

View File

@@ -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 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)
}
const nonDuplicates = i => {
return !importDeclarations.some(node => {
const result = node.source.raw === i.source.raw && node.specifiers.length === i.specifiers.length
return result && node.specifiers.every((item, index) => {
return i.specifiers[index].local.name === item.local.name
})
})
}
const newImports = imports.map(toImport).filter(nonDuplicates)
ast.program.body.splice(lastImportIndex + 1, 0, ...newImports)
}
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()
}

View File

@@ -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.5",