Merge pull request #227: scoped formatting and package boundaries

Preserve the contributor commits and adopt scoped formatting with infrastructure export checks.
This commit is contained in:
Sameh Khamis authored and GitHub committed 2026-09-11 16:17:21 -07:00
commit 092f28e301
20 files changed
+1251 -407

No files matched your search

+12
View File
@@ -42,6 +42,12 @@ jobs:
- name: Run setup policy checks
run: npm run doctor -- --json
- name: Check adopted formatting
run: npm run format:check
- name: Check package boundaries
run: npm run check:boundaries
- name: Run unit tests
run: npm test
@@ -70,6 +76,12 @@ jobs:
- name: Run the Pinokio install path
run: node scripts/pinokio-install.mjs
- name: Check adopted formatting
run: npm run format:check
- name: Check package boundaries
run: npm run check:boundaries
- name: Run focused onboarding tests
run: node --test src/setupDoctor.test.mjs src/pinokioEnvironment.test.mjs src/pinokioPreflight.test.mjs src/pinokioLauncherContract.test.mjs src/mapStartup.test.mjs src/hudSummaryResponse.test.mjs
+22
View File
@@ -0,0 +1,22 @@
dist
.gev-cache
.gev-logs
screenshots
qa-shots
output
coverage
3d-models
pinokio/ENVIRONMENT
pinokio/.installed
src/data/local_data/
*.geojson
*.geojsonl
# Local configuration, generated assets and browser evidence
.env
.env.*
!.env.example
node_modules
.vite
.vite-*
*.min.js
+6
View File
@@ -0,0 +1,6 @@
{
"singleQuote": true,
"tabWidth": 2,
"semi": true,
"endOfLine": "lf"
}
+7
View File
@@ -13,6 +13,13 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
## [Unreleased]
### Development
- Adopt Prettier tooling contributed by RohanDaCoder (#227), with an explicit
file scope, pinned formatter and Linux/Windows CI checks. Format the reusable
infrastructure modules and their consumer tests. Package boundary checks keep
those exports separate from app startup and local Node services.
### Fixed
- Separate optional Google server credentials for Places and Street View from
+16
View File
@@ -51,6 +51,22 @@ The highest-leverage places to jump in:
- Match the surrounding code — comment density, naming, and idiom.
- Prefer small, reviewable commits. Conventional-commit-style prefixes (`feat:`, `fix:`, `perf:`, `docs:`) are appreciated but not required.
## Formatting and reusable components
Run `npm run format` before submitting changes to adopted modules, then
`npm run format:check` and `npm run check:boundaries`. Formatting uses the
explicit file list in `scripts/format-scope.json`; add newly extracted modules
and tests there in a separate mechanical commit. Keep unadopted files consistent
with their surrounding style. CI checks all adopted files and package boundaries
on Linux and Windows.
Reusable package exports own their state and receive application operations
through explicit callbacks. They must not import the standalone bootstrap or
local Node services. Update `scripts/package-boundaries.json` and consumer tests
when adding an export or expanding a component. See
[formatting and component boundaries](docs/CODE-BOUNDARIES.md) for the current
ownership and adoption process.
## Pull requests
1. Branch off `main`.
+50
View File
@@ -0,0 +1,50 @@
# Formatting and component boundaries
Run `npm run format` to format the files in `scripts/format-scope.json` and
`npm run format:check` to check that same list without writing. CI checks the
entire adopted list on Linux and Windows. Prettier is pinned in the development
dependencies; use the installed version so local and CI output agree. The shared
configuration specifies two spaces, single quotes, semicolons and LF endings.
Add new reusable modules and their tests to the list as they are extracted.
Keep mechanical formatting in its own commit after behavior is stable. Existing
source-text regression assertions still apply; investigate failures and preserve
their behavioral coverage when a move or line wrap changes a tested shape.
Files outside the list retain their surrounding style until deliberately adopted.
Generated output, local configuration, browser evidence and bundled datasets are
excluded. The formatter validates every entry before writing any file.
## Current component ownership
| Surface | Owns | Receives from its caller |
| -------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------- |
| `gods-eye-view/infrastructure` | Datacenter/dam definitions and fresh layer construction | Context, overlay and render operations |
| `gods-eye-view/infrastructure/geojson` | Data loading, Cesium entities, selection handling and resource cleanup | A viewer and those same operations |
| `gods-eye-view/infrastructure/lod` | Pure visibility budgets and selection policy | Position/visibility records and camera measurements |
| `src/data/localGeojson.js` | Standalone compatibility wiring | The application's existing shared services |
| `src/main.js` and `vite.config.js` | Standalone startup and local Node services | Local configuration |
The three scoped package exports are browser source modules. Use their documented
exports instead of importing standalone startup or reaching into internal files.
The application owns the viewer, context store, overlay host and render scheduler;
layers use the supplied callbacks. See [the infrastructure contract](INFRASTRUCTURE-LAYERS.md).
`npm run check:boundaries` builds every declared package export, with app Vite
configuration disabled. `scripts/package-boundaries.json` lists each export's
component, owned modules and external runtime dependencies. A new export must be
classified. Imports outside the declared modules fail, including unused and
literal dynamic imports. Cesium stays external so the consuming application
supplies the same compatible instance as its viewer. Existing consumer tests
also check import-time inactivity and asset URLs under a non-root base.
These checks cover the declared exports, not every import in the application.
They check build-time imports, not arbitrary runtime-generated module URLs.
Keep runtime module discovery out of these exports. When extracting another
component, add its ownership and consumer tests together. Node services must use
separate entry points and their own checks when they become reusable; importing
them into a browser component is not supported. No Node service is exported yet.
This setup establishes boundaries for the existing infrastructure component.
Application construction, UI panels and individual source adapters remain future
extractions; they should become smaller modules with explicit lifecycle owners
as their callers migrate.
+9
View File
@@ -1,5 +1,14 @@
# God's Eye View Current State
## Scoped formatting and package checks
`npm run format` and `npm run format:check` operate on the explicit adopted-file
list. `npm run check:boundaries` checks the browser dependency graph of all
current package exports; infrastructure owns its three implementation modules
and takes Cesium from the consumer. CI runs both checks on Linux and Windows.
The standalone app, layer behavior and public export paths remain unchanged.
See [component ownership and adoption](CODE-BOUNDARIES.md).
## Google browser and server keys
Local Places nearby/text search and the CCTV Street View fallback prefer
+17
View File
@@ -17,6 +17,7 @@
"satellite.js": "^6.0.2"
},
"devDependencies": {
"prettier": "3.9.6",
"puppeteer": "^25.10.0",
"sharp": "^0.35.4",
"vite": "^6.0.0",
@@ -2220,6 +2221,22 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/prettier": {
"version": "3.9.6",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz",
"integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/protobufjs": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.8.0.tgz",
+6 -2
View File
@@ -2,7 +2,7 @@
"name": "gods-eye-view",
"private": true,
"version": "0.1.1",
"description": "A real-time intelligence console for planet Earth — photorealistic 3D globe, live aircraft/ships/satellites/earthquakes/CCTV, and hands-free voice control. Runs in a browser.",
"description": "A real-time intelligence console for planet Earth \u2014 photorealistic 3D globe, live aircraft/ships/satellites/earthquakes/CCTV, and hands-free voice control. Runs in a browser.",
"type": "module",
"license": "MIT",
"author": "Bilawal Sidhu",
@@ -33,6 +33,8 @@
],
"scripts": {
"doctor": "node scripts/setup-doctor.mjs",
"format": "node scripts/format.mjs --write",
"format:check": "node scripts/format.mjs --check",
"dev": "vite",
"dev:secure": "./scripts/dev-secure.sh",
"opensky:import": "./scripts/opensky-import-client.sh",
@@ -40,7 +42,8 @@
"preview": "vite preview",
"test": "node scripts/run-unit-tests.mjs",
"test:track": "node scripts/track-regression.mjs",
"qa:map-source-tray": "node scripts/qa-map-source-tray.mjs"
"qa:map-source-tray": "node scripts/qa-map-source-tray.mjs",
"check:boundaries": "node scripts/check-package-boundaries.mjs"
},
"dependencies": {
"@mapbox/vector-tile": "^3.0.0",
@@ -51,6 +54,7 @@
"satellite.js": "^6.0.2"
},
"devDependencies": {
"prettier": "3.9.6",
"puppeteer": "^25.10.0",
"sharp": "^0.35.4",
"vite": "^6.0.0",
+131
View File
@@ -0,0 +1,131 @@
import { readFile, realpath } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { build, normalizePath } from 'vite';
/** Build each browser export group and reject imports outside its declared ownership. */
export async function checkPackageBoundaries(root) {
root = await realpath(root);
const readJson = async (name) =>
JSON.parse(await readFile(path.join(root, name), 'utf8'));
const pkg = await readJson('package.json');
const groups = await readJson('scripts/package-boundaries.json');
const declaredExports = Object.keys(pkg.exports || {}).sort();
const classifiedExports = Object.values(groups)
.flatMap((group) => group.exports)
.sort();
if (
!declaredExports.length ||
JSON.stringify(declaredExports) !== JSON.stringify(classifiedExports)
) {
throw new Error(
'Every package export must belong to exactly one boundary group',
);
}
const reports = [];
for (const [name, group] of Object.entries(groups)) {
if (
!Array.isArray(group.modules) ||
!group.modules.length ||
!Array.isArray(group.external)
) {
throw new Error(`Invalid package boundary: ${name}`);
}
const allowed = new Set();
for (const module of group.modules) {
if (
typeof module !== 'string' ||
path.isAbsolute(module) ||
module.includes('\\') ||
module.split('/').includes('..')
) {
throw new Error(`Boundary modules must be repository paths: ${name}`);
}
const resolved = await realpath(path.join(root, module));
const relative = path.relative(root, resolved);
if (
relative === '..' ||
relative.startsWith(`..${path.sep}`) ||
path.isAbsolute(relative)
) {
throw new Error(`Boundary module escapes repository: ${name}`);
}
allowed.add(normalizePath(resolved));
}
for (const external of group.external) {
if (
!Object.hasOwn(pkg.dependencies || {}, external) &&
!Object.hasOwn(pkg.peerDependencies || {}, external)
) {
throw new Error(
`Boundary external must be a declared runtime dependency: ${external}`,
);
}
}
const input = group.exports.map((key) => {
const target = pkg.exports[key];
if (
typeof target !== 'string' ||
!target.startsWith('./') ||
!allowed.has(normalizePath(path.resolve(root, target)))
) {
throw new Error(`Export must point to an owned module: ${key}`);
}
return path.resolve(root, target);
});
const seen = new Set();
await build({
root,
configFile: false,
envFile: false,
publicDir: false,
logLevel: 'silent',
plugins: [
{
name: 'check-package-ownership',
moduleParsed(info) {
if (!allowed.has(info.id)) {
throw new Error(
`Package boundary ${name} imports an unowned module: ${path.relative(root, info.id)}`,
);
}
seen.add(info.id);
},
},
],
build: {
lib: { entry: input, formats: ['es'] },
write: false,
minify: false,
assetsInlineLimit: 0,
rollupOptions: {
input,
external: group.external,
// Unused imports must still obey ownership; tree shaking is not a boundary.
treeshake: false,
preserveEntrySignatures: 'strict',
},
},
});
reports.push({ name, exports: input.length, modules: seen.size });
}
return reports;
}
const invoked = process.argv[1]
? pathToFileURL(path.resolve(process.argv[1])).href
: '';
if (import.meta.url === invoked) {
try {
const reports = await checkPackageBoundaries(
fileURLToPath(new URL('../', import.meta.url)),
);
for (const report of reports)
console.log(
`Checked ${report.name}: ${report.exports} exports, ${report.modules} owned modules.`,
);
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
}
+15
View File
@@ -0,0 +1,15 @@
[
".prettierrc.json",
"package.json",
"scripts/format-scope.json",
"scripts/package-boundaries.json",
"scripts/format.mjs",
"scripts/check-package-boundaries.mjs",
"src/tooling/format.test.mjs",
"src/tooling/packageBoundaries.test.mjs",
"src/data/infrastructure.js",
"src/data/infrastructure.test.mjs",
"src/data/localGeojsonCore.js",
"src/data/localGeojsonLod.js",
"docs/CODE-BOUNDARIES.md"
]
+101
View File
@@ -0,0 +1,101 @@
import { readFile, writeFile, realpath, stat } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import * as prettier from 'prettier';
/** Check or format the explicitly adopted files, without scanning other source. */
export async function formatAdoptedFiles(root, mode) {
if (!['--check', '--write'].includes(mode)) {
throw new Error('Usage: node scripts/format.mjs --check|--write');
}
root = await realpath(root);
const scope = JSON.parse(
await readFile(path.join(root, 'scripts/format-scope.json'), 'utf8'),
);
if (
!Array.isArray(scope) ||
!scope.length ||
new Set(scope).size !== scope.length
) {
throw new Error(
'Formatting scope must be a nonempty list of unique file paths',
);
}
// Validate the whole scope before any write, including resolved symlink paths.
const files = [];
for (const name of scope) {
if (
typeof name !== 'string' ||
!name ||
name.includes('\\') ||
path.isAbsolute(name) ||
name.split('/').includes('..')
) {
throw new Error(
'Formatting scope entries must be paths inside the repository',
);
}
const file = path.join(root, name);
const relative = path.relative(root, await realpath(file));
if (
relative === '..' ||
relative.startsWith(`..${path.sep}`) ||
path.isAbsolute(relative) ||
!(await stat(file)).isFile()
) {
throw new Error(
`Formatting scope must contain repository files: ${name}`,
);
}
const info = await prettier.getFileInfo(file, {
ignorePath: path.join(root, '.prettierignore'),
});
if (info.ignored || !info.inferredParser) {
throw new Error(
`Formatting scope contains an ignored or unsupported file: ${name}`,
);
}
const options = { ...(await prettier.resolveConfig(file)), filepath: file };
const source = await readFile(file, 'utf8');
files.push({
name,
file,
source,
formatted: await prettier.format(source, options),
});
}
const changed = files.filter(({ source, formatted }) => source !== formatted);
if (mode === '--write') {
for (const { file, formatted } of changed) await writeFile(file, formatted);
}
return { count: files.length, changed: changed.map(({ name }) => name) };
}
const invoked = process.argv[1]
? pathToFileURL(path.resolve(process.argv[1])).href
: '';
if (import.meta.url === invoked) {
try {
if (process.argv.length !== 3)
throw new Error('Usage: node scripts/format.mjs --check|--write');
const mode = process.argv[2];
const result = await formatAdoptedFiles(
fileURLToPath(new URL('../', import.meta.url)),
mode,
);
if (mode === '--check' && result.changed.length) {
for (const name of result.changed)
console.error(`Needs formatting: ${name}`);
process.exitCode = 1;
} else {
console.log(
`${mode === '--write' ? 'Formatted' : 'Checked'} ${result.count} adopted files.`,
);
}
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"infrastructure": {
"exports": [
"./infrastructure",
"./infrastructure/geojson",
"./infrastructure/lod"
],
"modules": [
"src/data/infrastructure.js",
"src/data/localGeojsonCore.js",
"src/data/localGeojsonLod.js"
],
"external": ["cesium"]
}
}
+34 -24
View File
@@ -1,8 +1,12 @@
import { createLocalGeoJsonLayer } from './localGeojsonCore.js';
// Resolved by Vite in builds and relative to this module in other consumers.
const datacentersUrl = new URL('./local_data/datacenters/datacenters.geojsonl', import.meta.url).href;
const damsUrl = new URL('./local_data/dams/dams.geojsonl', import.meta.url).href;
const datacentersUrl = new URL(
'./local_data/datacenters/datacenters.geojsonl',
import.meta.url,
).href;
const damsUrl = new URL('./local_data/dams/dams.geojsonl', import.meta.url)
.href;
/**
* Create fresh datacenter and dam layers without starting or loading them.
@@ -10,29 +14,35 @@ const damsUrl = new URL('./local_data/dams/dams.geojsonl', import.meta.url).href
* @returns {object[]} Datacenters then dams, with stable standalone identities.
*/
export function createInfrastructureLayers(services) {
const datacenters = createLocalGeoJsonLayer({
id: 'local-datacenters',
url: datacentersUrl,
name: 'Datacenters',
color: '#00ffff', // Cyan
icon: '▣',
source: 'Local',
labels: true,
labelMax: 700,
labelGridPx: 138,
}, services);
const datacenters = createLocalGeoJsonLayer(
{
id: 'local-datacenters',
url: datacentersUrl,
name: 'Datacenters',
color: '#00ffff', // Cyan
icon: '▣',
source: 'Local',
labels: true,
labelMax: 700,
labelGridPx: 138,
},
services,
);
const dams = createLocalGeoJsonLayer({
id: 'local-dams',
url: damsUrl,
name: 'Dams',
color: '#0088ff', // Blue
icon: '▰',
source: 'USACE',
labels: true,
labelMax: 900,
labelGridPx: 132,
}, services);
const dams = createLocalGeoJsonLayer(
{
id: 'local-dams',
url: damsUrl,
name: 'Dams',
color: '#0088ff', // Blue
icon: '▰',
source: 'USACE',
labels: true,
labelMax: 900,
labelGridPx: 132,
},
services,
);
return [datacenters, dams];
}
+111 -36
View File
@@ -12,11 +12,18 @@ function services() {
return {
records,
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
registerEntityContext(entity, metadata) { records.set(metadata.id, { entity, ...metadata }); },
selectEntityContext(entity) { selection = entity; },
clearSelectedEntityContextForLayer() { selection = undefined; },
registerEntityContext(entity, metadata) {
records.set(metadata.id, { entity, ...metadata });
},
selectEntityContext(entity) {
selection = entity;
},
clearSelectedEntityContextForLayer() {
selection = undefined;
},
removeEntityContextsForLayer(id) {
for (const [key, record] of records) if (record.layerId === id) records.delete(key);
for (const [key, record] of records)
if (record.layerId === id) records.delete(key);
},
governorRequestRender() {},
selection: () => selection,
@@ -24,7 +31,12 @@ function services() {
}
test('package exports import without an application, DOM, fetch, or timers', () => {
const result = spawnSync(process.execPath, ['--input-type=module', '-e', `
const result = spawnSync(
process.execPath,
[
'--input-type=module',
'-e',
`
globalThis.fetch = () => { throw new Error('unexpected fetch'); };
globalThis.setTimeout = () => { throw new Error('unexpected timer'); };
for (const key of ['window', 'document']) {
@@ -33,51 +45,99 @@ test('package exports import without an application, DOM, fetch, or timers', ()
await import('gods-eye-view/infrastructure');
await import('gods-eye-view/infrastructure/geojson');
await import('gods-eye-view/infrastructure/lod');
`], { cwd: new URL('../..', import.meta.url), encoding: 'utf8' });
`,
],
{ cwd: new URL('../..', import.meta.url), encoding: 'utf8' },
);
assert.equal(result.status, 0, result.stderr);
});
test('infrastructure factory preserves identity and creates independent state without loading', t => {
t.mock.method(globalThis, 'fetch', () => { throw new Error('factory must not fetch'); });
test('infrastructure factory preserves identity and creates independent state without loading', (t) => {
t.mock.method(globalThis, 'fetch', () => {
throw new Error('factory must not fetch');
});
const first = createInfrastructureLayers(services());
const second = createInfrastructureLayers(services());
assert.deepEqual(first.map(({ id, name, source }) => ({ id, name, source })), [
{ id: 'local-datacenters', name: 'Datacenters', source: 'Local' },
{ id: 'local-dams', name: 'Dams', source: 'USACE' },
]);
assert.deepEqual(
first.map(({ id, name, source }) => ({ id, name, source })),
[
{ id: 'local-datacenters', name: 'Datacenters', source: 'Local' },
{ id: 'local-dams', name: 'Dams', source: 'USACE' },
],
);
first.forEach((layer, index) => {
assert.notEqual(layer, second[index]);
layer.destroy();
assert.deepEqual(second[index].getStats(), { count: 0, lastUpdate: null, error: null });
assert.deepEqual(second[index].getStats(), {
count: 0,
lastUpdate: null,
error: null,
});
second[index].destroy();
});
});
test('dataset URLs still name the complete bundled sources', () => {
for (const [file, count] of [['datacenters', 4351], ['dams', 704]]) {
const lines = readFileSync(new URL(`./local_data/${file}/${file}.geojsonl`, import.meta.url), 'utf8')
.split('\n').filter(line => line.trim());
for (const [file, count] of [
['datacenters', 4351],
['dams', 704],
]) {
const lines = readFileSync(
new URL(`./local_data/${file}/${file}.geojsonl`, import.meta.url),
'utf8',
)
.split('\n')
.filter((line) => line.trim());
assert.equal(lines.length, count);
}
});
test('two viewers use their supplied contexts and dispose independently', async t => {
t.mock.method(globalThis, 'fetch', async () => ({ ok: true, text: async () => JSON.stringify({
type: 'Feature', id: 'same-id', properties: { name: 'Dam' },
geometry: { type: 'Polygon', coordinates: [[[0, 0], [0.01, 0], [0, 0.01], [0, 0]]] },
}) }));
test('two viewers use their supplied contexts and dispose independently', async (t) => {
t.mock.method(globalThis, 'fetch', async () => ({
ok: true,
text: async () =>
JSON.stringify({
type: 'Feature',
id: 'same-id',
properties: { name: 'Dam' },
geometry: {
type: 'Polygon',
coordinates: [
[
[0, 0],
[0.01, 0],
[0, 0.01],
[0, 0],
],
],
},
}),
}));
const hosts = [services(), services()];
const instances = hosts.map(host => {
const instances = hosts.map((host) => {
const sources = new Cesium.DataSourceCollection();
let click;
const layer = createLocalGeoJsonLayer({
id: 'local-dams', name: 'Dams', color: '#0088ff', url: '/dams.geojsonl',
screenSpaceEventHandlerFactory: () => ({ setInputAction(fn) { click = fn; }, destroy() {} }),
}, host);
const layer = createLocalGeoJsonLayer(
{
id: 'local-dams',
name: 'Dams',
color: '#0088ff',
url: '/dams.geojsonl',
screenSpaceEventHandlerFactory: () => ({
setInputAction(fn) {
click = fn;
},
destroy() {},
}),
},
host,
);
const viewer = {
dataSources: sources,
scene: {
canvas: {}, preRender: new Cesium.Event(), requestRender() {},
canvas: {},
preRender: new Cesium.Event(),
requestRender() {},
screenSpaceCameraController: { enableInputs: true },
pick: () => ({ id: sources.get(0).entities.values[0] }),
},
@@ -85,7 +145,9 @@ test('two viewers use their supplied contexts and dispose independently', async
};
return { layer, viewer, click: () => click({ position: {} }) };
});
t.after(() => instances.forEach(({ layer, viewer }) => layer.destroy(viewer)));
t.after(() =>
instances.forEach(({ layer, viewer }) => layer.destroy(viewer)),
);
await Promise.all(instances.map(({ layer, viewer }) => layer.enable(viewer)));
assert.equal(hosts[0].records.size, 1);
assert.equal(hosts[1].records.size, 1);
@@ -109,22 +171,35 @@ test('consumer build includes only infrastructure code and resolves assets under
write: false,
assetsInlineLimit: 0,
rollupOptions: {
input: fileURLToPath(import.meta.resolve('gods-eye-view/infrastructure')),
input: fileURLToPath(
import.meta.resolve('gods-eye-view/infrastructure'),
),
external: ['cesium'],
preserveEntrySignatures: 'strict',
},
},
});
const output = result.output;
const entry = output.find(item => item.type === 'chunk' && item.isEntry);
const sources = Object.keys(entry.modules).filter(id => id.endsWith('.js'));
assert.deepEqual(sources.map(id => id.split('/').at(-1)).sort(), [
'infrastructure.js', 'localGeojsonCore.js', 'localGeojsonLod.js',
const entry = output.find((item) => item.type === 'chunk' && item.isEntry);
const sources = Object.keys(entry.modules).filter((id) => id.endsWith('.js'));
assert.deepEqual(sources.map((id) => id.split('/').at(-1)).sort(), [
'infrastructure.js',
'localGeojsonCore.js',
'localGeojsonLod.js',
]);
assert.deepEqual(entry.imports, ['cesium'], 'the viewer supplies the same Cesium dependency');
assert.deepEqual(
entry.imports,
['cesium'],
'the viewer supplies the same Cesium dependency',
);
for (const name of ['datacenters', 'dams']) {
const asset = output.find(item => item.type === 'asset' && item.fileName.includes(name));
const asset = output.find(
(item) => item.type === 'asset' && item.fileName.includes(name),
);
assert.ok(asset, `${name} must be emitted`);
assert.ok(entry.code.includes(`/example/${asset.fileName}`), `${name} must retain the consumer base path`);
assert.ok(
entry.code.includes(`/example/${asset.fileName}`),
`${name} must retain the consumer base path`,
);
}
});
+1 -1
View File
@@ -494,7 +494,7 @@ test('local infrastructure creates no native labels or per-frame geometry callba
assert.doesNotMatch(source, /new Cesium\.CallbackProperty/);
assert.match(source, /feature\.position = tip/);
assert.match(source, /record\.entity\.position\.setValue\(record\.tip\)/);
assert.match(source, /const stemPositionBuffers = \[\[base, tip\], \[base, tip\]\]/);
assert.match(source, /const stemPositionBuffers = \[\s*\[base, tip\],\s*\[base, tip\],?\s*\]/);
assert.match(source, /record\.entity\.polyline\.positions\.setValue\(stemPositions\)/);
assert.match(source, /viewer\.camera\.moveEnd\.addEventListener/);
assert.match(source, /if \(refreshStemGeometry \|\| terrainFloorChanged\)/);
+434 -299
View File
@@ -15,7 +15,8 @@ const LOCAL_OVERLAY_COLLISION_CAPACITY = 96;
const LOCAL_OVERLAY_CELL_SURPLUS = 2;
const LOCAL_OVERLAY_MAX_DISTANCE_M = 14000000;
const LOCAL_OVERLAY_FADE_START_M = 250000;
const LOCAL_OVERLAY_FADE_START_RATIO = LOCAL_OVERLAY_FADE_START_M / LOCAL_OVERLAY_MAX_DISTANCE_M;
const LOCAL_OVERLAY_FADE_START_RATIO =
LOCAL_OVERLAY_FADE_START_M / LOCAL_OVERLAY_MAX_DISTANCE_M;
// Stems are anchored at ellipsoid height 0, but high-elevation features
// (e.g. dams in river canyons) sit hundreds of meters above the ellipsoid,
// burying the short close-in stem inside the photoreal mesh. Once the
@@ -63,8 +64,12 @@ export function localInfrastructureOverlayCopy(properties, layerId) {
props.capacity,
]);
const line = [operator, capacity]
.filter((value, index, values) => value && values.indexOf(value) === index)
.filter((value) => value.toLocaleLowerCase() !== title.toLocaleLowerCase())
.filter(
(value, index, values) => value && values.indexOf(value) === index,
)
.filter(
(value) => value.toLocaleLowerCase() !== title.toLocaleLowerCase(),
)
.join(' · ');
if (line) details.push(clampCardLine(line));
} else if (layerId === 'local-dams') {
@@ -147,21 +152,29 @@ export function createLocalInfrastructureOverlayEntry({
* @param {number} [options.cohortLimit=Infinity] Host-safe materialization cap.
* @returns {object[]} Bounded overlay entries for shared-host arbitration.
*/
export function selectLocalInfrastructureOverlayCohort(records, {
maxEntries,
gridPx,
width,
height,
project,
cohortLimit = Number.POSITIVE_INFINITY,
}) {
export function selectLocalInfrastructureOverlayCohort(
records,
{
maxEntries,
gridPx,
width,
height,
project,
cohortLimit = Number.POSITIVE_INFINITY,
},
) {
const sourceCap = Math.max(0, Math.floor(Number(maxEntries) || 0));
const materializationCap = Number.isFinite(Number(cohortLimit))
? Math.max(0, Math.floor(Number(cohortLimit)))
: Number.POSITIVE_INFINITY;
const cap = Math.min(sourceCap, materializationCap);
const cellSize = Math.max(1, Number(gridPx) || 1);
if (!Array.isArray(records) || records.length === 0 || cap === 0 || typeof project !== 'function') {
if (
!Array.isArray(records) ||
records.length === 0 ||
cap === 0 ||
typeof project !== 'function'
) {
return [];
}
@@ -170,8 +183,13 @@ export function selectLocalInfrastructureOverlayCohort(records, {
for (const record of records) {
const screen = project(record);
if (!Number.isFinite(screen?.x) || !Number.isFinite(screen?.y)) continue;
if (screen.x < -padding || screen.x > width + padding
|| screen.y < -padding || screen.y > height + padding) continue;
if (
screen.x < -padding ||
screen.x > width + padding ||
screen.y < -padding ||
screen.y > height + padding
)
continue;
const key = `${Math.floor(screen.x / cellSize)}:${Math.floor(screen.y / cellSize)}`;
let contenders = cells.get(key);
if (!contenders) {
@@ -189,7 +207,8 @@ export function selectLocalInfrastructureOverlayCohort(records, {
}
primary.sort(compareLocalOverlayRecords);
surplus.sort(compareLocalOverlayRecords);
if (primary.length >= cap) return primary.slice(0, cap).map((record) => record.entry);
if (primary.length >= cap)
return primary.slice(0, cap).map((record) => record.entry);
const candidates = primary.concat(surplus.slice(0, cap - primary.length));
return candidates.map((record) => record.entry);
}
@@ -201,10 +220,7 @@ export function selectLocalInfrastructureOverlayCohort(records, {
* @param {object} [options.host] Test seam for the three host lifecycle calls.
* @returns {{show:function():void,publish:function(object[]):void,hide:function():void,destroy:function():void}}
*/
export function createLocalInfrastructureOverlayPublisher({
sourceId,
host,
}) {
export function createLocalInfrastructureOverlayPublisher({ sourceId, host }) {
let visible = false;
let published = false;
let destroyed = false;
@@ -227,17 +243,26 @@ export function createLocalInfrastructureOverlayPublisher({
},
publish(entries) {
if (destroyed || !visible) return;
if (lastPublication && entries.length === lastPublication.length
&& entries.every((entry, index) => {
if (
lastPublication &&
entries.length === lastPublication.length &&
entries.every((entry, index) => {
const previous = lastPublication[index];
return entry === previous.entry
&& entry.position?.x === previous.x
&& entry.position?.y === previous.y
&& entry.position?.z === previous.z;
})) return;
return (
entry === previous.entry &&
entry.position?.x === previous.x &&
entry.position?.y === previous.y &&
entry.position?.z === previous.z
);
})
)
return;
host.setEntries(sourceId, entries, sourceOptions);
lastPublication = entries.map(entry => ({
entry, x: entry.position?.x, y: entry.position?.y, z: entry.position?.z,
lastPublication = entries.map((entry) => ({
entry,
x: entry.position?.x,
y: entry.position?.y,
z: entry.position?.z,
}));
published = entries.length > 0;
},
@@ -288,26 +313,31 @@ export function localDatasetError(error) {
* One live instance per layer id is allowed in a given viewer/context/overlay host.
* Destroy the previous instance before replacing it. Importing creates no layers.
*/
export function createLocalGeoJsonLayer({
id,
url,
name,
color,
icon = '📍',
source = 'Local JSONL',
labels = true,
labelMax = DEFAULT_LABEL_MAX,
labelGridPx = DEFAULT_LABEL_GRID_PX,
screenSpaceEventHandlerFactory = (canvas) => new Cesium.ScreenSpaceEventHandler(canvas),
projectToWindow = (scene, position) => Cesium.SceneTransforms.worldToWindowCoordinates(scene, position),
}, {
overlayHost,
registerEntityContext,
selectEntityContext,
clearSelectedEntityContextForLayer,
removeEntityContextsForLayer,
governorRequestRender,
}) {
export function createLocalGeoJsonLayer(
{
id,
url,
name,
color,
icon = '📍',
source = 'Local JSONL',
labels = true,
labelMax = DEFAULT_LABEL_MAX,
labelGridPx = DEFAULT_LABEL_GRID_PX,
screenSpaceEventHandlerFactory = (canvas) =>
new Cesium.ScreenSpaceEventHandler(canvas),
projectToWindow = (scene, position) =>
Cesium.SceneTransforms.worldToWindowCoordinates(scene, position),
},
{
overlayHost,
registerEntityContext,
selectEntityContext,
clearSelectedEntityContextForLayer,
removeEntityContextsForLayer,
governorRequestRender,
},
) {
let _dataSource = null;
let _enabled = false;
let _clickHandler = null;
@@ -483,231 +513,279 @@ export function createLocalGeoJsonLayer({
// 1. Initialize data source
if (!_dataSource) {
if (!_loadPromise) _loadPromise = (async () => {
_loadController = new AbortController();
const baseColor = Cesium.Color.fromCssColorString(color);
if (!_loadPromise)
_loadPromise = (async () => {
_loadController = new AbortController();
const baseColor = Cesium.Color.fromCssColorString(color);
// Fetch and parse JSON Lines (.geojsonl) into a FeatureCollection.
// The source is built into a local and committed to `_dataSource`
// only once setup finishes: a half-built source published early would
// make every later enable() skip this block, so the layer could never
// clear its error or retry.
_error = null;
let loaded = null;
// Whether the scene has actually accepted `loaded` — the two rollback
// windows (before vs after the add settles) need different cleanup.
let addedToScene = false;
try {
const response = await fetch(url, { signal: _loadController.signal });
if (_destroyed) return;
// A 404 returns an HTML body that would otherwise die in JSON.parse
// one line later, reported as a parse error for a missing file.
if (!response.ok) {
throw new Error(`HTTP ${response.status ?? '?'}`);
}
const text = await response.text();
if (_destroyed) return;
const lines = text.split('\n').filter(l => l.trim().length > 0);
// Fetch and parse JSON Lines (.geojsonl) into a FeatureCollection.
// The source is built into a local and committed to `_dataSource`
// only once setup finishes: a half-built source published early would
// make every later enable() skip this block, so the layer could never
// clear its error or retry.
_error = null;
let loaded = null;
// Whether the scene has actually accepted `loaded` — the two rollback
// windows (before vs after the add settles) need different cleanup.
let addedToScene = false;
try {
const response = await fetch(url, {
signal: _loadController.signal,
});
if (_destroyed) return;
// A 404 returns an HTML body that would otherwise die in JSON.parse
// one line later, reported as a parse error for a missing file.
if (!response.ok) {
throw new Error(`HTTP ${response.status ?? '?'}`);
}
const text = await response.text();
if (_destroyed) return;
const lines = text.split('\n').filter((l) => l.trim().length > 0);
const features = lines.map(line => JSON.parse(line));
const features = lines.map((line) => JSON.parse(line));
const geojson = {
type: 'FeatureCollection',
features
};
const geojson = {
type: 'FeatureCollection',
features,
};
// Natively parse into entities and use it as our _dataSource
loaded = await Cesium.GeoJsonDataSource.load(geojson, {
clampToGround: true,
stroke: baseColor,
fill: baseColor.withAlpha(0.3),
strokeWidth: 2,
markerSize: 8,
markerColor: baseColor,
});
// Natively parse into entities and use it as our _dataSource
loaded = await Cesium.GeoJsonDataSource.load(geojson, {
clampToGround: true,
stroke: baseColor,
fill: baseColor.withAlpha(0.3),
strokeWidth: 2,
markerSize: 8,
markerColor: baseColor,
});
if (_destroyed) return;
loaded.name = name;
loaded.show = false;
// Cesium's DataSourceCollection.add() returns a promise and only
// inserts on a later microtask. Without this await, a throw during
// post-processing would roll back a source the scene had not
// accepted yet — and Cesium would then insert the "removed" source
// anyway, leaving an orphan the retry would double up on. Awaiting
// also routes an add() rejection into the error path below instead
// of leaving it uncaught with healthy-looking stats.
await viewer.dataSources.add(loaded);
addedToScene = true;
if (_destroyed) {
viewer.dataSources.remove(loaded, true);
return;
}
// Convert parsed points into 3D stems or style polygons
const entities = loaded.entities.values;
_count = entities.length;
_stemRecords = [];
_stemGeometryDirty = true;
for (let i = 0; i < entities.length; i++) {
const feature = entities[i];
feature.__localLayerId = id; // Tag it so our click handler knows it belongs to this layer
let pos = feature.position?.getValue(Cesium.JulianDate.now());
if (!pos) {
// It's a polygon or line
if (feature.polygon) {
feature.polygon.outline = true;
feature.polygon.outlineColor = baseColor;
// Calculate center point for the stem
const hierarchy = feature.polygon.hierarchy?.getValue(Cesium.JulianDate.now());
if (hierarchy && hierarchy.positions && hierarchy.positions.length > 0) {
pos = Cesium.BoundingSphere.fromPoints(hierarchy.positions).center;
}
}
if (_destroyed) return;
loaded.name = name;
loaded.show = false;
// Cesium's DataSourceCollection.add() returns a promise and only
// inserts on a later microtask. Without this await, a throw during
// post-processing would roll back a source the scene had not
// accepted yet — and Cesium would then insert the "removed" source
// anyway, leaving an orphan the retry would double up on. Awaiting
// also routes an add() rejection into the error path below instead
// of leaving it uncaught with healthy-looking stats.
await viewer.dataSources.add(loaded);
addedToScene = true;
if (_destroyed) {
viewer.dataSources.remove(loaded, true);
return;
}
if (!pos) continue;
// Convert parsed points into 3D stems or style polygons
const entities = loaded.entities.values;
_count = entities.length;
_stemRecords = [];
_stemGeometryDirty = true;
const carto = Cesium.Cartographic.fromCartesian(pos);
const groundHeight = 0; // Ellipsoid surface until a scene sample lands
const tipHeight = 2000; // Initial Stem height
for (let i = 0; i < entities.length; i++) {
const feature = entities[i];
feature.__localLayerId = id; // Tag it so our click handler knows it belongs to this layer
const base = Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, groundHeight);
const tip = Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, tipHeight);
const properties = propertyObject(feature);
const recordId = String(feature.id ?? i);
let pos = feature.position?.getValue(Cesium.JulianDate.now());
// Store references for bounded stem scaling and native picking.
feature.__localBaseCarto = carto;
feature.__localBaseCartesian = base;
registerEntityContext(feature, {
id: `${id}:${recordId}`,
layerId: id,
layerName: name,
source,
dataSource: loaded,
label: featureLabelFromProperties(properties, id),
properties,
latitude: Number(Cesium.Math.toDegrees(carto.latitude).toFixed(6)),
longitude: Number(Cesium.Math.toDegrees(carto.longitude).toFixed(6)),
});
if (!pos) {
// It's a polygon or line
if (feature.polygon) {
feature.polygon.outline = true;
feature.polygon.outlineColor = baseColor;
// Constant properties are refreshed on the existing 450 ms source
// cadence. Cesium no longer evaluates 2-3 callbacks per entity on
// every frame, while the point/stem pick surface stays native.
feature.position = tip;
const stemPositionBuffers = [[base, tip], [base, tip]];
feature.polyline = new Cesium.PolylineGraphics({
positions: stemPositionBuffers[0],
width: 3.5,
material: new Cesium.ColorMaterialProperty(baseColor),
});
feature.point = new Cesium.PointGraphics({
pixelSize: 10,
color: baseColor,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 2,
// Never depth-cull the anchor against the photoreal mesh —
// globe-horizon culling is handled by the pre-render occluder.
disableDepthTestDistance: Number.POSITIVE_INFINITY,
});
// Calculate center point for the stem
const hierarchy = feature.polygon.hierarchy?.getValue(
Cesium.JulianDate.now(),
);
if (
hierarchy &&
hierarchy.positions &&
hierarchy.positions.length > 0
) {
pos = Cesium.BoundingSphere.fromPoints(
hierarchy.positions,
).center;
}
}
}
const priority = labelPriorityFromProperties(properties, id);
_stemRecords.push({
id: recordId,
entity: feature,
carto,
base,
tip,
nextTip: Cesium.Cartesian3.clone(tip),
stemPositionBuffers,
stemPositionBufferIndex: 0,
groundHeight,
groundSampled: false,
lastGroundSampleMs: 0,
priority,
entry: labels ? createLocalInfrastructureOverlayEntry({
id: recordId,
if (!pos) continue;
const carto = Cesium.Cartographic.fromCartesian(pos);
const groundHeight = 0; // Ellipsoid surface until a scene sample lands
const tipHeight = 2000; // Initial Stem height
const base = Cesium.Cartesian3.fromRadians(
carto.longitude,
carto.latitude,
groundHeight,
);
const tip = Cesium.Cartesian3.fromRadians(
carto.longitude,
carto.latitude,
tipHeight,
);
const properties = propertyObject(feature);
const recordId = String(feature.id ?? i);
// Store references for bounded stem scaling and native picking.
feature.__localBaseCarto = carto;
feature.__localBaseCartesian = base;
registerEntityContext(feature, {
id: `${id}:${recordId}`,
layerId: id,
position: tip,
layerName: name,
source,
dataSource: loaded,
label: featureLabelFromProperties(properties, id),
properties,
latitude: Number(
Cesium.Math.toDegrees(carto.latitude).toFixed(6),
),
longitude: Number(
Cesium.Math.toDegrees(carto.longitude).toFixed(6),
),
});
// Constant properties are refreshed on the existing 450 ms source
// cadence. Cesium no longer evaluates 2-3 callbacks per entity on
// every frame, while the point/stem pick surface stays native.
feature.position = tip;
const stemPositionBuffers = [
[base, tip],
[base, tip],
];
feature.polyline = new Cesium.PolylineGraphics({
positions: stemPositionBuffers[0],
width: 3.5,
material: new Cesium.ColorMaterialProperty(baseColor),
});
feature.point = new Cesium.PointGraphics({
pixelSize: 10,
color: baseColor,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 2,
// Never depth-cull the anchor against the photoreal mesh —
// globe-horizon culling is handled by the pre-render occluder.
disableDepthTestDistance: Number.POSITIVE_INFINITY,
});
const priority = labelPriorityFromProperties(properties, id);
_stemRecords.push({
id: recordId,
entity: feature,
carto,
base,
tip,
nextTip: Cesium.Cartesian3.clone(tip),
stemPositionBuffers,
stemPositionBufferIndex: 0,
groundHeight,
groundSampled: false,
lastGroundSampleMs: 0,
priority,
accent: color,
}) : null,
});
}
// Setup finished — publish it.
_dataSource = loaded;
_lastUpdate = Date.now();
} catch (e) {
// The dataset ships with the build, so this is a broken install,
// not a blip — it has to reach the chip, not just the console.
if (!_destroyed) _error = localDatasetError(e);
// Roll the partial build back so a later enable() retries from
// scratch instead of inheriting a half-populated source. Only the
// post-add window has something in the scene to remove: a failure
// before (or inside) add() never reached the collection, and
// removing then would race Cesium's pending insert.
if (addedToScene) {
try { viewer?.dataSources?.remove(loaded, true); } catch { /* already gone */ }
}
if (_destroyed) return;
removeEntityContextsForLayer(id);
_count = 0;
_stemRecords = [];
console.error(`Failed to load ${id}:`, e);
}
if (_destroyed) return;
// 2. Install native global click handler
if (!_clickHandler) {
_clickHandler = screenSpaceEventHandlerFactory(viewer.scene.canvas);
_clickHandler.setInputAction((click) => {
if (!_enabled) return;
const picked = viewer.scene.pick(click.position);
if (picked && picked.id && picked.id.__localLayerId === id) {
const entity = picked.id;
viewer.selectedEntity = entity;
selectEntityContext(entity);
// We zoom to the surface base of the stem or the center of the polygon
let targetPos = null;
if (entity.polyline) {
// If it's a stem, fly to the base
const positions = entity.polyline.positions.getValue(Cesium.JulianDate.now());
if (positions && positions.length > 0) {
targetPos = positions[0];
}
} else if (entity.polygon && entity.polygon.hierarchy) {
// If it's a polygon, just fly to its center
const hierarchy = entity.polygon.hierarchy.getValue(Cesium.JulianDate.now());
if (hierarchy && hierarchy.positions.length > 0) {
targetPos = Cesium.BoundingSphere.fromPoints(hierarchy.positions).center;
}
}
if (targetPos) {
const carto = Cesium.Cartographic.fromCartesian(targetPos);
// Disable interactions so Cesium doesn't magically cancel the flight
viewer.scene.screenSpaceCameraController.enableInputs = false;
viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, 5000),
duration: 1.5,
complete: () => { viewer.scene.screenSpaceCameraController.enableInputs = true; },
cancel: () => { viewer.scene.screenSpaceCameraController.enableInputs = true; },
});
entry: labels
? createLocalInfrastructureOverlayEntry({
id: recordId,
layerId: id,
position: tip,
properties,
priority,
accent: color,
})
: null,
});
}
// Setup finished — publish it.
_dataSource = loaded;
_lastUpdate = Date.now();
} catch (e) {
// The dataset ships with the build, so this is a broken install,
// not a blip — it has to reach the chip, not just the console.
if (!_destroyed) _error = localDatasetError(e);
// Roll the partial build back so a later enable() retries from
// scratch instead of inheriting a half-populated source. Only the
// post-add window has something in the scene to remove: a failure
// before (or inside) add() never reached the collection, and
// removing then would race Cesium's pending insert.
if (addedToScene) {
try {
viewer?.dataSources?.remove(loaded, true);
} catch {
/* already gone */
}
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
}
})();
if (_destroyed) return;
removeEntityContextsForLayer(id);
_count = 0;
_stemRecords = [];
console.error(`Failed to load ${id}:`, e);
}
if (_destroyed) return;
// 2. Install native global click handler
if (!_clickHandler) {
_clickHandler = screenSpaceEventHandlerFactory(
viewer.scene.canvas,
);
_clickHandler.setInputAction((click) => {
if (!_enabled) return;
const picked = viewer.scene.pick(click.position);
if (picked && picked.id && picked.id.__localLayerId === id) {
const entity = picked.id;
viewer.selectedEntity = entity;
selectEntityContext(entity);
// We zoom to the surface base of the stem or the center of the polygon
let targetPos = null;
if (entity.polyline) {
// If it's a stem, fly to the base
const positions = entity.polyline.positions.getValue(
Cesium.JulianDate.now(),
);
if (positions && positions.length > 0) {
targetPos = positions[0];
}
} else if (entity.polygon && entity.polygon.hierarchy) {
// If it's a polygon, just fly to its center
const hierarchy = entity.polygon.hierarchy.getValue(
Cesium.JulianDate.now(),
);
if (hierarchy && hierarchy.positions.length > 0) {
targetPos = Cesium.BoundingSphere.fromPoints(
hierarchy.positions,
).center;
}
}
if (targetPos) {
const carto = Cesium.Cartographic.fromCartesian(targetPos);
// Disable interactions so Cesium doesn't magically cancel the flight
viewer.scene.screenSpaceCameraController.enableInputs = false;
viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromRadians(
carto.longitude,
carto.latitude,
5000,
),
duration: 1.5,
complete: () => {
viewer.scene.screenSpaceCameraController.enableInputs = true;
},
cancel: () => {
viewer.scene.screenSpaceCameraController.enableInputs = true;
},
});
}
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
}
})();
try {
await _loadPromise;
} finally {
@@ -741,7 +819,10 @@ export function createLocalGeoJsonLayer({
const probe = shouldRecomputeInfraLod({
nowMs: now,
lastProbeMs: _lastLodProbeMs,
movedSqM: Cesium.Cartesian3.distanceSquared(cameraPos, _lastLodCameraPos),
movedSqM: Cesium.Cartesian3.distanceSquared(
cameraPos,
_lastLodCameraPos,
),
cameraHeightM: viewer.camera.positionCartographic?.height,
});
_lastLodProbeMs = probe.lastProbeMs;
@@ -750,7 +831,10 @@ export function createLocalGeoJsonLayer({
if (probe.recompute) _stemGeometryDirty = true;
}
const occluder = new Cesium.EllipsoidalOccluder(Cesium.Ellipsoid.WGS84, cameraPos);
const occluder = new Cesium.EllipsoidalOccluder(
Cesium.Ellipsoid.WGS84,
cameraPos,
);
const visibleOverlayRecords = [];
const refreshStemGeometry = _stemGeometryDirty;
@@ -762,7 +846,8 @@ export function createLocalGeoJsonLayer({
// Capability can arrive late (WebGL context restore, a tileset that
// finally supports sampling). A parked camera has no moveEnd to
// re-open a spent budget, so the false→true edge does it.
if (canSampleGround && _lastGroundSampleCapability === false) _groundRetryArms = 0;
if (canSampleGround && _lastGroundSampleCapability === false)
_groundRetryArms = 0;
_lastGroundSampleCapability = canSampleGround;
let groundRetryPending = false;
let groundSampleProgress = false;
@@ -814,7 +899,8 @@ export function createLocalGeoJsonLayer({
const record = _stemRecords[i];
const isActive = !_lodComputed || _activeLodIds.has(record.id);
const isVisible = isActive && occluder.isPointVisible(record.base);
if (record.entity.show !== isVisible) record.entity.show = isVisible;
if (record.entity.show !== isVisible)
record.entity.show = isVisible;
// Records outside the globe-LOD active set carry no visible stem,
// so skip every per-frame cost for them — geometry trig, the
@@ -823,20 +909,32 @@ export function createLocalGeoJsonLayer({
if (!isActive) continue;
const wasGroundSampled = record.groundSampled;
const terrainFloorChanged = refreshLocalTerrainFloor(viewer, record);
const terrainFloorChanged = refreshLocalTerrainFloor(
viewer,
record,
);
if (refreshStemGeometry || terrainFloorChanged) {
updateLocalStemGeometry(viewer, record, now);
} else if (canSampleGround && !record.groundSampled
&& now - record.lastGroundSampleMs >= GROUND_SAMPLE_RETRY_MS) {
} else if (
canSampleGround &&
!record.groundSampled &&
now - record.lastGroundSampleMs >= GROUND_SAMPLE_RETRY_MS
) {
// Capability first: without it the distance below is pure waste,
// once per ungrounded record per walk, forever.
const distance = Cesium.Cartesian3.distance(viewer.camera.positionWC, record.base);
if (distance < GROUND_SAMPLE_MAX_DISTANCE_M
&& sampleLocalGroundHeight(viewer, record, now)) {
const distance = Cesium.Cartesian3.distance(
viewer.camera.positionWC,
record.base,
);
if (
distance < GROUND_SAMPLE_MAX_DISTANCE_M &&
sampleLocalGroundHeight(viewer, record, now)
) {
updateLocalStemGeometry(viewer, record, now, distance);
}
}
if (!wasGroundSampled && record.groundSampled) groundSampleProgress = true;
if (!wasGroundSampled && record.groundSampled)
groundSampleProgress = true;
// Still unsampled AND close enough for a retry to succeed: this
// layer has no hold and no periodic update, so under the idle
// governor the retry's preRender never arrives on a parked camera
@@ -845,9 +943,15 @@ export function createLocalGeoJsonLayer({
// Gated on a sampleable scene and in-range records only, so a far
// camera (or a keyless scene) stays fully idle; the distance is
// only computed for still-unsampled stems.
if (canSampleGround && !record.groundSampled && !groundRetryPending
&& Cesium.Cartesian3.distance(viewer.camera.positionWC, record.base)
< GROUND_SAMPLE_MAX_DISTANCE_M) {
if (
canSampleGround &&
!record.groundSampled &&
!groundRetryPending &&
Cesium.Cartesian3.distance(
viewer.camera.positionWC,
record.base,
) < GROUND_SAMPLE_MAX_DISTANCE_M
) {
groundRetryPending = true;
}
if (isVisible && record.entry) visibleOverlayRecords.push(record);
@@ -859,14 +963,17 @@ export function createLocalGeoJsonLayer({
if (groundRetryPending) scheduleGroundRetryRender(viewer);
const canvas = viewer.scene.canvas;
const cohort = selectLocalInfrastructureOverlayCohort(visibleOverlayRecords, {
maxEntries: labelMax,
gridPx: labelGridPx,
width: canvas.clientWidth || canvas.width || 0,
height: canvas.clientHeight || canvas.height || 0,
cohortLimit: LOCAL_OVERLAY_COHORT_LIMIT,
project: (record) => projectToWindow(viewer.scene, record.tip),
});
const cohort = selectLocalInfrastructureOverlayCohort(
visibleOverlayRecords,
{
maxEntries: labelMax,
gridPx: labelGridPx,
width: canvas.clientWidth || canvas.width || 0,
height: canvas.clientHeight || canvas.height || 0,
cohortLimit: LOCAL_OVERLAY_COHORT_LIMIT,
project: (record) => projectToWindow(viewer.scene, record.tip),
},
);
_overlayPublisher.publish(cohort);
});
}
@@ -913,7 +1020,7 @@ export function createLocalGeoJsonLayer({
_count = 0;
_lastUpdate = null;
_error = null;
}
},
};
}
@@ -923,24 +1030,36 @@ function compareLocalOverlayRecords(a, b) {
function insertLocalCellContender(contenders, record) {
let index = 0;
while (index < contenders.length && compareLocalOverlayRecords(contenders[index], record) <= 0) {
while (
index < contenders.length &&
compareLocalOverlayRecords(contenders[index], record) <= 0
) {
index++;
}
contenders.splice(index, 0, record);
if (contenders.length > LOCAL_OVERLAY_CELL_SURPLUS) contenders.length = LOCAL_OVERLAY_CELL_SURPLUS;
if (contenders.length > LOCAL_OVERLAY_CELL_SURPLUS)
contenders.length = LOCAL_OVERLAY_CELL_SURPLUS;
}
function refreshLocalTerrainFloor(viewer, record) {
const globe = viewer.scene.globe;
if (!record.groundSampled || !globe?.show || globe.tilesLoaded === false) return false;
if (Cesium.Cartesian3.distance(viewer.camera.positionWC, record.base)
>= GROUND_SAMPLE_MAX_DISTANCE_M) return false;
if (!record.groundSampled || !globe?.show || globe.tilesLoaded === false)
return false;
if (
Cesium.Cartesian3.distance(viewer.camera.positionWC, record.base) >=
GROUND_SAMPLE_MAX_DISTANCE_M
)
return false;
// Camera motion can refine terrain after the first successful depth sample.
// Reuse the loaded mesh on existing bounded walks; never arm a timer or do
// another GPU readback just to maintain this floor. Keep roof elevations.
const height = globe.getHeight?.(record.carto);
if (!Number.isFinite(height) || Math.abs(height) > GROUND_SAMPLE_MAX_ABS_HEIGHT_M
|| height <= record.groundHeight) return false;
if (
!Number.isFinite(height) ||
Math.abs(height) > GROUND_SAMPLE_MAX_ABS_HEIGHT_M ||
height <= record.groundHeight
)
return false;
setLocalGroundHeight(record, height);
return true;
}
@@ -957,13 +1076,22 @@ function sampleLocalGroundHeight(viewer, record, now) {
} catch {
return false; // tiles not ready; retry on a later bounded update
}
if (!Number.isFinite(sampled) || Math.abs(sampled) > GROUND_SAMPLE_MAX_ABS_HEIGHT_M) return false;
if (
!Number.isFinite(sampled) ||
Math.abs(sampled) > GROUND_SAMPLE_MAX_ABS_HEIGHT_M
)
return false;
// A coarse scene-depth sample can lie below the terrain already loaded by
// the visible globe. Keep that terrain as a floor while allowing roofs and
// valid below-sea-level elevations. Photoreal scenes hide the globe, so its
// inactive terrain must not constrain their geometry.
const terrainHeight = globe?.show ? globe.getHeight?.(record.carto) : undefined;
if (Number.isFinite(terrainHeight) && Math.abs(terrainHeight) <= GROUND_SAMPLE_MAX_ABS_HEIGHT_M) {
const terrainHeight = globe?.show
? globe.getHeight?.(record.carto)
: undefined;
if (
Number.isFinite(terrainHeight) &&
Math.abs(terrainHeight) <= GROUND_SAMPLE_MAX_ABS_HEIGHT_M
) {
sampled = Math.max(sampled, terrainHeight);
}
record.groundSampled = true;
@@ -987,12 +1115,13 @@ function updateLocalStemGeometry(viewer, record, now, knownDistance = null) {
const distance = Number.isFinite(knownDistance)
? knownDistance
: Cesium.Cartesian3.distance(viewer.camera.positionWC, record.base);
if (distance < GROUND_SAMPLE_MAX_DISTANCE_M) sampleLocalGroundHeight(viewer, record, now);
if (distance < GROUND_SAMPLE_MAX_DISTANCE_M)
sampleLocalGroundHeight(viewer, record, now);
// Keep the intended screen-size scaling in close-up views too. A 5 km
// minimum made a marker hundreds of metres tall beside a nearby building.
const effectiveDistance = Math.max(distance, 1);
const canvasHeight = viewer.scene.canvas.clientHeight || 1080;
const fov = viewer.camera.frustum.fov || (Math.PI / 3);
const fov = viewer.camera.frustum.fov || Math.PI / 3;
const targetPx = 65;
const fovFactor = 2 * Math.tan(fov / 2) * (targetPx / canvasHeight);
const tipHeight = record.groundHeight + effectiveDistance * fovFactor;
@@ -1003,12 +1132,16 @@ function updateLocalStemGeometry(viewer, record, now, knownDistance = null) {
Cesium.Ellipsoid.WGS84,
record.nextTip,
);
if (Cesium.Cartesian3.distanceSquared(record.tip, record.nextTip) <= LOCAL_STEM_TIP_EPSILON_SQ) {
if (
Cesium.Cartesian3.distanceSquared(record.tip, record.nextTip) <=
LOCAL_STEM_TIP_EPSILON_SQ
) {
return false;
}
Cesium.Cartesian3.clone(record.nextTip, record.tip);
record.stemPositionBufferIndex = 1 - record.stemPositionBufferIndex;
const stemPositions = record.stemPositionBuffers[record.stemPositionBufferIndex];
const stemPositions =
record.stemPositionBuffers[record.stemPositionBufferIndex];
stemPositions[0] = record.base;
stemPositions[1] = record.tip;
record.entity.position.setValue(record.tip);
@@ -1050,9 +1183,10 @@ function labelPriorityFromProperties(props, layerId) {
function propertyObject(entity) {
const source = entity?.properties;
const raw = typeof source?.getValue === 'function'
? source.getValue(Cesium.JulianDate.now())
: source || {};
const raw =
typeof source?.getValue === 'function'
? source.getValue(Cesium.JulianDate.now())
: source || {};
return unwrapProperties(raw);
}
@@ -1061,9 +1195,10 @@ function unwrapProperties(value) {
if (Array.isArray(value)) return value.map(unwrapProperties);
const out = {};
for (const [key, entry] of Object.entries(value)) {
out[key] = entry && typeof entry.getValue === 'function'
? unwrapProperties(entry.getValue(Cesium.JulianDate.now()))
: unwrapProperties(entry);
out[key] =
entry && typeof entry.getValue === 'function'
? unwrapProperties(entry.getValue(Cesium.JulianDate.now()))
: unwrapProperties(entry);
}
return out;
}
+92 -45
View File
@@ -58,8 +58,10 @@ export function infraLodBudget(cameraHeightM) {
const height = Number.isFinite(cameraHeightM)
? Math.max(0, cameraHeightM)
: INFRA_LOD_GLOBAL_HEIGHT_M;
if (height >= INFRA_LOD_GLOBAL_HEIGHT_M) return { activeLimit: INFRA_LOD_ACTIVE_MIN };
if (height >= INFRA_LOD_REGIONAL_HEIGHT_M) return { activeLimit: INFRA_LOD_ACTIVE_MID };
if (height >= INFRA_LOD_GLOBAL_HEIGHT_M)
return { activeLimit: INFRA_LOD_ACTIVE_MIN };
if (height >= INFRA_LOD_REGIONAL_HEIGHT_M)
return { activeLimit: INFRA_LOD_ACTIVE_MID };
return { activeLimit: INFRA_LOD_ACTIVE_MAX };
}
@@ -104,24 +106,33 @@ export const INFRA_LOD_MAX_DISTANCE_PENALTY = 200;
* @param {number} [options.maxDistancePenalty]
* @returns {number}
*/
export function infraRankScore(priority, distanceM, isIncumbent, {
incumbentBonus = INFRA_LOD_INCUMBENT_BONUS,
farM = INFRA_LOD_FAR_M,
maxDistancePenalty = INFRA_LOD_MAX_DISTANCE_PENALTY,
} = {}) {
export function infraRankScore(
priority,
distanceM,
isIncumbent,
{
incumbentBonus = INFRA_LOD_INCUMBENT_BONUS,
farM = INFRA_LOD_FAR_M,
maxDistancePenalty = INFRA_LOD_MAX_DISTANCE_PENALTY,
} = {},
) {
const p = Number.isFinite(priority) ? priority : 0;
const far = Number.isFinite(farM) && farM > 0 ? farM : INFRA_LOD_FAR_M;
const d = Number.isFinite(distanceM) && distanceM >= 0 ? distanceM : far;
const penaltyCap = Number.isFinite(maxDistancePenalty) && maxDistancePenalty >= 0
? maxDistancePenalty
: INFRA_LOD_MAX_DISTANCE_PENALTY;
const bonus = isIncumbent && Number.isFinite(incumbentBonus) ? incumbentBonus : 0;
const penaltyCap =
Number.isFinite(maxDistancePenalty) && maxDistancePenalty >= 0
? maxDistancePenalty
: INFRA_LOD_MAX_DISTANCE_PENALTY;
const bonus =
isIncumbent && Number.isFinite(incumbentBonus) ? incumbentBonus : 0;
return p + bonus - penaltyCap * Math.min(1, d / far);
}
/** Infinity-safe distance for a total sort order. */
function sortableDistance(distanceM) {
return Number.isFinite(distanceM) && distanceM >= 0 ? distanceM : Number.MAX_VALUE;
return Number.isFinite(distanceM) && distanceM >= 0
? distanceM
: Number.MAX_VALUE;
}
/**
@@ -142,35 +153,49 @@ function sortableDistance(distanceM) {
* @param {Iterable<string>|Set<string>} [options.incumbentIds] Ids that currently hold an active stem.
* @returns {{activeIds:string[], budget:{activeLimit:number}}}
*/
export function selectInfraLod(candidates, { cameraHeightM, incumbentIds } = {}) {
export function selectInfraLod(
candidates,
{ cameraHeightM, incumbentIds } = {},
) {
const budget = infraLodBudget(cameraHeightM);
const incumbents = incumbentIds instanceof Set ? incumbentIds : new Set(incumbentIds || []);
const incumbents =
incumbentIds instanceof Set ? incumbentIds : new Set(incumbentIds || []);
const byId = new Map();
for (const candidate of Array.isArray(candidates) ? candidates : []) {
if (!candidate || typeof candidate.id !== 'string' || !candidate.id) continue;
if (!candidate || typeof candidate.id !== 'string' || !candidate.id)
continue;
if (candidate.inView !== true) continue;
const distanceM = Number.isFinite(candidate.distanceM) && candidate.distanceM >= 0
? candidate.distanceM
: Number.POSITIVE_INFINITY;
const distanceM =
Number.isFinite(candidate.distanceM) && candidate.distanceM >= 0
? candidate.distanceM
: Number.POSITIVE_INFINITY;
const normalized = {
id: candidate.id,
distanceM,
score: infraRankScore(candidate.priority, candidate.distanceM, incumbents.has(candidate.id)),
score: infraRankScore(
candidate.priority,
candidate.distanceM,
incumbents.has(candidate.id),
),
};
const current = byId.get(normalized.id);
if (!current
|| normalized.score > current.score
|| (normalized.score === current.score && normalized.distanceM < current.distanceM)) {
if (
!current ||
normalized.score > current.score ||
(normalized.score === current.score &&
normalized.distanceM < current.distanceM)
) {
byId.set(normalized.id, normalized);
}
}
const ranked = [...byId.values()].sort((a, b) => (
b.score - a.score
|| sortableDistance(a.distanceM) - sortableDistance(b.distanceM)
|| a.id.localeCompare(b.id)
));
const ranked = [...byId.values()].sort(
(a, b) =>
b.score - a.score ||
sortableDistance(a.distanceM) - sortableDistance(b.distanceM) ||
a.id.localeCompare(b.id),
);
return {
activeIds: ranked.slice(0, budget.activeLimit).map((entry) => entry.id),
@@ -220,7 +245,9 @@ export function applyInfraEvictionGrace({
graceMs = INFRA_LOD_GRACE_MS,
} = {}) {
const selectedSet = new Set(
(Array.isArray(selectedIds) ? selectedIds : []).filter((id) => typeof id === 'string' && id),
(Array.isArray(selectedIds) ? selectedIds : []).filter(
(id) => typeof id === 'string' && id,
),
);
const keepIds = [...selectedSet];
const evictIds = [];
@@ -241,8 +268,13 @@ export function applyInfraEvictionGrace({
// Under cap pressure, grace-period stems go first: oldest-in-grace first
// (longest chance to return), then more misses, then id for determinism.
graced.sort((a, b) => a.since - b.since || b.misses - a.misses || a.id.localeCompare(b.id));
const cap = Number.isFinite(activeLimit) ? Math.max(0, Math.floor(activeLimit)) : INFRA_LOD_ACTIVE_MAX;
graced.sort(
(a, b) =>
a.since - b.since || b.misses - a.misses || a.id.localeCompare(b.id),
);
const cap = Number.isFinite(activeLimit)
? Math.max(0, Math.floor(activeLimit))
: INFRA_LOD_ACTIVE_MAX;
const capacity = Math.max(0, cap - keepIds.length);
const overflow = Math.max(0, graced.length - capacity);
for (let i = 0; i < graced.length; i++) {
@@ -251,7 +283,10 @@ export function applyInfraEvictionGrace({
continue;
}
keepIds.push(graced[i].id);
nextGrace.set(graced[i].id, { misses: graced[i].misses, since: graced[i].since });
nextGrace.set(graced[i].id, {
misses: graced[i].misses,
since: graced[i].since,
});
}
return { keepIds, evictIds, graceState: nextGrace };
@@ -307,13 +342,21 @@ export const INFRA_LOD_MOTION_EPSILON_MIN_M = 250;
* @param {number} [options.minM]
* @returns {number}
*/
export function infraLodMotionEpsilonM(cameraHeightM, {
ratio = INFRA_LOD_MOTION_EPSILON_RATIO,
minM = INFRA_LOD_MOTION_EPSILON_MIN_M,
} = {}) {
const floor = Number.isFinite(minM) && minM >= 0 ? minM : INFRA_LOD_MOTION_EPSILON_MIN_M;
const scale = Number.isFinite(ratio) && ratio >= 0 ? ratio : INFRA_LOD_MOTION_EPSILON_RATIO;
const height = Number.isFinite(cameraHeightM) && cameraHeightM > 0 ? cameraHeightM : 0;
export function infraLodMotionEpsilonM(
cameraHeightM,
{
ratio = INFRA_LOD_MOTION_EPSILON_RATIO,
minM = INFRA_LOD_MOTION_EPSILON_MIN_M,
} = {},
) {
const floor =
Number.isFinite(minM) && minM >= 0 ? minM : INFRA_LOD_MOTION_EPSILON_MIN_M;
const scale =
Number.isFinite(ratio) && ratio >= 0
? ratio
: INFRA_LOD_MOTION_EPSILON_RATIO;
const height =
Number.isFinite(cameraHeightM) && cameraHeightM > 0 ? cameraHeightM : 0;
return Math.max(floor, height * scale);
}
@@ -355,15 +398,19 @@ export function shouldRecomputeInfraLod({
motionEpsilonM,
} = {}) {
const now = Number.isFinite(nowMs) ? nowMs : 0;
const last = Number.isFinite(lastProbeMs) ? lastProbeMs : Number.NEGATIVE_INFINITY;
const interval = Number.isFinite(probeIntervalMs) && probeIntervalMs >= 0
? probeIntervalMs
: INFRA_LOD_MOTION_PROBE_INTERVAL_MS;
const last = Number.isFinite(lastProbeMs)
? lastProbeMs
: Number.NEGATIVE_INFINITY;
const interval =
Number.isFinite(probeIntervalMs) && probeIntervalMs >= 0
? probeIntervalMs
: INFRA_LOD_MOTION_PROBE_INTERVAL_MS;
if (now - last < interval) return { recompute: false, lastProbeMs: last };
const movedSq = Number.isFinite(movedSqM) && movedSqM > 0 ? movedSqM : 0;
const epsilon = Number.isFinite(motionEpsilonM) && motionEpsilonM >= 0
? motionEpsilonM
: infraLodMotionEpsilonM(cameraHeightM);
const epsilon =
Number.isFinite(motionEpsilonM) && motionEpsilonM >= 0
? motionEpsilonM
: infraLodMotionEpsilonM(cameraHeightM);
return { recompute: movedSq > epsilon * epsilon, lastProbeMs: now };
}
+95
View File
@@ -0,0 +1,95 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
mkdtemp,
mkdir,
writeFile,
readFile,
rm,
symlink,
} from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { formatAdoptedFiles } from '../../scripts/format.mjs';
async function fixture(t, scope = ['adopted.js']) {
const root = await mkdtemp(path.join(tmpdir(), 'gev-format-'));
t.after(() => rm(root, { recursive: true, force: true }));
await mkdir(path.join(root, 'scripts'));
await writeFile(
path.join(root, 'scripts/format-scope.json'),
JSON.stringify(scope),
);
await writeFile(
path.join(root, '.prettierrc.json'),
JSON.stringify({
singleQuote: true,
semi: true,
tabWidth: 2,
endOfLine: 'lf',
}),
);
await writeFile(path.join(root, '.prettierignore'), 'ignored.js\n');
await writeFile(path.join(root, 'adopted.js'), 'const value="yes"\r\n');
return root;
}
test('format check is read-only; write only changes adopted files and is repeatable', async (t) => {
const root = await fixture(t);
const outside = 'const untouched="yes"';
await writeFile(path.join(root, 'legacy.js'), outside);
assert.deepEqual((await formatAdoptedFiles(root, '--check')).changed, [
'adopted.js',
]);
assert.equal(
await readFile(path.join(root, 'adopted.js'), 'utf8'),
'const value="yes"\r\n',
);
await formatAdoptedFiles(root, '--write');
assert.equal(
await readFile(path.join(root, 'adopted.js'), 'utf8'),
"const value = 'yes';\n",
);
assert.equal(await readFile(path.join(root, 'legacy.js'), 'utf8'), outside);
assert.deepEqual((await formatAdoptedFiles(root, '--check')).changed, []);
assert.deepEqual((await formatAdoptedFiles(root, '--write')).changed, []);
});
test('invalid, ignored and missing scope entries fail before any file is written', async (t) => {
for (const scope of [
[],
['adopted.js', 'adopted.js'],
['adopted.js', '../escape.js'],
['adopted.js', 'ignored.js'],
['adopted.js', 'missing.js'],
]) {
const root = await fixture(t, scope);
await writeFile(path.join(root, 'ignored.js'), 'const ignored=1');
await assert.rejects(formatAdoptedFiles(root, '--write'));
assert.equal(
await readFile(path.join(root, 'adopted.js'), 'utf8'),
'const value="yes"\r\n',
);
}
});
test(
'formatting rejects a symlink outside the repository',
{ skip: process.platform === 'win32' },
async (t) => {
const root = await fixture(t, ['adopted.js', 'outside.js']);
const other = await fixture(t);
await symlink(
path.join(other, 'adopted.js'),
path.join(root, 'outside.js'),
);
await assert.rejects(
formatAdoptedFiles(root, '--write'),
/repository files/,
);
assert.equal(
await readFile(path.join(root, 'adopted.js'), 'utf8'),
'const value="yes"\r\n',
);
},
);
+77
View File
@@ -0,0 +1,77 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { checkPackageBoundaries } from '../../scripts/check-package-boundaries.mjs';
async function fixture(
t,
entry = 'export const value = 1;',
extraExports = {},
) {
const root = await mkdtemp(path.join(tmpdir(), 'gev-boundaries-'));
t.after(() => rm(root, { recursive: true, force: true }));
await mkdir(path.join(root, 'scripts'));
await writeFile(
path.join(root, 'package.json'),
JSON.stringify({
name: 'boundary-fixture',
type: 'module',
exports: { './feature': './entry.js', ...extraExports },
}),
);
await writeFile(
path.join(root, 'scripts/package-boundaries.json'),
JSON.stringify({
feature: { exports: ['./feature'], modules: ['entry.js'], external: [] },
}),
);
await writeFile(path.join(root, 'entry.js'), entry);
return root;
}
test('a declared browser export builds without running app setup', async (t) => {
const root = await fixture(t);
await writeFile(
path.join(root, 'vite.config.js'),
"throw new Error('must not load app config');",
);
assert.deepEqual(await checkPackageBoundaries(root), [
{ name: 'feature', exports: 1, modules: 1 },
]);
});
test('an unclassified package export fails the boundary gate', async (t) => {
const root = await fixture(t, undefined, { './another': './another.js' });
await assert.rejects(
checkPackageBoundaries(root),
/exactly one boundary group/,
);
});
test('even an unused import of application code violates ownership', async (t) => {
const root = await fixture(
t,
"import { app } from './startup.js'; export const value = 1;",
);
await writeFile(path.join(root, 'startup.js'), 'export const app = 2;');
await assert.rejects(checkPackageBoundaries(root), /unowned module.*startup/);
});
test('Node builtins cannot enter a browser export', async (t) => {
const root = await fixture(
t,
"import fs from 'node:fs'; export const value = fs;",
);
await assert.rejects(checkPackageBoundaries(root), /unowned module|browser/);
});
test('dynamic imports obey the same component ownership rule', async (t) => {
const root = await fixture(
t,
"export const load = () => import('./startup.js');",
);
await writeFile(path.join(root, 'startup.js'), 'export const app = 2;');
await assert.rejects(checkPackageBoundaries(root), /unowned module.*startup/);
});