Repository: gatsbyjs/gatsby
File: packages/gatsby/src/utils/page-data.ts (TypeScript)

Code before:
import fs from "fs-extra"
import { ExecutionResult } from "graphql"
import path from "path"
import { IGatsbyPage } from "../redux/types"
import { store } from "../redux"

interface IPageData {
  componentChunkName: IGatsbyPage["componentChunkName"]
  matchPath?: IGatsbyPage["matchPath"]
  path: IGatsbyPage["path"]
}

interface IPageDataWithQueryResult extends IPageData {
  result: ExecutionResult
}

export function fixedPagePath(pagePath: string): string {
  return pagePath === `/` ? `index` : pagePath
}

export function getFilePath(publicDir: string, pagePath: string): string {
  return path.join(
    publicDir,
    `page-data`,
    fixedPagePath(pagePath),
    `page-data.json`
  )
}

export async function readPageData(
  publicDir: string,
  pagePath: string
): Promise<IPageDataWithQueryResult> {
  const filePath = getFilePath(publicDir, pagePath)
  const rawPageData = await fs.readFile(filePath, `utf-8`)

  return JSON.parse(rawPageData)
}

export async function removePageData(
  publicDir: string,
  pagePath: string
): Promise<void> {
  const filePath = getFilePath(publicDir, pagePath)

  if (fs.existsSync(filePath)) {
    return await fs.remove(filePath)
  }

  return Promise.resolve()
}

export async function writePageData(
  publicDir: string,
  { componentChunkName, matchPath, path }: IPageData,
  result: IPageDataWithQueryResult["result"]
): Promise<void> {
  const filePath = getFilePath(publicDir, path)
  const body = {
    componentChunkName,
    path,
    matchPath,
    result,
  }
  const bodyStr = JSON.stringify(body)
  // transform asset size to kB (from bytes) to fit 64 bit to numbers
  const pageDataSize = Buffer.byteLength(bodyStr) / 1000

  store.dispatch({
    type: `ADD_PAGE_DATA_STATS`,
    payload: {
      filePath,
      size: pageDataSize,
    },
  })

  await fs.outputFile(filePath, bodyStr)
}


Review comment (bug):
@sidharthachatterjee we introduced a bug with this (thank @TylerBarnes for raising this issue)

we generate `pagesToWrite` at the beginning of function (sync), and then the loop is async, which means that some concurrent code can add pending pages/templates while the loop is running and after that we clear all pending data writes which means that pending writes added mid function run are never handled.

What we did with Tyler to debug here is we rerun code used to generate `pagesToWrite` after the loop and realized that that it doesn't match with one we generated at the beginning of the function.

I think we either need to make this entire flush sync (yikes) or add some listener (e.g. `emitter.on("ADD_PENDING_PAGE_DATA_WRITE", ...)` that would flip some switch and rerun `flush` or something before dispatching `CLEAR_PENDING_PAGE_DATA_WRITES`

Code after:
    // After this change (#24808), they are not and this means that
    // between adding a pending write for a page and actually flushing
    // them, a page might not exist anymore щ（ﾟДﾟщ）
    // This is why we need this check
    if (page) {
      const body = await writePageData(
        path.join(program.directory, `public`),
        page
      )

      if (program?._?.[0] === `develop`) {
        websocketManager.emitPageData({
          ...body.result,
          id: pagePath,
          result: {
            data: body.result.data,
            pageContext: body.result.pageContext,
          },
        })
      }
    }
  }

  store.dispatch({
    type: `CLEAR_PENDING_PAGE_DATA_WRITES`,
  })
  isFlushing = false
  return
}

export function enqueueFlush(): void {
  if (isWebpackStatusPending()) {
    isFlushPending = true
  } else {
    flush()
  }
}