diff --git a/packages/gatsby/src/schema/infer/__tests__/__snapshots__/inference-metadata.js.snap b/packages/gatsby/src/schema/infer/__tests__/__snapshots__/inference-metadata.ts.snap similarity index 100% rename from packages/gatsby/src/schema/infer/__tests__/__snapshots__/inference-metadata.js.snap rename to packages/gatsby/src/schema/infer/__tests__/__snapshots__/inference-metadata.ts.snap diff --git a/packages/gatsby/src/schema/infer/__tests__/inference-metadata.js b/packages/gatsby/src/schema/infer/__tests__/inference-metadata.ts similarity index 95% rename from packages/gatsby/src/schema/infer/__tests__/inference-metadata.js rename to packages/gatsby/src/schema/infer/__tests__/inference-metadata.ts index 968ea5743b8cd..bfbb787ce0dc2 100644 --- a/packages/gatsby/src/schema/infer/__tests__/inference-metadata.js +++ b/packages/gatsby/src/schema/infer/__tests__/inference-metadata.ts @@ -1,15 +1,17 @@ // NOTE: Previously `data-tree-utils-test.js` -const _ = require(`lodash`) +import _ from "lodash" -const { +import { addNode, deleteNode, addNodes, haveEqualFields, -} = require(`../inference-metadata`) -const { getExampleObject } = require(`../build-example-data`) + ITypeMetadata, +} from "../inference-metadata" +import { getExampleObject } from "../build-example-data" -const { TypeConflictReporter } = require(`../type-conflict-reporter`) +import { TypeConflictReporter } from "../type-conflict-reporter" +import { Node } from "../../../../index" const INVALID_VALUE = undefined @@ -18,23 +20,23 @@ const getExampleValue = ({ typeName, typeConflictReporter, ignoreFields, -}) => { +}: any): any => { const initialMetadata = { typeName, typeConflictReporter, ignoredFields: new Set(ignoreFields), - } - const inferenceMetadata = addNodes(initialMetadata, nodes) + } as ITypeMetadata + const inferenceMetadata: ITypeMetadata = addNodes(initialMetadata, nodes) return getExampleObject(inferenceMetadata) } -const getExampleValueWithoutConflicts = args => { +const getExampleValueWithoutConflicts = (args): any => { const value = getExampleValue(args) expect(args.typeConflictReporter.getConflicts()).toEqual([]) return value } -const getExampleValueConflicts = args => { +const getExampleValueConflicts = (args): any => { const typeConflictReporter = new TypeConflictReporter() getExampleValue({ ...args, typeConflictReporter }) return typeConflictReporter.getConflicts() @@ -154,9 +156,9 @@ describe(`Get example value for type inference`, () => { it(`should not mutate the nodes`, () => { getExampleValueWithoutConflicts({ nodes, typeConflictReporter }) expect(nodes[0].context.nestedObject).toBeNull() - expect(nodes[1].context.nestedObject.someOtherProperty).toEqual(1) - expect(nodes[2].context.nestedObject.someOtherProperty).toEqual(2) - expect(nodes[3].context.nestedObject.someOtherProperty).toEqual(3) + expect(nodes[1].context.nestedObject?.someOtherProperty).toEqual(1) + expect(nodes[2].context.nestedObject?.someOtherProperty).toEqual(2) + expect(nodes[3].context.nestedObject?.someOtherProperty).toEqual(3) }) it(`skips empty or sparse arrays`, () => { @@ -215,7 +217,7 @@ describe(`Get example value for type inference`, () => { }) it(`turns polymorphic fields null`, () => { - let example = getExampleValue({ + const example = getExampleValue({ nodes: [{ foo: null }, { foo: [1] }, { foo: { field: 1 } }], typeConflictReporter, }) @@ -223,7 +225,7 @@ describe(`Get example value for type inference`, () => { }) it(`handles polymorphic arrays`, () => { - let example = getExampleValue({ + const example = getExampleValue({ nodes: [{ foo: [[`foo`, `bar`]] }, { foo: [{ field: 1 }] }], typeConflictReporter, }) @@ -251,14 +253,14 @@ describe(`Get example value for type inference`, () => { it(`skips unsupported types`, () => { // Skips functions let example = getExampleValueWithoutConflicts({ - nodes: [{ foo: () => {} }], + nodes: [{ foo: (): void => {} }], typeConflictReporter, }) expect(example.foo).not.toBeDefined() // Skips array of functions example = getExampleValueWithoutConflicts({ - nodes: [{ foo: [() => {}] }], + nodes: [{ foo: [(): void => {}] }], typeConflictReporter, }) expect(example.foo).not.toBeDefined() @@ -342,7 +344,9 @@ describe(`Get example value for type inference`, () => { it(`goes through nested object-like objects`, () => { class ObjectLike { - constructor(key1, key2) { + key1: number + key2: string + constructor(key1: number, key2: string) { this.key1 = key1 this.key2 = key2 } @@ -717,7 +721,7 @@ describe(`Get example value for type inference`, () => { }) describe(`Incremental example value building`, () => { - const nodes = [ + const _nodes = [ { name: `The Mad Max`, hair: 1, @@ -766,12 +770,13 @@ describe(`Get example value for type inference`, () => { }, }, ] + const nodes = (_nodes as unknown) as Node[] it(`updates example value when nodes are added`, () => { let inferenceMetadata = { typeName: `IncrementalExampleValue`, typeConflictReporter, ignoredFields: new Set(), - } + } as ITypeMetadata const revisions = nodes.map(node => { inferenceMetadata = addNode(inferenceMetadata, node) @@ -787,7 +792,7 @@ describe(`Get example value for type inference`, () => { typeName: `IncrementalExampleValue`, typeConflictReporter, ignoredFields: new Set(), - } + } as ITypeMetadata inferenceMetadata = addNodes(inferenceMetadata, nodes) const fullExampleValue = getExampleObject(inferenceMetadata) @@ -1035,7 +1040,7 @@ describe(`Type conflicts`, () => { describe(`Type change detection`, () => { let initialMetadata - const nodes = () => [ + const nodes = (): object[] => [ { foo: `foo` }, { object: { foo: `foo`, bar: `bar` } }, { list: [`item`], bar: `bar` }, @@ -1044,13 +1049,16 @@ describe(`Type change detection`, () => { { relatedNodeList___NODE: [`foo`] }, ] - const addOne = (node, metadata = initialMetadata) => - addNode(_.cloneDeep(metadata), node) - const deleteOne = (node, metadata = initialMetadata) => - deleteNode(_.cloneDeep(metadata), node) + const addOne = ( + node: object, + metadata: ITypeMetadata = initialMetadata + ): ITypeMetadata => addNode(_.cloneDeep(metadata), node as Node) + + const deleteOne = (node: object, metadata = initialMetadata): ITypeMetadata => + deleteNode(_.cloneDeep(metadata), node as Node) beforeEach(() => { - initialMetadata = addNodes({}, nodes()) + initialMetadata = addNodes(undefined, nodes() as Node[]) initialMetadata.dirty = false }) diff --git a/packages/gatsby/src/schema/infer/inference-metadata.js b/packages/gatsby/src/schema/infer/inference-metadata.ts similarity index 63% rename from packages/gatsby/src/schema/infer/inference-metadata.js rename to packages/gatsby/src/schema/infer/inference-metadata.ts index 8ea5192eade07..2e21446ff5b88 100644 --- a/packages/gatsby/src/schema/infer/inference-metadata.js +++ b/packages/gatsby/src/schema/infer/inference-metadata.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ /* ## Incrementally track the structure of nodes with metadata @@ -46,86 +47,88 @@ This metadata can be later utilized for schema inference `addNode`, `deleteNode`, `getExampleObject` are O(N) where N is the number of fields in the node object (including nested fields) -### Metadata structure +### Caveats -```javascript -type TypeMetadata = { - total?: number, - ignored?: boolean, - ignoredFields?: Set, - fieldMap?: { [string]: ValueDescriptor }, - typeName?: string, - dirty?: boolean, // tracks structural changes only - disabled?: boolean, -} +* Conflict tracking for arrays is tricky, i.e.: { a: [5, "foo"] } and { a: [5] }, { a: ["foo"] } + are represented identically in metadata. To workaround it we additionally track first NodeId: + { a: { array: { item: { int: { total: 1, first: `1` }, string: { total: 1, first: `1` } }} + { a: { array: { item: { int: { total: 1, first: `1` }, string: { total: 1, first: `2` } }} + This way we can produce more useful conflict reports + (still rare edge cases possible when reporting may be confusing, i.e. when node is deleted) +*/ -type Count = number -type NodeId = string - -type ValueDescriptor = { - int?: TypeInfoNumber, - float?: TypeInfoNumber, - date?: TypeInfoDate, - string?: TypeInfoString, - boolean?: TypeInfoBoolean, - array?: TypeInfoArray, - relatedNode?: TypeInfoRelatedNode, - relatedNodeList?: TypeInfoRelatedNode, - object?: TypeInfoObject, +import { isEqual } from "lodash" +import { is32BitInteger } from "../../utils/is-32-bit-integer" +import { looksLikeADate } from "../types/date" +import { Node } from "../../../index" +import { TypeConflictReporter } from "./type-conflict-reporter" + +interface ITypeInfo { + first?: string + total: number + example?: unknown } -abstract type TypeInfo = { - first?: NodeId | void, // Set to undefined if "del" - total: Count, +interface ITypeInfoString extends ITypeInfo { + empty: number + example: string } -type TypeInfoString = TypeInfo & { - empty: Count, - example: string, +interface ITypeInfoDate extends ITypeInfo { + example: string } -type TypeInfoDate = TypeInfo & { - example: string, +interface ITypeInfoNumber extends ITypeInfo { + example: number } -type TypeInfoNumber = { - example: number, +interface ITypeInfoBoolean extends ITypeInfo { + example: boolean } -type TypeInfoBoolean = { - example: boolean, +interface ITypeInfoArray extends ITypeInfo { + item: IValueDescriptor } -// "dprops" is "descriptor props", which makes it easier to search for than "props" -type TypeInfoObject = TypeInfo & { - dprops?: {[name: "number" | "string" | "boolean" | "null" | "date" | "string" | "array" | "object"]: Descriptor}, +interface ITypeInfoRelatedNodes extends ITypeInfo { + nodes: { [key: string]: number } } -type TypeInfoArray = TypeInfo & { - item: ValueDescriptor, +interface ITypeInfoObject extends ITypeInfo { + dprops: { + [name: string]: IValueDescriptor + } } -type TypeInfoRelatedNode = TypeInfo & { - nodes: { [NodeId]: Count } +interface IValueDescriptor { + int?: ITypeInfoNumber + float?: ITypeInfoNumber + date?: ITypeInfoDate + string?: ITypeInfoString + boolean?: ITypeInfoBoolean + array?: ITypeInfoArray + relatedNode?: ITypeInfoRelatedNodes + relatedNodeList?: ITypeInfoRelatedNodes + object?: ITypeInfoObject } -``` - -### Caveats - -* Conflict tracking for arrays is tricky, i.e.: { a: [5, "foo"] } and { a: [5] }, { a: ["foo"] } - are represented identically in metadata. To workaround it we additionally track first NodeId: - { a: { array: { item: { int: { total: 1, first: `1` }, string: { total: 1, first: `1` } }} - { a: { array: { item: { int: { total: 1, first: `1` }, string: { total: 1, first: `2` } }} - This way we can produce more useful conflict reports - (still rare edge cases possible when reporting may be confusing, i.e. when node is deleted) -*/ +type ValueType = keyof IValueDescriptor + +export interface ITypeMetadata { + typeName?: string + disabled: boolean + ignored?: boolean + dirty?: boolean + total?: number + ignoredFields?: Set + fieldMap: Record + typeConflictReporter?: TypeConflictReporter + [key: string]: unknown +} -const { isEqual } = require(`lodash`) -import { is32BitInteger } from "../../utils/is-32-bit-integer" -const { looksLikeADate } = require(`../types/date`) +type Operation = "add" | "del" -const getType = (value, key) => { +const getType = (value: unknown, key: string): ValueType | "null" => { // Staying as close as possible to GraphQL types switch (typeof value) { case `number`: @@ -156,13 +159,13 @@ const getType = (value, key) => { } const updateValueDescriptorObject = ( - value, - typeInfo, - nodeId, - operation, - metadata, - path -) => { + value: object, + typeInfo: ITypeInfoObject, + nodeId: string, + operation: Operation, + metadata: ITypeMetadata, + path: object[] +): void => { path.push(value) const { dprops = {} } = typeInfo @@ -184,14 +187,14 @@ const updateValueDescriptorObject = ( } const updateValueDescriptorArray = ( - value, - key, - typeInfo, - nodeId, - operation, - metadata, - path -) => { + value: unknown[], + key: string, + typeInfo: ITypeInfoArray, + nodeId: string, + operation: Operation, + metadata: ITypeMetadata, + path: object[] +): void => { value.forEach(item => { let descriptor = typeInfo.item if (descriptor === undefined) { @@ -212,12 +215,12 @@ const updateValueDescriptorArray = ( } const updateValueDescriptorRelNodes = ( - listOfNodeIds, - delta, - operation, - typeInfo, - metadata -) => { + listOfNodeIds: string[], + delta: number, + operation: Operation, + typeInfo: ITypeInfoRelatedNodes, + metadata: ITypeMetadata +): void => { const { nodes = {} } = typeInfo typeInfo.nodes = nodes @@ -233,7 +236,11 @@ const updateValueDescriptorRelNodes = ( }) } -const updateValueDescriptorString = (value, delta, typeInfo) => { +const updateValueDescriptorString = ( + value: string, + delta: number, + typeInfo: ITypeInfoString +): void => { if (value === ``) { const { empty = 0 } = typeInfo typeInfo.empty = empty + delta @@ -243,17 +250,17 @@ const updateValueDescriptorString = (value, delta, typeInfo) => { } const updateValueDescriptor = ( - nodeId, - key, - value, - operation = `add` /* add | del */, - descriptor, - metadata, - path -) => { + nodeId: string, + key: string, + value: unknown, + operation: Operation = `add`, + descriptor: IValueDescriptor, + metadata: ITypeMetadata, + path: object[] +): void => { // The object may be traversed multiple times from root. // Each time it does it should not revisit the same node twice - if (path.includes(value)) { + if (path.includes(value as object)) { return } @@ -264,9 +271,10 @@ const updateValueDescriptor = ( } const delta = operation === `del` ? -1 : 1 - let typeInfo = descriptor[typeName] + + let typeInfo: ITypeInfo | undefined = descriptor[typeName] if (typeInfo === undefined) { - typeInfo = descriptor[typeName] = { total: 0 } + typeInfo = (descriptor[typeName] as ITypeInfo) = { total: 0 } } typeInfo.total += delta @@ -291,8 +299,8 @@ const updateValueDescriptor = ( switch (typeName) { case `object`: updateValueDescriptorObject( - value, - typeInfo, + value as object, + typeInfo as ITypeInfoObject, nodeId, operation, metadata, @@ -301,9 +309,9 @@ const updateValueDescriptor = ( return case `array`: updateValueDescriptorArray( - value, + value as Array, key, - typeInfo, + typeInfo as ITypeInfoArray, nodeId, operation, metadata, @@ -312,18 +320,28 @@ const updateValueDescriptor = ( return case `relatedNode`: updateValueDescriptorRelNodes( - [value], + [value as string], delta, operation, - typeInfo, + typeInfo as ITypeInfoRelatedNodes, metadata ) return case `relatedNodeList`: - updateValueDescriptorRelNodes(value, delta, operation, typeInfo, metadata) + updateValueDescriptorRelNodes( + value as string[], + delta, + operation, + typeInfo as ITypeInfoRelatedNodes, + metadata + ) return case `string`: - updateValueDescriptorString(value, delta, typeInfo) + updateValueDescriptorString( + value as string, + delta, + typeInfo as ITypeInfoString + ) return } @@ -333,38 +351,47 @@ const updateValueDescriptor = ( typeof typeInfo.example !== `undefined` ? typeInfo.example : value } -const mergeObjectKeys = (dpropsKeysA, dpropsKeysB) => { +const mergeObjectKeys = ( + dpropsKeysA: ITypeInfoObject["dprops"] = {}, + dpropsKeysB: ITypeInfoObject["dprops"] = {} +): string[] => { const dprops = Object.keys(dpropsKeysA) const otherProps = Object.keys(dpropsKeysB) return [...new Set(dprops.concat(otherProps))] } -const descriptorsAreEqual = (descriptor, otherDescriptor) => { +const descriptorsAreEqual = ( + descriptor?: IValueDescriptor, + otherDescriptor?: IValueDescriptor +): boolean => { const types = possibleTypes(descriptor) const otherTypes = possibleTypes(otherDescriptor) - const childDescriptorsAreEqual = type => { + const childDescriptorsAreEqual = (type: string): boolean => { switch (type) { case `array`: return descriptorsAreEqual( - descriptor.array.item, - otherDescriptor.array.item + descriptor?.array?.item, + otherDescriptor?.array?.item ) case `object`: { const dpropsKeys = mergeObjectKeys( - descriptor.object.dprops, - otherDescriptor.object.dprops + descriptor?.object?.dprops, + otherDescriptor?.object?.dprops ) return dpropsKeys.every(prop => descriptorsAreEqual( - descriptor.object.dprops[prop], - otherDescriptor.object.dprops[prop] + descriptor?.object?.dprops[prop], + otherDescriptor?.object?.dprops[prop] ) ) } case `relatedNode`: case `relatedNodeList`: { - return isEqual(descriptor.nodes, otherDescriptor.nodes) + // eslint-disable-next-line + // @ts-ignore + // FIXME See comment: https://github.com/gatsbyjs/gatsby/pull/23264#discussion_r410908538 + return isEqual(descriptor?.nodes, otherDescriptor?.nodes) } default: return true @@ -375,10 +402,14 @@ const descriptorsAreEqual = (descriptor, otherDescriptor) => { return isEqual(types, otherTypes) && types.every(childDescriptorsAreEqual) } -const nodeFields = (node, ignoredFields = new Set()) => +const nodeFields = (node: Node, ignoredFields = new Set()): string[] => Object.keys(node).filter(key => !ignoredFields.has(key)) -const updateTypeMetadata = (metadata = initialMetadata(), operation, node) => { +const updateTypeMetadata = ( + metadata = initialMetadata(), + operation: Operation, + node: Node +): ITypeMetadata => { if (metadata.disabled) { return metadata } @@ -409,44 +440,50 @@ const updateTypeMetadata = (metadata = initialMetadata(), operation, node) => { return metadata } -const ignore = (metadata = initialMetadata(), set = true) => { +const ignore = (metadata = initialMetadata(), set = true): ITypeMetadata => { metadata.ignored = set metadata.fieldMap = {} return metadata } -const disable = (metadata = initialMetadata(), set = true) => { +const disable = (metadata = initialMetadata(), set = true): ITypeMetadata => { metadata.disabled = set return metadata } -const addNode = (metadata, node) => updateTypeMetadata(metadata, `add`, node) -const deleteNode = (metadata, node) => updateTypeMetadata(metadata, `del`, node) -const addNodes = (metadata = initialMetadata(), nodes) => +const addNode = (metadata: ITypeMetadata, node: Node): ITypeMetadata => + updateTypeMetadata(metadata, `add`, node) + +const deleteNode = (metadata: ITypeMetadata, node: Node): ITypeMetadata => + updateTypeMetadata(metadata, `del`, node) +const addNodes = (metadata = initialMetadata(), nodes: Node[]): ITypeMetadata => nodes.reduce(addNode, metadata) -const possibleTypes = (descriptor = {}) => - Object.keys(descriptor).filter(type => descriptor[type].total > 0) +const possibleTypes = (descriptor: IValueDescriptor = {}): ValueType[] => + Object.keys(descriptor).filter( + type => descriptor[type].total > 0 + ) as ValueType[] -const isEmpty = ({ fieldMap }) => +const isEmpty = ({ fieldMap }): boolean => Object.keys(fieldMap).every( field => possibleTypes(fieldMap[field]).length === 0 ) // Even empty type may still have nodes -const hasNodes = typeMetadata => typeMetadata.total > 0 +const hasNodes = (typeMetadata: ITypeMetadata): boolean => + (typeMetadata.total ?? 0) > 0 const haveEqualFields = ( { fieldMap = {} } = {}, { fieldMap: otherFieldMap = {} } = {} -) => { +): boolean => { const fields = mergeObjectKeys(fieldMap, otherFieldMap) return fields.every(field => descriptorsAreEqual(fieldMap[field], otherFieldMap[field]) ) } -const initialMetadata = state => { +const initialMetadata = (state?: object): ITypeMetadata => { return { typeName: undefined, disabled: false, @@ -459,7 +496,7 @@ const initialMetadata = state => { } } -module.exports = { +export { addNode, addNodes, deleteNode, diff --git a/packages/gatsby/src/schema/infer/type-conflict-reporter.js b/packages/gatsby/src/schema/infer/type-conflict-reporter.ts similarity index 70% rename from packages/gatsby/src/schema/infer/type-conflict-reporter.js rename to packages/gatsby/src/schema/infer/type-conflict-reporter.ts index c6d61183dd073..4dcf54a7f9fb2 100644 --- a/packages/gatsby/src/schema/infer/type-conflict-reporter.js +++ b/packages/gatsby/src/schema/infer/type-conflict-reporter.ts @@ -1,37 +1,24 @@ -// @flow -const _ = require(`lodash`) -const report = require(`gatsby-cli/lib/reporter`) -const typeOf = require(`type-of`) -const util = require(`util`) - -export type TypeConflictExample = { - value: mixed, - parent: {}, - type: string, - arrayTypes: string[], +import sortBy from "lodash/sortBy" +import report from "gatsby-cli/lib/reporter" +import typeOf from "type-of" +import util from "util" + +import { Node } from "../../../index" + +export interface ITypeConflictExample { + value: unknown + parent: Node + type: string + arrayTypes: string[] } -type TypeConflict = { - value: mixed, - description: string, +interface ITypeConflict { + value: unknown + description?: string } -const isNodeWithDescription = node => - node && node.internal && node.internal.description - -const findNodeDescription = obj => { - if (obj) { - // TODO: Maybe get this back - // const node = findRootNodeAncestor(obj, isNodeWithDescription) - if (isNodeWithDescription(obj)) { - return obj.internal.description - } - } - return `` -} - -const formatValue = value => { - if (!_.isArray(value)) { +const formatValue = (value: unknown): string => { + if (!Array.isArray(value)) { return util.inspect(value, { colors: true, depth: 0, @@ -39,7 +26,7 @@ const formatValue = value => { }) } - const output = [] + const output: string[] = [] if (value.length === 1) { // For arrays usually a single conflicting item is exposed vs. the whole array @@ -48,7 +35,7 @@ const formatValue = value => { output.push(`...`) } else { let wasElipsisLast = false - const usedTypes = [] + const usedTypes: string[] = [] value.forEach(item => { const type = typeOf(item) if (usedTypes.includes(type)) { @@ -69,24 +56,24 @@ const formatValue = value => { class TypeConflictEntry { selector: string - types: Map + types: Map constructor(selector: string) { this.selector = selector this.types = new Map() } - addExample({ value, type, parent }: TypeConflictExample) { + addExample({ value, type, parent }: ITypeConflictExample): void { this.types.set(type, { value, - description: findNodeDescription(parent), + description: parent?.internal?.description ?? ``, }) } - printEntry() { - const sortedByTypeName = _.sortBy( + printEntry(): void { + const sortedByTypeName = sortBy( Array.from(this.types.entries()), - ([typeName, value]) => typeName + ([typeName]) => typeName ) report.log( @@ -109,7 +96,7 @@ class TypeConflictReporter { this.entries = new Map() } - clearConflicts() { + clearConflicts(): void { this.entries.clear() } @@ -124,7 +111,7 @@ class TypeConflictReporter { return dataEntry } - addConflict(selector: string, examples: TypeConflictExample[]) { + addConflict(selector: string, examples: ITypeConflictExample[]): void { if (selector.substring(0, 11) === `SitePlugin.`) { // Don't store and print out type conflicts in plugins. // This is out of user control so he/she can't do anything @@ -138,7 +125,7 @@ class TypeConflictReporter { .forEach(example => entry.addExample(example)) } - printConflicts() { + printConflicts(): void { if (this.entries.size > 0) { report.warn( `There are conflicting field types in your data.\n\n` + @@ -154,9 +141,9 @@ class TypeConflictReporter { } } - getConflicts() { + getConflicts(): TypeConflictEntry[] { return Array.from(this.entries.values()) } } -module.exports = { TypeConflictReporter, TypeConflictEntry } +export { TypeConflictReporter, TypeConflictEntry }