@morda-dev/create-sdk 1.1.1 → 1.1.4

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 CHANGED
@@ -1,9 +1,31 @@
1
1
  # create-sdk
2
+ ![npm](https://img.shields.io/npm/v/@morda-dev/create-sdk)
3
+ ![npm downloads](https://img.shields.io/npm/dm/@morda-dev/create-sdk)
4
+ ![CI](https://github.com/mordaHQ/create-sdk/actions/workflows/ci.yml/badge.svg)
2
5
 
3
- Scaffold a production-ready TypeScript SDK with a strict public API.
6
+ > Scaffold a **production-ready TypeScript SDK** with a **strict public API**.
7
+
8
+ `create-sdk` is a CLI tool that bootstraps a modern TypeScript SDK with
9
+ **ESM-first setup**, **API Extractor**, and **CI-ready structure** — so you
10
+ can focus on features, not boilerplate.
11
+
12
+ ---
13
+
14
+ ## ✨ Features
15
+
16
+ - ⚡ **Interactive CLI** (or fully automated with flags)
17
+ - 📦 **ESM-first TypeScript setup**
18
+ - 🔒 **Strict public API** via API Extractor
19
+ - 🧪 **CI-verified & smoke-tested**
20
+ - 🧱 Clean, scalable project structure
21
+ - 🚫 No hidden magic, no vendor lock-in
22
+
23
+ ---
4
24
 
5
25
  ## 🚀 Quick start
6
26
 
27
+ This creates a clean SDK with a strict public API enforced by API Extractor.
28
+
7
29
  ```bash
8
30
  npx @morda-dev/create-sdk my-sdk
9
31
  cd my-sdk
@@ -11,41 +33,91 @@ npm install
11
33
  npm run build
12
34
  ```
13
35
 
14
- ## What you get
36
+ That’s it. You now have a production-ready SDK.
37
+
38
+ ## 📘 Example SDK
39
+
40
+ See a minimal, production-style SDK generated with **create-sdk**:
41
+
42
+ 👉 https://github.com/mordaHQ/example-sdk
43
+
44
+ This example demonstrates:
45
+ - a single, explicit public API entrypoint (`src/index.ts`)
46
+ - API Extractor–controlled surface
47
+ - ESM-first configuration
48
+ - CI-ready setup
49
+ - safe evolution without accidental breaking changes
50
+
51
+ Use it as a reference for structuring real-world SDKs.
52
+
53
+ ## ❓ Why create-sdk?
54
+
55
+ Most SDK starters fall into one of two traps:
56
+
57
+ - ❌ too minimal — no real production setup
58
+ - ❌ too complex — opinionated, hard to extend
15
59
 
16
- TypeScript + ESM out of the box
60
+ **create-sdk** sits in the middle:
17
61
 
18
- 🔒 Strict public API enforcement (API Extractor)
62
+ > Strict where it matters. Flexible where it should be.
19
63
 
20
- 🧪 CI-ready project structure
64
+ You get:
21
65
 
22
- 📦 Clean SDK architecture
66
+ - a clearly defined public API
67
+ - predictable build & release flow
68
+ - freedom to grow the SDK your way
23
69
 
24
- 🚫 No accidental exports
70
+ ## 🧩 What’s included
25
71
 
26
- ## 📁 Project structure
27
- src/
28
- index.ts # public API surface
29
- modules/ # internal implementation
30
- types/ # public types
72
+ - TypeScript project with strict config
73
+ - API Extractor setup
74
+ - Ready-to-use CI workflow
75
+ - Clean module-based structure
76
+ - Example public API and types
31
77
 
32
- Only symbols exported from src/index.ts are considered public.
78
+ ## 🛠 CLI options
33
79
 
34
- ## 📜 Available scripts
35
- npm run build # build the SDK
36
- npm run api:check # validate public API
80
+ ```bash
81
+ npx @morda-dev/create-sdk <name> [options]
82
+ ```
83
+
84
+ ### Options
85
+
86
+ - `--yes` — skip all prompts
87
+ - `--no-install` — skip dependency installation
88
+ - `--no-git` — skip git initialization
89
+
90
+ ### Example
91
+
92
+ ```bash
93
+ npx @morda-dev/create-sdk my-sdk --yes --no-install
94
+ ```
95
+
96
+ ## 📦 Requirements
97
+
98
+ - Node.js >= 18
99
+ - npm / pnpm / yarn
100
+
101
+ ## ✅ Used in production
37
102
 
38
- ## 🧠 Philosophy
103
+ This template is used to scaffold and maintain real-world TypeScript SDKs.
39
104
 
40
- This CLI helps you build SDKs with a stable and explicit public API.
41
105
 
42
- Anything not exported from src/index.ts is treated as private by default.
43
- This prevents accidental breaking changes and enforces long-term stability.
106
+ ## 🧭 Roadmap
44
107
 
45
- ## ⚙️ Requirements
108
+ - Custom templates
109
+ - Optional monorepo mode
110
+ - Plugin system
111
+ - SDK release automation helpers
46
112
 
47
- Node.js >= 18
113
+ ## 🤝 Contributing
114
+
115
+ Issues and PRs are welcome.
116
+ This project is intentionally kept small, clean, and well-documented.
48
117
 
49
118
  ## 📄 License
50
119
 
51
- ISC
120
+ ISC
121
+
122
+
123
+ ---
package/bin/create-sdk.js CHANGED
@@ -1,17 +1,12 @@
1
1
  #!/usr/bin/env node
2
-
3
- import fs from "fs";
4
- import path from "path";
5
2
  import process from "process";
6
- import { fileURLToPath } from "url";
7
- import { execSync } from "child_process";
8
3
  import prompts from "prompts";
4
+ import { scaffold } from "../src/cli/scaffold.js";
9
5
 
10
6
  /* ============================================================================
11
- * Environment checks
7
+ * Node version check
12
8
  * ============================================================================
13
9
  */
14
-
15
10
  const REQUIRED_NODE_MAJOR = 18;
16
11
  const CURRENT_NODE_MAJOR = Number(process.versions.node.split(".")[0]);
17
12
 
@@ -24,67 +19,30 @@ if (CURRENT_NODE_MAJOR < REQUIRED_NODE_MAJOR) {
24
19
  }
25
20
 
26
21
  /* ============================================================================
27
- * CLI args & flags
22
+ * Args parsing
28
23
  * ============================================================================
29
24
  */
30
-
31
25
  const rawArgs = process.argv.slice(2);
32
26
 
33
27
  const flags = {
34
28
  yes: rawArgs.includes("--yes"),
35
29
  noGit: rawArgs.includes("--no-git"),
36
30
  noInstall: rawArgs.includes("--no-install"),
31
+ dryRun: rawArgs.includes("--dry-run"),
32
+ force: rawArgs.includes("--force"),
37
33
  };
38
34
 
39
- if (rawArgs.includes("--version") || rawArgs.includes("-v")) {
40
- console.log("create-sdk v1.1.0");
41
- process.exit(0);
42
- }
43
-
44
- if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
45
- console.log(`
46
- create-sdk — scaffold a production-ready TypeScript SDK
47
-
48
- Usage:
49
- npx @morda-dev/create-sdk <project-name>
50
-
51
- Options:
52
- --yes Skip prompts and use defaults
53
- --no-git Do not initialize git repository
54
- --no-install Do not install dependencies
55
- --help, -h Show this help
56
- --version, -v Show version
57
-
58
- Example:
59
- npx @morda-dev/create-sdk my-sdk
60
- npx @morda-dev/create-sdk my-sdk --yes --no-install
61
- `);
62
- process.exit(0);
63
- }
64
-
65
- /* ============================================================================
66
- * Paths
67
- * ============================================================================
68
- */
69
-
70
- const __filename = fileURLToPath(import.meta.url);
71
- const __dirname = path.dirname(__filename);
72
- const templateDir = path.resolve(__dirname, "../template");
35
+ const projectName = rawArgs.find((a) => !a.startsWith("-"));
73
36
 
74
37
  /* ============================================================================
75
- * Project name & interactive flow
38
+ * Interactive mode
76
39
  * ============================================================================
77
40
  */
78
-
79
- const argProjectName = rawArgs.find((a) => !a.startsWith("-"));
80
-
81
- let projectName;
41
+ let name = projectName;
82
42
  let initGit = !flags.noGit;
83
43
  let installDeps = !flags.noInstall;
84
44
 
85
- if (flags.yes && argProjectName) {
86
- projectName = argProjectName;
87
- } else if (!argProjectName) {
45
+ if (!flags.yes && !name) {
88
46
  const answers = await prompts(
89
47
  [
90
48
  {
@@ -114,148 +72,24 @@ if (flags.yes && argProjectName) {
114
72
  }
115
73
  );
116
74
 
117
- projectName = answers.name;
75
+ name = answers.name;
118
76
  initGit = answers.initGit;
119
77
  installDeps = answers.installDeps;
120
- } else {
121
- projectName = argProjectName;
122
78
  }
123
79
 
124
- if (!projectName) {
80
+ if (!name) {
125
81
  console.error("❌ Project name is required");
126
82
  process.exit(1);
127
83
  }
128
84
 
129
85
  /* ============================================================================
130
- * Target directory
131
- * ============================================================================
132
- */
133
-
134
- const dirName = projectName.startsWith("@")
135
- ? projectName.split("/")[1]
136
- : projectName;
137
-
138
- const targetDir = path.resolve(process.cwd(), dirName);
139
-
140
- /* ============================================================================
141
- * Safety checks
142
- * ============================================================================
143
- */
144
-
145
- if (!fs.existsSync(templateDir)) {
146
- console.error("❌ Template directory not found:", templateDir);
147
- process.exit(1);
148
- }
149
-
150
- if (fs.existsSync(targetDir)) {
151
- console.error(`❌ Directory "${dirName}" already exists`);
152
- process.exit(1);
153
- }
154
-
155
- /* ============================================================================
156
- * Scaffold project
157
- * ============================================================================
158
- */
159
-
160
- console.log("🚀 Creating SDK:", projectName);
161
-
162
- copyDir(templateDir, targetDir);
163
-
164
- /* ============================================================================
165
- * Update package.json
86
+ * Execute scaffold
166
87
  * ============================================================================
167
88
  */
168
-
169
- const pkgPath = path.join(targetDir, "package.json");
170
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
171
-
172
- pkg.name = projectName;
173
- pkg.version = "0.1.0";
174
- delete pkg.private;
175
-
176
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
177
-
178
- /* ============================================================================
179
- * Update README title
180
- * ============================================================================
181
- */
182
-
183
- const readmePath = path.join(targetDir, "README.md");
184
-
185
- if (fs.existsSync(readmePath)) {
186
- const updated = fs
187
- .readFileSync(readmePath, "utf8")
188
- .replace(/^# .*/m, `# ${projectName}`);
189
-
190
- fs.writeFileSync(readmePath, updated);
191
- }
192
-
193
- /* ============================================================================
194
- * Git init
195
- * ============================================================================
196
- */
197
-
198
- if (initGit) {
199
- try {
200
- execSync("git init", { cwd: targetDir, stdio: "ignore" });
201
- } catch {}
202
- }
203
-
204
- /* ============================================================================
205
- * Install dependencies
206
- * ============================================================================
207
- */
208
-
209
- if (installDeps) {
210
- try {
211
- execSync("npm install", { cwd: targetDir, stdio: "inherit" });
212
- } catch {
213
- console.warn("⚠️ Failed to install dependencies");
214
- }
215
- }
216
-
217
- /* ============================================================================
218
- * Done
219
- * ============================================================================
220
- */
221
-
222
- console.log("");
223
- console.log("🎉 SDK scaffolded successfully!");
224
- console.log("");
225
- console.log("Next steps:");
226
- console.log(` cd ${dirName}`);
227
- if (!installDeps) console.log(" npm install");
228
- console.log(" npm run build");
229
- console.log("");
230
- console.log("Happy hacking 🚀");
231
-
232
- /* ============================================================================
233
- * Helpers
234
- * ============================================================================
235
- */
236
-
237
- function copyDir(src, dest) {
238
- fs.mkdirSync(dest, { recursive: true });
239
-
240
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
241
- if (shouldIgnore(entry.name)) continue;
242
-
243
- const srcPath = path.join(src, entry.name);
244
- const destPath = path.join(dest, entry.name);
245
-
246
- if (entry.isDirectory()) {
247
- copyDir(srcPath, destPath);
248
- } else {
249
- fs.copyFileSync(srcPath, destPath);
250
- }
251
- }
252
- }
253
-
254
- function shouldIgnore(name) {
255
- return (
256
- name === "node_modules" ||
257
- name === ".git" ||
258
- name === "dist" ||
259
- name === ".DS_Store"
260
- );
261
- }
89
+ await scaffold({
90
+ projectName: name,
91
+ initGit,
92
+ installDeps,
93
+ dryRun: flags.dryRun,
94
+ force: flags.force,
95
+ });
package/package.json CHANGED
@@ -1,27 +1,36 @@
1
1
  {
2
2
  "name": "@morda-dev/create-sdk",
3
- "version": "1.1.1",
3
+ "version": "1.1.4",
4
4
  "description": "CLI to scaffold a production-ready TypeScript SDK with a strict public API",
5
+ "license": "ISC",
5
6
  "type": "module",
7
+
6
8
  "bin": {
7
9
  "create-sdk": "./bin/create-sdk.js"
8
10
  },
11
+
9
12
  "files": [
10
13
  "bin",
14
+ "src",
11
15
  "template",
12
- "README.md"
16
+ "README.md",
17
+ "LICENSE"
13
18
  ],
19
+
14
20
  "engines": {
15
21
  "node": ">=18"
16
22
  },
23
+
17
24
  "repository": {
18
25
  "type": "git",
19
26
  "url": "https://github.com/mordaHQ/create-sdk.git"
20
27
  },
28
+
21
29
  "homepage": "https://github.com/mordaHQ/create-sdk",
22
30
  "bugs": {
23
31
  "url": "https://github.com/mordaHQ/create-sdk/issues"
24
32
  },
33
+
25
34
  "keywords": [
26
35
  "create-sdk",
27
36
  "typescript",
@@ -29,13 +38,21 @@
29
38
  "cli",
30
39
  "scaffold",
31
40
  "api-extractor",
32
- "library"
41
+ "library",
42
+ "esm",
43
+ "developer-tools"
33
44
  ],
45
+
34
46
  "scripts": {
35
47
  "test:smoke": "node ./bin/create-sdk.js __tmp-smoke-sdk --yes --no-install --no-git && node -e \"import('fs').then(fs => fs.rmSync('__tmp-smoke-sdk', { recursive: true, force: true }))\""
36
48
  },
49
+
37
50
  "dependencies": {
38
51
  "prompts": "^2.4.2"
39
52
  },
40
- "license": "ISC"
53
+
54
+ "devDependencies": {
55
+ "@types/node": "^18.19.0",
56
+ "undici-types": "^7.16.0"
57
+ }
41
58
  }
@@ -0,0 +1,14 @@
1
+ export function parseArgs(rawArgs) {
2
+ const flags = {
3
+ yes: rawArgs.includes("--yes"),
4
+ noGit: rawArgs.includes("--no-git"),
5
+ noInstall: rawArgs.includes("--no-install"),
6
+ };
7
+
8
+ const projectName = rawArgs.find((a) => !a.startsWith("-"));
9
+
10
+ return {
11
+ projectName,
12
+ flags,
13
+ };
14
+ }
@@ -0,0 +1,2 @@
1
+ // reserved for future prompt extensions
2
+ export {};
package/src/cli/run.js ADDED
@@ -0,0 +1,80 @@
1
+ import prompts from "prompts";
2
+ import { parseArgs } from "./args.js";
3
+ import { scaffold } from "./scaffold.js";
4
+
5
+ export async function run(rawArgs) {
6
+ const { projectName, flags } = parseArgs(rawArgs);
7
+
8
+ const dryRun = flags.dryRun === true;
9
+
10
+ // ─────────────────────────────────────────────
11
+ // DRY-RUN MODE — NO INTERACTION, NO SIDE EFFECTS
12
+ // ─────────────────────────────────────────────
13
+ if (dryRun && !projectName) {
14
+ throw new Error("Project name is required when using --dry-run");
15
+ }
16
+
17
+ let name = projectName;
18
+ let initGit = !flags.noGit;
19
+ let installDeps = !flags.noInstall;
20
+ const force = flags.force === true;
21
+
22
+ // ❗❗❗ DRY-RUN MUST OVERRIDE EVERYTHING
23
+ if (dryRun) {
24
+ initGit = false;
25
+ installDeps = false;
26
+ }
27
+
28
+ // ─────────────────────────────────────────────
29
+ // Interactive mode (ONLY if not dry-run)
30
+ // ─────────────────────────────────────────────
31
+ if (!dryRun && !flags.yes && !projectName) {
32
+ const answers = await prompts(
33
+ [
34
+ {
35
+ type: "text",
36
+ name: "name",
37
+ message: "Project name:",
38
+ validate: (v) => (v ? true : "Project name is required"),
39
+ },
40
+ {
41
+ type: "confirm",
42
+ name: "initGit",
43
+ message: "Initialize git repository?",
44
+ initial: true,
45
+ },
46
+ {
47
+ type: "confirm",
48
+ name: "installDeps",
49
+ message: "Install dependencies now?",
50
+ initial: false,
51
+ },
52
+ ],
53
+ {
54
+ onCancel: () => {
55
+ console.log("\n❌ Cancelled");
56
+ process.exit(1);
57
+ },
58
+ }
59
+ );
60
+
61
+ name = answers.name;
62
+ initGit = answers.initGit;
63
+ installDeps = answers.installDeps;
64
+ }
65
+
66
+ if (!name) {
67
+ throw new Error("Project name is required");
68
+ }
69
+
70
+ // ─────────────────────────────────────────────
71
+ // Scaffold (SINGLE ENTRY POINT)
72
+ // ─────────────────────────────────────────────
73
+ await scaffold({
74
+ projectName: name,
75
+ initGit,
76
+ installDeps,
77
+ dryRun,
78
+ force,
79
+ });
80
+ }
@@ -0,0 +1,107 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { execSync } from "child_process";
4
+ import { fileURLToPath } from "url";
5
+ import { copyDir } from "./utils.js";
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const templateDir = path.resolve(__dirname, "../../template");
10
+
11
+ export async function scaffold({
12
+ projectName,
13
+ initGit = true,
14
+ installDeps = false,
15
+ dryRun = false,
16
+ force = false,
17
+ }) {
18
+ if (!projectName) {
19
+ throw new Error("Project name is required");
20
+ }
21
+
22
+ const dirName = projectName.startsWith("@")
23
+ ? projectName.split("/")[1]
24
+ : projectName;
25
+
26
+ const targetDir = path.resolve(process.cwd(), dirName);
27
+
28
+ // ─────────────────────────────────────────────
29
+ // 🧪 DRY-RUN — ABSOLUTE NO-SIDE-EFFECT MODE
30
+ // ─────────────────────────────────────────────
31
+ if (dryRun) {
32
+ console.log("🧪 [dry-run] Creating SDK:", projectName);
33
+ console.log("🧪 [dry-run] Target directory:", path.join("<cwd>", dirName));
34
+ console.log("🧪 [dry-run] Would copy template from:", templateDir);
35
+ if (initGit) console.log("🧪 [dry-run] Would init git");
36
+ if (installDeps) console.log("🧪 [dry-run] Would install dependencies");
37
+ console.log("🧪 [dry-run] Done (NO filesystem changes)");
38
+ return;
39
+ }
40
+
41
+ // ─────────────────────────────────────────────
42
+ // REAL MODE — FILESYSTEM IS TOUCHED ONLY HERE
43
+ // ─────────────────────────────────────────────
44
+ if (!fs.existsSync(templateDir)) {
45
+ throw new Error(`Template directory not found: ${templateDir}`);
46
+ }
47
+
48
+ if (fs.existsSync(targetDir)) {
49
+ if (!force) {
50
+ throw new Error(`Directory "${dirName}" already exists`);
51
+ }
52
+ fs.rmSync(targetDir, { recursive: true, force: true });
53
+ }
54
+
55
+ console.log("🚀 Creating SDK:", projectName);
56
+
57
+ // copy template
58
+ copyDir(templateDir, targetDir);
59
+
60
+ // package.json
61
+ const pkgPath = path.join(targetDir, "package.json");
62
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
63
+
64
+ pkg.name = projectName;
65
+ pkg.version = "0.1.0";
66
+
67
+ // шаблон НЕ должен быть private
68
+ delete pkg.private;
69
+
70
+ // exports добавляются ТОЛЬКО тут
71
+ pkg.main = "./dist/index.js";
72
+ pkg.types = "./dist/index.d.ts";
73
+ pkg.exports = {
74
+ ".": {
75
+ import: "./dist/index.js",
76
+ types: "./dist/index.d.ts",
77
+ },
78
+ };
79
+
80
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
81
+
82
+ // README
83
+ const readmePath = path.join(targetDir, "README.md");
84
+ if (fs.existsSync(readmePath)) {
85
+ const updated = fs
86
+ .readFileSync(readmePath, "utf8")
87
+ .replace(/^# .*/m, `# ${projectName}`);
88
+ fs.writeFileSync(readmePath, updated);
89
+ }
90
+
91
+ // git
92
+ if (initGit) {
93
+ try {
94
+ execSync("git init", { cwd: targetDir, stdio: "ignore" });
95
+ } catch {}
96
+ }
97
+
98
+ // deps
99
+ if (installDeps) {
100
+ execSync("npm install", {
101
+ cwd: targetDir,
102
+ stdio: "inherit",
103
+ });
104
+ }
105
+
106
+ console.log("🎉 SDK scaffolded successfully!");
107
+ }
@@ -0,0 +1,28 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ export function copyDir(src, dest) {
5
+ fs.mkdirSync(dest, { recursive: true });
6
+
7
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
8
+ if (shouldIgnore(entry.name)) continue;
9
+
10
+ const srcPath = path.join(src, entry.name);
11
+ const destPath = path.join(dest, entry.name);
12
+
13
+ if (entry.isDirectory()) {
14
+ copyDir(srcPath, destPath);
15
+ } else {
16
+ fs.copyFileSync(srcPath, destPath);
17
+ }
18
+ }
19
+ }
20
+
21
+ function shouldIgnore(name) {
22
+ return (
23
+ name === "node_modules" ||
24
+ name === ".git" ||
25
+ name === "dist" ||
26
+ name === ".DS_Store"
27
+ );
28
+ }
@@ -5,21 +5,6 @@
5
5
  "license": "ISC",
6
6
  "type": "module",
7
7
 
8
- "main": "./dist/index.js",
9
- "types": "./dist/index.d.ts",
10
-
11
- "exports": {
12
- ".": {
13
- "import": "./dist/index.js",
14
- "types": "./dist/index.d.ts"
15
- }
16
- },
17
-
18
- "files": [
19
- "dist",
20
- "etc"
21
- ],
22
-
23
8
  "sideEffects": false,
24
9
 
25
10
  "engines": {