spooder 4.2.17 → 4.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.
Files changed (3) hide show
  1. package/README.md +58 -18
  2. package/package.json +1 -1
  3. package/src/api.ts +70 -14
package/README.md CHANGED
@@ -200,7 +200,9 @@ When starting or restarting a server process, `spooder` can automatically update
200
200
  {
201
201
  "spooder": {
202
202
  "update": [
203
- "git pull",
203
+ "git reset --hard",
204
+ "git clean -fd",
205
+ "git pull origin main",
204
206
  "bun install"
205
207
  ]
206
208
  }
@@ -210,7 +212,7 @@ When starting or restarting a server process, `spooder` can automatically update
210
212
  Each command should be a separate entry in the array and will be executed in sequence. The server process will be started once all commands have resolved.
211
213
 
212
214
  > [!IMPORTANT]
213
- > Chainging commands using `&&` or `||` operators does not work.
215
+ > Chaining commands using `&&` or `||` operators does not work.
214
216
 
215
217
  If a command in the sequence fails, the remaining commands will not be executed, however the server will still be started. This is preferred over entering a restart loop or failing to start the server at all.
216
218
 
@@ -435,7 +437,7 @@ In addition to the information provided by the developer, `spooder` also include
435
437
  "modules": "108"
436
438
  },
