From 4acec88b5a316cea107863c5f456dcbb58477f49 Mon Sep 17 00:00:00 2001 From: bunsenstraat Date: Mon, 20 Nov 2023 08:51:38 +0100 Subject: [PATCH] yarn actions --- .../plugins/electron/scriptRunnerPlugin.ts | 41 ++++ apps/remixdesktop/src/plugins/scriptRunner.ts | 231 +++++++++++++----- 2 files changed, 208 insertions(+), 64 deletions(-) diff --git a/apps/remix-ide/src/app/plugins/electron/scriptRunnerPlugin.ts b/apps/remix-ide/src/app/plugins/electron/scriptRunnerPlugin.ts index 2347922101..fcc206f024 100644 --- a/apps/remix-ide/src/app/plugins/electron/scriptRunnerPlugin.ts +++ b/apps/remix-ide/src/app/plugins/electron/scriptRunnerPlugin.ts @@ -1,5 +1,7 @@ +import { AppModal } from '@remix-ui/app'; import { ElectronPlugin } from '@remixproject/engine-electron'; + export class scriptRunnerPlugin extends ElectronPlugin { constructor(){ super({ @@ -8,4 +10,43 @@ export class scriptRunnerPlugin extends ElectronPlugin { description: 'scriptRunner' }) } + + async onActivation(): Promise { + this.on('scriptRunner', 'missingModule', async (module: string) => { + console.log('missingModule', module) + const addModuleModal: AppModal = { + id: 'AddModuleModal', + title: `Missing module ${module}`, + message: `Do you want to install the missing module? ${module} \n\nYou can also install it manually using the terminal if you have yarn or npm installed:\nyarn add ${module}`, + okLabel: 'Install', + cancelLabel: 'No', + } + const result = await this.call('notification', 'modal', addModuleModal) + if (result) { + await this.addModule(module) + } + }) + } + + async addModule(module: string, version: string = ''): Promise { + await this.checkPackageJson() + await this.call('scriptRunner', 'yarnAdd', module, version) + } + + async checkPackageJson(): Promise { + const exists = await this.call('fileManager', 'exists', 'package.json') + if(!exists){ + const initPackageJsonModal: AppModal = { + id: 'InitPackageJsonModal', + title: `Missing package.json`, + message: `A package.json file is required to install the missing module. A package.json file contains meta data about your app or module. Do you want to create one?`, + okLabel: 'Yes, create a package.json file', + cancelLabel: 'No', + } + const result = await this.call('notification', 'modal', initPackageJsonModal) + if (result) { + await this.call('scriptRunner', 'yarnInit') + } + } + } } \ No newline at end of file diff --git a/apps/remixdesktop/src/plugins/scriptRunner.ts b/apps/remixdesktop/src/plugins/scriptRunner.ts index 2c6358ebd4..f3a615ad3c 100644 --- a/apps/remixdesktop/src/plugins/scriptRunner.ts +++ b/apps/remixdesktop/src/plugins/scriptRunner.ts @@ -1,15 +1,18 @@ -import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron" -import path from "path" +import {ElectronBasePlugin, ElectronBasePluginClient} from '@remixproject/plugin-electron' +import path from 'path' import * as esbuild from 'esbuild' import fs from 'fs/promises' -import os, { arch } from 'os' +import os, {arch} from 'os' +import {exec} from 'child_process' +import {promisify} from 'util' +const execAsync = promisify(exec) export const cacheDir = path.join(os.homedir(), '.cache_remix_ide') const profile = { - "name": "scriptRunner", - "displayName": "Script Runner", - "description": "Execute script and emit logs", + name: 'scriptRunner', + displayName: 'Script Runner', + description: 'Execute script and emit logs', } const convertPathToPosix = (pathName: string): string => { @@ -19,63 +22,155 @@ const convertPathToPosix = (pathName: string): string => { export class ScriptRunnerPlugin extends ElectronBasePlugin { constructor() { super(profile, clientProfile, ScriptRunnerClient) - this.methods = [...super.methods, 'execute'] + this.methods = [...super.methods] } } const clientProfile = { - "name": "scriptRunner", - "displayName": "Script Runner", - "description": "Execute script and emit logs", - "methods": ["execute"] + name: 'scriptRunner', + displayName: 'Script Runner', + description: 'Execute script and emit logs', + methods: ['execute', 'yarnAdd', 'yarnInit'], } class ScriptRunnerClient extends ElectronBasePluginClient { workingDir: string = '' + nodeVersion: string = '' + yarnVersion: string = '' constructor(webContentsId: number, profile: any) { super(webContentsId, profile) - this.onload(() => { + this.onload(async () => { this.on('fs' as any, 'workingDirChanged', async (path: string) => { this.workingDir = path }) + + try { + const result = await execAsync('node --version') + this.nodeVersion = result.stdout + this.call('terminal' as any, 'log', `Node version: ${this.nodeVersion}`) + return result.stdout + } catch (error) { + this.call('terminal' as any, 'log', `Node not found`) + } + + try { + const result = await execAsync('yarn --version') + console.log('result', result) + this.yarnVersion = result.stdout + this.call('terminal' as any, 'log', `Yarn version: ${this.yarnVersion}`) + return result.stdout + } catch (error) { + this.call('terminal' as any, 'log', `Yarn not found`) + } }) } - - async execute(content: string, dir: string): Promise { + async yarnAdd(module: string, version: string = ''): Promise { + const child = utilityProcess + .fork(path.join(__dirname, '/../tools/yarn/bin/', 'yarn.js'), [`--cwd=${this.workingDir}`, 'add', `${module}@${version}`], { + stdio: 'pipe', + }) + .addListener('exit', () => { + console.log('exit') + }) + child && + child.stdout && + child.stdout.on('data', (data) => { + this.call('terminal' as any, 'log', data.toString()) + }) + child && + child.stdout && + child.stdout.on('close', (data) => { + this.call('terminal' as any, 'log', 'close') + }) + child && + child.on('spawn', () => { + this.call('terminal' as any, 'log', 'yarn start') + }) + child && + child.on('exit', (data) => { + this.call('terminal' as any, 'log', 'yarn install done') + }) + } + + async yarnInit(): Promise { + const child = utilityProcess + .fork(path.join(__dirname, '/../tools/yarn/bin/', 'yarn.js'), [`--cwd=${this.workingDir}`], { + stdio: 'pipe', + }) + .addListener('exit', () => { + console.log('exit') + }) + child && + child.stdout && + child.stdout.on('data', (data) => { + this.call('terminal' as any, 'log', data.toString()) + }) + child && + child.stdout && + child.stdout.on('close', (data) => { + this.call('terminal' as any, 'log', 'close') + }) + child && + child.on('spawn', () => { + this.call('terminal' as any, 'log', 'yarn start') + }) + child && + child.on('exit', (data) => { + this.call('terminal' as any, 'log', 'yarn install done') + }) + } + + async execute(content: string, dir: string): Promise { this.call('terminal' as any, 'log', this.workingDir) - const child = utilityProcess.fork(path.join(__dirname,'/../tools/yarn/bin/', 'yarn.js'), [`--cwd=${this.workingDir}`], { - stdio: 'pipe', - }) - child && child.stdout && child.stdout.on('data', (data) => { - this.call('terminal' as any, 'log', data.toString()) - }) - child && child.stdout && child.stdout.on('end', (data) => { - this.call('terminal' as any, 'log', 'end') - }) - - dir = convertPathToPosix(this.fixPath(dir)) - console.log('execute', path) + dir = await convertPathToPosix(this.fixPath(dir)) + const out = convertPathToPosix(this.fixPath('dist')) - const build = await esbuild.build({ - entryPoints: [dir], - bundle: true, - outdir: out, - plugins: [], - }) - if(build.errors.length > 0) { - console.log('ERRORS', build.errors) - return + try { + const build = await esbuild.build({ + entryPoints: [dir], + bundle: true, + outdir: out, + plugins: [], + }) + console.log('build', build) + if (build.errors.length > 0) { + console.log('ERRORS', build.errors) + return + } + console.log(path.join(out, 'test.js')) + const child2 = utilityProcess.fork(path.join(out, 'test.js'), [], { + stdio: 'pipe', + }) + child2 && + child2.stdout && + child2.stdout.on('data', (data) => { + this.call('terminal' as any, 'log', data.toString()) + }) + } catch (e: any) { + // find all errors in string with 'Could not resolve' + const errors = e.toString().match(/Could not resolve "([^"]*)"/g) + if (errors) { + for (const error of errors) { + const match = error.match(/Could not resolve "([^"]*)"/) + if (match) { + const module = match[1] + const modulePath = path.join(this.workingDir, 'node_modules', module) + try { + await fs.stat(modulePath) + } catch (e) { + console.log('modulePath', modulePath) + this.emit('missingModule', module) + this.call('terminal' as any, 'log', { + type: 'error', + value: `Missing module ${module}`, + }) + } + } + } + } + console.log('ERROR', e) } - console.log(path.join(out,'test.js')) - const child2 = utilityProcess.fork(path.join(out,'test.js'), [], { - stdio: 'pipe' - }) - child2 && child2.stdout && child2.stdout.on('data', (data) => { - this.call('terminal' as any, 'log', data.toString()) - }) - - } fixPath(path: string): string { @@ -90,16 +185,25 @@ class ScriptRunnerClient extends ElectronBasePluginClient { } } +let onEndPlugin = { + name: 'onEnd', + setup(build: esbuild.PluginBuild) { + build.onEnd((result) => { + console.log(`build ended with ${result.errors.length} errors`) + }) + }, +} const onResolvePlugin = { name: 'onResolve', setup(build: esbuild.PluginBuild) { - - build.onLoad({ - filter: /.*/, - }, async args => { - console.log('onLoad', args) - /*if(args.namespace && args.namespace !== 'file'){ + build.onLoad( + { + filter: /.*/, + }, + async (args) => { + console.log('onLoad', args) + /*if(args.namespace && args.namespace !== 'file'){ const imported = await resolver.resolve(args.path) console.log('imported', imported) return { @@ -107,16 +211,15 @@ const onResolvePlugin = { loader: 'js', } }*/ - return undefined - - }) - } + return undefined + } + ) + }, } - -import { URL } from "url" -import axios from "axios" -import { app, utilityProcess } from "electron" +import {URL} from 'url' +import axios from 'axios' +import {app, utilityProcess} from 'electron' let httpPlugin = { name: 'http', @@ -125,7 +228,7 @@ let httpPlugin = { // esbuild doesn't attempt to map them to a file system location. // Tag them with the "http-url" namespace to associate them with // this plugin. - build.onResolve({ filter: /^https?:\/\// }, args => ({ + build.onResolve({filter: /^https?:\/\//}, (args) => ({ path: args.path, namespace: 'http-url', })) @@ -135,7 +238,7 @@ let httpPlugin = { // files will be in the "http-url" namespace. Make sure to keep // the newly resolved URL in the "http-url" namespace so imports // inside it will also be resolved as URLs recursively. - build.onResolve({ filter: /.*/, namespace: 'http-url' }, args => ({ + build.onResolve({filter: /.*/, namespace: 'http-url'}, (args) => ({ path: new URL(args.path, args.importer).toString(), namespace: 'http-url', })) @@ -144,11 +247,11 @@ let httpPlugin = { // from the internet. This has just enough logic to be able to // handle the example import from unpkg.com but in reality this // would probably need to be more complex. - build.onLoad({ filter: /.*/, namespace: 'http-url' }, async (args) => { + build.onLoad({filter: /.*/, namespace: 'http-url'}, async (args) => { // Download the file - const response = await axios.get(args.path, { responseType: 'arraybuffer' }) + const response = await axios.get(args.path, {responseType: 'arraybuffer'}) //console.log('response', response.data.toString()) - return { contents: response.data.toString(), loader: 'js' } + return {contents: response.data.toString(), loader: 'js'} }) }, -} \ No newline at end of file +}