Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c666bd5
Avoid more context key creation from chat inline anchor
roblourens Mar 1, 2026
5022ba5
Merge pull request #298605 from microsoft/roblou/yielding-crayfish
roblourens Mar 1, 2026
d63b8bd
chat: wrap markdown tables with horizontal scrollbar (#298604)
roblourens Mar 1, 2026
f85c0b3
improves npm caching (#298608)
hediet Mar 2, 2026
b753be8
customizations: allow transient selection of the folder to explore cu…
joshspicer Mar 2, 2026
c0ba041
Add progress content part component fixture
roblourens Mar 2, 2026
e322dd2
Merge branch 'main' into roblou/specified-narwhal
roblourens Mar 2, 2026
6278881
fix: enhance chat mode resolution by adding mode name lookup (#298225)
DonJayamanne Mar 2, 2026
190ed29
Remove edits2 setting (#298619)
roblourens Mar 2, 2026
bac17f9
Merge pull request #298638 from microsoft/roblou/specified-narwhal
roblourens Mar 2, 2026
f69012e
fix: include error handling in inline chat session overlay logic (#29…
jrieken Mar 2, 2026
a432b6a
Bump minimatch from 9.0.5 to 9.0.9 in /build/npm/gyp (#298336)
dependabot[bot] Mar 2, 2026
fd63d2a
Bump minimatch in /build (#298315)
dependabot[bot] Mar 2, 2026
35a6bb3
Bump minimatch (#298537)
dependabot[bot] Mar 2, 2026
d1d66da
Bump minimatch from 3.1.2 to 3.1.5 in /test/integration/browser (#298…
dependabot[bot] Mar 2, 2026
d2a693d
fix missing sourcemaps (#298660)
joaomoreno Mar 2, 2026
c95cc7a
Fix Guardian errors in sanity tests stage (#298662)
dmitrivMS Mar 2, 2026
43755b4
fix(json.schemaDownload.trustedDomains): avoid always update json.sch…
cathaysia Mar 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 94 additions & 10 deletions .vscode/extensions/vscode-extras/src/npmUpToDateFeature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface PostinstallState {

interface InstallState {
readonly root: string;
readonly stateContentsFile: string;
readonly current: PostinstallState;
readonly saved: PostinstallState | undefined;
readonly files: readonly string[];
Expand All @@ -29,6 +30,10 @@ export class NpmUpToDateFeature extends vscode.Disposable {
private readonly _disposables: vscode.Disposable[] = [];
private _watchers: fs.FSWatcher[] = [];
private _terminal: vscode.Terminal | undefined;
private _stateContentsFile: string | undefined;
private _root: string | undefined;

private static readonly _scheme = 'npm-dep-state';

constructor(private readonly _output: vscode.LogOutputChannel) {
const disposables: vscode.Disposable[] = [];
Expand All @@ -48,10 +53,28 @@ export class NpmUpToDateFeature extends vscode.Disposable {
this._statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
this._disposables.push(this._statusBarItem);

this._disposables.push(
vscode.workspace.registerTextDocumentContentProvider(NpmUpToDateFeature._scheme, {
provideTextDocumentContent: (uri) => {
const params = new URLSearchParams(uri.query);
const source = params.get('source');
const file = uri.path.slice(1); // strip leading /
if (source === 'saved') {
return this._readSavedContent(file);
}
return this._readCurrentContent(file);
}
})
);

this._disposables.push(
vscode.commands.registerCommand('vscode-extras.runNpmInstall', () => this._runNpmInstall())
);

this._disposables.push(
vscode.commands.registerCommand('vscode-extras.showDependencyDiff', (file: string) => this._showDiff(file))
);

this._disposables.push(
vscode.window.onDidCloseTerminal(t => {
if (t === this._terminal) {
Expand All @@ -66,8 +89,7 @@ export class NpmUpToDateFeature extends vscode.Disposable {

private _runNpmInstall(): void {
if (this._terminal) {
this._terminal.show();
return;
this._terminal.dispose();
}
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri;
if (!workspaceRoot) {
Expand Down Expand Up @@ -113,6 +135,8 @@ export class NpmUpToDateFeature extends vscode.Disposable {
return;
}

this._stateContentsFile = state.stateContentsFile;
this._root = state.root;
this._setupWatcher(state);

const changedFiles = this._getChangedFiles(state);
Expand All @@ -123,28 +147,88 @@ export class NpmUpToDateFeature extends vscode.Disposable {
} else {
this._statusBarItem.text = '$(warning) node_modules is stale - run npm i';
const tooltip = new vscode.MarkdownString();
tooltip.appendText('Dependencies are out of date. Click to run npm install.\n\nChanged files:\n');
for (const file of changedFiles) {
tooltip.appendText(` • ${file}\n`);
tooltip.isTrusted = true;
tooltip.supportHtml = true;
tooltip.appendMarkdown('**Dependencies are out of date.** Click to run npm install.\n\nChanged files:\n\n');
for (const entry of changedFiles) {
if (entry.isFile) {
const args = encodeURIComponent(JSON.stringify(entry.label));
tooltip.appendMarkdown(`- [${entry.label}](command:vscode-extras.showDependencyDiff?${args})\n`);
} else {
tooltip.appendMarkdown(`- ${entry.label}\n`);
}
}
this._statusBarItem.tooltip = tooltip;
this._statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
this._statusBarItem.show();
}
}

private _getChangedFiles(state: InstallState): string[] {
private _showDiff(file: string): void {
const cacheBuster = Date.now().toString();
const savedUri = vscode.Uri.from({
scheme: NpmUpToDateFeature._scheme,
path: `/${file}`,
query: new URLSearchParams({ source: 'saved', t: cacheBuster }).toString(),
});
const currentUri = vscode.Uri.from({
scheme: NpmUpToDateFeature._scheme,
path: `/${file}`,
query: new URLSearchParams({ source: 'current', t: cacheBuster }).toString(),
});

vscode.commands.executeCommand('vscode.diff', savedUri, currentUri, `${file} (last install ↔ current)`);
}

private _readSavedContent(file: string): string {
if (!this._stateContentsFile) {
return '';
}
try {
const contents: Record<string, string> = JSON.parse(fs.readFileSync(this._stateContentsFile, 'utf8'));
return contents[file] ?? '';
} catch {
return '';
}
}

private _readCurrentContent(file: string): string {
if (!this._root) {
return '';
}
try {
return this._normalizeFileContent(path.join(this._root, file));
} catch {
return '';
}
}

private _normalizeFileContent(filePath: string): string {
const raw = fs.readFileSync(filePath, 'utf8');
if (path.basename(filePath) === 'package.json') {
const json = JSON.parse(raw);
for (const key of NpmUpToDateFeature._packageJsonIgnoredKeys) {
delete json[key];
}
return JSON.stringify(json, null, '\t') + '\n';
}
return raw;
}

private static readonly _packageJsonIgnoredKeys = ['distro'];

private _getChangedFiles(state: InstallState): { readonly label: string; readonly isFile: boolean }[] {
if (!state.saved) {
return ['(no postinstall state found)'];
return [{ label: '(no postinstall state found)', isFile: false }];
}
const changed: string[] = [];
const changed: { readonly label: string; readonly isFile: boolean }[] = [];
if (state.saved.nodeVersion !== state.current.nodeVersion) {
changed.push(`Node.js version (${state.saved.nodeVersion} → ${state.current.nodeVersion})`);
changed.push({ label: `Node.js version (${state.saved.nodeVersion} → ${state.current.nodeVersion})`, isFile: false });
}
const allKeys = new Set([...Object.keys(state.current.fileHashes), ...Object.keys(state.saved.fileHashes)]);
for (const key of allKeys) {
if (state.current.fileHashes[key] !== state.saved.fileHashes[key]) {
changed.push(key);
changed.push({ label: key, isFile: true });
}
}
return changed;
Expand Down
2 changes: 1 addition & 1 deletion build/azure-pipelines/common/sanity-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
- checkout: self
fetchDepth: 1
fetchTags: false
sparseCheckoutDirectories: test/sanity .nvmrc
sparseCheckoutDirectories: build/azure-pipelines/config test/sanity .nvmrc
displayName: Checkout test/sanity

- ${{ if eq(parameters.os, 'windows') }}:
Expand Down
2 changes: 1 addition & 1 deletion build/azure-pipelines/product-quality-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ jobs:
continueOnError: true
condition: and(succeeded(), eq(lower(variables['VSCODE_PUBLISH']), 'true'))

- script: npm exec -- npm-run-all2 -lp hygiene eslint valid-layers-check define-class-fields-check vscode-dts-compile-check tsec-compile-check test-build-scripts
- script: npm exec -- npm-run-all2 -lp core-ci hygiene eslint valid-layers-check define-class-fields-check vscode-dts-compile-check tsec-compile-check test-build-scripts
env:
GITHUB_TOKEN: "$(github-distro-mixin-password)"
displayName: Compile & Hygiene
Expand Down
8 changes: 4 additions & 4 deletions build/npm/gyp/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 47 additions & 5 deletions build/npm/installStateHash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import { dirs } from './dirs.ts';

export const root = fs.realpathSync.native(path.dirname(path.dirname(import.meta.dirname)));
export const stateFile = path.join(root, 'node_modules', '.postinstall-state');
export const stateContentsFile = path.join(root, 'node_modules', '.postinstall-state-contents');
export const forceInstallMessage = 'Run \x1b[36mnode build/npm/fast-install.ts --force\x1b[0m to force a full install.';

export function collectInputFiles(): string[] {
const files: string[] = [];

for (const dir of dirs) {
const base = dir === '' ? root : path.join(root, dir);
for (const file of ['package.json', '.npmrc']) {
for (const file of ['package.json', 'package-lock.json', '.npmrc']) {
const filePath = path.join(base, file);
if (fs.existsSync(filePath)) {
files.push(filePath);
Expand All @@ -35,23 +36,55 @@ export interface PostinstallState {
readonly fileHashes: Record<string, string>;
}

function hashFileContent(filePath: string): string {
const packageJsonIgnoredKeys = new Set(['distro']);

function normalizeFileContent(filePath: string): string {
const raw = fs.readFileSync(filePath, 'utf8');
if (path.basename(filePath) === 'package.json') {
const json = JSON.parse(raw);
for (const key of packageJsonIgnoredKeys) {
delete json[key];
}
return JSON.stringify(json, null, '\t') + '\n';
}
return raw;
}

function hashContent(content: string): string {
const hash = crypto.createHash('sha256');
hash.update(fs.readFileSync(filePath));
hash.update(content);
return hash.digest('hex');
}

export function computeState(): PostinstallState {
const fileHashes: Record<string, string> = {};
for (const filePath of collectInputFiles()) {
fileHashes[path.relative(root, filePath)] = hashFileContent(filePath);
const key = path.relative(root, filePath);
try {
fileHashes[key] = hashContent(normalizeFileContent(filePath));
} catch {
// file may not be readable
}
}
return { nodeVersion: process.versions.node, fileHashes };
}

export function computeContents(): Record<string, string> {
const fileContents: Record<string, string> = {};
for (const filePath of collectInputFiles()) {
try {
fileContents[path.relative(root, filePath)] = normalizeFileContent(filePath);
} catch {
// file may not be readable
}
}
return fileContents;
}

export function readSavedState(): PostinstallState | undefined {
try {
return JSON.parse(fs.readFileSync(stateFile, 'utf8'));
const { nodeVersion, fileHashes } = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
return { nodeVersion, fileHashes };
} catch {
return undefined;
}
Expand All @@ -67,10 +100,19 @@ export function isUpToDate(): boolean {
&& JSON.stringify(saved.fileHashes) === JSON.stringify(current.fileHashes);
}

export function readSavedContents(): Record<string, string> | undefined {
try {
return JSON.parse(fs.readFileSync(stateContentsFile, 'utf8'));
} catch {
return undefined;
}
}

// When run directly, output state as JSON for tooling (e.g. the vscode-extras extension).
if (import.meta.filename === process.argv[1]) {
console.log(JSON.stringify({
root,
stateContentsFile,
current: computeState(),
saved: readSavedState(),
files: [...collectInputFiles(), stateFile],
Expand Down
3 changes: 2 additions & 1 deletion build/npm/postinstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import path from 'path';
import * as os from 'os';
import * as child_process from 'child_process';
import { dirs } from './dirs.ts';
import { root, stateFile, computeState, isUpToDate } from './installStateHash.ts';
import { root, stateFile, stateContentsFile, computeState, computeContents, isUpToDate } from './installStateHash.ts';

const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const rootNpmrcConfigKeys = getNpmrcConfigKeys(path.join(root, '.npmrc'));
Expand Down Expand Up @@ -287,6 +287,7 @@ async function main() {
child_process.execSync('git config blame.ignoreRevsFile .git-blame-ignore-revs');

fs.writeFileSync(stateFile, JSON.stringify(_state));
fs.writeFileSync(stateContentsFile, JSON.stringify(computeContents()));
}

main().catch(err => {
Expand Down
Loading
Loading