From 954fbc06e0c980dd14dd0e33ee03fb9312941748 Mon Sep 17 00:00:00 2001 From: Yanzhen Yu Date: Mon, 5 Jul 2021 16:57:45 +0800 Subject: [PATCH] impl basic state management with zustand --- packages/core/__tests__/application.spec.ts | 10 +-- packages/core/__tests__/component.spec.ts | 5 +- packages/core/__tests__/scope.spec.ts | 4 +- packages/core/__tests__/trait.spec.ts | 4 +- packages/core/src/application.ts | 2 +- packages/core/src/version.ts | 2 +- .../runtime/example/delete-button/index.html | 15 ++++- packages/runtime/package.json | 5 +- packages/runtime/src/App.tsx | 38 ++++++++--- .../runtime/src/components/_internal/Text.tsx | 18 ++++++ packages/runtime/src/components/core/Text.tsx | 37 ++++++----- .../runtime/src/components/plain/Button.tsx | 30 +++++++-- packages/runtime/src/registry.tsx | 12 +++- packages/runtime/src/store.ts | 63 +++++++++++++++++++ yarn.lock | 16 ++++- 15 files changed, 212 insertions(+), 49 deletions(-) create mode 100644 packages/runtime/src/components/_internal/Text.tsx create mode 100644 packages/runtime/src/store.ts diff --git a/packages/core/__tests__/application.spec.ts b/packages/core/__tests__/application.spec.ts index aed8ef58..d3025b63 100644 --- a/packages/core/__tests__/application.spec.ts +++ b/packages/core/__tests__/application.spec.ts @@ -14,14 +14,14 @@ describe("application", () => { components: [ { id: "input1", - type: "core/v1/test-component", + type: "core/v1/test_component", properties: { x: "foo", }, traits: [ { - type: "core/v1/test-trait", + type: "core/v1/test_trait", properties: { width: 2, }, @@ -47,7 +47,7 @@ describe("application", () => { Object { "id": "input1", "parsedType": Object { - "name": "test-component", + "name": "test_component", "version": "core/v1", }, "properties": Object { @@ -58,10 +58,10 @@ describe("application", () => { "properties": Object { "width": 2, }, - "type": "core/v1/test-trait", + "type": "core/v1/test_trait", }, ], - "type": "core/v1/test-component", + "type": "core/v1/test_component", }, ], }, diff --git a/packages/core/__tests__/component.spec.ts b/packages/core/__tests__/component.spec.ts index 1be128b9..eaa2d79f 100644 --- a/packages/core/__tests__/component.spec.ts +++ b/packages/core/__tests__/component.spec.ts @@ -6,8 +6,9 @@ describe("component", () => { createComponent({ version: "core/v1", metadata: { - name: "test-component", + name: "test_component", }, + spec: { properties: [ { @@ -44,7 +45,7 @@ describe("component", () => { Object { "kind": "Component", "metadata": Object { - "name": "test-component", + "name": "test_component", }, "parsedVersion": Object { "category": "core", diff --git a/packages/core/__tests__/scope.spec.ts b/packages/core/__tests__/scope.spec.ts index 8b65e3c5..f3030ad6 100644 --- a/packages/core/__tests__/scope.spec.ts +++ b/packages/core/__tests__/scope.spec.ts @@ -6,14 +6,14 @@ describe("scope", () => { createScope({ version: "core/v1", metadata: { - name: "test-scope", + name: "test_scope", }, }) ).toMatchInlineSnapshot(` Object { "kind": "Scope", "metadata": Object { - "name": "test-scope", + "name": "test_scope", }, "parsedVersion": Object { "category": "core", diff --git a/packages/core/__tests__/trait.spec.ts b/packages/core/__tests__/trait.spec.ts index 33293f17..c666e4cc 100644 --- a/packages/core/__tests__/trait.spec.ts +++ b/packages/core/__tests__/trait.spec.ts @@ -6,7 +6,7 @@ describe("trait", () => { createTrait({ version: "core/v1", metadata: { - name: "test-trait", + name: "test_trait", }, spec: { @@ -29,7 +29,7 @@ describe("trait", () => { Object { "kind": "Trait", "metadata": Object { - "name": "test-trait", + "name": "test_trait", }, "parsedVersion": Object { "category": "core", diff --git a/packages/core/src/application.ts b/packages/core/src/application.ts index c2c4ff00..bc977e7a 100644 --- a/packages/core/src/application.ts +++ b/packages/core/src/application.ts @@ -48,7 +48,7 @@ export type RuntimeApplication = Omit & { type A = RuntimeApplication["spec"]["components"]; -const TYPE_REG = /^([a-zA-Z-_\d]+\/[a-zA-Z-_\d]+)\/([a-zA-Z-_\d]+)$/; +const TYPE_REG = /^([a-zA-Z0-9_\d]+\/[a-zA-Z0-9_\d]+)\/([a-zA-Z0-9_\d]+)$/; function isValidType(v: string): boolean { return TYPE_REG.test(v); } diff --git a/packages/core/src/version.ts b/packages/core/src/version.ts index 25b576af..d64e95ab 100644 --- a/packages/core/src/version.ts +++ b/packages/core/src/version.ts @@ -1,4 +1,4 @@ -const VERSION_REG = /^([a-zA-Z-_\d]+)\/([a-zA-Z-_\d]+)$/; +const VERSION_REG = /^([a-zA-Z0-9_\d]+)\/([a-zA-Z0-9_\d]+)$/; export function isValidVersion(v: string): boolean { return VERSION_REG.test(v); diff --git a/packages/runtime/example/delete-button/index.html b/packages/runtime/example/delete-button/index.html index 96c0307c..6be1fdf3 100644 --- a/packages/runtime/example/delete-button/index.html +++ b/packages/runtime/example/delete-button/index.html @@ -18,11 +18,22 @@ spec: { components: [ { - id: "del-btn", + id: "del_btn", type: "plain/v1/button", properties: { text: { - raw: `*Markdown Button*`, + raw: `{{ del_btn.count < 3 ? '*Markdown Button*' : "**I'm** Done!" }}`, + format: "md", + }, + }, + traits: [], + }, + { + id: "debug_text", + type: "core/v1/text", + properties: { + value: { + raw: `{{ del_btn.value + '---' + del_btn.count }}`, format: "md", }, }, diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 8e915539..003419c4 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -6,11 +6,14 @@ }, "dependencies": { "@meta-ui/core": "^0.1.0", + "lodash": "^4.17.21", "react": "^17.0.0", "react-dom": "^17.0.0", - "react-markdown": "^6.0.2" + "react-markdown": "^6.0.2", + "zustand": "^3.5.5" }, "devDependencies": { + "@types/lodash": "^4.14.170", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", "@vitejs/plugin-react-refresh": "^1.3.1", diff --git a/packages/runtime/src/App.tsx b/packages/runtime/src/App.tsx index 2cb4c5c9..466a22bc 100644 --- a/packages/runtime/src/App.tsx +++ b/packages/runtime/src/App.tsx @@ -1,19 +1,43 @@ import React from "react"; -import { Application, createApplication } from "@meta-ui/core"; +import { + Application, + createApplication, + RuntimeApplication, +} from "@meta-ui/core"; import { registry } from "./registry"; +import { setStore, useStore } from "./store"; + +const ImplWrapper: React.FC<{ + component: RuntimeApplication["spec"]["components"][0]; +}> = ({ component: c }) => { + const Impl = registry.getComponent( + c.parsedType.version, + c.parsedType.name + ).impl; + + return ( + setStore({ [c.id]: partial })} + /> + ); +}; + +const DebugStore: React.FC = () => { + const store = useStore(); + + return
{JSON.stringify(store, null, 2)}
; +}; const App: React.FC<{ options: Application }> = ({ options }) => { const app = createApplication(options); - console.log(app); return (
+ {app.spec.components.map((c) => { - const Impl = registry.getComponent( - c.parsedType.version, - c.parsedType.name - ).impl; - return ; + return ; })}
); diff --git a/packages/runtime/src/components/_internal/Text.tsx b/packages/runtime/src/components/_internal/Text.tsx new file mode 100644 index 00000000..9d7cfee3 --- /dev/null +++ b/packages/runtime/src/components/_internal/Text.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import ReactMarkdown from "react-markdown"; + +export type TextProps = { + value: { + raw: string; + format: "plain" | "md"; + }; +}; + +const Text: React.FC = ({ value }) => { + if (value.format === "md") { + return {value.raw}; + } + return <>{value.raw}; +}; + +export default Text; diff --git a/packages/runtime/src/components/core/Text.tsx b/packages/runtime/src/components/core/Text.tsx index 57bb5cad..f01c0aea 100644 --- a/packages/runtime/src/components/core/Text.tsx +++ b/packages/runtime/src/components/core/Text.tsx @@ -1,21 +1,19 @@ -import React from "react"; +import React, { useEffect } from "react"; import { createComponent } from "@meta-ui/core"; -import ReactMarkdown from "react-markdown"; +import { Implementation } from "../../registry"; +import _Text, { TextProps } from "../_internal/Text"; +import { useExpression } from "../../store"; -export type TextProps = { - value?: { - raw: string; - format: "plain" | "md"; - }; -}; +const Text: Implementation = ({ value, mergeState }) => { + const raw = useExpression(value.raw); -const Text: React.FC = ({ - value = { raw: "**Hello World**", format: "md" }, -}) => { - if (value.format === "md") { - return {value.raw}; - } - return <>{value.raw}; + useEffect(() => { + mergeState({ value: raw }); + }, [raw]); + + // console.log("render text"); + + return <_Text value={{ ...value, raw }} />; }; export default { @@ -42,7 +40,14 @@ export default { }, ], acceptTraits: [], - state: {}, + state: { + type: "object", + properties: { + value: { + type: "string", + }, + }, + }, methods: [], }, }), diff --git a/packages/runtime/src/components/plain/Button.tsx b/packages/runtime/src/components/plain/Button.tsx index 097f8696..7a3562f1 100644 --- a/packages/runtime/src/components/plain/Button.tsx +++ b/packages/runtime/src/components/plain/Button.tsx @@ -1,11 +1,22 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { createComponent } from "@meta-ui/core"; -import Text, { TextProps } from "../core/Text"; +import Text, { TextProps } from "../_internal/Text"; +import { Implementation } from "../../registry"; +import { useExpression } from "../../store"; + +const Button: Implementation<{ text: TextProps["value"] }> = ({ + text, + mergeState, +}) => { + const [count, add] = useState(0); + const raw = useExpression(text.raw); + useEffect(() => { + mergeState({ value: raw, count }); + }, [raw, count]); -const Button: React.FC<{ text?: TextProps["value"] }> = ({ text }) => { return ( - ); }; @@ -34,7 +45,14 @@ export default { }, ], acceptTraits: [], - state: {}, + state: { + type: "object", + properties: { + value: { + type: "string", + }, + }, + }, methods: [], }, }), diff --git a/packages/runtime/src/registry.tsx b/packages/runtime/src/registry.tsx index d8ae86f5..17190ee6 100644 --- a/packages/runtime/src/registry.tsx +++ b/packages/runtime/src/registry.tsx @@ -1,11 +1,20 @@ import React from "react"; import { RuntimeComponent } from "@meta-ui/core"; +import { setStore } from "./store"; +// components import PlainButton from "./components/plain/Button"; +import CoreText from "./components/core/Text"; type ImplementedRuntimeComponent = RuntimeComponent & { - impl: React.FC; + impl: Implementation; }; +export type Implementation = React.FC< + T & { + mergeState: (partialState: Parameters[0]) => void; + } +>; + class Registry { components: Map> = new Map(); @@ -33,3 +42,4 @@ class Registry { export const registry = new Registry(); registry.registerComponent(PlainButton); +registry.registerComponent(CoreText); diff --git a/packages/runtime/src/store.ts b/packages/runtime/src/store.ts new file mode 100644 index 00000000..ac792a75 --- /dev/null +++ b/packages/runtime/src/store.ts @@ -0,0 +1,63 @@ +import { useMemo, useState } from "react"; +import create from "zustand"; +import _ from "lodash"; + +export const useStore = create>(() => ({})); + +export const setStore = useStore.setState; + +// TODO: use web worker +function evalInContext(expression: string, ctx: Record) { + try { + Object.keys(ctx).forEach((key) => { + // @ts-ignore + self[key] = ctx[key]; + }); + return eval(expression); + } catch (error) { + // console.error(error); + return undefined; + } finally { + Object.keys(ctx).forEach((key) => { + // @ts-ignore + delete self[key]; + }); + } +} +export function useExpression(raw: string) { + const { dynamic, expression } = useMemo(() => { + if (!raw) { + return { + dynamic: false, + expression: raw, + }; + } + const matchArr = raw.match(/{{(.+)}}/); + if (!matchArr) { + return { + dynamic: false, + expression: raw, + }; + } + return { + dynamic: true, + expression: matchArr[1], + }; + }, [raw]); + + const [state, setState] = useState(null); + + useStore.subscribe( + (value) => { + setState(value); + }, + (state) => { + if (!dynamic) { + return expression; + } + return evalInContext(expression, state); + } + ); + + return state; +} diff --git a/yarn.lock b/yarn.lock index ad3c24a8..6677ad40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1496,6 +1496,11 @@ resolved "http://192.168.26.29:7001/@types/json-schema/download/@types/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" integrity sha1-mKmTUWyFnrDVxMjwmDF6nqaNua0= +"@types/lodash@^4.14.170": + version "4.14.170" + resolved "http://192.168.26.29:7001/@types/lodash/download/@types/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6" + integrity sha1-DWdxHUv39MpRR+kJG4R0ebh5JdY= + "@types/mdast@^3.0.0": version "3.0.3" resolved "http://192.168.26.29:7001/@types/mdast/download/@types/mdast-3.0.3.tgz#2d7d671b1cd1ea3deb306ea75036c2a0407d2deb" @@ -4671,10 +4676,10 @@ lodash.truncate@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.truncate/download/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= -lodash@4.x, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.7.0: +lodash@4.x, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + resolved "http://192.168.26.29:7001/lodash/download/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha1-Z5WRxWTDv/quhFTPCz3zcMPWkRw= log-symbols@^4.1.0: version "4.1.0" @@ -7411,3 +7416,8 @@ yargs@^16.0.3, yargs@^16.2.0: string-width "^4.2.0" y18n "^5.0.5" yargs-parser "^20.2.2" + +zustand@^3.5.5: + version "3.5.5" + resolved "http://192.168.26.29:7001/zustand/download/zustand-3.5.5.tgz#628458ad70621ddc2a17dbee49be963e5c0dccb5" + integrity sha1-YoRYrXBiHdwqF9vuSb6WPlwNzLU=