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

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 (456) hide show
  1. package/dist/components/api/keys/CreateApiKeyDialog.d.ts.map +1 -1
  2. package/dist/components/api/keys/CreateApiKeyDialog.js +2 -1
  3. package/dist/components/app/layouts/PublicFooter.d.ts.map +1 -1
  4. package/dist/components/app/layouts/PublicFooter.js +2 -23
  5. package/dist/components/app/misc/ThemeToggle.d.ts.map +1 -1
  6. package/dist/components/app/misc/ThemeToggle.js +2 -1
  7. package/dist/components/auth/forms/LoginForm.js +1 -1
  8. package/dist/components/auth/forms/SignupForm.d.ts.map +1 -1
  9. package/dist/components/auth/forms/SignupForm.js +2 -1
  10. package/dist/components/auth/pages/AuthErrorPage.d.ts.map +1 -1
  11. package/dist/components/auth/pages/AuthErrorPage.js +2 -1
  12. package/dist/components/billing/ManageBillingButton.d.ts.map +1 -1
  13. package/dist/components/billing/ManageBillingButton.js +2 -1
  14. package/dist/components/dashboard/block-editor/array-field.d.ts.map +1 -1
  15. package/dist/components/dashboard/block-editor/array-field.js +2 -1
  16. package/dist/components/dashboard/block-editor/block-picker.d.ts.map +1 -1
  17. package/dist/components/dashboard/block-editor/block-picker.js +2 -1
  18. package/dist/components/dashboard/block-editor/builder-editor-view.d.ts.map +1 -1
  19. package/dist/components/dashboard/block-editor/builder-editor-view.js +3 -2
  20. package/dist/components/dashboard/block-editor/config-panel.d.ts.map +1 -1
  21. package/dist/components/dashboard/block-editor/config-panel.js +3 -2
  22. package/dist/components/dashboard/block-editor/dynamic-form.d.ts.map +1 -1
  23. package/dist/components/dashboard/block-editor/dynamic-form.js +2 -1
  24. package/dist/components/dashboard/block-editor/entity-fields-sidebar.d.ts.map +1 -1
  25. package/dist/components/dashboard/block-editor/entity-fields-sidebar.js +3 -2
  26. package/dist/components/dashboard/block-editor/pattern-reference-preview.d.ts.map +1 -1
  27. package/dist/components/dashboard/block-editor/pattern-reference-preview.js +2 -1
  28. package/dist/components/dashboard/layouts/TopNavbar.d.ts.map +1 -1
  29. package/dist/components/dashboard/layouts/TopNavbar.js +4 -3
  30. package/dist/components/dashboard/misc/SearchDropdown.d.ts.map +1 -1
  31. package/dist/components/dashboard/misc/SearchDropdown.js +2 -1
  32. package/dist/components/dashboard/mobile/MobileTopBar.d.ts.map +1 -1
  33. package/dist/components/dashboard/mobile/MobileTopBar.js +2 -1
  34. package/dist/components/devtools/BlockDetailViewer.d.ts.map +1 -1
  35. package/dist/components/devtools/BlockDetailViewer.js +2 -1
  36. package/dist/components/devtools/BlocksViewer.d.ts.map +1 -1
  37. package/dist/components/devtools/BlocksViewer.js +2 -1
  38. package/dist/components/devtools/ConfigViewer.d.ts.map +1 -1
  39. package/dist/components/devtools/ConfigViewer.js +3 -2
  40. package/dist/components/devtools/TestCasesViewer.d.ts.map +1 -1
  41. package/dist/components/devtools/TestCasesViewer.js +3 -2
  42. package/dist/components/devtools/TestCoverageDashboard.d.ts.map +1 -1
  43. package/dist/components/devtools/TestCoverageDashboard.js +2 -1
  44. package/dist/components/devtools/api-explorer/ApiDocsModal.d.ts.map +1 -1
  45. package/dist/components/devtools/api-explorer/ApiDocsModal.js +0 -0
  46. package/dist/components/devtools/api-explorer/ApiExplorer.d.ts.map +1 -1
  47. package/dist/components/devtools/api-explorer/ApiExplorer.js +2 -1
  48. package/dist/components/devtools/scheduled-actions/actions-table.d.ts.map +1 -1
  49. package/dist/components/devtools/scheduled-actions/actions-table.js +3 -2
  50. package/dist/components/docs/docs-sidebar.d.ts.map +1 -1
  51. package/dist/components/docs/docs-sidebar.js +3 -2
  52. package/dist/components/entities/EntityFieldRenderer.d.ts.map +1 -1
  53. package/dist/components/entities/EntityFieldRenderer.js +2 -1
  54. package/dist/components/entities/EntityList.d.ts.map +1 -1
  55. package/dist/components/entities/EntityList.js +2 -1
  56. package/dist/components/entities/EntityTable.d.ts.map +1 -1
  57. package/dist/components/entities/EntityTable.js +3 -2
  58. package/dist/components/entities/ErrorBoundary.d.ts.map +1 -1
  59. package/dist/components/entities/ErrorBoundary.js +2 -1
  60. package/dist/components/entities/wrappers/EntityDetailWrapper.d.ts.map +1 -1
  61. package/dist/components/entities/wrappers/EntityDetailWrapper.js +3 -2
  62. package/dist/components/media/MediaCard.d.ts.map +1 -1
  63. package/dist/components/media/MediaCard.js +2 -1
  64. package/dist/components/media/MediaDetailPanel.d.ts.map +1 -1
  65. package/dist/components/media/MediaDetailPanel.js +2 -1
  66. package/dist/components/media/MediaList.d.ts.map +1 -1
  67. package/dist/components/media/MediaList.js +2 -1
  68. package/dist/components/media/MediaSelector.d.ts.map +1 -1
  69. package/dist/components/media/MediaSelector.js +2 -1
  70. package/dist/components/media/MediaUploadZone.d.ts.map +1 -1
  71. package/dist/components/media/MediaUploadZone.js +2 -1
  72. package/dist/components/public/entities/PublicEntityDetail.d.ts.map +1 -1
  73. package/dist/components/public/entities/PublicEntityDetail.js +2 -1
  74. package/dist/components/teams/TeamPendingInvitations.d.ts.map +1 -1
  75. package/dist/components/teams/TeamPendingInvitations.js +3 -2
  76. package/dist/components/ui/accordion.js +1 -1
  77. package/dist/components/ui/audio-upload.d.ts.map +1 -1
  78. package/dist/components/ui/audio-upload.js +2 -1
  79. package/dist/components/ui/avatar.d.ts +17 -1
  80. package/dist/components/ui/avatar.d.ts.map +1 -1
  81. package/dist/components/ui/avatar.js +29 -2
  82. package/dist/components/ui/badge.js +1 -1
  83. package/dist/components/ui/breadcrumb.d.ts +13 -0
  84. package/dist/components/ui/breadcrumb.d.ts.map +1 -1
  85. package/dist/components/ui/breadcrumb.js +23 -10
  86. package/dist/components/ui/button.js +1 -1
  87. package/dist/components/ui/card.js +1 -1
  88. package/dist/components/ui/checkbox.js +1 -1
  89. package/dist/components/ui/file-upload.d.ts.map +1 -1
  90. package/dist/components/ui/file-upload.js +2 -1
  91. package/dist/components/ui/image-upload.d.ts.map +1 -1
  92. package/dist/components/ui/image-upload.js +4 -3
  93. package/dist/components/ui/input.js +1 -1
  94. package/dist/components/ui/label.js +1 -1
  95. package/dist/components/ui/optimized-image.d.ts.map +1 -1
  96. package/dist/components/ui/optimized-image.js +5 -0
  97. package/dist/components/ui/pagination.d.ts +1 -1
  98. package/dist/components/ui/pagination.d.ts.map +1 -1
  99. package/dist/components/ui/pagination.js +3 -0
  100. package/dist/components/ui/progress.js +1 -1
  101. package/dist/components/ui/relation-display.d.ts.map +1 -1
  102. package/dist/components/ui/relation-display.js +3 -2
  103. package/dist/components/ui/separator.js +1 -1
  104. package/dist/components/ui/simple-relation-select.d.ts.map +1 -1
  105. package/dist/components/ui/simple-relation-select.js +7 -6
  106. package/dist/components/ui/skeleton.js +1 -1
  107. package/dist/components/ui/slider.js +1 -1
  108. package/dist/components/ui/switch.js +1 -1
  109. package/dist/components/ui/tabs.js +1 -1
  110. package/dist/components/ui/textarea.js +1 -1
  111. package/dist/components/ui/user-display.d.ts.map +1 -1
  112. package/dist/components/ui/user-display.js +2 -1
  113. package/dist/components/ui/user-select.d.ts.map +1 -1
  114. package/dist/components/ui/user-select.js +2 -1
  115. package/dist/components/ui/video-upload.d.ts.map +1 -1
  116. package/dist/components/ui/video-upload.js +3 -2
  117. package/dist/components/users/tables/UsersTable.d.ts.map +1 -1
  118. package/dist/components/users/tables/UsersTable.js +3 -2
  119. package/dist/contexts/SubscriptionContext.d.ts.map +1 -1
  120. package/dist/contexts/SubscriptionContext.js +4 -3
  121. package/dist/contexts/TeamContext.d.ts.map +1 -1
  122. package/dist/contexts/TeamContext.js +3 -2
  123. package/dist/hooks/useAccountPreferencesSync.d.ts.map +1 -1
  124. package/dist/hooks/useAccountPreferencesSync.js +2 -1
  125. package/dist/hooks/useApiExplorerNavigation.d.ts.map +1 -1
  126. package/dist/hooks/useApiExplorerNavigation.js +2 -1
  127. package/dist/hooks/useAuth.d.ts.map +1 -1
  128. package/dist/hooks/useAuth.js +3 -2
  129. package/dist/hooks/useEnsureUserMetadata.d.ts.map +1 -1
  130. package/dist/hooks/useEnsureUserMetadata.js +3 -2
  131. package/dist/hooks/useEntityWithMeta.d.ts.map +1 -1
  132. package/dist/hooks/useEntityWithMeta.js +3 -2
  133. package/dist/hooks/useInvoice.d.ts.map +1 -1
  134. package/dist/hooks/useInvoice.js +2 -1
  135. package/dist/hooks/useInvoices.d.ts.map +1 -1
  136. package/dist/hooks/useInvoices.js +2 -1
  137. package/dist/hooks/useMedia.d.ts.map +1 -1
  138. package/dist/hooks/useMedia.js +10 -9
  139. package/dist/hooks/useMediaUpload.d.ts.map +1 -1
  140. package/dist/hooks/useMediaUpload.js +2 -1
  141. package/dist/hooks/useMembership.d.ts.map +1 -1
  142. package/dist/hooks/useMembership.js +3 -2
  143. package/dist/hooks/usePatternUsages.d.ts.map +1 -1
  144. package/dist/hooks/usePatternUsages.js +3 -2
  145. package/dist/hooks/usePrefetch.d.ts.map +1 -1
  146. package/dist/hooks/usePrefetch.js +3 -2
  147. package/dist/hooks/useQuota.d.ts.map +1 -1
  148. package/dist/hooks/useQuota.js +2 -1
  149. package/dist/hooks/useTeamInvitations.d.ts.map +1 -1
  150. package/dist/hooks/useTeamInvitations.js +4 -3
  151. package/dist/hooks/useTeamMembers.d.ts.map +1 -1
  152. package/dist/hooks/useTeamMembers.js +5 -4
  153. package/dist/hooks/useTeamUpdate.d.ts.map +1 -1
  154. package/dist/hooks/useTeamUpdate.js +2 -1
  155. package/dist/hooks/useUserProfile.d.ts.map +1 -1
  156. package/dist/hooks/useUserProfile.js +3 -2
  157. package/dist/hooks/useUserSettings.d.ts.map +1 -1
  158. package/dist/hooks/useUserSettings.js +3 -2
  159. package/dist/lib/actions/entity.actions.d.ts.map +1 -1
  160. package/dist/lib/actions/entity.actions.js +2 -1
  161. package/dist/lib/api/entities.d.ts.map +1 -1
  162. package/dist/lib/api/entities.js +4 -3
  163. package/dist/lib/auth/verify-email-link.d.ts +10 -0
  164. package/dist/lib/auth/verify-email-link.d.ts.map +1 -0
  165. package/dist/lib/auth/verify-email-link.js +14 -0
  166. package/dist/lib/auth-client.d.ts +2 -0
  167. package/dist/lib/auth-client.d.ts.map +1 -1
  168. package/dist/lib/auth-client.js +4 -0
  169. package/dist/lib/auth.d.ts.map +1 -1
  170. package/dist/lib/auth.js +10 -2
  171. package/dist/lib/base-path.d.ts +85 -0
  172. package/dist/lib/base-path.d.ts.map +1 -0
  173. package/dist/lib/base-path.js +79 -0
  174. package/dist/lib/billing/schema.d.ts +1 -1
  175. package/dist/lib/billing/schema.d.ts.map +1 -1
  176. package/dist/lib/billing/schema.js +1 -1
  177. package/dist/lib/blocks/sanitize-html.d.ts.map +1 -1
  178. package/dist/lib/blocks/sanitize-html.js +18 -2
  179. package/dist/lib/cache/revalidate-tag.d.ts +14 -0
  180. package/dist/lib/cache/revalidate-tag.d.ts.map +1 -0
  181. package/dist/lib/cache/revalidate-tag.js +8 -0
  182. package/dist/lib/config/dashboard-schema.d.ts +1 -1
  183. package/dist/lib/config/dashboard-schema.d.ts.map +1 -1
  184. package/dist/lib/config/dashboard-schema.js +1 -1
  185. package/dist/lib/config/types.d.ts +36 -16
  186. package/dist/lib/config/types.d.ts.map +1 -1
  187. package/dist/lib/docs/access.d.ts +28 -0
  188. package/dist/lib/docs/access.d.ts.map +1 -0
  189. package/dist/lib/docs/access.js +16 -0
  190. package/dist/lib/docs/parser.d.ts.map +1 -1
  191. package/dist/lib/docs/parser.js +2 -1
  192. package/dist/lib/docs/remark-doc-links.d.ts +14 -0
  193. package/dist/lib/docs/remark-doc-links.d.ts.map +1 -0
  194. package/dist/lib/docs/remark-doc-links.js +38 -0
  195. package/dist/lib/entities/api-generator.d.ts +1 -1
  196. package/dist/lib/entities/api-generator.d.ts.map +1 -1
  197. package/dist/lib/entities/api-generator.js +1 -1
  198. package/dist/lib/entities/schema-generator.d.ts +1 -1
  199. package/dist/lib/entities/schema-generator.d.ts.map +1 -1
  200. package/dist/lib/entities/schema-generator.js +1 -1
  201. package/dist/lib/mcp/engine.js +1 -1
  202. package/dist/lib/mcp/schema-builder.d.ts +1 -1
  203. package/dist/lib/mcp/schema-builder.d.ts.map +1 -1
  204. package/dist/lib/mcp/schema-builder.js +1 -1
  205. package/dist/lib/mcp/tool-generator.js +1 -1
  206. package/dist/lib/mcp/types.d.ts +1 -1
  207. package/dist/lib/mcp/types.d.ts.map +1 -1
  208. package/dist/lib/media/schemas.d.ts +1 -1
  209. package/dist/lib/media/schemas.d.ts.map +1 -1
  210. package/dist/lib/media/schemas.js +1 -1
  211. package/dist/lib/middleware/index.d.ts +5 -1
  212. package/dist/lib/middleware/index.d.ts.map +1 -1
  213. package/dist/lib/middleware/index.js +3 -2
  214. package/dist/lib/services/meta.service.d.ts +60 -0
  215. package/dist/lib/services/meta.service.d.ts.map +1 -1
  216. package/dist/lib/services/meta.service.js +132 -12
  217. package/dist/lib/services/middleware.service.d.ts.map +1 -1
  218. package/dist/lib/services/middleware.service.js +3 -2
  219. package/dist/lib/teams/schema.d.ts +1 -1
  220. package/dist/lib/teams/schema.d.ts.map +1 -1
  221. package/dist/lib/teams/schema.js +1 -1
  222. package/dist/lib/teams/teams.api.d.ts.map +1 -1
  223. package/dist/lib/teams/teams.api.js +4 -3
  224. package/dist/lib/user-data-client.d.ts.map +1 -1
  225. package/dist/lib/user-data-client.js +3 -2
  226. package/dist/lib/validation/invoices.d.ts +1 -1
  227. package/dist/lib/validation/invoices.d.ts.map +1 -1
  228. package/dist/lib/validation/invoices.js +1 -1
  229. package/dist/lib/validation-schemas.d.ts +1 -1
  230. package/dist/lib/validation-schemas.d.ts.map +1 -1
  231. package/dist/lib/validation-schemas.js +1 -1
  232. package/dist/lib/validation.d.ts +1 -1
  233. package/dist/lib/validation.d.ts.map +1 -1
  234. package/dist/lib/validation.js +1 -1
  235. package/dist/messages/de/footer.json +0 -1
  236. package/dist/messages/de/index.d.ts +0 -1
  237. package/dist/messages/de/index.d.ts.map +1 -1
  238. package/dist/messages/en/footer.json +0 -1
  239. package/dist/messages/en/index.d.ts +0 -1
  240. package/dist/messages/en/index.d.ts.map +1 -1
  241. package/dist/messages/es/footer.json +0 -1
  242. package/dist/messages/es/index.d.ts +0 -1
  243. package/dist/messages/es/index.d.ts.map +1 -1
  244. package/dist/messages/fr/footer.json +0 -1
  245. package/dist/messages/fr/index.d.ts +0 -1
  246. package/dist/messages/fr/index.d.ts.map +1 -1
  247. package/dist/messages/it/footer.json +0 -1
  248. package/dist/messages/it/index.d.ts +0 -1
  249. package/dist/messages/it/index.d.ts.map +1 -1
  250. package/dist/messages/pt/footer.json +0 -1
  251. package/dist/messages/pt/index.d.ts +0 -1
  252. package/dist/messages/pt/index.d.ts.map +1 -1
  253. package/dist/templates/app/(auth)/accept-invite/[token]/page.tsx +4 -3
  254. package/dist/templates/app/(auth)/verify-email/page.tsx +4 -3
  255. package/dist/templates/app/(public)/docs/[section]/[page]/page.tsx +11 -0
  256. package/dist/templates/app/api/auth/[...all]/route.ts +15 -14
  257. package/dist/templates/app/api/internal/user-metadata/route.ts +6 -8
  258. package/dist/templates/app/api/user/plan-flags/route.ts +1 -1
  259. package/dist/templates/app/api/user/profile/route.ts +12 -17
  260. package/dist/templates/app/api/v1/api-keys/route.ts +1 -1
  261. package/dist/templates/app/api/v1/auth/signup-with-invite/route.ts +2 -1
  262. package/dist/templates/app/api/v1/billing/cancel/route.ts +1 -1
  263. package/dist/templates/app/api/v1/billing/change-plan/route.ts +1 -1
  264. package/dist/templates/app/api/v1/billing/check-action/route.ts +1 -1
  265. package/dist/templates/app/api/v1/billing/checkout/route.ts +1 -1
  266. package/dist/templates/app/api/v1/blocks/validate/route.ts +1 -1
  267. package/dist/templates/app/api/v1/media/[id]/tags/route.ts +1 -1
  268. package/dist/templates/app/api/v1/post-categories/[id]/route.ts +1 -1
  269. package/dist/templates/app/api/v1/post-categories/route.ts +1 -1
  270. package/dist/templates/app/api/v1/teams/[teamId]/members/route.ts +2 -1
  271. package/dist/templates/app/api/v1/teams/[teamId]/subscription/route.ts +3 -5
  272. package/dist/templates/app/api/v1/teams/docs.md +4 -9
  273. package/dist/templates/app/api/v1/teams/switch/route.ts +1 -1
  274. package/dist/templates/app/api/v1/users/[id]/meta/[key]/route.ts +1 -1
  275. package/dist/templates/app/api/v1/users/[id]/route.ts +1 -1
  276. package/dist/templates/app/api/v1/users/route.ts +1 -1
  277. package/dist/templates/app/dashboard/(main)/media/page.tsx +3 -1
  278. package/dist/templates/app/dashboard/settings/api-keys/page.tsx +5 -4
  279. package/dist/templates/app/dashboard/settings/profile/page.tsx +3 -2
  280. package/dist/templates/app/devtools/scheduled-actions/page.tsx +2 -1
  281. package/dist/templates/app/superadmin/docs/[section]/[page]/page.tsx +16 -0
  282. package/dist/templates/app/superadmin/subscriptions/page.tsx +3 -2
  283. package/dist/templates/app/superadmin/teams/[teamId]/page.tsx +5 -4
  284. package/dist/templates/app/superadmin/users/[userId]/page.tsx +2 -1
  285. package/dist/templates/app/superadmin/users/page.tsx +2 -1
  286. package/dist/templates/blocks/cta-section/component.tsx +3 -2
  287. package/dist/templates/blocks/cta-section/schema.ts +1 -1
  288. package/dist/templates/blocks/features-grid/component.tsx +2 -1
  289. package/dist/templates/blocks/features-grid/schema.ts +1 -1
  290. package/dist/templates/blocks/hero/component.tsx +3 -2
  291. package/dist/templates/blocks/hero/schema.ts +1 -1
  292. package/dist/templates/blocks/testimonials/component.tsx +2 -1
  293. package/dist/templates/blocks/testimonials/schema.ts +1 -1
  294. package/dist/templates/blocks/text-content/component.tsx +2 -1
  295. package/dist/templates/blocks/text-content/schema.ts +1 -1
  296. package/dist/templates/ci-workflows/README.md +1 -1
  297. package/dist/templates/ci-workflows/github/cypress-regression.yml +1 -1
  298. package/dist/templates/ci-workflows/github/cypress-smoke.yml +1 -1
  299. package/dist/templates/contents/plugins/starter/plugin/api/example/route.ts.template +1 -1
  300. package/dist/templates/contents/themes/starter/blocks/cta-section/component.tsx +3 -2
  301. package/dist/templates/contents/themes/starter/blocks/cta-section/schema.ts +1 -1
  302. package/dist/templates/contents/themes/starter/blocks/features-grid/component.tsx +2 -1
  303. package/dist/templates/contents/themes/starter/blocks/features-grid/schema.ts +1 -1
  304. package/dist/templates/contents/themes/starter/blocks/hero/component.tsx +3 -2
  305. package/dist/templates/contents/themes/starter/blocks/hero/schema.ts +1 -1
  306. package/dist/templates/contents/themes/starter/blocks/testimonials/component.tsx +3 -2
  307. package/dist/templates/contents/themes/starter/blocks/testimonials/schema.ts +1 -1
  308. package/dist/templates/contents/themes/starter/blocks/text-content/component.tsx +2 -1
  309. package/dist/templates/contents/themes/starter/blocks/text-content/schema.ts +1 -1
  310. package/dist/templates/contents/themes/starter/entities/posts/components/post-header.tsx +2 -1
  311. package/dist/templates/contents/themes/starter/lib/hooks/useAnalyticsData.ts +2 -1
  312. package/dist/templates/contents/themes/starter/lib/hooks/useTaskStats.ts +2 -1
  313. package/dist/templates/eslint.config.mjs +133 -9
  314. package/dist/templates/features/blog/blocks/post-content/component.tsx +2 -1
  315. package/dist/templates/features/blog/blocks/post-content/schema.ts +1 -1
  316. package/dist/templates/features/blog/entities/posts/components/post-header.tsx +2 -1
  317. package/dist/templates/features/pages/blocks/hero/component.tsx +3 -2
  318. package/dist/templates/features/pages/blocks/hero/schema.ts +1 -1
  319. package/dist/templates/next.config.mjs +50 -29
  320. package/dist/templates/proxy.ts +100 -21
  321. package/dist/templates/public/sw.js +2 -2
  322. package/dist/templates/scripts/cy-run-prod.cjs +5 -5
  323. package/dist/types/blocks.d.ts +1 -1
  324. package/dist/types/blocks.d.ts.map +1 -1
  325. package/dist/types/blocks.js +1 -1
  326. package/package.json +9 -5
  327. package/scripts/build/docs-registry.mjs +7 -7
  328. package/scripts/build/registry/__tests__/billing-registry.test.mjs +43 -0
  329. package/scripts/build/registry/__tests__/build-output.test.mjs +209 -0
  330. package/scripts/build/registry/__tests__/copy-race-child.mjs +14 -0
  331. package/scripts/build/registry/__tests__/direct-fs-writes.mjs +203 -0
  332. package/scripts/build/registry/__tests__/globals-css-tag.test.mjs +66 -0
  333. package/scripts/build/registry/__tests__/icons-discovery.test.mjs +530 -29
  334. package/scripts/build/registry/__tests__/page-generator.test.mjs +211 -37
  335. package/scripts/build/registry/__tests__/published-signatures.test.mjs +239 -0
  336. package/scripts/build/registry/__tests__/route-file-rule.test.mjs +170 -0
  337. package/scripts/build/registry/__tests__/safe-fs.test.mjs +333 -0
  338. package/scripts/build/registry/__tests__/template-analysis.test.mjs +142 -0
  339. package/scripts/build/registry/__tests__/template-registry.test.mjs +279 -9
  340. package/scripts/build/registry/__tests__/templates-plan.test.mjs +128 -0
  341. package/scripts/build/registry/__tests__/templates-tree.test.mjs +367 -0
  342. package/scripts/build/registry/__tests__/write-places.test.mjs +425 -0
  343. package/scripts/build/registry/config.mjs +6 -65
  344. package/scripts/build/registry/discovery/icons.mjs +498 -43
  345. package/scripts/build/registry/discovery/templates.mjs +1 -1
  346. package/scripts/build/registry/generators/billing-registry.mjs +1 -1
  347. package/scripts/build/registry/generators/docs-registry.mjs +3 -4
  348. package/scripts/build/registry/generators/feature-registry.mjs +1 -1
  349. package/scripts/build/registry/generators/permissions-registry.mjs +2 -2
  350. package/scripts/build/registry/generators/template-registry.mjs +65 -49
  351. package/scripts/build/registry/index.mjs +1 -2
  352. package/scripts/build/registry/post-build/index.mjs +1 -1
  353. package/scripts/build/registry/post-build/own-gitignores.mjs +220 -0
  354. package/scripts/build/registry/post-build/page-generator.mjs +512 -268
  355. package/scripts/build/registry/post-build/route-cleanup.mjs +16 -13
  356. package/scripts/build/registry/post-build/test-fixtures.mjs +26 -8
  357. package/scripts/build/registry/project-mode.mjs +76 -0
  358. package/scripts/build/registry/shared/typescript-compiler.mjs +77 -0
  359. package/scripts/build/registry/validate-env.test.mjs +12 -8
  360. package/scripts/build/registry/watch.mjs +3 -53
  361. package/scripts/build/registry/write-places.mjs +228 -0
  362. package/scripts/build/registry.mjs +53 -30
  363. package/scripts/build/safe-fs.mjs +279 -0
  364. package/scripts/build/templates-plan.mjs +44 -0
  365. package/scripts/build/theme.mjs +46 -42
  366. package/scripts/build/update-tsconfig.mjs +16 -15
  367. package/scripts/db/cluster-changes.mjs +205 -0
  368. package/scripts/db/connection-time-limits.mjs +53 -0
  369. package/scripts/db/inspect-server.mjs +108 -0
  370. package/scripts/db/migration-time-limit.mjs +695 -0
  371. package/scripts/db/run-migrations.mjs +46 -41
  372. package/scripts/db/ssl-config.mjs +194 -0
  373. package/scripts/db/theme-location.mjs +69 -0
  374. package/scripts/db/verify-tables.mjs +3 -8
  375. package/scripts/db/verify-teams.mjs +2 -6
  376. package/scripts/db/verify-theme-migrations.mjs +169 -0
  377. package/scripts/maintenance/core-updater.mjs +1003 -0
  378. package/scripts/maintenance/step-guard.mjs +74 -0
  379. package/scripts/maintenance/update-core.mjs +14 -831
  380. package/scripts/utils/console-guard.mjs +13 -0
  381. package/scripts/utils/generated-tag.mjs +66 -0
  382. package/scripts/utils/index.mjs +9 -1
  383. package/scripts/utils/logging.mjs +140 -0
  384. package/templates/app/(auth)/accept-invite/[token]/page.tsx +4 -3
  385. package/templates/app/(auth)/verify-email/page.tsx +4 -3
  386. package/templates/app/(public)/docs/[section]/[page]/page.tsx +11 -0
  387. package/templates/app/api/auth/[...all]/route.ts +15 -14
  388. package/templates/app/api/internal/user-metadata/route.ts +6 -8
  389. package/templates/app/api/user/plan-flags/route.ts +1 -1
  390. package/templates/app/api/user/profile/route.ts +12 -17
  391. package/templates/app/api/v1/api-keys/route.ts +1 -1
  392. package/templates/app/api/v1/auth/signup-with-invite/route.ts +2 -1
  393. package/templates/app/api/v1/billing/cancel/route.ts +1 -1
  394. package/templates/app/api/v1/billing/change-plan/route.ts +1 -1
  395. package/templates/app/api/v1/billing/check-action/route.ts +1 -1
  396. package/templates/app/api/v1/billing/checkout/route.ts +1 -1
  397. package/templates/app/api/v1/blocks/validate/route.ts +1 -1
  398. package/templates/app/api/v1/media/[id]/tags/route.ts +1 -1
  399. package/templates/app/api/v1/post-categories/[id]/route.ts +1 -1
  400. package/templates/app/api/v1/post-categories/route.ts +1 -1
  401. package/templates/app/api/v1/teams/[teamId]/members/route.ts +2 -1
  402. package/templates/app/api/v1/teams/[teamId]/subscription/route.ts +3 -5
  403. package/templates/app/api/v1/teams/docs.md +4 -9
  404. package/templates/app/api/v1/teams/switch/route.ts +1 -1
  405. package/templates/app/api/v1/users/[id]/meta/[key]/route.ts +1 -1
  406. package/templates/app/api/v1/users/[id]/route.ts +1 -1
  407. package/templates/app/api/v1/users/route.ts +1 -1
  408. package/templates/app/dashboard/(main)/media/page.tsx +3 -1
  409. package/templates/app/dashboard/settings/api-keys/page.tsx +5 -4
  410. package/templates/app/dashboard/settings/profile/page.tsx +3 -2
  411. package/templates/app/devtools/scheduled-actions/page.tsx +2 -1
  412. package/templates/app/superadmin/docs/[section]/[page]/page.tsx +16 -0
  413. package/templates/app/superadmin/subscriptions/page.tsx +3 -2
  414. package/templates/app/superadmin/teams/[teamId]/page.tsx +5 -4
  415. package/templates/app/superadmin/users/[userId]/page.tsx +2 -1
  416. package/templates/app/superadmin/users/page.tsx +2 -1
  417. package/templates/blocks/cta-section/component.tsx +3 -2
  418. package/templates/blocks/cta-section/schema.ts +1 -1
  419. package/templates/blocks/features-grid/component.tsx +2 -1
  420. package/templates/blocks/features-grid/schema.ts +1 -1
  421. package/templates/blocks/hero/component.tsx +3 -2
  422. package/templates/blocks/hero/schema.ts +1 -1
  423. package/templates/blocks/testimonials/component.tsx +2 -1
  424. package/templates/blocks/testimonials/schema.ts +1 -1
  425. package/templates/blocks/text-content/component.tsx +2 -1
  426. package/templates/blocks/text-content/schema.ts +1 -1
  427. package/templates/ci-workflows/README.md +1 -1
  428. package/templates/ci-workflows/github/cypress-regression.yml +1 -1
  429. package/templates/ci-workflows/github/cypress-smoke.yml +1 -1
  430. package/templates/contents/plugins/starter/plugin/api/example/route.ts.template +1 -1
  431. package/templates/contents/themes/starter/blocks/cta-section/component.tsx +3 -2
  432. package/templates/contents/themes/starter/blocks/cta-section/schema.ts +1 -1
  433. package/templates/contents/themes/starter/blocks/features-grid/component.tsx +2 -1
  434. package/templates/contents/themes/starter/blocks/features-grid/schema.ts +1 -1
  435. package/templates/contents/themes/starter/blocks/hero/component.tsx +3 -2
  436. package/templates/contents/themes/starter/blocks/hero/schema.ts +1 -1
  437. package/templates/contents/themes/starter/blocks/testimonials/component.tsx +3 -2
  438. package/templates/contents/themes/starter/blocks/testimonials/schema.ts +1 -1
  439. package/templates/contents/themes/starter/blocks/text-content/component.tsx +2 -1
  440. package/templates/contents/themes/starter/blocks/text-content/schema.ts +1 -1
  441. package/templates/contents/themes/starter/entities/posts/components/post-header.tsx +2 -1
  442. package/templates/contents/themes/starter/lib/hooks/useAnalyticsData.ts +2 -1
  443. package/templates/contents/themes/starter/lib/hooks/useTaskStats.ts +2 -1
  444. package/templates/eslint.config.mjs +133 -9
  445. package/templates/features/blog/blocks/post-content/component.tsx +2 -1
  446. package/templates/features/blog/blocks/post-content/schema.ts +1 -1
  447. package/templates/features/blog/entities/posts/components/post-header.tsx +2 -1
  448. package/templates/features/pages/blocks/hero/component.tsx +3 -2
  449. package/templates/features/pages/blocks/hero/schema.ts +1 -1
  450. package/templates/next.config.mjs +50 -29
  451. package/templates/proxy.ts +100 -21
  452. package/templates/public/sw.js +2 -2
  453. package/templates/scripts/cy-run-prod.cjs +5 -5
  454. package/tests/jest/__mocks__/@nextsparkjs/registries/template-registry.client.ts +11 -0
  455. package/tests/jest/__mocks__/next-server.js +10 -0
  456. package/scripts/build/docs.mjs +0 -392
