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

@alu0101229775/espree-logging

v0.3.0

Published

Adds logs to javascript code

Downloads

4

Readme

Open in Codespaces

Práctica Espree logging

Introducción

Volver al principio 🔝

En el repo encontrará el programa logging-espree.js el cual implementa una función addLogging que:

  • cuando se llama analiza el código JS que se la da como entrada
  • produciendo como salida un código JS equivalente que inserta mensajes de console.log a la entrada de cada función.

Resumen de lo aprendido

Volver al principio 🔝

El programa genera un arbol AST a partir de un fichero de entrada con código y, una vez se tiene dicho arbol, se recorre para generar el código de nuevo pero modificado con los console.log() a través el escodegen y el transpile.

Asimismo, se ha aprendido a utilizar el debugger de Chrome para inspeccionar el programa.

El ejecutable

Volver al principio 🔝

El ejecutable está en bin/log.js y se puede ejecutar con npm start o node bin/log.js.

Contenido de bin/log.js:

#!/usr/bin/env node
import { program } from "commander";
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const { version } = require("../package.json");
import { transpile } from "../src/logging-espree.js";

program
  .version(version)
  .argument("<filename>", 'file with the original code')
  .option("-V, --version", "output the version number")
  .option("-o, --output <filename>", "file in which to write the output", "output.js")
  .option("-h, --help", "output usage information")
  .action((filename, options) => {
    transpile(filename, options.output);
  });

program.parse(process.argv);

El programa

Volver al principio 🔝

El programa está en src/logging-espree.js:

import * as escodegen from "escodegen";
import * as espree from "espree";
import * as estraverse from "estraverse";
import * as fs from "fs/promises";

/**
 * Read the file with the js program, calls addLogin to add the login messages and writes the output
 * @param {string} input_file - The name of the input file
 * @param {string} output_file - The name of the output file (default: output.js)
 */
export async function transpile(inputFile, outputFile) {
  try {
    if (inputFile) {
      // console.log(`Transpiling '${inputFile}' to '${outputFile}'`);
      /* Al leer un fichero se usa una función asíncrona
      * Todas las funciones asíncronas llevan una callback que es el único
      * lugar donde estamos seguros que el código se va a ejecutar después
      * de haber ejecutado la función
      * Normalmente se pasa como argumento el error y resultado de la 
      * función asíncrona, para el caso de que no hubiera error.
      */
      let input = await fs.readFile(inputFile, 'utf8', (err) => {
        console.log(`Input read from file '${inputFile}'`);
        /// Si hay error se envía un throw
        if (err) throw `Error reading '${inputFile}': ${err}`;
      });
      /// Se llama al addLoggin y se guarda en output
      const output = addLogging(input);
      /// Se muesta la cadena de entrada al programador
      console.error(`input:\n${input}\n---`);
      /// Manera correcta de realizar el write después del read 
      /// (dentro de la callback). Tendencia hacia la diagonalidad
      await fs.writeFile(outputFile, output, err => {
        /// Se comprueba si ha habido o no error y se imprime la salida por pantalla
        if (err) throw `Can't write to '${outputFile}': ${err}`;
        console.log(`Output in file '${outputFile}'`);
      });
    }
    else program.help();  //< En caso de no usar la sintaxis correcta se imprime la ayuda
  }
  catch (e) {
    console.error(`Hubo errores: ${e}`);
  }
}

/**
 * Builds the AST and
 * Traverses it searching for function nodes and callas addBeforeNode to transform the AST
 * @param {string} code - The source code 
 * @returns -- The transformed AST 
 */
export function addLogging(code) {
  const ast = espree.parse(code, {ecmaVersion: espree.latestEcmaVersion, loc: true});  //< Builds the AST
  estraverse.traverse(ast, {                                                           //< Traverses the AST searching for function nodes
    enter: function(node, parent) {
      if (node.type === 'FunctionDeclaration' ||
        node.type === 'FunctionExpression' ||
        node.type === 'ArrowFunctionExpression') {                                     //< If the node is a function node, calls addBeforeNode
        addBeforeCode(node);                                                           //< to transform the AST
      }
    }
  });
  return escodegen.generate(ast);                                                      //< Generates the code from the AST
}

/**
 * AST transformation
 * @param {AST function type node} node - The function node to be transformed
 */
