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

rollup-plugin-macros-calculate

v1.1.1

Published

``` npm i rollup-plugin-macros-calculate -D ```

Downloads

3

Readme

Installation

npm i rollup-plugin-macros-calculate -D

Motivation

Source code:

Let's say we have the following piece of code:

let results = (function() {

    let fs = require('fs')
    let dir = __dirname + '/replacements/'

    let filenames = fs.readdirSync(dir)

    return filenames.map(function (filename) {
      return require(dir + filename)
    })
})()

And let's say that our task is to leave this code still executed in common js format, but we need to assemble it into one iife bundle that will works on browser. Of course, we can use @rollup/plugin-commonjs with the dynamicRequireTargets option like this:

commonjs({
    dynamicRequireTargets: [`${packagespath}/*.js`],
}),

This option will allow you to shove dynamic imported packages into the bundle at the compile time, as we wanted. The source fragment will be generated in the following code:

let results = (function() {

    let fs = require$$0;
    let dir = __dirname + '/replacements/';

    let filenames = fs.readdirSync(dir);

    return filenames.map(function (filename) {
      return createCommonjsRequire("/lib")(dir + filename)
    })

})();

Its miraculous! However, we see that the commonjs plugin has left us a lot of work.

First of all, fs is not defined, because fs does not exists in browser and if you do nothing, then roll up will expect corresponding to external module in output.globals. This will cause an error. Dragging browserfs package here is redundant and doesn't make sense. Secondly, the __dirname variable also exists only in the nodejs runtime and has no meaning in the browser. The dynamicRequireTargets option will generate the getDynamicModules function, which will help createCommonjsRequire load modules during iteration in a loop, and it looks something like this:

  function getDynamicModules() {
  	return dynamicModules || (dynamicModules = {
        // ...
  		"/lib/replacements/important.js": requireImportant,
  		"/lib/replacements/interpolation.js": requireInterpolation,
  		"/lib/replacements/nth.js": requireNth,
  		"/lib/replacements/rgba.js": requireRgba,
  		"/lib/replacements/unquote.js": requireUnquote,
  		"/lib/replacements/variables.js": requireVariables,
        // ...
  	});
  }

We don't need either fs or __dirname anymore. But they are still present in the code and will cause an error in runtime. It turns out that we need to write a plugin ourselves that removes them (well, or use rollup-plugin-replace, for example). However, this will not solve the problem, because we need to get the filenames array from somewhere else, which should contain the module names for the dynamic require. And here we are faced with the need to write some kind of macro that should set this array of names to filenames in compile time.

Usage

And rollup-plugin-macros-calculate comes to our aid. Write config:

rollup.config.js:

import fs from "fs";
import path from 'path';
import { calculableMacros } from 'rollup-plugin-macros-calculate';
// ...

export default {
    input: './src/index.js',
    output: {
        file: './build/bundle.js',
        format: 'iife'
    },
    plugins: [
        // ...
        calculableMacros({
            externalPackages: { path, fs },
            onReplace: ar => `return [${ar.map(w => "'" + w + "'")}]`,
            macroses: {
                'fs.readdirSync': (function (_path) {
                    let dir = path.dirname(path.relative(process.cwd(), file))
                    return fs.readdirSync(dir + _path)
                }).toString(),
                '__dirname +': '', 
            }
        }),
        // ...
    ]
};

some minimal changes in source code. Change:

source:

let dir = __dirname + '/replacements/';

return fs.readdirSync(dir)

to

      
/** MACRO `fs.readdirSync, __dirname +` */

let dir = __dirname + '/replacements/';

return fs.readdirSync(dir)

/** END_MACRO */

Start rollup -c and we get the following output result:

output:

let results = (function () {

      let filenames = () => {
        return [
          "important.js",
          "interpolation.js",
          "nth.js",
          "rgba.js",
          "unquote.js",
          "variables.js",
          // ...
        ];
      };

      return filenames.map(function (filename) {
        return createCommonjsRequire("/lib")(dir + filename);
      });
})();

And we also see that var fs = require("fs") also disappeared. It didn't even require any additional actions, because rollup is able to do tree shaking. Excellent!

plugin options:

  • inclide - standart rollup inclide filter with glob support
  • exclude - standart rollup exclude filter with glob support
  • macroses - array of macros expressions. May be executable string or function body, which will injected to sources as is. As alternative also may be object with value and externalPackages field. This expression could include the variable file, as in the example above, which will contain the path to the processed file with the macro. This is the only magic variable that will be passed through the closure to the executed expression. If it is necessary, then make sure that it is not overridden inside the expression.
  • externalPackages - external packages used in the macroses
  • onReplace - after macro execution expression returns value with some whatever type. The onReplace callback gets the value and afford convert the execution result to string value, which will be injected to source code as is. If onReplace does not specified, the value will injected as is via expression return ${value} (for object the result value will be result of toString() method applying).
  • verbose - if verbode is true, before execution auto calculated expression will be print to the terminal

How it works?

If we take the example above, then based on the following source code:

let dir = __dirname + '/replacements/';

return fs.readdirSync(dir)

will be generated (before the building) and executed (also before the building) the following expression:

 let dir =  '/replacements/';

return (function (_path) {
    let dir = path.dirname(path.relative(process.cwd(), file))
    return fs.readdirSync(dir + _path)
})(dir)

The result of the execution will be pass to onReplace function and result of its processing will be injected on the place of the original source code:

return [
  "important.js",
  "interpolation.js",
  "nth.js",
  "rgba.js",
  "unquote.js",
  "variables.js",
  // ...
]

Advantages:

  • Preserving the structure of the source code
  • This is not a simple replacement of a piece of code. This is the execution of the source code with hot substitution of fragments in compile time in such a way that a logical connection remains between the replaced fragments
  • the ability to use third-party packages that are not designed into rollup plugins for compile time calculations without dragging them into a bundle (this is an advantage over rollup-plugin-ast-macros, since the latter does not allow the use of external packages in macros.)

Look up usage another example here (browser branch)

Known issues:

  • the file variable, which can be used in a macro, is taken as if from nowhere. This behavior is not intuitive. And obviously this variable is not typed (@ts-check scolds if, as in the example above, you write a function in order to further reduce it to a string (this is convenient) - you can solve it simply by declaring let file = " above. This variable will not get anywhere from the global space, but it will allow the editor to understand that file is a string). I don't think this is a matter of principle, but I would prefer a more explicit variable transfer inside the macro instructions, because I like consistency in everything. There was an idea to pass file as an additional argument to such an expression function - this may partially solve the type problem, but complicate the understanding of the expression construction. Therefore, I rejected this idea and am in search (open for suggestions)

Further plans:

  • Add the named_macroses field, which would allow you to connect individual macros into one connected calculation thread using the target options (:string[] = ['self']). Self will mean that the result of executing the current macro will be inserted into the source code in the place of this macro. Instead of self, there can be the name of another macro or the name of a file with a macro, then the result of executing the current macro will be passed through the closure to the macro whose name is specified in target