/packages/engine/theia',
20 | ],
21 | };
22 |
--------------------------------------------------------------------------------
/packages/api/src/lib/compiler/api.ts:
--------------------------------------------------------------------------------
1 | import { CompilationResult, CompilationFileSources, lastCompilationResult, CondensedCompilationInput, SourcesInput } from './type'
2 | import { StatusEvents, Api } from '@remixproject/plugin-utils'
3 |
4 | export interface ICompiler extends Api {
5 | events: {
6 | compilationFinished: (
7 | fileName: string,
8 | source: CompilationFileSources,
9 | languageVersion: string,
10 | data: CompilationResult
11 | ) => void
12 | } & StatusEvents
13 | methods: {
14 | getCompilationResult(): lastCompilationResult
15 | compile(fileName: string): void
16 | setCompilerConfig(settings: CondensedCompilationInput): void
17 | compileWithParameters(targets: SourcesInput, settings: CondensedCompilationInput): lastCompilationResult
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/engine/web/src/lib/view.ts:
--------------------------------------------------------------------------------
1 | import type { Profile, LocationProfile } from '@remixproject/plugin-utils'
2 | import { Plugin } from '@remixproject/engine'
3 |
4 |
5 | export function isView(profile: Profile): profile is (ViewProfile & P) {
6 | return !!profile['location']
7 | }
8 |
9 | export type ViewProfile = Profile & LocationProfile
10 |
11 | export abstract class ViewPlugin extends Plugin {
12 | abstract render(): any
13 |
14 | constructor(public profile: ViewProfile) {
15 | super(profile)
16 | }
17 |
18 | async activate() {
19 | await this.call(this.profile.location, 'addView', this.profile, this.render())
20 | super.activate()
21 | }
22 |
23 | deactivate() {
24 | this.call(this.profile.location, 'removeView', this.profile)
25 | super.deactivate()
26 | }
27 | }
--------------------------------------------------------------------------------
/packages/engine/vscode/src/lib/appmanager.ts:
--------------------------------------------------------------------------------
1 | import { PluginManager } from "@remixproject/engine"
2 | import axios from 'axios'
3 | export class VscodeAppManager extends PluginManager {
4 | pluginsDirectory:string
5 | target:string
6 | constructor () {
7 | super()
8 | this.pluginsDirectory = 'https://raw.githubusercontent.com/ethereum/remix-plugins-directory/master/build/metadata.json'
9 | this.target = "vscode"
10 | }
11 |
12 | async registeredPluginData () {
13 | let plugins
14 | try {
15 | plugins = await axios.get(this.pluginsDirectory)
16 | return plugins.data.filter((p:any)=>(p.targets && p.targets.includes(this.target)))
17 | } catch (e) {
18 | throw new Error("Could not fetch plugin profiles.")
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/packages/utils/src/lib/types/message.ts:
--------------------------------------------------------------------------------
1 | export interface PluginRequest {
2 | /** The name of the plugin making the request */
3 | from: string,
4 | /** @deprecated Will be remove in the next version */
5 | isFromNative?: boolean,
6 | /**
7 | * The path to access the request inside the plugin
8 | * @example 'remixd.cmd.git'
9 | */
10 | path?: string
11 | }
12 |
13 | type MessageActions = 'on' | 'off' | 'once' | 'call' | 'response' | 'emit' | 'cancel'
14 |
15 | /** @deprecated Use `MessageAcitons` instead */
16 | type OldMessageActions = 'notification' | 'request' | 'response' | 'listen'
17 |
18 | export interface Message {
19 | id: number
20 | action: MessageActions | OldMessageActions
21 | name: string
22 | key: string
23 | payload: any
24 | requestInfo: PluginRequest
25 | error?: Error | string
26 | }
27 |
--------------------------------------------------------------------------------
/packages/api/src/lib/file-system/file-panel/api.ts:
--------------------------------------------------------------------------------
1 | import { StatusEvents } from '@remixproject/plugin-utils'
2 | import { customAction } from './type';
3 | export interface IFilePanel {
4 | events: {
5 | setWorkspace: (workspace:any) => void
6 | workspaceRenamed: (workspace:any) => void
7 | workspaceDeleted: (workspace:any) => void
8 | workspaceCreated: (workspace:any) => void
9 | customAction: (cmd: customAction) => void
10 | } & StatusEvents
11 | methods: {
12 | getCurrentWorkspace(): { name: string, isLocalhost: boolean, absolutePath: string }
13 | getWorkspaces(): string[]
14 | deleteWorkspace(name:string): void
15 | createWorkspace(name:string, isEmpty:boolean): void
16 | renameWorkspace(oldName:string, newName:string): void
17 | registerContextMenuItem(cmd: customAction): void
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/examples/example/engine/web/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
23 |
--------------------------------------------------------------------------------
/examples/example/engine/web/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | displayName: 'example-engine-web',
3 | preset: '../../../../jest.preset.js',
4 | setupFilesAfterEnv: ['/src/test-setup.ts'],
5 | globals: {
6 | 'ts-jest': {
7 | tsConfig: '/tsconfig.spec.json',
8 | stringifyContentPathRegex: '\\.(html|svg)$',
9 | astTransformers: {
10 | before: [
11 | 'jest-preset-angular/build/InlineFilesTransformer',
12 | 'jest-preset-angular/build/StripStylesTransformer',
13 | ],
14 | },
15 | },
16 | },
17 | coverageDirectory: '../../../../coverage/examples/example/engine/web',
18 | snapshotSerializers: [
19 | 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js',
20 | 'jest-preset-angular/build/AngularSnapshotSerializer.js',
21 | 'jest-preset-angular/build/HTMLCommentSerializer.js',
22 | ],
23 | };
24 |
--------------------------------------------------------------------------------
/examples/example/plugin/webview/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | displayName: 'example-plugin-webview',
3 | preset: '../../../../jest.preset.js',
4 | setupFilesAfterEnv: ['/src/test-setup.ts'],
5 | globals: {
6 | 'ts-jest': {
7 | tsConfig: '/tsconfig.spec.json',
8 | stringifyContentPathRegex: '\\.(html|svg)$',
9 | astTransformers: {
10 | before: [
11 | 'jest-preset-angular/build/InlineFilesTransformer',
12 | 'jest-preset-angular/build/StripStylesTransformer',
13 | ],
14 | },
15 | },
16 | },
17 | coverageDirectory: '../../../../coverage/examples/example/plugin/webview',
18 | snapshotSerializers: [
19 | 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js',
20 | 'jest-preset-angular/build/AngularSnapshotSerializer.js',
21 | 'jest-preset-angular/build/HTMLCommentSerializer.js',
22 | ],
23 | };
24 |
--------------------------------------------------------------------------------
/packages/api/src/lib/theme/types.ts:
--------------------------------------------------------------------------------
1 | export interface Theme {
2 | url?: string
3 | /** @deprecated Use brightness instead */
4 | quality?: 'dark' | 'light'
5 | brightness: 'dark' | 'light'
6 | colors: {
7 | surface: string
8 | background: string
9 | foreground: string
10 | primary: string
11 | primaryContrast: string
12 | secondary?: string
13 | secondaryContrast?: string
14 | success?: string
15 | successContrast?: string
16 | warn: string
17 | warnContrast: string
18 | error: string
19 | errorContrast: string
20 | disabled: string
21 | }
22 | breakpoints: {
23 | xs: number
24 | sm: number
25 | md: number
26 | lg: number
27 | xl: number
28 | }
29 | fontFamily: string
30 | /** A unit to multiply for margin & padding */
31 | space: number
32 | }
33 |
34 | export interface ThemeUrls {
35 | light: string;
36 | dark: string;
37 | }
38 |
--------------------------------------------------------------------------------
/examples/example/engine/web/.browserslistrc:
--------------------------------------------------------------------------------
1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 |
5 | # For the full list of supported browsers by the Angular framework, please see:
6 | # https://angular.io/guide/browser-support
7 |
8 | # You can see what browsers were selected by your queries by running:
9 | # npx browserslist
10 |
11 | last 1 Chrome version
12 | last 1 Firefox version
13 | last 2 Edge major versions
14 | last 2 Safari major versions
15 | last 2 iOS major versions
16 | Firefox ESR
17 | not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line.
18 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
19 |
--------------------------------------------------------------------------------
/packages/engine/web/src/lib/host.ts:
--------------------------------------------------------------------------------
1 | import type { Profile } from '@remixproject/plugin-utils'
2 | import { Plugin } from '@remixproject/engine'
3 |
4 | // @todo(#250): move this into engine-web
5 | export abstract class HostPlugin extends Plugin {
6 |
7 | constructor(profile: Profile) {
8 | // Remove duplicated if needed
9 | const methods = Array.from(new Set([
10 | ...(profile.methods || []),
11 | 'currentFocus', 'focus', 'addView', 'removeView'
12 | ]))
13 | super({...profile, methods })
14 | }
15 |
16 | /** Give the name of the current focus plugin */
17 | abstract currentFocus(): string
18 |
19 | /** Display the view inside the host */
20 | abstract focus(name: string): void
21 |
22 | /** Add the view of a plugin into the DOM */
23 | abstract addView(profile: Profile, view: Element): void
24 |
25 | /** Remove the plugin from the view from the DOM */
26 | abstract removeView(profile: Profile): void
27 | }
--------------------------------------------------------------------------------
/examples/example/plugin/webview/.browserslistrc:
--------------------------------------------------------------------------------
1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 |
5 | # For the full list of supported browsers by the Angular framework, please see:
6 | # https://angular.io/guide/browser-support
7 |
8 | # You can see what browsers were selected by your queries by running:
9 | # npx browserslist
10 |
11 | last 1 Chrome version
12 | last 1 Firefox version
13 | last 2 Edge major versions
14 | last 2 Safari major versions
15 | last 2 iOS major versions
16 | Firefox ESR
17 | not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line.
18 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
19 |
--------------------------------------------------------------------------------
/examples/example/engine/web-e2e/src/plugins/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example plugins/index.js can be used to load plugins
3 | //
4 | // You can change the location of this file or turn off loading
5 | // the plugins file with the 'pluginsFile' configuration option.
6 | //
7 | // You can read more here:
8 | // https://on.cypress.io/plugins-guide
9 | // ***********************************************************
10 |
11 | // This function is called when a project is opened or re-opened (e.g. due to
12 | // the project's config changing)
13 |
14 | const { preprocessTypescript } = require('@nrwl/cypress/plugins/preprocessor');
15 |
16 | module.exports = (on, config) => {
17 | // `on` is used to hook into various events Cypress emits
18 | // `config` is the resolved Cypress config
19 |
20 | // Preprocess Typescript file using Nx helper
21 | on('file:preprocessor', preprocessTypescript(config));
22 | };
23 |
--------------------------------------------------------------------------------
/examples/example/plugin/webview-e2e/src/plugins/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example plugins/index.js can be used to load plugins
3 | //
4 | // You can change the location of this file or turn off loading
5 | // the plugins file with the 'pluginsFile' configuration option.
6 | //
7 | // You can read more here:
8 | // https://on.cypress.io/plugins-guide
9 | // ***********************************************************
10 |
11 | // This function is called when a project is opened or re-opened (e.g. due to
12 | // the project's config changing)
13 |
14 | const { preprocessTypescript } = require('@nrwl/cypress/plugins/preprocessor');
15 |
16 | module.exports = (on, config) => {
17 | // `on` is used to hook into various events Cypress emits
18 | // `config` is the resolved Cypress config
19 |
20 | // Preprocess Typescript file using Nx helper
21 | on('file:preprocessor', preprocessTypescript(config));
22 | };
23 |
--------------------------------------------------------------------------------
/examples/example/engine/web/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { BrowserModule } from '@angular/platform-browser';
2 | import { NgModule } from '@angular/core';
3 | import { ReactiveFormsModule } from '@angular/forms';
4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
5 |
6 | import { AppComponent } from './app.component';
7 | import { HostDirective } from './host.directive';
8 | import { TerminalComponent } from './terminal/terminal.component';
9 |
10 | // Material
11 | import { MatListModule } from '@angular/material/list';
12 | import { MatIconModule } from '@angular/material/icon';
13 |
14 | @NgModule({
15 | declarations: [
16 | AppComponent,
17 | HostDirective,
18 | TerminalComponent
19 | ],
20 | imports: [
21 | BrowserModule,
22 | ReactiveFormsModule,
23 | BrowserAnimationsModule,
24 | MatListModule,
25 | MatIconModule
26 | ],
27 | providers: [],
28 | bootstrap: [AppComponent],
29 | })
30 | export class AppModule {}
31 |
--------------------------------------------------------------------------------
/packages/plugin/iframe/src/lib/theme.ts:
--------------------------------------------------------------------------------
1 | import { PluginClient, PluginOptions } from '@remixproject/plugin'
2 | import { Theme } from "@remixproject/plugin-api"
3 |
4 | /** Start listening on theme changed */
5 | export async function listenOnThemeChanged(client: PluginClient, options?: Partial>) {
6 | if (options && options.customTheme) return
7 | const cssLink = document.createElement('link')
8 | cssLink.setAttribute('rel', 'stylesheet')
9 | document.head.prepend(cssLink)
10 | client.onload(async () => {
11 | client.on('theme', 'themeChanged', (_theme: Theme) => setTheme(cssLink, _theme))
12 | const theme = await client.call('theme', 'currentTheme')
13 | setTheme(cssLink, theme)
14 | })
15 | return cssLink
16 | }
17 |
18 |
19 | function setTheme(cssLink: HTMLLinkElement, theme: Theme) {
20 | cssLink.setAttribute('href', theme.url.replace(/^http:/,"").replace(/^https:/,""))
21 | document.documentElement.style.setProperty('--theme', theme.quality)
22 | }
23 |
--------------------------------------------------------------------------------
/packages/plugin/core/src/lib/node.ts:
--------------------------------------------------------------------------------
1 | import { PluginClient } from "./client"
2 | import { getRootPath } from '@remixproject/plugin-utils'
3 |
4 | /**
5 | * Access a service of an external plugin
6 | */
7 | export class PluginNode {
8 |
9 | /**
10 | * @param path Path to external plugin
11 | * @param client The main client used in this plugin
12 | */
13 | constructor(private path: string, private client: PluginClient) {}
14 |
15 | get(name: string) {
16 | return new PluginNode(`${this.path}.${name}`, this.client)
17 | }
18 |
19 | /** Call a method of the node */
20 | call(method: string, ...payload: any[]) {
21 | return this.client.call(this.path as any, method, payload)
22 | }
23 |
24 | /**
25 | * Listen to an event from the plugin
26 | * @note Event are trigger at the root level yet, not on a specific node
27 | */
28 | on(method: string, cb: Function) {
29 | // Events are triggered at the root level for now
30 | this.client.on(getRootPath(this.path) as any, method, cb)
31 | }
32 | }
--------------------------------------------------------------------------------
/packages/engine/vscode/src/util/path.ts:
--------------------------------------------------------------------------------
1 | import { join, relative } from 'path'
2 | import { workspace, } from 'vscode'
3 |
4 | export function absolutePath(path: string) {
5 | path = path.replace(/^\/browser\//,"").replace(/^browser\//,"")
6 | const root = workspace.workspaceFolders[0].uri.fsPath
7 | // vscode API will never get permission to WriteFile outside of its workspace directory
8 | if(!root) {
9 | return path
10 | }
11 | if(path.startsWith(root)) {
12 | path = relative(root, path)
13 | }
14 | const result = join(root, path)
15 | if (!result.startsWith(root)) {
16 | throw new Error(`Resolved path is should be inside the open workspace : "${root}". Got "${result}`)
17 | }
18 | return result
19 | }
20 |
21 | export function relativePath(path) {
22 | const root = workspace.workspaceFolders[0].uri.fsPath
23 | // vscode API will never get permission to WriteFile outside of its workspace directory
24 | if (!root) {
25 | return path
26 | }
27 | return relative(root, path)
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/packages/plugin/core/tests/node.spec.ts:
--------------------------------------------------------------------------------
1 | import { PluginClient } from '@remixproject/plugin'
2 | import { PluginNode } from '../src'
3 |
4 | class MockClient extends PluginClient {
5 | call = jest.fn()
6 | on = jest.fn()
7 | }
8 |
9 | describe('Plugin node', () => {
10 | let client: MockClient
11 | let node: PluginNode
12 | beforeEach(() => {
13 | client = new MockClient()
14 | node = new PluginNode('external', client)
15 | })
16 |
17 | test('Get', () => {
18 | const deepNode = node.get('service')
19 | expect(deepNode['path']).toBe('external.service')
20 | expect(deepNode instanceof PluginNode).toBeTruthy()
21 | })
22 |
23 | test('Call', () => {
24 | node.get('service').call('method', 'payload')
25 | expect(client.call).toHaveBeenCalledWith('external.service', 'method', ['payload'])
26 | })
27 |
28 | test('On', () => {
29 | node.get('service').on('method', () => {})
30 | expect(client.on.mock.calls[0][0]).toBe('external')
31 | expect(client.on.mock.calls[0][1]).toBe('method')
32 | })
33 |
34 | })
--------------------------------------------------------------------------------
/packages/plugin/webview/README.md:
--------------------------------------------------------------------------------
1 | # Plugin Webview
2 |
3 | This library provides connectors to connect a plugin to an engine that can load webview or iframes.
4 | ```
5 | npm install @remixproject/plugin-webview
6 | ```
7 |
8 | If you do not expose any API you can create an instance like this :
9 | ```typescript
10 | import { createClient } from '@remixproject/plugin-webview'
11 |
12 | const client = createClient()
13 | client.onload(async () => {
14 | const data = client.call('filemanager', 'readFile', 'ballot.sol')
15 | })
16 | ```
17 |
18 | If you need to expose an API to other plugin you need to extends the class:
19 | ```typescript
20 | import { createClient } from '@remixproject/plugin-webview'
21 | import { PluginClient } from '@remixproject/plugin'
22 |
23 | class MyPlugin extends PluginClient {
24 | methods = ['hello']
25 | hello() {
26 | console.log('Hello World')
27 | }
28 | }
29 | const client = createClient()
30 | client.onload(async () => {
31 | const data = client.call('filemanager', 'readFile', 'ballot.sol')
32 | })
33 | ```
--------------------------------------------------------------------------------
/packages/plugin/webworker/README.md:
--------------------------------------------------------------------------------
1 | # Plugin Webworker
2 |
3 | This library provides connectors to connect a plugin to an engine that can load webworkers.
4 | ```
5 | npm install @remixproject/plugin-webworker
6 | ```
7 |
8 | If you do not expose any API you can create an instance like this :
9 | ```typescript
10 | import { createClient } from '@remixproject/plugin-webworker'
11 |
12 | const client = createClient()
13 | client.onload(async () => {
14 | const data = client.call('filemanager', 'readFile', 'ballot.sol')
15 | })
16 | ```
17 |
18 | If you need to expose an API to other plugin you need to extends the class:
19 | ```typescript
20 | import { createClient } from '@remixproject/plugin-webworker'
21 | import { PluginClient } from '@rexmixproject/plugin'
22 |
23 | class MyPlugin extends PluginClient {
24 | methods = ['hello']
25 | hello() {
26 | console.log('Hello World')
27 | }
28 | }
29 | const client = createClient()
30 | client.onload(async () => {
31 | const data = client.call('filemanager', 'readFile', 'ballot.sol')
32 | })
33 | ```
--------------------------------------------------------------------------------
/packages/api/src/lib/file-system/file-manager/profile.ts:
--------------------------------------------------------------------------------
1 | import { IFileSystem } from './api'
2 | import { LocationProfile, Profile } from '@remixproject/plugin-utils'
3 |
4 | export const filSystemProfile: Profile & LocationProfile = {
5 | name: "fileManager",
6 | displayName: "Native Filemanager for Remix vscode plugin",
7 | description: "Provides communication between vscode filemanager and remix-plugin",
8 | location: "sidePanel",
9 | documentation: "https://remix-ide.readthedocs.io/en/latest/solidity_editor.html",
10 | version: "0.0.1",
11 | methods: [
12 | "getFolder",
13 | "getCurrentFile",
14 | "getFile",
15 | "setFile",
16 | "switchFile",
17 | // NextFileSystemAPI
18 | "open",
19 | "writeFile",
20 | "readFile",
21 | "rename",
22 | "copyFile",
23 | "mkdir",
24 | "readdir",
25 | "closeAllFiles",
26 | "closeFile",
27 | "remove",
28 | ],
29 | events: ['currentFileChanged', 'fileAdded', 'fileClosed', 'fileRemoved', 'fileRenamed', 'fileSaved', 'noFileSelected', 'folderAdded']
30 | };
--------------------------------------------------------------------------------
/packages/engine/core/doc/api/engine.md:
--------------------------------------------------------------------------------
1 | # Engine
2 |
3 | The `Engine` deals with the registration of the plugins, to make sure they are available for activation.
4 |
5 | It manages the interaction between plugins (calls & events).
6 |
7 | ## Constructor
8 | The `Engine` depends on the plugin manager for the permission system.
9 |
10 | ```typescript
11 | const manager = new PluginManager()
12 | const engine = new Engine()
13 | engine.register(manager)
14 | ```
15 |
16 |
17 | ### register
18 | ```typescript
19 | register(plugins: Plugin | Plugin[]): string | string[]
20 | ```
21 |
22 | Register one or several plugins into the engine and return their names.
23 |
24 | A plugin **must be register before being activated**.
25 |
26 | ### isRegistered
27 | ```typescript
28 | isRegistered(name: string): boolean
29 | ```
30 |
31 | Checks if a plugin with this name has already been registered by the engine.
32 |
33 | ## Hooks
34 |
35 | ### onRegistration
36 | ```typescript
37 | onRegistration(plugin: Plugin) {}
38 | ```
39 |
40 | This method triggered when a plugin is registered.
--------------------------------------------------------------------------------
/packages/engine/node/src/lib/child-process.ts:
--------------------------------------------------------------------------------
1 | import type { Message, Profile, ExternalProfile } from '@remixproject/plugin-utils'
2 | import { PluginConnector } from '@remixproject/engine'
3 | import { fork, ChildProcess } from 'child_process'
4 |
5 | export class ChildProcessPlugin extends PluginConnector {
6 | private readonly listener = ['message', (msg: Message) => this.getMessage(msg)] as const
7 | process: ChildProcess
8 |
9 | constructor(profile: Profile & ExternalProfile) {
10 | super(profile)
11 | }
12 |
13 | protected send(message: Partial): void {
14 | if (!this.process?.connected) {
15 | throw new Error(`Child process from plugin "${this.name}" is not yet connected`)
16 | }
17 | this.process.send(message)
18 | }
19 |
20 | protected connect(url: string): void {
21 | this.process = fork(url)
22 | this.process.on(...this.listener)
23 | }
24 |
25 | protected disconnect(): void {
26 | this.process.off(...this.listener)
27 | this.process.disconnect()
28 | this.process.kill()
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/packages/engine/web/src/lib/window.ts:
--------------------------------------------------------------------------------
1 | import { Plugin } from '@remixproject/engine'
2 | import { IWindow, windowProfile } from '@remixproject/plugin-api'
3 | import { MethodApi, PluginOptions } from '@remixproject/plugin-utils';
4 |
5 | export class WindowPlugin extends Plugin implements MethodApi {
6 |
7 | constructor(options: PluginOptions = {}) {
8 | super(windowProfile)
9 | // Leave 1min to let the user interact with the window
10 | super.setOptions({ queueTimeout: 60_000, ...options })
11 | }
12 |
13 | /** Display an input window */
14 | prompt(message?: string): Promise {
15 | return new Promise((res, rej) => res(window.prompt(message)));
16 | }
17 |
18 | /** Ask confirmation for an action */
19 | confirm(message: string): Promise {
20 | return new Promise((res) => res(window.confirm(message)));
21 | }
22 |
23 | /** Display a message with actions button. Returned the button clicked if any */
24 | alert(message: string, actions?: string[]): Promise {
25 | return new Promise((res, rej) => res(window.alert(message)));
26 | }
27 |
28 | }
--------------------------------------------------------------------------------
/packages/engine/vscode/src/lib/extension.ts:
--------------------------------------------------------------------------------
1 | // Check here : https://code.visualstudio.com/api/references/vscode-api#extensions
2 | import { PluginConnector } from "@remixproject/engine"
3 | import { Profile, ExternalProfile, Message } from '@remixproject/plugin-utils'
4 | import { extensions, Extension } from 'vscode'
5 |
6 | export class ExtensionPlugin extends PluginConnector {
7 | private extension: Extension
8 | private connector: any
9 |
10 | constructor(profile: Profile & ExternalProfile) {
11 | super(profile)
12 | }
13 |
14 | protected send(message: Partial): void {
15 | if (this.extension) {
16 | this.connector.onMessage(message)
17 | }
18 | }
19 |
20 | protected async connect(url: string): Promise {
21 | try {
22 | this.extension = extensions.getExtension(url)
23 | this.connector = await this.extension.activate()
24 | } catch (err) {
25 | throw new Error(`ExtensionPlugin "${this.profile.name}" could not connect to the engine.`)
26 | }
27 | }
28 |
29 | protected disconnect(): void {
30 | if (this.extension) {}
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/api/src/lib/git/api.ts:
--------------------------------------------------------------------------------
1 | import { StatusEvents } from '@remixproject/plugin-utils'
2 |
3 | export interface IGitSystem {
4 | events: {
5 | } & StatusEvents
6 | methods: {
7 | //Priority
8 | clone(url: string): string
9 | checkout(cmd: string): string // Switch branches or restore working tree files
10 | init(): string
11 | add(cmd: string): string
12 | commit(cmd: string): string
13 | fetch(cmd: string): string
14 | pull(cmd: string): string
15 | push(cmd: string): string
16 | reset(cmd: string): string
17 | status(cmd: string): string
18 | remote(cmd: string): string
19 | log(): string
20 |
21 | //Less priority
22 | /*
23 | gitMv(cmd: string): void //Move or rename a file, a directory, or a symlink
24 | gitRm(cmd: string)
25 | gitConfig(cmd: string): void //Get and set repository or global options
26 | gitBranch(cmd: string): void
27 | gitMerge(cmd: string): void
28 | gitRebase(cmd: string): void
29 | gitSwitch(cmd: string): void
30 | gitTag(cmd: string): void
31 | gitBlame(cmd: string): void
32 | */
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/packages/engine/vscode/src/lib/contentimport.ts:
--------------------------------------------------------------------------------
1 | import { contentImportProfile, IContentImport } from '@remixproject/plugin-api';
2 | import { ContentImport } from '@remixproject/plugin-api';
3 | import { MethodApi } from '@remixproject/plugin-utils';
4 | import { CommandPlugin } from './command';
5 | import { RemixURLResolver } from '@remix-project/remix-url-resolver';
6 |
7 | export class ContentImportPlugin extends CommandPlugin
8 | implements MethodApi {
9 | urlResolver: RemixURLResolver;
10 | constructor() {
11 | super(contentImportProfile);
12 | this.urlResolver = new RemixURLResolver();
13 | }
14 |
15 | async resolve(path: string): Promise {
16 | let resolved: any;
17 | try {
18 | resolved = await this.urlResolver.resolve(path);
19 | const { content, cleanUrl, type } = resolved;
20 | return { content, cleanUrl, type, url: path };
21 | } catch (e) {
22 | throw Error(e.message);
23 | }
24 | }
25 | // TODO: implement this method
26 | async resolveAndSave(url: string, targetPath: string): Promise {
27 | return '';
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/examples/example/engine/web/src/app/plugins/library.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { LibraryPlugin } from '@remixproject/engine';
3 | import { LibraryProfile } from '@remixproject/plugin-utils';
4 | import { EventEmitter } from 'events';
5 | import { Engine } from '../engine';
6 |
7 | interface Transaction {
8 | id: string;
9 | }
10 |
11 | class TransactionLibrary {
12 | private transactions: Transaction[] = [];
13 | events = new EventEmitter();
14 |
15 | sendTransaction(tx: Transaction) {
16 | this.transactions.push(tx);
17 | this.events.emit('newTransaction', tx)
18 | }
19 |
20 | }
21 |
22 | const profile: LibraryProfile = {
23 | name: 'library',
24 | methods: ['sendTransaction'],
25 | events: ['newTransaction']
26 | }
27 |
28 | @Injectable({ providedIn: 'root' })
29 | export class Library extends LibraryPlugin {
30 | library: TransactionLibrary;
31 | constructor(engine: Engine) {
32 | const library = new TransactionLibrary();
33 | super(library, profile);
34 | engine.register(this);
35 | }
36 |
37 | onActivation() {
38 | this.library.sendTransaction({ id: "0" });
39 | }
40 | }
--------------------------------------------------------------------------------
/packages/plugin/webworker/src/lib/connector.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { ClientConnector, connectClient, applyApi, Client, PluginClient } from '@remixproject/plugin'
3 | import type { Message, Api, ApiMap } from '@remixproject/plugin-utils'
4 | import { IRemixApi } from '@remixproject/plugin-api'
5 |
6 |
7 | export class WebworkerConnector implements ClientConnector {
8 |
9 | /** Send a message to the engine */
10 | send(message: Partial) {
11 | postMessage(message)
12 | }
13 |
14 | /** Get message from the engine */
15 | on(cb: (message: Partial) => void) {
16 | addEventListener('message', ({ data }) => cb(data))
17 | }
18 | }
19 |
20 | /**
21 | * Connect a Websocket plugin client to a web engine
22 | * @param client An optional websocket plugin client to connect to the engine.
23 | */
24 | export const createClient = <
25 | P extends Api,
26 | App extends ApiMap = Readonly
27 | >(client: PluginClient = new PluginClient()): Client
=> {
28 | const c = client as any
29 | connectClient(new WebworkerConnector(), c)
30 | applyApi(c)
31 | return c
32 | }
33 |
--------------------------------------------------------------------------------
/packages/api/src/lib/dgit/api.ts:
--------------------------------------------------------------------------------
1 | import { StatusEvents } from "@remixproject/plugin-utils";
2 | export interface IDgitSystem {
3 | events: StatusEvents
4 | methods: {
5 | init(): void;
6 | add(cmd: any): string;
7 | commit(cmd: any): string;
8 | status(cmd: any): any[];
9 | rm(cmd: any): string;
10 | log(cmd: any): any[];
11 | lsfiles(cmd: any): any[];
12 | readblob(cmd: any): { oid: string, blob: Uint8Array }
13 | resolveref(cmd: any): string
14 | branch(cmd: any): void
15 | checkout(cmd: any): void
16 | branches(): string[]
17 | currentbranch(): string
18 | push(cmd: any): string
19 | pull(cmd: any): void
20 | setIpfsConfig(config:any): boolean
21 | zip():void
22 | setItem(name:string, content:string):void
23 | getItem(name: string): string
24 | import(cmd: any): void
25 | export(cmd: any): void
26 | remotes(): any[]
27 | addremote(cmd: any): void
28 | delremote(cmd: any): void
29 | clone(cmd: any): void
30 | localStorageUsed(): any
31 | };
32 | }
33 |
--------------------------------------------------------------------------------
/packages/plugin/iframe/README.md:
--------------------------------------------------------------------------------
1 | # Plugin frame
2 |
3 | **Except if you want your plugin to ONLY work on the web, prefer [@remixproject/plugin-webview](../webview)**
4 |
5 | This library provides connectors to connect a plugin to an engine running in a web environment.
6 | ```
7 | npm install @remixproject/plugin-iframe
8 | ```
9 |
10 | If you do not expose any API you can create an instance like this :
11 | ```typescript
12 | import { createClient } from '@remixproject/plugin-iframe'
13 |
14 | const client = createClient()
15 | client.onload(async () => {
16 | const data = client.call('filemanager', 'readFile', 'ballot.sol')
17 | })
18 | ```
19 |
20 | If you need to expose an API to other plugin you need to extends the class:
21 | ```typescript
22 | import { createClient } from '@remixproject/plugin-iframe'
23 | import { PluginClient } from '@rexmixproject/plugin'
24 |
25 | class MyPlugin extends PluginClient {
26 | methods = ['hello']
27 | hello() {
28 | console.log('Hello World')
29 | }
30 | }
31 | const client = createClient()
32 | client.onload(async () => {
33 | const data = client.call('filemanager', 'readFile', 'ballot.sol')
34 | })
35 | ```
36 |
--------------------------------------------------------------------------------
/packages/api/src/lib/standard-profile.ts:
--------------------------------------------------------------------------------
1 | import { ProfileMap, Profile, ApiMap } from '@remixproject/plugin-utils'
2 | import { compilerProfile, ICompiler } from './compiler'
3 | import { filSystemProfile, IFileSystem } from './file-system/file-manager'
4 | import { editorProfile, IEditor } from './editor'
5 | import { networkProfile, INetwork } from './network'
6 | import { udappProfile, IUdapp } from './udapp'
7 | import { IPluginManager, pluginManagerProfile } from './plugin-manager'
8 |
9 | export interface IStandardApi {
10 | manager: IPluginManager,
11 | solidity: ICompiler
12 | fileManager: IFileSystem
13 | editor: IEditor
14 | network: INetwork
15 | udapp: IUdapp
16 | }
17 |
18 | export type StandardApi = Readonly
19 |
20 | /** Profiles of all the standard's Native Plugins */
21 | export const standardProfiles: ProfileMap = Object.freeze({
22 | manager: pluginManagerProfile,
23 | solidity: { ...compilerProfile, name: 'solidity' } as Profile,
24 | fileManager: { ...filSystemProfile, name: 'fileManager' } as Profile,
25 | editor: editorProfile,
26 | network: networkProfile,
27 | udapp: udappProfile,
28 | })
29 |
--------------------------------------------------------------------------------
/packages/api/README.md:
--------------------------------------------------------------------------------
1 | # plugin-api
2 |
3 | This library host all the API of the common plugins.
4 |
5 | Here is the list of native plugins exposed by Remix IDE
6 |
7 | _Click on the name of the api to get the full documentation._
8 |
9 |
10 | |API |Name |Description |
11 | |---------------|-------------------------------------|------------|
12 | |File System |[fileManager](./doc/file-system.md) |Manages the File System
13 | |Compiler |[solidity](./doc/solidity.md) |The solidity Compiler
14 | |Editor |[editor](./doc/editor.md) |Enables highlighting in the code Editor
15 | |Network |[network](./doc/network.md) |Defines the network (mainnet, ropsten, ...) and provider (web3, vm, injected) used
16 | |Udapp |[udapp](./doc/udapp.md) |Transaction listener
17 | |Unit Testing |[solidityUnitTesting](./doc/unit-testing.md) |Unit testing library in solidity
18 | |Settings |[settings](./doc/settings.md) |Global settings of the IDE
19 | |Content Import |[contentImport](./doc/content-import.md) |Import files from github, swarm, ipfs, http or https.
20 | |Terminal |[terminal](./doc/terminal.md) |Log to the terminal
21 |
22 |
--------------------------------------------------------------------------------
/packages/plugin/vscode/README.md:
--------------------------------------------------------------------------------
1 | # Plugin vscode
2 | This library provides connectors to run plugin in a vscode environment. Use this connector if you have a web based plugin that needs to run inside vscode.
3 |
4 | **Except if you want your plugin to ONLY work on vscode, prefer [@remixproject/plugin-webview](../webview/readme.md)**
5 |
6 | ```
7 | npm install @remixproject/plugin-vscode
8 | ```
9 |
10 | ## Webview
11 | Similar to `@remixproject/plugin-iframe`, the webview connector will connect to an engine running inside vscode.
12 |
13 | If you do not expose any API you can create an instance like this :
14 | ```html
15 |
21 | ```
22 |
23 | If you need to expose an API to other plugin you need to extends the class:
24 | ```html
25 |
37 | ```
--------------------------------------------------------------------------------
/packages/engine/core/doc/tutorial/4-external-plugins.md:
--------------------------------------------------------------------------------
1 | ## External Plugins
2 |
3 | The superpower of the Engine is the ability to interact safely with external plugins.
4 |
5 | Currently the Engine supports 2 communication channels:
6 | - Iframe
7 | - Websocket
8 |
9 | The interface of these plugins is exacly the same as the other plugins.
10 |
11 | ### Iframe
12 |
13 | For the `IframePlugin` we need to specify the `location` where the plugin will be displayed, and the `url` which will be used as the source of the iframe.
14 |
15 | > The Engine can fetch the content of plugin hosted on IPFS or any other server accessible through HTTPS
16 |
17 | ```typescript
18 | const ethdoc = new IframePlugin({
19 | name: 'ethdoc',
20 | location: 'sidePanel',
21 | url: 'ipfs://QmQmK435v4io3cp6N9aWQHYmgLxpUejjC1RmZCbqL7MJaM'
22 | })
23 | ```
24 |
25 | ### Websocket
26 |
27 | For the `WebsocketPlugin` you just need to specify the `url` as there is no UI to display.
28 |
29 | > This plugin is very useful for connecting to a local server like remixD, and an external API
30 |
31 | ```typescript
32 | const remixd = new WebsocketPlugin({
33 | name: 'remixd',
34 | url: 'wss://localhost:8000'
35 | })
36 | ```
37 |
38 | In the future, we'll implement more communication connection like REST, GraphQL, JSON RPC, gRPC, ...
--------------------------------------------------------------------------------
/examples/example/engine/web-e2e/src/support/commands.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 |
11 | // eslint-disable-next-line @typescript-eslint/no-namespace
12 | declare namespace Cypress {
13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
14 | interface Chainable {
15 | login(email: string, password: string): void;
16 | }
17 | }
18 | //
19 | // -- This is a parent command --
20 | Cypress.Commands.add('login', (email, password) => {
21 | console.log('Custom command example: Login', email, password);
22 | });
23 | //
24 | // -- This is a child command --
25 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
26 | //
27 | //
28 | // -- This is a dual command --
29 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
30 | //
31 | //
32 | // -- This will overwrite an existing command --
33 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
34 |
--------------------------------------------------------------------------------
/examples/example/plugin/webview-e2e/src/support/commands.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 |
11 | // eslint-disable-next-line @typescript-eslint/no-namespace
12 | declare namespace Cypress {
13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
14 | interface Chainable {
15 | login(email: string, password: string): void;
16 | }
17 | }
18 | //
19 | // -- This is a parent command --
20 | Cypress.Commands.add('login', (email, password) => {
21 | console.log('Custom command example: Login', email, password);
22 | });
23 | //
24 | // -- This is a child command --
25 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
26 | //
27 | //
28 | // -- This is a dual command --
29 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
30 | //
31 | //
32 | // -- This will overwrite an existing command --
33 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
34 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "parserOptions": {
5 | "ecmaVersion": 2018,
6 | "sourceType": "module",
7 | "project": "./tsconfig.*?.json"
8 | },
9 | "ignorePatterns": ["**/*"],
10 | "plugins": ["@typescript-eslint", "@nrwl/nx"],
11 | "extends": [
12 | "eslint:recommended",
13 | "plugin:@typescript-eslint/eslint-recommended",
14 | "plugin:@typescript-eslint/recommended",
15 | "prettier",
16 | "prettier/@typescript-eslint"
17 | ],
18 | "rules": {
19 | "@typescript-eslint/explicit-member-accessibility": "off",
20 | "@typescript-eslint/explicit-function-return-type": "off",
21 | "@typescript-eslint/no-parameter-properties": "off",
22 | "@nrwl/nx/enforce-module-boundaries": [
23 | "error",
24 | {
25 | "enforceBuildableLibDependency": true,
26 | "allow": [],
27 | "depConstraints": [
28 | {
29 | "sourceTag": "*",
30 | "onlyDependOnLibsWithTags": ["*"]
31 | }
32 | ]
33 | }
34 | ],
35 | "@typescript-eslint/explicit-module-boundary-types": "off"
36 | },
37 | "overrides": [
38 | {
39 | "files": ["*.tsx"],
40 | "rules": {
41 | "@typescript-eslint/no-unused-vars": "off"
42 | }
43 | }
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/packages/plugin/electron/src/lib/electronPluginClient.ts:
--------------------------------------------------------------------------------
1 | import { ClientConnector, connectClient, applyApi, Client, PluginClient } from '@remixproject/plugin'
2 | import type { Message, Api, ApiMap, Profile } from '@remixproject/plugin-utils'
3 | import { IRemixApi } from '@remixproject/plugin-api'
4 | import { ipcMain } from 'electron'
5 |
6 | export class ElectronPluginClientConnector implements ClientConnector {
7 |
8 | constructor(public profile: Profile, public browserWindow: Electron.BrowserWindow) {
9 | }
10 |
11 | /** Send a message to the engine */
12 | send(message: Partial) {
13 | this.browserWindow.webContents.send(this.profile.name + ':send', message)
14 | }
15 |
16 | /** Listen to message from the engine */
17 | on(cb: (message: Partial) => void) {
18 | ipcMain.on(this.profile.name + ':on:' + this.browserWindow.webContents.id, (event, message) => {
19 | cb(message)
20 | })
21 | }
22 | }
23 |
24 | export const createElectronClient = <
25 | P extends Api,
26 | App extends ApiMap = Readonly
27 | >(client: PluginClient = new PluginClient(), profile: Profile
28 | , window: Electron.BrowserWindow
29 | ): Client
=> {
30 | const c = client as any
31 | connectClient(new ElectronPluginClientConnector(profile, window), c)
32 | applyApi(c)
33 | return c
34 | }
--------------------------------------------------------------------------------
/packages/plugin/core/src/lib/api.ts:
--------------------------------------------------------------------------------
1 | import type { Profile, Api, MethodApi, CustomApi, ProfileMap, ApiMapFromProfileMap, PluginApi, ApiMap } from '@remixproject/plugin-utils'
2 | import { PluginClient } from './client'
3 |
4 | /**
5 | * Create an Api
6 | * @param profile The profile of the api
7 | */
8 | export function createApi(client: PluginClient, profile: Profile): CustomApi {
9 | if (typeof profile.name !== 'string') {
10 | throw new Error('Profile should have a name')
11 | }
12 | const on = >(event: event, cb: T['events'][event]) => {
13 | client.on.call(client, profile.name, event, cb)
14 | }
15 |
16 | const methods = (profile.methods || []).reduce((acc, method) => ({
17 | ...acc,
18 | [method]: client.call.bind(client, profile.name, method)
19 | }), {} as MethodApi)
20 | return { on, ...methods }
21 | }
22 |
23 |
24 | /**
25 | * Transform a list of profile into a map of API
26 | * @deprecated Use `applyApi` from connector instead
27 | */
28 | export function getApiMap, App extends ApiMap>(
29 | client: PluginClient,
30 | profiles: T
31 | ): PluginApi> {
32 | return Object.keys(profiles).reduce((acc, name) => {
33 | const profile = profiles[name] as Profile
34 | return { ...acc, [name]: createApi(client, profile ) }
35 | }, {} as PluginApi>)
36 | }
37 |
--------------------------------------------------------------------------------
/packages/api/src/lib/udapp/type.ts:
--------------------------------------------------------------------------------
1 | export type RemixTxEvent = {
2 | contractAddress: string
3 | data: string
4 | envMode: 'vm'
5 | executionCost: string
6 | from: string
7 | gas: string
8 | hash: string
9 | input: string
10 | logs: any[]
11 | returnValue: Uint8Array
12 | status: '0x01' | '0x00'
13 | transactionCost: string
14 | transactionHash: string
15 | value: string
16 | } | {
17 | blockHash: string
18 | blockNumber: number
19 | envMod: 'injected' | 'web3'
20 | from: string
21 | gas: number
22 | gasPrice: { c: number[], e: number, s: number }
23 | hash: string
24 | input: string
25 | none: number
26 | r: string
27 | s: string
28 | v: string
29 | status: '0x01' | '0x00'
30 | to: string
31 | transactionCost: string
32 | transactionIndex: number
33 | value: { c: number[], e: number, s: number }
34 | }
35 |
36 | export interface RemixTx {
37 | data: string
38 | from: string
39 | to?: string
40 | timestamp?: string
41 | gasLimit: string
42 | value: string
43 | useCall: boolean
44 | }
45 |
46 | export interface RemixTxReceipt {
47 | transactionHash: string
48 | status: 0 | 1
49 | gasUsed: string
50 | error: string
51 | return: string
52 | createdAddress?: string
53 | }
54 |
55 | export interface VMAccount {
56 | privateKey: string
57 | balance: string
58 | }
59 |
60 | export interface UdappSettings {
61 | selectedAccount:string
62 | selectedEnvMode: 'vm' | 'injected' | 'web3'
63 | networkEnvironment: string
64 | }
65 |
--------------------------------------------------------------------------------
/packages/engine/web/src/lib/web-worker.ts:
--------------------------------------------------------------------------------
1 | import type { Message, Profile, ExternalProfile, PluginOptions } from '@remixproject/plugin-utils'
2 | import { PluginConnector } from '@remixproject/engine'
3 |
4 | type WebworkerOptions = WorkerOptions & PluginOptions
5 |
6 | export class WebWorkerPlugin extends PluginConnector {
7 | private worker: Worker
8 | protected options: WebworkerOptions
9 |
10 | constructor(public profile: Profile & ExternalProfile, options?: WebworkerOptions) {
11 | super(profile)
12 | this.setOptions(options)
13 | }
14 |
15 | setOptions(options: Partial) {
16 | super.setOptions({ type: 'module', name: this.name, ...options })
17 | }
18 |
19 | connect(url: string) {
20 | if ('Worker' in window) {
21 | this.worker = new Worker(url, this.options)
22 | this.worker.onmessage = e => this.getEvent(e)
23 | return this.handshake()
24 | }
25 | }
26 |
27 | disconnect() {
28 | this.worker.terminate()
29 | }
30 |
31 | /** Get message from the iframe */
32 | private async getEvent(event: MessageEvent) {
33 | const message: Message = event.data
34 | this.getMessage(message)
35 | }
36 |
37 | /**
38 | * Post a message to the webview of this plugin
39 | * @param message The message to post
40 | */
41 | protected send(message: Partial) {
42 | if (!this.worker) {
43 | throw new Error('No worker attached to the plugin')
44 | }
45 | this.worker.postMessage(message)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/examples/example/engine/web/src/app/plugins/manager.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { PluginManager } from '@remixproject/engine';
3 | import { Profile } from '@remixproject/plugin-utils';
4 | import { BehaviorSubject } from 'rxjs';
5 | import { Engine } from '../engine';
6 |
7 |
8 | const localPlugins = ['manager', 'main', 'theme', 'window'];
9 |
10 | @Injectable({ providedIn: 'root' })
11 | export class Manager extends PluginManager {
12 | private activeProfiles = new BehaviorSubject([]);
13 | private idleProfiles = new BehaviorSubject([]);
14 | public activeProfiles$ = this.activeProfiles.asObservable();
15 | public idleProfiles$ = this.idleProfiles.asObservable();
16 |
17 | constructor(engine: Engine) {
18 | super()
19 | engine.register(this)
20 | }
21 |
22 | private async updateProfiles() {
23 | const actives = [];
24 | const idles = [];
25 | for (const profile of Object.values(this.profiles)) {
26 | await this.isActive(profile.name)
27 | ? actives.push(profile)
28 | : idles.push(profile)
29 | }
30 | this.activeProfiles.next(actives);
31 | this.idleProfiles.next(idles);
32 | }
33 |
34 | onProfileAdded() {
35 | this.updateProfiles();
36 | }
37 |
38 | onPluginActivated(profile: Profile) {
39 | this.updateProfiles();
40 | }
41 |
42 | onPluginDeactivated() {
43 | this.updateProfiles();
44 | }
45 |
46 | async canDeactivatePlugin(from: Profile, to: Profile) {
47 | return localPlugins.includes(from.name);
48 | }
49 | }
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "rootDir": ".",
5 | "sourceMap": true,
6 | "declaration": false,
7 | "moduleResolution": "node",
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "importHelpers": true,
11 | "target": "es2015",
12 | "module": "esnext",
13 | "typeRoots": ["node_modules/@types"],
14 | "lib": ["es2017", "dom"],
15 | "skipLibCheck": true,
16 | "skipDefaultLibCheck": true,
17 | "baseUrl": ".",
18 | "paths": {
19 | "@remixproject/plugin-utils": ["packages/utils/src/index.ts"],
20 | "@remixproject/plugin-api": ["packages/api/src/index.ts"],
21 | "@remixproject/engine": ["packages/engine/core/src/index.ts"],
22 | "@remixproject/engine-web": ["packages/engine/web/src/index.ts"],
23 | "@remixproject/plugin": ["packages/plugin/core/src/index.ts"],
24 | "@remixproject/plugin/ws": ["packages/plugin/ws/src/index.ts"],
25 | "@remixproject/plugin-webview": ["packages/plugin/webview/src/index.ts"],
26 | "@remixproject/plugin-webworker": [
27 | "packages/plugin/webworker/src/index.ts"
28 | ],
29 | "@remixproject/plugin-theia": ["packages/plugin/theia/src/index.ts"],
30 | "@remixproject/engine-theia": ["packages/engine/theia/src/index.ts"],
31 | "@remixproject/engine-electron": ["packages/engine/electron/src/index.ts"],
32 | "@remixproject/plugin-electron": ["packages/plugin/electron/src/index.ts"]
33 | }
34 | },
35 | "exclude": ["node_modules", "tmp"]
36 | }
37 |
--------------------------------------------------------------------------------
/packages/plugin/core/tests/origin.spec.ts:
--------------------------------------------------------------------------------
1 | import { checkOrigin } from "../src"
2 |
3 | declare const global // Needed to mock fetch
4 |
5 | describe('Origin', () => {
6 | test('Check origin', async () => {
7 | const port = 8080
8 | const origins = 'package://'
9 | const goodOrigin = 'http://remix.ethereum.org'
10 | const wrongOrigin = 'http://remix.ethereum.com'
11 | const goodLocalOrigin = `http://127.0.0.1:${port}`
12 | const wrongLocalOrigin = `http://localhost:${port + 1}`
13 | const wrongExternalOrigin = `${origins}wrong`
14 | const goodExternalOrigin = origins
15 | const allowOrigins = [goodLocalOrigin, goodExternalOrigin]
16 |
17 | // Mock fetch api
18 | const mockFetchPromise = Promise.resolve({
19 | json: () => Promise.resolve([
20 | "http://remix-alpha.ethereum.org",
21 | "http://remix.ethereum.org",
22 | "https://remix-alpha.ethereum.org",
23 | "https://remix.ethereum.org"
24 | ])
25 | })
26 | global.fetch = jest.fn().mockImplementation(() => mockFetchPromise)
27 |
28 | expect(await checkOrigin(goodOrigin)).toBeTruthy()
29 | expect(await checkOrigin(wrongOrigin)).toBeTruthy() // Check origin only with devmode & allowOrigins
30 | expect(await checkOrigin(goodLocalOrigin, { allowOrigins })).toBeTruthy()
31 | expect(await checkOrigin(wrongLocalOrigin, { allowOrigins })).toBeFalsy()
32 | expect(await checkOrigin(goodExternalOrigin, { allowOrigins })).toBeTruthy()
33 | expect(await checkOrigin(wrongExternalOrigin, { allowOrigins })).toBeFalsy()
34 | })
35 | })
--------------------------------------------------------------------------------
/packages/engine/vscode/src/lib/command.ts:
--------------------------------------------------------------------------------
1 | import { Plugin } from '@remixproject/engine'
2 | import { Profile, PluginOptions } from '@remixproject/plugin-utils'
3 | import { Disposable, commands } from 'vscode'
4 |
5 | export const transformCmd = (name: string, method: string) =>
6 | `remix.${name}.${method}`
7 |
8 | export interface CommandOptions extends PluginOptions {
9 | transformCmd: (name: string, method: string) => string
10 | }
11 |
12 | // WIP
13 | /**
14 | * Connect methods of the plugins with a command depending on the transformCmd function pass as option
15 | */
16 | export class CommandPlugin extends Plugin {
17 | subscriptions: Disposable[] = []
18 | options: CommandOptions
19 |
20 | constructor(profile: Profile) {
21 | super(profile)
22 | this.setOptions({
23 | transformCmd,
24 | })
25 | }
26 |
27 | setOptions(options: Partial) {
28 | return super.setOptions(options)
29 | }
30 |
31 | activate() {
32 | this.subscriptions = this.profile.methods
33 | .map((method) => {
34 | try {
35 | const cmd = this.options.transformCmd(this.profile.name, method)
36 | return commands.registerCommand(cmd, (...args) =>
37 | this.callPluginMethod(method, args)
38 | )
39 | } catch (err) {
40 | console.log(err)
41 | }
42 | })
43 | .filter((command) => {
44 | return command
45 | })
46 | super.activate()
47 | }
48 |
49 | deactivate() {
50 | super.deactivate()
51 | this.subscriptions.forEach((sub) => sub.dispose())
52 | }
53 | }
--------------------------------------------------------------------------------
/examples/example/engine/web/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component } from '@angular/core';
2 | import { IframePlugin, WebWorkerPlugin } from '@remixproject/engine-web';
3 | import { Theme, Manager, Window, Library } from './plugins';
4 | import { Engine } from './engine';
5 |
6 | const profiles = [
7 | { name: 'iframe', url: 'http://localhost:4201', location: 'main' },
8 | { name: 'scriptRunner', url: 'https://scriptRunner.dyn.plugin.remixproject.org/ipfs/QmbzZFuLHSeLcJ4RUZzNvP3LQ3Yr5Rv4uougPquAVQ2kv1', location: 'main' }
9 | ];
10 |
11 | @Component({
12 | selector: 'engine-root',
13 | templateUrl: './app.component.html',
14 | styleUrls: ['./app.component.scss'],
15 | changeDetection: ChangeDetectionStrategy.OnPush
16 | })
17 | export class AppComponent {
18 | actives$ = this.manager.activeProfiles$;
19 | idles$ = this.manager.idleProfiles$;
20 |
21 | constructor(
22 | private engine: Engine,
23 | private manager: Manager,
24 | private window: Window,
25 | private theme: Theme,
26 | private library: Library
27 | ) {}
28 |
29 | ngAfterViewInit() {
30 | try {
31 | const iframes = profiles.map(profile => new IframePlugin(profile));
32 | const worker = new WebWorkerPlugin({ name: 'worker', url: '/assets/web.worker.js', methods: ['execute'] })
33 | this.engine.register([...iframes, worker]);
34 | } catch (err) {
35 | console.error(err)
36 | }
37 | }
38 |
39 | deactivate(name: string) {
40 | this.manager.deactivatePlugin(name);
41 | }
42 |
43 | activate(name: string) {
44 | this.manager.activatePlugin(name);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/packages/utils/src/lib/types/plugin.ts:
--------------------------------------------------------------------------------
1 | import type { IPluginService } from './service'
2 | import { EventCallback, MethodParams, MethodKey, EventKey, Api, ApiMap, EventParams } from './api'
3 |
4 | export interface PluginBase {
5 | methods: string[],
6 | activateService: Record Promise>
7 | /** Listen on an event from another plugin */
8 | on, Key extends EventKey>(
9 | name: Name,
10 | key: Key,
11 | cb: EventCallback,
12 | ): void
13 |
14 | /** Listen one time on an event from another plugin, then remove event listener */
15 | once, Key extends EventKey>(
16 | name: Name,
17 | key: Key,
18 | cb: EventCallback,
19 | ): void
20 |
21 | /** Stop listening on an event from another plugin */
22 | off, Key extends EventKey>(
23 | name: Name,
24 | key: Key,
25 | ): void
26 |
27 | /** Call a method of another plugin */
28 | call, Key extends MethodKey>(
29 | name: Name,
30 | key: Key,
31 | ...payload: MethodParams
32 | ): Promise
33 |
34 | /** Clear calls in queue of a plugin called by plugin */
35 | cancel, Key extends MethodKey>(
36 | name: Name,
37 | key: Key,
38 | ): void
39 |
40 | /** Emit an event */
41 | emit>(key: Key, ...payload: EventParams): void
42 | }
43 |
--------------------------------------------------------------------------------
/packages/engine/web/README.md:
--------------------------------------------------------------------------------
1 | # Engine Web
2 | The web engine provides a connector for Iframe & Websocket.
3 | `npm install @remixproject/engine-web`
4 |
5 | ## Iframe
6 | The iframe connector is used to load & connect a plugin inside an iframe.
7 | Iframe based plugin are webview using an `index.html` as entry point & need to use `@remixproject/plugin-iframe`.
8 |
9 | ```typescript
10 | const myPlugin = new IframePlugin({
11 | name: 'my-plugin',
12 | url: 'https://my-plugin-path.com',
13 | methods: ['getData']
14 | })
15 | engine.register(myPlugin);
16 | // This will create the iframe with src="https://my-plugin-path.com"
17 | await manager.activatePlugin('my-plugin');
18 | const data = manager.call('my-plugin', 'getData');
19 | ```
20 |
21 | > Communication between the plugin & the engine uses the `window.postMessage()` API.
22 |
23 | ## Websocket
24 | The websocket connector wraps the native [Websocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object from the Web API.
25 | Websocket based plugin are usually server with a Websocket connection open. Any library can be used, remixproject provide a wrapper around the `ws` library : `@remixproject/plugin-ws`.
26 |
27 | ```typescript
28 | const myPlugin = new WebsocketOptions({
29 | name: 'my-plugin',
30 | url: 'https://my-server.com',
31 | methods: ['getData']
32 | }, {
33 | reconnectDelay: 5000 // Time in ms to wait to reconnect after a disconnection
34 | });
35 | engine.register(myPlugin);
36 | // This will open a connection with the server. The server must be running first.
37 | await manager.activatePlugin('my-plugin');
38 | const data = manager.call('my-plugin', 'getData');
39 | ```
40 |
41 |
--------------------------------------------------------------------------------
/packages/engine/core/doc/tutorial/1-getting-started.md:
--------------------------------------------------------------------------------
1 | ## Create the Engine
2 |
3 | 1. Create the Plugin Manager
4 |
5 | The plugin manager can activate/deactivate plugins, and manages permissions between plugins.
6 | ```typescript
7 | import { PluginManager } from '@remixproject/engine';
8 |
9 | const manager = new PluginManager()
10 | ```
11 |
12 | 2. Create the Engine
13 |
14 | The engine manages the communication between plugins. It requires a `PluginManager`.
15 | ```typescript
16 | import { PluginManager, Engine } from '@remixproject/engine';
17 |
18 | const manager = new PluginManager()
19 | const engine = new Engine()
20 | ```
21 |
22 | 3. Register a plugin
23 |
24 | We need to register a plugin before activating it. This is done by the `Engine`.
25 |
26 | > ⚠️ **IMPORTANT** You need to register the "manager" before beeing able to activate a plugin
27 | ```typescript
28 | import { PluginManager, Engine, Plugin } from '@remixproject/engine';
29 |
30 | const manager = new PluginManager()
31 | const engine = new Engine()
32 | const plugin = new Plugin({ name: 'plugin-name' })
33 |
34 | // Register plugin
35 | engine.register([manager, plugin])
36 | ```
37 |
38 | 4. Activate a plugin
39 |
40 | *Once your plugin is registered* you can activate it. This is done by the `PluginManager`
41 | ```typescript
42 | const manager = new PluginManager()
43 | const engine = new Engine()
44 | const plugin = new Plugin({ name: 'plugin-name' })
45 |
46 | // Register plugins
47 | engine.register([manager, plugin])
48 |
49 | // Activate plugins
50 | manager.activatePlugin('plugin-name')
51 | ```
52 |
53 |
54 | 🧪 [Tested code available here](../../examples/engine/tests/1-getting-started.ts)
--------------------------------------------------------------------------------
/packages/api/doc/content-import.md:
--------------------------------------------------------------------------------
1 | # Content Import
2 |
3 | - Name in Remix: `contentImport`
4 | - kind: `contentImport`
5 |
6 | |Type |Name |Description |
7 | |---------|-----------------------|------------|
8 | |_method_ |`resolve` |Resolve a file from github, ipfs, swarm, http or https
9 | |_method_ |`resolveAndSave` |Resolve a file from github, ipfs, swarm, http or https and save it in the file explorer
10 | ## Examples
11 |
12 | ### Methods
13 | `resolve`: Resolve a file from github, ipfs, swarm, http or https
14 | ```typescript
15 | const link = "https://github.com/GrandSchtroumpf/solidity-school/blob/master/std-0/1_HelloWorld/HelloWorld.sol"
16 |
17 | const { content } = await client.call('contentImport', 'resolve', link)
18 | // OR
19 | const { content } = await client.contentImport.resolve(link)
20 | ```
21 |
22 | `resolveAndSave`: Resolve and save a file from github, ipfs, swarm, http or https
23 | ```typescript
24 | const link = "https://github.com/GrandSchtroumpf/solidity-school/blob/master/std-0/1_HelloWorld/HelloWorld.sol"
25 | const targetPath = 'HelloWorld.sol' # optional
26 |
27 | const { content } = await client.call('contentImport', 'resolveAndSave', link, targetPath)
28 | // OR
29 | const { content } = await client.contentImport.resolveAndSave(link, targetPath)
30 | ```
31 |
32 | ## Types
33 | `ContentImport`: An object that describes the returned file
34 | ```typescript
35 | export interface ContentImport {
36 | content: string
37 | cleanUrl: string
38 | type: 'github' | 'http' | 'https' | 'swarm' | 'ipfs'
39 | url: string
40 | }
41 | ```
42 |
43 | > Type Definitions can be found [here](../src/lib/content-import/type.ts)
--------------------------------------------------------------------------------
/packages/plugin/core/tests/remix-api.spec.ts:
--------------------------------------------------------------------------------
1 | import { remixProfiles } from '@remixproject/plugin-api'
2 | import { createConnectorClient } from '../src'
3 |
4 | describe('Remix Api', () => {
5 | const client = createConnectorClient({
6 | send() {},
7 | on() {}
8 | })
9 |
10 | test('Solidity', () => {
11 | expect(client.solidity).toBeDefined()
12 | expect(client.solidity.getCompilationResult).toBeDefined()
13 | expect(client.solidity.on).toBeDefined()
14 | })
15 | test('File Manager', () => {
16 | expect(client.fileManager).toBeDefined()
17 | expect(client.fileManager.getCurrentFile).toBeDefined()
18 | expect(client.fileManager.getFile).toBeDefined()
19 | expect(client.fileManager.getFolder).toBeDefined()
20 | expect(client.fileManager.setFile).toBeDefined()
21 | expect(client.fileManager.switchFile).toBeDefined()
22 | expect(client.fileManager.on).toBeDefined()
23 | })
24 | test('Editor', () => {
25 | expect(client.editor.discardHighlight).toBeDefined()
26 | expect(client.editor.highlight).toBeDefined()
27 | expect(client.editor.on).toBeDefined()
28 | })
29 | test('Network', () => {
30 | expect(client.network.addNetwork).toBeDefined()
31 | expect(client.network.detectNetwork).toBeDefined()
32 | expect(client.network.getEndpoint).toBeDefined()
33 | expect(client.network.getNetworkProvider).toBeDefined()
34 | expect(client.network.removeNetwork).toBeDefined()
35 | expect(client.network.on).toBeDefined()
36 | })
37 | test('Udapp', () => {
38 | expect(client.udapp.createVMAccount).toBeDefined()
39 | expect(client.udapp.getAccounts).toBeDefined()
40 | expect(client.udapp.sendTransaction).toBeDefined()
41 | expect(client.udapp.on).toBeDefined()
42 | })
43 |
44 | })
45 |
--------------------------------------------------------------------------------
/packages/engine/core/tests/manager.spec.ts:
--------------------------------------------------------------------------------
1 | import { Engine, Plugin, PluginManager } from '../src'
2 |
3 | class To extends Plugin {
4 | constructor() {
5 | super({ name: 'to', methods: ['mockMethod'] })
6 | }
7 | askUserPermission = jest.fn(super.askUserPermission)
8 | mockMethod = jest.fn(() => this.askUserPermission('mockMethod'))
9 | }
10 |
11 | class MockManager extends PluginManager {
12 | canCall = jest.fn(async (from) => from === 'from')
13 | }
14 |
15 |
16 | describe('Abstract Plugin', () => {
17 | let from: Plugin
18 | let anotherFrom: Plugin
19 | let to: To
20 | let manager: MockManager
21 | beforeEach(async () => {
22 | from = new Plugin({ name: 'from' })
23 | anotherFrom = new Plugin({ name: 'anotherFrom' })
24 | to = new To()
25 | manager = new MockManager()
26 | const engine = new Engine()
27 | engine.register([manager, from, to, anotherFrom])
28 | await manager.activatePlugin(['from', 'to', 'anotherFrom'])
29 | })
30 |
31 | test('Can Call is exposed', async () => {
32 | await from.call('to', 'mockMethod')
33 | expect(to.mockMethod).toHaveBeenCalledTimes(1)
34 | expect(to.askUserPermission).toHaveBeenCalledWith('mockMethod')
35 | expect(manager.canCall).toHaveBeenCalledWith('from', 'to', 'mockMethod', undefined)
36 | })
37 |
38 | test('', async () => {
39 | const [ shouldBeTrue, shouldBeFalse ] = await Promise.all([
40 | from.call('to', 'mockMethod'),
41 | anotherFrom.call('to', 'mockMethod')
42 | ])
43 | expect(to.mockMethod).toHaveBeenCalledTimes(2)
44 | expect(shouldBeTrue).toBeTruthy()
45 | expect(shouldBeFalse).toBeFalsy()
46 | expect(manager.canCall.mock.calls[0][0]).toBe('from')
47 | expect(manager.canCall.mock.calls[1][0]).toBe('anotherFrom')
48 | })
49 | })
--------------------------------------------------------------------------------
/packages/utils/src/lib/types/profile.ts:
--------------------------------------------------------------------------------
1 | import { MethodKey, Api, ApiMap, EventKey } from './api'
2 |
3 | /** Describe a plugin */
4 | export interface Profile {
5 | name: string
6 | displayName?: string
7 | methods?: MethodKey[]
8 | events?: EventKey[]
9 | permission?: boolean
10 | hash?: string
11 | description?: string
12 | documentation?: string
13 | version?: string
14 | kind?: string,
15 | canActivate?: string[]
16 | icon?: string
17 | maintainedBy?: string,
18 | author?: string
19 | repo?: string
20 | authorContact?: string
21 | }
22 |
23 | export interface LocationProfile {
24 | location: string
25 | }
26 |
27 | export interface ExternalProfile {
28 | url: string
29 | }
30 |
31 | // export interface ExternalProfile extends ViewProfile {
32 | // url: string
33 | // }
34 |
35 | export interface HostProfile extends Profile {
36 | methods: ('addView' | 'removeView' | 'focus' | string)[]
37 | }
38 |
39 | export interface LibraryProfile extends Profile {
40 | events?: EventKey[]
41 | notifications?: {
42 | [name: string]: string[]
43 | }
44 | }
45 |
46 |
47 | /** A map of profile */
48 | export type ProfileMap = Partial<{
49 | [name in keyof T]: Profile
50 | }>
51 |
52 | // PROFILE TO API
53 |
54 | /** Extract the API of a profile */
55 | export type ApiFromProfile = T extends Profile ? I : never
56 | /** Create an ApiMap from a Profile Map */
57 | export type ApiMapFromProfileMap> = {
58 | [name in keyof T]: ApiFromProfile
59 | }
60 |
61 | /** Transform an ApiMap into a profile map */
62 | export type ProfileMapFromApiMap = Readonly<{
63 | [name in keyof T]: Profile
64 | }>
65 |
--------------------------------------------------------------------------------
/packages/plugin/core/src/lib/origin.ts:
--------------------------------------------------------------------------------
1 | import { PluginOptions } from "./client"
2 |
3 | // Old link: 'https://raw.githubusercontent.com/ethereum/remix-plugin/master/projects/client/assets/origins.json'
4 | export const remixOrgins = 'https://gist.githubusercontent.com/EthereumRemix/091ccc57986452bbb33f57abfb13d173/raw/3367e019335746b73288e3710af2922d4c8ef5a3/origins.json'
5 |
6 | /** Fetch the default origins for remix */
7 | export async function getOriginsFromUrl(url: string) {
8 | const res = await fetch(url)
9 | return res.json()
10 | }
11 |
12 |
13 | export function getDevmodeOrigins({ devMode }: Partial>) {
14 | const localhost = devMode.port ? [
15 | `http://127.0.0.1:${devMode.port}`,
16 | `http://localhost:${devMode.port}`,
17 | `https://127.0.0.1:${devMode.port}`,
18 | `https://localhost:${devMode.port}`,
19 | ] : []
20 | const devOrigins = devMode.origins
21 | ? (typeof devMode.origins === 'string') ? [devMode.origins] : devMode.origins
22 | : []
23 | return [ ...localhost, ...devOrigins ]
24 | }
25 |
26 |
27 | /**
28 | * Check if the sender has the right origin
29 | * @param origin The origin of the incoming message
30 | * @param options client plugin options
31 | */
32 | export async function checkOrigin(origin: string, options: Partial> = {}) {
33 | let origins = []
34 | if (options.allowOrigins) {
35 | if (Array.isArray(options.allowOrigins)) {
36 | origins = origins.concat(options.allowOrigins)
37 | } else {
38 | const allOrigins = await getOriginsFromUrl(options.allowOrigins)
39 | origins = origins.concat(allOrigins)
40 | }
41 | } else if (options.devMode) {
42 | const devModes = getDevmodeOrigins(options)
43 | origins = origins.concat(devModes)
44 | } else {
45 | return true
46 | }
47 | return origins.includes(origin)
48 | }
49 |
--------------------------------------------------------------------------------
/packages/api/doc/unit-testing.md:
--------------------------------------------------------------------------------
1 | # Content Import
2 |
3 | - Name in Remix: `solidityUnitTesting`
4 | - kind: `unitTesting`
5 |
6 | |Type |Name |Description |
7 | |---------|-----------------------|------------|
8 | |_method_ |`testFromPath` |Run a solidity test that is inside the file system
9 | |_method_ |`testFromSource` |Run a solidity test file from the source
10 |
11 | ## Examples
12 |
13 | ### Methods
14 | `testFromPath`: Run a solidity test that is inside the file system
15 | ```typescript
16 | const path = "browser/ballot_test.sol"
17 |
18 | const result = await client.call('solidityUnitTesting', 'testFromPath', path)
19 | // OR
20 | const result = await client.solidityUnitTesting.testFromPath(path)
21 | ```
22 |
23 | `testFromSource`: Run a solidity test file from the source
24 | ```typescript
25 | const testFile = `
26 | pragma solidity >=0.5.0 <0.6.0;
27 | import "remix_tests.sol";
28 | import "./HelloWorld.sol"; // HelloWorl.sol must be in "browser"
29 |
30 | contract HelloWorldTest {
31 | HelloWorld helloWorld;
32 | function beforeEach() public {
33 | helloWorld = new HelloWorld();
34 | }
35 |
36 | function checkPrint () public {
37 | string memory result = helloWorld.print();
38 | Assert.equal(result, string('Hello World!'), "Method 'print' should return 'Hello World!'");
39 | }
40 | }
41 | `
42 |
43 | const result = await client.call('solidityUnitTesting', 'testFromSource', testFile)
44 | // OR
45 | const result = await client.solidityUnitTesting.testFromSource(testFile)
46 | ```
47 |
48 | ## Types
49 | `ContentImport`: An object that describes the returned file
50 | ```typescript
51 | export interface UnitTestResult {
52 | totalFailing: number
53 | totalPassing: number
54 | totalTime: number
55 | errors: UnitTestError[]
56 | }
57 | ```
58 |
59 | > Type Definitions can be found [here](../src/lib/unit-testing/type.ts)
--------------------------------------------------------------------------------
/packages/api/src/lib/file-system/file-manager/api.ts:
--------------------------------------------------------------------------------
1 | import { Folder } from './type'
2 | import { StatusEvents } from '@remixproject/plugin-utils'
3 |
4 | export interface IFileSystem {
5 | events: {
6 | currentFileChanged: (file: string) => void
7 | fileSaved: (file: string) => void
8 | fileAdded: (file: string) => void
9 | folderAdded: (file: string) => void
10 | fileRemoved: (file: string) => void
11 | fileClosed: (file: string) => void
12 | noFileSelected: ()=> void
13 | fileRenamed: (oldName: string, newName:string, isFolder: boolean) => void
14 | } & StatusEvents
15 | methods: {
16 | /** Open the content of the file in the context (eg: Editor) */
17 | open(path: string): void
18 | /** Set the content of a specific file */
19 | writeFile(path: string, data: string): void
20 | /** Return the content of a specific file */
21 | readFile(path: string): string
22 | /** Change the path of a file */
23 | rename(oldPath: string, newPath: string): void
24 | /** Upsert a file with the content of the source file */
25 | copyFile(src: string, dest: string): void
26 | /** Create a directory */
27 | mkdir(path: string): void
28 | /** Get the list of files in the directory */
29 | readdir(path: string): string[]
30 | /** Removes a file or directory recursively */
31 | remove(path: string): void
32 | /** Get the name of the file currently focused if any */
33 | getCurrentFile(): string
34 | /** close all files */
35 | closeAllFiles(): void
36 | /** close a file */
37 | closeFile(): void
38 | // Old API
39 | /** @deprecated Use readdir */
40 | getFolder(path: string): Folder
41 | /** @deprecated Use readFile */
42 | getFile(path: string): string
43 | /** @deprecated Use writeFile */
44 | setFile(path: string, content: string): void
45 | /** @deprecated Use open */
46 | switchFile(path: string): void
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/packages/utils/src/lib/types/api.ts:
--------------------------------------------------------------------------------
1 | import { StatusEvents } from './status'
2 |
3 | export interface Api {
4 | events: {
5 | [key: string]: (...args: any[]) => void
6 | } & StatusEvents
7 | methods: {
8 | [key: string]: (...args: any[]) => void
9 | }
10 | }
11 |
12 | // Used by plugin.on and plugin.emit
13 | export type EventKey = Extract
14 | export type EventParams> = T extends Api
15 | ? Parameters
16 | : any[]
17 | export type EventCallback> = T extends Api
18 | ? T['events'][K]
19 | : (...payload: any[]) => void
20 |
21 | // Used by plugin.call
22 | export type MethodKey = Extract
23 | export type MethodParams> = T extends Api
24 | ? Parameters
25 | : any[]
26 |
27 | /////////
28 | // API //
29 | /////////
30 |
31 | // Get the events of the Api
32 | export interface EventApi {
33 | on: >(name: event, cb: T['events'][event]) => void
34 | }
35 | // Get the methods of the Api
36 | export type MethodApi = {
37 | [m in Extract]: (
38 | ...args: Parameters
39 | ) => Promise>
40 | }
41 | // Create the Api interface
42 | export type CustomApi = EventApi & MethodApi
43 |
44 | /** A map of Api used to describe all the plugin's api in the project */
45 | export type ApiMap = Readonly>
46 |
47 | /** A map of plugin based on the ApiMap. It enforces the PluginEngine */
48 | export type PluginApi = {
49 | [name in keyof T]: CustomApi
50 | }
51 |
52 | // The interface that a Plugin should implement
53 | export type API = {
54 | [M in keyof T['methods']]: T['methods'][M] | Promise
55 | }
56 |
--------------------------------------------------------------------------------
/examples/example/engine/web/src/app/plugins/theme.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { ThemePlugin } from '@remixproject/engine-web';
3 | import { Engine } from '../engine';
4 |
5 | const light = {
6 | brightness: 'light',
7 | fontFamily: 'Arial,"Helvetica Neue",Helvetica,sans-serif',
8 | colors: {
9 | surface: 'white',
10 | background: '#fafafa', // light grey
11 | foreground: 'black',
12 | primary: '#3f51b5', // indigo
13 | primaryContrast: 'white',
14 | secondary: '#e91e63', // pink
15 | secondaryContrast: 'rgba(white, 0.7)',
16 | success: '#4caf50', // green
17 | successContrast: 'rgba(black, 0.87)',
18 | warn: '#ff9800', // orange
19 | warnContrast: 'white',
20 | error: '#f44336', // red
21 | errorContrast: 'white',
22 | disabled: 'rgba(0,0,0,.26)',
23 | },
24 | } as const;
25 |
26 | const dark = {
27 | brightness: 'dark',
28 | fontFamily: 'Arial,"Helvetica Neue",Helvetica,sans-serif',
29 | colors: {
30 | surface: 'white',
31 | background: '#1E1E1E',
32 | foreground: 'fafafa',
33 | primary: '#3f51b5', // indigo
34 | primaryContrast: 'white',
35 | secondary: '#e91e63', // pink
36 | secondaryContrast: 'rgba(white, 0.7)',
37 | success: '#4caf50', // green
38 | successContrast: 'rgba(black, 0.87)',
39 | warn: '#ff9800', // orange
40 | warnContrast: 'white',
41 | error: '#f44336', // red
42 | errorContrast: 'white',
43 | disabled: 'rgba(0,0,0,.26)',
44 | },
45 | } as const;
46 |
47 | @Injectable({ providedIn: 'root' })
48 | export class Theme extends ThemePlugin {
49 | private themes = { dark, light };
50 | constructor(private engine: Engine) {
51 | super()
52 | this.engine.register(this);
53 | }
54 |
55 | selectTheme(brightness: 'dark' | 'light') {
56 | const theme = this.themes[brightness];
57 | this.setTheme(theme);
58 | }
59 |
60 | onActivation() {
61 | this.selectTheme('dark');
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/packages/engine/web/tests/view.spec.ts:
--------------------------------------------------------------------------------
1 | import { Engine, PluginManager } from '../../core/src'
2 | import { HostPlugin, ViewPlugin } from '../src'
3 | import { pluginManagerProfile } from '@remixproject/plugin-api'
4 |
5 | export class MockEngine extends Engine {
6 | onRegistration = jest.fn()
7 | }
8 |
9 | class MockManager extends PluginManager {
10 | constructor() {
11 | super(pluginManagerProfile)
12 | }
13 | }
14 |
15 | class MockHost extends HostPlugin {
16 | currentFocus = jest.fn()
17 | isFocus = jest.fn() // (name: string) =>
18 | focus = jest.fn() // (name: string) =>
19 | addView = jest.fn() // (profile: Profile) =>
20 | removeView = jest.fn() // (name: string) =>
21 | constructor() {
22 | super({ name: 'sidePanel', methods: [] })
23 | }
24 | }
25 |
26 | class MockView extends ViewPlugin {
27 | render = jest.fn(() => ({ id: 'element' } as Element))
28 | onActivation = jest.fn()
29 | onDeactivation = jest.fn()
30 | constructor() {
31 | super({ name: 'view', methods: [], location: 'sidePanel' })
32 | }
33 | }
34 |
35 | describe('View Plugin', () => {
36 | let manager: MockManager
37 | let host: MockHost
38 | let view: MockView
39 | beforeEach(() => {
40 | view = new MockView()
41 | host = new MockHost()
42 | manager = new MockManager()
43 | const engine = new MockEngine()
44 | engine.register([manager, view, host])
45 | })
46 |
47 | test('Activate View call host addView', async () => {
48 | await manager.activatePlugin(['sidePanel', 'view'])
49 | expect(host.addView).toHaveBeenCalledWith(view.profile, { id: 'element' })
50 | expect(view.onActivation).toHaveBeenCalledTimes(1)
51 | })
52 |
53 | test('Deactive View call host removeView', async () => {
54 | await manager.activatePlugin(['sidePanel', 'view'])
55 | await manager.deactivatePlugin('view')
56 | expect(host.removeView).toHaveBeenCalledWith(view.profile)
57 | expect(view.onDeactivation).toHaveBeenCalledTimes(1)
58 | })
59 | })
--------------------------------------------------------------------------------
/packages/engine/web/src/lib/theme.ts:
--------------------------------------------------------------------------------
1 | import { Plugin } from '@remixproject/engine'
2 | import { MethodApi } from '@remixproject/plugin-utils'
3 | import { ITheme, Theme, themeProfile } from '@remixproject/plugin-api'
4 |
5 | type DeepPartial = {
6 | [P in keyof T]?: DeepPartial;
7 | };
8 |
9 | /**
10 | * Utils function to create a theme with default value
11 | * Default values are taken from material design with colors
12 | * - primary: indigo
13 | * - secondary: pink
14 | * - warn: orange
15 | * - error: red
16 | */
17 | export function createTheme(params: DeepPartial = {}): Theme {
18 | return {
19 | brightness: 'light',
20 | fontFamily: 'Arial,"Helvetica Neue",Helvetica,sans-serif',
21 | space: 8,
22 | ...params,
23 | breakpoints: {
24 | xs: 0,
25 | sm: 600,
26 | md: 1024,
27 | lg: 1440,
28 | xl: 1920,
29 | ...params.breakpoints
30 | },
31 | colors: {
32 | surface: 'white',
33 | background: '#fafafa', // light grey
34 | foreground: 'black',
35 | primary: '#3f51b5', // indigo
36 | primaryContrast: 'white',
37 | secondary: '#e91e63', // pink
38 | secondaryContrast: 'rgba(white, 0.7)',
39 | success: '#4caf50', // green
40 | successContrast: 'rgba(black, 0.87)',
41 | warn: '#ff9800', // orange
42 | warnContrast: 'white',
43 | error: '#f44336', // red
44 | errorContrast: 'white',
45 | disabled: 'rgba(0,0,0,.26)',
46 | ...params.colors
47 | },
48 | }
49 | }
50 |
51 | export class ThemePlugin extends Plugin implements MethodApi {
52 | protected getTheme = createTheme
53 | protected theme: Theme
54 | constructor() {
55 | super(themeProfile)
56 | this.theme = this.getTheme()
57 | }
58 |
59 | /** Internal API to set the current theme */
60 | setTheme(theme: DeepPartial) {
61 | this.theme = this.getTheme(theme)
62 | this.emit('themeChanged', this.theme)
63 | }
64 |
65 | /** External API to get the current theme */
66 | async currentTheme(): Promise {
67 | return this.theme
68 | }
69 |
70 | }
--------------------------------------------------------------------------------
/packages/plugin/child-process/src/lib/connector.ts:
--------------------------------------------------------------------------------
1 | import { ClientConnector, connectClient, applyApi, Client, PluginClient } from '@remixproject/plugin'
2 | import type { Message, Api, ApiMap } from '@remixproject/plugin-utils'
3 | import { IRemixApi } from '@remixproject/plugin-api'
4 |
5 |
6 | export interface WS {
7 | send(data: string): void
8 | on(type: 'message', cb: (event: string) => any): this
9 | }
10 |
11 | /**
12 | * This Websocket connector works with the library `ws`
13 | */
14 | export class WebsocketConnector implements ClientConnector {
15 |
16 | constructor(private websocket: WS) {}
17 |
18 | /** Send a message to the engine */
19 | send(message: Partial) {
20 | this.websocket.send(JSON.stringify(message))
21 | }
22 |
23 | /** Get messae from the engine */
24 | on(cb: (message: Partial) => void) {
25 | this.websocket.on('message', (event) => {
26 | try {
27 | cb(JSON.parse(event))
28 | } catch (e) {
29 | console.error(e)
30 | }
31 | })
32 | }
33 | }
34 |
35 | /**
36 | * Connect a Websocket plugin client to a web engine
37 | * @param client An optional websocket plugin client to connect to the engine.
38 | *
39 | * ---------
40 | * @example
41 | * ```typescript
42 | * const wss = new WebSocket.Server({ port: 8080 });
43 | * wss.on('connection', (ws) => {
44 | * const client = createClient(ws)
45 | * })
46 | * ```
47 | * ---------
48 | * @example
49 | * ```typescript
50 | * class MyPlugin extends PluginClient {
51 | * methods = ['hello']
52 | * hello() {
53 | * console.log('Hello World')
54 | * }
55 | * }
56 | * const wss = new WebSocket.Server({ port: 8080 });
57 | * wss.on('connection', (ws) => {
58 | * const client = createClient(ws, new MyPlugin())
59 | * })
60 | * ```
61 | */
62 | export const createClient = <
63 | P extends Api,
64 | App extends ApiMap = Readonly
65 | >(websocket: WS, client: PluginClient = new PluginClient()): Client
=> {
66 | const c = client as any
67 | connectClient(new WebsocketConnector(websocket), c)
68 | applyApi(c)
69 | return c
70 | }
71 |
--------------------------------------------------------------------------------
/packages/plugin/electron/src/lib/electronBasePlugin.ts:
--------------------------------------------------------------------------------
1 | import { Plugin } from "@remixproject/engine";
2 | import { PluginClient } from "@remixproject/plugin";
3 | import { Profile } from "@remixproject/plugin-utils";
4 | import { BrowserWindow } from "electron";
5 | import { createElectronClient } from "./electronPluginClient";
6 |
7 | export interface ElectronBasePluginInterface {
8 | createClient(windowId: number): Promise;
9 | closeClient(windowId: number): Promise;
10 | }
11 |
12 | export abstract class ElectronBasePlugin extends Plugin implements ElectronBasePluginInterface {
13 | clients: ElectronBasePluginClient[] = [];
14 | clientClass: any
15 | clientProfile: Profile
16 | constructor(profile: Profile, clientProfile: Profile, clientClass: any) {
17 | super(profile);
18 | this.methods = ['createClient', 'closeClient'];
19 | this.clientClass = clientClass;
20 | this.clientProfile = clientProfile;
21 | }
22 |
23 | async createClient(webContentsId: number): Promise {
24 | if (this.clients.find(client => client.webContentsId === webContentsId)) return true
25 | const client = new this.clientClass(webContentsId, this.clientProfile);
26 | this.clients.push(client);
27 | return new Promise((resolve, reject) => {
28 | client.onload(() => {
29 | resolve(true)
30 | })
31 | })
32 | }
33 | async closeClient(windowId: number): Promise {
34 | this.clients = this.clients.filter(client => client.webContentsId !== windowId)
35 | return true;
36 | }
37 | }
38 |
39 | export class ElectronBasePluginClient extends PluginClient {
40 | window: Electron.BrowserWindow;
41 | webContentsId: number;
42 | constructor(webcontentsid: number, profile: Profile, methods: string[] = []) {
43 | super();
44 | this.methods = profile.methods;
45 | this.webContentsId = webcontentsid;
46 | BrowserWindow.getAllWindows().forEach((window) => {
47 | if (window.webContents.id === webcontentsid) {
48 | this.window = window;
49 | }
50 | });
51 | createElectronClient(this, profile, this.window);
52 | }
53 | }
54 |
55 |
--------------------------------------------------------------------------------
/packages/engine/core/doc/connector/plugin-connector.md:
--------------------------------------------------------------------------------
1 | # Develop & Publish a Plugin Connector
2 |
3 | ## Install
4 | ```bash
5 | npm install @remixproject/engine@next
6 | ```
7 |
8 | ## Create your connector
9 | Create a file `index.ts`
10 |
11 | ```typescript
12 | import { PluginConnector, Profile, ExternalProfile, Message } from '@remixproject/engine'
13 | import io from 'socket.io-client';
14 |
15 | export class SocketIOPlugin extends PluginConnector {
16 | socket: SocketIOClient.Socket
17 |
18 | constructor(profile: Profile & ExternalProfile) {
19 | super(profile)
20 | }
21 |
22 | protected connect(url: string): void {
23 | this.socket = io(url)
24 | this.socket.on('connect', () => {
25 | this.socket.on('message', (msg: Message) => this.getMessage(msg))
26 | })
27 | }
28 |
29 | protected disconnect(): void {
30 | this.socket.close()
31 | }
32 |
33 | protected send(message: Partial): void {
34 | this.socket.send(message)
35 | }
36 |
37 | }
38 | ```
39 |
40 | ## Build
41 |
42 | ```
43 | npx tsc index --declaration
44 | ```
45 |
46 | ## Package.json
47 | ```json
48 | {
49 | "name": "plugin-connector-socket.io",
50 | "main": "index.js",
51 | "types": "index.d.ts",
52 | "peerDependencies": {
53 | "@remixproject/engine": "next",
54 | "socket.io-client": "^2.3.0"
55 | }
56 | }
57 | ```
58 |
59 |
60 | ## Publish
61 | ```
62 | npm publish
63 | ```
64 |
65 | ----
66 |
67 | # Use a Plugin Connector
68 | Here is how to use your plugin connector in an engine :
69 |
70 | ## Install
71 | ```
72 | npm install @remixproject/engine@next plugin-connector-socket.io socket.io-client
73 | ```
74 |
75 | ## Create a client
76 | ```typescript
77 | import { PluginManager, Engine, Plugin } from '@remixproject/engine'
78 | import { SocketIOPlugin } from 'plugin-connector-socket.io'
79 |
80 | const manager = new PluginManager()
81 | const engine = new Engine()
82 | const plugin = new SocketIOPlugin({ name: 'socket', url: 'http://localhost:3000' })
83 |
84 | // Register plugins
85 | engine.register([manager, plugin])
86 |
87 | // Activate plugins
88 | manager.activatePlugin('socket')
89 | ```
--------------------------------------------------------------------------------
/packages/plugin/core/tests/api.spec.ts:
--------------------------------------------------------------------------------
1 | import { PluginClient, createApi, getApiMap } from '../src'
2 | import { Api, ExternalProfile, CustomApi, callEvent, listenEvent, StatusEvents, Profile, LocationProfile } from '@remixproject/plugin-utils'
3 |
4 | interface TestApi extends Api {
5 | events: {
6 | event: (isTrue: boolean) => void
7 | } & StatusEvents
8 | methods: {
9 | method: (isTrue: boolean) => boolean
10 | }
11 | }
12 |
13 | const profile: Profile & ExternalProfile & LocationProfile = {
14 | name: 'test',
15 | methods: ['method'],
16 | location: 'sidePanel',
17 | url: 'url'
18 | }
19 |
20 | describe('Client Api', () => {
21 | let client: PluginClient
22 | let api: CustomApi
23 | beforeEach(() => {
24 | client = new PluginClient()
25 | client['isLoaded'] = true
26 | api = createApi(client, profile)
27 | })
28 |
29 | test('Should create an Api', () => {
30 | expect(api).toBeDefined()
31 | expect(api.on).toBeDefined()
32 | expect(api.method).toBeDefined()
33 | })
34 |
35 | test('"Method" should send a message', done => {
36 | client.events.on('send', message => {
37 | expect(message).toEqual({
38 | action: 'request',
39 | name: 'test',
40 | key: 'method',
41 | payload: [true],
42 | id: 1,
43 | })
44 | done()
45 | })
46 | api.method(true)
47 | })
48 |
49 | test('"Method" should return a promise', done => {
50 | client.events.on('send', ({ name, key, id, payload }) => {
51 | client.events.emit(callEvent(name, key, id), payload[0])
52 | })
53 | api.method(true).then(isTrue => {
54 | expect(isTrue).toBeTruthy()
55 | done()
56 | })
57 | })
58 |
59 | test('"Event" should emit an event', done => {
60 | api.on('event', isTrue => {
61 | expect(isTrue).toBeTruthy()
62 | done()
63 | })
64 | client.events.emit(listenEvent('test', 'event'), true)
65 | })
66 |
67 | test('getApiMap returns a map of API', () => {
68 | const { test } = getApiMap(client, { test: profile })
69 | expect(test.on).toBeDefined()
70 | expect(test.method).toBeDefined()
71 | })
72 |
73 | })
74 |
--------------------------------------------------------------------------------
/packages/api/doc/udapp.md:
--------------------------------------------------------------------------------
1 | # Udapp
2 |
3 | - Name in Remix: `udapp`
4 | - kind: `udapp`
5 |
6 | The udapp exposes an interface for interacting with the account and transaction.
7 |
8 | |Type |Name |Description
9 | |---------|---------------------|--
10 | |_event_ |`newTransaction` |Triggered when a new transaction has been sent.
11 | |_method_ |`sendTransaction` |Send a transaction **only for testing networks**.
12 | |_method_ |`getAccounts` |Get an array with the accounts exposed.
13 | |_method_ |`createVMAccount` |Add an account if using the VM provider.
14 |
15 | ## Examples
16 |
17 | ### Events
18 | `newTransaction`: Triggered when a new transaction has been sent.
19 | ```typescript
20 | client.udapp.on('newTransaction', (tx: RemixTx) => {
21 | // Do something
22 | })
23 | // OR
24 | client.on('udapp', 'newTransaction', (tx: RemixTx) => {
25 | // Do something
26 | })
27 | ```
28 |
29 | ### Methods
30 | `sendTransaction`: Send a transaction **only for testing networks**.
31 | ```typescript
32 | const transaction: RemixTx = {
33 | gasLimit: '0x2710',
34 | from: '0xca35b7d915458ef540ade6068dfe2f44e8fa733c',
35 | to: '0xca35b7d915458ef540ade6068dfe2f44e8fa733c'
36 | data: '0x...',
37 | value: '0x00',
38 | useCall: false
39 | }
40 | const receipt = await client.udapp.sendTransaction(transaction)
41 | // OR
42 | const receipt = await client.call('udapp', 'sendTransaction', transaction)
43 | ```
44 |
45 | `getAccounts`: Get an array with the accounts exposed.
46 | ```typescript
47 | const accounts = await client.udapp.getAccounts()
48 | // OR
49 | const accounts = await client.call('udapp', 'getAccounts')
50 | ```
51 |
52 | `createVMAccount`: Add an account if using the VM provider.
53 | ```typescript
54 | const privateKey = "71975fbf7fe448e004ac7ae54cad0a383c3906055a75468714156a07385e96ce"
55 | const balance = "0x56BC75E2D63100000"
56 | const address = await client.udapp.createVMAccount({ privateKey, balance })
57 | // OR
58 | const address = await client.call('udapp', 'createVMAccount', { privateKey, balance })
59 | ```
60 |
61 | ## Types
62 | `RemixTx`: A modified version of the transaction for Remix.
63 | `RemixTxReceipt`: A modified version of the transaction receipt for Remix.
64 |
65 |
66 | > Type Definitions can be found [here](../src/lib/udapp/type.ts)
--------------------------------------------------------------------------------
/packages/engine/core/doc/connector/client-connector.md:
--------------------------------------------------------------------------------
1 | # Develop & Publish a Client Connector
2 |
3 | ## Install
4 | ```bash
5 | npm install @remixproject/plugin@next
6 | ```
7 |
8 | ## Create your connector
9 | Create a file `index.ts`
10 |
11 | ```typescript
12 | import { ClientConnector, createConnectorClient, PluginClient, Message } from '@remixproject/plugin'
13 |
14 | export class SocketIOConnector implements ClientConnector {
15 |
16 | constructor(private socket) {}
17 | send(message: Partial) {
18 | this.socket.emit('message', message)
19 | }
20 | on(cb: (message: Partial) => void) {
21 | this.socket.on('message', (msg) => cb(msg))
22 | }
23 | }
24 |
25 | // A simple wrapper function for the plugin developer
26 | export function createSocketIOClient(socket, client?: PluginClient) {
27 | const connector = new SocketIOConnector(socket)
28 | return createConnectorClient(connector, client)
29 | }
30 | ```
31 |
32 | ## Build
33 |
34 | ```
35 | npx tsc index --declaration
36 | ```
37 |
38 | ## Package.json
39 | ```json
40 | {
41 | "name": "client-connector-socket.io",
42 | "main": "index.js",
43 | "types": "index.d.ts",
44 | "dependencies": {
45 | "@remixproject/plugin": "next"
46 | },
47 | "peerDependencies": {
48 | "socket.io": "^2.3.0"
49 | }
50 | }
51 | ```
52 |
53 | Some notes here :
54 | - We use `dependancies` for `@remixproject/plugin` as this is the base code for your connector.
55 | - We use `peerDependencies` for the library we wrap (here `socket.io`), as we want to let the user choose his version of it.
56 |
57 | ## Publish
58 | ```
59 | npm publish
60 | ```
61 |
62 | ----
63 |
64 | # Use a Client Connector
65 | Here is how to use your client connector in a plugin :
66 |
67 | ## Install
68 | ```
69 | npm install client-connector-socket.io socket.io
70 | ```
71 |
72 | ## Create a client
73 | This example is an implementation of the [Server documentation from socket.io](https://socket.io/docs/server-api/).
74 | ```typescript
75 | const { createSocketIOClient } = require('client-connector-socket.io')
76 | const http = require('http').createServer();
77 | const io = require('socket.io')(http);
78 |
79 | io.on('connection', async (socket) => {
80 | const client = createSocketIOClient(socket)
81 | await client.onload()
82 | const code = await client.call('fileManager', 'read', 'Ballot.sol')
83 | });
84 |
85 | http.listen(3000);
86 | ```
--------------------------------------------------------------------------------