[HACKTOBERFEST] LINKEDIN EXTENSION (#15521)
# Twenty Browser Extension A Chrome browser extension for capturing LinkedIn profiles (people and companies) directly into Twenty CRM. This is a basic **v0** focused mostly on establishing a architectural foundation. ## Overview This extension integrates with LinkedIn to extract profile information and create records in Twenty CRM. It uses **WXT** as the framework - initially tried Plasmo, but found WXT to be significantly better due to its extensibility and closer alignment with the Chrome extension APIs, providing more control and flexibility. ## Architecture ### Package Structure The extension consists of two main packages: 1. **`twenty-browser-extension`** - The main extension package (WXT + React) 2. **`twenty-apps/browser-extension`** - Serverless functions for API interactions ### Extension Components #### Entrypoints - **Background Script** (`src/entrypoints/background/index.ts`) - Handles extension messaging protocol - Manages API calls to serverless functions - Coordinates communication between content scripts and popup - **Content Scripts** - **`add-person.content`** - Injects UI button on LinkedIn person profiles - **`add-company.content`** - Injects UI button on LinkedIn company profiles - Both scripts use WXT's `createIntegratedUi` for seamless DOM injection - Extract profile data from LinkedIn DOM - **Popup** (`src/entrypoints/popup/`) - React-based popup UI - Displays extracted profile information - Provides buttons to save person/company to Twenty #### Messaging System Uses `@webext-core/messaging` for type-safe communication between extension components: ```typescript // Defined in src/utils/messaging.ts - getPersonviaRelay() - Relays extraction from content script - getCompanyviaRelay() - Relays extraction from content script - extractPerson() - Extracts person data from LinkedIn DOM - extractCompany() - Extracts company data from LinkedIn DOM - createPerson() - Creates person record via serverless function - createCompany() - Creates company record via serverless function - openPopup() - Opens extension popup ``` #### Serverless Functions Located in `packages/twenty-apps/browser-extension/serverlessFunctions/`: - **`/s/create/person`** - Creates a new person record in Twenty - **`/s/create/company`** - Creates a new company record in Twenty - **`/s/get/person`** - Retrieves existing person record (placeholder) - **`/s/get/company`** - Retrieves existing company record (placeholder) ## Development Guide ### Prerequisites - Twenty CLI installed globally: `npm install -g twenty-cli` - API key from Twenty: https://twenty.com/settings/api-webhooks ### Setup ``` 1. **Configure environment variables:** - Set `TWENTY_API_URL` in the serverless function configuration - Set `TWENTY_API_KEY` (marked as secret) in the serverless function configuration - For local development, create a `.env` file or configure via `wxt.config.ts` ### Development Commands ```bash # Start development server with hot reload npx nx run dev twenty-browser-extension # Build for production npx nx run build twenty-browser-extension # Package extension for distribution npx nx run package twenty-browser-extension ``` ### Development Workflow 1. **Start the dev server:** ```bash npx nx run dev twenty-browser-extension ``` This starts WXT in development mode with hot module reloading. 2. **Load extension in Chrome:** - Navigate to `chrome://extensions/` - Enable "Developer mode" - Click "Load unpacked" - Select `packages/twenty-browser-extension/dist/chrome-mv3-dev/` 3. **Test on LinkedIn:** - Navigate to a LinkedIn person profile: `https://www.linkedin.com/in/...` - Navigate to a LinkedIn company profile: `https://www.linkedin.com/company/...` - The "Add to Twenty" button should appear in the profile header - Click the button to open the popup and save to Twenty ### Project Structure ``` packages/twenty-browser-extension/ ├── src/ │ ├── common/ │ │ └── constants/ # LinkedIn URL patterns │ ├── entrypoints/ │ │ ├── background/ # Background service worker │ │ ├── popup/ # Extension popup UI │ │ ├── add-person.content/ # Content script for person profiles │ │ └── add-company.content/ # Content script for company profiles │ ├── ui/ # Shared UI components and theme │ └── utils/ # Messaging utilities ├── public/ # Static assets (icons) ├── wxt.config.ts # WXT configuration └── project.json # Nx project configuration ``` ## Current Status (v0) This is a foundational version focused on architecture. Current features: ✅ Inject UI buttons into LinkedIn profiles ✅ Extract person and company data from LinkedIn ✅ Display extracted data in popup ✅ Create person records in Twenty ✅ Create company records in Twenty ## Planned Features - [ ] Provide a way to have API key and custom remote URLs. - [ ] Detect if record already exists and prevent duplicates - [ ] Open existing Twenty record when clicked (instead of creating duplicate) - [ ] Sidepanel Overlay UI for rich profile viewing/editing - [ ] Enhanced data extraction (email, phone, etc.) - [ ] Better error handling # Demo https://github.com/user-attachments/assets/0bbed724-a429-4af0-a0f1-fdad6997685e https://github.com/user-attachments/assets/85d2301d-19ee-43ba-b7f9-13ed3915f676
@@ -233,6 +233,7 @@
|
||||
"packages/twenty-sdk",
|
||||
"packages/twenty-apps",
|
||||
"packages/twenty-cli",
|
||||
"packages/twenty-browser-extension",
|
||||
"tools/eslint-rules"
|
||||
]
|
||||
}
|
||||
|
||||
2
packages/twenty-apps/browser-extension/.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
TWENTY_API_URL=
|
||||
TWENTY_API_KEY=
|
||||
2
packages/twenty-apps/browser-extension/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.yarn/install-state.gz
|
||||
.env
|
||||
942
packages/twenty-apps/browser-extension/.yarn/releases/yarn-4.9.2.cjs
vendored
Executable file
3
packages/twenty-apps/browser-extension/.yarnrc.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
yarnPath: .yarn/releases/yarn-4.9.2.cjs
|
||||
|
||||
nodeLinker: node-modules
|
||||
113
packages/twenty-apps/browser-extension/README.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# Browser Extension Serverless Functions
|
||||
|
||||
Serverless functions for the Twenty browser extension. These functions handle API interactions between the browser extension and the Twenty backend.
|
||||
|
||||
## Overview
|
||||
|
||||
This package contains serverless functions that are deployed to your Twenty workspace. The browser extension calls these functions to create and retrieve records in Twenty CRM.
|
||||
|
||||
## Functions
|
||||
|
||||
### Create Person
|
||||
**Endpoint:** `/s/create/person`
|
||||
|
||||
Creates a new person record in Twenty from LinkedIn profile data.
|
||||
|
||||
**Parameters:**
|
||||
- `firstName` (string) - Person's first name
|
||||
- `lastName` (string) - Person's last name
|
||||
|
||||
**Response:** Created person object
|
||||
|
||||
### Create Company
|
||||
**Endpoint:** `/s/create/company`
|
||||
|
||||
Creates a new company record in Twenty from LinkedIn company profile data.
|
||||
|
||||
**Parameters:**
|
||||
- `name` (string) - Company name
|
||||
|
||||
**Response:** Created company object
|
||||
|
||||
### Get Person
|
||||
**Endpoint:** `/s/get/person`
|
||||
|
||||
Retrieves an existing person record from Twenty (placeholder implementation).
|
||||
|
||||
### Get Company
|
||||
**Endpoint:** `/s/get/company`
|
||||
|
||||
Retrieves an existing company record from Twenty (placeholder implementation).
|
||||
|
||||
## Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Twenty CLI** installed globally:
|
||||
```bash
|
||||
npm install -g twenty-cli
|
||||
```
|
||||
- **API Key** from your Twenty workspace:
|
||||
- Go to https://twenty.com/settings/api-webhooks
|
||||
- Generate an API key
|
||||
|
||||
### Configuration
|
||||
|
||||
1. **Authenticate with Twenty CLI:**
|
||||
```bash
|
||||
twenty auth login
|
||||
```
|
||||
|
||||
2. **Sync serverless functions to your workspace:**
|
||||
```bash
|
||||
twenty app sync
|
||||
```
|
||||
|
||||
3. **Configure environment variables:**
|
||||
- `TWENTY_API_URL` - Your Twenty API URL (e.g., `https://your-workspace.twenty.com`)
|
||||
- `TWENTY_API_KEY` - Your Twenty API key (marked as secret)
|
||||
|
||||
Environment variables can be configured via the Twenty CLI or the Twenty web interface after syncing.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. The browser extension extracts data from LinkedIn profiles
|
||||
2. The extension calls the serverless functions via the background script
|
||||
3. Serverless functions authenticate with your Twenty API using the configured API key
|
||||
4. Functions create or retrieve records in your Twenty workspace
|
||||
5. Response is sent back to the extension for user feedback
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
serverlessFunctions/
|
||||
├── create-person/
|
||||
│ ├── serverlessFunction.manifest.jsonc # Function configuration
|
||||
│ └── src/
|
||||
│ └── index.ts # Function implementation
|
||||
├── create-company/
|
||||
│ ├── serverlessFunction.manifest.jsonc
|
||||
│ └── src/
|
||||
│ └── index.ts
|
||||
├── get-person/
|
||||
│ ├── serverlessFunction.manifest.jsonc
|
||||
│ └── src/
|
||||
│ └── index.ts
|
||||
└── get-company/
|
||||
├── serverlessFunction.manifest.jsonc
|
||||
└── src/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
These functions are managed by the Twenty CLI and are deployed to your workspace. After making changes:
|
||||
|
||||
1. Update the function code in `src/index.ts`
|
||||
2. Run `twenty app sync` to deploy changes to your workspace
|
||||
3. Test the functions via the browser extension or Twenty API directly
|
||||
|
||||
## Related Packages
|
||||
|
||||
- **`twenty-browser-extension`** - The main browser extension that calls these functions
|
||||
- See `packages/twenty-browser-extension/README.md` for the complete extension documentation
|
||||
29
packages/twenty-apps/browser-extension/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"npm": "please-use-yarn",
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2"
|
||||
},
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/appManifest.schema.json",
|
||||
"universalIdentifier": "627280a0-cb5b-40d3-a2e3-3e34b92926c8",
|
||||
"name": "Browser Extension",
|
||||
"description": "",
|
||||
"env": {
|
||||
"TWENTY_API_URL": {
|
||||
"isSecret": false,
|
||||
"value": "",
|
||||
"description": "Twenty API URL"
|
||||
},
|
||||
"TWENTY_API_KEY": {
|
||||
"isSecret": true,
|
||||
"value": "",
|
||||
"description": "Twenty API Key"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/serverlessFunction.schema.json",
|
||||
"universalIdentifier": "cead3d1e-1fbd-4b09-86a9-f0bedf4d54fa",
|
||||
"name": "create-company",
|
||||
"triggers": [
|
||||
{
|
||||
"universalIdentifier": "57ff5ea2-c4b7-458c-9296-27bad6acdaf9",
|
||||
"type": "route",
|
||||
"path": "/create/company",
|
||||
"httpMethod": "POST",
|
||||
"isAuthRequired": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export const main = async (params: {
|
||||
name: string
|
||||
}): Promise<object> => {
|
||||
const response = await fetch(`${process.env.TWENTY_API_URL}/rest/companies`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${process.env.TWENTY_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: params.name
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return (await response.json()) as object;
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/serverlessFunction.schema.json",
|
||||
"universalIdentifier": "7d38261b-99c5-43e7-83d8-bdcedc2dffdb",
|
||||
"name": "create-person",
|
||||
"triggers": [
|
||||
{
|
||||
"universalIdentifier": "ecf261b8-183b-4323-ab95-3b11009a0eae",
|
||||
"type": "route",
|
||||
"path": "/create/person",
|
||||
"httpMethod": "POST",
|
||||
"isAuthRequired": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export const main = async (params: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
}): Promise<object> => {
|
||||
const response = await fetch(`${process.env.TWENTY_API_URL}/rest/people`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${process.env.TWENTY_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: {
|
||||
firstName: params.firstName,
|
||||
lastName: params.lastName,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return (await response.json()) as object;
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/serverlessFunction.schema.json",
|
||||
"universalIdentifier": "8e43b96b-49a1-4e21-b257-e432a757b09f",
|
||||
"name": "get-company",
|
||||
"triggers": [
|
||||
{
|
||||
"universalIdentifier": "7a2bb8ad-6366-49ac-9f73-db9c4713c5af",
|
||||
"type": "route",
|
||||
"path": "/get/company",
|
||||
"httpMethod": "GET",
|
||||
"isAuthRequired": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export const main = async (params: {
|
||||
a: string;
|
||||
b: number;
|
||||
}): Promise<object> => {
|
||||
const { a, b } = params;
|
||||
|
||||
// Rename the parameters and code below with your own logic
|
||||
// This is just an example
|
||||
const message = `Hello, input: ${a} and ${b}`;
|
||||
|
||||
|
||||
|
||||
return { message };
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/serverlessFunction.schema.json",
|
||||
"universalIdentifier": "87ea9816-c9e5-4860-b49f-a5f0759800f7",
|
||||
"name": "get-person",
|
||||
"triggers": [
|
||||
{
|
||||
"universalIdentifier": "54aec609-0518-4fb0-bd90-7cd21507fe11",
|
||||
"type": "route",
|
||||
"path": "/get/person",
|
||||
"httpMethod": "GET",
|
||||
"isAuthRequired": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export const main = async (params: {
|
||||
a: string;
|
||||
b: number;
|
||||
}): Promise<object> => {
|
||||
const { a, b } = params;
|
||||
|
||||
// Rename the parameters and code below with your own logic
|
||||
// This is just an example
|
||||
const message = `Hello, input: ${a} and ${b}`;
|
||||
|
||||
|
||||
|
||||
return { message };
|
||||
};
|
||||
0
packages/twenty-apps/browser-extension/yarn.lock
Normal file
2
packages/twenty-browser-extension/.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
WXT_TWENTY_API_URL=
|
||||
WXT_TWENTY_API_KEY=
|
||||
26
packages/twenty-browser-extension/.gitignore
vendored
Executable file
@@ -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?
|
||||
158
packages/twenty-browser-extension/README.md
Executable file
@@ -0,0 +1,158 @@
|
||||
# Twenty Browser Extension
|
||||
|
||||
A Chrome browser extension for capturing LinkedIn profiles (people and companies) directly into Twenty CRM. This is a basic **v0** focused mostly on establishing a solid architectural foundation.
|
||||
|
||||
## Overview
|
||||
|
||||
This extension integrates with LinkedIn to extract profile information and create records in Twenty CRM. It uses **WXT** as the framework - initially tried Plasmo, but found WXT to be significantly better due to its extensibility and closer alignment with the Chrome extension APIs, providing more control and flexibility.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Package Structure
|
||||
|
||||
The extension consists of two main packages:
|
||||
|
||||
1. **`twenty-browser-extension`** - The main extension package (WXT + React)
|
||||
2. **`twenty-apps/browser-extension`** - Serverless functions for API interactions
|
||||
|
||||
### Extension Components
|
||||
|
||||
#### Entrypoints
|
||||
|
||||
- **Background Script** (`src/entrypoints/background/index.ts`)
|
||||
- Handles extension messaging protocol
|
||||
- Manages API calls to serverless functions
|
||||
- Coordinates communication between content scripts and popup
|
||||
|
||||
- **Content Scripts**
|
||||
- **`add-person.content`** - Injects UI button on LinkedIn person profiles
|
||||
- **`add-company.content`** - Injects UI button on LinkedIn company profiles
|
||||
- Both scripts use WXT's `createIntegratedUi` for seamless DOM injection
|
||||
- Extract profile data from LinkedIn DOM
|
||||
|
||||
- **Popup** (`src/entrypoints/popup/`)
|
||||
- React-based popup UI
|
||||
- Displays extracted profile information
|
||||
- Provides buttons to save person/company to Twenty
|
||||
|
||||
#### Messaging System
|
||||
|
||||
Uses `@webext-core/messaging` for type-safe communication between extension components:
|
||||
|
||||
```typescript
|
||||
// Defined in src/utils/messaging.ts
|
||||
- getPersonviaRelay() - Relays extraction from content script
|
||||
- getCompanyviaRelay() - Relays extraction from content script
|
||||
- extractPerson() - Extracts person data from LinkedIn DOM
|
||||
- extractCompany() - Extracts company data from LinkedIn DOM
|
||||
- createPerson() - Creates person record via serverless function
|
||||
- createCompany() - Creates company record via serverless function
|
||||
- openPopup() - Opens extension popup
|
||||
```
|
||||
|
||||
#### Serverless Functions
|
||||
|
||||
Located in `packages/twenty-apps/browser-extension/serverlessFunctions/`:
|
||||
|
||||
- **`/s/create/person`** - Creates a new person record in Twenty
|
||||
- **`/s/create/company`** - Creates a new company record in Twenty
|
||||
- **`/s/get/person`** - Retrieves existing person record (placeholder)
|
||||
- **`/s/get/company`** - Retrieves existing company record (placeholder)
|
||||
|
||||
## Development Guide
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Twenty CLI installed globally: `npm install -g twenty-cli`
|
||||
- API key from Twenty: https://twenty.com/settings/api-webhooks
|
||||
|
||||
### Setup
|
||||
```
|
||||
1. **Configure environment variables:**
|
||||
- Set `TWENTY_API_URL` in the serverless function configuration
|
||||
- Set `TWENTY_API_KEY` (marked as secret) in the serverless function configuration
|
||||
- For local development, create a `.env` file or configure via `wxt.config.ts`
|
||||
|
||||
### Development Commands
|
||||
|
||||
```bash
|
||||
# Start development server with hot reload
|
||||
npx nx run dev twenty-browser-extension
|
||||
|
||||
# Build for production
|
||||
npx nx run build twenty-browser-extension
|
||||
|
||||
# Package extension for distribution
|
||||
npx nx run package twenty-browser-extension
|
||||
```
|
||||
|
||||
### Development Workflow
|
||||
|
||||
1. **Start the dev server:**
|
||||
```bash
|
||||
npx nx run dev twenty-browser-extension
|
||||
```
|
||||
This starts WXT in development mode with hot module reloading.
|
||||
|
||||
2. **Load extension in Chrome:**
|
||||
- Navigate to `chrome://extensions/`
|
||||
- Enable "Developer mode"
|
||||
- Click "Load unpacked"
|
||||
- Select `packages/twenty-browser-extension/dist/chrome-mv3-dev/`
|
||||
|
||||
3. **Test on LinkedIn:**
|
||||
- Navigate to a LinkedIn person profile: `https://www.linkedin.com/in/...`
|
||||
- Navigate to a LinkedIn company profile: `https://www.linkedin.com/company/...`
|
||||
- The "Add to Twenty" button should appear in the profile header
|
||||
- Click the button to open the popup and save to Twenty
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
packages/twenty-browser-extension/
|
||||
├── src/
|
||||
│ ├── common/
|
||||
│ │ └── constants/ # LinkedIn URL patterns
|
||||
│ ├── entrypoints/
|
||||
│ │ ├── background/ # Background service worker
|
||||
│ │ ├── popup/ # Extension popup UI
|
||||
│ │ ├── add-person.content/ # Content script for person profiles
|
||||
│ │ └── add-company.content/ # Content script for company profiles
|
||||
│ ├── ui/ # Shared UI components and theme
|
||||
│ └── utils/ # Messaging utilities
|
||||
├── public/ # Static assets (icons)
|
||||
├── wxt.config.ts # WXT configuration
|
||||
└── project.json # Nx project configuration
|
||||
```
|
||||
|
||||
## Current Status (v0)
|
||||
|
||||
This is a foundational version focused on architecture. Current features:
|
||||
|
||||
✅ Inject UI buttons into LinkedIn profiles
|
||||
✅ Extract person and company data from LinkedIn
|
||||
✅ Display extracted data in popup
|
||||
✅ Create person records in Twenty
|
||||
✅ Create company records in Twenty
|
||||
|
||||
## Planned Features
|
||||
|
||||
- [ ] Provide a way to have API key and custom remote URLs.
|
||||
- [ ] Detect if record already exists and prevent duplicates
|
||||
- [ ] Open existing Twenty record when clicked (instead of creating duplicate)
|
||||
- [ ] Sidepanel Overlay UI for rich profile viewing/editing
|
||||
- [ ] Enhanced data extraction (email, phone, etc.)
|
||||
- [ ] Better error handling
|
||||
|
||||
## Why WXT?
|
||||
|
||||
We initially evaluated **Plasmo** but chose **WXT** for several reasons:
|
||||
|
||||
1. **Extensibility** - WXT provides more flexibility for custom extension patterns
|
||||
2. **Chrome API Proximity** - Closer to native Chrome extension APIs, giving more control
|
||||
3. **Type Safety** - Better TypeScript support for extension messaging
|
||||
4. **Architecture** - More transparent build process and entrypoint management
|
||||
|
||||
## Contributing
|
||||
|
||||
This extension is in early development. The architecture is designed to be extensible, but the current implementation is intentionally minimal to establish solid foundations.
|
||||
69
packages/twenty-browser-extension/eslint.config.mjs
Executable file
@@ -0,0 +1,69 @@
|
||||
import typescriptParser from '@typescript-eslint/parser';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import reactConfig from '../../eslint.config.react.mjs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export default [
|
||||
// Extend shared React configuration
|
||||
...reactConfig,
|
||||
|
||||
// Global ignores
|
||||
{
|
||||
ignores: [
|
||||
'**/node_modules/**',
|
||||
],
|
||||
},
|
||||
|
||||
// TypeScript project-specific configuration
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
parserOptions: {
|
||||
project: [path.resolve(__dirname, 'tsconfig.*.json')],
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'@nx/enforce-module-boundaries': [
|
||||
'error',
|
||||
{
|
||||
enforceBuildableLibDependency: true,
|
||||
allow: [],
|
||||
depConstraints: [
|
||||
{
|
||||
sourceTag: 'scope:sdk',
|
||||
onlyDependOnLibsWithTags: ['scope:sdk'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:shared',
|
||||
onlyDependOnLibsWithTags: ['scope:shared'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:backend',
|
||||
onlyDependOnLibsWithTags: ['scope:shared', 'scope:backend'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:frontend',
|
||||
onlyDependOnLibsWithTags: ['scope:shared', 'scope:frontend'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:zapier',
|
||||
onlyDependOnLibsWithTags: ['scope:shared'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:browser-extension',
|
||||
onlyDependOnLibsWithTags: ['scope:twenty-ui', 'scope:browser-extension']
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
];
|
||||
22
packages/twenty-browser-extension/package.json
Executable file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "twenty-browser-extension",
|
||||
"description": "Twenty Lead capture extension",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@webext-core/messaging": "^2.3.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"twenty-ui": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.39",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@wxt-dev/module-react": "^1.1.3",
|
||||
"typescript": "^5.9.2",
|
||||
"wxt": "^0.20.6"
|
||||
}
|
||||
}
|
||||
48
packages/twenty-browser-extension/project.json
Executable file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "twenty-browser-extension",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "packages/twenty-browser-extension/src",
|
||||
"projectType": "application",
|
||||
"tags": ["scope:browser-extension"],
|
||||
"targets": {
|
||||
"dev": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"command": "wxt --mode development",
|
||||
"cwd": "packages/twenty-browser-extension",
|
||||
"color": true
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"command": "wxt --mode development"
|
||||
}
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"command": "wxt build",
|
||||
"cwd": "packages/twenty-browser-extension",
|
||||
"color": true
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"command": "wxt build"
|
||||
}
|
||||
}
|
||||
},
|
||||
"package": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"command": "wxt zip",
|
||||
"cwd": "packages/twenty-browser-extension",
|
||||
"color": true
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"command": "wxt zip"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
packages/twenty-browser-extension/public/icon/128.png
Executable file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
packages/twenty-browser-extension/public/icon/16.png
Executable file
|
After Width: | Height: | Size: 559 B |
BIN
packages/twenty-browser-extension/public/icon/32.png
Executable file
|
After Width: | Height: | Size: 916 B |
BIN
packages/twenty-browser-extension/public/icon/48.png
Executable file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
packages/twenty-browser-extension/public/icon/96.png
Executable file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
packages/twenty-browser-extension/public/wxt.svg
Executable file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
packages/twenty-browser-extension/src/assets/react.svg
Executable file
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,2 @@
|
||||
export { LINKEDIN_MATCHES } from './linkedin';
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum LINKEDIN_MATCHES {
|
||||
COMPANY = "*://*.linkedin.com/company/*",
|
||||
PERSON = "*://*.linkedin.com/in/*",
|
||||
BASE_URL = "*://*.linkedin.com/*"
|
||||
}
|
||||
2
packages/twenty-browser-extension/src/common/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { LINKEDIN_MATCHES } from './constants';
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { LINKEDIN_MATCHES } from '@/common';
|
||||
import Main from '@/entrypoints/add-company.content/main';
|
||||
import { ThemeContext } from '@/ui/theme/context';
|
||||
import styled from '@emotion/styled';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
const companyPattern = new MatchPattern(LINKEDIN_MATCHES.COMPANY);
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
margin: ${({theme}) => `${theme.spacing(1)} ${0} ${0} ${theme.spacing(2)}`};
|
||||
`
|
||||
|
||||
export default defineContentScript({
|
||||
matches: [LINKEDIN_MATCHES.BASE_URL],
|
||||
runAt: 'document_end',
|
||||
async main(ctx) {
|
||||
|
||||
async function waitFor(sel: string) {
|
||||
return new Promise<Element>((resolve) => {
|
||||
const tryFind = () => {
|
||||
const el = document.querySelector(sel) as Element;
|
||||
if (el) return resolve(el);
|
||||
requestAnimationFrame(tryFind);
|
||||
};
|
||||
tryFind();
|
||||
});
|
||||
}
|
||||
|
||||
const anchor = await waitFor('[class*="org-top-card-primary-actions__inner"]');
|
||||
const ui = await createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
anchor,
|
||||
append:'last',
|
||||
onMount: (container) => {
|
||||
const app = document.createElement('div');
|
||||
container.append(app);
|
||||
|
||||
const root = ReactDOM.createRoot(app);
|
||||
const App = () => (
|
||||
<StyledContainer>
|
||||
<Main/>
|
||||
</StyledContainer>
|
||||
);
|
||||
|
||||
root.render(
|
||||
<ThemeContext>
|
||||
<App />
|
||||
</ThemeContext>
|
||||
);
|
||||
return root;
|
||||
},
|
||||
onRemove: (root) => {
|
||||
root?.unmount();
|
||||
}
|
||||
});
|
||||
|
||||
ctx.addEventListener(window, 'wxt:locationchange', ({newUrl}) => {
|
||||
const injectedBtn = document.querySelector('[data-id="twenty-btn"]');
|
||||
if(companyPattern.includes(newUrl) && !injectedBtn) ui.mount()
|
||||
})
|
||||
|
||||
ui.mount();
|
||||
|
||||
onMessage('extractCompany', async () => {
|
||||
const companyNameElement = document.querySelector('h1');
|
||||
const companyName = companyNameElement?.textContent ?? '';
|
||||
|
||||
return {
|
||||
companyName
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Button } from "@/ui/components/button";
|
||||
import { sendMessage } from "@/utils/messaging";
|
||||
|
||||
const Main = () => {
|
||||
|
||||
const handleClick = async () => {
|
||||
await sendMessage('openPopup')
|
||||
};
|
||||
|
||||
return <Button onClick={handleClick} data-id="twenty-btn">Add to Twenty</Button>;
|
||||
};
|
||||
|
||||
export default Main;
|
||||
@@ -0,0 +1,67 @@
|
||||
import { LINKEDIN_MATCHES } from '@/common';
|
||||
import Main from '@/entrypoints/add-person.content/main';
|
||||
import { ThemeContext } from '@/ui/theme/context';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
const personPattern = new MatchPattern(LINKEDIN_MATCHES.PERSON);
|
||||
|
||||
export default defineContentScript({
|
||||
matches: [LINKEDIN_MATCHES.BASE_URL],
|
||||
runAt: 'document_end',
|
||||
async main(ctx) {
|
||||
|
||||
async function waitFor(sel: string) {
|
||||
return new Promise<Element>((resolve) => {
|
||||
const tryFind = () => {
|
||||
const el = document.querySelector(sel);
|
||||
if (el) return resolve(el);
|
||||
requestAnimationFrame(tryFind);
|
||||
};
|
||||
tryFind();
|
||||
});
|
||||
}
|
||||
|
||||
const anchor = await waitFor('[class$="pv-top-card-v2-ctas__custom"]');
|
||||
const ui = await createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
anchor,
|
||||
append:'last',
|
||||
onMount: (container) => {
|
||||
const app = document.createElement('div');
|
||||
container.append(app);
|
||||
|
||||
const root = ReactDOM.createRoot(app);
|
||||
root.render(
|
||||
<ThemeContext>
|
||||
<Main />
|
||||
</ThemeContext>
|
||||
);
|
||||
return root;
|
||||
},
|
||||
onRemove: (root) => {
|
||||
root?.unmount();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
ctx.addEventListener(window, 'wxt:locationchange', ({newUrl, }) => {
|
||||
const injectedBtn = document.querySelector('[data-id="twenty-btn"]');
|
||||
if(personPattern.includes(newUrl) && !injectedBtn) ui.mount();
|
||||
});
|
||||
|
||||
ui.mount();
|
||||
|
||||
onMessage('extractPerson', async () => {
|
||||
const personNameElement = document.querySelector('h1');
|
||||
const personName = personNameElement ? personNameElement.textContent : '';
|
||||
const extractFirstAndLastName = (fullName: string) => {
|
||||
const spaceIndex = fullName.lastIndexOf(' ');
|
||||
const firstName = fullName.substring(0, spaceIndex);
|
||||
const lastName = fullName.substring(spaceIndex + 1);
|
||||
return { firstName, lastName };
|
||||
};
|
||||
|
||||
return extractFirstAndLastName(personName);
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Button } from "@/ui/components/button";
|
||||
import { sendMessage } from "@/utils/messaging";
|
||||
|
||||
const Main = () => {
|
||||
|
||||
const handleClick = async () => {
|
||||
await sendMessage('openPopup')
|
||||
};
|
||||
|
||||
return <Button onClick={handleClick} data-id="twenty-btn">Add to Twenty</Button>;
|
||||
};
|
||||
|
||||
export default Main;
|
||||
@@ -0,0 +1,70 @@
|
||||
import { onMessage } from "@/utils/messaging";
|
||||
import { SendMessageOptions } from "@webext-core/messaging";
|
||||
|
||||
export default defineBackground(async () => {
|
||||
onMessage('openPopup', async () => {
|
||||
await browser.action.openPopup();
|
||||
})
|
||||
|
||||
onMessage('getPersonviaRelay', async () => {
|
||||
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
|
||||
const {firstName, lastName} = await sendMessage('extractPerson', undefined, {
|
||||
tabId: tab.id,
|
||||
frameId: 0
|
||||
} as SendMessageOptions);
|
||||
|
||||
return {
|
||||
firstName,
|
||||
lastName
|
||||
}
|
||||
})
|
||||
|
||||
onMessage('getCompanyviaRelay', async () => {
|
||||
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
|
||||
const {companyName} = await sendMessage('extractCompany', undefined, {
|
||||
tabId: tab.id,
|
||||
frameId: 0
|
||||
} as SendMessageOptions);
|
||||
|
||||
return {
|
||||
companyName
|
||||
}
|
||||
})
|
||||
|
||||
onMessage('createPerson', async ({data}) => {
|
||||
const response = await fetch(`${import.meta.env.WXT_TWENTY_API_URL}/s/create/person`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${import.meta.env.WXT_TWENTY_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return (await response.json()) as {firstName: string; lastName:string;};
|
||||
})
|
||||
|
||||
onMessage('createCompany', async ({data}) => {
|
||||
const response = await fetch(`${import.meta.env.WXT_TWENTY_API_URL}/s/create/company`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${import.meta.env.WXT_TWENTY_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: data.name
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return (await response.json()) as {name: string};
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
import { sendMessage } from '@/utils/messaging';
|
||||
import styled from '@emotion/styled';
|
||||
import { useEffect, useState } from 'react';
|
||||
const StyledMain = styled.main``;
|
||||
|
||||
type PersonValue = { firstName: string; lastName: string; type: 'company' | 'person' };
|
||||
type CompanyValue = { companyName: string; type: 'company' | 'person' };
|
||||
type Value = PersonValue | CompanyValue | undefined;
|
||||
|
||||
function App() {
|
||||
const [value, setValue] = useState<Value>();
|
||||
useEffect(() => {
|
||||
browser.tabs.query({ active: true, currentWindow: true }, function(tabs) {
|
||||
const currentTab = tabs[0];
|
||||
if(currentTab.url?.includes('https://www.linkedin.com/in')) {
|
||||
sendMessage('getPersonviaRelay').then(data => {
|
||||
setValue({...data, type: 'person' })
|
||||
})
|
||||
}
|
||||
|
||||
if(currentTab.url?.includes('https://www.linkedin.com/company')) {
|
||||
sendMessage('getCompanyviaRelay').then(data => {
|
||||
setValue({...data, type: 'company'})
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
}, []);
|
||||
|
||||
const isPersonValue = (val: Value): val is PersonValue => {
|
||||
return val !== undefined && 'firstName' in val && 'lastName' in val;
|
||||
};
|
||||
|
||||
const isCompanyValue = (val: Value): val is CompanyValue => {
|
||||
return val !== undefined && 'companyName' in val;
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledMain>
|
||||
<h1>{JSON.stringify(value)}</h1>
|
||||
{
|
||||
isPersonValue(value) &&
|
||||
<button onClick={async () => {
|
||||
await sendMessage('createPerson', {
|
||||
firstName: value.firstName,
|
||||
lastName: value.lastName,
|
||||
});
|
||||
|
||||
}}>save person to twenty</button>
|
||||
}
|
||||
{
|
||||
isCompanyValue(value) &&
|
||||
<button onClick={async () => {
|
||||
await sendMessage('createCompany', {
|
||||
name: value.companyName
|
||||
});
|
||||
|
||||
}}>save company to twenty</button>
|
||||
}
|
||||
</StyledMain>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,24 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Twenty</title>
|
||||
<!-- <meta
|
||||
name="manifest.default_icon"
|
||||
content="{
|
||||
16: '/icon-16.png',
|
||||
32: '/icon-32.png',
|
||||
48: '/icon-48.png',
|
||||
96: '/icon-96.png',
|
||||
128: '/icon-128.png'
|
||||
}"
|
||||
/> -->
|
||||
<meta name="manifest.type" content="page_action|browser_action" />
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
import App from '@/entrypoints/popup/app';
|
||||
import { ThemeContext } from '@/ui/theme/context/ThemeContext';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './style.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<ThemeContext>
|
||||
<App/>
|
||||
</ThemeContext>
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,5 @@
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
min-height: 500px;
|
||||
}
|
||||
52
packages/twenty-browser-extension/src/ui/components/button.tsx
Executable file
@@ -0,0 +1,52 @@
|
||||
import styled from "@emotion/styled";
|
||||
import type { ComponentPropsWithRef } from "react";
|
||||
|
||||
const StyledButton = styled.button`
|
||||
--hover-bg-color: #378fe91a;
|
||||
--hover-color: #004182;
|
||||
--hover-border-color: #004182;
|
||||
--text-color: #0a66c2;
|
||||
--bg-color: #00000000;
|
||||
|
||||
|
||||
font-size: ${({theme}) => theme.spacing(3.5)};
|
||||
font-weight: ${({theme}) => theme.font.weight.semiBold};
|
||||
font-family: ${({theme}) => theme.font.family};
|
||||
|
||||
background-color: var(--bg-color);
|
||||
min-height:${({theme}) => theme.spacing(8)};
|
||||
padding: ${({theme}) => `${theme.spacing(1.5)} ${theme.spacing(4)}`};
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
|
||||
border-radius: ${({theme}) => theme.spacing(5)};
|
||||
border: 1px solid var(--text-color);
|
||||
|
||||
transition-property: background-color, box-shadow, color;
|
||||
transition-timing-function: cubic-bezier(.4, 0, .2, 1);
|
||||
transition-duration: 167ms;
|
||||
|
||||
|
||||
&:hover {
|
||||
background-color: var(--hover-bg-color);
|
||||
color: var(--hover-color);
|
||||
box-shadow: inset 0px 0px 0px 1px var(--hover-border-color);
|
||||
}
|
||||
|
||||
@media(max-width: 990px) {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledSpan = styled.span`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
interface ButtonProps extends ComponentPropsWithRef<'button'> {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const Button = ({children, ...rest}: ButtonProps) => {
|
||||
return <StyledButton {...rest}><StyledSpan>{children}</StyledSpan></StyledButton>
|
||||
}
|
||||
17
packages/twenty-browser-extension/src/ui/theme/context/ThemeContext.tsx
Executable file
@@ -0,0 +1,17 @@
|
||||
import { ThemeProvider } from '@emotion/react';
|
||||
import type React from 'react';
|
||||
import { THEME_DARK, ThemeContextProvider } from 'twenty-ui/theme';
|
||||
|
||||
type ThemeContextProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const ThemeContext = ({ children }: ThemeContextProps) => {
|
||||
return (
|
||||
<ThemeProvider theme={THEME_DARK}>
|
||||
<ThemeContextProvider theme={THEME_DARK}>
|
||||
{children}
|
||||
</ThemeContextProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
1
packages/twenty-browser-extension/src/ui/theme/context/index.ts
Executable file
@@ -0,0 +1 @@
|
||||
export { ThemeContext } from './ThemeContext';
|
||||
5
packages/twenty-browser-extension/src/ui/theme/emotion.d.ts
vendored
Executable file
@@ -0,0 +1,5 @@
|
||||
import type { ThemeType } from 'twenty-ui/theme';
|
||||
|
||||
declare module '@emotion/react' {
|
||||
export interface Theme extends ThemeType {}
|
||||
}
|
||||
13
packages/twenty-browser-extension/src/utils/messaging.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineExtensionMessaging } from '@webext-core/messaging';
|
||||
|
||||
interface ProtocolMap {
|
||||
getPersonviaRelay(): {firstName: string; lastName: string }
|
||||
openPopup(): void;
|
||||
extractPerson(): {firstName: string; lastName: string}
|
||||
getCompanyviaRelay(): {companyName: string}
|
||||
extractCompany(): {companyName: string}
|
||||
createPerson({firstName, lastName}: {firstName: string; lastName: string}): {firstName: string; lastName: string}
|
||||
createCompany({ name }: {name: string}): {name: string}
|
||||
}
|
||||
|
||||
export const { sendMessage, onMessage } = defineExtensionMessaging<ProtocolMap>()
|
||||
11
packages/twenty-browser-extension/tsconfig.json
Executable file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "./.wxt/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "@emotion/react",
|
||||
"baseUrl": "src",
|
||||
"paths": {
|
||||
"@/*": ["*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
12
packages/twenty-browser-extension/vite.config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
});
|
||||
13
packages/twenty-browser-extension/wxt.config.ts
Executable file
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
// See https://wxt.dev/api/config.html
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/module-react'],
|
||||
srcDir: 'src',
|
||||
outDir: 'dist',
|
||||
dev: {
|
||||
server: {
|
||||
port: 4000
|
||||
}
|
||||
}
|
||||
});
|
||||