Frontend Development 11 min read

Creating a Babel Macro to Import Remote Modules (importURL)

This article explains how to write a Babel macro that fetches JavaScript libraries from a URL at compile time, covering macro concepts, limitations of JavaScript macros, preparation steps, a full importURL macro implementation, AST handling, synchronous requests, and post‑processing details.

ByteFE
ByteFE
ByteFE
Creating a Babel Macro to Import Remote Modules (importURL)

Macros are often used to implement DSLs, such as JSX in Rust's Yew framework, where the language's powerful macro system enables JSX‑like syntax without a separate Babel step. In JavaScript, however, native macro support is absent, so developers rely on tools like babel-plugin-macros and template‑string functions to achieve limited macro‑like capabilities, for example using graphql.macro to embed GraphQL queries.

Compared with full Babel plugins, macros are easier to adopt because they work out‑of‑the‑box with Create‑React‑App; adding a babel-plugin-macros entry in the Babel config enables any custom macro without ejecting the CRA setup.

The tutorial then walks through creating a custom macro named importURL that retrieves a remote library during compilation, caches it locally, and replaces the macro call with a require(...) statement.

Preparation

Create a folder importURL and run npm init -y .

Set the name field in package.json to import-url.macro so Babel can recognize it.

Install required dependencies: babel-plugin-macros , fs-extra , find-root , and other Node utilities.

Macro Implementation

import { execSync } from 'child_process';
import findRoot from 'find-root';
import path from 'path';
import fse from 'fs-extra';
import { createMacro } from 'babel-plugin-macros';

const syncGet = (url) => {
  const data = execSync(`curl -L ${url}`).toString();
  if (data === '') {
    throw new Error('empty data');
  }
  return data;
};

let count = 0;
export const genUniqueName = () => `pkg${++count}.js`;

module.exports = createMacro((ctx) => {
  const { references, babel: { types: t } } = ctx;
  const workspacePath = findRoot(ctx.state.filename);
  const cacheDirPath = path.join(workspacePath, '.cache');
  const calls = references.default.map(path =>
    path.findParent(p => p.node.type === 'CallExpression')
  );
  calls.forEach(nodePath => {
    if (nodePath.node.type === 'CallExpression' &&
        nodePath.node.arguments[0]?.type === 'StringLiteral') {
      const url = nodePath.node.arguments[0].value;
      const codes = syncGet(url);
      const pkgName = genUniqueName();
      const cacheFilename = path.join(cacheDirPath, pkgName);
      fse.outputFileSync(cacheFilename, codes);
      const relativeFilename = path.relative(ctx.state.filename, cacheFilename);
      nodePath.replaceWith(t.stringLiteral(`require('${relativeFilename}')`));
    }
  });
});

The macro uses Babel's createMacro API, inspects all references to the default export, finds the parent CallExpression , extracts the URL string argument, synchronously fetches the code via curl , writes it to a cache folder, and finally replaces the macro call with a require to the cached file.

AST Exploration

Developers can use AST Explorer to view the syntax tree of code such as:

import importURL from 'importurl.macros';
const React = importURL('https://unpkg.com/[email protected]/umd/react.development.js');

The relevant CallExpression nodes are accessible via ctx.references , allowing the macro to manipulate them.

Synchronous Request Constraint

Babel transformations run synchronously, so the macro must perform a synchronous HTTP request. Node does not provide a native sync HTTP API, so the implementation resorts to executing curl with execSync .

Final Steps and Limitations

After writing the fetched code to .cache , the macro inserts a string literal require('...') into the source, completing the transformation. Current limitations include lack of URL deduplication and potential name collisions across files; a robust solution would hash the URL to generate unique cache filenames.

References

Handbook: https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/README.md

AST Explorer: https://astexplorer.net/

frontendBabelnodejsMacros
ByteFE
Written by

ByteFE

Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.