blob: dd5e6127e87375cb208af979aa4240c9c4f094c7 [file]
// Pinned Docusaurus compiler adapter. Rspack 1.2's closed native compiler is
// still reachable; only process exit gives this build a reliable memory bound.
const fs = require('node:fs');
const path = require('node:path');
const {createHash} = require('node:crypto');
const {createRequire} = require('node:module');
const {spawn} = require('node:child_process');
const digest = bytes => createHash('sha256').update(bytes).digest('hex');
function mdxCaches(config) {
const caches=[];
function inspect(item){
if(!item||typeof item!=='object')return;
if(item.options?.crossCompilerCache instanceof Map)caches.push(item.options);
for(const key of ['rules','oneOf','use']){
const value=item[key];if(Array.isArray(value))value.forEach(inspect);else inspect(value);
}
}
(config.module?.rules??[]).forEach(inspect);return caches;
}
async function snapshotMdx(config) {
const entries=new Map();
for(const options of mdxCaches(config))for(const [resource,entry]of options.crossCompilerCache){
const code=await entry.promise;
if(typeof code!=='string')throw new Error(`Unexpected compiled MDX value: ${resource}`);
if(entries.has(resource)&&entries.get(resource)!==code)throw new Error(`Conflicting compiled MDX resources: ${resource}`);
entries.set(resource,code);
}
return Buffer.from(JSON.stringify([...entries].sort(([a],[b])=>a.localeCompare(b))));
}
function restoreMdx(config,bytes) {
const entries=JSON.parse(bytes);
if(!Array.isArray(entries)||entries.some(row=>!Array.isArray(row)||row.length!==2||row.some(value=>typeof value!=='string')))throw new Error('Invalid isolated MDX snapshot');
for(const loader of mdxCaches(config)){
const cache=new Map(entries.map(([resource,code])=>[resource,{promise:Promise.resolve(code)}]));
cache.get=function(resource){if(!this.has(resource))throw new Error(`Missing isolated client MDX resource: ${resource}`);return Map.prototype.get.call(this,resource);};
loader.crossCompilerCache=cache;
}
}
const stateDirectory = (siteDir,locale) => path.join(siteDir,'.i18n-build/compiler-state',locale);
function reportFile(siteDir,locale) {return path.join(siteDir,'.i18n-build/compiler-metrics',locale+'.json');}
function updateReport(siteDir,locale,update) {
const file=reportFile(siteDir,locale);fs.mkdirSync(path.dirname(file),{recursive:true});
const value=fs.existsSync(file)?JSON.parse(fs.readFileSync(file)):{schemaVersion:1,locale,mode:process.env.DORIS_COMPILER_MODE??'isolated',records:[]};
update(value);fs.writeFileSync(file,JSON.stringify(value,null,2)+'\n');return value;
}
function coreModules(fromSite) {
const requireCore=createRequire(fromSite.resolve('@docusaurus/core/lib/commands/build/buildLocale.js'));
for(const name of ['@docusaurus/core','@docusaurus/bundler','@docusaurus/mdx-loader']) {
const version=requireCore(name+'/package.json').version;
if(version!=='3.6.3')throw new Error(`Revalidate isolated compilation for ${name} ${version}`);
}
return requireCore;
}
async function measure({siteDir,locale,name},compile) {
const startedAtMs=Date.now(),before=process.memoryUsage();let success=false,samples=0,peakRssBytes=before.rss,minimumSystemAvailableKiB=null;
const sample=()=>{samples++;peakRssBytes=Math.max(peakRssBytes,process.memoryUsage().rss);
try{const available=Number(fs.readFileSync('/proc/meminfo','utf8').match(/^MemAvailable:\s+(\d+)/m)?.[1]);if(available>0)minimumSystemAvailableKiB=Math.min(minimumSystemAvailableKiB??Infinity,available);}catch{}
};
const timer=setInterval(sample,1000);timer.unref();sample();
try{const result=await compile();success=true;return result;}
finally{sample();clearInterval(timer);const record={name,pid:process.pid,success,startedAtMs,finishedAtMs:Date.now(),sampleIntervalMs:1000,samples,peakRssBytes,minimumSystemAvailableKiB,before,afterClose:process.memoryUsage()};
updateReport(siteDir,locale,value=>value.records.push(record));console.log(JSON.stringify({event:'compiler_phase_end',locale,...record}));}
}
async function buildClient({fromSite,siteDir,outDir,locale,samplePlan}) {
const requireCore=coreModules(fromSite);
process.env.DOCUSAURUS_CURRENT_LOCALE=locale;
const {loadSite}=requireCore('@docusaurus/core/lib/server/site.js');
const {createConfigureWebpackUtils,executePluginsConfigureWebpack}=requireCore('@docusaurus/core/lib/webpack/configure.js');
const {createBuildClientConfig}=requireCore('@docusaurus/core/lib/webpack/client.js');
const {props}=await loadSite({siteDir,outDir,locale,localizePath:undefined});
const utils=await createConfigureWebpackUtils({siteConfig:props.siteConfig});
const {config:initial,clientManifestPath}=await createBuildClientConfig({props,minify:true,faster:props.siteConfig.future.experimental_faster,configureWebpackUtils:utils,bundleAnalyzer:false});
const config=executePluginsConfigureWebpack({plugins:props.plugins,config:initial,isServer:false,configureWebpackUtils:utils});
updateReport(siteDir,locale,value=>{value.bundler=utils.currentBundler.name;value.bundlerVersion='3.6.3';});
await measure({siteDir,locale,name:'client'},()=>requireCore('@docusaurus/bundler').compile({configs:[config],currentBundler:utils.currentBundler}));
const bytes=fs.readFileSync(clientManifestPath),directory=stateDirectory(siteDir,locale);fs.mkdirSync(directory,{recursive:true});
fs.writeFileSync(path.join(directory,'client-manifest.json'),bytes);
// The pinned MDX loader waits for client promises when compiling the
// server. Preserve their exact client JSX instead of recompiling in server
// mode, which would change inline image-loader behavior.
const mdx=await snapshotMdx(config),entryCount=JSON.parse(mdx).length;
fs.writeFileSync(path.join(directory,'client-mdx.json'),mdx);
console.log(JSON.stringify({event:'client_mdx_snapshot',locale,entries:entryCount,bytes:mdx.length}));
let scopeSha256;
if(samplePlan){const scope=fs.readFileSync(path.join(siteDir,'.i18n-build/ci-sample/scopes',locale+'.json'));fs.writeFileSync(path.join(directory,'client-scope.json'),scope);scopeSha256=digest(scope);}
return {clientManifestPath,manifestSha256:digest(bytes),mdxSha256:digest(mdx),mdxEntries:entryCount,bundler:utils.currentBundler.name,scopeSha256};
}
function createServerCompile({compile,restoreManifest,measureServer=callback=>callback()}) {
return async options=>{
const names=options.configs.map(config=>config.name);
if(names.length>2||names[0]!=='client'||names.length===2&&names[1]!=='server')throw new Error(`Revalidate the isolated compiler sequence: ${names.join(',')}`);
restoreManifest(options);
// The known hash-router path has no server compiler or SSG bundle.
if(names.length===1)return;
return measureServer(()=>compile({...options,configs:[options.configs[1]]}));
};
}
function install({fromSite,siteDir,locale,clientResult}) {
const requireCore=coreModules(fromSite),compiler=requireCore('@docusaurus/bundler/lib/compiler.js'),namespace=requireCore('@docusaurus/bundler');
const original=compiler.compile;
const wrapped=clientResult?createServerCompile({compile:original,restoreManifest:options=>{
if(options.currentBundler.name!==clientResult.bundler)throw new Error('Client/server bundler changed between isolated processes');
const generated=options.configs[0].resolve?.alias?.['@generated'];
if(typeof generated!=='string'||path.resolve(generated,'client-manifest.json')!==path.resolve(clientResult.clientManifestPath))throw new Error('Client manifest path changed between isolated processes');
const bytes=fs.readFileSync(path.join(stateDirectory(siteDir,locale),'client-manifest.json'));
if(digest(bytes)!==clientResult.manifestSha256)throw new Error('Isolated client manifest changed before SSG');
fs.mkdirSync(path.dirname(clientResult.clientManifestPath),{recursive:true});fs.writeFileSync(clientResult.clientManifestPath,bytes);
const mdx=fs.readFileSync(path.join(stateDirectory(siteDir,locale),'client-mdx.json'));
if(digest(mdx)!==clientResult.mdxSha256)throw new Error('Isolated client MDX changed before server compilation');
restoreMdx(options.configs[1]??options.configs[0],mdx);
},measureServer:callback=>measure({siteDir,locale,name:'server'},callback)})
:options=>measure({siteDir,locale,name:options.configs.map(c=>c.name).join('+')},()=>original(options));
compiler.compile=wrapped;
if(namespace.compile!==wrapped){compiler.compile=original;throw new Error('Docusaurus compile export no longer references the pinned adapter');}
return()=>{compiler.compile=original;};
}
async function runClient({siteDir,outDir,locale,samplePlan}) {
const directory=stateDirectory(siteDir,locale);fs.rmSync(directory,{recursive:true,force:true});
updateReport(siteDir,locale,value=>{value.mode='isolated';value.records=[];});
const args=[path.resolve(__dirname,'../build-locale.cjs'),'client',siteDir,outDir,locale,...(samplePlan?[samplePlan]:[])];
let signal=null;
await new Promise((resolve,reject)=>{
const child=spawn(process.execPath,args,{cwd:siteDir,stdio:'inherit',env:process.env});
console.log(JSON.stringify({event:'client_compiler_start',locale,pid:child.pid}));
const terminate=()=>{signal='SIGTERM';child.kill(signal);},interrupt=()=>{signal='SIGINT';child.kill(signal);};
const cleanup=()=>{process.removeListener('SIGTERM',terminate);process.removeListener('SIGINT',interrupt);};
process.once('SIGTERM',terminate);process.once('SIGINT',interrupt);
child.once('error',error=>{cleanup();reject(error);});
child.once('exit',(code,childSignal)=>{cleanup();updateReport(siteDir,locale,value=>{value.clientPid=child.pid;value.clientExitedAtMs=Date.now();value.clientExitCode=code;});
console.log(JSON.stringify({event:'client_compiler_exit',locale,pid:child.pid,code,signal:childSignal}));
if(code!==0||signal)reject(new Error(`Isolated client compiler failed (${signal??childSignal??code})`));else resolve();});
});
return JSON.parse(fs.readFileSync(path.join(directory,'result.json')));
}
function writeClientResult(siteDir,locale,result) {fs.writeFileSync(path.join(stateDirectory(siteDir,locale),'result.json'),JSON.stringify(result)+'\n');}
function combineSamplingScopes(server,client) {
const selection=scope=>JSON.stringify((scope.selected??[]).map(({pluginId,version,docId,route})=>[pluginId,version,docId,route]));
if(server.sourceSha!==client.sourceSha||server.locale!==client.locale||selection(server)!==selection(client))throw new Error('Isolated sample compiler scopes differ');
if(!Array.isArray(client.compiledMdxModules?.client))throw new Error('Isolated sample client compilation proof is missing');
return {...server,compiledMdxModules:{...server.compiledMdxModules,client:client.compiledMdxModules.client}};
}
function mergeSamplingScope(siteDir,locale,clientResult) {
const bytes=fs.readFileSync(path.join(stateDirectory(siteDir,locale),'client-scope.json'));
if(digest(bytes)!==clientResult.scopeSha256)throw new Error('Isolated sample client proof changed');
const file=path.join(siteDir,'.i18n-build/ci-sample/scopes',locale+'.json');
fs.writeFileSync(file,JSON.stringify(combineSamplingScopes(JSON.parse(fs.readFileSync(file)),JSON.parse(bytes)),null,2)+'\n');
}
module.exports={buildClient,createServerCompile,install,runClient,writeClientResult,stateDirectory,mdxCaches,snapshotMdx,restoreMdx,combineSamplingScopes,mergeSamplingScope};