jsx_rosetta 0.4.0 → 0.5.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d7b64119bbfe86413da7a779ada7e47cab06088411cb4667be2e79f8cfba3464
4
- data.tar.gz: c1c1ac742642015a6ae7037538a34bf58b42b6e2fc232bedf48f8660b13a640d
3
+ metadata.gz: 229956b183e3760f9555874fef214d7ce66d549a603e9d431f5c28251db25bb4
4
+ data.tar.gz: 226c173f5e1010d154801740a2924bbc776ff8eede26587ec1d9d796da51c5f1
5
5
  SHA512:
6
- metadata.gz: 44a3dc2fc89b0dbb59ca5026f2cb27cc7f22e498cf125483725e1db07a39db1e45616baff9766be82bc52e443b33bf64180b90175660536c91f4bfc89e5b1926
7
- data.tar.gz: a9c999e35af23ff2aba38a02ff57fc6229bf051dc339db859c341c9c1d607f88278542abd07a742ac84840616a305dcfe48b8bad32767ffbf5fc8d08c45ef7a9
6
+ metadata.gz: 4dbf8ac15094f2462a38c65dfbf539163cfb471077e4bd8874893eda3262da031df72da161793f4c65d75597d55c1d7ea09b395f991fb7fcf80b074d6a74e7ca
7
+ data.tar.gz: 94627c0471abeab22e92727047ec9e870439297bc6df6447da1bfb79914415d14abc665d3b2325d9496615e62c3f0099898db11a6c05dfd37acf739aa0af518c
data/CHANGELOG.md CHANGED
@@ -1,6 +1,220 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased]
3
+ ## [0.5.1] - 2026-05-11
4
+
5
+ A correctness pass on the v0.5.0 Phlex output. A random-sample review
6
+ surfaced six classes of render-time NameError that `ruby -c` couldn't
7
+ catch (the file parses, but evaluation crashes on a bare reference).
8
+ Each one is now closed at the source — the file loads and at worst
9
+ renders empty where a TODO marker tells the reviewer what to fill in.
10
+ Stress test stays at 895/929 clean and 0/1240 syntax failures; lint
11
+ volume on the generated `.rb` files drops ~38% (5443 → 3373 offenses).
12
+
13
+ ### Fixed — runtime NameErrors at render time
14
+
15
+ - **Destructure leaks downstream of an untranslatable init.** `const
16
+ { fieldValue } = customField` is captured as a TODO, but every use
17
+ site (`fieldValue?.X`, `!fieldValue`, `fieldValue > 0`) used to leak
18
+ as a bare snake_case ref — file loaded, but `field_value&.__typename`
19
+ NameErrored on first render. The translator now bails the whole
20
+ expression when a known-local sits at a member-chain root, on the
21
+ operand side of unary, or on either side of binary. The caller emits
22
+ a `# TODO: translate condition: …` line and falls through to an
23
+ `if false` fallback so the file at least loads.
24
+ - **Inline arrow handlers on PascalCase components.** `const handleClick
25
+ = () => {}` attached to `<Button onClick={handleClick}>` no longer
26
+ leaks as bare `handle_click`. Unconsumed local-arrow names are unioned
27
+ into `Component#local_binding_names`, so use sites emit `on_click: nil`
28
+ instead of a reference to an undefined method.
29
+ - **CamelCase / snake_case rest-prop mismatch.** `**descriptionProps`
30
+ emitted `**descriptionProps` in the initializer signature but the body
31
+ read `**(@description_props || {})` — silently dropped. The rest-name
32
+ is now snake_cased on both sides. `IR::Prop` gains an `alias_name:`
33
+ field so renamed destructures (`"data-testid": dataTestId`) resolve
34
+ the alias to the prop's snake_case ivar at use sites.
35
+ - **Unitless numeric inline styles.** `style={{ marginBottom: 16 }}` used
36
+ to emit `style: 'margin-bottom: 16;'` — invalid CSS, silently ignored
37
+ by browsers. Numeric style values now get a `px` suffix for any
38
+ property not in React's `isUnitlessNumber` table (`zIndex`,
39
+ `lineHeight`, `opacity`, etc. stay bare).
40
+ - **Style declarations rooted at an unresolvable local.** `style={{
41
+ borderRadius: token.borderRadius }}` used to emit `"border-radius:
42
+ #{token.borderRadius};"` and NameError at render. The style
43
+ declaration now drops with a per-decl TODO when its value can't
44
+ translate.
45
+ - **Uppercase / SCREAMING_SNAKE imports in attribute values.** JSX
46
+ `<Foo bar={DEFAULT_PAGE_SIZE}>` used to emit `bar: default_page_size`
47
+ (NameError at the call site). Attribute-position interpolation now
48
+ drops with a TODO when any unresolved identifier starts with
49
+ uppercase — almost always an external import. Lowercase unresolved
50
+ identifiers still pass through unchanged (they may be Rails helpers
51
+ like `current_user`).
52
+
53
+ ### Changed — rubocop output cleanup (5443 → 3373 offenses across 1240 .rb)
54
+
55
+ - **`Lint/MissingSuper`** — initializers emit `super()` to satisfy
56
+ Phlex 2.x's superclass.
57
+ - **`Style/Documentation`** — one-line class docstring above every
58
+ generated class (`# X — generated by jsx_rosetta from JSX. Review
59
+ before shipping.`).
60
+ - **`Style/StringLiterals`** — new helper `AST::Inflector.ruby_string_literal`
61
+ emits single-quoted strings when safe (no embedded single quote,
62
+ backslash, or control char). Non-ASCII like emojis stays in single
63
+ quotes. The translator also re-emits JS string literals (`"hi"` →
64
+ `'hi'`) when the same constraints hold.
65
+ - **`Style/NilComparison` / `Style/NonNilCheck`** — rewrite `x == nil`
66
+ and `x != nil` as `x.nil?` / `!x.nil?` at the binary-translation
67
+ layer.
68
+ - **`Lint/BinaryOperatorWithIdenticalOperands`** — dedupe `||` / `&&`
69
+ when both sides translate to the same Ruby. `value === null ||
70
+ value === undefined` collapses to `@value.nil?` instead of
71
+ `@value.nil? || @value.nil?`.
72
+ - **`Lint/EmptyInterpolation`** — bail on the whole template literal
73
+ when any interpolation segment fails translation or resolves to
74
+ literal `nil`. No more `"prefix-#{}-suffix"`.
75
+ - **`Lint/LiteralAsCondition` / `Lint/DuplicateElsifCondition`** —
76
+ emit a file-level `# rubocop:disable` directive (with a matching
77
+ enable at the bottom of the file) only when an intentional `if false`
78
+ fallback appears. The `# TODO: translate condition:` line above the
79
+ fallback already names the issue, so the cop's report would be
80
+ redundant noise.
81
+ - **`Style/IfInsideElse`** — refactor conditional rendering to chain
82
+ `if / elsif / elsif / else / end` instead of nested
83
+ `if / else / if / else / end`. Cascades into fewer
84
+ `Metrics/BlockNesting`, `Metrics/AbcSize`, and
85
+ `Lint/DuplicateElsifCondition` offenses too.
86
+ - **`Layout/TrailingWhitespace`** — post-process emitted output to
87
+ rstrip every line.
88
+ - **`Layout/FirstHashElementIndentation`** — force-inline object/array
89
+ literals inside `initialize` default values; the column mismatch
90
+ that triggered the cop only happens when the wrap kicks in there.
91
+
92
+ ## [0.5.0] - 2026-05-11
93
+
94
+ Closes the four v0.5.0-candidate items from the v0.4.0 Phlex sample
95
+ review, plus the four larger features queued at the top of the
96
+ roadmap: Apollo + Next.js hook hint translation, class-component
97
+ support, AG-Grid column-descriptor module emission, and pretty-printing
98
+ for long object/array literals.
99
+
100
+ ### Added — class-component support
101
+
102
+ - **`ClassDeclaration` → ViewComponent path.** Classes with a `render()`
103
+ method now lower as components instead of getting flagged with the
104
+ `:class_component` rejection. The `ExpressionTranslator` recognizes
105
+ `this.props.X` and translates to `@x` (plus a snake_case member chain
106
+ for `this.props.X.y.z`); `this.state.X` translates to `nil` since
107
+ there's no Rails-side equivalent without a backing data source. Other
108
+ class members (constructor, lifecycle hooks like
109
+ `componentDidCatch`/`getDerivedStateFromError`, custom event handlers)
110
+ get captured as LocalBinding-style TODO comments at the top of the
111
+ view template so the reviewer sees the verbatim sources. Props are
112
+ synthesized from direct `this.props.X` access AND from
113
+ `const { X } = this.props` destructure patterns — the generated
114
+ `initialize(...)` matches the original class's prop set.
115
+ Stress-test impact: the 4 class-component residuals (ErrorBoundary
116
+ and cousins) now translate cleanly.
117
+
118
+ ### Added — AG-Grid column-descriptor module emission
119
+
120
+ - **Data-factory components.** `export const createColumns = (token,
121
+ sortedInfo) => [{...}, {...}]` now lowers as an `IR::Component` with
122
+ `mode: :data_factory`. Phlex emits a snake_case method that returns
123
+ the translated array — `def create_columns(token: nil, sorted_info: nil)`
124
+ — instead of `view_template`. JSX inside object properties extracts to
125
+ private methods on the class via the existing IR::Lambda path
126
+ (`render: method(:render_id_cell)`). ViewComponent backend emits a
127
+ plain Ruby class with the method and a TODO note (the .erb pair is
128
+ skipped — pure-data classes don't have a template). Multi-positional
129
+ Identifier params are now also supported by `lower_params` — previously
130
+ only single-arg React-style signatures lowered cleanly.
131
+
132
+ ### Added — pretty-printing
133
+
134
+ - **Multi-line layout for long object/array literals.** When the single-
135
+ line rendering of an `IR::ObjectLiteral` or `IR::ArrayLiteral` exceeds
136
+ `LITERAL_INLINE_BUDGET` (80 chars), or contains a nested literal that
137
+ itself wrapped, the layout switches to one entry per line indented
138
+ two spaces past the parent line's indent. Closing bracket re-aligns
139
+ to the parent indent. Short literals (typical Select options, small
140
+ config objects) stay inline so non-AG-Grid output is unchanged. Helps
141
+ readability of the column-descriptor output from the new
142
+ data-factory path.
143
+
144
+ ### Stress test outcome
145
+
146
+ - 929-file Phlex stress rerun: **895/929 clean translations**
147
+ (up from 887/929 in v0.4.0 — 8 additional files now translate via
148
+ the class-component and data-factory paths). **0/1240 emitted `.rb`
149
+ files fail `ruby -c`** (unchanged; 16 more files emitted vs v0.4.0).
150
+ 221/929 files carry an Apollo TODO block (281 GraphQL operation
151
+ names captured); 105/929 carry a Next.js navigation-hook block.
152
+ - 385 specs, all green; rubocop clean.
153
+
154
+ ### Added — framework hook hints
155
+
156
+ - **Apollo data-fetching hooks recognized.** `useQuery`, `useLazyQuery`,
157
+ `useMutation`, `useSubscription`, and `useApolloClient` are now
158
+ detected at lowering time. Each call lands in `Component#react_hooks`
159
+ tagged `library: :apollo`, with the GraphQL operation name extracted
160
+ from a bare-Identifier first argument (`useQuery(GET_USERS_QUERY, …)`
161
+ → `operation: "GET_USERS_QUERY"`). Both backends emit a dedicated
162
+ Apollo TODO block above the template that points at the Rails analog
163
+ (move the fetch to the controller; mutations become form POSTs or
164
+ Turbo Stream responses). Operation names are echoed in the comment so
165
+ the reviewer can match the call back to its GraphQL document.
166
+ Destructured names (`{ data, loading, error }` from `useQuery`,
167
+ `[mutate, { loading }]` from `useMutation`) are captured in
168
+ `local_binding_names` so use sites translate to `nil` placeholders
169
+ instead of raising NameError at render time.
170
+ - **Next.js navigation hooks recognized.** `useRouter`, `usePathname`,
171
+ `useSearchParams`, `useParams`, `useSelectedLayoutSegment`, and
172
+ `useSelectedLayoutSegments` get the same treatment — tagged
173
+ `library: :next_js`, surfaced in a dedicated TODO block listing each
174
+ hook's Rails equivalent (`useRouter` → `redirect_to`; `usePathname`
175
+ → `request.path`; `useSearchParams` / `useParams` → `params`;
176
+ `useSelectedLayoutSegment(s)` → pattern-match `request.path`).
177
+
178
+ The `ReactHookCall` IR type gains `library` and `operation` fields;
179
+ both backends group hooks by library and emit one TODO block per group.
180
+ React-only files keep the unchanged single-block output.
181
+
182
+ ### Added
183
+
184
+ - **`BinaryExpression` and `LogicalExpression` translation.**
185
+ `email.emailAttachments.length > 0` now translates to
186
+ `@email.email_attachments.length > 0` instead of bailing to `if false`.
187
+ Covers `===`/`!==`/`==`/`!=`/`<`/`>`/`<=`/`>=`/`&&`/`||`/`??`. JS-only
188
+ operators map to their Ruby equivalents (`===` → `==`, `??` → `||`).
189
+ Recursive splitting respects nested parens and string literals; outer
190
+ parens are stripped so `(a > b) && c` parses cleanly.
191
+ - **Optional chaining (`?.`) → safe navigation (`&.`).** `user?.profile?.name`
192
+ now emits `@user&.profile&.name` instead of leaving a Ruby SyntaxError
193
+ in member-chain interpolations.
194
+ - **Nested render-function locals extracted to methods.**
195
+ `const renderHeader = () => <h1/>; ... {renderHeader()}` previously
196
+ dropped to `[untranslated: renderHeader()]`. New IR types `RenderMethod`
197
+ and `LocalRenderCall` mean the arrow gets extracted to a private method
198
+ on the Phlex class (`def render_header; h1 do; ...; end`) and the use
199
+ site emits a direct call. Args translate through the same path as
200
+ attribute interpolations. The ViewComponent backend emits a method
201
+ skeleton with a TODO since ERB-method bodies don't translate cleanly
202
+ to Ruby fragments.
203
+
204
+ ### Fixed
205
+
206
+ - **`useCallback` / `useRef` / `useMemo` identifier bindings recognized.**
207
+ `const handleChange = useCallback(...)` followed by
208
+ `onChange={handleChange}` used to emit `on_change: handle_change`
209
+ referencing a nonexistent method. The binding name is now captured in
210
+ `Component#local_binding_names` so the translator emits `nil` at use
211
+ sites, matching the destructure-pattern behavior added in v0.4.0.
212
+ - **Conditional guard on a known local no longer collapses to `if nil`.**
213
+ `error && <X />` where `error` is destructured from a hook used to
214
+ translate to `if nil` (the local-binding placeholder) — Ruby-valid but
215
+ silently never-rendering. Both backends now treat a `"nil"` translation
216
+ as untranslatable, falling through to the TODO-emission path so the
217
+ reviewer sees what to fill in.
4
218
 
5
219
  ## [0.4.0] - 2026-05-11
6
220
 
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.
@@ -19,6 +19,21 @@ module JsxRosetta
19
19
  parts = string.split("_")
20
20
  parts[0] + parts[1..].map(&:capitalize).join
21
21
  end
22
+
23
+ # Emit a Ruby string literal in the rubocop-default single-quoted
24
+ # form when safe. Falls back to `String#inspect` (double-quoted with
25
+ # escapes) when the source contains characters that prevent the
26
+ # single-quoted form: single quotes themselves, backslashes (Ruby
27
+ # single-quoted strings only escape `\\` and `\'`), or control
28
+ # characters (`\n`, `\t`, etc — single-quoted strings render those
29
+ # literally). Non-ASCII characters (emojis, unicode) are fine in
30
+ # single-quoted strings, so they don't force the fallback.
31
+ def ruby_string_literal(value)
32
+ str = value.to_s
33
+ return str.inspect if str.include?("'") || str.include?("\\") || str.match?(/[\x00-\x1f\x7f]/)
34
+
35
+ "'#{str}'"
36
+ end
22
37
  end
23
38
  end
24
39
  end