@c9up/ream 0.2.17 → 0.2.18

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.
Files changed (84) hide show
  1. package/dist/HttpKernel.d.ts +10 -0
  2. package/dist/HttpKernel.d.ts.map +1 -1
  3. package/dist/HttpKernel.js +61 -5
  4. package/dist/HttpKernel.js.map +1 -1
  5. package/dist/Ignitor.d.ts.map +1 -1
  6. package/dist/Ignitor.js +65 -15
  7. package/dist/Ignitor.js.map +1 -1
  8. package/dist/container/Container.d.ts.map +1 -1
  9. package/dist/container/Container.js +174 -45
  10. package/dist/container/Container.js.map +1 -1
  11. package/dist/container/types.d.ts +7 -0
  12. package/dist/container/types.d.ts.map +1 -1
  13. package/dist/dev/hmr.d.ts +37 -0
  14. package/dist/dev/hmr.d.ts.map +1 -0
  15. package/dist/dev/hmr.js +72 -0
  16. package/dist/dev/hmr.js.map +1 -0
  17. package/dist/graphql/GraphQLProvider.d.ts +14 -3
  18. package/dist/graphql/GraphQLProvider.d.ts.map +1 -1
  19. package/dist/graphql/GraphQLProvider.js +14 -3
  20. package/dist/graphql/GraphQLProvider.js.map +1 -1
  21. package/dist/hot.d.ts +29 -0
  22. package/dist/hot.d.ts.map +1 -0
  23. package/dist/hot.js +135 -0
  24. package/dist/hot.js.map +1 -0
  25. package/dist/http/Exception.d.ts +2 -2
  26. package/dist/http/Exception.d.ts.map +1 -1
  27. package/dist/http/Exception.js +3 -3
  28. package/dist/http/Exception.js.map +1 -1
  29. package/dist/http/Request.d.ts +11 -0
  30. package/dist/http/Request.d.ts.map +1 -1
  31. package/dist/http/Request.js +40 -0
  32. package/dist/http/Request.js.map +1 -1
  33. package/dist/http/RequestValidator.d.ts +80 -0
  34. package/dist/http/RequestValidator.d.ts.map +1 -0
  35. package/dist/http/RequestValidator.js +93 -0
  36. package/dist/http/RequestValidator.js.map +1 -0
  37. package/dist/index.d.ts +3 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +2 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/middleware/Pipeline.d.ts +2 -2
  42. package/dist/middleware/Pipeline.js +2 -2
  43. package/dist/middleware/Pipeline.js.map +1 -1
  44. package/dist/migrations/MigrationRegistry.d.ts +16 -0
  45. package/dist/migrations/MigrationRegistry.d.ts.map +1 -1
  46. package/dist/migrations/MigrationRegistry.js +24 -0
  47. package/dist/migrations/MigrationRegistry.js.map +1 -1
  48. package/dist/openapi/OpenApiGenerator.d.ts +16 -0
  49. package/dist/openapi/OpenApiGenerator.d.ts.map +1 -1
  50. package/dist/openapi/OpenApiGenerator.js +76 -4
  51. package/dist/openapi/OpenApiGenerator.js.map +1 -1
  52. package/dist/openapi/OpenApiProvider.d.ts +16 -5
  53. package/dist/openapi/OpenApiProvider.d.ts.map +1 -1
  54. package/dist/openapi/OpenApiProvider.js +21 -6
  55. package/dist/openapi/OpenApiProvider.js.map +1 -1
  56. package/dist/rpc/RpcProvider.d.ts +13 -2
  57. package/dist/rpc/RpcProvider.d.ts.map +1 -1
  58. package/dist/rpc/RpcProvider.js +13 -2
  59. package/dist/rpc/RpcProvider.js.map +1 -1
  60. package/dist/storage/StaticProvider.d.ts +12 -2
  61. package/dist/storage/StaticProvider.d.ts.map +1 -1
  62. package/dist/storage/StaticProvider.js +12 -2
  63. package/dist/storage/StaticProvider.js.map +1 -1
  64. package/events.win32-x64-msvc.node +0 -0
  65. package/index.win32-x64-msvc.node +0 -0
  66. package/package.json +14 -2
  67. package/scheduler.win32-x64-msvc.node +0 -0
  68. package/src/HttpKernel.ts +87 -5
  69. package/src/Ignitor.ts +70 -16
  70. package/src/container/Container.ts +178 -48
  71. package/src/container/types.ts +11 -0
  72. package/src/dev/hmr.ts +83 -0
  73. package/src/graphql/GraphQLProvider.ts +14 -3
  74. package/src/hot.ts +165 -0
  75. package/src/http/Exception.ts +4 -4
  76. package/src/http/Request.ts +54 -0
  77. package/src/http/RequestValidator.ts +143 -0
  78. package/src/index.ts +8 -1
  79. package/src/middleware/Pipeline.ts +4 -4
  80. package/src/migrations/MigrationRegistry.ts +23 -0
  81. package/src/openapi/OpenApiGenerator.ts +84 -4
  82. package/src/openapi/OpenApiProvider.ts +21 -6
  83. package/src/rpc/RpcProvider.ts +13 -2
  84. package/src/storage/StaticProvider.ts +12 -2
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * `OpenApiProvider` — serves an auto-generated OpenAPI 3.1 spec + Swagger UI.
3
3
  *
4
- * `boot()` mounts a global middleware that, on the first GET to `/api-docs`
4
+ * `start()` mounts a global middleware that, on the first GET to `/api-docs`
5
5
  * (JSON) or `/docs` (Swagger UI), lazily generates the spec from the router's
