Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
36 changes: 36 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Checks

on:
pull_request:
push:
branches: [main]

jobs:
lint-format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci || npm install
- run: npm run lint
- run: npm run format
Comment on lines +17 to +19

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Format files before enforcing Prettier

I checked the new workflow by running prettier --check ., and the current tree reports 15 unformatted files including .github/dependabot.yml, .github/workflows/codeql.yml, background.js, content_script.js, and options.js. Since this PR gates every PR/main push on npm run format, the Checks workflow is red until those existing files are formatted or deliberately ignored.

Useful? React with 👍 / 👎.


build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install --no-save
- name: Build extension zip
run: npm run package
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: tweai-extension
path: tweai-v*.zip
retention-days: 14
35 changes: 35 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Release

on:
push:
tags:
- 'v*'

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install --no-save
- name: Build extension zip
run: npm run package
- name: Capture zip path
id: zip
run: |
ZIP=$(ls tweai-v*.zip | head -1)
echo "path=$ZIP" >> "$GITHUB_OUTPUT"
echo "name=$(basename "$ZIP")" >> "$GITHUB_OUTPUT"
- name: Create GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${GITHUB_REF#refs/tags/}"
gh release create "$TAG" "${{ steps.zip.outputs.path }}" \
--title "$TAG" \
--generate-notes
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
node_modules/
env.json

# Build artifacts
dist/
*.zip

# Tooling
.eslintcache
8 changes: 8 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules
dist
*.zip
docs
tweai-mcp-server
_locales
LICENSE
.git
9 changes: 9 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2,
"arrowParens": "avoid",
"endOfLine": "lf"
}
94 changes: 94 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Flat config for ESLint 9.x. WebExtensions globals + browser env.
// Намеренно мягкие правила: цель — поймать опечатки и явные баги, не переписать
// весь codebase. Style-issues идут через prettier.

export default [
{
ignores: [
'node_modules/**',
'dist/**',
'*.zip',
'tweai-mcp-server/**',
'docs/**',
'_locales/**',
],
},
{
files: ['**/*.js', '**/*.mjs'],
languageOptions: {
ecmaVersion: 2022,
sourceType: 'script',
Comment on lines +17 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Exclude ESLint config from script-mode linting

When npm run lint runs eslint ., this glob also matches eslint.config.js, but the matched config forces every .js file to be parsed as sourceType: 'script'. Because this file itself contains export default, the lint job fails immediately with Parsing error: 'import' and 'export' may appear only with 'sourceType: module'; exclude eslint.config.js or add a module override for it.

Useful? React with 👍 / 👎.

globals: {
// WebExtensions
chrome: 'readonly',
browser: 'readonly',
// Browser
window: 'readonly',
document: 'readonly',
navigation: 'readonly',
location: 'readonly',
localStorage: 'readonly',
history: 'readonly',
console: 'readonly',
fetch: 'readonly',
URL: 'readonly',
URLSearchParams: 'readonly',
setTimeout: 'readonly',
clearTimeout: 'readonly',
setInterval: 'readonly',
clearInterval: 'readonly',
Promise: 'readonly',
Map: 'readonly',
Set: 'readonly',
Boolean: 'readonly',
Number: 'readonly',
String: 'readonly',
Object: 'readonly',
Array: 'readonly',
Math: 'readonly',
Date: 'readonly',
JSON: 'readonly',
RegExp: 'readonly',
Error: 'readonly',
AbortController: 'readonly',
AbortSignal: 'readonly',
MutationObserver: 'readonly',
IntersectionObserver: 'readonly',
InputEvent: 'readonly',
Event: 'readonly',
CustomEvent: 'readonly',
HTMLElement: 'readonly',
Comment on lines +58 to +60

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add the browser globals used by current scripts

After the config-file parsing issue is fixed, this browser globals list still makes no-undef fail on existing runtime code: options.js uses alert and profile-scraper.js uses HTMLImageElement, but neither global is declared while no-undef is an error. Add the missing globals, or import the standard browser globals set, so the newly required lint job can pass for the current extension code.

Useful? React with 👍 / 👎.

Element: 'readonly',
Node: 'readonly',
getComputedStyle: 'readonly',
requestAnimationFrame: 'readonly',
addEventListener: 'readonly',
// own globals (shared between content scripts in same isolated world)
TTASelectors: 'writable',
TTALogger: 'writable',
},
},
rules: {
'no-undef': 'error',
'no-unused-vars': ['warn', { args: 'none', varsIgnorePattern: '^_' }],
'no-constant-condition': ['error', { checkLoops: false }],
'no-empty': ['error', { allowEmptyCatch: true }],
'no-prototype-builtins': 'off',
'no-cond-assign': ['error', 'except-parens'],
'no-control-regex': 'off',
'no-useless-escape': 'warn',
},
},
{
// Build/tools — Node environment
files: ['tools/**/*.mjs', '*.mjs'],
languageOptions: {
sourceType: 'module',
globals: {
process: 'readonly',
console: 'readonly',
URL: 'readonly',
},
},
},
];
20 changes: 20 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "tweai",
"version": "1.8.1",
"private": true,
"description": "TweAI — AI Reply Assistant for X / Twitter. Chrome extension.",
"license": "Apache-2.0",
"type": "module",
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --check .",
"format:fix": "prettier --write .",
"package": "node tools/build.mjs",
"check": "npm run lint && npm run format"
},
"devDependencies": {
"eslint": "^9.0.0",
"prettier": "^3.0.0"
}
}
94 changes: 94 additions & 0 deletions tools/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/usr/bin/env node
// Сборка артефакта расширения для Chrome Web Store.
//
// Текущее состояние: vanilla content scripts без bundler — просто копируем
// рантайм-файлы в dist/, исключая node-репку MCP-сервера, docs, dev-файлы.
// Когда (если) перейдём на ESM-модули в options/ — этот же скрипт будет
// запускать esbuild перед копированием.
//
// Использование:
// node tools/build.mjs # → dist/ + tweai-v<version>.zip
// node tools/build.mjs --no-zip # только dist/

