| | import { diff, ChangeType, Change } from '@graphql-inspector/core' |
| | import { loadSchema } from '@graphql-tools/load' |
| | import fs from 'fs' |
| | import { renderContent } from '@/content-render/index' |
| |
|
| | interface UpcomingChange { |
| | location: string |
| | date: string |
| | description: string |
| | } |
| |
|
| | interface Preview { |
| | title: string |
| | toggled_on: string[] |
| | } |
| |
|
| | interface ChangelogSchemaChange { |
| | title: string |
| | changes: string[] |
| | } |
| |
|
| | interface ChangelogPreviewChange { |
| | title: string |
| | changes: string[] |
| | } |
| |
|
| | interface ChangelogUpcomingChange { |
| | title: string |
| | changes: string[] |
| | } |
| |
|
| | export interface ChangelogEntry { |
| | date?: string |
| | schemaChanges: ChangelogSchemaChange[] |
| | previewChanges: ChangelogPreviewChange[] |
| | upcomingChanges: ChangelogUpcomingChange[] |
| | } |
| |
|
| | interface PreviewChanges { |
| | title: string |
| | changes: Change[] |
| | } |
| |
|
| | interface SegmentedChanges { |
| | schemaChangesToReport: Change[] |
| | previewChangesToReport: Record<string, PreviewChanges> |
| | } |
| |
|
| | interface IgnoredChangeType { |
| | type: string |
| | count: number |
| | } |
| |
|
| | interface IgnoredChangesSummary { |
| | totalCount: number |
| | typeCount: number |
| | types: IgnoredChangeType[] |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | export function prependDatedEntry(changelogEntry: ChangelogEntry, targetPath: string): void { |
| | |
| | |
| | const todayString = new Date().toISOString().slice(0, 10) |
| | changelogEntry.date = todayString |
| |
|
| | const previousChangelogString = fs.readFileSync(targetPath, 'utf8') |
| | const previousChangelog = JSON.parse(previousChangelogString) as ChangelogEntry[] |
| | |
| | previousChangelog.unshift(changelogEntry) |
| | |
| | fs.writeFileSync(targetPath, JSON.stringify(previousChangelog, null, 2)) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | export async function createChangelogEntry( |
| | oldSchemaString: string, |
| | newSchemaString: string, |
| | previews: Preview[], |
| | oldUpcomingChanges: UpcomingChange[], |
| | newUpcomingChanges: UpcomingChange[], |
| | ): Promise<ChangelogEntry | null> { |
| | |
| | |
| | const oldSchema = await loadSchema(oldSchemaString, {} as any) |
| | const newSchema = await loadSchema(newSchemaString, {} as any) |
| |
|
| | |
| | const changes = await diff(oldSchema, newSchema) |
| | const changesToReport: Change[] = [] |
| | const ignoredChanges: Change[] = [] |
| | for (const change of changes) { |
| | if (CHANGES_TO_REPORT.includes(change.type)) { |
| | changesToReport.push(change) |
| | } else { |
| | |
| | ignoredChanges.push(change) |
| | } |
| | } |
| |
|
| | |
| | if (ignoredChanges.length > 0) { |
| | const ignoredTypes = [...new Set(ignoredChanges.map((change) => change.type))] |
| | console.warn( |
| | `⚠️ GraphQL changelog: Ignoring ${ignoredChanges.length} changes of ${ignoredTypes.length} type(s):`, |
| | ) |
| | for (const type of ignoredTypes) { |
| | const count = ignoredChanges.filter((change) => change.type === type).length |
| | console.warn(` - ${type} (${count} change${count > 1 ? 's' : ''})`) |
| | } |
| | console.warn( |
| | ' These change types are not in CHANGES_TO_REPORT and will not appear in the changelog.', |
| | ) |
| | } |
| |
|
| | |
| | ;(createChangelogEntry as any).lastIgnoredChanges = ignoredChanges |
| |
|
| | const { schemaChangesToReport, previewChangesToReport } = segmentPreviewChanges( |
| | changesToReport, |
| | previews, |
| | ) |
| |
|
| | const addedUpcomingChanges = newUpcomingChanges.filter(function (change): boolean { |
| | |
| | |
| | return !oldUpcomingChanges.find(function (oldChange) { |
| | return ( |
| | oldChange.location === change.location && |
| | oldChange.date === change.date && |
| | oldChange.description === change.description |
| | ) |
| | }) |
| | }) |
| |
|
| | |
| | if ( |
| | schemaChangesToReport.length > 0 || |
| | Object.keys(previewChangesToReport).length > 0 || |
| | addedUpcomingChanges.length > 0 |
| | ) { |
| | const changelogEntry: ChangelogEntry = { |
| | schemaChanges: [], |
| | previewChanges: [], |
| | upcomingChanges: [], |
| | } |
| |
|
| | const cleanedSchemaChanges = cleanMessagesFromChanges(schemaChangesToReport) |
| | const renderedScheamChanges = await Promise.all( |
| | cleanedSchemaChanges.map(async (change): Promise<string> => { |
| | return await renderContent(change) |
| | }), |
| | ) |
| | const schemaChange: ChangelogSchemaChange = { |
| | title: 'The GraphQL schema includes these changes:', |
| | |
| | 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): Promise<string> => { |
| | 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): Promise<string> => { |
| | return await renderContent(change) |
| | }), |
| | ) |
| | changelogEntry.upcomingChanges.push({ |
| | title: 'The following changes will be made to the schema:', |
| | changes: renderedUpcomingChanges, |
| | }) |
| | } |
| |
|
| | return changelogEntry |
| | } else { |
| | return null |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | export function cleanPreviewTitle(title: string): string { |
| | 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 |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | export function previewAnchor(previewTitle: string): string { |
| | return previewTitle |
| | .toLowerCase() |
| | .replace(/ /g, '-') |
| | .replace(/[^\w-]/g, '') |
| | } |
| |
|
| | |
| | |
| | |
| | export function cleanMessagesFromChanges(changes: Change[]): string[] { |
| | return changes.map(function (change): string { |
| | |
| | |
| | return change.message.replace(/'([a-zA-Z. :!]+)'/g, '`$1`') |
| | }) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | export function segmentPreviewChanges( |
| | changesToReport: Change[], |
| | previews: Preview[], |
| | ): SegmentedChanges { |
| | |
| | |
| | const pathToPreview: Record<string, string> = {} |
| | for (const preview of previews) { |
| | for (const path of preview.toggled_on) { |
| | pathToPreview[path] = preview.title |
| | } |
| | } |
| | const schemaChanges: Change[] = [] |
| | const changesByPreview: Record<string, PreviewChanges> = {} |
| |
|
| | for (const change of changesToReport) { |
| | |
| | |
| | const pathParts = change.path?.split('.') || [] |
| | let testPath: string | null = null |
| | let previewTitle: string | null = null |
| | let previewChanges: PreviewChanges | null = null |
| | while (pathParts.length > 0 && !previewTitle) { |
| | testPath = pathParts.join('.') |
| | previewTitle = pathToPreview[testPath] |
| | |
| | |
| | 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 } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | 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, |
| | ChangeType.DirectiveUsageFieldDefinitionRemoved, |
| | ] |
| |
|
| | |
| | |
| |
|
| | |
| | |
| | |
| | export function getLastIgnoredChanges(): Change[] { |
| | return (createChangelogEntry as any).lastIgnoredChanges || [] |
| | } |
| |
|
| | |
| | |
| | |
| | export function getIgnoredChangesSummary(): IgnoredChangesSummary | null { |
| | const ignored = getLastIgnoredChanges() |
| | if (ignored.length === 0) return null |
| |
|
| | const types = [...new Set(ignored.map((change) => change.type))] |
| | const summary: IgnoredChangesSummary = { |
| | totalCount: ignored.length, |
| | typeCount: types.length, |
| | types: types.map( |
| | (type): IgnoredChangeType => ({ |
| | type, |
| | count: ignored.filter((change) => change.type === type).length, |
| | }), |
| | ), |
| | } |
| |
|
| | return summary |
| | } |
| |
|
| | export default { createChangelogEntry, cleanPreviewTitle, previewAnchor, prependDatedEntry } |
| |
|