chore(deps): bump sharp to ^0.35.0 to resolve GHSA-f88m-g3jw-g9cj (#8952)

* chore(deps): bump sharp to ^0.35.0 to resolve GHSA-f88m-g3jw-g9cj

* chore(vscode): regenerate NOTICES.txt for sharp 0.35 bump

* fix(scripts): read sharp pin from core package.json to prevent drift

The published CLI's sharp version was hardcoded in prepare-package.js,
which drifted from the workspace dependency on every bump. Read it from
packages/core/package.json so the publish pin always matches the declared
dependency. Add a test assertion to catch future drift in CI.

* fix(scripts): read sharp pin from package-lock.json instead of core package.json range

The previous approach read the sharp version from packages/core/package.json
(which has ^0.35.0) and stripped the caret, producing 0.35.0. This is the
range floor, not the lockfile-resolved version (0.35.3). Read from
package-lock.json so the published CLI ships the same version CI tests.

* fix(scripts): pin published sharp to core resolution

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(scripts): validate sharp pin against core manifest and add hoisted fallback test

- Align the lockfile reader with the sibling pattern in build-standalone-release.js:
  validate the resolved version against packages/core's declared sharp range
- Wrap the lockfile read in try/catch so a missing or malformed file
  surfaces a clear error instead of an opaque ENOENT/SyntaxError
- Add a test for the hoisted fallback path (node_modules/sharp) that
  actually executes in the current production release
- Add a comment explaining why sharp is exact-pinned like all other
  native optional deps in the published manifest

* fix(scripts): accept compatible sharp lock versions

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
易良 2026-08-13 06:56:10 +00:00 committed by GitHub
parent a8bcaefea7
commit f159100c8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 810 additions and 155 deletions

818
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -155,7 +155,7 @@
"prettier": "^3.5.3",
"react-devtools-core": "^6.1.5",
"semver": "^7.7.2",
"sharp": "^0.34.5",
"sharp": "^0.35.0",
"strip-ansi": "^7.1.2",
"tsx": "^4.20.3",
"typescript-eslint": "^8.30.1",

View file

@ -91,7 +91,7 @@
"picomatch": "^4.0.1",
"prompts": "^2.4.2",
"proper-lockfile": "^4.1.2",
"sharp": "^0.34.5",
"sharp": "^0.35.0",
"shell-quote": "^1.9.0",
"simple-git": "^3.36.0",
"strip-ansi": "^7.1.0",

View file

@ -5,7 +5,7 @@
*/
import fs from 'node:fs/promises';
import type { Metadata } from 'sharp';
import type { Metadata, SharpConstructor } from 'sharp';
const IMAGE_VIEW_MAX_EDGE = 1568;
const IMAGE_VIEW_MAX_PATCHES = 1568;
@ -42,7 +42,7 @@ interface ImageSize {
interface PreparedImage {
bytes: Buffer;
metadata: Metadata;
sharp: typeof import('sharp');
sharp: SharpConstructor;
}
export type ImageViewErrorCode =
@ -116,15 +116,9 @@ async function prepareImage(
signal: AbortSignal,
): Promise<PreparedImage> {
signal.throwIfAborted();
let sharp: typeof import('sharp');
let sharp: SharpConstructor;
try {
// sharp is a CJS `export =` module, so the callable is on `.default`
// at runtime even though NodeNext types collapse that namespace away.
sharp = (
(await import('sharp')) as unknown as {
default: typeof import('sharp');
}
).default;
sharp = (await import('sharp')).default;
} catch {
throw new ImageViewError(
'renderer_unavailable',

View file

@ -14575,8 +14575,8 @@ ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
============================================================
sharp@0.34.5
(git://github.com/lovell/sharp.git)
sharp@0.35.3
(git+https://github.com/lovell/sharp.git)
Apache License
Version 2.0, January 2004
@ -15067,7 +15067,7 @@ detect-libc@2.1.2
============================================================
semver@7.7.3
semver@7.8.5
(git+https://github.com/npm/node-semver.git)
The ISC License

View file

@ -14,6 +14,7 @@ import fs from 'node:fs';
import { createRequire } from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import semver from 'semver';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -271,6 +272,31 @@ function writeDistPackageJson(rootDir, distDir) {
const rootPackageJson = JSON.parse(
fs.readFileSync(path.join(rootDir, 'package.json'), 'utf-8'),
);
let lockfile;
try {
lockfile = JSON.parse(
fs.readFileSync(path.join(rootDir, 'package-lock.json'), 'utf-8'),
);
} catch (error) {
throw new Error(`Cannot read package-lock.json: ${error.message}`);
}
const coreManifest = JSON.parse(
fs.readFileSync(
path.join(rootDir, 'packages', 'core', 'package.json'),
'utf-8',
),
);
const sharpVersion =
lockfile.packages?.['packages/core/node_modules/sharp']?.version ??
lockfile.packages?.['node_modules/sharp']?.version;
const declared = coreManifest.dependencies?.sharp;
if (!sharpVersion || !declared || !semver.satisfies(sharpVersion, declared)) {
throw new Error(
`sharp version is not locked in package-lock.json ` +
`(resolved ${sharpVersion ?? 'none'}, ` +
`packages/core declares ${declared ?? 'none'})`,
);
}
const distPackageJson = {
name: rootPackageJson.name,
@ -322,7 +348,12 @@ function writeDistPackageJson(rootDir, distDir) {
// is sufficient: its own optionalDependencies pull in the matching @img
// platform binary for every OS/arch npm installs onto, so the platform
// packages are not pinned here (pinning them drifts on a sharp bump).
sharp: '0.34.5',
// The version is exact-pinned like all other native optional deps in this
// manifest — a project-wide convention that keeps the published tarball
// reproducible. Sharp's recurring CVE stream means users must wait for a
// CLI release to pick up libvips fixes; nightly releases keep the
// turnaround short.
sharp: sharpVersion,
},
engines: rootPackageJson.engines,
};

View file

@ -722,6 +722,8 @@ describe('package asset scripts', () => {
expect(distPackageJson.optionalDependencies).toMatchObject({
'@qwen-code/audio-capture': rootPackageJson.version,
});
expect(distPackageJson.optionalDependencies.sharp).toBe('0.35.4');
expect(
existsSync(
path.join(
@ -740,6 +742,60 @@ describe('package asset scripts', () => {
).toBe(true);
});
it('falls back to the hoisted lockfile entry when core has no nested sharp', () => {
const rootDir = createFixtureRoot();
writeFile(
rootDir,
'package-lock.json',
JSON.stringify({
packages: {
'node_modules/sharp': {
version: '0.35.3',
},
},
}),
);
writeFile(
rootDir,
'packages/core/package.json',
JSON.stringify(
{
name: '@qwen-code/qwen-code-core',
version: '0.17.0',
dependencies: {
sharp: '^0.35.0',
},
},
null,
2,
),
);
createBundleArtifacts(rootDir);
stubConsole();
preparePackage({ rootDir, requireNativeAudioCapture: false });
const distPackageJson = JSON.parse(
readFileSync(path.join(rootDir, 'dist', 'package.json'), 'utf8'),
);
expect(distPackageJson.optionalDependencies.sharp).toBe('0.35.3');
});
it('rejects a locked sharp version outside the core declaration', () => {
const rootDir = createFixtureRoot();
writeFile(
rootDir,
'packages/core/package.json',
JSON.stringify({ dependencies: { sharp: '^0.34.0' } }),
);
createBundleArtifacts(rootDir);
stubConsole();
expect(() =>
preparePackage({ rootDir, requireNativeAudioCapture: false }),
).toThrow(/resolved 0\.35\.4, packages\/core declares \^0\.34\.0/);
});
it('omits browser MCP install hooks and deps from the prepared dist package', () => {
const rootDir = createFixtureRoot();
createBundleArtifacts(rootDir);
@ -1076,11 +1132,45 @@ describe('package asset scripts', () => {
),
);
writeFile(
rootDir,
'package-lock.json',
JSON.stringify(
{
packages: {
'node_modules/sharp': {
version: '0.35.3',
},
'packages/core/node_modules/sharp': {
version: '0.35.4',
},
},
},
null,
2,
),
);
writeFile(
rootDir,
'packages/cli/src/i18n/locales/en.json',
'{"hello":"world"}\n',
);
writeFile(
rootDir,
'packages/core/package.json',
JSON.stringify(
{
name: '@qwen-code/qwen-code-core',
version: '0.17.0',
dependencies: {
sharp: '^0.35.0',
},
},
null,
2,
),
);
writeFile(
rootDir,
'packages/audio-capture/package.json',