alien-middleware 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { AdapterRequestContext, HattipHandler } from '@hattip/core';
2
+ import { Any } from 'radashi';
2
3
 
3
4
  type RequestPlugin = {
4
5
  /**
@@ -22,11 +23,19 @@ type MiddlewareTypes<TProperties extends object = any, TEnv extends object = any
22
23
  env: TEnv;
23
24
  };
24
25
  interface HattipContext<TPlatform, TEnv extends object> extends AdapterRequestContext<TPlatform> {
25
- env<K extends keyof TEnv>(key: K): TEnv[K];
26
+ /**
27
+ * The `request.url` string parsed into a `URL` object. Parsing is performed
28
+ * on-demand and the result is cached.
29
+ */
30
+ url: URL;
31
+ env<K extends keyof TEnv>(key: Extract<K, string>): TEnv[K];
26
32
  env(key: never): string | undefined;
27
33
  }
28
- type CastNever<T, U> = [T] extends [never] ? U : T;
29
- type RequestContext<TProperties extends object = {}, TEnv extends object = {}, TPlatform = unknown> = HattipContext<TPlatform, TEnv> & CastNever<TProperties, {}>;
34
+ /**
35
+ * Converts a type `T` to something that can be intersected with an object.
36
+ */
37
+ type Intersectable<T extends object> = [T] extends [never] ? {} : [T] extends [Any] ? Record<PropertyKey, any> : T;
38
+ type RequestContext<TProperties extends object = any, TEnv extends object = any, TPlatform = any> = HattipContext<TPlatform, TEnv> & Intersectable<TProperties>;
30
39
  type Awaitable<T> = T | PromiseLike<T>;
31
40
  type RequestMiddleware<T extends MiddlewareChain = MiddlewareChain> = (context: RequestContext<InputProperties<T>, InputEnv<T>, Platform<T>>) => Awaitable<Response | RequestPlugin | void>;
32
41
  type ResponseMiddleware<T extends MiddlewareChain = MiddlewareChain> = (context: RequestContext<InputProperties<T>, InputEnv<T>, Platform<T>>, response: Response) => Awaitable<Response | void>;
@@ -38,7 +47,7 @@ type RequestHandler<TInputs extends MiddlewareTypes, TCurrent extends Middleware
38
47
  * response middleware. This means it will run *after* a `Response` is generated
39
48
  * by a request middleware.
40
49
  */
41
- type Middleware<TProperties extends object = {}, TEnv extends object = {}, TPlatform = unknown> = (context: RequestContext<TProperties, TEnv, TPlatform>, response: Response) => Awaitable<Response | RequestPlugin | void>;
50
+ type Middleware<TProperties extends object = any, TEnv extends object = any, TPlatform = unknown> = (context: RequestContext<TProperties, TEnv, TPlatform>, response: Response) => Awaitable<Response | RequestPlugin | void>;
42
51
  type Merge<TSource extends object, TOverrides extends object | undefined> = {} & (TOverrides extends object ? {
43
52
  [K in keyof TSource | keyof TOverrides]: K extends keyof TOverrides ? TOverrides[K] : K extends keyof TSource ? TSource[K] : never;
44
53
  } : TSource);
@@ -82,7 +91,7 @@ TPlatform = any> {
82
91
  */
83
92
  use<const TMiddleware extends Middleware<TCurrent['properties'], TCurrent['env'], TPlatform>>(middleware: TMiddleware): ApplyMiddleware<this, TMiddleware>;
84
93
  }
