Files
maka/apps/desktop/scripts/check-renderer-architecture.mjs
agent-radar 7be535059e
Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 19:05:14 +08:00

2586 lines
100 KiB
JavaScript

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { execFileSync } from 'node:child_process';
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
import { builtinModules } from 'node:module';
import { tmpdir } from 'node:os';
import { dirname, join, relative, resolve, sep } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { parse } from '@babel/parser';
const STATEFUL_HOOKS = new Set([
'use',
'useActionState',
'useDeferredValue',
'useEffect',
'useImperativeHandle',
'useInsertionEffect',
'useLayoutEffect',
'useOptimistic',
'useReducer',
'useRef',
'useState',
'useSyncExternalStore',
'useTransition',
]);
const REACT_LIFECYCLE_METHODS = new Set([
'UNSAFE_componentWillMount',
'UNSAFE_componentWillReceiveProps',
'UNSAFE_componentWillUpdate',
'componentDidCatch',
'componentDidMount',
'componentDidUpdate',
'componentWillMount',
'componentWillReceiveProps',
'componentWillUnmount',
'componentWillUpdate',
'getDerivedStateFromError',
'getDerivedStateFromProps',
'getSnapshotBeforeUpdate',
'shouldComponentUpdate',
]);
const ENVIRONMENT_CALLS = new Set([
'BroadcastChannel',
'EventSource',
'FileReader',
'IntersectionObserver',
'MutationObserver',
'ResizeObserver',
'SharedWorker',
'WebSocket',
'Worker',
'XMLHttpRequest',
'addEventListener',
'cancelAnimationFrame',
'clearInterval',
'clearTimeout',
'fetch',
'matchMedia',
'queueMicrotask',
'requestAnimationFrame',
'removeEventListener',
'setInterval',
'setTimeout',
]);
const ENVIRONMENT_OBJECTS = new Set([
'document',
'history',
'indexedDB',
'localStorage',
'location',
'navigator',
'sessionStorage',
]);
const FORBIDDEN_ENVIRONMENT_IMPORTS = new Set([
'electron',
...builtinModules,
...builtinModules.map((module) => `node:${module.replace(/^node:/u, '')}`),
]);
const SOURCE_EXTENSION = /\.(?:(?:c|m)?(?:js|ts)x?)$/u;
const SOURCE_FILE = SOURCE_EXTENSION;
const DECLARATION_FILE = /\.d\.(?:(?:c|m)?ts)$/u;
const LEGACY_APP_SHELL_FILE = /^(?:app-shell(?:-.+)?|use-app-shell-.+)\.(?:(?:c|m)?(?:js|ts)x?)$/u;
const LEGACY_APP_SHELL_MODULE = 'src/renderer/app-shell';
const LEGACY_APP_SHELL_ADAPTER = 'src/renderer/composition/legacy-desktop-region.tsx';
const RENDERER_ENTRY_INDEX = 'src/renderer/index.html';
const RENDERER_ENTRY_SOURCE = 'src/renderer/main.tsx';
const RENDERER_ENTRY_SCRIPT_SOURCE = '/main.tsx';
const RENDERER_LOADER_SOURCE = 'src/main/main-window.ts';
const MAIN_RENDERER_LOADER_SOURCE = 'src/main/main-renderer-loader.ts';
const RENDERER_VITE_CONFIG = 'vite.config.ts';
const RENDERER_BUILD_SCRIPT =
'vite build && node scripts/check-renderer-entry-output.mjs && node ../../scripts/check-third-party-notices.mjs';
const DESKTOP_SELF_PREFIX = '@maka/desktop/';
const CAPABILITY_DEBT_METRICS = [
'actionFactories',
'bridgePaths',
'dependencyPaths',
'environmentCapabilities',
'hookCalls',
'lifecycleMethods',
'unresolvedDependencies',
];
const ROOT_DEBT_METRICS = [
...CAPABILITY_DEBT_METRICS,
'importDeclarations',
'importSpecifiers',
'nonTriviaTokens',
];
const DEFAULT_LEGACY_GROWTH_DIRECTORIES = [
'src/renderer/astryx-theme',
'src/renderer/computer-use-overlay',
'src/renderer/locales',
'src/renderer/settings',
];
const PARSER_PLUGINS = [
'explicitResourceManagement',
'importAttributes',
'jsx',
'typescript',
];
function normalizePath(value) {
return value.split(sep).join('/');
}
function sortedObject(value) {
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)));
}
function sameJson(left, right) {
return JSON.stringify(sortedObject(left)) === JSON.stringify(sortedObject(right));
}
function isRecord(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function isSortedUniqueStrings(value) {
return (
Array.isArray(value) &&
value.every((item) => typeof item === 'string') &&
JSON.stringify(value) === JSON.stringify([...new Set(value)].sort())
);
}
function validateCountMap(value) {
return (
isRecord(value) &&
Object.entries(value).every(
([key, count]) => key.length > 0 && Number.isInteger(count) && count >= 0,
)
);
}
function validateArchitectureConfig(config, label, violations) {
if (!isRecord(config)) {
violations.push(`${label} architecture ledger must be an object`);
return false;
}
let valid = true;
const reject = (message) => {
valid = false;
violations.push(`${label} architecture ledger: ${message}`);
};
if (config.version !== 1) reject('version must be 1');
if (!isSortedUniqueStrings(config.legacyRendererFiles)) {
reject('legacyRendererFiles must be sorted unique strings');
}
if (!isSortedUniqueStrings(config.legacyGrowthDirectories)) {
reject('legacyGrowthDirectories must be sorted unique strings');
} else if (
config.legacyGrowthDirectories.some(
(path) => !path.startsWith('src/renderer/') || path.includes('..') || path.includes('\\'),
)
) {
reject('legacyGrowthDirectories must contain normalized renderer directories');
}
for (const field of ['legacyFeatureImports', 'legacyPlatformImports']) {
if (!isSortedUniqueStrings(config[field])) reject(`${field} must be sorted unique strings`);
}
if (
!isRecord(config.legacyAppShell) ||
!isRecord(config.legacyAppShell.files) ||
!isRecord(config.legacyAppShell.closure)
) {
reject('legacyAppShell.files and legacyAppShell.closure must be objects');
}
if (!isRecord(config.rootDebt)) reject('rootDebt must be an object');
if (!isRecord(config.rootDebtClosure)) reject('rootDebtClosure must be an object');
if (!Array.isArray(config.ownership)) reject('ownership must be an array');
if (!valid) return false;
const debtSections = [
{ entries: Object.entries(config.legacyAppShell.files), full: true },
{ entries: Object.entries(config.legacyAppShell.closure), full: false },
{ entries: Object.entries(config.rootDebt), full: true },
{ entries: Object.entries(config.rootDebtClosure), full: false },
];
for (const section of debtSections) {
for (const [path, debt] of section.entries) {
const requiredPrefix = section.full ? 'src/renderer/' : 'src/';
if (!path.startsWith(requiredPrefix) || path.includes('..') || path.includes('\\')) {
reject(`${path}: debt path must be a normalized ${section.full ? 'renderer' : 'Desktop'} source path`);
}
if (!isRecord(debt)) {
reject(`${path}: debt entry must be an object`);
continue;
}
if (section.full) {
for (const metric of ['importDeclarations', 'importSpecifiers', 'nonTriviaTokens']) {
if (!Number.isInteger(debt[metric]) || debt[metric] < 0) reject(`${path}: ${metric} must be a non-negative integer`);
}
}
if (!Number.isInteger(debt.unresolvedDependencies) || debt.unresolvedDependencies < 0) {
reject(`${path}: unresolvedDependencies must be a non-negative integer`);
}
for (const metric of ['bridgePaths', 'dependencyPaths', 'environmentCapabilities', 'hookCalls', 'lifecycleMethods']) {
if (!validateCountMap(debt[metric])) reject(`${path}: ${metric} must be a non-negative count map`);
}
if (!isSortedUniqueStrings(debt.actionFactories)) reject(`${path}: actionFactories must be sorted unique strings`);
}
}
const capabilities = new Set();
const targetZone = /^(?:application(?:\/[a-z0-9-]+)*|bootstrap|composition|features\/[a-z0-9-]+|shell|split-by-capability|testing)$/u;
for (const owner of config.ownership) {
if (!isRecord(owner)) {
reject('ownership entries must be objects');
continue;
}
if (typeof owner.capability !== 'string' || owner.capability.length === 0) reject('ownership capability must be non-empty');
if (capabilities.has(owner.capability)) reject(`duplicate ownership capability ${owner.capability}`);
capabilities.add(owner.capability);
if (typeof owner.targetZone !== 'string' || !targetZone.test(owner.targetZone)) {
reject(`${owner.capability}: unsupported targetZone ${String(owner.targetZone)}`);
}
if (!isSortedUniqueStrings(owner.legacyPaths)) {
reject(`${owner.capability}: legacyPaths must be sorted unique strings`);
}
}
return valid;
}
function childNodes(node) {
const children = [];
for (const [key, value] of Object.entries(node)) {
if (['comments', 'end', 'errors', 'extra', 'loc', 'start', 'tokens'].includes(key)) continue;
if (Array.isArray(value)) {
for (const item of value) {
if (item && typeof item === 'object' && typeof item.type === 'string') children.push(item);
}
continue;
}
if (value && typeof value === 'object' && typeof value.type === 'string') children.push(value);
}
return children;
}
function isMemberExpression(node) {
return node?.type === 'MemberExpression' || node?.type === 'OptionalMemberExpression';
}
function memberName(node) {
if (node?.type === 'Identifier') return node.name;
if (node?.type === 'StringLiteral') return node.value;
return undefined;
}
function unwrapExpression(node) {
let current = node;
while (
current &&
['ParenthesizedExpression', 'TSAsExpression', 'TSNonNullExpression', 'TSSatisfiesExpression', 'TSTypeAssertion', 'TypeCastExpression'].includes(
current.type,
)
) {
current = current.expression;
}
return current;
}
function memberPropertyName(node) {
if (!isMemberExpression(node)) return undefined;
const property = unwrapExpression(node.property);
if (!node.computed && property?.type === 'Identifier') return property.name;
return staticString(property);
}
function isWindowExpression(node, windowAliases = new Set(['globalThis', 'window'])) {
const expression = unwrapExpression(node);
if (expression?.type === 'Identifier' && windowAliases.has(expression.name)) return true;
return (
isMemberExpression(expression) &&
unwrapExpression(expression.object)?.type === 'Identifier' &&
windowAliases.has(unwrapExpression(expression.object).name) &&
memberPropertyName(expression) === 'window'
);
}
function isWindowMaka(node, windowAliases) {
const expression = unwrapExpression(node);
return (
isMemberExpression(expression) &&
isWindowExpression(expression.object, windowAliases) &&
memberPropertyName(expression) === 'maka'
);
}
function hookCallNames(
node,
hookAliases,
hookNamespaces,
reactNamespaces,
parents,
hookBindings,
) {
const expression = unwrapExpression(node);
if (expression?.type === 'Identifier') {
const binding = lexicalBindingIdentifier(expression, expression.name, parents);
return binding
? hookBindings.hookAliasBindings.get(binding)
: hookAliases.get(expression.name);
}
if (
isMemberExpression(expression) &&
unwrapExpression(expression.object)?.type === 'Identifier'
) {
const name = memberPropertyName(expression);
const namespaceNode = unwrapExpression(expression.object);
const namespace = namespaceNode.name;
const binding = lexicalBindingIdentifier(namespaceNode, namespace, parents);
const reactNamespace = binding
? hookBindings.reactNamespaceBindings.has(binding)
: reactNamespaces.has(namespace);
const hookNamespace = binding
? hookBindings.hookNamespaceBindings.has(binding)
: hookNamespaces.has(namespace);
if (reactNamespace && name && STATEFUL_HOOKS.has(name)) return new Set([name]);
if (hookNamespace && name && /^use[A-Z0-9]/u.test(name)) return new Set([name]);
}
return undefined;
}
function addHookNames(aliases, key, names) {
if (!names) return false;
let known = aliases.get(key);
if (!known) {
known = new Set();
aliases.set(key, known);
}
let changed = false;
for (const name of names) {
if (known.has(name)) continue;
known.add(name);
changed = true;
}
return changed;
}
function staticString(node) {
if (node?.type === 'StringLiteral') return node.value;
if (node?.type === 'BinaryExpression' && node.operator === '+') {
const left = staticString(node.left);
const right = staticString(node.right);
return left !== undefined && right !== undefined ? `${left}${right}` : undefined;
}
if (node?.type === 'TemplateLiteral') {
let value = '';
for (let index = 0; index < node.quasis.length; index += 1) {
value += node.quasis[index]?.value.cooked ?? node.quasis[index]?.value.raw ?? '';
if (index < node.expressions.length) {
const expression = staticString(node.expressions[index]);
if (expression === undefined) return undefined;
value += expression;
}
}
return value;
}
return undefined;
}
function environmentPath(
node,
windowAliases,
environmentAliases = new Map(),
parents = new WeakMap(),
) {
const expression = unwrapExpression(node);
if (expression?.type === 'Identifier') {
if (environmentAliases.has(expression.name)) return environmentAliases.get(expression.name);
if (windowAliases.has(expression.name)) return 'window';
if (ENVIRONMENT_OBJECTS.has(expression.name) && !environmentIdentifierIsShadowed(expression, expression.name, parents)) {
return expression.name;
}
return undefined;
}
if (!isMemberExpression(expression)) return undefined;
const base = environmentPath(
expression.object,
windowAliases,
environmentAliases,
parents,
);
const property = memberPropertyName(expression);
if (!base || !property) return undefined;
return `${base}.${property}`;
}
function bridgePath(node, windowAliases, bridgeAliases = new Map()) {
const expression = unwrapExpression(node);
if (expression?.type === 'Identifier') return bridgeAliases.get(expression.name);
if (isWindowMaka(expression, windowAliases)) return 'window.maka';
if (!isMemberExpression(expression)) return undefined;
const base = bridgePath(expression.object, windowAliases, bridgeAliases);
const property = memberPropertyName(expression);
if (!base || !property) return undefined;
return `${base}.${property}`;
}
function sourceDependency(node) {
if (
(node.type === 'ImportDeclaration' ||
node.type === 'ExportAllDeclaration' ||
node.type === 'ExportNamedDeclaration') &&
staticString(node.source) !== undefined
) {
return staticString(node.source);
}
if (node.type === 'ImportExpression' && staticString(node.source) !== undefined) {
return staticString(node.source);
}
if (
node.type === 'TSImportEqualsDeclaration' &&
node.moduleReference?.type === 'TSExternalModuleReference' &&
staticString(node.moduleReference.expression) !== undefined
) {
return staticString(node.moduleReference.expression);
}
if (node.type === 'TSImportType' && staticString(node.argument) !== undefined) {
return staticString(node.argument);
}
if (
(node.type === 'CallExpression' || node.type === 'OptionalCallExpression') &&
staticString(node.arguments?.[0]) !== undefined &&
(node.callee?.type === 'Import' ||
(node.callee?.type === 'Identifier' && node.callee.name === 'require'))
) {
return staticString(node.arguments[0]);
}
return undefined;
}
function bindingNames(pattern) {
if (!pattern) return [];
if (pattern.type === 'Identifier') return [pattern.name];
if (pattern.type === 'AssignmentPattern') return bindingNames(pattern.left);
if (pattern.type === 'RestElement') return bindingNames(pattern.argument);
if (pattern.type === 'TSParameterProperty') return bindingNames(pattern.parameter);
if (pattern.type === 'ArrayPattern') return pattern.elements.flatMap((element) => bindingNames(element));
if (pattern.type === 'ObjectPattern') {
return pattern.properties.flatMap((property) =>
property.type === 'RestElement' ? bindingNames(property.argument) : bindingNames(property.value),
);
}
return [];
}
function buildParentMap(root) {
const parents = new WeakMap();
function visit(node) {
for (const child of childNodes(node)) {
parents.set(child, node);
visit(child);
}
}
visit(root);
return parents;
}
function statementBindings(statement) {
const declaration =
statement?.type === 'ExportNamedDeclaration' || statement?.type === 'ExportDefaultDeclaration'
? statement.declaration
: statement;
if (!declaration) return [];
if (declaration.type === 'ImportDeclaration') return declaration.specifiers.map((specifier) => specifier.local.name);
if (declaration.type === 'VariableDeclaration') {
return declaration.declarations.flatMap((item) => bindingNames(item.id));
}
if (declaration.type === 'FunctionDeclaration' || declaration.type === 'ClassDeclaration') {
return bindingNames(declaration.id);
}
return [];
}
function bindingIdentifier(pattern, name) {
if (!pattern) return undefined;
if (pattern.type === 'Identifier') return pattern.name === name ? pattern : undefined;
if (pattern.type === 'AssignmentPattern') return bindingIdentifier(pattern.left, name);
if (pattern.type === 'RestElement') return bindingIdentifier(pattern.argument, name);
if (pattern.type === 'TSParameterProperty') return bindingIdentifier(pattern.parameter, name);
if (pattern.type === 'ArrayPattern') {
return pattern.elements
.map((element) => bindingIdentifier(element, name))
.find(Boolean);
}
if (pattern.type === 'ObjectPattern') {
return pattern.properties
.map((property) =>
property.type === 'RestElement'
? bindingIdentifier(property.argument, name)
: bindingIdentifier(property.value, name),
)
.find(Boolean);
}
return undefined;
}
function statementBindingIdentifier(statement, name, includeVar = true) {
const declaration =
statement?.type === 'ExportNamedDeclaration' || statement?.type === 'ExportDefaultDeclaration'
? statement.declaration
: statement;
if (!declaration) return undefined;
if (declaration.type === 'ImportDeclaration') {
return declaration.specifiers
.map((specifier) => specifier.local)
.find((local) => local.name === name);
}
if (declaration.type === 'VariableDeclaration') {
if (!includeVar && declaration.kind === 'var') return undefined;
return declaration.declarations
.map((item) => bindingIdentifier(item.id, name))
.find(Boolean);
}
if (declaration.type === 'FunctionDeclaration' || declaration.type === 'ClassDeclaration') {
return bindingIdentifier(declaration.id, name);
}
return undefined;
}
function hoistedVarBindingIdentifier(root, name) {
let found;
function visit(node, isRoot = false) {
if (!node || found) return;
if (
!isRoot &&
[
'ArrowFunctionExpression',
'ClassDeclaration',
'ClassExpression',
'ClassMethod',
'ClassPrivateMethod',
'FunctionDeclaration',
'FunctionExpression',
'ObjectMethod',
'StaticBlock',
].includes(node.type)
) {
return;
}
if (node.type === 'VariableDeclaration' && node.kind === 'var') {
found = node.declarations
.map((item) => bindingIdentifier(item.id, name))
.find(Boolean);
if (found) return;
}
for (const child of childNodes(node)) visit(child);
}
visit(root, true);
return found;
}
function lexicalBindingIdentifier(node, name, parents) {
let current = parents.get(node);
while (current) {
if (
current.type === 'FunctionDeclaration' ||
current.type === 'FunctionExpression' ||
current.type === 'ArrowFunctionExpression' ||
current.type === 'ObjectMethod' ||
current.type === 'ClassMethod' ||
current.type === 'ClassPrivateMethod'
) {
const parameter = current.params
.map((item) => bindingIdentifier(item, name))
.find(Boolean);
if (parameter) return parameter;
if (current.type !== 'ArrowFunctionExpression' && !current.type.endsWith('Method')) {
const functionName = bindingIdentifier(current.id, name);
if (functionName) return functionName;
}
const hoisted = hoistedVarBindingIdentifier(current.body, name);
if (hoisted) return hoisted;
}
if (current.type === 'CatchClause') {
const caught = bindingIdentifier(current.param, name);
if (caught) return caught;
}
if (current.type === 'Program' || current.type === 'BlockStatement') {
const declared = current.body
.map((statement) => statementBindingIdentifier(statement, name, false))
.find(Boolean);
if (declared) return declared;
if (current.type === 'Program') {
const hoisted = hoistedVarBindingIdentifier(current, name);
if (hoisted) return hoisted;
}
}
if (
current.type === 'ForStatement' &&
current.init?.type === 'VariableDeclaration' &&
current.init.kind !== 'var'
) {
const declared = statementBindingIdentifier(current.init, name, false);
if (declared) return declared;
}
if (
(current.type === 'ForInStatement' || current.type === 'ForOfStatement') &&
current.left?.type === 'VariableDeclaration' &&
current.left.kind !== 'var'
) {
const declared = statementBindingIdentifier(current.left, name, false);
if (declared) return declared;
}
current = parents.get(current);
}
return undefined;
}
function environmentIdentifierIsShadowed(node, name, parents) {
let current = parents.get(node);
while (current) {
if (
current.type === 'FunctionDeclaration' ||
current.type === 'FunctionExpression' ||
current.type === 'ArrowFunctionExpression' ||
current.type === 'ObjectMethod' ||
current.type === 'ClassMethod' ||
current.type === 'ClassPrivateMethod'
) {
if (
current.type !== 'ArrowFunctionExpression' &&
!current.type.endsWith('Method') &&
bindingNames(current.id).includes(name)
) {
return true;
}
if (current.params.some((parameter) => bindingNames(parameter).includes(name))) return true;
}
if (current.type === 'CatchClause' && bindingNames(current.param).includes(name)) return true;
if (current.type === 'Program' || current.type === 'BlockStatement') {
if (current.body.some((statement) => statementBindings(statement).includes(name))) return true;
}
if (
current.type === 'ForStatement' &&
current.init?.type === 'VariableDeclaration' &&
statementBindings(current.init).includes(name)
) {
return true;
}
if (
(current.type === 'ForInStatement' || current.type === 'ForOfStatement') &&
current.left?.type === 'VariableDeclaration' &&
statementBindings(current.left).includes(name)
) {
return true;
}
current = parents.get(current);
}
return false;
}
function collectAliases(program, parents) {
const hookAliases = new Map();
const hookAliasBindings = new Map();
const hookNamespaces = new Set();
const hookNamespaceBindings = new WeakSet();
const reactComponentBases = new Set();
const reactNamespaces = new Set();
const reactNamespaceBindings = new WeakSet();
const hookBindings = {
hookAliasBindings,
hookNamespaceBindings,
reactNamespaceBindings,
};
for (const statement of program.body) {
if (statement.type !== 'ImportDeclaration') continue;
const isReact = statement.source.value === 'react';
for (const specifier of statement.specifiers) {
if (isReact && (specifier.type === 'ImportDefaultSpecifier' || specifier.type === 'ImportNamespaceSpecifier')) {
reactNamespaces.add(specifier.local.name);
reactNamespaceBindings.add(specifier.local);
} else if (isReact && specifier.type === 'ImportSpecifier') {
const imported = memberName(specifier.imported);
if (imported && STATEFUL_HOOKS.has(imported)) {
addHookNames(hookAliases, specifier.local.name, [imported]);
addHookNames(hookAliasBindings, specifier.local, [imported]);
}
if (imported === 'Component' || imported === 'PureComponent') reactComponentBases.add(specifier.local.name);
} else if (!isReact) {
const imported = specifier.type === 'ImportSpecifier' ? memberName(specifier.imported) : specifier.local.name;
if (imported && /^use[A-Z0-9]/u.test(imported)) {
addHookNames(hookAliases, specifier.local.name, [imported]);
addHookNames(hookAliasBindings, specifier.local, [imported]);
}
if (specifier.type === 'ImportNamespaceSpecifier') {
hookNamespaces.add(specifier.local.name);
hookNamespaceBindings.add(specifier.local);
}
}
}
}
const windowAliases = new Set(['globalThis', 'self', 'window']);
const bridgeAliases = new Map();
const environmentAliases = new Map();
let changed = true;
while (changed) {
changed = false;
const addAlias = (aliases, name, path) => {
if (!path || aliases.has(name)) return;
aliases.set(name, path);
changed = true;
};
function visit(node) {
if (
node.type === 'ClassDeclaration' &&
node.id?.type === 'Identifier' &&
isReactComponentClass(node, reactComponentBases, reactNamespaces) &&
!reactComponentBases.has(node.id.name)
) {
reactComponentBases.add(node.id.name);
changed = true;
}
if (node.type === 'VariableDeclarator' && node.id?.type === 'Identifier') {
const declaredBinding =
lexicalBindingIdentifier(node.id, node.id.name, parents) ?? node.id;
const init = unwrapExpression(node.init);
const initBinding =
init?.type === 'Identifier'
? lexicalBindingIdentifier(init, init.name, parents)
: undefined;
const initIsReactNamespace =
init?.type === 'Identifier' &&
(initBinding
? reactNamespaceBindings.has(initBinding)
: reactNamespaces.has(init.name));
const initIsHookNamespace =
init?.type === 'Identifier' &&
(initBinding
? hookNamespaceBindings.has(initBinding)
: hookNamespaces.has(init.name));
if (initIsReactNamespace && !reactNamespaceBindings.has(declaredBinding)) {
reactNamespaces.add(node.id.name);
reactNamespaceBindings.add(declaredBinding);
changed = true;
}
if (initIsHookNamespace && !hookNamespaceBindings.has(declaredBinding)) {
hookNamespaces.add(node.id.name);
hookNamespaceBindings.add(declaredBinding);
changed = true;
}
if (isWindowExpression(node.init, windowAliases) && !windowAliases.has(node.id.name)) {
windowAliases.add(node.id.name);
changed = true;
}
addAlias(bridgeAliases, node.id.name, bridgePath(node.init, windowAliases, bridgeAliases));
const environment = environmentPath(
node.init,
windowAliases,
environmentAliases,
parents,
);
if (environment && !environment.startsWith('window.maka')) {
addAlias(environmentAliases, node.id.name, environment);
}
if (
node.init?.type === 'Identifier' &&
ENVIRONMENT_CALLS.has(node.init.name) &&
!environmentIdentifierIsShadowed(node.init, node.init.name, parents)
) {
addAlias(environmentAliases, node.id.name, node.init.name);
}
const hooks = hookCallNames(
node.init,
hookAliases,
hookNamespaces,
reactNamespaces,
parents,
hookBindings,
);
const nameChanged = addHookNames(hookAliases, node.id.name, hooks);
const bindingChanged = addHookNames(
hookAliasBindings,
declaredBinding,
hooks,
);
if (nameChanged || bindingChanged) {
changed = true;
}
}
if (node.type === 'VariableDeclarator' && node.id?.type === 'ObjectPattern') {
const bridgeBase = bridgePath(node.init, windowAliases, bridgeAliases);
const environmentBase = environmentPath(
node.init,
windowAliases,
environmentAliases,
parents,
);
for (const property of node.id.properties) {
if (property.type !== 'ObjectProperty' || property.computed || property.value?.type !== 'Identifier') continue;
const propertyName = memberName(property.key);
if (!propertyName) continue;
if (bridgeBase) addAlias(bridgeAliases, property.value.name, `${bridgeBase}.${propertyName}`);
if (isWindowExpression(node.init, windowAliases) && propertyName === 'maka') {
addAlias(bridgeAliases, property.value.name, 'window.maka');
}
if (environmentBase && !environmentBase.startsWith('window.maka')) {
addAlias(environmentAliases, property.value.name, `${environmentBase}.${propertyName}`);
}
}
}
if (node.type === 'AssignmentExpression' && node.left?.type === 'Identifier') {
const right = unwrapExpression(node.right);
const rightBinding =
right?.type === 'Identifier'
? lexicalBindingIdentifier(right, right.name, parents)
: undefined;
const leftBinding = lexicalBindingIdentifier(node.left, node.left.name, parents);
const rightIsReactNamespace =
right?.type === 'Identifier' &&
(rightBinding
? reactNamespaceBindings.has(rightBinding)
: reactNamespaces.has(right.name));
const rightIsHookNamespace =
right?.type === 'Identifier' &&
(rightBinding
? hookNamespaceBindings.has(rightBinding)
: hookNamespaces.has(right.name));
if (
rightIsReactNamespace &&
(leftBinding
? !reactNamespaceBindings.has(leftBinding)
: !reactNamespaces.has(node.left.name))
) {
reactNamespaces.add(node.left.name);
if (leftBinding) reactNamespaceBindings.add(leftBinding);
changed = true;
}
if (
rightIsHookNamespace &&
(leftBinding
? !hookNamespaceBindings.has(leftBinding)
: !hookNamespaces.has(node.left.name))
) {
hookNamespaces.add(node.left.name);
if (leftBinding) hookNamespaceBindings.add(leftBinding);
changed = true;
}
addAlias(bridgeAliases, node.left.name, bridgePath(node.right, windowAliases, bridgeAliases));
const environment = environmentPath(
node.right,
windowAliases,
environmentAliases,
parents,
);
if (environment && !environment.startsWith('window.maka')) {
addAlias(environmentAliases, node.left.name, environment);
}
if (
node.right?.type === 'Identifier' &&
ENVIRONMENT_CALLS.has(node.right.name) &&
!environmentIdentifierIsShadowed(node.right, node.right.name, parents)
) {
addAlias(environmentAliases, node.left.name, node.right.name);
}
const hooks = hookCallNames(
node.right,
hookAliases,
hookNamespaces,
reactNamespaces,
parents,
hookBindings,
);
const nameChanged = addHookNames(hookAliases, node.left.name, hooks);
const bindingChanged = leftBinding
? addHookNames(hookAliasBindings, leftBinding, hooks)
: false;
if (nameChanged || bindingChanged) {
changed = true;
}
}
if (
node.type === 'VariableDeclarator' &&
node.id?.type === 'ObjectPattern' &&
unwrapExpression(node.init)?.type === 'Identifier'
) {
const namespaceNode = unwrapExpression(node.init);
const namespace = namespaceNode.name;
const namespaceBinding = lexicalBindingIdentifier(
namespaceNode,
namespace,
parents,
);
const reactNamespace = namespaceBinding
? reactNamespaceBindings.has(namespaceBinding)
: reactNamespaces.has(namespace);
const hookNamespace = namespaceBinding
? hookNamespaceBindings.has(namespaceBinding)
: hookNamespaces.has(namespace);
if (reactNamespace || hookNamespace) {
for (const property of node.id.properties) {
if (property.type !== 'ObjectProperty' || property.computed) continue;
const imported = memberName(property.key);
const recognized = reactNamespace
? imported && STATEFUL_HOOKS.has(imported)
: imported && /^use[A-Z0-9]/u.test(imported);
const local =
property.value?.type === 'Identifier'
? property.value
: property.value?.type === 'AssignmentPattern' &&
property.value.left?.type === 'Identifier'
? property.value.left
: undefined;
if (recognized && local) {
const localBinding =
lexicalBindingIdentifier(local, local.name, parents) ?? local;
const nameChanged = addHookNames(hookAliases, local.name, [imported]);
const bindingChanged = addHookNames(
hookAliasBindings,
localBinding,
[imported],
);
if (nameChanged || bindingChanged) {
changed = true;
}
}
}
}
}
for (const child of childNodes(node)) visit(child);
}
visit(program);
}
return {
bridgeAliases,
environmentAliases,
hookAliases,
hookBindings,
hookNamespaces,
reactComponentBases,
reactNamespaces,
windowAliases,
};
}
function isReactComponentClass(node, reactComponentBases, reactNamespaces) {
if (node?.type !== 'ClassDeclaration' && node?.type !== 'ClassExpression') return false;
const superClass = unwrapExpression(node.superClass);
if (superClass?.type === 'Identifier') return reactComponentBases.has(superClass.name);
return (
isMemberExpression(superClass) &&
unwrapExpression(superClass.object)?.type === 'Identifier' &&
reactNamespaces.has(unwrapExpression(superClass.object).name) &&
['Component', 'PureComponent'].includes(memberPropertyName(superClass))
);
}
function enclosingClass(node, parents) {
let current = parents.get(node);
while (current) {
if (current.type === 'ClassDeclaration' || current.type === 'ClassExpression') return current;
current = parents.get(current);
}
return undefined;
}
export function analyzeRendererSource(source, file = 'fixture.ts') {
const typedSource = /\.(?:(?:c|m)?ts|tsx)$/u.test(file);
const jsxSource = /\.(?:jsx|tsx)$/u.test(file);
const ast = parse(source, {
createImportExpressions: true,
errorRecovery: false,
plugins: PARSER_PLUGINS.filter((plugin) => (plugin === 'typescript' ? typedSource : plugin === 'jsx' ? jsxSource : true)),
sourceFilename: file,
sourceType: 'module',
tokens: true,
});
const parents = buildParentMap(ast.program);
const {
bridgeAliases,
environmentAliases,
hookAliases,
hookBindings,
hookNamespaces,
reactComponentBases,
reactNamespaces,
windowAliases,
} = collectAliases(ast.program, parents);
const bridgePaths = {};
const environmentCapabilities = {};
const hookCalls = {};
const lifecycleMethods = {};
const actionFactories = [];
const dependencies = [];
const dependencyPaths = {};
let importDeclarations = 0;
let importSpecifiers = 0;
let unresolvedDependencies = 0;
function recordBridgePath(path) {
bridgePaths[path] = (bridgePaths[path] ?? 0) + 1;
}
function recordEnvironmentCapability(path) {
environmentCapabilities[path] = (environmentCapabilities[path] ?? 0) + 1;
}
function visit(node, parent) {
if (node.type === 'ImportDeclaration') {
importDeclarations += 1;
importSpecifiers += node.specifiers.length;
}
if (node.type === 'TSImportEqualsDeclaration') {
importDeclarations += 1;
importSpecifiers += 1;
}
const dependency = sourceDependency(node);
if (dependency !== undefined) {
dependencies.push(dependency);
dependencyPaths[dependency] = (dependencyPaths[dependency] ?? 0) + 1;
}
if (
(node.type === 'ImportExpression' && staticString(node.source) === undefined) ||
((node.type === 'CallExpression' || node.type === 'OptionalCallExpression') &&
(node.callee?.type === 'Import' ||
(node.callee?.type === 'Identifier' && node.callee.name === 'require')) &&
staticString(node.arguments?.[0]) === undefined) ||
((node.type === 'CallExpression' || node.type === 'OptionalCallExpression') &&
isMemberExpression(node.callee) &&
unwrapExpression(node.callee.object)?.type === 'MetaProperty' &&
unwrapExpression(node.callee.object).meta?.name === 'import' &&
unwrapExpression(node.callee.object).property?.name === 'meta' &&
['glob', 'globEager'].includes(memberPropertyName(node.callee)))
) {
unresolvedDependencies += 1;
}
const directEnvironmentPath = environmentPath(
node,
windowAliases,
environmentAliases,
parents,
);
if (
directEnvironmentPath &&
!directEnvironmentPath.startsWith('window.maka') &&
!(isMemberExpression(parent) && unwrapExpression(parent.object) === node)
) {
recordEnvironmentCapability(directEnvironmentPath);
}
if (isMemberExpression(node) && memberPropertyName(node) === undefined) {
const dynamicBridgeBase = bridgePath(node.object, windowAliases, bridgeAliases);
if (dynamicBridgeBase) recordBridgePath(`${dynamicBridgeBase}.*`);
const dynamicEnvironmentBase = environmentPath(
node.object,
windowAliases,
environmentAliases,
parents,
);
if (dynamicEnvironmentBase && !dynamicEnvironmentBase.startsWith('window.maka')) {
recordEnvironmentCapability(`${dynamicEnvironmentBase}.*`);
}
}
const directBridgePath = bridgePath(node, windowAliases, bridgeAliases);
if (
directBridgePath &&
!(isMemberExpression(parent) && unwrapExpression(parent.object) === node) &&
!(node.type === 'Identifier' && parent?.type === 'VariableDeclarator' && parent.id === node)
) {
recordBridgePath(directBridgePath);
}
if (
node.type === 'VariableDeclarator' &&
node.id?.type === 'ObjectPattern' &&
isWindowExpression(node.init, windowAliases)
) {
for (const property of node.id.properties) {
if (property.type === 'ObjectProperty' && !property.computed && memberName(property.key) === 'maka') {
recordBridgePath('window.maka');
}
}
}
if (node.type === 'CallExpression' || node.type === 'OptionalCallExpression') {
const names = hookCallNames(
node.callee,
hookAliases,
hookNamespaces,
reactNamespaces,
parents,
hookBindings,
);
for (const name of names ?? []) {
hookCalls[name] = (hookCalls[name] ?? 0) + 1;
}
const callee = unwrapExpression(node.callee);
if (
callee?.type === 'Identifier' &&
ENVIRONMENT_CALLS.has(callee.name) &&
!environmentIdentifierIsShadowed(callee, callee.name, parents)
) {
recordEnvironmentCapability(callee.name);
}
}
if (node.type === 'NewExpression') {
const callee = unwrapExpression(node.callee);
if (
callee?.type === 'Identifier' &&
ENVIRONMENT_CALLS.has(callee.name) &&
!environmentIdentifierIsShadowed(callee, callee.name, parents)
) {
recordEnvironmentCapability(callee.name);
}
}
const ownerClass = enclosingClass(node, parents);
const reactComponentOwner = isReactComponentClass(ownerClass, reactComponentBases, reactNamespaces);
if (reactComponentOwner && (node.type === 'ClassMethod' || node.type === 'ClassPrivateMethod')) {
const name = memberName(node.key);
if (name && REACT_LIFECYCLE_METHODS.has(name)) {
lifecycleMethods[name] = (lifecycleMethods[name] ?? 0) + 1;
}
}
if (reactComponentOwner && ['ClassProperty', 'ClassPrivateProperty', 'PropertyDefinition'].includes(node.type)) {
const name = memberName(node.key);
if (name === 'state' || (name && REACT_LIFECYCLE_METHODS.has(name))) {
lifecycleMethods[name] = (lifecycleMethods[name] ?? 0) + 1;
}
}
if (
reactComponentOwner &&
isMemberExpression(node) &&
unwrapExpression(node.object)?.type === 'ThisExpression' &&
['setState', 'state'].includes(memberPropertyName(node))
) {
const name = memberPropertyName(node);
lifecycleMethods[name] = (lifecycleMethods[name] ?? 0) + 1;
}
if (
(node.type === 'FunctionDeclaration' || node.type === 'VariableDeclarator') &&
node.id?.type === 'Identifier' &&
node.id.name.startsWith('createAppShell')
) {
actionFactories.push(node.id.name);
}
for (const child of childNodes(node)) {
parents.set(child, node);
visit(child, node);
}
}
visit(ast.program, undefined);
return {
actionFactories: actionFactories.sort(),
bridgePaths: sortedObject(bridgePaths),
dependencies,
dependencyPaths: sortedObject(dependencyPaths),
environmentCapabilities: sortedObject(environmentCapabilities),
hookCalls: sortedObject(hookCalls),
importDeclarations,
importSpecifiers,
lifecycleMethods: sortedObject(lifecycleMethods),
nonTriviaTokens: ast.tokens.length,
unresolvedDependencies,
};
}
function sourceFiles(root, { includeDeclarations = false } = {}) {
if (!existsSync(root)) return [];
return readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
const path = join(root, entry.name);
if (entry.isDirectory()) return sourceFiles(path, { includeDeclarations });
return SOURCE_FILE.test(entry.name) && (includeDeclarations || !DECLARATION_FILE.test(entry.name)) ? [path] : [];
});
}
function zoneFor(path) {
const normalized = normalizePath(path).replace(SOURCE_EXTENSION, '');
if (normalized.startsWith('src/main/')) return { kind: 'main' };
if (normalized.startsWith('src/preload/')) return { kind: 'preload' };
if (!normalized.startsWith('src/renderer/')) return { kind: 'external' };
const feature = normalized.match(/^src\/renderer\/features\/([^/]+)(?:\/(.*))?$/u);
if (feature) return { kind: 'feature', name: feature[1], subpath: feature[2] ?? '' };
for (const kind of ['application', 'bootstrap', 'composition', 'shell']) {
const prefix = `src/renderer/${kind}/`;
if (normalized.startsWith(prefix)) return { kind, subpath: normalized.slice(prefix.length) };
}
if (normalized.startsWith('src/renderer/platform/desktop/')) return { kind: 'platform' };
return { kind: 'legacy' };
}
function isRootClosureDebtSource(path) {
return ['external', 'legacy', 'main', 'preload'].includes(zoneFor(path).kind);
}
function resolveDependency(desktopRoot, importer, dependency) {
let target;
if (dependency.startsWith('.')) target = resolve(dirname(importer), dependency);
else if (dependency.startsWith(DESKTOP_SELF_PREFIX)) target = resolve(desktopRoot, dependency.slice(DESKTOP_SELF_PREFIX.length));
else return undefined;
const normalized = normalizePath(target);
return normalized.replace(/\.(?:c|m)?(?:js|ts)x?$/u, '');
}
function sourceFileIndex(desktopRoot) {
const index = new Map();
const paths = sourceFiles(resolve(desktopRoot, 'src'), { includeDeclarations: true }).sort();
for (const path of paths.filter((path) => !DECLARATION_FILE.test(path))) {
const key = normalizePath(path).replace(SOURCE_EXTENSION, '');
if (!index.has(key)) index.set(key, path);
}
for (const path of paths.filter((path) => DECLARATION_FILE.test(path))) {
const key = normalizePath(path).replace(SOURCE_EXTENSION, '');
index.set(key, path);
const runtimeAlias = key.replace(/\.d$/u, '');
if (!index.has(runtimeAlias)) index.set(runtimeAlias, path);
}
return index;
}
function resolveSourceFile(desktopRoot, importer, dependency, index) {
const target = resolveDependency(desktopRoot, importer, dependency);
if (!target) return undefined;
return index.get(target) ?? index.get(`${target}/index`);
}
function isSourceLikeDependency(dependency) {
const withoutQuery = dependency.split(/[?#]/u, 1)[0];
const lastSegment = withoutQuery.split('/').at(-1) ?? '';
return !lastSegment.includes('.') || SOURCE_EXTENSION.test(lastSegment);
}
function isAllowedLegacyGrowthPath(config, path) {
return config.legacyGrowthDirectories.some((directory) => path.startsWith(`${directory}/`));
}
function capabilityDebtMetrics(analysis) {
return {
bridgePaths: analysis.bridgePaths,
environmentCapabilities: analysis.environmentCapabilities,
hookCalls: analysis.hookCalls,
lifecycleMethods: analysis.lifecycleMethods,
unresolvedDependencies: analysis.unresolvedDependencies,
actionFactories: analysis.actionFactories,
dependencyPaths: analysis.dependencyPaths,
};
}
function debtMetrics(analysis) {
return {
importDeclarations: analysis.importDeclarations,
...capabilityDebtMetrics(analysis),
importSpecifiers: analysis.importSpecifiers,
nonTriviaTokens: analysis.nonTriviaTokens,
};
}
function collectRootDependencyClosure(desktopRoot, rootPaths, violations = [], label = 'renderer root') {
const index = sourceFileIndex(desktopRoot);
const sourceRoot = normalizePath(resolve(desktopRoot, 'src'));
const roots = new Set(rootPaths);
const queue = rootPaths.map((path) => resolve(desktopRoot, path));
const seen = new Set();
const closure = new Set();
while (queue.length > 0) {
const file = queue.shift();
const fileKey = normalizePath(file).replace(SOURCE_EXTENSION, '');
if (seen.has(fileKey) || !existsSync(file)) continue;
seen.add(fileKey);
const fileRelative = normalizePath(relative(desktopRoot, file));
let analysis;
try {
analysis = analyzeRendererSource(readFileSync(file, 'utf8'), fileRelative);
} catch (error) {
violations.push(`${fileRelative}: could not parse ${label} dependency closure source: ${error instanceof Error ? error.message : String(error)}`);
continue;
}
if (analysis.unresolvedDependencies > 0) {
violations.push(`${fileRelative}: ${label} closure contains a non-static import, require, or import.meta glob`);
}
for (const dependency of analysis.dependencies) {
const unresolvedTarget = resolveDependency(desktopRoot, file, dependency);
if (!unresolvedTarget) continue;
const normalizedTarget = normalizePath(unresolvedTarget);
if (normalizedTarget !== sourceRoot && !normalizedTarget.startsWith(`${sourceRoot}/`)) continue;
const target = resolveSourceFile(desktopRoot, file, dependency, index);
if (!target) {
if (isSourceLikeDependency(dependency)) {
violations.push(`${fileRelative}: ${label} closure dependency does not resolve to Desktop source: ${dependency}`);
}
continue;
}
const targetRelative = normalizePath(relative(desktopRoot, target));
if (!seen.has(normalizePath(target).replace(SOURCE_EXTENSION, ''))) queue.push(target);
if (isRootClosureDebtSource(targetRelative) && !DECLARATION_FILE.test(target) && !roots.has(targetRelative)) {
closure.add(targetRelative);
}
}
}
return [...closure].sort();
}
function featureForAbsolutePath(desktopRoot, absolutePath) {
const rendererFeatures = normalizePath(resolve(desktopRoot, 'src/renderer/features'));
if (absolutePath !== rendererFeatures && !absolutePath.startsWith(`${rendererFeatures}/`)) return undefined;
const rest = absolutePath.slice(rendererFeatures.length).replace(/^\//u, '');
const [name, ...segments] = rest.split('/');
return { name, subpath: segments.join('/') };
}
function isTestConsumer(path) {
const normalized = normalizePath(path);
return (
normalized.includes('/__tests__/') ||
normalized.includes('/e2e/') ||
normalized.includes('/stories/') ||
/\.(?:spec|stories|test)\.[^/]+$/u.test(normalized)
);
}
function isPublicFeaturePath(subpath) {
return subpath === '' || subpath === 'index';
}
function isFeatureTestSupportEntry(subpath) {
return subpath === 'stories' || subpath === 'testing';
}
function isApplicationContractPath(path) {
return path === 'src/renderer/application/contracts' || path.startsWith('src/renderer/application/contracts/');
}
function isPublicApplicationPath(path) {
if (isApplicationContractPath(path)) return true;
return /^src\/renderer\/application\/[^/]+(?:\/index)?$/u.test(path);
}
function isForbiddenEnvironmentDependency(dependency) {
return FORBIDDEN_ENVIRONMENT_IMPORTS.has(dependency) || dependency.startsWith('electron/');
}
function validateDependencies({
allowedLegacyFeatureImports,
allowedLegacyPlatformImports,
analysis,
desktopRoot,
file,
fileRelative,
observedLegacyFeatureImports,
observedLegacyPlatformImports,
violations,
}) {
const sourceZone = zoneFor(fileRelative);
for (const dependency of analysis.dependencies) {
if (
['application', 'bootstrap', 'composition', 'feature', 'platform', 'shell'].includes(sourceZone.kind) &&
!isTestConsumer(file) &&
isForbiddenEnvironmentDependency(dependency)
) {
violations.push(`${fileRelative}: ${sourceZone.kind} code imports forbidden environment module: ${dependency}`);
continue;
}
const target = resolveDependency(desktopRoot, file, dependency);
if (!target) continue;
const targetRelative = normalizePath(relative(desktopRoot, target));
const targetZone = zoneFor(targetRelative);
const targetFeature = featureForAbsolutePath(desktopRoot, target);
if (targetFeature) {
if (sourceZone.kind === 'feature' && sourceZone.name !== targetFeature.name) {
violations.push(`${fileRelative}: feature ${sourceZone.name} imports feature ${targetFeature.name}: ${dependency}`);
continue;
}
if (
sourceZone.kind === 'feature' &&
sourceZone.name === targetFeature.name &&
isFeatureTestSupportEntry(targetFeature.subpath) &&
!isFeatureTestSupportEntry(sourceZone.subpath) &&
!isTestConsumer(file)
) {
violations.push(
`${fileRelative}: feature production code cannot import its ${targetFeature.subpath} entry: ${dependency}`,
);
}
if (sourceZone.kind !== 'feature') {
const supportEntry = isFeatureTestSupportEntry(targetFeature.subpath);
if (!isPublicFeaturePath(targetFeature.subpath) && !(supportEntry && isTestConsumer(file))) {
violations.push(
`${fileRelative}: feature imports must use index${isTestConsumer(file) ? ', testing, or stories' : ''}: ${dependency}`,
);
}
if (supportEntry && !isTestConsumer(file)) {
violations.push(
`${fileRelative}: production code imports a feature ${targetFeature.subpath} entry: ${dependency}`,
);
}
}
}
if (sourceZone.kind === 'feature') {
if (targetZone.kind === 'legacy') {
const edge = `${fileRelative} -> ${targetRelative}`;
observedLegacyFeatureImports.add(edge);
if (!allowedLegacyFeatureImports.has(edge)) {
violations.push(`${fileRelative}: feature imports unbudgeted renderer legacy code: ${dependency}`);
}
}
if (targetZone.kind === 'application' && !isApplicationContractPath(targetRelative)) {
violations.push(`${fileRelative}: feature imports application implementation instead of a contract: ${dependency}`);
}
if (['bootstrap', 'composition', 'main', 'platform', 'preload', 'shell'].includes(targetZone.kind)) {
violations.push(`${fileRelative}: feature imports a forbidden Desktop/shell module: ${dependency}`);
}
}
if (sourceZone.kind === 'shell') {
const allowedApplicationContract = isApplicationContractPath(targetRelative);
if (
targetZone.kind === 'feature' ||
targetZone.kind === 'platform' ||
targetZone.kind === 'legacy' ||
targetZone.kind === 'bootstrap' ||
targetZone.kind === 'composition' ||
(targetZone.kind === 'application' && !allowedApplicationContract) ||
targetZone.kind === 'main' ||
targetZone.kind === 'preload'
) {
violations.push(`${fileRelative}: shell imports forbidden implementation module: ${dependency}`);
}
}
if (sourceZone.kind === 'application') {
if (
sourceZone.subpath === 'contracts' ||
sourceZone.subpath.startsWith('contracts/')
) {
if (targetZone.kind === 'application' && !isApplicationContractPath(targetRelative)) {
violations.push(`${fileRelative}: application contracts import application implementation: ${dependency}`);
}
}
if (
targetZone.kind === 'feature' ||
targetZone.kind === 'platform' ||
targetZone.kind === 'shell' ||
targetZone.kind === 'bootstrap' ||
targetZone.kind === 'composition' ||
targetZone.kind === 'legacy' ||
targetZone.kind === 'main' ||
targetZone.kind === 'preload'
) {
violations.push(`${fileRelative}: application authority imports an outer implementation: ${dependency}`);
}
}
if (sourceZone.kind === 'composition') {
if (targetZone.kind === 'application' && !isPublicApplicationPath(targetRelative)) {
violations.push(`${fileRelative}: composition imports application implementation instead of a public entry: ${dependency}`);
}
const allowedLegacyAdapter =
fileRelative === LEGACY_APP_SHELL_ADAPTER && targetRelative === LEGACY_APP_SHELL_MODULE;
if (
targetZone.kind === 'bootstrap' ||
targetZone.kind === 'main' ||
targetZone.kind === 'preload' ||
(targetZone.kind === 'legacy' && !allowedLegacyAdapter)
) {
violations.push(`${fileRelative}: composition imports a forbidden implementation module: ${dependency}`);
}
}
if (sourceZone.kind === 'bootstrap') {
if (!['bootstrap', 'composition', 'external', 'platform'].includes(targetZone.kind)) {
violations.push(`${fileRelative}: bootstrap imports outside composition/platform: ${dependency}`);
}
}
if (sourceZone.kind === 'platform') {
if (targetZone.kind === 'application' && !isPublicApplicationPath(targetRelative)) {
violations.push(`${fileRelative}: Desktop adapter imports application implementation instead of a public entry: ${dependency}`);
}
if (targetZone.kind === 'legacy') {
const edge = `${fileRelative} -> ${targetRelative}`;
observedLegacyPlatformImports.add(edge);
if (!allowedLegacyPlatformImports.has(edge)) {
violations.push(`${fileRelative}: Desktop adapter imports unbudgeted renderer legacy code: ${dependency}`);
}
}
if (['bootstrap', 'composition', 'main', 'shell'].includes(targetZone.kind)) {
violations.push(`${fileRelative}: Desktop adapter imports an outer or legacy implementation: ${dependency}`);
}
}
}
}
function validateStrictZone({ fileRelative, analysis, violations }) {
const zone = zoneFor(fileRelative);
if (['application', 'bootstrap', 'composition', 'feature', 'shell'].includes(zone.kind)) {
if (Object.keys(analysis.bridgePaths).length > 0) {
violations.push(`${fileRelative}: ${zone.kind} code accesses the Desktop global bridge`);
}
if (Object.keys(analysis.environmentCapabilities).some((path) => path.endsWith('.*'))) {
violations.push(`${fileRelative}: ${zone.kind} code contains non-static global environment access`);
}
}
if (['bootstrap', 'composition', 'platform', 'shell'].includes(zone.kind)) {
if (Object.keys(analysis.hookCalls).length > 0) {
violations.push(`${fileRelative}: ${zone.kind} code owns stateful React hooks`);
}
if (Object.keys(analysis.lifecycleMethods).length > 0) {
violations.push(`${fileRelative}: ${zone.kind} code owns React class lifecycle methods`);
}
const environmentPaths = Object.keys(analysis.environmentCapabilities);
const bootstrapEnvironmentIsOnlyMountLookup =
zone.kind === 'bootstrap' &&
environmentPaths.every((path) => ['document.getElementById', 'document.querySelector'].includes(path));
if (zone.kind !== 'platform' && environmentPaths.length > 0 && !bootstrapEnvironmentIsOnlyMountLookup) {
violations.push(`${fileRelative}: ${zone.kind} code directly accesses browser environment capabilities`);
}
}
if (zone.kind === 'platform' && (/\.(?:(?:c|m)?(?:jsx|tsx))$/u.test(fileRelative) || analysis.dependencies.includes('react'))) {
violations.push(`${fileRelative}: Desktop adapters cannot own React UI`);
}
if (['application', 'bootstrap', 'composition', 'feature', 'platform', 'shell'].includes(zone.kind) && analysis.unresolvedDependencies > 0) {
violations.push(`${fileRelative}: ${zone.kind} code contains a non-static import or require`);
}
}
function validateMetric(path, metric, actual, expected, violations) {
if (metric === 'actionFactories') {
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
violations.push(`${path}: ${metric} changed; expected ${JSON.stringify(expected)}, received ${JSON.stringify(actual)}`);
}
return;
}
if (typeof expected === 'number') {
if (actual !== expected) violations.push(`${path}: ${metric} changed; expected ${expected}, received ${actual}`);
return;
}
if (!sameJson(actual, expected)) {
violations.push(`${path}: ${metric} changed; expected ${JSON.stringify(expected)}, received ${JSON.stringify(actual)}`);
}
}
function validateDebtFile(desktopRoot, path, expected, violations, metrics = ROOT_DEBT_METRICS) {
const absolutePath = resolve(desktopRoot, path);
if (!existsSync(absolutePath)) {
violations.push(`${path}: debt ledger entry points to a missing file`);
return;
}
const analysis = analyzeRendererSource(readFileSync(absolutePath, 'utf8'), path);
for (const metric of metrics) {
validateMetric(path, metric, analysis[metric], expected[metric], violations);
}
}
function validateLegacyLedger(desktopRoot, config, violations) {
const rendererRoot = resolve(desktopRoot, 'src/renderer');
const actualLegacyFiles = sourceFiles(rendererRoot)
.map((path) => normalizePath(relative(desktopRoot, path)))
.filter((path) => zoneFor(path).kind === 'legacy')
.sort();
const expectedLegacyFiles = [...config.legacyRendererFiles].sort();
if (JSON.stringify(actualLegacyFiles) !== JSON.stringify(expectedLegacyFiles)) {
violations.push(
`unclassified legacy renderer file set changed; expected ${JSON.stringify(expectedLegacyFiles)}, received ${JSON.stringify(actualLegacyFiles)}`,
);
}
const actualFiles = readdirSync(rendererRoot, { withFileTypes: true })
.filter((entry) => entry.isFile() && LEGACY_APP_SHELL_FILE.test(entry.name))
.map((entry) => `src/renderer/${entry.name}`)
.sort();
const expectedFiles = Object.keys(config.legacyAppShell.files).sort();
if (JSON.stringify(actualFiles) !== JSON.stringify(expectedFiles)) {
violations.push(`legacy AppShell file set changed; expected ${JSON.stringify(expectedFiles)}, received ${JSON.stringify(actualFiles)}`);
}
for (const [path, expected] of Object.entries(config.legacyAppShell.files)) {
validateDebtFile(desktopRoot, path, expected, violations);
}
const actualClosure = collectRootDependencyClosure(desktopRoot, expectedFiles, violations, 'AppShell');
const expectedClosure = Object.keys(config.legacyAppShell.closure).sort();
if (JSON.stringify(actualClosure) !== JSON.stringify(expectedClosure)) {
violations.push(
`legacy AppShell transitive renderer closure changed; expected ${JSON.stringify(expectedClosure)}, received ${JSON.stringify(actualClosure)}`,
);
}
for (const [path, expected] of Object.entries(config.legacyAppShell.closure)) {
if (config.legacyAppShell.files[path]) {
violations.push(`${path}: AppShell root file must not also appear in its transitive closure`);
continue;
}
if (!isRootClosureDebtSource(path) || DECLARATION_FILE.test(path)) {
violations.push(`${path}: AppShell closure debt must point to a non-owner Desktop source`);
}
validateDebtFile(desktopRoot, path, expected, violations, CAPABILITY_DEBT_METRICS);
}
for (const [path, expected] of Object.entries(config.rootDebt)) {
validateDebtFile(desktopRoot, path, expected, violations);
}
const appShellDebtPaths = new Set([...expectedFiles, ...expectedClosure]);
const rootDebtPaths = Object.keys(config.rootDebt).sort();
const actualRootClosure = collectRootDependencyClosure(desktopRoot, rootDebtPaths, violations, 'renderer root')
.filter((path) => !appShellDebtPaths.has(path));
const expectedRootClosure = Object.keys(config.rootDebtClosure).sort();
if (JSON.stringify(actualRootClosure) !== JSON.stringify(expectedRootClosure)) {
violations.push(
`renderer root transitive legacy closure changed; expected ${JSON.stringify(expectedRootClosure)}, received ${JSON.stringify(actualRootClosure)}`,
);
}
for (const [path, expected] of Object.entries(config.rootDebtClosure)) {
if (config.rootDebt[path] || appShellDebtPaths.has(path)) {
violations.push(`${path}: renderer root closure debt overlaps another root debt section`);
continue;
}
if (!isRootClosureDebtSource(path) || DECLARATION_FILE.test(path)) {
violations.push(`${path}: renderer root closure debt must point to a non-owner Desktop source`);
}
validateDebtFile(desktopRoot, path, expected, violations, CAPABILITY_DEBT_METRICS);
}
const ownedPaths = new Map();
for (const owner of config.ownership) {
if (!owner.capability || !owner.targetZone || !Array.isArray(owner.legacyPaths)) {
violations.push('ownership entries require capability, targetZone, and legacyPaths');
continue;
}
for (const path of owner.legacyPaths) {
if (ownedPaths.has(path)) {
violations.push(`${path}: assigned to both ${ownedPaths.get(path)} and ${owner.capability}`);
}
ownedPaths.set(path, owner.capability);
}
}
const expectedOwnedPaths = [...expectedFiles, ...Object.keys(config.rootDebt)];
for (const path of expectedOwnedPaths) {
if (!ownedPaths.has(path)) violations.push(`${path}: missing target owner in the architecture ledger`);
}
for (const path of ownedPaths.keys()) {
if (!config.legacyAppShell.files[path] && !config.rootDebt[path]) {
violations.push(`${path}: ownership ledger path is not recorded renderer root debt`);
}
}
}
function htmlAttribute(attributes, name) {
const match = attributes.match(
new RegExp(`(?:^|\\s)${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, 'iu'),
);
return match?.[1] ?? match?.[2];
}
function parseContractSource(desktopRoot, path, violations) {
const absolutePath = resolve(desktopRoot, path);
if (!existsSync(absolutePath)) {
violations.push(`${path}: renderer entry contract source is missing`);
return undefined;
}
try {
return parse(readFileSync(absolutePath, 'utf8'), {
createImportExpressions: true,
errorRecovery: false,
plugins: PARSER_PLUGINS.filter((plugin) => plugin !== 'jsx'),
sourceFilename: path,
sourceType: 'module',
}).program;
} catch (error) {
violations.push(
`${path}: could not parse renderer entry contract: ${error instanceof Error ? error.message : String(error)}`,
);
return undefined;
}
}
function nodesIn(program) {
if (!program) return [];
const nodes = [];
function visit(node) {
nodes.push(node);
for (const child of childNodes(node)) visit(child);
}
visit(program);
return nodes;
}
function objectPropertyValues(object, name) {
if (object?.type !== 'ObjectExpression') return [];
return object.properties
.filter(
(property) =>
property.type === 'ObjectProperty' &&
!property.computed &&
memberName(property.key) === name,
)
.map((property) => unwrapExpression(property.value));
}
function isIdentifier(node, name) {
return unwrapExpression(node)?.type === 'Identifier' && unwrapExpression(node).name === name;
}
function hasNamedImport(program, source, importedName, localName = importedName) {
return program.body.some(
(node) =>
node.type === 'ImportDeclaration' &&
staticString(node.source) === source &&
node.specifiers.some(
(specifier) =>
specifier.type === 'ImportSpecifier' &&
memberName(specifier.imported) === importedName &&
specifier.local.name === localName,
),
);
}
function hasDefaultImport(program, source, localName) {
return program.body.some(
(node) =>
node.type === 'ImportDeclaration' &&
staticString(node.source) === source &&
node.specifiers.some(
(specifier) =>
specifier.type === 'ImportDefaultSpecifier' && specifier.local.name === localName,
),
);
}
function isNamedMember(node, objectName, propertyName) {
const expression = unwrapExpression(node);
return (
isMemberExpression(expression) &&
isIdentifier(expression.object, objectName) &&
memberPropertyName(expression) === propertyName
);
}
function isMemberCall(node, objectName, methodName, argumentObject, argumentProperty) {
const expression = unwrapExpression(node);
return (
expression?.type === 'CallExpression' &&
isMemberExpression(expression.callee) &&
isIdentifier(expression.callee.object, objectName) &&
memberPropertyName(expression.callee) === methodName &&
expression.arguments.length === 1 &&
isNamedMember(expression.arguments[0], argumentObject, argumentProperty)
);
}
function isRendererEntryPathInitializer(node) {
const expression = unwrapExpression(node);
return (
expression?.type === 'CallExpression' &&
isIdentifier(expression.callee, 'join') &&
expression.arguments.length === 5 &&
isIdentifier(expression.arguments[0], 'mainModuleDirectory') &&
JSON.stringify(expression.arguments.slice(1).map(staticString)) ===
JSON.stringify(['..', '..', 'dist-renderer', 'index.html'])
);
}
function isRendererEntryUrlInitializer(node) {
const expression = unwrapExpression(node);
if (expression?.type !== 'LogicalExpression' || expression.operator !== '??') return false;
const left = unwrapExpression(expression.left);
const right = unwrapExpression(expression.right);
const viteDevServerUrl = isIdentifier(left, 'viteDevServerUrl');
const fileUrl =
isMemberExpression(right) &&
memberPropertyName(right) === 'href' &&
unwrapExpression(right.object)?.type === 'CallExpression' &&
isIdentifier(unwrapExpression(right.object).callee, 'pathToFileURL') &&
unwrapExpression(right.object).arguments.length === 1 &&
isIdentifier(unwrapExpression(right.object).arguments[0], 'rendererEntryPath');
return viteDevServerUrl && fileUrl;
}
function isViteDevServerFlag(node) {
const expression = unwrapExpression(node);
const inner = expression?.type === 'UnaryExpression' && expression.operator === '!' ? unwrapExpression(expression.argument) : undefined;
return (
inner?.type === 'UnaryExpression' &&
inner.operator === '!' &&
isIdentifier(inner.argument, 'viteDevServerUrl')
);
}
function frozenRendererEntryObject(node) {
const expression = unwrapExpression(node);
if (
expression?.type !== 'CallExpression' ||
expression.arguments.length !== 1 ||
!isMemberExpression(expression.callee) ||
expression.callee.computed ||
!isIdentifier(expression.callee.object, 'Object') ||
memberPropertyName(expression.callee) !== 'freeze'
) {
return undefined;
}
const object = unwrapExpression(expression.arguments[0]);
return object?.type === 'ObjectExpression' ? object : undefined;
}
function expressionRootIdentifier(node) {
let expression = unwrapExpression(node);
while (isMemberExpression(expression)) expression = unwrapExpression(expression.object);
return expression?.type === 'Identifier' ? expression.name : undefined;
}
function mutatesIdentifier(node, name) {
if (node.type === 'AssignmentExpression') return expressionRootIdentifier(node.left) === name;
if (node.type === 'UpdateExpression') return expressionRootIdentifier(node.argument) === name;
return (
node.type === 'UnaryExpression' &&
node.operator === 'delete' &&
expressionRootIdentifier(node.argument) === name
);
}
function navigationApiToken(node) {
if (node?.type === 'Identifier' && ['loadFile', 'loadURL'].includes(node.name)) return node.name;
const value = staticString(node);
if (['loadFile', 'loadURL'].includes(value ?? '')) return value;
return undefined;
}
function isOnlyAwaitedMemberCall(block, objectName, methodName, argumentObject, argumentProperty) {
if (block?.type !== 'BlockStatement' || block.body.length !== 1) return false;
const [statement] = block.body;
const expression = statement?.type === 'ExpressionStatement' ? unwrapExpression(statement.expression) : undefined;
return (
expression?.type === 'AwaitExpression' &&
isMemberCall(expression.argument, objectName, methodName, argumentObject, argumentProperty)
);
}
function isImportMetaDirectory(node) {
const expression = unwrapExpression(node);
return (
isMemberExpression(expression) &&
unwrapExpression(expression.object)?.type === 'MetaProperty' &&
unwrapExpression(expression.object).meta?.name === 'import' &&
unwrapExpression(expression.object).property?.name === 'meta' &&
memberPropertyName(expression) === 'dirname'
);
}
function isProcessViteDevServerUrl(node) {
const expression = unwrapExpression(node);
return (
isMemberExpression(expression) &&
isMemberExpression(expression.object) &&
isIdentifier(expression.object.object, 'process') &&
memberPropertyName(expression.object) === 'env' &&
memberPropertyName(expression) === 'VITE_DEV_SERVER_URL'
);
}
function validateMainRendererLoader(desktopRoot, violations) {
const program = parseContractSource(desktopRoot, MAIN_RENDERER_LOADER_SOURCE, violations);
if (!program) return;
const nodes = nodesIn(program);
const declarators = (name) =>
nodes.filter(
(node) =>
node.type === 'VariableDeclarator' &&
node.id?.type === 'Identifier' &&
node.id.name === name,
);
const entryPaths = declarators('rendererEntryPath');
const entryUrls = declarators('rendererEntryUrl');
const loadCalls = nodes.filter(
(node) =>
node.type === 'CallExpression' &&
isMemberExpression(node.callee) &&
['loadFile', 'loadURL'].includes(memberPropertyName(node.callee)),
);
const loaderFunctions = nodes.filter(
(node) => node.type === 'FunctionDeclaration' && node.id?.name === 'loadMainRenderer',
);
const resolverFunctions = nodes.filter(
(node) => node.type === 'FunctionDeclaration' && node.id?.name === 'resolveMainRendererEntry',
);
const [loaderFunction] = loaderFunctions;
const [resolverFunction] = resolverFunctions;
const ifStatements = loaderFunction ? nodesIn(loaderFunction.body).filter((node) => node.type === 'IfStatement') : [];
const [loadBranch] = ifStatements;
const resolverReturns = resolverFunction
? nodesIn(resolverFunction.body).filter((node) => node.type === 'ReturnStatement')
: [];
const [resolverReturn] = resolverReturns;
const frozenEntry = frozenRendererEntryObject(resolverReturn?.argument);
const returnFilePaths = objectPropertyValues(frozenEntry, 'filePath');
const returnUrls = objectPropertyValues(frozenEntry, 'url');
const returnDevFlags = objectPropertyValues(frozenEntry, 'useDevServer');
const navigationTokens = nodes.filter((node) => navigationApiToken(node));
const valid =
hasNamedImport(program, 'node:path', 'join') &&
hasNamedImport(program, 'node:url', 'pathToFileURL') &&
!program.body.some((statement) => statementBindings(statement).includes('Object')) &&
resolverFunctions.length === 1 &&
resolverFunction.async !== true &&
JSON.stringify(resolverFunction.params.map((parameter) => parameter.type === 'Identifier' ? parameter.name : undefined)) ===
JSON.stringify(['mainModuleDirectory', 'viteDevServerUrl']) &&
resolverFunction.body.body.length === 3 &&
resolverReturns.length === 1 &&
Boolean(frozenEntry) &&
returnFilePaths.length === 1 &&
isIdentifier(returnFilePaths[0], 'rendererEntryPath') &&
returnUrls.length === 1 &&
isIdentifier(returnUrls[0], 'rendererEntryUrl') &&
returnDevFlags.length === 1 &&
isViteDevServerFlag(returnDevFlags[0]) &&
loaderFunctions.length === 1 &&
loaderFunction.async === true &&
JSON.stringify(loaderFunction.params.map((parameter) => parameter.type === 'Identifier' ? parameter.name : undefined)) ===
JSON.stringify(['mainWindow', 'rendererEntry']) &&
loaderFunction.body.body.length === 1 &&
entryPaths.length === 1 &&
isRendererEntryPathInitializer(entryPaths[0].init) &&
entryUrls.length === 1 &&
isRendererEntryUrlInitializer(entryUrls[0].init) &&
loadCalls.length === 2 &&
loadCalls.filter((node) => isMemberCall(node, 'mainWindow', 'loadFile', 'rendererEntry', 'filePath')).length === 1 &&
loadCalls.filter((node) => isMemberCall(node, 'mainWindow', 'loadURL', 'rendererEntry', 'url')).length === 1 &&
navigationTokens.length === 4 &&
ifStatements.length === 1 &&
isNamedMember(loadBranch.test, 'rendererEntry', 'useDevServer') &&
isOnlyAwaitedMemberCall(loadBranch.consequent, 'mainWindow', 'loadURL', 'rendererEntry', 'url') &&
isOnlyAwaitedMemberCall(loadBranch.alternate, 'mainWindow', 'loadFile', 'rendererEntry', 'filePath');
if (!valid) {
violations.push(
`${MAIN_RENDERER_LOADER_SOURCE}: renderer loader must load only the pinned dist-renderer/index.html entry`,
);
}
}
function validateMainWindowEntryContract(desktopRoot, violations) {
const program = parseContractSource(desktopRoot, RENDERER_LOADER_SOURCE, violations);
if (!program) return;
const nodes = nodesIn(program);
const parents = buildParentMap(program);
const loaderImports = program.body.filter(
(node) =>
node.type === 'ImportDeclaration' &&
staticString(node.source) === './main-renderer-loader.js' &&
['loadMainRenderer', 'resolveMainRendererEntry'].every((name) =>
node.specifiers.some(
(specifier) =>
specifier.type === 'ImportSpecifier' &&
memberName(specifier.imported) === name &&
specifier.local.name === name,
),
),
);
const entryDeclarators = nodes.filter(
(node) =>
node.type === 'VariableDeclarator' &&
node.id?.type === 'Identifier' &&
node.id.name === 'rendererEntry' &&
unwrapExpression(node.init)?.type === 'CallExpression' &&
isIdentifier(unwrapExpression(node.init).callee, 'resolveMainRendererEntry') &&
unwrapExpression(node.init).arguments.length === 2 &&
isImportMetaDirectory(unwrapExpression(node.init).arguments[0]) &&
isProcessViteDevServerUrl(unwrapExpression(node.init).arguments[1]),
);
const loaderCalls = nodes.filter(
(node) =>
node.type === 'CallExpression' &&
isIdentifier(node.callee, 'loadMainRenderer') &&
node.arguments.length === 2 &&
isIdentifier(node.arguments[0], 'mainWindow') &&
isIdentifier(node.arguments[1], 'rendererEntry'),
);
const entryDeclaration = entryDeclarators.length === 1 ? parents.get(entryDeclarators[0]) : undefined;
const entryMutations = nodes.filter((node) => mutatesIdentifier(node, 'rendererEntry'));
if (
loaderImports.length !== 1 ||
entryDeclarators.length !== 1 ||
entryDeclaration?.type !== 'VariableDeclaration' ||
entryDeclaration.kind !== 'const' ||
loaderCalls.length !== 1 ||
entryMutations.length > 0 ||
nodes.some((node) => navigationApiToken(node))
) {
violations.push(
`${RENDERER_LOADER_SOURCE}: main window must delegate exactly once to the pinned renderer loader`,
);
}
const allowedNavigationFiles = new Set([
'src/main/browser/controller.ts',
'src/main/computer-use/cursor-overlay-window.ts',
'src/main/computer-use/pip-electron.ts',
MAIN_RENDERER_LOADER_SOURCE,
'src/main/permission-overlay/permission-overlay-main.ts',
]);
for (const file of sourceFiles(resolve(desktopRoot, 'src/main'))) {
const fileRelative = normalizePath(relative(desktopRoot, file));
if (isTestConsumer(fileRelative) || allowedNavigationFiles.has(fileRelative)) continue;
const sourceProgram = parseContractSource(desktopRoot, fileRelative, violations);
if (sourceProgram && nodesIn(sourceProgram).some((node) => navigationApiToken(node))) {
violations.push(`${fileRelative}: main-process navigation APIs are restricted to approved loaders`);
}
}
validateMainRendererLoader(desktopRoot, violations);
}
function validateViteEntryContract(desktopRoot, violations) {
const program = parseContractSource(desktopRoot, RENDERER_VITE_CONFIG, violations);
if (!program) return;
const defaultExports = program.body.filter((node) => node.type === 'ExportDefaultDeclaration');
const declaration = unwrapExpression(defaultExports[0]?.declaration);
const config =
defaultExports.length === 1 &&
declaration?.type === 'CallExpression' &&
isIdentifier(declaration.callee, 'defineConfig') &&
declaration.arguments.length === 1
? unwrapExpression(declaration.arguments[0])
: undefined;
const roots = objectPropertyValues(config, 'root');
const builds = objectPropertyValues(config, 'build');
const plugins = objectPropertyValues(config, 'plugins');
const outDirs = objectPropertyValues(builds[0], 'outDir');
const rollupOptions = objectPropertyValues(builds[0], 'rollupOptions');
const hasInputOverride = rollupOptions.some(
(options) => options?.type !== 'ObjectExpression' || objectPropertyValues(options, 'input').length > 0,
);
const hasContractSpread =
config?.type !== 'ObjectExpression' ||
config.properties.some((property) => property.type === 'SpreadElement') ||
builds[0]?.type !== 'ObjectExpression' ||
builds[0].properties.some((property) => property.type === 'SpreadElement');
const pluginElements = plugins[0]?.type === 'ArrayExpression' ? plugins[0].elements : [];
const pluginCall = (index, name, argumentCount) => {
const expression = unwrapExpression(pluginElements[index]);
return expression?.type === 'CallExpression' &&
isIdentifier(expression.callee, name) &&
expression.arguments.length === argumentCount
? expression
: undefined;
};
const reactCall = pluginCall(0, 'react', 0);
const dependencyPatchesCall = pluginCall(1, 'dependencyPatchesCachePlugin', 1);
const bundledPackagesCall = pluginCall(2, 'bundledNpmPackagesPlugin', 0);
const rendererContractCall = pluginCall(3, 'rendererEntryContractPlugin', 1);
const rendererContractRoot = unwrapExpression(rendererContractCall?.arguments[0]);
const hasPinnedRendererContractRoot =
rendererContractRoot?.type === 'CallExpression' &&
isIdentifier(rendererContractRoot.callee, 'resolve') &&
rendererContractRoot.arguments.length === 2 &&
isImportMetaDirectory(rendererContractRoot.arguments[0]) &&
staticString(rendererContractRoot.arguments[1]) === 'src/renderer';
const hasPinnedImports =
hasNamedImport(program, 'vite', 'defineConfig') &&
hasNamedImport(program, 'node:path', 'resolve') &&
hasDefaultImport(program, '@vitejs/plugin-react', 'react') &&
hasNamedImport(program, './vite-dependency-patches.js', 'dependencyPatchesCachePlugin') &&
hasNamedImport(program, './vite-bundled-packages.js', 'bundledNpmPackagesPlugin') &&
hasNamedImport(
program,
'./scripts/vite-renderer-entry-contract.js',
'rendererEntryContractPlugin',
);
const hasPinnedPlugins =
pluginElements.length === 4 &&
Boolean(reactCall) &&
Boolean(dependencyPatchesCall) &&
isIdentifier(dependencyPatchesCall.arguments[0], 'REPO_ROOT') &&
Boolean(bundledPackagesCall) &&
Boolean(rendererContractCall) &&
hasPinnedRendererContractRoot;
if (
roots.length !== 1 ||
staticString(roots[0]) !== 'src/renderer' ||
builds.length !== 1 ||
outDirs.length !== 1 ||
staticString(outDirs[0]) !== '../../dist-renderer' ||
plugins.length !== 1 ||
!hasPinnedImports ||
!hasPinnedPlugins ||
hasInputOverride ||
hasContractSpread
) {
violations.push(
`${RENDERER_VITE_CONFIG}: Vite must build src/renderer/index.html into dist-renderer without an input override`,
);
}
const packagePath = resolve(desktopRoot, 'package.json');
let buildScript;
try {
buildScript = JSON.parse(readFileSync(packagePath, 'utf8')).scripts?.['build:renderer'];
} catch {
buildScript = undefined;
}
if (buildScript !== RENDERER_BUILD_SCRIPT) {
violations.push('package.json: build:renderer must retain the pinned Vite renderer entry command');
}
}
function validateRendererEntryContract(desktopRoot, config, violations) {
const indexPath = resolve(desktopRoot, RENDERER_ENTRY_INDEX);
const entryPath = resolve(desktopRoot, RENDERER_ENTRY_SOURCE);
if (!existsSync(indexPath)) {
violations.push(`${RENDERER_ENTRY_INDEX}: renderer entry index is missing`);
return;
}
if (!existsSync(entryPath)) {
violations.push(`${RENDERER_ENTRY_SOURCE}: renderer entry source is missing`);
}
if (!Object.hasOwn(config.rootDebt, RENDERER_ENTRY_SOURCE)) {
violations.push(`${RENDERER_ENTRY_SOURCE}: renderer entry must retain its permanent rootDebt guard`);
}
const html = readFileSync(indexPath, 'utf8');
const openingScriptCount = html.match(/<script\b/giu)?.length ?? 0;
const scripts = [...html.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script\s*>/giu)];
const [script] = scripts;
const isPinnedEntry =
openingScriptCount === 1 &&
scripts.length === 1 &&
htmlAttribute(script?.[1] ?? '', 'type') === 'module' &&
htmlAttribute(script?.[1] ?? '', 'src') === RENDERER_ENTRY_SCRIPT_SOURCE &&
(script?.[2] ?? '').trim().length === 0;
if (!isPinnedEntry) {
violations.push(
`${RENDERER_ENTRY_INDEX}: renderer entry contract requires exactly one empty external module script for ${RENDERER_ENTRY_SCRIPT_SOURCE}`,
);
}
validateMainWindowEntryContract(desktopRoot, violations);
validateViteEntryContract(desktopRoot, violations);
}
function metricTotal(value) {
if (typeof value === 'number') return value;
if (Array.isArray(value)) return value.length;
return Object.values(value).reduce((total, count) => total + count, 0);
}
function allowsMigrationDependency({ base, current, dependency, desktopRoot, path, section }) {
if (metricTotal(current.dependencyPaths) > metricTotal(base.dependencyPaths)) return false;
const target = resolveDependency(desktopRoot, resolve(desktopRoot, path), dependency);
if (!target) return false;
const targetRelative = normalizePath(relative(desktopRoot, target));
const targetZone = zoneFor(targetRelative);
if (section === 'legacyAppShell' || section === 'legacyAppShellClosure') {
if (targetZone.kind === 'shell' || isPublicApplicationPath(targetRelative)) return true;
const targetFeature = featureForAbsolutePath(desktopRoot, target);
return Boolean(targetFeature && isPublicFeaturePath(targetFeature.subpath));
}
if (section === 'rootDebt') return ['bootstrap', 'composition'].includes(targetZone.kind);
return ['application', 'bootstrap', 'composition', 'platform'].includes(targetZone.kind);
}
function debtForPath(desktopRoot, path) {
return debtMetrics(analyzeRendererSource(readFileSync(resolve(desktopRoot, path), 'utf8'), path));
}
function capabilityDebtForPath(desktopRoot, path) {
return capabilityDebtMetrics(analyzeRendererSource(readFileSync(resolve(desktopRoot, path), 'utf8'), path));
}
function collectLegacyImportEdges(desktopRoot) {
const feature = new Set();
const platform = new Set();
for (const file of sourceFiles(resolve(desktopRoot, 'src'))) {
const fileRelative = normalizePath(relative(desktopRoot, file));
const sourceZone = zoneFor(fileRelative);
if (sourceZone.kind !== 'feature' && sourceZone.kind !== 'platform') continue;
const analysis = analyzeRendererSource(readFileSync(file, 'utf8'), fileRelative);
for (const dependency of analysis.dependencies) {
const target = resolveDependency(desktopRoot, file, dependency);
if (!target) continue;
const targetRelative = normalizePath(relative(desktopRoot, target));
if (zoneFor(targetRelative).kind !== 'legacy') continue;
const edge = `${fileRelative} -> ${targetRelative}`;
if (sourceZone.kind === 'feature') feature.add(edge);
else platform.add(edge);
}
}
return { feature: [...feature].sort(), platform: [...platform].sort() };
}
export function generateArchitectureConfig(desktopRoot, config) {
const rendererRoot = resolve(desktopRoot, 'src/renderer');
const legacyRendererFiles = sourceFiles(rendererRoot)
.map((path) => normalizePath(relative(desktopRoot, path)))
.filter((path) => zoneFor(path).kind === 'legacy')
.sort();
const appShellFiles = readdirSync(rendererRoot, { withFileTypes: true })
.filter((entry) => entry.isFile() && LEGACY_APP_SHELL_FILE.test(entry.name))
.map((entry) => `src/renderer/${entry.name}`)
.sort();
const closureViolations = [];
const closureFiles = collectRootDependencyClosure(desktopRoot, appShellFiles, closureViolations, 'AppShell');
if (closureViolations.length > 0) {
throw new Error(`cannot generate AppShell closure:\n${closureViolations.join('\n')}`);
}
const imports = collectLegacyImportEdges(desktopRoot);
const rootDebt = {};
for (const path of Object.keys(config.rootDebt ?? {}).sort()) {
if (existsSync(resolve(desktopRoot, path))) rootDebt[path] = debtForPath(desktopRoot, path);
}
const appShellDebtPaths = new Set([...appShellFiles, ...closureFiles]);
const rootDebtClosureFiles = collectRootDependencyClosure(
desktopRoot,
Object.keys(rootDebt),
closureViolations,
'renderer root',
).filter((path) => !appShellDebtPaths.has(path));
if (closureViolations.length > 0) {
throw new Error(`cannot generate renderer root closure:\n${closureViolations.join('\n')}`);
}
return {
version: 1,
legacyRendererFiles,
legacyGrowthDirectories: config.legacyGrowthDirectories ?? DEFAULT_LEGACY_GROWTH_DIRECTORIES,
legacyFeatureImports: imports.feature,
legacyPlatformImports: imports.platform,
legacyAppShell: {
files: Object.fromEntries(appShellFiles.map((path) => [path, debtForPath(desktopRoot, path)])),
closure: Object.fromEntries(closureFiles.map((path) => [path, capabilityDebtForPath(desktopRoot, path)])),
},
rootDebt,
rootDebtClosure: Object.fromEntries(
rootDebtClosureFiles.map((path) => [path, capabilityDebtForPath(desktopRoot, path)]),
),
ownership: config.ownership ?? [],
};
}
function validateMonotonicDebt(config, baseConfig, desktopRoot, violations) {
if (!baseConfig) return;
for (const section of ['legacyAppShell', 'legacyAppShellClosure', 'rootDebt', 'rootDebtClosure']) {
const currentFiles =
section === 'legacyAppShell'
? config.legacyAppShell.files
: section === 'legacyAppShellClosure'
? config.legacyAppShell.closure
: section === 'rootDebt'
? config.rootDebt
: config.rootDebtClosure;
const baseFiles =
section === 'legacyAppShell'
? baseConfig.legacyAppShell.files
: section === 'legacyAppShellClosure'
? baseConfig.legacyAppShell.closure
: section === 'rootDebt'
? baseConfig.rootDebt
: baseConfig.rootDebtClosure;
for (const path of Object.keys(currentFiles)) {
const shiftedAppShellBase =
section === 'rootDebtClosure' ? baseConfig.legacyAppShell.closure[path] : undefined;
if (!baseFiles[path] && !shiftedAppShellBase) {
violations.push(`${path}: new ${section} debt entries are forbidden`);
}
}
if (section === 'rootDebt') {
for (const path of Object.keys(baseFiles)) {
if (!currentFiles[path] && existsSync(resolve(desktopRoot, path))) {
violations.push(`${path}: permanent root entry guard cannot be removed while the source exists`);
}
}
}
for (const [path, current] of Object.entries(currentFiles)) {
const base =
baseFiles[path] ??
(section === 'rootDebtClosure' ? baseConfig.legacyAppShell.closure[path] : undefined);
if (!base) continue;
const metrics = section.endsWith('Closure') ? CAPABILITY_DEBT_METRICS : ROOT_DEBT_METRICS;
for (const metric of metrics) {
if (metricTotal(current[metric]) > metricTotal(base[metric])) {
violations.push(`${path}: ${metric} debt increased from ${metricTotal(base[metric])} to ${metricTotal(current[metric])}`);
}
if (['bridgePaths', 'environmentCapabilities', 'hookCalls', 'lifecycleMethods'].includes(metric)) {
const increases = Object.fromEntries(
Object.entries(current[metric])
.map(([key, count]) => [key, Math.max(0, count - (base[metric][key] ?? 0))])
.filter(([, count]) => count > 0),
);
for (const [key, count] of Object.entries(increases)) {
if (count > 0) {
violations.push(`${path}: new or increased ${metric} debt ${key}`);
}
}
}
if (metric === 'actionFactories') {
for (const factory of current.actionFactories) {
if (!base.actionFactories.includes(factory)) violations.push(`${path}: new action factory debt ${factory}`);
}
}
if (metric === 'dependencyPaths') {
for (const [dependency, count] of Object.entries(current.dependencyPaths)) {
if (
count > (base.dependencyPaths[dependency] ?? 0) &&
!allowsMigrationDependency({ base, current, dependency, desktopRoot, path, section })
) {
violations.push(`${path}: new dependency debt ${dependency}`);
}
}
}
}
}
}
const baseLegacyFiles = new Set(baseConfig.legacyRendererFiles);
for (const path of config.legacyRendererFiles) {
if (!baseLegacyFiles.has(path) && !isAllowedLegacyGrowthPath(config, path)) {
violations.push(`${path}: new unclassified renderer source files are forbidden outside approved legacy directories`);
}
}
const baseLegacyGrowthDirectories = new Set(baseConfig.legacyGrowthDirectories);
for (const path of config.legacyGrowthDirectories) {
if (!baseLegacyGrowthDirectories.has(path)) {
violations.push(`${path}: new legacy growth directories are forbidden`);
}
}
const baseLegacyFeatureImports = new Set(baseConfig.legacyFeatureImports);
for (const edge of config.legacyFeatureImports) {
if (!baseLegacyFeatureImports.has(edge)) violations.push(`${edge}: new feature-to-legacy imports are forbidden`);
}
const baseLegacyPlatformImports = new Set(baseConfig.legacyPlatformImports);
for (const edge of config.legacyPlatformImports) {
if (!baseLegacyPlatformImports.has(edge)) violations.push(`${edge}: new platform-to-legacy imports are forbidden`);
}
}
export function checkRendererArchitecture({
desktopRoot,
config,
baseConfig,
enforceRendererEntryContract = true,
} = {}) {
const resolvedDesktopRoot = resolve(desktopRoot ?? fileURLToPath(new URL('..', import.meta.url)));
const resolvedConfig = config ?? JSON.parse(readFileSync(join(resolvedDesktopRoot, 'renderer-architecture.json'), 'utf8'));
const violations = [];
const currentConfigValid = validateArchitectureConfig(resolvedConfig, 'current', violations);
const baseConfigValid = baseConfig ? validateArchitectureConfig(baseConfig, 'base', violations) : true;
if (!currentConfigValid || !baseConfigValid) return violations.sort();
if (enforceRendererEntryContract) {
validateRendererEntryContract(resolvedDesktopRoot, resolvedConfig, violations);
}
validateLegacyLedger(resolvedDesktopRoot, resolvedConfig, violations);
validateMonotonicDebt(resolvedConfig, baseConfig, resolvedDesktopRoot, violations);
const allowedLegacyFeatureImports = new Set(resolvedConfig.legacyFeatureImports);
const allowedLegacyPlatformImports = new Set(resolvedConfig.legacyPlatformImports);
const observedLegacyFeatureImports = new Set();
const observedLegacyPlatformImports = new Set();
for (const scanRoot of ['src', 'stories', 'e2e']) {
for (const file of sourceFiles(resolve(resolvedDesktopRoot, scanRoot))) {
const fileRelative = normalizePath(relative(resolvedDesktopRoot, file));
let analysis;
try {
analysis = analyzeRendererSource(readFileSync(file, 'utf8'), fileRelative);
} catch (error) {
violations.push(`${fileRelative}: could not parse source: ${error instanceof Error ? error.message : String(error)}`);
continue;
}
validateStrictZone({ fileRelative, analysis, violations });
validateDependencies({
allowedLegacyFeatureImports,
allowedLegacyPlatformImports,
analysis,
desktopRoot: resolvedDesktopRoot,
file,
fileRelative,
observedLegacyFeatureImports,
observedLegacyPlatformImports,
violations,
});
}
}
for (const edge of allowedLegacyFeatureImports) {
if (!observedLegacyFeatureImports.has(edge)) violations.push(`${edge}: stale feature-to-legacy import budget`);
}
for (const edge of allowedLegacyPlatformImports) {
if (!observedLegacyPlatformImports.has(edge)) violations.push(`${edge}: stale platform-to-legacy import budget`);
}
return violations.sort();
}
// The monotonic-debt ratchet must measure debt against what the base commit's
// source tree *actually* contained, not against the numbers its ledger happened
// to record. A ledger that under-reports its own tree (for example, one
// generated on a branch that predated files already merged into main) would
// otherwise make a faithful baseline correction look like brand-new debt and
// wedge the ledger permanently. We materialize the base tree and re-derive its
// debt, keeping the base ledger only as the source of policy fields (hook
// transitions, growth directories, root-debt key set, ownership).
function deriveBaseTreeConfig(repoRoot, desktopRoot, base, baseCommittedConfig) {
const scratch = mkdtempSync(join(tmpdir(), 'renderer-arch-base-'));
const worktreePath = join(scratch, 'tree');
try {
execFileSync('git', ['worktree', 'add', '--detach', worktreePath, base], {
cwd: repoRoot,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
const baseDesktopRoot = resolve(worktreePath, relative(repoRoot, desktopRoot));
return generateArchitectureConfig(baseDesktopRoot, baseCommittedConfig);
} finally {
try {
execFileSync('git', ['worktree', 'remove', '--force', worktreePath], {
cwd: repoRoot,
stdio: 'ignore',
});
} catch {
try {
execFileSync('git', ['worktree', 'prune'], { cwd: repoRoot, stdio: 'ignore' });
} catch {
// Ignore prune failures; the scratch removal below is the real cleanup.
}
}
try {
rmSync(scratch, { recursive: true, force: true });
} catch {
// Best-effort cleanup of the scratch directory.
}
}
}
function loadBaseConfig(repoRoot, desktopRoot, base) {
if (!base) return { baseConfig: undefined, introducedLedger: false };
const relativeConfig = normalizePath(relative(repoRoot, join(desktopRoot, 'renderer-architecture.json')));
try {
execFileSync('git', ['rev-parse', '--verify', `${base}^{commit}`], {
cwd: repoRoot,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
});
} catch {
throw new Error(`base ref does not resolve to a commit: ${base}`);
}
let source;
try {
source = execFileSync('git', ['show', `${base}:${relativeConfig}`], {
cwd: repoRoot,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
});
} catch {
const diffStatus = execFileSync('git', ['diff', '--name-status', base, '--', relativeConfig], {
cwd: repoRoot,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
const worktreeStatus = execFileSync(
'git',
['status', '--porcelain=v1', '--untracked-files=all', '--', relativeConfig],
{
cwd: repoRoot,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
},
).trim();
if (diffStatus === `A\t${relativeConfig}` || worktreeStatus === `?? ${relativeConfig}`) {
return { baseConfig: undefined, introducedLedger: true };
}
throw new Error(`base ledger is missing at ${base}:${relativeConfig}`);
}
let baseCommittedConfig;
try {
baseCommittedConfig = JSON.parse(source);
} catch (error) {
throw new Error(
`base ledger is invalid JSON at ${base}:${relativeConfig}: ${error instanceof Error ? error.message : String(error)}`,
);
}
try {
return {
baseConfig: deriveBaseTreeConfig(repoRoot, desktopRoot, base, baseCommittedConfig),
introducedLedger: false,
};
} catch (error) {
// If the base tree cannot be materialized or analyzed (e.g. git worktree is
// unavailable), fall back to the committed base ledger so the ratchet still
// runs. This restores the pre-fix behavior rather than crashing the check.
console.warn(
`Renderer architecture check: could not derive base tree debt at ${base}; ` +
`falling back to the committed base ledger. (${error instanceof Error ? error.message : String(error)})`,
);
return { baseConfig: baseCommittedConfig, introducedLedger: false };
}
}
function parseCliArguments(args) {
let base;
let write = false;
for (let index = 0; index < args.length; index += 1) {
const argument = args[index];
if (argument === '--write' && !write) {
write = true;
continue;
}
if (argument === '--base' && base === undefined) {
const value = args[index + 1];
if (!value || value.startsWith('--')) {
throw new Error('usage: check-renderer-architecture.mjs [--write] [--base <commit>]');
}
base = value;
index += 1;
continue;
}
throw new Error('usage: check-renderer-architecture.mjs [--write] [--base <commit>]');
}
return { base, write };
}
function runCli() {
const desktopRoot = resolve(fileURLToPath(new URL('..', import.meta.url)));
const repoRoot = resolve(desktopRoot, '../..');
let base;
let config;
let loadedBase;
let write;
try {
({ base, write } = parseCliArguments(process.argv.slice(2)));
config = JSON.parse(readFileSync(join(desktopRoot, 'renderer-architecture.json'), 'utf8'));
loadedBase = loadBaseConfig(repoRoot, desktopRoot, base);
if (write) {
config = generateArchitectureConfig(desktopRoot, config);
writeFileSync(join(desktopRoot, 'renderer-architecture.json'), `${JSON.stringify(config, null, 2)}\n`);
console.log('Renderer architecture ledger updated.');
}
} catch (error) {
console.error(`Renderer architecture check could not start: ${error instanceof Error ? error.message : String(error)}`);
process.exitCode = 1;
return;
}
const { baseConfig, introducedLedger } = loadedBase;
const violations = checkRendererArchitecture({ baseConfig, config, desktopRoot });
if (violations.length > 0) {
console.error('Renderer architecture check failed:');
for (const violation of violations) console.error(`- ${violation}`);
process.exitCode = 1;
return;
}
if (introducedLedger) {
console.log(`Renderer architecture check passed; ${base} has no ledger and this change introduces it.`);
return;
}
console.log(`Renderer architecture check passed${baseConfig ? ` against ${base}` : ''}.`);
}
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) runCli();