This commit is contained in:
Pionxzh
2022-12-05 19:39:46 +08:00
commit c75b0f254d
26 changed files with 4056 additions and 0 deletions

14
.editorconfig Normal file
View File

@@ -0,0 +1,14 @@
# editorconfig.org
root = true
[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 4
[*.{diff,md}]
trim_trailing_whitespace = false

11
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,11 @@
module.exports = {
root: true,
extends: [
'@pionxzh/eslint-config-ts',
],
rules: {
'no-console': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-unused-vars': 'off',
},
}

63
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,63 @@
name: Release
on:
push:
branches:
- '**'
jobs:
release:
if: "contains(github.event.head_commit.message, 'chore: release')"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v2
- name: Use Node.js v16
uses: actions/setup-node@v3
with:
node-version: '16'
registry-url: https://registry.npmjs.org/
cache: pnpm
- run: pnpm install
- name: Build
run: pnpm run build
- name: Lint
run: pnpm run lint
- name: Test
run: pnpm run test
- name: Release
run: |
pnpm run clean
pnpm run release:ci
- name: Git commit
id: commit
run: |
git config --local user.email github-actions[bot]@users.noreply.github.com
git config --local user.name github-actions[bot]
git config --global core.autocrlf true
git config --global core.safecrlf false
git add .
git commit -m "CI build" -a
continue-on-error: true
- name: Git push
uses: ad-m/github-push-action@master
if: ${{ steps.commit.outcome == 'success' }}
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }}
- name: Log
if: ${{ steps.commit.outcome != 'success' }}
run: echo Nothing to commit.

39
.github/workflows/check.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: Check
on:
push:
branches:
- '**'
pull_request:
branches:
- main
jobs:
check:
if: "!contains(github.event.head_commit.message, 'chore: release')"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v2
- name: Use Node.js v16
uses: actions/setup-node@v3
with:
node-version: '16'
registry-url: https://registry.npmjs.org/
cache: pnpm
- run: pnpm install
- name: Build
run: pnpm run build
- name: Lint
run: pnpm run lint
- name: Test
run: pnpm run test

16
.github/workflows/release-please.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: release-please
on:
push:
branches:
- main
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: google-github-actions/release-please-action@v3
with:
release-type: node
command: manifest
monorepo-tags: true

27
.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
packages/**/dist
shared/**/dist
*.local
.turbo
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
tsconfig.tsbuildinfo

4
.husky/pre-commit Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm exec lint-staged

4
.husky/pre-push Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm run test

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022-Present Pionxzh
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

24
README.md Normal file
View File

