mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
chore: enforce scoped formatting and browser package boundaries
This commit is contained in:
1 parent
ef69f01cd8
commit
76ddc6a9ef
15 files changed
+547
-22
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user