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
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JsxRosetta
|
|
4
|
+
module IR
|
|
5
|
+
# When a shadcn TSX wraps a Radix primitive like
|
|
6
|
+
# `<SeparatorPrimitive.Root orientation="horizontal" />`, the translator
|
|
7
|
+
# has historically lowered it to a `ComponentInvocation` whose name is
|
|
8
|
+
# the member chain (`"SeparatorPrimitive.Root"`) — which emits as
|
|
9
|
+
# `render SeparatorPrimitive::Root.new(...)`, an undefined-constant
|
|
10
|
+
# NameError at render time.
|
|
11
|
+
#
|
|
12
|
+
# The shapes underneath are stable enough across Radix that we can map
|
|
13
|
+
# the common (LocalImportName, MemberName) pairs to plain HTML elements
|
|
14
|
+
# plus any always-applied attributes. The mapping is keyed on the LOCAL
|
|
15
|
+
# binding name shadcn uses by convention (e.g. `SeparatorPrimitive`),
|
|
16
|
+
# which matches the import-aliasing pattern in every shadcn fork I've
|
|
17
|
+
# surveyed. Unknown pairs fall through to the existing ComponentInvocation
|
|
18
|
+
# / TODO behavior so consumers can hand-shim primitives we don't cover.
|
|
19
|
+
#
|
|
20
|
+
# Source specifiers that count as "Radix" — both the `radix-ui` umbrella
|
|
21
|
+
# package and the older `@radix-ui/react-*` per-primitive packages.
|
|
22
|
+
RADIX_SOURCE_PATTERN = %r{\A(?:radix-ui|@radix-ui/react-[\w-]+)\z}
|
|
23
|
+
|
|
24
|
+
# Local binding names that resolve to Radix's `Slot` primitive.
|
|
25
|
+
# Matches `Slot` (shadcn-v4 umbrella) and `SlotPrimitive` (older
|
|
26
|
+
# convention). Anything looser would silently drop user-defined
|
|
27
|
+
# components whose names happen to start with "Slot".
|
|
28
|
+
SLOT_LOCAL_NAME_PATTERN = /\ASlot(?:Primitive)?\z/
|
|
29
|
+
|
|
30
|
+
module RadixRegistry
|
|
31
|
+
# Each entry maps [PrimitiveBaseName, MemberName] →
|
|
32
|
+
# { tag: <html-tag>, attrs: { <fixed kwarg name> => <value> } }
|
|
33
|
+
# The `attrs` are merged into the element's lowered attributes (the
|
|
34
|
+
# consumer's own kwargs win on conflict — see `merge_radix_attrs`).
|
|
35
|
+
#
|
|
36
|
+
# `PrimitiveBaseName` is the *canonical* Radix primitive name
|
|
37
|
+
# (e.g. `Separator`); the lookup strips an optional `Primitive` suffix
|
|
38
|
+
# from the consumer's local binding before keying in, so both shadcn-v3
|
|
39
|
+
# (`import { Separator as SeparatorPrimitive } from "radix-ui"`) and
|
|
40
|
+
# shadcn-v4 (`import { Separator } from "radix-ui"`) umbrella idioms
|
|
41
|
+
# resolve. Namespace imports (`import * as SeparatorPrimitive from
|
|
42
|
+
# "@radix-ui/react-separator"`) also strip the suffix.
|
|
43
|
+
MAP = {
|
|
44
|
+
# Separator / Label / Avatar / Switch primitives — the shapes we hit
|
|
45
|
+
# most often in the bulk shadcn translation pass.
|
|
46
|
+
%w[Separator Root] => { tag: "div", attrs: { role: "separator" } },
|
|
47
|
+
%w[Label Root] => { tag: "label", attrs: {} },
|
|
48
|
+
%w[Avatar Root] => { tag: "span", attrs: {} },
|
|
49
|
+
%w[Avatar Image] => { tag: "img", attrs: {} },
|
|
50
|
+
%w[Avatar Fallback] => { tag: "span", attrs: {} },
|
|
51
|
+
%w[Switch Root] => { tag: "button", attrs: { type: "button", role: "switch" } },
|
|
52
|
+
%w[Switch Thumb] => { tag: "span", attrs: {} },
|
|
53
|
+
%w[Progress Root] => { tag: "div", attrs: { role: "progressbar" } },
|
|
54
|
+
%w[Progress Indicator] => { tag: "div", attrs: {} },
|
|
55
|
+
# Aspect-ratio + scroll-area roots are presentational containers.
|
|
56
|
+
%w[AspectRatio Root] => { tag: "div", attrs: {} },
|
|
57
|
+
%w[ScrollArea Root] => { tag: "div", attrs: {} },
|
|
58
|
+
%w[ScrollArea Viewport] => { tag: "div", attrs: {} },
|
|
59
|
+
# Tabs primitives — `data-orientation` comes from the JSX attrs;
|
|
60
|
+
# role=tablist on List + role=tab on Trigger + role=tabpanel on Content.
|
|
61
|
+
%w[Tabs Root] => { tag: "div", attrs: {} },
|
|
62
|
+
%w[Tabs List] => { tag: "div", attrs: { role: "tablist" } },
|
|
63
|
+
%w[Tabs Trigger] => { tag: "button", attrs: { type: "button", role: "tab" } },
|
|
64
|
+
%w[Tabs Content] => { tag: "div", attrs: { role: "tabpanel" } },
|
|
65
|
+
# Toggle / ToggleGroup — pressable buttons.
|
|
66
|
+
%w[Toggle Root] => { tag: "button", attrs: { type: "button" } },
|
|
67
|
+
%w[ToggleGroup Root] => { tag: "div", attrs: { role: "group" } },
|
|
68
|
+
%w[ToggleGroup Item] => { tag: "button", attrs: { type: "button" } },
|
|
69
|
+
# Collapsible — presentational containers; the open/closed state is
|
|
70
|
+
# data-state driven by the consumer (Stimulus or otherwise).
|
|
71
|
+
%w[Collapsible Root] => { tag: "div", attrs: {} },
|
|
72
|
+
%w[Collapsible Trigger] => { tag: "button", attrs: { type: "button" } },
|
|
73
|
+
%w[Collapsible Content] => { tag: "div", attrs: {} }
|
|
74
|
+
}.freeze
|
|
75
|
+
|
|
76
|
+
# Strip an optional `Primitive` suffix from the local binding so the
|
|
77
|
+
# registry's canonical names match both umbrella imports (`Separator`)
|
|
78
|
+
# and the older convention (`SeparatorPrimitive`).
|
|
79
|
+
def self.lookup(local_name, member)
|
|
80
|
+
MAP[[local_name, member]] || MAP[[local_name.sub(/Primitive\z/, ""), member]]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
data/lib/jsx_rosetta/ir/types.rb
CHANGED
|
@@ -54,6 +54,14 @@ module JsxRosetta
|
|
|
54
54
|
# Without this capture, references to module-level
|
|
55
55
|
# constants from inside the JSX silently drop and
|
|
56
56
|
# produce unbacked snake_case references at render time.
|
|
57
|
+
# module_imports : [ModuleImport] — top-level `import` declarations.
|
|
58
|
+
# Backends thread the imported names into the
|
|
59
|
+
# ExpressionTranslator so any expression-context
|
|
60
|
+
# reference to an import (e.g. `styles.listContainer`
|
|
61
|
+
# from `import styles from "./X.module.css"`, or
|
|
62
|
+
# `AlertStatusEnum.Pending` from a TS enum import)
|
|
63
|
+
# bails out to a TODO instead of snake-casing to a
|
|
64
|
+
# bare identifier that NameErrors at render time.
|
|
57
65
|
# render_methods : [RenderMethod] — local arrow bindings that return JSX
|
|
58
66
|
# and are invoked from the JSX body (`const renderHeader
|
|
59
67
|
# = () => <div/>; ... {renderHeader()}`). Backends emit
|
|
@@ -67,11 +75,69 @@ module JsxRosetta
|
|
|
67
75
|
# that returns the translated data, instead of `view_template`.
|
|
68
76
|
# JSX inside object properties still extracts to private
|
|
69
77
|
# methods on the class via the IR::Lambda path.
|
|
78
|
+
# server_data_source : ServerDataSource | nil — capture of a
|
|
79
|
+
# Next.js `getServerSideProps` / `getStaticProps`
|
|
80
|
+
# export from the same source file as this component.
|
|
81
|
+
# Body preserved verbatim so the Phlex backend can
|
|
82
|
+
# emit it as a TODO comment block above the class
|
|
83
|
+
# (the matching Rails controller action is where the
|
|
84
|
+
# user ports the data-loading logic). Attached only
|
|
85
|
+
# to the first component when a file contains
|
|
86
|
+
# multiple — Next.js page files have exactly one
|
|
87
|
+
# default-export component, so collisions are rare.
|
|
88
|
+
# hoc_wrappers : [String] — Higher-Order Component wrapper names
|
|
89
|
+
# that the lowering peeled off when finding this
|
|
90
|
+
# component (e.g. `memo`, `forwardRef`, `observer`,
|
|
91
|
+
# `connect`, `withRouter`). Recorded in
|
|
92
|
+
# outside-in order so a `memo(forwardRef(...))`
|
|
93
|
+
# emits as ["memo", "forwardRef"]. Backends surface
|
|
94
|
+
# these as a TODO comment block above the class
|
|
95
|
+
# explaining each wrapper's Rails analog (or
|
|
96
|
+
# lack of one). Empty for components without
|
|
97
|
+
# wrappers — the common case.
|
|
70
98
|
Component = Data.define(:name, :props, :body, :rest_prop_name,
|
|
71
99
|
:local_bindings, :local_binding_names,
|
|
72
|
-
:module_bindings,
|
|
100
|
+
:module_bindings, :module_imports,
|
|
73
101
|
:stimulus_methods, :react_hooks,
|
|
74
|
-
:render_methods, :mode
|
|
102
|
+
:render_methods, :mode, :server_data_source,
|
|
103
|
+
:hoc_wrappers) do
|
|
104
|
+
include Node
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# A Next.js server data-loading export (`getServerSideProps` /
|
|
108
|
+
# `getStaticProps`). Captured at lowering time so the Phlex backend can
|
|
109
|
+
# surface the body as a TODO comment block pointed at the matching
|
|
110
|
+
# Rails controller action. Body is preserved verbatim — no JS-to-Ruby
|
|
111
|
+
# translation is attempted per project rules.
|
|
112
|
+
#
|
|
113
|
+
# hook_name : String — "getServerSideProps" or "getStaticProps".
|
|
114
|
+
# source : String — verbatim JS of the entire export statement.
|
|
115
|
+
ServerDataSource = Data.define(:hook_name, :source) do
|
|
116
|
+
include Node
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# A top-level `import` declaration. Captured at lowering time so the
|
|
120
|
+
# ExpressionTranslator can recognize use-site references and bail out
|
|
121
|
+
# to a TODO instead of emitting a bare snake_case identifier that
|
|
122
|
+
# NameErrors at render time.
|
|
123
|
+
#
|
|
124
|
+
# name : String — the local binding name (the side the source uses
|
|
125
|
+
# to reference the imported value). For `import { foo as bar }`
|
|
126
|
+
# this is "bar"; for `import * as styles` this is "styles";
|
|
127
|
+
# for `import Default` this is "Default".
|
|
128
|
+
# source : String — the module specifier verbatim
|
|
129
|
+
# (e.g. "./styles.module.css", "@apollo/client", "react").
|
|
130
|
+
# Lets backends apply per-source policy later (e.g. always
|
|
131
|
+
# strip `*.module.css` references).
|
|
132
|
+
# kind : Symbol — :default | :named | :namespace.
|
|
133
|
+
# imported_name : String? — original exported name from the source module.
|
|
134
|
+
# For `import { ChevronRight as CR } from "lucide-react"`,
|
|
135
|
+
# `name` is `"CR"` and `imported_name` is `"ChevronRight"`.
|
|
136
|
+
# nil for default / namespace imports where there's no
|
|
137
|
+
# distinct exported name. Backends that look up vendored
|
|
138
|
+
# data by canonical name (icons) need the imported name;
|
|
139
|
+
# most callers want the local binding.
|
|
140
|
+
ModuleImport = Data.define(:name, :source, :kind, :imported_name) do
|
|
75
141
|
include Node
|
|
76
142
|
end
|
|
77
143
|
|
|
@@ -85,6 +151,85 @@ module JsxRosetta
|
|
|
85
151
|
include Node
|
|
86
152
|
end
|
|
87
153
|
|
|
154
|
+
# A module-level call to `cva()` from class-variance-authority. shadcn
|
|
155
|
+
# components ubiquitously use this builder to attach a base class string
|
|
156
|
+
# plus per-axis variant maps to a JSX component. The translator
|
|
157
|
+
# recognizes the pattern at lowering time so backends can emit real
|
|
158
|
+
# Ruby constants (`FOO_BASE_CLASS`, `FOO_VARIANT_CLASSES`) instead of
|
|
159
|
+
# leaving the call as a TODO comment, and so the use-site
|
|
160
|
+
# `cn(fooVariants({ variant }), className)` translates to a proper
|
|
161
|
+
# Ruby string interpolation.
|
|
162
|
+
#
|
|
163
|
+
# name : String — the const binding name (e.g. "buttonVariants").
|
|
164
|
+
# base_class : String — the first string argument to cva().
|
|
165
|
+
# variants : Hash[String => Hash[String => String]]
|
|
166
|
+
# — { "variant" => { "default" => "...", "outline" => "..." } }
|
|
167
|
+
# default_variants : Hash[String => String] — per-axis default value name
|
|
168
|
+
# (matched against the variant axis keys).
|
|
169
|
+
# compound_source : String | nil — INTENTIONALLY UNPARSED verbatim JS
|
|
170
|
+
# source of any `compoundVariants` entry. Field name
|
|
171
|
+
# reads structural; it isn't — backends only print
|
|
172
|
+
# it as a TODO comment alongside the constants since
|
|
173
|
+
# compoundVariants semantics aren't supported in the
|
|
174
|
+
# first cut.
|
|
175
|
+
CvaBinding = Data.define(:name, :base_class, :variants, :default_variants, :compound_source) do
|
|
176
|
+
include Node
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# A literal-shaped module-level `const` declaration that lowers to a real
|
|
180
|
+
# Ruby constant. Distinct from `LocalBinding` (verbatim TODO block) and
|
|
181
|
+
# `CvaBinding` (structured cva variants). The detector accepts initializers
|
|
182
|
+
# whose value reduces to a Ruby-literal-friendly object — strings, numbers,
|
|
183
|
+
# booleans, null, arrays/hashes of the same. Non-literal initializers
|
|
184
|
+
# (call expressions, identifier references, JSX) still fall through to
|
|
185
|
+
# the `LocalBinding` path so their verbatim source surfaces in the
|
|
186
|
+
# module-bindings TODO block.
|
|
187
|
+
#
|
|
188
|
+
# name : String — original JS identifier (e.g. "TAGS", "COLUMNS").
|
|
189
|
+
# constant_name : String — Ruby constant identifier emitted above the class
|
|
190
|
+
# (`AST::Inflector.underscore(name).upcase`). Stored on the
|
|
191
|
+
# IR so future collision-detection has somewhere to bind.
|
|
192
|
+
# value : Object — Ruby-literal-friendly value (String, Integer,
|
|
193
|
+
# Float, true, false, nil, Array of the same, Hash with
|
|
194
|
+
# String keys mapping to the same). Backends call `.inspect`
|
|
195
|
+
# to emit the literal text.
|
|
196
|
+
ModuleConstant = Data.define(:name, :constant_name, :value) do
|
|
197
|
+
include Node
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# A className attribute value that resolves to a known cva binding's
|
|
201
|
+
# call shape — `className={cn(buttonVariants({ variant, size }),
|
|
202
|
+
# className)}` or the no-cn direct form `className={buttonVariants({
|
|
203
|
+
# variant })}`. The translator recognizes the AST shape at lowering
|
|
204
|
+
# so the backend never has to regex over verbatim JS source; this
|
|
205
|
+
# naturally handles literal-pinned axes, reversed arg order, and
|
|
206
|
+
# the cn-vs-no-cn forms.
|
|
207
|
+
#
|
|
208
|
+
# binding_name : String — referenced cva binding's const name.
|
|
209
|
+
# axes : [CvaAxisPair] — preserved in JSX source order.
|
|
210
|
+
# class_arg : Interpolation | nil — the optional trailing className
|
|
211
|
+
# arg from `cn(<cvaCall>, <classArg>)`. Nil for the
|
|
212
|
+
# single-arg `cn(<cvaCall>)` and the no-cn direct
|
|
213
|
+
# forms.
|
|
214
|
+
CvaCallSite = Data.define(:binding_name, :axes, :class_arg) do
|
|
215
|
+
include Node
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# One axis-value pair inside a cva call's options object. The
|
|
219
|
+
# discriminator `kind` tells the backend how to render the value:
|
|
220
|
+
#
|
|
221
|
+
# :prop_ref — JS identifier referencing a prop (`{ variant }` or
|
|
222
|
+
# `{ variant: someProp }`). Backends render as
|
|
223
|
+
# `@snake_case` against the receiving Phlex component.
|
|
224
|
+
# :literal_string — `{ variant: "default" }` — the literal value
|
|
225
|
+
# is the variant-table key.
|
|
226
|
+
# :literal_other — `{ size: 42 }` / `{ active: true }` — Ruby
|
|
227
|
+
# literal passed through to the bracket key.
|
|
228
|
+
# :literal_nil — `{ variant: null }` or `undefined`.
|
|
229
|
+
CvaAxisPair = Data.define(:axis, :kind, :source) do
|
|
230
|
+
include Node
|
|
231
|
+
end
|
|
232
|
+
|
|
88
233
|
# A hook invocation detected in the component body. Covers React's
|
|
89
234
|
# built-in hooks plus framework hooks we recognize (Apollo's `useQuery`/
|
|
90
235
|
# `useMutation`/etc., Next.js's `useRouter`/`usePathname`/etc.).
|
|
@@ -315,7 +460,17 @@ module JsxRosetta
|
|
|
315
460
|
# `name != original_name`, backends emit a collision
|
|
316
461
|
# marker comment in the generated controller JS so the
|
|
317
462
|
# reviewer can see the silent rename.
|
|
318
|
-
|
|
463
|
+
# params : [String | nil] — original arrow/function parameter
|
|
464
|
+
# names (e.g. `["e"]`, `["event"]`, or `[]` for
|
|
465
|
+
# `() => …`). A `nil` entry signals a non-identifier
|
|
466
|
+
# param (destructured `({target}) =>`, rest `(...args) =>`)
|
|
467
|
+
# that the pasted body can't safely reference — backends
|
|
468
|
+
# bail to the TODO form when any entry is nil.
|
|
469
|
+
# body_is_block : Boolean — true when the arrow body was a BlockStatement
|
|
470
|
+
# (`(e) => { … }`), false for an expression-form body
|
|
471
|
+
# (`(e) => doX(e)`). Backends use this to decide whether
|
|
472
|
+
# to strip outer braces when pasting verbatim.
|
|
473
|
+
StimulusMethod = Data.define(:name, :body_source, :original_name, :params, :body_is_block) do
|
|
319
474
|
include Node
|
|
320
475
|
end
|
|
321
476
|
|
|
@@ -369,6 +524,25 @@ module JsxRosetta
|
|
|
369
524
|
include Node
|
|
370
525
|
end
|
|
371
526
|
|
|
527
|
+
# An arrow/function expression whose body isn't JSX — typically a
|
|
528
|
+
# JSX event handler on a PascalCase component (`onClick={() => doX()}`)
|
|
529
|
+
# or a non-JSX-returning callback prop. We can't translate arbitrary
|
|
530
|
+
# JS bodies to Ruby procedurally, so backends emit a stub method on
|
|
531
|
+
# the class with the verbatim JS body preserved as a TODO comment;
|
|
532
|
+
# the kwarg at the use site becomes `method(:method_name)` so the
|
|
533
|
+
# receiving component has a callable reference and the structural
|
|
534
|
+
# attachment is preserved end-to-end.
|
|
535
|
+
#
|
|
536
|
+
# params : [String]
|
|
537
|
+
# body_source : String — verbatim JS of the arrow/function body
|
|
538
|
+
# (the part after `=>` for arrows, or the full block
|
|
539
|
+
# body for FunctionExpressions). Preserved as a
|
|
540
|
+
# comment in the emitted method so the reviewer can
|
|
541
|
+
# translate the behavior to Ruby.
|
|
542
|
+
EventHandler = Data.define(:params, :body_source) do
|
|
543
|
+
include Node
|
|
544
|
+
end
|
|
545
|
+
|
|
372
546
|
# A render-prop child: `<Form.List>{(fields) => <div>{fields}</div>}</Form.List>`.
|
|
373
547
|
# Backends emit this as a Ruby block on the render call, with the params
|
|
374
548
|
# bound as block arguments. Distinct from Loop (which iterates an
|
|
@@ -380,6 +554,16 @@ module JsxRosetta
|
|
|
380
554
|
include Node
|
|
381
555
|
end
|
|
382
556
|
|
|
557
|
+
# The Next.js `_app.tsx` content slot — what JSX writes as
|
|
558
|
+
# `<Component {...pageProps} />`. Lowered to a distinguished node so
|
|
559
|
+
# the Phlex backend can emit `yield` (the Rails layout convention) in
|
|
560
|
+
# its place when the host file is a layout. Outside the layout
|
|
561
|
+
# rendering path the node still emits a yield, since the only way to
|
|
562
|
+
# produce one in our IR is to hit this exact source shape.
|
|
563
|
+
LayoutYield = Data.define do
|
|
564
|
+
include Node
|
|
565
|
+
end
|
|
566
|
+
|
|
383
567
|
# A locally-declared JSX-returning arrow that's invoked inside the
|
|
384
568
|
# render body: `const renderHeader = (count) => <h1>{count}</h1>;
|
|
385
569
|
# ... {renderHeader(headerCount)}`. Backends emit one private method
|
data/lib/jsx_rosetta/ir.rb
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "ir/types"
|
|
4
|
+
require_relative "ir/radix_registry"
|
|
4
5
|
require_relative "ir/lowering"
|
|
5
6
|
|
|
6
7
|
module JsxRosetta
|
|
7
8
|
module IR
|
|
8
|
-
def self.lower(ast_file, source:)
|
|
9
|
-
Lowering.lower(ast_file, source: source)
|
|
9
|
+
def self.lower(ast_file, source:, keep_slot: false)
|
|
10
|
+
Lowering.lower(ast_file, source: source, keep_slot: keep_slot)
|
|
10
11
|
end
|
|
11
12
|
|
|
12
|
-
def self.lower_all(ast_file, source:)
|
|
13
|
-
Lowering.lower_all(ast_file, source: source)
|
|
13
|
+
def self.lower_all(ast_file, source:, keep_slot: false)
|
|
14
|
+
Lowering.lower_all(ast_file, source: source, keep_slot: keep_slot)
|
|
14
15
|
end
|
|
15
16
|
end
|
|
16
17
|
end
|