diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml new file mode 100644 index 000000000..b0a26b66d --- /dev/null +++ b/.github/workflows/publish-packages.yml @@ -0,0 +1,74 @@ +name: Publish core and map packages + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # Required for npm trusted publishing and provenance. + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: actions/setup-node@v7 + with: + node-version: 24 + registry-url: https://registry.npmjs.org + + # Trusted Publishing (OIDC) requires npm >= 11.5.1, which the runner's + # bundled npm is not guaranteed to be. Pin to a known-good version rather + # than `npm@latest`: that tag briefly shipped a build missing the bundled + # `sigstore` module, which breaks `npm publish --provenance`. + - name: Upgrade npm for Trusted Publishing + run: npm install -g npm@11.5.1 + + - run: npm ci + + - name: Build the distributed bundles + run: npm run build -w @geolibre/core -w @geolibre/map + + # The manifests in git point at TypeScript source so the monorepo needs no + # build step; npm consumers need `dist`. npm ignores entry fields nested + # under `publishConfig`, so hoist them here, just before publishing. + # @geolibre/core is prepared alongside @geolibre/map so the `"*"` + # dependency between them is pinned to this release's version. + - name: Rewrite the manifests for publishing + run: node scripts/prepare-npm-package.mjs packages/core packages/map + + # Each package is checked against its own version: the two move in + # lockstep today, but reading core's version for map would silently skip + # a map release (or attempt one npm has already seen) the moment they + # diverge. + - id: version + run: | + for pkg in core map; do + version=$(node -p "require('./packages/$pkg/package.json').version") + if npm view "@geolibre/$pkg@$version" version >/dev/null 2>&1; then + echo "$pkg=false" >> "$GITHUB_OUTPUT" + else + echo "$pkg=true" >> "$GITHUB_OUTPUT" + fi + done + + # No NODE_AUTH_TOKEN: npm authenticates via the OIDC id-token from the + # trusted publisher configured on each package (opengeos/GeoLibre, this + # workflow file) and attaches build provenance. npm has no PyPI-style + # pending publisher, so a package has to exist before that connection can + # be made: the first version is published from a maintainer's machine + # once, then every later release goes through here. + - name: Publish @geolibre/core + if: steps.version.outputs.core == 'true' + run: npm publish -w @geolibre/core --provenance --access public + + # Published after core so a consumer installing @geolibre/map never + # resolves a pinned @geolibre/core that is not on the registry yet. + - name: Publish @geolibre/map + if: steps.version.outputs.map == 'true' + run: npm publish -w @geolibre/map --provenance --access public diff --git a/CLAUDE.md b/CLAUDE.md index fa39e0d1b..010438d39 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -110,7 +110,7 @@ The app is **store-driven**. `@geolibre/core` holds the Zustand store, domain ty Rendering is MapLibre GL JS in the webview, with **deck.gl** for raster/point-cloud/3D overlays. -**Packages:** `@geolibre/core` (types, project format, store) · `@geolibre/map` (MapLibre lifecycle + layer sync) · `@geolibre/ui` (shadcn-style primitives) · `@geolibre/processing` (client-side algorithm registry) · `@geolibre/plugins` (plugin interface + built-in plugins) · `@geolibre/embed` (typed iframe embed client, the one package published to npm — `.github/workflows/publish-embed.yml` publishes it on each GitHub Release, skipping a version already there) · `geolibre-desktop` (shell layout, Tauri I/O, composition). +**Packages:** `@geolibre/core` (types, project format, store) · `@geolibre/map` (MapLibre lifecycle + layer sync) · `@geolibre/ui` (shadcn-style primitives) · `@geolibre/processing` (client-side algorithm registry) · `@geolibre/plugins` (plugin interface + built-in plugins) · `@geolibre/embed` (typed iframe embed client — `.github/workflows/publish-embed.yml` publishes it on each GitHub Release, skipping a version already there) · `geolibre-desktop` (shell layout, Tauri I/O, composition). **Plugins:** Built-in plugins live in `packages/plugins/src/plugins/`, are exported from that package's `index.ts`, and registered in `apps/geolibre-desktop/src/hooks/usePlugins.ts`. External plugins load from zips or a `plugin.json` manifest; bundled drop-ins under `apps/geolibre-desktop/public/plugins//` bake into both web and desktop builds. See `docs/plugin-api.md`. @@ -123,6 +123,28 @@ The browser build proxies the sidecar at `/sidecar` (same-origin, no CORS); conf ## Conventions - Never commit directly to `main`; branch and open a PR. +- **`@geolibre/core` and `@geolibre/map` are published to npm** by + `.github/workflows/publish-packages.yml` on each GitHub Release, alongside + `@geolibre/embed`. Their checked-in `main`/`types`/`exports` point at + TypeScript **source**, because that is how the monorepo consumes them: Vite, + `tsc` and tsx all resolve `./src/index.ts` through the package's own + `exports`, so `npm run dev` and `node --import tsx --test tests/.test.ts` + need no build step. The npm tarball ships `dist` instead, and npm cannot + express that split on its own: unlike pnpm and Yarn it deliberately **ignores + entry fields nested under `publishConfig`** (npm/cli#7586), so a manifest that + only states its dist entries there publishes `./src/index.ts` to consumers who + never receive `src`. The published entries therefore live under + `publishConfig`, and `scripts/prepare-npm-package.mjs` hoists them (and pins + the `"*"` `@geolibre/core` dependency to the release version) just before + `npm publish`. Point those top-level fields at `dist` and every frontend test + that imports a `@geolibre/map` subpath fails with `ERR_MODULE_NOT_FOUND`, + because `dist` is gitignored and nothing builds it before the suite. + `tests/prepare-npm-package.test.ts` guards both halves, including that each + published path is one the package's own `tsdown` entries actually emit + (`--format esm --dts` writes `.mjs` and `.d.mts`, **not** + `.d.ts`). The release workflow does build both packages, but a green build + proves only that the bundles were written, not that every path the manifest + publishes names one of them, so nothing else would notice that drift. - **`backend/geolibre_server/uv.lock` is committed** (the root `.gitignore` ignores `uv.lock` everywhere else and negates it for this one path). That project is bundled into the desktop installers and launched with `uv run --frozen --project ` from `src-tauri/src/lib.rs` — a directory the user cannot write (`C:\Program Files\…`, `/usr/lib/GeoLibre Desktop/…`). Ship it lockless and uv resolves, then tries to _write_ `uv.lock` there, fails with "Permission denied" and exits 2 — which reaches the user as "Jupyter server exited before it was ready (exit code: 2)" with the cause invisible. So: any edit to that `pyproject.toml`'s dependencies must land with a refreshed lock (`uv lock --project backend/geolibre_server`). CI's "Check the bundled sidecar lockfile is in sync" step (`uv lock --check`) fails if they drift. - Tauri CSP allowlists tile/style hosts (OpenFreeMap, CARTO) — new external map/tile hosts must be added there. - Map/tile-host CORS for selected release assets is handled by a dev-server raster proxy. diff --git a/package-lock.json b/package-lock.json index bd5de42ff..b6c8053cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6943,6 +6943,36 @@ "generate_style": "src/cli.ts" } }, + "node_modules/@quansync/fs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-1.0.0.tgz", + "integrity": "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quansync": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/@quansync/fs/node_modules/quansync": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-1.0.0.tgz", + "integrity": "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, "node_modules/@radix-ui/number": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz", @@ -12225,6 +12255,385 @@ } } }, + "node_modules/@yuku-codegen/binding-android-arm64": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-codegen/binding-android-arm64/-/binding-android-arm64-0.8.7.tgz", + "integrity": "sha512-C/0zV5IhgVdYhGJTwrY0v8dknxlhiKwtVJkMUaexu9/QvRmzlV4vfU3hZlUSgqc2BxQHntL1mCVbDq8j0FRFDw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@yuku-codegen/binding-darwin-arm64": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-codegen/binding-darwin-arm64/-/binding-darwin-arm64-0.8.7.tgz", + "integrity": "sha512-/u+REDMI4a0/lsJXTM4c53/w31OGZLOReZIyg62uhgLs0kc8NHsj/nOcxTdlQjq5gi0zhdkccD9LaLTXcdzPvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@yuku-codegen/binding-darwin-x64": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-codegen/binding-darwin-x64/-/binding-darwin-x64-0.8.7.tgz", + "integrity": "sha512-sMMzFOwCo4WXR+/6zIBThOocSC50iIIZZdfiIDbaLvj0Ax/rWt/iavyfEAqajyvzydLyCqR/ZItdLWSRlu1umw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@yuku-codegen/binding-freebsd-x64": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-codegen/binding-freebsd-x64/-/binding-freebsd-x64-0.8.7.tgz", + "integrity": "sha512-MpdpKXix9P+Y1rKgjvcNeNtGjXeL1CmttNhYINrWls8kRpm4xM/oBGTmn6w7to8lAlwj5jm8q03dQPl5mRv4Qw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@yuku-codegen/binding-linux-arm-gnu": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-codegen/binding-linux-arm-gnu/-/binding-linux-arm-gnu-0.8.7.tgz", + "integrity": "sha512-rr1srFLlPAmC1vtxfc9C1YLDe3iH09YjfSeeIidBqKhzx1MATjOAq4mjlRUOnhr/L27MotWIYOFKwVsd5JZFOg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@yuku-codegen/binding-linux-arm-musl": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-codegen/binding-linux-arm-musl/-/binding-linux-arm-musl-0.8.7.tgz", + "integrity": "sha512-eAufXh8qBRpiSO6ueaMDL+yyoXIGLhpUce72YbcACtZU2qhExwBIJyEtQf5kH2Ki0X2aqkSjSfog4OPtKXan3Q==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@yuku-codegen/binding-linux-arm64-gnu": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-codegen/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.8.7.tgz", + "integrity": "sha512-gw4w6wPoHObBrdIC4duVWLmJOvpdE25j5D7yrM5mACNlK4klRz/lv8hK+ssQk9EJHBgZjSaqZIJVFgqNYbfv7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@yuku-codegen/binding-linux-arm64-musl": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-codegen/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.8.7.tgz", + "integrity": "sha512-L68N6Y4XkqcIaKo3Ra88JEvBEH4AHff44A4INcrxeVWZ8CZtu2tCpfVxe3hR8qQQMcvBSQlDn24KpkCKEhVvfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@yuku-codegen/binding-linux-x64-gnu": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-codegen/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.8.7.tgz", + "integrity": "sha512-dzyAbltJmf3Cqlb8HcFuYIf5Yn0fl1vTr3XJ9HiVNNvOlhqPSArOqtw9vI1p6/VTXTjrMLXlU+s+/kNHiIy/Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@yuku-codegen/binding-linux-x64-musl": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-codegen/binding-linux-x64-musl/-/binding-linux-x64-musl-0.8.7.tgz", + "integrity": "sha512-Otw4MH3404q0Bbvl+YTdW9aoUV5vXmUw8260bWvt1XlaoIX/ceSgI4ygheRDMfBPoHt6FDayQaO9OLVUZAgkFA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@yuku-codegen/binding-win32-arm64": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-codegen/binding-win32-arm64/-/binding-win32-arm64-0.8.7.tgz", + "integrity": "sha512-qo/jyrzryiBuKEsFiuWaBCBe3tRMynQ0qFWFgOEjcCMQeZfBm+wKiVEUEFXXLc7bh8YezguAWp0Mtnhq4ARNyA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@yuku-codegen/binding-win32-x64": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-codegen/binding-win32-x64/-/binding-win32-x64-0.8.7.tgz", + "integrity": "sha512-D5lDsVDx6m00E6bWySlWdH72Ca4TPSaphDqB6QjU6MpuNLIJqoGoatYyq2rOmBE8Zv/kunot/o58KGL03P3eiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@yuku-parser/binding-android-arm64": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-parser/binding-android-arm64/-/binding-android-arm64-0.8.7.tgz", + "integrity": "sha512-eGKYiUDX7Y0V7tDTmg+JTVnXnjMqfXXsorZ+EDf5kxwchQ3Or1HS14MzI2fw+jFhHR85fCWt+mtX33Yao73hIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@yuku-parser/binding-darwin-arm64": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.8.7.tgz", + "integrity": "sha512-Re0RHelKLnjEURulY2/KxW+Ngb8zuNA4BRZuMwgGQNzVumT6u4U2N2hc01oeYVNVof0i7GrXE4UCNBgbpRRnjQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@yuku-parser/binding-darwin-x64": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-parser/binding-darwin-x64/-/binding-darwin-x64-0.8.7.tgz", + "integrity": "sha512-Hn8DROtQkjlA1ACbPgj4a7eP9IuVOI504oiTwpkWPbpaDWD9KdmnVYCqW+1LfenNK/g7O9NhWGpXEdaCNX7lIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@yuku-parser/binding-freebsd-x64": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.8.7.tgz", + "integrity": "sha512-bAP2OV8wRuzplX/jYxv9+vvqQT8JxyNphI8fLfXGL054Xs+4/J5u33cIm3y4rxY8rdoLmmdiJs2Tq7r7lrDRfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@yuku-parser/binding-linux-arm-gnu": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-parser/binding-linux-arm-gnu/-/binding-linux-arm-gnu-0.8.7.tgz", + "integrity": "sha512-kTYwJQQgmZeAWdDIWabiReIZMpmfLueIj1tCmjStUtFGhR1Z0qwxonKVfUC4N7h/VhGGzLZ//7O1kgt1QKqgCg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@yuku-parser/binding-linux-arm-musl": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-parser/binding-linux-arm-musl/-/binding-linux-arm-musl-0.8.7.tgz", + "integrity": "sha512-uL4jE8HPT2BLlxAXyD10LqgPuXa9eDa0BKpCdSANmzIJghq/2eZo3/gQNtaxPZMupWoxjYzSad9IXrwu7aYPXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@yuku-parser/binding-linux-arm64-gnu": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.8.7.tgz", + "integrity": "sha512-3gVN4pWSKZmXiNX7cU164dR9MPvesCHnlH6nPfpK+yQsCuYphjKKplcb4SnZBrhiqmXbgb2HR0c2TS0IZUPhgA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@yuku-parser/binding-linux-arm64-musl": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.8.7.tgz", + "integrity": "sha512-S0mwfEjoLpxzXeZw802Wa4RaELsQiPtWqG6INcy8j4GtvNFtl4LCX3eGO1XLn9pyLAISLzTRyU3zUCBUPin8lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@yuku-parser/binding-linux-x64-gnu": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.8.7.tgz", + "integrity": "sha512-lnbWdPmerE5D1uH1G4IEZKnPzCrWCStRGrtgpSIe1RibAo5bZIjDbbbPYXmMHCEh4F+x/JaJpElh26a3r+BPbg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@yuku-parser/binding-linux-x64-musl": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.8.7.tgz", + "integrity": "sha512-769uwndMvMzUvATWbAcEvyLHKA+DzhHSCl/obBUrRdYfRo26yxui6S8y3z7uJ+Naup7UKrDxrpK7OnQkxkl9KQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@yuku-parser/binding-win32-arm64": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-parser/binding-win32-arm64/-/binding-win32-arm64-0.8.7.tgz", + "integrity": "sha512-mEB/9PlaAkisJ6KWGz0zvywXoU6+80dTlR2LwS7s/jcXXoU6fm2+sitBZXtqu3+Q4DcDgPxM45uWMCzPs0TSRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@yuku-parser/binding-win32-x64": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-parser/binding-win32-x64/-/binding-win32-x64-0.8.7.tgz", + "integrity": "sha512-8vNB2DP0ou61nGb8tc/qfi41gfyDXz1MHr2zqL3nR+cJ6CEbiuWV/l/a/vv151gCgiZLLAyGkQGENpozdg716w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@yuku-toolchain/types": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@yuku-toolchain/types/-/types-0.8.7.tgz", + "integrity": "sha512-2Z53dNxAJL6UvFoIrDZvYf3zlO8s4VJK4O2hhaB4mXVwwpX/7ajtss3cmfqKvamlNLWyt9FSWs4eoYdlbxpnHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@zarrita/storage": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@zarrita/storage/-/storage-0.2.0.tgz", @@ -12457,6 +12866,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansis": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.3.1.tgz", + "integrity": "sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "node_modules/anynum": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", @@ -12979,6 +13398,16 @@ "node": ">= 0.8" } }, + "node_modules/cac": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cac/-/cac-7.0.0.tgz", + "integrity": "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -13687,6 +14116,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "dev": true, + "license": "MIT" + }, "node_modules/delaunator": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", @@ -13880,6 +14316,27 @@ "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", "license": "Apache-2.0" }, + "node_modules/dts-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/dts-resolver/-/dts-resolver-3.0.0.tgz", + "integrity": "sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "oxc-resolver": ">=11.0.0" + }, + "peerDependenciesMeta": { + "oxc-resolver": { + "optional": true + } + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -13938,6 +14395,16 @@ "dev": true, "license": "ISC" }, + "node_modules/empathic": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.1.tgz", + "integrity": "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -15360,6 +15827,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "5.0.0-beta.5", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.5.tgz", + "integrity": "sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "engines": { + "node": ">=20.20.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/gl-matrix": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz", @@ -15992,6 +16475,13 @@ "node": ">=16.9.0" } }, + "node_modules/hookable": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.1.tgz", + "integrity": "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==", + "dev": true, + "license": "MIT" + }, "node_modules/html-escaper": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", @@ -16308,6 +16798,19 @@ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "license": "MIT" }, + "node_modules/import-without-cache": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/import-without-cache/-/import-without-cache-0.4.0.tgz", + "integrity": "sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -18917,6 +19420,20 @@ "integrity": "sha512-9gRK4+sRWzeN6AOewNBTLXir7Zl/i3GB6Yl26gK4flxz8BXVpD3kt8amREmWNb0mxYOGDotvE5a4N+PtGGKdkg==", "license": "MIT" }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/ol": { "version": "10.9.0", "resolved": "https://registry.npmjs.org/ol/-/ol-10.9.0.tgz", @@ -20139,6 +20656,16 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve-protobuf-schema": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", @@ -20216,6 +20743,48 @@ "@rolldown/binding-win32-x64-msvc": "1.2.2" } }, + "node_modules/rolldown-plugin-dts": { + "version": "0.27.14", + "resolved": "https://registry.npmjs.org/rolldown-plugin-dts/-/rolldown-plugin-dts-0.27.14.tgz", + "integrity": "sha512-ZvuDDwoIpRK9RPxDXratCpklFO9QZZWndf/sd0VBFb4LEj0jj07UcHK9OCh7V4XiFz2Z89ziyBC2K6tJiDjrbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dts-resolver": "^3.0.0", + "get-tsconfig": "5.0.0-beta.5", + "obug": "^2.1.4", + "yuku-ast": "^0.8.0", + "yuku-codegen": "^0.8.0", + "yuku-parser": "^0.8.0" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@typescript/native-preview": "*", + "@volar/typescript": "~2.4.0", + "rolldown": "^1.0.0", + "typescript": "^5.0.0 || ^6.0.0 || ~7.0.0", + "vue-tsc": "~3.2.0 || ~3.3.0" + }, + "peerDependenciesMeta": { + "@typescript/native-preview": { + "optional": true + }, + "@volar/typescript": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vue-tsc": { + "optional": true + } + } + }, "node_modules/rollup": { "version": "4.62.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", @@ -21279,6 +21848,16 @@ "esm": "^3.2.25" } }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -21355,6 +21934,16 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/ts-algebra": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", @@ -21374,6 +21963,79 @@ "typescript": ">=4.8.4" } }, + "node_modules/tsdown": { + "version": "0.22.14", + "resolved": "https://registry.npmjs.org/tsdown/-/tsdown-0.22.14.tgz", + "integrity": "sha512-ule7Y+fsAN2iZbLDoo7C4KYljFJNJJ+fLshyn+9gozeTspVersWHxwdGB+Dm2hzA38s6muFnUTl0jK3vJm9ifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansis": "^4.3.1", + "cac": "^7.0.0", + "defu": "^6.1.7", + "empathic": "^2.0.1", + "hookable": "^6.1.1", + "import-without-cache": "^0.4.0", + "obug": "^2.1.4", + "picomatch": "^4.0.5", + "rolldown": "~1.2.0", + "rolldown-plugin-dts": "^0.27.13", + "tinyexec": "^1.2.4", + "tinyglobby": "^0.2.17", + "tree-kill": "^1.2.2", + "unconfig-core": "^7.5.0", + "verkit": "^0.3.0" + }, + "bin": { + "tsdown": "dist/run.mjs" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@arethetypeswrong/core": "^0.18.1", + "@tsdown/css": "0.22.14", + "@tsdown/exe": "0.22.14", + "@vitejs/devtools": "*", + "publint": "^0.3.8", + "tsx": "*", + "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0", + "unplugin-unused": "^0.5.0", + "unrun": "*" + }, + "peerDependenciesMeta": { + "@arethetypeswrong/core": { + "optional": true + }, + "@tsdown/css": { + "optional": true + }, + "@tsdown/exe": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "publint": { + "optional": true + }, + "tsx": { + "optional": true + }, + "typescript": { + "optional": true + }, + "unplugin-unused": { + "optional": true + }, + "unrun": { + "optional": true + } + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -21634,6 +22296,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unconfig-core": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/unconfig-core/-/unconfig-core-7.5.0.tgz", + "integrity": "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@quansync/fs": "^1.0.0", + "quansync": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unconfig-core/node_modules/quansync": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-1.0.0.tgz", + "integrity": "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, "node_modules/undici": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", @@ -21906,6 +22599,19 @@ "node": ">= 0.8" } }, + "node_modules/verkit": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.3.2.tgz", + "integrity": "sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, "node_modules/virtual-dom": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/virtual-dom/-/virtual-dom-2.1.1.tgz", @@ -23037,6 +23743,65 @@ "url": "https://opencollective.com/express" } }, + "node_modules/yuku-ast": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/yuku-ast/-/yuku-ast-0.8.7.tgz", + "integrity": "sha512-h6+4bDfyootiMB9vckk5uKo5r5j0GHrkr17FQTDNfEsFT3DWlN9uu1HJwQwc64pgmLCI945fWM3lbTIqxjT3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@yuku-toolchain/types": "^0.8.7" + } + }, + "node_modules/yuku-codegen": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/yuku-codegen/-/yuku-codegen-0.8.7.tgz", + "integrity": "sha512-adwDZSh8oVDzhE6Du9PwVWxcOxeV0e2EVhUuMKWfhSY4wkrDq9eqixlxFF3l/XGUV1E7UFzhpz9393MUumkyNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@yuku-toolchain/types": "^0.8.7" + }, + "optionalDependencies": { + "@yuku-codegen/binding-android-arm64": "0.8.7", + "@yuku-codegen/binding-darwin-arm64": "0.8.7", + "@yuku-codegen/binding-darwin-x64": "0.8.7", + "@yuku-codegen/binding-freebsd-x64": "0.8.7", + "@yuku-codegen/binding-linux-arm-gnu": "0.8.7", + "@yuku-codegen/binding-linux-arm-musl": "0.8.7", + "@yuku-codegen/binding-linux-arm64-gnu": "0.8.7", + "@yuku-codegen/binding-linux-arm64-musl": "0.8.7", + "@yuku-codegen/binding-linux-x64-gnu": "0.8.7", + "@yuku-codegen/binding-linux-x64-musl": "0.8.7", + "@yuku-codegen/binding-win32-arm64": "0.8.7", + "@yuku-codegen/binding-win32-x64": "0.8.7" + } + }, + "node_modules/yuku-parser": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/yuku-parser/-/yuku-parser-0.8.7.tgz", + "integrity": "sha512-vRD9nwt4L3aYpxNqeSC4WqLv58xrXef0Ong1Mc45CTXTIpvLafx7JO05sczmQZwdLEZvywrLOGdNC5+Rp5N1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@yuku-toolchain/types": "^0.8.7", + "yuku-ast": "^0.8.7" + }, + "optionalDependencies": { + "@yuku-parser/binding-android-arm64": "0.8.7", + "@yuku-parser/binding-darwin-arm64": "0.8.7", + "@yuku-parser/binding-darwin-x64": "0.8.7", + "@yuku-parser/binding-freebsd-x64": "0.8.7", + "@yuku-parser/binding-linux-arm-gnu": "0.8.7", + "@yuku-parser/binding-linux-arm-musl": "0.8.7", + "@yuku-parser/binding-linux-arm64-gnu": "0.8.7", + "@yuku-parser/binding-linux-arm64-musl": "0.8.7", + "@yuku-parser/binding-linux-x64-gnu": "0.8.7", + "@yuku-parser/binding-linux-x64-musl": "0.8.7", + "@yuku-parser/binding-win32-arm64": "0.8.7", + "@yuku-parser/binding-win32-x64": "0.8.7" + } + }, "node_modules/zarrita": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/zarrita/-/zarrita-0.7.3.tgz", @@ -23183,6 +23948,7 @@ "packages/core": { "name": "@geolibre/core", "version": "2.7.0", + "license": "MIT", "dependencies": { "@maplibre/maplibre-gl-style-spec": "^26.2.1", "uuid": "^14.0.1", @@ -23191,6 +23957,7 @@ }, "devDependencies": { "@types/geojson": "^7946.0.16", + "tsdown": "^0.22.14", "typescript": "^7.0.2" } }, @@ -23275,6 +24042,7 @@ "packages/map": { "name": "@geolibre/map", "version": "2.7.0", + "license": "MIT", "dependencies": { "@geolibre/core": "*", "@maplibre/geojson-vt": "^6.1.1", @@ -23301,6 +24069,7 @@ "@types/geojson": "^7946.0.16", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", + "tsdown": "^0.22.14", "typescript": "^7.0.2" }, "peerDependencies": { diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 000000000..5bbf75267 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,20 @@ +# @geolibre/core + +Domain types, the `.geolibre.json` project schema, and the Zustand store that +[GeoLibre](https://geolibre.app) is built on. + +```bash +npm install @geolibre/core +``` + +```ts +import type { GeoLibreLayer, GeoLibreProject } from "@geolibre/core"; +``` + +The package is ESM-only and ships TypeScript declarations. See +[`docs/project-format.md`](https://github.com/opengeos/GeoLibre/blob/main/docs/project-format.md) +for the project schema and +[`docs/architecture.md`](https://github.com/opengeos/GeoLibre/blob/main/docs/architecture.md) +for how the store drives the app. + +MIT licensed. diff --git a/packages/core/package.json b/packages/core/package.json index 9c53efb97..fbd6f70b5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,13 +1,47 @@ { "name": "@geolibre/core", "version": "2.7.0", - "private": true, + "description": "GeoLibre domain types, project schema, and Zustand store", + "keywords": [ + "geolibre", + "geospatial", + "gis", + "maplibre", + "zustand" + ], + "homepage": "https://geolibre.app", + "bugs": "https://github.com/opengeos/GeoLibre/issues", + "license": "MIT", + "author": "Qiusheng Wu", + "repository": { + "type": "git", + "url": "https://github.com/opengeos/GeoLibre", + "directory": "packages/core" + }, + "files": [ + "dist", + "README.md" + ], "type": "module", "main": "./src/index.ts", "types": "./src/index.ts", "exports": { ".": "./src/index.ts" }, + "publishConfig": { + "main": "./dist/index.mjs", + "types": "./dist/index.d.mts", + "exports": { + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs" + } + }, + "access": "public" + }, + "scripts": { + "build": "tsdown src/index.ts --format esm --dts" + }, "dependencies": { "@maplibre/maplibre-gl-style-spec": "^26.2.1", "uuid": "^14.0.1", @@ -16,6 +50,7 @@ }, "devDependencies": { "@types/geojson": "^7946.0.16", + "tsdown": "^0.22.14", "typescript": "^7.0.2" } } diff --git a/packages/map/README.md b/packages/map/README.md new file mode 100644 index 000000000..ffb6c46c2 --- /dev/null +++ b/packages/map/README.md @@ -0,0 +1,34 @@ +# @geolibre/map + +The headless half of [GeoLibre](https://geolibre.app)'s map layer: data loading, +layer synchronization, and the style engine, without React, the Zustand store, +Cesium, or any map controls. + +```bash +npm install @geolibre/map maplibre-gl +``` + +```ts +import maplibregl from "maplibre-gl"; +import { createLayerSync } from "@geolibre/map"; + +const map = new maplibregl.Map({ container: "map", style: "https://tiles.openfreemap.org/styles/liberty" }); +const sync = createLayerSync(map); + +map.on("load", () => { + // Bottom-to-top stacking order; call again whenever the layer list changes. + sync.sync(layers); +}); +``` + +Alongside `createLayerSync` the entry point exports the paint builders +(`fillPaint`, `linePaint`, `circlePaint`, ...), the COG DEM and PMTiles protocol +registration helpers, and the Mapbox Style / SLD / QML import and export +functions. See +[`docs/architecture.md`](https://github.com/opengeos/GeoLibre/blob/main/docs/architecture.md). + +The package is ESM-only and ships TypeScript declarations. `maplibre-gl` is a +dependency; `react` and `react-dom` are peer dependencies of the full +in-repository entry point and are not needed by the headless surface. + +MIT licensed. diff --git a/packages/map/package.json b/packages/map/package.json index 0946f6c6e..6022728e2 100644 --- a/packages/map/package.json +++ b/packages/map/package.json @@ -1,15 +1,63 @@ { "name": "@geolibre/map", "version": "2.7.0", - "private": true, + "description": "Headless MapLibre layer synchronization and style engine from GeoLibre", + "keywords": [ + "geolibre", + "geospatial", + "gis", + "headless", + "maplibre", + "pmtiles" + ], + "homepage": "https://geolibre.app", + "bugs": "https://github.com/opengeos/GeoLibre/issues", + "license": "MIT", + "author": "Qiusheng Wu", + "repository": { + "type": "git", + "url": "https://github.com/opengeos/GeoLibre", + "directory": "packages/map" + }, + "files": [ + "dist", + "README.md" + ], "type": "module", "main": "./src/index.ts", "types": "./src/index.ts", "exports": { ".": "./src/index.ts", + "./headless": "./src/headless.ts", "./derived-geometry": "./src/derived-geometry.ts", "./pmtiles-layer": "./src/pmtiles-layer.ts" }, + "publishConfig": { + "main": "./dist/headless.mjs", + "types": "./dist/headless.d.mts", + "exports": { + ".": { + "types": "./dist/headless.d.mts", + "import": "./dist/headless.mjs" + }, + "./headless": { + "types": "./dist/headless.d.mts", + "import": "./dist/headless.mjs" + }, + "./derived-geometry": { + "types": "./dist/derived-geometry.d.mts", + "import": "./dist/derived-geometry.mjs" + }, + "./pmtiles-layer": { + "types": "./dist/pmtiles-layer.d.mts", + "import": "./dist/pmtiles-layer.mjs" + } + }, + "access": "public" + }, + "scripts": { + "build": "tsdown src/headless.ts src/derived-geometry.ts src/pmtiles-layer.ts --format esm --dts" + }, "dependencies": { "@geolibre/core": "*", "@maplibre/geojson-vt": "^6.1.1", @@ -36,6 +84,7 @@ "@types/geojson": "^7946.0.16", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", + "tsdown": "^0.22.14", "typescript": "^7.0.2" }, "peerDependencies": { diff --git a/packages/map/src/headless.ts b/packages/map/src/headless.ts new file mode 100644 index 000000000..1ac4e49e2 --- /dev/null +++ b/packages/map/src/headless.ts @@ -0,0 +1,161 @@ +/** + * Headless entry (`@geolibre/map/headless`): data loading + layer + * synchronization without React, the zustand store, Cesium, or map controls. + * + * Feed a `GeoLibreLayer[]` to {@link createLayerSync}'s `sync()` whenever the + * layer model changes; it diffs against the previous array and drives + * MapLibre's addSource/addLayer through the same `syncLayer()` used by the + * full app. + */ +import type { GeoLibreLayer } from "@geolibre/core"; +import type * as maplibregl from "maplibre-gl"; +import { removeLayerFromMap, syncLayer } from "./layer-sync"; + +export interface LayerSync { + /** Diff-sync the full layer list (bottom-to-top stacking order). */ + sync(layers: GeoLibreLayer[]): void; + /** Remove every layer this sync previously added. */ + dispose(): void; +} + +/** + * Reapplies the input order among synchronized layers. Unlike MapController, + * this headless sync does not compute style-layer anchors, so it cannot preserve + * their placement relative to unmanaged or asynchronously inserted layers. + */ +export function createLayerSync(map: maplibregl.Map): LayerSync { + let synced: GeoLibreLayer[] = []; + + return { + sync(layers) { + const nextIds = new Set(layers.map((layer) => layer.id)); + const previousById = new Map(synced.map((layer) => [layer.id, layer])); + for (const previous of synced) { + if (!nextIds.has(previous.id)) removeLayerFromMap(map, previous.id, previous); + } + + // Input order is bottom-to-top: each addLayer without an anchor lands on + // top of the previous one, so re-adding the whole tail in order rebuilds + // the stack. MapLibre leaves an existing layer exactly where it is, so a + // layer that only moved has to be removed first or it keeps its old + // position and the documented order silently stops holding. Find the + // lowest position whose occupant changed -- a layer that moved, or a new + // layer inserted below existing ones -- and rebuild from there up. + const keptIds = synced.map((layer) => layer.id).filter((id) => nextIds.has(id)); + let rebuildFrom = layers.length; + let keptIndex = 0; + for (let index = 0; index < layers.length; index += 1) { + const id = layers[index].id; + if (!previousById.has(id) || id !== keptIds[keptIndex]) { + rebuildFrom = index; + break; + } + keptIndex += 1; + } + for (let index = rebuildFrom; index < layers.length; index += 1) { + const previous = previousById.get(layers[index].id); + if (previous) removeLayerFromMap(map, layers[index].id, previous); + } + + for (const layer of layers) syncLayer(map, layer); + synced = [...layers]; + }, + dispose() { + for (const layer of synced) removeLayerFromMap(map, layer.id, layer); + synced = []; + }, + }; +} + +// Data-loading helpers (COG / PMTiles protocols must be registered before use). +export { + registerCogDemSource, + encodeTerrariumDem, + CogDemError, + type CogDemErrorCode, +} from "./cog-dem-source"; +export { + registerPMTilesArchive, + unregisterPMTilesArchive, + ensureRemotePMTilesArchive, +} from "./layer-sync"; +export { readRemotePMTilesInfo, readPMTilesArchiveInfo } from "./pmtiles-layer"; +// Style engine outputs (standard MapLibre style-spec objects). +export { + fillPaint, + linePaint, + circlePaint, + heatmapPaint, + clusterCirclePaint, + fillExtrusionPaint, + rasterPaint, +} from "./style-mapper"; +// Vector geometry/ID conventions shared by the paths above. +export { + detectGeometryProfile, + getLayerBounds, + sourceId, + fillLayerId, + lineLayerId, + circleLayerId, +} from "./geojson-loader"; +export { + buildGeneratedGeometry, + buildInvertedMask, + generatedGeometryKinds, + lineDecorationColorValue, +} from "./derived-geometry"; +export { + buildMapboxStyle, + mapboxStyleToJson, + type ExportableLayer, + type MapboxStyleExportOptions, + type MapboxStyleExportResult, +} from "./mapbox-style-export"; +export { + applyMapboxStyleImport, + parseMapboxStyle, + type MapboxStyleImportResult, +} from "./mapbox-style-import"; +export { buildGeoLibreQueryStyle, geoLibreStyleSourceName } from "./query-param-style"; +export { + buildSld, + OGC_SCALE_DENOMINATOR_AT_ZOOM_0, + type SldExportableLayer, + type SldExportOptions, + type SldExportResult, +} from "./sld-export"; +export { applySldImport, parseSld, type SldImportResult } from "./sld-import"; +export { + buildQml, + type QmlExportableLayer, + type QmlExportOptions, + type QmlExportResult, +} from "./qml-export"; +export { applyQmlImport, parseQml, type QmlImportResult } from "./qml-import"; +export { + isMapboxStyleUrl, + loadMapboxStyle, + mapboxAccessTokenFromStyleUrl, + redactMapboxStyleUrl, + resolveMapboxInternalUrl, + transformMapboxStyle, +} from "./mapbox-style"; +export { + buildProtomapsBasemapStyle, + evictOfflineBasemapStyle, + getOfflineBasemapStyle, + isOfflineBasemapSentinel, + registerOfflineBasemapStyle, + OFFLINE_BASEMAP_SENTINEL_PREFIX, + PROTOMAPS_FLAVORS, + type ProtomapsBasemapStyleOptions, + type ProtomapsFlavor, +} from "./protomaps-basemap"; +export { + featuresIntersectingPolygon, + selectionModeFromModifiers, + type FeatureSelectionRequest, + type FeatureSelectionShape, +} from "./feature-selection"; +export { isPlaceholderLayer, placeholderMessage } from "./placeholders"; diff --git a/scripts/prepare-npm-package.mjs b/scripts/prepare-npm-package.mjs new file mode 100644 index 000000000..4fd5869f0 --- /dev/null +++ b/scripts/prepare-npm-package.mjs @@ -0,0 +1,91 @@ +#!/usr/bin/env node +/** + * Rewrite a workspace manifest into the form it should be published in. + * + * The monorepo consumes `@geolibre/core` and `@geolibre/map` straight from + * TypeScript source: Vite, tsx and `tsc` all resolve `./src/index.ts` through + * the package's own `exports`, so nothing has to be built before `npm run dev`, + * `npm run test:frontend`, or a single `node --import tsx --test` run. npm + * consumers need the built `dist` bundle instead. + * + * npm cannot express that split on its own. `publishConfig` only overrides npm + * *config* (registry, access, tag) at publish time -- npm deliberately ignores + * entry fields nested there, unlike pnpm and Yarn (npm/cli#7586). So the split + * lives here: the manifest keeps the source entries that the repo needs, states + * the published entries under `publishConfig`, and the publish workflow runs + * this script to hoist them just before `npm publish`. + * + * It also pins `"*"` workspace dependencies to the exact version being + * published. npm resolves `"*"` to the local workspace during development but + * publishes it verbatim, which would let a consumer install any `@geolibre/core` + * at all alongside a given `@geolibre/map`. + * + * Usage: node scripts/prepare-npm-package.mjs packages/map [...more] + */ +import { readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import process from "node:process"; + +/** Entry fields npm ignores inside `publishConfig` and that we hoist by hand. */ +const HOISTED_FIELDS = ["main", "module", "types", "typings", "browser", "exports", "files"]; + +/** + * @param {Record} manifest Parsed `package.json`. + * @param {(name: string) => string | undefined} workspaceVersion Version of a + * sibling workspace, or undefined when the name is not a workspace. + * @returns {Record} The manifest as it should be published. + */ +export function toPublishManifest(manifest, workspaceVersion = () => undefined) { + const published = { ...manifest }; + const publishConfig = { ...(manifest.publishConfig ?? {}) }; + + for (const field of HOISTED_FIELDS) { + if (field in publishConfig) { + published[field] = publishConfig[field]; + delete publishConfig[field]; + } + } + published.publishConfig = publishConfig; + delete published.private; + + for (const kind of ["dependencies", "peerDependencies", "optionalDependencies"]) { + const deps = manifest[kind]; + if (!deps) continue; + const pinned = { ...deps }; + for (const [name, range] of Object.entries(deps)) { + if (range !== "*") continue; + const version = workspaceVersion(name); + if (version) pinned[name] = `^${version}`; + } + published[kind] = pinned; + } + + return published; +} + +function main(dirs) { + if (dirs.length === 0) { + console.error("usage: node scripts/prepare-npm-package.mjs [...]"); + process.exit(1); + } + + const manifests = new Map( + dirs.map((dir) => { + const file = path.join(dir, "package.json"); + return [dir, JSON.parse(readFileSync(file, "utf8"))]; + }), + ); + const versions = new Map( + [...manifests.values()].map((manifest) => [manifest.name, manifest.version]), + ); + + for (const [dir, manifest] of manifests) { + const published = toPublishManifest(manifest, (name) => versions.get(name)); + writeFileSync(path.join(dir, "package.json"), `${JSON.stringify(published, null, 2)}\n`); + console.log(`prepared ${manifest.name}@${manifest.version} for publishing (${dir})`); + } +} + +if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) { + main(process.argv.slice(2)); +} diff --git a/tests/headless-layer-sync.test.ts b/tests/headless-layer-sync.test.ts new file mode 100644 index 000000000..3e0996c10 --- /dev/null +++ b/tests/headless-layer-sync.test.ts @@ -0,0 +1,109 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { DEFAULT_LAYER_STYLE, type GeoLibreLayer } from "@geolibre/core"; +import { createLayerSync } from "../packages/map/src/headless"; + +/** + * Stateful maplibre stub that keeps a real style-layer order, so a test can + * assert the stack `createLayerSync` leaves behind rather than the calls it + * made getting there. + */ +function makeMapStub() { + const layers: { id: string; source?: string }[] = []; + const sources = new Set(); + const map = { + getStyle: () => ({ layers: [...layers] }), + getLayersOrder: () => layers.map(({ id }) => id), + getLayer: (id: string) => layers.find((layer) => layer.id === id), + getSource: (id: string) => (sources.has(id) ? { id } : undefined), + addSource: (id: string) => { + sources.add(id); + }, + addLayer: (spec: { id: string; source?: string }, beforeId?: string) => { + const index = beforeId ? layers.findIndex((layer) => layer.id === beforeId) : -1; + if (index >= 0) layers.splice(index, 0, spec); + else layers.push(spec); + }, + removeLayer: (id: string) => { + const index = layers.findIndex((layer) => layer.id === id); + if (index >= 0) layers.splice(index, 1); + }, + removeSource: (id: string) => { + sources.delete(id); + }, + moveLayer: () => {}, + setLayoutProperty: () => {}, + setPaintProperty: () => {}, + setLayerZoomRange: () => {}, + }; + return { map, order: () => layers.map(({ id }) => id) }; +} + +function tileLayer(id: string): GeoLibreLayer { + return { + id, + name: id, + type: "xyz", + source: { + type: "raster", + tiles: [`https://tiles.example.com/${id}/{z}/{x}/{y}.png`], + tileSize: 256, + }, + visible: true, + opacity: 1, + style: { ...DEFAULT_LAYER_STYLE }, + metadata: {}, + }; +} + +describe("createLayerSync", () => { + it("stacks the first sync bottom-to-top in input order", () => { + const { map, order } = makeMapStub(); + const sync = createLayerSync(map as never); + + sync.sync([tileLayer("a"), tileLayer("b"), tileLayer("c")]); + + assert.deepEqual(order(), ["layer-a-raster", "layer-b-raster", "layer-c-raster"]); + }); + + it("restores the requested order when a layer moves", () => { + const { map, order } = makeMapStub(); + const sync = createLayerSync(map as never); + + sync.sync([tileLayer("a"), tileLayer("b"), tileLayer("c")]); + // "c" is moved to the bottom of the stack. + sync.sync([tileLayer("c"), tileLayer("a"), tileLayer("b")]); + + assert.deepEqual(order(), ["layer-c-raster", "layer-a-raster", "layer-b-raster"]); + }); + + it("puts a layer inserted in the middle below the layers above it", () => { + const { map, order } = makeMapStub(); + const sync = createLayerSync(map as never); + + sync.sync([tileLayer("a"), tileLayer("c")]); + sync.sync([tileLayer("a"), tileLayer("b"), tileLayer("c")]); + + assert.deepEqual(order(), ["layer-a-raster", "layer-b-raster", "layer-c-raster"]); + }); + + it("removes a layer dropped from the input", () => { + const { map, order } = makeMapStub(); + const sync = createLayerSync(map as never); + + sync.sync([tileLayer("a"), tileLayer("b")]); + sync.sync([tileLayer("b")]); + + assert.deepEqual(order(), ["layer-b-raster"]); + }); + + it("removes every layer it added on dispose", () => { + const { map, order } = makeMapStub(); + const sync = createLayerSync(map as never); + + sync.sync([tileLayer("a"), tileLayer("b")]); + sync.dispose(); + + assert.deepEqual(order(), []); + }); +}); diff --git a/tests/prepare-npm-package.test.ts b/tests/prepare-npm-package.test.ts new file mode 100644 index 000000000..a54b201b6 --- /dev/null +++ b/tests/prepare-npm-package.test.ts @@ -0,0 +1,116 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { describe, it } from "node:test"; +import { toPublishManifest } from "../scripts/prepare-npm-package.mjs"; + +const repoRoot = path.resolve(import.meta.dirname, ".."); + +function manifest(workspace: string): Record { + return JSON.parse(readFileSync(path.join(repoRoot, workspace, "package.json"), "utf8")); +} + +describe("toPublishManifest", () => { + it("hoists the entry fields npm ignores inside publishConfig", () => { + const published = toPublishManifest({ + name: "@scope/pkg", + version: "1.0.0", + main: "./src/index.ts", + types: "./src/index.ts", + exports: { ".": "./src/index.ts" }, + publishConfig: { + access: "public", + main: "./dist/index.mjs", + types: "./dist/index.d.mts", + exports: { ".": { types: "./dist/index.d.mts", import: "./dist/index.mjs" } }, + }, + }); + + assert.equal(published.main, "./dist/index.mjs"); + assert.equal(published.types, "./dist/index.d.mts"); + assert.deepEqual(published.exports, { + ".": { types: "./dist/index.d.mts", import: "./dist/index.mjs" }, + }); + // Only publish-time npm config is left behind. + assert.deepEqual(published.publishConfig, { access: "public" }); + }); + + it("pins a '*' workspace dependency to the version being published", () => { + const published = toPublishManifest( + { + name: "@scope/b", + version: "2.0.0", + dependencies: { "@scope/a": "*", "maplibre-gl": "^6.3.0" }, + }, + (name) => (name === "@scope/a" ? "2.0.0" : undefined), + ); + + assert.deepEqual(published.dependencies, { "@scope/a": "^2.0.0", "maplibre-gl": "^6.3.0" }); + }); + + it("leaves a '*' range alone when it names no workspace", () => { + const published = toPublishManifest({ + name: "@scope/b", + version: "2.0.0", + dependencies: { "some-package": "*" }, + }); + + assert.deepEqual(published.dependencies, { "some-package": "*" }); + }); + + it("publishes core and map from dist, with core pinned to the same version", () => { + const core = manifest("packages/core"); + const map = manifest("packages/map"); + const version = (name: string) => (name === core.name ? (core.version as string) : undefined); + + const publishedCore = toPublishManifest(core, version) as Record; + const publishedMap = toPublishManifest(map, version) as Record & { + dependencies: Record; + exports: Record; + }; + + assert.equal(publishedCore.main, "./dist/index.mjs"); + assert.equal(publishedMap.main, "./dist/headless.mjs"); + assert.equal(publishedMap.dependencies["@geolibre/core"], `^${core.version}`); + // Every subpath the repo exports stays reachable from the published package. + assert.deepEqual(Object.keys(publishedMap.exports), Object.keys(map.exports as object)); + }); + + // The build scripts are not part of `npm run ci`, so nothing else notices if + // a published entry names a file tsdown does not emit -- which would leave a + // consumer with no types (or no module at all) and no error anywhere here. + it("only publishes files the package's own tsdown entries produce", () => { + for (const workspace of ["packages/core", "packages/map"]) { + const pkg = manifest(workspace) as { + scripts: { build: string }; + publishConfig: Record; + }; + const entries = pkg.scripts.build + .split(/\s+/) + .filter((token) => token.startsWith("src/") && token.endsWith(".ts")) + .map((token) => path.basename(token, ".ts")); + assert.ok(entries.length > 0, `${workspace}: no tsdown entries found`); + // `tsdown --format esm --dts` writes dist/.mjs and .d.mts. + const emitted = new Set( + entries.flatMap((name) => [`./dist/${name}.mjs`, `./dist/${name}.d.mts`]), + ); + + const published = toPublishManifest(manifest(workspace)) as Record; + const targets = JSON.stringify([published.main, published.types, published.exports]).match( + /\.\/dist\/[^"]+/g, + ); + for (const target of targets ?? []) { + assert.ok(emitted.has(target), `${workspace}: ${target} is not emitted by the build`); + } + } + }); + + it("declares public access so the scoped packages are not published private", () => { + for (const workspace of ["packages/core", "packages/map"]) { + const published = toPublishManifest(manifest(workspace)) as { + publishConfig: { access?: string }; + }; + assert.equal(published.publishConfig.access, "public", workspace); + } + }); +});