@stacksjs/browser-extension 0.70.65

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.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright (c) 2023 Open Web Foundation
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @stacksjs/browser-extension
2
+
3
+ Build MV3 browser extensions (Chrome + Firefox) the Stacks way — manifest, content/background scripts, DNR rules, and packaging, all driven by a single `config/extension.ts`.
@@ -0,0 +1,16 @@
1
+ import type { BuildOptions, ExtensionConfig, ExtensionTarget } from './types';
2
+ /** Resolve the output directory for a target. */
3
+ export declare function resolveOutdir(config: ExtensionConfig, target: ExtensionTarget, override?: string): string;
4
+ /**
5
+ * Build a browser extension for one target: bundle stx pages → HTML, content +
6
+ * background scripts → classic IIFE bundles, copy static assets, and write the
7
+ * generated manifest.json. Mirrors the hand-rolled build-extension.ts every
8
+ * extension used to carry, now driven purely by `ExtensionConfig`.
9
+ *
10
+ * The stx page bundling is delegated to `bun-plugin-stx` (loaded lazily so the
11
+ * package doesn't hard-depend on it when only the manifest/packaging APIs are
12
+ * used).
13
+ */
14
+ export declare function buildExtension(config: ExtensionConfig, options: BuildOptions): Promise<{ outdir: string, target: ExtensionTarget }>;
15
+ /** Build every configured target. */
16
+ export declare function buildAllTargets(config: ExtensionConfig, options: Omit<BuildOptions, 'target'>): Promise<void>;
package/dist/build.js ADDED
@@ -0,0 +1,64 @@
1
+ import { cp, mkdir, rm } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { contentScriptOut, generateManifest } from "./manifest";
5
+ export function resolveOutdir(config, target, override) {
6
+ if (override)
7
+ return override;
8
+ if (typeof config.outdir === "string")
9
+ return config.outdir;
10
+ if (config.outdir?.[target])
11
+ return config.outdir[target];
12
+ return target === "firefox" ? "dist-firefox" : "dist";
13
+ }
14
+ export async function buildExtension(config, options) {
15
+ const target = options.target ?? "chrome", cwd = options.cwd ?? process.cwd(), outdir = resolve(cwd, resolveOutdir(config, target, options.outdir)), minify = options.minify ?? !0;
16
+ await rm(outdir, { recursive: !0, force: !0 });
17
+ await mkdir(outdir, { recursive: !0 });
18
+ if (config.public) {
19
+ const pub = resolve(cwd, config.public);
20
+ if (existsSync(pub))
21
+ await cp(pub, outdir, { recursive: !0 });
22
+ }
23
+ const pageEntries = collectPages(config, cwd);
24
+ if (pageEntries.length) {
25
+ const { default: stxPlugin } = await import("bun-plugin-stx");
26
+ await Bun.build({
27
+ entrypoints: pageEntries,
28
+ outdir,
29
+ target: "browser",
30
+ minify,
31
+ plugins: [stxPlugin]
32
+ });
33
+ }
34
+ const scripts = [];
35
+ if (config.background)
36
+ scripts.push({ entry: config.background, out: "background.js" });
37
+ for (const cs of config.content ?? [])
38
+ scripts.push({ entry: cs.entry, out: contentScriptOut(cs.entry, cs.out) });
39
+ await Promise.all(scripts.map(async ({ entry, out }) => {
40
+ const result = await Bun.build({
41
+ entrypoints: [resolve(cwd, entry)],
42
+ target: "browser",
43
+ format: "iife",
44
+ minify
45
+ });
46
+ if (!result.success)
47
+ throw Error(`[browser-extension] failed to build ${entry}: ${result.logs.join(`
48
+ `)}`);
49
+ await Bun.write(join(outdir, out), await result.outputs[0].text());
50
+ }));
51
+ await Bun.write(join(outdir, "manifest.json"), `${JSON.stringify(generateManifest(config, { version: options.version, target }), null, 2)}
52
+ `);
53
+ return { outdir, target };
54
+ }
55
+ function collectPages(config, cwd) {
56
+ const p = config.pages;
57
+ if (!p)
58
+ return [];
59
+ return [p.popup, p.options, ...Object.values(p.extra ?? {})].filter(Boolean).map((e) => resolve(cwd, e));
60
+ }
61
+ export async function buildAllTargets(config, options) {
62
+ for (const target of config.targets ?? ["chrome", "firefox"])
63
+ await buildExtension(config, { ...options, target });
64
+ }
@@ -0,0 +1,17 @@
1
+ import type { ExtensionConfig } from './types';
2
+ /**
3
+ * Identity helper for authoring `config/extension.ts` with full type-checking
4
+ * and editor completion:
5
+ *
6
+ * ```ts
7
+ * // config/extension.ts
8
+ * import { defineExtension } from '@stacksjs/browser-extension'
9
+ * export default defineExtension({ name: 'My Extension', description: '…', … })
10
+ * ```
11
+ */
12
+ export declare function defineExtension(config: ExtensionConfig): ExtensionConfig;
13
+ /**
14
+ * Load the project's extension config from `config/extension.ts` (default
15
+ * export). Returns null when the project has no extension config.
16
+ */
17
+ export declare function loadExtensionConfig(cwd?: string): Promise<ExtensionConfig | null>;
package/dist/config.js ADDED
@@ -0,0 +1,16 @@
1
+ import { existsSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ export function defineExtension(config) {
4
+ return config;
5
+ }
6
+ export async function loadExtensionConfig(cwd = process.cwd()) {
7
+ for (const rel of ["config/extension.ts", "extension.config.ts"]) {
8
+ const path = resolve(cwd, rel);
9
+ if (!existsSync(path))
10
+ continue;
11
+ const mod = await import(path), config = mod.default ?? mod.extension ?? mod.config;
12
+ if (config)
13
+ return config;
14
+ }
15
+ return null;
16
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @stacksjs/browser-extension
3
+ *
4
+ * Build MV3 browser extensions (Chrome + Firefox) from a single
5
+ * `ExtensionConfig` — manifest generation, content/background script bundling,
6
+ * stx pages, declarativeNetRequest rulesets, and store-ready packaging — with
7
+ * none of the per-project boilerplate.
8
+ */
9
+ export * from './build';
10
+ export * from './config';
11
+ export * from './manifest';
12
+ export * from './package';
13
+ export * from './types';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./build";
2
+ export * from "./config";
3
+ export * from "./manifest";
4
+ export * from "./package";
5
+ export * from "./types";
@@ -0,0 +1,44 @@
1
+ import type { ExtensionConfig, ExtensionTarget } from './types';
2
+ /** Output filename for a content script (explicit `out`, else entry basename). */
3
+ export declare function contentScriptOut(entry: string, out?: string): string;
4
+ /**
5
+ * Generate an MV3 manifest for a target from the extension config. Chrome uses
6
+ * a `service_worker` background + `minimum_chrome_version`; Firefox uses a
7
+ * `scripts` event page + `browser_specific_settings.gecko` (required by AMO).
8
+ */
9
+ export declare function generateManifest(config: ExtensionConfig, opts: { version: string, target?: ExtensionTarget }): GeneratedManifest;
10
+ /** MV3 manifest shape (loose — the two targets diverge on `background`). */
11
+ export declare interface GeneratedManifest {
12
+ manifest_version: 3
13
+ name: string
14
+ description: string
15
+ version: string
16
+ minimum_chrome_version?: string
17
+ action?: { default_title?: string, default_popup?: string }
18
+ options_page?: string
19
+ background?: { service_worker: string, type?: 'module' } | { scripts: string[], type?: 'module' }
20
+ browser_specific_settings?: {
21
+ gecko: {
22
+ id: string
23
+ strict_min_version: string
24
+ data_collection_permissions: { required: ['none'] }
25
+ }
26
+ }
27
+ permissions?: string[]
28
+ optional_permissions?: string[]
29
+ host_permissions?: string[]
30
+ icons?: Record<string, string>
31
+ content_scripts?: Array<{
32
+ matches: string[]
33
+ js: string[]
34
+ run_at?: string
35
+ world?: string
36
+ all_frames?: boolean
37
+ match_about_blank?: boolean
38
+ exclude_matches?: string[]
39
+ }>
40
+ declarative_net_request?: { rule_resources: Array<{ id: string, enabled: boolean, path: string }> }
41
+ content_security_policy?: { extension_pages: string }
42
+ web_accessible_resources?: Array<{ resources: string[], matches: string[] }>
43
+ [key: string]: unknown
44
+ }
@@ -0,0 +1,60 @@
1
+ import { basename } from "node:path";
2
+ export function contentScriptOut(entry, out) {
3
+ return out ?? basename(entry).replace(/\.[cm]?tsx?$/, ".js");
4
+ }
5
+ export function generateManifest(config, opts) {
6
+ const isFirefox = (opts.target ?? "chrome") === "firefox", m = config.manifest ?? {}, manifest = {
7
+ manifest_version: 3,
8
+ name: config.name,
9
+ description: config.description,
10
+ version: opts.version
11
+ };
12
+ if (!isFirefox && m.minimumChromeVersion)
13
+ manifest.minimum_chrome_version = m.minimumChromeVersion;
14
+ if (config.pages?.popup)
15
+ manifest.action = { default_title: config.name, default_popup: "popup.html" };
16
+ if (config.pages?.options)
17
+ manifest.options_page = "options.html";
18
+ if (config.background)
19
+ manifest.background = isFirefox ? { scripts: ["background.js"], type: "module" } : { service_worker: "background.js", type: "module" };
20
+ if (isFirefox && config.geckoId)
21
+ manifest.browser_specific_settings = {
22
+ gecko: {
23
+ id: config.geckoId,
24
+ strict_min_version: m.firefoxMinVersion ?? "128.0",
25
+ data_collection_permissions: { required: ["none"] }
26
+ }
27
+ };
28
+ if (m.permissions?.length)
29
+ manifest.permissions = m.permissions;
30
+ if (m.optionalPermissions?.length)
31
+ manifest.optional_permissions = m.optionalPermissions;
32
+ if (m.hostPermissions?.length)
33
+ manifest.host_permissions = m.hostPermissions;
34
+ if (config.icons)
35
+ manifest.icons = Object.fromEntries(Object.entries(config.icons).map(([size, path]) => [String(size), path]));
36
+ if (config.content?.length)
37
+ manifest.content_scripts = config.content.map((cs) => ({
38
+ matches: cs.matches,
39
+ js: [contentScriptOut(cs.entry, cs.out)],
40
+ ...cs.runAt ? { run_at: cs.runAt } : {},
41
+ ...cs.world ? { world: cs.world } : {},
42
+ ...cs.allFrames ? { all_frames: !0 } : {},
43
+ ...cs.matchAboutBlank ? { match_about_blank: !0 } : {},
44
+ ...cs.excludeMatches ? { exclude_matches: cs.excludeMatches } : {}
45
+ }));
46
+ if (config.rules?.length)
47
+ manifest.declarative_net_request = {
48
+ rule_resources: config.rules.map((r) => ({
49
+ id: r.id,
50
+ enabled: r.enabled ?? !0,
51
+ path: r.path ?? `rules/${r.id}.json`
52
+ }))
53
+ };
54
+ manifest.content_security_policy = {
55
+ extension_pages: m.contentSecurityPolicy ?? "script-src 'self'; object-src 'self'"
56
+ };
57
+ if (m.webAccessibleResources?.length)
58
+ manifest.web_accessible_resources = m.webAccessibleResources;
59
+ return { ...manifest, ...m.extra ?? {} };
60
+ }
@@ -0,0 +1,15 @@
1
+ import type { ExtensionConfig, ExtensionTarget } from './types';
2
+ /**
3
+ * Package a built extension into a store-ready `.zip` (one per target). Builds
4
+ * first unless `build: false`. Uses the system `zip` (available on CI runners
5
+ * and dev machines) so the archive matches what the Chrome/Firefox stores
6
+ * expect (files at the archive root, no wrapping directory).
7
+ */
8
+ export declare function packageExtension(config: ExtensionConfig, options: PackageOptions): Promise<string>;
9
+ export declare interface PackageOptions {
10
+ target?: ExtensionTarget
11
+ version: string
12
+ cwd?: string
13
+ outfile?: string
14
+ build?: boolean
15
+ }
@@ -0,0 +1,21 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir } from "node:fs/promises";
3
+ import { dirname, resolve } from "node:path";
4
+ import { buildExtension, resolveOutdir } from "./build";
5
+ function slug(name) {
6
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
7
+ }
8
+ export async function packageExtension(config, options) {
9
+ const target = options.target ?? "chrome", cwd = options.cwd ?? process.cwd();
10
+ if (options.build !== !1)
11
+ await buildExtension(config, { target, version: options.version, cwd });
12
+ const outdir = resolve(cwd, resolveOutdir(config, target));
13
+ if (!existsSync(outdir))
14
+ throw Error(`[browser-extension] nothing to package: ${outdir} does not exist`);
15
+ const suffix = target === "firefox" ? "-firefox" : "", outfile = resolve(cwd, options.outfile ?? `${slug(config.name)}-${options.version}${suffix}.zip`);
16
+ await mkdir(dirname(outfile), { recursive: !0 });
17
+ const proc = Bun.spawn(["zip", "-r", "-q", outfile, "."], { cwd: outdir, stdout: "pipe", stderr: "pipe" }), code = await proc.exited;
18
+ if (code !== 0)
19
+ throw Error(`[browser-extension] zip failed (${code}): ${await new Response(proc.stderr).text()}`);
20
+ return outfile;
21
+ }
@@ -0,0 +1,64 @@
1
+ /** A content script mapped into the manifest's `content_scripts`. */
2
+ export declare interface ContentScript {
3
+ entry: string
4
+ out?: string
5
+ matches: string[]
6
+ runAt?: 'document_start' | 'document_end' | 'document_idle'
7
+ world?: 'ISOLATED' | 'MAIN'
8
+ allFrames?: boolean
9
+ matchAboutBlank?: boolean
10
+ excludeMatches?: string[]
11
+ }
12
+ /** A declarativeNetRequest static ruleset. */
13
+ export declare interface RuleResource {
14
+ id: string
15
+ path?: string
16
+ enabled?: boolean
17
+ }
18
+ /** An stx page bundled to HTML (popup, options, …). */
19
+ export declare interface ExtensionPages {
20
+ popup?: string
21
+ options?: string
22
+ extra?: Record<string, string>
23
+ }
24
+ /** Overrides merged verbatim into the generated manifest, per target. */
25
+ export declare interface ManifestOverrides {
26
+ permissions?: string[]
27
+ hostPermissions?: string[]
28
+ optionalPermissions?: string[]
29
+ minimumChromeVersion?: string
30
+ firefoxMinVersion?: string
31
+ contentSecurityPolicy?: string
32
+ webAccessibleResources?: Array<{ resources: string[], matches: string[] }>
33
+ extra?: Record<string, unknown>
34
+ }
35
+ export declare interface ExtensionConfig {
36
+ name: string
37
+ description: string
38
+ geckoId?: string
39
+ targets?: ExtensionTarget[]
40
+ background?: string
41
+ content?: ContentScript[]
42
+ pages?: ExtensionPages
43
+ icons?: Record<number, string>
44
+ public?: string
45
+ rules?: RuleResource[]
46
+ manifest?: ManifestOverrides
47
+ outdir?: Partial<Record<ExtensionTarget, string>> | string
48
+ }
49
+ export declare interface BuildOptions {
50
+ target?: ExtensionTarget
51
+ version: string
52
+ outdir?: string
53
+ minify?: boolean
54
+ cwd?: string
55
+ }
56
+ /**
57
+ * Config-driven MV3 browser-extension types.
58
+ *
59
+ * A consuming app declares one `ExtensionConfig` (typically in
60
+ * `config/extension.ts` via `defineExtension`) and the framework derives the
61
+ * manifest, build graph, and packaging from it — no hand-written manifest.json
62
+ * or per-project build script.
63
+ */
64
+ export type ExtensionTarget = 'chrome' | 'firefox';
package/dist/types.js ADDED
File without changes
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@stacksjs/browser-extension",
3
+ "type": "module",
4
+ "version": "0.70.65",
5
+ "description": "Build MV3 browser extensions (Chrome + Firefox) the Stacks way — manifest, content/background scripts, DNR rules, packaging, all config-driven.",
6
+ "author": "Chris Breuer",
7
+ "contributors": [
8
+ "Chris Breuer <chris@stacksjs.com>"
9
+ ],
10
+ "license": "MIT",
11
+ "funding": "https://github.com/sponsors/chrisbbreuer",
12
+ "homepage": "https://github.com/stacksjs/stacks/tree/main/storage/framework/core/browser-extension#readme",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/stacksjs/stacks.git",
16
+ "directory": "./storage/framework/core/browser-extension"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/stacksjs/stacks/issues"
20
+ },
21
+ "keywords": [
22
+ "browser-extension",
23
+ "chrome-extension",
24
+ "firefox-extension",
25
+ "manifest-v3",
26
+ "mv3",
27
+ "declarativeNetRequest",
28
+ "bun",
29
+ "stacks"
30
+ ],
31
+ "sideEffects": false,
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.ts",
35
+ "development": "./src/index.ts",
36
+ "bun": "./dist/index.js",
37
+ "import": "./dist/index.js",
38
+ "default": "./dist/index.js"
39
+ },
40
+ "./*": {
41
+ "development": "./src/*",
42
+ "bun": "./dist/*",
43
+ "import": "./dist/*",
44
+ "default": "./dist/*"
45
+ }
46
+ },
47
+ "module": "dist/index.js",
48
+ "types": "dist/index.d.ts",
49
+ "files": [
50
+ "README.md",
51
+ "dist"
52
+ ],
53
+ "scripts": {
54
+ "build": "bun build.ts",
55
+ "typecheck": "bun tsc --noEmit",
56
+ "prepublishOnly": "bun run build"
57
+ },
58
+ "devDependencies": {
59
+ "better-dx": "^0.2.12"
60
+ }
61
+ }