diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..473e05e --- /dev/null +++ b/nodemon.json @@ -0,0 +1,13 @@ +{ + "env": { + "__DEV__": "true" + }, + "watch": [ + "src", "utils", "vite.config.ts" + ], + "ext": "tsx,css,html,ts", + "ignore": [ + "src/**/*.spec.ts" + ], + "exec": "vite build" +} diff --git a/package.json b/package.json new file mode 100755 index 0000000..04aa999 --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "vite-web-extension", + "displayName": "Web Extension Boilerplate", + "version": "1.1.0", + "description": "A simple chrome extension template with Vite, React, TypeScript and Tailwind CSS.", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/JohnBra/web-extension.git" + }, + "scripts": { + "build": "vite build", + "dev": "nodemon" + }, + "type": "module", + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "webextension-polyfill": "^0.10.0" + }, + "devDependencies": { + "@types/node": "^18.11.18", + "@types/react": "^18.0.27", + "@types/react-dom": "^18.0.10", + "@types/webextension-polyfill": "^0.10.0", + "@typescript-eslint/eslint-plugin": "^5.49.0", + "@typescript-eslint/parser": "^5.49.0", + "@vitejs/plugin-react-swc": "^3.0.1", + "autoprefixer": "^10.4.13", + "eslint": "^8.32.0", + "eslint-config-prettier": "^8.6.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.32.1", + "eslint-plugin-react-hooks": "^4.3.0", + "fs-extra": "^11.1.0", + "nodemon": "^2.0.20", + "postcss": "^8.4.21", + "tailwindcss": "^3.2.4", + "ts-node": "^10.9.1", + "typescript": "^4.9.4", + "vite": "^4.0.4" + } +} diff --git a/postcss.config.cjs b/postcss.config.cjs new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/icon-128.png b/public/icon-128.png new file mode 100644 index 0000000..15bd934 Binary files /dev/null and b/public/icon-128.png differ diff --git a/public/icon-34.png b/public/icon-34.png new file mode 100644 index 0000000..ff513cb Binary files /dev/null and b/public/icon-34.png differ diff --git a/src/assets/img/logo.svg b/src/assets/img/logo.svg new file mode 100644 index 0000000..6b60c10 --- /dev/null +++ b/src/assets/img/logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/styles/tailwind.css b/src/assets/styles/tailwind.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/src/assets/styles/tailwind.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 0000000..55db9b8 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,21 @@ +declare module '*.svg' { + import React = require('react'); + export const ReactComponent: React.SFC>; + const src: string; + export default src; +} + +declare module '*.jpg' { + const content: string; + export default content; +} + +declare module '*.png' { + const content: string; + export default content; +} + +declare module '*.json' { + const content: string; + export default content; +} diff --git a/src/manifest.ts b/src/manifest.ts new file mode 100755 index 0000000..39fe016 --- /dev/null +++ b/src/manifest.ts @@ -0,0 +1,42 @@ +import type { Manifest } from 'webextension-polyfill'; +import pkg from '../package.json'; + +const manifest: Manifest.WebExtensionManifest = { + manifest_version: 3, + name: pkg.displayName, + version: pkg.version, + description: pkg.description, + options_ui: { + page: 'src/pages/options/index.html', + }, + background: { + service_worker: 'src/pages/background/index.js', + type: 'module', + }, + action: { + default_popup: 'src/pages/popup/index.html', + default_icon: 'icon-34.png', + }, + chrome_url_overrides: { + newtab: 'src/pages/newtab/index.html', + }, + icons: { + '128': 'icon-128.png', + }, + content_scripts: [ + { + matches: ['http://*/*', 'https://*/*', ''], + js: ['src/pages/content/index.js'], + css: ['contentStyle.css'], + }, + ], + devtools_page: 'src/pages/devtools/index.html', + web_accessible_resources: [ + { + resources: ['contentStyle.css', 'icon-128.png', 'icon-34.png'], + matches: [], + }, + ], +}; + +export default manifest; diff --git a/src/pages/background/index.ts b/src/pages/background/index.ts new file mode 100644 index 0000000..def0db5 --- /dev/null +++ b/src/pages/background/index.ts @@ -0,0 +1 @@ +console.log('background script loaded'); diff --git a/src/pages/content/index.ts b/src/pages/content/index.ts new file mode 100644 index 0000000..55c69a9 --- /dev/null +++ b/src/pages/content/index.ts @@ -0,0 +1,5 @@ +try { + console.log('content script loaded'); +} catch (e) { + console.error(e); +} diff --git a/src/pages/content/style.css b/src/pages/content/style.css new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/devtools/index.html b/src/pages/devtools/index.html new file mode 100644 index 0000000..e17e5b9 --- /dev/null +++ b/src/pages/devtools/index.html @@ -0,0 +1,10 @@ + + + + + Devtools + + + + + diff --git a/src/pages/devtools/index.ts b/src/pages/devtools/index.ts new file mode 100644 index 0000000..f280e52 --- /dev/null +++ b/src/pages/devtools/index.ts @@ -0,0 +1,7 @@ +import Browser from 'webextension-polyfill'; + +Browser + .devtools + .panels + .create('Dev Tools', 'icon-34.png', 'src/pages/panel/index.html') + .catch(console.error); diff --git a/src/pages/newtab/Newtab.css b/src/pages/newtab/Newtab.css new file mode 100644 index 0000000..74b5e05 --- /dev/null +++ b/src/pages/newtab/Newtab.css @@ -0,0 +1,38 @@ +.App { + text-align: center; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #61dafb; +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/src/pages/newtab/Newtab.tsx b/src/pages/newtab/Newtab.tsx new file mode 100644 index 0000000..38ce5dc --- /dev/null +++ b/src/pages/newtab/Newtab.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import logo from '@assets/img/logo.svg'; +import '@pages/newtab/Newtab.css'; + +export default function Newtab(): JSX.Element { + return ( +
+
+ logo +

