Repository: honojs/hono
File: src/helper/ssg/ssg.test.tsx (TypeScript)

Code before:
  it('should generate redirect HTML for 301/302 route responses using plugin', async () => {
    const writtenFiles: Record<string, string> = {}
    const fsMock: FileSystemModule = {
      writeFile: (path, data) => {
        writtenFiles[path] = typeof data === 'string' ? data : data.toString()
        return Promise.resolve()
      },
      mkdir: vi.fn(() => Promise.resolve()),
    }
    const app = new Hono()
    app.get('/old', (c) => c.redirect('/new'))
    app.get('/new', (c) => c.html('New Page'))

    await toSSG(app, fsMock, { dir: './static', plugins: [redirectPlugin()] })

    expect(writtenFiles['static/old.html']).toBeDefined()
    const content = writtenFiles['static/old.html']
    // Should contain meta refresh
    expect(content).toContain('meta http-equiv="refresh" content="0;url=/new"')
    // Should contain canonical
    expect(content).toContain('rel="canonical" href="/new"')
    // Should contain link anchor
    expect(content).toContain('<a href="/new">Redirecting from')
  })

  it('should skip generating a redirect HTML when 301/302 has no Location header', async () => {
    const writtenFiles: Record<string, string> = {}
    const fsMock: FileSystemModule = {
      writeFile: (path, data) => {
        writtenFiles[path] = typeof data === 'string' ? data : data.toString()
        return Promise.resolve()
      },
      mkdir: vi.fn(() => Promise.resolve()),
    }
    const app = new Hono()
    // Return a 301 without Location header
    app.get('/bad', (c) => new Response(null, { status: 301 }))

    await toSSG(app, fsMock, { dir: './static', plugins: [redirectPlugin()] })

    expect(writtenFiles['static/bad.html']).toBeUndefined()
  })

  it('should handle asynchronous beforeRequestHook correctly', async () => {
    const beforeRequestHook: BeforeRequestHook = async (req) => {
      await new Promise((resolve) => setTimeout(resolve, 10))
      if (req.url.includes('/skip')) {
        return false
      }
      return req
    }

Review comment (suggestion):
The specification mandates that redirect responses must include a `Location` header [^1]. Current implementations simply bypass generating the file if no `Location` header is present, though options include displaying a warning or throwing an error could be considered.

[^1]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Location


Code after:
    }

    const result = await toSSG(app, fsMock, { beforeRequestHook })
    expect(result.files).not.toContain(expect.stringContaining('/skip'))
    expect(result.success).toBe(true)
    expect(result.files.length).toBeGreaterThan(0)
  })

  it('should handle asynchronous afterResponseHook correctly', async () => {
    const afterResponseHook: AfterResponseHook = async (res) => {
      await new Promise((resolve) => setTimeout(resolve, 10))
      if (res.headers.get('X-Skip') === 'true') {
        return false
      }
      return res
    }

    const result = await toSSG(app, fsMock, { afterResponseHook })
    expect(result.files).not.toContain(expect.stringContaining('/skip'))
    expect(result.success).toBe(true)
    expect(result.files.length).toBeGreaterThan(0)
  })

  it('should handle asynchronous afterGenerateHook correctly', async () => {
    const afterGenerateHook: AfterGenerateHook = async (result, fsModule, options) => {
      await new Promise((resolve) => setTimeout(resolve, 10))
      console.log(`Generated ${result.files.length} files.`)
    }

    const result = await toSSG(app, fsMock, { afterGenerateHook })
    expect(result.success).toBe(true)
    expect(result.files.length).toBeGreaterThan(0)
  })

  it('should avoid memory leak from `req.signal.addEventListener()`', async () => {
    const fsMock: FileSystemModule = {
      writeFile: vi.fn(() => Promise.resolve()),
      mkdir: vi.fn(() => Promise.resolve()),
    }

    const signalAddEventListener = vi.fn(() => {})
    const app = new Hono()
    app.get('/post/:post', ssgParams([{ post: '1' }, { post: '2' }]), (c) =>
      c.html(<h1>{c.req.param('post')}</h1>)
    )
    await toSSG(app, fsMock, {
      beforeRequestHook: (req) => {
        req.signal.addEventListener = signalAddEventListener
        return req
      },
    })