Skip to content

Commit

Permalink
feat: Add npm.scriptRunner
Browse files Browse the repository at this point in the history
  • Loading branch information
xymopen committed Dec 26, 2024
1 parent 5fdf3d9 commit cd1d2c0
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 53 deletions.
3 changes: 2 additions & 1 deletion extensions/npm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ The extension fetches data from <https://registry.npmjs.org> and <https://regist

- `npm.autoDetect` - Enable detecting scripts as tasks, the default is `on`.
- `npm.runSilent` - Run npm script with the `--silent` option, the default is `false`.
- `npm.packageManager` - The package manager used to run the scripts: `auto`, `npm`, `yarn`, `pnpm` or `bun`. The default is `auto`, which detects your package manager based on files in your workspace.
- `npm.packageManager` - The package manager used to install dependencies: `auto`, `npm`, `yarn`, `pnpm` or `bun`. The default is `auto`, which detects your package manager based on files in your workspace.
- `npm.scriptRunner` - The script runner used to run the scripts: `auto`, `npm`, `yarn`, `pnpm` or `bun`. The default is `auto`, which detects your script runner based on files in your workspace.
- `npm.exclude` - Glob patterns for folders that should be excluded from automatic script detection. The pattern is matched against the **absolute path** of the package.json. For example, to exclude all test folders use '&ast;&ast;/test/&ast;&ast;'.
- `npm.enableScriptExplorer` - Enable an explorer view for npm scripts.
- `npm.scriptExplorerAction` - The default click action: `open` or `run`, the default is `open`.
Expand Down
20 changes: 20 additions & 0 deletions extensions/npm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,26 @@
"default": "auto",
"description": "%config.npm.packageManager%"
},
"npm.scriptRunner": {
"scope": "resource",
"type": "string",
"enum": [
"auto",
"npm",
"yarn",
"pnpm",
"bun"
],
"enumDescriptions": [
"%config.npm.scriptRunner.auto%",
"%config.npm.scriptRunner.npm%",
"%config.npm.scriptRunner.yarn%",
"%config.npm.scriptRunner.pnpm%",
"%config.npm.scriptRunner.bun%"
],
"default": "auto",
"description": "%config.npm.scriptRunner%"
},
"npm.exclude": {
"type": [
"string",
Expand Down
18 changes: 12 additions & 6 deletions extensions/npm/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@
"virtualWorkspaces": "Functionality that requires running the 'npm' command is not available in virtual workspaces.",
"config.npm.autoDetect": "Controls whether npm scripts should be automatically detected.",
"config.npm.runSilent": "Run npm commands with the `--silent` option.",
"config.npm.packageManager": "The package manager used to run scripts.",
"config.npm.packageManager.npm": "Use npm as the package manager for running scripts.",
"config.npm.packageManager.yarn": "Use yarn as the package manager for running scripts.",
"config.npm.packageManager.pnpm": "Use pnpm as the package manager for running scripts.",
"config.npm.packageManager.bun": "Use bun as the package manager for running scripts.",
"config.npm.packageManager.auto": "Auto-detect which package manager to use for running scripts based on lock files and installed package managers.",
"config.npm.packageManager": "The package manager used to install dependencies.",
"config.npm.packageManager.npm": "Use npm as the package manager.",
"config.npm.packageManager.yarn": "Use yarn as the package manager.",
"config.npm.packageManager.pnpm": "Use pnpm as the package manager.",
"config.npm.packageManager.bun": "Use bun as the package manager.",
"config.npm.packageManager.auto": "Auto-detect which package manager to use based on lock files and installed package managers.",
"config.npm.scriptRunner": "The script runner used to run scripts.",
"config.npm.scriptRunner.npm": "Use npm as the script runner.",
"config.npm.scriptRunner.yarn": "Use yarn as the script runner.",
"config.npm.scriptRunner.pnpm": "Use pnpm as the script runner.",
"config.npm.scriptRunner.bun": "Use bun as the script runner.",
"config.npm.scriptRunner.auto": "Auto-detect which script runner to use based on lock files and installed package managers.",
"config.npm.exclude": "Configure glob patterns for folders that should be excluded from automatic script detection.",
"config.npm.enableScriptExplorer": "Enable an explorer view for npm scripts when there is no top-level 'package.json' file.",
"config.npm.scriptExplorerAction": "The default click action used in the NPM Scripts Explorer: `open` or `run`, the default is `open`.",
Expand Down
10 changes: 8 additions & 2 deletions extensions/npm/src/npmMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as vscode from 'vscode';
import { addJSONProviders } from './features/jsonContributions';
import { runSelectedScript, selectAndRunScriptFromFolder } from './commands';
import { NpmScriptsTreeDataProvider } from './npmView';
import { getPackageManager, invalidateTasksCache, NpmTaskProvider, hasPackageJson } from './tasks';
import { getScriptRunner, getPackageManager, invalidateTasksCache, NpmTaskProvider, hasPackageJson } from './tasks';
import { invalidateHoverScriptsCache, NpmScriptHoverProvider } from './scriptHover';
import { NpmScriptLensProvider } from './npmScriptLens';
import which from 'which';
Expand Down Expand Up @@ -63,9 +63,15 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
context.subscriptions.push(vscode.commands.registerCommand('npm.refresh', () => {
invalidateScriptCaches();
}));
context.subscriptions.push(vscode.commands.registerCommand('npm.scriptRunner', (args) => {
if (args instanceof vscode.Uri) {
return getScriptRunner(context, args, true);
}
return '';
}));
context.subscriptions.push(vscode.commands.registerCommand('npm.packageManager', (args) => {
if (args instanceof vscode.Uri) {
return getPackageManager(context, args);
return getPackageManager(context, args, true);
}
return '';
}));
Expand Down
9 changes: 5 additions & 4 deletions extensions/npm/src/npmView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import {
} from 'vscode';
import { readScripts } from './readScripts';
import {
createInstallationTask, getPackageManager, getTaskName, isAutoDetectionEnabled, isWorkspaceFolder, INpmTaskDefinition,
createInstallationTask, getTaskName, isAutoDetectionEnabled, isWorkspaceFolder, INpmTaskDefinition,
NpmTaskProvider,
startDebugging,
detectPackageManager,
ITaskWithLocation,
INSTALL_SCRIPT
} from './tasks';
Expand Down Expand Up @@ -150,8 +151,8 @@ export class NpmScriptsTreeDataProvider implements TreeDataProvider<TreeItem> {
}

private async runScript(script: NpmScript) {
// Call getPackageManager to trigger the multiple lock files warning.
await getPackageManager(this.context, script.getFolder().uri);
// Call detectPackageManager to trigger the multiple lock files warning.
await detectPackageManager(this.context, script.getFolder().uri, true);
tasks.executeTask(script.task);
}

Expand Down Expand Up @@ -181,7 +182,7 @@ export class NpmScriptsTreeDataProvider implements TreeDataProvider<TreeItem> {
if (!uri) {
return;
}
const task = await createInstallationTask(await getPackageManager(this.context, selection.folder.workspaceFolder.uri, true), selection.folder.workspaceFolder, uri);
const task = await createInstallationTask(this.context, selection.folder.workspaceFolder, uri);
tasks.executeTask(task);
}

Expand Down
4 changes: 2 additions & 2 deletions extensions/npm/src/scriptHover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import { INpmScriptInfo, readScripts } from './readScripts';
import {
createScriptRunnerTask,
getPackageManager, startDebugging
startDebugging
} from './tasks';


Expand Down Expand Up @@ -114,7 +114,7 @@ export class NpmScriptHoverProvider implements HoverProvider {
const documentUri = args.documentUri;
const folder = workspace.getWorkspaceFolder(documentUri);
if (folder) {
const task = await createScriptRunnerTask(await getPackageManager(this.context, folder.uri), script, folder, documentUri);
const task = await createScriptRunnerTask(this.context, script, folder, documentUri);
await tasks.executeTask(task);
}
}
Expand Down
95 changes: 57 additions & 38 deletions extensions/npm/src/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ export class NpmTaskProvider implements TaskProvider {
packageJsonUri = _task.scope.uri.with({ path: _task.scope.uri.path + '/package.json' });
}
if (kind.script === INSTALL_SCRIPT) {
return createInstallationTask(await getPackageManager(this.context, _task.scope.uri), _task.scope, packageJsonUri);
return createInstallationTask(this.context, _task.scope, packageJsonUri);
}
return createScriptRunnerTask(await getPackageManager(this.context, _task.scope.uri), kind.script, _task.scope, packageJsonUri);
return createScriptRunnerTask(this.context, kind.script, _task.scope, packageJsonUri);
}
return undefined;
}
Expand Down Expand Up @@ -125,27 +125,43 @@ export function isWorkspaceFolder(value: any): value is WorkspaceFolder {
return value && typeof value !== 'number';
}

