@zerotal/core 1.0.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/CHANGELOG.md +79 -0
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/package.json +72 -0
- package/src/application/Application.ts +1671 -0
- package/src/application/BootDoctor.ts +108 -0
- package/src/application/DevErrorPage.ts +567 -0
- package/src/application/ExceptionHandler.ts +183 -0
- package/src/application/currentApp.ts +73 -0
- package/src/assets/assets.ts +79 -0
- package/src/assets/index.ts +16 -0
- package/src/auth/AuthenticatedUser.ts +18 -0
- package/src/build/PackageLinter.ts +146 -0
- package/src/build/PackageScaffold.ts +127 -0
- package/src/build/codemod.ts +64 -0
- package/src/build/index.ts +12 -0
- package/src/command/Command.ts +254 -0
- package/src/command/CommandRunner.ts +593 -0
- package/src/command/OutputWriter.ts +61 -0
- package/src/command/builtin/CompileCommand.ts +46 -0
- package/src/command/builtin/CssBuildCommand.ts +71 -0
- package/src/command/builtin/KeyGenerateCommand.ts +58 -0
- package/src/command/builtin/LintPackagesCommand.ts +72 -0
- package/src/command/builtin/MakeCommandCommand.ts +85 -0
- package/src/command/builtin/MakeControllerCommand.ts +95 -0
- package/src/command/builtin/MakeEventCommand.ts +85 -0
- package/src/command/builtin/MakeJobCommand.ts +53 -0
- package/src/command/builtin/MakeListenerCommand.ts +35 -0
- package/src/command/builtin/MakeMiddlewareCommand.ts +63 -0
- package/src/command/builtin/MakeNotificationCommand.ts +48 -0
- package/src/command/builtin/MakeObserverCommand.ts +78 -0
- package/src/command/builtin/MakePackageCommand.ts +45 -0
- package/src/command/builtin/MakePolicyCommand.ts +66 -0
- package/src/command/builtin/MakeProviderCommand.ts +75 -0
- package/src/command/builtin/MakeRequestCommand.ts +47 -0
- package/src/command/builtin/MakeResourceCommand.ts +61 -0
- package/src/command/builtin/MakeTestCommand.ts +120 -0
- package/src/command/builtin/ReloadCommand.ts +52 -0
- package/src/command/builtin/ReplCommand.ts +174 -0
- package/src/command/builtin/RouteListCommand.ts +188 -0
- package/src/command/builtin/ServeCommand.ts +321 -0
- package/src/command/builtin/StartCommand.ts +3 -0
- package/src/command/builtin/StatusCommand.ts +71 -0
- package/src/command/builtin/TestCommand.ts +172 -0
- package/src/command/builtin/WorkerCommand.ts +27 -0
- package/src/command/builtin/index.ts +53 -0
- package/src/command/scaffold/worker.ts.txt +12 -0
- package/src/command/scaffold/zerotal.ts.txt +26 -0
- package/src/command/startZerotal.ts +55 -0
- package/src/config/AppConfig.ts +253 -0
- package/src/config/ConfigLoader.ts +117 -0
- package/src/config/ConfigManager.ts +169 -0
- package/src/config/index.ts +46 -0
- package/src/config/registry.ts +59 -0
- package/src/config/validation.ts +117 -0
- package/src/container/Container.ts +606 -0
- package/src/container/ContextualBindingBuilder.ts +57 -0
- package/src/container/ScopedResolver.ts +117 -0
- package/src/container/index.ts +32 -0
- package/src/container/inject.ts +55 -0
- package/src/container/types.ts +71 -0
- package/src/context/RequestContext.ts +91 -0
- package/src/contracts/auth.ts +24 -0
- package/src/contracts/index.ts +23 -0
- package/src/contracts/session.ts +70 -0
- package/src/contracts/transaction.ts +26 -0
- package/src/conventions/ConventionLoader.ts +128 -0
- package/src/conventions/builtinConcerns.ts +131 -0
- package/src/crypt/Crypt.ts +141 -0
- package/src/crypt/URLSigner.ts +96 -0
- package/src/datetime/Carbon.ts +1396 -0
- package/src/datetime/CarbonInterval.ts +421 -0
- package/src/datetime/clock.ts +28 -0
- package/src/datetime/index.ts +23 -0
- package/src/datetime/temporal-shim.ts +1 -0
- package/src/dev/BuildOutput.ts +131 -0
- package/src/dev/CssPlugins.ts +184 -0
- package/src/dev/DevBuildHook.ts +74 -0
- package/src/dev/DevOrchestrator.ts +213 -0
- package/src/dev/DevReloadMiddleware.ts +101 -0
- package/src/dev/DevReloadServer.ts +85 -0
- package/src/dev/DevWsServer.ts +45 -0
- package/src/dev/index.ts +19 -0
- package/src/dev/reloadClient.ts +39 -0
- package/src/env/Def.ts +232 -0
- package/src/env/EnvSchema.ts +105 -0
- package/src/env/index.ts +34 -0
- package/src/env/t.ts +128 -0
- package/src/errors/ConfigError.ts +12 -0
- package/src/errors/ContainerErrors.ts +143 -0
- package/src/errors/HttpError.ts +127 -0
- package/src/errors/ValidationError.ts +19 -0
- package/src/errors/ZerotalError.ts +25 -0
- package/src/errors/index.ts +46 -0
- package/src/events/CallQueuedListener.ts +66 -0
- package/src/events/Emitter.ts +280 -0
- package/src/events/EventFake.ts +160 -0
- package/src/events/FrameworkEvents.ts +252 -0
- package/src/facade/Facade.ts +101 -0
- package/src/facade/facades/App.ts +155 -0
- package/src/facade/facades/Artisan.ts +63 -0
- package/src/facade/facades/Config.ts +21 -0
- package/src/facade/facades/Events.ts +19 -0
- package/src/facade/facades/index.ts +28 -0
- package/src/global.d.ts +9 -0
- package/src/hash/Hash.ts +60 -0
- package/src/health/Health.ts +221 -0
- package/src/health/index.ts +27 -0
- package/src/helpers/Collection.ts +435 -0
- package/src/helpers/config.ts +59 -0
- package/src/helpers/fluent.ts +52 -0
- package/src/helpers/html.ts +11 -0
- package/src/helpers/index.ts +266 -0
- package/src/helpers/make.ts +35 -0
- package/src/helpers/markdown.ts +73 -0
- package/src/helpers/pageElements.ts +27 -0
- package/src/helpers/request.ts +62 -0
- package/src/helpers/response.ts +411 -0
- package/src/helpers/str.ts +208 -0
- package/src/http/Http.ts +298 -0
- package/src/http/HttpClient.ts +289 -0
- package/src/http/Resource.ts +171 -0
- package/src/http/UploadedFile.ts +204 -0
- package/src/http/Uri.ts +490 -0
- package/src/http/index.ts +46 -0
- package/src/http/negotiate.ts +213 -0
- package/src/http/originGuard.ts +76 -0
- package/src/http/sniffContentType.ts +105 -0
- package/src/http/url.ts +204 -0
- package/src/http/withHeaders.ts +24 -0
- package/src/index.ts +250 -0
- package/src/lock/LockManager.ts +228 -0
- package/src/lock/config.ts +49 -0
- package/src/lock/drivers/LockDriver.ts +32 -0
- package/src/lock/drivers/MemoryLockDriver.ts +52 -0
- package/src/lock/drivers/RedisLockDriver.ts +58 -0
- package/src/lock/drivers/SqliteLockDriver.ts +85 -0
- package/src/lock/errors.ts +20 -0
- package/src/lock/facades/Lock.ts +114 -0
- package/src/lock/index.ts +53 -0
- package/src/logger/Log.ts +35 -0
- package/src/logger/LogManager.ts +430 -0
- package/src/logger/LoggerMiddleware.ts +125 -0
- package/src/logger/channels/ConsoleChannel.ts +139 -0
- package/src/logger/channels/DailyChannel.ts +74 -0
- package/src/logger/channels/NullChannel.ts +17 -0
- package/src/logger/channels/SingleChannel.ts +34 -0
- package/src/logger/channels/StackChannel.ts +29 -0
- package/src/logger/config.ts +90 -0
- package/src/logger/format.ts +96 -0
- package/src/logger/frameworkLog.ts +93 -0
- package/src/logger/index.ts +68 -0
- package/src/logger/renderTable.ts +111 -0
- package/src/logger/types.ts +212 -0
- package/src/macros/config.macro.ts +50 -0
- package/src/metrics/HttpMetrics.ts +114 -0
- package/src/metrics/index.ts +18 -0
- package/src/middleware/BaseMiddleware.ts +72 -0
- package/src/middleware/CorsMiddleware.ts +152 -0
- package/src/middleware/RateLimiter.ts +255 -0
- package/src/middleware/SecureHeadersMiddleware.ts +127 -0
- package/src/middleware/ThrottleMiddleware.ts +252 -0
- package/src/middleware/WebhookMiddleware.ts +204 -0
- package/src/pipeline/ContextRegistry.ts +42 -0
- package/src/pipeline/HttpContext.ts +865 -0
- package/src/pipeline/Pipeline.ts +150 -0
- package/src/pipeline/currentPage.ts +46 -0
- package/src/pipeline/types.ts +80 -0
- package/src/provider/LockProvider.ts +64 -0
- package/src/provider/LogProvider.ts +137 -0
- package/src/provider/ServiceProvider.ts +84 -0
- package/src/provider/StorageProvider.ts +45 -0
- package/src/router/FileRouter.ts +526 -0
- package/src/router/Route.ts +76 -0
- package/src/router/RouteHandler.ts +335 -0
- package/src/router/Router.ts +1247 -0
- package/src/router/domain.ts +65 -0
- package/src/security/index.ts +22 -0
- package/src/storage/FakeDisk.ts +233 -0
- package/src/storage/StorageFilesMiddleware.ts +150 -0
- package/src/storage/StorageManager.ts +173 -0
- package/src/storage/config.ts +47 -0
- package/src/storage/drivers/LocalDriver.ts +138 -0
- package/src/storage/drivers/S3Driver.ts +169 -0
- package/src/storage/errors.ts +135 -0
- package/src/storage/facades/Storage.ts +3 -0
- package/src/storage/global.d.ts +7 -0
- package/src/storage/index.ts +22 -0
- package/src/storage/root.ts +59 -0
- package/src/storage/types.ts +104 -0
- package/src/support/appKey.ts +38 -0
- package/src/support/cookie.ts +72 -0
- package/src/support/crypto.ts +52 -0
- package/src/support/deepMerge.ts +117 -0
- package/src/support/env.ts +71 -0
- package/src/support/network.ts +79 -0
- package/src/support/port.ts +197 -0
- package/src/support/str.ts +122 -0
- package/src/view/FileRouteResolver.ts +59 -0
- package/src/view/index.ts +144 -0
- package/src/view/jsx-runtime.ts +233 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named helper exports — importable, tree-shakeable, never globals. Import from
|
|
3
|
+
* `@zerotal/core/helpers`.
|
|
4
|
+
*
|
|
5
|
+
* A grab-bag of small, dependency-free utilities used throughout an application:
|
|
6
|
+
* path resolution ({@link basePath}), environment-variable access
|
|
7
|
+
* ({@link env}, {@link requireEnv}, {@link setAppEnv}), value-flow combinators
|
|
8
|
+
* ({@link tap}, {@link tapAsync}, {@link pipe}, {@link pipeAsync}), error
|
|
9
|
+
* suppression ({@link rescue}, {@link rescueSync}), safe nested lookups
|
|
10
|
+
* ({@link data_get}), the {@link Str} string helper, and {@link markdownPage}
|
|
11
|
+
* for rendering standalone Markdown documents.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* import { env, requireEnv, tap, data_get } from '@zerotal/core/helpers';
|
|
15
|
+
*
|
|
16
|
+
* const debug = env('APP_DEBUG', false); // boolean, coerced
|
|
17
|
+
* const appKey = requireEnv('APP_KEY'); // throws if unset
|
|
18
|
+
* const city = data_get(payload, 'user.address.city', 'Unknown');
|
|
19
|
+
* const user = tap(await User.create(data), (u) => log(`created ${u.id}`));
|
|
20
|
+
*
|
|
21
|
+
* @packageDocumentation
|
|
22
|
+
*/
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { ConfigError } from "../errors/ConfigError.ts";
|
|
25
|
+
|
|
26
|
+
// ── basePath() ────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve a path relative to the application root (`process.cwd()`).
|
|
30
|
+
*
|
|
31
|
+
* Use in `bootstrap/app.ts` when declaring route files or directories so paths
|
|
32
|
+
* are always resolved from the project root rather than the calling file's
|
|
33
|
+
* directory.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // bootstrap/app.ts
|
|
37
|
+
* Application.create({ providers })
|
|
38
|
+
* .routing({ web: basePath('routes/web.ts') })
|
|
39
|
+
* .fileBasedRouting({ web: basePath('app/routes') });
|
|
40
|
+
*/
|
|
41
|
+
export function basePath(...segments: string[]): string {
|
|
42
|
+
return join(process.cwd(), ...segments);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── setAppEnv() ───────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Set `APP_ENV` from the CLI command name — call this in `zerotal.ts` BEFORE
|
|
49
|
+
* the dynamic import of `bootstrap/app.ts` so `Application.create()` sees
|
|
50
|
+
* the correct environment.
|
|
51
|
+
*
|
|
52
|
+
* If `APP_ENV` is already set (e.g. from `.env` or the shell), this is a no-op.
|
|
53
|
+
*
|
|
54
|
+
* | Command | APP_ENV |
|
|
55
|
+
* |----------------------|-----------|
|
|
56
|
+
* | serve / start / s | web |
|
|
57
|
+
* | worker / queue:work | worker |
|
|
58
|
+
* | anything else | console |
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* // zerotal.ts
|
|
62
|
+
* import { setAppEnv, CommandRunner } from '@zerotal/core';
|
|
63
|
+
* setAppEnv(process.argv[2]);
|
|
64
|
+
* const { default: app } = await import('./bootstrap/app.ts');
|
|
65
|
+
*/
|
|
66
|
+
/** Runtime-mode values that Application understands natively — never remapped. */
|
|
67
|
+
const _RUNTIME_MODES = new Set(["web", "worker", "console", "test", "testing", "repl"]);
|
|
68
|
+
|
|
69
|
+
export function setAppEnv(command?: string): void {
|
|
70
|
+
const normalizedCommand = (command ?? "").toLowerCase();
|
|
71
|
+
const current = Bun.env["APP_ENV"];
|
|
72
|
+
const environment = Bun.env as Record<string, string>;
|
|
73
|
+
|
|
74
|
+
if (["serve", "start", "s"].includes(normalizedCommand)) {
|
|
75
|
+
// Always force web mode for the HTTP server — deployment-env names like
|
|
76
|
+
// "local" or "production" must not leave the app in console mode.
|
|
77
|
+
if (!current || !_RUNTIME_MODES.has(current.toLowerCase())) {
|
|
78
|
+
environment["APP_ENV"] = "web";
|
|
79
|
+
}
|
|
80
|
+
} else if (["worker", "queue:work"].includes(normalizedCommand)) {
|
|
81
|
+
if (!current || !_RUNTIME_MODES.has(current.toLowerCase())) {
|
|
82
|
+
environment["APP_ENV"] = "worker";
|
|
83
|
+
}
|
|
84
|
+
} else if (!current || !_RUNTIME_MODES.has(current.toLowerCase())) {
|
|
85
|
+
// Mirror the serve/worker branches: a deployment-env name like "local" or
|
|
86
|
+
// "production" must not leave the app in web mode for a CLI command.
|
|
87
|
+
environment["APP_ENV"] = "console";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Read an environment variable with an optional typed fallback.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* env('APP_NAME', 'Zerotal App') // string
|
|
96
|
+
* env('APP_DEBUG', false) // boolean (coerces 'true'/'false' strings)
|
|
97
|
+
* env('PORT', 3000) // number (coerces numeric strings)
|
|
98
|
+
* env('APP_KEY') // string | undefined — no fallback
|
|
99
|
+
*/
|
|
100
|
+
export function env(key: string): string | undefined;
|
|
101
|
+
export function env(key: string, fallback: string): string;
|
|
102
|
+
export function env(key: string, fallback: boolean): boolean;
|
|
103
|
+
export function env(key: string, fallback: number): number;
|
|
104
|
+
export function env(
|
|
105
|
+
key: string,
|
|
106
|
+
fallback?: string | boolean | number,
|
|
107
|
+
): string | boolean | number | undefined {
|
|
108
|
+
const raw = Bun.env[key];
|
|
109
|
+
|
|
110
|
+
if (raw === undefined) return fallback;
|
|
111
|
+
|
|
112
|
+
// Coerce to match fallback type
|
|
113
|
+
if (typeof fallback === "boolean") {
|
|
114
|
+
return raw === "true" || raw === "1";
|
|
115
|
+
}
|
|
116
|
+
if (typeof fallback === "number") {
|
|
117
|
+
const parsed = Number(raw);
|
|
118
|
+
return Number.isNaN(parsed) ? fallback : parsed;
|
|
119
|
+
}
|
|
120
|
+
return raw;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Read a required environment variable.
|
|
125
|
+
* Throws ConfigError if the variable is not set.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* requireEnv('APP_KEY') // throws at boot if APP_KEY is missing
|
|
129
|
+
*/
|
|
130
|
+
export { Str } from "./str.ts";
|
|
131
|
+
|
|
132
|
+
export function requireEnv(key: string): string {
|
|
133
|
+
const value = Bun.env[key];
|
|
134
|
+
if (!value) {
|
|
135
|
+
throw new ConfigError(`Required environment variable "${key}" is not set.`);
|
|
136
|
+
}
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── tap() / tapAsync() ────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Pass a value to a callback and return the original value.
|
|
144
|
+
* Great for side-effects (logging, events) in the middle of a chain.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* return tap(await User.create(data), (user) => Events.emit(new UserRegistered(user.id)));
|
|
148
|
+
*/
|
|
149
|
+
export function tap<T>(value: T, callback: (val: T) => void): T {
|
|
150
|
+
callback(value);
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Async version of tap. Awaits the callback then returns the original value.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* return await tapAsync(await User.create(data), async (user) => {
|
|
159
|
+
* await Notification.send(user, new WelcomeEmail());
|
|
160
|
+
* });
|
|
161
|
+
*/
|
|
162
|
+
export async function tapAsync<T>(value: T, callback: (val: T) => Promise<void>): Promise<T> {
|
|
163
|
+
await callback(value);
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── pipe() / pipeAsync() ──────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Pass a value to a transformation function and return its result.
|
|
171
|
+
* The sibling of tap — use pipe when the value should change, tap when it shouldn't.
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* const slug = pipe(post.title, (t) => t.toLowerCase().replace(/\s+/g, '-'));
|
|
175
|
+
*/
|
|
176
|
+
export function pipe<T, R>(value: T, fn: (val: T) => R): R {
|
|
177
|
+
return fn(value);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Async version of pipe.
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* const hashed = await pipeAsync(password, (p) => bcrypt.hash(p, 12));
|
|
185
|
+
*/
|
|
186
|
+
export async function pipeAsync<T, R>(value: T, fn: (val: T) => Promise<R>): Promise<R> {
|
|
187
|
+
return fn(value);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── rescue() ──────────────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Execute a callback and return its value. On exception, return `fallback`
|
|
194
|
+
* instead of propagating. Fallback may itself be a function that receives
|
|
195
|
+
* the caught error.
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* const price = await rescue(() => stripe.getPrice(id), 0);
|
|
199
|
+
* const user = await rescue(() => User.findOrFail(id), (e) => { log(e); return null; });
|
|
200
|
+
*/
|
|
201
|
+
export async function rescue<T>(
|
|
202
|
+
callback: () => Promise<T> | T,
|
|
203
|
+
fallback: T | ((error: unknown) => T | Promise<T>),
|
|
204
|
+
): Promise<T> {
|
|
205
|
+
try {
|
|
206
|
+
return await callback();
|
|
207
|
+
} catch (error) {
|
|
208
|
+
return typeof fallback === "function"
|
|
209
|
+
? (fallback as (error: unknown) => T | Promise<T>)(error)
|
|
210
|
+
: fallback;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Synchronous sibling of `rescue` — for code paths that cannot await (JSON
|
|
216
|
+
* parsing, attribute decoding, hot loops). Runs `callback` and returns its
|
|
217
|
+
* value; on exception returns `fallback` (or the result of calling it with the
|
|
218
|
+
* caught error).
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* const payload = rescueSync(() => JSON.parse(raw), {});
|
|
222
|
+
* const cursor = rescueSync(() => JSON.parse(atob(token)), null);
|
|
223
|
+
*/
|
|
224
|
+
export function rescueSync<T>(callback: () => T, fallback: T | ((error: unknown) => T)): T {
|
|
225
|
+
try {
|
|
226
|
+
return callback();
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return typeof fallback === "function" ? (fallback as (error: unknown) => T)(error) : fallback;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── data_get() ────────────────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Safely read a deeply nested value using dot-notation.
|
|
236
|
+
* Returns `defaultValue` (default `undefined`) if any segment is absent.
|
|
237
|
+
*
|
|
238
|
+
* Designed for untyped JSON payloads (webhooks, external API responses) where
|
|
239
|
+
* optional chaining would be excessively verbose.
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* data_get(payload, 'user.address.city') // 'Cape Town' or undefined
|
|
243
|
+
* data_get(payload, 'items.0.price', 0) // first item's price or 0
|
|
244
|
+
*/
|
|
245
|
+
export function data_get(target: unknown, key: string, defaultValue?: unknown): unknown {
|
|
246
|
+
if (target === undefined || target === null) return defaultValue;
|
|
247
|
+
const keys = key.split(".");
|
|
248
|
+
let current = target as Record<string, unknown>;
|
|
249
|
+
for (const segment of keys) {
|
|
250
|
+
if (current === undefined || current === null) return defaultValue;
|
|
251
|
+
current = (current as Record<string, unknown>)[segment] as Record<string, unknown>;
|
|
252
|
+
}
|
|
253
|
+
return current !== undefined ? current : defaultValue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export { markdownPage, type BunMarkdownOptions } from "./markdown.ts";
|
|
257
|
+
|
|
258
|
+
// ── Shared framework helpers — canonical home ────────────────────────────────
|
|
259
|
+
// The one HTML escaper (both JSX runtimes render through it), the English
|
|
260
|
+
// inflector the convention layer builds table names with, deep config merging,
|
|
261
|
+
// and request-cookie parsing. Import from `@zerotal/core/helpers`; packages
|
|
262
|
+
// must not carry private copies.
|
|
263
|
+
export { escapeHtml } from "./html.ts";
|
|
264
|
+
export { pluralize, singularize, tableNameFor } from "../support/str.ts";
|
|
265
|
+
export { deepMerge } from "../support/deepMerge.ts";
|
|
266
|
+
export { parseCookieHeader } from "../support/cookie.ts";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global DI helpers — `make()` resolves from the container, `app()` returns the
|
|
3
|
+
* running application kernel. Thin wrappers over the {@link App} facade so app
|
|
4
|
+
* code can resolve dependencies without importing `Application` directly.
|
|
5
|
+
*/
|
|
6
|
+
import type { Application } from "../application/Application.ts";
|
|
7
|
+
import { currentApp } from "../application/currentApp.ts";
|
|
8
|
+
import { App } from "../facade/facades/App.ts";
|
|
9
|
+
import type { BindingToken } from "../container/types.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolve a binding — or auto-wire a class via its `@inject()`
|
|
13
|
+
* metadata — from the application container.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const users = await make(UsersService);
|
|
17
|
+
* const events = await make("events");
|
|
18
|
+
*/
|
|
19
|
+
export function make<T>(token: BindingToken<T>): Promise<T> {
|
|
20
|
+
return App.make(token);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Return the running {@link Application} kernel, or — when passed a token —
|
|
25
|
+
* resolve it from the container (`app(Token)`).
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* app().bind((c) => c.singleton(Clock, () => new SystemClock())); // kernel
|
|
29
|
+
* const users = await app(UsersService); // resolve
|
|
30
|
+
*/
|
|
31
|
+
export function app(): Application;
|
|
32
|
+
export function app<T>(token: BindingToken<T>): Promise<T>;
|
|
33
|
+
export function app<T>(token?: BindingToken<T>): Application | Promise<T> {
|
|
34
|
+
return token === undefined ? currentApp() : App.make(token);
|
|
35
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown rendering helpers: title extraction, a minimal HTML page shell, and
|
|
3
|
+
* the default `Bun.markdown` parser options the framework renders with.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Pull the first H1/H2 from markdown text to use as the page title. */
|
|
7
|
+
export function markdownExtractTitle(content: string): string | undefined {
|
|
8
|
+
return content.match(/^#{1,2}\s+(.+)$/m)?.[1]?.trim();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Minimal HTML shell for rendered markdown pages. */
|
|
12
|
+
export function markdownPage(title: string, body: string): string {
|
|
13
|
+
return `<html lang="en">
|
|
14
|
+
<head>
|
|
15
|
+
<meta charset="utf-8">
|
|
16
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
17
|
+
<title>${title}</title>
|
|
18
|
+
<style>
|
|
19
|
+
*,*::before,*::after{box-sizing:border-box}
|
|
20
|
+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,sans-serif;
|
|
21
|
+
line-height:1.7;color:#1a1a1a;max-width:860px;margin:0 auto;padding:2rem 1.5rem}
|
|
22
|
+
h1,h2,h3,h4{margin-top:2rem;margin-bottom:.5rem;line-height:1.3}
|
|
23
|
+
h1{font-size:2rem;border-bottom:2px solid #e5e7eb;padding-bottom:.5rem}
|
|
24
|
+
h2{font-size:1.5rem;border-bottom:1px solid #e5e7eb;padding-bottom:.3rem}
|
|
25
|
+
a{color:#2563eb}a:hover{color:#1d4ed8}
|
|
26
|
+
code{background:#f3f4f6;border-radius:4px;padding:.15em .35em;font-size:.9em}
|
|
27
|
+
pre{background:#f3f4f6;border-radius:6px;padding:1rem;overflow-x:auto;line-height:1.5}
|
|
28
|
+
pre code{background:none;padding:0;font-size:inherit}
|
|
29
|
+
blockquote{border-left:4px solid #d1d5db;margin:0;padding:.5rem 1rem;color:#6b7280}
|
|
30
|
+
table{border-collapse:collapse;width:100%;margin:1rem 0}
|
|
31
|
+
th,td{border:1px solid #d1d5db;padding:.5rem .75rem;text-align:left}
|
|
32
|
+
th{background:#f9fafb;font-weight:600}
|
|
33
|
+
img{max-width:100%;height:auto}
|
|
34
|
+
input[type=checkbox]{margin-right:.4em}
|
|
35
|
+
hr{border:none;border-top:1px solid #e5e7eb;margin:2rem 0}
|
|
36
|
+
</style>
|
|
37
|
+
</head>
|
|
38
|
+
<body>
|
|
39
|
+
${body}
|
|
40
|
+
</body>
|
|
41
|
+
</html>`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Options for `Bun.markdown.html()`. bun-types exposes the options type only inside
|
|
46
|
+
* its `markdown` namespace; this is the named, exported equivalent the framework uses.
|
|
47
|
+
*/
|
|
48
|
+
export interface BunMarkdownOptions {
|
|
49
|
+
tables?: boolean;
|
|
50
|
+
strikethrough?: boolean;
|
|
51
|
+
tasklists?: boolean;
|
|
52
|
+
autolinks?: boolean | { url?: boolean; www?: boolean; email?: boolean };
|
|
53
|
+
headings?: boolean | { ids?: boolean };
|
|
54
|
+
hardSoftBreaks?: boolean;
|
|
55
|
+
wikiLinks?: boolean;
|
|
56
|
+
underline?: boolean;
|
|
57
|
+
latexMath?: boolean;
|
|
58
|
+
collapseWhitespace?: boolean;
|
|
59
|
+
permissiveAtxHeaders?: boolean;
|
|
60
|
+
noIndentedCodeBlocks?: boolean;
|
|
61
|
+
noHtmlBlocks?: boolean;
|
|
62
|
+
noHtmlSpans?: boolean;
|
|
63
|
+
tagFilter?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Default markdown parser options — GFM extensions enabled. */
|
|
67
|
+
export const DEFAULT_MD_OPTIONS: BunMarkdownOptions = {
|
|
68
|
+
tables: true,
|
|
69
|
+
strikethrough: true,
|
|
70
|
+
tasklists: true,
|
|
71
|
+
autolinks: true,
|
|
72
|
+
headings: { ids: true },
|
|
73
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page-number window for rendering a numbered pager, with `'...'` gaps —
|
|
3
|
+
* e.g. `[1, '...', 4, 5, 6, '...', 20]`. The first and last pages are always
|
|
4
|
+
* included. Shared by every paginator result (`Model.paginate()`, the in-memory
|
|
5
|
+
* helper) so a pager renders identically whatever produced the page.
|
|
6
|
+
*
|
|
7
|
+
* @param current - The current 1-based page.
|
|
8
|
+
* @param last - The last page number.
|
|
9
|
+
* @param each - How many page links to show on each side of the current page (default `1`).
|
|
10
|
+
* @returns Page numbers interleaved with `'...'` for elided ranges.
|
|
11
|
+
*/
|
|
12
|
+
export function pageElements(current: number, last: number, each = 1): (number | "...")[] {
|
|
13
|
+
if (last <= 1) return [1];
|
|
14
|
+
each = Math.max(0, each);
|
|
15
|
+
const wanted = new Set<number>([1, last]);
|
|
16
|
+
for (let p = current - each; p <= current + each; p++) {
|
|
17
|
+
if (p >= 1 && p <= last) wanted.add(p);
|
|
18
|
+
}
|
|
19
|
+
const out: (number | "...")[] = [];
|
|
20
|
+
let prev = 0;
|
|
21
|
+
for (const p of [...wanted].sort((a, b) => a - b)) {
|
|
22
|
+
if (prev && p - prev > 1) out.push("...");
|
|
23
|
+
out.push(p);
|
|
24
|
+
prev = p;
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The global `request()` helper — returns the active HTTP context, or reads a
|
|
3
|
+
* single merged input value from it (route params, body, then query string).
|
|
4
|
+
*/
|
|
5
|
+
import { RequestContext } from "../context/RequestContext.ts";
|
|
6
|
+
import type { HttpContext } from "../pipeline/HttpContext.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Retrieves the active HTTP request context from the async execution fiber.
|
|
10
|
+
*
|
|
11
|
+
* @returns {HttpContext} The current request context.
|
|
12
|
+
* @throws {Error} If called outside of an active HTTP request lifecycle.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // Access the full context
|
|
16
|
+
* const ctx = request();
|
|
17
|
+
* const token = ctx.bearerToken();
|
|
18
|
+
*/
|
|
19
|
+
export function request(): HttpContext;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Retrieves a deeply merged input value from the current request.
|
|
23
|
+
*
|
|
24
|
+
* **Resolution Priority:**
|
|
25
|
+
* 1. Route parameters (e.g., `ctx.params['id']`)
|
|
26
|
+
* 2. Parsed body data (e.g., JSON or FormData)
|
|
27
|
+
* 3. Query string parameters (e.g., `?page=2`)
|
|
28
|
+
*
|
|
29
|
+
* ⚠️ **Note:** Body resolution is synchronous. It will only find body data if it
|
|
30
|
+
* has already been parsed and cached (e.g., via a `FormRequest` or `await ctx.body()`).
|
|
31
|
+
*
|
|
32
|
+
* @template T - The expected return type of the input value.
|
|
33
|
+
* @param {string} key - The name of the input field to resolve.
|
|
34
|
+
* @param {T} [fallback] - An optional default value to return if the key is missing.
|
|
35
|
+
* @returns {T | undefined} The resolved input value, the fallback, or undefined.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* // Get a value with a fallback
|
|
39
|
+
* const page = request('page', '1');
|
|
40
|
+
*
|
|
41
|
+
* // Get a typed value
|
|
42
|
+
* const id = request<number>('id');
|
|
43
|
+
*/
|
|
44
|
+
export function request<T = string>(key: string, fallback?: T): T | undefined;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Implementation of the global request helper.
|
|
48
|
+
*/
|
|
49
|
+
export function request<T = string>(key?: string, fallback?: T): HttpContext | T | undefined {
|
|
50
|
+
const ctx = RequestContext.get();
|
|
51
|
+
|
|
52
|
+
if (!ctx)
|
|
53
|
+
throw new Error(
|
|
54
|
+
"The request() helper can only be called within an active HTTP request context.",
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (key === undefined) {
|
|
58
|
+
return ctx;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return ctx.input<T>(key, fallback);
|
|
62
|
+
}
|