import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { enforceTaroH5RuntimePatches, replaceExactlyOnce, taroButtonLoadingPatch, taroH5RuntimePatchDefinition, taroInputWatcherPatch, } from './taro-components-h5-runtime-patch.js'; const repoRoot = process.cwd(); function writeJson(filePath, value) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8'); } function installedPristineSource(patch) { const target = path.join(repoRoot, 'node_modules', '@tarojs', 'components', patch.targetRelativePath); const installed = fs.readFileSync(target, 'utf8'); const pristine = installed.includes(patch.after) ? installed.replace(patch.after, patch.before) : installed; return pristine; } const pristineSources = new Map([ [taroInputWatcherPatch.id, installedPristineSource(taroInputWatcherPatch)], [taroButtonLoadingPatch.id, installedPristineSource(taroButtonLoadingPatch)], ]); function createFixture(options = {}) { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'tiku-taro-h5-runtime-patch-')); const taroManifest = { name: '@tiku-saas/taro', version: '0.1.0', scripts: { postinstall: taroH5RuntimePatchDefinition.postinstallCommand }, devDependencies: { '@tarojs/components': options.declaredVersion || taroH5RuntimePatchDefinition.packageVersion, }, }; writeJson(path.join(root, 'apps', 'taro', 'package.json'), taroManifest); writeJson(path.join(root, 'package-lock.json'), { lockfileVersion: 3, packages: { 'apps/taro': { hasInstallScript: true, devDependencies: { '@tarojs/components': options.lockWorkspaceVersion || taroH5RuntimePatchDefinition.packageVersion, }, }, 'node_modules/@tarojs/components': { version: options.lockVersion || taroH5RuntimePatchDefinition.packageVersion, integrity: options.integrity || taroH5RuntimePatchDefinition.lockIntegrity, }, }, }); writeJson(path.join(root, 'node_modules', '@tarojs', 'components', 'package.json'), { name: '@tarojs/components', version: options.installedVersion || taroH5RuntimePatchDefinition.packageVersion, }); const targets = new Map(); for (const patch of taroH5RuntimePatchDefinition.patches) { const target = path.join(root, 'node_modules', '@tarojs', 'components', patch.targetRelativePath); targets.set(patch.id, target); if (options.missingTarget !== patch.id) { fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync( target, options.sources?.[patch.id] ?? pristineSources.get(patch.id), 'utf8', ); } } return { root, targets }; } function targetContents(fixture) { return Object.fromEntries([...fixture.targets].map(([id, target]) => [ id, fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : null, ])); } const workingState = enforceTaroH5RuntimePatches({ root: repoRoot, mode: 'check' }); assert.equal(workingState.status, 'pass'); assert.equal(workingState.patches.inputWatcher.installedSha256, taroInputWatcherPatch.patchedSha256); assert.equal(workingState.patches.buttonLoading.installedSha256, taroButtonLoadingPatch.patchedSha256); const applied = createFixture(); try { const first = enforceTaroH5RuntimePatches({ root: applied.root, mode: 'apply' }); assert.equal(first.patches.inputWatcher.state, 'patched-now'); assert.equal(first.patches.buttonLoading.state, 'patched-now'); const second = enforceTaroH5RuntimePatches({ root: applied.root, mode: 'apply' }); assert.equal(second.patches.inputWatcher.state, 'patched'); assert.equal(second.patches.buttonLoading.state, 'patched'); assert.equal(enforceTaroH5RuntimePatches({ root: applied.root, mode: 'check' }).status, 'pass'); } finally { fs.rmSync(applied.root, { recursive: true, force: true }); } const pristineCheck = createFixture(); try { const before = targetContents(pristineCheck); assert.throws( () => enforceTaroH5RuntimePatches({ root: pristineCheck.root, mode: 'check' }), /patch is not applied/, ); assert.deepEqual(targetContents(pristineCheck), before, '--check must never modify a pristine install'); } finally { fs.rmSync(pristineCheck.root, { recursive: true, force: true }); } for (const [name, options, pattern] of [ ['declared version', { declaredVersion: '4.2.1' }, /must pin/], ['lock integrity', { integrity: 'sha512-unreviewed' }, /unexpected.*integrity/], ['installed version', { installedVersion: '4.2.1-beta.2' }, /does not match/], ['missing Input target', { missingTarget: taroInputWatcherPatch.id }, /target is missing/], ['missing Button target', { missingTarget: taroButtonLoadingPatch.id }, /target is missing/], ['unknown Input content', { sources: { [taroInputWatcherPatch.id]: `${pristineSources.get(taroInputWatcherPatch.id)}\n// tampered\n` } }, /unreviewed.*hash/], ['unknown Button content', { sources: { [taroButtonLoadingPatch.id]: `${pristineSources.get(taroButtonLoadingPatch.id)}\n// tampered\n` } }, /unreviewed.*hash/], ]) { const fixture = createFixture(options); try { const before = targetContents(fixture); assert.throws(() => enforceTaroH5RuntimePatches({ root: fixture.root, mode: 'apply' }), pattern, name); assert.deepEqual(targetContents(fixture), before, `${name} failure must not modify either target`); } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); } } assert.throws(() => replaceExactlyOnce('safe', 'unsafe', 'guarded'), /found 0/); assert.throws(() => replaceExactlyOnce('unsafe unsafe', 'unsafe', 'guarded'), /found 2/); assert.equal(replaceExactlyOnce('before unsafe after', 'unsafe', 'guarded'), 'before guarded after'); console.log('[PASS] Taro H5 runtime patch contract');