@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.
- package/README.md +44 -11
- package/bin/onlymapjs.mjs +160 -11
- package/dist/{LercDecode.es-peqB7bxk.js → LercDecode.es-B49qauXg.js} +1 -1
- package/dist/{basemap-BeKuGQJs.js → basemap-nJ_ODY7r.js} +168 -163
- package/dist/basemap.d.ts +3 -1
- package/dist/elements/om-map.d.ts +68 -0
- package/dist/{index-BPKvpqIk.js → index-CeOjfL2p.js} +2 -2
- package/dist/{index-DzyVbu_T.js → index-DSkgI4Oc.js} +12996 -12463
- package/dist/{index-BKU3HvOO.js → index-DjlGZov2.js} +1 -1
- package/dist/{index-DVnTYF5u.js → index-o_yRv2lD.js} +1 -1
- package/dist/{index-Dg8SKcgV.js → index-uDJbHjA9.js} +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/layer-registry.d.ts +8 -0
- package/dist/layout-audit.d.ts +53 -0
- package/dist/{lerc-DEpGyngt.js → lerc-CaQOmDCP.js} +2 -2
- package/dist/onlymap.standalone.js +22201 -21663
- package/dist/onlymapjs.js +78 -63
- package/dist/programmatic.d.ts +7 -1
- package/dist/{raster-CX7Y4jtJ.js → raster-DMq8FgiV.js} +2 -2
- package/dist/react/context.d.ts +9 -1
- package/dist/react/om-map.d.ts +3 -1
- package/dist/react/om-widget.d.ts +3 -1
- package/dist/react.js +336 -204
- package/dist/runtime-core.d.ts +20 -1
- package/dist/testing.d.ts +6 -0
- package/dist/version.d.ts +1 -1
- package/dist/widget-layout.d.ts +36 -0
- package/docs/basemaps.md +1 -1
- package/docs/react.md +2 -1
- package/docs/testing.md +13 -1
- package/llms.txt +2 -2
- package/onlymapjs.html-data.json +18 -0
- package/package.json +2 -2
- package/skills/onlymapjs/SKILL.md +3 -2
- package/skills/onlymapjs/references/react.md +2 -2
- package/skills/onlymapjs/references/syntax.md +4 -4
- 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
|
|
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
|
|
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
|
-
##
|
|
175
|
+
## Quick-start guide
|
|
176
176
|
|
|
177
|
-
|
|
177
|
+
### Use OnlyMapJS with Claude Code and Codex
|
|
178
178
|
|
|
179
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
330
|
+
process.exitCode = 1;
|
|
182
331
|
}
|