create-extension-react 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +135 -0
  3. package/package.json +54 -0
  4. package/scripts/create.js +418 -0
  5. package/scripts/dev.js +26 -0
  6. package/src/background.ts +9 -0
  7. package/src/bookmarks/index.module.css +17 -0
  8. package/src/bookmarks/index.tsx +42 -0
  9. package/src/bookmarks.html +12 -0
  10. package/src/components/BookmarksContent/index.module.css +95 -0
  11. package/src/components/BookmarksContent/index.tsx +100 -0
  12. package/src/components/ConfigInfo/index.module.css +30 -0
  13. package/src/components/ConfigInfo/index.tsx +36 -0
  14. package/src/components/HistoryContent/index.module.css +99 -0
  15. package/src/components/HistoryContent/index.tsx +86 -0
  16. package/src/components/NewTabContent/index.module.css +86 -0
  17. package/src/components/NewTabContent/index.tsx +30 -0
  18. package/src/components/NewTabHeader/index.module.css +76 -0
  19. package/src/components/NewTabHeader/index.tsx +14 -0
  20. package/src/components/PopupContent/index.module.css +46 -0
  21. package/src/components/PopupContent/index.tsx +29 -0
  22. package/src/components/PopupHeader/index.module.css +15 -0
  23. package/src/components/PopupHeader/index.tsx +14 -0
  24. package/src/components/ThemeButton/index.module.css +202 -0
  25. package/src/components/ThemeButton/index.tsx +48 -0
  26. package/src/content.ts +10 -0
  27. package/src/history/index.module.css +17 -0
  28. package/src/history/index.tsx +42 -0
  29. package/src/history.html +12 -0
  30. package/src/hooks/useStorage.ts +50 -0
  31. package/src/hooks/useTabs.ts +37 -0
  32. package/src/hooks/useTheme.ts +27 -0
  33. package/src/manifest.json +26 -0
  34. package/src/newtab/index.module.css +17 -0
  35. package/src/newtab/index.tsx +43 -0
  36. package/src/newtab.html +12 -0
  37. package/src/popup/index.module.css +8 -0
  38. package/src/popup/index.tsx +36 -0
  39. package/src/popup.html +12 -0
  40. package/src/store/README.md +176 -0
  41. package/src/store/chromeStorage.ts +88 -0
  42. package/src/store/configSlice.ts +40 -0
  43. package/src/store/hooks.ts +6 -0
  44. package/src/store/initConfig.ts +78 -0
  45. package/src/store/storageMiddleware.ts +70 -0
  46. package/src/store/store.ts +49 -0
  47. package/src/style.css +107 -0
  48. package/src/utils/initTheme.ts +40 -0
  49. package/src/variables.css +146 -0
  50. package/src/vite-env.d.ts +6 -0
  51. package/tsconfig.json +22 -0
  52. package/tsconfig.node.json +10 -0
  53. package/vite-plugin-fix-extension.ts +81 -0
  54. package/vite.config.ts +50 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jiangcheng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # Chrome Extension React + Vite 脚手架
