builtins-table.spec.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. /**
  2. * The Node-compatibility table and the module identity it owes its consumers.
  3. *
  4. * Two consumers read these specifiers — the worker vite build aliases them for
  5. * statically bundled code, and the module loader answers `require('node:fs')`
  6. * from VFS-loaded modules — and both must land on ONE module instance per
  7. * specifier. Class identity is what depends on it: `instanceof EventEmitter` and
  8. * `Buffer.isBuffer` compare against a specific copy, so a second instance turns
  9. * them into silent false answers rather than an error anyone can trace.
  10. *
  11. * The table holds factories, so what a table entry defers is the table read. The
  12. * namespace objects themselves belong to the static graph the worker bundle
  13. * evaluates at load, which is why nothing here asserts that a factory is
  14. * unevaluated.
  15. */
  16. import { describe, expect, it } from 'vitest'
  17. import { createNodeBuiltins, REPLACED_PREFIXES } from '../../src/node/builtins.ts'
  18. import { WorkerModuleLoader, type WorkerRequire } from '../../src/module-system/module-loader.ts'
  19. import { MemoryVfs } from '../../src/storage/memory.ts'
  20. /** A loader over an empty image: every specifier below resolves from the table. */
  21. function loaderRequire(): WorkerRequire {
  22. const vfs = new MemoryVfs()
  23. vfs.seedDirectory('/dsh')
  24. const loader = new WorkerModuleLoader({ vfs, root: '/dsh', staticModules: createNodeBuiltins() })
  25. return loader.createRequire('/dsh/')
  26. }
  27. describe('the replacement table', () => {
  28. it('holds a factory for every specifier', () => {
  29. const table = createNodeBuiltins()
  30. const notFunctions = Object.entries(table)
  31. .filter(([, value]) => typeof value !== 'function')
  32. .map(([specifier]) => specifier)
  33. // A module object left in the table would be called as a factory and fail at
  34. // the first require of that specifier, not at assembly.
  35. expect(notFunctions).toEqual([])
  36. expect(Object.keys(table).length).toBeGreaterThan(0)
  37. })
  38. it('keys every builtin with and without the node: prefix', () => {
  39. const table = createNodeBuiltins()
  40. expect(Object.keys(table)).toEqual(expect.arrayContaining(['fs', 'node:fs', 'fs/promises', 'node:fs/promises']))
  41. })
  42. it('leaves process out, because the host installs that global itself', () => {
  43. const table = createNodeBuiltins()
  44. expect([table['process'], table['node:process']]).toEqual([undefined, undefined])
  45. })
  46. it('answers path and path/posix from one module: the worker speaks POSIX only', () => {
  47. const table = createNodeBuiltins()
  48. expect(table['path']?.()).toBe(table['path/posix']?.())
  49. })
  50. it('answers a prefixed subpath with the module its exact key answers', () => {
  51. const table = createNodeBuiltins()
  52. expect(REPLACED_PREFIXES['@earendil-works/pi-ai/']?.()).toBe(table['@earendil-works/pi-ai']?.())
  53. })
  54. })
  55. describe('module identity through the loader', () => {
  56. it('hands the same instance to two requires of one specifier', () => {
  57. const require = loaderRequire()
  58. expect(require('node:events')).toBe(require('node:events'))
  59. })
  60. it('hands the same instance to the bare and prefixed specifiers', () => {
  61. const require = loaderRequire()
  62. expect(require('events')).toBe(require('node:events'))
  63. expect(require('fs')).toBe(require('node:fs'))
  64. expect(require('tty')).toBe(require('node:tty'))
  65. })
  66. it('reports that worker file descriptors are not terminals', () => {
  67. const tty = loaderRequire()('tty') as { isatty(fd: number): boolean }
  68. expect(tty.isatty(2)).toBe(false)
  69. })
  70. it('keeps class identity across those specifiers', () => {
  71. // The consequence the single-instance rule exists for: a second copy would
  72. // make this comparison answer false with nothing failing.
  73. const require = loaderRequire()
  74. const { EventEmitter } = require('events') as { EventEmitter: new () => unknown }
  75. const prefixed = require('node:events') as { EventEmitter: new () => unknown }
  76. expect(new EventEmitter() instanceof prefixed.EventEmitter).toBe(true)
  77. })
  78. it('refuses a specifier the table does not hold, instead of resolving it empty', () => {
  79. const require = loaderRequire()
  80. expect(() => require('node:dns')).toThrow()
  81. })
  82. it('exposes the package search paths used by the VFS resolver', () => {
  83. const require = loaderRequire()
  84. expect(require.resolve.paths('node:fs')).toBeNull()
  85. expect(require.resolve.paths('node:dns')).toBeNull()
  86. expect(require.resolve.paths('workspace-package')).toEqual(['/dsh/node_modules'])
  87. expect(require.resolve.paths('./local.js')).toEqual(['/dsh'])
  88. })
  89. })