11'use strict'
22
3+ const crypto = require ( 'node:crypto' )
4+ const fs = require ( 'node:fs/promises' )
35const path = require ( 'node:path' )
46
57const semver = require ( 'semver' )
6- const ssri = require ( 'ssri' )
78
89const constants = require ( '@socketregistry/scripts/constants' )
910const { execScript } = require ( '@socketsecurity/registry/lib/agent' )
1011const {
12+ extractPackage,
1113 fetchPackageManifest,
1214 getReleaseTag,
13- packPackage,
1415 readPackageJson
1516} = require ( '@socketsecurity/registry/lib/packages' )
1617const { readPackageJsonSync } = require ( '@socketsecurity/registry/lib/packages' )
18+ const { readFileUtf8 } = require ( '@socketsecurity/registry/lib/fs' )
1719const { pEach } = require ( '@socketsecurity/registry/lib/promises' )
20+ const { toSortedObject } = require ( '@socketsecurity/registry/lib/objects' )
1821
1922const {
2023 LATEST ,
24+ PACKAGE_JSON ,
2125 SOCKET_REGISTRY_PACKAGE_NAME ,
2226 SOCKET_REGISTRY_SCOPE ,
2327 abortSignal,
@@ -32,6 +36,49 @@ const registryPkg = packageData({
3236 path : registryPkgPath
3337} )
3438
39+ const EXTRACT_PACKAGE_TMP_PREFIX = 'release-npm-'
40+
41+ async function getPackageFileHashes ( spec ) {
42+ const fileHashes = { }
43+
44+ // Extract package to a temp directory and compute hashes.
45+ await extractPackage (
46+ spec ,
47+ {
48+ tmpPrefix : EXTRACT_PACKAGE_TMP_PREFIX
49+ } ,
50+ async tmpDir => {
51+ // Walk the directory and compute hashes for all files.
52+ async function walkDir ( dir , baseDir = tmpDir ) {
53+ const entries = await fs . readdir ( dir , { withFileTypes : true } )
54+ for ( const entry of entries ) {
55+ const fullPath = path . join ( dir , entry . name )
56+ const relativePath = path . relative ( baseDir , fullPath )
57+
58+ if ( entry . isDirectory ( ) ) {
59+ // Recurse into subdirectories.
60+ // eslint-disable-next-line no-await-in-loop
61+ await walkDir ( fullPath , baseDir )
62+ } else if ( entry . isFile ( ) && entry . name !== PACKAGE_JSON ) {
63+ // Skip package.json files as they contain version info.
64+ // eslint-disable-next-line no-await-in-loop
65+ const content = await readFileUtf8 ( fullPath )
66+ const hash = crypto
67+ . createHash ( 'sha256' )
68+ . update ( content , 'utf8' )
69+ . digest ( 'hex' )
70+ fileHashes [ relativePath ] = hash
71+ }
72+ }
73+ }
74+
75+ await walkDir ( tmpDir )
76+ }
77+ )
78+
79+ return toSortedObject ( fileHashes )
80+ }
81+
3582async function hasPackageChanged ( pkg , manifest_ ) {
3683 const manifest =
3784 manifest_ ?? ( await fetchPackageManifest ( `${ pkg . name } @${ pkg . tag } ` ) )
@@ -46,22 +93,11 @@ async function hasPackageChanged(pkg, manifest_) {
4693 const localPkgJson = readPackageJsonSync ( pkg . path )
4794
4895 // Check if dependencies have changed.
49- const localDeps = localPkgJson . dependencies ?? { }
50- const remoteDeps = manifest . dependencies ?? { }
51-
52- // Sort keys for consistent comparison.
53- const sortedLocalDeps = Object . keys ( localDeps ) . sort ( ) . reduce ( ( acc , key ) => {
54- acc [ key ] = localDeps [ key ]
55- return acc
56- } , { } )
96+ const localDeps = toSortedObject ( localPkgJson . dependencies ?? { } )
97+ const remoteDeps = toSortedObject ( manifest . dependencies ?? { } )
5798
58- const sortedRemoteDeps = Object . keys ( remoteDeps ) . sort ( ) . reduce ( ( acc , key ) => {
59- acc [ key ] = remoteDeps [ key ]
60- return acc
61- } , { } )
62-
63- const localDepsStr = JSON . stringify ( sortedLocalDeps )
64- const remoteDepsStr = JSON . stringify ( sortedRemoteDeps )
99+ const localDepsStr = JSON . stringify ( localDeps )
100+ const remoteDepsStr = JSON . stringify ( remoteDeps )
65101
66102 // If dependencies changed, we need to bump.
67103 if ( localDepsStr !== remoteDepsStr ) {
@@ -78,10 +114,35 @@ async function hasPackageChanged(pkg, manifest_) {
78114 }
79115 }
80116
81- // Skip tarball comparison entirely - it's too prone to false positives.
82- // If dependencies and key fields haven't changed, assume no bump is needed.
83- // The build process and manifest update will handle any actual code changes.
84- return false
117+ // Compare actual file contents by extracting packages and comparing SHA hashes.
118+ try {
119+ const { 0 : remoteHashes , 1 : localHashes } = await Promise . all ( [
120+ getPackageFileHashes ( `${ pkg . name } @${ manifest . version } ` ) ,
121+ getPackageFileHashes ( pkg . path , true )
122+ ] )
123+
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 ] ) {
136+ return true
137+ }
138+ }
139+
140+ return false
141+ } catch ( e ) {
142+ // If comparison fails, be conservative and assume changes.
143+ console . error ( `Error comparing packages for ${ pkg . name } :` , e ?. message )
144+ return true
145+ }
85146}
86147
87148async function maybeBumpPackage ( pkg , options = { } ) {
0 commit comments