69 lines
3.8 KiB
JavaScript
69 lines
3.8 KiB
JavaScript
import {existsSync, readFileSync} from 'node:fs';
|
||
import {resolve} from 'node:path';
|
||
import {fileURLToPath} from 'node:url';
|
||
import {spawn} from 'node:child_process';
|
||
import {parse} from 'dotenv';
|
||
|
||
const projectRoot=fileURLToPath(new URL('../',import.meta.url));
|
||
const directories={development:'.next-dev',test:'.next-test',production:'.next'};
|
||
const commands=new Set(['dev','build','start']);
|
||
const readConfig=file=>existsSync(file)?parse(readFileSync(file)):{};
|
||
// Normalize each layer before merging so a legacy process override still wins over a profile.
|
||
const apiConfig=values=>({...values,...(!Object.hasOwn(values,'UPSTREAM_API_URL')&&Object.hasOwn(values,'API_BASE_URL')?{UPSTREAM_API_URL:values.API_BASE_URL}:{})});
|
||
|
||
/** APP_ENV selects the deployment; NODE_ENV keeps Next's development/production semantics. */
|
||
export function nextCommand(environment='development',command='dev',args=[],{root=projectRoot,variables=process.env}={}) {
|
||
if(!Object.hasOwn(directories,environment)||!commands.has(command))throw new Error('Usage: node scripts/next-env.mjs development|test|production dev|build|start');
|
||
const base=resolve(root,'config',environment+'.env');
|
||
if(!existsSync(base))throw new Error(`缺少环境配置:config/${environment}.env`);
|
||
const env={
|
||
...apiConfig(readConfig(base)),
|
||
// Preserve the old local-development setup without leaking it into test/production.
|
||
...(environment==='development'?apiConfig(readConfig(resolve(root,'.env.local'))):{}),
|
||
...apiConfig(readConfig(resolve(root,'config',environment+'.local.env'))),
|
||
...apiConfig(Object.fromEntries(Object.entries(variables).filter(([,value])=>value!==undefined))),
|
||
APP_ENV:environment,
|
||
APP_DIST_DIR:directories[environment],
|
||
NODE_ENV:command==='dev'?'development':'production',
|
||
};
|
||
try {
|
||
const value=(env.UPSTREAM_API_URL||'').trim();
|
||
if(value==='current')env.UPSTREAM_API_URL=value;
|
||
else{
|
||
const api=new URL(value);
|
||
if(!['http:','https:'].includes(api.protocol)||api.username||api.password||api.search||api.hash)throw new Error();
|
||
env.UPSTREAM_API_URL=api.href.replace(/\/+$/,'');
|
||
}
|
||
env.API_BASE_URL=env.UPSTREAM_API_URL;
|
||
}catch{throw new Error(`config/${environment}.env 的 UPSTREAM_API_URL(兼容 API_BASE_URL)必须为 current 或有效的 HTTP(S) 接口地址`);}
|
||
const port=String(env.PORT||'21021');
|
||
if(!/^\d+$/.test(port)||Number(port)<1||Number(port)>65535)throw new Error('PORT 必须在 1–65535 之间');
|
||
env.PORT=port;
|
||
const hasOption=names=>args.some(arg=>names.some(name=>arg===name||arg.startsWith(name+'=')));
|
||
const options=command==='build'?[]:[
|
||
...(hasOption(['--hostname','-H'])?[]:['--hostname','0.0.0.0']),
|
||
...(hasOption(['--port','-p'])?[]:['--port',port]),
|
||
];
|
||
return {executable:process.execPath,args:[resolve(root,'node_modules/next/dist/bin/next'),command,...options,...args],cwd:root,env};
|
||
}
|
||
|
||
export async function runNext(environment,command,args,options) {
|
||
const spec=nextCommand(environment,command,args,options);
|
||
const child=spawn(spec.executable,spec.args,{cwd:spec.cwd,env:spec.env,stdio:'inherit'});
|
||
const interrupt=()=>child.kill('SIGINT'),terminate=()=>child.kill('SIGTERM');
|
||
process.on('SIGINT',interrupt);process.on('SIGTERM',terminate);
|
||
try {
|
||
return await new Promise((resolve,reject)=>{
|
||
child.once('error',reject);
|
||
child.once('exit',(code,signal)=>resolve(code??(signal==='SIGINT'?130:143)));
|
||
});
|
||
}finally{process.off('SIGINT',interrupt);process.off('SIGTERM',terminate);}
|
||
}
|
||
|
||
if(process.argv[1]&&resolve(process.argv[1])===fileURLToPath(import.meta.url)){
|
||
try {
|
||
const [environment='development',command='dev',...args]=process.argv.slice(2);
|
||
process.exitCode=await runNext(environment,command,args);
|
||
}catch(error){console.error(error instanceof Error?error.message:'启动 Next.js 失败');process.exitCode=1;}
|
||
}
|