@@ -0,0 +1,24 @@
ChatGPT Exporter
==========================
![license](https://img.shields.io/github/license/pionxzh/chatgpt-exporter?color=red)
[![](https://img.shields.io/static/v1?label=%20&message=GreasyFork&style=flat-square&labelColor=7B0000&color=960000&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3ggEBCQHM3fXsAAAAVdJREFUOMudkz2qwkAUhc/goBaGJBgUtBCZyj0ILkpwAW7Bws4yO3AHLiCtEFD8KVREkoiFxZzX5A2KGfN4F04zMN+ce+5c4LMUgDmANYBnrnV+plBSi+FwyHq9TgA2LQpvCiEiABwMBtzv95RSfoNEHy8DYBzHrNVqVEr9BWKcqNFoxF6vx3a7zc1mYyC73a4MogBg7vs+z+czO50OW60Wt9stK5UKp9Mpj8cjq9WqDTBHnjAdxzGQZrPJw+HA31oulzbAWgLoA0CWZVBKIY5jzGYzdLtdE9DlcrFNrY98zobqOA6TJKHW2jg4nU5sNBpFDp6mhVe5rsvVasUwDHm9Xqm15u12o+/7Hy0gD8KatOd5vN/v1FozTVN6nkchxFuI6hsAAIMg4OPxMJCXdtTbR7JJCMEgCJhlGUlyPB4XfumozInrupxMJpRSRtZlKoNYl+m/6/wDuWAjtPfsQuwAAAAASUVORK5CYII=)](https://greasyfork.org/scripts/456055-chatgpt-exporter)
A GreasyFork script to export the chat history of [ChatGPT](https://chatgpt.com/).
Supported Formats:
- Text
- Html
- JSON (TODO)
- GIF (TODO)
# Prerequisites
Install **Tampermonkey** **[Chrome](https://www.tampermonkey.net/)** / **[Firefox](https://addons.mozilla.org/firefox/addon/tampermonkey/)** / **[Edge](https://microsoftedge.microsoft.com/addons/detail/tampermonkey/iikmkjmpaadaobahmlepeloendndfphd?hl=zh-CN)**
# UserScript
| Greasyfork | GitHub |
| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| [Insatall](https://greasyfork.org/scripts/456055-chatgpt-exporter) | [Insatall](https://raw.githubusercontent.com/pionxzh/chatgpt-exporter/main/dist/chatgpt.user.js) |

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

37
package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "@pionxzh/chatgpt-exporter",
"type": "module",
"version": "1.0.0",
"private": true,
"packageManager": "pnpm@7.12.2",
"author": "pionxzh",
"license": "MIT",
"engines": {
"node": ">=16.0.0"
},
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"test": "turbo run test",
"lint": "turbo run lint",
"lint:fix": "turbo run lint:fix",
"clean": "rimraf ./dist",
"release:ci": "cpy packages/**/dist/*.user.js dist --flat",
"prepare": "husky install"
},
"devDependencies": {
"@pionxzh/eslint-config-ts": "^0.1.1",
"@types/node": "^18.11.10",
"cpy-cli": "^4.2.0",
"eslint": "^8.29.0",
"husky": "^8.0.2",
"lint-staged": "^13.1.0",
"rimraf": "^3.0.2",
"sass": "^1.56.1",
"turbo": "^1.6.3",
"typescript": "^4.9.3"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": "pnpm exec eslint"
}
}

View File

@@ -0,0 +1,25 @@
{
"name": "@pionxzh/chatgpt-userscript",
"type": "module",
"version": "1.0.0",
"private": true,
"title": "ChatGPT Exporter",
"description": "Easily export the whole ChatGPT conversation history for further analysis or sharing.",
"author": "pionxzh",
"license": "MIT",
"scripts": {
"dev": "vite",
"build": "vite build",
"test:type": "tsc --noEmit",
"test": "pnpm run test:type",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"dependencies": {
"sentinel-js": "^0.0.5",
"vite-plugin-monkey": "^2.7.3"
},
"devDependencies": {
"vite": "^3.1.4"
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,242 @@
import { chatGPTAvatar, chatGPTAvatarSVG, fileCode, iconCopy } from './icons'
import './style.scss'
import { copyToClipboard, downloadFile, getBase64FromImg, onloadSafe } from './utils'
import templateHtml from './template.html?raw'
appendLeftSidebarItem()
function appendLeftSidebarItem() {
onloadSafe(() => {
const firstItem = document.querySelector('[class^="Navigation__NavMenuItem"]')
if (!firstItem) return
const container = firstItem.parentElement
if (!container) return
const divider = document.createElement('div')
divider.className = 'Navigation__NavMenuDivider'
container.appendChild(divider)
const copyButton = <HTMLAnchorElement>firstItem.cloneNode(true)
copyButton.removeAttribute('href')
copyButton.innerHTML = `${iconCopy}Copy`
copyButton.addEventListener('click', () => copyToText())
container.appendChild(copyButton)
// const txtButton = <HTMLAnchorElement>firstItem.cloneNode(true)
// txtButton.removeAttribute('href')
// txtButton.innerHTML = `${fileLines}Export .txt`
// container.appendChild(txtButton)
const htmlButton = <HTMLAnchorElement>firstItem.cloneNode(true)
htmlButton.removeAttribute('href')
htmlButton.innerHTML = `${fileCode}Export to .html`
htmlButton.addEventListener('click', () => exportToHtml())
container.appendChild(htmlButton)
})
}
type ConversationLine = |
{ type: 'text'; text: string } |
{ type: 'image'; src: string } |
{ type: 'code'; code: string } |
{ type: 'code-block'; lang: string; code: string } |
{ type: 'link'; text: string; href: string }
interface ConversationItem {
author: {
name: string
avatar: string
}
lines: ConversationLine[][]
// text: string
}
function copyToText() {
const items = getConversation()
if (items.length === 0) {
// eslint-disable-next-line no-alert
alert('No conversation found. Please send a message first.')
return
}
const text = conversationToText(items)
copyToClipboard(text)
}
function exportToHtml() {
const items = getConversation()
if (items.length === 0) {
// eslint-disable-next-line no-alert
alert('No conversation found. Please send a message first.')
return
}
const lang = document.documentElement.lang ?? 'en'
const conversationHtml = items.map((item) => {
const { author: { name, avatar }, lines } = item
const avatarEl = name === 'ChatGPT'
? `${chatGPTAvatarSVG}`
: `<img src="${avatar}" alt="${name}" />`
const linesHtml = lines.map((line) => {
const lineHtml = line.map((item) => {
switch (item.type) {
case 'text':
return item.text
case 'image':
return `<img src="${item.src}" referrerpolicy="no-referrer" />`
case 'code':
return `<code>${item.code}</code>`
case 'code-block':
return `<pre><code class="language-${item.lang}">${item.code}</code></pre>`
case 'link':
return `<a href="${item.href}" target="_blank" rel="noopener noreferrer">${item.text}</a>`
default:
return ''
}
}).join('')
return lineHtml.startsWith('<pre>') ? lineHtml : `<p>${lineHtml}</p>`
}).join('')
return `
<div class="conversation-item">
<div class="author">
${avatarEl}
</div>
<div class="conversation-content">
${linesHtml}
</div>
</div>`
}).join('')
const html = templateHtml
.replace('{{time}}', new Date().toISOString())
.replace('{{lang}}', lang)
.replace('{{content}}', conversationHtml)
const fileName = `ChatGPT-${new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '')}.html`
downloadFile(fileName, 'text/html', html)
}
function getConversation(): ConversationItem[] {
const thread = document.querySelector('[class^="ThreadLayout__NodeWrapper"]')
if (!thread) return []
const items: ConversationItem[] = []
thread.querySelectorAll('[class^="ConversationItem__Message"]').forEach((item) => {
const avatarEl = item.querySelector<HTMLImageElement>('[class^="Avatar__Wrapper"] img:not([aria-hidden="true"])')
// actually we can get the name from the avatar's alt
// but let's keep it anonymous for privacy reasons
const name = avatarEl?.getAttribute('alt') ? 'You' : 'ChatGPT'
const avatar = avatarEl ? getBase64FromImg(avatarEl) : chatGPTAvatar
const textNode = <HTMLDivElement>item.children?.[1]?.firstChild?.firstChild
if (!textNode) return
const lines = parseTextNode(textNode)
items.push({ author: { name, avatar }, lines })
})
return items
}
function parseTextNode(textNode: HTMLDivElement): ConversationLine[][] {
const children = textNode.children
if (!children || children.length === 0) {
return [[{ type: 'text', text: textNode.textContent ?? '' }]]
}
const lines: ConversationLine[][] = []
for (let i = 0; i < children.length; i++) {
const child = children[i]
switch (child.tagName.toUpperCase()) {
case 'PRE': {
const codeEl = child.querySelector('code')
if (codeEl) {
const code = codeEl.textContent ?? ''
const classList = Array.from(codeEl.classList)
const lang = classList.find(c => c.startsWith('language-'))?.replace('language-', '') ?? ''
lines.push([{ type: 'code-block', lang, code }])
}
break
}
case 'P':
default: {
// const html = child.innerHTML
// const text = html
// .replace(/<br>/g, '\r\n')
// .replace(/<code>/g, '`')
// .replace(/<\/code>/g, '`')
// // image to [image]
// .replace(/<img[^>]+>/g, '[image]')
// // link to href
// .replace(/<a[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/g, '[$2]($1)')
const line: ConversationLine[] = []
const nodes = Array.from(child.childNodes)
if (nodes.length === 0) {
const text = child.textContent ?? ''
line.push({ type: 'text', text })
}
else {
nodes.forEach((item) => {
switch (item.nodeType) {
// code
case 1: {
// detect is it a link
if ('href' in item) {
const href = (<HTMLAnchorElement>item).getAttribute('href') ?? ''
const text = item.textContent ?? href
line.push({ type: 'link', text, href })
}
// detect is it an image
else if ((<HTMLImageElement>item).tagName?.toUpperCase() === 'IMG') {
const src = (<HTMLImageElement>item).getAttribute('src') ?? ''
line.push({ type: 'image', src })
}
else {
const text = item.textContent ?? ''
line.push({ type: 'code', code: text })
}
break
}
// text
case 3:
default: {
const text = item.textContent ?? ''
line.push({ type: 'text', text })
break
}
}
})
}
lines.push(line)
break
}
}
}
return lines
}
function conversationToText(conversation: ConversationItem[]) {
return conversation.map((item) => {
const { author: { name }, lines } = item
const text = lines.map((line) => {
return line.map((item) => {
switch (item.type) {
case 'text': return item.text
case 'image': return '[image]'
case 'link': return `[${item.text}](${item.href})`
case 'code': return `\`${item.code}\``
case 'code-block': return `\`\`\`${item.lang}\r\n${item.code}\`\`\``
default: return ''
}
}).join('')
}).join('\r\n\r\n')
return `${name}:\r\n${text}`
}).join('\r\n\r\n')
}

View File

@@ -0,0 +1,5 @@
declare module 'sentinel-js' {
export function on<T = HTMLElement>(cssSelectors: string | string[], callback: (el: T) => void): void
export function off<T = HTMLElement>(event: string | string[], callback: (el: T) => void): void
export function reset(): void
}

View File

@@ -0,0 +1,5 @@
.Navigation__NavMenuDivider {
border-bottom: 1px solid #e1e4e8;
margin: 0 0 16px;
padding: 0;
}

View File

@@ -0,0 +1,188 @@
<!DOCTYPE html>
<html lang="{{lang}}" data-theme="light">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="https://chat.openai.com/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ChatGPT Conversation</title>
<style>
:root {
--tw-prose-code: #111827;
--page-bg: #fff;
--page-text: #111827;
--conversation-odd-bg: rgba(247,247,248);
}
[data-theme="dark"] {
--tw-prose-code: #f9fafb;
--page-bg: rgba(52,53,65);
--page-text: #fff;
--conversation-odd-bg: rgb(68,70,84);
}
body {
font-family: sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--page-text);
background-color: var(--page-bg);
margin: 0;
padding: 0;
}
[data-theme="light"] .sun {
display: none;
}
[data-theme="dark"] .moon {
display: none;
}
.toggle {
display: inline-flex;
justify-content: center;
align-items: center;
width: 32px;
height: 32px;
border-radius: 4px;
background-color: #fff;
border: 1px solid #e2e8f0;
}
p:first-child {
margin-top: 0;
}
p>code {
color: var(--tw-prose-code);
font-weight: 600;
font-size: .875em;
}
p>code::before,
p>code::after {
content: "`";
}
pre {
color: #ffffff;
background-color: #000000;
overflow-x: auto;
margin: 0;
padding: 8px 16px;
border-radius: 0.375rem;
}
pre>code {
font-family: Söhne Mono, Monaco, Andale Mono, Ubuntu Mono, monospace !important;
font-weight: 400;
font-size: .875em;
line-height: 1.7142857;
}
.conversation {
margin: 0 auto;
max-width: 800px;
padding: 1rem;
}
.conversation-header {
margin-bottom: 1rem;
}
.conversation-header h1 {
margin: 0;
font-size: 1.5rem;
}
.conversation-header .conversation-time {
margin-top: 0.5rem;
font-size: 0.8rem;
}
.conversation-header .conversation-time-label {
font-weight: bold;
}
.conversation-header .conversation-time-value {
margin-left: 0.5rem;
}
.conversation-header p {
margin-top: 0.5rem;
font-size: 0.8rem;
}
.conversation-item {
display: flex;
padding: 1rem 0.5rem;
border-bottom: 1px solid rgba(0,0,0,.1)
}
.conversation-item:nth-child(odd) {
background-color: var(--conversation-odd-bg);
}
.author {
display: flex;
justify-content: center;
align-items: center;
width: 30px;
height: 30px;
border-radius: 0.125rem;
margin-right: 1rem;
background-color: rgb(16, 163, 127);
}
.author svg {
color: #fff;
}
.author img {
width: 100%;
height: 100%;
}
.conversation-content {
display: flex;
flex-direction: column;
width: 100%;
font-size: 1rem;
line-height: 1.5;
}
</style>
</head>
<body>
<div class="conversation">
<div class="conversation-header">
<h1>ChatGPT Conversation
<button class="toggle">
<svg class="sun" stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
<svg class="moon" stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
</button>
</h1>
<div class="conversation-time">
<span class="conversation-time-label">Time:</span>
<span class="conversation-time-value">{{time}}</span>
</div>
<p>Generated by <a href="https://github.com/pionxzh/chatgpt-exporter">ChatGPT Exporter</a></p>
</div>
{{content}}
</div>
<script>
function toggleDarkMode() {
const html = document.querySelector('html')
const isDarkMode = html.getAttribute('data-theme') === 'dark'
html.setAttribute('data-theme', isDarkMode ? 'light' : 'dark')
}
document.querySelector('.toggle').addEventListener('click', toggleDarkMode)
</script>
</body>
</html>

View File

@@ -0,0 +1,44 @@
export function onloadSafe(fn: () => void) {
if (document.readyState === 'complete') {
fn()
}
else {
window.addEventListener('load', fn)
}
}
export function copyToClipboard(text: string) {
try {
// modern browsers
navigator.clipboard.writeText(text)
}
catch {
const textarea = document.createElement('textarea')
textarea.value = text
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
}
}
export function downloadFile(filename: string, type: string, content: string) {
const blob = new Blob([content], { type })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}
export function getBase64FromImg(el: HTMLImageElement) {
const canvas = document.createElement('canvas')
canvas.width = el.naturalWidth
canvas.height = el.naturalHeight
const ctx = canvas.getContext('2d')
if (!ctx) return ''
ctx.drawImage(el, 0, 0)
return canvas.toDataURL('image/png')
}

View File

@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"types": ["vite/client"]
},
"include": [
"src",
"package.json",
"vite.config.ts"
]
}

View File

@@ -0,0 +1,33 @@
import { defineConfig } from 'vite'
import monkey from 'vite-plugin-monkey'
import packageJson from './package.json'
// https://vitejs.dev/config/
export default defineConfig({
// https://github.com/lisonge/vite-plugin-monkey/issues/10#issuecomment-1207264978
esbuild: {
charset: 'utf8',
},
plugins: [
monkey({
entry: 'src/main.ts',
userscript: {
'name': packageJson.title,
'author': packageJson.author,
'namespace': packageJson.author,
'description': packageJson.description,
'license': packageJson.license,
'match': 'https://chat.openai.com/chat',
'icon': 'https://chat.openai.com/favicon.ico',
'run-at': 'document-start',
},
build: {
fileName: 'chatgpt.user.js',
minifyCss: false,
},
server: {
open: true,
},
}),
],
})

3149
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,2 @@
packages:
- 'packages/*'

View File

@@ -0,0 +1,9 @@
{
"pull-request-header": ":robot: I have created a release",
"pull-request-title-pattern": "chore: release${component} ${version}",
"packages": {
"packages/exporter": {
"component": "exporter"
}
}
}

26
tsconfig.base.json Normal file
View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"outDir": "dist",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"composite": true,
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"skipLibCheck": true,
"useDefineForClassFields": true
},
"exclude": [
"node_modules",
"**/dist",
"**/.turbo"
]
}

23
turbo.json Normal file
View File

@@ -0,0 +1,23 @@
{
"$schema": "https://turborepo.org/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"dev": {
"cache": false
},
"test": {
"dependsOn": ["build"],
"outputs": [],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts", "test/**/*.tsx"]
},
"lint": {
"outputs": []
},
"lint:fix": {
"outputs": []
}
}
}