import assert from 'node:assert/strict'; import test from 'node:test'; import { execFileSync } from 'node:child_process'; import { mkdtempSync, mkdirSync, writeFileSync, rmSync, symlinkSync, } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { analyzeModule } from '../../scripts/module-analysis.mjs'; import { checkImportDirections } from '../../scripts/check-import-directions.mjs'; function fixture(t, exports = {}) { const root = mkdtempSync(path.join(os.tmpdir(), 'gev-directions-')); t.after(() => rmSync(root, { recursive: true, force: true })); execFileSync('git', ['init', '--quiet'], { cwd: root }); const write = (name, source = '') => { mkdirSync(path.dirname(path.join(root, name)), { recursive: true }); writeFileSync(path.join(root, name), source); }; write( 'package.json', JSON.stringify({ name: '@gods-eye-view/core', exports }), ); write('src/data/feedState.js'); return { root, write }; } test('module analysis parses dependencies and platform references without reading comments or property labels', () => { assert.deepEqual( analyzeModule( "// import('node:fs'); document\nimport x from 'one'; export { x } from 'two'; export * from 'three'; import('four'); const data = { document: 1 }; data.window;", ), { imports: ['one', 'two', 'three', 'four'], browser: [] }, ); assert.deepEqual( analyzeModule( "const data = { element: document }; globalThis['navigator'];", ).browser, ['document', 'navigator'], ); assert.throws(() => analyzeModule('import(name)'), /Computed/); assert.throws(() => analyzeModule("require('node:fs')"), /ES imports/); }); test('portable sources can share records, and standalone can assemble rendering', (t) => { const { root, write } = fixture(t, { './layers/demo/source': './src/layers/demo/source.js', }); write('src/layers/demo/source.js', "export { read } from './records.js';"); write('src/layers/demo/records.js', 'export const read = (value) => value;'); write('src/standalone/catalog.js', "import '../layers/demo/index.js';"); write('src/layers/demo/index.js', "import * as Cesium from 'cesium';"); assert.equal(checkImportDirections(root).modules, 5); }); for (const [name, files, pattern] of [ [ 'unreachable source imports rendering', { 'src/layers/demo/source.js': "import './rendering.js';", 'src/layers/demo/rendering.js': '', }, /Source imports rendering/, ], [ 'source reaches a platform through a record helper', { 'src/layers/demo/source.js': "import './records.js';", 'src/layers/demo/records.js': "import 'cesium';", }, /reaches platform\/rendering/, ], [ 'source reaches a DOM helper', { 'src/sources/demo.js': "import '../data/helper.js';", 'src/data/helper.js': 'export const view = () => globalThis.document;', }, /reaches browser globals/, ], [ 'browser imports Node without prefix', { 'src/unused.mjs': "import 'fs/promises';" }, /Node builtin/, ], [ 'browser hides Node outside src', { 'src/unused.js': "import '../shared/helper.js';", 'shared/helper.js': "export * from 'node:fs';", }, /Browser graph reaches Node/, ], [ 'browser imports server', { 'src/data/demo.js': "import '../../server/helper.js';", 'server/helper.js': '', }, /Browser imports server/, ], [ 'reusable module selects standalone', { 'src/app/demo.js': "import '../standalone/catalog.js';", 'src/standalone/catalog.js': '', }, /Reusable module imports standalone/, ], [ 'provider selects application setup', { 'server/providers/demo.js': "import '../standalone/key-setup.js';", 'server/standalone/key-setup.js': '', }, /Provider imports application/, ], [ 'voice controls reach protocol indirectly', { 'src/voice/sessionCommands.js': "import './helper.js';", 'src/voice/helper.js': "import './realtimeSession.js';", 'src/voice/realtimeSession.js': '', }, /Common voice controls import protocol/, ], [ 'dynamic import cannot evade direction', { 'src/app/demo.js': "const target = './module.js'; import(target);" }, /Computed/, ], [ 'browser imports test helper', { 'src/app/demo.js': "import '../testSupport/helper.js';", 'src/testSupport/helper.js': '', }, /non-runtime owner/, ], ]) { test(`rejects ${name}`, (t) => { const { root, write } = fixture(t); for (const [file, body] of Object.entries(files)) write(file, body); assert.throws(() => checkImportDirections(root), pattern); }); } test('self package imports cannot evade portable ownership', (t) => { const { root, write } = fixture(t, { './view': './src/ui/view.js' }); write('src/sources/demo.js', "import '@gods-eye-view/core/view';"); write('src/ui/view.js'); assert.throws(() => checkImportDirections(root), /Source imports rendering/); }); test('the compatibility exception is limited to the existing composition entry', (t) => { const { root, write } = fixture(t); write('src/standalone/catalog.js'); write('server/providers/local.js', "import '../standalone/key-setup.js';"); write('server/standalone/key-setup.js'); assert.doesNotThrow(() => checkImportDirections(root)); write('src/ui.js', "import './standalone/catalog.js';"); assert.throws( () => checkImportDirections(root), /Reusable module imports standalone/, ); write('src/ui.js'); write('src/ui/new.js', "import '../standalone/catalog.js';"); assert.throws( () => checkImportDirections(root), /Reusable module imports standalone/, ); }); test('symlink aliases do not hide renderer ownership', (t) => { const { root, write } = fixture(t); write('src/sources/demo.js', "import './helper/view.js';"); write('src/ui/view.js'); // Directory junctions exercise the same resolved edge without Windows symlink privileges. symlinkSync( path.join(root, 'src/ui'), path.join(root, 'src/sources/helper'), 'junction', ); assert.throws(() => checkImportDirections(root), /rendering|symlinks/); });