311 lines
10 KiB
JavaScript
311 lines
10 KiB
JavaScript
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, undefined);
|
|
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/);
|
|
});
|