Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/dobs/src/_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export type LooseObject<KnownKeys extends string, ValueType> = Partial<
Record<string, ValueType>;

export type Promisable<T> = T | Promise<T>;
export type Maybe<T> = T | null | undefined;
16 changes: 15 additions & 1 deletion packages/dobs/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createServer } from 'node:http';
import type { Middleware } from '@dobsjs/http';
import { deepmerge } from 'deepmerge-ts';
import { createPluginRunner, Plugin } from './plugin';

export interface ResolvedServerConfig {
/** port to serve (default: 8080) */
Expand All @@ -27,6 +28,8 @@ export interface ResolvedServerConfig {
*/
directory: string;
};

plugins: Plugin[];
}

export type ServerConfig = Partial<ResolvedServerConfig>;
Expand All @@ -41,8 +44,19 @@ export const DEFAULT_CONFIG: ResolvedServerConfig = {
build: {
directory: 'dist',
},
plugins: [],
};

export function resolveConfig(config: ServerConfig): ResolvedServerConfig {
return deepmerge(DEFAULT_CONFIG, config) as ResolvedServerConfig;
const runner = createPluginRunner(config?.plugins ?? []);

// [plugin] execute plugin.config
runner.execute('config', config);

const resolvedConfig = deepmerge(DEFAULT_CONFIG, config) as ResolvedServerConfig;

// [plugin] execute plugin.resolvedConfig
runner.execute('resolvedConfig', resolvedConfig);

return resolvedConfig;
}
2 changes: 1 addition & 1 deletion packages/dobs/src/defineRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ import { Routes } from './types';
* });
* ```
*/
export function defineRoutes(routes: Routes, wrappers: any[] = []) {
export function defineRoutes(routes: Routes, wrappers: any[] = []): Routes {
return wrappers.reduce((acc, wrapper) => wrapper(acc), routes);
}
41 changes: 40 additions & 1 deletion packages/dobs/src/experimental/cache.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Routes, Handler } from '../types';
import { mutateObjectValues } from '../shared/object';
import { mutateObjectKeys, mutateObjectValues } from '../shared/object';
import { Plugin } from '../plugin';

export interface CacheOptions {
/**
Expand Down Expand Up @@ -108,6 +109,7 @@ function createWrapper(
*
* export default defineRoutes((req, res) => {}, [useCache()])
* ```
*
* The values included in the ID generation are as follows: handler ID (GET, ALL, etc.) and pathname.
* Values not included are: query, req.body, etc.
*/
Expand All @@ -128,3 +130,40 @@ export function useCache(cacheOptions?: CacheOptions) {
);
};
}

// internal cache plugin
// concept: $ALL => ALL (caching enabled)

/**
* Plugin that enables data caching
*/
export function cachePlugin(cacheOptions?: CacheOptions): Plugin {
const cache = new (cacheOptions?.customCache ?? TtlCache)(cacheOptions?.ttl);

return {
name: 'dobs/experimental/cache-plugin',

generateRoute(router) {
const routerType = typeof router === 'function' ? 'function' : 'object';

if (routerType === 'function') {
return router;
}

const wrappedRouter = mutateObjectValues<string, Handler>(
router as any,
(value, key) => {
if (key.startsWith('$')) {
return createWrapper(value, cache, key);
}
return value;
},
);

return mutateObjectKeys(wrappedRouter, (key) => {
if (key.startsWith('$')) return key.slice(1);
return key;
});
},
};
}
58 changes: 58 additions & 0 deletions packages/dobs/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { BuildOptions } from 'rolldown';

import type { ResolvedServerConfig, ServerConfig } from './config';
import type { Promisable, Maybe } from './_types';
import type { Routes } from './types';

export interface Plugin {
name: string;

/** modify server config */
config?(config: ServerConfig): Maybe<ServerConfig>;

/** access to resolved server config */
resolvedConfig?(config: ResolvedServerConfig): Maybe<ResolvedServerConfig>;

/**
* resolve rolldown build options
* https://rolldown.rs/options/input
*/
resolveBuildOptions?(buildOptions: BuildOptions): Maybe<BuildOptions>;

generateRoute?(route: Routes): Promisable<Maybe<Routes>>;
}

type FunctionKeys<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
}[keyof T];

export function createPluginRunner(plugins: Plugin[]) {
return {
plugins,

async execute<K extends FunctionKeys<Plugin>>(
key: K,
...args: Parameters<NonNullable<Plugin[K]>>
): Promise<ReturnType<NonNullable<Plugin[K]>>> {
let result: any = args[0];

for (const plugin of plugins) {
const fn = plugin[key];
if (typeof fn !== 'function') continue;

try {
const returned = await (fn as any).apply(plugin, [result]);
if (returned !== undefined && returned !== null) {
result = returned;
}
} catch (e) {
throw new Error(
`[${plugin.name}] error has occurred during executing ${plugin.name}.${key} - ${e.message}`,
);
}
}

return result;
},
};
}
7 changes: 7 additions & 0 deletions packages/dobs/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Server } from 'node:http';
import httpServer, { Middleware } from '@dobsjs/http';
import { resolveConfig, ServerConfig } from '~/dobs/config';
import { createRouterMiddleware } from './router';
import { createPluginRunner } from '../plugin';

