@@ -0,0 +1,68 @@
|
||||
import {cp, lstat, mkdir, readFile, realpath, rm, writeFile} from 'node:fs/promises';
|
||||
import {basename, join, relative, resolve, sep} from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {copyReleaseTree, under} from './copy-release-tree.mjs';
|
||||
|
||||
const projectRoot=fileURLToPath(new URL('../',import.meta.url));
|
||||
|
||||
/** Assemble a relocatable release; only the selected public environment profile is shipped. */
|
||||
export async function packageStandalone(environment,{root=projectRoot}={}) {
|
||||
if(!['test','production'].includes(environment))throw new Error('Usage: node scripts/package-standalone.mjs test|production');
|
||||
root=await realpath(root);
|
||||
const build=environment==='test'?'.next-test':'.next';
|
||||
const dist=join(root,'dist'),destination=join(dist,environment),source=join(root,build,'standalone');
|
||||
async function directory(path,optional=false) {
|
||||
const info=await lstat(path).catch(error=>{if(optional&&error.code==='ENOENT')return null;throw error;});
|
||||
if(info&&(!info.isDirectory()||info.isSymbolicLink()||await realpath(path)!==path))throw new Error(`Unsafe release directory: ${path}`);
|
||||
}
|
||||
for(const path of [join(root,build),source])await directory(path);
|
||||
for(const file of [join(source,'server.js'),join(source,build,'BUILD_ID'),join(root,'config',environment+'.env')]) {
|
||||
if(!(await lstat(file)).isFile()||!(await readFile(file)).length)throw new Error(`Missing release file: ${file}`);
|
||||
}
|
||||
await directory(dist,true);await mkdir(dist,{recursive:true});await directory(destination,true);
|
||||
// A fixed dist/<environment> is the only location removed by this command.
|
||||
await rm(destination,{recursive:true,force:true});
|
||||
const filter=async path=>{
|
||||
const name=basename(path);
|
||||
if(name.startsWith('.env')||name.endsWith('.local.env')||name.startsWith('._')||name==='.DS_Store'||name.endsWith('.log'))return false;
|
||||
// Dynamic filesystem reads can make Next trace sibling build/cache/test directories.
|
||||
// Keep this project's runtime roots, never recursively include earlier dist releases.
|
||||
if(path.startsWith(source+sep)&&!['server.js','package.json','package-lock.json',build,'node_modules','src'].includes(relative(source,path).split(sep)[0]))return false;
|
||||
if(path===join(source,build,'cache'))return false;
|
||||
// npm installs a flat tree; pnpm/Next standalone keeps relative in-tree links.
|
||||
// copyReleaseTree rewrites safe links; reject only targets that escape the project root.
|
||||
if((await lstat(path)).isSymbolicLink()){
|
||||
const target=await realpath(path).catch(()=>null);
|
||||
if(!target||!under(target,root))throw new Error(`Release source contains a symbolic link: ${path}`);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
await copyReleaseTree(source,destination,{root,filter});
|
||||
// public/static are plain trees; dotenv is pnpm-linked — ship a concrete copy for runtime helpers.
|
||||
for(const [from,to] of [[join(root,'public'),'public'],[join(root,build,'static'),build+'/static'],
|
||||
[join(root,'node_modules/dotenv'),'node_modules/dotenv']]) {
|
||||
await cp(await realpath(from),join(destination,to),{recursive:true,dereference:true,filter});
|
||||
}
|
||||
await mkdir(join(destination,'config'),{recursive:true});await mkdir(join(destination,'scripts'),{recursive:true});
|
||||
for(const file of ['package.json','package-lock.json','scripts/next-env.mjs','scripts/check-health.mjs','config/'+environment+'.env']) {
|
||||
await cp(join(root,file),join(destination,file));
|
||||
}
|
||||
await writeFile(join(destination,'start.mjs'),`import {fileURLToPath} from 'node:url';
|
||||
import {nextCommand} from './scripts/next-env.mjs';
|
||||
const root=fileURLToPath(new URL('./',import.meta.url));
|
||||
process.chdir(root);
|
||||
const {env}=nextCommand(${JSON.stringify(environment)},'start',[],{root});
|
||||
Object.assign(process.env,env);
|
||||
process.env.HOSTNAME=process.env.HOSTNAME||'0.0.0.0';
|
||||
await import('./server.js');
|
||||
`);
|
||||
console.log(`Standalone release ready: ${destination}`);
|
||||
return destination;
|
||||
}
|
||||
|
||||
if(process.argv[1]&&resolve(process.argv[1])===fileURLToPath(import.meta.url)) {
|
||||
try {
|
||||
if(process.argv.length!==3)throw new Error('Usage: node scripts/package-standalone.mjs test|production');
|
||||
await packageStandalone(process.argv[2]);
|
||||
}catch(error){console.error(error.message);process.exitCode=1;}
|
||||
}
|
||||
Reference in New Issue
Block a user