Skip to content

Commit 3ee5647

Browse files
committed
add new feature: narrowed-show
1 parent e59e445 commit 3ee5647

File tree

7 files changed

+168
-28
lines changed

7 files changed

+168
-28
lines changed

.changeset/six-candles-type.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@solid-macros/volar": patch
3+
---
4+
5+
Add new feature: narrowed-show

packages/playground/src/app.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { onMount } from "solid-js";
1+
import { onMount, Show } from "solid-js";
22

33
export default function App() {
44
const div = <div />;
@@ -7,10 +7,15 @@ export default function App() {
77
console.log(div.clientTop);
88
});
99

10+
const nullable = Math.random() > 0.5 ? [] : null;
11+
1012
return (
1113
<main>
1214
Hello world!
1315
{div}
16+
<Show when={nullable} fallback={nullable satisfies null}>
17+
{nullable.length}
18+
</Show>
1419
</main>
1520
);
1621
}

packages/playground/ts-macro.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export default {
44
plugins: [
55
solidMacros({
66
typedDomJsx: true,
7+
narrowedShow: true,
78
}),
89
],
910
};

packages/volar/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,26 @@ console.log(el.clientTop);
3333
// before: type error
3434
// after: correctly typechecked
3535
```
36+
37+
### narrowed-show
38+
39+
Make `<Show>` narrow the types with the condition.
40+
41+
Pass `narrowedShow: true` to the plugin config to enable.
42+
43+
```tsx
44+
const nullableArray: number[] | null = Math.random() > 0.5 ? [0] : null;
45+
46+
<Show
47+
when={nullableArray}
48+
fallback={nullableArray}
49+
// before: number[] | null
50+
// after: null
51+
>
52+
{
53+
nullableArray.length
54+
// before: type error due to nullableArray being number[] | null
55+
// after: nullableArray narrowed to number[], no error
56+
}
57+
</Show>
58+
```
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import type { Code, Context, TsmLanguagePlugin } from "ts-macro";
2+
3+
export const narrowedShow = ({ ts }: Context): TsmLanguagePlugin => ({
4+
name: "@solid-macros/volar/narrowed-show",
5+
resolveVirtualCode({ ast, codes, lang }) {
6+
if (!lang.endsWith("x")) return;
7+
const showCompNames = new Set<string>();
8+
ast.forEachChild(function walk(node) {
9+
if (
10+
ts.isImportSpecifier(node) &&
11+
(node.propertyName ?? node.name).text === "Show"
12+
) {
13+
showCompNames.add(node.name.text);
14+
}
15+
if (
16+
ts.isJsxElement(node) &&
17+
showCompNames.has(node.openingElement.tagName.getText(ast))
18+
) {
19+
const condition = node.openingElement.attributes.properties.find(
20+
(node): node is import("typescript").JsxAttribute =>
21+
ts.isJsxAttribute(node) && node.name.getText(ast) === "when",
22+
);
23+
const fallback = node.openingElement.attributes.properties.find(
24+
(node): node is import("typescript").JsxAttribute =>
25+
ts.isJsxAttribute(node) && node.name.getText(ast) === "fallback",
26+
);
27+
if (condition?.initializer) {
28+
const conditionExpr = ts.isJsxExpression(condition.initializer)
29+
? condition.initializer.expression
30+
: condition.initializer;
31+
const fallbackExpr =
32+
fallback?.initializer &&
33+
(ts.isJsxExpression(fallback?.initializer)
34+
? fallback.initializer.expression
35+
: fallback?.initializer);
36+
codes.replaceRange(
37+
node.pos,
38+
node.end,
39+
// convert to a JsxExpression with a ternary
40+
"{(",
41+
// insert dummy <Show> for...
42+
// - making TS to not incorrectly flag Show as unused
43+
// - displaying correct semantic highlighting
44+
// - providing typechecks and go-to-defs for each props
45+
// - providing autocompletes
46+
["<", node.openingElement.getStart(ast)],
47+
[
48+
node.openingElement.tagName.getText(ast),
49+
node.openingElement.tagName.getStart(ast),
50+
],
51+
...node.openingElement.attributes.properties.flatMap(
52+
(node): Code[] => {
53+
if (ts.isJsxSpreadAttribute(node)) {
54+
return [[node.getFullText(ast), node.pos]];
55+
}
56+
57+
return [
58+
[node.name.getFullText(ast), node.name.pos],
59+
...(node.initializer
60+
? [
61+
"=",
62+
["when", "fallback", "children"].includes(
63+
node.name.getText(ast),
64+
)
65+
? // omit spans as they're used again later
66+
node.initializer.getFullText(ast)
67+
: // provide spans so typescript can work
68+
([
69+
node.initializer.getFullText(ast),
70+
node.initializer.pos,
71+
] satisfies Code),
72+
]
73+
: []),
74+
];
75+
},
76+
),
77+
// insert dummy children for fulfilling typechecks
78+
" children={<></>}",
79+
["/>", node.openingElement.end - 1],
80+
") && ((",
81+
conditionExpr
82+
? [conditionExpr.getText(ast), conditionExpr.getStart(ast)]
83+
: "undefined",
84+
") ? <>",
85+
...node.children.map(
86+
(child): Code => [child.getText(ast), child.getStart(ast)],
87+
),
88+
"</> : ",
89+
fallbackExpr
90+
? [fallbackExpr.getText(ast), fallbackExpr.getStart(ast)]
91+
: "null",
92+
")}",
93+
);
94+
}
95+
}
96+
node.forEachChild(walk);
97+
});
98+
},
99+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { Context, TsmLanguagePlugin } from "ts-macro";
2+
3+
export const typedDomJsx = ({ ts }: Context): TsmLanguagePlugin => ({
4+
name: "@solid-macros/volar/typed-dom-jsx",
5+
resolveVirtualCode({ ast, codes, lang }) {
6+
if (!lang.endsWith("x")) return;
7+
ast.forEachChild(function walk(node) {
8+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
9+
const tagName = ts.isJsxElement(node)
10+
? node.openingElement.tagName
11+
: node.tagName;
12+
if (/^\s*[a-z]/.test(tagName.getText())) {
13+
codes.replaceRange(
14+
node.getFullStart(),
15+
node.getEnd(),
16+
"(",
17+
[node.getText(ast), node.getStart(ast)],
18+
` as HTMLElementTagNameMap["${tagName.getText(ast)}"])`,
19+
);
20+
}
21+
} else if (!ts.isJsxFragment(node)) {
22+
node.forEachChild(walk);
23+
}
24+
});
25+
},
26+
});

packages/volar/src/index.ts

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,17 @@
1-
import { createPlugin, type TsmLanguagePlugin } from "ts-macro";
1+
import { createPlugin } from "ts-macro";
2+
import { narrowedShow } from "./features/narrowedShow";
3+
import { typedDomJsx } from "./features/typedDomJsx";
24

35
interface Options {
46
/** Whether to typecast JSX tags with DOM elements into corresponding HTML elements */
57
typedDomJsx?: boolean;
8+
/** Whether to make `<Show>` narrow the types with the condition */
9+
narrowedShow?: boolean;
610
}
711

8-
export default createPlugin(({ ts }, options: Options | undefined = {}) => {
12+
export default createPlugin<Options | undefined>((ctx, options) => {
913
return [
10-
options.typedDomJsx &&
11-
({
12-
name: "@solid-macros/volar/typed-dom-jsx",
13-
resolveVirtualCode({ ast, codes, lang }) {
14-
if (!lang.endsWith("x")) return;
15-
ast.forEachChild(function walk(node) {
16-
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
17-
const tagName = ts.isJsxElement(node)
18-
? node.openingElement.tagName
19-
: node.tagName;
20-
if (/^\s*[a-z]/.test(tagName.getText())) {
21-
codes.replaceRange(
22-
node.getFullStart(),
23-
node.getEnd(),
24-
"(",
25-
[node.getText(ast), node.getStart(ast)],
26-
` as HTMLElementTagNameMap["${tagName.getText(ast)}"])`,
27-
);
28-
}
29-
} else if (!ts.isJsxFragment(node)) {
30-
node.forEachChild(walk);
31-
}
32-
});
33-
},
34-
} satisfies TsmLanguagePlugin),
14+
options?.typedDomJsx && typedDomJsx(ctx),
15+
options?.narrowedShow && narrowedShow(ctx),
3516
].filter((v) => !!v);
3617
});

0 commit comments

Comments
 (0)