fix(03-05): close data package type and cycle boundaries

This commit is contained in:
unanmed 2026-09-10 17:40:29 +08:00
parent 86a4385261
commit 64b97bc515
10 changed files with 185 additions and 15 deletions

View File

@ -1,7 +1,6 @@
import {
Hookable,
HookController,
IFacedTileLocator,
IHookController,
logger
} from '@motajs/common';
@ -9,6 +8,7 @@ import {
FaceDirection,
IDataCommon,
IFaceHandler,
IFacedTileLocator,
SaveCompression
} from '@user/data-common';
import { HeroLocation } from './location';

View File

@ -1,10 +1,14 @@
import {
Hookable,
HookController,
IFacedTileLocator,
IHookController
} from '@motajs/common';
import { FaceDirection, IDataCommon, IFaceHandler } from '@user/data-common';
import {
FaceDirection,
IDataCommon,
IFaceHandler,
IFacedTileLocator
} from '@user/data-common';
import { HeroMover } from './mover';
import {
IHeroLocation,

View File

@ -23,12 +23,12 @@ import {
FaceDirection,
IDataCommon,
IFaceHandler,
IFacedTileLocator,
SaveCompression
} from '@user/data-common';
import {
Hookable,
HookController,
IFacedTileLocator,
IHookController,
logger
} from '@motajs/common';

View File

@ -1,5 +1,4 @@
import {
IFacedTileLocator,
IHookBase,
IHookable,
ITileLocator
@ -8,6 +7,7 @@ import {
FaceDirection,
IDataCommonExtended,
IFaceHandler,
IFacedTileLocator,
IItemRawData,
IObjectMovable,
IObjectMover,

View File

@ -1,7 +1,7 @@
import { logger } from '@motajs/common';
import { IFaceData, IRoleFaceBinder } from '../common';
import { IFaceData, IRoleFaceBinder } from './types';
import { isNil } from 'lodash-es';
import { FaceDirection } from '.';
import { FaceDirection } from './types';
interface FaceInfo {
/** 此图块的朝向 */

View File

@ -1,3 +1,5 @@
import { ITileLocator } from '@motajs/common';
export const enum FaceDirection {
Unknown,
Left,
@ -10,6 +12,11 @@ export const enum FaceDirection {
RightDown
}
export interface IFacedTileLocator extends ITileLocator {
/** 图块朝向 */
direction: FaceDirection;
}
export interface IFaceData {
/** 图块数字 */
readonly identifier: number;

View File

@ -1,4 +1,4 @@
import { IFacedTileLocator } from '@motajs/common';
import { IFacedTileLocator } from '../common';
import { IReadonlyGameEvent } from '../event';
//#region tile

View File

@ -1,5 +1,3 @@
import { FaceDirection } from '@user/data-common';
export interface ISearchable4Dir {
/** 获取上侧元素 */
up(): ISearchable4Dir | null;
@ -144,11 +142,6 @@ export interface ITileLocator {
y: number;
}
export interface IFacedTileLocator extends ITileLocator {
/** 图块朝向 */
direction: FaceDirection;
}
export const enum InternalDirectionGroup {
/** 上下左右四方向 */
Dir4,

View File

@ -0,0 +1,65 @@
import madge from 'madge';
import { relative, resolve } from 'node:path';
const root = process.cwd();
const entries = [
'packages-user/data-common/src/index.ts',
'packages-user/data-base/src/index.ts',
'packages-user/data-system/src/index.ts',
'packages-user/data-state/src/index.ts',
'packages/common/src/index.ts'
];
const dataPrefixes = [
'packages-user/data-common/',
'packages-user/data-base/',
'packages-user/data-system/',
'packages-user/data-state/'
];
function toRelativePath(file: string): string {
const absolute = resolve(root, file);
return relative(root, absolute).replaceAll('\\', '/');
}
function isInScope(cycle: readonly string[]): boolean {
return cycle.some(file => {
const normalized = file.replaceAll('\\', '/');
return (
normalized.includes('packages/common/') ||
dataPrefixes.some(prefix => normalized.includes(prefix))
);
});
}
const graph = await madge(entries, {
baseDir: root,
tsConfig: resolve(root, 'tsconfig.json'),
fileExtensions: ['ts', 'tsx'],
detectiveOptions: { ts: { skipTypeImports: false } }
});
const cycles = graph.circular() as readonly (readonly string[])[];
const inScope = cycles.filter(isInScope);
const outsideScope = cycles.filter(cycle => !isInScope(cycle));
console.log(
`Circular diagnostics: ${cycles.length} total, ${inScope.length} in-scope, ${outsideScope.length} outside scope`
);
for (const [index, cycle] of inScope.entries()) {
console.log(`IN-SCOPE CYCLE ${index + 1}:`);
console.log(cycle.map(toRelativePath).join(' -> '));
}
for (const [index, cycle] of outsideScope.entries()) {
console.log(`OUTSIDE-SCOPE CYCLE ${index + 1}:`);
console.log(cycle.map(toRelativePath).join(' -> '));
}
if (inScope.length > 0) {
console.error(
'Circular gate failed: four-package, common/data-common, or transitive common cycles remain'
);
process.exit(1);
}
console.log(
'Circular gate passed: four data packages and the transitive common boundary are acyclic'
);

101
script/check-data-type.ts Normal file
View File

@ -0,0 +1,101 @@
import { spawnSync } from 'node:child_process';
import { isAbsolute, relative, resolve } from 'node:path';
const root = process.cwd();
const dataPackages = [
'packages-user/data-common/',
'packages-user/data-base/',
'packages-user/data-system/',
'packages-user/data-state/'
];
interface ITypeDiagnostic {
readonly file: string;
readonly line: number;
readonly column: number;
readonly code: string;
readonly message: string;
}
function toRelativePath(file: string): string {
const absolute = isAbsolute(file) ? file : resolve(root, file);
return relative(root, absolute).replaceAll('\\', '/');
}
function isDataPath(file: string): boolean {
const normalized = toRelativePath(file);
return dataPackages.some(prefix => normalized.startsWith(prefix));
}
function parseDiagnostics(output: string): {
readonly diagnostics: readonly ITypeDiagnostic[];
readonly unparsed: readonly string[];
} {
const diagnostics: ITypeDiagnostic[] = [];
const unparsed: string[] = [];
const pattern = /^(.*?\.(?:ts|tsx|vue))\((\d+),(\d+)\): error (TS\d+): (.*)$/;
for (const line of output.split(/\r?\n/)) {
if (!line.includes('error TS')) continue;
const match = pattern.exec(line);
if (!match) {
unparsed.push(line);
continue;
}
diagnostics.push({
file: toRelativePath(match[1]),
line: Number(match[2]),
column: Number(match[3]),
code: match[4],
message: match[5]
});
}
return { diagnostics, unparsed };
}
const command = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
const result = spawnSync(
command,
['exec', 'vue-tsc', '--noEmit', '--pretty', 'false'],
{ cwd: root, encoding: 'utf8', shell: process.platform === 'win32' }
);
if (result.error) {
console.error(`type gate could not execute vue-tsc: ${result.error.message}`);
process.exit(1);
}
const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
const { diagnostics, unparsed } = parseDiagnostics(output);
const inScope = diagnostics.filter(diagnostic => isDataPath(diagnostic.file));
const outsideScope = diagnostics.filter(
diagnostic => !isDataPath(diagnostic.file)
);
console.log(
`Type diagnostics: ${diagnostics.length} total, ${inScope.length} in-scope, ${outsideScope.length} outside scope`
);
for (const diagnostic of inScope) {
console.log(
`IN-SCOPE ${diagnostic.file}:${diagnostic.line}:${diagnostic.column} ${diagnostic.code}: ${diagnostic.message}`
);
}
for (const diagnostic of outsideScope) {
console.log(
`OUTSIDE-SCOPE ${diagnostic.file}:${diagnostic.line}:${diagnostic.column} ${diagnostic.code}: ${diagnostic.message}`
);
}
if (unparsed.length > 0) {
console.error('Unparsed TypeScript diagnostics; failing closed:');
for (const line of unparsed) console.error(line);
process.exit(1);
}
if (inScope.length > 0) {
console.error('Type gate failed on in-scope data-package diagnostics');
process.exit(1);
}
console.log('Type gate passed: zero in-scope data-package diagnostics');