Skip to content

Commit 5c24235

Browse files
authored
Add regression tests for external components in JSX preserve mode (#8612)
Signed-off-by: Christoph Knittel <ck@cca.io>
1 parent bc382a4 commit 5c24235

7 files changed

Lines changed: 118 additions & 1 deletion

File tree

tests/ERROR_VARIANTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ Source: [typecore.ml:27](../compiler/ml/typecore.ml).
207207
| `Or_pattern_type_clash` || `or_pattern_type_clash.res` | |
208208
| `Multiply_bound_variable` || `multiply_bound_variable.res` | |
209209
| `Orpat_vars` || `orpat_vars_unbalanced.res` | |
210-
| `Expr_type_clash` || many `*.res` | Most-fired expression error. Trace-shape sub-cases covered: `if_return_type_mismatch.res` (IfReturn), `maybe_unwrap_option.res` (MaybeUnwrapOption), `string_concat_non_string.res` (StringConcat), `labeled_fn_argument_type_clash.res` (FunctionArgument with explicit label), `math_operator_*.res` (MathOperator family), `ternary_branch_mismatch.res`, `switch_different_types.res`, `try_catch_same_type.res`, `comparison_operator.res`, `array_item_type_mismatch.res`, `array_literal_passed_to_tuple.res`, `if_condition_mismatch.res`, `while_condition.res`, `for_loop_condition.res`, `assert_condition.res`, `function_call_mismatch.res`, `awaiting_non_promise.res`, multiple `jsx_*` fixtures, `object_literal_for_poly_field.res` (object literal against a polymorphic field annotation). |
210+
| `Expr_type_clash` || many `*.res` | Most-fired expression error. Trace-shape sub-cases covered: `if_return_type_mismatch.res` (IfReturn), `maybe_unwrap_option.res` (MaybeUnwrapOption), `string_concat_non_string.res` (StringConcat), `labeled_fn_argument_type_clash.res` (FunctionArgument with explicit label), `math_operator_*.res` (MathOperator family), `ternary_branch_mismatch.res`, `switch_different_types.res`, `try_catch_same_type.res`, `comparison_operator.res`, `array_item_type_mismatch.res`, `array_literal_passed_to_tuple.res`, `if_condition_mismatch.res`, `while_condition.res`, `for_loop_condition.res`, `assert_condition.res`, `function_call_mismatch.res`, `awaiting_non_promise.res`, multiple `jsx_*` fixtures (including `jsx_preserve_external_function.res` for a plain-function external in a JSX component position), `object_literal_for_poly_field.res` (object literal against a polymorphic field annotation). |
211211
| `Apply_non_function` || `apply_non_function.res` | |
212212
| `Apply_wrong_label` || `apply_wrong_label.res` | |
213213
| `Label_multiply_defined` || `label_multiply_defined_literal.res` | |
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// @ts-check
2+
3+
import assert from "node:assert/strict";
4+
import { readFileSync } from "node:fs";
5+
import { setup } from "#dev/process";
6+
7+
const { execBuildOrThrow, execClean } = setup(import.meta.dirname);
8+
9+
await execClean();
10+
await execBuildOrThrow();
11+
12+
const output = readFileSync(new URL("src/Test.jsx", import.meta.url), "utf8");
13+
assert.match(output, /import \* as SomeLib from "some-lib"/);
14+
assert.match(output, /<SomeLib\.Head>\s*\{<div\s*\/>\}\s*<\/SomeLib\.Head>/);
15+
assert.doesNotMatch(output, /=>/);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "jsx-preserve-external",
3+
"sources": ["src"],
4+
"package-specs": {
5+
"module": "esmodule",
6+
"in-source": true,
7+
"suffix": ".jsx"
8+
},
9+
"jsx": {
10+
"module": "Preact",
11+
"preserve": true
12+
},
13+
"compiler-flags": ["-check-lam"]
14+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
type element = Jsx.element
2+
// Custom JSX bindings must use the abstract component type, as React does.
3+
// A function alias here lets function-style externals through and can produce
4+
// invalid preserved JSX, as in the original bindings from issue #8047.
5+
type component<'props> = Jsx.component<'props>
6+
7+
@module("preact/jsx-runtime")
8+
external jsx: (component<'props>, 'props) => element = "jsx"
9+
10+
@module("preact/jsx-runtime")
11+
external jsxs: (component<'props>, 'props) => element = "jsxs"
12+
13+
type domProps = {children?: element}
14+
15+
module Elements = {
16+
external someElement: element => option<element> = "%identity"
17+
18+
@module("preact/jsx-runtime")
19+
external jsx: (string, domProps) => element = "jsx"
20+
21+
@module("preact/jsx-runtime")
22+
external jsxs: (string, domProps) => element = "jsxs"
23+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Regression for https://github.com/rescript-lang/rescript/issues/8047.
2+
// External components must remain imported values, without function wrappers.
3+
module Head = {
4+
type props = {children?: Preact.element}
5+
6+
@module("some-lib")
7+
external make: Preact.component<props> = "Head"
8+
}
9+
10+
let test =
11+
<Head>
12+
<div />
13+
</Head>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/jsx_preserve_external_function.res:30:4-7
4+
5+
28 │
6+
29 │ let test =
7+
30 │ <Head>
8+
31 │ <div />
9+
32 │ </Head>
10+
11+
This JSX tag has type: Head.props => Preact.element
12+
But JSX component positions require:
13+
Preact.component<'a> (defined as Jsx.component<'a>)
14+
15+
JSX tags must be React components, not plain functions.
16+
17+
Possible solutions:
18+
- If this function takes labeled props, annotate it with @react.component
19+
- If this function takes a single props record, annotate it with @react.componentWithProps
20+
- If this is already a valid component-like value, wrap it with React.component(...)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
@@config({flags: ["-bs-jsx-preserve"]})
2+
@@jsxConfig({version: 4, module_: "Preact"})
3+
4+
// With abstract components, the function-style external from #8047 is rejected.
5+
module Preact = {
6+
type element = Jsx.element
7+
type component<'props> = Jsx.component<'props>
8+
9+
@module("preact/jsx-runtime")
10+
external jsx: (component<'props>, 'props) => element = "jsx"
11+
12+
type domProps = {children?: element}
13+
14+
module Elements = {
15+
external someElement: element => option<element> = "%identity"
16+
17+
@module("preact/jsx-runtime")
18+
external jsx: (string, domProps) => element = "jsx"
19+
}
20+
}
21+
22+
module Head = {
23+
type props = {children?: Preact.element}
24+
25+
@module("some-lib")
26+
external make: props => Preact.element = "Head"
27+
}
28+
29+
let test =
30+
<Head>
31+
<div />
32+
</Head>

0 commit comments

Comments
 (0)