|
5 | 5 | - Log papercuts and friction (tooling, docs, APIs, tests, conventions) as you hit them with `pnpx frog log`. |
6 | 6 | - Do not add global, system, or internal friction. |
7 | 7 | - Run `pnpx frog list` first to see what is already known. |
| 8 | + |
| 9 | +## TypeScript Conventions |
| 10 | + |
| 11 | +- Treat `exactOptionalPropertyTypes` and `noUncheckedIndexedAccess` as design constraints. Include `| undefined` when an optional property can explicitly receive `undefined`, and narrow indexed reads before use. |
| 12 | +- Use `readonly T[]` for array types. Preserve mutable arrays only when mutation is part of the contract. |
| 13 | +- Use `type` for project-owned shapes. Use `interface` only when declaration merging or an external ambient contract requires it. |
| 14 | +- Include `.js` extensions on relative imports and exports so source remains valid under NodeNext ESM. |
| 15 | +- Import module-shaped internal files as namespaces (`import * as Store from './Store.js'`) and access members through the module name. Named imports are fine for types, leaf helpers, command handlers, and third-party APIs that are not module namespaces. |
| 16 | +- Import Node built-ins as namespaces unless the neighboring code or API is clearer with a named import. |
| 17 | +- Re-export public module files as namespaces (`export * as Store from './Store.js'`). Avoid flattening sibling-module symbols into a barrel. |
| 18 | +- Use static imports. Reserve dynamic imports for a real runtime or bundle boundary, not dependency-cycle workarounds. |
| 19 | +- Use `import type` and `export type` where an import or export is type-only. |
| 20 | +- Use functions and plain data for normal APIs. Classes are limited to errors and framework-required entrypoints such as Durable Objects. |
| 21 | +- Keep error classes in the module that throws them, below the public functions and types. Set custom error `name` values to the namespaced form, such as `Config.InvalidError`. |
| 22 | +- Use unions or `as const` objects instead of enums. |
| 23 | +- Prefer `camelCase` constants. Preserve uppercase names only when they mirror an external protocol or established neighboring code. |
| 24 | +- Use `const` generic parameters when callers should retain literal or tuple types. |
| 25 | +- Default optional option bags in the signature (`options: fn.Options = {}`), not with `options?: fn.Options` and downstream fallback logic. |
| 26 | +- Name typed option bags `options`. Use a domain noun only when the value is not an options bag. |
| 27 | +- Prefer one named object parameter over several positional parameters. An instance-like receiver such as `client`, `cache`, or `store` may be the first positional parameter, followed by an options bag. |
| 28 | +- Put a function's parameter, return, and error types in a matching `declare namespace` (`resolve.Options`, `resolve.ReturnType`, `resolve.ErrorType`). Keep a sibling exported type only when several functions share the domain type. |
| 29 | +- Avoid inline object types on local variables. When an explicit local object type improves clarity, name it directly above the use. |
| 30 | +- Do not extract a named type until it is reused or makes a difficult shape materially easier to read. |
| 31 | +- Keep shared domain types beside the module that owns the concept. |
| 32 | +- Let declared return types constrain intermediate expressions. Avoid redundant local annotations. |
| 33 | +- Return values directly unless a binding is reused or gives a complex expression a useful name. |
| 34 | +- For a fallible local derivation, prefer an IIFE expression over a mutable variable assigned across `try` and `catch` blocks. |
| 35 | +- Destructure when reading several properties. When normalizing one field, read `options.field` directly instead of creating a second name. |
| 36 | +- Prefer short names whose meaning is clear from local context, such as `options`, `client`, `entry`, and `fn`. |
| 37 | +- Keep wire formats, ordered tuples, protocol fields, and other order-sensitive shapes explicit. Do not alphabetize data whose order has meaning. |
| 38 | +- Avoid new `any`. Use a precise boundary type, validation, narrowing, or the smallest justified assertion. |
| 39 | +- Do not use section-divider comments. Use exports, TSDoc, and whitespace to express module structure. |
| 40 | +- Comment invariants and non-obvious reasons, not line-by-line mechanics. Keep comments independent of plans, task IDs, and prior versions. |
| 41 | + |
| 42 | +## Module and Instance Conventions |
| 43 | + |
| 44 | +- Organize each module file as one conceptual namespace containing its public types, constants, functions, and errors. Consumers should read calls as `Module.operation(...)`. |
| 45 | +- Prefer stateless module functions for pure behavior. Do not create an instance when inputs fully describe the operation. |
| 46 | +- When behavior needs dependencies or lifecycle state, expose a factory such as `Module.create(options)` or `Module.runtime(options)` that returns a plain object of operations and data. |
| 47 | +- Construct instances explicitly at the application boundary and pass them down. Do not hide construction in imports or module-scope singletons. |
| 48 | +- Scope an instance to the lifecycle that owns its state, such as one CLI run, Worker isolate, request pipeline, or test. Do not share mutable state more broadly than required. |
| 49 | +- Inject environment-specific capabilities through factory options or narrow structural types. Keep filesystem, network, cache, clock, and platform bindings out of domain modules. |
| 50 | +- Type dependencies by the smallest capability the module consumes, not by the concrete SDK client. This keeps adapters interchangeable without wrapper classes. |
| 51 | +- Let factory operations close over shared dependencies and state. Do not return methods that depend on `this` or require binding. |
| 52 | +- Name the returned public shape after its role (`Runtime`, `Client`, `Cache`, `Store`) when that role is meaningful. Use `create.ReturnType` for a factory-specific shape that has no independent domain name. |
| 53 | +- Keep one authoritative instance value. Derive related helpers and views from that instance instead of duplicating configuration or state. |
| 54 | +- Keep mutable caches and registries private to the instance. Expose explicit operations, and provide reset or disposal only when the lifecycle requires it. |
| 55 | +- Avoid side-effect registration. `sideEffects: false` means an import used only to install global behavior can disappear from a bundle. |
| 56 | +- Keep default implementations directly reachable from the factory or resolver that selects them so bundlers can tree-shake unused paths. |
| 57 | +- Pass an existing receiver first to stateless operations (`Github.publish(client, options)`). Create a factory only when several operations genuinely share dependencies, state, or lifecycle. |
| 58 | +- Keep transport-independent planning, parsing, and normalization pure. Put filesystem, GitHub, and Worker behavior in thin adapters around that core. |
| 59 | +- Keep internal helpers under `internal/` and export them only when another module has a real contract with them. |
| 60 | +- Add new public modules through the owning entrypoint as documented namespace exports. Keep the public surface lean and derive values that the library already knows. |
| 61 | +- A framework-mandated class should delegate reusable logic to module functions so the class remains a small lifecycle adapter. |
| 62 | + |
| 63 | +## Type Inference Conventions |
| 64 | + |
| 65 | +- Preserve literal inputs through public helpers when those literals affect the output type. |
| 66 | +- Keep generic types flowing from inputs through callbacks and return values. Do not erase them to `any` at an internal seam. |
| 67 | +- Prevent public callbacks, options, and return values from leaking `any`. |
| 68 | +- Add colocated `.test-d.ts` coverage with `expectTypeOf` when public inference or narrowing changes. |
| 69 | +- Revisit inference after changing an API. Prefer a narrower useful contract over a broad type that merely compiles. |
| 70 | + |
| 71 | +## Abstraction Conventions |
| 72 | + |
| 73 | +- Start with concrete code and extract only after repeated uses reveal a stable shared contract. |
| 74 | +- Prefer small local duplication over an abstraction that adds flags, modes, or call-site-specific branches. |
| 75 | +- Wait for at least three concrete uses before introducing a general abstraction unless a hard boundary already exists. |
| 76 | +- Optimize for code that is easy to change, not maximum DRYness. |
| 77 | +- Keep authoritative state, configuration, schemas, and constants in one place; derive dependent values. |
| 78 | +- Avoid wrappers that only rename another function or mirror an SDK without narrowing capabilities or adding a domain contract. |
| 79 | + |
| 80 | +## Documentation Conventions |
| 81 | + |
| 82 | +- Add TSDoc to every public export and public type property. Write or update the contract documentation alongside the implementation. |
| 83 | +- Document caller-visible purpose, inputs, output, defaults, errors, and side effects. Keep low-level wiring in nearby implementation comments. |
| 84 | +- Keep examples small and focused on the exported behavior. |
| 85 | +- Update the owning entrypoint documentation when adding or changing a public module. |
| 86 | + |
| 87 | +## Testing Conventions |
| 88 | + |
| 89 | +- Colocate unit and type tests with the module they cover. |
| 90 | +- Give each exported function under test its own `describe('functionName', ...)` block. |
| 91 | +- Prefer inline snapshots for stable structured values and thrown errors. Remove nondeterministic fields before snapshotting the remaining object. |
| 92 | +- Test observable behavior, meaningful edge cases, and public errors. Do not derive expected values from the implementation under test. |
| 93 | +- Exercise pure functions directly and composed behavior through the real adapter boundary. Avoid mocks when a real local implementation or narrow in-memory adapter is practical. |
| 94 | +- Add deterministic regression coverage for every bug fix. |
| 95 | +- Write behavioral and type tests alongside the implementation rather than after the module is complete. |
| 96 | + |
| 97 | +## Workflow Conventions |
| 98 | + |
| 99 | +- Use the smallest repository script that covers the changed behavior. Run focused tests while iterating. |
| 100 | +- Run `pnpm check:types` after TypeScript changes. |
| 101 | +- Run `pnpm test <paths>` for focused tests and `pnpm test` when the change warrants the full suite. |
| 102 | +- Treat `pnpm check` as mutating because it applies fixes. Inspect and keep only task-related changes. |
| 103 | +- Run `git diff --check` and inspect the final diff before reporting completion. |
0 commit comments