Skip to content

Commit 9d5c780

Browse files
chore: harden publish dry run
1 parent ee9b6bc commit 9d5c780

6 files changed

Lines changed: 159 additions & 25 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ The format follows Keep a Changelog and the project adheres to SemVer.
2020
- Split detailed usage material into focused docs for API reference, React hooks, secure storage, web backends, batch/transaction/migration workflows, recipes, MMKV migration, and benchmarks.
2121
- Expand npm package description and keywords around React Native secure storage, biometric storage, Keychain, Android Keystore, Nitro Modules, MMKV migration, Expo SecureStore, Zustand/Jotai, and IndexedDB.
2222
- Update release tooling patches for `@swc/core`, `@types/node`, and `turbo`.
23+
- Harden publish dry-runs, package docs syncing, and npm pack content validation.
2324

2425
## 0.4.5 - 2026-04-14
2526

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"scripts": {
99
"setup": "node scripts/setup.js",
1010
"publish-package": "node scripts/publish.js",
11+
"publish-package:dry": "node scripts/publish.js --dry-run",
1112
"build": "turbo run build",
1213
"lint": "turbo run lint",
1314
"format": "turbo run format",

packages/react-native-nitro-storage/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@
6060
"test:cpp": "node scripts/test-cpp.js",
6161
"check:pack": "node scripts/check-pack-contents.js",
6262
"prepublishOnly": "bun run clean && bun run codegen && bun run build && bun run test:types && bun run benchmark",
63-
"prepack": "node -e \"const fs=require('fs'); fs.copyFileSync('../../README.md','./README.md'); try{fs.copyFileSync('../../LICENSE','./LICENSE')}catch(e){} try{fs.copyFileSync('../../SECURITY.md','./SECURITY.md')}catch(e){} if(fs.existsSync('../../docs')) fs.cpSync('../../docs','./docs',{recursive:true})\"",
64-
"postpack": "node -e \"const fs=require('fs'); ['./README.md','./LICENSE','./SECURITY.md'].forEach(f=>fs.existsSync(f)&&fs.unlinkSync(f)); if(fs.existsSync('./docs')) fs.rmSync('./docs',{recursive:true,force:true})\""
63+
"prepack": "node scripts/sync-package-docs.js prepare",
64+
"postpack": "node scripts/sync-package-docs.js cleanup"
6565
},
6666
"keywords": [
6767
"react-native",

packages/react-native-nitro-storage/scripts/check-pack-contents.js

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ function fail(message) {
66
}
77

88
const requiredFiles = [
9+
"README.md",
10+
"LICENSE",
11+
"SECURITY.md",
12+
"app.plugin.js",
13+
"docs/api-reference.md",
14+
"docs/secure-storage.md",
15+
"docs/mmkv-migration.md",
16+
"nitro.json",
17+
"nitrogen/generated/shared/c++/HybridStorageSpec.hpp",
918
"src/index.ts",
1019
"src/index.web.ts",
1120
"lib/commonjs/index.js",
@@ -16,6 +25,9 @@ const requiredFiles = [
1625
const forbiddenPatterns = [
1726
/^src\/__tests__\//,
1827
/^scripts\//,
28+
/^cpp\/build\//,
29+
/^android\/build\//,
30+
/^android\/\.cxx\//,
1931
/(?:^|\/)[^/]*Test\.cpp$/,
2032
];
2133

@@ -39,9 +51,15 @@ if (!packMetadata || !Array.isArray(packMetadata.files)) {
3951
fail("npm pack metadata does not contain a files list.");
4052
}
4153

54+
if (packMetadata.name !== "react-native-nitro-storage") {
55+
fail(`Unexpected package name in npm pack output: ${packMetadata.name}`);
56+
}
57+
4258
const packagedFiles = new Set(packMetadata.files.map((file) => file.path));
4359

44-
const missingRequiredFiles = requiredFiles.filter((file) => !packagedFiles.has(file));
60+
const missingRequiredFiles = requiredFiles.filter(
61+
(file) => !packagedFiles.has(file),
62+
);
4563
if (missingRequiredFiles.length > 0) {
4664
fail(`Missing required packed files: ${missingRequiredFiles.join(", ")}`);
4765
}
@@ -50,7 +68,11 @@ const forbiddenFiles = Array.from(packagedFiles).filter((file) =>
5068
forbiddenPatterns.some((pattern) => pattern.test(file)),
5169
);
5270
if (forbiddenFiles.length > 0) {
53-
fail(`Forbidden files were included in npm pack output: ${forbiddenFiles.join(", ")}`);
71+
fail(
72+
`Forbidden files were included in npm pack output: ${forbiddenFiles.join(", ")}`,
73+
);
5474
}
5575

56-
console.log("✅ npm pack content guard passed.");
76+
console.log(
77+
`✅ npm pack content guard passed (${packMetadata.files.length} files checked).`,
78+
);
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
const fs = require("fs");
2+
const path = require("path");
3+
4+
const packageRoot = path.resolve(__dirname, "..");
5+
const repoRoot = path.resolve(packageRoot, "../..");
6+
7+
const entries = [
8+
{ source: "README.md", target: "README.md", type: "file" },
9+
{ source: "LICENSE", target: "LICENSE", type: "file" },
10+
{ source: "SECURITY.md", target: "SECURITY.md", type: "file" },
11+
{ source: "docs", target: "docs", type: "directory" },
12+
];
13+
14+
function removeTarget(target) {
15+
const targetPath = path.join(packageRoot, target);
16+
if (!fs.existsSync(targetPath)) {
17+
return;
18+
}
19+
20+
fs.rmSync(targetPath, { recursive: true, force: true });
21+
}
22+
23+
function copyEntry(entry) {
24+
const sourcePath = path.join(repoRoot, entry.source);
25+
const targetPath = path.join(packageRoot, entry.target);
26+
27+
if (!fs.existsSync(sourcePath)) {
28+
throw new Error(
29+
`Required package artifact source is missing: ${entry.source}`,
30+
);
31+
}
32+
33+
removeTarget(entry.target);
34+
35+
if (entry.type === "directory") {
36+
fs.cpSync(sourcePath, targetPath, { recursive: true });
37+
return;
38+
}
39+
40+
fs.copyFileSync(sourcePath, targetPath);
41+
}
42+
43+
function prepare() {
44+
entries.forEach(copyEntry);
45+
}
46+
47+
function cleanup() {
48+
entries.forEach((entry) => removeTarget(entry.target));
49+
}
50+
51+
const mode = process.argv[2];
52+
53+
try {
54+
if (mode === "prepare") {
55+
prepare();
56+
} else if (mode === "cleanup") {
57+
cleanup();
58+
} else {
59+
throw new Error(
60+
"Usage: node scripts/sync-package-docs.js <prepare|cleanup>",
61+
);
62+
}
63+
} catch (error) {
64+
console.error(error instanceof Error ? error.message : String(error));
65+
process.exit(1);
66+
}

scripts/publish.js

Lines changed: 64 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@ const colors = {
1616
const projectRoot = path.resolve(__dirname, "..");
1717
const packageDir = path.join(
1818
projectRoot,
19-
"packages/react-native-nitro-storage"
19+
"packages/react-native-nitro-storage",
2020
);
2121
const packageJsonPath = path.join(packageDir, "package.json");
2222
const packageFilter = "react-native-nitro-storage";
23-
const isCI =
24-
process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
23+
const packageDocsSyncScript = path.join(
24+
packageDir,
25+
"scripts/sync-package-docs.js",
26+
);
27+
const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
2528

2629
function log(message, color = "green") {
2730
console.log(colors[color](message));
@@ -54,6 +57,25 @@ function execCommandWithOutput(command, options = {}) {
5457
}
5558
}
5659

60+
function shellQuote(value) {
61+
return JSON.stringify(String(value));
62+
}
63+
64+
function validateNpmTag(tag) {
65+
if (!/^[a-z0-9][a-z0-9._-]*$/i.test(tag)) {
66+
log(`Invalid npm dist tag: ${tag}`, "red");
67+
process.exit(1);
68+
}
69+
70+
if (/^v?\d+\.\d+\.\d+/.test(tag)) {
71+
log(
72+
`Invalid npm dist tag "${tag}": use a release channel like latest or next.`,
73+
"red",
74+
);
75+
process.exit(1);
76+
}
77+
}
78+
5779
function isInteractive() {
5880
return process.stdin.isTTY && process.stdout.isTTY && !isCI;
5981
}
@@ -102,6 +124,16 @@ function checkNpmAuth() {
102124
return whoami !== null && whoami !== "";
103125
}
104126

127+
function cleanupPackageDocs() {
128+
if (!fs.existsSync(packageDocsSyncScript)) {
129+
return;
130+
}
131+
132+
execCommand(`node ${shellQuote(packageDocsSyncScript)} cleanup`, {
133+
cwd: packageDir,
134+
});
135+
}
136+
105137
function formatGitStatus(statusLines) {
106138
const preview = statusLines.slice(0, 10).join("\n");
107139
const remainder =
@@ -177,12 +209,15 @@ async function main() {
177209
const skipPackPreview = args.includes("--skip-pack-preview");
178210
const tag =
179211
args.find((arg) => arg.startsWith("--tag="))?.split("=")[1] || "latest";
212+
validateNpmTag(tag);
180213

181214
console.log("");
182215
log("📦 Publishing react-native-nitro-storage", "bold");
183216
console.log("");
184217

185218
const version = getPackageVersion();
219+
cleanupPackageDocs();
220+
186221
log(`Version: ${version}`, "cyan");
187222
log(`Tag: ${tag}`, "cyan");
188223
if (isDryRun) {
@@ -220,35 +255,33 @@ async function main() {
220255
console.log("");
221256
}
222257

223-
runCheck(
224-
"🧹 Running lint...",
225-
`bun run lint -- --filter=${packageFilter}`,
226-
{ cwd: projectRoot }
227-
);
258+
runCheck("🧹 Running lint...", `bun run lint -- --filter=${packageFilter}`, {
259+
cwd: projectRoot,
260+
});
228261
runCheck(
229262
"🎨 Running format check...",
230263
`bun run format:check -- --filter=${packageFilter}`,
231-
{ cwd: projectRoot }
264+
{ cwd: projectRoot },
232265
);
233266
runCheck(
234267
"📝 Running typecheck...",
235268
`bun run typecheck -- --filter=${packageFilter}`,
236-
{ cwd: projectRoot }
269+
{ cwd: projectRoot },
237270
);
238271
runCheck(
239272
"🔎 Running type-surface checks...",
240273
`bun run test:types -- --filter=${packageFilter}`,
241-
{ cwd: projectRoot }
274+
{ cwd: projectRoot },
242275
);
243276
runCheck(
244277
"🧪 Running unit tests...",
245278
`bun run test -- --filter=${packageFilter}`,
246-
{ cwd: projectRoot }
279+
{ cwd: projectRoot },
247280
);
248281
runCheck(
249282
"🧪 Running C++ tests...",
250283
`bun run test:cpp -- --filter=${packageFilter}`,
251-
{ cwd: projectRoot }
284+
{ cwd: projectRoot },
252285
);
253286
runCheck(
254287
"🏗️ Preparing package artifacts...",
@@ -261,7 +294,7 @@ async function main() {
261294
"bun run test:cpp",
262295
"bun run check:pack",
263296
].join(" && "),
264-
{ cwd: packageDir }
297+
{ cwd: packageDir },
265298
);
266299

267300
if (!skipPackPreview) {
@@ -275,10 +308,10 @@ async function main() {
275308
if (packSummary) {
276309
const packageSize = packSummary.packageSize ?? packSummary.size;
277310
console.log(
278-
` • tarball: ${packSummary.filename} (${formatBytes(packageSize)})`
311+
` • tarball: ${packSummary.filename} (${formatBytes(packageSize)})`,
279312
);
280313
console.log(
281-
` • unpacked: ${formatBytes(packSummary.unpackedSize)}, files: ${packSummary.files?.length ?? "?"}`
314+
` • unpacked: ${formatBytes(packSummary.unpackedSize)}, files: ${packSummary.files?.length ?? "?"}`,
282315
);
283316
}
284317
console.log("");
@@ -295,22 +328,33 @@ async function main() {
295328
}
296329

297330
if (isDryRun) {
298-
log("🏃 Dry run complete! Package is ready to publish.", "green");
331+
log("🏃 Running npm publish dry-run...", "cyan");
332+
const dryPublishCommand = `npm publish --dry-run --tag ${shellQuote(tag)} --access public`;
333+
if (!execCommand(dryPublishCommand, { cwd: packageDir })) {
334+
log("✗ npm publish dry-run failed", "red");
335+
cleanupPackageDocs();
336+
process.exit(1);
337+
}
338+
cleanupPackageDocs();
339+
console.log("");
340+
log("✅ Dry run complete. Package is ready to publish.", "green");
299341
log(
300342
`Run without --dry-run${yes ? "" : " --yes"} to publish version ${version}`,
301-
"cyan"
343+
"cyan",
302344
);
303345
} else {
304346
log("🚀 Publishing to npm...", "cyan");
305-
const publishCommand = `npm publish --tag ${tag} --access public${isCI ? " --provenance" : ""}`;
347+
const publishCommand = `npm publish --tag ${shellQuote(tag)} --access public${isCI ? " --provenance" : ""}`;
306348
if (!execCommand(publishCommand, { cwd: packageDir })) {
307349
log("✗ Publish failed", "red");
350+
cleanupPackageDocs();
308351
process.exit(1);
309352
}
353+
cleanupPackageDocs();
310354
console.log("");
311355
log(
312356
`✅ Successfully published react-native-nitro-storage@${version}`,
313-
"green"
357+
"green",
314358
);
315359
log(` https://www.npmjs.com/package/react-native-nitro-storage`, "cyan");
316360
}

0 commit comments

Comments
 (0)