;
cors?: CORSOptions;
csrf?: CSRFOptions;
- plugins: BuildPluginReturn[];
+ vitNodeApiConfig: VitNodeApiConfig;
+ vitNodeConfig: VitNodeConfig;
}) {
app.doc('/swagger/doc', {
openapi: '3.0.0',
@@ -54,7 +54,14 @@ export function VitNodeAPI({
app.use(cors(corsOptions));
app.use(csrf(csrfOptions));
app.get('/swagger', swaggerUI({ url: `/api/swagger/doc` }));
- app.use('*', globalMiddleware(options));
+ app.use(
+ '*',
+ globalMiddleware({
+ emailProvider: vitNodeApiConfig.emailProvider,
+ metadata: vitNodeConfig.metadata,
+ authorization: vitNodeApiConfig.authorization,
+ }),
+ );
app.use('/*/admin/*', globalAdminMiddleware());
app.onError(error => {
@@ -75,7 +82,7 @@ export function VitNodeAPI({
);
});
- [newBuildPluginCore, ...plugins].map(root => {
+ [newBuildPluginApiCore, ...vitNodeApiConfig.plugins].map(root => {
app.route(`/${root.name}`, root.hono);
});
diff --git a/packages/vitnode/src/api/lib/plugin.ts b/packages/vitnode/src/api/lib/plugin.ts
new file mode 100644
index 000000000..2bd77db6c
--- /dev/null
+++ b/packages/vitnode/src/api/lib/plugin.ts
@@ -0,0 +1,23 @@
+import { OpenAPIHono } from '@hono/zod-openapi';
+
+import type { BuildModuleReturn } from './module';
+
+export interface BuildPluginApiReturn {
+ hono: OpenAPIHono;
+ name: string;
+}
+
+export function buildApiPlugin({
+ name,
+ modules = [],
+}: {
+ modules?: BuildModuleReturn
[];
+ name: P;
+}): BuildPluginApiReturn {
+ const hono = new OpenAPIHono();
+ modules.forEach(handler => {
+ hono.route(`/${handler.name}`, handler.hono);
+ });
+
+ return { name, hono };
+}
diff --git a/packages/vitnode/src/api/middlewares/global/global.ts b/packages/vitnode/src/api/middlewares/global/global.ts
index 345c968cb..56b299209 100644
--- a/packages/vitnode/src/api/middlewares/global/global.ts
+++ b/packages/vitnode/src/api/middlewares/global/global.ts
@@ -1,4 +1,5 @@
import type { EmailApiPlugin } from '@/api/models/email';
+import type { VitNodeApiConfig, VitNodeConfig } from '@/vitnode.config';
import type { Context, Env, Next } from 'hono';
import { DeviceModel } from '@/api/models/device';
@@ -61,23 +62,8 @@ export const globalMiddleware = ({
authorization,
metadata,
emailProvider,
-}: {
- authorization?: {
- adminCookieExpires?: number;
- adminCookieName?: string;
- cookieExpires?: number;
- cookieName?: string;
- cookieSecure?: boolean;
- deviceCookieExpires?: number;
- deviceCookieName?: string;
- ssoPlugins?: SSOApiPlugin[];
- };
- emailProvider?: EmailApiPlugin;
- metadata: {
- shortTitle?: string;
- title: string;
- };
-}) => {
+}: Pick &
+ Pick) => {
return async (c: Context, next: Next) => {
c.set('core', {
metadata,
diff --git a/packages/vitnode/src/api/plugin.ts b/packages/vitnode/src/api/plugin.ts
index 7e4fc30b8..b582d2bb9 100644
--- a/packages/vitnode/src/api/plugin.ts
+++ b/packages/vitnode/src/api/plugin.ts
@@ -1,9 +1,9 @@
-import { buildPlugin } from '../lib/plugin';
+import { buildApiPlugin } from './lib/plugin';
import { adminModule } from './modules/admin/admin.module';
import { middlewareModule } from './modules/middleware/middleware.module';
import { usersModule } from './modules/users/users.module';
-export const newBuildPluginCore = buildPlugin({
+export const newBuildPluginApiCore = buildApiPlugin({
name: 'core',
modules: [middlewareModule, usersModule, adminModule],
});
diff --git a/packages/vitnode/src/database/schema/languages.ts b/packages/vitnode/src/database/schema/languages.ts
index 7719c8ea7..f7dd32998 100644
--- a/packages/vitnode/src/database/schema/languages.ts
+++ b/packages/vitnode/src/database/schema/languages.ts
@@ -16,8 +16,7 @@ export const core_languages = pgTable(
.timestamp()
.notNull()
.$onUpdate(() => new Date()),
- time_24: t.boolean().notNull().default(false),
- allow_in_input: t.boolean().default(true).notNull(),
+ time24: t.boolean().notNull().default(false),
}),
t => [
index('core_languages_code_idx').on(t.code),
diff --git a/packages/vitnode/src/lib/plugin.ts b/packages/vitnode/src/lib/plugin.ts
index 6992a2e9f..33731427d 100644
--- a/packages/vitnode/src/lib/plugin.ts
+++ b/packages/vitnode/src/lib/plugin.ts
@@ -1,23 +1,11 @@
-import { OpenAPIHono } from '@hono/zod-openapi';
-
-import type { BuildModuleReturn } from '../api/lib/module';
-
-export interface BuildPluginReturn {
- hono: OpenAPIHono;
- name: string;
+export interface BuildPluginReturn {
+ name: P;
+ pages?: () => React.ReactNode;
}
export function buildPlugin
({
name,
- modules = [],
-}: {
- modules?: BuildModuleReturn
[];
- name: P;
-}): BuildPluginReturn {
- const hono = new OpenAPIHono();
- modules.forEach(handler => {
- hono.route(`/${handler.name}`, handler.hono);
- });
-
- return { name, hono };
+ pages,
+}: BuildPluginReturn
): BuildPluginReturn
{
+ return { name, pages };
}
diff --git a/packages/vitnode/src/views/dynamic-view.tsx b/packages/vitnode/src/views/dynamic-view.tsx
index 9b331ecb6..886c9c573 100644
--- a/packages/vitnode/src/views/dynamic-view.tsx
+++ b/packages/vitnode/src/views/dynamic-view.tsx
@@ -41,6 +41,7 @@ export const generateMetadataDynamicView = async ({
export const DynamicView = async ({
params,
searchParams,
+ config,
}: DynamicViewProps & {
config: VitNodeConfig;
}) => {
@@ -75,6 +76,15 @@ export const DynamicView = async ({
return view;
}
+ // Try to render a plugin view if available
+ const plugin = config.plugins.find(
+ p => p.name === rest[0] && typeof p.pages === 'function',
+ );
+
+ if (plugin?.pages) {
+ return plugin.pages();
+ }
+
notFound();
};
diff --git a/packages/vitnode/src/vitnode.config.ts b/packages/vitnode/src/vitnode.config.ts
index 0b7eba16e..3269aa68a 100644
--- a/packages/vitnode/src/vitnode.config.ts
+++ b/packages/vitnode/src/vitnode.config.ts
@@ -1,7 +1,9 @@
import type { ThemeProvider } from 'next-themes';
-import type { BuildPluginReturn } from './lib/plugin';
+import type { BuildPluginApiReturn } from './api/lib/plugin';
import type { EmailApiPlugin } from './api/models/email';
+import type { SSOApiPlugin } from './api/models/sso';
+import type { BuildPluginReturn } from './lib/plugin';
export interface VitNodeConfig {
debug?: boolean;
@@ -22,6 +24,21 @@ export interface VitNodeConfig {
>;
}
+export interface VitNodeApiConfig {
+ authorization?: {
+ adminCookieExpires?: number;
+ adminCookieName?: string;
+ cookieExpires?: number;
+ cookieName?: string;
+ cookieSecure?: boolean;
+ deviceCookieExpires?: number;
+ deviceCookieName?: string;
+ ssoPlugins?: SSOApiPlugin[];
+ };
+ emailProvider?: EmailApiPlugin;
+ plugins: BuildPluginApiReturn[];
+}
+
export function buildConfig(
args: VitNodeConfig,
): VitNodeConfig {
@@ -34,6 +51,10 @@ export function buildConfig(
};
}
+export function buildApiConfig(args: VitNodeApiConfig): VitNodeApiConfig {
+ return args;
+}
+
export const handleRequestConfig = async ({
requestLocale,
vitNodeConfig,
diff --git a/plugins/blog/package.json b/plugins/blog/package.json
index 5aede2486..e418504e9 100644
--- a/plugins/blog/package.json
+++ b/plugins/blog/package.json
@@ -34,6 +34,8 @@
"devDependencies": {
"@swc/cli": "0.6.0",
"@swc/core": "^1.11.24",
+ "@types/react": "^19.1.3",
+ "@types/react-dom": "^19.1.5",
"concurrently": "^9.1.2",
"eslint": "^9.25.1",
"eslint-config-typescript-vitnode": "workspace:*",
diff --git a/plugins/blog/src/api.ts b/plugins/blog/src/api.ts
new file mode 100644
index 000000000..a0e33347c
--- /dev/null
+++ b/plugins/blog/src/api.ts
@@ -0,0 +1,11 @@
+import { buildApiPlugin } from 'vitnode/api/lib/plugin';
+
+import { categoriesModule } from './api/modules/categories/categories.module';
+import { configPlugin } from './config';
+
+export const blogApiPlugin = () => {
+ return buildApiPlugin({
+ ...configPlugin,
+ modules: [categoriesModule],
+ });
+};
diff --git a/plugins/blog/src/modules/categories/categories.module.ts b/plugins/blog/src/api/modules/categories/categories.module.ts
similarity index 100%
rename from plugins/blog/src/modules/categories/categories.module.ts
rename to plugins/blog/src/api/modules/categories/categories.module.ts
diff --git a/plugins/blog/src/modules/categories/route.ts b/plugins/blog/src/api/modules/categories/route.ts
similarity index 100%
rename from plugins/blog/src/modules/categories/route.ts
rename to plugins/blog/src/api/modules/categories/route.ts
diff --git a/plugins/blog/src/config.ts b/plugins/blog/src/config.ts
new file mode 100644
index 000000000..ca730fc84
--- /dev/null
+++ b/plugins/blog/src/config.ts
@@ -0,0 +1,3 @@
+export const configPlugin = {
+ name: 'blog',
+};
diff --git a/plugins/blog/src/plugin.config.ts b/plugins/blog/src/plugin.config.ts
deleted file mode 100644
index 9dab5187c..000000000
--- a/plugins/blog/src/plugin.config.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { buildPlugin } from 'vitnode/lib/plugin';
-
-import { categoriesModule } from './modules/categories/categories.module';
-
-export const blogPlugin = () => {
- return buildPlugin({
- name: 'blog',
- modules: [categoriesModule],
- });
-};
diff --git a/plugins/blog/src/plugin.tsx b/plugins/blog/src/plugin.tsx
new file mode 100644
index 000000000..714c43d50
--- /dev/null
+++ b/plugins/blog/src/plugin.tsx
@@ -0,0 +1,13 @@
+import { buildPlugin } from 'vitnode/lib/plugin';
+
+import { configPlugin } from './config';
+import { Test } from './views/test';
+
+export const blogPlugin = () => {
+ return buildPlugin({
+ ...configPlugin,
+ pages: () => {
+ return ;
+ },
+ });
+};
diff --git a/plugins/blog/src/views/test.tsx b/plugins/blog/src/views/test.tsx
new file mode 100644
index 000000000..18c67c7f2
--- /dev/null
+++ b/plugins/blog/src/views/test.tsx
@@ -0,0 +1,8 @@
+export const Test = () => {
+ return (
+
+
Test
+
This is a test page.
+
+ );
+};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 34e77f48f..246fd8411 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -460,6 +460,12 @@ importers:
'@swc/core':
specifier: ^1.11.24
version: 1.11.24
+ '@types/react':
+ specifier: ^19.1.3
+ version: 19.1.3
+ '@types/react-dom':
+ specifier: ^19.1.5
+ version: 19.1.5(@types/react@19.1.3)
concurrently:
specifier: ^9.1.2
version: 9.1.2
@@ -3409,6 +3415,11 @@ packages:
peerDependencies:
'@types/react': ^19.0.0
+ '@types/react-dom@19.1.5':
+ resolution: {integrity: sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==}
+ peerDependencies:
+ '@types/react': ^19.0.0
+
'@types/react-reconciler@0.28.9':
resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==}
peerDependencies:
@@ -9922,6 +9933,10 @@ snapshots:
dependencies:
'@types/react': 19.1.3
+ '@types/react-dom@19.1.5(@types/react@19.1.3)':
+ dependencies:
+ '@types/react': 19.1.3
+
'@types/react-reconciler@0.28.9(@types/react@19.1.3)':
dependencies:
'@types/react': 19.1.3