Skip to content

Commit f119f38

Browse files
authored
Merge pull request #54 from RustLangES/feat/vue-setup
feat: Add Vue setup and showcase support
2 parents 1b3df22 + 2442533 commit f119f38

32 files changed

+2148
-48
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* text=auto eol=lf

js/showcase/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
"types": "./dist/src/react/index.d.ts",
1313
"default": "./dist/react/index.js"
1414
},
15+
"./vue": {
16+
"types": "./dist/src/vue/index.d.ts",
17+
"default": "./dist/vue/index.js"
18+
},
1519
"./styles.css": {
1620
"default": "./dist/styles.css"
1721
}
@@ -32,7 +36,8 @@
3236
},
3337
"peerDependencies": {
3438
"react": "^19.1.0",
35-
"react-dom": "^19.1.0"
39+
"react-dom": "^19.1.0",
40+
"vue": "^3.5.17"
3641
},
3742
"devDependencies": {
3843
"@eslint/js": "^9.28.0",

js/showcase/src/case.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type PropDef = {
1515
disabled?: boolean;
1616
hidden?: boolean;
1717
optional?: boolean;
18+
isSlot?: boolean;
1819
} & (
1920
| {
2021
kind: "raw";
@@ -39,7 +40,7 @@ export type PropDef = {
3940
}
4041
| {
4142
kind: "icon";
42-
default?: boolean;
43+
default?: any;
4344
options?: never[];
4445
}
4546
| {
@@ -58,6 +59,11 @@ export type PropKind = PropDef["kind"];
5859

5960
export interface CaseDef<TComponent> {
6061
props: Record<string, Exclude<PropKind, "raw"> | PropDef>;
62+
slots?: Record<
63+
string,
64+
| Exclude<PropKind, "raw" | "function" | "callback">
65+
| Exclude<PropDef, { kind: "function" | "callback" }>
66+
>;
6167
component: TComponent;
6268
}
6369

@@ -167,8 +173,9 @@ function ShowCaseDef<TComponent, TNode>(
167173
const {
168174
defs: propDefs,
169175
componentProps,
176+
componentSlots,
170177
componentEvents,
171-
} = prepareProps(def.props, showcaseDef);
178+
} = prepareProps(def.props, def.slots, showcaseDef);
172179

173180
const inputs = (
174181
<div
@@ -186,6 +193,7 @@ function ShowCaseDef<TComponent, TNode>(
186193
inputs: showcaseDef.attach(inputs),
187194
component: def.component,
188195
props: componentProps,
196+
slots: componentSlots,
189197
events: componentEvents,
190198
});
191199
}

js/showcase/src/field.tsx

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,53 +13,65 @@ const PROP_KIND_DEFAULTS: { [K in PropKind]: any } = {
1313
function: () => {},
1414
};
1515

16+
type NormalizedProps = Required<PropDef> & { id: string };
17+
1618
function normalizeProp(
1719
propName: string,
18-
prop: CaseDef<unknown>["props"][string]
19-
): Required<PropDef> {
20+
prop: CaseDef<unknown>["props"][string],
21+
isSlot = false
22+
): NormalizedProps {
2023
if (typeof prop === "string") {
2124
return {
25+
id: propName,
2226
displayName: propName,
2327
kind: prop,
2428
disabled: false,
2529
hidden: false,
30+
isSlot,
2631
options: [],
2732
default: PROP_KIND_DEFAULTS[prop],
2833
optional: true,
29-
} satisfies Required<PropDef>;
34+
} satisfies NormalizedProps;
3035
}
3136

3237
return {
38+
id: propName,
3339
default: prop.optional ? undefined : PROP_KIND_DEFAULTS[prop.kind],
3440
displayName: propName,
3541
disabled: false,
3642
hidden: false,
43+
isSlot,
3744
optional: true,
3845
options: [],
3946
...prop,
40-
} satisfies Required<PropDef>;
47+
} satisfies NormalizedProps;
4148
}
4249

4350
export function normalizeProps(
44-
props: CaseDef<unknown>["props"]
45-
): Required<PropDef>[] {
51+
props: CaseDef<unknown>["props"],
52+
isSlot = false
53+
): NormalizedProps[] {
4654
return Object.entries(props).map(([propName, propDef]) =>
47-
normalizeProp(propName, propDef)
55+
normalizeProp(propName, propDef, isSlot)
4856
);
4957
}
5058

5159
export function prepareProps(
5260
props: CaseDef<unknown>["props"],
61+
slots: CaseDef<unknown>["slots"],
5362
showcaseDef: ShowcaseDef<unknown, unknown>
5463
): {
5564
defs: ShowcaseFieldProps[];
5665
componentProps: Record<string, MiniUI.Signal<unknown>>;
66+
componentSlots: Record<string, MiniUI.Signal<unknown>>;
5767
componentEvents: Record<string, MiniUI.Signal<void>>;
5868
} {
5969
const normalizedProps = normalizeProps(props);
70+
const normalizedSlots = normalizeProps(slots ?? {}, /* isSlot */ true);
6071

6172
const defs: ShowcaseFieldProps[] = [];
6273
const componentProps: [string, MiniUI.Signal<unknown>][] = [];
74+
const componentSlots: [string, MiniUI.Signal<unknown>][] = [];
6375
const componentEvents: [string, MiniUI.Signal<void>][] = [];
6476

6577
for (const propDef of normalizedProps) {
@@ -78,7 +90,7 @@ export function prepareProps(
7890
showcaseDef,
7991
});
8092
componentEvents.push([
81-
propDef.displayName,
93+
propDef.id,
8294
() => {
8395
valueSignal(true);
8496
},
@@ -91,13 +103,25 @@ export function prepareProps(
91103
valueSignal,
92104
showcaseDef,
93105
});
94-
componentProps.push([propDef.displayName, valueSignal]);
106+
componentProps.push([propDef.id, valueSignal]);
95107
}
96108
}
97109

110+
for (const slotDef of normalizedSlots) {
111+
const valueSignal = createSignal(slotDef.default);
112+
113+
defs.push({
114+
...slotDef,
115+
valueSignal,
116+
showcaseDef,
117+
});
118+
componentSlots.push([slotDef.id, valueSignal]);
119+
}
120+
98121
return {
99122
defs,
100123
componentProps: Object.fromEntries(componentProps),
124+
componentSlots: Object.fromEntries(componentSlots),
101125
componentEvents: Object.fromEntries(componentEvents),
102126
};
103127
}

