create-honua-app 0.1.1

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 (33) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +50 -0
  3. package/bin/create-honua-app.mjs +7 -0
  4. package/lib/cli.mjs +149 -0
  5. package/lib/scaffold.mjs +134 -0
  6. package/lib/templates.mjs +138 -0
  7. package/package.json +32 -0
  8. package/templates/react-ts/.stackblitzrc +4 -0
  9. package/templates/react-ts/README.md +64 -0
  10. package/templates/react-ts/_gitignore +6 -0
  11. package/templates/react-ts/fixtures/features.json +46 -0
  12. package/templates/react-ts/fixtures/layer.json +34 -0
  13. package/templates/react-ts/index.html +12 -0
  14. package/templates/react-ts/package.json +31 -0
  15. package/templates/react-ts/src/App.tsx +153 -0
  16. package/templates/react-ts/src/fixture-endpoint.ts +9 -0
  17. package/templates/react-ts/src/main.tsx +18 -0
  18. package/templates/react-ts/src/styles.css +149 -0
  19. package/templates/react-ts/tsconfig.json +15 -0
  20. package/templates/react-ts/vite.config.ts +50 -0
  21. package/templates/vanilla-ts/.stackblitzrc +4 -0
  22. package/templates/vanilla-ts/README.md +60 -0
  23. package/templates/vanilla-ts/_gitignore +6 -0
  24. package/templates/vanilla-ts/fixtures/features.json +46 -0
  25. package/templates/vanilla-ts/fixtures/layer.json +34 -0
  26. package/templates/vanilla-ts/index.html +37 -0
  27. package/templates/vanilla-ts/package.json +27 -0
  28. package/templates/vanilla-ts/src/fixture-endpoint.ts +9 -0
  29. package/templates/vanilla-ts/src/main.ts +113 -0
  30. package/templates/vanilla-ts/src/styles.css +149 -0
  31. package/templates/vanilla-ts/tsconfig.json +14 -0
  32. package/templates/vanilla-ts/vite.config.ts +50 -0
  33. package/templates.manifest.json +61 -0
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "create-honua-app",
3
+ "version": "0.1.1",
4
+ "description": "Scaffold a Honua JavaScript SDK map application: Vite + TypeScript + MapLibre starters that connect and mount a source without hand-assembling peers",
5
+ "license": "Apache-2.0",
6
+ "author": "Honua",
7
+ "homepage": "https://honua-io.github.io/honua-sdk-js/",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/honua-io/honua-sdk-js.git",
11
+ "directory": "packages/create-honua-app"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/honua-io/honua-sdk-js/issues"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "provenance": true
19
+ },
20
+ "keywords": ["gis", "geospatial", "maplibre", "typescript", "honua", "scaffold", "starter", "create-app"],
21
+ "type": "module",
22
+ "bin": {
23
+ "create-honua-app": "bin/create-honua-app.mjs"
24
+ },
25
+ "exports": {
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": ["bin", "lib", "templates", "templates.manifest.json", "README.md", "LICENSE"],
29
+ "engines": {
30
+ "node": ">=20.19.0"
31
+ }
32
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "installDependencies": true,
3
+ "startCommand": "npm run dev"
4
+ }
@@ -0,0 +1,64 @@
1
+ # Honua map app (React + TypeScript)
2
+
3
+ A Vite + React + TypeScript starter for the [Honua JavaScript SDK](https://github.com/honua-io/honua-sdk-js). The app
4
+ owns a plain `maplibre-gl` map and the SDK's React bridge mounts a discovered source onto it:
5
+
6
+ ```text
7
+ connect → source → useMountedSource
8
+ ```
9
+
10
+ ## Run it
11
+
12
+ ```bash
13
+ npm install
14
+ npm run dev
15
+ ```
16
+
17
+ The default lane serves a committed GeoServices fixture from the Vite dev server, so the first map needs no account,
18
+ no API key, and no third-party network call. The same fixture is served by `npm run preview` after `npm run build`.
19
+
20
+ ## Point it at live data
21
+
22
+ ```bash
23
+ VITE_HONUA_ENDPOINT=https://your-public-service.example/rest/services/example/FeatureServer/0 npm run dev
24
+ ```
25
+
26
+ - `VITE_HONUA_ENDPOINT` — an anonymous, CORS-enabled GeoServices FeatureServer layer or OGC API Features landing page.
27
+ - `VITE_HONUA_PROTOCOL` — `auto`, `geoservices-feature-service`, or `ogc-features` (default:
28
+ `geoservices-feature-service`, which matches the fixture).
29
+
30
+ Do not put durable credentials in Vite environment variables: Vite embeds them in public JavaScript. Use an anonymous
31
+ endpoint or a server-side proxy.
32
+
33
+ ## What to edit
34
+
35
+ | File | Why |
36
+ | --- | --- |
37
+ | `src/App.tsx` | Connection, query, and the mounted-source hook. Change the query here. |
38
+ | `src/main.tsx` | React root. `StrictMode` stays on: the SDK hooks are StrictMode-safe. |
39
+ | `src/fixture-endpoint.ts` | The same-origin path the offline fixture is served on. |
40
+ | `vite.config.ts` | The fixture service. Delete it once you point the app at a real endpoint. |
41
+
42
+ `HonuaMapProvider` publishes an app-owned map to descendants when the map is created deeper in the tree; this starter
43
+ passes the map to `useMountedSource` directly. Add `@vitejs/plugin-react` if you want React Fast Refresh — Vite
44
+ compiles the JSX without it.
45
+
46
+ ## Dependencies
47
+
48
+ `@honua/sdk-js`, `maplibre-gl`, and React are the packages this app calls directly. `@bufbuild/protobuf` and the two
49
+ `@connectrpc` packages are the SDK's optional transport peers: they are installed and pinned here so the bundle builds
50
+ without any peer-dependency assembly, and they are what a Honua-server connection uses.
51
+
52
+ This starter pins **MapLibre GL JS 5.24.0**, the major the published `@honua/sdk-js` release accepts as an optional
53
+ peer. MapLibre 6 is supported by the SDK source (`^5.0.0 || ^6.0.0`) and ships in the next release; upgrading to 6.x
54
+ also means configuring MapLibre's ESM worker before the first map is created — under Vite, `setWorkerUrl` with
55
+ `import maplibreWorkerUrl from "maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url"`.
56
+
57
+ ## Checks
58
+
59
+ ```bash
60
+ npm run typecheck
61
+ npm run build
62
+ ```
63
+
64
+ The fixture data is synthetic demonstration data from the Honua SDK sample fixtures (Apache-2.0).
@@ -0,0 +1,6 @@
1
+ node_modules
2
+ dist
3
+ .env
4
+ .env.local
5
+ *.log
6
+ .DS_Store
@@ -0,0 +1,46 @@
1
+ {
2
+ "objectIdFieldName": "OBJECTID",
3
+ "geometryType": "esriGeometryPolygon",
4
+ "spatialReference": { "wkid": 4326 },
5
+ "fields": [
6
+ { "name": "OBJECTID", "type": "esriFieldTypeOID", "alias": "OBJECTID" },
7
+ { "name": "NAME", "type": "esriFieldTypeString", "alias": "Name" },
8
+ { "name": "STATUS", "type": "esriFieldTypeString", "alias": "Status" },
9
+ { "name": "CATEGORY", "type": "esriFieldTypeString", "alias": "Category" }
10
+ ],
11
+ "features": [
12
+ {
13
+ "attributes": {
14
+ "OBJECTID": 1,
15
+ "NAME": "Civic center service zone",
16
+ "STATUS": "Monitoring",
17
+ "CATEGORY": "Operations"
18
+ },
19
+ "geometry": {
20
+ "rings": [[[-157.866, 21.311], [-157.856, 21.311], [-157.856, 21.302], [-157.866, 21.302], [-157.866, 21.311]]]
21
+ }
22
+ },
23
+ {
24
+ "attributes": {
25
+ "OBJECTID": 2,
26
+ "NAME": "Harbor response district",
27
+ "STATUS": "Field review",
28
+ "CATEGORY": "Maritime"
29
+ },
30
+ "geometry": {
31
+ "rings": [[[-157.891, 21.304], [-157.879, 21.304], [-157.879, 21.294], [-157.891, 21.294], [-157.891, 21.304]]]
32
+ }
33
+ },
34
+ {
35
+ "attributes": {
36
+ "OBJECTID": 3,
37
+ "NAME": "Kakaako utility corridor",
38
+ "STATUS": "Ready",
39
+ "CATEGORY": "Infrastructure"
40
+ },
41
+ "geometry": {
42
+ "rings": [[[-157.87, 21.296], [-157.856, 21.296], [-157.856, 21.286], [-157.87, 21.286], [-157.87, 21.296]]]
43
+ }
44
+ }
45
+ ]
46
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "currentVersion": 11.3,
3
+ "id": 0,
4
+ "name": "Honolulu operations areas",
5
+ "type": "Feature Layer",
6
+ "description": "The same three synthetic First Map polygons exposed by the OGC API Features projection.",
7
+ "copyrightText": "Copyright Honua contributors; synthetic demonstration data.",
8
+ "provenance": {
9
+ "source": "honua-sdk-js test/fixtures/honua-quickstart-demo",
10
+ "retrievedAt": "2026-07-13T00:00:00.000Z"
11
+ },
12
+ "geometryType": "esriGeometryPolygon",
13
+ "objectIdField": "OBJECTID",
14
+ "maxRecordCount": 25,
15
+ "advancedQueryCapabilities": {
16
+ "supportsPagination": true,
17
+ "supportsReturningQueryExtent": false,
18
+ "supportsStatistics": false
19
+ },
20
+ "extent": {
21
+ "xmin": -157.891,
22
+ "ymin": 21.286,
23
+ "xmax": -157.856,
24
+ "ymax": 21.311,
25
+ "spatialReference": { "wkid": 4326 }
26
+ },
27
+ "fields": [
28
+ { "name": "OBJECTID", "type": "esriFieldTypeOID", "alias": "OBJECTID" },
29
+ { "name": "NAME", "type": "esriFieldTypeString", "alias": "Name" },
30
+ { "name": "STATUS", "type": "esriFieldTypeString", "alias": "Status" },
31
+ { "name": "CATEGORY", "type": "esriFieldTypeString", "alias": "Category" }
32
+ ],
33
+ "capabilities": "Query"
34
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Honua map app (React)</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "honua-app",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=20.19.0"
8
+ },
9
+ "scripts": {
10
+ "dev": "vite",
11
+ "build": "vite build",
12
+ "preview": "vite preview",
13
+ "typecheck": "tsc --noEmit"
14
+ },
15
+ "dependencies": {
16
+ "@bufbuild/protobuf": "2.13.0",
17
+ "@connectrpc/connect": "2.1.2",
18
+ "@connectrpc/connect-web": "2.1.2",
19
+ "@honua/sdk-js": "0.1.2-beta.0",
20
+ "maplibre-gl": "5.24.0",
21
+ "react": "19.2.8",
22
+ "react-dom": "19.2.8"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "24.13.3",
26
+ "@types/react": "19.2.18",
27
+ "@types/react-dom": "19.2.4",
28
+ "typescript": "5.9.3",
29
+ "vite": "8.2.0"
30
+ }
31
+ }
@@ -0,0 +1,153 @@
1
+ import { createHonua } from "@honua/sdk-js";
2
+ import type { ConnectProtocolHint, Source } from "@honua/sdk-js";
3
+ import { useMountedSource } from "@honua/sdk-js/react";
4
+ import * as maplibregl from "maplibre-gl";
5
+ import type { RefObject } from "react";
6
+ import { useEffect, useRef, useState } from "react";
7
+
8
+ import { FIXTURE_LAYER_PATH } from "./fixture-endpoint.js";
9
+
10
+ /** Offline basemap: the starter never fetches third-party tiles on its default lane. */
11
+ const BASEMAP_STYLE = {
12
+ version: 8 as const,
13
+ sources: {},
14
+ layers: [{ id: "background", type: "background" as const, paint: { "background-color": "#0e1a17" } }],
15
+ };
16
+
17
+ const INITIAL_VIEW = { center: [-157.873, 21.2985] as [number, number], zoom: 11.4 };
18
+
19
+ /** Bounded row budget. The bridge reports truncation instead of hiding it. */
20
+ const MAX_FEATURES = 250;
21
+
22
+ const QUERY = { returnGeometry: true, pagination: { limit: MAX_FEATURES } };
23
+
24
+ const configuredEndpoint = import.meta.env.VITE_HONUA_ENDPOINT?.trim();
25
+ const configuredProtocol = import.meta.env.VITE_HONUA_PROTOCOL?.trim();
26
+
27
+ const ENDPOINT =
28
+ configuredEndpoint && configuredEndpoint.length > 0
29
+ ? configuredEndpoint
30
+ : new URL(FIXTURE_LAYER_PATH, window.location.origin).toString();
31
+ const PROTOCOL = (
32
+ configuredProtocol && configuredProtocol.length > 0 ? configuredProtocol : "geoservices-feature-service"
33
+ ) as ConnectProtocolHint;
34
+ const DATA_LANE = configuredEndpoint && configuredEndpoint.length > 0 ? "Live endpoint" : "Committed fixture";
35
+
36
+ interface ConnectionState {
37
+ readonly source: Source | null;
38
+ readonly sourceId?: string;
39
+ readonly protocol?: string;
40
+ readonly error?: string;
41
+ }
42
+
43
+ /** The app owns its MapLibre map, exactly like a `@vis.gl/react-maplibre` `<Map>` would. */
44
+ function useExternalMap(): { containerRef: RefObject<HTMLDivElement | null>; map: maplibregl.Map | null } {
45
+ const containerRef = useRef<HTMLDivElement | null>(null);
46
+ const [map, setMap] = useState<maplibregl.Map | null>(null);
47
+
48
+ useEffect(() => {
49
+ const container = containerRef.current;
50
+ if (!container) return;
51
+ const created = new maplibregl.Map({
52
+ container,
53
+ style: BASEMAP_STYLE,
54
+ center: INITIAL_VIEW.center,
55
+ zoom: INITIAL_VIEW.zoom,
56
+ attributionControl: false,
57
+ });
58
+ setMap(created);
59
+ return () => {
60
+ setMap(null);
61
+ created.remove();
62
+ };
63
+ }, []);
64
+
65
+ return { containerRef, map };
66
+ }
67
+
68
+ /** Connect once per effect run and publish the discovered source. */
69
+ function useHonuaSource(): ConnectionState {
70
+ const [state, setState] = useState<ConnectionState>({ source: null });
71
+
72
+ useEffect(() => {
73
+ const honua = createHonua();
74
+ let cancelled = false;
75
+ void (async () => {
76
+ try {
77
+ const connection = await honua.connect(
78
+ { url: ENDPOINT, protocol: PROTOCOL },
79
+ { authorizationScopeFingerprint: "anonymous-public" },
80
+ );
81
+ const inspection = await connection.inspect();
82
+ const sourceId = inspection.defaultSourceId ?? inspection.sources[0]?.descriptor.id;
83
+ if (!sourceId) throw new Error("The endpoint advertised no queryable source.");
84
+ if (cancelled) return;
85
+ setState({ source: connection.source(sourceId), sourceId, protocol: inspection.protocol });
86
+ } catch (error) {
87
+ if (!cancelled) setState({ source: null, error: error instanceof Error ? error.message : String(error) });
88
+ }
89
+ })();
90
+ return () => {
91
+ cancelled = true;
92
+ void honua.dispose();
93
+ };
94
+ }, []);
95
+
96
+ return state;
97
+ }
98
+
99
+ export function App() {
100
+ const { containerRef, map } = useExternalMap();
101
+ const connection = useHonuaSource();
102
+ const mounted = useMountedSource(map, connection.source, { query: QUERY, fitBounds: true });
103
+
104
+ const failure = connection.error ?? (mounted.error instanceof Error ? mounted.error.message : undefined);
105
+ const status = failure
106
+ ? `The workflow stopped: ${failure}`
107
+ : mounted.handle
108
+ ? "The queried source is mounted through the React bridge."
109
+ : "Connecting, discovering, and mounting…";
110
+
111
+ return (
112
+ <main className="app">
113
+ <section className="panel" aria-label="Workflow">
114
+ <p className="eyebrow">Honua JavaScript SDK · React</p>
115
+ <h1 className="title">connect → source → useMountedSource</h1>
116
+ <output className="status" aria-live="polite">
117
+ {status}
118
+ </output>
119
+ <dl className="facts">
120
+ <div>
121
+ <dt>Data lane</dt>
122
+ <dd>{DATA_LANE}</dd>
123
+ </div>
124
+ <div>
125
+ <dt>Endpoint</dt>
126
+ <dd>{ENDPOINT}</dd>
127
+ </div>
128
+ <div>
129
+ <dt>Protocol</dt>
130
+ <dd>{connection.protocol ?? "—"}</dd>
131
+ </div>
132
+ <div>
133
+ <dt>Source</dt>
134
+ <dd>{connection.sourceId ?? "—"}</dd>
135
+ </div>
136
+ <div>
137
+ <dt>Strategy</dt>
138
+ <dd>{mounted.diagnostics?.strategy ?? "—"}</dd>
139
+ </div>
140
+ <div>
141
+ <dt>Features</dt>
142
+ <dd id="fact-features">{mounted.diagnostics?.featureCount ?? "—"}</dd>
143
+ </div>
144
+ </dl>
145
+ <p className="hint">
146
+ Edit <code>src/App.tsx</code> to change the query, or set <code>VITE_HONUA_ENDPOINT</code> to run the same
147
+ components against a live public service.
148
+ </p>
149
+ </section>
150
+ <div className="map" ref={containerRef} role="application" aria-label="Mounted map" />
151
+ </main>
152
+ );
153
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Same-origin path the committed fixture service answers on.
3
+ *
4
+ * `vite.config.ts` serves the fixture here in both `npm run dev` and
5
+ * `npm run preview`, and `src/main.ts` connects here whenever no live endpoint
6
+ * is configured. Keeping the path in one module is what makes the default lane
7
+ * work with no account, no key, and no third-party network call.
8
+ */
9
+ export const FIXTURE_LAYER_PATH = "/fixture/rest/services/honua-demo/FeatureServer/0";
@@ -0,0 +1,18 @@
1
+ import "maplibre-gl/dist/maplibre-gl.css";
2
+
3
+ import { StrictMode } from "react";
4
+ import { createRoot } from "react-dom/client";
5
+
6
+ import { App } from "./App.js";
7
+ import "./styles.css";
8
+
9
+ const container = document.getElementById("root");
10
+ if (!container) throw new Error("Missing #root element");
11
+
12
+ // StrictMode double-invokes effects in development. The SDK's React hooks are
13
+ // StrictMode-safe, so the mount/unmount/mount cycle never leaks a map source.
14
+ createRoot(container).render(
15
+ <StrictMode>
16
+ <App />
17
+ </StrictMode>,
18
+ );
@@ -0,0 +1,149 @@
1
+ :root {
2
+ --surface: #f6f7f5;
3
+ --panel: #ffffff;
4
+ --ink: #10201b;
5
+ --muted: #5c6b66;
6
+ --line: #d9e0dc;
7
+ --accent: #0f766e;
8
+ color-scheme: light dark;
9
+ }
10
+
11
+ @media (prefers-color-scheme: dark) {
12
+ :root {
13
+ --surface: #0b1210;
14
+ --panel: #111a17;
15
+ --ink: #e8f0ec;
16
+ --muted: #93a49e;
17
+ --line: #23322c;
18
+ --accent: #5eead4;
19
+ }
20
+ }
21
+
22
+ * {
23
+ box-sizing: border-box;
24
+ }
25
+
26
+ body {
27
+ margin: 0;
28
+ background: var(--surface);
29
+ color: var(--ink);
30
+ font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
31
+ }
32
+
33
+ .app {
34
+ display: grid;
35
+ grid-template-columns: minmax(280px, 360px) 1fr;
36
+ gap: 16px;
37
+ padding: 16px;
38
+ height: 100vh;
39
+ }
40
+
41
+ @media (max-width: 820px) {
42
+ .app {
43
+ grid-template-columns: 1fr;
44
+ grid-template-rows: auto minmax(320px, 1fr);
45
+ height: auto;
46
+ }
47
+ }
48
+
49
+ .panel {
50
+ background: var(--panel);
51
+ border: 1px solid var(--line);
52
+ border-radius: 12px;
53
+ padding: 20px;
54
+ overflow-y: auto;
55
+ }
56
+
57
+ .eyebrow {
58
+ margin: 0;
59
+ color: var(--accent);
60
+ font-size: 12px;
61
+ font-weight: 600;
62
+ letter-spacing: 0.08em;
63
+ text-transform: uppercase;
64
+ }
65
+
66
+ .title {
67
+ margin: 6px 0 12px;
68
+ font-size: 18px;
69
+ line-height: 1.3;
70
+ }
71
+
72
+ .status {
73
+ margin: 0 0 16px;
74
+ color: var(--muted);
75
+ }
76
+
77
+ .stages {
78
+ list-style: none;
79
+ margin: 0 0 20px;
80
+ padding: 0;
81
+ display: grid;
82
+ gap: 6px;
83
+ }
84
+
85
+ .stages li {
86
+ display: flex;
87
+ justify-content: space-between;
88
+ gap: 12px;
89
+ padding: 8px 10px;
90
+ border: 1px solid var(--line);
91
+ border-radius: 8px;
92
+ font-size: 13px;
93
+ }
94
+
95
+ .stages li[data-state="running"] {
96
+ border-color: var(--accent);
97
+ }
98
+
99
+ .stages li[data-state="done"] .stage-state {
100
+ color: var(--accent);
101
+ }
102
+
103
+ .stages li[data-state="failed"] .stage-state {
104
+ color: #dc2626;
105
+ }
106
+
107
+ .stage-state {
108
+ color: var(--muted);
109
+ }
110
+
111
+ .facts {
112
+ display: grid;
113
+ gap: 8px;
114
+ margin: 0 0 16px;
115
+ }
116
+
117
+ .facts div {
118
+ display: grid;
119
+ grid-template-columns: 96px 1fr;
120
+ gap: 10px;
121
+ font-size: 13px;
122
+ }
123
+
124
+ .facts dt {
125
+ color: var(--muted);
126
+ }
127
+
128
+ .facts dd {
129
+ margin: 0;
130
+ overflow-wrap: anywhere;
131
+ }
132
+
133
+ .hint {
134
+ margin: 0;
135
+ color: var(--muted);
136
+ font-size: 13px;
137
+ }
138
+
139
+ code {
140
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
141
+ font-size: 12px;
142
+ }
143
+
144
+ .map {
145
+ min-height: 360px;
146
+ border: 1px solid var(--line);
147
+ border-radius: 12px;
148
+ overflow: hidden;
149
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "skipLibCheck": true,
9
+ "verbatimModuleSyntax": true,
10
+ "allowSyntheticDefaultImports": true,
11
+ "jsx": "react-jsx",
12
+ "types": ["vite/client", "node"]
13
+ },
14
+ "include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts"]
15
+ }
@@ -0,0 +1,50 @@
1
+ import { readFileSync } from "node:fs";
2
+ import type { IncomingMessage, ServerResponse } from "node:http";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import { type Plugin, defineConfig } from "vite";
6
+
7
+ import { FIXTURE_LAYER_PATH } from "./src/fixture-endpoint.js";
8
+
9
+ function fixtureDocument(name: string): string {
10
+ return readFileSync(fileURLToPath(new URL(`./fixtures/${name}`, import.meta.url)), "utf8");
11
+ }
12
+
13
+ /**
14
+ * Deterministic, offline GeoServices FeatureServer fixture.
15
+ *
16
+ * The starter's green path must never depend on a third-party endpoint, so the
17
+ * dev and preview servers answer the two routes the SDK's GeoServices adapter
18
+ * needs: the layer description and its bounded query. Point
19
+ * `VITE_HONUA_ENDPOINT` at a real service to run the same code against live data.
20
+ */
21
+ function honuaFixtureService(): Plugin {
22
+ const layer = fixtureDocument("layer.json");
23
+ const features = fixtureDocument("features.json");
24
+ const middleware = (request: IncomingMessage, response: ServerResponse, next: () => void): void => {
25
+ const pathname = new URL(request.url ?? "/", "http://localhost").pathname;
26
+ let body: string | undefined;
27
+ if (pathname === FIXTURE_LAYER_PATH) body = layer;
28
+ else if (pathname === `${FIXTURE_LAYER_PATH}/query`) body = features;
29
+ if (body === undefined) {
30
+ next();
31
+ return;
32
+ }
33
+ response.setHeader("content-type", "application/json; charset=utf-8");
34
+ response.setHeader("cache-control", "no-store");
35
+ response.end(body);
36
+ };
37
+ return {
38
+ name: "honua-fixture-service",
39
+ configureServer(server) {
40
+ server.middlewares.use(middleware);
41
+ },
42
+ configurePreviewServer(server) {
43
+ server.middlewares.use(middleware);
44
+ },
45
+ };
46
+ }
47
+
48
+ export default defineConfig({
49
+ plugins: [honuaFixtureService()],
50
+ });
@@ -0,0 +1,4 @@
1
+ {
2
+ "installDependencies": true,
3
+ "startCommand": "npm run dev"
4
+ }