npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

tohru

v1.2.2

Published

Extandable Electron flow desinger

Downloads

27

Readme

Tohru

Tohru

Extendable Electron flow designer. Tool allowing in easy way inject and run code in Electron.

Why this library exists?

  • Sadly, NightmareJS is very bugged
  • Most of this kind tools have huge API which it's hard to reuse and fit to mine scripts
  • Wrapping all possible actions is impossible and preparing such wrappers consume time and need changes in multiple files even for that simplest one

Goals

  • Pluginable and easy to hack API, to allow for even most edgy cases
  • Getting easily context of Electron's host, Electron's browser and Tohru itself
  • Queueable flow with hard and soft crashes
  • Way to catch each type of result
  • Uses Electron executable of user choice
  • Contains a lot of love ❤️

Getting started

Install Tohru and Electron:

npm install --save tohru electron
# or
yarn add tohru electron

Set-up your code, simplest example:

const { Tohru } = require('tohru');
// Outside of Electron's processes will return string with path of Electron
const electron = require('electron');

Tohru({ electron })
    .goto('https://google.com/')
    .wait('[name="q"]')
    .type('[name="q"]', 'My awesome query')
    .submit('form')
    .wait(1000)
    .end();

Enjoy browser automation!

API

API base on actions model, flow of this model looks like this:

  1. Register action - which will prepare initializer and action itself.
  2. Use registered action with needed arguments.
  3. Tohru will add this action into queue, and will run it after previously actions.

Instance

Tohru instance can be only created via exported function

// ES6+ or TypeScript
import { Tohru } from 'tohru';
// ES5
const { Tohru } = require('tohru');

const tohruInstance = Tohru({
    // settings
});

Every action returns actions list, so you can to make chain

Settings

Settings can contains given properties:

  • electron: string - electron executable path
  • tickRate?: number - time in ms between actions, default: 500
  • timeout?: number - time in ms how long every action can work, after that time error will be thrown, default: 10000
  • typeInterval?: number - time in ms how long .type() should wait before firing each key, default: 100
  • pollInterval?: number - time in ms how long .wait() should wait before each searching, default: 50
  • logLevel?: LogLevels - level of reporting, all equal and higher messages will be reported, default: 3
    • 5 - None
    • 4 - Critical
    • 3 - Error
    • 2 - Warning
    • 1 - Info
    • 0 - Debug
  • throwLevel?: LogLevels - level of reporting which throw an error and halt app, default: 2
    • same like above
  • defaultLogger?: boolean - if true Tohru will setup inner logger for loggin purposes, default: false
  • requirePath?: string - directory of your project, this path will be used to run require from your project, default: process.cwd()
  • assetsPath?: string - root path for uploads, default: process.cwd()

Actions

Actions are registered only in one instance of Tohru at the same time, so if you need to run those actions in multiple instances, you will need to prepare setup function. Registering such action looks like this:

tohruInstance.action('actionName', (context, params) => {
    // Here execute and return some async action or do whatever you need
});

So for example .waitSomeTime action can looks lioke this:

tohruInstance.action('waitSomeTime', (context, params) => {
    return new Promise(res => {
        setTimeout(res, 1000);
    });
});

// then you can use it:

tohruInstance
    .goto('https://goole.com/')
    .waitSomeTime()
    .end();

Ok, but if you want to get two arguments: time and callback:

tohruInstance.action('waitSomeTime', (context, params) => {
    return new Promise(res => {
        setTimeout(() => {
            res();
            params[1]();
        }, params[0]);
    });
});

// then you can use it:

tohruInstance
    .goto('https://goole.com/')
    .waitSomeTime(1000, () => {
        console.log('hello after 1000ms :D');
    })
    .end();

In all those examples is also context, which is descibed below.

Internal Actions

Few actions are registered at start of Tohru. Some of those actions are prepared, some are protected.

then(cb) (protected) - setup callback function that will be runned once when queue of actions will be empty

  • cb: () => void - callback

catch(cb) (protected) - setup callback function that will be runned once when critical error occurs

  • cb: (message: string) => void - callback

end() (protected) - closes all browsers and servers attached to this tohru instance


action(name, cb) (protected) - push new action

  • name: string - unique name of action
  • cb: (...params) => any - callback with params of your choice and return promise-like or just plain value

goto(url) - redirects to given path

  • url: string

type(selector, text) - simulates human-way of typing text

  • selector: string - selector, target of events
  • text: string

wait(target) - waits until given selector will be available or given time in ms

  • target: string | number - selector string or time in ms

click(selector) - clicks element

  • selector: string - selector, target of events

clickAll(selector) - clicks all elements

  • selector: string - selector, target of events

authentication(login, password) - allows pass though first native authentication prompt

  • login: string
  • password: string

select(selector, option) - select option in select input

  • selector: string
  • option: string | number - visible name, value string or index of option

upload(selector, ...files) - allows upload files into input upload type

  • selector: string
  • ...files: string[] - relative paths for images from assets path (check initialization options)

Context

It's most magic thing in whole project. Allows you to eval (yes, it's this prohibited function) code inside of Electron "host" and Browser window aka "client". So let's hack!

host(cb:, ...params) => Promise<unknown>

Allows you to execute code inside of host, and returns promise. Inside of callback you have access to host scope, so given variables are available:

  • app - Electron.App object
  • window - Electron.BrowserWindow object
  • require - function allowing you to require like in your project

Example usage looks like this:

tohruInstance.action('someAction', ctx => {
    return ctx.host(url => {
        window.loadURL(url);

        return new Promise(res => {
            webContents.once('dom-ready', res);
        });
    }, 'https://google.com/');
});

client(cb, ...params) => Promise<unknown>

Allows you to execute code inside of client, and returns promise. Inside of callback you have access to browser scope, so you can to use all objects like window, document etc., additionaly there are require which allows you to require like in your project. Example usage looks like this:

tohruInstance.action('someAction', ctx => {
    return ctx.client(target => {
        return new Promise((res, rej) => {
            document.querySelector(target) ? res() : rej();
        });
    }, '.button');
});

How it works?

First argument have to be function that will be changed into string and send to host that's why you don't have possibility pass variables thru JS itself.

All next arguments are optional, they allow you to pass variables to host/client scope. They will be used to execute your function from first argument in the same order.

How to operate on such a limited scope?

Here you have little example, how it works:

const x1 = 10;
const x2 = 20;
const x3 = 30;
const x4 = 40;

let y1 = 10;
let y2 = null;
let y3 = null;

const p1 = ctx.clientEval(([ x2, x3 ], x1) => {
    console.log(x1); // => 30
    console.log(x2); // => 20
    console.log(x3); // => 40
    console.log(x4); // => undefined
    y1 += 10; // => Uncaught ReferenceError: y1 is not defined
    return 20;
}, [ x2, x4 ], x3).then(result => {
    y2 = result;
});

const p2 = ctx.hostEval(([ x2, x3 ], x4) => {
    console.log(x1); // => undefined
    console.log(x2); // => 10
    console.log(x3); // => 40
    console.log(x4); // => 30
    y1 += 10; // => Uncaught ReferenceError: y1 is not defined
    return 30;
}, [ x1, x4 ], x3).then(result => {
    y3 = result;
});

Promise.all([p1, p2]).then(() => {
    console.log(y1); // => 10
    console.log(y2); // => 20
    console.log(y3); // => 30
});

I want to require() library X or file Y from my project inside of host/browser

Inside both contextes require() will try to find modules inside of your project directory or from your project's root directory.

Real require() is moved to _require(), so don't worry.

Licence

This project is licensed under the Apache-2.0 license - see the LICENSE.md file for details