@djangocfg/ext-base 1.0.3 → 1.0.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 +134 -8
- package/dist/api.cjs +40 -0
- package/dist/api.d.cts +35 -0
- package/dist/api.d.ts +35 -0
- package/dist/api.js +2 -0
- package/dist/auth.cjs +10 -0
- package/dist/auth.d.cts +1 -0
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +2 -0
- package/dist/chunk-3RG5ZIWI.js +8 -0
- package/dist/chunk-UXTBBEO5.js +237 -0
- package/dist/chunk-VJEGYVBV.js +140 -0
- package/dist/cli.mjs +530 -0
- package/dist/hooks.cjs +437 -0
- package/dist/hooks.d.cts +97 -0
- package/dist/hooks.d.ts +97 -0
- package/dist/hooks.js +95 -0
- package/dist/index.cjs +345 -0
- package/dist/index.d.cts +363 -0
- package/dist/index.d.ts +363 -0
- package/dist/index.js +3 -0
- package/package.json +3 -1
- package/src/cli/index.ts +281 -15
- package/src/context/ExtensionProvider.tsx +67 -4
- package/src/extensionConfig.ts +77 -28
- package/templates/extension-template/README.md.template +30 -0
- package/templates/extension-template/package.json.template +2 -1
- package/templates/extension-template/playground/.gitignore.template +34 -0
- package/templates/extension-template/playground/CLAUDE.md +35 -0
- package/templates/extension-template/playground/README.md.template +76 -0
- package/templates/extension-template/playground/app/globals.css.template +19 -0
- package/templates/extension-template/playground/app/layout.tsx.template +30 -0
- package/templates/extension-template/playground/app/page.tsx.template +44 -0
- package/templates/extension-template/playground/next.config.ts.template +62 -0
- package/templates/extension-template/playground/package.json.template +33 -0
- package/templates/extension-template/playground/tsconfig.json.template +27 -0
- package/templates/extension-template/src/contexts/__PROVIDER_NAME__Context.tsx +1 -1
- package/templates/extension-template/src/contexts/__PROVIDER_NAME__ExtensionProvider.tsx +1 -0
- package/templates/extension-template/src/index.ts +12 -4
- package/templates/extension-template/src/utils/withSmartProvider.tsx +70 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { EXTENSION_CATEGORIES, createExtensionConfig, createExtensionError, createExtensionLogger, extensionConfig, formatErrorMessage, handleExtensionError, isExtensionError } from './chunk-UXTBBEO5.js';
|
|
2
|
+
export { createExtensionAPI, getApiUrl, getSharedAuthStorage, isDevelopment, isProduction, isStaticBuild } from './chunk-VJEGYVBV.js';
|
|
3
|
+
import './chunk-3RG5ZIWI.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ext-base",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Base utilities and common code for DjangoCFG extensions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"django",
|
|
@@ -64,9 +64,11 @@
|
|
|
64
64
|
"build": "tsup",
|
|
65
65
|
"dev": "tsup --watch",
|
|
66
66
|
"check": "tsc --noEmit",
|
|
67
|
+
"test": "tsx src/cli/index.ts test",
|
|
67
68
|
"cli": "tsx src/cli/index.ts",
|
|
68
69
|
"cli:help": "tsx src/cli/index.ts --help",
|
|
69
70
|
"cli:create": "tsx src/cli/index.ts create",
|
|
71
|
+
"cli:test": "tsx src/cli/index.ts test",
|
|
70
72
|
"cli:list": "tsx src/cli/index.ts list",
|
|
71
73
|
"cli:info": "tsx src/cli/index.ts info"
|
|
72
74
|
},
|
package/src/cli/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ function getVersion(): string {
|
|
|
29
29
|
// CLI Commands
|
|
30
30
|
const COMMANDS = {
|
|
31
31
|
create: 'Create a new DjangoCFG extension',
|
|
32
|
+
test: 'Quick create test extension (no prompts)',
|
|
32
33
|
help: 'Show help',
|
|
33
34
|
'--help': 'Show help',
|
|
34
35
|
'-h': 'Show help',
|
|
@@ -273,13 +274,27 @@ async function createExtension() {
|
|
|
273
274
|
return;
|
|
274
275
|
}
|
|
275
276
|
|
|
276
|
-
//
|
|
277
|
-
|
|
277
|
+
// Create response with all fields
|
|
278
|
+
const fullResponse = {
|
|
279
|
+
...response,
|
|
280
|
+
packageName,
|
|
281
|
+
};
|
|
278
282
|
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
+
// Check if we're in djangocfg monorepo (early detection for directory naming)
|
|
284
|
+
const isInDjangoCfgMonorepo = existsSync(join(process.cwd(), '../../pnpm-workspace.yaml')) &&
|
|
285
|
+
existsSync(join(process.cwd(), '../../packages/api/package.json'));
|
|
286
|
+
|
|
287
|
+
// Generate directory name from package name
|
|
288
|
+
// In djangocfg monorepo: @djangocfg/demo123a -> demo123a
|
|
289
|
+
// Outside monorepo: @my-org/my-extension -> my-org-my-extension
|
|
290
|
+
let extDirName: string;
|
|
291
|
+
if (isInDjangoCfgMonorepo && fullResponse.packageName.startsWith('@djangocfg/')) {
|
|
292
|
+
// In djangocfg monorepo, just use the part after the slash
|
|
293
|
+
extDirName = fullResponse.packageName.split('/')[1];
|
|
294
|
+
} else {
|
|
295
|
+
// Outside monorepo or different scope, replace @ and / with -
|
|
296
|
+
extDirName = fullResponse.packageName.replace('@', '').replace('/', '-');
|
|
297
|
+
}
|
|
283
298
|
|
|
284
299
|
// Create extension in parent directory if we're in ext-base
|
|
285
300
|
const isInExtBase = process.cwd().endsWith('ext-base');
|
|
@@ -288,32 +303,32 @@ async function createExtension() {
|
|
|
288
303
|
: join(process.cwd(), 'extensions', extDirName);
|
|
289
304
|
|
|
290
305
|
console.log();
|
|
291
|
-
consola.start(`Creating extension: ${chalk.cyan(
|
|
306
|
+
consola.start(`Creating extension: ${chalk.cyan(fullResponse.packageName)}`);
|
|
292
307
|
|
|
293
308
|
try {
|
|
294
309
|
// Convert displayName to PascalCase for provider/component naming
|
|
295
310
|
// "demo" -> "Demo"
|
|
296
311
|
// "My Extension" -> "MyExtension"
|
|
297
312
|
// "my-extension" -> "MyExtension"
|
|
298
|
-
const providerName =
|
|
313
|
+
const providerName = fullResponse.displayName
|
|
299
314
|
.split(/[\s-_]+/) // Split by spaces, hyphens, underscores
|
|
300
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
315
|
+
.map((word: string) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
301
316
|
.join('');
|
|
302
317
|
|
|
303
318
|
// Extract short name from package name (last part after /)
|
|
304
|
-
const shortName =
|
|
319
|
+
const shortName = fullResponse.packageName.split('/').pop() || fullResponse.packageName.replace('@', '');
|
|
305
320
|
|
|
306
321
|
// Generate marketplace ID and links for README
|
|
307
|
-
const marketplaceId =
|
|
322
|
+
const marketplaceId = fullResponse.packageName.replace('@', '').replace('/', '-');
|
|
308
323
|
const marketplaceLink = `**[📦 View in Marketplace](https://hub.djangocfg.com/extensions/${marketplaceId})** • **[📖 Documentation](https://djangocfg.com)** • **[⭐ GitHub](https://github.com/markolofsen/django-cfg)**`;
|
|
309
324
|
|
|
310
325
|
// Prepare replacements
|
|
311
326
|
const replacements = {
|
|
312
327
|
'__NAME__': shortName,
|
|
313
|
-
'__PACKAGE_NAME__':
|
|
314
|
-
'__DISPLAY_NAME__':
|
|
315
|
-
'__DESCRIPTION__':
|
|
316
|
-
'__CATEGORY__':
|
|
328
|
+
'__PACKAGE_NAME__': fullResponse.packageName,
|
|
329
|
+
'__DISPLAY_NAME__': fullResponse.displayName,
|
|
330
|
+
'__DESCRIPTION__': fullResponse.description,
|
|
331
|
+
'__CATEGORY__': fullResponse.category,
|
|
317
332
|
'__PROVIDER_NAME__': providerName,
|
|
318
333
|
'__MARKETPLACE_ID__': marketplaceId,
|
|
319
334
|
'__MARKETPLACE_LINK__': marketplaceLink,
|
|
@@ -348,6 +363,29 @@ async function createExtension() {
|
|
|
348
363
|
copyTemplateRecursive(templateDir, extDir, replacements);
|
|
349
364
|
consola.success('Extension files created');
|
|
350
365
|
|
|
366
|
+
// Replace "latest" with "workspace:*" if in djangocfg monorepo
|
|
367
|
+
if (isInDjangoCfgMonorepo) {
|
|
368
|
+
consola.info('Detected djangocfg monorepo - using workspace:* for @djangocfg packages');
|
|
369
|
+
const pkgJsonPath = join(extDir, 'package.json');
|
|
370
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
371
|
+
|
|
372
|
+
// Replace "latest" with "workspace:*" for @djangocfg packages
|
|
373
|
+
const replaceDeps = (deps?: Record<string, string>) => {
|
|
374
|
+
if (!deps) return;
|
|
375
|
+
for (const [key, value] of Object.entries(deps)) {
|
|
376
|
+
if (key.startsWith('@djangocfg/') && value === 'latest') {
|
|
377
|
+
deps[key] = 'workspace:*';
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
replaceDeps(pkgJson.dependencies);
|
|
383
|
+
replaceDeps(pkgJson.peerDependencies);
|
|
384
|
+
replaceDeps(pkgJson.devDependencies);
|
|
385
|
+
|
|
386
|
+
writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + '\n', 'utf-8');
|
|
387
|
+
}
|
|
388
|
+
|
|
351
389
|
console.log();
|
|
352
390
|
|
|
353
391
|
// Auto-install dependencies
|
|
@@ -362,6 +400,23 @@ async function createExtension() {
|
|
|
362
400
|
consola.success('Dependencies installed successfully');
|
|
363
401
|
console.log();
|
|
364
402
|
|
|
403
|
+
// Install playground dependencies
|
|
404
|
+
const playgroundDir = join(extDir, 'playground');
|
|
405
|
+
if (existsSync(playgroundDir)) {
|
|
406
|
+
consola.start(`Installing playground dependencies with ${chalk.cyan(packageManager)}...`);
|
|
407
|
+
try {
|
|
408
|
+
execSync(`${packageManager} install`, {
|
|
409
|
+
cwd: playgroundDir,
|
|
410
|
+
stdio: 'inherit',
|
|
411
|
+
});
|
|
412
|
+
consola.success('Playground dependencies installed successfully');
|
|
413
|
+
console.log();
|
|
414
|
+
} catch (error) {
|
|
415
|
+
consola.warn(`Failed to install playground dependencies. Please run: ${chalk.cyan(`cd ${extDirName}/playground && ${packageManager} install`)}`);
|
|
416
|
+
console.log();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
365
420
|
// Run type check
|
|
366
421
|
consola.start('Running type check...');
|
|
367
422
|
try {
|
|
@@ -406,6 +461,213 @@ async function createExtension() {
|
|
|
406
461
|
}
|
|
407
462
|
}
|
|
408
463
|
|
|
464
|
+
// Create test extension (no prompts)
|
|
465
|
+
async function createTestExtension() {
|
|
466
|
+
printBanner();
|
|
467
|
+
|
|
468
|
+
// Clean up old test extensions first
|
|
469
|
+
const isInExtBase = process.cwd().endsWith('ext-base');
|
|
470
|
+
const baseDir = isInExtBase
|
|
471
|
+
? join(process.cwd(), '..')
|
|
472
|
+
: join(process.cwd(), 'extensions');
|
|
473
|
+
|
|
474
|
+
if (existsSync(baseDir)) {
|
|
475
|
+
const files = readdirSync(baseDir);
|
|
476
|
+
const testExtensions = files.filter(f => f.startsWith('test-'));
|
|
477
|
+
|
|
478
|
+
if (testExtensions.length > 0) {
|
|
479
|
+
consola.info(`Cleaning up ${testExtensions.length} old test extension(s)...`);
|
|
480
|
+
for (const testExt of testExtensions) {
|
|
481
|
+
const testPath = join(baseDir, testExt);
|
|
482
|
+
try {
|
|
483
|
+
execSync(`rm -rf "${testPath}"`, { stdio: 'ignore' });
|
|
484
|
+
consola.success(`Removed ${testExt}`);
|
|
485
|
+
} catch (error) {
|
|
486
|
+
consola.warn(`Failed to remove ${testExt}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
console.log();
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Generate unique test name
|
|
494
|
+
const timestamp = Date.now().toString(36);
|
|
495
|
+
const packageName = `@djangocfg/test-${timestamp}`;
|
|
496
|
+
const displayName = `Test ${timestamp}`;
|
|
497
|
+
const description = 'Test extension created for development';
|
|
498
|
+
const category = 'utilities';
|
|
499
|
+
|
|
500
|
+
console.log(chalk.yellow('Quick Test Extension Creation'));
|
|
501
|
+
console.log();
|
|
502
|
+
console.log(chalk.cyan(`Package: ${packageName}`));
|
|
503
|
+
console.log(chalk.cyan(`Name: ${displayName}`));
|
|
504
|
+
console.log();
|
|
505
|
+
|
|
506
|
+
// Check if we're in djangocfg monorepo
|
|
507
|
+
const isInDjangoCfgMonorepo = existsSync(join(process.cwd(), '../../pnpm-workspace.yaml')) &&
|
|
508
|
+
existsSync(join(process.cwd(), '../../packages/api/package.json'));
|
|
509
|
+
|
|
510
|
+
// Generate directory name from package name
|
|
511
|
+
const extDirName = packageName.split('/')[1]; // test-xyz
|
|
512
|
+
|
|
513
|
+
// Create extension in parent directory if we're in ext-base (reuse from above)
|
|
514
|
+
const extDir = isInExtBase
|
|
515
|
+
? join(process.cwd(), '..', extDirName)
|
|
516
|
+
: join(process.cwd(), 'extensions', extDirName);
|
|
517
|
+
|
|
518
|
+
// Check if already exists
|
|
519
|
+
if (existsSync(extDir)) {
|
|
520
|
+
consola.error(`Extension directory already exists: ${chalk.red(extDir)}`);
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
consola.start(`Creating test extension: ${chalk.cyan(packageName)}`);
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
// Convert displayName to PascalCase for provider/component naming
|
|
528
|
+
const providerName = displayName
|
|
529
|
+
.split(/[\s-_]+/)
|
|
530
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
531
|
+
.join('');
|
|
532
|
+
|
|
533
|
+
// Extract short name from package name
|
|
534
|
+
const shortName = packageName.split('/').pop() || packageName.replace('@', '');
|
|
535
|
+
|
|
536
|
+
// Generate marketplace ID and links for README
|
|
537
|
+
const marketplaceId = packageName.replace('@', '').replace('/', '-');
|
|
538
|
+
const marketplaceLink = `**[📦 View in Marketplace](https://hub.djangocfg.com/extensions/${marketplaceId})** • **[📖 Documentation](https://djangocfg.com)** • **[⭐ GitHub](https://github.com/markolofsen/django-cfg)**`;
|
|
539
|
+
|
|
540
|
+
// Prepare replacements
|
|
541
|
+
const replacements = {
|
|
542
|
+
'__NAME__': shortName,
|
|
543
|
+
'__PACKAGE_NAME__': packageName,
|
|
544
|
+
'__DISPLAY_NAME__': displayName,
|
|
545
|
+
'__DESCRIPTION__': description,
|
|
546
|
+
'__CATEGORY__': category,
|
|
547
|
+
'__PROVIDER_NAME__': providerName,
|
|
548
|
+
'__MARKETPLACE_ID__': marketplaceId,
|
|
549
|
+
'__MARKETPLACE_LINK__': marketplaceLink,
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// Find template directory
|
|
553
|
+
const templatePaths = [
|
|
554
|
+
join(__dirname, '../templates/extension-template'),
|
|
555
|
+
join(__dirname, '../../templates/extension-template'),
|
|
556
|
+
join(process.cwd(), 'extensions', 'ext-base', 'templates', 'extension-template'),
|
|
557
|
+
join(process.cwd(), 'templates', 'extension-template'),
|
|
558
|
+
];
|
|
559
|
+
|
|
560
|
+
let templateDir: string | null = null;
|
|
561
|
+
for (const path of templatePaths) {
|
|
562
|
+
if (existsSync(path)) {
|
|
563
|
+
templateDir = path;
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (!templateDir) {
|
|
569
|
+
throw new Error('Extension template not found');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Copy template with replacements
|
|
573
|
+
consola.start('Copying template files...');
|
|
574
|
+
copyTemplateRecursive(templateDir, extDir, replacements);
|
|
575
|
+
consola.success('Extension files created');
|
|
576
|
+
|
|
577
|
+
// Replace "latest" with "workspace:*" if in djangocfg monorepo
|
|
578
|
+
if (isInDjangoCfgMonorepo) {
|
|
579
|
+
consola.info('Detected djangocfg monorepo - using workspace:* for @djangocfg packages');
|
|
580
|
+
const pkgJsonPath = join(extDir, 'package.json');
|
|
581
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
582
|
+
|
|
583
|
+
const replaceDeps = (deps?: Record<string, string>) => {
|
|
584
|
+
if (!deps) return;
|
|
585
|
+
for (const [key, value] of Object.entries(deps)) {
|
|
586
|
+
if (key.startsWith('@djangocfg/') && value === 'latest') {
|
|
587
|
+
deps[key] = 'workspace:*';
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
replaceDeps(pkgJson.dependencies);
|
|
593
|
+
replaceDeps(pkgJson.peerDependencies);
|
|
594
|
+
replaceDeps(pkgJson.devDependencies);
|
|
595
|
+
|
|
596
|
+
// Also update playground package.json
|
|
597
|
+
const playgroundPkgPath = join(extDir, 'playground', 'package.json');
|
|
598
|
+
if (existsSync(playgroundPkgPath)) {
|
|
599
|
+
const playgroundPkg = JSON.parse(readFileSync(playgroundPkgPath, 'utf-8'));
|
|
600
|
+
replaceDeps(playgroundPkg.dependencies);
|
|
601
|
+
replaceDeps(playgroundPkg.devDependencies);
|
|
602
|
+
writeFileSync(playgroundPkgPath, JSON.stringify(playgroundPkg, null, 2) + '\n', 'utf-8');
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + '\n', 'utf-8');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
console.log();
|
|
609
|
+
|
|
610
|
+
// Auto-install dependencies
|
|
611
|
+
const packageManager = detectPackageManager();
|
|
612
|
+
if (packageManager) {
|
|
613
|
+
consola.start(`Installing dependencies with ${chalk.cyan(packageManager)}...`);
|
|
614
|
+
try {
|
|
615
|
+
execSync(`${packageManager} install`, {
|
|
616
|
+
cwd: extDir,
|
|
617
|
+
stdio: 'inherit',
|
|
618
|
+
});
|
|
619
|
+
consola.success('Dependencies installed successfully');
|
|
620
|
+
console.log();
|
|
621
|
+
|
|
622
|
+
// Install playground dependencies
|
|
623
|
+
const playgroundDir = join(extDir, 'playground');
|
|
624
|
+
if (existsSync(playgroundDir)) {
|
|
625
|
+
consola.start(`Installing playground dependencies with ${chalk.cyan(packageManager)}...`);
|
|
626
|
+
try {
|
|
627
|
+
execSync(`${packageManager} install`, {
|
|
628
|
+
cwd: playgroundDir,
|
|
629
|
+
stdio: 'inherit',
|
|
630
|
+
});
|
|
631
|
+
consola.success('Playground dependencies installed successfully');
|
|
632
|
+
console.log();
|
|
633
|
+
} catch (error) {
|
|
634
|
+
consola.warn(`Failed to install playground dependencies.`);
|
|
635
|
+
console.log();
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Run type check
|
|
640
|
+
consola.start('Running type check...');
|
|
641
|
+
try {
|
|
642
|
+
execSync(`${packageManager} run check`, {
|
|
643
|
+
cwd: extDir,
|
|
644
|
+
stdio: 'inherit',
|
|
645
|
+
});
|
|
646
|
+
consola.success('Type check passed');
|
|
647
|
+
} catch (error) {
|
|
648
|
+
consola.warn('Type check failed. Please review and fix type errors.');
|
|
649
|
+
}
|
|
650
|
+
} catch (error) {
|
|
651
|
+
consola.warn(`Failed to install dependencies automatically.`);
|
|
652
|
+
}
|
|
653
|
+
console.log();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
consola.success(`Test extension created: ${chalk.cyan(extDir)}`);
|
|
657
|
+
console.log();
|
|
658
|
+
|
|
659
|
+
console.log(chalk.yellow.bold('Quick commands:'));
|
|
660
|
+
console.log();
|
|
661
|
+
console.log(chalk.cyan(` cd ${extDirName}`));
|
|
662
|
+
console.log(chalk.cyan(` pnpm build`));
|
|
663
|
+
console.log(chalk.cyan(` pnpm dev:playground`));
|
|
664
|
+
console.log();
|
|
665
|
+
} catch (error) {
|
|
666
|
+
consola.error('Failed to create test extension:', error);
|
|
667
|
+
process.exit(1);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
409
671
|
// Main CLI
|
|
410
672
|
async function main() {
|
|
411
673
|
const args = process.argv.slice(2);
|
|
@@ -423,6 +685,10 @@ async function main() {
|
|
|
423
685
|
await createExtension();
|
|
424
686
|
break;
|
|
425
687
|
|
|
688
|
+
case 'test':
|
|
689
|
+
await createTestExtension();
|
|
690
|
+
break;
|
|
691
|
+
|
|
426
692
|
case 'help':
|
|
427
693
|
case '--help':
|
|
428
694
|
case '-h':
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
'use client';
|
|
6
6
|
|
|
7
|
-
import { useEffect } from 'react';
|
|
7
|
+
import React, { useEffect, ReactNode } from 'react';
|
|
8
8
|
import { SWRConfig } from 'swr';
|
|
9
|
+
import { AuthProvider, useAuth } from '@djangocfg/api/auth';
|
|
10
|
+
import { consola } from 'consola';
|
|
9
11
|
import type { ExtensionContextOptions, ExtensionProviderProps } from '../types';
|
|
10
12
|
import { createExtensionLogger } from '../logger';
|
|
11
13
|
import { isDevelopment } from '../config';
|
|
@@ -19,12 +21,68 @@ const DEFAULT_OPTIONS: ExtensionContextOptions = {
|
|
|
19
21
|
// Extension registry for debugging and tracking
|
|
20
22
|
const registeredExtensions = new Set<string>();
|
|
21
23
|
|
|
24
|
+
// Flag to track if we've shown the auth warning
|
|
25
|
+
let authWarningShown = false;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Component that checks for AuthProvider - will throw if not in tree
|
|
29
|
+
*/
|
|
30
|
+
function AuthChecker({ children }: { children: ReactNode }) {
|
|
31
|
+
useAuth(); // This throws if AuthProvider is not in parent tree
|
|
32
|
+
return <>{children}</>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Error boundary that catches missing AuthProvider and wraps as safety measure
|
|
37
|
+
*/
|
|
38
|
+
class AuthErrorBoundary extends React.Component<
|
|
39
|
+
{ children: ReactNode },
|
|
40
|
+
{ hasError: boolean; errorMessage: string }
|
|
41
|
+
> {
|
|
42
|
+
state = { hasError: false, errorMessage: '' };
|
|
43
|
+
|
|
44
|
+
static getDerivedStateFromError(error: Error) {
|
|
45
|
+
// Check if error is from missing AuthProvider
|
|
46
|
+
if (error?.message?.includes('useAuth must be used within an AuthProvider')) {
|
|
47
|
+
return { hasError: true, errorMessage: error.message };
|
|
48
|
+
}
|
|
49
|
+
// Re-throw other errors
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
componentDidCatch(error: Error) {
|
|
54
|
+
if (this.state.hasError && !authWarningShown) {
|
|
55
|
+
authWarningShown = true;
|
|
56
|
+
consola.error(
|
|
57
|
+
'❌ AuthProvider not found in parent component!\n' +
|
|
58
|
+
' Extension components require AuthProvider wrapper.\n' +
|
|
59
|
+
' Auto-wrapping applied as a safety measure.\n\n' +
|
|
60
|
+
' Please wrap your page with <BaseApp> or <AuthProvider>:\n\n' +
|
|
61
|
+
' import { BaseApp } from \'@djangocfg/layouts\';\n' +
|
|
62
|
+
' \n' +
|
|
63
|
+
' <BaseApp>\n' +
|
|
64
|
+
' <YourPageWithExtensions />\n' +
|
|
65
|
+
' </BaseApp>'
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
render() {
|
|
71
|
+
if (this.state.hasError) {
|
|
72
|
+
// Wrap with AuthProvider as safety measure
|
|
73
|
+
return <AuthProvider>{this.props.children}</AuthProvider>;
|
|
74
|
+
}
|
|
75
|
+
return this.props.children;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
22
79
|
/**
|
|
23
80
|
* Base provider with SWR configuration for extension contexts
|
|
24
81
|
*
|
|
25
82
|
* Provides:
|
|
26
83
|
* - SWR configuration for data fetching
|
|
27
84
|
* - Extension registration and metadata management
|
|
85
|
+
* - Auth context protection (auto-wraps if missing, shows error)
|
|
28
86
|
* - Auth context from @djangocfg/api (automatically available via useAuth)
|
|
29
87
|
*
|
|
30
88
|
* @example
|
|
@@ -89,9 +147,14 @@ export function ExtensionProvider({ children, metadata, options = {} }: Extensio
|
|
|
89
147
|
};
|
|
90
148
|
}, [metadata.name, metadata.version, metadata.displayName]);
|
|
91
149
|
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
150
|
+
// Check for AuthProvider and wrap if missing (safety measure)
|
|
151
|
+
return (
|
|
152
|
+
<AuthErrorBoundary>
|
|
153
|
+
<AuthChecker>
|
|
154
|
+
<SWRConfig value={config}>{children}</SWRConfig>
|
|
155
|
+
</AuthChecker>
|
|
156
|
+
</AuthErrorBoundary>
|
|
157
|
+
);
|
|
95
158
|
}
|
|
96
159
|
|
|
97
160
|
/**
|
package/src/extensionConfig.ts
CHANGED
|
@@ -11,38 +11,79 @@ export const extensionConfig = createExtensionConfig(packageJson, {
|
|
|
11
11
|
category: 'utilities',
|
|
12
12
|
features: [
|
|
13
13
|
'CLI for creating extensions',
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
14
|
+
'Next.js 15 playground for rapid development',
|
|
15
|
+
'Smart Provider Pattern for flexible component usage',
|
|
16
|
+
'Extension context and provider system',
|
|
17
|
+
'Pagination hooks (standard & infinite scroll)',
|
|
18
|
+
'API client utilities with auth integration',
|
|
19
|
+
'Type-safe context creation helpers',
|
|
20
|
+
'Logging system with structured output',
|
|
21
|
+
'Environment detection (dev, prod, static)',
|
|
22
|
+
'TypeScript types and utilities',
|
|
19
23
|
],
|
|
20
24
|
minVersion: '2.0.0',
|
|
21
25
|
examples: [
|
|
22
26
|
{
|
|
23
|
-
title: 'Create
|
|
24
|
-
description: '
|
|
25
|
-
code: `#
|
|
26
|
-
pnpm dlx @djangocfg/ext-base create
|
|
27
|
+
title: 'Create Extension with CLI',
|
|
28
|
+
description: 'Interactive wizard with playground environment',
|
|
29
|
+
code: `# Create extension with interactive wizard
|
|
30
|
+
pnpm dlx @djangocfg/ext-base create
|
|
27
31
|
|
|
28
|
-
#
|
|
32
|
+
# Quick test extension (auto-cleanup)
|
|
33
|
+
pnpm dlx @djangocfg/ext-base test
|
|
34
|
+
|
|
35
|
+
# List all extensions
|
|
36
|
+
pnpm dlx @djangocfg/ext-base list`,
|
|
29
37
|
language: 'bash',
|
|
30
38
|
},
|
|
31
39
|
{
|
|
32
|
-
title: '
|
|
33
|
-
description: '
|
|
34
|
-
code: `
|
|
40
|
+
title: 'Development with Playground',
|
|
41
|
+
description: 'Start Next.js playground for rapid testing',
|
|
42
|
+
code: `cd your-extension
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
# Start playground (auto-builds extension + opens browser)
|
|
45
|
+
pnpm dev:playground
|
|
46
|
+
|
|
47
|
+
# Build for production
|
|
48
|
+
pnpm build
|
|
49
|
+
|
|
50
|
+
# Type check
|
|
51
|
+
pnpm check`,
|
|
52
|
+
language: 'bash',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
title: 'Smart Provider Pattern',
|
|
56
|
+
description: 'Components that work standalone or with shared context',
|
|
57
|
+
code: `import { withSmartProvider } from '@your-org/my-extension';
|
|
58
|
+
|
|
59
|
+
// Your component
|
|
60
|
+
function MyComponent() {
|
|
61
|
+
const { data } = useMyExtension();
|
|
62
|
+
return <div>{data}</div>;
|
|
63
|
+
}
|
|
41
64
|
|
|
42
|
-
//
|
|
43
|
-
export
|
|
65
|
+
// Wrap with smart provider
|
|
66
|
+
export const MySmartComponent = withSmartProvider(MyComponent);
|
|
67
|
+
|
|
68
|
+
// Usage 1: Standalone (auto-wrapped)
|
|
69
|
+
<MySmartComponent />
|
|
70
|
+
|
|
71
|
+
// Usage 2: Manual provider (shared context)
|
|
72
|
+
<MyExtensionProvider>
|
|
73
|
+
<MySmartComponent />
|
|
74
|
+
<MySmartComponent />
|
|
75
|
+
</MyExtensionProvider>`,
|
|
76
|
+
language: 'tsx',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
title: 'Extension Provider',
|
|
80
|
+
description: 'Register and wrap your extension',
|
|
81
|
+
code: `import { ExtensionProvider } from '@djangocfg/ext-base/hooks';
|
|
82
|
+
import { extensionConfig } from './config';
|
|
83
|
+
|
|
84
|
+
export function MyExtensionProvider({ children }) {
|
|
44
85
|
return (
|
|
45
|
-
<ExtensionProvider>
|
|
86
|
+
<ExtensionProvider metadata={extensionConfig}>
|
|
46
87
|
{children}
|
|
47
88
|
</ExtensionProvider>
|
|
48
89
|
);
|
|
@@ -50,15 +91,23 @@ export default function App({ children }) {
|
|
|
50
91
|
language: 'tsx',
|
|
51
92
|
},
|
|
52
93
|
{
|
|
53
|
-
title: '
|
|
54
|
-
description: '
|
|
55
|
-
code: `import {
|
|
94
|
+
title: 'Pagination Hooks',
|
|
95
|
+
description: 'Built-in hooks for standard and infinite scroll',
|
|
96
|
+
code: `import { usePagination, useInfinitePagination } from '@djangocfg/ext-base/hooks';
|
|
56
97
|
|
|
57
|
-
|
|
98
|
+
// Standard pagination
|
|
99
|
+
const { items, page, totalPages, nextPage, prevPage } = usePagination({
|
|
100
|
+
keyPrefix: 'articles',
|
|
101
|
+
fetcher: async (page, pageSize) => api.articles.list({ page, page_size: pageSize }),
|
|
102
|
+
pageSize: 20,
|
|
103
|
+
});
|
|
58
104
|
|
|
59
|
-
//
|
|
60
|
-
const
|
|
61
|
-
|
|
105
|
+
// Infinite scroll
|
|
106
|
+
const { items, isLoading, hasMore, loadMore } = useInfinitePagination({
|
|
107
|
+
keyPrefix: 'articles',
|
|
108
|
+
fetcher: async (page, pageSize) => api.articles.list({ page, page_size: pageSize }),
|
|
109
|
+
pageSize: 20,
|
|
110
|
+
});`,
|
|
62
111
|
language: 'tsx',
|
|
63
112
|
},
|
|
64
113
|
],
|
|
@@ -54,6 +54,36 @@ export function MyComponent() {
|
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
## Development
|
|
58
|
+
|
|
59
|
+
### Playground
|
|
60
|
+
|
|
61
|
+
This extension includes a Next.js playground for local development:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Start the development playground
|
|
65
|
+
pnpm dev:playground
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Open http://localhost:3333 to see your extension in action with hot reload.
|
|
69
|
+
|
|
70
|
+
The playground includes:
|
|
71
|
+
- ✅ BaseApp wrapper (auth, theme, SWR)
|
|
72
|
+
- ✅ Hot Module Replacement
|
|
73
|
+
- ✅ Full TypeScript support
|
|
74
|
+
|
|
75
|
+
See [playground/README.md](./playground/README.md) for details.
|
|
76
|
+
|
|
77
|
+
### Build
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Build the extension
|
|
81
|
+
pnpm build
|
|
82
|
+
|
|
83
|
+
# Type check
|
|
84
|
+
pnpm check
|
|
85
|
+
```
|
|
86
|
+
|
|
57
87
|
## License
|
|
58
88
|
|
|
59
89
|
MIT
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"scripts": {
|
|
53
53
|
"build": "tsup",
|
|
54
54
|
"dev": "tsup --watch",
|
|
55
|
+
"dev:playground": "cd playground && pnpm dev",
|
|
55
56
|
"check": "tsc --noEmit"
|
|
56
57
|
},
|
|
57
58
|
"peerDependencies": {
|
|
@@ -71,7 +72,7 @@
|
|
|
71
72
|
"@djangocfg/ext-base": "latest",
|
|
72
73
|
"@djangocfg/typescript-config": "latest",
|
|
73
74
|
"@types/node": "^24.7.2",
|
|
74
|
-
"@types/react": "^19.
|
|
75
|
+
"@types/react": "^19.2.7",
|
|
75
76
|
"consola": "^3.4.2",
|
|
76
77
|
"swr": "^2.3.7",
|
|
77
78
|
"tsup": "^8.5.0",
|