spooder 5.0.3 → 5.1.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 +115 -25
  2. package/package.json +1 -1
  3. package/src/api.ts +72 -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: path, format: 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,73 @@ await parse_template(..., {
2002
2045
  </t-for>
2003
2046
  ```
2004
2047
 
2048
+ <a id="api-cache-busting"></a>
2049
+ ## API > Cache Busting
2050
+
2051
+ ### 🔧 ``cache_bust(string: path, format: 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
+ > ![NOTE]
2060
+ > 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.
2061
+
2062
+ ### 🔧 ``cache_bust_set_format(format: string): void``
2063
+
2064
+ 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()`
2005
2065
 
2006
- ### 🔧 ``get_git_hashes(length: number): Promise<Record<string, string>>``
2066
+ ```ts
2067
+ cache_bust('dogs.txt'); // > dogs.txt?v=fff
2068
+ cache_bust('dogs.txt', '$file?hash=$hash'); // > dogs.txt?hash=fff
2069
+
2070
+ cache_bust_set_format('$file#$hash');
2071
+ cache_bust('dogs.txt'); // > dogs#fff
2072
+ ```
2073
+
2074
+ ### 🔧 ``cache_bust_set_hash_length(length: number): void``
2075
+
2076
+ The default hash-length used by `cache_bust()` is 7. This can be changed with `cache_bust_set_hash_length()`.
2077
+
2078
+ > ![NOTE]
2079
+ > 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.
2080
+
2081
+ ```ts
2082
+ cache_bust_set_hash_length(10);
2083
+ cache_bust('dogs.txt'); // > dogs.txt?v=ffffffffff
2084
+ ```
2085
+
2086
+ ### 🔧 ``cache_bust_get_hash_table(): Record<string, string>``
2087
+
2088
+ 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.
2089
+
2090
+ <a id="api-templating"></a>
2091
+ ## API > Git
2092
+
2093
+ ### 🔧 ``git_get_hashes(length: number): Promise<Record<string, string>>``
2094
+
2095
+ ### 🔧 ``git_get_hashes_sync(length: number): Record<string, string>``
2007
2096
 
2008
2097
  Retrieve git hashes for all files in the repository. This is useful for implementing cache-busting functionality or creating file integrity checks.
2009
2098
 
2010
2099
  > [!IMPORTANT]
2011
- > Internally `get_git_hashes()` uses `git ls-tree -r HEAD`, so the working directory must be a git repository.
2100
+ > Internally `git_get_hashes()` uses `git ls-tree -r HEAD`, so the working directory must be a git repository.
2012
2101
 
2013
2102
  ```ts
2014
- const hashes = await get_git_hashes(7);
2103
+ const hashes = await git_get_hashes(7);
2015
2104
  // { 'docs/project-logo.png': '754d9ea' }
2016
2105
  ```
2017
2106
 
2018
2107
  You can specify the hash length (default is 7 characters for short hashes):
2019
2108
 
2020
2109
  ```ts
2021
- const full_hashes = await get_git_hashes(40);
2110
+ const full_hashes = await git_get_hashes(40);
2022
2111
  // { 'docs/project-logo.png': 'd65c52a41a75db43e184d2268c6ea9f9741de63e' }
2023
2112
  ```
2024
2113
 
2114
+
2025
2115
  <a id="api-database"></a>
2026
2116
  <a id="api-database-interface"></a>
2027
2117
  ## 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.0",
5
5
  "module": "./src/api.ts",
6
6
  "bin": {
7
7
  "spooder": "./src/cli.ts"
package/src/api.ts CHANGED
@@ -556,8 +556,36 @@ 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(path: string, format = cache_bust_global_format): string {
574
+ const hash = cache_bust_get_hash_table()[path] || '';
575
+ return format.replace('$file', path).replace('$hash', hash);
576
+ }
577
+
578
+ export function cache_bust_set_hash_length(length: number): void {
579
+ cache_bust_global_length = length;
580
+ }
559
581
 
560
- export async function get_git_hashes(length = 7): Promise<Record<string, string>> {
582
+ export function cache_bust_set_format(format: string): void {
583
+ cache_bust_global_format = format;
584
+ }
585
+ // endregion
586
+
587
+ // region git
588
+ export async function git_get_hashes(length = 7): Promise<Record<string, string>> {
561
589
  const cmd = ['git', 'ls-tree', '-r', 'HEAD'];
562
590
  const process = Bun.spawn(cmd, {
563
591
  stdout: 'pipe',
@@ -567,7 +595,7 @@ export async function get_git_hashes(length = 7): Promise<Record<string, string>
567
595
  await process.exited;
568
596
 
569
597
  if (process.exitCode as number > 0)
570
- throw new Error('get_git_hashes() failed, `' + cmd.join(' ') + '` exited with non-zero exit code.');
598
+ throw new Error('git_get_hashes() failed, `' + cmd.join(' ') + '` exited with non-zero exit code.');
571
599
 
572
600
  const stdout = await Bun.readableStreamToText(process.stdout as ReadableStream);
573
601
  const hash_map: Record<string, string> = {};
@@ -581,6 +609,27 @@ export async function get_git_hashes(length = 7): Promise<Record<string, string>
581
609
  return hash_map;
582
610
  }
583
611
 
612
+ export function git_get_hashes_sync(length = 7): Record<string, string> {
613
+ const cmd = ['git', 'ls-tree', '-r', 'HEAD'];
614
+ const process = Bun.spawnSync(cmd, {
615
+ stdout: 'pipe',
616
+ stderr: 'pipe'
617
+ });
618
+
619
+ if (process.exitCode > 0)
620
+ throw new Error('git_get_hashes_sync() failed, `' + cmd.join(' ') + '` exited with non-zero exit code.');
621
+
622
+ const stdout = process.stdout.toString();
623
+ const hash_map: Record<string, string> = {};
624
+
625
+ const regex = /([^\s]+)\s([^\s]+)\s([^\s]+)\t(.+)/g;
626
+ let match: RegExpExecArray | null;
627
+
628
+ while (match = regex.exec(stdout))
629
+ hash_map[match[4]] = match[3].substring(0, length);
630
+
631
+ return hash_map;
632
+ }
584
633
  // endregion
585
634
 
586
635
  // region serving
@@ -1004,11 +1053,19 @@ type BootstrapRoute = {
1004
1053
  subs?: Record<string, BootstrapSub>;
1005
1054
  };
1006
1055
 
1056
+ type BootstrapCacheBust = {
1057
+ prefix?: string;
1058
+ hash_length?: number;
1059
+ format?: string;
1060
+ };
1061
+
1007
1062
  type BootstrapOptions = {
1008
1063
  base?: string | BunFile;
1009
1064
  routes: Record<string, BootstrapRoute>;
1010
1065
  cache?: ReturnType<typeof cache_http> | CacheOptions;
1011
- cache_bust?: boolean;
1066
+
1067
+ cache_bust?: boolean | BootstrapCacheBust;
1068
+
1012
1069
  error?: {
1013
1070
  use_canary_reporting?: boolean;
1014
1071
  error_page: string | BunFile;
@@ -1428,17 +1485,19 @@ export function http_serve(port: number, hostname?: string) {
1428
1485
 
1429
1486
  /* Bootstrap a static web server */
1430
1487
  bootstrap: async function(options: BootstrapOptions) {
1431
- let git_hash_table: Record<string, string> = {};
1432
- let cache_bust_subs = {};
1488
+ let cache_bust_subs: Record<string, ReplacementValue> = {};
1433
1489
 
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
- };
1490
+ const cache_bust_opts = options.cache_bust;
1491
+ if (typeof cache_bust_opts === 'object' && cache_bust_opts !== null) {
1492
+ if (typeof cache_bust_opts.hash_length === 'number')
1493
+ cache_bust_set_hash_length(cache_bust_opts.hash_length);
1494
+
1495
+ if (typeof cache_bust_opts.format === 'string')
1496
+ cache_bust_set_format(cache_bust_opts.format);
1497
+
1498
+ cache_bust_subs[cache_bust_opts.prefix ?? 'cache_bust'] = cache_bust;
1499
+ } else if (cache_bust_opts === true) {
1500
+ cache_bust_subs = { cache_bust };
1442
1501
  }
1443
1502
 
1444
1503
  const global_sub_table = sub_table_merge(cache_bust_subs, options.global_subs);