From e32da59d30c8c0aee3e93df2954e27e4eda79e27 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Wed, 17 Jun 2020 11:39:14 +0200 Subject: [PATCH] [npm] support serverless & cleanup --- .../npm/extension-browser.webpack.config.js | 36 +++++++ extensions/npm/extension.webpack.config.js | 6 +- extensions/npm/package.json | 15 +-- .../npm/src/features/bowerJSONContribution.ts | 9 +- .../npm/src/features/jsonContributions.ts | 4 +- .../src/features/packageJSONContribution.ts | 93 +++++++++---------- .../markedTextUtil.ts => npmBrowserMain.ts} | 13 ++- extensions/npm/src/{main.ts => npmMain.ts} | 36 ++++--- extensions/npm/src/scriptHover.ts | 3 + extensions/npm/yarn.lock | 19 ++-- 10 files changed, 146 insertions(+), 88 deletions(-) create mode 100644 extensions/npm/extension-browser.webpack.config.js rename extensions/npm/src/{features/markedTextUtil.ts => npmBrowserMain.ts} (51%) rename extensions/npm/src/{main.ts => npmMain.ts} (86%) diff --git a/extensions/npm/extension-browser.webpack.config.js b/extensions/npm/extension-browser.webpack.config.js new file mode 100644 index 00000000000..ab50c8718a3 --- /dev/null +++ b/extensions/npm/extension-browser.webpack.config.js @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withDefaults = require('../shared.webpack.config'); +const path = require('path'); + +const clientConfig = withDefaults({ + target: 'webworker', + context: __dirname, + entry: { + extension: './src/npmBrowserMain.ts' + }, + output: { + filename: 'npmBrowserMain.js' + }, + performance: { + hints: false + }, + resolve: { + alias: { + 'vscode-nls': path.resolve(__dirname, '../../build/polyfills/vscode-nls.js') + } + }, + node: { + 'child_process': 'empty' + } +}); +clientConfig.module.rules[0].use.shift(); // remove nls loader + +module.exports = clientConfig; diff --git a/extensions/npm/extension.webpack.config.js b/extensions/npm/extension.webpack.config.js index 56a1589f460..1c6d9493e33 100644 --- a/extensions/npm/extension.webpack.config.js +++ b/extensions/npm/extension.webpack.config.js @@ -14,12 +14,10 @@ const withDefaults = require('../shared.webpack.config'); module.exports = withDefaults({ context: __dirname, entry: { - extension: './src/main.ts', + extension: './src/npmMain.ts', }, output: { - filename: 'main.js', - path: path.join(__dirname, 'dist'), - libraryTarget: 'commonjs', + filename: 'npmMain.js', }, resolve: { mainFields: ['module', 'main'], diff --git a/extensions/npm/package.json b/extensions/npm/package.json index ed1e8bd9015..a80135fac28 100644 --- a/extensions/npm/package.json +++ b/extensions/npm/package.json @@ -20,14 +20,15 @@ "dependencies": { "jsonc-parser": "^2.2.1", "minimatch": "^3.0.4", - "request-light": "^0.2.5", + "request-light": "^0.4.0", "vscode-nls": "^4.1.1" }, "devDependencies": { "@types/minimatch": "^3.0.3", "@types/node": "^12.11.7" }, - "main": "./out/main", + "main": "./out/npmMain", + "browser": "./dist/npmBrowserMain", "activationEvents": [ "onCommand:workbench.action.tasks.runTask", "onCommand:npm.runScriptFromFolder", @@ -181,12 +182,12 @@ } ], "explorer/context": [ - { - "when": "config.npm.enableRunFromFolder && explorerViewletVisible && explorerResourceIsFolder", + { + "when": "config.npm.enableRunFromFolder && explorerViewletVisible && explorerResourceIsFolder", "command": "npm.runScriptFromFolder", - "group": "2_workspace" - } - ] + "group": "2_workspace" + } + ] }, "configuration": { "id": "npm", diff --git a/extensions/npm/src/features/bowerJSONContribution.ts b/extensions/npm/src/features/bowerJSONContribution.ts index 705e40d34e7..cd648732fc7 100644 --- a/extensions/npm/src/features/bowerJSONContribution.ts +++ b/extensions/npm/src/features/bowerJSONContribution.ts @@ -3,11 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MarkedString, CompletionItemKind, CompletionItem, DocumentSelector, SnippetString, workspace } from 'vscode'; +import { MarkdownString, CompletionItemKind, CompletionItem, DocumentSelector, SnippetString, workspace } from 'vscode'; import { IJSONContribution, ISuggestionsCollector } from './jsonContributions'; import { XHRRequest } from 'request-light'; import { Location } from 'jsonc-parser'; -import { textToMarkedString } from './markedTextUtil'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); @@ -181,13 +180,15 @@ export class BowerJSONContribution implements IJSONContribution { }); } - public getInfoContribution(_resource: string, location: Location): Thenable | null { + public getInfoContribution(_resource: string, location: Location): Thenable | null { if ((location.matches(['dependencies', '*']) || location.matches(['devDependencies', '*']))) { const pack = location.path[location.path.length - 1]; if (typeof pack === 'string') { return this.getInfo(pack).then(documentation => { if (documentation) { - return [textToMarkedString(documentation)]; + const str = new MarkdownString(); + str.appendText(documentation); + return [str]; } return null; }); diff --git a/extensions/npm/src/features/jsonContributions.ts b/extensions/npm/src/features/jsonContributions.ts index 4a255baf823..3873b2dc31a 100644 --- a/extensions/npm/src/features/jsonContributions.ts +++ b/extensions/npm/src/features/jsonContributions.ts @@ -30,8 +30,8 @@ export interface IJSONContribution { resolveSuggestion?(item: CompletionItem): Thenable | null; } -export function addJSONProviders(xhr: XHRRequest): Disposable { - const contributions = [new PackageJSONContribution(xhr), new BowerJSONContribution(xhr)]; +export function addJSONProviders(xhr: XHRRequest, canRunNPM: boolean): Disposable { + const contributions = [new PackageJSONContribution(xhr, canRunNPM), new BowerJSONContribution(xhr)]; const subscriptions: Disposable[] = []; contributions.forEach(contribution => { const selector = contribution.getDocumentSelector(); diff --git a/extensions/npm/src/features/packageJSONContribution.ts b/extensions/npm/src/features/packageJSONContribution.ts index 4789886891c..135b632071c 100644 --- a/extensions/npm/src/features/packageJSONContribution.ts +++ b/extensions/npm/src/features/packageJSONContribution.ts @@ -3,11 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MarkedString, CompletionItemKind, CompletionItem, DocumentSelector, SnippetString, workspace } from 'vscode'; +import { MarkedString, CompletionItemKind, CompletionItem, DocumentSelector, SnippetString, workspace, MarkdownString } from 'vscode'; import { IJSONContribution, ISuggestionsCollector } from './jsonContributions'; import { XHRRequest } from 'request-light'; import { Location } from 'jsonc-parser'; -import { textToMarkedString } from './markedTextUtil'; import * as cp from 'child_process'; import * as nls from 'vscode-nls'; @@ -28,14 +27,12 @@ export class PackageJSONContribution implements IJSONContribution { 'jsdom', 'stylus', 'when', 'readable-stream', 'aws-sdk', 'concat-stream', 'chai', 'Thenable', 'wrench']; private knownScopes = ['@types', '@angular', '@babel', '@nuxtjs', '@vue', '@bazel']; - private xhr: XHRRequest; public getDocumentSelector(): DocumentSelector { return [{ language: 'json', scheme: '*', pattern: '**/package.json' }]; } - public constructor(xhr: XHRRequest) { - this.xhr = xhr; + public constructor(private xhr: XHRRequest, private canRunNPM: boolean) { } public collectDefaultSuggestions(_fileName: string, result: ISuggestionsCollector): Thenable { @@ -191,23 +188,23 @@ export class PackageJSONContribution implements IJSONContribution { const currentKey = location.path[location.path.length - 1]; if (typeof currentKey === 'string') { const info = await this.fetchPackageInfo(currentKey); - if (info && info.distTagsLatest) { + if (info && info.version) { - let name = JSON.stringify(info.distTagsLatest); + let name = JSON.stringify(info.version); let proposal = new CompletionItem(name); proposal.kind = CompletionItemKind.Property; proposal.insertText = name; proposal.documentation = localize('json.npm.latestversion', 'The currently latest version of the package'); result.add(proposal); - name = JSON.stringify('^' + info.distTagsLatest); + name = JSON.stringify('^' + info.version); proposal = new CompletionItem(name); proposal.kind = CompletionItemKind.Property; proposal.insertText = name; proposal.documentation = localize('json.npm.majorversion', 'Matches the most recent major version (1.x.x)'); result.add(proposal); - name = JSON.stringify('~' + info.distTagsLatest); + name = JSON.stringify('~' + info.version); proposal = new CompletionItem(name); proposal.kind = CompletionItemKind.Property; proposal.insertText = name; @@ -219,14 +216,27 @@ export class PackageJSONContribution implements IJSONContribution { return null; } + private getDocumentation(description: string | undefined, version: string | undefined, homepage: string | undefined): MarkdownString { + const str = new MarkdownString(); + if (description) { + str.appendText(description); + } + if (version) { + str.appendText('\n\n'); + str.appendText(localize('json.npm.version.hover', 'Latest version: {0}', version)); + } + if (homepage) { + str.appendText('\n\n'); + str.appendText(homepage); + } + return str; + } + public resolveSuggestion(item: CompletionItem): Thenable | null { - if (item.kind === CompletionItemKind.Property && item.documentation === '') { - return this.getInfo(item.label).then(infos => { - if (infos.length > 0) { - item.documentation = infos[0]; - if (infos.length > 1) { - item.detail = infos[1]; - } + if (item.kind === CompletionItemKind.Property && !item.documentation) { + return this.fetchPackageInfo(item.label).then(info => { + if (info) { + item.documentation = this.getDocumentation(info.description, info.version, info.homepage); return item; } return null; @@ -235,21 +245,11 @@ export class PackageJSONContribution implements IJSONContribution { return null; } - private async getInfo(pack: string): Promise { - let info = await this.fetchPackageInfo(pack); - if (info) { - const result: string[] = []; - result.push(info.description || ''); - result.push(info.distTagsLatest ? localize('json.npm.version.hover', 'Latest version: {0}', info.distTagsLatest) : ''); - result.push(info.homepage || ''); - return result; - } - - return []; - } - private async fetchPackageInfo(pack: string): Promise { - let info = await this.npmView(pack); + let info: ViewPackageInfo | undefined; + if (this.canRunNPM) { + info = await this.npmView(pack); + } if (!info) { info = await this.npmjsView(pack); } @@ -259,14 +259,14 @@ export class PackageJSONContribution implements IJSONContribution { private npmView(pack: string): Promise { return new Promise((resolve, _reject) => { - const command = 'npm view --json ' + pack + ' description dist-tags.latest homepage'; + const command = 'npm view --json ' + pack + ' description dist-tags.latest homepage version'; cp.exec(command, (error, stdout) => { if (!error) { try { const content = JSON.parse(stdout); resolve({ description: content['description'], - distTagsLatest: content['dist-tags.latest'], + version: content['dist-tags.latest'] || content['version'], homepage: content['homepage'] }); return; @@ -280,22 +280,20 @@ export class PackageJSONContribution implements IJSONContribution { } private async npmjsView(pack: string): Promise { - const queryUrl = 'https://registry.npmjs.org/' + encodeURIComponent(pack).replace(/%40/g, '@'); + const queryUrl = 'https://api.npms.io/v2/package/' + encodeURIComponent(pack); try { const success = await this.xhr({ url: queryUrl, agent: USER_AGENT }); const obj = JSON.parse(success.responseText); - if (obj) { - const latest = obj && obj['dist-tags'] && obj['dist-tags']['latest']; - if (latest) { - return { - description: obj.description || '', - distTagsLatest: latest, - homepage: obj.homepage || '' - }; - } + const metadata = obj?.collected?.metadata; + if (metadata) { + return { + description: metadata.description || '', + version: metadata.version, + homepage: metadata.links?.homepage || '' + }; } } catch (e) { @@ -308,9 +306,9 @@ export class PackageJSONContribution implements IJSONContribution { if ((location.matches(['dependencies', '*']) || location.matches(['devDependencies', '*']) || location.matches(['optionalDependencies', '*']) || location.matches(['peerDependencies', '*']))) { const pack = location.path[location.path.length - 1]; if (typeof pack === 'string') { - return this.getInfo(pack).then(infos => { - if (infos.length) { - return [infos.map(textToMarkedString).join('\n\n')]; + return this.fetchPackageInfo(pack).then(info => { + if (info) { + return [this.getDocumentation(info.description, info.version, info.homepage)]; } return null; }); @@ -339,7 +337,7 @@ export class PackageJSONContribution implements IJSONContribution { proposal.kind = CompletionItemKind.Property; proposal.insertText = insertText; proposal.filterText = JSON.stringify(name); - proposal.documentation = pack.description || ''; + proposal.documentation = this.getDocumentation(pack.description, pack.version, pack?.links?.homepage); collector.add(proposal); } } @@ -349,10 +347,11 @@ interface SearchPackageInfo { name: string; description?: string; version?: string; + links?: { homepage?: string; }; } interface ViewPackageInfo { description: string; - distTagsLatest?: string; + version?: string; homepage?: string; } diff --git a/extensions/npm/src/features/markedTextUtil.ts b/extensions/npm/src/npmBrowserMain.ts similarity index 51% rename from extensions/npm/src/features/markedTextUtil.ts rename to extensions/npm/src/npmBrowserMain.ts index 856fad050e5..96cfe579505 100644 --- a/extensions/npm/src/features/markedTextUtil.ts +++ b/extensions/npm/src/npmBrowserMain.ts @@ -3,8 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MarkedString } from 'vscode'; +import * as httpRequest from 'request-light'; +import * as vscode from 'vscode'; +import { addJSONProviders } from './features/jsonContributions'; -export function textToMarkedString(text: string): MarkedString { - return text.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'); // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash -} \ No newline at end of file +export async function activate(context: vscode.ExtensionContext): Promise { + context.subscriptions.push(addJSONProviders(httpRequest.xhr, false)); +} + +export function deactivate(): void { +} diff --git a/extensions/npm/src/main.ts b/extensions/npm/src/npmMain.ts similarity index 86% rename from extensions/npm/src/main.ts rename to extensions/npm/src/npmMain.ts index b79638ed1f0..764be6ea0fc 100644 --- a/extensions/npm/src/main.ts +++ b/extensions/npm/src/npmMain.ts @@ -14,13 +14,19 @@ import { invalidateHoverScriptsCache, NpmScriptHoverProvider } from './scriptHov let treeDataProvider: NpmScriptsTreeDataProvider | undefined; export async function activate(context: vscode.ExtensionContext): Promise { - registerTaskProvider(context); - treeDataProvider = registerExplorer(context); - registerHoverProvider(context); - configureHttpRequest(); - let d = vscode.workspace.onDidChangeConfiguration((e) => { - configureHttpRequest(); + context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('http.proxy') || e.affectsConfiguration('http.proxyStrictSSL')) { + configureHttpRequest(); + } + })); + + const canRunNPM = canRunNpmInCurrentWorkspace(); + context.subscriptions.push(addJSONProviders(httpRequest.xhr, canRunNPM)); + + treeDataProvider = registerExplorer(context); + + context.subscriptions.push(vscode.workspace.onDidChangeConfiguration((e) => { if (e.affectsConfiguration('npm.exclude') || e.affectsConfiguration('npm.autoDetect')) { invalidateTasksCache(); if (treeDataProvider) { @@ -32,15 +38,12 @@ export async function activate(context: vscode.ExtensionContext): Promise treeDataProvider.refresh(); } } - }); - context.subscriptions.push(d); + })); + + registerTaskProvider(context); + registerHoverProvider(context); - d = vscode.workspace.onDidChangeTextDocument((e) => { - invalidateHoverScriptsCache(e.document); - }); - context.subscriptions.push(d); context.subscriptions.push(vscode.commands.registerCommand('npm.runSelectedScript', runSelectedScript)); - context.subscriptions.push(addJSONProviders(httpRequest.xhr)); if (await hasPackageJson()) { vscode.commands.executeCommand('setContext', 'npm:showScriptExplorer', true); @@ -49,6 +52,13 @@ export async function activate(context: vscode.ExtensionContext): Promise context.subscriptions.push(vscode.commands.registerCommand('npm.runScriptFromFolder', selectAndRunScriptFromFolder)); } +function canRunNpmInCurrentWorkspace() { + if (vscode.workspace.workspaceFolders) { + return vscode.workspace.workspaceFolders.some(f => f.uri.scheme === 'file'); + } + return false; +} + function registerTaskProvider(context: vscode.ExtensionContext): vscode.Disposable | undefined { function invalidateScriptCaches() { diff --git a/extensions/npm/src/scriptHover.ts b/extensions/npm/src/scriptHover.ts index 46992af2088..aa803dbc1d4 100644 --- a/extensions/npm/src/scriptHover.ts +++ b/extensions/npm/src/scriptHover.ts @@ -32,6 +32,9 @@ export class NpmScriptHoverProvider implements HoverProvider { constructor(context: ExtensionContext) { context.subscriptions.push(commands.registerCommand('npm.runScriptFromHover', this.runScriptFromHover, this)); context.subscriptions.push(commands.registerCommand('npm.debugScriptFromHover', this.debugScriptFromHover, this)); + context.subscriptions.push(workspace.onDidChangeTextDocument((e) => { + invalidateHoverScriptsCache(e.document); + })); } public provideHover(document: TextDocument, position: Position, _token: CancellationToken): ProviderResult { diff --git a/extensions/npm/yarn.lock b/extensions/npm/yarn.lock index 9b8a23c9264..5e85de9297e 100644 --- a/extensions/npm/yarn.lock +++ b/extensions/npm/yarn.lock @@ -71,7 +71,7 @@ http-proxy-agent@^2.1.0: agent-base "4" debug "3.1.0" -https-proxy-agent@^2.2.3: +https-proxy-agent@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== @@ -96,16 +96,21 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -request-light@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.2.5.tgz#38a3da7b2e56f7af8cbba57e8a94930ee2380746" - integrity sha512-eBEh+GzJAftUnex6tcL6eV2JCifY0+sZMIUpUPOVXbs2nV5hla4ZMmO3icYKGuGVuQ2zHE9evh4OrRcH4iyYYw== +request-light@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.4.0.tgz#c6b91ef00b18cb0de75d2127e55b3a2c9f7f90f9" + integrity sha512-fimzjIVw506FBZLspTAXHdpvgvQebyjpNyLRd0e6drPPRq7gcrROeGWRyF81wLqFg5ijPgnOQbmfck5wdTqpSA== dependencies: http-proxy-agent "^2.1.0" - https-proxy-agent "^2.2.3" - vscode-nls "^4.1.1" + https-proxy-agent "^2.2.4" + vscode-nls "^4.1.2" vscode-nls@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c" integrity sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A== + +vscode-nls@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.2.tgz#ca8bf8bb82a0987b32801f9fddfdd2fb9fd3c167" + integrity sha512-7bOHxPsfyuCqmP+hZXscLhiHwe7CSuFE4hyhbs22xPIhQ4jv99FcR4eBzfYYVLP356HNFpdvz63FFb/xw6T4Iw==