layout.host.spec.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. /** Host-side source layout invariants. */
  2. import { readdir, readFile } from 'node:fs/promises'
  3. import { dirname, relative, resolve, sep } from 'node:path'
  4. import { fileURLToPath } from 'node:url'
  5. import { describe, expect, it } from 'vitest'
  6. const sourceRoot = fileURLToPath(new URL('../src/', import.meta.url))
  7. const packageRoot = fileURLToPath(new URL('../', import.meta.url))
  8. const testsRoot = fileURLToPath(new URL('./', import.meta.url))
  9. describe('Inspector execution layout', () => {
  10. it('keeps Client and Host implementation paths mirrored', async () => {
  11. expect(await sourceFiles('client')).toEqual(await sourceFiles('host'))
  12. })
  13. it('keeps Worker Client and Host backend paths mirrored', async () => {
  14. expect(await sourceFiles('worker/realms/client')).toEqual(await sourceFiles('worker/realms/host'))
  15. })
  16. it('keeps shared modules independent of execution-specific directories', async () => {
  17. await expectNoImports('shared', ['client', 'host', 'worker'])
  18. })
  19. it('keeps Client and Host modules isolated from each other and the Worker implementation', async () => {
  20. await expectNoImports('client', ['host', 'worker'])
  21. await expectNoImports('host', ['client', 'worker'])
  22. })
  23. it('keeps compiler files and specs on their declared execution face', async () => {
  24. const hostFiles = await compilerFiles('tsconfig.host.json')
  25. const clientFiles = await compilerFiles('tsconfig.client.json')
  26. expect(hostFiles.some(file => file.startsWith('src/client/'))).toBe(false)
  27. expect(clientFiles.some(file => file.startsWith('src/host/') || file.startsWith('src/worker/'))).toBe(false)
  28. const testFiles = (await walk(testsRoot)).filter(file => file.endsWith('.ts'))
  29. const specs = testFiles.filter(file => file.endsWith('.spec.ts'))
  30. expect(specs.every(file => file.endsWith('.host.spec.ts') || file.endsWith('.client.spec.ts'))).toBe(true)
  31. await expectTestImports(testFiles.filter(file =>
  32. file.endsWith('.host.ts') || file.endsWith('.host.spec.ts')), ['client'])
  33. await expectTestImports(testFiles.filter(file =>
  34. file.endsWith('.client.ts') || file.endsWith('.client.spec.ts')), ['host', 'worker'])
  35. })
  36. it('keeps Worker repositories and realm backends independent of the Chrome adapter', async () => {
  37. await expectNoImports('worker/inspection', ['worker/cdp'])
  38. await expectNoImports('worker/realms', ['worker/cdp'])
  39. })
  40. })
  41. async function sourceFiles(directory: string): Promise<string[]> {
  42. const root = resolve(sourceRoot, directory)
  43. return (await walk(root))
  44. .filter(file => file.endsWith('.ts'))
  45. .map(file => relative(root, file).split(sep).join('/'))
  46. .sort()
  47. }
  48. async function compilerFiles(config: string): Promise<string[]> {
  49. const parsed = JSON.parse(await readFile(resolve(packageRoot, config), 'utf8')) as { files?: unknown }
  50. if (!Array.isArray(parsed.files) || !parsed.files.every(file => typeof file === 'string')) {
  51. throw new Error(`${config} must declare a string files array`)
  52. }
  53. return parsed.files
  54. }
  55. async function expectTestImports(files: readonly string[], forbidden: readonly string[]): Promise<void> {
  56. for (const file of files) {
  57. const source = await readFile(file, 'utf8')
  58. for (const specifier of relativeSpecifiers(source)) {
  59. const target = resolve(dirname(file), specifier)
  60. for (const directory of forbidden) {
  61. const forbiddenRoot = resolve(sourceRoot, directory)
  62. expect(
  63. target === forbiddenRoot || target.startsWith(`${forbiddenRoot}${sep}`),
  64. `${relative(testsRoot, file)} imports ${specifier}`,
  65. ).toBe(false)
  66. }
  67. }
  68. }
  69. }
  70. async function expectNoImports(owner: string, forbidden: readonly string[]): Promise<void> {
  71. const root = resolve(sourceRoot, owner)
  72. for (const file of await walk(root)) {
  73. if (!file.endsWith('.ts')) continue
  74. const source = await readFile(file, 'utf8')
  75. for (const specifier of relativeSpecifiers(source)) {
  76. const target = resolve(dirname(file), specifier)
  77. for (const directory of forbidden) {
  78. const forbiddenRoot = resolve(sourceRoot, directory)
  79. expect(
  80. target === forbiddenRoot || target.startsWith(`${forbiddenRoot}${sep}`),
  81. `${relative(sourceRoot, file)} imports ${specifier}`,
  82. ).toBe(false)
  83. }
  84. }
  85. }
  86. }
  87. async function walk(directory: string): Promise<string[]> {
  88. const entries = await readdir(directory, { withFileTypes: true })
  89. const files = await Promise.all(entries.map(async (entry) => {
  90. const value = resolve(directory, entry.name)
  91. return entry.isDirectory() ? await walk(value) : [value]
  92. }))
  93. return files.flat()
  94. }
  95. function relativeSpecifiers(source: string): string[] {
  96. return [...source.matchAll(/(?:from\s+|import\s*\()['"](\.[^'"]+)['"]/gu)].map(match => match[1] ?? '')
  97. }