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.
- package/README.md +58 -18
- package/package.json +1 -1
- 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
|
|
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
|
-
>
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1141
|
-
|
|
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
|
-
|
|
1147
|
-
{$
|
|
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
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
|
|
145
|
-
for (const loop_entry of loop_entries)
|
|
146
|
-
|
|
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
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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
|
}
|