@vlynk-studios/nodulus-core 1.1.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/CHANGELOG.md +48 -0
- package/LICENSE +21 -0
- package/README.md +532 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +331 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +267 -0
- package/dist/index.js +880 -0
- package/dist/index.js.map +1 -0
- package/package.json +79 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import { Command as Command3 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/cli/commands/create-module.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import pc from "picocolors";
|
|
11
|
+
function createModuleCommand() {
|
|
12
|
+
return new Command("create-module").description("Scaffolds a new Nodulus module").argument("<name>", "Module name (lowercase, no spaces/special chars)").option("-p, --path <path>", "Destination folder path (default: src/modules/<name>)").option("--no-repository", "Skip generating a repository file").option("--no-schema", "Skip generating a schema file").option("--js", "Force generate JavaScript (.js) files").option("--ts", "Force generate TypeScript (.ts) files").action((name, options) => {
|
|
13
|
+
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
14
|
+
console.error(pc.red(`
|
|
15
|
+
Error: Invalid module name "${name}". Module names must be lowercase and contain only letters, numbers, or hyphens.
|
|
16
|
+
`));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
let ext;
|
|
20
|
+
if (options.js) {
|
|
21
|
+
ext = "js";
|
|
22
|
+
} else if (options.ts) {
|
|
23
|
+
ext = "ts";
|
|
24
|
+
} else {
|
|
25
|
+
const hasTsConfig = fs.existsSync(path.resolve(process.cwd(), "tsconfig.json"));
|
|
26
|
+
ext = hasTsConfig ? "ts" : "js";
|
|
27
|
+
}
|
|
28
|
+
const modulePath = options.path ? path.resolve(process.cwd(), options.path) : path.resolve(process.cwd(), `src/modules/${name}`);
|
|
29
|
+
if (fs.existsSync(modulePath)) {
|
|
30
|
+
console.error(pc.red(`
|
|
31
|
+
Error: The directory "${modulePath}" already exists. Cannot scaffold module here.
|
|
32
|
+
`));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
fs.mkdirSync(modulePath, { recursive: true });
|
|
36
|
+
const files = {
|
|
37
|
+
[`index.${ext}`]: generateIndex(name),
|
|
38
|
+
[`${name}.routes.${ext}`]: generateRoutes(name),
|
|
39
|
+
[`${name}.service.${ext}`]: generateService(name)
|
|
40
|
+
};
|
|
41
|
+
if (options.repository) {
|
|
42
|
+
files[`${name}.repository.${ext}`] = generateRepository(name);
|
|
43
|
+
}
|
|
44
|
+
if (options.schema) {
|
|
45
|
+
files[`${name}.schema.${ext}`] = generateSchema(name);
|
|
46
|
+
}
|
|
47
|
+
for (const [filename, content] of Object.entries(files)) {
|
|
48
|
+
fs.writeFileSync(path.join(modulePath, filename), content.trim() + "\n", "utf-8");
|
|
49
|
+
}
|
|
50
|
+
console.log(pc.green(`
|
|
51
|
+
\u2714 Module '${name}' created successfully at ${path.relative(process.cwd(), modulePath)}/`));
|
|
52
|
+
for (const filename of Object.keys(files)) {
|
|
53
|
+
console.log(` ${pc.cyan(filename)}`);
|
|
54
|
+
}
|
|
55
|
+
console.log(`
|
|
56
|
+
Next step: add '${name}' to the imports array of modules that require it.
|
|
57
|
+
`);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function generateIndex(name) {
|
|
61
|
+
return `
|
|
62
|
+
import { Module } from '@vlynk-studios/nodulus-core'
|
|
63
|
+
|
|
64
|
+
Module('${name}', {
|
|
65
|
+
imports: [],
|
|
66
|
+
exports: [],
|
|
67
|
+
})
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
function generateRoutes(name) {
|
|
71
|
+
return `
|
|
72
|
+
import { Controller } from '@vlynk-studios/nodulus-core'
|
|
73
|
+
import { Router } from 'express'
|
|
74
|
+
|
|
75
|
+
Controller('/${name}')
|
|
76
|
+
|
|
77
|
+
const router = Router()
|
|
78
|
+
|
|
79
|
+
// Add your routes here
|
|
80
|
+
// router.get('/', (req, res) => { ... })
|
|
81
|
+
|
|
82
|
+
export default router
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
85
|
+
function generateService(name) {
|
|
86
|
+
const capName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
87
|
+
return `
|
|
88
|
+
import { Service } from '@vlynk-studios/nodulus-core'
|
|
89
|
+
|
|
90
|
+
Service('${capName}Service', { module: '${name}' })
|
|
91
|
+
|
|
92
|
+
export class ${capName}Service {
|
|
93
|
+
// Business logic here
|
|
94
|
+
}
|
|
95
|
+
`;
|
|
96
|
+
}
|
|
97
|
+
function generateRepository(name) {
|
|
98
|
+
const capName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
99
|
+
return `
|
|
100
|
+
import { Repository } from '@vlynk-studios/nodulus-core'
|
|
101
|
+
|
|
102
|
+
Repository('${capName}Repository', { module: '${name}', source: 'database' })
|
|
103
|
+
|
|
104
|
+
export class ${capName}Repository {
|
|
105
|
+
// Database queries here
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
function generateSchema(name) {
|
|
110
|
+
const capName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
111
|
+
return `
|
|
112
|
+
import { Schema } from '@vlynk-studios/nodulus-core'
|
|
113
|
+
|
|
114
|
+
// import { z } from 'zod' // Uncomment and install your preferred validation library
|
|
115
|
+
Schema('${capName}Schema', { module: '${name}' })
|
|
116
|
+
|
|
117
|
+
// export const create${capName}Schema = z.object({
|
|
118
|
+
// // Define your schema here
|
|
119
|
+
// })
|
|
120
|
+
`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/cli/commands/sync-tsconfig.ts
|
|
124
|
+
import { Command as Command2 } from "commander";
|
|
125
|
+
import fs3 from "fs";
|
|
126
|
+
import path3 from "path";
|
|
127
|
+
import pc3 from "picocolors";
|
|
128
|
+
import fg from "fast-glob";
|
|
129
|
+
import { parse, stringify } from "comment-json";
|
|
130
|
+
|
|
131
|
+
// src/core/config.ts
|
|
132
|
+
import fs2 from "fs";
|
|
133
|
+
import path2 from "path";
|
|
134
|
+
import { pathToFileURL } from "url";
|
|
135
|
+
|
|
136
|
+
// src/core/logger.ts
|
|
137
|
+
import pc2 from "picocolors";
|
|
138
|
+
var LEVEL_STYLE = {
|
|
139
|
+
debug: (msg) => pc2.gray(msg),
|
|
140
|
+
info: (msg) => pc2.cyan(msg),
|
|
141
|
+
warn: (msg) => pc2.yellow(msg),
|
|
142
|
+
error: (msg) => pc2.red(msg)
|
|
143
|
+
};
|
|
144
|
+
var LEVEL_LABELS = {
|
|
145
|
+
debug: "debug",
|
|
146
|
+
info: "info ",
|
|
147
|
+
// trailing space for alignment
|
|
148
|
+
warn: "warn ",
|
|
149
|
+
error: "error"
|
|
150
|
+
};
|
|
151
|
+
var defaultLogHandler = (level, message) => {
|
|
152
|
+
const prefix = pc2.gray("[Nodulus]");
|
|
153
|
+
const label = LEVEL_STYLE[level](LEVEL_LABELS[level]);
|
|
154
|
+
const line = `${prefix} ${label} ${message}`;
|
|
155
|
+
if (level === "warn" || level === "error") {
|
|
156
|
+
process.stderr.write(line + "\n");
|
|
157
|
+
} else {
|
|
158
|
+
process.stdout.write(line + "\n");
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
function resolveLogLevel(explicit) {
|
|
162
|
+
if (explicit) return explicit;
|
|
163
|
+
const nodeDebug = process.env.NODE_DEBUG ?? "";
|
|
164
|
+
if (nodeDebug.split(",").map((s) => s.trim()).includes("nodulus")) {
|
|
165
|
+
return "debug";
|
|
166
|
+
}
|
|
167
|
+
return "info";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/core/config.ts
|
|
171
|
+
var defaultStrict = typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
|
|
172
|
+
var DEFAULTS = {
|
|
173
|
+
modules: "src/modules/*",
|
|
174
|
+
prefix: "",
|
|
175
|
+
aliases: {},
|
|
176
|
+
strict: defaultStrict,
|
|
177
|
+
resolveAliases: true,
|
|
178
|
+
logger: defaultLogHandler,
|
|
179
|
+
logLevel: resolveLogLevel()
|
|
180
|
+
};
|
|
181
|
+
var loadConfig = async (options = {}) => {
|
|
182
|
+
const cwd = process.cwd();
|
|
183
|
+
let fileConfig = {};
|
|
184
|
+
const tsPath = path2.join(cwd, "nodulus.config.ts");
|
|
185
|
+
const jsPath = path2.join(cwd, "nodulus.config.js");
|
|
186
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
187
|
+
const hasTsLoader = process.execArgv.some((arg) => arg.includes("ts-node") || arg.includes("tsx")) || process._preload_modules?.some((m) => m.includes("ts-node") || m.includes("tsx"));
|
|
188
|
+
const candidates = [];
|
|
189
|
+
if (!isProduction || hasTsLoader) {
|
|
190
|
+
candidates.push(tsPath);
|
|
191
|
+
}
|
|
192
|
+
candidates.push(jsPath);
|
|
193
|
+
let configPathToLoad = null;
|
|
194
|
+
for (const candidate of candidates) {
|
|
195
|
+
if (fs2.existsSync(candidate)) {
|
|
196
|
+
configPathToLoad = candidate;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (configPathToLoad) {
|
|
201
|
+
try {
|
|
202
|
+
const importUrl = pathToFileURL(configPathToLoad).href;
|
|
203
|
+
const mod = await import(importUrl);
|
|
204
|
+
fileConfig = mod.default || mod.config || mod;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (configPathToLoad.endsWith(".ts") && error.code === "ERR_UNKNOWN_FILE_EXTENSION") {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`[Nodulus] Found "nodulus.config.ts" but your environment cannot load raw TypeScript files.
|
|
209
|
+
- In production: Run "npm run build" to generate a .js config OR use nodulus.config.js.
|
|
210
|
+
- In development: Ensure you are running with a loader like "tsx" or "ts-node".`,
|
|
211
|
+
{ cause: error }
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
throw new Error(`[Nodulus] Failed to parse or evaluate config file at ${configPathToLoad}: ${error.message}`, { cause: error });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
modules: options.modules ?? fileConfig.modules ?? DEFAULTS.modules,
|
|
219
|
+
prefix: options.prefix ?? fileConfig.prefix ?? DEFAULTS.prefix,
|
|
220
|
+
aliases: {
|
|
221
|
+
...DEFAULTS.aliases,
|
|
222
|
+
...fileConfig.aliases || {},
|
|
223
|
+
...options.aliases || {}
|
|
224
|
+
// Options override file aliases
|
|
225
|
+
},
|
|
226
|
+
strict: options.strict ?? fileConfig.strict ?? DEFAULTS.strict,
|
|
227
|
+
resolveAliases: options.resolveAliases ?? fileConfig.resolveAliases ?? DEFAULTS.resolveAliases,
|
|
228
|
+
logger: options.logger ?? fileConfig.logger ?? DEFAULTS.logger,
|
|
229
|
+
logLevel: resolveLogLevel(options.logLevel ?? fileConfig.logLevel)
|
|
230
|
+
};
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// src/cli/commands/sync-tsconfig.ts
|
|
234
|
+
function syncTsconfigCommand() {
|
|
235
|
+
return new Command2("sync-tsconfig").description("Syncs Nodulus aliases into tsconfig.json paths array for IDE support").option("--tsconfig <path>", "Path to tsconfig.json", "tsconfig.json").action(async (options) => {
|
|
236
|
+
try {
|
|
237
|
+
const cwd = process.cwd();
|
|
238
|
+
const configPath = path3.resolve(cwd, options.tsconfig);
|
|
239
|
+
if (!fs3.existsSync(configPath)) {
|
|
240
|
+
console.error(pc3.red(`
|
|
241
|
+
Error: Could not find ${options.tsconfig} at ${configPath}
|
|
242
|
+
`));
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
const config = await loadConfig();
|
|
246
|
+
const globPattern = config.modules.replace(/\\/g, "/");
|
|
247
|
+
const moduleDirs = await fg(globPattern, {
|
|
248
|
+
onlyDirectories: true,
|
|
249
|
+
absolute: true,
|
|
250
|
+
cwd
|
|
251
|
+
});
|
|
252
|
+
moduleDirs.sort();
|
|
253
|
+
const pathsObj = {};
|
|
254
|
+
for (const dirPath of moduleDirs) {
|
|
255
|
+
const modName = path3.basename(dirPath);
|
|
256
|
+
const aliasKey = `@modules/${modName}`;
|
|
257
|
+
let indexPath = path3.join(dirPath, "index.ts");
|
|
258
|
+
if (!fs3.existsSync(indexPath)) {
|
|
259
|
+
indexPath = path3.join(dirPath, "index.js");
|
|
260
|
+
}
|
|
261
|
+
let relativePosixPath = path3.relative(cwd, indexPath).replace(/\\/g, "/");
|
|
262
|
+
if (!relativePosixPath.startsWith("./") && !relativePosixPath.startsWith("../")) {
|
|
263
|
+
relativePosixPath = "./" + relativePosixPath;
|
|
264
|
+
}
|
|
265
|
+
pathsObj[aliasKey] = [relativePosixPath];
|
|
266
|
+
}
|
|
267
|
+
if (config.aliases) {
|
|
268
|
+
for (const [alias, target] of Object.entries(config.aliases)) {
|
|
269
|
+
let relativePosixPath = path3.isAbsolute(target) ? path3.relative(cwd, target) : target;
|
|
270
|
+
relativePosixPath = relativePosixPath.replace(/\\/g, "/");
|
|
271
|
+
if (!relativePosixPath.startsWith("./") && !relativePosixPath.startsWith("../")) {
|
|
272
|
+
relativePosixPath = "./" + relativePosixPath;
|
|
273
|
+
}
|
|
274
|
+
const key = alias.endsWith("/*") ? alias : `${alias}/*`;
|
|
275
|
+
const val = relativePosixPath.endsWith("/*") ? relativePosixPath : `${relativePosixPath}/*`;
|
|
276
|
+
pathsObj[key] = [val];
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const rawContent = fs3.readFileSync(configPath, "utf8");
|
|
280
|
+
const tsconfig = parse(rawContent);
|
|
281
|
+
if (!tsconfig.compilerOptions) {
|
|
282
|
+
tsconfig.compilerOptions = {};
|
|
283
|
+
}
|
|
284
|
+
if (!tsconfig.compilerOptions.paths) {
|
|
285
|
+
tsconfig.compilerOptions.paths = {};
|
|
286
|
+
}
|
|
287
|
+
for (const key of Object.keys(tsconfig.compilerOptions.paths)) {
|
|
288
|
+
const val = tsconfig.compilerOptions.paths[key];
|
|
289
|
+
if (key.startsWith("@modules/") && !pathsObj[key]) {
|
|
290
|
+
delete tsconfig.compilerOptions.paths[key];
|
|
291
|
+
} else if (!pathsObj[key] && key.endsWith("/*") && Array.isArray(val) && val.length === 1 && typeof val[0] === "string" && (val[0].startsWith("./") || val[0].startsWith("../")) && val[0].endsWith("/*")) {
|
|
292
|
+
delete tsconfig.compilerOptions.paths[key];
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
Object.assign(tsconfig.compilerOptions.paths, pathsObj);
|
|
296
|
+
const sortedPaths = {};
|
|
297
|
+
Object.keys(tsconfig.compilerOptions.paths).sort().forEach((k) => {
|
|
298
|
+
sortedPaths[k] = tsconfig.compilerOptions.paths[k];
|
|
299
|
+
});
|
|
300
|
+
tsconfig.compilerOptions.paths = sortedPaths;
|
|
301
|
+
fs3.writeFileSync(configPath, stringify(tsconfig, null, 2) + "\n", "utf8");
|
|
302
|
+
const moduleCount = Object.keys(pathsObj).filter((k) => k.startsWith("@modules/")).length;
|
|
303
|
+
const aliasCount = Object.keys(pathsObj).length - moduleCount;
|
|
304
|
+
console.log(pc3.green(`
|
|
305
|
+
\u2714 tsconfig.json updated \u2014 ${moduleCount} module(s), ${aliasCount} folder alias(es)`));
|
|
306
|
+
console.log(`Added paths:`);
|
|
307
|
+
const maxKeyLength = Math.max(...Object.keys(pathsObj).map((k) => k.length));
|
|
308
|
+
for (const [key, paths] of Object.entries(pathsObj)) {
|
|
309
|
+
const paddedKey = key.padEnd(maxKeyLength);
|
|
310
|
+
console.log(` ${pc3.cyan(paddedKey)} \u2192 ${paths[0]}`);
|
|
311
|
+
}
|
|
312
|
+
console.log("");
|
|
313
|
+
} catch (err) {
|
|
314
|
+
console.error(pc3.red(`
|
|
315
|
+
Error synchronizing tsconfig: ${err.message}
|
|
316
|
+
`));
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/cli/index.ts
|
|
323
|
+
import { createRequire } from "module";
|
|
324
|
+
var require2 = createRequire(import.meta.url);
|
|
325
|
+
var pkg = require2("../../package.json");
|
|
326
|
+
var program = new Command3();
|
|
327
|
+
program.name("nodulus").description("Nodulus CLI").version(pkg.version);
|
|
328
|
+
program.addCommand(createModuleCommand());
|
|
329
|
+
program.addCommand(syncTsconfigCommand());
|
|
330
|
+
program.parse();
|
|
331
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/cli/index.ts","../../src/cli/commands/create-module.ts","../../src/cli/commands/sync-tsconfig.ts","../../src/core/config.ts","../../src/core/logger.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Command } from 'commander'\nimport { createModuleCommand } from './commands/create-module.js'\nimport { syncTsconfigCommand } from './commands/sync-tsconfig.js'\n\nimport { createRequire } from 'node:module';\n\nconst require = createRequire(import.meta.url);\nconst pkg = require('../../package.json');\n\nconst program = new Command()\nprogram.name('nodulus').description('Nodulus CLI').version(pkg.version)\nprogram.addCommand(createModuleCommand())\nprogram.addCommand(syncTsconfigCommand())\n\nprogram.parse()\n","import { Command } from 'commander';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport pc from 'picocolors';\n\nexport function createModuleCommand() {\n return new Command('create-module')\n .description('Scaffolds a new Nodulus module')\n .argument('<name>', 'Module name (lowercase, no spaces/special chars)')\n .option('-p, --path <path>', 'Destination folder path (default: src/modules/<name>)')\n .option('--no-repository', 'Skip generating a repository file')\n .option('--no-schema', 'Skip generating a schema file')\n .option('--js', 'Force generate JavaScript (.js) files')\n .option('--ts', 'Force generate TypeScript (.ts) files')\n .action((name: string, options: { path?: string; repository: boolean; schema: boolean; js?: boolean; ts?: boolean }) => {\n if (!/^[a-z0-9-]+$/.test(name)) {\n console.error(pc.red(`\\nError: Invalid module name \"${name}\". Module names must be lowercase and contain only letters, numbers, or hyphens.\\n`));\n process.exit(1);\n }\n\n // Detect language extension\n let ext: string;\n if (options.js) {\n ext = 'js';\n } else if (options.ts) {\n ext = 'ts';\n } else {\n // Auto-detect typescript project\n const hasTsConfig = fs.existsSync(path.resolve(process.cwd(), 'tsconfig.json'));\n ext = hasTsConfig ? 'ts' : 'js';\n }\n\n const modulePath = options.path ? path.resolve(process.cwd(), options.path) : path.resolve(process.cwd(), `src/modules/${name}`);\n\n if (fs.existsSync(modulePath)) {\n console.error(pc.red(`\\nError: The directory \"${modulePath}\" already exists. Cannot scaffold module here.\\n`));\n process.exit(1);\n }\n\n fs.mkdirSync(modulePath, { recursive: true });\n\n const files: Record<string, string> = {\n [`index.${ext}`]: generateIndex(name),\n [`${name}.routes.${ext}`]: generateRoutes(name),\n [`${name}.service.${ext}`]: generateService(name),\n };\n\n if (options.repository) {\n files[`${name}.repository.${ext}`] = generateRepository(name);\n }\n \n if (options.schema) {\n files[`${name}.schema.${ext}`] = generateSchema(name);\n }\n\n for (const [filename, content] of Object.entries(files)) {\n fs.writeFileSync(path.join(modulePath, filename), content.trim() + '\\n', 'utf-8');\n }\n\n console.log(pc.green(`\\n✔ Module '${name}' created successfully at ${path.relative(process.cwd(), modulePath)}/`));\n for (const filename of Object.keys(files)) {\n console.log(` ${pc.cyan(filename)}`);\n }\n console.log(`\\nNext step: add '${name}' to the imports array of modules that require it.\\n`);\n });\n}\n\nfunction generateIndex(name: string): string {\n return `\nimport { Module } from '@vlynk-studios/nodulus-core'\n\nModule('${name}', {\n imports: [],\n exports: [],\n})\n`;\n}\n\nfunction generateRoutes(name: string): string {\n return `\nimport { Controller } from '@vlynk-studios/nodulus-core'\nimport { Router } from 'express'\n\nController('/${name}')\n\nconst router = Router()\n\n// Add your routes here\n// router.get('/', (req, res) => { ... })\n\nexport default router\n`;\n}\n\nfunction generateService(name: string): string {\n const capName = name.charAt(0).toUpperCase() + name.slice(1);\n return `\nimport { Service } from '@vlynk-studios/nodulus-core'\n\nService('${capName}Service', { module: '${name}' })\n\nexport class ${capName}Service {\n // Business logic here\n}\n`;\n}\n\nfunction generateRepository(name: string): string {\n const capName = name.charAt(0).toUpperCase() + name.slice(1);\n return `\nimport { Repository } from '@vlynk-studios/nodulus-core'\n\nRepository('${capName}Repository', { module: '${name}', source: 'database' })\n\nexport class ${capName}Repository {\n // Database queries here\n}\n`;\n}\n\nfunction generateSchema(name: string): string {\n const capName = name.charAt(0).toUpperCase() + name.slice(1);\n return `\nimport { Schema } from '@vlynk-studios/nodulus-core'\n\n// import { z } from 'zod' // Uncomment and install your preferred validation library\nSchema('${capName}Schema', { module: '${name}' })\n\n// export const create${capName}Schema = z.object({\n// // Define your schema here\n// })\n`;\n}\n","import { Command } from 'commander';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport pc from 'picocolors';\nimport fg from 'fast-glob';\nimport { parse, stringify } from 'comment-json';\nimport { loadConfig } from '../../core/config.js';\n\nexport function syncTsconfigCommand() {\n return new Command('sync-tsconfig')\n .description('Syncs Nodulus aliases into tsconfig.json paths array for IDE support')\n .option('--tsconfig <path>', 'Path to tsconfig.json', 'tsconfig.json')\n .action(async (options: { tsconfig: string }) => {\n try {\n const cwd = process.cwd();\n const configPath = path.resolve(cwd, options.tsconfig);\n\n if (!fs.existsSync(configPath)) {\n console.error(pc.red(`\\nError: Could not find ${options.tsconfig} at ${configPath}\\n`));\n process.exit(1);\n }\n\n // 1. Load Nodulus config\n const config = await loadConfig();\n\n // 2. Scan module glob to find discovered modules\n const globPattern = config.modules.replace(/\\\\/g, '/');\n const moduleDirs = await fg(globPattern, {\n onlyDirectories: true,\n absolute: true,\n cwd\n });\n\n moduleDirs.sort();\n\n // 3. Build paths mapping object\n const pathsObj: Record<string, string[]> = {};\n\n // 3a. Register built-in module aliases\n for (const dirPath of moduleDirs) {\n const modName = path.basename(dirPath);\n const aliasKey = `@modules/${modName}`;\n \n let indexPath = path.join(dirPath, 'index.ts');\n if (!fs.existsSync(indexPath)) {\n indexPath = path.join(dirPath, 'index.js');\n }\n\n // Use posix separated paths\n let relativePosixPath = path.relative(cwd, indexPath).replace(/\\\\/g, '/');\n if (!relativePosixPath.startsWith('./') && !relativePosixPath.startsWith('../')) {\n relativePosixPath = './' + relativePosixPath;\n }\n \n pathsObj[aliasKey] = [relativePosixPath];\n }\n\n // 3b. Register folder/project aliases explicitly set in config\n if (config.aliases) {\n for (const [alias, target] of Object.entries(config.aliases)) {\n let relativePosixPath = path.isAbsolute(target) ? path.relative(cwd, target) : target;\n relativePosixPath = relativePosixPath.replace(/\\\\/g, '/');\n if (!relativePosixPath.startsWith('./') && !relativePosixPath.startsWith('../')) {\n relativePosixPath = './' + relativePosixPath;\n }\n\n // Append /* for standard TypeScript folder completion mapping\n const key = alias.endsWith('/*') ? alias : `${alias}/*`;\n const val = relativePosixPath.endsWith('/*') ? relativePosixPath : `${relativePosixPath}/*`;\n \n pathsObj[key] = [val];\n }\n }\n\n // 4. Update TS Config\n const rawContent = fs.readFileSync(configPath, 'utf8');\n const tsconfig = parse(rawContent) as any;\n\n if (!tsconfig.compilerOptions) {\n tsconfig.compilerOptions = {};\n }\n\n if (!tsconfig.compilerOptions.paths) {\n tsconfig.compilerOptions.paths = {};\n }\n\n // 4a. Clean up stale Nodulus managed modules that were deleted\n for (const key of Object.keys(tsconfig.compilerOptions.paths)) {\n const val = tsconfig.compilerOptions.paths[key];\n \n if (key.startsWith('@modules/') && !pathsObj[key]) {\n delete tsconfig.compilerOptions.paths[key];\n } \n // Check for stale config aliases using the Nodulus signature format\n else if (\n !pathsObj[key] &&\n key.endsWith('/*') &&\n Array.isArray(val) &&\n val.length === 1 &&\n typeof val[0] === 'string' &&\n (val[0].startsWith('./') || val[0].startsWith('../')) &&\n val[0].endsWith('/*')\n ) {\n delete tsconfig.compilerOptions.paths[key];\n }\n }\n\n // Merge paths overwriting existing Nodulus-managed keys safely\n Object.assign(tsconfig.compilerOptions.paths, pathsObj);\n\n // Sort keys purely for visual aesthetics\n const sortedPaths: Record<string, any> = {};\n Object.keys(tsconfig.compilerOptions.paths)\n .sort()\n .forEach((k) => {\n sortedPaths[k] = tsconfig.compilerOptions.paths[k];\n });\n \n tsconfig.compilerOptions.paths = sortedPaths;\n\n // 5. Save preserving comments\n fs.writeFileSync(configPath, stringify(tsconfig, null, 2) + '\\n', 'utf8');\n\n // 6. Pretty Output Calculation\n const moduleCount = Object.keys(pathsObj).filter(k => k.startsWith('@modules/')).length;\n const aliasCount = Object.keys(pathsObj).length - moduleCount;\n\n console.log(pc.green(`\\n✔ tsconfig.json updated — ${moduleCount} module(s), ${aliasCount} folder alias(es)`));\n console.log(`Added paths:`);\n \n // Find longest key for padding alignment\n const maxKeyLength = Math.max(...Object.keys(pathsObj).map(k => k.length));\n \n for (const [key, paths] of Object.entries(pathsObj)) {\n const paddedKey = key.padEnd(maxKeyLength);\n console.log(` ${pc.cyan(paddedKey)} → ${paths[0]}`);\n }\n console.log('');\n } catch (err: any) {\n console.error(pc.red(`\\nError synchronizing tsconfig: ${err.message}\\n`));\n process.exit(1);\n }\n });\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { CreateAppOptions, ResolvedConfig, NodulusConfig } from '../types/index.js';\nimport { defaultLogHandler, resolveLogLevel } from './logger.js';\n\nconst defaultStrict = typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production';\n\nexport const DEFAULTS: ResolvedConfig = {\n modules: 'src/modules/*',\n prefix: '',\n aliases: {},\n strict: defaultStrict,\n resolveAliases: true,\n logger: defaultLogHandler,\n logLevel: resolveLogLevel(),\n};\n\nexport const loadConfig = async (options: CreateAppOptions = {}): Promise<ResolvedConfig> => {\n const cwd = process.cwd();\n \n let fileConfig: NodulusConfig = {};\n \n const tsPath = path.join(cwd, 'nodulus.config.ts');\n const jsPath = path.join(cwd, 'nodulus.config.js');\n \n const isProduction = process.env.NODE_ENV === 'production';\n const hasTsLoader = \n process.execArgv.some(arg => arg.includes('ts-node') || arg.includes('tsx')) ||\n (process as any)._preload_modules?.some((m: string) => m.includes('ts-node') || m.includes('tsx'));\n\n // In production, only .js is tried by default.\n // In development (or when a TS loader is detected), .ts is tried first.\n const candidates: string[] = [];\n \n if (!isProduction || hasTsLoader) {\n candidates.push(tsPath);\n }\n candidates.push(jsPath);\n\n let configPathToLoad: string | null = null;\n\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) {\n configPathToLoad = candidate;\n break;\n }\n }\n\n if (configPathToLoad) {\n try {\n const importUrl = pathToFileURL(configPathToLoad).href;\n const mod = await import(importUrl);\n fileConfig = mod.default || mod.config || mod;\n } catch (error: any) {\n if (configPathToLoad.endsWith('.ts') && error.code === 'ERR_UNKNOWN_FILE_EXTENSION') {\n throw new Error(\n `[Nodulus] Found \"nodulus.config.ts\" but your environment cannot load raw TypeScript files.\\n` +\n ` - In production: Run \"npm run build\" to generate a .js config OR use nodulus.config.js.\\n` +\n ` - In development: Ensure you are running with a loader like \"tsx\" or \"ts-node\".`,\n { cause: error }\n );\n }\n throw new Error(`[Nodulus] Failed to parse or evaluate config file at ${configPathToLoad}: ${error.message}`, { cause: error });\n }\n }\n\n // Merge strategy: options > fileConfig > defaults\n return {\n modules: options.modules ?? fileConfig.modules ?? DEFAULTS.modules,\n prefix: options.prefix ?? fileConfig.prefix ?? DEFAULTS.prefix,\n aliases: {\n ...DEFAULTS.aliases,\n ...(fileConfig.aliases || {}),\n ...(options.aliases || {}) // Options override file aliases\n },\n strict: options.strict ?? fileConfig.strict ?? DEFAULTS.strict,\n resolveAliases: options.resolveAliases ?? fileConfig.resolveAliases ?? DEFAULTS.resolveAliases,\n logger: options.logger ?? fileConfig.logger ?? DEFAULTS.logger,\n logLevel: resolveLogLevel(options.logLevel ?? fileConfig.logLevel),\n };\n};\n","import pc from 'picocolors';\nimport type { LogLevel, LogHandler } from '../types/index.js';\n\nconst LEVEL_ORDER: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3,\n};\n\nconst LEVEL_STYLE: Record<LogLevel, (msg: string) => string> = {\n debug: (msg) => pc.gray(msg),\n info: (msg) => pc.cyan(msg),\n warn: (msg) => pc.yellow(msg),\n error: (msg) => pc.red(msg),\n};\n\nconst LEVEL_LABELS: Record<LogLevel, string> = {\n debug: 'debug',\n info: 'info ', // trailing space for alignment\n warn: 'warn ',\n error: 'error',\n};\n\n/**\n * Default log handler. Writes to process.stdout (info/debug) or process.stderr (warn/error).\n * All lines are prefixed with [Nodulus].\n */\nexport const defaultLogHandler: LogHandler = (level, message) => {\n const prefix = pc.gray('[Nodulus]');\n const label = LEVEL_STYLE[level](LEVEL_LABELS[level]);\n const line = `${prefix} ${label} ${message}`;\n \n if (level === 'warn' || level === 'error') {\n process.stderr.write(line + '\\n');\n } else {\n process.stdout.write(line + '\\n');\n }\n};\n\n/**\n * Resolves the effective minimum log level.\n * Priority: explicit logLevel option > NODE_DEBUG env var > default ('info').\n */\nexport function resolveLogLevel(explicit?: LogLevel): LogLevel {\n if (explicit) return explicit;\n\n const nodeDebug = process.env.NODE_DEBUG ?? '';\n if (nodeDebug.split(',').map(s => s.trim()).includes('nodulus')) {\n return 'debug';\n }\n\n return 'info';\n}\n\nexport interface Logger {\n debug(message: string, meta?: Record<string, unknown>): void;\n info(message: string, meta?: Record<string, unknown>): void;\n warn(message: string, meta?: Record<string, unknown>): void;\n error(message: string, meta?: Record<string, unknown>): void;\n}\n\n/**\n * Creates a bound logger that filters by minLevel and delegates to handler.\n * \n * @param handler - Where log events are sent.\n * @param minLevel - Events below this level are discarded.\n */\nexport function createLogger(handler: LogHandler, minLevel: LogLevel): Logger {\n const minOrder = LEVEL_ORDER[minLevel];\n\n const emit = (level: LogLevel, message: string, meta?: Record<string, unknown>) => {\n if (LEVEL_ORDER[level] >= minOrder) {\n handler(level, message, meta);\n }\n };\n\n return {\n debug: (msg, meta) => emit('debug', msg, meta),\n info: (msg, meta) => emit('info', msg, meta),\n warn: (msg, meta) => emit('warn', msg, meta),\n error: (msg, meta) => emit('error', msg, meta),\n };\n}\n"],"mappings":"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","names":["Command","Command","fs","path","pc","fs","path","pc","pc","path","fs","Command","path","fs","pc","require","Command"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { RequestHandler, Router, Application } from 'express';
|
|
2
|
+
|
|
3
|
+
interface ControllerEntry {
|
|
4
|
+
name: string;
|
|
5
|
+
path: string;
|
|
6
|
+
prefix: string;
|
|
7
|
+
middlewares: RequestHandler[];
|
|
8
|
+
router?: Router;
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
}
|
|
11
|
+
interface ModuleEntry {
|
|
12
|
+
name: string;
|
|
13
|
+
path: string;
|
|
14
|
+
indexPath: string;
|
|
15
|
+
imports: string[];
|
|
16
|
+
exports: string[];
|
|
17
|
+
controllers: ControllerEntry[];
|
|
18
|
+
}
|
|
19
|
+
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
20
|
+
/**
|
|
21
|
+
* Function that receives a log event from Nodulus.
|
|
22
|
+
*
|
|
23
|
+
* @param level - Severity level.
|
|
24
|
+
* @param message - Human-readable message.
|
|
25
|
+
* @param meta - Optional structured data for machine consumption.
|
|
26
|
+
*/
|
|
27
|
+
type LogHandler = (level: LogLevel, message: string, meta?: Record<string, unknown>) => void;
|
|
28
|
+
interface ModuleOptions {
|
|
29
|
+
/** Modules this module depends on. */
|
|
30
|
+
imports?: string[];
|
|
31
|
+
/**
|
|
32
|
+
* Names of exports that form the public API of this module.
|
|
33
|
+
* Nodulus validates that each name exists as a real export of index.ts.
|
|
34
|
+
* Error EXPORT_MISMATCH if a name is missing.
|
|
35
|
+
*/
|
|
36
|
+
exports?: string[];
|
|
37
|
+
/** Description — for documentation and future tooling. */
|
|
38
|
+
description?: string;
|
|
39
|
+
}
|
|
40
|
+
interface ControllerOptions {
|
|
41
|
+
/** Middlewares applied to all routes. Mounted before the router. Default: []. */
|
|
42
|
+
middlewares?: RequestHandler[];
|
|
43
|
+
/** If false, createApp() ignores this controller entirely. Default: true. */
|
|
44
|
+
enabled?: boolean;
|
|
45
|
+
}
|
|
46
|
+
interface ServiceOptions {
|
|
47
|
+
/** The module this service belongs to. If omitted, inferred from the file's parent folder name. */
|
|
48
|
+
module?: string;
|
|
49
|
+
/** Description — for documentation and future tooling. */
|
|
50
|
+
description?: string;
|
|
51
|
+
}
|
|
52
|
+
interface RepositoryOptions {
|
|
53
|
+
/** The module this repository belongs to. If omitted, inferred from the file's parent folder name. */
|
|
54
|
+
module?: string;
|
|
55
|
+
/** Description — for documentation and future tooling. */
|
|
56
|
+
description?: string;
|
|
57
|
+
/** Data source type this repository talks to. */
|
|
58
|
+
source?: 'database' | 'api' | 'cache' | 'file' | string;
|
|
59
|
+
}
|
|
60
|
+
/** Internal registry entry for a registered service. */
|
|
61
|
+
interface ServiceEntry {
|
|
62
|
+
name: string;
|
|
63
|
+
path: string;
|
|
64
|
+
type: 'service';
|
|
65
|
+
module: string;
|
|
66
|
+
description?: string;
|
|
67
|
+
}
|
|
68
|
+
/** Internal registry entry for a registered repository. */
|
|
69
|
+
interface RepositoryEntry {
|
|
70
|
+
name: string;
|
|
71
|
+
path: string;
|
|
72
|
+
type: 'repository';
|
|
73
|
+
module: string;
|
|
74
|
+
description?: string;
|
|
75
|
+
source?: string;
|
|
76
|
+
}
|
|
77
|
+
interface SchemaOptions {
|
|
78
|
+
/** The module this schema belongs to. If omitted, inferred from the file's parent folder name. */
|
|
79
|
+
module?: string;
|
|
80
|
+
/** Description — for documentation and future tooling. */
|
|
81
|
+
description?: string;
|
|
82
|
+
/** Validation library used to define this schema. */
|
|
83
|
+
library?: 'zod' | 'joi' | 'yup' | 'ajv' | string;
|
|
84
|
+
}
|
|
85
|
+
/** Internal registry entry for a registered schema. */
|
|
86
|
+
interface SchemaEntry {
|
|
87
|
+
name: string;
|
|
88
|
+
path: string;
|
|
89
|
+
type: 'schema';
|
|
90
|
+
module: string;
|
|
91
|
+
description?: string;
|
|
92
|
+
library?: string;
|
|
93
|
+
}
|
|
94
|
+
/** Discriminated union for all file-level identifier entries. */
|
|
95
|
+
type FileEntry = ServiceEntry | RepositoryEntry | SchemaEntry;
|
|
96
|
+
interface CreateAppOptions {
|
|
97
|
+
/** Glob pointing to module folders. Default: 'src/modules/*'. */
|
|
98
|
+
modules?: string;
|
|
99
|
+
/** Global route prefix. Example: '/api/v1'. Default: ''. */
|
|
100
|
+
prefix?: string;
|
|
101
|
+
/** Folder aliases beyond the auto-generated @modules/* entries. Default: {}. */
|
|
102
|
+
aliases?: Record<string, string>;
|
|
103
|
+
/**
|
|
104
|
+
* Enables circular dependency detection and undeclared import errors.
|
|
105
|
+
* Default: true in development, false in production.
|
|
106
|
+
*/
|
|
107
|
+
strict?: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* If false, the runtime ESM alias hook is not activated.
|
|
110
|
+
* Useful when the project resolves aliases via a bundler. Default: true.
|
|
111
|
+
*/
|
|
112
|
+
resolveAliases?: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Custom log handler. Receives all Nodulus log events.
|
|
115
|
+
*
|
|
116
|
+
* Default: prints [Nodulus] prefixed messages to stderr (warn/error)
|
|
117
|
+
* and stdout (info). debug is suppressed unless NODE_DEBUG includes 'nodulus'.
|
|
118
|
+
*/
|
|
119
|
+
logger?: LogHandler;
|
|
120
|
+
/**
|
|
121
|
+
* Minimum log level. Events below this level are not passed to the handler.
|
|
122
|
+
* Default: 'info' (debug is off unless explicitly set).
|
|
123
|
+
*/
|
|
124
|
+
logLevel?: LogLevel;
|
|
125
|
+
}
|
|
126
|
+
/** Resolved configuration used internally (defaults applied). */
|
|
127
|
+
interface ResolvedConfig {
|
|
128
|
+
modules: string;
|
|
129
|
+
prefix: string;
|
|
130
|
+
aliases: Record<string, string>;
|
|
131
|
+
strict: boolean;
|
|
132
|
+
resolveAliases: boolean;
|
|
133
|
+
logger: LogHandler;
|
|
134
|
+
logLevel: LogLevel;
|
|
135
|
+
}
|
|
136
|
+
/** A module as it appears in the NodularApp result after bootstrap. */
|
|
137
|
+
interface RegisteredModule {
|
|
138
|
+
name: string;
|
|
139
|
+
path: string;
|
|
140
|
+
imports: string[];
|
|
141
|
+
exports: string[];
|
|
142
|
+
controllers: string[];
|
|
143
|
+
}
|
|
144
|
+
interface MountedRoute {
|
|
145
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'USE';
|
|
146
|
+
path: string;
|
|
147
|
+
module: string;
|
|
148
|
+
controller: string;
|
|
149
|
+
}
|
|
150
|
+
/** Stable registry interface — guaranteed across minor versions. */
|
|
151
|
+
interface NodulusRegistry {
|
|
152
|
+
hasModule(name: string): boolean;
|
|
153
|
+
getModule(name: string): RegisteredModule | undefined;
|
|
154
|
+
getAllModules(): RegisteredModule[];
|
|
155
|
+
resolveAlias(alias: string): string | undefined;
|
|
156
|
+
getAllAliases(): Record<string, string>;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Advanced registry interface — exposes internal graph utilities.
|
|
160
|
+
* @unstable May change between minor versions.
|
|
161
|
+
*/
|
|
162
|
+
interface NodulusRegistryAdvanced extends NodulusRegistry {
|
|
163
|
+
/** @unstable */
|
|
164
|
+
getDependencyGraph(): Map<string, string[]>;
|
|
165
|
+
/** @unstable */
|
|
166
|
+
findCircularDependencies(): string[][];
|
|
167
|
+
}
|
|
168
|
+
/** Value returned by createApp() after a successful bootstrap. */
|
|
169
|
+
interface NodulusApp {
|
|
170
|
+
modules: RegisteredModule[];
|
|
171
|
+
routes: MountedRoute[];
|
|
172
|
+
registry: NodulusRegistry;
|
|
173
|
+
}
|
|
174
|
+
/** Shape of nodulus.config.ts. Options passed directly to createApp() take priority. */
|
|
175
|
+
type NodulusConfig = CreateAppOptions;
|
|
176
|
+
interface GetAliasesOptions {
|
|
177
|
+
/**
|
|
178
|
+
* If false, only returns auto-generated @modules/* aliases.
|
|
179
|
+
* Config-defined folder aliases (from nodulus.config.ts `aliases`) are excluded.
|
|
180
|
+
* Default: true (returns all aliases).
|
|
181
|
+
*/
|
|
182
|
+
includeFolders?: boolean;
|
|
183
|
+
/** If true, returns absolute paths. Default: false. */
|
|
184
|
+
absolute?: boolean;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Returns a read-only view of the registry active in the current async context.
|
|
189
|
+
*
|
|
190
|
+
* @unstable This function is intended for advanced framework integrations or
|
|
191
|
+
* internal debugging. The structure of the returned registry may change in future
|
|
192
|
+
* minor updates. For standard use cases, rely on the properties returned by `createApp()`.
|
|
193
|
+
*/
|
|
194
|
+
declare const getRegistry: () => NodulusRegistryAdvanced;
|
|
195
|
+
|
|
196
|
+
type NodulusErrorCode = "MODULE_NOT_FOUND" | "DUPLICATE_MODULE" | "MISSING_IMPORT" | "UNDECLARED_IMPORT" | "CIRCULAR_DEPENDENCY" | "EXPORT_MISMATCH" | "INVALID_CONTROLLER" | "ALIAS_NOT_FOUND" | "DUPLICATE_ALIAS" | "DUPLICATE_BOOTSTRAP" | "REGISTRY_MISSING_CONTEXT" | "INVALID_MODULE_DECLARATION" | "DUPLICATE_SERVICE" | "DUPLICATE_REPOSITORY" | "DUPLICATE_SCHEMA" | "INVALID_ESM_ENV";
|
|
197
|
+
declare class NodulusError extends Error {
|
|
198
|
+
readonly code: NodulusErrorCode;
|
|
199
|
+
readonly details?: string;
|
|
200
|
+
constructor(code: NodulusErrorCode, message: string, details?: string);
|
|
201
|
+
}
|
|
202
|
+
/** @deprecated — not used internally. Messages are defined at each throw site. */
|
|
203
|
+
declare const ERROR_MESSAGES: Record<NodulusErrorCode, string>;
|
|
204
|
+
|
|
205
|
+
declare function Module(name: string, options?: ModuleOptions): void;
|
|
206
|
+
|
|
207
|
+
declare function Controller(prefix: string, options?: ControllerOptions): void;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Declares a file as a named service and registers it in the Nodulus registry.
|
|
211
|
+
*
|
|
212
|
+
* The `module` field is inferred from the parent folder name when not provided explicitly.
|
|
213
|
+
*
|
|
214
|
+
* @param name - Unique service name within the registry (e.g. 'UserService').
|
|
215
|
+
* @param options - Optional configuration: module override and description.
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* // src/modules/users/users.service.ts
|
|
219
|
+
* import { Service } from 'nodulus'
|
|
220
|
+
*
|
|
221
|
+
* Service('UserService', { module: 'users' })
|
|
222
|
+
*
|
|
223
|
+
* export const UserService = { ... }
|
|
224
|
+
*/
|
|
225
|
+
declare function Service(name: string, options?: ServiceOptions): void;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Declares a file as a named repository and registers it in the Nodulus registry.
|
|
229
|
+
*
|
|
230
|
+
* The `module` field is inferred from the parent folder name when not provided explicitly.
|
|
231
|
+
*
|
|
232
|
+
* @param name - Unique repository name within the registry (e.g. 'UserRepository').
|
|
233
|
+
* @param options - Optional configuration: module override, description, and data source type.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* // src/modules/users/users.repository.ts
|
|
237
|
+
* import { Repository } from 'nodulus'
|
|
238
|
+
*
|
|
239
|
+
* Repository('UserRepository', { module: 'users', source: 'database' })
|
|
240
|
+
*
|
|
241
|
+
* export const UserRepository = { ... }
|
|
242
|
+
*/
|
|
243
|
+
declare function Repository(name: string, options?: RepositoryOptions): void;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Declares a file as a named validation schema and registers it in the Nodulus registry.
|
|
247
|
+
*
|
|
248
|
+
* The `module` field is inferred from the parent folder name when not provided explicitly.
|
|
249
|
+
*
|
|
250
|
+
* @param name - Unique schema name within the registry (e.g. 'CreateUserSchema').
|
|
251
|
+
* @param options - Optional configuration: module override, description, and validation library.
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* // src/modules/users/users.schema.ts
|
|
255
|
+
* import { Schema } from 'nodulus'
|
|
256
|
+
*
|
|
257
|
+
* Schema('CreateUserSchema', { module: 'users', library: 'zod' })
|
|
258
|
+
*
|
|
259
|
+
* export const CreateUserSchema = z.object({ ... })
|
|
260
|
+
*/
|
|
261
|
+
declare function Schema(name: string, options?: SchemaOptions): void;
|
|
262
|
+
|
|
263
|
+
declare function createApp(app: Application, options?: CreateAppOptions): Promise<NodulusApp>;
|
|
264
|
+
|
|
265
|
+
declare function getAliases(options?: GetAliasesOptions): Promise<Record<string, string>>;
|
|
266
|
+
|
|
267
|
+
export { Controller, type ControllerEntry, type ControllerOptions, type CreateAppOptions, ERROR_MESSAGES, type FileEntry, type GetAliasesOptions, type LogHandler, type LogLevel, Module, type ModuleEntry, type ModuleOptions, type MountedRoute, type NodulusApp, type NodulusConfig, NodulusError, type NodulusErrorCode, type NodulusRegistry, type NodulusRegistryAdvanced, type RegisteredModule, Repository, type RepositoryEntry, type RepositoryOptions, type ResolvedConfig, Schema, type SchemaEntry, type SchemaOptions, Service, type ServiceEntry, type ServiceOptions, createApp, getAliases, getRegistry };
|