@djangocfg/ext-base 1.0.1 → 1.0.3

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/src/cli/index.ts CHANGED
@@ -7,9 +7,11 @@
7
7
  import { consola } from 'consola';
8
8
  import chalk from 'chalk';
9
9
  import prompts from 'prompts';
10
- import { readFileSync } from 'fs';
10
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from 'fs';
11
11
  import { join, dirname } from 'path';
12
12
  import { fileURLToPath } from 'url';
13
+ import { execSync } from 'child_process';
14
+ import { EXTENSION_CATEGORIES } from '../types/context';
13
15
 
14
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
17
 
@@ -26,43 +28,62 @@ function getVersion(): string {
26
28
 
27
29
  // CLI Commands
28
30
  const COMMANDS = {
29
- install: 'Install an extension',
30
- list: 'List available extensions',
31
- info: 'Show extension info',
32
- init: 'Initialize extension in your project',
31
+ create: 'Create a new DjangoCFG extension',
33
32
  help: 'Show help',
33
+ '--help': 'Show help',
34
+ '-h': 'Show help',
34
35
  } as const;
35
36
 
36
37
  type Command = keyof typeof COMMANDS;
37
38
 
38
- // Available extensions
39
- const EXTENSIONS = {
40
- 'ext-leads': {
41
- name: '@djangocfg/ext-leads',
42
- description: 'Lead management and contact forms',
43
- features: ['Contact forms', 'Lead tracking', 'Email integration'],
44
- },
45
- 'ext-payments': {
46
- name: '@djangocfg/ext-payments',
47
- description: 'Payment processing with multiple providers',
48
- features: ['NowPayments', 'Crypto payments', 'Payment tracking'],
49
- },
50
- 'ext-newsletter': {
51
- name: '@djangocfg/ext-newsletter',
52
- description: 'Newsletter and email campaigns',
53
- features: ['Subscription management', 'Email templates', 'Campaign tracking'],
54
- },
55
- 'ext-support': {
56
- name: '@djangocfg/ext-support',
57
- description: 'Customer support and ticketing',
58
- features: ['Ticket system', 'Chat support', 'Knowledge base'],
59
- },
60
- 'ext-knowbase': {
61
- name: '@djangocfg/ext-knowbase',
62
- description: 'Knowledge base and documentation',
63
- features: ['Articles', 'Categories', 'Search', 'Markdown support'],
64
- },
65
- } as const;
39
+ // Template helpers
40
+ function replacePlaceholders(content: string, replacements: Record<string, string>): string {
41
+ let result = content;
42
+ for (const [key, value] of Object.entries(replacements)) {
43
+ result = result.replaceAll(key, value);
44
+ }
45
+ return result;
46
+ }
47
+
48
+ function copyTemplateRecursive(src: string, dest: string, replacements: Record<string, string>) {
49
+ if (!existsSync(src)) {
50
+ throw new Error(`Template not found: ${src}`);
51
+ }
52
+
53
+ const stats = statSync(src);
54
+
55
+ if (stats.isDirectory()) {
56
+ // Create destination directory
57
+ if (!existsSync(dest)) {
58
+ mkdirSync(dest, { recursive: true });
59
+ }
60
+
61
+ // Copy all files recursively
62
+ const files = readdirSync(src);
63
+ for (const file of files) {
64
+ const srcPath = join(src, file);
65
+ let destFile = file;
66
+
67
+ // Replace __PROVIDER_NAME__ in filenames
68
+ if (file.includes('__PROVIDER_NAME__')) {
69
+ destFile = file.replaceAll('__PROVIDER_NAME__', replacements['__PROVIDER_NAME__']);
70
+ }
71
+
72
+ // Remove .template extension
73
+ if (destFile.endsWith('.template')) {
74
+ destFile = destFile.slice(0, -9); // Remove '.template'
75
+ }
76
+
77
+ const destPath = join(dest, destFile);
78
+ copyTemplateRecursive(srcPath, destPath, replacements);
79
+ }
80
+ } else {
81
+ // Copy file with placeholder replacement
82
+ const content = readFileSync(src, 'utf-8');
83
+ const replaced = replacePlaceholders(content, replacements);
84
+ writeFileSync(dest, replaced, 'utf-8');
85
+ }
86
+ }
66
87
 
67
88
  // Print banner
68
89
  function printBanner() {
@@ -72,6 +93,80 @@ function printBanner() {
72
93
  console.log();
73
94
  }
74
95
 
96
+ // Check if package exists on npm
97
+ async function checkPackageExists(packageName: string): Promise<boolean> {
98
+ try {
99
+ const encodedName = encodeURIComponent(packageName);
100
+ const response = await fetch(`https://registry.npmjs.org/${encodedName}`);
101
+ return response.status === 200;
102
+ } catch {
103
+ // Network error or other issue - allow creation
104
+ return false;
105
+ }
106
+ }
107
+
108
+ // Check if extension directory already exists (case-insensitive)
109
+ function checkDirectoryExists(packageName: string): { exists: boolean; path?: string } {
110
+ // Generate directory name from package name
111
+ const extDirName = packageName.replace('@', '').replace('/', '-');
112
+
113
+ // Determine base directory
114
+ const isInExtBase = process.cwd().endsWith('ext-base');
115
+ const baseDir = isInExtBase
116
+ ? join(process.cwd(), '..')
117
+ : join(process.cwd(), 'extensions');
118
+
119
+ const extDir = join(baseDir, extDirName);
120
+
121
+ // Check exact match first
122
+ if (existsSync(extDir)) {
123
+ return { exists: true, path: extDir };
124
+ }
125
+
126
+ // Check case-insensitive match (important for macOS)
127
+ try {
128
+ const files = readdirSync(baseDir);
129
+ const lowerCaseName = extDirName.toLowerCase();
130
+
131
+ for (const file of files) {
132
+ if (file.toLowerCase() === lowerCaseName) {
133
+ const fullPath = join(baseDir, file);
134
+ const stats = statSync(fullPath);
135
+ if (stats.isDirectory()) {
136
+ return { exists: true, path: fullPath };
137
+ }
138
+ }
139
+ }
140
+ } catch {
141
+ // If we can't read directory, assume it doesn't exist
142
+ return { exists: false };
143
+ }
144
+
145
+ return { exists: false };
146
+ }
147
+
148
+ // Detect available package manager
149
+ function detectPackageManager(): string | null {
150
+ const managers = ['pnpm', 'yarn', 'npm'];
151
+
152
+ for (const manager of managers) {
153
+ try {
154
+ const result = execSync(`${manager} --version`, {
155
+ encoding: 'utf-8',
156
+ stdio: ['ignore', 'pipe', 'ignore'],
157
+ });
158
+ if (result) {
159
+ return manager;
160
+ }
161
+ } catch {
162
+ // Manager not available, try next
163
+ continue;
164
+ }
165
+ }
166
+
167
+ return null;
168
+ }
169
+
75
170
  // Print help
76
171
  function printHelp() {
77
172
  printBanner();
@@ -87,139 +182,228 @@ function printHelp() {
87
182
  console.log();
88
183
 
89
184
  console.log(chalk.yellow.bold('Examples:'));
90
- console.log(` ${chalk.gray('$')} ${chalk.cyan('djangocfg-ext install')}`);
91
- console.log(` ${chalk.gray('$')} ${chalk.cyan('djangocfg-ext list')}`);
92
- console.log(` ${chalk.gray('$')} ${chalk.cyan('djangocfg-ext info ext-leads')}`);
185
+ console.log(` ${chalk.gray('$')} ${chalk.cyan('djangocfg-ext create')}`);
93
186
  console.log();
94
187
  }
95
188
 
96
- // List extensions
97
- function listExtensions() {
189
+ // Create new extension
190
+ async function createExtension() {
98
191
  printBanner();
99
- console.log(chalk.yellow.bold('Available Extensions:'));
100
- console.log();
101
192
 
102
- Object.entries(EXTENSIONS).forEach(([key, ext]) => {
103
- console.log(chalk.cyan.bold(` 📦 ${ext.name}`));
104
- console.log(chalk.gray(` ${ext.description}`));
105
- console.log(chalk.gray(` Features: ${ext.features.join(', ')}`));
106
- console.log();
107
- });
108
- }
193
+ console.log(chalk.yellow('Create a new DjangoCFG extension'));
194
+ console.log();
109
195
 
110
- // Show extension info
111
- function showInfo(extensionKey: string) {
112
- const ext = EXTENSIONS[extensionKey as keyof typeof EXTENSIONS];
196
+ // Step 1: Get and validate package name with npm check
197
+ let packageName = '';
198
+ let packageNameValid = false;
199
+
200
+ while (!packageNameValid) {
201
+ const nameResponse = await prompts({
202
+ type: 'text',
203
+ name: 'packageName',
204
+ message: 'Package name (e.g., "@my-org/my-extension" or "my-extension"):',
205
+ validate: (value: string) => {
206
+ if (!value) return 'Package name is required';
207
+ const normalized = value.toLowerCase();
208
+ // Allow @scope/name or just name
209
+ if (!/^(@[a-zA-Z0-9-]+\/)?[a-zA-Z0-9][a-zA-Z0-9-]*$/.test(normalized)) {
210
+ return 'Invalid package name format. Use @scope/name or name';
211
+ }
212
+ return true;
213
+ },
214
+ });
215
+
216
+ if (!nameResponse.packageName) {
217
+ consola.info('Extension creation cancelled');
218
+ return;
219
+ }
220
+
221
+ // Always normalize to lowercase (npm requirement)
222
+ packageName = nameResponse.packageName.toLowerCase();
223
+
224
+ // Check if extension directory already exists (case-insensitive)
225
+ const dirCheck = checkDirectoryExists(packageName);
226
+ if (dirCheck.exists) {
227
+ consola.error(`Extension directory already exists: ${chalk.red(dirCheck.path)}`);
228
+ console.log(chalk.gray('Please try a different name.'));
229
+ console.log();
230
+ continue; // Ask again
231
+ }
113
232
 
114
- if (!ext) {
115
- consola.error(`Extension not found: ${extensionKey}`);
116
- consola.info('Run `djangocfg-ext list` to see available extensions');
117
- return;
118
- }
233
+ // Check if package already exists on npm
234
+ consola.start(`Checking if ${chalk.cyan(packageName)} is available on npm...`);
235
+ const packageExists = await checkPackageExists(packageName);
119
236
 
120
- printBanner();
121
- console.log(chalk.cyan.bold(`📦 ${ext.name}`));
122
- console.log();
123
- console.log(chalk.white(ext.description));
124
- console.log();
125
- console.log(chalk.yellow.bold('Features:'));
126
- ext.features.forEach(feature => {
127
- console.log(chalk.gray(` • ${feature}`));
128
- });
129
- console.log();
130
- console.log(chalk.yellow.bold('Installation:'));
131
- console.log(chalk.gray(` pnpm add ${ext.name}`));
132
- console.log();
133
- }
237
+ if (packageExists) {
238
+ consola.error(`Package ${chalk.red(packageName)} already exists on npm!`);
239
+ console.log(chalk.gray('Please try a different name.'));
240
+ console.log();
241
+ continue; // Ask again
242
+ }
134
243
 
135
- // Install extension
136
- async function installExtension() {
137
- printBanner();
244
+ consola.success('Package name is available!');
245
+ console.log();
246
+ packageNameValid = true;
247
+ }
138
248
 
139
- const response = await prompts({
140
- type: 'select',
141
- name: 'extension',
142
- message: 'Which extension would you like to install?',
143
- choices: Object.entries(EXTENSIONS).map(([key, ext]) => ({
144
- title: ext.name,
145
- description: ext.description,
146
- value: key,
147
- })),
148
- });
249
+ // Step 2: Get remaining details
250
+ const response = await prompts([
251
+ {
252
+ type: 'text',
253
+ name: 'displayName',
254
+ message: 'Display name (e.g., "My Extension"):',
255
+ validate: (value: string) => value ? true : 'Display name is required',
256
+ },
257
+ {
258
+ type: 'text',
259
+ name: 'description',
260
+ message: 'Description:',
261
+ validate: (value: string) => value ? true : 'Description is required',
262
+ },
263
+ {
264
+ type: 'select',
265
+ name: 'category',
266
+ message: 'Category:',
267
+ choices: EXTENSION_CATEGORIES,
268
+ },
269
+ ]);
149
270
 
150
- if (!response.extension) {
151
- consola.info('Installation cancelled');
271
+ if (!response.displayName) {
272
+ consola.info('Extension creation cancelled');
152
273
  return;
153
274
  }
154
275
 
155
- const ext = EXTENSIONS[response.extension as keyof typeof EXTENSIONS];
276
+ // Add packageName to response
277
+ response.packageName = packageName;
156
278
 
157
- console.log();
158
- consola.info(`Installing ${chalk.cyan(ext.name)}...`);
159
- console.log();
279
+ // Generate directory name from package name (replace @ and / with -)
280
+ // @my-org/my-extension -> my-org-my-extension
281
+ // my-extension -> my-extension
282
+ const extDirName = response.packageName.replace('@', '').replace('/', '-');
160
283
 
161
- // Show installation command
162
- console.log(chalk.yellow.bold('Run this command:'));
163
- console.log(chalk.cyan(` pnpm add ${ext.name}`));
164
- console.log();
284
+ // Create extension in parent directory if we're in ext-base
285
+ const isInExtBase = process.cwd().endsWith('ext-base');
286
+ const extDir = isInExtBase
287
+ ? join(process.cwd(), '..', extDirName)
288
+ : join(process.cwd(), 'extensions', extDirName);
165
289
 
166
- console.log(chalk.yellow.bold('Then import in your app:'));
167
- console.log(chalk.gray(` import { ExtensionProvider } from '${ext.name}';`));
168
290
  console.log();
291
+ consola.start(`Creating extension: ${chalk.cyan(response.packageName)}`);
169
292
 
170
- consola.success('See documentation at https://djangocfg.com/docs');
171
- }
293
+ try {
294
+ // Convert displayName to PascalCase for provider/component naming
295
+ // "demo" -> "Demo"
296
+ // "My Extension" -> "MyExtension"
297
+ // "my-extension" -> "MyExtension"
298
+ const providerName = response.displayName
299
+ .split(/[\s-_]+/) // Split by spaces, hyphens, underscores
300
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
301
+ .join('');
302
+
303
+ // Extract short name from package name (last part after /)
304
+ const shortName = response.packageName.split('/').pop() || response.packageName.replace('@', '');
305
+
306
+ // Generate marketplace ID and links for README
307
+ const marketplaceId = response.packageName.replace('@', '').replace('/', '-');
308
+ const marketplaceLink = `**[📦 View in Marketplace](https://hub.djangocfg.com/extensions/${marketplaceId})** • **[📖 Documentation](https://djangocfg.com)** • **[⭐ GitHub](https://github.com/markolofsen/django-cfg)**`;
309
+
310
+ // Prepare replacements
311
+ const replacements = {
312
+ '__NAME__': shortName,
313
+ '__PACKAGE_NAME__': response.packageName,
314
+ '__DISPLAY_NAME__': response.displayName,
315
+ '__DESCRIPTION__': response.description,
316
+ '__CATEGORY__': response.category,
317
+ '__PROVIDER_NAME__': providerName,
318
+ '__MARKETPLACE_ID__': marketplaceId,
319
+ '__MARKETPLACE_LINK__': marketplaceLink,
320
+ };
321
+
322
+ // Find template directory
323
+ const templatePaths = [
324
+ // When running from built dist/ (installed from npm)
325
+ join(__dirname, '../templates/extension-template'),
326
+ // When running from src/ during development (tsx)
327
+ join(__dirname, '../../templates/extension-template'),
328
+ // Workspace path (for development from monorepo root)
329
+ join(process.cwd(), 'extensions', 'ext-base', 'templates', 'extension-template'),
330
+ // When running from ext-base directory
331
+ join(process.cwd(), 'templates', 'extension-template'),
332
+ ];
333
+
334
+ let templateDir: string | null = null;
335
+ for (const path of templatePaths) {
336
+ if (existsSync(path)) {
337
+ templateDir = path;
338
+ break;
339
+ }
340
+ }
172
341
 
173
- // Init extension in project
174
- async function initExtension() {
175
- printBanner();
342
+ if (!templateDir) {
343
+ throw new Error('Extension template not found');
344
+ }
176
345
 
177
- console.log(chalk.yellow('This will create extension configuration in your project'));
178
- console.log();
346
+ // Copy template with replacements
347
+ consola.start('Copying template files...');
348
+ copyTemplateRecursive(templateDir, extDir, replacements);
349
+ consola.success('Extension files created');
179
350
 
180
- const response = await prompts([
181
- {
182
- type: 'select',
183
- name: 'extension',
184
- message: 'Which extension do you want to configure?',
185
- choices: Object.entries(EXTENSIONS).map(([key, ext]) => ({
186
- title: ext.name,
187
- description: ext.description,
188
- value: key,
189
- })),
190
- },
191
- {
192
- type: 'confirm',
193
- name: 'confirm',
194
- message: 'Generate extension configuration?',
195
- initial: true,
196
- },
197
- ]);
198
-
199
- if (!response.confirm) {
200
- consola.info('Initialization cancelled');
201
- return;
202
- }
351
+ console.log();
203
352
 
204
- const ext = EXTENSIONS[response.extension as keyof typeof EXTENSIONS];
353
+ // Auto-install dependencies
354
+ const packageManager = detectPackageManager();
355
+ if (packageManager) {
356
+ consola.start(`Installing dependencies with ${chalk.cyan(packageManager)}...`);
357
+ try {
358
+ execSync(`${packageManager} install`, {
359
+ cwd: extDir,
360
+ stdio: 'inherit',
361
+ });
362
+ consola.success('Dependencies installed successfully');
363
+ console.log();
364
+
365
+ // Run type check
366
+ consola.start('Running type check...');
367
+ try {
368
+ execSync(`${packageManager} run check`, {
369
+ cwd: extDir,
370
+ stdio: 'inherit',
371
+ });
372
+ consola.success('Type check passed');
373
+ } catch (error) {
374
+ consola.warn('Type check failed. Please review and fix type errors.');
375
+ }
376
+ } catch (error) {
377
+ consola.warn(`Failed to install dependencies automatically. Please run: ${chalk.cyan(`cd ${extDirName} && ${packageManager} install`)}`);
378
+ }
379
+ console.log();
380
+ } else {
381
+ consola.warn('No package manager found (pnpm, yarn, or npm). Please install dependencies manually.');
382
+ console.log();
383
+ }
205
384
 
206
- console.log();
207
- consola.info(`Initializing ${chalk.cyan(ext.name)}...`);
208
- console.log();
385
+ consola.success(`Extension created successfully: ${chalk.cyan(extDir)}`);
386
+ console.log();
209
387
 
210
- // Show what to do next
211
- console.log(chalk.yellow.bold('Next steps:'));
212
- console.log(chalk.gray(' 1. Install the extension: ') + chalk.cyan(`pnpm add ${ext.name}`));
213
- console.log(chalk.gray(' 2. Add provider to your layout:'));
214
- console.log();
215
- console.log(chalk.gray(' ') + chalk.white('import { ExtensionProvider } from \'' + ext.name + '\';'));
216
- console.log();
217
- console.log(chalk.gray(' ') + chalk.white('<ExtensionProvider>'));
218
- console.log(chalk.gray(' ') + chalk.white('{children}'));
219
- console.log(chalk.gray(' ') + chalk.white('</ExtensionProvider>'));
220
- console.log();
388
+ console.log(chalk.yellow.bold('Next steps:'));
389
+ console.log();
390
+ console.log(chalk.gray('1. Navigate to extension directory:'));
391
+ console.log(chalk.cyan(` cd ${extDirName}`));
392
+ console.log();
393
+ console.log(chalk.gray('2. Build the extension:'));
394
+ console.log(chalk.cyan(` ${packageManager || 'pnpm'} build`));
395
+ console.log();
396
+ console.log(chalk.gray('3. Add your features and customize:'));
397
+ console.log(chalk.cyan(` - Edit src/config.ts to add features`));
398
+ console.log(chalk.cyan(` - Add components, hooks, and utilities`));
399
+ console.log(chalk.cyan(` - Update README.md with usage examples`));
400
+ console.log();
221
401
 
222
- consola.success('Initialization complete!');
402
+ consola.info('Documentation: https://djangocfg.com/docs');
403
+ } catch (error) {
404
+ consola.error('Failed to create extension:', error);
405
+ process.exit(1);
406
+ }
223
407
  }
224
408
 
225
409
  // Main CLI
@@ -235,26 +419,8 @@ async function main() {
235
419
 
236
420
  // Handle commands
237
421
  switch (command) {
238
- case 'list':
239
- listExtensions();
240
- break;
241
-
242
- case 'info':
243
- const extKey = args[1];
244
- if (!extKey) {
245
- consola.error('Please specify an extension');
246
- consola.info('Example: djangocfg-ext info ext-leads');
247
- return;
248
- }
249
- showInfo(extKey);
250
- break;
251
-
252
- case 'install':
253
- await installExtension();
254
- break;
255
-
256
- case 'init':
257
- await initExtension();
422
+ case 'create':
423
+ await createExtension();
258
424
  break;
259
425
 
260
426
  case 'help':
package/src/config.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Extension configuration and environment utilities
3
+ */
4
+
5
+ export const isDevelopment = process.env.NODE_ENV === 'development';
6
+ export const isProduction = process.env.NODE_ENV === 'production';
7
+ export const isStaticBuild = process.env.STATIC_BUILD === 'true';
8
+
9
+ /**
10
+ * Get API URL from environment or default
11
+ */
12
+ export function getApiUrl(): string {
13
+ return process.env.NEXT_PUBLIC_API_URL || '/api';
14
+ }
15
+
16
+ // Re-export metadata
17
+ export * from './metadata';
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Base extension configuration
3
+ */
4
+
5
+ import { createExtensionConfig } from './utils/createExtensionConfig';
6
+ import packageJson from '../package.json';
7
+
8
+ export const extensionConfig = createExtensionConfig(packageJson, {
9
+ name: 'base',
10
+ displayName: 'Extension Base',
11
+ category: 'utilities',
12
+ features: [
13
+ 'CLI for creating extensions',
14
+ 'Base utilities and helpers',
15
+ 'Extension context system',
16
+ 'API client utilities',
17
+ 'Logging system',
18
+ 'TypeScript types',
19
+ ],
20
+ minVersion: '2.0.0',
21
+ examples: [
22
+ {
23
+ title: 'Create New Extension',
24
+ description: 'Use CLI to scaffold a new extension',
25
+ code: `# Install ext-base globally or use via pnpm
26
+ pnpm dlx @djangocfg/ext-base create my-extension
27
+
28
+ # Choose category, features, and start building`,
29
+ language: 'bash',
30
+ },
31
+ {
32
+ title: 'Use Extension Context',
33
+ description: 'Create a context provider for your extension',
34
+ code: `import { createExtensionContext } from '@djangocfg/ext-base';
35
+
36
+ export const { ExtensionProvider, useExtension } = createExtensionContext({
37
+ name: 'my-extension',
38
+ version: '1.0.0',
39
+ apiUrl: '/api/my-extension',
40
+ });
41
+
42
+ // In your app
43
+ export default function App({ children }) {
44
+ return (
45
+ <ExtensionProvider>
46
+ {children}
47
+ </ExtensionProvider>
48
+ );
49
+ }`,
50
+ language: 'tsx',
51
+ },
52
+ {
53
+ title: 'API Client',
54
+ description: 'Use the API client utilities',
55
+ code: `import { ApiClient } from '@djangocfg/ext-base';
56
+
57
+ const client = new ApiClient('/api');
58
+
59
+ // Make requests
60
+ const data = await client.get('/users');
61
+ const user = await client.post('/users', { name: 'John' });`,
62
+ language: 'tsx',
63
+ },
64
+ ],
65
+ });
package/src/index.ts CHANGED
@@ -5,8 +5,12 @@
5
5
  * Server-safe entry point - can be used in both server and client components.
6
6
  */
7
7
 
8
- // Types
8
+ // Types & Constants
9
9
  export type * from './types';
10
+ export { EXTENSION_CATEGORIES } from './types/context';
11
+
12
+ // Extension Config
13
+ export { extensionConfig } from './extensionConfig';
10
14
 
11
15
  // Config
12
16
  export * from './config';