Skip to content

Write embedded GeoJSON compactly when saving a project (#1829) - #1832

Merged
giswqs merged 4 commits into
mainfrom
fix/issue-1829-compact-embedded-geojson
Aug 10, 2026
Merged

Write embedded GeoJSON compactly when saving a project (#1829)#1832
giswqs merged 4 commits into
mainfrom
fix/issue-1829-compact-embedded-geojson

Conversation

@giswqs

@giswqs giswqs commented Aug 10, 2026

Copy link
Copy Markdown
Member

Fixes #1829

The problem

serializeProject pretty-printed the whole .geolibre.json at two-space indent, so every single coordinate value got its own indented line. Coordinate arrays are never hand-edited, and the whitespace cost roughly three bytes for every byte of data.

The fix

Serialization keeps the project structure indented (so the file is still readable and still diffs sensibly) but hands every GeoJSON feature, geometry and collection to JSON.stringify with no spacing. Since embedded feature data is essentially all of a large project's bytes, the result comes out within a few percent of a fully minified file without turning the whole thing into one unreadable line.

This applies everywhere a project is serialized, not just Save: Share, autosave snapshots, collaboration sync and the embed bridge all go through the same function.

Measured on the real app with dem_points.geojson (366,411 features) plus OTEX_p.geojson (877 features), saved with data embedded:

before after
file size 157.9 MiB 61.3 MiB
lines 5,889,137 668

A smaller single-layer case (OTEX_p.geojson alone) went from 23.0 MiB / 759,096 lines to 7.2 MiB / 385 lines, the same ~3.2x the reporter measured.

Two related fixes on the save path

  • buildCurrentProject no longer serializes the project it returns. Every caller re-serialized afterwards anyway (credentials are redacted first), so that unused string doubled the peak memory of a save.
  • Serializing throws RangeError: Invalid string length once the text passes V8's ~536 MB string cap. That throw escaped void handleSave() as an unhandled rejection, so Save silently did nothing. It is now caught and surfaced as a visible error suggesting PMTiles or FlatGeobuf, matching the guidance in the issue thread. (Autosave already guarded this; the explicit save path did not.)

Size warning

The embed prompt now shows an inline warning above 50 MB of embedded data, recommending PMTiles or FlatGeobuf instead of embedding features. The prompt's size estimate was already computed compactly, so with this change it finally matches the file that actually lands: the dialog said 61.3 MB and the file was 61.3 MiB.

New string added to en.json and all 17 other locale catalogs.

Verification

Real app at localhost:5173, Chromium via Playwright:

  1. Add Vector Layer -> OTEX_p.geojson -> Save with data embedded -> 7.2 MiB / 385 lines, structure still indented, feature collection on one line.
  2. Reopened that file: all 877 polygons render, no console errors.
  3. Added dem_points.geojson on top (61.3 MB embedded) -> the large-embed warning appears; checked in both dark and light themes.
  4. Completed that save -> 61.3 MiB, then reopened it: both layers restore (366.4k ft + 877 ft), zero console errors.

New tests in tests/core-project.test.ts cover the compact output for both layer.geojson and metadata.embeddedGeoJSON, the size ratio against a fully minified file, byte-identical output to JSON.stringify(project, null, 2) for a project with no GeoJSON, matching JSON.stringify semantics for dropped values / empty containers / toJSON, and a parseProject round trip.

npm run test:frontend (5735 pass), npm run typecheck, and pre-commit all green.

Summary by CodeRabbit

  • New Features

    • Added warnings with embedded vector data size when projects may become slow or unable to reopen.
    • Project saving now detects oversized or unprocessable projects and displays clear errors.
    • Large project files use more compact formatting for embedded GeoJSON while preserving readability.
  • Bug Fixes

    • Prevented failed serialization from disrupting save workflows.
    • Added guidance to use PMTiles or FlatGeobuf remote layers for oversized vector data.
  • Localization

    • Added translated warnings and save-error messages across supported languages.

`serializeProject` pretty-printed the entire `.geolibre.json`, so every
coordinate value got its own indented line. Coordinate arrays are never
hand-edited, and the whitespace cost roughly three bytes for every byte
of data: a project embedding 367k features weighed 158 MB and 5.9M lines
pretty-printed, but 61 MB and 668 lines compact. Files that large made
reopening unreliable, with layers rendering briefly and then vanishing as
the tab ran out of memory.

Serialization now indents the project structure as before but hands each
GeoJSON feature, geometry and collection to `JSON.stringify` with no
spacing, so files stay readable and diffable while dropping the bloat.
This applies everywhere projects are serialized: save, share, autosave,
collaboration sync and the embed bridge.

Two related fixes on the save path:

- `buildCurrentProject` no longer serializes the project it returns. Every
  caller re-serialized after redacting credentials, so the unused string
  doubled the peak memory of a save.
- Serialization throws `RangeError: Invalid string length` past V8's
  ~536 MB string cap. That escaped `void handleSave()` as an unhandled
  rejection, so Save silently did nothing; it now reports a visible error
  suggesting PMTiles or FlatGeobuf.

The embed prompt also warns above 50 MB of embedded data, pointing at
PMTiles/FlatGeobuf rather than embedding features.
Copilot AI lite review requested due to automatic review settings August 10, 2026 05:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The project serializer now compacts embedded GeoJSON while preserving project formatting. Save operations defer serialization until credential handling completes, report serialization failures, and warn about large embedded data. Localized messages cover these warnings and save errors.

Changes

Project save-size handling

Layer / File(s) Summary
Compact GeoJSON serialization
packages/core/src/project.ts, tests/core-project.test.ts
serializeProject compacts GeoJSON and preserves readable formatting for surrounding project data. Tests cover size, JSON behavior, circular references, sparse arrays, and round-trip parsing.
Deferred save serialization
apps/geolibre-desktop/src/hooks/useProjectFileActions.ts
Project serialization occurs after credential-redaction decisions. Serialization failures update action-error state and stop the save.
Large-data warning presentation
apps/geolibre-desktop/src/components/layout/toolbar/ProjectFileDialogs.tsx, apps/geolibre-desktop/src/i18n/locales/*.json
The embed-vector dialog warns when payload size reaches 50 MiB. Locales include embedded-data and oversized-save messages.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ProjectFileDialogs
  participant ProjectFileActions
  participant serializeProject
  User->>ProjectFileDialogs: embed vector data
  ProjectFileDialogs->>ProjectFileActions: create project snapshot
  ProjectFileActions->>serializeProject: serialize finalized project
  serializeProject-->>ProjectFileActions: serialized content or error
  ProjectFileActions-->>ProjectFileDialogs: warning or save error
Loading

Possibly related PRs

Suggested reviewers: harshshinde0, rohithpariki

Poem

A rabbit packs GeoJSON tight,
Keeps project spacing clear and light.
Amber warns when files grow wide,
Save checks every choice inside.
PMTiles waits by FlatGeobuf’s side.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: compact embedded GeoJSON during project saves.
Linked Issues check ✅ Passed The changes implement issue #1829 by compacting GeoJSON, warning about large projects, and surfacing serialization failures.
Out of Scope Changes check ✅ Passed The serialization, error handling, localization, and tests directly support the linked issue objectives.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-1829-compact-embedded-geojson

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: dependency version conflict. Check your lock file or package.json.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

🔍 Cloudflare PR preview

Item Value
Site https://091100d8.geolibre-preview.pages.dev
Demo app https://091100d8.geolibre-preview.pages.dev/demo/
Commit a1060d2

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/geolibre-desktop/src/components/layout/toolbar/ProjectFileDialogs.tsx (1)

245-265: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hoist the repeated formatByteSize call.

formatByteSize(projectFiles.embedVectorDataPrompt?.bytes ?? 0) runs twice: once for the description text and once for the warning text. Compute it once and reuse the value.

♻️ Proposed refactor
+          {(() => {
+            const embedBytes = projectFiles.embedVectorDataPrompt?.bytes ?? 0;
+            const formattedSize = formatByteSize(embedBytes);
+            return (
+              <>
           <DialogHeader>
             <DialogTitle>{t("toolbar.item.embedVectorTitle")}</DialogTitle>
             <DialogDescription>
               {t(
                 projectFiles.embedVectorDataPrompt?.desktop
                   ? "toolbar.item.embedVectorDescDesktop"
                   : "toolbar.item.embedVectorDesc",
                 {
                   count: projectFiles.embedVectorDataPrompt?.count ?? 0,
-                  size: formatByteSize(projectFiles.embedVectorDataPrompt?.bytes ?? 0),
+                  size: formattedSize,
                 },
               )}
             </DialogDescription>
           </DialogHeader>
-          {(projectFiles.embedVectorDataPrompt?.bytes ?? 0) >= LARGE_EMBED_WARNING_BYTES ? (
+          {embedBytes >= LARGE_EMBED_WARNING_BYTES ? (
             <p
               role="alert"
               className="rounded-md border border-amber-500/40 bg-amber-500/10 p-3 text-sm text-amber-700 dark:text-amber-300"
             >
-              {t("toolbar.item.embedVectorLargeWarning", {
-                size: formatByteSize(projectFiles.embedVectorDataPrompt?.bytes ?? 0),
-              })}
+              {t("toolbar.item.embedVectorLargeWarning", { size: formattedSize })}
             </p>
           ) : null}
+              </>
+            );
+          })()}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/components/layout/toolbar/ProjectFileDialogs.tsx`
around lines 245 - 265, In the component rendering the embed vector data dialog,
compute formatByteSize(projectFiles.embedVectorDataPrompt?.bytes ?? 0) once
before the DialogDescription and warning markup, store the result in a local
value, and reuse it for both translation calls. Preserve the existing fallback
and warning behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/hooks/useProjectFileActions.ts`:
- Around line 682-700: Update serializeForSave so the projectTooLargeToSave
message is selected only when the RangeError matches the known V8 string-length
failure, such as by checking its message. Route RangeErrors from custom toJSON
methods, getters, recursion, or any other cause to couldNotSaveProject, while
preserving the existing error logging and null return behavior.

In `@packages/core/src/project.ts`:
- Around line 167-171: Update the array serialization branch in
serializeProjectValue to visit every numeric index, including sparse holes, and
emit "null" for missing slots. Preserve the existing formatting and conversion
behavior for populated entries and empty arrays.
- Around line 154-159: Update serializeProjectValue and every recursive
serializeProjectValue(entry, depth + 1) call to accept and propagate the
containing property key or array index, then pass that key to the value’s toJSON
method instead of an empty string. Preserve the existing top-level behavior with
an empty key where no containing key exists.

In `@tests/core-project.test.ts`:
- Around line 636-649: Extend the test in “matches JSON.stringify for values it
drops, empty containers, and toJSON” with key-sensitive toJSON props in both an
object and an array, asserting serializeProject matches JSON.stringify. Also add
a sparse array such as [1, , 2] and include the same comparison, preserving the
existing edge-case coverage.

---

Outside diff comments:
In `@apps/geolibre-desktop/src/components/layout/toolbar/ProjectFileDialogs.tsx`:
- Around line 245-265: In the component rendering the embed vector data dialog,
compute formatByteSize(projectFiles.embedVectorDataPrompt?.bytes ?? 0) once
before the DialogDescription and warning markup, store the result in a local
value, and reuse it for both translation calls. Preserve the existing fallback
and warning behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4b6e08ec-bbff-48dc-baa4-b1ab6f2f768c

📥 Commits

Reviewing files that changed from the base of the PR and between b308b5e and a36dbc7.

📒 Files selected for processing (22)
  • apps/geolibre-desktop/src/components/layout/toolbar/ProjectFileDialogs.tsx
  • apps/geolibre-desktop/src/hooks/useProjectFileActions.ts
  • apps/geolibre-desktop/src/i18n/locales/ar.json
  • apps/geolibre-desktop/src/i18n/locales/de.json
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • apps/geolibre-desktop/src/i18n/locales/es.json
  • apps/geolibre-desktop/src/i18n/locales/fa.json
  • apps/geolibre-desktop/src/i18n/locales/fr.json
  • apps/geolibre-desktop/src/i18n/locales/hi.json
  • apps/geolibre-desktop/src/i18n/locales/id.json
  • apps/geolibre-desktop/src/i18n/locales/it.json
  • apps/geolibre-desktop/src/i18n/locales/ja.json
  • apps/geolibre-desktop/src/i18n/locales/ka.json
  • apps/geolibre-desktop/src/i18n/locales/ko.json
  • apps/geolibre-desktop/src/i18n/locales/nl.json
  • apps/geolibre-desktop/src/i18n/locales/pt.json
  • apps/geolibre-desktop/src/i18n/locales/ru.json
  • apps/geolibre-desktop/src/i18n/locales/th.json
  • apps/geolibre-desktop/src/i18n/locales/tr.json
  • apps/geolibre-desktop/src/i18n/locales/zh.json
  • packages/core/src/project.ts
  • tests/core-project.test.ts

Comment thread apps/geolibre-desktop/src/hooks/useProjectFileActions.ts
Comment thread packages/core/src/project.ts
Comment thread packages/core/src/project.ts Outdated
Comment thread tests/core-project.test.ts
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

🔍 GitHub Pages PR preview

Item Value
Site https://opengeos.org/pages-preview/GeoLibre/pr-1832/
Demo app https://opengeos.org/pages-preview/GeoLibre/pr-1832/demo/
Commit a1060d2

Note

GitHub Pages built this preview successfully, but its serving edge returned HTTP 403 when checked. The links may still be propagating.

Comment thread packages/core/src/project.ts Outdated
Comment thread apps/geolibre-desktop/src/hooks/useProjectFileActions.ts
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • packages/core/src/project.ts:152-181 (serializeProjectValue) — the new recursive serializer has no circular-reference detection, unlike native JSON.stringify. A cycle in project.metadata/layer.metadata/plugin state (all loosely typed as Record<string, unknown>) will stack-overflow (RangeError: Maximum call stack size exceeded) instead of throwing the clean TypeError JSON.stringify gives today. Medium confidence.
  • apps/geolibre-desktop/src/hooks/useProjectFileActions.ts:691-699 (serializeForSave) — classifies any RangeError as "project too large to save," but other RangeErrors (the stack-overflow above, or an invalid Date's toISOString) would be mislabeled as a size problem, contradicting the code's own comment that "anything else is a real serialization bug." Medium confidence; posted a suggested fix that checks the specific "Invalid string length" message.

Quality

  • packages/core/src/project.ts:158toJSON is always invoked with an empty-string key (toJSON.call(value, "")) instead of the actual property/array-index key that JSON.stringify passes. The JSDoc promises output "exactly as JSON.stringify(value, null, 2) would," which is true for the common case (Date.prototype.toJSON ignores its argument) but would diverge for any custom toJSON that varies its output based on the key. Low confidence / narrow real-world impact given this project's data shapes, not posted as a separate inline comment.

Security / Performance / CLAUDE.md

  • No issues found. The size-warning threshold, i18n additions (all 18 locales present and structurally consistent), buildCurrentProject/serializeForSave call-site wiring, and the new test coverage all check out. The compact-serialization logic itself matches JSON.stringify semantics correctly for dropped values, empty containers, key ordering, and array holes, per both manual tracing and the added tests.

- Detect circular references in the project serializer and throw the
  TypeError JSON.stringify raises, instead of recursing until the stack
  overflows. The RangeError an overflow raises is indistinguishable from
  the string-length cap the save path reads as "project too large".
- Visit sparse array holes by index rather than mapping, so a hole is
  written as null the way JSON.stringify writes it (mapping skipped the
  hole and emitted invalid JSON).
- Pass the property name (or stringified array index) to a value's
  toJSON hook, matching the key JSON.stringify supplies.
- Narrow the "project too large to save" message to the string-length
  RangeError, so other RangeErrors report a generic save failure.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/project.ts`:
- Line 171: Update the serializer’s primitive/object boundary in project.ts to
detect boxed Number, String, and Boolean values and serialize their unboxed
primitives before recursing through Object.entries(value). Preserve existing
handling for null, non-objects, and ordinary objects, matching native
JSON.stringify output for boxed primitives.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b2223481-986b-4ce8-949b-aef0227fc21b

📥 Commits

Reviewing files that changed from the base of the PR and between a36dbc7 and cf44621.

📒 Files selected for processing (3)
  • apps/geolibre-desktop/src/hooks/useProjectFileActions.ts
  • packages/core/src/project.ts
  • tests/core-project.test.ts

Comment thread packages/core/src/project.ts
Comment thread packages/core/src/project.ts
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bugs: None found. I traced the new serializeProjectValue recursive stringifier against native JSON.stringify semantics (toJSON hooks, dropped values, sparse arrays, cycles, key ordering) and the accompanying tests in tests/core-project.test.ts cover these cases well. The handleSave/serializeForSave/buildCurrentProject refactor in useProjectFileActions.ts correctly removes the redundant eager serializeProject call (verified no other caller relied on the removed content field) and correctly distinguishes the V8 string-length cap from other RangeErrors (e.g. stack overflow) so users aren't misdirected to a "too large" fix for an unrelated failure — confidence: high.

Security: None found.

Performance: None found — the change is a net performance/memory win as described (removes a duplicate full serialization on every save, and the compact GeoJSON formatting is the intended fix for the underlying OOM/slow-reopen issue). Low confidence aside: a hand-written recursive stringifier will hit V8's call-stack limit at a shallower depth than the native routine for extremely deep (non-cyclic) nesting, but this is already anticipated by the code (the RangeError message-matching explicitly separates "too large" from "stack overflow", and the latter is tested for the cycle case).

Quality: Two low-confidence nits posted inline on packages/core/src/project.ts:130-133: (1) isGeoJsonValue's heuristic (matching purely on a type string) has no current collision anywhere in the project schema — verified via a targeted search of packages/core/src — but is an implicit contract that could silently misfire if a future non-GeoJSON field is ever named type with one of those nine values; (2) the function doesn't unwrap boxed Number/String/Boolean primitives the way native JSON.stringify does, so the docstring's "exact parity" claim has one unlikely-to-matter exception. Neither affects correctness of current data.

One additional observation (not inline, since it touches files outside this diff): the new large-embed warning (LARGE_EMBED_WARNING_BYTES) is wired into the Save dialog's embed prompt only. The Share flow (buildEmbeddedProject/ShareProjectDialog.tsx) always embeds vector data unconditionally and shows no equivalent size warning, so a user sharing a very large project gets no heads-up before hitting the same slow-reopen risk. This may be deliberately out of scope for issue #1829 (which was about Save), so flagging as a possible follow-up rather than a defect.

CLAUDE.md adherence: The i18n changes follow the documented convention (en.json plus all 17 other locale catalogs updated with matching new keys, verified all 18 locale files present and consistent).

- Unwrap boxed Number/String/Boolean objects to their primitives, the way
  JSON.stringify does, instead of recursing over their (empty) own
  properties and writing {}.
- Document that isGeoJsonValue discriminates on the `type` string alone,
  so a future schema field must not reuse one of those nine names for a
  non-GeoJSON value.
Comment thread apps/geolibre-desktop/src/hooks/useProjectFileActions.ts Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • None found. The custom serializeProjectValue in packages/core/src/project.ts was checked against JSON.stringify(value, null, 2) semantics for toJSON hooks, boxed primitives, sparse arrays, dropped values (undefined/functions), empty containers, and cycle detection — all consistent, and well covered by the added tests. Confidence: high.
  • buildCurrentProject no longer returning content was verified against every call site (ProjectFileDialogs.tsx, SaveTemplateDialog, handleDuplicate, buildEmbeddedProject) — none destructured the old content field, and the pre-PR code always re-serialized it anyway, so dropping it is safe. Confidence: high.

Security

  • No injection, unsafe input handling, or secret-leak concerns; this is local JSON serialization/file-save logic with no new external input surface.

Performance

  • The change is a clear net improvement (compact GeoJSON subtrees via native JSON.stringify, no duplicate serialization in buildCurrentProject). No new inefficiencies introduced. Confidence: high.

Quality

  • apps/geolibre-desktop/src/hooks/useProjectFileActions.ts:699serializeForSave detects the "too large" case by matching error.message.includes("Invalid string length"), which is V8's specific wording for the string-length overflow. Other JS engines GeoLibre actually runs on (e.g. WebKitGTK/JavaScriptCore backing the Tauri webview on macOS/Linux, or Firefox) may phrase this differently, in which case the save falls back to the generic "Could not save the project" message instead of the new PMTiles/FlatGeobuf guidance — i.e., the exact UX gap this PR is fixing, just resurfacing on a different runtime. Flagged inline. Confidence: medium.
  • isGeoJsonValue (packages/core/src/project.ts:139) decides "is this feature data" purely by an object's type field matching one of nine GeoJSON type strings anywhere in the project tree, which the code's own comment calls out as an implicit, unenforced schema contract. I scanned types.ts and related modules (routing, geocoding, attribute-form, legend, comments) for a colliding type value and found none today, so this isn't a live bug — just a fragility worth keeping in mind for future schema additions (already well-documented in the source comment, so not filed as a separate inline comment).

CLAUDE.md

  • i18n conventions followed: en.json updated as source of truth, all 17 other locale catalogs updated with the same two keys, t() used for the new strings. No physical ml-/left- Tailwind utilities introduced in the new warning banner.
  • No other CLAUDE.md-governed constants/catalogs (whitebox menu, PMTiles zoom mirrors, etc.) are touched by this diff.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/project.ts (1)

210-212: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Read each object property when it is serialized.

Object.entries(value) evaluates every property now, before serializeProjectValue processes the first entry. An earlier toJSON hook can mutate a later sibling and cause this serializer to serialize stale values. Use Object.keys(value) and read value[entryKey] in the loop; cover this with a regression test against native JSON.stringify.

Proposed fix
-    for (const [entryKey, entry] of Object.entries(value)) {
+    for (const entryKey of Object.keys(value)) {
+      const entry = (value as Record<string, unknown>)[entryKey];
       const serialized = serializeProjectValue(entry, depth + 1, entryKey, ancestors);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/project.ts` around lines 210 - 212, Update the
object-property loop in serializeProjectValue to iterate with Object.keys(value)
and read value[entryKey] inside each iteration, ensuring each sibling is fetched
only when serialized. Add a regression test comparing the serializer’s output
with native JSON.stringify when an earlier toJSON hook mutates a later property.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/core/src/project.ts`:
- Around line 210-212: Update the object-property loop in serializeProjectValue
to iterate with Object.keys(value) and read value[entryKey] inside each
iteration, ensuring each sibling is fetched only when serialized. Add a
regression test comparing the serializer’s output with native JSON.stringify
when an earlier toJSON hook mutates a later property.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 10086b47-84ac-45fe-9455-c793419d6a98

📥 Commits

Reviewing files that changed from the base of the PR and between cf44621 and 5c106b2.

📒 Files selected for processing (2)
  • packages/core/src/project.ts
  • tests/core-project.test.ts

Detect the "too large to serialize" failure across webview engines, not
just V8. GeoLibre's Tauri webview is JavaScriptCore on macOS and Linux,
which reports an out-of-memory error rather than V8's "Invalid string
length", so matching only V8's wording left desktop users on those
platforms with the generic save failure instead of the PMTiles/FlatGeobuf
guidance. The check now tests the message against the known engine
phrasings, still narrow enough that a cycle or other real serialization
bug reports generically.
Comment thread packages/core/src/project.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/geolibre-desktop/src/hooks/useProjectFileActions.ts (1)

686-696: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Apply the same visibility and credential-stripping contract to template saves.

SaveTemplateDialog still receives the raw project from buildCurrentProject and passes it directly to createProjectTemplate, so templates can keep hidden GeoJSON fields and plain-text credentials. Strip those from the project before creating the template, and add coverage for template egress.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/hooks/useProjectFileActions.ts` around lines 686 -
696, Update the template-save flow in SaveTemplateDialog to sanitize the project
returned by buildCurrentProject before passing it to createProjectTemplate,
applying the same visibility filtering and credential redaction used for project
saves. Ensure template output excludes hidden GeoJSON fields and plain-text
credentials, and add coverage verifying this sanitized egress.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/geolibre-desktop/src/hooks/useProjectFileActions.ts`:
- Around line 686-696: Update the template-save flow in SaveTemplateDialog to
sanitize the project returned by buildCurrentProject before passing it to
createProjectTemplate, applying the same visibility filtering and credential
redaction used for project saves. Ensure template output excludes hidden GeoJSON
fields and plain-text credentials, and add coverage verifying this sanitized
egress.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 371321c7-c9bb-433e-99ed-5662f35d3c2f

📥 Commits

Reviewing files that changed from the base of the PR and between 5c106b2 and a1060d2.

📒 Files selected for processing (1)
  • apps/geolibre-desktop/src/hooks/useProjectFileActions.ts

@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • None confirmed. One low-confidence behavioral-change risk: the new hand-written recursive serializer (serializeProjectValue in packages/core/src/project.ts) replaces V8's native, deep-recursion-tolerant JSON.stringify for the non-GeoJSON project skeleton. A pathologically deeply-nested project could now hit RangeError: Maximum call stack size exceeded where the old native call would have succeeded. It degrades gracefully (falls to the generic "could not save" message rather than crashing), and nothing in types.ts appears deeply recursive, so likelihood is low — flagged inline for awareness. (Confidence: low)

Security

  • None found. No injection, unsafe input handling, or secret exposure in the diff. Error messages logged via console.error don't leak project content.

Performance

  • The core change (compact-serializing GeoJSON subtrees while keeping project structure indented) is correct and well-verified: isGeoJsonValue reliably matches since geojson/embeddedGeoJSON are always typed as FeatureCollection in types.ts, and the custom walker's semantics (toJSON handling, boxed primitives, sparse arrays, cycle detection, dropped values) closely mirror native JSON.stringify, backed by solid tests including a byte-for-byte equivalence check for non-GeoJSON projects. (Confidence: high — no issues)

Quality

  • isGeoJsonValue's structural type-sniffing (any object with type in the 9 GeoJSON type names gets compacted) is an implicit schema contract; already thoroughly self-documented in the code as an accepted tradeoff, and today nothing else in the schema collides with it, so this is informational rather than actionable. (Confidence: low)
  • The Share flow (TopToolbar.tsx's getProject for ShareProjectDialog, unchanged by this PR) still calls serializeProject directly rather than through a serializeForSave-style guard, so hitting the string-length cap while sharing a large embedded project would surface a raw engine message (e.g. "Invalid string length") instead of the new friendly "convert to PMTiles/FlatGeobuf" guidance — Share is actually the highest-risk path for this since it always embeds vector data. This is pre-existing code outside the diff, but directly relevant to the PR's stated goal of covering "everywhere a project is serialized," so worth considering as a follow-up. (Confidence: medium)

CLAUDE.md

  • No violations. en.json is updated as the source of truth and all 17 other locale catalogs receive the same two keys (embedVectorLargeWarning, projectTooLargeToSave) in matching positions, consistent with the i18n convention.

@giswqs
giswqs merged commit 391d66b into main Aug 10, 2026
26 checks passed
@giswqs
giswqs deleted the fix/issue-1829-compact-embedded-geojson branch August 10, 2026 11:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Minify embedded geometry in .geolibre.json on save (currently pretty-printed, causing huge file sizes)

2 participants