1 | import * as gatsby from 'gatsby'
|
2 | import * as pc from 'pascal-case'
|
3 | import * 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 | */
|
12 | const 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 | */
|
24 | const 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 | */
|
38 | const castArray = <T>(input: T | T[]): T[] =>
|
39 | Array.isArray(input) ? input : [input]
|
40 |
|
41 | /**
|
42 | * Reserved fields for Gatsby nodes.
|
43 | */
|
44 | const RESERVED_GATSBY_NODE_FIELDS = [
|
45 | 'id',
|
46 | 'internal',
|
47 | 'fields',
|
48 | 'parent',
|
49 | 'children',
|
50 | ] as const
|
51 |
|
52 | interface 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 | */
|
69 | export interface Stringable {
|
70 | toString(): string
|
71 | }
|
72 |
|
73 | /**
|
74 | * A record that can be globally identified using its `id` field.
|
75 | */
|
76 | export 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 | */
|
85 | export 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 | */
|
145 | type 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 | */
|
164 | export 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 | }
|