spooder 5.0.1 → 5.0.3
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 +226 -73
- package/package.json +1 -1
- package/src/api.ts +53 -20
package/README.md
CHANGED
|
@@ -525,8 +525,8 @@ pipe.once(event: string, callback: (data: object) => void | Promise<void>): void
|
|
|
525
525
|
pipe.off(event: string): void;
|
|
526
526
|
|
|
527
527
|
// templates
|
|
528
|
-
|
|
529
|
-
|
|
528
|
+
Replacements = Record<string, string | Array<string> | object | object[]> | ReplacerFn | AsyncReplaceFn;
|
|
529
|
+
parse_template(template: string, replacements: Replacements, drop_missing?: boolean): Promise<string>;
|
|
530
530
|
get_git_hashes(length: number): Promise<Record<string, string>>;
|
|
531
531
|
|
|
532
532
|
// database interface
|
|
@@ -1197,7 +1197,7 @@ server.websocket('/path/to/websocket', {
|
|
|
1197
1197
|
For simpler projects, the scaffolding can often look the same, potentially something similar to below.
|
|
1198
1198
|
|
|
1199
1199
|
```ts
|
|
1200
|
-
import { http_serve, cache_http,
|
|
1200
|
+
import { http_serve, cache_http, parse_template, http_apply_range, get_git_hashes } from 'spooder';
|
|
1201
1201
|
import path from 'node:path';
|
|
1202
1202
|
|
|
1203
1203
|
const server = http_serve(80);
|
|
@@ -1209,7 +1209,7 @@ const cache = cache_http({
|
|
|
1209
1209
|
});
|
|
1210
1210
|
|
|
1211
1211
|
const base_file = await Bun.file('./html/base_template.html').text();
|
|
1212
|
-
const
|
|
1212
|
+
const git_hash_table = await get_git_hashes();
|
|
1213
1213
|
|
|
1214
1214
|
async function default_handler(status_code: number): Promise<Response> {
|
|
1215
1215
|
const error_text = HTTP_STATUS_CODE[status_code] as string;
|
|
@@ -1239,10 +1239,12 @@ server.dir('/static', './static', async (file_path, file, stat, request) => {
|
|
|
1239
1239
|
if (stat.isDirectory())
|
|
1240
1240
|
return HTTP_STATUS_CODE.Unauthorized_401;
|
|
1241
1241
|
|
|
1242
|
-
//
|
|
1242
|
+
// serve css/js files directly
|
|
1243
1243
|
const ext = path.extname(file_path);
|
|
1244
1244
|
if (ext === '.css' || ext === '.js') {
|
|
1245
|
-
const content = await parse_template(await file.text(),
|
|
1245
|
+
const content = await parse_template(await file.text(), {
|
|
1246
|
+
asset: (file) => git_hash_table[file]
|
|
1247
|
+
}, true);
|
|
1246
1248
|
|
|
1247
1249
|
return new Response(content, {
|
|
1248
1250
|
headers: {
|
|
@@ -1261,8 +1263,8 @@ function add_route(route: string, file: string, title: string) {
|
|
|
1261
1263
|
const template = await parse_template(base_file, {
|
|
1262
1264
|
title: title,
|
|
1263
1265
|
content: file_content,
|
|
1264
|
-
|
|
1265
|
-
},
|
|
1266
|
+
asset: (file) => git_hash_table[file]
|
|
1267
|
+
}, true);
|
|
1266
1268
|
|
|
1267
1269
|
return template;
|
|
1268
1270
|
});
|
|
@@ -1341,6 +1343,172 @@ server.bootstrap({
|
|
|
1341
1343
|
});
|
|
1342
1344
|
```
|
|
1343
1345
|
|
|
1346
|
+
#### Bootstrap Options
|
|
1347
|
+
|
|
1348
|
+
The `BootstrapOptions` object accepts the following properties:
|
|
1349
|
+
|
|
1350
|
+
##### `base?: string | BunFile`
|
|
1351
|
+
Optional base template that wraps all route content. The base template should include `{{content}}` where the route content will be inserted.
|
|
1352
|
+
|
|
1353
|
+
```ts
|
|
1354
|
+
// Base template: base.html
|
|
1355
|
+
<html>
|
|
1356
|
+
<head><title>{{title}}</title></head>
|
|
1357
|
+
<body>{{content}}</body>
|
|
1358
|
+
</html>
|
|
1359
|
+
|
|
1360
|
+
// Usage
|
|
1361
|
+
server.bootstrap({
|
|
1362
|
+
base: Bun.file('./templates/base.html'),
|
|
1363
|
+
routes: {
|
|
1364
|
+
'/': {
|
|
1365
|
+
content: '<h1>Welcome</h1>',
|
|
1366
|
+
subs: { title: 'Home' }
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
```
|
|
1371
|
+
|
|
1372
|
+
##### `routes: Record<string, BootstrapRoute>`
|
|
1373
|
+
**Required.** Defines the routes and their content. Each route can have:
|
|
1374
|
+
- `content`: The page content (string or BunFile)
|
|
1375
|
+
- `subs?`: Template substitutions specific to this route
|
|
1376
|
+
|
|
1377
|
+
```ts
|
|
1378
|
+
routes: {
|
|
1379
|
+
'/about': {
|
|
1380
|
+
content: Bun.file('./pages/about.html'),
|
|
1381
|
+
subs: {
|
|
1382
|
+
title: 'About Us',
|
|
1383
|
+
description: 'Learn more about our company'
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
```
|
|
1388
|
+
|
|
1389
|
+
##### `cache?: CacheOptions | ReturnType<typeof cache_http>`
|
|
1390
|
+
Optional HTTP caching configuration. Can be:
|
|
1391
|
+
- A `CacheOptions` object (creates new cache instance)
|
|
1392
|
+
- An existing cache instance from `cache_http()`
|
|
1393
|
+
- Omitted to disable caching
|
|
1394
|
+
|
|
1395
|
+
```ts
|
|
1396
|
+
cache: {
|
|
1397
|
+
ttl: 5 * 60 * 1000, // 5 minutes
|
|
1398
|
+
max_size: 10 * 1024 * 1024, // 10 MB
|
|
1399
|
+
use_etags: true,
|
|
1400
|
+
use_canary_reporting: true
|
|
1401
|
+
}
|
|
1402
|
+
```
|
|
1403
|
+
|
|
1404
|
+
##### `cache_bust?: boolean`
|
|
1405
|
+
When enabled, automatically generates git hash-based cache busting for static assets using dynamic template resolvers.
|
|
1406
|
+
|
|
1407
|
+
```ts
|
|
1408
|
+
cache_bust: true
|
|
1409
|
+
```
|
|
1410
|
+
|
|
1411
|
+
With `cache_bust` enabled, you can use the `{{asset=filename}}` template function to generate cache-busted URLs:
|
|
1412
|
+
|
|
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
|
+
```
|
|
1418
|
+
|
|
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}}">
|
|
1424
|
+
```
|
|
1425
|
+
|
|
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.
|
|
1427
|
+
|
|
1428
|
+
##### `error?: object`
|
|
1429
|
+
Optional error page configuration:
|
|
1430
|
+
- `error_page`: Template for error pages (string or BunFile)
|
|
1431
|
+
- `use_canary_reporting?`: Whether to report errors via canary
|
|
1432
|
+
|
|
1433
|
+
Error templates receive `{{error_code}}` and `{{error_text}}` substitutions.
|
|
1434
|
+
|
|
1435
|
+
```ts
|
|
1436
|
+
error: {
|
|
1437
|
+
error_page: Bun.file('./templates/error.html'),
|
|
1438
|
+
use_canary_reporting: true
|
|
1439
|
+
}
|
|
1440
|
+
```
|
|
1441
|
+
|
|
1442
|
+
##### `static?: object`
|
|
1443
|
+
Optional static file serving configuration:
|
|
1444
|
+
- `route`: URL path prefix for static files
|
|
1445
|
+
- `directory`: Local directory containing static files
|
|
1446
|
+
- `sub_ext?`: Array of file extensions that should have template substitution applied
|
|
1447
|
+
|
|
1448
|
+
```ts
|
|
1449
|
+
static: {
|
|
1450
|
+
route: '/assets',
|
|
1451
|
+
directory: './public',
|
|
1452
|
+
sub_ext: ['.css', '.js'] // These files get template processing
|
|
1453
|
+
}
|
|
1454
|
+
```
|
|
1455
|
+
|
|
1456
|
+
Files with extensions in `sub_ext` will have template substitutions applied before serving. This includes support for functions to generate dynamic content:
|
|
1457
|
+
|
|
1458
|
+
```ts
|
|
1459
|
+
// Dynamic CSS with function-based substitutions
|
|
1460
|
+
static: {
|
|
1461
|
+
route: '/assets',
|
|
1462
|
+
directory: './public',
|
|
1463
|
+
sub_ext: ['.css']
|
|
1464
|
+
},
|
|
1465
|
+
|
|
1466
|
+
global_subs: {
|
|
1467
|
+
theme_color: () => {
|
|
1468
|
+
const hour = new Date().getHours();
|
|
1469
|
+
return hour < 6 || hour > 18 ? '#2d3748' : '#4a5568';
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
```
|
|
1473
|
+
|
|
1474
|
+
This allows CSS files to use dynamic substitutions: `color: {{theme_color}};`
|
|
1475
|
+
|
|
1476
|
+
##### `global_subs?: Record<string, BootstrapSub>`
|
|
1477
|
+
Optional global template substitutions available to all routes, error pages, and static files with `sub_ext`.
|
|
1478
|
+
|
|
1479
|
+
```ts
|
|
1480
|
+
global_subs: {
|
|
1481
|
+
site_name: 'My Website',
|
|
1482
|
+
version: '1.0.0',
|
|
1483
|
+
api_url: 'https://api.example.com',
|
|
1484
|
+
|
|
1485
|
+
// Function-based substitutions for dynamic content
|
|
1486
|
+
current_year: () => new Date().getFullYear().toString(),
|
|
1487
|
+
|
|
1488
|
+
build_time: async () => {
|
|
1489
|
+
// Example: fetch build timestamp from git
|
|
1490
|
+
const process = Bun.spawn(['git', 'log', '-1', '--format=%ct']);
|
|
1491
|
+
const output = await Bun.readableStreamToText(process.stdout);
|
|
1492
|
+
return new Date(parseInt(output.trim()) * 1000).toISOString();
|
|
1493
|
+
},
|
|
1494
|
+
|
|
1495
|
+
user_count: async () => {
|
|
1496
|
+
// Example: dynamic user count from database
|
|
1497
|
+
const count = await db.count('SELECT COUNT(*) as count FROM users');
|
|
1498
|
+
return count.toLocaleString();
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
```
|
|
1502
|
+
|
|
1503
|
+
Functions in `global_subs` and route-specific `subs` are called during template processing, allowing for dynamic content generation. Both synchronous and asynchronous functions are supported.
|
|
1504
|
+
|
|
1505
|
+
#### Template Processing Order
|
|
1506
|
+
|
|
1507
|
+
1. Route content is loaded
|
|
1508
|
+
2. If `base` is defined, content is wrapped using `{{content}}` substitution
|
|
1509
|
+
3. Route-specific `subs` and `global_subs` are applied
|
|
1510
|
+
4. Hash substitutions (if enabled) are applied
|
|
1511
|
+
|
|
1344
1512
|
<a id="api-error-handling"></a>
|
|
1345
1513
|
## API > Error Handling
|
|
1346
1514
|
|
|
@@ -1692,29 +1860,60 @@ await parse_template(template, replacements, true);
|
|
|
1692
1860
|
</html>
|
|
1693
1861
|
```
|
|
1694
1862
|
|
|
1695
|
-
|
|
1863
|
+
#### Custom Replacer Function
|
|
1864
|
+
|
|
1865
|
+
`parse_template` supports passing a function instead of a replacement object. This function will be called for each placeholder and the return value will be used as the replacement. Both synchronous and asynchronous functions are supported.
|
|
1696
1866
|
|
|
1697
1867
|
```ts
|
|
1698
|
-
const replacer = (
|
|
1699
|
-
|
|
1868
|
+
const replacer = (key: string) => {
|
|
1869
|
+
switch (key) {
|
|
1870
|
+
case 'timestamp': return Date.now().toString();
|
|
1871
|
+
case 'random': return Math.random().toString(36).substring(7);
|
|
1872
|
+
case 'greeting': return 'Hello, World!';
|
|
1873
|
+
default: return undefined;
|
|
1874
|
+
}
|
|
1700
1875
|
};
|
|
1701
1876
|
|
|
1702
|
-
await parse_template('
|
|
1877
|
+
await parse_template('Generated at {{timestamp}}: {{greeting}} (ID: {{random}})', replacer);
|
|
1878
|
+
// Result: "Generated at 1635789123456: Hello, World! (ID: x7k2p9m)"
|
|
1703
1879
|
```
|
|
1704
1880
|
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1881
|
+
Custom replacer functions are supported on a per-key basis, mixing with static string replacement.
|
|
1882
|
+
|
|
1883
|
+
```ts
|
|
1884
|
+
await parse_template('Hello {{foo}}, it is {{now}}', {
|
|
1885
|
+
foo: 'world',
|
|
1886
|
+
now: () => Date.now()
|
|
1887
|
+
});
|
|
1888
|
+
```
|
|
1889
|
+
|
|
1890
|
+
#### Key/Value Based Substitutions
|
|
1891
|
+
|
|
1892
|
+
`parse_template` supports key/value based substitutions using the `{{key=value}}` syntax. When a function replacer is provided for the key, the value is passed as a parameter to the function.
|
|
1893
|
+
|
|
1894
|
+
```ts
|
|
1895
|
+
await parse_template('Color: {{hex=blue}}', {
|
|
1896
|
+
hex: (color) => {
|
|
1897
|
+
const colors = { blue: '#0000ff', red: '#ff0000', green: '#00ff00' };
|
|
1898
|
+
return colors[color] || color;
|
|
1899
|
+
}
|
|
1900
|
+
});
|
|
1901
|
+
// Result: "Color: #0000ff"
|
|
1902
|
+
```
|
|
1903
|
+
|
|
1904
|
+
Global replacer functions also support the value parameter:
|
|
1905
|
+
|
|
1906
|
+
```ts
|
|
1907
|
+
await parse_template('Transform: {{upper=hello}} and {{lower=WORLD}}', (key, value) => {
|
|
1908
|
+
if (key === 'upper' && value) return value.toUpperCase();
|
|
1909
|
+
if (key === 'lower' && value) return value.toLowerCase();
|
|
1910
|
+
return 'unknown';
|
|
1911
|
+
});
|
|
1912
|
+
// Result: "Transform: HELLO and world"
|
|
1716
1913
|
```
|
|
1717
1914
|
|
|
1915
|
+
#### Conditional Rendering
|
|
1916
|
+
|
|
1718
1917
|
`parse_template` supports conditional rendering with the following syntax.
|
|
1719
1918
|
|
|
1720
1919
|
```html
|
|
@@ -1803,53 +2002,10 @@ await parse_template(..., {
|
|
|
1803
2002
|
</t-for>
|
|
1804
2003
|
```
|
|
1805
2004
|
|
|
1806
|
-
### 🔧 `generate_hash_subs(length: number, prefix: string, hashes?: Record<string, string>): Promise<Record<string, string>>`
|
|
1807
|
-
|
|
1808
|
-
Generate a replacement table for mapping file paths to hashes in templates. This is useful for cache-busting static assets.
|
|
1809
|
-
|
|
1810
|
-
> [!IMPORTANT]
|
|
1811
|
-
> Internally `generate_hash_subs()` uses `git ls-tree -r HEAD`, so the working directory must be a git repository.
|
|
1812
|
-
|
|
1813
|
-
```ts
|
|
1814
|
-
let hash_sub_table = {};
|
|
1815
|
-
|
|
1816
|
-
generate_hash_subs().then(subs => hash_sub_table = subs).catch(caution);
|
|
1817
|
-
|
|
1818
|
-
server.route('/test', (req, url) => {
|
|
1819
|
-
return parse_template('Hello world {{hash=docs/project-logo.png}}', hash_sub_table);
|
|
1820
|
-
});
|
|
1821
|
-
```
|
|
1822
|
-
|
|
1823
|
-
```html
|
|
1824
|
-
Hello world 754d9ea
|
|
1825
|
-
```
|
|
1826
|
-
|
|
1827
|
-
> [!IMPORTANT]
|
|
1828
|
-
> Specify paths as they appear in git, relative to the repository root and with forward slashes (no leading slash).
|
|
1829
|
-
|
|
1830
|
-
By default hashes are truncated to `7` characters (a short hash), a custom length can be provided instead.
|
|
1831
|
-
|
|
1832
|
-
```ts
|
|
1833
|
-
generate_hash_subs(40).then(...);
|
|
1834
|
-
// d65c52a41a75db43e184d2268c6ea9f9741de63e
|
|
1835
|
-
```
|
|
1836
|
-
|
|
1837
|
-
> [!NOTE]
|
|
1838
|
-
> SHA-1 hashes are `40` characters. Git is transitioning to SHA-256, which are `64` characters. Short hashes of `7` are generally sufficient for cache-busting.
|
|
1839
|
-
|
|
1840
|
-
Use a different prefix other than `hash=` by passing it as the first parameter.
|
|
1841
|
-
|
|
1842
|
-
```ts
|
|
1843
|
-
generate_hash_subs(7, '$#').then(subs => hash_sub_table = subs).catch(caution);
|
|
1844
|
-
|
|
1845
|
-
server.route('/test', (req, url) => {
|
|
1846
|
-
return parse_template('Hello world {{$#docs/project-logo.png}}', hash_sub_table);
|
|
1847
|
-
});
|
|
1848
|
-
```
|
|
1849
2005
|
|
|
1850
2006
|
### 🔧 ``get_git_hashes(length: number): Promise<Record<string, string>>``
|
|
1851
2007
|
|
|
1852
|
-
|
|
2008
|
+
Retrieve git hashes for all files in the repository. This is useful for implementing cache-busting functionality or creating file integrity checks.
|
|
1853
2009
|
|
|
1854
2010
|
> [!IMPORTANT]
|
|
1855
2011
|
> Internally `get_git_hashes()` uses `git ls-tree -r HEAD`, so the working directory must be a git repository.
|
|
@@ -1859,14 +2015,11 @@ const hashes = await get_git_hashes(7);
|
|
|
1859
2015
|
// { 'docs/project-logo.png': '754d9ea' }
|
|
1860
2016
|
```
|
|
1861
2017
|
|
|
1862
|
-
|
|
2018
|
+
You can specify the hash length (default is 7 characters for short hashes):
|
|
1863
2019
|
|
|
1864
2020
|
```ts
|
|
1865
|
-
const
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
// hashes[0] -> { 'docs/project-logo.png': '754d9ea' }
|
|
1869
|
-
// subs[0] -> { 'hash=docs/project-logo.png': '754d9ea' }
|
|
2021
|
+
const full_hashes = await get_git_hashes(40);
|
|
2022
|
+
// { 'docs/project-logo.png': 'd65c52a41a75db43e184d2268c6ea9f9741de63e' }
|
|
1870
2023
|
```
|
|
1871
2024
|
|
|
1872
2025
|
<a id="api-database"></a>
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -397,9 +397,14 @@ export async function safe(target_fn: Callable) {
|
|
|
397
397
|
// endregion
|
|
398
398
|
|
|
399
399
|
// region templates
|
|
400
|
-
type ReplacerFn = (key: string) => string | Array<string> | undefined;
|
|
401
|
-
type AsyncReplaceFn = (key: string) => Promise<string | Array<string> | undefined>;
|
|
402
|
-
type
|
|
400
|
+
type ReplacerFn = (key: string, value?: string) => string | Array<string> | undefined;
|
|
401
|
+
type AsyncReplaceFn = (key: string, value?: string) => Promise<string | Array<string> | undefined>;
|
|
402
|
+
type ReplacementValueFn = () => string | Array<string> | undefined;
|
|
403
|
+
type AsyncReplacementValueFn = () => Promise<string | Array<string> | undefined>;
|
|
404
|
+
type ReplacementValueWithKeyFn = (value?: string) => string | Array<string> | undefined;
|
|
405
|
+
type AsyncReplacementValueWithKeyFn = (value?: string) => Promise<string | Array<string> | undefined>;
|
|
406
|
+
type ReplacementValue = string | Array<string> | object | object[] | ReplacementValueFn | AsyncReplacementValueFn | ReplacementValueWithKeyFn | AsyncReplacementValueWithKeyFn;
|
|
407
|
+
type Replacements = Record<string, ReplacementValue> | ReplacerFn | AsyncReplaceFn;
|
|
403
408
|
|
|
404
409
|
function get_nested_property(obj: any, path: string): any {
|
|
405
410
|
const keys = path.split('.');
|
|
@@ -435,11 +440,14 @@ export async function parse_template(template: string, replacements: Replacement
|
|
|
435
440
|
|
|
436
441
|
if (typeof replacements === 'function') {
|
|
437
442
|
scoped_replacements = async (key: string) => {
|
|
438
|
-
if (key === alias_name)
|
|
443
|
+
if (key === alias_name)
|
|
444
|
+
return loop_entry;
|
|
445
|
+
|
|
439
446
|
if (key.startsWith(alias_name + '.')) {
|
|
440
447
|
const prop_path = key.substring(alias_name.length + 1);
|
|
441
448
|
return get_nested_property(loop_entry, prop_path);
|
|
442
449
|
}
|
|
450
|
+
|
|
443
451
|
return await replacements(key);
|
|
444
452
|
};
|
|
445
453
|
} else {
|
|
@@ -467,6 +475,7 @@ export async function parse_template(template: string, replacements: Replacement
|
|
|
467
475
|
|
|
468
476
|
if (!drop_missing && !condition_value)
|
|
469
477
|
return match;
|
|
478
|
+
|
|
470
479
|
if (condition_value)
|
|
471
480
|
return await parse_template(if_content, replacements, drop_missing);
|
|
472
481
|
|
|
@@ -478,15 +487,31 @@ export async function parse_template(template: string, replacements: Replacement
|
|
|
478
487
|
result = await replace_async(result, var_regex, async (match, var_name) => {
|
|
479
488
|
// Trim whitespace from variable name
|
|
480
489
|
var_name = var_name.trim();
|
|
490
|
+
|
|
491
|
+
// Check for key=value syntax
|
|
492
|
+
let key = var_name;
|
|
493
|
+
let value: string | undefined = undefined;
|
|
494
|
+
const equals_index = var_name.indexOf('=');
|
|
495
|
+
|
|
496
|
+
if (equals_index !== -1) {
|
|
497
|
+
key = var_name.substring(0, equals_index);
|
|
498
|
+
value = var_name.substring(equals_index + 1);
|
|
499
|
+
}
|
|
500
|
+
|
|
481
501
|
let replacement;
|
|
482
502
|
|
|
483
503
|
if (is_replacer_fn) {
|
|
484
|
-
replacement = await replacements(
|
|
504
|
+
replacement = await replacements(key, value);
|
|
485
505
|
} else {
|
|
486
506
|
// First try direct key lookup (handles hash keys with dots like "hash=.gitignore")
|
|
487
507
|
replacement = replacements[var_name];
|
|
488
508
|
|
|
489
|
-
// If direct lookup fails and
|
|
509
|
+
// If direct lookup fails and we have key=value syntax, try key lookup
|
|
510
|
+
if (replacement === undefined && value !== undefined) {
|
|
511
|
+
replacement = replacements[key];
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// If still undefined and variable contains dots, try nested property access
|
|
490
515
|
if (replacement === undefined && var_name.includes('.')) {
|
|
491
516
|
const dot_index = var_name.indexOf('.');
|
|
492
517
|
const base_key = var_name.substring(0, dot_index);
|
|
@@ -499,6 +524,13 @@ export async function parse_template(template: string, replacements: Replacement
|
|
|
499
524
|
}
|
|
500
525
|
}
|
|
501
526
|
|
|
527
|
+
if (replacement !== undefined && typeof replacement === 'function') {
|
|
528
|
+
if (value !== undefined && replacement.length > 0)
|
|
529
|
+
replacement = await replacement(value);
|
|
530
|
+
else
|
|
531
|
+
replacement = await replacement();
|
|
532
|
+
}
|
|
533
|
+
|
|
502
534
|
if (replacement !== undefined)
|
|
503
535
|
return replacement;
|
|
504
536
|
|
|
@@ -549,17 +581,6 @@ export async function get_git_hashes(length = 7): Promise<Record<string, string>
|
|
|
549
581
|
return hash_map;
|
|
550
582
|
}
|
|
551
583
|
|
|
552
|
-
export async function generate_hash_subs(length = 7, prefix = 'hash=', hashes?: Record<string, string>): Promise<Record<string, string>> {
|
|
553
|
-
const hash_map: Record<string, string> = {};
|
|
554
|
-
|
|
555
|
-
if (!hashes)
|
|
556
|
-
hashes = await get_git_hashes(length);
|
|
557
|
-
|
|
558
|
-
for (const [file, hash] of Object.entries(hashes))
|
|
559
|
-
hash_map[prefix + file] = hash;
|
|
560
|
-
|
|
561
|
-
return hash_map;
|
|
562
|
-
}
|
|
563
584
|
// endregion
|
|
564
585
|
|
|
565
586
|
// region serving
|
|
@@ -976,7 +997,7 @@ type WebsocketHandlers = {
|
|
|
976
997
|
drain?: (ws: WebSocket) => void
|
|
977
998
|
};
|
|
978
999
|
|
|
979
|
-
type BootstrapSub =
|
|
1000
|
+
type BootstrapSub = ReplacementValue;
|
|
980
1001
|
|
|
981
1002
|
type BootstrapRoute = {
|
|
982
1003
|
content: string | BunFile;
|
|
@@ -1407,8 +1428,20 @@ export function http_serve(port: number, hostname?: string) {
|
|
|
1407
1428
|
|
|
1408
1429
|
/* Bootstrap a static web server */
|
|
1409
1430
|
bootstrap: async function(options: BootstrapOptions) {
|
|
1410
|
-
|
|
1411
|
-
|
|
1431
|
+
let git_hash_table: Record<string, string> = {};
|
|
1432
|
+
let cache_bust_subs = {};
|
|
1433
|
+
|
|
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
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
const global_sub_table = sub_table_merge(cache_bust_subs, options.global_subs);
|
|
1412
1445
|
|
|
1413
1446
|
let cache = options.cache;
|
|
1414
1447
|
if (cache !== undefined && !is_cache_http(cache))
|