[PoC] Rewrite nested JSX component paths to direct hoisted exports#8293
[PoC] Rewrite nested JSX component paths to direct hoisted exports#8293fhammerschmidt wants to merge 12 commits intorescript-lang:masterfrom
Conversation
rescript
@rescript/darwin-arm64
@rescript/darwin-x64
@rescript/linux-arm64
@rescript/linux-x64
@rescript/runtime
@rescript/win32-x64
commit: |
There was a problem hiding this comment.
unfortunately I can't test this PR yet because it messes up imports, let's say I use .res.mjs as output suffix, it should generate import like this:
import * as Button$RescriptShadcn from "../registry/base/ui/Button.res.mjs";
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";but instead, it doesn't use .js for runtime but .mjs:
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.mjs";weird that it doesn't get detected by some tests btw!
|
I found another bug around namespace. JsxRuntime.jsx(Sidebar.Sidebar$Provider, ...)but if I enable namespace, it generates this: JsxRuntime.jsx(Sidebar$RescriptShadcn.Sidebar$RescriptShadcn$Provider, ...)while it should be: JsxRuntime.jsx(Sidebar$RescriptShadcn.Sidebar$Provider, ...)the sub component should not be namespaced. |
There was a problem hiding this comment.
Pull request overview
This PR is a PoC change to the ReScript JSX v4 pipeline so nested JSX component paths (e.g. <Sidebar.Provider>) lower to direct hoisted exports instead of cross-module member access (.make), improving runtime safety in environments like Next.js Server Components.
Changes:
- Mark JSX-generated component path identifiers with a new internal attribute (
res.jsxComponentPath) and preserve that through typed/lambda lowering. - Emit hidden hoisted exports (and
$jsxmarker bindings) for nested component modules, and rewrite transformed JSX runtime calls to target those hoisted exports. - Update/extend compiler test fixtures and add a build test asserting the new JS shape for nested JSX members.
Reviewed changes
Copilot reviewed 51 out of 51 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/tests/src/jsxv4_newtype.mjs | Updates expected JS output to include new $jsx markers and hidden exports. |
| tests/tests/src/jsx_optional_props_test.mjs | Updates expected JS output to include $jsx marker and hidden export for component. |
| tests/tests/src/async_jsx.mjs | Updates expected JS output to include $jsx markers and hidden exports for nested components. |
| tests/tests/src/alias_default_value_test.mjs | Updates expected JS output to include $jsx markers and hidden exports for nested components. |
| tests/syntax_tests/data/ppx/react/expected/v4.res.txt | Updates PPX expected output to include hoisted bindings/markers for nested modules. |
| tests/syntax_tests/data/ppx/react/expected/uncurriedProps.res.txt | Updates PPX output to tag component paths and add hoisted bindings/markers. |
| tests/syntax_tests/data/ppx/react/expected/typeConstraint.res.txt | Adds hoisted binding/marker expectations. |
| tests/syntax_tests/data/ppx/react/expected/topLevel.res.txt | Adds hoisted binding/marker expectations. |
| tests/syntax_tests/data/ppx/react/expected/spreadProps.res.txt | Updates expected output to annotate component-path arguments. |
| tests/syntax_tests/data/ppx/react/expected/sharedProps.res.txt | Adds hoisted binding/marker expectations for nested components. |
| tests/syntax_tests/data/ppx/react/expected/returnConstraint.res.txt | Adds hoisted binding/marker expectations. |
| tests/syntax_tests/data/ppx/react/expected/optionalKeyType.res.txt | Updates expected output to annotate component-path arguments. |
| tests/syntax_tests/data/ppx/react/expected/optimizeAutomaticMode.res.txt | Adds hoisted binding/marker expectations. |
| tests/syntax_tests/data/ppx/react/expected/noPropsWithKey.res.txt | Updates expected output to annotate component-path arguments and adds hoisted bindings/markers. |
| tests/syntax_tests/data/ppx/react/expected/newtype.res.txt | Adds hoisted binding/marker expectations. |
| tests/syntax_tests/data/ppx/react/expected/nested.res.txt | Updates expected output for nested-module component wrappers and component-path annotation. |
| tests/syntax_tests/data/ppx/react/expected/mangleKeyword.res.txt | Updates expected output to annotate component-path arguments and adds hoisted binding/marker. |
| tests/syntax_tests/data/ppx/react/expected/interface.res.txt | Adds hoisted binding/marker expectations. |
| tests/syntax_tests/data/ppx/react/expected/fragment.res.txt | Updates expected output to annotate component-path arguments. |
| tests/syntax_tests/data/ppx/react/expected/forwardRef.res.txt | Updates expected output to annotate component-path arguments and adds hoisted bindings/markers. |
| tests/syntax_tests/data/ppx/react/expected/fileLevelConfig.res.txt | Adds hoisted binding/marker expectations. |
| tests/syntax_tests/data/ppx/react/expected/externalWithCustomName.res.txt | Updates expected output to annotate component-path arguments for external component identifiers. |
| tests/syntax_tests/data/ppx/react/expected/defaultValueProp.res.txt | Adds hoisted binding/marker expectations. |
| tests/syntax_tests/data/ppx/react/expected/asyncAwait.res.txt | Adds hoisted binding/marker expectations. |
| tests/syntax_tests/data/ppx/react/expected/aliasProps.res.txt | Updates expected output to annotate component-path arguments and adds hoisted bindings/markers. |
| tests/gentype_tests/typescript-react-example/src/Hooks.res.js | Updates expected JS output to include $jsx markers and hidden exports. |
| tests/build_tests/rsc_nested_jsx_members/src/Sidebar.res | New build-test source defining nested component modules (Provider, Inset). |
| tests/build_tests/rsc_nested_jsx_members/src/React.res | New minimal React/JSX runtime bindings used by the build test. |
| tests/build_tests/rsc_nested_jsx_members/src/MainLayout.res | New build-test source using <Sidebar.Provider> nested JSX member syntax. |
| tests/build_tests/rsc_nested_jsx_members/rescript.json | New build-test config enabling JSX v4 + namespacing in-source ES modules. |
| tests/build_tests/rsc_nested_jsx_members/input.js | New build-test assertions verifying rewritten JSX tag shape and exports. |
| tests/analysis_tests/tests/src/expected/JsxV4.res.txt | Updates analysis expected output to include hoisted bindings/markers in signatures. |
| tests/analysis_tests/tests/src/expected/CreateInterface.res.txt | Updates analysis expected output to include hoisted binding/marker in signatures. |
| rewatch/tests/watch/06-watch-missing-source-folder.sh | Improves watcher test robustness (wait for shutdown + rebuild/error handling). |
| compiler/syntax/src/jsx_v4.ml | Adds component-path attribute emission and hoisted export generation for nested make. |
| compiler/syntax/src/jsx_ppx.ml | Tracks/appends hoisted structure items during JSX v4 PPX rewriting. |
| compiler/syntax/src/jsx_common.ml | Extends JSX config to store hoisted structure items. |
| compiler/ml/translmod.ml | Extends module field debug info to include jsx_component flag. |
| compiler/ml/translcore.ml | Routes annotated component-path identifiers through transl_jsx_value_path. |
| compiler/ml/printlambda.ml | Updates printer for extended module field debug info. |
| compiler/ml/lambda.mli | Extends Fld_module debug info, adds transl_jsx_value_path API. |
| compiler/ml/lambda.ml | Implements transl_jsx_value_path and Fld_module constructor. |
| compiler/core/lam_print.ml | Updates lambda pretty-printing for extended Fld_module. |
| compiler/core/lam_pass_remove_alias.ml | Updates alias simplification matching for extended Fld_module. |
| compiler/core/lam_compile.ml | Rewrites transformed JSX call tags to use hoisted hidden exports when possible. |
| compiler/core/lam_compat.mli | Mirrors Fld_module debug info change in compatibility layer. |
| compiler/core/lam_compat.ml | Mirrors Fld_module debug info change in compatibility layer. |
| compiler/core/lam_arity_analysis.ml | Updates arity analysis matching for extended Fld_module. |
| compiler/core/lam_analysis.ml | Updates effect analysis matching for extended Fld_module. |
| compiler/core/lam.ml | Updates module-field primitive construction to supply jsx_component=false. |
| compiler/core/js_of_lam_block.ml | Updates JS lowering of module field access to ignore the new flag. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
69b9f7d to
c15a048
Compare
There was a problem hiding this comment.
Pull request overview
This PoC adjusts the JSX v4 compilation pipeline to avoid generating nested component member access (e.g. .Provider.make) at transformed JSX callsites by introducing hoisted “direct export” bindings for nested component modules and rewriting generated JS to use those direct exports when available (to better support environments like RSC/Next.js).
Changes:
- Emit hoisted top-level bindings (plus
$jsxmarker exports) for nested module components so they can be referenced directly from generated JS. - Thread a
@res.jsxComponentPathmarker through the AST → lambda translation so JS lowering can distinguish JSX tag paths from ordinary.makeaccess. - Rewrite transformed JSX runtime calls during JS compilation to use the hoisted export when the
$jsxmarker indicates it exists; add/adjust compiler tests and add a new build test reproducer.
Reviewed changes
Copilot reviewed 58 out of 58 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/tests/src/jsxv4_newtype.mjs | Updates expected JS exports to include hoisted component bindings + $jsx markers. |
| tests/tests/src/jsx_optional_props_test.mjs | Updates expected JS exports for hoisted component binding + $jsx marker. |
| tests/tests/src/async_jsx.mjs | Updates expected JS exports for hoisted component bindings + $jsx markers. |
| tests/tests/src/alias_default_value_test.mjs | Updates expected JS exports for hoisted component bindings + $jsx markers. |
| tests/tests/src/ExternalArity.mjs | Updates expected JS exports for nested external component hoist + $jsx marker. |
| tests/syntax_tests/data/ppx/react/expected/v4.res.txt | Updates expected PPX output to include hoisted bindings + $jsx markers. |
| tests/syntax_tests/data/ppx/react/expected/uncurriedProps.res.txt | Marks React.jsx tag arg with @res.jsxComponentPath; adds hoisted bindings + $jsx markers. |
| tests/syntax_tests/data/ppx/react/expected/typeConstraint.res.txt | Adds hoisted binding + $jsx marker in expected output. |
| tests/syntax_tests/data/ppx/react/expected/topLevel.res.txt | Adds hoisted binding + $jsx marker in expected output. |
| tests/syntax_tests/data/ppx/react/expected/spreadProps.res.txt | Marks component path arg with @res.jsxComponentPath in expected output. |
| tests/syntax_tests/data/ppx/react/expected/sharedProps.res.txt | Adds hoisted bindings + $jsx markers in expected output. |
| tests/syntax_tests/data/ppx/react/expected/returnConstraint.res.txt | Adds hoisted bindings + $jsx markers in expected output. |
| tests/syntax_tests/data/ppx/react/expected/optionalKeyType.res.txt | Marks component path arg with @res.jsxComponentPath in expected output. |
| tests/syntax_tests/data/ppx/react/expected/optimizeAutomaticMode.res.txt | Adds hoisted binding + $jsx marker in expected output. |
| tests/syntax_tests/data/ppx/react/expected/noPropsWithKey.res.txt | Marks component path arg with @res.jsxComponentPath; adds hoisted bindings + $jsx markers. |
| tests/syntax_tests/data/ppx/react/expected/newtype.res.txt | Adds hoisted binding + $jsx marker in expected output. |
| tests/syntax_tests/data/ppx/react/expected/nested.res.txt | Adjusts nested module naming in expected output; adds hoisted bindings + $jsx markers; marks component path. |
| tests/syntax_tests/data/ppx/react/expected/mangleKeyword.res.txt | Marks component path arg with @res.jsxComponentPath; adds hoisted bindings + $jsx markers. |
| tests/syntax_tests/data/ppx/react/expected/interface.res.txt | Adds hoisted bindings + $jsx markers in expected output. |
| tests/syntax_tests/data/ppx/react/expected/fragment.res.txt | Marks component path arg with @res.jsxComponentPath in expected output. |
| tests/syntax_tests/data/ppx/react/expected/forwardRef.res.txt | Marks component path arg with @res.jsxComponentPath; adds hoisted bindings + $jsx markers. |
| tests/syntax_tests/data/ppx/react/expected/fileLevelConfig.res.txt | Adds hoisted binding + $jsx marker in expected output. |
| tests/syntax_tests/data/ppx/react/expected/externalWithTypeVariables.res.txt | Adds hoisted binding + $jsx marker in expected output for external component module. |
| tests/syntax_tests/data/ppx/react/expected/externalWithRef.res.txt | Adds hoisted binding + $jsx marker in expected output for external component module. |
| tests/syntax_tests/data/ppx/react/expected/externalWithCustomName.res.txt | Marks component path arg with @res.jsxComponentPath for custom external component name. |
| tests/syntax_tests/data/ppx/react/expected/defaultValueProp.res.txt | Adds hoisted bindings + $jsx markers in expected output. |
| tests/syntax_tests/data/ppx/react/expected/asyncAwait.res.txt | Adds hoisted bindings + $jsx markers in expected output. |
| tests/syntax_tests/data/ppx/react/expected/aliasProps.res.txt | Marks component path arg with @res.jsxComponentPath; adds hoisted bindings + $jsx markers. |
| tests/gentype_tests/typescript-react-example/src/Hooks.res.js | Updates expected JS exports to include hoisted component bindings + $jsx markers. |
| tests/build_tests/rsc_nested_jsx_members/src/SidebarExternalImpl.js | Adds JS implementation for an external Provider component used in the build test. |
| tests/build_tests/rsc_nested_jsx_members/src/SidebarExternal.res | Adds nested external component module used to reproduce/validate the rewrite. |
| tests/build_tests/rsc_nested_jsx_members/src/Sidebar.res | Adds nested component modules (Provider, Inset) used to reproduce/validate the rewrite. |
| tests/build_tests/rsc_nested_jsx_members/src/React.res | Adds minimal React/jsx-runtime bindings for the build test fixture. |
| tests/build_tests/rsc_nested_jsx_members/src/MainLayoutExternal.res | Uses <SidebarExternal.Provider> to validate external nested module rewrite. |
| tests/build_tests/rsc_nested_jsx_members/src/MainLayout.res | Uses <Sidebar.Provider> to validate nested module rewrite. |
| tests/build_tests/rsc_nested_jsx_members/rescript.json | Adds a namespaced, ESModule build-test project configuration for the fixture. |
| tests/build_tests/rsc_nested_jsx_members/input.js | Adds assertions ensuring generated JS uses hoisted exports (and avoids .Provider.make). |
| tests/build_tests/react_ppx/src/recursive_component_test.res.js | Updates expected JS exports to include hoisted binding + $jsx marker. |
| tests/analysis_tests/tests/src/expected/JsxV4.res.txt | Updates analysis expected output for new hoisted binding declarations + $jsx marker types. |
| tests/analysis_tests/tests/src/expected/CreateInterface.res.txt | Updates analysis expected output for new hoisted binding declarations + $jsx marker types. |
| rewatch/tests/watch/06-watch-missing-source-folder.sh | Makes watcher test more robust by waiting for lock cleanup and handling slow restores. |
| compiler/syntax/src/jsx_v4.ml | Implements hoisting of nested component exports; adds @res.jsxComponentPath tag marker; tracks nested module context. |
| compiler/syntax/src/jsx_ppx.ml | Resets/collects hoisted structure items at top level and appends them after normal items for JSX v4. |
| compiler/syntax/src/jsx_common.ml | Extends JSX config with hoisted_structure_items accumulator. |
| compiler/ml/translmod.ml | Updates module field debug info construction to include jsx_component=false. |
| compiler/ml/translcore.ml | Uses @res.jsxComponentPath to translate JSX tag paths via a dedicated lambda path translation. |
| compiler/ml/printlambda.ml | Updates printing for Fld_module to account for new record field. |
| compiler/ml/lambda.mli | Extends Fld_module debug info with jsx_component: bool; exposes transl_jsx_value_path. |
| compiler/ml/lambda.ml | Implements transl_jsx_path that marks final .make field access; adds fld_module helper. |
| compiler/core/lam_print.ml | Updates lam printer pattern matches for extended Fld_module. |
| compiler/core/lam_pass_remove_alias.ml | Updates alias simplifier pattern matches for extended Fld_module. |
| compiler/core/lam_compile.ml | Adds logic to detect/rewrite nested JSX component tags to hoisted exports when $jsx marker exists. |
| compiler/core/lam_compat.mli | Updates compat type for extended Fld_module. |
| compiler/core/lam_compat.ml | Updates compat type/matching for extended Fld_module. |
| compiler/core/lam_arity_analysis.ml | Updates arity analysis pattern matches for extended Fld_module. |
| compiler/core/lam_analysis.ml | Updates side-effect analysis pattern matches for extended Fld_module. |
| compiler/core/lam.ml | Ensures synthesized module-field accesses default jsx_component=false. |
| compiler/core/js_of_lam_block.ml | Updates module-field codegen to ignore jsx_component flag. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let () = | ||
| maybe_hoist_nested_make_component ~config ~empty_loc ~full_module_name | ||
| fn_name | ||
| in | ||
| (Some props_record_type, binding, new_binding)) |
| (Ext_list.append block args_code, b :: fn_code) | ||
| | {value = None} -> assert false) | ||
| in | ||
| let args = | ||
| if appinfo.ap_transformed_jsx then | ||
| match (appinfo.ap_args, args) with | ||
| | jsx_tag :: _, jsx_expr :: rest_args -> | ||
| rewrite_nested_jsx_component_expr jsx_tag jsx_expr :: rest_args | ||
| | _ -> args | ||
| else args | ||
| in |
| | { | ||
| primitive = Pfield (_, fld_info); | ||
| args = [Lglobal_module (id, dynamic_import)]; |
|
@fhammerschmidt the namespace issue seems to be fixed but not the runtime extension. |
|
Yeah it looks like we need a good repro for this. Codex:
|
|
@cknitt what's the path forward here since you implemented
|
|
@fhammerschmidt my bad, after deleting next cache, node_modules, etc and cleaning mismatch versions of rescript and @rescript/runtime, it seems like the import works, my bad! I'll try again once the PR builds and gets published. |
ReScript currently lowers nested JSX component paths like
<Sidebar.Provider>to cross-module member access in generated JS, for exampleSidebar.Provider.make. That works in many cases, but it breaks in environments like Next.js Server Components, where nested component member access can end up as undefined at runtime even though direct top-level component bindings work.This PR changes JSX v4 lowering so nested component modules also emit a hidden direct export for their underlying component function, and transformed JSX runtime calls use that direct export instead of .make member access when available. That keeps existing source syntax and normal exports intact while producing a safer JS shape for server-
component toolchains.
Example before:
Example after:
Justification
The change crosses several compiler layers because the information needed for the rewrite starts in the JSX transform but
is only actionable during JS lowering.
<Sidebar.Provider>is initially just JSX syntax. To compile it to a direct export safely, the compiler needs to:That is why the PR touches:
So the surface behavior change is small, but the data needed to do it correctly spans multiple stages of the compiler pipeline.
Either way I marked it as a PoC since it might not be the best way to do it.