web-extension 0.0.0-stage → 2.0.0

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 (53) hide show
  1. package/AI.md +270 -0
  2. package/DEVELOPER.md +122 -0
  3. package/LICENSE +21 -0
  4. package/README.md +140 -2
  5. package/dist/index.js +2125 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +54 -4
  8. package/templates/base/_gitignore +6 -0
  9. package/templates/base/public/icons/128.png +0 -0
  10. package/templates/base/public/icons/16.png +0 -0
  11. package/templates/base/public/icons/48.png +0 -0
  12. package/templates/features/background/js/src/background/index.js +6 -0
  13. package/templates/features/background/ts/src/background/index.ts +6 -0
  14. package/templates/features/content/content-only/js/src/content/index.js +32 -0
  15. package/templates/features/content/content-only/ts/src/content/index.ts +32 -0
  16. package/templates/features/content/full/js/src/content/index.js +6 -0
  17. package/templates/features/content/full/ts/src/content/index.ts +6 -0
  18. package/templates/features/content/style.css +16 -0
  19. package/templates/features/devtools/js/src/devtools/devtools.html +9 -0
  20. package/templates/features/devtools/js/src/devtools/devtools.js +11 -0
  21. package/templates/features/devtools/js/src/devtools/panel.html +18 -0
  22. package/templates/features/devtools/ts/src/devtools/devtools.html +9 -0
  23. package/templates/features/devtools/ts/src/devtools/devtools.ts +11 -0
  24. package/templates/features/devtools/ts/src/devtools/panel.html +18 -0
  25. package/templates/features/dnr/rules.json +18 -0
  26. package/templates/features/extras/storage/js/src/lib/storage.js +31 -0
  27. package/templates/features/extras/storage/ts/src/lib/storage.ts +15 -0
  28. package/templates/features/newtab/js/src/newtab/index.html +17 -0
  29. package/templates/features/newtab/js/src/newtab/main.js +8 -0
  30. package/templates/features/newtab/js/src/newtab/style.css +30 -0
  31. package/templates/features/newtab/ts/src/newtab/index.html +17 -0
  32. package/templates/features/newtab/ts/src/newtab/main.ts +8 -0
  33. package/templates/features/newtab/ts/src/newtab/style.css +30 -0
  34. package/templates/features/options/js/src/options/index.html +21 -0
  35. package/templates/features/options/js/src/options/main.js +11 -0
  36. package/templates/features/options/js/src/options/style.css +37 -0
  37. package/templates/features/options/ts/src/options/index.html +21 -0
  38. package/templates/features/options/ts/src/options/main.ts +11 -0
  39. package/templates/features/options/ts/src/options/style.css +37 -0
  40. package/templates/features/popup/js/src/popup/index.html +18 -0
  41. package/templates/features/popup/js/src/popup/main.js +9 -0
  42. package/templates/features/popup/js/src/popup/style.css +39 -0
  43. package/templates/features/popup/ts/src/popup/index.html +18 -0
  44. package/templates/features/popup/ts/src/popup/main.ts +9 -0
  45. package/templates/features/popup/ts/src/popup/style.css +39 -0
  46. package/templates/features/sidepanel/js/src/sidepanel/index.html +17 -0
  47. package/templates/features/sidepanel/js/src/sidepanel/main.js +7 -0
  48. package/templates/features/sidepanel/js/src/sidepanel/style.css +25 -0
  49. package/templates/features/sidepanel/ts/src/sidepanel/index.html +17 -0
  50. package/templates/features/sidepanel/ts/src/sidepanel/main.ts +7 -0
  51. package/templates/features/sidepanel/ts/src/sidepanel/style.css +25 -0
  52. package/templates/js/src/lib/browser.js +9 -0
  53. package/templates/ts/src/lib/browser.ts +9 -0
