adminforth 1.5.9-next.12 ā 1.5.9-next.14
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/commands/cli.js
CHANGED
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
const args = process.argv.slice(2);
|
|
4
4
|
const command = args[0];
|
|
5
5
|
|
|
6
|
-
import generateModels from "./generateModels.js";
|
|
7
6
|
import bundle from "./bundle.js";
|
|
7
|
+
import createApp from "./createApp/main.js";
|
|
8
|
+
import generateModels from "./generateModels.js";
|
|
8
9
|
|
|
9
10
|
switch (command) {
|
|
11
|
+
case "create-app":
|
|
12
|
+
createApp(args);
|
|
13
|
+
break;
|
|
10
14
|
case "generate-models":
|
|
11
15
|
generateModels();
|
|
12
16
|
break;
|
|
@@ -14,5 +18,5 @@ switch (command) {
|
|
|
14
18
|
bundle();
|
|
15
19
|
break;
|
|
16
20
|
default:
|
|
17
|
-
console.log("Unknown command. Available commands: generate-models, bundle");
|
|
18
|
-
}
|
|
21
|
+
console.log("Unknown command. Available commands: create-app, generate-models, bundle");
|
|
22
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<g clip-path="url(#clip0_1_8)">
|
|
3
|
+
<path d="M8.034 6.006V13H6.097V12.194C5.59433 12.8007 4.86633 13.104 3.913 13.104C3.25433 13.104 2.65633 12.9567 2.119 12.662C1.59033 12.3673 1.17433 11.947 0.871 11.401C0.567667 10.855 0.416 10.2223 0.416 9.503C0.416 8.78367 0.567667 8.151 0.871 7.605C1.17433 7.059 1.59033 6.63867 2.119 6.344C2.65633 6.04933 3.25433 5.902 3.913 5.902C4.80567 5.902 5.50333 6.18367 6.006 6.747V6.006H8.034ZM4.264 11.44C4.77533 11.44 5.2 11.2667 5.538 10.92C5.876 10.5647 6.045 10.0923 6.045 9.503C6.045 8.91367 5.876 8.44567 5.538 8.099C5.2 7.74367 4.77533 7.566 4.264 7.566C3.744 7.566 3.315 7.74367 2.977 8.099C2.639 8.44567 2.47 8.91367 2.47 9.503C2.47 10.0923 2.639 10.5647 2.977 10.92C3.315 11.2667 3.744 11.44 4.264 11.44Z" fill="url(#paint0_linear_1_8)"/>
|
|
4
|
+
<path d="M13.317 5.538C12.589 5.538 12.0387 5.69833 11.666 6.019C11.2933 6.331 11.107 6.80333 11.107 7.436V7.995H14.851V9.685H11.107V13H9.001V7.449C9.001 6.279 9.365 5.369 10.093 4.719C10.8297 4.069 11.8567 3.744 13.174 3.744C13.694 3.744 14.1837 3.80033 14.643 3.913C15.1023 4.017 15.501 4.173 15.839 4.381L15.189 6.045C14.669 5.707 14.045 5.538 13.317 5.538Z" fill="url(#paint1_linear_1_8)"/>
|
|
5
|
+
</g>
|
|
6
|
+
<defs>
|
|
7
|
+
<linearGradient id="paint0_linear_1_8" x1="1.5" y1="6.93333" x2="7.31883" y2="11.8926" gradientUnits="userSpaceOnUse">
|
|
8
|
+
<stop stop-color="#656E7A"/>
|
|
9
|
+
<stop offset="1" stop-color="#99A6B8"/>
|
|
10
|
+
</linearGradient>
|
|
11
|
+
<linearGradient id="paint1_linear_1_8" x1="12.5" y1="3.73333" x2="9.68642" y2="12.7016" gradientUnits="userSpaceOnUse">
|
|
12
|
+
<stop stop-color="#48C5FF"/>
|
|
13
|
+
<stop offset="1" stop-color="#A1E1FF"/>
|
|
14
|
+
</linearGradient>
|
|
15
|
+
<clipPath id="clip0_1_8">
|
|
16
|
+
<rect width="16" height="16" fill="white"/>
|
|
17
|
+
</clipPath>
|
|
18
|
+
</defs>
|
|
19
|
+
</svg>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
parseArgumentsIntoOptions,
|
|
5
|
+
prepareWorkflow,
|
|
6
|
+
promptForMissingOptions,
|
|
7
|
+
} from './utils.js';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export default async function createApp(args) {
|
|
11
|
+
// Step 1: Parse CLI arguments with `arg`
|
|
12
|
+
let options = parseArgumentsIntoOptions(args);
|
|
13
|
+
|
|
14
|
+
// Step 2: Ask for missing arguments via `inquirer`
|
|
15
|
+
options = await promptForMissingOptions(options);
|
|
16
|
+
|
|
17
|
+
// Step 3: Prepare a Listr-based workflow
|
|
18
|
+
const tasks = prepareWorkflow(options)
|
|
19
|
+
|
|
20
|
+
// Step 4: Run tasks
|
|
21
|
+
try {
|
|
22
|
+
await tasks.run();
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.error(chalk.red(`\nā ${err.message}\n`));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
export function gitignore() {
|
|
2
|
+
return `# Dependency directories
|
|
3
|
+
node_modules/
|
|
4
|
+
|
|
5
|
+
# dotenv environment variable files
|
|
6
|
+
.env
|
|
7
|
+
`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function env(dbUrl, prismaDbUrl) {
|
|
11
|
+
const base = `
|
|
12
|
+
ADMINFORTH_SECRET=123
|
|
13
|
+
NODE_ENV=development
|
|
14
|
+
DATABASE_URL=${dbUrl}
|
|
15
|
+
`
|
|
16
|
+
if (prismaDbUrl)
|
|
17
|
+
return base + `PRISMA_DATABASE_URL=${prismaDbUrl}`;
|
|
18
|
+
return base;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function envSample(dbUrl, prismaDbUrl) {
|
|
22
|
+
return env(dbUrl, prismaDbUrl);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function indexTs(appName) {
|
|
26
|
+
return `import express from 'express';
|
|
27
|
+
import AdminForth, { Filters } from 'adminforth';
|
|
28
|
+
import usersResource from "./resources/users";
|
|
29
|
+
|
|
30
|
+
const ADMIN_BASE_URL = '';
|
|
31
|
+
|
|
32
|
+
export const admin = new AdminForth({
|
|
33
|
+
baseUrl : ADMIN_BASE_URL,
|
|
34
|
+
auth: {
|
|
35
|
+
usersResourceId: 'users', // resource to get user during login
|
|
36
|
+
usernameField: 'email', // field where username is stored, should exist in resource
|
|
37
|
+
passwordHashField: 'password_hash',
|
|
38
|
+
rememberMeDays: 30, // users who will check "remember me" will stay logged in for 30 days
|
|
39
|
+
loginBackgroundImage: 'https://images.unsplash.com/photo-1534239697798-120952b76f2b?q=80&w=3389&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
|
|
40
|
+
loginBackgroundPosition: '1/2', // over, 3/4, 2/5, 3/5 (tailwind grid)
|
|
41
|
+
demoCredentials: "adminforth:adminforth", // never use it for production
|
|
42
|
+
loginPromptHTML: "Use email <b>adminforth</b> and password <b>adminforth</b> to login",
|
|
43
|
+
},
|
|
44
|
+
customization: {
|
|
45
|
+
brandName: '${appName}',
|
|
46
|
+
title: '${appName}',
|
|
47
|
+
favicon: '@@/assets/favicon.png',
|
|
48
|
+
brandLogo: '@@/assets/logo.svg',
|
|
49
|
+
datesFormat: 'DD MMM',
|
|
50
|
+
timeFormat: 'HH:mm a',
|
|
51
|
+
showBrandNameInSidebar: true,
|
|
52
|
+
styles: {
|
|
53
|
+
colors: {
|
|
54
|
+
light: {
|
|
55
|
+
// color for links, icons etc.
|
|
56
|
+
primary: '#B400B8',
|
|
57
|
+
// color for sidebar and text
|
|
58
|
+
sidebar: {main:'#571E58', text:'white'},
|
|
59
|
+
},
|
|
60
|
+
dark: {
|
|
61
|
+
primary: '#82ACFF',
|
|
62
|
+
sidebar: {main:'#1f2937', text:'#9ca3af'},
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
dataSources: [
|
|
68
|
+
{
|
|
69
|
+
id: 'maindb',
|
|
70
|
+
url: \`\${process.env.DATABASE_URL}\`
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
resources: [
|
|
74
|
+
usersResource,
|
|
75
|
+
],
|
|
76
|
+
menu: [
|
|
77
|
+
{
|
|
78
|
+
type: 'heading',
|
|
79
|
+
label: 'SYSTEM',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
label: 'Users',
|
|
83
|
+
icon: 'flowbite:user-solid', // any icon from iconify supported in format <setname>:<icon>, e.g. from here https://icon-sets.iconify.design/flowbite/
|
|
84
|
+
resourceId: 'users',
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (import.meta.url === \`file://\${process.argv[1]}\`) {
|
|
90
|
+
// if script is executed directly e.g. node index.ts or npm start
|
|
91
|
+
|
|
92
|
+
const app = express()
|
|
93
|
+
app.use(express.json());
|
|
94
|
+
const port = 3500;
|
|
95
|
+
|
|
96
|
+
// needed to compile SPA. Call it here or from a build script e.g. in Docker build time to reduce downtime
|
|
97
|
+
await admin.bundleNow({ hotReload: process.env.NODE_ENV === 'development'});
|
|
98
|
+
console.log('Bundling AdminForth done. For faster serving consider calling bundleNow() from a build script.');
|
|
99
|
+
|
|
100
|
+
// serve after you added all api
|
|
101
|
+
admin.express.serve(app)
|
|
102
|
+
|
|
103
|
+
admin.discoverDatabases().then(async () => {
|
|
104
|
+
if (!await admin.resource('users').get([Filters.EQ('email', 'adminforth')])) {
|
|
105
|
+
await admin.resource('users').create({
|
|
106
|
+
email: 'adminforth',
|
|
107
|
+
password_hash: await AdminForth.Utils.generatePasswordHash('adminforth'),
|
|
108
|
+
role: 'superadmin',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
admin.express.listen(port, () => {
|
|
114
|
+
console.log(\`Example app listening at http://localhost:\${port}\`)
|
|
115
|
+
console.log(\`\\nā” AdminForth is available at http://localhost:\${port}\${ADMIN_BASE_URL}\n\`)
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function schemaPrisma(provider, dbUrl) {
|
|
122
|
+
return `generator client {
|
|
123
|
+
provider = "prisma-client-js"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
datasource db {
|
|
127
|
+
provider = "${provider}"
|
|
128
|
+
url = env("PRISMA_DATABASE_URL")
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
model adminuser {
|
|
132
|
+
id String @id
|
|
133
|
+
email String @unique
|
|
134
|
+
password_hash String
|
|
135
|
+
role String
|
|
136
|
+
created_at DateTime
|
|
137
|
+
}
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function usersResource() {
|
|
142
|
+
return `import AdminForth, { AdminForthDataTypes, AdminForthResourceInput } from 'adminforth';
|
|
143
|
+
import type { AdminUser } from 'adminforth';
|
|
144
|
+
|
|
145
|
+
async function canModifyUsers({ adminUser }: { adminUser: AdminUser }): Promise<boolean> {
|
|
146
|
+
return adminUser.dbUser.role === 'superadmin';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export default {
|
|
150
|
+
dataSource: 'maindb',
|
|
151
|
+
table: 'adminuser',
|
|
152
|
+
resourceId: 'users',
|
|
153
|
+
label: 'Users',
|
|
154
|
+
recordLabel: (r) => \`š¤ \${r.email}\`,
|
|
155
|
+
options: {
|
|
156
|
+
allowedActions: {
|
|
157
|
+
edit: canModifyUsers,
|
|
158
|
+
delete: canModifyUsers,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
columns: [
|
|
162
|
+
{
|
|
163
|
+
name: 'id',
|
|
164
|
+
primaryKey: true,
|
|
165
|
+
type: AdminForthDataTypes.STRING,
|
|
166
|
+
fillOnCreate: ({ initialRecord, adminUser }) => Math.random().toString(36).substring(7),
|
|
167
|
+
showIn: ['list', 'filter', 'show'],
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: 'email',
|
|
171
|
+
required: true,
|
|
172
|
+
isUnique: true,
|
|
173
|
+
type: AdminForthDataTypes.STRING,
|
|
174
|
+
validation: [
|
|
175
|
+
// you can also use AdminForth.Utils.EMAIL_VALIDATOR which is alias to this object
|
|
176
|
+
{
|
|
177
|
+
regExp: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
|
|
178
|
+
message: 'Email is not valid, must be in format example@test.com'
|
|
179
|
+
},
|
|
180
|
+
]
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: 'created_at',
|
|
184
|
+
type: AdminForthDataTypes.DATETIME,
|
|
185
|
+
showIn: ['list', 'filter', 'show'],
|
|
186
|
+
fillOnCreate: ({ initialRecord, adminUser }) => (new Date()).toISOString(),
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: 'role',
|
|
190
|
+
type: AdminForthDataTypes.STRING,
|
|
191
|
+
enum: [
|
|
192
|
+
{ value: 'superadmin', label: 'Super Admin' },
|
|
193
|
+
{ value: 'user', label: 'User' },
|
|
194
|
+
]
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'password',
|
|
198
|
+
virtual: true, // field will not be persisted into db
|
|
199
|
+
required: { create: true }, // make required only on create page
|
|
200
|
+
editingNote: { edit: 'Leave empty to keep password unchanged' },
|
|
201
|
+
type: AdminForthDataTypes.STRING,
|
|
202
|
+
showIn: ['create', 'edit'], // to show field only on create and edit pages
|
|
203
|
+
masked: true, // to show stars in input field
|
|
204
|
+
|
|
205
|
+
minLength: 8,
|
|
206
|
+
validation: [
|
|
207
|
+
// request to have at least 1 digit, 1 upper case, 1 lower case
|
|
208
|
+
AdminForth.Utils.PASSWORD_VALIDATORS.UP_LOW_NUM,
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: 'password_hash',
|
|
213
|
+
type: AdminForthDataTypes.STRING,
|
|
214
|
+
backendOnly: true,
|
|
215
|
+
showIn: []
|
|
216
|
+
}
|
|
217
|
+
],
|
|
218
|
+
hooks: {
|
|
219
|
+
create: {
|
|
220
|
+
beforeSave: async ({ record, adminUser, resource }) => {
|
|
221
|
+
record.password_hash = await AdminForth.Utils.generatePasswordHash(record.password);
|
|
222
|
+
return { ok: true };
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
edit: {
|
|
226
|
+
beforeSave: async ({ record, adminUser, resource }) => {
|
|
227
|
+
if (record.password) {
|
|
228
|
+
record.password_hash = await AdminForth.Utils.generatePasswordHash(record.password);
|
|
229
|
+
}
|
|
230
|
+
return { ok: true }
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
}
|
|
234
|
+
} as AdminForthResourceInput;
|
|
235
|
+
`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Root package.json template
|
|
240
|
+
*/
|
|
241
|
+
export function rootPackageJson(appName) {
|
|
242
|
+
// Note: you might want to keep versions in sync or fetch from a config
|
|
243
|
+
return `{
|
|
244
|
+
"name": "${appName}",
|
|
245
|
+
"version": "1.0.0",
|
|
246
|
+
"main": "index.ts",
|
|
247
|
+
"type": "module",
|
|
248
|
+
"keywords": [],
|
|
249
|
+
"author": "",
|
|
250
|
+
"license": "ISC",
|
|
251
|
+
"description": "",
|
|
252
|
+
"scripts": {
|
|
253
|
+
"start": "tsx watch --env-file=.env index.ts",
|
|
254
|
+
"migrate": "npx prisma migrate deploy",
|
|
255
|
+
"makemigration": "npx --yes prisma migrate deploy; npx --yes prisma migrate dev",
|
|
256
|
+
"test": "echo \\"Error: no test specified\\" && exit 1"
|
|
257
|
+
},
|
|
258
|
+
"engines": {
|
|
259
|
+
"node": ">=20"
|
|
260
|
+
},
|
|
261
|
+
"dependencies": {
|
|
262
|
+
"adminforth": "latest",
|
|
263
|
+
"express": "latest"
|
|
264
|
+
},
|
|
265
|
+
"devDependencies": {
|
|
266
|
+
"typescript": "5.4.5",
|
|
267
|
+
"tsx": "4.11.2",
|
|
268
|
+
"@types/express": "latest",
|
|
269
|
+
"@types/node": "latest"
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Root tsconfig.json template
|
|
277
|
+
*/
|
|
278
|
+
export function rootTsConfig() {
|
|
279
|
+
return `{
|
|
280
|
+
"compilerOptions": {
|
|
281
|
+
"target": "esnext",
|
|
282
|
+
"module": "nodenext",
|
|
283
|
+
"esModuleInterop": true,
|
|
284
|
+
"forceConsistentCasingInFileNames": true,
|
|
285
|
+
"strict": true
|
|
286
|
+
},
|
|
287
|
+
"exclude": ["node_modules", "dist"]
|
|
288
|
+
}
|
|
289
|
+
`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* custom/package.json
|
|
294
|
+
*/
|
|
295
|
+
export function customPackageJson(appName) {
|
|
296
|
+
return `{
|
|
297
|
+
"name": "custom",
|
|
298
|
+
"version": "1.0.0",
|
|
299
|
+
"main": "index.ts",
|
|
300
|
+
"scripts": {
|
|
301
|
+
"test": "echo \\"Error: no test specified\\" && exit 1"
|
|
302
|
+
},
|
|
303
|
+
"keywords": [],
|
|
304
|
+
"author": "",
|
|
305
|
+
"license": "ISC",
|
|
306
|
+
"description": ""
|
|
307
|
+
}
|
|
308
|
+
`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* custom/tsconfig.json
|
|
313
|
+
*/
|
|
314
|
+
export function customTsconfig() {
|
|
315
|
+
return `{
|
|
316
|
+
"compilerOptions": {
|
|
317
|
+
"baseUrl": ".",
|
|
318
|
+
"paths": {
|
|
319
|
+
"@/": "../node_modules/adminforth/dist/spa/src/",
|
|
320
|
+
"": "../node_modules/adminforth/dist/spa/node_modules/",
|
|
321
|
+
"@@/*": "."
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
`;
|
|
326
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import arg from 'arg';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import fse from 'fs-extra';
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { Listr } from 'listr2'
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import {ConnectionString} from 'connection-string';
|
|
10
|
+
import {execa} from 'execa';
|
|
11
|
+
|
|
12
|
+
import * as templates from './templates.js';
|
|
13
|
+
|
|
14
|
+
export function parseArgumentsIntoOptions(rawArgs) {
|
|
15
|
+
const args = arg(
|
|
16
|
+
{
|
|
17
|
+
'--app-name': String,
|
|
18
|
+
'--db': String,
|
|
19
|
+
// you can add more flags here if needed
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
argv: rawArgs.slice(1), // skip "create-app"
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
appName: args['--app-name'],
|
|
28
|
+
db: args['--db'],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function promptForMissingOptions(options) {
|
|
33
|
+
const questions = [];
|
|
34
|
+
|
|
35
|
+
if (!options.appName) {
|
|
36
|
+
questions.push({
|
|
37
|
+
type: 'input',
|
|
38
|
+
name: 'appName',
|
|
39
|
+
message: 'Please specify the name of the app >',
|
|
40
|
+
default: 'adminforth-app',
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (!options.db) {
|
|
45
|
+
questions.push({
|
|
46
|
+
type: 'input',
|
|
47
|
+
name: 'db',
|
|
48
|
+
message: 'Please specify the database URL to use >',
|
|
49
|
+
default: 'sqlite://.db.sqlite',
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const answers = await inquirer.prompt(questions);
|
|
54
|
+
return {
|
|
55
|
+
...options,
|
|
56
|
+
appName: options.appName || answers.appName,
|
|
57
|
+
db: options.db || answers.db,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function checkNodeVersion(minRequiredVersion = 20) {
|
|
62
|
+
const current = process.versions.node.split('.');
|
|
63
|
+
const major = parseInt(current[0], 10);
|
|
64
|
+
|
|
65
|
+
if (isNaN(major) || major < minRequiredVersion) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Node.js v${minRequiredVersion}+ is required. You have ${process.versions.node}. ` +
|
|
68
|
+
`Please upgrade Node.js.`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseConnectionString(dbUrl) {
|
|
74
|
+
return new ConnectionString(dbUrl);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function detectDbProvider(protocol) {
|
|
78
|
+
if (protocol.startsWith('sqlite')) {
|
|
79
|
+
return 'sqlite';
|
|
80
|
+
} else if (protocol.startsWith('postgres')) {
|
|
81
|
+
return 'postgresql';
|
|
82
|
+
} else if (protocol.startsWith('mongodb')) {
|
|
83
|
+
return 'mongodb';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const message = `Unknown database provider for ${protocol}. Only SQLite, PostgreSQL, and MongoDB are supported now.`;
|
|
87
|
+
throw new Error(message);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function generateDbUrlForPrisma(connectionString) {
|
|
91
|
+
if (connectionString.protocol.startsWith('sqlite'))
|
|
92
|
+
return `file:${connectionString.host}`;
|
|
93
|
+
if (connectionString.protocol.startsWith('mongodb'))
|
|
94
|
+
return null;
|
|
95
|
+
return connectionString.toString();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function initialChecks() {
|
|
99
|
+
return [
|
|
100
|
+
{
|
|
101
|
+
title: 'š Checking Node.js version...',
|
|
102
|
+
task: () => checkNodeVersion(20)
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
title: 'š Validating current working directory...',
|
|
106
|
+
task: () => checkForExistingPackageJson()
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function checkForExistingPackageJson() {
|
|
112
|
+
if (fs.existsSync(path.join(process.cwd(), 'package.json'))) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`A package.json already exists in this directory.\n` +
|
|
115
|
+
`Please remove it or use an empty directory.`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function scaffoldProject(ctx, options, cwd) {
|
|
121
|
+
const connectionString = parseConnectionString(options.db);
|
|
122
|
+
const provider = detectDbProvider(connectionString.protocol);
|
|
123
|
+
const prismaDbUrl = generateDbUrlForPrisma(connectionString);
|
|
124
|
+
ctx.skipPrismaSetup = !prismaDbUrl;
|
|
125
|
+
const appName = options.appName;
|
|
126
|
+
|
|
127
|
+
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
128
|
+
const sourceAssetsDir = path.join(dirname, 'assets');
|
|
129
|
+
const targetAssetsDir = path.join(cwd, 'custom', 'assets');
|
|
130
|
+
|
|
131
|
+
await fse.ensureDir(targetAssetsDir);
|
|
132
|
+
await fse.copy(sourceAssetsDir, targetAssetsDir);
|
|
133
|
+
|
|
134
|
+
writeTemplateFiles(cwd, connectionString.toString(), prismaDbUrl, appName, provider);
|
|
135
|
+
|
|
136
|
+
const resourcesDir = path.join(cwd, 'resources');
|
|
137
|
+
await fse.ensureDir(resourcesDir);
|
|
138
|
+
fs.writeFileSync(path.join(resourcesDir, 'users.ts'), templates.usersResource());
|
|
139
|
+
|
|
140
|
+
const customDir = path.join(cwd, 'custom');
|
|
141
|
+
await fse.ensureDir(customDir);
|
|
142
|
+
ctx.customDir = customDir;
|
|
143
|
+
fs.writeFileSync(path.join(customDir, 'package.json'), templates.customPackageJson(appName));
|
|
144
|
+
fs.writeFileSync(path.join(customDir, 'tsconfig.json'), templates.customTsconfig());
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function writeTemplateFiles(cwd, dbUrl, prismaDbUrl, appName, provider) {
|
|
148
|
+
fs.writeFileSync(path.join(cwd, '.gitignore'), templates.gitignore());
|
|
149
|
+
fs.writeFileSync(path.join(cwd, '.env'), templates.env(dbUrl, prismaDbUrl));
|
|
150
|
+
fs.writeFileSync(path.join(cwd, '.env.sample'), templates.envSample(dbUrl, prismaDbUrl));
|
|
151
|
+
fs.writeFileSync(path.join(cwd, 'index.ts'), templates.indexTs(appName));
|
|
152
|
+
if (prismaDbUrl)
|
|
153
|
+
fs.writeFileSync(path.join(cwd, 'schema.prisma'), templates.schemaPrisma(provider, prismaDbUrl));
|
|
154
|
+
fs.writeFileSync(path.join(cwd, 'package.json'), templates.rootPackageJson(appName));
|
|
155
|
+
fs.writeFileSync(path.join(cwd, 'tsconfig.json'), templates.rootTsConfig());
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function installDependencies(ctx, cwd) {
|
|
159
|
+
const customDir = ctx.customDir;
|
|
160
|
+
|
|
161
|
+
await Promise.all([
|
|
162
|
+
await execa('npm', ['install', '--no-package-lock'], { cwd }),
|
|
163
|
+
await execa('npm', ['install'], { cwd: customDir }),
|
|
164
|
+
]);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function generateFinalInstructions(skipPrismaSetup) {
|
|
168
|
+
let instruction = 'āļø Run the following commands to get started:\n';
|
|
169
|
+
if (!skipPrismaSetup)
|
|
170
|
+
instruction += `
|
|
171
|
+
${chalk.dim('// runs "npx prisma migrate dev --name init" to generate and apply initial migration')}`;
|
|
172
|
+
${chalk.cyan('$ npm run makemigration -- --name init')}
|
|
173
|
+
instruction += `
|
|
174
|
+
${chalk.dim('\n// starts dev server with tsx watch for hot-reloading')}\n
|
|
175
|
+
${chalk.cyan('$ npm start')}
|
|
176
|
+
`;
|
|
177
|
+
|
|
178
|
+
instruction += 'š Happy coding!';
|
|
179
|
+
|
|
180
|
+
return instruction;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function prepareWorkflow(options) {
|
|
184
|
+
const cwd = process.cwd();
|
|
185
|
+
const tasks = new Listr([
|
|
186
|
+
{
|
|
187
|
+
title: 'š Initial checks...',
|
|
188
|
+
task: (_, task) =>
|
|
189
|
+
task.newListr(
|
|
190
|
+
initialChecks(),
|
|
191
|
+
{ concurrent: true },
|
|
192
|
+
)
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
title: 'š Scaffolding your project...',
|
|
196
|
+
task: async (ctx) => scaffoldProject(ctx, options, cwd)
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
title: 'š¦ Installing dependencies...',
|
|
200
|
+
task: async (ctx) => installDependencies(ctx, cwd)
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
title: 'š Preparing final instructions...',
|
|
204
|
+
task: (ctx) => {
|
|
205
|
+
console.log(chalk.green(`ā
Successfully created your new Adminforth project!\n`));
|
|
206
|
+
console.log(generateFinalInstructions(ctx.skipPrismaSetup));
|
|
207
|
+
console.log('\n\n');
|
|
208
|
+
}
|
|
209
|
+
}],
|
|
210
|
+
{
|
|
211
|
+
rendererOptions: {collapseSubtasks: false},
|
|
212
|
+
concurrent: false,
|
|
213
|
+
exitOnError: true,
|
|
214
|
+
collectErrors: true,
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
return tasks;
|
|
219
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adminforth",
|
|
3
|
-
"version": "1.5.9-next.
|
|
3
|
+
"version": "1.5.9-next.14",
|
|
4
4
|
"description": "OpenSource Vue3 powered forth-generation admin panel",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -40,14 +40,21 @@
|
|
|
40
40
|
"@clickhouse/client": "^1.4.0",
|
|
41
41
|
"@faker-js/faker": "^9.0.3",
|
|
42
42
|
"@types/express": "^5.0.0",
|
|
43
|
+
"arg": "^5.0.2",
|
|
43
44
|
"better-sqlite3": "^11.5.0",
|
|
45
|
+
"chalk": "^5.4.1",
|
|
46
|
+
"connection-string": "^4.4.0",
|
|
44
47
|
"dayjs": "^1.11.11",
|
|
45
48
|
"dotenv": "^16.4.5",
|
|
49
|
+
"esm": "^3.2.25",
|
|
50
|
+
"execa": "^9.5.2",
|
|
46
51
|
"express": "^4.21.0",
|
|
47
52
|
"filewatcher": "^3.0.1",
|
|
48
53
|
"fs-extra": "^11.2.0",
|
|
49
54
|
"fuse.js": "^7.0.0",
|
|
55
|
+
"inquirer": "^12.3.0",
|
|
50
56
|
"jsonwebtoken": "^9.0.2",
|
|
57
|
+
"listr2": "^8.2.5",
|
|
51
58
|
"mongodb": "6.6",
|
|
52
59
|
"node-fetch": "^3.3.2",
|
|
53
60
|
"pg": "^8.11.5",
|