|
| 1 | +#!/usr/bin/env node |
| 2 | +/** |
| 3 | + * End-to-end pre-release smoke test. |
| 4 | + * |
| 5 | + * Packs the package with `npm pack`, installs the tarball into a throwaway |
| 6 | + * project, and verifies the consumer-facing surface: |
| 7 | + * 1. The published tarball contains nitrogen native bindings + JS subpath |
| 8 | + * proxy `package.json` files (catches "missing autolinking.rb" type bugs). |
| 9 | + * 2. Node's CJS resolver (which honors the package `exports` map) can |
| 10 | + * resolve every documented entry point. |
| 11 | + * 3. The same subpaths also resolve via the legacy `main`/`module`/ |
| 12 | + * `react-native` fields in the proxy directories — this is what bundlers |
| 13 | + * that ignore `exports` (older Re.Pack/rspack/Metro setups) rely on. |
| 14 | + * 4. The podspec and the generated iOS autolinking.rb have valid Ruby |
| 15 | + * syntax, so `pod install` will not blow up at parse time. |
| 16 | + * |
| 17 | + * Designed to run inside `release-it`'s `before:init` hook so a broken |
| 18 | + * release is caught before the npm publish + git push. |
| 19 | + */ |
| 20 | +const { execSync } = require('node:child_process') |
| 21 | +const fs = require('node:fs') |
| 22 | +const os = require('node:os') |
| 23 | +const path = require('node:path') |
| 24 | + |
| 25 | +const ROOT = path.resolve(__dirname, '..') |
| 26 | +const PKG = require(path.join(ROOT, 'package.json')) |
| 27 | + |
| 28 | +const REQUIRED_TARBALL_ENTRIES = [ |
| 29 | + 'package/nitrogen/generated/ios/SensitiveInfo+autolinking.rb', |
| 30 | + 'package/nitrogen/generated/android/SensitiveInfo+autolinking.gradle', |
| 31 | + 'package/hooks/package.json', |
| 32 | + 'package/errors/package.json', |
| 33 | + 'package/lib/commonjs/index.js', |
| 34 | + 'package/lib/module/index.js', |
| 35 | + 'package/lib/commonjs/hooks/index.js', |
| 36 | + 'package/lib/module/hooks/index.js', |
| 37 | + 'package/lib/commonjs/errors.js', |
| 38 | + 'package/lib/module/errors.js', |
| 39 | +] |
| 40 | + |
| 41 | +const SUBPATHS = [ |
| 42 | + 'react-native-sensitive-info', |
| 43 | + 'react-native-sensitive-info/hooks', |
| 44 | + 'react-native-sensitive-info/errors', |
| 45 | + 'react-native-sensitive-info/package.json', |
| 46 | +] |
| 47 | + |
| 48 | +const PROXY_DIRS = ['hooks', 'errors'] |
| 49 | + |
| 50 | +const run = (cmd, opts = {}) => |
| 51 | + execSync(cmd, { stdio: ['ignore', 'pipe', 'pipe'], ...opts }) |
| 52 | + .toString() |
| 53 | + .trim() |
| 54 | + |
| 55 | +const fail = (msg) => { |
| 56 | + console.error(`\n[smoke-test-release] ❌ ${msg}\n`) |
| 57 | + process.exit(1) |
| 58 | +} |
| 59 | + |
| 60 | +const log = (msg) => console.log(`[smoke-test-release] ${msg}`) |
| 61 | + |
| 62 | +// 1. Pack the tarball. |
| 63 | +log('Packing tarball with `npm pack`…') |
| 64 | +const tarballName = run('npm pack --silent', { cwd: ROOT }).split('\n').pop() |
| 65 | +const tarballPath = path.join(ROOT, tarballName) |
| 66 | + |
| 67 | +try { |
| 68 | + // 2. Verify required entries are present. |
| 69 | + const entries = run(`tar -tzf ${tarballName}`, { cwd: ROOT }).split('\n') |
| 70 | + const missing = REQUIRED_TARBALL_ENTRIES.filter((e) => !entries.includes(e)) |
| 71 | + if (missing.length > 0) { |
| 72 | + fail( |
| 73 | + `Tarball is missing required entries:\n - ${missing.join('\n - ')}\n\nRun \`npm run codegen\` and rebuild before releasing.` |
| 74 | + ) |
| 75 | + } |
| 76 | + log( |
| 77 | + `Tarball contains all ${REQUIRED_TARBALL_ENTRIES.length} required entries.` |
| 78 | + ) |
| 79 | + |
| 80 | + // 3. Install the tarball into a throwaway project. |
| 81 | + const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'rnsi-smoke-')) |
| 82 | + fs.writeFileSync( |
| 83 | + path.join(sandbox, 'package.json'), |
| 84 | + JSON.stringify({ name: 'rnsi-smoke', version: '0.0.0', private: true }) |
| 85 | + ) |
| 86 | + log(`Installing tarball into ${sandbox}…`) |
| 87 | + run(`npm install --silent --no-save ${tarballPath}`, { cwd: sandbox }) |
| 88 | + |
| 89 | + // 4. Verify Node's CJS resolver finds every documented subpath |
| 90 | + // (this exercises the `exports` map). |
| 91 | + for (const subpath of SUBPATHS) { |
| 92 | + try { |
| 93 | + run(`node -e "require.resolve('${subpath}')"`, { cwd: sandbox }) |
| 94 | + log(`exports map resolves: ${subpath}`) |
| 95 | + } catch (err) { |
| 96 | + fail(`exports map cannot resolve "${subpath}":\n${err.message}`) |
| 97 | + } |
| 98 | + } |
| 99 | + |
| 100 | + // 5. Verify the legacy main/module/react-native proxies still point at |
| 101 | + // real files. Bundlers that ignore `exports` rely on these. |
| 102 | + for (const sub of PROXY_DIRS) { |
| 103 | + const proxyPath = path.join( |
| 104 | + sandbox, |
| 105 | + 'node_modules', |
| 106 | + PKG.name, |
| 107 | + sub, |
| 108 | + 'package.json' |
| 109 | + ) |
| 110 | + const proxy = JSON.parse(fs.readFileSync(proxyPath, 'utf8')) |
| 111 | + for (const field of ['main', 'module', 'react-native']) { |
| 112 | + const target = proxy[field] |
| 113 | + if (typeof target !== 'string') { |
| 114 | + fail(`Proxy ${sub}/package.json missing string "${field}" field.`) |
| 115 | + } |
| 116 | + const resolved = path.resolve(path.dirname(proxyPath), target) |
| 117 | + if (!fs.existsSync(resolved)) { |
| 118 | + fail( |
| 119 | + `Proxy ${sub}/package.json "${field}" → ${target} does not resolve to a file (${resolved}).` |
| 120 | + ) |
| 121 | + } |
| 122 | + } |
| 123 | + log(`legacy field proxy resolves: ${sub}/`) |
| 124 | + } |
| 125 | + |
| 126 | + // 6. Validate Ruby syntax for podspec + autolinking.rb so `pod install` |
| 127 | + // won't fail with a parse error on consumer machines. |
| 128 | + const podspecPath = path.join(ROOT, 'SensitiveInfo.podspec') |
| 129 | + const autolinkingPath = path.join( |
| 130 | + sandbox, |
| 131 | + 'node_modules', |
| 132 | + PKG.name, |
| 133 | + 'nitrogen/generated/ios/SensitiveInfo+autolinking.rb' |
| 134 | + ) |
| 135 | + try { |
| 136 | + run(`ruby -c "${podspecPath}"`) |
| 137 | + run(`ruby -c "${autolinkingPath}"`) |
| 138 | + log('podspec + autolinking.rb pass `ruby -c`.') |
| 139 | + } catch (err) { |
| 140 | + fail(`Ruby syntax check failed:\n${err.stderr?.toString() ?? err.message}`) |
| 141 | + } |
| 142 | + |
| 143 | + // 7. Cleanup sandbox. |
| 144 | + fs.rmSync(sandbox, { recursive: true, force: true }) |
| 145 | + |
| 146 | + console.log('\n[smoke-test-release] ✅ Release candidate looks healthy.\n') |
| 147 | +} finally { |
| 148 | + // Always remove the local tarball — release-it will pack again at publish time. |
| 149 | + if (fs.existsSync(tarballPath)) fs.unlinkSync(tarballPath) |
| 150 | +} |
0 commit comments