jsx_rosetta 0.5.1 → 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 +128 -11
- data/CLAUDE.md +70 -0
- data/README.md +50 -0
- data/agents/jsx-rosetta-resolve-todo-file.md +90 -0
- data/lib/jsx_rosetta/ast/inflector.rb +17 -0
- data/lib/jsx_rosetta/backend/phlex.rb +1078 -77
- data/lib/jsx_rosetta/backend/rails_view.rb +1 -1
- data/lib/jsx_rosetta/backend/view_component/expression_translator.rb +73 -20
- data/lib/jsx_rosetta/backend/view_component.rb +48 -2
- 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 +720 -31
- data/lib/jsx_rosetta/ir/radix_registry.rb +84 -0
- data/lib/jsx_rosetta/ir/types.rb +187 -3
- 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 +29 -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
|
@@ -1,5 +1,129 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
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
|
+
|
|
3
127
|
## [0.5.1] - 2026-05-11
|
|
4
128
|
|
|
5
129
|
A correctness pass on the v0.5.0 Phlex output. A random-sample review
|
|
@@ -308,9 +432,6 @@ generated Ruby parsed but didn't behave like the source JSX.
|
|
|
308
432
|
|
|
309
433
|
### Stress test outcome
|
|
310
434
|
|
|
311
|
-
- 929-file Phlex stress run on `reserv-web`: 887/929 clean translations
|
|
312
|
-
(unchanged — rejection logic untouched), **0/1224 syntax failures**
|
|
313
|
-
(down from 25 on v0.3.0). All emitted `.rb` files now pass `ruby -c`.
|
|
314
435
|
- Five residual bugs were caught during the v0.4.0 stress rerun and
|
|
315
436
|
fixed inline:
|
|
316
437
|
- Prop default expressions that translated to `nil # TODO: ...` inside
|
|
@@ -336,10 +457,8 @@ generated Ruby parsed but didn't behave like the source JSX.
|
|
|
336
457
|
|
|
337
458
|
## [0.3.0] - 2026-05-10
|
|
338
459
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
on v0.2.0: 838/929 (90.2%) clean exit, 91 hard failures across 5 distinct
|
|
342
|
-
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
|
|
343
462
|
that opens up lowercase JSX-returning helpers as components, lifting the
|
|
344
463
|
corpus to **887/929 (95.5%) clean exit**. The 42 remaining failures are
|
|
345
464
|
non-component modules (utility/hook libraries, AG-Grid column
|
|
@@ -421,10 +540,8 @@ initializers); each now reports a classifier-tagged error that explains
|
|
|
421
540
|
|
|
422
541
|
## [0.2.0] - 2026-05-10
|
|
423
542
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
return-shape gaps and a crash on nested destructure; this release fixes all
|
|
427
|
-
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**.
|
|
428
545
|
|
|
429
546
|
### Fixed
|
|
430
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
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: jsx-rosetta-resolve-todo-file
|
|
3
|
+
description: Process a single jsx_rosetta-generated Phlex/ViewComponent file and resolve its `# TODO:` comments by applying the recipes from the jsx-rosetta-resolve-todos skill. Designed for fan-out — one worker per file. Returns a JSON report with category counts.
|
|
4
|
+
tools: Read, Edit, Bash, Grep, Glob
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are a fan-out worker for the **jsx-rosetta-resolve-todos** skill. You receive one file path at a time and apply the recipes to its TODO comments.
|
|
8
|
+
|
|
9
|
+
## Inputs
|
|
10
|
+
|
|
11
|
+
The user message will name a single `.rb` file path. You may also be given:
|
|
12
|
+
|
|
13
|
+
- A path to a `data/design_tokens.yml` (for recipe 01)
|
|
14
|
+
- A path to a `data/target_app_conventions.yml` (for recipes 03–07)
|
|
15
|
+
- The host Rails app's root path (so you can grep for available helpers, controllers, Stimulus controllers)
|
|
16
|
+
|
|
17
|
+
## Hard rules
|
|
18
|
+
|
|
19
|
+
1. **Never speculate on JS-to-Ruby translation.** When unsure, sharpen the TODO; do not guess. This rule overrides any apparent "obvious" translation that isn't on a recipe's whitelist.
|
|
20
|
+
2. **Never ship a file that doesn't pass `ruby -c`.** Run it before any edits (to confirm baseline) and after every meaningful edit. If parsing breaks, revert and report `parse_failed`.
|
|
21
|
+
3. **One file per invocation.** Do not chain to other files; the dispatcher manages fan-out.
|
|
22
|
+
4. **No business-logic changes.** You apply mechanical transformations and emit sharpened TODOs. Anything that would alter observable behavior beyond the TODO's stated intent gets escalated.
|
|
23
|
+
|
|
24
|
+
## Workflow
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
1. Read the target file. Run `ruby -c <file>` — abort if it doesn't parse to begin with.
|
|
28
|
+
2. Read SKILL.md and the relevant recipe files (recipes/*.md).
|
|
29
|
+
3. If a design_tokens.yml is provided, run `ruby tools/apply_substitutions.rb
|
|
30
|
+
--config <yaml> <file>` first. This handles recipe 01 mechanically.
|
|
31
|
+
4. Walk remaining TODOs top-to-bottom. For each:
|
|
32
|
+
a. Classify against the routing table in SKILL.md.
|
|
33
|
+
b. Look up the recipe.
|
|
34
|
+
c. Take exactly one action: resolve, sharpen, or escalate.
|
|
35
|
+
5. Re-run `ruby -c`. If it fails, revert all your edits and report `parse_failed`.
|
|
36
|
+
6. Emit ONE JSON line as your final output (see format below).
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Output format
|
|
40
|
+
|
|
41
|
+
Emit exactly one JSON line as your last message. Schema:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"file": "<absolute path>",
|
|
46
|
+
"parsed_before": true,
|
|
47
|
+
"parsed_after": true,
|
|
48
|
+
"totals": {"resolved": <int>, "sharpened": <int>, "escalated": <int>, "skipped": <int>},
|
|
49
|
+
"by_category": {
|
|
50
|
+
"design_tokens": {"resolved": 3, "skipped": 1},
|
|
51
|
+
"react_hooks": {"sharpened": 2},
|
|
52
|
+
"apollo_hooks": {"sharpened": 1},
|
|
53
|
+
"event_handler": {"sharpened": 4, "escalated": 1},
|
|
54
|
+
"promoted_ivar": {"resolved": 5}
|
|
55
|
+
},
|
|
56
|
+
"notes": ["<optional one-liners about anything unusual>"]
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
If the file fails post-edit parse and is reverted:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{"file": "<path>", "parsed_before": true, "parsed_after": false, "parse_failed": true, "totals": {"resolved": 0, "sharpened": 0, "escalated": 0, "skipped": 0}}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Tool use
|
|
67
|
+
|
|
68
|
+
- **Read** — the target file, recipes, and the host app's relevant directories (app/controllers, app/helpers, app/javascript/controllers).
|
|
69
|
+
- **Grep** — finding existing helpers/controllers/Stimulus targets in the host app to inform sharpened TODOs.
|
|
70
|
+
- **Edit** — applying transformations to the target file. Prefer surgical edits over wholesale rewrites.
|
|
71
|
+
- **Bash** — restricted to `ruby -c <file>` for syntax validation and to running the skill's own scripts (`tools/apply_substitutions.rb`). Do NOT run other commands.
|
|
72
|
+
- **Glob** — locating skill files and recipe paths.
|
|
73
|
+
|
|
74
|
+
## Anti-patterns
|
|
75
|
+
|
|
76
|
+
- **Don't** read or edit any file other than the assigned target (and the skill's recipes/data, read-only). The dispatcher relies on file isolation.
|
|
77
|
+
- **Don't** invoke other agents.
|
|
78
|
+
- **Don't** echo recipe content back in your response — your output is a JSON line, nothing else (the dispatcher parses it).
|
|
79
|
+
- **Don't** add commentary to the file you're editing other than the sharpened TODOs themselves. The skill's emission rules cover what comments are allowed.
|
|
80
|
+
- **Don't** "improve" the surrounding code while you're there. Scope is TODO resolution only.
|
|
81
|
+
|
|
82
|
+
## Escalation
|
|
83
|
+
|
|
84
|
+
When a TODO genuinely has no good answer from the recipes:
|
|
85
|
+
|
|
86
|
+
1. Leave the TODO untouched in the file.
|
|
87
|
+
2. Increment the `escalated` counter in your category breakdown.
|
|
88
|
+
3. Optionally add a one-liner to `notes` explaining what's odd about it.
|
|
89
|
+
|
|
90
|
+
The dispatcher aggregates escalations into a corpus-level review queue for a human.
|
|
@@ -20,6 +20,23 @@ module JsxRosetta
|
|
|
20
20
|
parts[0] + parts[1..].map(&:capitalize).join
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
def upper_camelize(string)
|
|
24
|
+
string.split("_").map(&:capitalize).join
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Best-effort English singularization for plural controller names.
|
|
28
|
+
# Covers the common shapes (`ies`/`y`, `ses`/`s`, `xes`/`x`,
|
|
29
|
+
# `ches`/`ch`, `shes`/`sh`, trailing `s`). Irregular plurals
|
|
30
|
+
# (`people`, `children`, `mice`, `geese`) pass through unchanged —
|
|
31
|
+
# users who hit those rename the generated `as:` in routes.rb.
|
|
32
|
+
def singularize(string)
|
|
33
|
+
case string
|
|
34
|
+
when /(.+[^aeiou])ies\z/i then "#{Regexp.last_match(1)}y"
|
|
35
|
+
when /(.+(?:ss|sh|ch|x|z))es\z/i, /(.+)s\z/i then Regexp.last_match(1)
|
|
36
|
+
else string
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
23
40
|
# Emit a Ruby string literal in the rubocop-default single-quoted
|
|
24
41
|
# form when safe. Falls back to `String#inspect` (double-quoted with
|
|
25
42
|
# escapes) when the source contains characters that prevent the
|