@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
|
@@ -0,0 +1,1003 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Updates a NextSpark project to another release of the @nextsparkjs packages.
|
|
3
|
+
*
|
|
4
|
+
* A project takes NextSpark from npm: its package.json pins @nextsparkjs/core,
|
|
5
|
+
* @nextsparkjs/cli and the other @nextsparkjs packages to one version, app/ is
|
|
6
|
+
* kept in step with core's templates by `nextspark sync:app`, and contents/
|
|
7
|
+
* holds the project's own themes and plugins. So an update sets those pins to
|
|
8
|
+
* the new version, installs, and syncs app/. No framework source is copied into
|
|
9
|
+
* the project, and of package.json only the @nextsparkjs versions change.
|
|
10
|
+
*
|
|
11
|
+
* What can be checked without changing anything is checked before the first
|
|
12
|
+
* write, so a run that can't start says why and leaves the project as it was.
|
|
13
|
+
* A run only starts from a clean git tree, because a run that fails or is
|
|
14
|
+
* interrupted once it has started writing undoes nothing itself: pnpm install
|
|
15
|
+
* runs lifecycle scripts, and those can write anywhere in the project, so the
|
|
16
|
+
* one rollback that covers whatever was written is going back to the commit the
|
|
17
|
+
* run started from. It exits non-zero and prints what it got through, what git
|
|
18
|
+
* status shows and that rollback.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
22
|
+
import fs from 'node:fs'
|
|
23
|
+
import os from 'node:os'
|
|
24
|
+
import path from 'node:path'
|
|
25
|
+
import { fileURLToPath } from 'node:url'
|
|
26
|
+
|
|
27
|
+
const SCOPE = '@nextsparkjs/'
|
|
28
|
+
const CORE_PACKAGE = '@nextsparkjs/core'
|
|
29
|
+
const CLI_PACKAGE = '@nextsparkjs/cli'
|
|
30
|
+
const DEPENDENCY_FIELDS = ['dependencies', 'devDependencies']
|
|
31
|
+
const VERSION_FILE = 'core.version.json'
|
|
32
|
+
const LOCKFILE = 'pnpm-lock.yaml'
|
|
33
|
+
const LIST_LIMIT = 20
|
|
34
|
+
const STATUS_SHOWN = 40
|
|
35
|
+
/** Signals an update stops its running step for and reports on; SIGHUP is a closed terminal */
|
|
36
|
+
const HANDLED_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP']
|
|
37
|
+
/** How long a stopped step's processes get to exit before they are killed */
|
|
38
|
+
const STOP_GRACE_MS = 5000
|
|
39
|
+
/** How long processes sent SIGKILL, or a released step guard, get to be gone */
|
|
40
|
+
const KILL_WAIT_MS = 2000
|
|
41
|
+
/** How long a step's group gets to empty on its own once its command exited */
|
|
42
|
+
const SETTLE_MS = 500
|
|
43
|
+
/** Runs each long step and kills it if update-core goes away first */
|
|
44
|
+
const STEP_GUARD = fileURLToPath(new URL('./step-guard.mjs', import.meta.url))
|
|
45
|
+
|
|
46
|
+
const VERSION_PATTERN = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/
|
|
47
|
+
|
|
48
|
+
const HELP = `
|
|
49
|
+
NextSpark Core Updater
|
|
50
|
+
|
|
51
|
+
Usage:
|
|
52
|
+
pnpm update-core [options]
|
|
53
|
+
|
|
54
|
+
Options:
|
|
55
|
+
--version <version> Update to this version (e.g. 0.1.0-beta.189)
|
|
56
|
+
--latest Update to the version npm tags latest (default)
|
|
57
|
+
--branch, -b Create the branch update/<version> and update there
|
|
58
|
+
--list List the published versions
|
|
59
|
+
--check Say whether a newer version is published
|
|
60
|
+
--current Show the installed version
|
|
61
|
+
--help, -h Show this help message
|
|
62
|
+
|
|
63
|
+
What gets updated:
|
|
64
|
+
package.json Only the versions of the @nextsparkjs packages, all set to exactly
|
|
65
|
+
the target: a ^ or ~ range is replaced, since pnpm would install
|
|
66
|
+
the newest version the range allows instead of the target
|
|
67
|
+
pnpm-lock.yaml, node_modules Through pnpm install, run where pnpm-lock.yaml is (in a
|
|
68
|
+
web-mobile project, the directory above web/)
|
|
69
|
+
app/ Synced with core's templates by nextspark sync:app, which keeps
|
|
70
|
+
files the project customized and rebuilds the registries
|
|
71
|
+
next.config.mjs, tsconfig.json, i18n.ts, proxy.ts or middleware.ts
|
|
72
|
+
Also synced by sync:app, which keeps them when customized
|
|
73
|
+
core.version.json Written once everything above succeeded
|
|
74
|
+
|
|
75
|
+
What update-core itself never writes:
|
|
76
|
+
The rest of package.json (name, scripts, other dependencies)
|
|
77
|
+
contents/ The project's themes and plugins
|
|
78
|
+
.env* Environment files
|
|
79
|
+
Lifecycle scripts that pnpm install runs are not bound by this list.
|
|
80
|
+
|
|
81
|
+
The update stops before changing anything when the project isn't in a git
|
|
82
|
+
repository with a commit, when it has uncommitted changes or no committed
|
|
83
|
+
pnpm-lock.yaml, when a @nextsparkjs package isn't published at the target
|
|
84
|
+
version, when the target is older than the installed version, when package.json
|
|
85
|
+
takes a @nextsparkjs package from somewhere other than the registry, or when .env
|
|
86
|
+
doesn't set NEXT_PUBLIC_ACTIVE_THEME, which the registry build needs. When every
|
|
87
|
+
@nextsparkjs package is already installed and pinned exactly to the target, there
|
|
88
|
+
is nothing to do and nothing is changed; a ^ or ~ range is set to the exact
|
|
89
|
+
target like any other pin.
|
|
90
|
+
|
|
91
|
+
If a step fails, or the run is interrupted with Ctrl-C (SIGINT), SIGTERM or
|
|
92
|
+
SIGHUP, after it started changing the project, update-core stops the running
|
|
93
|
+
step (processes still running 5 s after the signal are killed), undoes nothing,
|
|
94
|
+
exits non-zero and prints what it got through, what git status shows and the
|
|
95
|
+
command that rolls the project back to the commit it started from:
|
|
96
|
+
git reset --hard <commit> && git clean -fd && rm -rf node_modules && pnpm install --frozen-lockfile
|
|
97
|
+
That command is also printed before the first change, for a run that is killed
|
|
98
|
+
without a chance to report (SIGKILL). The install or sync such a run was running
|
|
99
|
+
is then killed by a guard process, but not processes that left that step's
|
|
100
|
+
process group, not while the guard is stopped (SIGSTOP), and not at all if the
|
|
101
|
+
guard is killed too.
|
|
102
|
+
While update-core or its guard is stopped (SIGSTOP, or Ctrl-Z for update-core),
|
|
103
|
+
none of this is enforced: the step and the processes it started keep running,
|
|
104
|
+
with no 5 s limit, and what update-core reports can't be relied on.
|
|
105
|
+
The rollback can't bring back files .gitignore ignores that a lifecycle script
|
|
106
|
+
overwrote, nor git refs a script changed (a submodule is put back on the branch
|
|
107
|
+
it was on only if that branch still points at the commit the project records),
|
|
108
|
+
and removes untracked files created while the update ran.
|
|
109
|
+
`
|
|
110
|
+
|
|
111
|
+
/** Runs a command to completion, printing its output as it goes unless `capture` asks for it back. */
|
|
112
|
+
export function runCommand(command, args, { cwd, capture = false }) {
|
|
113
|
+
const result = spawnSync(command, args, {
|
|
114
|
+
cwd,
|
|
115
|
+
encoding: 'utf8',
|
|
116
|
+
stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
117
|
+
shell: process.platform === 'win32' && !path.isAbsolute(command),
|
|
118
|
+
})
|
|
119
|
+
return {
|
|
120
|
+
status: result.error ? null : result.status,
|
|
121
|
+
stdout: result.stdout ?? '',
|
|
122
|
+
stderr: result.stderr ?? '',
|
|
123
|
+
error: result.error ?? null,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Orders two semver versions: negative when `a` comes first, positive when `b`
|
|
129
|
+
* does. A release comes after its prereleases, and prerelease identifiers
|
|
130
|
+
* compare numerically when both are numbers.
|
|
131
|
+
*/
|
|
132
|
+
export function compareVersions(a, b) {
|
|
133
|
+
const left = a.match(VERSION_PATTERN)
|
|
134
|
+
const right = b.match(VERSION_PATTERN)
|
|
135
|
+
for (let i = 1; i <= 3; i++) {
|
|
136
|
+
const difference = Number(left[i]) - Number(right[i])
|
|
137
|
+
if (difference !== 0) return Math.sign(difference)
|
|
138
|
+
}
|
|
139
|
+
if (!left[4] || !right[4]) return left[4] ? -1 : right[4] ? 1 : 0
|
|
140
|
+
|
|
141
|
+
const leftIds = left[4].split('.')
|
|
142
|
+
const rightIds = right[4].split('.')
|
|
143
|
+
for (let i = 0; i < Math.max(leftIds.length, rightIds.length); i++) {
|
|
144
|
+
if (leftIds[i] === undefined) return -1
|
|
145
|
+
if (rightIds[i] === undefined) return 1
|
|
146
|
+
const leftNumeric = /^\d+$/.test(leftIds[i])
|
|
147
|
+
const rightNumeric = /^\d+$/.test(rightIds[i])
|
|
148
|
+
if (leftNumeric && rightNumeric) {
|
|
149
|
+
const difference = Number(leftIds[i]) - Number(rightIds[i])
|
|
150
|
+
if (difference !== 0) return Math.sign(difference)
|
|
151
|
+
} else if (leftNumeric !== rightNumeric) {
|
|
152
|
+
return leftNumeric ? -1 : 1
|
|
153
|
+
} else if (leftIds[i] !== rightIds[i]) {
|
|
154
|
+
return leftIds[i] < rightIds[i] ? -1 : 1
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return 0
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isVersion(value) {
|
|
161
|
+
return typeof value === 'string' && VERSION_PATTERN.test(value)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** A spec with a protocol or a path takes the package from somewhere other than the registry. */
|
|
165
|
+
function takesFromRegistry(spec) {
|
|
166
|
+
return !spec.includes(':') && !spec.includes('/')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function parseArguments(args) {
|
|
170
|
+
const flags = { version: null, list: false, check: false, current: false, help: false, branch: false }
|
|
171
|
+
const problems = []
|
|
172
|
+
|
|
173
|
+
for (let i = 0; i < args.length; i++) {
|
|
174
|
+
const arg = args[i]
|
|
175
|
+
if (arg === '--version' || arg === '-v') {
|
|
176
|
+
const value = args[i + 1]
|
|
177
|
+
if (value === undefined || value.startsWith('-')) {
|
|
178
|
+
problems.push(`${arg} needs a version, e.g. ${arg} 0.1.0-beta.189`)
|
|
179
|
+
} else {
|
|
180
|
+
flags.version = value
|
|
181
|
+
i++
|
|
182
|
+
}
|
|
183
|
+
} else if (arg.startsWith('--version=')) {
|
|
184
|
+
flags.version = arg.slice('--version='.length)
|
|
185
|
+
} else if (arg === '--latest') {
|
|
186
|
+
flags.version = null
|
|
187
|
+
} else if (arg === '--list') {
|
|
188
|
+
flags.list = true
|
|
189
|
+
} else if (arg === '--check') {
|
|
190
|
+
flags.check = true
|
|
191
|
+
} else if (arg === '--current') {
|
|
192
|
+
flags.current = true
|
|
193
|
+
} else if (arg === '--branch' || arg === '-b') {
|
|
194
|
+
flags.branch = true
|
|
195
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
196
|
+
flags.help = true
|
|
197
|
+
} else {
|
|
198
|
+
problems.push(`Unknown option: ${arg}`)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (flags.version !== null) {
|
|
203
|
+
flags.version = flags.version.replace(/^v/, '')
|
|
204
|
+
if (!isVersion(flags.version)) problems.push(`Not a version: ${flags.version}`)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { flags, problems }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function readJson(file) {
|
|
211
|
+
try {
|
|
212
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
213
|
+
} catch {
|
|
214
|
+
return null
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function readOptional(file) {
|
|
219
|
+
try {
|
|
220
|
+
return fs.readFileSync(file)
|
|
221
|
+
} catch {
|
|
222
|
+
return null
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** The version of `name` in the project's node_modules, or null. */
|
|
227
|
+
function installedVersion(cwd, name) {
|
|
228
|
+
return readJson(path.join(cwd, 'node_modules', name, 'package.json'))?.version ?? null
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The project's package.json and the @nextsparkjs packages it declares, or why it can't be updated. */
|
|
232
|
+
function readProject(cwd) {
|
|
233
|
+
let manifestText
|
|
234
|
+
try {
|
|
235
|
+
manifestText = fs.readFileSync(path.join(cwd, 'package.json'), 'utf8')
|
|
236
|
+
} catch {
|
|
237
|
+
return { problem: 'No package.json here. Run update-core from the project root.' }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let manifest
|
|
241
|
+
try {
|
|
242
|
+
manifest = JSON.parse(manifestText)
|
|
243
|
+
} catch (error) {
|
|
244
|
+
return { problem: `package.json is not valid JSON: ${error.message}` }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (readJson(path.join(cwd, 'packages', 'core', 'package.json'))?.name === CORE_PACKAGE) {
|
|
248
|
+
return { problem: 'This is the NextSpark monorepo, where core is developed. update-core updates a project that installs @nextsparkjs/core.' }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const packages = []
|
|
252
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
253
|
+
for (const [name, spec] of Object.entries(manifest[field] ?? {})) {
|
|
254
|
+
if (name.startsWith(SCOPE)) packages.push({ field, name, spec })
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!packages.some(({ name }) => name === CORE_PACKAGE)) {
|
|
259
|
+
if (fs.existsSync(path.join(cwd, 'core'))) {
|
|
260
|
+
return {
|
|
261
|
+
problem: 'This project keeps the framework in core/, the layout from before NextSpark shipped as npm packages, and update-core only updates projects that install @nextsparkjs/core. Nothing was changed.\n' +
|
|
262
|
+
' To move it over, create a project with create-nextspark-app and bring your contents/ across.',
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return { problem: `package.json doesn't depend on ${CORE_PACKAGE}, so there is nothing for update-core to update.` }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { manifestText, manifest, packages, installed: installedVersion(cwd, CORE_PACKAGE) }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Whatever `pnpm view <spec> <field> --json` prints, parsed, or the error it gave. */
|
|
272
|
+
function viewRegistry(run, cwd, spec, field) {
|
|
273
|
+
const result = run('pnpm', ['view', spec, field, '--json'], { cwd, capture: true })
|
|
274
|
+
if (result.error?.code === 'ENOENT') return { error: 'pnpm is not installed or not on PATH' }
|
|
275
|
+
if (result.status !== 0) {
|
|
276
|
+
return { error: (result.stderr || result.stdout || `pnpm view exited with ${result.status}`).trim() }
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
return { value: JSON.parse(result.stdout) }
|
|
280
|
+
} catch {
|
|
281
|
+
return { error: `pnpm view ${spec} ${field} printed something other than JSON` }
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function publishedVersions(run, cwd, name) {
|
|
286
|
+
const { value, error } = viewRegistry(run, cwd, name, 'versions')
|
|
287
|
+
if (error) return { error }
|
|
288
|
+
return { versions: (Array.isArray(value) ? value : [value]).filter(isVersion) }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function latestVersion(run, cwd) {
|
|
292
|
+
const { value, error } = viewRegistry(run, cwd, CORE_PACKAGE, 'dist-tags')
|
|
293
|
+
if (error) return { error }
|
|
294
|
+
if (!isVersion(value?.latest)) return { error: `npm has no latest tag for ${CORE_PACKAGE}` }
|
|
295
|
+
return { version: value.latest }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function realPath(file) {
|
|
299
|
+
try {
|
|
300
|
+
return fs.realpathSync(file)
|
|
301
|
+
} catch {
|
|
302
|
+
return path.resolve(file)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Where the project stands in git: the commit and branch it is on, whether
|
|
308
|
+
* anything is uncommitted, whether the commit has submodules and, for each
|
|
309
|
+
* checked-out submodule (nested ones too) that is on a branch, its path from
|
|
310
|
+
* `cwd`, the branch and the commit, or why git couldn't list them. Changes
|
|
311
|
+
* inside a submodule count as uncommitted even where .gitmodules or the git
|
|
312
|
+
* config tells git status to ignore them, since the rollback resets submodules
|
|
313
|
+
* too; so on a clean tree a submodule's commit is the one the project records
|
|
314
|
+
* for it.
|
|
315
|
+
*/
|
|
316
|
+
function gitState(run, cwd) {
|
|
317
|
+
const top = run('git', ['rev-parse', '--show-toplevel'], { cwd, capture: true })
|
|
318
|
+
if (top.status !== 0) return { repo: false }
|
|
319
|
+
|
|
320
|
+
const head = run('git', ['rev-parse', '--verify', '--quiet', 'HEAD'], { cwd, capture: true })
|
|
321
|
+
const status = run('git', ['status', '--porcelain=v1', '--ignore-submodules=none'], { cwd, capture: true })
|
|
322
|
+
const branch = run('git', ['symbolic-ref', '--quiet', '--short', 'HEAD'], { cwd, capture: true })
|
|
323
|
+
const gitmodules = run('git', ['ls-files', '--', ':/.gitmodules'], { cwd, capture: true })
|
|
324
|
+
const submodules = gitmodules.status !== 0 || gitmodules.stdout.trim() !== ''
|
|
325
|
+
const onBranch = []
|
|
326
|
+
let submodulesUnlisted = null
|
|
327
|
+
if (submodules) {
|
|
328
|
+
const listed = run('git', ['submodule', 'foreach', '--quiet', '--recursive', 'printf "%s\\0%s\\0%s\\0" "$displaypath" "$(git symbolic-ref --quiet HEAD)" "$(git rev-parse HEAD)"'], { cwd, capture: true })
|
|
329
|
+
if (listed.status !== 0) {
|
|
330
|
+
submodulesUnlisted = `git submodule foreach ${listed.error ? listed.error.message : `exited ${listed.status}`}: ${(listed.stderr || listed.stdout).trim()}`
|
|
331
|
+
} else {
|
|
332
|
+
const fields = listed.stdout.split('\0')
|
|
333
|
+
for (let i = 0; i + 2 < fields.length; i += 3) {
|
|
334
|
+
if (fields[i + 1]) onBranch.push({ path: fields[i], ref: fields[i + 1], commit: fields[i + 2] })
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
repo: true,
|
|
340
|
+
top: top.stdout.trim(),
|
|
341
|
+
head: head.status === 0 ? head.stdout.trim() : null,
|
|
342
|
+
dirty: status.status !== 0 || status.stdout.trim() !== '',
|
|
343
|
+
branch: branch.status === 0 ? branch.stdout.trim() : null,
|
|
344
|
+
submodules,
|
|
345
|
+
submodulesOnBranch: onBranch,
|
|
346
|
+
submodulesUnlisted,
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function branchName(version) {
|
|
351
|
+
return `update/${version.replace(/[.+]/g, '-')}`
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function branchExists(run, cwd, name) {
|
|
355
|
+
return run('git', ['rev-parse', '--verify', '--quiet', `refs/heads/${name}`], { cwd, capture: true }).status === 0
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** `value` as one shell word. */
|
|
359
|
+
function shellWord(value) {
|
|
360
|
+
return /^[\w./@%+=:,-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* The directory the project's pnpm install runs in: the nearest one from `cwd`
|
|
365
|
+
* up to the top of the repository with a pnpm-lock.yaml, which for a web-mobile
|
|
366
|
+
* project is the one above web/, or null when there is none. Run from a web/
|
|
367
|
+
* that carries a pnpm-workspace.yaml of its own, pnpm would take that file for
|
|
368
|
+
* the whole workspace and install web/ on its own.
|
|
369
|
+
*/
|
|
370
|
+
function installRoot(cwd, top) {
|
|
371
|
+
const stop = realPath(top)
|
|
372
|
+
for (let dir = realPath(cwd); ; dir = path.dirname(dir)) {
|
|
373
|
+
if (fs.existsSync(path.join(dir, LOCKFILE))) return dir
|
|
374
|
+
if (dir === stop || path.dirname(dir) === dir) return null
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* The command that puts the project back at the commit the update started from.
|
|
380
|
+
* The tree was clean then, so resetting tracked files and removing untracked ones
|
|
381
|
+
* undoes whatever the update and the scripts it ran wrote in the repository;
|
|
382
|
+
* `git clean` only cleans below the directory it runs in, so from web/ in a
|
|
383
|
+
* web-mobile project it is pointed at the top of the repository. Neither goes
|
|
384
|
+
* into submodules: when the commit has any, each checked-out submodule is reset
|
|
385
|
+
* to its own HEAD, checked out again at the commit the project records for it
|
|
386
|
+
* if a script moved it or deleted its worktree (which leaves it detached;
|
|
387
|
+
* --checkout overrides an update strategy such as merge that a submodule's
|
|
388
|
+
* config sets), and cleaned. A submodule that was on a branch when the update
|
|
389
|
+
* started is put back on that branch if the branch still points at the recorded
|
|
390
|
+
* commit; one whose branch moved, say with a commit made inside it, stays
|
|
391
|
+
* detached at the recorded commit, and its branch isn't touched. That step only
|
|
392
|
+
* runs where the submodule's .git is: in a directory without one, `git -C`
|
|
393
|
+
* would find the project's own repository. With --branch,
|
|
394
|
+
* the update branch is deleted with update-ref, which also succeeds when the run
|
|
395
|
+
* stopped before creating it. node_modules is removed before installing because
|
|
396
|
+
* an install that was stopped can leave it holding the new versions while the
|
|
397
|
+
* lockfile still names the old ones, and pnpm install then finds nothing to do;
|
|
398
|
+
* the install is frozen to the commit's lockfile, so it fails instead of
|
|
399
|
+
* rewriting a lockfile that doesn't match the commit's package.json.
|
|
400
|
+
*/
|
|
401
|
+
function rollbackCommand(git, { cwd, root, branch }) {
|
|
402
|
+
const commit = git.head.slice(0, 12)
|
|
403
|
+
const project = realPath(cwd)
|
|
404
|
+
const commands = [`git reset --hard ${commit}`]
|
|
405
|
+
if (git.submodules) commands.push('git submodule foreach --recursive git reset --hard', 'git submodule update --checkout --recursive')
|
|
406
|
+
for (const submodule of git.submodulesOnBranch ?? []) {
|
|
407
|
+
const dir = shellWord(submodule.path)
|
|
408
|
+
const ref = shellWord(submodule.ref)
|
|
409
|
+
// Exits 0 either way: the if leaves a submodule whose branch moved detached
|
|
410
|
+
commands.push(`sh -c ${shellWord(`if test -e ${shellWord(`${submodule.path}/.git`)} && test "$(git -C ${dir} rev-parse --quiet --verify ${ref})" = ${submodule.commit}; then git -C ${dir} symbolic-ref HEAD ${ref}; fi`)}`)
|
|
411
|
+
}
|
|
412
|
+
commands.push(realPath(git.top) === project ? 'git clean -fd' : 'git clean -fd :/')
|
|
413
|
+
if (git.submodules) commands.push('git submodule foreach --recursive git clean -fd')
|
|
414
|
+
if (branch) {
|
|
415
|
+
commands.push(git.branch ? `git checkout ${shellWord(git.branch)}` : `git checkout --detach ${commit}`, `git update-ref -d refs/heads/${branch}`)
|
|
416
|
+
}
|
|
417
|
+
const fromProject = path.relative(project, root)
|
|
418
|
+
commands.push(
|
|
419
|
+
`rm -rf ${[...(fromProject ? [path.join(fromProject, 'node_modules')] : []), 'node_modules'].map(shellWord).join(' ')}`,
|
|
420
|
+
fromProject ? `pnpm --dir ${shellWord(fromProject)} install --frozen-lockfile` : 'pnpm install --frozen-lockfile',
|
|
421
|
+
)
|
|
422
|
+
return commands.join(' && ')
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** package.json text with `manifest` in it, keeping the indentation and line endings it had. */
|
|
426
|
+
function manifestText(manifest, original) {
|
|
427
|
+
const indent = original.match(/^[ \t]+(?=")/m)?.[0] ?? 2
|
|
428
|
+
let text = JSON.stringify(manifest, null, indent)
|
|
429
|
+
if (original.endsWith('\n')) text += '\n'
|
|
430
|
+
if (original.includes('\r\n')) text = text.replace(/\r?\n/g, '\r\n')
|
|
431
|
+
return text
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* The entry point of the nextspark CLI installed in the project. Running it with
|
|
436
|
+
* node skips `pnpm exec`, which checks the install against the lockfile first
|
|
437
|
+
* and, under pnpm 11, fetches the registry metadata of every package to do so.
|
|
438
|
+
*/
|
|
439
|
+
function installedCli(cwd) {
|
|
440
|
+
const cliDir = path.join(cwd, 'node_modules', CLI_PACKAGE)
|
|
441
|
+
const bin = readJson(path.join(cliDir, 'package.json'))?.bin
|
|
442
|
+
const entry = typeof bin === 'string' ? bin : bin?.nextspark
|
|
443
|
+
return entry ? path.join(cliDir, entry) : null
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Whether the registry build sync:app runs has a theme to build: it reads
|
|
448
|
+
* NEXT_PUBLIC_ACTIVE_THEME from the project's .env or the environment, and
|
|
449
|
+
* without one it skips the build and still exits 0.
|
|
450
|
+
*/
|
|
451
|
+
function hasActiveTheme(cwd, env) {
|
|
452
|
+
if (env.NEXT_PUBLIC_ACTIVE_THEME) return true
|
|
453
|
+
const text = readOptional(path.join(cwd, '.env'))?.toString('utf8') ?? ''
|
|
454
|
+
const lines = text.split(/\r?\n/).filter((line) => /^\s*(?:export\s+)?NEXT_PUBLIC_ACTIVE_THEME\s*=/.test(line))
|
|
455
|
+
if (lines.length === 0) return false
|
|
456
|
+
const raw = lines.at(-1).slice(lines.at(-1).indexOf('=') + 1).trim()
|
|
457
|
+
const quoted = raw.match(/^(["'`])(.*?)\1/)
|
|
458
|
+
const value = quoted ? quoted[2] : raw.replace(/\s+#.*$/, '').trim()
|
|
459
|
+
return value !== ''
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function coreMigrations(cwd) {
|
|
463
|
+
try {
|
|
464
|
+
return fs.readdirSync(path.join(cwd, 'node_modules', CORE_PACKAGE, 'migrations')).filter((file) => file.endsWith('.sql'))
|
|
465
|
+
} catch {
|
|
466
|
+
return []
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Sends `signal` to every process of the group a step runs in. */
|
|
471
|
+
function signalGroup(pid, signal) {
|
|
472
|
+
if (process.platform === 'win32') {
|
|
473
|
+
// Windows has no process groups: taskkill /T ends the step's process tree
|
|
474
|
+
spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore', timeout: 10_000 })
|
|
475
|
+
return
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
process.kill(-pid, signal)
|
|
479
|
+
} catch {
|
|
480
|
+
// ESRCH: nothing left in the group
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Whether any process of a step's group may still be running. EPERM counts as
|
|
486
|
+
* running: a process in the group can't be signalled, which on macOS is also
|
|
487
|
+
* the answer while its processes have exited and wait to be reaped.
|
|
488
|
+
*/
|
|
489
|
+
function groupRunning(pid) {
|
|
490
|
+
if (process.platform === 'win32') return false
|
|
491
|
+
try {
|
|
492
|
+
process.kill(-pid, 0)
|
|
493
|
+
return true
|
|
494
|
+
} catch (error) {
|
|
495
|
+
return error.code === 'EPERM'
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* `promise`'s value, or `fallback` once `ms` pass. The timer is cleared as soon
|
|
503
|
+
* as either settles, so a wait that ended early doesn't keep update-core running.
|
|
504
|
+
*/
|
|
505
|
+
function within(promise, ms, fallback) {
|
|
506
|
+
let timer
|
|
507
|
+
const timeout = new Promise((resolve) => { timer = setTimeout(resolve, Math.max(0, ms), fallback) })
|
|
508
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer))
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** Waits until the group is empty or `deadline` passes, and says whether it emptied. */
|
|
512
|
+
async function groupGone(pid, deadline) {
|
|
513
|
+
while (Date.now() < deadline) {
|
|
514
|
+
if (!groupRunning(pid)) return true
|
|
515
|
+
await pause(Math.min(100, Math.max(0, deadline - Date.now())))
|
|
516
|
+
}
|
|
517
|
+
return !groupRunning(pid)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Watches for the signals an update reports on while it writes. The first one
|
|
522
|
+
* is passed on to the running step's group, if any, and kept for the report,
|
|
523
|
+
* and from it the step's processes have STOP_GRACE_MS to exit. A repeat changes
|
|
524
|
+
* nothing: `pnpm update-core` passes on the signals it gets, so the Ctrl-C a
|
|
525
|
+
* terminal sends to both can arrive twice.
|
|
526
|
+
*/
|
|
527
|
+
function watchSignals() {
|
|
528
|
+
let interrupt
|
|
529
|
+
const watch = {
|
|
530
|
+
signal: null,
|
|
531
|
+
/** When the processes of a step stopped by the signal get killed */
|
|
532
|
+
deadline: null,
|
|
533
|
+
/** The pid of the running step, which is its group's id, once known */
|
|
534
|
+
step: null,
|
|
535
|
+
interrupted: new Promise((resolve) => { interrupt = resolve }),
|
|
536
|
+
}
|
|
537
|
+
const onSignal = (signal) => {
|
|
538
|
+
if (watch.signal) return
|
|
539
|
+
watch.signal = signal
|
|
540
|
+
watch.deadline = Date.now() + STOP_GRACE_MS
|
|
541
|
+
if (watch.step) signalGroup(watch.step, signal)
|
|
542
|
+
interrupt()
|
|
543
|
+
}
|
|
544
|
+
for (const signal of HANDLED_SIGNALS) process.on(signal, onSignal)
|
|
545
|
+
watch.stop = () => {
|
|
546
|
+
for (const signal of HANDLED_SIGNALS) process.off(signal, onSignal)
|
|
547
|
+
}
|
|
548
|
+
return watch
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Stops what is left of a step's group: sends `signal` unless it was already
|
|
553
|
+
* sent, waits for the group to empty until `deadline`, and then kills it.
|
|
554
|
+
* Returns how it stopped: 'exited' or 'killed', and whether anything was still
|
|
555
|
+
* running after being killed.
|
|
556
|
+
*/
|
|
557
|
+
async function stopGroup(pid, { signal, deadline }) {
|
|
558
|
+
if (signal) signalGroup(pid, signal)
|
|
559
|
+
if (await groupGone(pid, deadline)) return { stop: 'exited', leftRunning: false }
|
|
560
|
+
signalGroup(pid, 'SIGKILL')
|
|
561
|
+
return { stop: 'killed', leftRunning: !(await groupGone(pid, Date.now() + KILL_WAIT_MS)) }
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Runs a step's command and resolves once it is done with it: when it exited
|
|
566
|
+
* and its group emptied, or, when the run was interrupted, when its group
|
|
567
|
+
* emptied or its processes were killed STOP_GRACE_MS after the signal, without
|
|
568
|
+
* waiting for the command itself to exit. The command runs through
|
|
569
|
+
* step-guard.mjs, in a process group of its own that also holds the lifecycle
|
|
570
|
+
* scripts pnpm install starts: when the command exits, failing or not,
|
|
571
|
+
* whatever is still left of its group after SETTLE_MS gets SIGTERM and is
|
|
572
|
+
* killed STOP_GRACE_MS later, and if update-core is killed before it is done
|
|
573
|
+
* with the step, the guard kills the group. The guard starts the command only
|
|
574
|
+
* once it is told 'start', which is sent once the group's id is known and no
|
|
575
|
+
* signal has come, so a guard that dies before reporting the pid leaves no
|
|
576
|
+
* command running (except on Windows, which runs the command at once). None of
|
|
577
|
+
* this reaches processes that leave the group, and none of it runs while
|
|
578
|
+
* update-core or the guard is stopped (SIGSTOP), or once both are gone. Ctrl-C
|
|
579
|
+
* reaches update-core only, which passes it on. The step's stdin is closed: from
|
|
580
|
+
* a background group, reading the terminal would stop it instead of failing.
|
|
581
|
+
* `started` in the result says whether the command may have started.
|
|
582
|
+
*/
|
|
583
|
+
async function runStep(command, args, { cwd, signals }) {
|
|
584
|
+
const guard = spawn(process.execPath, [STEP_GUARD, command, ...args], {
|
|
585
|
+
cwd,
|
|
586
|
+
stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
|
|
587
|
+
detached: process.platform !== 'win32',
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
let started
|
|
591
|
+
let finished
|
|
592
|
+
let commandStarted = process.platform === 'win32'
|
|
593
|
+
const startedStep = new Promise((resolve) => { started = resolve })
|
|
594
|
+
const exited = new Promise((resolve) => { finished = resolve })
|
|
595
|
+
guard.on('message', (message) => {
|
|
596
|
+
if (message?.pid) {
|
|
597
|
+
signals.step = message.pid
|
|
598
|
+
if (signals.signal) {
|
|
599
|
+
// A signal that came before the pid did is passed on now, and the command is never started
|
|
600
|
+
signalGroup(message.pid, signals.signal)
|
|
601
|
+
} else {
|
|
602
|
+
guard.send('start', () => {})
|
|
603
|
+
commandStarted = true
|
|
604
|
+
}
|
|
605
|
+
started(message.pid)
|
|
606
|
+
} else if (message?.exit) {
|
|
607
|
+
finished({ status: message.exit.code, signal: message.exit.signal, error: null })
|
|
608
|
+
} else if (message?.error) {
|
|
609
|
+
started(null)
|
|
610
|
+
finished({ status: null, signal: null, error: message.error })
|
|
611
|
+
}
|
|
612
|
+
})
|
|
613
|
+
const guardGone = new Promise((resolve) => {
|
|
614
|
+
guard.on('error', (error) => resolve({ status: null, signal: null, error }))
|
|
615
|
+
guard.once('exit', (code, signal) => resolve({ status: null, signal: null, error: { message: `its guard process exited (${signal ?? `exit ${code}`})` } }))
|
|
616
|
+
})
|
|
617
|
+
guardGone.then((result) => {
|
|
618
|
+
started(null)
|
|
619
|
+
finished(result)
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
// A signal waits for the step's pid only until its deadline
|
|
623
|
+
const pid = await Promise.race([
|
|
624
|
+
startedStep,
|
|
625
|
+
signals.interrupted.then(() => within(startedStep, signals.deadline - Date.now(), undefined)),
|
|
626
|
+
])
|
|
627
|
+
if (pid === undefined) {
|
|
628
|
+
// Without the pid the step's group is out of reach: closing the channel has the guard kill it, if the guard can still run
|
|
629
|
+
guard.kill('SIGCONT')
|
|
630
|
+
guard.disconnect()
|
|
631
|
+
if (!(await within(guardGone.then(() => true), KILL_WAIT_MS, false))) guard.kill('SIGKILL')
|
|
632
|
+
signals.step = null
|
|
633
|
+
return { status: null, signal: null, error: null, stop: 'unreached', leftRunning: false, started: commandStarted }
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const result = await Promise.race([exited, signals.interrupted.then(() => null)])
|
|
637
|
+
let outcome = { stop: null, leftRunning: false }
|
|
638
|
+
if (pid !== null) {
|
|
639
|
+
if (signals.signal) {
|
|
640
|
+
outcome = await stopGroup(pid, { deadline: signals.deadline })
|
|
641
|
+
} else if (!(await groupGone(pid, Date.now() + SETTLE_MS))) {
|
|
642
|
+
// The command exited, failing or not, and left processes running that could still write
|
|
643
|
+
outcome = await stopGroup(pid, { signal: 'SIGTERM', deadline: Math.min(Date.now() + STOP_GRACE_MS, signals.deadline ?? Infinity) })
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
signals.step = null
|
|
647
|
+
guard.send('release', () => {})
|
|
648
|
+
if (!(await within(guardGone.then(() => true), KILL_WAIT_MS, false))) guard.kill('SIGKILL')
|
|
649
|
+
return { ...(result ?? { status: null, signal: null, error: null }), ...outcome, started: commandStarted }
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function describeExit(result) {
|
|
653
|
+
if (result.error) return result.error.code === 'ENOENT' ? 'not found on PATH' : result.error.message
|
|
654
|
+
return result.signal ? `killed by ${result.signal}` : `exit ${result.status}`
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function listReleases({ run, cwd, installed, out, err }) {
|
|
658
|
+
const published = publishedVersions(run, cwd, CORE_PACKAGE)
|
|
659
|
+
if (published.error) {
|
|
660
|
+
err(`Could not read the published versions of ${CORE_PACKAGE}:\n${published.error}`)
|
|
661
|
+
return 1
|
|
662
|
+
}
|
|
663
|
+
const latest = latestVersion(run, cwd).version
|
|
664
|
+
|
|
665
|
+
const versions = [...published.versions].sort(compareVersions).reverse()
|
|
666
|
+
out(`Published versions of ${CORE_PACKAGE}:\n`)
|
|
667
|
+
for (const version of versions.slice(0, LIST_LIMIT)) {
|
|
668
|
+
const marks = [version === latest && 'latest', version === installed && 'installed'].filter(Boolean)
|
|
669
|
+
out(` ${version}${marks.length > 0 ? ` (${marks.join(', ')})` : ''}`)
|
|
670
|
+
}
|
|
671
|
+
if (versions.length > LIST_LIMIT) out(` ... and ${versions.length - LIST_LIMIT} older`)
|
|
672
|
+
return 0
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function checkUpdates({ run, cwd, installed, out, err }) {
|
|
676
|
+
const latest = latestVersion(run, cwd)
|
|
677
|
+
if (latest.error) {
|
|
678
|
+
err(`Could not read the latest version of ${CORE_PACKAGE}:\n${latest.error}`)
|
|
679
|
+
return 1
|
|
680
|
+
}
|
|
681
|
+
out(`Installed: ${installed ?? 'not installed'}`)
|
|
682
|
+
out(`Latest: ${latest.version}`)
|
|
683
|
+
if (!installed || compareVersions(latest.version, installed) > 0) {
|
|
684
|
+
out('\nUpdate available. Run: pnpm update-core')
|
|
685
|
+
} else {
|
|
686
|
+
out("\nYou're up to date.")
|
|
687
|
+
}
|
|
688
|
+
return 0
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function banner(out, title) {
|
|
692
|
+
out('\n========================================')
|
|
693
|
+
out(` ${title}`)
|
|
694
|
+
out('========================================')
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* The report of an update that stopped after it started writing: what stopped
|
|
699
|
+
* it, how the processes of the step it stopped ended, the steps it finished and
|
|
700
|
+
* the ones it never reached, what git status shows now, and the rollback.
|
|
701
|
+
* Returns the exit code, 128 plus the signal's number for an interrupted run.
|
|
702
|
+
*/
|
|
703
|
+
function stoppedReport({ run, cwd, err, target, rollback, signal, step, started, reason, stop, leftRunning, done, notReached }) {
|
|
704
|
+
const grace = `${STOP_GRACE_MS / 1000} s`
|
|
705
|
+
banner(err, `Update to ${target} did not finish`)
|
|
706
|
+
err('')
|
|
707
|
+
if (signal) err(` Interrupted by ${signal} ${started ? 'during' : 'before'}: ${step}`)
|
|
708
|
+
else err(` Failed ${started ? 'during' : 'before'}: ${step} (${reason})`)
|
|
709
|
+
// "during" only means the command may have started, so these speak of the step's process group, not of the command.
|
|
710
|
+
// Windows has no process groups to check: taskkill ran after the signal, and its result isn't known.
|
|
711
|
+
if (signal && started && stop === 'exited') {
|
|
712
|
+
err(process.platform === 'win32'
|
|
713
|
+
? ` taskkill /T /F was run on ${step} and the processes under it after the signal; whether they stopped isn't checked.`
|
|
714
|
+
: ` No process in the process group of ${step} was left running after the signal.`)
|
|
715
|
+
}
|
|
716
|
+
if (signal && started && stop === 'killed') err(` Processes in the process group of ${step} were still running ${grace} after the signal, and were killed with SIGKILL.`)
|
|
717
|
+
if (signal && stop === 'unreached') {
|
|
718
|
+
err(started
|
|
719
|
+
? ` The process that runs ${step} didn't say which process group it runs in within ${grace} of the signal, and was stopped; ${step} may still be running and write to the project.`
|
|
720
|
+
: ` The process that runs ${step} didn't say which process group it runs in within ${grace} of the signal, and was stopped before it started ${step}.`)
|
|
721
|
+
}
|
|
722
|
+
if (!signal && stop === 'exited') err(' Processes it started were still running after it exited, and exited after SIGTERM.')
|
|
723
|
+
if (!signal && stop === 'killed') err(` Processes it started were still running after it exited and ${grace} after SIGTERM, and were killed with SIGKILL.`)
|
|
724
|
+
if (leftRunning) err(' Some of its processes were still running after being killed, and may still write to the project.')
|
|
725
|
+
if (done.length > 0) {
|
|
726
|
+
err('\n Done before that:')
|
|
727
|
+
for (const line of done) err(` - ${line}`)
|
|
728
|
+
}
|
|
729
|
+
if (notReached.length > 0) {
|
|
730
|
+
err('\n Not reached:')
|
|
731
|
+
for (const line of notReached) err(` - ${line}`)
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
err('\n Nothing was undone. A step that stopped may have written part of its work, and the')
|
|
735
|
+
err(' lifecycle scripts pnpm install runs can write anywhere in the project.')
|
|
736
|
+
const status = run('git', ['status', '--short', '--ignore-submodules=none'], { cwd, capture: true })
|
|
737
|
+
if (status.status !== 0) {
|
|
738
|
+
err(` git status failed: ${(status.stderr || status.stdout).trim()}`)
|
|
739
|
+
} else {
|
|
740
|
+
const lines = status.stdout.split('\n').filter(Boolean)
|
|
741
|
+
if (lines.length === 0) {
|
|
742
|
+
err(' git status shows no changes.')
|
|
743
|
+
} else {
|
|
744
|
+
err(' git status now (files .gitignore ignores, like node_modules, are not listed):')
|
|
745
|
+
for (const line of lines.slice(0, STATUS_SHOWN)) err(` ${line}`)
|
|
746
|
+
if (lines.length > STATUS_SHOWN) err(` ... and ${lines.length - STATUS_SHOWN} more (git status lists them all)`)
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
err('\n To put the project back at the commit the update started from, run here:')
|
|
751
|
+
err(` ${rollback}`)
|
|
752
|
+
err(` Then ${signal ? '' : 'fix what failed and '}run update-core --version ${target} again.`)
|
|
753
|
+
err('')
|
|
754
|
+
return signal ? 128 + (os.constants.signals[signal] ?? 0) : 1
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Runs `update-core` with `args` against the project in `cwd` and resolves to
|
|
759
|
+
* the exit code. `run` executes the commands that only read and `out`/`err`
|
|
760
|
+
* print, so a test can hand in its own.
|
|
761
|
+
*/
|
|
762
|
+
export async function updateCore(args, { cwd = process.cwd(), env = process.env, run = runCommand, out = console.log, err = console.error, now = () => new Date() } = {}) {
|
|
763
|
+
const { flags, problems } = parseArguments(args)
|
|
764
|
+
if (problems.length > 0) {
|
|
765
|
+
for (const problem of problems) err(problem)
|
|
766
|
+
err('Run pnpm update-core --help for the options.')
|
|
767
|
+
return 1
|
|
768
|
+
}
|
|
769
|
+
if (flags.help) {
|
|
770
|
+
out(HELP)
|
|
771
|
+
return 0
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const project = readProject(cwd)
|
|
775
|
+
if (project.problem) {
|
|
776
|
+
err(`update-core: ${project.problem}`)
|
|
777
|
+
return 1
|
|
778
|
+
}
|
|
779
|
+
const { manifest, packages, installed } = project
|
|
780
|
+
|
|
781
|
+
if (flags.current) {
|
|
782
|
+
const spec = packages.find(({ name }) => name === CORE_PACKAGE).spec
|
|
783
|
+
out(installed ? `${CORE_PACKAGE} ${installed}` : `${CORE_PACKAGE} is not installed (package.json asks for ${spec})`)
|
|
784
|
+
return 0
|
|
785
|
+
}
|
|
786
|
+
if (flags.list) return listReleases({ run, cwd, installed, out, err })
|
|
787
|
+
if (flags.check) return checkUpdates({ run, cwd, installed, out, err })
|
|
788
|
+
|
|
789
|
+
out('\n========================================')
|
|
790
|
+
out(' NextSpark Core Updater')
|
|
791
|
+
out('========================================\n')
|
|
792
|
+
|
|
793
|
+
// Every check comes before the first write
|
|
794
|
+
out('[1/5] Checking the project...')
|
|
795
|
+
|
|
796
|
+
const fromElsewhere = packages.filter(({ spec }) => !takesFromRegistry(spec))
|
|
797
|
+
if (fromElsewhere.length > 0) {
|
|
798
|
+
err(' package.json takes these from somewhere other than the registry, so there is no version for update-core to set:')
|
|
799
|
+
for (const { name, spec } of fromElsewhere) err(` ${name}: ${spec}`)
|
|
800
|
+
err(' Nothing was changed.')
|
|
801
|
+
return 1
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (!packages.some(({ name }) => name === CLI_PACKAGE)) {
|
|
805
|
+
err(` update-core syncs app/ through the nextspark CLI, and package.json doesn't depend on ${CLI_PACKAGE}. Nothing was changed.`)
|
|
806
|
+
return 1
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (!installed) {
|
|
810
|
+
err(` node_modules has no ${CORE_PACKAGE}. Run pnpm install first, so the update starts from a known version. Nothing was changed.`)
|
|
811
|
+
return 1
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const git = gitState(run, cwd)
|
|
815
|
+
if (!git.repo || !git.head) {
|
|
816
|
+
err(` update-core needs the project in a git repository with a commit to start from: an update that fails or is interrupted is rolled back by returning to that commit. ${git.repo ? 'Make a first commit' : 'Run git init and commit the project'} first. Nothing was changed.`)
|
|
817
|
+
return 1
|
|
818
|
+
}
|
|
819
|
+
if (git.dirty) {
|
|
820
|
+
err(' Uncommitted changes. Commit or stash them first: an update that fails or is interrupted is rolled back by returning to the commit it started from, which would discard them. Nothing was changed.')
|
|
821
|
+
return 1
|
|
822
|
+
}
|
|
823
|
+
if (git.submodulesUnlisted) {
|
|
824
|
+
err(` Could not list the branches the submodules are on, which the rollback needs to put them back on: ${git.submodulesUnlisted}. Nothing was changed.`)
|
|
825
|
+
return 1
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// The rollback installs exactly what the commit's lockfile records, so the commit needs one
|
|
829
|
+
const root = installRoot(cwd, git.top)
|
|
830
|
+
const lockfile = root && run('git', ['ls-files', '--error-unmatch', '--', path.join(root, LOCKFILE)], { cwd, capture: true })
|
|
831
|
+
if (!root || lockfile.status !== 0) {
|
|
832
|
+
err(` ${root ? `${path.relative(cwd, path.join(root, LOCKFILE)) || LOCKFILE} isn't committed` : `No ${LOCKFILE} here or above, up to the top of the repository`}. An update that fails or is interrupted is rolled back by installing what the lockfile of the commit it started from records, so commit ${LOCKFILE} first. Nothing was changed.`)
|
|
833
|
+
return 1
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (!hasActiveTheme(cwd, env)) {
|
|
837
|
+
err(' NEXT_PUBLIC_ACTIVE_THEME is not set in .env, and sync:app skips the registry build without it, so app/(templates) would stay on the old core. Set it and run update-core again. Nothing was changed.')
|
|
838
|
+
return 1
|
|
839
|
+
}
|
|
840
|
+
out(` Installed: ${CORE_PACKAGE} ${installed}`)
|
|
841
|
+
|
|
842
|
+
let target = flags.version
|
|
843
|
+
if (!target) {
|
|
844
|
+
const latest = latestVersion(run, cwd)
|
|
845
|
+
if (latest.error) {
|
|
846
|
+
err(` Could not read the latest version of ${CORE_PACKAGE}:\n${latest.error}\n Nothing was changed.`)
|
|
847
|
+
return 1
|
|
848
|
+
}
|
|
849
|
+
target = latest.version
|
|
850
|
+
}
|
|
851
|
+
out(` Target: ${target}`)
|
|
852
|
+
|
|
853
|
+
if (compareVersions(target, installed) < 0) {
|
|
854
|
+
err(` ${target} is older than the installed ${installed}. update-core doesn't downgrade: app/ would be synced back and applied migrations can't be undone. Nothing was changed.`)
|
|
855
|
+
return 1
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (packages.every(({ name, spec }) => spec === target && installedVersion(cwd, name) === target)) {
|
|
859
|
+
out(`\n Already on ${target}: every @nextsparkjs package in package.json is pinned to it and node_modules holds it. Nothing was changed.\n`)
|
|
860
|
+
return 0
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const missing = []
|
|
864
|
+
for (const { name } of packages) {
|
|
865
|
+
const published = publishedVersions(run, cwd, name)
|
|
866
|
+
if (published.error) {
|
|
867
|
+
err(` Could not read the published versions of ${name}:\n${published.error}\n Nothing was changed.`)
|
|
868
|
+
return 1
|
|
869
|
+
}
|
|
870
|
+
if (!published.versions.includes(target)) missing.push(name)
|
|
871
|
+
}
|
|
872
|
+
if (missing.length > 0) {
|
|
873
|
+
err(` Not published at ${target}: ${missing.join(', ')}. Nothing was changed.`)
|
|
874
|
+
return 1
|
|
875
|
+
}
|
|
876
|
+
out(` Published: all ${packages.length} @nextsparkjs packages at ${target}`)
|
|
877
|
+
|
|
878
|
+
const branch = flags.branch ? branchName(target) : null
|
|
879
|
+
if (branch && branchExists(run, cwd, branch)) {
|
|
880
|
+
err(` Branch ${branch} already exists. Delete it (git branch -D ${branch}) or update without --branch. Nothing was changed.`)
|
|
881
|
+
return 1
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const rollback = rollbackCommand(git, { cwd, root, branch })
|
|
885
|
+
out(`\n Starting from commit ${git.head.slice(0, 12)}. If this run is killed before it can report (SIGKILL), roll back with:`)
|
|
886
|
+
out(` ${rollback}`)
|
|
887
|
+
out(' Killing this run also kills the install or sync it is running, unless its guard process is stopped or killed too, but not processes that left that step\'s process group.')
|
|
888
|
+
out(' The rollback removes files created in the repository while the update runs, and can\'t bring back ignored files, like .env, that a lifecycle script overwrites.')
|
|
889
|
+
|
|
890
|
+
// From here on the project changes, and whatever stops the run goes through stoppedReport
|
|
891
|
+
const signals = watchSignals()
|
|
892
|
+
const done = []
|
|
893
|
+
const steps = [
|
|
894
|
+
...(branch ? [`create and switch to branch ${branch}`] : []),
|
|
895
|
+
'set the @nextsparkjs versions in package.json',
|
|
896
|
+
'pnpm install',
|
|
897
|
+
'nextspark sync:app --force',
|
|
898
|
+
`write ${VERSION_FILE}`,
|
|
899
|
+
]
|
|
900
|
+
let current = 0
|
|
901
|
+
const stopped = ({ reason = null, started = true, stop = null, leftRunning = false } = {}) => stoppedReport({
|
|
902
|
+
run, cwd, err, target, rollback, reason, started, stop, leftRunning,
|
|
903
|
+
signal: signals.signal,
|
|
904
|
+
step: steps[current],
|
|
905
|
+
done,
|
|
906
|
+
notReached: steps.slice(current + 1),
|
|
907
|
+
})
|
|
908
|
+
// A signal that came in while a synchronous write ran is only delivered on the next turn of the event loop
|
|
909
|
+
const signalled = async () => {
|
|
910
|
+
await new Promise((resolve) => setImmediate(resolve))
|
|
911
|
+
return signals.signal !== null
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
try {
|
|
915
|
+
if (branch) {
|
|
916
|
+
out(`\n Creating branch ${branch}...`)
|
|
917
|
+
const created = run('git', ['checkout', '-b', branch], { cwd, capture: true })
|
|
918
|
+
if (created.status !== 0) {
|
|
919
|
+
// git can have created the branch, or switched to it, before failing
|
|
920
|
+
const exit = created.error ? created.error.message : `exited ${created.status}`
|
|
921
|
+
return stopped({ reason: `git checkout -b ${exit}: ${(created.stderr || created.stdout).trim()}` })
|
|
922
|
+
}
|
|
923
|
+
done.push(`created and switched to branch ${branch}`)
|
|
924
|
+
current++
|
|
925
|
+
if (await signalled()) return stopped({ started: false })
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
out('\n[2/5] Setting the @nextsparkjs versions in package.json...')
|
|
929
|
+
const bumped = []
|
|
930
|
+
for (const { field, name, spec } of packages) {
|
|
931
|
+
if (spec !== target) {
|
|
932
|
+
manifest[field][name] = target
|
|
933
|
+
bumped.push(`${name} ${spec} -> ${target}`)
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
if (bumped.length > 0) {
|
|
937
|
+
fs.writeFileSync(path.join(cwd, 'package.json'), manifestText(manifest, project.manifestText))
|
|
938
|
+
for (const line of bumped) out(` ${line}`)
|
|
939
|
+
done.push(`package.json: ${bumped.join(', ')}`)
|
|
940
|
+
} else {
|
|
941
|
+
out(` Already set to ${target}`)
|
|
942
|
+
}
|
|
943
|
+
current++
|
|
944
|
+
if (await signalled()) return stopped({ started: false })
|
|
945
|
+
|
|
946
|
+
out('\n[3/5] Installing...')
|
|
947
|
+
const migrationsBefore = new Set(coreMigrations(cwd))
|
|
948
|
+
const install = await runStep('pnpm', ['install'], { cwd: root, signals })
|
|
949
|
+
if (signals.signal) return stopped({ started: install.started, stop: install.stop, leftRunning: install.leftRunning })
|
|
950
|
+
if (install.status !== 0) return stopped({ reason: describeExit(install), started: install.started, stop: install.stop, leftRunning: install.leftRunning })
|
|
951
|
+
if (install.leftRunning) return stopped({ reason: 'it exited 0, but processes it started were still running after being killed', stop: install.stop, leftRunning: true })
|
|
952
|
+
if (install.stop) out(` Processes pnpm install started were still running after it exited, and were ${install.stop === 'killed' ? 'killed' : 'stopped with SIGTERM'}.`)
|
|
953
|
+
const offTarget = packages
|
|
954
|
+
.map(({ name }) => ({ name, version: installedVersion(cwd, name) }))
|
|
955
|
+
.filter(({ version }) => version !== target)
|
|
956
|
+
if (offTarget.length > 0) {
|
|
957
|
+
return stopped({ reason: `it exited 0, but node_modules holds ${offTarget.map(({ name, version }) => `${name} ${version ?? '(missing)'}`).join(', ')} instead of ${target}` })
|
|
958
|
+
}
|
|
959
|
+
done.push(`pnpm install: every @nextsparkjs package installed at ${target}`)
|
|
960
|
+
current++
|
|
961
|
+
|
|
962
|
+
out('\n[4/5] Syncing app/ with core and rebuilding the registries...')
|
|
963
|
+
const cli = installedCli(cwd)
|
|
964
|
+
if (!cli) return stopped({ reason: `node_modules has no nextspark CLI from ${CLI_PACKAGE}` })
|
|
965
|
+
fs.rmSync(path.join(cwd, '.next'), { recursive: true, force: true })
|
|
966
|
+
done.push('.next removed')
|
|
967
|
+
if (await signalled()) return stopped({ started: false })
|
|
968
|
+
const sync = await runStep(process.execPath, [cli, 'sync:app', '--force'], { cwd, signals })
|
|
969
|
+
if (signals.signal) return stopped({ started: sync.started, stop: sync.stop, leftRunning: sync.leftRunning })
|
|
970
|
+
if (sync.status !== 0) return stopped({ reason: `${describeExit(sync)}${sync.started ? '; what it reported is above' : ''}`, started: sync.started, stop: sync.stop, leftRunning: sync.leftRunning })
|
|
971
|
+
if (sync.leftRunning) return stopped({ reason: 'it exited 0, but processes it started were still running after being killed', stop: sync.stop, leftRunning: true })
|
|
972
|
+
if (sync.stop) out(` Processes nextspark sync:app started were still running after it exited, and were ${sync.stop === 'killed' ? 'killed' : 'stopped with SIGTERM'}.`)
|
|
973
|
+
done.push('app/ synced with core and registries rebuilt by nextspark sync:app')
|
|
974
|
+
current++
|
|
975
|
+
|
|
976
|
+
if (await signalled()) return stopped({ started: false })
|
|
977
|
+
|
|
978
|
+
out('\n[5/5] Recording the version...')
|
|
979
|
+
const record = { version: target, previousVersion: installed, updatedAt: now().toISOString() }
|
|
980
|
+
fs.writeFileSync(path.join(cwd, VERSION_FILE), `${JSON.stringify(record, null, 2)}\n`)
|
|
981
|
+
out(` ${VERSION_FILE}: ${installed} -> ${target}`)
|
|
982
|
+
|
|
983
|
+
const newMigrations = coreMigrations(cwd).filter((file) => !migrationsBefore.has(file))
|
|
984
|
+
|
|
985
|
+
banner(out, 'Update Complete')
|
|
986
|
+
out(`\n ${CORE_PACKAGE} ${installed} -> ${target}`)
|
|
987
|
+
if (branch) out(` Branch: ${branch}`)
|
|
988
|
+
if (newMigrations.length > 0) out(` New core migrations: ${newMigrations.length}`)
|
|
989
|
+
out('\n Next steps:')
|
|
990
|
+
const nextSteps = ['Review: git status && git diff', 'Test: pnpm build && pnpm dev']
|
|
991
|
+
if (newMigrations.length > 0) nextSteps.push('Migrate: pnpm db:migrate')
|
|
992
|
+
nextSteps.push(branch ? `Commit on ${branch} and merge it` : 'Commit the update')
|
|
993
|
+
nextSteps.forEach((step, index) => out(` ${index + 1}. ${step}`))
|
|
994
|
+
out('')
|
|
995
|
+
out(` Roll back: ${rollback}`)
|
|
996
|
+
out('')
|
|
997
|
+
return 0
|
|
998
|
+
} catch (error) {
|
|
999
|
+
return stopped({ reason: error.message })
|
|
1000
|
+
} finally {
|
|
1001
|
+
signals.stop()
|
|
1002
|
+
}
|
|
1003
|
+
}
|