@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.
Files changed (40) hide show
  1. package/README.md +134 -8
  2. package/dist/api.cjs +40 -0
  3. package/dist/api.d.cts +35 -0
  4. package/dist/api.d.ts +35 -0
  5. package/dist/api.js +2 -0
  6. package/dist/auth.cjs +10 -0
  7. package/dist/auth.d.cts +1 -0
  8. package/dist/auth.d.ts +1 -0
  9. package/dist/auth.js +2 -0
  10. package/dist/chunk-3RG5ZIWI.js +8 -0
  11. package/dist/chunk-UXTBBEO5.js +237 -0
  12. package/dist/chunk-VJEGYVBV.js +140 -0
  13. package/dist/cli.mjs +530 -0
  14. package/dist/hooks.cjs +437 -0
  15. package/dist/hooks.d.cts +97 -0
  16. package/dist/hooks.d.ts +97 -0
  17. package/dist/hooks.js +95 -0
  18. package/dist/index.cjs +345 -0
  19. package/dist/index.d.cts +363 -0
  20. package/dist/index.d.ts +363 -0
  21. package/dist/index.js +3 -0
  22. package/package.json +3 -1
  23. package/src/cli/index.ts +281 -15
  24. package/src/context/ExtensionProvider.tsx +67 -4
  25. package/src/extensionConfig.ts +77 -28
  26. package/templates/extension-template/README.md.template +30 -0
  27. package/templates/extension-template/package.json.template +2 -1
  28. package/templates/extension-template/playground/.gitignore.template +34 -0
  29. package/templates/extension-template/playground/CLAUDE.md +35 -0
  30. package/templates/extension-template/playground/README.md.template +76 -0
  31. package/templates/extension-template/playground/app/globals.css.template +19 -0
  32. package/templates/extension-template/playground/app/layout.tsx.template +30 -0
  33. package/templates/extension-template/playground/app/page.tsx.template +44 -0
  34. package/templates/extension-template/playground/next.config.ts.template +62 -0
  35. package/templates/extension-template/playground/package.json.template +33 -0
  36. package/templates/extension-template/playground/tsconfig.json.template +27 -0
  37. package/templates/extension-template/src/contexts/__PROVIDER_NAME__Context.tsx +1 -1
  38. package/templates/extension-template/src/contexts/__PROVIDER_NAME__ExtensionProvider.tsx +1 -0
  39. package/templates/extension-template/src/index.ts +12 -4
  40. 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",
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
- // Add packageName to response
277
- response.packageName = packageName;
277
+ // Create response with all fields
278
+ const fullResponse = {
279
+ ...response,
280
+ packageName,
281
+ };
278
282
 
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('/', '-');
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(response.packageName)}`);
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 = response.displayName
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 = response.packageName.split('/').pop() || response.packageName.replace('@', '');
319
+ const shortName = fullResponse.packageName.split('/').pop() || fullResponse.packageName.replace('@', '');
305
320
 
306
321
  // Generate marketplace ID and links for README
307
- const marketplaceId = response.packageName.replace('@', '').replace('/', '-');
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__': response.packageName,
314
- '__DISPLAY_NAME__': response.displayName,
315
- '__DESCRIPTION__': response.description,
316
- '__CATEGORY__': response.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
- // Note: useAuth from @djangocfg/api is automatically available
93
- // No need to wrap - extensions can import and use it directly
94
- return <SWRConfig value={config}>{children}</SWRConfig>;
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
  /**
@@ -11,38 +11,79 @@ export const extensionConfig = createExtensionConfig(packageJson, {
11
11
  category: 'utilities',
12
12
  features: [
13
13
  'CLI for creating extensions',
14
- 'Base utilities and helpers',
15
- 'Extension context system',
16
- 'API client utilities',
17
- 'Logging system',
18
- 'TypeScript types',
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 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
+ 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
- # Choose category, features, and start building`,
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: 'Use Extension Context',
33
- description: 'Create a context provider for your extension',
34
- code: `import { createExtensionContext } from '@djangocfg/ext-base';
40
+ title: 'Development with Playground',
41
+ description: 'Start Next.js playground for rapid testing',
42
+ code: `cd your-extension
35
43
 
36
- export const { ExtensionProvider, useExtension } = createExtensionContext({
37
- name: 'my-extension',
38
- version: '1.0.0',
39
- apiUrl: '/api/my-extension',
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
- // In your app
43
- export default function App({ children }) {
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: 'API Client',
54
- description: 'Use the API client utilities',
55
- code: `import { ApiClient } from '@djangocfg/ext-base';
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
- const client = new ApiClient('/api');
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
- // Make requests
60
- const data = await client.get('/users');
61
- const user = await client.post('/users', { name: 'John' });`,
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.0.0",
75
+ "@types/react": "^19.2.7",
75
76
  "consola": "^3.4.2",
76
77
  "swr": "^2.3.7",
77
78
  "tsup": "^8.5.0",