@chidchanun/bcp 0.2.14 → 0.2.15

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.
@@ -0,0 +1,391 @@
1
+ # Plugin & Module Platform
2
+
3
+ BCP `0.2.15` adds the server-only `bcp/plugins` entrypoint for composing framework/application extensions with explicit dependencies and lifecycle control.
4
+
5
+ ## Import
6
+
7
+ ```ts
8
+ import {
9
+ createPluginHost,
10
+ defineModule,
11
+ definePlugin,
12
+ } from "bcp/plugins";
13
+ ```
14
+
15
+ `bcp/plugins` is server-only and cannot be imported from page/client bundles.
16
+
17
+ ## Define a plugin
18
+
19
+ ```ts
20
+ export const databasePlugin =
21
+ definePlugin({
22
+ name: "database",
23
+
24
+ async setup(context) {
25
+ context.services.provide(
26
+ "database",
27
+ db
28
+ );
29
+ },
30
+
31
+ async start() {
32
+ await db.connect();
33
+ },
34
+
35
+ async stop() {
36
+ await db.disconnect();
37
+ },
38
+ });
39
+ ```
40
+
41
+ A plugin can expose four lifecycle hooks:
42
+
43
+ ```text
44
+ setup()
45
+ start()
46
+ stop()
47
+ dispose()
48
+ ```
49
+
50
+ `setup()` prepares dependency wiring and shared services. `start()` begins runtime work. `stop()` shuts active work down. `dispose()` releases setup-level resources when the host closes.
51
+
52
+ ## Dependency ordering
53
+
54
+ ```ts
55
+ const jobsPlugin =
56
+ definePlugin({
57
+ name: "jobs",
58
+ requires: [
59
+ "database",
60
+ ],
61
+ });
62
+ ```
63
+
64
+ The host resolves required dependencies before dependents:
65
+
66
+ ```text
67
+ database
68
+
69
+ jobs
70
+
71
+ workflow
72
+ ```
73
+
74
+ Startup follows this order. Shutdown and disposal run in reverse order.
75
+
76
+ Missing required dependencies throw `PluginDependencyError` before lifecycle execution begins.
77
+
78
+ Dependency cycles are rejected:
79
+
80
+ ```text
81
+ a -> b -> c -> a
82
+ ```
83
+
84
+ Optional dependencies participate in ordering only when they are registered:
85
+
86
+ ```ts
87
+ definePlugin({
88
+ name: "metrics-addon",
89
+ optional: [
90
+ "observability",
91
+ ],
92
+ });
93
+ ```
94
+
95
+ ## Create a host
96
+
97
+ ```ts
98
+ const host =
99
+ createPluginHost({
100
+ plugins: [
101
+ databasePlugin,
102
+ jobsPlugin,
103
+ ],
104
+ });
105
+
106
+ await host.start();
107
+ ```
108
+
109
+ For graceful shutdown:
110
+
111
+ ```ts
112
+ await host.stop();
113
+ await host.close();
114
+ ```
115
+
116
+ `close()` is idempotent. If the host is still started it stops the runtime before disposal.
117
+
118
+ ## Modules
119
+
120
+ A module is a named bundle of plugin definitions.
121
+
122
+ ```ts
123
+ export const backendModule =
124
+ defineModule({
125
+ name: "backend",
126
+ plugins: [
127
+ databasePlugin,
128
+ jobsPlugin,
129
+ workflowPlugin,
130
+ ],
131
+ });
132
+
133
+ const host =
134
+ createPluginHost({
135
+ modules: [
136
+ backendModule,
137
+ ],
138
+ });
139
+ ```
140
+
141
+ Modules do not create a second lifecycle system. They only group plugins; dependency resolution still occurs globally across the host.
142
+
143
+ You can also register before setup begins:
144
+
145
+ ```ts
146
+ host.use(databasePlugin);
147
+ host.use(backendModule);
148
+ ```
149
+
150
+ Registration is locked after setup starts so runtime dependency graphs cannot change underneath active plugins.
151
+
152
+ ## Typed plugin configuration
153
+
154
+ A plugin can parse its own configuration:
155
+
156
+ ```ts
157
+ const httpPlugin =
158
+ definePlugin<{
159
+ port: number;
160
+ }>({
161
+ name: "http",
162
+
163
+ config: {
164
+ port: 3000,
165
+ },
166
+
167
+ schema: {
168
+ parse(value) {
169
+ const port =
170
+ Number(
171
+ (
172
+ value as {
173
+ port?: unknown;
174
+ }
175
+ )?.port
176
+ );
177
+
178
+ if (!Number.isInteger(port)) {
179
+ throw new Error(
180
+ "port must be an integer"
181
+ );
182
+ }
183
+
184
+ return {
185
+ port,
186
+ };
187
+ },
188
+ },
189
+
190
+ setup(context) {
191
+ console.log(
192
+ context.config.port
193
+ );
194
+ },
195
+ });
196
+ ```
197
+
198
+ Application-level host configuration overrides the plugin default:
199
+
200
+ ```ts
201
+ const host =
202
+ createPluginHost({
203
+ plugins: [
204
+ httpPlugin,
205
+ ],
206
+ configs: {
207
+ http: {
208
+ port: 8080,
209
+ },
210
+ },
211
+ });
212
+ ```
213
+
214
+ A schema may be an object with `parse()` or a parser function.
215
+
216
+ ## Shared service registry
217
+
218
+ Plugins can publish typed application services:
219
+
220
+ ```ts
221
+ setup(context) {
222
+ context.services.provide(
223
+ "mailer",
224
+ mailer
225
+ );
226
+ }
227
+ ```
228
+
229
+ A dependent plugin can consume the service:
230
+
231
+ ```ts
232
+ setup(context) {
233
+ const mailer =
234
+ context.services.get<Mailer>(
235
+ "mailer"
236
+ );
237
+ }
238
+ ```
239
+
240
+ Available operations:
241
+
242
+ ```text
243
+ provide()
244
+ get()
245
+ optional()
246
+ has()
247
+ delete()
248
+ keys()
249
+ ```
250
+
251
+ Duplicate `provide()` calls are rejected unless `{ replace: true }` is explicit.
252
+
253
+ String and Symbol service keys are supported.
254
+
255
+ ## Asynchronous extension hooks
256
+
257
+ The host also exposes a small asynchronous hook bus:
258
+
259
+ ```ts
260
+ const unsubscribe =
261
+ context.hooks.on<{
262
+ userId: number;
263
+ }>(
264
+ "user.created",
265
+ async event => {
266
+ await sendWelcomeEmail(
267
+ event.userId
268
+ );
269
+ }
270
+ );
271
+ ```
272
+
273
+ Emit from another plugin:
274
+
275
+ ```ts
276
+ await context.hooks.emit(
277
+ "user.created",
278
+ {
279
+ userId: 42,
280
+ }
281
+ );
282
+ ```
283
+
284
+ Handlers run in registration order and are awaited. The hook bus is intended for in-process extension points, not durable event delivery.
285
+
286
+ Use `bcp/events` for durable integration events that must survive process failure.
287
+
288
+ ## Lifecycle failure behavior
289
+
290
+ If a plugin fails during `start()`, plugins that already started in that transition are stopped in reverse order before `PluginLifecycleError` is propagated.
291
+
292
+ ```text
293
+ database start ✅
294
+ jobs start ✅
295
+ workflow start ❌
296
+
297
+ rollback:
298
+ jobs stop
299
+ database stop
300
+ ```
301
+
302
+ This prevents partially-started application runtimes from being left active after a startup failure.
303
+
304
+ Lifecycle records can be inspected:
305
+
306
+ ```ts
307
+ host.plugin("jobs");
308
+ host.plugins();
309
+ ```
310
+
311
+ States include:
312
+
313
+ ```text
314
+ registered
315
+ setting-up
316
+ ready
317
+ starting
318
+ started
319
+ stopping
320
+ stopped
321
+ failed
322
+ ```
323
+
324
+ ## Relationship to existing BCP platforms
325
+
326
+ `bcp/plugins` composes services; it does not replace their own durability/lifecycle contracts.
327
+
328
+ ```text
329
+ Plugin Host
330
+ |
331
+ +-- Database plugin ------> bcp/database
332
+ +-- Jobs plugin ----------> bcp/jobs
333
+ +-- Workflow plugin ------> bcp/workflow
334
+ +-- Events plugin --------> bcp/events
335
+ +-- Realtime plugin ------> bcp/realtime
336
+ +-- Observability plugin -> bcp/observability
337
+ ```
338
+
339
+ A plugin can wrap any existing BCP subsystem and expose the resulting instance through the service registry.
340
+
341
+ ## Production guidance
342
+
343
+ Keep plugin names stable because dependency declarations reference names.
344
+
345
+ Prefer explicit required dependencies instead of relying on registration order.
346
+
347
+ Use plugin configuration parsing near the plugin boundary so invalid configuration fails before startup.
348
+
349
+ Use `dispose()` for resources created during setup, and `stop()` for active runtime processes such as workers, schedulers and network listeners.
350
+
351
+ Do not use the hook bus as a durable message broker. Use Transactional Outbox/Jobs for delivery guarantees.
352
+
353
+ ## 0.2.15 scope
354
+
355
+ Included:
356
+
357
+ ```text
358
+ bcp/plugins
359
+ definePlugin()
360
+ defineModule()
361
+ createPluginHost()
362
+ required dependencies
363
+ optional dependencies
364
+ topological ordering
365
+ cycle detection
366
+ setup/start/stop/dispose lifecycle
367
+ reverse shutdown
368
+ startup rollback
369
+ plugin state records
370
+ config parser/schema contract
371
+ host config overrides
372
+ service registry
373
+ async hook bus
374
+ server-only boundary
375
+ compiled plugins.mjs
376
+ unit tests
377
+ prepared package smoke
378
+ ```
379
+
380
+ Not included yet:
381
+
382
+ ```text
383
+ automatic npm plugin discovery
384
+ remote plugin loading
385
+ sandboxed/untrusted plugins
386
+ hot plugin replacement
387
+ plugin marketplace
388
+ CLI plugin install command
389
+ ```
390
+
391
+ Those can be added later without changing the core host contract.
@@ -0,0 +1,118 @@
1
+ # BCP Framework 0.2.15 — Plugin & Module Platform
2
+
3
+ **Release state:** unreleased
4
+
5
+ BCP `0.2.15` adds a server-only plugin/module composition layer through `bcp/plugins`.
6
+
7
+ ## Highlights
8
+
9
+ - new `bcp/plugins` public entrypoint,
10
+ - `definePlugin()` plugin definitions,
11
+ - `defineModule()` named plugin bundles,
12
+ - `createPluginHost()` lifecycle orchestration,
13
+ - required and optional plugin dependencies,
14
+ - deterministic topological dependency ordering,
15
+ - dependency cycle and missing-dependency errors,
16
+ - setup/start/stop/dispose lifecycle hooks,
17
+ - reverse shutdown/disposal ordering,
18
+ - startup rollback when a dependent plugin fails,
19
+ - inspectable plugin runtime state,
20
+ - plugin-local config parser/schema contract,
21
+ - host-level config overrides,
22
+ - shared string/Symbol service registry,
23
+ - asynchronous in-process hook bus,
24
+ - server-only browser boundary,
25
+ - compiled `plugins.mjs` npm runtime,
26
+ - unit and prepared-package smoke coverage.
27
+
28
+ ## Public API
29
+
30
+ ```ts
31
+ import {
32
+ createPluginHookBus,
33
+ createPluginHost,
34
+ createPluginServiceRegistry,
35
+ defineModule,
36
+ definePlugin,
37
+ PluginDependencyError,
38
+ PluginLifecycleError,
39
+ } from "bcp/plugins";
40
+ ```
41
+
42
+ ## Dependency lifecycle
43
+
44
+ ```text
45
+ database
46
+
47
+ jobs
48
+
49
+ workflow
50
+ ```
51
+
52
+ Startup follows dependency order. Stop/dispose runs in reverse order.
53
+
54
+ If a plugin fails during startup, already-started plugins from that transition are stopped before the failure is propagated.
55
+
56
+ ## Modules
57
+
58
+ Modules are composition bundles rather than a second runtime:
59
+
60
+ ```ts
61
+ const backend =
62
+ defineModule({
63
+ name: "backend",
64
+ plugins: [
65
+ databasePlugin,
66
+ jobsPlugin,
67
+ workflowPlugin,
68
+ ],
69
+ });
70
+ ```
71
+
72
+ All plugins still share one host-level dependency graph.
73
+
74
+ ## Configuration
75
+
76
+ Plugin definitions can provide defaults and a parser/schema. `PluginHostOptions.configs` can override raw configuration per plugin before parsing.
77
+
78
+ Invalid configuration fails at the plugin setup boundary.
79
+
80
+ ## Extension services
81
+
82
+ `PluginServiceRegistry` provides explicit shared service lookup between plugins.
83
+
84
+ `PluginHookBus` provides awaited in-process extension hooks. It is not a durable event system; `bcp/events` remains the durable integration-event platform.
85
+
86
+ ## Package/runtime contract
87
+
88
+ The prepared npm package exposes:
89
+
90
+ ```text
91
+ bcp/plugins
92
+ types -> packages/client/src/plugins.ts
93
+ browser -> packages/client/src/server-only.browser.mjs
94
+ default -> packages/client/src/plugins.mjs
95
+ ```
96
+
97
+ `plugins.mjs` is compiled during package preparation.
98
+
99
+ ## Compatibility
100
+
101
+ `0.2.15` has no intentional breaking changes from `0.2.14`.
102
+
103
+ Existing BCP entrypoints remain supported.
104
+
105
+ ## Validation
106
+
107
+ Before publishing:
108
+
109
+ ```bash
110
+ npm run typecheck
111
+ npm run test:unit
112
+ npm run test:integration
113
+ npm run test:e2e
114
+ npm run test:package
115
+ npm run rc:check
116
+ ```
117
+
118
+ Do not tag or publish until the exact final release commit passes the full RC sequence.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chidchanun/bcp",
3
- "version": "0.2.14",
3
+ "version": "0.2.15",
4
4
  "description": "BCP Framework - a React full-stack framework with file-based routing, SSR, APIs, middleware, islands, caching and standalone production builds.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -87,6 +87,11 @@
87
87
  "browser": "./packages/client/src/server-only.browser.mjs",
88
88
  "default": "./packages/client/src/testing.mjs"
89
89
  },
90
+ "./plugins": {
91
+ "types": "./packages/client/src/plugins.ts",
92
+ "browser": "./packages/client/src/server-only.browser.mjs",
93
+ "default": "./packages/client/src/plugins.mjs"
94
+ },
90
95
  "./observability": {
91
96
  "types": "./packages/client/src/observability.ts",
92
97
  "browser": "./packages/client/src/server-only.browser.mjs",
@@ -33,6 +33,7 @@ const SERVER_ONLY_IMPORTS =
33
33
  "bcp/events",
34
34
  "bcp/realtime",
35
35
  "bcp/testing",
36
+ "bcp/plugins",
36
37
  "bcp/observability",
37
38
  ]);
38
39