Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extension integration #240

Merged
merged 14 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/preview-close.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ on:
- 'core/test/**'
- 'loader-tests/**'
- '.devcontainer/**'
- 'extensions/**'
jobs:
prepare:
name: Prepare
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/preview-prepare.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ on:
- 'core/test/**'
- 'loader-tests/**'
- '.devcontainer/**'
- 'extensions/**'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We try to avoid calling CI without a reason.

For instance, we do not publish staging on changes irrelevant to web client.

permissions:
contents: read
jobs:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ Slow Reader is a local-first app. Clients do most of the work, and the server ju
- See **[`web/README.md`](./web/README.md)** for web client architecture.
- [`server/`](./server/): a small server that syncs data between users’ devices.
- [`proxy/`](./proxy/): HTTP proxy server to bypass censorship or to try web clients before they install the upcoming extensions (to bypass the CORS limit of the web apps).
- [`extension/`](./extension/): browser’s extension to avoid CORS limits in web client.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-links between docs

- [`api/`](./api/): types and constants shared between clients and server.
- [`docs/`](./docs/): guides for developers.
- [`scripts/`](./scripts/): scripts to test project and configure Google Cloud. Check the script’s descriptions for further details.
Expand Down
42 changes: 23 additions & 19 deletions extension/README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
# Slowreader Extension

Browser’s extensions to allow web client bypass CORS limit.

_See the [full architecture guide](../README.md) first._

## Scripts

- `pnpm install` to install dependencies
- `pnpm dev` to run the plugin in development mode
- `pnpm build` to build the plugin for production
- `cd extension && pnpm install` to install dependencies.
- `cd extension && `pnpm start` to run the plugin in development mode.
- `cd extension && pnpm build` to build the plugin for production.

## Quickstart
## Quick Start

- Run `pnpm install` to install dependencies
- Run `pnpm dev` to build the extension and watch the changes
- Open `chrome://extensions/` -> `Load unpacked` and choose `dist` folder from this repo. The extension should appear in the list of your extensions
- In the `.env` file of the main app, place the next line (`EXTENSION_ID` can be found in the `ID` line inside the uploaded extension block):
1. Run `cd extension && pnpm start` to build the extension and watch the changes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For steps it is better to use <ol>

2. Open `chrome://extensions/` → `Load unpacked` and choose `dist` folder from this repo. The extension should appear in the list of your extensions.
3. In the `.env` file of the main app, place the next line (`EXTENSION_ID` can be found in the `ID` line inside the uploaded extension block):

`VITE_EXTENSION_ID=<EXTENSION_ID>`
```
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do nested code blocks in lists in Markdown

VITE_EXTENSION_ID=EXTENSION_ID
```

- Run the main app
4. Run the web client.

During the development process, you can re-build the extension by clicking on the update button at the right bottom of the extension’s block.

Expand All @@ -26,7 +31,7 @@ You can see the console for errors and logs by clicking on the link at the line
Connect the extension on application start:

```ts
const port = chrome.runtime.connect(import.meta.env.VITE_EXTENSION_ID)
let port = chrome.runtime.connect(import.meta.env.VITE_EXTENSION_ID)
```

Send messages to the extension to fetch data:
Expand All @@ -38,7 +43,7 @@ port.postMessage({
})

port.onMessage.addListener(response => {
console.log(response.data) // The extension will send the message with fetched data
console.log(response.data) // The extension will send the message
})
```

Expand All @@ -48,12 +53,11 @@ Check if the extension was disconnected:
port.onDisconnect.addListener(() => {})
```

## Publishing

- Run `pnpm build` to build the production files (will be located in `dist/`)

- Zip the content of the `dist/` folder
See possible messages in [types API](./api.ts).