@@ -0,0 +1,170 @@
1
+ /**
2
+ * One rule decides what becomes of a theme template's route file, for every
3
+ * generator core exposes that writes route files or registers template
4
+ * components (`routeFileAction` in page-generator.mjs):
5
+ * - a template the page generator writes a route file for gets one;
6
+ * - a template over a route the app already has gets none, and the generator
7
+ * goes on without failing;
8
+ * - a page whose route file would import a default export it doesn't have is
9
+ * rejected, and nothing is written.
10
+ *
11
+ * Each generator runs over the same cases, with an analysis and without one.
12
+ *
13
+ * Run: node --test packages/core/scripts/build/registry/__tests__/route-file-rule.test.mjs
14
+ */
15
+
16
+ import { test } from 'node:test'
17
+ import assert from 'node:assert/strict'
18
+ import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises'
19
+ import { existsSync } from 'node:fs'
20
+ import { tmpdir } from 'node:os'
21
+ import { join, dirname } from 'node:path'
22
+
23
+ import {
24
+ generateTemplateRegistry,
25
+ generateTemplateRegistryClient
26
+ } from '../generators/template-registry.mjs'
27
+ import { analyzeTemplates, generateMissingPages, generateTemplatePage } from '../post-build/page-generator.mjs'
28
+
29
+ const PAGE = 'export default function Page() { return null }\n'
30
+ const METADATA_ONLY = "export const metadata = { title: 'Docs' }\n"
31
+ const ROUTE_FILE = 'app/(templates)/docs/page.tsx'
32
+
33
+ /**
34
+ * - `route`: what the page generator does with the template's route file
35
+ * - `component`: how the registries register the template
36
+ */
37
+ const CASES = [
38
+ { name: 'a page.tsx with a default export, no app page', file: 'page.tsx', content: PAGE, appPage: false, route: 'write', component: 'deferred' },
39
+ { name: 'a page.tsx with a default export, over an app page', file: 'page.tsx', content: PAGE, appPage: true, route: 'skip', component: 'deferred' },
40
+ { name: 'a page.tsx with no default export, no app page', file: 'page.tsx', content: METADATA_ONLY, appPage: false, route: 'reject' },
41
+ { name: 'a page.tsx with no default export, over an app page', file: 'page.tsx', content: METADATA_ONLY, appPage: true, route: 'skip', component: 'none' },
42
+ { name: 'a page.meta.ts, no app page', file: 'page.meta.ts', content: METADATA_ONLY, appPage: false, route: 'reject' },
43
+ { name: 'a page.meta.ts, over an app page', file: 'page.meta.ts', content: METADATA_ONLY, appPage: true, route: 'skip', component: 'none' },
44
+ ]
45
+
46
+ async function writeProjectFile(root, relativePath, content) {
47
+ const absolutePath = join(root, relativePath)
48
+ await mkdir(dirname(absolutePath), { recursive: true })
49
+ await writeFile(absolutePath, content, 'utf8')
50
+ }
51
+
52
+ /** A project with the case's theme template for app/docs/page.tsx, and that app page when the case has one. */
53
+ async function projectFor(testCase) {
54
+ const root = await mkdtemp(join(tmpdir(), 'nextspark-route-file-rule-test-'))
55
+ const relativePath = `docs/${testCase.file}`
56
+ await writeProjectFile(root, join('contents/themes/testtheme/templates', relativePath), testCase.content)
57
+ if (testCase.appPage) {
58
+ await writeProjectFile(root, 'app/docs/page.tsx', PAGE)
59
+ }
60
+
61
+ const template = {
62
+ name: 'docs/page',
63
+ themeName: 'testtheme',
64
+ templateType: 'page',
65
+ fileName: testCase.file,
66
+ relativePath,
67
+ appPath: 'app/docs/page.tsx',
68
+ templatePath: `@/contents/themes/testtheme/templates/${relativePath}`,
69
+ priority: 10
70
+ }
71
+ const config = { outputDir: join(root, '.nextspark/registries'), projectRoot: root }
72
+
73
+ return { root, template, config, cleanup: () => rm(root, { recursive: true, force: true }) }
74
+ }
75
+
76
+ /**
77
+ * The analysis a caller hands a generator. For a template analyzeTemplates
78
+ * accepts, that is its analysis. analyzeTemplates rejects the others itself, so
79
+ * for those this is the entry it would record - which leaves the generator's
80
+ * own use of the rule as the thing under test.
81
+ */
82
+ async function analysisFor(testCase, template, config) {
83
+ if (testCase.route !== 'reject') {
84
+ return analyzeTemplates([template], config)
85
+ }
86
+ return new Map([
87
+ [template.templatePath, { segmentConfig: {}, moduleExports: ['metadata'], hasDefaultExport: false, generatesRoute: true }]
88
+ ])
89
+ }
90
+
91
+ /** Run `check` for every case, with and without an analysis, in a fresh project each time. */
92
+ async function forEachCase(check) {
93
+ for (const testCase of CASES) {
94
+ for (const withAnalysis of [false, true]) {
95
+ const label = `${testCase.name}, ${withAnalysis ? 'with' : 'without'} an analysis`
96
+ const project = await projectFor(testCase)
97
+ try {
98
+ const analysis = withAnalysis ? await analysisFor(testCase, project.template, project.config) : undefined
99
+ await check({ testCase, label, analysis, ...project })
100
+ } finally {
101
+ await project.cleanup()
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ test('generateTemplatePage writes a route file only where one is generated, and rejects one that would break', async () => {
108
+ await forEachCase(async ({ testCase, label, analysis, root, template }) => {
109
+ // generateTemplatePage resolves templates against the last project root generateMissingPages was given
110
+ await generateMissingPages([], { projectRoot: root })
111
+ const outputPath = join(root, ROUTE_FILE)
112
+
113
+ if (testCase.route === 'reject') {
114
+ await assert.rejects(() => generateTemplatePage(template, outputPath, analysis), /has no default export/, label)
115
+ assert.equal(existsSync(outputPath), false, label)
116
+ return
117
+ }
118
+
119
+ const result = await generateTemplatePage(template, outputPath, analysis)
120
+ assert.equal(result?.written, testCase.route === 'write', label)
121
+ assert.equal(existsSync(outputPath), testCase.route === 'write', label)
122
+ if (testCase.route === 'write') {
123
+ assert.match(await readFile(outputPath, 'utf8'), /import TemplateComponent from/, label)
124
+ }
125
+ })
126
+ })
127
+
128
+ test('generateMissingPages writes a route file only where one is generated, and rejects one that would break before writing anything', async () => {
129
+ await forEachCase(async ({ testCase, label, analysis, root, template, config }) => {
130
+ if (testCase.route === 'reject') {
131
+ await assert.rejects(() => generateMissingPages([template], config, analysis), /has no default export/, label)
132
+ assert.equal(existsSync(join(root, 'app/(templates)')), false, label)
133
+ return
134
+ }
135
+
136
+ await generateMissingPages([template], config, analysis)
137
+ assert.equal(existsSync(join(root, ROUTE_FILE)), testCase.route === 'write', label)
138
+ })
139
+ })
140
+
141
+ test('generateTemplateRegistry registers a component only for a template that has one, and rejects a route that would break', async () => {
142
+ await forEachCase(async ({ testCase, label, analysis, template, config }) => {
143
+ if (testCase.route === 'reject') {
144
+ await assert.rejects(() => generateTemplateRegistry([template], config, analysis), /has no default export/, label)
145
+ return
146
+ }
147
+
148
+ const out = await generateTemplateRegistry([template], config, analysis)
149
+ const component = testCase.component === 'deferred'
150
+ ? /component: lazyTemplate\('app\/docs\/page\.tsx'/
151
+ : /'app\/docs\/page\.tsx': \{\n\s*appPath: 'app\/docs\/page\.tsx',\n\s*component: null,/
152
+ assert.match(out, component, label)
153
+ })
154
+ })
155
+
156
+ test('generateTemplateRegistryClient registers a component only for a template that has one, and rejects a route that would break', async () => {
157
+ await forEachCase(async ({ testCase, label, analysis, template, config }) => {
158
+ if (testCase.route === 'reject') {
159
+ await assert.rejects(() => generateTemplateRegistryClient([template], config, analysis), /has no default export/, label)
160
+ return
161
+ }
162
+
163
+ const out = await generateTemplateRegistryClient([template], config, analysis)
164
+ if (testCase.component === 'deferred') {
165
+ assert.match(out, /'app\/docs\/page\.tsx': dynamic\(/, label)
166
+ } else {
167
+ assert.doesNotMatch(out, /'app\/docs\/page\.tsx'/, label)
168
+ }
169
+ })
170
+ })
@@ -0,0 +1,333 @@
1
+ /**
2
+ * Tests for safe-fs, the one way the generator changes the file system: each of
3
+ * its calls refuses a path out of its root, through a symlink or into a file
4
+ * with other hard links, and changes nothing anywhere when it does; the build's
5
+ * steps that write refuse through it even when the check the build runs first
6
+ * is not asked; and no file of the generator changes the file system any other
7
+ * way.
8
+ *
9
+ * Run: node --test packages/core/scripts/build/registry/__tests__/safe-fs.test.mjs
10
+ */
11
+
12
+ import { test } from 'node:test'
13
+ import assert from 'node:assert/strict'
14
+ import { createHash, randomBytes } from 'node:crypto'
15
+ import { execFileSync, spawn } from 'node:child_process'
16
+ import { once } from 'node:events'
17
+ import { chmod, link, lstat, mkdir, mkdtemp, open, readdir, readFile, readlink, realpath, rm, symlink, writeFile } from 'node:fs/promises'
18
+ import { existsSync } from 'node:fs'
19
+ import { tmpdir } from 'node:os'
20
+ import { dirname, join } from 'node:path'
21
+ import { fileURLToPath } from 'node:url'
22
+
23
+ import { COPY_BLOCK_SIZE, isUnsafeWrite, projectFiles, UNSAFE_WRITE, unsafeWriteProblem } from '../../safe-fs.mjs'
24
+ import { cleanupDeletedTemplate, cleanupOldRouteFiles, cleanupOrphanedTemplates } from '../post-build/route-cleanup.mjs'
25
+ import { generateTestBlocksJson, generateTestEntitiesJson } from '../post-build/test-fixtures.mjs'
26
+ import { generateMissingPages } from '../post-build/page-generator.mjs'
27
+ import { syncAppGlobalsCss } from '../../theme.mjs'
28
+ import { directFsWrites, directFsWritesIn, generatorFiles } from './direct-fs-writes.mjs'
29
+
30
+ const CORE_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..')
31
+ const RUNS_AS_ROOT = process.getuid?.() === 0
32
+
33
+ async function directory() {
34
+ const root = await mkdtemp(join(tmpdir(), 'nextspark-safe-fs-test-'))
35
+ return { root, cleanup: () => rm(root, { recursive: true, force: true }) }
36
+ }
37
+
38
+ async function writeIn(root, path, content = '') {
39
+ await mkdir(dirname(join(root, path)), { recursive: true })
40
+ await writeFile(join(root, path), content)
41
+ }
42
+
43
+ /** Every entry under `root`: a symlink's target, 'dir', or a file's sha256. */
44
+ async function snapshot(root) {
45
+ const entries = {}
46
+ const walk = async relative => {
47
+ for (const entry of await readdir(join(root, relative), { withFileTypes: true })) {
48
+ const path = relative ? `${relative}/${entry.name}` : entry.name
49
+ const stats = await lstat(join(root, path))
50
+ if (stats.isSymbolicLink()) entries[path] = `-> ${await readlink(join(root, path))}`
51
+ else if (stats.isDirectory()) {
52
+ entries[path] = 'dir'
53
+ await walk(path)
54
+ } else entries[path] = createHash('sha256').update(await readFile(join(root, path))).digest('hex')
55
+ }
56
+ }
57
+ await walk('')
58
+ return entries
59
+ }
60
+
61
+ /** What `call` throws, or null. */
62
+ async function thrown(call) {
63
+ try {
64
+ await call()
65
+ return null
66
+ } catch (error) {
67
+ return error
68
+ }
69
+ }
70
+
71
+ test('each call refuses a path through a symlink, out of the root or into a hard-linked file, and changes nothing inside or outside', async () => {
72
+ const project = await directory()
73
+ const outside = await directory()
74
+ try {
75
+ await writeIn(outside.root, 'target/file.txt', 'outside\n')
76
+ await writeIn(outside.root, 'target/v1/plugin/legacy/route.ts', '// Auto-generated Plugin Route Proxy\n')
77
+ await writeIn(outside.root, 'lone.txt', 'outside\n')
78
+ await writeIn(project.root, 'app/page.tsx', 'inside\n')
79
+ await symlink(join(outside.root, 'target'), join(project.root, 'app/linked'))
80
+ await symlink(join(outside.root, 'lone.txt'), join(project.root, 'app/lone.txt'))
81
+ await link(join(outside.root, 'lone.txt'), join(project.root, 'hard.txt'))
82
+ await writeIn(project.root, 'source.txt', 'source\n')
83
+
84
+ const files = projectFiles(project.root)
85
+ const cases = [
86
+ ['writeFileSync through a symlinked directory', () => files.writeFileSync(join(project.root, 'app/linked/file.txt'), 'x'), 'app/linked', 'is a symlink'],
87
+ ['writeFile onto a symlink', () => files.writeFile(join(project.root, 'app/lone.txt'), 'x'), 'app/lone.txt', 'is a symlink'],
88
+ ['writeFile into a hard-linked file', () => files.writeFile(join(project.root, 'hard.txt'), 'x'), 'hard.txt', 'has other hard links'],
89
+ ['writeFile with wx through a symlink', () => files.writeFile(join(project.root, 'app/linked/new.txt'), 'x', { flag: 'wx' }), 'app/linked', 'is a symlink'],
90
+ ['writeFile out of the root with ..', () => files.writeFile('app/linked/../../../escape.txt', 'x'), null, null],
91
+ ['writeFile to the root itself', () => files.writeFile(project.root, 'x'), '.', 'is the root itself, which is not changed'],
92
+ ['writeFile to an absolute path outside', () => files.writeFile(join(outside.root, 'lone.txt'), 'x'), null, null],
93
+ ['mkdirSync through a symlink', () => files.mkdirSync(join(project.root, 'app/linked/made'), { recursive: true }), 'app/linked', 'is a symlink'],
94
+ ['mkdtemp beside a path through a symlink', () => files.mkdtemp(join(project.root, 'app/linked/backup-')), 'app/linked', 'is a symlink'],
95
+ ['copyFile to a path through a symlink', () => files.copyFile(join(project.root, 'source.txt'), join(project.root, 'app/linked/copy.txt')), 'app/linked', 'is a symlink'],
96
+ ['rm of a tree through a symlink', () => files.rm(join(project.root, 'app/linked/v1/plugin/legacy'), { recursive: true, force: true }), 'app/linked', 'is a symlink'],
97
+ ['rm of the symlink itself', () => files.rm(join(project.root, 'app/linked'), { recursive: true, force: true }), 'app/linked', 'is a symlink'],
98
+ ['rmdir through a symlink', () => files.rmdir(join(project.root, 'app/linked/v1')), 'app/linked', 'is a symlink'],
99
+ ['unlink through a symlink', () => files.unlink(join(project.root, 'app/linked/file.txt')), 'app/linked', 'is a symlink'],
100
+ ['rename out through a symlink', () => files.rename(join(project.root, 'app/page.tsx'), join(project.root, 'app/linked/page.tsx')), 'app/linked', 'is a symlink'],
101
+ ['rename in from a symlink', () => files.rename(join(project.root, 'app/linked/file.txt'), join(project.root, 'app/file.txt')), 'app/linked', 'is a symlink'],
102
+ ['a path from the root through a symlink', () => files.writeFile('app/linked/file.txt', 'x'), 'app/linked', 'is a symlink'],
103
+ ]
104
+
105
+ const before = { project: await snapshot(project.root), outside: await snapshot(outside.root) }
106
+ const wrong = []
107
+ for (const [label, call, path, problem] of cases) {
108
+ const error = await thrown(call)
109
+ if (!isUnsafeWrite(error)) {
110
+ wrong.push(`${label}: ${error ? `threw ${error.code} ${error.message}` : 'did not refuse'}`)
111
+ continue
112
+ }
113
+ if (error.code !== UNSAFE_WRITE) wrong.push(`${label}: code ${error.code}`)
114
+ if (path !== null && (error.path !== path || error.problem !== problem)) wrong.push(`${label}: named ${error.path} ${error.problem}`)
115
+ if (path === null && !error.problem.startsWith('is outside')) wrong.push(`${label}: named ${error.path} ${error.problem}`)
116
+ if (!error.message.includes('nothing was written')) wrong.push(`${label}: the message doesn't say nothing was written`)
117
+ }
118
+ assert.deepEqual(wrong, [])
119
+ assert.deepEqual(await snapshot(outside.root), before.outside, 'nothing outside changed')
120
+ assert.deepEqual(await snapshot(project.root), before.project, 'nothing inside changed')
121
+ } finally {
122
+ await outside.cleanup()
123
+ await project.cleanup()
124
+ }
125
+ })
126
+
127
+ test('each call does its work inside the root, reached through a symlink above it or not', async () => {
128
+ const holder = await directory()
129
+ try {
130
+ const real = join(holder.root, 'real')
131
+ await mkdir(real)
132
+ await symlink(real, join(holder.root, 'alias'))
133
+
134
+ for (const root of [real, join(holder.root, 'alias')]) {
135
+ const files = projectFiles(root)
136
+ await files.mkdir(join(root, 'a/b'), { recursive: true })
137
+ await files.writeFile(join(root, 'a/b/c.txt'), 'one', 'utf8')
138
+ files.writeFileSync(join(root, 'a/b/c.txt'), 'two')
139
+ await assert.rejects(files.writeFile(join(root, 'a/b/c.txt'), 'three', { flag: 'wx' }), { code: 'EEXIST' })
140
+ await files.copyFile(join(root, 'a/b/c.txt'), join(root, 'a/copy.txt'))
141
+ const temp = await files.mkdtemp(join(root, 'a/backup-'))
142
+ await files.rename(join(root, 'a/copy.txt'), join(temp, 'moved.txt'))
143
+ assert.equal(await readFile(join(temp, 'moved.txt'), 'utf8'), 'two')
144
+ await files.unlink(join(temp, 'moved.txt'))
145
+ await files.rmdir(temp)
146
+ await files.rm(join(root, 'a'), { recursive: true, force: true })
147
+ assert.equal(existsSync(join(root, 'a')), false)
148
+ }
149
+
150
+ // A path spelled with another path to the root than the one it was given as
151
+ const files = projectFiles(join(holder.root, 'alias'))
152
+ await files.writeFile(join(real, 'spelled-real.txt'), 'x')
153
+ assert.equal(await readFile(join(real, 'spelled-real.txt'), 'utf8'), 'x')
154
+ await files.writeFile(join(await realpath(real), 'spelled-resolved.txt'), 'x')
155
+ assert.equal(await readFile(join(real, 'spelled-resolved.txt'), 'utf8'), 'x')
156
+ await symlink(real, join(real, 'self'))
157
+ assert.deepEqual(
158
+ unsafeWriteProblem(join(holder.root, 'alias'), join(real, 'self/file.txt')),
159
+ { target: 'self/file.txt', path: 'self', problem: 'is a symlink' },
160
+ 'a symlink back to the root inside it is still a symlink'
161
+ )
162
+ assert.equal(unsafeWriteProblem(join(holder.root, 'alias'), join(holder.root, 'elsewhere.txt'))?.problem.startsWith('is outside'), true)
163
+ } finally {
164
+ await holder.cleanup()
165
+ }
166
+ })
167
+
168
+ test('copyFile opens its destination without following a symlink put there after safe-fs checked it', async () => {
169
+ const project = await directory()
170
+ const outside = await directory()
171
+ try {
172
+ const secret = join(outside.root, 'secret.txt')
173
+ await writeFile(secret, 'secret\n')
174
+ await writeIn(project.root, 'source.txt', 'copied content\n')
175
+ const dest = join(project.root, 'theme.mjs')
176
+ await writeFile(dest, 'placeholder\n')
177
+ const fifo = join(project.root, 'source.fifo')
178
+ execFileSync('mkfifo', [fifo])
179
+
180
+ // The child blocks opening the FIFO as its copy source - inside copyFileSync's own work,
181
+ // after safe-fs's check on `dest` already passed while it was still an ordinary file.
182
+ const child = spawn(process.execPath, [join(dirname(fileURLToPath(import.meta.url)), 'copy-race-child.mjs'), project.root, fifo, dest], {
183
+ stdio: ['ignore', 'pipe', 'ignore'],
184
+ })
185
+ let out = ''
186
+ child.stdout.on('data', chunk => { out += chunk })
187
+ await new Promise(resolve => setTimeout(resolve, 400))
188
+
189
+ await rm(dest, { force: true })
190
+ await symlink(secret, dest)
191
+
192
+ // Unblock the child's read with an empty write, so its copy proceeds against the now-swapped destination.
193
+ const writer = await open(fifo, 'w')
194
+ await writer.close()
195
+ const [exitCode] = await once(child, 'exit')
196
+
197
+ assert.equal(exitCode, 0, 'the child process itself must not crash')
198
+ assert.deepEqual(JSON.parse(out), { code: 'ELOOP' }, 'the destination open must refuse the symlink instead of following it')
199
+ assert.equal(await readFile(secret, 'utf8'), 'secret\n', 'the outside file the symlink points to must survive untouched')
200
+ } finally {
201
+ await outside.cleanup()
202
+ await project.cleanup()
203
+ }
204
+ })
205
+
206
+ test('copyFile copies a source of several blocks, including a partial last one, byte for byte and with the source mode', async () => {
207
+ const project = await directory()
208
+ try {
209
+ const content = randomBytes(COPY_BLOCK_SIZE * 2 + 12345)
210
+ const source = join(project.root, 'source.bin')
211
+ await writeFile(source, content)
212
+ await chmod(source, 0o640)
213
+ const dest = join(project.root, 'dest.bin')
214
+ await projectFiles(project.root).copyFile(source, dest)
215
+ assert.equal((await readFile(dest)).equals(content), true, 'the destination holds the exact bytes of the source')
216
+ assert.equal((await lstat(dest)).mode & 0o777, 0o640, 'the destination keeps the source mode')
217
+ } finally {
218
+ await project.cleanup()
219
+ }
220
+ })
221
+
222
+ test('copyFile leaves no descriptor open when its destination open fails partway through the copy', { skip: RUNS_AS_ROOT }, async () => {
223
+ const project = await directory()
224
+ try {
225
+ await writeIn(project.root, 'source.txt', 'x'.repeat(64))
226
+ const readonly = join(project.root, 'readonly')
227
+ await mkdir(readonly)
228
+ await chmod(readonly, 0o500)
229
+ try {
230
+ const files = projectFiles(project.root)
231
+ const before = await readdir('/dev/fd')
232
+ const error = await thrown(() => files.copyFile(join(project.root, 'source.txt'), join(readonly, 'dest.txt')))
233
+ assert.equal(error?.code, 'EACCES')
234
+ const after = await readdir('/dev/fd')
235
+ assert.deepEqual(after.filter(fd => !before.includes(fd)), [], 'no descriptor from the failed copy is left open')
236
+ } finally {
237
+ await chmod(readonly, 0o700)
238
+ }
239
+ } finally {
240
+ await project.cleanup()
241
+ }
242
+ })
243
+
244
+ test("the build's steps that write refuse through safe-fs even when the check before the build is not asked", async () => {
245
+ const project = await directory()
246
+ const outside = await directory()
247
+ try {
248
+ const root = project.root
249
+ const config = { projectRoot: root, monorepoRoot: null, isMonorepoMode: false, activeTheme: 'acme', themesDir: join(root, 'contents/themes'), pluginsDir: join(root, 'contents/plugins') }
250
+
251
+ // app/api linked outside, holding an old generated plugin route and a file of its own
252
+ await writeIn(outside.root, 'api/v1/plugin/legacy/route.ts', '// Auto-generated Plugin Route Proxy\n')
253
+ await writeIn(outside.root, 'api/v1/plugin/legacy/other.ts', 'export const other = 1\n')
254
+ await mkdir(join(root, 'app'), { recursive: true })
255
+ await symlink(join(outside.root, 'api'), join(root, 'app/api'))
256
+
257
+ // The theme's fixtures linked outside
258
+ await writeIn(outside.root, 'fixtures/entities.json', '{"outside":true}\n')
259
+ await writeIn(outside.root, 'fixtures/blocks.json', '{"outside":true}\n')
260
+ await mkdir(join(root, 'contents/themes/acme/tests/cypress'), { recursive: true })
261
+ await symlink(join(outside.root, 'fixtures'), join(root, 'contents/themes/acme/tests/cypress/fixtures'))
262
+
263
+ // app/(templates) linked outside, and app/globals.css linked to a file outside
264
+ await writeIn(outside.root, 'templates/(public)/orphan/page.tsx', 'export default function Orphan() { return null }\n')
265
+ await symlink(join(outside.root, 'templates'), join(root, 'app/(templates)'))
266
+ await writeIn(outside.root, 'globals.css', '@import "../elsewhere.css";\n')
267
+ await symlink(join(outside.root, 'globals.css'), join(root, 'app/globals.css'))
268
+
269
+ const before = await snapshot(outside.root)
270
+ const steps = [
271
+ ['cleanupOldRouteFiles', () => cleanupOldRouteFiles(config), 'app/api'],
272
+ ['generateTestEntitiesJson', () => generateTestEntitiesJson([], [{ name: 'acme' }], config), 'contents/themes/acme/tests/cypress/fixtures'],
273
+ ['generateTestBlocksJson', () => generateTestBlocksJson([], config), 'contents/themes/acme/tests/cypress/fixtures'],
274
+ ['cleanupOrphanedTemplates', () => cleanupOrphanedTemplates([], config), 'app/(templates)'],
275
+ ['cleanupDeletedTemplate', () => cleanupDeletedTemplate(join(root, 'contents/themes/acme/templates/(public)/orphan/page.tsx'), config), 'app/(templates)'],
276
+ ['generateMissingPages', () => generateMissingPages([], config, new Map()), 'app/(templates)'],
277
+ ['syncAppGlobalsCss', () => syncAppGlobalsCss(config, 'acme'), 'app/globals.css'],
278
+ ]
279
+ const wrong = []
280
+ for (const [label, step, path] of steps) {
281
+ const error = await thrown(step)
282
+ if (!isUnsafeWrite(error)) wrong.push(`${label}: ${error ? `threw ${error.message}` : 'did not refuse'}`)
283
+ else if (error.path !== path) wrong.push(`${label}: named ${error.path}`)
284
+ }
285
+ assert.deepEqual(wrong, [])
286
+ assert.deepEqual(await snapshot(outside.root), before, 'nothing outside changed')
287
+ } finally {
288
+ await outside.cleanup()
289
+ await project.cleanup()
290
+ }
291
+ })
292
+
293
+ test('the scan finds each way a file can change the file system without safe-fs, and lets reads through', () => {
294
+ const found = source => directFsWrites(source, 'example.mjs').map(({ what }) => what)
295
+ assert.deepEqual(found("import { existsSync, readFileSync, lstatSync, constants } from 'fs'\nimport { readdir } from 'node:fs/promises'\n"), [])
296
+ assert.deepEqual(found("import { writeFileSync } from 'fs'"), ['imports writeFileSync from fs'])
297
+ assert.deepEqual(found("import { rm as remove } from 'node:fs/promises'"), ['imports rm from node:fs/promises'])
298
+ assert.deepEqual(found("import { promises } from 'fs'"), ['imports promises from fs'])
299
+ assert.deepEqual(found("import { cpSync, symlinkSync, openSync } from 'fs'"), ['imports cpSync from fs', 'imports symlinkSync from fs', 'imports openSync from fs'])
300
+ assert.deepEqual(found("import fs from 'fs'"), ['imports fs whole, as fs'])
301
+ assert.deepEqual(found("import * as fsp from 'fs/promises'"), ['imports fs/promises whole, as fsp'])
302
+ assert.deepEqual(found("import fse from 'fs-extra'"), ['imports fs-extra whole, as fse'])
303
+ assert.deepEqual(found("export { writeFile } from 'fs/promises'"), ['re-exports fs/promises'])
304
+ assert.deepEqual(found("const { unlink } = await import('fs/promises')"), ['loads fs/promises with a call'])
305
+ assert.deepEqual(found("const fs = require('node:fs')"), ['loads node:fs with a call'])
306
+ assert.deepEqual(found("const fs = createRequire(import.meta.url)('fs')"), ['loads fs with a call'])
307
+ assert.deepEqual(directFsWrites("import fs = require('fs')", 'example.ts').map(({ what }) => what), ['imports fs whole, as fs'])
308
+ assert.deepEqual(directFsWrites("import type { Stats, WriteStream } from 'node:fs'", 'example.ts'), [])
309
+ })
310
+
311
+ test('no file of the registry build, the rest of scripts/build or the postinstall changes the file system except through safe-fs', () => {
312
+ const files = generatorFiles({
313
+ directories: [join(CORE_DIR, 'scripts/build')],
314
+ entries: [join(CORE_DIR, 'scripts/postinstall.mjs')],
315
+ within: join(CORE_DIR, 'scripts'),
316
+ })
317
+ const names = files.map(file => file.slice(CORE_DIR.length + 1))
318
+ for (const expected of [
319
+ 'scripts/build/registry.mjs',
320
+ 'scripts/build/theme.mjs',
321
+ 'scripts/build/templates-plan.mjs',
322
+ 'scripts/build/registry/post-build/page-generator.mjs',
323
+ 'scripts/build/registry/post-build/route-cleanup.mjs',
324
+ 'scripts/build/registry/post-build/test-fixtures.mjs',
325
+ 'scripts/build/registry/post-build/own-gitignores.mjs',
326
+ 'scripts/utils/logging.mjs',
327
+ 'scripts/postinstall.mjs',
328
+ ]) {
329
+ assert.ok(names.includes(expected), `${expected} is scanned`)
330
+ }
331
+ assert.equal(names.some(name => name.includes('__tests__') || name.endsWith('.test.mjs')), false, 'tests are not the generator')
332
+ assert.deepEqual(directFsWritesIn(files, { base: CORE_DIR, allowed: [join(CORE_DIR, 'scripts/build/safe-fs.mjs')] }), [])
333
+ })
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Tests that both template registries and the page generator take what they
3
+ * know about a theme template from the analysis the registry build parses it
4
+ * into once (`analyzeTemplates`), instead of parsing the template again.
5
+ *
6
+ * Each test hands a consumer an analysis that no longer matches the template
7
+ * on disk: the state a watch-mode rebuild reaches when a template is saved
8
+ * between two phases of the same build. A consumer that parses the file itself
9
+ * sees a different template than the others, and its output gives it away.
10
+ *
11
+ * Run: node --test packages/core/scripts/build/registry/__tests__/template-analysis.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 { existsSync } from 'node:fs'
18
+ import { tmpdir } from 'node:os'
19
+ import { join, dirname } from 'node:path'
20
+
21
+ import {
22
+ generateTemplateRegistry,
23
+ generateTemplateRegistryClient
24
+ } from '../generators/template-registry.mjs'
25
+ import { generateMissingPages } from '../post-build/page-generator.mjs'
26
+
27
+ async function createProject() {
28
+ return mkdtemp(join(tmpdir(), 'nextspark-template-analysis-test-'))
29
+ }
30
+
31
+ async function writeThemeTemplate(root, relativePath, content) {
32
+ const absolutePath = join(root, 'contents/themes/testtheme/templates', relativePath)
33
+ await mkdir(dirname(absolutePath), { recursive: true })
34
+ await writeFile(absolutePath, content, 'utf8')
35
+ return `@/contents/themes/testtheme/templates/${relativePath}`
36
+ }
37
+
38
+ /**
39
+ * A layout that had a default export when the build read it and has lost it
40
+ * on disk since.
41
+ */
42
+ async function layoutThatLostItsDefaultExport(root) {
43
+ const templatePath = await writeThemeTemplate(root, 'docs/layout.tsx', "export const viewport = { themeColor: 'black' }\n")
44
+ const template = {
45
+ name: 'docs/layout',
46
+ themeName: 'testtheme',
47
+ templateType: 'layout',
48
+ fileName: 'layout.tsx',
49
+ relativePath: 'docs/layout.tsx',
50
+ appPath: 'app/docs/layout.tsx',
51
+ templatePath,
52
+ priority: 10
53
+ }
54
+ const analysis = new Map([
55
+ [templatePath, { hasDefaultExport: true, generatesRoute: true, segmentConfig: {}, moduleExports: ['viewport'] }]
56
+ ])
57
+ return { template, analysis, config: { outputDir: join(root, '.nextspark/registries'), projectRoot: root } }
58
+ }
59
+
60
+ test('the server registry takes the default export from the analysis, not from the template file', async () => {
61
+ const root = await createProject()
62
+ try {
63
+ const { template, analysis, config } = await layoutThatLostItsDefaultExport(root)
64
+
65
+ const out = await generateTemplateRegistry([template], config, analysis)
66
+
67
+ assert.match(
68
+ out,
69
+ /component: lazyTemplate\('app\/docs\/layout\.tsx', \(\) => import\('@\/contents\/themes\/testtheme\/templates\/docs\/layout'\)\)/
70
+ )
71
+ } finally {
72
+ await rm(root, { recursive: true, force: true })
73
+ }
74
+ })
75
+
76
+ test('the client registry takes the default export from the analysis, not from the template file', async () => {
77
+ const root = await createProject()
78
+ try {
79
+ const { template, analysis, config } = await layoutThatLostItsDefaultExport(root)
80
+
81
+ const out = await generateTemplateRegistryClient([template], config, analysis)
82
+
83
+ assert.match(out, /'app\/docs\/layout\.tsx': dynamic\(\(\) => import\('@\/contents\/themes\/testtheme\/templates\/docs\/layout'\)\)/)
84
+ } finally {
85
+ await rm(root, { recursive: true, force: true })
86
+ }
87
+ })
88
+
89
+ test('the page generator takes the default export and route-level exports from the analysis, not from the template file', async () => {
90
+ const root = await createProject()
91
+ try {
92
+ const { template, analysis, config } = await layoutThatLostItsDefaultExport(root)
93
+
94
+ await generateMissingPages([template], config, analysis)
95
+
96
+ const generated = await readFile(join(root, 'app/(templates)/docs/layout.tsx'), 'utf8')
97
+ assert.match(generated, /import TemplateComponent from '@\/contents\/themes\/testtheme\/templates\/docs\/layout'/)
98
+ assert.match(generated, /export \{ viewport \} from/)
99
+ assert.doesNotMatch(generated, /Pass-through component/)
100
+ } finally {
101
+ await rm(root, { recursive: true, force: true })
102
+ }
103
+ })
104
+
105
+ test('the page generator takes whether to write a route file from the analysis, not from the app on disk', async () => {
106
+ const root = await createProject()
107
+ try {
108
+ // The analysis was taken while app/pricing/page.tsx existed; it is gone now.
109
+ const templatePath = await writeThemeTemplate(root, 'pricing/page.tsx', 'export default function Page() { return null }\n')
110
+ const template = {
111
+ name: 'pricing/page',
112
+ themeName: 'testtheme',
113
+ templateType: 'page',
114
+ fileName: 'page.tsx',
115
+ relativePath: 'pricing/page.tsx',
116
+ appPath: 'app/pricing/page.tsx',
117
+ templatePath,
118
+ priority: 10
119
+ }
120
+ const analysis = new Map([
121
+ [templatePath, { hasDefaultExport: true, generatesRoute: false, segmentConfig: {}, moduleExports: [] }]
122
+ ])
123
+
124
+ await generateMissingPages([template], { projectRoot: root }, analysis)
125
+
126
+ assert.equal(existsSync(join(root, 'app/(templates)/pricing/page.tsx')), false)
127
+ } finally {
128
+ await rm(root, { recursive: true, force: true })
129
+ }
130
+ })
131
+
132
+ test('a registry given no analysis for a template fails instead of reading the template itself', async () => {
133
+ const root = await createProject()
134
+ try {
135
+ const { template, config } = await layoutThatLostItsDefaultExport(root)
136
+
137
+ await assert.rejects(() => generateTemplateRegistry([template], config, new Map()), /was not read by analyzeTemplates/)
138
+ await assert.rejects(() => generateTemplateRegistryClient([template], config, new Map()), /was not read by analyzeTemplates/)
139
+ } finally {
140
+ await rm(root, { recursive: true, force: true })
141
+ }
142
+ })