scalameta / mdoc

Typechecked markdown documentation for Scala
https://scalameta.org/mdoc/
Apache License 2.0
394 stars 80 forks source link

mdoc.js: support ES modules and `SmallModulesFor` #638

Open armanbilge opened 2 years ago

armanbilge commented 2 years ago

Scala.js 1.10.0 added ModuleSplitStyle.SmallModulesFor to enable fast, iterative development of Scala.js apps.

The typical usage pattern is to list the application’s packages as argument. This way, often-changing classes receive independent, small modules, while the stable classes coming from libraries are bundled together as much as possible.

https://www.scala-js.org/news/2022/04/04/announcing-scalajs-1.10.0/#new-module-split-style-smallmodulesforpackages

IIUC all mdocs are compiled under the mdoc package so I expect that would be the argument to use for this setting. Then, mdoc --watch can provide a supercharged dev cycle.

armanbilge commented 2 years ago

Btw, I don't think this should be too hard to support. I think the key change here is adding support for ESModules, which means:

  1. supporting multiple JS file output from the linker
  2. Using in the browser with <script type="module" src="..."></script>

After enabling ES modules for (the non-mdoc part of) my project, the only change I had to make was (2).

Quafadas commented 2 months ago

I did a little investigation on this, as it's something I would like... from the outside, it doesn't look tooooo hard to get ESModules working.

In the first instance the module emitted by the linker, seems fine... as arman pointed out.

Let's assume we ran mdoc, on readme.md. I think the readme.js file emitted, should change to say.

<div id="mdoc-html-run1" data-mdoc-js></div>
<script type="module" src="readme.md.js"></script>
<script type="module" src="mdoc.js"></script>

instead of

<div id="mdoc-html-run1" data-mdoc-js></div>
<script type="text/javascript" src="readme.md.js" defer></script>
<script type="text/javascript" src="mdoc.js" defer></script>

So that change seems easy enough (famous last words). To keep the CLI working nicely however, I think it may also be necessary, to alter mdoc.js. Currently, it looks like this.

import { mdoc_js_run0 } from "./readme.md.js";

(function (global) {
  function findDivs() {
    return Array.from(global.document.querySelectorAll("div[data-mdoc-js]"));
  }

  function loadAll(scope) {    
    findDivs().forEach(function (el) {
      const idNum = el.getAttribute("id");
      console.log(idNum);
      var id = el.getAttribute("id").replace("-html-", "_js_");
      // eval(id)(el);
      mdoc_js_run0(el);
    });s
  }

  loadAll(global);
})(window);

I would propose, to make it look like this;

(function (global) {
  function findDivs() {
    return Array.from(global.document.querySelectorAll("div[data-mdoc-js]"));
  }

  function loadAll(scope) {
    findDivs().forEach(function (el) {

      var id = el.getAttribute("id").replace("-html-", "_js_");

      // https://stackoverflow.com/questions/2033711/how-can-i-attach-meta-data-to-a-dom-node
      const moduleName = el.getAttribute("data-mdoc-module-name");

      import(moduleName).then(function (module) {
      //import("./readme.md.js").then(function (module) {
        module[id](el);
      });
    });
  }

  loadAll(global);
})(window);

This would dynamically load the module, and then run the function it exports. i.e. preserve the existing live reload CLI functionality.

As things stand, it would require emitting an extra attribute on each of the divs that mdoc adds.

<div id="mdoc-html-run0" data-mdoc-js></div> would need to become

<div id="mdoc-html-run0" data-mdoc-js data-mdoc-module-name="readme.md.js" ></div>

I believe this would solve the ESModule problem, which I think is the key first step. It should be relatively simpler to then follow through on the SmallestModuleFor and EsModuleImportMap pieces later...

Quafadas commented 2 months ago

Here's my initial experiment in this.

https://github.com/Quafadas/mdoc_test