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=21021\nNEXT_PUBLIC_TRACKER_APP_ID=1047\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=21021\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','21021']); } } 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=21021\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,'https://webhgcgapi.mfcg.me');assert.equal(command.env.API_BASE_URL,command.env.UPSTREAM_API_URL);assert.equal(command.env.PORT,'21021');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/); });