create-arkstack 0.5.2 → 0.5.5

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.
Files changed (2) hide show
  1. package/bin/run.mjs +486 -0
  2. package/package.json +1 -1
package/bin/run.mjs ADDED
@@ -0,0 +1,486 @@
1
+ #!/usr/bin/env node
2
+ import { AbortPromptError, ExitPromptError } from "@inquirer/core";
3
+ import path, { basename, join, relative } from "node:path";
4
+ import { copyFile, readFile, readdir, rename, rm, unlink, writeFile } from "node:fs/promises";
5
+ import { Logger, Resolver } from "@h3ravel/shared";
6
+ import { Str } from "@h3ravel/support";
7
+ import { chdir } from "node:process";
8
+ import { detectPackageManager } from "@antfu/install-pkg";
9
+ import { downloadTemplate } from "giget";
10
+ import { existsSync } from "node:fs";
11
+ import { spawnSync } from "node:child_process";
12
+ import { Command, Kernel } from "@h3ravel/musket";
13
+ import inquirer from "inquirer";
14
+ //#region src/utils.ts
15
+ /**
16
+ * Removes all files in dirPath except the one specified by keepFileName
17
+ *
18
+ * @param dirPath
19
+ * @param keepFileName
20
+ */
21
+ async function cleanDirectoryExcept(dirPath, keepFileName) {
22
+ const files = await readdir(dirPath);
23
+ for (const file of files) {
24
+ if (file === keepFileName) continue;
25
+ await rm(path.join(dirPath, file), {
26
+ recursive: true,
27
+ force: true
28
+ });
29
+ }
30
+ }
31
+ /**
32
+ * Moves all files from dirPath to parent directory and removes dirPath
33
+ *
34
+ * @param dirPath
35
+ * @param parent
36
+ */
37
+ async function hoistDirectoryContents(parent, dirPath) {
38
+ const source = path.isAbsolute(dirPath) ? dirPath : path.join(process.cwd(), dirPath);
39
+ const targetParent = path.isAbsolute(parent) ? parent : path.join(process.cwd(), parent);
40
+ if (!source.startsWith(targetParent)) throw new Error("Source must be inside the parent directory");
41
+ const entries = await readdir(source);
42
+ for (const entry of entries) await rename(path.join(source, entry), path.join(targetParent, entry));
43
+ await rm(source, { recursive: true });
44
+ }
45
+ //#endregion
46
+ //#region src/templates.ts
47
+ /**
48
+ * List of first party templates
49
+ */
50
+ const templates = [{
51
+ name: "Express",
52
+ alias: "express",
53
+ hint: "Arkstack application running on Express",
54
+ source: "github:arkstack-hq/arkstack"
55
+ }, {
56
+ name: "H3",
57
+ alias: "h3",
58
+ hint: "Arkstack application running on H3",
59
+ source: "github:arkstack-hq/arkstack"
60
+ }];
61
+ const projectScopes = [{
62
+ name: "Lean",
63
+ hint: "A minimal Arkstack application running on {template}",
64
+ lean: true
65
+ }, {
66
+ name: "Full",
67
+ hint: "Full Arkstack experience on {template}",
68
+ lean: false
69
+ }];
70
+ //#endregion
71
+ //#region src/data.ts
72
+ const filesToRemove = [
73
+ "src/app",
74
+ "src/app/http/controllers",
75
+ "src/app/http/resources",
76
+ "src/models",
77
+ "src/app/models",
78
+ "database",
79
+ "src/routes/api.ts",
80
+ "src/database",
81
+ "src/core/database.ts",
82
+ "src/config/filesystem.ts",
83
+ "src/config/notifications.ts",
84
+ "prisma",
85
+ "prisma.config.ts",
86
+ "arkorm.config.ts",
87
+ "arkormx.config.ts",
88
+ "arkorm.config.js",
89
+ "arkormx.config.js",
90
+ "arkorm.config.mjs",
91
+ "arkormx.config.mjs"
92
+ ];
93
+ const fullDependencies = [
94
+ "@types/pg",
95
+ "@prisma/client",
96
+ "@prisma/adapter-pg",
97
+ "@arkstack/auth",
98
+ "@arkstack/database",
99
+ "@arkstack/filesystem",
100
+ "@arkstack/notifications",
101
+ "pg",
102
+ "kysely",
103
+ "prisma",
104
+ "arkormx"
105
+ ];
106
+ const leanDependencies = [];
107
+ const depsList = {
108
+ "@arkstack/auth": "^0.5.5",
109
+ "@arkstack/common": "^0.5.5",
110
+ "@arkstack/console": "^0.5.5",
111
+ "@arkstack/contract": "^0.5.5",
112
+ "@arkstack/database": "^0.5.5",
113
+ "@arkstack/driver-express": "^0.5.5",
114
+ "@arkstack/driver-h3": "^0.5.5",
115
+ "@arkstack/filesystem": "^0.5.5",
116
+ "@arkstack/http": "^0.5.5",
117
+ "@arkstack/view": "^0.5.5",
118
+ "@arkstack/notifications": "^0.5.5"
119
+ };
120
+ const environment = {
121
+ min: ["APP_URL", "APP_PORT"],
122
+ max: [
123
+ "JWT_SECRET",
124
+ "JWT_EXPIRES_IN",
125
+ "DATABASE_URL"
126
+ ]
127
+ };
128
+ //#endregion
129
+ //#region src/actions.ts
130
+ var actions_default = class {
131
+ location;
132
+ appName;
133
+ description;
134
+ skipInstallation;
135
+ packageJson = {};
136
+ pkgPath;
137
+ constructor(location, appName, description) {
138
+ this.location = location;
139
+ this.appName = appName;
140
+ this.description = description;
141
+ if (!this.location) this.location = join(process.cwd(), ".temp");
142
+ }
143
+ async pm() {
144
+ return await detectPackageManager() ?? "npm";
145
+ }
146
+ async runCmd(npx = false) {
147
+ if (npx) return "npx";
148
+ const pm = await this.pm();
149
+ return pm === "npm" ? "npm run" : pm;
150
+ }
151
+ async download(template, install = false, auth, overwrite = false) {
152
+ if (this.location?.includes(".temp") || overwrite && existsSync(this.location)) await rm(this.location, {
153
+ force: true,
154
+ recursive: true
155
+ });
156
+ else if (existsSync(this.location)) {
157
+ if ((await readdir(this.location ?? "./"))?.length > 0) {
158
+ console.log("\n");
159
+ Logger.parse([
160
+ [" ERROR ", "bgRed"],
161
+ [this.location, ["gray", "italic"]],
162
+ ["is not empty.", "white"]
163
+ ], " ");
164
+ console.log("");
165
+ process.exit(0);
166
+ }
167
+ }
168
+ this.skipInstallation = !install;
169
+ this.removeLockFile();
170
+ return await downloadTemplate(template, {
171
+ dir: this.location,
172
+ auth,
173
+ provider: "github",
174
+ registry: await this.pm(),
175
+ forceClean: false
176
+ });
177
+ }
178
+ /**
179
+ * Installs the project dependencies using the detected package manager.
180
+ * If a specific package name is provided, it will install that package
181
+ * instead of all dependencies.
182
+ *
183
+ * @param name
184
+ * @param args
185
+ * @returns
186
+ */
187
+ async installPackage(name, args = []) {
188
+ const bcmd = await Resolver.getPakageInstallCommand() + (name ? ` ${name}` : "");
189
+ const cmd = bcmd.split(" ")[0];
190
+ if (bcmd.includes(" ")) args.unshift(...bcmd.split(" ").slice(1));
191
+ const child = spawnSync(cmd, args, {
192
+ cwd: process.cwd(),
193
+ stdio: "ignore"
194
+ });
195
+ if (child.error) return child.status;
196
+ return 0;
197
+ }
198
+ async complete(install = false) {
199
+ let installed = false;
200
+ if (install) installed = await this.installPackage() === 0;
201
+ console.log("");
202
+ const installPath = "./" + relative(process.cwd(), this.location);
203
+ try {
204
+ chdir(path.join(process.cwd(), installPath));
205
+ } catch {}
206
+ Logger.success("Your Arkstack project has been created successfully");
207
+ Logger.parse([
208
+ ["cd", "cyan"],
209
+ [installPath, "yellow"],
210
+ installPath === process.cwd() ? ["✔", "green"] : ["", "green"]
211
+ ], " ");
212
+ if (!installed) Logger.parse([[await Resolver.getPakageInstallCommand(), "cyan"]]);
213
+ Logger.parse([[await this.runCmd(), "cyan"], ["dev", "yellow"]], " ");
214
+ Logger.parse([["Open", "cyan"], ["http://localhost:3000", "yellow"]]);
215
+ console.log("");
216
+ Logger.parse([["Have any questions", "white"]]);
217
+ Logger.parse([["Join our Discord server -", "white"], ["https://discord.gg/jmQybxKQ7R", "yellow"]]);
218
+ Logger.parse([["Checkout our other projects -", "white"], ["https://toneflix.net/open-source", "yellow"]]);
219
+ }
220
+ async removeLockFile() {
221
+ if (!this.skipInstallation) return;
222
+ await Promise.allSettled([
223
+ unlink(join(this.location, "package-lock.json")),
224
+ unlink(join(this.location, "yarn.lock")),
225
+ unlink(join(this.location, "pnpm-lock.yaml"))
226
+ ]);
227
+ }
228
+ async getBanner() {
229
+ return await readFile(join(process.cwd(), "./logo.txt"), "utf-8");
230
+ }
231
+ async createDotEnv(scope = "max") {
232
+ const envPath = join(this.location, ".env");
233
+ const exampleEnvPath = join(this.location, ".env.example");
234
+ const allowed = scope === "max" ? [...environment.max, ...environment.min] : environment.min;
235
+ if (existsSync(exampleEnvPath)) {
236
+ let lines = (await readFile(exampleEnvPath, "utf-8")).split(/\r?\n/);
237
+ for (let i = 0; i < lines.length; i++) {
238
+ const line = lines[i];
239
+ const key = line.split("=").at(0) ?? "";
240
+ if (key === "" || line === "" || line.trim().startsWith("#")) continue;
241
+ if (!allowed.includes(key)) delete lines[i];
242
+ }
243
+ lines = lines.slice(lines.findIndex((v) => v), lines.findLastIndex((v) => v) + 1);
244
+ await writeFile(exampleEnvPath, lines.join("\n"));
245
+ await copyFile(exampleEnvPath, envPath);
246
+ }
247
+ }
248
+ async saveProfile() {
249
+ if (this.pkgPath) await writeFile(this.pkgPath, JSON.stringify(this.packageJson, null, 2));
250
+ }
251
+ async makeProfile() {
252
+ const pkgPath = join(this.location, "package.json");
253
+ if (existsSync(pkgPath)) {
254
+ this.pkgPath = pkgPath;
255
+ this.packageJson = await readFile(pkgPath, "utf-8").then(JSON.parse);
256
+ for (const [name] of Object.entries(this.packageJson.dependencies)) if (name.includes("@arkstack/")) delete this.packageJson.dependencies[name];
257
+ const deps = Object.fromEntries([...Object.entries(depsList), ...Object.entries(this.packageJson.dependencies)]);
258
+ this.packageJson.dependencies = deps;
259
+ } else this.packageJson = {};
260
+ }
261
+ async makeFullProfile(_kit) {
262
+ await this.makeProfile();
263
+ if (!this.pkgPath) return;
264
+ for (const dep of leanDependencies) {
265
+ delete this.packageJson.dependencies?.[dep];
266
+ delete this.packageJson.devDependencies?.[dep];
267
+ }
268
+ }
269
+ async makeLeanProfile(_kit) {
270
+ await Promise.allSettled(filesToRemove.map((file) => rm(join(this.location, file), {
271
+ force: true,
272
+ recursive: true
273
+ })));
274
+ await this.makeProfile();
275
+ if (this.pkgPath) for (const dep of fullDependencies) {
276
+ delete this.packageJson.dependencies?.[dep];
277
+ delete this.packageJson.devDependencies?.[dep];
278
+ }
279
+ for (const file of [
280
+ "src/core/app.ts",
281
+ "src/core/router.ts",
282
+ "src/core/bootstrap.ts"
283
+ ]) {
284
+ const filePath = join(this.location, file);
285
+ if (!existsSync(filePath)) continue;
286
+ let content = await readFile(filePath, "utf-8");
287
+ content = content.replace("import { ValidatorDBDriver } from '@arkstack/database'\n", "").replace("import { ModelNotFoundException } from 'arkormx'\n", "").replace("import { prisma } from 'src/core/database'\n", "").replace("import { Prisma } from '@prisma/client'\n", "").replace("Validator.useDatabase(new ValidatorDBDriver())", "").replace(" async shutdown () {\n await prisma.$disconnect()\n process.exit(0)\n }", " async shutdown () {\n process.exit(0)\n }").replace(" * Shuts down the application by disconnecting from the database and exiting the process.", " * Shuts down the application and exits the process.").replace(/\n\s*if \((?:err|cause) instanceof Prisma\.PrismaClientKnownRequestError && (?:err|cause)\.code === "P2025"\) \{\n\s*error\.code = 404\n\s*error\.message = `\$\{(?:err|cause)\.meta\?\.modelName\} not found!`\n\s*\}\n/g, "\n").replace(/\n\s*if \((?:err|cause) instanceof ModelNotFoundException\) \{\n\s*error\.code = 404\n\s*error\.message = `\$\{(?:err|cause)\.getModelName\(\)\} not found!`\n\s*\}\n/g, "\n").replace(/if \(!\(err instanceof ValidationException\) &&\n\s*!\(err instanceof ModelNotFoundException\)\) {/g, "if (!(err instanceof ValidationException)) {").replace(/\s*\/\/ Register API routes\s*await ClearRouter\.group\('\/api', async \(\) => \{\s*await importFile\(join\(process\.cwd\(\), 'src\/routes\/api\.ts'\)\)\s*\}\)\s*/g, "\n\n ");
288
+ await writeFile(filePath, content, "utf-8");
289
+ }
290
+ }
291
+ async cleanup(kit) {
292
+ const pkg = this.packageJson;
293
+ delete pkg.packageManager;
294
+ delete pkg.scripts.predev;
295
+ delete pkg.scripts.prebuild;
296
+ delete pkg.scripts.precmd;
297
+ delete pkg.scripts.cmd;
298
+ pkg.scripts.dev = "ark dev";
299
+ pkg.scripts.build = "ark build";
300
+ pkg.scripts.postinstall = "prepare";
301
+ pkg.name = Str.slugify(this.appName ?? basename(this.location).replace(".", ""), "-");
302
+ if (this.description) pkg.description = this.description;
303
+ for (const name of Object.keys(pkg.dependencies)) if (name.includes("@arkstack/driver") && name !== "@arkstack/driver-" + kit) delete pkg.dependencies[name];
304
+ this.packageJson = pkg;
305
+ await Promise.allSettled([
306
+ this.saveProfile(),
307
+ this.removeLockFile(),
308
+ rm(join(this.location, "pnpm-workspace.yaml"), { force: true }),
309
+ rm(join(this.location, ".github"), {
310
+ force: true,
311
+ recursive: true
312
+ })
313
+ ]);
314
+ }
315
+ };
316
+ //#endregion
317
+ //#region src/logo.ts
318
+ const altLogo = String.raw`%c
319
+ _ __ _ _
320
+ /_\ _ __ ___/ _\ |_ __ _ ___| | __
321
+ //_\\| '__/ __\ \| __/ _\ |/ __| |/ /
322
+ / _ \ | | (___\ \ || (_| | (__| <
323
+ \_/ \_/_| \___\__/\__\__,_|\___|_|\_\
324
+
325
+ `;
326
+ //#endregion
327
+ //#region src/Commands/CreateArkstackCommand.ts
328
+ var CreateArkstackCommand = class extends Command {
329
+ signature = `create-arkstack
330
+ {location?: The location where this project should be created relative to the current dir.}
331
+ {--n|name?: The name of your project.}
332
+ {--i|install: Install node_modules right away}
333
+ {--t|token?: Kit repo authentication token.}
334
+ {--d|desc?: Project Description.}
335
+ {--k|kit?: Runtime template.}
336
+ {--p|pre: Download prerelease version if available.}
337
+ {--o|overwrite: Overwrite the installation directory if it is not empty.}
338
+ `;
339
+ description = "Display a personalized greeting.";
340
+ async handle() {
341
+ const options = this.options();
342
+ const pathName = this.argument("location");
343
+ console.log(altLogo, "font-family: monospace");
344
+ let { template } = await inquirer.prompt([{
345
+ type: "list",
346
+ name: "template",
347
+ message: "Choose Runtime:",
348
+ choices: templates.map((e) => ({
349
+ name: `${e.name} - ${e.hint}`,
350
+ value: e.alias,
351
+ disabled: !e.source ? "(Unavailable at this time)" : false
352
+ })),
353
+ default: "full",
354
+ when: () => !options.kit
355
+ }]).catch((err) => {
356
+ if (err instanceof AbortPromptError || err instanceof ExitPromptError) {
357
+ this.info("Thanks for trying out Arkstack.");
358
+ process.exit(0);
359
+ }
360
+ return err;
361
+ });
362
+ let { lean, appName, description } = await inquirer.prompt([
363
+ {
364
+ type: "list",
365
+ name: "lean",
366
+ message: "Project Scope:",
367
+ choices: projectScopes.map((e) => ({
368
+ name: `${e.name} - ${e.hint.replace("{template}", Str.title(template))}`,
369
+ value: e.lean
370
+ })),
371
+ default: "full",
372
+ when: () => !options.kit
373
+ },
374
+ {
375
+ type: "input",
376
+ name: "appName",
377
+ message: "What is the name of your project:",
378
+ default: `arkstack-${template}`,
379
+ when: () => !options.name
380
+ },
381
+ {
382
+ type: "input",
383
+ name: "description",
384
+ message: "Project Description:",
385
+ default: `Simple ${Str.of(template).ucfirst()}.js project created with Arkstack.`,
386
+ when: () => !options.desc
387
+ }
388
+ ]).catch((err) => {
389
+ if (err instanceof AbortPromptError || err instanceof ExitPromptError) {
390
+ this.info("Thanks for trying out Arkstack.");
391
+ process.exit(0);
392
+ }
393
+ return err;
394
+ });
395
+ let { location } = await inquirer.prompt([{
396
+ type: "input",
397
+ name: "location",
398
+ message: "Installation location relative to the current dir:",
399
+ default: Str.slugify(options.name ?? appName ?? basename(process.cwd()), "-"),
400
+ when: () => !pathName
401
+ }]).catch((err) => {
402
+ if (err instanceof AbortPromptError || err instanceof ExitPromptError) {
403
+ this.info("Thanks for trying out Arkstack.");
404
+ process.exit(0);
405
+ }
406
+ return err;
407
+ });
408
+ /**
409
+ * Find selected template kit
410
+ */
411
+ const kit = templates.find((e) => e.alias === template);
412
+ kit.lean = lean ?? false;
413
+ let { install, token, pre } = await inquirer.prompt([
414
+ {
415
+ type: "confirm",
416
+ name: "pre",
417
+ message: `An alpha version of the ${kit.name.replace(/\s*template$/i, "").trim()} template is available. Would you like to use it instead?`,
418
+ default: false,
419
+ when: () => kit.prereleaseSource && !options.pre
420
+ },
421
+ {
422
+ type: "input",
423
+ name: "token",
424
+ message: "Authentication token:",
425
+ when: () => options.kit && !options.token
426
+ },
427
+ {
428
+ type: "confirm",
429
+ name: "install",
430
+ message: "Would you like to install node_modules right away?:",
431
+ default: true,
432
+ when: () => !options.install
433
+ }
434
+ ]).catch((err) => {
435
+ if (err instanceof AbortPromptError || err instanceof ExitPromptError) {
436
+ this.info("Thanks for trying out Arkstack.");
437
+ process.exit(0);
438
+ }
439
+ return err;
440
+ });
441
+ pre = options.pre ?? pre;
442
+ token = options.token ?? token;
443
+ appName = options.name ?? appName;
444
+ install = options.install ?? install;
445
+ template = options.kit ?? template;
446
+ location = pathName ?? location;
447
+ description = options.description ?? description;
448
+ /**
449
+ * Validate selected kit
450
+ */
451
+ if (kit && !kit.source) {
452
+ this.error(`ERROR: The ${kit.name} kit is not currently available`);
453
+ process.exit(1);
454
+ }
455
+ const kitName = (kit.baseAlias ?? kit.alias).replace(/-lean$/i, "");
456
+ const source = pre && kit.prereleaseSource ? kit.prereleaseSource : kit.source;
457
+ const actions = new actions_default(join(process.cwd(), location), appName, description);
458
+ const spinner = this.spinner("Loading Template...").start();
459
+ const result = await actions.download(source, install, token, options.overwrite);
460
+ if (result.dir && kitName) {
461
+ await cleanDirectoryExcept(result.dir, kitName);
462
+ await hoistDirectoryContents(result.dir, join(result.dir, kitName));
463
+ }
464
+ if (kit.lean) {
465
+ spinner.info(Logger.parse([["Applying lean profile...", "green"]], "", false)).start();
466
+ await actions.makeLeanProfile(kitName);
467
+ } else await actions.makeFullProfile(kitName);
468
+ spinner.info(Logger.parse([["Cleaning Up...", "green"]], "", false)).start();
469
+ await actions.cleanup(kitName);
470
+ spinner.info(Logger.parse([["Initializing Project...", "green"]], "", false)).start();
471
+ await actions.createDotEnv(kit.lean ? "min" : "max");
472
+ await actions.complete(install);
473
+ spinner.succeed(Logger.parse([["Project initialization complete!", "green"]], "", false));
474
+ }
475
+ };
476
+ //#endregion
477
+ //#region src/run.ts
478
+ var Application = class {};
479
+ Kernel.init(new Application(), {
480
+ rootCommand: CreateArkstackCommand,
481
+ exceptionHandler: (e) => {
482
+ Logger.error(`ERROR: ${e.message}`);
483
+ }
484
+ });
485
+ //#endregion
486
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-arkstack",
3
3
  "type": "module",
4
- "version": "0.5.2",
4
+ "version": "0.5.5",
5
5
  "description": "Create new Arkstack framework applications on Express or H3 runtime drivers",
6
6
  "homepage": "https://arkstack.toneflix.net",
7
7
  "repository": {