not found");
10 | }
11 |
12 | hydrate(tree, root);
13 |
--------------------------------------------------------------------------------
/examples/website/src/jsx.ts:
--------------------------------------------------------------------------------
1 | declare namespace JSX {
2 | interface IntrinsicElements {
3 | [elemName: string]: any;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/examples/website/src/server.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 | const express = require("express");
4 | const { createServer: createViteServer } = require("vite");
5 |
6 | async function createServer() {
7 | const app = express();
8 |
9 | // Create vite server in middleware mode. This disables Vite's own HTML
10 | // serving logic and let the parent server take control.
11 | //
12 | // If you want to use Vite's own HTML serving logic (using Vite as
13 | // a development middleware), using 'html' instead.
14 | const vite = await createViteServer({
15 | server: { middlewareMode: "ssr" },
16 | });
17 | // use vite's connect instance as middleware
18 | app.use(vite.middlewares);
19 |
20 | app.use("*", async (req, res) => {
21 | const url = req.originalUrl;
22 |
23 | try {
24 | // 1. Read index.html
25 | let template = fs.readFileSync(
26 | path.resolve(__dirname, "..", "index.html"),
27 | "utf-8"
28 | );
29 |
30 | // 2. Apply vite HTML transforms. This injects the vite HMR client, and
31 | // also applies HTML transforms from Vite plugins, e.g. global preambles
32 | // from @vitejs/plugin-react-refresh
33 | template = await vite.transformIndexHtml(url, template);
34 |
35 | // 3. Load the server entry. vite.ssrLoadModule automatically transforms
36 | // your ESM source code to be usable in Node.js! There is no bundling
37 | // required, and provides efficient invalidation similar to HMR.
38 | const { render } = await vite.ssrLoadModule(
39 | path.resolve("/src/entry-server.js")
40 | );
41 |
42 | // 4. render the app HTML. This assumes entry-server.js's exported `render`
43 | // function calls appropriate framework SSR APIs,
44 | // e.g. ReactDOMServer.renderToString()
45 | const appHtml = render(url);
46 |
47 | // 5. Inject the app-rendered HTML into the template.
48 | const html = template.replace(``, appHtml);
49 |
50 | // 6. Send the rendered HTML back.
51 | res.status(200).set({ "Content-Type": "text/html" }).end(html);
52 | } catch (e) {
53 | // If an error is caught, let vite fix the stracktrace so it maps back to
54 | // your actual source code.
55 | vite.ssrFixStacktrace(e);
56 | console.error(e);
57 | res.status(500).end(e.message);
58 | }
59 | });
60 |
61 | app.listen(3000);
62 | }
63 |
64 | createServer();
65 |
--------------------------------------------------------------------------------
/examples/website/src/ui/AutoScale.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | createElement as c,
3 | RElement,
4 | useEffect,
5 | useRef,
6 | } from "../../../../packages/remini/lib";
7 |
8 | const AutoScale = (props: any): RElement => {
9 | const ref = useRef
();
10 |
11 | const resize = () => {
12 | if (ref.current === null) {
13 | return;
14 | }
15 |
16 | ref.current.style.height = "auto";
17 | ref.current.style.height = `${ref.current.scrollHeight}px`;
18 | };
19 |
20 | useEffect(() => {
21 | resize();
22 | }, []);
23 |
24 | return (
25 |
35 | );
36 | };
37 |
38 | export default AutoScale;
39 |
--------------------------------------------------------------------------------
/examples/website/src/ui/Button.tsx:
--------------------------------------------------------------------------------
1 | import { createElement as c, RElement } from "../../../../packages/remini/lib";
2 |
3 | const Button = ({ children, loading, ...props }: any): RElement => {
4 | return (
5 |
13 | );
14 | };
15 |
16 | export default Button;
17 |
--------------------------------------------------------------------------------
/examples/website/src/ui/Input.tsx:
--------------------------------------------------------------------------------
1 | import { createElement as c, RElement } from "../../../../packages/remini/lib";
2 | import { TEXT_PRIMARY } from "../constants";
3 |
4 | const Input = ({ loading, ...props }: any): RElement => {
5 | return (
6 |
7 |
12 |
13 | );
14 | };
15 |
16 | export default Input;
17 |
--------------------------------------------------------------------------------
/examples/website/src/ui/Label.tsx:
--------------------------------------------------------------------------------
1 | import { createElement as c, RElement } from "../../../../packages/remini/lib";
2 | import { TEXT_PRIMARY } from "../constants";
3 |
4 | const Label = ({ children, ...props }: any): RElement => {
5 | return (
6 |
9 | );
10 | };
11 |
12 | export default Label;
13 |
--------------------------------------------------------------------------------
/examples/website/src/ui/Spinner.tsx:
--------------------------------------------------------------------------------
1 | import { createElement as c, RElement } from "../../../../packages/remini/lib";
2 |
3 | const Spinner = (): RElement => {
4 | return (
5 |
25 | );
26 | };
27 |
28 | export default Spinner;
29 |
--------------------------------------------------------------------------------
/examples/website/src/utils/date.ts:
--------------------------------------------------------------------------------
1 | const SECOND = 1000;
2 | const MINUTE = 60 * SECOND;
3 | const HOUR = 60 * MINUTE;
4 | const DAY = 24 * HOUR;
5 | const MONTH = 30 * DAY;
6 | const YEAR = 12 * MONTH;
7 |
8 | const format = (ms: number, abbreviation: string, unit: number) => {
9 | const amount = Math.floor(ms / unit);
10 |
11 | return `${amount}${abbreviation}`;
12 | };
13 |
14 | export const getFriendlyTime = (date: Date): string => {
15 | const ms = Date.now() - date.getTime();
16 | if (isNaN(ms) || !isFinite(ms)) {
17 | throw new Error(`Wrong value provided: ${ms}`);
18 | }
19 |
20 | if (ms < SECOND) {
21 | return "second ago";
22 | } else if (ms < MINUTE) {
23 | const amount = Math.floor(ms / SECOND);
24 | if (amount < 2) {
25 | return `second ago`;
26 | }
27 | return `few seconds ago`;
28 | } else if (ms < HOUR) {
29 | return format(ms, "m", MINUTE);
30 | } else if (ms < DAY) {
31 | return format(ms, "h", HOUR);
32 | } else if (ms < MONTH) {
33 | return format(ms, "d", DAY);
34 | } else if (ms < YEAR) {
35 | return format(ms, "m", MONTH);
36 | } else {
37 | return format(ms, "y", YEAR);
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/examples/website/style.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/examples/website/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | purge: ["./src/**/*.ts"],
3 | darkMode: false, // or 'media' or 'class'
4 | theme: {
5 | extend: {},
6 | },
7 | variants: {},
8 | plugins: [],
9 | };
10 |
--------------------------------------------------------------------------------
/examples/website/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "ES2020",
4 | "noImplicitAny": true,
5 | "removeComments": true,
6 | "preserveConstEnums": true,
7 | "sourceMap": true,
8 | "strictNullChecks": true,
9 | "jsx": "react",
10 | "jsxFactory": "c",
11 | "jsxFragmentFactory": "Fragment"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/examples/website/vite.config.js:
--------------------------------------------------------------------------------
1 | import remini from "../../packages/remini-plugin";
2 | import refresh from "../../packages/vite-plugin";
3 |
4 | export default {
5 | plugins: [remini(), refresh()],
6 | };
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "workspaces": [
4 | "packages/*"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/packages/babel-plugin/babel.config.js:
--------------------------------------------------------------------------------
1 | const importMetaPreset = require("../import-meta-preset");
2 |
3 | module.exports = {
4 | presets: [
5 | importMetaPreset,
6 | ["@babel/preset-env", { targets: { node: "current" } }],
7 | "@babel/preset-typescript",
8 | ],
9 | };
10 |
--------------------------------------------------------------------------------
/packages/babel-plugin/index.js:
--------------------------------------------------------------------------------
1 | function isComponentLikeName(name) {
2 | return typeof name === "string" && /^[A-Z]/.test(name);
3 | }
4 |
5 | export default function babelPlugin(babel) {
6 | const { types: t } = babel;
7 |
8 | const refreshReg = t.identifier("$RefreshReg$");
9 |
10 | const hookCalls = new Map();
11 |
12 | function getHookCallsSignature(functionNode) {
13 | const fnHookCalls = hookCalls.get(functionNode);
14 |
15 | if (fnHookCalls === undefined) {
16 | return null;
17 | }
18 |
19 | return {
20 | key: fnHookCalls.map((call) => call.key).join(";"),
21 | };
22 | }
23 |
24 | const HookSearcher = {
25 | CallExpression(path) {
26 | const node = path.node;
27 | const callee = node.callee;
28 |
29 | let name = null;
30 | switch (callee.type) {
31 | case "Identifier":
32 | name = callee.name;
33 | break;
34 | case "MemberExpression":
35 | name = callee.property.name;
36 | break;
37 | }
38 |
39 | if (name === null || !/^use[A-Z]/.test(name)) {
40 | return;
41 | }
42 |
43 | const fnScope = path.scope.getFunctionParent();
44 | const fnNode = fnScope.block;
45 |
46 | if (!hookCalls.has(fnNode)) {
47 | hookCalls.set(fnNode, []);
48 | }
49 | const hookCallsForFn = hookCalls.get(fnNode);
50 |
51 | let key = "";
52 | if (path.parent.type === "VariableDeclarator") {
53 | // TODO: if there is no LHS, consider some other heuristic.
54 | key = path.parentPath.get("id").getSource();
55 | }
56 |
57 | const args = path.get("arguments");
58 |
59 | if (name === "useState" && args.length > 0) {
60 | // useState first argument is initial state.
61 | key += " = useState(" + args[0].getSource() + ")";
62 | }
63 |
64 | // TODO
65 | // if (name === 'useReducer' && args.length > 1) {
66 | // // useReducer second argument is initial state.
67 | // key += '(' + args[1].getSource() + ')';
68 | // }
69 |
70 | hookCallsForFn.push({
71 | callee: path.node.callee,
72 | name,
73 | key,
74 | });
75 | },
76 | };
77 |
78 | const analyzeBody = (path, name, insertPath) => {
79 | const fnNode = path.node;
80 | const hooks = getHookCallsSignature(fnNode);
81 |
82 | // $RefreshReg$(A, $id$(), "A", "[counter, setCounter] = useState(0)");
83 | insertPath.insertAfter(
84 | t.callExpression(
85 | refreshReg,
86 | [
87 | t.identifier(name),
88 | t.callExpression(t.identifier("$id$"), []),
89 | t.stringLiteral(name),
90 | hooks && t.stringLiteral(hooks.key),
91 | ].filter(Boolean)
92 | )
93 | );
94 | };
95 |
96 | return {
97 | visitor: {
98 | ExportDefaultDeclaration(path) {
99 | //
100 | },
101 | FunctionDeclaration: {
102 | exit(path) {
103 | const name = path.node.id.name;
104 |
105 | if (!isComponentLikeName(name)) {
106 | return;
107 | }
108 |
109 | analyzeBody(path, name, path);
110 | },
111 | },
112 | ExportNamedDeclaration(path) {
113 | //
114 | },
115 | VariableDeclaration(path) {
116 | const name = path.node.declarations[0].id.name;
117 |
118 | if (!isComponentLikeName(name)) {
119 | return;
120 | }
121 |
122 | // switch (path.parent.type) {
123 | // case "Program":
124 | // console.log("Program");
125 | // break;
126 | // case "ExportNamedDeclaration":
127 | // console.log("ExportNamedDeclaration");
128 | // break;
129 | // case "ExportDefaultDeclaration":
130 | // console.log("ExportDefaultDeclaration");
131 | // break;
132 | // }
133 |
134 | const declarations = path.get("declarations")[0];
135 | const init = declarations.get("init");
136 |
137 | if (
138 | init.type !== "ArrowFunctionExpression" &&
139 | init.type !== "FunctionExpression"
140 | ) {
141 | return;
142 | }
143 |
144 | analyzeBody(init, name, path);
145 | },
146 | Program: {
147 | enter(path) {
148 | path.traverse(HookSearcher);
149 | },
150 | exit() {
151 | hookCalls.clear();
152 | },
153 | },
154 | },
155 | };
156 | }
157 |
--------------------------------------------------------------------------------
/packages/babel-plugin/index.test.js:
--------------------------------------------------------------------------------
1 | import { transformSync } from "@babel/core";
2 | import babelPlugin from ".";
3 |
4 | const transform = (code) => {
5 | return transformSync(code, {
6 | babelrc: false,
7 | configFile: false,
8 | parserOpts: {
9 | sourceType: "module",
10 | allowAwaitOutsideFunction: true,
11 | plugins: [],
12 | },
13 | plugins: [babelPlugin],
14 | ast: true,
15 | });
16 | };
17 |
18 | const source = `
19 | import { createElement as c, useState } from "../../packages/remini/lib";
20 |
21 | const trap = () => {
22 | return "test";
23 | };
24 |
25 | const local = "a";
26 |
27 | const A = () => {
28 | const [counter, setCounter] = useState(0);
29 | const onClick = () => setCounter(counter + 1);
30 |
31 | return c(
32 | "div",
33 | {},
34 | c("button", { onClick }, \`Clicked \${counter} times! \${secondState}\`)
35 | );
36 | };
37 |
38 | function B() {
39 | return c("div", {}, "B");
40 | }
41 |
42 | const C = function () {
43 | return c("div", "C");
44 | };
45 |
46 | export const D = () => {
47 | return c("div", "D");
48 | };
49 |
50 | export const E = function () {
51 | return c("div", "E");
52 | };
53 |
54 | export default A;
55 | `;
56 |
57 | const expected = `
58 | import { createElement as c, useState } from "../../packages/remini/lib";
59 |
60 | const trap = () => {
61 | return "test";
62 | };
63 |
64 | const local = "a";
65 |
66 | const A = () => {
67 | const [counter, setCounter] = useState(0);
68 |
69 | const onClick = () => setCounter(counter + 1);
70 |
71 | return c("div", {}, c("button", {
72 | onClick
73 | }, \`Clicked \${counter} times! \${secondState}\`));
74 | };
75 |
76 | $RefreshReg$(A, $id$(), "A", "[counter, setCounter] = useState(0)")
77 |
78 | function B() {
79 | return c("div", {}, "B");
80 | }
81 |
82 | $RefreshReg$(B, $id$(), "B")
83 |
84 | const C = function () {
85 | return c("div", "C");
86 | };
87 |
88 | $RefreshReg$(C, $id$(), "C")
89 | export const D = () => {
90 | return c("div", "D");
91 | };
92 | $RefreshReg$(D, $id$(), "D");
93 | export const E = function () {
94 | return c("div", "E");
95 | };
96 | $RefreshReg$(E, $id$(), "E");
97 | export default A;
98 | `.trim();
99 |
100 | describe("Babel plugin", () => {
101 | it("works with various function declaration methods", () => {
102 | const result = transform(source);
103 | expect(result.code).toBe(expected);
104 | });
105 | });
106 |
--------------------------------------------------------------------------------
/packages/babel-plugin/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "babel-plugin",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "test": "jest --watch --env=jsdom"
6 | },
7 | "devDependencies": {
8 | "@babel/core": "^7.14.8",
9 | "@babel/preset-env": "^7.14.8",
10 | "@babel/preset-typescript": "^7.15.0",
11 | "jest": "^27.0.6"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/babel-plugin/runtime.js:
--------------------------------------------------------------------------------
1 | // TODO
2 | // Make sure all steps from https://overreacted.io/my-wishlist-for-hot-reloading/#correctness
3 | // and later paragraphs are covered.
4 | // (for example error recovery is not done for sure).
5 |
6 | let pendingUpdates = [];
7 | let isPerformingRefresh = false;
8 |
9 | function injectIntoGlobalHook() {
10 | if (!window.__UPDATE__ || !window.__COMPONENT_TO_NODE__) {
11 | throw new Error("Cannot use fast refresh. Missing global hooks.");
12 | }
13 | }
14 |
15 | // Also known as $RefreshReg$.
16 | function register(render, id, name, hooks) {
17 | if (render === null) {
18 | return;
19 | }
20 |
21 | render.$id$ = `${id}-${name}`;
22 | render.$hooks$ = hooks;
23 | pendingUpdates.push([render.$id$, render, hooks]);
24 | }
25 |
26 | function performRefresh() {
27 | if (pendingUpdates.length === 0) {
28 | return;
29 | }
30 |
31 | if (isPerformingRefresh) {
32 | return;
33 | }
34 |
35 | isPerformingRefresh = true;
36 | pendingUpdates.forEach(([id, render, hooks]) => {
37 | const nodes = __COMPONENT_TO_NODE__.get(id);
38 |
39 | if (!nodes) {
40 | return;
41 | }
42 |
43 | nodes.forEach((node) => {
44 | if (node.hooks) {
45 | for (let i = 0; i < node.hooks.length; i++) {
46 | if (node.hooks[i].type === 0) {
47 | // 1 means HookType.STATE
48 | // Reset state.
49 | if (node.render.$hooks$ !== hooks) {
50 | node.hooks[i] = undefined;
51 | }
52 | } else if (node.hooks[i].type === 1) {
53 | // 2 means HookType.EFFECT
54 | const callback = node.hooks[i].cleanup;
55 | if (typeof callback === "function") {
56 | callback();
57 | }
58 | node.hooks[i] = undefined;
59 | } else if (node.hooks[i].type === 2) {
60 | // 3 means HookType.REF
61 | // createElement(...) structure could be changed. Better regenerate.
62 | node.hooks[i] = undefined;
63 | } else if (node.hooks[i].type === 3) {
64 | // 4 means HookType.CONTEXT
65 | // Probably no action needed.
66 | } else if (node.hooks[i].type === 4) {
67 | // 5 means HookType.MEMO
68 | // Run memos with deps again.
69 | node.hooks[i] = undefined;
70 | }
71 | }
72 | }
73 |
74 | node.render = render;
75 | window.__UPDATE__(node, null);
76 | });
77 | });
78 | pendingUpdates = [];
79 | isPerformingRefresh = false;
80 | }
81 |
82 | export default {
83 | injectIntoGlobalHook,
84 | register,
85 | performRefresh,
86 | };
87 |
--------------------------------------------------------------------------------
/packages/babel-plugin/runtime.test.js:
--------------------------------------------------------------------------------
1 | import RefreshRuntime from "./runtime";
2 | import {
3 | createElement as c,
4 | render,
5 | useEffect,
6 | useMemo,
7 | useRef,
8 | useState,
9 | } from "../remini/lib";
10 |
11 | beforeEach(() => {
12 | document.body.innerHTML = "";
13 | });
14 |
15 | describe("Fast refresh runtime", () => {
16 | it("works with simple component", () => {
17 | const App = () => {
18 | return c("div", {}, "abc");
19 | };
20 | RefreshRuntime.register(App, "/User/test/Desktop/App.js", "App");
21 | RefreshRuntime.performRefresh();
22 |
23 | render(c(App), document.body);
24 |
25 | expect(document.body.innerHTML).toBe("abc
");
26 |
27 | const AppUpdated = () => {
28 | return c("div", {}, "abcde");
29 | };
30 |
31 | RefreshRuntime.register(AppUpdated, "/User/test/Desktop/App.js", "App");
32 | RefreshRuntime.performRefresh();
33 |
34 | expect(document.body.innerHTML).toBe("abcde
");
35 | });
36 |
37 | it("works with useState hook", () => {
38 | const App = () => {
39 | const [counter, setCounter] = useState(0);
40 | const onClick = () => setCounter(counter + 1);
41 | return c(
42 | "button",
43 | { onClick, id: "button" },
44 | `Clicked ${counter} times!`
45 | );
46 | };
47 | RefreshRuntime.register(
48 | App,
49 | "/User/test/Desktop/App.js",
50 | "App",
51 | "[counter, setCounter] = useState(0)"
52 | );
53 |
54 | render(c(App), document.body);
55 | RefreshRuntime.performRefresh();
56 |
57 | expect(document.body.innerHTML).toBe(
58 | ''
59 | );
60 |
61 | const button = document.getElementById("button");
62 | button.click();
63 | button.click();
64 |
65 | expect(document.body.innerHTML).toBe(
66 | ''
67 | );
68 |
69 | const AppUpdated = () => {
70 | const [counter, setCounter] = useState(10);
71 | const onClick = () => setCounter(counter + 1);
72 | return c(
73 | "button",
74 | { onClick, id: "button" },
75 | `Clicked ${counter} times!`
76 | );
77 | };
78 |
79 | RefreshRuntime.register(
80 | AppUpdated,
81 | "/User/test/Desktop/App.js",
82 | "App",
83 | "[counter, setCounter] = useState(10)"
84 | );
85 | RefreshRuntime.performRefresh();
86 |
87 | expect(document.body.innerHTML).toBe(
88 | ''
89 | );
90 | });
91 |
92 | it("cleans up and re-runs useEffects properly", () => {
93 | // TODO
94 | // Finish it and implement missing parts.
95 |
96 | const runOnEffect = jest.fn();
97 | const runOnCleanUp = jest.fn();
98 |
99 | const App = () => {
100 | useEffect(() => {
101 | runOnEffect();
102 | return () => {
103 | runOnCleanUp();
104 | };
105 | }, []);
106 |
107 | return c("div", {}, "abc");
108 | };
109 | RefreshRuntime.register(App, "/User/test/Desktop/App.js", "App");
110 |
111 | render(c(App), document.body);
112 | RefreshRuntime.performRefresh();
113 |
114 | expect(document.body.innerHTML).toBe("abc
");
115 | expect(runOnEffect).toHaveBeenCalledTimes(2);
116 |
117 | const UpdatedApp = () => {
118 | useEffect(() => {
119 | runOnEffect();
120 | return () => {
121 | runOnCleanUp();
122 | };
123 | }, []);
124 |
125 | return c("div", {}, "abcde");
126 | };
127 | RefreshRuntime.register(UpdatedApp, "/User/test/Desktop/App.js", "App");
128 | RefreshRuntime.performRefresh();
129 |
130 | expect(document.body.innerHTML).toBe("abcde
");
131 | expect(runOnEffect).toHaveBeenCalledTimes(3);
132 | expect(runOnCleanUp).toHaveBeenCalledTimes(2);
133 | });
134 |
135 | it("handles memo and ref hooks", () => {
136 | //
137 | });
138 |
139 | it("doesn't destroy state of siblings or parent", () => {
140 | const Parent = ({ children }) => {
141 | const [counter, setCounter] = useState(0);
142 | const onClick = () => setCounter(counter + 1);
143 | return c(
144 | "div",
145 | {},
146 | c("button", { id: "parent", onClick }, `Parent clicked: ${counter}`),
147 | children
148 | );
149 | };
150 |
151 | const Sibling = () => {
152 | const [counter, setCounter] = useState(0);
153 | const onClick = () => setCounter(counter + 1);
154 | return c(
155 | "button",
156 | { id: "sibling", onClick },
157 | `Sibling clicked: ${counter}`
158 | );
159 | };
160 |
161 | const Current = () => {
162 | return c("div", {}, "abc");
163 | };
164 |
165 | RefreshRuntime.register(Current, "/User/test/Desktop/index.js", "Current");
166 | render(c(Parent, {}, c(Current), c(Sibling)), document.body);
167 | RefreshRuntime.performRefresh();
168 |
169 | const parentButton = document.getElementById("parent");
170 | const siblingButton = document.getElementById("sibling");
171 |
172 | parentButton.click();
173 | parentButton.click();
174 | siblingButton.click();
175 | siblingButton.click();
176 | siblingButton.click();
177 |
178 | expect(document.body.innerHTML).toBe(
179 | 'abc
'
180 | );
181 |
182 | const UpdatedCurrent = () => {
183 | return c("div", {}, "abcde");
184 | };
185 |
186 | RefreshRuntime.register(
187 | UpdatedCurrent,
188 | "/User/test/Desktop/index.js",
189 | "Current"
190 | );
191 | RefreshRuntime.performRefresh();
192 |
193 | expect(document.body.innerHTML).toBe(
194 | 'abcde
'
195 | );
196 | });
197 |
198 | it("preserve state of children", () => {
199 | const Child = () => {
200 | const [counter, setCounter] = useState(0);
201 | const onClick = () => setCounter(counter + 1);
202 | return c("button", { onClick, id: "child" }, `Clicked ${counter} times!`);
203 | };
204 |
205 | const App = () => {
206 | const [state, setState] = useState("a");
207 | return c("div", {}, c("span", {}, state), c(Child));
208 | };
209 | RefreshRuntime.register(
210 | App,
211 | "/User/test/Desktop/App.js",
212 | "App",
213 | "[state, setState] = useState('a')"
214 | );
215 | render(c(App), document.body);
216 | RefreshRuntime.performRefresh();
217 |
218 | const button = document.getElementById("child");
219 |
220 | button.click();
221 | button.click();
222 |
223 | expect(document.body.innerHTML).toBe(
224 | 'a
'
225 | );
226 |
227 | const UpdatedApp = () => {
228 | const [state, setState] = useState("b");
229 | return c("div", {}, c("span", {}, state), c(Child));
230 | };
231 | RefreshRuntime.register(
232 | UpdatedApp,
233 | "/User/test/Desktop/App.js",
234 | "App",
235 | "[state, setState] = useState('b')"
236 | );
237 | RefreshRuntime.performRefresh();
238 |
239 | expect(document.body.innerHTML).toBe(
240 | 'b
'
241 | );
242 | });
243 |
244 | // TODO
245 | // Find a way to test recovering from syntax errors.
246 | });
247 |
--------------------------------------------------------------------------------
/packages/devtools/README.md:
--------------------------------------------------------------------------------
1 | # devtools
2 |
3 | WIP
4 |
5 | ## Installing dev version
6 |
7 | 1. Visit `chrome://extensions`.
8 | 2. Check `Developer mode`.
9 | 3. Click `Load unpacked`.
10 | 4. Select _directory_ with `manifest.json`.
11 |
12 | https://reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html
13 | https://twitter.com/taneliang/status/1293936745201860609
14 | https://blog.eliangtan.com/view-framework-1/
15 | https://blog.eliangtan.com/view-framework-2/
16 |
--------------------------------------------------------------------------------
/packages/devtools/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tchayen/remini/83d26828c17316a2b0436b1fea6118957b628902/packages/devtools/icon.png
--------------------------------------------------------------------------------
/packages/devtools/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 | Popup
11 |
12 |
13 |
--------------------------------------------------------------------------------
/packages/devtools/index.js:
--------------------------------------------------------------------------------
1 | import { createElement as c, render } from "./remini.js";
2 |
3 | chrome.devtools.panels.create("Remini", "", "/panel.html", (panel) => {
4 | const root = document.getElementById("root");
5 |
6 | const App = () => {
7 | return c(
8 | "div",
9 | {
10 | style: {
11 | background: "white",
12 | width: "100vw",
13 | height: "100vh",
14 | },
15 | class: "text-xl",
16 | },
17 | "abcd123"
18 | );
19 | };
20 |
21 | render(c(App), root);
22 | });
23 |
--------------------------------------------------------------------------------
/packages/devtools/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Hello Extensions",
3 | "description": "Base Level Extension",
4 | "version": "0.0.0",
5 | "manifest_version": 3,
6 | "devtools_page": "panel.html",
7 | "action": {
8 | "default_popup": "index.html",
9 | "default_icon": "icon.png"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/devtools/panel.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/packages/devtools/remini.js:
--------------------------------------------------------------------------------
1 | var e,t,n,o,r=Object.defineProperty,s=Object.defineProperties,i=Object.getOwnPropertyDescriptors,d=Object.getOwnPropertySymbols,p=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable,l=(e,t,n)=>t in e?r(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,c=(e,t)=>{for(var n in t||(t={}))p.call(t,n)&&l(e,n,t[n]);if(d)for(var n of d(t))a.call(t,n)&&l(e,n,t[n]);return e},f=(e,t)=>s(e,i(t));(t=e||(e={}))[t.COMPONENT=1]="COMPONENT",t[t.HOST=2]="HOST",t[t.TEXT=3]="TEXT",t[t.PROVIDER=4]="PROVIDER",t[t.NULL=5]="NULL",t[t.FRAGMENT=6]="FRAGMENT",(o=n||(n={}))[o.STATE=1]="STATE",o[o.EFFECT=2]="EFFECT",o[o.REF=3]="REF",o[o.CONTEXT=4]="CONTEXT",o[o.MEMO=5]="MEMO";const E=e=>!!e.match(new RegExp("on[A-Z].*")),h=e=>e.replace("on","").toLowerCase(),u=e=>"viewBox"===e?e:T(e),T=e=>e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase(),O=e=>Object.keys(e).map((t=>{const n=e[t];return`${T(t)}:${n}`})).join(";"),k=t=>{let n=t;for(;n.kind!==e.COMPONENT&&n.parent;)n=n.parent;return n.kind!==e.COMPONENT?null:n},N=t=>{let n=t;for(;n.kind!==e.HOST&&n.parent;)n=n.parent;if(n.kind!==e.HOST)throw new Error("Couldn't find node.");return n},v=t=>{var n;t.kind===e.HOST||t.kind===e.TEXT?null==(n=t.native.parentNode)||n.removeChild(t.native):t.descendants.forEach((e=>{v(e)}))},C={findClosestComponent:k,findClosestHostNode:N,createHostNode:e=>{const t="svg"===(n=e.tag)||"circle"===n||"path"===n?document.createElementNS("http://www.w3.org/2000/svg",n):document.createElement(n);var n;return Object.entries(e.props).forEach((([e,n])=>{if("children"===e||"ref"===e);else if("style"===e){const o="string"==typeof n?n:O(n);t.setAttribute(e,o)}else E(e)?t.addEventListener(h(e),n):t.setAttribute(u(e),n)})),t},updateHostNode:(e,t)=>{const n=e.native;Object.keys(e.props).forEach((o=>{if("children"===o||"ref"===o);else if(E(o))n.removeEventListener(h(o),e.props[o]);else if(t.props[o]||n.removeAttribute(o),t.props[o])if("style"===o){const e="string"==typeof t.props[o]?t.props[o]:O(t.props[o]);n.setAttribute(o,e)}else n.setAttribute(u(o),t.props[o])})),Object.keys(t.props).forEach((o=>{if("children"===o||"ref"===o);else if(E(o))n.addEventListener(h(o),t.props[o]);else if(!e.props[o])if("style"===o){const e="string"==typeof t.props[o]?t.props[o]:O(t.props[o]);n.setAttribute(o,e)}else n.setAttribute(u(o),t.props[o])}))},removeHostNode:v,appendChild:(e,t)=>{e.appendChild(t)},createTextNode:e=>document.createTextNode(e),updateTextNode:(e,t)=>{e.native.nodeValue=t}},w=t=>{if(t.kind===e.HOST||t.kind===e.TEXT){const{children:e}=t.native.parent;e.splice(e.indexOf(t.native),1)}else t.descendants.forEach((e=>{w(e)}))},g={findClosestComponent:k,createHostNode:e=>{const t={tag:e.tag,children:[],attributes:{}};return Object.entries(e.props).forEach((([e,n])=>{if("children"===e||"ref"===e);else if("style"===e){const o="string"==typeof n?n:O(n);t.attributes[e]=o}else E(e)||(t.attributes[u(e)]=n)})),t},findClosestHostNode:N,removeHostNode:w,updateHostNode:(e,t)=>{const n=e.native;Object.keys(e.props).forEach((e=>{if("children"===e||"ref"===e);else if(E(e));else if(t.props[e]||delete n.attributes[e],t.props[e])if("style"===e){const o="string"==typeof t.props[e]?t.props[e]:O(t.props[e]);n.attributes[e]=o}else n.attributes[u(e)]=t.props[e]})),Object.keys(t.props).forEach((o=>{if("children"===o||"ref"===o);else if(E(o));else if(!e.props[o])if("style"===o){const t="string"==typeof e.props[o]?e.props[o]:O(e.props[o]);n.attributes[o]=t}else n.attributes[u(o)]=t.props[o]}))},appendChild:(e,t)=>{e.children.push(t)},createTextNode:e=>e,updateTextNode:(e,t)=>{e.native=t}};let m=null,y=null,M=0,H=null;const b=new Map,x=new Map;let S=!1;const P=[],R=[];function F(t,n,...o){const r=f(c({},n||{}),{children:o.flat().map((t=>"string"==typeof t?{kind:e.TEXT,content:t}:"number"==typeof t?{kind:e.TEXT,content:t.toString()}:t)).filter(Boolean)});if("function"==typeof t)return t.context?{kind:e.PROVIDER,props:f(c({},r),{$$context:t.context})}:{kind:e.COMPONENT,render:t,props:r};if("string"==typeof t)return""===t?{kind:e.FRAGMENT,props:r}:{kind:e.HOST,tag:t,props:r};throw new Error("Something went wrong.")}const A=(t,o,r)=>{const{host:s,isHydrating:i}=r;H=s;const d=y,p=M;let a=null;if(o&&o.kind===e.TEXT||t.kind===e.TEXT)return;let l=[];if(t.kind===e.COMPONENT)y=t,M=0,l=[t.render(t.props)].filter(Boolean),M=0;else if(o&&"props"in o&&(t.kind===e.HOST||t.kind===e.PROVIDER||t.kind===e.FRAGMENT)){if(t.kind===e.PROVIDER){const e=b.get(t.context);e&&(a={context:t.context,value:e}),b.set(t.context,{value:t.props.value})}l=o.props.children}i&&t.kind===e.HOST&&o&&o.kind===e.HOST&&s.updateHostNode(t,o);const E=Math.max(t.descendants.length,l.length),h=[];for(let e=0;e{if(o&&d&&(o.kind===e.COMPONENT&&d.kind===e.COMPONENT&&o.render===d.render||o.kind===e.HOST&&d.kind===e.HOST&&o.tag===d.tag||o.kind===e.FRAGMENT&&d.kind===e.FRAGMENT||o.kind===e.PROVIDER&&d.kind===e.PROVIDER||o.kind===e.TEXT&&d.kind===e.TEXT))o.kind===e.HOST&&d.kind===e.HOST?s.updateHostNode(o,d):o.kind===e.TEXT&&d.kind===e.TEXT&&o.content!==d.content&&(o.content=d.content,s.updateTextNode(o,d.content)),"props"in o&&"props"in d&&(o.props=d.props),A(o,d,r);else if(o&&d){let i;if(d.kind===e.COMPONENT)i=f(c({},d),{parent:t,descendants:[],hooks:[]}),s.removeHostNode(o);else if(d.kind===e.HOST){const n=s.findClosestHostNode(t),r=f(c({},d),{parent:t,descendants:[]}),p=s.createHostNode(d);o.kind===e.HOST||o.kind===e.TEXT?n.native.replaceChild(p,o.native):(s.removeHostNode(o),s.appendChild(n.native,p)),r.native=p,i=r}else if(d.kind===e.TEXT){const n=s.findClosestHostNode(t),r=f(c({},d),{parent:t}),p=s.createTextNode(d.content);if(o.kind===e.TEXT)throw new Error("Update should have happened on this node.");o.kind===e.HOST?(n.native.replaceChild(p,o.native),r.native=p):(s.removeHostNode(o),s.appendChild(n.native,p),r.native=p),i=r}else if(d.kind===e.PROVIDER)i=f(c({},d),{context:d.props.$$context,parent:t,descendants:[]}),s.removeHostNode(o);else{if(d.kind!==e.FRAGMENT)throw new Error("Couldn't resolve node kind.");i=f(c({},d),{parent:t,descendants:[]}),s.removeHostNode(o)}o.kind===e.COMPONENT&&o.hooks.forEach((e=>{e.type===n.EFFECT&&e.cleanup&&e.cleanup()})),t.descendants[t.descendants.indexOf(o)]=i,A(i,d,r)}else if(o||void 0===d){if(void 0!==o&&!d){const r=t.descendants.indexOf(o);o.kind===e.COMPONENT?o.hooks.forEach((e=>{e.type===n.EFFECT&&e.cleanup&&e.cleanup()})):o.kind===e.PROVIDER&&b.delete(o.context),s.removeHostNode(o),t.descendants.splice(r,1)}}else{let o;if(d.kind===e.COMPONENT)o=f(c({},d),{parent:t,descendants:[],hooks:[]}),d.kind,e.COMPONENT;else if(d.kind===e.HOST){const r=f(c({},d),{parent:t,descendants:[]}),p=s.findClosestHostNode(t);i?(r.native=z,q()):(r.native=s.createHostNode(d),s.appendChild(p.native,r.native)),o=r;const a=s.findClosestComponent(t);if(a&&a.kind===e.COMPONENT)for(const e of a.hooks)e.type===n.REF&&d.props.ref===e&&(e.current=o.native)}else if(d.kind===e.TEXT){const e=f(c({},d),{parent:t}),n=s.findClosestHostNode(t);if(i)e.native=z,q();else{const t=s.createTextNode(d.content);s.appendChild(n.native,t),e.native=t}o=e}else if(d.kind===e.PROVIDER)o=f(c({},d),{parent:t,descendants:[],context:d.props.$$context});else{if(d.kind!==e.FRAGMENT)throw new Error("Couldn't resolve node kind.");o=f(c({},d),{parent:t,descendants:[]})}t.descendants.push(o),A(o,d,r)}})),t.kind===e.PROVIDER&&null!==a&&b.set(a.context,{value:a.value}),y=d,M=p,H=null},X=(e,t,n)=>{if(P.push({node:e,element:t}),S)return;let o;for(S=!0;o=P.shift();){let e;for(A(o.node,o.element,n);e=R.shift();)e()}b.clear(),S=!1},j=(t,o)=>{const r=y,s=M;if(!r||r.kind!==e.COMPONENT)throw new Error("Executing useEffect for non-function element.");R.push((()=>{if(!r||r.kind!==e.COMPONENT)throw new Error("Executing useEffect for non-function element.");if(void 0===r.hooks[s]){const e={type:n.EFFECT,cleanup:void 0,dependencies:o};r.hooks[s]=e;const i=t();e.cleanup=i||void 0}else if(o){const e=r.hooks[s];if(e.type!==n.EFFECT||void 0===e.dependencies)throw new Error("Something went wrong.");let i=!1;for(let t=0;t{const o=y,r=M,s=H;if(!o||o.kind!==e.COMPONENT)throw new Error("Executing useState for non-function element.");if(!s)throw new Error("Missing host context.");void 0===o.hooks[r]&&(o.hooks[r]={type:n.STATE,state:t});const i=o.hooks[r];if(i.type!==n.STATE)throw new Error("Something went wrong.");return M+=1,[i.state,t=>{if(!o||o.kind!==e.COMPONENT)throw new Error("Executing useState for non-function element.");i.state=t instanceof Function?t(i.state):t,X(o,null,{host:s})}]},D=()=>{if(!y||y.kind!==e.COMPONENT)throw new Error("Can't use useRef on this node.");let t=y.hooks[M];if(void 0===t&&(t={type:n.REF,current:null},y.hooks[M]=t),t.type!==n.REF)throw new Error("Something went wrong.");return M+=1,t},I=(t,o)=>{if(!y||y.kind!==e.COMPONENT)throw new Error("Can't call useMemo on this node.");if(void 0===y.hooks[M])y.hooks[M]={type:n.MEMO,memo:t(),dependencies:o};else{const e=y.hooks[M];if(e.type!==n.MEMO||!e.dependencies)throw new Error("Something went wrong.");let r=!1;for(let t=0;t{const e={},t=({value:e})=>F("a",{});return t.context=e,e.Provider=t,e},L=t=>{if(!y||y.kind!==e.COMPONENT)throw new Error("Can't call useContext on this node.");const o=b.get(t);(void 0===y.hooks[M]||o)&&(y.hooks[M]={type:n.CONTEXT,context:o.value});const r=y.hooks[M];if(r.type!==n.CONTEXT)throw new Error("Something went wrong.");return M+=1,r.context},G=(t,n)=>{m={kind:e.HOST,props:{children:[t]},tag:n.tagName.toLowerCase(),native:n,parent:null,descendants:[]},x.clear(),X(m,F("div",{},t),{host:C})},B=e=>{if("string"==typeof e)return e;const t=Object.keys(e.attributes).length>0?" ":"",n=Object.keys(e.attributes).map((t=>`${t}="${e.attributes[t]}"`)).join(" "),o=e.children.map((e=>B(e))).join("");return`<${e.tag}${t}${n}>${o}${e.tag}>`},U=t=>(m={kind:e.HOST,props:{children:[t],id:"root"},tag:"div",native:{tag:"div",attributes:{id:"root"},children:[]},parent:null,descendants:[]},X(m,F("div",{},t),{host:g}),B(m.native)),Z=(t,n)=>{m={kind:e.HOST,props:{children:[t]},tag:n.tagName.toLowerCase(),native:n,parent:null,descendants:[]},z=n.firstChild,X(m,F("div",{},t),{host:C,isHydrating:!0}),z=null};let z=null;const q=()=>{if(null!==z)if(z.firstChild)z=z.firstChild;else if(z.nextSibling)z=z.nextSibling;else{for(;!z.nextSibling;)if(z=z.parentNode,null===z)return;z=z.nextSibling}};export{m as _rootNode,V as createContext,F as createElement,Z as hydrate,G as render,U as renderToString,L as useContext,j as useEffect,I as useMemo,D as useRef,$ as useState};
2 |
--------------------------------------------------------------------------------
/packages/devtools/tailwind.css:
--------------------------------------------------------------------------------
1 | /*! tailwindcss v2.2.7 | MIT License | https://tailwindcss.com*/
2 |
3 | /*! modern-normalize v1.1.0 | MIT License | https://github.com/sindresorhus/modern-normalize */html{-webkit-text-size-adjust:100%;line-height:1.15;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;margin:0}hr{color:inherit;height:0}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],[type=submit],button{-webkit-appearance:button}legend{padding:0}progress{vertical-align:baseline}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}button{background-color:transparent;background-image:none}fieldset,ol,ul{margin:0;padding:0}ol,ul{list-style:none}html{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{font-family:inherit;line-height:inherit}*,:after,:before{border:0 solid;box-sizing:border-box}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{color:inherit;line-height:inherit;padding:0}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,:after,:before{--tw-border-opacity:1;border-color:rgba(229,231,235,var(--tw-border-opacity))}.absolute{position:absolute}.relative{position:relative}.inset-0{bottom:0;left:0;right:0;top:0}.top-6{top:1.5rem}.z-10{z-index:10}.mx-auto{margin-left:auto;margin-right:auto}.my-2{margin-bottom:.5rem;margin-top:.5rem}.mt-1{margin-top:.25rem}.mt-4{margin-top:1rem}.mt-10{margin-top:2.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.ml-1{margin-left:.25rem}.ml-3{margin-left:.75rem}.-ml-1{margin-left:-.25rem}.flex{display:flex}.table{display:table}.hidden{display:none}.h-3{height:.75rem}.h-6{height:1.5rem}.h-9{height:2.25rem}.h-12{height:3rem}.h-full{height:100%}.h-screen{height:100vh}.w-6{width:1.5rem}.w-12{width:3rem}.w-96{width:24rem}.w-3\/4{width:75%}.w-1\/5{width:20%}.w-1\/6{width:16.666667%}.w-full{width:100%}.w-screen{width:100vw}.flex-1{flex:1 1 0%}@-webkit-keyframes spin{to{transform:rotate(1turn)}}@keyframes spin{to{transform:rotate(1turn)}}@-webkit-keyframes ping{75%,to{opacity:0;transform:scale(2)}}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}@-webkit-keyframes pulse{50%{opacity:.5}}@keyframes pulse{50%{opacity:.5}}@-webkit-keyframes bounce{0%,to{-webkit-animation-timing-function:cubic-bezier(.8,0,1,1);animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{-webkit-animation-timing-function:cubic-bezier(0,0,.2,1);animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}}@keyframes bounce{0%,to{-webkit-animation-timing-function:cubic-bezier(.8,0,1,1);animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{-webkit-animation-timing-function:cubic-bezier(0,0,.2,1);animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}}.animate-spin{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.resize{resize:both}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.25rem*var(--tw-space-x-reverse))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.75rem*var(--tw-space-x-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.75rem*var(--tw-space-y-reverse));margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(1px*var(--tw-divide-y-reverse));border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)))}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.border{border-width:1px}.border-r{border-right-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-gray-200{--tw-border-opacity:1;border-color:rgba(229,231,235,var(--tw-border-opacity))}.focus\:border-blue-500:focus{--tw-border-opacity:1;border-color:rgba(59,130,246,var(--tw-border-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgba(255,255,255,var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgba(229,231,235,var(--tw-bg-opacity))}.bg-gray-400{--tw-bg-opacity:1;background-color:rgba(156,163,175,var(--tw-bg-opacity))}.bg-red-400{--tw-bg-opacity:1;background-color:rgba(248,113,113,var(--tw-bg-opacity))}.bg-yellow-300{--tw-bg-opacity:1;background-color:rgba(252,211,77,var(--tw-bg-opacity))}.bg-yellow-500{--tw-bg-opacity:1;background-color:rgba(245,158,11,var(--tw-bg-opacity))}.bg-green-400{--tw-bg-opacity:1;background-color:rgba(52,211,153,var(--tw-bg-opacity))}.bg-blue-400{--tw-bg-opacity:1;background-color:rgba(96,165,250,var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgba(59,130,246,var(--tw-bg-opacity))}.bg-purple-400{--tw-bg-opacity:1;background-color:rgba(167,139,250,var(--tw-bg-opacity))}.hover\:bg-blue-600:hover{--tw-bg-opacity:1;background-color:rgba(37,99,235,var(--tw-bg-opacity))}.p-2{padding:.5rem}.p-4{padding:1rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-bold{font-weight:700}.leading-tight{line-height:1.25}.text-black{--tw-text-opacity:1;color:rgba(0,0,0,var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgba(255,255,255,var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgba(156,163,175,var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgba(107,114,128,var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgba(55,65,81,var(--tw-text-opacity))}.text-blue-500{--tw-text-opacity:1;color:rgba(59,130,246,var(--tw-text-opacity))}.hover\:underline:hover{text-decoration:underline}.opacity-25{opacity:.25}.opacity-60{opacity:.6}.opacity-75{opacity:.75}*,:after,:before{--tw-shadow:0 0 #0000}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,0.1),0 4px 6px -2px rgba(0,0,0,0.05);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}*,:after,:before{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-blue-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgba(59,130,246,var(--tw-ring-opacity))}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px}
--------------------------------------------------------------------------------
/packages/fetch-todo/index.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 | const chalk = require("chalk");
4 |
5 | const lookForFiles = (directory, runOnFile) => {
6 | fs.readdir(directory, (error, files) => {
7 | if (error) {
8 | console.log(error);
9 | return;
10 | }
11 |
12 | files.forEach((file) => {
13 | const path = directory + "/" + file;
14 | const stat = fs.statSync(path);
15 |
16 | if (stat.isDirectory()) {
17 | if (file === "node_modules") {
18 | return;
19 | }
20 |
21 | lookForFiles(path, runOnFile);
22 | } else if (stat.isFile() && file.match(/(j|t)sx?$/)) {
23 | runOnFile(path);
24 | }
25 | });
26 | });
27 | };
28 |
29 | const processFile = (file) => {
30 | fs.readFile(file, { encoding: "utf-8" }, (error, data) => {
31 | if (error) {
32 | console.log(error);
33 | return;
34 | }
35 |
36 | const lines = data.split("\n");
37 | const todos = lines
38 | .map((line, index) => ({ line, index }))
39 | .filter((line) => line.line.match(/^\s*\/\/ TODO/))
40 | .map(({ index }) => {
41 | const comment = [];
42 | let i = index + 1;
43 | while (lines[i].match(/^\s*\/\/ /)) {
44 | comment.push(lines[i].trim().replace("// ", ""));
45 | i++;
46 | }
47 |
48 | const absolutePath = path.resolve(file);
49 |
50 | if (comment.length > 0) {
51 | const header = chalk.bold(
52 | chalk.green(`${absolutePath}:${index + 1}`)
53 | );
54 | console.log(`${header}\n${comment.join("\n")}\n`);
55 | }
56 |
57 | return {
58 | start: index,
59 | end: i,
60 | comment: comment.join("\n"),
61 | };
62 | });
63 | });
64 | };
65 |
66 | lookForFiles("..", processFile);
67 |
--------------------------------------------------------------------------------
/packages/fetch-todo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fetch-todo",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "start": "node ./index.js"
6 | },
7 | "devDependencies": {
8 | "chalk": "^4.1.2"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/import-meta-preset/index.js:
--------------------------------------------------------------------------------
1 | // This plugin replaces calls to import.meta with call to NAME, which is an
2 | // object injected to the top of the file.
3 | function plugin({ types: t }) {
4 | const NAME = "$meta$";
5 |
6 | return {
7 | visitor: {
8 | MetaProperty(path) {
9 | path.replaceWith(t.identifier(NAME));
10 | },
11 | Program(path) {
12 | const metaProperties = [
13 | t.objectProperty(
14 | t.identifier("env"),
15 | t.objectExpression([
16 | t.objectProperty(t.identifier("DEV"), t.booleanLiteral(true)),
17 | ])
18 | ),
19 | ];
20 |
21 | path.unshiftContainer(
22 | "body",
23 | t.variableDeclaration("const", [
24 | t.variableDeclarator(
25 | t.identifier(NAME),
26 | t.objectExpression(metaProperties)
27 | ),
28 | ])
29 | );
30 | },
31 | },
32 | };
33 | }
34 |
35 | module.exports = function preset() {
36 | return {
37 | plugins: [plugin],
38 | };
39 | };
40 |
--------------------------------------------------------------------------------
/packages/remini-plugin/index.js:
--------------------------------------------------------------------------------
1 | function remini() {
2 | return {
3 | name: "remini",
4 | config() {
5 | return {
6 | esbuild: {
7 | jsxFactory: "c",
8 | jsxFragment: "Fragment",
9 | },
10 | };
11 | },
12 | };
13 | }
14 |
15 | module.exports = remini;
16 |
--------------------------------------------------------------------------------
/packages/remini/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2021: true,
5 | node: true,
6 | },
7 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
8 | parser: "@typescript-eslint/parser",
9 | parserOptions: {
10 | sourceType: "module",
11 | },
12 | plugins: ["@typescript-eslint"],
13 | rules: {
14 | "@typescript-eslint/no-empty-function": "off",
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/packages/remini/babel.config.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line
2 | const importMetaPreset = require("../import-meta-preset");
3 |
4 | module.exports = {
5 | presets: [
6 | importMetaPreset,
7 | ["@babel/preset-env", { targets: { node: "current" } }],
8 | "@babel/preset-typescript",
9 | ],
10 | };
11 |
--------------------------------------------------------------------------------
/packages/remini/dom.ts:
--------------------------------------------------------------------------------
1 | import { HostElement, HostNode, NodeType, RNode, TextNode } from "./types";
2 | import {
3 | eventToKeyword,
4 | isEvent,
5 | keyToAttribute,
6 | styleObjectToString,
7 | findClosestComponent,
8 | findClosestHostNode,
9 | } from "./utils";
10 |
11 | function createElement(type: string): Element {
12 | if (
13 | type === "svg" ||
14 | type === "circle" ||
15 | type === "path" ||
16 | type === "rect" ||
17 | type === "foreignObject" ||
18 | type === "g"
19 | ) {
20 | return document.createElementNS("http://www.w3.org/2000/svg", type);
21 | } else {
22 | return document.createElement(type);
23 | }
24 | }
25 |
26 | export function createDom(element: HostElement): Element {
27 | const html = createElement(element.tag);
28 |
29 | const props = Object.entries(element.props);
30 | for (let i = 0; i < props.length; i++) {
31 | const [key, value] = props[i];
32 | if (key === "children" || key === "ref") {
33 | // Skip.
34 | } else if (key === "style") {
35 | const style =
36 | typeof value === "string" ? value : styleObjectToString(value);
37 | html.setAttribute(key, style);
38 | } else if (isEvent(key)) {
39 | html.addEventListener(eventToKeyword(key), value);
40 | } else {
41 | html.setAttribute(keyToAttribute(key), value);
42 | }
43 | }
44 |
45 | return html;
46 | }
47 |
48 | // Update two DOM nodes of the same HTML tag.
49 | export function updateDom(current: HostNode, expected: HostElement): void {
50 | const html = current.native as HTMLElement;
51 |
52 | const currentKeys = Object.keys(current.props);
53 | for (let i = 0; i < currentKeys.length; i++) {
54 | const key = currentKeys[i];
55 | if (key === "children" || key === "ref") {
56 | // Skip.
57 | } else if (isEvent(key)) {
58 | html.removeEventListener(eventToKeyword(key), current.props[key]);
59 | } else {
60 | // Prop will be removed.
61 | if (!expected.props[key]) {
62 | html.removeAttribute(key);
63 | }
64 | }
65 | }
66 |
67 | const expectedKeys = Object.keys(expected.props);
68 | for (let i = 0; i < expectedKeys.length; i++) {
69 | const key = expectedKeys[i];
70 | if (key === "children" || key === "ref") {
71 | // Skip.
72 | } else if (isEvent(key)) {
73 | html.addEventListener(eventToKeyword(key), expected.props[key]);
74 | } else {
75 | // Prop will be added/updated.
76 | if (expected.props[key] !== current.props[key]) {
77 | if (key === "style") {
78 | const style =
79 | typeof expected.props[key] === "string"
80 | ? expected.props[key]
81 | : styleObjectToString(expected.props[key]);
82 | html.setAttribute(key, style);
83 | } else {
84 | html.setAttribute(keyToAttribute(key), expected.props[key] as string);
85 | }
86 | }
87 | }
88 | }
89 | }
90 |
91 | export function removeDom(node: RNode): void {
92 | if (node.kind === NodeType.HOST || node.kind === NodeType.TEXT) {
93 | node.native.parentNode?.removeChild(node.native);
94 | } else {
95 | for (let i = 0; i < node.descendants.length; i++) {
96 | removeDom(node.descendants[i]);
97 | }
98 | }
99 | }
100 |
101 | export function appendChild(parent: Node, child: Node): void {
102 | parent.appendChild(child);
103 | }
104 |
105 | export function createTextNode(text: string): Text {
106 | return document.createTextNode(text);
107 | }
108 |
109 | export function updateTextNode(current: TextNode, text: string): void {
110 | current.native.nodeValue = text;
111 | }
112 |
113 | export const host = {
114 | findClosestComponent,
115 | findClosestHostNode,
116 | createHostNode: createDom,
117 | updateHostNode: updateDom,
118 | removeHostNode: removeDom,
119 | appendChild,
120 | createTextNode,
121 | updateTextNode,
122 | };
123 |
--------------------------------------------------------------------------------
/packages/remini/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/packages/remini/index.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | createElement as c,
4 | hydrate,
5 | render,
6 | renderToString,
7 | useContext,
8 | useEffect,
9 | useMemo,
10 | useRef,
11 | useState,
12 | } from "./lib";
13 | import { NodeType, RElement } from "./types";
14 |
15 | jest.useFakeTimers();
16 |
17 | beforeEach(() => {
18 | document.body.innerHTML = "";
19 | });
20 |
21 | describe("createElement", () => {
22 | it("works for simple HTML", () => {
23 | const result = c("button", {}, c("strong", {}, "Hello world"));
24 |
25 | const expected: RElement = {
26 | tag: "button",
27 | kind: NodeType.HOST,
28 | props: {
29 | children: [
30 | {
31 | kind: NodeType.HOST,
32 | tag: "strong",
33 | props: {
34 | children: [
35 | {
36 | kind: NodeType.TEXT,
37 | content: "Hello world",
38 | },
39 | ],
40 | },
41 | },
42 | ],
43 | },
44 | };
45 |
46 | expect(result).toStrictEqual(expected);
47 | });
48 |
49 | it("works with props", () => {
50 | const tree = c("a", { href: "https://google.com" }, "Google");
51 |
52 | const expected: RElement = {
53 | kind: NodeType.HOST,
54 | tag: "a",
55 | props: {
56 | href: "https://google.com",
57 | children: [
58 | {
59 | kind: NodeType.TEXT,
60 | content: "Google",
61 | },
62 | ],
63 | },
64 | };
65 |
66 | expect(tree).toStrictEqual(expected);
67 | });
68 |
69 | it("works with components", () => {
70 | const Title = ({ children }: { children: string }) => {
71 | return c("h1", {}, children);
72 | };
73 |
74 | const result = c(
75 | "div",
76 | {},
77 | c(Title, {}, "Hello world"),
78 | c("span", {}, "Text")
79 | );
80 |
81 | const expected: RElement = {
82 | kind: NodeType.HOST,
83 | tag: "div",
84 | props: {
85 | children: [
86 | {
87 | kind: NodeType.COMPONENT,
88 | render: Title,
89 | props: {
90 | children: [
91 | {
92 | kind: NodeType.TEXT,
93 | content: "Hello world",
94 | },
95 | ],
96 | },
97 | },
98 | {
99 | kind: NodeType.HOST,
100 | tag: "span",
101 | props: {
102 | children: [
103 | {
104 | kind: NodeType.TEXT,
105 | content: "Text",
106 | },
107 | ],
108 | },
109 | },
110 | ],
111 | },
112 | };
113 |
114 | expect(result).toStrictEqual(expected);
115 | });
116 |
117 | it("works with multiple children", () => {
118 | const Title = ({ children }: { children: string }) => {
119 | return c("h1", {}, children);
120 | };
121 |
122 | const result = c("div", {}, [
123 | c(Title, {}, "Hello world"),
124 | c("span", {}, "Text"),
125 | ]);
126 |
127 | const expected: RElement = {
128 | kind: NodeType.HOST,
129 | tag: "div",
130 | props: {
131 | children: [
132 | {
133 | kind: NodeType.COMPONENT,
134 | render: Title,
135 | props: {
136 | children: [
137 | {
138 | kind: NodeType.TEXT,
139 | content: "Hello world",
140 | },
141 | ],
142 | },
143 | },
144 | {
145 | kind: NodeType.HOST,
146 | tag: "span",
147 | props: {
148 | children: [
149 | {
150 | kind: NodeType.TEXT,
151 | content: "Text",
152 | },
153 | ],
154 | },
155 | },
156 | ],
157 | },
158 | };
159 |
160 | expect(result).toStrictEqual(expected);
161 | });
162 |
163 | it("works with text node as a sibling of host node", () => {
164 | // Contact: mail
165 | const result = c("div", {}, "Contact: ", c("span", {}, "mail"));
166 | const expected: RElement = {
167 | kind: NodeType.HOST,
168 | tag: "div",
169 | props: {
170 | children: [
171 | {
172 | kind: NodeType.TEXT,
173 | content: "Contact: ",
174 | },
175 | {
176 | kind: NodeType.HOST,
177 | tag: "span",
178 | props: {
179 | children: [
180 | {
181 | kind: NodeType.TEXT,
182 | content: "mail",
183 | },
184 | ],
185 | },
186 | },
187 | ],
188 | },
189 | };
190 | expect(result).toStrictEqual(expected);
191 | });
192 |
193 | it("works for array of items", () => {
194 | // {items.map(item => {item})
195 | const items = ["orange", "apple"];
196 | const result = c(
197 | "div",
198 | {},
199 | items.map((item) => c("span", {}, item))
200 | );
201 | const expected: RElement = {
202 | kind: NodeType.HOST,
203 | tag: "div",
204 | props: {
205 | children: [
206 | {
207 | kind: NodeType.HOST,
208 | tag: "span",
209 | props: {
210 | children: [
211 | {
212 | kind: NodeType.TEXT,
213 | content: "orange",
214 | },
215 | ],
216 | },
217 | },
218 | {
219 | kind: NodeType.HOST,
220 | tag: "span",
221 | props: {
222 | children: [
223 | {
224 | kind: NodeType.TEXT,
225 | content: "apple",
226 | },
227 | ],
228 | },
229 | },
230 | ],
231 | },
232 | };
233 | expect(result).toStrictEqual(expected);
234 | });
235 |
236 | it("works with empty children", () => {
237 | const result = c("input", {});
238 | const expected: RElement = {
239 | kind: NodeType.HOST,
240 | tag: "input",
241 | props: {
242 | children: [],
243 | },
244 | };
245 | expect(result).toStrictEqual(expected);
246 | });
247 |
248 | it("works with fragments", () => {
249 | const root = document.createElement("div");
250 | document.body.appendChild(root);
251 |
252 | // // <>a
b
>
253 | const tree = c("", {}, c("div", {}, "a"), c("div", {}, "b"));
254 |
255 | render(tree, root);
256 |
257 | expect(document.body.innerHTML).toBe("");
258 | });
259 | });
260 |
261 | describe("render", () => {
262 | it("works", () => {
263 | const root = document.createElement("div");
264 | document.body.appendChild(root);
265 |
266 | const Counter = ({ children }: { children: string }) => {
267 | return c(
268 | "div",
269 | {},
270 | c("span", {}, children),
271 | c("span", { style: "color: #ff0000" }, "0")
272 | );
273 | };
274 |
275 | const tree = c("div", {}, c(Counter, {}, "Counter: "), c("h1", {}, "Test"));
276 |
277 | render(tree, root);
278 |
279 | expect(document.body.innerHTML).toBe(
280 | ''
281 | );
282 | });
283 |
284 | it("works with state", () => {
285 | let update = () => {};
286 |
287 | const root = document.createElement("div");
288 | document.body.appendChild(root);
289 |
290 | const Counter = ({ children }: { children: string }) => {
291 | const [value, setValue] = useState(0);
292 |
293 | update = () => setValue(value + 1);
294 |
295 | return c("div", {}, c("span", {}, children), c("span", {}, `${value}`));
296 | };
297 |
298 | const tree = c("div", {}, c(Counter, {}, "Counter: "), c("h1", {}, "Test"));
299 |
300 | render(tree, root);
301 |
302 | expect(document.body.innerHTML).toBe(
303 | ""
304 | );
305 |
306 | update();
307 |
308 | expect(document.body.innerHTML).toBe(
309 | ""
310 | );
311 | });
312 |
313 | it("works with replacing nodes", () => {
314 | let update = () => {};
315 |
316 | const root = document.createElement("div");
317 | document.body.appendChild(root);
318 |
319 | const Alter = () => {
320 | const [show, setShow] = useState(false);
321 |
322 | update = () => {
323 | setShow(!show);
324 | };
325 |
326 | return c(
327 | "div",
328 | {},
329 | show ? c("span", {}, "Show") : null,
330 | c("div", {}, "This is always here")
331 | );
332 | };
333 |
334 | const tree = c(Alter);
335 |
336 | render(tree, root);
337 | expect(document.body.innerHTML).toBe(
338 | ""
339 | );
340 |
341 | update();
342 | expect(document.body.innerHTML).toBe(
343 | ""
344 | );
345 |
346 | update();
347 | expect(document.body.innerHTML).toBe(
348 | ""
349 | );
350 | });
351 | });
352 |
353 | describe("useState", () => {
354 | it("doesn't take immediate effect on state value", () => {
355 | const root = document.createElement("div");
356 | document.body.appendChild(root);
357 |
358 | let update = () => {};
359 | let nextValue;
360 |
361 | const App = () => {
362 | const [value, setValue] = useState(0);
363 |
364 | update = () => {
365 | setValue(value + 1);
366 | nextValue = value;
367 | };
368 |
369 | return c("span", {}, `${value}`);
370 | };
371 |
372 | render(c(App), root);
373 |
374 | expect(document.body.innerHTML).toBe("0
");
375 |
376 | update();
377 |
378 | expect(nextValue).toBe(0);
379 | expect(document.body.innerHTML).toBe("1
");
380 | });
381 |
382 | it("can be called more than once", () => {
383 | const root = document.createElement("div");
384 | document.body.appendChild(root);
385 |
386 | let updateA = () => {};
387 | let updateB = () => {};
388 |
389 | const App = () => {
390 | const [a, setA] = useState("a");
391 | const [b, setB] = useState(0);
392 |
393 | updateA = () => {
394 | setA("b");
395 | };
396 |
397 | updateB = () => {
398 | setB(1);
399 | };
400 |
401 | return c("div", {}, c("span", {}, a), c("span", {}, b.toString()));
402 | };
403 |
404 | render(c(App), root);
405 |
406 | expect(document.body.innerHTML).toBe(
407 | ""
408 | );
409 |
410 | updateA();
411 |
412 | expect(document.body.innerHTML).toBe(
413 | ""
414 | );
415 |
416 | updateB();
417 |
418 | expect(document.body.innerHTML).toBe(
419 | ""
420 | );
421 | });
422 |
423 | it("provides updater form", () => {
424 | const root = document.createElement("div");
425 | document.body.appendChild(root);
426 |
427 | const Counter = () => {
428 | const [count, setCount] = useState(0);
429 |
430 | useEffect(() => {
431 | const id = setInterval(() => {
432 | setCount((c) => c + 1);
433 | }, 1000);
434 | return () => clearInterval(id);
435 | }, []);
436 |
437 | return c("div", {}, `${count}`);
438 | };
439 |
440 | render(c(Counter), root);
441 | expect(document.body.innerHTML).toBe("");
442 |
443 | jest.runOnlyPendingTimers();
444 | expect(document.body.innerHTML).toBe("");
445 |
446 | jest.runOnlyPendingTimers();
447 | expect(document.body.innerHTML).toBe("");
448 | });
449 |
450 | it("does not work outside component", () => {
451 | expect(() => {
452 | const [state, setState] = useState(0);
453 | }).toThrowError("Executing useState for non-function element.");
454 | });
455 | });
456 |
457 | describe("useEffect", () => {
458 | it("works with empty deps array", () => {
459 | let update = () => {};
460 | const mock = jest.fn();
461 |
462 | const root = document.createElement("div");
463 | document.body.appendChild(root);
464 |
465 | const App = () => {
466 | const [, setData] = useState("");
467 |
468 | update = () => setData("123");
469 |
470 | useEffect(() => {
471 | mock();
472 | }, []);
473 |
474 | return c("span", {}, "Hello");
475 | };
476 |
477 | const tree = c(App);
478 |
479 | render(tree, root);
480 |
481 | update();
482 |
483 | expect(mock).toHaveBeenCalledTimes(1);
484 | });
485 |
486 | it("works with no deps array", () => {
487 | let update = () => {};
488 | const mock = jest.fn();
489 |
490 | const root = document.createElement("div");
491 | document.body.appendChild(root);
492 |
493 | const App = () => {
494 | const [, setData] = useState("");
495 |
496 | update = () => setData("123");
497 |
498 | useEffect(() => {
499 | mock();
500 | });
501 |
502 | return c("span", {}, "Hello");
503 | };
504 |
505 | const tree = c(App);
506 |
507 | render(tree, root);
508 |
509 | update();
510 |
511 | expect(mock).toHaveBeenCalledTimes(2);
512 | });
513 |
514 | it("works with deps array", () => {
515 | const root = document.createElement("div");
516 | document.body.appendChild(root);
517 |
518 | const Profile = ({ username }: { username: string }) => {
519 | const [user, setUser] = useState<{ username: string } | null>(null);
520 |
521 | useEffect(() => {
522 | setUser({ username });
523 | }, [username]);
524 |
525 | return c("div", {}, c("span", {}, user ? user.username : "Anonymous"));
526 | };
527 |
528 | const tree = c(Profile, { username: "John" });
529 |
530 | render(tree, root);
531 |
532 | expect(document.body.innerHTML).toBe(
533 | ""
534 | );
535 | });
536 |
537 | xit("works with comparing deps array", () => {
538 | // TODO
539 | // trigger a change and make sure it doesn't trigger useEffect if should not be changed.
540 | });
541 |
542 | it("works with callback", () => {
543 | const root = document.createElement("div");
544 | document.body.appendChild(root);
545 |
546 | const mock = jest.fn();
547 |
548 | const Goodbye = () => {
549 | useEffect(() => {
550 | return () => mock();
551 | }, []);
552 |
553 | return c("span", {}, "Hello");
554 | };
555 |
556 | const App = () => {
557 | const [show, setShow] = useState(true);
558 | useEffect(() => {
559 | setShow(false);
560 | }, []);
561 |
562 | return c("div", {}, show ? c(Goodbye) : null);
563 | };
564 |
565 | render(c(App), root);
566 |
567 | expect(mock).toHaveBeenCalledTimes(1);
568 | expect(document.body.innerHTML).toBe("");
569 | });
570 |
571 | xit("works with calling callback in a deeply nested component", () => {
572 | // TODO
573 | });
574 |
575 | it("works with two different effects in a component", () => {
576 | const root = document.createElement("div");
577 | document.body.appendChild(root);
578 |
579 | const Advanced = ({ enforceCount }: { enforceCount: number }) => {
580 | const [count, setCount] = useState(0);
581 | useEffect(() => {
582 | setCount(10);
583 | }, []);
584 |
585 | useEffect(() => {
586 | setCount(enforceCount);
587 | }, [enforceCount]);
588 |
589 | return c("div", {}, `${count}`);
590 | };
591 |
592 | const App = () => {
593 | const [enforceCount, setCount] = useState(5);
594 | useEffect(() => {
595 | setTimeout(() => {
596 | setCount(7);
597 | }, 1000);
598 | }, []);
599 |
600 | return c("div", {}, c(Advanced, { enforceCount }));
601 | };
602 |
603 | const tree = c(App);
604 | render(tree, root);
605 |
606 | expect(document.body.innerHTML).toBe("");
607 |
608 | jest.runOnlyPendingTimers();
609 | expect(document.body.innerHTML).toBe("");
610 | });
611 |
612 | it("does not work outside component", () => {
613 | expect(() => {
614 | useEffect(() => {}, []);
615 | }).toThrowError("Executing useEffect for non-function element.");
616 | });
617 | });
618 |
619 | describe("useRef", () => {
620 | it("works", () => {
621 | const root = document.createElement("div");
622 | document.body.appendChild(root);
623 |
624 | let availableInEffect: any = null;
625 |
626 | const App = () => {
627 | const ref = useRef();
628 |
629 | useEffect(() => {
630 | availableInEffect = ref.current;
631 | }, []);
632 |
633 | return c("div", {}, c("span", { ref }, "test"));
634 | };
635 |
636 | const tree = c(App);
637 | render(tree, root);
638 |
639 | expect(availableInEffect).not.toBeNull();
640 | expect(availableInEffect.tagName).toBe("SPAN");
641 | });
642 |
643 | it("works with two refs in one component", () => {
644 | const root = document.createElement("div");
645 | document.body.appendChild(root);
646 |
647 | let availableInEffect1: any = null;
648 | let availableInEffect2: any = null;
649 |
650 | const App = () => {
651 | const ref1 = useRef();
652 | const ref2 = useRef();
653 |
654 | useEffect(() => {
655 | availableInEffect1 = ref1.current;
656 | availableInEffect2 = ref2.current;
657 | }, []);
658 |
659 | return c(
660 | "div",
661 | {},
662 | c("span", { ref: ref1 }, "test"),
663 | c("button", { ref: ref2 }, "test")
664 | );
665 | };
666 |
667 | const tree = c(App);
668 | render(tree, root);
669 |
670 | expect(availableInEffect1).not.toBeNull();
671 | expect(availableInEffect1.tagName).toBe("SPAN");
672 |
673 | expect(availableInEffect2).not.toBeNull();
674 | expect(availableInEffect2.tagName).toBe("BUTTON");
675 | });
676 | });
677 |
678 | describe("useMemo", () => {
679 | it("works", () => {
680 | const root = document.createElement("div");
681 | document.body.appendChild(root);
682 | const mock = jest.fn();
683 |
684 | const App = () => {
685 | const _ = useMemo(mock, []);
686 | const [, setState] = useState(0);
687 |
688 | useEffect(() => {
689 | setTimeout(() => setState(1), 1000);
690 | }, []);
691 |
692 | return c("div", {}, "Test");
693 | };
694 |
695 | const tree = c(App);
696 | render(tree, root);
697 |
698 | expect(mock).toHaveBeenCalledTimes(1);
699 |
700 | jest.runOnlyPendingTimers();
701 |
702 | expect(mock).toHaveBeenCalledTimes(1);
703 | });
704 |
705 | it("works with deps", () => {
706 | const root = document.createElement("div");
707 | document.body.appendChild(root);
708 | const mock = jest.fn();
709 |
710 | const User = ({ username }: { username: string }) => {
711 | const uppercase = useMemo(() => {
712 | mock();
713 | return username.toUpperCase();
714 | }, [username]);
715 |
716 | return c("span", {}, uppercase);
717 | };
718 |
719 | const App = () => {
720 | const [username, setUsername] = useState("Alice");
721 | const [, setCounter] = useState(0);
722 |
723 | useEffect(() => {
724 | setTimeout(() => {
725 | setCounter(1);
726 | }, 500);
727 |
728 | setTimeout(() => {
729 | setUsername("Bob");
730 | }, 1000);
731 | }, []);
732 |
733 | return c(User, { username });
734 | };
735 |
736 | const tree = c(App);
737 | render(tree, root);
738 |
739 | expect(mock).toHaveBeenCalledTimes(1);
740 |
741 | jest.advanceTimersByTime(501);
742 | expect(mock).toHaveBeenCalledTimes(1);
743 |
744 | jest.advanceTimersByTime(501);
745 | expect(mock).toHaveBeenCalledTimes(2);
746 | });
747 | });
748 |
749 | describe("Context API", () => {
750 | it("works", () => {
751 | const root = document.createElement("div");
752 | document.body.appendChild(root);
753 |
754 | type Session = { username: string } | null;
755 | const SessionContext = createContext();
756 |
757 | const User = () => {
758 | const session = useContext(SessionContext);
759 |
760 | return c("div", {}, session ? session.username : "Anonymous");
761 | };
762 |
763 | const Sidebar = () => {
764 | return c(User);
765 | };
766 |
767 | const App = () => {
768 | const [session, setSession] = useState(null);
769 |
770 | useEffect(() => {
771 | setTimeout(() => {
772 | setSession({ username: "John" });
773 | }, 1000);
774 | }, []);
775 |
776 | return c(SessionContext.Provider, { value: session }, c(Sidebar));
777 | };
778 |
779 | const tree = c(App);
780 | render(tree, root);
781 |
782 | expect(document.body.innerHTML).toBe("");
783 |
784 | jest.runOnlyPendingTimers();
785 | expect(document.body.innerHTML).toBe("");
786 | });
787 |
788 | it("works with different contexts", () => {
789 | const root = document.createElement("div");
790 | document.body.appendChild(root);
791 |
792 | type Theme = "light" | "dark";
793 | const ThemeContext = createContext();
794 |
795 | type Session = { username: string } | null;
796 | const SessionContext = createContext();
797 |
798 | const User = () => {
799 | const theme = useContext(ThemeContext);
800 | const session = useContext(SessionContext);
801 |
802 | return c(
803 | "div",
804 | { style: { backgroundColor: theme === "light" ? "#fff" : "#000" } },
805 | session ? session.username : "Anonymous"
806 | );
807 | };
808 |
809 | const Sidebar = () => {
810 | return c(User);
811 | };
812 |
813 | const App = () => {
814 | const [session] = useState({ username: "Alice" });
815 |
816 | return c(
817 | ThemeContext.Provider,
818 | { value: "light" },
819 | c(SessionContext.Provider, { value: session }, c(Sidebar))
820 | );
821 | };
822 |
823 | const tree = c(App);
824 | render(tree, root);
825 |
826 | expect(document.body.innerHTML).toBe(
827 | ''
828 | );
829 | });
830 |
831 | it("works with nested providers with different values", () => {
832 | const root = document.createElement("div");
833 | document.body.appendChild(root);
834 |
835 | // P(1)
836 | // |
837 | // A
838 | // |\
839 | // B P(2)
840 | // |
841 | // B
842 | //
843 | // P(x) - provider with x as the value.
844 |
845 | const Context = createContext();
846 |
847 | const B = () => {
848 | const context = useContext(Context);
849 | return c("div", {}, `${context}`);
850 | };
851 |
852 | const A = () => {
853 | return c("div", {}, c(B), c(Context.Provider, { value: 2 }, c(B)));
854 | };
855 |
856 | const App = () => {
857 | return c("div", {}, c(Context.Provider, { value: 1 }, c(A)));
858 | };
859 |
860 | const tree = c(App);
861 | render(tree, root);
862 |
863 | expect(document.body.innerHTML).toBe(
864 | ""
865 | );
866 | });
867 | });
868 |
869 | describe("DOM", () => {
870 | it("works with basic elements", () => {
871 | const root = document.createElement("div");
872 | document.body.appendChild(root);
873 |
874 | const tree = c(
875 | "div",
876 | { id: "root" },
877 | c("a", { href: "https://google.com" }, "Google")
878 | );
879 |
880 | render(tree, root);
881 |
882 | expect(document.body.innerHTML).toBe(
883 | ''
884 | );
885 | });
886 |
887 | it("works with components", () => {
888 | const root = document.createElement("div");
889 | document.body.appendChild(root);
890 |
891 | const Title = ({ children }: { children: string }) => {
892 | return c("h1", {}, children);
893 | };
894 |
895 | const tree = c(
896 | "div",
897 | {},
898 | c(Title, {}, "Hello world"),
899 | c("span", {}, "Text")
900 | );
901 |
902 | render(tree, root);
903 |
904 | expect(document.body.innerHTML).toBe(
905 | ""
906 | );
907 | });
908 |
909 | it("works with event listeners", () => {
910 | const root = document.createElement("div");
911 | document.body.appendChild(root);
912 |
913 | const onClick = jest.fn();
914 |
915 | const Click = () => {
916 | return c("button", { id: "button", onClick }, "Click");
917 | };
918 |
919 | const tree = c(Click);
920 |
921 | render(tree, root);
922 |
923 | const button = document.getElementById("button");
924 |
925 | if (button === null) {
926 | throw new Error("Unexpected null.");
927 | }
928 |
929 | button.click();
930 |
931 | expect(onClick).toHaveBeenCalled();
932 | });
933 |
934 | it("works with updates", () => {
935 | const root = document.createElement("div");
936 | document.body.appendChild(root);
937 |
938 | const Counter = () => {
939 | const [value, setValue] = useState(0);
940 |
941 | const onClick = () => {
942 | setValue(value + 1);
943 | };
944 |
945 | return c(
946 | "div",
947 | {},
948 | c("span", { id: "value" }, `${value}`),
949 | c("button", { id: "button", onClick }, "Click")
950 | );
951 | };
952 |
953 | const tree = c(Counter);
954 |
955 | render(tree, root);
956 |
957 | const value = document.getElementById("value");
958 | const button = document.getElementById("button");
959 |
960 | if (value == null || button === null) {
961 | throw new Error("Unexpected null.");
962 | }
963 |
964 | expect(value.innerHTML).toBe("0");
965 |
966 | button.click();
967 |
968 | expect(value.innerHTML).toBe("1");
969 | });
970 |
971 | it("works with replacing list of nodes with another list", () => {
972 | const root = document.createElement("div");
973 | document.body.appendChild(root);
974 |
975 | const PlaceholderPost = ({ number }: { number: number }) =>
976 | c("div", {}, `placeholder-${number}`);
977 |
978 | type User = {
979 | name: string;
980 | };
981 |
982 | const Post = ({ name }: User) => c("div", {}, `u-${name}`);
983 |
984 | const App = () => {
985 | const [loading, setLoading] = useState(true);
986 | const [data, setData] = useState([]);
987 |
988 | useEffect(() => {
989 | setTimeout(() => {
990 | setData([{ name: "Alice" }, { name: "Bob" }]);
991 | setLoading(false);
992 | }, 500);
993 | }, []);
994 |
995 | return c(
996 | "div",
997 | {},
998 | loading
999 | ? c(
1000 | "div",
1001 | {},
1002 | c(PlaceholderPost, { number: 1 }),
1003 | c(PlaceholderPost, { number: 2 }),
1004 | c(PlaceholderPost, { number: 3 })
1005 | )
1006 | : c(
1007 | "div",
1008 | {},
1009 | data.map((post) => c(Post, post))
1010 | )
1011 | );
1012 | };
1013 |
1014 | const tree = c(App);
1015 | render(tree, root);
1016 |
1017 | expect(document.body.innerHTML).toBe(
1018 | "placeholder-1
placeholder-2
placeholder-3
"
1019 | );
1020 |
1021 | jest.runOnlyPendingTimers();
1022 |
1023 | expect(document.body.innerHTML).toBe(
1024 | ""
1025 | );
1026 | });
1027 |
1028 | it("works with inserting node between other nodes", () => {
1029 | const root = document.createElement("div");
1030 | document.body.appendChild(root);
1031 |
1032 | const Text = ({ children }: { children: string }) => {
1033 | return c("div", {}, children);
1034 | };
1035 |
1036 | const App = () => {
1037 | const [loading, setLoading] = useState(true);
1038 |
1039 | useEffect(() => {
1040 | setTimeout(() => {
1041 | setLoading(false);
1042 | }, 500);
1043 | }, []);
1044 |
1045 | return c(
1046 | "div",
1047 | {},
1048 | c(Text, {}, "a"),
1049 | loading ? null : c("div", {}, "b"),
1050 | c(Text, {}, "c"),
1051 | c("div", {}, "d")
1052 | );
1053 | };
1054 |
1055 | const tree = c(App);
1056 | render(tree, root);
1057 |
1058 | expect(document.body.innerHTML).toBe(
1059 | ""
1060 | );
1061 |
1062 | jest.runOnlyPendingTimers();
1063 |
1064 | expect(document.body.innerHTML).toBe(
1065 | ""
1066 | );
1067 | });
1068 |
1069 | it("works with removing DOM node when component that has non-host node as first child is detached", () => {
1070 | const root = document.createElement("div");
1071 | document.body.appendChild(root);
1072 |
1073 | const UserContext = createContext();
1074 |
1075 | const Login = () => {
1076 | const user = useContext<{ name: string }>(UserContext);
1077 |
1078 | return c("span", {}, user.name);
1079 | };
1080 |
1081 | const User = () => {
1082 | return c(UserContext.Provider, { value: { name: "John" } }, c(Login));
1083 | };
1084 |
1085 | const App = () => {
1086 | const [show, setShow] = useState(true);
1087 |
1088 | useEffect(() => {
1089 | setTimeout(() => {
1090 | setShow(false);
1091 | }, 500);
1092 | }, []);
1093 |
1094 | return c("div", {}, show ? c(User) : "test");
1095 | };
1096 |
1097 | const tree = c(App);
1098 | render(tree, root);
1099 |
1100 | expect(document.body.innerHTML).toBe(
1101 | ""
1102 | );
1103 |
1104 | jest.runOnlyPendingTimers();
1105 |
1106 | expect(document.body.innerHTML).toBe("");
1107 | });
1108 |
1109 | xit("has props that are gone removed from DOM", () => {
1110 | // TODO
1111 | });
1112 |
1113 | it("has text removed when replaced", () => {
1114 | const root = document.createElement("div");
1115 | document.body.appendChild(root);
1116 |
1117 | const Other = () => {
1118 | return c("h1", {}, "Other");
1119 | };
1120 |
1121 | const App = () => {
1122 | const [show, setShow] = useState(true);
1123 |
1124 | useEffect(() => {
1125 | setTimeout(() => {
1126 | setShow(false);
1127 | }, 500);
1128 | }, []);
1129 |
1130 | return c("div", {}, show ? "test" : c(Other));
1131 | };
1132 |
1133 | const tree = c(App);
1134 | render(tree, root);
1135 |
1136 | expect(document.body.innerHTML).toBe("");
1137 |
1138 | jest.runOnlyPendingTimers();
1139 |
1140 | expect(document.body.innerHTML).toBe(
1141 | ""
1142 | );
1143 | });
1144 |
1145 | it("has a node cleaned up after being replaced by provider", () => {
1146 | const root = document.createElement("div");
1147 | document.body.appendChild(root);
1148 |
1149 | const UserContext = createContext<{ name: string }>();
1150 |
1151 | const Consumer = () => {
1152 | const user = useContext(UserContext);
1153 |
1154 | return c("span", {}, user.name);
1155 | };
1156 |
1157 | const App = () => {
1158 | const [show, setShow] = useState(true);
1159 |
1160 | useEffect(() => {
1161 | setTimeout(() => {
1162 | setShow(false);
1163 | }, 500);
1164 | }, []);
1165 |
1166 | return c(
1167 | "div",
1168 | {},
1169 | show
1170 | ? c("div", {}, c("span", {}, "a"))
1171 | : c(UserContext.Provider, { value: { name: "John" } }, c(Consumer))
1172 | );
1173 | };
1174 |
1175 | const tree = c(App);
1176 | render(tree, root);
1177 |
1178 | expect(document.body.innerHTML).toBe(
1179 | ""
1180 | );
1181 |
1182 | jest.runOnlyPendingTimers();
1183 |
1184 | expect(document.body.innerHTML).toBe(
1185 | ""
1186 | );
1187 | });
1188 |
1189 | it("has text updated with text", () => {
1190 | const root = document.createElement("div");
1191 | document.body.appendChild(root);
1192 |
1193 | const App = () => {
1194 | const [show, setShow] = useState(true);
1195 |
1196 | useEffect(() => {
1197 | setTimeout(() => {
1198 | setShow(false);
1199 | }, 500);
1200 | }, []);
1201 |
1202 | return c("div", {}, show ? "a" : "b");
1203 | };
1204 |
1205 | const tree = c(App);
1206 | render(tree, root);
1207 |
1208 | expect(document.body.innerHTML).toBe("");
1209 |
1210 | jest.runOnlyPendingTimers();
1211 |
1212 | expect(document.body.innerHTML).toBe("");
1213 | });
1214 |
1215 | it("has host node replaced with text", () => {
1216 | const root = document.createElement("div");
1217 | document.body.appendChild(root);
1218 |
1219 | const App = () => {
1220 | const [show, setShow] = useState(true);
1221 |
1222 | useEffect(() => {
1223 | setTimeout(() => {
1224 | setShow(false);
1225 | }, 500);
1226 | }, []);
1227 |
1228 | return c("div", {}, show ? c("span", {}, "a") : "b");
1229 | };
1230 |
1231 | const tree = c(App);
1232 | render(tree, root);
1233 |
1234 | expect(document.body.innerHTML).toBe(
1235 | ""
1236 | );
1237 |
1238 | jest.runOnlyPendingTimers();
1239 |
1240 | expect(document.body.innerHTML).toBe("");
1241 | });
1242 |
1243 | it("has hooks triggered on cleanup when replacing component", () => {
1244 | const root = document.createElement("div");
1245 | document.body.appendChild(root);
1246 | const mock = jest.fn();
1247 |
1248 | const Cleanup = () => {
1249 | useEffect(() => {
1250 | return () => {
1251 | mock();
1252 | };
1253 | }, []);
1254 |
1255 | return c("div", {}, "a");
1256 | };
1257 |
1258 | const App = () => {
1259 | const [show, setShow] = useState(true);
1260 |
1261 | useEffect(() => {
1262 | setTimeout(() => {
1263 | setShow(false);
1264 | }, 500);
1265 | }, []);
1266 |
1267 | return c("div", {}, show ? c(Cleanup) : "b");
1268 | };
1269 |
1270 | const tree = c(App);
1271 | render(tree, root);
1272 |
1273 | jest.runOnlyPendingTimers();
1274 |
1275 | expect(mock).toHaveBeenCalledTimes(1);
1276 | });
1277 | });
1278 |
1279 | describe("SSR", () => {
1280 | it("works", () => {
1281 | const Counter = () => {
1282 | const [count, setCount] = useState(0);
1283 |
1284 | return c("span", { style: { display: "none" } }, `Count: ${count}`);
1285 | };
1286 |
1287 | const tree = c("div", {}, c(Counter));
1288 |
1289 | const string = renderToString(tree);
1290 | expect(string).toBe(
1291 | ''
1292 | );
1293 | });
1294 |
1295 | it("hydrates", () => {
1296 | const mock = jest.fn();
1297 |
1298 | const Title = ({ children }: { children: string }) => {
1299 | return c("h1", {}, children);
1300 | };
1301 |
1302 | const App = () => {
1303 | const onClick = () => {
1304 | mock();
1305 | };
1306 |
1307 | return c(
1308 | "div",
1309 | { id: "main" },
1310 | c(Title, {}, "Hello"),
1311 | c("span", {}, "World"),
1312 | c("button", { onClick, id: "button" }, "Click")
1313 | );
1314 | };
1315 |
1316 | const tree = c(App);
1317 | const html = renderToString(tree);
1318 | document.body.innerHTML = html;
1319 |
1320 | const root = document.getElementById("root");
1321 |
1322 | if (root === null) {
1323 | throw new Error("Unexpected null.");
1324 | }
1325 |
1326 | hydrate(tree, root);
1327 |
1328 | const button = document.getElementById("button");
1329 |
1330 | if (button === null) {
1331 | throw new Error("Unexpected null.");
1332 | }
1333 |
1334 | button.click();
1335 | expect(mock).toHaveBeenCalledTimes(1);
1336 | });
1337 | });
1338 |
--------------------------------------------------------------------------------
/packages/remini/lib.ts:
--------------------------------------------------------------------------------
1 | import { host as domHost } from "./dom";
2 | import { SSRNode, host as ssrHost } from "./ssr";
3 | import {
4 | Child,
5 | Children,
6 | ComponentType,
7 | Context,
8 | EffectHook,
9 | HookType,
10 | HostNode,
11 | HostType,
12 | NodeType,
13 | Props,
14 | ProviderProps,
15 | RElement,
16 | RNode,
17 | } from "./types";
18 |
19 | export type { RElement } from "./types";
20 |
21 | export const Fragment = "";
22 |
23 | // const Avatar = ({ author }: { author: number }) => {
24 | // return createElement("div", { class: "123" }, author.toString());
25 | // };
26 |
27 | // createElement(Avatar, { author: 1 });
28 |
29 | // type FirstArgument = T extends (arg1: infer U) => RElement ? U : any;
30 |
31 | export let _rootNode: HostNode | null = null;
32 |
33 | let _currentNode: RNode | null = null;
34 | let _hookIndex = 0;
35 | let _currentHost: HostType | null = null;
36 | const _contextValues: Map, any> = new Map();
37 |
38 | const _componentToNode = new Map();
39 |
40 | type Job = {
41 | node: RNode;
42 | element: RElement | null;
43 | };
44 |
45 | type UpdateConfig = {
46 | host: HostType;
47 | isHydrating?: boolean;
48 | };
49 |
50 | let _updating = false;
51 | const _tasks: Job[] = [];
52 | const _effects: (() => void)[] = [];
53 |
54 | export function createElement(
55 | component: ComponentType,
56 | props: Props,
57 | children: Children
58 | ): RElement;
59 |
60 | export function createElement(
61 | component: ComponentType,
62 | props?: Props,
63 | ...children: Child[]
64 | ): RElement;
65 |
66 | export function createElement(
67 | component: any,
68 | props: any,
69 | ...children: any
70 | ): RElement {
71 | const p = {
72 | ...(props || {}),
73 | children: children
74 | .flat()
75 | .map((child: Child) => {
76 | if (typeof child === "string") {
77 | return {
78 | kind: NodeType.TEXT,
79 | content: child,
80 | };
81 | } else if (typeof child === "number") {
82 | return {
83 | kind: NodeType.TEXT,
84 | content: child.toString(),
85 | };
86 | } else {
87 | // Null and false will be passed here and filtered below.
88 | return child;
89 | }
90 | })
91 | .filter(Boolean),
92 | };
93 |
94 | if (typeof component === "function") {
95 | // Provider has context injected as a param to its function.
96 | if (component.context) {
97 | return {
98 | kind: NodeType.PROVIDER,
99 | props: { ...p, $$context: component.context },
100 | };
101 | }
102 |
103 | return {
104 | kind: NodeType.COMPONENT,
105 | render: component,
106 | props: p,
107 | };
108 | } else if (typeof component === "string") {
109 | if (component === "") {
110 | return {
111 | kind: NodeType.FRAGMENT,
112 | props: p,
113 | };
114 | }
115 |
116 | return {
117 | kind: NodeType.HOST,
118 | tag: component,
119 | props: p,
120 | };
121 | }
122 |
123 | throw new Error("Something went wrong.");
124 | }
125 |
126 | // Null element argument is meant for updating components.
127 | function update(node: RNode, element: RElement | null, config: UpdateConfig) {
128 | const { host, isHydrating } = config;
129 | _currentHost = host;
130 |
131 | const previousNode = _currentNode;
132 | const previousIndex = _hookIndex;
133 |
134 | let replacedContext = null;
135 |
136 | if (
137 | (element && element.kind === NodeType.TEXT) ||
138 | node.kind === NodeType.TEXT
139 | ) {
140 | return;
141 | }
142 |
143 | let elements: RElement[] = [];
144 | if (node.kind === NodeType.COMPONENT) {
145 | _currentNode = node;
146 | _hookIndex = 0;
147 | // This will be always one element array because this implementation doesn't
148 | // support returning arrays from render functions.
149 | elements = [node.render(node.props)].filter(Boolean) as RElement[];
150 | _hookIndex = 0;
151 | } else if (
152 | element &&
153 | "props" in element &&
154 | (node.kind === NodeType.HOST ||
155 | node.kind === NodeType.PROVIDER ||
156 | node.kind === NodeType.FRAGMENT)
157 | ) {
158 | if (node.kind === NodeType.PROVIDER) {
159 | const currentValue = _contextValues.get(node.context);
160 |
161 | if (currentValue) {
162 | replacedContext = {
163 | context: node.context,
164 | value: currentValue,
165 | };
166 | }
167 |
168 | _contextValues.set(node.context, { value: node.props.value });
169 | }
170 |
171 | elements = element.props.children;
172 | }
173 |
174 | if (
175 | isHydrating &&
176 | node.kind === NodeType.HOST &&
177 | element &&
178 | element.kind === NodeType.HOST
179 | ) {
180 | host.updateHostNode(node, element);
181 | }
182 |
183 | // Reconcile.
184 | const length = Math.max(node.descendants.length, elements.length);
185 | const pairs: [left: RNode | undefined, right: RElement | undefined][] = [];
186 | for (let i = 0; i < length; i++) {
187 | pairs.push([node.descendants[i], elements[i]]);
188 | }
189 |
190 | for (const [current, expected] of pairs) {
191 | if (
192 | current &&
193 | expected &&
194 | ((current.kind === NodeType.COMPONENT &&
195 | expected.kind === NodeType.COMPONENT &&
196 | current.render === expected.render) ||
197 | (current.kind === NodeType.HOST &&
198 | expected.kind === NodeType.HOST &&
199 | current.tag === expected.tag) ||
200 | (current.kind === NodeType.FRAGMENT &&
201 | expected.kind === NodeType.FRAGMENT) ||
202 | (current.kind === NodeType.PROVIDER &&
203 | expected.kind === NodeType.PROVIDER) ||
204 | (current.kind === NodeType.TEXT && expected.kind === NodeType.TEXT))
205 | ) {
206 | // UPDATE
207 | if (current.kind === NodeType.HOST && expected.kind === NodeType.HOST) {
208 | host.updateHostNode(current, expected);
209 | } else if (
210 | // Text value changed.
211 | current.kind === NodeType.TEXT &&
212 | expected.kind === NodeType.TEXT &&
213 | current.content !== expected.content
214 | ) {
215 | current.content = expected.content;
216 | host.updateTextNode(current, expected.content);
217 | }
218 |
219 | // Props can be updated.
220 | if ("props" in current && "props" in expected) {
221 | current.props = expected.props;
222 | }
223 |
224 | update(current, expected, config);
225 | } else if (current && expected) {
226 | // REPLACE
227 | let newNode: RNode;
228 | if (expected.kind === NodeType.COMPONENT) {
229 | newNode = {
230 | ...expected,
231 | parent: node,
232 | descendants: [],
233 | hooks: [],
234 | };
235 |
236 | host.removeHostNode(current);
237 | } else if (expected.kind === NodeType.HOST) {
238 | const firstParentWithHostNode = host.findClosestHostNode(node);
239 |
240 | const nodeConstruction: any = {
241 | ...expected,
242 | parent: node,
243 | descendants: [],
244 | };
245 |
246 | const native = host.createHostNode(expected);
247 | if (current.kind === NodeType.HOST || current.kind === NodeType.TEXT) {
248 | firstParentWithHostNode.native.replaceChild(native, current.native);
249 | } else {
250 | host.removeHostNode(current);
251 | host.appendChild(firstParentWithHostNode.native, native);
252 | }
253 | nodeConstruction.native = native;
254 |
255 | newNode = nodeConstruction;
256 | } else if (expected.kind === NodeType.TEXT) {
257 | const firstParentWithHostNode = host.findClosestHostNode(node);
258 | const nodeConstruction: any = {
259 | ...expected,
260 | parent: node,
261 | };
262 |
263 | const native = host.createTextNode(expected.content);
264 | if (current.kind === NodeType.TEXT) {
265 | throw new Error("Update should have happened on this node.");
266 | } else if (current.kind === NodeType.HOST) {
267 | firstParentWithHostNode.native.replaceChild(native, current.native);
268 | nodeConstruction.native = native;
269 | } else {
270 | host.removeHostNode(current);
271 | host.appendChild(firstParentWithHostNode.native, native);
272 | nodeConstruction.native = native;
273 | }
274 |
275 | newNode = nodeConstruction;
276 | } else if (expected.kind === NodeType.PROVIDER) {
277 | newNode = {
278 | ...expected,
279 | context: expected.props.$$context,
280 | parent: node,
281 | descendants: [],
282 | };
283 |
284 | host.removeHostNode(current);
285 | } else if (expected.kind === NodeType.FRAGMENT) {
286 | newNode = {
287 | ...expected,
288 | parent: node,
289 | descendants: [],
290 | };
291 |
292 | host.removeHostNode(current);
293 | } else {
294 | throw new Error("Couldn't resolve node kind.");
295 | }
296 |
297 | if (current.kind === NodeType.COMPONENT) {
298 | current.hooks.forEach((hook) => {
299 | if (hook.type === HookType.EFFECT && hook.cleanup) {
300 | hook.cleanup();
301 | }
302 | });
303 |
304 | // Remove node from mapping.
305 | if (import.meta.env.DEV && current.render.$id$) {
306 | _componentToNode.set(
307 | current.render.$id$,
308 | (_componentToNode.get(current.render.$id$) || []).filter((node) => {
309 | return node !== current;
310 | })
311 | );
312 | }
313 | }
314 |
315 | node.descendants[node.descendants.indexOf(current)] = newNode;
316 | update(newNode, expected, config);
317 | } else if (!current && expected !== undefined) {
318 | // ADD
319 | let newNode: RNode;
320 | if (expected.kind === NodeType.COMPONENT) {
321 | newNode = {
322 | ...expected,
323 | parent: node,
324 | descendants: [],
325 | hooks: [],
326 | };
327 |
328 | if (expected.kind === NodeType.COMPONENT) {
329 | if (import.meta.env.DEV && expected.render.$id$) {
330 | _componentToNode.set(
331 | expected.render.$id$,
332 | (_componentToNode.get(expected.render.$id$) || []).concat(newNode)
333 | );
334 | }
335 | }
336 | } else if (expected.kind === NodeType.HOST) {
337 | const nodeConstruction: any = {
338 | ...expected,
339 | parent: node,
340 | descendants: [],
341 | };
342 | const firstParentWithHostNode = host.findClosestHostNode(node);
343 |
344 | if (isHydrating) {
345 | nodeConstruction.native = _node;
346 | getNextNode();
347 | } else {
348 | nodeConstruction.native = host.createHostNode(expected);
349 | host.appendChild(
350 | firstParentWithHostNode.native,
351 | nodeConstruction.native
352 | );
353 | }
354 |
355 | newNode = nodeConstruction;
356 |
357 | // Handle useRef.
358 | const closestComponent = host.findClosestComponent(node);
359 | if (closestComponent && closestComponent.kind === NodeType.COMPONENT) {
360 | for (const hook of closestComponent.hooks) {
361 | if (hook.type === HookType.REF && expected.props.ref === hook) {
362 | hook.current = (newNode as HostNode).native;
363 | }
364 | }
365 | }
366 | } else if (expected.kind === NodeType.TEXT) {
367 | const nodeConstruction: any = {
368 | ...expected,
369 | parent: node,
370 | };
371 |
372 | const firstParentWithNative = host.findClosestHostNode(node);
373 |
374 | if (isHydrating) {
375 | nodeConstruction.native = _node;
376 | getNextNode();
377 | } else {
378 | const hostNode = host.createTextNode(expected.content);
379 | host.appendChild(firstParentWithNative.native, hostNode);
380 | nodeConstruction.native = hostNode;
381 | }
382 |
383 | newNode = nodeConstruction;
384 | } else if (expected.kind === NodeType.PROVIDER) {
385 | newNode = {
386 | ...expected,
387 | parent: node,
388 | descendants: [],
389 | context: expected.props.$$context,
390 | };
391 | } else if (expected.kind === NodeType.FRAGMENT) {
392 | newNode = {
393 | ...expected,
394 | parent: node,
395 | descendants: [],
396 | };
397 | } else {
398 | throw new Error("Couldn't resolve node kind.");
399 | }
400 |
401 | node.descendants.push(newNode);
402 | update(newNode, expected, config);
403 | } else if (current !== undefined && !expected) {
404 | // REMOVE
405 | const indexOfCurrent = node.descendants.indexOf(current);
406 |
407 | if (current.kind === NodeType.COMPONENT) {
408 | current.hooks.forEach((hook) => {
409 | if (hook.type === HookType.EFFECT && hook.cleanup) {
410 | hook.cleanup();
411 | }
412 | });
413 |
414 | // Remove node from mapping.
415 | if (import.meta.env.DEV && current.render.$id$) {
416 | _componentToNode.set(
417 | current.render.$id$,
418 | (_componentToNode.get(current.render.$id$) || []).filter((node) => {
419 | return node !== current;
420 | })
421 | );
422 | }
423 | } else if (current.kind === NodeType.PROVIDER) {
424 | _contextValues.delete(current.context);
425 | }
426 |
427 | host.removeHostNode(current);
428 | node.descendants.splice(indexOfCurrent, 1);
429 | }
430 | }
431 |
432 | if (node.kind === NodeType.PROVIDER && replacedContext !== null) {
433 | _contextValues.set(replacedContext.context, {
434 | value: replacedContext.value,
435 | });
436 | }
437 |
438 | _currentNode = previousNode;
439 | _hookIndex = previousIndex;
440 | _currentHost = null;
441 | }
442 |
443 | function runUpdateLoop(
444 | node: RNode,
445 | element: RElement | null,
446 | config: UpdateConfig
447 | ) {
448 | _tasks.push({ node, element });
449 |
450 | if (_updating) {
451 | return;
452 | }
453 |
454 | _updating = true;
455 |
456 | let current: Job | undefined;
457 | // Run all state updates.
458 | while ((current = _tasks.shift())) {
459 | if (typeof performance !== "undefined") {
460 | const start = performance.now();
461 | update(current.node, current.element, config);
462 | const end = performance.now();
463 | console.log(`${Math.round(end - start)}ms`);
464 | } else {
465 | update(current.node, current.element, config);
466 | }
467 |
468 | // Run all effects queued for this update.
469 | let effect: (() => void) | undefined;
470 | while ((effect = _effects.shift())) {
471 | effect();
472 | }
473 | }
474 |
475 | _contextValues.clear();
476 | _updating = false;
477 | }
478 |
479 | export function useEffect(
480 | callback: () => void | (() => void),
481 | dependencies?: any[]
482 | ): void {
483 | // Capture the current node.
484 | const c = _currentNode;
485 | const i = _hookIndex;
486 |
487 | if (!c || c.kind !== NodeType.COMPONENT) {
488 | throw new Error("Executing useEffect for non-function element.");
489 | }
490 |
491 | _effects.push(() => {
492 | if (!c || c.kind !== NodeType.COMPONENT) {
493 | throw new Error("Executing useEffect for non-function element.");
494 | }
495 |
496 | if (c.hooks[i] === undefined) {
497 | // INITIALIZE
498 | const hook: EffectHook = {
499 | type: HookType.EFFECT,
500 | cleanup: undefined,
501 | dependencies,
502 | };
503 | c.hooks[i] = hook;
504 | const cleanup = callback();
505 | hook.cleanup = cleanup ? cleanup : undefined;
506 | } else if (dependencies) {
507 | // COMPARE DEPENDENCIES
508 | const hook = c.hooks[i];
509 | if (hook.type !== HookType.EFFECT || hook.dependencies === undefined) {
510 | throw new Error("Something went wrong.");
511 | }
512 |
513 | let shouldRun = false;
514 | for (let j = 0; j < dependencies.length; j++) {
515 | if (dependencies[j] !== hook.dependencies[j]) {
516 | shouldRun = true;
517 | }
518 | }
519 |
520 | if (shouldRun) {
521 | const cleanup = callback();
522 | c.hooks[i] = {
523 | type: HookType.EFFECT,
524 | cleanup: cleanup ? cleanup : undefined,
525 | dependencies,
526 | };
527 | }
528 | } else if (!dependencies) {
529 | // RUN ALWAYS
530 | const cleanup = callback();
531 | c.hooks[i] = {
532 | type: HookType.EFFECT,
533 | cleanup: cleanup ? cleanup : undefined,
534 | dependencies,
535 | };
536 | }
537 | });
538 |
539 | _hookIndex += 1;
540 | }
541 |
542 | export function useState(
543 | initial: T
544 | ): [T, (next: T | ((current: T) => T)) => void] {
545 | // Capture the current node.
546 | const c = _currentNode;
547 | const i = _hookIndex;
548 | const h = _currentHost;
549 |
550 | if (!c || c.kind !== NodeType.COMPONENT) {
551 | throw new Error("Executing useState for non-function element.");
552 | }
553 |
554 | if (!h) {
555 | throw new Error("Missing host context.");
556 | }
557 |
558 | if (c.hooks[i] === undefined) {
559 | c.hooks[i] = {
560 | type: HookType.STATE,
561 | state: initial,
562 | };
563 | }
564 |
565 | const hook = c.hooks[i];
566 | if (hook.type !== HookType.STATE) {
567 | throw new Error("Something went wrong.");
568 | }
569 |
570 | const setState = (next: T | ((current: T) => T)) => {
571 | if (!c || c.kind !== NodeType.COMPONENT) {
572 | throw new Error("Executing useState for non-function element.");
573 | }
574 |
575 | // https://github.com/microsoft/TypeScript/issues/37663#issuecomment-856866935
576 | // In case of a different iframe, window or realm, next won't be instance
577 | // of the same Function and will be saved instead of treated as callback.
578 | if (next instanceof Function) {
579 | hook.state = next(hook.state);
580 | } else {
581 | hook.state = next;
582 | }
583 |
584 | runUpdateLoop(c, null, { host: h });
585 | };
586 |
587 | _hookIndex += 1;
588 |
589 | return [hook.state, setState];
590 | }
591 |
592 | export function useRef(): { current: T | null } {
593 | if (!_currentNode || _currentNode.kind !== NodeType.COMPONENT) {
594 | throw new Error("Can't use useRef on this node.");
595 | }
596 |
597 | let ref = _currentNode.hooks[_hookIndex];
598 | if (ref === undefined) {
599 | ref = {
600 | type: HookType.REF,
601 | current: null,
602 | };
603 | _currentNode.hooks[_hookIndex] = ref;
604 | }
605 |
606 | if (ref.type !== HookType.REF) {
607 | throw new Error("Something went wrong.");
608 | }
609 |
610 | _hookIndex += 1;
611 |
612 | return ref;
613 | }
614 |
615 | export function useMemo(callback: () => T, dependencies: any[]): T {
616 | if (!_currentNode || _currentNode.kind !== NodeType.COMPONENT) {
617 | throw new Error("Can't call useMemo on this node.");
618 | }
619 |
620 | if (_currentNode.hooks[_hookIndex] === undefined) {
621 | _currentNode.hooks[_hookIndex] = {
622 | type: HookType.MEMO,
623 | memo: callback(),
624 | dependencies,
625 | };
626 | } else {
627 | const hook = _currentNode.hooks[_hookIndex];
628 | if (hook.type !== HookType.MEMO || !hook.dependencies) {
629 | throw new Error("Something went wrong.");
630 | }
631 |
632 | let shouldRun = false;
633 | for (let j = 0; j < dependencies.length; j++) {
634 | if (dependencies[j] !== hook.dependencies[j]) {
635 | shouldRun = true;
636 | }
637 | }
638 |
639 | if (shouldRun) {
640 | const memo = callback();
641 | _currentNode.hooks[_hookIndex] = {
642 | type: HookType.MEMO,
643 | memo,
644 | dependencies,
645 | };
646 | }
647 | }
648 |
649 | const hook = _currentNode.hooks[_hookIndex];
650 | if (hook.type !== HookType.MEMO) {
651 | throw new Error("Something went wrong.");
652 | }
653 |
654 | _hookIndex += 1;
655 | return hook.memo;
656 | }
657 |
658 | export function createContext(): Context {
659 | const context: any = {};
660 |
661 | const providerRender = ({ value }: ProviderProps): RElement => {
662 | // Doesn't matter at all what is being returned here as long as it is of
663 | // RElement type.
664 | return createElement("a", {});
665 | };
666 |
667 | providerRender.context = context;
668 | context.Provider = providerRender;
669 | return context;
670 | }
671 |
672 | export function useContext(context: Context): T {
673 | if (!_currentNode || _currentNode.kind !== NodeType.COMPONENT) {
674 | throw new Error("Can't call useContext on this node.");
675 | }
676 |
677 | const newValue = _contextValues.get(context);
678 | if (_currentNode.hooks[_hookIndex] === undefined || newValue) {
679 | _currentNode.hooks[_hookIndex] = {
680 | type: HookType.CONTEXT,
681 | context: newValue.value,
682 | };
683 | }
684 |
685 | const hook = _currentNode.hooks[_hookIndex];
686 | if (hook.type !== HookType.CONTEXT) {
687 | throw new Error("Something went wrong.");
688 | }
689 |
690 | _hookIndex += 1;
691 | return hook.context;
692 | }
693 |
694 | export function render(element: RElement, container: HTMLElement): void {
695 | _rootNode = {
696 | kind: NodeType.HOST,
697 | props: {
698 | children: [element],
699 | },
700 | tag: container.tagName.toLowerCase(),
701 | native: container,
702 | parent: null,
703 | descendants: [],
704 | };
705 |
706 | _componentToNode.clear();
707 |
708 | runUpdateLoop(_rootNode, createElement("div", {}, element), {
709 | host: domHost,
710 | });
711 | }
712 |
713 | export function renderWithConfig(
714 | element: RElement,
715 | container: HTMLElement,
716 | config: UpdateConfig
717 | ): void {
718 | _rootNode = {
719 | kind: NodeType.HOST,
720 | props: {
721 | children: [element],
722 | },
723 | tag: container.tagName.toLowerCase(),
724 | native: container,
725 | parent: null,
726 | descendants: [],
727 | };
728 |
729 | _componentToNode.clear();
730 |
731 | runUpdateLoop(_rootNode, createElement("div", {}, element), config);
732 | }
733 |
734 | const printSSRTree = (node: SSRNode | string): string => {
735 | if (typeof node === "string") {
736 | return node;
737 | }
738 |
739 | const optionalSpace = Object.keys(node.attributes).length > 0 ? " " : "";
740 |
741 | const attributes = Object.keys(node.attributes)
742 | .map((key) => `${key}="${node.attributes[key]}"`)
743 | .join(" ");
744 |
745 | const children = node.children.map((child) => printSSRTree(child)).join("");
746 |
747 | return `<${node.tag}${optionalSpace}${attributes}>${children}${node.tag}>`;
748 | };
749 |
750 | export function renderToString(element: RElement): string {
751 | _rootNode = {
752 | kind: NodeType.HOST,
753 | props: {
754 | children: [element],
755 | id: "root",
756 | },
757 | tag: "div",
758 | native: { tag: "div", attributes: { id: "root" }, children: [] },
759 | parent: null,
760 | descendants: [],
761 | };
762 |
763 | runUpdateLoop(_rootNode, createElement("div", {}, element), {
764 | host: ssrHost,
765 | });
766 |
767 | return printSSRTree(_rootNode.native);
768 | }
769 |
770 | export function hydrate(element: RElement, container: HTMLElement): void {
771 | _rootNode = {
772 | kind: NodeType.HOST,
773 | props: {
774 | children: [element],
775 | },
776 | tag: container.tagName.toLowerCase(),
777 | native: container,
778 | parent: null,
779 | descendants: [],
780 | };
781 |
782 | _node = container.firstChild as Node;
783 |
784 | runUpdateLoop(_rootNode, createElement("div", {}, element), {
785 | host: domHost,
786 | isHydrating: true,
787 | });
788 | _node = null;
789 | }
790 |
791 | // TODO
792 | // Move somewhere outside.
793 | let _node: Node | null = null;
794 | function getNextNode() {
795 | if (_node === null) {
796 | return;
797 | } else if (_node.firstChild) {
798 | _node = _node.firstChild;
799 | } else if (_node.nextSibling) {
800 | _node = _node.nextSibling;
801 | } else {
802 | while (!_node.nextSibling) {
803 | _node = _node.parentNode;
804 |
805 | if (_node === null) {
806 | return;
807 | }
808 | }
809 | _node = _node.nextSibling;
810 | }
811 | }
812 |
813 | if (import.meta.env.DEV && typeof window !== "undefined") {
814 | window.__UPDATE__ = (node: RNode) =>
815 | runUpdateLoop(node, null, { host: domHost });
816 | window.__COMPONENT_TO_NODE__ = _componentToNode;
817 | }
818 |
--------------------------------------------------------------------------------
/packages/remini/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "remini",
3 | "version": "0.0.0",
4 | "files": [
5 | "dist"
6 | ],
7 | "main": "./dist/remini.umd.js",
8 | "module": "./dist/remini.es.js",
9 | "exports": {
10 | ".": {
11 | "import": "./dist/remini.es.js",
12 | "require": "./dist/remini.umd.js"
13 | }
14 | },
15 | "scripts": {
16 | "build": "vite build",
17 | "test": "jest --watch --env=jsdom"
18 | },
19 | "devDependencies": {
20 | "@babel/core": "^7.14.8",
21 | "@babel/preset-env": "^7.14.8",
22 | "@babel/preset-typescript": "^7.14.5",
23 | "@types/jest": "^26.0.24",
24 | "@typescript-eslint/eslint-plugin": "^4.28.4",
25 | "@typescript-eslint/parser": "^4.28.4",
26 | "babel-jest": "^27.0.6",
27 | "eslint": "^7.30.0",
28 | "jest": "^27.0.6",
29 | "prettier": "^2.3.2",
30 | "typescript": "^4.3.5",
31 | "vite": "^2.4.3"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/remini/ssr.ts:
--------------------------------------------------------------------------------
1 | import { HostElement, HostNode, NodeType, RNode, TextNode } from "./types";
2 | import {
3 | isEvent,
4 | keyToAttribute,
5 | styleObjectToString,
6 | findClosestComponent,
7 | findClosestHostNode,
8 | } from "./utils";
9 |
10 | export type SSRNode = {
11 | tag: string;
12 | attributes: { [key: string]: string };
13 | children: (SSRNode | string)[];
14 | };
15 |
16 | export function removeHostNode(node: RNode): void {
17 | if (node.kind === NodeType.HOST || node.kind === NodeType.TEXT) {
18 | const { children } = node.native.parent;
19 | children.splice(children.indexOf(node.native), 1);
20 | } else {
21 | for (let i = 0; i < node.descendants.length; i++) {
22 | removeHostNode(node.descendants[i]);
23 | }
24 | }
25 | }
26 |
27 | // TODO
28 | // Remove code that is repeated in DOM.
29 |
30 | export function createHostNode(element: HostElement): SSRNode {
31 | const html: SSRNode = {
32 | tag: element.tag,
33 | children: [],
34 | attributes: {},
35 | };
36 |
37 | const props = Object.entries(element.props);
38 | for (let i = 0; i < props.length; i++) {
39 | const [key, value] = props[i];
40 |
41 | if (key === "children" || key === "ref") {
42 | // Skip.
43 | } else if (key === "style") {
44 | const style =
45 | typeof value === "string" ? value : styleObjectToString(value);
46 | html.attributes[key] = style;
47 | } else if (isEvent(key)) {
48 | //
49 | } else {
50 | html.attributes[keyToAttribute(key)] = value;
51 | }
52 | }
53 |
54 | return html;
55 | }
56 |
57 | export function updateHostNode(current: HostNode, expected: HostElement): void {
58 | const html = current.native as SSRNode;
59 |
60 | const currentKeys = Object.keys(current.props);
61 | for (let i = 0; i < currentKeys.length; i++) {
62 | const key = currentKeys[i];
63 | if (key === "children" || key === "ref") {
64 | // Skip.
65 | } else if (isEvent(key)) {
66 | // html.removeEventListener(eventToKeyword(key), current.props[key]);
67 | } else {
68 | // Prop will be removed.
69 | if (!expected.props[key]) {
70 | delete html.attributes[key];
71 | }
72 | }
73 | }
74 |
75 | const expectedKeys = Object.keys(expected.props);
76 | for (let i = 0; i < expectedKeys.length; i++) {
77 | const key = expectedKeys[i];
78 | if (key === "children" || key === "ref") {
79 | // Skip.
80 | } else if (isEvent(key)) {
81 | // html.addEventListener(eventToKeyword(key), expected.props[key]);
82 | } else {
83 | // Prop will be added/updated.
84 | if (expected.props[key] !== current.props[key]) {
85 | if (key === "style") {
86 | const style =
87 | typeof current.props[key] === "string"
88 | ? current.props[key]
89 | : styleObjectToString(current.props[key]);
90 | html.attributes[key] = style;
91 | } else {
92 | html.attributes[keyToAttribute(key)] = expected.props[key] as string;
93 | }
94 | }
95 | }
96 | }
97 | }
98 |
99 | export function appendChild(parent: SSRNode, child: SSRNode): void {
100 | parent.children.push(child);
101 | }
102 |
103 | export function createTextNode(text: string): string {
104 | return text;
105 | }
106 |
107 | export function updateTextNode(node: TextNode, text: string): void {
108 | node.native = text;
109 | }
110 |
111 | export const host = {
112 | findClosestComponent,
113 | createHostNode,
114 | findClosestHostNode,
115 | removeHostNode,
116 | updateHostNode,
117 | appendChild,
118 | createTextNode,
119 | updateTextNode,
120 | };
121 |
--------------------------------------------------------------------------------
/packages/remini/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "ES2020",
4 | "noImplicitAny": true,
5 | "removeComments": true,
6 | "preserveConstEnums": true,
7 | "sourceMap": true,
8 | "strictNullChecks": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/remini/types.ts:
--------------------------------------------------------------------------------
1 | declare global {
2 | interface Window {
3 | __UPDATE__: (node: RNode) => void;
4 | __COMPONENT_TO_NODE__: Map;
5 | }
6 | }
7 |
8 | export type HostType = {
9 | findClosestComponent: (node: RNode) => ComponentNode | null;
10 | findClosestHostNode: (node: RNode) => HostNode;
11 | createHostNode: (element: HostElement) => T;
12 | updateHostNode: (current: HostNode, expected: HostElement) => void;
13 | removeHostNode: (hostNode: RNode) => void;
14 | appendChild: (parent: T, child: T) => void;
15 | createTextNode: (text: string) => R;
16 | updateTextNode: (current: TextNode, text: string) => void;
17 | };
18 |
19 | export type Child = RElement | string | number | false | null;
20 | export type Children = RElement[] | string | number | false | null;
21 |
22 | export type ProviderProps = { value: T };
23 |
24 | export type Context = {
25 | Provider: ({ value }: ProviderProps) => RElement;
26 | };
27 |
28 | export type ElementProps = {
29 | children: RElement[];
30 | [key: string]: any;
31 | };
32 |
33 | export type Props = {
34 | [key: string]: any;
35 | style?: Record | string;
36 | };
37 |
38 | export type RenderFunction = ((props: any) => RElement | null) & {
39 | $id$?: string;
40 | };
41 |
42 | export type ComponentType = RenderFunction | string;
43 |
44 | export enum NodeType {
45 | COMPONENT,
46 | HOST,
47 | TEXT,
48 | PROVIDER,
49 | FRAGMENT,
50 | }
51 |
52 | export type ComponentElement = {
53 | kind: NodeType.COMPONENT;
54 | render: RenderFunction;
55 | props: ElementProps;
56 | };
57 |
58 | export type ComponentNode = ComponentElement & {
59 | parent: RNode | null;
60 | descendants: RNode[];
61 | hooks: Hook[];
62 | };
63 |
64 | export type HostElement = {
65 | kind: NodeType.HOST;
66 | tag: string;
67 | props: ElementProps;
68 | };
69 |
70 | export type HostNode = HostElement & {
71 | parent: RNode | null;
72 | descendants: RNode[];
73 | native: any;
74 | };
75 |
76 | export type TextElement = {
77 | kind: NodeType.TEXT;
78 | content: string;
79 | };
80 |
81 | export type TextNode = TextElement & {
82 | parent: RNode | null;
83 | native: any;
84 | };
85 |
86 | export type ProviderElement = {
87 | kind: NodeType.PROVIDER;
88 | props: ElementProps;
89 | };
90 |
91 | export type ProviderNode = ProviderElement & {
92 | parent: RNode | null;
93 | context: Context;
94 | descendants: RNode[];
95 | };
96 |
97 | export type FragmentElement = {
98 | kind: NodeType.FRAGMENT;
99 | props: ElementProps;
100 | };
101 |
102 | export type FragmentNode = FragmentElement & {
103 | parent: RNode | null;
104 | descendants: RNode[];
105 | };
106 |
107 | export type RElement =
108 | | ComponentElement
109 | | HostElement
110 | | TextElement
111 | | ProviderElement
112 | | FragmentElement;
113 |
114 | export type RNode =
115 | | ComponentNode
116 | | HostNode
117 | | TextNode
118 | | ProviderNode
119 | | FragmentNode;
120 |
121 | export enum HookType {
122 | STATE,
123 | EFFECT,
124 | REF,
125 | CONTEXT,
126 | MEMO,
127 | }
128 |
129 | export type StateHook = {
130 | type: HookType.STATE;
131 | state: any;
132 | };
133 |
134 | export type EffectHook = {
135 | type: HookType.EFFECT;
136 | cleanup: (() => void) | undefined;
137 | dependencies?: any[];
138 | };
139 |
140 | export type RefHook = {
141 | type: HookType.REF;
142 | current: any;
143 | };
144 |
145 | export type ContextHook = {
146 | type: HookType.CONTEXT;
147 | context: any;
148 | };
149 |
150 | export type MemoHook = {
151 | type: HookType.MEMO;
152 | memo: any;
153 | dependencies?: any[];
154 | };
155 |
156 | export type Hook = StateHook | EffectHook | RefHook | ContextHook | MemoHook;
157 |
--------------------------------------------------------------------------------
/packages/remini/utils.ts:
--------------------------------------------------------------------------------
1 | import { ComponentNode, HostNode, NodeType, RNode } from "./types";
2 |
3 | export const isEvent = (key: string): boolean =>
4 | !!key.match(new RegExp("on[A-Z].*"));
5 |
6 | export const eventToKeyword = (key: string): string =>
7 | key.replace("on", "").toLowerCase();
8 |
9 | export const keyToAttribute = (key: string): string => {
10 | if (key === "viewBox") {
11 | return key;
12 | } else {
13 | return camelCaseToKebab(key);
14 | }
15 | };
16 |
17 | const camelCaseToKebab = (str: string) =>
18 | str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
19 |
20 | export function styleObjectToString(style: {
21 | [key: string]: string | number;
22 | }): string {
23 | const string = Object.keys(style)
24 | .map((key) => {
25 | const value = style[key];
26 | return `${camelCaseToKebab(key)}:${value}`;
27 | })
28 | .join(";");
29 | return string;
30 | }
31 |
32 | export function findClosestComponent(node: RNode): ComponentNode | null {
33 | let current = node;
34 |
35 | while (current.kind !== NodeType.COMPONENT && current.parent) {
36 | current = current.parent;
37 | }
38 |
39 | if (current.kind !== NodeType.COMPONENT) {
40 | return null;
41 | }
42 |
43 | return current;
44 | }
45 |
46 | export function findClosestHostNode(node: RNode): HostNode {
47 | let current = node;
48 |
49 | while (current.kind !== NodeType.HOST && current.parent) {
50 | current = current.parent;
51 | }
52 |
53 | // Only interested in looking for host node as text node wouldn't have
54 | // children anyway.
55 | if (current.kind !== NodeType.HOST) {
56 | throw new Error("Couldn't find node.");
57 | }
58 |
59 | return current;
60 | }
61 |
--------------------------------------------------------------------------------
/packages/remini/vite.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | module.exports = {
4 | build: {
5 | lib: {
6 | entry: path.resolve(__dirname, "lib.ts"),
7 | name: "remini",
8 | },
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/packages/vite-plugin/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | node: true,
4 | commonjs: true,
5 | es2021: true,
6 | },
7 | extends: ["eslint:recommended"],
8 | };
9 |
--------------------------------------------------------------------------------
/packages/vite-plugin/index.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 | const { transformSync } = require("@babel/core");
4 | const babelPlugin = require("../babel-plugin");
5 |
6 | // /@fast-refresh is arbitrary, the only part that matters is the initial /.
7 | // It then gets resolved by resolveId() which then is recognized by load()
8 | // function.
9 | const runtimePublicPath = "/@fast-refresh";
10 |
11 | // path.join is used to resolve path locally to this file and not the directory
12 | // in which plugin is currently running.
13 | const runtimeFilePath = path.join(__dirname, "../babel-plugin/runtime.js");
14 |
15 | const runtimeCode = fs.readFileSync(runtimeFilePath, "utf-8");
16 |
17 | function isComponentLikeIdentifier(node) {
18 | return (
19 | node.type === "Identifier" &&
20 | typeof node.name === "string" &&
21 | /^[A-Z]/.test(node.name)
22 | );
23 | }
24 |
25 | // This initializing code is injected in transformIndexHtml().
26 | const preambleCode = `
27 | import RefreshRuntime from "${runtimePublicPath}"
28 | // RefreshRuntime.injectIntoGlobalHook(window);
29 | window.$RefreshReg$ = () => {};
30 | window.$id$ = () => {};
31 | window.__REFRESH_PLUGIN_ENABLED__ = true;
32 | `;
33 |
34 | const cleanIndent = (code) => {
35 | const lines = code.split("\n");
36 | // First line is often immediately followed by new line and is not
37 | // representative of the whitespace in the snippet.
38 | const firstLine = lines[0].length === 0 ? 1 : 0;
39 | const offset = lines[firstLine].match(/^(\s*)/)[0].length;
40 | return lines.map((line) => line.substring(offset, line.length)).join("\n");
41 | };
42 |
43 | // If every export is a component, we can assume it's a refresh boundary.
44 | function isRefreshBoundary(ast) {
45 | return ast.program.body.every((node) => {
46 | if (node.type !== "ExportNamedDeclaration") {
47 | return true;
48 | }
49 |
50 | const { declaration, specifiers } = node;
51 | if (declaration) {
52 | if (declaration.type === "VariableDeclaration") {
53 | return declaration.declarations.every((variable) =>
54 | isComponentLikeIdentifier(variable.id)
55 | );
56 | }
57 |
58 | if (declaration.type === "FunctionDeclaration") {
59 | return isComponentLikeIdentifier(declaration.id);
60 | }
61 | }
62 |
63 | return specifiers.every((spec) => {
64 | return isComponentLikeIdentifier(spec.exported);
65 | });
66 | });
67 | }
68 |
69 | function refreshPlugin() {
70 | let shouldSkip = false;
71 |
72 | return {
73 | name: "refresh",
74 | enforce: "pre",
75 | configResolved(config) {
76 | shouldSkip = config.command === "build" || config.isProduction;
77 | },
78 | resolveId(id) {
79 | if (id === runtimePublicPath) {
80 | return id;
81 | }
82 | },
83 |
84 | load(id) {
85 | if (id === runtimePublicPath) {
86 | return runtimeCode;
87 | }
88 | },
89 | transformIndexHtml() {
90 | if (shouldSkip) {
91 | return;
92 | }
93 |
94 | return [
95 | {
96 | tag: "script",
97 | attrs: { type: "module" },
98 | children: preambleCode,
99 | },
100 | ];
101 | },
102 | async transform(code, id, ssr) {
103 | if (
104 | shouldSkip ||
105 | !/\.(t|j)sx?$/.test(id) ||
106 | id.includes("node_modules") ||
107 | id.includes("?worker") ||
108 | ssr
109 | ) {
110 | return;
111 | }
112 |
113 | // Potentially might require adding JSX or decorators support in future.
114 | const parserPlugins = ["jsx", /\.tsx?$/.test(id) && "typescript"];
115 |
116 | const result = transformSync(code, {
117 | babelrc: false,
118 | configFile: false,
119 | filename: id,
120 | parserOpts: {
121 | sourceType: "module",
122 | allowAwaitOutsideFunction: true,
123 | plugins: parserPlugins,
124 | },
125 | plugins: [babelPlugin],
126 | ast: true,
127 | sourceMaps: true,
128 | sourceFileName: id,
129 | });
130 |
131 | if (result === null) {
132 | throw new Error("Babel transformation didn't succeed.");
133 | }
134 |
135 | // No component detected in the file. Don't inject code.
136 | if (!/\$RefreshReg\$\(/.test(result.code)) {
137 | return code;
138 | }
139 |
140 | const header = cleanIndent(`
141 | import RefreshRuntime from "${runtimePublicPath}";
142 | let prevRefreshReg;
143 | let prevId;
144 |
145 | if (!__REFRESH_PLUGIN_ENABLED__) {
146 | throw new Error("Refresh plugin can't detect initialization code.");
147 | }
148 |
149 | if (import.meta.hot) {
150 | prevRefreshReg = window.$RefreseshReg$;
151 | prevId = window.$id$;
152 | window.$RefreshReg$ = (render, id, name, hooks) => {
153 | RefreshRuntime.register(render, id, name, hooks);
154 | };
155 | window.$id$ = () => {
156 | return '${id}';
157 | };
158 | }
159 | //
160 | `);
161 |
162 | const footer = cleanIndent(`
163 | //
164 | if (import.meta.hot) {
165 | window.$RefreshReg$ = prevRefreshReg;
166 |
167 | if (${isRefreshBoundary(result.ast)}) {
168 | import.meta.hot.accept();
169 | }
170 |
171 | if (!window.__REFRESH_TIMEOUT__) {
172 | window.__REFRESH_TIMEOUT__ = setTimeout(() => {
173 | window.__REFRESH_TIMEOUT__ = 0;
174 | RefreshRuntime.performRefresh();
175 | }, 30);
176 | }
177 | }
178 | `);
179 |
180 | return {
181 | code: `${header}${result.code}${footer}`,
182 | map: result.map,
183 | };
184 | },
185 | };
186 | }
187 |
188 | module.exports = refreshPlugin;
189 |
--------------------------------------------------------------------------------
/packages/vite-plugin/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vite-plugin",
3 | "version": "0.0.0",
4 | "dependencies": {
5 | "@babel/core": "^7.14.8"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/vite-plugin/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@babel/code-frame@^7.14.5":
6 | version "7.14.5"
7 | resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb"
8 | integrity sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==
9 | dependencies:
10 | "@babel/highlight" "^7.14.5"
11 |
12 | "@babel/compat-data@^7.14.5":
13 | version "7.14.7"
14 | resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.14.7.tgz#7b047d7a3a89a67d2258dc61f604f098f1bc7e08"
15 | integrity sha512-nS6dZaISCXJ3+518CWiBfEr//gHyMO02uDxBkXTKZDN5POruCnOZ1N4YBRZDCabwF8nZMWBpRxIicmXtBs+fvw==
16 |
17 | "@babel/core@^7.14.8":
18 | version "7.14.8"
19 | resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.14.8.tgz#20cdf7c84b5d86d83fac8710a8bc605a7ba3f010"
20 | integrity sha512-/AtaeEhT6ErpDhInbXmjHcUQXH0L0TEgscfcxk1qbOvLuKCa5aZT0SOOtDKFY96/CLROwbLSKyFor6idgNaU4Q==
21 | dependencies:
22 | "@babel/code-frame" "^7.14.5"
23 | "@babel/generator" "^7.14.8"
24 | "@babel/helper-compilation-targets" "^7.14.5"
25 | "@babel/helper-module-transforms" "^7.14.8"
26 | "@babel/helpers" "^7.14.8"
27 | "@babel/parser" "^7.14.8"
28 | "@babel/template" "^7.14.5"
29 | "@babel/traverse" "^7.14.8"
30 | "@babel/types" "^7.14.8"
31 | convert-source-map "^1.7.0"
32 | debug "^4.1.0"
33 | gensync "^1.0.0-beta.2"
34 | json5 "^2.1.2"
35 | semver "^6.3.0"
36 | source-map "^0.5.0"
37 |
38 | "@babel/generator@^7.14.8":
39 | version "7.14.8"
40 | resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.14.8.tgz#bf86fd6af96cf3b74395a8ca409515f89423e070"
41 | integrity sha512-cYDUpvIzhBVnMzRoY1fkSEhK/HmwEVwlyULYgn/tMQYd6Obag3ylCjONle3gdErfXBW61SVTlR9QR7uWlgeIkg==
42 | dependencies:
43 | "@babel/types" "^7.14.8"
44 | jsesc "^2.5.1"
45 | source-map "^0.5.0"
46 |
47 | "@babel/helper-compilation-targets@^7.14.5":
48 | version "7.14.5"
49 | resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.14.5.tgz#7a99c5d0967911e972fe2c3411f7d5b498498ecf"
50 | integrity sha512-v+QtZqXEiOnpO6EYvlImB6zCD2Lel06RzOPzmkz/D/XgQiUu3C/Jb1LOqSt/AIA34TYi/Q+KlT8vTQrgdxkbLw==
51 | dependencies:
52 | "@babel/compat-data" "^7.14.5"
53 | "@babel/helper-validator-option" "^7.14.5"
54 | browserslist "^4.16.6"
55 | semver "^6.3.0"
56 |
57 | "@babel/helper-function-name@^7.14.5":
58 | version "7.14.5"
59 | resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz#89e2c474972f15d8e233b52ee8c480e2cfcd50c4"
60 | integrity sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==
61 | dependencies:
62 | "@babel/helper-get-function-arity" "^7.14.5"
63 | "@babel/template" "^7.14.5"
64 | "@babel/types" "^7.14.5"
65 |
66 | "@babel/helper-get-function-arity@^7.14.5":
67 | version "7.14.5"
68 | resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz#25fbfa579b0937eee1f3b805ece4ce398c431815"
69 | integrity sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==
70 | dependencies:
71 | "@babel/types" "^7.14.5"
72 |
73 | "@babel/helper-hoist-variables@^7.14.5":
74 | version "7.14.5"
75 | resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz#e0dd27c33a78e577d7c8884916a3e7ef1f7c7f8d"
76 | integrity sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ==
77 | dependencies:
78 | "@babel/types" "^7.14.5"
79 |
80 | "@babel/helper-member-expression-to-functions@^7.14.5":
81 | version "7.14.7"
82 | resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.7.tgz#97e56244beb94211fe277bd818e3a329c66f7970"
83 | integrity sha512-TMUt4xKxJn6ccjcOW7c4hlwyJArizskAhoSTOCkA0uZ+KghIaci0Qg9R043kUMWI9mtQfgny+NQ5QATnZ+paaA==
84 | dependencies:
85 | "@babel/types" "^7.14.5"
86 |
87 | "@babel/helper-module-imports@^7.14.5":
88 | version "7.14.5"
89 | resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz#6d1a44df6a38c957aa7c312da076429f11b422f3"
90 | integrity sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==
91 | dependencies:
92 | "@babel/types" "^7.14.5"
93 |
94 | "@babel/helper-module-transforms@^7.14.8":
95 | version "7.14.8"
96 | resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.14.8.tgz#d4279f7e3fd5f4d5d342d833af36d4dd87d7dc49"
97 | integrity sha512-RyE+NFOjXn5A9YU1dkpeBaduagTlZ0+fccnIcAGbv1KGUlReBj7utF7oEth8IdIBQPcux0DDgW5MFBH2xu9KcA==
98 | dependencies:
99 | "@babel/helper-module-imports" "^7.14.5"
100 | "@babel/helper-replace-supers" "^7.14.5"
101 | "@babel/helper-simple-access" "^7.14.8"
102 | "@babel/helper-split-export-declaration" "^7.14.5"
103 | "@babel/helper-validator-identifier" "^7.14.8"
104 | "@babel/template" "^7.14.5"
105 | "@babel/traverse" "^7.14.8"
106 | "@babel/types" "^7.14.8"
107 |
108 | "@babel/helper-optimise-call-expression@^7.14.5":
109 | version "7.14.5"
110 | resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz#f27395a8619e0665b3f0364cddb41c25d71b499c"
111 | integrity sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA==
112 | dependencies:
113 | "@babel/types" "^7.14.5"
114 |
115 | "@babel/helper-replace-supers@^7.14.5":
116 | version "7.14.5"
117 | resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.14.5.tgz#0ecc0b03c41cd567b4024ea016134c28414abb94"
118 | integrity sha512-3i1Qe9/8x/hCHINujn+iuHy+mMRLoc77b2nI9TB0zjH1hvn9qGlXjWlggdwUcju36PkPCy/lpM7LLUdcTyH4Ow==
119 | dependencies:
120 | "@babel/helper-member-expression-to-functions" "^7.14.5"
121 | "@babel/helper-optimise-call-expression" "^7.14.5"
122 | "@babel/traverse" "^7.14.5"
123 | "@babel/types" "^7.14.5"
124 |
125 | "@babel/helper-simple-access@^7.14.8":
126 | version "7.14.8"
127 | resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.14.8.tgz#82e1fec0644a7e775c74d305f212c39f8fe73924"
128 | integrity sha512-TrFN4RHh9gnWEU+s7JloIho2T76GPwRHhdzOWLqTrMnlas8T9O7ec+oEDNsRXndOmru9ymH9DFrEOxpzPoSbdg==
129 | dependencies:
130 | "@babel/types" "^7.14.8"
131 |
132 | "@babel/helper-split-export-declaration@^7.14.5":
133 | version "7.14.5"
134 | resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz#22b23a54ef51c2b7605d851930c1976dd0bc693a"
135 | integrity sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==
136 | dependencies:
137 | "@babel/types" "^7.14.5"
138 |
139 | "@babel/helper-validator-identifier@^7.14.5", "@babel/helper-validator-identifier@^7.14.8":
140 | version "7.14.8"
141 | resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.8.tgz#32be33a756f29e278a0d644fa08a2c9e0f88a34c"
142 | integrity sha512-ZGy6/XQjllhYQrNw/3zfWRwZCTVSiBLZ9DHVZxn9n2gip/7ab8mv2TWlKPIBk26RwedCBoWdjLmn+t9na2Gcow==
143 |
144 | "@babel/helper-validator-option@^7.14.5":
145 | version "7.14.5"
146 | resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3"
147 | integrity sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==
148 |
149 | "@babel/helpers@^7.14.8":
150 | version "7.14.8"
151 | resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.14.8.tgz#839f88f463025886cff7f85a35297007e2da1b77"
152 | integrity sha512-ZRDmI56pnV+p1dH6d+UN6GINGz7Krps3+270qqI9UJ4wxYThfAIcI5i7j5vXC4FJ3Wap+S9qcebxeYiqn87DZw==
153 | dependencies:
154 | "@babel/template" "^7.14.5"
155 | "@babel/traverse" "^7.14.8"
156 | "@babel/types" "^7.14.8"
157 |
158 | "@babel/highlight@^7.14.5":
159 | version "7.14.5"
160 | resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9"
161 | integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==
162 | dependencies:
163 | "@babel/helper-validator-identifier" "^7.14.5"
164 | chalk "^2.0.0"
165 | js-tokens "^4.0.0"
166 |
167 | "@babel/parser@^7.14.5", "@babel/parser@^7.14.8":
168 | version "7.14.8"
169 | resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.8.tgz#66fd41666b2d7b840bd5ace7f7416d5ac60208d4"
170 | integrity sha512-syoCQFOoo/fzkWDeM0dLEZi5xqurb5vuyzwIMNZRNun+N/9A4cUZeQaE7dTrB8jGaKuJRBtEOajtnmw0I5hvvA==
171 |
172 | "@babel/template@^7.14.5":
173 | version "7.14.5"
174 | resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.14.5.tgz#a9bc9d8b33354ff6e55a9c60d1109200a68974f4"
175 | integrity sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==
176 | dependencies:
177 | "@babel/code-frame" "^7.14.5"
178 | "@babel/parser" "^7.14.5"
179 | "@babel/types" "^7.14.5"
180 |
181 | "@babel/traverse@^7.14.5", "@babel/traverse@^7.14.8":
182 | version "7.14.8"
183 | resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.14.8.tgz#c0253f02677c5de1a8ff9df6b0aacbec7da1a8ce"
184 | integrity sha512-kexHhzCljJcFNn1KYAQ6A5wxMRzq9ebYpEDV4+WdNyr3i7O44tanbDOR/xjiG2F3sllan+LgwK+7OMk0EmydHg==
185 | dependencies:
186 | "@babel/code-frame" "^7.14.5"
187 | "@babel/generator" "^7.14.8"
188 | "@babel/helper-function-name" "^7.14.5"
189 | "@babel/helper-hoist-variables" "^7.14.5"
190 | "@babel/helper-split-export-declaration" "^7.14.5"
191 | "@babel/parser" "^7.14.8"
192 | "@babel/types" "^7.14.8"
193 | debug "^4.1.0"
194 | globals "^11.1.0"
195 |
196 | "@babel/types@^7.14.5", "@babel/types@^7.14.8":
197 | version "7.14.8"
198 | resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.14.8.tgz#38109de8fcadc06415fbd9b74df0065d4d41c728"
199 | integrity sha512-iob4soQa7dZw8nodR/KlOQkPh9S4I8RwCxwRIFuiMRYjOzH/KJzdUfDgz6cGi5dDaclXF4P2PAhCdrBJNIg68Q==
200 | dependencies:
201 | "@babel/helper-validator-identifier" "^7.14.8"
202 | to-fast-properties "^2.0.0"
203 |
204 | ansi-styles@^3.2.1:
205 | version "3.2.1"
206 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
207 | integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
208 | dependencies:
209 | color-convert "^1.9.0"
210 |
211 | browserslist@^4.16.6:
212 | version "4.16.6"
213 | resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2"
214 | integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==
215 | dependencies:
216 | caniuse-lite "^1.0.30001219"
217 | colorette "^1.2.2"
218 | electron-to-chromium "^1.3.723"
219 | escalade "^3.1.1"
220 | node-releases "^1.1.71"
221 |
222 | caniuse-lite@^1.0.30001219:
223 | version "1.0.30001248"
224 | resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001248.tgz#26ab45e340f155ea5da2920dadb76a533cb8ebce"
225 | integrity sha512-NwlQbJkxUFJ8nMErnGtT0QTM2TJ33xgz4KXJSMIrjXIbDVdaYueGyjOrLKRtJC+rTiWfi6j5cnZN1NBiSBJGNw==
226 |
227 | chalk@^2.0.0:
228 | version "2.4.2"
229 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
230 | integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
231 | dependencies:
232 | ansi-styles "^3.2.1"
233 | escape-string-regexp "^1.0.5"
234 | supports-color "^5.3.0"
235 |
236 | color-convert@^1.9.0:
237 | version "1.9.3"
238 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
239 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
240 | dependencies:
241 | color-name "1.1.3"
242 |
243 | color-name@1.1.3:
244 | version "1.1.3"
245 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
246 | integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
247 |
248 | colorette@^1.2.2:
249 | version "1.2.2"
250 | resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
251 | integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
252 |
253 | convert-source-map@^1.7.0:
254 | version "1.8.0"
255 | resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
256 | integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==
257 | dependencies:
258 | safe-buffer "~5.1.1"
259 |
260 | debug@^4.1.0:
261 | version "4.3.2"
262 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
263 | integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
264 | dependencies:
265 | ms "2.1.2"
266 |
267 | electron-to-chromium@^1.3.723:
268 | version "1.3.792"
269 | resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.792.tgz#791b0d8fcf7411885d086193fb49aaef0c1594ca"
270 | integrity sha512-RM2O2xrNarM7Cs+XF/OE2qX/aBROyOZqqgP+8FXMXSuWuUqCfUUzg7NytQrzZU3aSqk1Qq6zqnVkJsbfMkIatg==
271 |
272 | escalade@^3.1.1:
273 | version "3.1.1"
274 | resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
275 | integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
276 |
277 | escape-string-regexp@^1.0.5:
278 | version "1.0.5"
279 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
280 | integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
281 |
282 | gensync@^1.0.0-beta.2:
283 | version "1.0.0-beta.2"
284 | resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
285 | integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
286 |
287 | globals@^11.1.0:
288 | version "11.12.0"
289 | resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
290 | integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
291 |
292 | has-flag@^3.0.0:
293 | version "3.0.0"
294 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
295 | integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
296 |
297 | js-tokens@^4.0.0:
298 | version "4.0.0"
299 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
300 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
301 |
302 | jsesc@^2.5.1:
303 | version "2.5.2"
304 | resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
305 | integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
306 |
307 | json5@^2.1.2:
308 | version "2.2.0"
309 | resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3"
310 | integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==
311 | dependencies:
312 | minimist "^1.2.5"
313 |
314 | minimist@^1.2.5:
315 | version "1.2.5"
316 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
317 | integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
318 |
319 | ms@2.1.2:
320 | version "2.1.2"
321 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
322 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
323 |
324 | node-releases@^1.1.71:
325 | version "1.1.73"
326 | resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.73.tgz#dd4e81ddd5277ff846b80b52bb40c49edf7a7b20"
327 | integrity sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==
328 |
329 | safe-buffer@~5.1.1:
330 | version "5.1.2"
331 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
332 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
333 |
334 | semver@^6.3.0:
335 | version "6.3.0"
336 | resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
337 | integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
338 |
339 | source-map@^0.5.0:
340 | version "0.5.7"
341 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
342 | integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
343 |
344 | supports-color@^5.3.0:
345 | version "5.5.0"
346 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
347 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
348 | dependencies:
349 | has-flag "^3.0.0"
350 |
351 | to-fast-properties@^2.0.0:
352 | version "2.0.0"
353 | resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
354 | integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=
355 |
--------------------------------------------------------------------------------