forked from github/docs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
build-changelog.js
308 lines (289 loc) · 10.3 KB
/
build-changelog.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
#!/usr/bin/env node
import { diff, ChangeType } from '@graphql-inspector/core'
import { loadSchema } from '@graphql-tools/load'
import fs from 'fs'
import renderContent from '../../lib/render-content/index.js'
/**
* Tag `changelogEntry` with `date: YYYY-mm-dd`, then prepend it to the JSON
* structure written to `targetPath`. (`changelogEntry` and that file are modified in place.)
* @param {object} changelogEntry
* @param {string} targetPath
* @return {void}
*/
export function prependDatedEntry(changelogEntry, targetPath) {
// Build a `yyyy-mm-dd`-formatted date string
// and tag the changelog entry with it
const todayString = new Date().toISOString().slice(0, 10)
changelogEntry.date = todayString
const previousChangelogString = fs.readFileSync(targetPath)
const previousChangelog = JSON.parse(previousChangelogString)
// add a new entry to the changelog data
previousChangelog.unshift(changelogEntry)
// rewrite the updated changelog
fs.writeFileSync(targetPath, JSON.stringify(previousChangelog, null, 2))
}
/**
* Compare `oldSchemaString` to `newSchemaString`, and if there are any
* changes that warrant a changelog entry, return a changelog entry.
* Based on the parsed `previews`, identify changes that are under a preview.
* Otherwise, return null.
* @param {string} [oldSchemaString]
* @param {string} [newSchemaString]
* @param {Array<object>} [previews]
* @param {Array<object>} [oldUpcomingChanges]
* @param {Array<object>} [newUpcomingChanges]
* @return {object?}
*/
export async function createChangelogEntry(
oldSchemaString,
newSchemaString,
previews,
oldUpcomingChanges,
newUpcomingChanges
) {
// Create schema objects out of the strings
const oldSchema = await loadSchema(oldSchemaString, {})
const newSchema = await loadSchema(newSchemaString, {})
// Generate changes between the two schemas
const changes = await diff(oldSchema, newSchema)
const changesToReport = []
changes.forEach(function (change) {
if (CHANGES_TO_REPORT.includes(change.type)) {
changesToReport.push(change)
} else if (CHANGES_TO_IGNORE.includes(change.type)) {
// Do nothing
} else {
throw new Error(
'This change type should be added to CHANGES_TO_REPORT or CHANGES_TO_IGNORE: ' + change.type
)
}
})
const { schemaChangesToReport, previewChangesToReport } = segmentPreviewChanges(
changesToReport,
previews
)
const addedUpcomingChanges = newUpcomingChanges.filter(function (change) {
// Manually check each of `newUpcomingChanges` for an equivalent entry
// in `oldUpcomingChanges`.
return !oldUpcomingChanges.find(function (oldChange) {
return (
oldChange.location === change.location &&
oldChange.date === change.date &&
oldChange.description === change.description
)
})
})
// If there were any changes, create a changelog entry
if (
schemaChangesToReport.length > 0 ||
previewChangesToReport.length > 0 ||
addedUpcomingChanges.length > 0
) {
const changelogEntry = {
schemaChanges: [],
previewChanges: [],
upcomingChanges: [],
}
const cleanedSchemaChanges = cleanMessagesFromChanges(schemaChangesToReport)
const renderedScheamChanges = await Promise.all(
cleanedSchemaChanges.map(async (change) => {
return await renderContent(change)
})
)
const schemaChange = {
title: 'The GraphQL schema includes these changes:',
// Replace single quotes which wrap field/argument/type names with backticks
changes: renderedScheamChanges,
}
changelogEntry.schemaChanges.push(schemaChange)
for (const previewTitle in previewChangesToReport) {
const previewChanges = previewChangesToReport[previewTitle]
const cleanedPreviewChanges = cleanMessagesFromChanges(previewChanges.changes)
const renderedPreviewChanges = await Promise.all(
cleanedPreviewChanges.map(async (change) => {
return renderContent(change)
})
)
const cleanTitle = cleanPreviewTitle(previewTitle)
const entryTitle =
'The [' +
cleanTitle +
'](/graphql/overview/schema-previews#' +
previewAnchor(cleanTitle) +
') includes these changes:'
changelogEntry.previewChanges.push({
title: entryTitle,
changes: renderedPreviewChanges,
})
}
if (addedUpcomingChanges.length > 0) {
const cleanedUpcomingChanges = addedUpcomingChanges.map((change) => {
const location = change.location
const description = change.description
const date = change.date.split('T')[0]
return 'On member `' + location + '`:' + description + ' **Effective ' + date + '**.'
})
const renderedUpcomingChanges = await Promise.all(
cleanedUpcomingChanges.map(async (change) => {
return await renderContent(change)
})
)
changelogEntry.upcomingChanges.push({
title: 'The following changes will be made to the schema:',
changes: renderedUpcomingChanges,
})
}
return changelogEntry
} else {
return null
}
}
/**
* Prepare the preview title from github/github source for the docs.
* @param {string} title
* @return {string}
*/
export function cleanPreviewTitle(title) {
if (title === 'UpdateRefsPreview') {
title = 'Update refs preview'
} else if (title === 'MergeInfoPreview') {
title = 'Merge info preview'
} else if (!title.endsWith('preview')) {
title = title + ' preview'
}
return title
}
/**
* Turn the given title into an HTML-ready anchor.
* (ported from graphql-docs/lib/graphql_docs/update_internal_developer/change_log.rb#L281)
* @param {string} [previewTitle]
* @return {string}
*/
export function previewAnchor(previewTitle) {
return previewTitle
.toLowerCase()
.replace(/ /g, '-')
.replace(/[^\w-]/g, '')
}
/**
* Turn changes from graphql-inspector into messages for the HTML changelog.
* @param {Array<object>} changes
* @return {Array<string>}
*/
export function cleanMessagesFromChanges(changes) {
return changes.map(function (change) {
// replace single quotes around graphql names with backticks,
// to match previous behavior from graphql-schema-comparator
return change.message.replace(/'([a-zA-Z. :!]+)'/g, '`$1`')
})
}
/**
* Split `changesToReport` into two parts,
* one for changes in the main schema,
* and another for changes that are under preview.
* (Ported from /graphql-docs/lib/graphql_docs/update_internal_developer/change_log.rb#L230)
* @param {Array<object>} changesToReport
* @param {object} previews
* @return {object}
*/
export function segmentPreviewChanges(changesToReport, previews) {
// Build a map of `{ path => previewTitle` }
// for easier lookup of change to preview
const pathToPreview = {}
previews.forEach(function (preview) {
preview.toggled_on.forEach(function (path) {
pathToPreview[path] = preview.title
})
})
const schemaChanges = []
const changesByPreview = {}
changesToReport.forEach(function (change) {
// For each change, see if its path _or_ one of its ancestors
// is covered by a preview. If it is, mark this change as belonging to a preview
const pathParts = change.path.split('.')
let testPath = null
let previewTitle = null
let previewChanges = null
while (pathParts.length > 0 && !previewTitle) {
testPath = pathParts.join('.')
previewTitle = pathToPreview[testPath]
// If that path didn't find a match, then we'll
// check the next ancestor.
pathParts.pop()
}
if (previewTitle) {
previewChanges =
changesByPreview[previewTitle] ||
(changesByPreview[previewTitle] = {
title: previewTitle,
changes: [],
})
previewChanges.changes.push(change)
} else {
schemaChanges.push(change)
}
})
return { schemaChangesToReport: schemaChanges, previewChangesToReport: changesByPreview }
}
// We only want to report changes to schema structure.
// Deprecations are covered by "upcoming changes."
// By listing the changes explicitly here, we can make sure that,
// if the library changes, we don't miss publishing anything that we mean to.
// This was originally ported from graphql-docs/lib/graphql_docs/update_internal_developer/change_log.rb#L35-L103
const CHANGES_TO_REPORT = [
ChangeType.FieldArgumentDefaultChanged,
ChangeType.FieldArgumentTypeChanged,
ChangeType.EnumValueRemoved,
ChangeType.EnumValueAdded,
ChangeType.FieldRemoved,
ChangeType.FieldAdded,
ChangeType.FieldTypeChanged,
ChangeType.FieldArgumentAdded,
ChangeType.FieldArgumentRemoved,
ChangeType.ObjectTypeInterfaceAdded,
ChangeType.ObjectTypeInterfaceRemoved,
ChangeType.InputFieldRemoved,
ChangeType.InputFieldAdded,
ChangeType.InputFieldDefaultValueChanged,
ChangeType.InputFieldTypeChanged,
ChangeType.TypeRemoved,
ChangeType.TypeAdded,
ChangeType.TypeKindChanged,
ChangeType.UnionMemberRemoved,
ChangeType.UnionMemberAdded,
ChangeType.SchemaQueryTypeChanged,
ChangeType.SchemaMutationTypeChanged,
ChangeType.SchemaSubscriptionTypeChanged,
]
const CHANGES_TO_IGNORE = [
ChangeType.FieldArgumentDescriptionChanged,
ChangeType.DirectiveRemoved,
ChangeType.DirectiveAdded,
ChangeType.DirectiveDescriptionChanged,
ChangeType.DirectiveLocationAdded,
ChangeType.DirectiveLocationRemoved,
ChangeType.DirectiveArgumentAdded,
ChangeType.DirectiveArgumentRemoved,
ChangeType.DirectiveArgumentDescriptionChanged,
ChangeType.DirectiveArgumentDefaultValueChanged,
ChangeType.DirectiveArgumentTypeChanged,
ChangeType.EnumValueDescriptionChanged,
ChangeType.EnumValueDeprecationReasonChanged,
ChangeType.EnumValueDeprecationReasonAdded,
ChangeType.EnumValueDeprecationReasonRemoved,
ChangeType.FieldDescriptionChanged,
ChangeType.FieldDescriptionAdded,
ChangeType.FieldDescriptionRemoved,
ChangeType.FieldDeprecationAdded,
ChangeType.FieldDeprecationRemoved,
ChangeType.FieldDeprecationReasonChanged,
ChangeType.FieldDeprecationReasonAdded,
ChangeType.FieldDeprecationReasonRemoved,
ChangeType.InputFieldDescriptionAdded,
ChangeType.InputFieldDescriptionRemoved,
ChangeType.InputFieldDescriptionChanged,
ChangeType.TypeDescriptionChanged,
ChangeType.TypeDescriptionRemoved,
ChangeType.TypeDescriptionAdded,
]
export default { createChangelogEntry, cleanPreviewTitle, previewAnchor, prependDatedEntry }