Skip to content

Commit d25beed

Browse files
committed
fix(scripts): detect unpublished npm versions
Query the npm registry for the exact local version instead of comparing only with the latest dist-tag. Report unpublished packages accurately and avoid spawning pnpm for each lookup.
1 parent 16b690c commit d25beed

1 file changed

Lines changed: 51 additions & 33 deletions

File tree

scripts/check-npm-publish.mjs

Lines changed: 51 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,34 @@
11
#!/usr/bin/env node
22

3-
import { promises as fs, readFileSync } from 'fs'
4-
import path from 'path'
5-
import { execFileSync } from 'child_process'
6-
import process from 'process'
7-
8-
const __dirname = path.dirname(new URL(import.meta.url).pathname)
3+
import { promises as fs, readFileSync } from 'node:fs'
4+
import path from 'node:path'
5+
import process from 'node:process'
6+
import { fileURLToPath } from 'node:url'
7+
8+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
9+
const REGISTRY_URL = 'https://registry.npmjs.org/'
10+
const REQUEST_TIMEOUT = 10_000
11+
12+
const getPublishedPackage = async (packageName) => {
13+
const packageUrl = new URL(encodeURIComponent(packageName), REGISTRY_URL)
14+
const response = await fetch(packageUrl, {
15+
headers: { Accept: 'application/vnd.npm.install-v1+json' },
16+
signal: AbortSignal.timeout(REQUEST_TIMEOUT),
17+
})
18+
19+
if (response.status === 404) {
20+
return { latestVersion: null, versions: new Set() }
21+
}
22+
if (!response.ok) {
23+
throw new Error(`npm registry returned HTTP ${response.status} for ${packageName}`)
24+
}
925

10-
// Helper to run shell commands
11-
const getPublishedVersion = (packageName) =>
12-
execFileSync('pnpm', ['info', packageName, 'version'], { encoding: 'utf-8' }).trim()
26+
const packageMetadata = await response.json()
27+
return {
28+
latestVersion: packageMetadata['dist-tags']?.latest || null,
29+
versions: new Set(Object.keys(packageMetadata.versions || {})),
30+
}
31+
}
1332

1433
// Recursively find all directories containing package.json
1534
const findNodeProjects = async (dir) => {
@@ -36,8 +55,8 @@ const findNodeProjects = async (dir) => {
3655
return projects
3756
}
3857

39-
// Compare local version with the latest npm version
40-
const checkVersionMismatch = async (projectPath) => {
58+
// Check whether the exact local version has already been published to npm.
59+
const checkVersionPublished = async (projectPath) => {
4160
const packageJsonPath = path.join(projectPath, 'package.json')
4261

4362
try {
@@ -46,23 +65,22 @@ const checkVersionMismatch = async (projectPath) => {
4665
const localVersion = packageJson.version
4766
const packageName = packageJson.name
4867

49-
// Get the latest version from npm using pnpm info
50-
const npmVersion = getPublishedVersion(packageName)
68+
const { latestVersion, versions } = await getPublishedPackage(packageName)
5169

52-
if (localVersion === npmVersion) {
53-
console.log(`✅ ${packageName} is up to date (${npmVersion})`)
54-
} else {
55-
console.log(`❌ ${packageName} is outdated (local: ${localVersion}, npm: ${npmVersion})`)
70+
if (versions.has(localVersion)) {
71+
const latestSuffix =
72+
localVersion === latestVersion ? '' : `; npm latest is ${latestVersion || 'unset'}`
73+
console.log(`✅ ${packageName}@${localVersion} is published${latestSuffix}`)
74+
return null
5675
}
5776

58-
if (localVersion !== npmVersion) {
59-
return { packageName, localVersion, latestVersion: npmVersion }
60-
}
77+
console.log(
78+
`❌ ${packageName}@${localVersion} is not published (npm latest: ${latestVersion || 'none'})`,
79+
)
80+
return { packageName, localVersion, latestVersion }
6181
} catch (error) {
6282
throw new Error(`Failed to check npm version for ${projectPath}`, { cause: error })
6383
}
64-
65-
return null
6684
}
6785

6886
// Parse command-line arguments to get the project name if provided
@@ -79,8 +97,8 @@ const getArgs = () => {
7997
return specifiedProject
8098
}
8199

82-
// Main function to find outdated packages
83-
const findOutdatedProjects = async () => {
100+
// Main function to find unpublished local package versions
101+
const findUnpublishedProjects = async () => {
84102
const specifiedProject = getArgs()
85103
let projects = await findNodeProjects(path.join(__dirname, '../ui'))
86104
console.log(`Found ${projects.length} projects under 'ui' directory.\n`)
@@ -100,29 +118,29 @@ const findOutdatedProjects = async () => {
100118
}
101119
}
102120

103-
const outdatedProjects = []
121+
const unpublishedProjects = []
104122

105123
for (const project of projects) {
106-
const result = await checkVersionMismatch(project)
124+
const result = await checkVersionPublished(project)
107125
if (result) {
108-
outdatedProjects.push(result)
126+
unpublishedProjects.push(result)
109127
}
110128
}
111129

112130
console.log('\n==================================================\n')
113131

114-
if (outdatedProjects.length === 0) {
115-
console.log('✅ All projects have the latest versions pushed to npm.')
132+
if (unpublishedProjects.length === 0) {
133+
console.log('✅ Every local package version is published to npm.')
116134
} else {
117-
console.log('Projects with outdated versions:\n')
118-
outdatedProjects.forEach(({ packageName, localVersion, latestVersion }) => {
119-
console.log(`❌ ${packageName}: Local version ${localVersion}, NPM version ${latestVersion}`)
135+
console.log('Local package versions not published to npm:\n')
136+
unpublishedProjects.forEach(({ packageName, localVersion, latestVersion }) => {
137+
console.log(`❌ ${packageName}: local ${localVersion}, npm latest ${latestVersion || 'none'}`)
120138
})
121139
process.exit(1)
122140
}
123141
}
124142

125-
findOutdatedProjects().catch((error) => {
143+
findUnpublishedProjects().catch((error) => {
126144
console.error(error)
127145
process.exitCode = 1
128146
})

0 commit comments

Comments
 (0)