mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-23 09:42:53 +00:00
Route probe and user materialization through one projectRuntimeSource seam, where persisted Git source overrides the host-only convenience root and must belong to Git project data. Lock default revision, explicit revision, and subdirectory semantics with real Docker acceptance. Enforce provider-probe internals structurally and document that the current Docker POC needs a locally built image because its default reference is unpublished.
315 lines
10 KiB
JavaScript
315 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { readdir, readFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import ts from "typescript";
|
|
|
|
const SERVER_ROOT = "packages/server/src/server";
|
|
const FIXTURE_ROOT = "packages/fixture-workspace-runtime";
|
|
const migratedOwners = [
|
|
"/session/files/",
|
|
"/session/git-mutation/",
|
|
"/session/provider/",
|
|
"/session/workspace-git-observer/",
|
|
"/session/workspace-provisioning/",
|
|
"/session/workspace-scripts/",
|
|
];
|
|
const hostModules = new Set(["child_process", "fs", "fs/promises", "module", "which"]);
|
|
|
|
export async function findWorkspaceBoundaryViolations(repoRoot) {
|
|
const files = [
|
|
...(await collectSources(path.join(repoRoot, SERVER_ROOT))),
|
|
...(await collectSources(path.join(repoRoot, FIXTURE_ROOT))),
|
|
];
|
|
const modules = await Promise.all(
|
|
files.map(async (filename) => {
|
|
const source = await readFile(filename, "utf8");
|
|
return { filename, sourceFile: parseSource(filename, source) };
|
|
}),
|
|
);
|
|
const publicOwners = new Set(
|
|
modules
|
|
.filter(({ sourceFile }) => staticSpecifiers(sourceFile).some(isPublicEntryPoint))
|
|
.map(({ filename }) => toRepoPath(repoRoot, filename)),
|
|
);
|
|
const violations = [];
|
|
for (const { filename, sourceFile } of modules) {
|
|
const importer = toRepoPath(repoRoot, filename);
|
|
const external = importer.startsWith(`${FIXTURE_ROOT}/`);
|
|
const governed =
|
|
external ||
|
|
(!isTestFile(importer) && (ownsMigratedSurface(importer) || publicOwners.has(importer)));
|
|
for (const specifier of staticSpecifiers(sourceFile)) {
|
|
if (!isTestFile(importer) && isForbiddenInternal(importer, specifier)) {
|
|
violations.push(violation(importer, specifier, "module-entrypoint"));
|
|
}
|
|
}
|
|
for (const load of moduleLoads(sourceFile)) {
|
|
if (load.specifier === null) {
|
|
if (governed) violations.push(violation(importer, "<computed>", "nonliteral-module-load"));
|
|
continue;
|
|
}
|
|
const specifier = load.specifier;
|
|
if (isForbiddenInternal(importer, specifier)) {
|
|
violations.push(violation(importer, specifier, "module-entrypoint"));
|
|
}
|
|
if (governed && !external && isHostCapability(specifier)) {
|
|
violations.push(violation(importer, specifier, "workspace-host-access"));
|
|
}
|
|
if (external && isExternalEscape(importer, specifier)) {
|
|
violations.push(violation(importer, specifier, "external-runtime-contract"));
|
|
}
|
|
}
|
|
}
|
|
return violations.sort((left, right) =>
|
|
`${left.file}:${left.import}:${left.rule}`.localeCompare(
|
|
`${right.file}:${right.import}:${right.rule}`,
|
|
),
|
|
);
|
|
}
|
|
|
|
function parseSource(filename, source) {
|
|
return ts.createSourceFile(
|
|
filename,
|
|
source,
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
filename.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
);
|
|
}
|
|
|
|
function staticSpecifiers(sourceFile) {
|
|
const specifiers = [];
|
|
for (const statement of sourceFile.statements) {
|
|
if (
|
|
(ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)) &&
|
|
statement.moduleSpecifier &&
|
|
ts.isStringLiteral(statement.moduleSpecifier)
|
|
) {
|
|
specifiers.push(statement.moduleSpecifier.text);
|
|
}
|
|
}
|
|
return specifiers;
|
|
}
|
|
|
|
function moduleLoads(sourceFile) {
|
|
const loads = [];
|
|
const constants = constantBindings(sourceFile);
|
|
const loaderAliases = moduleLoaderAliases(sourceFile);
|
|
visit(sourceFile);
|
|
return loads;
|
|
|
|
function visit(node) {
|
|
if (
|
|
ts.isCallExpression(node) &&
|
|
(node.expression.kind === ts.SyntaxKind.ImportKeyword ||
|
|
(ts.isIdentifier(node.expression) && loaderAliases.has(node.expression.text)))
|
|
) {
|
|
const argument = node.arguments.length === 1 ? node.arguments[0] : undefined;
|
|
loads.push({ specifier: argument ? constantString(argument, constants) : null });
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
}
|
|
|
|
function constantBindings(sourceFile) {
|
|
const constants = new Map();
|
|
let changed = true;
|
|
while (changed) {
|
|
changed = false;
|
|
for (const statement of sourceFile.statements) {
|
|
if (!ts.isVariableStatement(statement)) continue;
|
|
if (!(statement.declarationList.flags & ts.NodeFlags.Const)) continue;
|
|
for (const declaration of statement.declarationList.declarations) {
|
|
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) continue;
|
|
const value = constantString(declaration.initializer, constants);
|
|
if (value === null || constants.get(declaration.name.text) === value) continue;
|
|
constants.set(declaration.name.text, value);
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
return constants;
|
|
}
|
|
|
|
function moduleLoaderAliases(sourceFile) {
|
|
const aliases = new Set(["require"]);
|
|
let changed = true;
|
|
while (changed) {
|
|
changed = false;
|
|
for (const statement of sourceFile.statements) {
|
|
if (!ts.isVariableStatement(statement)) continue;
|
|
for (const declaration of statement.declarationList.declarations) {
|
|
if (
|
|
!ts.isIdentifier(declaration.name) ||
|
|
!declaration.initializer ||
|
|
!ts.isIdentifier(declaration.initializer) ||
|
|
!aliases.has(declaration.initializer.text) ||
|
|
aliases.has(declaration.name.text)
|
|
) {
|
|
continue;
|
|
}
|
|
aliases.add(declaration.name.text);
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
return aliases;
|
|
}
|
|
|
|
function constantString(expression, constants) {
|
|
if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) {
|
|
return expression.text;
|
|
}
|
|
if (ts.isIdentifier(expression)) return constants.get(expression.text) ?? null;
|
|
if (ts.isParenthesizedExpression(expression)) {
|
|
return constantString(expression.expression, constants);
|
|
}
|
|
if (
|
|
ts.isBinaryExpression(expression) &&
|
|
expression.operatorToken.kind === ts.SyntaxKind.PlusToken
|
|
) {
|
|
const left = constantString(expression.left, constants);
|
|
const right = constantString(expression.right, constants);
|
|
return left === null || right === null ? null : left + right;
|
|
}
|
|
if (ts.isTemplateExpression(expression)) {
|
|
let value = expression.head.text;
|
|
for (const span of expression.templateSpans) {
|
|
const substitution = constantString(span.expression, constants);
|
|
if (substitution === null) return null;
|
|
value += substitution + span.literal.text;
|
|
}
|
|
return value;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function isPublicEntryPoint(specifier) {
|
|
return /workspace-(?:runtime|helper)\/index\.(?:js|ts)$/.test(specifier);
|
|
}
|
|
|
|
function ownsMigratedSurface(importer) {
|
|
return !isTestFile(importer) && migratedOwners.some((owner) => importer.includes(owner));
|
|
}
|
|
|
|
function isHostCapability(specifier) {
|
|
const normalized = specifier.startsWith("node:") ? specifier.slice(5) : specifier;
|
|
return (
|
|
hostModules.has(normalized) || /(?:^|\/)utils\/run-git-command\.(?:js|ts)$/.test(normalized)
|
|
);
|
|
}
|
|
|
|
function isForbiddenInternal(importer, specifier) {
|
|
const target = resolveSpecifier(importer, specifier);
|
|
if (!target || !target.includes("/internal/")) return false;
|
|
if (target.includes("/provider-probe/internal/")) {
|
|
return !(
|
|
importer.endsWith("/provider-probe/index.ts") ||
|
|
importer.includes("/provider-probe/internal/")
|
|
);
|
|
}
|
|
if (target.includes("/workspace-runtime/command/internal/")) {
|
|
return !(
|
|
importer.endsWith("/workspace-runtime/command/index.ts") ||
|
|
importer.includes("/workspace-runtime/command/internal/")
|
|
);
|
|
}
|
|
if (target.includes("/workspace-runtime/git-observation/internal/")) {
|
|
return !(
|
|
importer.endsWith("/workspace-runtime/git-observation/index.ts") ||
|
|
importer.includes("/workspace-runtime/git-observation/internal/") ||
|
|
importer.endsWith("/workspace-runtime/internal/service.ts")
|
|
);
|
|
}
|
|
if (target.includes("/workspace-runtime/internal/")) {
|
|
return !(
|
|
importer.endsWith("/workspace-runtime/index.ts") ||
|
|
importer.includes("/workspace-runtime/internal/")
|
|
);
|
|
}
|
|
if (target.includes("/workspace-helper/internal/")) {
|
|
return !(
|
|
importer.endsWith("/workspace-helper/index.ts") ||
|
|
importer.includes("/workspace-helper/internal/") ||
|
|
importer.endsWith("/workspace-runtime/internal/service.ts")
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isExternalEscape(importer, specifier) {
|
|
const normalized = specifier.startsWith("node:") ? specifier.slice(5) : specifier;
|
|
if (normalized === "module") return true;
|
|
if (specifier.startsWith("node:") || hostModules.has(specifier) || specifier === "node-pty") {
|
|
return false;
|
|
}
|
|
if (specifier === "@getpaseo/workspace-runtime-contract") return false;
|
|
if (path.posix.isAbsolute(specifier) || specifier.startsWith("file:")) return true;
|
|
if (specifier.startsWith(".")) {
|
|
const target = path.posix.normalize(path.posix.join(path.posix.dirname(importer), specifier));
|
|
return !target.startsWith(`${FIXTURE_ROOT}/`);
|
|
}
|
|
return (
|
|
specifier === "@getpaseo/server" ||
|
|
specifier.startsWith("@getpaseo/server/") ||
|
|
specifier.includes("packages/server/") ||
|
|
specifier.includes("/internal/")
|
|
);
|
|
}
|
|
|
|
function resolveSpecifier(importer, specifier) {
|
|
if (specifier.startsWith(".")) {
|
|
return path.posix.normalize(path.posix.join(path.posix.dirname(importer), specifier));
|
|
}
|
|
return specifier.replace(/^@getpaseo\/server\/?/, `${SERVER_ROOT}/`);
|
|
}
|
|
|
|
function isTestFile(filename) {
|
|
return /\.(?:test|spec)\.[^.]+$/.test(filename);
|
|
}
|
|
|
|
function violation(file, imported, rule) {
|
|
return {
|
|
file,
|
|
import: imported,
|
|
rule,
|
|
message: `${file}: ${rule} forbids ${imported}`,
|
|
};
|
|
}
|
|
|
|
async function collectSources(directory) {
|
|
let entries;
|
|
try {
|
|
entries = await readdir(directory, { withFileTypes: true });
|
|
} catch (error) {
|
|
if (error.code === "ENOENT") return [];
|
|
throw error;
|
|
}
|
|
const nested = await Promise.all(
|
|
entries.map((entry) => {
|
|
const target = path.join(directory, entry.name);
|
|
if (entry.isDirectory()) return collectSources(target);
|
|
return [".js", ".mjs", ".ts", ".tsx"].includes(path.extname(entry.name)) ? [target] : [];
|
|
}),
|
|
);
|
|
return nested.flat();
|
|
}
|
|
|
|
function toRepoPath(repoRoot, filename) {
|
|
return path.relative(repoRoot, filename).split(path.sep).join("/");
|
|
}
|
|
|
|
async function main() {
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const violations = await findWorkspaceBoundaryViolations(repoRoot);
|
|
for (const item of violations) process.stderr.write(`${item.message}\n`);
|
|
if (violations.length > 0) process.exitCode = 1;
|
|
}
|
|
|
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
await main();
|
|
}
|