@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/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
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist/esm"
5
+ },
6
+ "include": [
7
+ "./src"
8
+ ]
9
+ }