Blog Post

Writing Scripts As Frontend With Es6 And Jest

Context

As frontend developer when I want to extract information from a website to then inject into a document (usually markdown one), I do a bit of web scraping to understand how to get the data with the CSS selectors (e.g. classNames) of the elements I need from the webpage, and then I execute a simple script in the console like the following one:

copy(
  Array.from(document.querySelectorAll('.row-title'))
    .map(e => e.textContent)
    .join(';;')
);

Then in Sublime or VS Code, I paste them using those junctions chars ';;' and multi cursor capabilities of those editors to edit them as I want to give it the markdown format I need.

This time I wanted to extract the technologies of webpages with links to them to be saved in a document. But wappalyzer offers browser extensions and we can't access to the popover content of the browser extension from the console, so no possibility to extract the information from console script as I use to do.

Mission

I found this npm node module, and it opened my mind on how was the best way to use it as end-user to get the technologies used on websites and paste it into the clipboard.

I've created a lot of .sh in my career to automate dev processes, deployments, repeatable tasks using sometimes even AppleScript , but surprisingly for me, I never thought to use my web developer toolset (javascript and npm in this case) to write "desktop scripts". So npm scripts and this wappalyzer node module was a good start to build something to be executed with npx.

I wanted to write a script to extract the technologies of any page with npm to be executed through npx and get the output from wappalyzer to the clipboard in markdown.

So also the point of this "exercise" was not only to resolve it but with some restrictions to learn something in the way.

  • I wanted unit tests.
  • I wanted to use modules and last es6 syntax not just (it was executed inside node, so I knew it was going to be a pain point).
  • Register it to npm register so anyone can use it in the future or by anyone else.
  • And of course publish it in Github, so maybe someone can use it or make modifications to it in the future.

Toolset and Planning

I found this article"Building a simple command line tool with npm", but was not giving me es6 modules, babel, eslint and jest. I learned how to expose npm scripts through bin property in package.json. Those scripts we link will need to have a Shebang to be executed as node scripts.

Then I knew to use last es6 syntax, node modules imports, prettier, ... I needed a tool to compile the js and my first choose was webpack, as I use to do in my web projects. But webpack it's created to build web outputs and I had to hack it to don't expect HTML, CSS and accept scripts with Shebang in the .js so I finally decided to use simple babel-cli.

Jest, was a pretty clear decision for the unit tests, without any additional configuration, as it's super valid to test functions and we don't need anything specific here like enzyme or React Testing Library.

The last piece of the puzzle was how to copy things on the clipboard of the machine, as I know in the browser console there is this copy but was not available within node. But again a ready to use node module was there to do the job (clipboardy)

Implementation

So once I was clear how to build the project, I just needed to start.

Project Init

Just created a simple and empty new folder wappalyzer-to-md and npm npm init did the rest.

mkdir wappalyzer-to-md; cd wappalyzer-to-md; npm init

Configuration .babelrc and eslint

So now let's add the .babelrc and eslint configuration:

First we install what we need for our configuration:

npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/node

And the .babelrc file looks like that:

{
  "presets": ["@babel/preset-env"]
}

Then the eslint ones, as I'm unable to work nowadays without eslint + prettier. So again with dev dependencies:

npm install --save-dev babel-eslint eslint eslint-config-node eslint-config-prettier eslint-plugin-prettier babel-loader babel-polyfill

And again the configuration file (.eslintrc) was something like that, after some specific changes for this project:

{
  "extends": ["node", "prettier"],
  "parser": "babel-eslint",
  "parserOptions": {
    "ecmaVersion": 8,
    "ecmaFeatures": {
      "experimentalObjectRestSpread": true,
      "impliedStrict": true,
      "classes": true
    }
  },
  "env": {
    "es6": true,
    "browser": false,
    "jasmine": true,
    "node": true,
    "commonjs": false,
    "jest": true
  },
  "rules": {
    "no-unused-vars": [
      1,
      {
        "ignoreRestSiblings": true,
        "argsIgnorePattern": "^ignore",
        "varsIgnorePattern": "^ignore",
        "caughtErrorsIgnorePattern": "^ignore"
      }
    ],
    "arrow-body-style": [2, "as-needed"],
    "no-param-reassign": [
      2,
      {
        "props": false
      }
    ],
    "no-console": 0,
    "import": 0,
    "func-names": 0,
    "space-before-function-paren": 0,
    "max-len": 0,
    "no_underscore-dangle": 0,
    "consistent-return": 0,
    "comma-dangle": 0,
    "import/prefer-default-export": 0,
    "array-bracket-spacing": 0,
    "space-in-parens": 0,
    "prefer-arrow-callback": 0,
    "no-plusplus": 0,
    "no-use-before-define": 0,
    "global-require": 1,
    "import/no-commonjs": 0,
    "import/no-extraneous-dependencies": [
      "error",
      {
        "devDependencies": true
      }
    ],
    "no-process-exit": 0
  },
  "plugins": ["prettier"]
}

