diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 651d77e9a..d0bdd278a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,7 +60,9 @@ jobs: if [ $IS_FORK = true ]; then echo "Skipping the app signing: building from a fork." else + export BUILD_SUFFIX="linux"; if [ "${{ runner.OS }}" = "macOS" ]; then + export BUILD_SUFFIX="mac"; export CSC_LINK="${{ runner.temp }}/signing_certificate.p12" # APPLE_SIGNING_CERTIFICATE_P12 secret was produced by following the procedure from: # https://www.kencochrane.com/2020/08/01/build-and-sign-golang-binaries-for-macos-with-github-actions/#exporting-the-developer-certificate @@ -69,6 +71,7 @@ jobs: export CSC_KEY_PASSWORD="${{ secrets.KEYCHAIN_PASSWORD }}" elif [ "${{ runner.OS }}" = "Windows" ]; then + export BUILD_SUFFIX=""; export CSC_LINK="${{ runner.temp }}/signing_certificate.pfx" npm config set msvs_version 2017 --global echo "${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PFX }}" | base64 --decode > "$CSC_LINK" @@ -78,7 +81,15 @@ jobs: fi yarn --cwd ./electron/packager/ - yarn --cwd ./electron/packager/ package + yarn --cwd ./electron/packager/ package + + export BUILD_PREFIX="stable" + if [ "$IS_NIGHTLY" = true ]; then + export BUILD_PREFIX="nightly" + fi + + mv electron/build/dist/latest-$BUILD_SUFFIX.yml electron/build/dist/$BUILD_PREFIX-$BUILD_SUFFIX.yml + rm electron/build/dist/alpha* electron/build/dist/beta* - name: Upload [GitHub Actions] uses: actions/upload-artifact@v2 @@ -98,7 +109,9 @@ jobs: - path: '*Linux_64bit.zip' name: Linux_X86-64 - path: '*macOS_64bit.dmg' - name: macOS + name: macOS_dmg + - path: '*macOS_64bit.zip' + name: macOS_zip - path: '*Windows_64bit.exe' name: Windows_X86-64_interactive_installer - path: '*Windows_64bit.msi' diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 2d6ff2b63..3f21181c1 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -65,6 +65,7 @@ "css-element-queries": "^1.2.0", "dateformat": "^3.0.3", "deepmerge": "2.0.1", + "electron-updater": "^4.6.5", "fuzzy": "^0.1.3", "glob": "^7.1.6", "google-protobuf": "^3.11.4", @@ -82,6 +83,7 @@ "ps-tree": "^1.2.0", "query-string": "^7.0.1", "react-disable": "^0.1.0", + "react-markdown": "^8.0.0", "react-select": "^3.0.4", "react-tabs": "^3.1.2", "react-window": "^1.8.6", diff --git a/arduino-ide-extension/scripts/compose-changelog.js b/arduino-ide-extension/scripts/compose-changelog.js index 36b5c94c0..aba9dae46 100755 --- a/arduino-ide-extension/scripts/compose-changelog.js +++ b/arduino-ide-extension/scripts/compose-changelog.js @@ -1,36 +1,42 @@ // @ts-check - (async () => { const { Octokit } = require('@octokit/rest'); - const fs = require("fs"); - const path = require("path"); + const fs = require('fs'); + const path = require('path'); const octokit = new Octokit({ userAgent: 'Arduino IDE compose-changelog.js', }); - const response = await octokit.rest.repos.listReleases({ - owner: 'arduino', - repo: 'arduino-ide', - }).catch(err => { - console.error(err); - process.exit(1); - }) + const response = await octokit.rest.repos + .listReleases({ + owner: 'arduino', + repo: 'arduino-ide', + }) + .catch((err) => { + console.error(err); + process.exit(1); + }); const releases = response.data; - let fullChangelog = releases.reduce((acc, item) => { + let fullChangelog = releases.reduce((acc, item, index) => { // Process each line separately - const body = item.body.split('\n').map(processLine).join('\n') + const body = item.body.split('\n').map(processLine).join('\n'); // item.name is the name of the release changelog - return acc + `# ${item.name}\n\n${body}\n\n---\n\n`; + return ( + acc + + `## ${item.name}\n\n${body}${ + index !== releases.length - 1 ? '\n\n---\n\n' : '\n' + }` + ); }, ''); - const args = process.argv.slice(2) + const args = process.argv.slice(2); if (args.length == 0) { - console.error("Missing argument to destination file") - process.exit(1) + console.error('Missing argument to destination file'); + process.exit(1); } const changelogFile = path.resolve(args[0]); @@ -38,19 +44,18 @@ changelogFile, fullChangelog, { - flag: "w+", + flag: 'w+', }, - err => { + (err) => { if (err) { console.error(err); process.exit(1); } - console.log("Changelog written to", changelogFile); + console.log('Changelog written to', changelogFile); } - ) + ); })(); - // processLine applies different substitutions to line string. // We're assuming that there are no more than one substitution // per line to be applied. @@ -61,7 +66,8 @@ const processLine = (line) => { // * [#123](https://github.com/arduino/arduino-ide/pull/123/) // * [#123](https://github.com/arduino/arduino-ide/issues/123/) // If it does return the line as is. - let r = /(\(|\[)#\d+(\)|\])(\(|\[)https:\/\/github\.com\/arduino\/arduino-ide\/(pull|issues)\/(\d+)\/?(\)|\])/gm; + let r = + /(\(|\[)#\d+(\)|\])(\(|\[)https:\/\/github\.com\/arduino\/arduino-ide\/(pull|issues)\/(\d+)\/?(\)|\])/gm; if (r.test(line)) { return line; } @@ -70,9 +76,12 @@ const processLine = (line) => { // * #123 // If it does it's changed to: // * [#123](https://github.com/arduino/arduino-ide/pull/123) - r = /#(\d+)/gm; + r = /(?<![\w\d\/_]{1})#((\d)+)(?![\w\d\/_]{1})/gm; if (r.test(line)) { - return line.replace(r, `[#$1](https://github.com/arduino/arduino-ide/pull/$1)`) + return line.replace( + r, + `[#$1](https://github.com/arduino/arduino-ide/pull/$1)` + ); } // Check if a link with one of the following format exists: @@ -85,7 +94,8 @@ const processLine = (line) => { // * [#123](https://github.com/arduino/arduino-ide/issues/123) // * [#123](https://github.com/arduino/arduino-ide/pull/123/) // * [#123](https://github.com/arduino/arduino-ide/issues/123/) - r = /(https:\/\/github\.com\/arduino\/arduino-ide\/(pull|issues)\/(\d+)\/?)/gm; + r = + /(https:\/\/github\.com\/arduino\/arduino-ide\/(pull|issues)\/(\d+)\/?)/gm; if (r.test(line)) { return line.replace(r, `[#$3]($1)`); } @@ -95,11 +105,12 @@ const processLine = (line) => { // * https://github.com/arduino/arduino-ide/compare/2.0.0-rc2...2.0.0-rc3/ // If it does it's changed to: // * [`2.0.0-rc2...2.0.0-rc3`](https://github.com/arduino/arduino-ide/compare/2.0.0-rc2...2.0.0-rc3) - r = /(https:\/\/github\.com\/arduino\/arduino-ide\/compare\/([^\/]*))\/?\s?/gm; + r = + /(https:\/\/github\.com\/arduino\/arduino-ide\/compare\/([^\/]*))\/?\s?/gm; if (r.test(line)) { - return line.replace(r, '[`$2`]($1)');; + return line.replace(r, '[`$2`]($1)'); } // If nothing matches just return the line as is return line; -} +}; diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index b493bd3bf..7b0184bdd 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -68,8 +68,12 @@ import { ArduinoPreferences } from './arduino-preferences'; import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl'; import { SaveAsSketch } from './contributions/save-as-sketch'; import { SketchbookWidgetContribution } from './widgets/sketchbook/sketchbook-widget-contribution'; +import { IDEUpdaterCommands } from './ide-updater/ide-updater-commands'; +import { IDEUpdaterDialog } from './dialogs/ide-updater/ide-updater-dialog'; +import { IDEUpdater } from '../common/protocol/ide-updater'; const INIT_LIBS_AND_PACKAGES = 'initializedLibsAndPackages'; +export const SKIP_IDE_VERSION = 'skipIDEVersion'; @injectable() export class ArduinoFrontendContribution @@ -78,8 +82,7 @@ export class ArduinoFrontendContribution TabBarToolbarContribution, CommandContribution, MenuContribution, - ColorContribution -{ + ColorContribution { @inject(ILogger) protected logger: ILogger; @@ -157,6 +160,15 @@ export class ArduinoFrontendContribution @inject(LocalStorageService) protected readonly localStorageService: LocalStorageService; + @inject(IDEUpdaterCommands) + protected readonly updater: IDEUpdaterCommands; + + @inject(IDEUpdaterDialog) + protected readonly updaterDialog: IDEUpdaterDialog; + + @inject(IDEUpdater) + protected readonly updaterService: IDEUpdater; + protected invalidConfigPopup: | Promise<void | 'No' | 'Yes' | undefined> | undefined; @@ -251,7 +263,7 @@ export class ArduinoFrontendContribution }); } - onStart(app: FrontendApplication): void { + async onStart(app: FrontendApplication): Promise<void> { // Initialize all `pro-mode` widgets. This is a NOOP if in normal mode. for (const viewContribution of [ this.fileNavigatorContributions, @@ -266,6 +278,19 @@ export class ArduinoFrontendContribution viewContribution.initializeLayout(app); } } + + this.updaterService.init( + this.arduinoPreferences.get('arduino.ide.updateChannel') + ); + this.updater.checkForUpdates().then(async (updateInfo) => { + if (!updateInfo) return; + const versionToSkip = await this.localStorageService.getData<string>( + SKIP_IDE_VERSION + ); + if (versionToSkip === updateInfo.version) return; + this.updaterDialog.open(updateInfo); + }); + const start = async ({ selectedBoard }: BoardsConfig.Config) => { if (selectedBoard) { const { name, fqbn } = selectedBoard; diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 700012bbd..5ceae9179 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -262,6 +262,19 @@ import { UserFieldsDialogWidget, } from './dialogs/user-fields/user-fields-dialog'; import { nls } from '@theia/core/lib/common'; +import { IDEUpdaterCommands } from './ide-updater/ide-updater-commands'; +import { + IDEUpdater, + IDEUpdaterClient, + IDEUpdaterPath, +} from '../common/protocol/ide-updater'; +import { IDEUpdaterClientImpl } from './ide-updater/ide-updater-client-impl'; +import { + IDEUpdaterDialog, + IDEUpdaterDialogProps, + IDEUpdaterDialogWidget, +} from './dialogs/ide-updater/ide-updater-dialog'; +import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider'; const ElementQueries = require('css-element-queries/src/ElementQueries'); @@ -407,8 +420,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(SerialService) .toDynamicValue((context) => { const connection = context.container.get(WebSocketConnectionProvider); - const client = - context.container.get<SerialServiceClient>(SerialServiceClient); + const client = context.container.get<SerialServiceClient>( + SerialServiceClient + ); return connection.createProxy(SerialServicePath, client); }) .inSingletonScope(); @@ -472,12 +486,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { .inSingletonScope(); rebind(TheiaEditorWidgetFactory).to(EditorWidgetFactory).inSingletonScope(); rebind(TabBarToolbarFactory).toFactory( - ({ container: parentContainer }) => - () => { - const container = parentContainer.createChild(); - container.bind(TabBarToolbar).toSelf().inSingletonScope(); - return container.get(TabBarToolbar); - } + ({ container: parentContainer }) => () => { + const container = parentContainer.createChild(); + container.bind(TabBarToolbar).toSelf().inSingletonScope(); + return container.get(TabBarToolbar); + } ); bind(OutputWidget).toSelf().inSingletonScope(); rebind(TheiaOutputWidget).toService(OutputWidget); @@ -642,13 +655,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { // Enable the dirty indicator on uncloseable widgets. rebind(TabBarRendererFactory).toFactory((context) => () => { - const contextMenuRenderer = - context.container.get<ContextMenuRenderer>(ContextMenuRenderer); + const contextMenuRenderer = context.container.get<ContextMenuRenderer>( + ContextMenuRenderer + ); const decoratorService = context.container.get<TabBarDecoratorService>( TabBarDecoratorService ); - const iconThemeService = - context.container.get<IconThemeService>(IconThemeService); + const iconThemeService = context.container.get<IconThemeService>( + IconThemeService + ); return new TabBarRenderer( contextMenuRenderer, decoratorService, @@ -756,9 +771,32 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { title: 'UploadCertificate', }); + bind(IDEUpdaterDialogWidget).toSelf().inSingletonScope(); + bind(IDEUpdaterDialog).toSelf().inSingletonScope(); + bind(IDEUpdaterDialogProps).toConstantValue({ + title: 'IDEUpdater', + }); + bind(UserFieldsDialogWidget).toSelf().inSingletonScope(); bind(UserFieldsDialog).toSelf().inSingletonScope(); bind(UserFieldsDialogProps).toConstantValue({ title: 'UserFields', }); + + bind(IDEUpdaterCommands).toSelf().inSingletonScope(); + bind(CommandContribution).toService(IDEUpdaterCommands); + + // Frontend binding for the IDE Updater service + bind(IDEUpdaterClientImpl).toSelf().inSingletonScope(); + bind(IDEUpdaterClient).toService(IDEUpdaterClientImpl); + bind(IDEUpdater) + .toDynamicValue((context) => { + const client = context.container.get(IDEUpdaterClientImpl); + return ElectronIpcConnectionProvider.createProxy( + context.container, + IDEUpdaterPath, + client + ); + }) + .inSingletonScope(); }); diff --git a/arduino-ide-extension/src/browser/arduino-preferences.ts b/arduino-ide-extension/src/browser/arduino-preferences.ts index 5e1013a1d..910d724bc 100644 --- a/arduino-ide-extension/src/browser/arduino-preferences.ts +++ b/arduino-ide-extension/src/browser/arduino-preferences.ts @@ -9,6 +9,11 @@ import { import { nls } from '@theia/core/lib/common'; import { CompilerWarningLiterals, CompilerWarnings } from '../common/protocol'; +export enum UpdateChannel { + Stable = 'stable', + Nightly = 'nightly', +} + export const ArduinoConfigSchema: PreferenceSchema = { type: 'object', properties: { @@ -64,13 +69,14 @@ export const ArduinoConfigSchema: PreferenceSchema = { ), default: 0, }, - 'arduino.ide.autoUpdate': { - type: 'boolean', + 'arduino.ide.updateChannel': { + type: 'string', + enum: Object.values(UpdateChannel) as UpdateChannel[], + default: UpdateChannel.Stable, description: nls.localize( - 'arduino/preferences/ide.autoUpdate', - 'True to enable automatic update checks. The IDE will check for updates automatically and periodically.' + 'arduino/preferences/ide.updateChannel', + "Release channel to get updated from. 'stable' is the stable release, 'nightly' is the latest development build." ), - default: true, }, 'arduino.board.certificates': { type: 'string', @@ -171,7 +177,7 @@ export interface ArduinoConfiguration { 'arduino.upload.verify': boolean; 'arduino.window.autoScale': boolean; 'arduino.window.zoomLevel': number; - 'arduino.ide.autoUpdate': boolean; + 'arduino.ide.updateChannel': UpdateChannel; 'arduino.board.certificates': string; 'arduino.sketchbook.showAllFiles': boolean; 'arduino.cloud.enabled': boolean; @@ -188,16 +194,10 @@ export interface ArduinoConfiguration { export const ArduinoPreferences = Symbol('ArduinoPreferences'); export type ArduinoPreferences = PreferenceProxy<ArduinoConfiguration>; -export function createArduinoPreferences( - preferences: PreferenceService -): ArduinoPreferences { - return createPreferenceProxy(preferences, ArduinoConfigSchema); -} - export function bindArduinoPreferences(bind: interfaces.Bind): void { bind(ArduinoPreferences).toDynamicValue((ctx) => { const preferences = ctx.container.get<PreferenceService>(PreferenceService); - return createArduinoPreferences(preferences); + return createPreferenceProxy(preferences, ArduinoConfigSchema); }); bind(PreferenceContribution).toConstantValue({ schema: ArduinoConfigSchema, diff --git a/arduino-ide-extension/src/browser/components/ProgressBar.tsx b/arduino-ide-extension/src/browser/components/ProgressBar.tsx new file mode 100644 index 000000000..f91c9f991 --- /dev/null +++ b/arduino-ide-extension/src/browser/components/ProgressBar.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; + +export type ProgressBarProps = { + percent?: number; + showPercentage?: boolean; +}; + +export default function ProgressBar({ + percent = 0, + showPercentage = false, +}: ProgressBarProps): React.ReactElement { + const roundedPercent = Math.round(percent); + return ( + <div className="progress-bar"> + <div className="progress-bar--outer"> + <div + className="progress-bar--inner" + style={{ width: `${roundedPercent}%` }} + /> + </div> + {showPercentage && ( + <div className="progress-bar--percentage"> + <div className="progress-bar--percentage-text">{roundedPercent}%</div> + </div> + )} + </div> + ); +} diff --git a/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-component.tsx b/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-component.tsx new file mode 100644 index 000000000..3c1c4911e --- /dev/null +++ b/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-component.tsx @@ -0,0 +1,159 @@ +import { nls } from '@theia/core/lib/common'; +import { shell } from 'electron'; +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import ReactMarkdown from 'react-markdown'; +import { ProgressInfo, UpdateInfo } from '../../../common/protocol/ide-updater'; +import ProgressBar from '../../components/ProgressBar'; + +export type IDEUpdaterComponentProps = { + updateInfo: UpdateInfo; + downloadFinished?: boolean; + downloadStarted?: boolean; + progress?: ProgressInfo; + error?: Error; + onDownload: () => void; + onClose: () => void; + onSkipVersion: () => void; + onCloseAndInstall: () => void; +}; + +export const IDEUpdaterComponent = ({ + updateInfo: { version, releaseNotes }, + downloadStarted = false, + downloadFinished = false, + progress, + error, + onDownload, + onClose, + onSkipVersion, + onCloseAndInstall, +}: IDEUpdaterComponentProps): React.ReactElement => { + const changelogDivRef = React.useRef() as React.MutableRefObject< + HTMLDivElement + >; + React.useEffect(() => { + if (!!releaseNotes) { + let changelog: string; + if (typeof releaseNotes === 'string') changelog = releaseNotes; + else + changelog = releaseNotes.reduce((acc, item) => { + return item.note ? (acc += `${item.note}\n\n`) : acc; + }, ''); + ReactDOM.render( + <ReactMarkdown + components={{ + a: ({ href, children, ...props }) => ( + <a onClick={() => href && shell.openExternal(href)} {...props}> + {children} + </a> + ), + }} + > + {changelog} + </ReactMarkdown>, + changelogDivRef.current + ); + } + }, [releaseNotes]); + const closeButton = ( + <button onClick={onClose} type="button" className="theia-button secondary"> + {nls.localize('arduino/ide-updater/notNowButton', 'Not now')} + </button> + ); + + return ( + <div className="ide-updater-dialog--content"> + {downloadFinished ? ( + <div className="ide-updater-dialog--downloaded"> + <div> + {nls.localize( + 'arduino/ide-updater/versionDownloaded', + 'Arduino IDE {0} has been downloaded.', + version + )} + </div> + <div> + {nls.localize( + 'arduino/ide-updater/closeToInstallNotice', + 'Close the software and install the update on your machine.' + )} + </div> + <div className="buttons-container"> + {closeButton} + <button + onClick={onCloseAndInstall} + type="button" + className="theia-button close-and-install" + > + {nls.localize( + 'arduino/ide-updater/closeAndInstallButton', + 'Close and Install' + )} + </button> + </div> + </div> + ) : downloadStarted ? ( + <div className="ide-updater-dialog--downloading"> + <div> + {nls.localize( + 'arduino/ide-updater/downloadingNotice', + 'Downloading the latest version of the Arduino IDE.' + )} + </div> + <ProgressBar percent={progress?.percent} showPercentage /> + </div> + ) : ( + <div className="ide-updater-dialog--pre-download"> + <div className="ide-updater-dialog--logo-container"> + <div className="ide-updater-dialog--logo"></div> + </div> + <div className="ide-updater-dialog--new-version-text dialogSection"> + <div className="dialogRow"> + <div className="bold"> + {nls.localize( + 'arduino/ide-updater/updateAvailable', + 'Update Available' + )} + </div> + </div> + <div className="dialogRow"> + {nls.localize( + 'arduino/ide-updater/newVersionAvailable', + 'A new version of Arduino IDE ({0}) is available for download.', + version + )} + </div> + {releaseNotes && ( + <div className="dialogRow"> + <div className="changelog-container" ref={changelogDivRef} /> + </div> + )} + <div className="buttons-container"> + <button + onClick={onSkipVersion} + type="button" + className="theia-button secondary skip-version" + > + {nls.localize( + 'arduino/ide-updater/skipVersionButton', + 'Skip Version' + )} + </button> + <div className="push"></div> + {closeButton} + <button + onClick={onDownload} + type="button" + className="theia-button primary" + > + {nls.localize('arduino/ide-updater/downloadButton', 'Download')} + </button> + </div> + </div> + </div> + )} + {!!error && <div className="error-container">{error}</div>} + </div> + ); +}; diff --git a/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-dialog.tsx b/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-dialog.tsx new file mode 100644 index 000000000..56b8c84f5 --- /dev/null +++ b/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-dialog.tsx @@ -0,0 +1,166 @@ +import * as React from 'react'; +import { inject, injectable } from 'inversify'; +import { DialogProps } from '@theia/core/lib/browser/dialogs'; +import { AbstractDialog } from '../../theia/dialogs/dialogs'; +import { Widget } from '@phosphor/widgets'; +import { Message } from '@phosphor/messaging'; +import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget'; +import { nls } from '@theia/core'; +import { IDEUpdaterComponent } from './ide-updater-component'; +import { IDEUpdaterCommands } from '../../ide-updater/ide-updater-commands'; +import { + IDEUpdaterClient, + ProgressInfo, + UpdateInfo, +} from '../../../common/protocol/ide-updater'; +import { LocalStorageService } from '@theia/core/lib/browser'; +import { SKIP_IDE_VERSION } from '../../arduino-frontend-contribution'; + +@injectable() +export class IDEUpdaterDialogWidget extends ReactWidget { + protected isOpen = new Object(); + updateInfo: UpdateInfo; + progressInfo: ProgressInfo | undefined; + error: Error | undefined; + downloadFinished: boolean; + downloadStarted: boolean; + onClose: () => void; + + @inject(IDEUpdaterCommands) + protected readonly updater: IDEUpdaterCommands; + + @inject(IDEUpdaterClient) + protected readonly updaterClient: IDEUpdaterClient; + + @inject(LocalStorageService) + protected readonly localStorageService: LocalStorageService; + + init(updateInfo: UpdateInfo, onClose: () => void): void { + this.updateInfo = updateInfo; + this.progressInfo = undefined; + this.error = undefined; + this.downloadStarted = false; + this.downloadFinished = false; + this.onClose = onClose; + + this.updaterClient.onError((e) => { + this.error = e; + this.update(); + }); + this.updaterClient.onDownloadProgressChanged((e) => { + this.progressInfo = e; + this.update(); + }); + this.updaterClient.onDownloadFinished((e) => { + this.downloadFinished = true; + this.update(); + }); + } + + async onSkipVersion(): Promise<void> { + this.localStorageService.setData<string>( + SKIP_IDE_VERSION, + this.updateInfo.version + ); + this.close(); + } + + close(): void { + super.close(); + this.onClose(); + } + + onDispose(): void { + if (this.downloadStarted && !this.downloadFinished) + this.updater.stopDownload(); + } + + async onDownload(): Promise<void> { + this.progressInfo = undefined; + this.downloadStarted = true; + this.error = undefined; + this.updater.downloadUpdate(); + this.update(); + } + + onCloseAndInstall(): void { + this.updater.quitAndInstall(); + } + + protected render(): React.ReactNode { + return !!this.updateInfo ? ( + <form> + <IDEUpdaterComponent + updateInfo={this.updateInfo} + downloadStarted={this.downloadStarted} + downloadFinished={this.downloadFinished} + progress={this.progressInfo} + onClose={this.close.bind(this)} + onSkipVersion={this.onSkipVersion.bind(this)} + onDownload={this.onDownload.bind(this)} + onCloseAndInstall={this.onCloseAndInstall.bind(this)} + /> + </form> + ) : null; + } +} + +@injectable() +export class IDEUpdaterDialogProps extends DialogProps {} + +@injectable() +export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> { + @inject(IDEUpdaterDialogWidget) + protected readonly widget: IDEUpdaterDialogWidget; + + constructor( + @inject(IDEUpdaterDialogProps) + protected readonly props: IDEUpdaterDialogProps + ) { + super({ + title: nls.localize( + 'arduino/updater/ideUpdaterDialog', + 'Software Update' + ), + }); + this.contentNode.classList.add('ide-updater-dialog'); + this.acceptButton = undefined; + } + + get value(): UpdateInfo { + return this.widget.updateInfo; + } + + protected onAfterAttach(msg: Message): void { + if (this.widget.isAttached) { + Widget.detach(this.widget); + } + Widget.attach(this.widget, this.contentNode); + super.onAfterAttach(msg); + this.update(); + } + + async open( + data: UpdateInfo | undefined = undefined + ): Promise<UpdateInfo | undefined> { + if (data && data.version) { + this.widget.init(data, this.close.bind(this)); + return super.open(); + } + } + + protected onUpdateRequest(msg: Message): void { + super.onUpdateRequest(msg); + this.widget.update(); + } + + protected onActivateRequest(msg: Message): void { + super.onActivateRequest(msg); + this.widget.activate(); + } + + close(): void { + this.widget.dispose(); + super.close(); + } +} diff --git a/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx b/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx index 62c166c5e..e7fa7a060 100644 --- a/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx +++ b/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx @@ -260,18 +260,6 @@ export class SettingsComponent extends React.Component< 'Verify code after upload' )} </label> - <label className="flex-line"> - <input - type="checkbox" - checked={this.state.checkForUpdates} - onChange={this.checkForUpdatesDidChange} - disabled={true} - /> - {nls.localize( - 'arduino/preferences/checkForUpdates', - 'Check for updates on startup' - )} - </label> <label className="flex-line"> <input type="checkbox" @@ -444,7 +432,9 @@ export class SettingsComponent extends React.Component< ); } - protected noopKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { + protected noopKeyDown = ( + event: React.KeyboardEvent<HTMLInputElement> + ): void => { if (this.isControlKey(event)) { return; } @@ -454,7 +444,7 @@ export class SettingsComponent extends React.Component< protected numbersOnlyKeyDown = ( event: React.KeyboardEvent<HTMLInputElement> - ) => { + ): void => { if (this.isControlKey(event)) { return; } @@ -466,7 +456,7 @@ export class SettingsComponent extends React.Component< } }; - protected browseSketchbookDidClick = async () => { + protected browseSketchbookDidClick = async (): Promise<void> => { const uri = await this.props.fileDialogService.showOpenDialog({ title: nls.localize( 'arduino/preferences/newSketchbookLocation', @@ -483,7 +473,7 @@ export class SettingsComponent extends React.Component< } }; - protected editAdditionalUrlDidClick = async () => { + protected editAdditionalUrlDidClick = async (): Promise<void> => { const additionalUrls = await new AdditionalUrlsDialog( this.state.additionalUrls, this.props.windowService @@ -495,7 +485,7 @@ export class SettingsComponent extends React.Component< protected editorFontSizeDidChange = ( event: React.ChangeEvent<HTMLInputElement> - ) => { + ): void => { const { value } = event.target; if (value) { this.setState({ editorFontSize: parseInt(value, 10) }); @@ -504,7 +494,7 @@ export class SettingsComponent extends React.Component< protected additionalUrlsDidChange = ( event: React.ChangeEvent<HTMLInputElement> - ) => { + ): void => { this.setState({ additionalUrls: event.target.value.split(',').map((url) => url.trim()), }); @@ -512,13 +502,13 @@ export class SettingsComponent extends React.Component< protected autoScaleInterfaceDidChange = ( event: React.ChangeEvent<HTMLInputElement> - ) => { + ): void => { this.setState({ autoScaleInterface: event.target.checked }); }; protected interfaceScaleDidChange = ( event: React.ChangeEvent<HTMLInputElement> - ) => { + ): void => { const { value } = event.target; const percentage = parseInt(value, 10); if (isNaN(percentage)) { @@ -532,31 +522,25 @@ export class SettingsComponent extends React.Component< protected verifyAfterUploadDidChange = ( event: React.ChangeEvent<HTMLInputElement> - ) => { + ): void => { this.setState({ verifyAfterUpload: event.target.checked }); }; - protected checkForUpdatesDidChange = ( - event: React.ChangeEvent<HTMLInputElement> - ) => { - this.setState({ checkForUpdates: event.target.checked }); - }; - protected sketchbookShowAllFilesDidChange = ( event: React.ChangeEvent<HTMLInputElement> - ) => { + ): void => { this.setState({ sketchbookShowAllFiles: event.target.checked }); }; protected autoSaveDidChange = ( event: React.ChangeEvent<HTMLInputElement> - ) => { + ): void => { this.setState({ autoSave: event.target.checked ? 'on' : 'off' }); }; protected quickSuggestionsOtherDidChange = ( event: React.ChangeEvent<HTMLInputElement> - ) => { + ): void => { // need to persist react events through lifecycle https://reactjs.org/docs/events.html#event-pooling const newVal = event.target.checked ? true : false; @@ -570,7 +554,9 @@ export class SettingsComponent extends React.Component< }); }; - protected themeDidChange = (event: React.ChangeEvent<HTMLSelectElement>) => { + protected themeDidChange = ( + event: React.ChangeEvent<HTMLSelectElement> + ): void => { const { selectedIndex } = event.target.options; const theme = ThemeService.get().getThemes()[selectedIndex]; if (theme) { @@ -580,14 +566,14 @@ export class SettingsComponent extends React.Component< protected languageDidChange = ( event: React.ChangeEvent<HTMLSelectElement> - ) => { + ): void => { const selectedLanguage = event.target.value; this.setState({ currentLanguage: selectedLanguage }); }; protected compilerWarningsDidChange = ( event: React.ChangeEvent<HTMLSelectElement> - ) => { + ): void => { const { selectedIndex } = event.target.options; const compilerWarnings = CompilerWarningLiterals[selectedIndex]; if (compilerWarnings) { @@ -597,26 +583,28 @@ export class SettingsComponent extends React.Component< protected verboseOnCompileDidChange = ( event: React.ChangeEvent<HTMLInputElement> - ) => { + ): void => { this.setState({ verboseOnCompile: event.target.checked }); }; protected verboseOnUploadDidChange = ( event: React.ChangeEvent<HTMLInputElement> - ) => { + ): void => { this.setState({ verboseOnUpload: event.target.checked }); }; protected sketchpathDidChange = ( event: React.ChangeEvent<HTMLInputElement> - ) => { + ): void => { const sketchbookPath = event.target.value; if (sketchbookPath) { this.setState({ sketchbookPath }); } }; - protected noProxyDidChange = (event: React.ChangeEvent<HTMLInputElement>) => { + protected noProxyDidChange = ( + event: React.ChangeEvent<HTMLInputElement> + ): void => { if (event.target.checked) { this.setState({ network: 'none' }); } else { @@ -626,7 +614,7 @@ export class SettingsComponent extends React.Component< protected manualProxyDidChange = ( event: React.ChangeEvent<HTMLInputElement> - ) => { + ): void => { if (event.target.checked) { this.setState({ network: Network.Default() }); } else { @@ -636,7 +624,7 @@ export class SettingsComponent extends React.Component< protected httpProtocolDidChange = ( event: React.ChangeEvent<HTMLInputElement> - ) => { + ): void => { if (this.state.network !== 'none') { const network = this.cloneProxySettings; network.protocol = event.target.checked ? 'http' : 'socks'; @@ -646,7 +634,7 @@ export class SettingsComponent extends React.Component< protected socksProtocolDidChange = ( event: React.ChangeEvent<HTMLInputElement> - ) => { + ): void => { if (this.state.network !== 'none') { const network = this.cloneProxySettings; network.protocol = event.target.checked ? 'socks' : 'http'; @@ -656,7 +644,7 @@ export class SettingsComponent extends React.Component< protected hostnameDidChange = ( event: React.ChangeEvent<HTMLInputElement> - ) => { + ): void => { if (this.state.network !== 'none') { const network = this.cloneProxySettings; network.hostname = event.target.value; @@ -664,7 +652,9 @@ export class SettingsComponent extends React.Component< } }; - protected portDidChange = (event: React.ChangeEvent<HTMLInputElement>) => { + protected portDidChange = ( + event: React.ChangeEvent<HTMLInputElement> + ): void => { if (this.state.network !== 'none') { const network = this.cloneProxySettings; network.port = event.target.value; @@ -674,7 +664,7 @@ export class SettingsComponent extends React.Component< protected usernameDidChange = ( event: React.ChangeEvent<HTMLInputElement> - ) => { + ): void => { if (this.state.network !== 'none') { const network = this.cloneProxySettings; network.username = event.target.value; @@ -684,7 +674,7 @@ export class SettingsComponent extends React.Component< protected passwordDidChange = ( event: React.ChangeEvent<HTMLInputElement> - ) => { + ): void => { if (this.state.network !== 'none') { const network = this.cloneProxySettings; network.password = event.target.value; diff --git a/arduino-ide-extension/src/browser/dialogs/settings/settings.tsx b/arduino-ide-extension/src/browser/dialogs/settings/settings.tsx index d4a4e0642..8490f1e04 100644 --- a/arduino-ide-extension/src/browser/dialogs/settings/settings.tsx +++ b/arduino-ide-extension/src/browser/dialogs/settings/settings.tsx @@ -18,24 +18,22 @@ import { import { nls } from '@theia/core/lib/common'; import { AsyncLocalizationProvider } from '@theia/core/lib/common/i18n/localization'; -const EDITOR_SETTING = 'editor'; -const FONT_SIZE_SETTING = `${EDITOR_SETTING}.fontSize`; -const AUTO_SAVE_SETTING = `${EDITOR_SETTING}.autoSave`; -const QUICK_SUGGESTIONS_SETTING = `${EDITOR_SETTING}.quickSuggestions`; -const ARDUINO_SETTING = 'arduino'; -const WINDOW_SETTING = `${ARDUINO_SETTING}.window`; -// const IDE_SETTING = `${ARDUINO_SETTING}.ide`; -const COMPILE_SETTING = `${ARDUINO_SETTING}.compile`; -const UPLOAD_SETTING = `${ARDUINO_SETTING}.upload`; -const SKETCHBOOK_SETTING = `${ARDUINO_SETTING}.sketchbook`; -const AUTO_SCALE_SETTING = `${WINDOW_SETTING}.autoScale`; -const ZOOM_LEVEL_SETTING = `${WINDOW_SETTING}.zoomLevel`; -// const AUTO_UPDATE_SETTING = `${IDE_SETTING}.autoUpdate`; -const COMPILE_VERBOSE_SETTING = `${COMPILE_SETTING}.verbose`; -const COMPILE_WARNINGS_SETTING = `${COMPILE_SETTING}.warnings`; -const UPLOAD_VERBOSE_SETTING = `${UPLOAD_SETTING}.verbose`; -const UPLOAD_VERIFY_SETTING = `${UPLOAD_SETTING}.verify`; -const SHOW_ALL_FILES_SETTING = `${SKETCHBOOK_SETTING}.showAllFiles`; +export const EDITOR_SETTING = 'editor'; +export const FONT_SIZE_SETTING = `${EDITOR_SETTING}.fontSize`; +export const AUTO_SAVE_SETTING = `${EDITOR_SETTING}.autoSave`; +export const QUICK_SUGGESTIONS_SETTING = `${EDITOR_SETTING}.quickSuggestions`; +export const ARDUINO_SETTING = 'arduino'; +export const WINDOW_SETTING = `${ARDUINO_SETTING}.window`; +export const COMPILE_SETTING = `${ARDUINO_SETTING}.compile`; +export const UPLOAD_SETTING = `${ARDUINO_SETTING}.upload`; +export const SKETCHBOOK_SETTING = `${ARDUINO_SETTING}.sketchbook`; +export const AUTO_SCALE_SETTING = `${WINDOW_SETTING}.autoScale`; +export const ZOOM_LEVEL_SETTING = `${WINDOW_SETTING}.zoomLevel`; +export const COMPILE_VERBOSE_SETTING = `${COMPILE_SETTING}.verbose`; +export const COMPILE_WARNINGS_SETTING = `${COMPILE_SETTING}.warnings`; +export const UPLOAD_VERBOSE_SETTING = `${UPLOAD_SETTING}.verbose`; +export const UPLOAD_VERIFY_SETTING = `${UPLOAD_SETTING}.verify`; +export const SHOW_ALL_FILES_SETTING = `${SKETCHBOOK_SETTING}.showAllFiles`; export interface Settings extends Index { editorFontSize: number; // `editor.fontSize` @@ -48,7 +46,6 @@ export interface Settings extends Index { autoScaleInterface: boolean; // `arduino.window.autoScale` interfaceScale: number; // `arduino.window.zoomLevel` https://github.com/eclipse-theia/theia/issues/8751 - checkForUpdates?: boolean; // `arduino.ide.autoUpdate` verboseOnCompile: boolean; // `arduino.compile.verbose` compilerWarnings: CompilerWarnings; // `arduino.compile.warnings` verboseOnUpload: boolean; // `arduino.upload.verbose` @@ -93,7 +90,6 @@ export class SettingsService { @postConstruct() protected async init(): Promise<void> { - await this.appStateService.reachedState('ready'); // Hack for https://github.com/eclipse-theia/theia/issues/8993 const settings = await this.loadSettings(); this._settings = deepClone(settings); this.ready.resolve(); @@ -110,7 +106,6 @@ export class SettingsService { quickSuggestions, autoScaleInterface, interfaceScale, - // checkForUpdates, verboseOnCompile, compilerWarnings, verboseOnUpload, @@ -135,7 +130,6 @@ export class SettingsService { }), this.preferenceService.get<boolean>(AUTO_SCALE_SETTING, true), this.preferenceService.get<number>(ZOOM_LEVEL_SETTING, 0), - // this.preferenceService.get<string>(AUTO_UPDATE_SETTING, true), this.preferenceService.get<boolean>(COMPILE_VERBOSE_SETTING, true), this.preferenceService.get<any>(COMPILE_WARNINGS_SETTING, 'None'), this.preferenceService.get<boolean>(UPLOAD_VERBOSE_SETTING, true), @@ -154,7 +148,6 @@ export class SettingsService { quickSuggestions, autoScaleInterface, interfaceScale, - // checkForUpdates, verboseOnCompile, compilerWarnings, verboseOnUpload, @@ -234,7 +227,6 @@ export class SettingsService { quickSuggestions, autoScaleInterface, interfaceScale, - // checkForUpdates, verboseOnCompile, compilerWarnings, verboseOnUpload, @@ -284,7 +276,6 @@ export class SettingsService { interfaceScale, PreferenceScope.User ), - // this.preferenceService.set(AUTO_UPDATE_SETTING, checkForUpdates, PreferenceScope.User), this.preferenceService.set( COMPILE_VERBOSE_SETTING, verboseOnCompile, diff --git a/arduino-ide-extension/src/browser/ide-updater/ide-updater-client-impl.ts b/arduino-ide-extension/src/browser/ide-updater/ide-updater-client-impl.ts new file mode 100644 index 000000000..8f4df3d6e --- /dev/null +++ b/arduino-ide-extension/src/browser/ide-updater/ide-updater-client-impl.ts @@ -0,0 +1,40 @@ +import { Emitter } from '@theia/core'; +import { injectable } from '@theia/core/shared/inversify'; +import { UpdateInfo, ProgressInfo } from 'electron-updater'; +import { IDEUpdaterClient } from '../../common/protocol/ide-updater'; + +@injectable() +export class IDEUpdaterClientImpl implements IDEUpdaterClient { + protected readonly onErrorEmitter = new Emitter<Error>(); + protected readonly onCheckingForUpdateEmitter = new Emitter<void>(); + protected readonly onUpdateAvailableEmitter = new Emitter<UpdateInfo>(); + protected readonly onUpdateNotAvailableEmitter = new Emitter<UpdateInfo>(); + protected readonly onDownloadProgressEmitter = new Emitter<ProgressInfo>(); + protected readonly onDownloadFinishedEmitter = new Emitter<UpdateInfo>(); + + readonly onError = this.onErrorEmitter.event; + readonly onCheckingForUpdate = this.onCheckingForUpdateEmitter.event; + readonly onUpdateAvailable = this.onUpdateAvailableEmitter.event; + readonly onUpdateNotAvailable = this.onUpdateNotAvailableEmitter.event; + readonly onDownloadProgressChanged = this.onDownloadProgressEmitter.event; + readonly onDownloadFinished = this.onDownloadFinishedEmitter.event; + + notifyError(message: Error): void { + this.onErrorEmitter.fire(message); + } + notifyCheckingForUpdate(message: void): void { + this.onCheckingForUpdateEmitter.fire(message); + } + notifyUpdateAvailable(message: UpdateInfo): void { + this.onUpdateAvailableEmitter.fire(message); + } + notifyUpdateNotAvailable(message: UpdateInfo): void { + this.onUpdateNotAvailableEmitter.fire(message); + } + notifyDownloadProgressChanged(message: ProgressInfo): void { + this.onDownloadProgressEmitter.fire(message); + } + notifyDownloadFinished(message: UpdateInfo): void { + this.onDownloadFinishedEmitter.fire(message); + } +} diff --git a/arduino-ide-extension/src/browser/ide-updater/ide-updater-commands.ts b/arduino-ide-extension/src/browser/ide-updater/ide-updater-commands.ts new file mode 100644 index 000000000..3450e9b1b --- /dev/null +++ b/arduino-ide-extension/src/browser/ide-updater/ide-updater-commands.ts @@ -0,0 +1,71 @@ +import { + Command, + CommandContribution, + CommandRegistry, + MessageService, +} from '@theia/core'; +import { injectable, inject } from 'inversify'; +import { IDEUpdater, UpdateInfo } from '../../common/protocol/ide-updater'; + +@injectable() +export class IDEUpdaterCommands implements CommandContribution { + constructor( + @inject(IDEUpdater) + private readonly updater: IDEUpdater, + @inject(MessageService) + protected readonly messageService: MessageService + ) {} + + registerCommands(registry: CommandRegistry): void { + registry.registerCommand(IDEUpdaterCommands.CHECK_FOR_UPDATES, { + execute: this.checkForUpdates.bind(this), + }); + registry.registerCommand(IDEUpdaterCommands.DOWNLOAD_UPDATE, { + execute: this.downloadUpdate.bind(this), + }); + registry.registerCommand(IDEUpdaterCommands.STOP_DOWNLOAD, { + execute: this.stopDownload.bind(this), + }); + registry.registerCommand(IDEUpdaterCommands.INSTALL_UPDATE, { + execute: this.quitAndInstall.bind(this), + }); + } + + async checkForUpdates(): Promise<UpdateInfo | void> { + return await this.updater.checkForUpdates(); + } + + async downloadUpdate(): Promise<void> { + await this.updater.downloadUpdate(); + } + + async stopDownload(): Promise<void> { + await this.updater.stopDownload(); + } + + quitAndInstall(): void { + this.updater.quitAndInstall(); + } +} +export namespace IDEUpdaterCommands { + export const CHECK_FOR_UPDATES: Command = { + id: 'arduino-ide-check-for-updates', + category: 'Arduino', + label: 'Check for Arduino IDE updates', + }; + export const DOWNLOAD_UPDATE: Command = { + id: 'arduino-ide-download-update', + category: 'Arduino', + label: 'Download Arduino IDE updates', + }; + export const STOP_DOWNLOAD: Command = { + id: 'arduino-ide-stop-download', + category: 'Arduino', + label: 'Stop download of Arduino IDE updates', + }; + export const INSTALL_UPDATE: Command = { + id: 'arduino-ide-install-update', + category: 'Arduino', + label: 'Install Arduino IDE updates', + }; +} diff --git a/arduino-ide-extension/src/browser/style/ide-logo.png b/arduino-ide-extension/src/browser/style/ide-logo.png new file mode 100644 index 000000000..eba4b6f39 Binary files /dev/null and b/arduino-ide-extension/src/browser/style/ide-logo.png differ diff --git a/arduino-ide-extension/src/browser/style/ide-updater-dialog.css b/arduino-ide-extension/src/browser/style/ide-updater-dialog.css new file mode 100644 index 000000000..8215b0808 --- /dev/null +++ b/arduino-ide-extension/src/browser/style/ide-updater-dialog.css @@ -0,0 +1,67 @@ +.ide-updater-dialog { + width: 546px; +} + +.ide-updater-dialog .bold { + font-weight: bold; +} + +.ide-updater-dialog--pre-download { + display: flex; +} + +.ide-updater-dialog--logo-container { + margin-right: 28px; +} + +.ide-updater-dialog--logo { + background: url('./ide-logo.png') round; + width: 52px; + height: 52px; +} + +.dialogContent.ide-updater-dialog + .ide-updater-dialog--content + .ide-updater-dialog--new-version-text.dialogSection { + margin-top: 0; +} + +.ide-updater-dialog .changelog-container { + background: white; + border: 1px solid #dae3e3; + border-radius: 2px; + font-size: 12px; + height: 180px; + overflow: auto; + padding: 0 12px; + cursor: text; +} + +.ide-updater-dialog .changelog-container a { + color: #018184; +} + +.ide-updater-dialog .changelog-container a:hover { + text-decoration: underline; + cursor: pointer; +} + +.ide-updater-dialog .changelog-container code { + background: #ecf1f1; + border-radius: 2px; + padding: 0 2px; +} + +.ide-updater-dialog .changelog-container a code { + color: #018184; +} + +.ide-updater-dialog .buttons-container { + display: flex; + justify-content: flex-end; + margin-top: 28px; +} + +.ide-updater-dialog .buttons-container .push { + margin-right: auto; +} diff --git a/arduino-ide-extension/src/browser/style/index.css b/arduino-ide-extension/src/browser/style/index.css index 9e52aeb07..bf2907139 100644 --- a/arduino-ide-extension/src/browser/style/index.css +++ b/arduino-ide-extension/src/browser/style/index.css @@ -9,6 +9,7 @@ @import './editor.css'; @import './settings-dialog.css'; @import './firmware-uploader-dialog.css'; +@import './ide-updater-dialog.css'; @import './certificate-uploader-dialog.css'; @import './user-fields-dialog.css'; @import './debug.css'; @@ -16,6 +17,7 @@ @import './cloud-sketchbook.css'; @import './fonts.css'; @import './custom-codicon.css'; +@import './progress-bar.css'; .theia-input.warning:focus { outline-width: 1px; diff --git a/arduino-ide-extension/src/browser/style/progress-bar.css b/arduino-ide-extension/src/browser/style/progress-bar.css new file mode 100644 index 000000000..2fdbd4b7f --- /dev/null +++ b/arduino-ide-extension/src/browser/style/progress-bar.css @@ -0,0 +1,32 @@ +.progress-bar { + margin-top: 20px; +} + +.progress-bar--outer { + background: #e5e5e5; + border-radius: 11px; + height: 6px; + position: relative; + overflow: hidden; +} + +.progress-bar--inner { + transition: width 1s; + height: 100%; + background: #008184; + border-radius: 11px; +} + +.progress-bar--percentage { + align-items: flex-end; + display: flex; + height: 40px; + justify-content: center; + margin-top: 10px; + width: 100%; +} + +.progress-bar--percentage-text { + font-size: 14px; + line-height: 24px; +} diff --git a/arduino-ide-extension/src/common/protocol/ide-updater.ts b/arduino-ide-extension/src/common/protocol/ide-updater.ts new file mode 100644 index 000000000..2cc874471 --- /dev/null +++ b/arduino-ide-extension/src/common/protocol/ide-updater.ts @@ -0,0 +1,71 @@ +import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; +import { Event } from '@theia/core/lib/common/event'; +import { UpdateChannel } from '../../browser/arduino-preferences'; + +export interface ProgressInfo { + total: number; + delta: number; + transferred: number; + percent: number; + bytesPerSecond: number; +} + +export interface ReleaseNoteInfo { + readonly version: string; + readonly note: string | null; +} + +export interface BlockMapDataHolder { + size?: number; + blockMapSize?: number; + readonly sha512: string; + readonly isAdminRightsRequired?: boolean; +} + +export interface UpdateFileInfo extends BlockMapDataHolder { + url: string; +} + +export type UpdateInfo = { + readonly version: string; + readonly files: Array<UpdateFileInfo>; + releaseName?: string | null; + releaseNotes?: string | Array<ReleaseNoteInfo> | null; + releaseDate: string; + readonly stagingPercentage?: number; +}; + +export interface ProgressInfo { + total: number; + delta: number; + transferred: number; + percent: number; + bytesPerSecond: number; +} + +export const IDEUpdaterPath = '/services/ide-updater'; +export const IDEUpdater = Symbol('IDEUpdater'); +export interface IDEUpdater extends JsonRpcServer<IDEUpdaterClient> { + init(channel: UpdateChannel): void; + checkForUpdates(): Promise<UpdateInfo | void>; + downloadUpdate(): Promise<void>; + quitAndInstall(): void; + stopDownload(): void; + disconnectClient(client: IDEUpdaterClient): void; +} + +export const IDEUpdaterClient = Symbol('IDEUpdaterClient'); +export interface IDEUpdaterClient { + onError: Event<Error>; + onCheckingForUpdate: Event<void>; + onUpdateAvailable: Event<UpdateInfo>; + onUpdateNotAvailable: Event<UpdateInfo>; + onDownloadProgressChanged: Event<ProgressInfo>; + onDownloadFinished: Event<UpdateInfo>; + notifyError(message: Error): void; + notifyCheckingForUpdate(message: void): void; + notifyUpdateAvailable(message: UpdateInfo): void; + notifyUpdateNotAvailable(message: UpdateInfo): void; + notifyDownloadProgressChanged(message: ProgressInfo): void; + notifyDownloadFinished(message: UpdateInfo): void; +} diff --git a/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts b/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts index c8ebb13ca..537030925 100644 --- a/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts +++ b/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts @@ -2,7 +2,10 @@ import { ContainerModule } from 'inversify'; import { JsonRpcConnectionHandler } from '@theia/core/lib/common/messaging/proxy-factory'; import { ElectronConnectionHandler } from '@theia/core/lib/electron-common/messaging/electron-connection-handler'; import { ElectronMainWindowService } from '@theia/core/lib/electron-common/electron-main-window-service'; -import { ElectronMainApplication as TheiaElectronMainApplication } from '@theia/core/lib/electron-main/electron-main-application'; +import { + ElectronMainApplication as TheiaElectronMainApplication, + ElectronMainApplicationContribution, +} from '@theia/core/lib/electron-main/electron-main-application'; import { SplashService, splashServicePath, @@ -10,6 +13,12 @@ import { import { SplashServiceImpl } from './splash/splash-service-impl'; import { ElectronMainApplication } from './theia/electron-main-application'; import { ElectronMainWindowServiceImpl } from './theia/electron-main-window-service'; +import { + IDEUpdater, + IDEUpdaterClient, + IDEUpdaterPath, +} from '../common/protocol/ide-updater'; +import { IDEUpdaterImpl } from '../node/ide-updater/ide-updater-impl'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(ElectronMainApplication).toSelf().inSingletonScope(); @@ -28,4 +37,23 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { ) ) .inSingletonScope(); + + // IDE updater bindings + bind(IDEUpdaterImpl).toSelf().inSingletonScope(); + bind(IDEUpdater).toService(IDEUpdaterImpl); + bind(ElectronMainApplicationContribution).toService(IDEUpdater); + bind(ElectronConnectionHandler) + .toDynamicValue( + (context) => + new JsonRpcConnectionHandler<IDEUpdaterClient>( + IDEUpdaterPath, + (client) => { + const server = context.container.get<IDEUpdater>(IDEUpdater); + server.setClient(client); + client.onDidCloseConnection(() => server.disconnectClient(client)); + return server; + } + ) + ) + .inSingletonScope(); }); diff --git a/arduino-ide-extension/src/node/grpc-client-provider.ts b/arduino-ide-extension/src/node/grpc-client-provider.ts index 8c1ff0dc3..759c82278 100644 --- a/arduino-ide-extension/src/node/grpc-client-provider.ts +++ b/arduino-ide-extension/src/node/grpc-client-provider.ts @@ -70,11 +70,11 @@ export abstract class GrpcClientProvider<C> { protected abstract close(client: C): void; protected get channelOptions(): Record<string, unknown> { - const pjson = require('../../package.json') || { "version": "0.0.0" } + const pjson = require('../../package.json') || { version: '0.0.0' }; return { 'grpc.max_send_message_length': 512 * 1024 * 1024, 'grpc.max_receive_message_length': 512 * 1024 * 1024, - 'grpc.primary_user_agent': `arduino-ide/${pjson.version}` + 'grpc.primary_user_agent': `arduino-ide/${pjson.version}`, }; } } diff --git a/arduino-ide-extension/src/node/ide-updater/ide-updater-impl.ts b/arduino-ide-extension/src/node/ide-updater/ide-updater-impl.ts new file mode 100644 index 000000000..1b871bba9 --- /dev/null +++ b/arduino-ide-extension/src/node/ide-updater/ide-updater-impl.ts @@ -0,0 +1,128 @@ +import { injectable } from '@theia/core/shared/inversify'; +import { UpdateInfo, CancellationToken, autoUpdater } from 'electron-updater'; +import fetch, { Response } from 'node-fetch'; +import { UpdateChannel } from '../../browser/arduino-preferences'; +import { + IDEUpdater, + IDEUpdaterClient, +} from '../../common/protocol/ide-updater'; + +const CHANGELOG_BASE_URL = 'https://downloads.arduino.cc/arduino-ide/changelog'; +const IDE_DOWNLOAD_BASE_URL = 'https://downloads.arduino.cc/arduino-ide'; + +@injectable() +export class IDEUpdaterImpl implements IDEUpdater { + private updater = autoUpdater; + private cancellationToken?: CancellationToken; + protected theiaFEClient?: IDEUpdaterClient; + protected clients: Array<IDEUpdaterClient> = []; + + init(channel: UpdateChannel) { + this.updater.channel = channel; + this.updater.setFeedURL({ + provider: 'generic', + url: `${IDE_DOWNLOAD_BASE_URL}/${ + channel === UpdateChannel.Nightly ? 'nightly' : '' + }`, + channel, + }); + + this.updater.on('checking-for-update', (e) => + this.clients.forEach((c) => c.notifyCheckingForUpdate(e)) + ); + this.updater.on('update-available', (e) => + this.clients.forEach((c) => c.notifyUpdateAvailable(e)) + ); + this.updater.on('update-not-available', (e) => + this.clients.forEach((c) => c.notifyUpdateNotAvailable(e)) + ); + this.updater.on('download-progress', (e) => + this.clients.forEach((c) => c.notifyDownloadProgressChanged(e)) + ); + this.updater.on('update-downloaded', (e) => + this.clients.forEach((c) => c.notifyDownloadFinished(e)) + ); + this.updater.on('error', (e) => + this.clients.forEach((c) => c.notifyError(e)) + ); + } + + setClient(client: IDEUpdaterClient | undefined): void { + if (client) this.clients.push(client); + } + + async checkForUpdates(): Promise<UpdateInfo | void> { + const { + updateInfo, + cancellationToken, + } = await this.updater.checkForUpdates(); + + this.cancellationToken = cancellationToken; + if ( + this.updater.currentVersion.compare(updateInfo.version) === -1 || + true + ) { + /* + 'latest.txt' points to the latest changelog that has been generated by the CI, + so we need to make a first GET request to get the filename of the changelog + and a second GET to the actual changelog file + */ + try { + let response: Response | null = await fetch( + `${CHANGELOG_BASE_URL}/latest.txt` + ); + const latestChangelogFileName = response.ok + ? await response.text() + : null; + response = latestChangelogFileName + ? await fetch(`${CHANGELOG_BASE_URL}/${latestChangelogFileName}`) + : null; + const changelog = response?.ok ? await response?.text() : null; + + // We only want to see the release notes of newer versions + const currentVersionIndex = changelog?.indexOf( + `\r\n\r\n---\r\n\r\n## ${this.updater.currentVersion}\r\n\r\n` + ); + const newChangelog = + currentVersionIndex && currentVersionIndex > 0 + ? changelog?.slice(0, currentVersionIndex) + : changelog; + updateInfo.releaseNotes = newChangelog; + } catch { + /* + if the request for the changelog fails, we'll just avoid to show it + to the user, but we will still show the update info + */ + } + return updateInfo; + } + } + + async downloadUpdate(): Promise<void> { + try { + await this.updater.downloadUpdate(this.cancellationToken); + } catch (e) { + if (e.message === 'cancelled') return; + throw e; + } + } + + stopDownload(): void { + this.cancellationToken?.cancel(); + } + + quitAndInstall(): void { + this.updater.quitAndInstall(); + } + + disconnectClient(client: IDEUpdaterClient): void { + const index = this.clients.indexOf(client); + if (index !== -1) { + this.clients.splice(index, 1); + } + } + + dispose(): void { + this.clients.forEach(this.disconnectClient.bind(this)); + } +} diff --git a/browser-app/package.json b/browser-app/package.json index 731a07623..4295971bb 100644 --- a/browser-app/package.json +++ b/browser-app/package.json @@ -7,7 +7,7 @@ "@theia/core": "1.22.1", "@theia/debug": "1.22.1", "@theia/editor": "1.22.1", - "@theia/editor-preview": "1.22.1", + "@theia/editor-preview": "1.22.1", "@theia/file-search": "1.22.1", "@theia/filesystem": "1.22.1", "@theia/keymaps": "1.22.1", @@ -60,4 +60,4 @@ } } } -} +} \ No newline at end of file diff --git a/electron-app/package.json b/electron-app/package.json index 5cf039a8f..a500f5eae 100644 --- a/electron-app/package.json +++ b/electron-app/package.json @@ -64,4 +64,4 @@ } } } -} +} \ No newline at end of file diff --git a/electron/build/template-package.json b/electron/build/template-package.json index 38bc38bc8..4f21f78ff 100644 --- a/electron/build/template-package.json +++ b/electron/build/template-package.json @@ -54,6 +54,8 @@ "build": { "productName": "Arduino IDE", "asar": false, + "detectUpdateChannel": false, + "generateUpdatesFilesForAllChannels": true, "npmRebuild": false, "directories": { "buildResources": "resources" @@ -91,13 +93,17 @@ "entitlements": "resources/entitlements.mac.plist", "entitlementsInherit": "resources/entitlements.mac.plist", "target": [ - "dmg" + "dmg", + "zip" ] }, "linux": { "target": [ { "target": "zip" + }, + { + "target": "AppImage" } ], "category": "Development", @@ -147,4 +153,4 @@ "vscode-builtin-json-language-features": "https://open-vsx.org/api/vscode/json-language-features/1.46.1/file/vscode.json-language-features-1.46.1.vsix", "cortex-debug": "https://open-vsx.org/api/marus25/cortex-debug/0.3.10/file/marus25.cortex-debug-0.3.10.vsix" } -} +} \ No newline at end of file diff --git a/electron/packager/config.js b/electron/packager/config.js index 2bf92c362..e74d9d84d 100644 --- a/electron/packager/config.js +++ b/electron/packager/config.js @@ -8,87 +8,113 @@ const dateFormat = require('dateformat'); const { isNightly, isRelease, git } = require('./utils'); function artifactName() { - const { platform, arch } = process; - const id = (() => { - if (isRelease) { - return getVersion(); - } else if (isNightly) { - return `nightly-${timestamp()}` - } else { - return getVersion(); + const { platform, arch } = process; + const id = (() => { + if (isRelease) { + return getVersion(); + } else if (isNightly) { + return `nightly-${timestamp()}`; + } else { + return getVersion(); + } + })(); + const name = 'arduino-ide'; + switch (platform) { + case 'win32': { + if (arch === 'x64') { + return `${name}_${id}_Windows_64bit.\$\{ext}`; + } + throw new Error(`Unsupported platform, arch: ${platform}, ${arch}`); + } + case 'darwin': { + return `${name}_${id}_macOS_64bit.\$\{ext}`; + } + case 'linux': { + switch (arch) { + case 'arm': { + return `${name}_${id}_Linux_ARMv7.\$\{ext}`; } - })(); - const name = 'arduino-ide'; - switch (platform) { - case 'win32': { - if (arch === 'x64') { - return `${name}_${id}_Windows_64bit.\$\{ext}`; - } - throw new Error(`Unsupported platform, arch: ${platform}, ${arch}`); + case 'arm64': { + return `${name}_${id}_Linux_ARM64.\$\{ext}`; } - case 'darwin': { - return `${name}_${id}_macOS_64bit.\$\{ext}`; + case 'x64': { + return `${name}_${id}_Linux_64bit.\$\{ext}`; } - case 'linux': { - switch (arch) { - case 'arm': { - return `${name}_${id}_Linux_ARMv7.\$\{ext}`; - } - case 'arm64': { - return `${name}_${id}_Linux_ARM64.\$\{ext}`; - } - case 'x64': { - return `${name}_${id}_Linux_64bit.\$\{ext}`; - } - default: { - throw new Error(`Unsupported platform, arch: ${platform}, ${arch}`); - } - } + default: { + throw new Error(`Unsupported platform, arch: ${platform}, ${arch}`); } - default: throw new Error(`Unsupported platform, arch: ${platform}, ${arch}`); + } } + default: + throw new Error(`Unsupported platform, arch: ${platform}, ${arch}`); + } } function electronPlatform() { - switch (process.platform) { - case 'win32': { - return 'win'; - } - case 'darwin': { - return 'mac'; - } - case 'linux': { - return 'linux'; - } - default: throw new Error(`Unsupported platform: ${process.platform}.`); + switch (process.platform) { + case 'win32': { + return 'win'; + } + case 'darwin': { + return 'mac'; + } + case 'linux': { + return 'linux'; } + default: + throw new Error(`Unsupported platform: ${process.platform}.`); + } } function getVersion() { - const repositoryRootPath = git('rev-parse --show-toplevel'); - let version = JSON.parse(fs.readFileSync(path.join(repositoryRootPath, 'package.json'), { encoding: 'utf8' })).version; - if (!semver.valid(version)) { - throw new Error(`Could not read version from root package.json. Version was: '${version}'.`); + const repositoryRootPath = git('rev-parse --show-toplevel'); + let version = JSON.parse( + fs.readFileSync(path.join(repositoryRootPath, 'package.json'), { + encoding: 'utf8', + }) + ).version; + if (!semver.valid(version)) { + throw new Error( + `Could not read version from root package.json. Version was: '${version}'.` + ); + } + if (!isRelease) { + if (isNightly) { + version = `${version}-nightly.${timestamp()}`; + } else { + version = `${version}-snapshot.${currentCommitish()}`; } if (!isRelease) { - if (isNightly) { - version = `${version}-nightly-${timestamp()}`; - } else { - version = `${version}-snapshot-${currentCommitish()}`; - } - if (!semver.valid(version)) { - throw new Error(`Invalid patched version: '${version}'.`); - } + if (isNightly) { + version = `${version}-nightly-${timestamp()}`; + } else { + version = `${version}-snapshot-${currentCommitish()}`; + } + if (!semver.valid(version)) { + throw new Error(`Invalid patched version: '${version}'.`); + } } - return version; + } + return version; +} + +function getChannel() { + if (isRelease) { + return 'stable'; + } + if (isNightly) { + return 'nightly'; + } + + return 'none'; } function timestamp() { - return dateFormat(new Date(), 'yyyymmdd'); + return dateFormat(new Date(), 'yyyymmdd'); } function currentCommitish() { - return git('rev-parse --short HEAD'); + return git('rev-parse --short HEAD'); } // function currentBranch() { @@ -96,28 +122,38 @@ function currentCommitish() { // } function generateTemplate(buildDate) { - // do `export PUBLISH=true yarn package` if you want to mimic CI build locally. - // const electronPublish = release || (isCI && currentBranch() === 'main') || process.env.PUBLISH === 'true'; - const version = getVersion(); - const productName = 'Arduino IDE'; - const name = 'arduino-ide'; - let customizations = { - name, - description: productName, - version, - build: { - productName, - appId: 'arduino.ProIDE', - [electronPlatform()]: { - artifactName: artifactName() - } - } - }; - if (buildDate) { - customizations = merge(customizations, { theia: { frontend: { config: { buildDate } } } }); - } - const template = require('../build/template-package.json'); - return merge(template, customizations); + // do `export PUBLISH=true yarn package` if you want to mimic CI build locally. + // const electronPublish = release || (isCI && currentBranch() === 'main') || process.env.PUBLISH === 'true'; + const version = getVersion(); + const productName = 'Arduino IDE'; + const name = 'arduino-ide'; + const updateChannel = getChannel(); + let customizations = { + name, + description: productName, + version, + theia: { + frontend: { + config: { + 'arduino.ide.updateChannel': updateChannel, + }, + }, + }, + build: { + productName, + appId: 'cc.arduino.IDE2', + [electronPlatform()]: { + artifactName: artifactName(), + }, + }, + }; + if (buildDate) { + customizations = merge(customizations, { + theia: { frontend: { config: { buildDate } } }, + }); + } + const template = require('../build/template-package.json'); + return merge(template, customizations); } module.exports = { generateTemplate }; diff --git a/electron/packager/index.js b/electron/packager/index.js index 70ddc014f..17fc272a7 100644 --- a/electron/packager/index.js +++ b/electron/packager/index.js @@ -13,7 +13,7 @@ const template = require('./config').generateTemplate(new Date().toISOString()); const utils = require('./utils'); const merge = require('deepmerge'); - const { isRelease, isElectronPublish } = utils; + const { isRelease, isElectronPublish, getChannelFile } = utils; const { version } = template; const { productName } = template.build; @@ -277,6 +277,13 @@ ${fs.readFileSync(path('..', 'build', 'package.json')).toString()} const targetFolder = path('..', 'build', 'dist', 'build-artifacts'); mkdir('-p', targetFolder); const filesToCopy = []; + const channelFile = getChannelFile(platform); + // Channel file might be an empty string if we're not building a + // nightly or a full release. This can happen when building a package + // locally or a tester build when creating a new PR on GH. + if (channelFile) { + filesToCopy.push(channelFile) + } switch (platform) { case 'linux': { filesToCopy.push(...glob.sync('**/arduino-ide*.{zip,AppImage}', { cwd }).map(p => join(cwd, p))); diff --git a/electron/packager/utils.js b/electron/packager/utils.js index 40001782f..d8e7faf14 100644 --- a/electron/packager/utils.js +++ b/electron/packager/utils.js @@ -12,35 +12,42 @@ const fromFile = require('file-type').fromFile; * Resolves to an array of `npm` package names that are declared in the `package.json` but **not** used by the project. */ function collectUnusedDependencies(pathToProject = process.cwd()) { - const p = path.isAbsolute(pathToProject) ? pathToProject : path.resolve(process.cwd(), pathToProject); - console.log(`⏱️ >>> Collecting unused backend dependencies for ${p}...`); - return new Promise(resolve => { - depcheck(p, { - ignoreDirs: [ - 'frontend' - ], - parsers: { - '*.js': depcheck.parser.es6, - '*.jsx': depcheck.parser.jsx - }, - detectors: [ - depcheck.detector.requireCallExpression, - depcheck.detector.importDeclaration - ], - specials: [ - depcheck.special.eslint, - depcheck.special.webpack - ] - }, unused => { - const { dependencies } = unused - if (dependencies && dependencies.length > 0) { - console.log(`👌 <<< The following unused dependencies have been found: ${JSON.stringify(dependencies, null, 2)}`); - } else { - console.log('👌 <<< No unused dependencies have been found.'); - } - resolve(dependencies); - }); - }) + const p = path.isAbsolute(pathToProject) + ? pathToProject + : path.resolve(process.cwd(), pathToProject); + console.log(`⏱️ >>> Collecting unused backend dependencies for ${p}...`); + return new Promise((resolve) => { + depcheck( + p, + { + ignoreDirs: ['frontend'], + parsers: { + '*.js': depcheck.parser.es6, + '*.jsx': depcheck.parser.jsx, + }, + detectors: [ + depcheck.detector.requireCallExpression, + depcheck.detector.importDeclaration, + ], + specials: [depcheck.special.eslint, depcheck.special.webpack], + }, + (unused) => { + const { dependencies } = unused; + if (dependencies && dependencies.length > 0) { + console.log( + `👌 <<< The following unused dependencies have been found: ${JSON.stringify( + dependencies, + null, + 2 + )}` + ); + } else { + console.log('👌 <<< No unused dependencies have been found.'); + } + resolve(dependencies); + } + ); + }); } /** @@ -50,101 +57,111 @@ function collectUnusedDependencies(pathToProject = process.cwd()) { * If `pathToZip` is not a ZIP, rejects. `targetFolderName` is the destination folder not the new archive location. */ function adjustArchiveStructure(pathToZip, targetFolderName, noCleanup) { - return new Promise(async (resolve, reject) => { - if (!await isZip(pathToZip)) { - reject(new Error(`Expected a ZIP file.`)); - return; - } - if (!fs.existsSync(targetFolderName)) { - reject(new Error(`${targetFolderName} does not exist.`)); - return; - } - if (!fs.lstatSync(targetFolderName).isDirectory()) { - reject(new Error(`${targetFolderName} is not a directory.`)); - return; - } - console.log(`⏱️ >>> Adjusting ZIP structure ${pathToZip}...`); + return new Promise(async (resolve, reject) => { + if (!(await isZip(pathToZip))) { + reject(new Error(`Expected a ZIP file.`)); + return; + } + if (!fs.existsSync(targetFolderName)) { + reject(new Error(`${targetFolderName} does not exist.`)); + return; + } + if (!fs.lstatSync(targetFolderName).isDirectory()) { + reject(new Error(`${targetFolderName} is not a directory.`)); + return; + } + console.log(`⏱️ >>> Adjusting ZIP structure ${pathToZip}...`); - const root = basename(pathToZip); - const resources = await list(pathToZip); - const hasBaseFolder = resources.find(name => name === root); - if (hasBaseFolder) { - if (resources.filter(name => name.indexOf(path.sep) === -1).length > 1) { - console.warn(`${pathToZip} ZIP has the desired root folder ${root}, however the ZIP contains other entries too: ${JSON.stringify(resources)}`); - } - console.log(`👌 <<< The ZIP already has the desired ${root} folder.`); - resolve(pathToZip); - return; - } + const root = basename(pathToZip); + const resources = await list(pathToZip); + const hasBaseFolder = resources.find((name) => name === root); + if (hasBaseFolder) { + if ( + resources.filter((name) => name.indexOf(path.sep) === -1).length > 1 + ) { + console.warn( + `${pathToZip} ZIP has the desired root folder ${root}, however the ZIP contains other entries too: ${JSON.stringify( + resources + )}` + ); + } + console.log(`👌 <<< The ZIP already has the desired ${root} folder.`); + resolve(pathToZip); + return; + } - const track = temp.track(); - try { - const unzipOut = path.join(track.mkdirSync(), root); - fs.mkdirSync(unzipOut); - await unpack(pathToZip, unzipOut); - const adjustedZip = path.join(targetFolderName, path.basename(pathToZip)); - await pack(unzipOut, adjustedZip); - console.log(`👌 <<< Adjusted the ZIP structure. Moved the modified ${basename(pathToZip)} to the ${targetFolderName} folder.`); - resolve(adjustedZip); - } finally { - if (!noCleanup) { - track.cleanupSync(); - } - } - }); + const track = temp.track(); + try { + const unzipOut = path.join(track.mkdirSync(), root); + fs.mkdirSync(unzipOut); + await unpack(pathToZip, unzipOut); + const adjustedZip = path.join(targetFolderName, path.basename(pathToZip)); + await pack(unzipOut, adjustedZip); + console.log( + `👌 <<< Adjusted the ZIP structure. Moved the modified ${basename( + pathToZip + )} to the ${targetFolderName} folder.` + ); + resolve(adjustedZip); + } finally { + if (!noCleanup) { + track.cleanupSync(); + } + } + }); } /** * Returns the `basename` of `pathToFile` without the file extension. */ function basename(pathToFile) { - const name = path.basename(pathToFile); - const ext = path.extname(pathToFile); - return name.substr(0, name.length - ext.length); + const name = path.basename(pathToFile); + const ext = path.extname(pathToFile); + return name.substr(0, name.length - ext.length); } function unpack(what, where) { - return new Promise((resolve, reject) => { - zip.unpack(what, where, error => { - if (error) { - reject(error); - return; - } - resolve(); - }) + return new Promise((resolve, reject) => { + zip.unpack(what, where, (error) => { + if (error) { + reject(error); + return; + } + resolve(); }); + }); } function pack(what, where) { - return new Promise((resolve, reject) => { - zip.pack(what, where, error => { - if (error) { - reject(error); - return; - } - resolve(); - }) + return new Promise((resolve, reject) => { + zip.pack(what, where, (error) => { + if (error) { + reject(error); + return; + } + resolve(); }); + }); } function list(what) { - return new Promise((resolve, reject) => { - zip.list(what, (error, result) => { - if (error) { - reject(error); - return; - } - resolve(result.map(({ name }) => name)); - }) + return new Promise((resolve, reject) => { + zip.list(what, (error, result) => { + if (error) { + reject(error); + return; + } + resolve(result.map(({ name }) => name)); }); + }); } async function isZip(pathToFile) { - if (!fs.existsSync(pathToFile)) { - throw new Error(`${pathToFile} does not exist`); - } - const type = await fromFile(pathToFile); - return type && type.ext === 'zip'; + if (!fs.existsSync(pathToFile)) { + throw new Error(`${pathToFile} does not exist`); + } + const type = await fromFile(pathToFile); + return type && type.ext === 'zip'; } const isElectronPublish = false; // TODO: support auto-updates @@ -152,20 +169,62 @@ const isNightly = process.env.IS_NIGHTLY === 'true'; const isRelease = process.env.IS_RELEASE === 'true'; function git(command) { - try { - const gitPath = shell.which('git'); - const error = shell.error(); - if (error) { - throw new Error(error); - } - const { stderr, stdout } = shell.exec(`"${gitPath}" ${command}`, { silent: true }); - if (stderr) { - throw new Error(stderr.toString().trim()); - } - return stdout.toString().trim(); - } catch (e) { - throw e; + try { + const gitPath = shell.which('git'); + const error = shell.error(); + if (error) { + throw new Error(error); + } + const { stderr, stdout } = shell.exec(`"${gitPath}" ${command}`, { + silent: true, + }); + if (stderr) { + throw new Error(stderr.toString().trim()); } + return stdout.toString().trim(); + } catch (e) { + throw e; + } +} + +// getChannelFile returns the name of the channel file to be released +// together with the IDE file. +// The channel file depends on the platform and whether we're creating +// a nightly build or a full release. +// In all other cases, like when building a tester build for a PR, +// an empty string is returned since we don't need a channel file. +// The channel files are necessary for updates check with electron-updater +// to work correctly. +// For more information: https://www.electron.build/auto-update +function getChannelFile(platform) { + let currentChannel = ''; + if (isNightly) { + currentChannel = 'nightly'; + } else if (isRelease) { + currentChannel = 'stable'; + } else { + // We're not creating a nightly build nor releasing + // a new version, no need for a channel file. + return ''; + } + return ( + currentChannel + + { + linux: '-linux.yml', + win32: '.yml', + darwin: '-mac.yml', + }[platform] + ); } -module.exports = { collectUnusedDependencies, adjustArchiveStructure, isZip, unpack, isNightly, isRelease, isElectronPublish, git }; +module.exports = { + collectUnusedDependencies, + adjustArchiveStructure, + isZip, + unpack, + isNightly, + isRelease, + isElectronPublish, + git, + getChannelFile, +}; diff --git a/electron/packager/yarn.lock b/electron/packager/yarn.lock index 473faa674..396dd6c1d 100644 --- a/electron/packager/yarn.lock +++ b/electron/packager/yarn.lock @@ -1679,4 +1679,4 @@ yargs@^15.0.2: string-width "^4.2.0" which-module "^2.0.0" y18n "^4.0.0" - yargs-parser "^18.1.1" + yargs-parser "^18.1.1" \ No newline at end of file diff --git a/i18n/en.json b/i18n/en.json index dd93d03a7..614dbdf2b 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -31,7 +31,7 @@ "upload.verbose": "True for verbose upload output. False by default.", "window.autoScale": "True if the user interface automatically scales with the font size.", "window.zoomLevel": "Adjust the zoom level of the window. The original size is 0 and each increment above (e.g. 1) or below (e.g. -1) represents zooming 20% larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity.", - "ide.autoUpdate": "True to enable automatic update checks. The IDE will check for updates automatically and periodically.", + "ide.updateChannel": "Release channel to get updated from. 'stable' is the stable release, 'nightly' is the latest development build.", "board.certificates": "List of certificates that can be uploaded to boards", "sketchbook.showAllFiles": "True to show all sketch files inside the sketch. It is false by default.", "cloud.enabled": "True if the sketch sync functions are enabled. Defaults to true.", @@ -55,7 +55,6 @@ "compile": "compile", "upload": "upload", "verifyAfterUpload": "Verify code after upload", - "checkForUpdates": "Check for updates on startup", "editorQuickSuggestions": "Editor Quick Suggestions", "additionalManagerURLs": "Additional Boards Manager URLs", "noProxy": "No proxy", @@ -256,6 +255,20 @@ "dialog": { "dontAskAgain": "Don't ask again" }, + "ide-updater": { + "notNowButton": "Not now", + "versionDownloaded": "Arduino IDE {0} has been downloaded.", + "closeToInstallNotice": "Close the software and install the update on your machine.", + "closeAndInstallButton": "Close and Install", + "downloadingNotice": "Downloading the latest version of the Arduino IDE.", + "updateAvailable": "Update Available", + "newVersionAvailable": "A new version of Arduino IDE ({0}) is available for download.", + "skipVersionButton": "Skip Version", + "downloadButton": "Download" + }, + "updater": { + "ideUpdaterDialog": "Software Update" + }, "userFields": { "cancel": "Cancel", "upload": "Upload" diff --git a/yarn.lock b/yarn.lock index ad3488736..56e34bc9b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2912,6 +2912,13 @@ resolved "https://registry.yarnpkg.com/@types/dateformat/-/dateformat-3.0.1.tgz#98d747a2e5e9a56070c6bf14e27bff56204e34cc" integrity sha512-KlPPdikagvL6ELjWsljbyDIPzNCeliYkqRpI+zea99vBBbCIA5JNshZAwQKTON139c87y9qvTFVgkFd14rtS4g== +"@types/debug@^4.0.0": + version "4.1.7" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" + integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg== + dependencies: + "@types/ms" "*" + "@types/deepmerge@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@types/deepmerge/-/deepmerge-2.2.0.tgz#6f63896c217f3164782f52d858d9f3a927139f64" @@ -3015,6 +3022,13 @@ resolved "https://registry.yarnpkg.com/@types/google-protobuf/-/google-protobuf-3.7.4.tgz#1621c50ceaf5aefa699851da8e0ea606a2943a39" integrity sha512-6PjMFKl13cgB4kRdYtvyjKl8VVa0PXS2IdVxHhQ8GEKbxBkyJtSbaIeK1eZGjDKN7dvUh4vkOvU9FMwYNv4GQQ== +"@types/hast@^2.0.0": + version "2.3.4" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.4.tgz#8aa5ef92c117d20d974a82bdfb6a648b08c0bafc" + integrity sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g== + dependencies: + "@types/unist" "*" + "@types/js-yaml@^3.12.2": version "3.12.6" resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.6.tgz#7f10c926aa41e189a2755c4c7fcf8e4573bd7ac1" @@ -3091,7 +3105,14 @@ "@types/linkify-it" "*" "@types/mdurl" "*" -"@types/mdurl@*": +"@types/mdast@^3.0.0": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af" + integrity sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA== + dependencies: + "@types/unist" "*" + +"@types/mdurl@*", "@types/mdurl@^1.0.0": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9" integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA== @@ -3140,6 +3161,11 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea" integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ== +"@types/ms@*": + version "0.7.31" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" + integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== + "@types/multer@^1.4.7": version "1.4.7" resolved "https://registry.yarnpkg.com/@types/multer/-/multer-1.4.7.tgz#89cf03547c28c7bbcc726f029e2a76a7232cc79e" @@ -3337,7 +3363,7 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45" integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ== -"@types/semver@^7.3.8": +"@types/semver@^7.3.6", "@types/semver@^7.3.8": version "7.3.9" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.9.tgz#152c6c20a7688c30b967ec1841d31ace569863fc" integrity sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ== @@ -3410,6 +3436,11 @@ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.0.tgz#aee6e868fcef74f2b8c71614b6df81a601a42f17" integrity sha512-I8MnZqNXsOLHsU111oHbn3khtvKMi5Bn4qVFsIWSJcCP1KKDiXX5AEw8UPk0nSopeC+Hvxt6yAy1/a5PailFqg== +"@types/unist@*", "@types/unist@^2.0.0": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" + integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== + "@types/uuid@^7.0.3": version "7.0.4" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-7.0.4.tgz#00a5749810b4ad80bff73a61f9cc9d0d521feb3c" @@ -4473,6 +4504,11 @@ back@~0.1.5: resolved "https://registry.yarnpkg.com/back/-/back-0.1.5.tgz#342b96b804657b03ec9a31f248a11f200608dcc2" integrity sha1-NCuWuARlewPsmjHySKEfIAYI3MI= +bail@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" + integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -4721,6 +4757,14 @@ buffers@~0.1.1: resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" integrity sha1-skV5w77U1tOWru5tmorn9Ugqt7s= +builder-util-runtime@8.9.2: + version "8.9.2" + resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-8.9.2.tgz#a9669ae5b5dcabfe411ded26678e7ae997246c28" + integrity sha512-rhuKm5vh7E0aAmT6i8aoSfEjxzdYEFX7zDApK+eNgOhjofnWb74d9SRJv0H/8nsgOkos0TZ4zxW0P8J4N7xQ2A== + dependencies: + debug "^4.3.2" + sax "^1.2.4" + builtins@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" @@ -4988,6 +5032,11 @@ changes-stream@^2.2.0: http-https "~1.0.0" readable-stream "1.0.x" +character-entities@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.1.tgz#98724833e1e27990dee0bd0f2b8a859c3476aac7" + integrity sha512-OzmutCf2Kmc+6DrFrrPS8/tDh2+DpnrfzdICHWhcVC9eOd0N1PXmQEE1a8iM4IziIAG+8tmTq3K+oo0ubH6RRQ== + chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" @@ -5252,6 +5301,11 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +comma-separated-tokens@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.2.tgz#d4c25abb679b7751c880be623c1179780fe1dd98" + integrity sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg== + commander@^2.20.0, commander@^2.8.1: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -5767,6 +5821,13 @@ debug@^3.1.0, debug@^3.2.6: dependencies: ms "^2.1.1" +debug@^4.0.0, debug@^4.3.2: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + debug@~0.8.0: version "0.8.1" resolved "https://registry.yarnpkg.com/debug/-/debug-0.8.1.tgz#20ff4d26f5e422cb68a1bacbbb61039ad8c1c130" @@ -5790,6 +5851,13 @@ decamelize@^1.1.0, decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= +decode-named-character-reference@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.1.tgz#57b2bd9112659cacbc449d3577d7dadb8e1f3d1b" + integrity sha512-YV/0HQHreRwKb7uBopyIkLG17jG6Sv2qUchk9qSoVJ2f+flwRsPNBO0hAnjt6mTNYUT+vw9Gy2ihXg4sUWPi2w== + dependencies: + character-entities "^2.0.0" + decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" @@ -5977,6 +6045,11 @@ deprecation@^2.0.0, deprecation@^2.3.1: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== +dequal@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d" + integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug== + destroy@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" @@ -6210,6 +6283,20 @@ electron-to-chromium@^1.3.649: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.695.tgz#955f419cf99137226180cc4cca2e59015a4e248d" integrity sha512-lz66RliUqLHU1Ojxx1A4QUxKydjiQ79Y4dZyPobs2Dmxj5aVL2TM3KoQ2Gs7HS703Bfny+ukI3KOxwAB0xceHQ== +electron-updater@^4.6.5: + version "4.6.5" + resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-4.6.5.tgz#e9a75458bbfd6bb41a58a829839e150ad2eb2d3d" + integrity sha512-kdTly8O9mSZfm9fslc1mnCY+mYOeaYRy7ERa2Fed240u01BKll3aiupzkd07qKw69KvhBSzuHroIW3mF0D8DWA== + dependencies: + "@types/semver" "^7.3.6" + builder-util-runtime "8.9.2" + fs-extra "^10.0.0" + js-yaml "^4.1.0" + lazy-val "^1.0.5" + lodash.escaperegexp "^4.1.2" + lodash.isequal "^4.5.0" + semver "^7.3.5" + electron@^15.3.5: version "15.3.6" resolved "https://registry.yarnpkg.com/electron/-/electron-15.3.6.tgz#19b9aee1e063b1983b3d7f535567d90e0e1b4d04" @@ -6760,7 +6847,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.2, extend@~3.0.2: +extend@^3.0.0, extend@^3.0.2, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -7200,6 +7287,15 @@ fs-exists-sync@^0.1.0: resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" integrity sha1-mC1ok6+RjnLQjeyehnP/K1qNat0= +fs-extra@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" + integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^4.0.2: version "4.0.3" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" @@ -7893,6 +7989,11 @@ hash.js@^1.1.7: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hast-util-whitespace@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz#4fc1086467cc1ef5ba20673cb6b03cec3a970f1c" + integrity sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg== + he@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -8242,6 +8343,11 @@ init-package-json@^1.10.3: validate-npm-package-license "^3.0.1" validate-npm-package-name "^3.0.0" +inline-style-parser@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" + integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== + inquirer@^6.2.0: version "6.5.2" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca" @@ -8356,7 +8462,7 @@ is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-buffer@~2.0.3: +is-buffer@^2.0.0, is-buffer@~2.0.3: version "2.0.5" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== @@ -8582,6 +8688,11 @@ is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= +is-plain-obj@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.0.0.tgz#06c0999fd7574edf5a906ba5644ad0feb3a84d22" + integrity sha512-NXRbBtUdBioI73y/HmOhogw/U5msYPC9DAtGkJXeFcFWSFZw0mCUsPxk/snTuJHzNKA8kLBK4rH97RMB1BfCXw== + is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -8787,6 +8898,13 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + jsbn@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" @@ -8915,6 +9033,15 @@ jsonfile@^4.0.0: optionalDependencies: graceful-fs "^4.1.6" +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + jsonparse@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" @@ -9002,6 +9129,11 @@ kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +kleur@^4.0.3: + version "4.1.4" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.4.tgz#8c202987d7e577766d039a8cd461934c01cda04d" + integrity sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA== + lazy-cache@^2.0.1, lazy-cache@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-2.0.2.tgz#b9190a4f913354694840859f8a8f7084d8822264" @@ -9009,6 +9141,11 @@ lazy-cache@^2.0.1, lazy-cache@^2.0.2: dependencies: set-getter "^0.1.0" +lazy-val@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.5.tgz#6cf3b9f5bc31cee7ee3e369c0832b7583dcd923d" + integrity sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q== + lcid@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" @@ -9250,11 +9387,21 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= +lodash.escaperegexp@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" + integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c= + lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= + lodash.ismatch@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" @@ -9534,7 +9681,55 @@ matcher@^3.0.0: dependencies: escape-string-regexp "^4.0.0" -mdurl@^1.0.1: +mdast-util-definitions@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.0.tgz#b6d10ef00a3c4cf191e8d9a5fa58d7f4a366f817" + integrity sha512-5hcR7FL2EuZ4q6lLMUK5w4lHT2H3vqL9quPvYZ/Ku5iifrirfMHiGdhxdXMUbUkDmz5I+TYMd7nbaxUhbQkfpQ== + dependencies: + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + unist-util-visit "^3.0.0" + +mdast-util-from-markdown@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.2.0.tgz#84df2924ccc6c995dec1e2368b2b208ad0a76268" + integrity sha512-iZJyyvKD1+K7QX1b5jXdE7Sc5dtoTry1vzV28UZZe8Z1xVnB/czKntJ7ZAkG0tANqRnBF6p3p7GpU1y19DTf2Q== + dependencies: + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + decode-named-character-reference "^1.0.0" + mdast-util-to-string "^3.1.0" + micromark "^3.0.0" + micromark-util-decode-numeric-character-reference "^1.0.0" + micromark-util-decode-string "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + unist-util-stringify-position "^3.0.0" + uvu "^0.5.0" + +mdast-util-to-hast@^12.1.0: + version "12.1.1" + resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-12.1.1.tgz#89a2bb405eaf3b05eb8bf45157678f35eef5dbca" + integrity sha512-qE09zD6ylVP14jV4mjLIhDBOrpFdShHZcEsYvvKGABlr9mGbV7mTlRWdoFxL/EYSTNDiC9GZXy7y8Shgb9Dtzw== + dependencies: + "@types/hast" "^2.0.0" + "@types/mdast" "^3.0.0" + "@types/mdurl" "^1.0.0" + mdast-util-definitions "^5.0.0" + mdurl "^1.0.0" + micromark-util-sanitize-uri "^1.0.0" + unist-builder "^3.0.0" + unist-util-generated "^2.0.0" + unist-util-position "^4.0.0" + unist-util-visit "^4.0.0" + +mdast-util-to-string@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz#56c506d065fbf769515235e577b5a261552d56e9" + integrity sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA== + +mdurl@^1.0.0, mdurl@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= @@ -9631,6 +9826,201 @@ methods@^1.1.2, methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= +micromark-core-commonmark@^1.0.1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz#edff4c72e5993d93724a3c206970f5a15b0585ad" + integrity sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-factory-destination "^1.0.0" + micromark-factory-label "^1.0.0" + micromark-factory-space "^1.0.0" + micromark-factory-title "^1.0.0" + micromark-factory-whitespace "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-chunked "^1.0.0" + micromark-util-classify-character "^1.0.0" + micromark-util-html-tag-name "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-resolve-all "^1.0.0" + micromark-util-subtokenize "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.1" + uvu "^0.5.0" + +micromark-factory-destination@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz#fef1cb59ad4997c496f887b6977aa3034a5a277e" + integrity sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-factory-label@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz#6be2551fa8d13542fcbbac478258fb7a20047137" + integrity sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-factory-space@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz#cebff49968f2b9616c0fcb239e96685cb9497633" + integrity sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-factory-title@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz#7e09287c3748ff1693930f176e1c4a328382494f" + integrity sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-factory-whitespace@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz#e991e043ad376c1ba52f4e49858ce0794678621c" + integrity sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-character@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.1.0.tgz#d97c54d5742a0d9611a68ca0cd4124331f264d86" + integrity sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg== + dependencies: + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-chunked@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz#5b40d83f3d53b84c4c6bce30ed4257e9a4c79d06" + integrity sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g== + dependencies: + micromark-util-symbol "^1.0.0" + +micromark-util-classify-character@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz#cbd7b447cb79ee6997dd274a46fc4eb806460a20" + integrity sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-combine-extensions@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.0.0.tgz#91418e1e74fb893e3628b8d496085639124ff3d5" + integrity sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA== + dependencies: + micromark-util-chunked "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-decode-numeric-character-reference@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz#dcc85f13b5bd93ff8d2868c3dba28039d490b946" + integrity sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w== + dependencies: + micromark-util-symbol "^1.0.0" + +micromark-util-decode-string@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz#942252ab7a76dec2dbf089cc32505ee2bc3acf02" + integrity sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-decode-numeric-character-reference "^1.0.0" + micromark-util-symbol "^1.0.0" + +micromark-util-encode@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.0.1.tgz#2c1c22d3800870ad770ece5686ebca5920353383" + integrity sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA== + +micromark-util-html-tag-name@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.0.0.tgz#75737e92fef50af0c6212bd309bc5cb8dbd489ed" + integrity sha512-NenEKIshW2ZI/ERv9HtFNsrn3llSPZtY337LID/24WeLqMzeZhBEE6BQ0vS2ZBjshm5n40chKtJ3qjAbVV8S0g== + +micromark-util-normalize-identifier@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.0.0.tgz#4a3539cb8db954bbec5203952bfe8cedadae7828" + integrity sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg== + dependencies: + micromark-util-symbol "^1.0.0" + +micromark-util-resolve-all@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.0.0.tgz#a7c363f49a0162e931960c44f3127ab58f031d88" + integrity sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw== + dependencies: + micromark-util-types "^1.0.0" + +micromark-util-sanitize-uri@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.0.0.tgz#27dc875397cd15102274c6c6da5585d34d4f12b2" + integrity sha512-cCxvBKlmac4rxCGx6ejlIviRaMKZc0fWm5HdCHEeDWRSkn44l6NdYVRyU+0nT1XC72EQJMZV8IPHF+jTr56lAg== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-encode "^1.0.0" + micromark-util-symbol "^1.0.0" + +micromark-util-subtokenize@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz#ff6f1af6ac836f8bfdbf9b02f40431760ad89105" + integrity sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA== + dependencies: + micromark-util-chunked "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-util-symbol@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.0.1.tgz#b90344db62042ce454f351cf0bebcc0a6da4920e" + integrity sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ== + +micromark-util-types@^1.0.0, micromark-util-types@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.0.2.tgz#f4220fdb319205812f99c40f8c87a9be83eded20" + integrity sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w== + +micromark@^3.0.0: + version "3.0.10" + resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.0.10.tgz#1eac156f0399d42736458a14b0ca2d86190b457c" + integrity sha512-ryTDy6UUunOXy2HPjelppgJ2sNfcPz1pLlMdA6Rz9jPzhLikWXv/irpWV/I2jd68Uhmny7hHxAlAhk4+vWggpg== + dependencies: + "@types/debug" "^4.0.0" + debug "^4.0.0" + decode-named-character-reference "^1.0.0" + micromark-core-commonmark "^1.0.1" + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-chunked "^1.0.0" + micromark-util-combine-extensions "^1.0.0" + micromark-util-decode-numeric-character-reference "^1.0.0" + micromark-util-encode "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-resolve-all "^1.0.0" + micromark-util-sanitize-uri "^1.0.0" + micromark-util-subtokenize "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.1" + uvu "^0.5.0" + micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -9914,6 +10304,11 @@ move-file@^1.1.0: make-dir "^3.0.0" path-exists "^3.0.0" +mri@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" + integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -11210,6 +11605,15 @@ promzard@^0.3.0: dependencies: read "1" +prop-types@^15.0.0: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + prop-types@^15.5.0, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" @@ -11219,6 +11623,11 @@ prop-types@^15.5.0, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0, object-assign "^4.1.1" react-is "^16.8.1" +property-information@^6.0.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.1.1.tgz#5ca85510a3019726cb9afed4197b7b8ac5926a22" + integrity sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w== + proto-list@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" @@ -11497,16 +11906,41 @@ react-input-autosize@^3.0.0: dependencies: prop-types "^15.5.8" -react-is@^16.8.1: +react-is@^16.13.1, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-is@^17.0.0: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== +react-markdown@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.0.tgz#3243296a59ddb0f451d262cc2e11123674b416c2" + integrity sha512-qbrWpLny6Ef2xHqnYqtot948LXP+4FtC+MWIuaN1kvSnowM+r1qEeEHpSaU0TDBOisQuj+Qe6eFY15cNL3gLAw== + dependencies: + "@types/hast" "^2.0.0" + "@types/unist" "^2.0.0" + comma-separated-tokens "^2.0.0" + hast-util-whitespace "^2.0.0" + prop-types "^15.0.0" + property-information "^6.0.0" + react-is "^17.0.0" + remark-parse "^10.0.0" + remark-rehype "^10.0.0" + space-separated-tokens "^2.0.0" + style-to-object "^0.3.0" + unified "^10.0.0" + unist-util-visit "^4.0.0" + vfile "^5.0.0" + react-perfect-scrollbar@^1.5.3: version "1.5.8" resolved "https://registry.yarnpkg.com/react-perfect-scrollbar/-/react-perfect-scrollbar-1.5.8.tgz#380959387a325c5c9d0268afc08b3f73ed5b3078" @@ -11863,6 +12297,25 @@ relative@^3.0.2: dependencies: isobject "^2.0.0" +remark-parse@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-10.0.1.tgz#6f60ae53edbf0cf38ea223fe643db64d112e0775" + integrity sha512-1fUyHr2jLsVOkhbvPRBJ5zTKZZyD6yZzYaWCS6BPBdQ8vEMBCH+9zNCDA6tET/zHCi/jLqjCWtlJZUPk+DbnFw== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-from-markdown "^1.0.0" + unified "^10.0.0" + +remark-rehype@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-10.1.0.tgz#32dc99d2034c27ecaf2e0150d22a6dcccd9a6279" + integrity sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw== + dependencies: + "@types/hast" "^2.0.0" + "@types/mdast" "^3.0.0" + mdast-util-to-hast "^12.1.0" + unified "^10.0.0" + remarkable@^1.6.2, remarkable@^1.7.1: version "1.7.4" resolved "https://registry.yarnpkg.com/remarkable/-/remarkable-1.7.4.tgz#19073cb960398c87a7d6546eaa5e50d2022fcd00" @@ -12128,6 +12581,13 @@ rxjs@^6.6.7: dependencies: tslib "^1.9.0" +sade@^1.7.3: + version "1.8.1" + resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" + integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== + dependencies: + mri "^1.1.0" + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -12576,6 +13036,11 @@ source-map@~0.7.2: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== +space-separated-tokens@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.1.tgz#43193cec4fb858a2ce934b7f98b7f2c18107098b" + integrity sha512-ekwEbFp5aqSPKaqeY1PGrlGQxPNaq+Cnx4+bE2D8sciBQrHpbwoBbawqTN2+6jPs9IdWxxiUcN0K2pkczD3zmw== + spawn-rx@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/spawn-rx/-/spawn-rx-3.0.0.tgz#1d33511e13ec26337da51d78630e08beb57a6767" @@ -12959,6 +13424,13 @@ style-loader@^2.0.0: loader-utils "^2.0.0" schema-utils "^3.0.0" +style-to-object@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.3.0.tgz#b1b790d205991cc783801967214979ee19a76e46" + integrity sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA== + dependencies: + inline-style-parser "0.1.1" + success-symbol@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/success-symbol/-/success-symbol-0.1.0.tgz#24022e486f3bf1cdca094283b769c472d3b72897" @@ -13358,6 +13830,11 @@ trim-repeated@^1.0.0: dependencies: escape-string-regexp "^1.0.2" +trough@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/trough/-/trough-2.0.2.tgz#94a3aa9d5ce379fc561f6244905b3f36b7458d96" + integrity sha512-FnHq5sTMxC0sk957wHDzRnemFnNBvt/gSY99HzK8F7UP5WAbvP70yX5bd7CjEQkN+TjdxwI7g7lJ6podqrG2/w== + ts-md5@^1.2.2: version "1.2.7" resolved "https://registry.yarnpkg.com/ts-md5/-/ts-md5-1.2.7.tgz#b76471fc2fd38f0502441f6c3b9494ed04537401" @@ -13585,6 +14062,19 @@ unicode-property-aliases-ecmascript@^1.0.4: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4" integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg== +unified@^10.0.0: + version "10.1.1" + resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.1.tgz#345e349e3ab353ab612878338eb9d57b4dea1d46" + integrity sha512-v4ky1+6BN9X3pQrOdkFIPWAaeDsHPE1svRDxq7YpTc2plkIqFMwukfqM+l0ewpP9EfwARlt9pPFAeWYhHm8X9w== + dependencies: + "@types/unist" "^2.0.0" + bail "^2.0.0" + extend "^3.0.0" + is-buffer "^2.0.0" + is-plain-obj "^4.0.0" + trough "^2.0.0" + vfile "^5.0.0" + union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" @@ -13609,6 +14099,69 @@ unique-slug@^2.0.0: dependencies: imurmurhash "^0.1.4" +unist-builder@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-3.0.0.tgz#728baca4767c0e784e1e64bb44b5a5a753021a04" + integrity sha512-GFxmfEAa0vi9i5sd0R2kcrI9ks0r82NasRq5QHh2ysGngrc6GiqD5CDf1FjPenY4vApmFASBIIlk/jj5J5YbmQ== + dependencies: + "@types/unist" "^2.0.0" + +unist-util-generated@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-2.0.0.tgz#86fafb77eb6ce9bfa6b663c3f5ad4f8e56a60113" + integrity sha512-TiWE6DVtVe7Ye2QxOVW9kqybs6cZexNwTwSMVgkfjEReqy/xwGpAXb99OxktoWwmL+Z+Epb0Dn8/GNDYP1wnUw== + +unist-util-is@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.1.1.tgz#e8aece0b102fa9bc097b0fef8f870c496d4a6236" + integrity sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ== + +unist-util-position@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-4.0.1.tgz#f8484b2da19a897a0180556d160c28633070dbb9" + integrity sha512-mgy/zI9fQ2HlbOtTdr2w9lhVaiFUHWQnZrFF2EUoVOqtAUdzqMtNiD99qA5a1IcjWVR8O6aVYE9u7Z2z1v0SQA== + +unist-util-stringify-position@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.0.tgz#d517d2883d74d0daa0b565adc3d10a02b4a8cde9" + integrity sha512-SdfAl8fsDclywZpfMDTVDxA2V7LjtRDTOFd44wUJamgl6OlVngsqWjxvermMYf60elWHbxhuRCZml7AnuXCaSA== + dependencies: + "@types/unist" "^2.0.0" + +unist-util-visit-parents@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-4.1.1.tgz#e83559a4ad7e6048a46b1bdb22614f2f3f4724f2" + integrity sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + +unist-util-visit-parents@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.0.tgz#44bbc5d25f2411e7dfc5cecff12de43296aa8521" + integrity sha512-y+QVLcY5eR/YVpqDsLf/xh9R3Q2Y4HxkZTp7ViLDU6WtJCEcPmRzW1gpdWDCDIqIlhuPDXOgttqPlykrHYDekg== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + +unist-util-visit@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-3.1.0.tgz#9420d285e1aee938c7d9acbafc8e160186dbaf7b" + integrity sha512-Szoh+R/Ll68QWAyQyZZpQzZQm2UPbxibDvaY8Xc9SUtYgPsDzx5AWSk++UUt2hJuow8mvwR+rG+LQLw+KsuAKA== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + unist-util-visit-parents "^4.0.0" + +unist-util-visit@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.0.tgz#f41e407a9e94da31594e6b1c9811c51ab0b3d8f5" + integrity sha512-n7lyhFKJfVZ9MnKtqbsqkQEk5P1KShj0+//V7mAcoI6bpbUjh3C/OG8HVD+pBihfh6Ovl01m8dkcv9HNqYajmQ== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + unist-util-visit-parents "^5.0.0" + universal-user-agent@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-4.0.1.tgz#fd8d6cb773a679a709e967ef8288a31fcc03e557" @@ -13626,6 +14179,11 @@ universalify@^0.1.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -13769,6 +14327,16 @@ uuid@^8.0.0, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uvu@^0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.3.tgz#3d83c5bc1230f153451877bfc7f4aea2392219ae" + integrity sha512-brFwqA3FXzilmtnIyJ+CxdkInkY/i4ErvP7uV0DnUVxQcQ55reuHphorpF+tZoVHK2MniZ/VJzI7zJQoc9T9Yw== + dependencies: + dequal "^2.0.0" + diff "^5.0.0" + kleur "^4.0.3" + sade "^1.7.3" + v8-compile-cache@^2.0.3, v8-compile-cache@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" @@ -13815,6 +14383,24 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +vfile-message@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.0.tgz#5437035aa43185ff4b9210d32fada6c640e59143" + integrity sha512-4QJbBk+DkPEhBXq3f260xSaWtjE4gPKOfulzfMFF8ZNwaPZieWsg3iVlcmF04+eebzpcpeXOOFMfrYzJHVYg+g== + dependencies: + "@types/unist" "^2.0.0" + unist-util-stringify-position "^3.0.0" + +vfile@^5.0.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-5.3.0.tgz#4990c78cb3157005590ee8c930b71cd7fa6a006e" + integrity sha512-Tj44nY/48OQvarrE4FAjUfrv7GZOYzPbl5OD65HxVKwLJKMPU7zmfV8cCgCnzKWnSfYG2f3pxu+ALqs7j22xQQ== + dependencies: + "@types/unist" "^2.0.0" + is-buffer "^2.0.0" + unist-util-stringify-position "^3.0.0" + vfile-message "^3.0.0" + vhost@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/vhost/-/vhost-3.0.2.tgz#2fb1decd4c466aa88b0f9341af33dc1aff2478d5"