clovie 0.1.38 → 0.1.39

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.
@@ -3,7 +3,6 @@ import { Engine } from '@jucie.io/engine';
3
3
  import { Compile } from './services/Compile.js';
4
4
  import { Run } from './services/Run.js';
5
5
  import { Configurator } from './services/Configurator.js';
6
- import { Route } from './services/Router.js';
7
6
  import { transformConfig } from './utils/transformConfig.js';
8
7
  import { Server } from '@jucie.io/engine-server';
9
8
  import { EsBuildCompiler } from '@jucie.io/engine-esbuild';
@@ -22,7 +21,6 @@ export const createClovie = async (config = {}) => {
22
21
  clovie.configurator.onReady(async (opts) => {
23
22
  // Always install core services
24
23
  clovie.install(Compile)
25
- clovie.install(Route)
26
24
  clovie.install(Run)
27
25
 
28
26
  if (opts.scripts) {
@@ -0,0 +1 @@
1
+ export{ defineRoutes as defineApi } from '@jucie.io/engine-server';
@@ -0,0 +1 @@
1
+ export { defineHooks } from '@jucie.io/engine-server';
@@ -0,0 +1 @@
1
+ export { defineMiddleware } from '@jucie.io/engine-server';
@@ -0,0 +1 @@
1
+ export { defineRoutes } from '@jucie.io/engine-server';
package/lib/main.js ADDED
@@ -0,0 +1,5 @@
1
+ export { defineApi } from './factories/api.js';
2
+ export { defineRoutes } from './factories/routes.js';
3
+ export { defineHooks } from './factories/hooks.js';
4
+ export { defineMiddleware } from './factories/middleware.js';
5
+ export { createClovie } from './createClovie.js';
@@ -1,5 +1,9 @@
1
1
  import { ServiceProvider } from '@jucie.io/engine';
2
- import path from 'path';
2
+ import { normalizeToFactories } from '../utils/normalizeToFactories.js';
3
+ import { viewsToRoutes, pagesToRoutes } from '../utils/viewsToRoutes.js';
4
+ import { defineRoutes } from '../factories/routes.js';
5
+ import { defineHooks } from '../factories/hooks.js';
6
+ import { defineMiddleware } from '../factories/middleware.js';
3
7
 
4
8
  export class Run extends ServiceProvider {
5
9
  static manifest = {
@@ -112,10 +116,22 @@ export class Run extends ServiceProvider {
112
116
  }
113
117
 
114
118
  async #serve (opts) {
115
- const router = this.useContext('router');
116
- await router.setHooks(opts);
117
- await router.serveApi(opts);
118
- await router.serveRoutes(opts);
119
+ const [server, file, liveReload] = this.useContext('server', 'file', 'liveReload');
120
+ const services = { file, liveReload };
121
+
122
+ const viewRoutes = viewsToRoutes(opts, services);
123
+ const pageRoutes = pagesToRoutes(opts.routes, opts, services);
124
+
125
+ const factories = [
126
+ ...normalizeToFactories(opts.hooks, defineHooks),
127
+ ...normalizeToFactories(opts.middleware, defineMiddleware),
128
+ ...normalizeToFactories(opts.api, defineRoutes),
129
+ ...normalizeToFactories([...viewRoutes, ...pageRoutes], defineRoutes),
130
+ ];
131
+
132
+ if (factories.length) {
133
+ server.use(...factories);
134
+ }
119
135
  }
120
136
 
121
137
  async #watch(opts) {
@@ -0,0 +1,52 @@
1
+ import { definitionType } from '@jucie.io/engine';
2
+
3
+ /**
4
+ * Normalizes a config value into an array of engine-server factory definitions.
5
+ *
6
+ * Handles four input shapes:
7
+ * - Raw data (plain array or object) → wrapped in factoryFn
8
+ * - Single factory (created via createDefinition) → returned as-is
9
+ * - Array of factories → returned as-is
10
+ * - Intermixed (raw items + factories in the same array) → raw items
11
+ * are batched and wrapped, factories are preserved
12
+ *
13
+ * @param {*} value - The config value (routes, middleware, hooks, api, etc.)
14
+ * @param {Function} factoryFn - The define* factory to wrap raw data with
15
+ * @returns {Array} Array of factory definitions ready for server.use()
16
+ */
17
+ export function normalizeToFactories(value, factoryFn) {
18
+ if (!value) return [];
19
+
20
+ if (isFactory(value)) return [value];
21
+
22
+ if (!Array.isArray(value)) return [wrapRaw(factoryFn, value)];
23
+
24
+ const raw = [];
25
+ const factories = [];
26
+
27
+ for (const item of value) {
28
+ if (isFactory(item)) {
29
+ if (raw.length) {
30
+ factories.push(wrapRaw(factoryFn, [...raw]));
31
+ raw.length = 0;
32
+ }
33
+ factories.push(item);
34
+ } else {
35
+ raw.push(item);
36
+ }
37
+ }
38
+
39
+ if (raw.length) {
40
+ factories.push(wrapRaw(factoryFn, raw));
41
+ }
42
+
43
+ return factories;
44
+ }
45
+
46
+ function isFactory(value) {
47
+ return typeof value === 'function' && definitionType(value) !== undefined;
48
+ }
49
+
50
+ function wrapRaw(factoryFn, data) {
51
+ return factoryFn(() => data);
52
+ }
@@ -1,6 +1,6 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs';
3
- import { loadRenderEngine, validateRenderEngine } from './loadRenderEngine.js';
3
+ import { loadRenderEngine } from './loadRenderEngine.js';
4
4
 
5
5
  // Add this near the top of the file, after the imports
6
6
  const DATA_TYPES = {
@@ -195,40 +195,6 @@ export async function transformConfig(config, log = null) {
195
195
  if (discovered.data) {
196
196
  discovered.data = await resolveData(discovered.data);
197
197
  }
198
-
199
- // Middleware processing and adapter selection
200
- if (!discovered.adapter) {
201
- // Auto-detect adapter based on middleware presence
202
- if (discovered.middleware && discovered.middleware.length > 0) {
203
- discovered.adapter = 'express'; // Use Express for middleware
204
- logger.debug('Auto-selected Express adapter due to middleware configuration');
205
- } else {
206
- discovered.adapter = 'http'; // Default to HTTP adapter
207
- logger.debug('Using default HTTP adapter');
208
- }
209
- }
210
-
211
- // Validate middleware if provided
212
- if (discovered.middleware) {
213
- if (!Array.isArray(discovered.middleware)) {
214
- throw new Error('middleware must be an array of functions');
215
- }
216
-
217
- // Validate each middleware function
218
- discovered.middleware.forEach((mw, index) => {
219
- if (typeof mw !== 'function') {
220
- throw new Error(`middleware[${index}] must be a function, got ${typeof mw}`);
221
- }
222
- });
223
-
224
- logger.debug(`Configured ${discovered.middleware.length} middleware functions`);
225
-
226
- // Ensure Express adapter is used when middleware is present
227
- if (discovered.adapter !== 'express') {
228
- logger.warn('Middleware detected but adapter is not "express". Switching to Express adapter.');
229
- discovered.adapter = 'express';
230
- }
231
- }
232
198
 
233
199
  return discovered;
234
200
  }
@@ -0,0 +1,109 @@
1
+ import path from 'path';
2
+
3
+ /**
4
+ * Scans the views directory and produces engine-server route definitions.
5
+ * Each template file becomes a GET route whose handler renders the template.
6
+ *
7
+ * views/index.html → GET /
8
+ * views/about.html → GET /about
9
+ * views/blog/post.html → GET /blog/post
10
+ *
11
+ * @param {object} opts - Clovie config opts
12
+ * @param {object} services - Injected services { file, liveReload }
13
+ * @returns {Array} Array of engine-server route objects ({ method, path, handler })
14
+ */
15
+ export function viewsToRoutes(opts, services) {
16
+ if (!opts.views) return [];
17
+
18
+ const filePaths = services.file.getFilePaths(opts.views);
19
+ if (!filePaths.length) return [];
20
+
21
+ return filePaths.map(filePath => ({
22
+ method: 'GET',
23
+ path: filePathToRoutePath(filePath, opts.views),
24
+ handler: createTemplateHandler(filePath, opts, services),
25
+ meta: { source: 'views', template: filePath },
26
+ }));
27
+ }
28
+
29
+ /**
30
+ * Wraps an array of Clovie page configs ({ path, template, data }) into
31
+ * engine-server route objects ({ method, path, handler }).
32
+ *
33
+ * @param {Array} pages - Clovie page/route configs
34
+ * @param {object} opts - Clovie config opts
35
+ * @param {object} services - Injected services { file, liveReload }
36
+ * @returns {Array} Engine-server route objects
37
+ */
38
+ export function pagesToRoutes(pages, opts, services) {
39
+ if (!pages || !pages.length) return [];
40
+
41
+ return pages.map(page => ({
42
+ method: 'GET',
43
+ path: page.path,
44
+ handler: createTemplateHandler(
45
+ page.template,
46
+ opts,
47
+ services,
48
+ page.data ? (ctx) => page.data(ctx) : null,
49
+ ),
50
+ meta: page.meta || { source: 'routes', template: page.template },
51
+ }));
52
+ }
53
+
54
+ /**
55
+ * Creates a route handler that renders a template and returns HTML.
56
+ * Shared by both views-to-routes and explicit page route definitions.
57
+ *
58
+ * @param {string} templatePath - Path to the template file
59
+ * @param {object} opts - Clovie config opts (renderEngine, data, mode)
60
+ * @param {object} services - { file, liveReload }
61
+ * @param {Function|null} dataFn - Optional per-request data function (ctx) => data
62
+ */
63
+ export function createTemplateHandler(templatePath, opts, services, dataFn) {
64
+ return async (ctx) => {
65
+ const template = services.file.read(templatePath);
66
+ if (!template) {
67
+ return ctx.respond.text('Not Found', 404);
68
+ }
69
+
70
+ const globalData = opts.data || {};
71
+ const routeData = dataFn ? await dataFn(ctx) : {};
72
+ const mergedData = { ...globalData, ...routeData };
73
+
74
+ let html = await opts.renderEngine.render(template, mergedData);
75
+
76
+ if (services.liveReload && opts.mode === 'development') {
77
+ html = await services.liveReload.injectLiveReloadScript(html, opts);
78
+ }
79
+
80
+ return ctx.respond.html(html);
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Converts a template file path to a URL route path.
86
+ * views/index.html → /
87
+ * views/about.html → /about
88
+ * views/blog/post.njk → /blog/post
89
+ * views/blog/index.html → /blog
90
+ */
91
+ function filePathToRoutePath(filePath, viewsDir) {
92
+ const absoluteViewsDir = toAbsolutePath(viewsDir);
93
+ const absoluteFilePath = toAbsolutePath(filePath);
94
+ const relativePath = path.relative(absoluteViewsDir, absoluteFilePath);
95
+ const withoutExt = relativePath.replace(path.extname(relativePath), '');
96
+
97
+ if (withoutExt === 'index') return '/';
98
+
99
+ const segments = withoutExt.split(path.sep);
100
+ if (segments[segments.length - 1] === 'index') {
101
+ segments.pop();
102
+ }
103
+
104
+ return '/' + segments.join('/');
105
+ }
106
+
107
+ function toAbsolutePath(inputPath) {
108
+ return path.isAbsolute(inputPath) ? inputPath : path.resolve(process.cwd(), inputPath);
109
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clovie",
3
- "version": "0.1.38",
3
+ "version": "0.1.39",
4
4
  "description": "Vintage web dev tooling with modern quality of life",
5
5
  "keywords": [
6
6
  "static-site-generator",
@@ -35,7 +35,8 @@
35
35
  "type": "module",
36
36
  "exports": {
37
37
  ".": {
38
- "import": "./lib/createClovie.js"
38
+ "import": "./lib/main.js",
39
+ "require": "./lib/main.js"
39
40
  },
40
41
  "./server": "./lib/Server/main.js",
41
42
  "./cli": "./bin/cli.js"
@@ -75,9 +76,9 @@
75
76
  "publish:info": "node scripts/publish.js info"
76
77
  },
77
78
  "dependencies": {
78
- "@jucie.io/engine": "^1.1.67",
79
+ "@jucie.io/engine": "^1.1.68",
79
80
  "@jucie.io/engine-esbuild": "^1.0.2",
80
- "@jucie.io/engine-server": "^1.0.22",
81
+ "@jucie.io/engine-server": "^1.0.23",
81
82
  "@jucie.io/reactive": "^1.0.26",
82
83
  "chalk": "^5.6.2",
83
84
  "chokidar": "^3.5.3",
@@ -1,189 +0,0 @@
1
- import { ServiceProvider } from '@jucie.io/engine';
2
- import path from 'path';
3
-
4
- export class Route extends ServiceProvider {
5
- static manifest = {
6
- name: 'Clovie Route',
7
- namespace: 'router',
8
- version: '1.0.0',
9
- }
10
- #staticCache = new Set();
11
- #cache = new Map();
12
- #cacheSize = 100;
13
-
14
- initialize(useContext, config) {
15
- this.#cacheSize = config.cacheSize || 100;
16
- }
17
-
18
- actions(useContext) {
19
- return {
20
- setHooks: (opts) => {
21
- const server = this.useContext('server');
22
- if (!opts) {
23
- return;
24
- }
25
- if (opts.hooks) {
26
- server.hooks(opts.hooks);
27
- }
28
- return this;
29
- },
30
- clearStaticCache: () => {
31
- this.#staticCache.forEach(outputPath => {
32
- file.delete(outputPath);
33
- });
34
- this.#staticCache.clear();
35
- },
36
- serveRoutes: async (opts) => {
37
- const server = this.useContext('server');
38
- if (!opts.routes) {
39
- return;
40
- }
41
- for (const route of opts.routes) {
42
- server.add('GET', route.path, async (context) => {
43
- try {
44
- const content = await this.#routeHandler(route, context, opts);
45
- return context.respond.html(content);
46
- } catch (error) {
47
- console.error('Error rendering route', error);
48
- return context.respond.text('Internal Server Error', 500);
49
- }
50
- },
51
- route.meta
52
- );
53
- }
54
- },
55
- serveApi: (opts) => {
56
- const server = this.useContext('server');
57
- if (!opts.api) {
58
- return;
59
- }
60
- for (const api of opts.api) {
61
- server.add(api.method, api.path, (context) => {
62
- try {
63
- return this.#apiHandler(api, context, opts);
64
- } catch (error) {
65
- console.error('Error rendering route', error);
66
- return context.respond.text('Internal Server Error', 500);
67
- }
68
- },
69
- api.meta
70
- );
71
- }
72
- return;
73
- }
74
- }
75
- }
76
-
77
- #apiHandler(api, context) {
78
- return api.handler(context);
79
- }
80
-
81
- async #routeHandler(route, context, opts) {
82
- const [file, liveReload] = this.useContext('file', 'liveReload');
83
-
84
- // Create unique cache key from request
85
- const cacheKey = this.#instancePath(context.req);
86
-
87
- if (this.#cache.has(cacheKey)) {
88
- return await this.#cache.get(cacheKey).compile();
89
- }
90
-
91
- try {
92
- const instance = {
93
- watcherCleanup: liveReload && opts.mode === 'development' ? file.watch(route.template, async () => {
94
- instance.dirty = true;
95
- if (liveReload) {
96
- liveReload.notifyReload();
97
- }
98
- }) : null,
99
- route,
100
- outputPath: this.#formatOutputPath(opts.outputDir, context.req.path),
101
- dirty: true,
102
- data: async () => route.data ? await route.data(context) : {},
103
- compile: async () => {
104
- try {
105
- if (!file.exists(instance.route.template)) {
106
- throw new Error(`Template not found: ${instance.route.template}`);
107
- }
108
- instance.lastAccess = Date.now();
109
- instance.expires = Date.now() + 1000 * 60 * 60 * 24 * 30;
110
- const template = file.read(instance.route.template);
111
- let renderedContent = await opts.renderEngine.render(template, {...(opts.data || {}), ...(await instance.data() || {})})
112
- if (liveReload && opts.mode === 'development') {
113
- renderedContent = await liveReload.injectLiveReloadScript(renderedContent, opts);
114
- }
115
- file.write(instance.outputPath, renderedContent);
116
- instance.dirty = false;
117
- return renderedContent;
118
- // return file.read(instance.outputPath);
119
- } catch (error) {
120
- console.error('Error compiling route', error);
121
- } finally {
122
- instance.dirty = false;
123
- }
124
- }
125
- }
126
- this.#addInstanceCache(cacheKey, instance);
127
- return instance.compile();
128
- } catch (error) {
129
- console.error('Error rendering route', error);
130
- }
131
- }
132
-
133
- #addInstanceCache(path, instance) {
134
- this.#cache.set(path, instance);
135
- if (this.#cache.size > this.#cacheSize) {
136
- this.#cache.delete(this.#cache.keys().next().value);
137
- }
138
- return instance;
139
- }
140
-
141
- /**
142
- * Format output path to ensure proper .html extensions
143
- * @private
144
- */
145
- #formatOutputPath(outputDir, reqPath) {
146
- // Handle root path
147
- if (reqPath === '/' || reqPath === '') {
148
- return path.join(outputDir, 'index.html');
149
- }
150
-
151
- // If already has .html extension, use as-is
152
- if (reqPath.endsWith('.html')) {
153
- return path.join(outputDir, reqPath);
154
- }
155
-
156
- // Add .html extension
157
- // For paths like /posts or /posts/my-slug, both become .html files
158
- return path.join(outputDir, `${reqPath}.html`);
159
- }
160
-
161
- /**
162
- * Create unique instance cache key from request
163
- * Combines path, params, and query for unique identification
164
- * @private
165
- */
166
- #instancePath(req) {
167
- let key = req.path;
168
-
169
- // Add route params if present (e.g., /posts/:slug)
170
- if (req.params && Object.keys(req.params).length > 0) {
171
- const paramsStr = Object.entries(req.params)
172
- .sort(([a], [b]) => a.localeCompare(b))
173
- .map(([k, v]) => `${k}=${v}`)
174
- .join('&');
175
- key += `?params:${paramsStr}`;
176
- }
177
-
178
- // Add query params if present (e.g., ?page=1&sort=desc)
179
- if (req.query && Object.keys(req.query).length > 0) {
180
- const queryStr = Object.entries(req.query)
181
- .sort(([a], [b]) => a.localeCompare(b))
182
- .map(([k, v]) => `${k}=${v}`)
183
- .join('&');
184
- key += `${req.params ? '&' : '?'}query:${queryStr}`;
185
- }
186
-
187
- return key;
188
- }
189
- }