fix: convert apps/website from submodule to regular directory
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
# API Design Patterns
|
||||
|
||||
## Options Pattern
|
||||
|
||||
User-facing options with internal resolved version:
|
||||
|
||||
```typescript
|
||||
export interface Options {
|
||||
verbose?: boolean
|
||||
include?: string[]
|
||||
exclude?: string[]
|
||||
}
|
||||
|
||||
export interface ResolvedOptions extends Required<Options> {
|
||||
root: string
|
||||
}
|
||||
|
||||
function resolveOptions(options: Options = {}): ResolvedOptions {
|
||||
return {
|
||||
verbose: options.verbose ?? false,
|
||||
include: options.include ?? ['**/*'],
|
||||
exclude: options.exclude ?? ['node_modules'],
|
||||
root: process.cwd(),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Factory Functions
|
||||
|
||||
Create configured instances:
|
||||
|
||||
```typescript
|
||||
export function createContext(options: Options = {}) {
|
||||
const resolved = resolveOptions(options)
|
||||
const filter = createFilter(resolved.include, resolved.exclude)
|
||||
|
||||
return {
|
||||
options: resolved,
|
||||
filter,
|
||||
transform(code: string, id: string) { /* ... */ },
|
||||
async scanDirs() { /* ... */ },
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const ctx = createContext({ verbose: true })
|
||||
await ctx.scanDirs()
|
||||
```
|
||||
|
||||
## Builder Pattern
|
||||
|
||||
Chainable API with type accumulation:
|
||||
|
||||
```typescript
|
||||
export function createBuilder<TContext = unknown>() {
|
||||
return {
|
||||
context<T>(): Builder<T, unknown, unknown> {
|
||||
return this as any
|
||||
},
|
||||
input<T>(schema: T): Builder<TContext, T, unknown> {
|
||||
return this as any
|
||||
},
|
||||
output<T>(schema: T): Builder<TContext, unknown, T> {
|
||||
return this as any
|
||||
},
|
||||
build(): Procedure<TContext> { /* ... */ },
|
||||
}
|
||||
}
|
||||
|
||||
// Usage - types flow through chain
|
||||
const procedure = createBuilder()
|
||||
.context<{ user: User }>()
|
||||
.input(z.object({ id: z.string() }))
|
||||
.build()
|
||||
```
|
||||
|
||||
## Plugin Pattern (unplugin)
|
||||
|
||||
Universal plugin from single implementation:
|
||||
|
||||
```typescript
|
||||
import { createUnplugin } from 'unplugin'
|
||||
|
||||
export default createUnplugin<Options>((options) => {
|
||||
const ctx = createContext(options)
|
||||
|
||||
return {
|
||||
name: 'my-plugin',
|
||||
enforce: 'pre',
|
||||
|
||||
transformInclude(id) {
|
||||
return ctx.filter(id)
|
||||
},
|
||||
|
||||
transform(code, id) {
|
||||
return ctx.transform(code, id)
|
||||
},
|
||||
|
||||
// Bundler-specific hooks
|
||||
vite: {
|
||||
configResolved(config) { /* Vite-specific */ },
|
||||
},
|
||||
webpack(compiler) {
|
||||
compiler.hooks.watchRun.tap('my-plugin', () => { /* ... */ })
|
||||
},
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Export per-bundler entries:
|
||||
|
||||
```typescript
|
||||
// src/vite.ts
|
||||
import unplugin from '.'
|
||||
export default unplugin.vite
|
||||
|
||||
// src/webpack.ts
|
||||
import unplugin from '.'
|
||||
export default unplugin.webpack
|
||||
```
|
||||
|
||||
## Lazy Getters (Tree-shaking)
|
||||
|
||||
Defer bundler-specific code until accessed:
|
||||
|
||||
```typescript
|
||||
export function createPlugin<T>(factory: PluginFactory<T>) {
|
||||
return {
|
||||
get vite() { return getVitePlugin(factory) },
|
||||
get webpack() { return getWebpackPlugin(factory) },
|
||||
get rollup() { return getRollupPlugin(factory) },
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Only the accessed getter runs, rest is tree-shaken.
|
||||
|
||||
## Smart Defaults
|
||||
|
||||
Detect environment instead of requiring config:
|
||||
|
||||
```typescript
|
||||
import { isPackageExists } from 'local-pkg'
|
||||
|
||||
function resolveOptions(options: Options) {
|
||||
return {
|
||||
vue: options.vue ?? isPackageExists('vue'),
|
||||
react: options.react ?? isPackageExists('react'),
|
||||
typescript: options.typescript ?? isPackageExists('typescript'),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Resolver Pattern
|
||||
|
||||
Flexible resolution with function or object:
|
||||
|
||||
```typescript
|
||||
export type Resolver = ResolverFunction | ResolverObject
|
||||
|
||||
export type ResolverFunction = (name: string) => ResolveResult | undefined
|
||||
export interface ResolverObject {
|
||||
type: 'component' | 'directive'
|
||||
resolve: ResolverFunction
|
||||
}
|
||||
|
||||
export function ElementPlusResolver(): Resolver[] {
|
||||
return [
|
||||
{ type: 'component', resolve: (name) => resolveComponent(name) },
|
||||
{ type: 'directive', resolve: (name) => resolveDirective(name) },
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Fluent API (Validation)
|
||||
|
||||
Method chaining with clone for immutability:
|
||||
|
||||
```typescript
|
||||
class Schema<T> {
|
||||
private _def: SchemaDef
|
||||
|
||||
min(value: number): Schema<T> {
|
||||
return new Schema({ ...this._def, min: value })
|
||||
}
|
||||
|
||||
max(value: number): Schema<T> {
|
||||
return new Schema({ ...this._def, max: value })
|
||||
}
|
||||
|
||||
optional(): Schema<T | undefined> {
|
||||
return new Schema({ ...this._def, optional: true })
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const schema = z.string().min(5).max(10).optional()
|
||||
```
|
||||
|
||||
## Barrel Exports
|
||||
|
||||
Clean public API:
|
||||
|
||||
```typescript
|
||||
// src/index.ts
|
||||
export * from './config'
|
||||
export * from './types'
|
||||
export { createContext } from './context'
|
||||
export { default } from './plugin'
|
||||
```
|
||||
@@ -0,0 +1,191 @@
|
||||
# Type Patterns
|
||||
|
||||
## Utility Types
|
||||
|
||||
Common helpers used across libraries:
|
||||
|
||||
```typescript
|
||||
// Promise or sync
|
||||
export type Awaitable<T> = T | Promise<T>
|
||||
|
||||
// Single or array
|
||||
export type Arrayable<T> = T | T[]
|
||||
|
||||
// Nullable
|
||||
export type Nullable<T> = T | null | undefined
|
||||
|
||||
// Deep partial
|
||||
export type DeepPartial<T> = {
|
||||
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
|
||||
}
|
||||
|
||||
// Simplify intersection for better IDE display
|
||||
export type Simplify<T> = { [K in keyof T]: T[K] } & {}
|
||||
|
||||
// Prevent inference in specific position
|
||||
export type NoInfer<T> = [T][T extends any ? 0 : never]
|
||||
```
|
||||
|
||||
## Conditional Extraction
|
||||
|
||||
Extract types from structures:
|
||||
|
||||
```typescript
|
||||
// Extract input type from schema
|
||||
export type Input<T> = T extends { _input: infer U } ? U : unknown
|
||||
|
||||
// Extract output type
|
||||
export type Output<T> = T extends { _output: infer U } ? U : unknown
|
||||
|
||||
// Extract from nested property
|
||||
export type InferContext<T> = T extends { context: infer C } ? C : never
|
||||
```
|
||||
|
||||
## Brand Types
|
||||
|
||||
Nominal typing for primitives:
|
||||
|
||||
```typescript
|
||||
declare const brand: unique symbol
|
||||
|
||||
export type Brand<T, B> = T & { readonly [brand]: B }
|
||||
|
||||
export type UserId = Brand<string, 'UserId'>
|
||||
export type PostId = Brand<string, 'PostId'>
|
||||
|
||||
// Can't mix them up
|
||||
function getUser(id: UserId) { /* ... */ }
|
||||
getUser('abc' as UserId) // OK
|
||||
getUser('abc' as PostId) // Error!
|
||||
```
|
||||
|
||||
## Type Accumulation (Builders)
|
||||
|
||||
Each method updates generic parameters:
|
||||
|
||||
```typescript
|
||||
interface ProcedureBuilder<TContext, TInput, TOutput> {
|
||||
input<T>(schema: T): ProcedureBuilder<TContext, T, TOutput>
|
||||
output<T>(schema: T): ProcedureBuilder<TContext, TInput, T>
|
||||
query(fn: (opts: { ctx: TContext; input: TInput }) => TOutput): Procedure
|
||||
}
|
||||
|
||||
// Types flow through the chain
|
||||
const proc = builder
|
||||
.input(z.object({ id: z.string() })) // TInput = { id: string }
|
||||
.output(z.object({ name: z.string() })) // TOutput = { name: string }
|
||||
.query(({ input }) => ({ name: input.id }))
|
||||
```
|
||||
|
||||
## Module Augmentation
|
||||
|
||||
Allow users to extend library types:
|
||||
|
||||
```typescript
|
||||
// Library code
|
||||
export interface Register {}
|
||||
|
||||
export type DefaultError = Register extends { defaultError: infer E }
|
||||
? E
|
||||
: Error
|
||||
|
||||
// User code
|
||||
declare module 'my-lib' {
|
||||
interface Register {
|
||||
defaultError: MyCustomError
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Data Tagging
|
||||
|
||||
Attach type metadata with symbols:
|
||||
|
||||
```typescript
|
||||
declare const dataTagSymbol: unique symbol
|
||||
declare const errorTagSymbol: unique symbol
|
||||
|
||||
export type DataTag<TType, TData, TError> = TType & {
|
||||
[dataTagSymbol]: TData
|
||||
[errorTagSymbol]: TError
|
||||
}
|
||||
|
||||
// Extract tagged types
|
||||
export type InferData<T> = T extends { [dataTagSymbol]: infer D } ? D : unknown
|
||||
```
|
||||
|
||||
## Mapped Type Modifications
|
||||
|
||||
Column builder pattern (drizzle):
|
||||
|
||||
```typescript
|
||||
type NotNull<T extends ColumnBuilder> = T & { _: { notNull: true } }
|
||||
type HasDefault<T extends ColumnBuilder> = T & { _: { hasDefault: true } }
|
||||
|
||||
class ColumnBuilder<T extends ColumnConfig> {
|
||||
notNull(): NotNull<this> {
|
||||
// ...
|
||||
return this as NotNull<this>
|
||||
}
|
||||
|
||||
default(value: T['data']): HasDefault<this> {
|
||||
// ...
|
||||
return this as HasDefault<this>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Compile-Time Errors
|
||||
|
||||
Return readable error messages:
|
||||
|
||||
```typescript
|
||||
type TypeError<Message extends string> = { __error: Message }
|
||||
|
||||
type ValidateInput<T> = T extends string
|
||||
? T
|
||||
: TypeError<'Input must be a string'>
|
||||
|
||||
// Shows: Type 'TypeError<"Input must be a string">' is not assignable...
|
||||
```
|
||||
|
||||
## Function Overloads
|
||||
|
||||
Multiple signatures for different inputs:
|
||||
|
||||
```typescript
|
||||
export function useEventListener<E extends keyof WindowEventMap>(
|
||||
event: E,
|
||||
listener: (ev: WindowEventMap[E]) => any
|
||||
): void
|
||||
|
||||
export function useEventListener<E extends keyof DocumentEventMap>(
|
||||
target: Document,
|
||||
event: E,
|
||||
listener: (ev: DocumentEventMap[E]) => any
|
||||
): void
|
||||
|
||||
export function useEventListener(...args: any[]) {
|
||||
// Implementation
|
||||
}
|
||||
```
|
||||
|
||||
## Distributive Conditionals
|
||||
|
||||
Apply to each union member:
|
||||
|
||||
```typescript
|
||||
type ToArray<T> = T extends any ? T[] : never
|
||||
|
||||
type Result = ToArray<string | number>
|
||||
// Result = string[] | number[]
|
||||
```
|
||||
|
||||
Disable distribution with tuple:
|
||||
|
||||
```typescript
|
||||
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never
|
||||
|
||||
type Result = ToArrayNonDist<string | number>
|
||||
// Result = (string | number)[]
|
||||
```
|
||||
Reference in New Issue
Block a user