Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions errors/package.json
Original file line number Diff line number Diff line change
@@ -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
}
9 changes: 9 additions & 0 deletions hooks/package.json
Original file line number Diff line number Diff line change
@@ -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
}
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 .",
Expand All @@ -71,6 +72,8 @@
"src",
"react-native.config.js",
"lib",
"hooks",
"errors",
"nitrogen",
"cpp",
"nitro.json",
Expand Down Expand Up @@ -185,8 +188,7 @@
},
"hooks": {
"before:init": [
"npm run typecheck",
"npm run build"
"npm run release:prepare"
]
},
"plugins": {
Expand Down
150 changes: 150 additions & 0 deletions scripts/smoke-test-release.js
Original file line number Diff line number Diff line change
@@ -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',
]
Comment on lines +28 to +39

const SUBPATHS = [
'react-native-sensitive-info',
'react-native-sensitive-info/hooks',
'react-native-sensitive-info/errors',
'react-native-sensitive-info/package.json',
Comment on lines +42 to +45
]

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)
}
Comment on lines +55 to +58

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))
Comment on lines +69 to +70
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)
}
52 changes: 52 additions & 0 deletions scripts/verify-release-artifacts.js
Original file line number Diff line number Diff line change
@@ -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.`
)
Loading