-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.js
261 lines (217 loc) · 8.06 KB
/
main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
import { WebC } from "@11ty/webc";
import { mkdir, readFile, writeFile } from "node:fs/promises";
/**
* Cleans up the markup to be more React JSX friendly.
* @param {{ snippet: string; isCustomElement: boolean; tag: string; framework: "react" | "astro"}} Markup with metadata
* @returns {string} Reformatted markup.
*/
const sanitizeMarkup = ({ snippet, isCustomElement, tag, framework }) => {
let result = snippet;
if (framework === "react") {
result = result.replaceAll("class=", "className=");
if (isCustomElement) {
// TOOD Come up with a better way to handle this so web component props are not missed
result = result.replaceAll(`<${tag}>`, `<${tag} ref={wc}>`)
}
}
return result;
};
/**
* Extracts the name of the target component from the source WebC component
* @param {object} props - Props of the component
* @returns {{types: string; params: string;} | undefined} Props formatted into types and component props.
*/
const formatProps = (props) => {
if (typeof props === "undefined") {
return undefined;
}
const types = Object.entries(props).map(([ prop, propType ]) => `${prop}: ${propType};`).join(" ");
const params = Object.keys(props).map((prop) => `${prop}`).join(", ");
return {
types: `{${types}}`,
params:`{ ${params} }`,
};
};
/**
* Converts parts of the component including markup, styles and web component JS into a Astro component.
* @param {{ snippet: string; styles: string; name: string; props: string; js: string; tag: string;}} parts - Parts of a component.
* @returns {{ component: string; styles: string; name: string; js: string;}} partsToWrite - Parts to be written to disk.
*/
const toAstroComponent = ({ snippet, styles, name, props, js, tag }) => {
const { types, params } = formatProps(props) ?? {};
const boilerplate = `
---
${ types ? `type Props = ${types};\nconst ${params} = Astro.props;` : "" }
---
${sanitizeMarkup({ snippet, isCustomElement: js !== undefined, tag, framework: "astro"})}
${ styles ? `
<style>
${styles}
</style>
` : ""
}
${ js ? `
<script>
${js}
</script>
` : ""
}
`;
return {
component: boilerplate,
name,
};
};
/**
* Writes parts of the component to disk.
* @param {{ component: string; name: string; styles: string; js: string; outputDir: string;}} parts - Parts required to write component to disk.
*/
const toAstroComponentFile = async ({ component, name, outputDir}) => {
const componentFileName = `${name}.astro`;
const componentDir = `./${outputDir}/astro/components`;
const componentDirUrl = new URL(componentDir, import.meta.url);
await mkdir(componentDirUrl, { recursive: true });
await writeFile(`${componentDir}/${componentFileName}`, component);
};
/**
* Converts parts of the component including markup, styles and web component JS into a React component.
* @param {{ snippet: string; styles: string; name: string; props: string; js: string; tag: string;}} parts - Parts of a component.
* @returns {{ component: string; styles: string; name: string; js: string;}} partsToWrite - Parts to be written to disk.
*/
const toReactComponent = ({ snippet, styles, name, props, js, tag }) => {
const { types, params } = formatProps(props) ?? {};
const boilerplate = `
// This component was auto-generated from a WebC template
${ styles ? `import "./index.css";` : "" }
${ js ? `import "./web-component.js";\nimport { useRef } from 'react';` : "" }
${ types ? `type ${name}Props = ${types};` : "" }
export const ${name} = (${params ?? ""}) => {
${ js ? "const wc = useRef(null);" : "" }
return (
<>
${sanitizeMarkup({ snippet, isCustomElement: js !== undefined, tag, framework: "react"})}
</>
);
}
export default ${name};
`;
return {
component: boilerplate,
styles,
name,
js,
};
};
/**
* Writes parts of the component to disk.
* @param {{ component: string; name: string; styles: string; js: string; outputDir: string;}} parts - Parts required to write component to disk.
*/
const toReactComponentFile = async ({ component, name, styles, js, outputDir}) => {
const componentFileName = "index.tsx";
const stylesFileName = "index.css";
const jsFileName = "web-component.js";
const componentDir = `./${outputDir}/react/components/${name}`;
const componentDirUrl = new URL(componentDir, import.meta.url);
await mkdir(componentDirUrl, { recursive: true });
await writeFile(`${componentDir}/${componentFileName}`, component);
if (styles) {
await writeFile(`${componentDir}/${stylesFileName}`, styles);
}
if (js) {
const boilerplate = `
if(typeof window !== "undefined" && ("customElements" in window)) {
${js}
}
`;
await writeFile(`${componentDir}/${jsFileName}`, boilerplate);
}
};
/**
* Capitalizes the first letter of a work
* @param {string} word - Word to be capitalized.
* @returns {string} capitalizedWord - Capitalized word.
*/
const capitalize = (word) => word[0].toUpperCase() + word.substr(1).toLowerCase();
/**
* Extracts the name of the target component from the source WebC component
* @param {string} path - Path of the WebC component.
* @returns {string | undefined} componentName - Name of the component if regex matches.
*/
const getComponentName = (path) => {
const regex = /\/([^/]+)\.webc$/;
const match = path.match(regex);
if (match) {
const componentName = match[1];
const name = componentName.split("-")
.map(capitalize)
.join("");
return {
name,
tag: componentName,
};
}
return undefined;
};
/**
* Compiles the WebC component to constituent parts with some additional formatting.
* @param {string} path - Path of the WebC component.
* @param {string | undefined} schema - Path to the JSON file with the component props.
* @returns {Promise<{ snippet: string; styles: string; name: string; props: string; js: string; tag: string;}>} parts - Parts of a component.
*/
const compile = async (path, schema) => {
const component = new WebC();
// This enables aggregation of CSS and JS
// As of 0.4.0+ this is disabled by default
component.setBundlerMode(true);
component.defineComponents("./components/**.webc");
const filePath = new URL(path, import.meta.url);
const contents = await readFile(filePath, { encoding: "utf8" });
component.setContent(contents);
// TODO Add error handling for when missing props throws an error
let data, props;
if (schema) {
const schemaPath = new URL(schema, import.meta.url);
const propsContents = await readFile(schemaPath, { encoding: "utf8" });
props = JSON.parse(propsContents);
data = Object.fromEntries(Object.keys(props).map((key) => [`${key}`, `{${key}}`]));
}
let { html, css, js: rawJs, components } = await component.compile(data ? { data } : undefined);
const { name, tag } = getComponentName(path);
let snippet = html.trim();
const js = rawJs?.join("").trim();
if (js?.includes("customElements.define")) {
snippet = `
<${tag}>
${html}
</${tag}>
`;
}
return {
snippet: snippet.trim(),
styles: css[0]?.trim(),
name: name ?? "Component",
props,
js,
tag,
}
};
// const result = await compile("./components/component-with-props.webc", "./components/component-with-props.json");
// const result = await compile("./components/my-greeting.webc");
// await toReactComponentFile({...toReactComponent(result), outputDir: "output", });
// const result = await compile("./components/component-with-props.webc", "./components/component-with-props.json");
// console.log(toAstroComponent(result).component);
const reactify = async (filename, outputDir, props) => {
const result = await compile(filename, props);
await toReactComponentFile({...toReactComponent(result), outputDir, });
};
const astrofy = async (filename, outputDir, props) => {
const result = await compile(filename, props);
await toAstroComponentFile({...toAstroComponent(result), outputDir, });
};
export {
reactify,
astrofy,
compile,
toReactComponent,
toAstroComponent,
};