Initial check-in of module validation

This commit is contained in:
Erich Gamma 2016-11-15 20:52:41 +01:00
Родитель 9ec43ce83a
Коммит c908c0befc
6 изменённых файлов: 446 добавлений и 28 удалений

4
.vscode/settings.json поставляемый
Просмотреть файл

@ -10,6 +10,4 @@
"editor.tabSize": 4,
"editor.insertSpaces": false,
"files.trimTrailingWhitespace": true,
"files.autoSave": "afterDelay",
"typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version
}
"files.autoSave": "afterDelay"}

11
CHANGELOG.md Normal file
Просмотреть файл

@ -0,0 +1,11 @@
- 0.0.21 added command to run `npm start`.
- 0.0.20 when commands are run in the terminal, then the **integrated terminal** is used.
- 0.0.16 added `npm install ` to the context menu on `package.json` in the explorer.
- 0.0.15 added setting to run npm commands with `--silent`.
- 0.0.15 tweaks to the README so that the extension is found when searching for node.
- 0.0.14 added command to terminate a running script
- 0.0.13 save workspace before running scripts, added command to run `npm run build`
- 0.0.12 added support for `npm.useRootDirectory`
- 0.0.11 added command to run `npm test`.
- 0.0.7 adding an icon and changed the display name to 'npm Script Runner'.
- 0.0.4 the keybinding was changed from `R` to `N` to avoid conflicts with the default `workbench.action.files.newUntitledFile` command.

Просмотреть файл