export async function getPackageManager(extensionContext: ExtensionContext, folder: Uri, showWarning: boolean = true): Promise<string> {
let packageManagerName = workspace.getConfiguration('npm', folder).get<string>('packageManager', 'npm');

if (packageManagerName === 'auto') {
const { name, multipleLockFilesDetected: multiplePMDetected } = await findPreferredPM(folder.fsPath);
packageManagerName = name;
const neverShowWarning = 'npm.multiplePMWarning.neverShow';
if (showWarning && multiplePMDetected && !extensionContext.globalState.get<boolean>(neverShowWarning)) {
const multiplePMWarning = l10n.t('Using {0} as the preferred package manager. Found multiple lockfiles for {1}. To resolve this issue, delete the lockfiles that don\'t match your preferred package manager or change the setting "npm.packageManager" to a value other than "auto".', packageManagerName, folder.fsPath);
const neverShowAgain = l10n.t("Do not show again");
const learnMore = l10n.t("Learn more");
window.showInformationMessage(multiplePMWarning, learnMore, neverShowAgain).then(result => {
switch (result) {
case neverShowAgain: extensionContext.globalState.update(neverShowWarning, true); break;
case learnMore: env.openExternal(Uri.parse('https://docs.npmjs.com/cli/v9/configuring-npm/package-lock-json'));
}
});
}
export async function getScriptRunner(context: ExtensionContext, folder: Uri, showWarning: boolean): Promise<string> {
let scriptRunner = workspace.getConfiguration('npm', folder).get<string>('scriptRunner', 'npm');

if (scriptRunner === 'auto') {
scriptRunner = await detectPackageManager(context, folder, showWarning);
}

return packageManagerName;
return scriptRunner;
}

export async function getPackageManager(context: ExtensionContext, folder: Uri, showWarning: boolean): Promise<string> {
let packageManager = workspace.getConfiguration('npm', folder).get<string>('packageManager', 'npm');

if (packageManager === 'auto') {
packageManager = await detectPackageManager(context, folder, showWarning);
}

return packageManager;
}

export async function detectPackageManager(extensionContext: ExtensionContext, folder: Uri, showWarning: boolean): Promise<string> {
const { name, multipleLockFilesDetected: multiplePMDetected } = await findPreferredPM(folder.fsPath);
const neverShowWarning = 'npm.multiplePMWarning.neverShow';
if (showWarning && multiplePMDetected && !extensionContext.globalState.get<boolean>(neverShowWarning)) {
// todo: add text for npm.scriptRunner?
const multiplePMWarning = l10n.t('Using {0} as the preferred package manager. Found multiple lockfiles for {1}. To resolve this issue, delete the lockfiles that don\'t match your preferred package manager or change the setting "npm.packageManager" to a value other than "auto".', name, folder.fsPath);
const neverShowAgain = l10n.t("Do not show again");
const learnMore = l10n.t("Learn more");
window.showInformationMessage(multiplePMWarning, learnMore, neverShowAgain).then(result => {
switch (result) {
case neverShowAgain: extensionContext.globalState.update(neverShowWarning, true); break;
case learnMore: env.openExternal(Uri.parse('https://docs.npmjs.com/cli/v9/configuring-npm/package-lock-json'));
}
});
}

return name;
}

export async function hasNpmScripts(): Promise<boolean> {
Expand All @@ -169,15 +185,13 @@ export async function hasNpmScripts(): Promise<boolean> {
}
}

async function detectNpmScripts(context: ExtensionContext, showWarning: boolean): Promise<ITaskWithLocation[]> {
async function* findNpmPackages(): AsyncGenerator<Uri> {

const emptyTasks: ITaskWithLocation[] = [];
const allTasks: ITaskWithLocation[] = [];
const visitedPackageJsonFiles: Set<string> = new Set();

const folders = workspace.workspaceFolders;
if (!folders) {
return emptyTasks;
return;
}
try {
for (const folder of folders) {
Expand All @@ -186,14 +200,12 @@ async function detectNpmScripts(context: ExtensionContext, showWarning: boolean)
const paths = await workspace.findFiles(relativePattern, '**/{node_modules,.vscode-test}/**');
for (const path of paths) {
if (!isExcluded(folder, path) && !visitedPackageJsonFiles.has(path.fsPath)) {
const tasks = await provideNpmScriptsForFolder(context, path, showWarning);
yield path;
visitedPackageJsonFiles.add(path.fsPath);
allTasks.push(...tasks);
}
}
}
}
return allTasks;
} catch (error) {
return Promise.reject(error);
}
Expand Down Expand Up @@ -227,7 +239,12 @@ export async function detectNpmScriptsForFolder(context: ExtensionContext, folde

export async function provideNpmScripts(context: ExtensionContext, showWarning: boolean): Promise<ITaskWithLocation[]> {
if (!cachedTasks) {
cachedTasks = await detectNpmScripts(context, showWarning);
const allTasks: ITaskWithLocation[] = [];
for await (const path of findNpmPackages()) {
const tasks = await provideNpmScriptsForFolder(context, path, showWarning);
allTasks.push(...tasks);
}
cachedTasks = allTasks;
}
return cachedTasks;
}
Expand Down Expand Up @@ -277,15 +294,13 @@ async function provideNpmScriptsForFolder(context: ExtensionContext, packageJson

const result: ITaskWithLocation[] = [];

const packageManager = await getPackageManager(context, folder.uri, showWarning);

for (const { name, value, nameRange } of scripts.scripts) {
const task = await createScriptRunnerTask(packageManager, name, folder!, packageJsonUri, value);
const task = await createScriptRunnerTask(context, name, folder!, packageJsonUri, value, showWarning);
result.push({ task, location: new Location(packageJsonUri, nameRange) });
}

if (!workspace.getConfiguration('npm', folder).get<string[]>('scriptExplorerExclude', []).find(e => e.includes(INSTALL_SCRIPT))) {
result.push({ task: await createInstallationTask(packageManager, folder, packageJsonUri, 'install dependencies from package') });
result.push({ task: await createInstallationTask(context, folder, packageJsonUri, 'install dependencies from package', showWarning) });
}
return result;
}
Expand Down Expand Up @@ -317,7 +332,8 @@ function getRelativePath(rootUri: Uri, packageJsonUri: Uri): string {
return absolutePath.substring(rootUri.path.length + 1);
}

export async function createScriptRunnerTask(packageManager: string, script: string, folder: WorkspaceFolder, packageJsonUri: Uri, scriptValue?: string): Promise<Task> {
export async function createScriptRunnerTask(context: ExtensionContext, script: string, folder: WorkspaceFolder, packageJsonUri: Uri, scriptValue?: string, showWarning = true): Promise<Task> {
const scriptRunner = await getScriptRunner(context, folder.uri, showWarning);
let kind: INpmTaskDefinition = { type: 'npm', script };

const relativePackageJson = getRelativePath(folder.uri, packageJsonUri);
Expand All @@ -326,7 +342,7 @@ export async function createScriptRunnerTask(packageManager: string, script: str
}
const taskName = getTaskName(script, relativePackageJson);
const cwd = path.dirname(packageJsonUri.fsPath);
const task = new Task(kind, folder, taskName, 'npm', new ShellExecution(packageManager, getCommandLine(folder.uri, ['run', script]), { cwd: cwd }));
const task = new Task(kind, folder, taskName, 'npm', new ShellExecution(scriptRunner, getCommandLine(folder.uri, ['run', script]), { cwd: cwd }));
task.detail = scriptValue;

const lowerCaseTaskName = script.toLowerCase();
Expand All @@ -343,7 +359,8 @@ export async function createScriptRunnerTask(packageManager: string, script: str
return task;
}

export async function createInstallationTask(packageManager: string, folder: WorkspaceFolder, packageJsonUri: Uri, scriptValue?: string): Promise<Task> {
export async function createInstallationTask(context: ExtensionContext, folder: WorkspaceFolder, packageJsonUri: Uri, scriptValue?: string, showWarning = true): Promise<Task> {
const packageManager = await getPackageManager(context, folder.uri, showWarning);
const kind: INpmTaskDefinition = { type: 'npm', script: INSTALL_SCRIPT };

const relativePackageJson = getRelativePath(folder.uri, packageJsonUri);
Expand Down Expand Up @@ -411,15 +428,17 @@ export async function runScript(context: ExtensionContext, script: string, docum
const uri = document.uri;
const folder = workspace.getWorkspaceFolder(uri);
if (folder) {
const task = await createScriptRunnerTask(await getPackageManager(context, folder.uri), script, folder, uri);
const task = await createScriptRunnerTask(context, script, folder, uri);
tasks.executeTask(task);
}
}

export async function startDebugging(context: ExtensionContext, scriptName: string, cwd: string, folder: WorkspaceFolder) {
let scriptRunner = await getScriptRunner(context, folder.uri, true);

commands.executeCommand(
'extension.js-debug.createDebuggerTerminal',
`${await getPackageManager(context, folder.uri)} run ${scriptName}`,
`${scriptRunner} run ${scriptName}`,
folder,
{ cwd },
);
Expand Down

0 comments on commit cd1d2c0

Please sign in to comment.