untodo 0.0.1-alpha.0 → 0.0.1
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.
- package/README.md +226 -15
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# untodo
|
|
2
2
|
|
|
3
|
+
Type-safe TODO for humans and AI — trackable, structured, and lint-enforceable.
|
|
4
|
+
|
|
3
5
|
Type-safe replacement for `// TODO:` comments.
|
|
4
6
|
Trackable in your IDE, enforceable by lint, surfaced by the type system.
|
|
5
7
|
|
|
@@ -16,46 +18,178 @@ Trackable in your IDE, enforceable by lint, surfaced by the type system.
|
|
|
16
18
|
|
|
17
19
|
Inspired by [Kotlin's `TODO()`](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-t-o-d-o.html).
|
|
18
20
|
|
|
21
|
+
## Affinity with AI coding assistants
|
|
22
|
+
|
|
23
|
+
Now that AI-assisted coding is the norm, `// TODO:` comments have a new set of problems. **AI reads comments as plain text**, so it tends to miss the fact that a TODO exists, and the way TODOs are written drifts from author to author.
|
|
24
|
+
|
|
25
|
+
untodo addresses both issues.
|
|
26
|
+
|
|
27
|
+
### AI recognizes TODOs more reliably
|
|
28
|
+
|
|
29
|
+
A `TODO` in untodo is a function call backed by a type definition. AI coding assistants (Claude, GitHub Copilot, etc.) take type information and function signatures into account as context, so they pick up the existence and intent of a TODO far more reliably than from a `// TODO:` comment.
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// AI sees this only as "text"
|
|
33
|
+
// TODO: implement later (where? who? why?)
|
|
34
|
+
|
|
35
|
+
// AI sees this as "a call to an unimplemented function"
|
|
36
|
+
return TODO({ reason: "API not implemented yet", issue: 123, assignee: "alice" })
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Structured metadata like `reason`, `issue`, and `assignee` lets AI accurately read off who left the TODO, why, and which issue it is tied to.
|
|
40
|
+
|
|
41
|
+
### TODOs become consistent in style
|
|
42
|
+
|
|
43
|
+
`// TODO:` comments tend to be written differently by every author.
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// TODO: fix this
|
|
47
|
+
// TODO(alice): fix later
|
|
48
|
+
// TODO #123
|
|
49
|
+
// FIXME: has a bug
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
With untodo, ESLint / oxlint rules can ban comment-style TODOs and enforce the function-call form. Consistent shape also improves AI completion and suggestion quality when it reads the surrounding code.
|
|
53
|
+
|
|
19
54
|
## Install
|
|
20
55
|
|
|
21
56
|
```bash
|
|
22
57
|
npm install untodo
|
|
23
58
|
```
|
|
24
59
|
|
|
25
|
-
##
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
Run the bundled initializer once at the project root:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx untodo init
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This writes two files (and prints an ESLint snippet you can paste into your existing config). Existing files are skipped unless you pass `--force`.
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
wrote: ./untodo.config.ts
|
|
72
|
+
wrote: ./global.d.ts
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 1. `untodo.config.ts` — runtime configuration
|
|
76
|
+
|
|
77
|
+
Registers project-wide defaults via `defineConfig`. Per-call callbacks passed to `TODO()` / `FIXME()` / `HACK()` always win over what's set here, so this file is the right place to put fallback behavior.
|
|
78
|
+
|
|
79
|
+
The generated template ships with everything commented out:
|
|
26
80
|
|
|
27
81
|
```ts
|
|
28
|
-
import {
|
|
82
|
+
import { defineConfig } from 'untodo';
|
|
29
83
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
84
|
+
export default defineConfig({
|
|
85
|
+
// repo: 'org/repo',
|
|
86
|
+
// onTodo: (meta) => console.warn(`TODO: ${meta.reason}`),
|
|
87
|
+
// onFixme: (meta) => console.error(`FIXME: ${meta.reason}`),
|
|
88
|
+
// onHack: (meta) => console.warn(`HACK: ${meta.reason}`),
|
|
89
|
+
});
|
|
33
90
|
```
|
|
34
91
|
|
|
35
|
-
|
|
92
|
+
| Field | Type | Purpose |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `repo` | `string` | Repository in `org/repo` form. Tooling uses it to expand the `issue` meta field into a full URL (e.g. `https://github.com/org/repo/issues/123`). |
|
|
95
|
+
| `onTodo` | `(meta: TodoMeta) => void` | Default handler for every `TODO()` call that doesn't pass its own callback. |
|
|
96
|
+
| `onFixme` | `(meta: FixmeMeta) => void` | Default handler for every `FIXME()` call that doesn't pass its own callback. |
|
|
97
|
+
| `onHack` | `(meta: HackMeta) => void` | Default handler for every `HACK()` call that doesn't pass its own callback. |
|
|
98
|
+
|
|
99
|
+
A typical setup that warns in development and integrates with your logger:
|
|
36
100
|
|
|
37
101
|
```ts
|
|
38
|
-
import { defineConfig } from
|
|
102
|
+
import { defineConfig } from 'untodo';
|
|
39
103
|
|
|
40
104
|
export default defineConfig({
|
|
41
|
-
repo:
|
|
42
|
-
onTodo: (meta) =>
|
|
105
|
+
repo: 'ysknsid25/untodo',
|
|
106
|
+
onTodo: (meta) => {
|
|
107
|
+
const issue = meta.issue ? ` (#${meta.issue})` : '';
|
|
108
|
+
console.warn(`TODO${issue}: ${meta.reason}`);
|
|
109
|
+
},
|
|
110
|
+
onFixme: (meta) => console.error(`FIXME: ${meta.reason}`),
|
|
111
|
+
onHack: (meta) => console.warn(`HACK: ${meta.reason}`),
|
|
43
112
|
});
|
|
44
113
|
```
|
|
45
114
|
|
|
46
|
-
|
|
115
|
+
Import this file once from your application entry point (or test setup) so `defineConfig` runs before any `TODO()` is evaluated:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import './untodo.config';
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 2. `global.d.ts` — extending the meta types
|
|
122
|
+
|
|
123
|
+
`TODO()`, `FIXME()`, and `HACK()` accept structured metadata. By default only `reason: string` is required. Add project-specific fields to the `TodoMeta` / `FixmeMeta` / `HackMeta` interfaces via TypeScript [Declaration Merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html).
|
|
124
|
+
|
|
125
|
+
The generated template:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
declare module 'untodo' {
|
|
129
|
+
interface TodoMeta {
|
|
130
|
+
// issue?: number | string;
|
|
131
|
+
// assignee?: string;
|
|
132
|
+
}
|
|
133
|
+
interface FixmeMeta {
|
|
134
|
+
// issue?: number | string;
|
|
135
|
+
}
|
|
136
|
+
interface HackMeta {
|
|
137
|
+
// issue?: number | string;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export {};
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Uncomment or add fields to fit your team's workflow. For example:
|
|
47
145
|
|
|
48
146
|
```ts
|
|
49
|
-
|
|
50
|
-
|
|
147
|
+
declare module 'untodo' {
|
|
148
|
+
interface TodoMeta {
|
|
149
|
+
issue?: number | string;
|
|
150
|
+
assignee?: string;
|
|
151
|
+
severity?: 'low' | 'medium' | 'high';
|
|
152
|
+
}
|
|
153
|
+
interface FixmeMeta {
|
|
154
|
+
issue?: number | string;
|
|
155
|
+
}
|
|
156
|
+
interface HackMeta {
|
|
157
|
+
issue?: number | string;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export {};
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
After this, callers get full type checking and IDE autocomplete for the extra fields:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
return TODO({
|
|
168
|
+
reason: 'wire up the user repository',
|
|
169
|
+
issue: 123, // ← typed
|
|
170
|
+
assignee: 'alice', // ← typed
|
|
171
|
+
severity: 'high', // ← typed, only 'low' | 'medium' | 'high' allowed
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Make sure `global.d.ts` is included by your `tsconfig.json` (most default `"include"` patterns already pick up `**/*.d.ts`).
|
|
176
|
+
|
|
177
|
+
> The trailing `export {};` keeps the file as a module, which is required for `declare module` augmentation to apply.
|
|
178
|
+
|
|
179
|
+
### 3. Add the ESLint plugin
|
|
180
|
+
|
|
181
|
+
`init` finishes by printing a flat-config snippet. Paste it into your existing `eslint.config.ts` (or `.mjs`):
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import untodoPlugin from 'untodo/eslint';
|
|
51
185
|
|
|
52
186
|
export default [
|
|
53
187
|
{
|
|
54
188
|
plugins: { untodo: untodoPlugin },
|
|
55
189
|
rules: {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
190
|
+
'untodo/no-todo': 'error',
|
|
191
|
+
'untodo/no-fixme': 'error',
|
|
192
|
+
'untodo/no-hack': 'warn',
|
|
59
193
|
},
|
|
60
194
|
},
|
|
61
195
|
];
|
|
@@ -63,6 +197,83 @@ export default [
|
|
|
63
197
|
|
|
64
198
|
The same plugin works with [oxlint](https://oxc.rs/docs/guide/usage/linter.html)'s JS plugin support.
|
|
65
199
|
|
|
200
|
+
## Usage
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
import { TODO, FIXME, HACK } from 'untodo';
|
|
204
|
+
|
|
205
|
+
function fetchUser(): User {
|
|
206
|
+
return TODO({ reason: 'wire up the user repository' });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function parseDate(input: string): Date {
|
|
210
|
+
return FIXME({ reason: 'rejects valid ISO strings with offsets' });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function legacyAdapter(): Adapter {
|
|
214
|
+
return HACK({ reason: 'old API ships in the next major; remove then' });
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Because each function returns `never`, the surrounding code keeps type-checking as if a real value flowed through — there's no need to add `as User` casts or fake return values.
|
|
219
|
+
|
|
220
|
+
### Per-call handlers
|
|
221
|
+
|
|
222
|
+
Pass a callback as the second argument to override the global handler for one call:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
return TODO(
|
|
226
|
+
{ reason: 'wire up the user repository' },
|
|
227
|
+
(meta) => myLogger.warn('todo.hit', meta),
|
|
228
|
+
);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Opting out of the global handler
|
|
232
|
+
|
|
233
|
+
Once `defineConfig({ onTodo })` is set, **every** `TODO()` call without a per-call callback fires the global handler. That's usually what you want — but on a hot path, or for a deliberately quiet placeholder, you may want one specific call to skip the handler.
|
|
234
|
+
|
|
235
|
+
`untodo` does not bake an opt-out flag into the meta types — naming and semantics are policy, and policy belongs to your team. The recommended pattern is to add a boolean field via Declaration Merging and short-circuit inside your handler:
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
// global.d.ts — pick whatever name your team prefers (silent / muted / skip ...)
|
|
239
|
+
declare module 'untodo' {
|
|
240
|
+
interface TodoMeta {
|
|
241
|
+
silent?: boolean;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// untodo.config.ts
|
|
246
|
+
import { defineConfig } from 'untodo';
|
|
247
|
+
|
|
248
|
+
export default defineConfig({
|
|
249
|
+
onTodo: (meta) => {
|
|
250
|
+
if (meta.silent) return;
|
|
251
|
+
sendToSentry(meta);
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
// usage
|
|
258
|
+
TODO({ reason: 'normal placeholder' }); // global fires
|
|
259
|
+
TODO({ reason: 'tight loop', silent: true }); // global skipped
|
|
260
|
+
TODO({ reason: 'custom path' }, (m) => myLogger.warn(m)); // per-call wins, global skipped
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
The same pattern applies to `FixmeMeta` / `HackMeta`. Keeping the gate inside the handler — rather than as a library-level switch — means the predicate can be as nuanced as you need (`if (meta.severity === 'low' && process.env.CI) return;` etc.) without `untodo` having to anticipate every shape.
|
|
264
|
+
|
|
265
|
+
## API
|
|
266
|
+
|
|
267
|
+
| Export | Description |
|
|
268
|
+
|---|---|
|
|
269
|
+
| `TODO(meta, cb?)` | Marks a code path as not yet implemented. Returns `never`. |
|
|
270
|
+
| `FIXME(meta, cb?)` | Marks broken code that must be repaired. Returns `never`. |
|
|
271
|
+
| `HACK(meta, cb?)` | Marks a deliberate workaround to revisit. Returns `never`. |
|
|
272
|
+
| `defineConfig(config)` | Registers project-wide defaults (used in `untodo.config.ts`). |
|
|
273
|
+
| `untodo/eslint` | ESLint / oxlint plugin exposing `no-todo`, `no-fixme`, `no-hack` rules. |
|
|
274
|
+
|
|
275
|
+
None of `TODO` / `FIXME` / `HACK` throw at runtime — they only invoke the configured handler. Lint enforcement is what makes them block the build.
|
|
276
|
+
|
|
66
277
|
## Scripts
|
|
67
278
|
|
|
68
279
|
```bash
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "untodo",
|
|
3
|
-
"version": "0.0.1
|
|
4
|
-
"description": "Type-safe
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Type-safe TODO for humans and AI — trackable, structured, and lint-enforceable.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
7
7
|
"module": "./dist/index.mjs",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"vitest": "^2.1.8"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
|
-
"eslint": "^8.0.0 || ^9.0.0"
|
|
69
|
+
"eslint": "^8.0.0 || ^9.0.0 || ^10.0.0"
|
|
70
70
|
},
|
|
71
71
|
"peerDependenciesMeta": {
|
|
72
72
|
"eslint": {
|