Skip to content

Provide an actionable error when import map is missing (CDN usage) #226

Description

@DmitrySharabin

Problem

After the switch to Nudeps (#218), importing components directly from the Netlify CDN (elements.colorjs.io) without an import map produces a cryptic browser error:

Uncaught TypeError: The specifier "colorjs.io" was a bare specifier, but was not remapped to anything. Relative module specifiers must start with "./", "../" or "/".

This was reported in #225 and affected apps.colorjs.io (see color-js/apps#28, fixed in color-js/apps#29).

While the immediate breakage was fixed by adding the import map in the apps repo, any user loading components from elements.colorjs.io without the import map will hit the same cryptic error:

<!-- This fails with a confusing error -->
<script type="module" src="https://elements.colorjs.io/color-picker/color-picker.js"></script>

They need to include the import map first:

<script src="https://elements.colorjs.io/importmap.js"></script>
<script type="module" src="https://elements.colorjs.io/color-picker/color-picker.js"></script>

Root cause

The base class (src/common/color-element.js) uses bare specifier imports:

import Color from "colorjs.io";
import NudeElement from "nude-element";
import { states } from "nude-element/plugins";

Static imports are resolved during the ES module "link" phase — before any module body code runs. This means we cannot wrap them in try…catch or run import.meta.resolve() first. By the time any of our code could execute, the module has already failed.

Workarounds (available today)

Users can avoid the issue entirely by not importing from the Netlify CDN:

  • Use esm.sh (or another CDN that resolves specifiers server-side) — already documented in the README:
    <script src="https://esm.sh/color-elements" type="module"></script>
  • Use Nudeps — manages import maps and bare specifiers without a bundler
  • Use a bundler (webpack, Vite, Rollup, etc.) — resolves bare specifiers at build time

These don't solve the detection problem, but they're the recommended paths for users who don't want to manage import maps manually.

Possible solutions (detection)

1. Documentation only

Add a note to the README about needing the import map when loading from elements.colorjs.io.

  • Pro: No code changes, no degradation
  • Con: Easy to miss; doesn't help users who hit the error

2. Convert bare specifier imports to dynamic imports with detection

Replace static imports with import.meta.resolve() check + await import():

for (const specifier of ["colorjs.io", "nude-element", "nude-element/plugins"]) {
	try {
		import.meta.resolve(specifier);
	}
	catch {
		throw new Error(
			`color-elements: Cannot resolve "${specifier}". `
			+ `Include the import map first: `
			+ `<script src="https://elements.colorjs.io/importmap.js"><\/script>`,
		);
	}
}

const { default: Color } = await import("colorjs.io");
const { default: NudeElement } = await import("nude-element");
const { states } = await import("nude-element/plugins");

This could live directly in color-element.js or be isolated in a separate deps.js module.

  • Pro: Users get an actionable error message at runtime
  • Pro: import.meta.resolve() is synchronous — no network overhead for the check
  • Con: Degrades the codebase — static imports become dynamic
  • Con: Requires top-level await (supported in all modern browsers, but changes module semantics)
  • Con: Worse static analysis for bundlers (though practical impact is negligible since these deps are used in full)

3. Separate optional check script

A standalone module that users can load before component scripts. Checks import.meta.resolve() and warns.

  • Pro: Core code stays untouched
  • Con: Users who forget the import map are unlikely to include this either

Notes

  • import.meta.resolve() is supported in Chrome 105+, Firefox 106+, Safari 16.4+ — aligns with the project's modern browser target
  • The recommended CDN usage via esm.sh (in the README) resolves specifiers server-side and is unaffected
  • Both colorjs.io and nude-element are bare specifiers that need handling, not just nude-element

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions