@cfast/core 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/client/index.d.ts +1 -1
- package/dist/index.d.ts +95 -15
- package/dist/index.js +14 -1
- package/dist/{types-L3fwnDhg.d.ts → types-CQyKq4D1.d.ts} +79 -7
- package/llms.txt +85 -13
- package/package.json +12 -6
package/README.md
CHANGED
|
@@ -130,7 +130,7 @@ import { app } from '~/cfast';
|
|
|
130
130
|
|
|
131
131
|
export async function loader({ request, context }: Route.LoaderArgs) {
|
|
132
132
|
const ctx = await app.context(request, context);
|
|
133
|
-
return ctx.db.client.query(posts).findMany().run(
|
|
133
|
+
return ctx.db.client.query(posts).findMany().run();
|
|
134
134
|
}
|
|
135
135
|
```
|
|
136
136
|
|
|
@@ -165,7 +165,7 @@ Optional convenience wrappers that call `app.context()` and pass the result as t
|
|
|
165
165
|
|
|
166
166
|
```typescript
|
|
167
167
|
export const loader = app.loader(async (ctx, { params }) => {
|
|
168
|
-
return ctx.db.client.query(posts).findMany().run(
|
|
168
|
+
return ctx.db.client.query(posts).findMany().run();
|
|
169
169
|
});
|
|
170
170
|
```
|
|
171
171
|
|
|
@@ -443,7 +443,7 @@ export async function loader({ request, context }: Route.LoaderArgs) {
|
|
|
443
443
|
}
|
|
444
444
|
await ctx['rate-limit'].consume();
|
|
445
445
|
|
|
446
|
-
return ctx.db.client.query(posts).findMany().run(
|
|
446
|
+
return ctx.db.client.query(posts).findMany().run();
|
|
447
447
|
}
|
|
448
448
|
```
|
|
449
449
|
|
package/dist/client/index.d.ts
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Schema } from '@cfast/env';
|
|
2
2
|
import { Permissions } from '@cfast/permissions';
|
|
3
|
-
import { C as CreateAppConfig, A as App, P as PluginSetupContext,
|
|
4
|
-
export { b as AppContext, c as PluginProvides,
|
|
3
|
+
import { C as CreateAppConfig, A as App, a as CfastPlugin, P as PluginSetupContext, R as RequiresFromPlugins } from './types-CQyKq4D1.js';
|
|
4
|
+
export { b as AppContext, c as PluginContext, d as PluginProvides, e as RouteArgs } from './types-CQyKq4D1.js';
|
|
5
5
|
import { ComponentType, ReactNode } from 'react';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -29,12 +29,44 @@ declare function createApp<TSchema extends Schema, TPermissions extends Permissi
|
|
|
29
29
|
/**
|
|
30
30
|
* Defines a cfast plugin for use with `createApp().use()`.
|
|
31
31
|
*
|
|
32
|
-
*
|
|
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.
|
|
32
|
+
* Two equivalent forms are supported:
|
|
36
33
|
*
|
|
37
|
-
*
|
|
34
|
+
* 1. **Inferred form (preferred)** — pass plugin references in `requires` and the
|
|
35
|
+
* `setup(ctx)` parameter is automatically typed with their provides:
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* const dbPlugin = definePlugin({
|
|
39
|
+
* name: "db",
|
|
40
|
+
* requires: [authPlugin],
|
|
41
|
+
* setup(ctx) {
|
|
42
|
+
* ctx.auth.user; // typed from authPlugin
|
|
43
|
+
* return { client: createDb({}) };
|
|
44
|
+
* },
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* No need to import or declare a `TRequires` type token — the dependency type
|
|
49
|
+
* flows directly from the registered plugin objects. Leaf plugins simply omit
|
|
50
|
+
* `requires` and get `ctx: { request, env }`.
|
|
51
|
+
*
|
|
52
|
+
* 2. **Curried form (legacy)** — kept for backward compatibility with code that
|
|
53
|
+
* only has access to a `PluginProvides<typeof authPlugin>` *type* (e.g. when
|
|
54
|
+
* you cannot import the actual plugin value):
|
|
55
|
+
*
|
|
56
|
+
* ```ts
|
|
57
|
+
* definePlugin<AuthPluginProvides>()({
|
|
58
|
+
* name: "db",
|
|
59
|
+
* setup(ctx) { ctx.auth.user; return { ... }; },
|
|
60
|
+
* });
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* Plugins declared via the inferred form also benefit from runtime validation:
|
|
64
|
+
* `app.use(plugin)` throws a `CfastConfigError` if any plugin listed in
|
|
65
|
+
* `requires` has not yet been registered, with a message that names the missing
|
|
66
|
+
* dependency.
|
|
67
|
+
*
|
|
68
|
+
* @param config - Plugin configuration with `name`, `setup`, and optional
|
|
69
|
+
* `requires`, `Provider`, `client`.
|
|
38
70
|
* @returns A `CfastPlugin` instance ready to pass to `app.use()`.
|
|
39
71
|
*
|
|
40
72
|
* @example
|
|
@@ -47,25 +79,26 @@ declare function createApp<TSchema extends Schema, TPermissions extends Permissi
|
|
|
47
79
|
* },
|
|
48
80
|
* });
|
|
49
81
|
*
|
|
50
|
-
* //
|
|
51
|
-
*
|
|
52
|
-
* const dbPlugin = definePlugin<AuthPluginProvides>()({
|
|
82
|
+
* // Dependent plugin — requires inferred from plugin references
|
|
83
|
+
* const dbPlugin = definePlugin({
|
|
53
84
|
* name: 'db',
|
|
85
|
+
* requires: [authPlugin],
|
|
54
86
|
* setup(ctx) {
|
|
55
|
-
* ctx.auth.user; // typed from
|
|
87
|
+
* ctx.auth.user; // typed from authPlugin
|
|
56
88
|
* return { client: createDb({}) };
|
|
57
89
|
* },
|
|
58
90
|
* });
|
|
59
91
|
* ```
|
|
60
92
|
*/
|
|
61
|
-
declare function definePlugin<TName extends string, TProvides, TClient = unknown>(config: {
|
|
93
|
+
declare function definePlugin<TName extends string, TProvides, const TRequires extends readonly CfastPlugin<string, unknown, any, unknown, any>[] = [], TClient = unknown>(config: {
|
|
62
94
|
name: TName;
|
|
63
|
-
|
|
95
|
+
requires?: TRequires;
|
|
96
|
+
setup: (ctx: PluginSetupContext<RequiresFromPlugins<TRequires>>) => TProvides | Promise<TProvides>;
|
|
64
97
|
Provider?: ComponentType<{
|
|
65
98
|
children: ReactNode;
|
|
66
99
|
}>;
|
|
67
100
|
client?: TClient;
|
|
68
|
-
}): CfastPlugin<TName, Awaited<TProvides>,
|
|
101
|
+
}): CfastPlugin<TName, Awaited<TProvides>, RequiresFromPlugins<TRequires>, TClient>;
|
|
69
102
|
declare function definePlugin<TRequires>(): <TName extends string, TProvides, TClient = unknown>(config: {
|
|
70
103
|
name: TName;
|
|
71
104
|
setup: (ctx: PluginSetupContext<TRequires>) => TProvides | Promise<TProvides>;
|
|
@@ -74,6 +107,53 @@ declare function definePlugin<TRequires>(): <TName extends string, TProvides, TC
|
|
|
74
107
|
}>;
|
|
75
108
|
client?: TClient;
|
|
76
109
|
}) => CfastPlugin<TName, Awaited<TProvides>, TRequires, TClient>;
|
|
110
|
+
/**
|
|
111
|
+
* Returns an env-typed `definePlugin` factory.
|
|
112
|
+
*
|
|
113
|
+
* The scaffolded `Cloudflare.Env` type from `worker-configuration.d.ts` is
|
|
114
|
+
* not known to `@cfast/core` at build time, so the generic `definePlugin`
|
|
115
|
+
* exposes `ctx.env` as the loose `Record<string, unknown>` shape and
|
|
116
|
+
* consumers have to cast bindings (e.g. `ctx.env.DB as D1Database`). Apps
|
|
117
|
+
* that want precise bindings call this factory once with their env type
|
|
118
|
+
* and re-export the typed `definePlugin` from a local module:
|
|
119
|
+
*
|
|
120
|
+
* ```ts
|
|
121
|
+
* // app/plugins/define-plugin.ts
|
|
122
|
+
* import { definePluginFor } from "@cfast/core";
|
|
123
|
+
*
|
|
124
|
+
* export const definePlugin = definePluginFor<Cloudflare.Env>();
|
|
125
|
+
* ```
|
|
126
|
+
*
|
|
127
|
+
* Plugin authors then import from their local module and get `ctx.env.DB`
|
|
128
|
+
* typed as `D1Database` without any casting:
|
|
129
|
+
*
|
|
130
|
+
* ```ts
|
|
131
|
+
* import { definePlugin } from "~/plugins/define-plugin";
|
|
132
|
+
*
|
|
133
|
+
* export const dbPlugin = definePlugin({
|
|
134
|
+
* name: "db",
|
|
135
|
+
* setup(ctx) {
|
|
136
|
+
* const db = ctx.env.DB; // D1Database, no cast
|
|
137
|
+
* return { client: createDb({ d1: db }) };
|
|
138
|
+
* },
|
|
139
|
+
* });
|
|
140
|
+
* ```
|
|
141
|
+
*
|
|
142
|
+
* The returned factory has the same shape as {@link definePlugin} (with the
|
|
143
|
+
* curried legacy overload preserved) so existing code that switches from the
|
|
144
|
+
* generic form to the env-typed form only needs an import swap.
|
|
145
|
+
*/
|
|
146
|
+
declare function definePluginFor<TEnv>(): {
|
|
147
|
+
<TName extends string, TProvides, const TRequires extends readonly CfastPlugin<string, unknown, any, unknown, any>[] = [], TClient = unknown>(config: {
|
|
148
|
+
name: TName;
|
|
149
|
+
requires?: TRequires;
|
|
150
|
+
setup: (ctx: PluginSetupContext<RequiresFromPlugins<TRequires>, TEnv>) => TProvides | Promise<TProvides>;
|
|
151
|
+
Provider?: ComponentType<{
|
|
152
|
+
children: ReactNode;
|
|
153
|
+
}>;
|
|
154
|
+
client?: TClient;
|
|
155
|
+
}): CfastPlugin<TName, Awaited<TProvides>, RequiresFromPlugins<TRequires>, TClient, TEnv>;
|
|
156
|
+
};
|
|
77
157
|
|
|
78
158
|
/**
|
|
79
159
|
* Error thrown when a plugin's `setup()` function fails during `app.context()`.
|
|
@@ -101,4 +181,4 @@ declare class CfastConfigError extends Error {
|
|
|
101
181
|
constructor(message: string);
|
|
102
182
|
}
|
|
103
183
|
|
|
104
|
-
export { App, CfastConfigError, CfastPlugin, CfastPluginError, CreateAppConfig, PluginSetupContext, createApp, definePlugin };
|
|
184
|
+
export { App, CfastConfigError, CfastPlugin, CfastPluginError, CreateAppConfig, PluginSetupContext, createApp, definePlugin, definePluginFor };
|
package/dist/index.js
CHANGED
|
@@ -91,6 +91,15 @@ function buildApp(envInstance, permissions, plugins) {
|
|
|
91
91
|
`Duplicate plugin name "${plugin.name}". Each plugin must have a unique name.`
|
|
92
92
|
);
|
|
93
93
|
}
|
|
94
|
+
if (plugin.requires) {
|
|
95
|
+
for (const dep of plugin.requires) {
|
|
96
|
+
if (!pluginNames.has(dep.name)) {
|
|
97
|
+
throw new CfastConfigError(
|
|
98
|
+
`Plugin "${plugin.name}" requires "${dep.name}" but it has not been registered. Did you call .use(${dep.name}Plugin) before .use(${plugin.name}Plugin)?`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
94
103
|
return buildApp(envInstance, permissions, [...plugins, plugin]);
|
|
95
104
|
},
|
|
96
105
|
Provider: createCoreProvider(plugins)
|
|
@@ -105,9 +114,13 @@ function definePlugin(config) {
|
|
|
105
114
|
}
|
|
106
115
|
return config;
|
|
107
116
|
}
|
|
117
|
+
function definePluginFor() {
|
|
118
|
+
return (config) => config;
|
|
119
|
+
}
|
|
108
120
|
export {
|
|
109
121
|
CfastConfigError,
|
|
110
122
|
CfastPluginError,
|
|
111
123
|
createApp,
|
|
112
|
-
definePlugin
|
|
124
|
+
definePlugin,
|
|
125
|
+
definePluginFor
|
|
113
126
|
};
|
|
@@ -20,14 +20,43 @@ type CreateAppConfig<TSchema extends Schema, TPermissions extends Permissions> =
|
|
|
20
20
|
* Contains the current request, validated env, and all values provided by prior plugins
|
|
21
21
|
* (typed via `TRequires`).
|
|
22
22
|
*
|
|
23
|
+
* By default `env` is typed as the loose `Record<string, unknown>` shape used
|
|
24
|
+
* by the framework's generic plugin layer. Consumer apps that want precise
|
|
25
|
+
* bindings (e.g. `Cloudflare.Env` from `worker-configuration.d.ts`) can
|
|
26
|
+
* specialise the type via {@link definePluginFor} or by importing the
|
|
27
|
+
* {@link PluginContext} helper directly.
|
|
28
|
+
*
|
|
23
29
|
* @typeParam TRequires - Intersection of prior plugin provides (e.g., `AuthPluginProvides`).
|
|
30
|
+
* @typeParam TEnv - Env shape (defaults to the loose record shape).
|
|
24
31
|
*/
|
|
25
|
-
type PluginSetupContext<TRequires
|
|
32
|
+
type PluginSetupContext<TRequires, TEnv = Record<string, unknown>> = {
|
|
26
33
|
/** The incoming HTTP request for the current invocation. */
|
|
27
34
|
request: Request;
|
|
28
35
|
/** The validated environment bindings. */
|
|
29
|
-
env:
|
|
36
|
+
env: TEnv;
|
|
30
37
|
} & TRequires;
|
|
38
|
+
/**
|
|
39
|
+
* Convenience alias for consumers who want to annotate a plugin's `setup(ctx)`
|
|
40
|
+
* parameter without going through {@link definePluginFor}. Re-exported from
|
|
41
|
+
* the package entry as `PluginContext`.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* import type { PluginContext } from "@cfast/core";
|
|
46
|
+
* import type { PluginProvides } from "@cfast/core";
|
|
47
|
+
* import type { authPlugin } from "./plugins/auth";
|
|
48
|
+
*
|
|
49
|
+
* export const dbPlugin = definePlugin({
|
|
50
|
+
* name: "db",
|
|
51
|
+
* requires: [authPlugin],
|
|
52
|
+
* setup(ctx: PluginContext<Cloudflare.Env, PluginProvides<typeof authPlugin>>) {
|
|
53
|
+
* const db = ctx.env.DB; // typed as D1Database, no cast
|
|
54
|
+
* return { client: createDb({ d1: db }) };
|
|
55
|
+
* },
|
|
56
|
+
* });
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
type PluginContext<TEnv = Record<string, unknown>, TRequires = unknown> = PluginSetupContext<TRequires, TEnv>;
|
|
31
60
|
/**
|
|
32
61
|
* A cfast plugin definition created by {@link definePlugin}.
|
|
33
62
|
*
|
|
@@ -38,12 +67,22 @@ type PluginSetupContext<TRequires> = {
|
|
|
38
67
|
* @typeParam TProvides - The type returned by `setup()`, accessible as `ctx[name]`.
|
|
39
68
|
* @typeParam TRequires - The context shape this plugin depends on from prior plugins.
|
|
40
69
|
* @typeParam TClient - Client-side values exposed via `useApp()`.
|
|
70
|
+
* @typeParam TEnv - Env shape seen by `setup()`. Defaults to the loose record
|
|
71
|
+
* shape so non-specialised plugins do not need to parameterise this slot.
|
|
41
72
|
*/
|
|
42
|
-
type CfastPlugin<TName extends string = string, TProvides = unknown, TRequires = unknown, TClient = unknown
|
|
73
|
+
type CfastPlugin<TName extends string = string, TProvides = unknown, TRequires = unknown, TClient = unknown, TEnv = Record<string, unknown>> = {
|
|
43
74
|
/** Unique identifier used as the namespace key in the app context. */
|
|
44
75
|
name: TName;
|
|
76
|
+
/**
|
|
77
|
+
* Optional list of plugin references this plugin depends on.
|
|
78
|
+
*
|
|
79
|
+
* When supplied, `setup(ctx)` is automatically typed with the union of each
|
|
80
|
+
* required plugin's provides, and `app.use(this)` will throw at registration
|
|
81
|
+
* time if any of these plugins have not yet been registered.
|
|
82
|
+
*/
|
|
83
|
+
requires?: readonly CfastPlugin<string, unknown, any, unknown, any>[];
|
|
45
84
|
/** Called per-request to produce the values this plugin provides. */
|
|
46
|
-
setup: (ctx: PluginSetupContext<TRequires>) => TProvides | Promise<TProvides>;
|
|
85
|
+
setup: (ctx: PluginSetupContext<TRequires, TEnv>) => TProvides | Promise<TProvides>;
|
|
47
86
|
/** Optional client-side React provider, composed into `app.Provider`. */
|
|
48
87
|
Provider?: ComponentType<{
|
|
49
88
|
children: ReactNode;
|
|
@@ -51,6 +90,32 @@ type CfastPlugin<TName extends string = string, TProvides = unknown, TRequires =
|
|
|
51
90
|
/** Optional client-side values exposed via `useApp()`. */
|
|
52
91
|
client?: TClient;
|
|
53
92
|
};
|
|
93
|
+
/**
|
|
94
|
+
* Converts a union type to an intersection type.
|
|
95
|
+
*
|
|
96
|
+
* Used by {@link RequiresFromPlugins} to merge each required plugin's provides
|
|
97
|
+
* into a single intersection that becomes the `setup(ctx)` parameter shape.
|
|
98
|
+
*/
|
|
99
|
+
type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
|
|
100
|
+
/**
|
|
101
|
+
* Derives the `TRequires` shape for a `setup(ctx)` parameter from an array of
|
|
102
|
+
* plugin references passed via `definePlugin({ requires: [...] })`.
|
|
103
|
+
*
|
|
104
|
+
* Each plugin in the tuple contributes `{ [name]: provides }` and the results
|
|
105
|
+
* are intersected so `ctx` exposes every required plugin's namespace at the
|
|
106
|
+
* right key. An empty `requires` tuple resolves to `unknown` (no extra fields).
|
|
107
|
+
*
|
|
108
|
+
* Implementation note: we walk the tuple positionally with a mapped type
|
|
109
|
+
* (`{ [K in keyof R]: ... }[number]`) instead of using `R[number]` directly.
|
|
110
|
+
* The mapped form preserves per-element inference of `N` and `P`, so multiple
|
|
111
|
+
* required plugins each contribute their own `{ name: provides }` shape rather
|
|
112
|
+
* than collapsing into a union of widened `provides` values.
|
|
113
|
+
*/
|
|
114
|
+
type RequiresFromPlugins<R extends readonly CfastPlugin<string, unknown, any, unknown, any>[]> = R extends readonly [] ? unknown : UnionToIntersection<{
|
|
115
|
+
[K in keyof R]: R[K] extends CfastPlugin<infer N, infer P, unknown, unknown, any> ? {
|
|
116
|
+
[Key in N]: P;
|
|
117
|
+
} : never;
|
|
118
|
+
}[number]>;
|
|
54
119
|
/**
|
|
55
120
|
* Minimal plugin shape used internally for runtime iteration in `buildApp`.
|
|
56
121
|
*
|
|
@@ -68,6 +133,13 @@ type CfastPlugin<TName extends string = string, TProvides = unknown, TRequires =
|
|
|
68
133
|
type RuntimePlugin = {
|
|
69
134
|
/** Plugin name used as the namespace key. */
|
|
70
135
|
name: string;
|
|
136
|
+
/**
|
|
137
|
+
* Optional dependencies declared via `definePlugin({ requires: [...] })`.
|
|
138
|
+
*
|
|
139
|
+
* `app.use(plugin)` walks this list and verifies each entry is already
|
|
140
|
+
* registered, throwing `CfastConfigError` immediately if not.
|
|
141
|
+
*/
|
|
142
|
+
requires?: readonly CfastPlugin<string, unknown, any, unknown, any>[];
|
|
71
143
|
/**
|
|
72
144
|
* Setup function called per-request.
|
|
73
145
|
*
|
|
@@ -89,7 +161,7 @@ type RuntimePlugin = {
|
|
|
89
161
|
*
|
|
90
162
|
* @typeParam T - A `CfastPlugin` type to extract provides from.
|
|
91
163
|
*/
|
|
92
|
-
type PluginProvides<T> = T extends CfastPlugin<infer N, infer P, unknown, unknown> ? {
|
|
164
|
+
type PluginProvides<T> = T extends CfastPlugin<infer N, infer P, unknown, unknown, any> ? {
|
|
93
165
|
[K in N]: P;
|
|
94
166
|
} : never;
|
|
95
167
|
/**
|
|
@@ -138,7 +210,7 @@ type App<TSchema extends Schema, TPermissions extends Permissions, TPluginContex
|
|
|
138
210
|
/** Convenience wrapper for React Router actions that auto-creates the app context. */
|
|
139
211
|
action<T>(fn: (ctx: AppContext<TSchema, TPluginContext>, args: RouteArgs) => T | Promise<T>): (args: RouteArgs) => Promise<T>;
|
|
140
212
|
/** 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 & {
|
|
213
|
+
use<TName extends string, TProvides, TClient, TEnv = any>(plugin: CfastPlugin<TName, TProvides, TPluginContext, TClient, TEnv>): App<TSchema, TPermissions, TPluginContext & {
|
|
142
214
|
[K in TName]: TProvides;
|
|
143
215
|
}, TClientContext & (TClient extends object ? {
|
|
144
216
|
[K in TName]: TClient;
|
|
@@ -151,4 +223,4 @@ type App<TSchema extends Schema, TPermissions extends Permissions, TPluginContex
|
|
|
151
223
|
permissions: TPermissions;
|
|
152
224
|
};
|
|
153
225
|
|
|
154
|
-
export type { App as A, CreateAppConfig as C, PluginSetupContext as P,
|
|
226
|
+
export type { App as A, CreateAppConfig as C, PluginSetupContext as P, RequiresFromPlugins as R, CfastPlugin as a, AppContext as b, PluginContext as c, PluginProvides as d, RouteArgs as e, RuntimePlugin as f };
|
package/llms.txt
CHANGED
|
@@ -10,7 +10,7 @@ Use `@cfast/core` when you want multiple cfast packages (auth, db, storage, etc.
|
|
|
10
10
|
|
|
11
11
|
- **Plugin chain**: Plugins run in registration order via `.use()`. Each plugin's `setup()` receives everything prior plugins provided, plus `request` and `env`.
|
|
12
12
|
- **Namespaced context**: Each plugin's return value is nested under its `name` key (e.g., `ctx.auth.user`, `ctx.db.client`). No flat merging, no collisions.
|
|
13
|
-
- **
|
|
13
|
+
- **Inferred dependencies**: Dependent plugins list their requirements as plugin references in `requires: [authPlugin]`. TypeScript infers `setup(ctx)` from the listed plugins -- no manual `TRequires` type token needed. TypeScript also catches missing dependencies at the `.use()` call site, and `app.use(plugin)` throws a clear `CfastConfigError` at runtime if a required plugin has not yet been registered.
|
|
14
14
|
- **Client provider composition**: `<app.Provider>` nests all plugin React providers in registration order. `useApp()` accesses client-side plugin exports.
|
|
15
15
|
|
|
16
16
|
## API Reference
|
|
@@ -37,7 +37,7 @@ app.permissions: TPermissions // permissions config
|
|
|
37
37
|
### Plugin definition
|
|
38
38
|
|
|
39
39
|
```typescript
|
|
40
|
-
// Leaf plugin (no dependencies) --
|
|
40
|
+
// Leaf plugin (no dependencies) -- types are fully inferred
|
|
41
41
|
definePlugin({
|
|
42
42
|
name: "analytics",
|
|
43
43
|
setup(ctx) { return { track: (event: string) => {} }; },
|
|
@@ -45,27 +45,99 @@ definePlugin({
|
|
|
45
45
|
client?: { /* client-side values for useApp() */ },
|
|
46
46
|
}): CfastPlugin
|
|
47
47
|
|
|
48
|
-
// Dependent plugin --
|
|
49
|
-
|
|
48
|
+
// Dependent plugin -- list plugin references in `requires`. The setup ctx is
|
|
49
|
+
// inferred from those plugins automatically; no manual generic, no type token.
|
|
50
|
+
const authPlugin = definePlugin({
|
|
51
|
+
name: "auth",
|
|
52
|
+
setup() { return { user: getUser(), grants: getGrants() }; },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const dbPlugin = definePlugin({
|
|
50
56
|
name: "db",
|
|
57
|
+
requires: [authPlugin],
|
|
51
58
|
setup(ctx) {
|
|
52
|
-
ctx.auth.user;
|
|
53
|
-
|
|
59
|
+
ctx.auth.user; // typed from authPlugin
|
|
60
|
+
ctx.auth.grants; // typed from authPlugin
|
|
61
|
+
return { client: createDb({ grants: ctx.auth.grants }) };
|
|
54
62
|
},
|
|
55
63
|
}): CfastPlugin
|
|
64
|
+
|
|
65
|
+
// Multiple dependencies -- list each one
|
|
66
|
+
const adminPlugin = definePlugin({
|
|
67
|
+
name: "admin",
|
|
68
|
+
requires: [authPlugin, dbPlugin],
|
|
69
|
+
setup(ctx) {
|
|
70
|
+
ctx.auth.user; // typed
|
|
71
|
+
ctx.db.client; // typed
|
|
72
|
+
return { /* ... */ };
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Runtime dependency validation
|
|
78
|
+
|
|
79
|
+
`app.use(plugin)` throws `CfastConfigError` immediately if a plugin listed in
|
|
80
|
+
`requires` has not yet been registered:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
CfastConfigError: Plugin "db" requires "auth" but it has not been registered.
|
|
84
|
+
Did you call .use(authPlugin) before .use(dbPlugin)?
|
|
56
85
|
```
|
|
57
86
|
|
|
58
|
-
|
|
87
|
+
This catches misordering at composition time instead of letting it surface as
|
|
88
|
+
an undefined access deep in the request lifecycle.
|
|
89
|
+
|
|
90
|
+
### Plugin type token (legacy curried form)
|
|
91
|
+
|
|
92
|
+
For situations where you only have access to a *type*, not the actual plugin
|
|
93
|
+
value (e.g. you cannot import the plugin module), the legacy curried form is
|
|
94
|
+
still available:
|
|
59
95
|
|
|
60
96
|
```typescript
|
|
61
|
-
// Each package exports a type token for dependents:
|
|
62
97
|
export type AuthPluginProvides = PluginProvides<typeof authPlugin>;
|
|
63
98
|
// Resolves to: { auth: { user: AuthUser | null; grants: Grant[]; instance: AuthInstance } }
|
|
64
99
|
|
|
65
|
-
|
|
66
|
-
|
|
100
|
+
definePlugin<AuthPluginProvides>()({
|
|
101
|
+
name: "db",
|
|
102
|
+
setup(ctx) { ctx.auth.user; return { /* ... */ }; },
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Prefer the inferred form (`requires: [authPlugin]`) when you can — it removes
|
|
107
|
+
the need to declare and import a `PluginProvides` type token.
|
|
108
|
+
|
|
109
|
+
### Typed env (`definePluginFor<Env>`)
|
|
110
|
+
|
|
111
|
+
The generic `definePlugin` exposes `ctx.env` as `Record<string, unknown>`
|
|
112
|
+
because `@cfast/core` cannot know your scaffolded `Cloudflare.Env` shape. To
|
|
113
|
+
avoid `ctx.env.DB as D1Database` casts in every plugin, call `definePluginFor`
|
|
114
|
+
once with your env type and re-export the typed factory from a local module:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// app/plugins/define-plugin.ts
|
|
118
|
+
import { definePluginFor } from "@cfast/core";
|
|
119
|
+
export const definePlugin = definePluginFor<Cloudflare.Env>();
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
// app/plugins/db.ts
|
|
124
|
+
import { definePlugin } from "~/plugins/define-plugin";
|
|
125
|
+
|
|
126
|
+
export const dbPlugin = definePlugin({
|
|
127
|
+
name: "db",
|
|
128
|
+
requires: [authPlugin],
|
|
129
|
+
setup(ctx) {
|
|
130
|
+
const db = ctx.env.DB; // typed as D1Database — no cast
|
|
131
|
+
return { client: createDb({ d1: db, /* ... */ }) };
|
|
132
|
+
},
|
|
133
|
+
});
|
|
67
134
|
```
|
|
68
135
|
|
|
136
|
+
The returned factory has the same shape as `definePlugin`, so plugins built
|
|
137
|
+
with it slot into existing `app.use()` calls without further changes. If you
|
|
138
|
+
prefer to type a single plugin in-place without a factory, import the
|
|
139
|
+
`PluginContext<Env, TRequires>` helper and annotate the parameter directly.
|
|
140
|
+
|
|
69
141
|
### Client (`@cfast/core/client`)
|
|
70
142
|
|
|
71
143
|
```typescript
|
|
@@ -118,12 +190,12 @@ export default {
|
|
|
118
190
|
// Using app.context() directly:
|
|
119
191
|
export async function loader({ request, context }: Route.LoaderArgs) {
|
|
120
192
|
const ctx = await app.context(request, context);
|
|
121
|
-
return ctx.db.client.query(posts).findMany().run(
|
|
193
|
+
return ctx.db.client.query(posts).findMany().run();
|
|
122
194
|
}
|
|
123
195
|
|
|
124
196
|
// Using convenience wrapper:
|
|
125
197
|
export const loader = app.loader(async (ctx, { params }) => {
|
|
126
|
-
return ctx.db.client.query(posts).findMany().run(
|
|
198
|
+
return ctx.db.client.query(posts).findMany().run();
|
|
127
199
|
});
|
|
128
200
|
```
|
|
129
201
|
|
|
@@ -160,7 +232,7 @@ export const { createAction, composeActions } = createActions({
|
|
|
160
232
|
|
|
161
233
|
## Common Mistakes
|
|
162
234
|
|
|
163
|
-
- Registering plugins in the wrong order -- `dbPlugin` depends on `authPlugin`, so auth must be `.use()`'d first. TypeScript catches this at compile time.
|
|
235
|
+
- Registering plugins in the wrong order -- `dbPlugin` depends on `authPlugin`, so auth must be `.use()`'d first. TypeScript catches this at compile time, and `.use()` also throws `CfastConfigError` immediately at runtime if a plugin's `requires` are unsatisfied.
|
|
164
236
|
- Calling `app.context()` before `app.init()` -- env is not validated yet, will throw.
|
|
165
237
|
- Calling `useApp()` outside `<app.Provider>` -- throws a context error.
|
|
166
238
|
- Registering two plugins with the same `name` -- throws `CfastConfigError` at import time.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cfast/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "App composition layer with plugin system for @cfast/* packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cfast",
|
|
@@ -36,14 +36,18 @@
|
|
|
36
36
|
"publishConfig": {
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
|
-
"dependencies": {
|
|
40
|
-
"@cfast/env": "0.1.0",
|
|
41
|
-
"@cfast/permissions": "0.1.0"
|
|
42
|
-
},
|
|
43
39
|
"peerDependencies": {
|
|
40
|
+
"@cfast/env": ">=0.1.0 <0.3.0",
|
|
41
|
+
"@cfast/permissions": ">=0.3.0 <0.5.0",
|
|
44
42
|
"react": ">=18"
|
|
45
43
|
},
|
|
46
44
|
"peerDependenciesMeta": {
|
|
45
|
+
"@cfast/env": {
|
|
46
|
+
"optional": false
|
|
47
|
+
},
|
|
48
|
+
"@cfast/permissions": {
|
|
49
|
+
"optional": false
|
|
50
|
+
},
|
|
47
51
|
"react": {
|
|
48
52
|
"optional": true
|
|
49
53
|
}
|
|
@@ -55,7 +59,9 @@
|
|
|
55
59
|
"react-dom": "^19.2.4",
|
|
56
60
|
"tsup": "^8",
|
|
57
61
|
"typescript": "^5.7",
|
|
58
|
-
"vitest": "^4.1.0"
|
|
62
|
+
"vitest": "^4.1.0",
|
|
63
|
+
"@cfast/env": "0.1.1",
|
|
64
|
+
"@cfast/permissions": "0.4.0"
|
|
59
65
|
},
|
|
60
66
|
"scripts": {
|
|
61
67
|
"build": "tsup src/index.ts src/client/index.ts --format esm --dts",
|