Skip to content

Commit

Permalink
fix: update sharp special case for pnpm (#387)
Browse files Browse the repository at this point in the history
This ensures we trace child optional dependencies as well so that the
symlink hierarchy is maintained for pnpm.

x-ref: vercel/next.js#59346
x-ref: lovell/sharp#3967

---------

Co-authored-by: Steven <steven@ceriously.com>
  • Loading branch information
ijjk and styfle committed Feb 1, 2024
1 parent 3642bb4 commit d7fc336
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 49 deletions.
16 changes: 15 additions & 1 deletion src/utils/special-cases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,27 @@ const specialCases: Record<string, (o: SpecialCaseOpts) => 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;
}
}
}
}
},
Expand Down
124 changes: 81 additions & 43 deletions test/integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
});
}
9 changes: 9 additions & 0 deletions test/integration/sharp-pnpm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const sharp = require('sharp');

const roundedCorners = Buffer.from(
'<svg><rect x="0" y="0" width="200" height="200" rx="50" ry="50"/></svg>'
);

sharp(roundedCorners)
.resize(200, 200)
.png().toBuffer();
5 changes: 0 additions & 5 deletions test/integration/sharp.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const sharp = require('sharp');
const path = require('path');

const roundedCorners = Buffer.from(
'<svg><rect x="0" y="0" width="200" height="200" rx="50" ry="50"/></svg>'
Expand All @@ -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();

0 comments on commit d7fc336

Please sign in to comment.