@teleporthq/teleport-project-generator-next 0.43.13 → 0.43.15

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 (147) hide show
  1. package/__tests__/analytics-project-plugin.test.ts +132 -0
  2. package/__tests__/calendarkit-end2end.test.ts +98 -0
  3. package/__tests__/calendarkit-project-plugin.test.ts +99 -0
  4. package/__tests__/dragdrop-kanban-end2end.test.ts +176 -0
  5. package/__tests__/dragdrop-kanban-project-plugins.test.ts +144 -0
  6. package/__tests__/global-state-default-value.test.ts +69 -0
  7. package/dist/cjs/analytics/project-plugin.d.ts +8 -0
  8. package/dist/cjs/analytics/project-plugin.d.ts.map +1 -0
  9. package/dist/cjs/analytics/project-plugin.js +178 -0
  10. package/dist/cjs/analytics/project-plugin.js.map +1 -0
  11. package/dist/cjs/analytics/tracker-component.d.ts +2 -0
  12. package/dist/cjs/analytics/tracker-component.d.ts.map +1 -0
  13. package/dist/cjs/analytics/tracker-component.js +8 -0
  14. package/dist/cjs/analytics/tracker-component.js.map +1 -0
  15. package/dist/cjs/analytics/tracker-source.d.ts +2 -0
  16. package/dist/cjs/analytics/tracker-source.d.ts.map +1 -0
  17. package/dist/cjs/analytics/tracker-source.js +15 -0
  18. package/dist/cjs/analytics/tracker-source.js.map +1 -0
  19. package/dist/cjs/app-import-injection.d.ts +11 -0
  20. package/dist/cjs/app-import-injection.d.ts.map +1 -0
  21. package/dist/cjs/app-import-injection.js +42 -0
  22. package/dist/cjs/app-import-injection.js.map +1 -0
  23. package/dist/cjs/calendar/calendarkit-css.d.ts +3 -0
  24. package/dist/cjs/calendar/calendarkit-css.d.ts.map +1 -0
  25. package/dist/cjs/calendar/calendarkit-css.js +9 -0
  26. package/dist/cjs/calendar/calendarkit-css.js.map +1 -0
  27. package/dist/cjs/calendar/project-plugin.d.ts +16 -0
  28. package/dist/cjs/calendar/project-plugin.d.ts.map +1 -0
  29. package/dist/cjs/calendar/project-plugin.js +93 -0
  30. package/dist/cjs/calendar/project-plugin.js.map +1 -0
  31. package/dist/cjs/drag-drop/component-generator.d.ts +21 -0
  32. package/dist/cjs/drag-drop/component-generator.d.ts.map +1 -0
  33. package/dist/cjs/drag-drop/component-generator.js +27 -0
  34. package/dist/cjs/drag-drop/component-generator.js.map +1 -0
  35. package/dist/cjs/drag-drop/project-plugin.d.ts +15 -0
  36. package/dist/cjs/drag-drop/project-plugin.d.ts.map +1 -0
  37. package/dist/cjs/drag-drop/project-plugin.js +96 -0
  38. package/dist/cjs/drag-drop/project-plugin.js.map +1 -0
  39. package/dist/cjs/global-state/project-plugin.d.ts.map +1 -1
  40. package/dist/cjs/global-state/project-plugin.js +16 -6
  41. package/dist/cjs/global-state/project-plugin.js.map +1 -1
  42. package/dist/cjs/index.d.ts +8 -0
  43. package/dist/cjs/index.d.ts.map +1 -1
  44. package/dist/cjs/index.js +54 -13
  45. package/dist/cjs/index.js.map +1 -1
  46. package/dist/cjs/kanban/component-generator.d.ts +15 -0
  47. package/dist/cjs/kanban/component-generator.d.ts.map +1 -0
  48. package/dist/cjs/kanban/component-generator.js +21 -0
  49. package/dist/cjs/kanban/component-generator.js.map +1 -0
  50. package/dist/cjs/kanban/project-plugin.d.ts +13 -0
  51. package/dist/cjs/kanban/project-plugin.d.ts.map +1 -0
  52. package/dist/cjs/kanban/project-plugin.js +105 -0
  53. package/dist/cjs/kanban/project-plugin.js.map +1 -0
  54. package/dist/cjs/local-component-path-plugin.d.ts +28 -0
  55. package/dist/cjs/local-component-path-plugin.d.ts.map +1 -0
  56. package/dist/cjs/local-component-path-plugin.js +93 -0
  57. package/dist/cjs/local-component-path-plugin.js.map +1 -0
  58. package/dist/cjs/next-project-mapping.d.ts.map +1 -1
  59. package/dist/cjs/next-project-mapping.js +57 -0
  60. package/dist/cjs/next-project-mapping.js.map +1 -1
  61. package/dist/cjs/rich-text-editor/project-plugin.d.ts.map +1 -1
  62. package/dist/cjs/rich-text-editor/project-plugin.js +2 -106
  63. package/dist/cjs/rich-text-editor/project-plugin.js.map +1 -1
  64. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  65. package/dist/cjs/uidl-element-traversal.d.ts +18 -0
  66. package/dist/cjs/uidl-element-traversal.d.ts.map +1 -0
  67. package/dist/cjs/uidl-element-traversal.js +131 -0
  68. package/dist/cjs/uidl-element-traversal.js.map +1 -0
  69. package/dist/esm/analytics/project-plugin.d.ts +8 -0
  70. package/dist/esm/analytics/project-plugin.d.ts.map +1 -0
  71. package/dist/esm/analytics/project-plugin.js +175 -0
  72. package/dist/esm/analytics/project-plugin.js.map +1 -0
  73. package/dist/esm/analytics/tracker-component.d.ts +2 -0
  74. package/dist/esm/analytics/tracker-component.d.ts.map +1 -0
  75. package/dist/esm/analytics/tracker-component.js +5 -0
  76. package/dist/esm/analytics/tracker-component.js.map +1 -0
  77. package/dist/esm/analytics/tracker-source.d.ts +2 -0
  78. package/dist/esm/analytics/tracker-source.d.ts.map +1 -0
  79. package/dist/esm/analytics/tracker-source.js +12 -0
  80. package/dist/esm/analytics/tracker-source.js.map +1 -0
  81. package/dist/esm/app-import-injection.d.ts +11 -0
  82. package/dist/esm/app-import-injection.d.ts.map +1 -0
  83. package/dist/esm/app-import-injection.js +38 -0
  84. package/dist/esm/app-import-injection.js.map +1 -0
  85. package/dist/esm/calendar/calendarkit-css.d.ts +3 -0
  86. package/dist/esm/calendar/calendarkit-css.d.ts.map +1 -0
  87. package/dist/esm/calendar/calendarkit-css.js +6 -0
  88. package/dist/esm/calendar/calendarkit-css.js.map +1 -0
  89. package/dist/esm/calendar/project-plugin.d.ts +16 -0
  90. package/dist/esm/calendar/project-plugin.d.ts.map +1 -0
  91. package/dist/esm/calendar/project-plugin.js +90 -0
  92. package/dist/esm/calendar/project-plugin.js.map +1 -0
  93. package/dist/esm/drag-drop/component-generator.d.ts +21 -0
  94. package/dist/esm/drag-drop/component-generator.d.ts.map +1 -0
  95. package/dist/esm/drag-drop/component-generator.js +23 -0
  96. package/dist/esm/drag-drop/component-generator.js.map +1 -0
  97. package/dist/esm/drag-drop/project-plugin.d.ts +15 -0
  98. package/dist/esm/drag-drop/project-plugin.d.ts.map +1 -0
  99. package/dist/esm/drag-drop/project-plugin.js +93 -0
  100. package/dist/esm/drag-drop/project-plugin.js.map +1 -0
  101. package/dist/esm/global-state/project-plugin.d.ts.map +1 -1
  102. package/dist/esm/global-state/project-plugin.js +16 -6
  103. package/dist/esm/global-state/project-plugin.js.map +1 -1
  104. package/dist/esm/index.d.ts +8 -0
  105. package/dist/esm/index.d.ts.map +1 -1
  106. package/dist/esm/index.js +31 -0
  107. package/dist/esm/index.js.map +1 -1
  108. package/dist/esm/kanban/component-generator.d.ts +15 -0
  109. package/dist/esm/kanban/component-generator.d.ts.map +1 -0
  110. package/dist/esm/kanban/component-generator.js +17 -0
  111. package/dist/esm/kanban/component-generator.js.map +1 -0
  112. package/dist/esm/kanban/project-plugin.d.ts +13 -0
  113. package/dist/esm/kanban/project-plugin.d.ts.map +1 -0
  114. package/dist/esm/kanban/project-plugin.js +102 -0
  115. package/dist/esm/kanban/project-plugin.js.map +1 -0
  116. package/dist/esm/local-component-path-plugin.d.ts +28 -0
  117. package/dist/esm/local-component-path-plugin.d.ts.map +1 -0
  118. package/dist/esm/local-component-path-plugin.js +89 -0
  119. package/dist/esm/local-component-path-plugin.js.map +1 -0
  120. package/dist/esm/next-project-mapping.d.ts.map +1 -1
  121. package/dist/esm/next-project-mapping.js +57 -0
  122. package/dist/esm/next-project-mapping.js.map +1 -1
  123. package/dist/esm/rich-text-editor/project-plugin.d.ts.map +1 -1
  124. package/dist/esm/rich-text-editor/project-plugin.js +2 -106
  125. package/dist/esm/rich-text-editor/project-plugin.js.map +1 -1
  126. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  127. package/dist/esm/uidl-element-traversal.d.ts +18 -0
  128. package/dist/esm/uidl-element-traversal.d.ts.map +1 -0
  129. package/dist/esm/uidl-element-traversal.js +125 -0
  130. package/dist/esm/uidl-element-traversal.js.map +1 -0
  131. package/package.json +19 -19
  132. package/src/analytics/project-plugin.ts +145 -0
  133. package/src/analytics/tracker-component.ts +35 -0
  134. package/src/analytics/tracker-source.ts +430 -0
  135. package/src/app-import-injection.ts +46 -0
  136. package/src/calendar/calendarkit-css.ts +8 -0
  137. package/src/calendar/project-plugin.ts +48 -0
  138. package/src/drag-drop/component-generator.ts +245 -0
  139. package/src/drag-drop/project-plugin.ts +50 -0
  140. package/src/global-state/project-plugin.ts +15 -7
  141. package/src/index.ts +37 -0
  142. package/src/kanban/component-generator.ts +77 -0
  143. package/src/kanban/project-plugin.ts +63 -0
  144. package/src/local-component-path-plugin.ts +63 -0
  145. package/src/next-project-mapping.ts +57 -0
  146. package/src/rich-text-editor/project-plugin.ts +2 -113
  147. package/src/uidl-element-traversal.ts +141 -0