6
- * registered routes — so every route registered during boot/start is included
7
- * (the spec must NOT be generated eagerly at boot, before other providers have
8
- * registered their routes). Opt-out via `config.openapi.enabled = false`.
6
+ * registered routes — so every route registered while the application starts
7
+ * up is included (the spec must NOT be generated eagerly, before the other
8
+ * providers and the preloads have registered their routes). Opt-out via
9
+ * `config.openapi.enabled = false`.
9
10
  *
10
11
  * @implements Story 24.4
11
12
  */
@@ -31,7 +32,17 @@ export interface OpenApiProviderOptions {
31
32
  export declare class OpenApiProvider extends Provider {
32
33
  #private;
33
34
  constructor(app: AppContext, options?: OpenApiProviderOptions);
34
- boot(): Promise<void>;
35
+ /**
36
+ * Routes and server middleware go in `start`, not `boot`.
37
+ *
38
+ * Upstream documents that phase for exactly this, and the order is what makes
39
+ * it matter: providers boot, then providers START, then the preloads run —
40
+ * and the preloads are where an application writes its own routes and kernel.
41
+ * Mounted in `boot`, a framework route landed ahead of every application
42
+ * route, so an overlapping path was answered by the framework rather than by
43
+ * the app that meant to override it.
44
+ */
45
+ start(): Promise<void>;
35
46
  }
36
47
  export default OpenApiProvider;
37
48
  //# sourceMappingURL=OpenApiProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"OpenApiProvider.d.ts","sourceRoot":"","sources":["../../src/openapi/OpenApiProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAGzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAGxD,MAAM,WAAW,iBAAiB;IAChC,6DAA6D;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,8CAA8C;IAC9C,SAAS,CAAC,EAAE,gBAAgB,CAAA;CAC7B;AAED,qBAAa,eAAgB,SAAQ,QAAQ;;gBAG/B,GAAG,EAAE,UAAU,EAAE,OAAO,GAAE,sBAA2B;IAKlD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAqBrC;AAGD,eAAe,eAAe,CAAA"}
1
+ {"version":3,"file":"OpenApiProvider.d.ts","sourceRoot":"","sources":["../../src/openapi/OpenApiProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAGzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAGxD,MAAM,WAAW,iBAAiB;IAChC,6DAA6D;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,8CAA8C;IAC9C,SAAS,CAAC,EAAE,gBAAgB,CAAA;CAC7B;AAED,qBAAa,eAAgB,SAAQ,QAAQ;;gBAG/B,GAAG,EAAE,UAAU,EAAE,OAAO,GAAE,sBAA2B;IAKjE;;;;;;;;;OASG;IACY,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAyBtC;AAGD,eAAe,eAAe,CAAA"}
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * `OpenApiProvider` — serves an auto-generated OpenAPI 3.1 spec + Swagger UI.
3
3
  *
4
- * `boot()` mounts a global middleware that, on the first GET to `/api-docs`
4
+ * `start()` mounts a global middleware that, on the first GET to `/api-docs`
5
5
  * (JSON) or `/docs` (Swagger UI), lazily generates the spec from the router's
6
- * registered routes — so every route registered during boot/start is included
7
- * (the spec must NOT be generated eagerly at boot, before other providers have
8
- * registered their routes). Opt-out via `config.openapi.enabled = false`.
6
+ * registered routes — so every route registered while the application starts
7
+ * up is included (the spec must NOT be generated eagerly, before the other
8
+ * providers and the preloads have registered their routes). Opt-out via
9
+ * `config.openapi.enabled = false`.
9
10
  *
10
11
  * @implements Story 24.4
11
12
  */
@@ -18,7 +19,17 @@ export class OpenApiProvider extends Provider {
18
19
  super(app);
19
20
  this.#override = options.generator;
20
21
  }
21
- async boot() {
22
+ /**
23
+ * Routes and server middleware go in `start`, not `boot`.
24
+ *
25
+ * Upstream documents that phase for exactly this, and the order is what makes
26
+ * it matter: providers boot, then providers START, then the preloads run —
27
+ * and the preloads are where an application writes its own routes and kernel.
28
+ * Mounted in `boot`, a framework route landed ahead of every application
29
+ * route, so an overlapping path was answered by the framework rather than by
30
+ * the app that meant to override it.
31
+ */
32
+ async start() {
22
33
  const config = this.app.config.get('openapi') ?? {};
23
34
  if (config.enabled === false)
24
35
  return;
@@ -33,10 +44,14 @@ export class OpenApiProvider extends Provider {
33
44
  }
34
45
  async #buildGenerator(config) {
35
46
  const router = await this.app.container.make('router');
36
- return new OpenApiGenerator(router, {
47
+ const generator = new OpenApiGenerator(router, {
37
48
  title: config.title ?? 'API',
38
49
  version: config.version ?? '1.0.0',
39
50
  });
51
+ // The routes already name their validators; ask each one to describe
52
+ // itself instead of making the app repeat the shape by hand.
53
+ await generator.hydrateSchemas((token) => this.app.container.make(token));
54
+ return generator;
40
55
  }
41
56
  }
42
57
  // Default export so reamrc's provider loader can `() => import('@c9up/ream/<feature>/provider')` (resolves to { default }), matching events/rpc. Named export above stays.
@@ -1 +1 @@
1
- {"version":3,"file":"OpenApiProvider.js","sourceRoot":"","sources":["../../src/openapi/OpenApiProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAGzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAoB1D,MAAM,OAAO,eAAgB,SAAQ,QAAQ;IAClC,SAAS,CAAmB;IAErC,YAAY,GAAe,EAAE,UAAkC,EAAE;QAC/D,KAAK,CAAC,GAAG,CAAC,CAAA;QACV,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAA;IACpC,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAoB,SAAS,CAAC,IAAI,EAAE,CAAA;QACtE,IAAI,MAAM,CAAC,OAAO,KAAK,KAAK;YAAE,OAAM;QAEpC,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,CAAC,MAAM,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,CAAA;QACxE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAS,QAAQ,CAAC,CAAA;QAC9D,MAAM,UAAU,GAAG,IAAI,iBAAiB,CAAC;YACvC,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,IAAI,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE;SACjC,CAAC,CAAA;QACF,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;IAC3D,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,MAAyB;QAC7C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAS,QAAQ,CAAC,CAAA;QAC9D,OAAO,IAAI,gBAAgB,CAAC,MAAM,EAAE;YAClC,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,KAAK;YAC5B,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,OAAO;SACnC,CAAC,CAAA;IACJ,CAAC;CACF;AAED,2KAA2K;AAC3K,eAAe,eAAe,CAAA"}
1
+ {"version":3,"file":"OpenApiProvider.js","sourceRoot":"","sources":["../../src/openapi/OpenApiProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAGzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAoB1D,MAAM,OAAO,eAAgB,SAAQ,QAAQ;IAClC,SAAS,CAAmB;IAErC,YAAY,GAAe,EAAE,UAAkC,EAAE;QAC/D,KAAK,CAAC,GAAG,CAAC,CAAA;QACV,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAA;IACpC,CAAC;IAED;;;;;;;;;OASG;IACM,KAAK,CAAC,KAAK;QAClB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAoB,SAAS,CAAC,IAAI,EAAE,CAAA;QACtE,IAAI,MAAM,CAAC,OAAO,KAAK,KAAK;YAAE,OAAM;QAEpC,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,CAAC,MAAM,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,CAAA;QACxE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAS,QAAQ,CAAC,CAAA;QAC9D,MAAM,UAAU,GAAG,IAAI,iBAAiB,CAAC;YACvC,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,IAAI,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE;SACjC,CAAC,CAAA;QACF,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;IAC3D,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,MAAyB;QAC7C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAS,QAAQ,CAAC,CAAA;QAC9D,MAAM,SAAS,GAAG,IAAI,gBAAgB,CAAC,MAAM,EAAE;YAC7C,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,KAAK;YAC5B,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,OAAO;SACnC,CAAC,CAAA;QACF,qEAAqE;QACrE,6DAA6D;QAC7D,MAAM,SAAS,CAAC,cAAc,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAU,KAAK,CAAC,CAAC,CAAA;QAClF,OAAO,SAAS,CAAA;IAClB,CAAC;CACF;AAED,2KAA2K;AAC3K,eAAe,eAAe,CAAA"}
@@ -9,8 +9,9 @@
9
9
  * rpc.method('task.validate', handler).guard('jwt')
10
10
  * rpc.namespace('user', UserController)
11
11
  *
12
- * - `boot()` — mount `POST <config.rpc.path ?? '/rpc'>` on the core router,
12
+ * - `start()` — mount `POST <config.rpc.path ?? '/rpc'>` on the core router,
13
13
  * dispatching every request body (single or batch) to `RpcRouter.handle()`.
14
+ * In `start`, which is the phase upstream documents for routes.
14
15
  *
15
16
  * @implements Story 24.1, 24.2
16
17
  */
@@ -38,7 +39,17 @@ export declare class RpcProvider extends Provider {
38
39
  readonly rpc: RpcRouter;
39
40
  constructor(app: AppContext, options?: RpcProviderOptions);
40
41
  register(): void;
41
- boot(): Promise<void>;
42
+ /**
43
+ * Routes and server middleware go in `start`, not `boot`.
44
+ *
45
+ * Upstream documents that phase for exactly this, and the order is what makes
46
+ * it matter: providers boot, then providers START, then the preloads run —
47
+ * and the preloads are where an application writes its own routes and kernel.
48
+ * Mounted in `boot`, a framework route landed ahead of every application
49
+ * route, so an overlapping path was answered by the framework rather than by
50
+ * the app that meant to override it.
51
+ */
52
+ start(): Promise<void>;
42
53
  }
43
54
  export default RpcProvider;
44
55
  //# sourceMappingURL=RpcProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RpcProvider.d.ts","sourceRoot":"","sources":["../../src/rpc/RpcProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE1C,MAAM,WAAW,kBAAkB;IACjC,yDAAyD;IACzD,MAAM,CAAC,EAAE,SAAS,CAAA;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,qBAAa,WAAY,SAAQ,QAAQ;;IACvC,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAA;gBAGX,GAAG,EAAE,UAAU,EAAE,OAAO,GAAE,kBAAuB;IAKpD,QAAQ,IAAI,IAAI;IAmBV,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAarC;AAMD,eAAe,WAAW,CAAA"}
1
+ {"version":3,"file":"RpcProvider.d.ts","sourceRoot":"","sources":["../../src/rpc/RpcProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE1C,MAAM,WAAW,kBAAkB;IACjC,yDAAyD;IACzD,MAAM,CAAC,EAAE,SAAS,CAAA;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,qBAAa,WAAY,SAAQ,QAAQ;;IACvC,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAA;gBAGX,GAAG,EAAE,UAAU,EAAE,OAAO,GAAE,kBAAuB;IAKpD,QAAQ,IAAI,IAAI;IAmBzB;;;;;;;;;OASG;IACY,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAatC;AAMD,eAAe,WAAW,CAAA"}
@@ -9,8 +9,9 @@
9
9
  * rpc.method('task.validate', handler).guard('jwt')
10
10
  * rpc.namespace('user', UserController)
11
11
  *
12
- * - `boot()` — mount `POST <config.rpc.path ?? '/rpc'>` on the core router,
12
+ * - `start()` — mount `POST <config.rpc.path ?? '/rpc'>` on the core router,
13
13
  * dispatching every request body (single or batch) to `RpcRouter.handle()`.
14
+ * In `start`, which is the phase upstream documents for routes.
14
15
  *
15
16
  * @implements Story 24.1, 24.2
16
17
  */
@@ -39,7 +40,17 @@ export class RpcProvider extends Provider {
39
40
  container.singleton('rpc', () => this.rpc);
40
41
  this.#registered = true;
41
42
  }
42
- async boot() {
43
+ /**
44
+ * Routes and server middleware go in `start`, not `boot`.
45
+ *
46
+ * Upstream documents that phase for exactly this, and the order is what makes
47
+ * it matter: providers boot, then providers START, then the preloads run —
48
+ * and the preloads are where an application writes its own routes and kernel.
49
+ * Mounted in `boot`, a framework route landed ahead of every application
50
+ * route, so an overlapping path was answered by the framework rather than by
51
+ * the app that meant to override it.
52
+ */
53
+ async start() {
43
54
  const config = this.app.config.get('rpc');
44
55
  const path = config?.path ?? '/rpc';
45
56
  const router = await this.app.container.make('router');
@@ -1 +1 @@
1
- {"version":3,"file":"RpcProvider.js","sourceRoot":"","sources":["../../src/rpc/RpcProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAA;AAGlD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAoB1C,MAAM,OAAO,WAAY,SAAQ,QAAQ;IAC9B,GAAG,CAAW;IACvB,WAAW,GAAG,KAAK,CAAA;IAEnB,YAAY,GAAe,EAAE,UAA8B,EAAE;QAC3D,KAAK,CAAC,GAAG,CAAC,CAAA;QACV,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,MAAM,IAAI,IAAI,SAAS,EAAE,CAAA;IAC9C,CAAC;IAEQ,QAAQ;QACf,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,CAAA;QACpC,IAAI,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,0DAA0D;YAC1D,8DAA8D;YAC9D,IAAI,IAAI,CAAC,WAAW;gBAAE,OAAM;YAC5B,MAAM,IAAI,SAAS,CACjB,mCAAmC,EACnC,gEAAgE,EAChE;gBACE,IAAI,EAAE,kGAAkG;aACzG,CACF,CAAA;QACH,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,SAAS,CAAC,CAAA;QAChC,SAAS,CAAC,SAAS,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QAC1C,IAAI,CAAC,WAAW,GAAG,IAAI,CAAA;IACzB,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAY,KAAK,CAAC,CAAA;QACpD,MAAM,IAAI,GAAG,MAAM,EAAE,IAAI,IAAI,MAAM,CAAA;QACnC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAS,QAAQ,CAAC,CAAA;QAC9D,0EAA0E;QAC1E,2DAA2D;QAC3D,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,GAAgB,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;QAC3E,sEAAsE;QACtE,wEAAwE;QACxE,gCAAgC;QAChC,MAAM,MAAM,GAAG,MAAM,EAAE,MAAM,IAAI,EAAE,CAAA;QACnC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;YAAE,KAAK,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,CAAA;IAC/C,CAAC;CACF;AAED,uEAAuE;AACvE,+EAA+E;AAC/E,gFAAgF;AAChF,4BAA4B;AAC5B,eAAe,WAAW,CAAA"}
1
+ {"version":3,"file":"RpcProvider.js","sourceRoot":"","sources":["../../src/rpc/RpcProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAA;AAGlD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAoB1C,MAAM,OAAO,WAAY,SAAQ,QAAQ;IAC9B,GAAG,CAAW;IACvB,WAAW,GAAG,KAAK,CAAA;IAEnB,YAAY,GAAe,EAAE,UAA8B,EAAE;QAC3D,KAAK,CAAC,GAAG,CAAC,CAAA;QACV,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,MAAM,IAAI,IAAI,SAAS,EAAE,CAAA;IAC9C,CAAC;IAEQ,QAAQ;QACf,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,CAAA;QACpC,IAAI,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,0DAA0D;YAC1D,8DAA8D;YAC9D,IAAI,IAAI,CAAC,WAAW;gBAAE,OAAM;YAC5B,MAAM,IAAI,SAAS,CACjB,mCAAmC,EACnC,gEAAgE,EAChE;gBACE,IAAI,EAAE,kGAAkG;aACzG,CACF,CAAA;QACH,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,SAAS,CAAC,CAAA;QAChC,SAAS,CAAC,SAAS,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QAC1C,IAAI,CAAC,WAAW,GAAG,IAAI,CAAA;IACzB,CAAC;IAED;;;;;;;;;OASG;IACM,KAAK,CAAC,KAAK;QAClB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAY,KAAK,CAAC,CAAA;QACpD,MAAM,IAAI,GAAG,MAAM,EAAE,IAAI,IAAI,MAAM,CAAA;QACnC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAS,QAAQ,CAAC,CAAA;QAC9D,0EAA0E;QAC1E,2DAA2D;QAC3D,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,GAAgB,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;QAC3E,sEAAsE;QACtE,wEAAwE;QACxE,gCAAgC;QAChC,MAAM,MAAM,GAAG,MAAM,EAAE,MAAM,IAAI,EAAE,CAAA;QACnC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;YAAE,KAAK,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,CAAA;IAC/C,CAAC;CACF;AAED,uEAAuE;AACvE,+EAA+E;AAC/E,gFAAgF;AAChF,4BAA4B;AAC5B,eAAe,WAAW,CAAA"}
@@ -2,7 +2,7 @@
2
2
  * `StaticProvider` — serves static files from a directory.
3
3
  *
4
4
  * Opt-in: does nothing unless `config.static.root` is set (or a middleware is
5
- * injected for tests). When configured, `boot()` mounts a global
5
+ * injected for tests). When configured, `start()` mounts a global
6
6
  * `StaticMiddleware` (path-traversal + symlink-escape guarded) that serves
7
7
  * matching requests and falls through (`next()`) otherwise.
8
8
  *
@@ -21,7 +21,17 @@ export interface StaticProviderOptions {
21
21
  export declare class StaticProvider extends Provider {
22
22
  #private;
23
23
  constructor(app: AppContext, options?: StaticProviderOptions);
24
- boot(): Promise<void>;
24
+ /**
25
+ * Routes and server middleware go in `start`, not `boot`.
26
+ *
27
+ * Upstream documents that phase for exactly this, and the order is what makes
28
+ * it matter: providers boot, then providers START, then the preloads run —
29
+ * and the preloads are where an application writes its own routes and kernel.
30
+ * Mounted in `boot`, a framework route landed ahead of every application
31
+ * route, so an overlapping path was answered by the framework rather than by
32
+ * the app that meant to override it.
33
+ */
34
+ start(): Promise<void>;
25
35
  }
26
36
  export default StaticProvider;
27
37
  //# sourceMappingURL=StaticProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"StaticProvider.d.ts","sourceRoot":"","sources":["../../src/storage/StaticProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,OAAO,EAAqB,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAE3E,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,UAAU,CAAC,EAAE,gBAAgB,CAAA;CAC9B;AAED,qBAAa,cAAe,SAAQ,QAAQ;;gBAG9B,GAAG,EAAE,UAAU,EAAE,OAAO,GAAE,qBAA0B;IAKjD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAOrC;AAGD,eAAe,cAAc,CAAA"}
1
+ {"version":3,"file":"StaticProvider.d.ts","sourceRoot":"","sources":["../../src/storage/StaticProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,OAAO,EAAqB,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAE3E,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,UAAU,CAAC,EAAE,gBAAgB,CAAA;CAC9B;AAED,qBAAa,cAAe,SAAQ,QAAQ;;gBAG9B,GAAG,EAAE,UAAU,EAAE,OAAO,GAAE,qBAA0B;IAKhE;;;;;;;;;OASG;IACY,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAOtC;AAGD,eAAe,cAAc,CAAA"}
@@ -2,7 +2,7 @@
2
2
  * `StaticProvider` — serves static files from a directory.
3
3
  *
4
4
  * Opt-in: does nothing unless `config.static.root` is set (or a middleware is
5
- * injected for tests). When configured, `boot()` mounts a global
5
+ * injected for tests). When configured, `start()` mounts a global
6
6
  * `StaticMiddleware` (path-traversal + symlink-escape guarded) that serves
7
7
  * matching requests and falls through (`next()`) otherwise.
8
8
  *
@@ -19,7 +19,17 @@ export class StaticProvider extends Provider {
19
19
  super(app);
20
20
  this.#override = options.middleware;
21
21
  }
22
- async boot() {
22
+ /**
23
+ * Routes and server middleware go in `start`, not `boot`.
24
+ *
25
+ * Upstream documents that phase for exactly this, and the order is what makes
26
+ * it matter: providers boot, then providers START, then the preloads run —
27
+ * and the preloads are where an application writes its own routes and kernel.
28
+ * Mounted in `boot`, a framework route landed ahead of every application
29
+ * route, so an overlapping path was answered by the framework rather than by
30
+ * the app that meant to override it.
31
+ */
32
+ async start() {
23
33
  const config = this.app.config.get('static');
24
34
  const middleware = this.#override ?? (config?.root ? new StaticMiddleware(config) : undefined);
25
35
  if (!middleware)
@@ -1 +1 @@
1
- {"version":3,"file":"StaticProvider.js","sourceRoot":"","sources":["../../src/storage/StaticProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,OAAO,EAAqB,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAO3E,MAAM,OAAO,cAAe,SAAQ,QAAQ;IACjC,SAAS,CAAmB;IAErC,YAAY,GAAe,EAAE,UAAiC,EAAE;QAC9D,KAAK,CAAC,GAAG,CAAC,CAAA;QACV,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,UAAU,CAAA;IACrC,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAe,QAAQ,CAAC,CAAA;QAC1D,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;QAC9F,IAAI,CAAC,UAAU;YAAE,OAAM,CAAC,2BAA2B;QACnD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAS,QAAQ,CAAC,CAAA;QAC9D,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;IAC3D,CAAC;CACF;AAED,yKAAyK;AACzK,eAAe,cAAc,CAAA"}
1
+ {"version":3,"file":"StaticProvider.js","sourceRoot":"","sources":["../../src/storage/StaticProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,OAAO,EAAqB,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAO3E,MAAM,OAAO,cAAe,SAAQ,QAAQ;IACjC,SAAS,CAAmB;IAErC,YAAY,GAAe,EAAE,UAAiC,EAAE;QAC9D,KAAK,CAAC,GAAG,CAAC,CAAA;QACV,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,UAAU,CAAA;IACrC,CAAC;IAED;;;;;;;;;OASG;IACM,KAAK,CAAC,KAAK;QAClB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAe,QAAQ,CAAC,CAAA;QAC1D,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;QAC9F,IAAI,CAAC,UAAU;YAAE,OAAM,CAAC,2BAA2B;QACnD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAS,QAAQ,CAAC,CAAA;QAC9D,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;IAC3D,CAAC;CACF;AAED,yKAAyK;AACzK,eAAe,cAAc,CAAA"}
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c9up/ream",
3
- "version": "0.2.17",
3
+ "version": "0.2.18",
4
4
  "description": "Ream — Rust-powered Node.js application framework",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -202,6 +202,14 @@
202
202
  "./bodyparser_middleware": {
203
203
  "types": "./dist/bodyparser/BodyParserMiddleware.d.ts",
204
204
  "import": "./dist/bodyparser/BodyParserMiddleware.js"
205
+ },
206
+ "./hot": {
207
+ "types": "./dist/hot.d.ts",
208
+ "import": "./dist/hot.js"
209
+ },
210
+ "./dev/hmr": {
211
+ "types": "./dist/dev/hmr.d.ts",
212
+ "import": "./dist/dev/hmr.js"
205
213
  }
206
214
  },
207
215
  "files": [
@@ -225,7 +233,8 @@
225
233
  },
226
234
  "peerDependencies": {
227
235
  "@c9up/comet": "^0.1.0",
228
- "@c9up/quasar": "^0.1.0"
236
+ "@c9up/quasar": "^0.1.0",
237
+ "hot-hook": "^1.0.0"
229
238
  },
230
239
  "peerDependenciesMeta": {
231
240
  "@c9up/comet": {
@@ -233,6 +242,9 @@
233
242
  },
234
243
  "@c9up/quasar": {
235
244
  "optional": true
245
+ },
246
+ "hot-hook": {
247
+ "optional": true
236
248
  }
237
249
  },
238
250
  "devDependencies": {
Binary file
package/src/HttpKernel.ts CHANGED
@@ -32,6 +32,16 @@ export interface HttpKernelConfig {
32
32
  container?: Container
33
33
  exceptionHandler?: ExceptionHandler
34
34
  serverMiddleware?: MiddlewareFunction[]
35
+ /**
36
+ * A `<script>` appended to every HTML response, dev only.
37
+ *
38
+ * Set by the Ignitor when the application runs in development; `undefined`
39
+ * everywhere else, so a production response is byte-for-byte what the
40
+ * handler produced. It lives here rather than in a middleware because this
41
+ * is the one place every response passes through on its way out, whichever
42
+ * route or error handler produced it.
43
+ */
44
+ devReloadScript?: (nonce?: string) => string
35
45
  routerMiddleware?: MiddlewareFunction[]
36
46
  onError?: (error: unknown, ctx: HttpContext) => void
37
47
  debug?: boolean
@@ -345,7 +355,7 @@ export function createHttpKernel(
345
355
  correlationId,
346
356
  )
347
357
  }
348
- const serialized = await serializeResponse(ctx)
358
+ const serialized = await serializeResponse(ctx, config.devReloadScript)
349
359
  // The body is built; anything registered with `response.onFinish()` runs
350
360
  // now — after the answer is ready, before we hand it back.
351
361
  ctx.response.runFinishCallbacks()
@@ -401,7 +411,7 @@ export function createHttpKernel(
401
411
  correlationId,
402
412
  )
403
413
  }
404
- const serialized = await serializeResponse(ctx)
414
+ const serialized = await serializeResponse(ctx, config.devReloadScript)
405
415
  // The body is built; anything registered with `response.onFinish()` runs
406
416
  // now — after the answer is ready, before we hand it back.
407
417
  ctx.response.runFinishCallbacks()
@@ -452,15 +462,87 @@ function readControllerGuardMetadata(
452
462
  }
453
463
  }
454
464
 
455
- async function serializeResponse(ctx: HttpContext): Promise<HttpKernelResponse> {
465
+ async function serializeResponse(
466
+ ctx: HttpContext,
467
+ devReloadScript?: (nonce?: string) => string,
468
+ ): Promise<HttpKernelResponse> {
456
469
  // The terminal step, as AdonisJS' server calls `response.finish()`: it
457
470
  // stamps the request id and waits for a body still being produced.
458
471
  await ctx.response.finish()
459
472
  const streamId = ctx.response.getStreamId()
473
+ const headers = ctx.response.getHeaders()
474
+ const body = withDevReloadScript(
475
+ ctx.response.getBody(),
476
+ headers,
477
+ streamId,
478
+ devReloadScript === undefined ? undefined : devReloadScript(ctx.response.nonce),
479
+ )
460
480
  return {
461
481
  status: ctx.response.getStatus(),
462
- headers: ctx.response.getHeaders(),
463
- body: ctx.response.getBody(),
482
+ headers: reconcileRepresentation(headers, ctx.response.getBody(), body),
483
+ body,
464
484
  ...(streamId !== undefined ? { streamId } : {}),
465
485
  }
466
486
  }
487
+
488
+ /**
489
+ * Keep the representation headers describing the body that is actually sent.
490
+ *
491
+ * `content-length`, `etag` and the digest headers are computed from the body
492
+ * BEFORE the dev reload script is appended. Left alone they describe the
493
+ * previous one: a `content-length` short by the length of the script truncates
494
+ * it, and an `etag` computed before the change caches modified content under an
495
+ * unchanged validator.
496
+ *
497
+ * `content-length` is recomputed because it is cheap and exact; the validators
498
+ * are DROPPED rather than recomputed, because a validator this layer invents is
499
+ * not the one the handler meant.
500
+ */
501
+ function reconcileRepresentation(
502
+ headers: Record<string, string>,
503
+ original: string,
504
+ sent: string,
505
+ ): Record<string, string> {
506
+ if (sent === original) return headers
507
+ const reconciled = { ...headers }
508
+ if (reconciled['content-length'] !== undefined) {
509
+ reconciled['content-length'] = String(Buffer.byteLength(sent, 'utf8'))
510
+ }
511
+ // Every validator, not the two that came to mind. `Content-Digest` and
512
+ // `Repr-Digest` (RFC 9530) are what replaced `Digest`, and leaving them
513
+ // describing the old body is the same defect one header along — a client
514
+ // that checks them rejects a response that is perfectly fine.
515
+ for (const name of [
516
+ 'etag',
517
+ 'content-md5',
518
+ 'digest',
519
+ 'content-digest',
520
+ 'repr-digest',
521
+ 'want-digest',
522
+ 'want-content-digest',
523
+ 'want-repr-digest',
524
+ ]) {
525
+ delete reconciled[name]
526
+ }
527
+ return reconciled
528
+ }
529
+
530
+ /**
531
+ * Append the dev reload script to an HTML body.
532
+ *
533
+ * Only HTML, and never a stream: appending to an SSE or a download would
534
+ * corrupt it, and a page is the only thing that can act on the script anyway.
535
+ * Before `</body>` when there is one, so the script is inside the document
536
+ * rather than trailing after it.
537
+ */
538
+ function withDevReloadScript(
539
+ body: string,
540
+ headers: Record<string, string>,
541
+ streamId: string | undefined,
542
+ script?: string,
543
+ ): string {
544
+ if (script === undefined || streamId !== undefined || body === '') return body
545
+ if (!(headers['content-type'] ?? '').includes('text/html')) return body
546
+ const closing = body.lastIndexOf('</body>')
547
+ return closing === -1 ? body + script : body.slice(0, closing) + script + body.slice(closing)
548
+ }
package/src/Ignitor.ts CHANGED
@@ -17,6 +17,7 @@ import { Application } from './Application.js'
17
17
  import type { Console } from './console/Console.js'
18
18
  import type { CommandLoader, Kernel as ConsoleKernelInstance } from './console/Kernel.js'
19
19
  import { type CommandClass, isCommandClass } from './console/types.js'
20
+ import { HMR_PATH, hmrClientScript, hmrToken } from './dev/hmr.js'
20
21
  import type { DirectoriesNode } from './directories.js'
21
22
  import type { ErrorEvent } from './ErrorBoundary.js'
22
23
  import { ErrorBoundary } from './ErrorBoundary.js'
@@ -31,6 +32,7 @@ import type { HttpKernelRequest, HttpKernelResponse } from './HttpKernel.js'
31
32
  import { createHttpKernel } from './HttpKernel.js'
32
33
  import { ExceptionHandler } from './http/Exception.js'
33
34
  import { type ChildLoggerSource, HttpContext } from './http/HttpContext.js'
35
+ import { RequestValidator } from './http/RequestValidator.js'
34
36
  import type { MiddlewareFunction } from './middleware/Pipeline.js'
35
37
  import { MiddlewareRegistry } from './middleware/Pipeline.js'
36
38
  import { MigrationRegistry } from './migrations/MigrationRegistry.js'
@@ -325,6 +327,11 @@ export class Ignitor {
325
327
  // must stay agnostic reaches it through the container instead of importing
326
328
  // ream at runtime.
327
329
  this.#app.container.singleton('HttpContext', () => HttpContext)
330
+ // The seam a package installs its per-request messages provider or error
331
+ // reporter on. Bound rather than imported so an i18n provider can reach it
332
+ // without depending on ream — which is the only reason it is a container
333
+ // token and not just an import.
334
+ this.#app.container.singleton('requestValidator', () => RequestValidator)
328
335
  // `appRoot` is the URL passed to `new Ignitor(new URL('../', import.meta.url))`.
329
336
  // Providers resolve it through the container so they can interpret
330
337
  // relative paths in config files (e.g. `pages.root: './resources/pages'`)
@@ -659,6 +666,12 @@ export class Ignitor {
659
666
  }
660
667
 
661
668
  async #phaseStart(): Promise<void> {
669
+ // What the injected reload script polls, mounted BEFORE the socket accepts
670
+ // anything. Registered in `ready()` it came up after the server was already
671
+ // serving, so a page loaded in that window polled a route that did not
672
+ // exist yet.
673
+ this.#registerDevReloadRoute()
674
+
662
675
  // Providers start BEFORE the preload files, matching upstream's warm-up
663
676
  // order (`providers.start()` → `starting` hooks → preloads). A preload is
664
677
  // where an app writes its routes and kernel, and those reach for services
@@ -668,18 +681,28 @@ export class Ignitor {
668
681
  await callProviderPhase(provider, 'start')
669
682
  }
670
683
 
671
- // Import preload files (routes.ts, kernel.ts, etc.)
684
+ // Import preload files (routes.ts, kernel.ts, etc.).
685
+ //
686
+ // Filtered on the environment first, then imported TOGETHER — upstream's
687
+ // `PreloadsManager.import()` is a filter followed by one `Promise.all`, and
688
+ // this used to walk them one at a time.
689
+ //
690
+ // The consequence is worth knowing: evaluation order between two preload
691
+ // files is now whichever finishes loading first, so an application cannot
692
+ // rely on the array's order to decide which of two overlapping routes
693
+ // answers. Work that must be ordered belongs inside ONE preload, where the
694
+ // order is the file's own — which is how upstream applications are written.
672
695
  if (this.#reamrc?.preloads) {
673
- for (const preloadEntry of this.#reamrc.preloads) {
674
- const preloadImport = typeof preloadEntry === 'function' ? preloadEntry : preloadEntry.file
675
- const env =
676
- typeof preloadEntry === 'function'
677
- ? undefined
678
- : (preloadEntry as { environment?: string[] }).environment
679
-
680
- if (env && !env.includes(this.#app.getEnvironment())) continue
681
- await preloadImport()
682
- }
696
+ const applicable = this.#reamrc.preloads.filter((preloadEntry) => {
697
+ if (typeof preloadEntry === 'function') return true
698
+ const env = (preloadEntry as { environment?: string[] }).environment
699
+ return !env || env.includes(this.#app.getEnvironment())
700
+ })
701
+ await Promise.all(
702
+ applicable.map((preloadEntry) =>
703
+ typeof preloadEntry === 'function' ? preloadEntry() : preloadEntry.file(),
704
+ ),
705
+ )
683
706
  }
684
707
 
685
708
  // Auto-load module files (routes.ts, etc.) from modules directory
@@ -737,6 +760,7 @@ export class Ignitor {
737
760
  this.#server.getErrorHandler() ?? new ExceptionHandler(!this.#app.inProduction),
738
761
  serverMiddleware: this.#server.getServerMiddleware(),
739
762
  routerMiddleware: this.#router.getRouterMiddleware(),
763
+ devReloadScript: this.#devReloadScript(),
740
764
  onError: (error, ctx) => {
741
765
  this.#errorBoundary.serviceError('HttpKernel', error, ctx.id)
742
766
  },
@@ -859,11 +883,13 @@ export class Ignitor {
859
883
  // already-loaded ESM module returns the CACHED module without re-executing
860
884
  // its body (the thunk's specifier is opaque, so it cannot be cache-busted).
861
885
  // The "reload" therefore destroyed every preload-registered route on the
862
- // first file save and never restored them. Process-level restart is the
863
- // reload mechanism (`ream dev` runs `tsx watch`, which restarts on change);
864
- // this watcher only surfaces an informational event for hosts that embed
865
- // the Ignitor without a supervisor. A future true HMR needs a loader-hook
866
- // (hot-hook style) that can invalidate the ESM cache — plug it in here.
886
+ // first file save and never restored them.
887
+ //
888
+ // Module-level reloading now happens OUTSIDE the Ignitor: `ream dev` loads
889
+ // `@c9up/ream/hot`, which registers hot-hook's loader and can invalidate
890
+ // the ESM cache — the loader hook this comment used to ask for. What is
891
+ // left here only surfaces an informational event for hosts that embed the
892
+ // Ignitor without a supervisor.
867
893
  if (this.isDevMode()) {
868
894
  const watchDirs = this.#config.watchDirs ?? ['app', 'start']
869
895
  this.#hotReloadCleanup = startHotReload({
@@ -1125,6 +1151,7 @@ export class Ignitor {
1125
1151
  this.#server.getErrorHandler() ?? new ExceptionHandler(!this.#app.inProduction),
1126
1152
  serverMiddleware: this.#server.getServerMiddleware(),
1127
1153
  routerMiddleware: this.#router.getRouterMiddleware(),
1154
+ devReloadScript: this.#devReloadScript(),
1128
1155
  })
1129
1156
  return this.#kernel
1130
1157
  }
@@ -1135,6 +1162,33 @@ export class Ignitor {
1135
1162
  return this.#app.inDev
1136
1163
  }
1137
1164
 
1165
+ /**
1166
+ * The reload script, in development only.
1167
+ *
1168
+ * Gated on the environment rather than on `hot-hook` being present: the
1169
+ * script is just as useful without it, since a restart is exactly what it
1170
+ * detects. In production it is `undefined` and the response is untouched.
1171
+ */
1172
+ #devReloadScript(): ((nonce?: string) => string) | undefined {
1173
+ return this.isDevMode() ? hmrClientScript : undefined
1174
+ }
1175
+
1176
+ /**
1177
+ * Serve the token the injected script polls.
1178
+ *
1179
+ * Registered on the router in development, next to the application's own
1180
+ * routes, so it goes through the same server as everything else. Plain text
1181
+ * and uncached: it is read once a second and its whole content is the answer.
1182
+ */
1183
+ #registerDevReloadRoute(): void {
1184
+ if (!this.isDevMode()) return
1185
+ this.#router.get(HMR_PATH, ({ response }) => {
1186
+ response.header('cache-control', 'no-store')
1187
+ response.header('content-type', 'text/plain; charset=utf-8')
1188
+ response.send(hmrToken())
1189
+ })
1190
+ }
1191
+
1138
1192
  getPhase(): string {
1139
1193
  return this.#phase
1140
1194
  }