From adeebc46e2a4142ceb5e0caec05625c3185065d8 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sun, 30 Aug 2026 13:35:27 +0700 Subject: [PATCH] refactor(data): store city boundaries as .json behind a generated barrel Bundlers do not treat .geojson as a module, so importing a boundary statically failed to resolve. The content is JSON either way, and the extension was the only thing standing in the way of the coverage page reading a city outline. The barrel matches the one the panorama indexes already use: generated from what is on disk, rewritten after each city, so a build that has only produced some of them still compiles. --- scripts/build-city-boundaries.mjs | 42 +++++++++++++++++-- scripts/build-pano-index.mjs | 2 +- src/data/boundaries/{dh.geojson => dh.json} | 0 src/data/boundaries/{dl.geojson => dl.json} | 0 src/data/boundaries/{dn.geojson => dn.json} | 0 src/data/boundaries/{hn.geojson => hn.json} | 0 src/data/boundaries/index.js | 18 ++++++++ .../boundaries/{tphcm.geojson => tphcm.json} | 0 tests/pano-index.test.js | 2 +- 9 files changed, 59 insertions(+), 5 deletions(-) rename src/data/boundaries/{dh.geojson => dh.json} (100%) rename src/data/boundaries/{dl.geojson => dl.json} (100%) rename src/data/boundaries/{dn.geojson => dn.json} (100%) rename src/data/boundaries/{hn.geojson => hn.json} (100%) create mode 100644 src/data/boundaries/index.js rename src/data/boundaries/{tphcm.geojson => tphcm.json} (100%) diff --git a/scripts/build-city-boundaries.mjs b/scripts/build-city-boundaries.mjs index b3bf090..e539a1f 100644 --- a/scripts/build-city-boundaries.mjs +++ b/scripts/build-city-boundaries.mjs @@ -11,7 +11,7 @@ // Output lands in src/data/boundaries/.geojson and is meant to be edited // by hand afterwards if a city needs trimming. Re-running overwrites it. -import { mkdirSync, writeFileSync } from 'node:fs'; +import { mkdirSync, readdirSync, writeFileSync } from 'node:fs'; import * as turf from '@turf/turf'; const NOMINATIM = 'https://nominatim.openstreetmap.org/search'; @@ -166,7 +166,7 @@ async function buildCity(code, config) { }; mkdirSync(OUT_DIR, { recursive: true }); - const path = `${OUT_DIR}/${code.toLowerCase()}.geojson`; + const path = `${OUT_DIR}/${code.toLowerCase()}.json`; writeFileSync(path, JSON.stringify(out, null, 1) + '\n'); console.log( ` -> ${path} ${out.properties.areaKm2} km2, ` + @@ -174,9 +174,45 @@ async function buildCity(code, config) { ); } +/** + * Rewrite the barrel module that imports every boundary that exists. + * + * Same reason as the panorama indexes: static imports let the data be bundled + * with the server build, but a city that has not been generated yet would then + * be a hard build error. Generating the list from disk keeps a partial build + * usable. + * @returns {string[]} City codes now in the barrel. + */ +function writeBarrel() { + const entries = readdirSync(OUT_DIR) + .filter((name) => name.endsWith('.json') && name !== 'index.js') + .sort() + .map((name) => ({ + code: name.replace(/\.json$/, '').toUpperCase(), + ident: name.replace(/\.json$/, ''), + file: name, + })); + + const body = + '// Generated by scripts/build-city-boundaries.mjs. Do not edit by hand.\n' + + '//\n' + + '// Lists only the boundaries that have actually been built, so a partial\n' + + '// build still compiles.\n\n' + + entries.map((e) => `import ${e.ident} from './${e.file}';`).join('\n') + + '\n\nexport const CITY_BOUNDARIES = {\n' + + entries.map((e) => ` ${e.code}: ${e.ident},`).join('\n') + + '\n};\n'; + + writeFileSync(`${OUT_DIR}/index.js`, body); + return entries.map((e) => e.code); +} + const only = process.argv.slice(2); for (const [code, config] of Object.entries(CITIES)) { if (only.length && !only.includes(code)) continue; await buildCity(code, config); + // Rewritten after each city so an interrupted run leaves a consistent barrel. + writeBarrel(); } -console.log('\ndone'); +console.log(`\nbarrel: ${writeBarrel().join(', ')}`); +console.log('done'); diff --git a/scripts/build-pano-index.mjs b/scripts/build-pano-index.mjs index 7c5c133..cb9c109 100644 --- a/scripts/build-pano-index.mjs +++ b/scripts/build-pano-index.mjs @@ -155,7 +155,7 @@ function thinToGrid(points) { } async function buildCity(code, token) { - const boundaryPath = `${BOUNDARY_DIR}/${code.toLowerCase()}.geojson`; + const boundaryPath = `${BOUNDARY_DIR}/${code.toLowerCase()}.json`; if (!existsSync(boundaryPath)) { throw new Error(`${code}: missing ${boundaryPath} — run build-city-boundaries.mjs first`); } diff --git a/src/data/boundaries/dh.geojson b/src/data/boundaries/dh.json similarity index 100% rename from src/data/boundaries/dh.geojson rename to src/data/boundaries/dh.json diff --git a/src/data/boundaries/dl.geojson b/src/data/boundaries/dl.json similarity index 100% rename from src/data/boundaries/dl.geojson rename to src/data/boundaries/dl.json diff --git a/src/data/boundaries/dn.geojson b/src/data/boundaries/dn.json similarity index 100% rename from src/data/boundaries/dn.geojson rename to src/data/boundaries/dn.json diff --git a/src/data/boundaries/hn.geojson b/src/data/boundaries/hn.json similarity index 100% rename from src/data/boundaries/hn.geojson rename to src/data/boundaries/hn.json diff --git a/src/data/boundaries/index.js b/src/data/boundaries/index.js new file mode 100644 index 0000000..214075f --- /dev/null +++ b/src/data/boundaries/index.js @@ -0,0 +1,18 @@ +// Generated by scripts/build-city-boundaries.mjs. Do not edit by hand. +// +// Lists only the boundaries that have actually been built, so a partial +// build still compiles. + +import dh from './dh.json'; +import dl from './dl.json'; +import dn from './dn.json'; +import hn from './hn.json'; +import tphcm from './tphcm.json'; + +export const CITY_BOUNDARIES = { + DH: dh, + DL: dl, + DN: dn, + HN: hn, + TPHCM: tphcm, +}; diff --git a/src/data/boundaries/tphcm.geojson b/src/data/boundaries/tphcm.json similarity index 100% rename from src/data/boundaries/tphcm.geojson rename to src/data/boundaries/tphcm.json diff --git a/tests/pano-index.test.js b/tests/pano-index.test.js index 814828d..877ddc7 100644 --- a/tests/pano-index.test.js +++ b/tests/pano-index.test.js @@ -43,7 +43,7 @@ describe('panorama index', () => { it.each(CODES)('%s panoramas sit inside the city boundary', (code) => { const boundary = JSON.parse( - readFileSync(`src/data/boundaries/${code.toLowerCase()}.geojson`, 'utf8') + readFileSync(`src/data/boundaries/${code.toLowerCase()}.json`, 'utf8') ); // Sample rather than test every point: booleanPointInPolygon against a // detailed outline is slow, and a clipping bug would not hide in a sample.