Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ssg): enhance conbined hooks #2686

Merged
merged 4 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}