Skip to content

Commit 83efc24

Browse files
committed
feat(README): prep for learn
1 parent 7d95514 commit 83efc24

File tree

34 files changed

+2176
-890
lines changed

34 files changed

+2176
-890
lines changed

.github/workflows/codemod_publish.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,25 @@ jobs:
132132
133133
✅ Codemod has been successfully published to the registry!
134134
EOF
135+
136+
refresh-learn:
137+
name: Trigger nodejs/learn Refresh
138+
runs-on: ubuntu-latest
139+
needs: validate-and-publish
140+
141+
permissions:
142+
contents: read
143+
144+
steps:
145+
- name: Harden Runner
146+
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
147+
with:
148+
egress-policy: audit
149+
150+
- name: Trigger refresh.userland-migrations.yml on nodejs/learn
151+
env:
152+
GH_TOKEN: ${{ secrets.LEARN_REPO_TOKEN }}
153+
run: |
154+
gh workflow run refresh.userland-migrations.yml \
155+
--repo nodejs/learn \
156+
--ref main

docs/writing-a-readme.md

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
# Writing a README for a Recipe
2+
3+
Every recipe directory must have a `README.md` that explains what the codemod does, how to run it, and what the transformation looks like. This document describes the required structure, section-by-section writing guidance, and the conventions used across the repository.
4+
5+
## The mandatory template
6+
7+
````markdown
8+
---
9+
authors: github-username
10+
---
11+
12+
# TITLE HERE
13+
14+
One-paragraph description of what the codemod does and why.
15+
16+
## Usage
17+
18+
Run this codemod with:
19+
20+
```sh
21+
npx codemod @nodejs/<recipe-directory-name>
22+
```
23+
24+
## Examples
25+
26+
### Example 1
27+
28+
Brief label for what case this example covers.
29+
30+
```diff
31+
-before
32+
+after
33+
```
34+
35+
## Notes
36+
37+
Any behavioral nuances or things the reader should know that do not fit the description.
38+
39+
### Limitations
40+
41+
Anything the codemod cannot or does not handle.
42+
````
43+
44+
The `Notes` and `Limitations` sections are optional. Omit them when they would be empty. The rest of the template is mandatory.
45+
46+
---
47+
48+
## Frontmatter
49+
50+
Every README must open with a YAML frontmatter block:
51+
52+
```yaml
53+
---
54+
authors: github-username
55+
---
56+
```
57+
58+
**`authors` must be a GitHub username, not a display name.** The field maps to a GitHub account so that attribution is linkable and unambiguous.
59+
60+
- Correct: `authors: AugustinMauroy`
61+
- Incorrect: `authors: Augustin Mauroy`
62+
63+
When a recipe has multiple authors, use a YAML sequence:
64+
65+
```yaml
66+
---
67+
authors:
68+
- github-username-one
69+
- github-username-two
70+
---
71+
```
72+
73+
---
74+
75+
## Title
76+
77+
The title (`# ...`) follows the frontmatter block immediately.
78+
79+
### Naming conventions
80+
81+
Three patterns are used in the repository, depending on the nature of the migration:
82+
83+
**1. `DEPXXXX: old-api new-api` - for single-deprecation replacements**
84+
85+
Use this form when the recipe addresses one deprecation notice and the old and new names are short enough to fit on one line.
86+
87+
```markdown
88+
# DEP0147: fs.rmdir() fs.rm()
89+
90+
# DEP0178: dirent.path dirent.parentPath
91+
92+
# DEP0093: crypto.fips crypto.getFips() / crypto.setFips()
93+
```
94+
95+
**2. `DEPXXXX: Description` - for deprecations where the replacement is not a simple rename**
96+
97+
Use this form when the new API is not a direct drop-in or when listing all affected symbols inline would make the title too long.
98+
99+
```markdown
100+
# DEP0185: Instantiating node:repl Classes Without new
101+
102+
# DEP0191: repl.builtinModules / repl.\_builtinLibs module.builtinModules
103+
```
104+
105+
When multiple deprecation numbers apply, separate them with `/`:
106+
107+
```markdown
108+
# DEP0095/DEP0096/DEP0126/DEP0127: Deprecated node:timers APIs
109+
```
110+
111+
**3. Plain descriptive title - for non-deprecation migrations**
112+
113+
When the recipe does not correspond to a numbered deprecation, use a plain descriptive title.
114+
115+
```markdown
116+
# Correct TypeScript Specifiers
117+
118+
# Import Assertions to Attributes
119+
```
120+
121+
### General title rules
122+
123+
- Use backticks around API names and code tokens in the title.
124+
- Keep the title to one line.
125+
126+
---
127+
128+
## Description paragraph
129+
130+
Immediately after the title, write a single paragraph (no heading) that describes:
131+
132+
1. What the deprecated API is and the deprecation number, if applicable.
133+
2. What it is replaced with.
134+
3. Any notable variant handling (e.g., destructured imports, ESM, synchronous counterparts).
135+
136+
Link to the upstream deprecation notice when one exists:
137+
138+
```markdown
139+
See [DEP0147](https://nodejs.org/api/deprecations.html#DEP0147).
140+
```
141+
142+
If the recipe covers multiple deprecations, link each one or group them in a sentence.
143+
144+
**Examples of well-written description paragraphs:**
145+
146+
The `rmdir` recipe describes its scope precisely:
147+
148+
> Converts `fs.rmdir(path, { recursive: true })` calls to `fs.rm(path, { recursive: true, force: true })`. Also handles the synchronous variant (`fs.rmdirSync``fs.rmSync`), the promises variant (`fs.promises.rmdir``fs.promises.rm`), destructured imports, and aliased imports. The `force: true` option is always added alongside `recursive: true`.
149+
150+
The `repl-classes-with-new` recipe names the affected classes and the import styles it handles:
151+
152+
> Adds the missing `new` keyword to calls to `repl.REPLServer()` and `repl.Recoverable()` that are invoked as plain functions. Works with CommonJS `require`, ESM named imports, ESM namespace imports, and dynamic imports.
153+
154+
---
155+
156+
## Usage section
157+
158+
The `Usage` section gives the single command a reader needs to run the codemod. Use this exact structure:
159+
160+
````markdown
161+
## Usage
162+
163+
Run this codemod with:
164+
165+
```sh
166+
npx codemod @nodejs/<recipe-directory-name>
167+
```
168+
````
169+
170+
The package name is `@nodejs/` followed by the recipe's directory name as it appears under `recipes/`. For example, the recipe in `recipes/rmdir/` is run with:
171+
172+
```sh
173+
npx codemod @nodejs/rmdir
174+
```
175+
176+
If the codemod requires special flags or environment variables, document them here. See `recipes/correct-ts-specifiers/README.md` for an example of a recipe that requires `NODE_OPTIONS` to be set, along with guidance for running inside monorepos.
177+
178+
---
179+
180+
## Examples section
181+
182+
The `Examples` section is the most important part of the README. It shows exactly what the transformation does by deriving examples directly from the test fixtures in `tests/input/` and `tests/expected/`.
183+
184+
### How to derive examples from test fixtures
185+
186+
Each recipe has input fixtures in `tests/input/` and expected-output fixtures in `tests/expected/`. Read those files and construct `diff` blocks by diffing them line by line. Lines that are removed carry a `-` prefix; lines that are added carry a `+` prefix; lines that are unchanged and provide useful context carry a leading space.
187+
188+
For example, `tests/input/file-1.js` for the `rmdir` recipe contains:
189+
190+
```js
191+
const fs = require("node:fs");
192+
193+
const pathName = "path/to/directory";
194+
195+
fs.rmdir(pathName, { recursive: true }, () => {});
196+
fs.rmdirSync(pathName, { recursive: true });
197+
fs.promises.rmdir(pathName, { recursive: true });
198+
fs.rmdir(pathName, { recursive: false }); // should not be transformed
199+
fs.rmdir(pathName); // should not be transformed
200+
```
201+
202+
The corresponding `tests/expected/file-1.js` contains:
203+
204+
```js
205+
const fs = require("node:fs");
206+
207+
const pathName = "path/to/directory";
208+
209+
fs.rm(pathName, { recursive: true, force: true }, () => {});
210+
fs.rmSync(pathName, { recursive: true, force: true });
211+
fs.promises.rm(pathName, { recursive: true, force: true });
212+
fs.rmdir(pathName, { recursive: false }); // should not be transformed
213+
fs.rmdir(pathName); // should not be transformed
214+
```
215+
216+
This pair becomes the `diff` block shown in Example 1 of the `rmdir` README.
217+
218+
### Formatting diff blocks
219+
220+
Always use a fenced code block with the `diff` language tag:
221+
222+
````markdown
223+
```diff
224+
-fs.rmdir(pathName, { recursive: true }, () => { });
225+
+fs.rm(pathName, { recursive: true, force: true }, () => { });
226+
```
227+
````
228+
229+
Unchanged lines that provide essential context (such as the `require` statement or surrounding code) can be included without a prefix character. Omit lines that are not relevant to the transformation being illustrated. Do not pad with large blocks of unchanged code.
230+
231+
### Subsections for multiple transforms
232+
233+
When a recipe handles multiple distinct transformations, give each one its own `###` subsection with a short label that says what case it covers. Name subsections by the import style or the specific API variant, not by a generic "Case 1/2/3" numbering where a descriptive label is possible.
234+
235+
**Good subsection names:**
236+
237+
- `### Namespace require - all three API shapes are migrated`
238+
- `### Destructured require`
239+
- `### Aliased ESM import`
240+
- `### Reading crypto.fips becomes crypto.getFips()`
241+
242+
**Example of subsection structure:**
243+
244+
````markdown
245+
## Examples
246+
247+
### Example 1
248+
249+
Namespace `require` - all three API shapes are migrated, untouched calls are left alone:
250+
251+
```diff
252+
const fs = require("node:fs");
253+
-fs.rmdir(pathName, { recursive: true }, () => { });
254+
+fs.rm(pathName, { recursive: true, force: true }, () => { });
255+
```
256+
257+
### Example 2
258+
259+
Destructured `require` - `rm` is added to the destructured bindings and calls are updated:
260+
261+
```diff
262+
-const { rmdir, rmdirSync } = require("node:fs");
263+
+const { rm, rmdir, rmSync } = require("node:fs");
264+
```
265+
````
266+
267+
When a recipe makes a single, uniform transformation across all import styles, a single `diff` block without subsections is fine. See `recipes/util-extend-to-object-assign/README.md` for an example.
268+
269+
When the set of changes is better expressed as a table than as code (for example, a codemod that replaces many individual method names), a Markdown table is acceptable. See `recipes/util-is/README.md` for an example.
270+
271+
### How many examples to include
272+
273+
Include one example per meaningfully distinct case the codemod handles. At minimum, show one example. Show more when:
274+
275+
- The recipe handles both CJS and ESM import styles differently.
276+
- The recipe handles destructured bindings separately from namespace imports.
277+
- The recipe has edge cases that are explicitly not transformed.
278+
279+
Do not duplicate examples that differ only in whitespace or variable names.
280+
281+
---
282+
283+
## Notes section
284+
285+
The `Notes` section (heading `## Notes`) is for information that does not fit the description paragraph but is important for a reader running the codemod. Examples:
286+
287+
- The codemod reads `package.json` to determine module type and adjusts the output accordingly.
288+
- Internal bookkeeping fields used by the deprecated API have no public equivalent and must be reviewed manually.
289+
- The migration is one-and-done - the output should be committed and not re-run.
290+
291+
**Omit `## Notes` entirely when there is nothing to say.** Do not include an empty section or a placeholder like "N/A".
292+
293+
A `### Limitations` subsection may be nested under `## Notes` when the limitations are closely related to a noted behavior:
294+
295+
```markdown
296+
## Notes
297+
298+
### Limitations
299+
300+
Only calls that include `{ recursive: true }` in the options argument are transformed.
301+
Calls to `fs.rmdir` without that option are left untouched, as they do not trigger the deprecation.
302+
```
303+
304+
---
305+
306+
## Limitations section
307+
308+
The `Limitations` section (heading `## Limitations`, or `### Limitations` nested under `## Notes`) documents what the codemod explicitly does not handle. Examples:
309+
310+
- The codemod cannot determine algorithm-specific key or IV sizes; the reader must review and adjust the generated scaffolding.
311+
- `url.resolve` has no direct equivalent in the WHATWG URL API; the reader must implement custom logic.
312+
- Cases where the input is too dynamic to analyze statically are skipped and logged.
313+
314+
**Omit `## Limitations` entirely when there is nothing to say.**
315+
316+
A `## Caveats` heading is also acceptable and used in some recipes as an equivalent to `## Limitations`. Use either consistently; do not use both in the same file.
317+
318+
---
319+
320+
## Checklist for a complete README
321+
322+
Before opening a pull request, verify that the README:
323+
324+
- [ ] Opens with a `---`-delimited frontmatter block containing `authors: <github-username>`.
325+
- [ ] Has a title that follows the naming convention for its type (DEP number + arrow, DEP number + description, or plain descriptive).
326+
- [ ] Has a description paragraph that names the deprecated API, its replacement, and any notable variant handling.
327+
- [ ] Has a `## Usage` section with the correct `npx codemod @nodejs/<name>` command.
328+
- [ ] Has a `## Examples` section with at least one `diff` block derived from the test fixtures.
329+
- [ ] Uses `###` subsections in `## Examples` when there are multiple distinct transforms.
330+
- [ ] Includes `## Notes` and/or `## Limitations` only when there is substantive content to put in them.
331+
- [ ] Does not have any empty sections.

0 commit comments

Comments
 (0)