Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
548 changes: 548 additions & 0 deletions src/cli/builders/manifest/ManifestBase.test.ts

Large diffs are not rendered by default.

338 changes: 255 additions & 83 deletions src/cli/builders/manifest/ManifestBase.ts

Large diffs are not rendered by default.

17 changes: 9 additions & 8 deletions src/cli/builders/manifest/ManifestV2.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import ManifestBase from "./ManifestBase";

import {filterHostPatterns, filterPermissionsForMV2} from "./utils";
import {filterHostPatterns, filterOptionalPermissions, filterPermissionsForMV2} from "./utils";

import {CoreManifest, ManifestVersion} from "@typing/manifest";
import {Browser} from "@typing/browser";
Expand Down Expand Up @@ -65,10 +65,10 @@ export default class extends ManifestBase<ManifestV2> {
}

protected buildPermissions(): Partial<ManifestV2> | undefined {
const permissions: string[] = Array.from(filterPermissionsForMV2(this.permissions));
const permissions: string[] = Array.from(filterPermissionsForMV2(this.combinedPermissions));

if (this.hostPermissions.size > 0) {
permissions.push(...filterHostPatterns(this.hostPermissions));
if (this.combinedHostPermissions.size > 0) {
permissions.push(...filterHostPatterns(this.combinedHostPermissions));
}

if (permissions.length > 0) {
Expand All @@ -77,14 +77,15 @@ export default class extends ManifestBase<ManifestV2> {
}

protected buildOptionalPermissions(): Partial<ManifestV2> | undefined {
const optionalPermissions: string[] = Array.from(filterPermissionsForMV2(this.optionalPermissions)).filter(
permission => !this.permissions.has(permission)
const optionalPermissions: string[] = filterOptionalPermissions(
filterPermissionsForMV2(this.combinedOptionalPermissions),
filterPermissionsForMV2(this.combinedPermissions)
);

// prettier-ignore
const optionalHostPermissions: string[] = Array
.from(filterHostPatterns(new Set([...this.hostPermissions, ...this.optionalHostPermissions])))
.filter((permission) => !this.hostPermissions.has(permission));
.from(filterHostPatterns(new Set([...this.combinedHostPermissions, ...this.combinedOptionalHostPermissions])))
.filter((permission) => !this.combinedHostPermissions.has(permission));

if (optionalHostPermissions.length > 0) {
optionalPermissions.push(...optionalHostPermissions);
Expand Down
20 changes: 10 additions & 10 deletions src/cli/builders/manifest/ManifestV3.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import ManifestBase, {ManifestError} from "./ManifestBase";

import {filterHostPatterns, filterPermissionsForMV3} from "./utils";
import {filterHostPatterns, filterOptionalPermissions, filterPermissionsForMV3} from "./utils";

import {CoreManifest, ManifestAccessibleResource, ManifestVersion} from "@typing/manifest";
import {Browser} from "@typing/browser";
Expand Down Expand Up @@ -73,35 +73,35 @@ export default class extends ManifestBase<ManifestV3> {
}

protected buildPermissions(): Partial<ManifestV3> | undefined {
const permissions = Array.from(filterPermissionsForMV3(this.permissions));
const permissions = Array.from(filterPermissionsForMV3(this.combinedPermissions));

if (permissions.length > 0) {
return {permissions};
}
}

protected buildOptionalPermissions(): Partial<ManifestV3> | undefined {
// prettier-ignore
const optionalPermissions = Array
.from(filterPermissionsForMV3(this.optionalPermissions))
.filter((permission) => !this.permissions.has(permission));
const optionalPermissions = filterOptionalPermissions(
filterPermissionsForMV3(this.combinedOptionalPermissions),
filterPermissionsForMV3(this.combinedPermissions)
);

if (optionalPermissions.length > 0) {
return {optional_permissions: optionalPermissions};
}
}

protected buildHostPermissions(): Partial<ManifestV3> | undefined {
if (this.hostPermissions.size > 0) {
return {host_permissions: [...filterHostPatterns(this.hostPermissions)]};
if (this.combinedHostPermissions.size > 0) {
return {host_permissions: [...filterHostPatterns(this.combinedHostPermissions)]};
}
}

protected buildOptionalHostPermissions(): Partial<ManifestV3> | undefined {
// prettier-ignore
const optionalHostPermissions = Array
.from(filterHostPatterns(new Set([...this.hostPermissions, ...this.optionalHostPermissions])))
.filter((permission) => !this.hostPermissions.has(permission));
.from(filterHostPatterns(new Set([...this.combinedHostPermissions, ...this.combinedOptionalHostPermissions])))
.filter((permission) => !this.combinedHostPermissions.has(permission));

if (optionalHostPermissions.length > 0) {
return {optional_host_permissions: optionalHostPermissions};
Expand Down
62 changes: 61 additions & 1 deletion src/cli/builders/manifest/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import {filterHostPatterns, mergeWebAccessibleResources, normalizeDataCollectionPermissions} from "./utils";
import {
filterHostPatterns,
filterOptionalPermissions,
mergeWebAccessibleResources,
normalizeDataCollectionPermissions,
} from "./utils";
import {DataCollectionPermission} from "@typing/browser";

import {ManifestAccessibleResource} from "@typing/manifest";

type ManifestPermission = chrome.runtime.ManifestPermission;
type ManifestOptionalPermission = chrome.runtime.ManifestOptionalPermission;

const toSet = (arr: string[]) => new Set(arr);
const setToArray = (set: Set<string>) => Array.from(set);
const sortResources = (resources: ManifestAccessibleResource[]): ManifestAccessibleResource[] => {
Expand Down Expand Up @@ -96,6 +104,58 @@ describe("filterHostPatterns", () => {
});
});

describe("filterOptionalPermissions", () => {
test("removes permissions that are already required", () => {
const required = new Set<ManifestPermission>(["storage"]);
const optional = new Set<ManifestOptionalPermission>(["storage", "tabs"]);

const result = filterOptionalPermissions(optional, required);

expect(result).toEqual(expect.arrayContaining(["tabs"]));
expect(result).not.toEqual(expect.arrayContaining(["storage"]));
expect(result.length).toBe(1);
});

test("drops activeTab from optional when tabs is present in optional", () => {
const optional = new Set<ManifestOptionalPermission>(["activeTab", "tabs"]);
const required = new Set<ManifestPermission>();

const result = filterOptionalPermissions(optional, required);

// filterPermissions removes activeTab when tabs is present in the union
expect(result).toEqual(["tabs"]);
});

test("drops activeTab from optional when tabs is present in required", () => {
const optional = new Set<ManifestOptionalPermission>(["activeTab"]);
const required = new Set<ManifestPermission>(["tabs"]);

const result = filterOptionalPermissions(optional, required);

// Union contains tabs and activeTab; filterPermissions removes activeTab, then diff removes tabs as required -> empty
expect(result).toEqual([]);
});

test("keeps activeTab when tabs is absent from both optional and required", () => {
const optional = new Set<ManifestOptionalPermission>(["activeTab"]);
const required = new Set<ManifestPermission>();

const result = filterOptionalPermissions(optional, required);

expect(result).toEqual(["activeTab"]);
});

test("deduplicates and filters correctly when mixing optional and required", () => {
const optional = new Set<ManifestOptionalPermission>(["tabs", "storage", "activeTab"]);
const required = new Set<ManifestPermission>(["storage"]);

const result = filterOptionalPermissions(optional, required);

// activeTab should be removed because tabs is present; storage removed because it's required
expect(result).toEqual(["tabs"]);
});
});

describe("mergeWebAccessibleResources", () => {
test("merge resources with same matches without duplicates", () => {
const input = [
Expand Down
19 changes: 17 additions & 2 deletions src/cli/builders/manifest/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ type Permission = ManifestPermissions | ManifestOptionalPermissions;
* @param permissions - Set of permissions to filter
* @returns New set of permissions adapted for Manifest V2
*/
export const filterPermissions = <T extends Permission>(permissions: Set<T>): Set<T> => {
if (permissions.has("tabs" as T)) {
permissions.delete("activeTab" as T);
}
return permissions;
};

export const filterPermissionsForMV2 = <T extends Permission>(permissions: Set<T>): Set<T> => {
const filteredPermissions = new Set(permissions);

Expand All @@ -35,7 +42,7 @@ export const filterPermissionsForMV2 = <T extends Permission>(permissions: Set<T

filteredPermissions.delete("offscreen" as T);

return filteredPermissions;
return filterPermissions(filteredPermissions);
};

export const filterPermissionsForMV3 = <T extends Permission>(permissions: Set<T>): Set<T> => {
Expand All @@ -53,7 +60,15 @@ export const filterPermissionsForMV3 = <T extends Permission>(permissions: Set<T
filteredPermissions.delete("webRequestAuthProvider" as T);
filteredPermissions.delete("webRequestBlocking" as T);

return filteredPermissions;
return filterPermissions(filteredPermissions);
};

export const filterOptionalPermissions = <O extends ManifestOptionalPermissions, R extends ManifestPermissions>(
optional: Set<O>,
required: Set<R>
): O[] => {
const allPermissions = filterPermissions(new Set([...optional, ...required]));
return _.difference(Array.from(allPermissions), Array.from(required)) as O[];
};

export const filterHostPatterns = (patterns: Set<string>): Set<string> => {
Expand Down
1 change: 1 addition & 0 deletions src/cli/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export {default as pluginBackground} from "./background";
export {default as pluginContent} from "./content";
export {default as pluginDotenv} from "./dotenv";
export {default as pluginHtml} from "./html";
export {default as pluginManifest} from "./manifest";
export {default as pluginOptimization} from "./optimization";
export {default as pluginOutput} from "./output";
export {default as pluginIcon} from "./icon";
Expand Down
28 changes: 28 additions & 0 deletions src/cli/plugins/manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {definePlugin} from "@main/plugin";
import {fromRootPath} from "@cli/resolvers/path";
import fs from "fs";

export default definePlugin(() => {
return {
name: "adnbn:manifest",
manifest: ({config, manifest}) => {
try {
const packagePath = fromRootPath(config, "package.json");

const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf-8"));

packageJson.manifest && manifest.raw(packageJson.manifest);
} catch (e) {}

const configManifest = config.manifest;

if (typeof configManifest === "object") {
manifest.raw(configManifest);
} else if (typeof configManifest === "function") {
const result = configManifest(manifest);

result && manifest.raw(result);
}
},
};
});
2 changes: 1 addition & 1 deletion src/cli/plugins/version/AbstractVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ export default abstract class AbstractVersion {
return;
}

return String(_.isFunction(version) ? version : version);
return String(_.isFunction(version) ? version() : version);
}
}
4 changes: 4 additions & 0 deletions src/cli/resolvers/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
pluginLocale,
pluginMeta,
pluginOffscreen,
pluginManifest,
pluginOptimization,
pluginOutput,
pluginPage,
Expand Down Expand Up @@ -193,6 +194,7 @@ export default async (config: OptionalConfig): Promise<Config> => {
html = [],
bundler = {},
env = {},
manifest,
manifestVersion = (new Set<Browser>([Browser.Safari]).has(browser) ? 2 : 3) as ManifestVersion,
mode = Mode.Development,
analyze = false,
Expand Down Expand Up @@ -246,6 +248,7 @@ export default async (config: OptionalConfig): Promise<Config> => {
icon,
incognito,
specific,
manifest,
manifestVersion,
rootDir,
outDir,
Expand Down Expand Up @@ -326,6 +329,7 @@ export default async (config: OptionalConfig): Promise<Config> => {
pluginHtml(),
pluginVersion(),
pluginBundler(),
pluginManifest(),
];

return {
Expand Down
16 changes: 15 additions & 1 deletion src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type {Options as HtmlOptions} from "html-rspack-tags-plugin";

import {Command, Mode} from "@typing/app";
import {Browser, BrowserSpecific} from "@typing/browser";
import {ManifestIncognitoValue, ManifestVersion} from "@typing/manifest";
import {ManifestIncognitoValue, ManifestVersion, ManifestBuilder, OptionalManifest} from "@typing/manifest";
import {Plugin} from "@typing/plugin";
import {Language} from "@typing/locale";
import {Awaiter} from "@typing/helpers";
Expand Down Expand Up @@ -171,6 +171,20 @@ export interface Config {
*/
incognito?: ManifestIncognitoValue | (() => ManifestIncognitoValue | undefined);

/**
* Extension manifest without the version.
* Allows customizing the manifest.json file beyond the standard fields handled by the builder.
* The structure and available APIs depend on the manifest version (v2 or v3).
*
* Accepts:
* - an object with additional manifest fields
* - a function that receives a ManifestBuilder instance and returns manifest fields
*
* Note: Some fields like name, version, and permissions are handled automatically
* by the builder and should not be included here unless you need to override them.
*/
manifest?: OptionalManifest | ((builder: ManifestBuilder) => OptionalManifest | undefined);

/**
* Extension manifest version (e.g., v2 or v3).
* Defines the manifest structure and available APIs.
Expand Down
6 changes: 5 additions & 1 deletion src/types/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ export type SafariManifest = ChromeManifest & {

export type Manifest = ChromeManifest | FirefoxManifest | SafariManifest;

export type OptionalManifest = Partial<Omit<Manifest, "manifest_version">>;

export interface ManifestBuilder<T extends CoreManifest = Manifest> {
setName(name: string): this;

Expand Down Expand Up @@ -156,7 +158,7 @@ export interface ManifestBuilder<T extends CoreManifest = Manifest> {
appendOptionalHostPermissions(permissions: ManifestHostPermissions): this;

// Web Accessible Resource
setManifestAccessibleResource(accessibleResources: ManifestAccessibleResources): this;
setAccessibleResource(accessibleResources: ManifestAccessibleResources): this;

appendAccessibleResources(accessibleResources: ManifestAccessibleResources): this;

Expand All @@ -166,6 +168,8 @@ export interface ManifestBuilder<T extends CoreManifest = Manifest> {

// Getter
get(): T;

raw(manifest: OptionalManifest): this;
}

type Entry = string;
Expand Down