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.
Files changed (3) hide show
  1. package/README.md +129 -25
  2. package/package.json +1 -1
  3. 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
- get_git_hashes(length: number): Promise<Record<string, string>>;
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, get_git_hashes } from 'spooder';
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 get_git_hashes();
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
- asset: (file) => git_hash_table[file]
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
- When enabled, automatically generates git hash-based cache busting for static assets using dynamic template resolvers.
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
- ```ts
1408
- cache_bust: true
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
- With `cache_bust` enabled, you can use the `{{asset=filename}}` template function to generate cache-busted URLs:
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
- **Usage:**
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
- **Template usage:**
1420
- ```html
1421
- <link href="{{asset=static/css/style.css}}">
1422
- <script src="{{asset=static/js/app.js}}"></script>
1423
- <img src="{{asset=static/images/logo.png}}">
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
- The `asset` function automatically appends the git hash as a query parameter for cache busting, or returns the original filename if no hash is available.
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
- ### 🔧 ``get_git_hashes(length: number): Promise<Record<string, string>>``
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 `get_git_hashes()` uses `git ls-tree -r HEAD`, so the working directory must be a git repository.
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 get_git_hashes(7);
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 get_git_hashes(40);
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "spooder",
3
3
  "type": "module",
4
- "version": "5.0.3",
4
+ "version": "5.1.1",
5
5
  "module": "./src/api.ts",
6
6
  "bin": {
7
7
  "spooder": "./src/cli.ts"
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 async function get_git_hashes(length = 7): Promise<Record<string, string>> {
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('get_git_hashes() failed, `' + cmd.join(' ') + '` exited with non-zero exit code.');
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
- cache_bust?: boolean;
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 git_hash_table: Record<string, string> = {};
1432
- let cache_bust_subs = {};
1504
+ let cache_bust_subs: Record<string, ReplacementValue> = {};
1433
1505
 
1434
- if (options.cache_bust) {
1435
- git_hash_table = await get_git_hashes();
1436
- cache_bust_subs = {
1437
- asset: (file: string) => {
1438
- const hash = git_hash_table[file];
1439
- return hash ? `${file}?v=${hash}` : file;
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);