Skip to content

Commit b2e1af0

Browse files
authored
Merge branch 'main' into fix/328520-turn-changes-preview-icon
2 parents 15957d0 + 6a69379 commit b2e1af0

26 files changed

Lines changed: 549 additions & 90 deletions

File tree

.github/dependabot.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,7 @@ updates:
1414
directory: "/extensions/markdown-language-features"
1515
schedule:
1616
interval: "daily"
17+
time: "16:00"
18+
timezone: "America/Los_Angeles"
1719
allow:
1820
- dependency-name: "@vscode/markdown-editor"

.github/instructions/css-best-practices.instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ applyTo: "**/*.css"
88
## Selectors
99

1010
- Avoid `:has()` selectors. Because their result depends on descendant state, DOM mutations can invalidate styles on ancestors and cause expensive style recalculation, especially when selectors are broadly scoped. Instead, represent the state explicitly with a class or data attribute on the smallest container you own, and scope selectors to that marker. Add and remove the marker together with the state it represents.
11+
- Never match the `class` attribute by substring (`[class*="…"]`, `[class^="…"]`, `[class$="…"]`). A single such selector anywhere in the workbench stylesheet defeats Blink's per-class invalidation: every `classList` change then forces a style recalculation for that element, even when no rule references the class that changed. Measured on a 3.7k-node workbench, the ten `[class*="monaco-decoration-itemColor"]` selectors in the Modern UI tab styles alone made a full style recalculation 2.4x slower. When a class carries a generated suffix, have the code that applies it also set a stable marker class (see `DECORATION_LABEL_COLOR_CLASS`) and match that instead.

.vscode-test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ const extensions = [
5757
workspaceFolder: path.join(os.tmpdir(), `confeditout-${Math.floor(Math.random() * 100000)}`),
5858
mocha: { timeout: 60_000 }
5959
},
60+
{
61+
label: 'npm',
62+
workspaceFolder: path.join(os.tmpdir(), `npmout-${Math.floor(Math.random() * 100000)}`),
63+
mocha: { timeout: 60_000 }
64+
},
6065
{
6166
label: 'github-authentication',
6267
workspaceFolder: path.join(os.tmpdir(), `msft-auth-${Math.floor(Math.random() * 100000)}`),
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
export interface ViewPackageInfo {
7+
description: string;
8+
version?: string;
9+
time?: string;
10+
homepage?: string;
11+
installedVersion?: string;
12+
}
13+
14+
export interface NpmViewRecord {
15+
description?: string;
16+
version?: string;
17+
homepage?: string;
18+
time?: { [version: string]: string };
19+
'dist-tags.latest'?: string;
20+
}
21+
22+
/**
23+
* Parses the output of `npm view --json`. npm 12+ always returns an array `[{...}]`,
24+
* even for a single package, while older versions return the object directly.
25+
*/
26+
export function parseNpmViewOutput(stdout: string): ViewPackageInfo | undefined {
27+
try {
28+
const parsed = JSON.parse(stdout) as NpmViewRecord | NpmViewRecord[];
29+
const content = Array.isArray(parsed) ? parsed[0] : parsed;
30+
const version = content['dist-tags.latest'] || content.version;
31+
return {
32+
description: content.description ?? '',
33+
version,
34+
time: version ? content.time?.[version] : undefined,
35+
homepage: content.homepage
36+
};
37+
} catch (e) {
38+
return undefined;
39+
}
40+
}

extensions/npm/src/features/packageJSONContribution.ts

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Location } from 'jsonc-parser';
1111
import type * as cp from 'child_process';
1212
import { dirname } from 'path';
1313
import { fromNow } from './date';
14+
import { parseNpmViewOutput, ViewPackageInfo } from './npmViewParser';
1415

1516
const LIMIT = 40;
1617

