diff --git a/src/cli/builders/manifest/ManifestBase.ts b/src/cli/builders/manifest/ManifestBase.ts index 3644d0d..73313ca 100644 --- a/src/cli/builders/manifest/ManifestBase.ts +++ b/src/cli/builders/manifest/ManifestBase.ts @@ -1,8 +1,9 @@ +import _ from "lodash"; + import {mergeWebAccessibleResources} from "./utils"; import { CoreManifest, - FirefoxManifest, Manifest, ManifestAccessibleResource, ManifestAccessibleResources, @@ -14,13 +15,13 @@ import { ManifestHostPermissions, ManifestIcons, ManifestIncognito, - ManifestPermissions, ManifestOptionalPermissions, + ManifestPermissions, ManifestPopup, ManifestSidebar, ManifestVersion, } from "@typing/manifest"; -import {Browser} from "@typing/browser"; +import {Browser, BrowserSpecific} from "@typing/browser"; import {Language} from "@typing/locale"; import {CommandExecuteActionName} from "@typing/command"; import {DefaultIconGroupName} from "@typing/icon"; @@ -40,7 +41,6 @@ export class ManifestError extends Error { export default abstract class implements ManifestBuilder { protected name: string = "__MSG_app_name__"; - protected email?: string; protected author?: string; protected homepage?: string; protected shortName?: string; @@ -49,6 +49,7 @@ export default abstract class implements ManifestBuilder protected version: string = "0.0.0"; protected icon?: string; protected incognito?: ManifestIncognito; + protected specific?: BrowserSpecific; protected locale?: Language; protected icons: ManifestIcons = new Map(); protected background?: ManifestBackground; @@ -91,12 +92,6 @@ export default abstract class implements ManifestBuilder return this; } - public setEmail(email?: string): this { - this.email = email; - - return this; - } - public setName(name: string): this { this.name = name; @@ -139,6 +134,12 @@ export default abstract class implements ManifestBuilder return this; } + public setSpecific(settings?: BrowserSpecific): this { + this.specific = settings; + + return this; + } + public setIcons(icons?: ManifestIcons): this { this.icons = icons || new Map(); @@ -464,13 +465,52 @@ export default abstract class implements ManifestBuilder } } - protected buildBrowserSpecificSettings(): Partial | undefined { - if (this.browser === Browser.Firefox && this.email && this.permissions.has("storage")) { + protected buildBrowserSpecificSettings(): Partial | undefined { + const settings = this.specific || {}; + const {safari, gecko, geckoAndroid} = settings; + + if (this.browser === Browser.Firefox) { + const emptyGecko = + _.isEmpty(gecko?.id) && + _.isEmpty(gecko?.strictMinVersion) && + _.isEmpty(gecko?.strictMaxVersion) && + _.isEmpty(gecko?.updateUrl); + + const emptyGeckoAndroid = + _.isEmpty(geckoAndroid?.strictMinVersion) && _.isEmpty(geckoAndroid?.strictMaxVersion); + + if (emptyGecko && emptyGeckoAndroid) { + return; + } + + return { + browser_specific_settings: { + gecko: emptyGecko + ? undefined + : { + id: gecko?.id, + strict_min_version: gecko?.strictMinVersion, + strict_max_version: gecko?.strictMaxVersion, + update_url: gecko?.updateUrl, + }, + gecko_android: emptyGeckoAndroid + ? undefined + : { + strict_min_version: geckoAndroid?.strictMinVersion, + strict_max_version: geckoAndroid?.strictMaxVersion, + }, + }, + }; + } else if (this.browser === Browser.Safari) { + if (_.isEmpty(safari?.strictMinVersion) && _.isEmpty(safari?.strictMaxVersion)) { + return; + } + return { browser_specific_settings: { - gecko: { - id: this.email, - // strict_min_version: this.minimumVersion, + safari: { + strict_min_version: safari?.strictMinVersion, + strict_max_version: safari?.strictMaxVersion, }, }, }; diff --git a/src/cli/plugins/dotenv/crypt.test.ts b/src/cli/plugins/dotenv/crypt.test.ts deleted file mode 100644 index 613a323..0000000 --- a/src/cli/plugins/dotenv/crypt.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {decryptData, encryptData} from "./crypt"; - -describe("dotenv crypt - encryptData/decryptData", () => { - const sample = {foo: "bar", count: 42, flag: true, nested: {a: 1}}; - const key = "simple-key-123"; - - test("encrypt/decrypt roundtrip with explicit key", () => { - const encrypted = encryptData(sample, key); - expect(typeof encrypted).toBe("string"); - expect(encrypted).not.toEqual(JSON.stringify(sample)); - - const decrypted = decryptData(encrypted, key); - expect(decrypted).toEqual(sample); - }); - - test("deterministic encryption with same key and data", () => { - const e1 = encryptData(sample, key); - const e2 = encryptData(sample, key); - expect(e1).toBe(e2); - }); - - test("decrypt without key throws helpful error", () => { - const encrypted = encryptData(sample, key); - expect(() => decryptData(encrypted)).toThrow( - 'You need to specify the "key" argument for the decryptData function.' - ); - }); - - test("decrypt with wrong key fails (invalid JSON)", () => { - const encrypted = encryptData(sample, key); - expect(() => decryptData(encrypted, "wrong-key")).toThrow(); - }); -}); diff --git a/src/cli/plugins/dotenv/crypt.ts b/src/cli/plugins/dotenv/crypt.ts deleted file mode 100644 index 9f36572..0000000 --- a/src/cli/plugins/dotenv/crypt.ts +++ /dev/null @@ -1,27 +0,0 @@ -export const encryptData = (data: T, key: string): string => { - const jsonString = JSON.stringify(data); - const dataBytes = Array.from(jsonString, c => c.charCodeAt(0)); - const keyBytes = Array.from(key, c => c.charCodeAt(0)); - - const encryptedBytes = dataBytes.map((byte, i) => byte ^ keyBytes[i % keyBytes.length]); - const encryptedString = String.fromCharCode(...encryptedBytes); - - return btoa(encryptedString); -}; - -export const decryptData = (encrypted: string, key?: string): T => { - const encryptedString = atob(encrypted); - const encryptedBytes = Array.from(encryptedString, c => c.charCodeAt(0)); - let keyBytes: any[]; - try { - //@ts-expect-error: __ADNBN_ENV_CRYPTO_KEY__ is a virtual variable generated by the bundler `src/cli/plugins/dotenv/index.ts` - keyBytes = Array.from(key || __ADNBN_ENV_CRYPTO_KEY__, c => c.charCodeAt(0)); - } catch (err) { - throw new Error('You need to specify the "key" argument for the decryptData function.'); - } - - const decryptedBytes = encryptedBytes.map((byte, i) => byte ^ keyBytes[i % keyBytes.length]); - const decryptedString = String.fromCharCode(...decryptedBytes); - - return JSON.parse(decryptedString); -}; diff --git a/src/cli/plugins/dotenv/index.ts b/src/cli/plugins/dotenv/index.ts index 9d52381..cce1714 100644 --- a/src/cli/plugins/dotenv/index.ts +++ b/src/cli/plugins/dotenv/index.ts @@ -1,33 +1,21 @@ -import {createHash} from "crypto"; import {DefinePlugin} from "@rspack/core"; import {definePlugin} from "@main/plugin"; - -import {encryptData} from "./crypt"; import {filterEnvVars, resolveEnvOptions} from "./utils"; import {type DotenvParseOutput} from "dotenv"; -const generateKey = (value: string): string => { - return createHash("sha256").update(value).digest("base64"); -}; - export default definePlugin((vars: DotenvParseOutput = {}) => { return { name: "adnbn:dotenv", bundler: ({config}) => { - const {filter, crypt} = resolveEnvOptions(config.env); - - const filteredVars = filterEnvVars(vars, filter); - - const key = generateKey([config.app, ...Object.keys(filteredVars)].join("-")); + const {filter} = resolveEnvOptions(config.env); - const data = crypt ? encryptData(filteredVars, key) : filteredVars; + const data = filterEnvVars(vars, filter); return { plugins: [ new DefinePlugin({ - __ADNBN_ENV_CRYPTO_KEY__: JSON.stringify(key), "process.env": JSON.stringify(data), }), ], diff --git a/src/cli/plugins/dotenv/utils.test.ts b/src/cli/plugins/dotenv/utils.test.ts index 7230e48..da633dd 100644 --- a/src/cli/plugins/dotenv/utils.test.ts +++ b/src/cli/plugins/dotenv/utils.test.ts @@ -1,5 +1,5 @@ import {filterEnvVars, resolveEnvOptions} from "./utils"; -import {ReservedEnvKeys} from "../../../types/env"; +import {EnvReservedKeys} from "../../../types/env"; const baseVars = { APP: "myapp", @@ -38,9 +38,9 @@ describe("dotenv utils - filterEnvVars & resolveEnvOptions", () => { }); }); - test("object with filter and crypt true - filter selection plus crypt flag detection", () => { + test("object with filter true - filter selection flag detection", () => { const option = {filter: "PUBLIC_", crypt: true} as const; - const {filter, crypt} = resolveEnvOptions(option); + const {filter} = resolveEnvOptions(option); const filtered = filterEnvVars(baseVars, filter); expect(filtered).toEqual({ APP: baseVars.APP, @@ -50,8 +50,6 @@ describe("dotenv utils - filterEnvVars & resolveEnvOptions", () => { PUBLIC_API_URL: baseVars.PUBLIC_API_URL, PUBLIC_FEATURE: baseVars.PUBLIC_FEATURE, }); - - expect(crypt).toBe(true); }); test("empty string filter results in all keys", () => { @@ -71,7 +69,7 @@ describe("dotenv utils - filterEnvVars & resolveEnvOptions", () => { const {filter} = resolveEnvOptions(() => false); const filtered = filterEnvVars(baseVars, filter); const expected: any = {}; - for (const key of ReservedEnvKeys) expected[key] = (baseVars as any)[key]; + for (const key of EnvReservedKeys) expected[key] = (baseVars as any)[key]; expect(filtered).toEqual(expected); }); diff --git a/src/cli/plugins/dotenv/utils.ts b/src/cli/plugins/dotenv/utils.ts index ff88768..d00413b 100644 --- a/src/cli/plugins/dotenv/utils.ts +++ b/src/cli/plugins/dotenv/utils.ts @@ -1,25 +1,23 @@ import _ from "lodash"; -import {EnvFilterFunction, EnvFilterOptions, EnvFilterVariant, ReservedEnvKeys} from "@typing/env"; +import {EnvFilterFunction, EnvFilterOptions, EnvFilterVariant, EnvReservedKeys} from "@typing/env"; export type EnvOption = EnvFilterVariant | Partial; -export const resolveEnvOptions = (option?: EnvOption): {filter: EnvFilterFunction; crypt: boolean} => { +export const resolveEnvOptions = (option?: EnvOption): {filter: EnvFilterFunction} => { let userFilter: EnvFilterVariant | undefined; - let crypt: boolean = false; if (_.isString(option)) { userFilter = option; } else if (_.isFunction(option)) { userFilter = option; } else if (option && _.isObject(option)) { - const {filter: f, crypt: c} = option as Partial; + const {filter: f} = option as Partial; userFilter = f; - crypt = Boolean(c); } const filter = (key: string): boolean => { - if (ReservedEnvKeys.has(key)) { + if (EnvReservedKeys.has(key)) { return true; } @@ -34,7 +32,7 @@ export const resolveEnvOptions = (option?: EnvOption): {filter: EnvFilterFunctio return true; }; - return {filter, crypt}; + return {filter}; }; export const filterEnvVars = >(vars: T, filter: EnvFilterFunction): Partial => { diff --git a/src/cli/plugins/index.ts b/src/cli/plugins/index.ts index a47b0a5..ece5e46 100644 --- a/src/cli/plugins/index.ts +++ b/src/cli/plugins/index.ts @@ -1,21 +1,21 @@ -export {default as assetPlugin} from "./asset"; -export {default as bundlerPlugin} from "./bundler"; -export {default as backgroundPlugin} from "./background"; -export {default as contentPlugin} from "./content"; -export {default as dotenvPlugin} from "./dotenv"; -export {default as htmlPlugin} from "./html"; -export {default as optimizationPlugin} from "./optimization"; -export {default as outputPlugin} from "./output"; -export {default as iconPlugin} from "./icon"; -export {default as localePlugin} from "./locale"; -export {default as metaPlugin} from "./meta"; -export {default as offscreenPlugin} from "./offscreen"; -export {default as pagePlugin} from "./page"; -export {default as popupPlugin} from "./popup"; -export {default as publicPlugin} from "./public"; -export {default as sidebarPlugin} from "./sidebar"; -export {default as typescriptPlugin, TypescriptConfig, FileBuilder, VendorDeclaration} from "./typescript"; -export {default as reactPlugin} from "./react"; -export {default as stylePlugin} from "./style"; -export {default as viewPlugin} from "./view"; -export {default as versionPlugin} from "./version"; +export {default as pluginAsset} from "./asset"; +export {default as pluginBundler} from "./bundler"; +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 pluginOptimization} from "./optimization"; +export {default as pluginOutput} from "./output"; +export {default as pluginIcon} from "./icon"; +export {default as pluginLocale} from "./locale"; +export {default as pluginMeta} from "./meta"; +export {default as pluginOffscreen} from "./offscreen"; +export {default as pluginPage} from "./page"; +export {default as pluginPopup} from "./popup"; +export {default as pluginPublic} from "./public"; +export {default as pluginSidebar} from "./sidebar"; +export {default as pluginTypescript, TypescriptConfig, FileBuilder, VendorDeclaration} from "./typescript"; +export {default as pluginReact} from "./react"; +export {default as pluginStyle} from "./style"; +export {default as pluginView} from "./view"; +export {default as pluginVersion} from "./version"; diff --git a/src/cli/plugins/meta/AbstractMeta.ts b/src/cli/plugins/meta/AbstractMeta.ts index 321d180..4c86212 100644 --- a/src/cli/plugins/meta/AbstractMeta.ts +++ b/src/cli/plugins/meta/AbstractMeta.ts @@ -4,7 +4,7 @@ import {getEnv} from "@main/env"; import type {ReadonlyConfig} from "@typing/config"; -export default abstract class AbstractMeta { +export default abstract class AbstractMeta { public static value>( this: new (config: ReadonlyConfig) => T, config: ReadonlyConfig @@ -19,22 +19,22 @@ export default abstract class AbstractMeta { public getResolved(): V | undefined { const value = this.getValue(); - let resolved = _.isFunction(value) ? value() : value; + const resolved = _.isFunction(value) ? value() : value; if (this.isValid(resolved)) { return resolved; } if (_.isString(resolved)) { - resolved = getEnv(resolved); + const valueFromEnv = getEnv(resolved); - if (this.isValid(resolved)) { - return resolved; + if (this.isValid(valueFromEnv)) { + return valueFromEnv; } } } - protected isValid(value?: V): boolean { + protected isValid(value?: unknown): value is V { return true; } } diff --git a/src/cli/plugins/meta/Email.test.ts b/src/cli/plugins/meta/Email.test.ts deleted file mode 100644 index dff0fa0..0000000 --- a/src/cli/plugins/meta/Email.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import Email from "./Email"; - -describe("Email meta", () => { - const OLD_ENV = process.env; - - beforeEach(() => { - jest.resetModules(); - process.env = {...OLD_ENV}; - delete process.env.EMAIL; - }); - - afterAll(() => { - process.env = OLD_ENV; - }); - - const makeConfig = (email: any): any => ({email}); - - test("returns direct valid email", () => { - const config: any = makeConfig("test@example.com"); - const meta = new Email(config); - expect(meta.getResolved()).toBe("test@example.com"); - }); - - test("resolves from env var when key provided", () => { - process.env.EMAIL = "env@example.com"; - const config: any = makeConfig("EMAIL"); - const meta = new Email(config); - expect(meta.getResolved()).toBe("env@example.com"); - }); - - test("returns undefined for invalid direct value", () => { - const config: any = makeConfig("not-an-email"); - const meta = new Email(config); - expect(meta.getResolved()).toBeUndefined(); - }); - - test("returns undefined when env var is invalid", () => { - process.env.EMAIL = "not-an-email"; - const config: any = makeConfig("EMAIL"); - const meta = new Email(config); - expect(meta.getResolved()).toBeUndefined(); - }); - - test("returns undefined when function returns undefined", () => { - const config: any = makeConfig(() => undefined); - const meta = new Email(config); - expect(meta.getResolved()).toBeUndefined(); - }); -}); diff --git a/src/cli/plugins/meta/Email.ts b/src/cli/plugins/meta/Email.ts deleted file mode 100644 index 078f288..0000000 --- a/src/cli/plugins/meta/Email.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {z} from "zod"; - -import AbstractMeta from "./AbstractMeta"; - -import type {ReadonlyConfig} from "@typing/config"; - -const emailSchema = z.string().email(); - -export default class extends AbstractMeta { - public constructor(config: ReadonlyConfig) { - super(config); - } - - public getValue(): ReadonlyConfig["email"] { - return this.config.email; - } - - protected isValid(value?: string): boolean { - return emailSchema.safeParse(value).success; - } -} diff --git a/src/cli/plugins/meta/Homepage.ts b/src/cli/plugins/meta/Homepage.ts index 14506c9..a426b88 100644 --- a/src/cli/plugins/meta/Homepage.ts +++ b/src/cli/plugins/meta/Homepage.ts @@ -6,7 +6,7 @@ import type {ReadonlyConfig} from "@typing/config"; const urlSchema = z.string().url(); -export default class extends AbstractMeta { +export default class extends AbstractMeta { public constructor(config: ReadonlyConfig) { super(config); } @@ -15,7 +15,7 @@ export default class extends AbstractMeta { return this.config.homepage; } - protected isValid(value?: string): boolean { + protected isValid(value?: unknown): value is string { return urlSchema.safeParse(value).success; } } diff --git a/src/cli/plugins/meta/Incognito.ts b/src/cli/plugins/meta/Incognito.ts index 4804090..31fa8d3 100644 --- a/src/cli/plugins/meta/Incognito.ts +++ b/src/cli/plugins/meta/Incognito.ts @@ -12,7 +12,7 @@ export default class extends AbstractMeta { return this.config.incognito; } - protected isValid(value?: ManifestIncognitoValue): boolean { + protected isValid(value?: unknown): value is ManifestIncognito { return Object.values(ManifestIncognito).includes(value as ManifestIncognito); } } diff --git a/src/cli/plugins/meta/SpecificSettings.test.ts b/src/cli/plugins/meta/SpecificSettings.test.ts new file mode 100644 index 0000000..90939eb --- /dev/null +++ b/src/cli/plugins/meta/SpecificSettings.test.ts @@ -0,0 +1,84 @@ +import SpecificSettings from "./SpecificSettings"; + +describe("SpecificSettings meta", () => { + const OLD_ENV = process.env; + + beforeEach(() => { + jest.resetModules(); + process.env = {...OLD_ENV}; + delete (process.env as any).SPECIFIC; + }); + + afterAll(() => { + process.env = OLD_ENV; + }); + + const makeConfig = (specific: any): any => ({specific}); + + test("returns direct valid object", () => { + const specific = { + gecko: { + id: "@my-extension.example", + strictMinVersion: "58.0", + strictMaxVersion: "100.*", + updateUrl: "https://example.com/updates.json", + }, + geckoAndroid: { + strictMinVersion: "109.0", + strictMaxVersion: "120.*", + }, + safari: { + strictMinVersion: "14", + strictMaxVersion: "20", + }, + }; + + const meta = new SpecificSettings(makeConfig(specific)); + expect(meta.getResolved()).toEqual(specific); + }); + + test("returns undefined for invalid direct value (bad URL)", () => { + const specific = { + gecko: { + id: "@my-extension.example", + updateUrl: "not-a-url", + }, + }; + + const meta = new SpecificSettings(makeConfig(specific)); + expect(meta.getResolved()).toBeUndefined(); + }); + + test("returns undefined for invalid direct value (extra field)", () => { + const specific = { + gecko: { + id: "@my-extension.example", + foo: "bar", // extra field not allowed by strict schema + }, + }; + + const meta = new SpecificSettings(makeConfig(specific)); + expect(meta.getResolved()).toBeUndefined(); + }); + + test("returns value from function (valid object)", () => { + const specific = { + safari: {strictMinVersion: "14"}, + }; + const meta = new SpecificSettings(makeConfig(() => specific)); + expect(meta.getResolved()).toEqual(specific); + }); + + test("returns undefined when function returns undefined", () => { + const meta = new SpecificSettings(makeConfig(() => undefined)); + expect(meta.getResolved()).toBeUndefined(); + }); + + test("env set: key provided but env is string → undefined", () => { + // AbstractMeta treats string value as an env key and fetches process.env[key], + // but getEnv returns a string, while schema expects an object. Should be undefined. + process.env.SPECIFIC = "{gecko:{}}" as any; + const meta = new SpecificSettings(makeConfig("SPECIFIC")); + expect(meta.getResolved()).toBeUndefined(); + }); +}); diff --git a/src/cli/plugins/meta/SpecificSettings.ts b/src/cli/plugins/meta/SpecificSettings.ts new file mode 100644 index 0000000..6da4bda --- /dev/null +++ b/src/cli/plugins/meta/SpecificSettings.ts @@ -0,0 +1,40 @@ +import {z} from "zod"; + +import AbstractMeta from "./AbstractMeta"; + +import {BrowserSpecific} from "@typing/browser"; +import {ReadonlyConfig} from "@typing/config"; + +const VersionSpecificSchema = z + .object({ + strictMinVersion: z.string().min(1).optional(), + strictMaxVersion: z.string().min(1).optional(), + }) + .strict(); + +const GeckoSpecificSchema = VersionSpecificSchema.extend({ + id: z.string().min(1).optional(), + updateUrl: z.string().url().optional(), +}).strict(); + +const BrowserSpecificSchema = z + .object({ + gecko: GeckoSpecificSchema.optional(), + geckoAndroid: VersionSpecificSchema.optional(), + safari: VersionSpecificSchema.optional(), + }) + .strict(); + +export default class extends AbstractMeta { + public constructor(config: ReadonlyConfig) { + super(config); + } + + public getValue(): ReadonlyConfig["specific"] { + return this.config.specific; + } + + protected isValid(value?: unknown): value is BrowserSpecific { + return BrowserSpecificSchema.safeParse(value).success; + } +} diff --git a/src/cli/plugins/meta/index.ts b/src/cli/plugins/meta/index.ts index 29a309a..66da3d8 100644 --- a/src/cli/plugins/meta/index.ts +++ b/src/cli/plugins/meta/index.ts @@ -1,21 +1,21 @@ import {definePlugin} from "@main/plugin"; import Author from "./Author"; -import Email from "./Email"; import Homepage from "./Homepage"; import Incognito from "./Incognito"; +import SpecificSettings from "./SpecificSettings"; -export {Author, Email, Homepage, Incognito}; +export {Author, Homepage, Incognito, SpecificSettings}; export default definePlugin(() => { return { name: "adnbn:meta", manifest: ({manifest, config}) => { manifest - .setEmail(Email.value(config)) .setAuthor(Author.value(config)) .setHomepage(Homepage.value(config)) - .setIncognito(Incognito.value(config)); + .setIncognito(Incognito.value(config)) + .setSpecific(SpecificSettings.value(config)); }, }; }); diff --git a/src/cli/resolvers/config.ts b/src/cli/resolvers/config.ts index 680f5d9..8157eaf 100644 --- a/src/cli/resolvers/config.ts +++ b/src/cli/resolvers/config.ts @@ -4,27 +4,27 @@ import {loadConfig} from "c12"; import _ from "lodash"; import { - assetPlugin, - backgroundPlugin, - bundlerPlugin, - contentPlugin, - dotenvPlugin, - htmlPlugin, - iconPlugin, - localePlugin, - metaPlugin, - offscreenPlugin, - optimizationPlugin, - outputPlugin, - pagePlugin, - popupPlugin, - publicPlugin, - reactPlugin, - sidebarPlugin, - stylePlugin, - typescriptPlugin, - versionPlugin, - viewPlugin, + pluginAsset, + pluginBackground, + pluginBundler, + pluginContent, + pluginDotenv, + pluginHtml, + pluginIcon, + pluginLocale, + pluginMeta, + pluginOffscreen, + pluginOptimization, + pluginOutput, + pluginPage, + pluginPopup, + pluginPublic, + pluginReact, + pluginSidebar, + pluginStyle, + pluginTypescript, + pluginVersion, + pluginView, } from "../plugins"; import {fromRootPath, getAppPath, getAppSourcePath, getConfigFile} from "../resolvers/path"; @@ -177,11 +177,11 @@ export default async (config: OptionalConfig): Promise => { version = "VERSION", minimumVersion = "MINIMUM_VERSION", author = undefined, - email = "EMAIL", homepage = "HOMEPAGE", icon = DefaultIconGroupName, lang = Language.English, incognito, + specific, rootDir = ".", outDir = "dist", srcDir = "src", @@ -246,12 +246,12 @@ export default async (config: OptionalConfig): Promise => { shortName, version, minimumVersion, - email, author, homepage, lang, icon, incognito, + specific, manifestVersion, rootDir, outDir, @@ -311,27 +311,27 @@ export default async (config: OptionalConfig): Promise => { * Reordering may result in missing artifacts, incorrect configuration, or build failures. */ const corePlugins: Plugin[] = [ - dotenvPlugin(vars), - outputPlugin(), - optimizationPlugin(), - typescriptPlugin(), - reactPlugin(), - iconPlugin(), - assetPlugin(), - stylePlugin(), - localePlugin(), - metaPlugin(), - contentPlugin(), - backgroundPlugin(), - popupPlugin(), - publicPlugin(), - sidebarPlugin(), - offscreenPlugin(), - pagePlugin(), - viewPlugin(), - htmlPlugin(), - versionPlugin(), - bundlerPlugin(), + pluginDotenv(vars), + pluginOutput(), + pluginOptimization(), + pluginTypescript(), + pluginReact(), + pluginIcon(), + pluginAsset(), + pluginStyle(), + pluginLocale(), + pluginMeta(), + pluginContent(), + pluginBackground(), + pluginPopup(), + pluginPublic(), + pluginSidebar(), + pluginOffscreen(), + pluginPage(), + pluginView(), + pluginHtml(), + pluginVersion(), + pluginBundler(), ]; return { diff --git a/src/main/env.ts b/src/main/env.ts index 66be8b4..05e5cd3 100644 --- a/src/main/env.ts +++ b/src/main/env.ts @@ -1,27 +1,15 @@ -import {decryptData} from "@cli/plugins/dotenv/crypt"; - import {Browser} from "@typing/browser"; import {ManifestVersion} from "@typing/manifest"; -export const getEnv: { - (key: string): T | undefined; - (key: string, defaults: D): T | D; -} = (() => { - let envCache: object; - - return (key: string, defaults?: string) => { - let env: object; - - if (typeof process.env === "object") { - env = process.env; - } else { - envCache ??= decryptData(process.env); - env = envCache; - } - - return (env[key] ?? defaults) as any; - }; -})(); +export const getEnv = (key: string, defaults?: D): T | D => { + let env: object = {}; + + if (typeof process.env === "object") { + env = process.env; + } + + return env[key] ?? defaults; +}; export const getApp = (): string => { const app = getEnv("APP"); diff --git a/src/types/browser.ts b/src/types/browser.ts index 553310c..5f7992f 100644 --- a/src/types/browser.ts +++ b/src/types/browser.ts @@ -6,3 +6,19 @@ export enum Browser { Opera = "opera", Safari = "safari", } + +export interface VersionSpecific { + strictMinVersion?: string; + strictMaxVersion?: string; +} + +export interface GeckoSpecific extends VersionSpecific { + id?: string; + updateUrl?: string; +} + +export interface BrowserSpecific { + gecko?: GeckoSpecific; + geckoAndroid?: VersionSpecific; + safari?: VersionSpecific; +} diff --git a/src/types/config.ts b/src/types/config.ts index a77720f..6d464d5 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -2,7 +2,7 @@ import type {Configuration as RspackConfig, Filename} from "@rspack/core"; import type {Options as HtmlOptions} from "html-rspack-tags-plugin"; import {Command, Mode} from "@typing/app"; -import {Browser} from "@typing/browser"; +import {Browser, BrowserSpecific} from "@typing/browser"; import {ManifestIncognitoValue, ManifestVersion} from "@typing/manifest"; import {Plugin} from "@typing/plugin"; import {Language} from "@typing/locale"; @@ -10,8 +10,9 @@ import {Awaiter} from "@typing/helpers"; import {EnvFilterOptions, EnvFilterVariant} from "@typing/env"; /** - * Configuration object for defining various settings and build parameters - * for an extension application. + * Configuration options for building a browser extension. This interface defines + * all the properties required to customize the build process, extension metadata, + * file structure, and other build-related settings. */ export interface Config { /** @@ -151,15 +152,17 @@ export interface Config { icon: string; /** - * Used for Firefox under `browser_specific_settings.gecko.id`, - * but only if the "storage" permission is declared. - * Can be either: - * - a valid email - * - a function that returns the email + * Browser-specific settings (populate manifest.browser_specific_settings). + * + * Two forms are supported: + * - object — fixed values for browsers (e.g., gecko, safari); + * - function — lazy evaluation at build time; if it returns undefined, the field is omitted. * - * @default EMAIL + * Examples: + * - { gecko: { id: "addon@example.com", strictMinVersion: "109.0" } } + * - () => ({ gecko: { id: "addon@example.com" } }) */ - email: string | (() => string | undefined); + specific?: BrowserSpecific | (() => BrowserSpecific | undefined); /** * Used to specify how this extension will behave in incognito mode diff --git a/src/types/env.ts b/src/types/env.ts index bd598f8..14686d5 100644 --- a/src/types/env.ts +++ b/src/types/env.ts @@ -1,4 +1,4 @@ -export const ReservedEnvKeys = new Set(["APP", "BROWSER", "MODE", "MANIFEST_VERSION"]); +export const EnvReservedKeys = new Set(["APP", "BROWSER", "MODE", "MANIFEST_VERSION"]); export type EnvFilterFunction = (value: string) => boolean; @@ -6,5 +6,4 @@ export type EnvFilterVariant = string | EnvFilterFunction; export type EnvFilterOptions = { filter: EnvFilterVariant; - crypt: boolean; }; diff --git a/src/types/manifest.ts b/src/types/manifest.ts index 2014b41..ad0abb0 100644 --- a/src/types/manifest.ts +++ b/src/types/manifest.ts @@ -2,6 +2,7 @@ import {ContentScriptConfig} from "@typing/content"; import {BackgroundConfig} from "@typing/background"; import {CommandConfig} from "@typing/command"; import {Language} from "@typing/locale"; +import {BrowserSpecific} from "@typing/browser"; type ManifestCommon = chrome.runtime.Manifest; type ManifestBase = chrome.runtime.ManifestBase; @@ -86,8 +87,6 @@ export interface ManifestBuilder { setDescription(description?: string): this; - setEmail(email?: string): this; - setAuthor(author?: string): this; setHomepage(homepage?: string): this; @@ -100,6 +99,8 @@ export interface ManifestBuilder { setLocale(lang?: Language): this; + setSpecific(settings?: BrowserSpecific): this; + // Icons setIcons(icons?: ManifestIcons): this;