function addBeforeCode(node) {
  const name = node.id ? node.id.name : '<anonymous function>';                                                        //< Gets the name of the function
  const parameters = node.params.map(param => `\$\{ ${param.name} \}`);                                                //< Gets the parameters of the function
  const beforeCode = "console.log('Entering " + name + "(" + parameters + ") at line " + node.loc.start.line + "');";  //< Builds the code to be added
  const beforeNodes = espree.parse(beforeCode, { ecmaVersion: 6 }).body;                                               //< Builds the AST from the code to be added
  node.body.body = beforeNodes.concat(node.body.body);                                                                 //< Adds the code to the AST
}

Indicar los valores de los argumentos

Se ha modificado el código de logging-espree.js para que el log también indique los valores de los argumentos que se pasaron a la función. Ejemplo:

function foo(a, b) {
  var x = 'blah';
  var y = (function (z) {
    return z+3;
  })(2);
}
foo(1, 'wut', 3);
function foo(a, b) {
    console.log(`Entering foo(${ a }, ${ b })`);
    var x = 'blah';
    var y = function (z) {
        console.log(`Entering <anonymous function>(${ z })`);
        return z + 3;
    }(2);
}
foo(1, 'wut', 3);

Para ello, se ha modificado la función addBeforeCode para que añada el código de log de los argumentos de la función.

/**
 * AST transformation
 * @param {AST function type node} node - The function node to be transformed
 */
function addBeforeCode(node) {
  const name = node.id ? node.id.name : '<anonymous function>';                                                        //< Gets the name of the function
  const parameters = node.params.map(param => `\$\{ ${param.name} \}`);                                                //< Gets the parameters of the function
  const beforeCode = "console.log('Entering " + name + "(" + parameters + ") at line " + node.loc.start.line + "');";  //< Builds the code to be added
  const beforeNodes = espree.parse(beforeCode, { ecmaVersion: 6 }).body;                                               //< Builds the AST from the code to be added
  node.body.body = beforeNodes.concat(node.body.body);                                                                 //< Adds the code to the AST
}

CLI con Commander.js

Se hace un parsing de la línea de comandos mediante el módulo npm commander.js. Contenido de bin/log.js:

#!/usr/bin/env node
import { program } from "commander";
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const { version } = require("../package.json");
import { transpile } from "../src/logging-espree.js";

program
  .version(version)
  .argument("<filename>", 'file with the original code')
  .option("-V, --version", "output the version number")
  .option("-o, --output <filename>", "file in which to write the output", "output.js")
  .option("-h, --help", "output usage information")
  .action((filename, options) => {
    transpile(filename, options.output);
  });

program.parse(process.argv);

Reto 1: Soportar funciones flecha

Para soportar funciones flecha basta con añadir node.type === 'ArrowFunctionExpression' en el condicional if de la función addLogging en el fichero logging-espree.js:

/**
 * Builds the AST and
 * Traverses it searching for function nodes and callas addBeforeNode to transform the AST
 * @param {string} code - The source code 
 * @returns -- The transformed AST 
 */
export function addLogging(code) {
  const ast = espree.parse(code);                                                      //< Builds the AST
  estraverse.traverse(ast, {                                                           //< Traverses the AST searching for function nodes
    enter: function(node, parent) {
      if (node.type === 'FunctionDeclaration' ||
        node.type === 'FunctionExpression' ||
        node.type === 'ArrowFunctionExpression') {                                     //< If the node is a function node, calls addBeforeNode
        addBeforeCode(node);                                                           //< to transform the AST
      }
    }
  });
  return escodegen.generate(ast);                                                      //< Generates the code from the AST
}

Reto 2: Añadir el número de línea

Para añadir el número de línea en el logging hemos añadido node.loc.start.line al console.log() y {ecmaVersion: espree.latestEcmaVersion, loc: true} en el parser de espree en el fichero logging-espree.js:

/**
 * AST transformation
 * @param {AST function type node} node - The function node to be transformed
 */
function addBeforeCode(node) {
  const name = node.id ? node.id.name : '<anonymous function>';                                                        //< Gets the name of the function
  const parameters = node.params.map(param => `\$\{ ${param.name} \}`);                                                //< Gets the parameters of the function
  const beforeCode = "console.log('Entering " + name + "(" + parameters + ") at line " + node.loc.start.line + "');";  //< Builds the code to be added
  const beforeNodes = espree.parse(beforeCode, { ecmaVersion: 6 }).body;                                               //< Builds the AST from the code to be added
  node.body.body = beforeNodes.concat(node.body.body);                                                                 //< Adds the code to the AST
}
/**
 * Builds the AST and
 * Traverses it searching for function nodes and callas addBeforeNode to transform the AST
 * @param {string} code - The source code 
 * @returns -- The transformed AST 
 */