85
- declare function chain<TPlatform = unknown, TEnv extends object = {}, TProperties extends object = {}>(): MiddlewareChain<{
94
+ declare function chain<TProperties extends object = {}, TEnv extends object = {}, TPlatform = unknown>(): MiddlewareChain<{
86
95
  env: TEnv;
87
96
  properties: TProperties;
88
97
  }, {
@@ -91,4 +100,4 @@ declare function chain<TPlatform = unknown, TEnv extends object = {}, TPropertie
91
100
  }, TPlatform>;
92
101
  declare function chain<const T extends Middleware = Middleware>(middleware: T): ApplyFirstMiddleware<T>;
93
102
 
94
- export { MiddlewareChain, chain };
103
+ export { type Middleware, MiddlewareChain, type RequestContext, type RequestHandler, type RequestMiddleware, type RequestPlugin, type ResponseMiddleware, chain };
package/dist/index.js CHANGED
@@ -15,6 +15,14 @@ var kRequestChain = Symbol("requestChain");
15
15
  var kResponseChain = Symbol("responseChain");
16
16
  var kIgnoreNotFound = Symbol("ignoreNotFound");
17
17
  var kMiddlewareCache = Symbol("middlewareCache");
18
+ var urlDescriptor = {
19
+ configurable: true,
20
+ get() {
21
+ const url = new URL(this.request.url);
22
+ Object.defineProperty(this, "url", { value: url });
23
+ return url;
24
+ }
25
+ };
18
26
  var MiddlewareChain = class _MiddlewareChain {
19
27
  [kRequestChain] = [];
20
28
  [kResponseChain] = [];
@@ -36,6 +44,9 @@ var MiddlewareChain = class _MiddlewareChain {
36
44
  async function handler(parentContext) {
37
45
  const context = Object.create(parentContext);
38
46
  context[kIgnoreNotFound] = true;
47
+ if (!("url" in context)) {
48
+ Object.defineProperty(context, "url", urlDescriptor);
49
+ }
39
50
  const cache = context[kMiddlewareCache] ||= /* @__PURE__ */ new Set();
40
51
  let response;
41
52
  let env;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "alien-middleware",
3
3
  "type": "module",
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
5
  "exports": {
6
6
  "types": "./dist/index.d.ts",
7
7
  "default": "./dist/index.js"
package/readme.md CHANGED
@@ -27,8 +27,8 @@ import { chain } from 'alien-middleware'
27
27
  const app = chain()
28
28
 
29
29
  // Or create a chain with an initial middleware
30
- const appWithInitial = chain(ctx => {
31
- console.log('Initial middleware running for:', ctx.request.url)
30
+ const appWithInitial = chain(context => {
31
+ console.log('Initial middleware running for:', context.request.url)
32
32
  })
33
33
  ```
34
34
 
@@ -39,12 +39,12 @@ Use the `.use()` method to add middleware functions to the chain. Each call to `
39
39
  ```typescript
40
40
  import type { RequestContext } from 'alien-middleware'
41
41
 
42
- const firstMiddleware = (ctx: RequestContext) => {
42
+ const firstMiddleware = (context: RequestContext) => {
43
43
  console.log('First middleware')
44
44
  // Doesn't return anything, so the chain continues
45
45
  }
46
46
 
47
- const secondMiddleware = (ctx: RequestContext) => {
47
+ const secondMiddleware = (context: RequestContext) => {
48
48
  console.log('Second middleware')
49
49
  return new Response('Hello from middleware!', { status: 200 })
50
50
  // Returns a Response, terminating the request-phase chain
@@ -89,8 +89,8 @@ Request middleware runs sequentially before a `Response` is generated.
89
89
  - **Terminating the Chain:** Return a `Response` object to stop processing subsequent request middleware.
90
90
 
91
91
  ```typescript
92
- const earlyResponder = (ctx: RequestContext) => {
93
- if (ctx.request.url.endsWith('/forbidden')) {
92
+ const earlyResponder = (context: RequestContext) => {
93
+ if (context.request.url.endsWith('/forbidden')) {
94
94
  return new Response('Forbidden', { status: 403 })
95
95
  }
96
96
  // Otherwise, continue the chain
@@ -100,7 +100,7 @@ Request middleware runs sequentially before a `Response` is generated.
100
100
  - **Extending Context:** Return an object with a `define` property to add properties to the context for _downstream_ middleware.
101
101
 
102
102
  ```typescript
103
- const addUser = (ctx: RequestContext) => {
103
+ const addUser = (context: RequestContext) => {
104
104
  // In a real app, you might look up a user based on a token
105
105
  const user = { id: 1, name: 'Alice' }
106
106
 
@@ -108,24 +108,24 @@ Request middleware runs sequentially before a `Response` is generated.
108
108
  }
109
109
 
110
110
  const greetUser = (
111
- ctx: RequestContext<{ user: { id: number; name: string } }>
111
+ context: RequestContext<{ user: { id: number; name: string } }>
112
112
  ) => {
113
113
  // The `user` property is now available thanks to `addUser`
114
- return new Response(`Hello, ${ctx.user.name}!`)
114
+ return new Response(`Hello, ${context.user.name}!`)
115
115
  }
116
116
 
117
117
  const app = chain().use(addUser).use(greetUser)
118
118
  ```
119
119
 
120
- - **Extending Environment:** Return an object with an `env` property to add environment variables accessible via `ctx.env()`.
120
+ - **Extending Environment:** Return an object with an `env` property to add environment variables accessible via `context.env()`.
121
121
 
122
122
  ```typescript
123
- const addApiKey = (ctx: RequestContext) => {
123
+ const addApiKey = (context: RequestContext) => {
124
124
  return { env: { API_KEY: 'secret123' } }
125
125
  }
126
126
 
127
- const useApiKey = (ctx: RequestContext<{}, { API_KEY: string }>) => {
128
- const key = ctx.env('API_KEY')
127
+ const useApiKey = (context: RequestContext<{}, { API_KEY: string }>) => {
128
+ const key = context.env('API_KEY')
129
129
  console.log('API Key:', key) // Output: API Key: secret123
130
130
  }
131
131
 
@@ -147,11 +147,11 @@ Request middleware runs sequentially before a `Response` is generated.
147
147
  Response middleware runs _after_ a `Response` has been generated by a request middleware or the final handler. It receives both the context and the generated `Response`.
148
148
 
149
149
  ```typescript
150
- const poweredByMiddleware = (ctx: RequestContext, response: Response) => {
150
+ const poweredByMiddleware = (context: RequestContext, response: Response) => {
151
151
  response.headers.set('X-Powered-By', 'alien-middleware')
152
152
  }
153
153
 
154
- const mainHandler = (ctx: RequestContext) => {
154
+ const mainHandler = (context: RequestContext) => {
155
155
  return new Response('Main content')
156
156
  }
157
157
 
@@ -177,17 +177,17 @@ another chain, since the outer chain will still have a chance to return a
177
177
  You can compose middleware by nesting chains using `.use()`. Context modifications (`define`, `env`) within a nested chain are scoped to that chain and do not affect middleware outside of it.
178
178
 
179
179
  ```typescript
180
- const innerChain = chain((ctx: RequestContext) => {
180
+ const innerChain = chain((context: RequestContext) => {
181
181
  console.log('Inner chain start')
182
182
  return { define: { innerData: 'secret' } } // Only available inside innerChain
183
- }).use((ctx: RequestContext<{ innerData: string }>) => {
184
- console.log('Accessing inner data:', ctx.innerData)
183
+ }).use((context: RequestContext<{ innerData: string }>) => {
184
+ console.log('Accessing inner data:', context.innerData)
185
185
  })
186
186
 
187
- const outerMiddleware = (ctx: RequestContext) => {
188
- // ctx.innerData is not accessible here
187
+ const outerMiddleware = (context: RequestContext) => {
188
+ // context.innerData is not accessible here
189
189
  console.log('Outer middleware after inner chain')
190
- if (!('innerData' in ctx)) {
190
+ if (!('innerData' in context)) {
191
191
  console.log('innerData is correctly scoped.')
192
192
  }
193
193
  return new Response('Finished')
@@ -202,3 +202,36 @@ const finalApp = chain().use(innerChain).use(outerMiddleware)
202
202
  ```
203
203
 
204
204
  If a nested chain does not return a `Response`, execution continues with the next middleware in the outer chain.
205
+
206
+ ### Safe Environment Variables
207
+
208
+ When writing a Hattip handler without this package, the `context.env()` method is inherently unsafe. Its return type is always `string | undefined`, which means you either need to write defensive checks or use type assertions. Neither is ideal.
209
+
210
+ With alien-middleware, you **must** declare an environment variable's type in order to use it.
211
+
212
+ ```typescript
213
+ import { chain } from 'alien-middleware'
214
+
215
+ // A common pattern is to declare a dedicated type for the environment variables.
216
+ type Env = {
217
+ API_KEY: string
218
+ }
219
+
220
+ const app = chain<any, Env>().use(context => {
221
+ const key = context.env('API_KEY')
222
+ // ^? string
223
+ })
224
+ ```
225
+
226
+ When defining a middleware, you can declare env types that the middleware expects to use.
227
+
228
+ ```typescript
229
+ import type { RequestContext } from 'alien-middleware'
230
+
231
+ // Assuming `Env` is defined like in the previous example.
232
+ const myMiddleware = (context: RequestContext<any, Env>) => {
233
+ const key = context.env('API_KEY')
234
+ }
235
+ ```
236
+
237
+ In both examples, we skip declaring any additional context properties (the first type parameter) because we're not using any. The second type parameter is for environment variables. The third is for the special `context.platform` property, whose value is provided by the host platform (e.g. Node.js, Deno, Bun, etc). On principle, a middleware should avoid using the `context.platform` property, since that could make it non-portable unless you write extra fallback logic for other hosts.