@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
@@ -8,13 +8,28 @@
8
8
  * Sources are exactly what resolveIcon can be handed, and all of them are
9
9
  * source files (never the database):
10
10
  * - entity configs (`icon: Users` — an identifier imported from lucide-react)
11
- * - a theme's app.config (`customSidebarSections[].icon` — a string)
11
+ * - any `config/*.config.ts` of a theme or a plugin (`icon: 'Grid'`,
12
+ * `iconName: Users` — sidebar sections, dashboard menus, feature and flow
13
+ * definitions, all of a theme's or plugin's config surface)
12
14
  * - block configs (`icon: 'Grid'` — the block's own icon in the editor)
15
+ * - a literal name handed to DynamicIcon's `name` prop or to resolveIcon's
16
+ * first argument in a theme's or plugin's own component code
13
17
  *
14
18
  * What a block RENDERS is not here: page content names those icons in the page
15
19
  * builder, so they live in the database. Those blocks keep resolving through
16
20
  * the lucide namespace — deliberately, see themes' features-grid component.
17
21
  *
22
+ * A name that is not a string literal — a variable, a prop, a template
23
+ * expression — cannot be resolved while building, so it is left out of the
24
+ * registry rather than guessed; that is the same limit that makes the
25
+ * unresolved-reference warning below necessary for config files.
26
+ *
27
+ * Every source is read as a syntax tree (core's own TypeScript, #195), not as
28
+ * raw text: a name that only exists inside a comment or a docblock is not
29
+ * code the app can run, so it must not reach the registry either. Test files
30
+ * are skipped for the same reason from the other direction — an icon named
31
+ * only inside a test is never rendered in production.
32
+ *
18
33
  * @module core/scripts/build/registry/discovery/icons
19
34
  */
20
35
 
@@ -24,6 +39,7 @@ import { createRequire } from 'module'
24
39
  import { join, dirname } from 'path'
25
40
 
26
41
  import { verbose, log } from '../../../utils/index.mjs'
42
+ import { loadTypeScriptFor } from '../shared/typescript-compiler.mjs'
27
43
 
28
44
  /**
29
45
  * Icons the resolver falls back to when a name doesn't resolve. They are
@@ -32,6 +48,15 @@ import { verbose, log } from '../../../utils/index.mjs'
32
48
  */
33
49
  const FALLBACK_ICONS = ['Box', 'Circle', 'Folder', 'LayoutGrid']
34
50
 
51
+ /** A bare identifier-shaped name, the only shape the generated registry can
52
+ * safely turn into a named import or a lucide-react lookup key. */
53
+ const SAFE_NAME = /^[A-Za-z][\w$-]*$/
54
+
55
+ /** The property keys a config's icon can be written under — the plain
56
+ * `icon: X` shape and `iconName: X`, the one a theme's sidebar, dashboard,
57
+ * feature and flow configs use. */
58
+ const ICON_KEYS = new Set(['icon', 'iconName'])
59
+
35
60
  /**
36
61
  * Names lucide-react actually exports, read from its own barrel.
37
62
  * A name that isn't there would become a broken named import in the
@@ -63,6 +88,10 @@ async function readLucideExportNames(config) {
63
88
  /**
64
89
  * Which config files hold icons resolveIcon can be handed. Exported for tests.
65
90
  *
91
+ * Every file directly under a `config/` directory and named `*.config.ts`
92
+ * counts, not only `app.config.ts`: a theme's dashboard, features and flows
93
+ * configs name icons by string the same way its app config does.
94
+ *
66
95
  * Separators are normalised first: the generator also runs on Windows, where
67
96
  * join() produces '\\' and a '/'-shaped match would silently find nothing —
68
97
  * leaving a registry with only its fallbacks, and every entity icon rendering
@@ -73,53 +102,195 @@ export function isIconSourcePath(filePath) {
73
102
  return (
74
103
  (normalised.includes('/entities/') && normalised.endsWith('.config.ts')) ||
75
104
  (normalised.includes('/blocks/') && normalised.endsWith('/config.ts')) ||
76
- normalised.endsWith('/config/app.config.ts')
105
+ /\/config\/[^/]+\.config\.ts$/.test(normalised)
77
106
  )
78
107
  }
79
108
 
109
+ /**
110
+ * Whether a file is component source a theme or plugin could name an icon
111
+ * from at runtime — anything DynamicIcon or resolveIcon could be called
112
+ * from. Exported for tests. Same Windows-separator normalisation as
113
+ * isIconSourcePath, for the same reason.
114
+ */
115
+ export function isIconCallSourcePath(filePath) {
116
+ return /\.tsx?$/.test(filePath.replace(/\\/g, '/'))
117
+ }
118
+
119
+ /**
120
+ * Whether a path is test code rather than the app's own source. Exported for
121
+ * tests. A name that a test hands to resolveIcon or DynamicIcon to exercise a
122
+ * component is never reached by a real request, so it would only bloat the
123
+ * registry with icons nothing in production resolves.
124
+ */
125
+ export function isTestFilePath(filePath) {
126
+ const normalised = filePath.replace(/\\/g, '/')
127
+ return /\.(test|spec)\.tsx?$/.test(normalised) || /(^|\/)(__tests__|tests|cypress)\//.test(normalised)
128
+ }
129
+
130
+ /**
131
+ * Parse a source file into a TypeScript syntax tree. Exported for tests, so a
132
+ * test can build a tree once and hand it to more than one extractor.
133
+ *
134
+ * Parsed as its own kind of file: a TSX parse reads TypeScript-only syntax in
135
+ * a .ts file (`<T>(props) => ...`) as JSX and loses what follows, so the
136
+ * script kind is derived from `filePath` rather than assumed.
137
+ */
138
+ export async function parseIconSource(content, filePath, projectRoot = process.cwd()) {
139
+ const ts = await loadTypeScriptFor(projectRoot)
140
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, ts.getScriptKindFromFileName(filePath))
141
+ return { ts, sourceFile }
142
+ }
143
+
144
+ /** The property key of a `PropertyAssignment` node, whether written bare
145
+ * (`icon: X`), quoted (`'icon': X`) or computed with a literal (`['icon']: X`);
146
+ * anything else — a computed key that isn't a literal — has no static name. */
147
+ function propertyKeyName(nameNode, ts) {
148
+ if (ts.isIdentifier(nameNode) || ts.isStringLiteralLike(nameNode)) {
149
+ return nameNode.text
150
+ }
151
+ if (ts.isComputedPropertyName(nameNode)) {
152
+ const expression = unwrapTransparentExpression(nameNode.expression, ts)
153
+ return expression && ts.isStringLiteralLike(expression) ? expression.text : null
154
+ }
155
+ return null
156
+ }
157
+
158
+ /**
159
+ * Peel off the TypeScript wrappers that change nothing about which value an
160
+ * expression is at runtime — `(expr)`, `expr as T`, `<T>expr`,
161
+ * `expr satisfies T`, `expr!` — so `icon: 'Wallet' as const`, `icon: <any>Wallet`
162
+ * and `icon: Wallet!` are read the same as `icon: 'Wallet'` and `icon: Wallet`.
163
+ * A regex-based scanner never saw these wrappers in the first place; reading
164
+ * syntax does, so it has to strip them explicitly instead of failing to match
165
+ * and dropping the icon silently.
166
+ */
167
+ function unwrapTransparentExpression(expression, ts) {
168
+ let current = expression
169
+ while (current) {
170
+ if (
171
+ ts.isParenthesizedExpression(current) ||
172
+ ts.isAsExpression(current) ||
173
+ ts.isTypeAssertionExpression(current) ||
174
+ ts.isSatisfiesExpression(current) ||
175
+ ts.isNonNullExpression(current)
176
+ ) {
177
+ current = current.expression
178
+ continue
179
+ }
180
+ return current
181
+ }
182
+ return current
183
+ }
184
+
80
185
  /**
81
186
  * Map of local name -> exported lucide name for a file's lucide imports.
82
187
  * `import { Home as HouseIcon }` means the config's `icon: HouseIcon` is
83
- * lucide's `Home`.
188
+ * lucide's `Home`. Read from the syntax tree, so an import mentioned only in
189
+ * a comment is not a real import.
84
190
  */
85
- function parseLucideImports(content) {
191
+ function parseLucideImports(sourceFile, ts) {
86
192
  const imports = new Map()
87
193
 
88
- for (const match of content.matchAll(/import\s*(?:type\s*)?\{([^}]*)\}\s*from\s*['"]lucide-react['"]/g)) {
89
- for (const specifier of match[1].split(',')) {
90
- const parts = specifier.trim().replace(/^type\s+/, '').split(/\s+as\s+/)
91
- const exported = parts[0]?.trim()
92
- const local = (parts[1] || parts[0])?.trim()
93
- if (exported && local) {
94
- imports.set(local, exported)
95
- }
194
+ for (const statement of sourceFile.statements) {
195
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteralLike(statement.moduleSpecifier)) continue
196
+ if (statement.moduleSpecifier.text !== 'lucide-react') continue
197
+
198
+ const namedBindings = statement.importClause?.namedBindings
199
+ if (!namedBindings || !ts.isNamedImports(namedBindings)) continue
200
+
201
+ for (const element of namedBindings.elements) {
202
+ if (element.isTypeOnly) continue
203
+ imports.set(element.name.text, (element.propertyName ?? element.name).text)
96
204
  }
97
205
  }
98
206
 
99
207
  return imports
100
208
  }
101
209
 
210
+ /** The expression a `get icon()`-style accessor's body returns, when the
211
+ * body is exactly one `return <expr>` statement — the only shape simple
212
+ * enough to read a static icon name from. Anything else (no return,
213
+ * several statements, a conditional) has no single value to extract, so the
214
+ * caller falls back to the accessor node itself, which resolves as neither
215
+ * a literal nor a lucide import and is therefore reported as unresolved
216
+ * rather than silently dropped. */
217
+ function accessorReturnExpression(node, ts) {
218
+ const [statement] = node.body?.statements ?? []
219
+ return node.body?.statements.length === 1 && statement && ts.isReturnStatement(statement) && statement.expression
220
+ ? statement.expression
221
+ : null
222
+ }
223
+
224
+ /** Whether a `set icon(v)` accessor has a `get icon()` beside it in the same
225
+ * object literal or class, the member that actually supplies the value. */
226
+ function hasSiblingGetter(setter, ts) {
227
+ const siblings = setter.parent.properties ?? setter.parent.members ?? []
228
+ const key = propertyKeyName(setter.name, ts)
229
+ return siblings.some(sibling => ts.isGetAccessorDeclaration(sibling) && propertyKeyName(sibling.name, ts) === key)
230
+ }
231
+
232
+ /**
233
+ * Every icon-bearing property value in the tree, wherever it is nested —
234
+ * `icon` or `iconName` written plain, quoted or computed (`node.initializer`),
235
+ * as a class field with an initializer, the shorthand form `{ icon }`, where
236
+ * the property's own name doubles as the value (`node.name`), and a getter
237
+ * (`get icon() { return 'Wallet' }`), an accessor object configs use the same
238
+ * as a plain property.
239
+ *
240
+ * A member under an icon key that holds no name to read — a method
241
+ * (`icon() { ... }`, whose value is the function itself) or a setter with no
242
+ * getter beside it (whose value reads as undefined) — is collected as the
243
+ * member node, which resolves as neither a literal nor a lucide import and is
244
+ * therefore reported as unresolved rather than silently dropped.
245
+ */
246
+ function findIconPropertyValues(sourceFile, ts) {
247
+ const values = []
248
+
249
+ const visit = node => {
250
+ if ((ts.isPropertyAssignment(node) || (ts.isPropertyDeclaration(node) && node.initializer)) && ICON_KEYS.has(propertyKeyName(node.name, ts))) {
251
+ values.push(node.initializer)
252
+ } else if (ts.isShorthandPropertyAssignment(node) && ICON_KEYS.has(node.name.text)) {
253
+ values.push(node.name)
254
+ } else if (ts.isGetAccessorDeclaration(node) && ICON_KEYS.has(propertyKeyName(node.name, ts))) {
255
+ values.push(accessorReturnExpression(node, ts) ?? node)
256
+ } else if (ts.isMethodDeclaration(node) && ICON_KEYS.has(propertyKeyName(node.name, ts))) {
257
+ values.push(node)
258
+ } else if (ts.isSetAccessorDeclaration(node) && ICON_KEYS.has(propertyKeyName(node.name, ts)) && !hasSiblingGetter(node, ts)) {
259
+ values.push(node)
260
+ }
261
+ ts.forEachChild(node, visit)
262
+ }
263
+ visit(sourceFile)
264
+
265
+ return values
266
+ }
267
+
102
268
  /**
103
269
  * Icon names referenced by a single config file. Exported for tests.
104
270
  */
105
- export function extractIconNames(content) {
106
- const lucideImports = parseLucideImports(content)
271
+ export async function extractIconNames(content, filePath = 'icons.config.ts', projectRoot) {
272
+ const { ts, sourceFile } = await parseIconSource(content, filePath, projectRoot)
273
+ const lucideImports = parseLucideImports(sourceFile, ts)
107
274
  const names = []
108
275
 
109
- // `icon: Users` or `'icon': Users` — only counts when the identifier came
110
- // from lucide-react
111
- for (const match of content.matchAll(/(?:\bicon|['"]icon['"])\s*:\s*([A-Za-z_$][\w$]*)/g)) {
112
- const exported = lucideImports.get(match[1])
113
- if (exported) {
114
- names.push(exported)
276
+ for (const value of findIconPropertyValues(sourceFile, ts)) {
277
+ const initializer = unwrapTransparentExpression(value, ts)
278
+
279
+ // `icon: Users` — only counts when the identifier came from lucide-react
280
+ if (ts.isIdentifier(initializer)) {
281
+ const exported = lucideImports.get(initializer.text)
282
+ if (exported) names.push(exported)
283
+ continue
115
284
  }
116
- }
117
285
 
118
- // `icon: 'Users'` or `icon: 'pie-chart'` — a theme's sidebar sections and
119
- // block configs use both spellings; the caller folds kebab-case into the
120
- // lucide export name and validates the result against lucide's export list.
121
- for (const match of content.matchAll(/(?:\bicon|['"]icon['"])\s*:\s*['"]([A-Za-z][\w$-]*)['"]/g)) {
122
- names.push(match[1])
286
+ // `icon: 'Users'` or `icon: 'pie-chart'` — a theme's sidebar sections and
287
+ // block configs use both spellings; the caller folds kebab-case into the
288
+ // lucide export name and validates the result against lucide's export
289
+ // list. The generated registry is TypeScript built from this string, so
290
+ // anything not shaped like a name is dropped rather than trusted.
291
+ if (ts.isStringLiteralLike(initializer) && SAFE_NAME.test(initializer.text)) {
292
+ names.push(initializer.text)
293
+ }
123
294
  }
124
295
 
125
296
  return names
@@ -131,26 +302,282 @@ export function extractIconNames(content) {
131
302
  *
132
303
  * Only a direct named import of lucide-react and a string literal reach the
133
304
  * registry. Anything else — a namespace access (`I.Users`), an identifier
134
- * aliased through another module, a component of the project's own — is not
135
- * resolvable by reading this file, so it never enters the registry and
136
- * resolveIcon falls back. That is silent, hence the warning.
305
+ * aliased through another module, a call (`chooseIcon()`), a component of the
306
+ * project's own — is not resolvable by reading this file, so it never enters
307
+ * the registry and resolveIcon falls back. That is silent, hence the warning:
308
+ * whatever isn't one of the two resolvable shapes is reported, whatever kind
309
+ * of expression it is, rather than allowlisting the shapes worth warning about.
137
310
  */
138
- export function findUnresolvedIconRefs(content) {
139
- const lucideImports = parseLucideImports(content)
311
+ export async function findUnresolvedIconRefs(content, filePath = 'icons.config.ts', projectRoot) {
312
+ const { ts, sourceFile } = await parseIconSource(content, filePath, projectRoot)
313
+ const lucideImports = parseLucideImports(sourceFile, ts)
140
314
  const unresolved = []
141
315
 
142
- for (const match of content.matchAll(/(?:\bicon|['"]icon['"])\s*:\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g)) {
143
- const reference = match[1]
144
- const isMember = reference.includes('.')
316
+ for (const value of findIconPropertyValues(sourceFile, ts)) {
317
+ const initializer = unwrapTransparentExpression(value, ts)
145
318
 
146
- if (isMember || !lucideImports.has(reference)) {
147
- unresolved.push(reference)
319
+ // A wrapper that resolved to a usable literal or a known lucide import is
320
+ // not unresolved — extractIconNames already has it. Quoting the source as
321
+ // written (wrapper included) so the warning points at what a developer
322
+ // would grep for.
323
+ const resolvedAsLiteral = ts.isStringLiteralLike(initializer) && SAFE_NAME.test(initializer.text)
324
+ const resolvedAsLucideImport = ts.isIdentifier(initializer) && lucideImports.has(initializer.text)
325
+
326
+ if (!resolvedAsLiteral && !resolvedAsLucideImport) {
327
+ unresolved.push(value.getText(sourceFile))
148
328
  }
149
329
  }
150
330
 
151
331
  return unresolved
152
332
  }
153
333
 
334
+ /** The string literal an expression evaluates to, unwrapping a JSX
335
+ * expression container (`{'pie-chart'}`) and any transparent TypeScript
336
+ * wrapper (`'pie-chart' satisfies string`) around it, or null when the
337
+ * expression isn't a literal — a runtime value that resolveIcon or
338
+ * DynamicIcon reads at render time, not a name this build can see. */
339
+ function literalTextOf(expression, ts) {
340
+ const unwrapped = unwrapTransparentExpression(ts.isJsxExpression(expression) ? expression.expression : expression, ts)
341
+ return unwrapped && ts.isStringLiteralLike(unwrapped) ? unwrapped.text : null
342
+ }
343
+
344
+ /** Where a theme or plugin actually gets DynamicIcon and resolveIcon from —
345
+ * every subpath core publishes them under, via its `./lib/*` and
346
+ * `./components/*` export wildcards. Matched as a whole package name, not a
347
+ * string prefix: `@nextsparkjs/core-fake/lib/icons` starts with this string
348
+ * but is a different package entirely. */
349
+ const CORE_PACKAGE_PREFIX = '@nextsparkjs/core'
350
+
351
+ /** Whether a module specifier is core itself or one of its subpaths — the
352
+ * whole `@nextsparkjs/core` package segment, not merely a string prefix of it. */
353
+ function isCorePackageSpecifier(moduleSpecifierText) {
354
+ return moduleSpecifierText === CORE_PACKAGE_PREFIX || moduleSpecifierText.startsWith(`${CORE_PACKAGE_PREFIX}/`)
355
+ }
356
+
357
+ /**
358
+ * Local bindings for whatever a file imports from core, so a call site is
359
+ * matched by what it actually refers to rather than by spelling: a named
360
+ * import tracks straight to its local name (aliased or not), a namespace
361
+ * import (`import * as Core from '@nextsparkjs/core/...'`) is tracked so
362
+ * `Core.resolveIcon(...)` resolves too.
363
+ */
364
+ function parseCoreImports(sourceFile, ts) {
365
+ const named = new Map() // local name -> imported name
366
+ const namespaces = new Set() // local name bound to `import * as X`
367
+
368
+ for (const statement of sourceFile.statements) {
369
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteralLike(statement.moduleSpecifier)) continue
370
+ if (!isCorePackageSpecifier(statement.moduleSpecifier.text)) continue
371
+
372
+ const namedBindings = statement.importClause?.namedBindings
373
+ if (!namedBindings) continue
374
+
375
+ if (ts.isNamedImports(namedBindings)) {
376
+ for (const element of namedBindings.elements) {
377
+ if (element.isTypeOnly) continue
378
+ named.set(element.name.text, (element.propertyName ?? element.name).text)
379
+ }
380
+ } else if (ts.isNamespaceImport(namedBindings)) {
381
+ namespaces.add(namedBindings.name.text)
382
+ }
383
+ }
384
+
385
+ return { named, namespaces }
386
+ }
387
+
388
+ /** The bare names a node declares in its own lexical scope: a function's
389
+ * parameters, a `let`/`const`/`var` declaration's bindings, a catch clause's
390
+ * binding. Destructured names are collected too, so `function f({ resolveIcon })`
391
+ * shadows the same as `function f(resolveIcon)`. */
392
+ function collectBindingNames(name, ts, into) {
393
+ if (ts.isIdentifier(name)) {
394
+ into.add(name.text)
395
+ } else if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
396
+ for (const element of name.elements) {
397
+ if (ts.isBindingElement(element)) collectBindingNames(element.name, ts, into)
398
+ }
399
+ }
400
+ }
401
+
402
+ /** Whether `node` starts its own `var` scope, one a hoisted `var` cannot
403
+ * escape: any function (declaration, expression, arrow, method, getter,
404
+ * setter, constructor), a class `static { }` block, or a TypeScript
405
+ * `namespace`, which compiles to a function of its own. This is where
406
+ * `collectHoistedVarNames` below stops recursing. */
407
+ function isVarScopeBoundary(node, ts) {
408
+ return ts.isFunctionLike(node) || ts.isClassStaticBlockDeclaration(node) || ts.isModuleDeclaration(node)
409
+ }
410
+
411
+ /** Every `var`-declared name nested anywhere under `node` — inside an `if`,
412
+ * a loop, a `switch`, as deep as it goes — except inside a nested `var`
413
+ * scope (see isVarScopeBoundary). Unlike `let`/`const`, `var` ignores block
414
+ * boundaries, so a single level of `node.statements` (what the block/module
415
+ * branch below reads for `let`/`const`) misses one declared inside a nested
416
+ * block; this walks the whole subtree instead, the way hoisting actually
417
+ * works. */
418
+ function collectHoistedVarNames(node, ts, into) {
419
+ if (isVarScopeBoundary(node, ts)) return
420
+
421
+ if (ts.isVariableStatement(node) && (node.declarationList.flags & ts.NodeFlags.BlockScoped) === 0) {
422
+ for (const declaration of node.declarationList.declarations) collectBindingNames(declaration.name, ts, into)
423
+ } else if (
424
+ (ts.isForStatement(node) || ts.isForInStatement(node) || ts.isForOfStatement(node)) &&
425
+ node.initializer &&
426
+ ts.isVariableDeclarationList(node.initializer) &&
427
+ (node.initializer.flags & ts.NodeFlags.BlockScoped) === 0
428
+ ) {
429
+ for (const declaration of node.initializer.declarations) collectBindingNames(declaration.name, ts, into)
430
+ }
431
+
432
+ ts.forEachChild(node, child => collectHoistedVarNames(child, ts, into))
433
+ }
434
+
435
+ /** The declarations directly in `statements` that introduce a name into
436
+ * their enclosing scope: function, class, `let`/`const`/`var` declarations,
437
+ * and TypeScript's value-bearing `enum`, `namespace` and `import x = ...`.
438
+ * Shared by a block/module's own statement list and by a `switch`'s case
439
+ * clauses, which — unlike a block per clause — all share one scope across
440
+ * the whole switch. */
441
+ function blockScopedBindingNames(statements, ts, into) {
442
+ for (const statement of statements) {
443
+ if (
444
+ (ts.isFunctionDeclaration(statement) ||
445
+ ts.isClassDeclaration(statement) ||
446
+ ts.isEnumDeclaration(statement) ||
447
+ ts.isModuleDeclaration(statement) ||
448
+ ts.isImportEqualsDeclaration(statement)) &&
449
+ statement.name &&
450
+ ts.isIdentifier(statement.name)
451
+ ) {
452
+ into.add(statement.name.text)
453
+ } else if (ts.isVariableStatement(statement)) {
454
+ for (const declaration of statement.declarationList.declarations) {
455
+ collectBindingNames(declaration.name, ts, into)
456
+ }
457
+ }
458
+ }
459
+ }
460
+
461
+ /** The names `node` introduces into its own scope, or an empty set when it
462
+ * introduces none. Used while walking the tree to track which imported names
463
+ * are shadowed by a closer local declaration.
464
+ *
465
+ * A function's parameters — a getter's, a setter's and a constructor's as
466
+ * much as a method's or an arrow's — and a named function expression's own
467
+ * name are scoped to its parameter initializers and body. Its hoisted `var`
468
+ * names are added separately when the walk enters its body, because parameter
469
+ * initializers cannot see declarations from that body. A class expression's
470
+ * own name is scoped to itself, as is every `var` hoisted in a class `static
471
+ * { }` block or a `namespace` body. A block's (or the module's, or a
472
+ * namespace's) function, class and `let`/`const`/`var` declarations are
473
+ * scoped to the whole block, not merely to the statement that introduces
474
+ * them — a later sibling statement has to see the shadow too, the way it
475
+ * would at runtime. A `for`/`for-in`/`for-of` loop's own `let`/`const`
476
+ * initializer is scoped to the whole loop, not just to itself, and every
477
+ * clause of a `switch` shares one scope. */
478
+ function ownScopeBindingNames(node, ts) {
479
+ const names = new Set()
480
+
481
+ if (ts.isFunctionLike(node)) {
482
+ for (const parameter of node.parameters) collectBindingNames(parameter.name, ts, names)
483
+ if (ts.isFunctionExpression(node) && node.name) {
484
+ names.add(node.name.text)
485
+ }
486
+ } else if (ts.isClassExpression(node) && node.name) {
487
+ names.add(node.name.text)
488
+ } else if (ts.isClassStaticBlockDeclaration(node)) {
489
+ collectHoistedVarNames(node.body, ts, names)
490
+ } else if (ts.isModuleBlock(node)) {
491
+ blockScopedBindingNames(node.statements, ts, names)
492
+ for (const statement of node.statements) collectHoistedVarNames(statement, ts, names)
493
+ } else if (ts.isVariableDeclarationList(node)) {
494
+ for (const declaration of node.declarations) collectBindingNames(declaration.name, ts, names)
495
+ } else if ((ts.isForStatement(node) || ts.isForInStatement(node) || ts.isForOfStatement(node)) && node.initializer && ts.isVariableDeclarationList(node.initializer)) {
496
+ for (const declaration of node.initializer.declarations) collectBindingNames(declaration.name, ts, names)
497
+ } else if (ts.isCatchClause(node) && node.variableDeclaration) {
498
+ collectBindingNames(node.variableDeclaration.name, ts, names)
499
+ } else if (ts.isCaseBlock(node)) {
500
+ for (const clause of node.clauses) blockScopedBindingNames(clause.statements, ts, names)
501
+ } else if (ts.isBlock(node) || ts.isSourceFile(node)) {
502
+ blockScopedBindingNames(node.statements, ts, names)
503
+ if (ts.isSourceFile(node)) collectHoistedVarNames(node, ts, names)
504
+ }
505
+
506
+ return names
507
+ }
508
+
509
+ /**
510
+ * Whether `expression` — a JSX tag name or a call's callee — is the given
511
+ * core export, resolved through the file's own imports rather than by
512
+ * comparing text: `import { resolveIcon as ri }` still matches `ri(...)` and
513
+ * `ri!(...)`, `import * as Core from '@nextsparkjs/core/lib/icons'` still
514
+ * matches `Core.resolveIcon(...)`, and a same-named local that was never
515
+ * imported from core — a theme's own `resolveIcon` helper, or a function
516
+ * parameter that shadows the import — does not.
517
+ */
518
+ function referencesCoreExport(expression, exportName, imports, ts, shadowStack) {
519
+ const unwrapped = unwrapTransparentExpression(expression, ts)
520
+ const isShadowed = name => shadowStack.some(scope => scope.has(name))
521
+
522
+ if (ts.isIdentifier(unwrapped)) {
523
+ return !isShadowed(unwrapped.text) && imports.named.get(unwrapped.text) === exportName
524
+ }
525
+ if (ts.isPropertyAccessExpression(unwrapped) && ts.isIdentifier(unwrapped.expression)) {
526
+ return !isShadowed(unwrapped.expression.text) && imports.namespaces.has(unwrapped.expression.text) && unwrapped.name.text === exportName
527
+ }
528
+ return false
529
+ }
530
+
531
+ /**
532
+ * Icon names passed as a string literal to DynamicIcon's `name` prop or to
533
+ * resolveIcon's first argument. Exported for tests.
534
+ *
535
+ * Both accept a runtime variable too (`resolveIcon(item.icon)`, the normal
536
+ * way to render a value that came from the database) — that call is left
537
+ * alone, since there is no name in it to register.
538
+ */
539
+ export async function extractLiteralIconCallNames(content, filePath = 'icons.tsx', projectRoot) {
540
+ const { ts, sourceFile } = await parseIconSource(content, filePath, projectRoot)
541
+ const imports = parseCoreImports(sourceFile, ts)
542
+ const names = []
543
+
544
+ // A local declaration (a function parameter, a `let`/`const`, a catch
545
+ // binding) shadows an import of the same name for the rest of its scope,
546
+ // the same way it would at runtime — so the stack of scopes entered so far
547
+ // travels with the walk instead of imports being looked up by name alone.
548
+ const visit = (node, shadowStack) => {
549
+ const ownNames = ownScopeBindingNames(node, ts)
550
+ const nextStack = ownNames.size > 0 ? [...shadowStack, ownNames] : shadowStack
551
+
552
+ if ((ts.isJsxSelfClosingElement(node) || ts.isJsxOpeningElement(node)) && referencesCoreExport(node.tagName, 'DynamicIcon', imports, ts, nextStack)) {
553
+ for (const attribute of node.attributes.properties) {
554
+ if (!ts.isJsxAttribute(attribute) || attribute.name.text !== 'name' || !attribute.initializer) continue
555
+ const name = literalTextOf(attribute.initializer, ts)
556
+ if (name && SAFE_NAME.test(name)) names.push(name)
557
+ }
558
+ } else if (ts.isCallExpression(node) && referencesCoreExport(node.expression, 'resolveIcon', imports, ts, nextStack)) {
559
+ const [firstArgument] = node.arguments
560
+ const name = firstArgument && literalTextOf(firstArgument, ts)
561
+ if (name && SAFE_NAME.test(name)) names.push(name)
562
+ }
563
+
564
+ if (ts.isFunctionLike(node) && node.body) {
565
+ const bodyNames = new Set()
566
+ collectHoistedVarNames(node.body, ts, bodyNames)
567
+ const bodyStack = bodyNames.size > 0 ? [...nextStack, bodyNames] : nextStack
568
+ ts.forEachChild(node, child => {
569
+ const childStack = child === node.body ? bodyStack : node.parameters.includes(child) ? nextStack : shadowStack
570
+ visit(child, childStack)
571
+ })
572
+ } else {
573
+ ts.forEachChild(node, child => visit(child, nextStack))
574
+ }
575
+ }
576
+ visit(sourceFile, [])
577
+
578
+ return names
579
+ }
580
+
154
581
  async function collectConfigFiles(dir, matcher, found = []) {
155
582
  if (!existsSync(dir)) {
156
583
  return found
@@ -195,12 +622,17 @@ function coreEntitiesDir(config) {
195
622
  export async function discoverIcons(blocks, config) {
196
623
  const candidates = new Set(FALLBACK_ICONS)
197
624
 
198
- // Only the configs whose icons reach resolveIcon. Widening this would put
199
- // icons in the dashboard bundle that nothing can ask for.
625
+ // Only the configs whose icons reach resolveIcon: entity configs (core,
626
+ // themes and plugins), block configs, and every theme's or plugin's
627
+ // config/*.config.ts. Widening this further would put icons in the
628
+ // dashboard bundle that nothing can ask for. Test files are excluded even
629
+ // though a config rarely lives under one, for the same reason call sources
630
+ // exclude them below.
631
+ const isIconSource = filePath => isIconSourcePath(filePath) && !isTestFilePath(filePath)
200
632
  const iconSources = [
201
- ...(await collectConfigFiles(coreEntitiesDir(config), isIconSourcePath)),
202
- ...(await collectConfigFiles(config.themesDir, isIconSourcePath)),
203
- ...(await collectConfigFiles(config.pluginsDir, isIconSourcePath))
633
+ ...(await collectConfigFiles(coreEntitiesDir(config), isIconSource)),
634
+ ...(await collectConfigFiles(config.themesDir, isIconSource)),
635
+ ...(await collectConfigFiles(config.pluginsDir, isIconSource))
204
636
  ]
205
637
 
206
638
  const unresolved = []
@@ -208,10 +640,10 @@ export async function discoverIcons(blocks, config) {
208
640
  for (const configPath of iconSources) {
209
641
  try {
210
642
  const content = await readFile(configPath, 'utf8')
211
- for (const name of extractIconNames(content)) {
643
+ for (const name of await extractIconNames(content, configPath, config.projectRoot)) {
212
644
  candidates.add(name)
213
645
  }
214
- for (const reference of findUnresolvedIconRefs(content)) {
646
+ for (const reference of await findUnresolvedIconRefs(content, configPath, config.projectRoot)) {
215
647
  unresolved.push({ configPath, reference })
216
648
  }
217
649
  } catch {
@@ -226,6 +658,29 @@ export async function discoverIcons(blocks, config) {
226
658
  )
227
659
  }
228
660
 
661
+ // A theme or plugin can also name an icon directly in its own component
662
+ // code rather than through a config, so its trees are scanned for that too.
663
+ // Core is not: every core call site resolves a runtime value (item.icon,
664
+ // config.iconName), never a literal, so there is nothing to add here.
665
+ // Test files are excluded: a spec exercising DynamicIcon or resolveIcon
666
+ // with a literal name isn't a real call site the production bundle needs.
667
+ const isIconCallSource = filePath => isIconCallSourcePath(filePath) && !isTestFilePath(filePath)
668
+ const callSources = [
669
+ ...(await collectConfigFiles(config.themesDir, isIconCallSource)),
670
+ ...(await collectConfigFiles(config.pluginsDir, isIconCallSource))
671
+ ]
672
+
673
+ for (const sourcePath of callSources) {
674
+ try {
675
+ const content = await readFile(sourcePath, 'utf8')
676
+ for (const name of await extractLiteralIconCallNames(content, sourcePath, config.projectRoot)) {
677
+ candidates.add(name)
678
+ }
679
+ } catch {
680
+ verbose(`[icons] Could not read ${sourcePath}`)
681
+ }
682
+ }
683
+
229
684
  for (const block of blocks || []) {
230
685
  if (block.icon) {
231
686
  candidates.add(block.icon)
@@ -43,7 +43,7 @@ export async function discoverTemplates(config = DEFAULT_CONFIG) {
43
43
  ║ Or ensure your .env file has NEXT_PUBLIC_ACTIVE_THEME set correctly. ║
44
44
  ╚══════════════════════════════════════════════════════════════════════════════╝
45
45
  `
46
- console.error(errorMsg)
46
+ for (const line of errorMsg.split('\n')) console.error(line)
47
47
  throw new Error('NEXT_PUBLIC_ACTIVE_THEME environment variable is required for template discovery')
48
48
  }
49
49
 
@@ -140,7 +140,7 @@ export async function generateBillingRegistry(activeTheme, contentsDir, config)
140
140
  let billingConfig
141
141
  try {
142
142
  // Use jiti to import .ts files at build time (Node.js can't import .ts natively)
143
- const jiti = createJiti(import.meta.url, { interopDefault: true })
143
+ const jiti = createJiti(import.meta.url, { interopDefault: true, fsCache: false })
144
144
  const module = await jiti.import(absolutePath)
145
145
  billingConfig = module.billingConfig || module.default
146
146
  } catch (error) {