From 81df7433345c8cd14cae8cd66646d77e80dfce76 Mon Sep 17 00:00:00 2001 From: Bagel03 <70449196+Bagel03@users.noreply.github.com> Date: Tue, 24 Oct 2023 09:58:53 -0400 Subject: [PATCH] Add support for JSX Allows usage of JSX across the codebase, JSX is turned into regular HTML elements. --- gulp/webpack.config.js | 8 +++--- gulp/webpack.production.config.js | 7 ++--- src/js/globals.d.ts | 43 +++++++++++++++++++++++++++++++ src/js/jsx-runtime.ts | 41 +++++++++++++++++++++++++++++ src/js/tsconfig.json | 9 ++++++- 5 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 src/js/jsx-runtime.ts diff --git a/gulp/webpack.config.js b/gulp/webpack.config.js index 71ca12fb..a911d2af 100644 --- a/gulp/webpack.config.js +++ b/gulp/webpack.config.js @@ -31,7 +31,7 @@ const moduleRules = [ type: "javascript/auto", }, { - test: /\.js$/, + test: /\.jsx?$/, enforce: "pre", exclude: /node_modules/, use: [ @@ -45,7 +45,7 @@ const moduleRules = [ ], }, { - test: /\.[jt]s$/, + test: /\.[jt]sx?$/, use: [ { loader: "ts-loader", @@ -89,12 +89,14 @@ export default { fallback: { fs: false }, alias: { "global-compression": resolve("../src/js/core/lzstring.js"), + "root": resolve("../src/js/"), }, fullySpecified: false, - extensions: [".ts", ".js"], + extensions: [".ts", ".js", ".tsx", ".jsx"], }, devtool: "cheap-source-map", watch: true, + cache: false, plugins: [ new webpack.DefinePlugin(globalDefs), new webpack.IgnorePlugin({ resourceRegExp: /\.(png|jpe?g|svg)$/ }), diff --git a/gulp/webpack.production.config.js b/gulp/webpack.production.config.js index ad21177b..0e5265e1 100644 --- a/gulp/webpack.production.config.js +++ b/gulp/webpack.production.config.js @@ -34,7 +34,7 @@ const moduleRules = [ type: "javascript/auto", }, { - test: /\.js$/, + test: /\.jsx?$/, enforce: "pre", exclude: /node_modules/, use: [ @@ -56,7 +56,7 @@ const moduleRules = [ ], }, { - test: /\.[jt]s$/, + test: /\.[jt]sx?$/, use: [ { loader: "ts-loader", @@ -100,9 +100,10 @@ export default { fallback: { fs: false }, alias: { "global-compression": resolve("../src/js/core/lzstring.js"), + "root": resolve("../src/js/"), }, fullySpecified: false, - extensions: [".ts", ".js"], + extensions: [".ts", ".js", ".tsx", ".jsx"], }, stats: { optimizationBailout: true }, devtool: false, diff --git a/src/js/globals.d.ts b/src/js/globals.d.ts index 0878d9b9..cf553e2d 100644 --- a/src/js/globals.d.ts +++ b/src/js/globals.d.ts @@ -208,3 +208,46 @@ declare module "worker-loader?inline=true&fallback=false!*" { export default WebpackWorker; } + +// JSX type support - https://www.typescriptlang.org/docs/handbook/jsx.html +// modified from https://stackoverflow.com/a/68238924 +declare namespace JSX { + /** + * The return type of a JSX expression. + * + * In reality, Fragments can return arbitrary values, but we ignore this for convenience. + */ + type Element = HTMLElement; + /** + * Key-value list of intrinsic element names and their allowed properties. + * + * Because children are treated as a property, the Node type cannot be excluded from the index signature. + */ + type IntrinsicElements = { + [K in keyof HTMLElementTagNameMap]: { + children?: Node | Node[]; + [k: string]: Node | Node[] | string | number | boolean; + }; + }; + /** + * The property of the attributes object storing the children. + */ + type ElementChildrenAttribute = { children: unknown }; + + // The following do not have special meaning to TypeScript. + + /** + * An attributes object. + */ + type Props = { [k: string]: unknown }; + /** + * A functional component requiring attributes to match `T`. + */ + type Component = { + (props: T): Element; + }; + /** + * A child of a JSX element. + */ + type Node = Element | string | boolean | null | undefined; +} diff --git a/src/js/jsx-runtime.ts b/src/js/jsx-runtime.ts new file mode 100644 index 00000000..be285029 --- /dev/null +++ b/src/js/jsx-runtime.ts @@ -0,0 +1,41 @@ +function isDisplayed(node: JSX.Node): node is Exclude { + return typeof node !== "boolean" && node != null; +} + +/** + * JSX factory. + */ +function jsx(tag: T, props: JSX.IntrinsicElements[T]): HTMLElement; +function jsx(tag: JSX.Component, props: U): Element; +function jsx( + tag: keyof JSX.IntrinsicElements | JSX.Component, + props: U +): JSX.Element { + if (typeof tag === "function") return tag(props); + + const { children, ...attrs } = props as JSX.IntrinsicElements[keyof JSX.IntrinsicElements]; + + const element = document.createElement(tag); + Object.entries(attrs).forEach(([key, value]) => { + switch (typeof value) { + case "boolean": + if (!value) return; + return element.setAttribute(key, ""); + case "number": + case "string": + return element.setAttribute(key, `${value}`); + } + throw new TypeError("JSX element attribute assigned invalid type"); + }); + element.append(...([children].flat(Infinity) as JSX.Node[]).filter(isDisplayed)); + return element; +} + +// functional component, called indirectly as `jsx(Fragment, props)` +/** + * Groups elements without introducing a parent element. + */ +const Fragment = (props: JSX.Props) => props.children as JSX.Element; + +// jsxs is used when there are multiple children +export { jsx, jsx as jsxs, Fragment }; diff --git a/src/js/tsconfig.json b/src/js/tsconfig.json index 4d705825..423282e5 100644 --- a/src/js/tsconfig.json +++ b/src/js/tsconfig.json @@ -31,12 +31,19 @@ /* Module Resolution Options */ "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + "paths": { + "root/*": ["./*"] + }, + + /* JSX Compilation */ + "jsx": "react-jsx", + "jsxImportSource": "root", /* Experimental Options */ "resolveJsonModule": true, "skipLibCheck": true, "skipDefaultLibCheck": true }, - "include": ["./**/*.js", "./**/*.ts"], + "include": ["./**/*.js", "./**/*.ts", "./**/*.jsx", "./**/*.tsx"], "exclude": ["webworkers", "node_modules"] }