@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.
- package/dist/components/api/keys/CreateApiKeyDialog.d.ts.map +1 -1
- package/dist/components/api/keys/CreateApiKeyDialog.js +2 -1
- package/dist/components/app/layouts/PublicFooter.d.ts.map +1 -1
- package/dist/components/app/layouts/PublicFooter.js +2 -23
- package/dist/components/app/misc/ThemeToggle.d.ts.map +1 -1
- package/dist/components/app/misc/ThemeToggle.js +2 -1
- package/dist/components/auth/forms/LoginForm.js +1 -1
- package/dist/components/auth/forms/SignupForm.d.ts.map +1 -1
- package/dist/components/auth/forms/SignupForm.js +2 -1
- package/dist/components/auth/pages/AuthErrorPage.d.ts.map +1 -1
- package/dist/components/auth/pages/AuthErrorPage.js +2 -1
- package/dist/components/billing/ManageBillingButton.d.ts.map +1 -1
- package/dist/components/billing/ManageBillingButton.js +2 -1
- package/dist/components/dashboard/block-editor/array-field.d.ts.map +1 -1
- package/dist/components/dashboard/block-editor/array-field.js +2 -1
- package/dist/components/dashboard/block-editor/block-picker.d.ts.map +1 -1
- package/dist/components/dashboard/block-editor/block-picker.js +2 -1
- package/dist/components/dashboard/block-editor/builder-editor-view.d.ts.map +1 -1
- package/dist/components/dashboard/block-editor/builder-editor-view.js +3 -2
- package/dist/components/dashboard/block-editor/config-panel.d.ts.map +1 -1
- package/dist/components/dashboard/block-editor/config-panel.js +3 -2
- package/dist/components/dashboard/block-editor/dynamic-form.d.ts.map +1 -1
- package/dist/components/dashboard/block-editor/dynamic-form.js +2 -1
- package/dist/components/dashboard/block-editor/entity-fields-sidebar.d.ts.map +1 -1
- package/dist/components/dashboard/block-editor/entity-fields-sidebar.js +3 -2
- package/dist/components/dashboard/block-editor/pattern-reference-preview.d.ts.map +1 -1
- package/dist/components/dashboard/block-editor/pattern-reference-preview.js +2 -1
- package/dist/components/dashboard/layouts/TopNavbar.d.ts.map +1 -1
- package/dist/components/dashboard/layouts/TopNavbar.js +4 -3
- package/dist/components/dashboard/misc/SearchDropdown.d.ts.map +1 -1
- package/dist/components/dashboard/misc/SearchDropdown.js +2 -1
- package/dist/components/dashboard/mobile/MobileTopBar.d.ts.map +1 -1
- package/dist/components/dashboard/mobile/MobileTopBar.js +2 -1
- package/dist/components/devtools/BlockDetailViewer.d.ts.map +1 -1
- package/dist/components/devtools/BlockDetailViewer.js +2 -1
- package/dist/components/devtools/BlocksViewer.d.ts.map +1 -1
- package/dist/components/devtools/BlocksViewer.js +2 -1
- package/dist/components/devtools/ConfigViewer.d.ts.map +1 -1
- package/dist/components/devtools/ConfigViewer.js +3 -2
- package/dist/components/devtools/TestCasesViewer.d.ts.map +1 -1
- package/dist/components/devtools/TestCasesViewer.js +3 -2
- package/dist/components/devtools/TestCoverageDashboard.d.ts.map +1 -1
- package/dist/components/devtools/TestCoverageDashboard.js +2 -1
- package/dist/components/devtools/api-explorer/ApiDocsModal.d.ts.map +1 -1
- package/dist/components/devtools/api-explorer/ApiDocsModal.js +0 -0
- package/dist/components/devtools/api-explorer/ApiExplorer.d.ts.map +1 -1
- package/dist/components/devtools/api-explorer/ApiExplorer.js +2 -1
- package/dist/components/devtools/scheduled-actions/actions-table.d.ts.map +1 -1
- package/dist/components/devtools/scheduled-actions/actions-table.js +3 -2
- package/dist/components/docs/docs-sidebar.d.ts.map +1 -1
- package/dist/components/docs/docs-sidebar.js +3 -2
- package/dist/components/entities/EntityFieldRenderer.d.ts.map +1 -1
- package/dist/components/entities/EntityFieldRenderer.js +2 -1
- package/dist/components/entities/EntityList.d.ts.map +1 -1
- package/dist/components/entities/EntityList.js +2 -1
- package/dist/components/entities/EntityTable.d.ts.map +1 -1
- package/dist/components/entities/EntityTable.js +3 -2
- package/dist/components/entities/ErrorBoundary.d.ts.map +1 -1
- package/dist/components/entities/ErrorBoundary.js +2 -1
- package/dist/components/entities/wrappers/EntityDetailWrapper.d.ts.map +1 -1
- package/dist/components/entities/wrappers/EntityDetailWrapper.js +3 -2
- package/dist/components/media/MediaCard.d.ts.map +1 -1
- package/dist/components/media/MediaCard.js +2 -1
- package/dist/components/media/MediaDetailPanel.d.ts.map +1 -1
- package/dist/components/media/MediaDetailPanel.js +2 -1
- package/dist/components/media/MediaList.d.ts.map +1 -1
- package/dist/components/media/MediaList.js +2 -1
- package/dist/components/media/MediaSelector.d.ts.map +1 -1
- package/dist/components/media/MediaSelector.js +2 -1
- package/dist/components/media/MediaUploadZone.d.ts.map +1 -1
- package/dist/components/media/MediaUploadZone.js +2 -1
- package/dist/components/public/entities/PublicEntityDetail.d.ts.map +1 -1
- package/dist/components/public/entities/PublicEntityDetail.js +2 -1
- package/dist/components/teams/TeamPendingInvitations.d.ts.map +1 -1
- package/dist/components/teams/TeamPendingInvitations.js +3 -2
- package/dist/components/ui/accordion.js +1 -1
- package/dist/components/ui/audio-upload.d.ts.map +1 -1
- package/dist/components/ui/audio-upload.js +2 -1
- package/dist/components/ui/avatar.d.ts +17 -1
- package/dist/components/ui/avatar.d.ts.map +1 -1
- package/dist/components/ui/avatar.js +29 -2
- package/dist/components/ui/badge.js +1 -1
- package/dist/components/ui/breadcrumb.d.ts +13 -0
- package/dist/components/ui/breadcrumb.d.ts.map +1 -1
- package/dist/components/ui/breadcrumb.js +23 -10
- package/dist/components/ui/button.js +1 -1
- package/dist/components/ui/card.js +1 -1
- package/dist/components/ui/checkbox.js +1 -1
- package/dist/components/ui/file-upload.d.ts.map +1 -1
- package/dist/components/ui/file-upload.js +2 -1
- package/dist/components/ui/image-upload.d.ts.map +1 -1
- package/dist/components/ui/image-upload.js +4 -3
- package/dist/components/ui/input.js +1 -1
- package/dist/components/ui/label.js +1 -1
- package/dist/components/ui/optimized-image.d.ts.map +1 -1
- package/dist/components/ui/optimized-image.js +5 -0
- package/dist/components/ui/pagination.d.ts +1 -1
- package/dist/components/ui/pagination.d.ts.map +1 -1
- package/dist/components/ui/pagination.js +3 -0
- package/dist/components/ui/progress.js +1 -1
- package/dist/components/ui/relation-display.d.ts.map +1 -1
- package/dist/components/ui/relation-display.js +3 -2
- package/dist/components/ui/separator.js +1 -1
- package/dist/components/ui/simple-relation-select.d.ts.map +1 -1
- package/dist/components/ui/simple-relation-select.js +7 -6
- package/dist/components/ui/skeleton.js +1 -1
- package/dist/components/ui/slider.js +1 -1
- package/dist/components/ui/switch.js +1 -1
- package/dist/components/ui/tabs.js +1 -1
- package/dist/components/ui/textarea.js +1 -1
- package/dist/components/ui/user-display.d.ts.map +1 -1
- package/dist/components/ui/user-display.js +2 -1
- package/dist/components/ui/user-select.d.ts.map +1 -1
- package/dist/components/ui/user-select.js +2 -1
- package/dist/components/ui/video-upload.d.ts.map +1 -1
- package/dist/components/ui/video-upload.js +3 -2
- package/dist/components/users/tables/UsersTable.d.ts.map +1 -1
- package/dist/components/users/tables/UsersTable.js +3 -2
- package/dist/contexts/SubscriptionContext.d.ts.map +1 -1
- package/dist/contexts/SubscriptionContext.js +4 -3
- package/dist/contexts/TeamContext.d.ts.map +1 -1
- package/dist/contexts/TeamContext.js +3 -2
- package/dist/hooks/useAccountPreferencesSync.d.ts.map +1 -1
- package/dist/hooks/useAccountPreferencesSync.js +2 -1
- package/dist/hooks/useApiExplorerNavigation.d.ts.map +1 -1
- package/dist/hooks/useApiExplorerNavigation.js +2 -1
- package/dist/hooks/useAuth.d.ts.map +1 -1
- package/dist/hooks/useAuth.js +3 -2
- package/dist/hooks/useEnsureUserMetadata.d.ts.map +1 -1
- package/dist/hooks/useEnsureUserMetadata.js +3 -2
- package/dist/hooks/useEntityWithMeta.d.ts.map +1 -1
- package/dist/hooks/useEntityWithMeta.js +3 -2
- package/dist/hooks/useInvoice.d.ts.map +1 -1
- package/dist/hooks/useInvoice.js +2 -1
- package/dist/hooks/useInvoices.d.ts.map +1 -1
- package/dist/hooks/useInvoices.js +2 -1
- package/dist/hooks/useMedia.d.ts.map +1 -1
- package/dist/hooks/useMedia.js +10 -9
- package/dist/hooks/useMediaUpload.d.ts.map +1 -1
- package/dist/hooks/useMediaUpload.js +2 -1
- package/dist/hooks/useMembership.d.ts.map +1 -1
- package/dist/hooks/useMembership.js +3 -2
- package/dist/hooks/usePatternUsages.d.ts.map +1 -1
- package/dist/hooks/usePatternUsages.js +3 -2
- package/dist/hooks/usePrefetch.d.ts.map +1 -1
- package/dist/hooks/usePrefetch.js +3 -2
- package/dist/hooks/useQuota.d.ts.map +1 -1
- package/dist/hooks/useQuota.js +2 -1
- package/dist/hooks/useTeamInvitations.d.ts.map +1 -1
- package/dist/hooks/useTeamInvitations.js +4 -3
- package/dist/hooks/useTeamMembers.d.ts.map +1 -1
- package/dist/hooks/useTeamMembers.js +5 -4
- package/dist/hooks/useTeamUpdate.d.ts.map +1 -1
- package/dist/hooks/useTeamUpdate.js +2 -1
- package/dist/hooks/useUserProfile.d.ts.map +1 -1
- package/dist/hooks/useUserProfile.js +3 -2
- package/dist/hooks/useUserSettings.d.ts.map +1 -1
- package/dist/hooks/useUserSettings.js +3 -2
- package/dist/lib/actions/entity.actions.d.ts.map +1 -1
- package/dist/lib/actions/entity.actions.js +2 -1
- package/dist/lib/api/entities.d.ts.map +1 -1
- package/dist/lib/api/entities.js +4 -3
- package/dist/lib/auth/verify-email-link.d.ts +10 -0
- package/dist/lib/auth/verify-email-link.d.ts.map +1 -0
- package/dist/lib/auth/verify-email-link.js +14 -0
- package/dist/lib/auth-client.d.ts +2 -0
- package/dist/lib/auth-client.d.ts.map +1 -1
- package/dist/lib/auth-client.js +4 -0
- package/dist/lib/auth.d.ts.map +1 -1
- package/dist/lib/auth.js +10 -2
- package/dist/lib/base-path.d.ts +85 -0
- package/dist/lib/base-path.d.ts.map +1 -0
- package/dist/lib/base-path.js +79 -0
- package/dist/lib/billing/schema.d.ts +1 -1
- package/dist/lib/billing/schema.d.ts.map +1 -1
- package/dist/lib/billing/schema.js +1 -1
- package/dist/lib/blocks/sanitize-html.d.ts.map +1 -1
- package/dist/lib/blocks/sanitize-html.js +18 -2
- package/dist/lib/cache/revalidate-tag.d.ts +14 -0
- package/dist/lib/cache/revalidate-tag.d.ts.map +1 -0
- package/dist/lib/cache/revalidate-tag.js +8 -0
- package/dist/lib/config/dashboard-schema.d.ts +1 -1
- package/dist/lib/config/dashboard-schema.d.ts.map +1 -1
- package/dist/lib/config/dashboard-schema.js +1 -1
- package/dist/lib/config/types.d.ts +36 -16
- package/dist/lib/config/types.d.ts.map +1 -1
- package/dist/lib/docs/access.d.ts +28 -0
- package/dist/lib/docs/access.d.ts.map +1 -0
- package/dist/lib/docs/access.js +16 -0
- package/dist/lib/docs/parser.d.ts.map +1 -1
- package/dist/lib/docs/parser.js +2 -1
- package/dist/lib/docs/remark-doc-links.d.ts +14 -0
- package/dist/lib/docs/remark-doc-links.d.ts.map +1 -0
- package/dist/lib/docs/remark-doc-links.js +38 -0
- package/dist/lib/entities/api-generator.d.ts +1 -1
- package/dist/lib/entities/api-generator.d.ts.map +1 -1
- package/dist/lib/entities/api-generator.js +1 -1
- package/dist/lib/entities/schema-generator.d.ts +1 -1
- package/dist/lib/entities/schema-generator.d.ts.map +1 -1
- package/dist/lib/entities/schema-generator.js +1 -1
- package/dist/lib/mcp/engine.js +1 -1
- package/dist/lib/mcp/schema-builder.d.ts +1 -1
- package/dist/lib/mcp/schema-builder.d.ts.map +1 -1
- package/dist/lib/mcp/schema-builder.js +1 -1
- package/dist/lib/mcp/tool-generator.js +1 -1
- package/dist/lib/mcp/types.d.ts +1 -1
- package/dist/lib/mcp/types.d.ts.map +1 -1
- package/dist/lib/media/schemas.d.ts +1 -1
- package/dist/lib/media/schemas.d.ts.map +1 -1
- package/dist/lib/media/schemas.js +1 -1
- package/dist/lib/middleware/index.d.ts +5 -1
- package/dist/lib/middleware/index.d.ts.map +1 -1
- package/dist/lib/middleware/index.js +3 -2
- package/dist/lib/services/meta.service.d.ts +60 -0
- package/dist/lib/services/meta.service.d.ts.map +1 -1
- package/dist/lib/services/meta.service.js +132 -12
- package/dist/lib/services/middleware.service.d.ts.map +1 -1
- package/dist/lib/services/middleware.service.js +3 -2
- package/dist/lib/teams/schema.d.ts +1 -1
- package/dist/lib/teams/schema.d.ts.map +1 -1
- package/dist/lib/teams/schema.js +1 -1
- package/dist/lib/teams/teams.api.d.ts.map +1 -1
- package/dist/lib/teams/teams.api.js +4 -3
- package/dist/lib/user-data-client.d.ts.map +1 -1
- package/dist/lib/user-data-client.js +3 -2
- package/dist/lib/validation/invoices.d.ts +1 -1
- package/dist/lib/validation/invoices.d.ts.map +1 -1
- package/dist/lib/validation/invoices.js +1 -1
- package/dist/lib/validation-schemas.d.ts +1 -1
- package/dist/lib/validation-schemas.d.ts.map +1 -1
- package/dist/lib/validation-schemas.js +1 -1
- package/dist/lib/validation.d.ts +1 -1
- package/dist/lib/validation.d.ts.map +1 -1
- package/dist/lib/validation.js +1 -1
- package/dist/messages/de/footer.json +0 -1
- package/dist/messages/de/index.d.ts +0 -1
- package/dist/messages/de/index.d.ts.map +1 -1
- package/dist/messages/en/footer.json +0 -1
- package/dist/messages/en/index.d.ts +0 -1
- package/dist/messages/en/index.d.ts.map +1 -1
- package/dist/messages/es/footer.json +0 -1
- package/dist/messages/es/index.d.ts +0 -1
- package/dist/messages/es/index.d.ts.map +1 -1
- package/dist/messages/fr/footer.json +0 -1
- package/dist/messages/fr/index.d.ts +0 -1
- package/dist/messages/fr/index.d.ts.map +1 -1
- package/dist/messages/it/footer.json +0 -1
- package/dist/messages/it/index.d.ts +0 -1
- package/dist/messages/it/index.d.ts.map +1 -1
- package/dist/messages/pt/footer.json +0 -1
- package/dist/messages/pt/index.d.ts +0 -1
- package/dist/messages/pt/index.d.ts.map +1 -1
- package/dist/templates/app/(auth)/accept-invite/[token]/page.tsx +4 -3
- package/dist/templates/app/(auth)/verify-email/page.tsx +4 -3
- package/dist/templates/app/(public)/docs/[section]/[page]/page.tsx +11 -0
- package/dist/templates/app/api/auth/[...all]/route.ts +15 -14
- package/dist/templates/app/api/internal/user-metadata/route.ts +6 -8
- package/dist/templates/app/api/user/plan-flags/route.ts +1 -1
- package/dist/templates/app/api/user/profile/route.ts +12 -17
- package/dist/templates/app/api/v1/api-keys/route.ts +1 -1
- package/dist/templates/app/api/v1/auth/signup-with-invite/route.ts +2 -1
- package/dist/templates/app/api/v1/billing/cancel/route.ts +1 -1
- package/dist/templates/app/api/v1/billing/change-plan/route.ts +1 -1
- package/dist/templates/app/api/v1/billing/check-action/route.ts +1 -1
- package/dist/templates/app/api/v1/billing/checkout/route.ts +1 -1
- package/dist/templates/app/api/v1/blocks/validate/route.ts +1 -1
- package/dist/templates/app/api/v1/media/[id]/tags/route.ts +1 -1
- package/dist/templates/app/api/v1/post-categories/[id]/route.ts +1 -1
- package/dist/templates/app/api/v1/post-categories/route.ts +1 -1
- package/dist/templates/app/api/v1/teams/[teamId]/members/route.ts +2 -1
- package/dist/templates/app/api/v1/teams/[teamId]/subscription/route.ts +3 -5
- package/dist/templates/app/api/v1/teams/docs.md +4 -9
- package/dist/templates/app/api/v1/teams/switch/route.ts +1 -1
- package/dist/templates/app/api/v1/users/[id]/meta/[key]/route.ts +1 -1
- package/dist/templates/app/api/v1/users/[id]/route.ts +1 -1
- package/dist/templates/app/api/v1/users/route.ts +1 -1
- package/dist/templates/app/dashboard/(main)/media/page.tsx +3 -1
- package/dist/templates/app/dashboard/settings/api-keys/page.tsx +5 -4
- package/dist/templates/app/dashboard/settings/profile/page.tsx +3 -2
- package/dist/templates/app/devtools/scheduled-actions/page.tsx +2 -1
- package/dist/templates/app/superadmin/docs/[section]/[page]/page.tsx +16 -0
- package/dist/templates/app/superadmin/subscriptions/page.tsx +3 -2
- package/dist/templates/app/superadmin/teams/[teamId]/page.tsx +5 -4
- package/dist/templates/app/superadmin/users/[userId]/page.tsx +2 -1
- package/dist/templates/app/superadmin/users/page.tsx +2 -1
- package/dist/templates/blocks/cta-section/component.tsx +3 -2
- package/dist/templates/blocks/cta-section/schema.ts +1 -1
- package/dist/templates/blocks/features-grid/component.tsx +2 -1
- package/dist/templates/blocks/features-grid/schema.ts +1 -1
- package/dist/templates/blocks/hero/component.tsx +3 -2
- package/dist/templates/blocks/hero/schema.ts +1 -1
- package/dist/templates/blocks/testimonials/component.tsx +2 -1
- package/dist/templates/blocks/testimonials/schema.ts +1 -1
- package/dist/templates/blocks/text-content/component.tsx +2 -1
- package/dist/templates/blocks/text-content/schema.ts +1 -1
- package/dist/templates/ci-workflows/README.md +1 -1
- package/dist/templates/ci-workflows/github/cypress-regression.yml +1 -1
- package/dist/templates/ci-workflows/github/cypress-smoke.yml +1 -1
- package/dist/templates/contents/plugins/starter/plugin/api/example/route.ts.template +1 -1
- package/dist/templates/contents/themes/starter/blocks/cta-section/component.tsx +3 -2
- package/dist/templates/contents/themes/starter/blocks/cta-section/schema.ts +1 -1
- package/dist/templates/contents/themes/starter/blocks/features-grid/component.tsx +2 -1
- package/dist/templates/contents/themes/starter/blocks/features-grid/schema.ts +1 -1
- package/dist/templates/contents/themes/starter/blocks/hero/component.tsx +3 -2
- package/dist/templates/contents/themes/starter/blocks/hero/schema.ts +1 -1
- package/dist/templates/contents/themes/starter/blocks/testimonials/component.tsx +3 -2
- package/dist/templates/contents/themes/starter/blocks/testimonials/schema.ts +1 -1
- package/dist/templates/contents/themes/starter/blocks/text-content/component.tsx +2 -1
- package/dist/templates/contents/themes/starter/blocks/text-content/schema.ts +1 -1
- package/dist/templates/contents/themes/starter/entities/posts/components/post-header.tsx +2 -1
- package/dist/templates/contents/themes/starter/lib/hooks/useAnalyticsData.ts +2 -1
- package/dist/templates/contents/themes/starter/lib/hooks/useTaskStats.ts +2 -1
- package/dist/templates/eslint.config.mjs +133 -9
- package/dist/templates/features/blog/blocks/post-content/component.tsx +2 -1
- package/dist/templates/features/blog/blocks/post-content/schema.ts +1 -1
- package/dist/templates/features/blog/entities/posts/components/post-header.tsx +2 -1
- package/dist/templates/features/pages/blocks/hero/component.tsx +3 -2
- package/dist/templates/features/pages/blocks/hero/schema.ts +1 -1
- package/dist/templates/next.config.mjs +50 -29
- package/dist/templates/proxy.ts +100 -21
- package/dist/templates/public/sw.js +2 -2
- package/dist/templates/scripts/cy-run-prod.cjs +5 -5
- package/dist/types/blocks.d.ts +1 -1
- package/dist/types/blocks.d.ts.map +1 -1
- package/dist/types/blocks.js +1 -1
- package/package.json +9 -5
- package/scripts/build/docs-registry.mjs +7 -7
- package/scripts/build/registry/__tests__/billing-registry.test.mjs +43 -0
- package/scripts/build/registry/__tests__/build-output.test.mjs +209 -0
- package/scripts/build/registry/__tests__/copy-race-child.mjs +14 -0
- package/scripts/build/registry/__tests__/direct-fs-writes.mjs +203 -0
- package/scripts/build/registry/__tests__/globals-css-tag.test.mjs +66 -0
- package/scripts/build/registry/__tests__/icons-discovery.test.mjs +530 -29
- package/scripts/build/registry/__tests__/page-generator.test.mjs +211 -37
- package/scripts/build/registry/__tests__/published-signatures.test.mjs +239 -0
- package/scripts/build/registry/__tests__/route-file-rule.test.mjs +170 -0
- package/scripts/build/registry/__tests__/safe-fs.test.mjs +333 -0
- package/scripts/build/registry/__tests__/template-analysis.test.mjs +142 -0
- package/scripts/build/registry/__tests__/template-registry.test.mjs +279 -9
- package/scripts/build/registry/__tests__/templates-plan.test.mjs +128 -0
- package/scripts/build/registry/__tests__/templates-tree.test.mjs +367 -0
- package/scripts/build/registry/__tests__/write-places.test.mjs +425 -0
- package/scripts/build/registry/config.mjs +6 -65
- package/scripts/build/registry/discovery/icons.mjs +498 -43
- package/scripts/build/registry/discovery/templates.mjs +1 -1
- package/scripts/build/registry/generators/billing-registry.mjs +1 -1
- package/scripts/build/registry/generators/docs-registry.mjs +3 -4
- package/scripts/build/registry/generators/feature-registry.mjs +1 -1
- package/scripts/build/registry/generators/permissions-registry.mjs +2 -2
- package/scripts/build/registry/generators/template-registry.mjs +65 -49
- package/scripts/build/registry/index.mjs +1 -2
- package/scripts/build/registry/post-build/index.mjs +1 -1
- package/scripts/build/registry/post-build/own-gitignores.mjs +220 -0
- package/scripts/build/registry/post-build/page-generator.mjs +512 -268
- package/scripts/build/registry/post-build/route-cleanup.mjs +16 -13
- package/scripts/build/registry/post-build/test-fixtures.mjs +26 -8
- package/scripts/build/registry/project-mode.mjs +76 -0
- package/scripts/build/registry/shared/typescript-compiler.mjs +77 -0
- package/scripts/build/registry/validate-env.test.mjs +12 -8
- package/scripts/build/registry/watch.mjs +3 -53
- package/scripts/build/registry/write-places.mjs +228 -0
- package/scripts/build/registry.mjs +53 -30
- package/scripts/build/safe-fs.mjs +279 -0
- package/scripts/build/templates-plan.mjs +44 -0
- package/scripts/build/theme.mjs +46 -42
- package/scripts/build/update-tsconfig.mjs +16 -15
- package/scripts/db/cluster-changes.mjs +205 -0
- package/scripts/db/connection-time-limits.mjs +53 -0
- package/scripts/db/inspect-server.mjs +108 -0
- package/scripts/db/migration-time-limit.mjs +695 -0
- package/scripts/db/run-migrations.mjs +46 -41
- package/scripts/db/ssl-config.mjs +194 -0
- package/scripts/db/theme-location.mjs +69 -0
- package/scripts/db/verify-tables.mjs +3 -8
- package/scripts/db/verify-teams.mjs +2 -6
- package/scripts/db/verify-theme-migrations.mjs +169 -0
- package/scripts/maintenance/core-updater.mjs +1003 -0
- package/scripts/maintenance/step-guard.mjs +74 -0
- package/scripts/maintenance/update-core.mjs +14 -831
- package/scripts/utils/console-guard.mjs +13 -0
- package/scripts/utils/generated-tag.mjs +66 -0
- package/scripts/utils/index.mjs +9 -1
- package/scripts/utils/logging.mjs +140 -0
- package/templates/app/(auth)/accept-invite/[token]/page.tsx +4 -3
- package/templates/app/(auth)/verify-email/page.tsx +4 -3
- package/templates/app/(public)/docs/[section]/[page]/page.tsx +11 -0
- package/templates/app/api/auth/[...all]/route.ts +15 -14
- package/templates/app/api/internal/user-metadata/route.ts +6 -8
- package/templates/app/api/user/plan-flags/route.ts +1 -1
- package/templates/app/api/user/profile/route.ts +12 -17
- package/templates/app/api/v1/api-keys/route.ts +1 -1
- package/templates/app/api/v1/auth/signup-with-invite/route.ts +2 -1
- package/templates/app/api/v1/billing/cancel/route.ts +1 -1
- package/templates/app/api/v1/billing/change-plan/route.ts +1 -1
- package/templates/app/api/v1/billing/check-action/route.ts +1 -1
- package/templates/app/api/v1/billing/checkout/route.ts +1 -1
- package/templates/app/api/v1/blocks/validate/route.ts +1 -1
- package/templates/app/api/v1/media/[id]/tags/route.ts +1 -1
- package/templates/app/api/v1/post-categories/[id]/route.ts +1 -1
- package/templates/app/api/v1/post-categories/route.ts +1 -1
- package/templates/app/api/v1/teams/[teamId]/members/route.ts +2 -1
- package/templates/app/api/v1/teams/[teamId]/subscription/route.ts +3 -5
- package/templates/app/api/v1/teams/docs.md +4 -9
- package/templates/app/api/v1/teams/switch/route.ts +1 -1
- package/templates/app/api/v1/users/[id]/meta/[key]/route.ts +1 -1
- package/templates/app/api/v1/users/[id]/route.ts +1 -1
- package/templates/app/api/v1/users/route.ts +1 -1
- package/templates/app/dashboard/(main)/media/page.tsx +3 -1
- package/templates/app/dashboard/settings/api-keys/page.tsx +5 -4
- package/templates/app/dashboard/settings/profile/page.tsx +3 -2
- package/templates/app/devtools/scheduled-actions/page.tsx +2 -1
- package/templates/app/superadmin/docs/[section]/[page]/page.tsx +16 -0
- package/templates/app/superadmin/subscriptions/page.tsx +3 -2
- package/templates/app/superadmin/teams/[teamId]/page.tsx +5 -4
- package/templates/app/superadmin/users/[userId]/page.tsx +2 -1
- package/templates/app/superadmin/users/page.tsx +2 -1
- package/templates/blocks/cta-section/component.tsx +3 -2
- package/templates/blocks/cta-section/schema.ts +1 -1
- package/templates/blocks/features-grid/component.tsx +2 -1
- package/templates/blocks/features-grid/schema.ts +1 -1
- package/templates/blocks/hero/component.tsx +3 -2
- package/templates/blocks/hero/schema.ts +1 -1
- package/templates/blocks/testimonials/component.tsx +2 -1
- package/templates/blocks/testimonials/schema.ts +1 -1
- package/templates/blocks/text-content/component.tsx +2 -1
- package/templates/blocks/text-content/schema.ts +1 -1
- package/templates/ci-workflows/README.md +1 -1
- package/templates/ci-workflows/github/cypress-regression.yml +1 -1
- package/templates/ci-workflows/github/cypress-smoke.yml +1 -1
- package/templates/contents/plugins/starter/plugin/api/example/route.ts.template +1 -1
- package/templates/contents/themes/starter/blocks/cta-section/component.tsx +3 -2
- package/templates/contents/themes/starter/blocks/cta-section/schema.ts +1 -1
- package/templates/contents/themes/starter/blocks/features-grid/component.tsx +2 -1
- package/templates/contents/themes/starter/blocks/features-grid/schema.ts +1 -1
- package/templates/contents/themes/starter/blocks/hero/component.tsx +3 -2
- package/templates/contents/themes/starter/blocks/hero/schema.ts +1 -1
- package/templates/contents/themes/starter/blocks/testimonials/component.tsx +3 -2
- package/templates/contents/themes/starter/blocks/testimonials/schema.ts +1 -1
- package/templates/contents/themes/starter/blocks/text-content/component.tsx +2 -1
- package/templates/contents/themes/starter/blocks/text-content/schema.ts +1 -1
- package/templates/contents/themes/starter/entities/posts/components/post-header.tsx +2 -1
- package/templates/contents/themes/starter/lib/hooks/useAnalyticsData.ts +2 -1
- package/templates/contents/themes/starter/lib/hooks/useTaskStats.ts +2 -1
- package/templates/eslint.config.mjs +133 -9
- package/templates/features/blog/blocks/post-content/component.tsx +2 -1
- package/templates/features/blog/blocks/post-content/schema.ts +1 -1
- package/templates/features/blog/entities/posts/components/post-header.tsx +2 -1
- package/templates/features/pages/blocks/hero/component.tsx +3 -2
- package/templates/features/pages/blocks/hero/schema.ts +1 -1
- package/templates/next.config.mjs +50 -29
- package/templates/proxy.ts +100 -21
- package/templates/public/sw.js +2 -2
- package/templates/scripts/cy-run-prod.cjs +5 -5
- package/tests/jest/__mocks__/@nextsparkjs/registries/template-registry.client.ts +11 -0
- package/tests/jest/__mocks__/next-server.js +10 -0
- package/scripts/build/docs.mjs +0 -392
|
@@ -7,13 +7,18 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { createRequire } from 'node:module'
|
|
10
|
-
import { existsSync } from 'fs'
|
|
11
|
-
import {
|
|
12
|
-
import { join, dirname } from 'path'
|
|
10
|
+
import { constants, existsSync } from 'fs'
|
|
11
|
+
import { lstat, readdir, readFile } from 'fs/promises'
|
|
12
|
+
import { join, dirname, relative, sep } from 'path'
|
|
13
13
|
import { fileURLToPath } from 'url'
|
|
14
14
|
|
|
15
|
-
import { log, verbose } from '../../../utils/index.mjs'
|
|
16
|
-
import {
|
|
15
|
+
import { errorWithLines, log, verbose } from '../../../utils/index.mjs'
|
|
16
|
+
import { getProtectionLevel, ProtectionLevel } from '../../../../dist/config/protected-paths.js'
|
|
17
|
+
import { selectTypeScriptModule, loadTypeScriptFor } from '../shared/typescript-compiler.mjs'
|
|
18
|
+
import { ensureBackupsGitignore } from './own-gitignores.mjs'
|
|
19
|
+
import { projectFiles } from '../../safe-fs.mjs'
|
|
20
|
+
|
|
21
|
+
export { selectTypeScriptModule }
|
|
17
22
|
|
|
18
23
|
const __filename = fileURLToPath(import.meta.url)
|
|
19
24
|
const __dirname = dirname(__filename)
|
|
@@ -79,46 +84,27 @@ const SEGMENT_CONFIG_VALIDATORS = {
|
|
|
79
84
|
maxDuration: value => typeof value === 'number' && Number.isInteger(value) && value >= 0,
|
|
80
85
|
}
|
|
81
86
|
|
|
82
|
-
// Loaded lazily (and only once) since not every consumer of this module needs
|
|
83
|
-
// to parse a template - npm-mode builds without a template needing this path
|
|
84
|
-
// shouldn't pay for loading a TypeScript compiler up front.
|
|
85
|
-
let typescriptModulePromise = null
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* The TypeScript compiler, resolved from core first and then from the project:
|
|
89
|
-
* it is a devDependency of core, so an installed core finds it through the
|
|
90
|
-
* project that builds it, which has it to compile its own `.tsx` files.
|
|
91
|
-
*/
|
|
92
|
-
function loadTypeScript() {
|
|
93
|
-
if (!typescriptModulePromise) {
|
|
94
|
-
typescriptModulePromise = import('typescript')
|
|
95
|
-
.catch(() => createRequire(join(rootDir, 'package.json'))('typescript'))
|
|
96
|
-
.then(
|
|
97
|
-
module => module.default ?? module,
|
|
98
|
-
error => {
|
|
99
|
-
throw new Error(
|
|
100
|
-
'Parsing route-level exports out of a theme template requires the "typescript" package, but it could not be loaded ' +
|
|
101
|
-
`from @nextsparkjs/core or from the project at ${rootDir}. Add "typescript" to the project's devDependencies. (${error.message})`
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
)
|
|
105
|
-
}
|
|
106
|
-
return typescriptModulePromise
|
|
107
|
-
}
|
|
108
|
-
|
|
109
87
|
let nextParseModulePromise = null
|
|
110
88
|
|
|
111
89
|
/**
|
|
112
|
-
* Next.js's own module parser
|
|
113
|
-
* core, or null when neither has Next.js.
|
|
90
|
+
* Next.js's own module parser and SWC binding loader, resolved from the project
|
|
91
|
+
* and then from core, or null when neither has Next.js.
|
|
114
92
|
*/
|
|
115
93
|
function loadNextParseModule() {
|
|
116
94
|
if (!nextParseModulePromise) {
|
|
117
|
-
const
|
|
95
|
+
const parseModulePath = 'next/dist/build/analysis/parse-module'
|
|
96
|
+
const swcPath = 'next/dist/build/swc'
|
|
97
|
+
const loadFrom = require => ({
|
|
98
|
+
parseModule: require(parseModulePath).parseModule,
|
|
99
|
+
loadBindings: require(swcPath).loadBindings,
|
|
100
|
+
})
|
|
118
101
|
nextParseModulePromise = Promise.resolve()
|
|
119
|
-
.then(() => createRequire(join(rootDir, 'package.json'))
|
|
120
|
-
.catch(() => createRequire(import.meta.url)
|
|
121
|
-
.then(
|
|
102
|
+
.then(() => loadFrom(createRequire(join(rootDir, 'package.json'))))
|
|
103
|
+
.catch(() => loadFrom(createRequire(import.meta.url)))
|
|
104
|
+
.then(async ({ parseModule, loadBindings }) => {
|
|
105
|
+
await loadBindings()
|
|
106
|
+
return parseModule
|
|
107
|
+
}, () => null)
|
|
122
108
|
}
|
|
123
109
|
return nextParseModulePromise
|
|
124
110
|
}
|
|
@@ -201,32 +187,19 @@ function declaresDefaultExport(sourceFile, ts) {
|
|
|
201
187
|
}
|
|
202
188
|
|
|
203
189
|
/**
|
|
204
|
-
* Parse a theme template's source
|
|
205
|
-
* - `segmentConfig`: segment config keys that resolve to a literal value the
|
|
206
|
-
* generated route file can re-declare (`{ name: value }`).
|
|
207
|
-
* - `moduleExports`: exported names (functions, `metadata`, etc.) the
|
|
208
|
-
* generated route file can safely forward with `export { ... } from`.
|
|
209
|
-
* - `hasDefaultExport`: whether it exports a component as default, read from
|
|
210
|
-
* the syntax tree so a comment or string mentioning `export default` doesn't
|
|
211
|
-
* count and `export { Layout as default }` does.
|
|
190
|
+
* Parse a theme template's source into a TypeScript AST.
|
|
212
191
|
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
* in the build would say so beyond a console warning.
|
|
192
|
+
* The parser recovers from a syntax error by skipping code, exports included. A
|
|
193
|
+
* template Next.js cannot parse either is reported rather than read in part. One
|
|
194
|
+
* it can is read from what TypeScript recovered, with a warning: Next.js compiles
|
|
195
|
+
* templates with SWC, which may know syntax the installed TypeScript does not.
|
|
218
196
|
*/
|
|
219
|
-
|
|
220
|
-
const ts = await
|
|
197
|
+
async function parseTemplateSource(source, filePath) {
|
|
198
|
+
const ts = await loadTypeScriptFor(rootDir)
|
|
221
199
|
// Parsed as its own kind of file: a TSX parse reads TypeScript-only syntax in a
|
|
222
200
|
// .ts template (`<T>(props) => ...`, `<Props>value`) as JSX and loses what follows
|
|
223
201
|
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.getScriptKindFromFileName(filePath))
|
|
224
202
|
|
|
225
|
-
// The parser recovers from a syntax error by skipping code, exports included. A
|
|
226
|
-
// template Next.js cannot parse either is reported rather than read in part. One
|
|
227
|
-
// it can is read from what TypeScript recovered, with a warning: Next.js compiles
|
|
228
|
-
// templates with SWC, which may know syntax the installed TypeScript does not (a
|
|
229
|
-
// source phase import, for one).
|
|
230
203
|
const [parseError] = sourceFile.parseDiagnostics ?? []
|
|
231
204
|
if (parseError) {
|
|
232
205
|
const { line } = sourceFile.getLineAndCharacterOfPosition(parseError.start ?? 0)
|
|
@@ -240,6 +213,29 @@ export async function extractRouteExports(source, filePath) {
|
|
|
240
213
|
)
|
|
241
214
|
}
|
|
242
215
|
|
|
216
|
+
return { ts, sourceFile }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Split a parsed theme template's route-level exports into:
|
|
221
|
+
* - `segmentConfig`: segment config keys that resolve to a literal value the
|
|
222
|
+
* generated route file can re-declare (`{ name: value }`).
|
|
223
|
+
* - `moduleExports`: exported names (functions, `metadata`, etc.) the
|
|
224
|
+
* generated route file can safely forward with `export { ... } from`.
|
|
225
|
+
* - `hasDefaultExport`: whether it exports a component as default, read from
|
|
226
|
+
* the syntax tree so a comment or string mentioning `export default` doesn't
|
|
227
|
+
* count and `export { Layout as default }` does.
|
|
228
|
+
*
|
|
229
|
+
* - `errors`: one per segment config key that IS exported but not as a
|
|
230
|
+
* literal `export const` (an expression, an identifier, `let`/`var`, a
|
|
231
|
+
* re-export, or a value the Next.js schema rejects). Next.js would ignore
|
|
232
|
+
* such a value and fall back to its default, and nothing in the build would
|
|
233
|
+
* say so beyond a console warning, so a route file generated from the
|
|
234
|
+
* template must not be written with it. They are returned rather than
|
|
235
|
+
* thrown because a template whose app route already exists gets no route
|
|
236
|
+
* file, and nothing reads its segment config.
|
|
237
|
+
*/
|
|
238
|
+
function collectRouteExports(sourceFile, ts, filePath) {
|
|
243
239
|
const segmentConfig = {}
|
|
244
240
|
const moduleExports = []
|
|
245
241
|
const seenModuleExports = new Set()
|
|
@@ -324,41 +320,55 @@ export async function extractRouteExports(source, filePath) {
|
|
|
324
320
|
}
|
|
325
321
|
}
|
|
326
322
|
|
|
323
|
+
return { segmentConfig, moduleExports, hasDefaultExport: declaresDefaultExport(sourceFile, ts), errors }
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Parse a theme template's source and return its route-level exports as
|
|
328
|
+
* `collectRouteExports` splits them, throwing on segment config Next.js
|
|
329
|
+
* wouldn't read.
|
|
330
|
+
*/
|
|
331
|
+
export async function extractRouteExports(source, filePath) {
|
|
332
|
+
const { ts, sourceFile } = await parseTemplateSource(source, filePath)
|
|
333
|
+
const { errors, ...routeExports } = collectRouteExports(sourceFile, ts, filePath)
|
|
334
|
+
|
|
327
335
|
if (errors.length > 0) {
|
|
328
|
-
throw
|
|
336
|
+
throw errorWithLines(errors)
|
|
329
337
|
}
|
|
330
338
|
|
|
331
|
-
return
|
|
339
|
+
return routeExports
|
|
332
340
|
}
|
|
333
341
|
|
|
334
342
|
/**
|
|
335
343
|
* Resolve a `@/contents/...` template import path to its file on disk,
|
|
336
344
|
* trying both TSX and TS extensions.
|
|
337
345
|
*/
|
|
338
|
-
function resolveTemplateFilePath(templatePath) {
|
|
346
|
+
function resolveTemplateFilePath(templatePath, root) {
|
|
339
347
|
const baseTemplatePath = templatePath
|
|
340
|
-
.replace('@/',
|
|
348
|
+
.replace('@/', root + '/')
|
|
341
349
|
.replace(/\.(tsx|ts)$/, '')
|
|
342
350
|
return existsSync(baseTemplatePath + '.tsx') ? baseTemplatePath + '.tsx' : baseTemplatePath + '.ts'
|
|
343
351
|
}
|
|
344
352
|
|
|
345
353
|
/**
|
|
346
|
-
* Read a template file and
|
|
347
|
-
* is treated as having none
|
|
348
|
-
* template that hasn't been generated yet
|
|
349
|
-
*
|
|
354
|
+
* Read a template file and collect its route-level exports. A missing file
|
|
355
|
+
* is treated as having none and as having a default export - a build-time
|
|
356
|
+
* override may point at a template that hasn't been generated yet, and
|
|
357
|
+
* assuming a component exists there keeps that case importing it rather than
|
|
358
|
+
* silently downgrading to metadata-only.
|
|
350
359
|
*/
|
|
351
|
-
async function
|
|
352
|
-
const absoluteTemplatePath = resolveTemplateFilePath(templatePath)
|
|
360
|
+
async function readTemplateExports(templatePath, root) {
|
|
361
|
+
const absoluteTemplatePath = resolveTemplateFilePath(templatePath, root)
|
|
353
362
|
|
|
354
363
|
let templateContent
|
|
355
364
|
try {
|
|
356
365
|
templateContent = await readFile(absoluteTemplatePath, 'utf8')
|
|
357
366
|
} catch {
|
|
358
|
-
return { segmentConfig: {}, moduleExports: [], hasDefaultExport:
|
|
367
|
+
return { segmentConfig: {}, moduleExports: [], hasDefaultExport: true, errors: [] }
|
|
359
368
|
}
|
|
360
369
|
|
|
361
|
-
|
|
370
|
+
const { ts, sourceFile } = await parseTemplateSource(templateContent, absoluteTemplatePath)
|
|
371
|
+
return collectRouteExports(sourceFile, ts, absoluteTemplatePath)
|
|
362
372
|
}
|
|
363
373
|
|
|
364
374
|
/**
|
|
@@ -382,12 +392,10 @@ function renderModuleExportsBlock(moduleExports, templatePathWithoutExtension) {
|
|
|
382
392
|
/**
|
|
383
393
|
* Generate content for a regular page (page.tsx, error.tsx, etc.)
|
|
384
394
|
*/
|
|
385
|
-
|
|
395
|
+
function generateRegularPageContent(appPath, templatePath, { segmentConfig, moduleExports }) {
|
|
386
396
|
// Remove .tsx/.ts extension from import path (TypeScript doesn't allow file extensions in imports)
|
|
387
397
|
const templatePathWithoutExtension = templatePath.replace(/\.(tsx|ts)$/, '')
|
|
388
398
|
|
|
389
|
-
const { segmentConfig, moduleExports } = await readRouteExports(templatePath)
|
|
390
|
-
|
|
391
399
|
const segmentConfigBlock = renderSegmentConfigBlock(segmentConfig)
|
|
392
400
|
const segmentConfigSection = segmentConfigBlock
|
|
393
401
|
? `\n// Segment config re-declared as literals: Next.js only reads these when they're declared directly in the route file, not re-exported\n${segmentConfigBlock}`
|
|
@@ -413,27 +421,14 @@ ${segmentConfigSection}${moduleExportsSection}`
|
|
|
413
421
|
/**
|
|
414
422
|
* Generate content for a layout page
|
|
415
423
|
*/
|
|
416
|
-
|
|
424
|
+
function generateLayoutPageContent(appPath, componentName, templatePath, routeExports) {
|
|
417
425
|
if (templatePath) {
|
|
418
426
|
// Remove .tsx/.ts extension from import path (TypeScript doesn't allow file extensions in imports)
|
|
419
427
|
const templatePathWithoutExtension = templatePath.replace(/\.(tsx|ts)$/, '')
|
|
420
|
-
const absoluteTemplatePath = resolveTemplateFilePath(templatePath)
|
|
421
428
|
|
|
422
|
-
//
|
|
429
|
+
// Whether the template has a default export (component), and what
|
|
423
430
|
// route-level exports it defines alongside (or instead of) it
|
|
424
|
-
|
|
425
|
-
try {
|
|
426
|
-
templateContent = await readFile(absoluteTemplatePath, 'utf8')
|
|
427
|
-
} catch {
|
|
428
|
-
// Can't read the file - fall through to the default-export assumption below
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
let hasDefaultExport = true
|
|
432
|
-
let segmentConfig = {}
|
|
433
|
-
let moduleExports = []
|
|
434
|
-
if (templateContent !== null) {
|
|
435
|
-
;({ segmentConfig, moduleExports, hasDefaultExport } = await extractRouteExports(templateContent, absoluteTemplatePath))
|
|
436
|
-
}
|
|
431
|
+
const { hasDefaultExport, segmentConfig, moduleExports } = routeExports
|
|
437
432
|
|
|
438
433
|
const segmentConfigBlock = renderSegmentConfigBlock(segmentConfig)
|
|
439
434
|
const moduleExportsBlock = renderModuleExportsBlock(moduleExports, templatePathWithoutExtension)
|
|
@@ -456,9 +451,17 @@ export default TemplateComponent
|
|
|
456
451
|
${forwardedExportsSection}`
|
|
457
452
|
} else if (segmentConfigBlock || moduleExportsBlock) {
|
|
458
453
|
// No component, but the template still defines segment config and/or metadata -
|
|
459
|
-
// re-export those and provide a pass-through component
|
|
454
|
+
// re-export those and provide a pass-through component. Only a PROTECTED_RENDER
|
|
455
|
+
// path is labeled as one; anywhere else the template simply has no default export.
|
|
456
|
+
const renderProtected = getProtectionLevel(appPath) === ProtectionLevel.PROTECTED_RENDER
|
|
457
|
+
const description = renderProtected
|
|
458
|
+
? 'metadata-only (PROTECTED_RENDER)'
|
|
459
|
+
: 'route-level exports only (the theme template has no default export)'
|
|
460
|
+
const passThroughNote = renderProtected
|
|
461
|
+
? 'actual rendering blocked by PROTECTED_RENDER'
|
|
462
|
+
: 'the theme template exports no component, so this layout renders its children'
|
|
460
463
|
return `/**
|
|
461
|
-
* Layout template -
|
|
464
|
+
* Layout template - ${description}
|
|
462
465
|
* Template: ${appPath}
|
|
463
466
|
* Generated by: scripts/build-registry.mjs
|
|
464
467
|
*/
|
|
@@ -466,7 +469,7 @@ import type { ReactNode } from 'react'
|
|
|
466
469
|
|
|
467
470
|
// Re-export Next.js route-level exports from the theme template
|
|
468
471
|
${segmentConfigBlock}${moduleExportsBlock}
|
|
469
|
-
// Pass-through component (
|
|
472
|
+
// Pass-through component (${passThroughNote})
|
|
470
473
|
interface ${componentName}Props {
|
|
471
474
|
children: ReactNode
|
|
472
475
|
}
|
|
@@ -565,61 +568,29 @@ function addTemplateResolverToLayout(content, layoutPath) {
|
|
|
565
568
|
}
|
|
566
569
|
|
|
567
570
|
/**
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
572
|
-
* @param {string} sourcePath - Path to original layout in app/
|
|
573
|
-
* @param {string} targetPath - Path to write in app/(templates)/
|
|
574
|
-
* @param {string} layoutPath - Relative layout path (e.g., 'app/dashboard/layout.tsx')
|
|
575
|
-
* @param {Array} templates - All discovered templates (for build-time override check)
|
|
576
|
-
*/
|
|
577
|
-
async function duplicateLayoutDirect(sourcePath, targetPath, layoutPath, templates) {
|
|
578
|
-
// Create target directory if it doesn't exist
|
|
579
|
-
const targetDir = dirname(targetPath)
|
|
580
|
-
await mkdir(targetDir, { recursive: true })
|
|
581
|
-
|
|
582
|
-
// BUILD-TIME CHECK: Does a template override exist for this layout?
|
|
583
|
-
const layoutOverride = templates.find(t =>
|
|
584
|
-
t.appPath === layoutPath && t.templateType === 'layout'
|
|
585
|
-
)
|
|
586
|
-
|
|
587
|
-
if (layoutOverride) {
|
|
588
|
-
// Template override exists - generate direct import to it
|
|
589
|
-
const templatePathWithoutExtension = layoutOverride.templatePath.replace(/\.(tsx|ts)$/, '')
|
|
590
|
-
const componentName = `Layout${toPascalCase(layoutPath.replace(/[^\w]/g, '_'))}`
|
|
591
|
-
|
|
592
|
-
const { segmentConfig, moduleExports } = await readRouteExports(layoutOverride.templatePath)
|
|
593
|
-
const segmentConfigBlock = renderSegmentConfigBlock(segmentConfig)
|
|
594
|
-
const moduleExportsBlock = renderModuleExportsBlock(moduleExports, templatePathWithoutExtension)
|
|
595
|
-
const forwardedExportsSection = segmentConfigBlock || moduleExportsBlock
|
|
596
|
-
? `\n// Re-export Next.js route-level exports from the theme template\n${segmentConfigBlock}${moduleExportsBlock}`
|
|
597
|
-
: ''
|
|
598
|
-
|
|
599
|
-
const content = `/**
|
|
600
|
-
* Layout with template override - directly imports from theme
|
|
601
|
-
* Template: ${layoutPath}
|
|
602
|
-
* Override: ${layoutOverride.templatePath}
|
|
603
|
-
* Generated by: scripts/build-registry.mjs (build-time resolution)
|
|
571
|
+
* The comment a copy of an app/ layout in app/(templates)/ starts with. A
|
|
572
|
+
* comment is not a statement, so a 'use client' directive right after it is
|
|
573
|
+
* still the file's directive.
|
|
604
574
|
*/
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
}
|
|
575
|
+
function layoutCopyHeader(layoutPath) {
|
|
576
|
+
return `/**
|
|
577
|
+
* Generated by the NextSpark registry build: a copy of ${layoutPath}, so theme
|
|
578
|
+
* routes under app/(templates) render inside the same layout. Rewritten on every build.
|
|
579
|
+
*/
|
|
580
|
+
`
|
|
581
|
+
}
|
|
613
582
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
583
|
+
/**
|
|
584
|
+
* The content of the copy of an app/ layout in app/(templates)/. A layout that
|
|
585
|
+
* can't be read is replaced by a pass-through, which keeps the hierarchy.
|
|
586
|
+
*/
|
|
587
|
+
async function layoutCopyContent(layoutPath, appFiles) {
|
|
588
|
+
const layout = await projectFileContent(layoutPath, appFiles)
|
|
589
|
+
if (layout !== null) {
|
|
590
|
+
return layoutCopyHeader(layoutPath) + layout
|
|
591
|
+
} else {
|
|
621
592
|
const componentName = `PassThroughLayout${toPascalCase(layoutPath.replace(/[^\w]/g, '_'))}`
|
|
622
|
-
|
|
593
|
+
return `/**
|
|
623
594
|
* Basic layout wrapper - maintains Next.js structure
|
|
624
595
|
* Generated by: scripts/build-registry.mjs
|
|
625
596
|
*/
|
|
@@ -631,74 +602,206 @@ export default function ${componentName}({ children }: ${componentName}Props) {
|
|
|
631
602
|
return <>{children}</>
|
|
632
603
|
}
|
|
633
604
|
`
|
|
634
|
-
await writeFile(targetPath, basicContent, 'utf8')
|
|
635
|
-
verbose(`Generated basic layout wrapper for ${layoutPath}`)
|
|
636
605
|
}
|
|
637
606
|
}
|
|
638
607
|
|
|
639
608
|
/**
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
*
|
|
609
|
+
* The app/ layouts above a route directory, outermost first, that exist in the
|
|
610
|
+
* project: the ones a route generated into app/(templates)/ needs copied beside
|
|
611
|
+
* it to render inside the same hierarchy.
|
|
643
612
|
*
|
|
644
|
-
* @param {string}
|
|
645
|
-
* @param {string} templatesDir - Output directory for (templates)
|
|
646
|
-
* @param {Array} templates - All discovered templates (for build-time override check)
|
|
613
|
+
* @param {string} directory - App directory of a route (e.g. 'app/dashboard/(main)/posts')
|
|
647
614
|
*/
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
// Extract the directory path from app path (remove filename)
|
|
652
|
-
// Example: app/dashboard/(main)/plugins/page.tsx -> dashboard/(main)/plugins
|
|
653
|
-
const pathParts = appPath.replace('app/', '').replace(/\/[^\/]+$/, '').split('/')
|
|
654
|
-
|
|
655
|
-
// Generate all parent directories and check for layouts
|
|
615
|
+
function requiredLayoutPaths(directory, appFiles) {
|
|
616
|
+
const layoutPaths = []
|
|
656
617
|
let currentPath = ''
|
|
657
|
-
for (const part of
|
|
618
|
+
for (const part of directory.replace(/^app\/?/, '').split('/')) {
|
|
658
619
|
if (!part) continue
|
|
659
|
-
|
|
660
620
|
currentPath = currentPath ? `${currentPath}/${part}` : part
|
|
661
621
|
const layoutPath = `app/${currentPath}/layout.tsx`
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
// Check if core layout exists and template layout doesn't
|
|
666
|
-
if (existsSync(coreLayoutPath) && !existsSync(templateLayoutPath)) {
|
|
667
|
-
// Pass templates for build-time override resolution
|
|
668
|
-
await duplicateLayoutDirect(coreLayoutPath, templateLayoutPath, layoutPath, templates)
|
|
669
|
-
layoutsGenerated++
|
|
622
|
+
const exists = appFiles?.has(layoutPath) ? appFiles.get(layoutPath) !== null : existsSync(join(rootDir, layoutPath))
|
|
623
|
+
if (exists) {
|
|
624
|
+
layoutPaths.push(layoutPath)
|
|
670
625
|
}
|
|
671
626
|
}
|
|
627
|
+
return layoutPaths
|
|
628
|
+
}
|
|
672
629
|
|
|
673
|
-
|
|
630
|
+
/**
|
|
631
|
+
* A file's content, by path from the project root: what `appFiles` gives when
|
|
632
|
+
* it has the path - null standing for a file that is gone - and otherwise what
|
|
633
|
+
* is on disk; null when there is no such file.
|
|
634
|
+
*/
|
|
635
|
+
async function projectFileContent(path, appFiles) {
|
|
636
|
+
if (appFiles?.has(path)) return appFiles.get(path)
|
|
637
|
+
try {
|
|
638
|
+
return await readFile(join(rootDir, path), 'utf8')
|
|
639
|
+
} catch {
|
|
640
|
+
return null
|
|
641
|
+
}
|
|
674
642
|
}
|
|
675
643
|
|
|
676
644
|
/**
|
|
677
645
|
* Generate a single template page with template override system
|
|
646
|
+
*
|
|
647
|
+
* What `routeFileAction` decides applies: a template over a route the app
|
|
648
|
+
* already has gets no file, and the result says so; one whose route file would
|
|
649
|
+
* break is rejected before anything is written.
|
|
650
|
+
*
|
|
651
|
+
* @param {Object} template - The template to generate a route file from
|
|
652
|
+
* @param {string} outputPath - Where to write it
|
|
653
|
+
* @param {Map} [analysis] - What `analyzeTemplates` read from the build's templates; taken for this
|
|
654
|
+
* template when omitted
|
|
655
|
+
* @returns {Promise<{ written: boolean, reason?: string }>}
|
|
678
656
|
*/
|
|
679
|
-
export async function generateTemplatePage(template, outputPath) {
|
|
680
|
-
const
|
|
657
|
+
export async function generateTemplatePage(template, outputPath, analysis = null) {
|
|
658
|
+
const routeExports = analysis ? templateAnalysisFor(analysis, template) : await analyzeTemplate(template, rootDir)
|
|
681
659
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
660
|
+
if (routeFileAction(template, routeExports) === 'skip') {
|
|
661
|
+
return { written: false, reason: `the app already has ${template.appPath}, so no route file is generated for this template` }
|
|
662
|
+
}
|
|
685
663
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
664
|
+
const files = projectFiles(rootDir)
|
|
665
|
+
await files.mkdir(dirname(outputPath), { recursive: true })
|
|
666
|
+
await files.writeFile(outputPath, routeFileContent(template, routeExports), 'utf8')
|
|
667
|
+
verbose(`Generated: ${outputPath.replace(rootDir, '')}`)
|
|
668
|
+
return { written: true }
|
|
669
|
+
}
|
|
690
670
|
|
|
691
|
-
|
|
671
|
+
/**
|
|
672
|
+
* Whether `generateMissingPages` will write a route file at `appPath` -
|
|
673
|
+
* importing the template's default export - rather than leaving the app's
|
|
674
|
+
* own file in place and letting the override resolve at runtime through
|
|
675
|
+
* `getTemplateOrDefault`. A layout is always written, since build-time
|
|
676
|
+
* override resolution duplicates it into `app/(templates)/` regardless of
|
|
677
|
+
* whether the app already has one; anything else is only written when the
|
|
678
|
+
* app doesn't already have that route.
|
|
679
|
+
*
|
|
680
|
+
* @param {Map<string, string | null>} [appFiles] - Files under app/, by path from the project root, to take as
|
|
681
|
+
* having this content instead of what is on disk (null: removed)
|
|
682
|
+
*/
|
|
683
|
+
export function willGenerateRoute(appPath, templateType, root = rootDir, appFiles = undefined) {
|
|
684
|
+
if (templateType === 'layout') return true
|
|
685
|
+
if (appFiles?.has(appPath)) return appFiles.get(appPath) === null
|
|
686
|
+
return !existsSync(join(root, appPath))
|
|
687
|
+
}
|
|
692
688
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
689
|
+
/**
|
|
690
|
+
* Parse each template once and record what it contributes to the build: its
|
|
691
|
+
* route-level exports, whether it has a default export, and whether the page
|
|
692
|
+
* generator writes a route file for it (`generatesRoute`). The server template
|
|
693
|
+
* registry, the client template registry and the page generator all take
|
|
694
|
+
* these from the result instead of reading the template themselves, so a
|
|
695
|
+
* template that changes while a build runs can't look one way to one of them
|
|
696
|
+
* and another way to the next.
|
|
697
|
+
*
|
|
698
|
+
* Throws for a template the page generator writes a route file for when that
|
|
699
|
+
* file can't work: segment config Next.js wouldn't read, or a page (or any
|
|
700
|
+
* other non-layout route) with no default export to import - a plain .tsx/.ts
|
|
701
|
+
* as much as a standalone .meta.ts, which never has one. A layout with no
|
|
702
|
+
* default export gets a pass-through component instead. A template whose app
|
|
703
|
+
* route already exists gets no route file, so neither applies to it: the
|
|
704
|
+
* override resolves at runtime, metadata included.
|
|
705
|
+
*
|
|
706
|
+
* @param {Array} templates - List of template definitions
|
|
707
|
+
* @param {Object} config - Configuration object with projectRoot
|
|
708
|
+
* @param {Map<string, string | null>} [appFiles] - Files under app/, by path from the project root, to take as
|
|
709
|
+
* having this content instead of what is on disk (null: removed), so `generatesRoute` answers for the app as
|
|
710
|
+
* a sync about to run leaves it rather than for the app the analysis runs against
|
|
711
|
+
* @returns {Promise<Map<string, Object>>} Each template's analysis, keyed by its templatePath
|
|
712
|
+
*/
|
|
713
|
+
export async function analyzeTemplates(templates, config = null, appFiles = undefined) {
|
|
714
|
+
if (config?.projectRoot) {
|
|
715
|
+
rootDir = config.projectRoot
|
|
697
716
|
}
|
|
698
717
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
718
|
+
const analysis = new Map()
|
|
719
|
+
|
|
720
|
+
for (const template of templates) {
|
|
721
|
+
if (!analysis.has(template.templatePath)) {
|
|
722
|
+
analysis.set(template.templatePath, await analyzeTemplate(template, rootDir, appFiles))
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return analysis
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Parse one template and collect what `analyzeTemplates` records for it,
|
|
731
|
+
* rejecting it when the route file generated from it can't work: segment
|
|
732
|
+
* config Next.js wouldn't read, or what `routeFileAction` rejects. Every
|
|
733
|
+
* generator that has no analysis to hand reads a template through here.
|
|
734
|
+
*
|
|
735
|
+
* @param {Object} template - The template to read
|
|
736
|
+
* @param {string} root - The project root its path resolves against
|
|
737
|
+
* @param {Map<string, string | null>} [appFiles] - Files under app/ to take as having this content, as for
|
|
738
|
+
* `analyzeTemplates`
|
|
739
|
+
*/
|
|
740
|
+
async function analyzeTemplate(template, root, appFiles) {
|
|
741
|
+
const { appPath, templateType, templatePath } = template
|
|
742
|
+
const generatesRoute = willGenerateRoute(appPath, templateType, root, appFiles)
|
|
743
|
+
const { errors, ...routeExports } = await readTemplateExports(templatePath, root)
|
|
744
|
+
|
|
745
|
+
if (generatesRoute && errors.length > 0) {
|
|
746
|
+
throw errorWithLines(errors)
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const entry = { ...routeExports, generatesRoute }
|
|
750
|
+
routeFileAction(template, entry)
|
|
751
|
+
return entry
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* What becomes of a template's route file, decided from its analysis in this
|
|
756
|
+
* one place for every generator that writes route files or registers template
|
|
757
|
+
* components:
|
|
758
|
+
* - 'write': a route file is generated for it in app/(templates)
|
|
759
|
+
* - 'skip': the app already has that route, which stays; the override resolves
|
|
760
|
+
* at runtime, and nothing is written for it
|
|
761
|
+
*
|
|
762
|
+
* A page (or any other non-layout route) whose route file would import a default
|
|
763
|
+
* export it doesn't have - a plain .tsx/.ts as much as a standalone .meta.ts,
|
|
764
|
+
* which never has one - is rejected. A layout with no default export gets a
|
|
765
|
+
* pass-through component instead.
|
|
766
|
+
*
|
|
767
|
+
* @param {Object} template - The template
|
|
768
|
+
* @param {Object} entry - Its analysis, as `analyzeTemplates` records it
|
|
769
|
+
* @returns {'write' | 'skip'}
|
|
770
|
+
*/
|
|
771
|
+
export function routeFileAction(template, entry) {
|
|
772
|
+
const { appPath, templateType, templatePath } = template
|
|
773
|
+
|
|
774
|
+
if (!entry.generatesRoute) {
|
|
775
|
+
return 'skip'
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (templateType !== 'layout' && !entry.hasDefaultExport) {
|
|
779
|
+
throw new Error(
|
|
780
|
+
`${templatePath} has no default export, and the app has no existing route at "${appPath}" ` +
|
|
781
|
+
`for a metadata-only override to attach to - the registry build generates "${appPath}" importing this ` +
|
|
782
|
+
`template's default export, which doesn't exist. A page template needs a default export. For a ` +
|
|
783
|
+
`metadata-only override, add one to an app page that already exists at "${appPath}" (a ".meta.ts" file, ` +
|
|
784
|
+
`or a template with no default export, next to it).`
|
|
785
|
+
)
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return 'write'
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* The analysis `analyzeTemplates` recorded for a template. Missing means the
|
|
793
|
+
* caller is generating from templates other than the ones it analyzed, and
|
|
794
|
+
* reading the template here would bring back a second, possibly different,
|
|
795
|
+
* answer - so it fails instead.
|
|
796
|
+
*/
|
|
797
|
+
export function templateAnalysisFor(analysis, template) {
|
|
798
|
+
const entry = analysis?.get(template.templatePath)
|
|
799
|
+
if (!entry) {
|
|
800
|
+
throw new Error(
|
|
801
|
+
`${template.templatePath} was not read by analyzeTemplates() - pass the analysis of the same templates being generated.`
|
|
802
|
+
)
|
|
803
|
+
}
|
|
804
|
+
return entry
|
|
702
805
|
}
|
|
703
806
|
|
|
704
807
|
/**
|
|
@@ -708,115 +811,256 @@ export async function generateTemplatePage(template, outputPath) {
|
|
|
708
811
|
*
|
|
709
812
|
* @param {Array} templates - List of template definitions
|
|
710
813
|
* @param {Object} config - Configuration object with projectRoot
|
|
814
|
+
* @param {Map} [analysis] - What `analyzeTemplates` read from these templates; taken for this call when omitted
|
|
815
|
+
* @returns {Promise<{ created: number, updated: number, removed: number, backupDir: string | null, skipped: string[] }>}
|
|
816
|
+
* What changed in app/(templates), and the app paths of the templates that got no route file
|
|
711
817
|
*/
|
|
712
|
-
export async function generateMissingPages(templates, config = null) {
|
|
818
|
+
export async function generateMissingPages(templates, config = null, analysis = null) {
|
|
713
819
|
// Use config.projectRoot if provided, otherwise fall back to default rootDir
|
|
714
820
|
if (config?.projectRoot) {
|
|
715
821
|
rootDir = config.projectRoot
|
|
716
822
|
}
|
|
717
823
|
const templatesDir = join(rootDir, 'app', '(templates)')
|
|
718
824
|
|
|
719
|
-
//
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
825
|
+
// Every template has to pass routeFileAction before anything is written or deleted
|
|
826
|
+
analysis = analysis ?? (await analyzeTemplates(templates, config))
|
|
827
|
+
const skipped = templates
|
|
828
|
+
.filter(template => routeFileAction(template, templateAnalysisFor(analysis, template)) === 'skip')
|
|
829
|
+
.map(({ appPath }) => appPath)
|
|
830
|
+
|
|
831
|
+
const { created, updated, removed, backupDir } = await reconcileTemplatesTree(
|
|
832
|
+
templatesDir,
|
|
833
|
+
await planTemplatesTree(templates, analysis)
|
|
834
|
+
)
|
|
723
835
|
|
|
724
|
-
if (
|
|
725
|
-
|
|
836
|
+
if (created > 0 || updated > 0 || removed > 0) {
|
|
837
|
+
log(`app/(templates): ${created} new, ${updated} updated, ${removed} removed`, 'success')
|
|
838
|
+
} else {
|
|
839
|
+
verbose('app/(templates) is up to date')
|
|
840
|
+
}
|
|
841
|
+
if (backupDir) {
|
|
842
|
+
log(`app/(templates): what was replaced or removed is backed up in ${relative(rootDir, backupDir)}`, 'warning')
|
|
726
843
|
}
|
|
727
844
|
|
|
728
|
-
|
|
729
|
-
|
|
845
|
+
return { created, updated, removed, backupDir, skipped }
|
|
846
|
+
}
|
|
730
847
|
|
|
731
|
-
|
|
732
|
-
|
|
848
|
+
/**
|
|
849
|
+
* What `generateMissingPages` would change in app/(templates), worked out
|
|
850
|
+
* without writing anything: the files it would create, the ones it would
|
|
851
|
+
* replace and the ones it would remove - backing up both of those first - as
|
|
852
|
+
* paths from the project root.
|
|
853
|
+
*
|
|
854
|
+
* `appFiles` reaches the analysis too: whether a template gets a route file at
|
|
855
|
+
* all turns on whether the app has one at that path, so a route the sync is
|
|
856
|
+
* about to write - or remove - decides the plan the same way it decides the run.
|
|
857
|
+
*
|
|
858
|
+
* @param {Array} templates - List of template definitions
|
|
859
|
+
* @param {Object} config - Configuration object with projectRoot
|
|
860
|
+
* @param {Map<string, string | null>} [appFiles] - Files under app/, by path from the project root, to take as
|
|
861
|
+
* having this content instead of what is on disk (null: removed), to plan against changes not written yet
|
|
862
|
+
* @returns {Promise<{ create: string[], replace: string[], remove: string[] }>}
|
|
863
|
+
*/
|
|
864
|
+
export async function planMissingPages(templates, config = null, appFiles = undefined) {
|
|
865
|
+
if (config?.projectRoot) {
|
|
866
|
+
rootDir = config.projectRoot
|
|
867
|
+
}
|
|
868
|
+
const templatesDir = join(rootDir, 'app', '(templates)')
|
|
733
869
|
|
|
734
|
-
|
|
735
|
-
const
|
|
870
|
+
const analysis = await analyzeTemplates(templates, config, appFiles)
|
|
871
|
+
const { create, replace, remove } = await diffTemplatesTree(templatesDir, await planTemplatesTree(templates, analysis, appFiles))
|
|
736
872
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
873
|
+
const fromRoot = path => relative(rootDir, path).split(sep).join('/')
|
|
874
|
+
return { create: create.map(fromRoot), replace: replace.map(fromRoot), remove: remove.map(fromRoot) }
|
|
875
|
+
}
|
|
740
876
|
|
|
741
|
-
|
|
742
|
-
|
|
877
|
+
/**
|
|
878
|
+
* The route file generated from a template, with its component named after the
|
|
879
|
+
* last static segment of the template's route.
|
|
880
|
+
*/
|
|
881
|
+
function routeFileContent(template, routeExports) {
|
|
882
|
+
const { appPath, templateType, name, templatePath } = template
|
|
883
|
+
const routeSegments = name.split('/').filter(segment => !segment.startsWith('[') && !segment.endsWith(']'))
|
|
884
|
+
const routeName = routeSegments[routeSegments.length - 1] || 'page'
|
|
885
|
+
const componentName = `AutoGenerated${toPascalCase(routeName)}${toPascalCase(templateType)}`
|
|
743
886
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
887
|
+
return templateType === 'layout'
|
|
888
|
+
? generateLayoutPageContent(appPath, componentName, templatePath, routeExports)
|
|
889
|
+
: generateRegularPageContent(appPath, templatePath, routeExports)
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/** Where a file under app/ is generated inside app/(templates)/. */
|
|
893
|
+
function templatesTreePath(appPath) {
|
|
894
|
+
return join(rootDir, 'app', '(templates)', appPath.replace(/^app\//, ''))
|
|
895
|
+
}
|
|
750
896
|
|
|
751
|
-
|
|
752
|
-
|
|
897
|
+
/**
|
|
898
|
+
* Everything the registry build puts in app/(templates), as absolute path to
|
|
899
|
+
* content: a copy of each app/ layout above a template's route, and the route
|
|
900
|
+
* file of each template that gets one. A layout template's route file takes the
|
|
901
|
+
* place of the copy of the app layout at the same path.
|
|
902
|
+
*/
|
|
903
|
+
async function planTemplatesTree(templates, analysis, appFiles) {
|
|
904
|
+
const files = new Map()
|
|
905
|
+
|
|
906
|
+
const directories = new Set(templates.map(({ appPath }) => appPath.replace(/\/[^\/]+$/, '')))
|
|
907
|
+
for (const directory of directories) {
|
|
908
|
+
for (const layoutPath of requiredLayoutPaths(directory, appFiles)) {
|
|
909
|
+
const copyPath = templatesTreePath(layoutPath)
|
|
910
|
+
if (!files.has(copyPath)) {
|
|
911
|
+
files.set(copyPath, await layoutCopyContent(layoutPath, appFiles))
|
|
912
|
+
}
|
|
913
|
+
}
|
|
753
914
|
}
|
|
754
915
|
|
|
755
|
-
// Generate required layouts for ALL template page directories (even if layout override doesn't exist)
|
|
756
|
-
// This ensures that pages in app/(templates)/ have the necessary layout hierarchy
|
|
757
|
-
// Extract unique directories from all templates (not just layout templates)
|
|
758
|
-
const uniqueDirectories = new Set()
|
|
759
916
|
for (const template of templates) {
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
917
|
+
const routeExports = templateAnalysisFor(analysis, template)
|
|
918
|
+
if (routeFileAction(template, routeExports) === 'write') {
|
|
919
|
+
files.set(templatesTreePath(template.appPath), routeFileContent(template, routeExports))
|
|
920
|
+
}
|
|
764
921
|
}
|
|
765
922
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
923
|
+
return files
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Every file under a directory, recursively, dotfiles included; none when it
|
|
928
|
+
* doesn't exist. Finder's .DS_Store is nobody's output and nobody's edit, and
|
|
929
|
+
* is left alone.
|
|
930
|
+
*/
|
|
931
|
+
async function listFiles(directory) {
|
|
932
|
+
if (!existsSync(directory)) return []
|
|
933
|
+
|
|
934
|
+
const files = []
|
|
935
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
936
|
+
if (entry.name === '.DS_Store') continue
|
|
937
|
+
const path = join(directory, entry.name)
|
|
938
|
+
if (entry.isDirectory()) {
|
|
939
|
+
files.push(...(await listFiles(path)))
|
|
940
|
+
} else if (entry.isFile()) {
|
|
941
|
+
files.push(path)
|
|
942
|
+
}
|
|
770
943
|
}
|
|
944
|
+
return files
|
|
945
|
+
}
|
|
771
946
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
947
|
+
/**
|
|
948
|
+
* Remove a directory the tree needs a file in place of, once the files in it
|
|
949
|
+
* are backed up and removed: the directories left in it, and any .DS_Store.
|
|
950
|
+
*/
|
|
951
|
+
async function removeDirectoryInTheWay(directory) {
|
|
952
|
+
const files = projectFiles(rootDir)
|
|
953
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
954
|
+
const path = join(directory, entry.name)
|
|
955
|
+
if (entry.isDirectory()) {
|
|
956
|
+
await removeDirectoryInTheWay(path)
|
|
957
|
+
} else if (entry.name === '.DS_Store') {
|
|
958
|
+
await files.unlink(path)
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
await files.rmdir(directory)
|
|
962
|
+
}
|
|
776
963
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
const componentName = `AutoGenerated${toPascalCase(routeName)}${toPascalCase(templateType)}`
|
|
964
|
+
/** Remove the empty directories under `directory`, deepest first, keeping `directory` itself. */
|
|
965
|
+
async function removeEmptyDirectories(directory) {
|
|
966
|
+
if (!existsSync(directory)) return
|
|
781
967
|
|
|
782
|
-
|
|
783
|
-
if (
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
968
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
969
|
+
if (!entry.isDirectory()) continue
|
|
970
|
+
const path = join(directory, entry.name)
|
|
971
|
+
await removeEmptyDirectories(path)
|
|
972
|
+
if ((await readdir(path)).length === 0) {
|
|
973
|
+
await projectFiles(rootDir).rmdir(path)
|
|
787
974
|
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
788
977
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
await generateTemplatePage(template, templatePagePath)
|
|
804
|
-
generated++
|
|
805
|
-
}
|
|
806
|
-
} else {
|
|
807
|
-
// File doesn't exist, generate
|
|
808
|
-
await generateTemplatePage(template, templatePagePath)
|
|
809
|
-
generated++
|
|
978
|
+
/**
|
|
979
|
+
* How app/(templates) differs from `files`, as absolute paths: the files
|
|
980
|
+
* `files` has that don't exist yet - a directory where one goes counts as none,
|
|
981
|
+
* since the files in it are removed - the ones that exist with other content,
|
|
982
|
+
* and the files in the tree that `files` doesn't have.
|
|
983
|
+
*/
|
|
984
|
+
async function diffTemplatesTree(templatesDir, files) {
|
|
985
|
+
const create = []
|
|
986
|
+
const replace = []
|
|
987
|
+
for (const [path, content] of files) {
|
|
988
|
+
if (!existsSync(path) || (await lstat(path)).isDirectory()) {
|
|
989
|
+
create.push(path)
|
|
990
|
+
} else if ((await readFile(path, 'utf8')) !== content) {
|
|
991
|
+
replace.push(path)
|
|
810
992
|
}
|
|
811
993
|
}
|
|
812
994
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
995
|
+
const remove = (await listFiles(templatesDir)).filter(path => !files.has(path))
|
|
996
|
+
return { create, replace, remove }
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Make app/(templates) hold exactly `contents`. The registry build owns that tree,
|
|
1001
|
+
* but a file about to be replaced with different content, or removed because
|
|
1002
|
+
* this build didn't produce it, may hold someone's edit: it is first copied into
|
|
1003
|
+
* a directory under .nextspark/backups/ that belongs to this run alone - the
|
|
1004
|
+
* time, and a suffix no other run gets - and named in the output. A backup is
|
|
1005
|
+
* never written over.
|
|
1006
|
+
*
|
|
1007
|
+
* Whatever runs the build - build, dev, registry:build or sync:app - the
|
|
1008
|
+
* backups are kept out of git by .nextspark/backups/.gitignore, put in place
|
|
1009
|
+
* before anything in the tree is written; when one already there can't do
|
|
1010
|
+
* that, nothing in the tree is written at all.
|
|
1011
|
+
*/
|
|
1012
|
+
async function reconcileTemplatesTree(templatesDir, contents) {
|
|
1013
|
+
const { create, replace, remove } = await diffTemplatesTree(templatesDir, contents)
|
|
1014
|
+
if (replace.length > 0 || remove.length > 0) {
|
|
1015
|
+
await ensureBackupsGitignore(rootDir)
|
|
1016
|
+
}
|
|
1017
|
+
let backupDir = null
|
|
1018
|
+
const files = projectFiles(rootDir)
|
|
1019
|
+
|
|
1020
|
+
const backUp = async absolutePath => {
|
|
1021
|
+
if (!backupDir) {
|
|
1022
|
+
const backupsRoot = join(rootDir, '.nextspark', 'backups')
|
|
1023
|
+
await files.mkdir(backupsRoot, { recursive: true })
|
|
1024
|
+
backupDir = await files.mkdtemp(join(backupsRoot, `${new Date().toISOString().replace(/[:.]/g, '-')}-`))
|
|
1025
|
+
}
|
|
1026
|
+
const relativePath = relative(rootDir, absolutePath)
|
|
1027
|
+
const backupPath = join(backupDir, relativePath)
|
|
1028
|
+
await files.mkdir(dirname(backupPath), { recursive: true })
|
|
1029
|
+
await files.copyFile(absolutePath, backupPath, constants.COPYFILE_EXCL)
|
|
1030
|
+
log(`app/(templates): backed up ${relativePath} to ${relative(rootDir, backupPath)}`, 'warning')
|
|
821
1031
|
}
|
|
1032
|
+
|
|
1033
|
+
// What stands where a file goes - a file where the tree needs a directory,
|
|
1034
|
+
// or the files of a directory where it needs a file - is backed up and
|
|
1035
|
+
// removed before anything is created, and new files still come before the
|
|
1036
|
+
// rest is replaced or removed
|
|
1037
|
+
const inTheWay = new Set(remove.filter(path =>
|
|
1038
|
+
create.some(created => created.startsWith(`${path}${sep}`) || path.startsWith(`${created}${sep}`))
|
|
1039
|
+
))
|
|
1040
|
+
for (const path of inTheWay) {
|
|
1041
|
+
await backUp(path)
|
|
1042
|
+
await files.unlink(path)
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
for (const path of create) {
|
|
1046
|
+
if (existsSync(path)) {
|
|
1047
|
+
await removeDirectoryInTheWay(path)
|
|
1048
|
+
}
|
|
1049
|
+
await files.mkdir(dirname(path), { recursive: true })
|
|
1050
|
+
await files.writeFile(path, contents.get(path), 'utf8')
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
for (const path of replace) {
|
|
1054
|
+
await backUp(path)
|
|
1055
|
+
await files.writeFile(path, contents.get(path), 'utf8')
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
for (const path of remove.filter(path => !inTheWay.has(path))) {
|
|
1059
|
+
await backUp(path)
|
|
1060
|
+
await files.unlink(path)
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
await removeEmptyDirectories(templatesDir)
|
|
1064
|
+
|
|
1065
|
+
return { created: create.length, updated: replace.length, removed: remove.length, backupDir }
|
|
822
1066
|
}
|