import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; const repoRoot = process.cwd(); const scriptPath = path.join(repoRoot, 'scripts', 'taro-h5-release-manifest.js'); const distRoot = path.join(repoRoot, 'apps', 'taro', 'dist'); function run(args = []) { return spawnSync(process.execPath, [scriptPath, '--json', ...args], { cwd: repoRoot, encoding: 'utf8', env: { PATH: process.env.PATH || '', Path: process.env.Path || '', SystemRoot: process.env.SystemRoot || '', ComSpec: process.env.ComSpec || '', TEMP: process.env.TEMP || os.tmpdir(), TMP: process.env.TMP || os.tmpdir(), }, }); } function parseJson(result) { return JSON.parse(result.stdout || '{}'); } function withRuntimeConfig(portal, config, fn) { const filePath = path.join(distRoot, portal, 'runtime-config.json'); const existed = fs.existsSync(filePath); const previous = existed ? fs.readFileSync(filePath, 'utf8') : ''; fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, `${JSON.stringify(config, null, 2)}\n`, 'utf8'); try { return fn(filePath); } finally { if (existed) fs.writeFileSync(filePath, previous, 'utf8'); else fs.rmSync(filePath, { force: true }); } } const loose = run(); assert.equal(loose.status, 0, loose.stderr); const loosePayload = parseJson(loose); assert.equal(loosePayload.schemaVersion, 1); assert.equal(loosePayload.summary.portals, 3); assert.equal(loosePayload.portals.length, 3); assert.ok(loosePayload.portals.every(item => item.buildCommand.startsWith('npm run build:taro:h5:'))); assert.ok(loosePayload.checks.some(item => item.id === 'build.student.portal_env' && item.status === 'pass')); const strictMissingRuntime = run(['--require-runtime-config']); assert.notEqual(strictMissingRuntime.status, 0, 'strict manifest should fail when runtime-config files are missing'); const strictPayload = parseJson(strictMissingRuntime); assert.ok( strictPayload.checks.some(item => item.id === 'runtime.student.exists' && item.status === 'fail'), 'missing student runtime config should be a failure', ); const validConfig = { portal: 'student', apiBaseUrl: 'https://api.example.com', supabaseUrl: 'https://auth.example.com', supabasePublishableKey: 'sb_publishable_test_key', tenantCode: '', }; withRuntimeConfig('h5-student', validConfig, () => { const withStudentRuntime = run(); assert.equal(withStudentRuntime.status, 0, withStudentRuntime.stderr); const payload = parseJson(withStudentRuntime); const student = payload.portals.find(item => item.portal === 'student'); assert.equal(student.runtimeConfig.exists, true); assert.equal(student.runtimeConfig.tenantCodeMode, 'domain-resolved'); assert.ok(payload.checks.some(item => item.id === 'runtime.student.api_https' && item.status === 'pass')); }); withRuntimeConfig('h5-student', { ...validConfig, supabasePublishableKey: 'replace-with-supabase-publishable-key', }, () => { const placeholder = run(); assert.notEqual(placeholder.status, 0, 'production manifest should reject placeholder publishable key in dist runtime config'); const payload = parseJson(placeholder); assert.ok(payload.checks.some(item => item.id === 'runtime.student.publishable_key' && item.status === 'fail')); }); withRuntimeConfig('h5-student', { ...validConfig, DATABASE_URL: `postgres${'ql'}://postgres:postgres@example.com/postgres`, }, () => { const leakedSecret = run(); assert.notEqual(leakedSecret.status, 0, 'manifest should reject forbidden runtime config keys'); const payload = parseJson(leakedSecret); assert.ok(payload.checks.some(item => item.id === 'runtime.student.keys' && item.status === 'fail')); assert.ok(payload.checks.some(item => item.id === 'runtime.student.secrets' && item.status === 'fail')); }); const outputPath = path.join(os.tmpdir(), `taro-release-manifest-${Date.now()}.json`); const written = run(['--write', outputPath]); assert.equal(written.status, 0, written.stderr); assert.equal(fs.existsSync(outputPath), true); const writtenPayload = JSON.parse(fs.readFileSync(outputPath, 'utf8')); assert.equal(writtenPayload.schemaVersion, 1); fs.rmSync(outputPath, { force: true }); console.log('[PASS] Taro H5 release manifest');