@asynx/create-asynx-next-app 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/CONTRIBUTING.md +104 -0
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/dist/cli.js +56 -0
- package/dist/commands/create.js +32 -0
- package/dist/constants/template.js +17 -0
- package/dist/index.js +3 -0
- package/dist/utils/copy.js +16 -0
- package/dist/utils/exec.js +16 -0
- package/dist/utils/install.js +14 -0
- package/dist/utils/logger.js +7 -0
- package/dist/utils/package-manager.js +15 -0
- package/dist/utils/package.js +8 -0
- package/dist/utils/path.js +2 -0
- package/package.json +45 -0
- package/src/cli.ts +74 -0
- package/src/commands/create.ts +39 -0
- package/src/constants/template.ts +19 -0
- package/src/index.ts +5 -0
- package/src/utils/copy.ts +19 -0
- package/src/utils/exec.ts +20 -0
- package/src/utils/install.ts +18 -0
- package/src/utils/logger.ts +8 -0
- package/src/utils/package-manager.ts +17 -0
- package/src/utils/package.ts +14 -0
- package/src/utils/path.ts +4 -0
- package/templates/moderate/next-env.d.ts +4 -0
- package/templates/moderate/package.json +20 -0
- package/templates/moderate/src/app/globals.css +8 -0
- package/templates/moderate/src/app/layout.tsx +18 -0
- package/templates/moderate/src/app/page.tsx +8 -0
- package/templates/moderate/tsconfig.json +20 -0
- package/templates/moderate/utils/async.ts +3 -0
- package/templates/moderate/utils/date.ts +17 -0
- package/templates/moderate/utils/index.ts +4 -0
- package/templates/moderate/utils/number.ts +6 -0
- package/templates/moderate/utils/string.ts +7 -0
- package/templates/saas/next-env.d.ts +4 -0
- package/templates/saas/package.json +20 -0
- package/templates/saas/src/app/globals.css +8 -0
- package/templates/saas/src/app/layout.tsx +18 -0
- package/templates/saas/src/app/page.tsx +8 -0
- package/templates/saas/tsconfig.json +20 -0
- package/templates/saas/utils/async.ts +3 -0
- package/templates/saas/utils/date.ts +17 -0
- package/templates/saas/utils/index.ts +4 -0
- package/templates/saas/utils/number.ts +6 -0
- package/templates/saas/utils/string.ts +7 -0
- package/templates/simple/next-env.d.ts +4 -0
- package/templates/simple/package.json +20 -0
- package/templates/simple/src/app/globals.css +8 -0
- package/templates/simple/src/app/layout.tsx +18 -0
- package/templates/simple/src/app/page.tsx +8 -0
- package/templates/simple/tsconfig.json +20 -0
- package/templates/simple/utils/async.ts +3 -0
- package/templates/simple/utils/date.ts +17 -0
- package/templates/simple/utils/index.ts +4 -0
- package/templates/simple/utils/number.ts +6 -0
- package/templates/simple/utils/string.ts +7 -0
- package/tsconfig.json +13 -0
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Contributing to create-asynx-next-app
|
|
2
|
+
|
|
3
|
+
Thank you for considering contributing ❤️ We appreciate your help in making this project better.
|
|
4
|
+
|
|
5
|
+
We welcome contributions in the form of:
|
|
6
|
+
|
|
7
|
+
* 🐛 Bug fixes
|
|
8
|
+
* ✨ Template improvements
|
|
9
|
+
* 🆕 New templates
|
|
10
|
+
* 📚 Documentation improvements
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 🛠 Development Setup
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
To get started with development, clone the repository and install dependencies:
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
git clone [https://github.com/Asynx-Pvt-Ltd/create-asynx-next-app.git](https://github.com/Asynx-Pvt-Ltd/create-asynx-next-app.git)
|
|
22
|
+
cd create-asynx-next-app
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pnpm install
|
|
27
|
+
pnpm dev
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 📦 Templates
|
|
31
|
+
The core project templates live in the templates/ directory:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
templates/
|
|
35
|
+
├─ simple/
|
|
36
|
+
├─ moderate/
|
|
37
|
+
└─ saas/
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Each template must adhere to the following rules:
|
|
41
|
+
|
|
42
|
+
- Be a valid Next.js App Router project.
|
|
43
|
+
- Use TypeScript.
|
|
44
|
+
- Avoid external runtime dependencies on Asynx packages.
|
|
45
|
+
|
|
46
|
+
## 🧪 Testing changes
|
|
47
|
+
Before opening a Pull Request, please ensure your changes work by building the CLI and testing it locally:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pnpm build
|
|
51
|
+
node dist/index.js test-app
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Verify the generated application in the test-app directory runs correctly.
|
|
55
|
+
|
|
56
|
+
## 📌 Guidelines
|
|
57
|
+
When writing code, please follow these principles:
|
|
58
|
+
|
|
59
|
+
- Keep code readable.
|
|
60
|
+
- Prefer clarity over cleverness.
|
|
61
|
+
- Avoid unnecessary abstractions.
|
|
62
|
+
|
|
63
|
+
Follow the existing structure of the codebase.
|
|
64
|
+
|
|
65
|
+
## 📬 Pull Requests
|
|
66
|
+
When submitting a Pull Request (PR):
|
|
67
|
+
|
|
68
|
+
- Describe what you changed.
|
|
69
|
+
- Explain why it’s useful.
|
|
70
|
+
- Keep PRs focused on a single feature or fix.
|
|
71
|
+
|
|
72
|
+
Thanks for helping improve the project 🚀
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
## `CODE_OF_CONDUCT.md`
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
# Code of Conduct
|
|
80
|
+
|
|
81
|
+
This project is governed by a standard open-source code of conduct. We are committed to fostering a welcoming and inclusive environment.
|
|
82
|
+
|
|
83
|
+
We expect all contributors to adhere to the following guidelines:
|
|
84
|
+
|
|
85
|
+
* Be **respectful** of differing viewpoints and experiences.
|
|
86
|
+
* Be **inclusive** and encourage participation from everyone.
|
|
87
|
+
* Be **constructive** in all communication and feedback.
|
|
88
|
+
|
|
89
|
+
Harassment, discrimination, or abusive behavior will not be tolerated.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Enforcement
|
|
94
|
+
|
|
95
|
+
Project maintainers reserve the right to review, edit, remove, or block comments, commits, code, and contributors who violate this code of conduct.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Contact
|
|
100
|
+
|
|
101
|
+
If you experience or witness unacceptable behavior, or have any other concerns, please contact:
|
|
102
|
+
|
|
103
|
+
Asynx Devs Pvt Ltd
|
|
104
|
+
🌐 **Website:** https://asynx.in
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Asynx Devs Pvt Ltd
|
|
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/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# create-asynx-next-app
|
|
2
|
+
|
|
3
|
+
> Opinionated Next.js App Router scaffolder by **Asynx Devs Pvt Ltd**
|
|
4
|
+
|
|
5
|
+
A modern, open-source CLI to scaffold **scalable Next.js applications** using production-ready templates — from simple landing pages to complex SaaS apps.
|
|
6
|
+
|
|
7
|
+
-----
|
|
8
|
+
|
|
9
|
+
## ✨ Features
|
|
10
|
+
|
|
11
|
+
* ✅ **Next.js (latest)** — App Router only
|
|
12
|
+
* ✅ **TypeScript by default**
|
|
13
|
+
* ✅ **Multiple templates**
|
|
14
|
+
* Simple (Landing pages, MVPs)
|
|
15
|
+
* Moderate (Dashboards, content platforms)
|
|
16
|
+
* SaaS (Scalable, domain-driven architecture)
|
|
17
|
+
* ✅ **Predefined utilities included**
|
|
18
|
+
* ✅ **No vendor lock-in**
|
|
19
|
+
* ✅ **`pnpm` / `npm` / `npx` supported**
|
|
20
|
+
* ✅ **Cross-platform (Windows, macOS, Linux)**
|
|
21
|
+
|
|
22
|
+
-----
|
|
23
|
+
|
|
24
|
+
## 🚀 Usage
|
|
25
|
+
|
|
26
|
+
### Using pnpm (recommended)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pnpm create asynx-next-app my-app
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Using npx
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx create-asynx-next-app my-app
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
-----
|
|
39
|
+
|
|
40
|
+
## 🧩 Templates
|
|
41
|
+
|
|
42
|
+
| Template | Best for: |
|
|
43
|
+
| :--- | :--- |
|
|
44
|
+
| **Simple App** | Landing pages, Small tools, MVPs |
|
|
45
|
+
| **Moderate App** | Dashboards, Content platforms, Early-stage startups |
|
|
46
|
+
| **SaaS / Complex App** | SaaS products, Multi-tenant systems, Role-based platforms |
|
|
47
|
+
|
|
48
|
+
-----
|
|
49
|
+
|
|
50
|
+
## 📁 Project Structure
|
|
51
|
+
|
|
52
|
+
All generated projects follow this structure:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
src/
|
|
56
|
+
├─ app/
|
|
57
|
+
├─ components/
|
|
58
|
+
├─ lib/
|
|
59
|
+
├─ utils/
|
|
60
|
+
└─ types/
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
With route groups such as:
|
|
64
|
+
|
|
65
|
+
* `(marketing)`
|
|
66
|
+
* `(auth)`
|
|
67
|
+
* `(dashboard)`
|
|
68
|
+
|
|
69
|
+
-----
|
|
70
|
+
|
|
71
|
+
## 🛠 Included Utilities
|
|
72
|
+
|
|
73
|
+
Each project includes commonly used helpers:
|
|
74
|
+
|
|
75
|
+
* Date & time formatting
|
|
76
|
+
* Relative time helpers
|
|
77
|
+
* Number & currency formatting
|
|
78
|
+
* String utilities
|
|
79
|
+
* Async helpers
|
|
80
|
+
|
|
81
|
+
> 💡 **Note:** Utilities are copied into your project — you fully own the code.
|
|
82
|
+
|
|
83
|
+
-----
|
|
84
|
+
|
|
85
|
+
## 🧠 Philosophy
|
|
86
|
+
|
|
87
|
+
* This is **not a framework**.
|
|
88
|
+
* No runtime dependencies on Asynx packages
|
|
89
|
+
* No hidden abstractions
|
|
90
|
+
* No lock-in
|
|
91
|
+
* Clean, understandable code
|
|
92
|
+
|
|
93
|
+
> You can delete this CLI after creation — your project will continue to work.
|
|
94
|
+
|
|
95
|
+
-----
|
|
96
|
+
|
|
97
|
+
## 🤝 Contributing
|
|
98
|
+
|
|
99
|
+
Contributions are welcome\! Please read:
|
|
100
|
+
|
|
101
|
+
* [CONTRIBUTING.md](https://www.google.com/search?q=CONTRIBUTING.md)
|
|
102
|
+
* [CODE\_OF\_CONDUCT.md](https://www.google.com/search?q=CODE_OF_CONDUCT.md)
|
|
103
|
+
|
|
104
|
+
-----
|
|
105
|
+
|
|
106
|
+
## 🏢 About Asynx Devs Pvt Ltd
|
|
107
|
+
|
|
108
|
+
Asynx Devs builds scalable web products, internal tools, and SaaS platforms.
|
|
109
|
+
|
|
110
|
+
* 🌐 **Website:** [https://asynx.in](https://asynx.in)
|
|
111
|
+
* 🐙 **GitHub:** [https://github.com/Asynx-Pvt-Ltd](https://github.com/Asynx-Pvt-Ltd)
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import prompts from 'prompts';
|
|
3
|
+
import { createApp } from './commands/create.js';
|
|
4
|
+
import { TEMPLATES } from './constants/template.js';
|
|
5
|
+
import { logger } from './utils/logger.js';
|
|
6
|
+
function parseArgs() {
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
return {
|
|
9
|
+
projectName: args[0],
|
|
10
|
+
template: args.includes('--simple')
|
|
11
|
+
? 'simple'
|
|
12
|
+
: args.includes('--moderate')
|
|
13
|
+
? 'moderate'
|
|
14
|
+
: args.includes('--saas')
|
|
15
|
+
? 'saas'
|
|
16
|
+
: undefined,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export async function runCli() {
|
|
20
|
+
console.clear();
|
|
21
|
+
console.log(pc.bold(`
|
|
22
|
+
🚀 Asynx Next.js App Generator
|
|
23
|
+
--------------------------------
|
|
24
|
+
Opinionated Next.js templates for real-world apps.
|
|
25
|
+
|
|
26
|
+
by Asynx Devs Pvt Ltd
|
|
27
|
+
https://asynx.in
|
|
28
|
+
`));
|
|
29
|
+
const args = parseArgs();
|
|
30
|
+
const response = await prompts([
|
|
31
|
+
{
|
|
32
|
+
type: args.projectName ? null : 'text',
|
|
33
|
+
name: 'projectName',
|
|
34
|
+
message: 'What is your project name?',
|
|
35
|
+
initial: 'my-asynx-app',
|
|
36
|
+
validate: (name) => name.trim().length === 0 ? 'Project name is required' : true,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: args.template ? null : 'select',
|
|
40
|
+
name: 'template',
|
|
41
|
+
message: 'Select a project template',
|
|
42
|
+
choices: TEMPLATES.map((t) => ({
|
|
43
|
+
title: `${t.title} — ${pc.dim(t.description)}`,
|
|
44
|
+
value: t.value,
|
|
45
|
+
})),
|
|
46
|
+
},
|
|
47
|
+
], {
|
|
48
|
+
onCancel: () => {
|
|
49
|
+
logger.warn('Operation cancelled');
|
|
50
|
+
process.exit(0);
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
const projectName = args.projectName ?? response.projectName;
|
|
54
|
+
const template = args.template ?? response.template;
|
|
55
|
+
await createApp(projectName, template);
|
|
56
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { copyDir } from '../utils/copy.js';
|
|
5
|
+
import { logger } from '../utils/logger.js';
|
|
6
|
+
import { updatePackageName } from '../utils/package.js';
|
|
7
|
+
import { installDependencies } from '../utils/install.js';
|
|
8
|
+
import { detectPackageManager } from '../utils/package-manager.js';
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
export async function createApp(projectName, template) {
|
|
12
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
13
|
+
try {
|
|
14
|
+
await fs.access(targetDir);
|
|
15
|
+
logger.error(`Folder "${projectName}" already exists.`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// directory does not exist → good
|
|
20
|
+
}
|
|
21
|
+
logger.info(`Creating project in ${targetDir}`);
|
|
22
|
+
const templateDir = path.resolve(__dirname, '../../templates', template);
|
|
23
|
+
await copyDir(templateDir, targetDir);
|
|
24
|
+
await updatePackageName(targetDir, projectName);
|
|
25
|
+
const pm = detectPackageManager();
|
|
26
|
+
await installDependencies(targetDir, pm);
|
|
27
|
+
logger.success('Project ready!');
|
|
28
|
+
logger.info('');
|
|
29
|
+
logger.info('Next steps:');
|
|
30
|
+
logger.info(` cd ${projectName}`);
|
|
31
|
+
logger.info(` ${pm} dev`);
|
|
32
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const TEMPLATES = [
|
|
2
|
+
{
|
|
3
|
+
value: 'simple',
|
|
4
|
+
title: 'Simple App',
|
|
5
|
+
description: 'Landing pages, MVPs, small tools',
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
value: 'moderate',
|
|
9
|
+
title: 'Moderate App',
|
|
10
|
+
description: 'Dashboards, content platforms, startups',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
value: 'saas',
|
|
14
|
+
title: 'SaaS / Complex App',
|
|
15
|
+
description: 'Multi-tenant SaaS, role-based platforms',
|
|
16
|
+
},
|
|
17
|
+
];
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export async function copyDir(src, dest) {
|
|
4
|
+
await fs.mkdir(dest, { recursive: true });
|
|
5
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
6
|
+
for (const entry of entries) {
|
|
7
|
+
const srcPath = path.join(src, entry.name);
|
|
8
|
+
const destPath = path.join(dest, entry.name);
|
|
9
|
+
if (entry.isDirectory()) {
|
|
10
|
+
await copyDir(srcPath, destPath);
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
await fs.copyFile(srcPath, destPath);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
export function runCommand(command, args, cwd) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
const child = spawn(command, args, {
|
|
5
|
+
cwd,
|
|
6
|
+
stdio: 'inherit',
|
|
7
|
+
shell: true,
|
|
8
|
+
});
|
|
9
|
+
child.on('close', (code) => {
|
|
10
|
+
if (code === 0)
|
|
11
|
+
resolve();
|
|
12
|
+
else
|
|
13
|
+
reject(new Error(`${command} failed`));
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { runCommand } from './exec.js';
|
|
2
|
+
import { logger } from './logger.js';
|
|
3
|
+
export async function installDependencies(projectDir, pm) {
|
|
4
|
+
logger.info(`Installing dependencies using ${pm}...`);
|
|
5
|
+
if (pm === 'pnpm') {
|
|
6
|
+
await runCommand('pnpm', ['install'], projectDir);
|
|
7
|
+
}
|
|
8
|
+
else if (pm === 'yarn') {
|
|
9
|
+
await runCommand('yarn', [], projectDir);
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
await runCommand('npm', ['install'], projectDir);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
|
+
function commandExists(command) {
|
|
3
|
+
const result = spawnSync(command, ['--version'], {
|
|
4
|
+
stdio: 'ignore',
|
|
5
|
+
shell: true,
|
|
6
|
+
});
|
|
7
|
+
return result.status === 0;
|
|
8
|
+
}
|
|
9
|
+
export function detectPackageManager() {
|
|
10
|
+
if (commandExists('pnpm'))
|
|
11
|
+
return 'pnpm';
|
|
12
|
+
if (commandExists('yarn'))
|
|
13
|
+
return 'yarn';
|
|
14
|
+
return 'npm';
|
|
15
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export async function updatePackageName(projectDir, projectName) {
|
|
4
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
5
|
+
const raw = await fs.readFile(pkgPath, 'utf-8');
|
|
6
|
+
const updated = raw.replace('__PROJECT_NAME__', projectName);
|
|
7
|
+
await fs.writeFile(pkgPath, updated);
|
|
8
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@asynx/create-asynx-next-app",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Opinionated Next.js App Router scaffolder by Asynx Devs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-asynx-next-app": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsx src/index.ts",
|
|
12
|
+
"prepublishOnly": "pnpm build"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"nextjs",
|
|
16
|
+
"cli",
|
|
17
|
+
"scaffolding",
|
|
18
|
+
"",
|
|
19
|
+
"asynx"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/Asynx-Pvt-Ltd/create-asynx-next-app.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/Asynx-Pvt-Ltd/create-asynx-next-app/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://asynx.in",
|
|
29
|
+
"author": "Asynx Devs Pvt Ltd",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"packageManager": "pnpm@10.5.2",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^25.0.1",
|
|
37
|
+
"@types/prompts": "^2.4.9",
|
|
38
|
+
"tsx": "^4.21.0",
|
|
39
|
+
"typescript": "^5.9.3"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"picocolors": "^1.1.1",
|
|
43
|
+
"prompts": "^2.4.2"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import prompts from 'prompts';
|
|
3
|
+
import { createApp } from './commands/create.js';
|
|
4
|
+
import { TEMPLATES, TemplateType } from './constants/template.js';
|
|
5
|
+
import { logger } from './utils/logger.js';
|
|
6
|
+
|
|
7
|
+
interface CliOptions {
|
|
8
|
+
projectName?: string;
|
|
9
|
+
template?: TemplateType;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseArgs(): CliOptions {
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
projectName: args[0],
|
|
17
|
+
template: args.includes('--simple')
|
|
18
|
+
? 'simple'
|
|
19
|
+
: args.includes('--moderate')
|
|
20
|
+
? 'moderate'
|
|
21
|
+
: args.includes('--saas')
|
|
22
|
+
? 'saas'
|
|
23
|
+
: undefined,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function runCli() {
|
|
28
|
+
console.clear();
|
|
29
|
+
|
|
30
|
+
console.log(
|
|
31
|
+
pc.bold(`
|
|
32
|
+
🚀 Asynx Next.js App Generator
|
|
33
|
+
--------------------------------
|
|
34
|
+
Opinionated Next.js templates for real-world apps.
|
|
35
|
+
|
|
36
|
+
by Asynx Devs Pvt Ltd
|
|
37
|
+
https://asynx.in
|
|
38
|
+
`),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const args = parseArgs();
|
|
42
|
+
|
|
43
|
+
const response = await prompts(
|
|
44
|
+
[
|
|
45
|
+
{
|
|
46
|
+
type: args.projectName ? null : 'text',
|
|
47
|
+
name: 'projectName',
|
|
48
|
+
message: 'What is your project name?',
|
|
49
|
+
initial: 'my-asynx-app',
|
|
50
|
+
validate: (name: string) =>
|
|
51
|
+
name.trim().length === 0 ? 'Project name is required' : true,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: args.template ? null : 'select',
|
|
55
|
+
name: 'template',
|
|
56
|
+
message: 'Select a project template',
|
|
57
|
+
choices: TEMPLATES.map((t) => ({
|
|
58
|
+
title: `${t.title} — ${pc.dim(t.description)}`,
|
|
59
|
+
value: t.value,
|
|
60
|
+
})),
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
{
|
|
64
|
+
onCancel: () => {
|
|
65
|
+
logger.warn('Operation cancelled');
|
|
66
|
+
process.exit(0);
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const projectName = args.projectName ?? response.projectName;
|
|
72
|
+
const template = args.template ?? response.template;
|
|
73
|
+
await createApp(projectName, template);
|
|
74
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { copyDir } from '../utils/copy.js';
|
|
5
|
+
import { logger } from '../utils/logger.js';
|
|
6
|
+
import { updatePackageName } from '../utils/package.js';
|
|
7
|
+
import { installDependencies } from '../utils/install.js';
|
|
8
|
+
import { detectPackageManager } from '../utils/package-manager.js';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
export async function createApp(projectName: string, template: string) {
|
|
14
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
await fs.access(targetDir);
|
|
18
|
+
logger.error(`Folder "${projectName}" already exists.`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
} catch {
|
|
21
|
+
// directory does not exist → good
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
logger.info(`Creating project in ${targetDir}`);
|
|
25
|
+
|
|
26
|
+
const templateDir = path.resolve(__dirname, '../../templates', template);
|
|
27
|
+
|
|
28
|
+
await copyDir(templateDir, targetDir);
|
|
29
|
+
await updatePackageName(targetDir, projectName);
|
|
30
|
+
|
|
31
|
+
const pm = detectPackageManager();
|
|
32
|
+
await installDependencies(targetDir, pm);
|
|
33
|
+
|
|
34
|
+
logger.success('Project ready!');
|
|
35
|
+
logger.info('');
|
|
36
|
+
logger.info('Next steps:');
|
|
37
|
+
logger.info(` cd ${projectName}`);
|
|
38
|
+
logger.info(` ${pm} dev`);
|
|
39
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type TemplateType = 'simple' | 'moderate' | 'saas';
|
|
2
|
+
|
|
3
|
+
export const TEMPLATES = [
|
|
4
|
+
{
|
|
5
|
+
value: 'simple',
|
|
6
|
+
title: 'Simple App',
|
|
7
|
+
description: 'Landing pages, MVPs, small tools',
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
value: 'moderate',
|
|
11
|
+
title: 'Moderate App',
|
|
12
|
+
description: 'Dashboards, content platforms, startups',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
value: 'saas',
|
|
16
|
+
title: 'SaaS / Complex App',
|
|
17
|
+
description: 'Multi-tenant SaaS, role-based platforms',
|
|
18
|
+
},
|
|
19
|
+
] as const;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export async function copyDir(src: string, dest: string) {
|
|
5
|
+
await fs.mkdir(dest, { recursive: true });
|
|
6
|
+
|
|
7
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
8
|
+
|
|
9
|
+
for (const entry of entries) {
|
|
10
|
+
const srcPath = path.join(src, entry.name);
|
|
11
|
+
const destPath = path.join(dest, entry.name);
|
|
12
|
+
|
|
13
|
+
if (entry.isDirectory()) {
|
|
14
|
+
await copyDir(srcPath, destPath);
|
|
15
|
+
} else {
|
|
16
|
+
await fs.copyFile(srcPath, destPath);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
|
|
3
|
+
export function runCommand(
|
|
4
|
+
command: string,
|
|
5
|
+
args: string[],
|
|
6
|
+
cwd: string,
|
|
7
|
+
): Promise<void> {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const child = spawn(command, args, {
|
|
10
|
+
cwd,
|
|
11
|
+
stdio: 'inherit',
|
|
12
|
+
shell: true,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
child.on('close', (code) => {
|
|
16
|
+
if (code === 0) resolve();
|
|
17
|
+
else reject(new Error(`${command} failed`));
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { runCommand } from './exec.js';
|
|
2
|
+
import { logger } from './logger.js';
|
|
3
|
+
import { PackageManager } from './package-manager.js';
|
|
4
|
+
|
|
5
|
+
export async function installDependencies(
|
|
6
|
+
projectDir: string,
|
|
7
|
+
pm: PackageManager,
|
|
8
|
+
) {
|
|
9
|
+
logger.info(`Installing dependencies using ${pm}...`);
|
|
10
|
+
|
|
11
|
+
if (pm === 'pnpm') {
|
|
12
|
+
await runCommand('pnpm', ['install'], projectDir);
|
|
13
|
+
} else if (pm === 'yarn') {
|
|
14
|
+
await runCommand('yarn', [], projectDir);
|
|
15
|
+
} else {
|
|
16
|
+
await runCommand('npm', ['install'], projectDir);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
|
|
3
|
+
export const logger = {
|
|
4
|
+
info: (msg: string) => console.log(pc.cyan(msg)),
|
|
5
|
+
success: (msg: string) => console.log(pc.green(`✔ ${msg}`)),
|
|
6
|
+
warn: (msg: string) => console.log(pc.yellow(`⚠ ${msg}`)),
|
|
7
|
+
error: (msg: string) => console.error(pc.red(`✖ ${msg}`)),
|
|
8
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
export type PackageManager = 'pnpm' | 'yarn' | 'npm';
|
|
4
|
+
|
|
5
|
+
function commandExists(command: string) {
|
|
6
|
+
const result = spawnSync(command, ['--version'], {
|
|
7
|
+
stdio: 'ignore',
|
|
8
|
+
shell: true,
|
|
9
|
+
});
|
|
10
|
+
return result.status === 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function detectPackageManager(): PackageManager {
|
|
14
|
+
if (commandExists('pnpm')) return 'pnpm';
|
|
15
|
+
if (commandExists('yarn')) return 'yarn';
|
|
16
|
+
return 'npm';
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export async function updatePackageName(
|
|
5
|
+
projectDir: string,
|
|
6
|
+
projectName: string,
|
|
7
|
+
) {
|
|
8
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
9
|
+
|
|
10
|
+
const raw = await fs.readFile(pkgPath, 'utf-8');
|
|
11
|
+
const updated = raw.replace('__PROJECT_NAME__', projectName);
|
|
12
|
+
|
|
13
|
+
await fs.writeFile(pkgPath, updated);
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__",
|
|
3
|
+
"private": true,
|
|
4
|
+
"scripts": {
|
|
5
|
+
"dev": "next dev",
|
|
6
|
+
"build": "next build",
|
|
7
|
+
"start": "next start",
|
|
8
|
+
"lint": "next lint"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"next": "latest",
|
|
12
|
+
"react": "latest",
|
|
13
|
+
"react-dom": "latest"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"typescript": "^5.0.0",
|
|
17
|
+
"@types/react": "^18.0.0",
|
|
18
|
+
"@types/react-dom": "^18.0.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import './globals.css';
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
title: 'Asynx App',
|
|
5
|
+
description: 'Generated by create-asynx-next-app',
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export default function RootLayout({
|
|
9
|
+
children,
|
|
10
|
+
}: {
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
}) {
|
|
13
|
+
return (
|
|
14
|
+
<html lang="en">
|
|
15
|
+
<body>{children}</body>
|
|
16
|
+
</html>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": false,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"module": "esnext",
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"jsx": "preserve",
|
|
16
|
+
"incremental": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
|
19
|
+
"exclude": ["node_modules"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function formatDate(date: Date | string) {
|
|
2
|
+
return new Date(date).toLocaleDateString();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function formatDateLong(date: Date | string) {
|
|
6
|
+
return new Date(date).toLocaleDateString(undefined, {
|
|
7
|
+
year: 'numeric',
|
|
8
|
+
month: 'long',
|
|
9
|
+
day: 'numeric',
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function relativeTime(date: Date | string) {
|
|
14
|
+
const diff = Date.now() - new Date(date).getTime();
|
|
15
|
+
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
16
|
+
return `${days} day(s) ago`;
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__",
|
|
3
|
+
"private": true,
|
|
4
|
+
"scripts": {
|
|
5
|
+
"dev": "next dev",
|
|
6
|
+
"build": "next build",
|
|
7
|
+
"start": "next start",
|
|
8
|
+
"lint": "next lint"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"next": "latest",
|
|
12
|
+
"react": "latest",
|
|
13
|
+
"react-dom": "latest"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"typescript": "^5.0.0",
|
|
17
|
+
"@types/react": "^18.0.0",
|
|
18
|
+
"@types/react-dom": "^18.0.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import './globals.css';
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
title: 'Asynx App',
|
|
5
|
+
description: 'Generated by create-asynx-next-app',
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export default function RootLayout({
|
|
9
|
+
children,
|
|
10
|
+
}: {
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
}) {
|
|
13
|
+
return (
|
|
14
|
+
<html lang="en">
|
|
15
|
+
<body>{children}</body>
|
|
16
|
+
</html>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": false,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"module": "esnext",
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"jsx": "preserve",
|
|
16
|
+
"incremental": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
|
19
|
+
"exclude": ["node_modules"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function formatDate(date: Date | string) {
|
|
2
|
+
return new Date(date).toLocaleDateString();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function formatDateLong(date: Date | string) {
|
|
6
|
+
return new Date(date).toLocaleDateString(undefined, {
|
|
7
|
+
year: 'numeric',
|
|
8
|
+
month: 'long',
|
|
9
|
+
day: 'numeric',
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function relativeTime(date: Date | string) {
|
|
14
|
+
const diff = Date.now() - new Date(date).getTime();
|
|
15
|
+
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
16
|
+
return `${days} day(s) ago`;
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__",
|
|
3
|
+
"private": true,
|
|
4
|
+
"scripts": {
|
|
5
|
+
"dev": "next dev",
|
|
6
|
+
"build": "next build",
|
|
7
|
+
"start": "next start",
|
|
8
|
+
"lint": "next lint"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"next": "latest",
|
|
12
|
+
"react": "latest",
|
|
13
|
+
"react-dom": "latest"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"typescript": "^5.0.0",
|
|
17
|
+
"@types/react": "^18.0.0",
|
|
18
|
+
"@types/react-dom": "^18.0.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import './globals.css';
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
title: 'Asynx App',
|
|
5
|
+
description: 'Generated by create-asynx-next-app',
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export default function RootLayout({
|
|
9
|
+
children,
|
|
10
|
+
}: {
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
}) {
|
|
13
|
+
return (
|
|
14
|
+
<html lang="en">
|
|
15
|
+
<body>{children}</body>
|
|
16
|
+
</html>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": false,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"module": "esnext",
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"jsx": "preserve",
|
|
16
|
+
"incremental": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
|
19
|
+
"exclude": ["node_modules"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function formatDate(date: Date | string) {
|
|
2
|
+
return new Date(date).toLocaleDateString();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function formatDateLong(date: Date | string) {
|
|
6
|
+
return new Date(date).toLocaleDateString(undefined, {
|
|
7
|
+
year: 'numeric',
|
|
8
|
+
month: 'long',
|
|
9
|
+
day: 'numeric',
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function relativeTime(date: Date | string) {
|
|
14
|
+
const diff = Date.now() - new Date(date).getTime();
|
|
15
|
+
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
16
|
+
return `${days} day(s) ago`;
|
|
17
|
+
}
|
package/tsconfig.json
ADDED