Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
as a CORS-enabled asset on the demo's GitHub Pages site
(`/searoute-ts/marnet.json`).

### Changed
- The Eurostat network is no longer inlined into each build. It now ships once
as a single shared `dist/data/marnet.cjs` asset that both the CJS and ESM
builds load at runtime, roughly halving the published package size
(~329 KB → ~184 KB packed, ~2.5 MB → ~1.3 MB unpacked). No API change. (#10)

## 2.0.1 — 2026-07-02

### Fixed
Expand Down Expand Up @@ -128,9 +134,9 @@ see ~24% smaller numbers — they now match the geodesic length in nm correctly.
(Shanghai-Rotterdam, NY-Rotterdam, Yokohama-LA, etc.) within ±10%.

### Known follow-ups
- The marnet is inlined in the bundle (~1.1 MB per build, ~2.2 MB total
- ~~The marnet is inlined in the bundle (~1.1 MB per build, ~2.2 MB total
unpacked). Migrating to a single shared JSON asset is a non-breaking
follow-up and would roughly halve published size.
follow-up and would roughly halve published size.~~ Done in Unreleased (#10).
- Multi-resolution networks (Eurostat ships 5 km / 10 km / 20 km / 50 km /
100 km). Currently we only ship 100 km. Other resolutions could be loaded
via a separate import path.
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,8 +306,10 @@ or AIS-derived custom graphs.
marnet has been normalised so the Pacific is a connected graph, and all
distances use haversine internally.

**What's the bundle size?** 329 KB packed / 2.5 MB unpacked on npm. The
bundled marnet is the bulk (~1.1 MB JSON per build). Tree-shakeable.
**What's the bundle size?** ~184 KB packed / ~1.3 MB unpacked on npm. The
bundled marnet is the bulk (~1.1 MB JSON). It ships once as a shared
`dist/data/marnet.cjs` asset that both the CJS and ESM builds load at runtime,
rather than being inlined into each build. Tree-shakeable.

## Credits

Expand Down
12 changes: 12 additions & 0 deletions data/marnet.cjs

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions data/marnet.d.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { FeatureCollection, LineString } from 'geojson';

/** Optional native passage label carried on marnet edges. */
export type MarnetProperties = { pass?: string };

declare const marnetData: FeatureCollection<LineString, MarnetProperties>;
export default marnetData;
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
],
"scripts": {
"clean": "rm -rf dist build coverage",
"build": "npm run clean && npm run build:cjs && npm run build:esm && npm run build:types && node scripts/fixup-cjs.cjs",
"build": "npm run clean && npm run build:cjs && npm run build:esm && npm run build:types && node scripts/fixup-cjs.cjs && node scripts/copy-marnet.cjs dist",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:esm": "tsc -p tsconfig.esm.json",
"build:types": "tsc -p tsconfig.types.json",
Expand All @@ -62,7 +62,7 @@
"format:check": "prettier --check \"src/**/*.{ts,json}\"",
"test": "npm run build:test && ava",
"test:unit": "ava",
"build:test": "tsc -p tsconfig.test.json",
"build:test": "tsc -p tsconfig.test.json && node scripts/copy-marnet.cjs build",
"prepublishOnly": "npm run lint && npm run test"
},
"engines": {
Expand Down
38 changes: 13 additions & 25 deletions scripts/build-marnet.cjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
/* Generate src/lib/marnet.ts from the eurostat marnet_plus_100km GeoJSON.
* Embeds the data as a JSON string + JSON.parse() so the TypeScript compiler
* doesn't pretty-print a 1 MB object literal into a 2.8 MB JS module.
/* Generate data/marnet.cjs from the eurostat marnet_plus_100km GeoJSON.
*
* The network is emitted as a single CommonJS asset (JSON string + JSON.parse)
* that both the CJS and ESM builds resolve at runtime, instead of being inlined
* into each build's marnet module. Shipping it once (as dist/data/marnet.cjs)
* roughly halves the published package size.
*
* Run manually when refreshing the network:
* node scripts/build-marnet.cjs /path/to/marnet_plus_100km.geojson
Expand All @@ -9,7 +12,7 @@ const fs = require('fs');
const path = require('path');

const input = process.argv[2] || '/tmp/eurostat-marnet/marnet_plus_100km.geojson';
const outPath = path.resolve(__dirname, '..', 'src/lib/marnet.ts');
const outPath = path.resolve(__dirname, '..', 'data/marnet.cjs');

const src = fs.readFileSync(input, 'utf8');
const parsed = JSON.parse(src);
Expand All @@ -32,33 +35,18 @@ if (json.includes("'") || json.includes('\\')) {
process.exit(1);
}

const header = `import type { FeatureCollection, LineString } from "geojson";

/**
* Maritime network from Eurostat searoute v3.5 (marnet_plus_100km, 2025).
* 9,847 LineString segments at ~100 km resolution with explicit \`pass\`
* labels for Suez, Panama, Malacca, Gibraltar, Bab-el-Mandeb, Kiel,
* Corinth, Dover, Bering, Magellan, Northwest Passage, Northeast Passage.
const header = `/* Auto-generated by scripts/build-marnet.cjs. Do not edit by hand.
*
* Antimeridian normalization: vertices at lon === 180 are rewritten to
* lon === -180 so the eastern and western sides of the Pacific share a
* single graph vertex.
* Single shared copy of the Eurostat marnet_plus_100km network, loaded by
* both the CJS and ESM builds (see src/lib/marnet.ts). Shipping the network
* as one asset instead of inlining it into each build roughly halves the
* published package size.
*
* Source: https://github.com/eurostat/searoute (EUPL-1.2).
*
* Embedded as a JSON string so the TypeScript compiler does not
* pretty-print the 1 MB object literal (which would 2-3x the bundle size).
* The string is parsed once at module load.
*/
export type MarnetProperties = { pass?: string };

`;

// eslint-disable-next-line prefer-template
const body =
'const marnetData = JSON.parse(\n \'' +
json +
'\',\n) as FeatureCollection<LineString, MarnetProperties>;\n\nexport default marnetData;\n';
const body = 'module.exports = JSON.parse(\n \'' + json + '\',\n);\n';

fs.writeFileSync(outPath, header + body);
console.log('normalized', touched, 'antimeridian vertices');
Expand Down
31 changes: 31 additions & 0 deletions scripts/copy-marnet.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/* Copy the shared network asset into a build output directory.
*
* The CJS build (dist/cjs/lib), the ESM build (dist/esm/lib) and the test
* build (build/test/lib) all import `../../data/marnet.cjs`, which resolves to
* `<outRoot>/data/marnet.cjs`. Placing a single copy at that path lets both
* published builds share one asset (see issue #10) and lets the test build
* find it at runtime.
*
* Usage:
* node scripts/copy-marnet.cjs dist # -> dist/data/marnet.cjs (+ .d.cts)
* node scripts/copy-marnet.cjs build # -> build/data/marnet.cjs (+ .d.cts)
*/
const fs = require('fs');
const path = require('path');

const target = process.argv[2];
if (!target) {
console.error('usage: node scripts/copy-marnet.cjs <output-root>');
process.exit(1);
}

const root = path.resolve(__dirname, '..');
const srcDir = path.join(root, 'data');
const outDir = path.resolve(root, target, 'data');

fs.mkdirSync(outDir, { recursive: true });
for (const file of ['marnet.cjs', 'marnet.d.cts']) {
fs.copyFileSync(path.join(srcDir, file), path.join(outDir, file));
}

console.log('copied network asset to', path.relative(root, outDir));
14 changes: 14 additions & 0 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Feature, Point } from 'geojson';
import {
CANAL_MAX_DRAFT_M,
clearFinderCache,
DEFAULT_MARNET,
loadNetwork,
NoRouteError,
seaRoute,
Expand Down Expand Up @@ -454,6 +455,19 @@ test('seaRouteAlternatives filters near-duplicate-length routes', (t) => {
t.is(alts.length, 1);
});

// ── Bundled network asset ────────────────────────────────────────────────────

test('DEFAULT_MARNET loads the shared network asset', (t) => {
// The network no longer lives inlined in the compiled module; it is loaded at
// runtime from the single shared data/marnet.cjs asset. If that resolution
// ever breaks, DEFAULT_MARNET comes back empty (or the import throws).
t.is(DEFAULT_MARNET.type, 'FeatureCollection');
t.true(DEFAULT_MARNET.features.length > 9000);
t.true(DEFAULT_MARNET.features.every((f) => f.geometry.type === 'LineString'));
// The Eurostat network carries native `pass` labels on canal/strait edges.
t.true(DEFAULT_MARNET.features.some((f) => typeof f.properties?.pass === 'string'));
});

// ── Custom network ──────────────────────────────────────────────────────────

test('custom network option is honoured', (t) => {
Expand Down
16 changes: 9 additions & 7 deletions src/lib/marnet.ts

Large diffs are not rendered by default.

Loading