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

stacklizard

v0.2.4

Published

A static analysis tool for identifying ancestors of a JavaScript function that must be async if that function becomes async.

Downloads

1

Readme

stacklizard

A static-analysis tool to simulate marking one JavaScript function async, and determining what functions and function calls must change.

A simplified scenario

Suppose you have the following code:

// Goal:  Determine where we have to make changes if A.prototype.e becomes asynchronous.

function A() {
  this.x = this.d();
}
A.prototype = {
  a: function() {
    console.log(this.b());
  },

  b: function() {
    void(this.c);
  },

  get c() {
    return this.d() + 1;
  },

  d: function() {
    // just a comment
    let f = this.f();
    let e = this.e(f);
    return e + 1;
  },

  e: function(y) {
    return y + this.g() + 1;
  },

  f: function() {
    return 0;
  },

  g: function() {
    return 1;
  }
};

const B = new A();

In this scenario, B.d() === 3. Everything's fine. But for some reason, you need to mark A.prototype.e asynchronous:

  e: async function(y) {
    return y + this.g() + 1;
  },

Evaluating B.d() results in B.d() = [object Promise]1. That's not desirable. To fix this, we'd have to make the caller await the promise.

  d: function() {
    // just a comment
    let f = this.f();
    let e = await this.e(f);
    return e + 1;
  },

Except that causes SyntaxError: await is only valid in async functions and async generators. So then we mark d as async:

  d: async function() {
    // just a comment
    let f = this.f();
    let e = await this.e(f);
    return e + 1;
  },

B.d() = [object Promise]

This we can await no problem... except that the constructor A() references this.d(). We broke that as well, so we try to fix it:

function A() {
  this.x = await this.d();
}

SyntaxError: await is only valid in async functions and async generators

Okay, mark the constructor async:

async function A() {
  this.x = await this.d();
}
// ...
const B = new A();

TypeError: A is not a constructor

At this point you might throw your hands up in frustration (and rightly so). But if you have to make that original function e() async, it might be helpful to know all the places you need to make changes. StackLizard is for this purpose.

./stacklizard.js standalone docs/use-case/a/a.js 26
- e(), async a.js:26 FunctionExpression[0]
  - d(), await a.js:22 CallExpression[0], async a.js:19 FunctionExpression[0]
    - c(), await a.js:16 CallExpression[0], async a.js:15 FunctionExpression[0], accessor
      - b(), await a.js:12 MemberExpression[0], async a.js:11 FunctionExpression[0]
        - a(), await a.js:8 CallExpression[1], async a.js:7 FunctionExpression[0]
    - A(), await a.js:4 Identifier[1], async a.js:3 FunctionDeclaration[0], constructor
      - A(), await a.js:39 NewExpression[0]
- **SyntaxError**: async a.js:15 FunctionExpression[0], accessor
- **SyntaxError**: async a.js:3 FunctionDeclaration[0], constructor

Notably, StackLizard doesn't fix these problems for you, but it does point them out.

Installation

StackLizard should be treated as a NPM module, and installed as such:

npm install stacklizard

Command-line Usage

From the command-line, you have several subcommands. Generally speaking, I recommend the following:

  1. Using standalone or html subcommands to generate an initial configuration file
  2. Altering the configuration file as necessary
  3. Using the configuration subcommand with the generated configuration file to create revised results.
  4. Repeat as you desire.

standalone

This reads a single JavaScript file, marks one function async as you requested (by line number and optionally a "function index", the index of the function among the list of functions on that line), then generates a stack trace.

Optional arguments:

  • --fnIndex=0 to specify the 0th function on the line to mark async
  • --save-config path/to/json where you can specify a location to write a JSON configuration file for reuse.

configuration

This takes a configuration file you've generated via --save-config with some optional hand-editing, and re-runs the job based on that configuration.

Documentation for the configuration file format is at sample-config.json.yaml in this repository.

Optional arguments:

  • --ignore "pathToFile:line type[index]" to mark a node ignored. Cut & paste the string from an earlier serialization.
  • --save-config path/to/json where you can specify a location to write a JSON configuration file for reuse.

