Skip to content

Commit 5b7dafc

Browse files
authored
Add vscode-extras extension with npm up-to-date feature and related configurations (#298295)
1 parent 98ad6b6 commit 5b7dafc

File tree

12 files changed

+548
-65
lines changed

12 files changed

+548
-65
lines changed

.vscode/extensions/vscode-extras/package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "vscode-extras",
3+
"displayName": "VS Code Extras",
4+
"description": "Extra utility features for the VS Code selfhost workspace",
5+
"engines": {
6+
"vscode": "^1.88.0"
7+
},
8+
"version": "0.0.1",
9+
"publisher": "ms-vscode",
10+
"categories": [
11+
"Other"
12+
],
13+
"activationEvents": [
14+
"workspaceContains:src/vscode-dts/vscode.d.ts"
15+
],
16+
"main": "./out/extension.js",
17+
"repository": {
18+
"type": "git",
19+
"url": "https://github.com/microsoft/vscode.git"
20+
},
21+
"license": "MIT",
22+
"scripts": {
23+
"compile": "gulp compile-extension:vscode-extras",
24+
"watch": "gulp watch-extension:vscode-extras"
25+
},
26+
"contributes": {
27+
"configuration": {
28+
"title": "VS Code Extras",
29+
"properties": {
30+
"vscode-extras.npmUpToDateFeature.enabled": {
31+
"type": "boolean",
32+
"default": true,
33+
"description": "Show a status bar warning when npm dependencies are out of date."
34+
}
35+
}
36+
}
37+
}
38+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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 vscode from 'vscode';
7+
import { NpmUpToDateFeature } from './npmUpToDateFeature';
8+
9+
export class Extension extends vscode.Disposable {
10+
private readonly _output: vscode.LogOutputChannel;
11+
private _npmFeature: NpmUpToDateFeature | undefined;
12+
13+
constructor(context: vscode.ExtensionContext) {
14+
const disposables: vscode.Disposable[] = [];
15+
super(() => disposables.forEach(d => d.dispose()));
16+
17+
this._output = vscode.window.createOutputChannel('VS Code Extras', { log: true });
18+
disposables.push(this._output);
19+
20+
this._updateNpmFeature();
21+
22+
disposables.push(
23+
vscode.workspace.onDidChangeConfiguration(e => {
24+
if (e.affectsConfiguration('vscode-extras.npmUpToDateFeature.enabled')) {
25+
this._updateNpmFeature();
26+
}
27+
})
28+
);
29+
}
30+
31+
private _updateNpmFeature(): void {
32+
const enabled = vscode.workspace.getConfiguration('vscode-extras').get<boolean>('npmUpToDateFeature.enabled', true);
33+
if (enabled && !this._npmFeature) {
34+
this._npmFeature = new NpmUpToDateFeature(this._output);
35+
} else if (!enabled && this._npmFeature) {
36+
this._npmFeature.dispose();
37+
this._npmFeature = undefined;
38+
}
39+
}
40+
}
41+
42+
let extension: Extension | undefined;
43+
44+
export function activate(context: vscode.ExtensionContext) {
45+
extension = new Extension(context);
46+
context.subscriptions.push(extension);
47+
}
48+
49+
export function deactivate() {
50+
extension = undefined;
51+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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 cp from 'child_process';
7+
import * as fs from 'fs';
8+
import * as path from 'path';
9+
import * as vscode from 'vscode';
10+
11+
interface FileHashes {
12+
readonly [relativePath: string]: string;
13+
}
14+
15+
interface PostinstallState {
16+
readonly nodeVersion: string;
17+
readonly fileHashes: FileHashes;
18+
}
19+
20+
interface InstallState {
21+
readonly root: string;
22+
readonly current: PostinstallState;
23+
readonly saved: PostinstallState | undefined;
24+
readonly files: readonly string[];
25+
}
26+
27+
export class NpmUpToDateFeature extends vscode.Disposable {
28+
private readonly _statusBarItem: vscode.StatusBarItem;
29+
private readonly _disposables: vscode.Disposable[] = [];
30+
private _watchers: fs.FSWatcher[] = [];
31+
private _terminal: vscode.Terminal | undefined;
32+
33+
constructor(private readonly _output: vscode.LogOutputChannel) {
34+
const disposables: vscode.Disposable[] = [];
35+
super(() => {
36+
disposables.forEach(d => d.dispose());
37+
for (const w of this._watchers) {
38+
w.close();
39+
}
40+
});
41+
this._disposables = disposables;
42+
43+
this._statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 10000);
44+
this._statusBarItem.name = 'npm Install State';
45+
this._statusBarItem.text = '$(warning) node_modules is stale - run npm i';
46+
this._statusBarItem.tooltip = 'Dependencies are out of date. Click to run npm install.';
47+
this._statusBarItem.command = 'vscode-extras.runNpmInstall';
48+
this._statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
49+
this._disposables.push(this._statusBarItem);
50+
51+
this._disposables.push(
52+
vscode.commands.registerCommand('vscode-extras.runNpmInstall', () => this._runNpmInstall())
53+
);
54+
55+
this._disposables.push(
56+
vscode.window.onDidCloseTerminal(t => {
57+
if (t === this._terminal) {
58+
this._terminal = undefined;
59+
this._check();
60+
}
61+
})
62+
);
63+
64+
this._check();
65+
}
66+
67+
private _runNpmInstall(): void {
68+
if (this._terminal) {
69+
this._terminal.show();
70+
return;
71+
}
72+
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri;
73+
if (!workspaceRoot) {
74+
return;
75+
}
76+
this._terminal = vscode.window.createTerminal({ name: 'npm install', cwd: workspaceRoot });
77+
this._terminal.sendText('node build/npm/fast-install.ts --force');
78+
this._terminal.show();
79+
80+
this._statusBarItem.text = '$(loading~spin) npm i';
81+
this._statusBarItem.tooltip = 'npm install is running...';
82+
this._statusBarItem.backgroundColor = undefined;
83+
this._statusBarItem.command = 'vscode-extras.runNpmInstall';
84+
}
85+
86+
private _queryState(): InstallState | undefined {
87+
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
88+
if (!workspaceRoot) {
89+
return undefined;
90+
}
91+
try {
92+
const script = path.join(workspaceRoot, 'build', 'npm', 'installStateHash.ts');
93+
const output = cp.execFileSync(process.execPath, [script], {
94+
cwd: workspaceRoot,
95+
timeout: 10_000,
96+
encoding: 'utf8',
97+
});
98+
const parsed = JSON.parse(output.trim());
99+
this._output.trace('raw output:', output.trim());
100+
return parsed;
101+
} catch (e) {
102+
this._output.error('_queryState error:', e as any);
103+
return undefined;
104+
}
105+
}
106+
107+
private _check(): void {
108+
const state = this._queryState();
109+
this._output.trace('state:', JSON.stringify(state, null, 2));
110+
if (!state) {
111+
this._output.trace('no state, hiding');
112+
this._statusBarItem.hide();
113+
return;
114+
}
115+
116+
this._setupWatcher(state);
117+
118+
const changedFiles = this._getChangedFiles(state);
119+
this._output.trace('changedFiles:', JSON.stringify(changedFiles));
120+
121+
if (changedFiles.length === 0) {
122+
this._statusBarItem.hide();
123+
} else {
124+
this._statusBarItem.text = '$(warning) node_modules is stale - run npm i';
125+
const tooltip = new vscode.MarkdownString();
126+
tooltip.appendText('Dependencies are out of date. Click to run npm install.\n\nChanged files:\n');
127+
for (const file of changedFiles) {
128+
tooltip.appendText(` • ${file}\n`);
129+
}
130+
this._statusBarItem.tooltip = tooltip;
131+
this._statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
132+
this._statusBarItem.show();
133+
}
134+
}
135+
136+
private _getChangedFiles(state: InstallState): string[] {
137+
if (!state.saved) {
138+
return ['(no postinstall state found)'];
139+
}
140+
const changed: string[] = [];
141+
if (state.saved.nodeVersion !== state.current.nodeVersion) {
142+
changed.push(`Node.js version (${state.saved.nodeVersion}${state.current.nodeVersion})`);
143+
}
144+
const allKeys = new Set([...Object.keys(state.current.fileHashes), ...Object.keys(state.saved.fileHashes)]);
145+
for (const key of allKeys) {
146+
if (state.current.fileHashes[key] !== state.saved.fileHashes[key]) {
147+
changed.push(key);
148+
}
149+
}
150+
return changed;
151+
}
152+
153+
private _setupWatcher(state: InstallState): void {
154+
for (const w of this._watchers) {
155+
w.close();
156+
}
157+
this._watchers = [];
158+
159+
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
160+
const scheduleCheck = () => {
161+
if (debounceTimer) {
162+
clearTimeout(debounceTimer);
163+
}
164+
debounceTimer = setTimeout(() => this._check(), 500);
165+
};
166+
167+
for (const file of state.files) {
168+
try {
169+
const watcher = fs.watch(file, scheduleCheck);
170+
this._watchers.push(watcher);
171+
} catch {
172+
// file may not exist yet
173+
}
174+
}
175+
}
176+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"extends": "../../../extensions/tsconfig.base.json",
3+
"compilerOptions": {
4+
"rootDir": "./src",
5+
"outDir": "./out",
6+
"types": [
7+
"node"
8+
]
9+
},
10+
"include": [
11+
"src/**/*",
12+
"../../../src/vscode-dts/vscode.d.ts"
13+
]
14+
}

.vscode/extensions/vscode-selfhost-test-provider/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
"testObserver",
77
"testRelatedCode"
88
],
9+
"extensionDependencies": [
10+
"ms-vscode.vscode-extras"
11+
],
912
"engines": {
1013
"vscode": "^1.88.0"
1114
},

