easy-starter 2.0.0 ā 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 +5 -4
- package/bin/index.js +65 -38
- package/package.json +1 -1
- package/template/.cursorrules +32 -7
- package/template/README.md +48 -1
- package/template/gitignore +1 -0
- package/template/index.html +3 -3
- package/template/package.json +11 -4
- package/template/public/site.webmanifest +8 -8
- package/template/scripts/deploy.ts +3 -2
- package/template/server/index.tsx +42 -30
- package/template/src/Router.tsx +2 -2
- package/template/src/components/Img.tsx +127 -0
- package/template/src/pages/Index.tsx +4 -13
- package/template/src/pages/NotFound.tsx +1 -1
- package/template/src/pages/ResetPassword.tsx +6 -8
- package/template/src/pages/admin/Admin.tsx +31 -59
- package/template/src/pages/admin/AnalyticsTab.tsx +4 -4
- package/template/src/pages/admin/ErrorsTab.tsx +13 -13
- package/template/src/pages/admin/UsersTab.tsx +15 -22
- package/template/src/services/common.ts +11 -1
- package/template/src/styles.css +207 -0
- package/template/tsconfig.json +5 -1
- package/template/types.ts +4 -4
- package/template/vite.config.ts +7 -2
- package/template/public/images/icon-16.png +0 -0
- package/template/public/images/icon-180.png +0 -0
- package/template/public/images/icon-192.png +0 -0
- package/template/public/images/icon-32.png +0 -0
- package/template/public/images/icon-512.png +0 -0
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@ The generated boilerplate can be configured to contain:
|
|
|
35
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.
|
|
36
36
|
* **Routing:** [`easy-page-router`](https://www.npmjs.com/package/easy-page-router) - A lightweight, programmatic routing solution.
|
|
37
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.
|
|
38
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.
|
|
39
40
|
* **Deployment:** Integrated Github Actions workflow and PM2 deployment script for seamless, automated deployment straight to your own VPS.
|
|
40
41
|
|
|
@@ -46,11 +47,11 @@ When you run `npx easy-starter`, the CLI will prompt you with the following conf
|
|
|
46
47
|
|
|
47
48
|
1. **Project name:** Choose the name of the folder and the app.
|
|
48
49
|
2. **User Authentication:** Do you want to add user login/registration widgets? (Installs `easy-user-auth` and nodemailer, generates JWT secrets in `.env`).
|
|
49
|
-
3. **
|
|
50
|
+
3. **Analytics:** Do you want local visitor analytics? (Installs `easy-analytics`, adds dashboard visualization).
|
|
50
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).
|
|
51
|
-
5. **
|
|
52
|
-
6. **
|
|
53
|
-
7. **
|
|
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).
|
|
54
55
|
8. **Deployment Script:** Do you want to deploy to your VPS using GitHub Actions? (Installs `node-ssh`, adds SSH deploy script).
|
|
55
56
|
|
|
56
57
|
---
|
package/bin/index.js
CHANGED
|
@@ -14,11 +14,17 @@ const rl = readline.createInterface({
|
|
|
14
14
|
const askQuestion = (query) => new Promise(resolve => rl.question(query, resolve));
|
|
15
15
|
|
|
16
16
|
async function main() {
|
|
17
|
-
console.log('š Welcome to easy-starter v2.
|
|
17
|
+
console.log('š Welcome to easy-starter v2.1.0!');
|
|
18
18
|
|
|
19
|
-
|
|
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];
|
|
20
26
|
if (!projectName) {
|
|
21
|
-
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): ');
|
|
22
28
|
}
|
|
23
29
|
projectName = projectName.trim();
|
|
24
30
|
|
|
@@ -28,24 +34,28 @@ async function main() {
|
|
|
28
34
|
process.exit(1);
|
|
29
35
|
}
|
|
30
36
|
|
|
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
|
+
|
|
31
44
|
console.log('\nš 1. User Authentication & Login (easy-user-auth)');
|
|
32
45
|
console.log(' Provides out-of-the-box user registration, login dialogs, secure cookie-based');
|
|
33
46
|
console.log(' sessions, and forgotten password recovery emails.');
|
|
34
|
-
const
|
|
35
|
-
const useAuth = ansAuth.trim().toLowerCase() !== 'n';
|
|
47
|
+
const useAuth = await getAnswer(' Do you want to include user authentication? (y/n) [y]: ', 'auth');
|
|
36
48
|
|
|
37
49
|
console.log('\nš 2. Analytics Tracking (easy-analytics)');
|
|
38
50
|
console.log(' Tracks page visits, referrers, resolutions, and browsers locally without cookies.');
|
|
39
51
|
console.log(' Also captures and records client-side/frontend errors encountered by users.');
|
|
40
52
|
console.log(' If the Admin Dashboard is enabled, it generates interactive visual graphs inside /admin.');
|
|
41
|
-
const
|
|
42
|
-
const useAnalytics = ansAnalytics.trim().toLowerCase() !== 'n';
|
|
53
|
+
const useAnalytics = await getAnswer(' Do you want to include privacy-friendly analytics? (y/n) [y]: ', 'analytics');
|
|
43
54
|
|
|
44
55
|
console.log('\nš ļø 3. Admin Dashboard (/admin)');
|
|
45
56
|
console.log(' A secure section to manage your app. Protected by user authentication roles');
|
|
46
57
|
console.log(' (admin/user) if auth is enabled, or a static environment password if disabled.');
|
|
47
|
-
const
|
|
48
|
-
const useAdmin = ansAdmin.trim().toLowerCase() !== 'n';
|
|
58
|
+
const useAdmin = await getAnswer(' Do you want to include the admin dashboard? (y/n) [y]: ', 'admin');
|
|
49
59
|
|
|
50
60
|
let useAdminUsers = false;
|
|
51
61
|
let useAdminErrors = false;
|
|
@@ -55,8 +65,7 @@ async function main() {
|
|
|
55
65
|
console.log('\nš„ 3a. Admin User Management');
|
|
56
66
|
console.log(' Allows administrators to list registered users and change their roles');
|
|
57
67
|
console.log(' directly from the admin dashboard.');
|
|
58
|
-
|
|
59
|
-
useAdminUsers = ansAdminUsers.trim().toLowerCase() !== 'n';
|
|
68
|
+
useAdminUsers = await getAnswer(' Do you want to include User Management in the admin panel? (y/n) [y]: ', 'admin-users');
|
|
60
69
|
} else {
|
|
61
70
|
console.log('\nā¹ļø Note: User Management is disabled in the admin panel because it requires User Authentication.');
|
|
62
71
|
}
|
|
@@ -65,8 +74,7 @@ async function main() {
|
|
|
65
74
|
console.log('\nšØ 3b. Admin Error Logging');
|
|
66
75
|
console.log(' Captures frontend/client-side errors (requires easy-analytics) and lists them');
|
|
67
76
|
console.log(' inside the admin panel, including descriptions and collapsible stack traces.');
|
|
68
|
-
|
|
69
|
-
useAdminErrors = ansAdminErrors.trim().toLowerCase() !== 'n';
|
|
77
|
+
useAdminErrors = await getAnswer(' Do you want to include Error Logging in the admin panel? (y/n) [y]: ', 'admin-errors');
|
|
70
78
|
} else {
|
|
71
79
|
console.log('\nā¹ļø Note: Error Logging is disabled in the admin panel because it requires');
|
|
72
80
|
console.log(' Analytics Tracking to be enabled.');
|
|
@@ -76,26 +84,27 @@ async function main() {
|
|
|
76
84
|
console.log('\nā” 4. Server-Side Rendering (ssr-hook)');
|
|
77
85
|
console.log(' Enables server-side pre-rendering for React. Enhances initial load speeds,');
|
|
78
86
|
console.log(' prevents content flashes, and optimizes your site for search engine crawlers (SEO).');
|
|
79
|
-
const
|
|
80
|
-
const useSSR = ansSSR.trim().toLowerCase() !== 'n';
|
|
87
|
+
const useSSR = await getAnswer(' Do you want to enable Server-Side Rendering (SSR)? (y/n) [y]: ', 'ssr');
|
|
81
88
|
|
|
82
89
|
console.log('\nš¼ļø 5. Social Preview og:image');
|
|
83
90
|
console.log(' Automatically screenshots and generates social preview card og:images for your pages');
|
|
84
91
|
console.log(' using a headless browser rendering engine (puppeteer) and caches them in the database.');
|
|
85
|
-
const
|
|
86
|
-
const useOgImage = ansOgImage.trim().toLowerCase() !== 'n';
|
|
92
|
+
const useOgImage = await getAnswer(' Do you want to include dynamic og:image generation? (y/n) [y]: ', 'ogimage');
|
|
87
93
|
|
|
88
94
|
console.log('\nš± 6. Viewport Tuning');
|
|
89
95
|
console.log(' Injects a viewport event listener that forces a minimum width layout (600px)');
|
|
90
96
|
console.log(' for mobile devices, resolving scaling/rendering issues on iOS Safari.');
|
|
91
|
-
const
|
|
92
|
-
const useViewport = ansViewport.trim().toLowerCase() !== 'n';
|
|
97
|
+
const useViewport = await getAnswer(' Do you want to fix iOS Safari viewport width issues? (y/n) [y]: ', 'viewport');
|
|
93
98
|
|
|
94
|
-
console.log('\n
|
|
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');
|
|
103
|
+
|
|
104
|
+
console.log('\nš¢ 8. Deployment Integration (node-ssh)');
|
|
95
105
|
console.log(' Configures deploy scripts and GitHub Actions workflows to build and transfer your code');
|
|
96
106
|
console.log(' via SSH straight to your own VPS (Virtual Private Server), where it runs managed by PM2.');
|
|
97
|
-
const
|
|
98
|
-
const useDeploy = ansDeploy.trim().toLowerCase() !== 'n';
|
|
107
|
+
const useDeploy = await getAnswer(' Do you want to include VPS deploy scripts? (y/n) [y]: ', 'deploy');
|
|
99
108
|
|
|
100
109
|
rl.close();
|
|
101
110
|
|
|
@@ -112,7 +121,18 @@ async function main() {
|
|
|
112
121
|
const templatePath = path.join(__dirname, '..', 'template');
|
|
113
122
|
|
|
114
123
|
// Ignored files based on configuration
|
|
115
|
-
const ignoreList = [
|
|
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
|
+
];
|
|
116
136
|
if (!useDeploy) {
|
|
117
137
|
ignoreList.push('scripts');
|
|
118
138
|
ignoreList.push('.github');
|
|
@@ -124,6 +144,9 @@ async function main() {
|
|
|
124
144
|
if (!useAdmin) {
|
|
125
145
|
ignoreList.push('admin');
|
|
126
146
|
}
|
|
147
|
+
if (!useResizer) {
|
|
148
|
+
ignoreList.push('Img.tsx');
|
|
149
|
+
}
|
|
127
150
|
|
|
128
151
|
function copyFolderSync(from, to, ignoreFiles = []) {
|
|
129
152
|
fs.mkdirSync(to, { recursive: true });
|
|
@@ -210,6 +233,10 @@ async function main() {
|
|
|
210
233
|
content = handleBlock(content, 'NO_SSR', !useSSR);
|
|
211
234
|
|
|
212
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);
|
|
213
240
|
|
|
214
241
|
return content;
|
|
215
242
|
}
|
|
@@ -241,25 +268,25 @@ async function main() {
|
|
|
241
268
|
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
242
269
|
pkg.name = projectName;
|
|
243
270
|
|
|
244
|
-
if (useAnalytics) {
|
|
245
|
-
pkg.dependencies['easy-analytics']
|
|
271
|
+
if (!useAnalytics) {
|
|
272
|
+
delete pkg.dependencies['easy-analytics'];
|
|
246
273
|
}
|
|
247
|
-
if (useAuth) {
|
|
248
|
-
pkg.dependencies['easy-user-auth']
|
|
249
|
-
pkg.dependencies['nodemailer']
|
|
250
|
-
pkg.devDependencies['@types/nodemailer']
|
|
274
|
+
if (!useAuth) {
|
|
275
|
+
delete pkg.dependencies['easy-user-auth'];
|
|
276
|
+
delete pkg.dependencies['nodemailer'];
|
|
277
|
+
if (pkg.devDependencies) delete pkg.devDependencies['@types/nodemailer'];
|
|
251
278
|
}
|
|
252
|
-
if (useSSR) {
|
|
253
|
-
pkg.dependencies['ssr-hook']
|
|
279
|
+
if (!useSSR) {
|
|
280
|
+
delete pkg.dependencies['ssr-hook'];
|
|
254
281
|
}
|
|
255
|
-
if (useOgImage) {
|
|
256
|
-
pkg.dependencies['puppeteer']
|
|
282
|
+
if (!useOgImage) {
|
|
283
|
+
delete pkg.dependencies['puppeteer'];
|
|
257
284
|
}
|
|
258
|
-
if (
|
|
259
|
-
pkg.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
285
|
+
if (!useResizer) {
|
|
286
|
+
delete pkg.dependencies['easy-image-resizer'];
|
|
287
|
+
}
|
|
288
|
+
if (!useDeploy) {
|
|
289
|
+
if (pkg.devDependencies) delete pkg.devDependencies['node-ssh'];
|
|
263
290
|
if (pkg.scripts && pkg.scripts.deploy) {
|
|
264
291
|
delete pkg.scripts.deploy;
|
|
265
292
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "easy-starter",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
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"
|
package/template/.cursorrules
CHANGED
|
@@ -99,17 +99,42 @@ export type Database = {
|
|
|
99
99
|
* **Centralized & Generic**: Implement as many styles as possible in `src/styles.css` using generic, reusable selectors.
|
|
100
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
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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.
|
|
105
107
|
- **CORRECT (CSS):**
|
|
106
108
|
```css
|
|
107
109
|
.hero-banner {
|
|
108
|
-
background-image: url("
|
|
110
|
+
background-image: url("/images/hero.png?w=1200&format=webp&q=85");
|
|
109
111
|
}
|
|
110
112
|
```
|
|
111
113
|
* **JavaScript Imports for Images are Forbidden:**
|
|
112
114
|
- Do **NOT** import image assets inside `.js`, `.ts`, or `.tsx` files (e.g. do NOT write `import hero from "./hero.jpg"`).
|
|
113
|
-
* **
|
|
114
|
-
-
|
|
115
|
-
-
|
|
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 --- */
|
|
134
|
+
### Image Asset Handling
|
|
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 --- */
|
package/template/README.md
CHANGED
|
@@ -22,6 +22,9 @@ This project was bootstrapped with `easy-starter`, giving you maximum freedom wi
|
|
|
22
22
|
<!-- --- OGIMAGE START --- -->
|
|
23
23
|
* **og:image Generation**: Dynamic page screenshotting for social previews using headless `puppeteer`.
|
|
24
24
|
<!-- --- OGIMAGE END --- -->
|
|
25
|
+
<!-- --- RESIZER START --- -->
|
|
26
|
+
* **Image Resizing**: Dynamic image resizing and WebP/JPEG conversion on the fly via [`easy-image-resizer`](https://www.npmjs.com/package/easy-image-resizer) with automatic disk caching.
|
|
27
|
+
<!-- --- RESIZER END --- -->
|
|
25
28
|
|
|
26
29
|
---
|
|
27
30
|
|
|
@@ -135,7 +138,51 @@ export default function Index() {
|
|
|
135
138
|
```
|
|
136
139
|
<!-- --- NO_SSR END --- -->
|
|
137
140
|
|
|
138
|
-
|
|
141
|
+
<!-- --- RESIZER START --- -->
|
|
142
|
+
### 3. Dynamic Image Resizing (`easy-image-resizer`)
|
|
143
|
+
|
|
144
|
+
All images stored in `/public` (e.g., `/public/images/logo.png`) can be dynamically resized, cropped, and converted on the fly using `easy-image-resizer` Express middleware.
|
|
145
|
+
|
|
146
|
+
#### Using `<Img />` Component in React (TSX)
|
|
147
|
+
|
|
148
|
+
Use the `<Img />` component imported from `../components/Img` instead of standard `<img>` tags. It automatically appends query parameters and defaults to `"webp"` format for optimal performance:
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
import { Img } from "../components/Img";
|
|
152
|
+
|
|
153
|
+
export default function MyComponent() {
|
|
154
|
+
return (
|
|
155
|
+
<Img
|
|
156
|
+
src="/images/logo.png"
|
|
157
|
+
w={300}
|
|
158
|
+
h={150}
|
|
159
|
+
format="webp"
|
|
160
|
+
quality={85}
|
|
161
|
+
alt="App Logo"
|
|
162
|
+
/>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Available props on `<Img />`:
|
|
168
|
+
* `w` or `width`: Target width in pixels
|
|
169
|
+
* `h` or `height`: Target height in pixels
|
|
170
|
+
* `mw` or `maxWidth`, `mh` or `maxHeight`: Max dimension constraints
|
|
171
|
+
* `format` or `type`: `"webp"` (default), `"jpeg"`, or `"png"`
|
|
172
|
+
* `q` or `quality`: Image quality (1-100)
|
|
173
|
+
|
|
174
|
+
#### Using Image Resizing in CSS
|
|
175
|
+
|
|
176
|
+
You can also use URL query parameters directly inside CSS `url()` declarations:
|
|
177
|
+
|
|
178
|
+
```css
|
|
179
|
+
.hero-banner {
|
|
180
|
+
background-image: url("/images/hero.png?w=1200&format=webp&q=85");
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
<!-- --- RESIZER END --- -->
|
|
184
|
+
|
|
185
|
+
### 4. AI Integration (`.cursorrules`)
|
|
139
186
|
|
|
140
187
|
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`.
|
|
141
188
|
|
package/template/gitignore
CHANGED
package/template/index.html
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
-
<link rel="apple-touch-icon" sizes="180x180" href="/images/icon
|
|
7
|
-
<link rel="icon" type="image/png" sizes="32x32" href="/images/icon
|
|
8
|
-
<link rel="icon" type="image/png" sizes="16x16" href="/images/icon
|
|
6
|
+
<link rel="apple-touch-icon" sizes="180x180" href="/images/icon.png?w=180&h=180&format=webp" />
|
|
7
|
+
<link rel="icon" type="image/png" sizes="32x32" href="/images/icon.png?w=32&h=32" />
|
|
8
|
+
<link rel="icon" type="image/png" sizes="16x16" href="/images/icon.png?w=16&h=16" />
|
|
9
9
|
<link rel="manifest" href="/site.webmanifest" />
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/template/package.json
CHANGED
|
@@ -11,24 +11,31 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"cors": "^2.8.6",
|
|
14
|
+
"easy-analytics": "^1.0.1",
|
|
14
15
|
"easy-db-node": "^3.0.1",
|
|
16
|
+
"easy-image-resizer": "^2.0.1",
|
|
15
17
|
"easy-page-router": "^0.2.1",
|
|
18
|
+
"easy-user-auth": "^1.1.0",
|
|
16
19
|
"express": "^5.2.1",
|
|
20
|
+
"nodemailer": "^7.0.9",
|
|
21
|
+
"puppeteer": "^25.3.0",
|
|
17
22
|
"react": "^19.2.6",
|
|
18
23
|
"react-dom": "^19.2.6",
|
|
24
|
+
"ssr-hook": "^0.2.0",
|
|
19
25
|
"typed-client-server-api": "^1.1.0"
|
|
20
26
|
},
|
|
21
27
|
"devDependencies": {
|
|
22
28
|
"@types/cors": "^2.8.19",
|
|
23
29
|
"@types/express": "^5.0.6",
|
|
24
30
|
"@types/node": "^24.0.0",
|
|
31
|
+
"@types/nodemailer": "^6.4.15",
|
|
25
32
|
"@types/react": "^19.0.0",
|
|
26
33
|
"@types/react-dom": "^19.0.0",
|
|
34
|
+
"@vitejs/plugin-react": "^6.0.3",
|
|
27
35
|
"concurrently": "^9.2.1",
|
|
36
|
+
"node-ssh": "^13.2.1",
|
|
28
37
|
"tsx": "^4.21.0",
|
|
29
38
|
"typescript": "^7.0.0",
|
|
30
|
-
"vite": "^8.1.5"
|
|
31
|
-
"@vitejs/plugin-react": "^6.0.3",
|
|
32
|
-
"vite-imagetools": "^10.0.1"
|
|
39
|
+
"vite": "^8.1.5"
|
|
33
40
|
}
|
|
34
|
-
}
|
|
41
|
+
}
|
|
@@ -8,33 +8,33 @@
|
|
|
8
8
|
"background_color": "#09090b",
|
|
9
9
|
"icons": [
|
|
10
10
|
{
|
|
11
|
-
"src": "/images/icon
|
|
11
|
+
"src": "/images/icon.png?w=16&h=16",
|
|
12
12
|
"sizes": "16x16",
|
|
13
13
|
"type": "image/png",
|
|
14
14
|
"purpose": "maskable"
|
|
15
15
|
},
|
|
16
16
|
{
|
|
17
|
-
"src": "/images/icon
|
|
17
|
+
"src": "/images/icon.png?w=32&h=32",
|
|
18
18
|
"sizes": "32x32",
|
|
19
19
|
"type": "image/png",
|
|
20
20
|
"purpose": "maskable"
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
|
-
"src": "/images/icon
|
|
23
|
+
"src": "/images/icon.png?w=180&h=180&format=webp",
|
|
24
24
|
"sizes": "180x180",
|
|
25
|
-
"type": "image/
|
|
25
|
+
"type": "image/webp",
|
|
26
26
|
"purpose": "maskable"
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
|
-
"src": "/images/icon
|
|
29
|
+
"src": "/images/icon.png?w=192&h=192&format=webp",
|
|
30
30
|
"sizes": "192x192",
|
|
31
|
-
"type": "image/
|
|
31
|
+
"type": "image/webp",
|
|
32
32
|
"purpose": "maskable"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
|
-
"src": "/images/icon
|
|
35
|
+
"src": "/images/icon.png?w=512&h=512&format=webp",
|
|
36
36
|
"sizes": "512x512",
|
|
37
|
-
"type": "image/
|
|
37
|
+
"type": "image/webp",
|
|
38
38
|
"purpose": "maskable"
|
|
39
39
|
}
|
|
40
40
|
]
|
|
@@ -17,7 +17,8 @@ const path = resolve(process.cwd()); // Local project path
|
|
|
17
17
|
const ignoreFiles = [
|
|
18
18
|
".git",
|
|
19
19
|
".github",
|
|
20
|
-
".
|
|
20
|
+
".vite",
|
|
21
|
+
"dist",
|
|
21
22
|
"node_modules",
|
|
22
23
|
"scripts",
|
|
23
24
|
"easy-db",
|
|
@@ -70,7 +71,7 @@ async function deploy() {
|
|
|
70
71
|
const timeServerOff = new Date().getTime();
|
|
71
72
|
|
|
72
73
|
console.log(`Removing old build...`);
|
|
73
|
-
await sshExec(ssh, "rm -rf .
|
|
74
|
+
await sshExec(ssh, "rm -rf .vite", "Failed to remove .vite folder.");
|
|
74
75
|
await sshExec(ssh, "rm -rf dist", "Failed to remove dist folder.");
|
|
75
76
|
console.log(`ā Old build removed.\n`);
|
|
76
77
|
|