type CreateServerReturn<T extends ServerConfig> = T['mode'] extends 'middleware'
? Middleware[]
Expand All @@ -10,6 +11,12 @@ type CreateServerReturn<T extends ServerConfig> = T['mode'] extends 'middleware'
export async function createDobsServer<T extends ServerConfig>(
config?: T,
): Promise<CreateServerReturn<T>> {
const plugins = config?.plugins || [];
const runner = createPluginRunner(plugins);

// [plugin] execute plugin.config
await runner.execute('config', config);

const resolvedConfig = resolveConfig(config);
const server = httpServer(resolvedConfig.createServer);

Expand Down
44 changes: 28 additions & 16 deletions packages/dobs/src/server/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { lowercaseKeyObject } from '~/dobs/shared/object';
import { dynamicImport } from './load';
import nodeExternal from './plugins/external';
import { mkdirSync, writeFileSync } from 'node:fs';
import { createPluginRunner } from '../plugin';

type HandlerType = ((req: AppRequest, res: AppResponse) => any) | Record<string, any>;

Expand Down Expand Up @@ -90,6 +91,7 @@ export function createInternalRouter(
cachedModule: Map<string, any>,
preloadedModules?: Map<string, any>,
) {
const pluginRunner = createPluginRunner(config.plugins);
const routesDirectory = join(config.cwd, 'app');

const matchRoute = (url: string) => routes.find((route) => route.regex.test(url));
Expand All @@ -113,7 +115,7 @@ export function createInternalRouter(

try {
const method = (req.method || '').toLowerCase();
const handlers: PageType = pageModule;
const handlers: PageType = await pluginRunner.execute('generateRoute', pageModule);

const execute = async (handler: HandlerType) => {
if (typeof handler !== 'function') return res.send(handler);
Expand All @@ -140,25 +142,35 @@ export function createInternalRouter(
export async function createRouterMiddleware(
config: ResolvedServerConfig,
): Promise<Middleware> {
const pluginRunner = createPluginRunner(config.plugins);

const routesDirectory = join(config.cwd, 'app');
const tempDirectory = join(config.cwd, config.temp, 'routes');
const tempDirectoryPackageJSON = join(config.cwd, config.temp, 'package.json');

const cachedModule = new Map<string, any>();
const buildOption: () => BuildOptions = () => ({
input: routes.map((route) => join(routesDirectory, route.relativePath)),
output: {
format: 'cjs',
sourcemap: true,
esModule: true,
dir: tempDirectory,
},
resolve: {
conditionNames: ['require', 'node', 'default'],
},
write: false,
// exclude /node_modules/
plugins: [nodeExternal()],
});
const buildOption: () => BuildOptions = () => {
const bo: BuildOptions = {
input: routes.map((route) => join(routesDirectory, route.relativePath)),
output: {
format: 'cjs',
sourcemap: true,
esModule: true,
dir: tempDirectory,
},
resolve: {
conditionNames: ['require', 'node', 'default'],
},
write: false,
// exclude /node_modules/
plugins: [nodeExternal()],
};

// [plugin] execute plugin.resolveBuildOptions
pluginRunner.execute('resolveBuildOptions', bo);

return bo;
};
let routes = createRoutes(config);

// build initially (prod/dev)
Expand Down
11 changes: 10 additions & 1 deletion packages/dobs/src/shared/object.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { lowercaseKeyObject, mutateObjectValues } from './object';
import { lowercaseKeyObject, mutateObjectKeys, mutateObjectValues } from './object';

describe('lowercaseKeyObject', () => {
it('should lowercase keys of object', () => {
Expand All @@ -15,3 +15,12 @@ describe('mutateObjectValues', () => {
});
});
});

describe('mutateObjectKeys', () => {
it('should mutate keys of object', () => {
expect(mutateObjectKeys({ a: 5, b: 10 }, (k) => k + '0')).toStrictEqual({
a0: 5,
b0: 10,
});
});
});
21 changes: 21 additions & 0 deletions packages/dobs/src/shared/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,24 @@ export function mutateObjectValues<Key = string, Value = any>(
}
return result;
}

export function mutateObjectKeys<T extends Record<string, any>>(
obj: T,
fn: (key: string) => string,
): any {
if (typeof obj !== 'object' || obj === null) return obj;

if (Array.isArray(obj)) {
return obj.map((item) => mutateObjectKeys(item, fn));
}

const result: Record<string, any> = {};

for (const [key, value] of Object.entries(obj)) {
const newKey = fn(key);
result[newKey] =
typeof value === 'object' && value !== null ? mutateObjectKeys(value, fn) : value;
}

return result;
}
7 changes: 3 additions & 4 deletions packages/dobs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ import type { LooseObject, Promisable } from './_types';

export type Handler = (req: AppRequest, res: AppResponse) => Promisable<any>;

export type RouterKey = 'ALL' | 'GET' | 'POST' | 'DELETE' | 'UPDATE';

export type Routes =
| LooseObject<
'ALL' | 'GET' | 'POST' | 'DELETE' | 'UPDATE',
Handler | Record<string, any>
>
| LooseObject<RouterKey | `$${RouterKey}`, Handler | Record<string, any>>
| Handler;

export function defineConfig(userConfig: Omit<ServerConfig, 'mode'>) {
Expand Down
18 changes: 7 additions & 11 deletions playgrounds/default/app/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { defineRoutes } from 'dobs';
import { useCache } from 'dobs/experimental';

export default defineRoutes(
{
GET(req, res) {
setTimeout(() => {
res.send('Dynamic Handler~!');
}, 1000);
},
POST: { message: 'This is static data' },
export default defineRoutes({
$GET(req, res) {
setTimeout(() => {
res.send('Dynamic Handler~!');
}, 1000);
},
[useCache({ ttl: 1000 })],
);
POST: { message: 'This is static data' },
});
3 changes: 3 additions & 0 deletions playgrounds/default/dobs.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { cachePlugin } from 'dobs/experimental';

export default {
port: 3000,
plugins: [cachePlugin({ ttl: 1000 })],
} satisfies import('dobs').ServerConfig;