UNPKG

6.4 kBPlain TextView Raw
1import * as gatsby from 'gatsby'
2import * as pc from 'pascal-case'
3import * as cc from 'camel-case'
4
5/**
6 * Converts a collection of strings to a single Pascal cased string.
7 *
8 * @param parts Strings to convert into a single Pascal cased string.
9 *
10 * @return Pascal cased string version of `parts`.
11 */
12const pascalCase = (...parts: (string | null | undefined)[]): string =>
13 pc.pascalCase(parts.filter((p) => p != null).join(' '), {
14 transform: pc.pascalCaseTransformMerge,
15 })
16
17/**
18 * Converts a collection of strings to a single camel cased string.
19 *
20 * @param parts Strings to convert into a single camel cased string.
21 *
22 * @return Camel cased string version of `parts`.
23 */
24const camelCase = (...parts: (string | null | undefined)[]): string =>
25 cc.camelCase(parts.filter((p) => p != null).join(' '), {
26 transform: cc.camelCaseTransformMerge,
27 })
28
29/**
30 * Casts a value to an array. If the input is an array, the input is returned as
31 * is. Otherwise, the input is returned as a single element array with the input
32 * as its only value.
33 *
34 * @param input Input that will be casted to an array.
35 *
36 * @return `input` that is guaranteed to be an array.
37 */
38const castArray = <T>(input: T | T[]): T[] =>
39 Array.isArray(input) ? input : [input]
40
41/**
42 * Reserved fields for Gatsby nodes.
43 */
44const RESERVED_GATSBY_NODE_FIELDS = [
45 'id',
46 'internal',
47 'fields',
48 'parent',
49 'children',
50] as const
51
52interface CreateNodeHelpersParams {
53 /** Prefix for all nodes. Used as a namespace for node type names. */
54 typePrefix: string
55 /**
56 * Prefix for field names. Used as a namespace for fields that conflict with
57 * Gatsby's reserved field names.
58 * */
59 fieldPrefix?: string
60 /** Gatsby's `createNodeId` helper. */
61 createNodeId: gatsby.SourceNodesArgs['createNodeId']
62 /** Gatsby's `createContentDigest` helper. */
63 createContentDigest: gatsby.SourceNodesArgs['createContentDigest']
64}
65
66/**
67 * A value that can be converted to a string using `toString()`.
68 */
69export interface Stringable {
70 toString(): string
71}
72
73/**
74 * A record that can be globally identified using its `id` field.
75 */
76export interface IdentifiableRecord {
77 id: Stringable
78 // eslint-disable-next-line @typescript-eslint/no-explicit-any
79 [key: string]: any
80}
81
82/**
83 * Gatsby node helper functions to aid node creation.
84 */
85export interface NodeHelpers {
86 /**
87 * Creates a namespaced type name in Pascal case. Nodes created using a
88 * `createNodeFactory` function will automatically be namespaced using this
89 * function.
90 *
91 * @param parts Parts of the type name. If more than one string is provided,
92 * they will be concatenated in Pascal case.
93 *
94 * @return Namespaced type name.
95 */
96 createTypeName: (parts: string | string[]) => string
97
98 /**
99 * Creates a namespaced field name in camel case. Nodes created using a
100 * `createNodeFactory` function will automatically have namespaced fields
101 * using this function ONLY if the name conflicts with Gatsby's reserved
102 * fields.
103 *
104 * @param parts Parts of the field name. If more than one string is provided,
105 * they will be concatenated in camel case.
106 *
107 * @return Namespaced field name.
108 */
109 createFieldName: (parts: string | string[]) => string
110
111 /**
112 * Creates a deterministic node ID based on the `typePrefix` option provided
113 * to `createNodeHelpers` and the provided `parts` argument. Providing the
114 * same `parts` will always return the same result.
115 *
116 * @param parts Strings to globally identify a node within the domain of the
117 * node helpers.
118 *
119 * @return Node ID based on the provided `parts`.
120 */
121 createNodeId: (parts: string | string[]) => string
122
123 /**
124 * Creates a function that will convert an identifiable record (one that has
125 * an `id` field) to a valid input for Gatsby's `createNode` action.
126 *
127 * @param nameParts Parts of the type name for the resulting factory. All
128 * records called with the resulting function will have a type name based on
129 * this parameter.
130 *
131 * @param options Options to control the resulting function's output.
132 *
133 * @return A function that converts an identifiable record to a valid input
134 * for Gatsby's `createNode` action.
135 */
136 createNodeFactory: (
137 nameParts: string | string[],
138 options?: CreateNodeFactoryOptions,
139 ) => (node: IdentifiableRecord) => gatsby.NodeInput
140}
141
142/**
143 * Options for a node factory.
144 */
145type CreateNodeFactoryOptions = {
146 /**
147 * Determines if the node's `id` field is unique within all nodes created with
148 * this collection of node helpers.
149 *
150 * If `false`, the ID will be namespaced with the node's type and the
151 * `typePrefix` value.
152 *
153 * If `true`, the ID will not be namespaced with the node's type, but will still
154 * be namespaced with the `typePrefix` value.
155 *
156 * @defaultValue `false`
157 */
158 idIsGloballyUnique?: boolean
159}
160
161/**
162 * Creates Gatsby node helper functions to aid node creation.
163 */
164export const createNodeHelpers = ({
165 typePrefix,
166 fieldPrefix = typePrefix,
167 createNodeId: gatsbyCreateNodeId,
168 createContentDigest: gatsbyCreateContentDigest,
169}: CreateNodeHelpersParams): NodeHelpers => {
170 const createTypeName = (nameParts: string | string[]): string =>
171 pascalCase(typePrefix, ...castArray(nameParts))
172
173 const createFieldName = (nameParts: string | string[]): string =>
174 camelCase(fieldPrefix, ...castArray(nameParts))
175
176 const createNodeId = (nameParts: string | string[]): string =>
177 gatsbyCreateNodeId(
178 [typePrefix, ...castArray(nameParts)].filter((p) => p != null).join(' '),
179 )
180
181 const createNodeFactory = (
182 nameParts: string | string[],
183 { idIsGloballyUnique = false }: CreateNodeFactoryOptions = {},
184 ) => (node: IdentifiableRecord): gatsby.NodeInput => {
185 const id = idIsGloballyUnique
186 ? createNodeId(node.id.toString())
187 : createNodeId([...castArray(nameParts), node.id.toString()])
188
189 const res = {
190 ...node,
191 id,
192 internal: {
193 type: createTypeName(nameParts),
194 contentDigest: gatsbyCreateContentDigest(node),
195 },
196 } as gatsby.NodeInput
197
198 for (const reservedField of RESERVED_GATSBY_NODE_FIELDS) {
199 if (reservedField in node) {
200 res[createFieldName(reservedField)] = node[reservedField]
201 }
202 }
203
204 return res
205 }
206
207 return {
208 createTypeName,
209 createFieldName,
210 createNodeId,
211 createNodeFactory,
212 }
213}