@cfast/core 0.1.1 → 0.1.2

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.
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ComponentType, ReactNode } from 'react';
3
- import { d as RuntimePlugin } from '../types-L3fwnDhg.js';
3
+ import { e as RuntimePlugin } from '../types-C2gJHKfQ.js';
4
4
  import '@cfast/env';
5
5
  import '@cfast/permissions';
6
6
 
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, a as CfastPlugin } from './types-L3fwnDhg.js';
4
- export { b as AppContext, c as PluginProvides, R as RouteArgs } from './types-L3fwnDhg.js';
3
+ import { C as CreateAppConfig, A as App, a as CfastPlugin, P as PluginSetupContext, R as RequiresFromPlugins } from './types-C2gJHKfQ.js';
4
+ export { b as AppContext, c as PluginProvides, d as RouteArgs } from './types-C2gJHKfQ.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
- * 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.
32
+ * Two equivalent forms are supported:
36
33
  *
37
- * @param config - Plugin configuration with `name`, `setup`, and optional `Provider`/`client`.
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
- * // Plugin with dependencies (curried)
51
- * import type { AuthPluginProvides } from '@cfast/auth';
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 AuthPluginProvides
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>[] = [], TClient = unknown>(config: {
62
94
  name: TName;
63
- setup: (ctx: PluginSetupContext<unknown>) => TProvides | Promise<TProvides>;
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>, unknown, TClient>;
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>;
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)
@@ -42,6 +42,14 @@ type PluginSetupContext<TRequires> = {
42
42
  type CfastPlugin<TName extends string = string, TProvides = unknown, TRequires = unknown, TClient = unknown> = {
43
43
  /** Unique identifier used as the namespace key in the app context. */
44
44
  name: TName;
45
+ /**
46
+ * Optional list of plugin references this plugin depends on.
47
+ *
48
+ * When supplied, `setup(ctx)` is automatically typed with the union of each
49
+ * required plugin's provides, and `app.use(this)` will throw at registration
50
+ * time if any of these plugins have not yet been registered.
51
+ */
52
+ requires?: readonly CfastPlugin<string, unknown, any, unknown>[];
45
53
  /** Called per-request to produce the values this plugin provides. */
46
54
  setup: (ctx: PluginSetupContext<TRequires>) => TProvides | Promise<TProvides>;
47
55
  /** Optional client-side React provider, composed into `app.Provider`. */
@@ -51,6 +59,32 @@ type CfastPlugin<TName extends string = string, TProvides = unknown, TRequires =
51
59
  /** Optional client-side values exposed via `useApp()`. */
52
60
  client?: TClient;
53
61
  };
62
+ /**
63
+ * Converts a union type to an intersection type.
64
+ *
65
+ * Used by {@link RequiresFromPlugins} to merge each required plugin's provides
66
+ * into a single intersection that becomes the `setup(ctx)` parameter shape.
67
+ */
68
+ type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
69
+ /**
70
+ * Derives the `TRequires` shape for a `setup(ctx)` parameter from an array of
71
+ * plugin references passed via `definePlugin({ requires: [...] })`.
72
+ *
73
+ * Each plugin in the tuple contributes `{ [name]: provides }` and the results
74
+ * are intersected so `ctx` exposes every required plugin's namespace at the
75
+ * right key. An empty `requires` tuple resolves to `unknown` (no extra fields).
76
+ *
77
+ * Implementation note: we walk the tuple positionally with a mapped type
78
+ * (`{ [K in keyof R]: ... }[number]`) instead of using `R[number]` directly.
79
+ * The mapped form preserves per-element inference of `N` and `P`, so multiple
80
+ * required plugins each contribute their own `{ name: provides }` shape rather
81
+ * than collapsing into a union of widened `provides` values.
82
+ */
83
+ type RequiresFromPlugins<R extends readonly CfastPlugin<string, unknown, any, unknown>[]> = R extends readonly [] ? unknown : UnionToIntersection<{
84
+ [K in keyof R]: R[K] extends CfastPlugin<infer N, infer P, unknown, unknown> ? {
85
+ [Key in N]: P;
86
+ } : never;
87
+ }[number]>;
54
88
  /**
55
89
  * Minimal plugin shape used internally for runtime iteration in `buildApp`.
56
90
  *
@@ -68,6 +102,13 @@ type CfastPlugin<TName extends string = string, TProvides = unknown, TRequires =
68
102
  type RuntimePlugin = {
69
103
  /** Plugin name used as the namespace key. */
70
104
  name: string;
105
+ /**
106
+ * Optional dependencies declared via `definePlugin({ requires: [...] })`.
107
+ *
108
+ * `app.use(plugin)` walks this list and verifies each entry is already
109
+ * registered, throwing `CfastConfigError` immediately if not.
110
+ */
111
+ requires?: readonly CfastPlugin<string, unknown, any, unknown>[];
71
112
  /**
72
113
  * Setup function called per-request.
73
114
  *
@@ -151,4 +192,4 @@ type App<TSchema extends Schema, TPermissions extends Permissions, TPluginContex
151
192
  permissions: TPermissions;
152
193
  };
153
194
 
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 };
195
+ export type { App as A, CreateAppConfig as C, PluginSetupContext as P, RequiresFromPlugins as R, CfastPlugin as a, AppContext as b, PluginProvides as c, RouteArgs as d, RuntimePlugin as e };
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
- - **Compile-time dependencies**: Dependent plugins declare requirements via `definePlugin<TRequires>()` (curried form). TypeScript catches missing dependencies at the `.use()` call site.
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) -- direct form, full inference
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,67 @@ definePlugin({
45
45
  client?: { /* client-side values for useApp() */ },
46
46
  }): CfastPlugin
47
47
 
48
- // Dependent plugin -- curried form, specify TRequires explicitly
49
- definePlugin<AuthPluginProvides>()({
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; // typed from AuthPluginProvides
53
- return { client: createDb({}) };
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
- ### Plugin type token
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
- // Multiple dependencies -- intersect type tokens:
66
- definePlugin<AuthPluginProvides & DbPluginProvides>()({ ... })
100
+ definePlugin<AuthPluginProvides>()({
101
+ name: "db",
102
+ setup(ctx) { ctx.auth.user; return { /* ... */ }; },
103
+ });
67
104
  ```
68
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
+
69
109
  ### Client (`@cfast/core/client`)
70
110
 
71
111
  ```typescript
@@ -160,7 +200,7 @@ export const { createAction, composeActions } = createActions({
160
200
 
161
201
  ## Common Mistakes
162
202
 
163
- - Registering plugins in the wrong order -- `dbPlugin` depends on `authPlugin`, so auth must be `.use()`'d first. TypeScript catches this at compile time.
203
+ - 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
204
  - Calling `app.context()` before `app.init()` -- env is not validated yet, will throw.
165
205
  - Calling `useApp()` outside `<app.Provider>` -- throws a context error.
166
206
  - 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.1",
3
+ "version": "0.1.2",
4
4
  "description": "App composition layer with plugin system for @cfast/* packages",
5
5
  "keywords": [
6
6
  "cfast",
@@ -37,8 +37,8 @@
37
37
  "access": "public"
38
38
  },
39
39
  "dependencies": {
40
- "@cfast/env": "0.1.0",
41
- "@cfast/permissions": "0.1.0"
40
+ "@cfast/permissions": "0.2.0",
41
+ "@cfast/env": "0.1.1"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "react": ">=18"