-
-
Notifications
You must be signed in to change notification settings - Fork 680
Expand file tree
/
Copy pathvite.config.ts
More file actions
1053 lines (1010 loc) · 50.8 KB
/
Copy pathvite.config.ts
File metadata and controls
1053 lines (1010 loc) · 50.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import react from "@vitejs/plugin-react";
import { existsSync, readFileSync, rmSync } from "node:fs";
import type { IncomingMessage, ServerResponse } from "node:http";
import { createRequire } from "node:module";
import path from "node:path";
import type { RollupLog, RollupOptions, WarningHandlerWithDefault } from "rollup";
import { fileURLToPath } from "node:url";
import { defineConfig, loadEnv, type Plugin } from "vite";
import { VitePWA } from "vite-plugin-pwa";
import { bundledPlugins } from "./vite-plugins/bundled-plugins";
import { copyCesiumAssets } from "./vite-plugins/copy-cesium-assets";
import { copyRtlText } from "./vite-plugins/copy-rtl-text";
import { copyVectorOps } from "./vite-plugins/copy-vector-ops";
import { proxyBinaryRequestGuarded } from "./vite-proxy-guard";
const GEOAGENT_BROWSER_BUNDLE = "maplibre-gl-geoagent/dist/browser-";
const EARTH_ENGINE_CONTROL_BUNDLE = "maplibre-gl-earth-engine/dist/";
const EARTH_ENGINE_BROWSER_BUNDLE = "@google/earthengine/build/browser.js";
const GIS_CHUNK_WARNING_LIMIT_KB = 14000;
const APP_BASE = process.env.GEOLIBRE_APP_BASE;
const APP_VERSION = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8"))
.version as string;
// Vite resolves `mode` from the `--mode` CLI flag (defaulting to `development`
// for `vite`/`vite dev` and `production` for `vite build`). This shim runs at
// module load, before `defineConfig` receives the resolved mode, so read
// `--mode` from argv directly and fall back to NODE_ENV (which Vite's CLI sets
// from the command). This lets `loadEnv` pick up mode-specific files such as
// `.env.staging.local` under `vite build --mode staging`, not just NODE_ENV.
function resolveViteMode(): string {
const argv = process.argv;
const inline = argv.find((arg) => arg.startsWith("--mode="));
if (inline) return inline.slice("--mode=".length);
const flagIndex = argv.findIndex((arg) => arg === "--mode" || arg === "-m");
if (flagIndex !== -1 && argv[flagIndex + 1]) return argv[flagIndex + 1];
return process.env.NODE_ENV || "development";
}
// Vite only exposes `VITE_`-prefixed vars to the client, so the Google Maps key
// is surfaced as `VITE_GOOGLE_MAPS_API_KEY`. Accept a bare `GOOGLE_MAPS_API_KEY`
// too (handy for local shell/CI testing) and copy it into the prefixed name.
// `loadEnv(mode, dir, "")` reads the app's `.env*` files with no prefix filter,
// so a key placed in `apps/geolibre-desktop/.env.local` also works, not just a
// real shell env var (process.env alone would miss the file).
const CONFIG_DIR = path.dirname(fileURLToPath(import.meta.url));
const FILE_ENV = loadEnv(resolveViteMode(), CONFIG_DIR, "");
// A managed build can use the server-side AI proxy without embedding any
// provider credential. Only its public endpoint and selected model enter the
// client bundle.
for (const name of ["GEOLIBRE_AI_URL", "GEOLIBRE_AI_MODEL"] as const) {
const viteName = `VITE_${name}`;
if (!process.env[viteName]) {
const value = process.env[name] || FILE_ENV[viteName] || FILE_ENV[name];
if (value) process.env[viteName] = value;
}
}
if (!process.env.VITE_GOOGLE_MAPS_API_KEY) {
const googleMapsApiKey =
process.env.GOOGLE_MAPS_API_KEY ||
FILE_ENV.VITE_GOOGLE_MAPS_API_KEY ||
FILE_ENV.GOOGLE_MAPS_API_KEY;
if (googleMapsApiKey) {
process.env.VITE_GOOGLE_MAPS_API_KEY = googleMapsApiKey;
}
}
// Cesium Ion token for the 3D-globe view: same bare→prefixed bridge as the
// Google Maps key. A bare `CESIUM_TOKEN` (shell or .env file) is surfaced as
// `VITE_CESIUM_TOKEN` so `import.meta.env` exposes it, and getCesiumIonToken()
// then lets a runtime Settings override win over this build-time value.
if (!process.env.VITE_CESIUM_TOKEN) {
const cesiumToken =
process.env.CESIUM_TOKEN || FILE_ENV.VITE_CESIUM_TOKEN || FILE_ENV.CESIUM_TOKEN;
if (cesiumToken) {
process.env.VITE_CESIUM_TOKEN = cesiumToken;
}
}
// Mapbox access token for the basemap control's Mapbox styles: same
// bare→prefixed bridge as the Google Maps and Cesium keys. `MAPBOX_TOKEN` is the
// spelling Mapbox's own tooling uses, so accept it from the shell or an .env
// file and surface it as `VITE_MAPBOX_ACCESS_TOKEN`; getMapboxAccessToken() then
// lets a runtime Settings override win over this build-time value.
if (!process.env.VITE_MAPBOX_ACCESS_TOKEN) {
const mapboxAccessToken =
process.env.MAPBOX_TOKEN || FILE_ENV.VITE_MAPBOX_ACCESS_TOKEN || FILE_ENV.MAPBOX_TOKEN;
if (mapboxAccessToken) {
process.env.VITE_MAPBOX_ACCESS_TOKEN = mapboxAccessToken;
}
}
// Earth Engine OAuth client ID: same bare→prefixed bridge as the Google Maps
// and Cesium keys. The app reads `import.meta.env.VITE_GEE_OAUTH_CLIENT_ID`, so
// a bare `GEE_OAUTH_CLIENT_ID` (shell/.zshrc or an .env file) is surfaced under
// the prefixed name here; otherwise Vite ignores the unprefixed var and the
// plugin falls back to its hardcoded DEFAULT_GEE_OAUTH_CLIENT_ID.
if (!process.env.VITE_GEE_OAUTH_CLIENT_ID) {
const geeOauthClientId =
process.env.GEE_OAUTH_CLIENT_ID ||
FILE_ENV.VITE_GEE_OAUTH_CLIENT_ID ||
FILE_ENV.GEE_OAUTH_CLIENT_ID;
if (geeOauthClientId) {
process.env.VITE_GEE_OAUTH_CLIENT_ID = geeOauthClientId;
}
}
// Tauri sets TAURI_ENV_* env vars while running its beforeBuildCommand
// (`npm run build`), so their presence flags a desktop build. Used below to drop
// the service worker from the desktop bundle.
const IS_TAURI_BUILD = !!process.env.TAURI_ENV_PLATFORM;
// Strip ALL external CDN references (unpkg.com, cdn.jsdelivr.net, etc.) from the
// build output. When set, features that depend on external CDN-hosted resources
// (storymap HTML export, object detection models, ONNX WASM, 3D Tiles decoders,
// Pyodide, PGlite, CereusDB, GDAL) are either disabled or degraded. Intended for
// deployments that cannot reference untrusted external CDNs (e.g. Harmony/Amazon).
// This implicitly forces GEOLIBRE_PGLITE_CDN=0, GEOLIBRE_CEREUS_CDN=0,
// GEOLIBRE_GDAL_CDN=0, and GEOLIBRE_DUCKDB_WASM_CDN=0.
const NO_EXTERNAL_CDN = process.env.GEOLIBRE_NO_EXTERNAL_CDN === "1";
if (NO_EXTERNAL_CDN) {
process.env.GEOLIBRE_PGLITE_CDN = "0";
process.env.GEOLIBRE_CEREUS_CDN = "0";
process.env.GEOLIBRE_GDAL_CDN = "0";
process.env.GEOLIBRE_DUCKDB_WASM_CDN = "0";
}
// PGlite + PostGIS is ~25 MB raw and weighs ~22 MB inside the Tauri binary
// (postgis.tar is pre-gzipped, so brotli can't shrink it — it was the entire
// 42 → 63 MB binary regression). By default it is fetched from jsDelivr at
// runtime for every target — web, desktop, and embed — so it never inflates any
// build output. Override with GEOLIBRE_PGLITE_CDN=0 to force-bundle it for a
// fully offline build. The CDN URLs are pinned to the installed versions so they
// cannot drift from the lockfile; PGlite resolves its own .wasm/.data/postgis.tar
// relative to these. jsDelivr is already an allowed script-src in the web
// (docker/nginx.conf) and desktop (tauri.conf.json) CSPs — it serves Pyodide — so
// this adds no new external origin. Trade-off: the PostGIS SQL engine needs
// network on FIRST use. After that, the web build's service worker runtime-caches
// the jsDelivr-served Pyodide and PGlite/PostGIS engines (see the
// "geolibre-cdn-engines" CacheFirst rule below), so both the browser SQL and
// Python features keep working offline. (The desktop Tauri build has no service
// worker and still fetches these per the same first-use rule.)
const PGLITE_CDN = process.env.GEOLIBRE_PGLITE_CDN !== "0";
// PWA/offline support targets the standalone web build only. The Tauri desktop
// shell already works offline (assets are bundled in the binary), and the
// embedded Jupyter wheel (GEOLIBRE_EMBED=1) is served from inside a notebook
// where a service worker is meaningless and could even hijack the host page's
// scope. This is deliberately independent of PGLITE_CDN: the web build CDN-loads
// PGlite yet still ships a service worker.
const IS_EMBED = process.env.GEOLIBRE_EMBED === "1";
const PWA_DISABLED = IS_TAURI_BUILD || IS_EMBED;
// DuckDB-WASM from jsDelivr instead of the build output. Opt-IN, the reverse of
// PGLITE_CDN above, because DuckDB is on the critical path for opening a local
// vector file — making that need the network is a real behaviour change, so only
// a deployment that cannot serve the binaries asks for it.
//
// The one that cannot: `duckdb-mvp.wasm` (~40 MB) and `duckdb-eh.wasm` (~35 MB)
// both exceed the 25 MiB per-asset limit on Cloudflare Pages and Workers static
// assets, which rejects the upload outright. Nothing else in the build is close
// (the next largest is ~22 MB), so this single flag is what decides whether the
// app can be hosted there at all. GitHub Pages allows 100 MB per file and needs
// none of this.
//
// jsDelivr is already an allowed script-src in the web (docker/nginx.conf) and
// desktop CSPs, and maplibre-gl-duckdb already loads its own DuckDB from there,
// so this adds no new external origin. The web build's service worker
// runtime-caches it after first use (the "geolibre-cdn-engines" rule below).
// Ignored for the two targets that would be made worse by it, which is why this
// sits below PWA_DISABLED rather than beside PGLITE_CDN: a Tauri build must stay
// offline-capable, and an embed build ships no service worker, so there the
// engine would be refetched every notebook session with no runtime cache behind
// it. Neither target has the size ceiling this exists for -- a wheel and a
// binary are not uploaded to Cloudflare.
const DUCKDB_WASM_CDN = !PWA_DISABLED && process.env.GEOLIBRE_DUCKDB_WASM_CDN === "1";
// Microsoft Store MSIX build. Strips the in-app "Check for updates" flow (Help
// menu, command palette, About dialog, and the automated startup check) so the
// Store package updates only through the Store — Microsoft policy 10.2.5 rejects
// a Store app that updates itself outside the Store. Set ONLY by the dedicated
// Store build path (.github/workflows/msix-store.yml); every other build (the
// GitHub .exe/winget installer, the sideload MSIX, portable, macOS, Linux, web,
// and the Jupyter embed) leaves it unset, so their update checker is untouched.
const IS_STORE_BUILD = process.env.GEOLIBRE_STORE_BUILD === "1";
// Mac App Store build. The App Sandbox forbids spawning the Python sidecar,
// the JupyterLab server, and the martin helper processes, so the UI compiles
// those surfaces out (client/WASM engines keep working in the webview). Set
// ONLY by the dedicated MAS build path; every other build leaves it unset.
const IS_MAS_BUILD = process.env.GEOLIBRE_MAS_BUILD === "1";
const pgliteCdnRequire = createRequire(import.meta.url);
// The ESM entry of a package's manifest. Prefer the `module` field and the
// `import` condition of `exports` (both point at the ESM build); never fall back
// to `main`, which is the CJS entry (`dist/index.cjs` for PGlite) and would
// break the jsDelivr `import()` at runtime. Falls back to `dist/index.js`, the
// historical PGlite layout, if neither is declared.
function esmEntry(manifest: Record<string, unknown>): string {
if (typeof manifest.module === "string") return manifest.module;
const exportsRoot = (manifest.exports as Record<string, unknown> | undefined)?.["."];
const importEntry = (exportsRoot as Record<string, unknown> | undefined)?.import;
const importDefault =
typeof importEntry === "string"
? importEntry
: (importEntry as Record<string, unknown> | undefined)?.default;
if (typeof importDefault === "string") return importDefault;
return "dist/index.js";
}
// These packages do not expose "./package.json" via their `exports`, so resolve
// a known file inside the package and walk up to the owning package.json (the
// one whose `name` matches) to read the installed version and entry paths — so a
// CDN URL tracks the lockfile and any future dist restructuring instead of
// hardcoding paths.
function findPackageManifest(
startFile: string,
pkg: string,
): { dir: string; manifest: Record<string, unknown> } {
let dir = path.dirname(startFile);
while (dir !== path.dirname(dir)) {
try {
const parsed = JSON.parse(readFileSync(path.join(dir, "package.json"), "utf8"));
if (parsed.name === pkg) return { dir, manifest: parsed };
} catch {
// Not this directory's package.json; keep walking up.
}
dir = path.dirname(dir);
}
throw new Error(`Could not resolve installed version of ${pkg}`);
}
function installedPackage(pkg: string): { version: string; entry: string } {
const { manifest } = findPackageManifest(pgliteCdnRequire.resolve(pkg), pkg);
return { version: manifest.version as string, entry: esmEntry(manifest) };
}
function pgliteCdnUrl(pkg: string): string | null {
if (!PGLITE_CDN) return null;
const { version, entry } = installedPackage(pkg);
// Normalize a leading "./" from the manifest entry into the jsDelivr path.
const entryPath = entry.replace(/^\.?\//, "");
return `https://cdn.jsdelivr.net/npm/${pkg}@${version}/${entryPath}`;
}
const PGLITE_CDN_URL = pgliteCdnUrl("@electric-sql/pglite");
const PGLITE_POSTGIS_CDN_URL = pgliteCdnUrl("@electric-sql/pglite-postgis");
// CereusDB (Apache Sedona spatial SQL, compiled to WASM) ships a ~40 MB wasm
// blob that brotli only shrinks to ~8.6 MB — the entire 27 → 36 MB desktop
// installer growth in v1.3. Like PGlite above, fetch the wasm from jsDelivr at
// runtime for every target (web, desktop, embed) so it never inflates any build
// output; the small JS glue stays bundled and lazy-chunked. Override with
// GEOLIBRE_CEREUS_CDN=0 to force-bundle the wasm for a fully offline build. The
// URL is pinned to the installed version (so it tracks the lockfile) and jsDelivr
// is already an allowed connect-src in both the web (docker/nginx.conf) and
// desktop (tauri.conf.json) CSPs. Trade-off: the Sedona engine needs network on
// first use (the desktop app already fetches PGlite, Pyodide, tiles, and the
// DuckDB spatial extension the same way).
const CEREUS_CDN = process.env.GEOLIBRE_CEREUS_CDN !== "0";
function cereusWasmCdnUrl(): string | null {
if (!CEREUS_CDN) return null;
const pkg = "@cereusdb/standard";
// The "./wasm" export resolves straight to the .wasm file; walk up to the
// owning package.json for the installed version and the file's package-relative
// path, so the URL tracks the lockfile and any future dist restructuring.
const wasmFile = pgliteCdnRequire.resolve(`${pkg}/wasm`);
const { dir, manifest } = findPackageManifest(wasmFile, pkg);
const rel = path.relative(dir, wasmFile).split(path.sep).join("/");
return `https://cdn.jsdelivr.net/npm/${pkg}@${manifest.version}/${rel}`;
}
const CEREUS_WASM_CDN_URL = cereusWasmCdnUrl();
// gdal3.js (GDAL compiled to WASM) powers the Georeferencer's client-side
// GeoTIFF/COG export. Its wasm (~28 MB) + data (~12 MB) are huge, so — like
// PGlite/Cereus above — fetch them from jsDelivr at runtime (version-pinned to
// the lockfile) so they never inflate any build; the small JS glue stays
// bundled and lazy-chunked, loaded only when the user exports. jsDelivr is
// already an allowed connect-src in both CSPs. Set GEOLIBRE_GDAL_CDN=0 to force
// network-free use (then the loader has no paths and export is unavailable).
const GDAL_CDN = process.env.GEOLIBRE_GDAL_CDN !== "0";
function gdal3CdnPaths(): { wasm: string; data: string } | null {
if (!GDAL_CDN) return null;
const { manifest } = findPackageManifest(pgliteCdnRequire.resolve("gdal3.js"), "gdal3.js");
const base = `https://cdn.jsdelivr.net/npm/gdal3.js@${manifest.version}/dist/package`;
return {
wasm: `${base}/gdal3WebAssembly.wasm`,
data: `${base}/gdal3WebAssembly.data`,
};
}
const GDAL3_CDN_PATHS = gdal3CdnPaths();
const WMS_PROXY_PATH = "/__geolibre_wms_proxy";
const WFS_PROXY_PATH = "/__geolibre_wfs_proxy";
const GPX_PROXY_PATH = "/__geolibre_gpx_proxy";
const RASTER_PROXY_PATH = "/__geolibre_raster_proxy";
const DUCKDB_WORKER_PATH_PART = "/@duckdb/duckdb-wasm/dist/";
const DUCKDB_WORKER_SOURCE_MAP_RE =
/\n?\/\/# sourceMappingURL=duckdb-browser-(?:eh|mvp)\.worker\.js\.map\s*$/;
const EARTH_ENGINE_PARAMETER_ERROR = "Failed to locate function parameters";
const RADIX_OPTIMIZE_EXCLUDES = [
"@developmentseed/geotiff",
"@developmentseed/lzw-tiff-decoder",
"@radix-ui/react-dialog",
"@radix-ui/react-dropdown-menu",
"@radix-ui/react-label",
"@radix-ui/react-scroll-area",
"@radix-ui/react-separator",
"@radix-ui/react-slider",
"@radix-ui/react-slot",
];
function manualChunks(id: string): string | undefined {
// Lazy per-locale i18n catalogs (src/i18n/index.ts dynamic-imports every
// non-English locale). Give each its own predictably named `i18n-locale-<code>`
// chunk so the service worker can keep them OUT of the app-shell precache
// (globIgnored below) and CacheFirst-cache them on demand instead. The prefix
// is deliberately distinct from the auto-named `i18n-<hash>` chunk that holds
// the i18n init module + statically bundled English, which MUST stay precached
// for offline boot.
// Match any locale filename, including regional tags like `pt-BR` / `zh-Hans`
// (uppercase/mixed-case), so those chunks still get the `i18n-locale-` name
// and the Workbox exclusion below — not just the two-letter `[a-z]` codes.
const localeMatch = id.match(/\/i18n\/locales\/([^/]+)\.json(?:\?|$)/);
if (localeMatch && localeMatch[1] !== "en") return `i18n-locale-${localeMatch[1]}`;
if (!id.includes("node_modules")) return undefined;
// Only route JS/TS modules into manual chunks. The name-based rules below match
// on the module id, so a package's *stylesheet* (e.g.
// `maplibre-gl-duckdb/dist/style.css`, imported eagerly in main.tsx so plugin
// controls are styled) would otherwise be assigned to that package's JS chunk
// and bundled into it. Importing the CSS in the boot entry then forces the
// whole heavy plugin JS to load at boot just to fetch its stylesheet, dragging
// DuckDB, Earth Engine, GeoAgent, Mapillary, etc. into the offline-critical
// boot graph — a cold offline reload can't fetch those runtime-cached chunks
// and the shell never mounts (see e2e/pwa.spec.ts). Let CSS and other assets
// fall through to default handling so only their JS is code-split.
if (!/\.[mc]?[jt]sx?(?:\?|$)/.test(id)) return undefined;
// Keep @duckdb/duckdb-wasm AND its apache-arrow dependency together in one
// lazily-fetched chunk. apache-arrow is shared with maplibre-gl-duckdb; if it
// is left to default chunking it can be hoisted into a chunk the eager
// `maplibre` chunk imports, dragging the heavy DuckDB engine into the app's
// offline-critical boot graph (the cold offline reload then can't fetch it and
// the shell never mounts — see e2e/pwa.spec.ts). Co-locating it here keeps both
// out of boot.
if (id.includes("@duckdb/duckdb-wasm")) return "duckdb";
if (id.includes("apache-arrow")) return "duckdb";
// PGlite + the ~18.8 MB PostGIS extension only load when the user picks the
// PostGIS SQL engine; keep them in their own lazily-fetched chunk.
if (id.includes("@electric-sql/pglite")) return "pglite";
if (id.includes("maplibre-gl-earth-engine")) {
return "maplibre-earth-engine";
}
if (id.includes("maplibre-gl-geoagent")) return "maplibre-geoagent";
if (id.includes("@google/earthengine")) return "earth-engine-browser";
if (id.includes("mapillary-js")) return "mapillary";
if (id.includes("@geoman-io/maplibre-geoman-free")) return "maplibre-geoman";
// gdal3.js JS glue (the big wasm/data load from the CDN); lazy on export only.
if (id.includes("gdal3.js")) return "gdal3";
// maplibre-gl-duckdb pulls in @duckdb/duckdb-wasm + apache-arrow and is only
// loaded on demand (the DuckDB map control). It must NOT fall through to the
// generic `maplibre-gl` rule below, which would fold it into the eager
// `maplibre` chunk and force DuckDB into boot. Give it its own lazy chunk.
if (id.includes("maplibre-gl-duckdb")) return "maplibre-duckdb";
if (id.includes("maplibre-gl")) return "maplibre";
// Cesium is large (~several MB) and only loads when the user opens the 3D
// globe view; keep it in its own lazily-fetched chunk, off the boot graph.
// `@cesium/engine`/`@cesium/widgets` (which the `cesium` wrapper re-exports)
// are only reachable through the lazy `import("cesium")`, so Rollup already
// groups them into this chunk; matching them explicitly keeps that intent
// even if some future eager import would otherwise pull them onto the boot
// graph.
if (id.includes("/node_modules/cesium/") || id.includes("/node_modules/@cesium/"))
return "cesium";
// Returning undefined hands remaining node_modules back to Rollup's default
// chunking. We intentionally do not group them into a single "vendor" chunk:
// that produced a circular manual-chunks warning. Do not re-add a catch-all
// `return "vendor"` here without re-checking that warning.
return undefined;
}
function onwarn(warning: RollupLog, defaultHandler: WarningHandlerWithDefault): void {
if (
warning.code === "EVAL" &&
typeof warning.id === "string" &&
(warning.id.includes(GEOAGENT_BROWSER_BUNDLE) ||
warning.id.includes(EARTH_ENGINE_CONTROL_BUNDLE) ||
warning.id.includes(EARTH_ENGINE_BROWSER_BUNDLE))
) {
return;
}
// Prebuilt third-party bundles (e.g. maplibre-gl-lidar's Emscripten/WASM
// glue, a UMD lib inside maplibre-gl-components) assign to `module.exports`
// behind `typeof module` guards. Rolldown flags this as a CommonJS variable
// in an ESM file, but the guard makes it a no-op in the browser. Silence it
// for vendored files only; a real occurrence in our own source still warns.
if (warning.code === "COMMONJS_VARIABLE_IN_ESM") {
const file =
(typeof warning.id === "string" ? warning.id : undefined) ?? warning.loc?.file ?? "";
if (file.includes("/node_modules/") || file.includes("\\node_modules\\")) {
return;
}
}
defaultHandler(warning);
}
function wmsProxyPlugin(): Plugin {
return {
name: "geolibre-wms-proxy",
configureServer(server) {
server.middlewares.use(WMS_PROXY_PATH, async (req, res) => {
try {
await proxyWmsRequest(req, res);
} catch (error) {
const message = error instanceof Error ? error.message : "WMS proxy request failed";
res.statusCode = 502;
res.setHeader("content-type", "text/plain");
res.end(message);
}
});
server.middlewares.use(WFS_PROXY_PATH, async (req, res) => {
try {
await proxyBinaryRequestGuarded(req, res, WFS_PROXY_PATH);
} catch (error) {
const message = error instanceof Error ? error.message : "WFS proxy request failed";
res.statusCode = 502;
res.setHeader("content-type", "text/plain");
res.end(message);
}
});
server.middlewares.use(GPX_PROXY_PATH, async (req, res) => {
try {
await proxyBinaryRequestGuarded(req, res, GPX_PROXY_PATH);
} catch (error) {
const message = error instanceof Error ? error.message : "GPX proxy request failed";
res.statusCode = 502;
res.setHeader("content-type", "text/plain");
res.end(message);
}
});
server.middlewares.use(RASTER_PROXY_PATH, async (req, res) => {
try {
await proxyBinaryRequestGuarded(req, res, RASTER_PROXY_PATH);
} catch (error) {
const message = error instanceof Error ? error.message : "Raster proxy request failed";
res.statusCode = 502;
res.setHeader("content-type", "text/plain");
res.end(message);
}
});
},
};
}
function stripDuckDbWorkerSourcemapPlugin(): Plugin {
return {
name: "geolibre-strip-duckdb-worker-sourcemap",
configureServer(server) {
server.middlewares.use((req, res, next) => {
const requestUrl = new URL(req.url ?? "/", "http://localhost");
const decodedPath = safeDecodeURIComponent(requestUrl.pathname);
if (requestUrl.search || !isDuckDbWorkerRequest(decodedPath)) {
next();
return;
}
const workerFile = path.join(
__dirname,
"../../node_modules",
decodedPath.slice(decodedPath.indexOf(DUCKDB_WORKER_PATH_PART) + 1),
);
const source = readFileSync(workerFile, "utf8").replace(DUCKDB_WORKER_SOURCE_MAP_RE, "");
res.statusCode = 200;
res.setHeader("content-type", "application/javascript");
res.end(source);
});
},
generateBundle(_, bundle) {
for (const asset of Object.values(bundle)) {
if (
asset.type === "asset" &&
/duckdb-browser-(?:eh|mvp)\.worker-[\w-]+\.js$/.test(asset.fileName)
) {
const source =
typeof asset.source === "string"
? asset.source
: Buffer.from(asset.source).toString("utf8");
asset.source = source.replace(DUCKDB_WORKER_SOURCE_MAP_RE, "");
}
}
},
};
}
function selectiveJsMinifyPlugin(): Plugin {
return {
name: "geolibre-selective-js-minify",
apply: "build",
async generateBundle(_, bundle) {
if (process.env.TAURI_DEBUG) return;
const { transform } = await import("esbuild");
await Promise.all(
Object.values(bundle).map(async (asset) => {
if (asset.type !== "chunk") return;
if (shouldPreserveEarthEngineChunk(asset.fileName, asset.code)) {
return;
}
const result = await transform(asset.code, {
legalComments: "none",
minify: true,
target: "esnext",
});
asset.code = result.code;
}),
);
},
};
}
function shouldPreserveEarthEngineChunk(fileName: string, code: string): boolean {
return (
fileName.includes("earth-engine") ||
fileName.includes("maplibre-geoagent") ||
code.includes(EARTH_ENGINE_PARAMETER_ERROR)
);
}
function isDuckDbWorkerRequest(pathname: string): boolean {
return (
pathname.includes(DUCKDB_WORKER_PATH_PART) &&
/duckdb-browser-(?:eh|mvp)\.worker\.js$/.test(pathname)
);
}
// Embed build only: redirect imports of `./pglite-loader` to the CDN variant so
// the bundled PGlite/PostGIS packages (and their ~25 MB of WASM/data/postgis.tar)
// are removed from the graph entirely. A bundler emits a chunk for every parsed
// `import()` regardless of dead-code reachability, so swapping the whole module
// is the only reliable way to keep those assets out of the wheel.
function pgliteCdnLoaderPlugin(): Plugin {
const cdnLoader = path.resolve(__dirname, "src/lib/pglite-loader.cdn.ts");
return {
name: "geolibre-pglite-cdn-loader",
enforce: "pre",
resolveId(source) {
// Match `./pglite-loader` (and `.ts`) but never `pglite-loader.cdn`.
return /(?:^|\/)pglite-loader(?:\.ts)?$/.test(source) ? cdnLoader : null;
},
};
}
// Keep the ~40 MB CereusDB wasm out of `dist` (and out of the Tauri binary) in
// two places, both required:
// 1. Swap the bundled loader for the CDN one so cereus-loader.ts's
// `@cereusdb/standard/wasm?url` import is dropped from the module graph (a
// bundler emits the asset for every `?url` import it parses, so this must be
// a module swap, not an `if` inside one module — same reasoning as PGlite).
// 2. Neutralize wasm-bindgen's own default `new URL('cereusdb_bg.wasm',
// import.meta.url)` inside the package glue. We always init CereusDB with an
// explicit `wasmUrl` (the CDN), so that default branch is dead at runtime —
// but Vite statically emits the asset from the `new URL(..., import.meta.url)`
// pattern regardless of reachability, which re-introduced the 40 MB file.
const CEREUS_WASM_GLUE = "@cereusdb/standard/dist/wasm/cereusdb.js";
const CEREUS_DEFAULT_WASM_URL = "new URL('cereusdb_bg.wasm', import.meta.url)";
function cereusCdnLoaderPlugin(): Plugin {
const cdnLoader = path.resolve(__dirname, "src/lib/cereus-loader.cdn.ts");
return {
name: "geolibre-cereus-cdn-loader",
enforce: "pre",
resolveId(source) {
// Match `./cereus-loader` (and `.ts`) but never `cereus-loader.cdn`.
return /(?:^|\/)cereus-loader(?:\.ts)?$/.test(source) ? cdnLoader : null;
},
transform(code, id) {
const file = id.split("?")[0].replaceAll("\\", "/");
if (!file.endsWith(CEREUS_WASM_GLUE)) return null;
if (!code.includes(CEREUS_DEFAULT_WASM_URL)) {
// The glue is here but the expected expression is gone — most likely a
// new @cereusdb/standard shipped a different wasm-bindgen string or dist
// path. Warn loudly instead of silently returning null, which would let
// Vite re-emit the 40 MB wasm into dist with no error and a green CI.
this.warn(
`${CEREUS_WASM_GLUE} no longer contains the expected default wasm URL ` +
`expression; the ~40 MB wasm may be silently re-emitted into dist. ` +
`Update CEREUS_WASM_GLUE / CEREUS_DEFAULT_WASM_URL in vite.config.ts.`,
);
return null;
}
// Replace the dead default-path expression so Vite never sees the asset.
// replaceAll: wasm-bindgen can emit the pattern more than once (sync + async
// init paths). If the branch is somehow reached (init with no wasmUrl),
// throw clearly. map:null is fine — the replacement adds no newlines, so
// only columns on this one line shift; per-line stepping stays correct, and
// sourcemaps are off anyway unless TAURI_DEBUG.
return {
code: code.replaceAll(
CEREUS_DEFAULT_WASM_URL,
"(()=>{throw new Error('CereusDB must be initialised with an explicit wasmUrl (GEOLIBRE_CEREUS_CDN build)')})()",
),
map: null,
};
},
};
}
function duckdbWasmBundlesPlugin(): Plugin {
// Tauri first: DUCKDB_WASM_CDN is already false for a desktop build, but the
// ordering keeps that guarantee local to this decision rather than resting on
// a condition set 400 lines up.
const variant = IS_TAURI_BUILD
? "src/lib/duckdb-wasm-bundles.tauri.ts"
: DUCKDB_WASM_CDN
? "src/lib/duckdb-wasm-bundles.cdn.ts"
: "src/lib/duckdb-wasm-bundles.ts";
const modulePath = path.resolve(__dirname, variant);
return {
name: "geolibre-duckdb-wasm-bundles",
enforce: "pre",
resolveId(source) {
return /(?:^|\/)duckdb-wasm-bundles(?:\.ts)?$/.test(source) ? modulePath : null;
},
};
}
function removeJupyterLiteFromTauriDistPlugin(): Plugin {
return {
name: "geolibre-remove-jupyterlite-from-tauri-dist",
apply: "build",
closeBundle() {
if (!IS_TAURI_BUILD) return;
// The MAS build keeps JupyterLite: the Jupyter server is compiled out
// there and the Notebook panel falls back to the JupyterLite site
// (Pyodide runs inside WebKit, which App Review permits). Assert it is
// actually in the bundle: Tauri's asset resolver answers a missing asset
// with index.html, so a MAS build without the site renders a second copy
// of GeoLibre inside the Notebook panel instead of a notebook.
if (IS_MAS_BUILD) {
const entry = path.resolve(__dirname, "dist/jupyterlite/lab/index.html");
if (!existsSync(entry)) {
throw new Error(
"The Mac App Store build embeds the JupyterLite site, but " +
`${path.relative(__dirname, entry)} is missing. Run ` +
"`npm run build:jupyterlite` (it needs the `jupyter lite` CLI from " +
"apps/geolibre-desktop/jupyterlite/requirements.txt).",
);
}
return;
}
rmSync(path.resolve(__dirname, "dist/jupyterlite"), {
recursive: true,
force: true,
});
},
};
}
function projectUrlQueryPlugin(): Plugin {
return {
name: "geolibre-project-url-query",
configureServer(server) {
server.middlewares.use((req, _res, next) => {
if (isProjectUrlDocumentRequest(req)) {
const requestUrl = new URL(req.url ?? "/", "http://localhost");
req.url = requestUrl.pathname;
}
next();
});
},
};
}
function isProjectUrlDocumentRequest(req: IncomingMessage): boolean {
if (req.method !== "GET" && req.method !== "HEAD") return false;
const accept = req.headers.accept ?? "";
if (!accept.includes("text/html") && accept !== "*/*") return false;
const requestUrl = new URL(req.url ?? "/", "http://localhost");
if (requestUrl.pathname !== "/" && requestUrl.pathname !== "/index.html") {
return false;
}
return (
requestUrl.searchParams.has("url") ||
requestUrl.searchParams.has("project") ||
requestUrl.searchParams.has("projectUrl") ||
requestUrl.searchParams.has("project_url") ||
/^https?:\/\//i.test(safeDecodeURIComponent(requestUrl.search.slice(1)))
);
}
function safeDecodeURIComponent(value: string): string {
try {
return decodeURIComponent(value);
} catch {
return value;
}
}
async function proxyWmsRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
await proxyBinaryRequestGuarded(req, res, WMS_PROXY_PATH);
}
// Installable, offline-capable web build. See docs/architecture.md (Offline /
// PWA). The service worker precaches the app shell (HTML + the JS/CSS chunks the
// map needs to boot) and runtime-caches the heavy, lazily-fetched same-origin
// binaries (DuckDB-WASM + spatial extension, MapLibre feature plugins) with a
// hash-keyed CacheFirst strategy, so a feature works offline after its first
// online use without bloating the first-visit precache. PGlite/PostGIS and the
// Pyodide runtime are fetched cross-origin from jsDelivr (see PGLITE_CDN above),
// so they are not same-origin cacheable: the PostGIS SQL engine needs network on
// first use and is not available offline. The pglite-*/*.wasm/*.data ignores
// below still apply when GEOLIBRE_PGLITE_CDN=0 force-bundles PGlite into the
// web build, keeping that variant's first visit light.
function pwaPlugin(): Plugin[] {
// Hashed build chunks/binaries that are lazily fetched. Excluded from the
// precache so first visit stays light; the same-origin CacheFirst rule below
// caches them on first use for offline. Hashed filenames make CacheFirst safe
// (a redeploy mints new URLs, so a stale entry is never served as current).
const HEAVY_PRECACHE_IGNORES = [
// MapLibre core (~13 MB) and its feature-plugin chunks. The map boots from
// its first runtime fetch and is CacheFirst-cached thereafter.
"**/maplibre-*",
"**/duckdb-*",
// CesiumJS (~4.8 MB) for the 3D-globe view. Lazily imported only when a pane
// switches to the globe, so it is CacheFirst-cached on first use rather than
// bloating the app-shell precache. The `Cesium-*` (capital) glob catches the
// Rollup facade chunk for the dynamic `import("cesium")` boundary, which the
// lowercase glob misses on case-sensitive matchers.
"**/cesium-*",
"**/Cesium-*",
// h5wasm's ~5.6 MB single-file chunk (embedded libhdf5) for the local
// NetCDF/HDF reader. Lazily imported when a user opens a local file, so it
// is CacheFirst-cached on first use rather than bloating the precache.
"**/hdf5_hl-*",
"**/pglite-*",
"**/earth-engine-browser-*",
"**/mapillary-*",
"**/*.wasm",
"**/*.data",
// The self-hosted JupyterLite site (apps/.../public/jupyterlite/, ~70 MB of
// JS/HTML) is loaded on demand inside the Notebook panel's iframe and has
// its own service worker scoped to /jupyterlite/. Keep it entirely out of
// the app shell precache, or first visit would balloon by thousands of files.
"**/jupyterlite/**",
// Bundled drop-in plugins (public/plugins/<id>/) load at runtime through
// the external-plugin path (fetch → blob import), so they never need to be
// in the app-shell precache — and a large private plugin bundle would
// otherwise trip workbox's maximumFileSizeToCacheInBytes and fail the build.
"**/plugins/**",
// Lazy per-locale i18n catalogs (`i18n-locale-<code>` chunks from
// manualChunks). Only the active language is fetched, and it is CacheFirst-
// cached on first use like the other lazily-loaded chunks — keeping all 15
// non-English catalogs (several MB fully translated) out of the app-shell
// precache. The `-locale-` infix is required: the i18n init + English chunk
// is auto-named `i18n-<hash>` and must stay precached, so this must NOT match
// it. English is bundled there, so it stays precached and works offline.
"**/i18n-locale-*.js",
// Optional hosted-web authentication. These chunks are requested only when
// the matching provider is configured, so public deployments should not
// download either during service-worker installation.
"**/ClerkGate-*.js",
"**/Auth0Gate-*.js",
];
// Note: the 4 KB public/pyodide/pyodide-worker.js shim is intentionally left
// in the precache (revisioned, so no stale-after-deploy risk). The heavy
// Pyodide runtime it loads is fetched from the jsDelivr CDN (cross-origin) and
// is not cached — Pyodide offline needs a same-origin VITE_PYODIDE_INDEX_URL
// mirror. See docs/architecture.md.
return VitePWA({
disable: PWA_DISABLED,
// autoUpdate installs the new SW and lets it take control on the next
// deploy (skipWaiting + clientsClaim below), so its fresh precache serves
// subsequent requests. We deliberately suppress workbox's default
// force-reload-on-activate via `onNeedReload` in main.tsx — that reload
// fired spuriously on the relative-base `/demo/` subpath and discarded map
// state. Page refreshes are left to installStaleChunkReload
// (src/lib/stale-chunk-reload.ts), which reloads on-demand only when a now
// orphaned lazy chunk actually 404s; precached chunks never 404.
registerType: "autoUpdate",
// We register the SW by hand in main.tsx so registration lives next to the
// stale-chunk reload it coordinates with; no auto-injected snippet.
injectRegister: false,
includeAssets: ["favicon.ico", "favicon.png", "apple-touch-icon.png"],
manifest: {
name: "GeoLibre",
short_name: "GeoLibre",
description:
"A free and open-source, lightweight, cloud-native GIS platform for visualizing, exploring, and analyzing geospatial data, running in the browser, on the desktop, on mobile, and inside Jupyter notebooks while keeping your data local and private.",
theme_color: "#2f8f85",
background_color: "#ffffff",
display: "standalone",
orientation: "any",
categories: ["productivity", "utilities", "education"],
icons: [
{ src: "pwa-192x192.png", sizes: "192x192", type: "image/png" },
{ src: "pwa-512x512.png", sizes: "512x512", type: "image/png" },
{
src: "maskable-icon-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "maskable",
},
],
},
workbox: {
// Precache the app shell: HTML plus the JS/CSS/fonts that boot the map.
// The heavy lazily-fetched chunks/binaries are runtime-cached instead.
globPatterns: ["**/*.{js,css,html,woff,woff2}"],
globIgnores: HEAVY_PRECACHE_IGNORES,
// deck.gl/vendor shell chunks can run a few MB; allow them into the
// precache. MapLibre and the huge binaries are globIgnored above.
maximumFileSizeToCacheInBytes: 4 * 1024 * 1024,
cleanupOutdatedCaches: true,
clientsClaim: true,
skipWaiting: true,
navigateFallback: "index.html",
// Never SPA-fallback the sidecar proxy or any asset request; let those hit
// the network/precache directly.
navigateFallbackDenylist: [/^\/sidecar\//, /^\/__geolibre_/, /\/[^/?]+\.[^/]+$/],
runtimeCaching: [
{
// Hashed build assets under /assets/ that the precache skips: the
// MapLibre chunk, DuckDB-WASM + its spatial extension, PGlite/PostGIS,
// and the MapLibre feature-plugin chunks. Vite content-hashes every
// file it emits here, so CacheFirst is safe (a redeploy mints new
// URLs). This is what makes DuckDB-WASM + the spatial extension work
// offline after their first online use. Scoped to /assets/ so the
// non-hashed public files (e.g. pyodide-worker.js, dropped-in plugin
// bundles) are not pinned by hash-immutable CacheFirst — those are
// served from the revisioned precache and refresh on a SW update.
urlPattern: ({ url, sameOrigin }: { url: URL; sameOrigin: boolean }) =>
sameOrigin &&
url.pathname.includes("/assets/") &&
/\.(?:js|css|wasm|data|woff2?)$/.test(url.pathname),
handler: "CacheFirst",
options: {
cacheName: "geolibre-assets",
expiration: { maxEntries: 300, maxAgeSeconds: 60 * 60 * 24 * 30 },
cacheableResponse: { statuses: [0, 200] },
},
},
{
// CDN-loaded heavy engines: Pyodide (the Python runtime + its wheels),
// PGlite/PostGIS, and the CereusDB (Apache Sedona) wasm — all served
// version-pinned from jsDelivr (see PGLITE_CDN_URL, CEREUS_WASM_CDN_URL,
// and pyodide-config.ts). Their URLs embed the exact package version, so
// a redeploy/upgrade mints new URLs and CacheFirst never serves a stale
// engine. Caching them here is what lets the browser SQL (PostGIS),
// Sedona SQL, and Python features work OFFLINE after their first online
// use — closing the gap noted at the top of this file. jsDelivr sends
// permissive CORS headers, so these come back as normal (non-opaque)
// 200s and can be revalidated/evicted like any cache entry. When
// GEOLIBRE_PGLITE_CDN=0 / GEOLIBRE_CEREUS_CDN=0, those engines are
// bundled under /assets/ instead and this rule simply never matches them
// (Pyodide is always CDN-loaded regardless).
urlPattern: ({ url }: { url: URL }) =>
url.hostname === "cdn.jsdelivr.net" &&
(url.pathname.startsWith("/pyodide/") ||
url.pathname.startsWith("/npm/@electric-sql/") ||
url.pathname.startsWith("/npm/@cereusdb/") ||
// Only populated when GEOLIBRE_DUCKDB_WASM_CDN=1 moves the engine
// off the origin; harmless otherwise. maplibre-gl-duckdb fetches
// its own DuckDB from here regardless, so this caches that too.
url.pathname.startsWith("/npm/@duckdb/") ||
url.pathname.startsWith("/npm/gdal3.js")),
handler: "CacheFirst",
options: {
cacheName: "geolibre-cdn-engines",
expiration: { maxEntries: 400, maxAgeSeconds: 60 * 60 * 24 * 30 },
cacheableResponse: { statuses: [0, 200] },
},
},
{
// Basemap tiles/styles from the CORS-friendly default hosts only
// (OpenFreeMap, CARTO). Other remote tiles/services stay network-only
// by design — see docs for what is and isn't available offline.
urlPattern: ({ url }: { url: URL }) =>
/(?:^|\.)(?:openfreemap\.org|cartocdn\.com)$/.test(url.hostname),
handler: "CacheFirst",
options: {
// Generous cap: the "Download Offline Area" feature
// (lib/offline-tiles.ts) warms a whole region's tiles at once and
// would otherwise evict its own freshly-cached tiles past 600.
cacheName: "geolibre-basemaps",
expiration: { maxEntries: 8000, maxAgeSeconds: 60 * 60 * 24 * 30 },
cacheableResponse: { statuses: [0, 200] },
},
},
],
},
devOptions: {
// Keep the SW out of `vite dev`; it complicates HMR and the stale-chunk
// flow. PWA behavior is validated against the production build / preview.
enabled: false,
},
}) as Plugin[];
}
export default defineConfig({
base: APP_BASE,
plugins: [
...(PGLITE_CDN ? [pgliteCdnLoaderPlugin()] : []),
...(CEREUS_CDN ? [cereusCdnLoaderPlugin()] : []),
duckdbWasmBundlesPlugin(),
stripDuckDbWorkerSourcemapPlugin(),
projectUrlQueryPlugin(),
bundledPlugins(path.resolve(__dirname, "public/plugins")),
copyVectorOps(
path.resolve(__dirname, "../../backend/geolibre_server/geolibre_server/vector_ops.py"),
path.resolve(__dirname, "src/lib/pyodide/vector_ops.generated.py"),
),
copyRtlText(path.resolve(__dirname, "src/lib/vendor/mapbox-gl-rtl-text.generated.js")),
copyCesiumAssets(path.resolve(__dirname, "public/cesium")),
react(),
wmsProxyPlugin(),
selectiveJsMinifyPlugin(),
removeJupyterLiteFromTauriDistPlugin(),
...pwaPlugin(),
],
clearScreen: false,
define: {
__GEOLIBRE_VERSION__: JSON.stringify(APP_VERSION),
__GEOLIBRE_STORE_BUILD__: JSON.stringify(IS_STORE_BUILD),
__GEOLIBRE_MAS_BUILD__: JSON.stringify(IS_MAS_BUILD),
__GEOLIBRE_EMBED_BUILD__: JSON.stringify(IS_EMBED),
__NO_EXTERNAL_CDN__: JSON.stringify(NO_EXTERNAL_CDN),
__PGLITE_CDN_URL__: JSON.stringify(PGLITE_CDN_URL),
__PGLITE_POSTGIS_CDN_URL__: JSON.stringify(PGLITE_POSTGIS_CDN_URL),
__CEREUS_WASM_CDN_URL__: JSON.stringify(CEREUS_WASM_CDN_URL),
__GDAL3_CDN_PATHS__: JSON.stringify(GDAL3_CDN_PATHS),
},
server: {
port: 5173,
strictPort: true,
watch: {
// Never watch the Rust side. `tauri dev` runs this dev server as its
// `beforeDevCommand` and then starts cargo in the same tree, so the
// watcher would otherwise crawl `src-tauri/target/` while cargo is
// writing into it. On Windows that is fatal: chokidar's fs.watch on a
// build artifact cargo still holds open throws EBUSY (`errno -4082`) out
// of `NodeFsHandler._addToNodeFs`, which is an unhandled error — vite
// exits non-zero and tauri reports only `The "beforeDevCommand"
// terminated with a non-zero status code` (see the libsqlite3-sys
// `*-sqlite3.o` failure). Nothing under src-tauri feeds the frontend
// bundle, so there is no HMR to lose. These are appended to Vite's own
// defaults (node_modules, .git), not a replacement for them.
ignored: ["**/src-tauri/**"],
},
},
worker: {
format: "es",
},
envPrefix: ["VITE_", "TAURI_"],
optimizeDeps: {
// Pre-bundle the AI Assistant's heavy deps at dev-server startup. They are
// only reached through the lazily-imported assistant panel (and, for the
// provider models, through dynamic import() inside it), so Vite would
// otherwise discover them on first open and trigger a full-page reload to
// re-optimize — which manifests as the map reloading and the panel needing
// a second click. Listing them here pre-bundles them up front instead.
include: [
"@strands-agents/sdk",
"@strands-agents/sdk/models/google",
"@strands-agents/sdk/models/anthropic",
"@strands-agents/sdk/models/openai",
"@strands-agents/sdk/models/bedrock",
"@anthropic-ai/sdk",
"@google/genai",
"openai",
"zod",
// cog-tiler-wasm's plain-JS deps (the wasm tiler itself is excluded below
// so its asset URL survives). These are only reached through that lazy
// engine, so without pre-bundling Vite discovers them on first use and
// triggers a full-page reload to re-optimize. (geotiff is already
// pre-bundled via the deck.gl-geotiff static import.)
"proj4",
"geotiff-geokeys-to-proj4",
// Cesium (the 3D-globe view). Pre-bundle it up front so esbuild applies
// CJS→ESM interop to its CommonJS transitive deps (e.g. mersenne-twister,
// which has no ESM entry): without this, the dev server serves those raw
// and the `import x from "mersenne-twister"` default import throws. It is
// reached only through the lazy `import("cesium")` in CesiumCanvas, so
// without pre-bundling Vite would also discover it on first open and do a
// full-page reload to re-optimize. Cesium locates its Workers/Assets via
// the CESIUM_BASE_URL global (never `import.meta.url`), so pre-bundling
// does not mangle any asset reference.
"cesium",
],
// PGlite ships its own WASM + filesystem bundles and must not be pre-bundled
// by esbuild, which mangles those asset references (per PGlite's Vite guide).
// CereusDB (the WASM Sedona engine) is excluded for the same reason, and
// because its wasm-bindgen glue imports `./env_shim.js?v=...` — that query
// suffix breaks the dev-server dependency optimizer, so it must be served
// as-is rather than pre-bundled.
exclude: [
...RADIX_OPTIMIZE_EXCLUDES,
"@electric-sql/pglite",
"@electric-sql/pglite-postgis",
"@cereusdb/standard",
// geolibre-wasm/tools loads its bundled geolibre-cli.wasm via