package/dist/index.js ADDED
@@ -0,0 +1,2125 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import * as p2 from "@clack/prompts";
5
+ import pico from "picocolors";
6
+ import fs6 from "fs";
7
+ import path7 from "path";
8
+ import { fileURLToPath as fileURLToPath2 } from "url";
9
+
10
+ // src/utils/validate.ts
11
+ import path from "path";
12
+ function validatePackageName(name) {
13
+ if (!name || name.trim().length === 0) {
14
+ return "Project name cannot be empty.";
15
+ }
16
+ const trimmed = name.trim();
17
+ if (trimmed !== trimmed.toLowerCase()) {
18
+ return "Project name must be lowercase.";
19
+ }
20
+ if (/\s/.test(trimmed)) {
21
+ return "Project name cannot contain spaces.";
22
+ }
23
+ if (trimmed.startsWith(".") || trimmed.startsWith("_")) {
24
+ return "Project name cannot start with a dot or underscore.";
25
+ }
26
+ if (!/^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(trimmed)) {
27
+ return "Project name contains invalid characters for an npm package.";
28
+ }
29
+ if (encodeURIComponent(trimmed) !== trimmed && !trimmed.startsWith("@")) {
30
+ return "Project name must be URL-safe.";
31
+ }
32
+ return true;
33
+ }
34
+ function getPackageName(input) {
35
+ const base = path.basename(path.resolve(input));
36
+ return base.toLowerCase().replace(/[^a-z0-9-_]/g, "-").replace(/^-+|-+$/g, "") || "my-extension";
37
+ }
38
+ function validateMatchPattern(pattern) {
39
+ const p3 = pattern.trim();
40
+ if (!p3) {
41
+ return "Match pattern cannot be empty.";
42
+ }
43
+ if (p3 === "<all_urls>") {
44
+ return true;
45
+ }
46
+ const validSchemes = ["http://", "https://", "*://", "file://"];
47
+ const hasValidScheme = validSchemes.some((scheme) => p3.startsWith(scheme));
48
+ if (!hasValidScheme) {
49
+ return `Invalid match pattern "${p3}". Must start with http://, https://, *://, file://, or be <all_urls>`;
50
+ }
51
+ return true;
52
+ }
53
+ function parseAndValidateMatches(input) {
54
+ const list = Array.isArray(input) ? input : input.split(",").map((s) => s.trim()).filter(Boolean);
55
+ if (list.length === 0) {
56
+ return { valid: false, error: "At least one match pattern is required." };
57
+ }
58
+ for (const item of list) {
59
+ const res = validateMatchPattern(item);
60
+ if (res !== true) {
61
+ return { valid: false, error: res };
62
+ }
63
+ }
64
+ return { valid: true, matches: list };
65
+ }
66
+ function toSlug(str) {
67
+ return str.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "extension";
68
+ }
69
+
70
+ // src/cli/args.ts
71
+ var VALID_BROWSERS = ["chrome", "firefox", "safari", "chromium"];
72
+ var VALID_LANGS = ["ts", "js", "typescript", "javascript"];
73
+ var VALID_TYPES = ["full", "content-only"];
74
+ var VALID_MVS = ["2", "3"];
75
+ var VALID_FEATURES = [
76
+ "background",
77
+ "content",
78
+ "popup",
79
+ "options",
80
+ "sidepanel",
81
+ "dnr",
82
+ "devtools",
83
+ "newtab"
84
+ ];
85
+ var VALID_EXTRAS = [
86
+ "contextmenu",
87
+ "commands",
88
+ "omnibox",
89
+ "storage"
90
+ ];
91
+ var VALID_RUN_AT = [
92
+ "document_start",
93
+ "document_end",
94
+ "document_idle"
95
+ ];
96
+ var VALID_WORLD = ["ISOLATED", "MAIN"];
97
+ var VALID_IF_EXISTS = ["cancel", "overwrite", "merge"];
98
+ var VALID_PM = ["npm", "pnpm", "yarn", "bun"];
99
+ function parseCliArgs(argv) {
100
+ const result = {
101
+ rawFlags: {},
102
+ warnings: []
103
+ };
104
+ const positionals = [];
105
+ const browserList = [];
106
+ let i = 0;
107
+ while (i < argv.length) {
108
+ const arg = argv[i];
109
+ if (arg === "--help" || arg === "-h") {
110
+ result.help = true;
111
+ i++;
112
+ } else if (arg === "--version" || arg === "-v") {
113
+ result.version = true;
114
+ i++;
115
+ } else if (arg === "--yes" || arg === "-y") {
116
+ result.yes = true;
117
+ i++;
118
+ } else if (arg === "--overwrite") {
119
+ result.ifExists = "overwrite";
120
+ result.rawFlags["ifExists"] = "overwrite";
121
+ i++;
122
+ } else if (arg === "--css") {
123
+ result.css = true;
124
+ result.rawFlags["css"] = true;
125
+ i++;
126
+ } else if (arg === "--no-css") {
127
+ result.css = false;
128
+ result.rawFlags["css"] = false;
129
+ i++;
130
+ } else if (arg === "--install") {
131
+ result.install = true;
132
+ result.rawFlags["install"] = true;
133
+ i++;
134
+ } else if (arg === "--no-install") {
135
+ result.install = false;
136
+ result.rawFlags["install"] = false;
137
+ i++;
138
+ } else if (arg === "--git") {
139
+ result.git = true;
140
+ result.rawFlags["git"] = true;
141
+ i++;
142
+ } else if (arg === "--no-git") {
143
+ result.git = false;
144
+ result.rawFlags["git"] = false;
145
+ i++;
146
+ } else if (arg.startsWith("--")) {
147
+ const eqIdx = arg.indexOf("=");
148
+ let flag = arg.slice(2);
149
+ let val;
150
+ if (eqIdx !== -1) {
151
+ flag = arg.slice(2, eqIdx);
152
+ val = arg.slice(eqIdx + 1);
153
+ } else if (i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
154
+ val = argv[++i];
155
+ }
156
+ if (val === void 0) {
157
+ throw new Error(`Flag --${flag} requires a value.`);
158
+ }
159
+ switch (flag) {
160
+ case "name":
161
+ result.name = val;
162
+ result.rawFlags["name"] = val;
163
+ break;
164
+ case "if-exists":
165
+ if (!VALID_IF_EXISTS.includes(val)) {
166
+ throw new Error(
167
+ `Invalid value "${val}" for --if-exists. Expected one of: ${VALID_IF_EXISTS.join(", ")}`
168
+ );
169
+ }
170
+ result.ifExists = val;
171
+ result.rawFlags["ifExists"] = val;
172
+ break;
173
+ case "lang":
174
+ if (!VALID_LANGS.includes(val.toLowerCase())) {
175
+ throw new Error(
176
+ `Invalid value "${val}" for --lang. Expected one of: ts, js`
177
+ );
178
+ }
179
+ result.lang = val.toLowerCase() === "javascript" || val.toLowerCase() === "js" ? "js" : "ts";
180
+ result.rawFlags["lang"] = result.lang;
181
+ break;
182
+ case "type":
183
+ if (!VALID_TYPES.includes(val.toLowerCase())) {
184
+ throw new Error(
185
+ `Invalid value "${val}" for --type. Expected one of: ${VALID_TYPES.join(", ")}`
186
+ );
187
+ }
188
+ result.type = val.toLowerCase();
189
+ result.rawFlags["type"] = result.type;
190
+ break;
191
+ case "browser":
192
+ case "browsers": {
193
+ const parts = val.split(",").map((s) => s.trim().toLowerCase());
194
+ for (const b of parts) {
195
+ if (!VALID_BROWSERS.includes(b)) {
196
+ throw new Error(
197
+ `Invalid value "${b}" for --browser. Expected one of: chrome, firefox, safari`
198
+ );
199
+ }
200
+ const mapped = b === "chromium" ? "chrome" : b;
201
+ if (!browserList.includes(mapped)) {
202
+ browserList.push(mapped);
203
+ }
204
+ }
205
+ result.browser = browserList;
206
+ result.rawFlags["browser"] = browserList;
207
+ break;
208
+ }
209
+ case "firefox-mv":
210
+ if (!VALID_MVS.includes(val)) {
211
+ throw new Error(
212
+ `Invalid value "${val}" for --firefox-mv. Expected 2 or 3.`
213
+ );
214
+ }
215
+ result.firefoxMv = parseInt(val, 10);
216
+ result.rawFlags["firefoxMv"] = result.firefoxMv;
217
+ break;
218
+ case "safari-mv":
219
+ if (!VALID_MVS.includes(val)) {
220
+ throw new Error(
221
+ `Invalid value "${val}" for --safari-mv. Expected 2 or 3.`
222
+ );
223
+ }
224
+ result.safariMv = parseInt(val, 10);
225
+ result.rawFlags["safariMv"] = result.safariMv;
226
+ break;
227
+ case "features": {
228
+ if (val.trim().toLowerCase() === "none") {
229
+ result.features = [];
230
+ } else {
231
+ const list = val.split(",").map((s) => s.trim().toLowerCase());
232
+ for (const f of list) {
233
+ if (!VALID_FEATURES.includes(f)) {
234
+ throw new Error(
235
+ `Invalid feature "${f}". Expected one of: ${VALID_FEATURES.join(", ")} (or none)`
236
+ );
237
+ }
238
+ }
239
+ result.features = Array.from(new Set(list));
240
+ }
241
+ result.rawFlags["features"] = result.features;
242
+ break;
243
+ }
244
+ case "matches": {
245
+ const matchRes = parseAndValidateMatches(val);
246
+ if (!matchRes.valid) {
247
+ throw new Error(matchRes.error);
248
+ }
249
+ result.matches = matchRes.matches;
250
+ result.rawFlags["matches"] = result.matches;
251
+ break;
252
+ }
253
+ case "run-at":
254
+ if (!VALID_RUN_AT.includes(val)) {
255
+ throw new Error(
256
+ `Invalid value "${val}" for --run-at. Expected one of: ${VALID_RUN_AT.join(", ")}`
257
+ );
258
+ }
259
+ result.runAt = val;
260
+ result.rawFlags["runAt"] = result.runAt;
261
+ break;
262
+ case "world":
263
+ if (!VALID_WORLD.includes(val.toUpperCase())) {
264
+ throw new Error(
265
+ `Invalid value "${val}" for --world. Expected ISOLATED or MAIN`
266
+ );
267
+ }
268
+ result.world = val.toUpperCase();
269
+ result.rawFlags["world"] = result.world;
270
+ break;
271
+ case "extras": {
272
+ if (val.trim().toLowerCase() === "none") {
273
+ result.extras = [];
274
+ } else {
275
+ const list = val.split(",").map((s) => s.trim().toLowerCase());
276
+ for (const e of list) {
277
+ if (!VALID_EXTRAS.includes(e)) {
278
+ throw new Error(
279
+ `Invalid extra "${e}". Expected one of: ${VALID_EXTRAS.join(", ")} (or none)`
280
+ );
281
+ }
282
+ }
283
+ result.extras = Array.from(new Set(list));
284
+ }
285
+ result.rawFlags["extras"] = result.extras;
286
+ break;
287
+ }
288
+ case "pm":
289
+ if (!VALID_PM.includes(val.toLowerCase())) {
290
+ throw new Error(
291
+ `Invalid value "${val}" for --pm. Expected one of: ${VALID_PM.join(", ")}`
292
+ );
293
+ }
294
+ result.pm = val.toLowerCase();
295
+ result.rawFlags["pm"] = result.pm;
296
+ break;
297
+ default:
298
+ throw new Error(`Unknown option --${flag}. Run with --help to see valid options.`);
299
+ }
300
+ i++;
301
+ } else {
302
+ positionals.push(arg);
303
+ i++;
304
+ }
305
+ }
306
+ if (positionals.length > 0) {
307
+ if (positionals[0] === "init") {
308
+ result.subcommand = "init";
309
+ if (positionals.length > 1) {
310
+ result.dir = positionals[1];
311
+ }
312
+ } else {
313
+ result.subcommand = "init";
314
+ result.dir = positionals[0];
315
+ }
316
+ } else {
317
+ result.subcommand = "init";
318
+ }
319
+ return result;
320
+ }
321
+ function validateFlagCombinations(args) {
322
+ if (args.help || args.version) {
323
+ return;
324
+ }
325
+ const isContentOnly = args.type === "content-only";
326
+ if (isContentOnly && (args.features !== void 0 || args.extras !== void 0)) {
327
+ throw new Error(
328
+ "Cannot specify --features or --extras when --type is content-only."
329
+ );
330
+ }
331
+ const browsers = args.browser || ["chrome"];
332
+ const hasOnlySafari = browsers.length === 1 && browsers[0] === "safari";
333
+ if (args.features?.includes("sidepanel") && hasOnlySafari) {
334
+ throw new Error("Side panel feature is not supported when targeting only Safari.");
335
+ }
336
+ const hasFirefoxMv2 = browsers.includes("firefox") && args.firefoxMv === 2;
337
+ const hasSafariMv2 = browsers.includes("safari") && args.safariMv === 2;
338
+ const anyMv2 = hasFirefoxMv2 || hasSafariMv2;
339
+ if (args.features?.includes("dnr") && anyMv2) {
340
+ throw new Error(
341
+ "DeclarativeNetRequest (DNR) is not supported with Manifest V2 targets."
342
+ );
343
+ }
344
+ const hasBackground = !isContentOnly && (args.features === void 0 || args.features.includes("background"));
345
+ if (args.extras?.some((e) => ["contextmenu", "commands", "omnibox"].includes(e))) {
346
+ if (!hasBackground) {
347
+ throw new Error(
348
+ "Extras contextmenu, commands, and omnibox require background feature."
349
+ );
350
+ }
351
+ }
352
+ if (args.world === "MAIN" && anyMv2) {
353
+ throw new Error("Content script world MAIN is only supported in Manifest V3.");
354
+ }
355
+ if (args.firefoxMv !== void 0 && !browsers.includes("firefox")) {
356
+ args.warnings.push("--firefox-mv specified without Firefox target.");
357
+ }
358
+ if (args.safariMv !== void 0 && !browsers.includes("safari")) {
359
+ args.warnings.push("--safari-mv specified without Safari target.");
360
+ }
361
+ if (!isContentOnly && args.features !== void 0 && !args.features.includes("content")) {
362
+ if (args.matches !== void 0 || args.runAt !== void 0 || args.world !== void 0 || args.css !== void 0) {
363
+ args.warnings.push("Content script options specified but content feature was not selected.");
364
+ }
365
+ }
366
+ }
367
+ function printHelp() {
368
+ console.log(`
369
+ web-extension [init] [dir] [options]
370
+
371
+ Options:
372
+ [dir] Target directory (defaults to current or prompt)
373
+ --name <string> Project name (defaults to basename of dir or "my-extension")
374
+ --if-exists <action> Action if directory exists: cancel, overwrite, merge (default: cancel)
375
+ --overwrite Shorthand for --if-exists overwrite
376
+ --lang <ts|js> Project language: ts (TypeScript) or js (JavaScript) (default: ts)
377
+ --type <type> Project type: full or content-only (default: full)
378
+ --browser <browsers> Target browsers: chrome, firefox, safari (default: chrome)
379
+ --firefox-mv <2|3> Manifest version for Firefox (default: 3)
380
+ --safari-mv <2|3> Manifest version for Safari (default: 3)
381
+ --features <list> Comma-separated features: background,content,popup,options,sidepanel,dnr,devtools,newtab (or "none")
382
+ --matches <patterns> Content script match patterns (default: https://*/*)
383
+ --run-at <timing> Content script run timing: document_start, document_end, document_idle (default: document_idle)
384
+ --world <world> Execution world: ISOLATED or MAIN (default: ISOLATED)
385
+ --css / --no-css Generate CSS for content script (default: --css)
386
+ --extras <list> Extras: contextmenu,commands,omnibox,storage (or "none")
387
+ --pm <pm> Package manager override: npm, pnpm, yarn, bun
388
+ --install / --no-install Install dependencies automatically (default: --install)
389
+ --git / --no-git Initialize git repository (default: --git)
390
+ --yes, -y Use defaults for all unanswered questions
391
+ --help, -h Show this help message
392
+ --version, -v Show version number
393
+ `);
394
+ }
395
+
396
+ // src/cli/prompts.ts
397
+ import * as p from "@clack/prompts";
398
+ import fs3 from "fs";
399
+ import path4 from "path";
400
+
401
+ // src/utils/pm.ts
402
+ import fs from "fs";
403
+ import path2 from "path";
404
+ function detectPackageManager(input = {}) {
405
+ const fsImpl = input.fsModule ?? {
406
+ existsSync: fs.existsSync,
407
+ readFileSync: (p3) => fs.readFileSync(p3, "utf-8"),
408
+ realpathSync: fs.realpathSync
409
+ };
410
+ const env = input.env ?? process.env;
411
+ const argv1 = input.argv1 ?? process.argv[1];
412
+ const versions = input.versions ?? process.versions;
413
+ if (input.flag) {
414
+ const f = input.flag.trim().toLowerCase();
415
+ if (f === "npm" || f === "pnpm" || f === "yarn" || f === "bun") {
416
+ return { name: f, source: "flag", confident: true };
417
+ }
418
+ }
419
+ const userAgent = env.npm_config_user_agent;
420
+ if (userAgent) {
421
+ const ua = userAgent.trim().toLowerCase();
422
+ if (ua.startsWith("bun/")) {
423
+ return { name: "bun", source: "user-agent", confident: true };
424
+ }
425
+ if (ua.startsWith("pnpm/")) {
426
+ return { name: "pnpm", source: "user-agent", confident: true };
427
+ }
428
+ if (ua.startsWith("yarn/")) {
429
+ return { name: "yarn", source: "user-agent", confident: true };
430
+ }
431
+ if (ua.startsWith("npm/")) {
432
+ return { name: "npm", source: "user-agent", confident: true };
433
+ }
434
+ }
435
+ const startDir = path2.resolve(input.targetDir || input.cwd || process.cwd());
436
+ let curDir = startDir;
437
+ while (true) {
438
+ const pkgPath = path2.join(curDir, "package.json");
439
+ if (fsImpl.existsSync(pkgPath)) {
440
+ try {
441
+ const raw = fsImpl.readFileSync(pkgPath, "utf-8");
442
+ const parsed = JSON.parse(raw);
443
+ if (typeof parsed.packageManager === "string") {
444
+ const pmField = parsed.packageManager.toLowerCase();
445
+ if (pmField.startsWith("bun@") || pmField === "bun") {
446
+ return { name: "bun", source: "package-json", confident: true };
447
+ }
448
+ if (pmField.startsWith("pnpm@") || pmField === "pnpm") {
449
+ return { name: "pnpm", source: "package-json", confident: true };
450
+ }
451
+ if (pmField.startsWith("yarn@") || pmField === "yarn") {
452
+ return { name: "yarn", source: "package-json", confident: true };
453
+ }
454
+ if (pmField.startsWith("npm@") || pmField === "npm") {
455
+ return { name: "npm", source: "package-json", confident: true };
456
+ }
457
+ }
458
+ } catch {
459
+ }
460
+ }
461
+ const parent = path2.dirname(curDir);
462
+ if (parent === curDir) break;
463
+ curDir = parent;
464
+ }
465
+ curDir = startDir;
466
+ while (true) {
467
+ const found = [];
468
+ if (fsImpl.existsSync(path2.join(curDir, "bun.lock")) || fsImpl.existsSync(path2.join(curDir, "bun.lockb"))) {
469
+ found.push("bun");
470
+ }
471
+ if (fsImpl.existsSync(path2.join(curDir, "pnpm-lock.yaml"))) {
472
+ found.push("pnpm");
473
+ }
474
+ if (fsImpl.existsSync(path2.join(curDir, "yarn.lock"))) {
475
+ found.push("yarn");
476
+ }
477
+ if (fsImpl.existsSync(path2.join(curDir, "package-lock.json")) || fsImpl.existsSync(path2.join(curDir, "npm-shrinkwrap.json"))) {
478
+ found.push("npm");
479
+ }
480
+ if (found.length === 1) {
481
+ return { name: found[0], source: "lockfile", confident: true };
482
+ }
483
+ if (found.length > 1) {
484
+ const priority = ["bun", "pnpm", "yarn", "npm"];
485
+ const chosen = priority.find((p3) => found.includes(p3));
486
+ return { name: chosen, source: "lockfile", confident: false };
487
+ }
488
+ if (fsImpl.existsSync(path2.join(curDir, ".git"))) {
489
+ break;
490
+ }
491
+ const parent = path2.dirname(curDir);
492
+ if (parent === curDir) break;
493
+ curDir = parent;
494
+ }
495
+ if (argv1) {
496
+ try {
497
+ const real = fsImpl.existsSync(argv1) ? fsImpl.realpathSync(argv1) : argv1;
498
+ const normalized = real.replace(/\\/g, "/").toLowerCase();
499
+ if (normalized.includes("/.bun/") || normalized.includes("/bun/bin/")) {
500
+ return { name: "bun", source: "global-path", confident: false };
501
+ }
502
+ if (normalized.includes("/pnpm/global/") || normalized.includes("/appdata/local/pnpm")) {
503
+ return { name: "pnpm", source: "global-path", confident: false };
504
+ }
505
+ if (normalized.includes("/yarn/global/") || normalized.includes("/.config/yarn/global/") || normalized.includes("/appdata/local/yarn")) {
506
+ return { name: "yarn", source: "global-path", confident: false };
507
+ }
508
+ } catch {
509
+ }
510
+ }
511
+ if (versions && versions.bun) {
512
+ return { name: "bun", source: "runtime", confident: false };
513
+ }
514
+ return { name: "npm", source: "default", confident: false };
515
+ }
516
+ function formatPmSource(source) {
517
+ switch (source) {
518
+ case "flag":
519
+ return "command line flag";
520
+ case "user-agent":
521
+ return "user agent";
522
+ case "package-json":
523
+ return "package.json packageManager field";
524
+ case "lockfile":
525
+ return "lockfile";
526
+ case "global-path":
527
+ return "global installation directory";
528
+ case "runtime":
529
+ return "runtime environment";
530
+ case "prompt":
531
+ return "user selection";
532
+ case "default":
533
+ return "default fallback";
534
+ }
535
+ }
536
+ function getInstallCommand(pm) {
537
+ switch (pm) {
538
+ case "npm":
539
+ return "npm install";
540
+ case "pnpm":
541
+ return "pnpm install";
542
+ case "yarn":
543
+ return "yarn";
544
+ case "bun":
545
+ return "bun install";
546
+ }
547
+ }
548
+ function getRunCommand(pm, script) {
549
+ switch (pm) {
550
+ case "npm":
551
+ return `npm run ${script}`;
552
+ case "pnpm":
553
+ return `pnpm ${script}`;
554
+ case "yarn":
555
+ return `yarn ${script}`;
556
+ case "bun":
557
+ return `bun run ${script}`;
558
+ }
559
+ }
560
+
561
+ // src/utils/exec.ts
562
+ import { spawn } from "child_process";
563
+ import fs2 from "fs";
564
+ import path3 from "path";
565
+ function isCommandAvailable(command) {
566
+ return new Promise((resolve) => {
567
+ const isWindows = process.platform === "win32";
568
+ const child = spawn(command, ["--version"], {
569
+ shell: isWindows,
570
+ stdio: "ignore"
571
+ });
572
+ child.on("error", () => resolve(false));
573
+ child.on("close", (code) => resolve(code === 0));
574
+ });
575
+ }
576
+ function isInsideGitRepo(dir) {
577
+ let cur = path3.resolve(dir);
578
+ while (true) {
579
+ if (fs2.existsSync(path3.join(cur, ".git"))) {
580
+ return true;
581
+ }
582
+ const parent = path3.dirname(cur);
583
+ if (parent === cur) break;
584
+ cur = parent;
585
+ }
586
+ return false;
587
+ }
588
+ function runCommand(command, args, cwd) {
589
+ return new Promise((resolve) => {
590
+ const isWindows = process.platform === "win32";
591
+ const child = spawn(command, args, {
592
+ cwd,
593
+ shell: isWindows,
594
+ stdio: "inherit"
595
+ });
596
+ child.on("error", (err) => resolve({ success: false, error: err.message }));
597
+ child.on("close", (code) => {
598
+ if (code === 0) {
599
+ resolve({ success: true });
600
+ } else {
601
+ resolve({ success: false, error: `Exited with code ${code}` });
602
+ }
603
+ });
604
+ });
605
+ }
606
+
607
+ // src/cli/prompts.ts
608
+ async function runPrompts(args) {
609
+ const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);
610
+ const useDefaults = Boolean(args.yes);
611
+ function handleCancel(value) {
612
+ if (p.isCancel(value)) {
613
+ p.cancel("Operation cancelled.");
614
+ process.exit(0);
615
+ }
616
+ return value;
617
+ }
618
+ let projectName = args.name;
619
+ let targetDir = args.dir;
620
+ if (!projectName && !targetDir) {
621
+ if (!isTTY && !useDefaults) {
622
+ throw new Error("Missing project name or target directory. Provide [dir] or --name.");
623
+ }
624
+ if (useDefaults) {
625
+ projectName = "my-extension";
626
+ targetDir = "my-extension";
627
+ } else {
628
+ const nameInput = handleCancel(
629
+ await p.text({
630
+ message: "Project name:",
631
+ placeholder: "my-extension",
632
+ defaultValue: "my-extension",
633
+ validate(val) {
634
+ const trimmed = val ? val.trim() : "";
635
+ if (!trimmed) return "Project name cannot be empty.";
636
+ const check = validatePackageName(trimmed);
637
+ return check === true ? void 0 : check;
638
+ }
639
+ })
640
+ );
641
+ projectName = nameInput.trim();
642
+ targetDir = projectName;
643
+ }
644
+ } else if (!projectName && targetDir) {
645
+ projectName = getPackageName(targetDir);
646
+ } else if (projectName && !targetDir) {
647
+ targetDir = projectName;
648
+ }
649
+ const resolvedTargetDir = path4.resolve(targetDir);
650
+ let ifExists = args.ifExists || "cancel";
651
+ const dirExists = fs3.existsSync(resolvedTargetDir);
652
+ const dirHasFiles = dirExists && fs3.readdirSync(resolvedTargetDir).length > 0;
653
+ if (dirHasFiles && !args.ifExists) {
654
+ if (!isTTY && !useDefaults) {
655
+ throw new Error(
656
+ `Target directory "${targetDir}" is not empty. Specify --if-exists or --overwrite.`
657
+ );
658
+ }
659
+ if (useDefaults) {
660
+ throw new Error(
661
+ `Target directory "${targetDir}" is not empty and default action is cancel.`
662
+ );
663
+ }
664
+ ifExists = handleCancel(
665
+ await p.select({
666
+ message: `Target directory "${targetDir}" is not empty. How to proceed?`,
667
+ options: [
668
+ { value: "cancel", label: "Cancel", hint: "Exit without modifying files" },
669
+ { value: "overwrite", label: "Overwrite", hint: "Delete existing files and proceed" },
670
+ { value: "merge", label: "Merge", hint: "Keep existing files and add extension files" }
671
+ ],
672
+ initialValue: "cancel"
673
+ })
674
+ );
675
+ if (ifExists === "cancel") {
676
+ p.cancel("Operation cancelled.");
677
+ process.exit(0);
678
+ }
679
+ }
680
+ let language = args.lang || (useDefaults ? "ts" : void 0);
681
+ if (!language) {
682
+ if (!isTTY) throw new Error("Missing --lang flag in non-interactive environment.");
683
+ language = handleCancel(
684
+ await p.select({
685
+ message: "Select language:",
686
+ options: [
687
+ { value: "ts", label: "TypeScript", hint: "Recommended" },
688
+ { value: "js", label: "JavaScript" }
689
+ ],
690
+ initialValue: "ts"
691
+ })
692
+ );
693
+ }
694
+ let projectType = args.type || (useDefaults ? "full" : void 0);
695
+ if (!projectType) {
696
+ if (!isTTY) throw new Error("Missing --type flag in non-interactive environment.");
697
+ projectType = handleCancel(
698
+ await p.select({
699
+ message: "Select project type:",
700
+ options: [
701
+ { value: "full", label: "Full extension", hint: "Background, popup, options, content scripts, etc." },
702
+ { value: "content-only", label: "Content script only", hint: "DOM injector/script with no extension UI" }
703
+ ],
704
+ initialValue: "full"
705
+ })
706
+ );
707
+ }
708
+ let browsers = args.browser?.length ? args.browser : useDefaults ? ["chrome"] : void 0;
709
+ if (!browsers) {
710
+ if (!isTTY) throw new Error("Missing --browser flag in non-interactive environment.");
711
+ browsers = handleCancel(
712
+ await p.multiselect({
713
+ message: "Select target browsers:",
714
+ options: [
715
+ { value: "chrome", label: "Chromium (Chrome, Edge, Brave, Opera)", hint: "default" },
716
+ { value: "firefox", label: "Firefox" },
717
+ { value: "safari", label: "Safari" }
718
+ ],
719
+ initialValues: ["chrome"],
720
+ required: true
721
+ })
722
+ );
723
+ }
724
+ let firefoxMv = args.firefoxMv;
725
+ if (browsers.includes("firefox") && firefoxMv === void 0) {
726
+ if (useDefaults) {
727
+ firefoxMv = 3;
728
+ } else if (!isTTY) {
729
+ throw new Error("Missing --firefox-mv flag in non-interactive environment.");
730
+ } else {
731
+ firefoxMv = handleCancel(
732
+ await p.select({
733
+ message: "Manifest version for Firefox:",
734
+ options: [
735
+ { value: 3, label: "MV3", hint: "Recommended modern standard" },
736
+ { value: 2, label: "MV2", hint: "Legacy background page" }
737
+ ],
738
+ initialValue: 3
739
+ })
740
+ );
741
+ }
742
+ }
743
+ let safariMv = args.safariMv;
744
+ if (browsers.includes("safari") && safariMv === void 0) {
745
+ if (useDefaults) {
746
+ safariMv = 3;
747
+ } else if (!isTTY) {
748
+ throw new Error("Missing --safari-mv flag in non-interactive environment.");
749
+ } else {
750
+ safariMv = handleCancel(
751
+ await p.select({
752
+ message: "Manifest version for Safari:",
753
+ options: [
754
+ { value: 3, label: "MV3", hint: "Recommended modern standard" },
755
+ { value: 2, label: "MV2", hint: "Legacy extension converter target" }
756
+ ],
757
+ initialValue: 3
758
+ })
759
+ );
760
+ }
761
+ }
762
+ const anyMv2 = browsers.includes("firefox") && firefoxMv === 2 || browsers.includes("safari") && safariMv === 2;
763
+ const onlySafari = browsers.length === 1 && browsers[0] === "safari";
764
+ let features = [];
765
+ if (projectType === "full") {
766
+ if (args.features !== void 0) {
767
+ features = args.features;
768
+ } else if (useDefaults) {
769
+ features = ["background", "content"];
770
+ } else if (!isTTY) {
771
+ throw new Error("Missing --features flag in non-interactive environment.");
772
+ } else {
773
+ const featureOptions = [
774
+ { value: "background", label: "Background", hint: "Service worker or background script" },
775
+ { value: "content", label: "Content script", hint: "Inject scripts and styles into webpages" },
776
+ { value: "popup", label: "Popup", hint: "Toolbar action popup HTML dialog" },
777
+ { value: "options", label: "Options page", hint: "Extension settings interface" }
778
+ ];
779
+ if (!onlySafari) {
780
+ featureOptions.push({
781
+ value: "sidepanel",
782
+ label: "Side panel",
783
+ hint: browsers.includes("safari") ? "Not supported on Safari" : void 0
784
+ });
785
+ }
786
+ if (!anyMv2) {
787
+ featureOptions.push({
788
+ value: "dnr",
789
+ label: "declarativeNetRequest rules",
790
+ hint: "Declarative network request blocking/redirect rules"
791
+ });
792
+ }
793
+ featureOptions.push({ value: "devtools", label: "DevTools page", hint: "Custom developer tools tab" });
794
+ featureOptions.push({ value: "newtab", label: "New tab override", hint: "Override default new tab page" });
795
+ features = handleCancel(
796
+ await p.multiselect({
797
+ message: "Select extension features:",
798
+ options: featureOptions,
799
+ initialValues: ["background", "content"],
800
+ required: false
801
+ })
802
+ );
803
+ }
804
+ }
805
+ const hasContentScript = projectType === "content-only" || features.includes("content");
806
+ let contentScriptOptions = void 0;
807
+ if (hasContentScript) {
808
+ let matches = args.matches || [];
809
+ if (matches.length === 0) {
810
+ if (useDefaults) {
811
+ matches = ["https://*/*"];
812
+ } else if (!isTTY) {
813
+ throw new Error("Missing --matches flag in non-interactive environment.");
814
+ } else {
815
+ const matchesInput = handleCancel(
816
+ await p.text({
817
+ message: "Content script URL match patterns (comma-separated):",
818
+ placeholder: "https://*/*",
819
+ defaultValue: "https://*/*",
820
+ validate(val) {
821
+ const res = parseAndValidateMatches(val || "");
822
+ return res.valid ? void 0 : res.error;
823
+ }
824
+ })
825
+ );
826
+ const parsed = parseAndValidateMatches(matchesInput);
827
+ if (parsed.valid) {
828
+ matches = parsed.matches;
829
+ } else {
830
+ matches = ["https://*/*"];
831
+ }
832
+ }
833
+ }
834
+ let runAt = args.runAt || "document_idle";
835
+ if (!args.runAt) {
836
+ if (useDefaults) {
837
+ runAt = "document_idle";
838
+ } else if (!isTTY) {
839
+ throw new Error("Missing --run-at flag in non-interactive environment.");
840
+ } else {
841
+ runAt = handleCancel(
842
+ await p.select({
843
+ message: "Content script run timing:",
844
+ options: [
845
+ { value: "document_idle", label: "document_idle", hint: "Default, after page finishes loading" },
846
+ { value: "document_start", label: "document_start", hint: "Before any DOM is created" },
847
+ { value: "document_end", label: "document_end", hint: "Immediately after DOM is parsed" }
848
+ ],
849
+ initialValue: "document_idle"
850
+ })
851
+ );
852
+ }
853
+ }
854
+ let world = args.world || "ISOLATED";
855
+ if (!anyMv2 && args.world === void 0) {
856
+ if (useDefaults) {
857
+ world = "ISOLATED";
858
+ } else if (!isTTY) {
859
+ world = "ISOLATED";
860
+ } else {
861
+ world = handleCancel(
862
+ await p.select({
863
+ message: "Content script execution world:",
864
+ options: [
865
+ { value: "ISOLATED", label: "ISOLATED", hint: "Default, isolated JS execution space" },
866
+ { value: "MAIN", label: "MAIN", hint: "Share DOM and JS context with page scripts" }
867
+ ],
868
+ initialValue: "ISOLATED"
869
+ })
870
+ );
871
+ }
872
+ }
873
+ let css = args.css ?? true;
874
+ if (args.css === void 0) {
875
+ if (useDefaults) {
876
+ css = true;
877
+ } else if (!isTTY) {
878
+ css = true;
879
+ } else {
880
+ css = handleCancel(
881
+ await p.confirm({
882
+ message: "Create CSS style file for content script?",
883
+ initialValue: true
884
+ })
885
+ );
886
+ }
887
+ }
888
+ contentScriptOptions = {
889
+ matches,
890
+ runAt,
891
+ world,
892
+ css
893
+ };
894
+ }
895
+ let extras = [];
896
+ if (projectType === "full") {
897
+ if (args.extras !== void 0) {
898
+ extras = args.extras;
899
+ } else if (useDefaults) {
900
+ extras = [];
901
+ } else if (!isTTY) {
902
+ extras = [];
903
+ } else {
904
+ const extraOptions = [];
905
+ const hasBackground = features.includes("background");
906
+ if (hasBackground) {
907
+ extraOptions.push({
908
+ value: "contextmenu",
909
+ label: "Context menu sample",
910
+ hint: "contextMenus permission + listener"
911
+ });
912
+ extraOptions.push({
913
+ value: "commands",
914
+ label: "Keyboard shortcut sample",
915
+ hint: "commands manifest entry + listener"
916
+ });
917
+ extraOptions.push({
918
+ value: "omnibox",
919
+ label: "Omnibox address bar keyword sample",
920
+ hint: "omnibox keyword + handler"
921
+ });
922
+ }
923
+ extraOptions.push({
924
+ value: "storage",
925
+ label: "Storage helper library",
926
+ hint: "Type-safe local storage wrapper in src/lib/storage"
927
+ });
928
+ if (extraOptions.length > 0) {
929
+ extras = handleCancel(
930
+ await p.multiselect({
931
+ message: "Select optional extra samples:",
932
+ options: extraOptions,
933
+ initialValues: [],
934
+ required: false
935
+ })
936
+ );
937
+ }
938
+ }
939
+ }
940
+ const detectedPm = detectPackageManager({
941
+ flag: args.pm,
942
+ targetDir: resolvedTargetDir
943
+ });
944
+ let chosenPm = detectedPm.name;
945
+ if (!args.pm && !detectedPm.confident && isTTY && !useDefaults) {
946
+ chosenPm = handleCancel(
947
+ await p.select({
948
+ message: "Select package manager to use:",
949
+ options: [
950
+ { value: "npm", label: "npm" },
951
+ { value: "pnpm", label: "pnpm" },
952
+ { value: "yarn", label: "yarn" },
953
+ { value: "bun", label: "bun" }
954
+ ],
955
+ initialValue: detectedPm.name
956
+ })
957
+ );
958
+ } else {
959
+ p.log.info(`Using ${chosenPm} (detected from ${formatPmSource(detectedPm.source)})`);
960
+ }
961
+ let install = args.install ?? true;
962
+ if (args.install === void 0) {
963
+ if (useDefaults || !isTTY) {
964
+ install = true;
965
+ } else {
966
+ install = handleCancel(
967
+ await p.confirm({
968
+ message: "Install dependencies now?",
969
+ initialValue: true
970
+ })
971
+ );
972
+ }
973
+ }
974
+ let git = args.git ?? true;
975
+ const alreadyInGit = isInsideGitRepo(resolvedTargetDir);
976
+ if (args.git === void 0) {
977
+ if (alreadyInGit) {
978
+ git = false;
979
+ } else if (useDefaults || !isTTY) {
980
+ git = true;
981
+ } else {
982
+ git = handleCancel(
983
+ await p.confirm({
984
+ message: "Initialize a new git repository?",
985
+ initialValue: true
986
+ })
987
+ );
988
+ }
989
+ } else if (git && alreadyInGit) {
990
+ p.log.warn("Target directory is already inside a git repository; skipping git init.");
991
+ git = false;
992
+ }
993
+ return {
994
+ projectName: projectName || "my-extension",
995
+ targetDir: resolvedTargetDir,
996
+ ifExists,
997
+ language,
998
+ projectType,
999
+ browsers,
1000
+ firefoxMv,
1001
+ safariMv,
1002
+ features,
1003
+ contentScript: contentScriptOptions,
1004
+ extras,
1005
+ pm: chosenPm,
1006
+ install,
1007
+ git,
1008
+ isInteractive: isTTY && !useDefaults
1009
+ };
1010
+ }
1011
+
1012
+ // src/generate/index.ts
1013
+ import fs5 from "fs";
1014
+ import path6 from "path";
1015
+ import { fileURLToPath } from "url";
1016
+
1017
+ // src/generate/targets.ts
1018
+ var CAPABILITY_MATRIX = {
1019
+ "chrome-mv3": {
1020
+ supportsBackgroundServiceWorker: true,
1021
+ supportsBackgroundScripts: false,
1022
+ popupKey: "action",
1023
+ supportsSidePanel: true,
1024
+ sidePanelType: "side_panel",
1025
+ supportsDnr: true,
1026
+ supportsDevtools: true,
1027
+ supportsNewtab: true,
1028
+ supportsWorldMain: true
1029
+ },
1030
+ "firefox-mv3": {
1031
+ supportsBackgroundServiceWorker: false,
1032
+ supportsBackgroundScripts: true,
1033
+ popupKey: "action",
1034
+ supportsSidePanel: true,
1035
+ sidePanelType: "sidebar_action",
1036
+ supportsDnr: true,
1037
+ supportsDevtools: true,
1038
+ supportsNewtab: true,
1039
+ supportsWorldMain: true
1040
+ },
1041
+ "firefox-mv2": {
1042
+ supportsBackgroundServiceWorker: false,
1043
+ supportsBackgroundScripts: true,
1044
+ popupKey: "browser_action",
1045
+ supportsSidePanel: true,
1046
+ sidePanelType: "sidebar_action",
1047
+ supportsDnr: false,
1048
+ supportsDevtools: true,
1049
+ supportsNewtab: true,
1050
+ supportsWorldMain: false
1051
+ },
1052
+ "safari-mv3": {
1053
+ supportsBackgroundServiceWorker: true,
1054
+ supportsBackgroundScripts: false,
1055
+ popupKey: "action",
1056
+ supportsSidePanel: false,
1057
+ supportsDnr: true,
1058
+ supportsDevtools: false,
1059
+ supportsNewtab: false,
1060
+ supportsWorldMain: true
1061
+ },
1062
+ "safari-mv2": {
1063
+ supportsBackgroundServiceWorker: false,
1064
+ supportsBackgroundScripts: true,
1065
+ popupKey: "browser_action",
1066
+ supportsSidePanel: false,
1067
+ supportsDnr: false,
1068
+ supportsDevtools: false,
1069
+ supportsNewtab: false,
1070
+ supportsWorldMain: false
1071
+ }
1072
+ };
1073
+ function resolveTargets(browsers, firefoxMv = 3, safariMv = 3) {
1074
+ const targets = [];
1075
+ for (const b of browsers) {
1076
+ if (b === "chrome") {
1077
+ targets.push({
1078
+ id: "chrome-mv3",
1079
+ browser: "chrome",
1080
+ mv: 3,
1081
+ outDir: "dist/chrome-mv3"
1082
+ });
1083
+ } else if (b === "firefox") {
1084
+ const mv = firefoxMv || 3;
1085
+ targets.push({
1086
+ id: `firefox-mv${mv}`,
1087
+ browser: "firefox",
1088
+ mv,
1089
+ outDir: `dist/firefox-mv${mv}`
1090
+ });
1091
+ } else if (b === "safari") {
1092
+ const mv = safariMv || 3;
1093
+ targets.push({
1094
+ id: `safari-mv${mv}`,
1095
+ browser: "safari",
1096
+ mv,
1097
+ outDir: `dist/safari-mv${mv}`
1098
+ });
1099
+ }
1100
+ }
1101
+ return targets;
1102
+ }
1103
+
1104
+ // src/generate/manifest.ts
1105
+ function buildManifest(ctx, target) {
1106
+ const cap = CAPABILITY_MATRIX[target.id] || {
1107
+ supportsBackgroundServiceWorker: true,
1108
+ supportsBackgroundScripts: false,
1109
+ popupKey: "action",
1110
+ supportsSidePanel: false,
1111
+ supportsDnr: target.mv === 3,
1112
+ supportsDevtools: false,
1113
+ supportsNewtab: false,
1114
+ supportsWorldMain: target.mv === 3
1115
+ };
1116
+ const ext = ctx.language === "ts" ? "ts" : "js";
1117
+ const slug = toSlug(ctx.projectName);
1118
+ const manifest = {
1119
+ manifest_version: target.mv,
1120
+ name: ctx.projectName,
1121
+ version: "0.0.1",
1122
+ description: `${ctx.projectName} browser extension`,
1123
+ icons: {
1124
+ "16": "icons/16.png",
1125
+ "48": "icons/48.png",
1126
+ "128": "icons/128.png"
1127
+ }
1128
+ };
1129
+ if (ctx.projectType === "content-only") {
1130
+ const matches = ctx.contentScript?.matches?.length ? ctx.contentScript.matches : ["https://*/*"];
1131
+ const contentEntry = {
1132
+ matches,
1133
+ js: [`src/content/index.${ext}`],
1134
+ run_at: ctx.contentScript?.runAt || "document_idle"
1135
+ };
1136
+ if (ctx.contentScript?.css) {
1137
+ contentEntry.css = ["src/content/style.css"];
1138
+ }
1139
+ if (target.mv === 3 && cap.supportsWorldMain) {
1140
+ contentEntry.world = ctx.contentScript?.world || "ISOLATED";
1141
+ }
1142
+ manifest.content_scripts = [contentEntry];
1143
+ if (target.browser === "firefox") {
1144
+ manifest.browser_specific_settings = {
1145
+ gecko: {
1146
+ id: `${slug}@example.com`,
1147
+ strict_min_version: target.mv === 3 ? "109.0" : "58.0"
1148
+ }
1149
+ };
1150
+ }
1151
+ return manifest;
1152
+ }
1153
+ const permissions = [];
1154
+ if (ctx.features.includes("background")) {
1155
+ if (target.mv === 3) {
1156
+ if (cap.supportsBackgroundScripts) {
1157
+ manifest.background = {
1158
+ scripts: [`src/background/index.${ext}`]
1159
+ };
1160
+ } else {
1161
+ manifest.background = {
1162
+ service_worker: `src/background/index.${ext}`,
1163
+ type: "module"
1164
+ };
1165
+ }
1166
+ } else {
1167
+ manifest.background = {
1168
+ scripts: [`src/background/index.${ext}`],
1169
+ persistent: false
1170
+ };
1171
+ }
1172
+ }
1173
+ if (ctx.features.includes("popup")) {
1174
+ if (cap.popupKey === "action") {
1175
+ manifest.action = {
1176
+ default_popup: "src/popup/index.html"
1177
+ };
1178
+ } else {
1179
+ manifest.browser_action = {
1180
+ default_popup: "src/popup/index.html"
1181
+ };
1182
+ }
1183
+ }
1184
+ if (ctx.features.includes("options")) {
1185
+ if (target.mv === 3) {
1186
+ manifest.options_ui = {
1187
+ page: "src/options/index.html",
1188
+ open_in_tab: true
1189
+ };
1190
+ } else {
1191
+ manifest.options_ui = {
1192
+ page: "src/options/index.html",
1193
+ chrome_style: true
1194
+ };
1195
+ }
1196
+ }
1197
+ if (ctx.features.includes("sidepanel") && cap.supportsSidePanel) {
1198
+ if (cap.sidePanelType === "side_panel") {
1199
+ manifest.side_panel = {
1200
+ default_path: "src/sidepanel/index.html"
1201
+ };
1202
+ permissions.push("sidePanel");
1203
+ } else if (cap.sidePanelType === "sidebar_action") {
1204
+ manifest.sidebar_action = {
1205
+ default_panel: "src/sidepanel/index.html"
1206
+ };
1207
+ }
1208
+ }
1209
+ if (ctx.features.includes("devtools") && cap.supportsDevtools) {
1210
+ manifest.devtools_page = "src/devtools/devtools.html";
1211
+ }
1212
+ if (ctx.features.includes("newtab") && cap.supportsNewtab) {
1213
+ manifest.chrome_url_overrides = {
1214
+ newtab: "src/newtab/index.html"
1215
+ };
1216
+ }
1217
+ if (ctx.features.includes("content")) {
1218
+ const matches = ctx.contentScript?.matches?.length ? ctx.contentScript.matches : ["https://*/*"];
1219
+ const contentEntry = {
1220
+ matches,
1221
+ js: [`src/content/index.${ext}`],
1222
+ run_at: ctx.contentScript?.runAt || "document_idle"
1223
+ };
1224
+ if (ctx.contentScript?.css) {
1225
+ contentEntry.css = ["src/content/style.css"];
1226
+ }
1227
+ if (target.mv === 3 && cap.supportsWorldMain) {
1228
+ contentEntry.world = ctx.contentScript?.world || "ISOLATED";
1229
+ }
1230
+ manifest.content_scripts = [contentEntry];
1231
+ }
1232
+ if (ctx.features.includes("dnr") && cap.supportsDnr && target.mv === 3) {
1233
+ manifest.declarative_net_request = {
1234
+ rule_resources: [
1235
+ {
1236
+ id: "ruleset_1",
1237
+ enabled: true,
1238
+ path: "rules/rules.json"
1239
+ }
1240
+ ]
1241
+ };
1242
+ permissions.push("declarativeNetRequest");
1243
+ manifest.host_permissions = ["<all_urls>"];
1244
+ }
1245
+ if (ctx.extras.includes("storage")) {
1246
+ permissions.push("storage");
1247
+ }
1248
+ if (ctx.features.includes("background")) {
1249
+ if (ctx.extras.includes("contextmenu")) {
1250
+ permissions.push("contextMenus");
1251
+ }
1252
+ if (ctx.extras.includes("commands")) {
1253
+ manifest.commands = {
1254
+ _execute_action: {
1255
+ suggested_key: {
1256
+ default: "Ctrl+Shift+Y",
1257
+ mac: "Command+Shift+Y"
1258
+ },
1259
+ description: "Toggle extension action"
1260
+ }
1261
+ };
1262
+ }
1263
+ if (ctx.extras.includes("omnibox")) {
1264
+ manifest.omnibox = {
1265
+ keyword: slug.slice(0, 10)
1266
+ };
1267
+ }
1268
+ }
1269
+ if (permissions.length > 0) {
1270
+ manifest.permissions = permissions;
1271
+ }
1272
+ if (target.browser === "firefox") {
1273
+ manifest.browser_specific_settings = {
1274
+ gecko: {
1275
+ id: `${slug}@example.com`,
1276
+ strict_min_version: target.mv === 3 ? "109.0" : "58.0"
1277
+ }
1278
+ };
1279
+ }
1280
+ return manifest;
1281
+ }
1282
+
1283
+ // src/generate/pkgjson.ts
1284
+ function buildPackageJson(ctx, targets) {
1285
+ const scripts = {};
1286
+ for (const t of targets) {
1287
+ if (t.mv === 2 || t.browser === "firefox") {
1288
+ scripts[`dev:${t.browser}`] = `vite build --watch --mode ${t.id}`;
1289
+ } else {
1290
+ scripts[`dev:${t.browser}`] = `vite --mode ${t.id}`;
1291
+ }
1292
+ scripts[`build:${t.browser}`] = `vite build --mode ${t.id}`;
1293
+ if (t.browser === "firefox") {
1294
+ scripts["run:firefox"] = `web-ext run --source-dir dist/${t.id}`;
1295
+ }
1296
+ }
1297
+ const first = targets[0];
1298
+ scripts["dev"] = `npm run dev:${first.browser}`;
1299
+ scripts["build"] = `npm run build:${first.browser}`;
1300
+ if (targets.length > 1) {
1301
+ scripts["build:all"] = targets.map((t) => `npm run build:${t.browser}`).join(" && ");
1302
+ }
1303
+ scripts["typecheck"] = ctx.language === "ts" ? "tsc -b" : "tsc -p jsconfig.json --noEmit";
1304
+ const devDependencies = {
1305
+ vite: "^8.3.6",
1306
+ typescript: "^5.8.2"
1307
+ };
1308
+ const hasMv3 = targets.some((t) => t.mv === 3);
1309
+ if (hasMv3) {
1310
+ devDependencies["@crxjs/vite-plugin"] = "^3.0.0";
1311
+ }
1312
+ const hasChrome = targets.some((t) => t.browser === "chrome");
1313
+ const hasFirefox = targets.some((t) => t.browser === "firefox");
1314
+ const hasSafari = targets.some((t) => t.browser === "safari");
1315
+ if (hasChrome || hasSafari && !hasFirefox) {
1316
+ devDependencies["@types/chrome"] = "^0.3.7";
1317
+ }
1318
+ if (hasFirefox) {
1319
+ devDependencies["@types/firefox-webext-browser"] = "^143.0.1";
1320
+ devDependencies["web-ext"] = "^10.8.0";
1321
+ }
1322
+ return {
1323
+ name: ctx.projectName,
1324
+ version: "0.0.1",
1325
+ description: `${ctx.projectName} browser extension`,
1326
+ type: "module",
1327
+ scripts,
1328
+ devDependencies
1329
+ };
1330
+ }
1331
+
1332
+ // src/generate/tsconfig.ts
1333
+ function buildConfigs(ctx, targets) {
1334
+ const files = {};
1335
+ const types = [];
1336
+ const hasChrome = targets.some((t) => t.browser === "chrome");
1337
+ const hasFirefox = targets.some((t) => t.browser === "firefox");
1338
+ const hasSafari = targets.some((t) => t.browser === "safari");
1339
+ if (hasChrome || hasSafari && !hasFirefox) {
1340
+ types.push("chrome");
1341
+ }
1342
+ if (hasFirefox) {
1343
+ types.push("firefox-webext-browser");
1344
+ }
1345
+ const hasBackground = ctx.projectType === "full" && ctx.features.includes("background");
1346
+ if (ctx.language === "ts") {
1347
+ const references = [{ path: "./tsconfig.app.json" }];
1348
+ if (hasBackground) {
1349
+ references.push({ path: "./tsconfig.background.json" });
1350
+ }
1351
+ references.push({ path: "./tsconfig.node.json" });
1352
+ files["tsconfig.json"] = JSON.stringify(
1353
+ {
1354
+ files: [],
1355
+ references
1356
+ },
1357
+ null,
1358
+ 2
1359
+ );
1360
+ const appConfig = {
1361
+ compilerOptions: {
1362
+ target: "ES2022",
1363
+ lib: ["ES2022", "DOM", "DOM.Iterable"],
1364
+ module: "ESNext",
1365
+ moduleResolution: "bundler",
1366
+ strict: true,
1367
+ noEmit: true,
1368
+ isolatedModules: true,
1369
+ skipLibCheck: true,
1370
+ types
1371
+ },
1372
+ include: ["src/**/*"]
1373
+ };
1374
+ if (hasBackground) {
1375
+ appConfig.exclude = ["src/background/**/*"];
1376
+ }
1377
+ files["tsconfig.app.json"] = JSON.stringify(appConfig, null, 2);
1378
+ if (hasBackground) {
1379
+ const bgLib = ["ES2022", "WebWorker"];
1380
+ if (hasFirefox) {
1381
+ bgLib.push("DOM");
1382
+ }
1383
+ files["tsconfig.background.json"] = JSON.stringify(
1384
+ {
1385
+ compilerOptions: {
1386
+ target: "ES2022",
1387
+ lib: bgLib,
1388
+ module: "ESNext",
1389
+ moduleResolution: "bundler",
1390
+ strict: true,
1391
+ noEmit: true,
1392
+ isolatedModules: true,
1393
+ skipLibCheck: true,
1394
+ types
1395
+ },
1396
+ include: ["src/background/**/*", "src/lib/**/*"]
1397
+ },
1398
+ null,
1399
+ 2
1400
+ );
1401
+ }
1402
+ files["tsconfig.node.json"] = JSON.stringify(
1403
+ {
1404
+ compilerOptions: {
1405
+ target: "ES2022",
1406
+ lib: ["ES2022"],
1407
+ module: "ESNext",
1408
+ moduleResolution: "bundler",
1409
+ strict: true,
1410
+ noEmit: true,
1411
+ isolatedModules: true,
1412
+ skipLibCheck: true
1413
+ },
1414
+ include: [
1415
+ "vite.config.ts",
1416
+ "manifest.config.ts",
1417
+ "targets.config.ts",
1418
+ "build/**/*"
1419
+ ]
1420
+ },
1421
+ null,
1422
+ 2
1423
+ );
1424
+ } else {
1425
+ files["jsconfig.json"] = JSON.stringify(
1426
+ {
1427
+ compilerOptions: {
1428
+ target: "ES2022",
1429
+ lib: ["ES2022", "DOM", "DOM.Iterable"],
1430
+ module: "ESNext",
1431
+ moduleResolution: "bundler",
1432
+ checkJs: true,
1433
+ noEmit: true,
1434
+ types
1435
+ },
1436
+ include: [
1437
+ "src/**/*",
1438
+ "vite.config.js",
1439
+ "manifest.config.js",
1440
+ "targets.config.js",
1441
+ "build/**/*"
1442
+ ]
1443
+ },
1444
+ null,
1445
+ 2
1446
+ );
1447
+ }
1448
+ const hasMv3 = targets.some((t) => t.mv === 3);
1449
+ const envDtsLines = ['/// <reference types="vite/client" />'];
1450
+ if (hasMv3) {
1451
+ envDtsLines.push('/// <reference types="@crxjs/vite-plugin/client" />');
1452
+ }
1453
+ files["src/env.d.ts"] = envDtsLines.join("\n") + "\n";
1454
+ return files;
1455
+ }
1456
+
1457
+ // src/generate/copy.ts
1458
+ import fs4 from "fs";
1459
+ import path5 from "path";
1460
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".ico", ".webp", ".woff", ".woff2", ".ttf"]);
1461
+ function copyDirectory(sourceDir, targetDir, replacements = {}) {
1462
+ if (!fs4.existsSync(sourceDir)) {
1463
+ return;
1464
+ }
1465
+ const entries = fs4.readdirSync(sourceDir, { withFileTypes: true });
1466
+ for (const entry of entries) {
1467
+ const srcPath = path5.join(sourceDir, entry.name);
1468
+ const targetName = entry.name === "_gitignore" ? ".gitignore" : entry.name;
1469
+ const destPath = path5.join(targetDir, targetName);
1470
+ if (entry.isDirectory()) {
1471
+ fs4.mkdirSync(destPath, { recursive: true });
1472
+ copyDirectory(srcPath, destPath, replacements);
1473
+ } else if (entry.isFile()) {
1474
+ fs4.mkdirSync(path5.dirname(destPath), { recursive: true });
1475
+ const ext = path5.extname(entry.name).toLowerCase();
1476
+ if (BINARY_EXTENSIONS.has(ext)) {
1477
+ fs4.copyFileSync(srcPath, destPath);
1478
+ } else {
1479
+ let content = fs4.readFileSync(srcPath, "utf-8");
1480
+ for (const [key, val] of Object.entries(replacements)) {
1481
+ const placeholder = new RegExp(`\\{\\{${key}\\}\\}`, "g");
1482
+ content = content.replace(placeholder, val);
1483
+ }
1484
+ fs4.writeFileSync(destPath, content, "utf-8");
1485
+ }
1486
+ }
1487
+ }
1488
+ }
1489
+
1490
+ // src/generate/index.ts
1491
+ var __filename = fileURLToPath(import.meta.url);
1492
+ var __dirname = path6.dirname(__filename);
1493
+ function getTemplatesDir() {
1494
+ const candidate1 = path6.resolve(__dirname, "../../templates");
1495
+ if (fs5.existsSync(candidate1)) {
1496
+ return candidate1;
1497
+ }
1498
+ const candidate2 = path6.resolve(__dirname, "../templates");
1499
+ if (fs5.existsSync(candidate2)) {
1500
+ return candidate2;
1501
+ }
1502
+ return path6.resolve(process.cwd(), "templates");
1503
+ }
1504
+ function buildProjectReadme(ctx, targets) {
1505
+ const pm = ctx.pm;
1506
+ const devCmd = getRunCommand(pm, "dev");
1507
+ const buildCmd = getRunCommand(pm, "build");
1508
+ const firstTarget = targets[0];
1509
+ let loadInstructions = "";
1510
+ for (const t of targets) {
1511
+ if (t.browser === "chrome") {
1512
+ loadInstructions += `
1513
+ ### Chromium (Chrome, Edge, Brave, Opera)
1514
+ 1. Open \`chrome://extensions\` (or equivalent in your browser)
1515
+ 2. Enable **Developer mode** toggle in the top-right corner
1516
+ 3. Click **Load unpacked** and select the \`${t.outDir}\` directory
1517
+ `;
1518
+ } else if (t.browser === "firefox") {
1519
+ loadInstructions += `
1520
+ ### Firefox
1521
+ 1. Open \`about:debugging#/runtime/this-firefox\`
1522
+ 2. Click **Load Temporary Add-on...**
1523
+ 3. Select the \`manifest.json\` file inside \`${t.outDir}\`
1524
+ *(Alternatively, run \`${getRunCommand(pm, "run:firefox")}\`)*
1525
+ `;
1526
+ } else if (t.browser === "safari") {
1527
+ loadInstructions += `
1528
+ ### Safari
1529
+ Safari requires converting the built extension with Xcode:
1530
+ \`\`\`bash
1531
+ xcrun safari-web-extension-converter ${t.outDir}
1532
+ \`\`\`
1533
+ Then open and run the generated Xcode project.
1534
+ `;
1535
+ }
1536
+ }
1537
+ const scriptList = Object.entries(buildPackageJson(ctx, targets).scripts).map(([name, cmd]) => `- \`${getRunCommand(pm, name)}\`: \`${cmd}\``).join("\n");
1538
+ return `# ${ctx.projectName}
1539
+
1540
+ ${ctx.projectName} browser extension built with Vite and CRXJS.
1541
+
1542
+ ## Getting Started
1543
+
1544
+ ### Development
1545
+ \`\`\`bash
1546
+ ${devCmd}
1547
+ \`\`\`
1548
+
1549
+ ### Production Build
1550
+ \`\`\`bash
1551
+ ${buildCmd}
1552
+ \`\`\`
1553
+
1554
+ ## Available Scripts
1555
+ ${scriptList}
1556
+
1557
+ ## Loading the Extension in Your Browser
1558
+ ${loadInstructions}
1559
+
1560
+ ## Before Publishing
1561
+ - Update the extension name, description, and icons in \`public/icons/\`.
1562
+ ${targets.some((t) => t.browser === "firefox") ? "- Customize the Firefox Gecko ID in `manifest.config` (default is placeholder `@example.com`).\n" : ""}${ctx.features.includes("dnr") ? "- Review the default rules in `public/rules/rules.json` and narrow `host_permissions` in the manifest.\n" : ""}
1563
+ `;
1564
+ }
1565
+ function generateProject(ctx) {
1566
+ const targetDir = path6.resolve(ctx.targetDir);
1567
+ const templatesDir = getTemplatesDir();
1568
+ const slug = toSlug(ctx.projectName);
1569
+ const targets = resolveTargets(ctx.browsers, ctx.firefoxMv, ctx.safariMv);
1570
+ const hasMv2 = targets.some((t) => t.mv === 2);
1571
+ const ext = ctx.language === "ts" ? "ts" : "js";
1572
+ const replacements = {
1573
+ name: ctx.projectName,
1574
+ description: `${ctx.projectName} browser extension`,
1575
+ slug,
1576
+ pm: ctx.pm
1577
+ };
1578
+ fs5.mkdirSync(targetDir, { recursive: true });
1579
+ const baseDir = path6.join(templatesDir, "base");
1580
+ copyDirectory(baseDir, targetDir, replacements);
1581
+ const langDir = path6.join(templatesDir, ctx.language);
1582
+ copyDirectory(langDir, targetDir, replacements);
1583
+ if (ctx.projectType === "content-only") {
1584
+ const contentDir = path6.join(templatesDir, "features", "content", "content-only", ctx.language);
1585
+ copyDirectory(contentDir, targetDir, replacements);
1586
+ if (ctx.contentScript?.css) {
1587
+ const styleSrc = path6.join(templatesDir, "features", "content", "style.css");
1588
+ const styleDest = path6.join(targetDir, "src", "content", "style.css");
1589
+ fs5.mkdirSync(path6.dirname(styleDest), { recursive: true });
1590
+ fs5.copyFileSync(styleSrc, styleDest);
1591
+ }
1592
+ } else {
1593
+ if (ctx.features.includes("background")) {
1594
+ const bgDir = path6.join(templatesDir, "features", "background", ctx.language);
1595
+ copyDirectory(bgDir, targetDir, replacements);
1596
+ const bgFile = path6.join(targetDir, "src", "background", `index.${ext}`);
1597
+ let bgContent = fs5.readFileSync(bgFile, "utf-8");
1598
+ if (ctx.extras.includes("contextmenu")) {
1599
+ bgContent += `
1600
+ api.contextMenus?.create?.({
1601
+ id: '${slug}-menu',
1602
+ title: '${ctx.projectName} Action',
1603
+ contexts: ['all'],
1604
+ });
1605
+
1606
+ api.contextMenus?.onClicked?.addListener((info, tab) => {
1607
+ console.log('Context menu clicked:', info.menuItemId, 'tab:', tab?.id);
1608
+ });
1609
+ `;
1610
+ }
1611
+ if (ctx.extras.includes("commands")) {
1612
+ bgContent += `
1613
+ api.commands?.onCommand?.addListener((command) => {
1614
+ console.log('Command received:', command);
1615
+ });
1616
+ `;
1617
+ }
1618
+ if (ctx.extras.includes("omnibox")) {
1619
+ bgContent += `
1620
+ api.omnibox?.onInputEntered?.addListener((text, disposition) => {
1621
+ console.log('Omnibox search:', text, 'disposition:', disposition);
1622
+ });
1623
+ `;
1624
+ }
1625
+ fs5.writeFileSync(bgFile, bgContent, "utf-8");
1626
+ }
1627
+ if (ctx.features.includes("content")) {
1628
+ const contentDir = path6.join(templatesDir, "features", "content", "full", ctx.language);
1629
+ copyDirectory(contentDir, targetDir, replacements);
1630
+ if (ctx.contentScript?.css) {
1631
+ const styleSrc = path6.join(templatesDir, "features", "content", "style.css");
1632
+ const styleDest = path6.join(targetDir, "src", "content", "style.css");
1633
+ fs5.mkdirSync(path6.dirname(styleDest), { recursive: true });
1634
+ fs5.copyFileSync(styleSrc, styleDest);
1635
+ }
1636
+ }
1637
+ if (ctx.features.includes("popup")) {
1638
+ const popupDir = path6.join(templatesDir, "features", "popup", ctx.language);
1639
+ copyDirectory(popupDir, targetDir, replacements);
1640
+ }
1641
+ if (ctx.features.includes("options")) {
1642
+ const optionsDir = path6.join(templatesDir, "features", "options", ctx.language);
1643
+ copyDirectory(optionsDir, targetDir, replacements);
1644
+ }
1645
+ if (ctx.features.includes("sidepanel")) {
1646
+ const sidepanelDir = path6.join(templatesDir, "features", "sidepanel", ctx.language);
1647
+ copyDirectory(sidepanelDir, targetDir, replacements);
1648
+ }
1649
+ if (ctx.features.includes("devtools")) {
1650
+ const devtoolsDir = path6.join(templatesDir, "features", "devtools", ctx.language);
1651
+ copyDirectory(devtoolsDir, targetDir, replacements);
1652
+ }
1653
+ if (ctx.features.includes("newtab")) {
1654
+ const newtabDir = path6.join(templatesDir, "features", "newtab", ctx.language);
1655
+ copyDirectory(newtabDir, targetDir, replacements);
1656
+ }
1657
+ if (ctx.features.includes("dnr")) {
1658
+ const dnrSrc = path6.join(templatesDir, "features", "dnr", "rules.json");
1659
+ const dnrDest = path6.join(targetDir, "public", "rules", "rules.json");
1660
+ fs5.mkdirSync(path6.dirname(dnrDest), { recursive: true });
1661
+ fs5.copyFileSync(dnrSrc, dnrDest);
1662
+ }
1663
+ if (ctx.extras.includes("storage")) {
1664
+ const storageDir = path6.join(templatesDir, "features", "extras", "storage", ctx.language);
1665
+ copyDirectory(storageDir, targetDir, replacements);
1666
+ }
1667
+ }
1668
+ const targetsConfigContent = `export interface TargetConfig {
1669
+ id: string;
1670
+ browser: 'chrome' | 'firefox' | 'safari';
1671
+ mv: 2 | 3;
1672
+ outDir: string;
1673
+ }
1674
+
1675
+ export const targets: TargetConfig[] = ${JSON.stringify(targets, null, 2)};
1676
+
1677
+ export function getTarget(mode: string): TargetConfig {
1678
+ const found = targets.find((t) => t.id === mode);
1679
+ return found || targets[0];
1680
+ }
1681
+ `;
1682
+ fs5.writeFileSync(
1683
+ path6.join(targetDir, `targets.config.${ext}`),
1684
+ ctx.language === "ts" ? targetsConfigContent : targetsConfigContent.replace(/export interface TargetConfig \{[\s\S]*?\}\n\n/, "").replace(/: TargetConfig\[\]/g, "").replace(/: string/g, "").replace(/: TargetConfig/g, ""),
1685
+ "utf-8"
1686
+ );
1687
+ const manifestMap = {};
1688
+ for (const t of targets) {
1689
+ manifestMap[t.id] = buildManifest(ctx, t);
1690
+ }
1691
+ const manifestConfigContent = ctx.language === "ts" ? `import type { TargetConfig } from './targets.config';
1692
+
1693
+ const manifests: Record<string, any> = ${JSON.stringify(manifestMap, null, 2)};
1694
+
1695
+ export function buildManifest(target: TargetConfig): any {
1696
+ return manifests[target.id] || manifests['${targets[0].id}'];
1697
+ }
1698
+ ` : `const manifests = ${JSON.stringify(manifestMap, null, 2)};
1699
+
1700
+ export function buildManifest(target) {
1701
+ return manifests[target.id] || manifests['${targets[0].id}'];
1702
+ }
1703
+ `;
1704
+ fs5.writeFileSync(path6.join(targetDir, `manifest.config.${ext}`), manifestConfigContent, "utf-8");
1705
+ if (hasMv2) {
1706
+ const mv2Dir = path6.join(targetDir, "build");
1707
+ fs5.mkdirSync(mv2Dir, { recursive: true });
1708
+ const mv2Content = ctx.language === "ts" ? `import type { Plugin } from 'vite';
1709
+ import { build as viteBuild } from 'vite';
1710
+ import fs from 'node:fs';
1711
+ import path from 'node:path';
1712
+
1713
+ export function buildMv2Plugin(options: { target: any; manifest: any }): Plugin {
1714
+ const { manifest } = options;
1715
+ let config: any;
1716
+
1717
+ return {
1718
+ name: 'web-extension:mv2-builder',
1719
+ configResolved(resolvedConfig) {
1720
+ config = resolvedConfig;
1721
+ },
1722
+ async writeBundle() {
1723
+ const outDir = config.build.outDir;
1724
+ const root = config.root || process.cwd();
1725
+ const mv2Manifest = JSON.parse(JSON.stringify(manifest));
1726
+
1727
+ if (manifest.background?.scripts?.length) {
1728
+ const outScripts: string[] = [];
1729
+ for (let i = 0; i < manifest.background.scripts.length; i++) {
1730
+ const entryPath = path.resolve(root, manifest.background.scripts[i]);
1731
+ const outName = 'background.js';
1732
+ await viteBuild({
1733
+ configFile: false,
1734
+ root,
1735
+ build: {
1736
+ lib: {
1737
+ entry: entryPath,
1738
+ formats: ['iife'],
1739
+ name: 'ExtensionBackground',
1740
+ fileName: () => outName,
1741
+ },
1742
+ outDir,
1743
+ emptyOutDir: false,
1744
+ sourcemap: config.build.sourcemap,
1745
+ },
1746
+ });
1747
+ outScripts.push(outName);
1748
+ }
1749
+ mv2Manifest.background.scripts = outScripts;
1750
+ }
1751
+
1752
+ if (manifest.content_scripts?.length) {
1753
+ for (let sIdx = 0; sIdx < manifest.content_scripts.length; sIdx++) {
1754
+ const cs = manifest.content_scripts[sIdx];
1755
+ if (cs.js?.length) {
1756
+ const outJs: string[] = [];
1757
+ for (let jIdx = 0; jIdx < cs.js.length; jIdx++) {
1758
+ const entryPath = path.resolve(root, cs.js[jIdx]);
1759
+ const outName = \`content-\${sIdx}-\${jIdx}.js\`;
1760
+ await viteBuild({
1761
+ configFile: false,
1762
+ root,
1763
+ build: {
1764
+ lib: {
1765
+ entry: entryPath,
1766
+ formats: ['iife'],
1767
+ name: \`ExtensionContent_\${sIdx}_\${jIdx}\`,
1768
+ fileName: () => outName,
1769
+ },
1770
+ outDir,
1771
+ emptyOutDir: false,
1772
+ sourcemap: config.build.sourcemap,
1773
+ },
1774
+ });
1775
+ outJs.push(outName);
1776
+ }
1777
+ mv2Manifest.content_scripts[sIdx].js = outJs;
1778
+ }
1779
+
1780
+ if (cs.css?.length) {
1781
+ const outCss: string[] = [];
1782
+ for (let cIdx = 0; cIdx < cs.css.length; cIdx++) {
1783
+ const srcCss = path.resolve(root, cs.css[cIdx]);
1784
+ const outCssName = \`content-\${sIdx}-\${cIdx}.css\`;
1785
+ if (fs.existsSync(srcCss)) {
1786
+ fs.copyFileSync(srcCss, path.join(outDir, outCssName));
1787
+ outCss.push(outCssName);
1788
+ }
1789
+ }
1790
+ mv2Manifest.content_scripts[sIdx].css = outCss;
1791
+ }
1792
+ }
1793
+ }
1794
+
1795
+ const iconsDir = path.resolve(root, 'public', 'icons');
1796
+ if (fs.existsSync(iconsDir)) {
1797
+ const outIcons = path.join(outDir, 'icons');
1798
+ fs.mkdirSync(outIcons, { recursive: true });
1799
+ for (const icon of fs.readdirSync(iconsDir)) {
1800
+ fs.copyFileSync(path.join(iconsDir, icon), path.join(outIcons, icon));
1801
+ }
1802
+ }
1803
+
1804
+ fs.writeFileSync(
1805
+ path.join(outDir, 'manifest.json'),
1806
+ JSON.stringify(mv2Manifest, null, 2),
1807
+ );
1808
+ },
1809
+ };
1810
+ }
1811
+ ` : `import { build as viteBuild } from 'vite';
1812
+ import fs from 'node:fs';
1813
+ import path from 'node:path';
1814
+
1815
+ export function buildMv2Plugin(options) {
1816
+ const { manifest } = options;
1817
+ let config;
1818
+
1819
+ return {
1820
+ name: 'web-extension:mv2-builder',
1821
+ configResolved(resolvedConfig) {
1822
+ config = resolvedConfig;
1823
+ },
1824
+ async writeBundle() {
1825
+ const outDir = config.build.outDir;
1826
+ const root = config.root || process.cwd();
1827
+ const mv2Manifest = JSON.parse(JSON.stringify(manifest));
1828
+
1829
+ if (manifest.background?.scripts?.length) {
1830
+ const outScripts = [];
1831
+ for (let i = 0; i < manifest.background.scripts.length; i++) {
1832
+ const entryPath = path.resolve(root, manifest.background.scripts[i]);
1833
+ const outName = 'background.js';
1834
+ await viteBuild({
1835
+ configFile: false,
1836
+ root,
1837
+ build: {
1838
+ lib: {
1839
+ entry: entryPath,
1840
+ formats: ['iife'],
1841
+ name: 'ExtensionBackground',
1842
+ fileName: () => outName,
1843
+ },
1844
+ outDir,
1845
+ emptyOutDir: false,
1846
+ sourcemap: config.build.sourcemap,
1847
+ },
1848
+ });
1849
+ outScripts.push(outName);
1850
+ }
1851
+ mv2Manifest.background.scripts = outScripts;
1852
+ }
1853
+
1854
+ if (manifest.content_scripts?.length) {
1855
+ for (let sIdx = 0; sIdx < manifest.content_scripts.length; sIdx++) {
1856
+ const cs = manifest.content_scripts[sIdx];
1857
+ if (cs.js?.length) {
1858
+ const outJs = [];
1859
+ for (let jIdx = 0; jIdx < cs.js.length; jIdx++) {
1860
+ const entryPath = path.resolve(root, cs.js[jIdx]);
1861
+ const outName = \`content-\${sIdx}-\${jIdx}.js\`;
1862
+ await viteBuild({
1863
+ configFile: false,
1864
+ root,
1865
+ build: {
1866
+ lib: {
1867
+ entry: entryPath,
1868
+ formats: ['iife'],
1869
+ name: \`ExtensionContent_\${sIdx}_\${jIdx}\`,
1870
+ fileName: () => outName,
1871
+ },
1872
+ outDir,
1873
+ emptyOutDir: false,
1874
+ sourcemap: config.build.sourcemap,
1875
+ },
1876
+ });
1877
+ outJs.push(outName);
1878
+ }
1879
+ mv2Manifest.content_scripts[sIdx].js = outJs;
1880
+ }
1881
+
1882
+ if (cs.css?.length) {
1883
+ const outCss = [];
1884
+ for (let cIdx = 0; cIdx < cs.css.length; cIdx++) {
1885
+ const srcCss = path.resolve(root, cs.css[cIdx]);
1886
+ const outCssName = \`content-\${sIdx}-\${cIdx}.css\`;
1887
+ if (fs.existsSync(srcCss)) {
1888
+ fs.copyFileSync(srcCss, path.join(outDir, outCssName));
1889
+ outCss.push(outCssName);
1890
+ }
1891
+ }
1892
+ mv2Manifest.content_scripts[sIdx].css = outCss;
1893
+ }
1894
+ }
1895
+ }
1896
+
1897
+ const iconsDir = path.resolve(root, 'public', 'icons');
1898
+ if (fs.existsSync(iconsDir)) {
1899
+ const outIcons = path.join(outDir, 'icons');
1900
+ fs.mkdirSync(outIcons, { recursive: true });
1901
+ for (const icon of fs.readdirSync(iconsDir)) {
1902
+ fs.copyFileSync(path.join(iconsDir, icon), path.join(outIcons, icon));
1903
+ }
1904
+ }
1905
+
1906
+ fs.writeFileSync(
1907
+ path.join(outDir, 'manifest.json'),
1908
+ JSON.stringify(mv2Manifest, null, 2),
1909
+ );
1910
+ },
1911
+ };
1912
+ }
1913
+ `;
1914
+ fs5.writeFileSync(path6.join(mv2Dir, `mv2.${ext}`), mv2Content, "utf-8");
1915
+ }
1916
+ const hasMv3 = targets.some((t) => t.mv === 3);
1917
+ let pluginsExpression = "";
1918
+ if (hasMv3 && hasMv2) {
1919
+ pluginsExpression = `isMv3
1920
+ ? [
1921
+ crx({
1922
+ manifest,
1923
+ browser: target.browser === 'firefox' ? 'firefox' : 'chrome',
1924
+ }),
1925
+ ]
1926
+ : [buildMv2Plugin({ target, manifest })]`;
1927
+ } else if (hasMv3) {
1928
+ pluginsExpression = `[
1929
+ crx({
1930
+ manifest,
1931
+ browser: target.browser === 'firefox' ? 'firefox' : 'chrome',
1932
+ }),
1933
+ ]`;
1934
+ } else {
1935
+ pluginsExpression = `[buildMv2Plugin({ target, manifest })]`;
1936
+ }
1937
+ const viteConfigContent = `import { defineConfig } from 'vite';
1938
+ ${hasMv3 ? "import { crx } from '@crxjs/vite-plugin';" : ""}
1939
+ import { getTarget } from './targets.config';
1940
+ import { buildManifest } from './manifest.config';
1941
+ ${hasMv2 ? "import { buildMv2Plugin } from './build/mv2';" : ""}
1942
+
1943
+ function getHtmlInputs(manifest${ctx.language === "ts" ? ": any" : ""}) {
1944
+ const inputs${ctx.language === "ts" ? ": Record<string, string>" : ""} = {};
1945
+ if (manifest.action?.default_popup || manifest.browser_action?.default_popup) {
1946
+ inputs.popup = manifest.action?.default_popup || manifest.browser_action?.default_popup;
1947
+ }
1948
+ if (manifest.options_ui?.page) {
1949
+ inputs.options = manifest.options_ui.page;
1950
+ }
1951
+ if (manifest.side_panel?.default_path || manifest.sidebar_action?.default_panel) {
1952
+ inputs.sidepanel = manifest.side_panel?.default_path || manifest.sidebar_action?.default_panel;
1953
+ }
1954
+ if (manifest.devtools_page) {
1955
+ inputs.devtools = manifest.devtools_page;
1956
+ }
1957
+ if (manifest.chrome_url_overrides?.newtab) {
1958
+ inputs.newtab = manifest.chrome_url_overrides.newtab;
1959
+ }
1960
+ return inputs;
1961
+ }
1962
+
1963
+ export default defineConfig(({ mode }) => {
1964
+ const target = getTarget(mode);
1965
+ const manifest = buildManifest(target);
1966
+ const isMv3 = target.mv === 3;
1967
+
1968
+ const htmlInputs = !isMv3 ? getHtmlInputs(manifest) : {};
1969
+ const hasInputs = Object.keys(htmlInputs).length > 0;
1970
+
1971
+ return {
1972
+ build: {
1973
+ outDir: \`dist/\${target.id}\`,
1974
+ emptyOutDir: true,
1975
+ ...(!isMv3
1976
+ ? {
1977
+ rollupOptions: hasInputs
1978
+ ? { input: htmlInputs }
1979
+ : undefined,
1980
+ ...(hasInputs
1981
+ ? {}
1982
+ : {
1983
+ lib: {
1984
+ entry: manifest.content_scripts?.[0]?.js?.[0] || 'src/content/index.${ext}',
1985
+ formats: ['iife'],
1986
+ name: 'ExtensionContent',
1987
+ fileName: () => 'content-0-0.js',
1988
+ },
1989
+ }),
1990
+ }
1991
+ : {}),
1992
+ },
1993
+ plugins: ${pluginsExpression},
1994
+ };
1995
+ });
1996
+ `;
1997
+ fs5.writeFileSync(path6.join(targetDir, `vite.config.${ext}`), viteConfigContent, "utf-8");
1998
+ const pkgJson = buildPackageJson(ctx, targets);
1999
+ fs5.writeFileSync(path6.join(targetDir, "package.json"), JSON.stringify(pkgJson, null, 2), "utf-8");
2000
+ const configs = buildConfigs(ctx, targets);
2001
+ for (const [relPath, content] of Object.entries(configs)) {
2002
+ const fullPath = path6.join(targetDir, relPath);
2003
+ fs5.mkdirSync(path6.dirname(fullPath), { recursive: true });
2004
+ fs5.writeFileSync(fullPath, content, "utf-8");
2005
+ }
2006
+ const readmeContent = buildProjectReadme(ctx, targets);
2007
+ fs5.writeFileSync(path6.join(targetDir, "README.md"), readmeContent, "utf-8");
2008
+ return { targetDir, targets };
2009
+ }
2010
+
2011
+ // src/index.ts
2012
+ var __filename2 = fileURLToPath2(import.meta.url);
2013
+ var __dirname2 = path7.dirname(__filename2);
2014
+ function getCliVersion() {
2015
+ try {
2016
+ const pkgPath = path7.resolve(__dirname2, "../package.json");
2017
+ if (fs6.existsSync(pkgPath)) {
2018
+ const data = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
2019
+ return data.version || "2.0.0";
2020
+ }
2021
+ } catch {
2022
+ }
2023
+ return "2.0.0";
2024
+ }
2025
+ async function main() {
2026
+ const rawArgs = process.argv.slice(2);
2027
+ let parsedArgs;
2028
+ try {
2029
+ parsedArgs = parseCliArgs(rawArgs);
2030
+ } catch (err) {
2031
+ console.error(pico.red(`Error: ${err.message}`));
2032
+ process.exit(1);
2033
+ }
2034
+ if (parsedArgs.help) {
2035
+ printHelp();
2036
+ process.exit(0);
2037
+ }
2038
+ if (parsedArgs.version) {
2039
+ console.log(`web-extension v${getCliVersion()}`);
2040
+ process.exit(0);
2041
+ }
2042
+ try {
2043
+ validateFlagCombinations(parsedArgs);
2044
+ } catch (err) {
2045
+ console.error(pico.red(`Configuration Error: ${err.message}`));
2046
+ process.exit(1);
2047
+ }
2048
+ p2.intro(pico.bold(pico.cyan("web-extension scaffold")));
2049
+ for (const warning of parsedArgs.warnings) {
2050
+ p2.log.warn(warning);
2051
+ }
2052
+ let ctx;
2053
+ try {
2054
+ ctx = await runPrompts(parsedArgs);
2055
+ } catch (err) {
2056
+ console.error(pico.red(`Error: ${err.message}`));
2057
+ process.exit(1);
2058
+ }
2059
+ if (ctx.ifExists === "overwrite" && fs6.existsSync(ctx.targetDir)) {
2060
+ const entries = fs6.readdirSync(ctx.targetDir);
2061
+ for (const entry of entries) {
2062
+ fs6.rmSync(path7.join(ctx.targetDir, entry), { recursive: true, force: true });
2063
+ }
2064
+ }
2065
+ const s = p2.spinner();
2066
+ s.start("Generating project files...");
2067
+ try {
2068
+ generateProject(ctx);
2069
+ s.stop("Project files created successfully.");
2070
+ } catch (err) {
2071
+ s.stop("Failed to generate project files.");
2072
+ console.error(pico.red(err.message));
2073
+ process.exit(1);
2074
+ }
2075
+ if (ctx.git) {
2076
+ s.start("Initializing git repository...");
2077
+ const gitRes = await runCommand("git", ["init"], ctx.targetDir);
2078
+ if (gitRes.success) {
2079
+ s.stop("Git repository initialized.");
2080
+ } else {
2081
+ s.stop("Skipped git initialization.");
2082
+ p2.log.warn(`Git init failed: ${gitRes.error}`);
2083
+ }
2084
+ }
2085
+ if (ctx.install) {
2086
+ const pmAvailable = await isCommandAvailable(ctx.pm);
2087
+ if (!pmAvailable) {
2088
+ p2.log.warn(
2089
+ `Package manager "${ctx.pm}" is not found in PATH. Skipping dependency installation.`
2090
+ );
2091
+ p2.log.info(`Please run "${getInstallCommand(ctx.pm)}" manually.`);
2092
+ } else {
2093
+ s.start(`Installing dependencies with ${ctx.pm}...`);
2094
+ const installCmd = ctx.pm === "yarn" ? [] : ["install"];
2095
+ const installRes = await runCommand(ctx.pm, installCmd, ctx.targetDir);
2096
+ if (installRes.success) {
2097
+ s.stop("Dependencies installed successfully.");
2098
+ } else {
2099
+ s.stop("Failed to install dependencies.");
2100
+ p2.log.warn(`Installation failed: ${installRes.error}`);
2101
+ p2.log.info(`You can try running "${getInstallCommand(ctx.pm)}" manually.`);
2102
+ }
2103
+ }
2104
+ }
2105
+ const relPath = path7.relative(process.cwd(), ctx.targetDir);
2106
+ const cdInstruction = relPath && relPath !== "." ? `cd ${relPath}
2107
+ ` : "";
2108
+ const firstBrowser = ctx.browsers[0];
2109
+ const devCommand = getRunCommand(ctx.pm, `dev:${firstBrowser}`);
2110
+ p2.outro(
2111
+ pico.green(
2112
+ `Done! To get started:
2113
+
2114
+ ${pico.bold(
2115
+ `${cdInstruction}${devCommand}`
2116
+ )}
2117
+ `
2118
+ )
2119
+ );
2120
+ }
2121
+ main().catch((err) => {
2122
+ console.error(pico.red(`Fatal Error: ${err.message}`));
2123
+ process.exit(1);
2124
+ });
2125
+ //# sourceMappingURL=index.js.map