@nika-js/onlymap 0.4.3 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +44 -11
  2. package/bin/onlymapjs.mjs +160 -11
  3. package/dist/{LercDecode.es-peqB7bxk.js → LercDecode.es-B49qauXg.js} +1 -1
  4. package/dist/{basemap-BeKuGQJs.js → basemap-nJ_ODY7r.js} +168 -163
  5. package/dist/basemap.d.ts +3 -1
  6. package/dist/elements/om-map.d.ts +68 -0
  7. package/dist/{index-BPKvpqIk.js → index-CeOjfL2p.js} +2 -2
  8. package/dist/{index-DzyVbu_T.js → index-DSkgI4Oc.js} +12996 -12463
  9. package/dist/{index-BKU3HvOO.js → index-DjlGZov2.js} +1 -1
  10. package/dist/{index-DVnTYF5u.js → index-o_yRv2lD.js} +1 -1
  11. package/dist/{index-Dg8SKcgV.js → index-uDJbHjA9.js} +1 -1
  12. package/dist/index.d.ts +4 -2
  13. package/dist/layer-registry.d.ts +8 -0
  14. package/dist/layout-audit.d.ts +53 -0
  15. package/dist/{lerc-DEpGyngt.js → lerc-CaQOmDCP.js} +2 -2
  16. package/dist/onlymap.standalone.js +22201 -21663
  17. package/dist/onlymapjs.js +78 -63
  18. package/dist/programmatic.d.ts +7 -1
  19. package/dist/{raster-CX7Y4jtJ.js → raster-DMq8FgiV.js} +2 -2
  20. package/dist/react/context.d.ts +9 -1
  21. package/dist/react/om-map.d.ts +3 -1
  22. package/dist/react/om-widget.d.ts +3 -1
  23. package/dist/react.js +336 -204
  24. package/dist/runtime-core.d.ts +20 -1
  25. package/dist/testing.d.ts +6 -0
  26. package/dist/version.d.ts +1 -1
  27. package/dist/widget-layout.d.ts +36 -0
  28. package/docs/basemaps.md +1 -1
  29. package/docs/react.md +2 -1
  30. package/docs/testing.md +13 -1
  31. package/llms.txt +2 -2
  32. package/onlymapjs.html-data.json +18 -0
  33. package/package.json +2 -2
  34. package/skills/onlymapjs/SKILL.md +3 -2
  35. package/skills/onlymapjs/references/react.md +2 -2
  36. package/skills/onlymapjs/references/syntax.md +4 -4
  37. package/skills/onlymapjs/references/testing.md +16 -0
package/README.md CHANGED
@@ -69,7 +69,7 @@ Or with no build step at all, straight from a CDN:
69
69
 
70
70
  The bare package URL serves `dist/onlymap.standalone.js`, a single-file bundle built for exactly this (jsDelivr too). Use a CDN that serves the package's raw files — **not** a rebundling CDN like esm.sh, which re-splits the bundle into duplicate copies of the deck.gl/luma.gl runtime and breaks every layer's shader compilation.
71
71
 
72
- Then `npx @nika-js/onlymap init` wires up VS Code IntelliSense and `!`-prefixed manifest snippets for your project. The library ships with 528 unit/behavioral tests and 45 Playwright GPU tests.
72
+ Then `npx @nika-js/onlymap init` wires up VS Code IntelliSense and `!`-prefixed manifest snippets for your project. The library ships with 540 unit/behavioral tests and 53 Playwright GPU tests.
73
73
 
