-
Notifications
You must be signed in to change notification settings - Fork 306
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: third party plugin (stage 1 poc) (#2702)
* feat: add basic parse of external plugins * feat: add external plugin previewer * feat: able to render plugin into the plugin canvas * fix: tsconfig error * feat: move to new plugin infra * feat: internally convert dom nodes to ef template * feat: add a permission guard * feat: support permission grant * feat: add sdk entry * refactor: change inject content scripts * feat: add a mech for enable sdk * feat: add basic sdk * feat: add sdk * fix: prettier * chore: change some custom dom * chore: change some custom dom * feat: a minimal permission management * feat: poor man's permission system * fix: lockfile * chore: remove extra entry for sdk * fix: enable sdk * feat: add sns context * feat: add entry for third party plugin * feat: add setMetadata * feat: metadata badge * feat: metadata badge of 3rd plugin * feat: allow relative path * fix: url bug
- Loading branch information
1 parent
1066ab4
commit b7536f5
Showing
56 changed files
with
1,219 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"name": "@dimensiondev/external-plugin-previewer", | ||
"version": "0.0.0", | ||
"private": true, | ||
"scripts": { | ||
"start": "dev -- snowpack dev" | ||
}, | ||
"dependencies": { | ||
"@dimensiondev/maskbook-shared": "workspace:*", | ||
"ef.js": "^0.13.6" | ||
}, | ||
"devDependencies": { | ||
"snowpack": "^3.0.11" | ||
}, | ||
"main": "./dist/index.js", | ||
"types": "./dist" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
<title>External plugin debug playground</title> | ||
</head> | ||
<body> | ||
<div id="root"></div> | ||
<script type="module" src="/dist/playground.js"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
// Snowpack Configuration File | ||
// See all supported options: https://www.snowpack.dev/reference/configuration | ||
|
||
/** @type {import("snowpack").SnowpackUserConfig } */ | ||
module.exports = { | ||
mount: { | ||
public: { url: '/' }, | ||
src: { url: '/dist' }, | ||
}, | ||
plugins: [], | ||
packageOptions: {}, | ||
devOptions: { port: 28194 }, | ||
buildOptions: {}, | ||
} |
49 changes: 49 additions & 0 deletions
49
packages/external-plugin-previewer/src/Components/MaskCard.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { Card, CardContent, Typography, CardActions, Button } from '@material-ui/core' | ||
import { hostConfig } from '../host' | ||
import type { Component } from './index' | ||
import { useRef } from 'react' | ||
export const MaskCard: Component<MaskCardProps> = (props) => { | ||
const ref = useRef<HTMLDivElement>(null) | ||
return ( | ||
<Card ref={ref}> | ||
<CardContent> | ||
<Typography color="textSecondary" gutterBottom> | ||
{String(props.caption)} | ||
</Typography> | ||
<Typography variant="h5" component="div"> | ||
<slot name="title" /> | ||
</Typography> | ||
<Typography variant="body2" component="p"> | ||
<slot></slot> | ||
</Typography> | ||
</CardContent> | ||
<CardActions> | ||
<Button | ||
onClick={() => { | ||
const base = getContext(ref.current)?.trim() | ||
const url = base ? new URL(props.href, base) : new URL(props.href) | ||
hostConfig.permissionAwareOpen(url.toString()) | ||
}} | ||
size="small"> | ||
{String(props.button)} | ||
</Button> | ||
</CardActions> | ||
</Card> | ||
) | ||
} | ||
MaskCard.displayName = 'mask-card' | ||
export interface MaskCardProps { | ||
caption: string | ||
title: string | ||
button: string | ||
href: string | ||
} | ||
function getContext(node: Node | ShadowRoot | null): string | null { | ||
if (!node) return null | ||
if (node instanceof Element && node.hasAttribute('data-plugin')) { | ||
return node.getAttribute('data-plugin') | ||
} | ||
if (node instanceof ShadowRoot) return getContext(node.host) | ||
if (node.parentNode) return getContext(node.parentNode) | ||
return null | ||
} |
10 changes: 10 additions & 0 deletions
10
packages/external-plugin-previewer/src/Components/Translate.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import type { Component } from '.' | ||
|
||
export const Translate: Component<{}> = () => { | ||
return ( | ||
<span> | ||
i18n: <slot></slot> | ||
</span> | ||
) | ||
} | ||
Translate.displayName = 'i18n-translate' |
25 changes: 25 additions & 0 deletions
25
packages/external-plugin-previewer/src/Components/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { createElement } from 'react' | ||
|
||
export { MaskCard } from './MaskCard' | ||
export { Translate } from './Translate' | ||
|
||
export interface Component<P> { | ||
(props: P, dispatchEvent: (event: Event) => void): React.ReactChild | ||
displayName: string | ||
} | ||
|
||
export const span = createNativeTagDelegate('span') | ||
export const div = createNativeTagDelegate('div') | ||
export const br = createNativeTagDelegate('br', { children: false }) | ||
function createNativeTagDelegate<T extends keyof HTMLElementTagNameMap>( | ||
tag: T, | ||
accpetProps?: { [key in keyof HTMLElementTagNameMap[T]]?: boolean }, | ||
) { | ||
const C: Component<{}> = () => { | ||
// TODO: implement acceptProps | ||
if (accpetProps?.children === false) return createElement(tag) | ||
return createElement(tag, {}, <slot />) | ||
} | ||
C.displayName = tag | ||
return C | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { setDOMImpl } from 'ef.js' | ||
import type {} from 'react/experimental' | ||
import type {} from 'react-dom/experimental' | ||
import { createReactRootShadowedPartial, ReactRootShadowed } from '@dimensiondev/maskbook-shared' | ||
import * as Components from './Components' | ||
|
||
const createReactRootShadowed = createReactRootShadowedPartial({ | ||
preventEventPropagationList: [], | ||
}) | ||
setDOMImpl({ | ||
Node, | ||
document: new Proxy(document, { | ||
get(doc, key) { | ||
if (key === 'createElement') return createElement | ||
const val = (doc as any)[key] | ||
if (typeof val === 'function') return val.bind(doc) | ||
return val | ||
}, | ||
}), | ||
}) | ||
|
||
function createElement(element: string, options: ElementCreationOptions) { | ||
element = options.is || element | ||
const _ = shouldRender(element) | ||
const isValid = _ !== unknown | ||
const [nativeTag, Component] = _ | ||
const DOM = document.createElement(nativeTag) | ||
DOM.setAttribute('data-kind', element) | ||
|
||
const shadow = DOM.attachShadow({ mode: 'open' }) | ||
|
||
const props: any = { __proto__: null } | ||
isValid && render(Component, props, shadow) | ||
|
||
// No attributes allowed | ||
DOM.setAttribute = () => {} | ||
|
||
// No need to hook event listeners | ||
|
||
// Hook property access | ||
const proto = Object.getPrototypeOf(DOM) | ||
Object.setPrototypeOf( | ||
DOM, | ||
new Proxy(proto, { | ||
set(target, prop, value, receiver) { | ||
// Forward them instead. | ||
props[prop] = value | ||
isValid && render(Component, props, shadow) | ||
return true | ||
}, | ||
}), | ||
) | ||
return DOM | ||
} | ||
|
||
function render(f: Components.Component<any>, props: any, shadow: ShadowRoot) { | ||
const root: ReactRootShadowed = | ||
(shadow as any).__root || ((shadow as any).__root = createReactRootShadowed(shadow, { tag: 'span' })) | ||
root.render(<HooksContainer f={() => f(props, (event) => void shadow.host.dispatchEvent(event))} />) | ||
} | ||
// Need use a JSX component to hold hooks | ||
function HooksContainer(props: { f: () => React.ReactNode }) { | ||
return <>{props.f()}</> | ||
} | ||
|
||
const unknown = ['span', (() => null) as any as Components.Component<any>] as const | ||
|
||
function shouldRender(element: string): readonly [string, Components.Component<any>] { | ||
for (const F of Object.values(Components)) { | ||
if (F.displayName === element) return ['span', F] | ||
} | ||
return unknown | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
declare module 'ef.js' { | ||
export interface DOMImpl { | ||
Node: typeof Node | ||
document: typeof document | ||
} | ||
export function setDOMImpl(impl: DOMImpl): void | ||
export function create(template: string | TemplateStringsArray): typeof Component | ||
|
||
// Not exported | ||
class Component<T extends object = Record<string, any>> { | ||
constructor(options?: ComponentConstructorOptions<T>) | ||
$mount(opt: MountOptions): void | ||
$destroy(): void | ||
$methods: Record<string, Function> | ||
$data: T | ||
$subscribe(key: keyof T, callback: Function): void | ||
$unsubscribe(key: keyof T, callback: Function): void | ||
} | ||
export interface ComponentConstructorOptions<T> { | ||
$data: T | ||
} | ||
export interface MountOptions { | ||
target: Node | ||
} | ||
export function t(template: TemplateStringsArray): typeof Component | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
/** @internal */ | ||
export const hostConfig: HostConfig = { | ||
permissionAwareOpen(url: string) { | ||
return url | ||
}, | ||
} | ||
export interface HostConfig { | ||
permissionAwareOpen(url: string): void | ||
} | ||
export function setHostConfig(host: HostConfig) { | ||
hostConfig.permissionAwareOpen = host.permissionAwareOpen | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
export { setHostConfig } from './host' | ||
export type { HostConfig } from './host' | ||
/// <reference path="./global.d.ts" /> | ||
import { useEffect, useState } from 'react' | ||
import { create } from 'ef.js' | ||
import './DOMImpl' | ||
export function MaskExternalPluginPreviewRenderer({ pluginBase, payload, script, template, onError }: RenderData) { | ||
const [dom, setDOM] = useState<HTMLDivElement | null>(null) | ||
useEffect(() => { | ||
if (!dom) return | ||
dom.setAttribute('data-plugin', pluginBase) | ||
// This is safe. ef template does not allow any form of dynamic code execute in the template. | ||
try { | ||
const RemoteContent = create(template) | ||
const instance = new RemoteContent({ $data: { payload } }) | ||
instance.$mount({ target: dom }) | ||
return () => instance.$destroy() | ||
} catch (e) { | ||
onError?.(e) | ||
} | ||
return | ||
}, [dom, onError, payload, template, pluginBase]) | ||
return <div ref={(ref) => setDOM(ref)} /> | ||
} | ||
export interface RenderData { | ||
pluginBase: string | ||
template: string | ||
/** Currently not supported */ | ||
script: string | ||
payload: unknown | ||
onError?(e: Error): void | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import React from 'react' | ||
import { t } from 'ef.js' | ||
import { setupPortalShadowRoot } from '@dimensiondev/maskbook-shared' | ||
setupPortalShadowRoot({ mode: 'open' }, []) | ||
|
||
Object.assign(globalThis, { React }) | ||
|
||
const HelloWorld = t` | ||
>mask-card | ||
%caption = Caption! | ||
%title = This is preview of id {{payload.id}} | ||
%button = Details | ||
>mask-card | ||
%caption = Caption! | ||
%title = This is preview of id {{payload.id}} | ||
%button = Details | ||
` | ||
|
||
const ins = new HelloWorld() | ||
console.log('ins = ', ((globalThis as any).ins = ins)) | ||
ins.$mount({ target: document.body }) | ||
// will be set by Mask | ||
ins.$data.payload = { | ||
id: 1, | ||
} | ||
|
||
export {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
{ | ||
"extends": "../../tsconfig.json", | ||
"compilerOptions": { | ||
"rootDir": "./src/", | ||
"outDir": "./dist/", | ||
"stripInternal": true | ||
}, | ||
"include": ["./src/**/*"], | ||
"ts-node": { | ||
"transpileOnly": true, | ||
"compilerOptions": { "module": "CommonJS" } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,12 @@ | ||
import './extension/content-script/hmr' | ||
import Services from './extension/service' | ||
import { status } from './setup.ui' | ||
|
||
status.then((loaded) => { | ||
loaded && import('./extension/content-script/tasks') | ||
}) | ||
|
||
// The scope should be the ./ of the web page | ||
Services.ThirdPartyPlugin.isSDKEnabled(new URL('./', location.href).href).then((result) => { | ||
result && import('./extension/external-sdk') | ||
}) |
Oops, something went wrong.