@@ -0,0 +1,132 @@
1
+ import { parse } from '@babel/parser'
2
+ import { FileType, ProjectPluginStructure } from '@teleporthq/teleport-types'
3
+ import { NextAnalyticsProjectPlugin } from '../src/analytics/project-plugin'
4
+
5
+ const APP_CONTENT = `import { GlobalProvider } from '../global-context'
6
+
7
+ const MyApp = ({ Component, pageProps }) => {
8
+ return (
9
+ <GlobalProvider>
10
+ <Component {...pageProps} />
11
+ </GlobalProvider>
12
+ )
13
+ }
14
+
15
+ export default MyApp
16
+ `
17
+
18
+ function makeStructure(analyticsEnabled: boolean): ProjectPluginStructure {
19
+ const files = new Map()
20
+ files.set('_app', {
21
+ path: ['pages'],
22
+ files: [{ name: '_app', fileType: FileType.JS, content: APP_CONTENT }],
23
+ })
24
+
25
+ return {
26
+ uidl: {
27
+ name: 'test-project',
28
+ globals: { settings: { title: 'Test', language: 'en' }, assets: [] },
29
+ root: {} as never,
30
+ ...(analyticsEnabled ? { analytics: { enabled: true } } : {}),
31
+ },
32
+ files,
33
+ dependencies: {},
34
+ devDependencies: {},
35
+ template: { files: [], subFolders: [] },
36
+ } as unknown as ProjectPluginStructure
37
+ }
38
+
39
+ describe('NextAnalyticsProjectPlugin', () => {
40
+ it('does nothing when analytics is not enabled', async () => {
41
+ const plugin = new NextAnalyticsProjectPlugin()
42
+ const structure = makeStructure(false)
43
+
44
+ await plugin.runAfter(structure)
45
+
46
+ expect(structure.files.has('teleport-analytics-lib')).toBe(false)
47
+ expect(structure.files.has('teleport-analytics-tracker')).toBe(false)
48
+
49
+ const appFile = structure.files.get('_app').files[0]
50
+ expect(appFile.content).not.toContain('AnalyticsTracker')
51
+ })
52
+
53
+ it('emits the tracker lib + component and wires the env placeholders', async () => {
54
+ const plugin = new NextAnalyticsProjectPlugin()
55
+ const structure = makeStructure(true)
56
+
57
+ await plugin.runAfter(structure)
58
+
59
+ const lib = structure.files.get('teleport-analytics-lib')
60
+ expect(lib.path).toEqual(['lib'])
61
+ expect(lib.files[0].name).toBe('teleport-analytics')
62
+ expect(lib.files[0].content).toContain('NEXT_PUBLIC_TELEPORT_ANALYTICS_URL')
63
+ expect(lib.files[0].content).toContain('initTeleportAnalytics')
64
+ expect(lib.files[0].content).toContain('sendBeacon')
65
+ expect(lib.files[0].content).toContain("localStorage.getItem('cookieConsent')")
66
+
67
+ // Beacons/batches must use the CORS-safelisted text/plain content type so
68
+ // unload sends are preflight-free and survive a closing tab.
69
+ expect(lib.files[0].content).toContain('text/plain;charset=UTF-8')
70
+ expect(lib.files[0].content).not.toContain("type: 'application/json'")
71
+ expect(lib.files[0].content).not.toContain("'Content-Type': 'application/json'")
72
+
73
+ const tracker = structure.files.get('teleport-analytics-tracker')
74
+ expect(tracker.path).toEqual(['components', 'analytics'])
75
+ expect(tracker.files[0].content).toContain('routeChangeComplete')
76
+
77
+ expect(structure.uidl.globals.env).toEqual({
78
+ NEXT_PUBLIC_TELEPORT_ANALYTICS_URL: 'teleporthq.secrets.NEXT_PUBLIC_TELEPORT_ANALYTICS_URL',
79
+ NEXT_PUBLIC_TELEPORT_ANALYTICS_KEY: 'teleporthq.secrets.NEXT_PUBLIC_TELEPORT_ANALYTICS_KEY',
80
+ })
81
+ })
82
+
83
+ it('injects <AnalyticsTracker /> into _app as a fragment sibling', async () => {
84
+ const plugin = new NextAnalyticsProjectPlugin()
85
+ const structure = makeStructure(true)
86
+
87
+ await plugin.runAfter(structure)
88
+
89
+ const appFile = structure.files.get('_app').files[0]
90
+ expect(appFile.content).toContain(
91
+ "import AnalyticsTracker from '../components/analytics/AnalyticsTracker'"
92
+ )
93
+ expect(appFile.content).toContain('<AnalyticsTracker /></>')
94
+ expect(appFile.content).toContain('<>')
95
+ // Idempotent: a second run must not double-inject
96
+ await plugin.runAfter(structure)
97
+ const occurrences = appFile.content.split('<AnalyticsTracker />').length - 1
98
+ expect(occurrences).toBe(1)
99
+ })
100
+
101
+ it('produces a still-parseable _app after the string surgery', async () => {
102
+ const plugin = new NextAnalyticsProjectPlugin()
103
+ const structure = makeStructure(true)
104
+
105
+ await plugin.runAfter(structure)
106
+
107
+ const appFile = structure.files.get('_app').files[0]
108
+ // The fragment wrap is raw string manipulation — guarantee it never emits
109
+ // invalid JSX that would silently blank the deployed app.
110
+ expect(() =>
111
+ parse(appFile.content, {
112
+ sourceType: 'module',
113
+ plugins: ['jsx'],
114
+ })
115
+ ).not.toThrow()
116
+ })
117
+
118
+ it('preserves env values already set by the GUI mapper', async () => {
119
+ const plugin = new NextAnalyticsProjectPlugin()
120
+ const structure = makeStructure(true)
121
+ structure.uidl.globals.env = {
122
+ NEXT_PUBLIC_TELEPORT_ANALYTICS_URL: 'teleporthq.secrets.NEXT_PUBLIC_TELEPORT_ANALYTICS_URL',
123
+ NEXT_PUBLIC_TELEPORT_ANALYTICS_KEY: 'teleporthq.secrets.NEXT_PUBLIC_TELEPORT_ANALYTICS_KEY',
124
+ OTHER_KEY: 'value',
125
+ }
126
+
127
+ await plugin.runAfter(structure)
128
+
129
+ expect(Object.keys(structure.uidl.globals.env)).toHaveLength(3)
130
+ expect(structure.uidl.globals.env.OTHER_KEY).toBe('value')
131
+ })
132
+ })
@@ -0,0 +1,98 @@
1
+ import { GeneratedFolder, ProjectUIDL } from '@teleporthq/teleport-types'
2
+ import uidlSample from '../../../examples/test-samples/project-sample.json'
3
+ import { createNextProjectGenerator } from '../src'
4
+ import NextTemplate from '../src/project-template'
5
+
6
+ // The production template (react ^17) — the same one the GUI publish path
7
+ // feeds into packProject, so the react bump is asserted against real deps.
8
+ const template = JSON.parse(JSON.stringify(NextTemplate)) as GeneratedFolder
9
+
10
+ const CALENDAR_ELEMENT_NODE = {
11
+ type: 'element',
12
+ content: {
13
+ elementType: 'BasicScheduler',
14
+ name: 'calendar',
15
+ dependency: {
16
+ type: 'package',
17
+ path: 'calendarkit-basic',
18
+ version: '1.1.0',
19
+ meta: { namedImport: true },
20
+ },
21
+ attrs: {
22
+ view: { type: 'static', content: 'month' },
23
+ weekStartsOn: { type: 'static', content: 1 },
24
+ events: {
25
+ type: 'static',
26
+ content: [
27
+ {
28
+ id: '1',
29
+ title: 'Team Meeting',
30
+ start: '2026-06-15T09:00:00',
31
+ end: '2026-06-15T10:30:00',
32
+ },
33
+ ],
34
+ },
35
+ },
36
+ children: [],
37
+ },
38
+ }
39
+
40
+ const buildUidlWithCalendar = (): ProjectUIDL => {
41
+ const uidl = JSON.parse(JSON.stringify(uidlSample)) as ProjectUIDL
42
+ const indexPage = (uidl.root.node.content.children || []).find(
43
+ (child) =>
44
+ child.type === 'conditional' && (child.content as { value?: string }).value === 'index'
45
+ )
46
+ const pageElement = (indexPage as { content: { node: { content: { children: unknown[] } } } })
47
+ .content.node.content
48
+ pageElement.children.push(CALENDAR_ELEMENT_NODE)
49
+ return uidl
50
+ }
51
+
52
+ const findFile = (folder: GeneratedFolder, folderName: string, fileName: string) =>
53
+ folder.subFolders
54
+ .find((sub) => sub.name === folderName)
55
+ ?.files.find((file) => file.name === fileName)
56
+
57
+ describe('Next generator with a CalendarKit calendar element', () => {
58
+ const generator = createNextProjectGenerator()
59
+
60
+ it('generates the calendar page, bumps react and ships the precompiled stylesheet', async () => {
61
+ const outputFolder = await generator.generateProject(buildUidlWithCalendar(), template)
62
+
63
+ const packageFile = outputFolder.files.find((file) => file.name === 'package')
64
+ const packageJson = JSON.parse(packageFile?.content || '{}')
65
+ expect(packageJson.dependencies['calendarkit-basic']).toBe('1.1.0')
66
+ expect(packageJson.dependencies.react).toBe('^18.3.1')
67
+ expect(packageJson.dependencies['react-dom']).toBe('^18.3.1')
68
+ expect(packageJson.dependencies.next).toBe('^12.1.10')
69
+
70
+ const indexPage = findFile(outputFolder, 'pages', 'index')
71
+ expect(indexPage?.content).toContain("import { BasicScheduler } from 'calendarkit-basic'")
72
+ expect(indexPage?.content).toContain('new Date(e.start)')
73
+ expect(indexPage?.content).toContain('new Date(e.end)')
74
+
75
+ const cssFile = findFile(outputFolder, 'pages', 'calendarkit')
76
+ expect(cssFile?.fileType).toBe('css')
77
+ expect(cssFile?.content).toContain(':root')
78
+ expect(cssFile?.content).not.toContain('@tailwind')
79
+
80
+ const appFile = findFile(outputFolder, 'pages', '_app')
81
+ expect(appFile?.content).toContain("import './calendarkit.css'")
82
+ })
83
+
84
+ it('leaves react untouched for projects without a calendar', async () => {
85
+ const outputFolder = await generator.generateProject(
86
+ JSON.parse(JSON.stringify(uidlSample)) as ProjectUIDL,
87
+ template
88
+ )
89
+
90
+ const packageFile = outputFolder.files.find((file) => file.name === 'package')
91
+ const packageJson = JSON.parse(packageFile?.content || '{}')
92
+ expect(packageJson.dependencies['calendarkit-basic']).toBeUndefined()
93
+ expect(packageJson.dependencies.react).toBe('^17.0.2')
94
+
95
+ const pagesFolder = outputFolder.subFolders.find((sub) => sub.name === 'pages')
96
+ expect(pagesFolder?.files.find((file) => file.name === 'calendarkit')).toBeUndefined()
97
+ })
98
+ })
@@ -0,0 +1,99 @@
1
+ import { FileType, InMemoryFileRecord, ProjectPluginStructure } from '@teleporthq/teleport-types'
2
+ import { NextCalendarKitProjectPlugin } from '../src/calendar/project-plugin'
3
+ import { CALENDARKIT_CSS } from '../src/calendar/calendarkit-css'
4
+
5
+ const APP_CONTENT = `import './style.css'
6
+
7
+ export default function MyApp({ Component, pageProps }) {
8
+ return <Component {...pageProps} />
9
+ }
10
+ `
11
+
12
+ const buildStructure = (
13
+ dependencies: Record<string, string>,
14
+ withAppFile = true
15
+ ): ProjectPluginStructure => {
16
+ const files = new Map<string, InMemoryFileRecord>()
17
+ if (withAppFile) {
18
+ files.set('_app', {
19
+ path: ['pages'],
20
+ files: [{ name: '_app', fileType: FileType.JS, content: APP_CONTENT }],
21
+ })
22
+ }
23
+
24
+ return {
25
+ files,
26
+ dependencies,
27
+ devDependencies: {},
28
+ } as unknown as ProjectPluginStructure
29
+ }
30
+
31
+ describe('NextCalendarKitProjectPlugin', () => {
32
+ const plugin = new NextCalendarKitProjectPlugin()
33
+
34
+ it('is a no-op for projects without calendarkit-basic', async () => {
35
+ const structure = buildStructure({ react: '^17.0.2' })
36
+
37
+ await plugin.runAfter(structure)
38
+
39
+ expect(structure.dependencies.react).toBe('^17.0.2')
40
+ expect(structure.files.has('calendarkit-css')).toBe(false)
41
+ const appFile = structure.files.get('_app')?.files[0]
42
+ expect(appFile?.content).toBe(APP_CONTENT)
43
+ })
44
+
45
+ it('bumps react/react-dom, writes the stylesheet and imports it from _app', async () => {
46
+ const structure = buildStructure({
47
+ react: '^17.0.2',
48
+ 'react-dom': '^17.0.2',
49
+ 'calendarkit-basic': '1.1.0',
50
+ })
51
+
52
+ await plugin.runAfter(structure)
53
+
54
+ expect(structure.dependencies.react).toBe('^18.3.1')
55
+ expect(structure.dependencies['react-dom']).toBe('^18.3.1')
56
+
57
+ const cssRecord = structure.files.get('calendarkit-css')
58
+ expect(cssRecord?.path).toEqual(['pages'])
59
+ expect(cssRecord?.files[0]).toEqual({
60
+ name: 'calendarkit',
61
+ fileType: FileType.CSS,
62
+ content: CALENDARKIT_CSS,
63
+ })
64
+
65
+ const appContent = structure.files.get('_app')?.files[0].content as string
66
+ expect(appContent).toContain("import './calendarkit.css'")
67
+ expect(appContent.indexOf("import './calendarkit.css'")).toBeLessThan(
68
+ appContent.indexOf("import './style.css'")
69
+ )
70
+ })
71
+
72
+ it('is idempotent when run twice', async () => {
73
+ const structure = buildStructure({ 'calendarkit-basic': '1.1.0' })
74
+
75
+ await plugin.runAfter(structure)
76
+ await plugin.runAfter(structure)
77
+
78
+ const appContent = structure.files.get('_app')?.files[0].content as string
79
+ expect(appContent.match(/import '\.\/calendarkit\.css'/g)).toHaveLength(1)
80
+ })
81
+
82
+ it('still adds the stylesheet when no _app file exists', async () => {
83
+ const structure = buildStructure({ 'calendarkit-basic': '1.1.0' }, false)
84
+
85
+ await plugin.runAfter(structure)
86
+
87
+ expect(structure.files.has('calendarkit-css')).toBe(true)
88
+ expect(structure.dependencies.react).toBe('^18.3.1')
89
+ })
90
+
91
+ it('ships CSS with no tailwind directives and no global resets', () => {
92
+ expect(CALENDARKIT_CSS).not.toContain('@tailwind')
93
+ expect(CALENDARKIT_CSS).not.toMatch(/(^|\})\*\{/)
94
+ expect(CALENDARKIT_CSS).not.toMatch(/(^|\})body\{/)
95
+ expect(CALENDARKIT_CSS).toContain(':root')
96
+ expect(CALENDARKIT_CSS).toContain('.dark')
97
+ expect(CALENDARKIT_CSS).toContain('grid-cols-7')
98
+ })
99
+ })
@@ -0,0 +1,176 @@
1
+ import { GeneratedFolder, ProjectUIDL } from '@teleporthq/teleport-types'
2
+ import uidlSample from '../../../examples/test-samples/project-sample.json'
3
+ import { createNextProjectGenerator } from '../src'
4
+ import NextTemplate from '../src/project-template'
5
+
6
+ // The production template (react ^17) — the same one the GUI publish path
7
+ // feeds into packProject.
8
+ const template = JSON.parse(JSON.stringify(NextTemplate)) as GeneratedFolder
9
+
10
+ const textChild = (content: string) => ({ type: 'static', content })
11
+
12
+ const DRAG_AREA_NODE = {
13
+ type: 'element',
14
+ content: {
15
+ elementType: 'thq-drag-area',
16
+ name: 'tasks-drag-area',
17
+ children: [
18
+ {
19
+ type: 'element',
20
+ content: {
21
+ elementType: 'thq-droppable',
22
+ name: 'todo-zone',
23
+ attrs: { dropId: { type: 'static', content: 'todo' } },
24
+ children: [
25
+ {
26
+ type: 'element',
27
+ content: {
28
+ elementType: 'thq-draggable',
29
+ name: 'task-card',
30
+ attrs: { dragId: { type: 'static', content: 'task-1' } },
31
+ children: [textChild('Write the report')],
32
+ },
33
+ },
34
+ ],
35
+ },
36
+ },
37
+ {
38
+ type: 'element',
39
+ content: {
40
+ elementType: 'thq-droppable',
41
+ name: 'done-zone',
42
+ attrs: { dropId: { type: 'static', content: 'done' } },
43
+ children: [],
44
+ },
45
+ },
46
+ ],
47
+ },
48
+ }
49
+
50
+ const SORTABLE_NODE = {
51
+ type: 'element',
52
+ content: {
53
+ elementType: 'thq-sortable',
54
+ name: 'priority-list',
55
+ attrs: { direction: { type: 'static', content: 'vertical' } },
56
+ children: [
57
+ {
58
+ type: 'element',
59
+ content: {
60
+ elementType: 'thq-sortable-item',
61
+ name: 'priority-item',
62
+ children: [textChild('First priority')],
63
+ },
64
+ },
65
+ {
66
+ type: 'element',
67
+ content: {
68
+ elementType: 'thq-sortable-item',
69
+ name: 'priority-item-2',
70
+ children: [textChild('Second priority')],
71
+ },
72
+ },
73
+ ],
74
+ },
75
+ }
76
+
77
+ const KANBAN_NODE = {
78
+ type: 'element',
79
+ content: {
80
+ elementType: 'kanban-node',
81
+ name: 'project-board',
82
+ attrs: {
83
+ board: {
84
+ type: 'static',
85
+ content: {
86
+ columns: [
87
+ {
88
+ id: 'todo',
89
+ title: 'To Do',
90
+ cards: [{ id: '1', title: 'Design review', description: 'Review mockups' }],
91
+ },
92
+ { id: 'done', title: 'Done', cards: [] },
93
+ ],
94
+ },
95
+ },
96
+ disableColumnDrag: { type: 'static', content: true },
97
+ },
98
+ children: [],
99
+ },
100
+ }
101
+
102
+ const buildUidl = (): ProjectUIDL => {
103
+ const uidl = JSON.parse(JSON.stringify(uidlSample)) as ProjectUIDL
104
+ const indexPage = (uidl.root.node.content.children || []).find(
105
+ (child) =>
106
+ child.type === 'conditional' && (child.content as { value?: string }).value === 'index'
107
+ )
108
+ const pageElement = (indexPage as { content: { node: { content: { children: unknown[] } } } })
109
+ .content.node.content
110
+ pageElement.children.push(DRAG_AREA_NODE, SORTABLE_NODE, KANBAN_NODE)
111
+ return uidl
112
+ }
113
+
114
+ const findFile = (folder: GeneratedFolder, folderName: string, fileName: string) =>
115
+ folder.subFolders
116
+ .find((sub) => sub.name === folderName)
117
+ ?.files.find((file) => file.name === fileName)
118
+
119
+ describe('Next generator with drag-and-drop and kanban primitives', () => {
120
+ const generator = createNextProjectGenerator()
121
+
122
+ it('generates the wrapper components, page imports and dependencies', async () => {
123
+ const outputFolder = await generator.generateProject(buildUidl(), template)
124
+
125
+ const packageFile = outputFolder.files.find((file) => file.name === 'package')
126
+ const packageJson = JSON.parse(packageFile?.content || '{}')
127
+ expect(packageJson.dependencies['@dnd-kit/core']).toBe('^6.3.1')
128
+ expect(packageJson.dependencies['@dnd-kit/sortable']).toBe('^10.0.0')
129
+ expect(packageJson.dependencies['@dnd-kit/utilities']).toBe('^3.2.2')
130
+ expect(packageJson.dependencies['@asseinfo/react-kanban']).toBe('2.2.0')
131
+ // Neither library requires a react bump on its own.
132
+ expect(packageJson.dependencies.react).toBe('^17.0.2')
133
+
134
+ const dragDropComponent = findFile(outputFolder, 'components', 'tq-drag-drop')
135
+ expect(dragDropComponent?.content).toContain('export const TqDragArea')
136
+ expect(dragDropComponent?.content).toContain("from '@dnd-kit/core'")
137
+
138
+ const kanbanComponent = findFile(outputFolder, 'components', 'tq-kanban')
139
+ expect(kanbanComponent?.content).toContain('initialBoard')
140
+ expect(kanbanComponent?.content).toContain('ssr: false')
141
+
142
+ const indexPage = findFile(outputFolder, 'pages', 'index')
143
+ expect(indexPage?.content).toContain('TqDragArea')
144
+ expect(indexPage?.content).toContain('TqDraggable')
145
+ expect(indexPage?.content).toContain('TqDroppable')
146
+ expect(indexPage?.content).toContain('TqSortable')
147
+ expect(indexPage?.content).toContain("from '../components/tq-drag-drop'")
148
+ expect(indexPage?.content).toContain('TqKanban')
149
+ expect(indexPage?.content).toContain("from '../components/tq-kanban'")
150
+ expect(indexPage?.content).toContain('dragId="task-1"')
151
+ expect(indexPage?.content).toContain('dropId="todo"')
152
+
153
+ const npmrc = outputFolder.files.find((file) => file.name === '.npmrc')
154
+ expect(npmrc?.content).toContain('legacy-peer-deps=true')
155
+
156
+ const appFile = findFile(outputFolder, 'pages', '_app')
157
+ expect(appFile?.content).toContain("import '@asseinfo/react-kanban/dist/styles.css'")
158
+ })
159
+
160
+ it('emits none of the wrappers for projects without these primitives', async () => {
161
+ const outputFolder = await generator.generateProject(
162
+ JSON.parse(JSON.stringify(uidlSample)) as ProjectUIDL,
163
+ JSON.parse(JSON.stringify(NextTemplate)) as GeneratedFolder
164
+ )
165
+
166
+ const componentsFolder = outputFolder.subFolders.find((sub) => sub.name === 'components')
167
+ expect(componentsFolder?.files.find((file) => file.name === 'tq-drag-drop')).toBeUndefined()
168
+ expect(componentsFolder?.files.find((file) => file.name === 'tq-kanban')).toBeUndefined()
169
+ expect(outputFolder.files.find((file) => file.name === '.npmrc')).toBeUndefined()
170
+
171
+ const packageFile = outputFolder.files.find((file) => file.name === 'package')
172
+ const packageJson = JSON.parse(packageFile?.content || '{}')
173
+ expect(packageJson.dependencies['@dnd-kit/core']).toBeUndefined()
174
+ expect(packageJson.dependencies['@asseinfo/react-kanban']).toBeUndefined()
175
+ })
176
+ })
@@ -0,0 +1,144 @@
1
+ import {
2
+ FileType,
3
+ InMemoryFileRecord,
4
+ ProjectPluginStructure,
5
+ ProjectUIDL,
6
+ UIDLElementNode,
7
+ } from '@teleporthq/teleport-types'
8
+ import { NextDragDropProjectPlugin } from '../src/drag-drop/project-plugin'
9
+ import { NextKanbanProjectPlugin } from '../src/kanban/project-plugin'
10
+
11
+ const APP_CONTENT = `import './style.css'
12
+
13
+ export default function MyApp({ Component, pageProps }) {
14
+ return <Component {...pageProps} />
15
+ }
16
+ `
17
+
18
+ const elementNode = (elementType: string, children: UIDLElementNode[] = []): UIDLElementNode => ({
19
+ type: 'element',
20
+ content: {
21
+ elementType,
22
+ children,
23
+ },
24
+ })
25
+
26
+ const buildStructure = (pageChildren: UIDLElementNode[]): ProjectPluginStructure => {
27
+ const files = new Map<string, InMemoryFileRecord>()
28
+ files.set('_app', {
29
+ path: ['pages'],
30
+ files: [{ name: '_app', fileType: FileType.JS, content: APP_CONTENT }],
31
+ })
32
+
33
+ const uidl = {
34
+ name: 'test',
35
+ root: {
36
+ name: 'App',
37
+ node: elementNode('container', pageChildren),
38
+ },
39
+ components: {},
40
+ } as unknown as ProjectUIDL
41
+
42
+ return {
43
+ uidl,
44
+ files,
45
+ dependencies: {},
46
+ devDependencies: {},
47
+ } as unknown as ProjectPluginStructure
48
+ }
49
+
50
+ describe('NextDragDropProjectPlugin', () => {
51
+ const plugin = new NextDragDropProjectPlugin()
52
+
53
+ it('is a no-op for projects without drag-and-drop primitives', async () => {
54
+ const structure = buildStructure([elementNode('container')])
55
+
56
+ await plugin.runAfter(structure)
57
+
58
+ expect(structure.files.has('tq-drag-drop-component')).toBe(false)
59
+ expect(structure.dependencies['@dnd-kit/core']).toBeUndefined()
60
+ })
61
+
62
+ it.each(['thq-drag-area', 'thq-draggable', 'thq-droppable', 'thq-sortable', 'thq-sortable-item'])(
63
+ 'emits the wrapper file and dnd-kit deps when %s is used',
64
+ async (primitive) => {
65
+ const structure = buildStructure([elementNode(primitive)])
66
+
67
+ await plugin.runAfter(structure)
68
+
69
+ const record = structure.files.get('tq-drag-drop-component')
70
+ expect(record?.path).toEqual(['components'])
71
+ expect(record?.files[0].name).toBe('tq-drag-drop')
72
+ expect(record?.files[0].content).toContain('export const TqDragArea')
73
+ expect(record?.files[0].content).toContain('export const TqSortable')
74
+ expect(structure.dependencies['@dnd-kit/core']).toBe('^6.3.1')
75
+ expect(structure.dependencies['@dnd-kit/sortable']).toBe('^10.0.0')
76
+ expect(structure.dependencies['@dnd-kit/utilities']).toBe('^3.2.2')
77
+ }
78
+ )
79
+
80
+ it('detects primitives nested deep in the tree', async () => {
81
+ const structure = buildStructure([
82
+ elementNode('container', [elementNode('container', [elementNode('thq-sortable')])]),
83
+ ])
84
+
85
+ await plugin.runAfter(structure)
86
+
87
+ expect(structure.files.has('tq-drag-drop-component')).toBe(true)
88
+ })
89
+ })
90
+
91
+ describe('NextKanbanProjectPlugin', () => {
92
+ const plugin = new NextKanbanProjectPlugin()
93
+
94
+ it('is a no-op for projects without a kanban board', async () => {
95
+ const structure = buildStructure([elementNode('container')])
96
+
97
+ await plugin.runAfter(structure)
98
+
99
+ expect(structure.files.has('tq-kanban-component')).toBe(false)
100
+ expect(structure.files.has('tq-kanban-npmrc')).toBe(false)
101
+ expect(structure.dependencies['@asseinfo/react-kanban']).toBeUndefined()
102
+ const appContent = structure.files.get('_app')?.files[0].content as string
103
+ expect(appContent).toBe(APP_CONTENT)
104
+ })
105
+
106
+ it('emits the wrapper, dependency, stylesheet import and .npmrc when used', async () => {
107
+ const structure = buildStructure([elementNode('kanban-node')])
108
+
109
+ await plugin.runAfter(structure)
110
+
111
+ const componentRecord = structure.files.get('tq-kanban-component')
112
+ expect(componentRecord?.path).toEqual(['components'])
113
+ expect(componentRecord?.files[0].name).toBe('tq-kanban')
114
+ expect(componentRecord?.files[0].content).toContain(
115
+ "dynamic(() => import('@asseinfo/react-kanban')"
116
+ )
117
+ expect(componentRecord?.files[0].content).toContain('ssr: false')
118
+ expect(componentRecord?.files[0].content).toContain('initialBoard')
119
+
120
+ const npmrcRecord = structure.files.get('tq-kanban-npmrc')
121
+ expect(npmrcRecord?.path).toEqual([])
122
+ expect(npmrcRecord?.files[0].name).toBe('.npmrc')
123
+ expect(npmrcRecord?.files[0].fileType).toBeUndefined()
124
+ expect(npmrcRecord?.files[0].content).toContain('legacy-peer-deps=true')
125
+
126
+ expect(structure.dependencies['@asseinfo/react-kanban']).toBe('2.2.0')
127
+
128
+ const appContent = structure.files.get('_app')?.files[0].content as string
129
+ expect(appContent).toContain("import '@asseinfo/react-kanban/dist/styles.css'")
130
+ expect(appContent.indexOf('@asseinfo/react-kanban/dist/styles.css')).toBeLessThan(
131
+ appContent.indexOf('./style.css')
132
+ )
133
+ })
134
+
135
+ it('is idempotent when run twice', async () => {
136
+ const structure = buildStructure([elementNode('kanban-node')])
137
+
138
+ await plugin.runAfter(structure)
139
+ await plugin.runAfter(structure)
140
+
141
+ const appContent = structure.files.get('_app')?.files[0].content as string
142
+ expect(appContent.match(/@asseinfo\/react-kanban\/dist\/styles\.css/g)).toHaveLength(1)
143
+ })
144
+ })