Skip to content

Commit 11b10b6

Browse files
committed
feat: added auto middleware registration
1 parent 1475ddf commit 11b10b6

11 files changed

Lines changed: 149 additions & 125 deletions

File tree

Lines changed: 11 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
---
2-
title: Page Provider and CMS Binding
2+
title: CMS Integration
33
---
44

5-
A page provider is a middleware function that returns a [page](page.md).
5+
A `CMS` is integrated as a [middleware](middleware.md) that returns a [page](page.md).
66

7-
All CMS would be implemented as a page provider.
7+
## How to create a CMS middleware
88

9-
## How to create a page Provider
10-
11-
You can create a provider plugin with a module:
9+
You can create a middleware by writing a JavaScript module:
1210

1311
* that exports a handler function that accepts optionally, a props object (only data, no functions)
1412
* and returns a function that accepts a web api request and returns a [page](page.md) as object:
@@ -17,8 +15,7 @@ You can create a provider plugin with a module:
1715

1816
```tsx
1917
// ./src/cms/my-provider.js
20-
import {ContextProps} from "@combostrap/interact/types";
21-
import {MiddlewareHandler} from "./interactMiddleware";
18+
import {ContextProps, MiddlewareHandler} from "@combostrap/interact/types";
2219

2320
export async function handler(props): Promise<MiddlewareHandler> {
2421
return async (context: ContextProps) => {
@@ -30,9 +27,14 @@ export async function handler(props): Promise<MiddlewareHandler> {
3027
return
3128
}
3229

30+
// Modify the context if needed
31+
// context.response.status = 400
32+
// context.response.headers
33+
3334
// Fetch your data
3435
const data = await fetch(new URL(request.url).pathname)
3536

37+
// Return your page
3638
return {
3739
frontmatter: {
3840
layout: "holy"
@@ -50,32 +52,7 @@ export async function handler(props): Promise<MiddlewareHandler> {
5052

5153
## Example
5254

53-
* You can take a look to the `localPagesMiddleware.tsx` file, it's a CMS plugin that returns
55+
* You can take a look to the `local markdown middleware` file, it's a CMS plugin that returns
5456
local [Markdown file as page](md-page.md).
5557
* The [remote Markdown example](https://github.com/combostrap/interact/blob/main/apps/site/cms/remote-markdown.tsx) page
5658
provider that returns Markdown page from GitHub.
57-
58-
## Registration
59-
60-
You can register your page provider in the `pages.providers` node of the [configuration file](conf.md)
61-
62-
Example:
63-
64-
```json
65-
{
66-
"pages": {
67-
"providers": [
68-
{
69-
"importPath": "./cms/my-provider.js",
70-
"props": {
71-
"uri": "",
72-
"timeout": 1
73-
}
74-
}
75-
]
76-
}
77-
}
78-
```
79-
80-
* They are executed in order.
81-
* The import path is relative to the [root directory](directory-layout.md)

apps/site/pages/reference/directory-layout.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,24 @@
22
title: Directory Layout
33
---
44

5-
65
A typical interact project is composed of the following paths:
76

87
| Name | Default Value | Description |
98
|----------------------------------------------|---------------------------|--------------------------------------------------------------------------------------------------------------|
9+
| [@ alias](at-alias.md) | `src` | A directory that is mapped to the [@ alias](at-alias.md) |
10+
| `config` | `config` | a directory that contains extra configuration such as the [markdown config](remark-rehype-unified.md#config) |
11+
| `cache` | `.interact` | A directory that contains temporary runtime information |
12+
| [build](build.md) | `dist` | A directory where the [build result](build.md) is stored (static website and handler) |
1013
| [config file](conf.md) | `interact.config.json` | the [configuration file](conf.md) |
11-
| `root` | config file directory | The base directory of your project |
12-
| [pages](page.md) | `src/pages` | A directory that contains [pages](page.md) |
1314
| `images` | `src/images` | a directory that contains [raster image](../components/image.md) or [Svg](../components/svg.md) |
14-
| [public](public.md) | `public` | A directory that contains non-processed resources such as `pdf` that your pages may reference. |
15+
| [markdown components](markdown-component.md) | `src/components/markdown` | a directory that contains your custom markdown component |
1516
| [layouts](layout.md) | `src/components/layouts` | a directory that contains your custom [layouts](layout.md) |
16-
| [markdown components](markdown-component.md) | `src/components/markdown` | a directory that contains your custom markdown component |
17-
| `config` | `config` | a directory that contains extra configuration such as the [markdown config](remark-rehype-unified.md#config) |
18-
| [build](build.md) | `dist` | A directory where the [build result](build.md) is stored (static website and handler) |
19-
| `cache` | `.interact` | A directory for runtime information |
20-
| [@ alias](at-alias.md) | `src` | A directory that is mapped to the [@ alias](at-alias.md) |
17+
| [middlewares](middleware.md) | `src/middlewares` | a directory that contains the middlewares for auto-registration |
18+
| [public](public.md) | `public` | A directory that contains non-processed resources such as `pdf` that your pages may reference. |
19+
| [pages](page.md) | `src/pages` | A directory that contains [pages](page.md) |
20+
| `root` | config file directory | The base directory of your project |
2121

22-
If the values are relative (ie without starting slash), they are all relative to the `root` directory which is the
22+
If the path values are relative (ie without starting slash), they are all relative to the `root` directory which is the
2323
directory of your project.
2424

2525
## Configuration
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
title: Middleware
3+
---
4+
5+
A middleware is a module with a default function that:
6+
7+
* takes as input a context object with the [HTTP request](https://developer.mozilla.org/en-US/docs/Web/API/Request)
8+
* and returned:
9+
* a [Web Api HTTP Response](https://developer.mozilla.org/en-US/docs/Web/API/Response)
10+
* or a [page](page.md)
11+
12+
## Usage
13+
14+
* [CMS integration](cms.md) that returns a page
15+
* Request Context enhancement such as adding a user via User Authentication
16+
17+
## Registration
18+
19+
You can register your middleware:
20+
21+
* automatically by storing it in the [middlewares directory](directory-layout.md) (`src/middlewares` by default)
22+
* in the `middelwares` node of the [configuration file](conf.md)
23+
24+
Example:
25+
26+
```json
27+
{
28+
"middlewares": [
29+
{
30+
"importPath": "./cms/my-cms-middleware.js",
31+
"props": {
32+
"uri": "",
33+
"timeout": 1
34+
}
35+
}
36+
]
37+
}
38+
```
39+
40+
* They are executed in natural order.
41+
* The import path may be relative to the [root directory](directory-layout.md)
42+
43+
## How to list the middlewares
44+
45+
You can check them with the [cli](cli.md)
46+
47+
```bash
48+
interact config --filter="middlewares"
49+
```
50+
51+
## Example: How to create a page middleware
52+
53+
See the [page provider cms integration](cms.md)
54+

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"./components/*": "./src/resources/components/interact/*.tsx",
3232
"./vite/*": "./dist/interact/vite/*.js",
3333
"./markdown": "./src/resources/markdown/interactMarkdownProcessor.tsx",
34+
"./middlewares/*": "./src/resources/middlewares/*.tsx",
3435
"./config": "./src/interact/config/interactConfig.ts"
3536
},
3637
"dependencies": {

src/interact/config/configSchema.ts

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ const PathsSchema = z.object({
9090
publicDirectory: z.coerce.string<string>().describe("The path of the public directory").default("public"),
9191
layoutsDirectory: z.coerce.string<string>().describe("The path of the layout directory").default("src/components/layouts"),
9292
mdComponentsDirectory: z.coerce.string<string>().describe("The path of the markdown components directory").default("src/components/markdowns"),
93+
middlewaresDirectory: z.coerce.string<string>().describe("The path of the middlewares directory").default("src/middlewares"),
9394
configDirectory: z.coerce.string<string>().describe("The path of the config directory").default("config"),
9495
imagesDirectory: z.coerce.string<string>().describe("The path of the image directory").default("images"),
9596
// https://vite.dev/config/build-options#build-outdir
@@ -263,7 +264,6 @@ let configStyleSchema = z.object({
263264
export type styleConfigType = z.output<typeof configStyleSchema>;
264265

265266

266-
267267
/**
268268
* Components
269269
* Base schema shared by all components
@@ -318,28 +318,10 @@ const MiddlewareConfigSchema = z.object({
318318
importPath: z.coerce.string<string>(),
319319
props: z.record(z.string(), z.unknown()).optional(),
320320
}).describe("Provider Properties");
321-
const PagesConfigSchema = z.object({
322-
providers: z.array(MiddlewareConfigSchema).optional(),
323-
});
324-
325-
export type MiddlewareConfig = z.output<typeof MiddlewareConfigSchema>;
326-
export type PagesConfig = z.output<typeof PagesConfigSchema>;
327321

328-
export type ComponentsSet = z.output<typeof ComponentsConfigSetSchema>;
322+
const MiddlewaresSchema = z.array(MiddlewareConfigSchema).default([]);
329323

330324

331-
/**
332-
* Plugins
333-
*/
334-
const PluginConfigSchema = z.object({
335-
path: z.coerce.string<string>().optional(),
336-
props: z.record(z.coerce.string<string>(), z.any()).optional(),
337-
type: z.enum(['remark', 'rehype']).optional(),
338-
});
339-
340-
const PluginConfigSetSchema = z.record(z.coerce.string<string>(), PluginConfigSchema.nullable());
341-
export type pluginsConfigType = z.output<typeof PluginConfigSetSchema>;
342-
343325
const OutlineSchema = z.object({
344326
numbering: outlineNumberingSchema.default(outlineNumberingSchema.parse({})),
345327
})
@@ -353,15 +335,15 @@ let MarkdownConfigSchema = z.object({
353335
});
354336

355337
/**
356-
* The JSON Schema used to parse the Json file
338+
* The JSON Schema used to parse the JSON file
357339
*/
358340
export const JsonConfigSchema = z.object({
359341
$schema: z.coerce.string().optional(),
360342
site: SiteSchema.default(SiteSchema.parse({})),
361343
outline: OutlineSchema.default(OutlineSchema.parse({})),
362344
paths: PathsSchema.default(PathsSchema.parse({})),
363345
aliases: AliasesSchema.default(AliasesSchema.parse({})),
364-
pages: PagesConfigSchema.default(PagesConfigSchema.parse({})),
346+
middlewares: MiddlewaresSchema.default([]),
365347
images: ImageSchema.default(ImageSchema.parse({})),
366348
style: configStyleSchema.default(configStyleSchema.parse({})),
367349
components: ComponentsConfigSetSchema.default(ComponentsConfigSetSchema.parse({})),
@@ -375,4 +357,5 @@ export type aliasesConfigType = z.output<typeof AliasesSchema>;
375357
export type siteConfigType = z.output<typeof SiteSchema>;
376358
export type outlineConfigType = z.output<typeof OutlineSchema>;
377359
export type markdownConfigType = z.output<typeof MarkdownConfigSchema>;
378-
360+
export type MiddlewareConfig = z.output<typeof MiddlewaresSchema>;
361+
export type ComponentsSet = z.output<typeof ComponentsConfigSetSchema>;

src/interact/config/interactConfig.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
* so it has no import/export expect for type
44
*/
55
import {
6-
type pluginsConfigType, type ComponentsSet, type pathsConfigType, type imageConfigType,
7-
type siteConfigType, type styleConfigType, type outlineConfigType, type markdownConfigType, type PagesConfig,
8-
type aliasesConfigType
6+
type ComponentsSet, type pathsConfigType, type imageConfigType,
7+
type siteConfigType, type styleConfigType, type outlineConfigType, type markdownConfigType,
8+
type aliasesConfigType, type MiddlewareConfig
99
} from "./configSchema.js";
1010

1111
/**
@@ -15,10 +15,9 @@ import {
1515
export type InteractConfig = {
1616
style: styleConfigType,
1717
site: siteConfigType
18-
plugins: pluginsConfigType,
1918
outline: outlineConfigType,
2019
components: ComponentsSet,
21-
pages: PagesConfig,
20+
middlewares: MiddlewareConfig,
2221
images: imageConfigType,
2322
markdown: markdownConfigType,
2423
aliases: aliasesConfigType,

src/interact/config/interactConfigHandler.ts

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
JsonConfigSchema,
33
type FaviconSetSchemaType,
4-
type pluginsConfigType, type ComponentsSet
4+
type ComponentsSet
55
} from "./configSchema.js";
66
import fs, {existsSync} from 'fs'
77
import {readdirSync, readFileSync} from "node:fs";
@@ -87,15 +87,6 @@ export const defaultComponentsValue: ComponentsSet = {
8787
}
8888
}
8989

90-
const defaultMarkdownUnifiedPlugins: pluginsConfigType = {
91-
"rehype-github-alerts": {},
92-
"rehype-href-rewrite": {},
93-
"remark-link-checker": {
94-
props: {strict: true}
95-
},
96-
"remark-layout": {},
97-
"remark-frontmatter-modified-time": {}
98-
}
9990

10091
// Based on https://realfavicongenerator.net
10192
let defaultFavicons: FaviconSetSchemaType = {
@@ -237,6 +228,7 @@ class InteractConfigHandler {
237228
if (finalConfigData.paths.rootDirectory != null) {
238229
rootDirectory = finalConfigData.paths.rootDirectory;
239230
}
231+
240232
finalConfigData.paths = {
241233
configFile: this.configFile,
242234
rootDirectory: rootDirectory,
@@ -245,6 +237,7 @@ class InteractConfigHandler {
245237
imagesDirectory: this.#qualifiedDirectoryPath(rootDirectory, finalConfigData.paths.imagesDirectory),
246238
layoutsDirectory: this.#qualifiedDirectoryPath(rootDirectory, finalConfigData.paths.layoutsDirectory),
247239
mdComponentsDirectory: this.#qualifiedDirectoryPath(rootDirectory, finalConfigData.paths.mdComponentsDirectory),
240+
middlewaresDirectory: this.#qualifiedDirectoryPath(rootDirectory, finalConfigData.paths.middlewaresDirectory),
248241
configDirectory: this.#qualifiedDirectoryPath(rootDirectory, finalConfigData.paths.configDirectory),
249242
cacheDirectory: this.#qualifiedDirectoryPath(rootDirectory, ".interact"),
250243
interactResourcesDirectory: path.resolve(rootDirectory, interactRootDirectory, 'src/resources'),
@@ -267,11 +260,6 @@ class InteractConfigHandler {
267260
);
268261

269262

270-
/**
271-
* Default plugins
272-
*/
273-
finalConfigData.plugins = deepMerge(defaultMarkdownUnifiedPlugins, finalConfigData.plugins)
274-
275263
/**
276264
* Default ui components
277265
*/
@@ -316,6 +304,37 @@ class InteractConfigHandler {
316304
}
317305
}
318306

307+
/**
308+
* Middlewares
309+
*/
310+
const middlewareDirectories = [`${finalConfigData.paths.interactResourcesDirectory}/middlewares`, finalConfigData.paths.middlewaresDirectory];
311+
for (const [i, middlewaresDirectory] of middlewareDirectories.entries()) {
312+
let isProjectDir = i == 1
313+
if (isProjectDir && !existsSync(middlewaresDirectory)) {
314+
// interact dir should exist, we don't check
315+
// it will fail at readDir
316+
continue
317+
}
318+
319+
const filesNaturalSort = fs
320+
.readdirSync(middlewaresDirectory)
321+
.sort((a, b) => a.localeCompare(b, undefined, {numeric: true, sensitivity: 'base'}));
322+
323+
for (const fileName of filesNaturalSort) {
324+
const {name,ext} = path.parse(fileName);
325+
if (!/\.(jsx|tsx|js)$/.test(ext)) continue;
326+
let importPath = path.resolve(middlewaresDirectory, fileName);
327+
if (isProjectDir) {
328+
importPath = importPath.replace(finalConfigData.paths.atDirectory, atAliasCharacter);
329+
} else {
330+
importPath = `@combostrap/interact/middlewares/${name}`;
331+
}
332+
finalConfigData.middlewares.push({
333+
importPath: importPath,
334+
})
335+
}
336+
}
337+
319338
/**
320339
* Default Markdown
321340
*/

src/interact/vite/middlewareProvider.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type {Plugin} from 'vite';
2-
import type {MiddlewareConfig} from "../config/configSchema.js";
32
import {getInteractConfig} from "../config/interactConfig.js";
43
import path from "node:path";
54

@@ -8,18 +7,10 @@ export default function middlewareProvider(): Plugin {
87

98
let interactConfig = getInteractConfig()
109

11-
let middlewareConfigs: MiddlewareConfig[] = [
12-
...interactConfig.pages.providers || [],
13-
{
14-
importPath: path.resolve(interactConfig.paths.interactResourcesDirectory, 'middleware/localPagesMiddleware'),
15-
props: {
16-
pagesDirectory: interactConfig.paths.pagesDirectory
17-
}
18-
}]
1910
//const resolvedId = '\0' + virtualId;
2011
const resolvedId = virtualId;
2112
return {
22-
name: 'interact-middleware-registry',
13+
name: 'interact:middlewares',
2314
resolveId(id: string) {
2415
if (id === virtualId) return resolvedId;
2516
},
@@ -28,7 +19,7 @@ export default function middlewareProvider(): Plugin {
2819

2920
let imports: string[] = [];
3021
let middlewares: string[] = []
31-
for (const [i, middleware] of middlewareConfigs.entries()) {
22+
for (const [i, middleware] of interactConfig.middlewares.entries()) {
3223
let importPath = middleware.importPath;
3324
if (importPath.startsWith("./")) {
3425
importPath = path.resolve(interactConfig.paths.rootDirectory, importPath);

0 commit comments

Comments
 (0)