@moriajs/core 0.1.1 → 0.3.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/dist/router.js ADDED
@@ -0,0 +1,220 @@
1
+ /**
2
+ * File-based routing for MoriaJS.
3
+ *
4
+ * Scans `src/routes/` for route files and auto-registers them with Fastify.
5
+ *
6
+ * Convention:
7
+ * src/routes/api/hello.ts → GET /api/hello (API handler)
8
+ * src/routes/api/users/[id].ts → GET /api/users/:id (API handler)
9
+ * src/routes/pages/index.ts → GET / (SSR page)
10
+ * src/routes/pages/about.ts → GET /about (SSR page)
11
+ *
12
+ * API routes export named HTTP method functions:
13
+ * export function GET(request, reply) { ... }
14
+ *
15
+ * Page routes export a Mithril component + optional data loader:
16
+ * export default { view() { return m('h1', 'Hello') } }
17
+ * export async function getServerData(request) { return { user: ... } }
18
+ */
19
+ import { glob } from 'glob';
20
+ import path from 'node:path';
21
+ import { pathToFileURL } from 'node:url';
22
+ import { scanMiddleware, getMiddlewareChain } from './middleware.js';
23
+ /** Supported HTTP methods in route files. */
24
+ const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
25
+ /**
26
+ * Convert a file path to a URL path.
27
+ *
28
+ * - Strips file extension
29
+ * - Converts `[param]` → `:param`
30
+ * - Converts `[...slug]` → `*`
31
+ * - Converts `index` → `/`
32
+ *
33
+ * @example
34
+ * filePathToUrlPath('api/users/[id].ts') → '/api/users/:id'
35
+ * filePathToUrlPath('pages/index.ts') → '/'
36
+ * filePathToUrlPath('pages/about.ts') → '/about'
37
+ */
38
+ export function filePathToUrlPath(filePath) {
39
+ // Remove extension
40
+ let route = filePath.replace(/\.(ts|js|mts|mjs)$/, '');
41
+ // Normalize separators
42
+ route = route.replace(/\\/g, '/');
43
+ // Convert [param] → :param
44
+ route = route.replace(/\[([^\].]+)\]/g, ':$1');
45
+ // Convert [...slug] → *
46
+ route = route.replace(/\[\.\.\.([^\]]+)\]/g, '*');
47
+ // Handle pages prefix — strip "pages" and make root-relative
48
+ if (route.startsWith('pages/')) {
49
+ route = route.slice(5); // remove "pages"
50
+ }
51
+ // Handle api prefix — keep "api"
52
+ // (no transformation needed)
53
+ // Handle index files → parent path
54
+ route = route.replace(/\/index$/, '');
55
+ // Ensure leading slash
56
+ if (!route.startsWith('/')) {
57
+ route = '/' + route;
58
+ }
59
+ // Root case
60
+ if (route === '') {
61
+ route = '/';
62
+ }
63
+ return route;
64
+ }
65
+ /**
66
+ * Scan a directory for route files and return discovered routes.
67
+ *
68
+ * @param routesDir - Absolute path to the routes directory (e.g., `<project>/src/routes`)
69
+ */
70
+ export async function scanRoutes(routesDir) {
71
+ const pattern = '**/*.{ts,js,mts,mjs}';
72
+ const files = await glob(pattern, {
73
+ cwd: routesDir,
74
+ posix: true,
75
+ ignore: ['**/_*', '**/*.d.ts', '**/*.test.*', '**/*.spec.*'],
76
+ });
77
+ const routes = [];
78
+ for (const file of files) {
79
+ const urlPath = filePathToUrlPath(file);
80
+ const type = file.startsWith('api/') ? 'api' : 'page';
81
+ const absolutePath = path.resolve(routesDir, file);
82
+ const fileUrl = pathToFileURL(absolutePath).href;
83
+ // Dynamically import the route module
84
+ let mod;
85
+ try {
86
+ mod = await import(fileUrl);
87
+ }
88
+ catch (err) {
89
+ console.warn(`[moria] Failed to load route: ${file}`, err);
90
+ continue;
91
+ }
92
+ if (type === 'page') {
93
+ // ─── Page route: expects default Mithril component ───────
94
+ const component = mod.default;
95
+ const getServerData = typeof mod.getServerData === 'function'
96
+ ? mod.getServerData
97
+ : undefined;
98
+ // Page routes can also export raw HTTP handlers (backward compat)
99
+ if (component && typeof component === 'object' && 'view' in component) {
100
+ routes.push({
101
+ filePath: file,
102
+ urlPath,
103
+ type: 'page',
104
+ methods: {},
105
+ component,
106
+ getServerData,
107
+ });
108
+ continue;
109
+ }
110
+ // Fallback: if default export is a function, treat as API-style handler
111
+ if (typeof component === 'function') {
112
+ routes.push({
113
+ filePath: file,
114
+ urlPath,
115
+ type: 'page',
116
+ methods: { get: component },
117
+ });
118
+ continue;
119
+ }
120
+ // Also check for named HTTP method exports
121
+ const methods = {};
122
+ for (const method of HTTP_METHODS) {
123
+ const handler = mod[method] ?? mod[method.toLowerCase()];
124
+ if (typeof handler === 'function') {
125
+ methods[method.toLowerCase()] = handler;
126
+ }
127
+ }
128
+ if (Object.keys(methods).length > 0) {
129
+ routes.push({ filePath: file, urlPath, type: 'page', methods });
130
+ continue;
131
+ }
132
+ console.warn(`[moria] Page route has no component or handlers: ${file}`);
133
+ }
134
+ else {
135
+ // ─── API route: expects named HTTP method exports ────────
136
+ const methods = {};
137
+ for (const method of HTTP_METHODS) {
138
+ const handler = mod[method] ?? mod[method.toLowerCase()];
139
+ if (typeof handler === 'function') {
140
+ methods[method.toLowerCase()] = handler;
141
+ }
142
+ }
143
+ // Also support default export as GET handler
144
+ if (typeof mod.default === 'function' && !methods.get) {
145
+ methods.get = mod.default;
146
+ }
147
+ if (Object.keys(methods).length === 0) {
148
+ console.warn(`[moria] Route file has no handlers: ${file}`);
149
+ continue;
150
+ }
151
+ routes.push({ filePath: file, urlPath, type: 'api', methods });
152
+ }
153
+ }
154
+ return routes;
155
+ }
156
+ /**
157
+ * Register discovered routes with a Fastify server.
158
+ *
159
+ * Page routes with Mithril components are auto-wrapped with SSR rendering.
160
+ * API routes are registered directly as Fastify handlers.
161
+ * Middleware from `_middleware.ts` files is attached as `preHandler` hooks.
162
+ */
163
+ export async function registerRoutes(server, routesDir, options = {}) {
164
+ const routes = await scanRoutes(routesDir);
165
+ const mode = options.mode ?? 'development';
166
+ const config = options.config ?? {};
167
+ // ─── Scan file-based middleware ──────────────────────────
168
+ const middlewareEntries = await scanMiddleware(routesDir);
169
+ if (middlewareEntries.length > 0) {
170
+ server.log.info(`Found ${middlewareEntries.length} middleware file(s): ${middlewareEntries.map((m) => m.scope || '/').join(', ')}`);
171
+ }
172
+ for (const route of routes) {
173
+ // Resolve middleware chain for this route
174
+ const chain = getMiddlewareChain(route.filePath, middlewareEntries);
175
+ const preHandler = chain.length > 0 ? chain : undefined;
176
+ // ─── Page route with Mithril component → SSR handler ─────
177
+ if (route.component) {
178
+ const component = route.component;
179
+ const getServerData = route.getServerData;
180
+ const clientEntry = config.vite?.clientEntry ?? '/src/entry-client.ts';
181
+ server.route({
182
+ method: 'GET',
183
+ url: route.urlPath,
184
+ preHandler,
185
+ handler: async (request, reply) => {
186
+ // Load server data if available
187
+ let initialData;
188
+ if (getServerData) {
189
+ initialData = (await getServerData(request));
190
+ }
191
+ // Dynamic import of renderer (avoids circular deps)
192
+ const { renderToString } = await import('@moriajs/renderer');
193
+ const html = await renderToString(component, {
194
+ title: component.title ?? 'MoriaJS App',
195
+ initialData,
196
+ mode,
197
+ clientEntry,
198
+ });
199
+ reply.type('text/html');
200
+ return html;
201
+ },
202
+ });
203
+ server.log.info(`Page: GET ${route.urlPath} → ${route.filePath} (SSR)`);
204
+ continue;
205
+ }
206
+ // ─── API / raw handler routes ────────────────────────────
207
+ for (const [method, handler] of Object.entries(route.methods)) {
208
+ server.route({
209
+ method: method.toUpperCase(),
210
+ url: route.urlPath,
211
+ preHandler,
212
+ handler: handler,
213
+ });
214
+ server.log.info(`Route: ${method.toUpperCase()} ${route.urlPath} → ${route.filePath}`);
215
+ }
216
+ }
217
+ server.log.info(`Registered ${routes.length} file-based route(s)`);
218
+ return routes;
219
+ }
220
+ //# sourceMappingURL=router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"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"}
package/dist/vite.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Vite integration for MoriaJS.
3
+ *
4
+ * Provides Vite dev server in middleware mode (development)
5
+ * and static file serving for production builds.
6
+ */
7
+ import type { FastifyInstance } from 'fastify';
8
+ import type { ViteDevServer } from 'vite';
9
+ import { type MoriaConfig } from './config.js';
10
+ /**
11
+ * Attach Vite dev server to Fastify in middleware mode.
12
+ * This enables HMR and on-the-fly module transformation.
13
+ */
14
+ export declare function createViteDevMiddleware(server: FastifyInstance, config?: Partial<MoriaConfig>): Promise<ViteDevServer>;
15
+ /**
16
+ * Serve production-built client assets via @fastify/static.
17
+ */
18
+ export declare function serveProductionAssets(server: FastifyInstance, config?: Partial<MoriaConfig>): Promise<void>;
19
+ /**
20
+ * Generate the HTML shell for a page, with appropriate script tags
21
+ * depending on dev vs production mode.
22
+ */
23
+ export declare function getHtmlScripts(mode: 'development' | 'production', config?: Partial<MoriaConfig>): string;
24
+ //# sourceMappingURL=vite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C;;;GAGG;AACH,wBAAsB,uBAAuB,CACzC,MAAM,EAAE,eAAe,EACvB,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAClC,OAAO,CAAC,aAAa,CAAC,CA4BxB;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACvC,MAAM,EAAE,eAAe,EACvB,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAClC,OAAO,CAAC,IAAI,CAAC,CAaf;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,aAAa,GAAG,YAAY,EAAE,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,MAAM,CAW5G"}
package/dist/vite.js ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Vite integration for MoriaJS.
3
+ *
4
+ * Provides Vite dev server in middleware mode (development)
5
+ * and static file serving for production builds.
6
+ */
7
+ /**
8
+ * Attach Vite dev server to Fastify in middleware mode.
9
+ * This enables HMR and on-the-fly module transformation.
10
+ */
11
+ export async function createViteDevMiddleware(server, config = {}) {
12
+ const { createServer: createViteServer } = await import('vite');
13
+ const middie = (await import('@fastify/middie')).default;
14
+ // Register Express-style middleware support
15
+ await server.register(middie);
16
+ const vite = await createViteServer({
17
+ root: config.rootDir ?? process.cwd(),
18
+ configFile: config.vite?.configFile,
19
+ server: {
20
+ middlewareMode: true,
21
+ hmr: true,
22
+ },
23
+ appType: 'custom',
24
+ });
25
+ // Use Vite's connect middleware stack
26
+ server.use(vite.middlewares);
27
+ server.log.info('Vite dev server attached (HMR enabled)');
28
+ // Ensure Vite is closed when Fastify shuts down
29
+ server.addHook('onClose', async () => {
30
+ await vite.close();
31
+ });
32
+ return vite;
33
+ }
34
+ /**
35
+ * Serve production-built client assets via @fastify/static.
36
+ */
37
+ export async function serveProductionAssets(server, config = {}) {
38
+ const path = await import('node:path');
39
+ const fastifyStatic = (await import('@fastify/static')).default;
40
+ const distDir = path.resolve(config.rootDir ?? process.cwd(), 'dist', 'client');
41
+ await server.register(fastifyStatic, {
42
+ root: distDir,
43
+ prefix: '/assets/',
44
+ decorateReply: false,
45
+ });
46
+ server.log.info(`Serving static assets from ${distDir}`);
47
+ }
48
+ /**
49
+ * Generate the HTML shell for a page, with appropriate script tags
50
+ * depending on dev vs production mode.
51
+ */
52
+ export function getHtmlScripts(mode, config = {}) {
53
+ if (mode === 'development') {
54
+ const clientEntry = config.vite?.clientEntry ?? '/src/entry-client.ts';
55
+ return [
56
+ `<script type="module" src="/@vite/client"></script>`,
57
+ `<script type="module" src="${clientEntry}"></script>`,
58
+ ].join('\n ');
59
+ }
60
+ // Production: reference the built bundle
61
+ return `<script type="module" src="/assets/entry-client.js"></script>`;
62
+ }
63
+ //# sourceMappingURL=vite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite.js","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CACzC,MAAuB,EACvB,SAA+B,EAAE;IAEjC,MAAM,EAAE,YAAY,EAAE,gBAAgB,EAAE,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC;IAChE,MAAM,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,OAAO,CAAC;IAEzD,4CAA4C;IAC5C,MAAM,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAE9B,MAAM,IAAI,GAAG,MAAM,gBAAgB,CAAC;QAChC,IAAI,EAAE,MAAM,CAAC,OAAO,IAAI,OAAO,CAAC,GAAG,EAAE;QACrC,UAAU,EAAE,MAAM,CAAC,IAAI,EAAE,UAAU;QACnC,MAAM,EAAE;YACJ,cAAc,EAAE,IAAI;YACpB,GAAG,EAAE,IAAI;SACZ;QACD,OAAO,EAAE,QAAQ;KACpB,CAAC,CAAC;IAEH,sCAAsC;IACtC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAE7B,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,wCAAwC,CAAC,CAAC;IAE1D,gDAAgD;IAChD,MAAM,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,IAAI,EAAE;QACjC,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,OAAO,IAAI,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACvC,MAAuB,EACvB,SAA+B,EAAE;IAEjC,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC;IACvC,MAAM,aAAa,GAAG,CAAC,MAAM,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,OAAO,CAAC;IAEhE,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,IAAI,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;IAEhF,MAAM,MAAM,CAAC,QAAQ,CAAC,aAAa,EAAE;QACjC,IAAI,EAAE,OAAO;QACb,MAAM,EAAE,UAAU;QAClB,aAAa,EAAE,KAAK;KACvB,CAAC,CAAC;IAEH,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,8BAA8B,OAAO,EAAE,CAAC,CAAC;AAC7D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,IAAkC,EAAE,SAA+B,EAAE;IAChG,IAAI,IAAI,KAAK,aAAa,EAAE,CAAC;QACzB,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,EAAE,WAAW,IAAI,sBAAsB,CAAC;QACvE,OAAO;YACH,qDAAqD;YACrD,8BAA8B,WAAW,aAAa;SACzD,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACrB,CAAC;IAED,yCAAyC;IACzC,OAAO,+DAA+D,CAAC;AAC3E,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moriajs/core",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "MoriaJS framework core — Fastify server, Vite integration, and routing",
6
6
  "main": "./dist/index.js",
