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 +33 -80
- package/bin/index.js +251 -49
- package/package.json +20 -11
- package/template/.cursorrules +42 -10
- package/template/README.md +129 -25
- package/template/env +0 -1
- package/template/gitignore +2 -1
- package/template/index.html +15 -0
- package/template/package.json +14 -8
- package/template/{src → public}/site.webmanifest +6 -12
- package/template/scripts/deploy.ts +3 -2
- package/template/server/index.tsx +264 -56
- package/template/server/mail.tsx +120 -0
- package/template/src/App.tsx +33 -7
- package/template/src/Router.tsx +13 -4
- package/template/src/components/Img.tsx +127 -0
- package/template/src/index.tsx +28 -1
- package/template/src/pages/Index.tsx +36 -6
- package/template/src/pages/NotFound.tsx +1 -1
- package/template/src/pages/ResetPassword.tsx +124 -0
- package/template/src/pages/admin/Admin.tsx +332 -85
- package/template/src/pages/admin/AnalyticsTab.tsx +71 -0
- package/template/src/pages/admin/ErrorsTab.tsx +46 -0
- package/template/src/pages/admin/UsersTab.tsx +54 -0
- package/template/src/services/api.ts +5 -7
- package/template/src/services/common.ts +34 -1
- package/template/src/styles.css +667 -0
- package/template/tsconfig.json +7 -2
- package/template/types.ts +44 -3
- package/template/vite.config.ts +29 -0
- package/template/.parcelrc +0 -6
- package/template/src/images/icon.png +0 -0
- package/template/src/index.html +0 -16
- package/template/src/styles.scss +0 -292
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
|
-
|
|
13
|
+
---
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
## What's New in v2.0.0?
|
|
16
16
|
|
|
17
|
-
**
|
|
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
|
-
|
|
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
|
|
28
|
+
The generated boilerplate can be configured to contain:
|
|
28
29
|
|
|
29
|
-
* **Frontend/Backend:** React 19, TypeScript, and Express.
|
|
30
|
-
* **Bundler:**
|
|
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
|
-
|
|
42
|
+
---
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
## Interactive Configuration Prompts
|
|
42
45
|
|
|
43
|
-
|
|
46
|
+
When you run `npx easy-starter`, the CLI will prompt you with the following configuration questions:
|
|
44
47
|
|
|
45
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
30
|
-
|
|
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 (
|
|
33
|
-
console.log('
|
|
34
|
-
console.log('
|
|
35
|
-
const
|
|
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(
|
|
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
|
|
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, '
|
|
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
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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 (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
pkg.
|
|
118
|
-
|
|
119
|
-
delete pkg.
|
|
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.
|
|
130
|
-
|
|
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('\
|
|
329
|
+
console.log('\n🚀 Getting started:');
|
|
135
330
|
console.log(` cd ${projectName}`);
|
|
136
|
-
console.log(' npm run dev
|
|
137
|
-
|
|
138
|
-
|
|
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": "
|
|
4
|
-
"description": "A lightweight
|
|
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
|
-
"
|
|
11
|
-
"starter",
|
|
12
|
-
"react",
|
|
13
|
-
"
|
|
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
|
-
"
|
|
16
|
-
"
|
|
18
|
+
"user-authentication",
|
|
19
|
+
"user-login",
|
|
20
|
+
"easy-user-auth",
|
|
21
|
+
"easy-analytics",
|
|
17
22
|
"easy-db",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
23
|
+
"local-database",
|
|
24
|
+
"pm2-deployment",
|
|
25
|
+
"vps-deploy",
|
|
26
|
+
"ssh-deployment",
|
|
27
|
+
"ai-friendly",
|
|
20
28
|
"cursorrules",
|
|
21
|
-
"
|
|
29
|
+
"quickstart",
|
|
30
|
+
"zero-config"
|
|
22
31
|
],
|
|
23
32
|
"author": "Filip Paulů <ing.fenix@seznam.cz>",
|
|
24
33
|
"license": "MIT",
|
package/template/.cursorrules
CHANGED
|
@@ -95,14 +95,46 @@ export type Database = {
|
|
|
95
95
|
|
|
96
96
|
## 4. Styling & Image Manipulation Guidelines
|
|
97
97
|
|
|
98
|
-
### CSS
|
|
99
|
-
* **Centralized & Generic**: Implement as many styles as possible in `src/styles.
|
|
100
|
-
* **Minimize Inline Styles**: Avoid inline styles (e.g., `style={{ ... }}`) in React components. Keep style declarations in the central
|
|
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
|
-
* **
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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 --- */
|