@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/README.md +154 -25
- package/package.json +11 -8
- package/preview.png +0 -0
- package/src/cli/index.ts +326 -160
- package/src/config.ts +17 -0
- package/src/extensionConfig.ts +65 -0
- package/src/index.ts +5 -1
- package/src/metadata.ts +72 -0
- package/src/types/context.ts +135 -8
- package/src/utils/createExtensionConfig.ts +155 -0
- package/src/utils/index.ts +5 -0
- package/templates/extension-template/README.md.template +64 -0
- package/templates/extension-template/package.json.template +80 -0
- package/templates/extension-template/preview.png +0 -0
- package/templates/extension-template/src/components/.gitkeep +0 -0
- package/templates/extension-template/src/config.ts +34 -0
- package/templates/extension-template/src/contexts/__PROVIDER_NAME__Context.tsx +41 -0
- package/templates/extension-template/src/contexts/__PROVIDER_NAME__ExtensionProvider.tsx +36 -0
- package/templates/extension-template/src/hooks/index.ts +27 -0
- package/templates/extension-template/src/index.ts +17 -0
- package/templates/extension-template/src/types.ts +7 -0
- package/templates/extension-template/tsconfig.json +8 -0
- package/templates/extension-template/tsup.config.ts +40 -0
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
|
-
|
|
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
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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
|
-
//
|
|
97
|
-
function
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
consola.
|
|
116
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
244
|
+
consola.success('Package name is available!');
|
|
245
|
+
console.log();
|
|
246
|
+
packageNameValid = true;
|
|
247
|
+
}
|
|
138
248
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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.
|
|
151
|
-
consola.info('
|
|
271
|
+
if (!response.displayName) {
|
|
272
|
+
consola.info('Extension creation cancelled');
|
|
152
273
|
return;
|
|
153
274
|
}
|
|
154
275
|
|
|
155
|
-
|
|
276
|
+
// Add packageName to response
|
|
277
|
+
response.packageName = packageName;
|
|
156
278
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
342
|
+
if (!templateDir) {
|
|
343
|
+
throw new Error('Extension template not found');
|
|
344
|
+
}
|
|
176
345
|
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
console.log();
|
|
385
|
+
consola.success(`Extension created successfully: ${chalk.cyan(extDir)}`);
|
|
386
|
+
console.log();
|
|
209
387
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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 '
|
|
239
|
-
|
|
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';
|