From fc4de06ffef78572532bc2e0477fca9cd02c778d Mon Sep 17 00:00:00 2001 From: Mateus Andrade Date: Wed, 29 Apr 2026 10:19:27 -0300 Subject: [PATCH] Add release verification scripts and proxy shims Add pre-release verification and smoke-test tooling plus compatibility proxy package.json files. New scripts: `scripts/verify-release-artifacts.js` checks that generated native bindings and compiled JS artifacts exist before publishing; `scripts/smoke-test-release.js` packs the tarball into a temporary project and verifies tarball entries, exports subpath resolution, legacy main/module/react-native proxy targets, and Ruby syntax for podspec/autolinking. Add lightweight proxy package.json shims under `hooks/` and `errors/` so bundlers that ignore `exports` can still resolve subpaths. Update root package.json to include `hooks` and `errors` in the published files, add a `release:prepare` script that runs codegen and the new verification steps, and replace the release hook commands to run `release:prepare` before publishing. These changes catch missing build or packaging issues early in the release flow. --- errors/package.json | 9 ++ hooks/package.json | 9 ++ package.json | 6 +- scripts/smoke-test-release.js | 150 ++++++++++++++++++++++++++++ scripts/verify-release-artifacts.js | 52 ++++++++++ 5 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 errors/package.json create mode 100644 hooks/package.json create mode 100644 scripts/smoke-test-release.js create mode 100644 scripts/verify-release-artifacts.js diff --git a/errors/package.json b/errors/package.json new file mode 100644 index 00000000..5c6ce71d --- /dev/null +++ b/errors/package.json @@ -0,0 +1,9 @@ +{ + "//": "Compatibility shim for bundlers that don't honor the package `exports` field (e.g. older Re.Pack/rspack/Metro setups). The canonical resolution path is the `./errors` entry in the root package.json `exports` map.", + "name": "react-native-sensitive-info/errors", + "types": "../lib/typescript/commonjs/src/errors.d.ts", + "main": "../lib/commonjs/errors.js", + "module": "../lib/module/errors.js", + "react-native": "../lib/module/errors.js", + "sideEffects": false +} diff --git a/hooks/package.json b/hooks/package.json new file mode 100644 index 00000000..bf5e49ba --- /dev/null +++ b/hooks/package.json @@ -0,0 +1,9 @@ +{ + "//": "Compatibility shim for bundlers that don't honor the package `exports` field (e.g. older Re.Pack/rspack/Metro setups). The canonical resolution path is the `./hooks` entry in the root package.json `exports` map.", + "name": "react-native-sensitive-info/hooks", + "types": "../lib/typescript/commonjs/src/hooks/index.d.ts", + "main": "../lib/commonjs/hooks/index.js", + "module": "../lib/module/hooks/index.js", + "react-native": "../lib/module/hooks/index.js", + "sideEffects": false +} diff --git a/package.json b/package.json index 2b2c73a0..45a0834c 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "typecheck": "tsc --noEmit", "clean": "git clean -dfX", "release": "release-it", + "release:prepare": "npm run codegen && node scripts/verify-release-artifacts.js && node scripts/smoke-test-release.js", "build": "npm run typecheck && bob build", "codegen": "nitrogen --logLevel=\"debug\" && npm run build && node post-script.js", "lint": "biome check --write .", @@ -71,6 +72,8 @@ "src", "react-native.config.js", "lib", + "hooks", + "errors", "nitrogen", "cpp", "nitro.json", @@ -185,8 +188,7 @@ }, "hooks": { "before:init": [ - "npm run typecheck", - "npm run build" + "npm run release:prepare" ] }, "plugins": { diff --git a/scripts/smoke-test-release.js b/scripts/smoke-test-release.js new file mode 100644 index 00000000..8bffa822 --- /dev/null +++ b/scripts/smoke-test-release.js @@ -0,0 +1,150 @@ +#!/usr/bin/env node +/** + * End-to-end pre-release smoke test. + * + * Packs the package with `npm pack`, installs the tarball into a throwaway + * project, and verifies the consumer-facing surface: + * 1. The published tarball contains nitrogen native bindings + JS subpath + * proxy `package.json` files (catches "missing autolinking.rb" type bugs). + * 2. Node's CJS resolver (which honors the package `exports` map) can + * resolve every documented entry point. + * 3. The same subpaths also resolve via the legacy `main`/`module`/ + * `react-native` fields in the proxy directories — this is what bundlers + * that ignore `exports` (older Re.Pack/rspack/Metro setups) rely on. + * 4. The podspec and the generated iOS autolinking.rb have valid Ruby + * syntax, so `pod install` will not blow up at parse time. + * + * Designed to run inside `release-it`'s `before:init` hook so a broken + * release is caught before the npm publish + git push. + */ +const { execSync } = require('node:child_process') +const fs = require('node:fs') +const os = require('node:os') +const path = require('node:path') + +const ROOT = path.resolve(__dirname, '..') +const PKG = require(path.join(ROOT, 'package.json')) + +const REQUIRED_TARBALL_ENTRIES = [ + 'package/nitrogen/generated/ios/SensitiveInfo+autolinking.rb', + 'package/nitrogen/generated/android/SensitiveInfo+autolinking.gradle', + 'package/hooks/package.json', + 'package/errors/package.json', + 'package/lib/commonjs/index.js', + 'package/lib/module/index.js', + 'package/lib/commonjs/hooks/index.js', + 'package/lib/module/hooks/index.js', + 'package/lib/commonjs/errors.js', + 'package/lib/module/errors.js', +] + +const SUBPATHS = [ + 'react-native-sensitive-info', + 'react-native-sensitive-info/hooks', + 'react-native-sensitive-info/errors', + 'react-native-sensitive-info/package.json', +] + +const PROXY_DIRS = ['hooks', 'errors'] + +const run = (cmd, opts = {}) => + execSync(cmd, { stdio: ['ignore', 'pipe', 'pipe'], ...opts }) + .toString() + .trim() + +const fail = (msg) => { + console.error(`\n[smoke-test-release] ❌ ${msg}\n`) + process.exit(1) +} + +const log = (msg) => console.log(`[smoke-test-release] ${msg}`) + +// 1. Pack the tarball. +log('Packing tarball with `npm pack`…') +const tarballName = run('npm pack --silent', { cwd: ROOT }).split('\n').pop() +const tarballPath = path.join(ROOT, tarballName) + +try { + // 2. Verify required entries are present. + const entries = run(`tar -tzf ${tarballName}`, { cwd: ROOT }).split('\n') + const missing = REQUIRED_TARBALL_ENTRIES.filter((e) => !entries.includes(e)) + if (missing.length > 0) { + fail( + `Tarball is missing required entries:\n - ${missing.join('\n - ')}\n\nRun \`npm run codegen\` and rebuild before releasing.` + ) + } + log( + `Tarball contains all ${REQUIRED_TARBALL_ENTRIES.length} required entries.` + ) + + // 3. Install the tarball into a throwaway project. + const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'rnsi-smoke-')) + fs.writeFileSync( + path.join(sandbox, 'package.json'), + JSON.stringify({ name: 'rnsi-smoke', version: '0.0.0', private: true }) + ) + log(`Installing tarball into ${sandbox}…`) + run(`npm install --silent --no-save ${tarballPath}`, { cwd: sandbox }) + + // 4. Verify Node's CJS resolver finds every documented subpath + // (this exercises the `exports` map). + for (const subpath of SUBPATHS) { + try { + run(`node -e "require.resolve('${subpath}')"`, { cwd: sandbox }) + log(`exports map resolves: ${subpath}`) + } catch (err) { + fail(`exports map cannot resolve "${subpath}":\n${err.message}`) + } + } + + // 5. Verify the legacy main/module/react-native proxies still point at + // real files. Bundlers that ignore `exports` rely on these. + for (const sub of PROXY_DIRS) { + const proxyPath = path.join( + sandbox, + 'node_modules', + PKG.name, + sub, + 'package.json' + ) + const proxy = JSON.parse(fs.readFileSync(proxyPath, 'utf8')) + for (const field of ['main', 'module', 'react-native']) { + const target = proxy[field] + if (typeof target !== 'string') { + fail(`Proxy ${sub}/package.json missing string "${field}" field.`) + } + const resolved = path.resolve(path.dirname(proxyPath), target) + if (!fs.existsSync(resolved)) { + fail( + `Proxy ${sub}/package.json "${field}" → ${target} does not resolve to a file (${resolved}).` + ) + } + } + log(`legacy field proxy resolves: ${sub}/`) + } + + // 6. Validate Ruby syntax for podspec + autolinking.rb so `pod install` + // won't fail with a parse error on consumer machines. + const podspecPath = path.join(ROOT, 'SensitiveInfo.podspec') + const autolinkingPath = path.join( + sandbox, + 'node_modules', + PKG.name, + 'nitrogen/generated/ios/SensitiveInfo+autolinking.rb' + ) + try { + run(`ruby -c "${podspecPath}"`) + run(`ruby -c "${autolinkingPath}"`) + log('podspec + autolinking.rb pass `ruby -c`.') + } catch (err) { + fail(`Ruby syntax check failed:\n${err.stderr?.toString() ?? err.message}`) + } + + // 7. Cleanup sandbox. + fs.rmSync(sandbox, { recursive: true, force: true }) + + console.log('\n[smoke-test-release] ✅ Release candidate looks healthy.\n') +} finally { + // Always remove the local tarball — release-it will pack again at publish time. + if (fs.existsSync(tarballPath)) fs.unlinkSync(tarballPath) +} diff --git a/scripts/verify-release-artifacts.js b/scripts/verify-release-artifacts.js new file mode 100644 index 00000000..70635845 --- /dev/null +++ b/scripts/verify-release-artifacts.js @@ -0,0 +1,52 @@ +#!/usr/bin/env node +/** + * Verifies that all release artifacts exist before publishing. + * + * Run as part of `release-it`'s `before:init` hook (after `npm run codegen`) + * to guarantee that the published tarball contains the native bindings and + * compiled JS. If any artifact is missing the release is aborted with a + * non-zero exit code. + */ +const fs = require('node:fs') +const path = require('node:path') + +const ROOT = path.resolve(__dirname, '..') + +// Paths are relative to the repo root. +const REQUIRED_ARTIFACTS = [ + // Nitro-generated native bindings (regenerated by `npm run codegen`). + 'nitrogen/generated/ios/SensitiveInfo+autolinking.rb', + 'nitrogen/generated/android/SensitiveInfo+autolinking.gradle', + 'nitrogen/generated/shared/c++/HybridSensitiveInfoSpec.hpp', + + // Compiled JS entry points consumed via the package `exports` map. + 'lib/commonjs/index.js', + 'lib/module/index.js', + 'lib/commonjs/hooks/index.js', + 'lib/module/hooks/index.js', + 'lib/commonjs/errors.js', + 'lib/module/errors.js', + + // Subpath proxy `package.json` files for bundlers without `exports` support. + 'hooks/package.json', + 'errors/package.json', +] + +const missing = REQUIRED_ARTIFACTS.filter( + (rel) => !fs.existsSync(path.join(ROOT, rel)) +) + +if (missing.length > 0) { + console.error('\n[verify-release-artifacts] Missing required artifacts:') + for (const rel of missing) { + console.error(` - ${rel}`) + } + console.error( + '\nRun `npm run codegen` and ensure the build succeeded before releasing.\n' + ) + process.exit(1) +} + +console.log( + `[verify-release-artifacts] OK — ${REQUIRED_ARTIFACTS.length} artifacts present.` +)