# Module Market This directory **is** the WebToApp Module Market. Every JS/CSS module the in-app market shows is fetched directly from this folder over `raw.githubusercontent.com`, with `cdn.jsdelivr.net/gh/` as a CDN fallback. There is no other backend. A merged PR is published the moment it lands on `main`. This file is the canonical contribution guide for the Module Market. The root `README.md` and `.github/CONTRIBUTING.md` only keep high-level summaries and link here for the actual submission rules. > **English** · [简体中文](#中文) --- ## At a glance ``` modules/ ├── registry.json ← index file the app downloads first ├── submissions.json ← CI-generated PR / contributor metadata ├── README.md ← this file └── / ← one folder per module ├── module.json ← module manifest (required) ├── main.js ← module source (required) ├── style.css ← optional CSS, auto-injected on load └── icon.png ← optional 256 KB-max icon (also .svg/.webp/.jpg) ``` When a user opens the market, the app fetches both `registry.json` and `submissions.json`, then renders each entry that appears in **both** — that's how we guarantee the catalog only shows modules whose PR has actually been merged. Tapping **Install** then downloads `/module.json` and `main.js` (plus `style.css` when `hasCss` is `true`) and hands the result to the local extension manager. The registry is cached for one hour; the refresh button bypasses the cache. > **Heads-up:** `registry.json` and `module.json` both have an `icon` > field that takes a [Material Icons](https://fonts.google.com/icons) name > (e.g. `"auto_awesome"`, `"dark_mode"`). For a real branded picture, set > the registry-level **`iconUrl`** to either a relative path (`"icon.png"`) > or an absolute `https://` URL. The relative form must reference one of > `icon.png` / `icon.svg` / `icon.webp` / `icon.jpg` / `icon.jpeg` next to > `main.js` and stay under 256 KB — larger or differently-named files will > fail CI validation. When neither field is set, the app shows a > first-letter avatar instead. --- ## Submitting a module 1. **Fork** [`shiaho777/web-to-app`](https://github.com/shiaho777/web-to-app). 2. Create a unique kebab-case folder under `modules/`, e.g. `modules/dark-reader-lite/`. The folder name is your `path` in `registry.json`. 3. Add at minimum: - `module.json` — manifest, see [schema](#modulejson-schema) - `main.js` — code that runs in the WebView 4. Add a matching entry to [`registry.json`](registry.json). Keep `id`, `name`, `version`, `runAt`, and `permissions` consistent between both files (the registry is the listing surface, the manifest is what gets installed). 5. Open a pull request. A maintainer reviews using the [reviewer checklist](#reviewer-checklist) and merges. CI validates the catalog automatically — see [Local validation](#local-validation) below if you want to self-check first. 6. Once merged, every client picks up the new module on its next refresh. There is no separate developer account, no API key, no submission portal. --- ## `module.json` schema ```json { "id": "globally-unique-id", "name": "Display Name", "description": "Paragraph shown on the install page.", "icon": "material-icon-name", "category": "OTHER", "tags": ["tag1", "tag2"], "version": { "code": 1, "name": "1.0.0", "changelog": "Initial release" }, "author": { "name": "Your Name", "url": "https://github.com/your-handle", "email": "optional@example.com" }, "runAt": "DOCUMENT_END", "urlMatches": [ { "pattern": "*", "isRegex": false, "exclude": false } ], "permissions": ["DOM_ACCESS"], "configItems": [ { "key": "greeting", "name": "Greeting text", "description": "Shown in the floating banner.", "type": "TEXT", "defaultValue": "Hello, WebToApp!", "required": false } ] } ``` Note that `version` here is an object with `code` (monotonically increasing integer), `name` (semver string), and `changelog`. In `registry.json` the matching field is just the semver string — same number, different shape. ### Allowed `category` values `CONTENT_FILTER`, `CONTENT_ENHANCE`, `STYLE_MODIFIER`, `THEME`, `FUNCTION_ENHANCE`, `AUTOMATION`, `NAVIGATION`, `DATA_EXTRACT`, `DATA_SAVE`, `INTERACTION`, `ACCESSIBILITY`, `MEDIA`, `VIDEO`, `IMAGE`, `AUDIO`, `SECURITY`, `ANTI_TRACKING`, `SOCIAL`, `SHOPPING`, `READING`, `TRANSLATE`, `DEVELOPER`, `OTHER`. Unknown values fall back to `OTHER` — it does not break the install, just hides the module from the category filter chips. ### Allowed `runAt` values `DOCUMENT_START`, `DOCUMENT_END`, `DOCUMENT_IDLE`, `CONTEXT_MENU`, `BEFORE_UNLOAD`. Defaults to `DOCUMENT_END` if omitted. ### Allowed `permissions` values The list is informational on the install screen; the runtime does not sandbox by it. Reviewers use it to spot dangerous capabilities. `DOM_ACCESS`, `DOM_OBSERVE`, `CSS_INJECT`, `STORAGE`, `COOKIE`, `INDEXED_DB`, `CACHE`, `NETWORK`, `WEBSOCKET`, `FETCH_INTERCEPT`, `CLIPBOARD`, `NOTIFICATION`, `ALERT`, `KEYBOARD`, `MOUSE`, `TOUCH`, `LOCATION`, `CAMERA`, `MICROPHONE`, `DEVICE_INFO`, `MEDIA`, `FULLSCREEN`, `PICTURE_IN_PICTURE`, `SCREEN_CAPTURE`, `DOWNLOAD`, `FILE_ACCESS`, `EVAL`, `IFRAME`, `WINDOW_OPEN`, `HISTORY`, `NAVIGATION`. The dangerous ones (`COOKIE`, `INDEXED_DB`, `NETWORK`, `WEBSOCKET`, `FETCH_INTERCEPT`, `CLIPBOARD`, `LOCATION`, `CAMERA`, `MICROPHONE`, `SCREEN_CAPTURE`, `FILE_ACCESS`, `EVAL`, `IFRAME`) get extra scrutiny on review. ### Allowed `configItems[].type` values `TEXT`, `TEXTAREA`, `NUMBER`, `BOOLEAN`, `SELECT`, `MULTI_SELECT`, `RADIO`, `CHECKBOX`, `COLOR`, `URL`, `EMAIL`, `PASSWORD`, `REGEX`, `CSS_SELECTOR`, `JAVASCRIPT`, `JSON`, `RANGE`, `DATE`, `TIME`, `DATETIME`, `FILE`, `IMAGE`. ### `urlMatches` patterns Two pattern flavours: - **`isRegex: false`** (recommended): a Chrome-extension-style glob. - `*` matches any number of characters - `*://...` expands to `(https?|ftp|file)://` - `` and `*` both mean "every URL" - **`isRegex: true`**: a Java-style regex evaluated with a 200ms timeout per URL match. Set `exclude: true` to make a rule subtract from the match set. If only exclude rules are present, the module matches every other URL. --- ## `registry.json` entry schema The registry is the index the app fetches first to render the list. Each entry mirrors the module manifest plus a `path` (folder name) and a `hasCss` flag. ```json { "id": "globally-unique-id", "path": "folder-name-under-modules", "name": "Display Name", "description": "One-line summary", "icon": "material-icon-name", "category": "OTHER", "tags": ["tag1", "tag2"], "version": "1.0.0", "minAppVersion": 33, "author": { "name": "Your Name", "url": "https://github.com/your-handle" }, "runAt": "DOCUMENT_END", "permissions": ["DOM_ACCESS", "CSS_INJECT"], "urlMatches": [ { "pattern": "*", "isRegex": false, "exclude": false } ], "hasCss": false, "iconUrl": "icon.png" } ``` `minAppVersion` lets you ship a module that needs APIs only present from a specific WebToApp `versionCode` onwards — older clients hide the entry. The current `versionCode` is **47** (`v2.1.8`); set this only if you genuinely depend on a newer build. `iconUrl` is optional. Either a relative path (`"icon.png"`, `"icon.svg"`, `"icon.webp"`, `"icon.jpg"`, `"icon.jpeg"`) referencing a file in your module folder, or an absolute `https://` URL pointing at off-repo hosting. Relative icons must stay under 256 KB. When `iconUrl` is absent, the app shows a circular avatar with the module's first letter. --- ## Who shows up in the catalog (`submissions.json`) `submissions.json` is generated by the [`Module Market Publish`](../.github/workflows/modules-publish.yml) workflow on every push to `main`. It contains one entry per module that has actually landed on `main`, with PR number, merge timestamp, the GitHub identity of whoever opened the PR, and the GitHub identities of everyone else who has committed to that module's folder (the `contributors` list). The in-app market renders the original submitter plus these contributors as stacked avatars, and aggregates them into a contributors leaderboard. **The in-app market only shows modules that appear in `submissions.json`.** That's the entire mechanism behind "only merged PRs are visible" — there is no allow-listing inside the app, no client-side filter to bypass; if your module isn't in this file, users won't see it. The publish workflow: 1. On push to `main` that touches `modules/`, walks every folder. 2. For each folder, finds the introducing commit via `git log --diff-filter=A`. 3. Asks `GET /repos/{owner}/{repo}/commits/{sha}/pulls` whether that commit was part of a merged PR. If yes → records PR number, URL, `merged_at`, and the PR author's GitHub login + avatar. 4. Otherwise treats it as a maintainer direct-push and records the commit author — but only when the GitHub login matches the `MAINTAINERS` allow-list in the workflow. Anything else is left out on purpose. 5. For every module, walks its full commit history, resolves each distinct author's GitHub login via the commit API, and records everyone except the original submitter (and bots) under `contributors`. 6. Commits the regenerated file back to `main`. You don't have to do anything as a contributor — just merge a PR and wait a few seconds for the publish workflow to run. --- ## How `main.js` runs The runtime wraps your code in an IIFE before injection, with these globals available: | Global | Value | | --- | --- | | `__MODULE_INFO__` | `{ id, name, icon, version, uiConfig, runMode }` | | `__MODULE_CONFIG__` | object of `{ key: value }` from saved config | | `__MODULE_UI_CONFIG__` | the UI panel config (mirrors `__MODULE_INFO__.uiConfig`) | | `__MODULE_RUN_MODE__` | `'INTERACTIVE'` or `'AUTO'` | | `getConfig(key, defaultValue)` | convenience accessor for `__MODULE_CONFIG__` | Your code is also wrapped in a `try/catch` — uncaught errors are logged to `console.error` with the module name as a prefix and do **not** abort the page. So you do not need to wrap everything yourself, but you do need to think about silent failures. If you ship `style.css` (and set `hasCss: true` in `registry.json`), it is injected as a `