Skip to content

Commit 16b7d0d

Browse files
authoredMar 13, 2019
feat(gatsby-transformer-documentationjs): support linking typedefs (#11597)
1 parent 0fe1148 commit 16b7d0d

File tree

6 files changed

+1373
-596
lines changed

6 files changed

+1373
-596
lines changed
 

‎packages/gatsby-transformer-documentationjs/package.json

+2-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
},
99
"dependencies": {
1010
"@babel/runtime": "^7.0.0",
11-
"documentation": "^7.1.0",
11+
"documentation": "^9.0.0",
1212
"prismjs": "^1.14.0"
1313
},
1414
"devDependencies": {
@@ -34,6 +34,5 @@
3434
"build": "babel src --out-dir . --ignore **/__tests__",
3535
"prepare": "cross-env NODE_ENV=production npm run build",
3636
"watch": "babel -w src --out-dir . --ignore **/__tests__"
37-
},
38-
"gitHead": "5bd5aebe066b9875354a81a4b9ed98722731c465"
37+
}
3938
}
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,74 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`transformer-react-doc-gen: onCreateNode should extract out a description, params, and examples 1`] = `
4-
Array [
5-
Object {
6-
"children": Array [],
7-
"id": "uuid-from-gatsby",
8-
"internal": Object {
9-
"content": "A pretty cool jsdoc example
10-
",
11-
"contentDigest": "1effd106a9028bac69276a250a0f96f5",
12-
"mediaType": "text/markdown",
13-
"type": "DocumentationJSComponentDescription",
3+
exports[`transformer-react-doc-gen: onCreateNode Complex example should handle members should handle type unions 1`] = `
4+
Object {
5+
"elements": Array [
6+
Object {
7+
"name": "ObjectType",
8+
"type": "NameExpression",
9+
"typeDef___NODE": "documentationJS node_1 path #[{\\"name\\":\\"ObjectType\\",\\"kind\\":\\"typedef\\"}]",
1410
},
15-
"parent": "node_1",
16-
},
17-
Object {
18-
"children": Array [],
19-
"id": "uuid-from-gatsby",
20-
"internal": Object {
21-
"content": "A nice crispy apple
22-
",
23-
"contentDigest": "bdc56b714e173673c86ecf5f556c5fd1",
24-
"mediaType": "text/markdown",
25-
"type": "DocumentationJSComponentDescription",
11+
Object {
12+
"name": "Object",
13+
"type": "NameExpression",
14+
"typeDef___NODE": null,
2615
},
27-
"parent": "node_1",
16+
],
17+
"type": "UnionType",
18+
}
19+
`;
20+
21+
exports[`transformer-react-doc-gen: onCreateNode Complex example should handle typedefs should handle type applications 1`] = `
22+
Object {
23+
"children": Array [
24+
"documentationJS node_1 path #[{\\"name\\":\\"ObjectType\\",\\"kind\\":\\"typedef\\"},{\\"fieldName\\":\\"properties\\",\\"fieldIndex\\":0}]--DocumentationJSComponentDescription--comment.description",
25+
],
26+
"commentNumber": null,
27+
"description___NODE": "documentationJS node_1 path #[{\\"name\\":\\"ObjectType\\",\\"kind\\":\\"typedef\\"},{\\"fieldName\\":\\"properties\\",\\"fieldIndex\\":0}]--DocumentationJSComponentDescription--comment.description",
28+
"id": "documentationJS node_1 path #[{\\"name\\":\\"ObjectType\\",\\"kind\\":\\"typedef\\"},{\\"fieldName\\":\\"properties\\",\\"fieldIndex\\":0}]",
29+
"internal": Object {
30+
"contentDigest": "content-digest",
31+
"type": "DocumentationJs",
2832
},
29-
Object {
30-
"children": Array [],
31-
"commentNumber": 0,
32-
"description___NODE": "uuid-from-gatsby",
33-
"examples": Array [
33+
"level": 1,
34+
"name": "ready",
35+
"optional": false,
36+
"parent": "documentationJS node_1 path #[{\\"name\\":\\"ObjectType\\",\\"kind\\":\\"typedef\\"}]",
37+
"type": Object {
38+
"applications": Array [
3439
Object {
35-
"highlighted": "<span class=\\"token keyword\\">const</span> apple <span class=\\"token operator\\">=</span> <span class=\\"token function\\">require</span><span class=\\"token punctuation\\">(</span><span class=\\"token string\\">'apple'</span><span class=\\"token punctuation\\">)</span>
36-
<span class=\\"token function\\">apple</span><span class=\\"token punctuation\\">(</span><span class=\\"token punctuation\\">)</span>",
37-
"raw": "const apple = require('apple')
38-
apple()",
40+
"name": "any",
41+
"type": "NameExpression",
42+
"typeDef___NODE": null,
3943
},
4044
],
41-
"id": "uuid-from-gatsby",
42-
"internal": Object {
43-
"contentDigest": "eb588cf103987de4bb07a2d28a5cf7c5",
44-
"type": "DocumentationJs",
45+
"expression": Object {
46+
"name": "Promise",
47+
"type": "NameExpression",
48+
"typeDef___NODE": null,
4549
},
46-
"kind": "constant",
47-
"name": "apple",
48-
"params": Array [
49-
Object {
50-
"description___NODE": "uuid-from-gatsby",
51-
"name": "apple",
52-
"title": "param",
53-
"type": Object {
54-
"name": "string",
55-
"type": "NameExpression",
56-
},
57-
},
58-
],
59-
"parent": "node_1",
60-
"returns": Array [],
50+
"type": "TypeApplication",
6151
},
62-
]
52+
}
6353
`;
54+
55+
exports[`transformer-react-doc-gen: onCreateNode Simple example should extract out a description, params, and examples: description content 1`] = `
56+
"A pretty cool jsdoc example
57+
"
58+
`;
59+
60+
exports[`transformer-react-doc-gen: onCreateNode Simple example should extract out a description, params, and examples: example 1`] = `
61+
Object {
62+
"highlighted": "<span class=\\"token keyword\\">const</span> apple <span class=\\"token operator\\">=</span> <span class=\\"token function\\">require</span><span class=\\"token punctuation\\">(</span><span class=\\"token string\\">'apple'</span><span class=\\"token punctuation\\">)</span>
63+
<span class=\\"token function\\">apple</span><span class=\\"token punctuation\\">(</span><span class=\\"token punctuation\\">)</span>",
64+
"raw": "const apple = require('apple')
65+
apple()",
66+
}
67+
`;
68+
69+
exports[`transformer-react-doc-gen: onCreateNode Simple example should extract out a description, params, and examples: param description 1`] = `
70+
"A nice crispy apple
71+
"
72+
`;
73+
74+
exports[`transformer-react-doc-gen: onCreateNode Simple example should extract out a description, params, and examples: param name 1`] = `"paramName"`;
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,46 @@
11
/**
22
* A pretty cool jsdoc example
3-
* @param {string} apple A nice crispy apple
3+
* @param {string} paramName A nice crispy apple
44
* @example
55
* const apple = require('apple')
66
* apple()
77
*/
8-
const apple = apple => {
8+
exports.apple = paramName => {
99
console.log(`hi`)
1010
}
11+
12+
/**
13+
* Description of callback type
14+
* @callback CallbackType
15+
* @param {string} message A message to split
16+
* @returns {string[]} Splitted message
17+
*/
18+
19+
/**
20+
* More complex example
21+
*/
22+
exports.complex = {
23+
/**
24+
* Description of object type
25+
* @typedef {Object} ObjectType
26+
* @property {Promise<any>} ready Returns promise that resolves when instance is ready to use
27+
* @property {Object} nested This is nested object
28+
* @property {string} nested.foo This is field in nested object
29+
* @property {number} [nested.optional] This is optional field in nested object
30+
* @property {CallbackType} nested.callback This is function in nested object
31+
*/
32+
33+
/**
34+
* Description of function
35+
* @type {(ObjectType|Object)}
36+
*/
37+
object: {
38+
ready: new Promise(),
39+
nested: {
40+
foo: `bar`,
41+
callback: message => {
42+
return message.split(`,`)
43+
}
44+
}
45+
}
46+
}

‎packages/gatsby-transformer-documentationjs/src/__tests__/gatsby-node.js

+245-44
Original file line numberDiff line numberDiff line change
@@ -3,66 +3,267 @@ import path from "path"
33
import gatsbyNode from "../gatsby-node"
44

55
describe(`transformer-react-doc-gen: onCreateNode`, () => {
6-
let actions, node, createdNodes, updatedNodes
7-
const createNodeId = jest.fn()
8-
createNodeId.mockReturnValue(`uuid-from-gatsby`)
9-
let run = (node = node, opts = {}) =>
10-
gatsbyNode.onCreateNode(
6+
let createdNodes, updatedNodes
7+
const createNodeId = jest.fn(id => id)
8+
const createContentDigest = jest.fn().mockReturnValue(`content-digest`)
9+
10+
const node = {
11+
id: `node_1`,
12+
children: [],
13+
absolutePath: path.join(__dirname, `fixtures`, `code.js`),
14+
internal: {
15+
mediaType: `application/javascript`,
16+
type: `File`,
17+
},
18+
}
19+
20+
const actions = {
21+
createNode: jest.fn(n => createdNodes.push(n)),
22+
createParentChildLink: jest.fn(n => {
23+
updatedNodes.push(n)
24+
const parentNode = createdNodes.find(node => node.id === n.parent.id)
25+
if (parentNode) {
26+
parentNode.children.push(n.child.id)
27+
} else if (n.parent.id !== `node_1`) {
28+
throw new Error(`Creating parent-child link for not existing parent`)
29+
}
30+
}),
31+
}
32+
33+
const run = async (node = node, opts = {}) => {
34+
createdNodes = []
35+
updatedNodes = []
36+
await gatsbyNode.onCreateNode(
1137
{
1238
node,
1339
actions,
1440
createNodeId,
41+
createContentDigest,
1542
},
1643
opts
1744
)
45+
}
1846

19-
beforeEach(() => {
20-
createdNodes = []
21-
updatedNodes = []
22-
node = {
23-
id: `node_1`,
24-
children: [],
25-
absolutePath: path.join(__dirname, `fixtures`, `code.js`),
26-
internal: {
27-
mediaType: `application/javascript`,
28-
type: `File`,
29-
},
30-
}
31-
actions = {
32-
createNode: jest.fn(n => createdNodes.push(n)),
33-
createParentChildLink: jest.fn(n => updatedNodes.push(n)),
34-
}
35-
})
36-
37-
it(`should extract out a description, params, and examples`, async () => {
47+
beforeAll(async () => {
3848
await run(node)
39-
expect(createdNodes).toMatchSnapshot()
4049
})
4150

42-
it(`should only process javascript File nodes`, async () => {
43-
let result
44-
result = await run({ internal: { mediaType: `text/x-foo` } })
45-
expect(result).toBeNull()
51+
describe(`Simple example`, () => {
52+
it(`creates doc json apple node`, () => {
53+
const appleNode = createdNodes.find(node => node.name === `apple`)
54+
expect(appleNode).toBeDefined()
55+
})
56+
57+
it(`should extract out a description, params, and examples`, () => {
58+
const appleNode = createdNodes.find(node => node.name === `apple`)
4659

47-
result = await run({ internal: { mediaType: `application/javascript` } })
48-
expect(result).toBeNull()
60+
expect(appleNode.examples.length).toBe(1)
61+
expect(appleNode.examples[0]).toMatchSnapshot(`example`)
4962

50-
result = await run({
51-
id: `test`,
52-
children: [],
53-
absolutePath: path.join(__dirname, `fixtures`, `code.js`),
54-
internal: { mediaType: `application/javascript`, type: `File` },
63+
const appleDescriptionNode = createdNodes.find(
64+
node => node.id === appleNode.description___NODE
65+
)
66+
67+
expect(appleDescriptionNode).toBeDefined()
68+
expect(appleDescriptionNode.internal.content).toMatchSnapshot(
69+
`description content`
70+
)
71+
72+
const paramNode = createdNodes.find(
73+
node => node.id === appleNode.params___NODE[0]
74+
)
75+
76+
expect(paramNode).toBeDefined()
77+
expect(paramNode.name).toMatchSnapshot(`param name`)
78+
79+
const paramDescriptionNode = createdNodes.find(
80+
node => node.id === paramNode.description___NODE
81+
)
82+
83+
expect(paramDescriptionNode).toBeDefined()
84+
expect(paramDescriptionNode.internal.content).toMatchSnapshot(
85+
`param description`
86+
)
87+
})
88+
89+
it(`should extract code and docs location`, () => {
90+
const appleNode = createdNodes.find(node => node.name === `apple`)
91+
92+
expect(appleNode.docsLocation).toBeDefined()
93+
expect(appleNode.docsLocation).toEqual(
94+
expect.objectContaining({
95+
start: expect.objectContaining({
96+
line: 1,
97+
}),
98+
end: expect.objectContaining({
99+
line: 7,
100+
}),
101+
})
102+
)
103+
104+
expect(appleNode.codeLocation).toBeDefined()
105+
expect(appleNode.codeLocation).toEqual(
106+
expect.objectContaining({
107+
start: expect.objectContaining({
108+
line: 8,
109+
}),
110+
end: expect.objectContaining({
111+
line: 10,
112+
}),
113+
})
114+
)
55115
})
56-
expect(createdNodes.length).toBeGreaterThan(0)
57116
})
58117

59-
it(`should extract create description nodes with markdown types`, async () => {
60-
await run(node)
61-
let types = groupBy(createdNodes, `internal.type`)
62-
expect(
63-
types.DocumentationJSComponentDescription.every(
64-
d => d.internal.mediaType === `text/markdown`
118+
describe(`Complex example`, () => {
119+
let callbackNode, typedefNode
120+
121+
it(`should create top-level node for callback`, () => {
122+
callbackNode = createdNodes.find(
123+
node =>
124+
node.name === `CallbackType` &&
125+
node.kind === `typedef` &&
126+
node.parent === `node_1`
65127
)
66-
).toBe(true)
128+
expect(callbackNode).toBeDefined()
129+
})
130+
131+
describe(`should handle typedefs`, () => {
132+
it(`should create top-level node for typedef`, () => {
133+
typedefNode = createdNodes.find(
134+
node =>
135+
node.name === `ObjectType` &&
136+
node.kind === `typedef` &&
137+
node.parent === `node_1`
138+
)
139+
expect(typedefNode).toBeDefined()
140+
})
141+
142+
let readyNode, nestedNode
143+
144+
it(`should have property nodes for typedef`, () => {
145+
expect(typedefNode.properties___NODE).toBeDefined()
146+
expect(typedefNode.properties___NODE.length).toBe(2)
147+
;[readyNode, nestedNode] = typedefNode.properties___NODE.map(paramID =>
148+
createdNodes.find(node => node.id === paramID)
149+
)
150+
})
151+
152+
it(`should handle type applications`, () => {
153+
expect(readyNode).toMatchSnapshot()
154+
})
155+
156+
let nestedFooNode, nestedOptionalNode, nestedCallbackNode
157+
158+
it(`should have second param as nested object`, () => {
159+
expect(nestedNode.name).toBe(`nested`)
160+
expect(nestedNode.properties___NODE).toBeDefined()
161+
expect(nestedNode.properties___NODE.length).toBe(3)
162+
;[
163+
nestedFooNode,
164+
nestedOptionalNode,
165+
nestedCallbackNode,
166+
] = nestedNode.properties___NODE.map(paramID =>
167+
createdNodes.find(node => node.id === paramID)
168+
)
169+
})
170+
171+
it(`should strip prefixes from nested nodes`, () => {
172+
expect(nestedFooNode.name).not.toContain(`nested`)
173+
expect(nestedFooNode.name).toEqual(`foo`)
174+
})
175+
176+
it(`should handle optional types`, () => {
177+
expect(nestedOptionalNode.name).toEqual(`optional`)
178+
expect(nestedOptionalNode.optional).toEqual(true)
179+
expect(nestedOptionalNode.type).toEqual(
180+
expect.objectContaining({
181+
name: `number`,
182+
type: `NameExpression`,
183+
})
184+
)
185+
})
186+
187+
it(`should handle typedefs in nested properties`, () => {
188+
expect(nestedCallbackNode.name).toEqual(`callback`)
189+
expect(nestedCallbackNode.optional).toEqual(false)
190+
expect(nestedCallbackNode.type).toEqual(
191+
expect.objectContaining({
192+
name: `CallbackType`,
193+
type: `NameExpression`,
194+
typeDef___NODE: callbackNode.id,
195+
})
196+
)
197+
})
198+
})
199+
200+
describe(`should handle members`, () => {
201+
let complexNode, memberNode
202+
beforeAll(() => {
203+
complexNode = createdNodes.find(
204+
node => node.name === `complex` && node.parent === `node_1`
205+
)
206+
})
207+
208+
it(`should create top-level node for complex type`, () => {
209+
expect(complexNode).toBeDefined()
210+
})
211+
212+
it(`should have link from complex node to its members`, () => {
213+
expect(complexNode.members).toBeDefined()
214+
expect(complexNode.members.static___NODE).toBeDefined()
215+
expect(complexNode.members.static___NODE.length).toBe(1)
216+
217+
memberNode = createdNodes.find(
218+
node => node.id === complexNode.members.static___NODE[0]
219+
)
220+
expect(memberNode).toBeDefined()
221+
expect(memberNode.parent).toEqual(complexNode.id)
222+
})
223+
224+
it(`should handle type unions`, () => {
225+
expect(memberNode.type).toMatchSnapshot()
226+
})
227+
228+
it(`should link to type to type definition`, () => {
229+
const typeElement = memberNode.type.elements.find(
230+
type => type.name === `ObjectType`
231+
)
232+
expect(typeElement.typeDef___NODE).toBe(typedefNode.id)
233+
})
234+
})
235+
})
236+
237+
describe(`Sanity checks`, () => {
238+
it(`should extract create description nodes with markdown types`, () => {
239+
let types = groupBy(createdNodes, `internal.type`)
240+
expect(
241+
types.DocumentationJSComponentDescription.every(
242+
d => d.internal.mediaType === `text/markdown`
243+
)
244+
).toBe(true)
245+
})
246+
247+
it(`creates parent nodes before children`, () => {
248+
const seenNodes = []
249+
createdNodes.forEach(node => {
250+
seenNodes.push(node.id)
251+
252+
node.children.forEach(childID => {
253+
expect(seenNodes.includes(childID)).not.toBe(true)
254+
})
255+
})
256+
})
257+
258+
it(`should only process javascript File nodes`, async () => {
259+
await run({ internal: { mediaType: `text/x-foo` } })
260+
expect(createdNodes.length).toBe(0)
261+
262+
await run({ internal: { mediaType: `application/javascript` } })
263+
expect(createdNodes.length).toBe(0)
264+
265+
await run(node)
266+
expect(createdNodes.length).toBeGreaterThan(0)
267+
})
67268
})
68269
})

‎packages/gatsby-transformer-documentationjs/src/gatsby-node.js

+215-102
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
11
const documentation = require(`documentation`)
2-
const crypto = require(`crypto`)
3-
const digest = str =>
4-
crypto
5-
.createHash(`md5`)
6-
.update(str)
7-
.digest(`hex`)
82
const remark = require(`remark`)
93
const _ = require(`lodash`)
104
const Prism = require(`prismjs`)
@@ -17,49 +11,35 @@ const stringifyMarkdownAST = (node = ``) => {
1711
}
1812
}
1913

20-
const commentId = (parentId, commentNumber) =>
21-
`documentationJS ${parentId} comment #${commentNumber}`
14+
const docId = (parentId, docsJson) =>
15+
`documentationJS ${parentId} path #${JSON.stringify(docsJson.path)}`
2216
const descriptionId = (parentId, name) =>
2317
`${parentId}--DocumentationJSComponentDescription--${name}`
2418

25-
function createDescriptionNode(
26-
node,
27-
docNodeId,
28-
markdownStr,
29-
name,
30-
actions,
31-
createNodeId
32-
) {
33-
const { createNode } = actions
19+
function prepareDescriptionNode(node, markdownStr, name, helpers) {
20+
const { createNodeId, createContentDigest } = helpers
3421

3522
const descriptionNode = {
36-
id: createNodeId(descriptionId(docNodeId, name)),
23+
id: createNodeId(descriptionId(node.id, name)),
3724
parent: node.id,
3825
children: [],
3926
internal: {
4027
type: `DocumentationJSComponentDescription`,
4128
mediaType: `text/markdown`,
4229
content: markdownStr,
43-
contentDigest: digest(markdownStr),
30+
contentDigest: createContentDigest(markdownStr),
4431
},
4532
}
4633

47-
node.children = node.children.concat([descriptionNode.id])
48-
createNode(descriptionNode)
49-
50-
return descriptionNode.id
34+
return descriptionNode
5135
}
5236

5337
/**
5438
* Implement the onCreateNode API to create documentation.js nodes
5539
* @param {Object} super this is a super param
5640
*/
57-
exports.onCreateNode = async ({
58-
node,
59-
loadNodeContent,
60-
actions,
61-
createNodeId,
62-
}) => {
41+
exports.onCreateNode = async ({ node, actions, ...helpers }) => {
42+
const { createNodeId, createContentDigest } = helpers
6343
const { createNode, createParentChildLink } = actions
6444

6545
if (
@@ -80,76 +60,187 @@ exports.onCreateNode = async ({
8060
}
8161

8262
if (documentationJson && documentationJson.length > 0) {
83-
documentationJson.forEach((docsJson, i) => {
84-
const picked = _.pick(docsJson, [`kind`, `memberof`, `name`, `scope`])
85-
86-
// Defaults
87-
picked.params = [{ name: ``, type: { type: ``, name: `` } }]
88-
picked.returns = [{ type: { type: ``, name: `` } }]
89-
picked.examples = [{ raw: ``, highlighted: `` }]
90-
91-
// Prepare various sub-pieces.
92-
if (docsJson.description) {
93-
picked.description___NODE = createDescriptionNode(
94-
node,
95-
commentId(node.id, i),
96-
stringifyMarkdownAST(docsJson.description),
97-
`comment.description`,
98-
actions,
99-
createNodeId
100-
)
63+
const handledDocs = new WeakMap()
64+
const typeDefs = new Map()
65+
66+
const getNodeIDForType = typeName => {
67+
if (typeDefs.has(typeName)) {
68+
return typeDefs.get(typeName)
10169
}
10270

103-
const transformParam = param => {
104-
if (param.description) {
105-
param.description___NODE = createDescriptionNode(
106-
node,
107-
commentId(node.id, i),
108-
stringifyMarkdownAST(param.description),
109-
param.name,
110-
actions,
111-
createNodeId
112-
)
113-
delete param.description
114-
}
115-
delete param.lineNumber
116-
117-
// When documenting destructured parameters, the name
118-
// is parent.child where we just want the child.
119-
if (param.name.split(`.`).length > 1) {
120-
param.name = param.name
121-
.split(`.`)
122-
.slice(-1)
123-
.join(`.`)
124-
}
71+
const index = documentationJson.findIndex(
72+
docsJson =>
73+
docsJson.name === typeName &&
74+
[`typedef`, `constant`].includes(docsJson.kind)
75+
)
12576

126-
if (param.properties) {
127-
param.properties = param.properties.map(transformParam)
128-
}
77+
if (index !== -1) {
78+
return prepareNodeForDocs(documentationJson[index], {
79+
commentNumber: index,
80+
}).node.id
81+
}
82+
83+
return null
84+
}
12985

130-
return param
86+
const tryToAddTypeDef = type => {
87+
if (type.applications) {
88+
type.applications.forEach(tryToAddTypeDef)
13189
}
13290

133-
if (docsJson.params) {
134-
picked.params = docsJson.params.map(transformParam)
91+
if (type.expression) {
92+
tryToAddTypeDef(type.expression)
13593
}
13694

137-
if (docsJson.returns) {
138-
picked.returns = docsJson.returns.map(ret => {
139-
if (ret.description) {
140-
ret.description___NODE = createDescriptionNode(
141-
node,
142-
commentId(node.id, i),
143-
stringifyMarkdownAST(ret.description),
144-
ret.title,
145-
actions,
146-
createNodeId
147-
)
148-
delete ret.description
149-
}
95+
if (type.elements) {
96+
type.elements.forEach(tryToAddTypeDef)
97+
}
15098

151-
return ret
152-
})
99+
if (type.type === `NameExpression` && type.name) {
100+
type.typeDef___NODE = getNodeIDForType(type.name)
101+
}
102+
}
103+
104+
/**
105+
* Prepare Gatsby node from JsDoc object.
106+
* - set description and deprecated fields as markdown
107+
* - recursively process params, properties, returns
108+
* - link types to type definitions
109+
* - unwrap optional types to top level optional field
110+
* @param {Object} docsJson JsDoc object. See https://documentation.js.org/html-example/index.json for example of JsDoc objects shape.
111+
* @param {Object} args
112+
* @param {Number} [args.commentNumber] Index of JsDoc in root of module
113+
* @param {Number} args.level Nesting level
114+
* @param {string} args.parent Id of parent node
115+
*/
116+
const prepareNodeForDocs = (
117+
docsJson,
118+
{ commentNumber = null, level = 0, parent = node.id } = {}
119+
) => {
120+
if (handledDocs.has(docsJson)) {
121+
// this was already handled
122+
return handledDocs.get(docsJson)
123+
}
124+
125+
const docSkeletonNode = {
126+
commentNumber,
127+
level,
128+
id: createNodeId(docId(node.id, docsJson)),
129+
parent,
130+
children: [],
131+
internal: {
132+
type: `DocumentationJs`,
133+
},
134+
}
135+
136+
const children = []
137+
138+
const picked = _.pick(docsJson, [
139+
`kind`,
140+
`memberof`,
141+
`name`,
142+
`scope`,
143+
`type`,
144+
`default`,
145+
])
146+
147+
picked.optional = false
148+
if (docsJson.loc) {
149+
// loc is instance of SourceLocation class, and Gatsby doesn't support
150+
// class instances at this moment when inferring schema. Serializing
151+
// and desirializing converts class instance to plain object.
152+
picked.docsLocation = JSON.parse(JSON.stringify(docsJson.loc))
153+
}
154+
if (docsJson.context && docsJson.context.loc) {
155+
picked.codeLocation = JSON.parse(JSON.stringify(docsJson.context.loc))
156+
}
157+
158+
if (picked.type) {
159+
if (picked.type.type === `OptionalType` && picked.type.expression) {
160+
picked.optional = true
161+
picked.type = picked.type.expression
162+
}
163+
164+
tryToAddTypeDef(picked.type)
165+
}
166+
167+
const mdFields = [`description`, `deprecated`]
168+
169+
mdFields.forEach(fieldName => {
170+
if (docsJson[fieldName]) {
171+
const childNode = prepareDescriptionNode(
172+
docSkeletonNode,
173+
stringifyMarkdownAST(docsJson[fieldName]),
174+
`comment.${fieldName}`,
175+
helpers
176+
)
177+
178+
picked[`${fieldName}___NODE`] = childNode.id
179+
children.push({
180+
node: childNode,
181+
})
182+
}
183+
})
184+
185+
const docsSubfields = [`params`, `properties`, `returns`]
186+
docsSubfields.forEach(fieldName => {
187+
if (docsJson[fieldName] && docsJson[fieldName].length > 0) {
188+
picked[`${fieldName}___NODE`] = docsJson[fieldName].map(
189+
(docObj, fieldIndex) => {
190+
// When documenting destructured parameters, the name
191+
// is parent.child where we just want the child.
192+
if (docObj.name && docObj.name.split(`.`).length > 1) {
193+
docObj.name = docObj.name
194+
.split(`.`)
195+
.slice(-1)
196+
.join(`.`)
197+
}
198+
199+
const adjustedObj = {
200+
...docObj,
201+
path: [...docsJson.path, { fieldName, fieldIndex }],
202+
}
203+
204+
const nodeHierarchy = prepareNodeForDocs(adjustedObj, {
205+
level: level + 1,
206+
parent: docSkeletonNode.id,
207+
})
208+
children.push(nodeHierarchy)
209+
return nodeHierarchy.node.id
210+
}
211+
)
212+
}
213+
})
214+
215+
if (_.isPlainObject(docsJson.members)) {
216+
/*
217+
docsJson.members = {
218+
events: [],
219+
global: [],
220+
inner: [],
221+
instance: [],
222+
static: [],
223+
}
224+
each member type has array of jsdocs in same shape as top level jsdocs
225+
so we use same transformation as top level ones
226+
*/
227+
picked.members = _.reduce(
228+
docsJson.members,
229+
(acc, membersOfType, key) => {
230+
if (membersOfType.length > 0) {
231+
acc[`${key}___NODE`] = membersOfType.map(member => {
232+
const nodeHierarchy = prepareNodeForDocs(member, {
233+
level: level + 1,
234+
parent: docSkeletonNode.id,
235+
})
236+
children.push(nodeHierarchy)
237+
return nodeHierarchy.node.id
238+
})
239+
}
240+
return acc
241+
},
242+
{}
243+
)
153244
}
154245

155246
if (docsJson.examples) {
@@ -164,22 +255,44 @@ exports.onCreateNode = async ({
164255
})
165256
}
166257

167-
const strContent = JSON.stringify(picked, null, 4)
168-
169258
const docNode = {
259+
...docSkeletonNode,
170260
...picked,
171-
commentNumber: i,
172-
id: createNodeId(commentId(node.id, i)),
173-
parent: node.id,
174-
children: [],
175-
internal: {
176-
contentDigest: digest(strContent),
177-
type: `DocumentationJs`,
178-
},
179261
}
262+
docNode.internal.contentDigest = createContentDigest(docNode)
263+
264+
if (docNode.kind === `typedef`) {
265+
typeDefs.set(docNode.name, docNode.id)
266+
}
267+
268+
const nodeHierarchy = {
269+
node: docNode,
270+
children,
271+
}
272+
handledDocs.set(docsJson, nodeHierarchy)
273+
return nodeHierarchy
274+
}
275+
276+
const rootNodes = documentationJson.map((docJson, index) =>
277+
prepareNodeForDocs(docJson, { commentNumber: index })
278+
)
279+
280+
const createChildrenNodesRecursively = ({ node: parent, children }) => {
281+
if (children) {
282+
children.forEach(nodeHierarchy => {
283+
createNode(nodeHierarchy.node)
284+
createParentChildLink({
285+
parent,
286+
child: nodeHierarchy.node,
287+
})
288+
createChildrenNodesRecursively(nodeHierarchy)
289+
})
290+
}
291+
}
180292

181-
createParentChildLink({ parent: node, child: docNode })
182-
createNode(docNode)
293+
createChildrenNodesRecursively({
294+
node,
295+
children: rootNodes,
183296
})
184297

185298
return true

‎yarn.lock

+810-393
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.