Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
13 changes: 11 additions & 2 deletions extensions/npm/src/readScripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { JSONVisitor, visit } from 'jsonc-parser';
import { Location, Position, Range, TextDocument } from 'vscode';
import { Location, Position, Range, Uri } from 'vscode';

export interface INpmScriptReference {
name: string;
Expand All @@ -18,7 +18,16 @@ export interface INpmScriptInfo {
scripts: INpmScriptReference[];
}

export const readScripts = (document: TextDocument, buffer = document.getText()): INpmScriptInfo | undefined => {
export interface IPositionOffsetTransformer {
readonly uri: Uri;
positionAt(offset: number): Position;
getText?(): string;
}

export const readScripts = (document: IPositionOffsetTransformer & { getText?(): string }, buffer?: string): INpmScriptInfo | undefined => {
if (!buffer) {
buffer = document.getText?.() ?? '';
}
let start: Position | undefined;
let end: Position | undefined;
let inScripts = false;
Expand Down
65 changes: 62 additions & 3 deletions extensions/npm/src/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import * as fs from 'fs';
import minimatch from 'minimatch';
import { Utils } from 'vscode-uri';
import { findPreferredPM } from './preferred-pm';
import { readScripts } from './readScripts';
import { readScripts, IPositionOffsetTransformer } from './readScripts';

const excludeRegex = new RegExp('^(node_modules|.vscode-test)$', 'i');

Expand All @@ -31,6 +31,14 @@ type AutoDetect = 'on' | 'off';

let cachedTasks: ITaskWithLocation[] | undefined = undefined;

interface IPackageJsonCache {
mtime: number;
scripts: ReturnType<typeof readScripts>;
}

// Cache for package.json file reading to avoid unnecessary reads
const packageJsonCache = new Map<string, IPackageJsonCache>();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting there. Now a few more things:

  1. Use an LRU cache for package.json entries (set to 128 items)
  2. Persist the LRU cache in the workspace storage on the vscode.ExtensionContext (passed to the activate() method)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit 2a2cf9a. Implemented PackageJsonLRUCache class with:

  1. 128 entry limit with LRU eviction
  2. Loads from workspace storage on initialization
  3. Persists to workspace storage asynchronously on changes
  4. Initialized in activate() with the ExtensionContext


export const INSTALL_SCRIPT = 'install';

export interface ITaskLocation {
Expand Down Expand Up @@ -88,6 +96,7 @@ export class NpmTaskProvider implements TaskProvider {

export function invalidateTasksCache() {
cachedTasks = undefined;
packageJsonCache.clear();
}

const buildNames: string[] = ['build', 'compile', 'watch'];
Expand Down Expand Up @@ -473,6 +482,39 @@ export function findScriptAtPosition(document: TextDocument, buffer: string, pos
return undefined;
}

// Helper to transform offsets to positions for package.json content
class PositionOffsetTransformer implements IPositionOffsetTransformer {
private readonly lineOffsets: number[];

constructor(public readonly uri: Uri, content: string) {
const lines = content.split(/\r?\n/);
this.lineOffsets = [0];
for (let i = 0; i < lines.length; i++) {
const currentOffset = this.lineOffsets[i] + lines[i].length;
// Check if there's a \r character (Windows line ending)
const nextCharOffset = currentOffset < content.length ? content[currentOffset] : undefined;
const lineEndingLength = nextCharOffset === '\r' ? 2 : 1;
this.lineOffsets.push(currentOffset + lineEndingLength);
}
}

positionAt(offset: number): Position {
let low = 0;
let high = this.lineOffsets.length - 1;
while (low < high) {
const mid = Math.floor((low + high) / 2);
if (this.lineOffsets[mid] > offset) {
high = mid;
} else {
low = mid + 1;
}
}
const line = low - 1;
const character = offset - this.lineOffsets[line];
return new Position(line, character);
}
}

export async function getScripts(packageJsonUri: Uri) {
if (packageJsonUri.scheme !== 'file') {
return undefined;
Expand All @@ -484,8 +526,25 @@ export async function getScripts(packageJsonUri: Uri) {
}

try {
const document: TextDocument = await workspace.openTextDocument(packageJsonUri);
return readScripts(document);
// Check cache with mtime to avoid unnecessary file reads
const stat = await workspace.fs.stat(packageJsonUri);
const cached = packageJsonCache.get(packageJson);

if (cached && cached.mtime === stat.mtime) {
return cached.scripts;
}

// Use workspace.fs.readFile instead of openTextDocument to avoid triggering
// textDocument/didOpen notifications to language servers
const content = await workspace.fs.readFile(packageJsonUri);
const text = Buffer.from(content).toString('utf8');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use TextDecoder here for portability @copilot

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit fc51b35. Replaced Buffer.from(content).toString('utf8') with new TextDecoder('utf-8').decode(content) for better portability.

const transformer = new PositionOffsetTransformer(packageJsonUri, text);
const scripts = readScripts(transformer, text);

// Cache the result
packageJsonCache.set(packageJson, { mtime: stat.mtime, scripts });

return scripts;
} catch (e) {
const localizedParseError = l10n.t("Npm task detection: failed to parse the file {0}", packageJsonUri.fsPath);
throw new Error(localizedParseError);
Expand Down