-
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into feat/tests-and-logs
- Loading branch information
Showing
28 changed files
with
521 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@inox-tools/astro-tests': patch | ||
--- | ||
|
||
Export `TestApp` type |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@inox-tools/cut-short': minor | ||
--- | ||
|
||
Implement cut-short integration |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@inox-tools/utils': patch | ||
--- | ||
|
||
Add `MaybePromise`, `MaybeFactory` and `MaybeThunk` utility types. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
--- | ||
title: Cut-Short Requests | ||
--- | ||
|
||
Cut-short is an Astro integration that lets you stop processing a request instantly and send back a custom response, simplifying control flow in your Astro applications. By introducing the `endRequest` function, it eliminates the need for cumbersome workarounds like bubbling up response objects, throwing and catching sentinel errors, implementing custom middleware logic or replicating error response logic across all your pages. | ||
|
||
Keep the the custom response for specific conditions close to the conditions and have it shared across all your application. It's especially useful for scenarios like user authentication and access control, where you might need to redirect users to sign-in page from anywhere that requires authentication or to turn any page they don't have access to into a 404 to avoid information leak (like GitHub does). | ||
|
||
## Installing the integration | ||
|
||
import { PackageManagers } from 'starlight-package-managers'; | ||
|
||
<PackageManagers type="exec" pkg="astro" args="add @inox-tools/cut-short" /> | ||
|
||
## How to use | ||
|
||
From any code that is reachable from a page rendering context can use the `endRequest` function to stop the. | ||
|
||
A page-rendering context is when you are inside of: | ||
|
||
- A middleware; | ||
- The frontmatter of a page component (not components _in_ the page, see [streaming](#streaming)); | ||
- An API endpoint; | ||
- A function called from another page-rendering context. | ||
|
||
import { Tabs, TabItem } from '@astrojs/starlight/components'; | ||
|
||
<Tabs> | ||
<TabItem label="Common module"> | ||
|
||
```ts title="src/lib/auth.js" | ||
import { endRequest } from '@it-astro/cut-short'; | ||
|
||
export function getUser() { | ||
if (noUser) endRequest(new Response('No user', { status: 401 })); | ||
|
||
return {...}; | ||
} | ||
``` | ||
|
||
</TabItem> | ||
<TabItem label="Page"> | ||
|
||
```astro title="src/pages/dashboard.astro" | ||
--- | ||
import { getUser } from '../lib/auth'; | ||
|
||
// Calling a function from a rendering context allows it to stop the request. | ||
const user = getUser(); | ||
--- | ||
|
||
<Dashboard /> | ||
``` | ||
|
||
</TabItem> | ||
<TabItem label="Endpoint"> | ||
|
||
```ts title="src/pages/userId.ts" | ||
import { endRequest } from '@it-astro/cut-short'; | ||
import { getUser } from '../lib/auth'; | ||
|
||
export const GET = () => { | ||
if (someCondition) endRequest(new Response('Matched inline blocking condition')); | ||
|
||
// Calling a function from a rendering context allows it to stop the request. | ||
const user = getUser(); | ||
|
||
return new Response(user.id); | ||
}; | ||
``` | ||
|
||
</TabItem> | ||
</Tabs> | ||
|
||
### `endRequest()` | ||
|
||
**Type:** `(withResponse: Response | (() => Response | Promise<Response>)) => void` | ||
|
||
Stop the current request and send back a custom response. | ||
|
||
The argument can be a `Response` object to be used directly or a function that returns a `Response` object or a promise that resolves to a `Response`. | ||
|
||
## Streaming | ||
|
||
Once Astro executes the frontmatter of the page component, the HTML response is streamed to the client _as it is rendered_. This means that when the frontmatter of components deep in the page is executed the response has already been partially sent. In the example below, when `MyComponent` is executed, the response has already been constructed and is being streamed. | ||
|
||
```astro title="src/pages/index.astro" | ||
--- | ||
import MyComponent from '../components/MyComponent.astro'; | ||
--- | ||
|
||
<html> | ||
<head> | ||
<title>Example</title> | ||
</head> | ||
<body> | ||
<MyComponent /> | ||
</body> | ||
</html> | ||
``` | ||
|
||
This prevents components from changing the status code of the response and from completely switching the response under some condition. For that reason, calling `endRequest` from a component _in_ the page is not allowed, just like returning a response: | ||
|
||
```astro title="src/components/MyComponent.astro" | ||
--- | ||
import { endRequest } from '@it-astro/cut-short'; | ||
|
||
// Neither of these is allowed | ||
endRequest(new Response('Page not found', { status: 404 })); | ||
return new Response('Page not found', { status: 404 }); | ||
--- | ||
``` | ||
|
||
## License | ||
|
||
Cut-Short Requests is available under the MIT license. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import type { AstroGlobal } from 'astro'; | ||
import { endRequest } from '@it-astro:cut-short'; | ||
|
||
export function getUser(Astro: AstroGlobal): { id: string; permissions: string[] } { | ||
const cookie = Astro.cookies.get('username'); | ||
if (cookie === undefined) { | ||
endRequest(Astro.redirect('/signin')); | ||
} | ||
|
||
return { | ||
id: cookie.value, | ||
permissions: [], | ||
}; | ||
} | ||
|
||
export function validateUserPermisssion(Astro: AstroGlobal, permission: string): void { | ||
const user = getUser(Astro); | ||
|
||
if (!user.permissions.includes(permission)) { | ||
endRequest(Astro.redirect('/404')); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<p align="center"> | ||
<img alt="InoxTools" width="350px" src="https://github.com/Fryuni/inox-tools/blob/main/assets/shield.png?raw=true"/> | ||
</p> | ||
|
||
# Cut Short | ||
|
||
Immediately halt request processing and return custom responses effortlessly. | ||
|
||
## Install | ||
|
||
```js | ||
npm i @inox-tools/cut-short | ||
``` | ||
|
||
Add the integration to your `astro.config.mjs`: | ||
|
||
```js | ||
// astro.config.mjs | ||
import { defineConfig } from 'astro' | ||
import cutShort from '@inox-tools/cut-short'; | ||
|
||
export default defineConfig({ | ||
integrations: [cutShort({})] | ||
}) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
node_modules | ||
*.log | ||
src |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
{ | ||
"name": "@inox-tools/cut-short", | ||
"version": "0.0.0", | ||
"description": "Immediately halt request processing and return custom responses effortlessly.", | ||
"keywords": [ | ||
"astro-integration", | ||
"withastro", | ||
"astro" | ||
], | ||
"license": "MIT", | ||
"author": "Luiz Ferraz <[email protected]>", | ||
"type": "module", | ||
"exports": { | ||
".": { | ||
"types": "./dist/index.d.ts", | ||
"default": "./dist/index.js" | ||
} | ||
}, | ||
"files": [ | ||
"dist", | ||
"src", | ||
"virtual.d.ts" | ||
], | ||
"scripts": { | ||
"build": "tsup", | ||
"dev": "tsup --watch", | ||
"prepublish": "pnpm run build", | ||
"test": "vitest run --coverage", | ||
"test:dev": "vitest --coverage.enabled=true" | ||
}, | ||
"dependencies": { | ||
"@inox-tools/utils": "workspace:^", | ||
"astro-integration-kit": "catalog:", | ||
"debug": "catalog:" | ||
}, | ||
"devDependencies": { | ||
"@inox-tools/astro-tests": "workspace:", | ||
"@types/node": "catalog:", | ||
"@vitest/coverage-v8": "catalog:", | ||
"@vitest/ui": "catalog:", | ||
"jest-extended": "catalog:", | ||
"astro": "catalog:", | ||
"tsup": "catalog:", | ||
"typescript": "catalog:", | ||
"vite": "catalog:", | ||
"vitest": "catalog:" | ||
}, | ||
"peerDependencies": { | ||
"astro": "catalog:lax" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { defineIntegration, addVitePlugin, createResolver } from 'astro-integration-kit'; | ||
import { z } from 'astro/zod'; | ||
import { debug } from './internal/debug.js'; | ||
|
||
export default defineIntegration({ | ||
name: '@inox-tools/cut-short', | ||
optionsSchema: z.never().optional(), | ||
setup() { | ||
const { resolve } = createResolver(import.meta.url); | ||
|
||
return { | ||
hooks: { | ||
'astro:config:setup': (params) => { | ||
params.addMiddleware({ | ||
entrypoint: resolve('./runtime/middleware.js'), | ||
order: 'post', | ||
}); | ||
|
||
addVitePlugin(params, { | ||
warnDuplicated: true, | ||
plugin: { | ||
name: '@inox-tools/cut-short', | ||
enforce: 'pre', | ||
resolveId(source) { | ||
if (source === '@it-astro:cut-short') { | ||
return resolve('./runtime/entrypoint.js'); | ||
} | ||
}, | ||
}, | ||
}); | ||
}, | ||
'astro:config:done': (params) => { | ||
// Check if the version of Astro being used has the `injectTypes` utility. | ||
if (typeof params.injectTypes === 'function') { | ||
debug('Injecting types in .astro structure'); | ||
params.injectTypes({ | ||
filename: 'types.d.ts', | ||
content: "import '@inox-tools/cut-short';", | ||
}); | ||
} | ||
}, | ||
}, | ||
}; | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { loadThunkValue, type MaybeThunk } from '@inox-tools/utils/values'; | ||
|
||
export class CarrierError extends Error { | ||
public constructor(private readonly response: MaybeThunk<Response>) { | ||
super('CarrierError'); | ||
} | ||
|
||
public getResponse(): Promise<Response> { | ||
return Promise.resolve(loadThunkValue(this.response)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import debugC from 'debug'; | ||
|
||
export const debug = debugC('inox-tools:cut-short') | ||
|
||
export const getDebug = (segment?: string) => { | ||
return segment ? debug.extend(segment) : debug; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import type { MaybeThunk } from '@inox-tools/utils/values'; | ||
import { CarrierError } from '../internal/carrier.js'; | ||
|
||
export const endRequest = (withResponse: MaybeThunk<Response>): never => { | ||
throw new CarrierError(withResponse); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import type { MiddlewareHandler } from 'astro'; | ||
import { debug } from '../internal/debug.js'; | ||
import { CarrierError } from '../internal/carrier.js'; | ||
|
||
export const onRequest: MiddlewareHandler = async (_, next) => { | ||
try { | ||
return await next(); | ||
} catch (err: unknown) { | ||
if (err instanceof CarrierError) { | ||
debug('Returning response from CarrierError'); | ||
return err.getResponse(); | ||
} | ||
|
||
throw err; | ||
} | ||
}; |
Oops, something went wrong.