mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Templates listed in build/applicationViews.js expand to mount hosts, and mountApplicationViews() renders them before translation and wiring. Markup tests read the page through a server-side render helper, the i18n scanner reads view templates, and Vue preserves whitespace so views keep the exact DOM. No template is converted yet.
163 lines
5.5 KiB
JavaScript
163 lines
5.5 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];
|
|
if (!allowed.has(id)) {
|
|
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;
|
|
}
|
|
}
|