From 88784ad52a27d58baa3d352009da3eb0a087414b Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sat, 5 Sep 2026 08:12:01 +0800 Subject: [PATCH] fix(sdk): close boundary alias bypasses --- .../architecture/import-boundaries.test.mjs | 81 ++++++++++++++++++- .../scripts/check-import-boundaries.mjs | 1 + .../scripts/lib/import-boundaries.mjs | 43 ++++++++-- 3 files changed, 117 insertions(+), 8 deletions(-) diff --git a/cocoscreator_projects/framework-tests/architecture/import-boundaries.test.mjs b/cocoscreator_projects/framework-tests/architecture/import-boundaries.test.mjs index 10c8ae9..5ae7a96 100644 --- a/cocoscreator_projects/framework-tests/architecture/import-boundaries.test.mjs +++ b/cocoscreator_projects/framework-tests/architecture/import-boundaries.test.mjs @@ -28,10 +28,11 @@ async function writeFixture(root, relativePath, source) { await writeFile(file, source, 'utf8'); } -function scan(root) { +function scan(root, tsconfigPath) { return scanImportBoundaries({ frameworkDir: join(root, 'framework'), gamesDir: join(root, 'games'), + tsconfigPath: tsconfigPath ? join(root, tsconfigPath) : undefined, }); } @@ -217,6 +218,84 @@ test('scanner rejects every sdk runtime dependency on framework implementation l } }); +test('scanner resolves and rejects an sdk runtime path alias into framework implementation', async () => { + const root = await createFixtureRoot(); + await writeFixture(root, 'framework/core/internal.ts', 'export const value = true\n'); + await writeFixture( + root, + 'framework/sdk/runtime/bad.ts', + "import { value } from '@framework/core/internal'; void value\n", + ); + await writeFixture(root, 'tsconfig.json', JSON.stringify({ + compilerOptions: { + baseUrl: '.', + paths: { '@framework/*': ['framework/*'] }, + module: 'ESNext', + moduleResolution: 'Bundler', + allowImportingTsExtensions: true, + }, + })); + + const violations = scan(root, 'tsconfig.json'); + assert.equal(violations.length, 1); + assert.equal(violations[0].specifier, '@framework/core/internal'); + assert.match(violations[0].message, /sdk.*framework implementation.*framework\/core/); +}); + +test('scanner rejects Cocos imports throughout sdk runtime', async () => { + for (const specifier of ['cc', 'cc/env']) { + const root = await createFixtureRoot(); + await writeFixture( + root, + 'framework/sdk/runtime/bad.ts', + `import { Node } from '${specifier}'; void Node\n`, + ); + + const violations = scan(root); + assert.equal(violations.length, 1, specifier); + assert.equal(violations[0].specifier, specifier); + assert.match(violations[0].message, /sdk.*Cocos/); + } +}); + +test('sdk migration barrel exception permits only its four retained legacy dependencies', async () => { + const root = await createFixtureRoot(); + for (const file of [ + 'framework/core/events.ts', + 'framework/core/reactive.ts', + 'framework/platform/readonly.ts', + 'framework/platform/stores/types.ts', + 'framework/core/constants.ts', + ]) { + await writeFixture(root, file, 'export interface Value {}\n'); + } + await writeFixture(root, 'framework/sdk/index.ts', [ + "import type { Value as Events } from '../core/events.ts'; void (0 as unknown as Events);", + "import type { Value as Reactive } from '../core/reactive.ts'; void (0 as unknown as Reactive);", + "import type { Value as ReadonlyStore } from '../platform/readonly.ts'; void (0 as unknown as ReadonlyStore);", + "import type { Value as StoreTypes } from '../platform/stores/types.ts'; void (0 as unknown as StoreTypes);", + "import type { Value as Constants } from '../core/constants.ts'; void (0 as unknown as Constants);", + ].join('\n')); + + const violations = scan(root); + assert.equal(violations.length, 1); + assert.equal(violations[0].specifier, '../core/constants.ts'); + assert.match(violations[0].message, /sdk.*framework implementation/); +}); + +test('scanner permits sdk-internal relatives and ordinary external pure dependencies', async () => { + const root = await createFixtureRoot(); + await writeFixture(root, 'framework/sdk/contracts/index.ts', 'export interface Contract {}\n'); + await writeFixture(root, 'framework/sdk/runtime/helper.ts', 'export const helper = true\n'); + await writeFixture(root, 'framework/sdk/runtime/allowed.ts', [ + "import type { Contract } from '../contracts/index.ts'; void (0 as unknown as Contract);", + "import { helper } from './helper.ts'; void helper;", + "import type { PureValue } from 'pure-external'; void (0 as unknown as PureValue);", + ].join('\n')); + + assert.deepEqual(scan(root), []); +}); + test('scanner rejects game imports of framework net internals', async () => { const root = await createFixtureRoot(); await writeFixture(root, 'games/a/assets/game/bad.ts', "import '../../../framework/net/net-client.ts'\n"); diff --git a/cocoscreator_projects/scripts/check-import-boundaries.mjs b/cocoscreator_projects/scripts/check-import-boundaries.mjs index 80efaef..105ed15 100644 --- a/cocoscreator_projects/scripts/check-import-boundaries.mjs +++ b/cocoscreator_projects/scripts/check-import-boundaries.mjs @@ -6,6 +6,7 @@ const projectDir = resolve(fileURLToPath(new URL('..', import.meta.url))); const violations = scanImportBoundaries({ frameworkDir: resolve(projectDir, 'YouleNexus/assets/framework'), gamesDir: resolve(projectDir, 'games'), + tsconfigPath: resolve(projectDir, 'tsconfig.framework.json'), }); if (violations.length > 0) { diff --git a/cocoscreator_projects/scripts/lib/import-boundaries.mjs b/cocoscreator_projects/scripts/lib/import-boundaries.mjs index 7a03049..ad78ca3 100644 --- a/cocoscreator_projects/scripts/lib/import-boundaries.mjs +++ b/cocoscreator_projects/scripts/lib/import-boundaries.mjs @@ -7,6 +7,12 @@ const FRAMEWORK_INTERNAL = /framework[\\/](?:net|protocol|platform|application|d const LEGACY_RUNTIME = /framework[\\/](?:net[\\/]net-client|platform[\\/](?:session|startup|room-rpc-bus|readonly|stores[\\/](?:app-store|player-store|room-store|types))|protocol[\\/]room-handlers)\.ts$/; const CONTRACTS_PATH = /framework[\\/]sdk[\\/]contracts(?:[\\/]|$)/; const MIGRATION_ONLY_NAMES = new Set(['GameContext', 'IGameModule']); +const SDK_INDEX_MIGRATION_DEPENDENCIES = new Set([ + 'core/events.ts', + 'core/reactive.ts', + 'platform/readonly.ts', + 'platform/stores/types.ts', +]); /** * Recursively scans TypeScript imports for framework/game layering violations. @@ -15,14 +21,28 @@ const MIGRATION_ONLY_NAMES = new Set(['GameContext', 'IGameModule']); export function scanImportBoundaries(options) { const frameworkDir = resolve(options.frameworkDir); const gamesDir = resolve(options.gamesDir); + const compilerOptions = readCompilerOptions(options.tsconfigPath); + const resolutionBase = options.tsconfigPath + ? dirname(resolve(options.tsconfigPath)) + : frameworkDir; + const moduleCache = ts.createModuleResolutionCache( + resolutionBase, + (file) => pathKey(canonicalPath(file)), + compilerOptions, + ); const violations = []; for (const file of [...typescriptFiles(frameworkDir), ...typescriptFiles(gamesDir)]) { const source = readFileSync(file, 'utf8'); for (const imported of extractImports(source, file)) { - const resolved = imported.specifier.startsWith('.') - ? resolve(dirname(file), imported.specifier) - : null; + const resolved = resolveDependency( + imported.specifier, + file, + compilerOptions, + moduleCache, + ) ?? (imported.specifier.startsWith('.') + ? canonicalPath(resolve(dirname(file), imported.specifier)) + : null); const violation = findViolation({ file, specifier: imported.specifier, @@ -233,11 +253,15 @@ function findViolation(context) { return violation(filePath, specifier, `sdk/contracts may import only sibling contracts; sdk path resolved to ${resolvedPath}`); } + if (isSdk && isCocosSpecifier(specifier)) { + return violation(filePath, specifier, `sdk cannot import Cocos module ${specifier}`); + } + if (isSdk - && !isSdkMigrationBarrel(filePath, frameworkDir) && resolved && isInside(resolved, frameworkDir) - && !SDK_ALLOWED.test(resolvedPath)) { + && !SDK_ALLOWED.test(resolvedPath) + && !isSdkIndexMigrationDependency(filePath, resolved, frameworkDir)) { return violation(filePath, specifier, `sdk cannot import framework implementation ${resolvedPath}`); } @@ -273,8 +297,13 @@ function isLegacyRuntimeImporter(filePath, frameworkDir) { return filePath === `${normalizedFrameworkDir}/sdk/index.ts` || LEGACY_RUNTIME.test(filePath); } -function isSdkMigrationBarrel(filePath, frameworkDir) { - return filePath === `${displayPath(frameworkDir)}/sdk/index.ts`; +function isSdkIndexMigrationDependency(filePath, resolved, frameworkDir) { + if (filePath !== `${displayPath(frameworkDir)}/sdk/index.ts`) return false; + return SDK_INDEX_MIGRATION_DEPENDENCIES.has(displayPath(relative(frameworkDir, resolved))); +} + +function isCocosSpecifier(specifier) { + return specifier === 'cc' || specifier.startsWith('cc/'); } /**