Skip to content

Commit

Permalink
feat(ssg): enhance conbined hooks (#2686)
Browse files Browse the repository at this point in the history
* v0.1

* adding test

* 1.0
  • Loading branch information
watany-dev authored May 24, 2024
1 parent 406abbb commit 04caa07
Show file tree
Hide file tree
Showing 3 changed files with 303 additions and 12 deletions.
76 changes: 70 additions & 6 deletions deno_dist/helper/ssg/ssg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,66 @@ export type BeforeRequestHook = (req: Request) => Request | false | Promise<Requ
export type AfterResponseHook = (res: Response) => Response | false | Promise<Response | false>
export type AfterGenerateHook = (result: ToSSGResult) => void | Promise<void>

export const combineBeforeRequestHooks = (
hooks: BeforeRequestHook | BeforeRequestHook[]
): BeforeRequestHook => {
if (!Array.isArray(hooks)) {
return hooks
}
return async (req: Request): Promise<Request | false> => {
let currentReq = req
for (const hook of hooks) {
const result = await hook(currentReq)
if (result === false) {
return false
}
if (result instanceof Request) {
currentReq = result
}
}
return currentReq
}
}

export const combineAfterResponseHooks = (
hooks: AfterResponseHook | AfterResponseHook[]
): AfterResponseHook => {
if (!Array.isArray(hooks)) {
return hooks
}
return async (res: Response): Promise<Response | false> => {
let currentRes = res
for (const hook of hooks) {
const result = await hook(currentRes)
if (result === false) {
return false
}
if (result instanceof Response) {
currentRes = result
}
}
return currentRes
}
}

export const combineAfterGenerateHooks = (
hooks: AfterGenerateHook | AfterGenerateHook[]
): AfterGenerateHook => {
if (!Array.isArray(hooks)) {
return hooks
}
return async (result: ToSSGResult): Promise<void> => {
for (const hook of hooks) {
await hook(result)
}
}
}

export interface ToSSGOptions {
dir?: string
beforeRequestHook?: BeforeRequestHook
afterResponseHook?: AfterResponseHook
afterGenerateHook?: AfterGenerateHook
beforeRequestHook?: BeforeRequestHook | BeforeRequestHook[]
afterResponseHook?: AfterResponseHook | AfterResponseHook[]
afterGenerateHook?: AfterGenerateHook | AfterGenerateHook[]
concurrency?: number
extensionMap?: Record<string, string>
}
Expand Down Expand Up @@ -286,10 +341,16 @@ export const toSSG: ToSSGInterface = async (app, fs, options) => {
const outputDir = options?.dir ?? './static'
const concurrency = options?.concurrency ?? DEFAULT_CONCURRENCY

const combinedBeforeRequestHook = combineBeforeRequestHooks(
options?.beforeRequestHook || ((req) => req)
)
const combinedAfterResponseHook = combineAfterResponseHooks(
options?.afterResponseHook || ((req) => req)
)
const getInfoGen = fetchRoutesContent(
app,
options?.beforeRequestHook,
options?.afterResponseHook,
combinedBeforeRequestHook,
combinedAfterResponseHook,
concurrency
)
for (const getInfo of getInfoGen) {
Expand Down Expand Up @@ -319,6 +380,9 @@ export const toSSG: ToSSGInterface = async (app, fs, options) => {
const errorObj = error instanceof Error ? error : new Error(String(error))
result = { success: false, files: [], error: errorObj }
}
await options?.afterGenerateHook?.(result)
if (options?.afterGenerateHook) {
const conbinedAfterGenerateHooks = combineAfterGenerateHooks(options?.afterGenerateHook)
await conbinedAfterGenerateHooks(result)
}
return result
}
163 changes: 163 additions & 0 deletions src/helper/ssg/ssg.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
AfterResponseHook,
AfterGenerateHook,
FileSystemModule,
ToSSGResult,
} from './ssg'

const resolveRoutesContent = async (res: ReturnType<typeof fetchRoutesContent>) => {
Expand Down Expand Up @@ -627,3 +628,165 @@ describe('disableSSG/onlySSG middlewares', () => {
expect(res.status).toBe(404)
})
})

