Files
gods-eye-view/scripts/check-package-boundaries.mjs
T
Denis Groshev 048293fc2a feat(cockpit): ride a tracked satellite in an orbital view
With a satellite tracked and no aircraft tracked, Cockpit can ride the
satellite. Entry needs only the Satellites layer; debris can be tracked but
not entered, and voice stays aircraft-only. Double-clicking the tracked
satellite or the entry buttons (reading ORBIT) enter it.

The camera sits on the satellite's per-frame SGP4 position and looks
straight down, turned to the ground track (north-up for a satellite with
none). The optical zoom steps 1x to 8x with a focus pull, the arrows tilt
the view up to 45 degrees off nadir, and Home or 0 returns to nadir. Off
nadir the tilt reads its total angle first and the swath readout becomes an
approximation marked with ≈. While in orbit the map leases flat Esri
imagery, the cloud pass stays off and the view opens in the optical style.

The orbital HUD names the satellite with its owner's flag (from the
flag-icons package, loaded per country) or an organisation badge, its
operator, class, NORAD and COSPAR ids and TLE epoch with the element lines
on request, altitude, orbital speed and the sub-satellite point. CURRENTLY
OVER names the country and its first-level division, or the sea, and from
8x the most prominent populated place nearby, looked up offline from the
bundled Natural Earth packs (populated places are a new pack). The
satellites layer hides the ridden satellite's own marks, dims the other
orbit rings and ignores scene clicks and Escape while held. Exit restores
the camera's field of view and the satellite's follow camera.

C no longer enters or leaves Cockpit, bridge or orbit: it is left to the
app-wide CCTV shortcut, and Escape or the exit button leave first person.
2026-11-09 00:24:22 +01:00

170 lines
5.9 KiB
JavaScript

import { readFile, realpath } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import vue from '@vitejs/plugin-vue';
import { VUE_PLUGIN_OPTIONS } from '../build/vueOptions.js';
import { build, normalizePath } from 'vite';
/** Build each browser or Node 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)) {
const node = group.runtime === 'node';
if (group.runtime && !['browser', 'node'].includes(group.runtime)) {
throw new Error(`Invalid boundary runtime: ${name}`);
}
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) &&
!(node && Object.hasOwn(pkg.devDependencies || {}, external))
) {
throw new Error(
`Boundary external must be a declared dependency for its runtime: ${external}`,
);
}
}
const input = group.exports.map((key) => {
const declaration = pkg.exports[key];
const target = node ? declaration?.node : declaration;
if (
node &&
(typeof declaration !== 'object' ||
Object.keys(declaration).join() !== 'node')
) {
throw new Error(`Node export must have only a node condition: ${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,
// Keep this gate out of a running dev server's dependency cache
// (node_modules/.vite): a build there re-optimizes dependencies and the
// dev server's workers then fail with 504 "Outdated Optimize Dep".
cacheDir: path.join(root, 'node_modules/.vite-boundaries'),
configFile: false,
envFile: false,
publicDir: false,
ssr: node ? { noExternal: true } : undefined,
logLevel: 'silent',
plugins: [
vue(VUE_PLUGIN_OPTIONS),
{
name: 'check-package-ownership',
moduleParsed(info) {
// Plugin helpers are virtual; a single-file component's style and
// template blocks belong to the component file itself.
if (info.id.startsWith('\0')) return;
const id = info.id.split('?')[0];
// A declared dependency also owns the asset files it ships (a
// flag-icons SVG imported as a URL) that a glob resolves in place.
const dependencyAsset = group.external.some((external) =>
id.startsWith(
`${normalizePath(path.join(root, 'node_modules', external))}/`,
),
);
if (!allowed.has(id) && !dependencyAsset) {
throw new Error(
`Package boundary ${name} imports an unowned module: ${path.relative(root, id)}`,
);
}
seen.add(id);
},
},
],
build: {
ssr: node,
lib: { entry: input, formats: ['es'] },
write: false,
minify: false,
assetsInlineLimit: 0,
rollupOptions: {
input,
// A declared dependency also owns its package subpath exports
// (for example `@jtarrio/webrtlsdr/rtlsdr.js`).
external: (id) =>
group.external.some(
(name) => id === name || id.startsWith(`${name}/`),
),
// 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;
}
}