Back to Repositories

Testing ESM and CommonJS Module Integration in GatsbyJS

This integration test suite validates ESM and CommonJS module support in Gatsby Node API files, ensuring proper functionality across different module systems and build configurations.

Test Coverage Overview

The test suite provides comprehensive coverage of module system compatibility in Gatsby Node APIs.

  • Tests CommonJS require functionality in gatsby-node.js
  • Validates ESM import capabilities in gatsby-node.mjs
  • Verifies bundled ESM modules in Gatsby engine context
  • Checks build output and server-side rendering functionality

Implementation Analysis

The testing approach utilizes Jest’s asynchronous testing capabilities with file system operations and process management.

  • Implements fixture-based testing with dynamic file copying
  • Uses execa for build process execution
  • Employs child_process spawning for serve functionality
  • Implements retry logic for server readiness checks

Technical Details

  • Jest test framework with extended timeout (100000ms)
  • fs-extra for enhanced file operations
  • node-fetch for HTTP request validation
  • execa for process execution
  • Custom serve process management
  • Environment variable configuration for production builds

Best Practices Demonstrated

The test suite exemplifies robust integration testing practices with thorough cleanup and error handling.

  • Proper test isolation with afterEach cleanup
  • Comprehensive assertion coverage
  • Graceful process management and termination
  • Effective error handling and retry mechanisms
  • Clear test organization and descriptive test cases

gatsbyjs/gatsby

integration-tests/esm-in-gatsby-files/__tests__/gatsby-node.test.js

            
const path = require(`path`)
const fs = require(`fs-extra`)
const execa = require(`execa`)
const { spawn } = require(`child_process`)
const fetch = require(`node-fetch`)

jest.setTimeout(100000)

const fixtureRoot = path.resolve(__dirname, `fixtures`)
const siteRoot = path.resolve(__dirname, `..`)

const fixturePath = {
  gatsbyNodeCjs: path.join(fixtureRoot, `gatsby-node.js`),
  gatsbyNodeEsm: path.join(fixtureRoot, `gatsby-node.mjs`),
  gatsbyNodeEsmBundled: path.join(
    fixtureRoot,
    `gatsby-node-engine-bundled.mjs`
  ),
  ssrPage: path.join(fixtureRoot, `pages`, `ssr.js`),
}

const targetPath = {
  gatsbyNodeCjs: path.join(siteRoot, `gatsby-node.js`),
  gatsbyNodeEsm: path.join(siteRoot, `gatsby-node.mjs`),
  ssrPage: path.join(siteRoot, `src`, `pages`, `ssr.js`),
}

const gatsbyBin = path.join(`node_modules`, `gatsby`, `cli.js`)

async function build() {
  const { stdout } = await execa(process.execPath, [gatsbyBin, `build`], {
    env: {
      ...process.env,
      NODE_ENV: `production`,
    },
  })

  return stdout
}

async function serve() {
  const serveProcess = spawn(process.execPath, [gatsbyBin, `serve`], {
    env: {
      ...process.env,
      NODE_ENV: `production`,
    },
  })

  await waitForServeReady(serveProcess)

  return serveProcess
}

function wait(ms) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

async function waitForServeReady(serveProcess, retries = 0) {
  if (retries > 10) {
    console.error(
      `Server for esm-in-gatsby-files > gatsby-node.test.js failed to get ready after 10 tries`
    )
    serveProcess.kill()
  }

  await wait(500)

  let ready = false

  try {
    const { ok } = await fetch(`http://localhost:9000/`)
    ready = ok
  } catch (_) {
    // Do nothing
  }

  if (!ready) {
    retries++
    return waitForServeReady(serveProcess, retries)
  }
}

// Tests include multiple assertions since running multiple builds is time consuming

describe(`gatsby-node.js`, () => {
  afterEach(() => {
    fs.rmSync(targetPath.gatsbyNodeCjs)
  })

  it(`works with required CJS modules`, async () => {
    await fs.copyFile(fixturePath.gatsbyNodeCjs, targetPath.gatsbyNodeCjs)

    const stdout = await build()

    // Build succeeded
    expect(stdout).toContain(`Done building`)

    // Requires work
    expect(stdout).toContain(`hello-default-cjs`)
    expect(stdout).toContain(`hello-named-cjs`)

    // Node API works
    expect(stdout).toContain(`gatsby-node-cjs-on-pre-build`)
  })
})

describe(`gatsby-node.mjs`, () => {
  afterEach(async () => {
    await fs.rm(targetPath.gatsbyNodeEsm)
    if (fs.existsSync(targetPath.ssrPage)) {
      await fs.rm(targetPath.ssrPage)
    }
  })

  it(`works with imported ESM modules`, async () => {
    await fs.copyFile(fixturePath.gatsbyNodeEsm, targetPath.gatsbyNodeEsm)

    const stdout = await build()

    // Build succeeded
    expect(stdout).toContain(`Done building`)

    // Imports work
    expect(stdout).toContain(`hello-default-esm`)
    expect(stdout).toContain(`hello-named-esm`)

    // Node API works
    expect(stdout).toContain(`gatsby-node-esm-on-pre-build`)
  })

  it(`works when bundled in engines`, async () => {
    await fs.copyFile(
      fixturePath.gatsbyNodeEsmBundled,
      targetPath.gatsbyNodeEsm
    )
    // Need to copy this because other runs fail on unfound query node type if the page is left in src
    await fs.copyFile(fixturePath.ssrPage, targetPath.ssrPage)

    const buildStdout = await build()
    const serveProcess = await serve()
    const response = await fetch(`http://localhost:9000/ssr/`)
    const html = await response.text()
    serveProcess.kill()

    // Build succeeded
    expect(buildStdout).toContain(`Done building`)

    // Engine bundling works
    expect(html).toContain(`gatsby-node-engine-bundled-mjs`)
  })
})