Skip to content

Commit 2a151b0

Browse files
committed
refactor: replace recast with jscodeshift for injectImportsAndOptions (#4003)
fixes #3309 (cherry picked from commit 2e417b0)
1 parent 844f75b commit 2a151b0

File tree

3 files changed

+61
-52
lines changed

3 files changed

+61
-52
lines changed

packages/@vue/cli/__tests__/Generator.spec.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ new Vue({
2222
render: h => h(App)
2323
}).$mount('#app')
2424
`.trim())
25+
fs.writeFileSync(path.resolve(templateDir, 'empty-entry.js'), `;`)
2526

2627
// replace stubs
2728
fs.writeFileSync(path.resolve(templateDir, 'replace.js'), `
@@ -465,10 +466,28 @@ test('api: addEntryImport & addEntryInjection', async () => {
465466
] })
466467

467468
await generator.generate()
468-
expect(fs.readFileSync('/main.js', 'utf-8')).toMatch(/import foo from 'foo'\s+import bar from 'bar'/)
469+
expect(fs.readFileSync('/main.js', 'utf-8')).toMatch(/import foo from 'foo'\r?\nimport bar from 'bar'/)
469470
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+}\)/)
470471
})
471472

473+
test('api: injectImports to empty file', async () => {
474+
const generator = new Generator('/', { plugins: [
475+
{
476+
id: 'test',
477+
apply: api => {
478+
api.injectImports('main.js', `import foo from 'foo'`)
479+
api.injectImports('main.js', `import bar from 'bar'`)
480+
api.render({
481+
'main.js': path.join(templateDir, 'empty-entry.js')
482+
})
483+
}
484+
}
485+
] })
486+
487+
await generator.generate()
488+
expect(fs.readFileSync('/main.js', 'utf-8')).toMatch(/import foo from 'foo'\r?\nimport bar from 'bar'/)
489+
})
490+
472491
test('api: addEntryDuplicateImport', async () => {
473492
const generator = new Generator('/', { plugins: [
474493
{

packages/@vue/cli/lib/util/injectImportsAndOptions.js

Lines changed: 40 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -9,67 +9,56 @@ module.exports = function injectImportsAndOptions (source, imports, injections)
99
return source
1010
}
1111

12-
const recast = require('recast')
13-
const ast = recast.parse(source)
12+
const j = require('jscodeshift')
13+
const root = j(source)
1414

1515
if (hasImports) {
16-
const toImport = i => recast.parse(`${i}\n`).program.body[0]
17-
const importDeclarations = []
18-
let lastImportIndex = -1
19-
20-
recast.types.visit(ast, {
21-
visitImportDeclaration ({ node }) {
22-
lastImportIndex = ast.program.body.findIndex(n => n === node)
23-
importDeclarations.push(node)
24-
return false
25-
}
16+
const toImportAST = i => j(`${i}\n`).nodes()[0].program.body[0]
17+
const toImportHash = node => JSON.stringify({
18+
specifiers: node.specifiers.map(s => s.local.name),
19+
source: node.source.raw
2620
})
27-
// avoid blank line after the previous import
28-
if (lastImportIndex !== -1) {
29-
delete ast.program.body[lastImportIndex].loc
30-
}
3121

32-
const nonDuplicates = i => {
33-
return !importDeclarations.some(node => {
34-
const result = node.source.raw === i.source.raw && node.specifiers.length === i.specifiers.length
22+
const declarations = root.find(j.ImportDeclaration)
23+
const importSet = new Set(declarations.nodes().map(toImportHash))
24+
const nonDuplicates = node => !importSet.has(toImportHash(node))
3525

36-
return result && node.specifiers.every((item, index) => {
37-
return i.specifiers[index].local.name === item.local.name
38-
})
39-
})
40-
}
26+
const importASTNodes = imports.map(toImportAST).filter(nonDuplicates)
4127

42-
const newImports = imports.map(toImport).filter(nonDuplicates)
43-
ast.program.body.splice(lastImportIndex + 1, 0, ...newImports)
28+
if (declarations.length) {
29+
declarations
30+
.at(-1)
31+
// a tricky way to avoid blank line after the previous import
32+
.forEach(({ node }) => delete node.loc)
33+
.insertAfter(importASTNodes)
34+
} else {
35+
// no pre-existing import declarations
36+
root.get().node.program.body.unshift(...importASTNodes)
37+
}
4438
}
4539

4640
if (hasInjections) {
47-
const toProperty = i => {
48-
return recast.parse(`({${i}})`).program.body[0].expression.properties
41+
const toPropertyAST = i => {
42+
return j(`({${i}})`).nodes()[0].program.body[0].expression.properties[0]
4943
}
50-
recast.types.visit(ast, {
51-
visitNewExpression ({ node }) {
52-
if (node.callee.name === 'Vue') {
53-
const options = node.arguments[0]
54-
if (options && options.type === 'ObjectExpression') {
55-
const nonDuplicates = i => {
56-
return !options.properties.slice(0, -1).some(p => {
57-
return p.key.name === i[0].key.name &&
58-
recast.print(p.value).code === recast.print(i[0].value).code
59-
})
60-
}
61-
// inject at index length - 1 as it's usually the render fn
62-
options.properties = [
63-
...options.properties.slice(0, -1),
64-
...([].concat(...injections.map(toProperty).filter(nonDuplicates))),
65-
...options.properties.slice(-1)
66-
]
67-
}
68-
}
69-
return false
70-
}
71-
})
44+
45+
const properties = root
46+
.find(j.NewExpression, {
47+
callee: { name: 'Vue' },
48+
arguments: [{ type: 'ObjectExpression' }]
49+
})
50+
.map(path => path.get('arguments', 0))
51+
.get()
52+
.node
53+
.properties
54+
55+
const toPropertyHash = p => `${p.key.name}: ${j(p.value).toSource()}`
56+
const propertySet = new Set(properties.map(toPropertyHash))
57+
const nonDuplicates = p => !propertySet.has(toPropertyHash(p))
58+
59+
// inject at index length - 1 as it's usually the render fn
60+
properties.splice(-1, 0, ...injections.map(toPropertyAST).filter(nonDuplicates))
7261
}
7362

74-
return recast.print(ast).code
63+
return root.toSource()
7564
}

packages/@vue/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"isbinaryfile": "^3.0.2",
4646
"javascript-stringify": "^1.6.0",
4747
"js-yaml": "^3.13.1",
48+
"jscodeshift": "^0.6.4",
4849
"lodash.clonedeep": "^4.5.0",
4950
"minimist": "^1.2.0",
5051
"recast": "^0.17.5",

0 commit comments

Comments
 (0)