@stacksjs/browser-extension 0.70.67 → 0.70.69
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/README.md +75 -1
- package/dist/build.d.ts +8 -8
- package/dist/build.js +64 -25
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/sanitize.d.ts +11 -0
- package/dist/sanitize.js +7 -0
- package/dist/types.d.ts +28 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,3 +1,77 @@
|
|
|
1
1
|
# @stacksjs/browser-extension
|
|
2
2
|
|
|
3
|
-
Build MV3 browser extensions (Chrome + Firefox) the Stacks way — manifest,
|
|
3
|
+
Build MV3 browser extensions (Chrome + Firefox) the Stacks way — the manifest,
|
|
4
|
+
content/background scripts, stx pages, `declarativeNetRequest` rulesets, and
|
|
5
|
+
store-ready packaging are all derived from a single `config/extension.ts`. No
|
|
6
|
+
hand-written `manifest.json`, no per-project build script.
|
|
7
|
+
|
|
8
|
+
## Quick start
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
buddy extension:init # scaffold config/extension.ts + starter files
|
|
12
|
+
buddy extension:build # build all targets → dist/ (+ dist-firefox/)
|
|
13
|
+
buddy extension:build --target chrome
|
|
14
|
+
buddy extension:package # build + zip store-ready archives
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Configure
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// config/extension.ts
|
|
21
|
+
import { defineExtension } from '@stacksjs/browser-extension'
|
|
22
|
+
|
|
23
|
+
export default defineExtension({
|
|
24
|
+
name: 'My Extension',
|
|
25
|
+
description: 'Does something useful.',
|
|
26
|
+
geckoId: 'my-ext@example.com', // required to ship on Firefox
|
|
27
|
+
targets: ['chrome', 'firefox'],
|
|
28
|
+
|
|
29
|
+
background: 'src/background/index.ts',
|
|
30
|
+
content: [
|
|
31
|
+
{ entry: 'src/content/index.ts', matches: ['<all_urls>'], runAt: 'document_start' },
|
|
32
|
+
// world: 'MAIN' runs in the page's own JS context (patch page globals)
|
|
33
|
+
{ entry: 'src/content/inpage.ts', matches: ['*://example.com/*'], world: 'MAIN' },
|
|
34
|
+
],
|
|
35
|
+
pages: {
|
|
36
|
+
popup: { template: 'pages/popup.stx', script: 'src/ui/popup.ts' },
|
|
37
|
+
options: { template: 'pages/options.stx', script: 'src/ui/options.ts' },
|
|
38
|
+
},
|
|
39
|
+
icons: { 16: 'icons/icon-16.png', 48: 'icons/icon-48.png', 128: 'icons/icon-128.png' },
|
|
40
|
+
public: 'public', // copied verbatim into the build
|
|
41
|
+
assets: { 'styles.css': 'src/ui/styles.css' }, // extra file copies
|
|
42
|
+
|
|
43
|
+
// declarativeNetRequest — compiled from a module whose default export
|
|
44
|
+
// returns the rules array, written to rules/<id>.json:
|
|
45
|
+
rules: [{ id: 'static_rules', source: 'src/rules/static.ts' }],
|
|
46
|
+
|
|
47
|
+
manifest: {
|
|
48
|
+
permissions: ['declarativeNetRequest', 'storage', 'tabs'],
|
|
49
|
+
hostPermissions: ['http://*/*', 'https://*/*'],
|
|
50
|
+
minimumChromeVersion: '111',
|
|
51
|
+
firefoxMinVersion: '140.0',
|
|
52
|
+
webAccessibleResources: [{ resources: ['stubs/*.js'], matches: ['<all_urls>'] }],
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// App-specific post-processing the generic build can't express:
|
|
56
|
+
hooks: {
|
|
57
|
+
async postBuild({ outdir, target }) { /* … */ },
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## What the build does
|
|
63
|
+
|
|
64
|
+
Per target it: copies `public/**` + `assets`, bundles page scripts, builds the
|
|
65
|
+
stx pages to HTML (**sanitized for the extension CSP** — inline scripts/styles
|
|
66
|
+
stripped, asset paths made relative, stx dev-chunks removed), bundles
|
|
67
|
+
content/background scripts as classic **IIFE** bundles (esm would leak
|
|
68
|
+
top-level vars as page globals), compiles each ruleset's `source` to
|
|
69
|
+
`rules/<id>.json`, writes `manifest.json` (per-target: Chrome `service_worker`
|
|
70
|
+
vs Firefox event-page + `browser_specific_settings.gecko`), and runs your
|
|
71
|
+
`hooks.postBuild`.
|
|
72
|
+
|
|
73
|
+
## Programmatic API
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { buildExtension, buildAllTargets, packageExtension, generateManifest } from '@stacksjs/browser-extension'
|
|
77
|
+
```
|
package/dist/build.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import type { BuildOptions, ExtensionConfig, ExtensionTarget } from './types';
|
|
2
2
|
/** Resolve the output directory for a target. */
|
|
3
3
|
export declare function resolveOutdir(config: ExtensionConfig, target: ExtensionTarget, override?: string): string;
|
|
4
|
-
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
/**`) + extra `assets` copied in
|
|
5
|
+
* - stx pages → `<name>.html` (+ companion `<name>.js`), sanitized for the
|
|
6
|
+
* extension CSP, with stx dev chunks removed
|
|
7
|
+
* - content + background scripts → classic IIFE bundles
|
|
8
|
+
* - declarativeNetRequest rulesets compiled from their `source` modules
|
|
9
|
+
* - `manifest.json` generated per target
|
|
10
|
+
* - the app's `hooks.postBuild` run last
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
* package doesn't hard-depend on it when only the manifest/packaging APIs are
|
|
12
|
-
* used).
|
|
12
|
+
* Replaces the hand-rolled `build-extension.ts` every extension used to carry.
|
|
13
13
|
*/
|
|
14
14
|
export declare function buildExtension(config: ExtensionConfig, options: BuildOptions): Promise<{ outdir: string, target: ExtensionTarget }>;
|
|
15
15
|
/** Build every configured target. */
|
package/dist/build.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { Glob } from "bun";
|
|
1
2
|
import { cp, mkdir, rm } from "node:fs/promises";
|
|
2
3
|
import { existsSync } from "node:fs";
|
|
3
4
|
import { join, resolve } from "node:path";
|
|
4
5
|
import { contentScriptOut, generateManifest } from "./manifest";
|
|
6
|
+
import { sanitizeExtensionHtml } from "./sanitize";
|
|
5
7
|
export function resolveOutdir(config, target, override) {
|
|
6
8
|
if (override)
|
|
7
9
|
return override;
|
|
@@ -11,53 +13,90 @@ export function resolveOutdir(config, target, override) {
|
|
|
11
13
|
return config.outdir[target];
|
|
12
14
|
return target === "firefox" ? "dist-firefox" : "dist";
|
|
13
15
|
}
|
|
16
|
+
function normalizePage(page) {
|
|
17
|
+
return typeof page === "string" ? { template: page } : page;
|
|
18
|
+
}
|
|
19
|
+
function pageEntries(pages) {
|
|
20
|
+
if (!pages)
|
|
21
|
+
return [];
|
|
22
|
+
const out = [];
|
|
23
|
+
if (pages.popup)
|
|
24
|
+
out.push({ name: "popup", page: normalizePage(pages.popup) });
|
|
25
|
+
if (pages.options)
|
|
26
|
+
out.push({ name: "options", page: normalizePage(pages.options) });
|
|
27
|
+
for (const [name, page] of Object.entries(pages.extra ?? {}))
|
|
28
|
+
out.push({ name, page: normalizePage(page) });
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
async function buildScript(entry, out, outdir, cwd, minify) {
|
|
32
|
+
const result = await Bun.build({
|
|
33
|
+
entrypoints: [resolve(cwd, entry)],
|
|
34
|
+
target: "browser",
|
|
35
|
+
format: "iife",
|
|
36
|
+
splitting: !1,
|
|
37
|
+
minify
|
|
38
|
+
});
|
|
39
|
+
if (!result.success)
|
|
40
|
+
throw Error(`[browser-extension] failed to build ${entry}: ${result.logs.join(`
|
|
41
|
+
`)}`);
|
|
42
|
+
await Bun.write(join(outdir, out), await result.outputs[0].text());
|
|
43
|
+
}
|
|
14
44
|
export async function buildExtension(config, options) {
|
|
15
45
|
const target = options.target ?? "chrome", cwd = options.cwd ?? process.cwd(), outdir = resolve(cwd, resolveOutdir(config, target, options.outdir)), minify = options.minify ?? !0;
|
|
16
46
|
await rm(outdir, { recursive: !0, force: !0 });
|
|
17
|
-
await mkdir(outdir, { recursive: !0 });
|
|
47
|
+
await mkdir(join(outdir, "rules"), { recursive: !0 });
|
|
48
|
+
await mkdir(join(outdir, "icons"), { recursive: !0 });
|
|
18
49
|
if (config.public) {
|
|
19
50
|
const pub = resolve(cwd, config.public);
|
|
20
51
|
if (existsSync(pub))
|
|
21
52
|
await cp(pub, outdir, { recursive: !0 });
|
|
22
53
|
}
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
54
|
+
for (const [dest, src] of Object.entries(config.assets ?? {})) {
|
|
55
|
+
const s = resolve(cwd, src);
|
|
56
|
+
if (existsSync(s))
|
|
57
|
+
await Bun.write(join(outdir, dest), Bun.file(s));
|
|
58
|
+
}
|
|
59
|
+
const pages = pageEntries(config.pages);
|
|
60
|
+
await Promise.all(pages.filter((p) => p.page.script).map((p) => buildScript(p.page.script, `${p.name}.js`, outdir, cwd, minify)));
|
|
61
|
+
if (pages.length) {
|
|
62
|
+
const { default: stxPlugin } = await import("bun-plugin-stx"), result = await Bun.build({
|
|
63
|
+
entrypoints: pages.map((p) => resolve(cwd, p.page.template)),
|
|
28
64
|
outdir,
|
|
29
|
-
target: "browser",
|
|
30
65
|
minify,
|
|
66
|
+
naming: { entry: "[name].html" },
|
|
31
67
|
plugins: [stxPlugin()]
|
|
32
68
|
});
|
|
69
|
+
if (!result.success)
|
|
70
|
+
throw Error(`[browser-extension] failed to build pages: ${result.logs.join(`
|
|
71
|
+
`)}`);
|
|
72
|
+
for (const { name, page } of pages) {
|
|
73
|
+
const file = join(outdir, `${name}.html`);
|
|
74
|
+
if (!existsSync(file))
|
|
75
|
+
continue;
|
|
76
|
+
const own = page.script ? [`${name}.js`] : [];
|
|
77
|
+
await Bun.write(file, sanitizeExtensionHtml(await Bun.file(file).text(), own));
|
|
78
|
+
}
|
|
79
|
+
for await (const chunk of new Glob("chunk-*.js").scan(outdir))
|
|
80
|
+
await rm(join(outdir, chunk), { force: !0 });
|
|
33
81
|
}
|
|
34
82
|
const scripts = [];
|
|
35
83
|
if (config.background)
|
|
36
84
|
scripts.push({ entry: config.background, out: "background.js" });
|
|
37
85
|
for (const cs of config.content ?? [])
|
|
38
86
|
scripts.push({ entry: cs.entry, out: contentScriptOut(cs.entry, cs.out) });
|
|
39
|
-
await Promise.all(scripts.map(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
}));
|
|
87
|
+
await Promise.all(scripts.map((s) => buildScript(s.entry, s.out, outdir, cwd, minify)));
|
|
88
|
+
for (const rule of config.rules ?? []) {
|
|
89
|
+
if (!rule.source)
|
|
90
|
+
continue;
|
|
91
|
+
const mod = await import(resolve(cwd, rule.source)), build = mod.default ?? mod.buildRules ?? mod.rules ?? Object.values(mod).find((v) => typeof v === "function") ?? Object.values(mod).find((v) => Array.isArray(v)), data = typeof build === "function" ? await build() : build;
|
|
92
|
+
await Bun.write(join(outdir, rule.path ?? `rules/${rule.id}.json`), `${JSON.stringify(data, null, 2)}
|
|
93
|
+
`);
|
|
94
|
+
}
|
|
51
95
|
await Bun.write(join(outdir, "manifest.json"), `${JSON.stringify(generateManifest(config, { version: options.version, target }), null, 2)}
|
|
52
96
|
`);
|
|
97
|
+
await config.hooks?.postBuild?.({ config, target, outdir, version: options.version, cwd });
|
|
53
98
|
return { outdir, target };
|
|
54
99
|
}
|
|
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
100
|
export async function buildAllTargets(config, options) {
|
|
62
101
|
for (const target of config.targets ?? ["chrome", "firefox"])
|
|
63
102
|
await buildExtension(config, { ...options, target });
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitize an stx-built page for use as an extension page.
|
|
3
|
+
*
|
|
4
|
+
* MV3 extension pages run under a strict CSP (`script-src 'self'`) — no inline
|
|
5
|
+
* `<script>` or `<style>`, and asset URLs must be relative (the page loads from
|
|
6
|
+
* `chrome-extension://…/<name>.html`, not a web root). stx emits dev niceties
|
|
7
|
+
* (SEO meta, an inline style block, an inline hydration script) and absolute
|
|
8
|
+
* `/asset` paths, so strip/rewrite them, keeping only the page's own compiled
|
|
9
|
+
* script(s).
|
|
10
|
+
*/
|
|
11
|
+
export declare function sanitizeExtensionHtml(html: string, ownScripts: string[]): string;
|
package/dist/sanitize.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function sanitizeExtensionHtml(html, ownScripts) {
|
|
2
|
+
const keep = ownScripts.map((s) => `(?![^>]*src="/?${s.replace(".", "\\.")}")`).join("");
|
|
3
|
+
let out = html.replace(/\n?<!-- stx SEO Tags -->[\s\S]*?(?=\n\s*<meta charset=)/, "").replace(/\n?\s*<style\b[\s\S]*?<\/style>/g, "").replace(new RegExp(`\\n?\\s*<script\\b${keep}[\\s\\S]*?<\\/script>`, "g"), "").replaceAll('href="/styles.css"', 'href="styles.css"').replaceAll('href="/icons/', 'href="icons/');
|
|
4
|
+
for (const s of ownScripts)
|
|
5
|
+
out = out.replaceAll(`src="/${s}"`, `src="${s}"`);
|
|
6
|
+
return out;
|
|
7
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -9,17 +9,28 @@ export declare interface ContentScript {
|
|
|
9
9
|
matchAboutBlank?: boolean
|
|
10
10
|
excludeMatches?: string[]
|
|
11
11
|
}
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* A declarativeNetRequest static ruleset. Either point `path` at a pre-built
|
|
14
|
+
* JSON file, or give `source` — a module whose default export is a function (or
|
|
15
|
+
* value) producing the rules array, which the build compiles to
|
|
16
|
+
* `rules/<id>.json`.
|
|
17
|
+
*/
|
|
13
18
|
export declare interface RuleResource {
|
|
14
19
|
id: string
|
|
15
20
|
path?: string
|
|
16
21
|
enabled?: boolean
|
|
22
|
+
source?: string
|
|
23
|
+
}
|
|
24
|
+
/** An extension page: an stx template + an optional companion script. */
|
|
25
|
+
export declare interface ExtensionPage {
|
|
26
|
+
template: string
|
|
27
|
+
script?: string
|
|
17
28
|
}
|
|
18
|
-
/**
|
|
29
|
+
/** stx pages bundled to HTML (popup, options, …). A bare string = template only. */
|
|
19
30
|
export declare interface ExtensionPages {
|
|
20
|
-
popup?: string
|
|
21
|
-
options?: string
|
|
22
|
-
extra?: Record<string, string>
|
|
31
|
+
popup?: string | ExtensionPage
|
|
32
|
+
options?: string | ExtensionPage
|
|
33
|
+
extra?: Record<string, string | ExtensionPage>
|
|
23
34
|
}
|
|
24
35
|
/** Overrides merged verbatim into the generated manifest, per target. */
|
|
25
36
|
export declare interface ManifestOverrides {
|
|
@@ -32,6 +43,14 @@ export declare interface ManifestOverrides {
|
|
|
32
43
|
webAccessibleResources?: Array<{ resources: string[], matches: string[] }>
|
|
33
44
|
extra?: Record<string, unknown>
|
|
34
45
|
}
|
|
46
|
+
/** Context passed to build hooks. */
|
|
47
|
+
export declare interface BuildContext {
|
|
48
|
+
config: ExtensionConfig
|
|
49
|
+
target: ExtensionTarget
|
|
50
|
+
outdir: string
|
|
51
|
+
version: string
|
|
52
|
+
cwd: string
|
|
53
|
+
}
|
|
35
54
|
export declare interface ExtensionConfig {
|
|
36
55
|
name: string
|
|
37
56
|
description: string
|
|
@@ -42,9 +61,13 @@ export declare interface ExtensionConfig {
|
|
|
42
61
|
pages?: ExtensionPages
|
|
43
62
|
icons?: Record<number, string>
|
|
44
63
|
public?: string
|
|
64
|
+
assets?: Record<string, string>
|
|
45
65
|
rules?: RuleResource[]
|
|
46
66
|
manifest?: ManifestOverrides
|
|
47
67
|
outdir?: Partial<Record<ExtensionTarget, string>> | string
|
|
68
|
+
hooks?: {
|
|
69
|
+
postBuild?: (ctx: BuildContext) => void | Promise<void>
|
|
70
|
+
}
|
|
48
71
|
}
|
|
49
72
|
export declare interface BuildOptions {
|
|
50
73
|
target?: ExtensionTarget
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/browser-extension",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.70.
|
|
4
|
+
"version": "0.70.69",
|
|
5
5
|
"description": "Build MV3 browser extensions (Chrome + Firefox) the Stacks way — manifest, content/background scripts, DNR rules, packaging, all config-driven.",
|
|
6
6
|
"author": "Chris Breuer",
|
|
7
7
|
"contributors": [
|