Skip to content

Commit

Permalink
docs/issue 144 best practices and recommendations for import maps and…
Browse files Browse the repository at this point in the history
… referencing node modules (#153)
  • Loading branch information
thescientist13 authored Jan 27, 2025
1 parent ada9671 commit c0c4f64
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 27 deletions.
2 changes: 1 addition & 1 deletion src/pages/docs/introduction/about.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Greenwood's goal is to bring the power of the modern web right to your fingertip

Some of Greenwood's feature include:

- Unbundled local development workflow, using `ETag` headers for efficient caching and live reloads
- Unbundled local development workflow, using `ETag` cache headers for efficient caching and live reloads
- Out of the box support for ESM and Web APIs, on both the client and server
- Server Side Rendering of Web Components (Light and Shadow DOM)
- File-based routing, including API Routes
Expand Down
23 changes: 21 additions & 2 deletions src/pages/docs/introduction/web-standards.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,9 @@ export async function handler(request) {

## Import Maps

During local development, Greenwood loads all assets from your browser unbundled, serving the content right off disk. [**Import maps**](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) allow bare specifiers typically found when referencing packages from npm, to work natively in the browser. When installing a package as a **dependency** in your _package.json_, Greenwood will walk your dependencies and all their dependencies, to build up a map to be injected into the `<head>` of your HTML.
During local development, Greenwood serves all resources to your browser unbundled right off disk using efficient [E-Tag caching](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag). [**Import Maps**](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) allow bare specifiers for ESM compatible packages installed from npm to work natively in the browser. When Greenwood sees a package in the **dependency** field of your _package.json_, Greenwood will walk all your dependencies and build up an import map to be injected into the `<head>` of your HTML automatically, in conjunction with Greenwood's dev server.

This is a sample of an import map that would be generated after having installed the **lit** package:
Below is a sample of an import map that would be generated after having installed the **lit** package:

```html
<html>
Expand All @@ -128,6 +128,25 @@ This is a sample of an import map that would be generated after having installed
</html>
```

To generate this map, Greenwood first checks each package's [**exports**](https://nodejs.org/api/packages.html#package-entry-points) field, then looks for a **module** field, and finally a **main** field. If none of these fields are found, Greenwood will log some diagnostics information. For **exports** field, Greenwood supports the following [conditions](https://nodejs.org/api/packages.html#conditional-exports) in this priority order:

1. **import**
1. **module-sync**
1. **default**

### Compatibility

However in the land of _node_modules_, not all packages are created equal. Greenwood depends on packages following the standard conventions of the NodeJS entry point specification when resolving the location of dependencies on disk using [`import.meta.resolve`](https://nodejs.org/api/esm.html#importmetaresolvespecifier). For server-side only packages, this is is usually not an issue. Greenwood will output some diagnostic information that can be used when reaching out for help in case something ends up not working as expected, but if it works, it works!

Some known issues / examples observed so far include:

- `ERR_MODULE_NOT_FOUND` - Observed with packages like [**@types/trusted-types**](https://github.com/DefinitelyTyped/DefinitelyTyped) which has an [empty string](https://unpkg.com/browse/@types/trusted-types@2.0.7/package.json) for the **main** field, or [**font-awesome**](https://fontawesome.com/), which has [no entry point](https://unpkg.com/browse/font-awesome@4.7.0/package.json) at all, at least as of `v4.x`. This is also a fairly common issue with packages that primarily deal with shipping types since they will likely only define a `types` field in their _package.json_.
- `ERR_PACKAGE_PATH_NOT_EXPORTED` - Encountered with packages like [**geist-font**](https://github.com/vercel/geist-font/issues/150) or [**@libsql/core**](https://github.com/thescientist13/import-meta-resolve-demo?tab=readme-ov-file#no-main-exports-map-entry-point-err_package_path_not_exported), which has [no default export](https://github.com/vercel/geist-font/issues/150) in their exports map, which is assumed by the NodeJS resolver algorithm.

> In almost all of our observed diagnostic cases, they would all go away if [this feature](https://github.com/nodejs/node/issues/49445) was added to NodeJS, so please add an up-vote to that issue! 👍
>
> If you have any issues or questions, please reach out in our [discussion on the topic](https://github.com/ProjectEvergreen/greenwood/discussions/1396).
## URL

The [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) constructor provides an elegant way for referencing [static assets](/docs/resources/assets/) on the client and on the server, and it works great when combined with [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) for easily interacting with search params in a request.
Expand Down
44 changes: 42 additions & 2 deletions src/pages/docs/reference/plugins-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,47 @@ This plugin supports providing an array of "paired" URL objects that can either
<!-- prettier-ignore-end -->
> You can see more examples in the [Greenwood repo](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/cli/src/plugins/copy).
If you need to copy files out of _node_modules_, you can use some of Greenwood's helper utilities for locating npm packages on disk and copying them to the output directory. For [example](https://github.com/ProjectEvergreen/greenwood/blob/master/packages/plugin-polyfills/src/index.js):
<!-- prettier-ignore-start -->
<app-ctc-block variant="snippet" heading="src/pages/index.html">
```js
import {
derivePackageRoot,
resolveBareSpecifier,
} from "@greenwood/cli/src/lib/walker-package-ranger.js";

const greenwoodPluginPolyfills = () => {
return [{
type: "copy",
name: "plugin-copy-polyfills",
provider: async (compilation) => {
const { outputDir } = compilation.context;
const polyfillsResolved = resolveBareSpecifier("@webcomponents/webcomponentsjs");
const polyfillsRoot = derivePackageRoot(polyfillsResolved);

return [
{
from: new URL("./webcomponents-loader.js", polyfillsRoot),
to: new URL("./webcomponents-loader.js", outputDir),
},
{
from: new URL("./bundles/", polyfillsRoot),
to: new URL("./bundles/", outputDir),
},
];
},
}];
};

export { greenwoodPluginPolyfills }
```
</app-ctc-block>
<!-- prettier-ignore-end -->
## Renderer
Expand Down Expand Up @@ -457,7 +497,7 @@ When requesting a resource like a file, such as _/main.js_, Greenwood needs to k

When requesting a file and after knowing where to resolve it, such as _/path/to/user-workspace/main/scripts/main.js_, Greenwood needs to return the contents of that resource so can be served to a browser or bundled appropriately. This is done by passing an instance of `URL` and `Request` and returning an instance of `Response`. For example, Greenwood uses this lifecycle extensively to serve all the standard web content types like HTML, JS, CSS, images, fonts, etc and also providing the appropriate `Content-Type` header.

If you are supporting _non standard_ file formats, like TypeScript (_.ts_) or JSX (_.jsx_), this is where you would want to handle providing the contents of this file transformed into something a browser could understand; like compiling the TypeScript to JavaScript.
If you are supporting _non standard_ file formats, like TypeScript (_.ts_) or JSX (_.jsx_), this is where you would want to handle providing the contents of this file transformed into something a browser could understand; like compiling the TypeScript to JavaScript. This lifecycle is the right place to evaluate a predicate based on a file's extension.
Below is an example from [Greenwood's codebase](https://github.com/ProjectEvergreen/greenwood/blob/master/packages/cli/src/plugins/resource/plugin-standard-javascript.js) for serving JavaScript files.

Expand Down
18 changes: 8 additions & 10 deletions src/pages/docs/resources/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Greenwood is ECMAScript Modules (ESM) first, as shown with the usage of the `typ
<!doctype html>
<html lang="en" prefix="og:http://ogp.me/ns#">
<head>
<script type="module" src="./path/to/script.js"></script>
<script type="module" src="../path/to/script.js"></script>
</head>

<body>
Expand All @@ -62,9 +62,9 @@ Greenwood is ECMAScript Modules (ESM) first, as shown with the usage of the `typ

<!-- prettier-ignore-end -->

Keep in mind that the specification dictates the following conventions when referencing ESM files:
Keep in mind that [the specification](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#module_specifier_resolution) dictates the following requirements when referencing ESM files:

1. It must be a relative path
1. It must be a relative specifier (starts with a `./`, `../`, or `/`)
1. It must have an extension

<!-- eslint-disable no-unused-vars -->
Expand All @@ -83,9 +83,9 @@ import { Foo } from "./foo";

## Node Modules

Packages from [**npm**](https://www.npmjs.com/) (and compatible registries) can be used by installing them with your favorite package manager. In the browser, Greenwood will automatically build up an import map from any packages defined in the **dependencies** property of your _package.json_.
Packages from [**npm**](https://www.npmjs.com/) (and compatible registries) can be used by installing them with your favorite package manager. In the browser, Greenwood will automatically build up an [import map](/docs/introduction/web-standards/#import-maps) so that you can use [bare specifiers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#module_specifier_resolution).

Below are some examples:
Below is an example of using a bare specifier in a JavaScript file:

<!-- prettier-ignore-start -->

Expand All @@ -112,7 +112,7 @@ Below are some examples:
<!-- prettier-ignore-end -->
You can reference **node_modules** directly from a `<script>` tag by starting the path with `/node_modules`:
You can reference **node_modules** directly from a `<script>` tag by starting the path with a "shortcut" alias of **/node_modules/**, which will signal to Greenwood to use `import.meta.resolve` to automatically resolve the full path for you:
<!-- prettier-ignore-start -->
Expand All @@ -138,10 +138,8 @@ You can reference **node_modules** directly from a `<script>` tag by starting th
<!-- prettier-ignore-end -->
The rule of thumb is:
- If it's a package from npm installed in **dependencies**, you can use bare specifiers and no extension
- Otherwise, you will need to use a relative path and the extension
> Relative paths will also work in this context if you are comfortable resolving the full path to _node_modules_ on your own, e.g.
> `<script src="../../node_modules/htmx.org/dist/htmx.js"></script>`
## Prerendering
Expand Down
27 changes: 19 additions & 8 deletions src/pages/docs/resources/styles.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ Styles can be done in any standards compliant way that will work in a browser. S

<!-- prettier-ignore-end -->

## NPM
## Node Modules

Packages from [**npm**](https://www.npmjs.com/) can be used by installing them with your favorite package manager.
Like [with scripts](/docs/resources/scripts/#node-modules), packages from [**npm**](https://www.npmjs.com/) (and compatible registries) can be used by installing them with your favorite package manager. Similar conventions apply in regards to using the **/node_modules/** "shortcut" alias to let Greenwood resolve the location using `import.meta.resolve`, or you can provide the full relative path yourself.

In a CSS file, you can use relative paths to resolve to _node_modules_:
Here is an example of using relative and shortcut paths in a CSS file:

<!-- prettier-ignore-start -->

Expand All @@ -60,17 +60,19 @@ In a CSS file, you can use relative paths to resolve to _node_modules_:
```css
/* after having installed Open Props */
/* npm i open-props */
@import "../../node_modules/open-props/src/props.borders.css";
@import "../../node_modules/open-props/src/props.fonts.css";
@import "../../node_modules/open-props/src/props.shadows.css";
@import "../../node_modules/open-props/src/props.sizes.css";
@import "../../node_modules/open-props/borders.min.css";
@import "../../node_modules/open-props/fonts.min.css";

/* this would also work */
@import "/node_modules/open-props/borders.min.css";
@import "/node_modules/open-props/fonts.min.css";
```

</app-ctc-block>

<!-- prettier-ignore-end -->

From an HTML file, you can reference **node_modules** by starting the path with _node_modules_:
The same can be done from an HTML file with a `<link>` tag:

<!-- prettier-ignore-start -->

Expand All @@ -93,3 +95,12 @@ From an HTML file, you can reference **node_modules** by starting the path with
</app-ctc-block>

<!-- prettier-ignore-end -->

These conventions are also compatible with [**Import Attributes**](/docs/introduction/web-standards/#import-attributes) and CSS Module Scripts. For example, since [Spectrum Web Components](https://opensource.adobe.com/spectrum-web-components/) expose its CSS through an exports map, bare CSS specifiers also work in Greenwood.

```js
import SpectrumCard from "@spectrum-css/card" with { type: "css" };
import SpectrumTokens from "@spectrum-css/tokens" with { type: "css" };

console.log({ SpectrumCard, SpectrumTokens });
```
6 changes: 3 additions & 3 deletions src/pages/guides/ecosystem/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ Or to use something CSS based like [**Open Props**](https://open-props.style), s
```html
<html>
<head>
<link ref="stylesheet" href="/node_modules/open-props/src/props.fonts.css" />
<link ref="stylesheet" href="/node_modules/open-props/src/props.shadows.css" />
<link ref="stylesheet" href="/node_modules/open-props/src/props.sizes.css" />
<link ref="stylesheet" href="/node_modules/open-props/fonts.min.css" />
<link ref="stylesheet" href="/node_modules/open-props/shadows.min.css" />
<link ref="stylesheet" href="/node_modules/open-props/sizes.min.css" />

<style>
h1 {
Expand Down
2 changes: 1 addition & 1 deletion src/pages/guides/ecosystem/lit.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ tocHeading: 2

[**Lit**](https://lit.dev/) builds on top of the Web Components standards, adding additional developer experience ergonomics like reactivity, declarative templates and reducing boilerplate. Lit also has support for SSR (server-side rendering), which Greenwood supports through a plugin.

> You can see a complete hybrid project example in this [demonstration repo](https://github.com/thescientist13/greenwood-lit-ssr).
> You can see a complete hybrid project example in this [demonstration repo](https://github.com/thescientist13/greenwood-lit-ssr) as well as [demos](https://github.com/thescientist13/greenwood-lit-ssr/pulls?q=is%3Apr+is%3Aopen+label%3Ademo) of using Lit based component libraries like [**Shoelace**](https://shoelace.style/) and [**Spectrum Web Components**](https://opensource.adobe.com/spectrum-web-components/) with Greenwood.
## Installation

Expand Down

0 comments on commit c0c4f64

Please sign in to comment.