easy-starter 1.2.1 → 2.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/README.md CHANGED
@@ -1,118 +1,69 @@
1
1
  # easy-starter 🚀
2
2
 
3
- A lightweight CLI tool to bootstrap modern fullstack React web projects.
3
+ A lightweight CLI tool to bootstrap modern fullstack React web projects with Vite v8, TypeScript 7.0, and optional authentication.
4
4
 
5
5
  ```bash
6
6
  npx easy-starter [your-project-name]
7
7
  ```
8
8
 
9
- **Keywords:** React, Server-Side Rendering (SSR), Database (DB), Analytics, Type-Safe API, Fullstack, Minimalist
9
+ **Keywords:** React, Server-Side Rendering (SSR), Vite, Database (DB), Analytics, Type-Safe API, User Authentication, Fullstack, Minimalist
10
10
 
11
11
  Tired of rigid frameworks forcing you into complex folder structures and steep learning curves? **easy-starter** is built on the philosophy of maximum freedom and modularity. It provides a solid foundation by combining a set of carefully curated, simple, and powerful tools without locking you into a monolithic architecture like Next.js or Remix.
12
12
 
13
- ## Why easy-starter? (The Philosophy)
13
+ ---
14
14
 
15
- Modern full-stack frameworks are incredibly powerful, but they often come with significant trade-offs: strict file-system routing rules, immense complexity under the hood, and the modern paradigm of strictly dividing components into "client" and "server" boundaries.
15
+ ## What's New in v2.0.0?
16
16
 
17
- **easy-starter solves this by giving you back control.**
17
+ * **Vite v8 Bundler:** Switched from Parcel to Vite v8, offering lightning-fast HMR and seamless dev proxying.
18
+ * **TypeScript 7.0:** Fully upgraded dependencies and compiler options to support TypeScript 7.
19
+ * **Interactive Installer:** Run `npx easy-starter` and configure exactly what you need. Zero bloat — files, comments, and package dependencies are completely omitted if a feature is disabled.
20
+ * **User Authentication:** Out-of-the-box integration with `easy-user-auth` including secure cookie rotation, user registration/login modals, role checking, and custom password recovery emails.
21
+ * **Dynamic Social Previews:** Generate dynamic `og:image` screenshots of your pages automatically using headless browser rendering (`puppeteer`) and cached locally in `easy-db-node`.
22
+ * **Flexible Viewport Tuning:** Optional script injection to scale mobile layouts to a standard viewport width (min-width: 600px).
18
23
 
19
- * **No Server/Client Component Split:** Forget about adding `'use client'` everywhere or stressing about what runs where. With `easy-starter`, you just write standard React components. The integrated `ssr-hook` elegantly handles data fetching on the server and seamless hydration on the client for you.
20
- * **No rigid structure:** You decide where your components, pages, and server logic live.
21
- * **Clear Backend/Frontend separation:** Server code stays on the Express server, client code stays in React, connected by a seamlessly typed RPC API.
22
- * **Minimalist dependencies:** We rely on simple, purpose-built libraries instead of heavy abstractions.
23
- * **Instant Productivity:** You get SSR, a Database, Analytics, and a Type-Safe API out of the box in seconds.
24
+ ---
24
25
 
25
26
  ## What's included in the box?
26
27
 
27
- The generated boilerplate comes pre-configured with a stack designed for developer happiness and raw performance:
28
+ The generated boilerplate can be configured to contain:
28
29
 
