@teleporthq/teleport-plugin-next-inline-fetch 0.32.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/cjs/index.d.ts +5 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +272 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/tsconfig.tsbuildinfo +1 -0
- package/dist/cjs/utils.d.ts +9 -0
- package/dist/cjs/utils.d.ts.map +1 -0
- package/dist/cjs/utils.js +80 -0
- package/dist/cjs/utils.js.map +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +245 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/tsconfig.tsbuildinfo +1 -0
- package/dist/esm/utils.d.ts +9 -0
- package/dist/esm/utils.d.ts.map +1 -0
- package/dist/esm/utils.js +53 -0
- package/dist/esm/utils.js.map +1 -0
- package/package.json +32 -0
- package/src/index.ts +345 -0
- package/src/utils.ts +109 -0
- package/tsconfig.json +9 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChunkType,
|
|
3
|
+
ComponentPlugin,
|
|
4
|
+
ComponentPluginFactory,
|
|
5
|
+
FileType,
|
|
6
|
+
UIDLStaticValue,
|
|
7
|
+
} from '@teleporthq/teleport-types'
|
|
8
|
+
import { StringUtils, UIDLUtils } from '@teleporthq/teleport-shared'
|
|
9
|
+
import * as types from '@babel/types'
|
|
10
|
+
import { ASTUtils } from '@teleporthq/teleport-plugin-common'
|
|
11
|
+
import { computeUseEffectAST } from './utils'
|
|
12
|
+
import { join, relative } from 'path'
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
When defining a cms-item or cms-list in the UIDL, the user can specify a resource associated with the node.
|
|
16
|
+
Usually, this resource is a local resource, and we decide here either to extract it into getStaticProps or
|
|
17
|
+
directly pass it as a callback to the DataProvider.
|
|
18
|
+
All the top-level API calls which don't have any dynamic references are extracted into getStaticProps,
|
|
19
|
+
and the rest are passed as callbacks to the DataProvider which results in loading them at the client-side
|
|
20
|
+
when the app is deployed..
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export const createNextInlineFetchPlugin: ComponentPluginFactory<{}> = () => {
|
|
24
|
+
const nextComponentCMSFetchPlugin: ComponentPlugin = async (structure) => {
|
|
25
|
+
const { uidl, chunks, options, dependencies } = structure
|
|
26
|
+
const { resources } = options
|
|
27
|
+
|
|
28
|
+
if (resources?.items === undefined || resources?.path === undefined) {
|
|
29
|
+
return structure
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let getStaticPropsChunk = chunks.find((chunk) => chunk.name === 'getStaticProps')
|
|
33
|
+
const componentChunk = chunks.find((chunk) => chunk.name === 'jsx-component')
|
|
34
|
+
if (componentChunk === undefined) {
|
|
35
|
+
return structure
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
UIDLUtils.traverseNodes(uidl.node, (node) => {
|
|
39
|
+
if (node.type !== 'cms-item' && node.type !== 'cms-list') {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const jsxNode = componentChunk.meta.nodesLookup[node.content.key] as types.JSXElement
|
|
44
|
+
if (jsxNode === undefined || jsxNode.type !== 'JSXElement') {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
If a node already has a initialData on it. We don't need to do anything.
|
|
50
|
+
As the node is connected with getStaticProps in the page. And the resource is
|
|
51
|
+
defined inside `intialPropsData` in the UIDL for the page.
|
|
52
|
+
*/
|
|
53
|
+
if (node.content.initialData !== undefined || node.content.resource === undefined) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const isResourceContainsAnyDynamicValues = Object.values(
|
|
58
|
+
node.content.resource?.params || {}
|
|
59
|
+
).some((param) => param.type === 'expr' || param.type === 'dynamic')
|
|
60
|
+
|
|
61
|
+
/*
|
|
62
|
+
If the node is not using any dynamic stuff as parameters, it can be safely extracted.
|
|
63
|
+
If the node is using purely static values as params, no matter how deep the node is nested.
|
|
64
|
+
If can safely be extracted into server side getStaticProps
|
|
65
|
+
*/
|
|
66
|
+
if (isResourceContainsAnyDynamicValues === false) {
|
|
67
|
+
const propKey = StringUtils.createStateOrPropStoringValue(
|
|
68
|
+
node.content.renderPropIdentifier + 'Prop'
|
|
69
|
+
)
|
|
70
|
+
jsxNode.openingElement.attributes.push(
|
|
71
|
+
types.jsxAttribute(
|
|
72
|
+
types.jsxIdentifier('initialData'),
|
|
73
|
+
types.jsxExpressionContainer(
|
|
74
|
+
types.memberExpression(types.identifier('props'), types.identifier(propKey))
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
jsxNode.openingElement.attributes.push(
|
|
80
|
+
types.jsxAttribute(
|
|
81
|
+
types.jsxIdentifier('persistDataDuringLoading'),
|
|
82
|
+
types.jsxExpressionContainer(types.booleanLiteral(true))
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
let resourceName: string
|
|
87
|
+
if ('id' in node.content.resource) {
|
|
88
|
+
const usedResource = resources.items[node.content.resource.id]
|
|
89
|
+
resourceName = StringUtils.dashCaseToCamelCase(
|
|
90
|
+
StringUtils.camelCaseToDashCase(
|
|
91
|
+
StringUtils.removeIllegalCharacters(usedResource.name + 'Resource')
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
dependencies[resourceName] = {
|
|
95
|
+
type: 'local',
|
|
96
|
+
path: relative(
|
|
97
|
+
join(...uidl.outputOptions.folderPath, uidl.outputOptions.fileName),
|
|
98
|
+
join(...resources.path, StringUtils.camelCaseToDashCase(usedResource.name))
|
|
99
|
+
),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if ('name' in node.content.resource) {
|
|
104
|
+
resourceName = StringUtils.dashCaseToCamelCase(
|
|
105
|
+
StringUtils.camelCaseToDashCase(
|
|
106
|
+
StringUtils.removeIllegalCharacters(node.content.resource.name)
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const extractedResource = types.variableDeclaration('const', [
|
|
112
|
+
types.variableDeclarator(
|
|
113
|
+
types.identifier(propKey),
|
|
114
|
+
types.awaitExpression(
|
|
115
|
+
types.callExpression(types.identifier(resourceName), [
|
|
116
|
+
types.objectExpression([
|
|
117
|
+
types.spreadElement(
|
|
118
|
+
types.optionalMemberExpression(
|
|
119
|
+
types.identifier('context'),
|
|
120
|
+
types.identifier('params'),
|
|
121
|
+
false,
|
|
122
|
+
true
|
|
123
|
+
)
|
|
124
|
+
),
|
|
125
|
+
...Object.keys(node.content.resource?.params || {}).reduce(
|
|
126
|
+
(acc: types.ObjectProperty[], item) => {
|
|
127
|
+
const prop = node.content.resource.params[item]
|
|
128
|
+
acc.push(
|
|
129
|
+
types.objectProperty(
|
|
130
|
+
types.stringLiteral(item),
|
|
131
|
+
ASTUtils.resolveObjectValue(prop as UIDLStaticValue)
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return acc
|
|
136
|
+
},
|
|
137
|
+
[]
|
|
138
|
+
),
|
|
139
|
+
]),
|
|
140
|
+
])
|
|
141
|
+
)
|
|
142
|
+
),
|
|
143
|
+
])
|
|
144
|
+
|
|
145
|
+
const responseMemberAST = ASTUtils.generateMemberExpressionASTFromPath([
|
|
146
|
+
propKey,
|
|
147
|
+
...(node.content.valuePath || []),
|
|
148
|
+
])
|
|
149
|
+
const notFoundAST = types.ifStatement(
|
|
150
|
+
types.unaryExpression('!', responseMemberAST),
|
|
151
|
+
types.blockStatement([
|
|
152
|
+
types.returnStatement(
|
|
153
|
+
types.objectExpression([
|
|
154
|
+
types.objectProperty(types.identifier('notFound'), types.booleanLiteral(true)),
|
|
155
|
+
])
|
|
156
|
+
),
|
|
157
|
+
])
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if (getStaticPropsChunk === undefined) {
|
|
161
|
+
const tryBlock = types.tryStatement(
|
|
162
|
+
types.blockStatement([
|
|
163
|
+
extractedResource,
|
|
164
|
+
notFoundAST,
|
|
165
|
+
types.returnStatement(
|
|
166
|
+
types.objectExpression([
|
|
167
|
+
types.objectProperty(
|
|
168
|
+
types.identifier('props'),
|
|
169
|
+
types.objectExpression([
|
|
170
|
+
types.objectProperty(
|
|
171
|
+
types.identifier(propKey),
|
|
172
|
+
responseMemberAST,
|
|
173
|
+
false,
|
|
174
|
+
false
|
|
175
|
+
),
|
|
176
|
+
])
|
|
177
|
+
),
|
|
178
|
+
types.objectProperty(types.identifier('revalidate'), types.numericLiteral(60)),
|
|
179
|
+
])
|
|
180
|
+
),
|
|
181
|
+
]),
|
|
182
|
+
types.catchClause(
|
|
183
|
+
types.identifier('error'),
|
|
184
|
+
types.blockStatement([
|
|
185
|
+
types.returnStatement(
|
|
186
|
+
types.objectExpression([
|
|
187
|
+
types.objectProperty(types.identifier('notFound'), types.booleanLiteral(true)),
|
|
188
|
+
])
|
|
189
|
+
),
|
|
190
|
+
])
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
const staticPropsChunk = types.exportNamedDeclaration(
|
|
194
|
+
types.functionDeclaration(
|
|
195
|
+
types.identifier('getStaticProps'),
|
|
196
|
+
[types.identifier('context')],
|
|
197
|
+
types.blockStatement([tryBlock]),
|
|
198
|
+
false,
|
|
199
|
+
true
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
getStaticPropsChunk = {
|
|
204
|
+
name: 'getStaticProps',
|
|
205
|
+
type: ChunkType.AST,
|
|
206
|
+
fileType: FileType.JS,
|
|
207
|
+
linkAfter: ['jsx-component'],
|
|
208
|
+
content: staticPropsChunk,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
chunks.push(getStaticPropsChunk)
|
|
212
|
+
} else {
|
|
213
|
+
const functionDecleration = (getStaticPropsChunk.content as types.ExportNamedDeclaration)
|
|
214
|
+
.declaration as types.FunctionDeclaration
|
|
215
|
+
const functionBody = functionDecleration.body.body
|
|
216
|
+
const tryBlock = functionBody.find(
|
|
217
|
+
(subNode) => subNode.type === 'TryStatement'
|
|
218
|
+
) as types.TryStatement
|
|
219
|
+
|
|
220
|
+
if (!tryBlock) {
|
|
221
|
+
throw new Error(`Try block not found for getStaticProps`)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
tryBlock.block.body.unshift(extractedResource, notFoundAST)
|
|
225
|
+
|
|
226
|
+
const returnStatement: types.ReturnStatement = tryBlock.block.body.find(
|
|
227
|
+
(subNode) => subNode.type === 'ReturnStatement'
|
|
228
|
+
) as types.ReturnStatement
|
|
229
|
+
|
|
230
|
+
if (!returnStatement) {
|
|
231
|
+
throw new Error(`Failed to find return statement for getStatisProps`)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const propsObject = (returnStatement.argument as types.ObjectExpression).properties.find(
|
|
235
|
+
(property) =>
|
|
236
|
+
((property as types.ObjectProperty).key as types.Identifier).name === 'props'
|
|
237
|
+
) as types.ObjectProperty
|
|
238
|
+
|
|
239
|
+
const propsValue = propsObject.value as types.ObjectExpression
|
|
240
|
+
propsValue.properties.unshift(
|
|
241
|
+
types.objectProperty(types.identifier(propKey), responseMemberAST, false, false)
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
/*
|
|
246
|
+
If a resource is not extracted, it means the data-fetch happens on the client-side.
|
|
247
|
+
which in case, we need to proxy the call using /api/ route in next-js.
|
|
248
|
+
So, the reosurces can take advantage of
|
|
249
|
+
process.env values while accessing the actual functions from the `/resources/` folder.
|
|
250
|
+
*/
|
|
251
|
+
|
|
252
|
+
let resourceFileName: string
|
|
253
|
+
let resourceImportVariable: string
|
|
254
|
+
let importPath: string
|
|
255
|
+
let isNamedImport: boolean = false
|
|
256
|
+
let funcParams = ''
|
|
257
|
+
|
|
258
|
+
if ('id' in node.content.resource) {
|
|
259
|
+
const usedResource = resources.items[node.content.resource.id]
|
|
260
|
+
if (!usedResource) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`Tried to find a resource that does not exist ${node.content.resource.id}`
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (
|
|
267
|
+
Object.keys(node.content.resource.params).length > 0 &&
|
|
268
|
+
Object.keys(usedResource.params).length > 0
|
|
269
|
+
) {
|
|
270
|
+
funcParams = 'req.query'
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
resourceImportVariable = StringUtils.dashCaseToCamelCase(
|
|
274
|
+
StringUtils.camelize(`${usedResource.name}-resource`)
|
|
275
|
+
)
|
|
276
|
+
const importName = StringUtils.camelCaseToDashCase(usedResource.name)
|
|
277
|
+
importPath = relative(
|
|
278
|
+
['pages', 'api'].join('/'),
|
|
279
|
+
[...resources.path, importName].join('/')
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
resourceFileName = StringUtils.camelCaseToDashCase(
|
|
283
|
+
`${resourceImportVariable}-${importName}`
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
computeUseEffectAST({
|
|
287
|
+
fileName: resourceFileName,
|
|
288
|
+
resourceType: usedResource.method,
|
|
289
|
+
node,
|
|
290
|
+
componentChunk,
|
|
291
|
+
params: node.content.resource.params,
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if ('name' in node.content.resource) {
|
|
296
|
+
const { name, dependency } = node.content.resource
|
|
297
|
+
resourceImportVariable = dependency.meta?.originalName || name
|
|
298
|
+
importPath = dependency?.meta?.importAlias || dependency.path
|
|
299
|
+
resourceFileName = StringUtils.camelCaseToDashCase(node.content.resource.name)
|
|
300
|
+
isNamedImport = dependency?.meta?.namedImport || false
|
|
301
|
+
|
|
302
|
+
/*
|
|
303
|
+
When we are calling external functions to make a request call for us.
|
|
304
|
+
The `fetch` that happens behind the scenes are basically encapsulated.
|
|
305
|
+
So, we can't derieve if the resource call is actually a GET / POST.
|
|
306
|
+
So, we can mark these as POST by default since we are taking data from the component.
|
|
307
|
+
*/
|
|
308
|
+
|
|
309
|
+
funcParams = 'req.body'
|
|
310
|
+
computeUseEffectAST({
|
|
311
|
+
fileName: resourceFileName,
|
|
312
|
+
resourceType: 'POST',
|
|
313
|
+
node,
|
|
314
|
+
componentChunk,
|
|
315
|
+
params: node.content.resource.params,
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
options.extractedResources[resourceFileName] = {
|
|
320
|
+
fileName: resourceFileName,
|
|
321
|
+
fileType: FileType.JS,
|
|
322
|
+
path: ['pages', 'api'],
|
|
323
|
+
content: `import ${
|
|
324
|
+
isNamedImport ? '{ ' + resourceImportVariable + ' }' : resourceImportVariable
|
|
325
|
+
} from '${importPath}'
|
|
326
|
+
export default async function handler(req, res) {
|
|
327
|
+
try {
|
|
328
|
+
const response = await ${resourceImportVariable}(${funcParams})
|
|
329
|
+
return res.status(200).json(response)
|
|
330
|
+
} catch (error) {
|
|
331
|
+
return res.status(500).send('Something went wrong')
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
`,
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
return structure
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return nextComponentCMSFetchPlugin
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export default createNextInlineFetchPlugin()
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {
|
|
2
|
+
UIDLCMSItemNode,
|
|
3
|
+
UIDLCMSListNode,
|
|
4
|
+
ChunkDefinition,
|
|
5
|
+
UIDLResourceLink,
|
|
6
|
+
UIDLResourceItem,
|
|
7
|
+
} from '@teleporthq/teleport-types'
|
|
8
|
+
import * as types from '@babel/types'
|
|
9
|
+
import { ASTUtils } from '@teleporthq/teleport-plugin-common'
|
|
10
|
+
|
|
11
|
+
export const computeUseEffectAST = (params: {
|
|
12
|
+
fileName: string
|
|
13
|
+
resourceType: UIDLResourceItem['method']
|
|
14
|
+
node: UIDLCMSItemNode | UIDLCMSListNode
|
|
15
|
+
componentChunk: ChunkDefinition
|
|
16
|
+
params: UIDLResourceLink['params']
|
|
17
|
+
}) => {
|
|
18
|
+
const { node, fileName, componentChunk, resourceType } = params
|
|
19
|
+
if (node.type !== 'cms-item' && node.type !== 'cms-list') {
|
|
20
|
+
throw new Error('Invalid node type passed to computeUseEffectAST')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { key, valuePath = [] } = node.content
|
|
24
|
+
const jsxNode = componentChunk.meta.nodesLookup[key] as types.JSXElement
|
|
25
|
+
let resourcePath: types.StringLiteral | types.TemplateLiteral = types.stringLiteral(
|
|
26
|
+
`/api/${fileName}`
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const resourceParameters: types.ObjectProperty[] = []
|
|
30
|
+
|
|
31
|
+
if (resourceType === 'GET' && Object.keys(params).length > 0) {
|
|
32
|
+
resourcePath = types.templateLiteral(
|
|
33
|
+
[
|
|
34
|
+
types.templateElement({ raw: `/api/${fileName}?`, cooked: `/api/${fileName}?` }),
|
|
35
|
+
types.templateElement({ raw: '', cooked: '' }),
|
|
36
|
+
],
|
|
37
|
+
[types.newExpression(types.identifier('URLSearchParams'), [types.identifier('params')])]
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (resourceType === 'POST' && Object.keys(params).length > 0) {
|
|
42
|
+
resourceParameters.push(
|
|
43
|
+
types.objectProperty(types.identifier('method'), types.stringLiteral('POST')),
|
|
44
|
+
types.objectProperty(
|
|
45
|
+
types.identifier('body'),
|
|
46
|
+
types.callExpression(
|
|
47
|
+
types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
|
|
48
|
+
[types.identifier('params')]
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const headers = types.objectProperty(
|
|
55
|
+
types.identifier('headers'),
|
|
56
|
+
types.objectExpression([
|
|
57
|
+
types.objectProperty(
|
|
58
|
+
types.stringLiteral('Content-Type'),
|
|
59
|
+
types.stringLiteral('application/json')
|
|
60
|
+
),
|
|
61
|
+
])
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
/*
|
|
65
|
+
For NextJS projects, we wrap the direct CMS calls with a `/api`
|
|
66
|
+
from Next. So, the return type is always going to be `json`. We can safely
|
|
67
|
+
set the return type to `.json()`. Because the actual return type is handled
|
|
68
|
+
in the resource itself. `/api/resource/resource.js`
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
const fetchAST = types.callExpression(
|
|
72
|
+
types.memberExpression(
|
|
73
|
+
types.callExpression(
|
|
74
|
+
types.identifier('fetch'),
|
|
75
|
+
[resourcePath, types.objectExpression([headers, ...resourceParameters])].filter(Boolean)
|
|
76
|
+
),
|
|
77
|
+
types.identifier('then')
|
|
78
|
+
),
|
|
79
|
+
[
|
|
80
|
+
types.arrowFunctionExpression(
|
|
81
|
+
[types.identifier('res')],
|
|
82
|
+
types.callExpression(
|
|
83
|
+
types.memberExpression(types.identifier('res'), types.identifier('json')),
|
|
84
|
+
[]
|
|
85
|
+
)
|
|
86
|
+
),
|
|
87
|
+
]
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
const responseExpression = ASTUtils.generateMemberExpressionASTFromPath([
|
|
91
|
+
'data',
|
|
92
|
+
...valuePath,
|
|
93
|
+
]) as types.OptionalMemberExpression
|
|
94
|
+
|
|
95
|
+
const resourceAST = types.arrowFunctionExpression(
|
|
96
|
+
[types.identifier('params')],
|
|
97
|
+
valuePath.length >= 0
|
|
98
|
+
? types.callExpression(types.memberExpression(fetchAST, types.identifier('then'), false), [
|
|
99
|
+
types.arrowFunctionExpression([types.identifier('data')], responseExpression),
|
|
100
|
+
])
|
|
101
|
+
: fetchAST
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
jsxNode.openingElement.attributes.unshift(
|
|
105
|
+
types.jSXAttribute(types.jsxIdentifier('fetchData'), types.jsxExpressionContainer(resourceAST))
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return
|
|
109
|
+
}
|