create-npkg 0.0.1

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/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "create-npkg",
3
+ "version": "0.0.1",
4
+ "description": "Create a new TypeScript npm package with best practices",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-npkg": "./dist/src/index.js"
8
+ },
9
+ "exports": "./dist/index.js",
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "prepublishOnly": "npm run build",
14
+ "test": "node --test dist/test/**/*.test.js",
15
+ "test:watch": "node --test --watch dist/test/**/*.test.js"
16
+ },
17
+ "keywords": [
18
+ "create",
19
+ "typescript",
20
+ "npm",
21
+ "package",
22
+ "scaffold",
23
+ "generator"
24
+ ],
25
+ "author": "",
26
+ "license": "MIT",
27
+ "engines": {
28
+ "node": ">=18.0.0"
29
+ },
30
+ "dependencies": {
31
+ "prompts": "^2.4.2",
32
+ "kleur": "^4.1.5",
33
+ "ora": "^6.3.1",
34
+ "fs-extra": "^11.2.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^20.11.0",
38
+ "@types/fs-extra": "^11.0.4",
39
+ "@types/prompts": "^2.4.9",
40
+ "typescript": "^5.3.3"
41
+ }
42
+ }
package/src/index.ts ADDED
@@ -0,0 +1,468 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * create-npkg - Create a new TypeScript npm package
5
+ */
6
+
7
+ import fs from 'fs-extra';
8
+ import path from 'path';
9
+ import prompts from 'prompts';
10
+ import kleur from 'kleur';
11
+ import ora from 'ora';
12
+
13
+ export interface Options {
14
+ pkgName: string;
15
+ description: string;
16
+ author: string;
17
+ license: string;
18
+ includeTests: boolean;
19
+ initGit: boolean;
20
+ installDeps: boolean;
21
+ }
22
+
23
+ export interface TemplateContext {
24
+ name: string;
25
+ description: string;
26
+ author: string;
27
+ license: string;
28
+ includeTests: boolean;
29
+ year: number;
30
+ }
31
+
32
+ /**
33
+ * Validate package name according to npm naming rules
34
+ */
35
+ export function validatePackageName(name: string): { valid: boolean; error?: string } {
36
+ if (!name) {
37
+ return { valid: false, error: 'Package name is required' };
38
+ }
39
+ if (!/^[a-z0-9-]+$/.test(name)) {
40
+ return { valid: false, error: 'Package name must be lowercase, alphanumeric, and may contain hyphens' };
41
+ }
42
+ return { valid: true };
43
+ }
44
+
45
+ /**
46
+ * Main CLI entry point
47
+ */
48
+ export async function main(): Promise<void> {
49
+ console.log('');
50
+ console.log(kleur.cyan('╔═══════════════════════════════════════╗'));
51
+ console.log(kleur.cyan('║') + kleur.bold(' create-npkg ') + kleur.cyan(' ║'));
52
+ console.log(kleur.cyan('║') + kleur.dim(' Create a TypeScript npm package ') + kleur.cyan('║'));
53
+ console.log(kleur.cyan('╚═══════════════════════════════════════╝'));
54
+ console.log('');
55
+
56
+ // Get package name from args or prompt
57
+ const args = process.argv.slice(2);
58
+ let pkgName = args[0];
59
+
60
+ // Check for flags
61
+ const noGitIndex = args.indexOf('--no-git');
62
+ const noInstallIndex = args.indexOf('--no-install');
63
+ const yesIndex = args.indexOf('--yes') !== -1 || args.indexOf('-y') !== -1;
64
+
65
+ const initGit = noGitIndex === -1;
66
+ const installDeps = noInstallIndex === -1;
67
+ const useDefaults = yesIndex;
68
+
69
+ // Remove flags from args if present
70
+ if (noGitIndex > 0) args.splice(noGitIndex, 1);
71
+ if (noInstallIndex > 0) args.splice(noInstallIndex, 1);
72
+
73
+ // Prompt for package name if not provided
74
+ if (!pkgName) {
75
+ const response = await prompts({
76
+ type: 'text',
77
+ name: 'pkgName',
78
+ message: 'What is your package name?',
79
+ initial: 'my-package',
80
+ validate: (value: string) => {
81
+ if (!value) return 'Package name is required';
82
+ if (!/^[a-z0-9-]+$/.test(value)) {
83
+ return 'Package name must be lowercase, alphanumeric, and may contain hyphens';
84
+ }
85
+ if (fs.existsSync(path.join(process.cwd(), value))) {
86
+ return `Directory "${value}" already exists`;
87
+ }
88
+ return true;
89
+ }
90
+ });
91
+ pkgName = response.pkgName;
92
+ }
93
+
94
+ if (!pkgName) {
95
+ console.error(kleur.red('✖ Package name is required'));
96
+ process.exit(1);
97
+ }
98
+
99
+ // Validate package name
100
+ if (!/^[a-z0-9-]+$/.test(pkgName)) {
101
+ console.error(kleur.red('✖ Package name must be lowercase, alphanumeric, and may contain hyphens'));
102
+ process.exit(1);
103
+ }
104
+
105
+ // Check if directory exists
106
+ const targetDir = path.join(process.cwd(), pkgName);
107
+ if (fs.existsSync(targetDir)) {
108
+ console.error(kleur.red(`✖ Directory "${pkgName}" already exists`));
109
+ process.exit(1);
110
+ }
111
+
112
+ // Prompt for additional options (or use defaults)
113
+ const responses = useDefaults ? {
114
+ description: 'A TypeScript package',
115
+ author: '',
116
+ license: 'MIT',
117
+ includeTests: false
118
+ } : await prompts(
119
+ [
120
+ {
121
+ type: 'text',
122
+ name: 'description',
123
+ message: 'Package description:',
124
+ initial: 'A TypeScript package'
125
+ },
126
+ {
127
+ type: 'text',
128
+ name: 'author',
129
+ message: 'Author:',
130
+ initial: ''
131
+ },
132
+ {
133
+ type: 'select',
134
+ name: 'license',
135
+ message: 'License:',
136
+ choices: [
137
+ { title: 'MIT', value: 'MIT' },
138
+ { title: 'Apache-2.0', value: 'Apache-2.0' },
139
+ { title: 'ISC', value: 'ISC' },
140
+ { title: 'BSD-3-Clause', value: 'BSD-3-Clause' }
141
+ ],
142
+ initial: 0
143
+ },
144
+ {
145
+ type: 'confirm',
146
+ name: 'includeTests',
147
+ message: 'Include testing setup?',
148
+ initial: false
149
+ }
150
+ ],
151
+ {
152
+ onCancel: () => {
153
+ console.log(kleur.yellow('✖ Operation cancelled'));
154
+ process.exit(0);
155
+ }
156
+ }
157
+ );
158
+
159
+ const options: Options = {
160
+ pkgName,
161
+ description: responses.description || 'A TypeScript package',
162
+ author: responses.author || '',
163
+ license: responses.license || 'MIT',
164
+ includeTests: responses.includeTests,
165
+ initGit,
166
+ installDeps
167
+ };
168
+
169
+ try {
170
+ await createPackage(options);
171
+ } catch (error) {
172
+ if (error instanceof Error) {
173
+ console.error(kleur.red(`✖ ${error.message}`));
174
+ }
175
+ process.exit(1);
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Create the package structure
181
+ */
182
+ export async function createPackage(options: Options, cwd: string = process.cwd()): Promise<void> {
183
+ const { pkgName } = options;
184
+ const targetDir = path.join(cwd, pkgName);
185
+
186
+ const spinner = ora('Creating package structure...').start();
187
+
188
+ try {
189
+ // Create directory
190
+ await fs.ensureDir(targetDir);
191
+ await fs.ensureDir(path.join(targetDir, 'src'));
192
+
193
+ // Template context
194
+ const context: TemplateContext = {
195
+ name: pkgName,
196
+ description: options.description,
197
+ author: options.author,
198
+ license: options.license,
199
+ includeTests: options.includeTests,
200
+ year: new Date().getFullYear()
201
+ };
202
+
203
+ // Write package.json
204
+ const pkgJson = generatePackageJson(context);
205
+ await fs.writeJSON(path.join(targetDir, 'package.json'), pkgJson, { spaces: 2 });
206
+
207
+ // Write tsconfig.json
208
+ const tsConfig = generateTsConfig(context);
209
+ await fs.writeJSON(path.join(targetDir, 'tsconfig.json'), tsConfig, { spaces: 2 });
210
+
211
+ // Write .gitignore
212
+ await fs.writeFile(path.join(targetDir, '.gitignore'), generateGitignore());
213
+
214
+ // Write README.md
215
+ await fs.writeFile(path.join(targetDir, 'README.md'), generateReadme(context));
216
+
217
+ // Write src/index.ts
218
+ await fs.writeFile(path.join(targetDir, 'src/index.ts'), generateIndexFile(context));
219
+
220
+ // Write test files if requested
221
+ if (options.includeTests) {
222
+ await fs.ensureDir(path.join(targetDir, 'tests'));
223
+ await fs.writeFile(path.join(targetDir, 'tests/index.test.ts'), generateTestFile(context));
224
+ }
225
+
226
+ spinner.succeed('Package structure created');
227
+
228
+ // Initialize git if requested
229
+ if (options.initGit) {
230
+ spinner.start('Initializing git repository');
231
+ const { spawn } = await import('child_process');
232
+ await new Promise<void>((resolve, reject) => {
233
+ const git = spawn('git', ['init'], { cwd: targetDir });
234
+ git.on('close', (code) => {
235
+ if (code === 0) resolve();
236
+ else reject(new Error('Git initialization failed'));
237
+ });
238
+ });
239
+ spinner.succeed('Git repository initialized');
240
+ }
241
+
242
+ // Install dependencies if requested
243
+ if (options.installDeps) {
244
+ spinner.start('Installing dependencies');
245
+ const { spawn } = await import('child_process');
246
+ await new Promise<void>((resolve, reject) => {
247
+ const npm = spawn('npm', ['install'], {
248
+ cwd: targetDir,
249
+ stdio: 'inherit'
250
+ });
251
+ npm.on('close', (code) => {
252
+ if (code === 0) resolve();
253
+ else reject(new Error('Dependency installation failed'));
254
+ });
255
+ });
256
+ spinner.succeed('Dependencies installed');
257
+ }
258
+
259
+ // Print success message
260
+ console.log('');
261
+ console.log(kleur.green('✓ Package created successfully!'));
262
+ console.log('');
263
+ console.log(kleur.bold('Next steps:'));
264
+ console.log(` ${kleur.cyan(`cd ${pkgName}`)}`);
265
+ console.log(` ${kleur.cyan('npm run dev')}`);
266
+ console.log('');
267
+
268
+ } catch (error) {
269
+ spinner.fail('Failed to create package');
270
+ throw error;
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Generate package.json
276
+ */
277
+ export function generatePackageJson(context: TemplateContext): Record<string, any> {
278
+ const devDependencies: Record<string, string> = {
279
+ '@types/node': '^20.11.0',
280
+ 'typescript': '^5.3.3'
281
+ };
282
+
283
+ if (context.includeTests) {
284
+ devDependencies['@types/node'] = '^20.11.0';
285
+ }
286
+
287
+ return {
288
+ name: context.name,
289
+ version: '1.0.0',
290
+ type: 'module',
291
+ description: context.description,
292
+ main: './dist/index.js',
293
+ types: './dist/index.d.ts',
294
+ exports: {
295
+ '.': {
296
+ import: './dist/index.js',
297
+ types: './dist/index.d.ts'
298
+ }
299
+ },
300
+ files: ['dist'],
301
+ scripts: {
302
+ build: 'tsc',
303
+ dev: 'tsc --watch',
304
+ prepublishOnly: 'npm run build',
305
+ test: context.includeTests ? 'node --test' : undefined
306
+ },
307
+ keywords: [],
308
+ author: context.author,
309
+ license: context.license,
310
+ engines: {
311
+ node: '>=18.0.0'
312
+ },
313
+ devDependencies
314
+ };
315
+ }
316
+
317
+ /**
318
+ * Generate tsconfig.json
319
+ */
320
+ export function generateTsConfig(_context: TemplateContext): Record<string, any> {
321
+ return {
322
+ compilerOptions: {
323
+ target: 'ES2022',
324
+ module: 'NodeNext',
325
+ moduleResolution: 'NodeNext',
326
+ lib: ['ES2022'],
327
+ declaration: true,
328
+ declarationMap: true,
329
+ sourceMap: true,
330
+ outDir: './dist',
331
+ rootDir: './src',
332
+ strict: true,
333
+ noImplicitAny: true,
334
+ strictNullChecks: true,
335
+ strictFunctionTypes: true,
336
+ strictBindCallApply: true,
337
+ strictPropertyInitialization: true,
338
+ noImplicitThis: true,
339
+ alwaysStrict: true,
340
+ noUnusedLocals: true,
341
+ noUnusedParameters: true,
342
+ noImplicitReturns: true,
343
+ noFallthroughCasesInSwitch: true,
344
+ esModuleInterop: true,
345
+ skipLibCheck: true,
346
+ forceConsistentCasingInFileNames: true,
347
+ resolveJsonModule: true,
348
+ paths: {
349
+ '@src/*': ['./src/*']
350
+ }
351
+ },
352
+ include: ['src/**/*'],
353
+ exclude: ['node_modules', 'dist', '**/*.test.ts']
354
+ };
355
+ }
356
+
357
+ /**
358
+ * Generate .gitignore
359
+ */
360
+ export function generateGitignore(): string {
361
+ return `# Dependencies
362
+ node_modules/
363
+
364
+ # Build output
365
+ dist/
366
+
367
+ # IDE
368
+ .idea/
369
+ .vscode/
370
+ *.swp
371
+ *.swo
372
+
373
+ # OS
374
+ .DS_Store
375
+ Thumbs.db
376
+
377
+ # Logs
378
+ *.log
379
+ npm-debug.log*
380
+ yarn-debug.log*
381
+ yarn-error.log*
382
+
383
+ # Testing
384
+ coverage/
385
+ .nyc_output/
386
+
387
+ # Environment
388
+ .env
389
+ .env.local
390
+ `;
391
+ }
392
+
393
+ /**
394
+ * Generate README.md
395
+ */
396
+ export function generateReadme(context: TemplateContext): string {
397
+ return `# ${context.name}
398
+
399
+ ${context.description}
400
+
401
+ ## Installation
402
+
403
+ \`\`\`bash
404
+ npm install ${context.name}
405
+ \`\`\`
406
+
407
+ ## Usage
408
+
409
+ \`\`\`typescript
410
+ import { something } from '${context.name}';
411
+
412
+ // Your code here
413
+ \`\`\`
414
+
415
+ ## Development
416
+
417
+ \`\`\`bash
418
+ # Install dependencies
419
+ npm install
420
+
421
+ # Build
422
+ npm run build
423
+
424
+ # Watch mode
425
+ npm run dev
426
+ \`\`\`
427
+
428
+ ## License
429
+
430
+ ${context.license} © ${context.year} ${context.author ? context.author : 'Contributors'}
431
+ `;
432
+ }
433
+
434
+ /**
435
+ * Generate src/index.ts
436
+ */
437
+ export function generateIndexFile(_context: TemplateContext): string {
438
+ return `/**
439
+ * Main entry point for ${_context.name}
440
+ */
441
+
442
+ export function hello(name: string): string {
443
+ return \`Hello, \${name}!\`;
444
+ }
445
+
446
+ export default hello;
447
+ `;
448
+ }
449
+
450
+ /**
451
+ * Generate test file
452
+ */
453
+ export function generateTestFile(_context: TemplateContext): string {
454
+ return `import { test } from 'node:test';
455
+ import assert from 'node:assert';
456
+ import { hello } from '../src/index.js';
457
+
458
+ test('hello function', () => {
459
+ const result = hello('World');
460
+ assert.strictEqual(result, 'Hello, World!');
461
+ });
462
+ `;
463
+ }
464
+
465
+ // Run if called directly
466
+ if (import.meta.url === `file://${process.argv[1]}`) {
467
+ main();
468
+ }