Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a99b779
Merge main into develop
github-actions[bot] Feb 4, 2026
491dc83
refactor(Manifest): add raw method to manifestBuilder, add plugin and…
RostyslavNihrutsa Feb 25, 2026
ddc543b
refactor(Manifest): improve permissions and host permissions logic, i…
RostyslavNihrutsa Feb 26, 2026
40427f3
chore: merge pull request #95 from RostyslavNihrutsa/develop
atldays Feb 26, 2026
83db7c1
refactor(manifest): streamline `combined*` methods for readability an…
atldays Feb 26, 2026
7869818
chore: simplify documentation for `EntrypointOptions` by removing red…
atldays Mar 4, 2026
9c1dbcf
refactor(finder): improve sorting logic and enhance priority handling
atldays Apr 30, 2026
4ae0c7b
refactor(finder): restructure file collection logic to support groupe…
atldays Apr 30, 2026
0ff5c34
feat(config): add `shared` option for configurable shared source layer
atldays Apr 30, 2026
c0e9464
refactor(command): enhance shortcut key validation and add tests for …
atldays Apr 30, 2026
c02a4dd
fix(command): simplify error message for invalid command key options
atldays Apr 30, 2026
7864487
perf(build): enable separate TypeScript declaration file generation
atldays May 2, 2026
1afe794
chore(tsconfig): reformat include list and update exclude patterns
atldays May 2, 2026
1d5f3cd
fix(parsers): exclude `this` parameter from signature generation
atldays May 8, 2026
6acda7d
fix(parsers): correct regex patterns for object type formatting in `S…
atldays May 8, 2026
2e095da
chore(manifest): remove redundant comment in URL match validation logic
atldays May 8, 2026
db39da2
feat(offscreen): add lifecycle tests and improve iframe readiness han…
atldays May 8, 2026
209be45
chore(deps): bump lodash to v4.18.1
atldays May 8, 2026
409bf0b
chore(deps): update `ts-node` to v10.9.2 and clean up outdated depend…
atldays May 8, 2026
31c2407
chore(docs): remove projects skills
atldays May 8, 2026
d3b05a6
fix(locale): validate locale contract and tighten substitutions
atldays May 11, 2026
8469d92
feat(workspace): add workspace mode for single and multi app structure
atldays May 11, 2026
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
.env
.env.*
.idea
.codex
.claude
.output
addon
*.log
Expand Down
2 changes: 0 additions & 2 deletions .mailmap

This file was deleted.

5,000 changes: 2,452 additions & 2,548 deletions package-lock.json

Large diffs are not rendered by default.

18 changes: 8 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"version": "0.5.7",
"description": "Addon Bone - Cross-browser web extension framework with shared code base",
"homepage": "https://addonbone.com",
"author": "Anjey Tsibylskij (https://github.com/atldays)",
"license": "MIT",
"repository": {
"type": "git",
Expand All @@ -27,10 +28,6 @@
"opera",
"safari"
],
"author": "Addon Stack <addonbonedev@gmail.com>",
"contributors": [
"Anjey Tsibylskij (https://github.com/atldays)"
],
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
Expand Down Expand Up @@ -104,7 +101,7 @@
"prepare": "husky",
"preinstall": "npm run check:node-version",
"prepublishOnly": "npm run build",
"build": "cross-env NODE_OPTIONS=--max-old-space-size=15046 tsup && node ./scripts/copy-dts.js",
"build": "tsup && tsc -p tsconfig.build.json && node ./scripts/copy-dts.js && tsc-alias -p tsconfig.build.json -f -fe .js",
"format": "prettier --write .",
"typecheck": "tsc -p tsconfig.json --noEmit",
"check:node-version": "node ./scripts/check-node-version.js",
Expand All @@ -123,9 +120,9 @@
"release:preview": "release-it --no-github.release --no-npm.publish --no-git.tag --ci"
},
"dependencies": {
"@addon-core/browser": "^0.2.1",
"@addon-core/browser": "^0.6.0",
"@addon-core/inject-script": "^0.3.1",
"@addon-core/storage": "^0.4.0",
"@addon-core/storage": "^0.6.0",
"@rsdoctor/rspack-plugin": "^1.5.1",
"@rspack/cli": "^1.7.5",
"@rspack/core": "^1.7.5",
Expand All @@ -141,7 +138,7 @@
"html-rspack-tags-plugin": "^0.0.3",
"js-yaml": "^4.1.1",
"json-stringify-deterministic": "^1.0.12",
"lodash": "4.17.23",
"lodash": "4.18.1",
"mini-css-extract-plugin": "^2.9.2",
"nanoid": "^5.1.4",
"pluralize": "^8.0.0",
Expand All @@ -157,7 +154,6 @@
"zod": "^3.24.2"
},
"devDependencies": {
"glob": "^13.0.1",
"@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0",
"@microsoft/api-extractor": "^7.53.1",
Expand All @@ -181,14 +177,16 @@
"esbuild-fix-imports-plugin": "^1.0.22",
"esbuild-plugin-raw": "^0.3.0",
"fs-extra": "^11.3.0",
"glob": "^13.0.1",
"globby": "^14.1.0",
"husky": "^9.1.7",
"jest": "^30.2.0",
"jest-environment-jsdom": "^30.0.0",
"jest-webextension-mock": "^4.0.0",
"prettier": "^3.6.2",
"release-it": "^19.2.3",
"ts-node": "^1.7.1",
"ts-node": "10.9.2",
"tsc-alias": "^1.8.17",
"tsup": "^8.5.0",
"tsx": "^4.19.2",
"uglify-js": "^3.19.3",
Expand Down
115 changes: 115 additions & 0 deletions src/cli/builders/locale/LocaleBuilder.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import LocaleBuilder from "./LocaleBuilder";

import {Language, LocaleCustomKeyForLanguage, LocaleValuesSeparator} from "@typing/locale";
import {Browser} from "@typing/browser";

const makeValidator = () => ({
isValid: jest.fn(),
validate: jest.fn(),
});

describe("LocaleBuilder", () => {
test("returns the builder language", () => {
expect(new LocaleBuilder(Browser.Chrome, Language.French).lang()).toBe(Language.French);
});

test("flattens nested data, stringifies values and injects the language marker", () => {
const builder = new LocaleBuilder(Browser.Chrome, Language.English).merge({
app: {
name: "My App",
version: 2,
items: ["one", 2],
},
});

expect(Object.fromEntries(builder.get())).toEqual({
"app.name": "My App",
"app.version": "2",
"app.items": ["one", "2"].join(LocaleValuesSeparator),
[LocaleCustomKeyForLanguage]: Language.English,
});
});

test("lets later merged data override earlier values and clears cached items", () => {
const builder = new LocaleBuilder(Browser.Chrome, Language.English).merge({
title: "Before",
});

const first = builder.get();

builder.merge({
title: "After",
});

const second = builder.get();

expect(second).not.toBe(first);
expect(second.get("title")).toBe("After");
});

test("returns locale keys from converted items", () => {
const builder = new LocaleBuilder(Browser.Chrome, Language.English).merge({
app: {
name: "My App",
},
});

expect(builder.keys()).toEqual(new Set(["app.name", LocaleCustomKeyForLanguage]));
});

test("normalizes substitution names in locale structure", () => {
const builder = new LocaleBuilder(Browser.Chrome, Language.English).merge({
greeting: "Hello {{ name }} {{name}} {{ count }}",
cars: ["{{count}} car", "{{count}} cars"],
});

expect(builder.structure().greeting.substitutions).toEqual(["count", "name"]);
expect(builder.structure().cars).toEqual({
plural: true,
substitutions: ["count"],
});
});

test("builds browser messages and validates before output", () => {
const validator = makeValidator();
const builder = new LocaleBuilder(Browser.Chrome, Language.English).setValidator(validator).merge({
app: {
name: "My App",
},
});

expect(builder.build()).toEqual({
app_name: {
message: "My App",
},
[LocaleCustomKeyForLanguage]: {
message: Language.English,
},
});

expect(validator.validate).toHaveBeenCalledWith(builder);
});

test("throws validation errors when validator is not set", () => {
const builder = new LocaleBuilder(Browser.Chrome, Language.English);

expect(() => builder.validate()).toThrow("Locale for chrome:en - Validator is not set");
expect(builder.isValid()).toBe(false);
});

test("reports validation status from configured validator", () => {
const passingValidator = makeValidator();
const failingValidator = makeValidator();

failingValidator.validate.mockImplementation(() => {
throw new Error("Invalid locale");
});

const passing = new LocaleBuilder(Browser.Chrome, Language.English).setValidator(passingValidator);
const failing = new LocaleBuilder(Browser.Chrome, Language.English).setValidator(failingValidator);

expect(passing.validate()).toBe(passing);
expect(passing.isValid()).toBe(true);
expect(failing.isValid()).toBe(false);
});
});
2 changes: 1 addition & 1 deletion src/cli/builders/locale/LocaleBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export default class LocaleBuilder implements LocaleBuilderContract {
substitutions.push(match[1].trim());
}

return substitutions;
return _.uniq(substitutions.filter(Boolean)).sort();
};

return this.get()
Expand Down
117 changes: 117 additions & 0 deletions src/cli/builders/locale/LocaleStructureValidator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import LocaleBuilder from "./LocaleBuilder";
import LocaleStructureValidator from "./LocaleStructureValidator";

import {Language, LocaleBuilders, LocaleData} from "@typing/locale";
import {Browser} from "@typing/browser";

const makeBuilder = (lang: Language, data: LocaleData): LocaleBuilder => {
return new LocaleBuilder(Browser.Chrome, lang).merge(data);
};

const makeBuilders = (items: Array<[Language, LocaleData]>): LocaleBuilders => {
return new Map(items.map(([lang, data]) => [lang, makeBuilder(lang, data)]));
};

describe("LocaleStructureValidator", () => {
let consoleWarnSpy: jest.SpyInstance;

beforeEach(() => {
consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation();
});

afterEach(() => {
consoleWarnSpy.mockRestore();
});

test("allows non-default locales to omit default locale keys", () => {
const builders = makeBuilders([
[
Language.English,
{
app: {
name: "My App",
greeting: "Hello {{name}}",
},
},
],
[
Language.French,
{
app: {
name: "Mon App",
},
},
],
]);

const validator = new LocaleStructureValidator(Language.English);

expect(validator.validate(builders)).toBe(validator);
expect(validator.isValid(builders)).toBe(true);
});

test("rejects missing default locale", () => {
const builders = makeBuilders([
[
Language.French,
{
app: {
name: "Mon App",
},
},
],
]);

expect(() => new LocaleStructureValidator(Language.English).validate(builders)).toThrow(
'Default locale "en" not found in available translations. Available languages: fr'
);
});

test("warns about keys outside the default locale contract", () => {
const builders = makeBuilders([
[Language.English, {app: {name: "My App"}}],
[Language.French, {app: {name: "Mon App", extra: "Extra"}}],
]);

const validator = new LocaleStructureValidator(Language.English);

expect(validator.validate(builders)).toBe(validator);
expect(consoleWarnSpy).toHaveBeenCalledWith(
'Locale "fr" contains unknown key "app.extra" not found in default locale "en"'
);
expect(validator.isValid(builders)).toBe(true);
});

test("rejects substitution mismatch against the default locale contract", () => {
const builders = makeBuilders([
[Language.English, {app: {greeting: "Hello {{ name }}"}}],
[Language.French, {app: {greeting: "Bonjour {{firstName}}"}}],
]);

const validator = new LocaleStructureValidator(Language.English);

expect(() => validator.validate(builders)).toThrow(
'Locale "fr" key "app.greeting" substitutions [firstName] must match default locale "en" substitutions [name]'
);
expect(validator.isValid(builders)).toBe(false);
});

test("rejects plural mismatch against the default locale contract", () => {
const builders = makeBuilders([
[Language.English, {app: {cars: ["car", "cars"]}}],
[Language.French, {app: {cars: "voiture"}}],
]);

expect(() => new LocaleStructureValidator(Language.English).validate(builders)).toThrow(
'Locale "fr" key "app.cars" must be plural like default locale "en"'
);
});

test("allows empty builder collection", () => {
const builders: LocaleBuilders = new Map();
const validator = new LocaleStructureValidator(Language.English);

expect(validator.validate(builders)).toBe(validator);
expect(validator.isValid(builders)).toBe(true);
});
});
Loading
Loading