spooder 6.2.0 → 6.2.2

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.
Files changed (3) hide show
  1. package/README.md +28 -0
  2. package/package.json +1 -1
  3. package/src/api.ts +21 -5
package/README.md CHANGED
@@ -2520,6 +2520,34 @@ await parse_template(..., {
2520
2520
  </t-for>
2521
2521
  ```
2522
2522
 
2523
+ #### Object Serialization
2524
+
2525
+ When a replacement value is an object or array, it is automatically serialized to JSON. This allows server-side data to be embedded directly into client-side scripts.
2526
+
2527
+ ```ts
2528
+ const config = {
2529
+ debug: true,
2530
+ api_url: '/api/v1',
2531
+ features: ['auth', 'logging']
2532
+ };
2533
+
2534
+ await parse_template('<script>const CONFIG = {{config}};</script>', { config });
2535
+ // Result: "<script>const CONFIG = {"debug":true,"api_url":"/api/v1","features":["auth","logging"]};</script>"
2536
+ ```
2537
+
2538
+ This also works with nested objects accessed via dot notation:
2539
+
2540
+ ```ts
2541
+ const data = {
2542
+ app: {
2543
+ settings: { theme: 'dark', lang: 'en' }
2544
+ }
2545
+ };
2546
+
2547
+ await parse_template('<script>const SETTINGS = {{app.settings}};</script>', data);
2548
+ // Result: "<script>const SETTINGS = {"theme":"dark","lang":"en"};</script>"
2549
+ ```
2550
+
2523
2551
  <a id="api-cache-busting"></a>
2524
2552
  ## API > Cache Busting
2525
2553
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "spooder",
3
3
  "author": "Kruithne <kruithne@gmail.com>",
4
4
  "type": "module",
5
- "version": "6.2.0",
5
+ "version": "6.2.2",
6
6
  "module": "./src/api.ts",
7
7
  "repository": {
8
8
  "url": "https://github.com/Kruithne/spooder"
package/src/api.ts CHANGED
@@ -1040,8 +1040,12 @@ export async function parse_template(template: string, replacements: Replacement
1040
1040
  replacement = await replacement();
1041
1041
  }
1042
1042
 
1043
- if (replacement !== undefined)
1043
+ if (replacement !== undefined) {
1044
+ if (typeof replacement === 'object' && replacement !== null)
1045
+ return JSON.stringify(replacement);
1046
+
1044
1047
  return replacement;
1048
+ }
1045
1049
 
1046
1050
  if (!drop_missing)
1047
1051
  return match;
@@ -1332,6 +1336,8 @@ export const HTTP_STATUS_CODE = {
1332
1336
  type HTTP_METHOD = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE';
1333
1337
  type HTTP_METHODS = HTTP_METHOD|HTTP_METHOD[];
1334
1338
 
1339
+ type BodylessMethod = 'GET' | 'HEAD';
1340
+
1335
1341
  export function http_apply_range(file: BunFile, request: Request): BunFile {
1336
1342
  const range_header = request.headers.get('range');
1337
1343
  if (range_header !== null) {
@@ -1384,7 +1390,7 @@ type ErrorHandler = (err: Error, req: Request, url: URL) => Resolvable<Response>
1384
1390
  type DefaultHandler = (req: Request, status_code: number) => HandlerReturnType;
1385
1391
  type StatusCodeHandler = (req: Request) => HandlerReturnType;
1386
1392
 
1387
- type JSONRequestHandler = (req: Request, url: URL, json: JsonObject | null) => HandlerReturnType;
1393
+ type JSONRequestHandler<T extends JsonObject | null = JsonObject> = (req: Request, url: URL, json: T) => HandlerReturnType;
1388
1394
 
1389
1395
  export type ServerSentEventClient = {
1390
1396
  message: (message: string) => void;
@@ -1823,10 +1829,20 @@ export function http_serve(port: number, hostname?: string) {
1823
1829
  log_spooder(`server started on port {${server.port}} (host: {${hostname ?? 'unspecified'}})`);
1824
1830
 
1825
1831
  type ThrottleHandler = {
1826
- (delta: number, handler: JSONRequestHandler): JSONRequestHandler;
1832
+ (delta: number, handler: JSONRequestHandler<JsonObject>): JSONRequestHandler<JsonObject>;
1833
+ (delta: number, handler: JSONRequestHandler<null>): JSONRequestHandler<null>;
1827
1834
  (delta: number, handler: RequestHandler): RequestHandler;
1828
1835
  };
1829
1836
 
1837
+ type JsonEndpointHandler = {
1838
+ <M extends BodylessMethod>(path: string, handler: JSONRequestHandler<null>, method: M): void;
1839
+ <M extends BodylessMethod[]>(path: string, handler: JSONRequestHandler<null>, method: M): void;
1840
+ <M extends Exclude<HTTP_METHOD, BodylessMethod>>(path: string, handler: JSONRequestHandler<JsonObject>, method: M): void;
1841
+ <M extends Exclude<HTTP_METHOD, BodylessMethod>[]>(path: string, handler: JSONRequestHandler<JsonObject>, method: M): void;
1842
+ <M extends HTTP_METHOD[]>(path: string, handler: JSONRequestHandler<JsonObject | null>, method: M): void;
1843
+ (path: string, handler: JSONRequestHandler<JsonObject>): void;
1844
+ };
1845
+
1830
1846
  return {
1831
1847
  /** Register a handler for a specific route. */
1832
1848
  route: (path: string, handler: RequestHandler, method: HTTP_METHODS = 'GET'): void => {
@@ -1853,7 +1869,7 @@ export function http_serve(port: number, hostname?: string) {
1853
1869
  }) as ThrottleHandler,
1854
1870
 
1855
1871
  /** Register a JSON endpoint with automatic content validation. */
1856
- json: (path: string, handler: JSONRequestHandler, method: HTTP_METHODS = 'POST'): void => {
1872
+ json: ((path: string, handler: JSONRequestHandler<JsonObject | null>, method: HTTP_METHODS = 'POST'): void => {
1857
1873
  const json_wrapper: RequestHandler = async (req: Request, url: URL) => {
1858
1874
  // handle CORS preflight
1859
1875
  if (req.method === 'OPTIONS') {
@@ -1890,7 +1906,7 @@ export function http_serve(port: number, hostname?: string) {
1890
1906
 
1891
1907
  const methods: HTTP_METHODS = Array.isArray(method) ? [...method, 'OPTIONS'] : [method, 'OPTIONS'];
1892
1908
  routes.push([path.split('/'), json_wrapper, methods]);
1893
- },
1909
+ }) as JsonEndpointHandler,
1894
1910
 
1895
1911
  /** Unregister a specific route */
1896
1912
  unroute: (path: string): void => {