mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-13 14:26:37 +00:00
fix: Prompt sketch move when opening an invalid outside from IDE2
Log IDE2 version on start. Closes #964 Closes #1484 Co-authored-by: Alberto Iannaccone <a.iannaccone@arduino.cc> Co-authored-by: Akos Kitta <a.kitta@arduino.cc> Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
parent
0773c3915c
commit
2b2463b834
@ -45,6 +45,7 @@
|
|||||||
"@types/deepmerge": "^2.2.0",
|
"@types/deepmerge": "^2.2.0",
|
||||||
"@types/glob": "^7.2.0",
|
"@types/glob": "^7.2.0",
|
||||||
"@types/google-protobuf": "^3.7.2",
|
"@types/google-protobuf": "^3.7.2",
|
||||||
|
"@types/is-valid-path": "^0.1.0",
|
||||||
"@types/js-yaml": "^3.12.2",
|
"@types/js-yaml": "^3.12.2",
|
||||||
"@types/keytar": "^4.4.0",
|
"@types/keytar": "^4.4.0",
|
||||||
"@types/lodash.debounce": "^4.0.6",
|
"@types/lodash.debounce": "^4.0.6",
|
||||||
|
@ -12,7 +12,6 @@ import { MaybePromise } from '@theia/core/lib/common/types';
|
|||||||
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
||||||
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
|
||||||
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
|
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -61,6 +60,7 @@ import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
|||||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||||
import { NotificationManager } from '../theia/messages/notifications-manager';
|
import { NotificationManager } from '../theia/messages/notifications-manager';
|
||||||
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
|
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
|
||||||
|
import { WorkspaceService } from '../theia/workspace/workspace-service';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Command,
|
Command,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { nls } from '@theia/core/lib/common/nls';
|
import { nls } from '@theia/core/lib/common/nls';
|
||||||
import { injectable } from '@theia/core/shared/inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import type { EditorOpenerOptions } from '@theia/editor/lib/browser/editor-manager';
|
import type { EditorOpenerOptions } from '@theia/editor/lib/browser/editor-manager';
|
||||||
import { Later } from '../../common/nls';
|
import { Later } from '../../common/nls';
|
||||||
import { SketchesError } from '../../common/protocol';
|
import { Sketch, SketchesError } from '../../common/protocol';
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandRegistry,
|
CommandRegistry,
|
||||||
@ -10,9 +10,19 @@ import {
|
|||||||
URI,
|
URI,
|
||||||
} from './contribution';
|
} from './contribution';
|
||||||
import { SaveAsSketch } from './save-as-sketch';
|
import { SaveAsSketch } from './save-as-sketch';
|
||||||
|
import { promptMoveSketch } from './open-sketch';
|
||||||
|
import { ApplicationError } from '@theia/core/lib/common/application-error';
|
||||||
|
import { Deferred, wait } from '@theia/core/lib/common/promise-util';
|
||||||
|
import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
|
||||||
|
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||||
|
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||||
|
import { ContextKeyService as VSCodeContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/browser/contextKeyService';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class OpenSketchFiles extends SketchContribution {
|
export class OpenSketchFiles extends SketchContribution {
|
||||||
|
@inject(VSCodeContextKeyService)
|
||||||
|
private readonly contextKeyService: VSCodeContextKeyService;
|
||||||
|
|
||||||
override registerCommands(registry: CommandRegistry): void {
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
registry.registerCommand(OpenSketchFiles.Commands.OPEN_SKETCH_FILES, {
|
registry.registerCommand(OpenSketchFiles.Commands.OPEN_SKETCH_FILES, {
|
||||||
execute: (uri: URI) => this.openSketchFiles(uri),
|
execute: (uri: URI) => this.openSketchFiles(uri),
|
||||||
@ -55,9 +65,25 @@ export class OpenSketchFiles extends SketchContribution {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const { workspaceError } = this.workspaceService;
|
||||||
|
// This happens when the IDE2 has been started (from either a terminal or clicking on an `ino` file) with a /path/to/invalid/sketch. (#964)
|
||||||
|
if (SketchesError.InvalidName.is(workspaceError)) {
|
||||||
|
await this.promptMove(workspaceError);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// This happens when the user gracefully closed IDE2, all went well
|
||||||
|
// but the main sketch file was renamed outside of IDE2 and when the user restarts the IDE2
|
||||||
|
// the workspace path still exists, but the sketch path is not valid anymore. (#964)
|
||||||
|
if (SketchesError.InvalidName.is(err)) {
|
||||||
|
const movedSketch = await this.promptMove(err);
|
||||||
|
if (!movedSketch) {
|
||||||
|
// If user did not accept the move, or move was not possible, force reload with a fallback.
|
||||||
|
return this.openFallbackSketch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (SketchesError.NotFound.is(err)) {
|
if (SketchesError.NotFound.is(err)) {
|
||||||
this.openFallbackSketch();
|
return this.openFallbackSketch();
|
||||||
} else {
|
} else {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
const message =
|
const message =
|
||||||
@ -71,6 +97,31 @@ export class OpenSketchFiles extends SketchContribution {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async promptMove(
|
||||||
|
err: ApplicationError<
|
||||||
|
number,
|
||||||
|
{
|
||||||
|
invalidMainSketchUri: string;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
): Promise<Sketch | undefined> {
|
||||||
|
const { invalidMainSketchUri } = err.data;
|
||||||
|
requestAnimationFrame(() => this.messageService.error(err.message));
|
||||||
|
await wait(10); // let IDE2 toast the error message.
|
||||||
|
const movedSketch = await promptMoveSketch(invalidMainSketchUri, {
|
||||||
|
fileService: this.fileService,
|
||||||
|
sketchService: this.sketchService,
|
||||||
|
labelProvider: this.labelProvider,
|
||||||
|
});
|
||||||
|
if (movedSketch) {
|
||||||
|
this.workspaceService.open(new URI(movedSketch.uri), {
|
||||||
|
preserveWindow: true,
|
||||||
|
});
|
||||||
|
return movedSketch;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
private async openFallbackSketch(): Promise<void> {
|
private async openFallbackSketch(): Promise<void> {
|
||||||
const sketch = await this.sketchService.createNewSketch();
|
const sketch = await this.sketchService.createNewSketch();
|
||||||
this.workspaceService.open(new URI(sketch.uri), { preserveWindow: true });
|
this.workspaceService.open(new URI(sketch.uri), { preserveWindow: true });
|
||||||
@ -84,8 +135,48 @@ export class OpenSketchFiles extends SketchContribution {
|
|||||||
const widget = this.editorManager.all.find(
|
const widget = this.editorManager.all.find(
|
||||||
(widget) => widget.editor.uri.toString() === uri
|
(widget) => widget.editor.uri.toString() === uri
|
||||||
);
|
);
|
||||||
|
const disposables = new DisposableCollection();
|
||||||
if (!widget || forceOpen) {
|
if (!widget || forceOpen) {
|
||||||
return this.editorManager.open(
|
const deferred = new Deferred<EditorWidget>();
|
||||||
|
disposables.push(
|
||||||
|
this.editorManager.onCreated((editor) => {
|
||||||
|
if (editor.editor.uri.toString() === uri) {
|
||||||
|
if (editor.isVisible) {
|
||||||
|
disposables.dispose();
|
||||||
|
deferred.resolve(editor);
|
||||||
|
} else {
|
||||||
|
// In Theia, the promise resolves after opening the editor, but the editor is neither attached to the DOM, nor visible.
|
||||||
|
// This is a hack to first get an event from monaco after the widget update request, then IDE2 waits for the next monaco context key event.
|
||||||
|
// Here, the monaco context key event is not used, but this is the first event after the editor is visible in the UI.
|
||||||
|
disposables.push(
|
||||||
|
(editor.editor as MonacoEditor).onDidResize((dimension) => {
|
||||||
|
if (dimension) {
|
||||||
|
const isKeyOwner = (
|
||||||
|
arg: unknown
|
||||||
|
): arg is { key: string } => {
|
||||||
|
if (typeof arg === 'object') {
|
||||||
|
const object = arg as Record<string, unknown>;
|
||||||
|
return typeof object['key'] === 'string';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
disposables.push(
|
||||||
|
this.contextKeyService.onDidChangeContext((e) => {
|
||||||
|
// `commentIsEmpty` is the first context key change event received from monaco after the editor is for real visible in the UI.
|
||||||
|
if (isKeyOwner(e) && e.key === 'commentIsEmpty') {
|
||||||
|
deferred.resolve(editor);
|
||||||
|
disposables.dispose();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.editorManager.open(
|
||||||
new URI(uri),
|
new URI(uri),
|
||||||
options ?? {
|
options ?? {
|
||||||
mode: 'reveal',
|
mode: 'reveal',
|
||||||
@ -93,6 +184,20 @@ export class OpenSketchFiles extends SketchContribution {
|
|||||||
counter: 0,
|
counter: 0,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
const timeout = 5_000; // number of ms IDE2 waits for the editor to show up in the UI
|
||||||
|
const result = await Promise.race([
|
||||||
|
deferred.promise,
|
||||||
|
wait(timeout).then(() => {
|
||||||
|
disposables.dispose();
|
||||||
|
return 'timeout';
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
if (result === 'timeout') {
|
||||||
|
console.warn(
|
||||||
|
`Timeout after ${timeout} millis. The editor has not shown up in time. URI: ${uri}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||||
import { nls } from '@theia/core/lib/common/nls';
|
import { nls } from '@theia/core/lib/common/nls';
|
||||||
import { injectable } from '@theia/core/shared/inversify';
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
import { SketchesError, SketchRef } from '../../common/protocol';
|
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||||
|
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
||||||
|
import {
|
||||||
|
SketchesError,
|
||||||
|
SketchesService,
|
||||||
|
SketchRef,
|
||||||
|
} from '../../common/protocol';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
@ -108,45 +114,11 @@ export class OpenSketch extends SketchContribution {
|
|||||||
return sketch;
|
return sketch;
|
||||||
}
|
}
|
||||||
if (Sketch.isSketchFile(sketchFileUri)) {
|
if (Sketch.isSketchFile(sketchFileUri)) {
|
||||||
const name = new URI(sketchFileUri).path.name;
|
return promptMoveSketch(sketchFileUri, {
|
||||||
const nameWithExt = this.labelProvider.getName(new URI(sketchFileUri));
|
fileService: this.fileService,
|
||||||
const { response } = await remote.dialog.showMessageBox({
|
sketchService: this.sketchService,
|
||||||
title: nls.localize('arduino/sketch/moving', 'Moving'),
|
labelProvider: this.labelProvider,
|
||||||
type: 'question',
|
|
||||||
buttons: [
|
|
||||||
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
|
|
||||||
nls.localize('vscode/issueMainService/ok', 'OK'),
|
|
||||||
],
|
|
||||||
message: nls.localize(
|
|
||||||
'arduino/sketch/movingMsg',
|
|
||||||
'The file "{0}" needs to be inside a sketch folder named "{1}".\nCreate this folder, move the file, and continue?',
|
|
||||||
nameWithExt,
|
|
||||||
name
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
if (response === 1) {
|
|
||||||
// OK
|
|
||||||
const newSketchUri = new URI(sketchFileUri).parent.resolve(name);
|
|
||||||
const exists = await this.fileService.exists(newSketchUri);
|
|
||||||
if (exists) {
|
|
||||||
await remote.dialog.showMessageBox({
|
|
||||||
type: 'error',
|
|
||||||
title: nls.localize('vscode/dialog/dialogErrorMessage', 'Error'),
|
|
||||||
message: nls.localize(
|
|
||||||
'arduino/sketch/cantOpen',
|
|
||||||
'A folder named "{0}" already exists. Can\'t open sketch.',
|
|
||||||
name
|
|
||||||
),
|
|
||||||
});
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
await this.fileService.createFolder(newSketchUri);
|
|
||||||
await this.fileService.move(
|
|
||||||
new URI(sketchFileUri),
|
|
||||||
new URI(newSketchUri.resolve(nameWithExt).toString())
|
|
||||||
);
|
|
||||||
return this.sketchService.getSketchFolder(newSketchUri.toString());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -158,3 +130,55 @@ export namespace OpenSketch {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function promptMoveSketch(
|
||||||
|
sketchFileUri: string | URI,
|
||||||
|
options: {
|
||||||
|
fileService: FileService;
|
||||||
|
sketchService: SketchesService;
|
||||||
|
labelProvider: LabelProvider;
|
||||||
|
}
|
||||||
|
): Promise<Sketch | undefined> {
|
||||||
|
const { fileService, sketchService, labelProvider } = options;
|
||||||
|
const uri =
|
||||||
|
sketchFileUri instanceof URI ? sketchFileUri : new URI(sketchFileUri);
|
||||||
|
const name = uri.path.name;
|
||||||
|
const nameWithExt = labelProvider.getName(uri);
|
||||||
|
const { response } = await remote.dialog.showMessageBox({
|
||||||
|
title: nls.localize('arduino/sketch/moving', 'Moving'),
|
||||||
|
type: 'question',
|
||||||
|
buttons: [
|
||||||
|
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
|
||||||
|
nls.localize('vscode/issueMainService/ok', 'OK'),
|
||||||
|
],
|
||||||
|
message: nls.localize(
|
||||||
|
'arduino/sketch/movingMsg',
|
||||||
|
'The file "{0}" needs to be inside a sketch folder named "{1}".\nCreate this folder, move the file, and continue?',
|
||||||
|
nameWithExt,
|
||||||
|
name
|
||||||
|
),
|
||||||
|
});
|
||||||
|
if (response === 1) {
|
||||||
|
// OK
|
||||||
|
const newSketchUri = uri.parent.resolve(name);
|
||||||
|
const exists = await fileService.exists(newSketchUri);
|
||||||
|
if (exists) {
|
||||||
|
await remote.dialog.showMessageBox({
|
||||||
|
type: 'error',
|
||||||
|
title: nls.localize('vscode/dialog/dialogErrorMessage', 'Error'),
|
||||||
|
message: nls.localize(
|
||||||
|
'arduino/sketch/cantOpen',
|
||||||
|
'A folder named "{0}" already exists. Can\'t open sketch.',
|
||||||
|
name
|
||||||
|
),
|
||||||
|
});
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
await fileService.createFolder(newSketchUri);
|
||||||
|
await fileService.move(
|
||||||
|
uri,
|
||||||
|
new URI(newSketchUri.resolve(nameWithExt).toString())
|
||||||
|
);
|
||||||
|
return sketchService.getSketchFolder(newSketchUri.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
SketchesService,
|
SketchesService,
|
||||||
Sketch,
|
Sketch,
|
||||||
|
SketchesError,
|
||||||
} from '../../../common/protocol/sketches-service';
|
} from '../../../common/protocol/sketches-service';
|
||||||
import { FileStat } from '@theia/filesystem/lib/common/files';
|
import { FileStat } from '@theia/filesystem/lib/common/files';
|
||||||
import {
|
import {
|
||||||
@ -38,6 +39,7 @@ export class WorkspaceService extends TheiaWorkspaceService {
|
|||||||
private readonly providers: ContributionProvider<StartupTaskProvider>;
|
private readonly providers: ContributionProvider<StartupTaskProvider>;
|
||||||
|
|
||||||
private version?: string;
|
private version?: string;
|
||||||
|
private _workspaceError: Error | undefined;
|
||||||
|
|
||||||
async onStart(application: FrontendApplication): Promise<void> {
|
async onStart(application: FrontendApplication): Promise<void> {
|
||||||
const info = await this.applicationServer.getApplicationInfo();
|
const info = await this.applicationServer.getApplicationInfo();
|
||||||
@ -51,6 +53,10 @@ export class WorkspaceService extends TheiaWorkspaceService {
|
|||||||
this.onCurrentWidgetChange({ newValue, oldValue: null });
|
this.onCurrentWidgetChange({ newValue, oldValue: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get workspaceError(): Error | undefined {
|
||||||
|
return this._workspaceError;
|
||||||
|
}
|
||||||
|
|
||||||
protected override async toFileStat(
|
protected override async toFileStat(
|
||||||
uri: string | URI | undefined
|
uri: string | URI | undefined
|
||||||
): Promise<FileStat | undefined> {
|
): Promise<FileStat | undefined> {
|
||||||
@ -59,6 +65,31 @@ export class WorkspaceService extends TheiaWorkspaceService {
|
|||||||
const newSketchUri = await this.sketchService.createNewSketch();
|
const newSketchUri = await this.sketchService.createNewSketch();
|
||||||
return this.toFileStat(newSketchUri.uri);
|
return this.toFileStat(newSketchUri.uri);
|
||||||
}
|
}
|
||||||
|
// When opening a file instead of a directory, IDE2 (and Theia) expects a workspace JSON file.
|
||||||
|
// Nothing will work if the workspace file is invalid. Users tend to start (see #964) IDE2 from the `.ino` files,
|
||||||
|
// so here, IDE2 tries to load the sketch via the CLI from the main sketch file URI.
|
||||||
|
// If loading the sketch is OK, IDE2 starts and uses the sketch folder as the workspace root instead of the sketch file.
|
||||||
|
// If loading fails due to invalid name error, IDE2 loads a temp sketch and preserves the startup error, and offers the sketch move to the user later.
|
||||||
|
// If loading the sketch fails, create a fallback sketch and open the new temp sketch folder as the workspace root.
|
||||||
|
if (stat.isFile && stat.resource.path.ext === '.ino') {
|
||||||
|
try {
|
||||||
|
const sketch = await this.sketchService.loadSketch(
|
||||||
|
stat.resource.toString()
|
||||||
|
);
|
||||||
|
return this.toFileStat(sketch.uri);
|
||||||
|
} catch (err) {
|
||||||
|
if (SketchesError.InvalidName.is(err)) {
|
||||||
|
this._workspaceError = err;
|
||||||
|
const newSketchUri = await this.sketchService.createNewSketch();
|
||||||
|
return this.toFileStat(newSketchUri.uri);
|
||||||
|
} else if (SketchesError.NotFound.is(err)) {
|
||||||
|
this._workspaceError = err;
|
||||||
|
const newSketchUri = await this.sketchService.createNewSketch();
|
||||||
|
return this.toFileStat(newSketchUri.uri);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
return stat;
|
return stat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import URI from '@theia/core/lib/common/uri';
|
|||||||
export namespace SketchesError {
|
export namespace SketchesError {
|
||||||
export const Codes = {
|
export const Codes = {
|
||||||
NotFound: 5001,
|
NotFound: 5001,
|
||||||
|
InvalidName: 5002,
|
||||||
};
|
};
|
||||||
export const NotFound = ApplicationError.declare(
|
export const NotFound = ApplicationError.declare(
|
||||||
Codes.NotFound,
|
Codes.NotFound,
|
||||||
@ -14,6 +15,15 @@ export namespace SketchesError {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
export const InvalidName = ApplicationError.declare(
|
||||||
|
Codes.InvalidName,
|
||||||
|
(message: string, invalidMainSketchUri: string) => {
|
||||||
|
return {
|
||||||
|
message,
|
||||||
|
data: { invalidMainSketchUri },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SketchesServicePath = '/services/sketches-service';
|
export const SketchesServicePath = '/services/sketches-service';
|
||||||
|
@ -8,8 +8,8 @@ import {
|
|||||||
} from '@theia/core/electron-shared/electron';
|
} from '@theia/core/electron-shared/electron';
|
||||||
import { fork } from 'child_process';
|
import { fork } from 'child_process';
|
||||||
import { AddressInfo } from 'net';
|
import { AddressInfo } from 'net';
|
||||||
import { join, dirname } from 'path';
|
import { join, isAbsolute, resolve } from 'path';
|
||||||
import * as fs from 'fs-extra';
|
import { promises as fs, Stats } from 'fs';
|
||||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||||
import { ElectronSecurityToken } from '@theia/core/lib/electron-common/electron-token';
|
import { ElectronSecurityToken } from '@theia/core/lib/electron-common/electron-token';
|
||||||
import { FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
|
import { FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
|
||||||
@ -27,6 +27,7 @@ import {
|
|||||||
CLOSE_PLOTTER_WINDOW,
|
CLOSE_PLOTTER_WINDOW,
|
||||||
SHOW_PLOTTER_WINDOW,
|
SHOW_PLOTTER_WINDOW,
|
||||||
} from '../../common/ipc-communication';
|
} from '../../common/ipc-communication';
|
||||||
|
import isValidPath = require('is-valid-path');
|
||||||
|
|
||||||
app.commandLine.appendSwitch('disable-http-cache');
|
app.commandLine.appendSwitch('disable-http-cache');
|
||||||
|
|
||||||
@ -69,8 +70,10 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
|
|||||||
// Explicitly set the app name to have better menu items on macOS. ("About", "Hide", and "Quit")
|
// Explicitly set the app name to have better menu items on macOS. ("About", "Hide", and "Quit")
|
||||||
// See: https://github.com/electron-userland/electron-builder/issues/2468
|
// See: https://github.com/electron-userland/electron-builder/issues/2468
|
||||||
// Regression in Theia: https://github.com/eclipse-theia/theia/issues/8701
|
// Regression in Theia: https://github.com/eclipse-theia/theia/issues/8701
|
||||||
|
console.log(`${config.applicationName} ${app.getVersion()}`);
|
||||||
app.on('ready', () => app.setName(config.applicationName));
|
app.on('ready', () => app.setName(config.applicationName));
|
||||||
this.attachFileAssociations();
|
const cwd = process.cwd();
|
||||||
|
this.attachFileAssociations(cwd);
|
||||||
this.useNativeWindowFrame = this.getTitleBarStyle(config) === 'native';
|
this.useNativeWindowFrame = this.getTitleBarStyle(config) === 'native';
|
||||||
this._config = config;
|
this._config = config;
|
||||||
this.hookApplicationEvents();
|
this.hookApplicationEvents();
|
||||||
@ -84,7 +87,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
|
|||||||
return this.launch({
|
return this.launch({
|
||||||
secondInstance: false,
|
secondInstance: false,
|
||||||
argv: this.processArgv.getProcessArgvWithoutBin(process.argv),
|
argv: this.processArgv.getProcessArgvWithoutBin(process.argv),
|
||||||
cwd: process.cwd(),
|
cwd,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,7 +122,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
|
|||||||
let traceFile: string | undefined;
|
let traceFile: string | undefined;
|
||||||
if (appPath) {
|
if (appPath) {
|
||||||
const tracesPath = join(appPath, 'traces');
|
const tracesPath = join(appPath, 'traces');
|
||||||
await fs.promises.mkdir(tracesPath, { recursive: true });
|
await fs.mkdir(tracesPath, { recursive: true });
|
||||||
traceFile = join(tracesPath, `trace-${new Date().toISOString()}.trace`);
|
traceFile = join(tracesPath, `trace-${new Date().toISOString()}.trace`);
|
||||||
}
|
}
|
||||||
console.log('>>> Content tracing has started...');
|
console.log('>>> Content tracing has started...');
|
||||||
@ -135,14 +138,18 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
|
|||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
private attachFileAssociations(): void {
|
private attachFileAssociations(cwd: string): void {
|
||||||
// OSX: register open-file event
|
// OSX: register open-file event
|
||||||
if (os.isOSX) {
|
if (os.isOSX) {
|
||||||
app.on('open-file', async (event, uri) => {
|
app.on('open-file', async (event, path) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (uri.endsWith('.ino') && (await fs.pathExists(uri))) {
|
const resolvedPath = await this.resolvePath(path, cwd);
|
||||||
this.openFilePromise.reject();
|
if (resolvedPath) {
|
||||||
await this.openSketch(dirname(uri));
|
const sketchFolderPath = await this.isValidSketchPath(resolvedPath);
|
||||||
|
if (sketchFolderPath) {
|
||||||
|
this.openFilePromise.reject(new InterruptWorkspaceRestoreError());
|
||||||
|
await this.openSketch(sketchFolderPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setTimeout(() => this.openFilePromise.resolve(), 500);
|
setTimeout(() => this.openFilePromise.resolve(), 500);
|
||||||
@ -151,8 +158,68 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async isValidSketchPath(uri: string): Promise<boolean | undefined> {
|
/**
|
||||||
return typeof uri === 'string' && (await fs.pathExists(uri));
|
* The `path` argument is valid, if accessible and either pointing to a `.ino` file,
|
||||||
|
* or it's a directory, and one of the files in the directory is an `.ino` file.
|
||||||
|
*
|
||||||
|
* If `undefined`, `path` was pointing to neither an accessible sketch file nor a sketch folder.
|
||||||
|
*
|
||||||
|
* The sketch folder name and sketch file name can be different. This method is not sketch folder name compliant.
|
||||||
|
* The `path` must be an absolute, resolved path.
|
||||||
|
*/
|
||||||
|
private async isValidSketchPath(path: string): Promise<string | undefined> {
|
||||||
|
let stats: Stats | undefined = undefined;
|
||||||
|
try {
|
||||||
|
stats = await fs.stat(path);
|
||||||
|
} catch (err) {
|
||||||
|
if ('code' in err && err.code === 'ENOENT') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
if (!stats) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (stats.isFile() && path.endsWith('.ino')) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const entries = await fs.readdir(path, { withFileTypes: true });
|
||||||
|
const sketchFilename = entries
|
||||||
|
.filter((entry) => entry.isFile() && entry.name.endsWith('.ino'))
|
||||||
|
.map(({ name }) => name)
|
||||||
|
.sort((left, right) => left.localeCompare(right))[0];
|
||||||
|
if (sketchFilename) {
|
||||||
|
return join(path, sketchFilename);
|
||||||
|
}
|
||||||
|
// If no sketches found in the folder, but the folder exists,
|
||||||
|
// return with the path of the empty folder and let IDE2's frontend
|
||||||
|
// figure out the workspace root.
|
||||||
|
return path;
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async resolvePath(
|
||||||
|
maybePath: string,
|
||||||
|
cwd: string
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
if (!isValidPath(maybePath)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (isAbsolute(maybePath)) {
|
||||||
|
return maybePath;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const resolved = await fs.realpath(resolve(cwd, maybePath));
|
||||||
|
return resolved;
|
||||||
|
} catch (err) {
|
||||||
|
if ('code' in err && err.code === 'ENOENT') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async launch(
|
protected override async launch(
|
||||||
@ -163,12 +230,15 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
|
|||||||
// 1. The `open-file` command has been received by the app, rejecting the promise
|
// 1. The `open-file` command has been received by the app, rejecting the promise
|
||||||
// 2. A short timeout resolves the promise automatically, falling back to the usual app launch
|
// 2. A short timeout resolves the promise automatically, falling back to the usual app launch
|
||||||
await this.openFilePromise.promise;
|
await this.openFilePromise.promise;
|
||||||
} catch {
|
} catch (err) {
|
||||||
// Application has received the `open-file` event and will skip the default application launch
|
if (err instanceof InterruptWorkspaceRestoreError) {
|
||||||
return;
|
// Application has received the `open-file` event and will skip the default application launch
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!os.isOSX && (await this.launchFromArgs(params))) {
|
if (await this.launchFromArgs(params)) {
|
||||||
// Application has received a file in its arguments and will skip the default application launch
|
// Application has received a file in its arguments and will skip the default application launch
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -182,7 +252,13 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
|
|||||||
`Restoring workspace roots: ${workspaces.map(({ file }) => file)}`
|
`Restoring workspace roots: ${workspaces.map(({ file }) => file)}`
|
||||||
);
|
);
|
||||||
for (const workspace of workspaces) {
|
for (const workspace of workspaces) {
|
||||||
if (await this.isValidSketchPath(workspace.file)) {
|
const resolvedPath = await this.resolvePath(workspace.file, params.cwd);
|
||||||
|
if (!resolvedPath) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const sketchFolderPath = await this.isValidSketchPath(resolvedPath);
|
||||||
|
if (sketchFolderPath) {
|
||||||
|
workspace.file = sketchFolderPath;
|
||||||
if (this.isTempSketch.is(workspace.file)) {
|
if (this.isTempSketch.is(workspace.file)) {
|
||||||
console.info(
|
console.info(
|
||||||
`Skipped opening sketch. The sketch was detected as temporary. Workspace path: ${workspace.file}.`
|
`Skipped opening sketch. The sketch was detected as temporary. Workspace path: ${workspace.file}.`
|
||||||
@ -205,38 +281,40 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
|
|||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
// Copy to prevent manipulation of original array
|
// Copy to prevent manipulation of original array
|
||||||
const argCopy = [...params.argv];
|
const argCopy = [...params.argv];
|
||||||
let uri: string | undefined;
|
let path: string | undefined;
|
||||||
for (const possibleUri of argCopy) {
|
for (const maybePath of argCopy) {
|
||||||
if (
|
const resolvedPath = await this.resolvePath(maybePath, params.cwd);
|
||||||
possibleUri.endsWith('.ino') &&
|
if (!resolvedPath) {
|
||||||
(await this.isValidSketchPath(possibleUri))
|
continue;
|
||||||
) {
|
}
|
||||||
uri = possibleUri;
|
const sketchFolderPath = await this.isValidSketchPath(resolvedPath);
|
||||||
|
if (sketchFolderPath) {
|
||||||
|
path = sketchFolderPath;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (uri) {
|
if (path) {
|
||||||
await this.openSketch(dirname(uri));
|
await this.openSketch(path);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async openSketch(
|
private async openSketch(
|
||||||
workspace: WorkspaceOptions | string
|
workspaceOrPath: WorkspaceOptions | string
|
||||||
): Promise<BrowserWindow> {
|
): Promise<BrowserWindow> {
|
||||||
const options = await this.getLastWindowOptions();
|
const options = await this.getLastWindowOptions();
|
||||||
let file: string;
|
let file: string;
|
||||||
if (typeof workspace === 'object') {
|
if (typeof workspaceOrPath === 'object') {
|
||||||
options.x = workspace.x;
|
options.x = workspaceOrPath.x;
|
||||||
options.y = workspace.y;
|
options.y = workspaceOrPath.y;
|
||||||
options.width = workspace.width;
|
options.width = workspaceOrPath.width;
|
||||||
options.height = workspace.height;
|
options.height = workspaceOrPath.height;
|
||||||
options.isMaximized = workspace.isMaximized;
|
options.isMaximized = workspaceOrPath.isMaximized;
|
||||||
options.isFullScreen = workspace.isFullScreen;
|
options.isFullScreen = workspaceOrPath.isFullScreen;
|
||||||
file = workspace.file;
|
file = workspaceOrPath.file;
|
||||||
} else {
|
} else {
|
||||||
file = workspace;
|
file = workspaceOrPath;
|
||||||
}
|
}
|
||||||
const [uri, electronWindow] = await Promise.all([
|
const [uri, electronWindow] = await Promise.all([
|
||||||
this.createWindowUri(),
|
this.createWindowUri(),
|
||||||
@ -486,3 +564,12 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
|
|||||||
return this._firstWindowId;
|
return this._firstWindowId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class InterruptWorkspaceRestoreError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super(
|
||||||
|
"Received 'open-file' event. Interrupting the default launch workflow."
|
||||||
|
);
|
||||||
|
Object.setPrototypeOf(this, InterruptWorkspaceRestoreError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,7 +4,6 @@ import {
|
|||||||
ArduinoFirmwareUploader,
|
ArduinoFirmwareUploader,
|
||||||
ArduinoFirmwareUploaderPath,
|
ArduinoFirmwareUploaderPath,
|
||||||
} from '../common/protocol/arduino-firmware-uploader';
|
} from '../common/protocol/arduino-firmware-uploader';
|
||||||
|
|
||||||
import { ILogger } from '@theia/core/lib/common/logger';
|
import { ILogger } from '@theia/core/lib/common/logger';
|
||||||
import {
|
import {
|
||||||
BackendApplicationContribution,
|
BackendApplicationContribution,
|
||||||
@ -26,7 +25,7 @@ import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connec
|
|||||||
import { CoreClientProvider } from './core-client-provider';
|
import { CoreClientProvider } from './core-client-provider';
|
||||||
import { ConnectionHandler, JsonRpcConnectionHandler } from '@theia/core';
|
import { ConnectionHandler, JsonRpcConnectionHandler } from '@theia/core';
|
||||||
import { DefaultWorkspaceServer } from './theia/workspace/default-workspace-server';
|
import { DefaultWorkspaceServer } from './theia/workspace/default-workspace-server';
|
||||||
import { WorkspaceServer as TheiaWorkspaceServer } from '@theia/workspace/lib/common';
|
import { WorkspaceServer as TheiaWorkspaceServer } from '@theia/workspace/lib/common/workspace-protocol';
|
||||||
import { SketchesServiceImpl } from './sketches-service-impl';
|
import { SketchesServiceImpl } from './sketches-service-impl';
|
||||||
import {
|
import {
|
||||||
SketchesService,
|
SketchesService,
|
||||||
@ -40,7 +39,6 @@ import {
|
|||||||
ArduinoDaemon,
|
ArduinoDaemon,
|
||||||
ArduinoDaemonPath,
|
ArduinoDaemonPath,
|
||||||
} from '../common/protocol/arduino-daemon';
|
} from '../common/protocol/arduino-daemon';
|
||||||
|
|
||||||
import { ConfigServiceImpl } from './config-service-impl';
|
import { ConfigServiceImpl } from './config-service-impl';
|
||||||
import { EnvVariablesServer as TheiaEnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
import { EnvVariablesServer as TheiaEnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||||
import { EnvVariablesServer } from './theia/env-variables/env-variables-server';
|
import { EnvVariablesServer } from './theia/env-variables/env-variables-server';
|
||||||
|
@ -199,11 +199,22 @@ export class SketchesServiceImpl
|
|||||||
const sketch = await new Promise<SketchWithDetails>((resolve, reject) => {
|
const sketch = await new Promise<SketchWithDetails>((resolve, reject) => {
|
||||||
client.loadSketch(req, async (err, resp) => {
|
client.loadSketch(req, async (err, resp) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(
|
let rejectWith: unknown = err;
|
||||||
isNotFoundError(err)
|
if (isNotFoundError(err)) {
|
||||||
? SketchesError.NotFound(err.details, uri)
|
const invalidMainSketchFilePath = await isInvalidSketchNameError(
|
||||||
: err
|
err,
|
||||||
);
|
requestSketchPath
|
||||||
|
);
|
||||||
|
if (invalidMainSketchFilePath) {
|
||||||
|
rejectWith = SketchesError.InvalidName(
|
||||||
|
err.details,
|
||||||
|
FileUri.create(invalidMainSketchFilePath).toString()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
rejectWith = SketchesError.NotFound(err.details, uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reject(rejectWith);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const responseSketchPath = maybeNormalizeDrive(resp.getLocationPath());
|
const responseSketchPath = maybeNormalizeDrive(resp.getLocationPath());
|
||||||
@ -313,7 +324,10 @@ export class SketchesServiceImpl
|
|||||||
)} before marking it as recently opened.`
|
)} before marking it as recently opened.`
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (SketchesError.NotFound.is(err)) {
|
if (
|
||||||
|
SketchesError.NotFound.is(err) ||
|
||||||
|
SketchesError.InvalidName.is(err)
|
||||||
|
) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Could not load sketch from '${uri}'. Not marking as recently opened.`
|
`Could not load sketch from '${uri}'. Not marking as recently opened.`
|
||||||
);
|
);
|
||||||
@ -517,7 +531,7 @@ export class SketchesServiceImpl
|
|||||||
const sketch = await this.loadSketch(uri);
|
const sketch = await this.loadSketch(uri);
|
||||||
return sketch;
|
return sketch;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (SketchesError.NotFound.is(err)) {
|
if (SketchesError.NotFound.is(err) || SketchesError.InvalidName.is(err)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
@ -704,6 +718,63 @@ function isNotFoundError(err: unknown): err is ServiceError {
|
|||||||
return ServiceError.is(err) && err.code === 5; // `NOT_FOUND` https://grpc.github.io/grpc/core/md_doc_statuscodes.html
|
return ServiceError.is(err) && err.code === 5; // `NOT_FOUND` https://grpc.github.io/grpc/core/md_doc_statuscodes.html
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to detect whether the error was caused by an invalid main sketch file name.
|
||||||
|
* IDE2 should handle gracefully when there is an invalid sketch folder name. See the [spec](https://arduino.github.io/arduino-cli/latest/sketch-specification/#sketch-root-folder) for details.
|
||||||
|
* The CLI does not have error codes (https://github.com/arduino/arduino-cli/issues/1762), so IDE2 parses the error message and tries to guess it.
|
||||||
|
* Nothing guarantees that the invalid existing main sketch file still exits by the time client performs the sketch move.
|
||||||
|
*/
|
||||||
|
async function isInvalidSketchNameError(
|
||||||
|
cliErr: unknown,
|
||||||
|
requestSketchPath: string
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
if (isNotFoundError(cliErr)) {
|
||||||
|
const ino = requestSketchPath.endsWith('.ino');
|
||||||
|
if (ino) {
|
||||||
|
const sketchFolderPath = path.dirname(requestSketchPath);
|
||||||
|
const sketchName = path.basename(sketchFolderPath);
|
||||||
|
const pattern = `${invalidSketchNameErrorRegExpPrefix}${path.join(
|
||||||
|
sketchFolderPath,
|
||||||
|
`${sketchName}.ino`
|
||||||
|
)}`.replace(/\\/g, '\\\\'); // make windows path separator with \\ to have a valid regexp.
|
||||||
|
if (new RegExp(pattern, 'i').test(cliErr.details)) {
|
||||||
|
try {
|
||||||
|
await fs.access(requestSketchPath);
|
||||||
|
return requestSketchPath;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const resources = await fs.readdir(requestSketchPath, {
|
||||||
|
withFileTypes: true,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
resources
|
||||||
|
.filter((resource) => resource.isFile())
|
||||||
|
.filter((resource) => resource.name.endsWith('.ino'))
|
||||||
|
// A folder might contain multiple sketches. It's OK to ick the first one as IDE2 cannot do much,
|
||||||
|
// but ensure a deterministic behavior as `readdir(3)` does not guarantee an order. Sort them.
|
||||||
|
.sort(({ name: left }, { name: right }) =>
|
||||||
|
left.localeCompare(right)
|
||||||
|
)
|
||||||
|
.map(({ name }) => name)
|
||||||
|
.map((name) => path.join(requestSketchPath, name))[0]
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if ('code' in err && err.code === 'ENOTDIR') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const invalidSketchNameErrorRegExpPrefix =
|
||||||
|
'.*: main file missing from sketch: ';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* When a new sketch is created, add a suffix to distinguish it
|
* When a new sketch is created, add a suffix to distinguish it
|
||||||
* from other new sketches I created today.
|
* from other new sketches I created today.
|
||||||
|
@ -3139,6 +3139,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812"
|
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812"
|
||||||
integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==
|
integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==
|
||||||
|
|
||||||
|
"@types/is-valid-path@^0.1.0":
|
||||||
|
version "0.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/is-valid-path/-/is-valid-path-0.1.0.tgz#d5c6e96801303112c9626d44268c6fabc72d272f"
|
||||||
|
integrity sha512-2ontWtpN8O2nf5S7EjDDJ0DwrRa2t7wmS3Wmo322yWYG6yFBYC1QCaLhz4Iz+mzJy8Kf4zP5yVyEd1ANPDmOFQ==
|
||||||
|
|
||||||
"@types/js-yaml@^3.12.2":
|
"@types/js-yaml@^3.12.2":
|
||||||
version "3.12.7"
|
version "3.12.7"
|
||||||
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.7.tgz#330c5d97a3500e9c903210d6e49f02964af04a0e"
|
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.7.tgz#330c5d97a3500e9c903210d6e49f02964af04a0e"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user