@omega.js/web 0.1.0
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/LICENSE +98 -0
- package/README.md +640 -0
- package/bin/omega +2 -0
- package/bin/omega-web +2 -0
- package/bin/omg +2 -0
- package/core/_includes/admin/sections/sidebar.json +100 -0
- package/core/_includes/admin/sections/topbar.json +11 -0
- package/core/_includes/backend/sections/sidebar.json +39 -0
- package/core/_includes/backend/sections/topbar.json +16 -0
- package/core/_includes/core/body.html +102 -0
- package/core/_includes/core/foot.html +536 -0
- package/core/_includes/core/head.html +423 -0
- package/core/_includes/core/pricing/price-per-unit.html +33 -0
- package/core/_includes/core/showcase/entry-body.html +94 -0
- package/core/_includes/core/showcase/index-body.html +43 -0
- package/core/_includes/core/showcase/nav.html +60 -0
- package/core/_includes/frontend/sections/footer.json +74 -0
- package/core/_includes/frontend/sections/nav.json +70 -0
- package/core/_includes/global/sections/account.json +31 -0
- package/core/_includes/modules/engagement/giscus.html +26 -0
- package/core/_layouts/blueprint/404.html +10 -0
- package/core/_layouts/blueprint/about.html +10 -0
- package/core/_layouts/blueprint/account/index.html +11 -0
- package/core/_layouts/blueprint/admin/calendar/index.html +590 -0
- package/core/_layouts/blueprint/admin/dashboard/index.html +301 -0
- package/core/_layouts/blueprint/admin/firebase/index.html +214 -0
- package/core/_layouts/blueprint/admin/posts/editor.html +157 -0
- package/core/_layouts/blueprint/admin/posts/index.html +126 -0
- package/core/_layouts/blueprint/admin/stackblitz/index.html +30 -0
- package/core/_layouts/blueprint/admin/studio/index.html +85 -0
- package/core/_layouts/blueprint/admin/users/index.html +272 -0
- package/core/_layouts/blueprint/admin/users/new.html +82 -0
- package/core/_layouts/blueprint/admin/verts/index.html +204 -0
- package/core/_layouts/blueprint/alternatives/alternative.html +15 -0
- package/core/_layouts/blueprint/alternatives/index.html +10 -0
- package/core/_layouts/blueprint/app.html +10 -0
- package/core/_layouts/blueprint/auth/reset.html +19 -0
- package/core/_layouts/blueprint/auth/signin.html +19 -0
- package/core/_layouts/blueprint/auth/signup.html +19 -0
- package/core/_layouts/blueprint/auth/token.html +19 -0
- package/core/_layouts/blueprint/blog/categories/category.html +8 -0
- package/core/_layouts/blueprint/blog/categories/index.html +11 -0
- package/core/_layouts/blueprint/blog/index.html +10 -0
- package/core/_layouts/blueprint/blog/post.html +5 -0
- package/core/_layouts/blueprint/blog/tags/index.html +11 -0
- package/core/_layouts/blueprint/blog/tags/tag.html +8 -0
- package/core/_layouts/blueprint/careers.md +13 -0
- package/core/_layouts/blueprint/collection/category.html +9 -0
- package/core/_layouts/blueprint/collection/document.html +9 -0
- package/core/_layouts/blueprint/collection/index.html +9 -0
- package/core/_layouts/blueprint/connections/callback.html +11 -0
- package/core/_layouts/blueprint/contact.html +14 -0
- package/core/_layouts/blueprint/dashboard/index.html +10 -0
- package/core/_layouts/blueprint/download.html +20 -0
- package/core/_layouts/blueprint/extension/index.html +14 -0
- package/core/_layouts/blueprint/extension/installed.html +11 -0
- package/core/_layouts/blueprint/feedback.html +10 -0
- package/core/_layouts/blueprint/index.html +16 -0
- package/core/_layouts/blueprint/legal/cookies.md +57 -0
- package/core/_layouts/blueprint/legal/privacy.md +109 -0
- package/core/_layouts/blueprint/legal/terms.md +155 -0
- package/core/_layouts/blueprint/payment/checkout.html +15 -0
- package/core/_layouts/blueprint/payment/confirmation.html +14 -0
- package/core/_layouts/blueprint/portal/email-preferences.html +15 -0
- package/core/_layouts/blueprint/pricing.html +20 -0
- package/core/_layouts/blueprint/status.html +10 -0
- package/core/_layouts/blueprint/team/index.html +10 -0
- package/core/_layouts/blueprint/team/member.html +5 -0
- package/core/_layouts/blueprint/updates/index.html +10 -0
- package/core/_layouts/blueprint/updates/update.html +10 -0
- package/core/_layouts/core/root.html +45 -0
- package/core/_layouts/modules/utilities/redirect.html +129 -0
- package/core/css/_studio.scss +237 -0
- package/core/css/components/_index.scss +146 -0
- package/core/css/core/_.scss +0 -0
- package/core/css/core/_alert.scss +87 -0
- package/core/css/core/_animations.scss +343 -0
- package/core/css/core/_bindings.scss +116 -0
- package/core/css/core/_blog-search.scss +57 -0
- package/core/css/core/_chatsy.scss +61 -0
- package/core/css/core/_consent.scss +424 -0
- package/core/css/core/_custom-font-awesome.scss +146 -0
- package/core/css/core/_exit-popup.scss +42 -0
- package/core/css/core/_generated-badge.scss +33 -0
- package/core/css/core/_initialize.scss +29 -0
- package/core/css/core/_lazy-loading.scss +104 -0
- package/core/css/core/_social-sharing.scss +41 -0
- package/core/css/core/_tones.scss +32 -0
- package/core/css/core/_utilities.scss +157 -0
- package/core/css/main.scss +54 -0
- package/core/css/motion/_index.scss +368 -0
- package/core/css/pages/404/index.scss +0 -0
- package/core/css/pages/admin/calendar/index.scss +692 -0
- package/core/css/pages/admin/firebase/index.scss +33 -0
- package/core/css/pages/admin/index.scss +9 -0
- package/core/css/pages/admin/users/index.scss +9 -0
- package/core/css/pages/admin/verts/index.scss +9 -0
- package/core/css/pages/alternatives/[alternative]/index.scss +0 -0
- package/core/css/pages/blog/[slug].scss +55 -0
- package/core/css/pages/dashboard/account/index.scss +242 -0
- package/core/css/pages/download/index.scss +480 -0
- package/core/css/pages/extension/installed/index.scss +317 -0
- package/core/css/pages/feedback/index.scss +31 -0
- package/core/css/pages/payment/confirmation/index.scss +14 -0
- package/core/css/pages/pricing/index.scss +0 -0
- package/core/css/pages/status/index.scss +162 -0
- package/core/css/pages/test/libraries/layers/index.scss +28 -0
- package/core/css/pages/updates/[update].scss +46 -0
- package/core/css/pages/updates/index.scss +13 -0
- package/core/css/shell/_index.scss +207 -0
- package/core/css/tokens/_index.scss +166 -0
- package/core/icons/flags/ar.svg +1 -0
- package/core/icons/flags/bd.svg +1 -0
- package/core/icons/flags/ca.svg +1 -0
- package/core/icons/flags/ch.svg +1 -0
- package/core/icons/flags/cn.svg +1 -0
- package/core/icons/flags/co.svg +1 -0
- package/core/icons/flags/cw.svg +1 -0
- package/core/icons/flags/cz.svg +1 -0
- package/core/icons/flags/de.svg +1 -0
- package/core/icons/flags/dk.svg +1 -0
- package/core/icons/flags/eg.svg +1 -0
- package/core/icons/flags/es.svg +1 -0
- package/core/icons/flags/fi.svg +1 -0
- package/core/icons/flags/fr.svg +1 -0
- package/core/icons/flags/gb.svg +1 -0
- package/core/icons/flags/gq.svg +1 -0
- package/core/icons/flags/gr.svg +1 -0
- package/core/icons/flags/gt.svg +1 -0
- package/core/icons/flags/hn.svg +1 -0
- package/core/icons/flags/id.svg +1 -0
- package/core/icons/flags/ie.svg +1 -0
- package/core/icons/flags/in.svg +1 -0
- package/core/icons/flags/ir.svg +1 -0
- package/core/icons/flags/it.svg +1 -0
- package/core/icons/flags/jp.svg +1 -0
- package/core/icons/flags/kp.svg +1 -0
- package/core/icons/flags/kr.svg +1 -0
- package/core/icons/flags/ly.svg +1 -0
- package/core/icons/flags/ma.svg +1 -0
- package/core/icons/flags/mu.svg +1 -0
- package/core/icons/flags/mx.svg +1 -0
- package/core/icons/flags/ng.svg +1 -0
- package/core/icons/flags/pa.svg +1 -0
- package/core/icons/flags/ph.svg +1 -0
- package/core/icons/flags/pk.svg +1 -0
- package/core/icons/flags/pt.svg +1 -0
- package/core/icons/flags/ru.svg +1 -0
- package/core/icons/flags/sa.svg +1 -0
- package/core/icons/flags/sd.svg +1 -0
- package/core/icons/flags/se.svg +1 -0
- package/core/icons/flags/th.svg +1 -0
- package/core/icons/flags/tm.svg +1 -0
- package/core/icons/flags/tr.svg +1 -0
- package/core/icons/flags/us.svg +1 -0
- package/core/icons/flags/uy.svg +1 -0
- package/core/icons/flags/vn.svg +1 -0
- package/core/icons/flags/za.svg +1 -0
- package/core/images/exit-popup/subscriber-1.jpg +0 -0
- package/core/images/exit-popup/subscriber-2.jpg +0 -0
- package/core/images/exit-popup/subscriber-3.jpg +0 -0
- package/core/images/exit-popup/subscriber-4.jpg +0 -0
- package/core/images/placeholder/photo-1.jpg +0 -0
- package/core/images/placeholder/photo-2.jpg +0 -0
- package/core/images/placeholder/photo-3.jpg +0 -0
- package/core/js/core/analytics-loader.js +253 -0
- package/core/js/core/app-shell.js +136 -0
- package/core/js/core/appearance.js +195 -0
- package/core/js/core/auth.js +457 -0
- package/core/js/core/complete.js +72 -0
- package/core/js/core/consent.js +384 -0
- package/core/js/core/dev-icon-audit.js +63 -0
- package/core/js/core/dev-palette.js +670 -0
- package/core/js/core/dev-sections.js +53 -0
- package/core/js/core/exit-popup.js +276 -0
- package/core/js/core/initialize.js +8 -0
- package/core/js/core/language-switcher.js +167 -0
- package/core/js/core/lazy-loading.js +479 -0
- package/core/js/core/motion.js +28 -0
- package/core/js/core/query-strings.js +146 -0
- package/core/js/core/service-worker.js +28 -0
- package/core/js/core/social-sharing.js +293 -0
- package/core/js/first-paint.js +35 -0
- package/core/js/layouts/modules/utilities/redirect.js +122 -0
- package/core/js/libs/admin-helpers.js +62 -0
- package/core/js/libs/alert-dismiss.js +35 -0
- package/core/js/libs/analytics.js +368 -0
- package/core/js/libs/auth/email.js +144 -0
- package/core/js/libs/auth/errors.js +119 -0
- package/core/js/libs/auth/forms.js +206 -0
- package/core/js/libs/auth/index.js +181 -0
- package/core/js/libs/auth/oauth.js +394 -0
- package/core/js/libs/auth/orphan.js +110 -0
- package/core/js/libs/auth/password-toggle.js +44 -0
- package/core/js/libs/auth/session-params.js +266 -0
- package/core/js/libs/auth/tracking.js +41 -0
- package/core/js/libs/charts.js +516 -0
- package/core/js/libs/consent-region.js +127 -0
- package/core/js/libs/dev.js +150 -0
- package/core/js/libs/graph.js +201 -0
- package/core/js/libs/initialize-tooltips.js +31 -0
- package/core/js/libs/logger.js +38 -0
- package/core/js/libs/omega-copy.js +89 -0
- package/core/js/libs/path-prefix.js +44 -0
- package/core/js/libs/payment-config.js +76 -0
- package/core/js/libs/recaptcha.js +94 -0
- package/core/js/libs/sale-name.js +295 -0
- package/core/js/libs/showcase-frames.js +37 -0
- package/core/js/libs/srcset.js +69 -0
- package/core/js/libs/tracking-consent.js +161 -0
- package/core/js/main.js +144 -0
- package/core/js/pages/404/index.js +57 -0
- package/core/js/pages/admin/calendar/calendar-core.js +701 -0
- package/core/js/pages/admin/calendar/calendar-events.js +853 -0
- package/core/js/pages/admin/calendar/calendar-renderer.js +807 -0
- package/core/js/pages/admin/calendar/campaign-preview.js +105 -0
- package/core/js/pages/admin/calendar/index.js +40 -0
- package/core/js/pages/admin/firebase/index.js +727 -0
- package/core/js/pages/admin/index.js +531 -0
- package/core/js/pages/admin/posts/editor/index.js +230 -0
- package/core/js/pages/admin/posts/index.js +237 -0
- package/core/js/pages/admin/studio/index.js +538 -0
- package/core/js/pages/admin/users/index.js +669 -0
- package/core/js/pages/admin/users/new/index.js +65 -0
- package/core/js/pages/admin/verts/index.js +407 -0
- package/core/js/pages/alternatives/[alternative]/index.js +17 -0
- package/core/js/pages/app/index.js +135 -0
- package/core/js/pages/blog/[slug].js +132 -0
- package/core/js/pages/blog/_search.mjs +108 -0
- package/core/js/pages/blog/index.js +193 -0
- package/core/js/pages/blog/page/[num]/index.js +7 -0
- package/core/js/pages/connections/callback/index.js +148 -0
- package/core/js/pages/contact/index.js +170 -0
- package/core/js/pages/cookies.js +2 -0
- package/core/js/pages/dashboard/account/index.js +366 -0
- package/core/js/pages/dashboard/account/modules/orders.js +82 -0
- package/core/js/pages/dashboard/account/sections/api-keys.js +149 -0
- package/core/js/pages/dashboard/account/sections/billing.js +1667 -0
- package/core/js/pages/dashboard/account/sections/connections.js +333 -0
- package/core/js/pages/dashboard/account/sections/data-request.js +225 -0
- package/core/js/pages/dashboard/account/sections/delete.js +80 -0
- package/core/js/pages/dashboard/account/sections/notifications.js +167 -0
- package/core/js/pages/dashboard/account/sections/orders.js +151 -0
- package/core/js/pages/dashboard/account/sections/profile.js +200 -0
- package/core/js/pages/dashboard/account/sections/referrals.js +217 -0
- package/core/js/pages/dashboard/account/sections/refund.js +253 -0
- package/core/js/pages/dashboard/account/sections/security.js +726 -0
- package/core/js/pages/dashboard/account/sections/team.js +247 -0
- package/core/js/pages/download/index.js +418 -0
- package/core/js/pages/extension/index.js +164 -0
- package/core/js/pages/extension/installed/index.js +135 -0
- package/core/js/pages/feedback/index.js +193 -0
- package/core/js/pages/legal/_document.js +91 -0
- package/core/js/pages/payment/checkout/index.js +420 -0
- package/core/js/pages/payment/checkout/modules/api.js +139 -0
- package/core/js/pages/payment/checkout/modules/dev-section.js +175 -0
- package/core/js/pages/payment/checkout/modules/discount.js +69 -0
- package/core/js/pages/payment/checkout/modules/pricing.js +73 -0
- package/core/js/pages/payment/checkout/modules/state.js +339 -0
- package/core/js/pages/payment/checkout/modules/tracking.js +54 -0
- package/core/js/pages/payment/confirmation/index.js +91 -0
- package/core/js/pages/payment/confirmation/modules/celebration.js +45 -0
- package/core/js/pages/payment/confirmation/modules/state.js +112 -0
- package/core/js/pages/payment/confirmation/modules/tracking.js +133 -0
- package/core/js/pages/payment/confirmation/modules/verify.js +142 -0
- package/core/js/pages/portal/email-preferences/index.js +183 -0
- package/core/js/pages/pricing/index.js +425 -0
- package/core/js/pages/privacy.js +2 -0
- package/core/js/pages/reset/index.js +12 -0
- package/core/js/pages/signin/index.js +22 -0
- package/core/js/pages/signup/index.js +23 -0
- package/core/js/pages/status/index.js +687 -0
- package/core/js/pages/terms.js +2 -0
- package/core/js/pages/test/components/[category]/[name]/index.js +26 -0
- package/core/js/pages/test/libraries/appearance/index.js +220 -0
- package/core/js/pages/test/libraries/form-manager/index.js +328 -0
- package/core/js/pages/test/libraries/layers/index.js +16 -0
- package/core/js/pages/test/libraries/verts/index.js +68 -0
- package/core/js/pages/test/sections/[category]/[name]/index.js +28 -0
- package/core/js/pages/test/styleguide/index.js +35 -0
- package/core/js/pages/token/index.js +203 -0
- package/core/logos/brandmarks/original/apple-app-store.svg +1 -0
- package/core/logos/brandmarks/original/apple.svg +6 -0
- package/core/logos/brandmarks/original/bitbucket.svg +14 -0
- package/core/logos/brandmarks/original/bitcoin.svg +1 -0
- package/core/logos/brandmarks/original/bluesky.svg +7 -0
- package/core/logos/brandmarks/original/bootstrap.svg +43 -0
- package/core/logos/brandmarks/original/brave.svg +1 -0
- package/core/logos/brandmarks/original/chrome.svg +1 -0
- package/core/logos/brandmarks/original/discord.svg +1 -0
- package/core/logos/brandmarks/original/dropbox.svg +10 -0
- package/core/logos/brandmarks/original/edge.svg +1 -0
- package/core/logos/brandmarks/original/facebook-f.svg +1 -0
- package/core/logos/brandmarks/original/facebook.svg +8 -0
- package/core/logos/brandmarks/original/figma.svg +10 -0
- package/core/logos/brandmarks/original/firefox.svg +1 -0
- package/core/logos/brandmarks/original/github.svg +6 -0
- package/core/logos/brandmarks/original/gitlab.svg +17 -0
- package/core/logos/brandmarks/original/google-a-s.svg +1 -0
- package/core/logos/brandmarks/original/google-chrome-webstore.svg +49 -0
- package/core/logos/brandmarks/original/google-play-store.svg +1 -0
- package/core/logos/brandmarks/original/google-web-search.svg +1 -0
- package/core/logos/brandmarks/original/google.svg +1 -0
- package/core/logos/brandmarks/original/gulp.svg +4 -0
- package/core/logos/brandmarks/original/html-5.svg +1 -0
- package/core/logos/brandmarks/original/instagram.svg +1 -0
- package/core/logos/brandmarks/original/javascript.svg +1 -0
- package/core/logos/brandmarks/original/jira.svg +18 -0
- package/core/logos/brandmarks/original/kick.svg +7 -0
- package/core/logos/brandmarks/original/linkedin.svg +1 -0
- package/core/logos/brandmarks/original/linux.svg +28 -0
- package/core/logos/brandmarks/original/messenger.svg +16 -0
- package/core/logos/brandmarks/original/microsoft.svg +1 -0
- package/core/logos/brandmarks/original/npm.svg +8 -0
- package/core/logos/brandmarks/original/opera.svg +1 -0
- package/core/logos/brandmarks/original/patreon.svg +6 -0
- package/core/logos/brandmarks/original/pinterest.svg +6 -0
- package/core/logos/brandmarks/original/reddit.svg +1 -0
- package/core/logos/brandmarks/original/safari.svg +31 -0
- package/core/logos/brandmarks/original/sass.svg +3 -0
- package/core/logos/brandmarks/original/signal.svg +6 -0
- package/core/logos/brandmarks/original/skype.svg +47 -0
- package/core/logos/brandmarks/original/slack.svg +33 -0
- package/core/logos/brandmarks/original/soundcloud.svg +1 -0
- package/core/logos/brandmarks/original/spotify.svg +5 -0
- package/core/logos/brandmarks/original/steam.svg +6 -0
- package/core/logos/brandmarks/original/telegram.svg +14 -0
- package/core/logos/brandmarks/original/tiktok.svg +1 -0
- package/core/logos/brandmarks/original/trello.svg +16 -0
- package/core/logos/brandmarks/original/tumblr.svg +1 -0
- package/core/logos/brandmarks/original/twitch.svg +6 -0
- package/core/logos/brandmarks/original/twitter.svg +6 -0
- package/core/logos/brandmarks/original/unity.svg +6 -0
- package/core/logos/brandmarks/original/x.svg +7 -0
- package/core/logos/brandmarks/original/yahoo-y.svg +1 -0
- package/core/logos/brandmarks/original/youtube-square.svg +1 -0
- package/core/logos/brandmarks/original/youtube.svg +1 -0
- package/core/logos/combomarks/dark/airbnb.svg +30 -0
- package/core/logos/combomarks/dark/slack.svg +38 -0
- package/core/logos/combomarks/dark/spotify.svg +35 -0
- package/core/logos/combomarks/dark/twilio.svg +26 -0
- package/core/logos/combomarks/dark/weebly.svg +35 -0
- package/core/logos/combomarks/gray/airbnb.svg +33 -0
- package/core/logos/combomarks/gray/alphabet.svg +23 -0
- package/core/logos/combomarks/gray/amazon.svg +38 -0
- package/core/logos/combomarks/gray/boring-company.svg +91 -0
- package/core/logos/combomarks/gray/burberry.svg +239 -0
- package/core/logos/combomarks/gray/embark.svg +21 -0
- package/core/logos/combomarks/gray/fitbit.svg +61 -0
- package/core/logos/combomarks/gray/fred-perry.svg +94 -0
- package/core/logos/combomarks/gray/google.svg +27 -0
- package/core/logos/combomarks/gray/hollister.svg +34 -0
- package/core/logos/combomarks/gray/hubspot.svg +27 -0
- package/core/logos/combomarks/gray/kaplan.svg +33 -0
- package/core/logos/combomarks/gray/lenovo.svg +27 -0
- package/core/logos/combomarks/gray/levis.svg +25 -0
- package/core/logos/combomarks/gray/mapbox.svg +37 -0
- package/core/logos/combomarks/gray/netflix.svg +15 -0
- package/core/logos/combomarks/gray/new-balance.svg +32 -0
- package/core/logos/combomarks/gray/nike.svg +17 -0
- package/core/logos/combomarks/gray/paypal.svg +41 -0
- package/core/logos/combomarks/gray/puma.svg +82 -0
- package/core/logos/combomarks/gray/samsung.svg +26 -0
- package/core/logos/combomarks/gray/shopify.svg +36 -0
- package/core/logos/combomarks/gray/slack.svg +42 -0
- package/core/logos/combomarks/gray/spotify.svg +38 -0
- package/core/logos/combomarks/gray/stripe.svg +24 -0
- package/core/logos/combomarks/gray/tnf.svg +30 -0
- package/core/logos/combomarks/gray/twilio.svg +29 -0
- package/core/logos/combomarks/gray/uber.svg +24 -0
- package/core/logos/combomarks/gray/weebly.svg +38 -0
- package/core/logos/combomarks/original/airbnb.svg +33 -0
- package/core/logos/combomarks/original/alphabet.svg +23 -0
- package/core/logos/combomarks/original/amazon.svg +38 -0
- package/core/logos/combomarks/original/asana.svg +8 -0
- package/core/logos/combomarks/original/boring-company.svg +91 -0
- package/core/logos/combomarks/original/discord.svg +6 -0
- package/core/logos/combomarks/original/embark.svg +24 -0
- package/core/logos/combomarks/original/fitbit.svg +60 -0
- package/core/logos/combomarks/original/github.svg +6 -0
- package/core/logos/combomarks/original/gitlab.svg +20 -0
- package/core/logos/combomarks/original/google.svg +28 -0
- package/core/logos/combomarks/original/hubspot.svg +29 -0
- package/core/logos/combomarks/original/instagram.svg +7 -0
- package/core/logos/combomarks/original/kickstarter.svg +6 -0
- package/core/logos/combomarks/original/lenovo.svg +24 -0
- package/core/logos/combomarks/original/linkedin.svg +6 -0
- package/core/logos/combomarks/original/mapbox.svg +39 -0
- package/core/logos/combomarks/original/mastodon.svg +7 -0
- package/core/logos/combomarks/original/medium.svg +6 -0
- package/core/logos/combomarks/original/meta.svg +22 -0
- package/core/logos/combomarks/original/microsoft.svg +13 -0
- package/core/logos/combomarks/original/monday.svg +12 -0
- package/core/logos/combomarks/original/netflix.svg +15 -0
- package/core/logos/combomarks/original/notion.svg +9 -0
- package/core/logos/combomarks/original/paypal.svg +41 -0
- package/core/logos/combomarks/original/reddit.svg +13 -0
- package/core/logos/combomarks/original/samsung.svg +25 -0
- package/core/logos/combomarks/original/shopify.svg +35 -0
- package/core/logos/combomarks/original/slack.svg +47 -0
- package/core/logos/combomarks/original/spotify.svg +37 -0
- package/core/logos/combomarks/original/stackoverflow.svg +28 -0
- package/core/logos/combomarks/original/stripe.svg +24 -0
- package/core/logos/combomarks/original/tiktok.svg +12 -0
- package/core/logos/combomarks/original/tumblr.svg +6 -0
- package/core/logos/combomarks/original/uber.svg +24 -0
- package/core/logos/combomarks/original/weebly.svg +34 -0
- package/core/logos/combomarks/original/whatsapp.svg +20 -0
- package/core/logos/combomarks/original/youtube.svg +10 -0
- package/core/logos/combomarks/original/zoom.svg +7 -0
- package/core/logos/combomarks/white/airbnb.svg +33 -0
- package/core/logos/combomarks/white/alphabet.svg +23 -0
- package/core/logos/combomarks/white/amazon.svg +38 -0
- package/core/logos/combomarks/white/boring-company.svg +91 -0
- package/core/logos/combomarks/white/embark.svg +21 -0
- package/core/logos/combomarks/white/fitbit.svg +61 -0
- package/core/logos/combomarks/white/google.svg +27 -0
- package/core/logos/combomarks/white/hubspot.svg +27 -0
- package/core/logos/combomarks/white/kaplan.svg +33 -0
- package/core/logos/combomarks/white/lenovo.svg +27 -0
- package/core/logos/combomarks/white/mapbox.svg +37 -0
- package/core/logos/combomarks/white/netflix.svg +15 -0
- package/core/logos/combomarks/white/paypal.svg +40 -0
- package/core/logos/combomarks/white/samsung.svg +26 -0
- package/core/logos/combomarks/white/shopify.svg +36 -0
- package/core/logos/combomarks/white/slack.svg +42 -0
- package/core/logos/combomarks/white/spotify.svg +38 -0
- package/core/logos/combomarks/white/stripe.svg +24 -0
- package/core/logos/combomarks/white/twilio.svg +29 -0
- package/core/logos/combomarks/white/uber.svg +24 -0
- package/core/logos/combomarks/white/weebly.svg +38 -0
- package/defaults/pages/404.md +4 -0
- package/defaults/pages/about.md +4 -0
- package/defaults/pages/admin/calendar/index.html +4 -0
- package/defaults/pages/admin/firebase/index.html +4 -0
- package/defaults/pages/admin/index.md +4 -0
- package/defaults/pages/admin/posts/editor.html +4 -0
- package/defaults/pages/admin/posts/index.html +4 -0
- package/defaults/pages/admin/stackblitz/index.html +4 -0
- package/defaults/pages/admin/studio/index.html +4 -0
- package/defaults/pages/admin/users/index.html +4 -0
- package/defaults/pages/admin/users/new.html +4 -0
- package/defaults/pages/admin/verts/index.html +4 -0
- package/defaults/pages/ads-txt.html +11 -0
- package/defaults/pages/alternatives/index.md +4 -0
- package/defaults/pages/app.md +4 -0
- package/defaults/pages/auth/reset.md +4 -0
- package/defaults/pages/auth/signin.md +4 -0
- package/defaults/pages/auth/signup.md +4 -0
- package/defaults/pages/auth/token.md +4 -0
- package/defaults/pages/blog/categories/category.html +21 -0
- package/defaults/pages/blog/categories.md +4 -0
- package/defaults/pages/blog/index-json.html +27 -0
- package/defaults/pages/blog/tags/tag.html +21 -0
- package/defaults/pages/blog/tags.md +4 -0
- package/defaults/pages/blog.md +22 -0
- package/defaults/pages/careers.md +4 -0
- package/defaults/pages/connections/callback.md +4 -0
- package/defaults/pages/contact.md +4 -0
- package/defaults/pages/dashboard/account.md +4 -0
- package/defaults/pages/dashboard/index.md +12 -0
- package/defaults/pages/download.md +4 -0
- package/defaults/pages/extension/index.html +4 -0
- package/defaults/pages/extension/installed.html +4 -0
- package/defaults/pages/feedback.md +4 -0
- package/defaults/pages/feeds/posts-json.html +40 -0
- package/defaults/pages/feeds/posts-xml.html +38 -0
- package/defaults/pages/humans.html +25 -0
- package/defaults/pages/index.md +4 -0
- package/defaults/pages/legal/cookies.md +4 -0
- package/defaults/pages/legal/privacy.md +4 -0
- package/defaults/pages/legal/terms.md +4 -0
- package/defaults/pages/llms-txt.html +46 -0
- package/defaults/pages/opensearch.html +16 -0
- package/defaults/pages/pages-json.html +42 -0
- package/defaults/pages/payment/checkout.md +4 -0
- package/defaults/pages/payment/confirmation.md +4 -0
- package/defaults/pages/portal/email-preferences.md +4 -0
- package/defaults/pages/pricing.md +4 -0
- package/defaults/pages/redirects/account.md +12 -0
- package/defaults/pages/redirects/admin/dashboard.md +11 -0
- package/defaults/pages/redirects/auth/change-password.md +9 -0
- package/defaults/pages/redirects/auth/forgot.md +9 -0
- package/defaults/pages/redirects/auth/join.md +9 -0
- package/defaults/pages/redirects/auth/login.md +9 -0
- package/defaults/pages/redirects/auth/recover.md +9 -0
- package/defaults/pages/redirects/auth/register.md +9 -0
- package/defaults/pages/redirects/auth/reset-password.md +9 -0
- package/defaults/pages/redirects/billing/cancel.md +9 -0
- package/defaults/pages/redirects/billing/refund.md +9 -0
- package/defaults/pages/robots.html +36 -0
- package/defaults/pages/security-txt.html +11 -0
- package/defaults/pages/sitemap.html +37 -0
- package/defaults/pages/status.md +4 -0
- package/defaults/pages/team/index.md +4 -0
- package/defaults/pages/test/account/dashboard.html +16 -0
- package/defaults/pages/test/index.md +12 -0
- package/defaults/pages/test/libraries/appearance.html +268 -0
- package/defaults/pages/test/libraries/cover.html +50 -0
- package/defaults/pages/test/libraries/error.html +30 -0
- package/defaults/pages/test/libraries/firestore.html +202 -0
- package/defaults/pages/test/libraries/form-manager.html +374 -0
- package/defaults/pages/test/libraries/layers.html +52 -0
- package/defaults/pages/test/libraries/lazy-loading.html +450 -0
- package/defaults/pages/test/libraries/verts.html +149 -0
- package/defaults/pages/test/redirect/external.md +13 -0
- package/defaults/pages/test/redirect/internal.md +13 -0
- package/defaults/pages/test/styleguide/index.html +588 -0
- package/defaults/pages/test/translation/index.md +50 -0
- package/defaults/pages/updates/index.md +4 -0
- package/defaults/sample-posts/2026-01-12-welcome-to-the-blog.md +26 -0
- package/defaults/sample-posts/2026-02-03-a-field-guide-to-good-defaults.md +50 -0
- package/defaults/sample-posts/2026-02-24-shipping-week-notes.md +37 -0
- package/defaults/sample-posts/2026-03-17-the-case-for-one-config.md +36 -0
- package/defaults/sample-posts/2026-04-09-write-release-notes-people-read.md +34 -0
- package/defaults/sample-posts/2026-05-21-dark-mode-done-properly.md +38 -0
- package/defaults/sample-posts/2026-06-02-the-boring-deploy-is-the-good-deploy.md +30 -0
- package/defaults/sample-posts/2026-06-11-naming-things-a-survival-guide.md +30 -0
- package/defaults/sample-posts/2026-06-19-your-pricing-page-is-a-product.md +28 -0
- package/defaults/sample-posts/2026-06-30-email-is-an-api-with-feelings.md +28 -0
- package/defaults/sample-posts/2026-07-08-ship-the-docs-with-the-diff.md +30 -0
- package/defaults/sample-team/avery-quinn.md +22 -0
- package/defaults/sample-team/jordan-reyes.md +25 -0
- package/defaults/sample-team/riley-nakamura.md +22 -0
- package/defaults/sample-team/sam-okafor.md +22 -0
- package/defaults/sample-updates/v1.0.0.md +28 -0
- package/defaults/sample-updates/v1.1.0.md +27 -0
- package/defaults/sample-updates/v1.2.0.md +27 -0
- package/defaults/sample-updates/v1.3.0.md +28 -0
- package/defaults/showcase/components.html +62 -0
- package/defaults/showcase/entry.html +76 -0
- package/defaults/showcase/frame.html +62 -0
- package/defaults/showcase/index.html +59 -0
- package/dist/assets.js +1349 -0
- package/dist/brand-tokens.js +195 -0
- package/dist/build.js +192 -0
- package/dist/cachebreak-html.js +71 -0
- package/dist/cli-run.js +25 -0
- package/dist/cli.js +47 -0
- package/dist/collections.js +183 -0
- package/dist/commands/audit.js +470 -0
- package/dist/commands/build.js +197 -0
- package/dist/commands/clean.js +18 -0
- package/dist/commands/customize.js +125 -0
- package/dist/commands/deploy.js +441 -0
- package/dist/commands/dev.js +1067 -0
- package/dist/commands/install.js +31 -0
- package/dist/commands/lib/deploy-precheck.js +43 -0
- package/dist/commands/lib/ensure-target.js +115 -0
- package/dist/commands/migrate.js +86 -0
- package/dist/commands/purge.js +31 -0
- package/dist/commands/test.js +237 -0
- package/dist/commands/translate.js +57 -0
- package/dist/commands/update.js +22 -0
- package/dist/commands/version.js +11 -0
- package/dist/config-sections.js +245 -0
- package/dist/consumer-scan.js +118 -0
- package/dist/consumer.js +49 -0
- package/dist/customize.js +328 -0
- package/dist/decisions.js +153 -0
- package/dist/dist-audit.js +417 -0
- package/dist/dynamic-pages.js +357 -0
- package/dist/engine.js +1402 -0
- package/dist/firebase-auth-helpers.js +198 -0
- package/dist/font-metrics.js +420 -0
- package/dist/frontmatter-liquid.js +253 -0
- package/dist/github-secrets.js +80 -0
- package/dist/hero-animations.js +170 -0
- package/dist/imagemin.js +261 -0
- package/dist/index.js +41 -0
- package/dist/inline-icons.js +65 -0
- package/dist/language-flags.js +55 -0
- package/dist/layers.js +80 -0
- package/dist/layouts.js +44 -0
- package/dist/limit-collections.js +199 -0
- package/dist/link-resolver.js +303 -0
- package/dist/markdown-images.js +85 -0
- package/dist/merge.js +20 -0
- package/dist/migrate/codemod.js +172 -0
- package/dist/migrate/config-convert.js +448 -0
- package/dist/migrate/consumer-assets.js +130 -0
- package/dist/migrate/index.js +223 -0
- package/dist/migrate/lint.js +108 -0
- package/dist/migrate/rules.js +1929 -0
- package/dist/minify-html.js +148 -0
- package/dist/mode-helpers.js +96 -0
- package/dist/omega-bin.js +7 -0
- package/dist/overrides.js +190 -0
- package/dist/path-prefix.js +167 -0
- package/dist/paths.js +37 -0
- package/dist/pricing.js +301 -0
- package/dist/purge.js +95 -0
- package/dist/redirect-page.js +45 -0
- package/dist/sample-content.js +210 -0
- package/dist/scaffold.js +123 -0
- package/dist/sections.js +810 -0
- package/dist/service-worker.js +200 -0
- package/dist/social-pages.js +142 -0
- package/dist/srcset.js +67 -0
- package/dist/static-assets.js +154 -0
- package/dist/target-shortlinks.js +123 -0
- package/dist/theme-vocabulary.js +76 -0
- package/dist/translate/collect-text-nodes.js +93 -0
- package/dist/translate/default-routes.js +152 -0
- package/dist/translate/generate-defaults.js +297 -0
- package/dist/translate/index.js +568 -0
- package/dist/translate/packaged-defaults.js +127 -0
- package/dist/translate/sitemap.js +108 -0
- package/dist/vendor/account/engine.js +182 -0
- package/dist/vendor/account/features.js +220 -0
- package/dist/vendor/account/index.js +53 -0
- package/dist/vendor/account/schema.js +272 -0
- package/dist/vendor/account/subscription.js +38 -0
- package/dist/vendor/config/company.js +31 -0
- package/dist/vendor/config/defaults.js +173 -0
- package/dist/vendor/config/demo.js +18 -0
- package/dist/vendor/config/desktop-artifacts.js +110 -0
- package/dist/vendor/config/edit.js +769 -0
- package/dist/vendor/config/env-delivery.js +145 -0
- package/dist/vendor/config/env-rules.js +93 -0
- package/dist/vendor/config/env-schema.js +1078 -0
- package/dist/vendor/config/env.js +445 -0
- package/dist/vendor/config/hooks.js +97 -0
- package/dist/vendor/config/index.js +237 -0
- package/dist/vendor/config/instances.js +208 -0
- package/dist/vendor/config/load.js +490 -0
- package/dist/vendor/config/merge.js +68 -0
- package/dist/vendor/config/order.js +139 -0
- package/dist/vendor/config/ports.js +374 -0
- package/dist/vendor/config/providers.js +32 -0
- package/dist/vendor/config/repo.js +142 -0
- package/dist/vendor/config/retired-keys.js +430 -0
- package/dist/vendor/config/schema.js +1610 -0
- package/dist/vendor/config/secrets.js +50 -0
- package/dist/vendor/config/seed.js +34 -0
- package/dist/vendor/config/site-global.js +205 -0
- package/dist/vendor/config/validate.js +554 -0
- package/dist/vendor/config/winback.js +61 -0
- package/dist/vendor/devkit/actions-secrets.js +145 -0
- package/dist/vendor/devkit/attach-log-file.js +262 -0
- package/dist/vendor/devkit/bare-requires.js +153 -0
- package/dist/vendor/devkit/bundle.js +292 -0
- package/dist/vendor/devkit/ci-workflows.js +520 -0
- package/dist/vendor/devkit/cli-router.js +156 -0
- package/dist/vendor/devkit/command-path.js +46 -0
- package/dist/vendor/devkit/defaults-engine.js +372 -0
- package/dist/vendor/devkit/deploy-precheck.js +54 -0
- package/dist/vendor/devkit/deploy-record.js +180 -0
- package/dist/vendor/devkit/deploy.js +286 -0
- package/dist/vendor/devkit/env-watch.js +132 -0
- package/dist/vendor/devkit/framework-deps.js +46 -0
- package/dist/vendor/devkit/icons.js +178 -0
- package/dist/vendor/devkit/license.js +155 -0
- package/dist/vendor/devkit/local-https.js +360 -0
- package/dist/vendor/devkit/local.js +1905 -0
- package/dist/vendor/devkit/logger.js +128 -0
- package/dist/vendor/devkit/merge-line-files.js +296 -0
- package/dist/vendor/devkit/omega-bin.js +345 -0
- package/dist/vendor/devkit/reads.js +274 -0
- package/dist/vendor/devkit/safe-install.js +18 -0
- package/dist/vendor/devkit/scaffold-guard.js +96 -0
- package/dist/vendor/devkit/stop-signals.js +28 -0
- package/dist/vendor/devkit/strip-dev-blocks-plugin.js +29 -0
- package/dist/vendor/devkit/strip-dev-blocks.js +32 -0
- package/dist/vendor/devkit/target-secrets.js +165 -0
- package/dist/vendor/devkit/test/scope.js +162 -0
- package/dist/vendor/devkit/translate/cache.js +84 -0
- package/dist/vendor/devkit/translate/engine.js +243 -0
- package/dist/vendor/devkit/translate/index.js +49 -0
- package/dist/vendor/devkit/translate/languages.js +134 -0
- package/dist/vendor/devkit/translate/providers.js +188 -0
- package/dist/vendor/devkit/update.js +569 -0
- package/dist/vendor/template-kit/data/language-flags.js +54 -0
- package/dist/vendor/template-kit/data/languages.js +195 -0
- package/dist/vendor/template-kit/data/social-urls.js +26 -0
- package/dist/vendor/template-kit/filters.js +227 -0
- package/dist/vendor/template-kit/index.js +28 -0
- package/dist/vendor/template-kit/jekyll-compat.js +268 -0
- package/dist/vendor/template-kit/register-liquid.js +0 -0
- package/dist/vendor/template-kit/tags/collections.js +191 -0
- package/dist/vendor/template-kit/tags/conditionals.js +101 -0
- package/dist/vendor/template-kit/tags/content.js +175 -0
- package/dist/vendor/template-kit/tags/index.js +33 -0
- package/dist/vendor/template-kit/tags/media.js +376 -0
- package/dist/vendor/template-kit/variable-resolver.js +163 -0
- package/docs/ads-system.md +109 -0
- package/docs/classy-v2/DIRECTION.md +92 -0
- package/docs/classy-v2/draft-1.html +1932 -0
- package/docs/classy-v2/draft-2.html +1956 -0
- package/docs/frontmatter.md +153 -0
- package/docs/index.md +254 -0
- package/docs/libs.md +298 -0
- package/docs/omega-sections-spec.md +159 -0
- package/docs/page-contract.md +112 -0
- package/docs/sections.md +845 -0
- package/docs/shared/agent-docs.md +89 -0
- package/docs/shared/analytics.md +612 -0
- package/docs/shared/brands.md +51 -0
- package/docs/shared/breaking-changes.md +497 -0
- package/docs/shared/config.md +1387 -0
- package/docs/shared/deploys.md +215 -0
- package/docs/shared/icons.md +201 -0
- package/docs/shared/local-dev.md +147 -0
- package/docs/shared/logging.md +202 -0
- package/docs/shared/monitoring.md +153 -0
- package/docs/shared/publishing.md +183 -0
- package/docs/shared/rulings.md +34 -0
- package/docs/shared/testing.md +147 -0
- package/docs/shared/theming.md +604 -0
- package/docs/shared/translation.md +291 -0
- package/docs/shared/updates.md +61 -0
- package/docs/template-kit.md +128 -0
- package/docs/test-framework.md +163 -0
- package/package.json +122 -0
- package/runtime/boot.js +149 -0
- package/runtime/icons.js +77 -0
- package/runtime/manager.js +36 -0
- package/scaffold/.github/workflows/build.yml +139 -0
- package/scaffold/.nvmrc +1 -0
- package/scaffold/AGENTS.md +55 -0
- package/scaffold/CLAUDE.md +1 -0
- package/scaffold/_.gitattributes +4 -0
- package/scaffold/_.gitignore +27 -0
- package/scaffold/config/omega.json5 +20 -0
- package/scaffold/src/assets/css/pages/.gitkeep +0 -0
- package/scaffold/src/assets/images/.gitkeep +0 -0
- package/scaffold/src/assets/js/pages/.gitkeep +0 -0
- package/scaffold/src/pages/.gitkeep +0 -0
- package/scaffold/src/pages/example.md.txt +94 -0
- package/scaffold/src/service-worker.js +16 -0
- package/sw/entry.js +19 -0
- package/sw/manager.js +281 -0
- package/themes/_template/README.md +115 -0
- package/themes/_template/_config.scss +60 -0
- package/themes/_template/_theme.js +14 -0
- package/themes/_template/_theme.scss +28 -0
- package/themes/_template/css/base/_root.scss +19 -0
- package/themes/_template/css/components/_components.scss +23 -0
- package/themes/base/README.md +5 -0
- package/themes/base/_components/data/org-chart/component.html +33 -0
- package/themes/base/_components/data/org-chart/component.json5 +33 -0
- package/themes/base/_components/data/org-chart/component.scss +183 -0
- package/themes/base/_components/heading/masthead/component.html +22 -0
- package/themes/base/_components/heading/masthead/component.json5 +30 -0
- package/themes/base/_components/heading/section-head/component.html +18 -0
- package/themes/base/_components/heading/section-head/component.json5 +27 -0
- package/themes/base/_components/pricing/features/component.html +29 -0
- package/themes/base/_components/pricing/features/component.json5 +30 -0
- package/themes/base/_hero/orbit/index.html +30 -0
- package/themes/base/_hero/orbit/script.js +33 -0
- package/themes/base/_hero/orbit/style.scss +133 -0
- package/themes/base/_includes/admin/sections/sidebar.html +3 -0
- package/themes/base/_includes/admin/sections/topbar.html +3 -0
- package/themes/base/_includes/backend/sections/sidebar.html +3 -0
- package/themes/base/_includes/backend/sections/topbar.html +3 -0
- package/themes/base/_includes/frontend/components/connection-card.html +49 -0
- package/themes/base/_includes/frontend/components/post-card.html +54 -0
- package/themes/base/_includes/frontend/sections/account-connections.html +42 -0
- package/themes/base/_includes/frontend/sections/account-section-header.html +11 -0
- package/themes/base/_includes/frontend/sections/footer.html +154 -0
- package/themes/base/_includes/frontend/sections/nav.html +276 -0
- package/themes/base/_includes/global/sections/account.html +72 -0
- package/themes/base/_includes/global/sections/app-sidebar.html +248 -0
- package/themes/base/_includes/global/sections/app-topbar.html +228 -0
- package/themes/base/_includes/global/sections/page-header.html +99 -0
- package/themes/base/_layouts/admin/core/minimal-viewport-locked.html +22 -0
- package/themes/base/_layouts/admin/core/minimal.html +23 -0
- package/themes/base/_layouts/backend/core/base.html +57 -0
- package/themes/base/_layouts/backend/core/minimal-viewport-locked.html +32 -0
- package/themes/base/_layouts/backend/core/minimal.html +32 -0
- package/themes/base/_layouts/backend/pages/dashboard/index.html +230 -0
- package/themes/base/_layouts/frontend/core/base.html +31 -0
- package/themes/base/_layouts/frontend/core/cover.html +40 -0
- package/themes/base/_layouts/frontend/core/minimal.html +47 -0
- package/themes/base/_layouts/frontend/pages/404.html +54 -0
- package/themes/base/_layouts/frontend/pages/about.html +144 -0
- package/themes/base/_layouts/frontend/pages/account/index.html +2104 -0
- package/themes/base/_layouts/frontend/pages/alternatives/alternative.html +319 -0
- package/themes/base/_layouts/frontend/pages/alternatives/index.html +186 -0
- package/themes/base/_layouts/frontend/pages/app.html +142 -0
- package/themes/base/_layouts/frontend/pages/auth/reset.html +53 -0
- package/themes/base/_layouts/frontend/pages/auth/signin.html +90 -0
- package/themes/base/_layouts/frontend/pages/auth/signup.html +141 -0
- package/themes/base/_layouts/frontend/pages/auth/token.html +44 -0
- package/themes/base/_layouts/frontend/pages/blog/categories/category.html +88 -0
- package/themes/base/_layouts/frontend/pages/blog/categories/index.html +88 -0
- package/themes/base/_layouts/frontend/pages/blog/index.html +255 -0
- package/themes/base/_layouts/frontend/pages/blog/post.html +261 -0
- package/themes/base/_layouts/frontend/pages/blog/tags/index.html +84 -0
- package/themes/base/_layouts/frontend/pages/blog/tags/tag.html +88 -0
- package/themes/base/_layouts/frontend/pages/collection/category.html +72 -0
- package/themes/base/_layouts/frontend/pages/collection/document.html +54 -0
- package/themes/base/_layouts/frontend/pages/collection/index.html +84 -0
- package/themes/base/_layouts/frontend/pages/connections/callback.html +55 -0
- package/themes/base/_layouts/frontend/pages/contact.html +214 -0
- package/themes/base/_layouts/frontend/pages/download.html +793 -0
- package/themes/base/_layouts/frontend/pages/extension/index.html +253 -0
- package/themes/base/_layouts/frontend/pages/extension/installed.html +198 -0
- package/themes/base/_layouts/frontend/pages/feedback.html +171 -0
- package/themes/base/_layouts/frontend/pages/index.html +66 -0
- package/themes/base/_layouts/frontend/pages/legal/document.html +78 -0
- package/themes/base/_layouts/frontend/pages/payment/checkout.html +459 -0
- package/themes/base/_layouts/frontend/pages/payment/confirmation.html +131 -0
- package/themes/base/_layouts/frontend/pages/portal/email-preferences.html +104 -0
- package/themes/base/_layouts/frontend/pages/pricing.html +596 -0
- package/themes/base/_layouts/frontend/pages/status.html +361 -0
- package/themes/base/_layouts/frontend/pages/team/index.html +210 -0
- package/themes/base/_layouts/frontend/pages/team/member.html +125 -0
- package/themes/base/_layouts/frontend/pages/updates/index.html +119 -0
- package/themes/base/_layouts/frontend/pages/updates/update.html +118 -0
- package/themes/base/_sections/about/hero/section.html +68 -0
- package/themes/base/_sections/about/hero/section.json5 +58 -0
- package/themes/base/_sections/about/letter/section.html +60 -0
- package/themes/base/_sections/about/letter/section.json5 +56 -0
- package/themes/base/_sections/about/photo-band/section.html +24 -0
- package/themes/base/_sections/about/photo-band/section.json5 +23 -0
- package/themes/base/_sections/about/principles/section.html +17 -0
- package/themes/base/_sections/about/principles/section.json5 +34 -0
- package/themes/base/_sections/about/timeline/section.html +20 -0
- package/themes/base/_sections/about/timeline/section.json5 +34 -0
- package/themes/base/_sections/marketing/bento/section.html +91 -0
- package/themes/base/_sections/marketing/bento/section.json5 +152 -0
- package/themes/base/_sections/marketing/cta/section.html +44 -0
- package/themes/base/_sections/marketing/cta/section.json5 +47 -0
- package/themes/base/_sections/marketing/faq/section.html +52 -0
- package/themes/base/_sections/marketing/faq/section.json5 +68 -0
- package/themes/base/_sections/marketing/hero/section.html +349 -0
- package/themes/base/_sections/marketing/hero/section.json5 +335 -0
- package/themes/base/_sections/marketing/newsletter-cta/section.html +38 -0
- package/themes/base/_sections/marketing/newsletter-cta/section.js +86 -0
- package/themes/base/_sections/marketing/newsletter-cta/section.json5 +29 -0
- package/themes/base/_sections/marketing/pricing-cards/section.html +115 -0
- package/themes/base/_sections/marketing/pricing-cards/section.json5 +72 -0
- package/themes/base/_sections/marketing/product-demo/section.html +62 -0
- package/themes/base/_sections/marketing/product-demo/section.js +44 -0
- package/themes/base/_sections/marketing/product-demo/section.json5 +52 -0
- package/themes/base/_sections/marketing/prose/section.html +22 -0
- package/themes/base/_sections/marketing/prose/section.json5 +41 -0
- package/themes/base/_sections/marketing/showcase/section.html +35 -0
- package/themes/base/_sections/marketing/showcase/section.json5 +52 -0
- package/themes/base/_sections/marketing/stats/section.html +32 -0
- package/themes/base/_sections/marketing/stats/section.json5 +44 -0
- package/themes/base/_sections/marketing/testimonials/section.html +54 -0
- package/themes/base/_sections/marketing/testimonials/section.json5 +70 -0
- package/themes/base/_sections/marketing/trusted-by/section.html +30 -0
- package/themes/base/_sections/marketing/trusted-by/section.json5 +38 -0
- package/themes/base/_sections/verts/unit/section.html +7 -0
- package/themes/base/_sections/verts/unit/section.js +22 -0
- package/themes/base/_sections/verts/unit/section.json5 +24 -0
- package/themes/base/_sections/verts/unit/section.scss +24 -0
- package/themes/base/_theme.js +13 -0
- package/themes/base/_theme.scss +4 -0
- package/themes/bootstrap/_theme.js +16 -0
- package/themes/bootstrap/_theme.scss +11 -0
- package/themes/bootstrap/js/index.esm.js +19 -0
- package/themes/bootstrap/js/index.umd.js +34 -0
- package/themes/bootstrap/js/src/alert.js +87 -0
- package/themes/bootstrap/js/src/base-component.js +86 -0
- package/themes/bootstrap/js/src/button.js +72 -0
- package/themes/bootstrap/js/src/carousel.js +474 -0
- package/themes/bootstrap/js/src/collapse.js +297 -0
- package/themes/bootstrap/js/src/dom/data.js +55 -0
- package/themes/bootstrap/js/src/dom/event-handler.js +317 -0
- package/themes/bootstrap/js/src/dom/manipulator.js +71 -0
- package/themes/bootstrap/js/src/dom/selector-engine.js +126 -0
- package/themes/bootstrap/js/src/dropdown.js +458 -0
- package/themes/bootstrap/js/src/modal.js +378 -0
- package/themes/bootstrap/js/src/offcanvas.js +282 -0
- package/themes/bootstrap/js/src/popover.js +97 -0
- package/themes/bootstrap/js/src/scrollspy.js +296 -0
- package/themes/bootstrap/js/src/tab.js +315 -0
- package/themes/bootstrap/js/src/toast.js +224 -0
- package/themes/bootstrap/js/src/tooltip.js +632 -0
- package/themes/bootstrap/js/src/util/backdrop.js +151 -0
- package/themes/bootstrap/js/src/util/component-functions.js +35 -0
- package/themes/bootstrap/js/src/util/config.js +65 -0
- package/themes/bootstrap/js/src/util/focustrap.js +115 -0
- package/themes/bootstrap/js/src/util/index.js +306 -0
- package/themes/bootstrap/js/src/util/sanitizer.js +117 -0
- package/themes/bootstrap/js/src/util/scrollbar.js +114 -0
- package/themes/bootstrap/js/src/util/swipe.js +146 -0
- package/themes/bootstrap/js/src/util/template-factory.js +160 -0
- package/themes/bootstrap/overrides/_accordion.scss +21 -0
- package/themes/bootstrap/overrides/_avatars.scss +51 -0
- package/themes/bootstrap/overrides/_blog-images.scss +45 -0
- package/themes/bootstrap/overrides/_buttons-adaptive.scss +141 -0
- package/themes/bootstrap/overrides/_color-shades.scss +25 -0
- package/themes/bootstrap/overrides/_cursor-utilities.scss +13 -0
- package/themes/bootstrap/overrides/_index.scss +30 -0
- package/themes/bootstrap/overrides/_links.scss +26 -0
- package/themes/bootstrap/overrides/_soft-colors.scss +99 -0
- package/themes/bootstrap/overrides/_spacing-extended.scss +74 -0
- package/themes/bootstrap/overrides/_spinners.scss +22 -0
- package/themes/bootstrap/overrides/_typography-utilities.scss +40 -0
- package/themes/bootstrap/scss/_accordion.scss +153 -0
- package/themes/bootstrap/scss/_alert.scss +68 -0
- package/themes/bootstrap/scss/_badge.scss +38 -0
- package/themes/bootstrap/scss/_breadcrumb.scss +40 -0
- package/themes/bootstrap/scss/_button-group.scss +147 -0
- package/themes/bootstrap/scss/_buttons.scss +216 -0
- package/themes/bootstrap/scss/_card.scss +238 -0
- package/themes/bootstrap/scss/_carousel.scss +226 -0
- package/themes/bootstrap/scss/_close.scss +66 -0
- package/themes/bootstrap/scss/_containers.scss +41 -0
- package/themes/bootstrap/scss/_dropdown.scss +250 -0
- package/themes/bootstrap/scss/_forms.scss +9 -0
- package/themes/bootstrap/scss/_functions.scss +302 -0
- package/themes/bootstrap/scss/_grid.scss +39 -0
- package/themes/bootstrap/scss/_helpers.scss +12 -0
- package/themes/bootstrap/scss/_images.scss +42 -0
- package/themes/bootstrap/scss/_list-group.scss +199 -0
- package/themes/bootstrap/scss/_maps.scss +174 -0
- package/themes/bootstrap/scss/_mixins.scss +42 -0
- package/themes/bootstrap/scss/_modal.scss +240 -0
- package/themes/bootstrap/scss/_nav.scss +197 -0
- package/themes/bootstrap/scss/_navbar.scss +289 -0
- package/themes/bootstrap/scss/_offcanvas.scss +147 -0
- package/themes/bootstrap/scss/_pagination.scss +109 -0
- package/themes/bootstrap/scss/_placeholders.scss +51 -0
- package/themes/bootstrap/scss/_popover.scss +196 -0
- package/themes/bootstrap/scss/_progress.scss +68 -0
- package/themes/bootstrap/scss/_reboot.scss +611 -0
- package/themes/bootstrap/scss/_root.scss +187 -0
- package/themes/bootstrap/scss/_spinners.scss +85 -0
- package/themes/bootstrap/scss/_tables.scss +171 -0
- package/themes/bootstrap/scss/_toasts.scss +73 -0
- package/themes/bootstrap/scss/_tooltip.scss +119 -0
- package/themes/bootstrap/scss/_transitions.scss +27 -0
- package/themes/bootstrap/scss/_type.scss +106 -0
- package/themes/bootstrap/scss/_utilities.scss +806 -0
- package/themes/bootstrap/scss/_variables-dark.scss +102 -0
- package/themes/bootstrap/scss/_variables.scss +1753 -0
- package/themes/bootstrap/scss/bootstrap-grid.scss +62 -0
- package/themes/bootstrap/scss/bootstrap-reboot.scss +10 -0
- package/themes/bootstrap/scss/bootstrap-utilities.scss +19 -0
- package/themes/bootstrap/scss/bootstrap.scss +52 -0
- package/themes/bootstrap/scss/forms/_floating-labels.scss +97 -0
- package/themes/bootstrap/scss/forms/_form-check.scss +189 -0
- package/themes/bootstrap/scss/forms/_form-control.scss +214 -0
- package/themes/bootstrap/scss/forms/_form-range.scss +91 -0
- package/themes/bootstrap/scss/forms/_form-select.scss +80 -0
- package/themes/bootstrap/scss/forms/_form-text.scss +11 -0
- package/themes/bootstrap/scss/forms/_input-group.scss +132 -0
- package/themes/bootstrap/scss/forms/_labels.scss +36 -0
- package/themes/bootstrap/scss/forms/_validation.scss +12 -0
- package/themes/bootstrap/scss/helpers/_clearfix.scss +3 -0
- package/themes/bootstrap/scss/helpers/_color-bg.scss +7 -0
- package/themes/bootstrap/scss/helpers/_colored-links.scss +30 -0
- package/themes/bootstrap/scss/helpers/_focus-ring.scss +5 -0
- package/themes/bootstrap/scss/helpers/_icon-link.scss +25 -0
- package/themes/bootstrap/scss/helpers/_position.scss +36 -0
- package/themes/bootstrap/scss/helpers/_ratio.scss +26 -0
- package/themes/bootstrap/scss/helpers/_stacks.scss +15 -0
- package/themes/bootstrap/scss/helpers/_stretched-link.scss +15 -0
- package/themes/bootstrap/scss/helpers/_text-truncation.scss +7 -0
- package/themes/bootstrap/scss/helpers/_visually-hidden.scss +8 -0
- package/themes/bootstrap/scss/helpers/_vr.scss +8 -0
- package/themes/bootstrap/scss/mixins/_alert.scss +18 -0
- package/themes/bootstrap/scss/mixins/_backdrop.scss +14 -0
- package/themes/bootstrap/scss/mixins/_banner.scss +7 -0
- package/themes/bootstrap/scss/mixins/_border-radius.scss +78 -0
- package/themes/bootstrap/scss/mixins/_box-shadow.scss +18 -0
- package/themes/bootstrap/scss/mixins/_breakpoints.scss +127 -0
- package/themes/bootstrap/scss/mixins/_buttons.scss +70 -0
- package/themes/bootstrap/scss/mixins/_caret.scss +69 -0
- package/themes/bootstrap/scss/mixins/_clearfix.scss +9 -0
- package/themes/bootstrap/scss/mixins/_color-mode.scss +21 -0
- package/themes/bootstrap/scss/mixins/_color-scheme.scss +7 -0
- package/themes/bootstrap/scss/mixins/_container.scss +11 -0
- package/themes/bootstrap/scss/mixins/_deprecate.scss +10 -0
- package/themes/bootstrap/scss/mixins/_forms.scss +163 -0
- package/themes/bootstrap/scss/mixins/_gradients.scss +47 -0
- package/themes/bootstrap/scss/mixins/_grid.scss +151 -0
- package/themes/bootstrap/scss/mixins/_image.scss +16 -0
- package/themes/bootstrap/scss/mixins/_list-group.scss +26 -0
- package/themes/bootstrap/scss/mixins/_lists.scss +7 -0
- package/themes/bootstrap/scss/mixins/_pagination.scss +10 -0
- package/themes/bootstrap/scss/mixins/_reset-text.scss +17 -0
- package/themes/bootstrap/scss/mixins/_resize.scss +6 -0
- package/themes/bootstrap/scss/mixins/_table-variants.scss +24 -0
- package/themes/bootstrap/scss/mixins/_text-truncate.scss +8 -0
- package/themes/bootstrap/scss/mixins/_transition.scss +26 -0
- package/themes/bootstrap/scss/mixins/_utilities.scss +97 -0
- package/themes/bootstrap/scss/mixins/_visually-hidden.scss +38 -0
- package/themes/bootstrap/scss/tests/jasmine.js +16 -0
- package/themes/bootstrap/scss/tests/mixins/_auto-import-of-variables-dark.test.scss +7 -0
- package/themes/bootstrap/scss/tests/mixins/_color-modes.test.scss +69 -0
- package/themes/bootstrap/scss/tests/mixins/_media-query-color-mode-full.test.scss +8 -0
- package/themes/bootstrap/scss/tests/mixins/_utilities.test.scss +393 -0
- package/themes/bootstrap/scss/tests/sass-true/register.js +14 -0
- package/themes/bootstrap/scss/tests/sass-true/runner.js +17 -0
- package/themes/bootstrap/scss/tests/utilities/_api.test.scss +75 -0
- package/themes/bootstrap/scss/utilities/_api.scss +47 -0
- package/themes/bootstrap/scss/vendor/_rfs.scss +348 -0
- package/themes/classy/README.md +89 -0
- package/themes/classy/_config.scss +245 -0
- package/themes/classy/_theme.js +31 -0
- package/themes/classy/_theme.scss +57 -0
- package/themes/classy/css/app/_panels.scss +183 -0
- package/themes/classy/css/base/_fonts.scss +79 -0
- package/themes/classy/css/base/_no-raise.scss +49 -0
- package/themes/classy/css/base/_root.scss +113 -0
- package/themes/classy/css/base/_typography.scss +154 -0
- package/themes/classy/css/base/_utilities.scss +284 -0
- package/themes/classy/css/components/_alerts.scss +51 -0
- package/themes/classy/css/components/_badges.scss +46 -0
- package/themes/classy/css/components/_buttons.scss +175 -0
- package/themes/classy/css/components/_cards.scss +41 -0
- package/themes/classy/css/components/_dropdowns.scss +48 -0
- package/themes/classy/css/components/_forms.scss +171 -0
- package/themes/classy/css/components/_receipt.scss +45 -0
- package/themes/classy/css/layout/_footer.scss +298 -0
- package/themes/classy/css/layout/_general.scss +209 -0
- package/themes/classy/css/layout/_nav.scss +154 -0
- package/themes/classy/css/layout/_shell.scss +458 -0
- package/themes/classy/css/marketing/_bento.scss +238 -0
- package/themes/classy/css/marketing/_content.scss +1328 -0
- package/themes/classy/css/marketing/_downloads.scss +246 -0
- package/themes/classy/css/marketing/_hero.scss +311 -0
- package/themes/classy/css/marketing/_sections.scss +649 -0
- package/themes/classy/css/pages/404/index.scss +67 -0
- package/themes/classy/css/pages/_auth.scss +235 -0
- package/themes/classy/css/pages/admin/firebase/index.scss +22 -0
- package/themes/classy/css/pages/blog/[slug].scss +56 -0
- package/themes/classy/css/pages/cookies.scss +2 -0
- package/themes/classy/css/pages/dashboard/account/index.scss +31 -0
- package/themes/classy/css/pages/feedback/index.scss +39 -0
- package/themes/classy/css/pages/legal/_document.scss +192 -0
- package/themes/classy/css/pages/payment/checkout/index.scss +585 -0
- package/themes/classy/css/pages/payment/confirmation/index.scss +9 -0
- package/themes/classy/css/pages/privacy.scss +2 -0
- package/themes/classy/css/pages/status/index.scss +164 -0
- package/themes/classy/css/pages/terms.scss +2 -0
- package/themes/classy/css/pages/updates/index.scss +128 -0
- package/themes/classy/fonts/inter-italic-latin-ext.woff2 +0 -0
- package/themes/classy/fonts/inter-italic-latin.woff2 +0 -0
- package/themes/classy/fonts/inter-normal-latin-ext.woff2 +0 -0
- package/themes/classy/fonts/inter-normal-latin.woff2 +0 -0
- package/themes/classy/fonts/newsreader-italic-latin-ext.woff2 +0 -0
- package/themes/classy/fonts/newsreader-italic-latin.woff2 +0 -0
- package/themes/classy/fonts/newsreader-normal-latin-ext.woff2 +0 -0
- package/themes/classy/fonts/newsreader-normal-latin.woff2 +0 -0
- package/themes/classy/js/hero-demo-form.js +56 -0
- package/themes/neobrutalism/README.md +118 -0
- package/themes/neobrutalism/_config.scss +139 -0
- package/themes/neobrutalism/_layouts/frontend/core/base.html +30 -0
- package/themes/neobrutalism/_layouts/frontend/pages/index.html +275 -0
- package/themes/neobrutalism/_theme.js +24 -0
- package/themes/neobrutalism/_theme.scss +61 -0
- package/themes/neobrutalism/css/base/_mixins.scss +46 -0
- package/themes/neobrutalism/css/base/_root.scss +169 -0
- package/themes/neobrutalism/css/base/_typography.scss +61 -0
- package/themes/neobrutalism/css/base/_utilities.scss +89 -0
- package/themes/neobrutalism/css/components/_billing-toggle.scss +74 -0
- package/themes/neobrutalism/css/components/_buttons.scss +148 -0
- package/themes/neobrutalism/css/components/_cards.scss +47 -0
- package/themes/neobrutalism/css/components/_forms.scss +88 -0
- package/themes/neobrutalism/css/components/_marketing.scss +244 -0
- package/themes/neobrutalism/css/layout/_general.scss +213 -0
- package/themes/neobrutalism/css/layout/_navigation.scss +171 -0
- package/themes/neobrutalism/css/pages/index.scss +203 -0
- package/themes/neobrutalism/css/pages/pricing/index.scss +58 -0
- package/themes/neobrutalism/css/pages/test/libraries/layers/index.scss +7 -0
- package/themes/neobrutalism/js/pages/test/libraries/layers/index.js +9 -0
- package/themes/newsflash/README.md +117 -0
- package/themes/newsflash/_components/heading/lede/component.html +14 -0
- package/themes/newsflash/_components/heading/lede/component.json5 +23 -0
- package/themes/newsflash/_components/heading/rule-head/component.html +17 -0
- package/themes/newsflash/_components/heading/rule-head/component.json5 +23 -0
- package/themes/newsflash/_components/news/byline/component.html +12 -0
- package/themes/newsflash/_components/news/byline/component.json5 +18 -0
- package/themes/newsflash/_components/news/story-card/component.html +25 -0
- package/themes/newsflash/_components/news/story-card/component.json5 +21 -0
- package/themes/newsflash/_config.scss +147 -0
- package/themes/newsflash/_layouts/frontend/core/base.html +51 -0
- package/themes/newsflash/_layouts/frontend/pages/blog/index.html +299 -0
- package/themes/newsflash/_layouts/frontend/pages/blog/post.html +235 -0
- package/themes/newsflash/_layouts/frontend/pages/index.html +328 -0
- package/themes/newsflash/_sections/marketing/desks/section.html +39 -0
- package/themes/newsflash/_sections/marketing/desks/section.json5 +49 -0
- package/themes/newsflash/_sections/marketing/newsletter-cta/section.html +103 -0
- package/themes/newsflash/_sections/marketing/newsletter-cta/section.json5 +50 -0
- package/themes/newsflash/_sections/marketing/rundown/section.html +28 -0
- package/themes/newsflash/_sections/marketing/rundown/section.json5 +40 -0
- package/themes/newsflash/_theme.js +24 -0
- package/themes/newsflash/_theme.scss +70 -0
- package/themes/newsflash/css/base/_fonts.scss +80 -0
- package/themes/newsflash/css/base/_mixins.scss +51 -0
- package/themes/newsflash/css/base/_root.scss +257 -0
- package/themes/newsflash/css/base/_typography.scss +49 -0
- package/themes/newsflash/css/base/_utilities.scss +122 -0
- package/themes/newsflash/css/components/_badges.scss +65 -0
- package/themes/newsflash/css/components/_billing-toggle.scss +61 -0
- package/themes/newsflash/css/components/_buttons.scss +139 -0
- package/themes/newsflash/css/components/_cards.scss +47 -0
- package/themes/newsflash/css/components/_editorial.scss +173 -0
- package/themes/newsflash/css/components/_forms.scss +75 -0
- package/themes/newsflash/css/components/_infinite-scroll.scss +102 -0
- package/themes/newsflash/css/components/_panels.scss +156 -0
- package/themes/newsflash/css/components/_ticker.scss +67 -0
- package/themes/newsflash/css/layout/_general.scss +330 -0
- package/themes/newsflash/css/layout/_navigation.scss +168 -0
- package/themes/newsflash/css/pages/404/index.scss +79 -0
- package/themes/newsflash/css/pages/blog/[slug].scss +186 -0
- package/themes/newsflash/css/pages/blog/index.scss +17 -0
- package/themes/newsflash/css/pages/index.scss +157 -0
- package/themes/newsflash/css/pages/pricing/index.scss +55 -0
- package/themes/newsflash/css/pages/test/libraries/layers/index.scss +7 -0
- package/themes/newsflash/fonts/fraunces-italic-latin-ext.woff2 +0 -0
- package/themes/newsflash/fonts/fraunces-italic-latin.woff2 +0 -0
- package/themes/newsflash/fonts/fraunces-normal-latin-ext.woff2 +0 -0
- package/themes/newsflash/fonts/fraunces-normal-latin.woff2 +0 -0
- package/themes/newsflash/fonts/schibsted-grotesk-italic-latin-ext.woff2 +0 -0
- package/themes/newsflash/fonts/schibsted-grotesk-italic-latin.woff2 +0 -0
- package/themes/newsflash/fonts/schibsted-grotesk-normal-latin-ext.woff2 +0 -0
- package/themes/newsflash/fonts/schibsted-grotesk-normal-latin.woff2 +0 -0
- package/themes/newsflash/js/pages/blog/[slug].js +29 -0
- package/themes/newsflash/js/pages/test/libraries/layers/index.js +9 -0
- package/translations/es/pages/404.json +86 -0
- package/translations/es/pages/account.json +36 -0
- package/translations/es/pages/app.json +44 -0
- package/translations/es/pages/cancel.json +36 -0
- package/translations/es/pages/change-password.json +36 -0
- package/translations/es/pages/connections/callback.json +41 -0
- package/translations/es/pages/dashboard/account.json +475 -0
- package/translations/es/pages/dashboard.json +36 -0
- package/translations/es/pages/forgot.json +36 -0
- package/translations/es/pages/join.json +36 -0
- package/translations/es/pages/login.json +36 -0
- package/translations/es/pages/payment/checkout.json +83 -0
- package/translations/es/pages/payment/confirmation.json +62 -0
- package/translations/es/pages/portal/email-preferences.json +46 -0
- package/translations/es/pages/recover.json +36 -0
- package/translations/es/pages/refund.json +36 -0
- package/translations/es/pages/register.json +36 -0
- package/translations/es/pages/reset-password.json +36 -0
- package/translations/es/pages/reset.json +42 -0
- package/translations/es/pages/signin.json +45 -0
- package/translations/es/pages/signup.json +60 -0
- package/translations/es/pages/token.json +39 -0
- package/translations/fa/pages/404.json +86 -0
- package/translations/fa/pages/account.json +36 -0
- package/translations/fa/pages/app.json +44 -0
- package/translations/fa/pages/cancel.json +36 -0
- package/translations/fa/pages/change-password.json +36 -0
- package/translations/fa/pages/connections/callback.json +41 -0
- package/translations/fa/pages/dashboard/account.json +475 -0
- package/translations/fa/pages/dashboard.json +36 -0
- package/translations/fa/pages/forgot.json +36 -0
- package/translations/fa/pages/join.json +36 -0
- package/translations/fa/pages/login.json +36 -0
- package/translations/fa/pages/payment/checkout.json +83 -0
- package/translations/fa/pages/payment/confirmation.json +62 -0
- package/translations/fa/pages/portal/email-preferences.json +46 -0
- package/translations/fa/pages/recover.json +36 -0
- package/translations/fa/pages/refund.json +36 -0
- package/translations/fa/pages/register.json +36 -0
- package/translations/fa/pages/reset-password.json +36 -0
- package/translations/fa/pages/reset.json +42 -0
- package/translations/fa/pages/signin.json +45 -0
- package/translations/fa/pages/signup.json +60 -0
- package/translations/fa/pages/token.json +39 -0
|
@@ -0,0 +1,1905 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-development linking for the OMEGA monorepo (master plan §8).
|
|
3
|
+
*
|
|
4
|
+
* The single home for every "work against the local framework source" mechanic:
|
|
5
|
+
* - resolveMonorepoRoot() — find the Omega monorepo on this machine
|
|
6
|
+
* - findBrandRoot() — walk up from a cwd to the brand repo root
|
|
7
|
+
* - discoverTargets() — list the brand's target directories
|
|
8
|
+
* - frameworkPackagesOf() — which @omega.js packages a target depends on
|
|
9
|
+
* - linkLocalPackages() — file:-install those from the monorepo (idempotent)
|
|
10
|
+
* - startMonorepoWatch() — spawn the monorepo's src→dist watch (`npm start`)
|
|
11
|
+
* - acquireWatchLock() / releaseWatchLock() — single-instance guard for the watch
|
|
12
|
+
* - startVendorPropagation() — re-prepare dist-building frameworks when a
|
|
13
|
+
* vendored shared package (devkit, config, account) changes
|
|
14
|
+
* - ensureFreshLocalDist() / freshnessBoot() — check a locally-linked
|
|
15
|
+
* framework's dist (and its @omega.js/* runtime deps') at CLI boot: a link
|
|
16
|
+
* into the monorepo is read-only and stops the boot loudly, any other local
|
|
17
|
+
* checkout is rebuilt and the invocation re-execs once
|
|
18
|
+
*
|
|
19
|
+
* Consumers: `omega dev --local` (@omega.js/web), `mgr i local`
|
|
20
|
+
* (@omega.js/backend, @omega.js/desktop, @omega.js/extension), and the monorepo's
|
|
21
|
+
* own scripts/watch-all.js (lock helpers + vendor propagation).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// Libraries
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const os = require('os');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const { spawn, spawnSync } = require('child_process');
|
|
29
|
+
const { createRequire } = require('module');
|
|
30
|
+
const { safeInstall } = require('./safe-install');
|
|
31
|
+
const Logger = require('./logger');
|
|
32
|
+
|
|
33
|
+
// Constants
|
|
34
|
+
const SCOPE = '@omega.js/';
|
|
35
|
+
const DEFAULT_MONOREPO = path.join(os.homedir(), 'Developer', 'Repositories', 'Omega', 'omega');
|
|
36
|
+
const WATCH_LOCK = path.join('.omega', 'dev-watch.lock');
|
|
37
|
+
// The marker a vendor-propagation pass holds while it re-prepares frameworks
|
|
38
|
+
// (#622) — the same pid-stamped shape as the watch lock, so an abandoned one
|
|
39
|
+
// reads as idle.
|
|
40
|
+
const VENDOR_PROPAGATION_LOCK = path.join('.omega', 'vendor-propagation.lock');
|
|
41
|
+
// Where scripts/watch-all.js tees its output (#197). A boot that did NOT spawn
|
|
42
|
+
// the watch reads its state from here — the file is truncated per launch and
|
|
43
|
+
// its header names the pid that owns it.
|
|
44
|
+
const WATCH_LOG = path.join('.temp', 'logs', 'watch-all.log');
|
|
45
|
+
const WATCH_LOG_HEADER = /^# omega log — \S+ — pid=(\d+)$/;
|
|
46
|
+
const WATCH_LOG_POLL_MS = 200;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check whether a directory is the Omega monorepo root (package.json named
|
|
50
|
+
* "omega" and a packages/devkit workspace).
|
|
51
|
+
* @param {string} dir - Directory to test.
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
function isMonorepoRoot(dir) {
|
|
55
|
+
try {
|
|
56
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
57
|
+
return pkg.name === 'omega' && fs.existsSync(path.join(dir, 'packages', 'devkit', 'package.json'));
|
|
58
|
+
} catch (e) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the Omega monorepo root on this machine.
|
|
65
|
+
*
|
|
66
|
+
* Order: OMEGA_MONOREPO env override → self-location (walk up from this file's
|
|
67
|
+
* REAL path — covers the workspace copy at <root>/packages/devkit/src, copies
|
|
68
|
+
* vendored into a linked framework's dist/vendor/devkit, and consumers living
|
|
69
|
+
* inside the monorepo; a registry install outside it finds no root and falls
|
|
70
|
+
* through) → the conventional clone location.
|
|
71
|
+
* @returns {string} Absolute monorepo root path.
|
|
72
|
+
* @throws {Error} When no candidate is a valid monorepo root.
|
|
73
|
+
*/
|
|
74
|
+
function resolveMonorepoRoot() {
|
|
75
|
+
const fromEnv = process.env.OMEGA_MONOREPO;
|
|
76
|
+
if (fromEnv) {
|
|
77
|
+
if (!isMonorepoRoot(fromEnv)) {
|
|
78
|
+
throw new Error(`OMEGA_MONOREPO is set but is not the Omega monorepo root: ${fromEnv}`);
|
|
79
|
+
}
|
|
80
|
+
return path.resolve(fromEnv);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let dir = fs.realpathSync(__dirname);
|
|
84
|
+
while (true) {
|
|
85
|
+
if (isMonorepoRoot(dir)) {
|
|
86
|
+
return dir;
|
|
87
|
+
}
|
|
88
|
+
const parent = path.dirname(dir);
|
|
89
|
+
if (parent === dir) {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
dir = parent;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (isMonorepoRoot(DEFAULT_MONOREPO)) {
|
|
96
|
+
return DEFAULT_MONOREPO;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Could not find the Omega monorepo (looked for ${DEFAULT_MONOREPO}). `
|
|
101
|
+
+ `Clone it there or set OMEGA_MONOREPO to its root.`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Map an @omega.js package name to its monorepo package directory.
|
|
107
|
+
* @param {string} monorepoRoot - Monorepo root path.
|
|
108
|
+
* @param {string} name - Package name (e.g. '@omega.js/client').
|
|
109
|
+
* @returns {string} Absolute path to packages/<short-name>.
|
|
110
|
+
*/
|
|
111
|
+
function packageDir(monorepoRoot, name) {
|
|
112
|
+
return path.join(monorepoRoot, 'packages', name.slice(SCOPE.length));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Check whether a directory contains at least one targets/<name>/package.json.
|
|
117
|
+
* @param {string} dir - Directory to test.
|
|
118
|
+
* @returns {boolean}
|
|
119
|
+
*/
|
|
120
|
+
function hasTargets(dir) {
|
|
121
|
+
const targetsDir = path.join(dir, 'targets');
|
|
122
|
+
try {
|
|
123
|
+
return fs.readdirSync(targetsDir).some((entry) => fs.existsSync(path.join(targetsDir, entry, 'package.json')));
|
|
124
|
+
} catch (e) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Walk up from a starting directory to the brand repo root: the first
|
|
131
|
+
* directory with BOTH a package.json and a targets/<name>/package.json (a brand
|
|
132
|
+
* monorepo). The Omega monorepo itself is never a brand root — a standalone
|
|
133
|
+
* project living inside it (spike, fixture) resolves to itself. Falls back to the
|
|
134
|
+
* nearest package.json (a standalone project), then to the starting directory.
|
|
135
|
+
* @param {string} startDir - Directory to walk up from (usually process.cwd()).
|
|
136
|
+
* @returns {string} Absolute brand root path.
|
|
137
|
+
*/
|
|
138
|
+
function findBrandRoot(startDir) {
|
|
139
|
+
let dir = path.resolve(startDir);
|
|
140
|
+
let nearestPackage = null;
|
|
141
|
+
|
|
142
|
+
while (true) {
|
|
143
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) {
|
|
144
|
+
nearestPackage = nearestPackage || dir;
|
|
145
|
+
if (hasTargets(dir) && !isMonorepoRoot(dir)) {
|
|
146
|
+
return dir;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const parent = path.dirname(dir);
|
|
150
|
+
if (parent === dir) {
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
dir = parent;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return nearestPackage || path.resolve(startDir);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* List a brand's target directories: the brand root itself plus every
|
|
161
|
+
* targets/<name> that has a package.json.
|
|
162
|
+
* @param {string} brandRoot - Brand root path.
|
|
163
|
+
* @returns {string[]} Absolute target directory paths.
|
|
164
|
+
*/
|
|
165
|
+
function discoverTargets(brandRoot) {
|
|
166
|
+
const targets = [brandRoot];
|
|
167
|
+
const targetsDir = path.join(brandRoot, 'targets');
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
for (const entry of fs.readdirSync(targetsDir).sort()) {
|
|
171
|
+
const targetDir = path.join(targetsDir, entry);
|
|
172
|
+
if (fs.existsSync(path.join(targetDir, 'package.json'))) {
|
|
173
|
+
targets.push(targetDir);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch (e) {
|
|
177
|
+
// No targets/ directory — standalone brand, the root is the only target
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return targets;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Collect a target's @omega.js dependencies from its package.json (the ONE
|
|
185
|
+
* manifest at the target root — scripts + runtime deps; src/dist pillar).
|
|
186
|
+
* @param {string} targetDir - Target directory.
|
|
187
|
+
* @returns {Array<{name: string, spec: string, dev: boolean, dir: string}>}
|
|
188
|
+
*/
|
|
189
|
+
function frameworkPackagesOf(targetDir) {
|
|
190
|
+
const entries = [];
|
|
191
|
+
|
|
192
|
+
let pkg;
|
|
193
|
+
try {
|
|
194
|
+
pkg = JSON.parse(fs.readFileSync(path.join(targetDir, 'package.json'), 'utf8'));
|
|
195
|
+
} catch (e) {
|
|
196
|
+
return entries;
|
|
197
|
+
}
|
|
198
|
+
for (const [depKey, dev] of [['dependencies', false], ['devDependencies', true]]) {
|
|
199
|
+
for (const [name, spec] of Object.entries(pkg[depKey] || {})) {
|
|
200
|
+
if (name.startsWith(SCOPE)) {
|
|
201
|
+
entries.push({ name, spec, dev, dir: targetDir });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return entries;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Check whether a dependency is already the monorepo copy: the FIRST
|
|
211
|
+
* node_modules entry walking up from dir (mirroring Node resolution — npm
|
|
212
|
+
* workspaces hoist installs to the brand root) resolves to the target package
|
|
213
|
+
* directory.
|
|
214
|
+
* @param {string} dir - Directory the dependency is declared in.
|
|
215
|
+
* @param {string} name - Package name.
|
|
216
|
+
* @param {string} targetDir - Monorepo package directory.
|
|
217
|
+
* @returns {boolean}
|
|
218
|
+
*/
|
|
219
|
+
function isLinkedTo(dir, name, targetDir) {
|
|
220
|
+
let current = path.resolve(dir);
|
|
221
|
+
|
|
222
|
+
while (true) {
|
|
223
|
+
const candidate = path.join(current, 'node_modules', name);
|
|
224
|
+
if (fs.existsSync(candidate)) {
|
|
225
|
+
try {
|
|
226
|
+
return fs.realpathSync(candidate) === fs.realpathSync(targetDir);
|
|
227
|
+
} catch (e) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const parent = path.dirname(current);
|
|
232
|
+
if (parent === current) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
current = parent;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Rewrite one dependency's spec in a manifest, preserving its dev/prod
|
|
241
|
+
* placement (the same in-place rewrite npm does on --save).
|
|
242
|
+
* @param {string} targetDir - Directory holding the package.json.
|
|
243
|
+
* @param {string} name - Dependency name.
|
|
244
|
+
* @param {boolean} dev - Whether the entry lives in devDependencies.
|
|
245
|
+
* @param {string} spec - New spec value.
|
|
246
|
+
*/
|
|
247
|
+
function setDependencySpec(targetDir, name, dev, spec) {
|
|
248
|
+
const manifestPath = path.join(targetDir, 'package.json');
|
|
249
|
+
const pkg = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
250
|
+
pkg[dev ? 'devDependencies' : 'dependencies'][name] = spec;
|
|
251
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* file: spec path — relative to the declaring manifest, forward slashes.
|
|
256
|
+
* Computed from REAL paths on both ends: symlinks resolve physically, so a
|
|
257
|
+
* spec relativized through an aliased view (macOS /var → /private/var, a
|
|
258
|
+
* symlinked brand dir) would count the wrong number of ups and dangle.
|
|
259
|
+
*/
|
|
260
|
+
function relativeSpecPath(fromDir, target) {
|
|
261
|
+
return path.relative(fs.realpathSync(fromDir), fs.realpathSync(target)).split(path.sep).join('/');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* file:-install a brand's @omega.js dependencies from the local monorepo.
|
|
266
|
+
*
|
|
267
|
+
* Tree-wide by construction: npm resolves the WHOLE workspace tree on any
|
|
268
|
+
* install anchored in a brand monorepo, so linking one target while a SIBLING
|
|
269
|
+
* target still carries an unpublished registry spec (`@omega.js/backend: *`)
|
|
270
|
+
* 404s before anything links (the cp194 wizard-rehearsal catch — only
|
|
271
|
+
* reachable in a brand OUTSIDE the omega monorepo, the real consumer
|
|
272
|
+
* topology). Every target's @omega.js specs are therefore flipped to file:
|
|
273
|
+
* first (dev/prod placement preserved — the entry is edited in place), then
|
|
274
|
+
* ONE `npm install` materializes the links for the whole tree. Standalone
|
|
275
|
+
* targets degenerate to themselves.
|
|
276
|
+
*
|
|
277
|
+
* Idempotent: dependencies already resolving to the monorepo copy are
|
|
278
|
+
* skipped, and when nothing needs linking no install runs.
|
|
279
|
+
* @param {object} options
|
|
280
|
+
* @param {string} options.dir - Target directory to link from (any target in the brand).
|
|
281
|
+
* @param {string} options.monorepoRoot - Monorepo root path.
|
|
282
|
+
* @param {object} [options.logger] - Logger with log/warn (silent when omitted).
|
|
283
|
+
* @param {boolean} [options.dryRun] - Plan only, write and install nothing.
|
|
284
|
+
* @returns {Promise<Array<{name: string, dir: string, target: string, action: 'link'|'skip'|'missing'}>>}
|
|
285
|
+
*/
|
|
286
|
+
async function linkLocalPackages(options) {
|
|
287
|
+
const { dir, monorepoRoot, logger, dryRun } = options;
|
|
288
|
+
const actions = [];
|
|
289
|
+
|
|
290
|
+
const installRoot = findBrandRoot(dir);
|
|
291
|
+
let installNeeded = false;
|
|
292
|
+
|
|
293
|
+
// Manifest snapshots for rollback: every spec flips BEFORE the single
|
|
294
|
+
// install, so an install failure must restore the originals — a
|
|
295
|
+
// half-flipped tree is a quiet git-dirty diff someone could commit.
|
|
296
|
+
const manifestBackups = new Map();
|
|
297
|
+
const backupManifest = (targetDir) => {
|
|
298
|
+
const manifestPath = path.join(targetDir, 'package.json');
|
|
299
|
+
if (!manifestBackups.has(manifestPath)) {
|
|
300
|
+
manifestBackups.set(manifestPath, fs.readFileSync(manifestPath, 'utf8'));
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
for (const targetDir of discoverTargets(installRoot)) {
|
|
305
|
+
for (const entry of frameworkPackagesOf(targetDir)) {
|
|
306
|
+
const target = packageDir(monorepoRoot, entry.name);
|
|
307
|
+
|
|
308
|
+
if (!fs.existsSync(path.join(target, 'package.json'))) {
|
|
309
|
+
actions.push({ name: entry.name, dir: entry.dir, target, action: 'missing' });
|
|
310
|
+
logger && logger.warn(`${entry.name}: no monorepo package at ${target} — skipping`);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (isLinkedTo(entry.dir, entry.name, target)) {
|
|
315
|
+
actions.push({ name: entry.name, dir: entry.dir, target, action: 'skip' });
|
|
316
|
+
logger && logger.log(`${entry.name}: already linked to the monorepo`);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const spec = `file:${relativeSpecPath(entry.dir, target)}`;
|
|
321
|
+
if (entry.spec === spec) {
|
|
322
|
+
// Spec already correct, only the install is missing — no rewrite,
|
|
323
|
+
// so a committed relative spec is never churned to a new layout.
|
|
324
|
+
actions.push({ name: entry.name, dir: entry.dir, target, action: 'link' });
|
|
325
|
+
logger && logger.log(`${entry.name}: spec already file: — installing`);
|
|
326
|
+
installNeeded = true;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
actions.push({ name: entry.name, dir: entry.dir, target, action: 'link' });
|
|
331
|
+
logger && logger.log(`${entry.name}: linking → ${target}`);
|
|
332
|
+
installNeeded = true;
|
|
333
|
+
if (!dryRun) {
|
|
334
|
+
backupManifest(entry.dir);
|
|
335
|
+
setDependencySpec(entry.dir, entry.name, entry.dev, spec);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (installNeeded && !dryRun) {
|
|
341
|
+
try {
|
|
342
|
+
await safeInstall('npm install', { log: true, config: { cwd: installRoot } });
|
|
343
|
+
} catch (error) {
|
|
344
|
+
for (const [manifestPath, contents] of manifestBackups) {
|
|
345
|
+
fs.writeFileSync(manifestPath, contents);
|
|
346
|
+
}
|
|
347
|
+
logger && logger.warn(`install failed — restored ${manifestBackups.size} manifest(s) to their pre-link specs`);
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return actions;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Flip a brand tree's @omega.js `file:` specs back to registry specs — the
|
|
357
|
+
* publish-day inverse of linkLocalPackages(). Every file:-spec'd entry in
|
|
358
|
+
* every target manifest becomes the EXACT `<version>` of the CURRENTLY LINKED
|
|
359
|
+
* copy (read from the file: target's own package.json — no monorepo lookup, no
|
|
360
|
+
* registry call, so it works on any machine), then ONE `npm install`
|
|
361
|
+
* re-resolves the tree from the registry. Exact, never a caret (#794): the
|
|
362
|
+
* @omega.js family ships lockstep, so a caret here would let one target float
|
|
363
|
+
* ahead of its siblings on the next release and put two copies of
|
|
364
|
+
* @omega.js/client in one brand. An explicit `range` still wins verbatim — the
|
|
365
|
+
* caller owns that string. Idempotent: registry-spec'd entries are untouched;
|
|
366
|
+
* nothing to flip → no install. Same transactional manifest restore as the
|
|
367
|
+
* linker when the install fails.
|
|
368
|
+
* @param {object} options
|
|
369
|
+
* @param {string} options.dir - Any directory inside the brand.
|
|
370
|
+
* @param {object} [options.logger] - Logger with log/warn (silent when omitted).
|
|
371
|
+
* @param {boolean} [options.dryRun] - Plan only, write and install nothing.
|
|
372
|
+
* @param {string} [options.range] - Explicit spec for every flipped entry, used verbatim (e.g. '0.1.0').
|
|
373
|
+
* @returns {Promise<Array<{name: string, dir: string, spec: string, action: 'flip'|'skip'|'unresolvable'}>>}
|
|
374
|
+
*/
|
|
375
|
+
async function restoreRegistrySpecs(options) {
|
|
376
|
+
const { dir, logger, dryRun, range } = options;
|
|
377
|
+
const actions = [];
|
|
378
|
+
|
|
379
|
+
const installRoot = findBrandRoot(dir);
|
|
380
|
+
let installNeeded = false;
|
|
381
|
+
|
|
382
|
+
const manifestBackups = new Map();
|
|
383
|
+
const backupManifest = (targetDir) => {
|
|
384
|
+
const manifestPath = path.join(targetDir, 'package.json');
|
|
385
|
+
if (!manifestBackups.has(manifestPath)) {
|
|
386
|
+
manifestBackups.set(manifestPath, fs.readFileSync(manifestPath, 'utf8'));
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
for (const targetDir of discoverTargets(installRoot)) {
|
|
391
|
+
for (const entry of frameworkPackagesOf(targetDir)) {
|
|
392
|
+
if (!entry.spec.startsWith('file:')) {
|
|
393
|
+
actions.push({ name: entry.name, dir: entry.dir, spec: entry.spec, action: 'skip' });
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
let spec = range;
|
|
398
|
+
if (!spec) {
|
|
399
|
+
const target = path.resolve(targetDir, entry.spec.slice('file:'.length));
|
|
400
|
+
try {
|
|
401
|
+
spec = JSON.parse(fs.readFileSync(path.join(target, 'package.json'), 'utf8')).version;
|
|
402
|
+
} catch (e) {
|
|
403
|
+
actions.push({ name: entry.name, dir: entry.dir, spec: entry.spec, action: 'unresolvable' });
|
|
404
|
+
logger && logger.warn(`${entry.name}: cannot read ${target}/package.json — pass { range } or fix the link`);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
actions.push({ name: entry.name, dir: entry.dir, spec, action: 'flip' });
|
|
410
|
+
logger && logger.log(`${entry.name}: ${entry.spec} → ${spec}`);
|
|
411
|
+
installNeeded = true;
|
|
412
|
+
if (!dryRun) {
|
|
413
|
+
backupManifest(entry.dir);
|
|
414
|
+
setDependencySpec(entry.dir, entry.name, entry.dev, spec);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (installNeeded && !dryRun) {
|
|
420
|
+
try {
|
|
421
|
+
await safeInstall('npm install', { log: true, config: { cwd: installRoot } });
|
|
422
|
+
} catch (error) {
|
|
423
|
+
for (const [manifestPath, contents] of manifestBackups) {
|
|
424
|
+
fs.writeFileSync(manifestPath, contents);
|
|
425
|
+
}
|
|
426
|
+
logger && logger.warn(`install failed — restored ${manifestBackups.size} manifest(s) to their file: specs`);
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return actions;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Read a pid-stamped lock file and return the live owner pid — clearing the
|
|
436
|
+
* file when its process is gone. Signal 0 delivers nothing; it only asks
|
|
437
|
+
* whether the pid is still there.
|
|
438
|
+
* @param {string} lockPath - Absolute path of the lock file.
|
|
439
|
+
* @returns {number|null} Live pid, or null when unlocked (or stale).
|
|
440
|
+
*/
|
|
441
|
+
function readLivePid(lockPath) {
|
|
442
|
+
let pid;
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
pid = JSON.parse(fs.readFileSync(lockPath, 'utf8')).pid;
|
|
446
|
+
} catch (e) {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
process.kill(pid, 0);
|
|
452
|
+
return pid;
|
|
453
|
+
} catch (e) {
|
|
454
|
+
// Stale lock — the owning process is gone
|
|
455
|
+
fs.rmSync(lockPath, { force: true });
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Read the watch lock and return the live owner pid — clearing the lock when
|
|
462
|
+
* its process is gone.
|
|
463
|
+
* @param {string} monorepoRoot - Monorepo root path.
|
|
464
|
+
* @returns {number|null} Live pid, or null when unlocked.
|
|
465
|
+
*/
|
|
466
|
+
function readLiveWatchPid(monorepoRoot) {
|
|
467
|
+
return readLivePid(path.join(monorepoRoot, WATCH_LOCK));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Whether a vendor-propagation pass is re-preparing the frameworks right now
|
|
472
|
+
* ([#622](https://github.com/Omega-JS-Stack/omega/issues/622)) — a marker
|
|
473
|
+
* whose owner process is gone (a watch killed mid-pass) reads as idle.
|
|
474
|
+
* @param {string} monorepoRoot - Monorepo root path.
|
|
475
|
+
* @returns {boolean}
|
|
476
|
+
*/
|
|
477
|
+
function vendorPropagationActive(monorepoRoot) {
|
|
478
|
+
return readLivePid(path.join(monorepoRoot, VENDOR_PROPAGATION_LOCK)) !== null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Take the single-instance watch lock for this process.
|
|
483
|
+
* @param {string} monorepoRoot - Monorepo root path.
|
|
484
|
+
* @returns {{acquired: boolean, pid: number}} pid is the lock owner (ours on success).
|
|
485
|
+
*/
|
|
486
|
+
function acquireWatchLock(monorepoRoot) {
|
|
487
|
+
const livePid = readLiveWatchPid(monorepoRoot);
|
|
488
|
+
if (livePid) {
|
|
489
|
+
return { acquired: false, pid: livePid };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const lockPath = path.join(monorepoRoot, WATCH_LOCK);
|
|
493
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
494
|
+
fs.writeFileSync(lockPath, JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }, null, 2));
|
|
495
|
+
return { acquired: true, pid: process.pid };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Release the watch lock — only when this process owns it.
|
|
500
|
+
* @param {string} monorepoRoot - Monorepo root path.
|
|
501
|
+
*/
|
|
502
|
+
function releaseWatchLock(monorepoRoot) {
|
|
503
|
+
const lockPath = path.join(monorepoRoot, WATCH_LOCK);
|
|
504
|
+
try {
|
|
505
|
+
if (JSON.parse(fs.readFileSync(lockPath, 'utf8')).pid === process.pid) {
|
|
506
|
+
fs.rmSync(lockPath, { force: true });
|
|
507
|
+
}
|
|
508
|
+
} catch (e) {
|
|
509
|
+
// No lock (or unreadable) — nothing to release
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* The monorepo a brand's `@omega.js/*` dependencies actually resolve INTO, or
|
|
515
|
+
* null when they come from the registry.
|
|
516
|
+
*
|
|
517
|
+
* Asks resolution, never the manifest: a `file:` spec can be stale, an install
|
|
518
|
+
* can overwrite a link, and npm-workspace hoisting puts the copy at the brand
|
|
519
|
+
* root rather than the target. The first dependency of any target that lands
|
|
520
|
+
* in a `packages/<name>` directory under a monorepo root answers the question
|
|
521
|
+
* — a brand is linked as a whole, never one target at a time ([#587](https://github.com/Omega-JS-Stack/omega/issues/587)).
|
|
522
|
+
* @param {string} brandRoot - Brand root path.
|
|
523
|
+
* @returns {string|null} Absolute monorepo root, or null for a registry install.
|
|
524
|
+
*/
|
|
525
|
+
function resolveLinkedMonorepo(brandRoot) {
|
|
526
|
+
for (const targetDir of discoverTargets(brandRoot)) {
|
|
527
|
+
for (const entry of frameworkPackagesOf(targetDir)) {
|
|
528
|
+
const realDir = resolvePackageRealDir(entry.name, targetDir);
|
|
529
|
+
if (!isLocalCheckout(realDir)) {
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// <root>/packages/<name> — the layout packageDir() writes and every
|
|
534
|
+
// link points at.
|
|
535
|
+
const monorepoRoot = path.dirname(path.dirname(realDir));
|
|
536
|
+
if (isMonorepoRoot(monorepoRoot)) {
|
|
537
|
+
return monorepoRoot;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Bound on the wait for a fresh watch's initial prepare: a package that never
|
|
546
|
+
// reports must delay a boot, never strand it.
|
|
547
|
+
const WATCH_READY_TIMEOUT_MS = 120000;
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Readiness tracker for the monorepo watch's initial prepare, fed the watch's
|
|
551
|
+
* own stdout lines.
|
|
552
|
+
*
|
|
553
|
+
* The watch announces its roster (`Watching N packages (src→dist): ...`) and
|
|
554
|
+
* every package prints `'prepare-package': Ready for changes!` behind its
|
|
555
|
+
* `[name]` prefix once its first src→dist pass has landed — so "every dist is
|
|
556
|
+
* written" is exactly "N distinct prefixes have reported"
|
|
557
|
+
* ([#670](https://github.com/Omega-JS-Stack/omega/issues/670)). A package
|
|
558
|
+
* repeats that line on every later rebuild, so DISTINCT prefixes are counted:
|
|
559
|
+
* one chatty package can never stand in for one still preparing.
|
|
560
|
+
* @returns {{push: function, isReady: function, pending: function}} push(line)
|
|
561
|
+
* consumes one stdout line; pending() names the roster entries yet to report.
|
|
562
|
+
*/
|
|
563
|
+
function createWatchReadyTracker() {
|
|
564
|
+
let expected = null;
|
|
565
|
+
let roster = [];
|
|
566
|
+
const reported = new Set();
|
|
567
|
+
|
|
568
|
+
return {
|
|
569
|
+
push(line) {
|
|
570
|
+
const text = line.trim();
|
|
571
|
+
|
|
572
|
+
// The shared-package roster line (`Watching N shared packages ...`)
|
|
573
|
+
// is a different watch with no ready lines — it must not set N.
|
|
574
|
+
const rosterMatch = /^Watching (\d+) packages \(src→dist\): (.+)$/.exec(text);
|
|
575
|
+
if (rosterMatch) {
|
|
576
|
+
expected = Number(rosterMatch[1]);
|
|
577
|
+
roster = rosterMatch[2].split(',').map((name) => name.trim());
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const readyMatch = /^\[([^\]]+)\].*'prepare-package': Ready for changes!$/.exec(text);
|
|
582
|
+
if (readyMatch) {
|
|
583
|
+
reported.add(readyMatch[1].trim());
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
isReady() {
|
|
587
|
+
return expected !== null && reported.size >= expected;
|
|
588
|
+
},
|
|
589
|
+
pending() {
|
|
590
|
+
return roster.filter((name) => !reported.has(name));
|
|
591
|
+
},
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Readiness for a watch this process did NOT start
|
|
597
|
+
* ([#622](https://github.com/Omega-JS-Stack/omega/issues/622)).
|
|
598
|
+
*
|
|
599
|
+
* A boot that reuses a running watch has no child stdout to read, and skipping
|
|
600
|
+
* the gate is what let a leg require a framework dist mid-purge: the watch may
|
|
601
|
+
* be seconds old with its initial prepare still landing, or a vendor
|
|
602
|
+
* propagation pass may be re-preparing every framework right now. Both are
|
|
603
|
+
* readable from OUTSIDE the watch — its tee'd log (`.temp/logs/watch-all.log`)
|
|
604
|
+
* fed through the same tracker the fresh branch uses, plus the propagation
|
|
605
|
+
* marker — so the wait costs a long-idle watch nothing (every package has
|
|
606
|
+
* already printed its ready line) and holds a boot only while something is
|
|
607
|
+
* genuinely in flight.
|
|
608
|
+
*
|
|
609
|
+
* The log is only believed when its header pid IS the lock owner's: watch-all
|
|
610
|
+
* truncates the file at launch, so a header naming anyone else is a log this
|
|
611
|
+
* watch never wrote and there is nothing to gate on. Same three outcomes as the
|
|
612
|
+
* fresh branch, and it never rejects: 'ready', 'exit' (the watch died while we
|
|
613
|
+
* waited — nothing is coming) or 'timeout' after WATCH_READY_TIMEOUT_MS.
|
|
614
|
+
* @param {object} options
|
|
615
|
+
* @param {string} options.monorepoRoot - Monorepo root path.
|
|
616
|
+
* @param {number} options.pid - The running watch's pid (the lock owner).
|
|
617
|
+
* @param {object} [options.logger] - Logger with log (silent when omitted); warnings carry the watch's own tag.
|
|
618
|
+
* @returns {Promise<'ready'|'exit'|'timeout'>}
|
|
619
|
+
*/
|
|
620
|
+
function awaitRunningWatchReady(options) {
|
|
621
|
+
const { monorepoRoot, pid, logger } = options;
|
|
622
|
+
|
|
623
|
+
let fd;
|
|
624
|
+
try {
|
|
625
|
+
fd = fs.openSync(path.join(monorepoRoot, WATCH_LOG), 'r');
|
|
626
|
+
} catch (e) {
|
|
627
|
+
// No log to read (a CI run skips the tee) — a boot never pays the timeout
|
|
628
|
+
// for a file that does not exist.
|
|
629
|
+
return Promise.resolve('ready');
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
let tracker = createWatchReadyTracker();
|
|
633
|
+
let headerPid = null;
|
|
634
|
+
let offset = 0;
|
|
635
|
+
let carry = '';
|
|
636
|
+
const drain = () => {
|
|
637
|
+
const size = fs.fstatSync(fd).size;
|
|
638
|
+
// A relaunch truncates the log — re-read the new one from its header, and
|
|
639
|
+
// keep nothing the old file said: a fresh tracker, and headerPid back to
|
|
640
|
+
// null ("no header read yet"), so the new log's own header is what decides
|
|
641
|
+
// whether any of this belongs to the watch this boot waits on.
|
|
642
|
+
if (size < offset) {
|
|
643
|
+
offset = 0;
|
|
644
|
+
carry = '';
|
|
645
|
+
tracker = createWatchReadyTracker();
|
|
646
|
+
headerPid = null;
|
|
647
|
+
}
|
|
648
|
+
if (size === offset) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const buffer = Buffer.alloc(size - offset);
|
|
653
|
+
// readSync may return short — advance by what was READ, never by the size
|
|
654
|
+
// stat'd above, or the gap becomes garbage bytes appended to carry.
|
|
655
|
+
const read = fs.readSync(fd, buffer, 0, buffer.length, offset);
|
|
656
|
+
offset += read;
|
|
657
|
+
carry += buffer.toString('utf8', 0, read);
|
|
658
|
+
const lines = carry.split('\n');
|
|
659
|
+
carry = lines.pop();
|
|
660
|
+
for (const line of lines) {
|
|
661
|
+
const header = WATCH_LOG_HEADER.exec(line.trim());
|
|
662
|
+
if (header) {
|
|
663
|
+
headerPid = Number(header[1]);
|
|
664
|
+
}
|
|
665
|
+
tracker.push(line);
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
const settled = () => tracker.isReady() && !vendorPropagationActive(monorepoRoot);
|
|
669
|
+
|
|
670
|
+
drain();
|
|
671
|
+
if (headerPid !== pid) {
|
|
672
|
+
fs.closeSync(fd);
|
|
673
|
+
logger && logger.log(`Monorepo watch (pid ${pid}) writes no log this boot can read — booting without a readiness wait`);
|
|
674
|
+
return Promise.resolve('ready');
|
|
675
|
+
}
|
|
676
|
+
if (settled()) {
|
|
677
|
+
fs.closeSync(fd);
|
|
678
|
+
return Promise.resolve('ready');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const stragglers = tracker.pending();
|
|
682
|
+
logger && logger.log(`Monorepo watch (pid ${pid}) is mid-prepare${stragglers.length > 0 ? ` (${stragglers.join(', ')})` : ''} — waiting for it to land`);
|
|
683
|
+
// Warnings carry the watch's own identity, the way the spawned branch's do —
|
|
684
|
+
// a caller's logger is only guaranteed to have log().
|
|
685
|
+
const watchLogger = new Logger('watch');
|
|
686
|
+
|
|
687
|
+
return new Promise((resolve) => {
|
|
688
|
+
// Deliberately NOT unref'd: while the gate holds, this wait is the only
|
|
689
|
+
// thing the boot is doing, and an unref'd timer would let the process fall
|
|
690
|
+
// out from under it.
|
|
691
|
+
const poll = setInterval(() => {
|
|
692
|
+
drain();
|
|
693
|
+
// Checked BEFORE the verdict: a relaunched log clears the header, and
|
|
694
|
+
// until the new one names a pid there is nothing to judge (truncate and
|
|
695
|
+
// header write are two steps). Once it names a pid that is NOT the one
|
|
696
|
+
// this boot waits on, every line in the file belongs to a different
|
|
697
|
+
// watch — unreadable state, so the gate settles the way it does for a
|
|
698
|
+
// watch that died, rather than settling 'ready' off another's lines.
|
|
699
|
+
if (headerPid !== null && headerPid !== pid) {
|
|
700
|
+
watchLogger.warn(`Watch (pid ${pid}) no longer owns its log (now written by pid ${headerPid}) — continuing anyway`);
|
|
701
|
+
finish('exit');
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
if (settled()) {
|
|
705
|
+
finish('ready');
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
try {
|
|
709
|
+
process.kill(pid, 0);
|
|
710
|
+
} catch (e) {
|
|
711
|
+
// The watch died mid-wait — that pass is never landing, and the boot's
|
|
712
|
+
// own freshness sweep already ruled on the dists.
|
|
713
|
+
watchLogger.warn(`Watch (pid ${pid}) exited before its prepare landed — continuing anyway`);
|
|
714
|
+
finish('exit');
|
|
715
|
+
}
|
|
716
|
+
}, WATCH_LOG_POLL_MS);
|
|
717
|
+
const deadline = setTimeout(() => {
|
|
718
|
+
const pendingNames = tracker.pending();
|
|
719
|
+
watchLogger.warn(`Watch still preparing after ${WATCH_READY_TIMEOUT_MS / 1000}s${pendingNames.length > 0 ? ` (${pendingNames.join(', ')})` : ''} — continuing anyway`);
|
|
720
|
+
finish('timeout');
|
|
721
|
+
}, WATCH_READY_TIMEOUT_MS);
|
|
722
|
+
|
|
723
|
+
// Declared after the timers it clears — every path that calls it runs on a
|
|
724
|
+
// later tick, so the bindings are live by then.
|
|
725
|
+
const finish = (outcome) => {
|
|
726
|
+
clearInterval(poll);
|
|
727
|
+
clearTimeout(deadline);
|
|
728
|
+
fs.closeSync(fd);
|
|
729
|
+
resolve(outcome);
|
|
730
|
+
};
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Spawn the monorepo's src→dist watch (`npm start` at the monorepo root) as a
|
|
736
|
+
* SESSION-SCOPED child, unless one is already running (lock held by a live
|
|
737
|
+
* process — the watch script itself takes the lock).
|
|
738
|
+
*
|
|
739
|
+
* Session-scoped means the watch dies with the process that started it: the
|
|
740
|
+
* `exit` hook below fires on every ordinary end, including the deliberate
|
|
741
|
+
* `process.exit()` a caller's own Ctrl-C shutdown makes. Callers keep their own
|
|
742
|
+
* signal policy (a dev server exits on SIGINT, the brand-root orchestrator
|
|
743
|
+
* stops its legs first) — the watch's death is wired HERE, once, so no caller
|
|
744
|
+
* re-implements it ([#587](https://github.com/Omega-JS-Stack/omega/issues/587)).
|
|
745
|
+
*
|
|
746
|
+
* A FRESH watch rewrites every package's dist as it runs its initial prepare
|
|
747
|
+
* pass, so `ready` gates that pass: a caller awaits it before booting anything
|
|
748
|
+
* that loads those dists ([#670](https://github.com/Omega-JS-Stack/omega/issues/670)).
|
|
749
|
+
* It resolves when every watched package has reported ready, when the watch
|
|
750
|
+
* child exits before reporting, or after WATCH_READY_TIMEOUT_MS with one
|
|
751
|
+
* warning naming the stragglers — it never rejects, and resolves with which of
|
|
752
|
+
* the three settled it ('ready' | 'exit' | 'timeout') so a caller only claims
|
|
753
|
+
* success on 'ready'. An ALREADY-RUNNING watch gates too — the same three
|
|
754
|
+
* outcomes, read from its log instead of a child's stdout
|
|
755
|
+
* (awaitRunningWatchReady, [#622](https://github.com/Omega-JS-Stack/omega/issues/622)).
|
|
756
|
+
* @param {object} options
|
|
757
|
+
* @param {string} options.monorepoRoot - Monorepo root path.
|
|
758
|
+
* @param {object} [options.logger] - Logger with log (silent when omitted).
|
|
759
|
+
* @returns {{alreadyRunning: boolean, pid: number|null, child: object|null, ready: Promise<'ready'|'exit'|'timeout'>}}
|
|
760
|
+
*/
|
|
761
|
+
function startMonorepoWatch(options) {
|
|
762
|
+
const { monorepoRoot, logger } = options;
|
|
763
|
+
|
|
764
|
+
const livePid = readLiveWatchPid(monorepoRoot);
|
|
765
|
+
if (livePid) {
|
|
766
|
+
logger && logger.log(`Monorepo watch already running (pid ${livePid}) — leaving it be`);
|
|
767
|
+
return { alreadyRunning: true, pid: livePid, child: null, ready: awaitRunningWatchReady({ monorepoRoot, pid: livePid, logger }) };
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const child = spawn('npm', ['start'], { cwd: monorepoRoot, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
771
|
+
// Forwarded child output keeps its OWN identity — these lines come from the
|
|
772
|
+
// monorepo watch, not from whatever command spawned it, so they stamp
|
|
773
|
+
// `[@omega.js/<host>:watch]` instead of borrowing the caller's logger (#12).
|
|
774
|
+
const watchLogger = new Logger('watch');
|
|
775
|
+
const forward = (stream) => {
|
|
776
|
+
stream.setEncoding('utf8');
|
|
777
|
+
stream.on('data', (chunk) => {
|
|
778
|
+
for (const line of chunk.split('\n')) {
|
|
779
|
+
if (line.trim()) {
|
|
780
|
+
watchLogger.log(line);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
};
|
|
785
|
+
forward(child.stdout);
|
|
786
|
+
forward(child.stderr);
|
|
787
|
+
|
|
788
|
+
const tracker = createWatchReadyTracker();
|
|
789
|
+
let settleReady;
|
|
790
|
+
const ready = new Promise((resolve) => {
|
|
791
|
+
settleReady = resolve;
|
|
792
|
+
});
|
|
793
|
+
const readyTimer = setTimeout(() => {
|
|
794
|
+
const stragglers = tracker.pending();
|
|
795
|
+
watchLogger.warn(`Initial prepare still running after ${WATCH_READY_TIMEOUT_MS / 1000}s${stragglers.length > 0 ? ` (${stragglers.join(', ')})` : ''} — continuing anyway`);
|
|
796
|
+
settleReady('timeout');
|
|
797
|
+
}, WATCH_READY_TIMEOUT_MS);
|
|
798
|
+
// Never the reason a process stays alive: the watch child already holds the
|
|
799
|
+
// loop open for as long as the session wants it.
|
|
800
|
+
readyTimer.unref();
|
|
801
|
+
|
|
802
|
+
// Its OWN line buffering, beside the forwarding above: readiness matches a
|
|
803
|
+
// line's tail and a chunk boundary can split one. A smeared LOG line is
|
|
804
|
+
// cosmetic, where a missed ready line would hold a boot for the full timeout.
|
|
805
|
+
let pending = '';
|
|
806
|
+
child.stdout.on('data', (chunk) => {
|
|
807
|
+
pending += chunk;
|
|
808
|
+
const lines = pending.split('\n');
|
|
809
|
+
pending = lines.pop();
|
|
810
|
+
lines.forEach((line) => tracker.push(line));
|
|
811
|
+
if (tracker.isReady()) {
|
|
812
|
+
clearTimeout(readyTimer);
|
|
813
|
+
settleReady('ready');
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
// A watch that dies before it reports — a lost lock race, a spawn that never
|
|
818
|
+
// ran — has no pass left to wait for, so its exit settles the wait instead of
|
|
819
|
+
// parking a boot on the full timeout.
|
|
820
|
+
child.on('exit', () => {
|
|
821
|
+
clearTimeout(readyTimer);
|
|
822
|
+
settleReady('exit');
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
// Session-scoped: no orphaned watcher outlives the session that spawned it.
|
|
826
|
+
process.on('exit', () => {
|
|
827
|
+
try {
|
|
828
|
+
child.kill('SIGTERM');
|
|
829
|
+
} catch (e) {
|
|
830
|
+
// Already gone
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
logger && logger.log(`Monorepo watch started (pid ${child.pid}) — src→dist rebuilds are live`);
|
|
835
|
+
|
|
836
|
+
return { alreadyRunning: false, pid: child.pid, child, ready };
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Propagate vendored shared-package edits to the frameworks that embed them:
|
|
841
|
+
* watch each vendorable package's src/ and re-run `npm run prepare` in the
|
|
842
|
+
* dependents that actually vendor a changed package. A framework's own
|
|
843
|
+
* prepare:watch only sees its OWN src, while the devkit/config/account copies
|
|
844
|
+
* inside its dist/vendor/* refresh only on a full prepare — without this, a
|
|
845
|
+
* shared-package edit strands every dist-running framework on stale vendored
|
|
846
|
+
* code until a manual rebuild.
|
|
847
|
+
*
|
|
848
|
+
* Each pass is SCOPED (a dependent whose dist/vendor lacks every changed
|
|
849
|
+
* package is skipped; a dependent with no vendor tree yet always runs — first
|
|
850
|
+
* build) and CONCURRENT (independent prepares, no ordering contract). Edits
|
|
851
|
+
* inside the debounce window fold into one pass; edits landing mid-pass queue
|
|
852
|
+
* exactly one follow-up pass. A dependent's prepare failing is logged and
|
|
853
|
+
* never stops the rest of the pass.
|
|
854
|
+
*
|
|
855
|
+
* A running pass holds `.omega/vendor-propagation.lock` (pid inside) for its
|
|
856
|
+
* whole life, so a brand booting on these dists can wait it out instead of
|
|
857
|
+
* requiring a framework mid-purge
|
|
858
|
+
* ([#622](https://github.com/Omega-JS-Stack/omega/issues/622)).
|
|
859
|
+
* @param {object} options
|
|
860
|
+
* @param {string} options.packagesDir - The monorepo's packages/ directory.
|
|
861
|
+
* @param {string[]} options.packages - Vendorable package dir names to watch (missing src/ dirs are skipped).
|
|
862
|
+
* @param {Array<{name: string, dir: string}>} options.dependents - Packages eligible for re-prepare.
|
|
863
|
+
* @param {function} [options.runPrepare] - (dependent) => Promise; defaults to spawning `npm run prepare` in dependent.dir.
|
|
864
|
+
* @param {function} [options.log] - Line logger (silent when omitted).
|
|
865
|
+
* @param {number} [options.debounceMs] - Quiet window before a pass (default 400).
|
|
866
|
+
* @returns {{watched: string[], poke: function, close: function}} poke(name)
|
|
867
|
+
* injects a change event without the fs (test seam — the fs watchers call
|
|
868
|
+
* the same path).
|
|
869
|
+
*/
|
|
870
|
+
function startVendorPropagation(options) {
|
|
871
|
+
const { packagesDir, packages, dependents, log = () => {}, debounceMs = 400 } = options;
|
|
872
|
+
|
|
873
|
+
// This prepare takes the dependent's heal lock for the child's whole life:
|
|
874
|
+
// it writes the same dist a booting CLI's heal rebuilds, so the two must
|
|
875
|
+
// never run at once. The wait is async — the watcher keeps serving its other
|
|
876
|
+
// watches while a peer's prepare finishes.
|
|
877
|
+
const runPrepare = options.runPrepare || ((dependent) => acquireHealLock(dependent.dir).then((release) => new Promise((resolve) => {
|
|
878
|
+
const done = () => {
|
|
879
|
+
release();
|
|
880
|
+
resolve();
|
|
881
|
+
};
|
|
882
|
+
const child = spawn('npm', ['run', 'prepare'], { cwd: dependent.dir, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
883
|
+
|
|
884
|
+
// Quiet on success — surface output only when the prepare fails
|
|
885
|
+
let output = '';
|
|
886
|
+
child.stdout.on('data', (chunk) => { output += chunk; });
|
|
887
|
+
child.stderr.on('data', (chunk) => { output += chunk; });
|
|
888
|
+
|
|
889
|
+
child.on('error', (error) => {
|
|
890
|
+
log(`prepare failed to spawn in ${dependent.name}: ${error.message}`);
|
|
891
|
+
done();
|
|
892
|
+
});
|
|
893
|
+
child.on('exit', (code) => {
|
|
894
|
+
if (code !== 0) {
|
|
895
|
+
log(`prepare exited ${code} in ${dependent.name}:\n${output.trim()}`);
|
|
896
|
+
} else {
|
|
897
|
+
log(`re-prepared ${dependent.name}`);
|
|
898
|
+
}
|
|
899
|
+
done();
|
|
900
|
+
});
|
|
901
|
+
})));
|
|
902
|
+
|
|
903
|
+
// The monorepo root by construction: packagesDir IS <root>/packages, so the
|
|
904
|
+
// marker lands beside the watch lock, where a booting brand looks for it.
|
|
905
|
+
const markerPath = path.join(path.dirname(packagesDir), VENDOR_PROPAGATION_LOCK);
|
|
906
|
+
const holdMarker = () => {
|
|
907
|
+
fs.mkdirSync(path.dirname(markerPath), { recursive: true });
|
|
908
|
+
fs.writeFileSync(markerPath, JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }, null, 2));
|
|
909
|
+
};
|
|
910
|
+
const releaseMarker = () => fs.rmSync(markerPath, { force: true });
|
|
911
|
+
|
|
912
|
+
let timer = null;
|
|
913
|
+
let running = false;
|
|
914
|
+
let pending = false;
|
|
915
|
+
let closed = false;
|
|
916
|
+
const dirty = new Set();
|
|
917
|
+
|
|
918
|
+
const pass = async () => {
|
|
919
|
+
try {
|
|
920
|
+
running = true;
|
|
921
|
+
// A pass DELETES each dependent's dist before recopying it, so a target
|
|
922
|
+
// booting into one requires a half-purged framework (#622). The marker
|
|
923
|
+
// says "a purge is in flight" to anything that gates on it (a brand
|
|
924
|
+
// boot's readiness wait) — pid-stamped, so a watch killed mid-pass
|
|
925
|
+
// leaves nothing a later boot honors. Held INSIDE the try so the finally
|
|
926
|
+
// owns the whole lifecycle: no throw can leave the flag or the marker up.
|
|
927
|
+
holdMarker();
|
|
928
|
+
do {
|
|
929
|
+
pending = false;
|
|
930
|
+
const changed = [...dirty];
|
|
931
|
+
dirty.clear();
|
|
932
|
+
|
|
933
|
+
// A spurious trigger with nothing dirty (fs watchers can replay stale
|
|
934
|
+
// events under load) is a no-op, never a full re-prepare
|
|
935
|
+
if (changed.length === 0) {
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Scope: skip dependents whose dist/vendor embeds none of the changed
|
|
940
|
+
// packages (a missing vendor tree means not-yet-prepared — always run)
|
|
941
|
+
const affected = dependents.filter((dependent) => {
|
|
942
|
+
const vendorRoot = path.join(dependent.dir, 'dist', 'vendor');
|
|
943
|
+
return !fs.existsSync(vendorRoot)
|
|
944
|
+
|| changed.some((name) => fs.existsSync(path.join(vendorRoot, name)));
|
|
945
|
+
});
|
|
946
|
+
const skipped = dependents.length - affected.length;
|
|
947
|
+
log(`${changed.join(', ')} changed — re-preparing ${affected.map((d) => d.name).join(', ') || '(none)'}${skipped > 0 ? ` (${skipped} unaffected)` : ''}`);
|
|
948
|
+
|
|
949
|
+
if (closed) {
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
952
|
+
await Promise.all(affected.map((dependent) =>
|
|
953
|
+
Promise.resolve()
|
|
954
|
+
.then(() => runPrepare(dependent))
|
|
955
|
+
.catch((error) => log(`prepare failed in ${dependent.name}: ${error.message}`))
|
|
956
|
+
));
|
|
957
|
+
} while (pending && !closed);
|
|
958
|
+
} finally {
|
|
959
|
+
releaseMarker();
|
|
960
|
+
running = false;
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
const trigger = () => {
|
|
965
|
+
if (closed) {
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
if (running) {
|
|
969
|
+
pending = true;
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
// Floated on purpose, and un-caught on purpose: prepare failures are
|
|
973
|
+
// already handled per-dependent, so a rejection here means the marker
|
|
974
|
+
// machinery itself broke — a watch that cannot say "a purge is in flight"
|
|
975
|
+
// crashes loudly rather than purge dists behind a gate no boot can see.
|
|
976
|
+
pass();
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
const onSourceEvent = (name) => {
|
|
980
|
+
if (closed) {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
dirty.add(name);
|
|
984
|
+
clearTimeout(timer);
|
|
985
|
+
timer = setTimeout(trigger, debounceMs);
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
const watchers = [];
|
|
989
|
+
const watched = [];
|
|
990
|
+
for (const name of packages) {
|
|
991
|
+
const srcDir = path.join(packagesDir, name, 'src');
|
|
992
|
+
if (!fs.existsSync(srcDir)) {
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
watchers.push(fs.watch(srcDir, { recursive: true }, () => onSourceEvent(name)));
|
|
997
|
+
watched.push(name);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
return {
|
|
1001
|
+
watched,
|
|
1002
|
+
poke: onSourceEvent,
|
|
1003
|
+
close: () => {
|
|
1004
|
+
closed = true;
|
|
1005
|
+
clearTimeout(timer);
|
|
1006
|
+
watchers.forEach((watcher) => watcher.close());
|
|
1007
|
+
},
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// The vendorable private packages folded into framework dists. Hardcoded here
|
|
1012
|
+
// because this module must stay stdlib-only — tools/vendor.js
|
|
1013
|
+
// VENDORABLE_PACKAGES is the SSOT (a devkit test pins the two lists equal).
|
|
1014
|
+
const FRESHNESS_VENDORABLES = ['devkit', 'config', 'account', 'template-kit', 'analytics', 'monitoring'];
|
|
1015
|
+
|
|
1016
|
+
// Directory names the freshness scan never descends into.
|
|
1017
|
+
const FRESHNESS_SKIP_DIRS = new Set(['node_modules', '.temp', 'dist']);
|
|
1018
|
+
|
|
1019
|
+
// Dist paths prepare legitimately generates with no src counterpart, so their
|
|
1020
|
+
// presence is never "a src file was deleted": the vendor hook's module copies
|
|
1021
|
+
// (dist/vendor/**). Nothing else — vendor-docs writes to the package ROOT
|
|
1022
|
+
// docs/ and prepare-package rewrites the ROOT package.json, so a dist/docs or
|
|
1023
|
+
// dist/package.json IS a leftover. A host's declared `omega.vendorAssets`
|
|
1024
|
+
// destinations join the set per package (see allowedDistExtras) — that is how
|
|
1025
|
+
// desktop/extension end up with a dist/assets tree the web package owns.
|
|
1026
|
+
const DIST_EXTRAS = ['vendor'];
|
|
1027
|
+
|
|
1028
|
+
// The one dist subtree the orphan scan skips entirely: a package's self-tests
|
|
1029
|
+
// SEED runtime state into dist/test/fixtures/ (backend writes its fixture
|
|
1030
|
+
// project's firestore.rules and service-account.json there). Nothing in it can
|
|
1031
|
+
// shadow a consumer require, and the exemption survives a crashed run where a
|
|
1032
|
+
// teardown would not (#352). Files there WITH a src counterpart still age.
|
|
1033
|
+
const DIST_FIXTURE_STATE = 'test/fixtures/';
|
|
1034
|
+
|
|
1035
|
+
// The per-package heal mutex (mkdir-as-mutex, owner pid inside) — .omega/ is
|
|
1036
|
+
// gitignored monorepo-wide, so the lock never dirties a package.
|
|
1037
|
+
const HEAL_LOCK = path.join('.omega', 'heal.lock');
|
|
1038
|
+
// A prepare legitimately holds the lock for minutes, so the loser waits long
|
|
1039
|
+
// and only ever proceeds unlocked as a last resort — a heal must never fail
|
|
1040
|
+
// over its own bookkeeping.
|
|
1041
|
+
const HEAL_LOCK_WAIT_MS = 120000;
|
|
1042
|
+
const HEAL_LOCK_POLL_MS = 50;
|
|
1043
|
+
// Grace for the window between mkdir and the owner file landing: a lock with no
|
|
1044
|
+
// readable owner is only abandoned once it is older than this.
|
|
1045
|
+
const HEAL_LOCK_ORPHAN_MS = 5000;
|
|
1046
|
+
|
|
1047
|
+
// Bounded grace for an in-flight watcher copy before the boot heals anyway.
|
|
1048
|
+
const WATCH_GRACE_MS = 2000;
|
|
1049
|
+
const WATCH_POLL_MS = 100;
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Synchronous wait — this whole path is sync by design (it runs before a CLI's
|
|
1053
|
+
* first await, and freshnessBoot may re-exec the process).
|
|
1054
|
+
* @param {number} ms - Milliseconds to block.
|
|
1055
|
+
*/
|
|
1056
|
+
function sleepSync(ms) {
|
|
1057
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* Newest mtime (ms) of any file or directory under dir, recursive — skipping
|
|
1062
|
+
* node_modules/.temp/dist and never following symlinks. Directory mtimes are
|
|
1063
|
+
* counted too, so a deletion (which touches only the parent dir) still reads
|
|
1064
|
+
* as a change. Missing dir → 0.
|
|
1065
|
+
* @param {string} dir - Directory to scan.
|
|
1066
|
+
* @returns {number} Newest mtimeMs, or 0 when nothing exists.
|
|
1067
|
+
*/
|
|
1068
|
+
function newestMtimeUnder(dir) {
|
|
1069
|
+
let newest = 0;
|
|
1070
|
+
const queue = [dir];
|
|
1071
|
+
|
|
1072
|
+
while (queue.length > 0) {
|
|
1073
|
+
const current = queue.pop();
|
|
1074
|
+
let entries;
|
|
1075
|
+
try {
|
|
1076
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
1077
|
+
newest = Math.max(newest, fs.statSync(current).mtimeMs);
|
|
1078
|
+
} catch (e) {
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
for (const entry of entries) {
|
|
1082
|
+
if (entry.isSymbolicLink()) {
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
const abs = path.join(current, entry.name);
|
|
1086
|
+
if (entry.isDirectory()) {
|
|
1087
|
+
if (!FRESHNESS_SKIP_DIRS.has(entry.name)) {
|
|
1088
|
+
queue.push(abs);
|
|
1089
|
+
}
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
if (entry.isFile()) {
|
|
1093
|
+
try {
|
|
1094
|
+
newest = Math.max(newest, fs.statSync(abs).mtimeMs);
|
|
1095
|
+
} catch (e) {
|
|
1096
|
+
// Raced deletion — skip
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
return newest;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* Newest mtime (ms) of a path that may be a FILE or a directory — a declared
|
|
1107
|
+
* `omega.vendorAssets` entry names either shape. Missing path → 0.
|
|
1108
|
+
* @param {string} target - File or directory to measure.
|
|
1109
|
+
* @returns {number} Newest mtimeMs, or 0 when nothing exists.
|
|
1110
|
+
*/
|
|
1111
|
+
function newestMtimeOf(target) {
|
|
1112
|
+
let stat;
|
|
1113
|
+
try {
|
|
1114
|
+
stat = fs.statSync(target);
|
|
1115
|
+
} catch (e) {
|
|
1116
|
+
return 0;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
return stat.isDirectory() ? newestMtimeUnder(target) : stat.mtimeMs;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* Every file under dir, keyed by its dir-relative POSIX path — skipping
|
|
1124
|
+
* node_modules/.temp/dist and never following symlinks (a framework dist can
|
|
1125
|
+
* carry a circular self-link fixture). Missing dir → empty map.
|
|
1126
|
+
* @param {string} dir - Directory to scan.
|
|
1127
|
+
* @returns {Map<string, number>} relative path → mtimeMs.
|
|
1128
|
+
*/
|
|
1129
|
+
function filesUnder(dir) {
|
|
1130
|
+
const files = new Map();
|
|
1131
|
+
const queue = [''];
|
|
1132
|
+
|
|
1133
|
+
while (queue.length > 0) {
|
|
1134
|
+
const relative = queue.pop();
|
|
1135
|
+
let entries;
|
|
1136
|
+
try {
|
|
1137
|
+
entries = fs.readdirSync(path.join(dir, relative), { withFileTypes: true });
|
|
1138
|
+
} catch (e) {
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
for (const entry of entries) {
|
|
1142
|
+
if (entry.isSymbolicLink()) {
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
const child = relative ? `${relative}/${entry.name}` : entry.name;
|
|
1146
|
+
if (entry.isDirectory()) {
|
|
1147
|
+
if (!FRESHNESS_SKIP_DIRS.has(entry.name)) {
|
|
1148
|
+
queue.push(child);
|
|
1149
|
+
}
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
if (entry.isFile()) {
|
|
1153
|
+
try {
|
|
1154
|
+
files.set(child, fs.statSync(path.join(dir, child)).mtimeMs);
|
|
1155
|
+
} catch (e) {
|
|
1156
|
+
// Raced deletion — skip
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
return files;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* The dist paths a prepare of THIS package generates without a src counterpart:
|
|
1167
|
+
* the standing set (`dist/vendor/**`) plus every `omega.vendorAssets`
|
|
1168
|
+
* destination the host declares (each is a dist-relative file or directory the
|
|
1169
|
+
* vendor hook copies from another package).
|
|
1170
|
+
* @param {object|null} pkg - The package's manifest.
|
|
1171
|
+
* @returns {string[]} Dist-relative POSIX paths (files or directory roots).
|
|
1172
|
+
*/
|
|
1173
|
+
function allowedDistExtras(pkg) {
|
|
1174
|
+
const declared = ((pkg && pkg.omega && pkg.omega.vendorAssets) || [])
|
|
1175
|
+
.map((entry) => entry && entry.to)
|
|
1176
|
+
.filter(Boolean)
|
|
1177
|
+
.map((to) => to.split(path.sep).join('/').replace(/^\.\//, '').replace(/\/+$/, ''));
|
|
1178
|
+
|
|
1179
|
+
return DIST_EXTRAS.concat(declared);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/**
|
|
1183
|
+
* Per-file staleness evidence for a locally-linked package's dist.
|
|
1184
|
+
*
|
|
1185
|
+
* A whole-tree newest-mtime compare is not enough: prepare's after-hook writes
|
|
1186
|
+
* into dist too (vendored modules, declared assets), so ANY dist write
|
|
1187
|
+
* after a src edit made the tree read "fresh" while individual dist files were
|
|
1188
|
+
* stale or missing outright (the 2026-08-05 incident). Evidence instead: every
|
|
1189
|
+
* src file must exist at its mapped dist path with an mtime at least as new, no
|
|
1190
|
+
* dist file may lack a src counterpart (a deleted source) unless prepare
|
|
1191
|
+
* generates it, and — inside the monorepo — each embedded dist/vendor/<name>
|
|
1192
|
+
* copy must be newer than that private package's src, and each declared
|
|
1193
|
+
* `omega.vendorAssets` destination newer than the sibling package's source it
|
|
1194
|
+
* was copied from (both kinds of copy only refresh on a full prepare).
|
|
1195
|
+
* @param {object} options
|
|
1196
|
+
* @param {string} options.srcDir - The package's src directory.
|
|
1197
|
+
* @param {string} options.distDir - The package's dist directory.
|
|
1198
|
+
* @param {object|null} options.pkg - The package's manifest.
|
|
1199
|
+
* @param {string} options.monorepoRoot - Monorepo root (only read when inMonorepo).
|
|
1200
|
+
* @param {boolean} options.inMonorepo - Whether the package lives in the monorepo.
|
|
1201
|
+
* @returns {string|null} The first piece of evidence, or null when fresh.
|
|
1202
|
+
*/
|
|
1203
|
+
function distStaleReason(options) {
|
|
1204
|
+
const { srcDir, distDir, pkg, monorepoRoot, inMonorepo } = options;
|
|
1205
|
+
|
|
1206
|
+
if (!fs.existsSync(distDir)) {
|
|
1207
|
+
return 'dist/ is missing';
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
const srcFiles = filesUnder(srcDir);
|
|
1211
|
+
const distFiles = filesUnder(distDir);
|
|
1212
|
+
|
|
1213
|
+
for (const [relative, mtime] of srcFiles) {
|
|
1214
|
+
const distMtime = distFiles.get(relative);
|
|
1215
|
+
if (distMtime === undefined) {
|
|
1216
|
+
return `dist/${relative} is missing`;
|
|
1217
|
+
}
|
|
1218
|
+
if (mtime > distMtime) {
|
|
1219
|
+
return `dist/${relative} is older than src/${relative}`;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
const extras = allowedDistExtras(pkg);
|
|
1224
|
+
for (const relative of distFiles.keys()) {
|
|
1225
|
+
if (srcFiles.has(relative) || relative.startsWith(DIST_FIXTURE_STATE)) {
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
if (!extras.some((extra) => relative === extra || relative.startsWith(`${extra}/`))) {
|
|
1229
|
+
return `dist/${relative} has no src counterpart (deleted source)`;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if (inMonorepo) {
|
|
1234
|
+
for (const name of FRESHNESS_VENDORABLES) {
|
|
1235
|
+
const vendorDir = path.join(distDir, 'vendor', name);
|
|
1236
|
+
if (fs.existsSync(vendorDir)
|
|
1237
|
+
&& newestMtimeUnder(path.join(monorepoRoot, 'packages', name, 'src')) > newestMtimeUnder(vendorDir)) {
|
|
1238
|
+
return `dist/vendor/${name} is older than packages/${name}/src`;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// The declared assets the orphan scan above exempts: that exemption stopped
|
|
1243
|
+
// the false "deleted source" hits, but it left the destinations with NO
|
|
1244
|
+
// freshness question at all, so an edit to web's core/ or themes/ never made
|
|
1245
|
+
// desktop/extension stale and a watcher-down boot served the old copy
|
|
1246
|
+
// (#199). The source is a monorepo sibling package (the vendor hook copies
|
|
1247
|
+
// it verbatim, no build step), so the inMonorepo guard skips the loop on a
|
|
1248
|
+
// published install — where there is no sibling to compare against.
|
|
1249
|
+
for (const entry of (pkg && pkg.omega && pkg.omega.vendorAssets) || []) {
|
|
1250
|
+
if (!entry || !entry.package || !entry.from || !entry.to) {
|
|
1251
|
+
continue; // Malformed — the vendor hook is the one that fails on it
|
|
1252
|
+
}
|
|
1253
|
+
if (!entry.package.startsWith('@omega.js/')) {
|
|
1254
|
+
continue; // Foreign scope — the short-name → packages/<name> mapping below only holds for ours
|
|
1255
|
+
}
|
|
1256
|
+
const sourceMtime = newestMtimeOf(path.join(monorepoRoot, 'packages', entry.package.split('/').pop(), entry.from));
|
|
1257
|
+
if (sourceMtime === 0) {
|
|
1258
|
+
// A wrong `from` (rename/typo) lands here too and stays silent — the
|
|
1259
|
+
// vendor hook's warning owns that mistake; returning stale instead
|
|
1260
|
+
// would loop a full prepare on every boot with nothing to fix it
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
const destination = path.join(distDir, entry.to);
|
|
1264
|
+
if (!fs.existsSync(destination)) {
|
|
1265
|
+
return `dist/${entry.to} was never vendored from ${entry.package}'s ${entry.from}`;
|
|
1266
|
+
}
|
|
1267
|
+
if (sourceMtime > newestMtimeOf(destination)) {
|
|
1268
|
+
return `dist/${entry.to} is older than ${entry.package}'s ${entry.from}`;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
return null;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
/**
|
|
1277
|
+
* Whether a held heal lock can be stolen: its owner process is gone, or the
|
|
1278
|
+
* lock has no readable owner and is older than the mkdir→write grace (the
|
|
1279
|
+
* holder died mid-acquire).
|
|
1280
|
+
* @param {string} lockDir - The lock directory.
|
|
1281
|
+
* @returns {boolean}
|
|
1282
|
+
*/
|
|
1283
|
+
function healLockAbandoned(lockDir) {
|
|
1284
|
+
let pid;
|
|
1285
|
+
try {
|
|
1286
|
+
pid = JSON.parse(fs.readFileSync(path.join(lockDir, 'owner.json'), 'utf8')).pid;
|
|
1287
|
+
} catch (e) {
|
|
1288
|
+
try {
|
|
1289
|
+
return Date.now() - fs.statSync(lockDir).mtimeMs > HEAL_LOCK_ORPHAN_MS;
|
|
1290
|
+
} catch (statError) {
|
|
1291
|
+
return false; // Lock vanished — the caller's next mkdir wins it
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
try {
|
|
1296
|
+
process.kill(pid, 0);
|
|
1297
|
+
return false;
|
|
1298
|
+
} catch (e) {
|
|
1299
|
+
// Only "no such process" means gone: EPERM is a LIVE process this user may
|
|
1300
|
+
// not signal, and stealing its lock would double the build it is running
|
|
1301
|
+
return e.code === 'ESRCH';
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* Make the lock's parent (`<package>/.omega`) — the one piece of bookkeeping
|
|
1307
|
+
* that must exist before any mkdir-as-mutex attempt.
|
|
1308
|
+
* @param {string} lockDir - The lock directory.
|
|
1309
|
+
* @returns {boolean} False when the dir is unmakeable (read-only fs, full
|
|
1310
|
+
* disk) — the caller then proceeds unlocked rather than failing the heal.
|
|
1311
|
+
*/
|
|
1312
|
+
function ensureHealLockParent(lockDir) {
|
|
1313
|
+
try {
|
|
1314
|
+
fs.mkdirSync(path.dirname(lockDir), { recursive: true });
|
|
1315
|
+
return true;
|
|
1316
|
+
} catch (e) {
|
|
1317
|
+
return false;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* ONE attempt at the mkdir-as-mutex, stealing an abandoned lock and retrying
|
|
1323
|
+
* once. Never throws: a heal must not fail over its own bookkeeping.
|
|
1324
|
+
* @param {string} lockDir - The lock directory.
|
|
1325
|
+
* @returns {boolean} Whether this process now holds the lock.
|
|
1326
|
+
*/
|
|
1327
|
+
function tryHealLock(lockDir) {
|
|
1328
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1329
|
+
try {
|
|
1330
|
+
fs.mkdirSync(lockDir);
|
|
1331
|
+
} catch (e) {
|
|
1332
|
+
if (attempt === 0 && healLockAbandoned(lockDir)) {
|
|
1333
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
return false;
|
|
1337
|
+
}
|
|
1338
|
+
try {
|
|
1339
|
+
fs.writeFileSync(path.join(lockDir, 'owner.json'), JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }));
|
|
1340
|
+
} catch (e) {
|
|
1341
|
+
// The owner file is liveness evidence, not the mutex itself — a lock
|
|
1342
|
+
// nobody can read is only abandoned after the orphan grace, which is
|
|
1343
|
+
// exactly the right answer for a holder that could not write it
|
|
1344
|
+
}
|
|
1345
|
+
return true;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
return false;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/**
|
|
1352
|
+
* Release a held heal lock. Idempotent — a spawn that emits both 'error' and
|
|
1353
|
+
* 'exit' must never delete a lock a LATER process has since won.
|
|
1354
|
+
* @param {string} lockDir - The lock directory.
|
|
1355
|
+
* @returns {function} The release function.
|
|
1356
|
+
*/
|
|
1357
|
+
function healLockReleaser(lockDir) {
|
|
1358
|
+
let released = false;
|
|
1359
|
+
return () => {
|
|
1360
|
+
if (released) {
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
released = true;
|
|
1364
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/**
|
|
1369
|
+
* Run fn under the package's cross-process heal lock, so two CLIs booting on
|
|
1370
|
+
* the same stale link produce ONE build. The loser waits for the winner (a
|
|
1371
|
+
* prepare can run for minutes), an abandoned lock is stolen, and a wait that
|
|
1372
|
+
* outlives the deadline proceeds unlocked rather than failing the boot.
|
|
1373
|
+
* @param {string} realDir - The package directory.
|
|
1374
|
+
* @param {function} fn - Critical section.
|
|
1375
|
+
* @returns {*} fn's result.
|
|
1376
|
+
*/
|
|
1377
|
+
function withHealLock(realDir, fn) {
|
|
1378
|
+
const lockDir = path.join(realDir, HEAL_LOCK);
|
|
1379
|
+
const deadline = Date.now() + HEAL_LOCK_WAIT_MS;
|
|
1380
|
+
let release = null;
|
|
1381
|
+
|
|
1382
|
+
if (ensureHealLockParent(lockDir)) {
|
|
1383
|
+
while (true) {
|
|
1384
|
+
if (tryHealLock(lockDir)) {
|
|
1385
|
+
release = healLockReleaser(lockDir);
|
|
1386
|
+
break;
|
|
1387
|
+
}
|
|
1388
|
+
if (Date.now() >= deadline) {
|
|
1389
|
+
break;
|
|
1390
|
+
}
|
|
1391
|
+
sleepSync(HEAL_LOCK_POLL_MS);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
try {
|
|
1396
|
+
return fn();
|
|
1397
|
+
} finally {
|
|
1398
|
+
if (release) {
|
|
1399
|
+
release();
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/**
|
|
1405
|
+
* The same heal lock, awaited WITHOUT blocking: the watch process takes it
|
|
1406
|
+
* around its own `npm run prepare` spawn, and must keep serving its other
|
|
1407
|
+
* watchers while a peer's prepare finishes (never sleepSync here).
|
|
1408
|
+
* @param {string} realDir - The package directory.
|
|
1409
|
+
* @returns {Promise<function>} Resolves to a release function — always safe to
|
|
1410
|
+
* call, including when the wait timed out and the caller proceeds unlocked.
|
|
1411
|
+
*/
|
|
1412
|
+
function acquireHealLock(realDir) {
|
|
1413
|
+
const lockDir = path.join(realDir, HEAL_LOCK);
|
|
1414
|
+
const unlocked = () => {};
|
|
1415
|
+
if (!ensureHealLockParent(lockDir)) {
|
|
1416
|
+
return Promise.resolve(unlocked);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
const deadline = Date.now() + HEAL_LOCK_WAIT_MS;
|
|
1420
|
+
return new Promise((resolve) => {
|
|
1421
|
+
const attempt = () => {
|
|
1422
|
+
if (tryHealLock(lockDir)) {
|
|
1423
|
+
resolve(healLockReleaser(lockDir));
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
if (Date.now() >= deadline) {
|
|
1427
|
+
resolve(unlocked);
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
setTimeout(attempt, HEAL_LOCK_POLL_MS);
|
|
1431
|
+
};
|
|
1432
|
+
attempt();
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* Resolve a package's REAL on-disk directory as seen from fromDir, mirroring
|
|
1438
|
+
* Node resolution. require.resolve of '<name>/package.json' first (works when
|
|
1439
|
+
* there is no exports map — @omega.js/backend), then a manual node_modules
|
|
1440
|
+
* walk-up (exports maps rarely expose './package.json', and a missing dist
|
|
1441
|
+
* makes the '.' entry unresolvable — exactly the stale case this exists for).
|
|
1442
|
+
* @param {string} packageName - Package name (e.g. '@omega.js/web').
|
|
1443
|
+
* @param {string} fromDir - Directory to resolve from.
|
|
1444
|
+
* @returns {string|null} Real package directory, or null when unresolvable.
|
|
1445
|
+
*/
|
|
1446
|
+
function resolvePackageRealDir(packageName, fromDir) {
|
|
1447
|
+
try {
|
|
1448
|
+
const req = createRequire(path.join(fromDir, 'package.json'));
|
|
1449
|
+
return fs.realpathSync(path.dirname(req.resolve(`${packageName}/package.json`)));
|
|
1450
|
+
} catch (e) {
|
|
1451
|
+
// Fall through to the manual walk
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
let dir = path.resolve(fromDir);
|
|
1455
|
+
while (true) {
|
|
1456
|
+
const candidate = path.join(dir, 'node_modules', packageName);
|
|
1457
|
+
if (fs.existsSync(path.join(candidate, 'package.json'))) {
|
|
1458
|
+
try {
|
|
1459
|
+
return fs.realpathSync(candidate);
|
|
1460
|
+
} catch (e) {
|
|
1461
|
+
return null;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
const parent = path.dirname(dir);
|
|
1465
|
+
if (parent === dir) {
|
|
1466
|
+
return null;
|
|
1467
|
+
}
|
|
1468
|
+
dir = parent;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/**
|
|
1473
|
+
* Whether a resolved directory is a SOURCE CHECKOUT rather than a registry
|
|
1474
|
+
* install: a real path still inside node_modules is installed, and an
|
|
1475
|
+
* unresolvable package is nothing at all.
|
|
1476
|
+
* @param {string|null} realDir - Resolved package directory.
|
|
1477
|
+
* @returns {boolean}
|
|
1478
|
+
*/
|
|
1479
|
+
function isLocalCheckout(realDir) {
|
|
1480
|
+
return Boolean(realDir) && !realDir.split(path.sep).includes('node_modules');
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
/**
|
|
1484
|
+
* Read a package's manifest.
|
|
1485
|
+
* @param {string} realDir - The package directory.
|
|
1486
|
+
* @returns {object|null} The manifest, or null when it is unreadable.
|
|
1487
|
+
*/
|
|
1488
|
+
function readPackageManifest(realDir) {
|
|
1489
|
+
try {
|
|
1490
|
+
return JSON.parse(fs.readFileSync(path.join(realDir, 'package.json'), 'utf8'));
|
|
1491
|
+
} catch (e) {
|
|
1492
|
+
return null;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// One watch-down warning per process, however many packages get checked.
|
|
1497
|
+
let warnedWatchDown = false;
|
|
1498
|
+
|
|
1499
|
+
/**
|
|
1500
|
+
* Warn once that the monorepo's src→dist watch is not running: every framework
|
|
1501
|
+
* CLI boots through the freshness path, so all five surfaces get this for free.
|
|
1502
|
+
* The boot's own heal (#398) does not retire it — it is a boot-time repair of a
|
|
1503
|
+
* STALE dist, and this warning fires on a fresh one too, so the wording states
|
|
1504
|
+
* the standing deal rather than claiming a heal this boot may not have run.
|
|
1505
|
+
* @param {string} monorepoRoot - Monorepo root path.
|
|
1506
|
+
*/
|
|
1507
|
+
function warnWatchDown(monorepoRoot) {
|
|
1508
|
+
if (warnedWatchDown) {
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
warnedWatchDown = true;
|
|
1512
|
+
console.warn(`omega: the monorepo src→dist watch is not running: a stale linked dist is healed once at boot, but nothing rebuilds it as you edit — run \`npm start\` in ${monorepoRoot} for live rebuilds`);
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
/**
|
|
1516
|
+
* The loud stop for a monorepo-linked package whose dist is missing or stale
|
|
1517
|
+
* while its watch RUNS (#281) — the only way a 'stale-linked' result happens
|
|
1518
|
+
* (#398). A consumer build does not fix it: that watch owns the build and is
|
|
1519
|
+
* mid-flight, so the build says what is unbuilt, where, and who is building it.
|
|
1520
|
+
* @param {object} result - A 'stale-linked' ensureFreshLocalDist result.
|
|
1521
|
+
* @returns {string} The multi-line message, stderr-bound.
|
|
1522
|
+
*/
|
|
1523
|
+
function staleLinkedMessage(result) {
|
|
1524
|
+
return [
|
|
1525
|
+
'',
|
|
1526
|
+
`omega: ${result.packageName} is linked into the omega monorepo and its dist is not built (${result.reason}).`,
|
|
1527
|
+
`omega: linked packages are read-only to consumer builds, so this build will not rebuild ${result.dir}.`,
|
|
1528
|
+
`omega: the monorepo watch (\`npm start\` in ${result.monorepoRoot}) is running but has not landed that build yet: give it a moment, then re-run this command.`,
|
|
1529
|
+
'',
|
|
1530
|
+
].join('\n');
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
* The loud stop's sibling for a monorepo-linked package whose watch-down heal
|
|
1535
|
+
* FAILED (#398). That heal was the dist's only chance — no watch is coming to
|
|
1536
|
+
* land the build — so continuing would serve code nobody built, the silent
|
|
1537
|
+
* fallback #281 forbids. Say which prepare broke and where to run it by hand.
|
|
1538
|
+
* @param {object} result - A 'heal-failed' ensureFreshLocalDist result.
|
|
1539
|
+
* @returns {string} The multi-line message, stderr-bound.
|
|
1540
|
+
*/
|
|
1541
|
+
function healFailedMessage(result) {
|
|
1542
|
+
return [
|
|
1543
|
+
'',
|
|
1544
|
+
`omega: ${result.packageName} is linked into the omega monorepo with a stale dist (${result.reason}), and this boot's rebuild FAILED (${result.failure}).`,
|
|
1545
|
+
`omega: the monorepo watch is not running, so nothing else will build it — running on that dist would serve code nobody built.`,
|
|
1546
|
+
`omega: run \`npm run prepare\` in ${result.dir} by hand, fix what it reports, then re-run this command (or start the watch: \`npm start\` in ${result.monorepoRoot}).`,
|
|
1547
|
+
'',
|
|
1548
|
+
].join('\n');
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
/**
|
|
1552
|
+
* Check that a locally-linked framework's dist is at least as new as its src —
|
|
1553
|
+
* reporting or rebuilding it when a src edit landed without a prepare, so
|
|
1554
|
+
* consumers never run stale dist code just because nobody remembered to
|
|
1555
|
+
* rebuild (Ian's ask, 2026-07-20).
|
|
1556
|
+
*
|
|
1557
|
+
* Only acts on SOURCE CHECKOUTS: a registry install (real path still inside
|
|
1558
|
+
* node_modules) is untouched. Staleness is PER-FILE evidence (distStaleReason),
|
|
1559
|
+
* never a whole-tree mtime compare. A live monorepo watch buys a bounded grace
|
|
1560
|
+
* for its in-flight copy — then the check reports anyway, because a watcher that
|
|
1561
|
+
* silently died is exactly what let a stale dist serve for a day (#195).
|
|
1562
|
+
*
|
|
1563
|
+
* A link into the OMEGA MONOREPO is read-only here WHILE ITS WATCH RUNS (#281,
|
|
1564
|
+
* narrowed by #398): that checkout is shared (a fleet of agents, several brands,
|
|
1565
|
+
* the monorepo's own processes), so a consumer build never prepares out from
|
|
1566
|
+
* under the process that owns the build — a prepare purges the dist a sibling is
|
|
1567
|
+
* mid-require on, and refetches network caches, all of it invisible to git.
|
|
1568
|
+
* Those come back 'stale-linked' and freshnessBoot stops the invocation loudly.
|
|
1569
|
+
* With the watch DOWN nothing else owns that rebuild, so it heals like any other
|
|
1570
|
+
* checkout — the heal lock still serializes the CLIs booting on it, and the
|
|
1571
|
+
* largest concurrent writer is by definition not running. Every heal takes the
|
|
1572
|
+
* package's cross-process lock, so N booting CLIs produce one build. When THAT
|
|
1573
|
+
* heal fails it comes back 'heal-failed', another loud stop: no watch is coming
|
|
1574
|
+
* to land the build, so there is no stale dist to fall back onto. A plain
|
|
1575
|
+
* checkout's failed rebuild stays 'rebuild-failed' and continues — its dist is
|
|
1576
|
+
* the developer's own to fix, and nothing shared depends on this boot stopping.
|
|
1577
|
+
* @param {object} options
|
|
1578
|
+
* @param {string} options.packageName - The package to check (e.g. '@omega.js/web').
|
|
1579
|
+
* @param {string} [options.fromDir] - Resolution origin (default process.cwd()).
|
|
1580
|
+
* @returns {{status: 'skipped'|'reexec-guard'|'registry'|'not-buildable'|'fresh'|'rebuilt'|'stale-linked'|'rebuild-failed'|'heal-failed', by?: 'self'|'watch'|'peer', packageName: string, dir?: string, reason?: string, failure?: string, monorepoRoot?: string}}
|
|
1581
|
+
* Every HEALED outcome is 'rebuilt' — `by` only says who built it — because
|
|
1582
|
+
* whoever built it, this process booted from the pre-heal dist and must
|
|
1583
|
+
* re-exec.
|
|
1584
|
+
*/
|
|
1585
|
+
function ensureFreshLocalDist(options) {
|
|
1586
|
+
const { packageName, fromDir = process.cwd() } = options;
|
|
1587
|
+
|
|
1588
|
+
// Env seams: test/CI hatch, and the loop guard freshnessBoot sets on re-exec
|
|
1589
|
+
if (process.env.OMEGA_SKIP_FRESHNESS) {
|
|
1590
|
+
return { status: 'skipped', packageName };
|
|
1591
|
+
}
|
|
1592
|
+
if (process.env.OMEGA_FRESH_REEXEC) {
|
|
1593
|
+
return { status: 'reexec-guard', packageName };
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
const realDir = resolvePackageRealDir(packageName, fromDir);
|
|
1597
|
+
if (!isLocalCheckout(realDir)) {
|
|
1598
|
+
// Unresolvable (bootstrap dir, nothing installed) or a real registry
|
|
1599
|
+
// install — either way there is no local source checkout to freshen
|
|
1600
|
+
return { status: 'registry', packageName, dir: realDir || undefined };
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
const pkg = readPackageManifest(realDir);
|
|
1604
|
+
const srcDir = path.join(realDir, 'src');
|
|
1605
|
+
if (!fs.existsSync(srcDir) || !(pkg && pkg.scripts && pkg.scripts.prepare)) {
|
|
1606
|
+
return { status: 'not-buildable', packageName, dir: realDir };
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
const distDir = path.join(realDir, 'dist');
|
|
1610
|
+
const monorepoRoot = path.dirname(path.dirname(realDir));
|
|
1611
|
+
const inMonorepo = isMonorepoRoot(monorepoRoot);
|
|
1612
|
+
const watchPid = inMonorepo ? readLiveWatchPid(monorepoRoot) : null;
|
|
1613
|
+
|
|
1614
|
+
// Liveness: a monorepo-linked CLI booting with no watch running still heals
|
|
1615
|
+
// itself below, but src→dist propagation is only as live as this boot — say
|
|
1616
|
+
// so once, loudly, instead of letting a dead watcher go unnoticed for a day
|
|
1617
|
+
if (inMonorepo && !watchPid) {
|
|
1618
|
+
warnWatchDown(monorepoRoot);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
const staleness = () => distStaleReason({ srcDir, distDir, pkg, monorepoRoot, inMonorepo });
|
|
1622
|
+
let reason = staleness();
|
|
1623
|
+
if (!reason) {
|
|
1624
|
+
return { status: 'fresh', packageName, dir: realDir };
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// A live watch owns the copy — give its in-flight write a bounded moment to
|
|
1628
|
+
// land, then heal anyway (blind trust of the lock is what masked #195)
|
|
1629
|
+
if (watchPid) {
|
|
1630
|
+
const deadline = Date.now() + WATCH_GRACE_MS;
|
|
1631
|
+
while (Date.now() < deadline) {
|
|
1632
|
+
sleepSync(WATCH_POLL_MS);
|
|
1633
|
+
reason = staleness();
|
|
1634
|
+
if (!reason) {
|
|
1635
|
+
console.log(`\x1b[2momega: local ${packageName} dist was stale — the monorepo watch rebuilt it\x1b[0m`);
|
|
1636
|
+
return { status: 'rebuilt', by: 'watch', packageName, dir: realDir };
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// Linked into the monorepo WITH its watch running: that watch owns this dist
|
|
1642
|
+
// and will land the build itself, so a consumer build stays out of it (#281).
|
|
1643
|
+
// Report it and let the boot stop the invocation. Watch DOWN falls through to
|
|
1644
|
+
// the heal below — nothing else owns the rebuild then (#398).
|
|
1645
|
+
if (inMonorepo && watchPid) {
|
|
1646
|
+
return { status: 'stale-linked', packageName, dir: realDir, reason, monorepoRoot };
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
return withHealLock(realDir, () => {
|
|
1650
|
+
// The lock loser arrives after the winner's prepare: re-check instead of
|
|
1651
|
+
// building again — running twice equals running once. Still a HEAL, so it
|
|
1652
|
+
// re-execs like any other: this process's modules came from the stale dist
|
|
1653
|
+
reason = staleness();
|
|
1654
|
+
if (!reason) {
|
|
1655
|
+
return { status: 'rebuilt', by: 'peer', packageName, dir: realDir };
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
console.log(`omega: local ${packageName} dist is stale (${reason}) — rebuilding…`);
|
|
1659
|
+
const result = spawnSync('npm', ['run', 'prepare'], {
|
|
1660
|
+
cwd: realDir,
|
|
1661
|
+
stdio: 'inherit',
|
|
1662
|
+
shell: process.platform === 'win32',
|
|
1663
|
+
});
|
|
1664
|
+
if (result.status !== 0) {
|
|
1665
|
+
const failure = `npm run prepare exited ${result.status === null ? String(result.error && result.error.message || 'spawn error') : result.status}`;
|
|
1666
|
+
if (inMonorepo) {
|
|
1667
|
+
// The watch is down (a live one never reaches this heal), so this
|
|
1668
|
+
// prepare was the dist's only chance — the boot stops on it (#398)
|
|
1669
|
+
return { status: 'heal-failed', packageName, dir: realDir, reason, failure, monorepoRoot };
|
|
1670
|
+
}
|
|
1671
|
+
console.warn(`omega: rebuild failed (${failure} in ${realDir}) — continuing on the stale dist`);
|
|
1672
|
+
return { status: 'rebuild-failed', packageName, dir: realDir };
|
|
1673
|
+
}
|
|
1674
|
+
return { status: 'rebuilt', by: 'self', packageName, dir: realDir };
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
/**
|
|
1679
|
+
* The ordered list of packages ONE boot checks: the CLI host plus every
|
|
1680
|
+
* `@omega.js/*` RUNTIME dependency reachable from it through local checkouts.
|
|
1681
|
+
*
|
|
1682
|
+
* The host alone is not enough: web's bundle carries `@omega.js/client`'s dist
|
|
1683
|
+
* verbatim, so a boot that heals web and stops there still serves the browser a
|
|
1684
|
+
* stale client (#198). devDependencies are never walked — nothing a consumer
|
|
1685
|
+
* RUNS comes from them. A dep that resolves into node_modules (registry
|
|
1686
|
+
* install) or nowhere stays on the list — ensureFreshLocalDist classifies it —
|
|
1687
|
+
* but its own deps do not, because there is no local source under it to go
|
|
1688
|
+
* stale. Each dep resolves from ITS depender's real dir, so the walk follows
|
|
1689
|
+
* the actual node_modules chain rather than guessing a layout.
|
|
1690
|
+
*
|
|
1691
|
+
* Order is post-order — DEPS FIRST, host LAST — so a host prepare that consumes
|
|
1692
|
+
* a dep's artifacts (vendored copies, bundled dists) sees the freshened ones.
|
|
1693
|
+
* @param {object} options
|
|
1694
|
+
* @param {string} options.packageName - The CLI host package.
|
|
1695
|
+
* @param {string} [options.fromDir] - Resolution origin (default process.cwd()).
|
|
1696
|
+
* @returns {Array<{packageName: string, fromDir: string}>} ensureFreshLocalDist arguments, in check order.
|
|
1697
|
+
*/
|
|
1698
|
+
function freshnessCheckList(options) {
|
|
1699
|
+
const { packageName, fromDir = process.cwd() } = options;
|
|
1700
|
+
const list = [];
|
|
1701
|
+
const visited = new Set();
|
|
1702
|
+
|
|
1703
|
+
const walk = (name, origin) => {
|
|
1704
|
+
if (visited.has(name)) {
|
|
1705
|
+
return; // A cycle or a diamond — one check per package either way
|
|
1706
|
+
}
|
|
1707
|
+
visited.add(name);
|
|
1708
|
+
|
|
1709
|
+
const realDir = resolvePackageRealDir(name, origin);
|
|
1710
|
+
if (isLocalCheckout(realDir)) {
|
|
1711
|
+
const dependencies = (readPackageManifest(realDir) || {}).dependencies || {};
|
|
1712
|
+
for (const dep of Object.keys(dependencies)) {
|
|
1713
|
+
if (dep.startsWith(SCOPE)) {
|
|
1714
|
+
walk(dep, realDir);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
list.push({ packageName: name, fromDir: origin });
|
|
1720
|
+
};
|
|
1721
|
+
|
|
1722
|
+
walk(packageName, fromDir);
|
|
1723
|
+
return list;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// One freshness scan per process per module instance — a dispatcher hop that
|
|
1727
|
+
// re-enters the SAME framework's run() must not scan (or rebuild) twice.
|
|
1728
|
+
let freshnessBootRan = false;
|
|
1729
|
+
|
|
1730
|
+
/**
|
|
1731
|
+
* CLI-boot wiring for ensureFreshLocalDist: every framework run() calls this
|
|
1732
|
+
* first, naming the HOST package — and the boot checks the host's whole
|
|
1733
|
+
* `@omega.js/*` runtime dependency closure (freshnessCheckList), deps first,
|
|
1734
|
+
* because the host's dist is not the only code this invocation serves (#198).
|
|
1735
|
+
* On a rebuild ANYWHERE in that list, the running process booted from the STALE
|
|
1736
|
+
* dist — so the same invocation re-execs ONCE (OMEGA_FRESH_REEXEC guards the
|
|
1737
|
+
* loop) and this process exits with the child's status. That is EVERY heal,
|
|
1738
|
+
* whoever built it (`by: self|watch|peer`): a dist the watch or a peer CLI
|
|
1739
|
+
* rebuilt leaves this process just as stale as one it rebuilt itself.
|
|
1740
|
+
*
|
|
1741
|
+
* A 'stale-linked' entry is the LOUD STOP (#281): the package lives in the
|
|
1742
|
+
* shared monorepo with its watch mid-build, nothing here may build over it, and
|
|
1743
|
+
* running on a dist nobody built is the silent fallback the ruling forbids — so
|
|
1744
|
+
* the boot prints what is unbuilt and exits 1 before the verb runs. With that
|
|
1745
|
+
* watch down the entry heals instead (#398) and re-execs like any other — and a
|
|
1746
|
+
* heal that FAILS ('heal-failed') is the same loud stop for the same reason:
|
|
1747
|
+
* with no watch coming, the stale dist is not something to fall back onto. Every
|
|
1748
|
+
* other outcome returns the HOST's result and the boot continues.
|
|
1749
|
+
* @param {object} options - Same as ensureFreshLocalDist (packageName = the host).
|
|
1750
|
+
* @returns {{status: string, by?: string, packageName: string, dir?: string}}
|
|
1751
|
+
*/
|
|
1752
|
+
function freshnessBoot(options) {
|
|
1753
|
+
if (freshnessBootRan) {
|
|
1754
|
+
return { status: 'already-checked', packageName: options.packageName };
|
|
1755
|
+
}
|
|
1756
|
+
freshnessBootRan = true;
|
|
1757
|
+
|
|
1758
|
+
// The whole list runs even after a heal: a dep rebuild can leave its
|
|
1759
|
+
// dependents stale in turn, and the re-exec below covers them all at once
|
|
1760
|
+
let result = null;
|
|
1761
|
+
let healed = false;
|
|
1762
|
+
for (const entry of freshnessCheckList(options)) {
|
|
1763
|
+
const entryResult = ensureFreshLocalDist(entry);
|
|
1764
|
+
if (entryResult.status === 'stale-linked') {
|
|
1765
|
+
console.error(staleLinkedMessage(entryResult));
|
|
1766
|
+
process.exit(1);
|
|
1767
|
+
}
|
|
1768
|
+
if (entryResult.status === 'heal-failed') {
|
|
1769
|
+
console.error(healFailedMessage(entryResult));
|
|
1770
|
+
process.exit(1);
|
|
1771
|
+
}
|
|
1772
|
+
healed = healed || entryResult.status === 'rebuilt';
|
|
1773
|
+
if (entry.packageName === options.packageName) {
|
|
1774
|
+
result = entryResult; // The host is last — its result is the boot's
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
if (!healed) {
|
|
1779
|
+
return result;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
const child = spawnSync(process.execPath, process.argv.slice(1), {
|
|
1783
|
+
stdio: 'inherit',
|
|
1784
|
+
env: Object.assign({}, process.env, { OMEGA_FRESH_REEXEC: '1' }),
|
|
1785
|
+
});
|
|
1786
|
+
process.exit(child.status === null ? 1 : child.status);
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
/**
|
|
1790
|
+
* ONE freshness pass for a FAN-OUT, run BEFORE it (#340): the brand-root
|
|
1791
|
+
* `omega dev` sweeps every lane's host closure in this process, then spawns the
|
|
1792
|
+
* lanes — each lane's own freshnessBoot then finds nothing to do.
|
|
1793
|
+
*
|
|
1794
|
+
* Why it must be hoisted: a heal runs `npm run prepare`, which PURGES dist
|
|
1795
|
+
* before recopying, and a lane's framework bin opens by loading its own built
|
|
1796
|
+
* dist entry. Two lanes checking themselves in parallel means one lane's purge
|
|
1797
|
+
* window is the other lane's require — the backend lane died MODULE_NOT_FOUND
|
|
1798
|
+
* dispatching through the web package the web lane was mid-rebuild on. The heal lock does
|
|
1799
|
+
* not help: it serializes BUILDS, and the casualty is a bystander require.
|
|
1800
|
+
*
|
|
1801
|
+
* The lanes' closures overlap almost entirely (they share the brand's
|
|
1802
|
+
* node_modules), so entries are deduped by the package DIRECTORY they resolve
|
|
1803
|
+
* to — one check per real package, deps-first per host (freshnessCheckList).
|
|
1804
|
+
*
|
|
1805
|
+
* Nothing here re-execs, unlike freshnessBoot: what this pass heals is the
|
|
1806
|
+
* LANES' code, which no lane has loaded yet — this process's own closure was
|
|
1807
|
+
* settled by its CLI-boot freshnessBoot. That is also why the re-exec loop
|
|
1808
|
+
* guard is lifted for the pass: a dev boot that re-execed after healing its own
|
|
1809
|
+
* host must still hoist the lanes' check, or the fan-out races exactly as before.
|
|
1810
|
+
*
|
|
1811
|
+
* A 'stale-linked' entry (a monorepo link whose watch is mid-build, read-only
|
|
1812
|
+
* here — #281) is REPORTED, message and all, and returned: the caller stops the
|
|
1813
|
+
* boot before any lane spawns, instead of each lane discovering it separately,
|
|
1814
|
+
* half-booted. A 'heal-failed' entry (a monorepo link whose watch-down heal
|
|
1815
|
+
* broke — #398) comes back the same way, for the same reason.
|
|
1816
|
+
* @param {object} options
|
|
1817
|
+
* @param {Array<{packageName: string, fromDir: string}>} options.hosts - One entry
|
|
1818
|
+
* per lane: the framework package that lane runs, and the target dir it resolves from.
|
|
1819
|
+
* @returns {{checked: object[], healed: string[], staleLinked: object[], healFailed: object[], failed: string[]}}
|
|
1820
|
+
* checked = every ensureFreshLocalDist result, in check order; staleLinked and
|
|
1821
|
+
* healFailed are the caller's two stop conditions, failed is the plain
|
|
1822
|
+
* checkouts that warned and carried on.
|
|
1823
|
+
*/
|
|
1824
|
+
function freshnessSweep(options) {
|
|
1825
|
+
const { hosts = [] } = options;
|
|
1826
|
+
|
|
1827
|
+
const entries = [];
|
|
1828
|
+
const seen = new Set();
|
|
1829
|
+
for (const host of hosts) {
|
|
1830
|
+
for (const entry of freshnessCheckList(host)) {
|
|
1831
|
+
// The resolved dir is the identity — the same package reached from two
|
|
1832
|
+
// targets is ONE dist. Unresolvable entries keep their own key so
|
|
1833
|
+
// ensureFreshLocalDist still gets to classify them.
|
|
1834
|
+
const key = resolvePackageRealDir(entry.packageName, entry.fromDir) || `${entry.packageName}\u0000${entry.fromDir}`;
|
|
1835
|
+
if (seen.has(key)) {
|
|
1836
|
+
continue;
|
|
1837
|
+
}
|
|
1838
|
+
seen.add(key);
|
|
1839
|
+
entries.push(entry);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
const priorReexec = process.env.OMEGA_FRESH_REEXEC;
|
|
1844
|
+
delete process.env.OMEGA_FRESH_REEXEC;
|
|
1845
|
+
|
|
1846
|
+
const checked = [];
|
|
1847
|
+
try {
|
|
1848
|
+
for (const entry of entries) {
|
|
1849
|
+
checked.push(ensureFreshLocalDist(entry));
|
|
1850
|
+
}
|
|
1851
|
+
} finally {
|
|
1852
|
+
if (priorReexec !== undefined) {
|
|
1853
|
+
process.env.OMEGA_FRESH_REEXEC = priorReexec;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
const staleLinked = checked.filter((result) => result.status === 'stale-linked');
|
|
1858
|
+
for (const result of staleLinked) {
|
|
1859
|
+
console.error(staleLinkedMessage(result));
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
const healFailed = checked.filter((result) => result.status === 'heal-failed');
|
|
1863
|
+
for (const result of healFailed) {
|
|
1864
|
+
console.error(healFailedMessage(result));
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
return {
|
|
1868
|
+
checked,
|
|
1869
|
+
healed: checked.filter((result) => result.status === 'rebuilt').map((result) => result.packageName),
|
|
1870
|
+
staleLinked,
|
|
1871
|
+
healFailed,
|
|
1872
|
+
failed: checked.filter((result) => result.status === 'rebuild-failed').map((result) => result.packageName),
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// Exports
|
|
1877
|
+
module.exports = {
|
|
1878
|
+
DEFAULT_MONOREPO,
|
|
1879
|
+
WATCH_LOCK,
|
|
1880
|
+
WATCH_LOG,
|
|
1881
|
+
VENDOR_PROPAGATION_LOCK,
|
|
1882
|
+
HEAL_LOCK,
|
|
1883
|
+
isMonorepoRoot,
|
|
1884
|
+
resolveMonorepoRoot,
|
|
1885
|
+
packageDir,
|
|
1886
|
+
findBrandRoot,
|
|
1887
|
+
discoverTargets,
|
|
1888
|
+
frameworkPackagesOf,
|
|
1889
|
+
linkLocalPackages,
|
|
1890
|
+
restoreRegistrySpecs,
|
|
1891
|
+
resolveLinkedMonorepo,
|
|
1892
|
+
createWatchReadyTracker,
|
|
1893
|
+
awaitRunningWatchReady,
|
|
1894
|
+
startMonorepoWatch,
|
|
1895
|
+
startVendorPropagation,
|
|
1896
|
+
vendorPropagationActive,
|
|
1897
|
+
readLiveWatchPid,
|
|
1898
|
+
acquireWatchLock,
|
|
1899
|
+
releaseWatchLock,
|
|
1900
|
+
FRESHNESS_VENDORABLES,
|
|
1901
|
+
ensureFreshLocalDist,
|
|
1902
|
+
freshnessCheckList,
|
|
1903
|
+
freshnessBoot,
|
|
1904
|
+
freshnessSweep,
|
|
1905
|
+
};
|