Fix: region loader finds the manifest in prod (dist/) — completes P1 region ownership (v0.20.0) #35
+51
-1
@@ -1,5 +1,8 @@
|
||||
import { afterAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { copyFileSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
allTerritoryKeys,
|
||||
expandCountriesToKeys,
|
||||
@@ -101,3 +104,50 @@ describe('regions (region-key loader, v0.19 #16 P1)', () => {
|
||||
|
||||
afterAll(() => resetRegions());
|
||||
});
|
||||
|
||||
describe('loadRegions resolves the manifest per environment (dev public/ vs prod dist/)', () => {
|
||||
const realRoot = root;
|
||||
// A root that mimics the prod Docker image: no public/, the region files live
|
||||
// in dist/ (where Vite copies public/* at build). This is the exact layout that
|
||||
// broke v0.19.0 in prod (the loader only looked in public/).
|
||||
const base = mkdtempSync(path.join(tmpdir(), 'history-regions-'));
|
||||
const prodRoot = path.join(base, 'prod');
|
||||
|
||||
beforeEach(() => {
|
||||
mkdirSync(path.join(prodRoot, 'dist'), { recursive: true });
|
||||
copyFileSync(
|
||||
path.join(realRoot, 'public', 'region-keys.json'),
|
||||
path.join(prodRoot, 'dist', 'region-keys.json'),
|
||||
);
|
||||
copyFileSync(
|
||||
path.join(realRoot, 'public', 'regions.geo.json'),
|
||||
path.join(prodRoot, 'dist', 'regions.geo.json'),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => resetRegions());
|
||||
|
||||
afterAll(() => {
|
||||
rmSync(base, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('loads from dist/ when public/ is absent (the prod container layout)', () => {
|
||||
expect(loadRegions(prodRoot)).toBe(true);
|
||||
expect(hasRegionData()).toBe(true);
|
||||
expect(validKeys().size).toBe(1796);
|
||||
expect(expandCountriesToKeys(['FRA'])).toHaveLength(42);
|
||||
});
|
||||
|
||||
it('still loads from public/ (the dev layout, the first candidate)', () => {
|
||||
expect(loadRegions(realRoot)).toBe(true);
|
||||
expect(validKeys().size).toBe(1796);
|
||||
});
|
||||
|
||||
it('returns false and no data when neither public/ nor dist/ holds the files', () => {
|
||||
const emptyRoot = path.join(base, 'empty');
|
||||
mkdirSync(emptyRoot, { recursive: true });
|
||||
expect(loadRegions(emptyRoot)).toBe(false);
|
||||
expect(hasRegionData()).toBe(false);
|
||||
expect(validKeys().size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
+23
-10
@@ -1,7 +1,9 @@
|
||||
// Region-key data access (server). Loads the deterministic region-key manifest
|
||||
// (public/region-keys.json: stable feature id -> region key) and the sub-national
|
||||
// geo (public/regions.geo.json: feature id -> { name, parent }), then joins them
|
||||
// into the lookups the region-level ownership model (v0.19, #16 P1) needs:
|
||||
// (region-keys.json: stable feature id -> region key) and the sub-national geo
|
||||
// (regions.geo.json: feature id -> { name, parent }) — from public/ in dev (Vite
|
||||
// serves it) or dist/ in prod (Vite copies public/* -> dist/ at build, and the
|
||||
// Docker image keeps only dist/) — and joins them into the lookups the
|
||||
// region-level ownership model (v0.19, #16 P1) needs:
|
||||
//
|
||||
// - expand a nation's ISO_A3 country list -> the region keys of all its sub-regions
|
||||
// - build the LLM "regions" reference table (key -> "name (parent ISO_A3)")
|
||||
@@ -35,18 +37,29 @@ interface RegionData {
|
||||
let data: RegionData | null = null;
|
||||
|
||||
/**
|
||||
* Load (or reload) the region-key manifest + geo from `root`. Idempotent.
|
||||
* Returns true when both files loaded cleanly. A missing/broken file clears the
|
||||
* data and returns false, so the server still boots and degrades gracefully
|
||||
* (expansion becomes a no-op, ownership falls back to the raw input keys).
|
||||
* Load (or reload) the region-key manifest + geo. Idempotent. The two files are
|
||||
* served to the client from `public/` in dev (Vite serves it directly) and from
|
||||
* `dist/` in prod (Vite copies `public/*` → `dist/` at build and the Docker image
|
||||
* keeps only `dist/`), so probe `public/` then `dist/` (plus the root as a last
|
||||
* resort) and use the first directory that holds both files. Returns true when
|
||||
* both loaded cleanly; a missing/broken pair clears the data and returns false
|
||||
* so the server still boots and degrades gracefully (expansion becomes a no-op,
|
||||
* ownership falls back to the raw input keys).
|
||||
*/
|
||||
export function loadRegions(root: string): boolean {
|
||||
const keyPath = path.join(root, 'public', 'region-keys.json');
|
||||
const geoPath = path.join(root, 'public', 'regions.geo.json');
|
||||
if (!existsSync(keyPath) || !existsSync(geoPath)) {
|
||||
const keyFile = 'region-keys.json';
|
||||
const geoFile = 'regions.geo.json';
|
||||
const base = [
|
||||
path.join(root, 'public'), // dev: Vite serves public/ at /
|
||||
path.join(root, 'dist'), // prod: Vite copies public/* -> dist/ (== clientDir)
|
||||
root, // last resort
|
||||
].find((d) => existsSync(path.join(d, keyFile)) && existsSync(path.join(d, geoFile)));
|
||||
if (!base) {
|
||||
data = null;
|
||||
return false;
|
||||
}
|
||||
const keyPath = path.join(base, keyFile);
|
||||
const geoPath = path.join(base, geoFile);
|
||||
try {
|
||||
const idToKey = JSON.parse(readFileSync(keyPath, 'utf8')) as Record<string, string>;
|
||||
const geo = JSON.parse(readFileSync(geoPath, 'utf8')) as {
|
||||
|
||||
Reference in New Issue
Block a user