74
74
  The [examples](https://github.com/NikaGeospatial/onlymapjs/tree/main/examples) are the best tour: widgets, behaviors & overlays, basemaps, columnar/Arrow data, manual drawing, 3D models, scene lighting (with the native lighting widget), DEM terrain, a live WebSocket ship feed, and a polled driver fleet.
75
75
 
@@ -81,7 +81,7 @@ A handful of elements, one rule: **attributes are kebab-case versions of deck.gl
81
81
  |---|---|
82
82
  | `<om-map>` | The map. `center`, `zoom`, `pitch`, `bearing`; `basemap` takes a free preset (`positron`, `liberty`, `dark-matter`, `osm`, …), a style URL, or `"none"` (standalone canvas) — and switches **live**; `validate` for a live on-page error panel. |
83
83
  | `<om-layer>` | Any of **34 layer types** by name — all of deck.gl's core, geo, aggregation, and mesh layers (Scatterplot, GeoJson, Arc, Path, Heatmap, Hexagon, Trips, Tile, Tile3D, Scenegraph, …) plus the built-in `PopupLayer` for WebGL badges/labels at scale and the native `COGLayer` for GeoTIFF rasters. `id` required; `label`/`color` feed the legend. |
84
- | `<om-widget>` | UI panels. Built-ins: `legend` (symbology-aware: color scales render as gradient ramps or class ranges, categorical ternaries as discrete palettes), `layer-switcher`, `basemap-switcher`, `lighting`, `zoom-controls`, `undo-redo`, `scale-bar`, `attribution`, `filter`, `draw`, `vega-lite` (live charts). Or write your own inline with HTML + a `<script type="om/widget">`. Adjacent compact button widgets (`zoom-controls`, `undo-redo`, `widgets-toggle`) **auto-cluster** into one control group (opt out per widget with `cluster="false"`), and `<om-map widgets-hidden>` / the `set-widgets-visible` action / `<om-widget type="widgets-toggle">` hide all chrome without destroying it — attribution never hides. **Placement is managed**: `position` takes one of 8 logical, RTL-aware slots (`top-start`, `top-center`, `top-end`, `center-start`, `center-end`, `bottom-start`, `bottom-center`, `bottom-end`; legacy corner names alias) — same-slot widgets stack with flush edges and a shared gap, `order` sets in-slot ordering, and `position="manual"` opts out entirely (a plain block you style yourself, even outside the map). A slot dims automatically while an open popup covers it (`widgets-dim="off"` to disable). Themeable from plain page CSS via custom properties: `om-map { --om-widget-bg: #111827; --om-widget-fg: #f9fafb; }` (also `-muted`, `-border`, `-hover-bg`, `-accent`), plus layout tokens (`--om-widget-inset-x/-y`, `--om-widget-gap-x/-y`, `--om-widget-opacity`, `--om-widget-radius`) or the no-CSS sugar `<om-map widget-style="gap:10 opacity:0.9">`. |
84
+ | `<om-widget>` | UI panels. Built-ins: `legend` (symbology-aware: color scales render as gradient ramps or class ranges, categorical ternaries as discrete palettes), `layer-switcher`, `basemap-switcher`, `lighting`, `zoom-controls`, `undo-redo`, `scale-bar`, `attribution`, `filter`, `draw`, `vega-lite` (live charts). Or write your own inline with HTML + a `<script type="om/widget">`. Adjacent compact button widgets (`zoom-controls`, `undo-redo`, `widgets-toggle`) **auto-cluster** into one control group (opt out per widget with `cluster="false"`), and `<om-map widgets-hidden>` / the `set-widgets-visible` action / `<om-widget type="widgets-toggle">` hide all authored chrome without destroying it — provider attribution and the license badge never hide. **Placement is managed**: `position` takes one of 8 logical, RTL-aware slots (`top-start`, `top-center`, `top-end`, `center-start`, `center-end`, `bottom-start`, `bottom-center`, `bottom-end`; legacy corner names alias) — same-slot widgets stack with flush edges and a shared gap, `order` sets in-slot ordering, and `position="manual"` opts out entirely (a plain block you style yourself, even outside the map). At map widths ≤640px, managed widgets automatically move into accessible top/end/bottom/start drawers; `fold="never"` keeps an essential control out, `widgets-fold="off"` disables folding, and `--om-widget-fold-breakpoint` changes the map-width threshold. Provider attribution is an in-flow member of `bottom-end` and the license badge of `bottom-start`, so neither covers a widget. A slot dims automatically while an open popup covers it (`widgets-dim="off"` to disable), except slots containing required chrome. Themeable from plain page CSS via custom properties: `om-map { --om-widget-bg: #111827; --om-widget-fg: #f9fafb; }` (also `-muted`, `-border`, `-hover-bg`, `-accent`), plus layout tokens (`--om-widget-inset-x/-y`, `--om-widget-gap-x/-y`, `--om-widget-opacity`, `--om-widget-radius`) or the no-CSS sugar `<om-map widget-style="gap:10 opacity:0.9">`. |
85
85
  | `<om-overlay>` | Rich HTML anchored to a map location — a static `anchor="[lng, lat]"`, the current selection, or a feature's own geometry via `anchor-layer`/`anchor-feature-id`. `{{field}}` interpolates the picked feature, HTML-escaped by default. |
86
86
  | `<om-behavior>` | Declarative interactions: `on="click|hover|drag|load|data-loaded"` → a named action. |
87
87
  | `<om-story>` | A storyboard: `<om-step>` children fire actions on a timeline. Controlled by the `player` widget, behaviors, or `storyEl.play()/pause()/seek()`. |
@@ -172,23 +172,54 @@ That opt-in command updates `.vscode/settings.json` and copies the `!`-prefixed
172
172
 
173
173
  The package also ships `!`-prefixed manifest snippets (`node_modules/@nika-js/onlymap/.vscode/onlymap.code-snippets`) — type `!starter`, `!map`, `!layer`, `!draw`, etc. to scaffold a well-formed element.
174
174
 
175
- ## LLM Skill
175
+ ## Quick-start guide
176
176
 
177
- The npm package and public mirror include a portable Skill at `skills/onlymapjs`. Skill-aware agents can install that folder to learn the OnlyMapJS manifest syntax, authoring patterns, and validation workflow instead of treating the library like raw deck.gl.
177
+ ### Use OnlyMapJS with Claude Code and Codex
178
178
 
179
- With the Vercel Labs `skills` CLI, install it from the public GitHub repo:
179
+ The public repo includes a portable `onlymapjs` Skill that teaches coding agents the manifest syntax, React adapter, common map patterns, and validation/testing workflow. Install the library in the app you want to map:
180
180
 
181
181
  ```bash
182
+ npm install @nika-js/onlymap
183
+ ```
184
+
185
+ Then, from that app's repository root, install the Skill for your agent:
186
+
187
+ ```bash
188
+ # Claude Code
189
+ npx -y skills add NikaGeospatial/onlymapjs --skill onlymapjs --agent claude-code
190
+
191
+ # Codex
182
192
  npx -y skills add NikaGeospatial/onlymapjs --skill onlymapjs --agent codex
183
193
  ```
184
194
 
185
- Or install globally for Codex:
195
+ Using both agents in the same repository? Install it for both in one command:
186
196
 
187
197
  ```bash
188
- npx -y skills add NikaGeospatial/onlymapjs --skill onlymapjs --agent codex --global
198
+ npx -y skills add NikaGeospatial/onlymapjs \
199
+ --skill onlymapjs \
200
+ --agent claude-code \
201
+ --agent codex
202
+ ```
203
+
204
+ Start or restart Claude Code or Codex in that repository, then describe the map you want. Mention the installed Skill explicitly when you want to guarantee it is used:
205
+
206
+ ```text
207
+ Use the installed OnlyMapJS skill to build a full-screen map in this project.
208
+ Inspect my data before choosing accessors. Add a legend and click details,
209
+ include a no-JavaScript fallback, validate the result, and run the relevant tests.
189
210
  ```
190
211
 
191
- To inspect it without installing:
212
+ For an existing React app:
213
+
214
+ ```text
215
+ Use the installed OnlyMapJS skill to add this map to my React app. Use the
216
+ @nika-js/onlymap/react adapter, preserve the app's existing state patterns,
217
+ and verify the result with the library's headless testing tools.
218
+ ```
219
+
220
+ The Skill steers HTML projects toward declarative `om-*` manifests and React projects toward the first-party adapter. It also tells the agent to use `OmMap.validate()`, `OmMap.snapshotIR()`, and the headless harness rather than guessing whether the generated map works.
221
+
222
+ To inspect the Skill before installing it:
192
223
 
193
224
  ```bash
194
225
  npx -y skills add NikaGeospatial/onlymapjs --list
@@ -213,7 +244,9 @@ await h.pick({ layer: "quakes", featureId: "q1" }); // same code path as a real
213
244
  expect(overlay.shadowRoot.textContent).toContain("M 6.5");
214
245
  ```
215
246
 
216
- Plus `OmMap.snapshotIR(html)` to lock down what a manifest *means* in a snapshot test, and `await mapEl.ready` / the `om-map-ready` event to de-flake browser e2e tests. Full guide: [docs/testing.md](docs/testing.md).
247
+ Plus `OmMap.snapshotIR(html)` to lock down what a manifest *means* in a snapshot test, and `await mapEl.ready` / the `om-map-ready` event to de-flake browser e2e tests.
248
+
249
+ **Layout, checked like types.** `npx @nika-js/onlymap check-layout <manifest.html>` audits a page's real rendered widget layout in headless Chromium — no screenshots, no eyeballing: it asserts no widgets overlap, each is hit-testable (not painted under the canvas), each sits in its slot, and same-slot edges are flush, across a viewport-width sweep, emitting structured `{severity, element, message, fix}` diagnostics and exiting 0/1. The same audit is importable as `auditLayout(page)` for your own Playwright suite. Full guide: [docs/testing.md](docs/testing.md).
217
250
 
218
251
  ## 3D
219
252
 
@@ -223,8 +256,8 @@ Plus `OmMap.snapshotIR(html)` to lock down what a manifest *means* in a snapshot
223
256
 
224
257
  - **`OmMap.*`** — `validate`, `snapshotIR`, `registerLayer`, `registerWidget`, `registerAction`, `registerSource`, `registerFormat`, `registerBasemap`, `configureBasemap`, `configureData`, `configureTelemetry`, `configureLicense`, `getLayerSchema`
225
258
  - **`@nika-js/onlymap/deck`** — the bundled deck.gl classes (`CompositeLayer`, `TileLayer`, …) for building custom layer types: shims must extend the same class hierarchy the core renders with, not a second installed deck.gl copy. Recipe: [docs/custom-layers.md](docs/custom-layers.md)
226
- - **On a `<om-map>` element** — `ready` (promise), `flyTo(coords, zoom?)`, `setLayerVisible(id, bool)`, `getLayers()`, `emit(action, payload)`, `snapshot(opts?)` (canvas-only PNG of basemap + layers at device pixels — DOM widgets/overlays and provider attribution are NOT captured, so exports must render credits themselves; `{as: "blob"}` for files, default dataURL); the `om-view-changed` event fires once the camera settles (debounced; `detail` = `{longitude, latitude, zoom, pitch, bearing, origin}`, where `origin` is `"user"` for gesture-driven bursts vs `"programmatic"` for API/story moves — the echo-suppression signal for state sync) — the camera-persistence hook; `document.querySelector("om-map")` is fully typed
227
- - **`MapController`** — the framework-grade programmatic front-end (typed `LayerDescriptor`s → the same reconcile core, no DOM manifest): `setLayers`, `watch`, `emit`, camera methods, `injectPick`, `ready`, `snapshot`, an `onViewChange(view, origin)` option (the `om-view-changed` twin). The React adapter rides it; usable directly from vanilla TS or other frameworks
259
+ - **On a `<om-map>` element** — `ready` (promise), `flyTo(coords, zoom?)`, `setLayerVisible(id, bool)`, `getLayers()`, `emit(action, payload)`, `snapshot(opts?)` (canvas-only PNG of basemap + layers at device pixels — DOM widgets/overlays and provider attribution are NOT captured, so exports must render credits themselves; `{as: "blob"}` for files, default dataURL); the `om-view-changed` event fires once the camera settles (debounced; `detail` = `{longitude, latitude, zoom, pitch, bearing, origin}`, where `origin` is `"user"` for gesture-driven bursts vs `"programmatic"` for API/story moves — the echo-suppression signal for state sync) — the camera-persistence hook; the `om-map-point` event (`detail = {coordinate: [lng,lat]|null, kind: "click"|"hover"}`) fires on every click/hover with the map coordinate, including empty-map clicks picks discard — the hook for custom capture tools the built-in draw widget doesn't cover; the `om-tileset-load` event (`detail = {layerId, tileset}`) surfaces a `Tile3DLayer`'s live deck `Tileset3D` for tools that need the real tileset (e.g. region export), not the IR; `document.querySelector("om-map")` is fully typed
260
+ - **`MapController`** — the framework-grade programmatic front-end (typed `LayerDescriptor`s → the same reconcile core, no DOM manifest): `setLayers`, `watch`, `emit`, camera methods, `injectPick`, `ready`, `snapshot`, an `onViewChange(view, origin)` option (the `om-view-changed` twin), plus `onMapPoint` / `onTilesetLoad` options (the `om-map-point` / `om-tileset-load` twins). The React adapter rides it; usable directly from vanilla TS or other frameworks
228
261
  - **`getStore(token)`** — the external-store contract: per-token `{subscribe, getSnapshot}` stores (`viewport`/`selection`/`layers`/`data:<id>`) with cached immutable plain-data snapshots and `origin` tagging — directly consumable by `useSyncExternalStore` (the React adapter's own hooks ride it), MobX autoruns, Redux listeners, Zustand mirrors. ~20-line integration-tested recipes for Redux Toolkit, MobX/mobx-keystone, Zustand, and Jotai: [docs/external-stores.md](docs/external-stores.md)
229
262
  - **Testing** — `mountForTest`, and imports are SSR-safe (importing in Node/jsdom never touches browser globals)
230
263
 
package/bin/onlymapjs.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
- import { dirname, join, resolve } from "node:path";
2
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
3
+ import { createServer } from "node:http";
4
+ import { dirname, extname, join, resolve, sep } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
 
6
7
  const CUSTOM_DATA = "./node_modules/@nika-js/onlymap/onlymapjs.html-data.json";
@@ -9,16 +10,161 @@ const SNIPPET_FILE = "onlymap.code-snippets";
9
10
  function usage() {
10
11
  console.log(`Usage:
11
12
  onlymapjs init [--force]
13
+ onlymapjs check-layout <manifest.html>
12
14
  onlymapjs --help
13
15
 
14
16
  Commands:
15
17
  init Configure the current project for OnlyMapJS authoring in VS Code.
18
+ check-layout
19
+ Load a browser-runnable manifest in isolated headless Chromium,
20
+ audit real widget geometry at 360/640/768/1024px, and exit 0/1.
16
21
 
17
22
  Options:
18
23
  --force Overwrite .vscode/${SNIPPET_FILE} if it already exists.
19
24
  `);
20
25
  }
21
26
 
27
+ const MIME_TYPES = {
28
+ ".html": "text/html; charset=utf-8",
29
+ ".js": "text/javascript; charset=utf-8",
30
+ ".mjs": "text/javascript; charset=utf-8",
31
+ ".css": "text/css; charset=utf-8",
32
+ ".json": "application/json; charset=utf-8",
33
+ ".geojson": "application/geo+json; charset=utf-8",
34
+ ".csv": "text/csv; charset=utf-8",
35
+ ".svg": "image/svg+xml",
36
+ ".png": "image/png",
37
+ ".jpg": "image/jpeg",
38
+ ".jpeg": "image/jpeg",
39
+ ".webp": "image/webp",
40
+ ".wasm": "application/wasm",
41
+ };
42
+
43
+ function isInside(root, candidate) {
44
+ return candidate === root || candidate.startsWith(`${root}${sep}`);
45
+ }
46
+
47
+ function browserManifestHtml(text) {
48
+ // A plain static server cannot resolve package bare specifiers. Rewrite
49
+ // the root OnlyMapJS entry only in the ephemeral response; relative data
50
+ // and all authored source files keep their original URLs.
51
+ return text
52
+ .replaceAll('"@nika-js/onlymap"', '"/__onlymapjs__/dist/onlymapjs.js"')
53
+ .replaceAll("'@nika-js/onlymap'", "'/__onlymapjs__/dist/onlymapjs.js'")
54
+ .replaceAll('"@nika-js/onlymap/onlymapjs.css"', '"/__onlymapjs__/dist/onlymapjs.css"')
55
+ .replaceAll("'@nika-js/onlymap/onlymapjs.css'", "'/__onlymapjs__/dist/onlymapjs.css'");
56
+ }
57
+
58
+ async function startStaticServer(manifestPath, packageRoot) {
59
+ const manifestRoot = dirname(manifestPath);
60
+ const server = createServer((req, res) => {
61
+ try {
62
+ const pathname = decodeURIComponent(new URL(req.url ?? "/", "http://127.0.0.1").pathname);
63
+ let target;
64
+ let root;
65
+ let isManifest = false;
66
+ if (pathname === "/") {
67
+ target = manifestPath;
68
+ root = manifestRoot;
69
+ isManifest = true;
70
+ } else if (pathname.startsWith("/__onlymapjs__/")) {
71
+ root = packageRoot;
72
+ target = resolve(root, pathname.slice("/__onlymapjs__/".length));
73
+ } else {
74
+ root = manifestRoot;
75
+ target = resolve(root, pathname.slice(1));
76
+ }
77
+ if (!isInside(root, target) || !existsSync(target) || !statSync(target).isFile()) {
78
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
79
+ res.end("Not found");
80
+ return;
81
+ }
82
+ const type = MIME_TYPES[extname(target).toLowerCase()] ?? "application/octet-stream";
83
+ const content = readFileSync(target);
84
+ res.writeHead(200, { "content-type": type, "cache-control": "no-store" });
85
+ res.end(isManifest ? browserManifestHtml(content.toString("utf8")) : content);
86
+ } catch (error) {
87
+ res.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
88
+ res.end(error instanceof Error ? error.message : String(error));
89
+ }
90
+ });
91
+ await new Promise((resolveListen, reject) => {
92
+ server.once("error", reject);
93
+ server.listen(0, "127.0.0.1", resolveListen);
94
+ });
95
+ const address = server.address();
96
+ if (!address || typeof address === "string") throw new Error("Could not allocate the local layout-audit server.");
97
+ return { server, url: `http://127.0.0.1:${address.port}/` };
98
+ }
99
+
100
+ async function checkLayout(manifestArg) {
101
+ if (!manifestArg) throw new Error("check-layout needs a manifest HTML path.\nUsage: onlymapjs check-layout <manifest.html>");
102
+ const manifestPath = resolve(process.cwd(), manifestArg);
103
+ if (!existsSync(manifestPath) || !statSync(manifestPath).isFile()) {
104
+ throw new Error(`Manifest not found: ${manifestPath}`);
105
+ }
106
+ if (extname(manifestPath).toLowerCase() !== ".html") {
107
+ throw new Error(`check-layout expects an .html manifest, received: ${manifestPath}`);
108
+ }
109
+
110
+ let playwright;
111
+ try {
112
+ playwright = await import("playwright");
113
+ } catch (error) {
114
+ if (error?.code === "ERR_MODULE_NOT_FOUND" || String(error).includes("Cannot find package 'playwright'")) {
115
+ throw new Error(
116
+ "check-layout needs Playwright in this project.\n" +
117
+ "Install it with:\n npm install --save-dev playwright\n npx playwright install chromium",
118
+ );
119
+ }
120
+ throw error;
121
+ }
122
+
123
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
124
+ const { auditLayout } = await import(new URL("../dist/onlymapjs.js", import.meta.url));
125
+ const { server, url } = await startStaticServer(manifestPath, packageRoot);
126
+ let browser;
127
+ try {
128
+ console.log(`Auditing ${manifestPath}`);
129
+ console.log("Note: check-layout executes this page's scripts inside an isolated headless Chromium context.");
130
+ try {
131
+ browser = await playwright.chromium.launch({ headless: true });
132
+ } catch (error) {
133
+ if (String(error).includes("Executable doesn't exist") || String(error).includes("browserType.launch")) {
134
+ throw new Error(`Chromium is not installed for Playwright.\nRun:\n npx playwright install chromium\n\n${String(error)}`);
135
+ }
136
+ throw error;
137
+ }
138
+ const context = await browser.newContext({ serviceWorkers: "block" });
139
+ const page = await context.newPage();
140
+ const pageErrors = [];
141
+ page.on("pageerror", (error) => pageErrors.push(String(error).split("\n")[0]));
142
+ page.on("console", (msg) => {
143
+ if (msg.type() === "error") pageErrors.push(msg.text().split("\n")[0]);
144
+ });
145
+ await page.goto(url, { waitUntil: "domcontentloaded" });
146
+ let diagnostics;
147
+ try {
148
+ diagnostics = await auditLayout(page);
149
+ } catch (error) {
150
+ // The page never produced a map. Its own console/pageerror output is the
151
+ // real cause — surface it so the failure is self-explaining.
152
+ const detail = pageErrors.length ? `\nPage errors:\n ${pageErrors.slice(0, 3).join("\n ")}` : "";
153
+ throw new Error(`${error instanceof Error ? error.message : String(error)}${detail}`);
154
+ }
155
+ if (diagnostics.length === 0) {
156
+ console.log("CLEAN: no widget layout problems at 360, 640, 768, or 1024px.");
157
+ return 0;
158
+ }
159
+ for (const diagnostic of diagnostics) console.error(JSON.stringify(diagnostic));
160
+ console.error(`FAILED: ${diagnostics.length} layout diagnostic(s).`);
161
+ return diagnostics.some((diagnostic) => diagnostic.severity === "error") ? 1 : 0;
162
+ } finally {
163
+ await browser?.close();
164
+ await new Promise((resolveClose) => server.close(resolveClose));
165
+ }
166
+ }
167
+
22
168
  function parseJsonc(text) {
23
169
  let out = "";
24
170
  let inString = false;
@@ -167,16 +313,19 @@ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
167
313
  process.exit(0);
168
314
  }
169
315
 
170
- const [command, ...rest] = args;
171
- if (command !== "init") {
172
- console.error(`Unknown command: ${command}\n`);
173
- usage();
174
- process.exit(1);
175
- }
176
-
177
316
  try {
178
- init({ force: rest.includes("--force") });
317
+ const [command, ...rest] = args;
318
+ if (command === "init") {
319
+ init({ force: rest.includes("--force") });
320
+ } else if (command === "check-layout") {
321
+ if (rest.length !== 1) throw new Error("Usage: onlymapjs check-layout <manifest.html>");
322
+ process.exitCode = await checkLayout(rest[0]);
323
+ } else {
324
+ console.error(`Unknown command: ${command}\n`);
325
+ usage();
326
+ process.exitCode = 1;
327
+ }
179
328
  } catch (err) {
180
329
  console.error(err instanceof Error ? err.message : String(err));
181
- process.exit(1);
330
+ process.exitCode = 1;
182
331
  }
@@ -19,7 +19,7 @@ async function v(N = {}) {
19
19
  if (n) {
20
20
  const { createRequire: I } = await import(
21
21
  /*webpackIgnore:true*/
22
- "./lerc-DEpGyngt.js"
22
+ "./lerc-CaQOmDCP.js"
23
23
  ).then((g) => g._);
24
24
  var O = I(import.meta.url);
25
25
  }