diff --git a/README.md b/README.md index 40c1acd..5151f11 100644 --- a/README.md +++ b/README.md @@ -34,12 +34,17 @@ To get started with `@medishn/gland`, follow these steps: ``` 2. **Define Routes and Handlers** +Create a `.confmodule` file to configure your routes, caching, and file-watching behavior. This file allows you to dynamically load and configure modules in your application. Below is an example configuration file: - Create a `.confmodule` file with the following content: - - ``` - path=router - ``` +```conf +path = path.join(__dirname, 'router'); +router { + [0]: 'index.ts'; + [1]: 'test.ts'; +} +cache = true; +watch = true; +``` 3. **Create Router:(/router/example.ts)** ```typescript diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 7864b76..ee7c4d9 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -26,4 +26,20 @@ Initial release with the following features: - Currently, the load method accepts arguments in the form of objects and there is no need to load a file. ### Changed -- logger and qiu dependencies are removed along with their documents \ No newline at end of file +- logger and qiu dependencies are removed along with their documents + +## [1.1.0] - 2024-09-16 + +### Added +- **Batch Loading Support**: Implemented batch loading of routes with a configurable batch size of 10 to improve the efficiency of module loading, especially with a large number of routes. +- **File Watcher**: Added a file watcher feature to reload modules dynamically when the files change. This can be enabled via the `watch` option in the configuration. +- **Route Parsing**: Enhanced configuration parsing to support the definition of multiple routes under the `router` section. The routes are automatically resolved based on the provided configuration file. + +### Changed +- **Improved Configuration Handling**: The module loader now merges the default configuration with the provided configuration, ensuring proper handling of missing or optional fields like `path`, `routes`, `cache`, and `watch`. + +### Fixed +- **Error Handling**: Improved error handling for missing routes in the configuration file. A clear error message is now thrown if no routes are specified. + +### Performance Improvements +- **Module Caching**: Added caching logic to prevent redundant module imports, leading to faster performance when caching is enabled. \ No newline at end of file diff --git a/docs/documents/guides/2.configuration.md b/docs/documents/guides/2.configuration.md index e2b9287..f877cce 100644 --- a/docs/documents/guides/2.configuration.md +++ b/docs/documents/guides/2.configuration.md @@ -1,6 +1,6 @@ # Configuration Guide for @medishn/gland -This guide provides a detailed overview of the configuration options available in `@medishn/gland`, allowing you to customize the behavior of your web server, routing, logging, middleware, and database query execution. These configurations can be defined in a separate configuration file, typically named `.confmodule`, and loaded into your application. +This guide provides a comprehensive overview of the `.confmodule` configuration file, which is used to customize the behavior of the `@medishn/gland` framework. The `.confmodule` allows you to define routing, caching, and file-watching settings that control how the framework operates. ## Table of Contents @@ -9,92 +9,118 @@ This guide provides a detailed overview of the configuration options available i - [Introduction](#introduction) - [Basic Configuration](#basic-configuration) - [Configuration Options](#configuration-options) - - [Server Configuration](#server-configuration) - - [Example](#example) - - [Loading Configuration](#loading-configuration) - - [Best Practices](#best-practices) + - [Path Configuration](#path-configuration) + - [Router Configuration](#router-configuration) + - [Cache](#cache) + - [Watch](#watch) + - [Example](#example) + - [Best Practices](#best-practices) --- ## Introduction -`gland` supports both programmatic and file-based configuration to allow flexible and maintainable setups. The framework uses the `.confmodule` file for configuration, which can include details about routing, middleware, logging, and database connections. This file is loaded during server initialization. +`@medishn/gland` supports flexible configuration through the `.confmodule` file, allowing you to define routing paths, module caching, and file-watching behavior. This file is loaded when the framework starts, providing an easy way to configure the application without hardcoding settings. -The configuration system is designed to: - -- Define paths for routes and middleware. -- Set up logging preferences (log levels, transport options, etc.). -- Configure SQL database connections for `Qiu`. -- Customize global or route-specific middleware. +Key use cases for the `.confmodule` file include: +- Defining where your route files are located. +- Specifying which files should be loaded as routes. +- Enabling or disabling module caching. +- Enabling file watching for automatic reloads when files change. ## Basic Configuration -Below is an example of a basic `.confmodule` configuration file: +The `.confmodule` file uses a simple key-value format to specify configuration options. Below is an example of a basic configuration file: +```ini +path = path.join(__dirname, 'router'); +router { + [0]: 'index.ts'; + [1]: 'test.ts'; +} +cache = true; +watch = false; ``` -path=router -recursive=true -pattern=*.ts -cacheModules=true -logLevel=info -``` - -In this example: -- **`path=router`** specifies the directory where route modules are located. -- **`recursive=true`** indicates that the framework should search for route files recursively. -- **`pattern=*.ts`** sets the file pattern to match for route files. -- **`cacheModules=true`** enables module caching. -- **`logLevel=info`** sets the logging level to `info`. ## Configuration Options -The `.confmodule` file can contain several key configuration options, each affecting different parts of the application. +The following sections describe the available options in the `.confmodule` file in detail. -### Server Configuration +### Path Configuration -The server configuration controls how the web server behaves and interacts with routes and middleware. Key server settings include: +- **`path`**: Defines the base directory where the router files are located. + - Type: `String` + - Default: `'router'` + + The `path` option typically uses `path.join` to resolve the absolute path to the directory where your route modules are stored. -| Option | Type | Description | Default Value | -|-------------------|---------|---------------------------------------------------------------------------------------|---------------| -| `path` | String | Specifies the directory where route modules are located. | `'router'` | -| `recursive` | Boolean | Whether to recursively search for route files in subdirectories. | `true` | -| `pattern` | String | File pattern to match for route modules (e.g., `*.ts`, `*.js`). | `*.ts` | -| `cacheModules` | Boolean | Enables or disables caching of route and middleware modules for better performance. | `true` | -| `logLevel` | String | Defines the level of logging (`error`, `warn`, `info`, `debug`). | `'info'` | + **Example:** + ```ini + path = path.join(__dirname, 'router'); + ``` -#### Example +### Router Configuration -```ini -path=router -recursive=true -pattern=*.js -cacheModules=false -logLevel=debug -``` -### Loading Configuration +- **`router`**: Specifies the list of route files to be loaded by the framework. + - Type: `Array` + - Default: `[]` + + The `router` section lists the files that should be used as route modules. Each file is referenced with a specific index (e.g., `[0]`, `[1]`). The files are loaded in the order they appear. + + **Example:** + ```ini + router { + [0]: 'index.ts'; + [1]: 'test.ts'; + } + ``` -You can load the configuration file (`.confmodule`) in your main application file using the `load()` method. +### Cache -```typescript -import gland from '@medishn/gland'; -import path from 'path'; +- **`cache`**: Controls whether the router modules should be cached to improve performance. + - Type: `Boolean` + - Default: `true` + + When `cache` is set to `true`, the route modules are loaded once and then reused from memory, reducing overhead. If set to `false`, the modules are reloaded every time they are needed. -const app = new gland(); + **Example:** + ```ini + cache = true; + ``` -// Load configuration -app.load(path.join(__dirname, '.confmodule')); +### Watch -// Initialize the server -app.init(3000, () => { - console.log('Server running on port 3000'); -}); +- **`watch`**: Enables or disables file watching. When enabled, the framework will automatically reload route modules if changes are detected in the files. + - Type: `Boolean` + - Default: `false` + + When `watch` is set to `true`, the framework watches the route files for changes. If any file is modified, it will be reloaded automatically. This is useful during development but may not be necessary in production. + + **Example:** + ```ini + watch = false; + ``` + +### Example + +Here is a full example of a `.confmodule` file with all options: + +```ini +path = path.join(__dirname, 'router'); +router { + [0]: 'index.ts'; + [1]: 'test.ts'; +} +cache = true; +watch = false; ``` -### Best Practices +## Best Practices + +- **Separate configuration files by environment**: Create multiple configuration files for different environments (e.g., `.confmodule.dev` for development, `.confmodule.prod` for production) and load them based on the current environment. -- **Use separate configuration files for different environments (development, production, etc.).** - You can name them `.confmodule.dev`, `.confmodule.prod`, and load the appropriate configuration based on the environment. +- **Use `cache` in production**: Caching the modules improves performance by reducing the need to reload them on each request. This is particularly useful for production environments. -- **Avoid hardcoding sensitive information** (e.g., database credentials) in your configuration files. Instead, use environment variables or a secrets management system. +- **Enable `watch` during development**: Automatically reloading route modules when files change can speed up the development process by reducing the need for manual restarts. -- **Use the `cacheModules` option in production** to improve performance by caching route and middleware modules. +- **Keep the configuration simple**: Only include essential options in your `.confmodule` file to maintain clarity and avoid complexity. diff --git a/examples/.confmodule b/examples/.confmodule index 7cdb7ad..5097eb9 100644 --- a/examples/.confmodule +++ b/examples/.confmodule @@ -1 +1,7 @@ -path=router \ No newline at end of file +path = path.join(__dirname, 'router'); +router { + [0]: 'index.ts'; + [1]: 'test.ts' +} +cache=true; +watch= false; \ No newline at end of file diff --git a/examples/app.ts b/examples/app.ts index c45e35b..6b1d056 100644 --- a/examples/app.ts +++ b/examples/app.ts @@ -1,7 +1,7 @@ import path from 'path'; import gland from '../dist'; const g = new gland(); -g.load({ path: path.join(__dirname, 'router') }); +g.load(path.join(__dirname, '.confmodule')); g.init(3000, () => { console.log('server run on port 3000'); }); diff --git a/examples/router/test.ts b/examples/router/test.ts new file mode 100644 index 0000000..ee120bf --- /dev/null +++ b/examples/router/test.ts @@ -0,0 +1,10 @@ +import { Context, Get, Route } from '../../dist'; + +@Route('/test') +export class Test { + @Get() + test(ctx: Context) { + ctx.write('hello world2222'); + ctx.end(); + } +} diff --git a/lib/core/server.ts b/lib/core/server.ts index dd9eb57..6e6f080 100644 --- a/lib/core/server.ts +++ b/lib/core/server.ts @@ -113,7 +113,7 @@ export class WebServer extends Server implements Gland.APP { init(...args: ListenArgs): this { return this.listen(...args); } - async load(conf: Partial | string) { + async load(conf: string) { await LoadModules.load(conf); } } diff --git a/lib/helper/load.ts b/lib/helper/load.ts index 896bc7f..b32742e 100644 --- a/lib/helper/load.ts +++ b/lib/helper/load.ts @@ -1,130 +1,111 @@ import path from 'path'; +import * as fs from 'fs'; import { Router } from '../core/router'; import { getEx } from '../core/decorators'; import { ModuleConfig } from '../types'; -import * as fs from 'fs'; + export namespace LoadModules { export const moduleCache: { [key: string]: any } = {}; const modules: { [key: string]: any } = {}; + const defaultConfig: ModuleConfig = { path: 'router', - recursive: true, - pattern: '*.ts', - cacheModules: true, - logLevel: 'info', + routes: [], + cache: true, + watch: false, }; let config: ModuleConfig = defaultConfig; - export async function load(conf: Partial | string) { - let baseDir: string = ''; - if (typeof conf === 'string') { - const configFile = path.resolve(conf); - const fileConfig = await parseConfig(configFile); - config = { ...defaultConfig, ...fileConfig }; - baseDir = path.join(path.dirname(conf), config.path); - } else if (typeof conf === 'object') { - config = { ...defaultConfig, ...conf }; - if (path.isAbsolute(config.path!)) { - baseDir = config.path!; - } else { - throw new Error(`Error: The directory '${conf.path}' does not exist or is invalid. Please provide a valid path.`); - } + /** + * Load the modules based on the provided configuration. + */ + export async function load(configFilePath: string) { + const configFile = path.resolve(configFilePath); + const fileConfig = await parseConfig(configFile); + console.log('fileConfig', fileConfig); + + config = { ...defaultConfig, ...fileConfig }; + + if (!config.routes.length) { + throw new Error('No routes specified in the configuration file.'); } - const files = await findModules(baseDir, config.pattern!, config.recursive!); + + const baseDir = path.resolve(config.path); + const files = config.routes.map((route) => path.join(baseDir, route)); const BATCH_SIZE = 10; for (let i = 0; i < files.length; i += BATCH_SIZE) { const fileBatch = files.slice(i, i + BATCH_SIZE); const importPromises = fileBatch.map(async (file) => { - const resolvedPath = path.resolve(file); - if (!config.cacheModules || !moduleCache[resolvedPath]) { - const moduleExports = await importModule(resolvedPath); - moduleCache[resolvedPath] = moduleExports; - } - const moduleName = path.basename(resolvedPath, path.extname(resolvedPath)); - modules[moduleName] = moduleCache[resolvedPath]; + await loadModule(file); }); await Promise.all(importPromises); } + if (config.watch) { + watch(baseDir, files); + } const exp = getEx(); Router.init(exp); return modules; } - // Parse the config file with caching support + // Parse the configuration file for settings export async function parseConfig(configPath: string): Promise> { - const configCache: { [key: string]: Partial } = {}; - if (configCache[configPath]) { - return configCache[configPath]; - } - const config: Partial = {}; try { const content = await fs.promises.readFile(configPath, 'utf-8'); - if (!content) { - throw new Error(`Config file at ${configPath} is empty or could not be read.`); - } + const configLines = content + .split('\n') + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('//')); + console.log('configLines', configLines, '\n'); - content.split('\n').forEach((line) => { - const [key, value] = line.split('=').map((s) => s.trim()); - if (key && value) { - switch (key) { - case 'recursive': - case 'cacheModules': - config[key] = value === 'true'; - break; - case 'logLevel': - config[key] = value as ModuleConfig['logLevel']; - break; - case 'path': - case 'pattern': - config[key] = value.replace(/['"]/g, ''); - break; - default: - throw new Error(`Unknown config key: ${key}`); - } + for (const line of configLines) { + if (line.startsWith('path')) { + // Safely evaluate the expression using Function + const expression = line.split('=')[1].trim(); + const evaluatePath = new Function('path', '__dirname', `return ${expression};`); + config.path = evaluatePath(path, path.dirname(configPath)); + } else if (line.startsWith('router {')) { + const routeLines = configLines.slice(configLines.indexOf(line) + 1, configLines.indexOf('}')).map((r) => r.split(':')[1].trim().replace(/['";]/g, '')); + config.routes = routeLines; + } else if (line.startsWith('cache')) { + config.cache = Boolean(line.split('=')[1].trim()); + } else if (line.startsWith('watch')) { + config.watch = Boolean(line.split('=')[1].trim().includes('true')); } - }); - - configCache[configPath] = config; + } } catch (err: any) { throw new Error(`Error reading or parsing config file: ${err.message}`); } return config; } - export async function findModules(directory: string, pattern: string, recursive: boolean): Promise { - let fileList: string[] = []; - const queue: string[] = [directory]; - const fileCache: { [key: string]: boolean } = {}; - - while (queue.length) { - const currentDir = queue.shift()!; - const files = await fs.promises.readdir(currentDir); - - await Promise.all( - files.map(async (file) => { - const filePath = path.join(currentDir, file); - if (fileCache[filePath]) return; // Skip cached files - fileCache[filePath] = true; - - const stat = await fs.promises.stat(filePath); - if (stat.isDirectory() && recursive) { - queue.push(filePath); - } else if (stat.isFile() && fileMatchesPattern(file, pattern)) { - fileList.push(filePath); - } - }), - ); - } - - return fileList; - } export async function importModule(filePath: string) { return import(filePath); } - export function fileMatchesPattern(fileName: string, pattern: string): boolean { - const regexPattern = new RegExp(pattern.replace('*', '.*')); - return regexPattern.test(fileName); + async function loadModule(file: string) { + const resolvedPath = path.resolve(file); + if (!config.cache || !moduleCache[resolvedPath]) { + const moduleExports = await importModule(`${resolvedPath}`); + moduleCache[resolvedPath] = moduleExports; + } + const moduleName = path.basename(resolvedPath, path.extname(resolvedPath)); + modules[moduleName] = moduleCache[resolvedPath]; + } + function watch(baseDir: string, files: string[]) { + files.forEach((file) => { + const resolvedPath = path.resolve(file); + fs.watch(resolvedPath, (eventType) => { + if (eventType === 'change') { + console.log(`File ${file} has changed. Reloading module...`); + loadModule(file).catch((err) => { + console.error(`Failed to reload module ${file}: ${err.message}`); + }); + } + }); + }); + + console.log(`Watching files in directory: ${baseDir}`); } } diff --git a/lib/types/index.ts b/lib/types/index.ts index 985d33f..b234eb4 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -63,10 +63,10 @@ export type ListenArgs = | [path: string, backlog?: number, listeningListener?: () => void] | [path: string, listeningListener?: () => void] | [options: Gland.ListenOptions, listeningListener?: () => void]; + export interface ModuleConfig { path: string; - recursive?: boolean; - pattern?: string; - cacheModules?: boolean; - logLevel?: 'none' | 'error' | 'warn' | 'info' | 'debug'; + routes: string[]; + cache?: boolean; + watch?: boolean; } diff --git a/package.json b/package.json index 759e032..77fc9c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@medishn/gland", - "version": "1.0.2", + "version": "1.1.0", "description": "Glands is a lightweight framework for Node.js designed for simplicity.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/scripts/release.sh b/scripts/release.sh index 67fd455..b298521 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -1,29 +1,21 @@ #!/bin/bash set -e - -# Run the build script npm run build - -# Get the version from package.json VERSION=$(node -p "require('./package.json').version") -# Get the changelog content -CHANGELOG_CONTENT=$(cat docs/CHANGELOG.md) +CHANGELOG_CONTENT=$(awk "/## \[${VERSION}\]/{flag=1;next}/## \[/{flag=0}flag" docs/CHANGELOG.md | sed '/^$/d') -# Check for changes in the working directory +if [ -z "$CHANGELOG_CONTENT" ]; then + echo "Changelog for version ${VERSION} not found. Please ensure the changelog is updated." + exit 1 +fi if [ -n "$(git status --porcelain)" ]; then - # There are changes to commit git add . git commit -m "chore: release version ${VERSION}" - - # Tag the commit with the new version - git tag -a "v${VERSION}" -m "Release ${VERSION}: ${CHANGELOG_CONTENT}" - - # Push the commit and the tag + git tag -a "v${VERSION}" -m "Release ${VERSION}" -m "${CHANGELOG_CONTENT}" git push origin main --tags else echo "No changes to commit." fi - -# Publish the package npm publish --access public +echo "Version ${VERSION} successfully published." diff --git a/test/unit/helper/Load.spec.ts b/test/unit/helper/Load.spec.ts deleted file mode 100644 index 1c57851..0000000 --- a/test/unit/helper/Load.spec.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { expect } from 'chai'; -import sinon from 'sinon'; -import * as fs from 'fs'; -import path from 'path'; -import { LoadModules } from '../../../lib/helper/load'; - -describe('LoadModules', () => { - let readFileStub: sinon.SinonStub; - let readdirStub: sinon.SinonStub; - let statStub: sinon.SinonStub; - let importStub: sinon.SinonStub; - - beforeEach(() => { - readFileStub = sinon.stub(fs.promises, 'readFile'); - readdirStub = sinon.stub(fs.promises, 'readdir'); - statStub = sinon.stub(fs.promises, 'stat'); - importStub = sinon.stub(LoadModules, 'importModule'); - }); - - afterEach(() => { - sinon.restore(); - }); - - describe('parseConfig', () => { - it('should parse configuration file and return the correct config object', async () => { - const mockConfig = ` - path=./modules - recursive=true - pattern=*.ts - cacheModules=true - logLevel=info - `; - - readFileStub.resolves(mockConfig); - - const result = await LoadModules['parseConfig']('./mock-config.conf'); - expect(result).to.deep.equal({ - path: './modules', - recursive: true, - pattern: '*.ts', - cacheModules: true, - logLevel: 'info', - }); - }); - - it('should cache the config after reading', async () => { - const mockConfig = `path=mockDir`; - - readFileStub.resolves(mockConfig); - const result1 = await LoadModules['parseConfig']('./mock-config.conf'); - const result2 = await LoadModules['parseConfig']('./mock-config.conf'); - expect(result2).to.deep.equal(result1); - }); - }); - - describe('findModules', () => { - it('should return a list of files matching the pattern', async () => { - const mockFiles = ['file1.ts', 'file2.ts']; - const mockStat = { - isDirectory: () => false, - isFile: () => true, - }; - - readdirStub.resolves(mockFiles); - statStub.resolves(mockStat); - - const result = await LoadModules['findModules']('./modules', '*.ts', true); - expect(result).to.deep.equal(['modules/file1.ts', 'modules/file2.ts']); - }); - - it('should recursively find files in subdirectories if recursive is true', async () => { - const mockFiles = ['subdir', 'file1.ts']; - const mockStatDir = { - isDirectory: () => true, - isFile: () => false, - }; - const mockStatFile = { - isDirectory: () => false, - isFile: () => true, - }; - - readdirStub.onFirstCall().resolves(mockFiles); - readdirStub.onSecondCall().resolves(['file2.ts']); - statStub.onFirstCall().resolves(mockStatDir); - statStub.onSecondCall().resolves(mockStatFile); - statStub.onThirdCall().resolves(mockStatFile); - - const result = await LoadModules['findModules']('./modules', '*.ts', true); - expect(result).to.deep.equal(['modules/file1.ts', 'modules/subdir/file2.ts']); - }); - }); - - describe('load', () => { - it('should load all modules and return them in the module cache', async () => { - const mockConfig = ` - path=./modules - recursive=true - pattern=*.ts - cacheModules=true - logLevel=info - `; - - readFileStub.resolves(mockConfig); - readdirStub.resolves(['file1.ts', 'file2.ts']); - statStub.resolves({ isDirectory: () => false, isFile: () => true }); - - // Mock the dynamic imports - importStub.withArgs(path.resolve('/home/mahdi/Projects/PACKAGE/gland/modules/file1.ts')).resolves({ module1: 'mockModule1' }); - importStub.withArgs(path.resolve('/home/mahdi/Projects/PACKAGE/gland/modules/file2.ts')).resolves({ module2: 'mockModule2' }); - - const modules = await LoadModules.load(path.join(__dirname, '.confmodule')); - - expect(modules).to.have.keys('file1', 'file2'); - expect(modules['file1']).to.deep.equal({ module1: 'This is module 1' }); - expect(modules['file2']).to.deep.equal({ module2: 'This is module 2' }); - }); - }); -}); diff --git a/tsconfig.json b/tsconfig.json index 4374fc6..85cbd90 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,6 @@ "skipLibCheck": true, "moduleResolution": "node", "declaration": true, - "declarationMap": true, "rootDir": "lib", "outDir": "dist", "resolveJsonModule": true,