describe('Request hooks - filterPathsBeforeRequestHook and denyPathsBeforeRequestHook', () => {
let app: Hono
let fsMock: FileSystemModule

const filterPathsBeforeRequestHook = (allowedPaths: string | string[]): BeforeRequestHook => {
const baseURL = 'http://localhost'
return async (req: Request): Promise<Request | false> => {
const paths = Array.isArray(allowedPaths) ? allowedPaths : [allowedPaths]
const pathname = new URL(req.url, baseURL).pathname

if (paths.some((path) => pathname === path || pathname.startsWith(`${path}/`))) {
return req
}

return false
}
}

const denyPathsBeforeRequestHook = (deniedPaths: string | string[]): BeforeRequestHook => {
const baseURL = 'http://localhost'
return async (req: Request): Promise<Request | false> => {
const paths = Array.isArray(deniedPaths) ? deniedPaths : [deniedPaths]
const pathname = new URL(req.url, baseURL).pathname

if (!paths.some((path) => pathname === path || pathname.startsWith(`${path}/`))) {
return req
}
return false
}
}

beforeEach(() => {
app = new Hono()
app.get('/allowed-path', (c) => c.html('Allowed Path Page'))
app.get('/denied-path', (c) => c.html('Denied Path Page'))
app.get('/other-path', (c) => c.html('Other Path Page'))

fsMock = {
writeFile: vi.fn(() => Promise.resolve()),
mkdir: vi.fn(() => Promise.resolve()),
}
})

it('should only process requests for allowed paths with filterPathsBeforeRequestHook', async () => {
const allowedPathsHook = filterPathsBeforeRequestHook(['/allowed-path'])

const result = await toSSG(app, fsMock, {
dir: './static',
beforeRequestHook: allowedPathsHook,
})

expect(result.files.some((file) => file.includes('allowed-path.html'))).toBe(true)
expect(result.files.some((file) => file.includes('other-path.html'))).toBe(false)
})

it('should deny requests for specified paths with denyPathsBeforeRequestHook', async () => {
const deniedPathsHook = denyPathsBeforeRequestHook(['/denied-path'])

const result = await toSSG(app, fsMock, { dir: './static', beforeRequestHook: deniedPathsHook })

expect(result.files.some((file) => file.includes('denied-path.html'))).toBe(false)

expect(result.files.some((file) => file.includes('allowed-path.html'))).toBe(true)
expect(result.files.some((file) => file.includes('other-path.html'))).toBe(true)
})
})

describe('Combined Response hooks - modify response content', () => {
let app: Hono
let fsMock: FileSystemModule

const prependContentAfterResponseHook = (prefix: string): AfterResponseHook => {
return async (res: Response): Promise<Response> => {
const originalText = await res.text()
return new Response(`${prefix}${originalText}`, { ...res })
}
}

const appendContentAfterResponseHook = (suffix: string): AfterResponseHook => {
return async (res: Response): Promise<Response> => {
const originalText = await res.text()
return new Response(`${originalText}${suffix}`, { ...res })
}
}

beforeEach(() => {
app = new Hono()
app.get('/content-path', (c) => c.text('Original Content'))

fsMock = {
writeFile: vi.fn(() => Promise.resolve()),
mkdir: vi.fn(() => Promise.resolve()),
}
})

it('should modify response content with combined AfterResponseHooks', async () => {
const prefixHook = prependContentAfterResponseHook('Prefix-')
const suffixHook = appendContentAfterResponseHook('-Suffix')

const combinedHook = [prefixHook, suffixHook]

await toSSG(app, fsMock, {
dir: './static',
afterResponseHook: combinedHook,
})

// Assert that the response content is modified by both hooks
// This assumes you have a way to inspect the content of saved files or you need to mock/stub the Response text method correctly.
expect(fsMock.writeFile).toHaveBeenCalledWith(
'static/content-path.txt',
'Prefix-Original Content-Suffix'
)
})
})

