diff --git a/.github/workflows/run.yml b/.github/workflows/run.yml index 8f37804..fe0f571 100644 --- a/.github/workflows/run.yml +++ b/.github/workflows/run.yml @@ -7,20 +7,38 @@ on: workflow_dispatch: jobs: - check-script: + ci: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Set up Node.js - uses: actions/setup-node@v4 + - name: Set up Bun + uses: oven-sh/setup-bun@v2 with: - node-version: 22.x - - name: Run script - run: node META.js + 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@v4 + - 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: check-script + needs: run steps: - name: Checkout repository uses: actions/checkout@v4 @@ -29,56 +47,25 @@ jobs: for file in $(ls); do curl -i "https://purge.jsdelivr.net/gh/${{ github.repository }}@script/${file}" done + generate-config: runs-on: ubuntu-latest - needs: check-script + needs: run + permissions: + contents: write steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 + - name: Set up Bun + uses: oven-sh/setup-bun@v2 with: - node-version: "20" - - name: Remove validation function - run: | - sed -i '/function validateOriginalConfig/,/^}/d' META.js - sed -i 's/validateOriginalConfig(config);//' META.js + bun-version: latest - name: Install dependencies - run: npm install yaml - - name: Create conversion script - run: | - cat > generate.js << 'EOL' - const fs = require('fs'); - const vm = require('vm'); - const yaml = require('yaml'); - // Read the JS config file - const jsContent = fs.readFileSync('./META.js', 'utf8'); - // Create a sandbox context - const context = { - console, - require, - process, - __dirname, - module, - exports, - }; - // Create empty config object - const baseConfig = {}; - // Prepare the script - const fullScript = `${jsContent}\nmodule.exports = main(${JSON.stringify(baseConfig)});`; - // Execute in sandbox - vm.createContext(context); - vm.runInContext(fullScript, context); - // Get the result and convert to YAML - const config = context.module.exports; - const yamlContent = yaml.stringify(config); - // Write output - fs.writeFileSync('./META.yaml', yamlContent); - EOL + run: bun install --frozen-lockfile - name: Generate YAML configuration run: | mkdir -p publish - node generate.js + bun run generate cat > publish/META.yaml << 'EOL' proxy-providers-defaults: &ppd type: http diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0558b3d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +META.yaml diff --git a/META.generate.js b/META.generate.js new file mode 100644 index 0000000..ba6f885 --- /dev/null +++ b/META.generate.js @@ -0,0 +1,40 @@ +const fs = require("node:fs"); +const vm = require("node:vm"); +const yaml = require("yaml"); + +function loadMain() { + const content = fs.readFileSync("./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("./META.yaml", yaml.stringify(config)); + console.log("Generated META.yaml"); +} + +try { + main(); +} catch (error) { + console.error(error.message); + process.exitCode = 1; +} diff --git a/META.js b/META.js index c43abd5..d928918 100644 --- a/META.js +++ b/META.js @@ -763,9 +763,11 @@ function validateOriginalConfig(config) { return true; } -function main(config) { +function main(config, options = {}) { try { - validateOriginalConfig(config); + if (!options.skipValidation) { + validateOriginalConfig(config); + } Object.assign(config, generalConfig, { dns, hosts, diff --git a/META.run.js b/META.run.js new file mode 100644 index 0000000..a7f7f0b --- /dev/null +++ b/META.run.js @@ -0,0 +1,88 @@ +const fs = require("node:fs"); +const vm = require("node:vm"); + +function loadMetaModule() { + const content = fs.readFileSync("./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; +} diff --git a/META.test.js b/META.test.js index 619f28d..a49fb30 100644 --- a/META.test.js +++ b/META.test.js @@ -59,6 +59,16 @@ test("generated config does not force deprecated global fingerprinting", () => { 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(); diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..596af4c --- /dev/null +++ b/bun.lock @@ -0,0 +1,15 @@ +{ + "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=="], + } +} diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..899ef6f --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,2 @@ +[install] +auto = "disable" diff --git a/package.json b/package.json new file mode 100644 index 0000000..a799c6d --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "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" + } +} diff --git a/workflow.test.js b/workflow.test.js new file mode 100644 index 0000000..8f97640 --- /dev/null +++ b/workflow.test.js @@ -0,0 +1,98 @@ +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 = "E:/github/meta"; +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 }); +});