From 75480bd53f247fb214075339a6a55cf757adaab5 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Tue, 1 Aug 2023 18:33:52 +0200 Subject: [PATCH] fix: extra metadata is unavailable at webpack time merge in the frontend config and any other metadata from the final app's package.json, as those are not available at theia build time. Signed-off-by: Akos Kitta --- .vscode/launch.json | 45 ++++----- .../src/browser/app-service.ts | 5 +- .../src/browser/contributions/about.ts | 15 +-- .../src/common/protocol/config-service.ts | 1 - .../electron-browser/electron-app-service.ts | 6 +- .../src/electron-browser/preload.ts | 4 +- .../src/electron-common/electron-arduino.ts | 15 ++- .../src/electron-main/electron-arduino.ts | 7 +- .../theia/electron-main-application.ts | 97 ++++++++++++++++++- .../src/node/config-service-impl.ts | 4 - electron-app/package.json | 1 + electron-app/scripts/package.js | 7 ++ 12 files changed, 149 insertions(+), 58 deletions(-) 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(), ];