js/showcase/src/index.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,7 @@ export interface ShowcaseDef<TComponent, TNode>
1515
instiate(node: TComponent, props: unknown): TNode;
1616
render(node: TComponent): Node;
1717
renderNode(node: TNode): Node;
18-
renderCaseSplitted(props: {
19-
inputs: TNode;
20-
component: TComponent;
21-
props: Record<string, MiniUI.Signal<unknown>>;
22-
events: Record<string, MiniUI.Signal<void>>;
23-
}): TNode;
18+
renderCaseSplitted: TComponent;
2419
attach(node: MiniUI.Node): TNode;
2520
createErrorBoundary(
2621
render: () => TNode,

js/showcase/src/miniui/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export function createEffect(fn: () => void): () => void {
8484
return effect(fn);
8585
}
8686

87-
export function renderH(parent: HTMLElement, node: MiniUI.Node) {
87+
export function renderH(parent: Element, node: MiniUI.Node) {
8888
parent.innerHTML = "";
8989
appendChildren(parent, [node]);
9090
}

js/showcase/src/react/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function setupShowcase(
2121
])
2222
);
2323

24-
createShowcase<(props: unknown) => React.ReactNode, React.ReactNode>({
24+
createShowcase<(props: any) => React.ReactNode, React.ReactNode>({
2525
...config,
2626
icons,
2727
instiate,

js/showcase/src/vue/ErrorBoundary.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import {
2+
ComponentPublicInstance,
3+
defineComponent,
4+
h as vueH,
5+
VNode,
6+
} from "vue";
7+
import { ErrorsDef, ErrorStack } from "../error";
8+
9+
export function createErrorBoundary(
10+
render: () => VNode,
11+
renderErrors: (error: ErrorsDef) => VNode
12+
): VNode {
13+
return vueH(ErrorBoundary, { render, renderErrors });
14+
}
15+
16+
type ErrorBoundary = ComponentPublicInstance<{}, {}, ErrorBoundaryData>;
17+
type ErrorBoundaryData = {
18+
hasError: boolean;
19+
errorsDef: ErrorsDef | null;
20+
};
21+
const ErrorBoundary = defineComponent<{
22+
render: () => VNode;
23+
renderErrors: (error: ErrorsDef) => VNode;
24+
}>({
25+
name: "ErrorBoundary",
26+
props: ["render", "renderErrors"],
27+
data: () =>
28+
({
29+
hasError: false,
30+
errorsDef: null as ErrorsDef | null,
31+
}) as ErrorBoundaryData,
32+
setup({ render, renderErrors }) {
33+
return (ctx: ErrorBoundary) =>
34+
ctx.$data.hasError && ctx.$data.errorsDef != null
35+
? renderErrors(ctx.$data.errorsDef)
36+
: render();
37+
},
38+
errorCaptured(this: ErrorBoundary, err) {
39+
console.log("ERROR CAPTURED", err);
40+
this.$data.hasError = true;
41+
this.$data.errorsDef = errorToDef(err as Error);
42+
return false;
43+
},
44+
});
45+
46+
function errorToDef(error: Error): ErrorsDef {
47+
const stack: ErrorStack[] = (error.stack?.split?.("\n") ?? [])
48+
.map(line => {
49+
if (line.length === 0) {
50+
return;
51+
}
52+
53+
// split by really last @ or @http(s)?://
54+
let [, name, source = ""] = line.match(/(.*)@(https?:\/\/.*)$/) ??
55+
line.match(/(.*)@(:?[^@]*)$/) ?? [, line];
56+
57+
// replace empty string to anonymous call
58+
name = name || "<anonymous>";
59+
60+
// Ignore all the internal functions of react and vite
61+
if (
62+
source.includes("vite/client") ||
63+
name.includes("/node_modules/") ||
64+
name.startsWith("__require")
65+
) {
66+
return;
67+
}
68+
69+
// Remove any URL prefix, leave just path
70+
source = source.startsWith(location.origin)
71+
? source.substring(location.origin.length)
72+
: source;
73+
74+
// Remove node_modules prefix from URL
75+
const NODE_MODULES_DEPS = "/node_modules/.vite/deps/";
76+
source = source.startsWith(NODE_MODULES_DEPS)
77+
? "/node_modules/" +
78+
source.substring(NODE_MODULES_DEPS.length).replace(/_/g, "/")
79+
: source;
80+
81+
const SHOWCASE_DIST = "showcase/dist";
82+
source = source.includes(SHOWCASE_DIST)
83+
? "@showcase" +
84+
source.substring(source.indexOf(SHOWCASE_DIST) + SHOWCASE_DIST.length)
85+
: source;
86+
87+
// match to <source-file>?<timestamp-or-version>:<line>:<column>
88+
const [, sourceFile, lineN, columnN] = source.match(
89+
/^(.+)?(?:t=\d+|v=\w+):(\d+):(\d+)$/
90+
) ?? [, source, "", ""];
91+
92+
if (sourceFile.endsWith("vue.js?")) {
93+
return;
94+
}
95+
96+
return {
97+
line: parseInt(lineN),
98+
column: parseInt(columnN),
99+
name: name,
100+
source: sourceFile,
101+
} satisfies ErrorStack;
102+
})
103+
.filter((e): e is ErrorStack => !!e);
104+
105+
return {
106+
message: error.message,
107+
stack,
108+
};
109+
}

js/showcase/src/vue/index.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import {
2+
ConcreteComponent,
3+
createApp,
4+
defineComponent,
5+
VNode,
6+
h as vueH,
7+
} from "vue";
8+
import { createShowcase, registerCase, ShowcaseConfigDef } from "..";
9+
import { createErrorBoundary } from "./ErrorBoundary";
10+
import {
11+
attach,
12+
instiate,
13+
render,
14+
renderCaseSplitted,
15+
renderNode,
16+
} from "./render";
17+
18+
export function setupShowcase(
19+
config: ShowcaseConfigDef<ConcreteComponent<any>> & {
20+
showcases: Record<string, ConcreteComponent<any>>;
21+
}
22+
) {
23+
const virtualElem = document.createElement("div");
24+
Object.values(config.showcases).forEach(comp => {
25+
createApp(comp).mount(virtualElem);
26+
});
27+
28+
const icons = Object.fromEntries(
29+
Object.entries(config?.icons ?? {}).map(([iconName, iconComp]) => [
30+
iconName,
31+
vueH(iconComp),
32+
])
33+
);
34+
35+
createShowcase<ConcreteComponent<any>, VNode>({
36+
...config,
37+
icons,
38+
instiate,
39+
render,
40+
renderNode,
41+
renderCaseSplitted,
42+
attach,
43+
createErrorBoundary,
44+
});
45+
}
46+
47+
export default defineComponent({
48+
name: "Showcase",
49+
props: ["name", "props", "slots", "component"],
50+
setup(props, { slots }) {
51+
if (slots.default) {
52+
registerCase(props.name, () => slots.default!());
53+
} else {
54+
registerCase(props.name, {
55+
props: props.props,
56+
slots: props.slots,
57+
component: props.component,
58+
});
59+
}
60+
return () => vueH("div");
61+
},
62+
});

0 commit comments

Comments
 (0)