create-ej-app 28.0.0 → 30.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/cli.js +183 -0
- package/package.json +2 -1
package/cli.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import { randomBytes } from 'crypto';
|
|
5
|
+
import enquirer from 'enquirer';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
/**
|
|
11
|
+
* Local build and install output that must never reach a new project, even
|
|
12
|
+
* when the CLI runs from a checkout where a template was built or installed.
|
|
13
|
+
*/
|
|
14
|
+
const SKIPPED_TEMPLATE_PATHS = new Set([
|
|
15
|
+
'node_modules',
|
|
16
|
+
'.next',
|
|
17
|
+
'.swc',
|
|
18
|
+
'out',
|
|
19
|
+
'build',
|
|
20
|
+
'.env',
|
|
21
|
+
'next-env.d.ts',
|
|
22
|
+
// Generated by the Workflow SDK on every build
|
|
23
|
+
path.join('src', 'app', '.well-known')
|
|
24
|
+
]);
|
|
25
|
+
/** Template files whose `{{PROJECT_NAME}}` and `{{PROJECT_DESCRIPTION}}` get filled in. */
|
|
26
|
+
const PLACEHOLDER_FILES = ['README.md', 'AGENTS.md'];
|
|
27
|
+
const detectPackageManager = () => {
|
|
28
|
+
const userAgent = process.env.npm_config_user_agent;
|
|
29
|
+
if (userAgent) {
|
|
30
|
+
if (userAgent.startsWith('pnpm'))
|
|
31
|
+
return 'pnpm';
|
|
32
|
+
if (userAgent.startsWith('yarn'))
|
|
33
|
+
return 'yarn';
|
|
34
|
+
if (userAgent.startsWith('bun'))
|
|
35
|
+
return 'bun';
|
|
36
|
+
}
|
|
37
|
+
return 'npm';
|
|
38
|
+
};
|
|
39
|
+
const getInstallCommand = (packageManager) => {
|
|
40
|
+
switch (packageManager) {
|
|
41
|
+
case 'pnpm':
|
|
42
|
+
return 'pnpm i';
|
|
43
|
+
case 'yarn':
|
|
44
|
+
return 'yarn';
|
|
45
|
+
case 'bun':
|
|
46
|
+
return 'bun i';
|
|
47
|
+
default:
|
|
48
|
+
return 'npm i';
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const getRunCommand = (packageManager, script) => packageManager === 'npm'
|
|
52
|
+
? `npm run ${script}`
|
|
53
|
+
: `${packageManager} run ${script}`;
|
|
54
|
+
program.action(async () => {
|
|
55
|
+
const response = await enquirer.prompt([
|
|
56
|
+
{
|
|
57
|
+
type: 'input',
|
|
58
|
+
name: 'projectName',
|
|
59
|
+
message: 'Enter the project name:',
|
|
60
|
+
initial: 'my-project',
|
|
61
|
+
// The name becomes a directory and the package.json name, so keep it
|
|
62
|
+
// to characters that are safe for both.
|
|
63
|
+
validate: (value) => /^[a-z0-9][a-z0-9._-]{0,213}$/.test(value) ||
|
|
64
|
+
'Use lowercase letters, numbers, dots, hyphens, and underscores.'
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
type: 'input',
|
|
68
|
+
name: 'description',
|
|
69
|
+
message: 'Enter a description for the project:',
|
|
70
|
+
initial: 'A Legendary Project'
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
type: 'select',
|
|
74
|
+
name: 'template',
|
|
75
|
+
message: 'Select a template:',
|
|
76
|
+
choices: [
|
|
77
|
+
{
|
|
78
|
+
name: 'nextjs',
|
|
79
|
+
message: 'Full-stack Next.js 16 app with Better Auth, an admin panel, shadcn/ui, and Drizzle'
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'saas-starter',
|
|
83
|
+
message: 'SaaS starter with organizations, Stripe, support tickets, Better Auth, shadcn/ui, and Drizzle'
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'api',
|
|
87
|
+
message: 'API server with Elysia, Drizzle, and TypeScript'
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
type: 'select',
|
|
93
|
+
name: 'git',
|
|
94
|
+
message: 'Initialize a git repository?',
|
|
95
|
+
choices: ['yes', 'no']
|
|
96
|
+
}
|
|
97
|
+
]);
|
|
98
|
+
const { projectName, description, template, git } = response;
|
|
99
|
+
const projectDir = path.join(process.cwd(), projectName);
|
|
100
|
+
if (fs.existsSync(projectDir)) {
|
|
101
|
+
console.error(`Directory '${projectName}' already exists. Choose another project name or remove the directory.`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
fs.mkdirSync(projectDir);
|
|
105
|
+
const templateDir = path.join(__dirname, 'template', template);
|
|
106
|
+
fs.cpSync(templateDir, projectDir, {
|
|
107
|
+
recursive: true,
|
|
108
|
+
filter: source => {
|
|
109
|
+
const relative = path.relative(templateDir, source);
|
|
110
|
+
return (!SKIPPED_TEMPLATE_PATHS.has(relative) &&
|
|
111
|
+
!relative.split(path.sep).includes('node_modules') &&
|
|
112
|
+
!relative.endsWith('.tsbuildinfo'));
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
const renameGitignore = (dir) => {
|
|
116
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
117
|
+
const fullPath = path.join(dir, entry.name);
|
|
118
|
+
if (entry.isDirectory()) {
|
|
119
|
+
renameGitignore(fullPath);
|
|
120
|
+
}
|
|
121
|
+
else if (entry.name === 'gitignore') {
|
|
122
|
+
fs.renameSync(fullPath, path.join(dir, '.gitignore'));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
renameGitignore(projectDir);
|
|
127
|
+
const packageJsonFile = path.join(projectDir, 'package.json');
|
|
128
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonFile, 'utf-8'));
|
|
129
|
+
packageJson.name = projectName;
|
|
130
|
+
packageJson.description = description;
|
|
131
|
+
fs.writeFileSync(packageJsonFile, `${JSON.stringify(packageJson, null, 2)}\n`);
|
|
132
|
+
// Better Auth warns about weak secrets, so every project gets a random one.
|
|
133
|
+
const envExampleFile = path.join(projectDir, '.env.example');
|
|
134
|
+
const envExample = fs.readFileSync(envExampleFile, 'utf-8');
|
|
135
|
+
fs.writeFileSync(path.join(projectDir, '.env'), envExample.replace(/^BETTER_AUTH_SECRET=.*$/m, `BETTER_AUTH_SECRET="${randomBytes(32).toString('base64')}"`));
|
|
136
|
+
for (const file of PLACEHOLDER_FILES) {
|
|
137
|
+
const filePath = path.join(projectDir, file);
|
|
138
|
+
if (!fs.existsSync(filePath))
|
|
139
|
+
continue;
|
|
140
|
+
fs.writeFileSync(filePath, fs
|
|
141
|
+
.readFileSync(filePath, 'utf-8')
|
|
142
|
+
.replaceAll('{{PROJECT_NAME}}', projectName)
|
|
143
|
+
.replaceAll('{{PROJECT_DESCRIPTION}}', description));
|
|
144
|
+
}
|
|
145
|
+
const readmeFile = path.join(projectDir, 'README.md');
|
|
146
|
+
if (!fs.existsSync(readmeFile)) {
|
|
147
|
+
fs.writeFileSync(readmeFile, `# ${projectName}\n\n${description}\n`);
|
|
148
|
+
}
|
|
149
|
+
if (git === 'yes') {
|
|
150
|
+
try {
|
|
151
|
+
execSync('git init', { cwd: projectDir, stdio: 'inherit' });
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
console.error('Error initializing git repository:', err);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const pm = detectPackageManager();
|
|
158
|
+
const installCommand = getInstallCommand(pm);
|
|
159
|
+
const devCommand = getRunCommand(pm, 'dev');
|
|
160
|
+
const dbPushCommand = getRunCommand(pm, 'db:push');
|
|
161
|
+
console.log(`
|
|
162
|
+
Created a new app called '${projectName}' 🎉
|
|
163
|
+
|
|
164
|
+
To open the project in VS Code, run:
|
|
165
|
+
|
|
166
|
+
code ${projectName}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
To get started, run these commands:
|
|
170
|
+
cd ${projectName}
|
|
171
|
+
|
|
172
|
+
${installCommand}
|
|
173
|
+
|
|
174
|
+
Fill in the values in .env, then create the database tables:
|
|
175
|
+
|
|
176
|
+
${dbPushCommand}
|
|
177
|
+
|
|
178
|
+
${devCommand}
|
|
179
|
+
|
|
180
|
+
Happy coding! 🚀
|
|
181
|
+
`);
|
|
182
|
+
});
|
|
183
|
+
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-ej-app",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "30.0.0",
|
|
4
4
|
"description": "Bootstrap a next.js app with my preferred setup",
|
|
5
5
|
"main": "cli.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc",
|
|
9
|
+
"prepublishOnly": "npm run build",
|
|
9
10
|
"start": "node ."
|
|
10
11
|
},
|
|
11
12
|
"bin": {
|