|
| 1 | +// Custom Jest resolver. When CONTRACT_ARTIFACTS_VERSION is set, redirects *only* JSON artifact files under |
| 2 | +// @aztec/noir-contracts.js/artifacts/ and @aztec/noir-test-contracts.js/artifacts/ to a local cache of the pinned |
| 3 | +// legacy versions. TypeScript wrapper classes (e.g. Token.ts) continue to load from the current workspace and use the |
| 4 | +// current @aztec/aztec.js — only the artifact JSON (the deployed-contract ABI / bytecode / notes surface) is swapped. |
| 5 | +// |
| 6 | +// Why JSON-only: the JSON artifact is the actual interchange surface a "deployed contract" exposes. The TS wrapper is |
| 7 | +// generated client-side ergonomics that's tightly coupled to the current @aztec/aztec.js API. Redirecting the wrapper |
| 8 | +// would couple this test to a moving aztec.js surface and break at import time on unrelated breaking changes; we want |
| 9 | +// to fail only on actual artifact-compat regressions. |
| 10 | +// |
| 11 | +// The cache is populated on demand by running `npm install` into .legacy-contracts/<version>/. |
| 12 | +// |
| 13 | +// Activated by env var; passthrough otherwise. |
| 14 | +/* eslint-disable @typescript-eslint/no-require-imports */ |
| 15 | + |
| 16 | +const path = require('path'); |
| 17 | +const fs = require('fs'); |
| 18 | +const { execSync } = require('child_process'); |
| 19 | + |
| 20 | +const version = process.env.CONTRACT_ARTIFACTS_VERSION; |
| 21 | +const REDIRECTED = ['@aztec/noir-contracts.js', '@aztec/noir-test-contracts.js']; |
| 22 | + |
| 23 | +// Jest sets rootDir to <e2e>/src; this file lives there too. |
| 24 | +const e2eRoot = path.resolve(__dirname, '..'); |
| 25 | +const cacheRoot = version ? path.join(e2eRoot, '.legacy-contracts', version) : null; |
| 26 | + |
| 27 | +function pkgJsonPath(name) { |
| 28 | + return path.join(cacheRoot, 'node_modules', name, 'package.json'); |
| 29 | +} |
| 30 | + |
| 31 | +function ensureCache() { |
| 32 | + const missing = REDIRECTED.some(p => !fs.existsSync(pkgJsonPath(p))); |
| 33 | + if (!missing) { |
| 34 | + return; |
| 35 | + } |
| 36 | + fs.mkdirSync(cacheRoot, { recursive: true }); |
| 37 | + // Seed a standalone package.json so `npm install --prefix` treats cacheRoot as its own project. Without this, npm |
| 38 | + // walks up and finds the yarn-project workspace root, which breaks on `workspace:` protocol deps and risks |
| 39 | + // clobbering the monorepo's node_modules. |
| 40 | + const seed = path.join(cacheRoot, 'package.json'); |
| 41 | + if (!fs.existsSync(seed)) { |
| 42 | + fs.writeFileSync(seed, JSON.stringify({ name: 'legacy-contracts-cache', private: true })); |
| 43 | + } |
| 44 | + |
| 45 | + const specs = REDIRECTED.map(p => `${p}@${version}`).join(' '); |
| 46 | + process.stderr.write(`[legacy-contracts] installing ${specs} into ${cacheRoot}\n`); |
| 47 | + // --prefix: install into cacheRoot instead of cwd, so the cache is isolated from the monorepo. |
| 48 | + // --no-save: don't write the installed packages back to the seeded package.json. |
| 49 | + // --ignore-scripts: skip lifecycle scripts (preinstall/postinstall) of the legacy packages and their transitive |
| 50 | + // deps; we only want the files on disk, not to run any build steps. |
| 51 | + // --legacy-peer-deps: tolerate peer-dependency mismatches between the pinned legacy @aztec/* graph and whatever |
| 52 | + // current versions npm would otherwise try to reconcile. |
| 53 | + execSync(`npm install --prefix "${cacheRoot}" --no-save --ignore-scripts --legacy-peer-deps ${specs}`, { |
| 54 | + stdio: 'inherit', |
| 55 | + }); |
| 56 | + |
| 57 | + // Verify versions on disk match the requested version. |
| 58 | + for (const p of REDIRECTED) { |
| 59 | + const onDisk = JSON.parse(fs.readFileSync(pkgJsonPath(p), 'utf8')).version; |
| 60 | + if (onDisk !== version) { |
| 61 | + throw new Error(`[legacy-contracts] ${p} on disk is ${onDisk}, expected ${version}`); |
| 62 | + } |
| 63 | + } |
| 64 | +} |
| 65 | + |
| 66 | +if (version) { |
| 67 | + ensureCache(); |
| 68 | +} |
| 69 | + |
| 70 | +let bannerPrinted = false; |
| 71 | +const seen = new Set(); |
| 72 | + |
| 73 | +function printBannerOnce() { |
| 74 | + if (bannerPrinted || !version) { |
| 75 | + return; |
| 76 | + } |
| 77 | + bannerPrinted = true; |
| 78 | + const lines = ['='.repeat(60), `[legacy-contracts][jest] CONTRACT_ARTIFACTS_VERSION=${version}`]; |
| 79 | + for (const p of REDIRECTED) { |
| 80 | + const v = JSON.parse(fs.readFileSync(pkgJsonPath(p), 'utf8')).version; |
| 81 | + if (v !== version) { |
| 82 | + throw new Error(`[legacy-contracts] ${p} on disk is ${v}, expected ${version}`); |
| 83 | + } |
| 84 | + lines.push(`[legacy-contracts][jest] redirecting ${p}/artifacts/*.json -> .legacy-contracts/${version}/...`); |
| 85 | + } |
| 86 | + lines.push('='.repeat(60)); |
| 87 | + process.stderr.write(lines.join('\n') + '\n'); |
| 88 | +} |
| 89 | + |
| 90 | +// Match a resolved absolute path against the workspace artifacts dirs and return the legacy cache equivalent, or null |
| 91 | +// if it's not an artifact path we should redirect. |
| 92 | +function legacyArtifactPath(resolved) { |
| 93 | + if (!resolved.endsWith('.json')) { |
| 94 | + return null; |
| 95 | + } |
| 96 | + for (const pkg of REDIRECTED) { |
| 97 | + // pkg = '@aztec/noir-contracts.js' -> match '/noir-contracts.js/artifacts/' |
| 98 | + const dirName = pkg.split('/')[1]; |
| 99 | + const marker = `/${dirName}/artifacts/`; |
| 100 | + const idx = resolved.indexOf(marker); |
| 101 | + if (idx === -1) { |
| 102 | + continue; |
| 103 | + } |
| 104 | + const basename = resolved.slice(idx + marker.length); |
| 105 | + return path.join(cacheRoot, 'node_modules', pkg, 'artifacts', basename); |
| 106 | + } |
| 107 | + return null; |
| 108 | +} |
| 109 | + |
| 110 | +module.exports = function legacyResolver(request, options) { |
| 111 | + // Always run the default resolver first. We only inspect (and possibly rewrite) the *result*; this catches both |
| 112 | + // bare-specifier imports of `@aztec/noir-contracts.js/artifacts/foo.json` and the relative `../artifacts/foo.json` |
| 113 | + // imports inside the workspace TS wrapper classes — both resolve to the same workspace artifact path that we then |
| 114 | + // redirect. |
| 115 | + const resolved = options.defaultResolver(request, options); |
| 116 | + if (!version) { |
| 117 | + return resolved; |
| 118 | + } |
| 119 | + printBannerOnce(); |
| 120 | + const legacy = legacyArtifactPath(resolved); |
| 121 | + if (!legacy) { |
| 122 | + return resolved; |
| 123 | + } |
| 124 | + if (!fs.existsSync(legacy)) { |
| 125 | + throw new Error( |
| 126 | + `[legacy-contracts] artifact ${path.basename(legacy)} not present in legacy cache @${version}; ` + |
| 127 | + `the contract may have been added after that release. Pin a newer CONTRACT_ARTIFACTS_VERSION or skip this test.`, |
| 128 | + ); |
| 129 | + } |
| 130 | + if (!seen.has(resolved)) { |
| 131 | + seen.add(resolved); |
| 132 | + process.stderr.write(`[legacy-contracts][jest] redirected ${path.basename(legacy)} -> ${legacy}\n`); |
| 133 | + } |
| 134 | + return legacy; |
| 135 | +}; |
0 commit comments