| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657 |
- import fs from 'node:fs/promises';
- import path from 'node:path';
- import crypto from 'node:crypto';
- import {fileURLToPath} from 'node:url';
- const sha=bytes=>crypto.createHash('sha256').update(bytes.toString('utf8').replaceAll('\r\n','\n')).digest('hex');
- const jsType=value=>/^(?:application|text)\/javascript$/i.test(String(value).split(';')[0].trim());
- const remedy='将 mjs 加入 nginx 的 JavaScript MIME 映射,确认配置后执行 nginx -t && nginx -s reload,再重新预检。';
- /** Read-only gate: a correct file hash cannot compensate for an unusable module Content-Type. */
- export async function preflightMjsMime({origin,targetDir,mimeFile='/etc/nginx/mime.types',files,fetchImpl=fetch}){
- const result={ok:false,readOnly:true,checkedAt:new Date().toISOString(),checks:[],modules:[],businessWrites:0};
- const check=(name,ok,extra={})=>result.checks.push({name,ok,...extra});
- try{
- const base=new URL(origin);
- if(!['http:','https:'].includes(base.protocol)||base.username||base.password||base.pathname!=='/'||base.search||base.hash)throw Error('INVALID_ORIGIN');
- if(!path.isAbsolute(targetDir)||await fs.realpath(targetDir)!==path.resolve(targetDir))throw Error('INVALID_TARGET_DIRECTORY');
- result.origin=base.origin;result.targetDir=targetDir;
- const text=(await fs.readFile(mimeFile,'utf8')).replace(/#[^\r\n]*/g,'');
- const mappings=text.split(';').map(s=>s.replace(/[{}]/g,' ').trim().split(/\s+/)).filter(words=>words.includes('mjs'));
- check('nginx-mjs-javascript-mapping',mappings.length===1&&mappings[0].some(jsType),{mimeFile});
- if(!result.checks[0].ok)throw Error('MJS_JAVASCRIPT_MAPPING_MISSING');
- const modules=files.filter(file=>file.path.endsWith('.mjs'));
- for(const file of modules){
- if(file.path.startsWith('/')||file.path.includes('\\')||file.path.split('/').some(p=>p==='..'||!p))throw Error('INVALID_MODULE_PATH');
- const local=path.resolve(targetDir,file.path);
- let bytes;
- try{bytes=await fs.readFile(local);}catch(e){if(e.code==='ENOENT'){result.modules.push({path:file.path,checked:false,reason:'not-yet-deployed'});continue;}throw e;}
- const url=new URL(file.path,base);url.searchParams.set('mjsPreflight',crypto.randomUUID());
- const response=await fetchImpl(url,{headers:{'cache-control':'no-cache'},redirect:'error',signal:AbortSignal.timeout(30000)});
- const contentType=response.headers.get('content-type')||'',body=Buffer.from(await response.arrayBuffer());
- const row={path:file.path,checked:true,status:response.status,contentType,bytesMatchTarget:sha(bytes)===sha(body),ok:response.status===200&&jsType(contentType)&&sha(bytes)===sha(body)};
- result.modules.push(row);check('public-mjs:'+file.path,row.ok);
- }
- result.publicModulesChecked=result.modules.filter(m=>m.checked).length;
- result.publicVerificationDeferred=result.modules.some(m=>!m.checked);
- result.ok=result.checks.every(c=>c.ok);
- if(!result.ok)result.errorCode='PUBLIC_MJS_NOT_LOADABLE';
- }catch(e){result.errorCode=/^[A-Z_]+$/.test(e.message)?e.message:'MJS_PREFLIGHT_UNAVAILABLE';}
- if(!result.ok)result.remedy=remedy;
- return result;
- }
- if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){
- try{
- const options={origin:'https://kangshifu.fmode.cn'};const args=process.argv.slice(2);
- for(let i=0;i<args.length;i++){
- const key={'--origin':'origin','--target-dir':'targetDir','--mime-file':'mimeFile','--manifest':'manifest'}[args[i]];
- if(!key||!args[i+1]||args[i+1].startsWith('--'))throw Error('USAGE');options[key]=args[++i];
- }
- if(!options.targetDir)throw Error('USAGE');
- const manifestPath=options.manifest||'deploy/board-local-20261111.json';
- if(!/^deploy\/[a-zA-Z0-9_.-]+\.json$/.test(manifestPath))throw Error('USAGE');
- const manifest=JSON.parse(await fs.readFile(new URL('../'+manifestPath,import.meta.url)));
- const result=await preflightMjsMime({...options,files:manifest.files});console.log(JSON.stringify(result,null,2));if(!result.ok)process.exitCode=1;
- }catch{console.error(JSON.stringify({ok:false,readOnly:true,errorCode:'MJS_PREFLIGHT_STOPPED',remedy}));process.exitCode=1;}
- }
|