@cfast/core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daniel Schmidt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,510 @@
1
+ # @cfast/core
2
+
3
+ **The app object that wires @cfast/\* packages together.**
4
+
5
+ `@cfast/core` is the optional composition layer for the cfast framework. It provides `createApp()` — a single definition that connects env, auth, db, storage, and any other plugins into a typed per-request context on the server and a unified provider tree on the client.
6
+
7
+ Individual packages remain fully usable on their own. Core is for when you want them to work together without wiring boilerplate in every route.
8
+
9
+ ## Why This Exists
10
+
11
+ Without core, every route loader in a cfast app repeats the same initialization:
12
+
13
+ ```typescript
14
+ export async function loader({ request, context }: Route.LoaderArgs) {
15
+ const env = context.cloudflare.env;
16
+ const ctx = await requireAuthContext(request);
17
+ const db = createCfDb(env.DB, ctx);
18
+ // NOW you can do your thing
19
+ }
20
+ ```
21
+
22
+ Three lines of setup, duplicated across every route. On the client side, there's no unified provider tree — each package that needs a React context requires separate setup.
23
+
24
+ `@cfast/core` eliminates this by running a plugin chain once per request (server) and composing providers automatically (client).
25
+
26
+ ## Design Decisions and Their Rationale
27
+
28
+ ### Why a plugin system instead of a config object?
29
+
30
+ We considered three approaches:
31
+
32
+ 1. **Declarative config** — `createApp({ auth: authConfig, db: dbConfig })` where core knows about all packages. Simple DX, but core must depend on every package and changes require new core releases.
33
+ 2. **Plugin registration** — `createApp().use(authPlugin()).use(dbPlugin())` where packages own their integration logic. Core stays thin and decoupled.
34
+ 3. **Callback composition** — `createApp({ getContext: async (req) => { ... } })` where the user writes the wiring function. Maximum flexibility, but doesn't eliminate boilerplate — just moves it.
35
+
36
+ We chose **plugins** because cfast's identity is "composable libraries." Core shouldn't be a monolithic hub that knows about every package. Each package can ship its own plugin, and third-party packages can integrate without waiting for core releases.
37
+
38
+ ### Why namespaced context instead of flat merging?
39
+
40
+ Each plugin's `setup()` return is nested under its `name` key:
41
+
42
+ ```typescript
43
+ // ctx.auth.user — not ctx.user
44
+ // ctx.db.client — not ctx.db
45
+ ```
46
+
47
+ This prevents plugins from silently overriding each other's values. Two plugins that both return `{ client }` would collide in a flat merge — with namespacing, they're `ctx.foo.client` and `ctx.bar.client`. Core throws at startup if two plugins share a `name`.
48
+
49
+ ### Why are plugins ordered and not dependency-sorted?
50
+
51
+ Plugins run in registration order, not topologically sorted by their `requires` declarations. This is simpler to reason about — you read the `.use()` chain top to bottom and know exactly what runs when. Core validates at startup that each plugin's requirements are met by prior plugins, and throws with a clear error if not.
52
+
53
+ The alternative (automatic sorting) would make the execution order implicit and harder to debug when something goes wrong.
54
+
55
+ ### Why `requires` as a generic parameter?
56
+
57
+ Plugin dependencies are declared via a TypeScript generic on `definePlugin<TRequires>()`. Earlier designs used an `as` cast on a `requires` property (`requires: {} as { auth: {...} }`), but a generic parameter is cleaner — no phantom runtime values, and the intent is unambiguous.
58
+
59
+ Because TypeScript cannot partially infer generic parameters in a single call, dependent plugins use a **curried form** — you specify `TRequires` explicitly and let the compiler infer the rest:
60
+
61
+ ```typescript
62
+ import type { AuthPluginProvides } from '@cfast/auth';
63
+ definePlugin<AuthPluginProvides>()({ name: 'db', setup(ctx) { ... } })
64
+ // ^^ curried call
65
+ ```
66
+
67
+ This creates a compile-time contract: if `authPlugin` renames a field, dependent plugins break at the type level.
68
+
69
+ ---
70
+
71
+ ## Setup
72
+
73
+ ```typescript
74
+ // app/cfast.ts
75
+ import { createApp } from '@cfast/core';
76
+ import { authPlugin } from '@cfast/auth';
77
+ import { dbPlugin } from '@cfast/db';
78
+ import { storagePlugin } from '@cfast/storage';
79
+ import { envSchema } from './env';
80
+ import { permissions } from './permissions';
81
+
82
+ export const app = createApp({ env: envSchema, permissions })
83
+ .use(authPlugin({
84
+ magicLink: { sendMagicLink: async ({ email, url }) => { /* ... */ } },
85
+ session: { expiresIn: '30d' },
86
+ defaultRoles: ['reader'],
87
+ }))
88
+ .use(dbPlugin({ schema }))
89
+ .use(storagePlugin(storageSchema));
90
+ ```
91
+
92
+ `createApp` takes the two leaf packages (`env` and `permissions`) as direct config because every cfast app needs them and they have no dependencies. Everything else is a plugin.
93
+
94
+ ## Server API
95
+
96
+ ### `app.init(rawEnv)`
97
+
98
+ Call once in the Workers entry point. Validates env bindings (delegates to `@cfast/env`).
99
+
100
+ ```typescript
101
+ // workers/app.ts
102
+ import { app } from '~/cfast';
103
+ import { requestHandler } from 'react-router';
104
+
105
+ export default {
106
+ async fetch(request: Request, rawEnv: Record<string, unknown>, ctx: ExecutionContext) {
107
+ app.init(rawEnv);
108
+ return requestHandler(request, {
109
+ cloudflare: { env: app.env(), ctx },
110
+ });
111
+ },
112
+ };
113
+ ```
114
+
115
+ ### `app.env()`
116
+
117
+ Returns the typed, validated environment. Same as calling `env.get()` directly, but accessible from the app object.
118
+
119
+ ### `app.permissions`
120
+
121
+ The permissions config passed to `createApp()`, exposed for direct access (e.g., checking grants outside a request context).
122
+
123
+ ### `app.context(request, context)`
124
+
125
+ Builds the per-request context by running each plugin's `setup()` in registration order.
126
+
127
+ ```typescript
128
+ // app/routes/posts.tsx
129
+ import { app } from '~/cfast';
130
+
131
+ export async function loader({ request, context }: Route.LoaderArgs) {
132
+ const ctx = await app.context(request, context);
133
+ return ctx.db.client.query(posts).findMany().run({});
134
+ }
135
+ ```
136
+
137
+ The return type is the intersection of all plugin namespaces:
138
+
139
+ ```typescript
140
+ // With authPlugin + dbPlugin + storagePlugin:
141
+ type AppContext = {
142
+ env: ParsedEnv<typeof envSchema>;
143
+ auth: { user: AuthUser | null; grants: Grant[]; instance: AuthInstance };
144
+ db: { client: Db };
145
+ storage: { handle: HandleFn; getSignedUrl: SignedUrlFn; /* ... */ };
146
+ };
147
+ ```
148
+
149
+ **Each plugin's `setup()` receives everything prior plugins have provided**, plus `request` and `env`:
150
+
151
+ 1. `authPlugin.setup({ request, env })` → returns `{ user, grants, instance }`
152
+ 2. `dbPlugin.setup({ request, env, auth: { user, grants, instance } })` → returns `{ client }`
153
+ 3. `storagePlugin.setup({ request, env, auth: {...}, db: {...} })` → returns `{ handle, ... }`
154
+
155
+ If a plugin's `setup()` throws, the error is wrapped with the plugin name:
156
+
157
+ ```
158
+ CfastPluginError: Plugin "db" setup failed: D1 binding not found
159
+ Caused by: Error: D1 binding not found
160
+ ```
161
+
162
+ ### `app.loader(loaderFn)` and `app.action(actionFn)`
163
+
164
+ Optional convenience wrappers that call `app.context()` and pass the result as the first argument:
165
+
166
+ ```typescript
167
+ export const loader = app.loader(async (ctx, { params }) => {
168
+ return ctx.db.client.query(posts).findMany().run({});
169
+ });
170
+ ```
171
+
172
+ These are thin sugar — `app.context()` is always available for cases where you need more control (e.g., routes that don't need the full context, or actions already handled by `@cfast/actions`).
173
+
174
+ ### Integration with `@cfast/actions`
175
+
176
+ `@cfast/actions` has its own context mechanism via `createActions({ getContext })`. With core, the wiring becomes a one-liner:
177
+
178
+ ```typescript
179
+ // app/actions.server.ts
180
+ import { createActions } from '@cfast/actions';
181
+ import { app } from '~/cfast';
182
+
183
+ export const { createAction, composeActions } = createActions({
184
+ getContext: async ({ request, context }) => {
185
+ const ctx = await app.context(request, context);
186
+ return { db: ctx.db.client, user: ctx.auth.user, grants: ctx.auth.grants };
187
+ },
188
+ });
189
+ ```
190
+
191
+ Core doesn't wrap or replace `@cfast/actions` — actions own their execution semantics.
192
+
193
+ ---
194
+
195
+ ## Client API
196
+
197
+ ### `<app.Provider>`
198
+
199
+ Composes all plugin providers into a single tree. Plugins without a `Provider` are skipped.
200
+
201
+ ```typescript
202
+ // app/root.tsx
203
+ import { app } from '~/cfast';
204
+
205
+ export function Layout({ children }: { children: React.ReactNode }) {
206
+ return (
207
+ <html>
208
+ <body>
209
+ <app.Provider>
210
+ {children}
211
+ </app.Provider>
212
+ </body>
213
+ </html>
214
+ );
215
+ }
216
+ ```
217
+
218
+ The rendered tree nests providers in registration order:
219
+
220
+ ```tsx
221
+ <CoreContext.Provider value={clientValues}>
222
+ <AuthProvider> {/* from authPlugin */}
223
+ <StorageProvider> {/* from storagePlugin */}
224
+ {children}
225
+ </StorageProvider>
226
+ </AuthProvider>
227
+ </CoreContext.Provider>
228
+ ```
229
+
230
+ ### `useApp()`
231
+
232
+ Typed access to all plugins' client-side exports:
233
+
234
+ ```typescript
235
+ import { useApp } from '@cfast/core/client';
236
+
237
+ function MyComponent() {
238
+ const { auth, storage } = useApp();
239
+ const user = auth.useCurrentUser();
240
+ const upload = storage.useUpload('avatar');
241
+ }
242
+ ```
243
+
244
+ The type reflects only plugins that declared a `client` export. Plugins without client exports don't appear.
245
+
246
+ Individual package hooks (`useCurrentUser()`, `useUpload()`) continue to work directly — `useApp()` is additive, not a replacement.
247
+
248
+ ---
249
+
250
+ ## Plugin API
251
+
252
+ ### `definePlugin(config)` / `definePlugin<TRequires>()(config)`
253
+
254
+ Creates a plugin. This is the API package authors use.
255
+
256
+ The direct form (no dependencies) infers all type parameters:
257
+
258
+ ```typescript
259
+ import { definePlugin } from '@cfast/core';
260
+
261
+ export const myPlugin = (config: MyConfig) =>
262
+ definePlugin({
263
+ name: 'my-plugin',
264
+ setup(ctx) {
265
+ // ctx is { request, env } when no dependencies
266
+ return { /* values exposed as ctx['my-plugin'] */ };
267
+ },
268
+ });
269
+ ```
270
+
271
+ The curried form (with dependencies) lets you specify `TRequires` while inferring the rest:
272
+
273
+ ```typescript
274
+ definePlugin<AuthPluginProvides>()({
275
+ name: 'db',
276
+ setup(ctx) {
277
+ ctx.auth.user // typed from TRequires
278
+ return { client };
279
+ },
280
+ });
281
+ ```
282
+
283
+ ### Plugin config
284
+
285
+ | Field | Type | Required | Description |
286
+ |---|---|---|---|
287
+ | `name` | `string` | Yes | Unique identifier. Used as the namespace key in context. |
288
+ | `setup` | `(ctx) => TProvides \| Promise<TProvides>` | Yes | Called per-request. Receives `{ request, env }` plus all prior plugin namespaces. Returns the values this plugin provides. |
289
+ | `Provider` | `React.ComponentType<{ children: ReactNode }>` | No | Client-side React provider. Composed into `app.Provider`. |
290
+ | `client` | `Record<string, unknown>` | No | Client-side values exposed via `useApp()`. |
291
+
292
+ ### Declaring dependencies
293
+
294
+ Import the type token from the package you depend on:
295
+
296
+ ```typescript
297
+ import { definePlugin } from '@cfast/core';
298
+ import type { AuthPluginProvides } from '@cfast/auth';
299
+
300
+ export const dbPlugin = (config: DbPluginConfig) =>
301
+ definePlugin<AuthPluginProvides>()({
302
+ name: 'db',
303
+ setup(ctx) {
304
+ // ctx.auth.user and ctx.auth.grants are typed
305
+ const client = createDb({
306
+ d1: ctx.env.DB,
307
+ schema: config.schema,
308
+ grants: ctx.auth.grants,
309
+ user: ctx.auth.user,
310
+ });
311
+ return { client };
312
+ },
313
+ });
314
+ ```
315
+
316
+ Each package that ships a plugin should export a `PluginProvides` type:
317
+
318
+ ```typescript
319
+ // @cfast/auth exports:
320
+ export type AuthPluginProvides = PluginProvides<typeof authPlugin>;
321
+ // Resolves to: { auth: { user: AuthUser | null; grants: Grant[]; instance: AuthInstance } }
322
+ ```
323
+
324
+ `PluginProvides<T>` is a utility type exported by `@cfast/core` that extracts `{ [name]: ReturnType<setup> }` from any plugin definition.
325
+
326
+ ### Multiple dependencies
327
+
328
+ Intersect the type tokens:
329
+
330
+ ```typescript
331
+ import type { AuthPluginProvides } from '@cfast/auth';
332
+ import type { DbPluginProvides } from '@cfast/db';
333
+
334
+ export const adminPlugin = (config: AdminConfig) =>
335
+ definePlugin<AuthPluginProvides & DbPluginProvides>()({
336
+ name: 'admin',
337
+ setup(ctx) {
338
+ ctx.auth.user // typed
339
+ ctx.db.client // typed
340
+ return { /* ... */ };
341
+ },
342
+ });
343
+ ```
344
+
345
+ ### No dependencies (leaf plugins)
346
+
347
+ Omit the generic — `TRequires` defaults to `{}`:
348
+
349
+ ```typescript
350
+ export const analyticsPlugin = (config: AnalyticsConfig) =>
351
+ definePlugin({
352
+ name: 'analytics',
353
+ setup(ctx) {
354
+ // ctx has { request, env } only
355
+ return { track: (event: string) => { /* ... */ } };
356
+ },
357
+ });
358
+ ```
359
+
360
+ ### Client-only plugins
361
+
362
+ Plugins that only provide client-side functionality can return `{}` from `setup`:
363
+
364
+ ```typescript
365
+ export const themePlugin = (config: ThemeConfig) =>
366
+ definePlugin({
367
+ name: 'theme',
368
+ setup() {
369
+ return {};
370
+ },
371
+ Provider({ children }) {
372
+ return <ThemeProvider theme={config.theme}>{children}</ThemeProvider>;
373
+ },
374
+ client: {
375
+ useTheme: () => useContext(ThemeContext),
376
+ },
377
+ });
378
+ ```
379
+
380
+ ---
381
+
382
+ ## Writing a Plugin: Complete Example
383
+
384
+ A rate-limiting plugin that uses KV to track request counts:
385
+
386
+ ```typescript
387
+ // packages/rate-limit/src/plugin.ts
388
+ import { definePlugin, type PluginProvides } from '@cfast/core';
389
+ import type { AuthPluginProvides } from '@cfast/auth';
390
+
391
+ export type RateLimitConfig = {
392
+ kv: string; // env binding name for KV namespace
393
+ maxRequests: number; // per window
394
+ windowMs: number; // window size in milliseconds
395
+ };
396
+
397
+ export const rateLimitPlugin = (config: RateLimitConfig) =>
398
+ definePlugin<AuthPluginProvides>()({
399
+ name: 'rate-limit',
400
+
401
+ async setup(ctx) {
402
+ const kv = ctx.env[config.kv] as KVNamespace;
403
+ const key = ctx.auth.user?.id ?? ctx.request.headers.get('cf-connecting-ip') ?? 'unknown';
404
+ const windowKey = `rl:${key}:${Math.floor(Date.now() / config.windowMs)}`;
405
+
406
+ const current = parseInt(await kv.get(windowKey) ?? '0', 10);
407
+ const remaining = Math.max(0, config.maxRequests - current);
408
+ const limited = current >= config.maxRequests;
409
+
410
+ return {
411
+ limited,
412
+ remaining,
413
+ async consume() {
414
+ await kv.put(windowKey, String(current + 1), {
415
+ expirationTtl: Math.ceil(config.windowMs / 1000),
416
+ });
417
+ },
418
+ };
419
+ },
420
+ });
421
+
422
+ // Type token for dependents
423
+ export type RateLimitPluginProvides = PluginProvides<ReturnType<typeof rateLimitPlugin>>;
424
+ ```
425
+
426
+ Usage:
427
+
428
+ ```typescript
429
+ // app/cfast.ts
430
+ import { rateLimitPlugin } from '@cfast/rate-limit';
431
+
432
+ export const app = createApp({ env: envSchema, permissions })
433
+ .use(authPlugin({ /* ... */ }))
434
+ .use(rateLimitPlugin({ kv: 'RATE_LIMIT', maxRequests: 100, windowMs: 60_000 }))
435
+ .use(dbPlugin({ schema }));
436
+
437
+ // app/routes/api.tsx
438
+ export async function loader({ request, context }: Route.LoaderArgs) {
439
+ const ctx = await app.context(request, context);
440
+
441
+ if (ctx['rate-limit'].limited) {
442
+ throw new Response('Too many requests', { status: 429 });
443
+ }
444
+ await ctx['rate-limit'].consume();
445
+
446
+ return ctx.db.client.query(posts).findMany().run({});
447
+ }
448
+ ```
449
+
450
+ ---
451
+
452
+ ## Startup Validation
453
+
454
+ `createApp().use()` performs validation eagerly (at import time, not per-request):
455
+
456
+ 1. **Duplicate names** — If two plugins share a `name`, `.use()` throws a `CfastConfigError` immediately.
457
+ 2. **Missing requirements** — If a plugin's `TRequires` includes keys not provided by prior plugins, TypeScript reports a type error at the `.use()` call site. This is compile-time only — there is no runtime check for missing requirements.
458
+
459
+ If plugin `setup()` throws during `app.context()`, the error is wrapped in a `CfastPluginError` with the plugin name for diagnostics.
460
+
461
+ ---
462
+
463
+ ## Exports
464
+
465
+ Server (`@cfast/core`):
466
+
467
+ ```typescript
468
+ export { createApp } from './create-app.js';
469
+ export { definePlugin } from './define-plugin.js';
470
+ export { CfastPluginError, CfastConfigError } from './errors.js';
471
+ export type {
472
+ CfastPlugin, CreateAppConfig, PluginSetupContext,
473
+ AppContext, PluginProvides, App, RouteArgs,
474
+ } from './types.js';
475
+ ```
476
+
477
+ Client (`@cfast/core/client`):
478
+
479
+ ```typescript
480
+ export { useApp } from './client/use-app.js';
481
+ export { createCoreProvider, CoreContext } from './client/provider.js';
482
+ ```
483
+
484
+ ---
485
+
486
+ ## Integration with Other @cfast Packages
487
+
488
+ - **`@cfast/env`** — Core accepts the env schema directly and delegates to `defineEnv` internally. `app.init()` calls `env.init()`, `app.env()` calls `env.get()`.
489
+ - **`@cfast/permissions`** — Core accepts the permissions config directly and makes it available to all plugins via `ctx.env` (the env binding) and the base context.
490
+ - **`@cfast/auth`**, **`@cfast/db`**, **`@cfast/storage`**, etc. — Each ships an optional plugin export alongside their standalone API. The plugin wraps their existing factory function and registers it with core.
491
+ - **`@cfast/actions`** — Not wrapped by core. Actions have their own context mechanism. Core's `app.context()` feeds into `createActions({ getContext })`.
492
+ - **`@cfast/admin`** — Can ship an `adminPlugin` that depends on auth + db + forms plugins.
493
+
494
+ ---
495
+
496
+ ## Known Limitations
497
+
498
+ ### 1. Plugin setup runs on every request
499
+
500
+ There is no caching of plugin `setup()` results across requests. Each call to `app.context()` runs the full plugin chain. This is intentional — most setup work is cheap (object construction, cookie parsing) and the per-request user context makes caching unsafe.
501
+
502
+ If a plugin has expensive one-time initialization, it should do that work in the plugin factory (the outer function) rather than in `setup()`.
503
+
504
+ ### 2. Provider ordering matches registration order
505
+
506
+ Client-side providers nest in the order plugins were registered. If a provider needs to wrap another provider (e.g., auth must wrap storage for token access), the plugins must be registered in the correct order. This is the same constraint as the server-side `setup()` chain.
507
+
508
+ ### 3. `useApp()` requires `app.Provider` in the tree
509
+
510
+ Calling `useApp()` outside of `app.Provider` throws a context error. Individual package hooks may or may not have this requirement depending on their implementation.
@@ -0,0 +1,31 @@
1
+ // src/client/provider.tsx
2
+ import { createContext } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ var CoreContext = createContext(null);
5
+ function createCoreProvider(plugins) {
6
+ const clientValue = {};
7
+ for (const plugin of plugins) {
8
+ if (plugin.client) {
9
+ clientValue[plugin.name] = plugin.client;
10
+ }
11
+ }
12
+ const providers = [];
13
+ for (const p of plugins) {
14
+ if (p.Provider) {
15
+ providers.push(p.Provider);
16
+ }
17
+ }
18
+ return function CfastProvider({ children }) {
19
+ let tree = children;
20
+ for (let i = providers.length - 1; i >= 0; i--) {
21
+ const P = providers[i];
22
+ tree = /* @__PURE__ */ jsx(P, { children: tree });
23
+ }
24
+ return /* @__PURE__ */ jsx(CoreContext.Provider, { value: clientValue, children: tree });
25
+ };
26
+ }
27
+
28
+ export {
29
+ CoreContext,
30
+ createCoreProvider
31
+ };
@@ -0,0 +1,44 @@
1
+ import * as react from 'react';
2
+ import { ComponentType, ReactNode } from 'react';
3
+ import { d as RuntimePlugin } from '../types-L3fwnDhg.js';
4
+ import '@cfast/env';
5
+ import '@cfast/permissions';
6
+
7
+ /**
8
+ * Accesses all plugins' client-side exports from the nearest `app.Provider`.
9
+ *
10
+ * The returned object is keyed by plugin name, containing each plugin's `client` values.
11
+ * Throws if called outside of `<app.Provider>`.
12
+ *
13
+ * @typeParam T - The expected shape of the client context (defaults to `Record<string, unknown>`).
14
+ * @returns The merged client-side plugin values.
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * import { useApp } from '@cfast/core/client';
19
+ *
20
+ * function MyComponent() {
21
+ * const { auth, storage } = useApp();
22
+ * const user = auth.useCurrentUser();
23
+ * }
24
+ * ```
25
+ */
26
+ declare function useApp<T = Record<string, unknown>>(): T;
27
+
28
+ /** React context holding client-side plugin values for `useApp()`. */
29
+ declare const CoreContext: react.Context<Record<string, unknown> | null>;
30
+ type ProviderComponent = ComponentType<{
31
+ children: ReactNode;
32
+ }>;
33
+ /**
34
+ * Creates a composed React provider that nests all plugin providers and exposes
35
+ * client-side plugin values via {@link CoreContext}.
36
+ *
37
+ * Plugins are nested in registration order (first registered = outermost).
38
+ *
39
+ * @param plugins - The registered plugins, each with optional `Provider` and `client`.
40
+ * @returns A React component that wraps children with all plugin providers and the core context.
41
+ */
42
+ declare function createCoreProvider(plugins: Pick<RuntimePlugin, "name" | "Provider" | "client">[]): ProviderComponent;
43
+
44
+ export { CoreContext, createCoreProvider, useApp };
@@ -0,0 +1,21 @@
1
+ import {
2
+ CoreContext,
3
+ createCoreProvider
4
+ } from "../chunk-DFUBJVKG.js";
5
+
6
+ // src/client/use-app.ts
7
+ import { useContext } from "react";
8
+ function useApp() {
9
+ const ctx = useContext(CoreContext);
10
+ if (ctx === null) {
11
+ throw new Error(
12
+ "@cfast/core: useApp() must be used inside <app.Provider>. Wrap your app with the Provider from createApp()."
13
+ );
14
+ }
15
+ return ctx;
16
+ }
17
+ export {
18
+ CoreContext,
19
+ createCoreProvider,
20
+ useApp
21
+ };
@@ -0,0 +1,104 @@
1
+ import { Schema } from '@cfast/env';
2
+ import { Permissions } from '@cfast/permissions';
3
+ import { C as CreateAppConfig, A as App, P as PluginSetupContext, a as CfastPlugin } from './types-L3fwnDhg.js';
4
+ export { b as AppContext, c as PluginProvides, R as RouteArgs } from './types-L3fwnDhg.js';
5
+ import { ComponentType, ReactNode } from 'react';
6
+
7
+ /**
8
+ * Creates a cfast application instance that wires env, permissions, and plugins into a typed per-request context.
9
+ *
10
+ * Call `.use(plugin)` to register plugins, then use `app.context(request, context)` in route
11
+ * loaders/actions to build the per-request context with all plugin values.
12
+ *
13
+ * @param config - Application configuration containing the env schema and permissions definition.
14
+ * @returns An `App` instance with methods for context creation, route helpers, and plugin registration.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import { createApp } from '@cfast/core';
19
+ * import { authPlugin } from '@cfast/auth';
20
+ * import { envSchema } from './env';
21
+ * import { permissions } from './permissions';
22
+ *
23
+ * export const app = createApp({ env: envSchema, permissions })
24
+ * .use(authPlugin({ magicLink: { sendMagicLink: async ({ email, url }) => {} } }));
25
+ * ```
26
+ */
27
+ declare function createApp<TSchema extends Schema, TPermissions extends Permissions>(config: CreateAppConfig<TSchema, TPermissions>): App<TSchema, TPermissions, unknown, unknown>;
28
+
29
+ /**
30
+ * Defines a cfast plugin for use with `createApp().use()`.
31
+ *
32
+ * Has two call signatures:
33
+ * - **Direct form** (no dependencies): `definePlugin({ name, setup, ... })` -- types are fully inferred.
34
+ * - **Curried form** (with dependencies): `definePlugin<TRequires>()({ name, setup, ... })` --
35
+ * specify `TRequires` explicitly so `setup(ctx)` receives typed prior-plugin context.
36
+ *
37
+ * @param config - Plugin configuration with `name`, `setup`, and optional `Provider`/`client`.
38
+ * @returns A `CfastPlugin` instance ready to pass to `app.use()`.
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * // Leaf plugin (no dependencies)
43
+ * const analyticsPlugin = definePlugin({
44
+ * name: 'analytics',
45
+ * setup(ctx) {
46
+ * return { track: (event: string) => {} };
47
+ * },
48
+ * });
49
+ *
50
+ * // Plugin with dependencies (curried)
51
+ * import type { AuthPluginProvides } from '@cfast/auth';
52
+ * const dbPlugin = definePlugin<AuthPluginProvides>()({
53
+ * name: 'db',
54
+ * setup(ctx) {
55
+ * ctx.auth.user; // typed from AuthPluginProvides
56
+ * return { client: createDb({}) };
57
+ * },
58
+ * });
59
+ * ```
60
+ */
61
+ declare function definePlugin<TName extends string, TProvides, TClient = unknown>(config: {
62
+ name: TName;
63
+ setup: (ctx: PluginSetupContext<unknown>) => TProvides | Promise<TProvides>;
64
+ Provider?: ComponentType<{
65
+ children: ReactNode;
66
+ }>;
67
+ client?: TClient;
68
+ }): CfastPlugin<TName, Awaited<TProvides>, unknown, TClient>;
69
+ declare function definePlugin<TRequires>(): <TName extends string, TProvides, TClient = unknown>(config: {
70
+ name: TName;
71
+ setup: (ctx: PluginSetupContext<TRequires>) => TProvides | Promise<TProvides>;
72
+ Provider?: ComponentType<{
73
+ children: ReactNode;
74
+ }>;
75
+ client?: TClient;
76
+ }) => CfastPlugin<TName, Awaited<TProvides>, TRequires, TClient>;
77
+
78
+ /**
79
+ * Error thrown when a plugin's `setup()` function fails during `app.context()`.
80
+ *
81
+ * Wraps the original error with the plugin name for diagnostics.
82
+ */
83
+ declare class CfastPluginError extends Error {
84
+ /** The name of the plugin whose `setup()` threw. */
85
+ readonly pluginName: string;
86
+ /** The original error thrown by the plugin. */
87
+ readonly cause: unknown;
88
+ /**
89
+ * @param pluginName - The name of the plugin that failed.
90
+ * @param cause - The original error thrown by the plugin's `setup()`.
91
+ */
92
+ constructor(pluginName: string, cause: unknown);
93
+ }
94
+ /**
95
+ * Error thrown for configuration issues detected at startup (e.g., duplicate plugin names).
96
+ */
97
+ declare class CfastConfigError extends Error {
98
+ /**
99
+ * @param message - Description of the configuration error.
100
+ */
101
+ constructor(message: string);
102
+ }
103
+
104
+ export { App, CfastConfigError, CfastPlugin, CfastPluginError, CreateAppConfig, PluginSetupContext, createApp, definePlugin };
package/dist/index.js ADDED
@@ -0,0 +1,113 @@
1
+ import {
2
+ createCoreProvider
3
+ } from "./chunk-DFUBJVKG.js";
4
+
5
+ // src/create-app.ts
6
+ import { defineEnv } from "@cfast/env";
7
+
8
+ // src/errors.ts
9
+ var CfastPluginError = class extends Error {
10
+ /** The name of the plugin whose `setup()` threw. */
11
+ pluginName;
12
+ /** The original error thrown by the plugin. */
13
+ cause;
14
+ /**
15
+ * @param pluginName - The name of the plugin that failed.
16
+ * @param cause - The original error thrown by the plugin's `setup()`.
17
+ */
18
+ constructor(pluginName, cause) {
19
+ const causeMessage = cause instanceof Error ? cause.message : String(cause);
20
+ super(`Plugin "${pluginName}" setup failed: ${causeMessage}`);
21
+ this.name = "CfastPluginError";
22
+ this.pluginName = pluginName;
23
+ this.cause = cause;
24
+ }
25
+ };
26
+ var CfastConfigError = class extends Error {
27
+ /**
28
+ * @param message - Description of the configuration error.
29
+ */
30
+ constructor(message) {
31
+ super(message);
32
+ this.name = "CfastConfigError";
33
+ }
34
+ };
35
+
36
+ // src/create-app.ts
37
+ function createApp(config) {
38
+ const envInstance = defineEnv(config.env);
39
+ return buildApp(
40
+ envInstance,
41
+ config.permissions,
42
+ []
43
+ );
44
+ }
45
+ function buildApp(envInstance, permissions, plugins) {
46
+ const pluginNames = new Set(plugins.map((p) => p.name));
47
+ const app = {
48
+ permissions,
49
+ init(rawEnv) {
50
+ envInstance.init(rawEnv);
51
+ },
52
+ env() {
53
+ return envInstance.get();
54
+ },
55
+ async context(request, _context) {
56
+ const env = envInstance.get();
57
+ const accumulated = {};
58
+ for (const plugin of plugins) {
59
+ const setupCtx = {
60
+ request,
61
+ env,
62
+ ...accumulated
63
+ };
64
+ try {
65
+ const result = await plugin.setup(
66
+ setupCtx
67
+ );
68
+ accumulated[plugin.name] = result;
69
+ } catch (e) {
70
+ if (e instanceof CfastPluginError) throw e;
71
+ throw new CfastPluginError(plugin.name, e);
72
+ }
73
+ }
74
+ return { env, ...accumulated };
75
+ },
76
+ loader(fn) {
77
+ return async (args) => {
78
+ const ctx = await app.context(args.request, args.context);
79
+ return fn(ctx, args);
80
+ };
81
+ },
82
+ action(fn) {
83
+ return async (args) => {
84
+ const ctx = await app.context(args.request, args.context);
85
+ return fn(ctx, args);
86
+ };
87
+ },
88
+ use(plugin) {
89
+ if (pluginNames.has(plugin.name)) {
90
+ throw new CfastConfigError(
91
+ `Duplicate plugin name "${plugin.name}". Each plugin must have a unique name.`
92
+ );
93
+ }
94
+ return buildApp(envInstance, permissions, [...plugins, plugin]);
95
+ },
96
+ Provider: createCoreProvider(plugins)
97
+ };
98
+ return app;
99
+ }
100
+
101
+ // src/define-plugin.ts
102
+ function definePlugin(config) {
103
+ if (config === void 0) {
104
+ return (innerConfig) => innerConfig;
105
+ }
106
+ return config;
107
+ }
108
+ export {
109
+ CfastConfigError,
110
+ CfastPluginError,
111
+ createApp,
112
+ definePlugin
113
+ };
@@ -0,0 +1,49 @@
1
+ import { Schema, ParsedEnv } from '@cfast/env';
2
+ import { Permissions } from '@cfast/permissions';
3
+ import { ComponentType, ReactNode } from 'react';
4
+
5
+ type CreateAppConfig<TSchema extends Schema, TPermissions extends Permissions> = {
6
+ env: TSchema;
7
+ permissions: TPermissions;
8
+ };
9
+ type PluginSetupContext<TRequires> = {
10
+ request: Request;
11
+ env: Record<string, unknown>;
12
+ } & TRequires;
13
+ type CfastPlugin<TName extends string = string, TProvides = unknown, TRequires = {}, TClient = {}> = {
14
+ name: TName;
15
+ setup: (ctx: PluginSetupContext<TRequires>) => TProvides | Promise<TProvides>;
16
+ Provider?: ComponentType<{
17
+ children: ReactNode;
18
+ }>;
19
+ client?: TClient;
20
+ };
21
+ type PluginProvides<T> = T extends CfastPlugin<infer N, infer P, unknown, unknown> ? {
22
+ [K in N]: P;
23
+ } : never;
24
+ type AppContext<TSchema extends Schema, TPluginContext> = {
25
+ env: ParsedEnv<TSchema>;
26
+ } & TPluginContext;
27
+ type RouteArgs = {
28
+ request: Request;
29
+ params: Record<string, string | undefined>;
30
+ context: unknown;
31
+ };
32
+ type App<TSchema extends Schema, TPermissions extends Permissions, TPluginContext, TClientContext> = {
33
+ init(rawEnv: Record<string, unknown>): void;
34
+ env(): ParsedEnv<TSchema>;
35
+ context(request: Request, context?: unknown): Promise<AppContext<TSchema, TPluginContext>>;
36
+ loader<T>(fn: (ctx: AppContext<TSchema, TPluginContext>, args: RouteArgs) => T | Promise<T>): (args: RouteArgs) => Promise<T>;
37
+ action<T>(fn: (ctx: AppContext<TSchema, TPluginContext>, args: RouteArgs) => T | Promise<T>): (args: RouteArgs) => Promise<T>;
38
+ use<TName extends string, TProvides, TClient>(plugin: CfastPlugin<TName, TProvides, TPluginContext, TClient>): App<TSchema, TPermissions, TPluginContext & {
39
+ [K in TName]: TProvides;
40
+ }, TClientContext & (TClient extends {} ? {
41
+ [K in TName]: TClient;
42
+ } : {})>;
43
+ Provider: ComponentType<{
44
+ children: ReactNode;
45
+ }>;
46
+ permissions: TPermissions;
47
+ };
48
+
49
+ export type { App as A, CreateAppConfig as C, PluginSetupContext as P, RouteArgs as R, CfastPlugin as a, AppContext as b, PluginProvides as c };
@@ -0,0 +1,49 @@
1
+ import { Schema, ParsedEnv } from '@cfast/env';
2
+ import { Permissions } from '@cfast/permissions';
3
+ import { ComponentType, ReactNode } from 'react';
4
+
5
+ type CreateAppConfig<TSchema extends Schema, TPermissions extends Permissions> = {
6
+ env: TSchema;
7
+ permissions: TPermissions;
8
+ };
9
+ type PluginSetupContext<TRequires> = {
10
+ request: Request;
11
+ env: Record<string, unknown>;
12
+ } & TRequires;
13
+ type CfastPlugin<TName extends string = string, TProvides = unknown, TRequires = unknown, TClient = unknown> = {
14
+ name: TName;
15
+ setup: (ctx: PluginSetupContext<TRequires>) => TProvides | Promise<TProvides>;
16
+ Provider?: ComponentType<{
17
+ children: ReactNode;
18
+ }>;
19
+ client?: TClient;
20
+ };
21
+ type PluginProvides<T> = T extends CfastPlugin<infer N, infer P, unknown, unknown> ? {
22
+ [K in N]: P;
23
+ } : never;
24
+ type AppContext<TSchema extends Schema, TPluginContext> = {
25
+ env: ParsedEnv<TSchema>;
26
+ } & TPluginContext;
27
+ type RouteArgs = {
28
+ request: Request;
29
+ params: Record<string, string | undefined>;
30
+ context: unknown;
31
+ };
32
+ type App<TSchema extends Schema, TPermissions extends Permissions, TPluginContext, TClientContext> = {
33
+ init(rawEnv: Record<string, unknown>): void;
34
+ env(): ParsedEnv<TSchema>;
35
+ context(request: Request, context?: unknown): Promise<AppContext<TSchema, TPluginContext>>;
36
+ loader<T>(fn: (ctx: AppContext<TSchema, TPluginContext>, args: RouteArgs) => T | Promise<T>): (args: RouteArgs) => Promise<T>;
37
+ action<T>(fn: (ctx: AppContext<TSchema, TPluginContext>, args: RouteArgs) => T | Promise<T>): (args: RouteArgs) => Promise<T>;
38
+ use<TName extends string, TProvides, TClient>(plugin: CfastPlugin<TName, TProvides, TPluginContext, TClient>): App<TSchema, TPermissions, TPluginContext & {
39
+ [K in TName]: TProvides;
40
+ }, TClientContext & (TClient extends object ? {
41
+ [K in TName]: TClient;
42
+ } : unknown)>;
43
+ Provider: ComponentType<{
44
+ children: ReactNode;
45
+ }>;
46
+ permissions: TPermissions;
47
+ };
48
+
49
+ export type { App as A, CreateAppConfig as C, PluginSetupContext as P, RouteArgs as R, CfastPlugin as a, AppContext as b, PluginProvides as c };
@@ -0,0 +1,123 @@
1
+ import { Schema, ParsedEnv } from '@cfast/env';
2
+ import { Permissions } from '@cfast/permissions';
3
+ import { ComponentType, ReactNode } from 'react';
4
+
5
+ /**
6
+ * Configuration object for {@link createApp}.
7
+ *
8
+ * @typeParam TSchema - The env schema type from `@cfast/env`.
9
+ * @typeParam TPermissions - The permissions definition from `@cfast/permissions`.
10
+ */
11
+ type CreateAppConfig<TSchema extends Schema, TPermissions extends Permissions> = {
12
+ /** The environment variable schema. Validated at `app.init()` time via `@cfast/env`. */
13
+ env: TSchema;
14
+ /** The permissions config from `definePermissions()`. Made available to all plugins. */
15
+ permissions: TPermissions;
16
+ };
17
+ /**
18
+ * The context object passed to a plugin's `setup()` function.
19
+ *
20
+ * Contains the current request, validated env, and all values provided by prior plugins
21
+ * (typed via `TRequires`).
22
+ *
23
+ * @typeParam TRequires - Intersection of prior plugin provides (e.g., `AuthPluginProvides`).
24
+ */
25
+ type PluginSetupContext<TRequires> = {
26
+ /** The incoming HTTP request for the current invocation. */
27
+ request: Request;
28
+ /** The validated environment bindings. */
29
+ env: Record<string, unknown>;
30
+ } & TRequires;
31
+ /**
32
+ * A cfast plugin definition created by {@link definePlugin}.
33
+ *
34
+ * Plugins provide server-side context values via `setup()`, optional client-side React providers,
35
+ * and optional client-side values accessible via `useApp()`.
36
+ *
37
+ * @typeParam TName - The unique plugin name, used as the namespace key in `AppContext`.
38
+ * @typeParam TProvides - The type returned by `setup()`, accessible as `ctx[name]`.
39
+ * @typeParam TRequires - The context shape this plugin depends on from prior plugins.
40
+ * @typeParam TClient - Client-side values exposed via `useApp()`.
41
+ */
42
+ type CfastPlugin<TName extends string = string, TProvides = unknown, TRequires = unknown, TClient = unknown> = {
43
+ /** Unique identifier used as the namespace key in the app context. */
44
+ name: TName;
45
+ /** Called per-request to produce the values this plugin provides. */
46
+ setup: (ctx: PluginSetupContext<TRequires>) => TProvides | Promise<TProvides>;
47
+ /** Optional client-side React provider, composed into `app.Provider`. */
48
+ Provider?: ComponentType<{
49
+ children: ReactNode;
50
+ }>;
51
+ /** Optional client-side values exposed via `useApp()`. */
52
+ client?: TClient;
53
+ };
54
+ /**
55
+ * Utility type that extracts `{ [name]: ReturnType<setup> }` from a plugin definition.
56
+ *
57
+ * Use this to create a type token that dependent plugins can reference via `definePlugin<TRequires>()`.
58
+ *
59
+ * @typeParam T - A `CfastPlugin` type to extract provides from.
60
+ */
61
+ type PluginProvides<T> = T extends CfastPlugin<infer N, infer P, unknown, unknown> ? {
62
+ [K in N]: P;
63
+ } : never;
64
+ /**
65
+ * The accumulated per-request context after all plugins have run.
66
+ *
67
+ * Contains the validated env plus each plugin's namespaced values.
68
+ *
69
+ * @typeParam TSchema - The env schema type.
70
+ * @typeParam TPluginContext - The intersection of all registered plugins' provides.
71
+ */
72
+ type AppContext<TSchema extends Schema, TPluginContext> = {
73
+ /** The validated environment bindings. */
74
+ env: ParsedEnv<TSchema>;
75
+ } & TPluginContext;
76
+ /**
77
+ * Route handler arguments passed through from React Router loaders and actions.
78
+ */
79
+ type RouteArgs = {
80
+ /** The incoming HTTP request. */
81
+ request: Request;
82
+ /** URL route parameters (e.g., `{ postId: "abc" }`). */
83
+ params: Record<string, string | undefined>;
84
+ /** The React Router context object (contains `cloudflare.env`, etc.). */
85
+ context: unknown;
86
+ };
87
+ /**
88
+ * The app object returned by `createApp()` and extended by `.use()` calls.
89
+ *
90
+ * Provides methods for environment initialization, per-request context creation,
91
+ * route handler wrappers, plugin registration, and a composed React provider.
92
+ *
93
+ * @typeParam TSchema - The env schema type.
94
+ * @typeParam TPermissions - The permissions definition type.
95
+ * @typeParam TPluginContext - The accumulated plugin context type.
96
+ * @typeParam TClientContext - The accumulated client-side context type.
97
+ */
98
+ type App<TSchema extends Schema, TPermissions extends Permissions, TPluginContext, TClientContext> = {
99
+ /** Validates and initializes environment bindings. Call once in the Workers entry point. */
100
+ init(rawEnv: Record<string, unknown>): void;
101
+ /** Returns the typed, validated environment. */
102
+ env(): ParsedEnv<TSchema>;
103
+ /** Builds the per-request context by running each plugin's `setup()` in order. */
104
+ context(request: Request, context?: unknown): Promise<AppContext<TSchema, TPluginContext>>;
105
+ /** Convenience wrapper for React Router loaders that auto-creates the app context. */
106
+ loader<T>(fn: (ctx: AppContext<TSchema, TPluginContext>, args: RouteArgs) => T | Promise<T>): (args: RouteArgs) => Promise<T>;
107
+ /** Convenience wrapper for React Router actions that auto-creates the app context. */
108
+ action<T>(fn: (ctx: AppContext<TSchema, TPluginContext>, args: RouteArgs) => T | Promise<T>): (args: RouteArgs) => Promise<T>;
109
+ /** Registers a plugin, extending the app's context type. Throws on duplicate names. */
110
+ use<TName extends string, TProvides, TClient>(plugin: CfastPlugin<TName, TProvides, TPluginContext, TClient>): App<TSchema, TPermissions, TPluginContext & {
111
+ [K in TName]: TProvides;
112
+ }, TClientContext & (TClient extends object ? {
113
+ [K in TName]: TClient;
114
+ } : unknown)>;
115
+ /** Composed React provider tree from all registered plugins. */
116
+ Provider: ComponentType<{
117
+ children: ReactNode;
118
+ }>;
119
+ /** The permissions config passed to `createApp()`. */
120
+ permissions: TPermissions;
121
+ };
122
+
123
+ export type { App as A, CreateAppConfig as C, PluginSetupContext as P, RouteArgs as R, CfastPlugin as a, AppContext as b, PluginProvides as c };
@@ -0,0 +1,154 @@
1
+ import { Schema, ParsedEnv } from '@cfast/env';
2
+ import { Permissions } from '@cfast/permissions';
3
+ import { ComponentType, ReactNode } from 'react';
4
+
5
+ /**
6
+ * Configuration object for {@link createApp}.
7
+ *
8
+ * @typeParam TSchema - The env schema type from `@cfast/env`.
9
+ * @typeParam TPermissions - The permissions definition from `@cfast/permissions`.
10
+ */
11
+ type CreateAppConfig<TSchema extends Schema, TPermissions extends Permissions> = {
12
+ /** The environment variable schema. Validated at `app.init()` time via `@cfast/env`. */
13
+ env: TSchema;
14
+ /** The permissions config from `definePermissions()`. Made available to all plugins. */
15
+ permissions: TPermissions;
16
+ };
17
+ /**
18
+ * The context object passed to a plugin's `setup()` function.
19
+ *
20
+ * Contains the current request, validated env, and all values provided by prior plugins
21
+ * (typed via `TRequires`).
22
+ *
23
+ * @typeParam TRequires - Intersection of prior plugin provides (e.g., `AuthPluginProvides`).
24
+ */
25
+ type PluginSetupContext<TRequires> = {
26
+ /** The incoming HTTP request for the current invocation. */
27
+ request: Request;
28
+ /** The validated environment bindings. */
29
+ env: Record<string, unknown>;
30
+ } & TRequires;
31
+ /**
32
+ * A cfast plugin definition created by {@link definePlugin}.
33
+ *
34
+ * Plugins provide server-side context values via `setup()`, optional client-side React providers,
35
+ * and optional client-side values accessible via `useApp()`.
36
+ *
37
+ * @typeParam TName - The unique plugin name, used as the namespace key in `AppContext`.
38
+ * @typeParam TProvides - The type returned by `setup()`, accessible as `ctx[name]`.
39
+ * @typeParam TRequires - The context shape this plugin depends on from prior plugins.
40
+ * @typeParam TClient - Client-side values exposed via `useApp()`.
41
+ */
42
+ type CfastPlugin<TName extends string = string, TProvides = unknown, TRequires = unknown, TClient = unknown> = {
43
+ /** Unique identifier used as the namespace key in the app context. */
44
+ name: TName;
45
+ /** Called per-request to produce the values this plugin provides. */
46
+ setup: (ctx: PluginSetupContext<TRequires>) => TProvides | Promise<TProvides>;
47
+ /** Optional client-side React provider, composed into `app.Provider`. */
48
+ Provider?: ComponentType<{
49
+ children: ReactNode;
50
+ }>;
51
+ /** Optional client-side values exposed via `useApp()`. */
52
+ client?: TClient;
53
+ };
54
+ /**
55
+ * Minimal plugin shape used internally for runtime iteration in `buildApp`.
56
+ *
57
+ * The `setup` parameter uses `PluginSetupContext<never>` so that every concrete
58
+ * `CfastPlugin<TName, TProvides, TRequires, TClient>` is structurally assignable
59
+ * to this type without casting — `never` extends any `TRequires`, satisfying
60
+ * function parameter contravariance.
61
+ *
62
+ * At the call site in `context()`, we use a single documented cast to invoke
63
+ * `setup` with the actual runtime context, since TypeScript cannot prove that
64
+ * accumulated plugin results satisfy each plugin's `TRequires`.
65
+ *
66
+ * This type is internal and not part of the public API.
67
+ */
68
+ type RuntimePlugin = {
69
+ /** Plugin name used as the namespace key. */
70
+ name: string;
71
+ /**
72
+ * Setup function called per-request.
73
+ *
74
+ * Typed with `never` parameter to make all `CfastPlugin` subtypes assignable.
75
+ * Called at runtime via a documented cast in `context()`.
76
+ */
77
+ setup: (ctx: PluginSetupContext<never>) => unknown | Promise<unknown>;
78
+ /** Optional client-side React provider. */
79
+ Provider?: ComponentType<{
80
+ children: ReactNode;
81
+ }>;
82
+ /** Optional client-side values. */
83
+ client?: unknown;
84
+ };
85
+ /**
86
+ * Utility type that extracts `{ [name]: ReturnType<setup> }` from a plugin definition.
87
+ *
88
+ * Use this to create a type token that dependent plugins can reference via `definePlugin<TRequires>()`.
89
+ *
90
+ * @typeParam T - A `CfastPlugin` type to extract provides from.
91
+ */
92
+ type PluginProvides<T> = T extends CfastPlugin<infer N, infer P, unknown, unknown> ? {
93
+ [K in N]: P;
94
+ } : never;
95
+ /**
96
+ * The accumulated per-request context after all plugins have run.
97
+ *
98
+ * Contains the validated env plus each plugin's namespaced values.
99
+ *
100
+ * @typeParam TSchema - The env schema type.
101
+ * @typeParam TPluginContext - The intersection of all registered plugins' provides.
102
+ */
103
+ type AppContext<TSchema extends Schema, TPluginContext> = {
104
+ /** The validated environment bindings. */
105
+ env: ParsedEnv<TSchema>;
106
+ } & TPluginContext;
107
+ /**
108
+ * Route handler arguments passed through from React Router loaders and actions.
109
+ */
110
+ type RouteArgs = {
111
+ /** The incoming HTTP request. */
112
+ request: Request;
113
+ /** URL route parameters (e.g., `{ postId: "abc" }`). */
114
+ params: Record<string, string | undefined>;
115
+ /** The React Router context object (contains `cloudflare.env`, etc.). */
116
+ context: unknown;
117
+ };
118
+ /**
119
+ * The app object returned by `createApp()` and extended by `.use()` calls.
120
+ *
121
+ * Provides methods for environment initialization, per-request context creation,
122
+ * route handler wrappers, plugin registration, and a composed React provider.
123
+ *
124
+ * @typeParam TSchema - The env schema type.
125
+ * @typeParam TPermissions - The permissions definition type.
126
+ * @typeParam TPluginContext - The accumulated plugin context type.
127
+ * @typeParam TClientContext - The accumulated client-side context type.
128
+ */
129
+ type App<TSchema extends Schema, TPermissions extends Permissions, TPluginContext, TClientContext> = {
130
+ /** Validates and initializes environment bindings. Call once in the Workers entry point. */
131
+ init(rawEnv: Record<string, unknown>): void;
132
+ /** Returns the typed, validated environment. */
133
+ env(): ParsedEnv<TSchema>;
134
+ /** Builds the per-request context by running each plugin's `setup()` in order. */
135
+ context(request: Request, context?: unknown): Promise<AppContext<TSchema, TPluginContext>>;
136
+ /** Convenience wrapper for React Router loaders that auto-creates the app context. */
137
+ loader<T>(fn: (ctx: AppContext<TSchema, TPluginContext>, args: RouteArgs) => T | Promise<T>): (args: RouteArgs) => Promise<T>;
138
+ /** Convenience wrapper for React Router actions that auto-creates the app context. */
139
+ action<T>(fn: (ctx: AppContext<TSchema, TPluginContext>, args: RouteArgs) => T | Promise<T>): (args: RouteArgs) => Promise<T>;
140
+ /** Registers a plugin, extending the app's context type. Throws on duplicate names. */
141
+ use<TName extends string, TProvides, TClient>(plugin: CfastPlugin<TName, TProvides, TPluginContext, TClient>): App<TSchema, TPermissions, TPluginContext & {
142
+ [K in TName]: TProvides;
143
+ }, TClientContext & (TClient extends object ? {
144
+ [K in TName]: TClient;
145
+ } : unknown)>;
146
+ /** Composed React provider tree from all registered plugins. */
147
+ Provider: ComponentType<{
148
+ children: ReactNode;
149
+ }>;
150
+ /** The permissions config passed to `createApp()`. */
151
+ permissions: TPermissions;
152
+ };
153
+
154
+ export type { App as A, CreateAppConfig as C, PluginSetupContext as P, RouteArgs as R, CfastPlugin as a, AppContext as b, PluginProvides as c, RuntimePlugin as d };
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@cfast/core",
3
+ "version": "0.0.1",
4
+ "description": "App composition layer with plugin system for @cfast/* packages",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/DanielMSchmidt/cfast.git",
9
+ "directory": "packages/core"
10
+ },
11
+ "type": "module",
12
+ "main": "dist/index.js",
13
+ "types": "dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "import": "./dist/index.js",
17
+ "types": "./dist/index.d.ts"
18
+ },
19
+ "./client": {
20
+ "import": "./dist/client/index.js",
21
+ "types": "./dist/client/index.d.ts"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "sideEffects": false,
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "dependencies": {
32
+ "@cfast/permissions": "0.0.1",
33
+ "@cfast/env": "0.0.1"
34
+ },
35
+ "peerDependencies": {
36
+ "react": ">=18"
37
+ },
38
+ "peerDependenciesMeta": {
39
+ "react": {
40
+ "optional": true
41
+ }
42
+ },
43
+ "devDependencies": {
44
+ "@types/react": "^19.2.14",
45
+ "@types/react-dom": "^19.2.3",
46
+ "react": "^19.1.0",
47
+ "react-dom": "^19.2.4",
48
+ "tsup": "^8",
49
+ "typescript": "^5.7",
50
+ "vitest": "^4.1.0"
51
+ },
52
+ "scripts": {
53
+ "build": "tsup src/index.ts src/client/index.ts --format esm --dts",
54
+ "dev": "tsup src/index.ts src/client/index.ts --format esm --dts --watch",
55
+ "typecheck": "tsc --noEmit",
56
+ "lint": "eslint src/",
57
+ "test": "vitest run --passWithNoTests"
58
+ }
59
+ }