A framework calls the user's code. Convention, lifecycle, and extension points beat a large "kit of helpers". Publishable packages and exports: skill typescript-libraries. Types: typescript-strict.
Lib vs framework
| Library | Framework | |
|---|---|---|
| Control | Consumer calls | Framework calls (IoC) |
| Shape | Small functions/types | App shape, lifecycle, plugins |
| Config | Arguments | Convention + typed config + escape hatches |
| Success | Minimal and stable API | DX: right defaults, actionable errors, safe extension |
If the user only needs functions, don't invent a framework.
Typical stack
| Piece | Role |
|---|---|
| TypeScript | Public contracts, conscious module augmentation |
| Plugin API | setup / hooks / resolveId — the domain one |
| Config schema | Zod/Valibot/TypeBox at boot |
| CLI (optional) | create-*, dev, build |
| Test harness | Minimal app + fake plugin |
npm delivery follows typescript-libraries (exports, peers, semver).
Organization
src/
index.ts # API pública: createApp, definePlugin, tipos
core/ # lifecycle, DI, graph
plugins/ # plugins oficiais (opcionais via subpath)
config.ts # load + validate
errors.ts # erros com hint
packages/ # monorepo: core, cli, plugin-*
create-foo/ # scaffold
- Small core. Official features are plugins (even if first-party).
- The user does not import
core/internal. - File conventions (
app/,plugins/,routes/) documented and stable.
Non-negotiable rules
- Lifecycle order documented and tested (register → boot → ready → close).
- Plugins are composition, not monkey-patching of internals.
- Config validated at boot; fails with field path and expected value.
- Public API vs
experimentalvs@internal— no gray area. - Breaking a hook/config = major. Prefer adding a hook to changing the payload.
- Errors say what to do (file, option, link). Not
undefined is not a functionat the edge. - Explicit escape hatch (
hooks,rawoptions) better than "read the core source". - Defaults that work locally; no 40 mandatory flags in the
create.
Lifecycle and IoC
The framework owns the flow. The user fills slots.
export interface Plugin<TOptions = unknown> {
name: string
setup?: (ctx: FrameworkContext, options: TOptions) => void | Promise<void>
}
export function definePlugin<T>(plugin: Plugin<T>): Plugin<T> {
return plugin
}
// createApp() registra plugins, valida config, dispara hooks na ordem.
definePlugin/defineConfigexist for inference, not for magic.- Context (
app,config,hooks,logger) is the unit of extension — not globals. - Hooks: stable names (
build:done), versioned payload. Avoidon('*')as public API. - Async:
awaithooks in series when order matters; parallel only if independent and documented. - Shutdown:
close/disposein reverse boot order; plugins don't leak handles.
Convention and config
- Convention over configuration: default folders and names; config only for deviation.
- One config object typed (
FooConfig); nested by domain, not 200 keys at the root. satisfies FooConfig/defineConfig()for autocomplete withoutany.- Framework feature flags are stable or experimental — not “eternal beta” in the default API.
- Path resolution relative to the project root, not to the accidental
cwd.
Types as contract
- The user extends via generics of the
createAppor via documented module augmentation (declare module 'foo'). - Don't abuse declaration merging for implicit global state.
- Plugins that add fields to the
appmust type that augmentation (the plugin's opt-in declaration merging). - Avoid
anyin thectx; a poorly typed plugin contaminates the entire app. - Generics flow from config → app → plugins; sensible defaults.
DX
- Messages: problem + location + fix. E.g.: “Plugin
authduplicated. Use a uniquenameorenforce: 'pre'.” - Stack traces point to the user's file, not just to the minified core (source maps in the CLI).
- Scaffold (
create-foo) generates the happy path: tsconfig, scriptdev, an example plugin. - Docs: lifecycle, plugin order, “how to write a plugin”, what is stable.
- Fast dev mode; graph cache with explicit invalidation.
Official plugins vs ecosystem
- First-party: reference quality (types, tests, errors).
- Third-party: stable contract; no importing internals (
foo/dist/core.js). - Don't version a plugin against the core's
*— peer^on the current major. - Isolation: plugin A doesn't break B; order
enforce: 'pre' | 'post'only when the graph requires it.
Tests
- Harness:
createTestApp({ plugins })in-process. - Cover: hook order, config failure, duplicate plugin, close/dispose, type extension (compilation test).
- Snapshot of error messages stable (without absolute machine paths).
Anti-patterns
- God
config: Record<string, any> - Plugin that imports the core's internal files
- Undocumented lifecycle / different order in dev vs prod
- Turning the framework into a language (heavy DSL) without need
- Silent breaking of hooks in minor
- Global singleton (
getApp()) as the only form of extension - Scaffold that generates deprecated code
- “80-helper lib” sold as a framework
Integration with other skills
- Package and publish:
typescript-libraries - Strict types and schemas:
typescript-strict - App CLI vs lib: preserve the repo's bundler
- HTTP if the framework is for server:
api-design - UI kit:
react-component-engineering/design-system— do not mix with the runtime core
Completion criteria
create*+definePlugin/defineConfigtyped- Documented, tested lifecycle, with shutdown
- Config validated with actionable errors
- Stable vs experimental surface marked
- Plugins do not depend on internals
- Scaffold or minimal example boots the happy path
- Packaging aligned with
typescript-libraries