build/gulpfile.extensions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ const compilations = [
9595

9696
'.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json',
9797
'.vscode/extensions/vscode-selfhost-import-aid/tsconfig.json',
98+
'.vscode/extensions/vscode-extras/tsconfig.json',
9899
];
99100

100101
const getBaseUrl = (out: string) => `https://main.vscode-cdn.net/sourcemaps/${commit}/${out}`;

build/npm/dirs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export const dirs = [
6060
'test/mcp',
6161
'.vscode/extensions/vscode-selfhost-import-aid',
6262
'.vscode/extensions/vscode-selfhost-test-provider',
63+
'.vscode/extensions/vscode-extras',
6364
];
6465

6566
if (existsSync(`${import.meta.dirname}/../../.build/distro/npm`)) {

build/npm/fast-install.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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 child_process from 'child_process';
7+
import { root, isUpToDate, forceInstallMessage } from './installStateHash.ts';
8+
9+
if (!process.argv.includes('--force') && isUpToDate()) {
10+
console.log(`\x1b[32mAll dependencies up to date.\x1b[0m ${forceInstallMessage}`);
11+
process.exit(0);
12+
}
13+
14+
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
15+
const result = child_process.spawnSync(npm, ['install'], {
16+
cwd: root,
17+
stdio: 'inherit',
18+
shell: true,
19+
env: { ...process.env, VSCODE_FORCE_INSTALL: '1' },
20+
});
21+
22+
process.exit(result.status ?? 1);

0 commit comments

Comments
 (0)