437
439
  "bun": {
438
- "version": "0.6.4",
440
+ "version": "0.6.5",
439
441
  "rev": "f02561530fda1ee9396f51c8bc99b38716e38296",
440
442
  "memory_usage": {
441
443
  "rss": 99672064,
@@ -716,6 +718,9 @@ server.route('/api/endpoint', async (req, url) => {
716
718
 
717
719
  try {
718
720
  const json = await req.json();
721
+ if (json === null || typeof json !== 'object' || Array.isArray(json))
722
+ return 400;
723
+
719
724
  // do something with json.
720
725
  return 200;
721
726
  } catch (err) {
@@ -735,6 +740,9 @@ server.route('/api/endpoint', validate_req_json(async (json, req, url) => {
735
740
 
736
741
  This behaves the same as the code above, where a `400` status code is returned if the `Content-Type` header is not `application/json` or if the request body is not valid JSON, and no error is thrown.
737
742
 
743
+ > [!NOTE]
744
+ > While arrays and other primitives are valid JSON, `validate_req_json` will only pass objects to the handler, since they are the most common use case for JSON request bodies and it removes the need to validate that in the handler. If you need to use arrays or other primitives, either box them in an object or provide your own validation.
745
+
738
746
  <a id="api-routing-directory-serving"></a>
739
747
  ## API > Routing > Directory Serving
740
748
 
@@ -772,7 +780,7 @@ function default_directory_handler(file_path: string, file: BunFile, stat: DirSt
772
780
  ```
773
781
 
774
782
  > [!NOTE]
775
- > Uncaught `ENOENT` errors throw from the directory handler will return a `404` response, other errors will return a `500` response.
783
+ > Uncaught `ENOENT` errors thrown from the directory handler will return a `404` response, other errors will return a `500` response.
776
784
 
777
785
  > [!NOTE]
778
786
  > The call to `apply_range` in the default directory handler will automatically slice the file based on the [`Range`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range) header. This function is also exposed as part of the `spooder` API for use in your own handlers.
@@ -1058,6 +1066,24 @@ const html = parse_template(template, replacements);
1058
1066
  </html>
1059
1067
  ```
1060
1068
 
1069
+ Object properties are supported by using the dot syntax.
1070
+
1071
+ ```ts
1072
+ const template = `<span>{$foo.bar}</span>`;
1073
+
1074
+ const replacements = {
1075
+ foo: {
1076
+ bar: 'Hello, world!';
1077
+ }
1078
+ };
1079
+
1080
+ const html = parse_template(template, replacements);
1081
+ ```
1082
+
1083
+ ```html
1084
+ <span>Hello, world!</span>
1085
+ ```
1086
+
1061
1087
  By default, placeholders that do not appear in the replacement object will be left as-is. Set `drop_missing` to `true` to remove them.
1062
1088
 
1063
1089
  ```ts
@@ -1099,16 +1125,15 @@ parse_template('Hello {$world}', replacer);
1099
1125
  </body>
1100
1126
  </html>
1101
1127
  ```
1102
-
1103
1128
  `parse_template` supports looping arrays with the following syntax.
1104
1129
 
1105
1130
  ```html
1106
- {$for:foo}My colour is %s{/for}
1131
+ {$for:foo as bar}My colour is {$bar}{/for}
1107
1132
  ```
1108
1133
  ```ts
1109
1134
  const template = `
1110
1135
  <ul>
1111
- {$for:foo}<li>%s</li>{/for}
1136
+ {$for:foo as bar}<li>{$bar}</li>{/for}
1112
1137
  </ul>
1113
1138
  `;
1114
1139
 
@@ -1127,24 +1152,39 @@ const html = parse_template(template, replacements);
1127
1152
  </ul>
1128
1153
  ```
1129
1154
 
1130
- All placeholders inside a `{$for:}` loop are substituted, but only if the loop variable exists.
1131
-
1132
- In the following example, `missing` does not exist, so `test` is not substituted inside the loop, but `test` is still substituted outside the loop.
1155
+ Loops also support object properties using the dot syntax.
1133
1156
 
1134
1157
  ```html
1135
- <div>Hello {$test}!</div>
1136
- {$for:missing}<div>Loop {$test}</div>{/for}
1158
+ {$for:foo as bar}My colour is {$bar.baz}{/for}
1137
1159
  ```
1138
-
1139
1160
  ```ts
1140
- parse_template(..., {
1141
- test: 'world'
1142
- });
1161
+ const template = `
1162
+ <ul>
1163
+ {$for:foo as bar}<li>My colour is {$bar.baz}</li>{/for}
1164
+ </ul>
1165
+ `;
1143
1166
  ```
1144
1167
 
1168
+ If/else statements are supported in templates, which include blocks based on the existence/truthiness of a property.
1169
+
1170
+ ```html
1171
+ {$if:is_on_earth}
1172
+ Hello, world!
1173
+ {else}
1174
+ Hello, Mars!
1175
+ {/if}
1176
+ ```
1177
+
1178
+ This also works with nested properties and loops.
1179
+
1145
1180
  ```html
1146
- <div>Hello world!</div>
1147
- {$for}Loop <div>{$test}</div>{/for}
1181
+ {$for:countries as country}
1182
+ {$if:country.is_in_europe}
1183
+ Europe: {$country.name}
1184
+ {else}
1185
+ Not Europe: {$country.name}
1186
+ {/if}
1187
+ {/for}
1148
1188
  ```
1149
1189
 
1150
1190
  <a id="api-content-generate-hash-subs"></a>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "spooder",
3
3
  "type": "module",
4
- "version": "4.2.17",
4
+ "version": "4.3.0",
5
5
  "exports": {
6
6
  ".": {
7
7
  "bun": "./src/api.ts",
package/src/api.ts CHANGED
@@ -108,8 +108,8 @@ export async function safe(target_fn: Callable) {
108
108
  }
109
109
  }
110
110
 
111
- type ReplacerFn = (key: string) => string | Array<string> | undefined;
112
- type Replacements = Record<string, string | Array<string>> | ReplacerFn;
111
+ type ReplacerFn = (key: string) => string | Array<any> | Record<string, any> | undefined;
112
+ type Replacements = Record<string, string | Array<any> | Record<string, any>> | ReplacerFn;
113
113
 
114
114
  export function parse_template(template: string, replacements: Replacements, drop_missing = false): string {
115
115
  let result = '';
@@ -118,6 +118,28 @@ export function parse_template(template: string, replacements: Replacements, dro
118
118
 
119
119
  const is_replacer_fn = typeof replacements === 'function';
120
120
 
121
+ function getValue(key: string): any {
122
+ const var_parts = key.split('.');
123
+ let value: any = is_replacer_fn ? replacements(var_parts[0]) : replacements[var_parts[0]];
124
+
125
+ for (let j = 1; j < var_parts.length; j++) {
126
+ if (value && typeof value === 'object') {
127
+ value = value[var_parts[j]];
128
+ } else {
129
+ value = undefined;
130
+ break;
131
+ }
132
+ }
133
+ return value;
134
+ }
135
+
136
+ function parseBlock(content: string, local_replacements: Replacements): string {
137
+ return parse_template(content, {
138
+ ...replacements,
139
+ ...local_replacements
140
+ }, drop_missing);
141
+ }
142
+
121
143
  const template_length = template.length;
122
144
  for (let i = 0; i < template_length; i++) {
123
145
  const char = template[i];
@@ -130,9 +152,8 @@ export function parse_template(template: string, replacements: Replacements, dro
130
152
  buffer_active = false;
131
153
 
132
154
  if (buffer.startsWith('for:')) {
133
- const loop_key = buffer.substring(4);
134
-
135
- const loop_entries = is_replacer_fn ? replacements(loop_key) : replacements[loop_key];
155
+ const [loop_key, loop_var] = buffer.substring(4).split(' as ');
156
+ const loop_entries = getValue(loop_key);
136
157
  const loop_content_start_index = i + 1;
137
158
  const loop_close_index = template.indexOf('{/for}', loop_content_start_index);
138
159
 
@@ -141,19 +162,48 @@ export function parse_template(template: string, replacements: Replacements, dro
141
162
  result += '{$' + buffer + '}';
142
163
  } else {
143
164
  const loop_content = template.substring(loop_content_start_index, loop_close_index);
144
- if (loop_entries !== undefined) {
145
- for (const loop_entry of loop_entries) {
146
- const inner_content = loop_content.replaceAll('%s', loop_entry);
147
- result += parse_template(inner_content, replacements, drop_missing);
148
- }
165
+ if (Array.isArray(loop_entries)) {
166
+ for (const loop_entry of loop_entries)
167
+ result += parseBlock(loop_content, { [loop_var]: loop_entry });
149
168
  } else {
150
169
  if (!drop_missing)
151
170
  result += '{$' + buffer + '}' + loop_content + '{/for}';
152
171
  }
153
- i += loop_content.length + 6;
172
+ i = loop_close_index + 5; // Move past {/for}
173
+ }
174
+ } else if (buffer.startsWith('if:')) {
175
+ const condition_key = buffer.substring(3);
176
+ const condition_value = getValue(condition_key);
177
+ const if_content_start_index = i + 1;
178
+ const if_close_index = template.indexOf('{/if}', if_content_start_index);
179
+
180
+ if (if_close_index === -1) {
181
+ if (!drop_missing)
182
+ result += '{$' + buffer + '}';
183
+ } else {
184
+ const if_content = template.substring(if_content_start_index, if_close_index);
185
+ const else_index = if_content.indexOf('{else}');
186
+
187
+ if (else_index === -1) {
188
+ // No else block
189
+ if (condition_value) {
190
+ result += parseBlock(if_content, {});
191
+ }
192
+ } else {
193
+ // Has else block
194
+ const true_content = if_content.substring(0, else_index);
195
+ const false_content = if_content.substring(else_index + 6); // +6 to move past {else}
196
+
197
+ if (condition_value)
198
+ result += parseBlock(true_content, {});
199
+ else
200
+ result += parseBlock(false_content, {});
201
+ }
202
+ i = if_close_index + 4; // Move past {/if}
154
203
  }
155
204
  } else {
156
- const replacement = is_replacer_fn ? replacements(buffer) : replacements[buffer];
205
+ const replacement = getValue(buffer);
206
+
157
207
  if (replacement !== undefined)
158
208
  result += replacement;
159
209
  else if (!drop_missing)
@@ -398,7 +448,7 @@ type ErrorHandler = (err: Error, req: Request, url: URL) => Resolvable<Response>
398
448
  type DefaultHandler = (req: Request, status_code: number) => HandlerReturnType;
399
449
  type StatusCodeHandler = (req: Request) => HandlerReturnType;
400
450
 
401
- type JSONRequestHandler = (req: Request, url: URL, json: JsonSerializable) => HandlerReturnType;
451
+ type JSONRequestHandler = (req: Request, url: URL, json: JsonObject) => HandlerReturnType;
402
452
 
403
453
  type ServerSentEventClient = {
404
454
  message: (message: string) => void;
@@ -451,7 +501,13 @@ export function validate_req_json(JSONRequestHandler: JSONRequestHandler): Reque
451
501
  if (req.headers.get('Content-Type') !== 'application/json')
452
502
  return 400; // Bad Request
453
503
 
454
- return JSONRequestHandler(req, url, await req.json());
504
+ const json = await req.json();
505
+
506
+ // validate json is a plain object
507
+ if (json === null || typeof json !== 'object' || Array.isArray(json))
508
+ return 400; // Bad Request
509
+
510
+ return JSONRequestHandler(req, url, json);
455
511
  } catch (e) {
456
512
  return 400; // Bad Request
457
513
  }