html

This takes a few arguments:

  • A root directory for a HTML project
  • A path to the HTML file where scripts run
  • A path to the HTML or JavaScript file containing the function to mark async
  • The line number of the function
  • --fnIndex=0 to specify the 0th function on the line to mark async
  • --save-config path/to/json where you can specify a location to write a JSON configuration file for reuse.

Usage within Node

Standalone mode

const StackLizard = require("stacklizard");

(async function() {
  const parseDriver = StackLizard.buildDriver("javascript", rootDir, options = {});

  // option 1: load from the file system
  await parseDriver.appendJSFile("path/to/JSFile/from/rootDir"); // always a relative path

  // option 2: load from in-memory string, no file i/o
  parseDriver.appendSource(pathToFile, firstLineInFile, source); 

  // Generate the Abstract Syntax Tree via espree and gather information via estraverse.
  parseDriver.parseSources();

  // Get a function AST node.
  const startAsync = parseDriver.functionNodeFromLine(
    "path/to/JSFile/from/rootDir", lineNumber, functionIndex
  );
  
  // Mark nodes async and await as needed from the function AST node, marked async.  Returns a Map().
  const asyncRefs = parseDriver.getAsyncStacks(startAsync);

  // Build a serializer.
  const serializer = StackLizard.getSerializer(
    "markdown", startAsync, asyncRefs, parseDriver, {nested: true}
  );

  // Serialize the results in a human-readable form.
  console.log(serializer.serialize());
  
  // Get a configuration to save to a file.
  const configuration = {
    driver: parseDriver.getConfiguration(startAsync),
    serializer: serializer.getConfiguration()
  };
})();

HTML mode

(async function() {
  const parseDriver = StackLizard.buildDriver("html", rootDirectory, options = {});

  // load from the file system, and get all the JavaScript code inline and from external files
  await parseDriver.appendSourcesViaHTML(pathToHTML);

  // Generate the Abstract Syntax Tree via espree and gather information via estraverse.
  parseDriver.parseSources();

  // Get a function AST node.
  const startAsync = parseDriver.functionNodeFromLine(
    args.pathToJS, args.line, args.fnIndex
  );

  // Mark nodes async and await as needed from the function AST node, marked async.  Returns a Map().
  const asyncRefs = parseDriver.getAsyncStacks(startAsync);

  // Build a serializer.
  const serializer = StackLizard.getSerializer(
    "markdown", startAsync, asyncRefs, parseDriver, {nested: true}
  );

  // Serialize the results in a human-readable form.
  console.log(serializer.serialize());

  // Get a configuration to save to a file.
  const configuration = {
    driver: parseDriver.getConfiguration(startAsync),
    serializer: serializer.getConfiguration()
  };
})();

Configuration mode

// config is a JSON object, parsed from a configuration file saved in a previous session.
async function doTheAnalysis(config) {
  // Build the parse driver.
  const parseDriver = StackLizard.buildDriver(
    config.driver.type,
    path.resolve(process.cwd(), config.driver.root), // probably something like this
    config.driver.options || {}
  );

  // Analyze everything at once.
  const {startAsync, asyncRefs} = await parseDriver.analyzeByConfiguration(config.driver);

  // Build the serializer.
  const serializer = StackLizard.getSerializer(
    config.serializer.type,
    startAsync,
    asyncRefs,
    parseDriver,
    config.serializer.options || {}
  );

  // Serialize the results in a human-readable form.
  console.log(serializer.serialize());
}

A few notes

  • StackLizard picks up await nodes by their local name ("b", not "A.prototype.b"), and marks them most aggressively, sometimes too much so. You can override this and tell StackLizard to ignore a node via the ignore parameter in a configuration file (recommended) or with code like this:
  const ignorable = this.nodeByLineFilterIndex(
    ignore.path,
    ignore.line,
    ignore.index,
    n => n.type === ignore.type
  );
  this.markIgnored(ignorable);