Skip to content

Commit 50594e9

Browse files
committed
Enhance JSX file shrinking logic to better support useEffect blocks and tags, and update error message formatting in dictionary
- Should preserve component returns. - [Fragment] has incorrect wrapping - remove class names and other attributes from jsx [Fragment] - should remove useEffect completely - should strip assignments to variables, eg. [Fragment] -> const [count, setCount] - strip type assignments to interfaces and types e.g. [Fragment] -> interface Props { title }
1 parent d608128 commit 50594e9

4 files changed

Lines changed: 224 additions & 37 deletions

File tree

apps/editor/src/context/utils/shrink-file/languages/jsx.ts

Lines changed: 198 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,27 @@ export const shrink_jsx = (content: string): string => {
22
const lines = content.split(/\r?\n/)
33
const result: string[] = []
44
let is_in_block_comment = false
5-
let skip_body_depth = 0
5+
const block_stack: { is_skipped: boolean; is_type_decl?: boolean }[] = []
66
let last_code_buffer = ''
7+
let is_in_string: false | '"' | "'" | '`' = false
8+
let jsx_tag_state: 'none' | 'name' | 'attributes' = 'none'
9+
let jsx_attribute_brace_depth = 0
10+
let global_paren_depth = 0
11+
let use_effect_start_depth = -1
712

813
for (const line of lines) {
914
let processed_line = ''
1015
let i = 0
11-
let is_in_string: false | '"' | "'" | '`' = false
1216

1317
while (i < line.length) {
1418
const char = line[i]
1519
const next_char = line[i + 1]
1620

21+
const current_skipped =
22+
block_stack.length > 0
23+
? block_stack[block_stack.length - 1].is_skipped
24+
: false
25+
1726
if (is_in_block_comment) {
1827
if (char == '*' && next_char == '/') {
1928
is_in_block_comment = false
@@ -26,7 +35,11 @@ export const shrink_jsx = (content: string): string => {
2635

2736
if (is_in_string) {
2837
if (char == '\\') {
29-
if (skip_body_depth == 0) {
38+
if (
39+
jsx_tag_state !== 'attributes' &&
40+
!current_skipped &&
41+
use_effect_start_depth === -1
42+
) {
3043
processed_line += char + (next_char || '')
3144
}
3245
last_code_buffer += char + (next_char || '')
@@ -36,7 +49,11 @@ export const shrink_jsx = (content: string): string => {
3649
if (char === is_in_string) {
3750
is_in_string = false
3851
}
39-
if (skip_body_depth == 0) {
52+
if (
53+
jsx_tag_state !== 'attributes' &&
54+
!current_skipped &&
55+
use_effect_start_depth === -1
56+
) {
4057
processed_line += char
4158
}
4259
last_code_buffer += char
@@ -46,7 +63,11 @@ export const shrink_jsx = (content: string): string => {
4663

4764
if (char == '"' || char === "'" || char == '`') {
4865
is_in_string = char
49-
if (skip_body_depth == 0) {
66+
if (
67+
jsx_tag_state !== 'attributes' &&
68+
!current_skipped &&
69+
use_effect_start_depth === -1
70+
) {
5071
processed_line += char
5172
}
5273
last_code_buffer += char
@@ -64,41 +85,163 @@ export const shrink_jsx = (content: string): string => {
6485
continue
6586
}
6687

67-
if (char == '{') {
68-
if (skip_body_depth > 0) {
69-
skip_body_depth++
88+
if (jsx_tag_state === 'attributes') {
89+
if (char === '{') {
90+
jsx_attribute_brace_depth++
91+
} else if (char === '}') {
92+
if (jsx_attribute_brace_depth > 0) {
93+
jsx_attribute_brace_depth--
94+
}
95+
} else if (
96+
char === '/' &&
97+
next_char === '>' &&
98+
jsx_attribute_brace_depth === 0
99+
) {
100+
jsx_tag_state = 'none'
101+
if (!current_skipped && use_effect_start_depth === -1) {
102+
processed_line += '/>'
103+
}
104+
last_code_buffer += '/>'
105+
i += 2
106+
continue
107+
} else if (char === '>' && jsx_attribute_brace_depth === 0) {
108+
jsx_tag_state = 'none'
109+
if (!current_skipped && use_effect_start_depth === -1) {
110+
processed_line += '>'
111+
}
112+
last_code_buffer += '>'
113+
i++
114+
continue
115+
}
116+
last_code_buffer += char
117+
i++
118+
continue
119+
}
120+
121+
if (char === '<' && next_char && /[a-zA-Z]/.test(next_char)) {
122+
const trimmed_buffer = last_code_buffer.trimEnd()
123+
const last_non_space = trimmed_buffer.slice(-1)
124+
const is_jsx_context =
125+
['(', '=', ':', '>', '?', '&', '|', ',', '{', '['].includes(
126+
last_non_space
127+
) || /(^|\W)(return|yield|await|default)$/.test(trimmed_buffer)
128+
129+
if (is_jsx_context) {
130+
jsx_tag_state = 'name'
131+
}
132+
} else if (jsx_tag_state === 'name') {
133+
if (/\s/.test(char)) {
134+
jsx_tag_state = 'attributes'
135+
jsx_attribute_brace_depth = 0
70136
last_code_buffer += char
71137
i++
72138
continue
73-
} else {
74-
const is_keeper =
75-
/(^|\s)(class|interface|enum|namespace|type)(\s|$)/.test(
76-
last_code_buffer
77-
) || /(^|\s)(import|export|default)\s*$/.test(last_code_buffer)
78-
const is_object_literal = /(=|:|\()\s*$/.test(
79-
last_code_buffer.trimEnd()
80-
)
81-
82-
if (!is_keeper && !is_object_literal) {
83-
skip_body_depth = 1
84-
processed_line += `{}`
85-
last_code_buffer += '{}'
86-
i++
87-
continue
88-
} else {
89-
last_code_buffer = ''
90-
}
139+
} else if (char === '>') {
140+
jsx_tag_state = 'none'
141+
} else if (char === '/' && next_char === '>') {
142+
jsx_tag_state = 'none'
91143
}
92-
} else if (char == '}') {
93-
if (skip_body_depth > 0) {
94-
skip_body_depth--
144+
}
145+
146+
if (char === '(') {
147+
global_paren_depth++
148+
if (
149+
use_effect_start_depth === -1 &&
150+
/(^|\W)(React\.)?useEffect\s*$/.test(last_code_buffer)
151+
) {
152+
use_effect_start_depth = global_paren_depth
153+
processed_line = processed_line.replace(/(React\.)?useEffect\s*$/, '')
95154
last_code_buffer += char
96155
i++
97156
continue
98157
}
158+
} else if (char === ')') {
159+
if (use_effect_start_depth === global_paren_depth) {
160+
use_effect_start_depth = -1
161+
global_paren_depth--
162+
last_code_buffer += char
163+
i++
164+
continue
165+
}
166+
global_paren_depth--
167+
}
168+
169+
if (char == '{') {
170+
const is_keeper =
171+
/(^|\s)(class|interface|enum|namespace|type)(\s|$)/.test(
172+
last_code_buffer
173+
) || /(^|\s)(import|export|default)\s*$/.test(last_code_buffer)
174+
const is_object_literal = /(=|:|\()\s*$/.test(
175+
last_code_buffer.trimEnd()
176+
)
177+
178+
const last_statement = last_code_buffer.split(/[{;}]/).pop() || ''
179+
const is_type_decl = /(^|\s)(interface|type)\b/.test(last_statement)
180+
181+
let parent_skipped = current_skipped
182+
if (use_effect_start_depth !== -1) {
183+
parent_skipped = true
184+
}
185+
const should_skip = parent_skipped || (!is_keeper && !is_object_literal)
186+
187+
block_stack.push({ is_skipped: should_skip, is_type_decl })
188+
189+
if (!parent_skipped) {
190+
processed_line += '{'
191+
}
192+
193+
last_code_buffer += '{'
194+
i++
195+
continue
196+
} else if (char == '}') {
197+
let parent_skipped =
198+
block_stack.length > 1
199+
? block_stack[block_stack.length - 2].is_skipped
200+
: false
201+
if (use_effect_start_depth !== -1) {
202+
parent_skipped = true
203+
}
204+
205+
if (!parent_skipped) {
206+
processed_line += '}'
207+
}
208+
209+
if (block_stack.length > 0) {
210+
block_stack.pop()
211+
}
212+
last_code_buffer += '}'
213+
i++
214+
continue
215+
}
216+
217+
let active_skipped =
218+
block_stack.length > 0
219+
? block_stack[block_stack.length - 1].is_skipped
220+
: false
221+
let parent_skipped =
222+
block_stack.length > 1
223+
? block_stack[block_stack.length - 2].is_skipped
224+
: false
225+
226+
if (use_effect_start_depth !== -1) {
227+
active_skipped = true
228+
parent_skipped = true
229+
}
230+
231+
if (active_skipped && !parent_skipped && use_effect_start_depth === -1) {
232+
if (/(^|\W)return$/.test(last_code_buffer) && /\W/.test(char)) {
233+
block_stack[block_stack.length - 1].is_skipped = false
234+
const prefix =
235+
processed_line.length === 0 ? line.match(/^\s*/)?.[0] || '' : ''
236+
processed_line += prefix + 'return'
237+
}
99238
}
100239

101-
if (skip_body_depth == 0) {
240+
if (
241+
(block_stack.length === 0 ||
242+
!block_stack[block_stack.length - 1].is_skipped) &&
243+
use_effect_start_depth === -1
244+
) {
102245
processed_line += char
103246
}
104247
last_code_buffer += char
@@ -107,7 +250,31 @@ export const shrink_jsx = (content: string): string => {
107250

108251
const trimmed = processed_line.trim()
109252
if (trimmed) {
110-
result.push(processed_line.trimEnd().replace(/;+$/, ''))
253+
let final_line = processed_line.trimEnd().replace(/;+$/, '')
254+
255+
const ends_with_open_or_comma = /[[({,]$/.test(final_line)
256+
if (
257+
!ends_with_open_or_comma &&
258+
!final_line.includes('=>') &&
259+
/^\s*(?:export\s+)?(?:const|let|var)\s+/.test(final_line)
260+
) {
261+
const match = final_line.match(
262+
/^(\s*(?:export\s+)?(?:const|let|var)\s+(?:\[[^\]]+\]|\{[^}]+\}|[^\s=:]+)(?:\s*:[^=]+)?)\s*=(?!>).+$/
263+
)
264+
if (match) {
265+
final_line = match[1].trimEnd()
266+
}
267+
}
268+
269+
const in_type_decl = block_stack.some((b) => b.is_type_decl)
270+
if (in_type_decl && !ends_with_open_or_comma) {
271+
final_line = final_line.replace(
272+
/^(\s*['"]?[a-zA-Z0-9_?-]+['"]?)\s*:.*$/,
273+
'$1'
274+
)
275+
}
276+
277+
result.push(final_line)
111278
}
112279

113280
last_code_buffer += ' '

apps/editor/src/context/utils/shrink-file/test-cases/jsx/input.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ export const MyComponent: React.FC<Props> = ({ title }) => {
88
// This comment goes away
99
const [count, setCount] = React.useState(0);
1010

11+
useEffect(() => {
12+
// test
13+
const x = 'test';
14+
}, [])
15+
1116
return (
1217
<div className="container">
1318
{/* Block comment */}

apps/editor/src/context/utils/shrink-file/test-cases/jsx/output.txt

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
11
import React from 'react'
22
interface Props {
3-
title: string
3+
title
4+
}
5+
export const MyComponent: React.FC<Props> = ({ title }) => {
6+
const [count, setCount]
7+
return (
8+
<div>
9+
{}
10+
<h1>{}</h1>
11+
<button>
12+
Click me
13+
</button>
14+
</div>
15+
)
16+
}
17+
function Header() {
18+
return <header>Header</header>
419
}
5-
export const MyComponent: React.FC<Props> = ({ title }) => {}
6-
function Header() {}
720
class OldComponent extends React.Component {
8-
render() {}
21+
render() {
22+
return <div>Old</div>
23+
}
924
}
1025
import {
1126
useState,

packages/shared/src/constants/dictionary.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ export const dictionary = {
217217
ERROR_DURING_PROCESSING: (message: string) =>
218218
`An error occurred during processing: ${message}.`,
219219
ERROR_APPLYING_CHANGES: (message: string) =>
220-
`An error occurred while applying changes: ${message}.`,
220+
`An error occurred while applying changes (${message}).`,
221221
INVALID_FILE_PATH_TRAVERSAL: (file_path: string) =>
222222
`Invalid file path: ${file_path}. Path may contain traversal attempts.`,
223223
FAILED_TO_UNDO_CHANGES: (message: string) =>
@@ -369,6 +369,6 @@ export const dictionary = {
369369
'A configuration with these properties already exists.',
370370
CONFIGURATION_NOT_FOUND: 'Configuration not found.',
371371
APPLYING_CHANGES_GENERIC_ERROR: (msg: string) =>
372-
`An error occurred while applying changes: ${msg}.`
372+
`An error occurred while applying changes (${msg}).`
373373
}
374374
}

0 commit comments

Comments
 (0)