B2B-88: add starter kit structure and elements
This commit is contained in:
3
packages/shared/eslint.config.mjs
Normal file
3
packages/shared/eslint.config.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
import eslintConfigBase from '@kit/eslint-config/base.js';
|
||||
|
||||
export default eslintConfigBase;
|
||||
17
packages/shared/node_modules/.bin/pino
generated
vendored
Executable file
17
packages/shared/node_modules/.bin/pino
generated
vendored
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
|
||||
esac
|
||||
|
||||
if [ -z "$NODE_PATH" ]; then
|
||||
export NODE_PATH="/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/pino@9.7.0/node_modules/pino/node_modules:/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/pino@9.7.0/node_modules:/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/node_modules"
|
||||
else
|
||||
export NODE_PATH="/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/pino@9.7.0/node_modules/pino/node_modules:/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/pino@9.7.0/node_modules:/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/node_modules:$NODE_PATH"
|
||||
fi
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../pino/bin.js" "$@"
|
||||
else
|
||||
exec node "$basedir/../pino/bin.js" "$@"
|
||||
fi
|
||||
1
packages/shared/node_modules/@kit/eslint-config
generated
vendored
Symbolic link
1
packages/shared/node_modules/@kit/eslint-config
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../tooling/eslint
|
||||
1
packages/shared/node_modules/@kit/prettier-config
generated
vendored
Symbolic link
1
packages/shared/node_modules/@kit/prettier-config
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../tooling/prettier
|
||||
1
packages/shared/node_modules/@kit/tsconfig
generated
vendored
Symbolic link
1
packages/shared/node_modules/@kit/tsconfig
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../tooling/typescript
|
||||
1
packages/shared/node_modules/@types/react
generated
vendored
Symbolic link
1
packages/shared/node_modules/@types/react
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../node_modules/.pnpm/@types+react@19.1.4/node_modules/@types/react
|
||||
1
packages/shared/node_modules/pino
generated
vendored
Symbolic link
1
packages/shared/node_modules/pino
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../node_modules/.pnpm/pino@9.7.0/node_modules/pino
|
||||
35
packages/shared/package.json
Normal file
35
packages/shared/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@kit/shared",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"clean": "git clean -xdf .turbo node_modules",
|
||||
"format": "prettier --check \"**/*.{ts,tsx}\"",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"prettier": "@kit/prettier-config",
|
||||
"exports": {
|
||||
"./logger": "./src/logger/index.ts",
|
||||
"./utils": "./src/utils.ts",
|
||||
"./hooks": "./src/hooks/index.ts",
|
||||
"./events": "./src/events/index.tsx",
|
||||
"./registry": "./src/registry/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/eslint-config": "workspace:*",
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@types/react": "19.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"pino": "^9.6.0"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
118
packages/shared/src/events/index.tsx
Normal file
118
packages/shared/src/events/index.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useCallback, useContext, useRef } from 'react';
|
||||
|
||||
type EmptyPayload = NonNullable<unknown>;
|
||||
|
||||
// Base event types
|
||||
export interface BaseAppEventTypes {
|
||||
'user.signedIn': { userId: string };
|
||||
'user.signedUp': { method: `magiclink` | `password` };
|
||||
'user.updated': EmptyPayload;
|
||||
'checkout.started': { planId: string; account?: string };
|
||||
|
||||
// Add more base event types here
|
||||
}
|
||||
|
||||
export type ConsumerProvidedEventTypes = EmptyPayload;
|
||||
|
||||
// Helper type for extending event types
|
||||
export type ExtendedAppEventTypes<
|
||||
T extends ConsumerProvidedEventTypes = ConsumerProvidedEventTypes,
|
||||
> = BaseAppEventTypes & T;
|
||||
|
||||
// Generic type for the entire module
|
||||
export type AppEventType<T extends ConsumerProvidedEventTypes> =
|
||||
keyof ExtendedAppEventTypes<T>;
|
||||
|
||||
export type AppEvent<
|
||||
T extends ConsumerProvidedEventTypes = ConsumerProvidedEventTypes,
|
||||
K extends AppEventType<T> = AppEventType<T>,
|
||||
> = {
|
||||
type: K;
|
||||
payload: ExtendedAppEventTypes<T>[K];
|
||||
};
|
||||
|
||||
export type EventCallback<
|
||||
T extends ConsumerProvidedEventTypes,
|
||||
K extends AppEventType<T> = AppEventType<T>,
|
||||
> = (event: AppEvent<T, K>) => void;
|
||||
|
||||
interface InternalAppEventsContextType<
|
||||
T extends ConsumerProvidedEventTypes = ConsumerProvidedEventTypes,
|
||||
K extends AppEventType<T> = AppEventType<T>,
|
||||
> {
|
||||
emit: (event: AppEvent<never, never>) => void;
|
||||
on: (eventType: K, callback: EventCallback<T, K>) => void;
|
||||
off: (eventType: K, callback: EventCallback<T, K>) => void;
|
||||
}
|
||||
|
||||
interface AppEventsContextType<T extends ConsumerProvidedEventTypes> {
|
||||
emit: <K extends AppEventType<T>>(event: AppEvent<T, K>) => void;
|
||||
|
||||
on: <K extends AppEventType<T>>(
|
||||
eventType: K,
|
||||
callback: EventCallback<T, K>,
|
||||
) => void;
|
||||
|
||||
off: <K extends AppEventType<T>>(
|
||||
eventType: K,
|
||||
callback: EventCallback<T, K>,
|
||||
) => void;
|
||||
}
|
||||
|
||||
const AppEventsContext = createContext<InternalAppEventsContextType | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
export function AppEventsProvider<
|
||||
T extends ConsumerProvidedEventTypes = ConsumerProvidedEventTypes,
|
||||
K extends AppEventType<T> = AppEventType<T>,
|
||||
>({ children }: React.PropsWithChildren) {
|
||||
const listeners = useRef<Record<K, EventCallback<T, K>[]>>(
|
||||
{} as Record<K, EventCallback<T, K>[]>,
|
||||
);
|
||||
|
||||
const emit = useCallback(
|
||||
(event: AppEvent<T, K>) => {
|
||||
const eventListeners = listeners.current[event.type] ?? [];
|
||||
|
||||
eventListeners.forEach((callback) => callback(event));
|
||||
},
|
||||
[listeners],
|
||||
);
|
||||
|
||||
const on = useCallback((eventType: K, callback: EventCallback<T, K>) => {
|
||||
listeners.current = {
|
||||
...listeners.current,
|
||||
[eventType]: [...(listeners.current[eventType] ?? []), callback],
|
||||
};
|
||||
}, []) as AppEventsContextType<T>['on'];
|
||||
|
||||
const off = useCallback((eventType: K, callback: EventCallback<T, K>) => {
|
||||
listeners.current = {
|
||||
...listeners.current,
|
||||
[eventType]: (listeners.current[eventType] ?? []).filter(
|
||||
(cb) => cb !== callback,
|
||||
),
|
||||
};
|
||||
}, []) as AppEventsContextType<T>['off'];
|
||||
|
||||
return (
|
||||
<AppEventsContext.Provider value={{ emit, on, off }}>
|
||||
{children}
|
||||
</AppEventsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAppEvents<
|
||||
T extends ConsumerProvidedEventTypes = ConsumerProvidedEventTypes,
|
||||
>(): AppEventsContextType<T> {
|
||||
const context = useContext(AppEventsContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useAppEvents must be used within an AppEventsProvider');
|
||||
}
|
||||
|
||||
return context as AppEventsContextType<T>;
|
||||
}
|
||||
1
packages/shared/src/hooks/index.ts
Normal file
1
packages/shared/src/hooks/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './use-csrf-token';
|
||||
17
packages/shared/src/hooks/use-csrf-token.ts
Normal file
17
packages/shared/src/hooks/use-csrf-token.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Get the CSRF token from the meta tag.
|
||||
* @returns The CSRF token.
|
||||
*/
|
||||
export function useCsrfToken() {
|
||||
if (typeof document === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const meta = document.querySelector('meta[name="csrf-token"]');
|
||||
|
||||
if (!meta) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return meta.getAttribute('content') ?? '';
|
||||
}
|
||||
9
packages/shared/src/logger/impl/console.ts
Normal file
9
packages/shared/src/logger/impl/console.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
const Logger = {
|
||||
info: console.info,
|
||||
error: console.error,
|
||||
warn: console.warn,
|
||||
debug: console.debug,
|
||||
fatal: console.error,
|
||||
}
|
||||
|
||||
export { Logger };
|
||||
18
packages/shared/src/logger/impl/pino.ts
Normal file
18
packages/shared/src/logger/impl/pino.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { pino } from 'pino';
|
||||
|
||||
/**
|
||||
* @name Logger
|
||||
* @description A logger implementation using Pino
|
||||
*/
|
||||
const Logger = pino({
|
||||
browser: {
|
||||
asObject: true,
|
||||
},
|
||||
level: 'debug',
|
||||
base: {
|
||||
env: process.env.NODE_ENV,
|
||||
},
|
||||
errorKey: 'error',
|
||||
});
|
||||
|
||||
export { Logger };
|
||||
33
packages/shared/src/logger/index.ts
Normal file
33
packages/shared/src/logger/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { createRegistry } from '../registry';
|
||||
import { Logger as LoggerInstance } from './logger';
|
||||
|
||||
// Define the type for the logger provider. Currently supporting 'pino'.
|
||||
type LoggerProvider = 'pino' | 'console';
|
||||
|
||||
// Use pino as the default logger provider
|
||||
const LOGGER = (process.env.LOGGER ?? 'pino') as LoggerProvider;
|
||||
|
||||
// Create a registry for logger implementations
|
||||
const loggerRegistry = createRegistry<LoggerInstance, LoggerProvider>();
|
||||
|
||||
// Register the 'pino' logger implementation
|
||||
loggerRegistry.register('pino', async () => {
|
||||
const { Logger: PinoLogger } = await import('./impl/pino');
|
||||
|
||||
return PinoLogger;
|
||||
});
|
||||
|
||||
// Register the 'console' logger implementation
|
||||
loggerRegistry.register('console', async () => {
|
||||
const { Logger: ConsoleLogger } = await import('./impl/console');
|
||||
|
||||
return ConsoleLogger;
|
||||
});
|
||||
|
||||
/**
|
||||
* @name getLogger
|
||||
* @description Retrieves the logger implementation based on the LOGGER environment variable using the registry API.
|
||||
*/
|
||||
export async function getLogger() {
|
||||
return loggerRegistry.get(LOGGER);
|
||||
}
|
||||
17
packages/shared/src/logger/logger.ts
Normal file
17
packages/shared/src/logger/logger.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
type LogFn = {
|
||||
<T extends object>(obj: T, msg?: string, ...args: unknown[]): void;
|
||||
(obj: unknown, msg?: string, ...args: unknown[]): void;
|
||||
(msg: string, ...args: unknown[]): void;
|
||||
};
|
||||
|
||||
/**
|
||||
* @name Logger
|
||||
* @description Logger interface for logging messages
|
||||
*/
|
||||
export interface Logger {
|
||||
info: LogFn;
|
||||
error: LogFn;
|
||||
warn: LogFn;
|
||||
debug: LogFn;
|
||||
fatal: LogFn;
|
||||
}
|
||||
106
packages/shared/src/registry/index.ts
Normal file
106
packages/shared/src/registry/index.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Implementation factory type
|
||||
*/
|
||||
export type ImplementationFactory<T> = () => T | Promise<T>;
|
||||
|
||||
/**
|
||||
* Public API types with improved get method
|
||||
*/
|
||||
export interface Registry<T, Names extends string> {
|
||||
register: (
|
||||
name: Names,
|
||||
factory: ImplementationFactory<T>,
|
||||
) => Registry<T, Names>;
|
||||
|
||||
// Overloaded get method that infers return types based on input.
|
||||
get: {
|
||||
<K extends Names>(name: K): Promise<T>;
|
||||
<K extends [Names, ...Names[]]>(
|
||||
...names: K
|
||||
): Promise<{ [P in keyof K]: T }>;
|
||||
};
|
||||
|
||||
setup: (group?: string) => Promise<void>;
|
||||
addSetup: (
|
||||
group: string,
|
||||
callback: () => Promise<void>,
|
||||
) => Registry<T, Names>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name createRegistry
|
||||
* @description Creates a new registry instance with the provided implementations.
|
||||
* @returns A new registry instance.
|
||||
*/
|
||||
export function createRegistry<T, Names extends string = string>(): Registry<
|
||||
T,
|
||||
Names
|
||||
> {
|
||||
const implementations = new Map<Names, ImplementationFactory<T>>();
|
||||
const setupCallbacks = new Map<string, Array<() => Promise<void>>>();
|
||||
const setupPromises = new Map<string, Promise<void>>();
|
||||
|
||||
const registry: Registry<T, Names> = {
|
||||
register(name, factory) {
|
||||
implementations.set(name, factory);
|
||||
return registry;
|
||||
},
|
||||
|
||||
// Updated get method overload that supports tuple inference
|
||||
get: (async (...names: Names[]) => {
|
||||
await registry.setup();
|
||||
|
||||
if (names.length === 1) {
|
||||
return await getImplementation(names[0]!);
|
||||
}
|
||||
|
||||
return await Promise.all(names.map((name) => getImplementation(name)));
|
||||
}) as Registry<T, Names>['get'],
|
||||
|
||||
async setup(group?: string) {
|
||||
if (group) {
|
||||
if (!setupPromises.has(group)) {
|
||||
const callbacks = setupCallbacks.get(group) ?? [];
|
||||
|
||||
setupPromises.set(
|
||||
group,
|
||||
Promise.all(callbacks.map((cb) => cb())).then(() => void 0),
|
||||
);
|
||||
}
|
||||
|
||||
return setupPromises.get(group);
|
||||
}
|
||||
|
||||
const groups = Array.from(setupCallbacks.keys());
|
||||
|
||||
await Promise.all(groups.map((group) => registry.setup(group)));
|
||||
},
|
||||
|
||||
addSetup(group, callback) {
|
||||
if (!setupCallbacks.has(group)) {
|
||||
setupCallbacks.set(group, []);
|
||||
}
|
||||
|
||||
setupCallbacks.get(group)!.push(callback);
|
||||
return registry;
|
||||
},
|
||||
};
|
||||
|
||||
async function getImplementation(name: Names) {
|
||||
const factory = implementations.get(name);
|
||||
|
||||
if (!factory) {
|
||||
throw new Error(`Implementation "${name}" not found`);
|
||||
}
|
||||
|
||||
const implementation = await factory();
|
||||
|
||||
if (!implementation) {
|
||||
throw new Error(`Implementation "${name}" is not available`);
|
||||
}
|
||||
|
||||
return implementation;
|
||||
}
|
||||
|
||||
return registry;
|
||||
}
|
||||
23
packages/shared/src/utils.ts
Normal file
23
packages/shared/src/utils.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Check if the code is running in a browser environment.
|
||||
*/
|
||||
export function isBrowser() {
|
||||
return typeof window !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* @name formatCurrency
|
||||
* @description Format the currency based on the currency code
|
||||
*/
|
||||
export function formatCurrency(params: {
|
||||
currencyCode: string;
|
||||
locale: string;
|
||||
value: string | number;
|
||||
}) {
|
||||
const [lang, region] = params.locale.split('-');
|
||||
|
||||
return new Intl.NumberFormat(region ?? lang, {
|
||||
style: 'currency',
|
||||
currency: params.currencyCode,
|
||||
}).format(Number(params.value));
|
||||
}
|
||||
8
packages/shared/tsconfig.json
Normal file
8
packages/shared/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@kit/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
"include": ["*.ts", "src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user