jsx_rosetta 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +342 -11
- data/CLAUDE.md +70 -0
- data/README.md +50 -0
- data/ROADMAP.md +92 -0
- data/agents/jsx-rosetta-resolve-todo-file.md +90 -0
- data/lib/jsx_rosetta/ast/inflector.rb +32 -0
- data/lib/jsx_rosetta/backend/phlex.rb +1421 -158
- data/lib/jsx_rosetta/backend/rails_view.rb +1 -1
- data/lib/jsx_rosetta/backend/view_component/expression_translator.rb +357 -33
- data/lib/jsx_rosetta/backend/view_component.rb +261 -31
- data/lib/jsx_rosetta/cli.rb +175 -37
- data/lib/jsx_rosetta/icons/lucide.json +37 -0
- data/lib/jsx_rosetta/icons.rb +44 -0
- data/lib/jsx_rosetta/ir/lowering.rb +1164 -70
- data/lib/jsx_rosetta/ir/module_shape_classifier.rb +20 -1
- data/lib/jsx_rosetta/ir/radix_registry.rb +84 -0
- data/lib/jsx_rosetta/ir/types.rb +264 -19
- data/lib/jsx_rosetta/ir.rb +5 -4
- data/lib/jsx_rosetta/pages_routing.rb +640 -0
- data/lib/jsx_rosetta/version.rb +1 -1
- data/lib/jsx_rosetta.rb +8 -6
- data/plans/nextjs_pages_to_rails.md +200 -0
- data/plans/nextjs_pages_to_rails_slice_2.md +118 -0
- data/plans/nextjs_pages_to_rails_slice_3.md +121 -0
- data/plans/nextjs_pages_to_rails_slice_4.md +301 -0
- data/plans/translator_widening_and_pages_followups.md +120 -0
- data/plans/translator_widening_slice_a.md +208 -0
- data/skills/jsx-rosetta-resolve-todos/SKILL.md +206 -0
- data/skills/jsx-rosetta-resolve-todos/data/design_tokens.template.yml +71 -0
- data/skills/jsx-rosetta-resolve-todos/data/target_app_conventions.template.yml +107 -0
- data/skills/jsx-rosetta-resolve-todos/examples/design_tokens.ant_design_v5.yml +190 -0
- data/skills/jsx-rosetta-resolve-todos/recipes/01_design_tokens.md +74 -0
- data/skills/jsx-rosetta-resolve-todos/recipes/02_promoted_ivar.md +49 -0
- data/skills/jsx-rosetta-resolve-todos/recipes/03_react_hooks.md +34 -0
- data/skills/jsx-rosetta-resolve-todos/recipes/04_apollo_hooks.md +34 -0
- data/skills/jsx-rosetta-resolve-todos/recipes/05_event_handlers.md +45 -0
- data/skills/jsx-rosetta-resolve-todos/recipes/06_module_constants.md +29 -0
- data/skills/jsx-rosetta-resolve-todos/recipes/07_nextjs_navigation.md +44 -0
- data/skills/jsx-rosetta-resolve-todos/recipes/08_generic_js_bailouts.md +55 -0
- data/skills/jsx-rosetta-resolve-todos/tools/apply_promoted_ivar.rb +189 -0
- data/skills/jsx-rosetta-resolve-todos/tools/apply_substitutions.rb +292 -0
- data/skills/jsx-rosetta-resolve-todos/tools/diff_corpus.rb +161 -0
- data/skills/jsx-rosetta-resolve-todos/tools/discover_bailouts.rb +211 -0
- metadata +30 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 161aa59c1e226755b166b5c7366d0c2b5bc1b3d1f55762b1c8c37f551392179a
|
|
4
|
+
data.tar.gz: 826dd0aff4042b710b354b79c0aa912587477c0f7b07892541fa0e1c7bed090b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ea022328a7f0cc56e87b08064a2aa4cb602254caf66c5f0f511faa295f8823867ba1fcfece9952d8d1fb064993736af21df8d060ef3b20ddd0ff8788d8f65d8c
|
|
7
|
+
data.tar.gz: fa4085f3972ec65ba2062bbed47f936fb0519d37220d6631fd8c427d03edfb2e20ecf3961fc9dd650f6b27d82db43bd2196a3d9e7b3ab44284d06a07b608efac
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,344 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
### Added — translate cva() variant builders into Ruby constants
|
|
6
|
+
|
|
7
|
+
- **`const fooVariants = cva(base, { variants, defaultVariants })`** —
|
|
8
|
+
the dominant variant-builder pattern in shadcn/ui-shaped components —
|
|
9
|
+
used to land as a 40-line TODO comment block above the class, and
|
|
10
|
+
its use-site `cn(fooVariants({ variant }), className)` emitted the
|
|
11
|
+
verbatim JS as a literal string into the `class:` attribute. Both
|
|
12
|
+
paths are now translated end-to-end.
|
|
13
|
+
- Lowering recognizes the `cva(<base>, { variants, defaultVariants,
|
|
14
|
+
compoundVariants })` call shape and records it as a new
|
|
15
|
+
`IR::CvaBinding` node (parallel to `IR::LocalBinding`).
|
|
16
|
+
- Phlex backend renders each `CvaBinding` as three module-level
|
|
17
|
+
Ruby constants alongside the class — `FOO_BASE_CLASS`,
|
|
18
|
+
`FOO_VARIANT_CLASSES` (a frozen hash of axis → option → class
|
|
19
|
+
string), and `FOO_DEFAULT_VARIANTS`.
|
|
20
|
+
- The use-site call `cn(fooVariants({ variant, size }), className)`
|
|
21
|
+
in a `className={...}` attribute now emits a Ruby string
|
|
22
|
+
interpolation against those constants:
|
|
23
|
+
`"#{FOO_BASE_CLASS} #{FOO_VARIANT_CLASSES["variant"][@variant]}
|
|
24
|
+
#{FOO_VARIANT_CLASSES["size"][@size]} #{@class_name}"`.
|
|
25
|
+
- `render_initializer` now uses `defaultVariants` as the Ruby kwarg
|
|
26
|
+
default for any prop name matching a cva axis (so `variant:
|
|
27
|
+
"default"` flows from the cva binding even though the React
|
|
28
|
+
function signature took the prop undefaulted).
|
|
29
|
+
- `compoundVariants` (the rule-of-rules cva feature) is not yet
|
|
30
|
+
translated — the verbatim JS source surfaces as a `# TODO:
|
|
31
|
+
compoundVariants from FOO aren't translated` comment alongside the
|
|
32
|
+
emitted constants so the reviewer can hand-port the rules.
|
|
33
|
+
- Other module-level constants (non-cva) still take the existing
|
|
34
|
+
"# TODO: module-level constants" pre-class comment path.
|
|
35
|
+
|
|
36
|
+
### Added — drop Slot.Root branch from polymorphic asChild tags
|
|
37
|
+
|
|
38
|
+
- **shadcn's `<Comp asChild>` no longer NameErrors at render.** Components
|
|
39
|
+
using `const Comp = asChild ? Slot : "div"` (or `Slot.Root`) routed
|
|
40
|
+
the truthy branch through Radix's `Slot`, which has no Ruby class on
|
|
41
|
+
the Phlex side. Lowering now detects this Slot-vs-tag conditional
|
|
42
|
+
(Slot rooted at a `radix-ui`/`@radix-ui/react-*` import) and emits
|
|
43
|
+
only the non-Slot branch — no conditional, no `as_child` kwarg,
|
|
44
|
+
just the underlying tag.
|
|
45
|
+
- New CLI flag `--keep-slot` and `JsxRosetta.translate(..., keep_slot:
|
|
46
|
+
true)` preserves the full polymorphic conditional for consumers
|
|
47
|
+
that shim `Components::Slot::Root` themselves.
|
|
48
|
+
- Detection respects the import source: a project-local `import
|
|
49
|
+
{ Slot } from "./my-slot"` is untouched.
|
|
50
|
+
|
|
51
|
+
### Added — map known Radix primitive tags to underlying HTML elements
|
|
52
|
+
|
|
53
|
+
- **`<SeparatorPrimitive.Root />`, `<LabelPrimitive.Root />`, etc.**
|
|
54
|
+
used to lower as `ComponentInvocation`s, producing
|
|
55
|
+
`render SeparatorPrimitive::Root.new(...)` — undefined constants,
|
|
56
|
+
NameError on render. Now: when the import source matches a Radix
|
|
57
|
+
package (`radix-ui`, `@radix-ui/react-*`) and the
|
|
58
|
+
`(LocalName, Member)` pair is in a small registry, lower as an
|
|
59
|
+
HTML Element with the underlying tag and any always-applied
|
|
60
|
+
attributes (`role`, `type`, etc.). Consumer attrs win on collision
|
|
61
|
+
with the registry defaults.
|
|
62
|
+
- Registry lives at `lib/jsx_rosetta/ir/radix_registry.rb`. Covers
|
|
63
|
+
Separator / Label / Avatar (Root/Image/Fallback) / Switch
|
|
64
|
+
(Root/Thumb) / Progress (Root/Indicator) / AspectRatio (Root) /
|
|
65
|
+
ScrollArea (Root/Viewport). Unknown primitives fall through to
|
|
66
|
+
the existing `ComponentInvocation` / TODO behavior.
|
|
67
|
+
- Works with both named-aliased imports
|
|
68
|
+
(`import { Separator as SeparatorPrimitive } from "radix-ui"`)
|
|
69
|
+
and namespace imports
|
|
70
|
+
(`import * as AvatarPrimitive from "@radix-ui/react-avatar"`).
|
|
71
|
+
|
|
72
|
+
### Added — auto-emit Lucide icon shims as a translation sidecar
|
|
73
|
+
|
|
74
|
+
- **`import { ChevronRight } from "lucide-react"` now lands a working
|
|
75
|
+
`ChevronRight` Phlex class.** Previously the translator carried the
|
|
76
|
+
React import through verbatim, so the generated component contained
|
|
77
|
+
`render ChevronRight.new(...)` referencing a non-existent Ruby class
|
|
78
|
+
— NameError at render time. The Phlex backend now detects Lucide
|
|
79
|
+
imports (`lucide-react`, `lucide`) that are actually used as JSX
|
|
80
|
+
component tags and emits two kinds of sidecar files:
|
|
81
|
+
- `lucide_icon.rb` — a shared `LucideIcon < Phlex::HTML` base.
|
|
82
|
+
- `<icon>.rb` — one file per referenced icon, defining a subclass
|
|
83
|
+
that renders the vendored SVG path data inline. One file per
|
|
84
|
+
icon means Zeitwerk autoloads each cleanly under the consumer's
|
|
85
|
+
`app/components/` (or wherever the output directory points).
|
|
86
|
+
- Vendored path data lives at `lib/jsx_rosetta/icons/lucide.json`
|
|
87
|
+
(the ~35 icons most commonly imported by shadcn/ui sources). Icons
|
|
88
|
+
not in the vendored set emit a class whose `inner_svg` is empty
|
|
89
|
+
with a TODO comment pointing at the lucide.json refresh path.
|
|
90
|
+
- Tolerates both canonical (`ChevronRight`) and legacy `*Icon`
|
|
91
|
+
(`ChevronRightIcon`) names — same path data either way.
|
|
92
|
+
- `--phlex-namespace=Components` wraps every sidecar in the same
|
|
93
|
+
module as the main translation, so the consumer's autoloader
|
|
94
|
+
config doesn't need a special case.
|
|
95
|
+
|
|
96
|
+
### Added — translate Stimulus controller bodies when safe
|
|
97
|
+
|
|
98
|
+
- **Paste JSX handler bodies into the generated `_controller.js`.**
|
|
99
|
+
Auto-generated Stimulus controllers used to leave the handler body
|
|
100
|
+
as a TODO comment with the verbatim source above an empty
|
|
101
|
+
`clickHandler(event) { // ... }` stub. For DOM-driven handlers (the
|
|
102
|
+
common shape in shadcn-style UI), the JSX body is *already valid JS*
|
|
103
|
+
— we now paste it directly into the method, using the original
|
|
104
|
+
arrow's parameter name so identifier references in the body still
|
|
105
|
+
resolve.
|
|
106
|
+
- `IR::StimulusMethod` gains a `params:` field — the original arrow
|
|
107
|
+
parameter names — used as the Stimulus method's parameter signature.
|
|
108
|
+
- New `safe_to_paste_handler?` heuristic on the Phlex backend bails
|
|
109
|
+
out (falls back to the previous TODO behavior) when the body
|
|
110
|
+
references React state setters (`setX(`), React hooks (`useX(`),
|
|
111
|
+
or is just an identifier-bound `// originally bound to: …` comment.
|
|
112
|
+
- Collision markers are preserved across the new path.
|
|
113
|
+
|
|
114
|
+
### Fixed — silent children loss on self-closing-with-spread tags
|
|
115
|
+
|
|
116
|
+
- **Auto-yield on blockless spread-children tags.** The shadcn idiom
|
|
117
|
+
`<tag {...props} />` (self-closing JSX whose rest-spread carries React
|
|
118
|
+
`children`) translated to a Phlex `tag(..., **(@props || {}))` call
|
|
119
|
+
with no block — so callers' `Component.new { ... }` blocks were
|
|
120
|
+
silently dropped at render. Now: when an Element or
|
|
121
|
+
ComponentInvocation has no explicit IR children, a `SpreadAttribute`,
|
|
122
|
+
and a non-void tag, emit `tag(...) do; yield if block_given?; end`.
|
|
123
|
+
The `block_given?` guard preserves the no-block use case. Void HTML
|
|
124
|
+
elements (input, img, br, …) still emit blockless. Explicit-children
|
|
125
|
+
paths and `RenderProp` flows are untouched.
|
|
126
|
+
|
|
127
|
+
## [0.5.1] - 2026-05-11
|
|
128
|
+
|
|
129
|
+
A correctness pass on the v0.5.0 Phlex output. A random-sample review
|
|
130
|
+
surfaced six classes of render-time NameError that `ruby -c` couldn't
|
|
131
|
+
catch (the file parses, but evaluation crashes on a bare reference).
|
|
132
|
+
Each one is now closed at the source — the file loads and at worst
|
|
133
|
+
renders empty where a TODO marker tells the reviewer what to fill in.
|
|
134
|
+
Stress test stays at 895/929 clean and 0/1240 syntax failures; lint
|
|
135
|
+
volume on the generated `.rb` files drops ~38% (5443 → 3373 offenses).
|
|
136
|
+
|
|
137
|
+
### Fixed — runtime NameErrors at render time
|
|
138
|
+
|
|
139
|
+
- **Destructure leaks downstream of an untranslatable init.** `const
|
|
140
|
+
{ fieldValue } = customField` is captured as a TODO, but every use
|
|
141
|
+
site (`fieldValue?.X`, `!fieldValue`, `fieldValue > 0`) used to leak
|
|
142
|
+
as a bare snake_case ref — file loaded, but `field_value&.__typename`
|
|
143
|
+
NameErrored on first render. The translator now bails the whole
|
|
144
|
+
expression when a known-local sits at a member-chain root, on the
|
|
145
|
+
operand side of unary, or on either side of binary. The caller emits
|
|
146
|
+
a `# TODO: translate condition: …` line and falls through to an
|
|
147
|
+
`if false` fallback so the file at least loads.
|
|
148
|
+
- **Inline arrow handlers on PascalCase components.** `const handleClick
|
|
149
|
+
= () => {}` attached to `<Button onClick={handleClick}>` no longer
|
|
150
|
+
leaks as bare `handle_click`. Unconsumed local-arrow names are unioned
|
|
151
|
+
into `Component#local_binding_names`, so use sites emit `on_click: nil`
|
|
152
|
+
instead of a reference to an undefined method.
|
|
153
|
+
- **CamelCase / snake_case rest-prop mismatch.** `**descriptionProps`
|
|
154
|
+
emitted `**descriptionProps` in the initializer signature but the body
|
|
155
|
+
read `**(@description_props || {})` — silently dropped. The rest-name
|
|
156
|
+
is now snake_cased on both sides. `IR::Prop` gains an `alias_name:`
|
|
157
|
+
field so renamed destructures (`"data-testid": dataTestId`) resolve
|
|
158
|
+
the alias to the prop's snake_case ivar at use sites.
|
|
159
|
+
- **Unitless numeric inline styles.** `style={{ marginBottom: 16 }}` used
|
|
160
|
+
to emit `style: 'margin-bottom: 16;'` — invalid CSS, silently ignored
|
|
161
|
+
by browsers. Numeric style values now get a `px` suffix for any
|
|
162
|
+
property not in React's `isUnitlessNumber` table (`zIndex`,
|
|
163
|
+
`lineHeight`, `opacity`, etc. stay bare).
|
|
164
|
+
- **Style declarations rooted at an unresolvable local.** `style={{
|
|
165
|
+
borderRadius: token.borderRadius }}` used to emit `"border-radius:
|
|
166
|
+
#{token.borderRadius};"` and NameError at render. The style
|
|
167
|
+
declaration now drops with a per-decl TODO when its value can't
|
|
168
|
+
translate.
|
|
169
|
+
- **Uppercase / SCREAMING_SNAKE imports in attribute values.** JSX
|
|
170
|
+
`<Foo bar={DEFAULT_PAGE_SIZE}>` used to emit `bar: default_page_size`
|
|
171
|
+
(NameError at the call site). Attribute-position interpolation now
|
|
172
|
+
drops with a TODO when any unresolved identifier starts with
|
|
173
|
+
uppercase — almost always an external import. Lowercase unresolved
|
|
174
|
+
identifiers still pass through unchanged (they may be Rails helpers
|
|
175
|
+
like `current_user`).
|
|
176
|
+
|
|
177
|
+
### Changed — rubocop output cleanup (5443 → 3373 offenses across 1240 .rb)
|
|
178
|
+
|
|
179
|
+
- **`Lint/MissingSuper`** — initializers emit `super()` to satisfy
|
|
180
|
+
Phlex 2.x's superclass.
|
|
181
|
+
- **`Style/Documentation`** — one-line class docstring above every
|
|
182
|
+
generated class (`# X — generated by jsx_rosetta from JSX. Review
|
|
183
|
+
before shipping.`).
|
|
184
|
+
- **`Style/StringLiterals`** — new helper `AST::Inflector.ruby_string_literal`
|
|
185
|
+
emits single-quoted strings when safe (no embedded single quote,
|
|
186
|
+
backslash, or control char). Non-ASCII like emojis stays in single
|
|
187
|
+
quotes. The translator also re-emits JS string literals (`"hi"` →
|
|
188
|
+
`'hi'`) when the same constraints hold.
|
|
189
|
+
- **`Style/NilComparison` / `Style/NonNilCheck`** — rewrite `x == nil`
|
|
190
|
+
and `x != nil` as `x.nil?` / `!x.nil?` at the binary-translation
|
|
191
|
+
layer.
|
|
192
|
+
- **`Lint/BinaryOperatorWithIdenticalOperands`** — dedupe `||` / `&&`
|
|
193
|
+
when both sides translate to the same Ruby. `value === null ||
|
|
194
|
+
value === undefined` collapses to `@value.nil?` instead of
|
|
195
|
+
`@value.nil? || @value.nil?`.
|
|
196
|
+
- **`Lint/EmptyInterpolation`** — bail on the whole template literal
|
|
197
|
+
when any interpolation segment fails translation or resolves to
|
|
198
|
+
literal `nil`. No more `"prefix-#{}-suffix"`.
|
|
199
|
+
- **`Lint/LiteralAsCondition` / `Lint/DuplicateElsifCondition`** —
|
|
200
|
+
emit a file-level `# rubocop:disable` directive (with a matching
|
|
201
|
+
enable at the bottom of the file) only when an intentional `if false`
|
|
202
|
+
fallback appears. The `# TODO: translate condition:` line above the
|
|
203
|
+
fallback already names the issue, so the cop's report would be
|
|
204
|
+
redundant noise.
|
|
205
|
+
- **`Style/IfInsideElse`** — refactor conditional rendering to chain
|
|
206
|
+
`if / elsif / elsif / else / end` instead of nested
|
|
207
|
+
`if / else / if / else / end`. Cascades into fewer
|
|
208
|
+
`Metrics/BlockNesting`, `Metrics/AbcSize`, and
|
|
209
|
+
`Lint/DuplicateElsifCondition` offenses too.
|
|
210
|
+
- **`Layout/TrailingWhitespace`** — post-process emitted output to
|
|
211
|
+
rstrip every line.
|
|
212
|
+
- **`Layout/FirstHashElementIndentation`** — force-inline object/array
|
|
213
|
+
literals inside `initialize` default values; the column mismatch
|
|
214
|
+
that triggered the cop only happens when the wrap kicks in there.
|
|
215
|
+
|
|
216
|
+
## [0.5.0] - 2026-05-11
|
|
217
|
+
|
|
218
|
+
Closes the four v0.5.0-candidate items from the v0.4.0 Phlex sample
|
|
219
|
+
review, plus the four larger features queued at the top of the
|
|
220
|
+
roadmap: Apollo + Next.js hook hint translation, class-component
|
|
221
|
+
support, AG-Grid column-descriptor module emission, and pretty-printing
|
|
222
|
+
for long object/array literals.
|
|
223
|
+
|
|
224
|
+
### Added — class-component support
|
|
225
|
+
|
|
226
|
+
- **`ClassDeclaration` → ViewComponent path.** Classes with a `render()`
|
|
227
|
+
method now lower as components instead of getting flagged with the
|
|
228
|
+
`:class_component` rejection. The `ExpressionTranslator` recognizes
|
|
229
|
+
`this.props.X` and translates to `@x` (plus a snake_case member chain
|
|
230
|
+
for `this.props.X.y.z`); `this.state.X` translates to `nil` since
|
|
231
|
+
there's no Rails-side equivalent without a backing data source. Other
|
|
232
|
+
class members (constructor, lifecycle hooks like
|
|
233
|
+
`componentDidCatch`/`getDerivedStateFromError`, custom event handlers)
|
|
234
|
+
get captured as LocalBinding-style TODO comments at the top of the
|
|
235
|
+
view template so the reviewer sees the verbatim sources. Props are
|
|
236
|
+
synthesized from direct `this.props.X` access AND from
|
|
237
|
+
`const { X } = this.props` destructure patterns — the generated
|
|
238
|
+
`initialize(...)` matches the original class's prop set.
|
|
239
|
+
Stress-test impact: the 4 class-component residuals (ErrorBoundary
|
|
240
|
+
and cousins) now translate cleanly.
|
|
241
|
+
|
|
242
|
+
### Added — AG-Grid column-descriptor module emission
|
|
243
|
+
|
|
244
|
+
- **Data-factory components.** `export const createColumns = (token,
|
|
245
|
+
sortedInfo) => [{...}, {...}]` now lowers as an `IR::Component` with
|
|
246
|
+
`mode: :data_factory`. Phlex emits a snake_case method that returns
|
|
247
|
+
the translated array — `def create_columns(token: nil, sorted_info: nil)`
|
|
248
|
+
— instead of `view_template`. JSX inside object properties extracts to
|
|
249
|
+
private methods on the class via the existing IR::Lambda path
|
|
250
|
+
(`render: method(:render_id_cell)`). ViewComponent backend emits a
|
|
251
|
+
plain Ruby class with the method and a TODO note (the .erb pair is
|
|
252
|
+
skipped — pure-data classes don't have a template). Multi-positional
|
|
253
|
+
Identifier params are now also supported by `lower_params` — previously
|
|
254
|
+
only single-arg React-style signatures lowered cleanly.
|
|
255
|
+
|
|
256
|
+
### Added — pretty-printing
|
|
257
|
+
|
|
258
|
+
- **Multi-line layout for long object/array literals.** When the single-
|
|
259
|
+
line rendering of an `IR::ObjectLiteral` or `IR::ArrayLiteral` exceeds
|
|
260
|
+
`LITERAL_INLINE_BUDGET` (80 chars), or contains a nested literal that
|
|
261
|
+
itself wrapped, the layout switches to one entry per line indented
|
|
262
|
+
two spaces past the parent line's indent. Closing bracket re-aligns
|
|
263
|
+
to the parent indent. Short literals (typical Select options, small
|
|
264
|
+
config objects) stay inline so non-AG-Grid output is unchanged. Helps
|
|
265
|
+
readability of the column-descriptor output from the new
|
|
266
|
+
data-factory path.
|
|
267
|
+
|
|
268
|
+
### Stress test outcome
|
|
269
|
+
|
|
270
|
+
- 929-file Phlex stress rerun: **895/929 clean translations**
|
|
271
|
+
(up from 887/929 in v0.4.0 — 8 additional files now translate via
|
|
272
|
+
the class-component and data-factory paths). **0/1240 emitted `.rb`
|
|
273
|
+
files fail `ruby -c`** (unchanged; 16 more files emitted vs v0.4.0).
|
|
274
|
+
221/929 files carry an Apollo TODO block (281 GraphQL operation
|
|
275
|
+
names captured); 105/929 carry a Next.js navigation-hook block.
|
|
276
|
+
- 385 specs, all green; rubocop clean.
|
|
277
|
+
|
|
278
|
+
### Added — framework hook hints
|
|
279
|
+
|
|
280
|
+
- **Apollo data-fetching hooks recognized.** `useQuery`, `useLazyQuery`,
|
|
281
|
+
`useMutation`, `useSubscription`, and `useApolloClient` are now
|
|
282
|
+
detected at lowering time. Each call lands in `Component#react_hooks`
|
|
283
|
+
tagged `library: :apollo`, with the GraphQL operation name extracted
|
|
284
|
+
from a bare-Identifier first argument (`useQuery(GET_USERS_QUERY, …)`
|
|
285
|
+
→ `operation: "GET_USERS_QUERY"`). Both backends emit a dedicated
|
|
286
|
+
Apollo TODO block above the template that points at the Rails analog
|
|
287
|
+
(move the fetch to the controller; mutations become form POSTs or
|
|
288
|
+
Turbo Stream responses). Operation names are echoed in the comment so
|
|
289
|
+
the reviewer can match the call back to its GraphQL document.
|
|
290
|
+
Destructured names (`{ data, loading, error }` from `useQuery`,
|
|
291
|
+
`[mutate, { loading }]` from `useMutation`) are captured in
|
|
292
|
+
`local_binding_names` so use sites translate to `nil` placeholders
|
|
293
|
+
instead of raising NameError at render time.
|
|
294
|
+
- **Next.js navigation hooks recognized.** `useRouter`, `usePathname`,
|
|
295
|
+
`useSearchParams`, `useParams`, `useSelectedLayoutSegment`, and
|
|
296
|
+
`useSelectedLayoutSegments` get the same treatment — tagged
|
|
297
|
+
`library: :next_js`, surfaced in a dedicated TODO block listing each
|
|
298
|
+
hook's Rails equivalent (`useRouter` → `redirect_to`; `usePathname`
|
|
299
|
+
→ `request.path`; `useSearchParams` / `useParams` → `params`;
|
|
300
|
+
`useSelectedLayoutSegment(s)` → pattern-match `request.path`).
|
|
301
|
+
|
|
302
|
+
The `ReactHookCall` IR type gains `library` and `operation` fields;
|
|
303
|
+
both backends group hooks by library and emit one TODO block per group.
|
|
304
|
+
React-only files keep the unchanged single-block output.
|
|
305
|
+
|
|
306
|
+
### Added
|
|
307
|
+
|
|
308
|
+
- **`BinaryExpression` and `LogicalExpression` translation.**
|
|
309
|
+
`email.emailAttachments.length > 0` now translates to
|
|
310
|
+
`@email.email_attachments.length > 0` instead of bailing to `if false`.
|
|
311
|
+
Covers `===`/`!==`/`==`/`!=`/`<`/`>`/`<=`/`>=`/`&&`/`||`/`??`. JS-only
|
|
312
|
+
operators map to their Ruby equivalents (`===` → `==`, `??` → `||`).
|
|
313
|
+
Recursive splitting respects nested parens and string literals; outer
|
|
314
|
+
parens are stripped so `(a > b) && c` parses cleanly.
|
|
315
|
+
- **Optional chaining (`?.`) → safe navigation (`&.`).** `user?.profile?.name`
|
|
316
|
+
now emits `@user&.profile&.name` instead of leaving a Ruby SyntaxError
|
|
317
|
+
in member-chain interpolations.
|
|
318
|
+
- **Nested render-function locals extracted to methods.**
|
|
319
|
+
`const renderHeader = () => <h1/>; ... {renderHeader()}` previously
|
|
320
|
+
dropped to `[untranslated: renderHeader()]`. New IR types `RenderMethod`
|
|
321
|
+
and `LocalRenderCall` mean the arrow gets extracted to a private method
|
|
322
|
+
on the Phlex class (`def render_header; h1 do; ...; end`) and the use
|
|
323
|
+
site emits a direct call. Args translate through the same path as
|
|
324
|
+
attribute interpolations. The ViewComponent backend emits a method
|
|
325
|
+
skeleton with a TODO since ERB-method bodies don't translate cleanly
|
|
326
|
+
to Ruby fragments.
|
|
327
|
+
|
|
328
|
+
### Fixed
|
|
329
|
+
|
|
330
|
+
- **`useCallback` / `useRef` / `useMemo` identifier bindings recognized.**
|
|
331
|
+
`const handleChange = useCallback(...)` followed by
|
|
332
|
+
`onChange={handleChange}` used to emit `on_change: handle_change`
|
|
333
|
+
referencing a nonexistent method. The binding name is now captured in
|
|
334
|
+
`Component#local_binding_names` so the translator emits `nil` at use
|
|
335
|
+
sites, matching the destructure-pattern behavior added in v0.4.0.
|
|
336
|
+
- **Conditional guard on a known local no longer collapses to `if nil`.**
|
|
337
|
+
`error && <X />` where `error` is destructured from a hook used to
|
|
338
|
+
translate to `if nil` (the local-binding placeholder) — Ruby-valid but
|
|
339
|
+
silently never-rendering. Both backends now treat a `"nil"` translation
|
|
340
|
+
as untranslatable, falling through to the TODO-emission path so the
|
|
341
|
+
reviewer sees what to fill in.
|
|
342
|
+
|
|
5
343
|
## [0.4.0] - 2026-05-11
|
|
6
344
|
|
|
7
345
|
Closes nine translation gaps identified during a sample review of 12
|
|
@@ -94,9 +432,6 @@ generated Ruby parsed but didn't behave like the source JSX.
|
|
|
94
432
|
|
|
95
433
|
### Stress test outcome
|
|
96
434
|
|
|
97
|
-
- 929-file Phlex stress run on `reserv-web`: 887/929 clean translations
|
|
98
|
-
(unchanged — rejection logic untouched), **0/1224 syntax failures**
|
|
99
|
-
(down from 25 on v0.3.0). All emitted `.rb` files now pass `ruby -c`.
|
|
100
435
|
- Five residual bugs were caught during the v0.4.0 stress rerun and
|
|
101
436
|
fixed inline:
|
|
102
437
|
- Prop default expressions that translated to `nil # TODO: ...` inside
|
|
@@ -122,10 +457,8 @@ generated Ruby parsed but didn't behave like the source JSX.
|
|
|
122
457
|
|
|
123
458
|
## [0.3.0] - 2026-05-10
|
|
124
459
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
on v0.2.0: 838/929 (90.2%) clean exit, 91 hard failures across 5 distinct
|
|
128
|
-
error categories. This release ships fixes for all five plus a follow-up
|
|
460
|
+
Baseline outcome on v0.2.0: 838/929 (90.2%) clean exit, 91 hard failures across
|
|
461
|
+
5 distinct error categories. This release ships fixes for all five plus a follow-up
|
|
129
462
|
that opens up lowercase JSX-returning helpers as components, lifting the
|
|
130
463
|
corpus to **887/929 (95.5%) clean exit**. The 42 remaining failures are
|
|
131
464
|
non-component modules (utility/hook libraries, AG-Grid column
|
|
@@ -207,10 +540,8 @@ initializers); each now reports a classifier-tagged error that explains
|
|
|
207
540
|
|
|
208
541
|
## [0.2.0] - 2026-05-10
|
|
209
542
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return-shape gaps and a crash on nested destructure; this release fixes all
|
|
213
|
-
four. Probe outcome: 33/39 → **39/39 emit**.
|
|
543
|
+
The slice exposed three return-shape gaps and a crash on nested destructure;
|
|
544
|
+
this release fixes all four. Probe outcome: 33/39 → **39/39 emit**.
|
|
214
545
|
|
|
215
546
|
### Fixed
|
|
216
547
|
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
The README has the user-facing pipeline overview, backend trade-offs, and CLI surface. This file covers the things that aren't obvious from the README or a cold read of the source.
|
|
6
|
+
|
|
7
|
+
## Common commands
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bin/setup # bundle install + npm install in node/ (required after fresh clone)
|
|
11
|
+
bundle exec rspec # full test suite
|
|
12
|
+
bundle exec rspec spec/backend/phlex_spec.rb # single file
|
|
13
|
+
bundle exec rspec spec/backend/phlex_spec.rb:123 # single example by line
|
|
14
|
+
bundle exec rspec -e "guard-ladder" # examples whose describe/it matches
|
|
15
|
+
bundle exec rubocop
|
|
16
|
+
bundle exec rake # default: rspec + rubocop
|
|
17
|
+
bundle exec exe/jsx_rosetta translate path/X.tsx --as=phlex --phlex-suffix=Component -o /tmp
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`bin/setup` is the first-run requirement — the Node sidecar's `node_modules` isn't bundled, and `JsxRosetta.parse` shells out to `node/parse.js`. If `rspec` fails with parser errors, re-run `bin/setup`. `JSX_ROSETTA_NODE` overrides the `node` binary path.
|
|
21
|
+
|
|
22
|
+
`tmp/run_phlex_stress.sh` translates a large external JSX/TSX corpus through the Phlex backend; results land in `tmp/stress/` (gitignored). Use after non-trivial changes to confirm clean-translation count and `ruby -c` pass count don't regress. The TSV at `tmp/stress/phlex_results.tsv` is the headline.
|
|
23
|
+
|
|
24
|
+
## Pipeline
|
|
25
|
+
|
|
26
|
+
JSX → Babel AST → IR → backend. See README for the user-facing diagram. Hot files:
|
|
27
|
+
|
|
28
|
+
- `lib/jsx_rosetta/ir/lowering.rb` (1700+ lines, single class) — the source of truth for AST→IR. New translation behaviors land here.
|
|
29
|
+
- `lib/jsx_rosetta/ir/types.rb` — `Data.define`'d value classes. `IR::Component` is the root.
|
|
30
|
+
- `lib/jsx_rosetta/backend/view_component/expression_translator.rb` — shared by every backend for JS-expression fragments inside JSX bodies/attributes. Despite the directory name, the Phlex backend uses it too.
|
|
31
|
+
- `lib/jsx_rosetta/backend/phlex.rb` — the most actively maintained backend; most recent improvements target it first.
|
|
32
|
+
|
|
33
|
+
When in doubt, lowering preserves verbatim JS as `IR::Interpolation` so the backend's `ExpressionTranslator` can take a second pass at it.
|
|
34
|
+
|
|
35
|
+
## ExpressionTranslator identifier resolution (load-bearing)
|
|
36
|
+
|
|
37
|
+
An identifier reference inside JSX is classified into one of five buckets in order. This is the central machinery for closing render-time NameErrors — most recent fixes work by tightening which names land in bucket 4.
|
|
38
|
+
|
|
39
|
+
1. **Local scope** (pushed via `with_locals` — loop bindings, render-prop params, render-method params) → bare snake_case.
|
|
40
|
+
2. **Prop alias** (`"data-testid": dataTestId` in destructure) → `@ivar` of the underlying prop name.
|
|
41
|
+
3. **Prop name** → `@snake_case_ivar`.
|
|
42
|
+
4. **`local_binding_names` ∪ `imported_names`** (hook tuple destructures, top-level `const`/`function`/`import` declarations) → `nil` at leaf position; **bail** (return nil from the translate call) at member-chain root / unary operand / binary operand. The bail routes the caller into a TODO emission instead of a bare snake_case ref that NameErrors at render.
|
|
43
|
+
5. **Fallthrough** → bare snake_case identifier, recorded as `unresolved`. Backends drop with TODO on uppercase (PascalCase / SCREAMING) since those are almost always imports; lowercase passes through as a Rails-helper-shaped reference (`current_user` etc).
|
|
44
|
+
|
|
45
|
+
When adding a "we know X exists but can't translate it" class, plumb the names into the translator via `imported_names:` or `local_binding_names:` so bucket 4 fires.
|
|
46
|
+
|
|
47
|
+
## Project conventions
|
|
48
|
+
|
|
49
|
+
- **Preserve-as-TODO over speculative translation.** Flag `# TODO:` with verbatim source rather than guess. Translator bailout + backend drop-with-TODO helpers exist to keep this safe.
|
|
50
|
+
- **No JS-to-Ruby translation paths that best-guess arbitrary call expressions.** Extending the `ExpressionTranslator` for unambiguous mappings (`===`→`==`, `??`→`||`, etc.) is fine; guessing call semantics is not.
|
|
51
|
+
- **No external-corpus references in committed artifacts.** Specs, fixtures, source comments, commit messages, CHANGELOG, README must stay generic ("the stress corpus" / "a real-world JSX corpus"). Anything under `tmp/` is gitignored and can reference whatever.
|
|
52
|
+
|
|
53
|
+
## Emission rules that are subtle
|
|
54
|
+
|
|
55
|
+
- **Phlex attribute casing.** HTML-element attrs (lowercase tag) preserve camelCase verbatim — Phlex 2 only converts `_` to `-`, so SVG attrs like `preserveAspectRatio` and `viewBox` must stay camelCase. Component invocations (PascalCase tag) full-snake_case all keys per Ruby kwarg convention. See `plain_attribute_part` in `backend/phlex.rb`.
|
|
56
|
+
- **Dropped attributes are omitted, not nilled.** When a value bails to `nil` with a recorded TODO, the entire kwarg drops from the emission (the TODO above the element is the record). Explicit `attr={null}` in source still emits `attr: nil` (translation succeeded, no TODO → not a drop). Mechanism: `plain_attribute_part` watches whether a TODO was appended during value computation.
|
|
57
|
+
- **Sibling components share `module_bindings` by reference.** `lower_all` attaches the *same* array to every sibling, so backends emit the constants TODO prefix only on the first sibling via `first_emit_for_module_bindings?` (object-identity check on the array).
|
|
58
|
+
- **JSX in non-child positions** lowers through `lower_value_expression` → `lower_jsx_value`. Single-child Fragments unwrap (common React workaround for "single ReactNode required"). Hit by attribute values (`icon={<Foo/>}`) and render lambdas in column-config arrays.
|
|
59
|
+
- **Inline arrow handlers split by tag case.** On PascalCase tags, lower to `IR::EventHandler` (verbatim JS body) → backend extracts a stub instance method (`handle_click`, `handle_change`) and emits `on_click: method(:handle_click)`. On HTML elements, lower to Stimulus method extraction instead (see `stimulus_methods` on `IR::Component`).
|
|
60
|
+
|
|
61
|
+
## Spec gotchas
|
|
62
|
+
|
|
63
|
+
- `spec/backend/phlex_spec.rb` is the canonical large suite (~1000 lines now) — most behavior changes get their regression cover here.
|
|
64
|
+
- The Phlex spec helper's `files_for` uses **no suffix by default** — paths are `x.rb` (not `x_component.rb`) unless the spec passes `suffix: "Component"`.
|
|
65
|
+
- `spec/fixtures/jsx/*.{jsx,tsx}` + `spec/fixtures/expected/*` feed the Button full-IR-equality test; touch carefully.
|
|
66
|
+
- `spec/.rspec_status` is gitignored and huge — don't read it.
|
|
67
|
+
|
|
68
|
+
## Commit message style
|
|
69
|
+
|
|
70
|
+
Recent commits use a focused subject + structured body (subject explains *what changed*, body has sections like "Bug fixes:", "Implementation:", "Stress corpus impact:"). When behavior changes shift the stress numbers (clean translations / syntax fails / top dropped-attribute categories), include before/after counts in the body — they're the most useful signal for whether the change earns its complexity.
|
data/README.md
CHANGED
|
@@ -168,6 +168,56 @@ auto-perform. Common cases:
|
|
|
168
168
|
- **Reserved Rails controller names** (in the routes script) → `# WARNING:`
|
|
169
169
|
line listing collisions.
|
|
170
170
|
|
|
171
|
+
## Resolving TODOs (optional Claude Code skill)
|
|
172
|
+
|
|
173
|
+
The repository ships an optional [Claude Code](https://docs.claude.com/en/docs/claude-code/overview) skill and worker agent that automate the last-mile work of clearing `# TODO:` comments from the generated output. The skill is fully generic — it ships no app-specific assumptions, no design-system data, and no host-stack conventions. You bring those.
|
|
174
|
+
|
|
175
|
+
**What it does:**
|
|
176
|
+
|
|
177
|
+
- A pure-Ruby corpus scanner (`tools/discover_bailouts.rb`) tallies the dropped-expression patterns in your generated output and surfaces likely design-system / config-namespace clusters.
|
|
178
|
+
- A pure-Ruby substitution pass (`tools/apply_substitutions.rb`) reads a YAML you supply (`match:` regex + `tokens:` value table) and splices values into the generated `render Foo.new(...)` calls. Always validates with `ruby -c` before writing; reverts on failure.
|
|
179
|
+
- LLM-driven recipes for React hooks, Apollo data fetching, event handlers, Next.js navigation, and module-level constants. These default to **sharpening** verbose TODOs into single-decision items rather than guessing translations.
|
|
180
|
+
- A `jsx-rosetta-resolve-todo-file` worker agent for parallel fan-out across a corpus.
|
|
181
|
+
|
|
182
|
+
A reference Ant Design v5 token mapping ships under `examples/` — drop-in usable if your source app uses Ant Design with default theming.
|
|
183
|
+
|
|
184
|
+
**Layout in this repo:**
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
skills/jsx-rosetta-resolve-todos/
|
|
188
|
+
├── SKILL.md # workflow + routing table
|
|
189
|
+
├── recipes/ # per-TODO-class recipes
|
|
190
|
+
├── tools/ # discover_bailouts.rb, apply_substitutions.rb
|
|
191
|
+
├── data/design_tokens.template.yml # blank schema for your design system
|
|
192
|
+
└── examples/design_tokens.ant_design_v5.yml # reference for Ant-using apps
|
|
193
|
+
|
|
194
|
+
agents/
|
|
195
|
+
└── jsx-rosetta-resolve-todo-file.md # fan-out worker definition
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Installing into your environment:**
|
|
199
|
+
|
|
200
|
+
User-level (available in every Claude Code session on your machine):
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
mkdir -p ~/.claude/skills ~/.claude/agents
|
|
204
|
+
ln -s "$(pwd)/skills/jsx-rosetta-resolve-todos" ~/.claude/skills/
|
|
205
|
+
ln -s "$(pwd)/agents/jsx-rosetta-resolve-todo-file.md" ~/.claude/agents/
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Project-level (only available in the consuming Rails app):
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
# from your Rails app's repo root
|
|
212
|
+
mkdir -p .claude/skills .claude/agents
|
|
213
|
+
cp -R /path/to/jsx_rosetta/skills/jsx-rosetta-resolve-todos .claude/skills/
|
|
214
|
+
cp /path/to/jsx_rosetta/agents/jsx-rosetta-resolve-todo-file.md .claude/agents/
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
In either case, your app-specific configuration (the populated `data/design_tokens.yml`, `data/target_app_conventions.md`, etc.) belongs **inside the consuming app's** `.claude/skills/jsx-rosetta-resolve-todos/data/` — not here. Anything app-specific should be `.gitignore`d in your Rails repo if it shouldn't be shared.
|
|
218
|
+
|
|
219
|
+
The scripts under `tools/` are runnable directly with `ruby` — Claude Code is not required to use them. The recipes and the agent are what require Claude Code.
|
|
220
|
+
|
|
171
221
|
## What's deferred
|
|
172
222
|
|
|
173
223
|
- Translating React state primitives (`useState` / `useEffect` etc.). The
|
data/ROADMAP.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
3
|
+
Forward-looking list of work items. Shipped releases are documented in
|
|
4
|
+
[CHANGELOG.md](CHANGELOG.md); the v0.1.0 design lives in [PLAN.md](PLAN.md).
|
|
5
|
+
|
|
6
|
+
Items are tagged by source so the lineage is traceable:
|
|
7
|
+
- **[review]** — surfaced by a subagent sample review of the Phlex
|
|
8
|
+
stress run (highest signal — these are gaps seen in the wild).
|
|
9
|
+
- **[plan-oos]** — explicitly listed as out-of-scope in a prior release plan.
|
|
10
|
+
- **[stress]** — surfaced by the 929-file stress run rejection logs.
|
|
11
|
+
|
|
12
|
+
## Next up
|
|
13
|
+
|
|
14
|
+
v0.5.0 ships every roadmap "Larger features" item that was queued. The
|
|
15
|
+
remaining work in this file is Stress-test residual triage and Polish.
|
|
16
|
+
|
|
17
|
+
## v0.5+ — Larger features
|
|
18
|
+
|
|
19
|
+
_All previously-listed items shipped in v0.5.0. Drop new larger
|
|
20
|
+
features here as they surface._
|
|
21
|
+
|
|
22
|
+
## Stress-test residuals (42/929 rejected)
|
|
23
|
+
|
|
24
|
+
The non-component-shape rejections each have a classifier-tagged error
|
|
25
|
+
message; most are intentional drops. Worth revisiting if a specific
|
|
26
|
+
shape becomes high-value:
|
|
27
|
+
|
|
28
|
+
- [ ] Custom-hooks modules (`useFoo.ts` returning behavior) —
|
|
29
|
+
classifier says "translate behavior to Stimulus." [stress]
|
|
30
|
+
- [ ] Side-effect-only modules — classifier says "register in a Rails
|
|
31
|
+
initializer." [stress]
|
|
32
|
+
- [ ] Types-only / constants-only modules — currently dropped; could
|
|
33
|
+
emit Ruby constants for the constants subset. [stress]
|
|
34
|
+
- [ ] Mixed-export modules — classifier asks the human to split the
|
|
35
|
+
file. No automated fix planned. [stress]
|
|
36
|
+
|
|
37
|
+
## Polish / quality
|
|
38
|
+
|
|
39
|
+
- [ ] **Spec coverage gap.** Some v0.4.0 fixes (template-literal
|
|
40
|
+
escaping, multi-line comment prefixing) have one spec each; consider
|
|
41
|
+
fuzzing across a broader corpus.
|
|
42
|
+
- [ ] **README update.** v0.3.0 added the Phlex backend, v0.4.0 closed
|
|
43
|
+
many gaps — README still describes the v0.2.0 ViewComponent-only
|
|
44
|
+
surface. Add a Phlex section and an example of the recursive
|
|
45
|
+
object-literal translation.
|
|
46
|
+
- [ ] **CHANGELOG TLDR.** The v0.4.0 entry is dense; a one-paragraph
|
|
47
|
+
"what this means for users" intro would help.
|
|
48
|
+
|
|
49
|
+
## Done — historical reference
|
|
50
|
+
|
|
51
|
+
See [CHANGELOG.md](CHANGELOG.md). Major arcs to date:
|
|
52
|
+
- **v0.1.0** — ViewComponent backend, three-stage pipeline.
|
|
53
|
+
- **v0.2.0** — Stimulus extraction, sidecar layout, helpers, RailsView,
|
|
54
|
+
routes, compound components, asChild polymorphism, React hooks.
|
|
55
|
+
- **v0.3.0** — Phlex 2.x backend (three naming strategies), 887/929
|
|
56
|
+
clean translations.
|
|
57
|
+
- **v0.4.0** — Closed nine gaps surfaced by a sample review of v0.3.0
|
|
58
|
+
Phlex output (A, B, D, E, F, G, H, J, K); 0/1224 syntax failures
|
|
59
|
+
(down from 25); 343 specs.
|
|
60
|
+
- **v0.5.0** — Closed all four v0.5.0 candidate items from the v0.4.0
|
|
61
|
+
sample review **plus** every "Larger features" item that was queued:
|
|
62
|
+
- useCallback / useRef / useMemo identifier-bound hook results captured
|
|
63
|
+
in `local_binding_names` so use sites emit `nil` instead of bare
|
|
64
|
+
snake_case refs to nonexistent methods.
|
|
65
|
+
- `BinaryExpression` / `LogicalExpression` translation in the
|
|
66
|
+
`ExpressionTranslator` (incl. `===`/`!==`/`??` mapping).
|
|
67
|
+
- Optional chaining (`?.`) → safe navigation (`&.`) in member chains.
|
|
68
|
+
- Nested render-function locals (`const renderHeader = () => <div/>;
|
|
69
|
+
... {renderHeader()}`) extracted to private methods on the class.
|
|
70
|
+
- `error && <X/>` guard on a known local no longer collapses to
|
|
71
|
+
`if nil` — falls through to the TODO path.
|
|
72
|
+
- Apollo hooks (`useQuery` / `useLazyQuery` / `useMutation` /
|
|
73
|
+
`useSubscription` / `useApolloClient`) detected with the GraphQL
|
|
74
|
+
operation name extracted from a bare-Identifier first argument;
|
|
75
|
+
emitted as a dedicated TODO block pointing at the Rails controller
|
|
76
|
+
fetch. Stress-test impact: 221/929 files now carry the Apollo block
|
|
77
|
+
(281 operation names captured).
|
|
78
|
+
- Next.js navigation hooks (`useRouter` / `usePathname` /
|
|
79
|
+
`useSearchParams` / `useParams` / `useSelectedLayoutSegment(s)`)
|
|
80
|
+
detected and surfaced in a dedicated TODO block listing each
|
|
81
|
+
hook's Rails analog. Stress-test impact: 105/929 files now carry
|
|
82
|
+
the Next.js block.
|
|
83
|
+
- Class-component support (`render()` method extraction, `this.props.X`
|
|
84
|
+
→ `@x` translation, non-render members captured as TODO comments).
|
|
85
|
+
The 4 class-component residuals now translate cleanly.
|
|
86
|
+
- Data-factory components (`export const createColumns = (token) =>
|
|
87
|
+
[{...}]`) lower with `mode: :data_factory`; Phlex emits a snake_case
|
|
88
|
+
method that returns the translated array, with JSX render lambdas
|
|
89
|
+
extracted to private methods.
|
|
90
|
+
- Pretty-printing for long `ObjectLiteral` / `ArrayLiteral` output:
|
|
91
|
+
multi-line layout when single-line exceeds 80 chars; short literals
|
|
92
|
+
stay inline.
|