@hyperfrontend/testing/consumer

consumer

A packed package installed outside the workspace and driven through plain Node child processes, so a suite checks what a consumer installs rather than what the workspace resolves.

Usage

usePackedConsumer registers the node:test hooks that install the tarball once for the suite and delete the directory afterwards. The handle it returns always points at the suite's consumer, so capture it once at the top of the spec or in the describe body:

import { describe, expect, it } from '@hyperfrontend/testing'
import { usePackedConsumer } from '@hyperfrontend/testing/consumer'

const consumer = usePackedConsumer({ tarball: process.env.MY_LIB_TARBALL })

describe('my-lib as a consumer sees it', () => {
  it('imports every export entry under plain Node', () => {
    expect(consumer.entries('import').filter((entry) => !entry.ok)).toEqual([])
  })

  it('hashes through the node entry', () => {
    const hash = consumer.run(async () => {
      const { createHash } = await import('my-lib/node')
      return createHash('abc')
    })
    expect(hash).toBe('ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad')
  })
})

entries reads the installed manifest's exports map and loads each subpath in a child process of its own, with import or require as the map allows, so an entry that resolves only through a bundler, a path alias or a loader hook fails here with the child's error attached.

run serialises the function it is given with toString(), writes it into a script inside the consumer and runs that script with the Node binary running the suite. The function therefore closes over nothing from the spec: import the package inside the body and pass values through args, which travel as JSON, as the result does. Pass kind: 'require' to run the body as CommonJS. A child that exits non-zero makes run throw with everything the child wrote to stderr.

write and node are the pieces underneath, for a script that is more than one function, such as a build driven by the installed package. peers installs the packages a consumer is expected to bring, such as an optional peer dependency. node points the children at another Node binary, so a suite running on a current release can check the package on the oldest one it declares. For a consumer one test owns, createPackedConsumer installs straight away and leaves removal to the caller.

Design

The install uses the npm cache the suite's own project install just filled, so it is fast and works offline; the consumer directory sits under the system temporary directory, outside any workspace, with nothing installed above it to fall back on. The result of run goes to a file rather than stdout so whatever the package prints cannot corrupt it.

API Reference§

ƒ Functions

§function

createPackedConsumer(options: PackedConsumerOptions): PackedConsumer

Installs a packed tarball into a fresh directory under the system temporary directory and returns a handle that drives the package through plain Node child processes.
Nothing of the test process reaches the children: no loader hooks, no path aliases, no hoisted node_modules. A package that only works through those fails here, the way it would for a consumer. The caller removes the directory; for one a whole suite shares, usePackedConsumer registers the hooks itself.
Parameters
NameTypeDescription
§options
PackedConsumerOptionsThe tarball, its peers, the install timeout and the Node binary for the children.
Returns
PackedConsumer
The consumer handle.
Example

Loading every entry of a packed package

const consumer = createPackedConsumer({ tarball: '/tmp/my-lib-1.0.0.tgz' })
try {
  expect(consumer.entries('import').filter((entry) => !entry.ok)).toEqual([])
} finally {
  consumer.remove()
}
§function

usePackedConsumer(options: PackedConsumerOptions): PackedConsumer

Gives the enclosing suite one installed consumer, created before its first test and deleted after its last, by registering the node:test hooks that do it.
Call it in a describe body or at the top of a spec file. The returned handle forwards to the consumer current at the time of each call, so it can be captured once and used from any test or hook. Using it outside a test throws, since nothing is installed then.
Functions handed to run are serialised with toString() and executed in the child, so they can close over nothing from the spec: pass values through args and import the package inside the body.
Parameters
NameTypeDescription
§options
PackedConsumerOptionsThe tarball, its peers, the install timeout and the Node binary for the children.
Returns
PackedConsumer
A handle to the suite's consumer.
Example

Calling the packed package the way a consumer does

const consumer = usePackedConsumer({ tarball: process.env.MY_LIB_TARBALL })

it('imports every entry under plain Node', () => {
  expect(consumer.entries('import').filter((entry) => !entry.ok)).toEqual([])
})

it('hashes through the node entry', () => {
  const hash = consumer.run(async () => {
    const { createHash } = await import('my-lib/node')
    return createHash('abc')
  })
  expect(hash).toBe('ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad')
})

◆ Types

§type

ConsumerRun

What a child process run from the consumer reported.
§type

EntryLoad

One export entry loaded through plain Node.
§type

ModuleKind

Which module system a child process loads the package through.
type ModuleKind = "import" | "require"
§type

PackedConsumer

A directory outside the workspace with a packed package installed into it, driven through plain Node child processes.
§type

PackedConsumerOptions

What to install and how long to wait for it.
§type

RunOptions

How one script runs inside the consumer.