Libraries to use and package setup npm build

Then we needed to be able to use babel transformation process to build the ./src, and connect it in the bin script.

{
  // ...
  "scripts": {
    "build": "babel src --out-dir dist",
    "test": "jest --watchAll"
  },
  // ...
  "bin": {
    "wappalyzer-to-md": "./dist/cli.js"
  },
  // ...
}

With the previous configuration, we are able to execute the babel compiled sources. And we can update them each time we build the project.

So let's create also our first file which will be our start point of the script /src/cli.js:

console.log('-- Starting Execution --');
const [, , url] = process.argv;

(async () => {
  try {
    // 1.- We need to validate params
    // 2.- Make the call to the sever side to check
    // 3.- Transform it to markdown
    // 4.- Copy it to the clipboard

    console.log(' -- 📋 Markdown Copied 📋 -- ');
  } catch (e) {
    console.error(' --💥 Something when wrong 💥-- ');
    console.error(e);
  } finally {
    process.exit(0);
  }
})();

We are not only creating the file but defining the "script workflow" with it splitting it to be able to work with tests in the correct way 💁‍.

Executing the project with npm link

npm link it's a great way to test and use npm modules before registering them.

To execute the previous example we just need to execute in the console in our project folder.

npm link

Then we are able to execute it with:

npx wappalyzer-to-md

And get the console output in the console.

-- Starting Execution --
-- 📋 Markdown Copied 📋 --

Jest setup npm test

Let's just add the uni test configuration to be able to implement each part of the workflow described previously with unit test. For it let's install jest configuration

npm install --save-dev jest babel-jest

And having the jest --watchAll in the test script made me able to execute npm test

I got these issues: ReferenceError: regeneratorRuntime is not defined described here and I got it resolved by specifying the target for babel in the .babelrc.

{
  "presets": [
    [
      "@babel/preset-env", {
        "targets": {
          "node": "current"
        }
      }
    ]
  ]
}

Then I was able to start coding with npm test running as usual with any js project.

Implementing the plan

For the implementation, we need only 3 libraries the two already described wappalyzer and clipboardy and the indispensable lodash one.

npm install lodash clipboardy wappalyzer

You have the full project available in my Github account to check it, use it, adapt or improve it.

Basically, I created the following folder structure for each of the steps described in the cli.js backbone:

  • validation: To have params validations, as in the cli the inputs are coming from the call args.
  • data: To wrap the logic of using the Wappalyzer npm module and test it.
  • logic: for me, the logic in this program was to transfer the Json output from Wappalyzer to the "standard" markdown I was trying to create. Pretty straight and quite exciting to create in an elegant way 🤓
  • output: in the script, we haven't viewed, but we do have output logic, so again I decided to isolate the clipboardy logic and wrap in the logic folder.

These are the main parts of the small script project, I think the best way to know how each part works, as I think we should always do, it's just to read the specs test on each folder to then understand then easy cli.js file which orchestrates the script pieces.

Additionally, to execute before register the script project, we should npm link the project an execute it again now with valid params like:

npx wappalyzer-to-md https://robertovg.com

And we will have in the clipboard a markdown document with what we wanted to extract 🥳.

Register in npmjs.com to be widely used 🤔.

So last step was to register to npm, so we can use it without the sources, that's pretty sexy approach and honestly the first time I think to register on npm something which is not created to be used as node module but as npm script.

Pretty simple, this is done by npm publish as always and first, you need to set up an account in npmjs.com and log to npm through npm login call.

Then the package it's available for anyone calling

npx wappalyzer-to-md <url>

And listed publicly in npm register.

Conclusion

Nothing crazy here, but funny for me to use my web toolset to make a script. I will try to use this approach when I see the opportunity in the future if I need to resolve scripting challenges again.

Have you ever use npm script in this way? Do you see improvements in the way I use it? Or maybe do you have any doubt? I'm always happy to learn from any feedback and hopefully, this can be useful not just for me but for anyone facing problems which could be resolved by npm scripts.

I hope you enjoy reading 🤗.

Go back to the blog section.

You can follow me