69 lines
4.1 KiB
JavaScript
69 lines
4.1 KiB
JavaScript
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;}
|
|
}
|