@ -38,17 +38,3 @@ in a terminal window or whether the output form the command is shown in the `Out
## Keyboard Shortcuts
The extension defines a chording keyboard shortcut for the `R` key. As a consequence an existing keybinding for `R` is not executed immediately. If this is not desired, then please bind another key for these commands, see the [customization](https://code.visualstudio.com/docs/customization/keybindings) documentation.
## Release Notes
- 0.0.21 added command to run `npm start`.
- 0.0.20 when commands are run in the terminal, then the **integrated terminal** is used.
- 0.0.16 added `npm install ` to the context menu on `package.json` in the explorer.
- 0.0.15 added setting to run npm commands with `--silent`.
- 0.0.15 tweaks to the README so that the extension is found when searching for node.
- 0.0.14 added command to terminate a running script
- 0.0.13 save workspace before running scripts, added command to run `npm run build`
- 0.0.12 added support for `npm.useRootDirectory`
- 0.0.11 added command to run `npm test`.
- 0.0.7 adding an icon and changed the display name to 'npm Script Runner'.
- 0.0.4 the keybinding was changed from `R` to `N` to avoid conflicts with the default `workbench.action.files.newUntitledFile` command.

Просмотреть файл

@ -20,14 +20,14 @@
"Other"
],
"activationEvents": [
"onLanguage:json",
"onCommand:npm-script.install",
"onCommand:npm-script.run",
"onCommand:npm-script.showOutput",
"onCommand:npm-script.rerun-last-script",
"onCommand:npm-script.terminate-script",
"onCommand:npm-script.test",
"onCommand:npm-script.start",
"onCommand:npm-script.build"
"onCommand:npm-script.start"
],
"main": "./out/src/main",
"contributes": {
@ -155,6 +155,7 @@
},
"dependencies": {
"run-in-terminal": "^0.0.2",
"tree-kill": "^1.1.0"
"tree-kill": "^1.1.0",
"jsonc-parser": "^0.2.3"
}
}

185
src/async.ts Normal file
Просмотреть файл

@ -0,0 +1,185 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface ITask<T> {
(): T;
}
/**
* A helper to prevent accumulation of sequential async tasks.
*
* Imagine a mail man with the sole task of delivering letters. As soon as
* a letter submitted for delivery, he drives to the destination, delivers it
* and returns to his base. Imagine that during the trip, N more letters were submitted.
* When the mail man returns, he picks those N letters and delivers them all in a
* single trip. Even though N+1 submissions occurred, only 2 deliveries were made.
*
* The throttler implements this via the queue() method, by providing it a task
* factory. Following the example:
*
* var throttler = new Throttler();
* var letters = [];
*
* function letterReceived(l) {
* letters.push(l);
* throttler.queue(() => { return makeTheTrip(); });
* }
*/
export class Throttler<T> {
private activePromise: Promise<T>;
private queuedPromise: Promise<T>;
private queuedPromiseFactory: ITask<Promise<T>>;
constructor() {
this.activePromise = null;
this.queuedPromise = null;
this.queuedPromiseFactory = null;
}
public queue(promiseFactory: ITask<Promise<T>>): Promise<T> {
if (this.activePromise) {
this.queuedPromiseFactory = promiseFactory;
if (!this.queuedPromise) {
var onComplete = () => {
this.queuedPromise = null;
var result = this.queue(this.queuedPromiseFactory);
this.queuedPromiseFactory = null;
return result;
};
this.queuedPromise = new Promise<T>((resolve, reject) => {
this.activePromise.then(onComplete, onComplete).then(resolve);
});
}
return new Promise<T>((resolve, reject) => {
this.queuedPromise.then(resolve, reject);
});
}
this.activePromise = promiseFactory();
return new Promise<T>((resolve, reject) => {
this.activePromise.then((result: T) => {
this.activePromise = null;
resolve(result);
}, (err: any) => {
this.activePromise = null;
reject(err);
});
});
}
}
/**
* A helper to delay execution of a task that is being requested often.
*
* Following the throttler, now imagine the mail man wants to optimize the number of
* trips proactively. The trip itself can be long, so the he decides not to make the trip
* as soon as a letter is submitted. Instead he waits a while, in case more
* letters are submitted. After said waiting period, if no letters were submitted, he
* decides to make the trip. Imagine that N more letters were submitted after the first
* one, all within a short period of time between each other. Even though N+1
* submissions occurred, only 1 delivery was made.
*
* The delayer offers this behavior via the trigger() method, into which both the task
* to be executed and the waiting period (delay) must be passed in as arguments. Following
* the example:
*
* var delayer = new Delayer(WAITING_PERIOD);
* var letters = [];
*
* function letterReceived(l) {
* letters.push(l);
* delayer.trigger(() => { return makeTheTrip(); });
* }
*/
export class Delayer<T> {
public defaultDelay: number;
private timeout: NodeJS.Timer;
private completionPromise: Promise<T>;
private onResolve: (value: T | Thenable<T>) => void;
private task: ITask<T>;
constructor(defaultDelay: number) {
this.defaultDelay = defaultDelay;
this.timeout = null;
this.completionPromise = null;
this.onResolve = null;
this.task = null;
}
public trigger(task: ITask<T>, delay: number = this.defaultDelay): Promise<T> {
this.task = task;
this.cancelTimeout();
if (!this.completionPromise) {
this.completionPromise = new Promise<T>((resolve, reject) => {
this.onResolve = resolve;
}).then(() => {
this.completionPromise = null;
this.onResolve = null;
var result = this.task();
this.task = null;
return result;
});
}
this.timeout = setTimeout(() => {
this.timeout = null;
this.onResolve(null);
}, delay);
return this.completionPromise;
}
public isTriggered(): boolean {
return this.timeout !== null;
}
public cancel(): void {
this.cancelTimeout();
if (this.completionPromise) {
this.completionPromise = null;
}
}
private cancelTimeout(): void {
if (this.timeout !== null) {
clearTimeout(this.timeout);
this.timeout = null;
}
}
}
/**
* A helper to delay execution of a task that is being requested often, while
* preventing accumulation of consecutive executions, while the task runs.
*
* Simply combine the two mail man strategies from the Throttler and Delayer
* helpers, for an analogy.
*/
export class ThrottledDelayer<T> extends Delayer<Promise<T>> {
private throttler: Throttler<T>;
constructor(defaultDelay: number) {
super(defaultDelay);
this.throttler = new Throttler();
}
public trigger(promiseFactory: ITask<Promise<T>>, delay?: number): Promise<Promise<T>> {
return super.trigger(() => this.throttler.queue(promiseFactory), delay);
}
}

Просмотреть файл

@ -2,9 +2,19 @@ import * as cp from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
import { window, commands, workspace, OutputChannel, ExtensionContext, ViewColumn, QuickPickItem, Terminal } from 'vscode';
import {
window, commands, workspace, languages, OutputChannel, ExtensionContext, ViewColumn,
QuickPickItem, Terminal, DiagnosticCollection, Diagnostic, Range, TextDocument, DiagnosticSeverity,
CodeActionProvider, CodeActionContext, CancellationToken, Command
} from 'vscode';
import { runInTerminal } from 'run-in-terminal';
import { kill } from 'tree-kill';
import { parseTree, Node, } from 'jsonc-parser';
import { ThrottledDelayer } from './async';
let diagnosticCollection: DiagnosticCollection;
let delayer: ThrottledDelayer<void> = null;
interface Script extends QuickPickItem {
scriptName: string;
@ -18,7 +28,49 @@ interface Process {
}
class ProcessItem implements QuickPickItem {
constructor (public label: string, public description: string, public pid: number) {}
constructor(public label: string, public description: string, public pid: number) { }
}
interface SourceRange {
name: {
offset: number;
length: number;
};
version: {
offset: number;
length: number;
};
}
interface DependencySourceRanges {
[dependency: string]: SourceRange;
}
class NpmCodeActionProvider implements CodeActionProvider {
public provideCodeActions(document: TextDocument, range: Range, context: CodeActionContext, token: CancellationToken): Command[] {
let cmds: Command[] = [];
context.diagnostics.forEach(diag => {
if (diag.message.indexOf('[npm] ') === 0) {
let [_, moduleName] = /^\[npm\] Module '(\S*)'/.exec(diag.message);
cmds.push({
title: `run: npm install '${moduleName}'`,
command: 'npm-script.installInOutputWindow',
arguments: [moduleName]
});
cmds.push({
title: `run: npm install`,
command: 'npm-script.installInOutputWindow',
arguments: []
});
cmds.push({
title: `validate installed modules`,
command: 'npm-script.validate',
arguments: []
});
}
});
return cmds;
}
}
const runningProcesses: Map<number, Process> = new Map();
@ -29,8 +81,45 @@ let lastScript: Script = null;
export function activate(context: ExtensionContext) {
registerCommands(context);
diagnosticCollection = languages.createDiagnosticCollection('npm-script-runner');
context.subscriptions.push(diagnosticCollection);
outputChannel = window.createOutputChannel('npm');
context.subscriptions.push(outputChannel);
context.subscriptions.push(languages.registerCodeActionsProvider('json', new NpmCodeActionProvider()));
workspace.onDidSaveTextDocument(document => {
console.log("onDidSaveTextDocument ", document.fileName);
validateDocument(document);
}, null, context.subscriptions);
window.onDidChangeActiveTextEditor(editor => {
console.log("onDidChangeActiveTextEditor", editor.document.fileName);
if (editor && editor.document) {
validateDocument(editor.document);
}
}, null, context.subscriptions);
// for now do not remove the markers on close
// workspace.onDidCloseTextDocument(document => {
// diagnosticCollection.clear();
// }, null, context.subscriptions);
// workaround for onDidOpenTextDocument
// workspace.onDidOpenTextDocument(document => {
// console.log("onDidOpenTextDocument ", document.fileName);
// validateDocument(document);
// }, null, context.subscriptions);
window.visibleTextEditors.forEach(each => {
if (each.document) {
validateDocument(each.document);
}
});
context.subscriptions.push();
}
export function deactivate() {
@ -39,6 +128,23 @@ export function deactivate() {
}
}
function validateDocument(document: TextDocument) {
//console.log('validateDocument ', document.fileName);
if (!document || path.basename(document.fileName) !== 'package.json') {
return;
}
if (!delayer) {
delayer = new ThrottledDelayer<void>(200);
}
//console.log('trigger');
delayer.trigger(() => doValidate(document));
}
function validateAllDocuments() {
workspace.textDocuments.forEach(each => validateDocument(each));
}
function registerCommands(context: ExtensionContext) {
context.subscriptions.push(
commands.registerCommand('npm-script.install', runNpmInstall),
@ -48,6 +154,8 @@ function registerCommands(context: ExtensionContext) {
commands.registerCommand('npm-script.showOutput', showNpmOutput),
commands.registerCommand('npm-script.rerun-last-script', rerunLastScript),
commands.registerCommand('npm-script.build', runNpmBuild),
commands.registerCommand('npm-script.installInOutputWindow', runNpmInstallInOutputWindow),
commands.registerCommand('npm-script.validate', validateAllDocuments),
commands.registerCommand('npm-script.terminate-script', terminateScript)
);
}
@ -59,6 +167,13 @@ function runNpmInstall() {
}
}
function runNpmInstallInOutputWindow(arg) {
let dirs = getIncludedDirectories();
for (let dir of dirs) {
runNpmCommand(['install', arg], dir, true);
}
}
function runNpmTest() {
runNpmCommand(['test']);
}
@ -71,6 +186,102 @@ function runNpmBuild() {
runNpmCommand(['run-script', 'build']);
}
function doValidate(document: TextDocument): Promise<void> {
//console.log('do validate');
return new Promise<void>((resolve, reject) => {
getInstalledModules().then(result => {
let errors = [];
let definedDependencies: DependencySourceRanges = {};
if (!anyModuleErrors(result)) {
resolve();
}
let node = parseTree(document.getText(), errors);
node.children.forEach(child => {
let children = child.children;
if (children && children.length === 2 && isDependency(children[0].value)) {
collectDefinedDependencies(definedDependencies, child.children[1]);
}
});
diagnosticCollection.clear();
let diagnostics: Diagnostic[] = [];
for (var moduleName in definedDependencies) {
if (definedDependencies.hasOwnProperty(moduleName)) {
let diagnostic = getDiagnostic(document, result, moduleName, definedDependencies[moduleName]);
if (diagnostic) {
diagnostics.push(diagnostic);
}
}
}
diagnosticCollection.set(document.uri, diagnostics);
//console.log("diagnostic count ", diagnostics.length, " ", document.uri.fsPath);
resolve();
}, error => {
reject(error);
});
});
}
function getDiagnostic(document: TextDocument, result: Object, moduleName: string, source: SourceRange): Diagnostic {
let deps = ['dependencies', 'devDependencies'];
let diagnostic = null;
deps.forEach(each => {
if (result[each] && result[each][moduleName]) {
if (result[each][moduleName]['missing'] === true) {
let range = new Range(document.positionAt(source.name.offset), document.positionAt(source.name.offset + source.name.length));
diagnostic = new Diagnostic(range, `[npm] Module '${moduleName}' not installed`, DiagnosticSeverity.Warning);
}
if (result[each][moduleName]['invalid'] === true) {
let range = new Range(document.positionAt(source.version.offset), document.positionAt(source.version.offset + source.version.length));
diagnostic = new Diagnostic(range, `[npm] Module '${moduleName}' installed version is invalid`, DiagnosticSeverity.Warning);
}
}
});
return diagnostic;
}
function anyModuleErrors(result: any): boolean {
let problems: string[] = result['problems'];
let errorCount = 0;
if (problems) {
problems.forEach(each => {
if (each.startsWith('missing:') || each.startsWith('invalid:')) {
errorCount++;
}
});
}
return errorCount > 0;
}
function collectDefinedDependencies(deps: Object, node: Node) {
node.children.forEach(child => {
if (child.type === 'property' && child.children.length === 2) {
let dependencyName = child.children[0];
let version = child.children[1];
deps[dependencyName.value] = {
name: {
offset: dependencyName.offset,
length: dependencyName.length
},
version: {
offset: version.offset,
length: version.length
}
};
}
});
}
function isDependency(value: string) {
return value === 'dependencies' || value === 'devDependencies';
}
function showNpmOutput(): void {
outputChannel.show(ViewColumn.Three);
}
@ -120,7 +331,7 @@ function rerunLastScript(): void {
}
function terminateScript(): void {
if(useTerminal()) {
if (useTerminal()) {
window.showInformationMessage('Killing is only supported when the setting "runInTerminal" is "false"');
} else {
let items: ProcessItem[] = [];
@ -130,7 +341,7 @@ function terminateScript(): void {
});
window.showQuickPick(items).then((value) => {
if(value) {
if (value) {
outputChannel.appendLine('');
outputChannel.appendLine(`Killing process ${value.label} (pid: ${value.pid})`);
outputChannel.appendLine('');
@ -163,7 +374,7 @@ function readScripts(): any {
});
});
}
} catch(e) {
} catch (e) {
window.showInformationMessage(`Cannot read '${fileName}'`);
return undefined;
}
@ -177,7 +388,7 @@ function readScripts(): any {
return scripts;
}
function runNpmCommand(args: string[], cwd?: string): void {
function runNpmCommand(args: string[], cwd?: string, alwaysRunInputWindow = false): void {
if (runSilent()) {
args.push('--silent');
}
@ -186,7 +397,7 @@ function runNpmCommand(args: string[], cwd?: string): void {
cwd = workspace.rootPath;
}
if (useTerminal()) {
if (useTerminal() && !alwaysRunInputWindow) {
if (typeof window.createTerminal === 'function') {
runCommandInIntegratedTerminal(args, cwd);
} else {
@ -199,6 +410,30 @@ function runNpmCommand(args: string[], cwd?: string): void {
});
}
function getInstalledModules(): Promise<Object> {
return new Promise((resolve, reject) => {
let cmd = getNpmBin() + ' ' + 'ls --depth 0 --json';
let jsonResult = '';
let errors = '';
let p = cp.exec(cmd, { cwd: workspace.rootPath, env: process.env }, (error: Error, stdout: string, stderr: string) => {
reject(error);
});
p.stderr.on('data', (chunk: string) => errors += chunk);
p.stdout.on('data', (chunk: string) => jsonResult += chunk);
p.on('exit', (code, signal) => {
let resp = '';
try {
resp = JSON.parse(jsonResult);
resolve(resp);
} catch (e) {
reject(e);
}
});
});
}
function runCommandInOutputWindow(args: string[], cwd: string) {
let cmd = getNpmBin() + ' ' + args.join(' ');
let p = cp.exec(cmd, { cwd: cwd, env: process.env });
@ -214,7 +449,7 @@ function runCommandInOutputWindow(args: string[], cwd: string) {
p.on('exit', (code, signal) => {
runningProcesses.delete(p.pid);
if(signal === 'SIGTERM') {
if (signal === 'SIGTERM') {
outputChannel.appendLine('Successfully killed process');
outputChannel.appendLine('-----------------------');
outputChannel.appendLine('');
@ -222,6 +457,7 @@ function runCommandInOutputWindow(args: string[], cwd: string) {
outputChannel.appendLine('-----------------------');
outputChannel.appendLine('');
}
validateAllDocuments();
});
showNpmOutput();
@ -267,3 +503,4 @@ function getIncludedDirectories() {
function getNpmBin() {
return workspace.getConfiguration('npm')['bin'] || 'npm';
}