describe('Combined Generate hooks - AfterGenerateHook', () => {
let app: Hono
let fsMock: FileSystemModule

const logResultAfterGenerateHook = (): AfterGenerateHook => {
return async (result: ToSSGResult): Promise<void> => {
console.log('Generation completed with status:', result.success) // Log the generation success
}
}

const appendFilesAfterGenerateHook = (additionalFiles: string[]): AfterGenerateHook => {
return async (result: ToSSGResult): Promise<void> => {
result.files = result.files.concat(additionalFiles) // Append additional files to the result
}
}

beforeEach(() => {
app = new Hono()
app.get('/path', (c) => c.text('Page Content'))

fsMock = {
writeFile: vi.fn(() => Promise.resolve()),
mkdir: vi.fn(() => Promise.resolve()),
}
})

it('should execute combined AfterGenerateHooks affecting the result', async () => {
const logHook = logResultAfterGenerateHook()
const appendHook = appendFilesAfterGenerateHook(['/extra/file1.html', '/extra/file2.html'])

const combinedHook = [logHook, appendHook]

const consoleSpy = vi.spyOn(console, 'log')
const result = await toSSG(app, fsMock, {
dir: './static',
afterGenerateHook: combinedHook,
})

// Check that the log function was called correctly
expect(consoleSpy).toHaveBeenCalledWith('Generation completed with status:', true)

// Check that additional files were appended to the result
expect(result.files).toContain('/extra/file1.html')
expect(result.files).toContain('/extra/file2.html')
})
})
76 changes: 70 additions & 6 deletions src/helper/ssg/ssg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,66 @@ export type BeforeRequestHook = (req: Request) => Request | false | Promise<Requ
export type AfterResponseHook = (res: Response) => Response | false | Promise<Response | false>
export type AfterGenerateHook = (result: ToSSGResult) => void | Promise<void>

export const combineBeforeRequestHooks = (
hooks: BeforeRequestHook | BeforeRequestHook[]
): BeforeRequestHook => {
if (!Array.isArray(hooks)) {
return hooks
}
return async (req: Request): Promise<Request | false> => {
let currentReq = req
for (const hook of hooks) {
const result = await hook(currentReq)
if (result === false) {
return false
}
if (result instanceof Request) {
currentReq = result
}
}
return currentReq
}
}

export const combineAfterResponseHooks = (
hooks: AfterResponseHook | AfterResponseHook[]
): AfterResponseHook => {
if (!Array.isArray(hooks)) {
return hooks
}
return async (res: Response): Promise<Response | false> => {
let currentRes = res
for (const hook of hooks) {
const result = await hook(currentRes)
if (result === false) {
return false
}
if (result instanceof Response) {
currentRes = result
}
}
return currentRes
}
}

export const combineAfterGenerateHooks = (
hooks: AfterGenerateHook | AfterGenerateHook[]
): AfterGenerateHook => {
if (!Array.isArray(hooks)) {
return hooks
}
return async (result: ToSSGResult): Promise<void> => {
for (const hook of hooks) {
await hook(result)
}
}
}

export interface ToSSGOptions {
dir?: string
beforeRequestHook?: BeforeRequestHook
afterResponseHook?: AfterResponseHook
afterGenerateHook?: AfterGenerateHook
beforeRequestHook?: BeforeRequestHook | BeforeRequestHook[]
afterResponseHook?: AfterResponseHook | AfterResponseHook[]
afterGenerateHook?: AfterGenerateHook | AfterGenerateHook[]
concurrency?: number
extensionMap?: Record<string, string>
}
Expand Down Expand Up @@ -286,10 +341,16 @@ export const toSSG: ToSSGInterface = async (app, fs, options) => {
const outputDir = options?.dir ?? './static'
const concurrency = options?.concurrency ?? DEFAULT_CONCURRENCY

const combinedBeforeRequestHook = combineBeforeRequestHooks(
options?.beforeRequestHook || ((req) => req)
)
const combinedAfterResponseHook = combineAfterResponseHooks(
options?.afterResponseHook || ((req) => req)
)
const getInfoGen = fetchRoutesContent(
app,
options?.beforeRequestHook,
options?.afterResponseHook,
combinedBeforeRequestHook,
combinedAfterResponseHook,
concurrency
)
for (const getInfo of getInfoGen) {
Expand Down Expand Up @@ -319,6 +380,9 @@ export const toSSG: ToSSGInterface = async (app, fs, options) => {
const errorObj = error instanceof Error ? error : new Error(String(error))
result = { success: false, files: [], error: errorObj }
}
await options?.afterGenerateHook?.(result)
if (options?.afterGenerateHook) {
const conbinedAfterGenerateHooks = combineAfterGenerateHooks(options?.afterGenerateHook)
await conbinedAfterGenerateHooks(result)
}
return result
}

0 comments on commit 04caa07

Please sign in to comment.