@shaferllc/keel 0.36.0 → 0.59.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 +14 -2
- package/dist/core/application.d.ts +58 -2
- package/dist/core/application.js +99 -3
- package/dist/core/auth.d.ts +21 -2
- package/dist/core/auth.js +38 -3
- package/dist/core/authorization.d.ts +52 -0
- package/dist/core/authorization.js +97 -0
- package/dist/core/broadcasting.d.ts +49 -0
- package/dist/core/broadcasting.js +84 -0
- package/dist/core/broker.d.ts +398 -0
- package/dist/core/broker.js +602 -0
- package/dist/core/crypto.d.ts +51 -1
- package/dist/core/crypto.js +96 -3
- package/dist/core/database.d.ts +26 -1
- package/dist/core/database.js +65 -2
- package/dist/core/decorators.d.ts +39 -0
- package/dist/core/decorators.js +72 -0
- package/dist/core/exceptions.d.ts +77 -6
- package/dist/core/exceptions.js +168 -10
- package/dist/core/helpers.d.ts +6 -0
- package/dist/core/helpers.js +12 -0
- package/dist/core/http/kernel.js +14 -2
- package/dist/core/http/router.d.ts +14 -0
- package/dist/core/http/router.js +30 -1
- package/dist/core/index.d.ts +31 -8
- package/dist/core/index.js +17 -5
- package/dist/core/logger.d.ts +5 -0
- package/dist/core/logger.js +24 -2
- package/dist/core/migrations.js +3 -3
- package/dist/core/model.d.ts +19 -1
- package/dist/core/model.js +72 -4
- package/dist/core/provider.d.ts +13 -4
- package/dist/core/provider.js +12 -2
- package/dist/core/redis.d.ts +78 -0
- package/dist/core/redis.js +176 -0
- package/dist/core/request-logger.d.ts +26 -0
- package/dist/core/request-logger.js +48 -0
- package/dist/core/request.d.ts +17 -1
- package/dist/core/request.js +27 -1
- package/dist/core/scheduler.d.ts +60 -0
- package/dist/core/scheduler.js +166 -0
- package/dist/core/session.js +17 -2
- package/dist/core/storage.d.ts +57 -0
- package/dist/core/storage.js +98 -0
- package/dist/core/template.d.ts +50 -0
- package/dist/core/template.js +753 -0
- package/dist/core/testing.d.ts +54 -0
- package/dist/core/testing.js +141 -0
- package/dist/core/transformer.d.ts +89 -0
- package/dist/core/transformer.js +152 -0
- package/dist/core/validation.d.ts +20 -0
- package/dist/core/validation.js +52 -1
- package/dist/core/vite.d.ts +117 -0
- package/dist/core/vite.js +258 -0
- package/dist/vite/index.d.ts +40 -0
- package/dist/vite/index.js +146 -0
- package/package.json +16 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task scheduling — declare recurring work with a fluent cadence, then let a
|
|
3
|
+
* cron trigger drive it. A code-defined scheduler, edge-first: you run the
|
|
4
|
+
* scheduler from a single per-minute trigger (Cloudflare Cron Triggers, or a
|
|
5
|
+
* Node interval), and it runs whatever's due.
|
|
6
|
+
*
|
|
7
|
+
* schedule(new PruneSessions()).daily();
|
|
8
|
+
* schedule(() => syncInventory()).everyFiveMinutes();
|
|
9
|
+
* schedule(new SendDigest()).cron("0 9 * * 1"); // 9am Mondays
|
|
10
|
+
*
|
|
11
|
+
* // from a Cloudflare `scheduled()` handler (or a Node setInterval):
|
|
12
|
+
* await scheduler().runDue(new Date(event.scheduledTime));
|
|
13
|
+
*
|
|
14
|
+
* A task is a `Job` or a plain function. `runDue(now)` runs every task whose
|
|
15
|
+
* cron expression matches `now` (to the minute).
|
|
16
|
+
*/
|
|
17
|
+
import { type Dispatchable } from "./queue.js";
|
|
18
|
+
/** Whether a 5-field cron expression matches `date` (to the minute, in the Date's own fields). */
|
|
19
|
+
export declare function cronMatches(expression: string, date: Date): boolean;
|
|
20
|
+
export declare class ScheduledTask {
|
|
21
|
+
readonly job: Dispatchable;
|
|
22
|
+
expression: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
constructor(job: Dispatchable);
|
|
25
|
+
/** Set a raw cron expression. */
|
|
26
|
+
cron(expression: string): this;
|
|
27
|
+
/** Label the task (for logging / introspection). */
|
|
28
|
+
named(name: string): this;
|
|
29
|
+
everyMinute(): this;
|
|
30
|
+
everyFiveMinutes(): this;
|
|
31
|
+
everyTenMinutes(): this;
|
|
32
|
+
everyFifteenMinutes(): this;
|
|
33
|
+
everyThirtyMinutes(): this;
|
|
34
|
+
hourly(): this;
|
|
35
|
+
/** At `minute` past every hour. */
|
|
36
|
+
hourlyAt(minute: number): this;
|
|
37
|
+
daily(): this;
|
|
38
|
+
/** Once a day at `HH:MM`. */
|
|
39
|
+
dailyAt(time: string): this;
|
|
40
|
+
/** Weekly on `weekday` (0 = Sunday) at midnight. */
|
|
41
|
+
weekly(weekday?: number): this;
|
|
42
|
+
/** Monthly on `day` at midnight. */
|
|
43
|
+
monthly(day?: number): this;
|
|
44
|
+
isDue(now: Date): boolean;
|
|
45
|
+
}
|
|
46
|
+
export declare class Scheduler {
|
|
47
|
+
readonly tasks: ScheduledTask[];
|
|
48
|
+
/** Register a task and return it for cadence configuration. */
|
|
49
|
+
schedule(job: Dispatchable): ScheduledTask;
|
|
50
|
+
/** The tasks due at `now`. */
|
|
51
|
+
due(now?: Date): ScheduledTask[];
|
|
52
|
+
/** Run every task due at `now` (in registration order); returns how many ran. */
|
|
53
|
+
runDue(now?: Date): Promise<number>;
|
|
54
|
+
}
|
|
55
|
+
/** The default scheduler. */
|
|
56
|
+
export declare function scheduler(): Scheduler;
|
|
57
|
+
/** Replace the default scheduler (e.g. to reset between tests). */
|
|
58
|
+
export declare function setScheduler(next: Scheduler): Scheduler;
|
|
59
|
+
/** Schedule a task on the default scheduler. */
|
|
60
|
+
export declare function schedule(job: Dispatchable): ScheduledTask;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task scheduling — declare recurring work with a fluent cadence, then let a
|
|
3
|
+
* cron trigger drive it. A code-defined scheduler, edge-first: you run the
|
|
4
|
+
* scheduler from a single per-minute trigger (Cloudflare Cron Triggers, or a
|
|
5
|
+
* Node interval), and it runs whatever's due.
|
|
6
|
+
*
|
|
7
|
+
* schedule(new PruneSessions()).daily();
|
|
8
|
+
* schedule(() => syncInventory()).everyFiveMinutes();
|
|
9
|
+
* schedule(new SendDigest()).cron("0 9 * * 1"); // 9am Mondays
|
|
10
|
+
*
|
|
11
|
+
* // from a Cloudflare `scheduled()` handler (or a Node setInterval):
|
|
12
|
+
* await scheduler().runDue(new Date(event.scheduledTime));
|
|
13
|
+
*
|
|
14
|
+
* A task is a `Job` or a plain function. `runDue(now)` runs every task whose
|
|
15
|
+
* cron expression matches `now` (to the minute).
|
|
16
|
+
*/
|
|
17
|
+
import { Job } from "./queue.js";
|
|
18
|
+
function runTask(job) {
|
|
19
|
+
return Promise.resolve(job instanceof Job ? job.handle() : job());
|
|
20
|
+
}
|
|
21
|
+
/* -------------------------------- cron ------------------------------------ */
|
|
22
|
+
/** Match one cron field (`*`, `5`, `1,2`, `1-5`, `*/5`, `0-30/10`) against a value. */
|
|
23
|
+
function fieldMatches(spec, value, min, max) {
|
|
24
|
+
if (spec === "*")
|
|
25
|
+
return true;
|
|
26
|
+
for (const part of spec.split(",")) {
|
|
27
|
+
const [range, stepStr] = part.split("/");
|
|
28
|
+
const step = stepStr ? Number(stepStr) : 1;
|
|
29
|
+
if (!Number.isFinite(step) || step < 1)
|
|
30
|
+
continue;
|
|
31
|
+
let lo;
|
|
32
|
+
let hi;
|
|
33
|
+
if (range === "*" || range === undefined) {
|
|
34
|
+
lo = min;
|
|
35
|
+
hi = max;
|
|
36
|
+
}
|
|
37
|
+
else if (range.includes("-")) {
|
|
38
|
+
const [a, b] = range.split("-");
|
|
39
|
+
lo = Number(a);
|
|
40
|
+
hi = Number(b);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
lo = Number(range);
|
|
44
|
+
hi = stepStr ? max : lo; // "5/10" means 5, 15, 25, … up to max
|
|
45
|
+
}
|
|
46
|
+
if (value >= lo && value <= hi && (value - lo) % step === 0)
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
/** Whether a 5-field cron expression matches `date` (to the minute, in the Date's own fields). */
|
|
52
|
+
export function cronMatches(expression, date) {
|
|
53
|
+
const parts = expression.trim().split(/\s+/);
|
|
54
|
+
if (parts.length !== 5)
|
|
55
|
+
throw new Error(`Invalid cron expression: "${expression}" (need 5 fields)`);
|
|
56
|
+
const [min, hour, dom, mon, dow] = parts;
|
|
57
|
+
const minuteOk = fieldMatches(min, date.getMinutes(), 0, 59);
|
|
58
|
+
const hourOk = fieldMatches(hour, date.getHours(), 0, 23);
|
|
59
|
+
const monthOk = fieldMatches(mon, date.getMonth() + 1, 1, 12);
|
|
60
|
+
const domOk = fieldMatches(dom, date.getDate(), 1, 31);
|
|
61
|
+
const dowOk = fieldMatches(dow, date.getDay(), 0, 6);
|
|
62
|
+
// Standard (Vixie) cron: when both day-of-month and day-of-week are restricted,
|
|
63
|
+
// the day matches if *either* does; otherwise both must match.
|
|
64
|
+
const dayOk = dom !== "*" && dow !== "*" ? domOk || dowOk : domOk && dowOk;
|
|
65
|
+
return minuteOk && hourOk && monthOk && dayOk;
|
|
66
|
+
}
|
|
67
|
+
/* ----------------------------- scheduled task ----------------------------- */
|
|
68
|
+
export class ScheduledTask {
|
|
69
|
+
job;
|
|
70
|
+
expression = "* * * * *";
|
|
71
|
+
name;
|
|
72
|
+
constructor(job) {
|
|
73
|
+
this.job = job;
|
|
74
|
+
}
|
|
75
|
+
/** Set a raw cron expression. */
|
|
76
|
+
cron(expression) {
|
|
77
|
+
this.expression = expression;
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
/** Label the task (for logging / introspection). */
|
|
81
|
+
named(name) {
|
|
82
|
+
this.name = name;
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
everyMinute() {
|
|
86
|
+
return this.cron("* * * * *");
|
|
87
|
+
}
|
|
88
|
+
everyFiveMinutes() {
|
|
89
|
+
return this.cron("*/5 * * * *");
|
|
90
|
+
}
|
|
91
|
+
everyTenMinutes() {
|
|
92
|
+
return this.cron("*/10 * * * *");
|
|
93
|
+
}
|
|
94
|
+
everyFifteenMinutes() {
|
|
95
|
+
return this.cron("*/15 * * * *");
|
|
96
|
+
}
|
|
97
|
+
everyThirtyMinutes() {
|
|
98
|
+
return this.cron("*/30 * * * *");
|
|
99
|
+
}
|
|
100
|
+
hourly() {
|
|
101
|
+
return this.cron("0 * * * *");
|
|
102
|
+
}
|
|
103
|
+
/** At `minute` past every hour. */
|
|
104
|
+
hourlyAt(minute) {
|
|
105
|
+
return this.cron(`${minute} * * * *`);
|
|
106
|
+
}
|
|
107
|
+
daily() {
|
|
108
|
+
return this.cron("0 0 * * *");
|
|
109
|
+
}
|
|
110
|
+
/** Once a day at `HH:MM`. */
|
|
111
|
+
dailyAt(time) {
|
|
112
|
+
const [h, m] = time.split(":");
|
|
113
|
+
return this.cron(`${Number(m)} ${Number(h)} * * *`);
|
|
114
|
+
}
|
|
115
|
+
/** Weekly on `weekday` (0 = Sunday) at midnight. */
|
|
116
|
+
weekly(weekday = 0) {
|
|
117
|
+
return this.cron(`0 0 * * ${weekday}`);
|
|
118
|
+
}
|
|
119
|
+
/** Monthly on `day` at midnight. */
|
|
120
|
+
monthly(day = 1) {
|
|
121
|
+
return this.cron(`0 0 ${day} * *`);
|
|
122
|
+
}
|
|
123
|
+
isDue(now) {
|
|
124
|
+
return cronMatches(this.expression, now);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/* ------------------------------- scheduler -------------------------------- */
|
|
128
|
+
export class Scheduler {
|
|
129
|
+
tasks = [];
|
|
130
|
+
/** Register a task and return it for cadence configuration. */
|
|
131
|
+
schedule(job) {
|
|
132
|
+
const task = new ScheduledTask(job);
|
|
133
|
+
this.tasks.push(task);
|
|
134
|
+
return task;
|
|
135
|
+
}
|
|
136
|
+
/** The tasks due at `now`. */
|
|
137
|
+
due(now = new Date()) {
|
|
138
|
+
return this.tasks.filter((t) => t.isDue(now));
|
|
139
|
+
}
|
|
140
|
+
/** Run every task due at `now` (in registration order); returns how many ran. */
|
|
141
|
+
async runDue(now = new Date()) {
|
|
142
|
+
let count = 0;
|
|
143
|
+
for (const task of this.tasks) {
|
|
144
|
+
if (task.isDue(now)) {
|
|
145
|
+
await runTask(task.job);
|
|
146
|
+
count++;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return count;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/* -------------------------------- global ---------------------------------- */
|
|
153
|
+
let instance = new Scheduler();
|
|
154
|
+
/** The default scheduler. */
|
|
155
|
+
export function scheduler() {
|
|
156
|
+
return instance;
|
|
157
|
+
}
|
|
158
|
+
/** Replace the default scheduler (e.g. to reset between tests). */
|
|
159
|
+
export function setScheduler(next) {
|
|
160
|
+
instance = next;
|
|
161
|
+
return instance;
|
|
162
|
+
}
|
|
163
|
+
/** Schedule a task on the default scheduler. */
|
|
164
|
+
export function schedule(job) {
|
|
165
|
+
return instance.schedule(job);
|
|
166
|
+
}
|
package/dist/core/session.js
CHANGED
|
@@ -11,6 +11,21 @@ import { getCookie, setCookie } from "hono/cookie";
|
|
|
11
11
|
import { ctx } from "./request.js";
|
|
12
12
|
const FLASH = "__flash";
|
|
13
13
|
const OLD = "__old";
|
|
14
|
+
/** UTF-8-safe base64 — plain btoa throws on non-Latin1 (emoji, many scripts). */
|
|
15
|
+
function b64encode(json) {
|
|
16
|
+
const bytes = new TextEncoder().encode(json);
|
|
17
|
+
let s = "";
|
|
18
|
+
for (const b of bytes)
|
|
19
|
+
s += String.fromCharCode(b);
|
|
20
|
+
return btoa(s);
|
|
21
|
+
}
|
|
22
|
+
function b64decode(raw) {
|
|
23
|
+
const s = atob(raw);
|
|
24
|
+
const bytes = new Uint8Array(s.length);
|
|
25
|
+
for (let i = 0; i < s.length; i++)
|
|
26
|
+
bytes[i] = s.charCodeAt(i);
|
|
27
|
+
return new TextDecoder().decode(bytes);
|
|
28
|
+
}
|
|
14
29
|
export class Session {
|
|
15
30
|
data;
|
|
16
31
|
constructor(data) {
|
|
@@ -81,7 +96,7 @@ export function sessionMiddleware(options = {}) {
|
|
|
81
96
|
const raw = getCookie(c, name);
|
|
82
97
|
if (raw) {
|
|
83
98
|
try {
|
|
84
|
-
data = JSON.parse(
|
|
99
|
+
data = JSON.parse(b64decode(raw));
|
|
85
100
|
}
|
|
86
101
|
catch {
|
|
87
102
|
/* tampered/expired — start fresh */
|
|
@@ -94,7 +109,7 @@ export function sessionMiddleware(options = {}) {
|
|
|
94
109
|
await next();
|
|
95
110
|
const toStore = { ...data };
|
|
96
111
|
delete toStore[OLD];
|
|
97
|
-
setCookie(c, name,
|
|
112
|
+
setCookie(c, name, b64encode(JSON.stringify(toStore)), {
|
|
98
113
|
httpOnly: true,
|
|
99
114
|
path: "/",
|
|
100
115
|
sameSite: "Lax",
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File storage over a pluggable `Disk` — like the database and mail layers, the
|
|
3
|
+
* core imports no filesystem or SDK, so it runs on Node and the edge. Point a
|
|
4
|
+
* disk at the local filesystem (Node), S3 (`fetch`), or a Cloudflare R2 binding;
|
|
5
|
+
* `MemoryDisk` is the built-in default for tests.
|
|
6
|
+
*
|
|
7
|
+
* setDisk(new MemoryDisk()); // or setDisk(r2Disk(env.BUCKET))
|
|
8
|
+
* await storage().put("avatars/1.png", bytes);
|
|
9
|
+
* const bytes = await storage().get("avatars/1.png");
|
|
10
|
+
* const url = storage().url("avatars/1.png");
|
|
11
|
+
*
|
|
12
|
+
* Register several disks by name and select one with `storage("s3")`.
|
|
13
|
+
*/
|
|
14
|
+
/** The bridge to a storage backend — implement it once per backend. */
|
|
15
|
+
export interface Disk {
|
|
16
|
+
put(path: string, bytes: Uint8Array): Promise<void>;
|
|
17
|
+
get(path: string): Promise<Uint8Array | null>;
|
|
18
|
+
exists(path: string): Promise<boolean>;
|
|
19
|
+
delete(path: string): Promise<void>;
|
|
20
|
+
/** Paths currently stored, optionally filtered to those under `prefix`. */
|
|
21
|
+
list(prefix?: string): Promise<string[]>;
|
|
22
|
+
/** A URL for the stored object (public or signed — the disk decides). */
|
|
23
|
+
url(path: string): string;
|
|
24
|
+
}
|
|
25
|
+
export type Contents = string | Uint8Array | ArrayBuffer;
|
|
26
|
+
/** An in-memory `Disk` — the default; ideal for tests. Not shared across processes. */
|
|
27
|
+
export declare class MemoryDisk implements Disk {
|
|
28
|
+
private baseUrl;
|
|
29
|
+
private files;
|
|
30
|
+
constructor(baseUrl?: string);
|
|
31
|
+
put(path: string, bytes: Uint8Array): Promise<void>;
|
|
32
|
+
get(path: string): Promise<Uint8Array | null>;
|
|
33
|
+
exists(path: string): Promise<boolean>;
|
|
34
|
+
delete(path: string): Promise<void>;
|
|
35
|
+
list(prefix?: string): Promise<string[]>;
|
|
36
|
+
url(path: string): string;
|
|
37
|
+
}
|
|
38
|
+
export declare class Storage {
|
|
39
|
+
private disk;
|
|
40
|
+
constructor(disk: Disk);
|
|
41
|
+
/** Write a file (string, bytes, or ArrayBuffer — strings are UTF-8 encoded). */
|
|
42
|
+
put(path: string, contents: Contents): Promise<void>;
|
|
43
|
+
/** Read a file's raw bytes, or null if it doesn't exist. */
|
|
44
|
+
get(path: string): Promise<Uint8Array | null>;
|
|
45
|
+
/** Read a file as UTF-8 text, or null if it doesn't exist. */
|
|
46
|
+
getText(path: string): Promise<string | null>;
|
|
47
|
+
exists(path: string): Promise<boolean>;
|
|
48
|
+
delete(path: string): Promise<void>;
|
|
49
|
+
list(prefix?: string): Promise<string[]>;
|
|
50
|
+
url(path: string): string;
|
|
51
|
+
/** The underlying driver, for backend-specific operations. */
|
|
52
|
+
get driver(): Disk;
|
|
53
|
+
}
|
|
54
|
+
/** Register a disk, optionally under a name (default: `"default"`). */
|
|
55
|
+
export declare function setDisk(disk: Disk, name?: string): Storage;
|
|
56
|
+
/** The default disk, or a named one registered with `setDisk(disk, name)`. */
|
|
57
|
+
export declare function storage(name?: string): Storage;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File storage over a pluggable `Disk` — like the database and mail layers, the
|
|
3
|
+
* core imports no filesystem or SDK, so it runs on Node and the edge. Point a
|
|
4
|
+
* disk at the local filesystem (Node), S3 (`fetch`), or a Cloudflare R2 binding;
|
|
5
|
+
* `MemoryDisk` is the built-in default for tests.
|
|
6
|
+
*
|
|
7
|
+
* setDisk(new MemoryDisk()); // or setDisk(r2Disk(env.BUCKET))
|
|
8
|
+
* await storage().put("avatars/1.png", bytes);
|
|
9
|
+
* const bytes = await storage().get("avatars/1.png");
|
|
10
|
+
* const url = storage().url("avatars/1.png");
|
|
11
|
+
*
|
|
12
|
+
* Register several disks by name and select one with `storage("s3")`.
|
|
13
|
+
*/
|
|
14
|
+
function toBytes(contents) {
|
|
15
|
+
if (typeof contents === "string")
|
|
16
|
+
return new TextEncoder().encode(contents);
|
|
17
|
+
if (contents instanceof Uint8Array)
|
|
18
|
+
return contents;
|
|
19
|
+
return new Uint8Array(contents);
|
|
20
|
+
}
|
|
21
|
+
/* ------------------------------ memory disk ------------------------------- */
|
|
22
|
+
/** An in-memory `Disk` — the default; ideal for tests. Not shared across processes. */
|
|
23
|
+
export class MemoryDisk {
|
|
24
|
+
baseUrl;
|
|
25
|
+
files = new Map();
|
|
26
|
+
constructor(baseUrl = "/storage") {
|
|
27
|
+
this.baseUrl = baseUrl;
|
|
28
|
+
}
|
|
29
|
+
async put(path, bytes) {
|
|
30
|
+
this.files.set(path, bytes);
|
|
31
|
+
}
|
|
32
|
+
async get(path) {
|
|
33
|
+
return this.files.get(path) ?? null;
|
|
34
|
+
}
|
|
35
|
+
async exists(path) {
|
|
36
|
+
return this.files.has(path);
|
|
37
|
+
}
|
|
38
|
+
async delete(path) {
|
|
39
|
+
this.files.delete(path);
|
|
40
|
+
}
|
|
41
|
+
async list(prefix = "") {
|
|
42
|
+
return [...this.files.keys()].filter((p) => p.startsWith(prefix)).sort();
|
|
43
|
+
}
|
|
44
|
+
url(path) {
|
|
45
|
+
return `${this.baseUrl}/${path}`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/* -------------------------------- storage --------------------------------- */
|
|
49
|
+
export class Storage {
|
|
50
|
+
disk;
|
|
51
|
+
constructor(disk) {
|
|
52
|
+
this.disk = disk;
|
|
53
|
+
}
|
|
54
|
+
/** Write a file (string, bytes, or ArrayBuffer — strings are UTF-8 encoded). */
|
|
55
|
+
put(path, contents) {
|
|
56
|
+
return this.disk.put(path, toBytes(contents));
|
|
57
|
+
}
|
|
58
|
+
/** Read a file's raw bytes, or null if it doesn't exist. */
|
|
59
|
+
get(path) {
|
|
60
|
+
return this.disk.get(path);
|
|
61
|
+
}
|
|
62
|
+
/** Read a file as UTF-8 text, or null if it doesn't exist. */
|
|
63
|
+
async getText(path) {
|
|
64
|
+
const bytes = await this.disk.get(path);
|
|
65
|
+
return bytes == null ? null : new TextDecoder().decode(bytes);
|
|
66
|
+
}
|
|
67
|
+
exists(path) {
|
|
68
|
+
return this.disk.exists(path);
|
|
69
|
+
}
|
|
70
|
+
delete(path) {
|
|
71
|
+
return this.disk.delete(path);
|
|
72
|
+
}
|
|
73
|
+
list(prefix) {
|
|
74
|
+
return this.disk.list(prefix);
|
|
75
|
+
}
|
|
76
|
+
url(path) {
|
|
77
|
+
return this.disk.url(path);
|
|
78
|
+
}
|
|
79
|
+
/** The underlying driver, for backend-specific operations. */
|
|
80
|
+
get driver() {
|
|
81
|
+
return this.disk;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/* -------------------------------- global ---------------------------------- */
|
|
85
|
+
const disks = new Map([["default", new Storage(new MemoryDisk())]]);
|
|
86
|
+
/** Register a disk, optionally under a name (default: `"default"`). */
|
|
87
|
+
export function setDisk(disk, name = "default") {
|
|
88
|
+
const store = new Storage(disk);
|
|
89
|
+
disks.set(name, store);
|
|
90
|
+
return store;
|
|
91
|
+
}
|
|
92
|
+
/** The default disk, or a named one registered with `setDisk(disk, name)`. */
|
|
93
|
+
export function storage(name = "default") {
|
|
94
|
+
const store = disks.get(name);
|
|
95
|
+
if (!store)
|
|
96
|
+
throw new Error(`No storage disk named "${name}". Register it with setDisk().`);
|
|
97
|
+
return store;
|
|
98
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A string templating engine in the spirit of AdonisJS Edge — `{{ }}`
|
|
3
|
+
* interpolation and `@`-prefixed tags for logic, includes, layouts, and
|
|
4
|
+
* components.
|
|
5
|
+
*
|
|
6
|
+
* const t = new TemplateEngine();
|
|
7
|
+
* t.register("hello", "Hello, {{ name }}!");
|
|
8
|
+
* await t.render("hello", { name: "Ada" }); // "Hello, Ada!"
|
|
9
|
+
*
|
|
10
|
+
* Unlike engines that compile templates to a function via `eval`/`new
|
|
11
|
+
* Function`, this one *interprets* them against a small, safe expression
|
|
12
|
+
* evaluator — no dynamic code generation — so it runs unchanged on Node and on
|
|
13
|
+
* Cloudflare Workers (where `eval` is forbidden). The expression language is a
|
|
14
|
+
* practical subset of JS: literals, property/index access, method and helper
|
|
15
|
+
* calls, the usual operators, ternaries, arrays/objects, and `|` filters.
|
|
16
|
+
*/
|
|
17
|
+
/** HTML-escape a value for safe interpolation. */
|
|
18
|
+
export declare function escapeHtml(value: unknown): string;
|
|
19
|
+
export type Filter = (value: unknown, ...args: unknown[]) => unknown;
|
|
20
|
+
export interface RenderContext {
|
|
21
|
+
sections: Record<string, string>;
|
|
22
|
+
slots: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
export declare class TemplateEngine {
|
|
25
|
+
private templates;
|
|
26
|
+
private globals;
|
|
27
|
+
private filters;
|
|
28
|
+
constructor();
|
|
29
|
+
/** Register a template by name from its source string. */
|
|
30
|
+
register(name: string, source: string): this;
|
|
31
|
+
/** Register many templates at once (e.g. a Node loader reads files and passes them here). */
|
|
32
|
+
registerAll(sources: Record<string, string>): this;
|
|
33
|
+
/** True if a template is registered. */
|
|
34
|
+
has(name: string): boolean;
|
|
35
|
+
/** Expose a value/function to every template as a global. */
|
|
36
|
+
global(name: string, value: unknown): this;
|
|
37
|
+
/** Register a `{{ value | name }}` filter. */
|
|
38
|
+
filter(name: string, fn: Filter): this;
|
|
39
|
+
/** Render a registered template with the given state. */
|
|
40
|
+
render(name: string, state?: Record<string, unknown>): Promise<string>;
|
|
41
|
+
private ev;
|
|
42
|
+
private renderNodes;
|
|
43
|
+
private renderNode;
|
|
44
|
+
}
|
|
45
|
+
/** The default template engine (register templates/globals/filters on it). */
|
|
46
|
+
export declare function templates(): TemplateEngine;
|
|
47
|
+
/** Replace the default template engine. */
|
|
48
|
+
export declare function setTemplateEngine(e: TemplateEngine): TemplateEngine;
|
|
49
|
+
/** Render a registered template on the default engine. */
|
|
50
|
+
export declare function render(name: string, state?: Record<string, unknown>): Promise<string>;
|