setup-express-api 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/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # setup-express-api
2
+
3
+ Scaffold an Express API into the current empty directory.
4
+
5
+ ## Usage
6
+
7
+ ```sh
8
+ npx setup-express-api@latest ./my-api
9
+ ```
10
+
11
+ For a reproducible scaffold, request a specific version:
12
+
13
+ ```sh
14
+ npx setup-express-api@1.0.0 ./my-api
15
+ ```
package/bin/cli.js ADDED
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from "node:path";
4
+ import { assertTargetDirectory, scaffold } from "../src/scaffold.js";
5
+ import { promptForProjectConfig } from "../src/utils/project-config.js";
6
+
7
+ const argumentsList = process.argv.slice(2);
8
+ const showHelp = argumentsList.includes("--help") || argumentsList.includes("-h");
9
+ const skipInstall = argumentsList.includes("--skip-install");
10
+ const skipPrompts = argumentsList.includes("-y") || argumentsList.includes("--yes");
11
+ const targetArguments = argumentsList.filter((argument) => !argument.startsWith("-"));
12
+
13
+ if (showHelp) {
14
+ console.log("Usage: setup-express-api <directory> [--skip-install] [-y|--yes]");
15
+ console.log("\nExample: npx setup-express-api ./my-api");
16
+ process.exit(0);
17
+ }
18
+
19
+ if (targetArguments.length !== 1) {
20
+ console.error("Specify exactly one install directory. Example: npx setup-express-api ./my-api");
21
+ process.exit(1);
22
+ }
23
+
24
+ try {
25
+ const targetDir = path.resolve(process.cwd(), targetArguments[0]);
26
+ assertTargetDirectory(targetDir);
27
+
28
+ const projectConfig = skipPrompts
29
+ ? {
30
+ name: path.basename(targetDir).toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "express-api-server",
31
+ description: "",
32
+ author: "",
33
+ license: "MIT"
34
+ }
35
+ : await promptForProjectConfig(targetDir);
36
+
37
+ const result = await scaffold(targetDir, { projectConfig, installDependencies: !skipInstall });
38
+ console.log(`setup-express-api@${result.templateVersion}`);
39
+ } catch (error) {
40
+ console.error(error.message);
41
+ process.exitCode = 1;
42
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "setup-express-api",
3
+ "version": "1.0.0",
4
+ "description": "Create an Express API with auth features with better-auth.",
5
+ "type": "module",
6
+ "bin": {
7
+ "setup-express-api": "./bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src"
12
+ ],
13
+ "scripts": {
14
+ "test": "node --test test/*.test.js"
15
+ },
16
+ "dependencies": {
17
+ "@thantpyaeshine/express-api": "1.0.0"
18
+ },
19
+ "keywords": [
20
+ "setup-express-api",
21
+ "create-express-api",
22
+ "express",
23
+ "cli",
24
+ "api",
25
+ "typescript",
26
+ "better-auth"
27
+ ],
28
+ "license": "MIT",
29
+ "engines": {
30
+ "node": ">=22"
31
+ },
32
+ "allowScripts": {
33
+ "esbuild": true,
34
+ "@parcel/watcher": true,
35
+ "unrs-resolver": true
36
+ }
37
+ }
@@ -0,0 +1,145 @@
1
+ import fs from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const require = createRequire(import.meta.url);
7
+ const moduleDirectory = path.dirname(fileURLToPath(import.meta.url));
8
+ const cliPackagePath = path.resolve(moduleDirectory, "..", "package.json");
9
+
10
+ class PackageIntegrityError extends Error {
11
+ constructor(message) {
12
+ super(`${message}\n\nThis is a package release issue, not caused by your environment.`);
13
+ }
14
+ }
15
+
16
+ function readPackageJson(packageJsonPath, label) {
17
+ try {
18
+ return JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
19
+ } catch {
20
+ throw new PackageIntegrityError(`Unable to read ${label} package metadata.`);
21
+ }
22
+ }
23
+
24
+ function getTemplatePackageName(cliPackage) {
25
+ const dependencyNames = Object.keys(cliPackage.dependencies ?? {});
26
+
27
+ if (dependencyNames.length === 0) {
28
+ throw new PackageIntegrityError("CLI package metadata is missing its template dependency.");
29
+ }
30
+
31
+ return dependencyNames[0];
32
+ }
33
+
34
+ function compareVersions(left, right) {
35
+ const toParts = (value) =>
36
+ String(value ?? "0")
37
+ .split("-")[0]
38
+ .split(".")
39
+ .map((part) => Number.parseInt(part, 10) || 0);
40
+
41
+ const maxLength = Math.max(toParts(left).length, toParts(right).length);
42
+ for (let index = 0; index < maxLength; index += 1) {
43
+ const difference = (toParts(left)[index] ?? 0) - (toParts(right)[index] ?? 0);
44
+ if (difference !== 0) {
45
+ return difference;
46
+ }
47
+ }
48
+
49
+ return 0;
50
+ }
51
+
52
+ function resolveTemplatePackage(templatePackageName, resolvePackage) {
53
+ try {
54
+ return resolvePackage(`${templatePackageName}/package.json`);
55
+ } catch {
56
+ throw new Error(
57
+ `Unable to resolve required template package: ${templatePackageName}.\n\nThis CLI installation is incomplete. Reinstall the package or run npm install in the project that owns this CLI.`
58
+ );
59
+ }
60
+ }
61
+
62
+ export function assertTargetDirectory(targetDir) {
63
+ if (!fs.existsSync(targetDir)) {
64
+ throw new Error(`Target directory does not exist: ${targetDir}`);
65
+ }
66
+
67
+ if (!fs.statSync(targetDir).isDirectory()) {
68
+ throw new Error(`Target path is not a directory: ${targetDir}`);
69
+ }
70
+
71
+ if (fs.readdirSync(targetDir).length > 0) {
72
+ throw new Error(`Target directory is not empty: ${targetDir}`);
73
+ }
74
+ }
75
+
76
+ export async function scaffold(targetDir, options = {}) {
77
+ assertTargetDirectory(targetDir);
78
+
79
+ const cliPackage = readPackageJson(options.cliPackagePath || cliPackagePath, "CLI");
80
+ const templatePackageName = getTemplatePackageName(cliPackage);
81
+ const declaredTemplateVersion = cliPackage.dependencies?.[templatePackageName];
82
+
83
+ if (!declaredTemplateVersion) {
84
+ throw new PackageIntegrityError(
85
+ `Template dependency is missing from package metadata.\n\nPackage: ${cliPackage.name}\nExpected dependency: ${templatePackageName}`
86
+ );
87
+ }
88
+
89
+ const templatePackagePath = resolveTemplatePackage(templatePackageName, options.resolvePackage || require.resolve);
90
+ const templatePackage = readPackageJson(templatePackagePath, "template");
91
+
92
+ if (compareVersions(templatePackage.version, cliPackage.version) < 0) {
93
+ throw new PackageIntegrityError(
94
+ `Template version is older than the CLI version.\n\nCLI version: ${cliPackage.version}\nTemplate version: ${templatePackage.version}\n\nUpgrade the template dependency before scaffolding a new project.`
95
+ );
96
+ }
97
+
98
+ const templateDirectory = path.dirname(templatePackagePath);
99
+ if (!fs.existsSync(templateDirectory) || !fs.statSync(templateDirectory).isDirectory()) {
100
+ throw new PackageIntegrityError(`Template files are missing from ${templatePackageName}.`);
101
+ }
102
+
103
+ try {
104
+ fs.cpSync(templateDirectory, targetDir, { recursive: true });
105
+ if (options.projectConfig) {
106
+ const generatedPackagePath = path.join(targetDir, "package.json");
107
+ const generatedPackage = readPackageJson(generatedPackagePath, "generated project");
108
+ fs.writeFileSync(
109
+ generatedPackagePath,
110
+ `${JSON.stringify({ ...generatedPackage, ...options.projectConfig }, null, 2)}\n`
111
+ );
112
+ }
113
+ } catch {
114
+ throw new Error(`Unable to copy template files into: ${targetDir}`);
115
+ }
116
+
117
+ if (options.installDependencies !== false) {
118
+ const install = options.install || installDependencies;
119
+ await install(targetDir);
120
+ }
121
+
122
+ return { targetDir, templateVersion: templatePackage.version };
123
+ }
124
+
125
+ function installDependencies(targetDir) {
126
+ const { spawnSync } = require("node:child_process");
127
+ const isWindows = process.platform === "win32";
128
+ const command = isWindows ? (process.env.ComSpec || "cmd.exe") : "npm";
129
+ const args = isWindows ? ["/d", "/s", "/c", "npm", "install"] : ["install"];
130
+
131
+ const result = spawnSync(command, args, {
132
+ cwd: targetDir,
133
+ stdio: "inherit",
134
+ shell: false,
135
+ windowsHide: true
136
+ });
137
+
138
+ if (result.error) {
139
+ throw new Error(`Unable to start npm install: ${result.error.message}`);
140
+ }
141
+
142
+ if (result.status !== 0) {
143
+ throw new Error(`npm install failed with exit code ${result.status}. Run npm install in ${targetDir} after resolving the reported issue.`);
144
+ }
145
+ }
@@ -0,0 +1,43 @@
1
+ import path from "node:path";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ import readline from "node:readline/promises";
4
+
5
+ function toPackageName(value) {
6
+ return value
7
+ .toLowerCase()
8
+ .replace(/[^a-z0-9-]/g, "-")
9
+ .replace(/-+/g, "-")
10
+ .replace(/^-|-$/g, "");
11
+ }
12
+
13
+ async function ask(prompt, question, defaultValue = "") {
14
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
15
+ const answer = await prompt.question(`${question}${suffix}: `);
16
+ return answer.trim() || defaultValue;
17
+ }
18
+
19
+ async function askPackageName(prompt, defaultValue) {
20
+ let packageName = toPackageName(await ask(prompt, "Package name", defaultValue));
21
+ while (!packageName) {
22
+ console.log("Package name must include at least one letter or number.");
23
+ packageName = toPackageName(await ask(prompt, "Package name", defaultValue));
24
+ }
25
+ return packageName;
26
+ }
27
+
28
+ export async function promptForProjectConfig(targetDir) {
29
+ const prompt = readline.createInterface({ input, output });
30
+ const defaultName = toPackageName(path.basename(targetDir)) || "express-api-server";
31
+
32
+ try {
33
+ console.log("\nCreate an Express API\n");
34
+ return {
35
+ name: await askPackageName(prompt, defaultName),
36
+ description: await ask(prompt, "Description"),
37
+ author: await ask(prompt, "Author"),
38
+ license: await ask(prompt, "License", "MIT")
39
+ };
40
+ } finally {
41
+ prompt.close();
42
+ }
43
+ }