@fairfox/polly 0.1.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 (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -0
  3. package/cli/polly.ts +564 -0
  4. package/dist/background/api-client.d.ts +7 -0
  5. package/dist/background/context-menu.d.ts +7 -0
  6. package/dist/background/index.d.ts +31 -0
  7. package/dist/background/index.js +1309 -0
  8. package/dist/background/index.js.map +25 -0
  9. package/dist/background/log-store.d.ts +22 -0
  10. package/dist/background/message-router.d.ts +30 -0
  11. package/dist/background/message-router.js +1300 -0
  12. package/dist/background/message-router.js.map +24 -0
  13. package/dist/background/offscreen-manager.d.ts +10 -0
  14. package/dist/index.d.ts +18 -0
  15. package/dist/index.js +1471 -0
  16. package/dist/index.js.map +27 -0
  17. package/dist/shared/adapters/chrome/context-menus.chrome.d.ts +8 -0
  18. package/dist/shared/adapters/chrome/offscreen.chrome.d.ts +6 -0
  19. package/dist/shared/adapters/chrome/runtime.chrome.d.ts +13 -0
  20. package/dist/shared/adapters/chrome/storage.chrome.d.ts +8 -0
  21. package/dist/shared/adapters/chrome/tabs.chrome.d.ts +16 -0
  22. package/dist/shared/adapters/chrome/window.chrome.d.ts +6 -0
  23. package/dist/shared/adapters/context-menus.adapter.d.ts +22 -0
  24. package/dist/shared/adapters/fetch.adapter.d.ts +6 -0
  25. package/dist/shared/adapters/index.d.ts +34 -0
  26. package/dist/shared/adapters/index.js +298 -0
  27. package/dist/shared/adapters/index.js.map +18 -0
  28. package/dist/shared/adapters/logger.adapter.d.ts +44 -0
  29. package/dist/shared/adapters/offscreen.adapter.d.ts +20 -0
  30. package/dist/shared/adapters/runtime.adapter.d.ts +66 -0
  31. package/dist/shared/adapters/storage.adapter.d.ts +29 -0
  32. package/dist/shared/adapters/tabs.adapter.d.ts +39 -0
  33. package/dist/shared/adapters/window.adapter.d.ts +14 -0
  34. package/dist/shared/lib/context-helpers.d.ts +64 -0
  35. package/dist/shared/lib/context-helpers.js +1086 -0
  36. package/dist/shared/lib/context-helpers.js.map +24 -0
  37. package/dist/shared/lib/context-specific-helpers.d.ts +160 -0
  38. package/dist/shared/lib/errors.d.ts +67 -0
  39. package/dist/shared/lib/errors.js +94 -0
  40. package/dist/shared/lib/errors.js.map +10 -0
  41. package/dist/shared/lib/handler-execution-tracker.d.ts +24 -0
  42. package/dist/shared/lib/message-bus.d.ts +233 -0
  43. package/dist/shared/lib/message-bus.js +1033 -0
  44. package/dist/shared/lib/message-bus.js.map +23 -0
  45. package/dist/shared/lib/state.d.ts +102 -0
  46. package/dist/shared/lib/state.js +1265 -0
  47. package/dist/shared/lib/state.js.map +24 -0
  48. package/dist/shared/lib/test-helpers.d.ts +133 -0
  49. package/dist/shared/lib/test-helpers.js +136 -0
  50. package/dist/shared/lib/test-helpers.js.map +10 -0
  51. package/dist/shared/state/app-state.d.ts +8 -0
  52. package/dist/shared/state/app-state.js +1272 -0
  53. package/dist/shared/state/app-state.js.map +25 -0
  54. package/dist/shared/types/messages.d.ts +341 -0
  55. package/dist/shared/types/messages.js +25 -0
  56. package/dist/shared/types/messages.js.map +10 -0
  57. package/package.json +110 -0
package/cli/polly.ts ADDED
@@ -0,0 +1,564 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Polly CLI
4
+ *
5
+ * Command-line tool for building multi-execution-context applications
6
+ * with reactive state and cross-context messaging.
7
+ *
8
+ * Supports: Chrome extensions, PWAs, Node/Bun/Deno apps with workers
9
+ *
10
+ * Usage:
11
+ * polly init [name] Create a new project
12
+ * polly check Run all checks (typecheck, lint, test, build)
13
+ * polly build [options] Build the project
14
+ * polly dev Build with watch mode
15
+ * polly typecheck Type check your code
16
+ * polly lint [--fix] Lint your code
17
+ * polly format Format your code
18
+ * polly test [args] Run tests (requires bun test)
19
+ * polly verify [args] Run formal verification
20
+ * polly visualize [args] Generate architecture diagrams
21
+ * polly help Show help
22
+ *
23
+ * Options:
24
+ * --prod Build for production (minified)
25
+ * --config <path> Path to config file (default: polly.config.ts)
26
+ * --fix Auto-fix lint/format issues
27
+ */
28
+
29
+ // Use Bun built-ins instead of Node.js APIs
30
+ const __dirname = import.meta.dir;
31
+
32
+ const command = process.argv[2];
33
+ const commandArgs = process.argv.slice(3);
34
+ const cwd = process.cwd();
35
+
36
+ // Parse arguments
37
+ const args = {
38
+ prod: process.argv.includes("--prod"),
39
+ config: process.argv.includes("--config")
40
+ ? process.argv[process.argv.indexOf("--config") + 1]
41
+ : undefined,
42
+ };
43
+
44
+ /**
45
+ * Load user's configuration
46
+ */
47
+ async function loadConfig() {
48
+ const configPaths = [
49
+ args.config,
50
+ `${cwd}/polly.config.ts`,
51
+ `${cwd}/polly.config.js`,
52
+ `${cwd}/polly.config.mjs`,
53
+ ].filter(Boolean) as string[];
54
+
55
+ for (const configPath of configPaths) {
56
+ // Use Bun.file().exists() instead of existsSync
57
+ if (await Bun.file(configPath).exists()) {
58
+ try {
59
+ const config = await import(configPath);
60
+ return config.default || config;
61
+ } catch (error) {
62
+ console.error(`❌ Failed to load config: ${configPath}`);
63
+ throw error;
64
+ }
65
+ }
66
+ }
67
+ return {
68
+ srcDir: "src",
69
+ distDir: "dist",
70
+ manifest: "manifest.json",
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Build command - build the extension
76
+ */
77
+ async function build() {
78
+ const config = await loadConfig();
79
+
80
+ // Import the build script from framework
81
+ const buildScriptPath = `${__dirname}/../scripts/build-extension.ts`;
82
+
83
+ // Pass config via environment
84
+ process.env["WEB_EXT_SRC"] = `${cwd}/${config.srcDir || "src"}`;
85
+ process.env["WEB_EXT_DIST"] = `${cwd}/${config.distDir || "dist"}`;
86
+ process.env["WEB_EXT_MANIFEST"] = `${cwd}/${config.manifest || "manifest.json"}`;
87
+ process.env["WEB_EXT_CWD"] = cwd;
88
+ process.env["WEB_EXT_PROD"] = args.prod ? "true" : "false";
89
+
90
+ // Run build
91
+ const proc = Bun.spawn(["bun", buildScriptPath], {
92
+ cwd,
93
+ stdout: "inherit",
94
+ stderr: "inherit",
95
+ });
96
+
97
+ const exitCode = await proc.exited;
98
+ if (exitCode !== 0) {
99
+ process.exit(exitCode);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Dev command - build with watch mode
105
+ */
106
+ async function dev() {
107
+ await build();
108
+ }
109
+
110
+ /**
111
+ * Verify command - delegate to @fairfox/web-ext-verify
112
+ */
113
+ async function verify() {
114
+ const verifyCli = `${__dirname}/../../verify/src/cli.ts`;
115
+
116
+ const proc = Bun.spawn(["bun", verifyCli, ...commandArgs], {
117
+ cwd,
118
+ stdout: "inherit",
119
+ stderr: "inherit",
120
+ stdin: "inherit",
121
+ });
122
+
123
+ const exitCode = await proc.exited;
124
+ if (exitCode !== 0) {
125
+ throw new Error(`Verification failed with exit code ${exitCode}`);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Visualize command - delegate to @fairfox/web-ext-visualize
131
+ */
132
+ async function visualize() {
133
+ const visualizeCli = `${__dirname}/../../visualize/src/cli.ts`;
134
+
135
+ const proc = Bun.spawn(["bun", visualizeCli, ...commandArgs], {
136
+ cwd,
137
+ stdout: "inherit",
138
+ stderr: "inherit",
139
+ stdin: "inherit",
140
+ });
141
+
142
+ const exitCode = await proc.exited;
143
+ if (exitCode !== 0) {
144
+ throw new Error(`Visualization failed with exit code ${exitCode}`);
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Typecheck command - run TypeScript type checking
150
+ */
151
+ async function typecheck() {
152
+ const proc = Bun.spawn(["bunx", "tsc", "--noEmit"], {
153
+ cwd,
154
+ stdout: "inherit",
155
+ stderr: "inherit",
156
+ });
157
+
158
+ const exitCode = await proc.exited;
159
+ if (exitCode !== 0) {
160
+ throw new Error(`Type checking failed with exit code ${exitCode}`);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Lint command - run Biome linter
166
+ */
167
+ async function lint() {
168
+ const fix = commandArgs.includes("--fix");
169
+ const lintArgs = fix ? ["check", "--write", "."] : ["check", "."];
170
+
171
+ const proc = Bun.spawn(["bunx", "@biomejs/biome", ...lintArgs], {
172
+ cwd,
173
+ stdout: "inherit",
174
+ stderr: "inherit",
175
+ });
176
+
177
+ const exitCode = await proc.exited;
178
+ if (exitCode !== 0) {
179
+ throw new Error(`Linting failed with exit code ${exitCode}`);
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Format command - run Biome formatter
185
+ */
186
+ async function format() {
187
+ const proc = Bun.spawn(["bunx", "@biomejs/biome", "format", "--write", "."], {
188
+ cwd,
189
+ stdout: "inherit",
190
+ stderr: "inherit",
191
+ });
192
+
193
+ const exitCode = await proc.exited;
194
+ if (exitCode !== 0) {
195
+ throw new Error(`Formatting failed with exit code ${exitCode}`);
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Test command - run Bun tests
201
+ */
202
+ async function test() {
203
+ const proc = Bun.spawn(["bun", "test", ...commandArgs], {
204
+ cwd,
205
+ stdout: "pipe",
206
+ stderr: "pipe",
207
+ stdin: "inherit",
208
+ });
209
+
210
+ const stdout = await new Response(proc.stdout).text();
211
+ const stderr = await new Response(proc.stderr).text();
212
+
213
+ // Output the results
214
+ if (stdout) process.stdout.write(stdout);
215
+ if (stderr) process.stderr.write(stderr);
216
+
217
+ const exitCode = await proc.exited;
218
+
219
+ // Check if no tests were found (not a failure)
220
+ if (stderr.includes("0 test files matching")) {
221
+ return;
222
+ }
223
+
224
+ if (exitCode !== 0) {
225
+ throw new Error(`Tests failed with exit code ${exitCode}`);
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Check command - run all quality checks in sequence
231
+ */
232
+ async function check() {
233
+ const checks = [
234
+ { name: "Type checking", fn: typecheck },
235
+ { name: "Linting", fn: lint },
236
+ { name: "Testing", fn: test },
237
+ { name: "Building", fn: build },
238
+ { name: "Verification", fn: verify, optional: true },
239
+ { name: "Visualization", fn: visualize, optional: true },
240
+ ];
241
+
242
+ for (const { name, fn, optional } of checks) {
243
+ try {
244
+ await fn();
245
+ } catch (_error) {
246
+ if (optional) {
247
+ continue;
248
+ }
249
+ console.error(`\n\x1b[31m✗ ${name} failed\x1b[0m\n`);
250
+ process.exit(1);
251
+ }
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Init command - scaffold a new extension
257
+ */
258
+ async function init() {
259
+ const projectName = commandArgs[0] || "my-extension";
260
+ const projectPath = `${cwd}/${projectName}`;
261
+
262
+ // Check if directory already exists
263
+ const { existsSync, mkdirSync, writeFileSync } = await import("node:fs");
264
+
265
+ if (existsSync(projectPath)) {
266
+ console.error(`\x1b[31m✗ Directory '${projectName}' already exists\x1b[0m\n`);
267
+ process.exit(1);
268
+ }
269
+
270
+ // Create project structure
271
+ const dirs = [
272
+ projectPath,
273
+ `${projectPath}/src`,
274
+ `${projectPath}/src/background`,
275
+ `${projectPath}/src/popup`,
276
+ ];
277
+
278
+ for (const dir of dirs) {
279
+ mkdirSync(dir, { recursive: true });
280
+ }
281
+
282
+ // Create package.json
283
+ const packageJson = {
284
+ name: projectName,
285
+ version: "0.1.0",
286
+ type: "module",
287
+ scripts: {
288
+ check: "web-ext check",
289
+ build: "web-ext build",
290
+ "build:prod": "web-ext build --prod",
291
+ typecheck: "web-ext typecheck",
292
+ lint: "web-ext lint",
293
+ "lint:fix": "web-ext lint --fix",
294
+ format: "web-ext format",
295
+ test: "web-ext test",
296
+ verify: "web-ext verify",
297
+ "verify:setup": "web-ext verify --setup",
298
+ visualize: "web-ext visualize",
299
+ "visualize:export": "web-ext visualize --export",
300
+ "visualize:serve": "web-ext visualize --serve",
301
+ },
302
+ dependencies: {
303
+ "@fairfox/web-ext": "*",
304
+ },
305
+ };
306
+
307
+ writeFileSync(`${projectPath}/package.json`, JSON.stringify(packageJson, null, 2));
308
+
309
+ // Create manifest.json at root
310
+ const manifest = {
311
+ manifest_version: 3,
312
+ name: projectName,
313
+ version: "0.1.0",
314
+ description: "A Chrome extension built with @fairfox/web-ext",
315
+ background: {
316
+ service_worker: "background/index.js",
317
+ type: "module",
318
+ },
319
+ action: {
320
+ default_popup: "popup/popup.html",
321
+ },
322
+ permissions: ["storage"],
323
+ };
324
+
325
+ writeFileSync(`${projectPath}/manifest.json`, JSON.stringify(manifest, null, 2));
326
+
327
+ // Create background script
328
+ const backgroundScript = `/**
329
+ * Background Service Worker
330
+ */
331
+
332
+ import { getMessageBus } from "@fairfox/web-ext/message-bus";
333
+ import { MessageRouter } from "@fairfox/web-ext/message-router";
334
+
335
+ const bus = getMessageBus("background");
336
+ new MessageRouter(bus);
337
+
338
+ // Add your message handlers here
339
+ bus.on("PING", async () => {
340
+ return { success: true, message: "pong" };
341
+ });
342
+
343
+ console.log("Background service worker initialized");
344
+ `;
345
+
346
+ writeFileSync(`${projectPath}/src/background/index.ts`, backgroundScript);
347
+
348
+ // Create popup HTML in src/popup
349
+ const popupHtml = `<!DOCTYPE html>
350
+ <html>
351
+ <head>
352
+ <meta charset="utf-8" />
353
+ <title>${projectName}</title>
354
+ </head>
355
+ <body>
356
+ <div id="root"></div>
357
+ <script type="module" src="./index.js"></script>
358
+ </body>
359
+ </html>
360
+ `;
361
+
362
+ writeFileSync(`${projectPath}/src/popup/popup.html`, popupHtml);
363
+
364
+ // Create popup script
365
+ const popupScript = `/**
366
+ * Popup UI
367
+ */
368
+
369
+ import { getMessageBus } from "@fairfox/web-ext/message-bus";
370
+
371
+ const bus = getMessageBus("popup");
372
+
373
+ // Simple example without UI framework
374
+ const root = document.getElementById("root");
375
+
376
+ if (root) {
377
+ root.innerHTML = \`
378
+ <div style="padding: 16px; min-width: 200px;">
379
+ <h1 style="margin: 0 0 8px 0; font-size: 18px;">${projectName}</h1>
380
+ <button id="ping-btn" style="padding: 8px 16px;">Ping Background</button>
381
+ <p id="response" style="margin-top: 8px; font-size: 14px;"></p>
382
+ </div>
383
+ \`;
384
+
385
+ const btn = document.getElementById("ping-btn");
386
+ const response = document.getElementById("response");
387
+
388
+ btn?.addEventListener("click", async () => {
389
+ const result = await bus.send({ type: "PING" });
390
+ if (response) {
391
+ response.textContent = JSON.stringify(result);
392
+ }
393
+ });
394
+ }
395
+ `;
396
+
397
+ writeFileSync(`${projectPath}/src/popup/index.ts`, popupScript);
398
+
399
+ // Create tsconfig.json
400
+ const tsconfig = {
401
+ compilerOptions: {
402
+ target: "ES2022",
403
+ module: "ESNext",
404
+ lib: ["ES2022", "DOM"],
405
+ moduleResolution: "bundler",
406
+ strict: true,
407
+ esModuleInterop: true,
408
+ skipLibCheck: true,
409
+ forceConsistentCasingInFileNames: true,
410
+ resolveJsonModule: true,
411
+ allowSyntheticDefaultImports: true,
412
+ jsx: "react-jsx",
413
+ jsxImportSource: "preact",
414
+ },
415
+ include: ["src/**/*"],
416
+ };
417
+
418
+ writeFileSync(`${projectPath}/tsconfig.json`, JSON.stringify(tsconfig, null, 2));
419
+
420
+ // Create biome.json
421
+ const biomeConfig = {
422
+ files: {
423
+ includes: ["src/**/*.ts", "src/**/*.tsx"],
424
+ ignoreUnknown: true,
425
+ },
426
+ linter: {
427
+ enabled: true,
428
+ rules: {
429
+ recommended: true,
430
+ },
431
+ },
432
+ formatter: {
433
+ enabled: true,
434
+ indentStyle: "space",
435
+ indentWidth: 2,
436
+ },
437
+ };
438
+
439
+ writeFileSync(`${projectPath}/biome.json`, JSON.stringify(biomeConfig, null, 2));
440
+
441
+ // Create README
442
+ const readme = `# ${projectName}
443
+
444
+ A Chrome extension built with [@fairfox/web-ext](https://github.com/fairfox/web-ext).
445
+
446
+ ## Getting Started
447
+
448
+ 1. Install dependencies:
449
+ \`\`\`bash
450
+ bun install
451
+ \`\`\`
452
+
453
+ 2. Build the extension:
454
+ \`\`\`bash
455
+ bun run build
456
+ \`\`\`
457
+
458
+ 3. Load the extension in Chrome:
459
+ - Open \`chrome://extensions\`
460
+ - Enable "Developer mode"
461
+ - Click "Load unpacked"
462
+ - Select the \`dist/\` folder
463
+
464
+ ## Development
465
+
466
+ - \`bun run build\` - Build the extension
467
+ - \`bun run check\` - Run all checks (typecheck, lint, test, build)
468
+ - \`bun run typecheck\` - Type check your code
469
+ - \`bun run lint\` - Lint your code
470
+ - \`bun run format\` - Format your code
471
+ - \`bun run verify\` - Run formal verification
472
+ - \`bun run visualize\` - Generate architecture diagrams
473
+
474
+ ## Project Structure
475
+
476
+ \`\`\`
477
+ ${projectName}/
478
+ ├── src/
479
+ │ ├── background/
480
+ │ │ └── index.ts # Background service worker
481
+ │ └── popup/
482
+ │ ├── popup.html # Popup HTML
483
+ │ └── index.ts # Popup script
484
+ ├── manifest.json # Extension manifest
485
+ ├── dist/ # Build output (load this in Chrome)
486
+ ├── package.json
487
+ ├── tsconfig.json
488
+ └── biome.json
489
+ \`\`\`
490
+ `;
491
+
492
+ writeFileSync(`${projectPath}/README.md`, readme);
493
+
494
+ // Create .gitignore
495
+ const gitignore = `node_modules
496
+ dist
497
+ docs
498
+ .DS_Store
499
+ `;
500
+
501
+ writeFileSync(`${projectPath}/.gitignore`, gitignore);
502
+ }
503
+
504
+ /**
505
+ * Help command
506
+ */
507
+ function help() {
508
+ // Help is shown automatically via commander
509
+ }
510
+
511
+ /**
512
+ * Main entry point
513
+ */
514
+ async function main() {
515
+ try {
516
+ switch (command) {
517
+ case "init":
518
+ await init();
519
+ break;
520
+ case "check":
521
+ await check();
522
+ break;
523
+ case "build":
524
+ await build();
525
+ break;
526
+ case "dev":
527
+ await dev();
528
+ break;
529
+ case "typecheck":
530
+ await typecheck();
531
+ break;
532
+ case "lint":
533
+ await lint();
534
+ break;
535
+ case "format":
536
+ await format();
537
+ break;
538
+ case "test":
539
+ await test();
540
+ break;
541
+ case "verify":
542
+ await verify();
543
+ break;
544
+ case "visualize":
545
+ await visualize();
546
+ break;
547
+ case "help":
548
+ case "--help":
549
+ case "-h":
550
+ case undefined:
551
+ help();
552
+ break;
553
+ default:
554
+ console.error(`❌ Unknown command: ${command}\n`);
555
+ help();
556
+ process.exit(1);
557
+ }
558
+ } catch (error) {
559
+ console.error("\n❌ Command failed:", error);
560
+ process.exit(1);
561
+ }
562
+ }
563
+
564
+ main();
@@ -0,0 +1,7 @@
1
+ import { type MessageBus } from "@/shared/lib/message-bus";
2
+ export declare class APIClient {
3
+ private bus;
4
+ constructor(bus?: MessageBus);
5
+ private setupHandlers;
6
+ }
7
+ export { APIClient as ApiClient };
@@ -0,0 +1,7 @@
1
+ import { type MessageBus } from "@/shared/lib/message-bus";
2
+ export declare class ContextMenuManager {
3
+ private bus;
4
+ constructor(bus?: MessageBus);
5
+ setup(): Promise<void>;
6
+ private setupHandlers;
7
+ }
@@ -0,0 +1,31 @@
1
+ import type { MessageBus } from "../shared/lib/message-bus";
2
+ import type { BaseMessage, ExtensionMessage } from "../shared/types/messages";
3
+ /**
4
+ * Initialize background script with message router.
5
+ *
6
+ * This is the recommended way to setup your background script.
7
+ * It automatically creates the message bus and router.
8
+ *
9
+ * @returns MessageBus instance for registering handlers
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * // src/background/index.ts
14
+ * import { createBackground } from '@fairfox/web-ext/background'
15
+ *
16
+ * const bus = createBackground()
17
+ *
18
+ * bus.on('MY_MESSAGE', async (payload) => {
19
+ * return { success: true }
20
+ * })
21
+ * ```
22
+ *
23
+ * @example With custom message types
24
+ * ```typescript
25
+ * type MyMessages = { type: 'MY_MESSAGE'; data: string }
26
+ * const bus = createBackground<MyMessages>()
27
+ * ```
28
+ */
29
+ export declare function createBackground<TMessage extends BaseMessage = ExtensionMessage>(): MessageBus<TMessage>;
30
+ export { MessageRouter } from "./message-router";
31
+ export { getMessageBus } from "../shared/lib/message-bus";