Skip to content

Commit be62356

Browse files
alexzhang1030antfu
andauthored
feat: add to-string-literal and to-template-literal commands (#2)
Co-authored-by: Anthony Fu <[email protected]>
1 parent 3e46b8e commit be62356

File tree

11 files changed

+505
-15
lines changed

11 files changed

+505
-15
lines changed

README.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,86 @@ Will be converted to:
199199
const { default: bar, foo } = await import('./foo')
200200
```
201201

202+
### `to-string-literal`
203+
204+
Convert template literals to string literals.
205+
206+
Triggers:
207+
- `/// to-string-literal`
208+
- `/// to-sl`
209+
- `/// 2string-literal`
210+
- `/// 2sl`
211+
212+
or if you fancy `@`:
213+
214+
- `// @to-string-literal`
215+
- `// @to-sl`
216+
- `// @2string-literal`
217+
- `// @2sl`
218+
219+
```js
220+
/// @2sl
221+
const foo = `foo`
222+
223+
// @2sl
224+
const quxx = `${qux}quxx`
225+
226+
// Also supports using numbers to specify which items need to be converted (starts from 1)
227+
// @2sl 1 3
228+
const bar = `bar`; const baz = `baz`; const qux = `qux`
229+
```
230+
231+
Will be converted to:
232+
233+
```js
234+
const foo = 'bar'
235+
236+
// eslint-disable-next-line prefer-template
237+
const quxx = qux + 'quxx'
238+
239+
const bar = 'bar'; const baz = `baz`; const qux = 'qux'
240+
```
241+
242+
### `to-template-literal`
243+
244+
Convert string literals to template literals.
245+
246+
Triggers:
247+
- `/// to-template-literal`
248+
- `/// to-tl`
249+
- `/// 2template-literal`
250+
- `/// 2tl`
251+
252+
or if you fancy `@`:
253+
254+
- `// @to-template-literal`
255+
- `// @to-tl`
256+
- `// @2template-literal`
257+
- `// @2tl`
258+
259+
```js
260+
/// @2tl
261+
const bar = 'bar'
262+
263+
// @2tl
264+
// eslint-disable-next-line prefer-template
265+
const quxx = qux + 'quxx'
266+
267+
// Also supports using numbers to specify which items need to be converted (starts from 1)
268+
// @2tl 1 3
269+
const foo = 'foo'; const baz = 'baz'; const qux = 'qux'
270+
```
271+
272+
Will be converted to:
273+
274+
```js
275+
const bar = `bar`
276+
277+
const quxx = `${qux}quxx`
278+
279+
const foo = `foo`; const baz = 'baz'; const qux = `qux`
280+
```
281+
202282
## Custom Commands
203283

204284
It's also possible to define your custom commands.

eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default antfu(
1212
rules: {
1313
'command/command': 'off',
1414
'antfu/top-level-function': 'off',
15+
'style/max-statements-per-line': 'off',
1516
},
1617
},
1718
{

src/commands/_utils.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { Tree } from '../types'
2+
3+
export function getNodesByIndexes<T>(nodes: T[], indexes: number[]) {
4+
return indexes.length
5+
? indexes.map(n => nodes[n]).filter(Boolean)
6+
: nodes
7+
}
8+
9+
/**
10+
*
11+
* @param value Accepts a string of numbers separated by spaces
12+
* @param integer If true, only positive integers are returned
13+
*/
14+
export function parseToNumberArray(value: string | undefined, integer = false) {
15+
return value?.split(' ')
16+
.map(Number)
17+
.filter(n =>
18+
!Number.isNaN(n)
19+
&& integer
20+
? (Number.isInteger(n) && n > 0)
21+
: true,
22+
) ?? []
23+
}
24+
25+
export function insideRange(node: Tree.Node, range: [number, number], includeStart = true, includeEnd = true) {
26+
return (
27+
(includeStart ? node.range[0] >= range[0] : node.range[0] > range[0])
28+
&& (includeEnd ? node.range[1] <= range[1] : node.range[1] < range[1])
29+
)
30+
}

src/commands/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { keepSorted } from './keep-sorted'
44
import { toForEach } from './to-for-each'
55
import { toForOf } from './to-for-of'
66
import { toDynamicImport } from './to-dynamic-import'
7+
import { toStringLiteral } from './to-string-literal'
8+
import { toTemplateLiteral } from './to-template-literal'
79

810
// @keep-sorted
911
export {
@@ -13,6 +15,8 @@ export {
1315
toForEach,
1416
toForOf,
1517
toFunction,
18+
toStringLiteral,
19+
toTemplateLiteral,
1620
}
1721

1822
// @keep-sorted
@@ -23,4 +27,6 @@ export const builtinCommands = [
2327
toForEach,
2428
toForOf,
2529
toFunction,
30+
toStringLiteral,
31+
toTemplateLiteral,
2632
]
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { toStringLiteral as command } from './to-string-literal'
2+
import { d, run } from './_test-utils'
3+
4+
run(
5+
command,
6+
{
7+
code: d`
8+
// @2sl
9+
const a = \`a\`; const b = \`b\`; const c = 'c';
10+
`,
11+
output: d`
12+
const a = 'a'; const b = 'b'; const c = 'c';
13+
`,
14+
errors: ['command-removal', 'command-fix', 'command-fix'],
15+
},
16+
// You can specify which one to convert
17+
{
18+
code: d`
19+
// @2sl 2 3
20+
const a = \`a\`, b = \`b\`, c = \`c\`, d = \`d\`;
21+
`,
22+
output: d`
23+
const a = \`a\`, b = 'b', c = 'c', d = \`d\`;
24+
`,
25+
errors: ['command-removal', 'command-fix', 'command-fix'],
26+
},
27+
// mixed
28+
{
29+
code: d`
30+
// @2sl 1 3
31+
const a = 'a', b = 'b', c = \`c\`, d = 'd', e = \`e\`, f = \`f\`;
32+
`,
33+
output: d`
34+
const a = 'a', b = 'b', c = 'c', d = 'd', e = \`e\`, f = 'f';
35+
`,
36+
errors: ['command-removal', 'command-fix', 'command-fix'],
37+
},
38+
// `a${b}d` -> `'a' + b + 'd'`
39+
{
40+
code: d`
41+
// @2sl
42+
const a = \`\${g}a\${a}a\${b}c\${d}e\${a}\`;
43+
`,
44+
output: d`
45+
const a = g + 'a' + a + 'a' + b + 'c' + d + 'e' + a;
46+
`,
47+
errors: ['command-removal', 'command-fix'],
48+
},
49+
)

src/commands/to-string-literal.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import type { Command, Tree } from '../types'
2+
import { getNodesByIndexes, parseToNumberArray } from './_utils'
3+
4+
export const toStringLiteral: Command = {
5+
name: 'to-string-literal',
6+
match: /^[\/:@]\s*(?:to-|2)?(?:string-literal|sl)\s{0,}(.*)?$/,
7+
action(ctx) {
8+
const numbers = ctx.matches[1]
9+
// From integers 1-based to 0-based to match array indexes
10+
const indexes = parseToNumberArray(numbers, true).map(n => n - 1)
11+
const nodes = ctx.findNodeBelow({
12+
types: ['TemplateLiteral'],
13+
shallow: true,
14+
findAll: true,
15+
})
16+
if (!nodes?.length)
17+
return ctx.reportError('No template literals found')
18+
19+
ctx.removeComment()
20+
for (const node of getNodesByIndexes(nodes, indexes)) {
21+
const ids = extractIdentifiers(node)
22+
let raw = ctx.source.getText(node).slice(1, -1)
23+
24+
if (ids.length)
25+
raw = toStringWithIds(raw, node, ids)
26+
else
27+
raw = `'${raw}'`
28+
29+
ctx.report({
30+
node,
31+
message: 'Convert to string literal',
32+
fix(fixer) {
33+
return fixer.replaceTextRange(node.range, raw)
34+
},
35+
})
36+
}
37+
},
38+
}
39+
40+
interface Identifier {
41+
name: string
42+
range: [number, number]
43+
}
44+
45+
function extractIdentifiers(node: Tree.TemplateLiteral) {
46+
const ids: Identifier[] = []
47+
for (const child of node.expressions) {
48+
if (child.type === 'Identifier')
49+
ids.push({ name: child.name, range: child.range })
50+
// TODO: sub expressions, e.g. `${a + b}` -> '' + a + b + ''
51+
}
52+
return ids
53+
}
54+
55+
function toStringWithIds(raw: string, node: Tree.TemplateLiteral, ids: Identifier[]) {
56+
let hasStart = false
57+
let hasEnd = false
58+
ids.forEach(({ name, range }, index) => {
59+
let startStr = `' + `
60+
let endStr = ` + '`
61+
62+
if (index === 0) {
63+
hasStart = range[0] - /* `${ */3 === node.range[0]
64+
if (hasStart)
65+
startStr = ''
66+
}
67+
if (index === ids.length - 1) {
68+
hasEnd = range[1] + /* }` */2 === node.range[1]
69+
if (hasEnd)
70+
endStr = ''
71+
}
72+
73+
raw = raw.replace(`\${${name}}`, `${startStr}${name}${endStr}`)
74+
})
75+
return `${hasStart ? '' : `'`}${raw}${hasEnd ? '' : `'`}`
76+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { toTemplateLiteral as command } from './to-template-literal'
2+
import { d, run } from './_test-utils'
3+
4+
run(
5+
command,
6+
{
7+
code: d`
8+
// @2tl
9+
const a = \`a\${a}\`, b = \`b\`, c = "c", d = 2;
10+
`,
11+
output: d`
12+
const a = \`a\${a}\`, b = \`b\`, c = \`c\`, d = 2;
13+
`,
14+
errors: ['command-removal', 'command-fix'],
15+
},
16+
// You can specify which one to convert
17+
{
18+
code: d`
19+
// @2tl 1 4
20+
const a = 'a', b = 'b', c = 'c', d = 'd';
21+
`,
22+
output: d`
23+
const a = \`a\`, b = 'b', c = 'c', d = \`d\`;
24+
`,
25+
errors: ['command-removal', 'command-fix', 'command-fix'],
26+
},
27+
// mixed
28+
{
29+
code: d`
30+
// @2tl 1 3
31+
const a = \`a\`; const b = \`b\`; const c = 'c'; const d = \`d\`; const e = 'e'; const f = 'f';
32+
`,
33+
output: d`
34+
const a = \`a\`; const b = \`b\`; const c = \`c\`; const d = \`d\`; const e = 'e'; const f = \`f\`;
35+
`,
36+
errors: ['command-removal', 'command-fix', 'command-fix'],
37+
},
38+
// 'a' + b + 'c' -> `a${b}c`
39+
{
40+
code: d`
41+
// @2tl
42+
const a = 'a' + b + 'c';
43+
`,
44+
output: d`
45+
const a = \`a\${b}c\`;
46+
`,
47+
errors: ['command-removal', 'command-fix'],
48+
},
49+
{
50+
code: d`
51+
// @2tl
52+
const b = b + 'c' + d + 'e' + f + z + 'g' + h + 'i' + j;
53+
`,
54+
output: d`
55+
const b = \`\${b}c\${d}e\${f}\${z}g\${h}i\${j}\`;
56+
`,
57+
errors: ['command-removal', 'command-fix'],
58+
},
59+
{
60+
code: d`
61+
// @2tl
62+
const a = a + b + c;
63+
`,
64+
output: d`
65+
const a = \`\${a}\${b}\${c}\`;
66+
`,
67+
errors: ['command-removal', 'command-fix'],
68+
},
69+
{
70+
code: d`
71+
// @2tl 2 4
72+
const a = a + b; const d = d + 'e'; const c = '3'; const d = '4';
73+
`,
74+
output: d`
75+
const a = a + b; const d = \`\${d}e\`; const c = '3'; const d = \`4\`;
76+
`,
77+
errors: ['command-removal', 'command-fix', 'command-fix'],
78+
},
79+
{
80+
code: d`
81+
// @2tl 1 2
82+
const a = '4' + b; const d = d + 'e'; const c = '3'; const d = '4';
83+
`,
84+
output: d`
85+
const a = \`4\${b}\`; const d = \`\${d}e\`; const c = '3'; const d = '4';
86+
`,
87+
errors: ['command-removal', 'command-fix', 'command-fix'],
88+
},
89+
)

0 commit comments

Comments
 (0)