@stacksjs/browser-extension 0.70.67 → 0.70.68

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/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
- * 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`.
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
- * 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).
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 pageEntries = collectPages(config, cwd);
24
- if (pageEntries.length) {
25
- const { default: stxPlugin } = await import("bun-plugin-stx");
26
- await Bun.build({
27
- entrypoints: pageEntries,
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(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
- }));
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
@@ -10,4 +10,5 @@ export * from './build';
10
10
  export * from './config';
11
11
  export * from './manifest';
12
12
  export * from './package';
13
+ export * from './sanitize';
13
14
  export * from './types';
package/dist/index.js CHANGED
@@ -2,4 +2,5 @@ export * from "./build";
2
2
  export * from "./config";
3
3
  export * from "./manifest";
4
4
  export * from "./package";
5
+ export * from "./sanitize";
5
6
  export * from "./types";
@@ -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;
@@ -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
- /** A declarativeNetRequest static ruleset. */
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
- /** An stx page bundled to HTML (popup, options, …). */
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.67",
4
+ "version": "0.70.68",
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": [