@erwininteractive/mvc 0.1.1

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.
Files changed (34) hide show
  1. package/README.md +174 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +61 -0
  4. package/dist/framework/App.d.ts +38 -0
  5. package/dist/framework/App.js +100 -0
  6. package/dist/framework/Auth.d.ts +27 -0
  7. package/dist/framework/Auth.js +67 -0
  8. package/dist/framework/Router.d.ts +29 -0
  9. package/dist/framework/Router.js +125 -0
  10. package/dist/framework/db.d.ts +13 -0
  11. package/dist/framework/db.js +41 -0
  12. package/dist/framework/index.d.ts +5 -0
  13. package/dist/framework/index.js +22 -0
  14. package/dist/generators/generateController.d.ts +7 -0
  15. package/dist/generators/generateController.js +110 -0
  16. package/dist/generators/generateModel.d.ts +7 -0
  17. package/dist/generators/generateModel.js +77 -0
  18. package/dist/generators/initApp.d.ts +8 -0
  19. package/dist/generators/initApp.js +113 -0
  20. package/dist/generators/paths.d.ts +17 -0
  21. package/dist/generators/paths.js +55 -0
  22. package/package.json +72 -0
  23. package/prisma/schema.prisma +19 -0
  24. package/templates/appScaffold/README.md +297 -0
  25. package/templates/appScaffold/package.json +23 -0
  26. package/templates/appScaffold/public/favicon.svg +16 -0
  27. package/templates/appScaffold/src/controllers/HomeController.ts +9 -0
  28. package/templates/appScaffold/src/middleware/auth.ts +8 -0
  29. package/templates/appScaffold/src/server.ts +24 -0
  30. package/templates/appScaffold/src/views/index.ejs +300 -0
  31. package/templates/appScaffold/tsconfig.json +16 -0
  32. package/templates/controller.ts.ejs +98 -0
  33. package/templates/model.prisma.ejs +7 -0
  34. package/templates/view.ejs.ejs +42 -0
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerController = exports.registerControllers = exports.authenticate = exports.verifyToken = exports.signToken = exports.verifyPassword = exports.hashPassword = exports.disconnectPrisma = exports.getPrismaClient = exports.startServer = exports.createMvcApp = void 0;
4
+ // App factory and server
5
+ var App_1 = require("./App");
6
+ Object.defineProperty(exports, "createMvcApp", { enumerable: true, get: function () { return App_1.createMvcApp; } });
7
+ Object.defineProperty(exports, "startServer", { enumerable: true, get: function () { return App_1.startServer; } });
8
+ // Database
9
+ var db_1 = require("./db");
10
+ Object.defineProperty(exports, "getPrismaClient", { enumerable: true, get: function () { return db_1.getPrismaClient; } });
11
+ Object.defineProperty(exports, "disconnectPrisma", { enumerable: true, get: function () { return db_1.disconnectPrisma; } });
12
+ // Authentication
13
+ var Auth_1 = require("./Auth");
14
+ Object.defineProperty(exports, "hashPassword", { enumerable: true, get: function () { return Auth_1.hashPassword; } });
15
+ Object.defineProperty(exports, "verifyPassword", { enumerable: true, get: function () { return Auth_1.verifyPassword; } });
16
+ Object.defineProperty(exports, "signToken", { enumerable: true, get: function () { return Auth_1.signToken; } });
17
+ Object.defineProperty(exports, "verifyToken", { enumerable: true, get: function () { return Auth_1.verifyToken; } });
18
+ Object.defineProperty(exports, "authenticate", { enumerable: true, get: function () { return Auth_1.authenticate; } });
19
+ // Routing
20
+ var Router_1 = require("./Router");
21
+ Object.defineProperty(exports, "registerControllers", { enumerable: true, get: function () { return Router_1.registerControllers; } });
22
+ Object.defineProperty(exports, "registerController", { enumerable: true, get: function () { return Router_1.registerController; } });
@@ -0,0 +1,7 @@
1
+ export interface GenerateControllerOptions {
2
+ views?: boolean;
3
+ }
4
+ /**
5
+ * Generate a CRUD controller.
6
+ */
7
+ export declare function generateController(name: string, options?: GenerateControllerOptions): Promise<void>;
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateController = generateController;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const ejs_1 = __importDefault(require("ejs"));
10
+ const paths_1 = require("./paths");
11
+ /**
12
+ * Generate a CRUD controller.
13
+ */
14
+ async function generateController(name, options = {}) {
15
+ const { views = true } = options;
16
+ const modelName = capitalize(name);
17
+ const lowerModelName = name.toLowerCase();
18
+ const controllerName = `${modelName}Controller`;
19
+ const resourcePath = pluralize(lowerModelName);
20
+ console.log(`Generating controller: ${controllerName}`);
21
+ // Ensure controllers directory exists
22
+ const controllersDir = path_1.default.resolve("src/controllers");
23
+ if (!fs_1.default.existsSync(controllersDir)) {
24
+ fs_1.default.mkdirSync(controllersDir, { recursive: true });
25
+ }
26
+ // Load and render controller template
27
+ const templatePath = path_1.default.join((0, paths_1.getTemplatesDir)(), "controller.ts.ejs");
28
+ if (!fs_1.default.existsSync(templatePath)) {
29
+ throw new Error("Controller template not found");
30
+ }
31
+ const template = fs_1.default.readFileSync(templatePath, "utf-8");
32
+ const controllerContent = ejs_1.default.render(template, {
33
+ modelName,
34
+ lowerModelName,
35
+ controllerName,
36
+ resourcePath,
37
+ });
38
+ // Write controller file
39
+ const controllerPath = path_1.default.join(controllersDir, `${controllerName}.ts`);
40
+ if (fs_1.default.existsSync(controllerPath)) {
41
+ throw new Error(`Controller ${controllerName}.ts already exists`);
42
+ }
43
+ fs_1.default.writeFileSync(controllerPath, controllerContent);
44
+ console.log(`Created src/controllers/${controllerName}.ts`);
45
+ // Generate views if enabled
46
+ if (views) {
47
+ await generateViews(lowerModelName, modelName);
48
+ }
49
+ console.log(`
50
+ Controller ${controllerName} created successfully!
51
+
52
+ Routes:
53
+ GET /${resourcePath} -> index
54
+ GET /${resourcePath}/:id -> show
55
+ POST /${resourcePath} -> store
56
+ PUT /${resourcePath}/:id -> update
57
+ DELETE /${resourcePath}/:id -> destroy
58
+ `);
59
+ }
60
+ /**
61
+ * Generate basic views for a resource.
62
+ */
63
+ async function generateViews(lowerName, modelName) {
64
+ const viewsDir = path_1.default.resolve(`src/views/${lowerName}`);
65
+ if (!fs_1.default.existsSync(viewsDir)) {
66
+ fs_1.default.mkdirSync(viewsDir, { recursive: true });
67
+ }
68
+ // Load view template
69
+ const viewTemplatePath = path_1.default.join((0, paths_1.getTemplatesDir)(), "view.ejs.ejs");
70
+ if (!fs_1.default.existsSync(viewTemplatePath)) {
71
+ console.warn("View template not found, skipping view generation");
72
+ return;
73
+ }
74
+ const viewTemplate = fs_1.default.readFileSync(viewTemplatePath, "utf-8");
75
+ // Generate index view
76
+ const indexContent = ejs_1.default.render(viewTemplate, {
77
+ title: `${modelName} List`,
78
+ modelName,
79
+ lowerName,
80
+ viewType: "index",
81
+ });
82
+ fs_1.default.writeFileSync(path_1.default.join(viewsDir, "index.ejs"), indexContent);
83
+ // Generate show view
84
+ const showContent = ejs_1.default.render(viewTemplate, {
85
+ title: `${modelName} Details`,
86
+ modelName,
87
+ lowerName,
88
+ viewType: "show",
89
+ });
90
+ fs_1.default.writeFileSync(path_1.default.join(viewsDir, "show.ejs"), showContent);
91
+ console.log(`Created views in src/views/${lowerName}/`);
92
+ }
93
+ /**
94
+ * Capitalize the first letter of a string.
95
+ */
96
+ function capitalize(str) {
97
+ return str.charAt(0).toUpperCase() + str.slice(1);
98
+ }
99
+ /**
100
+ * Simple pluralization.
101
+ */
102
+ function pluralize(str) {
103
+ if (str.endsWith("s")) {
104
+ return str;
105
+ }
106
+ if (str.endsWith("y")) {
107
+ return str.slice(0, -1) + "ies";
108
+ }
109
+ return str + "s";
110
+ }
@@ -0,0 +1,7 @@
1
+ export interface GenerateModelOptions {
2
+ skipMigrate?: boolean;
3
+ }
4
+ /**
5
+ * Generate a Prisma model and append it to schema.prisma.
6
+ */
7
+ export declare function generateModel(name: string, options?: GenerateModelOptions): Promise<void>;
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateModel = generateModel;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const ejs_1 = __importDefault(require("ejs"));
10
+ const child_process_1 = require("child_process");
11
+ const paths_1 = require("./paths");
12
+ /**
13
+ * Generate a Prisma model and append it to schema.prisma.
14
+ */
15
+ async function generateModel(name, options = {}) {
16
+ const modelName = capitalize(name);
17
+ const tableName = pluralize(name.toLowerCase());
18
+ console.log(`Generating model: ${modelName}`);
19
+ // Find prisma schema
20
+ const schemaPath = path_1.default.resolve("prisma/schema.prisma");
21
+ if (!fs_1.default.existsSync(schemaPath)) {
22
+ throw new Error("prisma/schema.prisma not found. Are you in a project directory?");
23
+ }
24
+ // Load and render template
25
+ const templatePath = path_1.default.join((0, paths_1.getTemplatesDir)(), "model.prisma.ejs");
26
+ if (!fs_1.default.existsSync(templatePath)) {
27
+ throw new Error("Model template not found");
28
+ }
29
+ const template = fs_1.default.readFileSync(templatePath, "utf-8");
30
+ const modelContent = ejs_1.default.render(template, { modelName, tableName });
31
+ // Check if model already exists
32
+ const existingSchema = fs_1.default.readFileSync(schemaPath, "utf-8");
33
+ if (existingSchema.includes(`model ${modelName} {`)) {
34
+ throw new Error(`Model ${modelName} already exists in schema.prisma`);
35
+ }
36
+ // Append model to schema
37
+ fs_1.default.appendFileSync(schemaPath, "\n" + modelContent);
38
+ console.log(`Added model ${modelName} to prisma/schema.prisma`);
39
+ // Run Prisma migrate
40
+ if (!options.skipMigrate) {
41
+ console.log("\nRunning Prisma migrate...");
42
+ try {
43
+ (0, child_process_1.execSync)(`npx prisma migrate dev --name add-${name.toLowerCase()}`, {
44
+ stdio: "inherit",
45
+ });
46
+ }
47
+ catch {
48
+ console.error("Migration failed. You may need to run it manually.");
49
+ }
50
+ console.log("\nGenerating Prisma client...");
51
+ try {
52
+ (0, child_process_1.execSync)("npx prisma generate", { stdio: "inherit" });
53
+ }
54
+ catch {
55
+ console.error("Failed to generate Prisma client.");
56
+ }
57
+ }
58
+ console.log(`\nModel ${modelName} created successfully!`);
59
+ }
60
+ /**
61
+ * Capitalize the first letter of a string.
62
+ */
63
+ function capitalize(str) {
64
+ return str.charAt(0).toUpperCase() + str.slice(1);
65
+ }
66
+ /**
67
+ * Simple pluralization - add 's' if not already ending in 's'.
68
+ */
69
+ function pluralize(str) {
70
+ if (str.endsWith("s")) {
71
+ return str;
72
+ }
73
+ if (str.endsWith("y")) {
74
+ return str.slice(0, -1) + "ies";
75
+ }
76
+ return str + "s";
77
+ }
@@ -0,0 +1,8 @@
1
+ export interface InitOptions {
2
+ skipInstall?: boolean;
3
+ withDatabase?: boolean;
4
+ }
5
+ /**
6
+ * Scaffold a new MVC application.
7
+ */
8
+ export declare function initApp(dir: string, options?: InitOptions): Promise<void>;
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.initApp = initApp;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const child_process_1 = require("child_process");
10
+ const paths_1 = require("./paths");
11
+ /**
12
+ * Scaffold a new MVC application.
13
+ */
14
+ async function initApp(dir, options = {}) {
15
+ const targetDir = path_1.default.resolve(dir);
16
+ const templateDir = path_1.default.join((0, paths_1.getTemplatesDir)(), "appScaffold");
17
+ console.log(`Creating new MVC application in ${targetDir}...`);
18
+ // Create target directory
19
+ if (!fs_1.default.existsSync(targetDir)) {
20
+ fs_1.default.mkdirSync(targetDir, { recursive: true });
21
+ }
22
+ // Copy app scaffold templates
23
+ copyDirRecursive(templateDir, targetDir);
24
+ // Copy .env.example
25
+ const envExample = (0, paths_1.getEnvExamplePath)();
26
+ if (fs_1.default.existsSync(envExample)) {
27
+ fs_1.default.copyFileSync(envExample, path_1.default.join(targetDir, ".env.example"));
28
+ }
29
+ // Update package.json with app name and framework path
30
+ const appPackageJson = path_1.default.join(targetDir, "package.json");
31
+ if (fs_1.default.existsSync(appPackageJson)) {
32
+ const pkg = JSON.parse(fs_1.default.readFileSync(appPackageJson, "utf-8"));
33
+ pkg.name = path_1.default.basename(dir);
34
+ // Use relative path to framework for local development
35
+ // When published, this would be the npm package version
36
+ const frameworkRoot = (0, paths_1.getPackageRoot)();
37
+ const relativePath = path_1.default.relative(targetDir, frameworkRoot);
38
+ pkg.dependencies["@erwininteractive/mvc"] = `file:${relativePath}`;
39
+ fs_1.default.writeFileSync(appPackageJson, JSON.stringify(pkg, null, 2));
40
+ }
41
+ console.log("Application scaffolded successfully!");
42
+ // Install dependencies
43
+ if (!options.skipInstall) {
44
+ console.log("\nInstalling dependencies...");
45
+ try {
46
+ (0, child_process_1.execSync)("npm install", { cwd: targetDir, stdio: "inherit" });
47
+ }
48
+ catch {
49
+ console.error("Failed to install dependencies. Please run 'npm install' manually.");
50
+ }
51
+ }
52
+ // Setup database if requested
53
+ if (options.withDatabase) {
54
+ setupDatabase(targetDir);
55
+ }
56
+ console.log(`
57
+ Next steps:
58
+ cd ${dir}
59
+ npm run dev
60
+
61
+ Your app is ready! Visit http://localhost:3000
62
+ ${!options.withDatabase ? `
63
+ To add database support later:
64
+ npm run db:setup
65
+ # Edit .env with DATABASE_URL
66
+ npx prisma migrate dev --name init
67
+ ` : ""}`);
68
+ }
69
+ /**
70
+ * Setup Prisma database support.
71
+ */
72
+ function setupDatabase(targetDir) {
73
+ // Copy prisma schema
74
+ const prismaDir = path_1.default.join(targetDir, "prisma");
75
+ if (!fs_1.default.existsSync(prismaDir)) {
76
+ fs_1.default.mkdirSync(prismaDir, { recursive: true });
77
+ }
78
+ const frameworkPrismaSchema = path_1.default.join((0, paths_1.getPrismaDir)(), "schema.prisma");
79
+ if (fs_1.default.existsSync(frameworkPrismaSchema)) {
80
+ fs_1.default.copyFileSync(frameworkPrismaSchema, path_1.default.join(prismaDir, "schema.prisma"));
81
+ }
82
+ // Install Prisma
83
+ console.log("\nSetting up database...");
84
+ try {
85
+ (0, child_process_1.execSync)("npm install @prisma/client prisma", { cwd: targetDir, stdio: "inherit" });
86
+ (0, child_process_1.execSync)("npx prisma generate", { cwd: targetDir, stdio: "inherit" });
87
+ }
88
+ catch {
89
+ console.error("Failed to setup Prisma. Run 'npm run db:setup' manually.");
90
+ }
91
+ }
92
+ /**
93
+ * Recursively copy a directory.
94
+ */
95
+ function copyDirRecursive(src, dest) {
96
+ if (!fs_1.default.existsSync(src)) {
97
+ return;
98
+ }
99
+ if (!fs_1.default.existsSync(dest)) {
100
+ fs_1.default.mkdirSync(dest, { recursive: true });
101
+ }
102
+ const entries = fs_1.default.readdirSync(src, { withFileTypes: true });
103
+ for (const entry of entries) {
104
+ const srcPath = path_1.default.join(src, entry.name);
105
+ const destPath = path_1.default.join(dest, entry.name);
106
+ if (entry.isDirectory()) {
107
+ copyDirRecursive(srcPath, destPath);
108
+ }
109
+ else {
110
+ fs_1.default.copyFileSync(srcPath, destPath);
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Get the root directory of the framework package.
3
+ * Works whether running from src/ (development) or dist/ (production).
4
+ */
5
+ export declare function getPackageRoot(): string;
6
+ /**
7
+ * Get the templates directory path.
8
+ */
9
+ export declare function getTemplatesDir(): string;
10
+ /**
11
+ * Get the prisma directory path.
12
+ */
13
+ export declare function getPrismaDir(): string;
14
+ /**
15
+ * Get the .env.example file path.
16
+ */
17
+ export declare function getEnvExamplePath(): string;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getPackageRoot = getPackageRoot;
7
+ exports.getTemplatesDir = getTemplatesDir;
8
+ exports.getPrismaDir = getPrismaDir;
9
+ exports.getEnvExamplePath = getEnvExamplePath;
10
+ const path_1 = __importDefault(require("path"));
11
+ const fs_1 = __importDefault(require("fs"));
12
+ /**
13
+ * Get the root directory of the framework package.
14
+ * Works whether running from src/ (development) or dist/ (production).
15
+ */
16
+ function getPackageRoot() {
17
+ // Start from this file's directory and walk up to find package.json
18
+ let dir = __dirname;
19
+ // Walk up at most 5 levels to find package.json with our package name
20
+ for (let i = 0; i < 5; i++) {
21
+ const packageJsonPath = path_1.default.join(dir, "package.json");
22
+ if (fs_1.default.existsSync(packageJsonPath)) {
23
+ try {
24
+ const pkg = JSON.parse(fs_1.default.readFileSync(packageJsonPath, "utf-8"));
25
+ if (pkg.name === "@erwininteractive/mvc") {
26
+ return dir;
27
+ }
28
+ }
29
+ catch {
30
+ // Continue searching
31
+ }
32
+ }
33
+ dir = path_1.default.dirname(dir);
34
+ }
35
+ // Fallback: assume we're in dist/generators or src/generators
36
+ return path_1.default.resolve(__dirname, "../..");
37
+ }
38
+ /**
39
+ * Get the templates directory path.
40
+ */
41
+ function getTemplatesDir() {
42
+ return path_1.default.join(getPackageRoot(), "templates");
43
+ }
44
+ /**
45
+ * Get the prisma directory path.
46
+ */
47
+ function getPrismaDir() {
48
+ return path_1.default.join(getPackageRoot(), "prisma");
49
+ }
50
+ /**
51
+ * Get the .env.example file path.
52
+ */
53
+ function getEnvExamplePath() {
54
+ return path_1.default.join(getPackageRoot(), ".env.example");
55
+ }
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@erwininteractive/mvc",
3
+ "version": "0.1.1",
4
+ "description": "A lightweight, full-featured MVC framework for Node.js with Express, Prisma, and EJS",
5
+ "main": "dist/framework/index.js",
6
+ "types": "dist/framework/index.d.ts",
7
+ "bin": {
8
+ "erwinmvc": "dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist/**",
12
+ "prisma/**",
13
+ "templates/**",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc -p tsconfig.json && node scripts/build-cli.js",
18
+ "dev": "tsx src/cli.ts",
19
+ "test": "jest",
20
+ "prepare": "npm run build"
21
+ },
22
+ "keywords": [
23
+ "mvc",
24
+ "framework",
25
+ "express",
26
+ "prisma",
27
+ "ejs",
28
+ "typescript",
29
+ "node"
30
+ ],
31
+ "author": "Erwin Interactive",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/erwininteractive/mvc.git"
36
+ },
37
+ "engines": {
38
+ "node": ">=20.0.0"
39
+ },
40
+ "dependencies": {
41
+ "@prisma/client": "^6.0.0",
42
+ "bcryptjs": "^2.4.3",
43
+ "commander": "^12.1.0",
44
+ "connect-redis": "^7.1.1",
45
+ "cors": "^2.8.5",
46
+ "dotenv": "^16.4.5",
47
+ "ejs": "^3.1.10",
48
+ "express": "^4.21.0",
49
+ "express-session": "^1.18.0",
50
+ "helmet": "^8.0.0",
51
+ "jsonwebtoken": "^9.0.2",
52
+ "prisma": "^6.0.0",
53
+ "redis": "^4.7.0"
54
+ },
55
+ "devDependencies": {
56
+ "@types/bcryptjs": "^2.4.6",
57
+ "@types/cors": "^2.8.17",
58
+ "@types/ejs": "^3.1.5",
59
+ "@types/express": "^5.0.0",
60
+ "@types/express-session": "^1.18.0",
61
+ "@types/jest": "^29.5.13",
62
+ "@types/jsonwebtoken": "^9.0.7",
63
+ "@types/node": "^22.7.5",
64
+ "@types/supertest": "^6.0.2",
65
+ "esbuild": "^0.24.0",
66
+ "jest": "^29.7.0",
67
+ "supertest": "^7.0.0",
68
+ "ts-jest": "^29.2.5",
69
+ "tsx": "^4.19.1",
70
+ "typescript": "^5.6.3"
71
+ }
72
+ }
@@ -0,0 +1,19 @@
1
+ generator client {
2
+ provider = "prisma-client-js"
3
+ }
4
+
5
+ datasource db {
6
+ provider = "postgresql"
7
+ url = env("DATABASE_URL")
8
+ }
9
+
10
+ model User {
11
+ id Int @id @default(autoincrement())
12
+ email String @unique
13
+ hashedPassword String
14
+ role String @default("user")
15
+ createdAt DateTime @default(now())
16
+ updatedAt DateTime @updatedAt
17
+
18
+ @@map("users")
19
+ }