29
- * **Frontend/Backend:** React 19, TypeScript, and Express.
30
- * **Bundler:** Parcel - zero-configuration, lightning-fast builds.
30
+ * **Frontend/Backend:** React 19, TypeScript 7.0, and Express.
31
+ * **Bundler:** Vite v8 - lightning-fast client bundles with native dev proxying.
32
+ * **User Authentication:** [`easy-user-auth`](https://www.npmjs.com/package/easy-user-auth) - Highly secure password-recovery, registration, and cookie rotation framework.
31
33
  * **Server-Side Rendering (SSR):** [`ssr-hook`](https://www.npmjs.com/package/ssr-hook) - Dead-simple SSR for React. Fetches data on the server and hydrates on the client without the flash of loading content.
32
34
  * **Type-Safe API:** [`typed-client-server-api`](https://www.npmjs.com/package/typed-client-server-api) - An RPC-like interface giving you full TypeScript autocomplete and safety between your Express server and React client.
33
35
  * **Database:** [`easy-db-node`](https://www.npmjs.com/package/easy-db-node) - A simple, local JSON-based database perfect for small to medium projects where setting up Postgres/MongoDB is overkill.
34
36
  * **Routing:** [`easy-page-router`](https://www.npmjs.com/package/easy-page-router) - A lightweight, programmatic routing solution.
35
37
  * **Analytics:** [`easy-analytics`](https://www.npmjs.com/package/easy-analytics) - Privacy-friendly, local analytics tracking.
38
+ * **Image Resizing:** [`easy-image-resizer`](https://www.npmjs.com/package/easy-image-resizer) - Express middleware for dynamic image resizing and WebP/JPEG conversion with automatic mtime disk caching.
36
39
  * **AI-Friendly (.cursorrules):** Pre-configured, LLM-optimized guidelines in the project root so AI coding assistants (Cursor, Windsurf, Copilot, etc.) immediately understand the codebase conventions, API restrictions, routing patterns, and database operations.
37
40
  * **Deployment:** Integrated Github Actions workflow and PM2 deployment script for seamless, automated deployment straight to your own VPS.
38
41
 
39
- ## Core Concepts & Examples
42
+ ---
40
43
 
41
- > **Note:** `easy-starter` gives you a solid foundation, but you are not forced into any of these patterns. If you prefer a different routing library (like `react-router`), another data fetching solution (like `react-query` or `swr`), or standard REST endpoints, you are completely free to swap them out and use your own libraries!
44
+ ## Interactive Configuration Prompts
42
45
 
43
- ### 1. Defining your API
46
+ When you run `npx easy-starter`, the CLI will prompt you with the following configuration questions:
44
47
 
45
- Your API contract is defined in a single file (`types.d.ts`). This ensures your frontend and backend are always perfectly in sync.
48
+ 1. **Project name:** Choose the name of the folder and the app.
49
+ 2. **User Authentication:** Do you want to add user login/registration widgets? (Installs `easy-user-auth` and nodemailer, generates JWT secrets in `.env`).
50
+ 3. **Analytics:** Do you want local visitor analytics? (Installs `easy-analytics`, adds dashboard visualization).
51
+ 4. **Admin Dashboard:** Do you want to include `/admin` page? (If Auth is enabled, protects it using user roles. If Auth is disabled, protects it using a static password).
52
+ 5. **SSR Rendering:** Do you want Server-Side Rendering? (Installs `ssr-hook`, configures hydration).
53
+ 6. **og:image Generation:** Do you want dynamic `og:image` generation? (Installs `puppeteer` to automatically screenshot URLs for social cards, cached via easy-db).
54
+ 7. **Image Resizing:** Do you want dynamic image resizing? (Installs `easy-image-resizer` Express middleware for on-the-fly image resizing and WebP conversion with automatic caching).
55
+ 8. **Deployment Script:** Do you want to deploy to your VPS using GitHub Actions? (Installs `node-ssh`, adds SSH deploy script).
46
56
 
47
- ```typescript
48
- // types.d.ts
49
- import { APIDefinition, Endpoint } from "typed-client-server-api";
50
-
51
- export type API = APIDefinition<{
52
- // Define an endpoint: parameters -> response
53
- getItems: Endpoint<{}, Item[]>;
54
- addItem: Endpoint<{ name: string; value: number }, string>;
55
- }>;
56
- ```
57
-
58
- On the server, you implement these endpoints:
59
-
60
- ```typescript
61
- // server/index.tsx
62
- import { initApi } from "typed-client-server-api";
63
- import { API } from "../types";
64
-
65
- initApi<API>(app, {
66
- getItems: async () => {
67
- return db.select("item");
68
- },
69
- addItem: async ({ name, value }) => {
70
- return db.insert("item", { name, value });
71
- }
72
- });
73
- ```
74
-
75
- ### 2. Fetching Data with SSR (`useSSRApi`)
76
-
77
- To fetch data on the server during SSR and seamlessly hydrate it on the client without loading spinners, use `useSSRApi` in your React components.
78
-
79
- ```tsx
80
- // src/pages/Index.tsx
81
- import React from "react";
82
- import { useSSRApi } from "../services/api";
83
-
84
- export default function Index() {
85
- // This will run on the server, fetch the data, embed it in the HTML,
86
- // and hydrate perfectly on the client. Fully type-safe!
87
- const [items, error, isLoading, reload] = useSSRApi.getItems({});
88
-
89
- if (isLoading) return <p>Loading...</p>;
90
- if (error) return <p>Error: {error.message}</p>;
91
-
92
- return (
93
- <ul>
94
- {items?.map(item => <li key={item._id}>{item.name}</li>)}
95
- </ul>
96
- );
97
- }
98
- ```
99
-
100
- ### 3. AI Integration (`.cursorrules`)
101
-
102
- This template includes a pre-configured `.cursorrules` file in the root. If you develop using AI assistants such as Cursor, Windsurf, or GitHub Copilot, they will automatically read this file to understand the specific conventions of `easy-starter`.
57
+ ---
103
58
 
104
59
  ## Getting Started
105
60
 
106
- Bootstrapping a new project is as simple as running one command:
61
+ Bootstrapping a new project is as simple as running:
107
62
 
108
63
  ```bash
109
64
  npx easy-starter [your-project-name]
110
65
  ```
111
66
 
112
- ### Interactive CLI
113
-
114
- If you run `npx easy-starter` without a project name, the CLI will guide you interactively. It will also ask if you want to include a ready-to-use **Deployment Script** (`scripts/deploy.ts` and GitHub Actions workflow) utilizing `node-ssh` and PM2 for dead-simple automated deployments to your VPS.
115
-
116
67
  ### Running your project
117
68
 
118
69
  Once created, navigate to your new project folder and start the development server:
@@ -122,13 +73,15 @@ cd your-project-name
122
73
  npm run dev
123
74
  ```
124
75
 
125
- This command runs both the Express backend and the Parcel frontend bundler concurrently. Your app will be live at `http://localhost:1234`!
76
+ This command starts both the Express backend on port `1111` and the Vite dev server on port `5173`. Your app will be live at `http://localhost:5173`!
77
+
78
+ ---
126
79
 
127
80
  ## Available Scripts
128
81
 
129
82
  Inside your newly generated project, you can use the following scripts:
130
83
 
131
- * `npm run dev`: Starts the development server with hot-reloading for both backend and frontend.
84
+ * `npm run dev`: Starts the development server with hot-reloading. Open `http://localhost:5173`.
132
85
  * `npm run build`: Builds the project for production.
133
86
  * `npm start`: Starts the production server (make sure to build first).
134
87
  * `npm run deploy`: (Optional) Executes the `scripts/deploy.ts` script to deploy your built project via SSH and PM2.
package/bin/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const readline = require('readline');
6
+ const crypto = require('crypto');
6
7
  const { execSync } = require('child_process');
7
8
 
8
9
  const rl = readline.createInterface({
@@ -13,34 +14,104 @@ const rl = readline.createInterface({
13
14
  const askQuestion = (query) => new Promise(resolve => rl.question(query, resolve));
14
15
 
15
16
  async function main() {
16
- console.log('🚀 Welcome to easy-starter!');
17
+ console.log('🚀 Welcome to easy-starter v2.1.0!');
17
18
 
18
- let projectName = process.argv[2];
19
+ const isYes = process.argv.includes('-y') || process.argv.includes('--yes') || process.argv.includes('--default');
20
+ const hasFlag = (flag) => process.argv.includes(`--${flag}`);
21
+ const hasNoFlag = (flag) => process.argv.includes(`--no-${flag}`);
22
+
23
+ let args = process.argv.slice(2).filter(arg => !arg.startsWith('-'));
24
+
25
+ let projectName = args[0];
19
26
  if (!projectName) {
20
- projectName = await askQuestion('Enter the project name (e.g., my-awesome-app): ');
27
+ projectName = isYes ? 'my-app' : await askQuestion('Enter the project name (e.g., my-awesome-app): ');
21
28
  }
29
+ projectName = projectName.trim();
22
30
 
23
31
  if (!projectName) {
24
- console.error('Project name cannot be empty!');
32
+ console.error('Project name cannot be empty!');
25
33
  rl.close();
26
34
  process.exit(1);
27
35
  }
28
36
 
29
- const useAdminDashboard = await askQuestion('Do you want to include an admin dashboard for analytics? (y/n) [y]: ');
30
- const includeAdmin = useAdminDashboard.toLowerCase() !== 'n';
37
+ const getAnswer = async (question, flagName) => {
38
+ if (hasNoFlag(flagName)) return false;
39
+ if (hasFlag(flagName) || isYes) return true;
40
+ const ans = await askQuestion(question);
41
+ return ans.trim().toLowerCase() !== 'n';
42
+ };
43
+
44
+ console.log('\n🔒 1. User Authentication & Login (easy-user-auth)');
45
+ console.log(' Provides out-of-the-box user registration, login dialogs, secure cookie-based');
46
+ console.log(' sessions, and forgotten password recovery emails.');
47
+ const useAuth = await getAnswer(' Do you want to include user authentication? (y/n) [y]: ', 'auth');
48
+
49
+ console.log('\n📊 2. Analytics Tracking (easy-analytics)');
50
+ console.log(' Tracks page visits, referrers, resolutions, and browsers locally without cookies.');
51
+ console.log(' Also captures and records client-side/frontend errors encountered by users.');
52
+ console.log(' If the Admin Dashboard is enabled, it generates interactive visual graphs inside /admin.');
53
+ const useAnalytics = await getAnswer(' Do you want to include privacy-friendly analytics? (y/n) [y]: ', 'analytics');
54
+
55
+ console.log('\n🛠️ 3. Admin Dashboard (/admin)');
56
+ console.log(' A secure section to manage your app. Protected by user authentication roles');
57
+ console.log(' (admin/user) if auth is enabled, or a static environment password if disabled.');
58
+ const useAdmin = await getAnswer(' Do you want to include the admin dashboard? (y/n) [y]: ', 'admin');
59
+
60
+ let useAdminUsers = false;
61
+ let useAdminErrors = false;
62
+
63
+ if (useAdmin) {
64
+ if (useAuth) {
65
+ console.log('\n👥 3a. Admin User Management');
66
+ console.log(' Allows administrators to list registered users and change their roles');
67
+ console.log(' directly from the admin dashboard.');
68
+ useAdminUsers = await getAnswer(' Do you want to include User Management in the admin panel? (y/n) [y]: ', 'admin-users');
69
+ } else {
70
+ console.log('\nℹ️ Note: User Management is disabled in the admin panel because it requires User Authentication.');
71
+ }
72
+
73
+ if (useAnalytics) {
74
+ console.log('\n🚨 3b. Admin Error Logging');
75
+ console.log(' Captures frontend/client-side errors (requires easy-analytics) and lists them');
76
+ console.log(' inside the admin panel, including descriptions and collapsible stack traces.');
77
+ useAdminErrors = await getAnswer(' Do you want to include Error Logging in the admin panel? (y/n) [y]: ', 'admin-errors');
78
+ } else {
79
+ console.log('\nℹ️ Note: Error Logging is disabled in the admin panel because it requires');
80
+ console.log(' Analytics Tracking to be enabled.');
81
+ }
82
+ }
83
+
84
+ console.log('\n⚡ 4. Server-Side Rendering (ssr-hook)');
85
+ console.log(' Enables server-side pre-rendering for React. Enhances initial load speeds,');
86
+ console.log(' prevents content flashes, and optimizes your site for search engine crawlers (SEO).');
87
+ const useSSR = await getAnswer(' Do you want to enable Server-Side Rendering (SSR)? (y/n) [y]: ', 'ssr');
88
+
89
+ console.log('\n🖼️ 5. Social Preview og:image');
90
+ console.log(' Automatically screenshots and generates social preview card og:images for your pages');
91
+ console.log(' using a headless browser rendering engine (puppeteer) and caches them in the database.');
92
+ const useOgImage = await getAnswer(' Do you want to include dynamic og:image generation? (y/n) [y]: ', 'ogimage');
93
+
94
+ console.log('\n📱 6. Viewport Tuning');
95
+ console.log(' Injects a viewport event listener that forces a minimum width layout (600px)');
96
+ console.log(' for mobile devices, resolving scaling/rendering issues on iOS Safari.');
97
+ const useViewport = await getAnswer(' Do you want to fix iOS Safari viewport width issues? (y/n) [y]: ', 'viewport');
98
+
99
+ console.log('\n🖼️ 7. Express Middleware Image Resizing (easy-image-resizer)');
100
+ console.log(' Enables dynamic image resizing and format conversion on the fly');
101
+ console.log(' via URL query parameters (e.g. /images/icon.png?w=192&format=webp) with automatic mtime caching.');
102
+ const useResizer = await getAnswer(' Do you want to include dynamic image resizing? (y/n) [y]: ', 'resizer');
31
103
 
32
- console.log('\n🚢 Deployment (GitHub Actions, VPS, SSH, PM2)');
33
- console.log('You can automatically include a deployment script designed for GitHub Actions.');
34
- console.log('It connects to your VPS via SSH, uploads your project, builds it, and runs it via PM2.');
35
- const useDeployScript = await askQuestion('Do you want to include this GitHub Actions deploy script? (y/n) [y]: ');
36
- const includeDeploy = useDeployScript.toLowerCase() !== 'n';
104
+ console.log('\n🚢 8. Deployment Integration (node-ssh)');
105
+ console.log(' Configures deploy scripts and GitHub Actions workflows to build and transfer your code');
106
+ console.log(' via SSH straight to your own VPS (Virtual Private Server), where it runs managed by PM2.');
107
+ const useDeploy = await getAnswer(' Do you want to include VPS deploy scripts? (y/n) [y]: ', 'deploy');
37
108
 
38
109
  rl.close();
39
110
 
40
111
  const targetPath = path.join(process.cwd(), projectName);
41
112
 
42
113
  if (fs.existsSync(targetPath)) {
43
- console.error(`Folder ${projectName} already exists!`);
114
+ console.error(`❌ Folder ${projectName} already exists!`);
44
115
  process.exit(1);
45
116
  }
46
117
 
@@ -49,6 +120,34 @@ async function main() {
49
120
 
50
121
  const templatePath = path.join(__dirname, '..', 'template');
51
122
 
123
+ // Ignored files based on configuration
124
+ const ignoreList = [
125
+ 'env',
126
+ 'node_modules',
127
+ 'dist',
128
+ 'cache',
129
+ 'easy-db',
130
+ 'easy-db-files',
131
+ 'easy-db-backup',
132
+ '.vite',
133
+ 'package-lock.json',
134
+ '.DS_Store'
135
+ ];
136
+ if (!useDeploy) {
137
+ ignoreList.push('scripts');
138
+ ignoreList.push('.github');
139
+ }
140
+ if (!useAuth) {
141
+ ignoreList.push('mail.tsx');
142
+ ignoreList.push('ResetPassword.tsx');
143
+ }
144
+ if (!useAdmin) {
145
+ ignoreList.push('admin');
146
+ }
147
+ if (!useResizer) {
148
+ ignoreList.push('Img.tsx');
149
+ }
150
+
52
151
  function copyFolderSync(from, to, ignoreFiles = []) {
53
152
  fs.mkdirSync(to, { recursive: true });
54
153
  fs.readdirSync(from).forEach(element => {
@@ -64,78 +163,181 @@ async function main() {
64
163
  });
65
164
  }
66
165
 
67
- const ignoreList = includeDeploy ? [] : ['scripts', '.github'];
68
166
  copyFolderSync(templatePath, targetPath, ignoreList);
69
167
 
70
- // Rename safe files back to dotfiles
71
- if (fs.existsSync(path.join(targetPath, 'env'))) {
72
- fs.renameSync(path.join(targetPath, 'env'), path.join(targetPath, '.env'));
73
- }
168
+ // Rename gitignore to .gitignore
74
169
  if (fs.existsSync(path.join(targetPath, 'gitignore'))) {
75
170
  fs.renameSync(path.join(targetPath, 'gitignore'), path.join(targetPath, '.gitignore'));
76
171
  }
77
172
 
78
- // Update site.webmanifest
79
- const manifestPath = path.join(targetPath, 'src', 'site.webmanifest');
173
+ // Update site.webmanifest name
174
+ const manifestPath = path.join(targetPath, 'public', 'site.webmanifest');
80
175
  if (fs.existsSync(manifestPath)) {
81
176
  let manifestContent = fs.readFileSync(manifestPath, 'utf-8');
82
177
  manifestContent = manifestContent.replace(/__NAME__/g, projectName);
83
178
  fs.writeFileSync(manifestPath, manifestContent);
84
179
  }
85
180
 
86
- // Handle Admin section removal if the user doesn't want it
87
- if (!includeAdmin) {
88
- const adminPath = path.join(targetPath, 'src', 'pages', 'admin');
89
- if (fs.existsSync(adminPath)) {
90
- fs.rmSync(adminPath, { recursive: true, force: true });
181
+ // Helper for processing feature block comments recursively
182
+ const startRegex = (blockName) => new RegExp(`(?:\\/\\/|\\/\\*|<!--)\\s*---\\s*${blockName}\\s*START\\s*---\\s*(?:\\*\\/|-->)?\\r?\\n?`, 'g');
183
+ const endRegex = (blockName) => new RegExp(`(?:\\/\\/|\\/\\*|<!--)\\s*---\\s*${blockName}\\s*END\\s*---\\s*(?:\\*\\/|-->)?\\r?\\n?`, 'g');
184
+ const fullRegex = (blockName) => new RegExp(`(?:\\/\\/|\\/\\*|<!--)\\s*---\\s*${blockName}\\s*START\\s*---\\s*(?:\\*\\/|-->)?\\r?\\n?[\\s\\S]*?(?:\\/\\/|\\/\\*|<!--)\\s*---\\s*${blockName}\\s*END\\s*---\\s*(?:\\*\\/|-->)?\\r?\\n?`, 'g');
185
+
186
+ function handleBlock(content, name, enabled) {
187
+ if (enabled) {
188
+ return content.replace(startRegex(name), '').replace(endRegex(name), '');
189
+ } else {
190
+ return content.replace(fullRegex(name), '');
91
191
  }
192
+ }
92
193
 
93
- // Strip // --- ADMIN START --- ... // --- ADMIN END --- blocks
94
- const stripAdminBlocks = (filePath) => {
95
- if (fs.existsSync(filePath)) {
96
- let content = fs.readFileSync(filePath, 'utf-8');
97
- content = content.replace(/\/\/ --- ADMIN START ---[\s\S]*?\/\/ --- ADMIN END ---\n?/g, '');
98
- fs.writeFileSync(filePath, content);
99
- }
100
- };
194
+ function processBlocks(content) {
195
+ content = handleBlock(content, 'AUTH', useAuth);
196
+ content = handleBlock(content, 'NO_AUTH', !useAuth);
197
+
198
+ content = handleBlock(content, 'VIEWPORT', useViewport);
199
+
200
+ content = handleBlock(content, 'ADMIN', useAdmin);
201
+ content = handleBlock(content, 'NO_ADMIN', !useAdmin);
202
+
203
+ if (useAdmin) {
204
+ content = handleBlock(content, 'ADMIN_WITH_AUTH', useAuth);
205
+ content = handleBlock(content, 'ADMIN_NO_AUTH', !useAuth);
206
+
207
+ content = handleBlock(content, 'ADMIN_USERS', useAuth && useAdminUsers);
208
+ content = handleBlock(content, 'NO_ADMIN_USERS', !(useAuth && useAdminUsers));
209
+
210
+ content = handleBlock(content, 'ADMIN_ERRORS', useAdminErrors);
211
+ content = handleBlock(content, 'NO_ADMIN_ERRORS', !useAdminErrors);
212
+
213
+ content = handleBlock(content, 'ADMIN_ERRORS_WITH_AUTH', useAdminErrors && useAuth);
214
+ content = handleBlock(content, 'ADMIN_ERRORS_NO_AUTH', useAdminErrors && !useAuth);
215
+ } else {
216
+ content = handleBlock(content, 'ADMIN_WITH_AUTH', false);
217
+ content = handleBlock(content, 'ADMIN_NO_AUTH', false);
218
+
219
+ content = handleBlock(content, 'ADMIN_USERS', false);
220
+ content = handleBlock(content, 'NO_ADMIN_USERS', true);
101
221
 
102
- stripAdminBlocks(path.join(targetPath, 'src', 'Router.tsx'));
103
- stripAdminBlocks(path.join(targetPath, 'server', 'index.tsx'));
104
- stripAdminBlocks(path.join(targetPath, 'src', 'styles.scss'));
222
+ content = handleBlock(content, 'ADMIN_ERRORS', false);
223
+ content = handleBlock(content, 'NO_ADMIN_ERRORS', true);
224
+
225
+ content = handleBlock(content, 'ADMIN_ERRORS_WITH_AUTH', false);
226
+ content = handleBlock(content, 'ADMIN_ERRORS_NO_AUTH', false);
227
+ }
228
+
229
+ content = handleBlock(content, 'ANALYTICS', useAnalytics);
230
+ content = handleBlock(content, 'NO_ANALYTICS', !useAnalytics);
231
+
232
+ content = handleBlock(content, 'SSR', useSSR);
233
+ content = handleBlock(content, 'NO_SSR', !useSSR);
234
+
235
+ content = handleBlock(content, 'OGIMAGE', useOgImage);
236
+ content = handleBlock(content, 'NO_OGIMAGE', !useOgImage);
237
+
238
+ content = handleBlock(content, 'RESIZER', useResizer);
239
+ content = handleBlock(content, 'NO_RESIZER', !useResizer);
240
+
241
+ return content;
105
242
  }
106
243
 
107
- // Update package.json from template
244
+ function processDirectory(directory) {
245
+ fs.readdirSync(directory).forEach(element => {
246
+ const fullPath = path.join(directory, element);
247
+ const stat = fs.lstatSync(fullPath);
248
+ if (stat.isDirectory()) {
249
+ processDirectory(fullPath);
250
+ } else if (stat.isFile()) {
251
+ const ext = path.extname(element);
252
+ const textExtensions = ['.ts', '.tsx', '.js', '.jsx', '.css', '.html', '.cursorrules', '.webmanifest', 'README.md'];
253
+ if (textExtensions.includes(ext) || element === '.cursorrules' || element === 'README.md') {
254
+ let fileContent = fs.readFileSync(fullPath, 'utf-8');
255
+ fileContent = processBlocks(fileContent);
256
+ fileContent = fileContent.replace(/__NAME__/g, projectName);
257
+ fs.writeFileSync(fullPath, fileContent);
258
+ }
259
+ }
260
+ });
261
+ }
262
+
263
+ processDirectory(targetPath);
264
+
265
+ // Generate dynamic package.json
108
266
  const packageJsonPath = path.join(targetPath, 'package.json');
109
267
  if (fs.existsSync(packageJsonPath)) {
110
268
  const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
111
269
  pkg.name = projectName;
112
270
 
113
- if (includeDeploy) {
114
- pkg.devDependencies = pkg.devDependencies || {};
115
- pkg.devDependencies['node-ssh'] = '^13.2.1';
116
- pkg.scripts = pkg.scripts || {};
117
- pkg.scripts['deploy'] = 'tsx scripts/deploy.ts';
118
- } else if (pkg.scripts && pkg.scripts.deploy) {
119
- delete pkg.scripts.deploy;
271
+ if (!useAnalytics) {
272
+ delete pkg.dependencies['easy-analytics'];
273
+ }
274
+ if (!useAuth) {
275
+ delete pkg.dependencies['easy-user-auth'];
276
+ delete pkg.dependencies['nodemailer'];
277
+ if (pkg.devDependencies) delete pkg.devDependencies['@types/nodemailer'];
278
+ }
279
+ if (!useSSR) {
280
+ delete pkg.dependencies['ssr-hook'];
281
+ }
282
+ if (!useOgImage) {
283
+ delete pkg.dependencies['puppeteer'];
284
+ }
285
+ if (!useResizer) {
286
+ delete pkg.dependencies['easy-image-resizer'];
287
+ }
288
+ if (!useDeploy) {
289
+ if (pkg.devDependencies) delete pkg.devDependencies['node-ssh'];
290
+ if (pkg.scripts && pkg.scripts.deploy) {
291
+ delete pkg.scripts.deploy;
292
+ }
120
293
  }
121
294
 
122
295
  fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2));
123
296
  }
124
297
 
298
+ // Generate dynamic .env file
299
+ const envPath = path.join(targetPath, '.env');
300
+ let envContent = `SERVER_PORT=1111\nSERVER_URL=https://${projectName}.com\n`;
301
+
302
+ if (useAdmin && !useAuth) {
303
+ envContent += `ADMIN_PASSWORD=admin123\n`;
304
+ }
305
+
306
+ if (useAuth) {
307
+ const jwtSecret = crypto.randomBytes(32).toString('hex');
308
+ envContent += `\n# Authentication\nJWT_SECRET=${jwtSecret}\n`;
309
+ envContent += `# SMTP mail configuration (required for forgotten password recovery)\nMAIL_SMTP=smtp.example.com\nMAIL_PORT=587\nMAIL_USER=your-email@example.com\nMAIL_PASSWORD=your-email-password\n`;
310
+ }
311
+
312
+ fs.writeFileSync(envPath, envContent);
313
+
125
314
  console.log('📦 Installing dependencies (this may take a while)...');
126
315
  try {
127
316
  execSync('npm install', { cwd: targetPath, stdio: 'inherit' });
128
317
  } catch (error) {
129
- console.error('Failed to install dependencies.', error);
130
- process.exit(1);
318
+ console.warn('\n⚠️ Failed to install dependencies with default npm install (often due to Puppeteer browser download issue).');
319
+ console.log('Retrying with PUPPETEER_SKIP_DOWNLOAD=1...');
320
+ try {
321
+ execSync('npm install', { cwd: targetPath, stdio: 'inherit', env: { ...process.env, PUPPETEER_SKIP_DOWNLOAD: '1' } });
322
+ } catch (retryError) {
323
+ console.error('❌ Failed to install dependencies on retry.', retryError);
324
+ process.exit(1);
325
+ }
131
326
  }
132
327
 
133
328
  console.log(`\n🎉 Project ${projectName} has been successfully created!`);
134
- console.log('\nGetting started:');
329
+ console.log('\n🚀 Getting started:');
135
330
  console.log(` cd ${projectName}`);
136
- console.log(' npm run dev -> http://localhost:1234');
137
- if (includeDeploy) {
138
- console.log(' npm run deploy');
331
+ console.log(' npm run dev (Starts backend and Vite frontend dev server at http://localhost:5173)');
332
+
333
+ if (useAuth) {
334
+ console.log('\n⚠️ IMPORTANT FOR AUTHENTICATION:');
335
+ console.log(' Configure your SMTP settings inside the generated .env file');
336
+ console.log(' to enable password recovery email delivery.');
337
+ }
338
+
339
+ if (useDeploy) {
340
+ console.log(' npm run deploy (Deploy to your VPS)');
139
341
  }
140
342
  }
141
343
 
package/package.json CHANGED
@@ -1,24 +1,33 @@
1
1
  {
2
2
  "name": "easy-starter",
3
- "version": "1.2.1",
4
- "description": "A lightweight CLI tool to bootstrap modern fullstack React web projects.",
3
+ "version": "2.1.0",
4
+ "description": "A lightweight, production-ready fullstack React boilerplate generator with Vite v8, TypeScript 7, server-side rendering (SSR), local analytics, SSH deploy script, PM2, and secure user authentication.",
5
5
  "bin": {
6
6
  "easy-starter": "./bin/index.js"
7
7
  },
8
8
  "keywords": [
9
9
  "cli",
10
- "bootstrap",
11
- "starter",
12
- "react",
13
- "easy",
10
+ "boilerplate",
11
+ "starter-kit",
12
+ "react-starter",
13
+ "vite-template",
14
+ "typescript-boilerplate",
15
+ "fullstack-boilerplate",
16
+ "server-side-rendering",
14
17
  "ssr",
15
- "typescript",
16
- "fullstack",
18
+ "user-authentication",
19
+ "user-login",
20
+ "easy-user-auth",
21
+ "easy-analytics",
17
22
  "easy-db",
18
- "parcel",
19
- "ai",
23
+ "local-database",
24
+ "pm2-deployment",
25
+ "vps-deploy",
26
+ "ssh-deployment",
27
+ "ai-friendly",
20
28
  "cursorrules",
21
- "ai-friendly"
29
+ "quickstart",
30
+ "zero-config"
22
31
  ],
23
32
  "author": "Filip Paulů <ing.fenix@seznam.cz>",
24
33
  "license": "MIT",
@@ -95,14 +95,46 @@ export type Database = {
95
95
 
96
96
  ## 4. Styling & Image Manipulation Guidelines
97
97
 
98
- ### CSS / SCSS Implementation
99
- * **Centralized & Generic**: Implement as many styles as possible in `src/styles.scss` using generic, reusable selectors.
100
- * **Minimize Inline Styles**: Avoid inline styles (e.g., `style={{ ... }}`) in React components. Keep style declarations in the central SCSS file to maintain design consistency and code quality.
101
-
98
+ ### CSS Implementation
99
+ * **Centralized & Generic**: Implement as many styles as possible in `src/styles.css` using generic, reusable selectors.
100
+ * **Minimize Inline Styles**: Avoid inline styles (e.g., `style={{ ... }}`) in React components. Keep style declarations in the central CSS file to maintain design consistency and code quality.
101
+
102
+ /* --- RESIZER START --- */
103
+ ### Image Asset Handling (`easy-image-resizer` & `<Img />` Component)
104
+ * **Dynamic Image Resizing via Express Middleware (`easy-image-resizer`):**
105
+ - All image assets (logos, icons, banners) must be placed inside `/public` (e.g. `/public/images/icon.png`).
106
+ - Request dynamically resized, cropped, or format-converted images on the fly via URL query parameters in CSS `url()` or via `<Img />` component props.
107
+ - **CORRECT (CSS):**
108
+ ```css
109
+ .hero-banner {
110
+ background-image: url("/images/hero.png?w=1200&format=webp&q=85");
111
+ }
112
+ ```
113
+ * **JavaScript Imports for Images are Forbidden:**
114
+ - Do **NOT** import image assets inside `.js`, `.ts`, or `.tsx` files (e.g. do NOT write `import hero from "./hero.jpg"`).
115
+ * **Use `<Img />` Component for Images in React TSX (Do NOT use standard `<img>` tags):**
116
+ - When displaying images inside React TSX components, **always use the `<Img />` component** imported from `../components/Img` (or `./components/Img`) instead of native HTML `<img>` tags.
117
+ - The `<Img />` component automatically sets default format to `"webp"` and constructs resizer query parameters.
118
+ - Supported props on `<Img />`:
119
+ - `src`: Original path to image inside `/public` (e.g. `"/images/icon.png"`)
120
+ - `w` or `width`: Target width (px)
121
+ - `h` or `height`: Target height (px)
122
+ - `mw` or `maxWidth`: Maximum width (px)
123
+ - `mh` or `maxHeight`: Maximum height (px)
124
+ - `format` or `type`: `"webp"` (default), `"jpeg"`, or `"png"`
125
+ - `q` or `quality`: 1 to 100
126
+ - **CORRECT (TSX):**
127
+ ```tsx
128
+ import { Img } from "../components/Img";
129
+
130
+ <Img src="/images/logo.png" w={300} h={100} alt="Application Logo" />
131
+ ```
132
+ /* --- RESIZER END --- */
133
+ /* --- NO_RESIZER START --- */
102
134
  ### Image Asset Handling
103
- * **STRONG PREFERENCE**: It is highly preferred to insert images as `background-image` declarations inside `src/styles.scss` rather than using `<img>` tags in React components wherever possible.
104
- * **CRITICAL FOR SCSS**: When referencing images inside `src/styles.scss` (e.g., as background-image), always append the `?as=webp` query parameter to convert them to modern WebP format during build, unless the source file is already an `.svg`. Do specify width or height query parameters.
105
- * **CORRECT**: `background-image: url("./images/icon.png?as=webp&height=128");`
106
- * **CRITICAL FOR CODE (React / HTML / JavaScript)**: If you must reference an image path directly within the code (e.g., in `<img src="..." />` or JS variables), you **MUST** only refer to images located inside the `public` directory (and nowhere else).
107
- * **CORRECT**: `<img src="/logo.png" />` (when referencing `/public/logo.png`)
108
- * **INCORRECT**: `<img src="../src/images/logo.png" />` or importing images directly into React files.
135
+ * **JavaScript Imports for Images are Forbidden:**
136
+ - Do **NOT** import image assets inside `.js`, `.ts`, or `.tsx` files (e.g. do NOT write `import hero from "./hero.jpg"`).
137
+ * **Referencing Images in TSX/React (via `/public`):**
138
+ - Place original images in `/public` and reference them using standard absolute URL paths.
139
+ - **CORRECT (TSX):** `<img src="/images/logo.png" alt="Logo" />`
140
+ /* --- NO_RESIZER END --- */