2
+
3
+ 一个用于快速开发 Chrome Extension 的脚手架,使用 React + Vite 构建,支持热重载。
4
+
5
+ ## 功能特性
6
+
7
+ - ⚡️ 纯 Vite + React,无额外插件依赖
8
+ - ⚛️ React 18 支持
9
+ - 🔥 开发模式支持热重载(文件变化自动重建)
10
+ - 📦 TypeScript 支持
11
+ - 🎨 现代化 UI 模板
12
+ - 🧭 支持 New Tab / History / Bookmarks 页面
13
+ - 🛠️ 完全可自定义的构建配置
14
+
15
+ ## 项目结构
16
+
17
+ ```
18
+ .
19
+ ├── src/
20
+ │ ├── manifest.json # Chrome Extension 配置文件
21
+ │ ├── popup.html # Popup 页面 HTML
22
+ │ ├── popup.tsx # Popup 页面 React 组件
23
+ │ ├── popup.css # Popup 页面样式
24
+ │ ├── background.ts # Service Worker 后台脚本
25
+ │ └── content.ts # Content Script 内容脚本
26
+ ├── scripts/
27
+ │ └── dev.js # 开发模式脚本(支持热重载)
28
+ ├── vite.config.ts # Vite 配置文件
29
+ ├── tsconfig.json # TypeScript 配置
30
+ └── package.json
31
+ ```
32
+
33
+ ## 快速开始
34
+
35
+ ### 使用脚手架创建项目
36
+
37
+ ```bash
38
+ npx create-extension-react my-extension
39
+ ```
40
+
41
+ 安装时会提示选择 `newtab` / `history` / `bookmarks` 三选一,并只生成对应模板。
42
+
43
+ 也可以通过参数直接指定类型:
44
+
45
+ ```bash
46
+ npx create-extension-react my-extension --type newtab
47
+ ```
48
+
49
+ ### 安装依赖
50
+
51
+ ```bash
52
+ npm install
53
+ ```
54
+
55
+ ### 开发模式(支持热重载)
56
+
57
+ ```bash
58
+ npm run dev
59
+ ```
60
+
61
+ 开发模式会:
62
+
63
+ 1. 自动监听文件变化并重新构建
64
+ 2. 自动复制 `manifest.json` 到 `dist` 目录
65
+ 3. 在 Chrome 中加载扩展后,修改代码会自动重建,刷新扩展即可看到更新
66
+
67
+ **在 Chrome 中加载扩展:**
68
+
69
+ 1. 打开 Chrome 浏览器
70
+ 2. 访问 `chrome://extensions/`
71
+ 3. 开启"开发者模式"
72
+ 4. 点击"加载已解压的扩展程序"
73
+ 5. 选择项目的 `dist` 目录
74
+ 6. 开发时,修改代码后点击扩展的刷新按钮即可看到更新
75
+
76
+ ### 构建生产版本
77
+
78
+ ```bash
79
+ npm run build
80
+ ```
81
+
82
+ 构建完成后,`dist` 目录就是可以打包的扩展文件。
83
+
84
+ **注意:**
85
+
86
+ - `npm run dev` - 开发模式,代码**不压缩**,构建速度快,便于调试
87
+ - `npm run build` - 生产模式,代码**会压缩**,文件体积小,适合发布
88
+
89
+ ## 开发说明
90
+
91
+ ### 热重载
92
+
93
+ 在开发模式下(`npm run dev`),修改代码后:
94
+
95
+ 1. Vite 会自动监听文件变化并重新构建到 `dist` 目录
96
+ 2. `manifest.json` 也会自动同步到 `dist` 目录
97
+ 3. 在 Chrome 扩展管理页面点击扩展的刷新按钮即可看到更新
98
+
99
+ ### 自定义配置
100
+
101
+ 这是一个纯 Vite + React 的配置,你可以完全自定义:
102
+
103
+ - **修改构建配置**:编辑 `vite.config.ts`
104
+ - **添加新页面**:在 `src` 目录创建文件,然后在 `vite.config.ts` 的 `rollupOptions.input` 中添加入口点
105
+ - **修改开发脚本**:编辑 `scripts/dev.js`
106
+
107
+ ### 页面覆盖
108
+
109
+ 项目内置了 3 个可用页面:
110
+
111
+ - `newtab`:新标签页
112
+ - `history`:历史记录页
113
+ - `bookmarks`:书签页
114
+
115
+ 对应入口文件:
116
+
117
+ - `src/newtab.html` → `src/newtab/index.tsx`
118
+ - `src/history.html` → `src/history/index.tsx`
119
+ - `src/bookmarks.html` → `src/bookmarks/index.tsx`
120
+
121
+ 如果只需要其中某一类页面,请在 `src/manifest.json` 的 `chrome_url_overrides` 中保留对应条目,并同步删除 `vite.config.ts` 的 `rollupOptions.input` 入口。
122
+
123
+ ### 使用 Chrome API
124
+
125
+ 项目已配置 TypeScript 类型支持,可以直接使用 Chrome API:
126
+
127
+ ```typescript
128
+ chrome.tabs.query({ active: true }, (tabs) => {
129
+ console.log(tabs);
130
+ });
131
+ ```
132
+
133
+ ## 许可证
134
+
135
+ MIT
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "create-extension-react",
3
+ "version": "0.0.1",
4
+ "author": "Jiangcheng <plutavian@gmail.com>",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/AsukaCC/create-extension-react",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/AsukaCC/create-extension-react.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/AsukaCC/create-extension-react/issues"
13
+ },
14
+ "type": "module",
15
+ "description": "Chrome Extension scaffold with React and Vite",
16
+ "keywords": [
17
+ "create-extension-react",
18
+ "chrome",
19
+ "chrome extension"
20
+ ],
21
+ "files": [
22
+ "scripts/",
23
+ "src/",
24
+ "vite-plugin-fix-extension.ts",
25
+ "vite.config.ts",
26
+ "tsconfig.json",
27
+ "tsconfig.node.json",
28
+ "README.md",
29
+ "LICENSE"
30
+ ],
31
+ "bin": {
32
+ "create-extension-react": "scripts/create.js"
33
+ },
34
+ "scripts": {
35
+ "dev": "node scripts/dev.js",
36
+ "build": "vite build"
37
+ },
38
+ "dependencies": {
39
+ "@reduxjs/toolkit": "^2.11.2",
40
+ "dayjs": "^1.11.19",
41
+ "react": "^18.2.0",
42
+ "react-dom": "^18.2.0",
43
+ "react-redux": "^9.2.0",
44
+ "redux-persist": "^6.0.0"
45
+ },
46
+ "devDependencies": {
47
+ "@types/chrome": "^0.0.268",
48
+ "@types/react": "^18.2.66",
49
+ "@types/react-dom": "^18.2.22",
50
+ "@vitejs/plugin-react": "^4.2.1",
51
+ "typescript": "^5.4.3",
52
+ "vite": "^5.2.0"
53
+ }
54
+ }
@@ -0,0 +1,418 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import readline from 'readline';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const templateRoot = path.resolve(__dirname, '..');
10
+
11
+ const TYPE_OPTIONS = ['newtab', 'history', 'bookmarks'];
12
+ const PAGE_META = {
13
+ newtab: {
14
+ label: 'newtab',
15
+ desc: '新标签页',
16
+ html: 'src/newtab.html',
17
+ entry: 'src/newtab/index.tsx',
18
+ permission: null,
19
+ },
20
+ history: {
21
+ label: 'history',
22
+ desc: '历史记录页',
23
+ html: 'src/history.html',
24
+ entry: 'src/history/index.tsx',
25
+ permission: 'history',
26
+ },
27
+ bookmarks: {
28
+ label: 'bookmarks',
29
+ desc: '书签页',
30
+ html: 'src/bookmarks.html',
31
+ entry: 'src/bookmarks/index.tsx',
32
+ permission: 'bookmarks',
33
+ },
34
+ };
35
+ const PRUNE_PATHS = {
36
+ newtab: ['src/newtab.html', 'src/newtab', 'src/components/NewTabContent'],
37
+ history: ['src/history.html', 'src/history', 'src/components/HistoryContent'],
38
+ bookmarks: [
39
+ 'src/bookmarks.html',
40
+ 'src/bookmarks',
41
+ 'src/components/BookmarksContent',
42
+ ],
43
+ };
44
+ const STYLE_PATTERNS = {
45
+ newtab: [
46
+ /\/\* NewTab 特定全局样式 \*\/[\s\S]*?}\r?\n\r?\n?/,
47
+ /\/\* 亮色主题背景渐变 \*\/[\s\S]*?body\.newtab[\s\S]*?}\r?\n\r?\n?/,
48
+ /\/\* 暗色主题背景渐变 \*\/[\s\S]*?body\.newtab[\s\S]*?}\r?\n\r?\n?/,
49
+ ],
50
+ history: [
51
+ /\/\* History 特定全局样式 \*\/[\s\S]*?}\r?\n\r?\n?/,
52
+ /\/\* 亮色主题背景渐变 \*\/[\s\S]*?body\.history[\s\S]*?}\r?\n\r?\n?/,
53
+ /\/\* 暗色主题背景渐变 \*\/[\s\S]*?body\.history[\s\S]*?}\r?\n\r?\n?/,
54
+ ],
55
+ bookmarks: [
56
+ /\/\* Bookmarks 特定全局样式 \*\/[\s\S]*?}\r?\n\r?\n?/,
57
+ /\/\* 亮色主题背景渐变 \*\/[\s\S]*?body\.bookmarks[\s\S]*?}\r?\n\r?\n?/,
58
+ /\/\* 暗色主题背景渐变 \*\/[\s\S]*?body\.bookmarks[\s\S]*?}\r?\n\r?\n?/,
59
+ ],
60
+ };
61
+
62
+ const getOptionValue = (args, keys) => {
63
+ const index = args.findIndex((arg) => keys.includes(arg));
64
+ if (index === -1) return null;
65
+ return args[index + 1] ?? null;
66
+ };
67
+
68
+ const promptType = async () => {
69
+ const rl = readline.createInterface({
70
+ input: process.stdin,
71
+ output: process.stdout,
72
+ });
73
+
74
+ const question = (text) =>
75
+ new Promise((resolve) => {
76
+ rl.question(text, resolve);
77
+ });
78
+
79
+ if (!process.stdin.isTTY) {
80
+ rl.close();
81
+ return 'newtab';
82
+ }
83
+
84
+ readline.emitKeypressEvents(process.stdin, rl);
85
+ process.stdin.setRawMode(true);
86
+
87
+ let selectedIndex = Math.max(TYPE_OPTIONS.indexOf('newtab'), 0);
88
+ const render = () => {
89
+ process.stdout.write('\x1b[2J');
90
+ process.stdout.write('\x1b[0f');
91
+ console.log('请选择要创建的扩展类型(↑↓ 选择,Enter 确认):');
92
+ TYPE_OPTIONS.forEach((option, index) => {
93
+ const prefix = index === selectedIndex ? '➤' : ' ';
94
+ console.log(`${prefix} ${option}`);
95
+ });
96
+ };
97
+
98
+ render();
99
+
100
+ const selectedType = await new Promise((resolve) => {
101
+ const cleanup = () => {
102
+ process.stdin.removeListener('keypress', onKeypress);
103
+ process.stdin.removeListener('close', onClose);
104
+ process.stdin.removeListener('end', onClose);
105
+ process.stdin.removeListener('error', onClose);
106
+ };
107
+
108
+ const onClose = () => {
109
+ cleanup();
110
+ resolve(null);
111
+ };
112
+
113
+ const onKeypress = (_, key) => {
114
+ if (!key) return;
115
+ if (key.name === 'up') {
116
+ selectedIndex =
117
+ (selectedIndex - 1 + TYPE_OPTIONS.length) % TYPE_OPTIONS.length;
118
+ render();
119
+ } else if (key.name === 'down') {
120
+ selectedIndex = (selectedIndex + 1) % TYPE_OPTIONS.length;
121
+ render();
122
+ } else if (key.name === 'return') {
123
+ cleanup();
124
+ resolve(TYPE_OPTIONS[selectedIndex]);
125
+ } else if (key.name === 'c' && key.ctrl) {
126
+ cleanup();
127
+ resolve(null);
128
+ }
129
+ };
130
+
131
+ process.stdin.on('keypress', onKeypress);
132
+ process.stdin.once('close', onClose);
133
+ process.stdin.once('end', onClose);
134
+ process.stdin.once('error', onClose);
135
+ });
136
+
137
+ process.stdin.setRawMode(false);
138
+ rl.close();
139
+
140
+ return selectedType;
141
+ };
142
+
143
+ const ensureEmptyDir = async (targetDir) => {
144
+ try {
145
+ const entries = await fs.readdir(targetDir);
146
+ return entries.length === 0;
147
+ } catch (error) {
148
+ if (error && error.code === 'ENOENT') {
149
+ await fs.mkdir(targetDir, { recursive: true });
150
+ return true;
151
+ }
152
+ throw error;
153
+ }
154
+ };
155
+
156
+ const copyDir = async (
157
+ source,
158
+ destination,
159
+ ignore = new Set(),
160
+ ignorePaths = new Set()
161
+ ) => {
162
+ await fs.mkdir(destination, { recursive: true });
163
+ const entries = await fs.readdir(source, { withFileTypes: true });
164
+
165
+ for (const entry of entries) {
166
+ if (ignore.has(entry.name)) {
167
+ continue;
168
+ }
169
+
170
+ const sourcePath = path.join(source, entry.name);
171
+ const destPath = path.join(destination, entry.name);
172
+ if (ignorePaths.has(sourcePath)) {
173
+ continue;
174
+ }
175
+
176
+ if (entry.isDirectory()) {
177
+ await copyDir(sourcePath, destPath, ignore, ignorePaths);
178
+ } else if (entry.isFile()) {
179
+ await fs.copyFile(sourcePath, destPath);
180
+ }
181
+ }
182
+ };
183
+
184
+ const removePath = async (targetPath) => {
185
+ try {
186
+ await fs.rm(targetPath, { recursive: true, force: true });
187
+ } catch (error) {
188
+ if (error && error.code !== 'ENOENT') {
189
+ throw error;
190
+ }
191
+ }
192
+ };
193
+
194
+ const updateManifest = async (targetDir, type) => {
195
+ const manifestPath = path.join(targetDir, 'src', 'manifest.json');
196
+ const data = JSON.parse(await fs.readFile(manifestPath, 'utf-8'));
197
+
198
+ data.chrome_url_overrides = {
199
+ [type]: `${type}.html`,
200
+ };
201
+
202
+ const permissions = new Set(data.permissions || []);
203
+ permissions.delete('history');
204
+ permissions.delete('bookmarks');
205
+ const permission = PAGE_META[type]?.permission;
206
+ if (permission) {
207
+ permissions.add(permission);
208
+ }
209
+ data.permissions = Array.from(permissions);
210
+
211
+ await fs.writeFile(manifestPath, JSON.stringify(data, null, 2));
212
+ };
213
+
214
+ const updateViteConfig = async (targetDir, type) => {
215
+ const vitePath = path.join(targetDir, 'vite.config.ts');
216
+ const content = await fs.readFile(vitePath, 'utf-8');
217
+ const lines = content.split(/\r?\n/);
218
+
219
+ const filtered = lines.filter((line) => {
220
+ if (line.includes('newtab: resolve(')) return type === 'newtab';
221
+ if (line.includes('history: resolve(')) return type === 'history';
222
+ if (line.includes('bookmarks: resolve(')) return type === 'bookmarks';
223
+ return true;
224
+ });
225
+
226
+ await fs.writeFile(vitePath, filtered.join('\n'));
227
+ };
228
+
229
+ const removePatterns = (content, patterns) =>
230
+ patterns.reduce((acc, pattern) => acc.replace(pattern, ''), content);
231
+
232
+ const updateStyles = async (targetDir, type) => {
233
+ const stylePath = path.join(targetDir, 'src', 'style.css');
234
+ let content = await fs.readFile(stylePath, 'utf-8');
235
+
236
+ const targets = TYPE_OPTIONS.filter((option) => option !== type);
237
+ targets.forEach((option) => {
238
+ content = removePatterns(content, STYLE_PATTERNS[option] || []);
239
+ });
240
+
241
+ await fs.writeFile(stylePath, content.trimEnd() + '\n');
242
+ };
243
+
244
+ const updateReadme = async (targetDir, type) => {
245
+ const readmePath = path.join(targetDir, 'README.md');
246
+ let content = await fs.readFile(readmePath, 'utf-8');
247
+
248
+ const meta = PAGE_META[type];
249
+ const section = [
250
+ '### 页面覆盖',
251
+ '',
252
+ '项目内置 1 个页面:',
253
+ '',
254
+ `- \`${meta.label}\`:${meta.desc}`,
255
+ '',
256
+ '对应入口文件:',
257
+ '',
258
+ `- \`${meta.html}\` → \`${meta.entry}\``,
259
+ '',
260
+ ].join('\n');
261
+
262
+ const sectionRegex = /### 页面覆盖[\s\S]*?(?=\n### |\n## |$)/;
263
+ if (sectionRegex.test(content)) {
264
+ content = content.replace(sectionRegex, section.trim());
265
+ } else {
266
+ content = content.trimEnd() + '\n\n' + section.trim() + '\n';
267
+ }
268
+
269
+ await fs.writeFile(readmePath, content.trimEnd() + '\n');
270
+ };
271
+
272
+ const updatePackageJson = async (targetDir, projectName) => {
273
+ const pkgPath = path.join(targetDir, 'package.json');
274
+ const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
275
+ pkg.name = projectName;
276
+ if (pkg.bin) {
277
+ delete pkg.bin;
278
+ }
279
+ await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2));
280
+ };
281
+
282
+ const updatePackageLock = async (targetDir, projectName) => {
283
+ const lockPath = path.join(targetDir, 'package-lock.json');
284
+ try {
285
+ const lock = JSON.parse(await fs.readFile(lockPath, 'utf-8'));
286
+ lock.name = projectName;
287
+ if (lock.packages && lock.packages['']) {
288
+ lock.packages[''].name = projectName;
289
+ }
290
+ await fs.writeFile(lockPath, JSON.stringify(lock, null, 2));
291
+ } catch (error) {
292
+ if (error && error.code !== 'ENOENT') {
293
+ throw error;
294
+ }
295
+ }
296
+ };
297
+
298
+ const pruneFiles = async (targetDir, type) => {
299
+ const removals = TYPE_OPTIONS.filter((option) => option !== type)
300
+ .flatMap((option) => PRUNE_PATHS[option] || [])
301
+ .map((item) => removePath(path.join(targetDir, item)));
302
+ await Promise.all(removals);
303
+ };
304
+
305
+ const createProgress = (total) => {
306
+ let current = 0;
307
+ const isTty = Boolean(process.stdout.isTTY);
308
+ const barWidth = 24;
309
+
310
+ const render = (label) => {
311
+ if (!isTty) {
312
+ return;
313
+ }
314
+ const percent = Math.min(100, Math.round((current / total) * 100));
315
+ const filled = Math.round((barWidth * percent) / 100);
316
+ const bar = `${'#'.repeat(filled)}${'.'.repeat(barWidth - filled)}`;
317
+ const text = `[${bar}] ${percent}% ${label}`;
318
+ process.stdout.write(`\r${text}`);
319
+ };
320
+
321
+ const step = (label) => {
322
+ current += 1;
323
+ render(label);
324
+ if (current >= total && isTty) {
325
+ process.stdout.write('\r\x1b[2K');
326
+ }
327
+ };
328
+
329
+ return step;
330
+ };
331
+
332
+ const main = async () => {
333
+ const args = process.argv.slice(2);
334
+ const targetArg = args.find((arg) => !arg.startsWith('-')) || 'extension-app';
335
+ const targetDir = path.resolve(process.cwd(), targetArg);
336
+ const projectName = path.basename(targetDir);
337
+ const typeArg = getOptionValue(args, ['--type', '-t']);
338
+ let selectedType = typeArg && TYPE_OPTIONS.includes(typeArg) ? typeArg : null;
339
+
340
+ if (!selectedType) {
341
+ selectedType = await promptType();
342
+ }
343
+
344
+ if (!selectedType || !TYPE_OPTIONS.includes(selectedType)) {
345
+ console.log('未选择有效类型,已退出。');
346
+ process.exit(1);
347
+ }
348
+
349
+ const empty = await ensureEmptyDir(targetDir);
350
+ if (!empty) {
351
+ console.log(`目录不为空,请选择空目录:${targetDir}`);
352
+ process.exit(1);
353
+ }
354
+
355
+ const ignore = new Set([
356
+ 'node_modules',
357
+ 'dist',
358
+ '.git',
359
+ '.DS_Store',
360
+ 'scripts',
361
+ ]);
362
+
363
+ const ignorePaths = new Set();
364
+ const relativeTarget = path.relative(templateRoot, targetDir);
365
+ if (
366
+ relativeTarget === '' ||
367
+ (!relativeTarget.startsWith('..') && !path.isAbsolute(relativeTarget))
368
+ ) {
369
+ ignorePaths.add(targetDir);
370
+ }
371
+
372
+ const step = createProgress(9);
373
+
374
+ step('复制模板');
375
+ await copyDir(templateRoot, targetDir, ignore, ignorePaths);
376
+
377
+ step('写入脚本');
378
+ await fs.mkdir(path.join(targetDir, 'scripts'), { recursive: true });
379
+ await fs.copyFile(
380
+ path.join(templateRoot, 'scripts', 'dev.js'),
381
+ path.join(targetDir, 'scripts', 'dev.js')
382
+ );
383
+
384
+ step('裁剪文件');
385
+ await pruneFiles(targetDir, selectedType);
386
+
387
+ step('更新 manifest');
388
+ await updateManifest(targetDir, selectedType);
389
+
390
+ step('更新 Vite 配置');
391
+ await updateViteConfig(targetDir, selectedType);
392
+
393
+ step('更新样式');
394
+ await updateStyles(targetDir, selectedType);
395
+
396
+ step('更新 README');
397
+ await updateReadme(targetDir, selectedType);
398
+
399
+ step('更新 package.json');
400
+ await updatePackageJson(targetDir, projectName);
401
+
402
+ step('更新 package-lock.json');
403
+ await updatePackageLock(targetDir, projectName);
404
+
405
+ console.log('');
406
+ console.log(`✅ 已创建项目:${targetDir}`);
407
+ console.log(`✅ 选择页面类型:${selectedType}`);
408
+ console.log('');
409
+ console.log('下一步:');
410
+ console.log(`cd ${path.relative(process.cwd(), targetDir)}`);
411
+ console.log('npm install');
412
+ console.log('npm run dev');
413
+ };
414
+
415
+ main().catch((error) => {
416
+ console.error('创建失败:', error);
417
+ process.exit(1);
418
+ });
package/scripts/dev.js ADDED
@@ -0,0 +1,26 @@
1
+ import { spawn } from 'child_process';
2
+
3
+ console.log('🚀 Development mode started');
4
+ console.log('📦 Watching for file changes...');
5
+ console.log('💡 Load the extension from the "dist" folder in Chrome');
6
+ console.log('');
7
+
8
+ // 启动 Vite watch 模式(插件会自动处理修复)
9
+ const viteProcess = spawn(
10
+ 'vite',
11
+ ['build', '--watch', '--mode', 'development'],
12
+ {
13
+ stdio: 'inherit',
14
+ shell: true,
15
+ env: {
16
+ ...process.env,
17
+ NODE_ENV: 'development',
18
+ },
19
+ }
20
+ );
21
+
22
+ // 处理退出
23
+ process.on('SIGINT', () => {
24
+ viteProcess.kill();
25
+ process.exit();
26
+ });
@@ -0,0 +1,9 @@
1
+ // Service Worker for Chrome Extension
2
+ chrome.runtime.onInstalled.addListener(() => {
3
+ console.log('Extension installed');
4
+ });
5
+
6
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
7
+ console.log('Message received:', message);
8
+ return true;
9
+ });
@@ -0,0 +1,17 @@
1
+ .container {
2
+ min-height: 100vh;
3
+ display: flex;
4
+ flex-direction: column;
5
+ position: relative;
6
+ color: var(--text-primary);
7
+ transition: color 0.3s ease;
8
+ }
9
+
10
+ .themeButtonWrapper {
11
+ position: absolute;
12
+ top: 20px;
13
+ right: 20px;
14
+ z-index: 1000;
15
+ filter: drop-shadow(0 2px 4px var(--shadow-md));
16
+ transition: filter 0.3s ease;
17
+ }