@teleporthq/teleport-project-generator 0.44.0-alpha.0 → 0.44.0
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__/content-settings-propagation.ts +53 -0
- package/__tests__/details-page-info-propagation.ts +111 -0
- package/__tests__/file-handlers.ts +34 -0
- package/__tests__/index.ts +37 -10
- package/__tests__/resource-runtime-params.ts +113 -0
- package/__tests__/utils.ts +104 -2
- package/dist/cjs/file-handlers.d.ts.map +1 -1
- package/dist/cjs/file-handlers.js +22 -0
- package/dist/cjs/file-handlers.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +226 -104
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/resource.d.ts.map +1 -1
- package/dist/cjs/resource.js +150 -0
- package/dist/cjs/resource.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils.d.ts +17 -1
- package/dist/cjs/utils.d.ts.map +1 -1
- package/dist/cjs/utils.js +66 -10
- package/dist/cjs/utils.js.map +1 -1
- package/dist/esm/file-handlers.d.ts.map +1 -1
- package/dist/esm/file-handlers.js +23 -1
- package/dist/esm/file-handlers.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +227 -105
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/resource.d.ts.map +1 -1
- package/dist/esm/resource.js +150 -0
- package/dist/esm/resource.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils.d.ts +17 -1
- package/dist/esm/utils.d.ts.map +1 -1
- package/dist/esm/utils.js +65 -10
- package/dist/esm/utils.js.map +1 -1
- package/package.json +9 -9
- package/src/file-handlers.ts +33 -1
- package/src/index.ts +266 -110
- package/src/resource.ts +192 -0
- package/src/utils.ts +104 -14
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ProjectUIDL } from '@teleporthq/teleport-types'
|
|
2
|
+
import { createProjectGenerator } from '../src'
|
|
3
|
+
import { createStrategyWithCommonGenerator } from './mocks'
|
|
4
|
+
import projectUIDL from '../../../examples/test-samples/project-sample.json'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Each content preset's settings reach the page generators on their own: a
|
|
8
|
+
* Help Center needs no Blog. Passed only inside the Blog's condition, a
|
|
9
|
+
* Help-Center-only project generated its help articles' transform with an
|
|
10
|
+
* EMPTY taxonomy — every article lost its categories' ids, and the article
|
|
11
|
+
* page's breadcrumbs showed "Help Center" and nothing after it.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const helpCenterSettings = {
|
|
15
|
+
categories: [{ id: 'guides', name: 'Guides', slug: 'guides', children: [] }],
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const generatedWith = async (uidl: ProjectUIDL) => {
|
|
19
|
+
const strategy = createStrategyWithCommonGenerator()
|
|
20
|
+
const generator = createProjectGenerator(strategy)
|
|
21
|
+
await generator.generateProject(uidl)
|
|
22
|
+
return (generator.pageGenerator.generateComponent as jest.Mock).mock.calls.map(
|
|
23
|
+
(call) => call[1] as Record<string, unknown>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('content preset settings handed to the page generators', () => {
|
|
28
|
+
it('include a Help Center’s on a project without a blog', async () => {
|
|
29
|
+
const uidl = JSON.parse(JSON.stringify(projectUIDL)) as ProjectUIDL
|
|
30
|
+
delete (uidl as { blogSettings?: unknown }).blogSettings
|
|
31
|
+
uidl.helpCenterSettings = helpCenterSettings as never
|
|
32
|
+
|
|
33
|
+
const pageOptions = await generatedWith(uidl)
|
|
34
|
+
|
|
35
|
+
expect(pageOptions.length).toBeGreaterThan(0)
|
|
36
|
+
for (const options of pageOptions) {
|
|
37
|
+
expect(options.helpCenterSettings).toEqual(helpCenterSettings)
|
|
38
|
+
expect(options).not.toHaveProperty('blogSettings')
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('include both presets’ when the project has both', async () => {
|
|
43
|
+
const uidl = JSON.parse(JSON.stringify(projectUIDL)) as ProjectUIDL
|
|
44
|
+
const blogSettings = { categories: [] }
|
|
45
|
+
uidl.blogSettings = blogSettings as never
|
|
46
|
+
uidl.helpCenterSettings = helpCenterSettings as never
|
|
47
|
+
|
|
48
|
+
const [options] = await generatedWith(uidl)
|
|
49
|
+
|
|
50
|
+
expect(options.blogSettings).toEqual(blogSettings)
|
|
51
|
+
expect(options.helpCenterSettings).toEqual(helpCenterSettings)
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createPageUIDLs } from '../src/utils'
|
|
2
|
+
import { createStrategyWithCommonGenerator } from './mocks'
|
|
3
|
+
import {
|
|
4
|
+
conditionalNode,
|
|
5
|
+
dynamicNode,
|
|
6
|
+
elementNode,
|
|
7
|
+
staticNode,
|
|
8
|
+
} from '@teleporthq/teleport-uidl-builders'
|
|
9
|
+
import { ProjectUIDL } from '@teleporthq/teleport-types'
|
|
10
|
+
|
|
11
|
+
// Regression guard: `detailsPageInfo` (the table-name metadata a page's
|
|
12
|
+
// route-level `pageOptions` carries for entity-bound details pages) must
|
|
13
|
+
// survive `createPageUIDL`'s output-options construction and land on the
|
|
14
|
+
// final page UIDL's `outputOptions`. Without it, downstream plugins (e.g.
|
|
15
|
+
// teleport-plugin-next-static-props' `pageHasSameTableMutationWorkflow`,
|
|
16
|
+
// which decides getServerSideProps vs. getStaticProps+ISR) have no way to
|
|
17
|
+
// correlate "the row this page reads" with "the table a same-page mutation
|
|
18
|
+
// workflow writes to" — silently disabling that entire fix.
|
|
19
|
+
describe('createPageUIDLs propagates detailsPageInfo to outputOptions', () => {
|
|
20
|
+
const buildProjectUIDL = (): ProjectUIDL => {
|
|
21
|
+
const routeRef = dynamicNode('state', 'route')
|
|
22
|
+
|
|
23
|
+
const homeNode = conditionalNode(
|
|
24
|
+
routeRef,
|
|
25
|
+
elementNode('container', {}, [staticNode('Home')]),
|
|
26
|
+
'home'
|
|
27
|
+
)
|
|
28
|
+
const detailsNode = conditionalNode(
|
|
29
|
+
routeRef,
|
|
30
|
+
elementNode('container', {}, [staticNode('Details')]),
|
|
31
|
+
'edit-press-item/[id]'
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
name: 'testProject',
|
|
36
|
+
globals: {
|
|
37
|
+
settings: { language: 'en', title: 'Test Project' },
|
|
38
|
+
meta: [],
|
|
39
|
+
assets: [],
|
|
40
|
+
},
|
|
41
|
+
root: {
|
|
42
|
+
name: 'App',
|
|
43
|
+
stateDefinitions: {
|
|
44
|
+
route: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
defaultValue: 'home',
|
|
47
|
+
values: [
|
|
48
|
+
{
|
|
49
|
+
value: 'home',
|
|
50
|
+
pageOptions: {
|
|
51
|
+
navLink: '/',
|
|
52
|
+
fileName: 'index',
|
|
53
|
+
componentName: 'Home',
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
value: 'edit-press-item/[id]',
|
|
58
|
+
pageOptions: {
|
|
59
|
+
navLink: '/edit-press-item/[id]',
|
|
60
|
+
fileName: '[id]',
|
|
61
|
+
componentName: 'EditPressItem',
|
|
62
|
+
dynamicRouteAttribute: 'id',
|
|
63
|
+
initialPropsData: {
|
|
64
|
+
exposeAs: { name: 'pressItem', valuePath: [] },
|
|
65
|
+
resource: { id: 'fetch-press-item', params: {} },
|
|
66
|
+
},
|
|
67
|
+
detailsPageInfo: {
|
|
68
|
+
dataSourceId: 'ds-1',
|
|
69
|
+
dataSourceName: 'TeleportHQ database',
|
|
70
|
+
dataSourceType: 'teleport',
|
|
71
|
+
tableName: 'press_items',
|
|
72
|
+
differentiatorColumn: 'id',
|
|
73
|
+
featureIdentifier: 'pressItem',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
node: elementNode('Router', {}, [homeNode, detailsNode]),
|
|
81
|
+
},
|
|
82
|
+
components: {},
|
|
83
|
+
} as unknown as ProjectUIDL
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
it('carries detailsPageInfo through onto the details page outputOptions', () => {
|
|
87
|
+
const uidl = buildProjectUIDL()
|
|
88
|
+
const strategy = createStrategyWithCommonGenerator()
|
|
89
|
+
|
|
90
|
+
const pages = createPageUIDLs(uidl, strategy)
|
|
91
|
+
expect(pages).toHaveLength(2)
|
|
92
|
+
|
|
93
|
+
const detailsPage = pages.find((p) => p.name === 'EditPressItem')
|
|
94
|
+
expect(detailsPage).toBeDefined()
|
|
95
|
+
expect(detailsPage!.outputOptions?.dynamicRouteAttribute).toBe('id')
|
|
96
|
+
expect(detailsPage!.outputOptions?.detailsPageInfo?.tableName).toBe('press_items')
|
|
97
|
+
expect(detailsPage!.outputOptions?.detailsPageInfo?.differentiatorColumn).toBe('id')
|
|
98
|
+
expect(detailsPage!.outputOptions?.initialPropsData).toBeDefined()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('leaves detailsPageInfo undefined for a regular page that is not a details page', () => {
|
|
102
|
+
const uidl = buildProjectUIDL()
|
|
103
|
+
const strategy = createStrategyWithCommonGenerator()
|
|
104
|
+
|
|
105
|
+
const pages = createPageUIDLs(uidl, strategy)
|
|
106
|
+
const homePage = pages.find((p) => p.name === 'Home')
|
|
107
|
+
|
|
108
|
+
expect(homePage).toBeDefined()
|
|
109
|
+
expect(homePage!.outputOptions?.detailsPageInfo).toBeUndefined()
|
|
110
|
+
})
|
|
111
|
+
})
|
|
@@ -30,6 +30,40 @@ describe('createHtmlIndexFile', () => {
|
|
|
30
30
|
expect(entryFile.content).toContain('<html')
|
|
31
31
|
expect(entryFile.content).toContain('{{root-placeholder}}')
|
|
32
32
|
})
|
|
33
|
+
|
|
34
|
+
it('puts the inline script error guard first in <head> when the project runs scripts of its own', async () => {
|
|
35
|
+
const uidl = {
|
|
36
|
+
...uidlSample,
|
|
37
|
+
globals: {
|
|
38
|
+
...uidlSample.globals,
|
|
39
|
+
assets: [],
|
|
40
|
+
customCode: { body: '<script>null.addEventListener("click", () => {})</script>' },
|
|
41
|
+
},
|
|
42
|
+
} as unknown as ProjectUIDL
|
|
43
|
+
|
|
44
|
+
const [entryFile] = await createEntryFile(uidl, createStrategyWithCommonGenerator(), {})
|
|
45
|
+
const { content } = entryFile
|
|
46
|
+
const firstScript = content.slice(content.indexOf('<script'))
|
|
47
|
+
|
|
48
|
+
expect(content.indexOf('<script')).toBeGreaterThan(content.indexOf('<head>'))
|
|
49
|
+
expect(firstScript.indexOf('stopImmediatePropagation')).toBeLessThan(
|
|
50
|
+
firstScript.indexOf('</script>')
|
|
51
|
+
)
|
|
52
|
+
expect(content.indexOf('stopImmediatePropagation')).toBeLessThan(
|
|
53
|
+
content.indexOf('null.addEventListener')
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('emits no guard for a project without scripts of its own', async () => {
|
|
58
|
+
const uidl = {
|
|
59
|
+
...uidlSample,
|
|
60
|
+
globals: { ...uidlSample.globals, assets: [], customCode: { head: '<style></style>' } },
|
|
61
|
+
} as unknown as ProjectUIDL
|
|
62
|
+
|
|
63
|
+
const [entryFile] = await createEntryFile(uidl, createStrategyWithCommonGenerator(), {})
|
|
64
|
+
|
|
65
|
+
expect(entryFile.content).not.toContain('stopImmediatePropagation')
|
|
66
|
+
})
|
|
33
67
|
})
|
|
34
68
|
|
|
35
69
|
describe('createManifestJSONFile', () => {
|
package/__tests__/index.ts
CHANGED
|
@@ -56,7 +56,7 @@ describe('Generic Project Generator', () => {
|
|
|
56
56
|
},
|
|
57
57
|
designLanguage: undefined,
|
|
58
58
|
mapping: {},
|
|
59
|
-
|
|
59
|
+
extractedResources: {},
|
|
60
60
|
skipValidation: true,
|
|
61
61
|
})
|
|
62
62
|
)
|
|
@@ -64,6 +64,8 @@ describe('Generic Project Generator', () => {
|
|
|
64
64
|
|
|
65
65
|
const routerUIDL = {
|
|
66
66
|
...uidl.root,
|
|
67
|
+
styleSetDefinitions: {},
|
|
68
|
+
propDefinitions: {},
|
|
67
69
|
outputOptions: {
|
|
68
70
|
fileName: 'index',
|
|
69
71
|
},
|
|
@@ -112,7 +114,7 @@ describe('Generic Project Generator', () => {
|
|
|
112
114
|
expect(generator.componentGenerator.generateComponent).toBeCalledTimes(4)
|
|
113
115
|
expect(generator.componentGenerator.generateComponent).toBeCalledWith(
|
|
114
116
|
expect.objectContaining({ name: 'ExpandableArea' }),
|
|
115
|
-
{
|
|
117
|
+
expect.objectContaining({
|
|
116
118
|
assets: {
|
|
117
119
|
identifier: null,
|
|
118
120
|
mappings: {},
|
|
@@ -122,17 +124,24 @@ describe('Generic Project Generator', () => {
|
|
|
122
124
|
},
|
|
123
125
|
designLanguage: undefined,
|
|
124
126
|
projectRouteDefinition: uidl.root.stateDefinitions.route,
|
|
125
|
-
|
|
127
|
+
extractedResources: {},
|
|
126
128
|
mapping: {},
|
|
127
129
|
skipValidation: true,
|
|
128
|
-
|
|
130
|
+
skipI18n: true,
|
|
131
|
+
projectComponents: expect.objectContaining({
|
|
132
|
+
ExpandableArea: expect.objectContaining({ name: 'ExpandableArea' }),
|
|
133
|
+
Modal: expect.objectContaining({ name: 'Modal' }),
|
|
134
|
+
ModalWindow: expect.objectContaining({ name: 'ModalWindow' }),
|
|
135
|
+
OneComponent: expect.objectContaining({ name: 'OneComponent' }),
|
|
136
|
+
}),
|
|
137
|
+
})
|
|
129
138
|
)
|
|
130
139
|
expect(generator.pageGenerator.generateComponent).toBeCalledTimes(3)
|
|
131
140
|
expect(generator.pageGenerator.generateComponent).toBeCalledWith(
|
|
132
141
|
expect.objectContaining({
|
|
133
142
|
name: 'Home',
|
|
134
143
|
}),
|
|
135
|
-
{
|
|
144
|
+
expect.objectContaining({
|
|
136
145
|
assets: {
|
|
137
146
|
identifier: null,
|
|
138
147
|
mappings: {},
|
|
@@ -142,14 +151,23 @@ describe('Generic Project Generator', () => {
|
|
|
142
151
|
},
|
|
143
152
|
designLanguage: undefined,
|
|
144
153
|
projectRouteDefinition: uidl.root.stateDefinitions.route,
|
|
145
|
-
|
|
154
|
+
extractedResources: {},
|
|
146
155
|
mapping: {},
|
|
147
156
|
skipValidation: true,
|
|
148
|
-
|
|
157
|
+
skipI18n: true,
|
|
158
|
+
projectComponents: expect.objectContaining({
|
|
159
|
+
ExpandableArea: expect.objectContaining({ name: 'ExpandableArea' }),
|
|
160
|
+
Modal: expect.objectContaining({ name: 'Modal' }),
|
|
161
|
+
ModalWindow: expect.objectContaining({ name: 'ModalWindow' }),
|
|
162
|
+
OneComponent: expect.objectContaining({ name: 'OneComponent' }),
|
|
163
|
+
}),
|
|
164
|
+
})
|
|
149
165
|
)
|
|
150
166
|
|
|
151
167
|
const routerUIDL = {
|
|
152
168
|
...uidl.root,
|
|
169
|
+
styleSetDefinitions: {},
|
|
170
|
+
propDefinitions: {},
|
|
153
171
|
outputOptions: {
|
|
154
172
|
fileName: strategy.router.fileName,
|
|
155
173
|
},
|
|
@@ -187,7 +205,7 @@ describe('Generic Project Generator', () => {
|
|
|
187
205
|
expect(generator.pageGenerator.generateComponent).toBeCalledTimes(3)
|
|
188
206
|
expect(generator.componentGenerator.generateComponent).toBeCalledWith(
|
|
189
207
|
expect.objectContaining({ name: 'ExpandableArea' }),
|
|
190
|
-
{
|
|
208
|
+
expect.objectContaining({
|
|
191
209
|
assets: {
|
|
192
210
|
identifier: null,
|
|
193
211
|
mappings: {},
|
|
@@ -195,18 +213,27 @@ describe('Generic Project Generator', () => {
|
|
|
195
213
|
fontsFolder: 'test/static/fonts',
|
|
196
214
|
localFonts: [],
|
|
197
215
|
},
|
|
198
|
-
|
|
216
|
+
extractedResources: {},
|
|
199
217
|
designLanguage: undefined,
|
|
200
218
|
projectRouteDefinition: uidl.root.stateDefinitions.route,
|
|
201
219
|
mapping: {},
|
|
202
220
|
skipValidation: true,
|
|
203
|
-
|
|
221
|
+
skipI18n: true,
|
|
222
|
+
projectComponents: expect.objectContaining({
|
|
223
|
+
ExpandableArea: expect.objectContaining({ name: 'ExpandableArea' }),
|
|
224
|
+
Modal: expect.objectContaining({ name: 'Modal' }),
|
|
225
|
+
ModalWindow: expect.objectContaining({ name: 'ModalWindow' }),
|
|
226
|
+
OneComponent: expect.objectContaining({ name: 'OneComponent' }),
|
|
227
|
+
}),
|
|
228
|
+
})
|
|
204
229
|
)
|
|
205
230
|
|
|
206
231
|
expect(generator.routerGenerator.generateComponent).toBeCalledTimes(1)
|
|
207
232
|
|
|
208
233
|
const routerUIDL = {
|
|
209
234
|
...uidl.root,
|
|
235
|
+
styleSetDefinitions: {},
|
|
236
|
+
propDefinitions: {},
|
|
210
237
|
outputOptions: {
|
|
211
238
|
fileName: 'index',
|
|
212
239
|
},
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { UIDLResourceItem } from '@teleporthq/teleport-types'
|
|
2
|
+
import * as types from '@babel/types'
|
|
3
|
+
import generate from '@babel/generator'
|
|
4
|
+
|
|
5
|
+
import { resourceGenerator } from '../src/resource'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A data source resource is generated as a direct `fetchData` call, and the
|
|
9
|
+
* params it declares serve two different purposes: `dataSourceId`,
|
|
10
|
+
* `dataSourceType` and `tableName` pick the utility module at generation time,
|
|
11
|
+
* while anything else is a value the handler reads out of `req.query`.
|
|
12
|
+
*
|
|
13
|
+
* Forwarding only the caller's arguments dropped the second kind. For a details
|
|
14
|
+
* page over a REST endpoint that wraps its list, that meant the handler never
|
|
15
|
+
* learned where the records were: `getStaticPaths` produced no paths and every
|
|
16
|
+
* `/page/<slug>` answered 404, with nothing in the output to point at why.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const staticParam = (content: string) => ({ type: 'static' as const, content })
|
|
20
|
+
|
|
21
|
+
const dataSourceResource = (params: Record<string, unknown> = {}): UIDLResourceItem =>
|
|
22
|
+
({
|
|
23
|
+
id: 'TQ_resource',
|
|
24
|
+
name: 'fetch_data_detail',
|
|
25
|
+
path: {
|
|
26
|
+
baseUrl: staticParam('/api/data-source'),
|
|
27
|
+
route: staticParam('/92c59975/data'),
|
|
28
|
+
},
|
|
29
|
+
params: {
|
|
30
|
+
dataSourceId: staticParam('92c59975-43bc-47aa-99fd-114912138d5b'),
|
|
31
|
+
dataSourceType: staticParam('rest-api'),
|
|
32
|
+
tableName: staticParam('data'),
|
|
33
|
+
...params,
|
|
34
|
+
},
|
|
35
|
+
} as unknown as UIDLResourceItem)
|
|
36
|
+
|
|
37
|
+
const generatedCode = (resource: UIDLResourceItem): string => {
|
|
38
|
+
const { chunks } = resourceGenerator(resource, {})
|
|
39
|
+
return chunks
|
|
40
|
+
.map((chunk) => generate(chunk.content as types.Node).code)
|
|
41
|
+
.join('\n')
|
|
42
|
+
.replace(/\s+/g, ' ')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('data source resource params', () => {
|
|
46
|
+
it('forwards a runtime param the handler needs, so a wrapped list is unwrapped', () => {
|
|
47
|
+
const code = generatedCode(
|
|
48
|
+
dataSourceResource({ collectionPath: staticParam('["categories"]') })
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
expect(code).toContain('collectionPath: "[\\"categories\\"]"')
|
|
52
|
+
expect(code).toContain('...params')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("lets the caller's own argument win over the baked-in default", () => {
|
|
56
|
+
const code = generatedCode(
|
|
57
|
+
dataSourceResource({ collectionPath: staticParam('["categories"]') })
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
// The spread has to come LAST, or a resource default would silently
|
|
61
|
+
// override what the page passed at the call site.
|
|
62
|
+
expect(code.indexOf('...params')).toBeGreaterThan(code.indexOf('collectionPath'))
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('never forwards the params that already picked the module', () => {
|
|
66
|
+
const code = generatedCode(
|
|
67
|
+
dataSourceResource({ collectionPath: staticParam('["categories"]') })
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
expect(code).not.toContain('dataSourceId:')
|
|
71
|
+
expect(code).not.toContain('dataSourceType:')
|
|
72
|
+
expect(code).not.toContain('tableName:')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('passes params through untouched when there is no runtime param', () => {
|
|
76
|
+
// Every table-backed resource takes this path, and its generated code has
|
|
77
|
+
// to stay exactly what it was.
|
|
78
|
+
expect(generatedCode(dataSourceResource())).toContain('fetchData(params)')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('skips a dynamic param, which has no value at generation time', () => {
|
|
82
|
+
const code = generatedCode(
|
|
83
|
+
dataSourceResource({
|
|
84
|
+
collectionPath: { type: 'dynamic', content: { referenceType: 'prop', id: 'path' } },
|
|
85
|
+
})
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
expect(code).toContain('fetchData(params)')
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('structured resource params', () => {
|
|
93
|
+
it('JSON-encodes a param that is not a primitive rather than dropping it', () => {
|
|
94
|
+
// `req.query` carries everything structured as JSON already (`filters`,
|
|
95
|
+
// `sorts`, `collectionPath`), and the handlers parse it back. Skipping it
|
|
96
|
+
// would reintroduce the silent drop this whole path exists to prevent.
|
|
97
|
+
const code = generatedCode(
|
|
98
|
+
dataSourceResource({
|
|
99
|
+
collectionPath: { type: 'static', content: ['response', 'items'] },
|
|
100
|
+
} as never)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
expect(code).toContain('collectionPath: "[\\"response\\",\\"items\\"]"')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('skips a param with no value at all', () => {
|
|
107
|
+
const code = generatedCode(
|
|
108
|
+
dataSourceResource({ collectionPath: { type: 'static', content: null } } as never)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
expect(code).toContain('fetchData(params)')
|
|
112
|
+
})
|
|
113
|
+
})
|
package/__tests__/utils.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { injectFilesToPath, extractPageOptions, prepareComponentOutputOptions } from '../src/utils'
|
|
2
|
-
import { UIDLStateDefinition } from '@teleporthq/teleport-types'
|
|
2
|
+
import { UIDLRouteDefinitions, UIDLStateDefinition } from '@teleporthq/teleport-types'
|
|
3
3
|
import { emptyFolder, folderWithFiles, createStrategyWithCommonGenerator } from './mocks'
|
|
4
4
|
import { component, elementNode } from '@teleporthq/teleport-uidl-builders'
|
|
5
5
|
import { GenericUtils } from '@teleporthq/teleport-shared'
|
|
@@ -140,7 +140,7 @@ describe('injectFilesToPath', () => {
|
|
|
140
140
|
})
|
|
141
141
|
|
|
142
142
|
describe('extractPageOptions', () => {
|
|
143
|
-
const routeDefinitions:
|
|
143
|
+
const routeDefinitions: UIDLRouteDefinitions = {
|
|
144
144
|
type: 'string',
|
|
145
145
|
defaultValue: 'home',
|
|
146
146
|
values: [
|
|
@@ -203,6 +203,108 @@ describe('extractPageOptions', () => {
|
|
|
203
203
|
expect(pageOptions.fileName).toBe('about-us')
|
|
204
204
|
expect(pageOptions.componentName).toBe('AboutUs')
|
|
205
205
|
})
|
|
206
|
+
it('routes the home page to / even when it brings its own address', () => {
|
|
207
|
+
// A Help Center list made the home page arrives with the address it had (`/help`).
|
|
208
|
+
// It is written as `index`, so links to it must say `/`, not `/help`.
|
|
209
|
+
const homeWithAddress: UIDLRouteDefinitions = {
|
|
210
|
+
type: 'string',
|
|
211
|
+
defaultValue: 'help-articles-list',
|
|
212
|
+
values: [{ value: 'help-articles-list', pageOptions: { navLink: '/help' } }],
|
|
213
|
+
}
|
|
214
|
+
const { pageOptions, isHomePage } = extractPageOptions(
|
|
215
|
+
homeWithAddress,
|
|
216
|
+
'help-articles-list',
|
|
217
|
+
true
|
|
218
|
+
)
|
|
219
|
+
expect(isHomePage).toBe(true)
|
|
220
|
+
expect(pageOptions.navLink).toBe('/')
|
|
221
|
+
expect(pageOptions.fileName).toBe('index')
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Two admin edit pages — one over the platform `teleport_product_reviews`
|
|
227
|
+
* table, one over an author-created `product_reviews` — both arrived carrying
|
|
228
|
+
* `navLink: '/admin/product-reviews/update/[id]'`. The dedupe suffixed the
|
|
229
|
+
* route as a plain string, the second page became `.../[id]1`, and because a
|
|
230
|
+
* segment is a route parameter only when the brackets span all of it, Next
|
|
231
|
+
* refused `getStaticPaths` on it and the production build died in Vercel.
|
|
232
|
+
*/
|
|
233
|
+
describe('extractPageOptions — duplicate dynamic routes', () => {
|
|
234
|
+
const duplicateUpdateRoutes: UIDLRouteDefinitions = {
|
|
235
|
+
type: 'string',
|
|
236
|
+
defaultValue: 'home',
|
|
237
|
+
values: [
|
|
238
|
+
{ value: 'home', pageOptions: { navLink: '/' } },
|
|
239
|
+
{
|
|
240
|
+
value: 'admin/product-reviews/update/product-reviews-update',
|
|
241
|
+
pageOptions: {
|
|
242
|
+
navLink: '/admin/product-reviews/update/[id]',
|
|
243
|
+
dynamicRouteAttribute: 'id',
|
|
244
|
+
componentName: 'ProductReviewsUpdate',
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
value: 'admin/product-reviews/update/product-reviews-update1',
|
|
249
|
+
pageOptions: {
|
|
250
|
+
navLink: '/admin/product-reviews/update/[id]',
|
|
251
|
+
dynamicRouteAttribute: 'id',
|
|
252
|
+
componentName: 'ProductReviewsUpdate',
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* `createPageUIDL` writes each resolved `pageOptions` back into the route
|
|
260
|
+
* definition before moving to the next page, so the pages resolve in order
|
|
261
|
+
* and only one of the two can move. Replayed here so the assertion is about
|
|
262
|
+
* the pair of routes the project actually ships, not one call in isolation.
|
|
263
|
+
*/
|
|
264
|
+
const resolveEveryPage = (routes: UIDLRouteDefinitions) =>
|
|
265
|
+
routes.values
|
|
266
|
+
.filter((route) => route.pageOptions)
|
|
267
|
+
.map((route) => {
|
|
268
|
+
const { pageOptions } = extractPageOptions(routes, route.value.toString(), true)
|
|
269
|
+
route.pageOptions = pageOptions
|
|
270
|
+
return pageOptions
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('moves the collision onto the static part and keeps the route parameter', () => {
|
|
274
|
+
const [, first, second] = resolveEveryPage(duplicateUpdateRoutes)
|
|
275
|
+
|
|
276
|
+
expect([first.navLink, second.navLink].sort()).toEqual([
|
|
277
|
+
'/admin/product-reviews/update/[id]',
|
|
278
|
+
'/admin/product-reviews/update1/[id]',
|
|
279
|
+
])
|
|
280
|
+
// Both stay real dynamic pages: Next writes
|
|
281
|
+
// `pages/admin/product-reviews/update/[id].js` and `.../update1/[id].js`,
|
|
282
|
+
// and `getStaticPaths` is legal on each.
|
|
283
|
+
expect(first.fileName).toBe('[id]')
|
|
284
|
+
expect(second.fileName).toBe('[id]')
|
|
285
|
+
expect(first.dynamicRouteAttribute).toBe('id')
|
|
286
|
+
expect(second.dynamicRouteAttribute).toBe('id')
|
|
287
|
+
expect([first.componentName, second.componentName].sort()).toEqual([
|
|
288
|
+
'ProductReviewsUpdate',
|
|
289
|
+
'ProductReviewsUpdate1',
|
|
290
|
+
])
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('still suffixes duplicate STATIC routes the way it always did', () => {
|
|
294
|
+
const duplicateListRoutes: UIDLRouteDefinitions = {
|
|
295
|
+
type: 'string',
|
|
296
|
+
defaultValue: 'home',
|
|
297
|
+
values: [
|
|
298
|
+
{ value: 'home', pageOptions: { navLink: '/' } },
|
|
299
|
+
{ value: 'product-reviews-list', pageOptions: { navLink: '/product-reviews-list' } },
|
|
300
|
+
{ value: 'product-reviews-list1', pageOptions: { navLink: '/product-reviews-list' } },
|
|
301
|
+
],
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const { pageOptions } = extractPageOptions(duplicateListRoutes, 'product-reviews-list1', true)
|
|
305
|
+
expect(pageOptions.navLink).toBe('/product-reviews-list1')
|
|
306
|
+
expect(pageOptions.fileName).toBe('product-reviews-list1')
|
|
307
|
+
})
|
|
206
308
|
})
|
|
207
309
|
|
|
208
310
|
describe('prepareComponentOutputOptions', () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-handlers.d.ts","sourceRoot":"","sources":["../../src/file-handlers.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"file-handlers.d.ts","sourceRoot":"","sources":["../../src/file-handlers.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,aAAa,EACb,eAAe,EACf,WAAW,EAGX,aAAa,EACb,gBAAgB,EAChB,eAAe,EAMf,kBAAkB,EACnB,MAAM,4BAA4B,CAAA;AAMnC,eAAO,MAAM,UAAU,aACX,aAAa,aACZ,kBAAkB,WACpB,gBAAgB,oEAG1B,CAAA;AAED,eAAO,MAAM,eAAe,kBACX,aAAa,aACjB,kBAAkB,WACpB,gBAAgB,oEAG1B,CAAA;AAED,eAAO,MAAM,qBAAqB,SAC1B,WAAW,YACP,eAAe,aACd,kBAAkB,oEAoB9B,CAAA;AAED,eAAO,MAAM,gBAAgB,aACjB,aAAa,aACZ,kBAAkB,WACpB,gBAAgB,oEAU1B,CAAA;AAED,eAAO,MAAM,gBAAgB,SACrB,aAAa,YACT,eAAe,mBACR,kBAAkB;;;EAoCpC,CAAA;AAED,eAAO,MAAM,eAAe,SACpB,WAAW,YACP,eAAe,oBACP,gBAAgB,6BA2BnC,CAAA;AA0OD,eAAO,MAAM,cAAc,QAAS,OAAO,MAAM,EAAE,MAAM,CAAC;;;;GAqBzD,CAAA;AAED,eAAO,MAAM,mBAAmB;;;;CAM/B,CAAA;AAGD,eAAO,MAAM,sBAAsB,SAC3B,WAAW,UACT,gBAAgB,CAAC,QAAQ,CAAC,KACjC,aA0BF,CAAA;AAED,eAAO,MAAM,iBAAiB,aAClB,eAAe,QACnB,WAAW,gBACH,OAAO,MAAM,EAAE,MAAM,CAAC,oBAClB,OAAO,MAAM,EAAE,MAAM,CAAC,SAmCzC,CAAA"}
|
|
@@ -76,6 +76,7 @@ var createComponentModule = function (uidl, strategy, generator) { return __awai
|
|
|
76
76
|
localDependenciesPrefix: componentLocalDependenciesPrefix,
|
|
77
77
|
strategy: strategy,
|
|
78
78
|
moduleComponents: uidl.components,
|
|
79
|
+
extractedResources: {},
|
|
79
80
|
};
|
|
80
81
|
root.outputOptions = root.outputOptions || {};
|
|
81
82
|
root.outputOptions.fileName = 'components.module';
|
|
@@ -106,6 +107,7 @@ var createRouterFile = function (root, strategy, routerGenerator) { return __awa
|
|
|
106
107
|
strategy: strategy,
|
|
107
108
|
isRootComponent: true,
|
|
108
109
|
designLanguage: root === null || root === void 0 ? void 0 : root.designLanguage,
|
|
110
|
+
extractedResources: {},
|
|
109
111
|
};
|
|
110
112
|
if (projectStyleSheet) {
|
|
111
113
|
options = __assign(__assign({}, options), { projectStyleSet: {
|
|
@@ -162,6 +164,12 @@ var createHTMLEntryFileChunks = function (uidl, options) {
|
|
|
162
164
|
var bodyNode = teleport_plugin_common_1.HASTBuilders.createHTMLNode('body');
|
|
163
165
|
teleport_plugin_common_1.HASTUtils.addChildNode(htmlNode, headNode);
|
|
164
166
|
teleport_plugin_common_1.HASTUtils.addChildNode(htmlNode, bodyNode);
|
|
167
|
+
// First in <head>, ahead of every script it guards.
|
|
168
|
+
if (teleport_shared_1.InlineScriptErrorGuard.documentRunsProjectScripts(uidl.globals)) {
|
|
169
|
+
var guardTag = teleport_plugin_common_1.HASTBuilders.createHTMLNode('script');
|
|
170
|
+
teleport_plugin_common_1.HASTUtils.addTextNode(guardTag, teleport_shared_1.InlineScriptErrorGuard.INLINE_SCRIPT_ERROR_GUARD);
|
|
171
|
+
teleport_plugin_common_1.HASTUtils.addChildNode(headNode, guardTag);
|
|
172
|
+
}
|
|
165
173
|
// Vue and React use a standard <div id="app"/> in the body tag.
|
|
166
174
|
// Nuxt has an internal templating so requires an override
|
|
167
175
|
if (appRootOverride) {
|
|
@@ -218,6 +226,20 @@ var createHTMLEntryFileChunks = function (uidl, options) {
|
|
|
218
226
|
});
|
|
219
227
|
teleport_plugin_common_1.HASTUtils.addChildNode(headNode, metaTag);
|
|
220
228
|
});
|
|
229
|
+
// Preconnect hints first: they only help if the browser sees them BEFORE the
|
|
230
|
+
// stylesheet link that triggers the two-hop font fetch. Mirrors the JSX head
|
|
231
|
+
// builder in teleport-plugin-common, and shares its resolver.
|
|
232
|
+
teleport_shared_1.FontPreconnect.resolveFontPreconnectHints(assets.map(function (asset) {
|
|
233
|
+
return 'path' in asset ? teleport_shared_1.UIDLUtils.prefixAssetsPath(asset.path, options.assets) : undefined;
|
|
234
|
+
})).forEach(function (hint) {
|
|
235
|
+
var preconnectTag = teleport_plugin_common_1.HASTBuilders.createHTMLNode('link');
|
|
236
|
+
teleport_plugin_common_1.HASTUtils.addAttributeToNode(preconnectTag, 'rel', 'preconnect');
|
|
237
|
+
teleport_plugin_common_1.HASTUtils.addAttributeToNode(preconnectTag, 'href', hint.href);
|
|
238
|
+
if (hint.crossorigin) {
|
|
239
|
+
teleport_plugin_common_1.HASTUtils.addBooleanAttributeToNode(preconnectTag, 'crossorigin');
|
|
240
|
+
}
|
|
241
|
+
teleport_plugin_common_1.HASTUtils.addChildNode(headNode, preconnectTag);
|
|
242
|
+
});
|
|
221
243
|
assets.forEach(function (asset) {
|
|
222
244
|
var assetPath;
|
|
223
245
|
if ('path' in asset) {
|