diff --git a/.vscode/launch.json b/.vscode/launch.json index ed00831ae..3c54efee9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,16 +5,16 @@ "type": "node", "request": "launch", "name": "App", - "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", "windows": { - "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd", }, "cwd": "${workspaceFolder}/electron-app", "args": [ ".", "--log-level=debug", "--hostname=localhost", - "--app-project-path=${workspaceRoot}/electron-app", + "--app-project-path=${workspaceFolder}/electron-app", "--remote-debugging-port=9222", "--no-app-auto-install", "--plugins=local-dir:./plugins", @@ -26,11 +26,11 @@ }, "sourceMaps": true, "outFiles": [ - "${workspaceRoot}/electron-app/src-gen/backend/*.js", - "${workspaceRoot}/electron-app/src-gen/frontend/*.js", - "${workspaceRoot}/electron-app/lib/**/*.js", - "${workspaceRoot}/arduino-ide-extension/lib/**/*.js", - "${workspaceRoot}/node_modules/@theia/**/*.js" + "${workspaceFolder}/electron-app/lib/backend/electron-main.js", + "${workspaceFolder}/electron-app/lib/backend/main.js", + "${workspaceFolder}/electron-app/lib/**/*.js", + "${workspaceFolder}/arduino-ide-extension/lib/**/*.js", + "${workspaceFolder}/node_modules/@theia/**/*.js" ], "smartStep": true, "internalConsoleOptions": "openOnSessionStart", @@ -40,16 +40,16 @@ "type": "node", "request": "launch", "name": "App [Dev]", - "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", "windows": { - "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd", }, "cwd": "${workspaceFolder}/electron-app", "args": [ ".", "--log-level=debug", "--hostname=localhost", - "--app-project-path=${workspaceRoot}/electron-app", + "--app-project-path=${workspaceFolder}/electron-app", "--remote-debugging-port=9222", "--no-app-auto-install", "--plugins=local-dir:./plugins", @@ -63,11 +63,11 @@ }, "sourceMaps": true, "outFiles": [ - "${workspaceRoot}/electron-app/src-gen/backend/*.js", - "${workspaceRoot}/electron-app/src-gen/frontend/*.js", - "${workspaceRoot}/electron-app/lib/**/*.js", - "${workspaceRoot}/arduino-ide-extension/lib/**/*.js", - "${workspaceRoot}/node_modules/@theia/**/*.js" + "${workspaceFolder}/electron-app/lib/backend/electron-main.js", + "${workspaceFolder}/electron-app/lib/backend/main.js", + "${workspaceFolder}/electron-app/lib/**/*.js", + "${workspaceFolder}/arduino-ide-extension/lib/**/*.js", + "${workspaceFolder}/node_modules/@theia/**/*.js" ], "smartStep": true, "internalConsoleOptions": "openOnSessionStart", @@ -84,7 +84,7 @@ "type": "node", "request": "launch", "name": "Run Test [current]", - "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "args": [ "--require", "reflect-metadata/Reflect", @@ -95,7 +95,7 @@ "**/${fileBasenameNoExtension}.js" ], "env": { - "TS_NODE_PROJECT": "${workspaceRoot}/tsconfig.json", + "TS_NODE_PROJECT": "${workspaceFolder}/tsconfig.json", "IDE2_TEST": "true" }, "sourceMaps": true, @@ -109,19 +109,12 @@ "name": "Attach by Process ID", "processId": "${command:PickProcess}" }, - { - "type": "node", - "request": "launch", - "name": "Electron Packager", - "program": "${workspaceRoot}/electron/packager/index.js", - "cwd": "${workspaceFolder}/electron/packager" - } ], "compounds": [ { "name": "Launch Electron Backend & Frontend", "configurations": [ - "App (Electron)", + "App", "Attach to Electron Frontend" ] } diff --git a/arduino-ide-extension/src/browser/app-service.ts b/arduino-ide-extension/src/browser/app-service.ts index 20557f412..4598b9fd5 100644 --- a/arduino-ide-extension/src/browser/app-service.ts +++ b/arduino-ide-extension/src/browser/app-service.ts @@ -1,11 +1,14 @@ import type { Disposable } from '@theia/core/lib/common/disposable'; +import type { AppInfo } from '../electron-common/electron-arduino'; import type { StartupTasks } from '../electron-common/startup-task'; import type { Sketch } from './contributions/contribution'; +export type { AppInfo }; + export const AppService = Symbol('AppService'); export interface AppService { quit(): void; - version(): Promise; + info(): Promise; registerStartupTasksHandler( handler: (tasks: StartupTasks) => void ): Disposable; diff --git a/arduino-ide-extension/src/browser/contributions/about.ts b/arduino-ide-extension/src/browser/contributions/about.ts index 03eae0cbf..201d13b41 100644 --- a/arduino-ide-extension/src/browser/contributions/about.ts +++ b/arduino-ide-extension/src/browser/contributions/about.ts @@ -4,7 +4,6 @@ import { nls } from '@theia/core/lib/common/nls'; import { isOSX, isWindows } from '@theia/core/lib/common/os'; import { inject, injectable } from '@theia/core/shared/inversify'; import moment from 'moment'; -import { ConfigService } from '../../common/protocol'; import { AppService } from '../app-service'; import { ArduinoMenus } from '../menu/arduino-menus'; import { @@ -18,8 +17,6 @@ import { export class About extends Contribution { @inject(ClipboardService) private readonly clipboardService: ClipboardService; - @inject(ConfigService) - private readonly configService: ConfigService; @inject(AppService) private readonly appService: AppService; @@ -42,11 +39,9 @@ export class About extends Contribution { } private async showAbout(): Promise { - const [appVersion, cliVersion] = await Promise.all([ - this.appService.version(), - this.configService.getVersion(), - ]); - const buildDate = this.buildDate; + const appInfo = await this.appService.info(); + const { appVersion, cliVersion, buildDate } = appInfo; + const detail = (showAll: boolean) => nls.localize( 'arduino/about/detail', @@ -84,10 +79,6 @@ export class About extends Contribution { return FrontendApplicationConfigProvider.get().applicationName; } - private get buildDate(): string | undefined { - return FrontendApplicationConfigProvider.get().buildDate; - } - private ago(isoTime: string): string { const now = moment(Date.now()); const other = moment(isoTime); diff --git a/arduino-ide-extension/src/common/protocol/config-service.ts b/arduino-ide-extension/src/common/protocol/config-service.ts index 6baaf9dc4..4ec13c0da 100644 --- a/arduino-ide-extension/src/common/protocol/config-service.ts +++ b/arduino-ide-extension/src/common/protocol/config-service.ts @@ -3,7 +3,6 @@ import { RecursivePartial } from '@theia/core/lib/common/types'; export const ConfigServicePath = '/services/config-service'; export const ConfigService = Symbol('ConfigService'); export interface ConfigService { - getVersion(): Promise>; getConfiguration(): Promise; setConfiguration(config: Config): Promise; } diff --git a/arduino-ide-extension/src/electron-browser/electron-app-service.ts b/arduino-ide-extension/src/electron-browser/electron-app-service.ts index 968247566..a564646ca 100644 --- a/arduino-ide-extension/src/electron-browser/electron-app-service.ts +++ b/arduino-ide-extension/src/electron-browser/electron-app-service.ts @@ -1,6 +1,6 @@ import type { Disposable } from '@theia/core/lib/common/disposable'; import { injectable } from '@theia/core/shared/inversify'; -import type { AppService } from '../browser/app-service'; +import type { AppInfo, AppService } from '../browser/app-service'; import type { Sketch } from '../common/protocol/sketches-service'; import type { StartupTasks } from '../electron-common/startup-task'; @@ -10,8 +10,8 @@ export class ElectronAppService implements AppService { window.electronArduino.quitApp(); } - version(): Promise { - return window.electronArduino.appVersion(); + info(): Promise { + return window.electronArduino.appInfo(); } registerStartupTasksHandler( diff --git a/arduino-ide-extension/src/electron-browser/preload.ts b/arduino-ide-extension/src/electron-browser/preload.ts index feb24ba97..b70529423 100644 --- a/arduino-ide-extension/src/electron-browser/preload.ts +++ b/arduino-ide-extension/src/electron-browser/preload.ts @@ -10,7 +10,7 @@ import { import { v4 } from 'uuid'; import type { Sketch } from '../common/protocol/sketches-service'; import { - CHANNEL_APP_VERSION, + CHANNEL_APP_INFO, CHANNEL_IS_FIRST_WINDOW, CHANNEL_MAIN_MENU_ITEM_DID_CLICK, CHANNEL_OPEN_PATH, @@ -76,7 +76,7 @@ const api: ElectronArduino = { ipcRenderer.invoke(CHANNEL_SHOW_OPEN_DIALOG, options), showSaveDialog: (options: SaveDialogOptions) => ipcRenderer.invoke(CHANNEL_SHOW_SAVE_DIALOG, options), - appVersion: () => ipcRenderer.invoke(CHANNEL_APP_VERSION), + appInfo: () => ipcRenderer.invoke(CHANNEL_APP_INFO), quitApp: () => ipcRenderer.send(CHANNEL_QUIT_APP), isFirstWindow: () => ipcRenderer.invoke(CHANNEL_IS_FIRST_WINDOW), requestReload: (options: StartupTasks) => diff --git a/arduino-ide-extension/src/electron-common/electron-arduino.ts b/arduino-ide-extension/src/electron-common/electron-arduino.ts index 2d5174981..54f76d3c1 100644 --- a/arduino-ide-extension/src/electron-common/electron-arduino.ts +++ b/arduino-ide-extension/src/electron-common/electron-arduino.ts @@ -11,6 +11,17 @@ import type { InternalMenuDto as TheiaInternalMenuDto, MenuDto, } from '@theia/core/lib/electron-common/electron-api'; + +export const appInfoPropertyLiterals = [ + 'appVersion', + 'cliVersion', + 'buildDate', +] as const; +export type AppInfoProperty = (typeof appInfoPropertyLiterals)[number]; +export type AppInfo = { + readonly [P in AppInfoProperty]: string; +}; + import type { Sketch } from '../common/protocol/sketches-service'; import type { StartupTasks } from './startup-task'; @@ -50,7 +61,7 @@ export interface ElectronArduino { showMessageBox(options: MessageBoxOptions): Promise; showOpenDialog(options: OpenDialogOptions): Promise; showSaveDialog(options: SaveDialogOptions): Promise; - appVersion(): Promise; + appInfo(): Promise; quitApp(): void; isFirstWindow(): Promise; requestReload(tasks: StartupTasks): void; @@ -77,7 +88,7 @@ declare global { export const CHANNEL_SHOW_MESSAGE_BOX = 'Arduino:ShowMessageBox'; export const CHANNEL_SHOW_OPEN_DIALOG = 'Arduino:ShowOpenDialog'; export const CHANNEL_SHOW_SAVE_DIALOG = 'Arduino:ShowSaveDialog'; -export const CHANNEL_APP_VERSION = 'Arduino:AppVersion'; +export const CHANNEL_APP_INFO = 'Arduino:AppInfo'; export const CHANNEL_QUIT_APP = 'Arduino:QuitApp'; export const CHANNEL_IS_FIRST_WINDOW = 'Arduino:IsFirstWindow'; export const CHANNEL_SCHEDULE_DELETION = 'Arduino:ScheduleDeletion'; diff --git a/arduino-ide-extension/src/electron-main/electron-arduino.ts b/arduino-ide-extension/src/electron-main/electron-arduino.ts index 263ad87c0..2697bf0c1 100644 --- a/arduino-ide-extension/src/electron-main/electron-arduino.ts +++ b/arduino-ide-extension/src/electron-main/electron-arduino.ts @@ -18,7 +18,8 @@ import { createDisposableListener } from '@theia/core/lib/electron-main/event-ut import { injectable } from '@theia/core/shared/inversify'; import { WebContents } from '@theia/electron/shared/electron'; import { - CHANNEL_APP_VERSION, + AppInfo, + CHANNEL_APP_INFO, CHANNEL_IS_FIRST_WINDOW, CHANNEL_MAIN_MENU_ITEM_DID_CLICK, CHANNEL_OPEN_PATH, @@ -85,8 +86,8 @@ export class ElectronArduino implements ElectronMainApplicationContribution { return result; } ); - ipcMain.handle(CHANNEL_APP_VERSION, async () => { - return app.appVersion; + ipcMain.handle(CHANNEL_APP_INFO, async (): Promise => { + return app.appInfo; }); ipcMain.on(CHANNEL_QUIT_APP, () => app.requestStop()); ipcMain.handle(CHANNEL_IS_FIRST_WINDOW, async (event) => { diff --git a/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts b/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts index 6f9a393d7..76ee458be 100644 --- a/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts +++ b/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts @@ -10,7 +10,7 @@ import { fork } from 'node:child_process'; import { AddressInfo } from 'node:net'; import { join, isAbsolute, resolve } from 'node:path'; import { promises as fs, rm, rmSync } from 'node:fs'; -import { MaybePromise } from '@theia/core/lib/common/types'; +import type { MaybePromise, Mutable } from '@theia/core/lib/common/types'; import { ElectronSecurityToken } from '@theia/core/lib/electron-common/electron-token'; import { FrontendApplicationConfig } from '@theia/application-package/lib/application-props'; import { @@ -31,6 +31,8 @@ import { } from '@theia/core/lib/common/disposable'; import { Sketch } from '../../common/protocol'; import { + AppInfo, + appInfoPropertyLiterals, CHANNEL_PLOTTER_WINDOW_DID_CLOSE, CHANNEL_SCHEDULE_DELETION, CHANNEL_SHOW_PLOTTER_WINDOW, @@ -72,6 +74,11 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { private readonly isTempSketch: IsTempSketch; private startup = false; private _firstWindowId: number | undefined; + private _appInfo: AppInfo = { + appVersion: '', + cliVersion: '', + buildDate: '', + }; private openFilePromise = new Deferred(); /** * It contains all things the IDE2 must clean up before a normal stop. @@ -111,7 +118,8 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { const cwd = process.cwd(); this.attachFileAssociations(cwd); this.useNativeWindowFrame = this.getTitleBarStyle(config) === 'native'; - this._config = config; + this._config = await updateFrontendApplicationConfigFromPackageJson(config); + this._appInfo = updateAppInfo(this._appInfo, this._config); this.hookApplicationEvents(); const [port] = await Promise.all([this.startBackend(), app.whenReady()]); this.startContentTracing(); @@ -615,8 +623,8 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { return this._firstWindowId; } - get appVersion(): string { - return app.getVersion(); + get appInfo(): AppInfo { + return this._appInfo; } private async delete(sketch: Sketch): Promise { @@ -681,3 +689,84 @@ class InterruptWorkspaceRestoreError extends Error { Object.setPrototypeOf(this, InterruptWorkspaceRestoreError.prototype); } } + +// This is a workaround for a limitation with the Theia CLI and `electron-builder`. +// It is possible to run the `electron-builder` with `-c.extraMetadata.foo.bar=36` option. +// On the fly, a `package.json` file will be generated for the final bundled application with the additional `{ "foo": { "bar": 36 } }` metadata. +// The Theia build (via the CLI) requires the extra `foo.bar=36` metadata to be in the `package.json` at build time (before `electron-builder` time). +// See the generated `./electron-app/src-gen/backend/electron-main.js` and how this works. +// This method merges in any additional required properties defined in the current! `package.json` of the application. For example, the `buildDate`. +// The current package.json is the package.json of the `electron-app` if running from the source code, +// but it's the `package.json` inside the `resources/app/` folder if it's the final bundled app. +// See https://github.com/arduino/arduino-ide/pull/2144#pullrequestreview-1556343430. +async function updateFrontendApplicationConfigFromPackageJson( + config: FrontendApplicationConfig +): Promise { + try { + const modulePath = __filename; + // must go from `./lib/backend/electron-main.js` to `./package.json` when the app is webpacked. + const packageJsonPath = join(modulePath, '..', '..', '..', 'package.json'); + console.debug( + `Checking for frontend application configuration customizations. Module path: ${modulePath}, destination 'package.json': ${packageJsonPath}` + ); + const rawPackageJson = await fs.readFile(packageJsonPath, { + encoding: 'utf8', + }); + const packageJson = JSON.parse(rawPackageJson); + if (packageJson?.theia?.frontend?.config) { + const packageJsonConfig: Record = + packageJson?.theia?.frontend?.config; + for (const property of appInfoPropertyLiterals) { + const value = packageJsonConfig[property]; + if (value && !config[property]) { + if (!config[property]) { + console.debug( + `Setting 'theia.frontend.config.${property}' application configuration value to: ${JSON.stringify( + value + )} (type of ${typeof value})` + ); + } else { + console.warn( + `Overriding 'theia.frontend.config.${property}' application configuration value with: ${JSON.stringify( + value + )} (type of ${typeof value}). Original value: ${JSON.stringify( + config[property] + )}` + ); + } + config[property] = value; + } + } + console.debug( + `Frontend application configuration after modifications: ${JSON.stringify( + config + )}` + ); + return config; + } + } catch (err) { + console.error( + `Could not read the frontend application configuration from the 'package.json' file. Falling back to (the Theia CLI) generated default config: ${JSON.stringify( + config + )}`, + err + ); + } + return config; +} + +/** + * Mutates the `toUpdate` argument and returns with it. + */ +function updateAppInfo( + toUpdate: Mutable, + updateWith: Record +): AppInfo { + appInfoPropertyLiterals.forEach((property) => { + const newValue = updateWith[property]; + if (typeof newValue === 'string') { + toUpdate[property] = newValue; + } + }); + return toUpdate; +} diff --git a/arduino-ide-extension/src/node/config-service-impl.ts b/arduino-ide-extension/src/node/config-service-impl.ts index cb7ed7138..1f56748cc 100644 --- a/arduino-ide-extension/src/node/config-service-impl.ts +++ b/arduino-ide-extension/src/node/config-service-impl.ts @@ -131,10 +131,6 @@ export class ConfigServiceImpl return this.configChangeEmitter.event; } - async getVersion(): Promise { - return require('../../package.json').arduino?.cli?.version || ''; - } - private async initConfig(): Promise { this.logger.info('>>> Initializing CLI configuration...'); try { diff --git a/electron-app/package.json b/electron-app/package.json index 3bd615789..931a05f22 100644 --- a/electron-app/package.json +++ b/electron-app/package.json @@ -44,6 +44,7 @@ "prepare": "theia download:plugins", "prebuild": "rimraf lib", "build": "theia build", + "prebuild:dev": "yarn prebuild", "build:dev": "theia build --mode development", "test": "mocha \"./test/**/*.test.js\"", "start": "theia start --plugins=local-dir:../plugins", diff --git a/electron-app/scripts/package.js b/electron-app/scripts/package.js index 048465bbc..cf839895f 100644 --- a/electron-app/scripts/package.js +++ b/electron-app/scripts/package.js @@ -10,6 +10,9 @@ async function run() { require('../package.json').devDependencies['electron']; const platform = electronPlatform(); const version = await getVersion(); + /** @type {string|unknown} */ + const cliVersion = require('../../arduino-ide-extension/package.json') + .arduino['arduino-cli'].version; const artifactName = await getArtifactName(version); const args = [ '--publish', @@ -22,6 +25,10 @@ async function run() { 'arduino-ide', // overrides the `name` in the `package.json` to keep the `localStorage` location. (https://github.com/arduino/arduino-ide/pull/2144#pullrequestreview-1554005028) `-c.${platform}.artifactName`, artifactName, + '-c.extraMetadata.theia.frontend.config.appVersion', + version, + '-c.extraMetadata.theia.frontend.config.cliVersion', + typeof cliVersion === 'string' ? cliVersion : '', '-c.extraMetadata.theia.frontend.config.buildDate', new Date().toISOString(), ];