项目文件夹

文件
2026-07-13 12:31:12 +08:00

570 行
23 KiB
Markdown

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# 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
└── <module-path>/ ← 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-path>/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)://`
- `<all_urls>` 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 `<style>` tag with `id="ext-module-<module-id>"` immediately
before your code runs.
A minimal hello-world:
```js
(function () {
var greeting = getConfig('greeting', 'Hello!');
var banner = document.createElement('div');
banner.textContent = greeting;
banner.style.cssText = 'position:fixed;top:24px;left:50%;' +
'transform:translateX(-50%);padding:10px 16px;background:#111;' +
'color:#fff;border-radius:12px;z-index:2147483647;';
document.body.appendChild(banner);
setTimeout(function () { banner.remove(); }, 3000);
})();
```
See [`hello-world/main.js`](hello-world/main.js) and
[`reading-mode/main.js`](reading-mode/main.js) for fuller examples.
### Optional: register a panel button
If your module ships an interactive UI, register it with the floating
panel so the user can summon it:
```js
__WTA_MODULE_UI__.register({
id: __MODULE_INFO__.id,
name: __MODULE_INFO__.name,
icon: __MODULE_INFO__.icon,
uiConfig: __MODULE_UI_CONFIG__,
runMode: __MODULE_RUN_MODE__,
onClick: function () {
// open your UI
}
});
```
If you skip this and your module has any `configItems`, the runtime will
auto-register a default entry so the user can still open module settings.
---
## Versioning
Bump `version.code` and `version.name` in `module.json` **and** `version`
in `registry.json` together when you publish an update. The client
compares semver against the locally installed copy and offers a one-tap
upgrade.
User config is preserved across updates: keys still declared in the new
manifest's `configItems` keep their values, keys you removed get cleaned
up. So renaming a config key resets it to its default — bump the version
and document the change in `version.changelog`.
---
## Local validation
The repo ships a Python validator that mirrors the install-time checks the
app performs, plus a few correctness rules CI uses to gate PRs:
```
python3 .github/scripts/ci/validate_modules.py
```
It uses only the standard library, so no `pip install` step is needed.
The same script runs in [GitHub Actions](../.github/workflows/modules-check.yml)
on every PR that touches `modules/`. A green CI run is required before a
maintainer can merge.
The validator catches:
- Invalid JSON in `registry.json` or any `module.json`
- Missing required fields (`id`, `name`, `version`)
- Unknown values for `category`, `runAt`, `permissions`,
`configItems[].type`
- Cross-file mismatches between `registry.json` and `module.json`
(`id`, `name`, `version`, `runAt`, missing permissions)
- Folder names that aren't kebab-case
- Modules folder ↔ registry entry mismatches (orphan folders / ghost
registry entries)
- Duplicate `id`s or `path`s
- Missing required files (`module.json`, `main.js`)
- Stray files (warns about extra dirs, and icon files that exist on disk but
are not referenced by `iconUrl`)
- `hasCss` flag disagreeing with whether `style.css` is present on disk
- `iconUrl` referencing a missing or oversized file in the module folder
- Top-level `return` in `main.js` (would break inside the IIFE wrapper)
- `getConfig(...)` calls without matching `configItems`
---
## Reviewer checklist
For maintainers — this is what gets checked before a merge.
- [ ] Folder name and `id` are unique and kebab-case
- [ ] `module.json` and `registry.json` agree on `id`, `name`, `version`,
`runAt`, `permissions`, and `urlMatches`
- [ ] `main.js` is readable; no obfuscation without a source link in the PR
- [ ] No unconditional network calls to third-party endpoints
- [ ] No reading of `document.cookie` or auth tokens unless declared in
`permissions`
- [ ] `urlMatches` is appropriately scoped (avoid `*` for invasive modules)
- [ ] `main.js` runs cleanly inside the IIFE wrapper (no top-level `return`)
- [ ] `runAt` matches what the code expects — `DOCUMENT_START` modules must
not assume `document.body` exists
- [ ] `version.code` was bumped if `version.name` changed
- [ ] `hasCss` is `true` if and only if `style.css` is present in the folder
- [ ] If `iconUrl` is set, the file exists, is under 256 KB, and uses one
of the allowed extensions (`png`, `svg`, `webp`, `jpg`, `jpeg`)
- [ ] No unreferenced icon files or other extra files that the runtime cannot
reach from `iconUrl`
---
<a id="中文"></a>
## 中文
这个目录就是 WebToApp 的**模块市场**。所有用户在 App 里看到的市场模块都是直
接从这个文件夹通过 `raw.githubusercontent.com` 拉取的,CDN 兜底是
`cdn.jsdelivr.net/gh/`。**没有其他后端**。PR 一旦合并到 `main`,下一刻就上
线。
本文件是模块市场投稿规则的唯一主文档。仓库根目录 `README.md`
`.github/CONTRIBUTING.md` 只保留高层说明,真正的字段规则和提交流程都以这里
为准。
### 目录结构
```
modules/
├── registry.json ← App 首次下载的索引
├── submissions.json ← CI 生成:PR / 贡献者元数据
├── README.md ← 本文件
└── <模块目录>/
├── module.json ← 模块清单(必需)
├── main.js ← 模块代码(必需)
├── style.css ← 可选 CSS,加载时自动注入
└── icon.png ← 可选图标,最大 256 KB(也支持 .svg/.webp/.jpg
```
App 打开市场时同时拉 `registry.json``submissions.json`,**只渲染两边都
出现的条目**——这是"只展示已合并 PR" 这条承诺的实现机制。点击安装时才拉
对应模块的 `module.json``main.js`(外加 `style.css`,如果 `hasCss`
`true`),然后交给本地的扩展管理器。registry 缓存 1 小时,刷新按钮可绕开
缓存。
> **图标:** `registry.json` 里的 `iconUrl` 字段可以指向模块目录里的图片
> `"icon.png"`、`"icon.svg"`、`"icon.webp"`、`"icon.jpg"`、
> `"icon.jpeg"` 之一,最大 256 KB),也可以是绝对 `https://` URL。如果
> 不填,App 会用模块名首字母自动生成圆形头像。`icon`/`module.json::icon`
> 是 [Material Icons](https://fonts.google.com/icons) 的字符串名,
> 当前主要用于本地内置模块。
### `submissions.json`:决定谁出现在市场
`submissions.json`
[`Module Market Publish`](../.github/workflows/modules-publish.yml) 工作
流在每次 `main` 分支推送时自动生成,每条对应一个真正落地到 `main` 的模块,
带 PR 编号、合并时间、PR 作者的 GitHub 身份,以及该模块目录下所有其他提交者
的 GitHub 身份(`contributors` 列表)。App 端市场会把原始提交者和这些贡献者
以叠加头像的形式展示出来,并聚合出一个贡献者榜单。
**App 端的市场只会展示出现在这个文件里的模块**——这是"只展示已合并 PR"的
全部机制:客户端没有别的过滤,模块如果不在这里,用户根本看不到。
工作流逻辑:
1. 推送到 `main` 且改了 `modules/`,遍历所有模块目录。
2.`git log --diff-filter=A` 找出每个目录的引入提交。
3. 调用 `GET /repos/{owner}/{repo}/commits/{sha}/pulls` 看这个提交是不是
某个已合并 PR 的一部分。是的话记录 PR 编号、URL、`merged_at`、PR 作者
的 GitHub login + 头像。
4. 否则当作维护者直推处理,记录提交作者——但**只有当 GitHub login 在
workflow 的 `MAINTAINERS` 白名单里**才会被记录。其他直推故意不进。
5. 针对每个模块,遍历其完整提交历史,用 commit API 解析出每位作者的 GitHub
login,把除原始提交者(以及 bot)之外的人都记到 `contributors` 里。
6. 把生成的文件 commit 回 `main`
作为贡献者你不用做任何事——PR 合并几秒钟之后这个工作流自动跑完,下次有
人打开市场就能看到你的模块。
### 提交流程
1. Fork [`shiaho777/web-to-app`](https://github.com/shiaho777/web-to-app)
2.`modules/` 下新建一个唯一的 kebab-case 目录,比如
`modules/dark-reader-lite/`。文件夹名就是 `registry.json` 里的 `path`
3. 至少包含:
- `module.json` — 清单(schema 见英文段)
- `main.js` — 在 WebView 中执行的代码
4. 在 [`registry.json`](registry.json) 中加一条对应记录,保持 `id``name`
`version``runAt``permissions` 在两个文件之间一致
5. 提 PR;维护者按 [审核 Checklist](#审核-checklist) 审查并合并。CI 会自动
校验 modules/ 目录——见下方 [本地校验](#本地校验),可以提交前先自查
6. 合并后所有客户端在下次刷新(默认 1 小时缓存)即可看到
### 字段差异
`module.json` 里的 `version` 是对象(`code` 整数、`name` 字符串、
`changelog`),`registry.json` 里的 `version` 直接是 `name` 那个字符串。
版本号要保持一致。
`category``runAt``permissions``configItems[].type` 的允许值与英文段
完全相同。
### `urlMatches` 模式
- `isRegex: false`(推荐):Chrome 扩展风格的 glob
- `*` 匹配任意字符
- `*://...` 展开为 `(https?|ftp|file)://`
- `<all_urls>``*` 都表示"匹配所有 URL"
- `isRegex: true`:Java 正则,每次匹配有 200ms 超时
`exclude: true` 让一条规则从结果集中扣除。如果只有 exclude 规则,模块匹
配除此之外的所有 URL。
### `main.js` 注入合约
代码会被包在 IIFE 里执行,注入的全局变量:
| 全局 | 值 |
| --- | --- |
| `__MODULE_INFO__` | `{ id, name, icon, version, uiConfig, runMode }` |
| `__MODULE_CONFIG__` | 用户保存的 `{ key: value }` 配置 |
| `__MODULE_UI_CONFIG__` | 面板 UI 配置 |
| `__MODULE_RUN_MODE__` | `'INTERACTIVE'``'AUTO'` |
| `getConfig(key, defaultValue)` | `__MODULE_CONFIG__` 的便捷访问器 |
代码外层还有一层 `try/catch` —— 抛出的异常会以模块名为前缀写到
`console.error`,不会中断页面其他注入。所以你不必自己再包一层 `try/catch`
但要意识到错误是静默吞掉的。
如果带 `style.css`(同时把 `registry.json` 里的 `hasCss` 设为 `true`),
它会以 `id="ext-module-<模块id>"``<style>` 标签在你代码运行之前注入。
如果模块带了 `configItems`、但你没调用 `__WTA_MODULE_UI__.register({...})`
注册一个面板按钮,运行时会自动给用户加一个入口,至少能让用户打开模块设置。
### 版本升级
发新版时**同时**升 `module.json``version.code`/`version.name`
`registry.json``version`。客户端会用 semver 比较版本提示更新。
更新会保留用户配置:新清单 `configItems` 里仍然存在的 key,值会被保留;移
除的 key 会被自动清掉。所以重命名 config key 等于重置它——把更新写进
`version.changelog`
### 本地校验
仓库里有一份 Python 校验脚本,模拟 App 安装时做的检查 + CI 用来卡 PR 的几条
规则:
```
python3 .github/scripts/ci/validate_modules.py
```
只用了标准库,无需 `pip install`。同样的脚本会在
[GitHub Actions](../.github/workflows/modules-check.yml) 里跑——任何动到
`modules/` 的 PR 都必须 CI 通过才能合并。
校验内容:
- `registry.json` 或任何 `module.json` 的 JSON 解析错误
- 必填字段缺失(`id``name``version`
- `category` / `runAt` / `permissions` / `configItems[].type` 的非法枚举值
- `registry.json``module.json` 之间的字段不一致(`id``name`
`version``runAt`、漏报权限)
- 文件夹名不是 kebab-case
- modules 目录与 registry 条目不对齐(孤立目录 / 鬼条目)
- 重复的 `id``path`
- 缺少必需文件(`module.json``main.js`
- 多余文件(例如未被 `iconUrl` 引用的图标文件、额外子目录等会给 warning)
- `hasCss` 标志和 `style.css` 文件存在与否不匹配
- `iconUrl` 引用了不存在或超过 256 KB 的图片
- `main.js` 顶层 `return`(在 IIFE 包裹里会变成语法错误)
- `getConfig(...)` 调用但 `configItems` 没声明对应字段
### 审核 Checklist
- [ ] 目录名和 `id` 唯一且为 kebab-case
- [ ] `module.json``registry.json``id`/`name`/`version`/`runAt`/
`permissions`/`urlMatches` 一致
- [ ] `main.js` 可读;没有特殊说明的话不接受混淆/压缩代码
- [ ] 没有无条件调用第三方网络
- [ ] 没有未在 `permissions` 中声明就读取 `document.cookie` 或鉴权 token
- [ ] `urlMatches` 范围合理(侵入性强的模块不要无脑写 `*`
- [ ] 代码能在 IIFE 包裹下正常执行(不要写顶层 `return`
- [ ] `runAt` 与代码预期一致(`DOCUMENT_START` 不能假设 `document.body`
已存在)
- [ ] `version.name` 变了的话 `version.code` 也要随之 +1
- [ ] `hasCss` 当且仅当目录里有 `style.css` 时才为 `true`
- [ ] 如果设置了 `iconUrl`,对应文件存在、不超过 256 KB,扩展名是
`png`/`svg`/`webp`/`jpg`/`jpeg` 之一
- [ ] 不要放未被 `iconUrl` 引用的图标文件或其他运行时根本用不到的冗余文件