@nextsparkjs/core 0.1.0-beta.188 → 0.1.0-beta.189

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 (412) hide show
  1. package/dist/components/api/keys/ApiKeyDisplay.js +19 -19
  2. package/dist/components/api/keys/CreateApiKeyDialog.js +21 -21
  3. package/dist/components/app/layouts/PublicNavbar.d.ts.map +1 -1
  4. package/dist/components/app/layouts/PublicNavbar.js +34 -16
  5. package/dist/components/app/misc/ThemeToggle.d.ts.map +1 -1
  6. package/dist/components/app/misc/ThemeToggle.js +3 -4
  7. package/dist/components/auth/SessionCookieRefresher.d.ts +5 -0
  8. package/dist/components/auth/SessionCookieRefresher.d.ts.map +1 -1
  9. package/dist/components/auth/SessionCookieRefresher.js +7 -1
  10. package/dist/components/auth/forms/LoginForm.d.ts.map +1 -1
  11. package/dist/components/auth/forms/LoginForm.js +49 -12
  12. package/dist/components/auth/forms/SignupForm.d.ts.map +1 -1
  13. package/dist/components/auth/forms/SignupForm.js +4 -3
  14. package/dist/components/dashboard/block-editor/block-settings-panel.js +1 -1
  15. package/dist/components/dashboard/block-editor/floating-block-toolbar.js +1 -1
  16. package/dist/components/dashboard/block-editor/sortable-block.js +1 -1
  17. package/dist/components/dashboard/layouts/Sidebar.d.ts.map +1 -1
  18. package/dist/components/dashboard/layouts/Sidebar.js +8 -6
  19. package/dist/components/dashboard/layouts/TopNavbar.d.ts.map +1 -1
  20. package/dist/components/dashboard/layouts/TopNavbar.js +51 -54
  21. package/dist/components/dashboard/misc/NotificationsDropdown.d.ts.map +1 -1
  22. package/dist/components/dashboard/misc/NotificationsDropdown.js +17 -15
  23. package/dist/components/dashboard/misc/SearchDropdown.d.ts.map +1 -1
  24. package/dist/components/dashboard/misc/SearchDropdown.js +19 -22
  25. package/dist/components/dashboard/mobile/MobileBottomNav.js +1 -1
  26. package/dist/components/dashboard/mobile/MobileTopBar.js +2 -2
  27. package/dist/components/dashboard/navigation/DynamicNavigation.js +5 -5
  28. package/dist/components/devtools/BlockDetailViewer.js +1 -1
  29. package/dist/components/devtools/BlocksViewer.js +1 -1
  30. package/dist/components/entities/EntityChildManager.js +5 -5
  31. package/dist/components/entities/EntityDetailPage.d.ts.map +1 -1
  32. package/dist/components/entities/EntityDetailPage.js +16 -15
  33. package/dist/components/entities/EntityFieldRenderer.d.ts.map +1 -1
  34. package/dist/components/entities/EntityFieldRenderer.js +12 -7
  35. package/dist/components/entities/EntityPageHeader.js +5 -5
  36. package/dist/components/entities/ErrorBoundary.d.ts.map +1 -1
  37. package/dist/components/entities/ErrorBoundary.js +20 -21
  38. package/dist/components/entities/wrappers/EntityDetailWrapper.js +5 -5
  39. package/dist/components/entities/wrappers/EntityFormWrapper.js +2 -2
  40. package/dist/components/entities/wrappers/EntityListWrapper.js +3 -3
  41. package/dist/components/permissions/NoPermission.js +10 -10
  42. package/dist/components/public/pageBuilder/PageRenderer.d.ts.map +1 -1
  43. package/dist/components/public/pageBuilder/PageRenderer.js +2 -1
  44. package/dist/components/settings/SettingsPageSkeleton.js +4 -4
  45. package/dist/components/settings/layouts/SettingsSidebar.js +6 -6
  46. package/dist/components/shared/MultiSelectFilter.d.ts +3 -1
  47. package/dist/components/shared/MultiSelectFilter.d.ts.map +1 -1
  48. package/dist/components/shared/MultiSelectFilter.js +7 -3
  49. package/dist/components/shared/SearchInput.d.ts +2 -0
  50. package/dist/components/shared/SearchInput.d.ts.map +1 -1
  51. package/dist/components/shared/SearchInput.js +64 -6
  52. package/dist/components/ui/address-input.js +18 -18
  53. package/dist/components/ui/audio-upload.js +9 -9
  54. package/dist/components/ui/combobox.d.ts.map +1 -1
  55. package/dist/components/ui/combobox.js +4 -4
  56. package/dist/components/ui/country-select.d.ts.map +1 -1
  57. package/dist/components/ui/country-select.js +3 -3
  58. package/dist/components/ui/currency-select.d.ts.map +1 -1
  59. package/dist/components/ui/currency-select.js +3 -3
  60. package/dist/components/ui/dynamic-icon.d.ts.map +1 -1
  61. package/dist/components/ui/dynamic-icon.js +3 -2
  62. package/dist/components/ui/file-upload.js +7 -7
  63. package/dist/components/ui/image-upload.js +8 -8
  64. package/dist/components/ui/multi-select.d.ts +2 -1
  65. package/dist/components/ui/multi-select.d.ts.map +1 -1
  66. package/dist/components/ui/multi-select.js +4 -3
  67. package/dist/components/ui/phone-input.d.ts.map +1 -1
  68. package/dist/components/ui/phone-input.js +3 -3
  69. package/dist/components/ui/relation-display.js +1 -1
  70. package/dist/components/ui/simple-relation-select.d.ts +2 -1
  71. package/dist/components/ui/simple-relation-select.d.ts.map +1 -1
  72. package/dist/components/ui/simple-relation-select.js +7 -6
  73. package/dist/components/ui/tags-input.d.ts.map +1 -1
  74. package/dist/components/ui/tags-input.js +1 -1
  75. package/dist/components/ui/timezone-select.d.ts.map +1 -1
  76. package/dist/components/ui/timezone-select.js +3 -3
  77. package/dist/components/ui/user-display.js +4 -4
  78. package/dist/components/ui/user-select.d.ts.map +1 -1
  79. package/dist/components/ui/user-select.js +9 -9
  80. package/dist/components/ui/video-upload.js +9 -9
  81. package/dist/contexts/SubscriptionContext.d.ts.map +1 -1
  82. package/dist/contexts/SubscriptionContext.js +5 -1
  83. package/dist/contexts/TeamContext.d.ts.map +1 -1
  84. package/dist/contexts/TeamContext.js +83 -26
  85. package/dist/emails/otp-verification.d.ts.map +1 -1
  86. package/dist/emails/otp-verification.js +3 -2
  87. package/dist/hooks/useAccountPreferencesSync.d.ts +13 -0
  88. package/dist/hooks/useAccountPreferencesSync.d.ts.map +1 -0
  89. package/dist/hooks/useAccountPreferencesSync.js +95 -0
  90. package/dist/hooks/useAuth.d.ts +202 -15
  91. package/dist/hooks/useAuth.d.ts.map +1 -1
  92. package/dist/hooks/useAuth.js +49 -13
  93. package/dist/hooks/useEntityConfig.js +2 -2
  94. package/dist/hooks/useEntitySearch.js +10 -10
  95. package/dist/hooks/useNotifications.js +12 -12
  96. package/dist/hooks/usePrefetch.d.ts.map +1 -1
  97. package/dist/hooks/usePrefetch.js +6 -2
  98. package/dist/hooks/useSearch.js +15 -15
  99. package/dist/hooks/useSessionCookieRefresh.d.ts +18 -1
  100. package/dist/hooks/useSessionCookieRefresh.d.ts.map +1 -1
  101. package/dist/hooks/useSessionCookieRefresh.js +17 -4
  102. package/dist/hooks/useSessionHint.d.ts +12 -0
  103. package/dist/hooks/useSessionHint.d.ts.map +1 -0
  104. package/dist/hooks/useSessionHint.js +15 -0
  105. package/dist/hooks/useTranslationPreload.js +6 -6
  106. package/dist/i18n.d.ts +13 -0
  107. package/dist/i18n.d.ts.map +1 -1
  108. package/dist/i18n.js +2 -27
  109. package/dist/lib/actions/entity.actions.d.ts.map +1 -1
  110. package/dist/lib/actions/entity.actions.js +3 -2
  111. package/dist/lib/api/auth/dual-auth.d.ts +4 -1
  112. package/dist/lib/api/auth/dual-auth.d.ts.map +1 -1
  113. package/dist/lib/api/auth/dual-auth.js +5 -16
  114. package/dist/lib/api/entities.js +33 -33
  115. package/dist/lib/api/keys.d.ts +27 -27
  116. package/dist/lib/api/keys.js +27 -27
  117. package/dist/lib/auth/callback-url.d.ts +11 -0
  118. package/dist/lib/auth/callback-url.d.ts.map +1 -0
  119. package/dist/lib/auth/callback-url.js +14 -0
  120. package/dist/lib/auth/otp-config.d.ts +55 -0
  121. package/dist/lib/auth/otp-config.d.ts.map +1 -0
  122. package/dist/lib/auth/otp-config.js +48 -0
  123. package/dist/lib/auth/registration-helpers.d.ts.map +1 -1
  124. package/dist/lib/auth/registration-helpers.js +3 -1
  125. package/dist/lib/auth/session-hint.d.ts +31 -0
  126. package/dist/lib/auth/session-hint.d.ts.map +1 -0
  127. package/dist/lib/auth/session-hint.js +37 -0
  128. package/dist/lib/auth.d.ts.map +1 -1
  129. package/dist/lib/auth.js +6 -4
  130. package/dist/lib/blocks/loader.d.ts +3 -11
  131. package/dist/lib/blocks/loader.d.ts.map +1 -1
  132. package/dist/lib/blocks/loader.js +1 -9
  133. package/dist/lib/blocks/loader.server.d.ts +18 -0
  134. package/dist/lib/blocks/loader.server.d.ts.map +1 -0
  135. package/dist/lib/blocks/loader.server.js +11 -0
  136. package/dist/lib/blocks/sanitize-html.d.ts +31 -0
  137. package/dist/lib/blocks/sanitize-html.d.ts.map +1 -0
  138. package/dist/lib/blocks/sanitize-html.js +60 -0
  139. package/dist/lib/config/config-sync.d.ts +1 -0
  140. package/dist/lib/config/config-sync.d.ts.map +1 -1
  141. package/dist/lib/config/config-sync.js +4 -1
  142. package/dist/lib/config/types.d.ts +48 -0
  143. package/dist/lib/config/types.d.ts.map +1 -1
  144. package/dist/lib/email/types.d.ts +6 -0
  145. package/dist/lib/email/types.d.ts.map +1 -1
  146. package/dist/lib/entities/serialization.d.ts.map +1 -1
  147. package/dist/lib/entities/serialization.js +3 -6
  148. package/dist/lib/icons.d.ts +12 -0
  149. package/dist/lib/icons.d.ts.map +1 -0
  150. package/dist/lib/icons.js +30 -0
  151. package/dist/lib/locale.d.ts +25 -1
  152. package/dist/lib/locale.d.ts.map +1 -1
  153. package/dist/lib/locale.js +49 -35
  154. package/dist/lib/mcp/engine.js +1 -1
  155. package/dist/lib/mcp/errors.js +17 -17
  156. package/dist/lib/mcp/schema-builder.js +16 -16
  157. package/dist/lib/mcp/tool-generator.js +18 -18
  158. package/dist/lib/search-highlighting.d.ts +7 -0
  159. package/dist/lib/search-highlighting.d.ts.map +1 -1
  160. package/dist/lib/search-highlighting.js +6 -2
  161. package/dist/lib/selectors/core-selectors.d.ts +2 -0
  162. package/dist/lib/selectors/core-selectors.d.ts.map +1 -1
  163. package/dist/lib/selectors/domains/auth.selectors.d.ts +1 -0
  164. package/dist/lib/selectors/domains/auth.selectors.d.ts.map +1 -1
  165. package/dist/lib/selectors/domains/auth.selectors.js +1 -0
  166. package/dist/lib/selectors/domains/entities.selectors.d.ts +2 -0
  167. package/dist/lib/selectors/domains/entities.selectors.d.ts.map +1 -1
  168. package/dist/lib/selectors/domains/entities.selectors.js +2 -0
  169. package/dist/lib/selectors/selectors.d.ts +4 -0
  170. package/dist/lib/selectors/selectors.d.ts.map +1 -1
  171. package/dist/lib/services/block.service.js +1 -1
  172. package/dist/lib/teams/active-team-cookie.d.ts +24 -0
  173. package/dist/lib/teams/active-team-cookie.d.ts.map +1 -0
  174. package/dist/lib/teams/active-team-cookie.js +17 -0
  175. package/dist/lib/teams/dashboard-team.d.ts +20 -0
  176. package/dist/lib/teams/dashboard-team.d.ts.map +1 -0
  177. package/dist/lib/teams/dashboard-team.js +38 -0
  178. package/dist/lib/teams/schema.d.ts.map +1 -1
  179. package/dist/lib/teams/schema.js +1 -1
  180. package/dist/lib/template-resolver.d.ts.map +1 -1
  181. package/dist/lib/template-resolver.js +0 -1
  182. package/dist/lib/theme/get-default-theme-mode.d.ts +7 -4
  183. package/dist/lib/theme/get-default-theme-mode.d.ts.map +1 -1
  184. package/dist/lib/theme/get-default-theme-mode.js +2 -44
  185. package/dist/messages/de/auth.json +4 -2
  186. package/dist/messages/de/common.json +83 -3
  187. package/dist/messages/de/email.json +1 -1
  188. package/dist/messages/de/index.d.ts +87 -0
  189. package/dist/messages/de/index.d.ts.map +1 -1
  190. package/dist/messages/de/settings.json +5 -0
  191. package/dist/messages/en/auth.json +4 -2
  192. package/dist/messages/en/common.json +83 -3
  193. package/dist/messages/en/email.json +1 -1
  194. package/dist/messages/en/index.d.ts +86 -0
  195. package/dist/messages/en/index.d.ts.map +1 -1
  196. package/dist/messages/en/settings.json +4 -0
  197. package/dist/messages/es/auth.json +4 -2
  198. package/dist/messages/es/common.json +83 -3
  199. package/dist/messages/es/email.json +1 -1
  200. package/dist/messages/es/index.d.ts +86 -0
  201. package/dist/messages/es/index.d.ts.map +1 -1
  202. package/dist/messages/es/settings.json +4 -0
  203. package/dist/messages/fr/auth.json +4 -2
  204. package/dist/messages/fr/common.json +83 -3
  205. package/dist/messages/fr/email.json +1 -1
  206. package/dist/messages/fr/index.d.ts +87 -0
  207. package/dist/messages/fr/index.d.ts.map +1 -1
  208. package/dist/messages/fr/settings.json +5 -0
  209. package/dist/messages/it/auth.json +4 -2
  210. package/dist/messages/it/common.json +83 -3
  211. package/dist/messages/it/email.json +1 -1
  212. package/dist/messages/it/index.d.ts +87 -0
  213. package/dist/messages/it/index.d.ts.map +1 -1
  214. package/dist/messages/it/settings.json +5 -0
  215. package/dist/messages/pt/auth.json +4 -2
  216. package/dist/messages/pt/common.json +83 -3
  217. package/dist/messages/pt/email.json +1 -1
  218. package/dist/messages/pt/index.d.ts +87 -0
  219. package/dist/messages/pt/index.d.ts.map +1 -1
  220. package/dist/messages/pt/settings.json +5 -0
  221. package/dist/nextspark-registries.d.ts +54 -0
  222. package/dist/providers/DashboardProviders.d.ts +13 -10
  223. package/dist/providers/DashboardProviders.d.ts.map +1 -1
  224. package/dist/providers/DashboardProviders.js +7 -1
  225. package/dist/templates/app/(auth)/forgot-password/page.tsx +2 -2
  226. package/dist/templates/app/(auth)/reset-password/page.tsx +6 -6
  227. package/dist/templates/app/403/page.tsx +6 -6
  228. package/dist/templates/app/api/v1/teams/switch/route.ts +24 -5
  229. package/dist/templates/app/dashboard/(main)/[entity]/layout.tsx +9 -7
  230. package/dist/templates/app/dashboard/(main)/layout.tsx +4 -3
  231. package/dist/templates/app/dashboard/settings/api-keys/page.tsx +36 -34
  232. package/dist/templates/app/dashboard/settings/profile/page.tsx +7 -6
  233. package/dist/templates/app/dashboard/settings/security/page.tsx +5 -4
  234. package/dist/templates/app/layout.ppr.tsx +8 -8
  235. package/dist/templates/app/layout.tsx +12 -17
  236. package/dist/templates/blocks/cta-section/component.tsx +5 -2
  237. package/dist/templates/blocks/cta-section/config.ts +3 -2
  238. package/dist/templates/blocks/cta-section/fields.ts +2 -2
  239. package/dist/templates/blocks/cta-section/schema.ts +2 -2
  240. package/dist/templates/blocks/features-grid/component.tsx +10 -2
  241. package/dist/templates/blocks/features-grid/config.ts +3 -2
  242. package/dist/templates/blocks/features-grid/fields.ts +2 -2
  243. package/dist/templates/blocks/features-grid/schema.ts +2 -2
  244. package/dist/templates/blocks/hero/component.tsx +6 -3
  245. package/dist/templates/blocks/hero/config.ts +3 -2
  246. package/dist/templates/blocks/hero/fields.ts +2 -2
  247. package/dist/templates/blocks/hero/schema.ts +2 -2
  248. package/dist/templates/blocks/testimonials/component.tsx +6 -3
  249. package/dist/templates/blocks/testimonials/config.ts +3 -2
  250. package/dist/templates/blocks/testimonials/fields.ts +2 -2
  251. package/dist/templates/blocks/testimonials/schema.ts +2 -2
  252. package/dist/templates/blocks/text-content/component.tsx +7 -3
  253. package/dist/templates/blocks/text-content/config.ts +3 -2
  254. package/dist/templates/blocks/text-content/fields.ts +2 -2
  255. package/dist/templates/blocks/text-content/schema.ts +2 -2
  256. package/dist/templates/contents/themes/starter/blocks/cta-section/component.tsx +102 -0
  257. package/dist/templates/contents/themes/starter/blocks/cta-section/config.ts +11 -0
  258. package/dist/templates/contents/themes/starter/blocks/cta-section/fields.ts +89 -0
  259. package/dist/templates/contents/themes/starter/blocks/cta-section/index.ts +6 -0
  260. package/dist/templates/contents/themes/starter/blocks/cta-section/schema.ts +32 -0
  261. package/dist/templates/contents/themes/starter/blocks/cta-section/thumbnail.png +0 -0
  262. package/dist/templates/contents/themes/starter/blocks/features-grid/component.tsx +119 -0
  263. package/dist/templates/contents/themes/starter/blocks/features-grid/config.ts +11 -0
  264. package/dist/templates/contents/themes/starter/blocks/features-grid/fields.ts +97 -0
  265. package/dist/templates/contents/themes/starter/blocks/features-grid/index.ts +6 -0
  266. package/dist/templates/contents/themes/starter/blocks/features-grid/schema.ts +40 -0
  267. package/dist/templates/contents/themes/starter/blocks/features-grid/thumbnail.png +0 -0
  268. package/dist/templates/contents/themes/starter/blocks/hero/component.tsx +105 -0
  269. package/dist/templates/contents/themes/starter/blocks/hero/config.ts +11 -0
  270. package/dist/templates/contents/themes/starter/blocks/hero/fields.ts +60 -0
  271. package/dist/templates/contents/themes/starter/blocks/hero/index.ts +6 -0
  272. package/dist/templates/contents/themes/starter/blocks/hero/schema.ts +33 -0
  273. package/dist/templates/contents/themes/starter/blocks/hero/thumbnail.png +0 -0
  274. package/dist/templates/contents/themes/starter/blocks/testimonials/component.tsx +116 -0
  275. package/dist/templates/contents/themes/starter/blocks/testimonials/config.ts +11 -0
  276. package/dist/templates/contents/themes/starter/blocks/testimonials/fields.ts +105 -0
  277. package/dist/templates/contents/themes/starter/blocks/testimonials/index.ts +6 -0
  278. package/dist/templates/contents/themes/starter/blocks/testimonials/schema.ts +41 -0
  279. package/dist/templates/contents/themes/starter/blocks/testimonials/thumbnail.png +0 -0
  280. package/dist/templates/contents/themes/starter/blocks/text-content/component.tsx +100 -0
  281. package/dist/templates/contents/themes/starter/blocks/text-content/config.ts +11 -0
  282. package/dist/templates/contents/themes/starter/blocks/text-content/fields.ts +88 -0
  283. package/dist/templates/contents/themes/starter/blocks/text-content/index.ts +6 -0
  284. package/dist/templates/contents/themes/starter/blocks/text-content/schema.ts +30 -0
  285. package/dist/templates/contents/themes/starter/blocks/text-content/thumbnail.png +0 -0
  286. package/dist/templates/contents/themes/starter/config/app.config.ts +8 -0
  287. package/dist/templates/contents/themes/starter/entities/tasks/tasks.config.ts +7 -0
  288. package/dist/templates/contents/themes/starter/tests/cypress/e2e/_utils/devtools/access.cy.ts +9 -1
  289. package/dist/templates/contents/themes/starter/tests/cypress/e2e/api/entities/tasks/TaskAPIController.js +2 -2
  290. package/dist/templates/contents/themes/starter/tests/cypress/e2e/uat/entities/tasks/tasks-crud.cy.ts +45 -38
  291. package/dist/templates/contents/themes/starter/tests/cypress/fixtures/entities.json +16 -0
  292. package/dist/templates/contents/themes/starter/tests/cypress/src/components/DevKeyringPOM.ts +3 -1
  293. package/dist/templates/contents/themes/starter/tests/cypress/src/core/BlockEditorBasePOM.ts +3 -3
  294. package/dist/templates/contents/themes/starter/tests/cypress/src/entities/PagesPOM.ts +137 -0
  295. package/dist/templates/contents/themes/starter/tests/cypress/src/entities/index.ts +2 -0
  296. package/dist/templates/contents/themes/starter/tests/cypress/src/index.ts +1 -0
  297. package/dist/templates/contents/themes/starter/tests/cypress/src/session-helpers.ts +42 -11
  298. package/dist/templates/contents/themes/starter/tests/cypress/support/commands.ts +29 -18
  299. package/dist/templates/features/blog/blocks/post-content/component.tsx +5 -1
  300. package/dist/templates/features/blog/blocks/post-content/schema.ts +1 -1
  301. package/dist/templates/features/pages/blocks/hero/component.tsx +3 -0
  302. package/dist/templates/features/pages/blocks/hero/schema.ts +1 -1
  303. package/dist/templates/proxy.ts +138 -53
  304. package/dist/types/blocks.d.ts.map +1 -1
  305. package/dist/types/blocks.js +1 -1
  306. package/dist/types/task.types.d.ts +1 -1
  307. package/dist/types/task.types.js +7 -7
  308. package/dist/utils/dev/TranslationDebugger.js +2 -2
  309. package/package.json +14 -5
  310. package/scripts/build/registry/__tests__/block-registry.test.mjs +73 -0
  311. package/scripts/build/registry/__tests__/icons-discovery.test.mjs +130 -0
  312. package/scripts/build/registry/__tests__/identifier.test.mjs +65 -0
  313. package/scripts/build/registry/__tests__/page-generator.test.mjs +430 -0
  314. package/scripts/build/registry/__tests__/scheduled-actions-registry.test.mjs +41 -0
  315. package/scripts/build/registry/__tests__/template-registry.test.mjs +89 -0
  316. package/scripts/build/registry/discovery/icons.mjs +275 -0
  317. package/scripts/build/registry/discovery/index.mjs +1 -0
  318. package/scripts/build/registry/generators/block-registry.mjs +203 -55
  319. package/scripts/build/registry/generators/feature-registry.mjs +9 -2
  320. package/scripts/build/registry/generators/icon-registry.mjs +56 -0
  321. package/scripts/build/registry/generators/identifier.mjs +28 -0
  322. package/scripts/build/registry/generators/index.mjs +1 -0
  323. package/scripts/build/registry/generators/plugin-registry.mjs +4 -3
  324. package/scripts/build/registry/generators/scheduled-actions-registry.mjs +4 -2
  325. package/scripts/build/registry/generators/template-registry.mjs +49 -41
  326. package/scripts/build/registry/generators/theme-registry.mjs +6 -7
  327. package/scripts/build/registry/post-build/page-generator.mjs +380 -59
  328. package/scripts/build/registry.mjs +9 -1
  329. package/templates/app/(auth)/forgot-password/page.tsx +2 -2
  330. package/templates/app/(auth)/reset-password/page.tsx +6 -6
  331. package/templates/app/403/page.tsx +6 -6
  332. package/templates/app/api/v1/teams/switch/route.ts +24 -5
  333. package/templates/app/dashboard/(main)/[entity]/layout.tsx +9 -7
  334. package/templates/app/dashboard/(main)/layout.tsx +4 -3
  335. package/templates/app/dashboard/settings/api-keys/page.tsx +36 -34
  336. package/templates/app/dashboard/settings/profile/page.tsx +7 -6
  337. package/templates/app/dashboard/settings/security/page.tsx +5 -4
  338. package/templates/app/layout.ppr.tsx +8 -8
  339. package/templates/app/layout.tsx +12 -17
  340. package/templates/blocks/cta-section/component.tsx +5 -2
  341. package/templates/blocks/cta-section/config.ts +3 -2
  342. package/templates/blocks/cta-section/fields.ts +2 -2
  343. package/templates/blocks/cta-section/schema.ts +2 -2
  344. package/templates/blocks/features-grid/component.tsx +10 -2
  345. package/templates/blocks/features-grid/config.ts +3 -2
  346. package/templates/blocks/features-grid/fields.ts +2 -2
  347. package/templates/blocks/features-grid/schema.ts +2 -2
  348. package/templates/blocks/hero/component.tsx +6 -3
  349. package/templates/blocks/hero/config.ts +3 -2
  350. package/templates/blocks/hero/fields.ts +2 -2
  351. package/templates/blocks/hero/schema.ts +2 -2
  352. package/templates/blocks/testimonials/component.tsx +6 -3
  353. package/templates/blocks/testimonials/config.ts +3 -2
  354. package/templates/blocks/testimonials/fields.ts +2 -2
  355. package/templates/blocks/testimonials/schema.ts +2 -2
  356. package/templates/blocks/text-content/component.tsx +7 -3
  357. package/templates/blocks/text-content/config.ts +3 -2
  358. package/templates/blocks/text-content/fields.ts +2 -2
  359. package/templates/blocks/text-content/schema.ts +2 -2
  360. package/templates/contents/themes/starter/blocks/cta-section/component.tsx +102 -0
  361. package/templates/contents/themes/starter/blocks/cta-section/config.ts +11 -0
  362. package/templates/contents/themes/starter/blocks/cta-section/fields.ts +89 -0
  363. package/templates/contents/themes/starter/blocks/cta-section/index.ts +6 -0
  364. package/templates/contents/themes/starter/blocks/cta-section/schema.ts +32 -0
  365. package/templates/contents/themes/starter/blocks/cta-section/thumbnail.png +0 -0
  366. package/templates/contents/themes/starter/blocks/features-grid/component.tsx +119 -0
  367. package/templates/contents/themes/starter/blocks/features-grid/config.ts +11 -0
  368. package/templates/contents/themes/starter/blocks/features-grid/fields.ts +97 -0
  369. package/templates/contents/themes/starter/blocks/features-grid/index.ts +6 -0
  370. package/templates/contents/themes/starter/blocks/features-grid/schema.ts +40 -0
  371. package/templates/contents/themes/starter/blocks/features-grid/thumbnail.png +0 -0
  372. package/templates/contents/themes/starter/blocks/hero/component.tsx +105 -0
  373. package/templates/contents/themes/starter/blocks/hero/config.ts +11 -0
  374. package/templates/contents/themes/starter/blocks/hero/fields.ts +60 -0
  375. package/templates/contents/themes/starter/blocks/hero/index.ts +6 -0
  376. package/templates/contents/themes/starter/blocks/hero/schema.ts +33 -0
  377. package/templates/contents/themes/starter/blocks/hero/thumbnail.png +0 -0
  378. package/templates/contents/themes/starter/blocks/testimonials/component.tsx +116 -0
  379. package/templates/contents/themes/starter/blocks/testimonials/config.ts +11 -0
  380. package/templates/contents/themes/starter/blocks/testimonials/fields.ts +105 -0
  381. package/templates/contents/themes/starter/blocks/testimonials/index.ts +6 -0
  382. package/templates/contents/themes/starter/blocks/testimonials/schema.ts +41 -0
  383. package/templates/contents/themes/starter/blocks/testimonials/thumbnail.png +0 -0
  384. package/templates/contents/themes/starter/blocks/text-content/component.tsx +100 -0
  385. package/templates/contents/themes/starter/blocks/text-content/config.ts +11 -0
  386. package/templates/contents/themes/starter/blocks/text-content/fields.ts +88 -0
  387. package/templates/contents/themes/starter/blocks/text-content/index.ts +6 -0
  388. package/templates/contents/themes/starter/blocks/text-content/schema.ts +30 -0
  389. package/templates/contents/themes/starter/blocks/text-content/thumbnail.png +0 -0
  390. package/templates/contents/themes/starter/config/app.config.ts +8 -0
  391. package/templates/contents/themes/starter/entities/tasks/tasks.config.ts +7 -0
  392. package/templates/contents/themes/starter/tests/cypress/e2e/_utils/devtools/access.cy.ts +9 -1
  393. package/templates/contents/themes/starter/tests/cypress/e2e/api/entities/tasks/TaskAPIController.js +2 -2
  394. package/templates/contents/themes/starter/tests/cypress/e2e/uat/entities/tasks/tasks-crud.cy.ts +45 -38
  395. package/templates/contents/themes/starter/tests/cypress/fixtures/entities.json +16 -0
  396. package/templates/contents/themes/starter/tests/cypress/src/components/DevKeyringPOM.ts +3 -1
  397. package/templates/contents/themes/starter/tests/cypress/src/core/BlockEditorBasePOM.ts +3 -3
  398. package/templates/contents/themes/starter/tests/cypress/src/entities/PagesPOM.ts +137 -0
  399. package/templates/contents/themes/starter/tests/cypress/src/entities/index.ts +2 -0
  400. package/templates/contents/themes/starter/tests/cypress/src/index.ts +1 -0
  401. package/templates/contents/themes/starter/tests/cypress/src/session-helpers.ts +42 -11
  402. package/templates/contents/themes/starter/tests/cypress/support/commands.ts +29 -18
  403. package/templates/features/blog/blocks/post-content/component.tsx +5 -1
  404. package/templates/features/blog/blocks/post-content/schema.ts +1 -1
  405. package/templates/features/pages/blocks/hero/component.tsx +3 -0
  406. package/templates/features/pages/blocks/hero/schema.ts +1 -1
  407. package/templates/proxy.ts +138 -53
  408. package/tests/jest/__mocks__/@nextsparkjs/registries/block-registry.client.ts +9 -0
  409. package/tests/jest/__mocks__/@nextsparkjs/registries/block-registry.lazy.ts +5 -0
  410. package/tests/jest/__mocks__/@nextsparkjs/registries/icon-registry.ts +42 -0
  411. package/tests/jest/__mocks__/next-intl-server.js +85 -5
  412. package/tests/jest/__mocks__/next-server.js +32 -0
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Tests for icon discovery.
3
+ *
4
+ * The two halves that can silently produce an empty or wrong registry — which
5
+ * degrades to every icon rendering as a Box, with no error anywhere — are the
6
+ * path matcher and the name extraction. Both are pure, so both are tested here
7
+ * directly; discoverIcons() itself only wires them to the filesystem.
8
+ *
9
+ * Run: node --test packages/core/scripts/build/registry/__tests__/icons-discovery.test.mjs
10
+ */
11
+
12
+ import { test } from 'node:test'
13
+ import assert from 'node:assert/strict'
14
+
15
+ import { findUnresolvedIconRefs, extractIconNames, isIconSourcePath } from '../discovery/icons.mjs'
16
+
17
+ test('matches entity, block and app configs', () => {
18
+ assert.ok(isIconSourcePath('/p/contents/themes/default/entities/tasks/tasks.config.ts'))
19
+ assert.ok(isIconSourcePath('/p/themes/default/blocks/hero/config.ts'))
20
+ assert.ok(isIconSourcePath('/p/themes/default/config/app.config.ts'))
21
+ })
22
+
23
+ test('matches the same paths with Windows separators', () => {
24
+ assert.ok(isIconSourcePath('C:\\p\\contents\\themes\\default\\entities\\tasks\\tasks.config.ts'))
25
+ assert.ok(isIconSourcePath('C:\\p\\themes\\default\\blocks\\hero\\config.ts'))
26
+ assert.ok(isIconSourcePath('C:\\p\\themes\\default\\config\\app.config.ts'))
27
+ })
28
+
29
+ test('ignores configs whose icons never reach resolveIcon', () => {
30
+ assert.equal(isIconSourcePath('/p/themes/crm/config/features.config.ts'), false)
31
+ assert.equal(isIconSourcePath('/p/themes/default/config/theme.config.ts'), false)
32
+ assert.equal(isIconSourcePath('/p/themes/default/entities/tasks/messages/en.ts'), false)
33
+ })
34
+
35
+ test('does not match a file that merely ends in config.ts', () => {
36
+ assert.equal(isIconSourcePath('/p/themes/default/blocks/hero/notconfig.ts'), false)
37
+ })
38
+
39
+ test('reads an icon under a quoted key', () => {
40
+ const source = `
41
+ import { Users } from 'lucide-react'
42
+ export const config = { 'icon': Users }
43
+ `
44
+ assert.deepEqual(extractIconNames(source), ['Users'])
45
+ })
46
+
47
+ test('reads an icon declared as a lucide import', () => {
48
+ const source = `
49
+ import { Users, CheckSquare } from 'lucide-react'
50
+ export const config = { icon: CheckSquare }
51
+ `
52
+ assert.deepEqual(extractIconNames(source), ['CheckSquare'])
53
+ })
54
+
55
+ test('resolves an aliased import back to the lucide export name', () => {
56
+ // `icon: HouseIcon` is lucide's `Home`; the registry is keyed by the export
57
+ const source = `
58
+ import { Home as HouseIcon } from 'lucide-react'
59
+ export const config = { icon: HouseIcon }
60
+ `
61
+ assert.deepEqual(extractIconNames(source), ['Home'])
62
+ })
63
+
64
+ test('ignores an identifier that did not come from lucide', () => {
65
+ const source = `
66
+ import { MyOwnIcon } from '@/components/icons'
67
+ export const config = { icon: MyOwnIcon }
68
+ `
69
+ assert.deepEqual(extractIconNames(source), [])
70
+ })
71
+
72
+ test('reads a string icon name, kebab-case included', () => {
73
+ const source = `export const config = { icon: 'pie-chart' }`
74
+ assert.deepEqual(extractIconNames(source), ['pie-chart'])
75
+ })
76
+
77
+ test('does not match a different key that ends in icon', () => {
78
+ const source = `
79
+ import { Users } from 'lucide-react'
80
+ export const config = { sidebarIcon: Users, my_icon: Users }
81
+ `
82
+ assert.deepEqual(extractIconNames(source), [])
83
+ })
84
+
85
+ test('never yields anything but a bare name, so nothing can be injected', () => {
86
+ // The generated registry is TypeScript built from these strings, and every
87
+ // name becomes a named import. Whatever a config holds, what comes out here
88
+ // is a bare name — and discoverIcons then drops any that lucide doesn't
89
+ // export, which 'X' is.
90
+ const sources = [
91
+ `export const config = { icon: 'X"; process.exit(1); //' }`,
92
+ `export const config = { icon: 'Users } from "x"; import evil from "y"; //' }`,
93
+ 'export const config = { icon: `${danger}` }'
94
+ ]
95
+
96
+ for (const source of sources) {
97
+ for (const name of extractIconNames(source)) {
98
+ assert.match(name, /^[A-Za-z][\w$-]*$/, `unsafe name from: ${source}`)
99
+ }
100
+ }
101
+ })
102
+
103
+ // Only a direct lucide import or a string literal reaches the registry, so
104
+ // every other shape has to be reported rather than silently rendering a Box.
105
+ test('reports an icon reference the build cannot resolve', () => {
106
+ assert.deepEqual(
107
+ findUnresolvedIconRefs("import * as I from 'lucide-react'\nexport default { icon: I.Users }"),
108
+ ['I.Users']
109
+ )
110
+ assert.deepEqual(
111
+ findUnresolvedIconRefs("import { Users } from './icons'\nexport default { icon: Users }"),
112
+ ['Users']
113
+ )
114
+ assert.deepEqual(
115
+ findUnresolvedIconRefs("import { Users } from 'lucide-react'\nconst Mine = Users\nexport default { icon: Mine }"),
116
+ ['Mine']
117
+ )
118
+ })
119
+
120
+ test('says nothing about a reference it can resolve', () => {
121
+ assert.deepEqual(
122
+ findUnresolvedIconRefs("import { Users } from 'lucide-react'\nexport default { icon: Users }"),
123
+ []
124
+ )
125
+ assert.deepEqual(
126
+ findUnresolvedIconRefs("import { Home as HouseIcon } from 'lucide-react'\nexport default { icon: HouseIcon }"),
127
+ []
128
+ )
129
+ assert.deepEqual(findUnresolvedIconRefs("export default { icon: 'pie-chart' }"), [])
130
+ })
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Tests for identifier safety in generated registries.
3
+ *
4
+ * The aliases the registries import under are built from user-chosen names, and
5
+ * an invalid one is not a local problem: every route reaches the app through
6
+ * these files, so a single `as 7startupsAppConfig` stops the whole project from
7
+ * parsing. Replacing hyphens — what this used to do — leaves that case open.
8
+ *
9
+ * Run: node --test packages/core/scripts/build/registry/__tests__/identifier.test.mjs
10
+ */
11
+
12
+ import { test } from 'node:test'
13
+ import assert from 'node:assert/strict'
14
+
15
+ import { toSafeIdentifier } from '../generators/identifier.mjs'
16
+
17
+ /** What the generated files are fed to: an ES module parser. */
18
+ function isValidIdentifier(value) {
19
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value)
20
+ }
21
+
22
+ test('leaves a name that is already an identifier alone', () => {
23
+ assert.equal(toSafeIdentifier('default'), 'default')
24
+ assert.equal(toSafeIdentifier('myTheme'), 'myTheme')
25
+ assert.equal(toSafeIdentifier('theme_2'), 'theme_2')
26
+ })
27
+
28
+ test('replaces the characters a directory name allows and an identifier does not', () => {
29
+ assert.equal(toSafeIdentifier('my-theme'), 'my_theme')
30
+ assert.equal(toSafeIdentifier('my.theme'), 'my_theme')
31
+ assert.equal(toSafeIdentifier('my theme'), 'my_theme')
32
+ assert.equal(toSafeIdentifier('@scope/name'), '_scope_name')
33
+ })
34
+
35
+ test('prefixes a leading digit', () => {
36
+ assert.equal(toSafeIdentifier('7startups'), '_7startups')
37
+ assert.equal(toSafeIdentifier('123-app'), '_123_app')
38
+ })
39
+
40
+ test('never returns an empty identifier', () => {
41
+ assert.equal(toSafeIdentifier(''), '_')
42
+ assert.equal(toSafeIdentifier('---'), '___')
43
+ })
44
+
45
+ test('every result is a valid identifier, suffixed or not', () => {
46
+ const names = [
47
+ 'default', 'my-theme', '7startups', '123-app', 'a.b.c', '', '---',
48
+ '2026', 'über-theme', 'theme!', '9', '_private', '$dollar',
49
+ ]
50
+
51
+ for (const name of names) {
52
+ const safe = toSafeIdentifier(name)
53
+ assert.ok(isValidIdentifier(safe), `${JSON.stringify(name)} produced ${JSON.stringify(safe)}`)
54
+ // The generators append a role to the alias, so that has to stay valid too
55
+ assert.ok(isValidIdentifier(`${safe}AppConfig`), `${safe}AppConfig is not an identifier`)
56
+ }
57
+ })
58
+
59
+ test('distinct names stay distinct where it matters', () => {
60
+ // Themes sit in one directory, so their names cannot collide after cleaning
61
+ // unless they differed only by a separator — which the filesystem allows.
62
+ // Documented rather than fixed: the alias is cosmetic, the registry is keyed
63
+ // by the real name.
64
+ assert.equal(toSafeIdentifier('my-theme'), toSafeIdentifier('my.theme'))
65
+ })
@@ -0,0 +1,430 @@
1
+ /**
2
+ * Tests for the page-generator's route-level export handling.
3
+ *
4
+ * Next.js only reads route segment config (`revalidate`, `dynamic`, etc.)
5
+ * when it's a literal `export const` declared directly in the route file -
6
+ * a re-export is silently ignored and the default config is used instead
7
+ * (#191). The generator has to tell the two kinds of route export apart:
8
+ * segment config gets re-declared as a literal, everything else (metadata,
9
+ * generateMetadata, ...) can be forwarded with a plain re-export.
10
+ *
11
+ * Run: node --test packages/core/scripts/build/registry/__tests__/page-generator.test.mjs
12
+ */
13
+
14
+ import { test } from 'node:test'
15
+ import assert from 'node:assert/strict'
16
+ import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises'
17
+ import { tmpdir } from 'node:os'
18
+ import { join, dirname } from 'node:path'
19
+
20
+ import {
21
+ SEGMENT_CONFIG_EXPORTS,
22
+ MODULE_LEVEL_EXPORTS,
23
+ extractRouteExports,
24
+ generateTemplatePage,
25
+ generateMissingPages,
26
+ } from '../post-build/page-generator.mjs'
27
+
28
+ const FIXTURE_FILE = '/virtual/theme/templates/(public)/page.tsx'
29
+ const FIXTURE_FILE_PATTERN = FIXTURE_FILE.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
30
+
31
+ // --- helpers for the content-generation tests ------------------------------
32
+
33
+ async function createProjectRoot() {
34
+ return mkdtemp(join(tmpdir(), 'nextspark-page-generator-test-'))
35
+ }
36
+
37
+ // generateTemplatePage resolves '@/...' template paths against the
38
+ // generator's module-level rootDir, which is only ever set as a side effect
39
+ // of generateMissingPages. Templates being empty makes it a no-op beyond
40
+ // that assignment (cleanupOrphanedTemplates returns early when app/(templates)
41
+ // doesn't exist yet under the given root).
42
+ async function useProjectRoot(root) {
43
+ await generateMissingPages([], { projectRoot: root })
44
+ }
45
+
46
+ async function writeThemeTemplate(root, relativePath, content) {
47
+ const absolutePath = join(root, 'contents/themes/testtheme/templates', relativePath)
48
+ await mkdir(dirname(absolutePath), { recursive: true })
49
+ await writeFile(absolutePath, content, 'utf8')
50
+ return `@/contents/themes/testtheme/templates/${relativePath}`
51
+ }
52
+
53
+ // --- the two exported export-name tables ------------------------------------
54
+
55
+ test('segment config and module-level export lists do not overlap', () => {
56
+ const overlap = SEGMENT_CONFIG_EXPORTS.filter(name => MODULE_LEVEL_EXPORTS.includes(name))
57
+ assert.deepEqual(overlap, [])
58
+ })
59
+
60
+ test('experimental_ppr is one of the tracked segment config keys', () => {
61
+ assert.ok(SEGMENT_CONFIG_EXPORTS.includes('experimental_ppr'))
62
+ })
63
+
64
+ // --- extractRouteExports: valid segment config literals ---------------------
65
+
66
+ test('a number literal is captured as segment config', async () => {
67
+ const { segmentConfig } = await extractRouteExports('export const maxDuration = 30\n', FIXTURE_FILE)
68
+ assert.equal(segmentConfig.maxDuration, 30)
69
+ })
70
+
71
+ test('revalidate = false is captured as a literal, not treated as absent', async () => {
72
+ const { segmentConfig } = await extractRouteExports('export const revalidate = false\n', FIXTURE_FILE)
73
+ assert.equal(segmentConfig.revalidate, false)
74
+ })
75
+
76
+ test('a valid enum string is captured', async () => {
77
+ const { segmentConfig } = await extractRouteExports("export const dynamic = 'force-static'\n", FIXTURE_FILE)
78
+ assert.equal(segmentConfig.dynamic, 'force-static')
79
+ })
80
+
81
+ test('an array of strings is captured', async () => {
82
+ const { segmentConfig } = await extractRouteExports("export const preferredRegion = ['iad1', 'sfo1']\n", FIXTURE_FILE)
83
+ assert.deepEqual(segmentConfig.preferredRegion, ['iad1', 'sfo1'])
84
+ })
85
+
86
+ test('a satisfies-wrapped literal is unwrapped and captured', async () => {
87
+ const { segmentConfig } = await extractRouteExports(
88
+ "export const runtime = 'edge' satisfies 'edge' | 'nodejs'\n",
89
+ FIXTURE_FILE
90
+ )
91
+ assert.equal(segmentConfig.runtime, 'edge')
92
+ })
93
+
94
+ test('an as-const-wrapped literal is unwrapped and captured', async () => {
95
+ const { segmentConfig } = await extractRouteExports('export const dynamicParams = false as const\n', FIXTURE_FILE)
96
+ assert.equal(segmentConfig.dynamicParams, false)
97
+ })
98
+
99
+ test('a boolean literal is captured for experimental_ppr', async () => {
100
+ const { segmentConfig } = await extractRouteExports('export const experimental_ppr = true\n', FIXTURE_FILE)
101
+ assert.equal(segmentConfig.experimental_ppr, true)
102
+ })
103
+
104
+ // --- extractRouteExports: module-level exports -------------------------------
105
+
106
+ test('an exported function declaration is a module-level export', async () => {
107
+ const { moduleExports, segmentConfig } = await extractRouteExports(
108
+ 'export async function generateMetadata() { return {} }\n',
109
+ FIXTURE_FILE
110
+ )
111
+ assert.deepEqual(moduleExports, ['generateMetadata'])
112
+ assert.deepEqual(segmentConfig, {})
113
+ })
114
+
115
+ test('a locally re-exported function is a module-level export', async () => {
116
+ const source = `async function generateStaticParams() { return [] }\nexport { generateStaticParams }\n`
117
+ const { moduleExports } = await extractRouteExports(source, FIXTURE_FILE)
118
+ assert.deepEqual(moduleExports, ['generateStaticParams'])
119
+ })
120
+
121
+ test('an exported const object (metadata) is a module-level export, value untouched', async () => {
122
+ const source = `export const metadata = { title: 'Blog' }\n`
123
+ const { moduleExports, segmentConfig } = await extractRouteExports(source, FIXTURE_FILE)
124
+ assert.deepEqual(moduleExports, ['metadata'])
125
+ assert.deepEqual(segmentConfig, {})
126
+ })
127
+
128
+ // --- extractRouteExports: what does NOT count as an export -------------------
129
+
130
+ test('a commented-out export is not detected', async () => {
131
+ const source = '// export const revalidate = 60\nexport default function Page() { return null }\n'
132
+ const { segmentConfig } = await extractRouteExports(source, FIXTURE_FILE)
133
+ assert.deepEqual(segmentConfig, {})
134
+ })
135
+
136
+ test('export syntax inside a string literal is not detected', async () => {
137
+ const source = 'const code = "export const revalidate = 60"\nexport default function Page() { return null }\n'
138
+ const { segmentConfig } = await extractRouteExports(source, FIXTURE_FILE)
139
+ assert.deepEqual(segmentConfig, {})
140
+ })
141
+
142
+ // --- extractRouteExports: invalid segment config must throw ------------------
143
+
144
+ test('an expression value fails with an error naming the file and the key', async () => {
145
+ await assert.rejects(
146
+ () => extractRouteExports('export const revalidate = 60 * 60\n', FIXTURE_FILE),
147
+ error => {
148
+ assert.match(error.message, new RegExp(FIXTURE_FILE_PATTERN))
149
+ assert.match(error.message, /"revalidate"/)
150
+ assert.match(error.message, /literal/)
151
+ return true
152
+ }
153
+ )
154
+ })
155
+
156
+ test('an identifier value fails', async () => {
157
+ const source = 'const HOUR = 3600\nexport const revalidate = HOUR\n'
158
+ await assert.rejects(() => extractRouteExports(source, FIXTURE_FILE), /"revalidate"/)
159
+ })
160
+
161
+ test('re-exporting a segment config key from another module fails', async () => {
162
+ const source = "export { revalidate } from './shared-config'\n"
163
+ await assert.rejects(() => extractRouteExports(source, FIXTURE_FILE), /re-exported from another module/)
164
+ })
165
+
166
+ test('a local export specifier (no inline const) fails for a segment config key', async () => {
167
+ const source = 'const revalidate = 60\nexport { revalidate }\n'
168
+ await assert.rejects(() => extractRouteExports(source, FIXTURE_FILE), /"revalidate"/)
169
+ })
170
+
171
+ test('let/var for a segment config key fails', async () => {
172
+ const source = "export let dynamic = 'force-static'\n"
173
+ await assert.rejects(() => extractRouteExports(source, FIXTURE_FILE), /"let" or "var"/)
174
+ })
175
+
176
+ test('a value outside the schema fails', async () => {
177
+ const source = "export const dynamic = 'nonsense'\n"
178
+ await assert.rejects(() => extractRouteExports(source, FIXTURE_FILE), /outside what Next\.js accepts/)
179
+ })
180
+
181
+ test('a negative maxDuration fails schema validation', async () => {
182
+ const source = 'export const maxDuration = -5\n'
183
+ await assert.rejects(() => extractRouteExports(source, FIXTURE_FILE), /"maxDuration"/)
184
+ })
185
+
186
+ test('an array with a non-string element fails schema validation', async () => {
187
+ const source = "export const preferredRegion = ['iad1', 42]\n"
188
+ await assert.rejects(() => extractRouteExports(source, FIXTURE_FILE), /"preferredRegion"/)
189
+ })
190
+
191
+ test('the error names the line the bad export is declared on', async () => {
192
+ const source = "import { x } from 'y'\n\nexport const revalidate = computeRevalidate()\n"
193
+ await assert.rejects(() => extractRouteExports(source, FIXTURE_FILE), new RegExp(`${FIXTURE_FILE_PATTERN}:3`))
194
+ })
195
+
196
+ // --- generated page content ---------------------------------------------------
197
+
198
+ test('the generated page re-declares segment config as a literal and forwards module exports separately', async () => {
199
+ const root = await createProjectRoot()
200
+ try {
201
+ await useProjectRoot(root)
202
+ const templatePath = await writeThemeTemplate(
203
+ root,
204
+ '(public)/blog/[slug]/page.tsx',
205
+ "export async function generateMetadata() { return {} }\nexport const revalidate = 3600\nexport default function Page() { return null }\n"
206
+ )
207
+
208
+ const outputPath = join(root, 'app/(templates)/(public)/blog/[slug]/page.tsx')
209
+ await generateTemplatePage(
210
+ { appPath: 'app/(public)/blog/[slug]/page.tsx', templateType: 'page', name: '(public)/blog/[slug]/page', templatePath },
211
+ outputPath
212
+ )
213
+
214
+ const generated = await readFile(outputPath, 'utf8')
215
+ assert.match(generated, /export const revalidate = 3600/)
216
+ assert.match(generated, /export \{ generateMetadata \} from '@\/contents\/themes\/testtheme\/templates\/\(public\)\/blog\/\[slug\]\/page'/)
217
+
218
+ const exportBlockLine = generated.split('\n').find(line => line.startsWith('export {'))
219
+ assert.ok(exportBlockLine, 'expected an export {} re-export line')
220
+ for (const key of SEGMENT_CONFIG_EXPORTS) {
221
+ assert.ok(!exportBlockLine.includes(key), `segment config key "${key}" must not appear in export {}`)
222
+ }
223
+ } finally {
224
+ await rm(root, { recursive: true, force: true })
225
+ }
226
+ })
227
+
228
+ test('a page with no route-level exports is generated unchanged (no forwarded-exports section)', async () => {
229
+ const root = await createProjectRoot()
230
+ try {
231
+ await useProjectRoot(root)
232
+ const templatePath = await writeThemeTemplate(root, 'plain/page.tsx', 'export default function Page() { return null }\n')
233
+
234
+ const outputPath = join(root, 'app/(templates)/plain/page.tsx')
235
+ await generateTemplatePage(
236
+ { appPath: 'app/plain/page.tsx', templateType: 'page', name: 'plain/page', templatePath },
237
+ outputPath
238
+ )
239
+
240
+ const generated = await readFile(outputPath, 'utf8')
241
+ assert.doesNotMatch(generated, /export const/)
242
+ assert.doesNotMatch(generated, /export \{/)
243
+ assert.match(generated, /export default TemplateComponent\n$/)
244
+ } finally {
245
+ await rm(root, { recursive: true, force: true })
246
+ }
247
+ })
248
+
249
+ test('an invalid segment config value fails page generation instead of being silently dropped', async () => {
250
+ const root = await createProjectRoot()
251
+ try {
252
+ await useProjectRoot(root)
253
+ const templatePath = await writeThemeTemplate(
254
+ root,
255
+ 'broken/page.tsx',
256
+ 'export const revalidate = process.env.REVALIDATE\nexport default function Page() { return null }\n'
257
+ )
258
+
259
+ const outputPath = join(root, 'app/(templates)/broken/page.tsx')
260
+ await assert.rejects(
261
+ () =>
262
+ generateTemplatePage(
263
+ { appPath: 'app/broken/page.tsx', templateType: 'page', name: 'broken/page', templatePath },
264
+ outputPath
265
+ ),
266
+ /"revalidate"/
267
+ )
268
+ } finally {
269
+ await rm(root, { recursive: true, force: true })
270
+ }
271
+ })
272
+
273
+ test('a layout with a default export and metadata forwards both, unlike the previous component-only behavior', async () => {
274
+ const root = await createProjectRoot()
275
+ try {
276
+ await useProjectRoot(root)
277
+ const templatePath = await writeThemeTemplate(
278
+ root,
279
+ 'dashboard/layout.tsx',
280
+ "export const metadata = { title: 'Dashboard' }\nexport default function Layout({ children }) { return children }\n"
281
+ )
282
+
283
+ const outputPath = join(root, 'app/(templates)/dashboard/layout.tsx')
284
+ await generateTemplatePage(
285
+ { appPath: 'app/dashboard/layout.tsx', templateType: 'layout', name: 'dashboard/layout', templatePath },
286
+ outputPath
287
+ )
288
+
289
+ const generated = await readFile(outputPath, 'utf8')
290
+ assert.match(generated, /import TemplateComponent from/)
291
+ assert.match(generated, /export \{ metadata \} from/)
292
+ } finally {
293
+ await rm(root, { recursive: true, force: true })
294
+ }
295
+ })
296
+
297
+ test('a layout template with no exports at all still gets the plain pass-through wrapper', async () => {
298
+ const root = await createProjectRoot()
299
+ try {
300
+ await useProjectRoot(root)
301
+ const templatePath = await writeThemeTemplate(root, 'empty/layout.tsx', '')
302
+
303
+ const outputPath = join(root, 'app/(templates)/empty/layout.tsx')
304
+ await generateTemplatePage(
305
+ { appPath: 'app/empty/layout.tsx', templateType: 'layout', name: 'empty/layout', templatePath },
306
+ outputPath
307
+ )
308
+
309
+ const generated = await readFile(outputPath, 'utf8')
310
+ assert.match(generated, /Layout template - empty template file/)
311
+ assert.doesNotMatch(generated, /export \{/)
312
+ assert.doesNotMatch(generated, /export const/)
313
+ } finally {
314
+ await rm(root, { recursive: true, force: true })
315
+ }
316
+ })
317
+
318
+ // --- default export detection (decides component import vs pass-through) ----
319
+
320
+ for (const [label, source, expected] of [
321
+ ['export default function', 'export default function Layout({ children }) { return children }\n', true],
322
+ ['export default of a declared identifier', 'const Layout = ({ children }) => children\nexport default Layout\n', true],
323
+ ['a local export clause naming default', 'function Layout({ children }) { return children }\nexport { Layout as default }\n', true],
324
+ ['a default re-exported from another module', "export { default } from './shell'\n", true],
325
+ ['a comment that mentions export default', "// Intentionally no export default: metadata-only layout.\nexport const metadata = { title: 'Docs' }\n", false],
326
+ ['a string that contains export default', "export const metadata = { title: 'export default' }\n", false],
327
+ ['a type-only default', 'type Props = { title: string }\nexport type { Props as default }\n', false],
328
+ ]) {
329
+ test(`hasDefaultExport is ${expected} for ${label}`, async () => {
330
+ const { hasDefaultExport } = await extractRouteExports(source, FIXTURE_FILE)
331
+ assert.equal(hasDefaultExport, expected)
332
+ })
333
+ }
334
+
335
+ test('a metadata-only layout whose comment mentions export default gets the pass-through, not an import of a missing default', async () => {
336
+ const root = await createProjectRoot()
337
+ try {
338
+ await useProjectRoot(root)
339
+ const templatePath = await writeThemeTemplate(
340
+ root,
341
+ 'docs/layout.tsx',
342
+ "// Intentionally no export default: metadata-only layout.\nexport const metadata = { title: 'Docs' }\n"
343
+ )
344
+
345
+ const outputPath = join(root, 'app/(templates)/docs/layout.tsx')
346
+ await generateTemplatePage(
347
+ { appPath: 'app/docs/layout.tsx', templateType: 'layout', name: 'docs/layout', templatePath },
348
+ outputPath
349
+ )
350
+
351
+ const generated = await readFile(outputPath, 'utf8')
352
+ assert.match(generated, /Layout template - metadata-only/)
353
+ assert.doesNotMatch(generated, /import TemplateComponent from/)
354
+ assert.match(generated, /export \{ metadata \} from/)
355
+ } finally {
356
+ await rm(root, { recursive: true, force: true })
357
+ }
358
+ })
359
+
360
+ test('a layout exported through `export { X as default }` imports the theme component instead of a pass-through', async () => {
361
+ const root = await createProjectRoot()
362
+ try {
363
+ await useProjectRoot(root)
364
+ const templatePath = await writeThemeTemplate(
365
+ root,
366
+ 'dashboard/layout.tsx',
367
+ 'function DashboardLayout({ children }) { return children }\nexport { DashboardLayout as default }\n'
368
+ )
369
+
370
+ const outputPath = join(root, 'app/(templates)/dashboard/layout.tsx')
371
+ await generateTemplatePage(
372
+ { appPath: 'app/dashboard/layout.tsx', templateType: 'layout', name: 'dashboard/layout', templatePath },
373
+ outputPath
374
+ )
375
+
376
+ const generated = await readFile(outputPath, 'utf8')
377
+ assert.match(generated, /import TemplateComponent from/)
378
+ assert.doesNotMatch(generated, /Pass-through component|empty template file/)
379
+ } finally {
380
+ await rm(root, { recursive: true, force: true })
381
+ }
382
+ })
383
+
384
+ // --- parsing as the template's own kind of file ------------------------------
385
+
386
+ test('a .ts template is parsed as TypeScript, so syntax a TSX parse reads as JSX keeps its default export', async () => {
387
+ const source = 'type Props = { children: string }\nconst Layout = <T>({ children }: T & Props): string => children\nexport default Layout\n'
388
+ const { hasDefaultExport } = await extractRouteExports(source, '/virtual/theme/templates/(public)/layout.ts')
389
+ assert.equal(hasDefaultExport, true)
390
+ })
391
+
392
+ test('a .ts layout with TypeScript-only syntax imports the theme component instead of a pass-through', async () => {
393
+ const root = await createProjectRoot()
394
+ try {
395
+ await useProjectRoot(root)
396
+ const templatePath = await writeThemeTemplate(
397
+ root,
398
+ 'reports/layout.ts',
399
+ "import type { ReactNode } from 'react'\nconst ReportsLayout = <T>({ children }: T & { children: ReactNode }) => children\nexport default ReportsLayout\n"
400
+ )
401
+
402
+ const outputPath = join(root, 'app/(templates)/reports/layout.ts')
403
+ await generateTemplatePage(
404
+ { appPath: 'app/reports/layout.ts', templateType: 'layout', name: 'reports/layout', templatePath },
405
+ outputPath
406
+ )
407
+
408
+ const generated = await readFile(outputPath, 'utf8')
409
+ assert.match(generated, /import TemplateComponent from/)
410
+ } finally {
411
+ await rm(root, { recursive: true, force: true })
412
+ }
413
+ })
414
+
415
+ test('a template that does not parse fails with its file and line instead of being read in part', async () => {
416
+ const source = 'export const revalidate = 60\nexport default function Page( {\n'
417
+ await assert.rejects(
418
+ () => extractRouteExports(source, FIXTURE_FILE),
419
+ new RegExp(`${FIXTURE_FILE_PATTERN}:\\d+: the template does not parse`)
420
+ )
421
+ })
422
+
423
+ test('syntax Next.js parses but the installed TypeScript does not (a source phase import) is read instead of rejected', async () => {
424
+ const source = "import source wasm from './module.wasm'\nexport const runtime = 'edge'\nexport default wasm\n"
425
+ for (const file of ['/virtual/theme/templates/(public)/page.ts', FIXTURE_FILE]) {
426
+ const { segmentConfig, hasDefaultExport } = await extractRouteExports(source, file)
427
+ assert.deepEqual(segmentConfig, { runtime: 'edge' }, file)
428
+ assert.equal(hasDefaultExport, true, file)
429
+ }
430
+ })
@@ -0,0 +1,41 @@
1
+ import { test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { generateScheduledActionsRegistry } from '../generators/scheduled-actions-registry.mjs'
5
+
6
+ const theme = (name) => ({
7
+ name,
8
+ hasScheduledActions: true,
9
+ scheduledActionsPath: `@/contents/themes/${name}/lib/scheduled-actions`,
10
+ })
11
+
12
+ test('generates a registry for a theme with scheduled actions', () => {
13
+ const out = generateScheduledActionsRegistry([theme('default')], {})
14
+
15
+ assert.match(out, /import \* as defaultScheduledActions from '@\/contents\/themes\/default\/lib\/scheduled-actions'/)
16
+ assert.match(out, /'default': \{/)
17
+ assert.match(out, /registerAllHandlers: defaultScheduledActions\.registerAllHandlers/)
18
+ })
19
+
20
+ test('a theme name that is not a valid identifier still produces one', () => {
21
+ const out = generateScheduledActionsRegistry([theme('7startups'), theme('my-theme')], {})
22
+
23
+ assert.match(out, /import \* as _7startupsScheduledActions from/)
24
+ assert.match(out, /import \* as my_themeScheduledActions from/)
25
+ assert.doesNotMatch(out, /import \* as 7startups/)
26
+ })
27
+
28
+ // The generated file is TypeScript shipped to the project: a build-script import
29
+ // leaking into it would not resolve there.
30
+ test('does not leak the generator\'s own imports into the generated file', () => {
31
+ const out = generateScheduledActionsRegistry([theme('default')], {})
32
+
33
+ assert.doesNotMatch(out, /identifier\.mjs/)
34
+ })
35
+
36
+ test('a project with no scheduled actions still gets a valid registry', () => {
37
+ const out = generateScheduledActionsRegistry([{ name: 'starter', hasScheduledActions: false }], {})
38
+
39
+ assert.match(out, /SCHEDULED_ACTIONS_REGISTRY/)
40
+ assert.doesNotMatch(out, /starterScheduledActions/)
41
+ })