create-firem 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/auto-update.yml +42 -0
- package/.github/workflows/npm-publish.yml +42 -0
- package/LICENSE +21 -0
- package/agents.md +32 -0
- package/bin/index.js +405 -0
- package/package.json +32 -0
- package/readme.md +90 -0
- package/scripts/auto-update-packages.js +110 -0
- package/specs/cli.md +83 -0
- package/specs/fireabse.md +66 -0
- package/specs/render.md +140 -0
- package/specs/template.md +170 -0
- package/templates/basic/package.json +32 -0
- package/templates/basic/src/App.jsx +28 -0
- package/templates/basic/src/main.jsx +15 -0
- package/templates/common/gitignore +27 -0
- package/templates/common/index.html +15 -0
- package/templates/common/src/components/AppRouter.jsx +23 -0
- package/templates/common/src/config/firebase.ts +30 -0
- package/templates/common/src/config/routerConfig.js +6 -0
- package/templates/common/src/theme/theme.js +14 -0
- package/templates/common/vite.config.js +28 -0
- package/templates/dashboard/package.json +32 -0
- package/templates/dashboard/src/App.jsx +50 -0
- package/templates/dashboard/src/components/Login.jsx +67 -0
- package/templates/dashboard/src/context/AuthContext.jsx +43 -0
- package/templates/dashboard/src/main.jsx +15 -0
- package/templates/dashboard/src/pages/DashboardOverview.jsx +21 -0
- package/templates/dashboard/src/pages/Profile.jsx +23 -0
- package/templates/landing/package.json +32 -0
- package/templates/landing/src/App.jsx +131 -0
- package/templates/landing/src/context/AuthContext.jsx +43 -0
- package/templates/landing/src/main.jsx +15 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Auto Update Template Packages
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
# Run every Monday at 15:00 UTC (9:00 AM UTC-6)
|
|
6
|
+
- cron: '0 15 * * 1'
|
|
7
|
+
workflow_dispatch: # Allow manual trigger
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
update-packages:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout Code
|
|
17
|
+
uses: actions/checkout@v6
|
|
18
|
+
|
|
19
|
+
- name: Setup Node.js
|
|
20
|
+
uses: actions/setup-node@v6
|
|
21
|
+
with:
|
|
22
|
+
node-version: 24
|
|
23
|
+
cache: 'npm'
|
|
24
|
+
|
|
25
|
+
- name: Install CLI Dependencies
|
|
26
|
+
run: npm ci || npm install
|
|
27
|
+
|
|
28
|
+
- name: Run Update Script
|
|
29
|
+
run: node scripts/auto-update-packages.js
|
|
30
|
+
|
|
31
|
+
- name: Commit and Push Changes
|
|
32
|
+
run: |
|
|
33
|
+
git config --global user.name "github-actions[bot]"
|
|
34
|
+
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
35
|
+
git add .
|
|
36
|
+
# Check if there are staged changes to commit
|
|
37
|
+
if ! git diff-index --quiet HEAD; then
|
|
38
|
+
git commit -m "chore: auto-update template dependencies and bump version"
|
|
39
|
+
git push origin HEAD
|
|
40
|
+
else
|
|
41
|
+
echo "No updates found. Repository is already up-to-date."
|
|
42
|
+
fi
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
- master
|
|
8
|
+
workflow_dispatch: # Allow manual trigger
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
publish:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout Code
|
|
18
|
+
uses: actions/checkout@v6
|
|
19
|
+
|
|
20
|
+
- name: Setup Node.js
|
|
21
|
+
uses: actions/setup-node@v6
|
|
22
|
+
with:
|
|
23
|
+
node-version: 24
|
|
24
|
+
registry-url: 'https://registry.npmjs.org'
|
|
25
|
+
|
|
26
|
+
- name: Install CLI Dependencies
|
|
27
|
+
run: npm ci || npm install
|
|
28
|
+
|
|
29
|
+
- name: Check and Publish to npm
|
|
30
|
+
run: |
|
|
31
|
+
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
|
32
|
+
PACKAGE_NAME=$(node -p "require('./package.json').name")
|
|
33
|
+
|
|
34
|
+
# Check if current version already exists on npm
|
|
35
|
+
if npm view "$PACKAGE_NAME@$CURRENT_VERSION" version >/dev/null 2>&1; then
|
|
36
|
+
echo "Version $CURRENT_VERSION of $PACKAGE_NAME is already published on npm. Skipping publication."
|
|
37
|
+
else
|
|
38
|
+
echo "Publishing version $CURRENT_VERSION of $PACKAGE_NAME to npm..."
|
|
39
|
+
npm publish
|
|
40
|
+
fi
|
|
41
|
+
env:
|
|
42
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wayproyect
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/agents.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Technical Document for Agents (agents.md)
|
|
2
|
+
|
|
3
|
+
## Repository Purpose
|
|
4
|
+
The primary objective of this repository is to build and maintain the `create-firem` project generator. This tool is designed to be executed via standard Node package managers to quickly scaffold a new project template for users.
|
|
5
|
+
|
|
6
|
+
## Invocation Commands
|
|
7
|
+
The finished CLI tool will be invoked by end-users using either of the following commands:
|
|
8
|
+
- `npx create-firem`
|
|
9
|
+
- `npm init firem`
|
|
10
|
+
|
|
11
|
+
## Architecture & Structure
|
|
12
|
+
As an agent working on this repository, please adhere to the following architectural guidelines for building a modern CLI generator:
|
|
13
|
+
|
|
14
|
+
1. **CLI Entry Point**: The project must define a main executable script. This is typically configured in the `bin` field of the `package.json` file.
|
|
15
|
+
2. **Template Directory**: A dedicated directory (e.g., `template/` or `templates/`) should exist containing the base boilerplate code that will be copied to the target user directory.
|
|
16
|
+
3. **Interactivity**: The CLI should optionally prompt the user for relevant project details (e.g., project name, features to include, language preference) using tools like `inquirer` or `prompts`.
|
|
17
|
+
4. **Scaffolding Logic**: The tool should handle copying files, correctly renaming dotfiles (such as renaming a placeholder like `gitignore` to `.gitignore`, as npm strips `.gitignore` during publish), and modifying `package.json` placeholders based on user input.
|
|
18
|
+
5. **Dependencies**: Upon copying the template, the CLI should optionally install the required dependencies automatically or prompt the user if they'd like to install them.
|
|
19
|
+
|
|
20
|
+
## AI Agent Instructions
|
|
21
|
+
- **Code Style**: Maintain clean, modular JavaScript/TypeScript code.
|
|
22
|
+
- **Dependencies**: Keep dependencies to a minimum to ensure fast and reliable execution. Prefer built-in Node.js modules like `fs`, `path`, and `child_process` when possible.
|
|
23
|
+
- **Testing**: Ensure any scaffolding logic is thoroughly tested to prevent broken templates for the end-user.
|
|
24
|
+
- **Documentation**: Update `README.md` with clear instructions on how users can leverage the generated project and how contributors can maintain and update the template generator.
|
|
25
|
+
|
|
26
|
+
## Current State
|
|
27
|
+
The project generator CLI and base templates are fully functional:
|
|
28
|
+
- **CLI Scaffold Logic**: Completed (`bin/index.js`), with full support for interactive scaffolding and command flags (`--template`, `--layout`, `--auth`, `--install`). Includes layout flex direction adjustment for sidebar menus.
|
|
29
|
+
- **Templates**: Completed `basic`, `landing`, and `dashboard` templates under `templates/` with `react-router-dom` pre-configured.
|
|
30
|
+
- **SPA & Modular Rendering**: Pure Single Page Application (SPA) architecture implemented. Vite config includes advanced code splitting via Rollup manual chunks (`vendor-mui`, `vendor-firebase`, `vendor-core`, etc.) to keep initial bundle sizes lightweight.
|
|
31
|
+
- **Flexible Routing**: Pre-configured `AppRouter.jsx` wrapper reading from `routerConfig.js` to dynamically toggle between `'browser'` (BrowserRouter) and `'hash'` (HashRouter) routing modes.
|
|
32
|
+
- **Development Setup**: Root dependencies updated to include development packages (like `firebase`) to prevent editor resolution errors.
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import prompts from 'prompts';
|
|
5
|
+
import { blue, green, red, yellow, bold, cyan } from 'kolorist';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
|
|
15
|
+
const program = new Command();
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.name('create-firem')
|
|
19
|
+
.description('Scaffold a new React + Firebase + Material-UI project')
|
|
20
|
+
.argument('[project-name]', 'Name of the project')
|
|
21
|
+
.option('-t, --template <type>', 'Template type: dashboard, landing, basic')
|
|
22
|
+
.option('-l, --layout <position>', 'Navigation layout: top, bottom, sidebar, none')
|
|
23
|
+
.option('-a, --auth <boolean>', 'Include authentication (for landing template)')
|
|
24
|
+
.option('--install', 'Automatically install dependencies')
|
|
25
|
+
.parse(process.argv);
|
|
26
|
+
|
|
27
|
+
async function main() {
|
|
28
|
+
console.log(cyan(bold('\n🔥 Welcome to create-firem! Scaffold React + Firebase + MUI in seconds.\n')));
|
|
29
|
+
|
|
30
|
+
const args = program.args;
|
|
31
|
+
const options = program.opts();
|
|
32
|
+
|
|
33
|
+
let targetDir = args[0];
|
|
34
|
+
let template = options.template;
|
|
35
|
+
let layout = options.layout;
|
|
36
|
+
let includeAuth = options.auth !== undefined ? options.auth === 'true' : null;
|
|
37
|
+
|
|
38
|
+
// Prompt for project name if not provided
|
|
39
|
+
if (!targetDir) {
|
|
40
|
+
const response = await prompts({
|
|
41
|
+
type: 'text',
|
|
42
|
+
name: 'projectName',
|
|
43
|
+
message: 'What is the name of your project?',
|
|
44
|
+
initial: 'my-firem-app',
|
|
45
|
+
validate: (value) => {
|
|
46
|
+
const pattern = /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
47
|
+
return pattern.test(value) ? true : 'Invalid package.json name';
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!response.projectName) {
|
|
52
|
+
console.log(yellow('\nOperation cancelled.'));
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
targetDir = response.projectName;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Prompt for template type if not provided
|
|
59
|
+
if (!template) {
|
|
60
|
+
const response = await prompts({
|
|
61
|
+
type: 'select',
|
|
62
|
+
name: 'template',
|
|
63
|
+
message: 'Select a template type:',
|
|
64
|
+
choices: [
|
|
65
|
+
{ title: '📊 Dashboard (Firebase Auth, Persistent Login, Admin View)', value: 'dashboard' },
|
|
66
|
+
{ title: '🚀 Landing Page (Direct load, Optional Auth)', value: 'landing' },
|
|
67
|
+
{ title: '📦 Basic (Minimal skeleton)', value: 'basic' }
|
|
68
|
+
]
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!response.template) {
|
|
72
|
+
console.log(yellow('\nOperation cancelled.'));
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
template = response.template;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Prompt for auth conditionally (Landing template only)
|
|
79
|
+
if (template === 'landing' && includeAuth === null) {
|
|
80
|
+
const response = await prompts({
|
|
81
|
+
type: 'confirm',
|
|
82
|
+
name: 'includeAuth',
|
|
83
|
+
message: 'Do you want to include Firebase Authentication in your Landing Page?',
|
|
84
|
+
initial: false
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
includeAuth = response.includeAuth;
|
|
88
|
+
} else if (template === 'dashboard') {
|
|
89
|
+
includeAuth = true; // Dashboard always includes auth
|
|
90
|
+
} else if (template === 'basic') {
|
|
91
|
+
includeAuth = false; // Basic is minimal
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Prompt for navigation layout position if not provided
|
|
95
|
+
if (!layout) {
|
|
96
|
+
const response = await prompts({
|
|
97
|
+
type: 'select',
|
|
98
|
+
name: 'layout',
|
|
99
|
+
message: 'Where should the main navigation menu be positioned?',
|
|
100
|
+
choices: [
|
|
101
|
+
{ title: '⬆️ Top (Superior)', value: 'top' },
|
|
102
|
+
{ title: '⬇️ Bottom (Inferior - Mobile first)', value: 'bottom' },
|
|
103
|
+
{ title: '⬅️ Sidebar (Lateral)', value: 'sidebar' },
|
|
104
|
+
{ title: '❌ None (Sin menú)', value: 'none' }
|
|
105
|
+
]
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (!response.layout) {
|
|
109
|
+
console.log(yellow('\nOperation cancelled.'));
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
|
112
|
+
layout = response.layout;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const rootPath = path.resolve(targetDir);
|
|
116
|
+
|
|
117
|
+
if (fs.existsSync(rootPath)) {
|
|
118
|
+
const overwriteResponse = await prompts({
|
|
119
|
+
type: 'confirm',
|
|
120
|
+
name: 'overwrite',
|
|
121
|
+
message: `Directory '${targetDir}' already exists. Overwrite?`,
|
|
122
|
+
initial: false
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (!overwriteResponse.overwrite) {
|
|
126
|
+
console.log(red('\nAborted.'));
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
fs.rmSync(rootPath, { recursive: true, force: true });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fs.mkdirSync(rootPath, { recursive: true });
|
|
133
|
+
|
|
134
|
+
const spinner = ora('Scaffolding project...').start();
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const templatesDir = path.join(__dirname, '../templates');
|
|
138
|
+
|
|
139
|
+
// Copy common templates
|
|
140
|
+
copyDir(path.join(templatesDir, 'common'), rootPath);
|
|
141
|
+
|
|
142
|
+
// Copy template-specific files
|
|
143
|
+
copyDir(path.join(templatesDir, template), rootPath);
|
|
144
|
+
|
|
145
|
+
// Dynamic Navigation template setup based on layout selection
|
|
146
|
+
setupNavigation(rootPath, layout);
|
|
147
|
+
|
|
148
|
+
// Dynamic Auth setup
|
|
149
|
+
setupAuth(rootPath, includeAuth, template);
|
|
150
|
+
|
|
151
|
+
// Configure layout flex direction in App.jsx if layout is sidebar
|
|
152
|
+
const appPath = path.join(rootPath, 'src/App.jsx');
|
|
153
|
+
if (fs.existsSync(appPath) && layout === 'sidebar') {
|
|
154
|
+
let content = fs.readFileSync(appPath, 'utf8');
|
|
155
|
+
content = content.replace(/flexDirection:\s*'column'/g, "flexDirection: 'row'");
|
|
156
|
+
fs.writeFileSync(appPath, content);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Setup package.json name and other details
|
|
160
|
+
const pkgPath = path.join(rootPath, 'package.json');
|
|
161
|
+
if (fs.existsSync(pkgPath)) {
|
|
162
|
+
let pkgContent = fs.readFileSync(pkgPath, 'utf8');
|
|
163
|
+
pkgContent = pkgContent.replace(/{{PROJECT_NAME}}/g, path.basename(rootPath));
|
|
164
|
+
fs.writeFileSync(pkgPath, pkgContent);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Setup index.html title and details
|
|
168
|
+
const indexPath = path.join(rootPath, 'index.html');
|
|
169
|
+
if (fs.existsSync(indexPath)) {
|
|
170
|
+
let indexContent = fs.readFileSync(indexPath, 'utf8');
|
|
171
|
+
indexContent = indexContent.replace(/{{PROJECT_NAME}}/g, path.basename(rootPath));
|
|
172
|
+
fs.writeFileSync(indexPath, indexContent);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Rename gitignore to .gitignore
|
|
176
|
+
const gitignorePath = path.join(rootPath, 'gitignore');
|
|
177
|
+
if (fs.existsSync(gitignorePath)) {
|
|
178
|
+
fs.renameSync(gitignorePath, path.join(rootPath, '.gitignore'));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
spinner.succeed(green('Project scaffolded successfully!'));
|
|
183
|
+
|
|
184
|
+
let autoInstall = options.install;
|
|
185
|
+
if (!autoInstall) {
|
|
186
|
+
const installResponse = await prompts({
|
|
187
|
+
type: 'confirm',
|
|
188
|
+
name: 'install',
|
|
189
|
+
message: 'Do you want to run "npm install" now?',
|
|
190
|
+
initial: true
|
|
191
|
+
});
|
|
192
|
+
autoInstall = installResponse.install;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (autoInstall) {
|
|
196
|
+
const installSpinner = ora('Installing dependencies (this might take a minute)...').start();
|
|
197
|
+
try {
|
|
198
|
+
execSync('npm install', { cwd: rootPath, stdio: 'ignore' });
|
|
199
|
+
installSpinner.succeed(green('Dependencies installed successfully!'));
|
|
200
|
+
} catch (err) {
|
|
201
|
+
installSpinner.fail(red('Failed to install dependencies. You can run "npm install" manually.'));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log(cyan('\nDone! To get started:'));
|
|
206
|
+
console.log(cyan(` cd ${targetDir}`));
|
|
207
|
+
if (!autoInstall) {
|
|
208
|
+
console.log(cyan(' npm install'));
|
|
209
|
+
}
|
|
210
|
+
console.log(cyan(' npm run dev\n'));
|
|
211
|
+
|
|
212
|
+
} catch (error) {
|
|
213
|
+
spinner.fail(red('Error scaffolding project.'));
|
|
214
|
+
console.error(error);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function copyDir(src, dest) {
|
|
219
|
+
if (!fs.existsSync(src)) return;
|
|
220
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
221
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
222
|
+
|
|
223
|
+
for (const entry of entries) {
|
|
224
|
+
const srcPath = path.join(src, entry.name);
|
|
225
|
+
const destPath = path.join(dest, entry.name);
|
|
226
|
+
|
|
227
|
+
if (entry.isDirectory()) {
|
|
228
|
+
copyDir(srcPath, destPath);
|
|
229
|
+
} else {
|
|
230
|
+
fs.copyFileSync(srcPath, destPath);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function setupNavigation(rootPath, layout) {
|
|
236
|
+
const navComponentPath = path.join(rootPath, 'src/components/Navigation.jsx');
|
|
237
|
+
|
|
238
|
+
// Custom navigation content based on selection
|
|
239
|
+
let navContent = '';
|
|
240
|
+
|
|
241
|
+
if (layout === 'top') {
|
|
242
|
+
navContent = `import React from 'react';
|
|
243
|
+
import { AppBar, Toolbar, Typography, Button, Box } from '@mui/material';
|
|
244
|
+
import { useNavigate } from 'react-router-dom';
|
|
245
|
+
|
|
246
|
+
export default function Navigation({ onLogout, isAuthenticated }) {
|
|
247
|
+
const navigate = useNavigate();
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<AppBar position="static">
|
|
251
|
+
<Toolbar>
|
|
252
|
+
<Typography
|
|
253
|
+
variant="h6"
|
|
254
|
+
component="div"
|
|
255
|
+
sx={{ flexGrow: 1, cursor: 'pointer' }}
|
|
256
|
+
onClick={() => navigate('/')}
|
|
257
|
+
>
|
|
258
|
+
Firem App
|
|
259
|
+
</Typography>
|
|
260
|
+
{isAuthenticated && (
|
|
261
|
+
<Box sx={{ display: 'flex', gap: 1 }}>
|
|
262
|
+
<Button color="inherit" onClick={() => navigate('/')}>Home</Button>
|
|
263
|
+
<Button color="inherit" onClick={() => navigate('/profile')}>Profile</Button>
|
|
264
|
+
<Button color="inherit" onClick={onLogout}>Logout</Button>
|
|
265
|
+
</Box>
|
|
266
|
+
)}
|
|
267
|
+
</Toolbar>
|
|
268
|
+
</AppBar>
|
|
269
|
+
);
|
|
270
|
+
}`;
|
|
271
|
+
} else if (layout === 'bottom') {
|
|
272
|
+
navContent = `import React from 'react';
|
|
273
|
+
import { Paper, BottomNavigation, BottomNavigationAction } from '@mui/material';
|
|
274
|
+
import HomeIcon from '@mui/icons-material/Home';
|
|
275
|
+
import PersonIcon from '@mui/icons-material/Person';
|
|
276
|
+
import ExitToAppIcon from '@mui/icons-material/ExitToApp';
|
|
277
|
+
import { useNavigate, useLocation } from 'react-router-dom';
|
|
278
|
+
|
|
279
|
+
export default function Navigation({ onLogout, isAuthenticated }) {
|
|
280
|
+
const navigate = useNavigate();
|
|
281
|
+
const location = useLocation();
|
|
282
|
+
|
|
283
|
+
const getIndexFromPath = (path) => {
|
|
284
|
+
if (path === '/profile') return 1;
|
|
285
|
+
return 0;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const value = getIndexFromPath(location.pathname);
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<Paper sx={{ position: 'fixed', bottom: 0, left: 0, right: 0 }} elevation={3}>
|
|
292
|
+
<BottomNavigation
|
|
293
|
+
showLabels
|
|
294
|
+
value={value}
|
|
295
|
+
onChange={(event, newValue) => {
|
|
296
|
+
if (newValue === 0) {
|
|
297
|
+
navigate('/');
|
|
298
|
+
} else if (newValue === 1) {
|
|
299
|
+
navigate('/profile');
|
|
300
|
+
} else if (newValue === 2 && isAuthenticated) {
|
|
301
|
+
onLogout();
|
|
302
|
+
}
|
|
303
|
+
}}
|
|
304
|
+
>
|
|
305
|
+
<BottomNavigationAction label="Home" icon={<HomeIcon />} />
|
|
306
|
+
<BottomNavigationAction label="Profile" icon={<PersonIcon />} />
|
|
307
|
+
{isAuthenticated && <BottomNavigationAction label="Logout" icon={<ExitToAppIcon />} />}
|
|
308
|
+
</BottomNavigation>
|
|
309
|
+
</Paper>
|
|
310
|
+
);
|
|
311
|
+
}`;
|
|
312
|
+
} else if (layout === 'sidebar') {
|
|
313
|
+
navContent = `import React from 'react';
|
|
314
|
+
import { Drawer, List, ListItem, ListItemIcon, ListItemText, Toolbar, Divider, Box } from '@mui/material';
|
|
315
|
+
import HomeIcon from '@mui/icons-material/Home';
|
|
316
|
+
import PersonIcon from '@mui/icons-material/Person';
|
|
317
|
+
import ExitToAppIcon from '@mui/icons-material/ExitToApp';
|
|
318
|
+
import { useNavigate } from 'react-router-dom';
|
|
319
|
+
|
|
320
|
+
const drawerWidth = 240;
|
|
321
|
+
|
|
322
|
+
export default function Navigation({ onLogout, isAuthenticated }) {
|
|
323
|
+
const navigate = useNavigate();
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<Drawer
|
|
327
|
+
variant="permanent"
|
|
328
|
+
sx={{
|
|
329
|
+
width: drawerWidth,
|
|
330
|
+
flexShrink: 0,
|
|
331
|
+
\`& .MuiDrawer-paper\`: { width: drawerWidth, boxSizing: 'border-box' },
|
|
332
|
+
}}
|
|
333
|
+
>
|
|
334
|
+
<Toolbar>
|
|
335
|
+
<Box fontWeight="bold" sx={{ cursor: 'pointer' }} onClick={() => navigate('/')}>
|
|
336
|
+
Firem App
|
|
337
|
+
</Box>
|
|
338
|
+
</Toolbar>
|
|
339
|
+
<Divider />
|
|
340
|
+
<List>
|
|
341
|
+
<ListItem button onClick={() => navigate('/')}>
|
|
342
|
+
<ListItemIcon><HomeIcon /></ListItemIcon>
|
|
343
|
+
<ListItemText primary="Home" />
|
|
344
|
+
</ListItem>
|
|
345
|
+
<ListItem button onClick={() => navigate('/profile')}>
|
|
346
|
+
<ListItemIcon><PersonIcon /></ListItemIcon>
|
|
347
|
+
<ListItemText primary="Profile" />
|
|
348
|
+
</ListItem>
|
|
349
|
+
{isAuthenticated && (
|
|
350
|
+
<ListItem button onClick={onLogout}>
|
|
351
|
+
<ListItemIcon><ExitToAppIcon /></ListItemIcon>
|
|
352
|
+
<ListItemText primary="Logout" />
|
|
353
|
+
</ListItem>
|
|
354
|
+
)}
|
|
355
|
+
</List>
|
|
356
|
+
</Drawer>
|
|
357
|
+
);
|
|
358
|
+
}`;
|
|
359
|
+
} else if (layout === 'none') {
|
|
360
|
+
navContent = `import React from 'react';
|
|
361
|
+
|
|
362
|
+
export default function Navigation() {
|
|
363
|
+
return null;
|
|
364
|
+
}`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Create src/components if not exists
|
|
368
|
+
fs.mkdirSync(path.dirname(navComponentPath), { recursive: true });
|
|
369
|
+
fs.writeFileSync(navComponentPath, navContent);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function setupAuth(rootPath, includeAuth, template) {
|
|
373
|
+
if (template === 'landing') {
|
|
374
|
+
const appPath = path.join(rootPath, 'src/App.jsx');
|
|
375
|
+
if (fs.existsSync(appPath)) {
|
|
376
|
+
let content = fs.readFileSync(appPath, 'utf8');
|
|
377
|
+
if (!includeAuth) {
|
|
378
|
+
// Remove auth features from app
|
|
379
|
+
content = content.replace(/\/\* AUTH_START \*\/([\s\S]*?)\/\* AUTH_END \*\//g, '');
|
|
380
|
+
// Enable AUTH_NO block
|
|
381
|
+
content = content.replace(/\/\* AUTH_NO_START \*\/([\s\S]*?)\/\* AUTH_NO_END \*\//g, (match, p1) => {
|
|
382
|
+
return p1.replace(/\/\/\s?/g, '');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// Remove context directory since auth isn't needed
|
|
386
|
+
const authCtxPath = path.join(rootPath, 'src/context');
|
|
387
|
+
if (fs.existsSync(authCtxPath)) {
|
|
388
|
+
fs.rmSync(authCtxPath, { recursive: true, force: true });
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
// Keep them, clean up tags
|
|
392
|
+
content = content.replace(/\/\* AUTH_START \*\//g, '');
|
|
393
|
+
content = content.replace(/\/\* AUTH_END \*\//g, '');
|
|
394
|
+
// Remove AUTH_NO block
|
|
395
|
+
content = content.replace(/\/\* AUTH_NO_START \*\/([\s\S]*?)\/\* AUTH_NO_END \*\//g, '');
|
|
396
|
+
}
|
|
397
|
+
fs.writeFileSync(appPath, content);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
main().catch((err) => {
|
|
403
|
+
console.error(err);
|
|
404
|
+
process.exit(1);
|
|
405
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-firem",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Scaffold a new React + Firebase + Material-UI project",
|
|
5
|
+
"main": "bin/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-firem": "./bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node bin/index.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"cli",
|
|
15
|
+
"generator",
|
|
16
|
+
"scaffold",
|
|
17
|
+
"react",
|
|
18
|
+
"firebase",
|
|
19
|
+
"mui"
|
|
20
|
+
],
|
|
21
|
+
"author": "",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"commander": "^12.0.0",
|
|
25
|
+
"kolorist": "^1.8.0",
|
|
26
|
+
"ora": "^8.0.1",
|
|
27
|
+
"prompts": "^2.4.2"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"firebase": "^12.13.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# create-firem
|
|
2
|
+
|
|
3
|
+
A modern CLI tool for quickly scaffolding new Firem project templates.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
You can generate a new project interactively by running one of the following commands in your terminal:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx create-firem
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or, using npm init:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm init firem
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
*(The CLI will prompt you for project details, features to include, and automatically set up the boilerplate code.)*
|
|
20
|
+
|
|
21
|
+
## Development & Testing
|
|
22
|
+
|
|
23
|
+
If you want to contribute to this generator or test it locally, follow these steps:
|
|
24
|
+
|
|
25
|
+
1. Clone this repository.
|
|
26
|
+
2. Install the dependencies of the CLI itself:
|
|
27
|
+
```bash
|
|
28
|
+
npm install
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Testing the Scaffolding & Templates Locally
|
|
32
|
+
|
|
33
|
+
You can test the scaffolding process using two methods:
|
|
34
|
+
|
|
35
|
+
#### Method A: Direct Execution (Recommended for quick iteration)
|
|
36
|
+
Run the generator script directly from the root of the repository:
|
|
37
|
+
```bash
|
|
38
|
+
node bin/index.js <test-project-name> [options]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Example commands to test different configurations:
|
|
42
|
+
- **Dashboard with Sidebar Layout**:
|
|
43
|
+
```bash
|
|
44
|
+
node bin/index.js test-dashboard --template dashboard --layout sidebar
|
|
45
|
+
```
|
|
46
|
+
- **Landing Page with Top Nav & Auth**:
|
|
47
|
+
```bash
|
|
48
|
+
node bin/index.js test-landing --template landing --layout top --auth true
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
#### Method B: Global Symlink
|
|
52
|
+
1. Link the package globally:
|
|
53
|
+
```bash
|
|
54
|
+
npm link
|
|
55
|
+
```
|
|
56
|
+
2. Run `create-firem` anywhere on your machine to test the scaffolding:
|
|
57
|
+
```bash
|
|
58
|
+
create-firem my-scaffolded-app
|
|
59
|
+
```
|
|
60
|
+
3. To clean up and unlink when done:
|
|
61
|
+
```bash
|
|
62
|
+
npm unlink -g create-firem
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Verifying the Generated Template
|
|
66
|
+
|
|
67
|
+
Once a template is generated, enter its directory and run:
|
|
68
|
+
```bash
|
|
69
|
+
cd <generated-project-name>
|
|
70
|
+
npm install
|
|
71
|
+
npm run dev
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Key aspects to verify:
|
|
75
|
+
1. **Routing Modes**: Open `src/config/routerConfig.js` and test toggling `ROUTER_MODE` between `'browser'` (BrowserRouter) and `'hash'` (HashRouter).
|
|
76
|
+
2. **Vite Chunk Splitting**: Run `npm run build` to verify that assets are split modularly into `vendor-mui`, `vendor-firebase`, `vendor-core`, etc., keeping the initial bundle sizes lightweight.
|
|
77
|
+
3. **Navigation Layouts**: Change navigation layout options (`top`, `bottom`, `sidebar`) during scaffolding to ensure they render and respond properly.
|
|
78
|
+
|
|
79
|
+
## Architecture
|
|
80
|
+
|
|
81
|
+
- **`bin/index.js`**: Contains the executable CLI entry point and scaffolding code (copying files, prompting users, rewriting package placeholders, and adjusting layouts).
|
|
82
|
+
- **`templates/`**: Contains the boilerplate files for the generated projects:
|
|
83
|
+
- `common/`: Core configuration files (such as `firebase.ts`, MUI theme, and `AppRouter.jsx`) shared by all templates.
|
|
84
|
+
- `basic/`: Minimal skeleton template.
|
|
85
|
+
- `landing/`: Public-facing landing page layout.
|
|
86
|
+
- `dashboard/`: Full persistent login administration panel.
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|