@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
|
@@ -6,10 +6,44 @@
|
|
|
6
6
|
import {
|
|
7
7
|
render,
|
|
8
8
|
performanceMonitor,
|
|
9
|
-
importPeerDependency,
|
|
10
9
|
renderComponentFactory
|
|
11
10
|
} from '@coherent.js/core';
|
|
12
11
|
|
|
12
|
+
function missingPeer(packageName, integrationName, cause) {
|
|
13
|
+
const error = new Error(
|
|
14
|
+
`${integrationName} requires the '${packageName}' package to be installed.\n` +
|
|
15
|
+
`Please install it with: npm install ${packageName} (or pnpm add / yarn add)`
|
|
16
|
+
);
|
|
17
|
+
error.cause = cause;
|
|
18
|
+
return error;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The React module to build elements with.
|
|
23
|
+
*
|
|
24
|
+
* An injected `options.React` wins. Otherwise React is imported from *this*
|
|
25
|
+
* package: it declares `react` as an optional peer, so package managers link
|
|
26
|
+
* the app's copy next to it. (core's `importPeerDependency` imports relative
|
|
27
|
+
* to @coherent.js/core instead, which cannot see the app's `react` under
|
|
28
|
+
* pnpm's isolated layout.) The literal specifier keeps it bundler-friendly.
|
|
29
|
+
*
|
|
30
|
+
* @param {Object} [injected] - A React module or namespace supplied by the app
|
|
31
|
+
* @param {string} integrationName - Used in the error message
|
|
32
|
+
* @returns {Promise<Object>} Object exposing createElement / useState / useEffect
|
|
33
|
+
*/
|
|
34
|
+
async function loadReact(injected, integrationName) {
|
|
35
|
+
let mod = injected;
|
|
36
|
+
if (!mod) {
|
|
37
|
+
try {
|
|
38
|
+
mod = await import('react');
|
|
39
|
+
} catch (_error) {
|
|
40
|
+
throw missingPeer('react', integrationName, _error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Namespace imports of CommonJS React carry the API on `default`.
|
|
44
|
+
return typeof mod.createElement === 'function' ? mod : mod.default;
|
|
45
|
+
}
|
|
46
|
+
|
|
13
47
|
/**
|
|
14
48
|
* Create a Next.js API route handler for Coherent.js components
|
|
15
49
|
*
|
|
@@ -33,8 +67,8 @@ export function createCoherentNextHandler(componentFactory, options = {}) {
|
|
|
33
67
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
34
68
|
res.status(200).send(finalHtml);
|
|
35
69
|
} catch (_error) {
|
|
36
|
-
console.error('Coherent.js Next.js handler
|
|
37
|
-
res.status(500).json({
|
|
70
|
+
console.error('Coherent.js Next.js handler error:', _error);
|
|
71
|
+
res.status(500).json({ error: _error.message });
|
|
38
72
|
}
|
|
39
73
|
};
|
|
40
74
|
}
|
|
@@ -42,17 +76,21 @@ export function createCoherentNextHandler(componentFactory, options = {}) {
|
|
|
42
76
|
/**
|
|
43
77
|
* Create a Next.js App Router route handler for Coherent.js components
|
|
44
78
|
*
|
|
45
|
-
*
|
|
79
|
+
* The factory receives the same `(request, context)` pair Next.js passes to
|
|
80
|
+
* route handlers, so dynamic segments are available as `context.params`
|
|
81
|
+
* (a Promise since Next.js 15).
|
|
82
|
+
*
|
|
83
|
+
* @param {Function} componentFactory - `(request, context) => component`
|
|
46
84
|
* @param {Object} options - Handler options
|
|
47
85
|
* @returns {Function} Next.js App Router route handler
|
|
48
86
|
*/
|
|
49
87
|
export function createCoherentAppRouterHandler(componentFactory, options = {}) {
|
|
50
|
-
return async function handler(request) {
|
|
88
|
+
return async function handler(request, context) {
|
|
51
89
|
try {
|
|
52
90
|
// Use shared rendering utility
|
|
53
91
|
const finalHtml = await renderComponentFactory(
|
|
54
92
|
componentFactory,
|
|
55
|
-
[request],
|
|
93
|
+
[request, context],
|
|
56
94
|
options
|
|
57
95
|
);
|
|
58
96
|
|
|
@@ -62,9 +100,9 @@ export function createCoherentAppRouterHandler(componentFactory, options = {}) {
|
|
|
62
100
|
headers: { 'Content-Type': 'text/html; charset=utf-8' }
|
|
63
101
|
});
|
|
64
102
|
} catch (_error) {
|
|
65
|
-
console.error('Coherent.js Next.js App Router handler
|
|
103
|
+
console.error('Coherent.js Next.js App Router handler error:', _error);
|
|
66
104
|
return new Response(
|
|
67
|
-
JSON.stringify({
|
|
105
|
+
JSON.stringify({ error: _error.message }),
|
|
68
106
|
{
|
|
69
107
|
status: 500,
|
|
70
108
|
headers: { 'Content-Type': 'application/json' }
|
|
@@ -79,22 +117,16 @@ export function createCoherentAppRouterHandler(componentFactory, options = {}) {
|
|
|
79
117
|
*
|
|
80
118
|
* @param {Function} componentFactory - Function that returns a Coherent.js component
|
|
81
119
|
* @param {Object} options - Component options
|
|
82
|
-
* @
|
|
120
|
+
* @param {boolean} [options.enablePerformanceMonitoring=false] - Enable performance monitoring
|
|
121
|
+
* @param {Object} [options.React] - React module to use instead of importing `react`
|
|
122
|
+
* @returns {Promise<Function>} Next.js Server Component
|
|
83
123
|
*/
|
|
84
124
|
export async function createCoherentServerComponent(componentFactory, options = {}) {
|
|
85
125
|
const {
|
|
86
126
|
enablePerformanceMonitoring = false
|
|
87
127
|
} = options;
|
|
88
128
|
|
|
89
|
-
|
|
90
|
-
let React;
|
|
91
|
-
try {
|
|
92
|
-
React = await importPeerDependency('react', 'React');
|
|
93
|
-
} catch (_error) {
|
|
94
|
-
throw new Error(
|
|
95
|
-
`Next.js Server Component integration requires React. ${ _error.message}`
|
|
96
|
-
);
|
|
97
|
-
}
|
|
129
|
+
const React = await loadReact(options.React, 'Next.js Server Component integration');
|
|
98
130
|
|
|
99
131
|
return async function CoherentServerComponent(props) {
|
|
100
132
|
try {
|
|
@@ -104,7 +136,7 @@ export async function createCoherentServerComponent(componentFactory, options =
|
|
|
104
136
|
);
|
|
105
137
|
|
|
106
138
|
if (!component) {
|
|
107
|
-
return React.
|
|
139
|
+
return React.createElement('div', null, 'Error: Component factory returned null/undefined');
|
|
108
140
|
}
|
|
109
141
|
|
|
110
142
|
// Render component
|
|
@@ -118,12 +150,12 @@ export async function createCoherentServerComponent(componentFactory, options =
|
|
|
118
150
|
}
|
|
119
151
|
|
|
120
152
|
// Return dangerouslySetInnerHTML to render HTML
|
|
121
|
-
return React.
|
|
153
|
+
return React.createElement('div', {
|
|
122
154
|
dangerouslySetInnerHTML: { __html: html }
|
|
123
155
|
});
|
|
124
156
|
} catch (_error) {
|
|
125
|
-
console.error('Coherent.js Next.js Server Component
|
|
126
|
-
return React.
|
|
157
|
+
console.error('Coherent.js Next.js Server Component error:', _error);
|
|
158
|
+
return React.createElement('div', null, `Error: ${_error.message}`);
|
|
127
159
|
}
|
|
128
160
|
};
|
|
129
161
|
}
|
|
@@ -133,22 +165,16 @@ export async function createCoherentServerComponent(componentFactory, options =
|
|
|
133
165
|
*
|
|
134
166
|
* @param {Function} componentFactory - Function that returns a Coherent.js component
|
|
135
167
|
* @param {Object} options - Component options
|
|
136
|
-
* @
|
|
168
|
+
* @param {boolean} [options.enablePerformanceMonitoring=false] - Enable performance monitoring
|
|
169
|
+
* @param {Object} [options.React] - React module to use instead of importing `react`
|
|
170
|
+
* @returns {Promise<Function>} Next.js Client Component
|
|
137
171
|
*/
|
|
138
172
|
export async function createCoherentClientComponent(componentFactory, options = {}) {
|
|
139
173
|
const {
|
|
140
174
|
enablePerformanceMonitoring = false
|
|
141
175
|
} = options;
|
|
142
176
|
|
|
143
|
-
|
|
144
|
-
let React;
|
|
145
|
-
try {
|
|
146
|
-
React = await importPeerDependency('react', 'React');
|
|
147
|
-
} catch (_error) {
|
|
148
|
-
throw new Error(
|
|
149
|
-
`Next.js Client Component integration requires React. ${ _error.message}`
|
|
150
|
-
);
|
|
151
|
-
}
|
|
177
|
+
const React = await loadReact(options.React, 'Next.js Client Component integration');
|
|
152
178
|
|
|
153
179
|
return function CoherentClientComponent(props) {
|
|
154
180
|
const [html, setHtml] = React.useState('');
|
|
@@ -178,7 +204,7 @@ export async function createCoherentClientComponent(componentFactory, options =
|
|
|
178
204
|
|
|
179
205
|
setHtml(renderedHtml);
|
|
180
206
|
} catch (_error) {
|
|
181
|
-
console.error('Coherent.js Next.js Client Component
|
|
207
|
+
console.error('Coherent.js Next.js Client Component error:', _error);
|
|
182
208
|
setHtml(`Error: ${_error.message}`);
|
|
183
209
|
}
|
|
184
210
|
}
|
|
@@ -201,9 +227,14 @@ export async function createCoherentClientComponent(componentFactory, options =
|
|
|
201
227
|
*/
|
|
202
228
|
export async function createNextIntegration(options = {}) {
|
|
203
229
|
try {
|
|
204
|
-
// Verify Next.js and React are available
|
|
205
|
-
|
|
206
|
-
|
|
230
|
+
// Verify Next.js and React are available, resolved from this package
|
|
231
|
+
// (see loadReact for why not core's importPeerDependency)
|
|
232
|
+
try {
|
|
233
|
+
await import('next');
|
|
234
|
+
} catch (_error) {
|
|
235
|
+
throw missingPeer('next', 'Next.js integration', _error);
|
|
236
|
+
}
|
|
237
|
+
await loadReact(options.React, 'Next.js integration');
|
|
207
238
|
|
|
208
239
|
return {
|
|
209
240
|
createCoherentNextHandler: (componentFactory, handlerOptions = {}) =>
|
package/src/remix/index.js
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
// Public entry point for @coherent.js/integrations/remix. Provides SSR
|
|
4
4
|
// integration and loader utilities for Remix projects.
|
|
5
5
|
//
|
|
6
|
-
// Remix must be installed as a peer dependency to use
|
|
6
|
+
// Remix (and therefore React) must be installed as a peer dependency to use
|
|
7
|
+
// this integration.
|
|
7
8
|
//
|
|
8
9
|
// Usage:
|
|
9
10
|
// import { createRemixAdapter } from '@coherent.js/integrations/remix';
|
|
10
11
|
|
|
12
|
+
import { createElement } from 'react';
|
|
11
13
|
import { render } from '@coherent.js/core';
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -75,12 +77,22 @@ export function createRemixAdapter(_options = {}) {
|
|
|
75
77
|
/**
|
|
76
78
|
* HOC: Wrap a Coherent.js component for use in Remix routes
|
|
77
79
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
+
* Returns a React component that renders the Coherent.js markup inside a
|
|
81
|
+
* wrapper element via `dangerouslySetInnerHTML`. Returning the HTML string
|
|
82
|
+
* itself would make React escape it and show the tags as text. The
|
|
83
|
+
* Coherent.js renderer escapes text and attribute values, so only markup the
|
|
84
|
+
* component describes (or passes through its explicit `html` field) is raw.
|
|
85
|
+
*
|
|
86
|
+
* @param {Function|Object} Component - Coherent.js component function or object
|
|
87
|
+
* @param {Object} [options] - Wrapper options
|
|
88
|
+
* @param {string} [options.as='div'] - Tag name of the wrapper element
|
|
89
|
+
* @returns {Function} Remix-compatible React component
|
|
80
90
|
*/
|
|
81
|
-
export function withCoherent(Component) {
|
|
91
|
+
export function withCoherent(Component, options = {}) {
|
|
92
|
+
const { as = 'div' } = options;
|
|
93
|
+
|
|
82
94
|
return function CoherentRemixComponent(props) {
|
|
83
95
|
const def = typeof Component === 'function' ? Component(props) : Component;
|
|
84
|
-
return render(def);
|
|
96
|
+
return createElement(as, { dangerouslySetInnerHTML: { __html: render(def) } });
|
|
85
97
|
};
|
|
86
98
|
}
|
package/src/sveltekit/index.js
CHANGED
|
@@ -61,10 +61,40 @@ export function createSvelteKitAdapter(_options = {}) {
|
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** Local name the preprocessor imports Coherent.js's `render` under. */
|
|
65
|
+
const RENDER_IDENTIFIER = '__coherentRender';
|
|
66
|
+
const RENDER_IMPORT = `import { render as ${RENDER_IDENTIFIER} } from '@coherent.js/core';`;
|
|
67
|
+
|
|
68
|
+
/** `<script context="module">` (Svelte 4) or `<script module>` (Svelte 5). */
|
|
69
|
+
function isModuleScript(attributes) {
|
|
70
|
+
return /\bcontext\s*=\s*["']?module\b/.test(attributes) || /(?:^|\s)module(?:\s|=|$)/.test(attributes);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Make `RENDER_IDENTIFIER` available to the markup: add the import to the
|
|
75
|
+
* instance `<script>`, or add an instance script if there is none. Inserted
|
|
76
|
+
* without newlines so the component's line numbers do not move.
|
|
77
|
+
*/
|
|
78
|
+
function injectRenderImport(code) {
|
|
79
|
+
// Case-insensitive like HTML: an instance script written <SCRIPT> was
|
|
80
|
+
// missed, and a second instance script was added next to it.
|
|
81
|
+
for (const match of code.matchAll(/<script\b([^>]*)>/gi)) {
|
|
82
|
+
if (!isModuleScript(match[1])) {
|
|
83
|
+
const at = match.index + match[0].length;
|
|
84
|
+
return `${code.slice(0, at)}${RENDER_IMPORT}${code.slice(at)}`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return `<script>${RENDER_IMPORT}</script>${code}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
64
90
|
/**
|
|
65
91
|
* Create a Svelte preprocessor for Coherent.js templates
|
|
66
92
|
*
|
|
67
|
-
*
|
|
93
|
+
* Replaces each `<coherent>{ ...object literal... }</coherent>` block with
|
|
94
|
+
* `{@html ...}` of the rendered component, and imports `render` from
|
|
95
|
+
* `@coherent.js/core` into the component's instance script so the generated
|
|
96
|
+
* expression resolves. The renderer escapes text content, so `{@html}` only
|
|
97
|
+
* emits markup the component itself describes.
|
|
68
98
|
*
|
|
69
99
|
* @param {Object} [options] - Preprocessor options
|
|
70
100
|
* @param {string} [options.tag] - Custom tag to process (default: 'coherent')
|
|
@@ -76,24 +106,18 @@ export function createPreprocessor(options = {}) {
|
|
|
76
106
|
return {
|
|
77
107
|
name: 'coherent-preprocessor',
|
|
78
108
|
markup({ content, filename: _filename }) {
|
|
79
|
-
// Find <coherent> blocks
|
|
109
|
+
// Find <coherent> blocks; their content is a JS expression (usually an object literal)
|
|
80
110
|
const regex = new RegExp(`<${tag}>(.*?)</${tag}>`, 'gs');
|
|
81
|
-
let
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const objectStr = match[1].trim();
|
|
89
|
-
transformed = transformed.replace(match[0], `{@html coherentRender(${objectStr})}`);
|
|
90
|
-
} catch {
|
|
91
|
-
// Leave unchanged if parsing fails
|
|
92
|
-
}
|
|
93
|
-
}
|
|
111
|
+
let found = false;
|
|
112
|
+
|
|
113
|
+
// Replacer function, so `$&`-style sequences in the block stay literal
|
|
114
|
+
const transformed = content.replace(regex, (_block, expression) => {
|
|
115
|
+
found = true;
|
|
116
|
+
return `{@html ${RENDER_IDENTIFIER}(${expression.trim()})}`;
|
|
117
|
+
});
|
|
94
118
|
|
|
95
119
|
return {
|
|
96
|
-
code: transformed,
|
|
120
|
+
code: found ? injectRenderImport(transformed) : content,
|
|
97
121
|
map: null
|
|
98
122
|
};
|
|
99
123
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Type definitions for Coherent.js Astro integration.
|
|
2
|
+
//
|
|
3
|
+
// Aligned with the runtime exports of ../../src/astro/index.js.
|
|
4
|
+
|
|
5
|
+
import type { AstroIntegration } from 'astro';
|
|
6
|
+
import type { CoherentNode, RenderOptions } from '@coherent.js/core';
|
|
7
|
+
|
|
8
|
+
export interface CoherentAstroOptions {
|
|
9
|
+
/** Pre-bundle `@coherent.js/client` for client-side hydration. */
|
|
10
|
+
hydrate?: boolean;
|
|
11
|
+
/** Custom hydration script path. */
|
|
12
|
+
hydrateScript?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A Coherent.js component: a node, or a function of props returning one. */
|
|
16
|
+
export type CoherentAstroComponent<Props = Record<string, unknown>> =
|
|
17
|
+
| CoherentNode
|
|
18
|
+
| ((props: Props) => CoherentNode);
|
|
19
|
+
|
|
20
|
+
/** The SSR renderer Astro loads from the integration's server entrypoint. */
|
|
21
|
+
export interface CoherentAstroRenderer {
|
|
22
|
+
name: string;
|
|
23
|
+
check(Component: unknown, props?: Record<string, unknown>): boolean;
|
|
24
|
+
renderToStaticMarkup(Component: unknown, props?: Record<string, unknown>): { html: string };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Astro integration that registers the Coherent.js renderer
|
|
29
|
+
* (`@coherent.js/integrations/astro/server`).
|
|
30
|
+
*/
|
|
31
|
+
export function createAstroIntegration(options?: CoherentAstroOptions): AstroIntegration;
|
|
32
|
+
|
|
33
|
+
/** Render a Coherent.js component (or component function) to an HTML string. */
|
|
34
|
+
export function renderComponent<Props = Record<string, unknown>>(
|
|
35
|
+
Component: CoherentAstroComponent<Props>,
|
|
36
|
+
props?: Props,
|
|
37
|
+
renderOptions?: RenderOptions
|
|
38
|
+
): string;
|
|
39
|
+
|
|
40
|
+
/** Create an Astro SSR renderer for Coherent.js components. */
|
|
41
|
+
export function createRenderer(options?: RenderOptions): CoherentAstroRenderer;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Type definitions for the Coherent.js Astro server entrypoint
|
|
2
|
+
// (../../src/astro/server.js): Astro imports its default export as the
|
|
3
|
+
// SSR renderer.
|
|
4
|
+
|
|
5
|
+
import type { CoherentAstroRenderer } from './index.js';
|
|
6
|
+
|
|
7
|
+
declare const renderer: CoherentAstroRenderer;
|
|
8
|
+
|
|
9
|
+
export default renderer;
|
package/types/express/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export interface CoherentMiddlewareOptions {
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* HTML template to wrap rendered components
|
|
15
|
-
* @default '<!DOCTYPE html
|
|
15
|
+
* @default '<!DOCTYPE html>\n{{content}}'
|
|
16
16
|
*/
|
|
17
17
|
template?: string;
|
|
18
18
|
|
|
@@ -21,6 +21,26 @@ export interface CoherentMiddlewareOptions {
|
|
|
21
21
|
* @default true
|
|
22
22
|
*/
|
|
23
23
|
enableSSR?: boolean;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Also render component-shaped objects passed to `res.send()`.
|
|
27
|
+
*
|
|
28
|
+
* Detection is a heuristic: every single-key object qualifies, so JSON
|
|
29
|
+
* payloads such as `{ ok: true }` or `{ users: [...] }` would be rendered
|
|
30
|
+
* as HTML. Use `res.coherent(component)` instead unless the app never
|
|
31
|
+
* sends single-key JSON objects.
|
|
32
|
+
* @default false
|
|
33
|
+
*/
|
|
34
|
+
autoRender?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Per-call options for `res.coherent()`; each falls back to the middleware's.
|
|
39
|
+
*/
|
|
40
|
+
export interface CoherentRenderOptions {
|
|
41
|
+
enablePerformanceMonitoring?: boolean;
|
|
42
|
+
/** HTML template with a `{{content}}` placeholder. */
|
|
43
|
+
template?: string;
|
|
24
44
|
}
|
|
25
45
|
|
|
26
46
|
export interface CoherentHandlerOptions {
|
|
@@ -32,7 +52,7 @@ export interface CoherentHandlerOptions {
|
|
|
32
52
|
|
|
33
53
|
/**
|
|
34
54
|
* HTML template to wrap rendered components
|
|
35
|
-
* @default '<!DOCTYPE html
|
|
55
|
+
* @default '<!DOCTYPE html>\n{{content}}'
|
|
36
56
|
*/
|
|
37
57
|
template?: string;
|
|
38
58
|
|
|
@@ -51,13 +71,15 @@ export interface SetupCoherentExpressOptions {
|
|
|
51
71
|
useMiddleware?: boolean;
|
|
52
72
|
|
|
53
73
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
74
|
+
* Register {@link enhancedExpressEngine} as a view engine. It becomes the
|
|
75
|
+
* app's default `view engine` only if none is set yet.
|
|
76
|
+
* @default false
|
|
56
77
|
*/
|
|
57
78
|
useEngine?: boolean;
|
|
58
|
-
|
|
79
|
+
|
|
59
80
|
/**
|
|
60
|
-
* Name of the view engine
|
|
81
|
+
* Name of the view engine, i.e. the view file extension. Use `'js'` to
|
|
82
|
+
* render `views/*.js` modules whose default export is the component.
|
|
61
83
|
* @default 'coherent'
|
|
62
84
|
*/
|
|
63
85
|
engineName?: string;
|
|
@@ -67,7 +89,20 @@ export interface SetupCoherentExpressOptions {
|
|
|
67
89
|
* @default false
|
|
68
90
|
*/
|
|
69
91
|
enablePerformanceMonitoring?: boolean;
|
|
70
|
-
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* HTML template with a `{{content}}` placeholder, used by `res.coherent()`
|
|
95
|
+
* @default '<!DOCTYPE html>\n{{content}}'
|
|
96
|
+
*/
|
|
97
|
+
template?: string;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Also render component-shaped objects passed to `res.send()`.
|
|
101
|
+
* See {@link CoherentMiddlewareOptions.autoRender} for why this is opt-in.
|
|
102
|
+
* @default false
|
|
103
|
+
*/
|
|
104
|
+
autoRender?: boolean;
|
|
105
|
+
|
|
71
106
|
/**
|
|
72
107
|
* Static file directory for client-side assets
|
|
73
108
|
* @default 'public'
|
|
@@ -75,9 +110,23 @@ export interface SetupCoherentExpressOptions {
|
|
|
75
110
|
staticDir?: string;
|
|
76
111
|
}
|
|
77
112
|
|
|
113
|
+
declare global {
|
|
114
|
+
namespace Express {
|
|
115
|
+
interface Response {
|
|
116
|
+
/**
|
|
117
|
+
* Render a Coherent.js component (wrapped in the middleware's template)
|
|
118
|
+
* and send it as `text/html`. Rendering errors are passed to the app's
|
|
119
|
+
* error middleware, as `res.render()` does. Added by
|
|
120
|
+
* `coherentMiddleware()` / `setupCoherent()`.
|
|
121
|
+
*/
|
|
122
|
+
coherent(component: CoherentNode, options?: CoherentRenderOptions): this;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
78
127
|
/**
|
|
79
128
|
* Coherent.js Express middleware
|
|
80
|
-
* Adds
|
|
129
|
+
* Adds `res.coherent()` (and, with `autoRender`, `res.send` rendering)
|
|
81
130
|
* @param options Configuration options
|
|
82
131
|
* @returns Express middleware function
|
|
83
132
|
*/
|
|
@@ -89,19 +138,29 @@ export function coherentMiddleware(options?: CoherentMiddlewareOptions): (
|
|
|
89
138
|
|
|
90
139
|
/**
|
|
91
140
|
* Create an Express route handler for Coherent.js components
|
|
92
|
-
* @param componentFactory Function that returns a Coherent component
|
|
141
|
+
* @param componentFactory Function that returns a Coherent component. If it
|
|
142
|
+
* responds itself (e.g. `res.redirect()`), its return value is ignored.
|
|
93
143
|
* @param options Configuration options
|
|
94
144
|
* @returns Express route handler
|
|
95
145
|
*/
|
|
96
146
|
export function createCoherentHandler(
|
|
97
|
-
componentFactory: (
|
|
147
|
+
componentFactory: (
|
|
148
|
+
req: Request,
|
|
149
|
+
res: Response,
|
|
150
|
+
next: NextFunction
|
|
151
|
+
) => CoherentNode | void | Promise<CoherentNode | void>,
|
|
98
152
|
options?: CoherentHandlerOptions
|
|
99
|
-
): (req: Request, res: Response, next: NextFunction) => void
|
|
153
|
+
): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
100
154
|
|
|
101
155
|
/**
|
|
102
|
-
*
|
|
156
|
+
* Express view engine for Coherent.js views.
|
|
157
|
+
*
|
|
158
|
+
* A `.js`/`.mjs`/`.cjs` view module's default export is the component (a
|
|
159
|
+
* function is called with the render locals). For any other view file the
|
|
160
|
+
* locals themselves are rendered as the component. Express's `settings`,
|
|
161
|
+
* `_locals` and `cache` keys are stripped from the locals first.
|
|
103
162
|
* @param filePath Path to the view file
|
|
104
|
-
* @param options
|
|
163
|
+
* @param options Render locals
|
|
105
164
|
* @param callback Callback function
|
|
106
165
|
*/
|
|
107
166
|
export function enhancedExpressEngine(
|
|
@@ -112,7 +171,7 @@ export function enhancedExpressEngine(
|
|
|
112
171
|
|
|
113
172
|
/**
|
|
114
173
|
* Setup Coherent.js with Express app
|
|
115
|
-
*
|
|
174
|
+
* Installs {@link coherentMiddleware} and, with `useEngine`, the view engine
|
|
116
175
|
* @param app Express application instance
|
|
117
176
|
* @param options Configuration options
|
|
118
177
|
*/
|
|
@@ -135,8 +194,7 @@ export function createExpressIntegration(
|
|
|
135
194
|
): Promise<(app: Application) => Application>;
|
|
136
195
|
|
|
137
196
|
/**
|
|
138
|
-
*
|
|
139
|
-
* `options` argument.
|
|
197
|
+
* Returns {@link enhancedExpressEngine}, for `app.engine('js', expressEngine())`.
|
|
140
198
|
*
|
|
141
199
|
* Kept for consumers migrating from the standalone `@coherent.js/express`
|
|
142
200
|
* package; new code should prefer {@link setupCoherent}.
|
package/types/fastify/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Wave 2c (integrations consolidation). Declarations are aligned with the
|
|
5
5
|
// runtime exports of ../../src/fastify/coherent-fastify.js.
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import type { FastifyPluginCallback, FastifyReply, FastifyRequest } from 'fastify';
|
|
8
8
|
import type { CoherentNode } from '@coherent.js/core';
|
|
9
9
|
|
|
10
10
|
export interface CoherentFastifyOptions {
|
|
@@ -26,6 +26,17 @@ export interface CoherentFastifyOptions {
|
|
|
26
26
|
*/
|
|
27
27
|
enableSSR?: boolean;
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Also render component-shaped objects returned from route handlers.
|
|
31
|
+
*
|
|
32
|
+
* Detection is a heuristic: every single-key object qualifies, so JSON
|
|
33
|
+
* payloads such as `{ ok: true }` or `{ error: '...' }` would be rendered
|
|
34
|
+
* as HTML, even with a JSON response schema. Use `reply.coherent(component)`
|
|
35
|
+
* instead unless the app never returns single-key JSON objects.
|
|
36
|
+
* @default false
|
|
37
|
+
*/
|
|
38
|
+
autoRender?: boolean;
|
|
39
|
+
|
|
29
40
|
/**
|
|
30
41
|
* Static file directory for client-side assets
|
|
31
42
|
* @default 'public'
|
|
@@ -54,17 +65,17 @@ export interface CoherentFastifyHandlerOptions {
|
|
|
54
65
|
}
|
|
55
66
|
|
|
56
67
|
/**
|
|
57
|
-
* Fastify plugin for Coherent.js
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
68
|
+
* Fastify plugin for Coherent.js (wrapped with fastify-plugin, so its
|
|
69
|
+
* decorators apply to the registering scope). Adds `reply.coherent()` and,
|
|
70
|
+
* with `autoRender`, rendering of returned components.
|
|
71
|
+
*
|
|
72
|
+
* Register it; do not call it:
|
|
73
|
+
*
|
|
74
|
+
* ```ts
|
|
75
|
+
* await fastify.register(coherentFastify, { template });
|
|
76
|
+
* ```
|
|
62
77
|
*/
|
|
63
|
-
export
|
|
64
|
-
fastify: FastifyInstance,
|
|
65
|
-
options: CoherentFastifyOptions,
|
|
66
|
-
done: () => void
|
|
67
|
-
): void;
|
|
78
|
+
export const coherentFastify: FastifyPluginCallback<CoherentFastifyOptions>;
|
|
68
79
|
|
|
69
80
|
/**
|
|
70
81
|
* Create a Fastify route handler for Coherent.js components
|
|
@@ -81,15 +92,10 @@ export function createHandler(
|
|
|
81
92
|
): (request: FastifyRequest, reply: FastifyReply) => Promise<any>;
|
|
82
93
|
|
|
83
94
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* @param fastify Fastify instance
|
|
87
|
-
* @param options Configuration options
|
|
95
|
+
* Alias of {@link coherentFastify}. It is a Fastify plugin: register it with
|
|
96
|
+
* `await fastify.register(setupCoherent, options)` rather than calling it.
|
|
88
97
|
*/
|
|
89
|
-
export
|
|
90
|
-
fastify: FastifyInstance,
|
|
91
|
-
options?: CoherentFastifyOptions
|
|
92
|
-
): void;
|
|
98
|
+
export const setupCoherent: FastifyPluginCallback<CoherentFastifyOptions>;
|
|
93
99
|
|
|
94
100
|
/**
|
|
95
101
|
* Fastify reply extensions
|
|
@@ -104,14 +110,17 @@ declare module 'fastify' {
|
|
|
104
110
|
isCoherentObject(obj: any): boolean;
|
|
105
111
|
|
|
106
112
|
/**
|
|
107
|
-
* Render and send a Coherent component as HTML response
|
|
113
|
+
* Render and send a Coherent component as HTML response. A rendering
|
|
114
|
+
* error is sent through Fastify's error handling (`setErrorHandler`).
|
|
115
|
+
* Returns the reply, so `return reply.coherent(page)` works in async
|
|
116
|
+
* handlers.
|
|
108
117
|
* @param component Coherent component to render
|
|
109
118
|
* @param options Rendering options
|
|
110
119
|
*/
|
|
111
120
|
coherent(
|
|
112
121
|
component: CoherentNode,
|
|
113
122
|
options?: CoherentFastifyHandlerOptions
|
|
114
|
-
):
|
|
123
|
+
): FastifyReply;
|
|
115
124
|
}
|
|
116
125
|
}
|
|
117
126
|
|