diff --git a/extensions/npm/README.md b/extensions/npm/README.md index 215ca927ff434..d5538706019be 100644 --- a/extensions/npm/README.md +++ b/extensions/npm/README.md @@ -34,7 +34,8 @@ The extension fetches data from and 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(args, context, true); + } + return ''; + })); context.subscriptions.push(vscode.commands.registerCommand('npm.packageManager', (args) => { if (args instanceof vscode.Uri) { - return getPackageManager(context, args); + return getPackageManager(args, context, true); } return ''; })); diff --git a/extensions/npm/src/npmScriptLens.ts b/extensions/npm/src/npmScriptLens.ts index c8e506904f882..f02502e8f990d 100644 --- a/extensions/npm/src/npmScriptLens.ts +++ b/extensions/npm/src/npmScriptLens.ts @@ -15,8 +15,8 @@ import { workspace, l10n } from 'vscode'; -import { findPreferredPM } from './preferred-pm'; import { readScripts } from './readScripts'; +import { getScriptRunner } from './tasks'; const enum Constants { @@ -87,7 +87,7 @@ export class NpmScriptLensProvider implements CodeLensProvider, Disposable { } if (this.lensLocation === 'all') { - const packageManager = await findPreferredPM(Uri.joinPath(document.uri, '..').fsPath); + const scriptRunner = await getScriptRunner(Uri.joinPath(document.uri, '..')); return tokens.scripts.map( ({ name, nameRange }) => new CodeLens( @@ -95,7 +95,7 @@ export class NpmScriptLensProvider implements CodeLensProvider, Disposable { { title, command: 'extension.js-debug.createDebuggerTerminal', - arguments: [`${packageManager.name} run ${name}`, workspace.getWorkspaceFolder(document.uri), { cwd }], + arguments: [`${scriptRunner} ${scriptRunner === 'node' ? '--run' : 'run'} ${name}`, workspace.getWorkspaceFolder(document.uri), { cwd }], }, ), ); diff --git a/extensions/npm/src/npmView.ts b/extensions/npm/src/npmView.ts index e041b43f0919e..027d7f60e5487 100644 --- a/extensions/npm/src/npmView.ts +++ b/extensions/npm/src/npmView.ts @@ -13,9 +13,10 @@ import { } from 'vscode'; import { readScripts } from './readScripts'; import { - createTask, getPackageManager, getTaskName, isAutoDetectionEnabled, isWorkspaceFolder, INpmTaskDefinition, + createInstallationTask, getTaskName, isAutoDetectionEnabled, isWorkspaceFolder, INpmTaskDefinition, NpmTaskProvider, startDebugging, + detectPackageManager, ITaskWithLocation, INSTALL_SCRIPT } from './tasks'; @@ -150,8 +151,8 @@ export class NpmScriptsTreeDataProvider implements TreeDataProvider { } 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(script.getFolder().uri, this.context, true); tasks.executeTask(script.task); } @@ -181,7 +182,7 @@ export class NpmScriptsTreeDataProvider implements TreeDataProvider { if (!uri) { return; } - const task = await createTask(await getPackageManager(this.context, selection.folder.workspaceFolder.uri, true), 'install', ['install'], selection.folder.workspaceFolder, uri, undefined, []); + const task = await createInstallationTask(this.context, selection.folder.workspaceFolder, uri); tasks.executeTask(task); } diff --git a/extensions/npm/src/scriptHover.ts b/extensions/npm/src/scriptHover.ts index b3a87e0519821..33f2346e81588 100644 --- a/extensions/npm/src/scriptHover.ts +++ b/extensions/npm/src/scriptHover.ts @@ -12,8 +12,8 @@ import { } from 'vscode'; import { INpmScriptInfo, readScripts } from './readScripts'; import { - createTask, - getPackageManager, startDebugging + createScriptRunnerTask, + startDebugging } from './tasks'; @@ -114,7 +114,7 @@ export class NpmScriptHoverProvider implements HoverProvider { const documentUri = args.documentUri; const folder = workspace.getWorkspaceFolder(documentUri); if (folder) { - const task = await createTask(await getPackageManager(this.context, folder.uri), script, ['run', script], folder, documentUri); + const task = await createScriptRunnerTask(this.context, script, folder, documentUri); await tasks.executeTask(task); } } diff --git a/extensions/npm/src/tasks.ts b/extensions/npm/src/tasks.ts index 6f1a1fbe649cf..b6db1680e411f 100644 --- a/extensions/npm/src/tasks.ts +++ b/extensions/npm/src/tasks.ts @@ -71,11 +71,10 @@ export class NpmTaskProvider implements TaskProvider { } else { packageJsonUri = _task.scope.uri.with({ path: _task.scope.uri.path + '/package.json' }); } - const cmd = [kind.script]; - if (kind.script !== INSTALL_SCRIPT) { - cmd.unshift('run'); + if (kind.script === INSTALL_SCRIPT) { + return createInstallationTask(this.context, _task.scope, packageJsonUri); } - return createTask(await getPackageManager(this.context, _task.scope.uri), kind, cmd, _task.scope, packageJsonUri); + return createScriptRunnerTask(this.context, kind.script, _task.scope, packageJsonUri); } return undefined; } @@ -104,49 +103,61 @@ function isTestTask(name: string): boolean { } return false; } +const preScripts: Set = new Set([ + 'est', /* test typo? */ 'install', 'pack', 'pack', 'publish', 'restart', + 'shrinkwrap', 'stop', 'uninstall', 'version' +]); + +const postScripts: Set = new Set([ + 'install', 'pack', 'pack', 'publish', 'publishOnly', 'restart', 'shrinkwrap', + 'stop', 'test', 'uninstall', 'version' +]); + +function canHavePrePostScript(name: string): boolean { + return preScripts.has(name) || postScripts.has(name); +} -function isPrePostScript(name: string): boolean { - const prePostScripts: Set = new Set([ - 'preuninstall', 'postuninstall', 'prepack', 'postpack', 'preinstall', 'postinstall', - 'prepack', 'postpack', 'prepublish', 'postpublish', 'preversion', 'postversion', - 'prestop', 'poststop', 'prerestart', 'postrestart', 'preshrinkwrap', 'postshrinkwrap', - 'pretest', 'postest', 'prepublishOnly' - ]); +export function isWorkspaceFolder(value: any): value is WorkspaceFolder { + return value && typeof value !== 'number'; +} - const prepost = ['pre' + name, 'post' + name]; - for (const knownScript of prePostScripts) { - if (knownScript === prepost[0] || knownScript === prepost[1]) { - return true; - } +export async function getScriptRunner(folder: Uri, context?: ExtensionContext, showWarning?: boolean): Promise { + let scriptRunner = workspace.getConfiguration('npm', folder).get('scriptRunner', 'npm'); + + if (scriptRunner === 'auto') { + scriptRunner = await detectPackageManager(folder, context, showWarning); } - return false; + + return scriptRunner; } -export function isWorkspaceFolder(value: any): value is WorkspaceFolder { - return value && typeof value !== 'number'; +export async function getPackageManager(folder: Uri, context?: ExtensionContext, showWarning?: boolean): Promise { + let packageManager = workspace.getConfiguration('npm', folder).get('packageManager', 'npm'); + + if (packageManager === 'auto') { + packageManager = await detectPackageManager(folder, context, showWarning); + } + + return packageManager; } -export async function getPackageManager(extensionContext: ExtensionContext, folder: Uri, showWarning: boolean = true): Promise { - let packageManagerName = workspace.getConfiguration('npm', folder).get('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(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 detectPackageManager(folder: Uri, extensionContext?: ExtensionContext, showWarning: boolean = false): Promise { + const { name, multipleLockFilesDetected: multiplePMDetected } = await findPreferredPM(folder.fsPath); + const neverShowWarning = 'npm.multiplePMWarning.neverShow'; + if (showWarning && multiplePMDetected && extensionContext && !extensionContext.globalState.get(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 packageManagerName; + return name; } export async function hasNpmScripts(): Promise { @@ -170,15 +181,13 @@ export async function hasNpmScripts(): Promise { } } -async function detectNpmScripts(context: ExtensionContext, showWarning: boolean): Promise { +async function* findNpmPackages(): AsyncGenerator { - const emptyTasks: ITaskWithLocation[] = []; - const allTasks: ITaskWithLocation[] = []; const visitedPackageJsonFiles: Set = new Set(); const folders = workspace.workspaceFolders; if (!folders) { - return emptyTasks; + return; } try { for (const folder of folders) { @@ -187,14 +196,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); } @@ -228,7 +235,12 @@ export async function detectNpmScriptsForFolder(context: ExtensionContext, folde export async function provideNpmScripts(context: ExtensionContext, showWarning: boolean): Promise { 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; } @@ -278,15 +290,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 createTask(packageManager, name, ['run', name], folder!, packageJsonUri, value, undefined); + const task = await createScriptRunnerTask(context, name, folder!, packageJsonUri, value, showWarning); result.push({ task, location: new Location(packageJsonUri, nameRange) }); } if (!workspace.getConfiguration('npm', folder).get('scriptExplorerExclude', []).find(e => e.includes(INSTALL_SCRIPT))) { - result.push({ task: await createTask(packageManager, INSTALL_SCRIPT, [INSTALL_SCRIPT], folder, packageJsonUri, 'install dependencies from package', []) }); + result.push({ task: await createInstallationTask(context, folder, packageJsonUri, 'install dependencies from package', showWarning) }); } return result; } @@ -298,50 +308,45 @@ export function getTaskName(script: string, relativePath: string | undefined) { return script; } -export async function createTask(packageManager: string, script: INpmTaskDefinition | string, cmd: string[], folder: WorkspaceFolder, packageJsonUri: Uri, scriptValue?: string, matcher?: any): Promise { - let kind: INpmTaskDefinition; - if (typeof script === 'string') { - kind = { type: 'npm', script: script }; - } else { - kind = script; - } - - function getCommandLine(cmd: string[]): (string | ShellQuotedString)[] { - const result: (string | ShellQuotedString)[] = new Array(cmd.length); - for (let i = 0; i < cmd.length; i++) { - if (/\s/.test(cmd[i])) { - result[i] = { value: cmd[i], quoting: cmd[i].includes('--') ? ShellQuoting.Weak : ShellQuoting.Strong }; - } else { - result[i] = cmd[i]; - } - } - if (workspace.getConfiguration('npm', folder.uri).get('runSilent')) { - result.unshift('--silent'); +function getCommandLine(scope: Uri, cmd: string[]): (string | ShellQuotedString)[] { + const result: (string | ShellQuotedString)[] = new Array(cmd.length); + for (let i = 0; i < cmd.length; i++) { + if (/\s/.test(cmd[i])) { + result[i] = { value: cmd[i], quoting: cmd[i].includes('--') ? ShellQuoting.Weak : ShellQuoting.Strong }; + } else { + result[i] = cmd[i]; } - return result; } - - function getRelativePath(packageJsonUri: Uri): string { - const rootUri = folder.uri; - const absolutePath = packageJsonUri.path.substring(0, packageJsonUri.path.length - 'package.json'.length); - return absolutePath.substring(rootUri.path.length + 1); + if (workspace.getConfiguration('npm', scope).get('runSilent')) { + result.unshift('--silent'); } + return result; +} + +function getRelativePath(rootUri: Uri, packageJsonUri: Uri): string { + const absolutePath = packageJsonUri.path.substring(0, packageJsonUri.path.length - 'package.json'.length); + return absolutePath.substring(rootUri.path.length + 1); +} + +export async function createScriptRunnerTask(context: ExtensionContext, script: string, folder: WorkspaceFolder, packageJsonUri: Uri, scriptValue?: string, showWarning = true): Promise { + const scriptRunner = await getScriptRunner(folder.uri, context, showWarning); + const kind: INpmTaskDefinition = { type: 'npm', script }; - const relativePackageJson = getRelativePath(packageJsonUri); + const relativePackageJson = getRelativePath(folder.uri, packageJsonUri); if (relativePackageJson.length && !kind.path) { kind.path = relativePackageJson.substring(0, relativePackageJson.length - 1); } - const taskName = getTaskName(kind.script, relativePackageJson); + const taskName = getTaskName(script, relativePackageJson); const cwd = path.dirname(packageJsonUri.fsPath); - const task = new Task(kind, folder, taskName, 'npm', new ShellExecution(packageManager, getCommandLine(cmd), { cwd: cwd }), matcher); + const task = new Task(kind, folder, taskName, 'npm', new ShellExecution(scriptRunner, getCommandLine(folder.uri, [scriptRunner === 'node' ? '--run' : 'run', script]), { cwd: cwd })); task.detail = scriptValue; - const lowerCaseTaskName = kind.script.toLowerCase(); + const lowerCaseTaskName = script.toLowerCase(); if (isBuildTask(lowerCaseTaskName)) { task.group = TaskGroup.Build; } else if (isTestTask(lowerCaseTaskName)) { task.group = TaskGroup.Test; - } else if (isPrePostScript(lowerCaseTaskName)) { + } else if (canHavePrePostScript(lowerCaseTaskName)) { task.group = TaskGroup.Clean; // hack: use Clean group to tag pre/post scripts } else if (scriptValue && isDebugScript(scriptValue)) { // todo@connor4312: all scripts are now debuggable, what is a 'debug script'? @@ -350,6 +355,23 @@ export async function createTask(packageManager: string, script: INpmTaskDefinit return task; } +export async function createInstallationTask(context: ExtensionContext, folder: WorkspaceFolder, packageJsonUri: Uri, scriptValue?: string, showWarning = true): Promise { + const packageManager = await getPackageManager(folder.uri, context, showWarning); + const kind: INpmTaskDefinition = { type: 'npm', script: INSTALL_SCRIPT }; + + const relativePackageJson = getRelativePath(folder.uri, packageJsonUri); + if (relativePackageJson.length && !kind.path) { + kind.path = relativePackageJson.substring(0, relativePackageJson.length - 1); + } + const taskName = getTaskName(INSTALL_SCRIPT, relativePackageJson); + const cwd = path.dirname(packageJsonUri.fsPath); + const task = new Task(kind, folder, taskName, 'npm', new ShellExecution(packageManager, getCommandLine(folder.uri, [INSTALL_SCRIPT]), { cwd: cwd })); + task.detail = scriptValue; + task.group = TaskGroup.Clean; + + return task; +} + export function getPackageJsonUriFromTask(task: Task): Uri | null { if (isWorkspaceFolder(task.scope)) { @@ -403,15 +425,17 @@ export async function runScript(context: ExtensionContext, script: string, docum const uri = document.uri; const folder = workspace.getWorkspaceFolder(uri); if (folder) { - const task = await createTask(await getPackageManager(context, folder.uri), script, ['run', 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) { + const scriptRunner = await getScriptRunner(folder.uri, context, true); + commands.executeCommand( 'extension.js-debug.createDebuggerTerminal', - `${await getPackageManager(context, folder.uri)} run ${scriptName}`, + `${scriptRunner} ${scriptRunner === 'node' ? '--run' : 'run'} ${scriptName}`, folder, { cwd }, );