'use strict' const fs=require('fs'), path=require('path'), cp=require('child_process') const repo=path.resolve(__dirname,'../..'), workspace=path.dirname(repo) const apiMode=process.argv.includes('--api') const appidArg=process.argv.find(arg=>arg.startsWith('--appid=')) const localAppid=appidArg && appidArg.slice(8) if(appidArg && !/^wx[0-9a-f]{16}$/.test(localAppid)) throw Error('Invalid local AppID') const root=path.join(workspace,'.local-acceptance'), build=path.join(root,apiMode?'mp-local-api-build':'mp-mock-build') const modules=path.join(root,'mp-build','node_modules') if(!fs.existsSync(path.join(modules,'@wepy/cli/bin/wepy.js'))) throw Error('Run the isolated dependency installation first') fs.mkdirSync(build,{recursive:true}) // Only remove this tool's generated directories, never source or another dependency tree. for(const name of ['src','weapp']) {const target=path.resolve(build,name);if(!target.startsWith(build+path.sep))throw Error('Unsafe target');fs.rmSync(target,{recursive:true,force:true})} fs.cpSync(path.join(repo,'src'),path.join(build,'src'),{recursive:true}) for(const name of ['package.json','package-lock.json','wepy.config.js'])fs.copyFileSync(path.join(repo,name),path.join(build,name)) if(!fs.existsSync(path.join(build,'node_modules')))fs.symlinkSync(modules,path.join(build,'node_modules'),'junction') function walk(dir,fn){for(const entry of fs.readdirSync(dir,{withFileTypes:true})){const p=path.join(dir,entry.name);if(entry.isDirectory())walk(p,fn);else fn(p)}} // Static media is local. No remote pictures, fonts, embedded web pages or SDK destinations remain. const urlPattern=/(?:https?|wss?):\/\/[^\s"'`<>\\)]+/gi let replaced=0 walk(path.join(build,'src'),p=>{if(!/\.(js|wpy|wxss|less|json|wxml)$/.test(p))return;let text=fs.readFileSync(p,'utf8');text=text.replace(urlPattern,()=>{replaced++;return '/images/tabBar/my.png'});text=text.replace(/]*>(?:<\/web-view>)?/g,'本地模拟:外部网页已禁用');fs.writeFileSync(p,text)}) // Replace just the two host assignments in the copied config, with a hard assertion on source shape. const config=path.join(build,'src/config.js');let text=fs.readFileSync(config,'utf8');let hosts=0 text=text.replace(/^(host|hostGo) = '[^']*'.*$/gm,(_,name)=>{hosts++;return name+" = 'http://127.0.0.1:18765/go/api'"}) if(hosts!==2)throw Error('Expected exactly two active API host assignments') fs.writeFileSync(config,text) // The legacy error branch throws after rejecting. In the acceptance copy, unsupported mocks // should show the existing modal and reject normally, not crash the simulator callback. const requests=path.join(build,'src/mixins/https.js') fs.writeFileSync(requests,fs.readFileSync(requests,'utf8').replace(/throw errorMessage/g,'return')) fs.copyFileSync(path.join(__dirname,'im.js'),path.join(build,'src/utils/im.js')) fs.writeFileSync(path.join(build,'src/mp_ecard_sdk/main.js'),"export function initEid() {}\nexport function startEid() { wx.showModal({title:'本地模拟',content:'实名认证未执行,认证状态不会改变',showCancel:false}) }\n") const result=cp.spawnSync(process.execPath,[path.join(modules,'@wepy/cli/bin/wepy.js'),'build','--no-cache'],{cwd:build,env:{...process.env,NODE_ENV:'production'},encoding:'utf8',maxBuffer:20*1024*1024}) fs.mkdirSync(path.join(root,'logs'),{recursive:true});fs.writeFileSync(path.join(root,'logs/mp-mock-build.log'),(result.stdout||'')+(result.stderr||'')) if(result.status!==0)throw Error('WePY build failed: '+(result.error?result.error.code:result.status)+'; inspect local log (redact before sharing)') const output=path.join(build,'weapp') const fixTemplates=require('./fix-template-expressions.cjs') walk(output,p=>{if(p.endsWith('.wxml'))fs.writeFileSync(p,fixTemplates(fs.readFileSync(p,'utf8')))}) fs.copyFileSync(path.join(__dirname,'runtime.js'),path.join(output,'acceptance-runtime.js')) const appFile=path.join(output,'app.js');fs.writeFileSync(appFile,"require('./acceptance-runtime').install(wx"+(apiMode?", {apiOrigin:'http://127.0.0.1:18766'}":"")+");\n"+fs.readFileSync(appFile,'utf8')) const appPath=path.join(output,'app.json');const app=JSON.parse(fs.readFileSync(appPath));app.pages.unshift('local-acceptance/index');app.window.navigationBarTitleText='本地模拟验收';fs.writeFileSync(appPath,JSON.stringify(app,null,2)) const panel=path.join(output,'local-acceptance');fs.mkdirSync(panel,{recursive:true}) fs.writeFileSync(path.join(panel,'index.json'),JSON.stringify({navigationBarTitleText:'本地模拟验收'})) fs.writeFileSync(path.join(panel,'index.wxml'),'福恋 · 本地模拟验收合成数据,不访问后端或第三方服务。远程图片已替换,外部字体可能与正式版不同。支付、认证、上传不属于本轮可验收范围。重新编译回到此入口。测试注册验证码:000000。不要输入真实个人资料。') fs.writeFileSync(path.join(panel,'index.js'),`Page({data:{failed:false},onShow(){this.setData({failed:wx.__loveAcceptance.snapshot().failure})},enter(){wx.switchTab({url:'/pages/tabBar/home'})},membership(){wx.navigateTo({url:'/pages/users/upgradeVIP'})},edit(){wx.navigateTo({url:'/pages/users/selfTextarea?text=自我描述'})},publish(){wx.navigateTo({url:'/pages/dynamic/issue'})},reset(){wx.__loveAcceptance.reset();wx.reLaunch({url:'/local-acceptance/index'})},failure(){wx.__loveAcceptance.scenario('failure',!this.data.failed);this.onShow()}})`) // Permanent visual marker on every generated page, including subpackages. walk(output,p=>{if(p.endsWith('.wxml')){let markup=fs.readFileSync(p,'utf8').replace(/open-type="[^"]*"/g,'disabled="true" data-local-disabled="true"');fs.writeFileSync(p,markup+'\n本地模拟 · 合成数据\n')}}) const project=JSON.parse(fs.readFileSync(path.join(repo,'project.config.json'))) if(!process.argv.includes('--source-appid')) project.appid='touristappid';if(localAppid)project.appid=localAppid;project.projectname=apiMode?'福恋-本地API验收':'福恋-本地模拟验收';project.setting={...project.setting,urlCheck:!apiMode};delete project.packOptions fs.writeFileSync(path.join(build,'project.config.json'),JSON.stringify(project,null,2)) const testPackage=JSON.parse(fs.readFileSync(path.join(build,'package.json'))) testPackage.private=true testPackage.scripts={build:'node ../../ufutx_love_mp/tools/local-acceptance/build.cjs'+(apiMode?' --api':'')+(process.argv.includes('--source-appid')?' --source-appid':'')+(localAppid?' --appid='+localAppid:''),test:'node ../../ufutx_love_mp/tools/local-acceptance/test.cjs'} fs.writeFileSync(path.join(build,'package.json'),JSON.stringify(testPackage,null,2)) // Built vendor code can contain dormant SDK URLs. Scrub them too; runtime denies all native transport. walk(output,p=>{if(!/\.(js|wxml|wxss|json)$/.test(p)||p.endsWith('acceptance-runtime.js'))return;let t=fs.readFileSync(p,'utf8');t=t.replace(urlPattern,url=>url.startsWith('http://127.0.0.1:18765/')||url==='http://127.0.0.1:18766'?url:'/images/tabBar/my.png');fs.writeFileSync(p,t)}) if(apiMode){const p=path.join(panel,'index.wxml');fs.writeFileSync(p,fs.readFileSync(p,'utf8').replace('福恋 · 本地模拟验收','福恋 · 本地 API 验收').replace('合成数据,不访问后端或第三方服务。','登录、资料读取/保存与 DMA 走本机真实 Go API;其他流程仍为 mock。').replace('重置合成数据','重置 mock 数据(不重置数据库)'))} fs.writeFileSync(path.join(build,'ACCEPTANCE-ONLY.txt'),'LOCAL MOCK ONLY. Never upload or publish this project. Generated from the current working tree. See repository tools/local-acceptance/README.txt.\n') const crypto=require('crypto') fs.writeFileSync(path.join(build,'acceptance-manifest.json'),JSON.stringify({builtAt:new Date().toISOString(),node:process.version,mode:apiMode?'local-api-with-mock-fallbacks':'offline-in-process-mock',source:repo,privateGoModulesUsed:apiMode,lockSha256:crypto.createHash('sha256').update(fs.readFileSync(path.join(repo,'package-lock.json'))).digest('hex'),runtimeSha256:crypto.createHash('sha256').update(fs.readFileSync(path.join(__dirname,'runtime.js'))).digest('hex')},null,2)) console.log(JSON.stringify({build,staticUrlsReplaced:replaced,mode:apiMode?'local-api-with-mock-fallbacks':'offline-in-process-mock',privateGoModulesUsed:apiMode}))