Skip to content

Commit

Permalink
feat(@lexical/devtools): Added basic extension scaffolding (#5747)
Browse files Browse the repository at this point in the history
  • Loading branch information
StyleT authored Apr 2, 2024
1 parent df1aa31 commit e9f43a9
Show file tree
Hide file tree
Showing 44 changed files with 10,454 additions and 519 deletions.
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ packages/**/dist/**
packages/**/build/**
packages/**/npm/**
packages/**/config/**
packages/**/.wxt/**
packages/playwright
packages/playwright-core
packages/**/vite.config.js
Expand Down
9,915 changes: 9,400 additions & 515 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"extract-codes": "node scripts/build.js --codes",
"flow": "node ./scripts/check-flow-types.js",
"tsc": "tsc",
"tsc-extension": "npm run compile -w @lexical/devtools",
"tsc-watch": "tsc -w",
"collab": "cross-env HOST=localhost PORT=1234 npx y-websocket-server",
"validation": "npx ts-node --cwdMode packages/lexical-playground/src/server/validation.ts",
Expand Down Expand Up @@ -84,7 +85,7 @@
"debug-test-unit": "node --inspect-brk node_modules/.bin/jest --runInBand --collectCoverage=false",
"lint": "eslint ./",
"prettier": "prettier --list-different .",
"ci-check": "npm-run-all --parallel tsc flow prettier lint",
"ci-check": "npm-run-all --parallel tsc tsc-extension flow prettier lint",
"prettier:fix": "prettier --write .",
"prepare-ci": "npm run build-dev --prefix packages/lexical-playground",
"prepare-ci-prod": "npm run build-playground-prod",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!.gitignore
26 changes: 26 additions & 0 deletions packages/lexical-devtools/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
.output
stats.html
stats-*.json
.wxt
web-ext.config.ts

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
27 changes: 27 additions & 0 deletions packages/lexical-devtools/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Lexical DevTools browser extension

This is the source code for the Lexical DevTools browser extension.

## Local development

Lexical DevTools extension uses [WXT](https://wxt.dev/) framework to simplify development. Please refer to [WXT Development Guide](https://wxt.dev/guide/development.html) for comprehensive documentation.

**TLDR:**
```bash
$ npm run dev
# In browser: Alt+R to force reload extension
```

**Useful Hints:**
- Extension activity log: [chrome://extensions/?activity=eddfjidloofnnmloonifcjkpmfmlblab](chrome://extensions/?activity=eddfjidloofnnmloonifcjkpmfmlblab)
- Status of ServiceWorkers: [chrome://serviceworker-internals/?devtools](chrome://serviceworker-internals/?devtools)
- WXT Framework debugging: `DEBUG_WXT=1 npm run dev`

## Design

This extension follows typical [Browser DevTools architecture](https://developer.chrome.com/docs/extensions/how-to/devtools/extend-devtools) that includes sereral independent contexts that communicate via events or extension APIs.

<figure align="center">
<img src="./docs/architecture-diagram.png" alt="DevTools extension architecture" width="526">
<figcaption>DevTools extension architecture.</figcaption>
</figure>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 33 additions & 0 deletions packages/lexical-devtools/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "@lexical/devtools",
"description": "Lexical DevTools browser extension",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "wxt",
"dev:firefox": "wxt -b firefox",
"build": "wxt build",
"build:firefox": "wxt build -b firefox",
"zip": "wxt zip",
"zip:firefox": "wxt zip -b firefox",
"compile": "tsc --noEmit",
"postinstall": "wxt prepare"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@eduardoac-skimlinks/webext-redux": "3.0.1-release-candidate",
"zustand": "^4.5.1",
"webext-bridge": "~6.0.1"
},
"devDependencies": {
"@types/react": "^18.2.46",
"@types/react-dom": "^18.2.18",
"@vitejs/plugin-react": "^4.2.1",
"typescript": "^5.3.3",
"lexical": "0.14.2",
"wxt": "^0.17.0",
"vite": "^5.2.2"
}
}
38 changes: 38 additions & 0 deletions packages/lexical-devtools/src/components/EditorsRefreshCTA.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import * as React from 'react';
import {useState} from 'react';

interface Props {
tabID: number;
setErrorMessage: (value: string) => void;
sendMessage: (message: string, t: null, target: string) => Promise<unknown>;
}

function EditorsRefreshCTA({tabID, setErrorMessage, sendMessage}: Props) {
const [isRefreshing, setIsRefreshing] = useState(false);

const handleRefreshClick = () => {
setIsRefreshing(true);
sendMessage('refreshLexicalEditorsForTabID', null, `window@${tabID}`)
.catch((err) => {
setErrorMessage(err.message);
console.error(err);
})
.finally(() => setIsRefreshing(false));
};

return (
<button onClick={handleRefreshClick} disabled={isRefreshing}>
{isRefreshing ? 'Refreshing...' : 'Refresh'}
</button>
);
}

export default EditorsRefreshCTA;
32 changes: 32 additions & 0 deletions packages/lexical-devtools/src/entrypoints/background.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {onMessage} from 'webext-bridge/background';

import store from '../store';
import storeBackgroundWrapper from '../store-sync/background';

export default defineBackground(() => {
// Way for content script & injected scripts to get their tab ID
onMessage('getTabID', async (message) => {
let tabID: number | undefined = message.sender.tabId;
if (message.sender.context === 'popup') {
tabID = (await browser.tabs.query({active: true, currentWindow: true}))[0]
.id;
}
if (tabID === undefined) {
throw new Error(
`Could not get tab ID for message: ${message.toString()}`,
);
}
return tabID;
});

// Store initialization so other extension surfaces can use it
// as all changes go through background SW
storeBackgroundWrapper(store);
});
29 changes: 29 additions & 0 deletions packages/lexical-devtools/src/entrypoints/content/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {allowWindowMessaging, sendMessage} from 'webext-bridge/content-script';

import useExtensionStore from '../../store';
import storeReadyPromise from '../../store-sync/content-script';
import injectScript from './injectScript';

export default defineContentScript({
main(ctx) {
allowWindowMessaging('lexical-extension');

sendMessage('getTabID', null, 'background')
.then((tabID) => {
return storeReadyPromise(useExtensionStore).then(() => {
injectScript('/injected.js');
});
})
.catch(console.error);
},
matches: ['<all_urls>'],
registration: 'manifest',
runAt: 'document_end',
});
17 changes: 17 additions & 0 deletions packages/lexical-devtools/src/entrypoints/content/injectScript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import {PublicPath} from 'wxt/browser';

export default function injectScript(src: PublicPath) {
const s = document.createElement('script');
s.src = browser.runtime.getURL(src);
s.type = 'module'; // ESM module support
s.onload = () => s.remove();
(document.head || document.documentElement).append(s);
}
73 changes: 73 additions & 0 deletions packages/lexical-devtools/src/entrypoints/devtools-panel/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import * as React from 'react';
import {useState} from 'react';
import {sendMessage} from 'webext-bridge/devtools';

import lexicalLogo from '@/public/lexical.svg';

import EditorsRefreshCTA from '../../components/EditorsRefreshCTA';
import useStore from '../../store';

interface Props {
tabID: number;
}

function App({tabID}: Props) {
const [errorMessage, setErrorMessage] = useState('');

const {lexicalState} = useStore();
const states = lexicalState[tabID] ?? {};
const lexicalCount = Object.keys(states ?? {}).length;

return (
<>
<div>
<a href="https://lexical.dev" target="_blank">
<img src={lexicalLogo} className="logo" alt="Lexical logo" />
</a>
</div>
{errorMessage !== '' ? (
<div className="card error">{errorMessage}</div>
) : null}
<div className="card">
{states === undefined ? (
<span>Loading...</span>
) : (
<span>
Found <b>{lexicalCount}</b> editor{lexicalCount > 1 ? 's' : ''} on
the page
</span>
)}
<p>
<EditorsRefreshCTA
tabID={tabID}
setErrorMessage={setErrorMessage}
sendMessage={sendMessage}
/>
</p>
</div>
{Object.entries(states).map(([key, state]) => (
<p key={key}>
<b>ID: {key}</b>
<br />
<textarea
readOnly={true}
value={JSON.stringify(state)}
rows={5}
cols={150}
/>
<hr />
</p>
))}
</>
);
}

export default App;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
24 changes: 24 additions & 0 deletions packages/lexical-devtools/src/entrypoints/devtools-panel/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import React from 'react';
import ReactDOM from 'react-dom/client';

import store from '../../store.ts';
import storeReadyPromise from '../../store-sync/content-script';
import App from './App.tsx';

const tabID = browser.devtools.inspectedWindow.tabId;

storeReadyPromise(store).then(() =>
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App tabID={tabID} />
</React.StrictMode>,
),
);
8 changes: 8 additions & 0 deletions packages/lexical-devtools/src/entrypoints/devtools/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script type="module" src="./main.ts"></script>
</head>
</html>
14 changes: 14 additions & 0 deletions packages/lexical-devtools/src/entrypoints/devtools/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

// Create the panel which appears within the browser devtools
browser.devtools.panels.create(
'Lexical',
'icon/128.png',
'devtools-panel.html',
);
22 changes: 22 additions & 0 deletions packages/lexical-devtools/src/entrypoints/injected/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import {sendMessage, setNamespace} from 'webext-bridge/window';

import extensionStore from '../../store';
import storeReadyPromise from '../../store-sync/window';
import main from './main';

export default defineUnlistedScript({
main() {
setNamespace('lexical-extension');
sendMessage('getTabID', null, 'background').then((tabID) =>
storeReadyPromise(extensionStore).then(() => main(tabID, extensionStore)),
);
},
});
Loading

0 comments on commit e9f43a9

Please sign in to comment.