Skip to content

Commit 4235b22

Browse files
committed
feat: support applying jscodeshift codemods to .vue files
1 parent ce3470f commit 4235b22

15 files changed

+518
-235
lines changed

.editorconfig

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
root = true
2+
3+
[*]
4+
end_of_line = lf
5+
indent_style = space
6+
indent_size = 2
7+
insert_final_newline = true
8+
trim_trailing_whitespace = true

.prettierrc.js

Whitespace-only changes.

README.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ Inspired by [react-codemod](https://github.com/reactjs/react-codemod).
88

99
## Command Line Usage
1010

11-
`npx vue-codemod -t <transformation> --params [transformation params] <path> [...additional options]`
11+
`npx vue-codemod <path> -t <transformation> --params [transformation params] [...additional options]`
1212

1313
- `transformation` (required) - name of transformation, see available transformations below; or you can provide a path to a custom transformation module.
1414
- `path` (required) - files or directory to transform.
15-
- `--params` (optional) - additional transformation specific args .
15+
- `--params` (optional) - additional transformation specific args.
1616
- use the `--dry` options for a dry-run.
1717

1818
## Programmatic API
@@ -23,9 +23,9 @@ TODO
2323

2424
(Sort by priority)
2525

26-
- [ ] Basic testing setup and a dummy CLI
27-
- [ ] Support applying `jscodeshift` codemods to `.vue` files
28-
- [ ] Provide a programmatic interface for usage in `vue-cli-plugin-vue-next`
26+
- [x] Basic testing setup and a dummy CLI
27+
- [x] Support applying `jscodeshift` codemods to `.vue` files
28+
- [x] Provide a programmatic interface for usage in `vue-cli-plugin-vue-next`
2929
- [ ] Define an interface for transformation of template blocks
3030
- [ ] A playground for writing transformations
3131
- [ ] Implement more transformations for [active RFCs](https://github.com/vuejs/rfcs/tree/master/active-rfcs)

bin/vue-codemod.ts

100644100755
+73-36
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,83 @@
11
#!/usr/bin/env node
22

3-
import program from 'commander'
4-
import execa from 'execa'
5-
63
import * as fs from 'fs'
74
import * as path from 'path'
5+
import Module from 'module'
86

9-
import { version } from '../package.json'
10-
11-
program
12-
.name('vue-codemod')
13-
.version(version)
14-
15-
.requiredOption('-t, --transformation <name>', 'Name or path of a transfromation module')
16-
.requiredOption('-f, --file <glob>', 'Files or directory to run codemod on')
17-
.option('--dry', 'Dry run (no changes are made to files)')
18-
19-
.parse(process.argv)
20-
21-
function resolveTransformation (name: string) {
22-
const builtInPath = require.resolve(`../transforms/${name}`)
23-
if (fs.existsSync(builtInPath)) {
24-
return builtInPath
25-
}
26-
27-
const customModulePath = path.resolve(process.cwd(), name)
28-
if (fs.existsSync(customModulePath)) {
29-
return customModulePath
7+
import * as yargs from 'yargs'
8+
import * as globby from 'globby'
9+
10+
import createDebug from 'debug'
11+
12+
import builtInTransformations from '../transformations'
13+
import runTransformation from '../src/run-transformation'
14+
15+
const debug = createDebug('vue-codemod')
16+
const log = console.log.bind(console)
17+
18+
const { _: files, transformation: transformationName, params } = yargs
19+
.usage('Usage: $0 [file pattern]')
20+
.option('transformation', {
21+
alias: 't',
22+
type: 'string',
23+
describe: 'Name or path of the transformation module',
24+
})
25+
.option('params', {
26+
alias: 'p',
27+
describe: 'Custom params to the transformation',
28+
})
29+
.demandOption('transformation')
30+
.help().argv
31+
32+
// TODO: port the `Runner` interface of jscodeshift
33+
async function main() {
34+
const resolvedPaths = globby.sync(files)
35+
const transformation = loadTransformation(transformationName)
36+
37+
log(`Processing ${resolvedPaths.length} files…`)
38+
39+
for (const p of resolvedPaths) {
40+
debug(`Processing ${p}…`)
41+
const fileInfo = {
42+
path: p,
43+
source: fs.readFileSync(p).toString(),
44+
}
45+
try {
46+
const result = runTransformation(
47+
fileInfo,
48+
transformation,
49+
params as object
50+
)
51+
fs.writeFileSync(p, result)
52+
} catch (e) {
53+
console.error(e)
54+
}
3055
}
3156
}
3257

33-
const transformationPath = resolveTransformation(program.transformation)
34-
if (!transformationPath) {
35-
console.error(`Cannot find transformation module ${program.transformation}`)
58+
main().catch((err) => {
59+
console.error(err)
3660
process.exit(1)
37-
}
38-
39-
// TODO:
40-
// don't depend on the jscodeshift **CLI** interface
41-
// so that we can apply the adapter to all code transforms directly
42-
const jscodeshiftExecutable = require.resolve('.bin/jscodeshift')
43-
execa.sync(jscodeshiftExecutable, ['-t', transformationPath, program.file, '--extensions', 'vue,js'], {
44-
stdio: 'inherit',
45-
stripFinalNewline: false
4661
})
62+
63+
function loadTransformation(nameOrPath: string) {
64+
let transformation = builtInTransformations[nameOrPath]
65+
if (transformation) {
66+
return transformation
67+
}
68+
69+
const customModulePath = path.resolve(process.cwd(), nameOrPath)
70+
if (fs.existsSync(customModulePath)) {
71+
const requireFunc = Module.createRequire(
72+
path.resolve(process.cwd(), './package.json')
73+
)
74+
// TODO: interop with ES module
75+
// TODO: fix absolute path
76+
const module = requireFunc(`./${nameOrPath}`)
77+
const transformation =
78+
typeof module.default === 'function' ? module.default : module
79+
return transformation
80+
}
81+
82+
throw new Error(`Cannot find transformation module ${nameOrPath}`)
83+
}

package.json

+23-3
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,45 @@
33
"version": "0.0.1",
44
"description": "Vue codemod scripts",
55
"main": "index.js",
6+
"bin": "./dist/bin/vue-codemod.js",
7+
"files": [
8+
"dist"
9+
],
10+
"scripts": {
11+
"build": "tsc",
12+
"prepublishOnly": "tsc"
13+
},
614
"repository": {
715
"type": "git",
816
"url": "https://github.com/vuejs/vue-codemod.git"
917
},
1018
"author": "Haoqun Jiang",
1119
"license": "MIT",
1220
"dependencies": {
13-
"commander": "^4.1.0",
14-
"execa": "^4.0.0",
21+
"@types/debug": "^4.1.5",
22+
"@vue/compiler-sfc": "^3.0.0-alpha.12",
23+
"debug": "^4.1.1",
1524
"globby": "^11.0.0",
1625
"inquirer": "^7.0.3",
1726
"jscodeshift": "^0.7.0",
18-
"vue-jscodeshift-adapter": "^2.0.3"
27+
"vue": "^3.0.0-alpha.12",
28+
"vue-sfc-descriptor-to-string": "^1.0.0",
29+
"yargs": "^15.3.1"
1930
},
2031
"devDependencies": {
2132
"@types/jest": "^24.0.25",
2233
"@types/jscodeshift": "^0.6.3",
2334
"@types/node": "^13.1.6",
35+
"@types/yargs": "^15.0.4",
2436
"jest": "^24.9.0",
37+
"prettier": "^2.0.4",
2538
"typescript": "^3.7.4"
39+
},
40+
"prettier": {
41+
"semi": false,
42+
"singleQuote": true
43+
},
44+
"engines": {
45+
"node": ">= 12.2.0"
2646
}
2747
}

src/VueTransformation.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default class VueTransformation {
2+
// TODO:
3+
}

src/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// TODO: Programmatic API
2+
3+
export { default as VueTransformation } from './VueTransformation'
4+
5+
export { default as runTransformation } from './run-transformation'

src/run-transformation.ts

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import jscodeshift, { Transform, Parser } from 'jscodeshift'
2+
// @ts-ignore
3+
import getParser from 'jscodeshift/dist/getParser'
4+
import { parse } from '@vue/compiler-sfc'
5+
import descriptorToString from 'vue-sfc-descriptor-to-string'
6+
import createDebug from 'debug'
7+
8+
import VueTransformation from './VueTransformation'
9+
10+
const debug = createDebug('vue-codemod')
11+
12+
type FileInfo = {
13+
path: string
14+
source: string
15+
}
16+
17+
type JSTransformation = Transform & {
18+
parser?: string | Parser
19+
}
20+
21+
export default function runTransformation(
22+
fileInfo: FileInfo,
23+
transformation: VueTransformation | JSTransformation,
24+
params: object = {}
25+
) {
26+
if (transformation instanceof VueTransformation) {
27+
debug('TODO: Running VueTransformation')
28+
return
29+
}
30+
31+
debug('Running jscodeshift transform')
32+
33+
let parser = getParser()
34+
if (transformation.parser) {
35+
parser =
36+
typeof transformation.parser === 'string'
37+
? getParser(transformation.parser)
38+
: transformation.parser
39+
}
40+
const j = jscodeshift.withParser(parser)
41+
const api = {
42+
j,
43+
jscodeshift: j,
44+
stats: () => {},
45+
report: () => {},
46+
}
47+
48+
const { path, source } = fileInfo
49+
const extension = (/\.([^.]*)$/.exec(path) || [])[0]
50+
51+
if (extension !== '.vue') {
52+
const out = transformation(fileInfo, api, params)
53+
if (!out) {
54+
return source // skipped
55+
}
56+
return out
57+
}
58+
59+
const { descriptor } = parse(source, { filename: path })
60+
if (descriptor.script) {
61+
fileInfo.source = descriptor.script.content
62+
const out = transformation(fileInfo, api, params)
63+
64+
if (!out || out === descriptor.script.content) {
65+
return source // skipped
66+
}
67+
descriptor.script.content = out
68+
// TODO: preserve indentation
69+
return descriptorToString(descriptor, {
70+
indents: {
71+
template: 0,
72+
},
73+
})
74+
}
75+
}

src/typings.d.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
declare module 'vue-sfc-descriptor-to-string' {
2+
import { SFCDescriptor } from '@vue/compiler-sfc'
3+
4+
interface Options {
5+
indents?: {
6+
template?: number
7+
script?: number
8+
style?: number
9+
}
10+
}
11+
12+
function toString(sfcDescriptor: SFCDescriptor, options?: Options): string
13+
14+
export = toString
15+
}

transforms/rfc-0009/index.ts transformations/create-app-mount/index.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { Transform } from "jscodeshift"
2-
import adapt from '../../utils/adapter'
1+
import { Transform } from 'jscodeshift'
32

43
import removeProductionTip from './remove-production-tip'
54
import transformMount from './transformMount'
@@ -11,8 +10,8 @@ const transform: Transform = (file, api) => {
1110
// add a `createApp` import
1211
const vueImportExpr = root.find(j.ImportDeclaration, {
1312
source: {
14-
value: 'vue'
15-
}
13+
value: 'vue',
14+
},
1615
})
1716
vueImportExpr.forEach(({ node }) => {
1817
node.specifiers.push(j.importSpecifier(j.identifier('createApp')))
@@ -30,4 +29,4 @@ const transform: Transform = (file, api) => {
3029
return root.toSource({ lineTerminator: '\n' })
3130
}
3231

33-
export default adapt(transform)
32+
export default transform

transformations/index.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import createAppMount from './create-app-mount'
2+
3+
const transformationMap : {
4+
[name: string]: Function
5+
} = {
6+
'create-app-mount': createAppMount,
7+
// TODO:
8+
}
9+
10+
export default transformationMap

transforms/new-vue-to-create-app.ts

-1
This file was deleted.

0 commit comments

Comments
 (0)