Compare commits

..
64 Commits
Author SHA1 Message Date
xixu-me 8a028ed239 fix(actions): avoid status event fan-out 2026-08-18 11:29:42 +08:00
xixu-me 12823a29fc fix(actions): retry Dependabot merge after checks 2026-08-18 11:25:31 +08:00
xixu-me 625f456f01 fix(ci): evaluate all Dependabot pull requests 2026-08-14 18:15:02 +08:00
xixu-me 787f59a6fa ci: trigger Dependabot auto-merge from workflow runs 2026-07-03 23:32:39 +08:00
xixu-me 7591fe6e74 ci: tolerate protected Dependabot workflow updates 2026-07-03 23:13:02 +08:00
xixu-me b9280aaa3a ci: add Dependabot auto merge workflow 2026-07-03 23:02:17 +08:00
xixu-me 8ed4f4fe71 fix: update LICENSE link format in README 2026-03-31 04:04:32 +08:00
xixu-me ab11fcfabc chore: switch license to MIT and refresh README 2026-03-31 04:01:42 +08:00
xixu-me c27e34a963 Fix copyright notice formatting in README 2025-10-12 17:48:56 +08:00
xixu-me b5fc50e236 Remove service configuration section from README
Deleted the 'Service Configuration' section from the README to streamline documentation and focus on location configuration details.
2025-07-14 10:18:36 +08:00
xixu-me 792540a0ee Update wording in README for META.js Customizer
Clarified the description of the META.js Customizer tool to refer to creating custom JavaScript instead of custom META.js.
2025-07-14 10:17:28 +08:00
xixu-me 2de8afcceb Clarify customization section in README
Updated README to specify that the customization section is in the JavaScript code, not the configuration file.
2025-07-14 10:16:54 +08:00
xixu-me 307c56f043 Clarify GitHub URL label in README
Updated the label for the GitHub URL from 'GitHub' to 'GitHub blob' to more accurately describe the link destination.
2025-07-14 10:15:13 +08:00
xixu-me 9ba7312a33 Update GitHub URL in README
Changed the import URL from GitHub Raw to the standard GitHub blob link for improved clarity and accessibility.
2025-07-14 10:14:30 +08:00
xixu-me 7de0450b81 Update section headers in README
Simplified the section headers for JavaScript and YAML configuration URLs by removing '(Dynamic)' and '(Static)' descriptors for clarity.
2025-07-14 10:12:24 +08:00
xixu-me 18c0f75e4c Update README with customization and related projects
Added information about the META.js Customizer web app and RFM rulesets integration. Expanded usage guidelines and introduced a new 'Related Projects' section for better user guidance. Minor wording improvements for clarity.
2025-07-14 10:11:40 +08:00
xixu-me 84d63043ed Update README with simplified META overview
Revised the README to provide a more concise and streamlined overview of META's features, usage, and customization options. Removed redundant sections and reorganized content for better readability and clarity.
2025-06-10 16:56:25 +08:00
xixu-me af3833183c Delete .github directory 2025-06-10 16:37:51 +08:00
xixu-me 3aad4f58b5 Update README.md 2025-05-27 23:03:15 +08:00
xixu-me ed784c8933 Update README.md 2025-05-27 21:28:32 +08:00
xixu-me ded7c0e75a Update README.md 2025-05-27 21:10:26 +08:00
xixu-me 45059ecb01 Update README.md 2025-05-27 20:45:04 +08:00
xixu-me 8d828284a8 Delete .github/FUNDING.yml 2025-05-27 20:10:07 +08:00
xixu-me 6c8bf804f7 Update README.md 2025-03-29 19:45:28 +08:00
xixu-me 848924697c Update README.md 2025-01-25 10:14:43 +08:00
xixu-me da0d8fa62b Update README.md 2025-01-25 10:12:56 +08:00
xixu-me e7762ac45a Delete run.yml 2025-01-14 19:01:48 +08:00
xixu-me 78076ac090 Create dependabot.yml 2025-01-14 18:45:12 +08:00
xixu-me d0b0d5ea8d Create run.yml 2025-01-14 18:43:17 +08:00
xixu-me 95a79e426b Delete generate-yaml-configuration.yml 2025-01-14 18:43:14 +08:00
xixu-me 852f30ed4e Delete check-and-purge-cdn-cache.yml 2025-01-14 18:43:12 +08:00
xixu-me 77d6e3c8ae Create check-and-purge-cdn-cache.yml 2025-01-14 18:37:03 +08:00
xixu-me 837f21b6ff Create generate-yaml-configuration.yml 2025-01-14 18:37:01 +08:00
xixu-me b199a5e73d Update README.md 2025-01-12 18:37:31 +08:00
xixu-me 8044ece749 Update README.md 2025-01-11 16:59:00 +08:00
xixu-me 8b404a8ee7 Update README.md 2025-01-11 16:55:15 +08:00
xixu-me a55e705bbe Update README.md 2025-01-11 16:54:10 +08:00
xixu-me acad00f9d4 Update README.md 2025-01-11 13:18:05 +08:00
xixu-me e323bde06e Update README.md 2025-01-10 02:15:09 +08:00
xixu-me 655cd09f97 Update README.md 2025-01-09 22:51:49 +08:00
xixu-me 774e62f424 Update README.md 2024-12-07 09:53:10 +08:00
xixu-me d0d4ac01df Update README.md 2024-11-04 12:16:58 +08:00
xixu-me a4d8144e9b Update README.md 2024-11-03 20:20:22 +08:00
xixu-me 90a26c8163 Update README.md 2024-11-03 19:57:14 +08:00
xixu-me ed69bf3155 Update README.md 2024-11-03 19:56:52 +08:00
xixu-me cfe8ac9677 Update README.md 2024-11-03 19:56:30 +08:00
xixu-me e30291a356 Update README.md 2024-11-03 19:51:35 +08:00
xixu-me debc730224 Update README.md 2024-11-03 19:50:42 +08:00
xixu-me 287c0cff4e Update README.md 2024-11-03 19:38:45 +08:00
xixu-me 05dc774bc8 Update README.md 2024-11-02 16:37:46 +08:00
xixu-me 7eeebf362a Update README.md 2024-10-29 21:28:55 +08:00
xixu-me 73ea14ccdf Update README.md 2024-10-13 23:22:40 +08:00
xixu-me 41feab091a Delete .github/workflows/validate-commit.yml 2024-10-11 20:56:35 +08:00
xixu-me 3e7f54632e Update validate-commit.yml 2024-10-11 20:53:18 +08:00
xixu-me 1e256e10b0 Create validate-commit.yml 2024-10-11 20:51:01 +08:00
xixu-me f347467a85 Delete .github/workflows/validate-commit.yml 2024-10-11 20:50:34 +08:00
xixu-me 4cab4cc656 Update validate-commit.yml 2024-10-11 20:48:53 +08:00
xixu-me b4ac71cd4a Update validate-commit.yml 2024-10-11 20:47:21 +08:00
xixu-me ac6eb7fc37 Update validate-commit.yml 2024-10-11 20:45:03 +08:00
xixu-me 1aafaa06cc Update validate-commit.yml 2024-10-11 20:40:33 +08:00
xixu-me 4af23243c9 Create validate-commit.yml 2024-10-11 20:35:20 +08:00
xixu-me 94cb212397 Create FUNDING.yml 2024-10-11 18:27:08 +08:00
xixu-me 12271383c5 Update README.md 2024-10-11 12:34:31 +08:00
xixu-me eec51c9dd0 Delete script.js 2024-10-11 12:27:53 +08:00
13 changed files with 327 additions and 1273 deletions