@@ -325,21 +326,7 @@ export class PackageJSONContribution implements IJSONContribution {
325326
private async npmView(npmCommandPath: string, pack: string, resource: Uri | undefined): Promise<ViewPackageInfo | undefined> {
326327
const args = ['view', '--json', '--', pack, 'description', 'dist-tags.latest', 'homepage', 'version', 'time'];
327328
const stdout = await this.runNpmCommand(npmCommandPath, args, resource);
328-
if (stdout) {
329-
try {
330-
const content = JSON.parse(stdout);
331-
const version = content['dist-tags.latest'] || content['version'];
332-
return {
333-
description: content['description'],
334-
version,
335-
time: content.time?.[version],
336-
homepage: content['homepage']
337-
};
338-
} catch (e) {
339-
// ignore
340-
}
341-
}
342-
return undefined;
329+
return stdout ? parseNpmViewOutput(stdout) : undefined;
343330
}
344331

345332
private async npmjsView(pack: string): Promise<ViewPackageInfo | undefined> {
@@ -429,11 +416,3 @@ interface SearchPackageInfo {
429416
version?: string;
430417
links?: { homepage?: string };
431418
}
432-
433-
interface ViewPackageInfo {
434-
description: string;
435-
version?: string;
436-
time?: string;
437-
homepage?: string;
438-
installedVersion?: string;
439-
}

extensions/npm/src/test/index.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as path from 'path';
7+
import * as testRunner from '../../../../test/integration/electron/testrunner';
8+
9+
const options: import('mocha').MochaOptions = {
10+
ui: 'tdd',
11+
color: true,
12+
timeout: 60000
13+
};
14+
15+
// These integration tests is being run in multiple environments (electron, web, remote)
16+
// so we need to set the suite name based on the environment as the suite name is used
17+
// for the test results file name
18+
let suite = '';
19+
if (process.env.VSCODE_BROWSER) {
20+
suite = `${process.env.VSCODE_BROWSER} Browser Integration Npm Tests`;
21+
} else if (process.env.REMOTE_VSCODE) {
22+
suite = 'Remote Integration Npm Tests';
23+
} else {
24+
suite = 'Integration Npm Tests';
25+
}
26+
27+
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE) {
28+
options.reporter = 'mocha-multi-reporters';
29+
options.reporterOptions = {
30+
reporterEnabled: 'spec, mocha-junit-reporter',
31+
mochaJunitReporterReporterOptions: {
32+
testsuitesTitle: `${suite} ${process.platform}`,
33+
mochaFile: path.join(
34+
process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE || __dirname,
35+
`test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
36+
}
37+
};
38+
}
39+
40+
testRunner.configure(options);
41+
42+
export = testRunner;
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as assert from 'assert';
7+
import { NpmViewRecord, parseNpmViewOutput } from '../features/npmViewParser';
8+
9+
const npmViewOutput: NpmViewRecord = {
10+
description: 'React is a JavaScript library for building user interfaces.',
11+
'dist-tags.latest': '19.1.0',
12+
homepage: 'https://react.dev/',
13+
version: '19.1.0',
14+
time: {
15+
'19.1.0': '2025-05-20T20:58:48.397Z',
16+
'0.0.0-experimental-98e8ed76': '2026-07-25T21:39:01.123Z'
17+
}
18+
};
19+
20+
suite('npmViewParser', () => {
21+
22+
test('parses object output (npm <= 11)', () => {
23+
const info = parseNpmViewOutput(JSON.stringify(npmViewOutput));
24+
assert.ok(info);
25+
assert.strictEqual(info!.description, 'React is a JavaScript library for building user interfaces.');
26+
assert.strictEqual(info!.version, '19.1.0');
27+
assert.strictEqual(info!.time, '2025-05-20T20:58:48.397Z');
28+
assert.strictEqual(info!.homepage, 'https://react.dev/');
29+
});
30+
31+
test('parses array output (npm 12+)', () => {
32+
const info = parseNpmViewOutput(JSON.stringify([npmViewOutput]));
33+
assert.ok(info);
34+
assert.strictEqual(info!.description, 'React is a JavaScript library for building user interfaces.');
35+
assert.strictEqual(info!.version, '19.1.0');
36+
assert.strictEqual(info!.time, '2025-05-20T20:58:48.397Z');
37+
assert.strictEqual(info!.homepage, 'https://react.dev/');
38+
});
39+
40+
test('prefers dist-tags.latest over version', () => {
41+
const info = parseNpmViewOutput(JSON.stringify({
42+
'dist-tags.latest': '19.1.0',
43+
version: '0.0.0-experimental-98e8ed76',
44+
time: {
45+
'19.1.0': '2025-05-20T20:58:48.397Z',
46+
'0.0.0-experimental-98e8ed76': '2026-07-25T21:39:01.123Z'
47+
}
48+
}));
49+
assert.ok(info);
50+
assert.strictEqual(info!.version, '19.1.0');
51+
assert.strictEqual(info!.time, '2025-05-20T20:58:48.397Z');
52+
assert.notStrictEqual(info!.time, '2026-07-25T21:39:01.123Z');
53+
});
54+
55+
test('uses the first element when the array contains multiple packages', () => {
56+
const first = { ...npmViewOutput, description: 'first package' };
57+
const second = { ...npmViewOutput, description: 'second package' };
58+
const info = parseNpmViewOutput(JSON.stringify([first, second]));
59+
assert.ok(info);
60+
assert.strictEqual(info!.description, 'first package');
61+
});
62+
63+
test('falls back to the version field when dist-tags.latest is missing', () => {
64+
const info = parseNpmViewOutput(JSON.stringify([
65+
{ version: '0.0.0-experimental-98e8ed76', time: { '0.0.0-experimental-98e8ed76': '2026-07-25T21:39:01.123Z' } }
66+
]));
67+
assert.ok(info);
68+
assert.strictEqual(info!.version, '0.0.0-experimental-98e8ed76');
69+
assert.strictEqual(info!.time, '2026-07-25T21:39:01.123Z');
70+
assert.strictEqual(info!.description, '');
71+
assert.strictEqual(info!.homepage, undefined);
72+
});
73+
74+
test('returns undefined version and time when neither field is present', () => {
75+
const info = parseNpmViewOutput(JSON.stringify({ description: 'React is a JavaScript library for building user interfaces.' }));
76+
assert.ok(info);
77+
assert.strictEqual(info!.version, undefined);
78+
assert.strictEqual(info!.time, undefined);
79+
});
80+
81+
test('returns empty description when description is missing', () => {
82+
const info = parseNpmViewOutput(JSON.stringify({ 'dist-tags.latest': '19.1.0' }));
83+
assert.ok(info);
84+
assert.strictEqual(info!.description, '');
85+
assert.strictEqual(info!.version, '19.1.0');
86+
});
87+
88+
test('returns undefined time when the resolved version has no matching time entry', () => {
89+
const info = parseNpmViewOutput(JSON.stringify({ 'dist-tags.latest': '19.1.0', time: { '18.3.1': '2024-04-26T09:39:52.159Z' } }));
90+
assert.ok(info);
91+
assert.strictEqual(info!.version, '19.1.0');
92+
assert.strictEqual(info!.time, undefined);
93+
});
94+
95+
test('returns undefined for invalid JSON', () => {
96+
assert.strictEqual(parseNpmViewOutput('not json'), undefined);
97+
assert.strictEqual(parseNpmViewOutput('{'), undefined);
98+
assert.strictEqual(parseNpmViewOutput(''), undefined);
99+
});
100+
101+
test('returns undefined for non-object output', () => {
102+
assert.strictEqual(parseNpmViewOutput('null'), undefined);
103+
assert.strictEqual(parseNpmViewOutput('[]'), undefined);
104+
});
105+
});

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@
111111
"@microsoft/mxc-sdk": "0.7.0",
112112
"@parcel/watcher": "^2.5.6",
113113
"@types/semver": "^7.5.8",
114-
"@vscode/codicons": "^0.0.46-36",
114+
"@vscode/codicons": "^0.0.46-37",
115115
"@vscode/copilot-api": "^0.5.2",
116116
"@vscode/deviceid": "^0.1.1",
117117
"@vscode/diff": "0.0.2-7",

remote/web/package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)