fix(data): keep node:fs out of the browser-built data modules (#83)

naturalEarthRegions.js and neighborhoodPolygons.js each carried an
isNode branch that dynamically imported node:fs to read their bundled JSON
packs, because a plain dynamic JSON import needs an import attribute in
Node. Vite only warns about externalizing node:fs for the browser, so both
warnings survived every production build and the runtime boundary rested on
an environment guard.

Give both loaders the import attribute instead. Vite bundles the JSON as a
module exactly as before (the emitted regions/marine/san-francisco chunks
are byte-identical), Node loads the same files under node:test, and the
isNode split disappears.

Add a source-boundary test so a node: import cannot silently return to a
browser-built module.

Closes #34
This commit is contained in:
Ethan Stoner authored and GitHub committed 2026-08-31 15:25:02 -05:00
1 parent ac927de71a
commit 6d83bb6008
4 files changed
+49 -27

No files matched your search

+5 -11
View File
@@ -113,19 +113,13 @@ function suffixVariants(norm) {
/** @type {Array|null} flat entry list for listRegions() */
let _entries = null;
const isNode = typeof process !== 'undefined' && !!process.versions?.node
&& typeof window === 'undefined';
async function loadPackFile(base) {
if (isNode) {
const { readFileSync } = await import(/* @vite-ignore */ 'node:fs');
const url = new URL(`./local_data/natural_earth/${base}.json`, import.meta.url);
return JSON.parse(readFileSync(url, 'utf8'));
}
// Vite bundles these JSON files as modules (same pattern as neighborhoodPolygons.js)
// Vite bundles these JSON files as modules; the import attribute is what Node
// needs to load the same files under node:test (same pattern as
// neighborhoodPolygons.js). One path, so no node: import reaches the browser.
const mod = base === 'regions'
? await import('./local_data/natural_earth/regions.json')
: await import('./local_data/natural_earth/marine.json');
? await import('./local_data/natural_earth/regions.json', { with: { type: 'json' } })
: await import('./local_data/natural_earth/marine.json', { with: { type: 'json' } });
return mod.default || mod;
}
+5 -16
View File
@@ -14,12 +14,9 @@ import { createRetryableLoader } from './retryableLoad.js';
// bbox = [west, south, east, north]; only load a city file when the point falls in its box.
const CITY_FILES = [
{ id: 'san-francisco', bbox: [-122.55, 37.70, -122.35, 37.84], loader: () => import('./local_data/neighborhoods/san-francisco.json') },
{ id: 'san-francisco', bbox: [-122.55, 37.70, -122.35, 37.84], loader: () => import('./local_data/neighborhoods/san-francisco.json', { with: { type: 'json' } }) },
];
const isNode = typeof process !== 'undefined' && !!process.versions?.node
&& typeof window === 'undefined';
/**
* city id → memoized loader. Failures are NOT memoized: a transient
* chunk-load error used to cache an empty array forever, silently demoting
@@ -94,18 +91,10 @@ function cityLoader(city) {
let loader = _cityLoaders.get(city.id);
if (!loader) {
loader = createRetryableLoader(async () => {
let fc;
if (isNode) {
// node:test path — plain dynamic JSON import needs an import attribute in Node,
// so read the file directly (same dual-path pattern as naturalEarthRegions.js).
const { readFileSync } = await import(/* @vite-ignore */ 'node:fs');
const url = new URL(`./local_data/neighborhoods/${city.id}.json`, import.meta.url);
fc = JSON.parse(readFileSync(url, 'utf8'));
} else {
// Vite bundles the JSON file as a module.
const mod = await city.loader();
fc = mod.default || mod;
}
// One path for both runtimes: Vite bundles the JSON as a module, and the
// import attribute is what Node needs to load the same file under node:test.
const mod = await city.loader();
const fc = mod.default || mod;
return Array.isArray(fc.features) ? fc.features : [];
});
_cityLoaders.set(city.id, loader);