- [Follow this official guide to publish the extension in the Chrome Web Store](https://developer.chrome.com/docs/webstore/publish)
## Publishing

- After the extension is published in the Chrome Web Store, add the <EXTENSION_ID> of the published extension as a prod env for the main app.
1. Run `pnpm build` to build the production files (will be located in `dist/`)
2. Zip the content of the `dist/` folder
3. Follow [official guide](https://developer.chrome.com/docs/webstore/publish) to publish the extension in the Chrome Web Store.
4. After the extension is published in the Chrome Web Store, add the `EXTENSION_ID` of the published extension as a prod env for the web app.
9 changes: 9 additions & 0 deletions extension/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type AppMessage = {
options: RequestInit
url: string
}

export type ExtensionMessage =
| { data: string; type: 'fetched' }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way of typing messages is better because it tells that msg.error will be only for msg.type = 'error'

| { error: string; type: 'error' }
| { type: 'connected' }
26 changes: 12 additions & 14 deletions extension/src/background.ts → extension/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,32 @@ import { config } from './config.js'

const FETCH_TIMEOUT_MS = 30000

const sendMessage = (
function sendMessage(
port: chrome.runtime.Port,
message: ExtensionMessage
): void => {
): void {
port.postMessage(message)
}

chrome.runtime.onConnectExternal.addListener(port => {
if (port.sender?.origin === config.HOST) {
sendMessage(port, { type: 'connected' })
port.onMessage.addListener(async (message: AppMessage) => {
if (message.url) {
await fetch(message.url, {
try {
let response = await fetch(message.url, {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By using await everywhere we are having better readability in contrast with mixing await and .then()

...message.options,
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
})
.then(data => {
sendMessage(port, { data, type: 'fetched' })
})
.catch(error => {
sendMessage(port, {
data: null,
error: error.toString(),
type: 'error'
})
let data = await response.text()
sendMessage(port, { data, type: 'fetched' })
} catch (error) {
if (error instanceof Error) {
sendMessage(port, {
error: error.toString(),
type: 'error'
})
}
}
return true
})
}
})
18 changes: 0 additions & 18 deletions extension/build.js

This file was deleted.

File renamed without changes.
21 changes: 21 additions & 0 deletions extension/manifest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { defineManifest } from '@crxjs/vite-plugin'

const URL =
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use function instead of static JSON file for manifest

process.env.NODE_ENV === 'production'
? 'https://*.slowreader.app/*'
: 'http://localhost:5173/*'

export default defineManifest(async () => ({
background: {
service_worker: 'background.ts',
type: 'module'
},
description: 'Fetch data from websites for dev.slowreader.app',
externally_connectable: {
matches: [URL]
},
host_permissions: [URL],
manifest_version: 3,
name: 'Slowreader Extension',
version: '0.0.1'
}))
14 changes: 0 additions & 14 deletions extension/manifest.dev.json

This file was deleted.

14 changes: 0 additions & 14 deletions extension/manifest.json

This file was deleted.

14 changes: 0 additions & 14 deletions extension/manifest.prod.json

This file was deleted.

19 changes: 4 additions & 15 deletions extension/package.json
Original file line number Diff line number Diff line change
@@ -1,29 +1,18 @@
{
"name": "slowreader-extension",
"name": "@slowreader/extension",
"private": true,
"version": "0.0.0",
"type": "module",
"engines": {
"node": "^22.6.0",
"pnpm": "^9.0.0"
},
"scripts": {
"dev": "node build.js dev && vite",
"build": "node build.js prod && vite build"
"start": "vite",
"build": "vite build",
"test": "pnpm build"
},
"keywords": [],
"author": "[email protected]",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You do not need this keys on private: true

"license": "ISC",
"packageManager": "[email protected]",
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@crxjs/vite-plugin": "2.0.0-beta.23",
"@types/react": "^18.2.64",
"@types/react-dom": "^18.2.21",
"@vitejs/plugin-react": "^4.2.1",
"vite": "^5.3.4"
}
}
14 changes: 0 additions & 14 deletions extension/src/api.ts

This file was deleted.

File renamed without changes.
11 changes: 2 additions & 9 deletions extension/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
import { crx } from '@crxjs/vite-plugin'
import react from '@vitejs/plugin-react'
import path from 'node:path'
import { defineConfig } from 'vite'

import manifest from './manifest.json'

const outDir = path.resolve(__dirname, 'dist')
import defineManifest from './manifest.config.ts'

export default defineConfig({
build: {
outDir
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You do not need to specify outDir since dist is the default value

},
plugins: [react(), crx({ manifest })]
plugins: [crx({ manifest: defineManifest })]
})
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"server",
"web",
"loader-tests",
"proxy"
"proxy",
"extension"
],
"devDependencies": {
"@logux/eslint-config": "53.3.0",
Expand Down
Loading
Loading