@developerankitkumar/create-reactforge 0.0.0-stage → 1.0.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,3 +1,88 @@
1
- # Temporary Holding Version
1
+ # React TypeScript Starter CLI
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A reusable React + TypeScript starter template with Material UI, SCSS, authentication, API integration, dynamic themes, and ready-to-use components.
4
+
5
+ ## Quick Start
6
+
7
+ Create a new project using:
8
+
9
+ ```bash
10
+ npx --yes @developerankitkumar/create-react-starter@latest my-new-app
11
+ ```
12
+
13
+ Then run:
14
+
15
+ ```bash
16
+ cd my-new-app
17
+ npm install
18
+ npm run dev
19
+ ```
20
+
21
+ ## Features
22
+
23
+ - React with TypeScript and Vite
24
+ - Material UI (MUI)
25
+ - SCSS architecture
26
+ - Dynamic Light, Dark, and System themes
27
+ - Authentication and protected routes
28
+ - Mock API and real backend API modes
29
+ - Configurable backend URL
30
+ - Reusable layout with sidebar and topbar
31
+ - Reusable form and table components
32
+ - User management example
33
+ - Snackbar notifications
34
+ - Excel import and file upload components
35
+ - Font Awesome Free icons
36
+
37
+ ## Interactive Setup
38
+
39
+ The CLI allows you to configure:
40
+
41
+ 1. Project name
42
+ 2. Default theme: System, Light, or Dark
43
+ 3. API mode: Mock API or Real API
44
+ 4. Backend URL when Real API is selected
45
+
46
+ ## Environment Configuration
47
+
48
+ Generated projects support environment variables such as:
49
+
50
+ ```env
51
+ VITE_USE_MOCK=true
52
+ VITE_API_BASE_URL=http://localhost:8000
53
+ ```
54
+
55
+ Use Mock API mode for frontend development without a backend, or Real API mode to connect your application to an existing backend.
56
+
57
+ ## Project Structure
58
+
59
+ ```text
60
+ src/
61
+ ├── components/
62
+ ├── context/
63
+ ├── interface/
64
+ ├── mock/
65
+ ├── pages/
66
+ ├── services/
67
+ ├── styles/
68
+ ├── types/
69
+ └── utils/
70
+ ```
71
+
72
+ ## Build for Production
73
+
74
+ ```bash
75
+ npm run build
76
+ ```
77
+
78
+ ## NPM Package
79
+
80
+ [@developerankitkumar/create-react-starter](https://www.npmjs.com/package/@developerankitkumar/create-react-starter)
81
+
82
+ ## GitHub Repository
83
+
84
+ [developerankitkumar/create-react-starter](https://github.com/developerankitkumar/create-react-starter)
85
+
86
+ ## Author
87
+
88
+ Developed by **developerankitkumar**.
package/bin/index.js ADDED
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import prompts from 'prompts';
7
+ import pc from 'picocolors';
8
+ import { downloadTemplate } from './template-downloader.js';
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+
12
+ let projectName = process.argv[2];
13
+
14
+ if (!projectName) {
15
+ const response = await prompts({
16
+ type: 'text',
17
+ name: 'projectName',
18
+ message: 'Enter your project name:',
19
+ initial: 'my-react-app',
20
+ validate: (value) =>
21
+ /^[a-z0-9][a-z0-9._-]*$/.test(value)
22
+ ? true
23
+ : 'Use a valid lowercase project name',
24
+ });
25
+
26
+ projectName = response.projectName;
27
+ }
28
+
29
+ if (!projectName) {
30
+ console.log(pc.yellow('Project creation cancelled.'));
31
+ process.exit(1);
32
+ }
33
+
34
+ if (
35
+ !/^[a-z0-9][a-z0-9._-]*$/.test(projectName) ||
36
+ projectName === '.' ||
37
+ projectName === '..'
38
+ ) {
39
+ console.error(pc.red('Invalid project name.'));
40
+ process.exit(1);
41
+ }
42
+
43
+ const themeResponse = await prompts({
44
+ type: 'select',
45
+ name: 'theme',
46
+ message: 'Choose default theme:',
47
+ choices: [
48
+ { title: 'System (Recommended)', value: 'system' },
49
+ { title: 'Light', value: 'light' },
50
+ { title: 'Dark', value: 'dark' },
51
+ ],
52
+ initial: 0,
53
+ });
54
+
55
+ if (!themeResponse.theme) {
56
+ console.log(pc.yellow('Project creation cancelled.'));
57
+ process.exit(1);
58
+ }
59
+
60
+ const authResponse = await prompts({
61
+ type: 'select',
62
+ name: 'authMode',
63
+ message: 'Choose API / Authentication mode:',
64
+ choices: [
65
+ { title: 'Mock API (Demo / Development)', value: 'mock' },
66
+ { title: 'Real API (Backend Integration)', value: 'real' },
67
+ ],
68
+ initial: 0,
69
+ });
70
+
71
+ if (!authResponse.authMode) {
72
+ console.log(pc.yellow('Project creation cancelled.'));
73
+ process.exit(1);
74
+ }
75
+
76
+ let apiBaseUrl = '';
77
+
78
+ if (authResponse.authMode === 'real') {
79
+ const apiResponse = await prompts({
80
+ type: 'text',
81
+ name: 'baseUrl',
82
+ message: 'Enter Backend API Base URL:',
83
+ initial: 'http://localhost:8000',
84
+ validate: (value) => {
85
+ try {
86
+ const url = new URL(value);
87
+
88
+ return ['http:', 'https:'].includes(url.protocol)
89
+ ? true
90
+ : 'URL must start with http:// or https://';
91
+ } catch {
92
+ return 'Please enter a valid API URL';
93
+ }
94
+ },
95
+ });
96
+
97
+ if (!apiResponse.baseUrl) {
98
+ console.log(pc.yellow('Project creation cancelled.'));
99
+ process.exit(1);
100
+ }
101
+
102
+ apiBaseUrl = apiResponse.baseUrl.trim().replace(/\/+$/, '');
103
+ }
104
+
105
+ const targetDir = path.resolve(process.cwd(), projectName);
106
+ const licenseResponse = await prompts({
107
+ type: 'password',
108
+ name: 'licenseKey',
109
+ message: 'Enter your React Starter license key:',
110
+ validate: (value) =>
111
+ /^AK-[A-F0-9]{8}(-[A-F0-9]{8}){3}$/i.test(value.trim())
112
+ ? true
113
+ : 'Please enter a valid license key format',
114
+ });
115
+
116
+ if (!licenseResponse.licenseKey) {
117
+ console.log(pc.yellow('Project creation cancelled.'));
118
+ process.exit(1);
119
+ }
120
+
121
+ const licenseKey = licenseResponse.licenseKey.trim();
122
+ if (fs.existsSync(targetDir)) {
123
+ console.error(pc.red(`Folder "${projectName}" already exists.`));
124
+ process.exit(1);
125
+ }
126
+
127
+ try {
128
+ console.log(pc.cyan('\nValidating license and downloading template...\n'));
129
+
130
+ await downloadTemplate({
131
+ licenseKey,
132
+ targetDir,
133
+ projectName,
134
+ serverUrl:
135
+ process.env.REACT_STARTER_LICENSE_SERVER_URL ||
136
+ 'https://license.ankitdeveloper.site',
137
+ });
138
+
139
+ console.log(pc.green('✔ License verified and template downloaded.\n'));
140
+
141
+ // Generate environment configuration
142
+ const envPath = path.join(targetDir, '.env');
143
+
144
+ const envContent = [
145
+ `VITE_USE_MOCK=${authResponse.authMode === 'mock'}`,
146
+ `VITE_API_BASE_URL=${apiBaseUrl}`,
147
+ 'VITE_NGROK_SKIP=',
148
+ '',
149
+ ].join('\n');
150
+
151
+ fs.writeFileSync(envPath, envContent);
152
+
153
+ // Apply selected default theme to generated project
154
+ const themeContextPath = path.join(
155
+ targetDir,
156
+ 'src/context/ThemeModeContext.tsx'
157
+ );
158
+
159
+ let themeContext = fs.readFileSync(themeContextPath, 'utf8');
160
+
161
+ const themePlaceholder =
162
+ "export const DEFAULT_THEME_MODE: ThemeMode = 'system'";
163
+
164
+ if (!themeContext.includes(themePlaceholder)) {
165
+ throw new Error('Default theme configuration not found in template.');
166
+ }
167
+
168
+ themeContext = themeContext.replace(
169
+ themePlaceholder,
170
+ `export const DEFAULT_THEME_MODE: ThemeMode = '${themeResponse.theme}'`
171
+ );
172
+
173
+ fs.writeFileSync(themeContextPath, themeContext);
174
+
175
+ // Create .gitignore in the generated project
176
+ const gitignorePath = path.join(targetDir, '.gitignore');
177
+
178
+ fs.writeFileSync(
179
+ gitignorePath,
180
+ [
181
+ 'node_modules/',
182
+ 'dist/',
183
+ '.env',
184
+ '.env.*',
185
+ '!.env.example',
186
+ '.DS_Store',
187
+ '.idea/',
188
+ '*.log',
189
+ '',
190
+ ].join('\n')
191
+ );
192
+
193
+ const packagePath = path.join(targetDir, 'package.json');
194
+ const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
195
+
196
+ pkg.name = projectName;
197
+ pkg.version = '0.1.0';
198
+ pkg.private = true;
199
+
200
+ fs.writeFileSync(
201
+ packagePath,
202
+ JSON.stringify(pkg, null, 2) + '\n'
203
+ );
204
+
205
+ // Regenerate lockfile during npm install
206
+ const lockPath = path.join(targetDir, 'package-lock.json');
207
+
208
+ if (fs.existsSync(lockPath)) {
209
+ fs.rmSync(lockPath);
210
+ }
211
+
212
+ console.log(pc.green('✔ Project created successfully!\n'));
213
+
214
+ console.log('Next steps:\n');
215
+ console.log(pc.cyan(` cd ${projectName}`));
216
+ console.log(pc.cyan(' npm install'));
217
+ console.log(pc.cyan(' npm run dev\n'));
218
+
219
+ } catch (error) {
220
+ console.error(pc.red(`Project creation failed: ${error.message}`));
221
+ process.exit(1);
222
+ }
@@ -0,0 +1,168 @@
1
+
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { unzipSync } from 'fflate';
6
+
7
+ const MAX_ZIP_SIZE = 20 * 1024 * 1024;
8
+ const MAX_FILE_SIZE = 30 * 1024 * 1024;
9
+ const MAX_TOTAL_SIZE = 100 * 1024 * 1024;
10
+ const MAX_FILES = 5000;
11
+
12
+ export async function downloadTemplate({
13
+ licenseKey,
14
+ targetDir,
15
+ projectName,
16
+ installationId = randomUUID(),
17
+ serverUrl = 'https://license.ankitdeveloper.site',
18
+ }) {
19
+ if (fs.existsSync(targetDir)) {
20
+ throw new Error('Target directory already exists.');
21
+ }
22
+
23
+ const url = new URL('/api/v1/templates/download', serverUrl);
24
+
25
+ // HTTP is allowed only for local development.
26
+ const isLocalhost = ['127.0.0.1', 'localhost', '::1']
27
+ .includes(url.hostname);
28
+
29
+ if (
30
+ url.protocol !== 'https:' &&
31
+ !(url.protocol === 'http:' && isLocalhost)
32
+ ) {
33
+ throw new Error('License server must use HTTPS.');
34
+ }
35
+
36
+ const response = await fetch(url, {
37
+ method: 'POST',
38
+ headers: {
39
+ 'Content-Type': 'application/json',
40
+ 'Accept': 'application/zip',
41
+ },
42
+ body: JSON.stringify({
43
+ license_key: licenseKey,
44
+ installation_id: installationId,
45
+ project_name: projectName,
46
+ }),
47
+ signal: AbortSignal.timeout(60000),
48
+ redirect: 'error',
49
+ });
50
+
51
+ if (!response.ok) {
52
+ if (response.status === 403) {
53
+ throw new Error('Invalid, expired or revoked license.');
54
+ }
55
+
56
+ throw new Error(
57
+ `Template download failed (HTTP ${response.status}).`
58
+ );
59
+ }
60
+
61
+ if (!response.body) {
62
+ throw new Error('Empty template response.');
63
+ }
64
+
65
+ const chunks = [];
66
+ let downloadedSize = 0;
67
+
68
+ for await (const chunk of response.body) {
69
+ downloadedSize += chunk.length;
70
+
71
+ if (downloadedSize > MAX_ZIP_SIZE) {
72
+ throw new Error('Template ZIP exceeds maximum size.');
73
+ }
74
+
75
+ chunks.push(chunk);
76
+ }
77
+
78
+ const zipBuffer = Buffer.concat(chunks);
79
+
80
+ // ZIP entries are checked before writing to disk.
81
+ const entries = unzipSync(
82
+ new Uint8Array(zipBuffer),
83
+ {
84
+ filter(file) {
85
+ if (file.originalSize > MAX_FILE_SIZE) {
86
+ throw new Error('Template contains an oversized file.');
87
+ }
88
+ return true;
89
+ },
90
+ }
91
+ );
92
+
93
+ const filenames = Object.keys(entries);
94
+
95
+ if (filenames.length > MAX_FILES) {
96
+ throw new Error('Template contains too many files.');
97
+ }
98
+
99
+ let totalSize = 0;
100
+
101
+ for (const [filename, data] of Object.entries(entries)) {
102
+ if (
103
+ !filename ||
104
+ filename.includes('\\') ||
105
+ filename.includes('\0') ||
106
+ filename.startsWith('/') ||
107
+ /^[a-zA-Z]:/.test(filename)
108
+ ) {
109
+ throw new Error('Unsafe ZIP entry detected.');
110
+ }
111
+
112
+ const segments = filename.split('/');
113
+
114
+ if (
115
+ segments.some(
116
+ (segment) => segment === '..' || segment === '.'
117
+ )
118
+ ) {
119
+ throw new Error('ZIP path traversal detected.');
120
+ }
121
+
122
+ totalSize += data.length;
123
+
124
+ if (
125
+ data.length > MAX_FILE_SIZE ||
126
+ totalSize > MAX_TOTAL_SIZE
127
+ ) {
128
+ throw new Error('Template exceeds extraction size limits.');
129
+ }
130
+ }
131
+
132
+ fs.mkdirSync(targetDir, { recursive: false });
133
+
134
+ try {
135
+ for (const [filename, data] of Object.entries(entries)) {
136
+ const outputPath = path.resolve(targetDir, filename);
137
+
138
+ if (
139
+ outputPath !== targetDir &&
140
+ !outputPath.startsWith(targetDir + path.sep)
141
+ ) {
142
+ throw new Error('Unsafe extraction path.');
143
+ }
144
+
145
+ if (filename.endsWith('/')) {
146
+ fs.mkdirSync(outputPath, { recursive: true });
147
+ continue;
148
+ }
149
+
150
+ fs.mkdirSync(path.dirname(outputPath), {
151
+ recursive: true,
152
+ });
153
+
154
+ fs.writeFileSync(outputPath, data, {
155
+ flag: 'wx',
156
+ });
157
+ }
158
+ } catch (error) {
159
+ fs.rmSync(targetDir, {
160
+ recursive: true,
161
+ force: true,
162
+ });
163
+
164
+ throw error;
165
+ }
166
+
167
+ return targetDir;
168
+ }
package/package.json CHANGED
@@ -1,6 +1,35 @@
1
1
  {
2
2
  "name": "@developerankitkumar/create-reactforge",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "main": "index.js",
5
+ "scripts": {
6
+ "test": "echo \"Error: no test specified\" && exit 1"
7
+ },
8
+ "keywords": [],
9
+ "author": "",
10
+ "license": "ISC",
11
+ "description": "ReactForge - React TypeScript Starter CLI with License Verification",
12
+ "dependencies": {
13
+ "fflate": "^0.8.3",
14
+ "picocolors": "^1.1.1",
15
+ "prompts": "^2.4.2"
16
+ },
17
+ "type": "module",
18
+ "bin": {
19
+ "create-reactforge": "./bin/index.js"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "files": [
25
+ "bin"
26
+ ],
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/developerankitkumar/reactforge-platform.git"
30
+ },
31
+ "homepage": "https://github.com/developerankitkumar/reactforge-platform#readme",
32
+ "bugs": {
33
+ "url": "https://github.com/developerankitkumar/reactforge-platform/issues"
34
+ }
35
+ }