@@ -18,13 +18,19 @@
18
18
  "@fastify/compress": "^8.0.0",
19
19
  "@fastify/static": "^9.0.0",
20
20
  "@fastify/helmet": "^13.0.0",
21
- "pino-pretty": "^13.0.0"
21
+ "@fastify/middie": "^9.0.0",
22
+ "pino-pretty": "^13.0.0",
23
+ "vite": "^6.0.0",
24
+ "glob": "^11.0.0"
22
25
  },
23
26
  "devDependencies": {
24
27
  "typescript": "^5.7.0",
25
28
  "rimraf": "^6.0.0",
26
29
  "@types/node": "^22.0.0"
27
30
  },
31
+ "peerDependencies": {
32
+ "@moriajs/renderer": "0.3.0"
33
+ },
28
34
  "license": "MIT",
29
35
  "author": "Guntur-D <guntur.d.npm@gmail.com>",
30
36
  "repository": {
package/src/app.ts CHANGED
@@ -3,8 +3,13 @@ import cors from '@fastify/cors';
3
3
  import cookie from '@fastify/cookie';
4
4
  import compress from '@fastify/compress';
5
5
  import helmet from '@fastify/helmet';
6
+ import path from 'node:path';
7
+ import fs from 'node:fs';
6
8
  import { type MoriaConfig } from './config.js';
7
9
  import { type MoriaPlugin } from './plugins.js';
10
+ import { createViteDevMiddleware, serveProductionAssets } from './vite.js';
11
+ import { registerRoutes } from './router.js';
12
+ import type { ViteDevServer } from 'vite';
8
13
 
9
14
  /**
10
15
  * Options for creating a MoriaJS application.
@@ -23,6 +28,8 @@ export interface MoriaAppOptions {
23
28
  export interface MoriaApp {
24
29
  /** The underlying Fastify instance */
25
30
  server: FastifyInstance;
31
+ /** The Vite dev server (only in development mode) */
32
+ vite?: ViteDevServer;
26
33
  /** Register a MoriaJS plugin */
27
34
  use: (plugin: MoriaPlugin) => Promise<void>;
28
35
  /** Start the server */
@@ -38,12 +45,14 @@ export interface MoriaApp {
38
45
  * ```ts
39
46
  * import { createApp } from '@moriajs/core';
40
47
  *
41
- * const app = await createApp();
48
+ * const app = await createApp({ config: { mode: 'development' } });
42
49
  * await app.listen({ port: 3000 });
43
50
  * ```
44
51
  */
45
52
  export async function createApp(options: MoriaAppOptions = {}): Promise<MoriaApp> {
46
53
  const config = options.config ?? {};
54
+ const mode = config.mode ?? (process.env.NODE_ENV === 'production' ? 'production' : 'development');
55
+ const rootDir = config.rootDir ?? process.cwd();
47
56
  const port = config.server?.port ?? 3000;
48
57
  const host = config.server?.host ?? '0.0.0.0';
49
58
 
@@ -52,7 +61,7 @@ export async function createApp(options: MoriaAppOptions = {}): Promise<MoriaApp
52
61
  logger: {
53
62
  level: config.server?.logLevel ?? 'info',
54
63
  transport:
55
- process.env.NODE_ENV !== 'production'
64
+ mode !== 'production'
56
65
  ? { target: 'pino-pretty', options: { colorize: true } }
57
66
  : undefined,
58
67
  },
@@ -69,17 +78,41 @@ export async function createApp(options: MoriaAppOptions = {}): Promise<MoriaApp
69
78
  await server.register(compress);
70
79
  await server.register(helmet, {
71
80
  // Relax CSP in development for Vite HMR
72
- contentSecurityPolicy: process.env.NODE_ENV === 'production',
81
+ contentSecurityPolicy: mode === 'production',
73
82
  });
74
83
 
84
+ // ─── Global Middleware ─────────────────────────────────────
85
+ if (config.middleware && config.middleware.length > 0) {
86
+ for (const mw of config.middleware) {
87
+ server.addHook('onRequest', mw);
88
+ }
89
+ server.log.info(`Registered ${config.middleware.length} global middleware(s)`);
90
+ }
91
+
75
92
  // Health check route
76
93
  server.get('/health', async () => ({ status: 'ok', timestamp: new Date().toISOString() }));
77
94
 
95
+ // ─── Vite Integration ────────────────────────────────────
96
+ let vite: ViteDevServer | undefined;
97
+
98
+ if (mode === 'development') {
99
+ vite = await createViteDevMiddleware(server, { ...config, rootDir });
100
+ } else {
101
+ await serveProductionAssets(server, { ...config, rootDir });
102
+ }
103
+
104
+ // ─── File-Based Routing ──────────────────────────────────
105
+ const routesDir = path.resolve(rootDir, config.routes?.dir ?? 'src/routes');
106
+ if (fs.existsSync(routesDir)) {
107
+ await registerRoutes(server, routesDir, { mode, config });
108
+ }
109
+
78
110
  // Plugin registry
79
111
  const plugins: MoriaPlugin[] = [];
80
112
 
81
113
  const app: MoriaApp = {
82
114
  server,
115
+ vite,
83
116
 
84
117
  async use(plugin: MoriaPlugin) {
85
118
  plugins.push(plugin);
package/src/config.ts CHANGED
@@ -3,6 +3,12 @@
3
3
  * Used in `moria.config.ts` files in user projects.
4
4
  */
5
5
  export interface MoriaConfig {
6
+ /** Application mode */
7
+ mode?: 'development' | 'production';
8
+
9
+ /** Project root directory (auto-detected if not set) */
10
+ rootDir?: string;
11
+
6
12
  /** Server configuration */
7
13
  server?: {
8
14
  /** Port to listen on (default: 3000) */
@@ -44,7 +50,18 @@ export interface MoriaConfig {
44
50
  vite?: {
45
51
  /** Path to Vite config file */
46
52
  configFile?: string;
53
+ /** Client entry point (default: '/src/entry-client.ts') */
54
+ clientEntry?: string;
47
55
  };
56
+
57
+ /** File-based routing configuration */
58
+ routes?: {
59
+ /** Routes directory relative to rootDir (default: 'src/routes') */
60
+ dir?: string;
61
+ };
62
+
63
+ /** Global middleware (runs on every request) */
64
+ middleware?: Array<import('./middleware.js').MoriaMiddleware>;
48
65
  }
49
66
 
50
67
  /**
package/src/index.ts CHANGED
@@ -2,13 +2,19 @@
2
2
  * @moriajs/core
3
3
  *
4
4
  * Framework core: Fastify server factory with sensible defaults,
5
- * plugin registration, and configuration system.
5
+ * plugin registration, Vite integration, file-based routing,
6
+ * and middleware system.
6
7
  */
7
8
 
8
9
  export { createApp } from './app.js';
9
10
  export { defineConfig } from './config.js';
10
11
  export { defineMoriaPlugin } from './plugins.js';
12
+ export { defineMiddleware } from './middleware.js';
13
+ export { createViteDevMiddleware, serveProductionAssets, getHtmlScripts } from './vite.js';
14
+ export { scanRoutes, registerRoutes, filePathToUrlPath } from './router.js';
11
15
 
12
16
  export type { MoriaApp, MoriaAppOptions } from './app.js';
13
17
  export type { MoriaConfig } from './config.js';
14
18
  export type { MoriaPlugin, MoriaPluginContext } from './plugins.js';
19
+ export type { MoriaMiddleware, MiddlewareEntry } from './middleware.js';
20
+ export type { RouteEntry, RouteHandler } from './router.js';
@@ -0,0 +1,159 @@
1
+ /**
2
+ * MoriaJS Middleware System
3
+ *
4
+ * Provides file-based middleware via `_middleware.ts` files and
5
+ * a `defineMiddleware` helper for type-safe middleware definition.
6
+ *
7
+ * Middleware files are scoped to sibling and child routes:
8
+ * src/routes/_middleware.ts → applies to ALL routes
9
+ * src/routes/api/_middleware.ts → applies to /api/* routes
10
+ * src/routes/pages/_middleware.ts → applies to page routes
11
+ *
12
+ * Middleware functions run as Fastify preHandler hooks in order:
13
+ * root → parent → child (outermost first)
14
+ */
15
+
16
+ import type { FastifyRequest, FastifyReply } from 'fastify';
17
+ import { glob } from 'glob';
18
+ import path from 'node:path';
19
+ import { pathToFileURL } from 'node:url';
20
+
21
+ /**
22
+ * A MoriaJS middleware function.
23
+ *
24
+ * - Return nothing to continue to the next middleware / handler
25
+ * - Call `reply.send()` or return a value to short-circuit
26
+ */
27
+ export type MoriaMiddleware = (
28
+ request: FastifyRequest,
29
+ reply: FastifyReply
30
+ ) => void | Promise<void>;
31
+
32
+ /**
33
+ * A resolved middleware entry from a `_middleware.ts` file.
34
+ */
35
+ export interface MiddlewareEntry {
36
+ /** Directory path relative to routes dir (e.g., '', 'api', 'api/admin') */
37
+ scope: string;
38
+ /** URL prefix this middleware applies to */
39
+ urlPrefix: string;
40
+ /** Ordered array of middleware functions */
41
+ handlers: MoriaMiddleware[];
42
+ }
43
+
44
+ /**
45
+ * Define a type-safe middleware function.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * import { defineMiddleware } from '@moriajs/core';
50
+ *
51
+ * export default defineMiddleware(async (request, reply) => {
52
+ * request.log.info('request received');
53
+ * });
54
+ * ```
55
+ */
56
+ export function defineMiddleware(fn: MoriaMiddleware): MoriaMiddleware {
57
+ return fn;
58
+ }
59
+
60
+ /**
61
+ * Scan a routes directory for `_middleware.ts` files and return
62
+ * resolved middleware entries, sorted from root → deepest.
63
+ */
64
+ export async function scanMiddleware(routesDir: string): Promise<MiddlewareEntry[]> {
65
+ const pattern = '**/_middleware.{ts,js,mts,mjs}';
66
+ const files = await glob(pattern, {
67
+ cwd: routesDir,
68
+ posix: true,
69
+ });
70
+
71
+ const entries: MiddlewareEntry[] = [];
72
+
73
+ for (const file of files) {
74
+ const dir = path.posix.dirname(file); // e.g., '.', 'api', 'pages/admin'
75
+ const scope = dir === '.' ? '' : dir;
76
+
77
+ // Build URL prefix from scope
78
+ let urlPrefix = '/';
79
+ if (scope) {
80
+ // pages/ prefix is stripped in route URLs
81
+ let adjustedScope = scope;
82
+ if (adjustedScope.startsWith('pages')) {
83
+ adjustedScope = adjustedScope.slice(5); // remove 'pages'
84
+ }
85
+ if (adjustedScope && !adjustedScope.startsWith('/')) {
86
+ adjustedScope = '/' + adjustedScope;
87
+ }
88
+ urlPrefix = adjustedScope || '/';
89
+ }
90
+
91
+ const absolutePath = path.resolve(routesDir, file);
92
+ const fileUrl = pathToFileURL(absolutePath).href;
93
+
94
+ let mod: Record<string, unknown>;
95
+ try {
96
+ mod = await import(fileUrl);
97
+ } catch (err) {
98
+ console.warn(`[moria] Failed to load middleware: ${file}`, err);
99
+ continue;
100
+ }
101
+
102
+ // Resolve handlers from default export
103
+ const exported = mod.default;
104
+ let handlers: MoriaMiddleware[] = [];
105
+
106
+ if (Array.isArray(exported)) {
107
+ handlers = exported.filter((fn) => typeof fn === 'function') as MoriaMiddleware[];
108
+ } else if (typeof exported === 'function') {
109
+ handlers = [exported as MoriaMiddleware];
110
+ }
111
+
112
+ if (handlers.length === 0) {
113
+ console.warn(`[moria] Middleware file has no handlers: ${file}`);
114
+ continue;
115
+ }
116
+
117
+ entries.push({ scope, urlPrefix, handlers });
118
+ }
119
+
120
+ // Sort by scope depth (root first, deeper scopes later)
121
+ entries.sort((a, b) => {
122
+ const depthA = a.scope === '' ? 0 : a.scope.split('/').length;
123
+ const depthB = b.scope === '' ? 0 : b.scope.split('/').length;
124
+ return depthA - depthB;
125
+ });
126
+
127
+ return entries;
128
+ }
129
+
130
+ /**
131
+ * Get the ordered middleware chain for a given route URL path.
132
+ *
133
+ * Returns middleware from outermost (root) to innermost (closest parent).
134
+ *
135
+ * @param routeFilePath - File path relative to routes dir (e.g., 'api/hello.ts')
136
+ * @param entries - Scanned middleware entries
137
+ */
138
+ export function getMiddlewareChain(
139
+ routeFilePath: string,
140
+ entries: MiddlewareEntry[]
141
+ ): MoriaMiddleware[] {
142
+ const routeDir = path.posix.dirname(routeFilePath);
143
+ const chain: MoriaMiddleware[] = [];
144
+
145
+ for (const entry of entries) {
146
+ // Root middleware (scope '') applies to everything
147
+ if (entry.scope === '') {
148
+ chain.push(...entry.handlers);
149
+ continue;
150
+ }
151
+
152
+ // Check if the route file is within this middleware's scope
153
+ if (routeDir === entry.scope || routeDir.startsWith(entry.scope + '/')) {
154
+ chain.push(...entry.handlers);
155
+ }
156
+ }
157
+
158
+ return chain;
159
+ }