@teleporthq/teleport-plugin-next-static-props 0.44.0-alpha.0 → 0.44.1
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/__tests__/entity-redirect.test.ts +301 -0
- package/__tests__/fetch-failure-revalidate.test.ts +69 -0
- package/__tests__/page-folder-name-collision.test.ts +96 -0
- package/__tests__/same-table-mutation-server-side-props.test.ts +285 -0
- package/__tests__/visitor-read.test.ts +157 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +41 -8
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils.d.ts +2 -2
- package/dist/cjs/utils.d.ts.map +1 -1
- package/dist/cjs/utils.js +297 -51
- package/dist/cjs/utils.js.map +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +42 -9
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils.d.ts +2 -2
- package/dist/esm/utils.d.ts.map +1 -1
- package/dist/esm/utils.js +297 -51
- package/dist/esm/utils.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +71 -11
- package/src/utils.ts +531 -84
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import generator from '@babel/generator'
|
|
2
|
+
import * as types from '@babel/types'
|
|
3
|
+
import { ComponentStructure } from '@teleporthq/teleport-types'
|
|
4
|
+
import { createStaticPropsPlugin } from '../src/index'
|
|
5
|
+
|
|
6
|
+
// Entity-level redirects (`initialPropsData.redirect`): a details page whose
|
|
7
|
+
// fetched row carries a destination answers with an HTTP redirect instead of
|
|
8
|
+
// rendering. Two invariants matter beyond the emitted shape:
|
|
9
|
+
// - the redirect must live INSIDE an IfStatement — several later plugins
|
|
10
|
+
// (addDynamicSeoPropsToGetStaticProps, the parallel/inline-fetch plugins)
|
|
11
|
+
// locate the props return via `.find((s) => s.type === 'ReturnStatement')`
|
|
12
|
+
// on the try block, so a second top-level return would derail them;
|
|
13
|
+
// - `statusCode` (not `permanent`) is emitted, because Next.js maps
|
|
14
|
+
// `permanent: true/false` to 308/307 while the feature promises 301/302.
|
|
15
|
+
|
|
16
|
+
const RESOURCE_ID = 'fetch-blog-post'
|
|
17
|
+
|
|
18
|
+
const makeStructure = (params: {
|
|
19
|
+
redirect?: {
|
|
20
|
+
destinationField: string
|
|
21
|
+
typeField?: string
|
|
22
|
+
unlessFieldEquals?: { field: string; value: string }
|
|
23
|
+
ownRowsFilter?: Array<Record<string, unknown>>
|
|
24
|
+
statusCode?: 301 | 302 | 307 | 308
|
|
25
|
+
}
|
|
26
|
+
skipI18n?: boolean
|
|
27
|
+
revalidate?: number
|
|
28
|
+
resourceParams?: Record<string, unknown>
|
|
29
|
+
}): ComponentStructure => {
|
|
30
|
+
const { redirect, skipI18n, revalidate, resourceParams } = params
|
|
31
|
+
return {
|
|
32
|
+
uidl: {
|
|
33
|
+
name: 'BlogPostDetails',
|
|
34
|
+
node: { type: 'element', content: { elementType: 'container' } },
|
|
35
|
+
outputOptions: {
|
|
36
|
+
pageId: 'page-blog-post-details',
|
|
37
|
+
folderPath: ['blog'],
|
|
38
|
+
fileName: '[slug]',
|
|
39
|
+
initialPropsData: {
|
|
40
|
+
exposeAs: { name: 'blogPost', valuePath: ['data', '0'] },
|
|
41
|
+
resource: { id: RESOURCE_ID, params: resourceParams || {} },
|
|
42
|
+
...(revalidate ? { cache: { revalidate } } : {}),
|
|
43
|
+
...(redirect ? { redirect } : {}),
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
chunks: [],
|
|
48
|
+
dependencies: {},
|
|
49
|
+
options: {
|
|
50
|
+
skipI18n,
|
|
51
|
+
resources: {
|
|
52
|
+
items: {
|
|
53
|
+
[RESOURCE_ID]: { id: RESOURCE_ID, name: 'blog-post' },
|
|
54
|
+
},
|
|
55
|
+
path: ['utils', 'data-sources'],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
} as unknown as ComponentStructure
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const generateCode = async (structure: ComponentStructure): Promise<string> => {
|
|
62
|
+
const plugin = createStaticPropsPlugin()
|
|
63
|
+
const result = await plugin(structure)
|
|
64
|
+
const chunk = result.chunks.find((c) => c.name === 'getStaticProps')
|
|
65
|
+
expect(chunk).toBeDefined()
|
|
66
|
+
return generator(chunk?.content as types.Node).code
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe('teleport-plugin-next-static-props: two rows at one address', () => {
|
|
70
|
+
const OWN_ROWS = [
|
|
71
|
+
{ type: 'condition', source: 'product_page', destination: 'personalise', operand: '=' },
|
|
72
|
+
]
|
|
73
|
+
const productPage = (ownRowsFilter?: Array<Record<string, unknown>>) =>
|
|
74
|
+
makeStructure({
|
|
75
|
+
redirect: {
|
|
76
|
+
destinationField: 'productPageUrl',
|
|
77
|
+
unlessFieldEquals: { field: 'productPageKey', value: 'personalise' },
|
|
78
|
+
...(ownRowsFilter ? { ownRowsFilter } : {}),
|
|
79
|
+
statusCode: 302,
|
|
80
|
+
},
|
|
81
|
+
revalidate: 60,
|
|
82
|
+
resourceParams: {
|
|
83
|
+
filters: {
|
|
84
|
+
type: 'expr',
|
|
85
|
+
content:
|
|
86
|
+
'JSON.stringify([{ type: "condition", source: "slug", destination: params.slug, operand: "=" }])',
|
|
87
|
+
},
|
|
88
|
+
limit: { type: 'static', content: 1 },
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// Two products may share a slug on different product pages: the page shows
|
|
93
|
+
// its own one, and redirects only when it has none at that address.
|
|
94
|
+
it('looks for an own row at the address before sending the visitor on', async () => {
|
|
95
|
+
const code = await generateCode(productPage(OWN_ROWS))
|
|
96
|
+
expect(code).toContain(
|
|
97
|
+
'if (String(response?.data?.[0]?.productPageKey ?? "") !== "personalise") {'
|
|
98
|
+
)
|
|
99
|
+
expect(code).toContain('const ownRowsResponse = await')
|
|
100
|
+
expect(code).toContain('"filters": JSON.stringify(JSON.parse(JSON.stringify([{')
|
|
101
|
+
expect(code).toContain('.concat([{')
|
|
102
|
+
expect(code).toContain('destination: "personalise"')
|
|
103
|
+
expect(code).toContain('response.data = ownRowsResponse.data')
|
|
104
|
+
expect(code.indexOf('ownRowsResponse')).toBeLessThan(code.indexOf('entityRedirectUrl'))
|
|
105
|
+
expect(code.indexOf('notFound')).toBeLessThan(code.indexOf('ownRowsResponse'))
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('never redirects while pre-rendering: the path answers 404 until it revalidates', async () => {
|
|
109
|
+
const code = await generateCode(productPage(OWN_ROWS))
|
|
110
|
+
const redirectCheck = code.indexOf('if (entityRedirectUrl)')
|
|
111
|
+
const buildCheck = code.indexOf('process.env.NEXT_PHASE === "phase-production-build"')
|
|
112
|
+
expect(buildCheck).toBeGreaterThan(redirectCheck)
|
|
113
|
+
expect(buildCheck).toBeLessThan(code.indexOf('redirect: {'))
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('leaves a page without the own-rows filter, and the blog redirects, as they were', async () => {
|
|
117
|
+
const withoutFilter = await generateCode(productPage())
|
|
118
|
+
expect(withoutFilter).not.toContain('ownRowsResponse')
|
|
119
|
+
const blog = await generateCode(
|
|
120
|
+
makeStructure({ redirect: { destinationField: 'redirectUrl', typeField: 'redirectType' } })
|
|
121
|
+
)
|
|
122
|
+
expect(blog).not.toContain('ownRowsResponse')
|
|
123
|
+
expect(blog).not.toContain('NEXT_PHASE')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('keeps the props return the only top-level return', async () => {
|
|
127
|
+
const plugin = createStaticPropsPlugin()
|
|
128
|
+
const result = await plugin(productPage(OWN_ROWS))
|
|
129
|
+
const chunk = result.chunks.find((c) => c.name === 'getStaticProps')
|
|
130
|
+
const fn = (chunk?.content as types.ExportNamedDeclaration)
|
|
131
|
+
.declaration as types.FunctionDeclaration
|
|
132
|
+
const tryStmt = fn.body.body.find(
|
|
133
|
+
(statement): statement is types.TryStatement => statement.type === 'TryStatement'
|
|
134
|
+
)
|
|
135
|
+
expect(
|
|
136
|
+
tryStmt!.block.body.filter((statement) => statement.type === 'ReturnStatement')
|
|
137
|
+
).toHaveLength(1)
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('teleport-plugin-next-static-props: entity redirect', () => {
|
|
142
|
+
// A page that shows a SUBSET of a table's rows (a custom product page shows
|
|
143
|
+
// the products assigned to it): every other row is sent to its own page,
|
|
144
|
+
// with a temporary status because a row can move back.
|
|
145
|
+
it('redirects only while the row belongs to another page, with the status asked for', async () => {
|
|
146
|
+
const code = await generateCode(
|
|
147
|
+
makeStructure({
|
|
148
|
+
redirect: {
|
|
149
|
+
destinationField: 'productPageUrl',
|
|
150
|
+
unlessFieldEquals: { field: 'productPageKey', value: 'personalise' },
|
|
151
|
+
statusCode: 302,
|
|
152
|
+
},
|
|
153
|
+
revalidate: 60,
|
|
154
|
+
})
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
expect(code).toContain(
|
|
158
|
+
'const entityRedirectUrl = String(response?.data?.[0]?.productPageKey ?? "") !== "personalise" ? response?.data?.[0]?.productPageUrl : undefined'
|
|
159
|
+
)
|
|
160
|
+
expect(code).toContain('statusCode: 302')
|
|
161
|
+
expect(code).toContain('revalidate: 60')
|
|
162
|
+
expect(code.indexOf('notFound')).toBeLessThan(code.indexOf('entityRedirectUrl'))
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('keeps the standard product page own products: its key is empty', async () => {
|
|
166
|
+
const code = await generateCode(
|
|
167
|
+
makeStructure({
|
|
168
|
+
redirect: {
|
|
169
|
+
destinationField: 'productPageUrl',
|
|
170
|
+
unlessFieldEquals: { field: 'productPageKey', value: '' },
|
|
171
|
+
statusCode: 302,
|
|
172
|
+
},
|
|
173
|
+
})
|
|
174
|
+
)
|
|
175
|
+
expect(code).toContain('String(response?.data?.[0]?.productPageKey ?? "") !== ""')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('emits a statusCode redirect between the notFound check and the props return', async () => {
|
|
179
|
+
const code = await generateCode(
|
|
180
|
+
makeStructure({ redirect: { destinationField: 'redirectUrl', typeField: 'redirectType' } })
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
expect(code).toContain('const entityRedirectUrl = response?.data?.[0]?.redirectUrl')
|
|
184
|
+
expect(code).toContain('if (entityRedirectUrl)')
|
|
185
|
+
expect(code).toContain('statusCode: response?.data?.[0]?.redirectType === "302" ? 302 : 301')
|
|
186
|
+
expect(code).not.toContain('permanent')
|
|
187
|
+
|
|
188
|
+
// The 404 for a missing row must win over the redirect check.
|
|
189
|
+
expect(code.indexOf('notFound')).toBeLessThan(code.indexOf('entityRedirectUrl'))
|
|
190
|
+
expect(code.indexOf('entityRedirectUrl')).toBeLessThan(code.indexOf('props:'))
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
/*
|
|
194
|
+
Without `revalidate`, an ISR entry has NO expiry: Next.js caches the branch
|
|
195
|
+
it took and never runs getStaticProps for that path again. A page that
|
|
196
|
+
redirected once would redirect for ever — clearing `redirect_url` in the
|
|
197
|
+
admin would change the row and change nothing a visitor sees — and a row
|
|
198
|
+
that 404'd would stay 404 after being published. Every branch carries the
|
|
199
|
+
window, not just the one that renders: the missing row, the redirect, the
|
|
200
|
+
props and the failed fetch.
|
|
201
|
+
*/
|
|
202
|
+
it('gives the redirect and the notFound the same revalidate window as the props', async () => {
|
|
203
|
+
const code = await generateCode(
|
|
204
|
+
makeStructure({
|
|
205
|
+
redirect: { destinationField: 'redirectUrl', typeField: 'redirectType' },
|
|
206
|
+
revalidate: 60,
|
|
207
|
+
})
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
expect(code.match(/revalidate: 60/g)).toHaveLength(4)
|
|
211
|
+
const redirectAt = code.indexOf('if (entityRedirectUrl)')
|
|
212
|
+
const propsAt = code.indexOf('props:')
|
|
213
|
+
// The one between the redirect check and the props return is the redirect's.
|
|
214
|
+
expect(code.indexOf('revalidate: 60', redirectAt)).toBeLessThan(propsAt)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('emits no revalidate anywhere when the page has no cache window', async () => {
|
|
218
|
+
const code = await generateCode(
|
|
219
|
+
makeStructure({ redirect: { destinationField: 'redirectUrl', typeField: 'redirectType' } })
|
|
220
|
+
)
|
|
221
|
+
expect(code).not.toContain('revalidate')
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('keeps the props return as the ONLY top-level ReturnStatement in the try block', async () => {
|
|
225
|
+
const plugin = createStaticPropsPlugin()
|
|
226
|
+
const result = await plugin(
|
|
227
|
+
makeStructure({ redirect: { destinationField: 'redirectUrl', typeField: 'redirectType' } })
|
|
228
|
+
)
|
|
229
|
+
const chunk = result.chunks.find((c) => c.name === 'getStaticProps')
|
|
230
|
+
const exportDecl = chunk?.content as types.ExportNamedDeclaration
|
|
231
|
+
const fn = exportDecl.declaration as types.FunctionDeclaration
|
|
232
|
+
const tryStmt = fn.body.body.find(
|
|
233
|
+
(statement): statement is types.TryStatement => statement.type === 'TryStatement'
|
|
234
|
+
)
|
|
235
|
+
expect(tryStmt).toBeDefined()
|
|
236
|
+
|
|
237
|
+
const topLevelReturns = tryStmt!.block.body.filter(
|
|
238
|
+
(statement) => statement.type === 'ReturnStatement'
|
|
239
|
+
)
|
|
240
|
+
expect(topLevelReturns).toHaveLength(1)
|
|
241
|
+
|
|
242
|
+
// And the one `.find(ReturnStatement)` later plugins run must land on the
|
|
243
|
+
// props return, not the redirect.
|
|
244
|
+
const found = tryStmt!.block.body.find(
|
|
245
|
+
(statement) => statement.type === 'ReturnStatement'
|
|
246
|
+
) as types.ReturnStatement
|
|
247
|
+
const foundCode = generator(found).code
|
|
248
|
+
expect(foundCode).toContain('props:')
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('prefixes internal destinations with the active non-default locale when i18n is on', async () => {
|
|
252
|
+
const code = await generateCode(
|
|
253
|
+
makeStructure({ redirect: { destinationField: 'redirectUrl', typeField: 'redirectType' } })
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
expect(code).toContain('context?.locale')
|
|
257
|
+
expect(code).toContain('context?.defaultLocale')
|
|
258
|
+
expect(code).toContain('entityRedirectUrl.startsWith("/")')
|
|
259
|
+
// tslint:disable-next-line:no-invalid-template-strings
|
|
260
|
+
expect(code).toContain('`/${context.locale}${entityRedirectUrl}`')
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('uses the destination as-is when i18n is skipped', async () => {
|
|
264
|
+
const code = await generateCode(
|
|
265
|
+
makeStructure({
|
|
266
|
+
redirect: { destinationField: 'redirectUrl', typeField: 'redirectType' },
|
|
267
|
+
skipI18n: true,
|
|
268
|
+
})
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
expect(code).toContain('destination: entityRedirectUrl')
|
|
272
|
+
expect(code).not.toContain('entityRedirectUrl.startsWith')
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('defaults to a permanent 301 when no typeField is configured', async () => {
|
|
276
|
+
const code = await generateCode(
|
|
277
|
+
makeStructure({ redirect: { destinationField: 'redirectUrl' } })
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
expect(code).toContain('statusCode: 301')
|
|
281
|
+
expect(code).not.toContain('redirectType')
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('emits no redirect code at all without a redirect config (regression guard)', async () => {
|
|
285
|
+
const code = await generateCode(makeStructure({}))
|
|
286
|
+
|
|
287
|
+
expect(code).not.toContain('entityRedirectUrl')
|
|
288
|
+
expect(code).not.toContain('redirect')
|
|
289
|
+
})
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
describe('teleport-plugin-next-static-props: the route reaches the page', () => {
|
|
293
|
+
it("passes the route's parameters as props, before the fetched row", async () => {
|
|
294
|
+
// A child list filtered by the entry the address names reads `props.slug`
|
|
295
|
+
// in the browser; without it the filter was dropped and every row showed.
|
|
296
|
+
const code = (await generateCode(makeStructure({}))).replace(/\s+/g, ' ')
|
|
297
|
+
expect(code).toContain(
|
|
298
|
+
'props: { ...context?.params, blogPost: response?.data?.[0], ...response?.meta }'
|
|
299
|
+
)
|
|
300
|
+
})
|
|
301
|
+
})
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import generator from '@babel/generator'
|
|
2
|
+
import * as types from '@babel/types'
|
|
3
|
+
import { ComponentStructure } from '@teleporthq/teleport-types'
|
|
4
|
+
import { createStaticPropsPlugin } from '../src/index'
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
A details page answers 404 both when its row does not exist and when the
|
|
8
|
+
fetch for it FAILED. The failure is not a fact about the row: a timeout or a
|
|
9
|
+
rate limit — every details page calls its source while the site is built —
|
|
10
|
+
must not leave the record unreachable until the next deploy. Next.js caches a
|
|
11
|
+
`notFound` that carries no `revalidate` for good, so the failure branch
|
|
12
|
+
carries the same window as every other branch.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const RESOURCE_ID = 'fetch-template-detail'
|
|
16
|
+
|
|
17
|
+
const makeStructure = (revalidate?: number): ComponentStructure =>
|
|
18
|
+
({
|
|
19
|
+
uidl: {
|
|
20
|
+
name: 'TemplateDetails',
|
|
21
|
+
node: { type: 'element', content: { elementType: 'container' } },
|
|
22
|
+
outputOptions: {
|
|
23
|
+
pageId: 'page-template-details',
|
|
24
|
+
folderPath: ['templates'],
|
|
25
|
+
fileName: '[slug]',
|
|
26
|
+
initialPropsData: {
|
|
27
|
+
exposeAs: { name: 'template', valuePath: ['data', '0'] },
|
|
28
|
+
resource: { id: RESOURCE_ID, params: {} },
|
|
29
|
+
...(revalidate ? { cache: { revalidate } } : {}),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
chunks: [],
|
|
34
|
+
dependencies: {},
|
|
35
|
+
options: {
|
|
36
|
+
skipI18n: true,
|
|
37
|
+
resources: {
|
|
38
|
+
items: { [RESOURCE_ID]: { id: RESOURCE_ID, name: 'template-detail' } },
|
|
39
|
+
path: ['utils', 'data-sources'],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
} as unknown as ComponentStructure)
|
|
43
|
+
|
|
44
|
+
const catchBranchCode = async (structure: ComponentStructure): Promise<string> => {
|
|
45
|
+
const result = await createStaticPropsPlugin()(structure)
|
|
46
|
+
const chunk = result.chunks.find((c) => c.name === 'getStaticProps')
|
|
47
|
+
const declaration = (chunk?.content as types.ExportNamedDeclaration)
|
|
48
|
+
.declaration as types.FunctionDeclaration
|
|
49
|
+
const tryStatement = declaration.body.body.find(
|
|
50
|
+
(statement): statement is types.TryStatement => statement.type === 'TryStatement'
|
|
51
|
+
)
|
|
52
|
+
return generator(tryStatement?.handler as types.Node).code
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('teleport-plugin-next-static-props: a failed fetch', () => {
|
|
56
|
+
it('answers 404 with the page revalidate window, so the next request retries', async () => {
|
|
57
|
+
const code = await catchBranchCode(makeStructure(60))
|
|
58
|
+
|
|
59
|
+
expect(code).toContain('notFound: true')
|
|
60
|
+
expect(code).toContain('revalidate: 60')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('carries no revalidate when the page has no window', async () => {
|
|
64
|
+
const code = await catchBranchCode(makeStructure())
|
|
65
|
+
|
|
66
|
+
expect(code).toContain('notFound: true')
|
|
67
|
+
expect(code).not.toContain('revalidate')
|
|
68
|
+
})
|
|
69
|
+
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { ComponentStructure } from '@teleporthq/teleport-types'
|
|
2
|
+
import { createStaticPropsPlugin } from '../src/index'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Regression guard for the `pages/resources/[id].js` build break:
|
|
6
|
+
*
|
|
7
|
+
* Module not found: Can't resolve '../fetch_content_items_detail'
|
|
8
|
+
*
|
|
9
|
+
* `outputOptions.folderPath` is relative to the PAGES ROOT while
|
|
10
|
+
* `resources.path` is relative to the PROJECT ROOT, so resolving one against
|
|
11
|
+
* the other cancelled the `resources` segment they only appeared to share and
|
|
12
|
+
* the import pointed inside `pages/`. Every page whose folder is named after a
|
|
13
|
+
* project-root folder hits it — and only those, which is why it went unnoticed.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const RESOURCE_ID = 'fetch-content-item'
|
|
17
|
+
const RESOURCE_NAME = 'fetch_content_items_detail'
|
|
18
|
+
|
|
19
|
+
const makeStructure = (params: {
|
|
20
|
+
folderPath: string[]
|
|
21
|
+
resourcesPath?: string[]
|
|
22
|
+
pagesPath?: string[]
|
|
23
|
+
}): ComponentStructure => {
|
|
24
|
+
const { folderPath, resourcesPath = ['resources'], pagesPath } = params
|
|
25
|
+
return {
|
|
26
|
+
uidl: {
|
|
27
|
+
name: 'CustomResources',
|
|
28
|
+
node: { type: 'element', content: { elementType: 'container' } },
|
|
29
|
+
outputOptions: {
|
|
30
|
+
folderPath,
|
|
31
|
+
fileName: '[id]',
|
|
32
|
+
initialPropsData: {
|
|
33
|
+
exposeAs: { name: 'contentItem', valuePath: [] },
|
|
34
|
+
resource: { id: RESOURCE_ID, params: {} },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
chunks: [],
|
|
39
|
+
dependencies: {},
|
|
40
|
+
options: {
|
|
41
|
+
...(pagesPath && { pagesPath }),
|
|
42
|
+
resources: {
|
|
43
|
+
items: { [RESOURCE_ID]: { id: RESOURCE_ID, name: RESOURCE_NAME } },
|
|
44
|
+
path: resourcesPath,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
} as unknown as ComponentStructure
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('teleport-plugin-next-static-props: resource import path', () => {
|
|
51
|
+
const plugin = createStaticPropsPlugin()
|
|
52
|
+
|
|
53
|
+
it('reaches the project resources folder from a page whose folder is named "resources"', async () => {
|
|
54
|
+
const structure = await plugin(makeStructure({ folderPath: ['resources'] }))
|
|
55
|
+
|
|
56
|
+
expect(structure.dependencies.fetchContentItemsDetailResource.path).toBe(
|
|
57
|
+
`../../resources/${RESOURCE_NAME}`
|
|
58
|
+
)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('keeps working for a page in a non-colliding subfolder', async () => {
|
|
62
|
+
const structure = await plugin(makeStructure({ folderPath: ['blog'] }))
|
|
63
|
+
|
|
64
|
+
expect(structure.dependencies.fetchContentItemsDetailResource.path).toBe(
|
|
65
|
+
`../../resources/${RESOURCE_NAME}`
|
|
66
|
+
)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('keeps working for a page at the pages root', async () => {
|
|
70
|
+
const structure = await plugin(makeStructure({ folderPath: [] }))
|
|
71
|
+
|
|
72
|
+
expect(structure.dependencies.fetchContentItemsDetailResource.path).toBe(
|
|
73
|
+
`../resources/${RESOURCE_NAME}`
|
|
74
|
+
)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('does not collapse a multi-segment resources folder either', async () => {
|
|
78
|
+
const structure = await plugin(
|
|
79
|
+
makeStructure({ folderPath: ['utils'], resourcesPath: ['utils', 'data-sources'] })
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
expect(structure.dependencies.fetchContentItemsDetailResource.path).toBe(
|
|
83
|
+
`../../utils/data-sources/${RESOURCE_NAME}`
|
|
84
|
+
)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('honours a pages root that is not the project root child', async () => {
|
|
88
|
+
const structure = await plugin(
|
|
89
|
+
makeStructure({ folderPath: ['resources'], pagesPath: ['src', 'pages'] })
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
expect(structure.dependencies.fetchContentItemsDetailResource.path).toBe(
|
|
93
|
+
`../../../resources/${RESOURCE_NAME}`
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
})
|