export function addLogging(code) {
  const ast = espree.parse(code, {ecmaVersion: espree.latestEcmaVersion, loc: true});  //< Builds the AST
  estraverse.traverse(ast, {                                                           //< Traverses the AST searching for function nodes
    enter: function(node, parent) {
      if (node.type === 'FunctionDeclaration' ||
        node.type === 'FunctionExpression' ||
        node.type === 'ArrowFunctionExpression') {                                     //< If the node is a function node, calls addBeforeNode
        addBeforeCode(node);                                                           //< to transform the AST
      }
    }
  });
  return escodegen.generate(ast);                                                      //< Generates the code from the AST
}

Publicación como paquete npm

Volver al principio 🔝

Seguimos los pasos de la documentación de npm para publicar nuestro paquete en npmjs.com también disponible en los apuntes de la asignatura.

Creeamos un usuario en npmjs.com:

npm adduser

Iniciamos sesión en npmjs.com:

npm login
npm whoami

Configuramos nuestro paquete:

npm set init.author.name "Gerard Antony Caramazza Vilá"
npm set init.author.email "[email protected]"
npm set init.author.url "https://github.com/alu0101229775"

Cambiamos la visibilidad del repositorio a público: Visibilidad Repositorio

A continuación ejecutamos el siguiente comando para publicar el paquete:

npm publish --access=public

Tests and Covering

Volver al principio 🔝

Para ejecutar los test añadimos las siguientes lineas en nuestro fichero package.json:

"scripts": {
    "test": "mocha test/test.mjs",
    "cov": "c8 npm test",
    "cov-doc": "c8 --reporter=html --reporter=text --report-dir docs/coverage mocha",
    "doc": "documentation build ./src/** -f html -o docs",
    "test1": "bin/log.js test/data/test1.js",
    "test2": "bin/log.js test/data/test2.js",
    "test3": "bin/log.js test/data/test3.js"
}

Modificamos el fichero test.mjs para que se ejecuten los tests:

import { transpile } from "../src/logging-espree.js";
import assert from 'assert';
import * as fs from "fs/promises";
import { dirname } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));
import Tst from './test-description.mjs';

const Test = Tst.map(t => ({
  input: __dirname + '/data/' + t.input,
  output: __dirname + '/data/' + t.output,
  correctLogged: __dirname + '/data/' + t.correctLogged,
  correctOut: __dirname + '/data/' + t.correctOut,
})
)

function removeSpaces(s) {
  return s.replace(/\s/g, '');
}

for (let i = 0; i < Test.length; i++) {
  it(`transpile(${Tst[i].input}, ${Tst[i].output})`, async () => {
    /// Compile the input and check the output program is what expected
    await transpile(Test[i].input, Test[i].output);
    let output = await fs.readFile(Test[i].output, 'utf-8')
    let correctLogged = await fs.readFile(Test[i].correctLogged, 'utf-8')
    assert.equal(removeSpaces(output), removeSpaces(correctLogged));
    await fs.unlink(Test[i].output);

    /// Run the output program and check the logged output is what expected
    let correctOut = await fs.readFile(Test[i].correctOut, 'utf-8')
    let oldLog = console.log; // mocking console.log
    let result = "";
    console.log = function (...s) { result += s.join('') }
      eval(output);
      assert.equal(removeSpaces(result), removeSpaces(correctOut))
    console.log = oldLog;
  }); 
}

Añadimos 3 tests más en la carpeta test/data: Test 1:

function foo(a) {
  console.log(a);
  let b = () => {
    console.log('pl');
  }
  b();
}
foo(() => console.log('hi'));

Rúbrica

Volver al principio 🔝

  • [x] Opciones en línea de comandos (-o, -V, -h, etc.)
  • [x] Añade mensajes de logs a la entrada de las function()
  • [x] Añade mensajes de logs a la entrada de las arrow () => { ... }
  • [x] Tutorial README.md y paneles bien presentados
  • [x] Da información correcta de los números de línea
  • [x] El package.json tiene scripts para ejecutar el programa
  • [x] El paquete está publicado en npmjs con ámbito aluXXX
  • [x] Contiene un ejecutable que se ejecuta correctamente (--help, etc.)
  • [x] El módulo exporta las funciones adecuadas
  • [x] Contiene suficientes tests
  • [x] Estudio de covering
  • [x] Se ha hecho CI con GitHub Actions
  • [x] La documentación es completa: API, ejecutable, instalación, etc.
  • [x] Se ha probado que la librería está accesible y funciona
  • [x] Se ha probado que el ejecutable queda correctamente instalado, puede ser ejecutado con el nombre publicado y produce salidas correctas
  • [x] Se ha hecho un buen uso del versionado semántico en la evolución del módulo