+ Edit src/pages/newtab/Newtab.tsx and save to reload. +

+ + Learn React! + +
+
+ ); +} diff --git a/src/pages/newtab/index.css b/src/pages/newtab/index.css new file mode 100644 index 0000000..ec2585e --- /dev/null +++ b/src/pages/newtab/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/src/pages/newtab/index.html b/src/pages/newtab/index.html new file mode 100644 index 0000000..7e455b9 --- /dev/null +++ b/src/pages/newtab/index.html @@ -0,0 +1,12 @@ + + + + + New tab + + + +
+ + + diff --git a/src/pages/newtab/index.tsx b/src/pages/newtab/index.tsx new file mode 100644 index 0000000..6923d34 --- /dev/null +++ b/src/pages/newtab/index.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import Newtab from '@pages/newtab/Newtab'; +import '@pages/newtab/index.css'; + +function init() { + const rootContainer = document.querySelector("#__root"); + if (!rootContainer) throw new Error("Can't find Newtab root element"); + const root = createRoot(rootContainer); + root.render(); +} + +init(); diff --git a/src/pages/options/Options.css b/src/pages/options/Options.css new file mode 100644 index 0000000..1ea51cb --- /dev/null +++ b/src/pages/options/Options.css @@ -0,0 +1,8 @@ +.container { + width: 100%; + height: 50vh; + font-size: 2rem; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/src/pages/options/Options.tsx b/src/pages/options/Options.tsx new file mode 100644 index 0000000..b6c79c8 --- /dev/null +++ b/src/pages/options/Options.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import '@pages/options/Options.css'; + +export default function Options(): JSX.Element { + return
Options
; +} diff --git a/src/pages/options/index.css b/src/pages/options/index.css new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/options/index.html b/src/pages/options/index.html new file mode 100644 index 0000000..fe96b7f --- /dev/null +++ b/src/pages/options/index.html @@ -0,0 +1,12 @@ + + + + + Options + + + +
+ + + diff --git a/src/pages/options/index.tsx b/src/pages/options/index.tsx new file mode 100644 index 0000000..cac9888 --- /dev/null +++ b/src/pages/options/index.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import Options from '@pages/options/Options'; +import '@pages/options/index.css'; + +function init() { + const rootContainer = document.querySelector("#__root"); + if (!rootContainer) throw new Error("Can't find Options root element"); + const root = createRoot(rootContainer); + root.render(); +} + +init(); diff --git a/src/pages/panel/Panel.css b/src/pages/panel/Panel.css new file mode 100644 index 0000000..843f23e --- /dev/null +++ b/src/pages/panel/Panel.css @@ -0,0 +1,7 @@ +body { + background-color: #242424; +} + +.container { + color: #ffffff; +} \ No newline at end of file diff --git a/src/pages/panel/Panel.tsx b/src/pages/panel/Panel.tsx new file mode 100644 index 0000000..44eb2ed --- /dev/null +++ b/src/pages/panel/Panel.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import '@pages/panel/Panel.css'; + +export default function Panel(): JSX.Element { + return ( +
+

Dev Tools Panel

+
+ ); +} diff --git a/src/pages/panel/index.css b/src/pages/panel/index.css new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/panel/index.html b/src/pages/panel/index.html new file mode 100644 index 0000000..564b65b --- /dev/null +++ b/src/pages/panel/index.html @@ -0,0 +1,12 @@ + + + + + Devtools Panel + + + +
+ + + diff --git a/src/pages/panel/index.tsx b/src/pages/panel/index.tsx new file mode 100644 index 0000000..84fa209 --- /dev/null +++ b/src/pages/panel/index.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import Panel from '@pages/panel/Panel'; +import '@pages/panel/index.css'; + +function init() { + const rootContainer = document.querySelector("#__root"); + if (!rootContainer) throw new Error("Can't find Panel root element"); + const root = createRoot(rootContainer); + root.render(); +} + +init(); diff --git a/src/pages/popup/Popup.tsx b/src/pages/popup/Popup.tsx new file mode 100644 index 0000000..94555c7 --- /dev/null +++ b/src/pages/popup/Popup.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import logo from '@assets/img/logo.svg'; + +export default function Popup(): JSX.Element { + return ( +
+
+ logo +

+ Edit src/pages/popup/Popup.jsx and save to reload. +

+ + Learn React! + +

Popup styled with TailwindCSS!

+
+
+ ); +} diff --git a/src/pages/popup/index.css b/src/pages/popup/index.css new file mode 100644 index 0000000..906301e --- /dev/null +++ b/src/pages/popup/index.css @@ -0,0 +1,12 @@ +body { + width: 300px; + height: 260px; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + position: relative; +} diff --git a/src/pages/popup/index.html b/src/pages/popup/index.html new file mode 100644 index 0000000..7b8317a --- /dev/null +++ b/src/pages/popup/index.html @@ -0,0 +1,12 @@ + + + + + Popup + + + +
+ + + diff --git a/src/pages/popup/index.tsx b/src/pages/popup/index.tsx new file mode 100644 index 0000000..c62dee0 --- /dev/null +++ b/src/pages/popup/index.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import '@pages/popup/index.css'; +import '@assets/styles/tailwind.css'; +import Popup from '@pages/popup/Popup'; + +function init() { + const rootContainer = document.querySelector("#__root"); + if (!rootContainer) throw new Error("Can't find Popup root element"); + const root = createRoot(rootContainer); + root.render(); +} + +init(); diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tailwind.config.cjs b/tailwind.config.cjs new file mode 100644 index 0000000..8b13b45 --- /dev/null +++ b/tailwind.config.cjs @@ -0,0 +1,11 @@ +module.exports = { + content: ["./src/**/*.{js,jsx,ts,tsx}"], + theme: { + extend: { + animation: { + 'spin-slow': 'spin 20s linear infinite', + } + }, + }, + plugins: [], +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c30993f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "esnext", + "types": ["vite/client", "node"], + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "noEmit": true, + "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "@src/*": ["src/*"], + "@assets/*": ["src/assets/*"], + "@pages/*": ["src/pages/*"] + } + }, + "include": ["src", + "utils", "vite.config.ts"], +} diff --git a/utils/log.ts b/utils/log.ts new file mode 100644 index 0000000..6f79d20 --- /dev/null +++ b/utils/log.ts @@ -0,0 +1,48 @@ +type ColorType = 'success' | 'info' | 'error' | 'warning' | keyof typeof COLORS; + +export default function colorLog(message: string, type?: ColorType) { + let color: string = type || COLORS.FgBlack; + + switch (type) { + case 'success': + color = COLORS.FgGreen; + break; + case 'info': + color = COLORS.FgBlue; + break; + case 'error': + color = COLORS.FgRed; + break; + case 'warning': + color = COLORS.FgYellow; + break; + } + + console.log(color, message); +} + +const COLORS = { + Reset: '\x1b[0m', + Bright: '\x1b[1m', + Dim: '\x1b[2m', + Underscore: '\x1b[4m', + Blink: '\x1b[5m', + Reverse: '\x1b[7m', + Hidden: '\x1b[8m', + FgBlack: '\x1b[30m', + FgRed: '\x1b[31m', + FgGreen: '\x1b[32m', + FgYellow: '\x1b[33m', + FgBlue: '\x1b[34m', + FgMagenta: '\x1b[35m', + FgCyan: '\x1b[36m', + FgWhite: '\x1b[37m', + BgBlack: '\x1b[40m', + BgRed: '\x1b[41m', + BgGreen: '\x1b[42m', + BgYellow: '\x1b[43m', + BgBlue: '\x1b[44m', + BgMagenta: '\x1b[45m', + BgCyan: '\x1b[46m', + BgWhite: '\x1b[47m', +} as const; diff --git a/utils/plugins/copy-content-style.ts b/utils/plugins/copy-content-style.ts new file mode 100644 index 0000000..c898441 --- /dev/null +++ b/utils/plugins/copy-content-style.ts @@ -0,0 +1,21 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import colorLog from '../log'; +import { PluginOption } from 'vite'; + +const { resolve } = path; + +const root = resolve(__dirname, '..', '..'); +const contentStyle = resolve(root, 'src', 'pages', 'content', 'style.css'); +const outDir = resolve(__dirname, '..', '..', 'public'); + +export default function copyContentStyle(): PluginOption { + return { + name: 'make-manifest', + buildEnd() { + fs.copyFileSync(contentStyle, resolve(outDir, 'contentStyle.css')); + + colorLog('contentStyle copied', 'success'); + }, + }; +} diff --git a/utils/plugins/make-manifest.ts b/utils/plugins/make-manifest.ts new file mode 100644 index 0000000..ff0619a --- /dev/null +++ b/utils/plugins/make-manifest.ts @@ -0,0 +1,26 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import colorLog from '../log'; +import manifest from '../../src/manifest'; +import { PluginOption } from 'vite'; + +const { resolve } = path; + +const outDir = resolve(__dirname, '..', '..', 'public'); + +export default function makeManifest(): PluginOption { + return { + name: 'make-manifest', + buildEnd() { + if (!fs.existsSync(outDir)) { + fs.mkdirSync(outDir); + } + + const manifestPath = resolve(outDir, 'manifest.json'); + + fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); + + colorLog(`Manifest file copy complete: ${manifestPath}`, 'success'); + }, + }; +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..2784ac7 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,41 @@ +import react from '@vitejs/plugin-react-swc'; +import { resolve } from 'path'; +import { defineConfig } from 'vite'; +import copyContentStyle from './utils/plugins/copy-content-style'; +import makeManifest from './utils/plugins/make-manifest'; + +const root = resolve(__dirname, 'src'); +const pagesDir = resolve(root, 'pages'); +const assetsDir = resolve(root, 'assets'); +const outDir = resolve(__dirname, 'dist'); +const publicDir = resolve(__dirname, 'public'); + +export default defineConfig({ + resolve: { + alias: { + '@src': root, + '@assets': assetsDir, + '@pages': pagesDir, + }, + }, + plugins: [react(), makeManifest(), copyContentStyle()], + publicDir, + build: { + outDir, + sourcemap: process.env.__DEV__ === 'true', + rollupOptions: { + input: { + devtools: resolve(pagesDir, 'devtools', 'index.html'), + panel: resolve(pagesDir, 'panel', 'index.html'), + content: resolve(pagesDir, 'content', 'index.ts'), + background: resolve(pagesDir, 'background', 'index.ts'), + popup: resolve(pagesDir, 'popup', 'index.html'), + newtab: resolve(pagesDir, 'newtab', 'index.html'), + options: resolve(pagesDir, 'options', 'index.html'), + }, + output: { + entryFileNames: (chunk) => `src/pages/${chunk.name}/index.js`, + }, + }, + }, +});