No files matched your search

+201
View File
@@ -0,0 +1,201 @@
name: Dependabot Auto Merge
on:
check_suite:
types: [completed]
workflow_run:
workflows:
- "CodeQL"
- "The Workflow"
types:
- completed
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
checks: read
statuses: read
jobs:
merge:
name: Auto-merge Dependabot PRs
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Merge Dependabot PRs when checks pass
uses: actions/github-script@v9
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const successfulCheckConclusions = new Set(["success", "skipped", "neutral"]);
const successfulStatusStates = new Set(["success"]);
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const latestBy = (items, keyOf, timeOf) => {
const latest = new Map();
for (const item of items) {
const key = keyOf(item);
const itemTime = new Date(timeOf(item) || 0).getTime();
const existing = latest.get(key);
const existingTime = existing ? new Date(timeOf(existing) || 0).getTime() : -1;
if (!existing || itemTime >= existingTime) {
latest.set(key, item);
}
}
return [...latest.values()];
};
const findCandidatePulls = async () => {
const pulls = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: "open",
per_page: 100,
});
return pulls.filter((pr) => pr.user?.login === "dependabot[bot]");
};
const getMergeablePullRequest = async (pull_number) => {
for (let attempt = 1; attempt <= 6; attempt += 1) {
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number });
if (pr.mergeable !== null) {
return pr;
}
core.info("PR #" + pull_number + " mergeability is still being computed; retry " + attempt + "/6.");
await wait(5000);
}
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number });
return pr;
};
const getSignalState = async (sha) => {
const checkRuns = await github.paginate(github.rest.checks.listForRef, {
owner,
repo,
ref: sha,
per_page: 100,
});
const statuses = await github.paginate(github.rest.repos.listCommitStatusesForRef, {
owner,
repo,
ref: sha,
per_page: 100,
});
const latestChecks = latestBy(
checkRuns.filter((run) => run.name !== "Dependabot Auto Merge"),
(run) => (run.app?.slug || "unknown") + ":" + run.name,
(run) => run.completed_at || run.started_at || run.created_at,
);
const latestStatuses = latestBy(statuses, (status) => status.context, (status) => status.updated_at || status.created_at);
const pendingChecks = latestChecks.filter((run) => run.status !== "completed");
const failedChecks = latestChecks.filter(
(run) => run.status === "completed" && !successfulCheckConclusions.has(String(run.conclusion || "").toLowerCase()),
);
const failedStatuses = latestStatuses.filter((status) => !successfulStatusStates.has(String(status.state || "").toLowerCase()));
return {
totalSignals: latestChecks.length + latestStatuses.length,
pendingChecks,
failedChecks,
failedStatuses,
};
};
const { data: repository } = await github.rest.repos.get({ owner, repo });
const mergeMethods = [];
if (repository.allow_merge_commit) mergeMethods.push("merge");
if (repository.allow_squash_merge) mergeMethods.push("squash");
if (repository.allow_rebase_merge) mergeMethods.push("rebase");
if (mergeMethods.length === 0) {
core.info("This repository has no enabled pull request merge methods.");
return;
}
const pulls = await findCandidatePulls();
if (pulls.length === 0) {
core.info("No open Dependabot PRs to evaluate.");
return;
}
for (const candidate of pulls) {
const pull_number = candidate.number;
const pr = await getMergeablePullRequest(pull_number);
if (pr.user?.login !== "dependabot[bot]") {
core.info("PR #" + pull_number + " is no longer a Dependabot PR.");
continue;
}
if (pr.state !== "open" || pr.draft) {
core.info("PR #" + pull_number + " is not an open, ready PR.");
continue;
}
if (pr.mergeable !== true) {
core.info("PR #" + pull_number + " is not currently mergeable.");
continue;
}
const signalState = await getSignalState(pr.head.sha);
if (signalState.totalSignals === 0) {
core.info("PR #" + pull_number + " has no checks or statuses yet; skipping.");
continue;
}
if (signalState.pendingChecks.length > 0) {
core.info("PR #" + pull_number + " still has pending checks: " + signalState.pendingChecks.map((run) => run.name).join(", ") + ".");
continue;
}
if (signalState.failedChecks.length > 0 || signalState.failedStatuses.length > 0) {
const failedChecks = signalState.failedChecks.map((run) => run.name + "=" + run.conclusion).join(", ");
const failedStatuses = signalState.failedStatuses.map((status) => status.context + "=" + status.state).join(", ");
core.info("PR #" + pull_number + " is not green. Checks: " + (failedChecks || "none") + ". Statuses: " + (failedStatuses || "none") + ".");
continue;
}
let merged = false;
let lastError = null;
for (const merge_method of mergeMethods) {
try {
await github.rest.pulls.merge({ owner, repo, pull_number, merge_method });
core.info("Merged Dependabot PR #" + pull_number + " with " + merge_method + ".");
merged = true;
break;
} catch (error) {
lastError = error;
if (error.status === 403 || error.status === 405 || error.status === 409) {
core.info("Cannot merge PR #" + pull_number + " with " + merge_method + ": " + error.message);
continue;
}
throw error;
}
}
if (!merged) {
core.info("PR #" + pull_number + " could not be merged: " + (lastError?.message || "unknown error") + ".");
continue;
}
if (pr.head.repo?.full_name === owner + "/" + repo) {
try {
await github.rest.git.deleteRef({ owner, repo, ref: "heads/" + pr.head.ref });
core.info("Deleted branch " + pr.head.ref + ".");
} catch (error) {
core.info("Could not delete branch " + pr.head.ref + ": " + error.message);
}
}
}
-106
View File
@@ -1,106 +0,0 @@
name: The Workflow
on:
push:
branches:
- script
workflow_dispatch:
jobs:
ci:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run test suite
run: bun test META.test.js workflow.test.js
run:
runs-on: ubuntu-latest
needs: ci
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Validate runnable scenarios
run: bun run run
purge-dns-cache:
runs-on: ubuntu-latest
needs: run
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Purge CDN cache
run: |
for file in $(ls); do
curl -i "https://purge.jsdelivr.net/gh/${{ github.repository }}@script/${file}"
done
generate-config:
runs-on: ubuntu-latest
needs: run
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Generate YAML configuration
run: |
mkdir -p publish
bun run generate
cat > publish/META.yaml << 'EOL'
proxy-providers-defaults: &ppd
type: http
interval: 86400
health-check:
enable: true
url: "https://www.gstatic.com/generate_204"
interval: 300
filter: "^(?!.*(群|邀请|返利|循环|官网|客服|网站|网址|获取|订阅|流量|到期|机场|下次|版本|官址|备用|过期|已用|联系|邮箱|工单|贩卖|通知|倒卖|防止|国内|地址|频道|无法|说明|使用|提示|特别|访问|支持|教程|关注|更新|作者|加入|USE|USED|TOTAL|EXPIRE|EMAIL|Panel|Channel|Author))"
proxy-providers:
Provider A:
<<: *ppd
url: "https://the.url.of/A.yaml"
override:
additional-prefix: "[A] "
Provider B:
<<: *ppd
url: "https://the.url.of/B.yaml"
override:
additional-prefix: "[B] "
Provider C:
<<: *ppd
url: "https://the.url.of/C.yaml"
override:
additional-prefix: "[C] "
EOL
cat META.yaml >> publish/META.yaml
- name: Push to config branch
run: |
cd publish
git init
git config --local user.name "github-actions[bot]"
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -b config
git add .
git commit -m "Update YAML configuration"
git remote add origin "https://github-actions[bot]:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}"
git push -f origin config
-2
View File
@@ -1,2 +0,0 @@
node_modules/
META.yaml
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Xi Xu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-41
View File
@@ -1,41 +0,0 @@
const fs = require("node:fs");
const path = require("node:path");
const vm = require("node:vm");
const yaml = require("yaml");
function loadMain() {
const content = fs.readFileSync(path.join(__dirname, "META.js"), "utf8");
const wrapped = `${content}
module.exports = { main };`;
const context = {
module: { exports: {} },
console: {
log() {},
warn() {},
error() {},
},
};
vm.runInNewContext(wrapped, context, { filename: "META.js" });
return context.module.exports.main;
}
function main() {
const generateConfig = loadMain();
const config = generateConfig({}, { skipValidation: true });
if (config.error) {
throw new Error(config.error);
}
fs.writeFileSync(path.join(__dirname, "META.yaml"), yaml.stringify(config));
console.log("Generated META.yaml");
}
try {
main();
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
-791
View File
@@ -1,791 +0,0 @@
// from https://github.com/xixu-me/META
// ################################################### this section can be flexibly customized ###################################################
// Number of STATIC proxy groups (for IP-sensitive services)
const STATIC_PROXY_COUNT = 4;
// Routing rules are matched in order from top to bottom, with the rule at the top of the list taking precedence over the rules below it.
const services = [
// Media & Content
{ name: "bilibili", tld: "com" },
{ name: "Netflix", tld: "com" },
{ name: "YouTube", tld: "com" },
// AI/LLM Services
{ name: "Anthropic", tld: "com" },
{ name: "Arco", domain: "www.youware.com" },
{ name: "Gemini", tld: "google", alias: "Google-Gemini" },
{ name: "OpenAI", tld: "com" },
{ name: "xAI", tld: "ai", sld: "x" },
// Social Media & Communication
{ name: "Douyin", domain: "douyin.com", alias: "Douyin" },
{ name: "rednote", domain: "xiaohongshu.com", alias: "Xiaohongshu" },
{ name: "Telegram", tld: "org" },
{
name: "Truth Social",
tld: "com",
sld: "truthsocial",
alias: "TruthSocial",
},
{ name: "X", tld: "com" },
// Financial Services
{ name: "Binance", tld: "com" },
{ name: "Bybit", tld: "com" },
{ name: "OKX", tld: "com" },
// Technology Companies
{ name: "Apple", tld: "com" },
{ name: "Google", tld: "google", sld: "about" },
{ name: "Microsoft", tld: "com" },
// Infrastructure
{ name: "Speedtest", tld: "net" },
{ name: "Xget", domain: "xuc.xi-xu.me" },
{ name: "Cloudflare", tld: "com" },
];
const LOCATION_ICON_SET_URL = "https://img.icons8.com/color/144/";
const locations = [
{
name: "Argentina 🇦🇷",
icon: `${LOCATION_ICON_SET_URL}argentina`,
keywords: ["阿根廷", "Argentina"],
codes: ["ARG", "AR"],
emoji: ["🇦🇷"],
},
{
name: "Canada 🇨🇦",
icon: `${LOCATION_ICON_SET_URL}canada`,
keywords: ["加拿大", "Canada"],
codes: ["CAN", "CA"],
emoji: ["🇨🇦"],
},
{
name: "Finland 🇫🇮",
icon: `${LOCATION_ICON_SET_URL}finland`,
keywords: ["芬兰", "Finland"],
codes: ["FIN", "FI"],
emoji: ["🇫🇮"],
},
{
name: "France 🇫🇷",
icon: `${LOCATION_ICON_SET_URL}france`,
keywords: ["法国", "France"],
codes: ["FR"],
emoji: ["🇫🇷"],
},
{
name: "Germany 🇩🇪",
icon: `${LOCATION_ICON_SET_URL}germany`,
keywords: ["德国", "Germany"],
codes: ["GER", "DE"],
emoji: ["🇩🇪"],
},
{
name: "Hong Kong, China 🇭🇰",
icon: `${LOCATION_ICON_SET_URL}hongkong-circular`,
keywords: ["香港", "Hong Kong"],
codes: ["HK"],
emoji: ["🇭🇰"],
},
{
name: "India 🇮🇳",
icon: `${LOCATION_ICON_SET_URL}india`,
keywords: ["印度", "India"],
codes: ["IND", "IN"],
emoji: ["🇮🇳"],
},
{
name: "Iraq 🇮🇶",
icon: `${LOCATION_ICON_SET_URL}iraq`,
keywords: ["伊拉克", "Iraq"],
codes: ["IQ"],
emoji: ["🇮🇶"],
},
{
name: "Japan 🇯🇵",
icon: `${LOCATION_ICON_SET_URL}japan`,
keywords: ["日本", "Japan"],
codes: ["JP"],
emoji: ["🇯🇵"],
},
{
name: "Korea 🇰🇷",
icon: `${LOCATION_ICON_SET_URL}south-korea`,
keywords: ["韩国", "Korea"],
codes: ["KR"],
emoji: ["🇰🇷"],
},
{
name: "Russia 🇷🇺",
icon: `${LOCATION_ICON_SET_URL}russian-federation`,
keywords: ["俄罗斯", "Russia Federation", "Russia"],
codes: ["RU"],
emoji: ["🇷🇺"],
},
{
name: "Singapore 🇸🇬",
icon: `${LOCATION_ICON_SET_URL}singapore`,
keywords: ["新加坡", "Singapore"],
codes: ["SG"],
emoji: ["🇸🇬"],
},
{
name: "Taiwan, China 🇨🇳",
icon: `${LOCATION_ICON_SET_URL}china-circular`,
keywords: ["台湾", "Taiwan"],
codes: ["TW"],
emoji: ["🇹🇼"],
},
{
name: "Thailand 🇹🇭",
icon: `${LOCATION_ICON_SET_URL}thailand`,
keywords: ["泰国", "Thailand"],
codes: ["TH"],
emoji: ["🇹🇭"],
},
{
name: "Türkiye 🇹🇷",
icon: `${LOCATION_ICON_SET_URL}turkey`,
keywords: ["土耳其", "Türkiye", "Turkey"],
codes: ["TUR", "TR"],
emoji: ["🇹🇷"],
},
{
name: "Ukraine 🇺🇦",
icon: `${LOCATION_ICON_SET_URL}ukraine`,
keywords: ["乌克兰", "Ukraine"],
codes: ["UKR", "UA"],
emoji: ["🇺🇦"],
},
{
name: "United Kingdom 🇬🇧",
icon: `${LOCATION_ICON_SET_URL}great-britain`,
keywords: ["英国", "United Kingdom", "Great Britain"],
codes: ["UK", "GB"],
emoji: ["🇬🇧"],
},
{
name: "United States 🇺🇸",
icon: `${LOCATION_ICON_SET_URL}usa`,
keywords: ["美国", "United States of America", "United States"],
codes: ["USA", "US"],
emoji: ["🇺🇸"],
},
{
name: "Global 🌐",
icon: "https://img.icons8.com/?size=144&id=3685&format=png&color=7bbbe9",
keywords: ["Global", "Cloudflare"],
codes: ["GL", "CF"],
emoji: ["🌐"],
},
];
// ####################################### DO NOT MODIFY THE CODE BELOW UNLESS YOU KNOW WHAT YOU ARE DOING #######################################
function extractFavicon(domain) {
return `https://t1.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=https://${domain}&size=128`;
}
function escapeRegex(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function buildLocationFilter({ keywords = [], codes = [], emoji = [] }) {
const patterns = [
...keywords.map((keyword) => escapeRegex(keyword)),
...codes.map((code) => `\\b${escapeRegex(code)}\\b`),
...emoji.map((symbol) => escapeRegex(symbol)),
];
return `(?i)${patterns.join("|")}`;
}
locations.forEach((location) => {
location.filter = buildLocationFilter(location);
});
// General Configuration
const generalConfig = {
"mixed-port": 7890,
"allow-lan": false,
mode: "rule",
"log-level": "info",
ipv6: true,
"find-process-mode": "strict",
profile: {
"store-selected": true,
"store-fake-ip": true,
},
"unified-delay": true,
"tcp-concurrent": true,
"global-ua": "\u0063\u006c\u0061\u0073\u0068.\u006D\u0065\u0074\u0061",
"etag-support": true,
};
// DNS
const chineseNameservers = [
"https://dns.alidns.com/dns-query",
"https://doh.pub/dns-query",
];
const internationalNameservers = [
"https://cloudflare-dns.com/dns-query",
"https://dns.google/dns-query",
// "https://dns.mullvad.net/dns-query",
// "https://wikimedia-dns.org/dns-query",
// "https://doh.dns.sb/dns-query",
];
const dns = {
enable: true,
"cache-algorithm": "arc",
"prefer-h3": false,
"use-hosts": true,
"use-system-hosts": false,
"respect-rules": true,
ipv6: true,
"enhanced-mode": "fake-ip",
"fake-ip-range": "198.18.0.1/16",
"fake-ip-range6": "fd00::/18",
"fake-ip-filter-mode": "blacklist",
"fake-ip-filter": [
"rule-set:fake-ip-filter",
"rule-set:direct",
"rule-set:private",
"rule-set:connectivity-check",
],
"default-nameserver": [
"tls://223.5.5.5",
"tls://223.6.6.6",
"tls://119.29.29.29",
],
"proxy-server-nameserver": chineseNameservers,
"nameserver-policy": {
"rule-set:private": ["system://"],
"rule-set:direct,geolocation-cn": chineseNameservers,
},
nameserver: internationalNameservers,
};
// Hosts
const hosts = {};
// Domain Sniffing
const sniffer = {
enable: false,
};
// TUN
const tun = {
enable: true,
stack: "mixed",
"auto-route": true,
"auto-detect-interface": true,
"strict-route": true,
"dns-hijack": ["any:53", "tcp://any:53"],
};
// Proxy Groups
const proxyGroupDefaults = {
url: "https://www.gstatic.com/generate_204",
interval: 300,
lazy: true,
timeout: 5000,
"max-failed-times": 5,
};
// Generate STATIC proxy group names
function generateStaticProxyNames() {
if (STATIC_PROXY_COUNT === 1) {
return ["STATIC"];
}
return Array.from(
{ length: STATIC_PROXY_COUNT },
(_, i) => `STATIC-${i + 1}`,
);
}
const staticProxyNames = generateStaticProxyNames();
const serviceProxyGroupProxies = [
"PROXY",
...staticProxyNames,
"AUTO",
"DIRECT",
"Chinese Mainland 🇨🇳",
...locations.map(({ name }) => name),
];
const locationPolicyProxyGroupDefaults = {
...proxyGroupDefaults,
proxies: ["REJECT"],
"include-all": true,
};
const SERVICE_ICON_SET_URL =
"https://cdn.jsdelivr.net/gh/xixu-me/favicons@assets/favicons/";
function getServiceIcon(service) {
if (service.icon) return service.icon;
if (service.domain) return extractFavicon(service.domain);
if (service.tld) {
const secondLevelDomain = service.sld || service.name.toLowerCase();
return `${SERVICE_ICON_SET_URL}${secondLevelDomain}.${service.tld}.png`;
}
console.warn(`No icon configuration for service: ${service.name}`);
return null;
}
services.forEach((service) => {
service.icon = getServiceIcon(service);
});
function generateServiceProxyGroups(items, defaultConfig) {
return items.map(({ name, icon }) => ({
...defaultConfig,
name,
icon,
type: "select",
proxies: serviceProxyGroupProxies,
}));
}
function generateLocationPolicyProxyGroups(
items,
defaultConfig,
type,
extraProps = {},
) {
const getStrategyName = (type, strategy) => {
switch (type) {
case "url-test":
return "AUTO";
case "fallback":
return "FALLBACK";
case "load-balance":
switch (strategy) {
case "consistent-hashing":
return "LOAD BALANCING (consistent hashing)";
case "round-robin":
return "LOAD BALANCING (round-robin)";
case "sticky-sessions":
return "LOAD BALANCING (sticky sessions)";
default:
return "LOAD BALANCING";
}
default:
return type.toUpperCase();
}
};
return items.map(({ name, icon, filter }) => {
const strategyName = getStrategyName(type, extraProps.strategy);
const emoji = name.split(" ").pop();
const newName = `${strategyName} ${emoji}`;
return {
...defaultConfig,
name: newName,
type,
icon,
filter,
hidden: true,
...extraProps,
};
});
}
function generateLocationSelectProxyGroups() {
return locations.map(({ name, icon }) => ({
...proxyGroupDefaults,
name,
type: "select",
icon,
proxies: [
`LOAD BALANCING (consistent hashing) ${name.split(" ").pop()}`,
`LOAD BALANCING (round-robin) ${name.split(" ").pop()}`,
`LOAD BALANCING (sticky sessions) ${name.split(" ").pop()}`,
`FALLBACK ${name.split(" ").pop()}`,
`AUTO ${name.split(" ").pop()}`,
],
}));
}
const BASE_ICON_SET_URL =
"https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/";
// Generate STATIC proxy groups
function generateStaticProxyGroups(defaultConfig) {
return staticProxyNames.map((name, index) => ({
...defaultConfig,
name,
type: "select",
"include-all": true,
icon: `${BASE_ICON_SET_URL}Static.png`,
}));
}
const proxyGroups = [
{
...proxyGroupDefaults,
name: "PROXY",
type: "select",
proxies: [
...staticProxyNames,
"AUTO",
"DIRECT",
"Chinese Mainland 🇨🇳",
...locations.map(({ name }) => name),
],
icon: `${BASE_ICON_SET_URL}Proxy.png`,
},
{
...proxyGroupDefaults,
name: "AUTO",
type: "url-test",
tolerance: 50,
"include-all": true,
icon: `${BASE_ICON_SET_URL}Auto.png`,
hidden: true,
},
...generateStaticProxyGroups(proxyGroupDefaults),
...generateServiceProxyGroups(services, proxyGroupDefaults),
{
...proxyGroupDefaults,
name: "Domestic",
type: "select",
proxies: [
"DIRECT",
"PROXY",
...staticProxyNames,
"AUTO",
"Chinese Mainland 🇨🇳",
...locations.map(({ name }) => name),
],
icon: `${BASE_ICON_SET_URL}Domestic.png`,
},
{
...proxyGroupDefaults,
name: "Others",
type: "select",
proxies: [...serviceProxyGroupProxies],
icon: `${BASE_ICON_SET_URL}Final.png`,
},
{
...proxyGroupDefaults,
name: "Advertising",
type: "select",
proxies: ["REJECT", "DIRECT", ...staticProxyNames],
icon: `${BASE_ICON_SET_URL}Advertising.png`,
},
{
...locationPolicyProxyGroupDefaults,
name: "Chinese Mainland 🇨🇳",
type: "select",
icon: `${LOCATION_ICON_SET_URL}china`,
proxies: ["REJECT"],
"include-all": true,
filter:
"(?i)\u5927\u9646|\u4e2d\u56fd|\u7535\u4fe1|\u79fb\u52a8|\u8054\u901a|Mainland|China|CN|mainland|china|cn|🇨🇳",
},
...generateLocationPolicyProxyGroups(
locations,
locationPolicyProxyGroupDefaults,
"url-test",
{
tolerance: 50,
},
),
...generateLocationPolicyProxyGroups(
locations,
locationPolicyProxyGroupDefaults,
"fallback",
),
...generateLocationPolicyProxyGroups(
locations,
locationPolicyProxyGroupDefaults,
"load-balance",
{
strategy: "round-robin",
},
),
...generateLocationPolicyProxyGroups(
locations,
locationPolicyProxyGroupDefaults,
"load-balance",
{
strategy: "consistent-hashing",
},
),
...generateLocationPolicyProxyGroups(
locations,
locationPolicyProxyGroupDefaults,
"load-balance",
{
strategy: "sticky-sessions",
},
),
...generateLocationSelectProxyGroups(),
];
// Routing Rules
function generateServiceRules(services) {
return services.map(
({ name }) => `RULE-SET,${name.toLowerCase().replace(/\s+/g, "")},${name}`,
);
}
const rules = [
"RULE-SET,applications,DIRECT",
"RULE-SET,win-phonelink,DIRECT",
"RULE-SET,lancidr,DIRECT,no-resolve",
"RULE-SET,private,DIRECT",
...generateServiceRules(services),
"RULE-SET,reject,Advertising",
"RULE-SET,win-spy,Advertising",
"RULE-SET,direct,Domestic",
"RULE-SET,proxy,PROXY",
"RULE-SET,telegramcidr,Telegram",
"RULE-SET,xcidr,X",
"RULE-SET,googlecidr,Google",
"RULE-SET,cloudflarecidr,Cloudflare",
"RULE-SET,cncidr,Domestic",
"MATCH,Others",
];
// Rule Providers
const ruleProviderDefaults = {
type: "http",
interval: 14400,
};
function generateServiceRuleProviders(services, defaultConfig) {
return services.reduce((acc, { name, alias }) => {
const fileName = (alias || name).toLowerCase();
const ruleName = name.toLowerCase().replace(/\s+/g, "");
acc[ruleName] = {
...defaultConfig,
format: "mrs",
behavior: "domain",
url: `https://cdn.jsdelivr.net/gh/xixu-me/RFM@universal/${fileName}.mrs`,
path: `./rulesets/${ruleName}.mrs`,
};
return acc;
}, {});
}
const ruleProviders = {
"fake-ip-filter": {
...ruleProviderDefaults,
format: "mrs",
behavior: "domain",
url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@basic/fake-ip-filter.mrs",
path: "./rulesets/fake-ip-filter.mrs",
},
"connectivity-check": {
...ruleProviderDefaults,
format: "mrs",
behavior: "domain",
url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@universal/connectivity-check.mrs",
path: "./rulesets/connectivity-check.mrs",
},
applications: {
...ruleProviderDefaults,
format: "yaml",
behavior: "classical",
url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@basic/applications.yaml",
path: "./rulesets/applications.yaml",
},
"win-phonelink": {
...ruleProviderDefaults,
format: "yaml",
behavior: "classical",
url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@fixed/win-phonelink.yaml",
path: "./rulesets/win-phonelink.yaml",
},
lancidr: {
...ruleProviderDefaults,
format: "mrs",
behavior: "ipcidr",
url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@basic/lancidr.mrs",
path: "./rulesets/lancidr.mrs",
},
cncidr: {
...ruleProviderDefaults,
format: "mrs",
behavior: "ipcidr",
url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@basic/cncidr.mrs",
path: "./rulesets/cncidr.mrs",
},
cloudflarecidr: {
...ruleProviderDefaults,
format: "mrs",
behavior: "ipcidr",
url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@basic/cloudflarecidr.mrs",
path: "./rulesets/cloudflarecidr.mrs",
},
googlecidr: {
...ruleProviderDefaults,
format: "mrs",
behavior: "ipcidr",
url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@basic/googlecidr.mrs",
path: "./rulesets/googlecidr.mrs",
},
telegramcidr: {
...ruleProviderDefaults,
format: "mrs",
behavior: "ipcidr",
url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@basic/telegramcidr.mrs",
path: "./rulesets/telegramcidr.mrs",
},
xcidr: {
...ruleProviderDefaults,
format: "mrs",
behavior: "ipcidr",
url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@basic/xcidr.mrs",
path: "./rulesets/xcidr.mrs",
},
private: {
...ruleProviderDefaults,
format: "mrs",
behavior: "domain",
url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@basic/private.mrs",
path: "./rulesets/private.mrs",
},
direct: {
...ruleProviderDefaults,
format: "mrs",
behavior: "domain",
url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@basic/direct.mrs",
path: "./rulesets/direct.mrs",
},
proxy: {
...ruleProviderDefaults,
format: "mrs",
behavior: "domain",
url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@basic/proxy.mrs",
path: "./rulesets/proxy.mrs",
},
"geolocation-cn": {
...ruleProviderDefaults,
format: "mrs",
behavior: "domain",
url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@universal/geolocation-cn.mrs",
path: "./rulesets/geolocation-cn.mrs",
},
reject: {
...ruleProviderDefaults,
format: "mrs",
behavior: "domain",
url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@basic/reject.mrs",
path: "./rulesets/reject.mrs",
},
"win-spy": {
...ruleProviderDefaults,
format: "mrs",
behavior: "domain",
url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@universal/win-spy.mrs",
path: "./rulesets/win-spy.mrs",
},
...generateServiceRuleProviders(services, ruleProviderDefaults),
};
// generate configuration using the above settings
function validateOriginalConfig(config) {
if (!config)
throw new Error("Configuration object cannot be null or undefined");
const proxyCount = Array.isArray(config.proxies) ? config.proxies.length : 0;
const proxyProviderCount = Object.keys(
config["proxy-providers"] || {},
).length;
if (proxyCount === 0 && proxyProviderCount === 0)
throw new Error(
"The original configuration must contain a non-empty proxies array (see https://wiki.\u006D\u0065\u0074\u0061\u0063\u0075\u0062\u0065\u0078.one/en/config/proxies/) or a proxy-providers object with at least one property (see https://wiki.\u006D\u0065\u0074\u0061\u0063\u0075\u0062\u0065\u0078.one/en/config/proxy-providers/)",
);
if (proxyCount > 0) {
const proxiesWithoutRemoteEndpoint = new Set([
"direct",
"dns",
"pass",
"reject",
]);
config.proxies.forEach((proxy, index) => {
if (!proxy.name || !proxy.type) {
throw new Error(
`Invalid proxy number ${
index + 1
} configuration (see https://wiki.\u006D\u0065\u0074\u0061\u0063\u0075\u0062\u0065\u0078.one/en/config/proxies/)`,
);
}
if (
!proxiesWithoutRemoteEndpoint.has(String(proxy.type).toLowerCase()) &&
(!proxy.server || proxy.port == null)
) {
throw new Error(
`Invalid proxy number ${
index + 1
} configuration (see https://wiki.\u006D\u0065\u0074\u0061\u0063\u0075\u0062\u0065\u0078.one/en/config/proxies/)`,
);
}
});
console.log(
`The original configuration contains ${proxyCount} proxies, which will be preserved`,
);
}
if (proxyProviderCount > 0) {
Object.entries(config["proxy-providers"]).forEach(
([name, provider], index) => {
if (!provider.type || (provider.type === "http" && !provider.url)) {
throw new Error(
`Invalid proxy provider number ${
index + 1
} configuration (see https://wiki.\u006D\u0065\u0074\u0061\u0063\u0075\u0062\u0065\u0078.one/en/config/proxy-providers/)`,
);
}
},
);
console.log(
`The original configuration contains ${proxyProviderCount} proxy providers, which will be preserved`,
);
}
return true;
}
function main(config, options = {}) {
try {
if (!options.skipValidation) {
validateOriginalConfig(config);
}
Object.assign(config, generalConfig, {
dns,
hosts,
sniffer,
tun,
"proxy-groups": proxyGroups,
rules,
"rule-providers": ruleProviders,
});
console.log("The generated configuration is as follows");
console.log(config);
return config;
} catch (error) {
console.error(
`An error occurred during configuration generation: ${error.message}`,
);
return { error: error.message, originalConfig: config };
}
}
-89
View File
@@ -1,89 +0,0 @@
const fs = require("node:fs");
const path = require("node:path");
const vm = require("node:vm");
function loadMetaModule() {
const content = fs.readFileSync(path.join(__dirname, "META.js"), "utf8");
const wrapped = `${content}
module.exports = {
main,
validateOriginalConfig,
};`;
const context = {
module: { exports: {} },
console: {
log() {},
warn() {},
error() {},
},
};
vm.runInNewContext(wrapped, context, { filename: "META.js" });
return context.module.exports;
}
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}
function runScenario(name, input, options) {
const { main } = loadMetaModule();
const config = main(structuredClone(input), options);
assert(!config.error, `${name}: ${config.error}`);
assert(Array.isArray(config.rules), `${name}: rules should be an array`);
assert(
Array.isArray(config["proxy-groups"]),
`${name}: proxy-groups should be an array`
);
assert(
config["rule-providers"] && typeof config["rule-providers"] === "object",
`${name}: rule-providers should be present`
);
}
function main() {
runScenario(
"proxy-based config",
{
proxies: [
{
name: "sample",
type: "socks5",
server: "127.0.0.1",
port: 1080,
},
],
},
{}
);
runScenario(
"provider-based config",
{
"proxy-providers": {
provider: {
type: "http",
url: "https://example.com/provider.yaml",
path: "./providers/provider.yaml",
},
},
},
{}
);
runScenario("publish generation", {}, { skipValidation: true });
console.log("Validated META.js against 3 scenarios.");
}
try {
main();
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
-117
View File
@@ -1,117 +0,0 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const vm = require("node:vm");
const repoRoot = __dirname;
function loadMetaModule() {
const content = fs.readFileSync(path.join(repoRoot, "META.js"), "utf8");
const wrapped = `${content}
module.exports = {
locations,
generalConfig,
tun,
main,
validateOriginalConfig,
};`;
const context = {
module: { exports: {} },
console: {
log() {},
warn() {},
error() {},
},
};
vm.runInNewContext(wrapped, context, { filename: "META.js" });
return context.module.exports;
}
function mihomoRegex(source) {
return new RegExp(source.replace(/^\(\?i\)/, ""), "i");
}
test("validateOriginalConfig accepts mihomo dns outbound proxies", () => {
const { validateOriginalConfig } = loadMetaModule();
assert.doesNotThrow(() =>
validateOriginalConfig({
proxies: [{ name: "dns-out", type: "dns" }],
})
);
});
test("generated config does not force deprecated global fingerprinting", () => {
const { main } = loadMetaModule();
const config = main({
proxies: [
{
name: "sample",
type: "socks5",
server: "127.0.0.1",
port: 1080,
},
],
});
assert.equal(config["global-client-fingerprint"], undefined);
});
test("main can skip source validation for publish-time generation", () => {
const { main } = loadMetaModule();
const config = main({}, { skipValidation: true });
assert.equal(config.error, undefined);
assert.ok(Array.isArray(config.rules));
assert.ok(Array.isArray(config["proxy-groups"]));
});
test("generated tun config fully replaces the source tun settings", () => {
const { main } = loadMetaModule();
const config = main({
proxies: [
{
name: "sample",
type: "socks5",
server: "127.0.0.1",
port: 1080,
},
],
tun: {
enable: false,
"auto-route": false,
"auto-redirect": true,
mtu: 9000,
},
});
assert.deepEqual(JSON.parse(JSON.stringify(config.tun)), {
enable: true,
stack: "mixed",
"auto-route": true,
"auto-detect-interface": true,
"strict-route": true,
"dns-hijack": ["any:53", "tcp://any:53"],
});
});
test("location filters avoid matching unrelated country names", () => {
const { locations } = loadMetaModule();
const india = locations.find((location) => location.name === "India 🇮🇳");
const canada = locations.find((location) => location.name === "Canada 🇨🇦");
assert.ok(india, "India location should exist");
assert.ok(canada, "Canada location should exist");
assert.equal(mihomoRegex(india.filter).test("Argentina 01"), false);
assert.equal(mihomoRegex(india.filter).test("Finland-IEPL"), false);
assert.equal(mihomoRegex(canada.filter).test("Singapore-Anycast"), false);
});
+105
View File
@@ -0,0 +1,105 @@
# META
META stands for Modular Enhanced Traffic Architecture.
That idea shapes the project itself: the inputs stay modular, the generated output adds stronger routing structure on top, and the overall repository is organized as an architecture with clear roles for source, validation, and published artifacts.
The repository currently uses a split branch model:
- `main`: project documentation
- `script`: source code, tests, and GitHub Actions workflow
- `config`: generated `META.yaml`
> [!NOTE]
> If you want to use or customize META, the source of truth is the `script` branch. The `config` branch is generated from it after CI passes.
## What META Generates
META starts from your existing proxy configuration and builds a layered traffic profile instead of leaving you with a flat set of raw inputs:
- Service-oriented policy groups
- Location-based groups with `select`, `url-test`, `fallback`, and multiple load-balancing strategies
- DNS, TUN, rule providers, and routing rules for a broad range of network environments
- A published YAML artifact for users who prefer a static configuration
The generator preserves your original proxies or proxy providers, then injects META's standardized policy groups and rules on top.
## Import URLs
META currently documents both direct raw access and [Xget](https://github.com/xixu-me/xget) for remote imports.
### JavaScript
| Source | URL |
| --- | --- |
| GitHub Raw | <https://raw.githubusercontent.com/xixu-me/meta/refs/heads/script/META.js> |
| Xget | <https://xget.xi-xu.me/gh/xixu-me/meta/raw/refs/heads/script/META.js> |
### YAML
| Source | URL |
| --- | --- |
| GitHub Raw | <https://raw.githubusercontent.com/xixu-me/meta/refs/heads/config/META.yaml> |
| Xget | <https://xget.xi-xu.me/gh/xixu-me/meta/raw/refs/heads/config/META.yaml> |
## Customization
`META.js` is intentionally organized so the top section is the only part most users need to edit:
```javascript
// ################################################### this section can be flexibly customized ###################################################
```
The main customization points are:
- `STATIC_PROXY_COUNT`: number of static groups for IP-sensitive services
- `services`: service list and icon/domain metadata
- `locations`: country and region groups, including keywords, country codes, and emoji matchers
META derives the more error-prone parts, such as location filter regexes, from this top-level data so the editable surface stays small while the generated structure stays rich.
For a GUI-based workflow, use [META.js Customizer](https://github.com/xixu-me/META.js-Customizer) at [metajs.xi-xu.me](https://metajs.xi-xu.me).
## RFM Integration
META is built around [RFM](https://github.com/xixu-me/rfm), which provides the ruleset foundation for the generated configuration.
- META consumes RFM rulesets as its primary rule-provider source
- Domain, classical, and IP-based routing behavior are organized around RFM outputs
- Keeping RFM current directly improves the quality and coverage of META's generated configuration
Within the overall project structure, META acts as the assembly layer while RFM supplies the ruleset base that gives the generated output its breadth and consistency.
## Maintenance
The branches are intentionally separated:
- `script` is where generator changes, tests, and workflow updates happen
- `config` is force-updated by GitHub Actions with the latest generated YAML
- `main` is the stable documentation entrypoint for the project
The source branch uses Bun for local verification and publishes the generated YAML only after automated checks pass. This keeps the documentation branch, source branch, and generated branch aligned without turning the main README into a workflow manual.
## Design Notes
The project leans into the meaning behind its name: small editable building blocks, stronger generated defaults, and a structure that can scale without turning into a monolithic config blob.
- It treats service routing as the primary UX surface instead of exposing only raw proxy groups
- It generates several per-location strategies automatically so users can switch between stability and performance without editing rules
- It keeps source validation strict enough to catch malformed inputs while still supporting special outbound forms used by the underlying ecosystem
- It favors portable defaults so the generated configuration travels better across different clients and environments
## Related Projects
- [META.js Customizer](https://github.com/xixu-me/META.js-Customizer): browser-based META configuration builder
- [RFM](https://github.com/xixu-me/rfm): ruleset backbone used by META
## Disclaimer
- META is a configuration generator. You are responsible for how you deploy and use the generated configuration.
- Compatibility can change as clients, parsers, and third-party services evolve.
- Review generated output before using it in environments where routing, privacy, or availability is sensitive.
## License
Licensed under the MIT License. See [`LICENSE`](./LICENSE).
-15
View File
@@ -1,15 +0,0 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "meta",
"dependencies": {
"yaml": "^2.8.1",
},
},
},
"packages": {
"yaml": ["yaml@2.8.3", "", { "bin": "bin.mjs" }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="],
}
}
-2
View File
@@ -1,2 +0,0 @@
[install]
auto = "disable"
-12
View File
@@ -1,12 +0,0 @@
{
"name": "meta",
"private": true,
"scripts": {
"generate": "bun META.generate.js",
"test": "bun test META.test.js workflow.test.js",
"run": "bun META.run.js"
},
"dependencies": {
"yaml": "^2.8.1"
}
}
-98
View File
@@ -1,98 +0,0 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const childProcess = require("node:child_process");
const repoRoot = __dirname;
const workflowPath = path.join(repoRoot, ".github", "workflows", "run.yml");
const runScriptPath = path.join(repoRoot, "META.run.js");
function runBun(args) {
return childProcess.spawnSync("bun", args, {
cwd: repoRoot,
encoding: "utf8",
});
}
function ensureDependenciesInstalled() {
const result = runBun(["install", "--frozen-lockfile"]);
assert.equal(result.status, 0, result.stderr || result.stdout);
}
test("workflow runs CI before run and publish jobs", () => {
const workflow = fs.readFileSync(workflowPath, "utf8");
assert.match(workflow, /^ ci:\s*$/m, "workflow should define a ci job");
assert.match(
workflow,
/uses:\s+oven-sh\/setup-bun@v2/,
"workflow should install Bun via the official setup action"
);
assert.match(
workflow,
/^ run:\s*$(?:\r?\n)(?:\s+.*\r?\n)*?\s+needs:\s+ci\s*$/m,
"run job should depend on ci"
);
assert.match(
workflow,
/^ purge-dns-cache:\s*$(?:\r?\n)(?:\s+.*\r?\n)*?\s+needs:\s+run\s*$/m,
"purge-dns-cache should depend on run"
);
assert.match(
workflow,
/^ generate-config:\s*$(?:\r?\n)(?:\s+.*\r?\n)*?\s+needs:\s+run\s*$/m,
"generate-config should depend on run"
);
});
test("workflow installs dependencies and runs tests in ci job", () => {
const workflow = fs.readFileSync(workflowPath, "utf8");
assert.match(
workflow,
/bun install --frozen-lockfile/,
"ci job should install dependencies with Bun"
);
assert.match(workflow, /bun test/, "ci job should run the test suite with Bun");
assert.match(workflow, /bun run run/, "run job should use Bun scripts");
assert.match(
workflow,
/bun run generate/,
"generate-config job should use Bun scripts"
);
assert.doesNotMatch(
workflow,
/sed -i .*validateOriginalConfig/,
"workflow should not mutate META.js to bypass validation"
);
});
test("META.run.js executes the generator against sample inputs", () => {
const result = runBun([runScriptPath]);
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.match(
result.stdout,
/validated/i,
"run script should report successful validation"
);
});
test("META.generate.js creates YAML output without mutating source", () => {
const outputPath = path.join(repoRoot, "META.yaml");
fs.rmSync(outputPath, { force: true });
ensureDependenciesInstalled();
const result = runBun(["META.generate.js"]);
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.ok(fs.existsSync(outputPath), "generate script should create META.yaml");
assert.match(
fs.readFileSync(outputPath, "utf8"),
/mixed-port:\s*7890/,
"generated YAML should include core config values"
);
fs.rmSync(outputPath, { force: true });
});