spooder 5.0.3 → 5.1.1
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 +129 -25
- package/package.json +1 -1
- package/src/api.ts +88 -13
package/README.md
CHANGED
|
@@ -96,6 +96,8 @@ The `CLI` component of `spooder` is a global command-line tool for running serve
|
|
|
96
96
|
- [API > Workers](#api-workers)
|
|
97
97
|
- [API > Caching](#api-caching)
|
|
98
98
|
- [API > Templating](#api-templating)
|
|
99
|
+
- [API > Cache Busting](#api-cache-busting)
|
|
100
|
+
- [API > Git](#api-git)
|
|
99
101
|
- [API > Database](#api-database)
|
|
100
102
|
- [API > Database > Schema](#api-database-schema)
|
|
101
103
|
- [API > Database > Interface](#api-database-interface)
|
|
@@ -527,7 +529,16 @@ pipe.off(event: string): void;
|
|
|
527
529
|
// templates
|
|
528
530
|
Replacements = Record<string, string | Array<string> | object | object[]> | ReplacerFn | AsyncReplaceFn;
|
|
529
531
|
parse_template(template: string, replacements: Replacements, drop_missing?: boolean): Promise<string>;
|
|
530
|
-
|
|
532
|
+
|
|
533
|
+
// cache busting
|
|
534
|
+
cache_bust(string|string[]: path, format: string): string|string[]
|
|
535
|
+
cache_bust_set_hash_length(length: number): void;
|
|
536
|
+
cache_bust_set_format(format: string): void;
|
|
537
|
+
cache_bust_get_hash_table(): Record<string, string>;
|
|
538
|
+
|
|
539
|
+
// git
|
|
540
|
+
git_get_hashes(length: number): Promise<Record<string, string>>;
|
|
541
|
+
git_get_hashes_sync(length: number): Record<string, string>
|
|
531
542
|
|
|
532
543
|
// database interface
|
|
533
544
|
db_sqlite(filename: string, options: number|object): db_sqlite;
|
|
@@ -1197,7 +1208,7 @@ server.websocket('/path/to/websocket', {
|
|
|
1197
1208
|
For simpler projects, the scaffolding can often look the same, potentially something similar to below.
|
|
1198
1209
|
|
|
1199
1210
|
```ts
|
|
1200
|
-
import { http_serve, cache_http, parse_template, http_apply_range,
|
|
1211
|
+
import { http_serve, cache_http, parse_template, http_apply_range, git_get_hashes } from 'spooder';
|
|
1201
1212
|
import path from 'node:path';
|
|
1202
1213
|
|
|
1203
1214
|
const server = http_serve(80);
|
|
@@ -1209,7 +1220,7 @@ const cache = cache_http({
|
|
|
1209
1220
|
});
|
|
1210
1221
|
|
|
1211
1222
|
const base_file = await Bun.file('./html/base_template.html').text();
|
|
1212
|
-
const git_hash_table = await
|
|
1223
|
+
const git_hash_table = await git_get_hashes();
|
|
1213
1224
|
|
|
1214
1225
|
async function default_handler(status_code: number): Promise<Response> {
|
|
1215
1226
|
const error_text = HTTP_STATUS_CODE[status_code] as string;
|
|
@@ -1243,7 +1254,7 @@ server.dir('/static', './static', async (file_path, file, stat, request) => {
|
|
|
1243
1254
|
const ext = path.extname(file_path);
|
|
1244
1255
|
if (ext === '.css' || ext === '.js') {
|
|
1245
1256
|
const content = await parse_template(await file.text(), {
|
|
1246
|
-
|
|
1257
|
+
cache_bust: (file) => `${file}?v=${git_hash_table[file]}`
|
|
1247
1258
|
}, true);
|
|
1248
1259
|
|
|
1249
1260
|
return new Response(content, {
|
|
@@ -1302,7 +1313,11 @@ server.bootstrap({
|
|
|
1302
1313
|
error_page: Bun.file('./html/error.html')
|
|
1303
1314
|
},
|
|
1304
1315
|
|
|
1305
|
-
cache_bust: true
|
|
1316
|
+
cache_bust: { // true or options
|
|
1317
|
+
format: '$file#$hash', // default: $file?v=$hash
|
|
1318
|
+
hash_length: 20, // default: 7
|
|
1319
|
+
prefix: 'bust' // default: cache_bust
|
|
1320
|
+
},
|
|
1306
1321
|
|
|
1307
1322
|
static: {
|
|
1308
1323
|
directory: './static',
|
|
@@ -1401,29 +1416,29 @@ cache: {
|
|
|
1401
1416
|
}
|
|
1402
1417
|
```
|
|
1403
1418
|
|
|
1404
|
-
##### `cache_bust?: boolean`
|
|
1405
|
-
|
|
1419
|
+
##### `cache_bust?: CacheBustOptions | boolean`
|
|
1420
|
+
Enables the use of the [`cache_bust()`](#api-cache-busting) API inside templates using the ``{{cache_bust=file}}`` directive.
|
|
1406
1421
|
|
|
1407
|
-
```
|
|
1408
|
-
cache_bust
|
|
1422
|
+
```html
|
|
1423
|
+
<link href="{{cache_bust=static/css/style.css}}">
|
|
1424
|
+
<script src="{{cache_bust=static/js/app.js}}"></script>
|
|
1425
|
+
<img src="{{cache_bust=static/images/logo.png}}">
|
|
1409
1426
|
```
|
|
1410
1427
|
|
|
1411
|
-
|
|
1428
|
+
Since this uses the [`cache_bust()`](#api-cache-busting) API internally, it is effected by the `cache_bust_set_hash_length` and `cache_bust_set_format` global functions.
|
|
1412
1429
|
|
|
1413
|
-
|
|
1414
|
-
```ts
|
|
1415
|
-
cache_bust: true
|
|
1416
|
-
// Creates dynamic asset resolver: {{asset=static/css/style.css}} -> "static/css/style.css?v=a1b2c3d"
|
|
1417
|
-
```
|
|
1430
|
+
Setting `cache_bust` to `true` assumes the normal defaults, however this can be customized by providing an options object.
|
|
1418
1431
|
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1432
|
+
```ts
|
|
1433
|
+
cache_bust: { // true or options
|
|
1434
|
+
format: '$file#$hash', // default: $file?v=$hash
|
|
1435
|
+
hash_length: 20, // default: 7
|
|
1436
|
+
prefix: 'bust' // default: cache_bust
|
|
1437
|
+
},
|
|
1424
1438
|
```
|
|
1425
1439
|
|
|
1426
|
-
|
|
1440
|
+
> ![IMPORTANT]
|
|
1441
|
+
> `format` and `hash_length` internally call `cache_bust_set_format` and `cache_bust_set_hash_length` respectively, so these values will effect `cache_bust()` globally.
|
|
1427
1442
|
|
|
1428
1443
|
##### `error?: object`
|
|
1429
1444
|
Optional error page configuration:
|
|
@@ -1962,6 +1977,34 @@ const html = await parse_template(template, replacements);
|
|
|
1962
1977
|
</ul>
|
|
1963
1978
|
```
|
|
1964
1979
|
|
|
1980
|
+
#### Simple Array Iteration
|
|
1981
|
+
|
|
1982
|
+
For simple arrays containing strings, you can iterate directly over the array items:
|
|
1983
|
+
|
|
1984
|
+
```ts
|
|
1985
|
+
const template = `
|
|
1986
|
+
<ul>
|
|
1987
|
+
<t-for items="fruits" as="fruit">
|
|
1988
|
+
<li>{{fruit}}</li>
|
|
1989
|
+
</t-for>
|
|
1990
|
+
</ul>
|
|
1991
|
+
`;
|
|
1992
|
+
|
|
1993
|
+
const replacements = {
|
|
1994
|
+
fruits: ['apple', 'banana', 'orange']
|
|
1995
|
+
};
|
|
1996
|
+
|
|
1997
|
+
const html = await parse_template(template, replacements);
|
|
1998
|
+
```
|
|
1999
|
+
|
|
2000
|
+
```html
|
|
2001
|
+
<ul>
|
|
2002
|
+
<li>apple</li>
|
|
2003
|
+
<li>banana</li>
|
|
2004
|
+
<li>orange</li>
|
|
2005
|
+
</ul>
|
|
2006
|
+
```
|
|
2007
|
+
|
|
1965
2008
|
#### Dot Notation Property Access
|
|
1966
2009
|
|
|
1967
2010
|
You can access nested object properties using dot notation:
|
|
@@ -2002,26 +2045,87 @@ await parse_template(..., {
|
|
|
2002
2045
|
</t-for>
|
|
2003
2046
|
```
|
|
2004
2047
|
|
|
2048
|
+
<a id="api-cache-busting"></a>
|
|
2049
|
+
## API > Cache Busting
|
|
2005
2050
|
|
|
2006
|
-
### 🔧 ``
|
|
2051
|
+
### 🔧 ``cache_bust(string|string[]: path, format: string): string|string[]``
|
|
2052
|
+
|
|
2053
|
+
Appends a hash-suffix to the provided string, formatted by default as a query parameter, for cache-busting purposes.
|
|
2054
|
+
|
|
2055
|
+
```ts
|
|
2056
|
+
cache_bust('static/my_image.png'); // > static/my_image.png?v=123fea
|
|
2057
|
+
```
|
|
2058
|
+
|
|
2059
|
+
This works on an array of paths as well.
|
|
2060
|
+
|
|
2061
|
+
```ts
|
|
2062
|
+
cache_bust([
|
|
2063
|
+
'static/js/script1.js',
|
|
2064
|
+
'static/js/script2.js'
|
|
2065
|
+
]);
|
|
2066
|
+
|
|
2067
|
+
// [
|
|
2068
|
+
// 'static/js/script1.js?v=fffffff',
|
|
2069
|
+
// 'static/js/script2.js?v=fffffff'
|
|
2070
|
+
// ]
|
|
2071
|
+
```
|
|
2072
|
+
|
|
2073
|
+
> ![NOTE]
|
|
2074
|
+
> Internally `cache_bust()` uses `git_get_hashes()` to hash paths, requiring the input `path` to be a valid git path. If the path cannot be resolved in git, an empty hash is substituted.
|
|
2075
|
+
|
|
2076
|
+
### 🔧 ``cache_bust_set_format(format: string): void``
|
|
2077
|
+
|
|
2078
|
+
The default format for used for `cache_bust()` is `$file?v=$hash`, this can be customized per-call with the `format` parameter, or globally using `cache_bust_set_format()`
|
|
2079
|
+
|
|
2080
|
+
```ts
|
|
2081
|
+
cache_bust('dogs.txt'); // > dogs.txt?v=fff
|
|
2082
|
+
cache_bust('dogs.txt', '$file?hash=$hash'); // > dogs.txt?hash=fff
|
|
2083
|
+
|
|
2084
|
+
cache_bust_set_format('$file#$hash');
|
|
2085
|
+
cache_bust('dogs.txt'); // > dogs#fff
|
|
2086
|
+
```
|
|
2087
|
+
|
|
2088
|
+
### 🔧 ``cache_bust_set_hash_length(length: number): void``
|
|
2089
|
+
|
|
2090
|
+
The default hash-length used by `cache_bust()` is 7. This can be changed with `cache_bust_set_hash_length()`.
|
|
2091
|
+
|
|
2092
|
+
> ![NOTE]
|
|
2093
|
+
> Hashes are cached once at the specified length, therefore `cache_bust_set_hash_length()` must be called before calling `cache_bust()` and has no effect calling it after.
|
|
2094
|
+
|
|
2095
|
+
```ts
|
|
2096
|
+
cache_bust_set_hash_length(10);
|
|
2097
|
+
cache_bust('dogs.txt'); // > dogs.txt?v=ffffffffff
|
|
2098
|
+
```
|
|
2099
|
+
|
|
2100
|
+
### 🔧 ``cache_bust_get_hash_table(): Record<string, string>``
|
|
2101
|
+
|
|
2102
|
+
This function returns the internal hash table used by `cache_bust()`. This is exposed to userland in the event that you which to use the hashes for other purposes, avoiding the need to call and store `git_get_hashes()` twice.
|
|
2103
|
+
|
|
2104
|
+
<a id="api-templating"></a>
|
|
2105
|
+
## API > Git
|
|
2106
|
+
|
|
2107
|
+
### 🔧 ``git_get_hashes(length: number): Promise<Record<string, string>>``
|
|
2108
|
+
|
|
2109
|
+
### 🔧 ``git_get_hashes_sync(length: number): Record<string, string>``
|
|
2007
2110
|
|
|
2008
2111
|
Retrieve git hashes for all files in the repository. This is useful for implementing cache-busting functionality or creating file integrity checks.
|
|
2009
2112
|
|
|
2010
2113
|
> [!IMPORTANT]
|
|
2011
|
-
> Internally `
|
|
2114
|
+
> Internally `git_get_hashes()` uses `git ls-tree -r HEAD`, so the working directory must be a git repository.
|
|
2012
2115
|
|
|
2013
2116
|
```ts
|
|
2014
|
-
const hashes = await
|
|
2117
|
+
const hashes = await git_get_hashes(7);
|
|
2015
2118
|
// { 'docs/project-logo.png': '754d9ea' }
|
|
2016
2119
|
```
|
|
2017
2120
|
|
|
2018
2121
|
You can specify the hash length (default is 7 characters for short hashes):
|
|
2019
2122
|
|
|
2020
2123
|
```ts
|
|
2021
|
-
const full_hashes = await
|
|
2124
|
+
const full_hashes = await git_get_hashes(40);
|
|
2022
2125
|
// { 'docs/project-logo.png': 'd65c52a41a75db43e184d2268c6ea9f9741de63e' }
|
|
2023
2126
|
```
|
|
2024
2127
|
|
|
2128
|
+
|
|
2025
2129
|
<a id="api-database"></a>
|
|
2026
2130
|
<a id="api-database-interface"></a>
|
|
2027
2131
|
## API > Database
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -556,8 +556,52 @@ async function replace_async(str: string, regex: RegExp, replacer_fn: (match: st
|
|
|
556
556
|
|
|
557
557
|
return result;
|
|
558
558
|
}
|
|
559
|
+
// endregion
|
|
560
|
+
|
|
561
|
+
// region cache busting
|
|
562
|
+
let cache_bust_map: Record<string, string> | null = null;
|
|
563
|
+
let cache_bust_global_length = 7;
|
|
564
|
+
let cache_bust_global_format = '$file?v=$hash';
|
|
565
|
+
|
|
566
|
+
export function cache_bust_get_hash_table(): Record<string, string> {
|
|
567
|
+
if (cache_bust_map === null)
|
|
568
|
+
cache_bust_map = git_get_hashes_sync(cache_bust_global_length);
|
|
569
|
+
|
|
570
|
+
return cache_bust_map;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export function cache_bust(paths: string|string[], format = cache_bust_global_format): string|string[] {
|
|
574
|
+
const hash_table = cache_bust_get_hash_table();
|
|
575
|
+
|
|
576
|
+
if (Array.isArray(paths)) {
|
|
577
|
+
const n_paths = paths.length;
|
|
578
|
+
const result = Array<string>(n_paths);
|
|
579
|
+
|
|
580
|
+
for (let i = 0; i < n_paths; i++) {
|
|
581
|
+
const path = paths[i];
|
|
582
|
+
const hash = hash_table[path] ?? '';
|
|
583
|
+
|
|
584
|
+
result[i] = format.replace('$file', path).replace('$hash', hash);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return result;
|
|
588
|
+
} else {
|
|
589
|
+
const hash = cache_bust_get_hash_table()[paths] ?? '';
|
|
590
|
+
return format.replace('$file', paths).replace('$hash', hash);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
559
593
|
|
|
560
|
-
export
|
|
594
|
+
export function cache_bust_set_hash_length(length: number): void {
|
|
595
|
+
cache_bust_global_length = length;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export function cache_bust_set_format(format: string): void {
|
|
599
|
+
cache_bust_global_format = format;
|
|
600
|
+
}
|
|
601
|
+
// endregion
|
|
602
|
+
|
|
603
|
+
// region git
|
|
604
|
+
export async function git_get_hashes(length = 7): Promise<Record<string, string>> {
|
|
561
605
|
const cmd = ['git', 'ls-tree', '-r', 'HEAD'];
|
|
562
606
|
const process = Bun.spawn(cmd, {
|
|
563
607
|
stdout: 'pipe',
|
|
@@ -567,7 +611,7 @@ export async function get_git_hashes(length = 7): Promise<Record<string, string>
|
|
|
567
611
|
await process.exited;
|
|
568
612
|
|
|
569
613
|
if (process.exitCode as number > 0)
|
|
570
|
-
throw new Error('
|
|
614
|
+
throw new Error('git_get_hashes() failed, `' + cmd.join(' ') + '` exited with non-zero exit code.');
|
|
571
615
|
|
|
572
616
|
const stdout = await Bun.readableStreamToText(process.stdout as ReadableStream);
|
|
573
617
|
const hash_map: Record<string, string> = {};
|
|
@@ -581,6 +625,27 @@ export async function get_git_hashes(length = 7): Promise<Record<string, string>
|
|
|
581
625
|
return hash_map;
|
|
582
626
|
}
|
|
583
627
|
|
|
628
|
+
export function git_get_hashes_sync(length = 7): Record<string, string> {
|
|
629
|
+
const cmd = ['git', 'ls-tree', '-r', 'HEAD'];
|
|
630
|
+
const process = Bun.spawnSync(cmd, {
|
|
631
|
+
stdout: 'pipe',
|
|
632
|
+
stderr: 'pipe'
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
if (process.exitCode > 0)
|
|
636
|
+
throw new Error('git_get_hashes_sync() failed, `' + cmd.join(' ') + '` exited with non-zero exit code.');
|
|
637
|
+
|
|
638
|
+
const stdout = process.stdout.toString();
|
|
639
|
+
const hash_map: Record<string, string> = {};
|
|
640
|
+
|
|
641
|
+
const regex = /([^\s]+)\s([^\s]+)\s([^\s]+)\t(.+)/g;
|
|
642
|
+
let match: RegExpExecArray | null;
|
|
643
|
+
|
|
644
|
+
while (match = regex.exec(stdout))
|
|
645
|
+
hash_map[match[4]] = match[3].substring(0, length);
|
|
646
|
+
|
|
647
|
+
return hash_map;
|
|
648
|
+
}
|
|
584
649
|
// endregion
|
|
585
650
|
|
|
586
651
|
// region serving
|
|
@@ -1004,11 +1069,19 @@ type BootstrapRoute = {
|
|
|
1004
1069
|
subs?: Record<string, BootstrapSub>;
|
|
1005
1070
|
};
|
|
1006
1071
|
|
|
1072
|
+
type BootstrapCacheBust = {
|
|
1073
|
+
prefix?: string;
|
|
1074
|
+
hash_length?: number;
|
|
1075
|
+
format?: string;
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1007
1078
|
type BootstrapOptions = {
|
|
1008
1079
|
base?: string | BunFile;
|
|
1009
1080
|
routes: Record<string, BootstrapRoute>;
|
|
1010
1081
|
cache?: ReturnType<typeof cache_http> | CacheOptions;
|
|
1011
|
-
|
|
1082
|
+
|
|
1083
|
+
cache_bust?: boolean | BootstrapCacheBust;
|
|
1084
|
+
|
|
1012
1085
|
error?: {
|
|
1013
1086
|
use_canary_reporting?: boolean;
|
|
1014
1087
|
error_page: string | BunFile;
|
|
@@ -1428,17 +1501,19 @@ export function http_serve(port: number, hostname?: string) {
|
|
|
1428
1501
|
|
|
1429
1502
|
/* Bootstrap a static web server */
|
|
1430
1503
|
bootstrap: async function(options: BootstrapOptions) {
|
|
1431
|
-
let
|
|
1432
|
-
let cache_bust_subs = {};
|
|
1504
|
+
let cache_bust_subs: Record<string, ReplacementValue> = {};
|
|
1433
1505
|
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1506
|
+
const cache_bust_opts = options.cache_bust;
|
|
1507
|
+
if (typeof cache_bust_opts === 'object' && cache_bust_opts !== null) {
|
|
1508
|
+
if (typeof cache_bust_opts.hash_length === 'number')
|
|
1509
|
+
cache_bust_set_hash_length(cache_bust_opts.hash_length);
|
|
1510
|
+
|
|
1511
|
+
if (typeof cache_bust_opts.format === 'string')
|
|
1512
|
+
cache_bust_set_format(cache_bust_opts.format);
|
|
1513
|
+
|
|
1514
|
+
cache_bust_subs[cache_bust_opts.prefix ?? 'cache_bust'] = cache_bust;
|
|
1515
|
+
} else if (cache_bust_opts === true) {
|
|
1516
|
+
cache_bust_subs = { cache_bust };
|
|
1442
1517
|
}
|
|
1443
1518
|
|
|
1444
1519
|
const global_sub_table = sub_table_merge(cache_bust_subs, options.global_subs);
|