@coherent.js/integrations 1.1.2 → 2.0.0-rc.0
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 +31 -0
- package/package.json +11 -7
- package/src/astro/index.js +2 -1
- package/src/astro/server.js +12 -0
- package/src/express/coherent-express.js +151 -45
- package/src/express/index.js +6 -11
- package/src/fastify/coherent-fastify.js +45 -23
- package/src/koa/coherent-koa.js +37 -9
- package/src/nextjs/coherent-nextjs.js +67 -36
- package/src/remix/index.js +17 -5
- package/src/sveltekit/index.js +40 -16
- package/types/astro/index.d.ts +41 -0
- package/types/astro/server.d.ts +9 -0
- package/types/express/index.d.ts +74 -16
- package/types/fastify/index.d.ts +30 -21
- package/types/koa/index.d.ts +97 -0
- package/types/nextjs/index.d.ts +22 -4
- package/types/remix/index.d.ts +68 -0
- package/types/sveltekit/index.d.ts +68 -0
package/README.md
CHANGED
|
@@ -12,6 +12,12 @@ Framework integration adapters for Coherent.js — bridges between your chosen H
|
|
|
12
12
|
- `@coherent.js/integrations/remix` — Remix adapter (added in 1.0.0)
|
|
13
13
|
- `@coherent.js/integrations/sveltekit` — SvelteKit adapter (added in 1.0.0)
|
|
14
14
|
|
|
15
|
+
**Stability:** the Astro, Remix and SvelteKit adapters are young. Their tests run a real
|
|
16
|
+
`astro build`, compile and server-render a component with the Svelte compiler, and server-render
|
|
17
|
+
the Remix wrapper with React, but the adapters have seen little production use. The Express,
|
|
18
|
+
Fastify and Koa adapters are tested over real HTTP requests, and the Next.js adapter with Web
|
|
19
|
+
`Request`/`Response` objects and `react-dom/server`.
|
|
20
|
+
|
|
15
21
|
## Migration from pre-1.0
|
|
16
22
|
|
|
17
23
|
Each framework previously shipped as its own package (`@coherent.js/express`, etc.). Migrate by changing import paths:
|
|
@@ -32,6 +38,31 @@ pnpm add express # or fastify / koa / next / etc. — only the ones you use
|
|
|
32
38
|
|
|
33
39
|
Framework peer dependencies are declared optional, so consumers only install the framework(s) they actually use.
|
|
34
40
|
|
|
41
|
+
## Rendering components (Express, Fastify, Koa)
|
|
42
|
+
|
|
43
|
+
Render explicitly; plain objects are sent as JSON:
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
// Express
|
|
47
|
+
setupCoherent(app, { template: '<!DOCTYPE html>\n{{content}}' });
|
|
48
|
+
app.get('/', (req, res) => res.coherent(HomePage()));
|
|
49
|
+
app.get('/api/users', (req, res) => res.send({ users })); // JSON
|
|
50
|
+
|
|
51
|
+
// Fastify
|
|
52
|
+
await fastify.register(setupCoherent, { template });
|
|
53
|
+
fastify.get('/', async (request, reply) => reply.coherent(HomePage()));
|
|
54
|
+
|
|
55
|
+
// Koa
|
|
56
|
+
setupCoherent(app, { template });
|
|
57
|
+
router.get('/', (ctx) => ctx.coherent(HomePage()));
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`autoRender: true` restores the earlier behavior of rendering any
|
|
61
|
+
component-shaped object passed to `res.send` / returned from a Fastify handler
|
|
62
|
+
/ assigned to `ctx.body`. It is off by default because "component-shaped"
|
|
63
|
+
means "has exactly one key", which matches JSON such as `{ ok: true }` or
|
|
64
|
+
`{ error: 'Invalid credentials' }` too.
|
|
65
|
+
|
|
35
66
|
## License
|
|
36
67
|
|
|
37
68
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coherent.js/integrations",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0-rc.0",
|
|
4
4
|
"description": "Framework integration adapters for Coherent.js: Express, Fastify, Koa, Next.js, Astro, Remix, SvelteKit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"default": "./src/fastify/index.js"
|
|
14
14
|
},
|
|
15
15
|
"./koa": {
|
|
16
|
+
"types": "./types/koa/index.d.ts",
|
|
16
17
|
"default": "./src/koa/index.js"
|
|
17
18
|
},
|
|
18
19
|
"./nextjs": {
|
|
@@ -20,12 +21,19 @@
|
|
|
20
21
|
"default": "./src/nextjs/index.js"
|
|
21
22
|
},
|
|
22
23
|
"./astro": {
|
|
24
|
+
"types": "./types/astro/index.d.ts",
|
|
23
25
|
"default": "./src/astro/index.js"
|
|
24
26
|
},
|
|
27
|
+
"./astro/server": {
|
|
28
|
+
"types": "./types/astro/server.d.ts",
|
|
29
|
+
"default": "./src/astro/server.js"
|
|
30
|
+
},
|
|
25
31
|
"./remix": {
|
|
32
|
+
"types": "./types/remix/index.d.ts",
|
|
26
33
|
"default": "./src/remix/index.js"
|
|
27
34
|
},
|
|
28
35
|
"./sveltekit": {
|
|
36
|
+
"types": "./types/sveltekit/index.d.ts",
|
|
29
37
|
"default": "./src/sveltekit/index.js"
|
|
30
38
|
}
|
|
31
39
|
},
|
|
@@ -51,20 +59,16 @@
|
|
|
51
59
|
"fastify-plugin": "^6.0.0"
|
|
52
60
|
},
|
|
53
61
|
"peerDependencies": {
|
|
54
|
-
"@
|
|
62
|
+
"@coherent.js/core": "^2.0.0-rc.0",
|
|
55
63
|
"@sveltejs/kit": ">=2.0.0",
|
|
56
64
|
"astro": ">=4.0.0",
|
|
57
65
|
"express": ">=4.18.0 < 6.0.0",
|
|
58
66
|
"fastify": ">=4.0.0 < 6.0.0",
|
|
59
67
|
"koa": ">=2.13.0 < 4.0.0",
|
|
60
68
|
"next": ">=13.0.0",
|
|
61
|
-
"react": ">=18.0.0"
|
|
62
|
-
"@coherent.js/core": "^1.1.2"
|
|
69
|
+
"react": ">=18.0.0"
|
|
63
70
|
},
|
|
64
71
|
"peerDependenciesMeta": {
|
|
65
|
-
"@remix-run/server-runtime": {
|
|
66
|
-
"optional": true
|
|
67
|
-
},
|
|
68
72
|
"@sveltejs/kit": {
|
|
69
73
|
"optional": true
|
|
70
74
|
},
|
package/src/astro/index.js
CHANGED
|
@@ -30,7 +30,8 @@ export function createAstroIntegration(options = {}) {
|
|
|
30
30
|
'astro:config:setup': ({ addRenderer, updateConfig }) => {
|
|
31
31
|
addRenderer({
|
|
32
32
|
name: '@coherent.js/integrations/astro',
|
|
33
|
-
|
|
33
|
+
// Astro imports this module's default export as the SSR renderer.
|
|
34
|
+
serverEntrypoint: '@coherent.js/integrations/astro/server',
|
|
34
35
|
});
|
|
35
36
|
|
|
36
37
|
if (options.hydrate) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// src/astro/server.js
|
|
2
|
+
//
|
|
3
|
+
// Astro server entrypoint for @coherent.js/integrations/astro.
|
|
4
|
+
//
|
|
5
|
+
// `createAstroIntegration()` registers this module with `addRenderer({
|
|
6
|
+
// serverEntrypoint })`. Astro imports its *default export* as the SSR
|
|
7
|
+
// renderer (`{ name, check, renderToStaticMarkup }`), so it must have one —
|
|
8
|
+
// without it `astro build` fails with `"default" is not exported`.
|
|
9
|
+
|
|
10
|
+
import { createRenderer } from './index.js';
|
|
11
|
+
|
|
12
|
+
export default createRenderer();
|
|
@@ -3,54 +3,90 @@
|
|
|
3
3
|
* Provides middleware and utilities for using Coherent.js with Express
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
6
7
|
import {
|
|
7
8
|
render,
|
|
8
9
|
importPeerDependency,
|
|
9
10
|
renderWithTemplate,
|
|
10
|
-
renderComponentFactory,
|
|
11
11
|
isCoherentComponent
|
|
12
12
|
} from '@coherent.js/core';
|
|
13
13
|
|
|
14
|
+
const DEFAULT_TEMPLATE = '<!DOCTYPE html>\n{{content}}';
|
|
15
|
+
|
|
14
16
|
/**
|
|
15
|
-
* Coherent.js Express middleware
|
|
16
|
-
*
|
|
17
|
+
* Coherent.js Express middleware.
|
|
18
|
+
*
|
|
19
|
+
* Adds `res.coherent(component, renderOptions?)`, which renders a Coherent.js
|
|
20
|
+
* component (wrapped in `template`) and sends it as `text/html`. Rendering
|
|
21
|
+
* errors are forwarded to Express error handling, like `res.render` does.
|
|
22
|
+
*
|
|
23
|
+
* With `autoRender: true` it additionally overrides `res.send` so that any
|
|
24
|
+
* object that looks like a component is rendered to HTML. That detection is
|
|
25
|
+
* a heuristic -- every single-key object qualifies, so `res.send({ ok: true })`
|
|
26
|
+
* or `res.json({ users })` routed through `res.send` would be rendered as
|
|
27
|
+
* `<ok>` / `<users>` instead of serialized as JSON. It is off by default; only
|
|
28
|
+
* enable it for apps that never send single-key JSON objects.
|
|
17
29
|
*
|
|
18
30
|
* @param {Object} options - Configuration options
|
|
19
|
-
* @param {boolean} options.enablePerformanceMonitoring - Enable performance monitoring
|
|
20
|
-
* @param {string} options.template - HTML template with {{content}} placeholder
|
|
31
|
+
* @param {boolean} [options.enablePerformanceMonitoring=false] - Enable performance monitoring
|
|
32
|
+
* @param {string} [options.template] - HTML template with {{content}} placeholder
|
|
33
|
+
* @param {boolean} [options.autoRender=false] - Render component-shaped objects passed to `res.send`
|
|
21
34
|
* @returns {Function} Express middleware function
|
|
22
35
|
*/
|
|
23
36
|
export function coherentMiddleware(options = {}) {
|
|
24
37
|
const {
|
|
25
38
|
enablePerformanceMonitoring = false,
|
|
26
|
-
template =
|
|
39
|
+
template = DEFAULT_TEMPLATE,
|
|
40
|
+
autoRender = false
|
|
27
41
|
} = options;
|
|
28
42
|
|
|
29
43
|
return (req, res, next) => {
|
|
30
44
|
// Store original send method
|
|
31
45
|
const originalSend = res.send;
|
|
32
46
|
|
|
33
|
-
//
|
|
34
|
-
res.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
47
|
+
// Explicit rendering: res.coherent(component, { template?, enablePerformanceMonitoring? })
|
|
48
|
+
res.coherent = function coherent(component, renderOptions = {}) {
|
|
49
|
+
let finalHtml;
|
|
50
|
+
try {
|
|
51
|
+
finalHtml = renderWithTemplate(component, {
|
|
52
|
+
enablePerformanceMonitoring: renderOptions.enablePerformanceMonitoring ?? enablePerformanceMonitoring,
|
|
53
|
+
template: renderOptions.template ?? template
|
|
54
|
+
});
|
|
55
|
+
} catch (_error) {
|
|
56
|
+
// Same contract as res.render(): hand the error to the app's error
|
|
57
|
+
// middleware. req.next is the router's current `next`, so this works
|
|
58
|
+
// from sync, async (Express 4 and 5) and callback code alike.
|
|
59
|
+
(req.next ?? next)(_error);
|
|
60
|
+
return this;
|
|
48
61
|
}
|
|
49
62
|
|
|
50
|
-
|
|
51
|
-
return originalSend.call(this,
|
|
63
|
+
this.set('Content-Type', 'text/html; charset=utf-8');
|
|
64
|
+
return originalSend.call(this, finalHtml);
|
|
52
65
|
};
|
|
53
66
|
|
|
67
|
+
if (autoRender) {
|
|
68
|
+
// Override send method to handle Coherent.js objects
|
|
69
|
+
res.send = function(data) {
|
|
70
|
+
// If data looks like a Coherent.js object (plain object with a single key), render it
|
|
71
|
+
if (isCoherentComponent(data)) {
|
|
72
|
+
try {
|
|
73
|
+
// Use shared rendering utility
|
|
74
|
+
const finalHtml = renderWithTemplate(data, { enablePerformanceMonitoring, template });
|
|
75
|
+
|
|
76
|
+
// Set content type and send HTML
|
|
77
|
+
res.set('Content-Type', 'text/html');
|
|
78
|
+
return originalSend.call(this, finalHtml);
|
|
79
|
+
} catch (_error) {
|
|
80
|
+
console.error('Coherent.js rendering error:', _error);
|
|
81
|
+
return next(_error);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// For non-Coherent.js data, use original send method
|
|
86
|
+
return originalSend.call(this, data);
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
54
90
|
next();
|
|
55
91
|
};
|
|
56
92
|
}
|
|
@@ -58,6 +94,9 @@ export function coherentMiddleware(options = {}) {
|
|
|
58
94
|
/**
|
|
59
95
|
* Create an Express route handler for Coherent.js components
|
|
60
96
|
*
|
|
97
|
+
* The factory receives `(req, res, next)`. If it answers the request itself
|
|
98
|
+
* (`res.redirect()`, `res.json()`, ...) nothing further is sent.
|
|
99
|
+
*
|
|
61
100
|
* @param {Function} componentFactory - Function that returns a Coherent.js component
|
|
62
101
|
* @param {Object} options - Handler options
|
|
63
102
|
* @returns {Function} Express route handler
|
|
@@ -65,63 +104,130 @@ export function coherentMiddleware(options = {}) {
|
|
|
65
104
|
export function createCoherentHandler(componentFactory, options = {}) {
|
|
66
105
|
return async (req, res, next) => {
|
|
67
106
|
try {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
107
|
+
const component = await componentFactory(req, res, next);
|
|
108
|
+
|
|
109
|
+
// The factory already responded: sending again would throw
|
|
110
|
+
// ERR_HTTP_HEADERS_SENT.
|
|
111
|
+
if (res.headersSent) return;
|
|
112
|
+
|
|
113
|
+
if (!component) {
|
|
114
|
+
throw new Error('Component factory returned null/undefined');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const finalHtml = renderWithTemplate(component, options);
|
|
74
118
|
|
|
75
119
|
// Send HTML response
|
|
76
120
|
res.set('Content-Type', 'text/html');
|
|
77
121
|
res.send(finalHtml);
|
|
78
122
|
} catch (_error) {
|
|
79
|
-
console.error('Coherent.js handler
|
|
123
|
+
console.error('Coherent.js handler error:', _error);
|
|
80
124
|
next(_error);
|
|
81
125
|
}
|
|
82
126
|
};
|
|
83
127
|
}
|
|
84
128
|
|
|
129
|
+
/** Keys Express merges into the view-engine options besides the locals. */
|
|
130
|
+
const EXPRESS_VIEW_OPTION_KEYS = new Set(['settings', '_locals', 'cache']);
|
|
131
|
+
|
|
132
|
+
/** View files that are loaded as ES modules rather than used as markers. */
|
|
133
|
+
const VIEW_MODULE_EXTENSION = /\.(?:js|mjs|cjs)$/;
|
|
134
|
+
|
|
85
135
|
/**
|
|
86
|
-
*
|
|
136
|
+
* Resolve the component for a view.
|
|
87
137
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
138
|
+
* A JavaScript view module's default export is the component: a function is
|
|
139
|
+
* called with the render locals, anything else is used as-is. Any other view
|
|
140
|
+
* file (e.g. an empty `home.coherent`) only satisfies Express's lookup, and
|
|
141
|
+
* the locals themselves are the component: `res.render('home', { div: ... })`.
|
|
142
|
+
*/
|
|
143
|
+
async function resolveViewComponent(filePath, locals) {
|
|
144
|
+
if (!VIEW_MODULE_EXTENSION.test(filePath)) {
|
|
145
|
+
return locals;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const viewModule = await import(pathToFileURL(filePath).href);
|
|
149
|
+
if (!('default' in viewModule)) {
|
|
150
|
+
throw new Error(`Coherent.js view "${filePath}" has no default export`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const view = viewModule.default;
|
|
154
|
+
return typeof view === 'function' ? view(locals) : view;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Express view engine for Coherent.js views.
|
|
159
|
+
*
|
|
160
|
+
* Register it with `setupCoherent(app, { useEngine: true })`, or directly with
|
|
161
|
+
* `app.engine('js', enhancedExpressEngine)` to render view modules
|
|
162
|
+
* (`views/home.js` exporting a component or `(locals) => component` as
|
|
163
|
+
* default). Express's own `settings`, `_locals` and `cache` keys are removed
|
|
164
|
+
* before the locals reach the component.
|
|
165
|
+
*
|
|
166
|
+
* @param {string} filePath - Absolute path of the view file Express resolved
|
|
167
|
+
* @param {Object} options - Render locals merged by Express
|
|
90
168
|
* @param {Function} callback - Callback function
|
|
91
169
|
*/
|
|
92
170
|
export function enhancedExpressEngine(filePath, options, callback) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
callback(null, html);
|
|
97
|
-
} catch (_error) {
|
|
98
|
-
callback(_error);
|
|
171
|
+
const locals = {};
|
|
172
|
+
for (const [key, value] of Object.entries(options ?? {})) {
|
|
173
|
+
if (!EXPRESS_VIEW_OPTION_KEYS.has(key)) locals[key] = value;
|
|
99
174
|
}
|
|
175
|
+
|
|
176
|
+
resolveViewComponent(filePath, locals).then(
|
|
177
|
+
(component) => {
|
|
178
|
+
let html;
|
|
179
|
+
try {
|
|
180
|
+
html = render(component);
|
|
181
|
+
} catch (_error) {
|
|
182
|
+
callback(_error);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
callback(null, html);
|
|
186
|
+
},
|
|
187
|
+
(_error) => callback(_error)
|
|
188
|
+
);
|
|
100
189
|
}
|
|
101
190
|
|
|
102
191
|
/**
|
|
103
192
|
* Setup Coherent.js with Express app
|
|
104
193
|
*
|
|
194
|
+
* Installs {@link coherentMiddleware} (so routes can call `res.coherent()`)
|
|
195
|
+
* and, when asked to, registers {@link enhancedExpressEngine} as a view
|
|
196
|
+
* engine. The engine is opt-in so it does not take over an app's existing
|
|
197
|
+
* `view engine`; it only becomes the default engine when none is set.
|
|
198
|
+
*
|
|
105
199
|
* @param {Object} app - Express app instance
|
|
106
200
|
* @param {Object} options - Setup options
|
|
201
|
+
* @param {boolean} [options.useMiddleware=true] - Install coherentMiddleware
|
|
202
|
+
* @param {boolean} [options.useEngine=false] - Register the view engine
|
|
203
|
+
* @param {string} [options.engineName='coherent'] - View engine name / file extension
|
|
204
|
+
* (use 'js' to render `views/*.js` modules)
|
|
205
|
+
* @param {boolean} [options.enablePerformanceMonitoring=false] - Enable performance monitoring
|
|
206
|
+
* @param {string} [options.template] - HTML template with {{content}} placeholder
|
|
207
|
+
* @param {boolean} [options.autoRender=false] - Render component-shaped objects passed to `res.send`
|
|
208
|
+
* (see coherentMiddleware for why this is opt-in)
|
|
107
209
|
*/
|
|
108
210
|
export function setupCoherent(app, options = {}) {
|
|
109
211
|
const {
|
|
110
212
|
useMiddleware = true,
|
|
111
|
-
useEngine =
|
|
213
|
+
useEngine = false,
|
|
112
214
|
engineName = 'coherent',
|
|
113
|
-
enablePerformanceMonitoring = false
|
|
215
|
+
enablePerformanceMonitoring = false,
|
|
216
|
+
template,
|
|
217
|
+
autoRender = false
|
|
114
218
|
} = options;
|
|
115
219
|
|
|
116
|
-
// Register
|
|
220
|
+
// Register the view engine (opt-in); never override an existing default engine
|
|
117
221
|
if (useEngine) {
|
|
118
222
|
app.engine(engineName, enhancedExpressEngine);
|
|
119
|
-
app.
|
|
223
|
+
if (!app.get('view engine')) {
|
|
224
|
+
app.set('view engine', engineName);
|
|
225
|
+
}
|
|
120
226
|
}
|
|
121
227
|
|
|
122
|
-
//
|
|
228
|
+
// Install the middleware (res.coherent, plus res.send auto-rendering when opted in)
|
|
123
229
|
if (useMiddleware) {
|
|
124
|
-
app.use(coherentMiddleware({ enablePerformanceMonitoring }));
|
|
230
|
+
app.use(coherentMiddleware({ enablePerformanceMonitoring, template, autoRender }));
|
|
125
231
|
}
|
|
126
232
|
}
|
|
127
233
|
|
package/src/express/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// compatibility with consumers that imported it from the legacy
|
|
9
9
|
// @coherent.js/express package.
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { enhancedExpressEngine } from './coherent-express.js';
|
|
12
12
|
|
|
13
13
|
export {
|
|
14
14
|
coherentMiddleware,
|
|
@@ -18,19 +18,14 @@ export {
|
|
|
18
18
|
createExpressIntegration
|
|
19
19
|
} from './coherent-express.js';
|
|
20
20
|
|
|
21
|
+
export { default } from './coherent-express.js';
|
|
22
|
+
|
|
21
23
|
/**
|
|
22
|
-
* Factory returning a classic Express view engine
|
|
23
|
-
*
|
|
24
|
+
* Factory returning a classic Express view engine for Coherent.js views
|
|
25
|
+
* (see `enhancedExpressEngine`): `app.engine('js', expressEngine())`.
|
|
24
26
|
*
|
|
25
27
|
* @returns {(filePath: string, options: unknown, callback: (err: Error | null, html?: string) => void) => void}
|
|
26
28
|
*/
|
|
27
29
|
export function expressEngine() {
|
|
28
|
-
return
|
|
29
|
-
try {
|
|
30
|
-
const html = render(options);
|
|
31
|
-
callback(null, html);
|
|
32
|
-
} catch (_error) {
|
|
33
|
-
callback(_error);
|
|
34
|
-
}
|
|
35
|
-
};
|
|
30
|
+
return enhancedExpressEngine;
|
|
36
31
|
}
|
|
@@ -24,12 +24,22 @@ import {
|
|
|
24
24
|
* @param {Object} options - Plugin options
|
|
25
25
|
* @param {boolean} [options.enablePerformanceMonitoring] - Enable performance monitoring
|
|
26
26
|
* @param {string} [options.template] - HTML template with {{content}} placeholder
|
|
27
|
+
* @param {boolean} [options.autoRender] - Render component-shaped handler return values
|
|
28
|
+
* (see the preSerialization hook below for why this is opt-in)
|
|
27
29
|
* @param {Function} done - Callback to signal plugin registration completion
|
|
28
30
|
*/
|
|
29
31
|
function coherentFastifyImpl(fastify, options = {}, done) {
|
|
32
|
+
if (typeof done !== 'function') {
|
|
33
|
+
throw new TypeError(
|
|
34
|
+
'coherentFastify/setupCoherent is a Fastify plugin and cannot be called directly: ' +
|
|
35
|
+
'use `await fastify.register(setupCoherent, options)`.'
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
const {
|
|
31
40
|
enablePerformanceMonitoring = false,
|
|
32
|
-
template = '<!DOCTYPE html>\n{{content}}'
|
|
41
|
+
template = '<!DOCTYPE html>\n{{content}}',
|
|
42
|
+
autoRender = false
|
|
33
43
|
} = options;
|
|
34
44
|
|
|
35
45
|
// Add decorator to check if an object is a Coherent.js component
|
|
@@ -48,39 +58,47 @@ function coherentFastifyImpl(fastify, options = {}, done) {
|
|
|
48
58
|
template: renderTemplate = template
|
|
49
59
|
} = renderOptions;
|
|
50
60
|
|
|
61
|
+
let finalHtml;
|
|
51
62
|
try {
|
|
52
|
-
|
|
63
|
+
finalHtml = renderWithTemplate(component, {
|
|
53
64
|
enablePerformanceMonitoring: renderPerformanceMonitoring,
|
|
54
65
|
template: renderTemplate
|
|
55
66
|
});
|
|
56
|
-
this.header('Content-Type', 'text/html; charset=utf-8');
|
|
57
|
-
this.send(finalHtml);
|
|
58
67
|
} catch (_error) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
});
|
|
68
|
+
// Sending an Error runs Fastify's error pipeline (onError hooks, the
|
|
69
|
+
// app's setErrorHandler, its logger) instead of answering with the raw
|
|
70
|
+
// message.
|
|
71
|
+
return this.send(_error);
|
|
64
72
|
}
|
|
73
|
+
|
|
74
|
+
this.header('Content-Type', 'text/html; charset=utf-8');
|
|
75
|
+
return this.send(finalHtml);
|
|
65
76
|
});
|
|
66
77
|
|
|
67
|
-
// Auto-render: if a handler returns a Coherent.js component
|
|
68
|
-
// intercept before serialization and replace the payload with HTML.
|
|
78
|
+
// Auto-render (opt-in): if a handler returns a Coherent.js component
|
|
79
|
+
// object, intercept before serialization and replace the payload with HTML.
|
|
80
|
+
//
|
|
81
|
+
// Off by default because detection is a heuristic -- every single-key
|
|
82
|
+
// object qualifies, so `return { ok: true }` or a 401 `{ error: '...' }`
|
|
83
|
+
// would be rendered as `<ok>` / `<error>` HTML even with a JSON response
|
|
84
|
+
// schema. Use `reply.coherent(component)` for explicit rendering instead.
|
|
69
85
|
//
|
|
70
86
|
// - `onSend` runs after JSON serialization (payload is already a string),
|
|
71
87
|
// so the component object would never be detected there.
|
|
72
88
|
// - `preSerialization` runs before serialization. We render to HTML and
|
|
73
89
|
// install an identity serializer for this reply, so Fastify doesn't
|
|
74
90
|
// JSON-stringify the HTML string we just produced.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
91
|
+
if (autoRender) {
|
|
92
|
+
fastify.addHook('preSerialization', async (request, reply, payload) => {
|
|
93
|
+
if (reply.isCoherentObject?.(payload)) {
|
|
94
|
+
const finalHtml = renderWithTemplate(payload, { enablePerformanceMonitoring, template });
|
|
95
|
+
reply.header('Content-Type', 'text/html; charset=utf-8');
|
|
96
|
+
reply.serializer((p) => p);
|
|
97
|
+
return finalHtml;
|
|
98
|
+
}
|
|
99
|
+
return payload;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
84
102
|
|
|
85
103
|
done();
|
|
86
104
|
}
|
|
@@ -88,10 +106,14 @@ function coherentFastifyImpl(fastify, options = {}, done) {
|
|
|
88
106
|
/**
|
|
89
107
|
* Fastify plugin for Coherent.js — wrapped with fastify-plugin so decorators
|
|
90
108
|
* and hooks apply to the parent (root) context. Register at the top of your
|
|
91
|
-
* app, then
|
|
109
|
+
* app, then render components explicitly with `reply.coherent()`:
|
|
92
110
|
*
|
|
93
111
|
* await fastify.register(coherentFastify, { template: APP_HTML_TEMPLATE });
|
|
94
|
-
* fastify.get('/', async () => HomePage({}));
|
|
112
|
+
* fastify.get('/', async (request, reply) => reply.coherent(HomePage({})));
|
|
113
|
+
*
|
|
114
|
+
* Pass `autoRender: true` to also render component objects returned from
|
|
115
|
+
* handlers (`fastify.get('/', async () => HomePage({}))`); see the
|
|
116
|
+
* preSerialization hook above for why that is not the default.
|
|
95
117
|
*/
|
|
96
118
|
export const coherentFastify = fp(coherentFastifyImpl, {
|
|
97
119
|
name: 'coherent-fastify',
|
|
@@ -123,7 +145,7 @@ export function createHandler(componentFactory, options = {}) {
|
|
|
123
145
|
reply.header('Content-Type', 'text/html; charset=utf-8');
|
|
124
146
|
return finalHtml;
|
|
125
147
|
} catch (_error) {
|
|
126
|
-
console.error('Coherent.js handler
|
|
148
|
+
console.error('Coherent.js handler error:', _error);
|
|
127
149
|
throw _error;
|
|
128
150
|
}
|
|
129
151
|
};
|
package/src/koa/coherent-koa.js
CHANGED
|
@@ -11,25 +11,48 @@ import {
|
|
|
11
11
|
} from '@coherent.js/core';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Coherent.js Koa middleware
|
|
15
|
-
*
|
|
14
|
+
* Coherent.js Koa middleware.
|
|
15
|
+
*
|
|
16
|
+
* Adds `ctx.coherent(component, renderOptions?)`, which renders a Coherent.js
|
|
17
|
+
* component (wrapped in `template`) into `ctx.body` as `text/html`. Rendering
|
|
18
|
+
* errors are thrown, so they reach the app's error-handling middleware.
|
|
19
|
+
*
|
|
20
|
+
* With `autoRender: true` it additionally renders any `ctx.body` that looks
|
|
21
|
+
* like a component once downstream middleware has finished. That detection
|
|
22
|
+
* is a heuristic -- every single-key object qualifies, so
|
|
23
|
+
* `ctx.body = { ok: true }` would be rendered as `<ok>` instead of serialized
|
|
24
|
+
* as JSON. It is off by default; only enable it for apps that never respond
|
|
25
|
+
* with single-key JSON objects.
|
|
16
26
|
*
|
|
17
27
|
* @param {Object} options - Configuration options
|
|
18
|
-
* @param {boolean} options.enablePerformanceMonitoring - Enable performance monitoring
|
|
19
|
-
* @param {string} options.template - HTML template with {{content}} placeholder
|
|
28
|
+
* @param {boolean} [options.enablePerformanceMonitoring=false] - Enable performance monitoring
|
|
29
|
+
* @param {string} [options.template] - HTML template with {{content}} placeholder
|
|
30
|
+
* @param {boolean} [options.autoRender=false] - Render component-shaped `ctx.body` values
|
|
20
31
|
* @returns {Function} Koa middleware function
|
|
21
32
|
*/
|
|
22
33
|
export function coherentKoaMiddleware(options = {}) {
|
|
23
34
|
const {
|
|
24
35
|
enablePerformanceMonitoring = false,
|
|
25
|
-
template = '<!DOCTYPE html>\n{{content}}'
|
|
36
|
+
template = '<!DOCTYPE html>\n{{content}}',
|
|
37
|
+
autoRender = false
|
|
26
38
|
} = options;
|
|
27
39
|
|
|
28
40
|
return async (ctx, next) => {
|
|
41
|
+
// Explicit rendering: ctx.coherent(component, { template?, enablePerformanceMonitoring? })
|
|
42
|
+
ctx.coherent = (component, renderOptions = {}) => {
|
|
43
|
+
const finalHtml = renderWithTemplate(component, {
|
|
44
|
+
enablePerformanceMonitoring: renderOptions.enablePerformanceMonitoring ?? enablePerformanceMonitoring,
|
|
45
|
+
template: renderOptions.template ?? template
|
|
46
|
+
});
|
|
47
|
+
ctx.type = 'html';
|
|
48
|
+
ctx.body = finalHtml;
|
|
49
|
+
return finalHtml;
|
|
50
|
+
};
|
|
51
|
+
|
|
29
52
|
await next();
|
|
30
53
|
|
|
31
|
-
//
|
|
32
|
-
if (isCoherentComponent(ctx.body)) {
|
|
54
|
+
// Opt-in: if the response body looks like a Coherent.js object, render it
|
|
55
|
+
if (autoRender && isCoherentComponent(ctx.body)) {
|
|
33
56
|
try {
|
|
34
57
|
// Use shared rendering utility
|
|
35
58
|
const finalHtml = renderWithTemplate(ctx.body, { enablePerformanceMonitoring, template });
|
|
@@ -38,7 +61,7 @@ export function coherentKoaMiddleware(options = {}) {
|
|
|
38
61
|
ctx.type = 'text/html';
|
|
39
62
|
ctx.body = finalHtml;
|
|
40
63
|
} catch (_error) {
|
|
41
|
-
console.error('Coherent.js rendering
|
|
64
|
+
console.error('Coherent.js rendering error:', _error);
|
|
42
65
|
throw _error;
|
|
43
66
|
}
|
|
44
67
|
}
|
|
@@ -66,7 +89,7 @@ export function createHandler(componentFactory, options = {}) {
|
|
|
66
89
|
ctx.type = 'text/html';
|
|
67
90
|
ctx.body = finalHtml;
|
|
68
91
|
} catch (_error) {
|
|
69
|
-
console.error('Coherent.js handler
|
|
92
|
+
console.error('Coherent.js handler error:', _error);
|
|
70
93
|
throw _error;
|
|
71
94
|
}
|
|
72
95
|
};
|
|
@@ -75,8 +98,13 @@ export function createHandler(componentFactory, options = {}) {
|
|
|
75
98
|
/**
|
|
76
99
|
* Setup Coherent.js with Koa app
|
|
77
100
|
*
|
|
101
|
+
* Installs {@link coherentKoaMiddleware}, so downstream middleware can call
|
|
102
|
+
* `ctx.coherent(component)`. Every option except `useMiddleware` is forwarded
|
|
103
|
+
* to the middleware, including `template` and `autoRender`.
|
|
104
|
+
*
|
|
78
105
|
* @param {Object} app - Koa app instance
|
|
79
106
|
* @param {Object} options - Setup options
|
|
107
|
+
* @param {boolean} [options.useMiddleware=true] - Install coherentKoaMiddleware
|
|
80
108
|
*/
|
|
81
109
|
export function setupCoherent(app, options = {}) {
|
|
82
110
|
const { useMiddleware = true, ...middlewareOptions } = options;
|