smotaal
6/14/2018 - 11:31 AM

Runtime linking for ES2015 Modules [sic]

Runtime linking for ES2015 Modules [sic]

// GIST: https://jsbin.com/gist/985fa2ef7681dfb48f7bce7b67fa0b18
// GIST: https://gist.github.com/SMotaal/985fa2ef7681dfb48f7bce7b67fa0b18
// LOCAL: https://jsbin.com/local/spring-night-2A7
(() => {
  console.clear();

  /** @type {typeof global & typeof window} */
  const scope = this
    || typeof window === 'object' && window
    || typeof global === 'object' && global
    || typeof self === 'object' && self
    || {};

  //Logging
  const
    ENTRIES = new class Entries extends Array { },
    ERRORS = new class Errors extends Array { },
    MARK = (OP, ID, value) =>
      ENTRIES.push({ name: `«${ID}» — ${OP}`, value });

  // Executors
  const maybe = ƒ => { try { return ƒ() } catch (e) { } };
  const noop = () => { };

  // Formatters
  const reindent = (
    string, indent = (/\n?( *)$/.exec(string))[1].length
  ) => string.replace(new RegExp(`^ {0,${indent}}`, 'gm'), '');

  (function (helpers) {

    const {
      runtime: { global, DynamicImport, RegisterModule },
      performance: { START = noop, END = noop },
    } = helpers;

    const ModuleSources = global['[[ModuleSources]]'] = {};

    (() => {

      const origin = `x:/`;
      const normalize = (specifier, referrer) => {
        const base = referrer && (
          referrer.includes(':/') && referrer || normalize(referrer, origin)
        ) || origin;
        const reference = /^#|[.]{0,2}[/]|\w+:/.test(specifier)
          ? specifier.replace(/^[/]{2,}/, '/') : `/#${specifier}`;
        return `${new URL(reference, base)}`;
      }
      const unnormalize = (url) => url.replace(/^x:(\/#|)/, '');

      const ModuleLoader = global['[[ModuleLoader]]'] = {
        resolve: (specifier, referrer) =>
          unnormalize(normalize(specifier, referrer)),

        load: async (specifier, referrer) => {
          const sourceURL = ModuleLoader.resolve(specifier, referrer)
          if (sourceURL in ModuleMap)
            return ModuleMap[sourceURL];

          if (sourceURL in ModuleSources)
            return ModuleMap.define(ModuleSources[sourceURL], specifier, referrer);

          throw `Failed to load module from "${sourceURL}"`;
        },

        import: async (specifier, referrer) => {
          return (await ModuleLoader.load(specifier, referrer)).namespace();
        }
      };

      const ValidModuleSpecifiers = /^((import(?= +\* +as \w+| +(?:\w+, *)?\{[\w\s,]*?\}| +\w+)|export(?= +\*| +\{[\w\s,]*?\})) +(.*?) +from +(['"]))(.*?)(\4)/gmsu;

      class Module {
        constructor(properties) {
          Object.assign(this, properties);
        }

        async link() {
          const sourceURL = this.sourceURL;
          const links = this.links;

          START('[3] link', sourceURL);

          let linkedSource = this.prelinkedSource;

          for (const link of links) {
            const requiredModule = ModuleMap[link.url];
            linkedSource = linkedSource.replace(link.marker, `${link.head}${
              requiredModule
              && (
                requiredModule.url || (await requiredModule.link())
              ) || await (
                (await ModuleLoader.load(link.specifier, sourceURL)).link()
              ) || link.specifier
              }${link.tail}`.padEnd(link.declaration.length));
          }

          const url = this.url = RegisterModule(this.linkedSource = linkedSource, sourceURL);

          ModuleMap[sourceURL] = ModuleMap[url] = this;

          END('[3] link', sourceURL);

          return url;
        }

        async instantiate() {
          const sourceURL = this.sourceURL;
          const url = this.url || await this.link();

          START('[4] instantiate', sourceURL);
          try {
            await (this.namespace = () => DynamicImport(url))();
          } catch (exception) {
            this.error = exception;
          }
          END('[4] instantiate', sourceURL);

          return ModuleMap[sourceURL] = ModuleMap[url] = this;
        }

        async namespace() {
          return (await this.instantiate()).namespace();
        }
      }

      const ModuleMap = global['[[ModuleMap]]'] = {

        define: (source, specifier, referrer = origin) => {

          // const mappedURL = normalize(specifier, referrer);
          // const sourceURL = unnormalize(mappedURL);
          const sourceURL = ModuleLoader.resolve(specifier, referrer);
          const baseURL = /^\w/.test(sourceURL) ? origin : sourceURL;

          if (sourceURL in ModuleMap)
            console.warn(`\n\nRedefining "${sourceURL}"!\n\n`);

          START('[1] define', sourceURL);

          const links = [];

          START('[2] prelink', sourceURL);

          const prelinkedSource = source.replace(ValidModuleSpecifiers, (
            declaration, head, type, bindings, quote, specifier, tail
          ) => {
            const marker = `««—${links.length}—${specifier}—»»`;
            links.push({
              declaration, head, type, bindings, quote, specifier, tail, marker,
              url: ModuleLoader.resolve(specifier, baseURL)
            });

            return marker;
          });

          END('[2] prelink', sourceURL);

          const module = new Module({
            source, sourceURL, links, prelinkedSource
          });

          END('[1] define', sourceURL);
          return ModuleMap[sourceURL] = module;
        },

      }

      return ModuleSources;
    })();

    setTimeout(() => test({ global, timeline: ENTRIES }), 100);
  })({
    runtime: RuntimeHelpers(),
    performance: PerformanceHelpers(),
  });

  function RuntimeHelpers() {
    const
      JS = 'text/javascript',
      UID = () => ~~(1e6 * Math.random()),
      SRC = (uid = UID()) =>
        `VirtualModule-${uid}`,
      SourceText = (source, url = SRC()) =>
        `${source}\n\n//# sourceURL=${url}\n`;

    if (typeof window !== 'undefined' && (
      typeof process === 'undefined'
      || process.__nwjs
    )) {
      const importModule = async (url) => eval(`import("${url}")`);

      const
        SourceFile = (source, url = SRC(), type = JS) =>
          new File([SourceText(source, url)], url, { type }),
        SourceFileURL = (source, url = SRC(), type = JS) =>
          URL.createObjectURL(SourceFile(source, url, type)),
        RegisterModule = (source, sourceURL) =>
          SourceFileURL(SourceText(source, sourceURL)),
        DynamicImport = (url) => importModule(url);

      return { global: window, RegisterModule, DynamicImport };
    }

    if (typeof process !== 'undefined') {
      global.URL || (global.URL = require('url').URL);
      // const moduleHelpers = require('./node-vm-module-helpers');
      // const moduleHelpers = require('./node-module-wrap-helpers');
      const moduleHelpers = require('/Users/daflair/Projects/imaging-js/packages/kernel/node-module-wrap-helpers.js');
      moduleHelpers.global = global;

      return moduleHelpers;
    }

    throw Error('Unsupported runtime');
  }

  function PerformanceHelpers() {
    const { performance, PerformanceObserver } =
      (scope.PerformanceObserver && scope.performance)
      && (!scope.process || !scope.process.moduleLoadList)
      && scope || maybe(() => require('perf_hooks')) || {};

    performance && PerformanceObserver && new PerformanceObserver(
      (list, observer) => ENTRIES.push(...list.getEntries())
    ).observe({ entryTypes: ['measure'] }); // console.log({ performance, scope })

    return performance ? {
      performance, ENTRIES, MARK,
      START: (OP, ID) =>
        performance.mark(`START: «${ID}» — ${OP}`),
      END: (OP, ID) =>
        performance.measure(
          `«${ID}» — ${OP}`, `START: «${ID}» — ${OP}`, (
            performance.mark(`END: «${ID}» — ${OP}`),
            `END: «${ID}» — ${OP}`
          )),
    } : { performance, ENTRIES, MARK };
  }

  async function test({
    global,
    ModuleLoader = global['[[ModuleLoader]]'],
    ModuleSources = global['[[ModuleSources]]'],
    records = new class Records { },
    namespaces = new class Namespaces { },
    timeline = [],
    sources = {
      ['/modules/typescript']: reindent(`
      export const typescript = true;
      export default "TypeScript"
      `),
      ['typescript']: reindent(`
      export * from '/modules/typescript';
      export {\ndefault\n} from '/modules/typescript';
      `),
      ['/index']: reindent(`
      import ts from 'typescript'; // import ts, {a, b, c} from 'typescript';
      export * from 'typescript';
      export const index = true;
      export { ts };
      `),
      ['/tests/works']: reindent(`
      import * as ts1 from '../modules/typescript';
      import * as ts2 from '//modules/x/../typescript';
      import * as ts3 from 'typescript';
      console.log('/tests/works', {
          '/modules/typescript': ts1,
          '//modules/x/../typescript': ts2,
          'typescript': ts3
      });
      `),
      ['/tests/async/resolve']: reindent(`
      export function then(resolve, reject) {
        try { resolve([1, 2, 3, 'Resolved Module']) }
        catch (e) { console.warn('Thenable module not resolved!') }
      }
      `),
      ['/tests/async/reject']: reindent(`
      export function then(resolve, reject) {
          try { reject('Rejected Module') }
          catch (e) { console.warn('Thenable module not rejected!') }
      }
      `),
      ['/tests/fails']: `import x from 'xyz';`,
      ['https://unpkg.com/lit-html@0.9.0/lit-html.js']: `export {};`,
      ['lit-html']: `export * from 'https://unpkg.com/lit-html@0.9.0/lit-html.js';`,
      ['/tests/deadlock/a']: `export * from './b';\nexport default 'A';\nconsole.log('deadlock a');`,
      ['/tests/deadlock/b']: `export * from './a';\nexport default 'B';\nconsole.log('deadlock b');`,
    },
    specifiers = Object.keys(sources),
  }) {
    Object.assign(ModuleSources, sources);
    for (const specifier of specifiers) {
      try {
        records[specifier] = await ModuleLoader.load(specifier);
        namespaces[specifier] = await ModuleLoader.import(specifier);
      } catch (exception) {
        namespaces[specifier] = exception;
      }
    }
    console.dir(timeline.reduce(
      (entries, { name, duration, value }) => ((
        entries[name] = value || duration || true
      ), entries), {
        '[RECORDS]': records,
        '[NAMESPACES]': namespaces
      }
    ));
  }
})()
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>

</body>
</html>

This prototype simply relies on async execution to walk the module graph and uses the platform to create either a ModuleWrap in node (when possible) or a blob which is used to determine the actual specifier to use for each link outwards.

A given limitation is that circular references will result in deadlock.