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.
This commit is contained in:
tiennm99 committed 2026-08-30 13:35:27 +07:00
1 parent faa7cf987b
commit adeebc46e2
9 files changed
+59 -5

No files matched your search

+39 -3
View File
@@ -11,7 +11,7 @@
// Output lands in src/data/boundaries/<code>.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');
+1 -1
View File
@@ -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`);
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+18
View File
@@ -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,
};
File renamed without changes.
+1 -1
View File
@@ -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.