Skip to content

Commit 8c2ca9e

Browse files
committed
Update release:npm for better comparisons
1 parent 1642534 commit 8c2ca9e

File tree

3 files changed

+150
-52
lines changed

3 files changed

+150
-52
lines changed

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"eta": "3.5.0",
2020
"fast-glob": "3.3.3",
2121
"fs-extra": "11.3.1",
22+
"minimatch": "9.0.5",
2223
"npm-package-arg": "13.0.0",
2324
"out-url": "1.2.2",
2425
"semver": "7.7.2",

scripts/release-npm-packages.js

Lines changed: 146 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const crypto = require('node:crypto')
44
const fs = require('node:fs/promises')
55
const path = require('node:path')
66

7+
const { minimatch } = require('minimatch')
78
const semver = require('semver')
89

910
const constants = require('@socketregistry/scripts/constants')
@@ -17,7 +18,10 @@ const {
1718
const { readPackageJsonSync } = require('@socketsecurity/registry/lib/packages')
1819
const { readFileUtf8 } = require('@socketsecurity/registry/lib/fs')
1920
const { pEach } = require('@socketsecurity/registry/lib/promises')
20-
const { toSortedObject } = require('@socketsecurity/registry/lib/objects')
21+
const {
22+
isObjectObject,
23+
toSortedObject
24+
} = require('@socketsecurity/registry/lib/objects')
2125

2226
const {
2327
LATEST,
@@ -38,10 +42,95 @@ const registryPkg = packageData({
3842

3943
const EXTRACT_PACKAGE_TMP_PREFIX = 'release-npm-'
4044

41-
async function getPackageFileHashes(spec) {
45+
async function getLocalPackageFileHashes(packagePath) {
4246
const fileHashes = {}
4347

44-
// Extract package to a temp directory and compute hashes.
48+
// Read package.json to get files field.
49+
const pkgJsonPath = path.join(packagePath, PACKAGE_JSON)
50+
const pkgJsonContent = await readFileUtf8(pkgJsonPath)
51+
const pkgJson = JSON.parse(pkgJsonContent)
52+
const filesPatterns = pkgJson.files || []
53+
54+
// Always include package.json.
55+
const pkgJsonRelPath = PACKAGE_JSON
56+
const exportsValue = pkgJson.exports
57+
const relevantData = {
58+
dependencies: toSortedObject(pkgJson.dependencies ?? {}),
59+
exports: isObjectObject(exportsValue)
60+
? toSortedObject(exportsValue)
61+
: (exportsValue ?? null),
62+
files: pkgJson.files ?? null,
63+
sideEffects: pkgJson.sideEffects ?? null,
64+
engines: pkgJson.engines ?? null
65+
}
66+
const pkgJsonHash = crypto
67+
.createHash('sha256')
68+
.update(JSON.stringify(relevantData), 'utf8')
69+
.digest('hex')
70+
fileHashes[pkgJsonRelPath] = pkgJsonHash
71+
72+
// Walk and hash files.
73+
async function walkDir(dir, baseDir = packagePath) {
74+
const entries = await fs.readdir(dir, { withFileTypes: true })
75+
for (const entry of entries) {
76+
const fullPath = path.join(dir, entry.name)
77+
const relativePath = path.relative(baseDir, fullPath)
78+
79+
if (entry.isDirectory()) {
80+
// Always recurse for patterns with ** or when we're at root level and have patterns.
81+
const shouldRecurse =
82+
relativePath === '' ||
83+
filesPatterns.some(pattern => {
84+
return (
85+
pattern.includes('**') || pattern.startsWith(relativePath + '/')
86+
)
87+
})
88+
89+
if (shouldRecurse) {
90+
// eslint-disable-next-line no-await-in-loop
91+
await walkDir(fullPath, baseDir)
92+
}
93+
} else if (entry.isFile() && entry.name !== PACKAGE_JSON) {
94+
// Check if file is npm auto-included (LICENSE/README with any case/extension in root).
95+
const isRootAutoIncluded =
96+
relativePath === entry.name && isNpmAutoIncluded(entry.name)
97+
98+
// Check if file matches any of the patterns.
99+
const matchesPattern = filesPatterns.some(pattern => {
100+
// Handle patterns like **/LICENSE{.original,}
101+
if (pattern.includes('**')) {
102+
const fileName = path.basename(relativePath)
103+
const filePattern = pattern.replace('**/', '')
104+
return (
105+
minimatch(fileName, filePattern) ||
106+
minimatch(relativePath, pattern)
107+
)
108+
}
109+
return minimatch(relativePath, pattern)
110+
})
111+
112+
if (isRootAutoIncluded || matchesPattern) {
113+
// eslint-disable-next-line no-await-in-loop
114+
const content = await readFileUtf8(fullPath)
115+
const hash = crypto
116+
.createHash('sha256')
117+
.update(content, 'utf8')
118+
.digest('hex')
119+
fileHashes[relativePath] = hash
120+
}
121+
}
122+
}
123+
}
124+
125+
await walkDir(packagePath)
126+
127+
return toSortedObject(fileHashes)
128+
}
129+
130+
async function getRemotePackageFileHashes(spec) {
131+
const fileHashes = {}
132+
133+
// Extract remote package and hash files.
45134
await extractPackage(
46135
spec,
47136
{
@@ -59,15 +148,36 @@ async function getPackageFileHashes(spec) {
59148
// Recurse into subdirectories.
60149
// eslint-disable-next-line no-await-in-loop
61150
await walkDir(fullPath, baseDir)
62-
} else if (entry.isFile() && entry.name !== PACKAGE_JSON) {
63-
// Skip package.json files as they contain version info.
151+
} else if (entry.isFile()) {
64152
// eslint-disable-next-line no-await-in-loop
65153
const content = await readFileUtf8(fullPath)
66-
const hash = crypto
67-
.createHash('sha256')
68-
.update(content, 'utf8')
69-
.digest('hex')
70-
fileHashes[relativePath] = hash
154+
155+
if (entry.name === PACKAGE_JSON) {
156+
// For package.json, hash only relevant fields (not version).
157+
const pkgJson = JSON.parse(content)
158+
const exportsValue = pkgJson.exports
159+
const relevantData = {
160+
dependencies: toSortedObject(pkgJson.dependencies ?? {}),
161+
exports: isObjectObject(exportsValue)
162+
? toSortedObject(exportsValue)
163+
: (exportsValue ?? null),
164+
files: pkgJson.files ?? null,
165+
sideEffects: pkgJson.sideEffects ?? null,
166+
engines: pkgJson.engines ?? null
167+
}
168+
const hash = crypto
169+
.createHash('sha256')
170+
.update(JSON.stringify(relevantData), 'utf8')
171+
.digest('hex')
172+
fileHashes[relativePath] = hash
173+
} else {
174+
// For other files, hash the entire content.
175+
const hash = crypto
176+
.createHash('sha256')
177+
.update(content, 'utf8')
178+
.digest('hex')
179+
fileHashes[relativePath] = hash
180+
}
71181
}
72182
}
73183
}
@@ -80,6 +190,8 @@ async function getPackageFileHashes(spec) {
80190
}
81191

82192
async function hasPackageChanged(pkg, manifest_) {
193+
const { spinner } = constants
194+
83195
const manifest =
84196
manifest_ ?? (await fetchPackageManifest(`${pkg.name}@${pkg.tag}`))
85197

@@ -89,63 +201,44 @@ async function hasPackageChanged(pkg, manifest_) {
89201
)
90202
}
91203

92-
// First check if package.json version or dependencies have changed.
93-
const localPkgJson = readPackageJsonSync(pkg.path)
94-
95-
// Check if dependencies have changed.
96-
const localDeps = toSortedObject(localPkgJson.dependencies ?? {})
97-
const remoteDeps = toSortedObject(manifest.dependencies ?? {})
98-
99-
const localDepsStr = JSON.stringify(localDeps)
100-
const remoteDepsStr = JSON.stringify(remoteDeps)
101-
102-
// If dependencies changed, we need to bump.
103-
if (localDepsStr !== remoteDepsStr) {
104-
return true
105-
}
106-
107-
// Check if other important fields have changed.
108-
const fieldsToCheck = ['exports', 'files', 'sideEffects', 'engines']
109-
for (const field of fieldsToCheck) {
110-
const localValue = JSON.stringify(localPkgJson[field] ?? null)
111-
const remoteValue = JSON.stringify(manifest[field] ?? null)
112-
if (localValue !== remoteValue) {
113-
return true
114-
}
115-
}
116-
117204
// Compare actual file contents by extracting packages and comparing SHA hashes.
118205
try {
119206
const { 0: remoteHashes, 1: localHashes } = await Promise.all([
120-
getPackageFileHashes(`${pkg.name}@${manifest.version}`),
121-
getPackageFileHashes(pkg.path, true)
207+
getRemotePackageFileHashes(`${pkg.name}@${manifest.version}`),
208+
getLocalPackageFileHashes(pkg.path)
122209
])
123210

124-
// Compare the file hashes.
125-
const remoteFiles = Object.keys(remoteHashes)
126-
const localFiles = Object.keys(localHashes)
127-
128-
// Check if file lists are different.
129-
if (JSON.stringify(remoteFiles) !== JSON.stringify(localFiles)) {
130-
return true
131-
}
132-
133-
// Check if any file content is different.
134-
for (const file of remoteFiles) {
135-
if (remoteHashes[file] !== localHashes[file]) {
211+
// Use remote files as source of truth and check if local matches.
212+
for (const [file, remoteHash] of Object.entries(remoteHashes)) {
213+
const localHash = localHashes[file]
214+
if (!localHash) {
215+
// File exists in remote but not locally - this is a real difference.
216+
spinner?.warn(
217+
`${pkg.name}: File '${file}' exists in published package but not locally`
218+
)
219+
return true
220+
}
221+
if (remoteHash !== localHash) {
222+
spinner?.info(`${pkg.name}: File '${file}' content differs`)
136223
return true
137224
}
138225
}
139226

140227
return false
141228
} catch (e) {
142229
// If comparison fails, be conservative and assume changes.
143-
console.error(`Error comparing packages for ${pkg.name}:`, e?.message)
230+
spinner?.fail(`${pkg.name}: ${e?.message}`)
144231
return true
145232
}
146233
}
147234

148-
async function maybeBumpPackage(pkg, options = {}) {
235+
function isNpmAutoIncluded(fileName) {
236+
const upperName = fileName.toUpperCase()
237+
// NPM automatically includes LICENSE and README files with any case and extension.
238+
return upperName.startsWith('LICENSE') || upperName.startsWith('README')
239+
}
240+
241+
async function maybeBumpPackage(pkg, options) {
149242
const {
150243
spinner,
151244
state = {
@@ -169,7 +262,8 @@ async function maybeBumpPackage(pkg, options = {}) {
169262
// Compare the shasum of the @socketregistry the latest package from
170263
// registry.npmjs.org against the local version. If they are different
171264
// then bump the local version.
172-
if (await hasPackageChanged(pkg, manifest)) {
265+
const hasChanged = await hasPackageChanged(pkg, manifest)
266+
if (hasChanged) {
173267
let version = semver.inc(manifest.version, 'patch')
174268
if (pkg.tag !== LATEST) {
175269
version = `${semver.inc(version, 'patch')}-${pkg.tag}`

0 commit comments

Comments
 (0)