Dev. To /open-wc/nes…

Dev. To/Dakmor

Published: July 2, 2019 ยท 8 minutes to read

So you have this great idea, and now you want to actually do it. I’m pretty sure you don’t want to start from scratch, so let’s use the existing open source package.

If you want to play together, all the code is on Github.

In our example, we want to use lit-elements and lit-html.

Mkdir nested-dependecies in-frontend CD nested-dependecies in-frontend NPM install lit-element [email protected] --save-exactCopy the code

Note: we are using the pinned version here on purpose.

We then simply load the two packages in main.js.

import { LitElement } from "lit-element";
import { html } from "lit-html";

console.log(LitElement);
console.log(html);
Copy the code

To get an idea of how big our application is, we want to create a scrolling bundle. First, install Rollup.

npm install -D rollup
Copy the code

Then create a rollup.config.js.

export default {
  input: "main.js",
  output: {
    file: "bundle.js",
    format: "iife"
  },
};
Copy the code

Next, add “build” to our package.json script block: “Rollup -c rollup.config.js && du -h bundle.js” into the scripts block of our package.json so that we can easily build the file and print its file size.

Let’s run it through NPM run build ๐Ÿ™‚

(!) Unresolved dependencies
https://rollupjs.org/guide/en#warning-treating-module-as-external-dependency
lit-element (imported by main.js)
lit-html (imported by main.js)
Copy the code

Oh, it doesn’t work! ๐Ÿ˜ญ

Well, I’ve heard this question before…… We need to add some plug-ins so that Rollup can understand how nodes are resolved (i.e., bare module specifiers such as import {HTML} from ‘lit-html’).

npm i -D rollup-plugin-node-resolve
Copy the code
+ import resolve from "rollup-plugin-node-resolve";
+
   export default {
    input: "main.js",
    output: {
      file: "bundle.js",
      format: "iife"
    },
+ plugins: [resolve()]
  };
Copy the code
$ npm run build
#.
created bundle.js in 414ms
96K     bundle.js
Copy the code

So, this seems to work. ๐Ÿ’ช

What happens if someone likes Yarn?

Install YARN first, then compile. The result should be the same.

$ yarn install
$ yarn build
#.
created bundle.js in 583ms
124K    bundle.js
Copy the code

A: wow! This was unexpected –124K YARN build and 96K NPM build?

It appears that the yarn built file contains some additional files…… Maybe a bag was duplicated?

$ yarn list --pattern lit-*โ”œ โ”€ [email protected] โ”‚ โ”” โ”€ [email protected] โ”” - [email protected]Copy the code

Yes, lit-HTML versions 1.0.0 and 1.1.0 are installed.

The reason is most likely that when we installed lit-html with the NPM install –save-exact [email protected] command above, we nailed it to the 1.0.0 version of the root dependency.

Although NPM seems to do a good job of refactoring it, I don’t feel safe using NPM because NPM also likes to install nested dependencies if the dependency tree gets big.

$ npm ls lit-element lit-htmlโ”œ โ”€ โ”ฌ [email protected] โ”‚ โ”” โ”€ โ”€ [email protected] deduped โ”” โ”€ โ”€ [email protected]Copy the code

Especially when you use beta dependencies like 0.x.x, it can get tricky. In this case, SemVer says that each 0.x.0 release represents a breakthrough change. This means that 0.8.0 will be considered incompatible with 0.9.0. So even if the API you use works in both versions, you get nested dependencies that can silently break your application ๐Ÿ˜ฑ.

How does node resolution work

In NodeJS, when you import a file with a bare specifier, for example: import {LitElement} from “lit-element”; Node’s module parsing function gets the string lit-Element and starts searching all the directories listed in module.paths for imported modules, which you can check just like any other value in the Node repL.

$ node
module.paths
[
  '/some/path/nested-dependencies-in-frontend/node_modules',
  '/some/path/node_modules',
  '/some/node_modules',
  '/node_modules',
]
# unimportant folders are hidden here
Copy the code

Basically, Node looks at each node_modules folder, starting with the module’s parent directory and moving up the file tree until it finds a directory name that matches the module’s specifier (in our case, lit-Element). The parsing algorithm always starts in the parent directory of the current module, so it’s always relative to where you imported the file from. If we examine module.paths from the lit-Element directory, we see a different list.

$ cd node_modules/lit-element
$ node
module.paths
[
  '/some/path/nested-dependencies-in-frontend/node_modules/lit-element/node_modules',
  '/some/path/nested-dependencies-in-frontend/node_modules',
  '/some/path/node_modules',
  '/some/node_modules',
  '/node_modules',
]
Copy the code

Now we can understand what nested dependencies of nodes are. Each module can have its own node_modules directory, endlessly, and the import referenced in the module’s files will always look up…… in the nearest node_modules directory first

Advantages of nested dependencies on Node Disadvantages of front-end nested dependencies
Each package can have its own version, each dependent version Shipping the same code twice means longer download and processing times.
Packages are not affected by dependencies of other packages in the application There may be some problems if the same code is imported twice from two different places (for example, throughWeakMapsOr singleletons for performance optimization).
Access to many additional files does not require a “hefty fee”. Checking whether a file exists is an additional request.
On a server, you usually don’t care too much about how much extra code (file size) there is. Overall, in short, your site will get slower and slower

The problem