import { readFile, rm, mkdir, cp, readdir, stat } from 'node:fs/promises';
import { spawn } from 'node:child_process';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
const DIST = join(ROOT, 'dist');

const INCLUDE = [
'manifest.json',
'background.js',
'content_script.js',
'selectors.js',
'dev-logger.js',
'ad-blocker.js',
'profile-scraper.js',
'options.html',
'options.js',
'options.css',
'styles.css',
'icons',
'_locales',
'LICENSE',
];

async function readVersion() {
const m = JSON.parse(await readFile(join(ROOT, 'manifest.json'), 'utf8'));
return m.version;
}

async function copyTree(src, dst) {
await cp(src, dst, { recursive: true });
}

async function zipDir(srcDir, outZip) {
// Используем системный `zip` — он есть на macOS/Linux и GitHub Actions
// runner. Без extra deps: добавлять JS-zip-библиотеку ради одной операции
// не стоит.
await new Promise((resolve, reject) => {
const proc = spawn('zip', ['-r', outZip, '.'], { cwd: srcDir, stdio: 'inherit' });
proc.on('exit', code => (code === 0 ? resolve() : reject(new Error(`zip exit ${code}`))));
proc.on('error', reject);
});
}

async function main() {
const version = await readVersion();
const wantZip = !process.argv.includes('--no-zip');

await rm(DIST, { recursive: true, force: true });
await mkdir(DIST, { recursive: true });

for (const name of INCLUDE) {
const src = join(ROOT, name);
const dst = join(DIST, name);
try {
await stat(src);
} catch {
console.warn(`[build] skip missing: ${name}`);
continue;
}
await copyTree(src, dst);
}

console.log(`[build] dist/ ready — version ${version}`);
const entries = await readdir(DIST);
console.log(`[build] ${entries.length} top-level entries`);

if (wantZip) {
const zipPath = join(ROOT, `tweai-v${version}.zip`);
await rm(zipPath, { force: true });
await zipDir(DIST, zipPath);
const s = await stat(zipPath);
console.log(`[build] wrote ${zipPath} (${(s.size / 1024).toFixed(1)} KB)`);
}
}

main().catch(e => {
console.error('[build] failed:', e);
process.exit(1);
});
Loading