@djangocfg/ext-base 1.0.0

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 (43) hide show
  1. package/README.md +346 -0
  2. package/dist/api.cjs +41 -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-MECBWZG4.js +44 -0
  12. package/dist/chunk-YQGNYUBX.js +67 -0
  13. package/dist/hooks.cjs +190 -0
  14. package/dist/hooks.d.cts +96 -0
  15. package/dist/hooks.d.ts +96 -0
  16. package/dist/hooks.js +65 -0
  17. package/dist/index.cjs +131 -0
  18. package/dist/index.d.cts +246 -0
  19. package/dist/index.d.ts +246 -0
  20. package/dist/index.js +3 -0
  21. package/package.json +80 -0
  22. package/src/api/createExtensionAPI.ts +63 -0
  23. package/src/api/index.ts +5 -0
  24. package/src/auth/index.ts +13 -0
  25. package/src/config/env.ts +59 -0
  26. package/src/config/index.ts +5 -0
  27. package/src/context/ExtensionProvider.tsx +102 -0
  28. package/src/context/createExtensionContext.tsx +78 -0
  29. package/src/context/index.ts +7 -0
  30. package/src/hooks/index.ts +6 -0
  31. package/src/hooks/useInfinitePagination.ts +117 -0
  32. package/src/hooks/usePagination.ts +155 -0
  33. package/src/hooks.ts +17 -0
  34. package/src/index.ts +21 -0
  35. package/src/logger/createExtensionLogger.ts +61 -0
  36. package/src/logger/index.ts +5 -0
  37. package/src/types/context.ts +93 -0
  38. package/src/types/error.ts +12 -0
  39. package/src/types/index.ts +17 -0
  40. package/src/types/logger.ts +17 -0
  41. package/src/types/pagination.ts +47 -0
  42. package/src/utils/errors.ts +71 -0
  43. package/src/utils/index.ts +10 -0
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Environment configuration utilities
3
+ *
4
+ * Safe to use in both client and server contexts.
5
+ * Can be imported from server-safe entry point.
6
+ */
7
+
8
+ /**
9
+ * Check if running in development mode
10
+ */
11
+ export const isDevelopment = process.env.NODE_ENV === 'development';
12
+
13
+ /**
14
+ * Check if running in production mode
15
+ */
16
+ export const isProduction = process.env.NODE_ENV === 'production';
17
+
18
+ /**
19
+ * Check if running in test mode
20
+ */
21
+ export const isTest = process.env.NODE_ENV === 'test';
22
+
23
+ /**
24
+ * Check if this is a static build (Next.js export)
25
+ */
26
+ export const isStaticBuild = process.env.NEXT_PUBLIC_STATIC_BUILD === 'true';
27
+
28
+ /**
29
+ * Check if code is running on client side
30
+ * Safe to use in both client and server contexts
31
+ */
32
+ export const isClient = typeof window !== 'undefined';
33
+
34
+ /**
35
+ * Check if code is running on server side
36
+ * Safe to use in both client and server contexts
37
+ */
38
+ export const isServer = !isClient;
39
+
40
+ /**
41
+ * Get API URL from environment
42
+ * Returns empty string if not set
43
+ */
44
+ export const getApiUrl = (): string => {
45
+ return process.env.NEXT_PUBLIC_API_URL || '';
46
+ };
47
+
48
+ /**
49
+ * Environment configuration object
50
+ */
51
+ export const env = {
52
+ isDevelopment,
53
+ isProduction,
54
+ isTest,
55
+ isStaticBuild,
56
+ isClient,
57
+ isServer,
58
+ getApiUrl,
59
+ } as const;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Configuration utilities
3
+ */
4
+
5
+ export * from './env';
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Base provider wrapper with SWR configuration and auth for extensions
3
+ */
4
+
5
+ 'use client';
6
+
7
+ import { useEffect } from 'react';
8
+ import { SWRConfig } from 'swr';
9
+ import type { ExtensionContextOptions, ExtensionProviderProps } from '../types';
10
+ import { createExtensionLogger } from '../logger';
11
+ import { isDevelopment } from '../config';
12
+
13
+ const DEFAULT_OPTIONS: ExtensionContextOptions = {
14
+ revalidateOnFocus: false,
15
+ revalidateOnReconnect: false,
16
+ revalidateIfStale: false,
17
+ };
18
+
19
+ // Extension registry for debugging and tracking
20
+ const registeredExtensions = new Set<string>();
21
+
22
+ /**
23
+ * Base provider with SWR configuration for extension contexts
24
+ *
25
+ * Provides:
26
+ * - SWR configuration for data fetching
27
+ * - Extension registration and metadata management
28
+ * - Auth context from @djangocfg/api (automatically available via useAuth)
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * // In extension package:
33
+ * export function NewsletterProvider({ children }: { children: ReactNode }) {
34
+ * return (
35
+ * <ExtensionProvider
36
+ * metadata={{
37
+ * name: 'newsletter',
38
+ * version: '1.0.0',
39
+ * author: 'DjangoCFG',
40
+ * displayName: 'Newsletter',
41
+ * description: 'Newsletter subscription management',
42
+ * icon: '📧',
43
+ * license: 'MIT',
44
+ * githubUrl: 'https://github.com/markolofsen/django-cfg',
45
+ * keywords: ['newsletter', 'email', 'subscription'],
46
+ * }}
47
+ * options={{ revalidateOnFocus: true }}
48
+ * >
49
+ * {children}
50
+ * </ExtensionProvider>
51
+ * );
52
+ * }
53
+ *
54
+ * // In components:
55
+ * import { useAuth } from '@djangocfg/api/auth';
56
+ *
57
+ * function MyComponent() {
58
+ * const { user, isAuthenticated } = useAuth();
59
+ * // Auth is automatically available!
60
+ * }
61
+ * ```
62
+ */
63
+ export function ExtensionProvider({ children, metadata, options = {} }: ExtensionProviderProps) {
64
+ const config = { ...DEFAULT_OPTIONS, ...options };
65
+
66
+ // Register extension on mount
67
+ useEffect(() => {
68
+ if (registeredExtensions.has(metadata.name)) {
69
+ if (isDevelopment) {
70
+ console.warn(
71
+ `[ExtensionProvider] Extension "${metadata.name}" is already registered. ` +
72
+ `This might indicate that the extension is mounted multiple times.`
73
+ );
74
+ }
75
+ return;
76
+ }
77
+
78
+ registeredExtensions.add(metadata.name);
79
+
80
+ if (isDevelopment) {
81
+ const logger = createExtensionLogger({ tag: 'ext-base', level: 'info' });
82
+ logger.info(
83
+ `Extension registered: ${metadata.displayName || metadata.name} v${metadata.version}`
84
+ );
85
+ }
86
+
87
+ return () => {
88
+ registeredExtensions.delete(metadata.name);
89
+ };
90
+ }, [metadata.name, metadata.version, metadata.displayName]);
91
+
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>;
95
+ }
96
+
97
+ /**
98
+ * Get all registered extensions (for debugging)
99
+ */
100
+ export function getRegisteredExtensions(): string[] {
101
+ return Array.from(registeredExtensions);
102
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Utility for creating typed React contexts for extensions
3
+ */
4
+
5
+ 'use client';
6
+
7
+ import { createContext, useContext, type Context, type ReactNode, type ReactElement } from 'react';
8
+
9
+ export interface CreateExtensionContextOptions<T> {
10
+ /** Display name for the context (for debugging) */
11
+ displayName: string;
12
+
13
+ /** Error message when hook is used outside provider */
14
+ errorMessage?: string;
15
+ }
16
+
17
+ export interface ExtensionContextReturn<T> {
18
+ /** The React Context */
19
+ Context: Context<T | undefined>;
20
+
21
+ /** Provider component */
22
+ Provider: (props: { value: T; children: ReactNode }) => ReactElement;
23
+
24
+ /** Hook to access context value */
25
+ useContext: () => T;
26
+ }
27
+
28
+ /**
29
+ * Creates a typed React context with provider and hook
30
+ *
31
+ * @example
32
+ * ```tsx
33
+ * interface MyContextValue {
34
+ * data: string[];
35
+ * isLoading: boolean;
36
+ * }
37
+ *
38
+ * const { Provider, useContext } = createExtensionContext<MyContextValue>({
39
+ * displayName: 'MyContext'
40
+ * });
41
+ *
42
+ * // In provider component:
43
+ * <Provider value={{ data: [], isLoading: false }}>
44
+ * {children}
45
+ * </Provider>
46
+ *
47
+ * // In consumer component:
48
+ * const { data, isLoading } = useContext();
49
+ * ```
50
+ */
51
+ export function createExtensionContext<T>({
52
+ displayName,
53
+ errorMessage,
54
+ }: CreateExtensionContextOptions<T>): ExtensionContextReturn<T> {
55
+ const Context = createContext<T | undefined>(undefined);
56
+ Context.displayName = displayName;
57
+
58
+ const Provider = ({ value, children }: { value: T; children: ReactNode }) => {
59
+ return <Context.Provider value={value}>{children}</Context.Provider>;
60
+ };
61
+
62
+ const useContextHook = (): T => {
63
+ const context = useContext(Context);
64
+ if (context === undefined) {
65
+ throw new Error(
66
+ errorMessage ||
67
+ `use${displayName} must be used within ${displayName}Provider`
68
+ );
69
+ }
70
+ return context;
71
+ };
72
+
73
+ return {
74
+ Context,
75
+ Provider,
76
+ useContext: useContextHook,
77
+ };
78
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Context utilities for extension packages
3
+ */
4
+
5
+ export { createExtensionContext } from './createExtensionContext';
6
+ export { ExtensionProvider } from './ExtensionProvider';
7
+ export type { ExtensionContextReturn, CreateExtensionContextOptions } from './createExtensionContext';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Common hooks for extension packages
3
+ */
4
+
5
+ export { useInfinitePagination } from './useInfinitePagination';
6
+ export { usePagination } from './usePagination';
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Generic hook for infinite scroll pagination
3
+ * Works with any paginated API endpoint
4
+ */
5
+
6
+ 'use client';
7
+
8
+ import useSWRInfinite from 'swr/infinite';
9
+ import type {
10
+ PaginatedResponse,
11
+ InfinitePaginationReturn,
12
+ InfinitePaginationOptions,
13
+ } from '../types';
14
+
15
+ const DEFAULT_PAGE_SIZE = 20;
16
+
17
+ interface UseInfinitePaginationParams<T> {
18
+ /** Unique key prefix for SWR cache */
19
+ keyPrefix: string;
20
+
21
+ /** Fetcher function that takes (page, pageSize) and returns paginated data */
22
+ fetcher: (page: number, pageSize: number) => Promise<PaginatedResponse<T>>;
23
+
24
+ /** Options for pagination behavior */
25
+ options?: InfinitePaginationOptions;
26
+ }
27
+
28
+ /**
29
+ * Hook for infinite scroll with automatic pagination
30
+ *
31
+ * @example
32
+ * ```tsx
33
+ * const { items, loadMore, hasMore, isLoading } = useInfinitePagination({
34
+ * keyPrefix: 'tickets',
35
+ * fetcher: (page, pageSize) => apiSupport.tickets.list({ page, page_size: pageSize }),
36
+ * options: { pageSize: 20 }
37
+ * });
38
+ * ```
39
+ */
40
+ export function useInfinitePagination<T>({
41
+ keyPrefix,
42
+ fetcher,
43
+ options = {},
44
+ }: UseInfinitePaginationParams<T>): InfinitePaginationReturn<T> {
45
+ const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
46
+
47
+ const getKey = (pageIndex: number, previousPageData: PaginatedResponse<T> | null) => {
48
+ // Reached the end
49
+ if (previousPageData && !previousPageData.has_next && !previousPageData.next) {
50
+ return null;
51
+ }
52
+
53
+ // First page, no previous data
54
+ if (pageIndex === 0) {
55
+ return [keyPrefix, 'infinite', 1, pageSize];
56
+ }
57
+
58
+ // Add the page number to the SWR key
59
+ return [keyPrefix, 'infinite', pageIndex + 1, pageSize];
60
+ };
61
+
62
+ const swrFetcher = async ([, , page, size]: [string, string, number, number]) => {
63
+ return fetcher(page, size);
64
+ };
65
+
66
+ const {
67
+ data,
68
+ error,
69
+ isLoading,
70
+ isValidating,
71
+ size,
72
+ setSize,
73
+ mutate,
74
+ } = useSWRInfinite<PaginatedResponse<T>>(getKey, swrFetcher, {
75
+ revalidateFirstPage: options.revalidateFirstPage ?? false,
76
+ parallel: options.parallel ?? false,
77
+ });
78
+
79
+ // Flatten all pages into single array
80
+ const items: T[] = data ? data.flatMap((page) => page.results) : [];
81
+
82
+ // Check if there are more pages
83
+ const hasMore = data
84
+ ? Boolean(data[data.length - 1]?.has_next) || Boolean(data[data.length - 1]?.next)
85
+ : false;
86
+
87
+ // Total count from last page
88
+ const totalCount = data?.[data.length - 1]?.count ?? 0;
89
+
90
+ // Loading more state
91
+ const isLoadingMore = Boolean(
92
+ isValidating && data && typeof data[size - 1] !== 'undefined'
93
+ );
94
+
95
+ // Function to load next page
96
+ const loadMore = () => {
97
+ if (hasMore && !isLoadingMore) {
98
+ setSize(size + 1);
99
+ }
100
+ };
101
+
102
+ // Refresh all pages
103
+ const refresh = async () => {
104
+ await mutate();
105
+ };
106
+
107
+ return {
108
+ items,
109
+ isLoading,
110
+ isLoadingMore,
111
+ error,
112
+ hasMore,
113
+ totalCount,
114
+ loadMore,
115
+ refresh,
116
+ };
117
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Generic hook for standard pagination
3
+ * Works with any paginated API endpoint
4
+ */
5
+
6
+ 'use client';
7
+
8
+ import { useState, useCallback } from 'react';
9
+ import useSWR from 'swr';
10
+ import type { PaginatedResponse, PaginationParams, PaginationState } from '../types';
11
+
12
+ const DEFAULT_PAGE_SIZE = 20;
13
+
14
+ interface UsePaginationParams<T> {
15
+ /** Unique key prefix for SWR cache */
16
+ keyPrefix: string;
17
+
18
+ /** Fetcher function that takes pagination params and returns paginated data */
19
+ fetcher: (params: PaginationParams) => Promise<PaginatedResponse<T>>;
20
+
21
+ /** Initial page number (default: 1) */
22
+ initialPage?: number;
23
+
24
+ /** Items per page (default: 20) */
25
+ pageSize?: number;
26
+
27
+ /** Initial ordering */
28
+ initialOrdering?: string;
29
+ }
30
+
31
+ interface UsePaginationReturn<T> {
32
+ /** Current page items */
33
+ items: T[];
34
+
35
+ /** Pagination state */
36
+ pagination: PaginationState;
37
+
38
+ /** Loading state */
39
+ isLoading: boolean;
40
+
41
+ /** Error state */
42
+ error: any;
43
+
44
+ /** Go to specific page */
45
+ goToPage: (page: number) => void;
46
+
47
+ /** Go to next page */
48
+ nextPage: () => void;
49
+
50
+ /** Go to previous page */
51
+ previousPage: () => void;
52
+
53
+ /** Change page size */
54
+ setPageSize: (size: number) => void;
55
+
56
+ /** Change ordering */
57
+ setOrdering: (ordering: string) => void;
58
+
59
+ /** Refresh current page */
60
+ refresh: () => Promise<void>;
61
+ }
62
+
63
+ /**
64
+ * Hook for standard pagination with page controls
65
+ *
66
+ * @example
67
+ * ```tsx
68
+ * const { items, pagination, goToPage, nextPage } = usePagination({
69
+ * keyPrefix: 'campaigns',
70
+ * fetcher: (params) => apiNewsletter.campaigns.list(params),
71
+ * pageSize: 10
72
+ * });
73
+ * ```
74
+ */
75
+ export function usePagination<T>({
76
+ keyPrefix,
77
+ fetcher,
78
+ initialPage = 1,
79
+ pageSize: initialPageSize = DEFAULT_PAGE_SIZE,
80
+ initialOrdering,
81
+ }: UsePaginationParams<T>): UsePaginationReturn<T> {
82
+ const [page, setPage] = useState(initialPage);
83
+ const [pageSize, setPageSizeState] = useState(initialPageSize);
84
+ const [ordering, setOrdering] = useState<string | undefined>(initialOrdering);
85
+
86
+ const params: PaginationParams = {
87
+ page,
88
+ page_size: pageSize,
89
+ ordering,
90
+ };
91
+
92
+ const { data, error, isLoading, mutate } = useSWR(
93
+ [keyPrefix, 'paginated', params],
94
+ () => fetcher(params),
95
+ {
96
+ revalidateOnFocus: false,
97
+ revalidateIfStale: false,
98
+ }
99
+ );
100
+
101
+ const items = data?.results ?? [];
102
+ const totalCount = data?.count ?? 0;
103
+ const totalPages = Math.ceil(totalCount / pageSize);
104
+ const hasNext = data?.has_next ?? Boolean(data?.next);
105
+ const hasPrevious = data?.has_previous ?? Boolean(data?.previous);
106
+
107
+ const pagination: PaginationState = {
108
+ page,
109
+ pageSize,
110
+ totalCount,
111
+ totalPages,
112
+ hasNext,
113
+ hasPrevious,
114
+ };
115
+
116
+ const goToPage = useCallback((newPage: number) => {
117
+ if (newPage >= 1 && newPage <= totalPages) {
118
+ setPage(newPage);
119
+ }
120
+ }, [totalPages]);
121
+
122
+ const nextPage = useCallback(() => {
123
+ if (hasNext) {
124
+ setPage((prev) => prev + 1);
125
+ }
126
+ }, [hasNext]);
127
+
128
+ const previousPage = useCallback(() => {
129
+ if (hasPrevious) {
130
+ setPage((prev) => Math.max(1, prev - 1));
131
+ }
132
+ }, [hasPrevious]);
133
+
134
+ const setPageSize = useCallback((newSize: number) => {
135
+ setPageSizeState(newSize);
136
+ setPage(1); // Reset to first page when changing page size
137
+ }, []);
138
+
139
+ const refresh = useCallback(async () => {
140
+ await mutate();
141
+ }, [mutate]);
142
+
143
+ return {
144
+ items,
145
+ pagination,
146
+ isLoading,
147
+ error,
148
+ goToPage,
149
+ nextPage,
150
+ previousPage,
151
+ setPageSize,
152
+ setOrdering,
153
+ refresh,
154
+ };
155
+ }
package/src/hooks.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @djangocfg/ext-base/hooks
3
+ *
4
+ * Client-only hooks and components for extension packages.
5
+ * Use this entry point in client components only.
6
+ */
7
+
8
+ 'use client';
9
+
10
+ // Re-export everything from main entry (server-safe)
11
+ export * from './index';
12
+
13
+ // Client-only hooks
14
+ export * from './hooks';
15
+
16
+ // Client-only context utilities
17
+ export * from './context';
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @djangocfg/ext-base
3
+ *
4
+ * Base utilities for extension packages.
5
+ * Server-safe entry point - can be used in both server and client components.
6
+ */
7
+
8
+ // Types
9
+ export type * from './types';
10
+
11
+ // Config
12
+ export * from './config';
13
+
14
+ // API
15
+ export * from './api';
16
+
17
+ // Utils
18
+ export * from './utils';
19
+
20
+ // Logger
21
+ export * from './logger';
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Logger factory for extension packages
3
+ */
4
+
5
+ import { createConsola } from 'consola';
6
+ import type { ExtensionLogger, LoggerOptions } from '../types';
7
+
8
+ const isDevelopment = process.env.NODE_ENV === 'development';
9
+
10
+ const LEVEL_MAP = {
11
+ debug: 4,
12
+ info: 3,
13
+ warn: 2,
14
+ error: 1,
15
+ } as const;
16
+
17
+ /**
18
+ * Creates a tagged logger for an extension
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * // In extension package
23
+ * export const logger = createExtensionLogger({
24
+ * tag: 'ext-newsletter',
25
+ * level: 'info'
26
+ * });
27
+ *
28
+ * // Usage
29
+ * logger.info('Campaign created:', campaignId);
30
+ * logger.error('Failed to send campaign:', error);
31
+ * ```
32
+ */
33
+ export function createExtensionLogger(options: LoggerOptions): ExtensionLogger {
34
+ const { tag, level = 'info', enabled = true } = options;
35
+
36
+ if (!enabled) {
37
+ // Return no-op logger if disabled
38
+ const noop = () => {};
39
+ return {
40
+ info: noop,
41
+ warn: noop,
42
+ error: noop,
43
+ debug: noop,
44
+ success: noop,
45
+ };
46
+ }
47
+
48
+ const logLevel = isDevelopment ? LEVEL_MAP[level] : LEVEL_MAP.error;
49
+
50
+ const consola = createConsola({
51
+ level: logLevel,
52
+ }).withTag(tag);
53
+
54
+ return {
55
+ info: (...args: unknown[]) => consola.info(...(args as [any, ...any[]])),
56
+ warn: (...args: unknown[]) => consola.warn(...(args as [any, ...any[]])),
57
+ error: (...args: unknown[]) => consola.error(...(args as [any, ...any[]])),
58
+ debug: (...args: unknown[]) => consola.debug(...(args as [any, ...any[]])),
59
+ success: (...args: unknown[]) => consola.success(...(args as [any, ...any[]])),
60
+ };
61
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Logger utilities for extension packages
3
+ */
4
+
5
+ export { createExtensionLogger } from './createExtensionLogger';