Skip to content

Commit

Permalink
feat: create markdown snippets linter (nodejs#7431)
Browse files Browse the repository at this point in the history
* feat: create markdown snippets linter

* chore: review

* chore: rollback test
  • Loading branch information
araujogui authored Jan 29, 2025
1 parent 7efa770 commit 70f5a9f
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 32 deletions.
3 changes: 2 additions & 1 deletion apps/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
"deploy": "cross-env NEXT_PUBLIC_STATIC_EXPORT=true npm run build",
"check-types": "tsc --noEmit",
"lint:js": "eslint \"**/*.{js,mjs,ts,tsx}\"",
"lint:snippets": "node ./scripts/lint-snippets/index.mjs",
"lint:md": "eslint \"**/*.md?(x)\" --cache --cache-strategy=content --cache-location=.eslintmdcache",
"lint:css": "stylelint \"**/*.css\" --allow-empty-input --cache --cache-strategy=content --cache-location=.stylelintcache",
"lint": "turbo run lint:md lint:js lint:css",
"lint": "turbo run lint:md lint:snippets lint:js lint:css",
"lint:fix": "turbo run lint:md lint:js lint:css --no-cache -- --fix",
"sync-orama": "node ./scripts/orama-search/sync-orama-cloud.mjs",
"storybook": "cross-env NODE_NO_WARNINGS=1 storybook dev -p 6006 --no-open",
Expand Down
68 changes: 41 additions & 27 deletions apps/site/pages/en/learn/modules/how-to-use-streams.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,18 +248,24 @@ class MyStream extends Writable {
process.stdout.write(data.toString().toUpperCase() + '\n', cb);
}
}
const stream = new MyStream();

