Skip to content
This repository has been archived by the owner on Apr 25, 2023. It is now read-only.

Commit

Permalink
feat: plugin system beta (#87)
Browse files Browse the repository at this point in the history
  • Loading branch information
rot1024 committed Sep 26, 2021
1 parent ed29403 commit d76f1c0
Show file tree
Hide file tree
Showing 93 changed files with 2,029 additions and 1,416 deletions.
2 changes: 1 addition & 1 deletion bin/pluginDoc.ts
Expand Up @@ -3,7 +3,7 @@ import * as ts from "typescript";

import tsconfig from "../tsconfig.json";

const files = ["./src/plugin/api.ts"];
const files = ["./src/components/molecules/Visualizer/Plugin/types.ts"];
const program = ts.createProgram(files, tsconfig as any);
const tc = program.getTypeChecker();

Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -176,7 +176,7 @@
"mini-svg-data-uri": "^1.2.3",
"parse-domain": "^3.0.3",
"quickjs-emscripten": "^0.13.0",
"quickjs-emscripten-sync": "^1.1.0",
"quickjs-emscripten-sync": "^1.2.0",
"rc-slider": "9.7.1",
"react": "^17.0.2",
"react-accessible-accordion": "^3.3.4",
Expand Down
1 change: 1 addition & 0 deletions src/app.tsx
Expand Up @@ -24,6 +24,7 @@ import { Provider as ThemeProvider, styled } from "./theme";
const EarthEditor = React.lazy(() => import("@reearth/components/pages/EarthEditor"));
const Dashboard = React.lazy(() => import("@reearth/components/pages/Dashboard"));
const GraphQLPlayground = React.lazy(() => import("@reearth/components/pages/GraphQLPlayground"));

const enableWhyDidYouRender = false;

if (enableWhyDidYouRender && process.env.NODE_ENV === "development") {
Expand Down
93 changes: 34 additions & 59 deletions src/components/atoms/Plugin/hooks.ts
@@ -1,5 +1,5 @@
import { getQuickJS } from "quickjs-emscripten";
import Arena from "quickjs-emscripten-sync";
import { Arena } from "quickjs-emscripten-sync";
import { useCallback, useEffect, useRef, useState, useMemo } from "react";

import type { Ref as IFrameRef } from "./IFrame";
Expand All @@ -9,15 +9,16 @@ export type IFrameAPI = {
postMessage: (message: any) => void;
};

export type Options<T> = {
export type Options = {
src?: string;
sourceCode?: string;
skip?: boolean;
iframeCanBeVisible?: boolean;
exposed?: { [key: string]: any };
isMarshalable?: (obj: any) => boolean;
isMarshalable?: boolean | "json" | ((obj: any) => boolean | "json");
onError?: (err: any) => void;
staticExposed?: (api: IFrameAPI) => T;
onPreInit?: () => void;
onDispose?: () => void;
exposed?: ((api: IFrameAPI) => { [key: string]: any }) | { [key: string]: any };
};

// restrict any classes
Expand All @@ -34,19 +35,21 @@ const defaultOnError = (err: any) => {
console.error("plugin error", err?.message || err);
};

export default function useHook<T>({
export default function useHook({
src,
sourceCode,
skip,
iframeCanBeVisible,
exposed,
isMarshalable,
onPreInit,
onError = defaultOnError,
staticExposed,
}: Options<T> = {}) {
onDispose,
exposed,
}: Options = {}) {
const arena = useRef<Arena | undefined>();
const eventLoop = useRef<number>();
const [loaded, setLoaded] = useState(false);
const [code, setCode] = useState("");
const iFrameRef = useRef<IFrameRef>(null);
const [[iFrameHtml, iFrameOptions], setIFrameState] = useState<
[string, { visible?: boolean } | undefined]
Expand Down Expand Up @@ -93,30 +96,40 @@ export default function useHook<T>({
[iframeCanBeVisible],
);

const staticExpose = useCallback(() => {
if (!arena.current) return;
const exposed = staticExposed?.(iFrameApi);
if (exposed) {
arena.current.expose(exposed);
}
}, [iFrameApi, staticExposed]);
useEffect(() => {
(async () => {
const code = sourceCode ?? (src ? await (await fetch(src)).text() : "");
setCode(code);
})();
}, [sourceCode, src]);

// init and dispose of vm
useEffect(() => {
if (skip) return;
if (skip || !code) return;

onPreInit?.();

(async () => {
const vm = (await getQuickJS()).createVm();
arena.current = new Arena(vm, {
isMarshalable: target => defaultIsMarshalable(target) || !!isMarshalable?.(target),
isMarshalable: target =>
defaultIsMarshalable(target) ||
(typeof isMarshalable === "function" ? isMarshalable(target) : "json"),
});
staticExpose();

const e = typeof exposed === "function" ? exposed(iFrameApi) : exposed;
if (e) {
arena.current.expose(e);
}

evalCode(code);
setLoaded(true);
})();

return () => {
onDispose?.();
setIFrameState(["", undefined]);
if (typeof eventLoop.current === "number") {
// eslint-disable-next-line react-hooks/exhaustive-deps
window.clearTimeout(eventLoop.current);
}
if (arena.current) {
Expand All @@ -131,45 +144,7 @@ export default function useHook<T>({
}
}
};
}, [isMarshalable, onError, skip, src, sourceCode, staticExpose]);

const exposer = useMemo(() => {
if (!arena.current || !loaded) return;
return arena.current.evalCode<(keys: string[], value: any) => void>(`(keys, value) => {
if (!keys.length) return;
let o = globalThis;
for (const k of keys.slice(0, -1)) {
if (typeof o !== "object") break;
if (typeof o?.[k] !== "object") {
o[k] = {};
}
o = o[k];
}
if (typeof o !== "object") return;
o[keys[keys.length - 1]] = value;
}`);
}, [loaded]);

useEffect(() => {
if (!arena.current || !exposer || !exposed) return;
for (const [k, v] of Object.entries(exposed)) {
const keys = k.split(".");
exposer(keys, v);
}
}, [exposed, exposer]);

useEffect(() => {
if (!arena.current || !loaded || (!src && !sourceCode)) return;

setIFrameState(s => (!s[0] && !s[1] ? s : ["", undefined]));
// load JS
(async () => {
if (!arena.current) return;
const code = sourceCode ?? (src ? await (await fetch(src)).text() : "");
evalCode(code);
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [src, sourceCode, loaded]); // ignore evalCode
}, [code, evalCode, iFrameApi, isMarshalable, onDispose, onPreInit, skip, exposed]);

return {
iFrameHtml,
Expand Down
20 changes: 11 additions & 9 deletions src/components/atoms/Plugin/index.stories.tsx
Expand Up @@ -22,10 +22,10 @@ Default.args = {
height: "300px",
backgroundColor: "#fff",
},
exposed: {
"console.log": action("console.log"),
},
staticExposed: ({ render, postMessage }) => ({
exposed: ({ render, postMessage }) => ({
console: {
log: action("console.log"),
},
reearth: {
on(type: string, value: (message: any) => void | undefined) {
if (type === "message") {
Expand Down Expand Up @@ -54,10 +54,10 @@ HiddenIFrame.args = {
height: "300px",
backgroundColor: "#fff",
},
exposed: {
"console.log": action("console.log"),
},
staticExposed: ({ render, postMessage }) => ({
exposed: ({ render, postMessage }) => ({
console: {
log: action("console.log"),
},
reearth: {
on(type: string, value: (message: any) => void | undefined) {
if (type === "message") {
Expand All @@ -81,6 +81,8 @@ export const SourceCode: Story<Props> = args => <Component {...args} />;
SourceCode.args = {
sourceCode: `console.log("Hello")`,
exposed: {
"console.log": action("console.log"),
console: {
log: action("console.log"),
},
},
};
17 changes: 10 additions & 7 deletions src/components/atoms/Plugin/index.tsx
Expand Up @@ -12,13 +12,14 @@ export type Props = {
style?: CSSProperties;
src?: string;
sourceCode?: string;
exposed?: { [key: string]: any };
renderPlaceholder?: ReactNode;
iFrameProps?: IframeHTMLAttributes<HTMLIFrameElement>;
isMarshalable?: (target: any) => boolean;
staticExposed?: (api: IFrameAPI) => any;
isMarshalable?: boolean | "json" | ((target: any) => boolean | "json");
exposed?: ((api: IFrameAPI) => { [key: string]: any }) | { [key: string]: any };
onMessage?: (message: any) => void;
onPreInit?: () => void;
onError?: (err: any) => void;
onDispose?: () => void;
};

const Plugin: React.FC<Props> = ({
Expand All @@ -28,23 +29,25 @@ const Plugin: React.FC<Props> = ({
style,
src,
sourceCode,
exposed,
renderPlaceholder,
iFrameProps,
isMarshalable,
staticExposed,
exposed,
onMessage,
onPreInit,
onError,
onDispose,
}) => {
const { iFrameRef, iFrameHtml, iFrameVisible } = useHook({
iframeCanBeVisible: canBeVisible,
skip,
src,
sourceCode,
exposed,
isMarshalable,
staticExposed,
exposed,
onPreInit,
onError,
onDispose,
});

return iFrameHtml ? (
Expand Down
4 changes: 2 additions & 2 deletions src/components/molecules/Common/Header/index.tsx
Expand Up @@ -6,9 +6,9 @@ import WorkspaceCreationModal from "@reearth/components/molecules/Common/Workspa
import { styled, metrics, css } from "@reearth/theme";

import Profile from "./profile";
import { User, Team, Project } from "./types";
import type { User, Team, Project } from "./types";

export * from "./types";
export type { User, Team, Project } from "./types";

export interface Props {
className?: string;
Expand Down
2 changes: 1 addition & 1 deletion src/components/molecules/EarthEditor/Header/index.tsx
Expand Up @@ -17,7 +17,7 @@ import ProjectMenu from "@reearth/components/molecules/Common/ProjectMenu";
import { styled } from "@reearth/theme";

// Proxy dependent types
export { User, Team } from "@reearth/components/molecules/Common/Header";
export type { User, Team } from "@reearth/components/molecules/Common/Header";

export type publishingType = "publishing" | "updating" | "unpublishing";
export type Project = {
Expand Down
Expand Up @@ -12,9 +12,9 @@ import { metricsSizes } from "@reearth/theme/metrics";
import Header from "./Header";
import useHooks from "./hooks";
import List from "./List";
import { DatasetSchema, Type } from "./types";
import type { DatasetSchema, Type } from "./types";

export { DatasetSchema, Dataset, DatasetField, Type } from "./types";
export type { DatasetSchema, Dataset, DatasetField, Type } from "./types";

export type Props = {
className?: string;
Expand Down
Expand Up @@ -8,7 +8,7 @@ import fonts from "@reearth/theme/fonts";

import PropertyLinkPanel, { Props as PropertyLinkPanelProps } from "./PropertyLinkPanel";

export { Dataset, DatasetField, DatasetSchema, Type } from "./PropertyLinkPanel";
export type { Dataset, DatasetField, DatasetSchema, Type } from "./PropertyLinkPanel";

export type Props = {
className?: string;
Expand Down
Expand Up @@ -28,7 +28,7 @@ import { FieldProps } from "./types";
import TypographyField from "./TypographyField";
import URLField, { Asset as AssetType } from "./URLField";

export { Dataset, DatasetSchema, DatasetField, Type as DatasetType } from "./PropertyTitle";
export type { Dataset, DatasetSchema, DatasetField, Type as DatasetType } from "./PropertyTitle";

export type ValueType = ValueTypeType;
export type ValueTypes = ValueTypesType;
Expand Down
5 changes: 2 additions & 3 deletions src/components/molecules/Visualizer/Block/index.stories.tsx
@@ -1,8 +1,7 @@
import { Meta, Story } from "@storybook/react";
import React from "react";

import { Provider } from "../context";
import { context } from "../storybook";
import { Provider } from "../storybook";

import Component, { Props } from ".";

Expand All @@ -26,7 +25,7 @@ Default.args = {
};

export const Plugin: Story<Props> = args => (
<Provider value={context}>
<Provider>
<div style={{ background: "#fff" }}>
<Component {...args} />
</div>
Expand Down
24 changes: 12 additions & 12 deletions src/components/molecules/Visualizer/Block/index.tsx
Expand Up @@ -3,20 +3,20 @@ import React, { ComponentType } from "react";
import { styled } from "@reearth/theme";
import { ValueType, ValueTypes } from "@reearth/util/value";

import Plugin, { Block, Primitive } from "../Plugin";
import Plugin from "../Plugin";
import type { Block, Layer, InfoboxProperty } from "../Plugin";

import builtin from "./builtin";

export type { Primitive, Block } from "../Plugin";
export type { Block, Layer } from "../Plugin";

export type Props<PP = any, IP = any, SP = any> = {
export type Props<BP = any, PP = any> = {
isEditable?: boolean;
isBuilt?: boolean;
isSelected?: boolean;
primitive?: Primitive;
block?: Block;
sceneProperty?: SP;
infoboxProperty?: IP;
layer?: Layer;
block?: Block<BP>;
infoboxProperty?: InfoboxProperty;
pluginProperty?: PP;
pluginBaseUrl?: string;
onClick?: () => void;
Expand All @@ -28,12 +28,12 @@ export type Props<PP = any, IP = any, SP = any> = {
) => void;
};

export type Component<PP = any, IP = any, SP = any> = ComponentType<Props<PP, IP, SP>>;
export type Component<BP = any, PP = any> = ComponentType<Props<BP, PP>>;

export default function BlockComponent<PP = any, IP = any, SP = any>({
export default function BlockComponent<P = any, PP = any>({
pluginBaseUrl,
...props
}: Props<PP, IP, SP>): JSX.Element | null {
}: Props<P, PP>): JSX.Element | null {
const Builtin =
props.block?.pluginId && props.block.extensionId
? builtin[`${props.block.pluginId}/${props.block.extensionId}`]
Expand All @@ -51,8 +51,8 @@ export default function BlockComponent<PP = any, IP = any, SP = any>({
pluginBaseUrl={pluginBaseUrl}
visible
property={props.pluginProperty}
sceneProperty={props.sceneProperty}
primitive={props.primitive}
pluginProperty={props.pluginProperty}
layer={props.layer}
block={props.block}
/>
</Wrapper>
Expand Down

0 comments on commit d76f1c0

Please sign in to comment.