Importing JavaScript modules from URL

This article was originally published on Medium on the 6th of May 2019


In Dixons Carphone, we are rebuilding our platform with microfrontend architecture. It means investigating ways how to achieve the best user and developer experience possible. This is a series about our journey.

What is microfrontend

I will not repeat what was already written somewhere else. Should you wonder what microfrontend is, take a short break with the excellent article written by Vinci Rufus and then come back.

Our vision

We want to be able to load our micro apps from the registry by URL. Therefore we need to make NodeJS able to import modules from some URL.

Loader hooks to the rescue

Since Node 11 there is an experimental API which makes you able to change NodeJS module resolution behaviour. As the API is really experimental, it is changing with Node 12 ( API ), which I will use in the examples.

Let’s show some code. We want to be able to do this:

import app from 'http://localhost:8080/mock@1.0.0/index.js'
import _ from 'https://unpkg.com/lodash@4.17.11/lodash.js'
import express from 'express'

const server = express()

server.get('/', (req, res) => res.send(app.render(_.flattenDeep([1, [2, [3, [4]], 5]]))))

server.listen(3000)

It is just a simple Express server which renders tiny application. The file loads lodash from unpkg.com and app from own HTTP server:

const React = require('react')
const ReactDOMServer = require('react-dom/server')

module.exports = {
  render: (data) => {
    return ReactDOMServer.renderToString(
      React.createElement('h1', { children: data })
    )
  }
}

Note that we import react and react-dom/server. We don't have it installed next to the application and in future, when we will be bundling the application we will mark those “platform” dependencies like externals. It will be provided by a renderer application.

Loader magic

Let's finally show some code which does the magic (check the comments in the code for explanation):

import axios from 'axios';
import fs from 'fs'
import path from 'path';
import process from 'process';
import Module from 'module';
import packageJson from './package.json'

const builtins = Module.builtinModules;

const baseURL = new URL('file://');
baseURL.pathname = `${process.cwd()}/`;

export async function resolve(specifier, parentModuleURL = baseURL, defaultResolve) {
  // Basic support for built-in modules
  if (builtins.includes(specifier)) {
    return {
      url: specifier,
      format: 'builtin'
    };
  }

  if (specifier.startsWith('http')) {
    // We load from unpkg so we want to save the file under some "normal" name. This extracts the package name
    const s = specifier.split('@')[0]
    const basename = s.substring(
      s.lastIndexOf('/') + 1,
      s.length
    ) + '.js'
    // basename should be for example 'lodash.js'
    const finalPath = path.resolve('.tmp', basename)

    if (!fs.existsSync(finalPath)) {
      // Download from URL if the file does not exist yet
      const source = (await axios.get(specifier)).data
      fs.writeFileSync(
        finalPath,
        source
      )
    }

    return {
      // Tell NodeJS to load this module from our new file
      url: 'file://' + finalPath,
      format: 'commonjs'
    }
  }

  // If required module is specified as dependency in package.json, use defaultResolver
  if (!!packageJson.dependencies[specifier]) {
    return defaultResolve(specifier, parentModuleURL)
  }

  // Otherways, resolve it as normal import
  const resolved = new URL(specifier, parentModuleURL);
  return {
    url: resolved.href,
    format: 'module'
  };
}

What is not solved yet

This module loading is really just an experiment. We need to make this work with Next.js which we use in our wrapper application that renders page layouts.

We did not have to solve these areas yet:

  • whitelisting domains to download from
  • redownload script on change — if there is file already downloaded, it does not download it again
  • Make it work with Next.js (via Webpack plugin)

Next steps

In the next article, I will show how we solved the integration to Next.js via Webpack plugin. Let me know, what do you think about our idea and about importing modules from some URLs in general.

Would you do micro-frontends in a better way?