In short, automatic module resolution in favor of nesting can be dangerous for the front end.

  • What we care about is load and parse performance
  • What we care about is the size of the file
  • Some packages must be monad (that is, unique in the module diagram) to work in our application.
    • Examples includelit-htmlandgraphql
  • We should have complete control over the content on the client browser.

Node-like module parsing, designed for server-side environments, can become a serious problem when implemented in browsers. I believe that even if node resolution is technically possible, loading code for a complex data grid more than once should never be our goal as front-end developers.

The solution

Fortunately, we now have solutions to these problems, and there are proposals that could eliminate the need for flexibility altogether in the future.

Work for the day

Here are some tips for using bare module specifiers in your front-end code.

  • Make sure that the modules in your dependency tree all use similar version range common dependencies
  • Avoid pinning specific package versions as much as possible (as we did above)NPM I - S [email protected]).
  • If you’re usingnpm.
    • Run after installing the packagenpm dedupeTo remove nested duplicate packages.
    • You can try deleting yourspackage-lock.jsonAnd then reinstall. Sometimes there can be magic help ๐Ÿง™โ™‚๏ธ.
  • If you are usingyarn.
    • Consider using YARN Resolutions to specify the version of any repeated packages you like

Looking to the future

If we can tell the JavaScript environment (i.e. the browser) exactly on which path to find a file specified by a string, we don’t need node-like parsing or programming-time deduplicating routines.

We would write something like this and pass it to the browser to specify which paths map to which packages.

{
  "lit-html": "./node_modules/lit-html.js"."lit-element": "./node_modules/lit-element.js"
}
Copy the code

Using this import map to resolve the package path means that there is always only one version of lit-HTML and lit-Element, because the global environment already knows where to find them.

Fortunately โœจ, this is already a proposed specification called import Maps. Since it is intended for the browser, no conversion is required at all! All you need to do is provide the map, and no build steps are required at development time?

Sounds crazy ๐Ÿ˜œ? Let’s give it a try! ๐Ÿค—

Note: Please note that this is an experimental API proposal and it has not been finalized or accepted by implementers.

For now it only works in Chrome 75+, behind a logo. So type chrome://flags/ in the URL bar, then search for built-in Module infra and import Maps and enable it. Here’s a direct link: Chrome ://flags/#enable-built-in- infra.

Use the imported map in your browser

To use the imported map, let’s create an index.html file.

<html lang="en-GB">
<head>
  <script type="importmap">
    {
      "imports": {
        "lit-html": "./node_modules/lit-html/lit-html.js"."lit-html/": "./node_modules/lit-html/"."lit-element": "./node_modules/lit-element/lit-element.js"."lit-element/": "./node_modules/lit-element/"}}</script>
  <title>My app</title>
</head>

<body>
  <crowd-chant>
    <span slot="what">Bare Imports!</span>
    <span slot="when">Now!</span>
  </crowd-chant>

  <script type="module" src="./main.js"></script>
</body>

</html>
Copy the code

And adjust main.js.

import { html, LitElement } from "lit-element";

class CrowdChant extends LitElement {
  render() {
    return html`
      <h2>What do we want?</h2>
      <slot name="what"></slot>
      <h2>When do we want them?</h2>
      <time><slot name="when">Now!</slot></time>
    `;
  }
}

customElements.define("crowd-chant", CrowdChant);
Copy the code

Save the file and serve it locally by running NPX http-server-o in the same directory.

This will open http://localhost:8080/ and you will see your custom elements displayed on the screen. ๐ŸŽ‰

What dark magic is this ๐Ÿ”ฎ? Without any bundled programs, tools, or build steps, we wrote a componentized application with the bare specifiers we know and love.

Let’s break it down.

import { html } from 'lit-html';
// will actually import "./node_modules/lit-html/lit-html.js"
// because of
// "lit-html": "./node_modules/lit-html/lit-html.js",

import { repeat } from 'lit-html/directives/repeat.js'
// will actually import "./node_modules/lit-html/directives/repeat.js"
// beacause of
// "lit-html/": "./node_modules/lit-html/",
Copy the code

So that means

  1. You can import packages directly because the names of packages are mapped to a specific file
  2. You can import subdirectories and files because packageName+’/’ will be mapped to its directory.
  3. Do not omit.js when importing files from subdirectories.

What does all this mean for my manufacturing?

Again, it’s important to point out that this is still experimental technology. In any case, you may still want to use a tool like Rollup to optimize builds for your production site. Together we are exploring what these new apis will do for our website and applications. The underlying import-maps proposal is still unstable, but that shouldn’t stop us from experimenting and extracting utility from it. After all, most of us are comfortable using Babel to implement experimental syntaxes like decorators, although at the time of this writing, the proposal has at least four flavors.

If you want to try importing maps today, even in unsupported browsers, you’ll need a build step or a runtime solution like SystemJS. For the build step option, you’ll replace rollup-plugin-node-resolve with something that respects your import map instead of using node resolution.

Wouldn’t it be nice if you could rollup to your index.html and have it figure out what your entry point is and whether there’s an import map?

That’s why we released experimental support for imported maps at Open-WC, our rollup-plugin-index-html.

You can read all about it at dev.to. Please follow the bulletin in this space ๐Ÿ˜‰.

Please follow us on Twitter, or follow me on my personal Twitter. Be sure to check out our other tools and recommendations at open-wc.org.

Thanks to Benny and Lars for their feedback and for helping me turn my doodle into a traceable story.