@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.
Files changed (41) hide show
  1. package/__tests__/content-settings-propagation.ts +53 -0
  2. package/__tests__/details-page-info-propagation.ts +111 -0
  3. package/__tests__/file-handlers.ts +34 -0
  4. package/__tests__/index.ts +37 -10
  5. package/__tests__/resource-runtime-params.ts +113 -0
  6. package/__tests__/utils.ts +104 -2
  7. package/dist/cjs/file-handlers.d.ts.map +1 -1
  8. package/dist/cjs/file-handlers.js +22 -0
  9. package/dist/cjs/file-handlers.js.map +1 -1
  10. package/dist/cjs/index.d.ts +1 -1
  11. package/dist/cjs/index.d.ts.map +1 -1
  12. package/dist/cjs/index.js +226 -104
  13. package/dist/cjs/index.js.map +1 -1
  14. package/dist/cjs/resource.d.ts.map +1 -1
  15. package/dist/cjs/resource.js +150 -0
  16. package/dist/cjs/resource.js.map +1 -1
  17. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  18. package/dist/cjs/utils.d.ts +17 -1
  19. package/dist/cjs/utils.d.ts.map +1 -1
  20. package/dist/cjs/utils.js +66 -10
  21. package/dist/cjs/utils.js.map +1 -1
  22. package/dist/esm/file-handlers.d.ts.map +1 -1
  23. package/dist/esm/file-handlers.js +23 -1
  24. package/dist/esm/file-handlers.js.map +1 -1
  25. package/dist/esm/index.d.ts +1 -1
  26. package/dist/esm/index.d.ts.map +1 -1
  27. package/dist/esm/index.js +227 -105
  28. package/dist/esm/index.js.map +1 -1
  29. package/dist/esm/resource.d.ts.map +1 -1
  30. package/dist/esm/resource.js +150 -0
  31. package/dist/esm/resource.js.map +1 -1
  32. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  33. package/dist/esm/utils.d.ts +17 -1
  34. package/dist/esm/utils.d.ts.map +1 -1
  35. package/dist/esm/utils.js +65 -10
  36. package/dist/esm/utils.js.map +1 -1
  37. package/package.json +9 -9
  38. package/src/file-handlers.ts +33 -1
  39. package/src/index.ts +266 -110
  40. package/src/resource.ts +192 -0
  41. 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', () => {
@@ -56,7 +56,7 @@ describe('Generic Project Generator', () => {
56
56
  },
57
57
  designLanguage: undefined,
58
58
  mapping: {},
59
- projectResources: {},
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
- projectResources: {},
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
- projectResources: {},
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
- projectResources: {},
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
+ })
@@ -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: UIDLStateDefinition = {
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":"AAEA,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,oEAmB9B,CAAA;AAED,eAAO,MAAM,gBAAgB,aACjB,aAAa,aACZ,kBAAkB,WACpB,gBAAgB,oEAU1B,CAAA;AAED,eAAO,MAAM,gBAAgB,SACrB,aAAa,YACT,eAAe,mBACR,kBAAkB;;;EAmCpC,CAAA;AAED,eAAO,MAAM,eAAe,SACpB,WAAW,YACP,eAAe,oBACP,gBAAgB,6BA2BnC,CAAA;AAkND,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"}
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) {