diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..e0609e8 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,27 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + - name: Install dependencies + run: npm ci + - name: Lint + run: npm run lint + - name: Format + run: npm run format + - name: Test + run: npm test + - name: Test with coverage + run: npm run test:coverage diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/package.json b/package.json index 9e2fea0..3120fde 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,12 @@ "name": "@node-core/api-docs-tooling", "scripts": { "lint": "eslint .", - "format": "prettier --write .", + "lint:fix": "eslint --fix .", + "format": "prettier .", + "format:write": "prettier --write .", + "test": "node --test", + "test:watch": "node --test --watch", + "test:coverage": "node --experimental-test-coverage --test", "prepare": "husky" }, "bin": { diff --git a/src/generators/json-simple/index.mjs b/src/generators/json-simple/index.mjs index ac12b65..5433bdc 100644 --- a/src/generators/json-simple/index.mjs +++ b/src/generators/json-simple/index.mjs @@ -25,7 +25,6 @@ export default { dependsOn: 'ast', async generate(input, options) { - // This simply grabs all the different files and stringifies them const stringifiedContent = JSON.stringify(input, null, 2); diff --git a/src/test/metadata.test.mjs b/src/test/metadata.test.mjs new file mode 100644 index 0000000..39129de --- /dev/null +++ b/src/test/metadata.test.mjs @@ -0,0 +1,58 @@ +import { strictEqual, deepStrictEqual } from 'node:assert'; +import { describe, it } from 'node:test'; +import GitHubSlugger from 'github-slugger'; +import { VFile } from 'vfile'; + +import createMetadata from '../metadata.mjs'; + +describe('createMetadata', () => { + it('should set the heading correctly', () => { + const slugger = new GitHubSlugger(); + const metadata = createMetadata(slugger); + const heading = { + text: 'Test Heading', + type: 'test', + name: 'test', + depth: 1, + }; + metadata.setHeading(heading); + strictEqual(metadata.create(new VFile(), {}).heading, heading); + }); + + it('should set the stability correctly', () => { + const slugger = new GitHubSlugger(); + const metadata = createMetadata(slugger); + const stability = 2; + metadata.setStability(stability); + strictEqual(metadata.create(new VFile(), {}).stability, stability); + }); + + it('should create a metadata entry correctly', () => { + const slugger = new GitHubSlugger(); + const metadata = createMetadata(slugger); + const apiDoc = new VFile({ path: 'test.md' }); + const section = { type: 'root', children: [] }; + const heading = { + text: 'Test Heading', + type: 'test', + name: 'test', + depth: 1, + }; + const stability = 2; + const properties = { source_link: 'test.com' }; + metadata.setHeading(heading); + metadata.setStability(stability); + metadata.updateProperties(properties); + const expected = { + api: 'test', + slug: 'test.html#test-heading', + sourceLink: 'test.com', + updates: [], + changes: [], + heading, + stability, + content: section, + }; + deepStrictEqual(metadata.create(apiDoc, section), expected); + }); +}); diff --git a/src/test/parser.test.mjs b/src/test/parser.test.mjs new file mode 100644 index 0000000..a53a901 --- /dev/null +++ b/src/test/parser.test.mjs @@ -0,0 +1,83 @@ +import { deepStrictEqual } from 'node:assert'; +import { describe, it } from 'node:test'; +import { VFile } from 'vfile'; +import createParser from '../parser.mjs'; + +describe('createParser', () => { + it('should parse a single API doc correctly', async () => { + const parser = createParser(); + const apiDoc = new VFile({ + path: 'test.md', + value: '# Test Heading\n\nThis is a test.', + }); + const expected = [ + { + api: 'test', + slug: 'test.html#test-heading', + sourceLink: undefined, + updates: [], + changes: [], + heading: { + text: 'Test Heading', + type: 'module', + name: 'Test Heading', + depth: 1, + }, + stability: undefined, + content: { type: 'root', children: [] }, + }, + ]; + const actual = await parser.parseApiDoc(apiDoc); + delete actual[0].content.toJSON; + deepStrictEqual(actual, expected); + }); + + it('should parse multiple API docs correctly', async () => { + const parser = createParser(); + const apiDocs = [ + new VFile({ + path: 'test1.md', + value: '# Test Heading 1\n\nThis is a test.', + }), + new VFile({ + path: 'test2.md', + value: '# Test Heading 2\n\nThis is another test.', + }), + ]; + const expected = [ + { + api: 'test1', + slug: 'test1.html#test-heading-1', + sourceLink: undefined, + updates: [], + changes: [], + heading: { + text: 'Test Heading 1', + type: 'module', + name: 'Test Heading 1', + depth: 1, + }, + stability: undefined, + content: { type: 'root', children: [] }, + }, + { + api: 'test2', + slug: 'test2.html#test-heading-2', + sourceLink: undefined, + updates: [], + changes: [], + heading: { + text: 'Test Heading 2', + type: 'module', + name: 'Test Heading 2', + depth: 1, + }, + stability: undefined, + content: { type: 'root', children: [] }, + }, + ]; + const actual = await parser.parseApiDocs(apiDocs); + actual.forEach(entry => delete entry.content.toJSON); + deepStrictEqual(actual, expected); + }); +}); diff --git a/src/test/queries.test.mjs b/src/test/queries.test.mjs new file mode 100644 index 0000000..acb02c9 --- /dev/null +++ b/src/test/queries.test.mjs @@ -0,0 +1,93 @@ +import { strictEqual, deepStrictEqual } from 'node:assert'; +import { describe, it } from 'node:test'; +import createQueries from '../queries.mjs'; + +describe('createQueries', () => { + it('should add YAML metadata correctly', () => { + const queries = createQueries(); + const node = { value: 'type: test\nname: test\n' }; + const apiEntryMetadata = { + updateProperties: properties => { + deepStrictEqual(properties, { type: 'test', name: 'test' }); + }, + }; + queries.addYAMLMetadata(node, apiEntryMetadata); + }); + + // valid type + it('should update type to reference correctly', () => { + const queries = createQueries(); + const node = { value: 'This is a {string} type.' }; + queries.updateTypeToReferenceLink(node); + strictEqual(node.type, 'html'); + strictEqual( + node.value, + 'This is a [`string`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) type.' + ); + }); + + it('should update type to reference not correctly if no match', () => { + const queries = createQueries(); + const node = { value: 'This is a {test} type.' }; + queries.updateTypeToReferenceLink(node); + strictEqual(node.type, 'html'); + strictEqual(node.value, 'This is a test type.'); + }); + + it('should add heading metadata correctly', () => { + const queries = createQueries(); + const node = { + depth: 2, + children: [{ type: 'text', value: 'Test Heading' }], + }; + const apiEntryMetadata = { + setHeading: heading => { + deepStrictEqual(heading, { + text: 'Test Heading', + type: 'module', + name: 'Test Heading', + depth: 2, + }); + }, + }; + queries.addHeadingMetadata(node, apiEntryMetadata); + }); + + it('should update markdown link correctly', () => { + const queries = createQueries(); + const node = { type: 'link', url: 'test.md#heading' }; + queries.updateMarkdownLink(node); + strictEqual(node.url, 'test.html#heading'); + }); + + it('should update link reference correctly', () => { + const queries = createQueries(); + const node = { type: 'linkReference', identifier: 'test' }; + const definitions = [{ identifier: 'test', url: 'test.html#test' }]; + queries.updateLinkReference(node, definitions); + strictEqual(node.type, 'link'); + strictEqual(node.url, 'test.html#test'); + }); + + it('should add stability index metadata correctly', () => { + const queries = createQueries(); + const node = { + type: 'blockquote', + children: [ + { + type: 'paragraph', + children: [{ type: 'text', value: 'Stability: 2 - Frozen' }], + }, + ], + }; + const apiEntryMetadata = { + setStability: stability => { + deepStrictEqual(stability.toJSON(), { + index: 2, + description: 'Frozen', + }); + }, + }; + queries.addStabilityIndexMetadata(node, apiEntryMetadata); + }); +}); diff --git a/src/utils/tests/parser.test.mjs b/src/utils/tests/parser.test.mjs new file mode 100644 index 0000000..a00f068 --- /dev/null +++ b/src/utils/tests/parser.test.mjs @@ -0,0 +1,84 @@ +import { strictEqual, deepStrictEqual } from 'node:assert'; +import { describe, it } from 'node:test'; + +import { + parseYAMLIntoMetadata, + transformTypeToReferenceLink, + parseHeadingIntoMetadata, +} from '../parser.mjs'; + +describe('transformTypeToReferenceLink', () => { + it('should transform a JavaScript primitive type into a Markdown link', () => { + strictEqual( + transformTypeToReferenceLink('string'), + '[`string`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type)' + ); + }); + + it('should transform a JavaScript global type into a Markdown link', () => { + strictEqual( + transformTypeToReferenceLink('Array'), + '[`Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)' + ); + }); +}); + +describe('parseYAMLIntoMetadata', () => { + it('should parse a YAML string into a JavaScript object', () => { + const input = 'name: test\ntype: string\nintroduced_in: v1.0.0'; + const expectedOutput = { + name: 'test', + type: 'string', + updates: [ + { + type: 'introduced_in', + version: ['v1.0.0'], + }, + ], + }; + deepStrictEqual(parseYAMLIntoMetadata(input), expectedOutput); + }); + + it('should parse a YAML string with multiple versions into a JavaScript object', () => { + const input = 'name: test\ntype: string\nintroduced_in: [v1.0.0, v1.1.0]'; + const expectedOutput = { + name: 'test', + type: 'string', + updates: [ + { + type: 'introduced_in', + version: ['v1.0.0', 'v1.1.0'], + }, + ], + }; + deepStrictEqual(parseYAMLIntoMetadata(input), expectedOutput); + }); + + it('should parse a YAML string with source_link into a JavaScript object', () => { + const input = + 'name: test\ntype: string\nintroduced_in: v1.0.0\nsource_link: https://github.com/nodejs/node'; + const expectedOutput = { + name: 'test', + type: 'string', + updates: [ + { + type: 'introduced_in', + version: ['v1.0.0'], + }, + ], + source_link: 'https://github.com/nodejs/node', + }; + deepStrictEqual(parseYAMLIntoMetadata(input), expectedOutput); + }); + + it('should parse a raw Heading string into Heading metadata', () => { + const input = '## test'; + const expectedOutput = { + text: '## test', + type: 'module', + name: '## test', + depth: 2, + }; + deepStrictEqual(parseHeadingIntoMetadata(input, 2), expectedOutput); + }); +}); diff --git a/src/utils/tests/slugger.test.mjs b/src/utils/tests/slugger.test.mjs new file mode 100644 index 0000000..d4c0dc3 --- /dev/null +++ b/src/utils/tests/slugger.test.mjs @@ -0,0 +1,22 @@ +import { strictEqual } from 'node:assert'; +import { describe, it } from 'node:test'; + +import { createNodeSlugger } from '../slugger.mjs'; + +describe('createNodeSlugger', () => { + it('should create a new instance of the GitHub Slugger', () => { + const slugger = createNodeSlugger(); + strictEqual(typeof slugger.slug, 'function'); + strictEqual(typeof slugger.reset, 'function'); + }); + + it('should create a new slug based on the provided string', () => { + const slugger = createNodeSlugger(); + strictEqual(slugger.slug('Test'), 'test'); + }); + + it('should reset the cache of the Slugger', () => { + const slugger = createNodeSlugger(); + slugger.reset(); + }); +}); diff --git a/src/utils/tests/unist.test.mjs b/src/utils/tests/unist.test.mjs new file mode 100644 index 0000000..a67de08 --- /dev/null +++ b/src/utils/tests/unist.test.mjs @@ -0,0 +1,72 @@ +import { strictEqual } from 'node:assert'; +import { describe, it } from 'node:test'; + +import { transformNodesToString, callIfBefore } from '../unist.mjs'; + +describe('transformNodesToString', () => { + it('should transform a list of Nodes into a string', () => { + const nodes = [ + { + type: 'paragraph', + children: [ + { + type: 'text', + value: 'Hello, ', + }, + { + type: 'strong', + children: [ + { + type: 'text', + value: 'World', + }, + ], + }, + ], + }, + ]; + strictEqual(transformNodesToString(nodes), 'Hello, **World**'); + }); +}); + +describe('callIfBefore', () => { + it('should call the callback if the first Node is before the second Node', () => { + const nodeA = { + position: { + start: { + line: 1, + }, + }, + }; + const nodeB = { + position: { + start: { + line: 2, + }, + }, + }; + callIfBefore(nodeA, nodeB, (nodeA, nodeB) => { + strictEqual(nodeA.position.start.line < nodeB.position.start.line, true); + }); + }); + + it('should not call the callback if the first Node is not before the second Node', () => { + const nodeA = { + position: { + start: { + line: 2, + }, + }, + }; + const nodeB = { + position: { + start: { + line: 1, + }, + }, + }; + callIfBefore(nodeA, nodeB, (nodeA, nodeB) => { + strictEqual(nodeA.position.start.line < nodeB.position.start.line, true); + }); + }); +});