Skip to content

Commit ba3af5a

Browse files
committed
fix: bypass SFW shim for test installs (SFW strips registry time metadata)
1 parent 263ce37 commit ba3af5a

2 files changed

Lines changed: 47 additions & 40 deletions

File tree

scripts/npm/install-npm-packages.mjs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ import {
104104
import { filterPackagesByChanges } from '../utils/git.mjs'
105105
import {
106106
copySocketOverride,
107+
PNPM_REAL_BIN,
107108
PNPM_HOISTED_INSTALL_FLAGS,
108109
PNPM_INSTALL_ENV,
109110
} from '../utils/package.mjs'
@@ -524,7 +525,7 @@ async function installPackage(packageInfo) {
524525
// If Socket override has different dependencies, install them.
525526
// Always use pnpm for installation to avoid npm override conflicts.
526527
if (overridePkgJson.dependencies) {
527-
await runCommandQuietStrict('pnpm', ['install'], {
528+
await runCommandQuietStrict(PNPM_REAL_BIN, ['install'], {
528529
cwd: installedPath,
529530
env: {
530531
...process.env,
@@ -539,7 +540,7 @@ async function installPackage(packageInfo) {
539540
// The hoisted install at parent level makes test runners available to nested package.
540541
// Always use pnpm for installation to avoid npm override conflicts.
541542
await runCommandQuietStrict(
542-
'pnpm',
543+
PNPM_REAL_BIN,
543544
['install', ...PNPM_HOISTED_INSTALL_FLAGS],
544545
{
545546
cwd: packageTempDir,
@@ -778,7 +779,7 @@ async function installPackage(packageInfo) {
778779
await pRetry(
779780
async () => {
780781
await runCommandQuietStrict(
781-
'pnpm',
782+
PNPM_REAL_BIN,
782783
['install', ...PNPM_HOISTED_INSTALL_FLAGS],
783784
{
784785
cwd: packageTempDir,
@@ -816,7 +817,7 @@ async function installPackage(packageInfo) {
816817
// Always use pnpm for installation to avoid npm override conflicts.
817818
writeProgress('👷')
818819
await runCommandQuietStrict(
819-
'pnpm',
820+
PNPM_REAL_BIN,
820821
['install', ...PNPM_HOISTED_INSTALL_FLAGS],
821822
{
822823
cwd: packageTempDir,
@@ -940,7 +941,7 @@ async function installPackage(packageInfo) {
940941
// If Socket override has different dependencies, install them.
941942
// Always use pnpm for installation to avoid npm override conflicts.
942943
if (overridePkgJson.dependencies) {
943-
await runCommandQuietStrict('pnpm', ['install'], {
944+
await runCommandQuietStrict(PNPM_REAL_BIN, ['install'], {
944945
cwd: installedPath,
945946
env: {
946947
...process.env,
@@ -955,7 +956,7 @@ async function installPackage(packageInfo) {
955956
// The hoisted install at parent level makes test runners available to nested package.
956957
// Always use pnpm for installation to avoid npm override conflicts.
957958
await runCommandQuietStrict(
958-
'pnpm',
959+
PNPM_REAL_BIN,
959960
['install', ...PNPM_HOISTED_INSTALL_FLAGS],
960961
{
961962
cwd: packageTempDir,

scripts/utils/package.mjs

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,39 @@ import { readPackageJson } from '@socketsecurity/lib/packages/operations'
1414
import { pEach } from '@socketsecurity/lib/promises'
1515
import { withSpinner } from '@socketsecurity/lib/spinner'
1616

17+
import { whichSync } from '@socketsecurity/lib/bin'
18+
1719
import { cleanTestScript } from '../../test/utils/script-cleaning.mjs'
1820
import { testRunners } from '../../test/utils/test-runners.mjs'
1921
import { DEFAULT_CONCURRENCY } from '../constants/core.mjs'
2022
import { ROOT_PATH } from '../constants/paths.mjs'
2123
import { spawn } from './spawn.mjs'
2224
import process from 'node:process'
2325

26+
// Resolve real pnpm binary, bypassing SFW shim.
27+
// SFW shims intercept pnpm and proxy registry requests, stripping metadata
28+
// fields (e.g. "time") which causes pnpm v11 ERR_PNPM_MISSING_TIME failures.
29+
// Strip the SFW shim dir from PATH to find the real binary.
30+
function getRealPnpmBin() {
31+
const sfwShimDir = process.env['SFW_SHIM_DIR']
32+
if (sfwShimDir) {
33+
const cleanPath = (process.env['PATH'] ?? '')
34+
.split(path.delimiter)
35+
.filter(p => p !== sfwShimDir)
36+
.join(path.delimiter)
37+
const real = whichSync('pnpm', { nothrow: true, path: cleanPath })
38+
if (real && typeof real === 'string') {
39+
return real
40+
}
41+
}
42+
return 'pnpm'
43+
}
44+
45+
/** Real pnpm binary path (bypasses SFW shim if present). */
46+
export const PNPM_REAL_BIN = getRealPnpmBin()
47+
2448
// Shared pnpm flags to make it behave like npm with hoisting.
25-
const PNPM_NPM_LIKE_FLAGS = [
49+
export const PNPM_NPM_LIKE_FLAGS = [
2650
'--config.shamefully-hoist=true',
2751
'--config.node-linker=hoisted',
2852
'--config.auto-install-peers=false',
@@ -31,7 +55,7 @@ const PNPM_NPM_LIKE_FLAGS = [
3155

3256
// Basic pnpm install flags for CI-friendly behavior.
3357
// These are for isolated test installs of third-party packages we don't control.
34-
const PNPM_INSTALL_BASE_FLAGS = [
58+
export const PNPM_INSTALL_BASE_FLAGS = [
3559
// Allow git-resolved subdeps in third-party packages (e.g. evalmd → markdown-it).
3660
'--config.block-exotic-subdeps=false',
3761
// Prevent interactive prompts in CI environments.
@@ -47,25 +71,25 @@ const PNPM_INSTALL_BASE_FLAGS = [
4771
]
4872

4973
// Pnpm install flags with hoisting for npm-like behavior.
50-
const PNPM_HOISTED_INSTALL_FLAGS = [
74+
export const PNPM_HOISTED_INSTALL_FLAGS = [
5175
...PNPM_NPM_LIKE_FLAGS,
5276
...PNPM_INSTALL_BASE_FLAGS,
5377
]
5478

5579
// Environment override to force pnpm to install devDependencies.
5680
// By default, pnpm skips devDependencies when CI or NODE_ENV=production is detected.
57-
const PNPM_INSTALL_ENV = { CI: undefined, NODE_ENV: undefined }
81+
export const PNPM_INSTALL_ENV = { CI: undefined, NODE_ENV: undefined }
5882

5983
/**
6084
* Reads and caches editable package.json files to avoid redundant disk I/O.
6185
* @type {Map<string, any>}
6286
*/
63-
const editablePackageJsonCache = new Map()
87+
export const editablePackageJsonCache = new Map()
6488

6589
/**
6690
* Reads an editable package.json with caching support.
6791
*/
68-
async function readCachedEditablePackageJson(pkgPath, options = {}) {
92+
export async function readCachedEditablePackageJson(pkgPath, options = {}) {
6993
const cacheKey = pkgPath
7094

7195
if (!editablePackageJsonCache.has(cacheKey)) {
@@ -83,14 +107,14 @@ async function readCachedEditablePackageJson(pkgPath, options = {}) {
83107
/**
84108
* Clears the editable package.json cache.
85109
*/
86-
function clearPackageJsonCache() {
110+
export function clearPackageJsonCache() {
87111
editablePackageJsonCache.clear()
88112
}
89113

90114
/**
91115
* Updates multiple package.json files in parallel.
92116
*/
93-
async function updatePackagesJson(packages, options = {}) {
117+
export async function updatePackagesJson(packages, options = {}) {
94118
const { concurrency = DEFAULT_CONCURRENCY, spinner } = options
95119

96120
await pEach(
@@ -111,7 +135,7 @@ async function updatePackagesJson(packages, options = {}) {
111135
/**
112136
* Collects package.json data from multiple packages.
113137
*/
114-
async function collectPackageData(paths, options = {}) {
138+
export async function collectPackageData(paths, options = {}) {
115139
const {
116140
concurrency = DEFAULT_CONCURRENCY,
117141
fields = ['name', 'version', 'description'],
@@ -142,7 +166,7 @@ async function collectPackageData(paths, options = {}) {
142166
/**
143167
* Common patterns for processing packages with spinner feedback.
144168
*/
145-
async function processWithSpinner(items, processor, options = {}) {
169+
export async function processWithSpinner(items, processor, options = {}) {
146170
const {
147171
concurrency = DEFAULT_CONCURRENCY,
148172
errorMessage,
@@ -195,7 +219,7 @@ async function processWithSpinner(items, processor, options = {}) {
195219
/**
196220
* Resolves the real path of a file or directory, handling symlinks.
197221
*/
198-
async function resolveRealPath(pathStr) {
222+
export async function resolveRealPath(pathStr) {
199223
try {
200224
return await fs.realpath(pathStr)
201225
} catch {
@@ -206,7 +230,7 @@ async function resolveRealPath(pathStr) {
206230
/**
207231
* Computes a hash of override package dependencies for cache validation.
208232
*/
209-
async function computeOverrideHash(overridePath) {
233+
export async function computeOverrideHash(overridePath) {
210234
try {
211235
const pkgJsonPath = path.join(overridePath, 'package.json')
212236
const pkgJson = await readPackageJson(pkgJsonPath)
@@ -224,7 +248,7 @@ async function computeOverrideHash(overridePath) {
224248
/**
225249
* Copies Socket override files to a package directory.
226250
*/
227-
async function copySocketOverride(fromPath, toPath, options) {
251+
export async function copySocketOverride(fromPath, toPath, options) {
228252
const opts = { __proto__: null, ...options }
229253
const { excludePackageJson = true } = opts
230254

@@ -263,7 +287,7 @@ async function copySocketOverride(fromPath, toPath, options) {
263287
/**
264288
* Builds test environment with proper PATH for test runners.
265289
*/
266-
function buildTestEnv(packageTempDir, installedPath) {
290+
export function buildTestEnv(packageTempDir, installedPath) {
267291
const packageBinPath = path.join(packageTempDir, 'node_modules', '.bin')
268292
const nestedBinPath = path.join(installedPath, 'node_modules', '.bin')
269293
const rootBinPath = path.join(ROOT_PATH, 'node_modules', '.bin')
@@ -276,7 +300,7 @@ function buildTestEnv(packageTempDir, installedPath) {
276300
/**
277301
* Run a command with spawn.
278302
*/
279-
async function runCommand(command, args, options = {}) {
303+
export async function runCommand(command, args, options = {}) {
280304
try {
281305
const result = await spawn(command, args, {
282306
stdio: 'pipe',
@@ -304,7 +328,7 @@ async function runCommand(command, args, options = {}) {
304328
* @param {string} [options.versionSpec] - Version or URL to install (optional, for npm packages)
305329
* @returns {Promise<{installed: boolean, packagePath?: string, reason?: string}>}
306330
*/
307-
async function installPackageForTesting(sourcePath, packageName, options = {}) {
331+
export async function installPackageForTesting(sourcePath, packageName, options = {}) {
308332
const { versionSpec } = options
309333

310334
if (!existsSync(sourcePath)) {
@@ -484,21 +508,3 @@ async function installPackageForTesting(sourcePath, packageName, options = {}) {
484508
}
485509
}
486510

487-
export {
488-
buildTestEnv,
489-
clearPackageJsonCache,
490-
collectPackageData,
491-
computeOverrideHash,
492-
copySocketOverride,
493-
editablePackageJsonCache,
494-
installPackageForTesting,
495-
PNPM_HOISTED_INSTALL_FLAGS,
496-
PNPM_INSTALL_BASE_FLAGS,
497-
PNPM_INSTALL_ENV,
498-
PNPM_NPM_LIKE_FLAGS,
499-
processWithSpinner,
500-
readCachedEditablePackageJson,
501-
resolveRealPath,
502-
runCommand,
503-
updatePackagesJson,
504-
}

0 commit comments

Comments
 (0)