import { test } from "node:test"; import assert from "node:assert/strict"; import { mkdtemp, mkdir, writeFile, readFile, realpath, rm, } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { spawnSync } from "node:child_process"; import ts from "typescript"; import vm from "node:vm"; import { nextCommand, runNext } from "../scripts/next-env.mjs"; async function fixture(t) { const root = await mkdtemp(join(tmpdir(), "91-environments-")); t.after(() => rm(root, { recursive: true, force: true })); await mkdir(join(root, "config")); for (const environment of ["development", "test", "production"]) await writeFile( join(root, "config", environment + ".env"), `API_BASE_URL=https://${environment}.api.test\nPORT=21030\nNEXT_PUBLIC_TRACKER_APP_ID=1050\n`, ); return root; } test("environment defaults, personal overrides and process values use the reference precedence without changing NODE_ENV semantics", async (t) => { const root = await fixture(t); await writeFile( join(root, "config/test.local.env"), "API_BASE_URL=https://override.api.test\nPORT=21022\nLOCAL_MARKER=yes\n", ); const base = nextCommand("test", "dev", [], { root, variables: {} }); assert.equal(base.env.API_BASE_URL, "https://override.api.test"); assert.equal(base.env.PORT, "21022"); assert.equal(base.env.LOCAL_MARKER, "yes"); assert.equal(base.env.APP_ENV, "test"); assert.equal(base.env.NODE_ENV, "development"); assert.equal(base.env.APP_DIST_DIR, ".next-test"); const processOverride = nextCommand("test", "start", [], { root, variables: { API_BASE_URL: "https://process.api.test/", PORT: "31021", APP_ENV: "production", NODE_ENV: "test", APP_DIST_DIR: "outside", }, }); assert.equal(processOverride.env.API_BASE_URL, "https://process.api.test"); assert.equal(processOverride.env.PORT, "31021"); assert.equal(processOverride.env.APP_ENV, "test"); assert.equal(processOverride.env.NODE_ENV, "production"); assert.equal(processOverride.env.APP_DIST_DIR, ".next-test"); }); test("development supports legacy .env.local while test and production isolate local overrides", async (t) => { const root = await fixture(t); await writeFile( join(root, ".env.local"), "API_BASE_URL=https://legacy.api.test\nLEGACY_MARKER=dev-only\n", ); await writeFile( join(root, "config/development.local.env"), "API_BASE_URL=https://development-local.api.test\n", ); const dev = nextCommand("development", "dev", [], { root, variables: {} }); assert.equal(dev.env.API_BASE_URL, "https://development-local.api.test"); assert.equal(dev.env.LEGACY_MARKER, "dev-only"); for (const environment of ["test", "production"]) { const result = nextCommand(environment, "build", [], { root, variables: {}, }); assert.equal(result.env.API_BASE_URL, `https://${environment}.api.test`); assert.equal(result.env.LEGACY_MARKER, undefined); } }); test("UPSTREAM_API_URL follows profile, local and process precedence while preserving legacy process overrides", async (t) => { const root = await fixture(t); await writeFile( join(root, "config/test.env"), "UPSTREAM_API_URL=http://127.0.0.1:21111\nPORT=21030\n", ); await writeFile( join(root, "config/test.local.env"), "UPSTREAM_API_URL=http://127.0.0.1:21112/\n", ); let result = nextCommand("test", "start", [], { root, variables: {} }); assert.equal(result.env.UPSTREAM_API_URL, "http://127.0.0.1:21112"); assert.equal(result.env.API_BASE_URL, result.env.UPSTREAM_API_URL); result = nextCommand("test", "start", [], { root, variables: { UPSTREAM_API_URL: "https://primary.api.test/", API_BASE_URL: "https://ignored.api.test", }, }); assert.equal(result.env.UPSTREAM_API_URL, "https://primary.api.test"); assert.equal(result.env.API_BASE_URL, result.env.UPSTREAM_API_URL); result = nextCommand("test", "start", [], { root, variables: { API_BASE_URL: "https://legacy-process.api.test" }, }); assert.equal(result.env.UPSTREAM_API_URL, "https://legacy-process.api.test"); for (const UPSTREAM_API_URL of [ "", "javascript:bad", "https://user:password@api.test", "https://api.test/?query=1", "https://api.test/#fragment", ]) { assert.throws( () => nextCommand("test", "start", [], { root, variables: { UPSTREAM_API_URL }, }), /UPSTREAM_API_URL/, ); } }); test("each environment selects its own build directory and supports port/hostname command overrides", async (t) => { const root = await fixture(t); for (const [environment, directory] of [ ["development", ".next-dev"], ["test", ".next-test"], ["production", ".next"], ]) { for (const command of ["dev", "build", "start"]) { const result = nextCommand(environment, command, [], { root, variables: {}, }); assert.equal(result.env.APP_DIST_DIR, directory); assert.equal(result.cwd, root); assert.equal( result.env.NODE_ENV, command === "dev" ? "development" : "production", ); if (command === "build") assert.deepEqual(result.args.slice(1), ["build"]); else assert.deepEqual(result.args.slice(2), [ "--hostname", "0.0.0.0", "--port", "21030", ]); } } const explicit = nextCommand( "test", "dev", ["--port", "31022", "--hostname=127.0.0.1", "--webpack"], { root, variables: {} }, ); assert.deepEqual(explicit.args.slice(1), [ "dev", "--port", "31022", "--hostname=127.0.0.1", "--webpack", ]); }); test("current-domain profiles preserve runtime resolution instead of requiring a build-time URL", async (t) => { const root = await fixture(t); for (const environment of ["test", "production"]) { await writeFile( join(root, "config", environment + ".env"), "UPSTREAM_API_URL=current\nPORT=21030\n", ); for (const command of ["dev", "build", "start"]) { const result = nextCommand(environment, command, [], { root, variables: {}, }); assert.equal(result.env.UPSTREAM_API_URL, "current"); assert.equal(result.env.API_BASE_URL, "current"); } const override = nextCommand(environment, "start", [], { root, variables: { UPSTREAM_API_URL: "http://127.0.0.1:21111" }, }); assert.equal(override.env.UPSTREAM_API_URL, "http://127.0.0.1:21111"); } }); test("invalid environment, command, API or port fails before a child process is started", async (t) => { const root = await fixture(t); for (const [environment, command] of [ ["staging", "dev"], ["../test", "dev"], ["development", "deploy"], ]) assert.throws( () => nextCommand(environment, command, [], { root, variables: {} }), /Usage:/, ); for (const API_BASE_URL of [ "", "javascript:bad", "https://name:password@api.test", "https://api.test/#fragment", ]) assert.throws( () => nextCommand("test", "dev", [], { root, variables: { API_BASE_URL } }), /API_BASE_URL/, ); for (const PORT of ["0", "65536", "not-a-port", "-1"]) assert.throws( () => nextCommand("test", "dev", [], { root, variables: { PORT } }), /PORT/, ); await rm(join(root, "config/test.env")); assert.throws( () => nextCommand("test", "dev", [], { root, variables: {} }), /缺少环境配置/, ); }); test("real child execution receives the selected environment, preserves args and propagates its exit status", async (t) => { const root = await fixture(t), bin = join(root, "node_modules/next/dist/bin"); await mkdir(bin, { recursive: true }); const output = join(root, "captured.json"); await writeFile( join(bin, "next"), `require('node:fs').writeFileSync(process.env.CAPTURE,JSON.stringify({app:process.env.APP_ENV,node:process.env.NODE_ENV,api:process.env.API_BASE_URL,args:process.argv.slice(2),cwd:process.cwd()}));process.exit(7);`, ); assert.equal( await runNext("test", "start", ["--port", "31021"], { root, variables: { CAPTURE: output }, }), 7, ); const saved = JSON.parse(await readFile(output, "utf8")); assert.deepEqual(saved, { app: "test", node: "production", api: "https://test.api.test", args: ["start", "--hostname", "0.0.0.0", "--port", "31021"], cwd: await realpath(root), }); }); test("project profiles and Next configuration keep the requested API and port with distinct artifacts", async () => { const source = ts.transpileModule( await readFile(new URL("../next.config.ts", import.meta.url), "utf8"), { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS, }, }, ).outputText; for (const [environment, directory] of [ ["development", ".next-dev"], ["test", ".next-test"], ["production", ".next"], ]) { const command = nextCommand(environment, "build", [], { variables: {} }), exports = {}; vm.runInNewContext(source, { exports, process: { env: command.env, cwd: () => process.cwd() }, }); assert.equal(exports.default.distDir, directory); assert.equal( command.env.UPSTREAM_API_URL, environment === "production" ? "http://139.162.49.252:21040" : "http://172.104.181.99:26792", ); assert.equal(command.env.API_BASE_URL, command.env.UPSTREAM_API_URL); assert.equal( command.env.PORT, environment === "production" ? "21042" : "21030", ); assert.equal(command.env.NEXT_PUBLIC_TRACKER_APP_ID, "1050"); const ignored = spawnSync( "git", ["check-ignore", "--no-index", `config/${environment}.local.env`], { encoding: "utf8" }, ); assert.equal(ignored.status, 0); } const pkg = JSON.parse( await readFile(new URL("../package.json", import.meta.url), "utf8"), ); for (const name of [ "dev", "dev:test", "build", "build:dev", "build:test", "start", "start:dev", "start:test", ]) assert.match(pkg.scripts[name], /next-env\.mjs/); });