From d7fc3365f8bd3adcf44a856db15a8dde51457e17 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 1 Feb 2024 12:00:14 -0800 Subject: [PATCH] fix: update sharp special case for pnpm (#387) This ensures we trace child optional dependencies as well so that the symlink hierarchy is maintained for pnpm. x-ref: https://github.com/vercel/next.js/issues/59346 x-ref: https://github.com/lovell/sharp/issues/3967 --------- Co-authored-by: Steven --- src/utils/special-cases.ts | 16 ++++- test/integration.test.js | 124 +++++++++++++++++++++------------ test/integration/sharp-pnpm.js | 9 +++ test/integration/sharp.js | 5 -- 4 files changed, 105 insertions(+), 49 deletions(-) create mode 100644 test/integration/sharp-pnpm.js diff --git a/src/utils/special-cases.ts b/src/utils/special-cases.ts index c5d0f9f3..6c5d6bfa 100644 --- a/src/utils/special-cases.ts +++ b/src/utils/special-cases.ts @@ -109,13 +109,27 @@ const specialCases: Record void> = { emitAsset(resolve(id.replace('index.js', 'preload.js'))); } }, - 'sharp' ({ id, emitAssetDirectory }) { + 'sharp': async ({ id, emitAssetDirectory, job }) => { if (id.endsWith('sharp/lib/index.js')) { const file = resolve(id, '..', '..', 'package.json'); const pkg = JSON.parse(readFileSync(file, 'utf8')); for (const dep of Object.keys(pkg.optionalDependencies || {})) { const dir = resolve(id, '..', '..', '..', dep); emitAssetDirectory(dir); + + try { + const file = resolve(dir, 'package.json'); + const pkg = JSON.parse(readFileSync(file, 'utf8')); + for (const innerDep of Object.keys(pkg.optionalDependencies || {})) { + const innerDir = resolve(await job.realpath(dir), '..', '..', innerDep); + emitAssetDirectory(innerDir); + } + } catch (err: any) { + if (err && err.code !== 'ENOENT') { + console.error(`Error reading "sharp" dependencies from "${dir}/package.json"'`); + throw err; + } + } } } }, diff --git a/test/integration.test.js b/test/integration.test.js index e7d9cdcd..a57d40d4 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -3,68 +3,106 @@ const path = require('path'); const { nodeFileTrace } = require('../out/node-file-trace'); const os = require('os'); const rimraf = require('rimraf'); -const { readFile, writeFile, readlink, symlink } = promises; -const { fork } = require('child_process'); +const { readFile, writeFile, readlink, symlink, copyFile } = promises; +const { fork, exec: execOrig } = require('child_process'); + +const exec = require('util').promisify(execOrig); jest.setTimeout(200_000); const integrationDir = `${__dirname}${path.sep}integration`; for (const integrationTest of readdirSync(integrationDir)) { + let currentIntegrationDir = integrationDir; + it(`should correctly trace and correctly execute ${integrationTest}`, async () => { console.log('Tracing and executing ' + integrationTest); - const nftCache = {} + const nftCache = {}; + const rand = Math.random().toString().slice(2); const fails = integrationTest.endsWith('failure.js'); - const { fileList, reasons, warnings } = await nodeFileTrace([`${integrationDir}/${integrationTest}`], { - log: true, - cache: nftCache, - base: path.resolve(__dirname, '..'), - processCwd: integrationDir, - // ignore other integration tests - ignore: ['test/integration/**'] - }); + let traceBase = path.resolve(__dirname, '..'); + + if (integrationTest === 'sharp-pnpm.js') { + if (process.version.startsWith('v18.') && process.platform === 'win32') { + console.log( + 'Skipping sharp-pnpm.js on Node 18 and Windows because of a bug: ' + + 'https://github.com/nodejs/node/issues/18518' + ); + return; + } + const tmpdir = path.resolve(os.tmpdir(), `node-file-trace-${integrationTest}-${rand}`); + rimraf.sync(tmpdir); + mkdirSync(tmpdir); + await copyFile( + path.join(integrationDir, integrationTest), + path.join(tmpdir, integrationTest) + ); + await writeFile( + path.join(tmpdir, 'package.json'), + JSON.stringify({ packageManager: 'pnpm@8.14.3', dependencies: { sharp: '0.33.2' } }) + ); + await exec(`corepack enable && pnpm i`, { cwd: tmpdir, stdio: 'inherit' }); + currentIntegrationDir = tmpdir; + traceBase = tmpdir; + } + + const { fileList, reasons, warnings } = await nodeFileTrace( + [`${currentIntegrationDir}/${integrationTest}`], + { + log: true, + cache: nftCache, + base: traceBase, + processCwd: currentIntegrationDir, + // ignore other integration tests + ignore: ['test/integration/**'], + } + ); // warnings.forEach(warning => console.warn(warning)); - const randomTmpId = Math.random().toString().slice(2) - const tmpdir = path.resolve(os.tmpdir(), `node-file-trace-${randomTmpId}`); + const tmpdir = path.resolve(os.tmpdir(), `node-file-trace-${rand}`); rimraf.sync(tmpdir); mkdirSync(tmpdir); - await Promise.all([...fileList].map(async file => { - const inPath = path.resolve(__dirname, '..', file); - const outPath = path.resolve(tmpdir, file); - try { - var symlinkPath = await readlink(inPath); - } - catch (e) { - if (e.code !== 'EINVAL' && e.code !== 'UNKNOWN') throw e; - } - mkdirSync(path.dirname(outPath), { recursive: true }); - if (symlinkPath) { - await symlink(symlinkPath, outPath); - } - else { - await writeFile(outPath, await readFile(inPath), { mode: 0o777 }); - } - })); - const testFile = path.join(tmpdir, 'test', 'integration', integrationTest); + + await Promise.all( + [...fileList].map(async (file) => { + const inPath = path.resolve(traceBase, file); + const outPath = path.resolve(tmpdir, file); + try { + var symlinkPath = await readlink(inPath); + } catch (e) { + if (e.code !== 'EINVAL' && e.code !== 'UNKNOWN') throw e; + } + mkdirSync(path.dirname(outPath), { recursive: true }); + if (symlinkPath) { + await symlink(symlinkPath, outPath); + } else { + await writeFile(outPath, await readFile(inPath), { mode: 0o777 }); + } + }) + ); + const testFile = path.join(tmpdir, path.relative(traceBase, currentIntegrationDir), integrationTest); + const ps = fork(testFile, { - stdio: fails ? 'pipe' : 'inherit' + stdio: fails ? 'pipe' : 'inherit', }); - const code = await new Promise(resolve => ps.on('close', resolve)); + const code = await new Promise((resolve) => ps.on('close', resolve)); expect(code).toBe(fails ? 1 : 0); rimraf.sync(tmpdir); - + // TODO: ensure analysis cache is safe for below case // seems this fails with cache since < 0.14.0 if (integrationTest !== 'browserify-middleware.js') { - const cachedResult = await nodeFileTrace([`${integrationDir}/${integrationTest}`], { - log: true, - cache: nftCache, - base: path.resolve(__dirname, '..'), - processCwd: integrationDir, - // ignore other integration tests - ignore: ['test/integration/**'] - }); - expect([...cachedResult.fileList].sort()).toEqual([...fileList].sort()) + const cachedResult = await nodeFileTrace( + [`${currentIntegrationDir}/${integrationTest}`], + { + log: true, + cache: nftCache, + base: traceBase, + processCwd: currentIntegrationDir, + // ignore other integration tests + ignore: ['test/integration/**'], + } + ); + expect([...cachedResult.fileList].sort()).toEqual([...fileList].sort()); } }); } diff --git a/test/integration/sharp-pnpm.js b/test/integration/sharp-pnpm.js new file mode 100644 index 00000000..71f8e5c7 --- /dev/null +++ b/test/integration/sharp-pnpm.js @@ -0,0 +1,9 @@ +const sharp = require('sharp'); + +const roundedCorners = Buffer.from( + '' +); + +sharp(roundedCorners) + .resize(200, 200) + .png().toBuffer(); diff --git a/test/integration/sharp.js b/test/integration/sharp.js index 8ed7491a..71f8e5c7 100644 --- a/test/integration/sharp.js +++ b/test/integration/sharp.js @@ -1,5 +1,4 @@ const sharp = require('sharp'); -const path = require('path'); const roundedCorners = Buffer.from( '' @@ -8,7 +7,3 @@ const roundedCorners = Buffer.from( sharp(roundedCorners) .resize(200, 200) .png().toBuffer(); - -sharp(path.resolve(__dirname, '../fixtures/img.jpg')) - .resize({ width: 100, height: 100 }) - .jpeg().toBuffer();