@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,425 @@
1
+ /**
2
+ * Tests for the check of where the registry build writes, which the build runs
3
+ * before writing anything and `nextspark sync:app` and `nextspark dev` run from
4
+ * the installed core: a symlink, something of another kind or that can't be
5
+ * read, or a .gitignore of .nextspark/backups or .nextspark/registries that
6
+ * would not keep what is beside it out of git, is found, and a build that finds
7
+ * one writes nothing, in the project or through it. And the .gitignore the
8
+ * build keeps in .nextspark/registries leaves the registries out of git.
9
+ *
10
+ * Run: node --test packages/core/scripts/build/registry/__tests__/write-places.test.mjs
11
+ */
12
+
13
+ import { test } from 'node:test'
14
+ import assert from 'node:assert/strict'
15
+ import { execFileSync, spawn, spawnSync } from 'node:child_process'
16
+ import { createHash } from 'node:crypto'
17
+ import { chmod, lstat, mkdir, mkdtemp, readdir, readFile, readlink, 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 { unsafeWritePlaces } from '../write-places.mjs'
24
+ import {
25
+ BACKUPS_GITIGNORE,
26
+ BACKUPS_GITIGNORE_CONTENT,
27
+ REGISTRIES_GITIGNORE,
28
+ REGISTRIES_GITIGNORE_CONTENT,
29
+ backupsGitignoreState,
30
+ ensureBackupsGitignore,
31
+ ensureRegistriesGitignore,
32
+ ownGitignoreState,
33
+ } from '../post-build/own-gitignores.mjs'
34
+
35
+ const CORE_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..')
36
+ const RUNS_AS_ROOT = process.getuid?.() === 0
37
+
38
+ async function directory() {
39
+ const root = await mkdtemp(join(tmpdir(), 'nextspark-write-places-test-'))
40
+ return { root, cleanup: () => rm(root, { recursive: true, force: true }) }
41
+ }
42
+
43
+ async function writeIn(root, path, content = '') {
44
+ await mkdir(dirname(join(root, path)), { recursive: true })
45
+ await writeFile(join(root, path), content)
46
+ }
47
+
48
+ test('a symlink where the build or a caller writes, or something else in the way, is found without going through it', async () => {
49
+ const { root, cleanup } = await directory()
50
+ const outside = await directory()
51
+ try {
52
+ await writeIn(outside.root, 'dashboard/layout.tsx')
53
+ await mkdir(join(root, 'app'), { recursive: true })
54
+ await writeIn(root, '.nextspark/backups/2026-09-16T00-00-00-000Z-q1w2e3/i18n.ts')
55
+
56
+ assert.deepEqual(unsafeWritePlaces(root), [])
57
+
58
+ await symlink(outside.root, join(root, 'app/(templates)'))
59
+ assert.deepEqual(unsafeWritePlaces(root), [{ path: 'app/(templates)', problem: 'is a symlink' }])
60
+
61
+ await rm(join(root, 'app/(templates)'))
62
+ await mkdir(join(root, 'app/(templates)/(public)'), { recursive: true })
63
+ await symlink(join(outside.root, 'dashboard'), join(root, 'app/(templates)/(public)/dashboard'))
64
+ await symlink(join(outside.root, 'missing'), join(root, '.nextspark/sync-state.json'))
65
+ await symlink(join(outside.root, 'dashboard'), join(root, '.nextspark/registries'))
66
+ await symlink(join(outside.root, 'dashboard'), join(root, 'app/dashboard'))
67
+ await symlink(join(outside.root, 'globals.css'), join(root, 'app/globals.css'))
68
+ assert.deepEqual(unsafeWritePlaces(root, ['app/dashboard/page.tsx', 'app/layout.tsx', 'i18n.ts', '.nextspark/sync-state.json']), [
69
+ { path: 'app/globals.css', problem: 'is a symlink' },
70
+ { path: '.nextspark/registries', problem: 'is a symlink' },
71
+ { path: 'app/dashboard', problem: 'is a symlink' },
72
+ { path: '.nextspark/sync-state.json', problem: 'is a symlink' },
73
+ { path: 'app/(templates)/(public)/dashboard', problem: 'is a symlink' },
74
+ ])
75
+ assert.deepEqual(unsafeWritePlaces(root).map(({ path }) => path), [
76
+ 'app/globals.css',
77
+ '.nextspark/registries',
78
+ 'app/(templates)/(public)/dashboard',
79
+ ], 'app/dashboard and the sync state count only for a caller that writes under them')
80
+
81
+ await rm(join(root, '.nextspark/registries'))
82
+ await mkdir(join(root, '.nextspark/registries'))
83
+ await symlink(join(outside.root, 'index.ts'), join(root, '.nextspark/registries/index.ts'))
84
+ assert.ok(unsafeWritePlaces(root).some(({ path, problem }) => path === '.nextspark/registries/index.ts' && problem === 'is a symlink'), 'a symlink in the registries is found')
85
+
86
+ await rm(join(root, '.nextspark/registries/index.ts'))
87
+ await mkdir(join(root, '.nextspark/registries/index.ts'))
88
+ await mkdir(join(root, 'app/(templates)/(public)/page.tsx'), { recursive: true })
89
+ await rm(join(root, 'app/dashboard'))
90
+ await writeFile(join(root, 'app/dashboard'), '')
91
+ await rm(join(root, 'app/globals.css'))
92
+ await mkdir(join(root, 'app/globals.css'))
93
+ await mkdir(join(root, 'i18n.ts'))
94
+ assert.deepEqual(unsafeWritePlaces(root, ['app/dashboard/page.tsx', 'app/layout.tsx', 'i18n.ts', '.nextspark/sync-state.json']), [
95
+ { path: 'app/globals.css', problem: 'is not a file' },
96
+ { path: 'app/dashboard', problem: 'is not a directory' },
97
+ { path: 'i18n.ts', problem: 'is not a file' },
98
+ { path: '.nextspark/sync-state.json', problem: 'is a symlink' },
99
+ { path: 'app/(templates)/(public)/dashboard', problem: 'is a symlink' },
100
+ { path: '.nextspark/registries/index.ts', problem: 'is not a file' },
101
+ ], 'a directory in app/(templates) where a file goes is left to the registry build')
102
+
103
+ await rm(join(root, '.nextspark'), { recursive: true })
104
+ await writeFile(join(root, '.nextspark'), '')
105
+ await rm(join(root, 'app'), { recursive: true })
106
+ await symlink(outside.root, join(root, 'app'))
107
+ assert.deepEqual(unsafeWritePlaces(root), [
108
+ { path: 'app', problem: 'is a symlink' },
109
+ { path: '.nextspark', problem: 'is not a directory' },
110
+ ])
111
+ } finally {
112
+ await outside.cleanup()
113
+ await cleanup()
114
+ }
115
+ })
116
+
117
+ test('a directory the build lists that cannot be read is found', { skip: RUNS_AS_ROOT }, async () => {
118
+ const { root, cleanup } = await directory()
119
+ try {
120
+ await mkdir(join(root, 'app/(templates)/dashboard'), { recursive: true })
121
+ await mkdir(join(root, '.nextspark/registries'), { recursive: true })
122
+ await mkdir(join(root, '.nextspark/backups'), { recursive: true })
123
+ await chmod(join(root, 'app/(templates)/dashboard'), 0)
124
+ await chmod(join(root, '.nextspark/registries'), 0)
125
+ await chmod(join(root, '.nextspark/backups'), 0)
126
+ assert.deepEqual(unsafeWritePlaces(root), [
127
+ { path: '.nextspark/backups', problem: "can't be read" },
128
+ { path: '.nextspark/registries', problem: "can't be read" },
129
+ { path: 'app/(templates)/dashboard', problem: "can't be read" },
130
+ ])
131
+ } finally {
132
+ await chmod(join(root, 'app/(templates)/dashboard'), 0o755).catch(() => {})
133
+ await chmod(join(root, '.nextspark/registries'), 0o755).catch(() => {})
134
+ await chmod(join(root, '.nextspark/backups'), 0o755).catch(() => {})
135
+ await cleanup()
136
+ }
137
+ })
138
+
139
+ test('a .nextspark/backups/.gitignore counts as in place only as a readable file with * as its one pattern, and anything else is found', async () => {
140
+ const contents = [
141
+ ['*\n', 'in place'],
142
+ ['\uFEFF*\r\n', 'in place'],
143
+ ['# Our own words\r\n\n* \r\n', 'in place'],
144
+ ['*\\ \n', 'other'],
145
+ ['\\*\n', 'other'],
146
+ ['/*\n', 'other'],
147
+ ['*\n!*/\n', 'other'],
148
+ ['*\n!manual-snapshots/\n', 'other'],
149
+ ['# nothing\n', 'other'],
150
+ [' *\n', 'other'],
151
+ ['*\t\n', 'other'],
152
+ ]
153
+ for (const [content, state] of contents) {
154
+ const { root, cleanup } = await directory()
155
+ try {
156
+ await writeIn(root, BACKUPS_GITIGNORE, content)
157
+ assert.equal(backupsGitignoreState(root), state, JSON.stringify(content))
158
+ assert.equal(unsafeWritePlaces(root).length, state === 'in place' ? 0 : 1, JSON.stringify(content))
159
+ if (state === 'in place') {
160
+ assert.equal(await ensureBackupsGitignore(root), false, JSON.stringify(content))
161
+ } else {
162
+ await assert.rejects(ensureBackupsGitignore(root), /Nothing is backed up under \.nextspark\/backups/, JSON.stringify(content))
163
+ }
164
+ assert.equal(await readFile(join(root, BACKUPS_GITIGNORE), 'utf-8'), content, 'one already there is never written over')
165
+ } finally {
166
+ await cleanup()
167
+ }
168
+ }
169
+
170
+ const { root, cleanup } = await directory()
171
+ const outside = await directory()
172
+ try {
173
+ assert.equal(backupsGitignoreState(root), 'missing')
174
+ assert.deepEqual(unsafeWritePlaces(root), [])
175
+
176
+ await mkdir(join(root, '.nextspark/backups'), { recursive: true })
177
+ await symlink(join(outside.root, 'rules'), join(root, BACKUPS_GITIGNORE))
178
+ assert.equal(backupsGitignoreState(root), 'symlink')
179
+ assert.deepEqual(unsafeWritePlaces(root).map(({ path }) => path), [BACKUPS_GITIGNORE])
180
+ await assert.rejects(ensureBackupsGitignore(root))
181
+ assert.equal(existsSync(join(outside.root, 'rules')), false, 'nothing is written through the symlink')
182
+
183
+ await rm(join(root, BACKUPS_GITIGNORE))
184
+ await mkdir(join(root, BACKUPS_GITIGNORE))
185
+ assert.equal(backupsGitignoreState(root), 'not a file')
186
+ assert.deepEqual(unsafeWritePlaces(root).map(({ path }) => path), [BACKUPS_GITIGNORE])
187
+
188
+ await rm(join(root, BACKUPS_GITIGNORE), { recursive: true })
189
+ assert.equal(await ensureBackupsGitignore(root), true)
190
+ assert.equal(await readFile(join(root, BACKUPS_GITIGNORE), 'utf-8'), BACKUPS_GITIGNORE_CONTENT)
191
+
192
+ assert.equal(ownGitignoreState(root, REGISTRIES_GITIGNORE), 'missing')
193
+ await writeIn(root, REGISTRIES_GITIGNORE, '*\n!index.ts\n')
194
+ assert.equal(ownGitignoreState(root, REGISTRIES_GITIGNORE), 'other')
195
+ assert.deepEqual(unsafeWritePlaces(root).map(({ path }) => path), [REGISTRIES_GITIGNORE], "the registries' .gitignore is read as the backups' is")
196
+ await assert.rejects(ensureRegistriesGitignore(root), /No registry is written under \.nextspark\/registries/)
197
+ await rm(join(root, REGISTRIES_GITIGNORE))
198
+ await symlink(join(outside.root, 'rules'), join(root, REGISTRIES_GITIGNORE))
199
+ assert.deepEqual(unsafeWritePlaces(root), [{ path: REGISTRIES_GITIGNORE, problem: 'is a symlink, which git does not read, so git would pick up what is beside it: make it a file with * as its only pattern, or remove it for nextspark to write it' }], 'found once, not again as a symlink in the registries')
200
+ await rm(join(root, REGISTRIES_GITIGNORE))
201
+ assert.equal(await ensureRegistriesGitignore(root), true)
202
+ assert.equal(await readFile(join(root, REGISTRIES_GITIGNORE), 'utf-8'), REGISTRIES_GITIGNORE_CONTENT)
203
+ } finally {
204
+ await outside.cleanup()
205
+ await cleanup()
206
+ }
207
+ })
208
+
209
+ test('a .nextspark/backups/.gitignore that cannot be read is found, not taken as missing', { skip: RUNS_AS_ROOT }, async () => {
210
+ const { root, cleanup } = await directory()
211
+ try {
212
+ await writeIn(root, BACKUPS_GITIGNORE, '*\n')
213
+ await chmod(join(root, BACKUPS_GITIGNORE), 0)
214
+ assert.equal(backupsGitignoreState(root), 'unreadable')
215
+ assert.deepEqual(unsafeWritePlaces(root).map(({ path }) => path), [BACKUPS_GITIGNORE])
216
+ await assert.rejects(ensureBackupsGitignore(root), /can't be read/)
217
+ } finally {
218
+ await chmod(join(root, BACKUPS_GITIGNORE), 0o644).catch(() => {})
219
+ await cleanup()
220
+ }
221
+ })
222
+
223
+ test('the .gitignore kept in .nextspark/backups ignores a backup whatever its directory is named and whatever the rules above take back', async () => {
224
+ const { root, cleanup } = await directory()
225
+ const gitIgnores = path => spawnSync('git', ['check-ignore', '-q', '--', path], { cwd: root }).status === 0
226
+ try {
227
+ execFileSync('git', ['init', '-q'], { cwd: root })
228
+ await writeFile(join(root, '.gitignore'), '!.nextspark/\n!.nextspark/backups/**\n')
229
+ await writeFile(join(root, '.git/info/exclude'), '!*\n')
230
+ await writeIn(root, '.nextspark/.gitignore', '!backups/\n!backups/**\n')
231
+ await writeIn(root, '.nextspark/backups/2026-09-16T00-00-00-000Z-r3g1st/.gitignore', '!*\n')
232
+ const backups = [
233
+ '.nextspark/backups/2026-09-16T00-00-00-000Z-Zz9Qa1/app/(templates)/(public)/page.tsx',
234
+ '.nextspark/backups/2026-09-16T00-00-00-000Z-r3g1st/i18n.ts',
235
+ '.nextspark/backups/named by hand/.env.local',
236
+ ]
237
+ for (const file of backups) await writeIn(root, file, 'SECRET=1\n')
238
+ assert.deepEqual(backups.map(gitIgnores), [false, false, false])
239
+
240
+ assert.equal(await ensureBackupsGitignore(root), true)
241
+ assert.equal(await ensureBackupsGitignore(root), false, 'a second call leaves it as it is')
242
+ assert.equal(backupsGitignoreState(root), 'in place')
243
+
244
+ assert.deepEqual(backups.map(gitIgnores), [true, true, true])
245
+ assert.equal(
246
+ execFileSync('git', ['status', '--porcelain', '--untracked-files=all'], { cwd: root, encoding: 'utf-8' }),
247
+ '?? .gitignore\n?? .nextspark/.gitignore\n'
248
+ )
249
+ } finally {
250
+ await cleanup()
251
+ }
252
+ })
253
+
254
+ /** Every entry under `root`: a file by its content's hash, a symlink by where it points, a directory by its name. */
255
+ async function snapshot(root) {
256
+ const entries = {}
257
+ const walk = async relative => {
258
+ for (const entry of await readdir(join(root, relative), { withFileTypes: true })) {
259
+ const path = relative ? `${relative}/${entry.name}` : entry.name
260
+ const stats = await lstat(join(root, path))
261
+ if (stats.isSymbolicLink()) {
262
+ entries[path] = `-> ${await readlink(join(root, path))}`
263
+ } else if (stats.isDirectory()) {
264
+ entries[path] = 'dir'
265
+ if (stats.mode & 0o400) await walk(path)
266
+ } else if (stats.mode & 0o400) {
267
+ entries[path] = createHash('sha256').update(await readFile(join(root, path))).digest('hex')
268
+ } else {
269
+ entries[path] = 'unreadable file'
270
+ }
271
+ }
272
+ }
273
+ await walk('')
274
+ return entries
275
+ }
276
+
277
+ /** A project for core's real registry build, with a stale file in app/(templates) the build would back up and remove. */
278
+ async function buildableProject() {
279
+ const project = await directory()
280
+ await writeIn(project.root, '.env', 'NEXT_PUBLIC_ACTIVE_THEME=acme\n')
281
+ await writeIn(project.root, 'package.json', '{}')
282
+ await writeIn(project.root, 'app/layout.tsx', 'export default function Layout({ children }) { return children }\n')
283
+ await writeIn(project.root, 'app/(templates)/stale/page.tsx', 'export default function Stale() { return null }\n')
284
+ await writeIn(project.root, 'contents/themes/acme/templates/pricing/page.tsx', 'export default function Pricing() { return null }\n')
285
+ return project
286
+ }
287
+
288
+ function runBuild(root) {
289
+ const result = spawnSync('node', ['scripts/build/registry.mjs'], {
290
+ cwd: CORE_DIR,
291
+ env: { ...process.env, NEXTSPARK_PROJECT_ROOT: root },
292
+ encoding: 'utf8',
293
+ })
294
+ return { status: result.status, output: `${result.stdout}${result.stderr}` }
295
+ }
296
+
297
+ test("the registry build writes nothing, in the project or through it, when a place it writes under isn't safe", async () => {
298
+ const cases = [
299
+ ['a registry that is a symlink to a file outside', async (root, outside) => {
300
+ await writeIn(outside, 'index.ts', '// outside\n')
301
+ await mkdir(join(root, '.nextspark/registries'), { recursive: true })
302
+ await symlink(join(outside, 'index.ts'), join(root, '.nextspark/registries/index.ts'))
303
+ }, '.nextspark/registries/index.ts is a symlink'],
304
+ ['the registries directory a symlink to one outside', async (root, outside) => {
305
+ await mkdir(join(root, '.nextspark'), { recursive: true })
306
+ await symlink(outside, join(root, '.nextspark/registries'))
307
+ }, '.nextspark/registries is a symlink'],
308
+ ['app/(templates) a symlink to a directory outside', async (root, outside) => {
309
+ await rm(join(root, 'app/(templates)'), { recursive: true })
310
+ await symlink(outside, join(root, 'app/(templates)'))
311
+ }, 'app/(templates) is a symlink'],
312
+ ['app/globals.css a symlink to a file outside', async (root, outside) => {
313
+ await writeIn(outside, 'globals.css', '/* outside */\n')
314
+ await symlink(join(outside, 'globals.css'), join(root, 'app/globals.css'))
315
+ }, 'app/globals.css is a symlink'],
316
+ ['a directory where a registry goes', async root => {
317
+ await writeIn(root, '.nextspark/registries/index.ts/stale.ts')
318
+ }, '.nextspark/registries/index.ts is not a file'],
319
+ ['a backups .gitignore that takes the backups back', async root => {
320
+ await writeIn(root, BACKUPS_GITIGNORE, '*\n!*/\n')
321
+ }, `${BACKUPS_GITIGNORE} has patterns other than *`],
322
+ ['a registries .gitignore that takes the registries back', async root => {
323
+ await writeIn(root, REGISTRIES_GITIGNORE, '*\n!*.ts\n')
324
+ }, `${REGISTRIES_GITIGNORE} has patterns other than *`],
325
+ ['app/api a symlink to a directory outside holding an old generated plugin route and another file', async (root, outside) => {
326
+ await writeIn(outside, 'v1/plugin/legacy/route.ts', '// Auto-generated Plugin Route Proxy\n')
327
+ await writeIn(outside, 'v1/plugin/legacy/other.ts', 'export const other = 1\n')
328
+ await symlink(outside, join(root, 'app/api'))
329
+ }, 'app/api is a symlink'],
330
+ ["the active theme's fixtures directory a symlink to one outside", async (root, outside) => {
331
+ await writeIn(outside, 'entities.json', '{"outside":true}\n')
332
+ await writeIn(outside, 'blocks.json', '{"outside":true}\n')
333
+ await mkdir(join(root, 'contents/themes/acme/tests/cypress'), { recursive: true })
334
+ await symlink(outside, join(root, 'contents/themes/acme/tests/cypress/fixtures'))
335
+ }, 'contents/themes/acme/tests/cypress/fixtures is a symlink'],
336
+ ["the active theme's entities.json a symlink to a file outside", async (root, outside) => {
337
+ await writeIn(outside, 'entities.json', '{"outside":true}\n')
338
+ await mkdir(join(root, 'contents/themes/acme/tests/cypress/fixtures'), { recursive: true })
339
+ await symlink(join(outside, 'entities.json'), join(root, 'contents/themes/acme/tests/cypress/fixtures/entities.json'))
340
+ }, 'contents/themes/acme/tests/cypress/fixtures/entities.json is a symlink'],
341
+ ['the active theme a symlink to a directory outside with fixtures', async (root, outside) => {
342
+ await writeIn(outside, 'templates/pricing/page.tsx', 'export default function Pricing() { return null }\n')
343
+ await writeIn(outside, 'tests/cypress/fixtures/entities.json', '{"outside":true}\n')
344
+ await writeIn(outside, 'tests/cypress/fixtures/blocks.json', '{"outside":true}\n')
345
+ await rm(join(root, 'contents/themes/acme'), { recursive: true })
346
+ await symlink(outside, join(root, 'contents/themes/acme'))
347
+ }, 'contents/themes/acme is a symlink'],
348
+ ...(RUNS_AS_ROOT ? [] : [['a backups .gitignore that cannot be read', async root => {
349
+ await writeIn(root, BACKUPS_GITIGNORE, '*\n')
350
+ await chmod(join(root, BACKUPS_GITIGNORE), 0)
351
+ }, `${BACKUPS_GITIGNORE} can't be read`]]),
352
+ ]
353
+
354
+ const wrong = []
355
+ for (const [label, setUp, named] of cases) {
356
+ const project = await buildableProject()
357
+ const outside = await directory()
358
+ try {
359
+ await setUp(project.root, outside.root)
360
+ const before = { project: await snapshot(project.root), outside: await snapshot(outside.root) }
361
+ const { status, output } = runBuild(project.root)
362
+ const after = { project: await snapshot(project.root), outside: await snapshot(outside.root) }
363
+
364
+ if (status !== 1) wrong.push(`${label}: exited ${status}`)
365
+ if (JSON.stringify(after.project) !== JSON.stringify(before.project)) wrong.push(`${label}: the project changed`)
366
+ if (JSON.stringify(after.outside) !== JSON.stringify(before.outside)) wrong.push(`${label}: what is outside changed`)
367
+ if (!output.split('\n').some(line => line.includes("Build failed before writing anything"))) wrong.push(`${label}: no line says the build stopped before writing`)
368
+ if (!output.split('\n').some(line => line.trim().startsWith(named))) wrong.push(`${label}: no line names ${named}`)
369
+ } finally {
370
+ await chmod(join(project.root, BACKUPS_GITIGNORE), 0o644).catch(() => {})
371
+ await outside.cleanup()
372
+ await project.cleanup()
373
+ }
374
+ }
375
+
376
+ const control = await buildableProject()
377
+ try {
378
+ await writeIn(control.root, 'app/api/v1/plugin/legacy/route.ts', '// Auto-generated Plugin Route Proxy\n')
379
+ await mkdir(join(control.root, 'contents/themes/acme/tests/cypress/fixtures'), { recursive: true })
380
+ const { status } = runBuild(control.root)
381
+ if (status !== 0) wrong.push(`with nothing in the way, the build exited ${status}`)
382
+ if (!existsSync(join(control.root, '.nextspark/registries/index.ts'))) wrong.push('with nothing in the way, the build wrote no registry')
383
+ if (existsSync(join(control.root, 'app/(templates)/stale/page.tsx'))) wrong.push('with nothing in the way, the build left the stale file')
384
+ if (existsSync(join(control.root, 'app/api/v1/plugin/legacy'))) wrong.push('with nothing in the way, the build left the old generated plugin route')
385
+ // entities.json is written only for a theme discovery finds, which this one, with no theme.config.ts, is not
386
+ if (!existsSync(join(control.root, 'contents/themes/acme/tests/cypress/fixtures/blocks.json'))) wrong.push('with nothing in the way, the build wrote no blocks.json')
387
+ } finally {
388
+ await control.cleanup()
389
+ }
390
+
391
+ assert.deepEqual(wrong, [])
392
+ })
393
+
394
+ test("the registry build keeps .nextspark/registries out of git with a .gitignore of its own, while it runs and after, whatever the project's rules take back", async () => {
395
+ const project = await buildableProject()
396
+ const root = project.root
397
+ try {
398
+ execFileSync('git', ['init', '-q'], { cwd: root })
399
+ await writeFile(join(root, '.gitignore'), 'node_modules/\n.env\napp/(templates)/\n!.nextspark/\n!.nextspark/registries/**\n')
400
+ await writeIn(root, '.nextspark/.gitignore', '!registries/\n!registries/**\n')
401
+ await writeFile(join(root, '.git/info/exclude'), '!*\n')
402
+ const visible = () => execFileSync('git', ['status', '--porcelain', '--untracked-files=all'], { cwd: root, encoding: 'utf8' })
403
+ .split('\n').filter(line => line.includes('.nextspark/registries'))
404
+
405
+ const build = spawn('node', ['scripts/build/registry.mjs'], { cwd: CORE_DIR, env: { ...process.env, NEXTSPARK_PROJECT_ROOT: root }, stdio: 'ignore' })
406
+ let exited = null
407
+ build.on('exit', code => { exited = code })
408
+ const during = new Set()
409
+ let polls = 0
410
+ while (exited === null) {
411
+ for (const line of visible()) during.add(line)
412
+ polls++
413
+ await new Promise(resolve => setTimeout(resolve, 5))
414
+ }
415
+
416
+ assert.equal(exited, 0)
417
+ assert.ok(polls > 1, 'git was asked while the build ran')
418
+ assert.deepEqual([...during], [], 'git picks up no registry while the build runs')
419
+ assert.deepEqual(visible(), [], 'nor after')
420
+ assert.equal(await readFile(join(root, REGISTRIES_GITIGNORE), 'utf8'), REGISTRIES_GITIGNORE_CONTENT)
421
+ assert.ok(existsSync(join(root, '.nextspark/registries/index.ts')), 'the registries are written')
422
+ } finally {
423
+ await project.cleanup()
424
+ }
425
+ })
@@ -14,9 +14,11 @@
14
14
 
15
15
  import { join, dirname } from 'path'
16
16
  import { fileURLToPath } from 'url'
17
- import { existsSync, lstatSync, readlinkSync } from 'fs'
17
+ import { existsSync } from 'fs'
18
18
  import dotenv from 'dotenv'
19
19
  import { loadNextSparkConfigSync } from '../config-loader.mjs'
20
+ import { detectMonorepoRoot, isInstalledAsPackage } from './project-mode.mjs'
21
+ import { shownPath } from '../../utils/logging.mjs'
20
22
 
21
23
  // Load .env from the correct project root
22
24
  // Priority: NEXTSPARK_PROJECT_ROOT env var > cwd
@@ -32,26 +34,7 @@ const __dirname = dirname(__filename)
32
34
  // Root directory (5 levels up from packages/core/scripts/build/registry/)
33
35
  export const rootDir = join(__dirname, '../../../../..')
34
36
 
35
- /**
36
- * Detect monorepo root by searching for pnpm-workspace.yaml
37
- * @param {string} startDir - Directory to start searching from
38
- * @returns {string|null} Monorepo root path or null if not in monorepo
39
- */
40
- export function detectMonorepoRoot(startDir = process.cwd()) {
41
- let dir = startDir
42
- const maxDepth = 10
43
- let depth = 0
44
-
45
- while (dir !== '/' && depth < maxDepth) {
46
- if (existsSync(join(dir, 'pnpm-workspace.yaml'))) {
47
- return dir
48
- }
49
- dir = dirname(dir)
50
- depth++
51
- }
52
-
53
- return null
54
- }
37
+ export { detectMonorepoRoot, isInstalledAsPackage }
55
38
 
56
39
  /**
57
40
  * Detect project root by searching for nextspark.config.ts or apps/dev in monorepo
@@ -85,49 +68,6 @@ export function detectProjectRoot() {
85
68
  return process.cwd()
86
69
  }
87
70
 
88
- /**
89
- * Check if NextSpark is installed as a package (from npm registry)
90
- *
91
- * This handles both traditional npm/yarn AND pnpm:
92
- * - npm/yarn: packages are copied to node_modules (not symlinks)
93
- * - pnpm: ALL packages are symlinks, but npm packages point to .pnpm/ store
94
- * - pnpm workspace: local packages symlink to the actual package path
95
- *
96
- * @param {string} root - Project root path
97
- * @returns {boolean} True if installed from npm registry
98
- */
99
- export function isInstalledAsPackage(root) {
100
- const corePath = join(root, 'node_modules/@nextsparkjs/core')
101
- if (!existsSync(corePath)) {
102
- return false
103
- }
104
-
105
- try {
106
- const stat = lstatSync(corePath)
107
-
108
- // If not a symlink, it's a traditional npm/yarn install
109
- if (!stat.isSymbolicLink()) {
110
- return true
111
- }
112
-
113
- // For symlinks (pnpm), check WHERE it points to:
114
- // - npm install: points to .pnpm/@nextsparkjs+core@version.../node_modules/@nextsparkjs/core
115
- // - workspace: points to ../../packages/core or similar
116
- const linkTarget = readlinkSync(corePath)
117
-
118
- // If symlink points to .pnpm/ or .pnpmN/ directory, it's a real npm installation via pnpm
119
- // The .pnpm/.pnpm2/etc directory is pnpm's content-addressable store
120
- if (/\.pnpm\d?[/\\]/.test(linkTarget)) {
121
- return true
122
- }
123
-
124
- // Otherwise it's a workspace symlink (monorepo development)
125
- return false
126
- } catch {
127
- return false
128
- }
129
- }
130
-
131
71
  /**
132
72
  * Get dynamic configuration based on project root
133
73
  * @param {string|null} projectRoot - Optional project root path
@@ -216,7 +156,8 @@ export function validateEnvironment(config) {
216
156
  const hasEnvVarsFromProcess = !!process.env.NEXT_PUBLIC_ACTIVE_THEME
217
157
 
218
158
  if (!existsSync(envFilePath) && !hasEnvVarsFromProcess) {
219
- let fix = `Create a .env file in your project root: ${config.projectRoot}`
159
+ // The error spans lines, printed one per call: the path is shown on the one line it is on
160
+ let fix = `Create a .env file in your project root: ${shownPath(config.projectRoot)}`
220
161
  if (existsSync(envExamplePath)) {
221
162
  fix = `Copy the example file:\n cp .env.example .env\n Then update it with your configuration.`
222
163
  }