Skip to content

Commit 538c71e

Browse files
committed
feat: implement VueTransformation
1 parent 6965519 commit 538c71e

File tree

5 files changed

+229
-52
lines changed

5 files changed

+229
-52
lines changed

bin/vue-codemod.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as globby from 'globby'
1010
import createDebug from 'debug'
1111

1212
import builtInTransformations from '../transformations'
13+
import vueTransformations from '../vue-transformations'
1314
import runTransformation from '../src/runTransformation'
1415

1516
const debug = createDebug('vue-codemod')
@@ -61,9 +62,13 @@ main().catch((err) => {
6162
})
6263

6364
function loadTransformationModule(nameOrPath: string) {
64-
let transformation = builtInTransformations[nameOrPath]
65-
if (transformation) {
66-
return transformation
65+
let jsTransformation = builtInTransformations[nameOrPath]
66+
let vueTransformation = vueTransformations[nameOrPath]
67+
if (jsTransformation) {
68+
return jsTransformation
69+
}
70+
if (vueTransformations) {
71+
return vueTransformation
6772
}
6873

6974
const customModulePath = path.resolve(process.cwd(), nameOrPath)

src/VueTransformation.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1-
export default abstract class VueTransformation {
2-
// TODO:
1+
interface FileInfo {
2+
/** The absolute path to the current file. */
3+
path: string;
4+
/** The source code of the current file. */
5+
source: string;
6+
}
7+
8+
interface Options {
9+
[option: string]: any;
10+
}
11+
12+
export default interface VueTransformation {
13+
(file: FileInfo, options: Options): string | null | undefined | void;
14+
type: string
315
}

src/runTransformation.ts

Lines changed: 53 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type FileInfo = {
1616
}
1717

1818
type JSTransformation = Transform & {
19+
type: 'JSTransformation'
1920
parser?: string | Parser
2021
}
2122

@@ -29,7 +30,7 @@ type JSTransformationModule =
2930
type VueTransformationModule =
3031
| VueTransformation
3132
| {
32-
default: VueTransformation
33+
default: VueTransformation,
3334
}
3435

3536
type TransformationModule = JSTransformationModule | VueTransformationModule
@@ -45,69 +46,74 @@ export default function runTransformation(
4546
// @ts-ignore
4647
transformation = transformationModule.default
4748
} else {
49+
// @ts-ignore
4850
transformation = transformationModule
4951
}
5052

51-
if (transformation instanceof VueTransformation) {
53+
if (transformation.type === 'vueTransformation') {
5254
debug('TODO: Running VueTransformation')
53-
return fileInfo.source
54-
}
5555

56-
debug('Running jscodeshift transform')
56+
const out = transformation(fileInfo, params)
57+
return out
58+
} else {
59+
debug('Running jscodeshift transform')
60+
61+
const { path, source } = fileInfo
62+
const extension = (/\.([^.]*)$/.exec(path) || [])[0]
63+
let lang = extension.slice(1)
5764

58-
const { path, source } = fileInfo
59-
const extension = (/\.([^.]*)$/.exec(path) || [])[0]
60-
let lang = extension.slice(1)
65+
let descriptor: SFCDescriptor
66+
if (extension === '.vue') {
67+
descriptor = parseSFC(source, { filename: path }).descriptor
6168

62-
let descriptor: SFCDescriptor
63-
if (extension === '.vue') {
64-
descriptor = parseSFC(source, { filename: path }).descriptor
69+
// skip .vue files without script block
70+
if (!descriptor.script) {
71+
return source
72+
}
6573

66-
// skip .vue files without script block
67-
if (!descriptor.script) {
68-
return source
74+
lang = descriptor.script.lang || 'js'
75+
fileInfo.source = descriptor.script.content
6976
}
7077

71-
lang = descriptor.script.lang || 'js'
72-
fileInfo.source = descriptor.script.content
73-
}
78+
let parser = getParser()
79+
let parserOption = (transformationModule as JSTransformationModule).parser
80+
// force inject `parser` option for .tsx? files, unless the module specifies a custom implementation
81+
if (typeof parserOption !== 'object') {
82+
if (lang.startsWith('ts')) {
83+
parserOption = lang
84+
}
85+
}
7486

75-
let parser = getParser()
76-
let parserOption = (transformationModule as JSTransformationModule).parser
77-
// force inject `parser` option for .tsx? files, unless the module specifies a custom implementation
78-
if (typeof parserOption !== 'object') {
79-
if (lang.startsWith('ts')) {
80-
parserOption = lang
87+
if (parserOption) {
88+
parser =
89+
typeof parserOption === 'string'
90+
? getParser(parserOption)
91+
: parserOption
8192
}
82-
}
8393

84-
if (parserOption) {
85-
parser =
86-
typeof parserOption === 'string' ? getParser(parserOption) : parserOption
87-
}
94+
const j = jscodeshift.withParser(parser)
95+
const api = {
96+
j,
97+
jscodeshift: j,
98+
stats: () => {},
99+
report: () => {},
100+
}
88101

89-
const j = jscodeshift.withParser(parser)
90-
const api = {
91-
j,
92-
jscodeshift: j,
93-
stats: () => {},
94-
report: () => {},
95-
}
102+
const out = transformation(fileInfo, api, params)
103+
if (!out) {
104+
return source // skipped
105+
}
96106

97-
const out = transformation(fileInfo, api, params)
98-
if (!out) {
99-
return source // skipped
100-
}
107+
// need to reconstruct the .vue file from descriptor blocks
108+
if (extension === '.vue') {
109+
if (out === descriptor!.script!.content) {
110+
return source // skipped, don't bother re-stringifying
111+
}
101112

102-
// need to reconstruct the .vue file from descriptor blocks
103-
if (extension === '.vue') {
104-
if (out === descriptor!.script!.content) {
105-
return source // skipped, don't bother re-stringifying
113+
descriptor!.script!.content = out
114+
return stringifySFC(descriptor!)
106115
}
107116

108-
descriptor!.script!.content = out
109-
return stringifySFC(descriptor!)
117+
return out
110118
}
111-
112-
return out
113119
}

src/wrapVueTransformation.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import * as _ from 'lodash'
2+
import type { Operation } from './operationUtils'
3+
import type VueTransformation from './VueTransformation'
4+
5+
const BOM = '\uFEFF'
6+
7+
type FileInfo = {
8+
path: string
9+
source: string
10+
}
11+
12+
export type Context = {
13+
file: FileInfo,
14+
}
15+
16+
export type VueASTTransformation<Params = void> = {
17+
(context: Context, params: Params): Operation[],
18+
type?: string
19+
}
20+
21+
export default function astTransformationToVueTransformationModule<
22+
Params = any
23+
>(transformAST: VueASTTransformation<Params>): VueTransformation {
24+
const transform: VueTransformation = (file, options: Params) => {
25+
const source = file.source
26+
const fixOperations: Operation[] = transformAST({ file }, options)
27+
28+
return applyOperation(source, fixOperations)
29+
}
30+
31+
transform.type = "vueTransformation"
32+
33+
return transform
34+
}
35+
36+
/**
37+
* Modify source files
38+
* @param sourceCode File's source code
39+
* @param tempOperations Modify the object
40+
*/
41+
export function applyOperation(sourceCode: string, tempOperations: Operation[]) {
42+
// clone the array
43+
const bom = sourceCode.startsWith(BOM) ? BOM : "",
44+
text: string = bom ? sourceCode.slice(1) : sourceCode;
45+
let lastPos: number = Number.MIN_VALUE,
46+
output: string = bom;
47+
48+
let applyOperations: Operation[] = [];
49+
50+
// The Lodash grouping function is called to group the objects in the array according to range
51+
let tempOperation: Operation | null = mergeOperations(tempOperations, text)
52+
if (tempOperation) {
53+
applyOperations.push(tempOperation)
54+
}
55+
56+
for (const operation of applyOperations.sort(compareOperationsByRange)) {
57+
attemptOperation(operation);
58+
}
59+
60+
// all fix were recovered.
61+
output += text.slice(Math.max(0, lastPos));
62+
63+
return output;
64+
65+
/**
66+
* Try to use the 'operation' from a problem.
67+
* @param {Message} problem The message object to apply operations from
68+
* @returns {boolean} Whether operation was successfully applied
69+
*/
70+
function attemptOperation(operation: Operation) {
71+
const start = operation.range[0];
72+
const end = operation.range[1];
73+
// Remain it as a problem if it's overlapped or it's a negative range
74+
if (lastPos >= start || start > end) {
75+
return false;
76+
}
77+
78+
// Remove BOM.
79+
if (
80+
(start < 0 && end >= 0) ||
81+
(start === 0 && operation.text.startsWith(BOM))
82+
) {
83+
output = "";
84+
}
85+
86+
// Make output to this operation.
87+
output += text.slice(Math.max(0, lastPos), Math.max(0, start));
88+
output += operation.text;
89+
lastPos = end;
90+
return true;
91+
}
92+
}
93+
94+
/**
95+
* Merges the given operations array into one.
96+
* @param {Operation[]} operations The operations to merge.
97+
* @param {SourceCode} sourceCode The source code object to get the text between operations.
98+
* @returns {{text: string, range: number[]}} The merged operations
99+
*/
100+
function mergeOperations(operations: Operation[], sourceCode: String): Operation | null {
101+
if (operations.length === 0) {
102+
return null;
103+
}
104+
if (operations.length === 1) {
105+
return operations[0];
106+
}
107+
108+
operations.sort(compareOperationsByRange);
109+
110+
const originalText = sourceCode;
111+
const start = operations[0].range[0];
112+
const end = operations[operations.length - 1].range[1];
113+
let text: string = "";
114+
let lastPos: number = Number.MIN_SAFE_INTEGER;
115+
116+
for (const operation of operations) {
117+
if (operation.range[0] < lastPos) {
118+
continue;
119+
}
120+
121+
if (operation.range[0] >= 0) {
122+
text += originalText.slice(Math.max(0, start, lastPos), operation.range[0]);
123+
}
124+
text += operation.text;
125+
lastPos = operation.range[1];
126+
}
127+
text += originalText.slice(Math.max(0, start, lastPos), end);
128+
return { range: [start, end], text } as Operation;
129+
}
130+
131+
/**
132+
* Compares items in a operations array by range.
133+
* @param {Operation} a The first message.
134+
* @param {Operation} b The second message.
135+
* @returns {int} -1 if a comes before b, 1 if a comes after b, 0 if equal.
136+
* @private
137+
*/
138+
function compareOperationsByRange(a: Operation, b: Operation): number {
139+
return a.range[0] - b.range[0] || a.range[1] - b.range[1];
140+
}

vue-transformations/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type VueTransformation from '../src/VueTransformation'
2+
3+
type VueTransformationModule = {
4+
default: VueTransformation
5+
}
6+
7+
const transformationMap: {
8+
[name: string]: VueTransformationModule
9+
} = {
10+
'slot-attribute': require('./slot-attribute'),
11+
'slot-default': require('./slot-default'),
12+
}
13+
14+
export default transformationMap

0 commit comments

Comments
 (0)