import { afterEach, test } from 'node:test'; import assert from 'node:assert/strict'; import { existsSync } from 'node:fs'; import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { findCompatibilityDependency, scanImportBoundaries, } from '../../scripts/lib/import-boundaries.mjs'; const roots = []; afterEach(async () => { await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); }); async function createFixtureRoot() { const root = await mkdtemp(join(tmpdir(), 'youle-import-boundaries-')); roots.push(root); return root; } async function writeFixture(root, relativePath, source) { const file = join(root, relativePath); await mkdir(dirname(file), { recursive: true }); await writeFile(file, source, 'utf8'); } function scan(root, tsconfigPath) { return scanImportBoundaries({ frameworkDir: join(root, 'framework'), gamesDir: join(root, 'games'), tsconfigPath: tsconfigPath ? join(root, tsconfigPath) : undefined, }); } const frameworkRoot = fileURLToPath(new URL('../../YouleNexus/assets/framework/', import.meta.url)); const modernRuntimeEntries = [ 'net/wire-client.ts', 'platform/runtime-session.ts', 'protocol/platform-handlers.ts', 'protocol/router.ts', // Task 10 creates this entry; the same assertion starts covering it as soon as it exists. 'platform/runtime.ts', ]; const legacyRuntimeFiles = new Set([ 'net/net-client.ts', 'platform/session.ts', 'platform/startup.ts', 'platform/room-rpc-bus.ts', 'platform/readonly.ts', 'platform/stores/app-store.ts', 'platform/stores/player-store.ts', 'platform/stores/room-store.ts', 'platform/stores/types.ts', 'protocol/room-handlers.ts', ].map((file) => resolve(frameworkRoot, file))); function scanCompatibilityFixture(root, entry = 'framework/protocol/router.ts', tsconfigPath) { return findCompatibilityDependency({ entry: join(root, entry), forbiddenFiles: [join(root, 'framework/platform/session.ts')], traversalRoot: join(root, 'framework'), tsconfigPath: tsconfigPath ? join(root, tsconfigPath) : undefined, }); } test('every existing modern runtime entry is transitively isolated from the legacy compatibility island', () => { const entries = modernRuntimeEntries .map((file) => resolve(frameworkRoot, file)) .filter((file) => existsSync(file)); assert.ok(entries.some((file) => file.endsWith(`${join('protocol', 'router.ts')}`))); for (const entry of entries) { const dependency = findCompatibilityDependency({ entry, forbiddenFiles: [...legacyRuntimeFiles], traversalRoot: frameworkRoot, tsconfigPath: fileURLToPath(new URL('../../tsconfig.framework.json', import.meta.url)), }); assert.equal( dependency, null, dependency?.path.map((file) => file.slice(frameworkRoot.length)).join(' -> '), ); } }); test('compatibility graph catches static imports, re-exports, and import types through barrels', async () => { const cases = [ "import '../platform/session.ts'\n", "import Legacy = require('../platform/session.ts')\n", "export * from '../platform/session.ts'\n", "export type { LegacySession } from '../platform/session.ts'\n", "type Legacy = import('../platform/session.ts').LegacySession\n", ]; for (const source of cases) { const root = await createFixtureRoot(); await writeFixture(root, 'framework/platform/session.ts', 'export interface LegacySession {}\n'); await writeFixture(root, 'framework/protocol/barrel.ts', source); await writeFixture(root, 'framework/protocol/router.ts', "export * from './barrel.ts'\n"); const found = scanCompatibilityFixture(root); assert.equal(found?.kind, 'forbidden', source); assert.deepEqual( found?.path.map((file) => file.replaceAll('\\', '/').split('/framework/')[1]), ['protocol/router.ts', 'protocol/barrel.ts', 'platform/session.ts'], source, ); } }); test('compatibility graph reports direct and ordinary transitive dependency paths', async () => { const directRoot = await createFixtureRoot(); await writeFixture(directRoot, 'framework/platform/session.ts', 'export const legacy = true\n'); await writeFixture(directRoot, 'framework/protocol/router.ts', "import '../platform/session.ts'\n"); assert.equal(scanCompatibilityFixture(directRoot)?.path.length, 2); const transitiveRoot = await createFixtureRoot(); await writeFixture(transitiveRoot, 'framework/platform/session.ts', 'export const legacy = true\n'); await writeFixture(transitiveRoot, 'framework/protocol/helper.ts', "import '../platform/session.ts'\n"); await writeFixture(transitiveRoot, 'framework/protocol/router.ts', "import './helper.ts'\n"); assert.equal(scanCompatibilityFixture(transitiveRoot)?.path.length, 3); }); test('compatibility graph catches string and no-substitution-template dynamic imports', async () => { for (const source of [ "await import('../platform/session.ts')\n", 'await import(`../platform/session.ts`)\n', ]) { const root = await createFixtureRoot(); await writeFixture(root, 'framework/platform/session.ts', 'export const legacy = true\n'); await writeFixture(root, 'framework/protocol/router.ts', source); assert.equal(scanCompatibilityFixture(root)?.kind, 'forbidden', source); } }); test('compatibility graph treats literal require calls as static dependencies', async () => { for (const source of [ "require('../platform/session.ts')\n", 'require(`../platform/session.ts`)\n', ]) { const root = await createFixtureRoot(); await writeFixture(root, 'framework/platform/session.ts', 'export const legacy = true\n'); await writeFixture(root, 'framework/protocol/router.ts', source); const found = scanCompatibilityFixture(root); assert.equal(found?.kind, 'forbidden', source); assert.deepEqual( found?.path.map((file) => file.replaceAll('\\', '/').split('/framework/')[1]), ['protocol/router.ts', 'platform/session.ts'], source, ); } }); test('compatibility graph resolves tsconfig path aliases to canonical files', async () => { const root = await createFixtureRoot(); await writeFixture(root, 'framework/platform/session.ts', 'export const legacy = true\n'); await writeFixture(root, 'framework/protocol/router.ts', "import '@legacy/session'\n"); await writeFixture(root, 'tsconfig.json', JSON.stringify({ compilerOptions: { baseUrl: '.', paths: { '@legacy/*': ['framework/platform/*'] }, module: 'ESNext', moduleResolution: 'Bundler', allowImportingTsExtensions: true, }, })); const found = scanCompatibilityFixture(root, undefined, 'tsconfig.json'); assert.equal(found?.kind, 'forbidden'); assert.ok(found?.path.at(-1)?.replaceAll('\\', '/').endsWith('/framework/platform/session.ts')); }); test('compatibility graph fails closed on a reachable non-static dynamic import', async () => { const root = await createFixtureRoot(); await writeFixture(root, 'framework/platform/session.ts', 'export const legacy = true\n'); await writeFixture( root, 'framework/protocol/router.ts', "const target = '../platform/session.ts'; void import(target)\n", ); const found = scanCompatibilityFixture(root); assert.equal(found?.kind, 'dynamic'); assert.ok(found?.path[0]?.replaceAll('\\', '/').endsWith('/framework/protocol/router.ts')); }); test('compatibility graph fails closed on reachable non-static require calls', async () => { for (const source of [ "const target = '../platform/session.ts'; require(target)\n", "const name = 'session'; require(`../platform/${name}.ts`)\n", ]) { const root = await createFixtureRoot(); await writeFixture(root, 'framework/platform/session.ts', 'export const legacy = true\n'); await writeFixture(root, 'framework/protocol/router.ts', source); const found = scanCompatibilityFixture(root); assert.equal(found?.kind, 'dynamic', source); assert.ok( found?.path[0]?.replaceAll('\\', '/').endsWith('/framework/protocol/router.ts'), source, ); } }); test('compatibility graph applies the same isolation to a future platform/runtime.ts entry', async () => { const root = await createFixtureRoot(); await writeFixture(root, 'framework/platform/session.ts', 'export const legacy = true\n'); await writeFixture(root, 'framework/application/barrel.ts', "export * from '../platform/session.ts'\n"); await writeFixture(root, 'framework/platform/runtime.ts', "import '../application/barrel.ts'\n"); const found = scanCompatibilityFixture(root, 'framework/platform/runtime.ts'); assert.equal(found?.kind, 'forbidden'); assert.equal(found?.path.length, 3); }); test('scanner rejects sdk contracts importing a framework platform path', async () => { const root = await createFixtureRoot(); await writeFixture(root, 'framework/sdk/contracts/bad.ts', "import '../platform/session.ts'\n"); assert.match(scan(root)[0].message, /sdk.*platform/); }); test('scanner rejects every sdk runtime dependency on framework implementation layers', async () => { const layers = [ 'core', 'platform', 'net', 'config', 'native', 'protocol', 'application', 'domain', 'presentation', 'ui', 'compat', ]; for (const layer of layers) { const root = await createFixtureRoot(); await writeFixture(root, `framework/${layer}/internal.ts`, 'export const value = true\n'); await writeFixture( root, 'framework/sdk/runtime/bad.ts', `import { value } from '../../${layer}/internal.ts'; void value\n`, ); const violations = scan(root); assert.equal(violations.length, 1, layer); assert.match(violations[0].message, /sdk.*framework implementation/, layer); } }); 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 resolves and rejects a nested sdk CommonJS 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/nested/bad.ts', "const value = require('@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 resolves and rejects a TypeScript import-equals sdk path alias', async () => { const root = await createFixtureRoot(); await writeFixture(root, 'framework/core/internal.ts', 'export const value = true\n'); await writeFixture( root, 'framework/sdk/runtime/nested/bad.ts', "import alias = require('@framework/core/internal'); void alias\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 resolves and rejects a no-substitution-template dynamic sdk path alias', async () => { const root = await createFixtureRoot(); await writeFixture(root, 'framework/core/internal.ts', 'export const value = true\n'); await writeFixture( root, 'framework/sdk/runtime/nested/bad.ts', 'void import(`@framework/core/internal`)\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('scanner rejects a no-substitution-template dynamic Cocos import in nested sdk code', async () => { const root = await createFixtureRoot(); await writeFixture( root, 'framework/sdk/testing/nested/bad.ts', 'void import(`cc/env`)\n', ); const violations = scan(root); assert.equal(violations.length, 1); assert.equal(violations[0].specifier, 'cc/env'); assert.match(violations[0].message, /sdk.*Cocos/); }); test('scanner rejects a CommonJS Cocos import in nested sdk code', async () => { const root = await createFixtureRoot(); await writeFixture( root, 'framework/sdk/testing/nested/bad.ts', "const env = require('cc/env'); void env\n", ); const violations = scan(root); assert.equal(violations.length, 1); assert.equal(violations[0].specifier, 'cc/env'); 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);", "const pure = require('pure-commonjs-external'); void pure;", ].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"); assert.match(scan(root)[0].message, /game.*framework\/net/); }); test('scanner rejects multiline game imports of framework net internals', async () => { const root = await createFixtureRoot(); await writeFixture(root, 'games/a/assets/game/bad.ts', "import {\n NetClient,\n} from '../../../../framework/net/net-client.ts'\n"); const violations = scan(root); assert.equal(violations.length, 1); assert.match(violations[0].message, /game.*framework\/net/); }); test('scanner rejects multiline sdk contract imports outside their sibling directory', async () => { const root = await createFixtureRoot(); await writeFixture(root, 'framework/sdk/contracts/bad.ts', "import {\n PlatformSession,\n} from '../../platform/session.ts'\n"); const violations = scan(root); assert.equal(violations.length, 1); assert.match(violations[0].message, /sdk\/contracts.*sibling contracts/); }); test('scanner permits a game import of the public sdk barrel', async () => { const root = await createFixtureRoot(); await writeFixture(root, 'games/a/assets/game/allowed.ts', "import '../../../../framework/sdk/index.ts'\n"); assert.deepEqual(scan(root), []); }); test('scanner rejects games importing migration-only sdk declarations', async () => { const root = await createFixtureRoot(); await writeFixture(root, 'games/a/assets/game/bad.ts', "import type { GameContext } from '../../../../framework/sdk/index.ts'\n"); assert.match(scan(root)[0].message, /game.*GameContext.*migration-only/); }); test('scanner rejects cc imports in sdk contracts', async () => { const root = await createFixtureRoot(); await writeFixture(root, 'framework/sdk/contracts/bad.ts', "import { Node } from 'cc'\n"); assert.match(scan(root)[0].message, /sdk\/contracts.*cc/); }); test('scanner rejects non-relative imports in sdk contracts', async () => { const root = await createFixtureRoot(); await writeFixture(root, 'framework/sdk/contracts/bad.ts', "import 'node:fs'\n"); const violations = scan(root); assert.equal(violations.length, 1); assert.match(violations[0].message, /sdk\/contracts.*non-relative/); }); test('scanner rejects import-type leaks from sdk contracts', async () => { const root = await createFixtureRoot(); await writeFixture(root, 'framework/sdk/contracts/bad.ts', "type ReadonlyRoom = import('../../platform/readonly.ts').ReadonlyRoomStore\n"); const violations = scan(root); assert.equal(violations.length, 1); assert.match(violations[0].message, /sdk\/contracts.*sibling contracts/); }); test('scanner rejects export-star leaks from sdk contracts', async () => { const root = await createFixtureRoot(); await writeFixture(root, 'framework/sdk/contracts/index.ts', "export * from '../../platform/readonly.ts'\n"); const violations = scan(root); assert.equal(violations.length, 1); assert.match(violations[0].message, /sdk\/contracts.*sibling contracts/); }); test('scanner rejects named re-export leaks from sdk contracts', async () => { const root = await createFixtureRoot(); await writeFixture(root, 'framework/sdk/contracts/index.ts', "export { ReadonlyRoomStore } from '../../platform/readonly.ts'\n"); const violations = scan(root); assert.equal(violations.length, 1); assert.match(violations[0].message, /sdk\/contracts.*sibling contracts/); }); test('scanner permits cc imports in game code', async () => { const root = await createFixtureRoot(); await writeFixture(root, 'games/a/assets/game/allowed.ts', "import { Node } from 'cc'\n"); assert.deepEqual(scan(root), []); }); test('scanner rejects framework imports resolving under games', async () => { const root = await createFixtureRoot(); await writeFixture(root, 'framework/ui/bad.ts', "import '../../games/a/assets/game/entry.ts'\n"); assert.match(scan(root)[0].message, /framework.*games/); }); test('scanner rejects framework production imports of migration-only sdk declarations', async () => { const root = await createFixtureRoot(); await writeFixture(root, 'framework/application/bad.ts', "import type { GameContext } from '../sdk/index.ts'\n"); const violations = scan(root); assert.equal(violations.length, 1); assert.match(violations[0].message, /GameContext.*migration-only/); }); test('scanner rejects alias imports of migration-only sdk declarations', async () => { const root = await createFixtureRoot(); await writeFixture(root, 'framework/application/bad.ts', "import type { IGameModule } from '@internal/sdk'\n"); const violations = scan(root); assert.equal(violations.length, 1); assert.match(violations[0].message, /IGameModule.*migration-only/); }); test('scanner rejects new production imports of a quarantined legacy runtime', async () => { const root = await createFixtureRoot(); await writeFixture(root, 'framework/application/bad.ts', "import '../net/net-client.ts'\n"); const violations = scan(root); assert.equal(violations.length, 1); assert.match(violations[0].message, /new production code.*legacy runtime/); }); test('scanner rejects CommonJS imports of a quarantined legacy runtime', async () => { const root = await createFixtureRoot(); await writeFixture(root, 'framework/net/net-client.ts', 'export const legacy = true\n'); await writeFixture( root, 'framework/application/nested/bad.ts', "const legacy = require('../../net/net-client.ts'); void legacy\n", ); const violations = scan(root); assert.equal(violations.length, 1); assert.equal(violations[0].specifier, '../../net/net-client.ts'); assert.match(violations[0].message, /new production code.*legacy runtime/); }); test('scanner rejects string-literal dynamic imports that cross a boundary', async () => { const root = await createFixtureRoot(); await writeFixture(root, 'games/a/assets/game/bad.ts', "await import('../../../framework/net/net-client.ts')\n"); assert.match(scan(root)[0].message, /game.*framework\/net/); });