for (let i = 0; i < 10; i++) {
const waitDrain = !stream.write('hello');
async function main() {
const stream = new MyStream();

if (waitDrain) {
console.log('>> wait drain');
await once(stream, 'drain');
for (let i = 0; i < 10; i++) {
const waitDrain = !stream.write('hello');

if (waitDrain) {
console.log('>> wait drain');
await once(stream, 'drain');
}
}

stream.end('world');
}

stream.end('world');
// Call the async function
main().catch(console.error);
```

```mjs
Expand Down Expand Up @@ -663,15 +669,19 @@ Here's an example demonstrating the use of async iterators with a readable strea
const fs = require('node:fs');
const { pipeline } = require('node:stream/promises');

await pipeline(
fs.createReadStream(import.meta.filename),
async function* (source) {
for await (let chunk of source) {
yield chunk.toString().toUpperCase();
}
},
process.stdout
);
async function main() {
await pipeline(
fs.createReadStream(__filename),
async function* (source) {
for await (let chunk of source) {
yield chunk.toString().toUpperCase();
}
},
process.stdout
);
}

main().catch(console.error);
```
```mjs
Expand Down Expand Up @@ -798,18 +808,22 @@ The helper functions are useful if you need to return a Web Stream from a Node.j
```cjs
const { pipeline } = require('node:stream/promises');

const { body } = await fetch('https://nodejs.org/api/stream.html');
async function main() {
const { body } = await fetch('https://nodejs.org/api/stream.html');

await pipeline(
body,
new TextDecoderStream(),
async function* (source) {
for await (const chunk of source) {
yield chunk.toString().toUpperCase();
}
},
process.stdout
);
await pipeline(
body,
new TextDecoderStream(),
async function* (source) {
for await (const chunk of source) {
yield chunk.toString().toUpperCase();
}
},
process.stdout
);
}

main().catch(console.error);
```
```mjs
Expand Down
9 changes: 5 additions & 4 deletions apps/site/pages/en/learn/test-runner/mocking.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,20 +149,19 @@ This leverages [`mock`](https://nodejs.org/api/test.html#class-mocktracker) from
import assert from 'node:assert/strict';
import { before, describe, it, mock } from 'node:test';


describe('foo', { concurrency: true }, () => {
let barMock = mock.fn();
let foo;

before(async () => {
const barNamedExports = await import('./bar.mjs')
// discard the original default export
.then(({ default, ...rest }) => rest);
.then(({ default: _, ...rest }) => rest);

// It's usually not necessary to manually call restore() after each
// nor reset() after all (node does this automatically).
mock.module('./bar.mjs', {
defaultExport: barMock
defaultExport: barMock,
// Keep the other exports that you don't want to mock.
namedExports: barNamedExports,
});
Expand All @@ -173,7 +172,9 @@ describe('foo', { concurrency: true }, () => {
});

it('should do the thing', () => {
barMock.mockImplementationOnce(function bar_mock() {/**/});
barMock.mockImplementationOnce(function bar_mock() {
/**/
});

assert.equal(foo(), 42);
});
Expand Down
96 changes: 96 additions & 0 deletions apps/site/scripts/lint-snippets/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { readFile } from 'node:fs/promises';

import { parse } from 'acorn';
import { glob } from 'glob';
import remarkParse from 'remark-parse';
import { unified } from 'unified';
import { visit } from 'unist-util-visit';

const SUPPORTED_LANGUAGES = ['js', 'mjs', 'cjs'];

// Initialize the markdown parser
const markdownParser = unified().use(remarkParse);

/**
* Parse JavaScript code using Acorn
*
* @param {string} code - The code to parse
* @param {string} language - The language identifier
* @returns {void}
* @throws {Error} If parsing fails
*/
function parseJavaScript(code, language) {
parse(code, {
ecmaVersion: 'latest',
sourceType: language === 'cjs' ? 'script' : 'module',
allowReturnOutsideFunction: true,
});
}

/**
* Validate code blocks in a markdown file
*
* @param {string} filePath - Path to the markdown file
* @returns {Array<{path: string, position: number, message: string}>} Array of errors
*/
async function validateFile(filePath) {
const errors = [];

const content = await readFile(filePath, 'utf-8');
const tree = markdownParser.parse(content);

visit(tree, 'code', node => {
// TODO: Add TypeScript support
if (!SUPPORTED_LANGUAGES.includes(node.lang)) {
return;
}

try {
parseJavaScript(node.value, node.lang);
} catch (err) {
errors.push({
path: filePath,
position: node.position.start.line,
message: err.message,
});
}
});

return errors;
}

/**
* Print validation errors to console
*
* @param {Array<{path: string, position: number, message: string}>} errors
* @returns {void}
*/
function reportErrors(errors) {
if (errors.length === 0) {
return;
}

console.error('Errors found in the following files:');
errors.forEach(({ path, position, message }) => {
console.error(`- ${path}:${position}: ${message}`);
});
}

// Get all markdown files
const filePaths = await glob('**/*.md', {
root: process.cwd(),
cwd: 'apps/site/pages/en/learn/',
absolute: true,
});

// Validate all files and collect errors
const allErrors = await Promise.all(filePaths.map(validateFile));

// Flatten the array of errors
const flattenedErrors = allErrors.flat();

// Report errors if any
reportErrors(flattenedErrors);

// Exit with appropriate code
process.exit(flattenedErrors.length > 0 ? 1 : 0);
4 changes: 4 additions & 0 deletions apps/site/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@
"inputs": ["{app,pages}/**/*.{md,mdx}", "*.{md,mdx}"],
"outputs": [".eslintmdcache"]
},
"lint:snippets": {
"inputs": ["{app,pages}/**/*.{md,mdx}", "*.{md,mdx}"],
"outputs": []
},
"lint:css": {
"inputs": ["{app,components,layouts,pages,styles}/**/*.css"],
"outputs": [".stylelintcache"]
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"prepare": "husky"
},
"dependencies": {
"acorn": "^8.14.0",
"husky": "9.1.7",
"lint-staged": "15.3.0",
"turbo": "2.3.3"
Expand Down
2 changes: 2 additions & 0 deletions turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
"lint": {
"dependsOn": [
"@node-core/website#lint:md",
"@node-core/website#lint:snippets",
"@node-core/website#lint:css",
"lint:js"
]
},
"lint:lint-staged": {
"dependsOn": [
"@node-core/website#lint:md",
"@node-core/website#lint:snippets",
"@node-core/website#lint:css",
"@node-core/website#lint:js"
]
Expand Down

0 comments on commit 70f5a9f

Please sign in to comment.