From d1f68b85b21a034da67adebaedc0e82b11e22608 Mon Sep 17 00:00:00 2001 From: Quentin Goinaud Date: Sun, 6 Oct 2024 08:57:27 +0200 Subject: [PATCH] imrovements --- .changeset/curly-comics-begin.md | 7 + .changeset/serious-rice-act.md | 4 +- assets/electron/template/app/package.json | 3 +- assets/electron/template/app/pnpm-lock.yaml | 9 + assets/electron/template/app/src/index.js | 29 +- components.d.ts | 2 + index.html | 6 - src/constants.ts | 12 + src/main/api.ts | 5 + src/renderer/components/ScenarioListItem.vue | 2 +- src/renderer/pages/editor.vue | 165 ++++++--- .../pages/project-settings-editor.vue | 52 +++ src/renderer/pages/variables-editor.vue | 100 +++++- src/renderer/utils/code-editor.ts | 24 +- src/shared/libs/core-app/variables.ts | 34 +- src/shared/libs/plugin-electron/forge.ts | 84 +++-- src/shared/libs/plugin-electron/index.ts | 20 +- src/shared/libs/plugin-electron/preview.ts | 20 ++ src/shared/libs/plugin-system/index.ts | 5 + src/shared/libs/plugin-system/prompt.ts | 53 +++ src/shared/libs/plugin-tauri/configure.ts | 263 ++++++++++++++ .../libs/plugin-tauri/declarations.d.ts | 1 + .../plugin-tauri/fixtures/build/index.html | 11 + src/shared/libs/plugin-tauri/index.ts | 55 +++ src/shared/libs/plugin-tauri/make.ts | 6 + src/shared/libs/plugin-tauri/model.ts | 23 ++ src/shared/libs/plugin-tauri/package.ts | 6 + .../libs/plugin-tauri/public/electron.webp | Bin 0 -> 1426 bytes src/shared/libs/plugin-tauri/shared.ts | 326 ++++++++++++++++++ tests/e2e/tests/basic.spec.ts | 14 +- 30 files changed, 1174 insertions(+), 167 deletions(-) create mode 100644 .changeset/curly-comics-begin.md create mode 100644 src/renderer/pages/project-settings-editor.vue create mode 100644 src/shared/libs/plugin-electron/preview.ts create mode 100644 src/shared/libs/plugin-system/prompt.ts create mode 100644 src/shared/libs/plugin-tauri/configure.ts create mode 100644 src/shared/libs/plugin-tauri/declarations.d.ts create mode 100644 src/shared/libs/plugin-tauri/fixtures/build/index.html create mode 100644 src/shared/libs/plugin-tauri/index.ts create mode 100644 src/shared/libs/plugin-tauri/make.ts create mode 100644 src/shared/libs/plugin-tauri/model.ts create mode 100644 src/shared/libs/plugin-tauri/package.ts create mode 100644 src/shared/libs/plugin-tauri/public/electron.webp create mode 100644 src/shared/libs/plugin-tauri/shared.ts diff --git a/.changeset/curly-comics-begin.md b/.changeset/curly-comics-begin.md new file mode 100644 index 0000000..ebca6b4 --- /dev/null +++ b/.changeset/curly-comics-begin.md @@ -0,0 +1,7 @@ +--- +'@pipelab/app': patch +--- + +feat(electron): support url preview +feat(app): add dialog prompt +feat(app): improve ui diff --git a/.changeset/serious-rice-act.md b/.changeset/serious-rice-act.md index eac5dd3..06fffc1 100644 --- a/.changeset/serious-rice-act.md +++ b/.changeset/serious-rice-act.md @@ -2,5 +2,5 @@ '@pipelab/app': patch --- -add live logs hooks support -fix steam permission, general improvements & logged out detection +fix(app): add live logs hooks support +fix(plugin-steam): fix steam permission, general improvements & logged out detection diff --git a/assets/electron/template/app/package.json b/assets/electron/template/app/package.json index 58fa1b1..b985eda 100644 --- a/assets/electron/template/app/package.json +++ b/assets/electron/template/app/package.json @@ -13,7 +13,6 @@ "lint": "echo \"No linting configured\"" }, "devDependencies": { - "@pipelab/core": "1.2.3", "@electron-forge/cli": "7.4.0", "@electron-forge/maker-deb": "7.4.0", "@electron-forge/maker-dmg": "^7.4.0", @@ -23,6 +22,7 @@ "@electron-forge/plugin-auto-unpack-natives": "7.4.0", "@electron-forge/plugin-fuses": "7.4.0", "@electron/fuses": "1.8.0", + "@pipelab/core": "1.2.3", "electron": "32.1.2" }, "keywords": [], @@ -37,6 +37,7 @@ "electron-serve": "^2.1.1", "electron-squirrel-startup": "1.0.1", "execa": "^9.4.0", + "mri": "1.2.0", "serve-handler": "^6.1.5", "ws": "^8.18.0" } diff --git a/assets/electron/template/app/pnpm-lock.yaml b/assets/electron/template/app/pnpm-lock.yaml index f04cfcf..72f1e26 100644 --- a/assets/electron/template/app/pnpm-lock.yaml +++ b/assets/electron/template/app/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: execa: specifier: ^9.4.0 version: 9.4.0 + mri: + specifier: 1.2.0 + version: 1.2.0 serve-handler: specifier: ^6.1.5 version: 6.1.5 @@ -1201,6 +1204,10 @@ packages: engines: {node: '>=10'} hasBin: true + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -3404,6 +3411,8 @@ snapshots: mkdirp@1.0.4: {} + mri@1.2.0: {} + ms@2.0.0: {} ms@2.1.2: {} diff --git a/assets/electron/template/app/src/index.js b/assets/electron/template/app/src/index.js index a2a683b..44614f5 100644 --- a/assets/electron/template/app/src/index.js +++ b/assets/electron/template/app/src/index.js @@ -6,6 +6,7 @@ import serve from 'serve-handler' import { createServer } from 'http' import { WebSocketServer } from 'ws'; import './custom-main.js' +import mri from 'mri'; // user import userFolder from './handlers/user/folder.js' @@ -52,12 +53,22 @@ function assertUnreachable(x) { throw new Error("Didn't expect to get here"); } -<% if (config.enableInProcessGPU) { %> - app.commandLine.appendSwitch('in-process-gpu') - <% } %> +console.log('process.argv', process.argv) +const argv = process.argv +const cliArgs = mri(argv, { + alias: { + u: 'url' + }, +}); + + //region commandLine Flags + <% if (config.enableInProcessGPU) { %> + app.commandLine.appendSwitch('in-process-gpu') + <% } %> <% if (config.enableDisableRendererBackgrounding) { %> app.commandLine.appendSwitch('disable-renderer-backgrounding') <% } %> +//endregion /** * @param {BrowserWindow} mainWindow @@ -197,11 +208,17 @@ const createWindow = async () => { } }) - const port = await createAppServer(mainWindow) + const argUrl = cliArgs.url + if (argUrl) { + console.log('argUrl', argUrl) + await mainWindow.loadURL(argUrl) + } else { + const port = await createAppServer(mainWindow) - console.log('port', port) + console.log('port', port) - await mainWindow.loadURL(`http://localhost:${port}`) + await mainWindow.loadURL(`http://localhost:${port}`) + } } const registerHandlers = async () => { diff --git a/components.d.ts b/components.d.ts index 5fd735f..c77dd7e 100644 --- a/components.d.ts +++ b/components.d.ts @@ -7,12 +7,14 @@ export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { + Avatar: typeof import('primevue/avatar')['default'] Button: typeof import('primevue/button')['default'] Checkbox: typeof import('primevue/checkbox')['default'] Chip: typeof import('primevue/chip')['default'] Dialog: typeof import('primevue/dialog')['default'] Divider: typeof import('primevue/divider')['default'] Drawer: typeof import('primevue/drawer')['default'] + Fieldset: typeof import('primevue/fieldset')['default'] IconField: typeof import('primevue/iconfield')['default'] Inplace: typeof import('primevue/inplace')['default'] InputIcon: typeof import('primevue/inputicon')['default'] diff --git a/index.html b/index.html index 079211d..abfb193 100644 --- a/index.html +++ b/index.html @@ -26,12 +26,6 @@ href="https://fonts.gstatic.com" crossorigin > - - - diff --git a/src/renderer/pages/variables-editor.vue b/src/renderer/pages/variables-editor.vue index 2961168..bf90193 100644 --- a/src/renderer/pages/variables-editor.vue +++ b/src/renderer/pages/variables-editor.vue @@ -3,43 +3,90 @@ - + + + +
+ + +
+
+ + +
+
+ + + +
+
+
+ + +
+
diff --git a/src/renderer/utils/code-editor.ts b/src/renderer/utils/code-editor.ts index 9600b61..ee72abf 100644 --- a/src/renderer/utils/code-editor.ts +++ b/src/renderer/utils/code-editor.ts @@ -1,6 +1,6 @@ import { readonly, Ref, ref, shallowRef, watch } from 'vue' import { EditorView, keymap, lineNumbers, ViewUpdate } from '@codemirror/view' -import { history, historyKeymap, indentWithTab } from '@codemirror/commands' +import { history, historyKeymap, indentWithTab, defaultKeymap } from '@codemirror/commands' import type { Extension } from '@codemirror/state' import { autocompletion } from '@codemirror/autocomplete' import { javascript } from '@codemirror/lang-javascript' @@ -16,31 +16,28 @@ export const createCodeEditor = ( const resolveValue = (raw: string): string => { // Parse the raw string to extract step, output, and field - const match = raw.match(/steps\['([\w-]+)'\]\['outputs'\]\['([\w-]+)'\]/); - if (!match) return `Invalid: ${raw}`; + const match = raw.match(/steps\['([\w-]+)'\]\['outputs'\]\['([\w-]+)'\]/) + if (!match) return `Invalid: ${raw}` - const [, step, field] = match; + const [, step, field] = match // Simulate fetching or computing the value dynamically // In a real scenario, this might involve API calls, computation, or accessing a state management store const simulateDynamicResolution = (step: string, field: string): string => { // This is a placeholder. Replace with your actual dynamic resolution logic - const timestamp = new Date().toISOString(); - return `${step}.${field} @ ${timestamp}`; - }; + const timestamp = new Date().toISOString() + return `${step}.${field} @ ${timestamp}` + } - return simulateDynamicResolution(step, field); - }; + return simulateDynamicResolution(step, field) + } const createBaseEditor = () => { return new EditorView({ doc: '', extensions: [ lineNumbers(), - keymap.of([ - indentWithTab, - ...historyKeymap - ]), + keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap]), javascript(), autocompletion(), history(), @@ -75,6 +72,7 @@ export const createCodeEditor = ( const internalValue = ref(editor.value.state.doc.toString()) const update = (value: string) => { + console.log('value', value) if (editor.value.state.doc.toString() === value) { return } diff --git a/src/shared/libs/core-app/variables.ts b/src/shared/libs/core-app/variables.ts index 13fa2b3..06b7366 100644 --- a/src/shared/libs/core-app/variables.ts +++ b/src/shared/libs/core-app/variables.ts @@ -1,32 +1,10 @@ -type StringTypeToType = - T extends 'string' ? string - : T extends 'boolean' ? boolean - : T extends 'array' ? unknown[] - : never - export interface VariableBase { - value: unknown; - id: string; - name: string; - description: string; -} - -export interface VariableString extends VariableBase { - type: "string"; - value: string; -} - -export interface VariableBoolean extends VariableBase { - type: "boolean"; - value: boolean; -} - -export interface VariableArray extends VariableBase { - type: "array"; - of: Variable["type"]; - value: Array>; + value: string + id: string + name: string + description: string } -export type Variable = VariableString | VariableBoolean | VariableArray; +export type Variable = VariableBase -export const foo = 'aaa' \ No newline at end of file +export const foo = 'aaa' diff --git a/src/shared/libs/plugin-electron/forge.ts b/src/shared/libs/plugin-electron/forge.ts index c865ec8..4a33438 100644 --- a/src/shared/libs/plugin-electron/forge.ts +++ b/src/shared/libs/plugin-electron/forge.ts @@ -1,4 +1,4 @@ -import { outFolderName } from 'src/constants' +import { getBinName, outFolderName } from 'src/constants' import { ActionRunnerData, createAction, InputsDefinition, runWithLiveLogs } from '../plugin-core' import type { MakeOptions } from '@electron-forge/core' import { ElectronConfiguration } from './model' @@ -10,6 +10,33 @@ import { merge } from 'ts-deepmerge' export const IDMake = 'electron:make' export const IDPackage = 'electron:package' +export const IDPreview = 'electron:preview' + +const paramsInputFolder = { + 'input-folder': { + value: '', + label: 'Folder to package', + control: { + type: 'path', + options: { + properties: ['openDirectory'] + } + } + } +} satisfies InputsDefinition + +const paramsInputURL = { + 'input-url': { + value: '', + label: 'URL to preview', + control: { + type: 'input', + options: { + kind: 'text' + } + } + } +} satisfies InputsDefinition const params = { arch: { @@ -80,16 +107,6 @@ const params = { control: { type: 'json' } - }, - 'input-folder': { - value: '', - label: 'Folder to package', - control: { - type: 'path', - options: { - properties: ['openDirectory'] - } - } } } satisfies InputsDefinition @@ -100,7 +117,8 @@ export const createProps = ( name: string, description: string, icon: string, - displayString: string + displayString: string, + inputType: 'folder' | 'url' ) => createAction({ id, @@ -109,7 +127,10 @@ export const createProps = ( icon, displayString, meta: {}, - params, + params: { + ...params, + ...(inputType === 'folder' ? paramsInputFolder : paramsInputURL) + }, outputs: { output: { label: 'Output', @@ -127,7 +148,7 @@ export const createProps = ( export const forge = async ( action: 'make' | 'package', { cwd, log, inputs, setOutput, paths }: ActionRunnerData> -) => { +): Promise<{ folder: string, binary: string | undefined } | undefined> => { log('Building electron') const { assets, unpack } = paths @@ -153,7 +174,9 @@ export const forge = async ( 'dist', 'electron-forge.js' ) - const appFolder = inputs['input-folder'] + + const isUrl = 'input-url' in inputs + const isAppFolder = 'input-folder' in inputs const templateFolder = join(assets, 'electron', 'template', 'app') @@ -169,10 +192,15 @@ export const forge = async ( const placeAppFolder = join(destinationFolder, 'src', 'app') - // copy app to template - await cp(appFolder, placeAppFolder, { - recursive: true - }) + // if input is folder, copy folder to destination + if (isAppFolder) { + const appFolder = inputs['input-folder'] + + // copy app to template + await cp(appFolder, placeAppFolder, { + recursive: true + }) + } const completeConfiguration = merge( { @@ -205,7 +233,7 @@ export const forge = async ( ejs.renderFile( join(templateFolder, 'forge.config.cjs'), { - config: completeConfiguration + config: completeConfiguration, }, {}, (err: Error, str: string) => { @@ -307,10 +335,21 @@ export const forge = async ( finalPlatform as NodeJS.Platform, finalArch as NodeJS.Architecture ) + const binName = getBinName(completeConfiguration.name) - setOutput('output', join(destinationFolder, 'out', outName)) + const output = join(destinationFolder, 'out', outName) + setOutput('output', output) + return { + folder: output, + binary: join(output, binName) + } } else { - setOutput('output', join(destinationFolder, 'out', 'make')) + const output = join(destinationFolder, 'out', 'make') + setOutput('output', output) + return { + folder: output, + binary: undefined + } } } catch (e) { if (e instanceof Error) { @@ -322,5 +361,6 @@ export const forge = async ( } } log(e) + return undefined } } diff --git a/src/shared/libs/plugin-electron/index.ts b/src/shared/libs/plugin-electron/index.ts index 828e1fc..4be3d20 100644 --- a/src/shared/libs/plugin-electron/index.ts +++ b/src/shared/libs/plugin-electron/index.ts @@ -1,9 +1,10 @@ import { makeRunner } from './make.js' import { packageRunner } from './package.js' +import { previewRunner } from './preview.js' import { createNodeDefinition } from '@pipelab/plugin-core' import icon from './public/electron.webp' -import { createProps, IDMake, IDPackage } from './forge.js' +import { createProps, IDMake, IDPackage, IDPreview } from './forge.js' import { configureRunner, props } from './configure.js' export default createNodeDefinition({ @@ -22,7 +23,8 @@ export default createNodeDefinition({ 'Create Installer', 'Create a distributable installer for your chosen platform', '', - "`Build package for ${fmt.param(params['input-folder'], 'primary')}`" + "`Build package for ${fmt.param(params['input-folder'], 'primary')}`", + 'folder' ), runner: makeRunner // disabled: platform === 'linux' ? 'Electron is not supported on Linux' : undefined @@ -34,10 +36,22 @@ export default createNodeDefinition({ 'Prepare App Bundle', 'Gather all necessary files and prepare your app for distribution, creating a platform-specific bundle.', '', - "`Package app from ${fmt.param(params['input-folder'], 'primary')}`" + "`Package app from ${fmt.param(params['input-folder'], 'primary')}`", + 'folder' ), runner: packageRunner }, + { + node: createProps( + IDPreview, + 'Preview app', + 'Package and preview your app from an URL', + '', + "`Preview app from ${fmt.param(params['input-url'], 'primary')}`", + 'url' + ), + runner: previewRunner + }, { node: props, runner: configureRunner diff --git a/src/shared/libs/plugin-electron/preview.ts b/src/shared/libs/plugin-electron/preview.ts new file mode 100644 index 0000000..1b7cc2d --- /dev/null +++ b/src/shared/libs/plugin-electron/preview.ts @@ -0,0 +1,20 @@ +import { createActionRunner, runWithLiveLogs } from '@pipelab/plugin-core' +import { createProps, forge } from './forge' + +export const previewRunner = createActionRunner>(async (options) => { + const isUrl = 'input-url' in options.inputs + if (!isUrl) { + return; + } + + const url = options.inputs['input-url'] + if (url === '') { + throw new Error("URL can't be empty") + } + + const output = await forge('package', options) + options.log('Opening preview', output) + options.log('Opening url', url) + await runWithLiveLogs(output.binary, ['--url', url], {}, options.log) + return +}) diff --git a/src/shared/libs/plugin-system/index.ts b/src/shared/libs/plugin-system/index.ts index bc5a3f1..e35381b 100644 --- a/src/shared/libs/plugin-system/index.ts +++ b/src/shared/libs/plugin-system/index.ts @@ -4,6 +4,7 @@ import { logAction, logActionRunner } from './log.js' // import { forLoop, ForLoopRunner } from './for.js' import { manualEvent, manualEvaluator } from './manual.js' import { alertAction, alertActionRunner } from './alert.js' +import { promptAction, promptActionRunner } from './prompt.js' import { sleepAction, sleepActionRunner } from './sleep.js' export default createNodeDefinition({ @@ -35,6 +36,10 @@ export default createNodeDefinition({ node: alertAction, runner: alertActionRunner }, + { + node: promptAction, + runner: promptActionRunner + }, { node: sleepAction, runner: sleepActionRunner diff --git a/src/shared/libs/plugin-system/prompt.ts b/src/shared/libs/plugin-system/prompt.ts new file mode 100644 index 0000000..16b9f09 --- /dev/null +++ b/src/shared/libs/plugin-system/prompt.ts @@ -0,0 +1,53 @@ +import { createAction, createActionRunner } from '@pipelab/plugin-core' + +export const ID = 'system:prompt' + +export type Data = { + text: string +} + +export const promptAction = createAction({ + id: ID, + name: 'Prompt', + description: 'Prompt a message', + icon: '', + displayString: "`Prompt ${fmt.param(params.message ?? 'No message')}`", + meta: {}, + params: { + message: { + value: '', + label: 'Message', + control: { + type: 'input', + options: { + kind: 'text' + } + } + } + }, + + outputs: { + answer: { + label: 'Answer', + value: '' + } + } +}) + +export const promptActionRunner = createActionRunner( + async ({ log, inputs, api, setOutput, browserWindow }) => { + browserWindow.flashFrame(true) + // 'cancel' | 'ok' + const _answer = await api.execute('dialog:prompt', { + message: inputs.message + }) + + console.log('_answer', _answer) + + if ('answer' in _answer) { + setOutput('answer', _answer.answer) + } else { + log('error') + } + } +) diff --git a/src/shared/libs/plugin-tauri/configure.ts b/src/shared/libs/plugin-tauri/configure.ts new file mode 100644 index 0000000..7ff60bf --- /dev/null +++ b/src/shared/libs/plugin-tauri/configure.ts @@ -0,0 +1,263 @@ +import { createAction, createActionRunner } from '@pipelab/plugin-core' +import { ElectronConfiguration } from './model' + +export const props = createAction({ + id: 'electron:configure', + description: 'Configure electron', + displayString: "'Configure Electron'", + icon: '', + meta: {}, + name: 'Configure Electron', + outputs: { + configuration: { + label: 'Configuration', + value: {} as Partial + } + }, + params: { + name: { + label: 'Application name', + value: 'Pipelab', + description: 'The name of the application', + required: true, + control: { + type: 'input', + options: { + kind: 'text' + } + } + }, + appBundleId: { + label: 'Application bundle ID', + value: 'com.pipelab.app', + description: 'The bundle ID of the application', + required: true, + control: { + type: 'input', + options: { + kind: 'text' + } + } + }, + appCopyright: { + label: 'Application copyright', + value: 'Copyright © 2024 Pipelab', + description: 'The copyright of the application', + required: false, + control: { + type: 'input', + options: { + kind: 'text' + } + } + }, + appVersion: { + label: 'Application version', + value: '1.0.0', + description: 'The version of the application', + required: true, + control: { + type: 'input', + options: { + kind: 'text' + } + } + }, + icon: { + label: 'Application icon', + value: '', + description: 'The icon of the application', + required: false, + control: { + type: 'path', + options: { + filters: [ + { name: 'Image', extensions: ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'ico', 'icns'] } + ] + }, + label: 'Path to an image file' + } + }, + author: { + label: 'Application author', + value: 'Pipelab', + description: 'The author of the application', + required: true, + control: { + type: 'input', + options: { + kind: 'text' + } + } + }, + description: { + label: 'Application description', + value: 'A simple Electron application', + description: 'The description of the application', + required: false, + control: { + type: 'input', + options: { + kind: 'text' + } + } + }, + + appCategoryType: { + platforms: ['darwin'], + label: 'Application category type', + value: 'public.app-category.developer-tools', + description: 'The category type of the application', + required: false, + control: { + type: 'input', + options: { + kind: 'text' + } + } + }, + + // window + width: { + label: 'Window width', + value: 800, + description: 'The width of the window', + required: false, + control: { + type: 'input', + options: { + kind: 'number' + } + } + }, + height: { + label: 'Window height', + value: 600, + description: 'The height of the window', + required: false, + control: { + type: 'input', + options: { + kind: 'number' + } + } + }, + fullscreen: { + label: 'Fullscreen', + value: false, + description: 'Whether to start the application in fullscreen mode', + required: false, + control: { + type: 'boolean' + } + }, + frame: { + label: 'Frame', + value: true, + description: 'Whether to show the window frame', + required: false, + control: { + type: 'boolean' + } + }, + transparent: { + label: 'Transparent', + value: false, + description: 'Whether to make the window transparent', + required: false, + control: { + type: 'boolean' + } + }, + toolbar: { + label: 'Toolbar', + value: true, + description: 'Whether to show the toolbar', + required: false, + control: { + type: 'boolean' + } + }, + alwaysOnTop: { + label: 'Always on top', + value: false, + description: 'Whether to always keep the window on top', + required: false, + control: { + type: 'boolean' + } + }, + + electronVersion: { + value: '', + label: 'Electron version', + description: 'The version of Electron to use', + required: false, + control: { + type: 'input', + options: { + kind: 'text' + } + } + }, + customMainCode: { + required: false, + label: 'Custom main code', + value: '', + control: { + type: 'path', + options: { + filters: [{ name: 'JavaScript', extensions: ['js'] }] + }, + label: 'Path to a file containing custom main code' + } + }, + + // Flags + + enableInProcessGPU: { + required: false, + label: 'Enable in-process GPU', + value: false, + control: { + type: 'boolean' + } + }, + enableDisableRendererBackgrounding: { + required: false, + label: 'Disable renderer backgrounding', + value: false, + control: { + type: 'boolean' + } + }, + + // websocket api + websocketApi: { + required: false, + label: 'WebSocket APIs to allow (empty = all)', + value: [], + control: { + type: 'array', + options: { + kind: 'text' + } + } + }, + + // integrations + + // enableSteamSupport: { + // required: false, + // label: 'Enable steam support', + // value: false, + // control: { + // type: 'boolean' + // } + // } + } +}) + +export const configureRunner = createActionRunner(async ({ setOutput, inputs }) => { + setOutput('configuration', inputs) +}) diff --git a/src/shared/libs/plugin-tauri/declarations.d.ts b/src/shared/libs/plugin-tauri/declarations.d.ts new file mode 100644 index 0000000..5ab250f --- /dev/null +++ b/src/shared/libs/plugin-tauri/declarations.d.ts @@ -0,0 +1 @@ +declare module '*.webp' diff --git a/src/shared/libs/plugin-tauri/fixtures/build/index.html b/src/shared/libs/plugin-tauri/fixtures/build/index.html new file mode 100644 index 0000000..22946b9 --- /dev/null +++ b/src/shared/libs/plugin-tauri/fixtures/build/index.html @@ -0,0 +1,11 @@ + + + + + + Document + + +

Hello

+ + \ No newline at end of file diff --git a/src/shared/libs/plugin-tauri/index.ts b/src/shared/libs/plugin-tauri/index.ts new file mode 100644 index 0000000..a7f79dd --- /dev/null +++ b/src/shared/libs/plugin-tauri/index.ts @@ -0,0 +1,55 @@ +import { makeRunner } from './make.js' +import { packageRunner } from './package.js' + +import { createNodeDefinition } from '@pipelab/plugin-core' +import icon from './public/electron.webp' +import { createProps, IDMake, IDPackage } from './shared.js' +import { configureRunner, props } from './configure.js' + +export default createNodeDefinition({ + description: 'Electron', + name: 'Electron', + id: 'electron', + icon: { + type: 'image', + image: icon + }, + nodes: [ + // make and package + { + node: createProps( + IDMake, + 'Create Installer', + 'Create a distributable installer for your chosen platform', + '', + "`Build package for ${fmt.param(params['input-folder'], 'primary')}`" + ), + runner: makeRunner + // disabled: platform === 'linux' ? 'Electron is not supported on Linux' : undefined + }, + // package + { + node: createProps( + IDPackage, + 'Prepare App Bundle', + 'Gather all necessary files and prepare your app for distribution, creating a platform-specific bundle.', + '', + "`Package app from ${fmt.param(params['input-folder'], 'primary')}`" + ), + runner: packageRunner + }, + { + node: props, + runner: configureRunner + }, + // { + // node: propsConfigureV2, + // runner: configureV2Runner + // } + // make without package + // { + // node: packageApp, + // runner: packageRunner, + // }, + ] +}) diff --git a/src/shared/libs/plugin-tauri/make.ts b/src/shared/libs/plugin-tauri/make.ts new file mode 100644 index 0000000..88c7930 --- /dev/null +++ b/src/shared/libs/plugin-tauri/make.ts @@ -0,0 +1,6 @@ +import { createActionRunner } from '@pipelab/plugin-core' +import { forge, createProps } from './shared' + +export const makeRunner = createActionRunner>(async (options) => { + await forge('make', options) +}) diff --git a/src/shared/libs/plugin-tauri/model.ts b/src/shared/libs/plugin-tauri/model.ts new file mode 100644 index 0000000..180d4e3 --- /dev/null +++ b/src/shared/libs/plugin-tauri/model.ts @@ -0,0 +1,23 @@ +export type ElectronConfiguration = { + name: string + appBundleId: string + appCopyright: string + appVersion: string + electronVersion: string + customMainCode: string + author: string + description: string + width: number + height: number + fullscreen: boolean + frame: boolean + transparent: boolean + toolbar: boolean + alwaysOnTop: boolean + enableInProcessGPU: boolean + enableDisableRendererBackgrounding: boolean + icon: string + appCategoryType: string + + // steamSupport: boolean +} diff --git a/src/shared/libs/plugin-tauri/package.ts b/src/shared/libs/plugin-tauri/package.ts new file mode 100644 index 0000000..db71f10 --- /dev/null +++ b/src/shared/libs/plugin-tauri/package.ts @@ -0,0 +1,6 @@ +import { createActionRunner } from '@pipelab/plugin-core' +import { createProps, forge } from './shared' + +export const packageRunner = createActionRunner>(async (options) => { + await forge('package', options) +}) diff --git a/src/shared/libs/plugin-tauri/public/electron.webp b/src/shared/libs/plugin-tauri/public/electron.webp new file mode 100644 index 0000000000000000000000000000000000000000..e217f2c6e1f5e1da5c8c73c8d4873353f202df4c GIT binary patch literal 1426 zcmV;D1#S9LNk&GB1pok7MM6+kP&il$0000G0000#002J#06|PpNT~q;00EG5ZQCKn z8+ryY*0$NPcV*kQZIo7}$yQ0Vs_fm0oo#Do5u%<(!6hv zuN<4z8misF*&ihK>r?$4Elu`+7Q=6L*U``<$PX*U7@V zd{Oy-rYS#1)E@PWq}>+9XOj%1-4oRpl4ALHMEPkiPL8PG6Jwnaj;~Rle1@of4g9A< zdDp9Mw~$V+rfJ0Tt4>u}Lc6hwCV4zzol5(KcrF0s3!bvw{lWGwpDik7E8)vy3%{|we$0033fr+`Y>Q^G^=-x00I#_o09jmX zfWBPa0p~T-BL~1PrpW+MC*=8=RtblB7Ng2KzgRw|RfSb7BT=n)kK?>s9Xdt~znh|_ zGZ+S;=126cMNIFkwxezwBwf%IBp`m$X-!8HVA9*8XRQmv<02iu2f=*%I~vY6g-v(b zFIyi+y0BblV`$^&k~Ao+J@>h(N0ZZ_)akMHalD6Xx;s$aWZ?4P?z^a@Bz%4A*y4T- zU>pEeP&gpE0{{Rp5&)e6DnI~006v{Wn@T04q9Ldh4fwDVi9i?nHX$C{Z_ksa0ld5a z0|b_9?et|UzzrZ+oll2&hj5D({Z3lhD_v?{mmw-h zk9eQ~dD`X^0092_D1Y{!^tb=?LB$aUN|}mdCK6eN$z;Ftb2G|xKC>t)cSXp}aUf6O zum4qnGvzj7naawO9il~vi1I5m59P!Z)|SNsVl(QI7Pzv*2`YR<`)ggZc{qf>V+OnM zO?o-+ps|#6YlD-SCX1qvw{NQvH_99-pBPnE;;$|I|AJd%7NDSwcI>8M98h+L#u=n< zZYtdPo8Dv(qGw%y!}BiTgp)o8GPObp$J)nmCcvU9;vffkyW;OT;&Ve zfpM=bTcAWr;Z7X5^y)KtQFZdf-u#tsaG(r|xxVOkpVyUiBv88i%vvl?16~O$D1rd6 zz&%zm+n-!PzKcbjAH+qTHNB74gCLE$3zRcE+53@*b!bADQr@ zS|#);jkSXBOiyC%zEby?Y?(Ta@U)u4>xx9@xJv$z>l1CWqSUjBY- z^;2P3xav*RIn8>lrGfRPn~`cS?EPyQ|B8mu2BX?0?D>~T^M3NR3-z@p+|mHoB?wCF z;G7QUKNYZe={i{lIsNtar-6aQ;wkX7>UlOzTVd{2Ej#zZnQ?3zIBuTx0Hi8s4!b1b zgrhr$`ide*nS8j6cc4IY=c+Hqa)p<_=kbigr~VqT&4lYMe#IYLxN7JuIrB^MDIc4` zeCR!65zt#nAPg(Do_TP4v&e~eH5=x6#AK`2e`MambW^(`|JIRf2ILvIv!|06$)N>J zfD#yzoPO?gFDAcDa8Kc{&Ct&GtzkH+!n~+tN&F5}B9hZfzyKKdXPu-6=$uuq;V|2F zpD+o@oZ0lG4FaGDaDJr9)@7#Tjc1bUq>5s)A8loG29jRN>Mcc5yk(!+lMY<@Tq1f+ zuTL!Ve5?L{l+$jEdwx>aR1FfeT4ot4!VFhn@eZ)cV0CuR&0{{R3 literal 0 HcmV?d00001 diff --git a/src/shared/libs/plugin-tauri/shared.ts b/src/shared/libs/plugin-tauri/shared.ts new file mode 100644 index 0000000..c865ec8 --- /dev/null +++ b/src/shared/libs/plugin-tauri/shared.ts @@ -0,0 +1,326 @@ +import { outFolderName } from 'src/constants' +import { ActionRunnerData, createAction, InputsDefinition, runWithLiveLogs } from '../plugin-core' +import type { MakeOptions } from '@electron-forge/core' +import { ElectronConfiguration } from './model' +import { writeFile } from 'node:fs/promises' +import ejs from 'ejs' +import { merge } from 'ts-deepmerge' + +// TODO: https://js.electronforge.io/modules/_electron_forge_core.html + +export const IDMake = 'electron:make' +export const IDPackage = 'electron:package' + +const params = { + arch: { + value: '' as MakeOptions['arch'], + label: 'Architecture', + required: false, + control: { + type: 'select', + options: { + placeholder: 'Architecture', + options: [ + { + label: 'Older PCs (ia32)', + value: 'ia32' + }, + { + label: 'Modern PCs (x64)', + value: 'x64' + }, + { + label: 'Older Mobile/Pi (armv7l)', + value: 'armv7l' + }, + { + label: 'New Mobile/Apple Silicon (arm64)', + value: 'arm64' + }, + { + label: 'Mac Universal (universal)', + value: 'universal' + }, + { + label: 'Special Systems (mips64el)', + value: 'mips64el' + } + ] + } + } + }, + platform: { + value: '' as MakeOptions['platform'], + label: 'Platform', + required: false, + control: { + type: 'select', + options: { + placeholder: 'Platform', + options: [ + { + label: 'Windows (win32)', + value: 'win32' + }, + { + label: 'macOS (darwin)', + value: 'darwin' + }, + { + label: 'Linux (linux)', + value: 'linux' + } + ] + } + } + }, + configuration: { + label: 'Electron configuration', + value: {} as Partial, + control: { + type: 'json' + } + }, + 'input-folder': { + value: '', + label: 'Folder to package', + control: { + type: 'path', + options: { + properties: ['openDirectory'] + } + } + } +} satisfies InputsDefinition + +// type Inputs = ParamsToInput + +export const createProps = ( + id: string, + name: string, + description: string, + icon: string, + displayString: string +) => + createAction({ + id, + name, + description, + icon, + displayString, + meta: {}, + params, + outputs: { + output: { + label: 'Output', + value: '', + control: { + type: 'path', + options: { + properties: ['openDirectory'] + } + } + } + } + }) + +export const forge = async ( + action: 'make' | 'package', + { cwd, log, inputs, setOutput, paths }: ActionRunnerData> +) => { + log('Building electron') + + const { assets, unpack } = paths + + const { join, basename, delimiter } = await import('node:path') + const { cp } = await import('node:fs/promises') + const { arch, platform } = await import('process') + // const { fileURLToPath } = await import('url') + // const __dirname = fileURLToPath(dirname(import.meta.url)) + // const { app } = await import('electron') + + const modulesPath = join(unpack, 'node_modules') + + const pnpm = join(modulesPath, 'pnpm', 'bin', 'pnpm.cjs') + + const destinationFolder = join(cwd, 'build') + + const forge = join( + destinationFolder, + 'node_modules', + '@electron-forge', + 'cli', + 'dist', + 'electron-forge.js' + ) + const appFolder = inputs['input-folder'] + + const templateFolder = join(assets, 'electron', 'template', 'app') + + // copy template to destination + await cp(templateFolder, destinationFolder, { + recursive: true, + filter: (src) => { + // log('src', src) + // log('dest', dest) + return basename(src) !== 'node_modules' + } + }) + + const placeAppFolder = join(destinationFolder, 'src', 'app') + + // copy app to template + await cp(appFolder, placeAppFolder, { + recursive: true + }) + + const completeConfiguration = merge( + { + alwaysOnTop: false, + appBundleId: 'com.pipelab.app', + appCategoryType: '', + appCopyright: 'Copyright © 2024 Pipelab', + appVersion: '1.0.0', + author: 'Pipelab', + customMainCode: '', + description: 'A simple Electron application', + electronVersion: '', + enableDisableRendererBackgrounding: false, + enableInProcessGPU: false, + frame: true, + fullscreen: false, + icon: '', + height: 600, + name: 'Pipelab', + toolbar: true, + transparent: false, + width: 800 + } satisfies ElectronConfiguration, + inputs.configuration + ) + + log('completeConfiguration', completeConfiguration) + + // render forge config + ejs.renderFile( + join(templateFolder, 'forge.config.cjs'), + { + config: completeConfiguration + }, + {}, + (err: Error, str: string) => { + writeFile(join(destinationFolder, 'forge.config.cjs'), str, 'utf8') + } + ) + + // index / main + ejs.renderFile( + join(templateFolder, 'src', 'index.js'), + { + config: completeConfiguration + }, + {}, + (err: Error, str: string) => { + writeFile(join(destinationFolder, 'src', 'index.js'), str, 'utf8') + } + ) + + // preload + ejs.renderFile( + join(templateFolder, 'src', 'preload.js'), + { + config: completeConfiguration + }, + {}, + (err: Error, str: string) => { + writeFile(join(destinationFolder, 'src', 'preload.js'), str, 'utf8') + } + ) + + // copy custom main code + const destinationFile = join(destinationFolder, 'src', 'custom-main.js') + if (completeConfiguration.customMainCode) { + await cp(completeConfiguration.customMainCode, destinationFile) + } else { + await writeFile(destinationFile, 'console.log("No custom main code provided")') + } + + const shimsPaths = join(assets, 'shims') + + log('Installing packages') + await runWithLiveLogs( + process.execPath, + [pnpm, 'install', '--prefer-offline'], + { + cwd: destinationFolder, + env: { + // DEBUG: '*', + ELECTRON_RUN_AS_NODE: '1', + PATH: `${shimsPaths}${delimiter}${process.env.PATH}` + } + }, + log + ) + + // override electron version + if (completeConfiguration.electronVersion && completeConfiguration.electronVersion !== '') { + log(`Installing electron@${completeConfiguration.electronVersion}`) + await runWithLiveLogs( + process.execPath, + [pnpm, 'install', `electron@${completeConfiguration.electronVersion}`, '--prefer-offline'], + { + cwd: destinationFolder, + env: { + // DEBUG: '*', + ELECTRON_RUN_AS_NODE: '1', + PATH: `${shimsPaths}${delimiter}${process.env.PATH}` + } + }, + log + ) + } + + try { + const finalPlatform = inputs.platform ?? platform ?? '' + const finalArch = inputs.arch ?? arch ?? '' + + const logs = await runWithLiveLogs( + process.execPath, + [forge, action, '--', '--arch', finalArch, '--platform', finalPlatform], + { + cwd: destinationFolder, + env: { + // DEBUG: '*', + ELECTRON_NO_ASAR: '1', + ELECTRON_RUN_AS_NODE: '1', + PATH: `${shimsPaths}${delimiter}${process.env.PATH}` + } + }, + log + ) + + log('logs', logs) + + if (action === 'package') { + const outName = outFolderName( + completeConfiguration.name, + finalPlatform as NodeJS.Platform, + finalArch as NodeJS.Architecture + ) + + setOutput('output', join(destinationFolder, 'out', outName)) + } else { + setOutput('output', join(destinationFolder, 'out', 'make')) + } + } catch (e) { + if (e instanceof Error) { + if (e.name === 'RequestError') { + log('Request error') + } + if (e.name === 'RequestError') { + log('Request error') + } + } + log(e) + } +} diff --git a/tests/e2e/tests/basic.spec.ts b/tests/e2e/tests/basic.spec.ts index b3541e8..da59599 100644 --- a/tests/e2e/tests/basic.spec.ts +++ b/tests/e2e/tests/basic.spec.ts @@ -4,24 +4,14 @@ import { join } from 'path' import { tmpdir } from 'os' import { nanoid } from 'nanoid' import { readFile } from 'fs/promises' -import { name, outFolderName } from '../../../src/constants' +import { getBinName, name, outFolderName } from '../../../src/constants' import { platform, arch } from 'process' -const getBinName = () => { - if (platform === 'win32') { - return `${name}.exe` - } - if (platform === 'darwin') { - return `${name}.app/Contents/MacOS/${name}` - } - return name -} - const tmpLogFile = join(tmpdir(), nanoid() + 'pipelab-app-test.log.json') const root = process.cwd() const binFolder = outFolderName('Pipelab', platform, arch) -const binName = getBinName() +const binName = getBinName(name) const bin = join(root, 'out', binFolder, binName) // const bin = '/home/quentin/Projects/pipelab-monorepo/out/@pipelab-app-win32-x64/@pipelab-app.exe'