diff --git a/.github/workflows/devcontainer-build.yml b/.github/workflows/devcontainer-build.yml index 65cbe309b45..70ec21a19f0 100644 --- a/.github/workflows/devcontainer-build.yml +++ b/.github/workflows/devcontainer-build.yml @@ -3,21 +3,29 @@ name: Devcontainer image # Builds the dev container base image used by .devcontainer/devcontainer.json # (VS Code Dev Containers + GitHub Codespaces) and publishes it to GHCR. # -# Currently triggered only by manual dispatch — auto-triggers on -# pull_request and push were removed pending a one-time TryGhost org-admin -# bootstrap of the ghost-devcontainer package on GHCR. Until then, -# GITHUB_TOKEN can't create the package on first push (`denied: -# permission_denied: write_package`), and we don't want every PR's CI to -# show a noisy failed check. -# -# Once the package shell exists at github.com/orgs/TryGhost/packages and -# is linked to this repo with Actions write access, restore the -# `pull_request:` and `push:` triggers in a tiny follow-up PR so the -# image rebuilds automatically on changes to docker/ghost-dev/** and -# pnpm-install inputs. +# Triggers only on push to main (path-filtered) and manual dispatch. PRs +# don't trigger this workflow — devcontainer.json references the :latest +# tag, so pre-publishing per-PR images would just produce unused tags. +# A broken Dockerfile change in a PR will surface when the merge to main +# fires this workflow; the previously-good :latest stays in place until +# the next successful run. on: workflow_dispatch: + push: + branches: [main] + paths: + - 'docker/ghost-dev/**' + - '.github/workflows/devcontainer-build.yml' + - '.github/scripts/**' + - '.github/hooks/**' + - 'package.json' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml' + - '.npmrc' + - 'ghost/core/package.json' + - 'ghost/i18n/package.json' + - 'ghost/parse-email-address/package.json' permissions: contents: read @@ -27,7 +35,7 @@ jobs: publish: name: Build & push runs-on: ubuntu-latest - if: github.repository == 'TryGhost/Ghost' + if: github.repository == 'TryGhost/Ghost' && github.ref == 'refs/heads/main' concurrency: group: devcontainer-image-${{ github.ref }} cancel-in-progress: true @@ -35,6 +43,9 @@ jobs: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Set up QEMU + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 @@ -50,6 +61,7 @@ jobs: with: context: . file: docker/ghost-dev/Dockerfile + platforms: linux/amd64,linux/arm64 push: true tags: | ghcr.io/tryghost/ghost-devcontainer:latest diff --git a/.gitignore b/.gitignore index 3746f57e62e..3708c671f19 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ yarn-error.log* # Runtime data pids +.ghost-dev *.pid *.seed *.pid.lock diff --git a/apps/announcement-bar/package.json b/apps/announcement-bar/package.json index adbb895d7ff..2c9904b11c8 100644 --- a/apps/announcement-bar/package.json +++ b/apps/announcement-bar/package.json @@ -18,7 +18,7 @@ "react-dom": "17.0.2" }, "scripts": { - "dev": "concurrently \"vite preview -l silent\" \"pnpm build:watch\"", + "dev": "concurrently --kill-others --names preview,build \"vite preview -l silent\" \"pnpm build:watch\"", "build": "vite build", "build:watch": "vite build --watch", "test": "vitest run", diff --git a/apps/comments-ui/package.json b/apps/comments-ui/package.json index aa7b79bfba3..7863636c26b 100644 --- a/apps/comments-ui/package.json +++ b/apps/comments-ui/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/comments-ui", - "version": "1.4.16", + "version": "1.4.17", "license": "MIT", "repository": "https://github.com/TryGhost/Ghost", "author": "Ghost Foundation", @@ -15,7 +15,7 @@ "registry": "https://registry.npmjs.org/" }, "scripts": { - "dev": "concurrently \"pnpm preview --host -l silent\" \"pnpm build:watch\"", + "dev": "concurrently --kill-others --names preview,build \"pnpm preview --host -l silent\" \"pnpm build:watch\"", "dev:test": "vite build && vite preview --port 7175", "build": "vite build", "build:watch": "vite build --watch", @@ -69,16 +69,15 @@ "@vitejs/plugin-react": "catalog:", "@vitest/coverage-v8": "catalog:", "autoprefixer": "10.4.21", - "bson-objectid": "2.0.4", + "bson-objectid": "catalog:", "concurrently": "catalog:", "eslint": "catalog:", "eslint-plugin-i18next": "6.1.4", - "eslint-plugin-react-hooks": "4.6.2", - "eslint-plugin-react-refresh": "catalog:", "eslint-plugin-tailwindcss": "3.18.2", "jsdom": "catalog:", "moment": "2.30.1", "postcss": "catalog:", + "postcss-import": "16.1.1", "sinon": "21.1.1", "tailwindcss": "3.4.18", "vite": "catalog:", diff --git a/apps/portal/package.json b/apps/portal/package.json index f7fa8838a74..972651af4c5 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -14,7 +14,7 @@ "registry": "https://registry.npmjs.org/" }, "scripts": { - "dev": "concurrently \"pnpm preview -l silent\" \"pnpm build:watch\"", + "dev": "concurrently --kill-others --names preview,build \"pnpm preview -l silent\" \"pnpm build:watch\"", "build": "vite build", "build:watch": "vite build --watch", "preview": "vite preview", diff --git a/apps/signup-form/package.json b/apps/signup-form/package.json index 18bbcf90bfc..e9ddadfb153 100644 --- a/apps/signup-form/package.json +++ b/apps/signup-form/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/signup-form", - "version": "0.3.23", + "version": "0.3.25", "license": "MIT", "repository": "https://github.com/TryGhost/Ghost", "author": "Ghost Foundation", @@ -14,8 +14,8 @@ "registry": "https://registry.npmjs.org/" }, "scripts": { - "dev": "concurrently \"vite --port 6173\" \"vite preview -l silent\" \"vite build --watch\"", - "preview": "concurrently \"vite preview -l silent\" \"vite build --watch\"", + "dev": "concurrently --kill-others --names dev,preview,build \"vite --port 6173\" \"vite preview -l silent\" \"vite build --watch\"", + "preview": "concurrently --kill-others --names preview,build \"vite preview -l silent\" \"vite build --watch\"", "dev:test": "vite build && vite preview --port 6175", "build": "tsc && vite build", "lint": "pnpm run lint:js", @@ -48,8 +48,6 @@ "autoprefixer": "10.4.21", "concurrently": "catalog:", "eslint": "catalog:", - "eslint-plugin-react-hooks": "4.6.2", - "eslint-plugin-react-refresh": "catalog:", "eslint-plugin-tailwindcss": "3.18.2", "jsdom": "catalog:", "postcss": "catalog:", diff --git a/apps/signup-form/vite.config.mts b/apps/signup-form/vite.config.mts index 67cbe93fdca..a675294dbb8 100644 --- a/apps/signup-form/vite.config.mts +++ b/apps/signup-form/vite.config.mts @@ -67,7 +67,6 @@ export default (function viteConfig() { test: { globals: true, // required for @testing-library/jest-dom extensions environment: 'jsdom', - setupFiles: './test/test-setup.js', include: ['./test/unit/*'], testTimeout: process.env.TIMEOUT ? parseInt(process.env.TIMEOUT) : 10000, ...(process.env.CI && { // https://github.com/vitest-dev/vitest/issues/1674 diff --git a/apps/sodo-search/package.json b/apps/sodo-search/package.json index 43b729f14f3..ac76efdc447 100644 --- a/apps/sodo-search/package.json +++ b/apps/sodo-search/package.json @@ -21,7 +21,7 @@ "react-dom": "17.0.2" }, "scripts": { - "dev": "concurrently \"vite preview -l silent\" \"pnpm build:watch\" \"pnpm tailwind\"", + "dev": "concurrently --kill-others --names preview,build,tailwind \"vite preview -l silent\" \"pnpm build:watch\" \"pnpm tailwind\"", "build": "vite build && pnpm tailwind:base", "build:watch": "vite build --watch", "tailwind": "pnpm tailwind:base --watch ", diff --git a/ghost/admin/package.json b/ghost/admin/package.json index f5f5d2021d9..05e09fb9fbe 100644 --- a/ghost/admin/package.json +++ b/ghost/admin/package.json @@ -1,6 +1,6 @@ { "name": "ghost-admin", - "version": "6.39.1-rc.0", + "version": "6.40.0-rc.0", "description": "Ember.js admin client for Ghost", "author": "Ghost Foundation", "homepage": "http://ghost.org", diff --git a/ghost/core/core/server/api/endpoints/automated-emails.js b/ghost/core/core/server/api/endpoints/automated-emails.js index 839a2750006..94d91b1aa12 100644 --- a/ghost/core/core/server/api/endpoints/automated-emails.js +++ b/ghost/core/core/server/api/endpoints/automated-emails.js @@ -9,7 +9,7 @@ const messages = { }; // NOTE: This file is in a transitionary state. The `automated_emails` database table was split into -// `welcome_email_automations` (automation metadata: status, name, slug) and +// `automations` (automation metadata: status, name, slug) and // `welcome_email_automated_emails` (email content: subject, lexical, sender fields). This controller // acts as a facade that joins/splits data between those two models while preserving the original // `automated_emails` API shape externally. @@ -51,7 +51,7 @@ const controller = { ], permissions: true, async query(frame) { - const result = await models.WelcomeEmailAutomation.findPage({ + const result = await models.Automation.findPage({ ...frame.options, withRelated: ['welcomeEmailAutomatedEmail'] }); @@ -75,7 +75,7 @@ const controller = { ], permissions: true, async query(frame) { - const model = await models.WelcomeEmailAutomation.findOne(frame.data, { + const model = await models.Automation.findOne(frame.data, { ...frame.options, withRelated: ['welcomeEmailAutomatedEmail'] }); @@ -102,7 +102,7 @@ const controller = { const automationData = _.pick(data, AUTOMATION_FIELDS); return models.Base.transaction(async (transacting) => { - const automation = await models.WelcomeEmailAutomation.add(automationData, {...frame.options, transacting}); + const automation = await models.Automation.add(automationData, {...frame.options, transacting}); const email = await models.WelcomeEmailAutomatedEmail.add( { ...emailData, @@ -139,7 +139,7 @@ const controller = { const automationData = _.pick(data, AUTOMATION_FIELDS); return models.Base.transaction(async (transacting) => { - let automation = await models.WelcomeEmailAutomation.findOne({id: frame.options.id}, { + let automation = await models.Automation.findOne({id: frame.options.id}, { transacting, withRelated: ['welcomeEmailAutomatedEmail'] }); @@ -159,7 +159,7 @@ const controller = { } if (Object.keys(automationData).length > 0) { - automation = await models.WelcomeEmailAutomation.edit(automationData, { + automation = await models.Automation.edit(automationData, { ...frame.options, transacting }); diff --git a/ghost/core/core/server/data/exporter/table-lists.js b/ghost/core/core/server/data/exporter/table-lists.js index 1a001c0743d..065e49d4676 100644 --- a/ghost/core/core/server/data/exporter/table-lists.js +++ b/ghost/core/core/server/data/exporter/table-lists.js @@ -58,7 +58,7 @@ const BACKUP_TABLES = [ 'recommendation_subscribe_events', 'outbox', 'gifts', - 'welcome_email_automations', + 'automations', 'welcome_email_automation_runs', 'welcome_email_automated_emails' ]; diff --git a/ghost/core/core/server/data/migrations/versions/6.40/2026-05-18-19-32-45-rename-welcome-email-automations-table-to-automations.js b/ghost/core/core/server/data/migrations/versions/6.40/2026-05-18-19-32-45-rename-welcome-email-automations-table-to-automations.js new file mode 100644 index 00000000000..9bf541362f2 --- /dev/null +++ b/ghost/core/core/server/data/migrations/versions/6.40/2026-05-18-19-32-45-rename-welcome-email-automations-table-to-automations.js @@ -0,0 +1,42 @@ +const logging = require('@tryghost/logging'); +const {createNonTransactionalMigration} = require('../../utils'); + +const FROM_TABLE = 'welcome_email_automations'; +const TO_TABLE = 'automations'; + +module.exports = createNonTransactionalMigration( + async function up(connection) { + const fromExists = await connection.schema.hasTable(FROM_TABLE); + const toExists = await connection.schema.hasTable(TO_TABLE); + + if (toExists) { + logging.warn(`Skipping renaming table: ${TO_TABLE} already exists`); + return; + } + + if (!fromExists) { + logging.warn(`Skipping renaming table: ${FROM_TABLE} does not exist`); + return; + } + + logging.info(`Renaming table: ${FROM_TABLE} -> ${TO_TABLE}`); + await connection.schema.renameTable(FROM_TABLE, TO_TABLE); + }, + async function down(connection) { + const fromExists = await connection.schema.hasTable(FROM_TABLE); + const toExists = await connection.schema.hasTable(TO_TABLE); + + if (fromExists) { + logging.warn(`Skipping renaming table: ${FROM_TABLE} already exists`); + return; + } + + if (!toExists) { + logging.warn(`Skipping renaming table: ${TO_TABLE} does not exist`); + return; + } + + logging.info(`Renaming table: ${TO_TABLE} -> ${FROM_TABLE}`); + await connection.schema.renameTable(TO_TABLE, FROM_TABLE); + } +); diff --git a/ghost/core/core/server/data/schema/schema.js b/ghost/core/core/server/data/schema/schema.js index e5fa6a599be..077a23aa9f4 100644 --- a/ghost/core/core/server/data/schema/schema.js +++ b/ghost/core/core/server/data/schema/schema.js @@ -1171,7 +1171,7 @@ module.exports = { created_at: {type: 'dateTime', nullable: false}, updated_at: {type: 'dateTime', nullable: true} }, - welcome_email_automations: { + automations: { id: {type: 'string', maxlength: 24, nullable: false, primary: true}, status: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'inactive', validations: {isIn: [['active', 'inactive']]}}, name: {type: 'string', maxlength: 191, nullable: false, unique: true}, @@ -1181,7 +1181,7 @@ module.exports = { }, welcome_email_automated_emails: { id: {type: 'string', maxlength: 24, nullable: false, primary: true}, - welcome_email_automation_id: {type: 'string', maxlength: 24, nullable: false, references: 'welcome_email_automations.id', constraintName: 'weae_automation_id_foreign', cascadeDelete: true}, + welcome_email_automation_id: {type: 'string', maxlength: 24, nullable: false, references: 'automations.id', constraintName: 'weae_automation_id_foreign', cascadeDelete: true}, next_welcome_email_automated_email_id: {type: 'string', maxlength: 24, nullable: true, references: 'welcome_email_automated_emails.id', constraintName: 'weae_next_email_id_foreign', cascadeDelete: false}, delay_days: {type: 'integer', nullable: false, unsigned: true}, subject: {type: 'string', maxlength: 300, nullable: false}, @@ -1195,7 +1195,7 @@ module.exports = { }, welcome_email_automation_runs: { id: {type: 'string', maxlength: 24, nullable: false, primary: true}, - welcome_email_automation_id: {type: 'string', maxlength: 24, nullable: false, references: 'welcome_email_automations.id', constraintName: 'wear_automation_id_foreign', cascadeDelete: true}, + welcome_email_automation_id: {type: 'string', maxlength: 24, nullable: false, references: 'automations.id', constraintName: 'wear_automation_id_foreign', cascadeDelete: true}, member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', constraintName: 'wear_member_id_foreign', cascadeDelete: true}, next_welcome_email_automated_email_id: {type: 'string', maxlength: 24, nullable: true, references: 'welcome_email_automated_emails.id', constraintName: 'wear_next_email_id_foreign', cascadeDelete: false}, ready_at: {type: 'dateTime', nullable: true}, diff --git a/ghost/core/core/server/models/welcome-email-automation.js b/ghost/core/core/server/models/automation.js similarity index 89% rename from ghost/core/core/server/models/welcome-email-automation.js rename to ghost/core/core/server/models/automation.js index 5e981e20006..8c51fc4abf8 100644 --- a/ghost/core/core/server/models/welcome-email-automation.js +++ b/ghost/core/core/server/models/automation.js @@ -4,8 +4,8 @@ const {MEMBER_WELCOME_EMAIL_SLUGS} = require('../services/member-welcome-emails/ const MEMBER_WELCOME_EMAIL_SLUG_SET = new Set(Object.values(MEMBER_WELCOME_EMAIL_SLUGS)); -const WelcomeEmailAutomation = ghostBookshelf.Model.extend({ - tableName: 'welcome_email_automations', +const Automation = ghostBookshelf.Model.extend({ + tableName: 'automations', defaults() { return { @@ -57,5 +57,5 @@ const WelcomeEmailAutomation = ghostBookshelf.Model.extend({ }); module.exports = { - WelcomeEmailAutomation: ghostBookshelf.model('WelcomeEmailAutomation', WelcomeEmailAutomation) + Automation: ghostBookshelf.model('Automation', Automation) }; diff --git a/ghost/core/core/server/models/index.js b/ghost/core/core/server/models/index.js index 4f590faf85d..dd9653cca3d 100644 --- a/ghost/core/core/server/models/index.js +++ b/ghost/core/core/server/models/index.js @@ -9,6 +9,7 @@ const {Action} = require('./action'); const {ApiKey, ApiKeys} = require('./api-key'); const {Author, Authors} = require('./author'); const {AutomatedEmailRecipient, AutomatedEmailRecipients} = require('./automated-email-recipient'); +const {Automation} = require('./automation'); const {Benefit, Benefits} = require('./benefit'); const {CollectionPost} = require('./collection-post'); const {Collection} = require('./collection'); @@ -75,7 +76,6 @@ const {User, Users} = require('./user'); const {Webhook, Webhooks} = require('./webhook'); const {WelcomeEmailAutomatedEmail} = require('./welcome-email-automated-email'); const {WelcomeEmailAutomationRun} = require('./welcome-email-automation-run'); -const {WelcomeEmailAutomation} = require('./welcome-email-automation'); // enable event listeners require('./base/listeners'); @@ -91,6 +91,7 @@ exports.Author = Author; exports.Authors = Authors; exports.AutomatedEmailRecipient = AutomatedEmailRecipient; exports.AutomatedEmailRecipients = AutomatedEmailRecipients; +exports.Automation = Automation; exports.Benefit = Benefit; exports.Benefits = Benefits; exports.CollectionPost = CollectionPost; @@ -188,7 +189,6 @@ exports.Webhook = Webhook; exports.Webhooks = Webhooks; exports.WelcomeEmailAutomatedEmail = WelcomeEmailAutomatedEmail; exports.WelcomeEmailAutomationRun = WelcomeEmailAutomationRun; -exports.WelcomeEmailAutomation = WelcomeEmailAutomation; function init() { // `init` used to be a necessary call, but now it's unnecessary. diff --git a/ghost/core/core/server/models/welcome-email-automated-email.js b/ghost/core/core/server/models/welcome-email-automated-email.js index c1d75e88a93..b5cb8a17d89 100644 --- a/ghost/core/core/server/models/welcome-email-automated-email.js +++ b/ghost/core/core/server/models/welcome-email-automated-email.js @@ -14,8 +14,8 @@ const WelcomeEmailAutomatedEmail = ghostBookshelf.Model.extend({ return this.belongsTo('EmailDesignSetting', 'email_design_setting_id', 'id'); }, - welcomeEmailAutomation() { - return this.belongsTo('WelcomeEmailAutomation', 'welcome_email_automation_id', 'id'); + automation() { + return this.belongsTo('Automation', 'welcome_email_automation_id', 'id'); }, nextWelcomeEmailAutomatedEmail() { diff --git a/ghost/core/core/server/models/welcome-email-automation-run.js b/ghost/core/core/server/models/welcome-email-automation-run.js index d91cd62c5e6..5db236e8c0a 100644 --- a/ghost/core/core/server/models/welcome-email-automation-run.js +++ b/ghost/core/core/server/models/welcome-email-automation-run.js @@ -9,8 +9,8 @@ const WelcomeEmailAutomationRun = ghostBookshelf.Model.extend({ }; }, - welcomeEmailAutomation() { - return this.belongsTo('WelcomeEmailAutomation', 'welcome_email_automation_id', 'id'); + automation() { + return this.belongsTo('Automation', 'welcome_email_automation_id', 'id'); }, member() { diff --git a/ghost/core/core/server/services/auth/session/session-service.js b/ghost/core/core/server/services/auth/session/session-service.js index f6c4a400d5c..5ae627990e1 100644 --- a/ghost/core/core/server/services/auth/session/session-service.js +++ b/ghost/core/core/server/services/auth/session/session-service.js @@ -56,9 +56,9 @@ const AUTH_CODE_CHALLENGE_BYTES = 16; * @param {(req: Req) => string} deps.getOriginOfRequest * @param {((key: 'require_email_mfa') => boolean) & ((key: 'admin_session_secret' | 'title') => string)} deps.getSettingsCache * @param {() => string} deps.getBlogLogo - * @param {import('../../core/core/server/services/mail').GhostMailer} deps.mailer - * @param {import('../../core/core/server/services/i18n').t} deps.t - * @param {import('../../core/core/shared/url-utils')} deps.urlUtils + * @param {import('../../mail').GhostMailer} deps.mailer + * @param {import('../../i18n').t} deps.t + * @param {import('../../../../shared/url-utils')} deps.urlUtils * @param {() => boolean} deps.isStaffDeviceVerificationDisabled * @returns {SessionService} */ diff --git a/ghost/core/core/server/services/automations/poll.js b/ghost/core/core/server/services/automations/poll.js index a4ad9a79d8b..792a6621ebb 100644 --- a/ghost/core/core/server/services/automations/poll.js +++ b/ghost/core/core/server/services/automations/poll.js @@ -55,7 +55,7 @@ async function fetchAndLockRuns() { return await db.knex.transaction(async (trx) => { /** @type {Run[]} */ const runs = await trx('welcome_email_automation_runs as r') - .join('welcome_email_automations as a', 'r.welcome_email_automation_id', 'a.id') + .join('automations as a', 'r.welcome_email_automation_id', 'a.id') .join('welcome_email_automated_emails as e', 'r.next_welcome_email_automated_email_id', 'e.id') .whereNotNull('r.next_welcome_email_automated_email_id') .where('r.ready_at', '<=', now) diff --git a/ghost/core/core/server/services/i18n.js b/ghost/core/core/server/services/i18n.js index 1f487c51038..47cf1f89a47 100644 --- a/ghost/core/core/server/services/i18n.js +++ b/ghost/core/core/server/services/i18n.js @@ -1,6 +1,5 @@ const debug = require('@tryghost/debug')('i18n'); -/** @type {import('i18next').i18n} */ let i18nInstance; module.exports.init = function () { diff --git a/ghost/core/core/server/services/member-welcome-emails/service.js b/ghost/core/core/server/services/member-welcome-emails/service.js index de14fe7184e..81c00f9f934 100644 --- a/ghost/core/core/server/services/member-welcome-emails/service.js +++ b/ghost/core/core/server/services/member-welcome-emails/service.js @@ -9,7 +9,7 @@ const emailAddressService = require('../email-address'); const settingsHelpers = require('../settings-helpers'); const EmailAddressParser = require('../email-address/email-address-parser'); const mail = require('../mail'); -const {WelcomeEmailAutomation, WelcomeEmailAutomatedEmail, Newsletter} = require('../../models'); +const {Automation, WelcomeEmailAutomatedEmail, Newsletter} = require('../../models'); const MemberWelcomeEmailRenderer = require('./member-welcome-email-renderer'); const {MEMBER_WELCOME_EMAIL_LOG_KEY, MEMBER_WELCOME_EMAIL_TAG, MEMBER_WELCOME_EMAIL_SLUGS, MESSAGES} = require('./constants'); @@ -178,7 +178,7 @@ class MemberWelcomeEmailService { } async #loadWelcomeEmailsCollection() { - return WelcomeEmailAutomation.findAll({ + return Automation.findAll({ filter: WELCOME_EMAIL_FILTER, withRelated: ['welcomeEmailAutomatedEmail'] }); @@ -336,7 +336,7 @@ class MemberWelcomeEmailService { this.#defaultNewsletterSenderOptions = await this.#getDefaultNewsletterSenderOptions(); for (const [memberStatus, slug] of Object.entries(MEMBER_WELCOME_EMAIL_SLUGS)) { - const row = await WelcomeEmailAutomation.findOne({slug}, { + const row = await Automation.findOne({slug}, { withRelated: ['welcomeEmailAutomatedEmail', 'welcomeEmailAutomatedEmail.emailDesignSetting'] }); @@ -427,7 +427,7 @@ class MemberWelcomeEmailService { return false; } - const row = await WelcomeEmailAutomation.findOne({slug}, {withRelated: ['welcomeEmailAutomatedEmail']}); + const row = await Automation.findOne({slug}, {withRelated: ['welcomeEmailAutomatedEmail']}); if (!row) { return false; } @@ -437,7 +437,7 @@ class MemberWelcomeEmailService { async #renderWelcomeEmailPreview({automatedEmailId, subject, lexical, memberEmail = 'jamie@example.com'}) { // Still validate the automated email exists (for permission purposes) - const automation = await WelcomeEmailAutomation.findOne({id: automatedEmailId}, { + const automation = await Automation.findOne({id: automatedEmailId}, { withRelated: ['welcomeEmailAutomatedEmail', 'welcomeEmailAutomatedEmail.emailDesignSetting'] }); const automatedEmail = automation?.related('welcomeEmailAutomatedEmail'); diff --git a/ghost/core/core/server/services/members/api.js b/ghost/core/core/server/services/members/api.js index a7296b756bd..6ee7956cf95 100644 --- a/ghost/core/core/server/services/members/api.js +++ b/ghost/core/core/server/services/members/api.js @@ -242,7 +242,7 @@ function createApiInstance(config) { MemberFeedback: models.MemberFeedback, EmailSpamComplaintEvent: models.EmailSpamComplaintEvent, Outbox: models.Outbox, - WelcomeEmailAutomation: models.WelcomeEmailAutomation, + Automation: models.Automation, WelcomeEmailAutomationRun: models.WelcomeEmailAutomationRun, AutomatedEmailRecipient: models.AutomatedEmailRecipient, Gift: models.Gift diff --git a/ghost/core/core/server/services/members/members-api/members-api.js b/ghost/core/core/server/services/members/members-api/members-api.js index 90f81558443..4ab360f1625 100644 --- a/ghost/core/core/server/services/members/members-api/members-api.js +++ b/ghost/core/core/server/services/members/members-api/members-api.js @@ -65,7 +65,7 @@ module.exports = function MembersAPI({ Comment, MemberFeedback, Outbox, - WelcomeEmailAutomation, + Automation, WelcomeEmailAutomationRun, AutomatedEmailRecipient, Gift @@ -104,7 +104,7 @@ module.exports = function MembersAPI({ tokenService, newslettersService, productRepository, - WelcomeEmailAutomation, + Automation, WelcomeEmailAutomationRun, Member, MemberNewsletter, diff --git a/ghost/core/core/server/services/members/members-api/repositories/event-repository.js b/ghost/core/core/server/services/members/members-api/repositories/event-repository.js index a9500220853..048d80adf1d 100644 --- a/ghost/core/core/server/services/members/members-api/repositories/event-repository.js +++ b/ghost/core/core/server/services/members/members-api/repositories/event-repository.js @@ -1051,7 +1051,7 @@ module.exports = class EventRepository { async getAutomatedEmailSentEvents(options = {}, filter) { options = { ...options, - withRelated: ['member', 'automatedEmail.welcomeEmailAutomation'], + withRelated: ['member', 'automatedEmail.automation'], filter: 'custom:true', useBasicCount: true, mongoTransformer: chainTransformers( @@ -1067,10 +1067,10 @@ module.exports = class EventRepository { const data = models.map((model) => { const automatedEmail = model.related('automatedEmail'); - const automation = automatedEmail.related('welcomeEmailAutomation'); + const automation = automatedEmail.related('automation'); if (!automation || !automation.id) { throw new errors.InternalServerError({ - message: `Automated email recipient ${model.id} has no associated welcome email automation` + message: `Automated email recipient ${model.id} has no associated automation` }); } diff --git a/ghost/core/core/server/services/members/members-api/repositories/member-repository.js b/ghost/core/core/server/services/members/members-api/repositories/member-repository.js index 98f40ae38bf..cd2f8d03dfd 100644 --- a/ghost/core/core/server/services/members/members-api/repositories/member-repository.js +++ b/ghost/core/core/server/services/members/members-api/repositories/member-repository.js @@ -61,12 +61,12 @@ module.exports = class MemberRepository { * @param {any} deps.StripeCustomerSubscription * @param {any} deps.OfferRedemption * @param {any} deps.Outbox - * @param {import('../../services/stripe-api')} deps.stripeAPIService + * @param {import('../../../stripe/stripe-api')} deps.stripeAPIService * @param {any} deps.productRepository * @param {any} deps.offersAPI * @param {ITokenService} deps.tokenService * @param {any} deps.newslettersService - * @param {any} deps.WelcomeEmailAutomation + * @param {any} deps.Automation * @param {any} deps.WelcomeEmailAutomationRun */ constructor({ @@ -87,7 +87,7 @@ module.exports = class MemberRepository { offersAPI, tokenService, newslettersService, - WelcomeEmailAutomation, + Automation, WelcomeEmailAutomationRun }) { this._Member = Member; @@ -107,7 +107,7 @@ module.exports = class MemberRepository { this._offersAPI = offersAPI; this.tokenService = tokenService; this._newslettersService = newslettersService; - this._WelcomeEmailAutomation = WelcomeEmailAutomation; + this._Automation = Automation; this._WelcomeEmailAutomationRun = WelcomeEmailAutomationRun; DomainEvents.subscribe(OfferRedemptionEvent, async function (event) { @@ -189,11 +189,11 @@ module.exports = class MemberRepository { * @param {object} [options] bookshelf options (transacting, context, etc.) */ async enqueueWelcomeEmailRun(memberId, slug, options = {}) { - if (!this._WelcomeEmailAutomation || !this._WelcomeEmailAutomationRun) { + if (!this._Automation || !this._WelcomeEmailAutomationRun) { return null; } - const automation = await this._WelcomeEmailAutomation.findOne( + const automation = await this._Automation.findOne( {slug}, {...options, withRelated: ['welcomeEmailAutomatedEmail']} ); diff --git a/ghost/core/core/server/services/oembed/nft-oembed-provider.js b/ghost/core/core/server/services/oembed/nft-oembed-provider.js index 8f1ec570f51..45a59bc984a 100644 --- a/ghost/core/core/server/services/oembed/nft-oembed-provider.js +++ b/ghost/core/core/server/services/oembed/nft-oembed-provider.js @@ -1,6 +1,6 @@ /** - * @typedef {import('./oembed').ICustomProvider} ICustomProvider - * @typedef {import('./oembed').IExternalRequest} IExternalRequest + * @typedef {import('./oembed-service').ICustomProvider} ICustomProvider + * @typedef {import('./oembed-service').IExternalRequest} IExternalRequest */ const OPENSEA_ETH_PATH_REGEX = /^\/assets\/ethereum\/(0x[a-f0-9]+)\/(\d+)/; diff --git a/ghost/core/core/server/services/oembed/twitter-oembed-provider.js b/ghost/core/core/server/services/oembed/twitter-oembed-provider.js index 78cf0750b2d..2c440b2b45a 100644 --- a/ghost/core/core/server/services/oembed/twitter-oembed-provider.js +++ b/ghost/core/core/server/services/oembed/twitter-oembed-provider.js @@ -1,8 +1,8 @@ const logging = require('@tryghost/logging'); /** - * @typedef {import('./oembed').ICustomProvider} ICustomProvider - * @typedef {import('./oembed').IExternalRequest} IExternalRequest + * @typedef {import('./oembed-service').ICustomProvider} ICustomProvider + * @typedef {import('./oembed-service').IExternalRequest} IExternalRequest */ const TWITTER_PATH_REGEX = /\/status\/(\d+)/; diff --git a/ghost/core/core/server/services/offers/application/unique-checker.js b/ghost/core/core/server/services/offers/application/unique-checker.js index 6a6f57b4003..1058f37bf21 100644 --- a/ghost/core/core/server/services/offers/application/unique-checker.js +++ b/ghost/core/core/server/services/offers/application/unique-checker.js @@ -1,6 +1,6 @@ class UniqueChecker { /** - * @param {import('./OfferRepository')} repository + * @param {import('../offer-bookshelf-repository')} repository * @param {import('knex').Transaction} transaction */ constructor(repository, transaction) { diff --git a/ghost/core/core/server/services/outbox/handlers/member-created.js b/ghost/core/core/server/services/outbox/handlers/member-created.js index ad80129d746..7e5b9a23fcf 100644 --- a/ghost/core/core/server/services/outbox/handlers/member-created.js +++ b/ghost/core/core/server/services/outbox/handlers/member-created.js @@ -1,7 +1,7 @@ const {OUTBOX_LOG_KEY} = require('../jobs/lib/constants'); const memberWelcomeEmailService = require('../../member-welcome-emails/service'); const logging = require('@tryghost/logging'); -const {WelcomeEmailAutomation, AutomatedEmailRecipient} = require('../../../models'); +const {Automation, AutomatedEmailRecipient} = require('../../../models'); const {MEMBER_WELCOME_EMAIL_SLUGS} = require('../../member-welcome-emails/constants'); const LOG_KEY = `${OUTBOX_LOG_KEY}[MEMBER-WELCOME-EMAIL]`; @@ -21,7 +21,7 @@ async function handle({payload}) { return; } - const automation = await WelcomeEmailAutomation.findOne({slug}, {withRelated: ['welcomeEmailAutomatedEmail']}); + const automation = await Automation.findOne({slug}, {withRelated: ['welcomeEmailAutomatedEmail']}); if (!automation) { logging.warn({ system: { diff --git a/ghost/core/core/server/services/stripe/services/webhook/subscription-event-service.js b/ghost/core/core/server/services/stripe/services/webhook/subscription-event-service.js index 9c17f1b3e22..5154a4a88f8 100644 --- a/ghost/core/core/server/services/stripe/services/webhook/subscription-event-service.js +++ b/ghost/core/core/server/services/stripe/services/webhook/subscription-event-service.js @@ -12,7 +12,7 @@ const _ = require('lodash'); module.exports = class SubscriptionEventService { /** * @param {object} deps - * @param {import('../../repositories/MemberRepository')} deps.memberRepository + * @param {import('../../../members/members-api/repositories/member-repository')} deps.memberRepository */ constructor(deps) { this.deps = deps; diff --git a/ghost/core/core/shared/sentry-knex-tracing-integration.js b/ghost/core/core/shared/sentry-knex-tracing-integration.js index a5dfc807a1a..a08b2fb50ca 100644 --- a/ghost/core/core/shared/sentry-knex-tracing-integration.js +++ b/ghost/core/core/shared/sentry-knex-tracing-integration.js @@ -2,14 +2,12 @@ * @typedef {import('knex').Knex.Client} KnexClient */ -/** - * @typedef {import('@sentry/types').Integration} SentryIntegration - */ - /** * Sentry Knex tracing integration * - * @implements {SentryIntegration} + * Implements the Sentry `Integration` interface. The type is not annotated via + * JSDoc because `Integration` is only exported from `@sentry/types`, which is + * not a direct dependency of ghost/core (only `@sentry/node` is). */ class SentryKnexTracingIntegration { static id = 'Knex'; diff --git a/ghost/core/package.json b/ghost/core/package.json index 461a889420d..f154b3f33dc 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -1,6 +1,6 @@ { "name": "ghost", - "version": "6.39.1-rc.0", + "version": "6.40.0-rc.0", "description": "The professional publishing platform", "author": "Ghost Foundation", "homepage": "https://ghost.org", @@ -145,7 +145,7 @@ "bookshelf": "1.2.0", "bookshelf-relations": "2.8.0", "brute-knex": "4.0.1", - "bson-objectid": "2.0.4", + "bson-objectid": "catalog:", "cache-manager": "4.1.0", "cache-manager-ioredis": "2.1.0", "chalk": "4.1.2", diff --git a/ghost/core/test/e2e-api/admin/automated-emails.test.js b/ghost/core/test/e2e-api/admin/automated-emails.test.js index a8e9518d0ea..8076efd5251 100644 --- a/ghost/core/test/e2e-api/admin/automated-emails.test.js +++ b/ghost/core/test/e2e-api/admin/automated-emails.test.js @@ -40,7 +40,7 @@ describe('Automated Emails API', function () { beforeEach(async function () { await dbUtils.truncate('brute'); await dbUtils.truncate('welcome_email_automated_emails'); - await dbUtils.truncate('welcome_email_automations'); + await dbUtils.truncate('automations'); }); describe('Browse', function () { diff --git a/ghost/core/test/e2e-api/members/gift-subscriptions.test.js b/ghost/core/test/e2e-api/members/gift-subscriptions.test.js index b979336fa1d..4bafcd0e1a0 100644 --- a/ghost/core/test/e2e-api/members/gift-subscriptions.test.js +++ b/ghost/core/test/e2e-api/members/gift-subscriptions.test.js @@ -760,7 +760,7 @@ describe('Gift Subscriptions', function () { {slug: 'default-automated-email'}, {require: true} ); - freeWelcomeAutomation = await models.WelcomeEmailAutomation.add({ + freeWelcomeAutomation = await models.Automation.add({ name: 'Free welcome email', slug: 'member-welcome-email-free', status: 'active' @@ -772,7 +772,7 @@ describe('Gift Subscriptions', function () { lexical: JSON.stringify({root: {children: [{type: 'paragraph', children: [{text: 'Welcome'}]}]}}), email_design_setting_id: emailDesignSetting.id }); - paidWelcomeAutomation = await models.WelcomeEmailAutomation.add({ + paidWelcomeAutomation = await models.Automation.add({ name: 'Paid welcome email', slug: 'member-welcome-email-paid', status: 'active' @@ -862,7 +862,7 @@ describe('Gift Subscriptions', function () { for (const run of runs.models) { await models.WelcomeEmailAutomationRun.destroy({id: run.id}); } - await models.WelcomeEmailAutomation.destroy({id: automation.id}); + await models.Automation.destroy({id: automation.id}); } } }); @@ -892,7 +892,7 @@ describe('Gift Subscriptions', function () { {slug: 'default-automated-email'}, {require: true} ); - freeWelcomeAutomation = await models.WelcomeEmailAutomation.add({ + freeWelcomeAutomation = await models.Automation.add({ name: 'Free welcome email', slug: 'member-welcome-email-free', status: 'active' @@ -904,7 +904,7 @@ describe('Gift Subscriptions', function () { lexical: JSON.stringify({root: {children: [{type: 'paragraph', children: [{text: 'Welcome'}]}]}}), email_design_setting_id: emailDesignSetting.id }); - paidWelcomeAutomation = await models.WelcomeEmailAutomation.add({ + paidWelcomeAutomation = await models.Automation.add({ name: 'Paid welcome email', slug: 'member-welcome-email-paid', status: 'active' @@ -998,7 +998,7 @@ describe('Gift Subscriptions', function () { for (const run of runs.models) { await models.WelcomeEmailAutomationRun.destroy({id: run.id}); } - await models.WelcomeEmailAutomation.destroy({id: automation.id}); + await models.Automation.destroy({id: automation.id}); } } }); @@ -1178,7 +1178,7 @@ describe('Gift Subscriptions', function () { {slug: 'default-automated-email'}, {require: true} ); - paidWelcomeAutomation = await models.WelcomeEmailAutomation.add({ + paidWelcomeAutomation = await models.Automation.add({ name: 'Paid welcome email', slug: 'member-welcome-email-paid', status: 'active' @@ -1226,7 +1226,7 @@ describe('Gift Subscriptions', function () { for (const run of runs.models) { await models.WelcomeEmailAutomationRun.destroy({id: run.id}); } - await models.WelcomeEmailAutomation.destroy({id: paidWelcomeAutomation.id}); + await models.Automation.destroy({id: paidWelcomeAutomation.id}); } } }); diff --git a/ghost/core/test/integration/exporter/exporter.test.js b/ghost/core/test/integration/exporter/exporter.test.js index 8600c3957e2..18969ae1905 100644 --- a/ghost/core/test/integration/exporter/exporter.test.js +++ b/ghost/core/test/integration/exporter/exporter.test.js @@ -26,6 +26,7 @@ describe('Exporter', function () { 'actions', 'api_keys', 'automated_email_recipients', + 'automations', 'benefits', 'brute', 'collections', @@ -102,8 +103,7 @@ describe('Exporter', function () { 'users', 'webhooks', 'welcome_email_automated_emails', - 'welcome_email_automation_runs', - 'welcome_email_automations' + 'welcome_email_automation_runs' ]; assertExists(exportData); diff --git a/ghost/core/test/integration/jobs/process-outbox.test.js b/ghost/core/test/integration/jobs/process-outbox.test.js index 65b44e496bf..9cb6c4b56e8 100644 --- a/ghost/core/test/integration/jobs/process-outbox.test.js +++ b/ghost/core/test/integration/jobs/process-outbox.test.js @@ -28,7 +28,7 @@ describe('Process Outbox Job', function () { afterEach(async function () { sinon.restore(); await db.knex('outbox').del(); - await db.knex('welcome_email_automations').where('slug', MEMBER_WELCOME_EMAIL_SLUGS.free).del(); + await db.knex('automations').where('slug', MEMBER_WELCOME_EMAIL_SLUGS.free).del(); try { await jobService.removeJob(JOB_NAME); } catch (err) { @@ -65,7 +65,7 @@ describe('Process Outbox Job', function () { }); const automationId = ObjectId().toHexString(); - await db.knex('welcome_email_automations').insert({ + await db.knex('automations').insert({ id: automationId, status: 'active', name: 'Free Member Welcome Email', diff --git a/ghost/core/test/integration/services/automations/poll.test.js b/ghost/core/test/integration/services/automations/poll.test.js index 004258a037b..cfb88aff4be 100644 --- a/ghost/core/test/integration/services/automations/poll.test.js +++ b/ghost/core/test/integration/services/automations/poll.test.js @@ -47,7 +47,7 @@ describe('automations poll', function () { await testUtils.knex('automated_email_recipients').del(); await testUtils.knex('welcome_email_automation_runs').del(); await testUtils.knex('welcome_email_automated_emails').del(); - await testUtils.knex('welcome_email_automations').del(); + await testUtils.knex('automations').del(); await testUtils.knex('members').del(); await testUtils.knex('email_design_settings') .where('slug', 'like', 'default-automated-email-%') @@ -105,7 +105,7 @@ describe('automations poll', function () { async function createAutomation(attrs = {}) { const currentTime = new Date(); - return insert('welcome_email_automations', { + return insert('automations', { id: ObjectId().toHexString(), status: 'active', name: `Automation ${ObjectId().toHexString()}`, diff --git a/ghost/core/test/integration/services/member-welcome-emails.test.js b/ghost/core/test/integration/services/member-welcome-emails.test.js index bbd7d8733ab..62d1541277a 100644 --- a/ghost/core/test/integration/services/member-welcome-emails.test.js +++ b/ghost/core/test/integration/services/member-welcome-emails.test.js @@ -68,7 +68,7 @@ describe('Member Welcome Emails Integration', function () { }); const freeAutomationId = ObjectId().toHexString(); - await db.knex('welcome_email_automations').insert({ + await db.knex('automations').insert({ id: freeAutomationId, status: 'active', name: 'Free Member Welcome Email', @@ -86,7 +86,7 @@ describe('Member Welcome Emails Integration', function () { }); const paidAutomationId = ObjectId().toHexString(); - await db.knex('welcome_email_automations').insert({ + await db.knex('automations').insert({ id: paidAutomationId, status: 'active', name: 'Paid Member Welcome Email', @@ -120,8 +120,8 @@ describe('Member Welcome Emails Integration', function () { await db.knex('automated_email_recipients').del(); await db.knex('outbox').del(); await db.knex('members').del(); - await db.knex('welcome_email_automations').where('slug', MEMBER_WELCOME_EMAIL_SLUGS.free).del(); - await db.knex('welcome_email_automations').where('slug', MEMBER_WELCOME_EMAIL_SLUGS.paid).del(); + await db.knex('automations').where('slug', MEMBER_WELCOME_EMAIL_SLUGS.free).del(); + await db.knex('automations').where('slug', MEMBER_WELCOME_EMAIL_SLUGS.paid).del(); }); describe('Member creation with welcome emails', function () { @@ -211,13 +211,13 @@ describe('Member Welcome Emails Integration', function () { async function getAutomatedEmailBySlug(slug) { return db.knex('welcome_email_automated_emails') - .join('welcome_email_automations', 'welcome_email_automated_emails.welcome_email_automation_id', 'welcome_email_automations.id') - .where('welcome_email_automations.slug', slug) + .join('automations', 'welcome_email_automated_emails.welcome_email_automation_id', 'automations.id') + .where('automations.slug', slug) .first('welcome_email_automated_emails.*'); } it('does not send email when template is inactive', async function () { - await db.knex('welcome_email_automations') + await db.knex('automations') .where('slug', MEMBER_WELCOME_EMAIL_SLUGS.free) .update({status: 'inactive'}); @@ -243,7 +243,7 @@ describe('Member Welcome Emails Integration', function () { }); it('does not send email when no template exists', async function () { - await db.knex('welcome_email_automations').where('slug', MEMBER_WELCOME_EMAIL_SLUGS.free).del(); + await db.knex('automations').where('slug', MEMBER_WELCOME_EMAIL_SLUGS.free).del(); await models.Outbox.add({ event_type: 'MemberCreatedEvent', @@ -267,7 +267,7 @@ describe('Member Welcome Emails Integration', function () { }); it('does not send email when paid template is inactive but entry has status paid', async function () { - await db.knex('welcome_email_automations') + await db.knex('automations') .where('slug', MEMBER_WELCOME_EMAIL_SLUGS.paid) .update({status: 'inactive'}); @@ -293,7 +293,7 @@ describe('Member Welcome Emails Integration', function () { }); it('does not send email when no paid template exists but entry has status paid', async function () { - await db.knex('welcome_email_automations').where('slug', MEMBER_WELCOME_EMAIL_SLUGS.paid).del(); + await db.knex('automations').where('slug', MEMBER_WELCOME_EMAIL_SLUGS.paid).del(); await models.Outbox.add({ event_type: 'MemberCreatedEvent', @@ -350,8 +350,8 @@ describe('Member Welcome Emails Integration', function () { assert.equal(record.member_name, memberName); const automatedEmail = await db.knex('welcome_email_automated_emails') - .join('welcome_email_automations', 'welcome_email_automated_emails.welcome_email_automation_id', 'welcome_email_automations.id') - .where('welcome_email_automations.slug', MEMBER_WELCOME_EMAIL_SLUGS.free) + .join('automations', 'welcome_email_automated_emails.welcome_email_automation_id', 'automations.id') + .where('automations.slug', MEMBER_WELCOME_EMAIL_SLUGS.free) .first('welcome_email_automated_emails.id'); assert.equal(record.automated_email_id, automatedEmail.id); }); @@ -454,7 +454,7 @@ describe('Member Welcome Emails Integration', function () { }); it('uses mock member UUID when sending test welcome emails', async function () { - const automation = await db.knex('welcome_email_automations') + const automation = await db.knex('automations') .where('slug', MEMBER_WELCOME_EMAIL_SLUGS.free) .first(); @@ -492,7 +492,7 @@ describe('Member Welcome Emails Integration', function () { it('uses automated sender overrides for test welcome emails', async function () { memberWelcomeEmailService.init(); - const automation = await db.knex('welcome_email_automations') + const automation = await db.knex('automations') .where('slug', MEMBER_WELCOME_EMAIL_SLUGS.free) .first(); const automatedEmail = await getAutomatedEmailBySlug(MEMBER_WELCOME_EMAIL_SLUGS.free); diff --git a/ghost/core/test/unit/server/data/schema/integrity.test.js b/ghost/core/test/unit/server/data/schema/integrity.test.js index 715bbb91586..98c1ea82710 100644 --- a/ghost/core/test/unit/server/data/schema/integrity.test.js +++ b/ghost/core/test/unit/server/data/schema/integrity.test.js @@ -35,7 +35,7 @@ const validateRouteSettings = require('../../../../../core/server/services/route */ describe('DB version integrity', function () { // Only these variables should need updating - const currentSchemaHash = '28bf4f027ceb900645a650bfe0f00fb0'; + const currentSchemaHash = 'fda24cc02133e9e6bbae411cf282e5f8'; const currentFixturesHash = 'b76d01321e02fb99b11e7a29f91859f7'; const currentSettingsHash = '8453fbccc17256a188cc5ffed8c31945'; const currentRoutesHash = '3d180d52c663d173a6be791ef411ed01'; diff --git a/ghost/core/test/unit/server/models/welcome-email-automation.test.js b/ghost/core/test/unit/server/models/automation.test.js similarity index 86% rename from ghost/core/test/unit/server/models/welcome-email-automation.test.js rename to ghost/core/test/unit/server/models/automation.test.js index 563c785ba9f..cf12efc2d18 100644 --- a/ghost/core/test/unit/server/models/welcome-email-automation.test.js +++ b/ghost/core/test/unit/server/models/automation.test.js @@ -3,21 +3,21 @@ const sinon = require('sinon'); const models = require('../../../../core/server/models'); const logging = require('@tryghost/logging'); -describe('Unit: models/welcome-email-automation', function () { +describe('Unit: models/automation', function () { afterEach(function () { sinon.restore(); }); describe('defaults', function () { it('sets default status to inactive', function () { - const model = new models.WelcomeEmailAutomation(); + const model = new models.Automation(); const defaults = model.defaults(); assert.equal(defaults.status, 'inactive'); }); it('returns expected default values', function () { - const model = new models.WelcomeEmailAutomation(); + const model = new models.Automation(); const defaults = model.defaults(); assert.ok(defaults); @@ -29,7 +29,7 @@ describe('Unit: models/welcome-email-automation', function () { describe('onSaved', function () { it('logs when a welcome email is enabled', function () { const infoStub = sinon.stub(logging, 'info'); - const model = models.WelcomeEmailAutomation.forge({ + const model = models.Automation.forge({ id: 'test-id', slug: 'member-welcome-email-free', status: 'active' @@ -46,7 +46,7 @@ describe('Unit: models/welcome-email-automation', function () { it('logs when a welcome email is disabled', function () { const infoStub = sinon.stub(logging, 'info'); - const model = models.WelcomeEmailAutomation.forge({ + const model = models.Automation.forge({ id: 'test-id', slug: 'member-welcome-email-paid', status: 'inactive' @@ -63,7 +63,7 @@ describe('Unit: models/welcome-email-automation', function () { it('does not log for non-welcome-email slugs', function () { const infoStub = sinon.stub(logging, 'info'); - const model = models.WelcomeEmailAutomation.forge({ + const model = models.Automation.forge({ id: 'test-id', slug: 'some-other-slug', status: 'active' @@ -77,7 +77,7 @@ describe('Unit: models/welcome-email-automation', function () { it('does not log when status has not changed', function () { const infoStub = sinon.stub(logging, 'info'); - const model = models.WelcomeEmailAutomation.forge({ + const model = models.Automation.forge({ id: 'test-id', slug: 'member-welcome-email-free', status: 'active' diff --git a/ghost/core/test/unit/server/models/welcome-email-automation-run.test.js b/ghost/core/test/unit/server/models/welcome-email-automation-run.test.js index 9d67d77f43e..603ac6b78e1 100644 --- a/ghost/core/test/unit/server/models/welcome-email-automation-run.test.js +++ b/ghost/core/test/unit/server/models/welcome-email-automation-run.test.js @@ -24,9 +24,9 @@ describe('Unit: models/welcome-email-automation-run', function () { }); describe('relationships', function () { - it('has a welcomeEmailAutomation relationship', function () { + it('has an automation relationship', function () { const model = new models.WelcomeEmailAutomationRun(); - assert.equal(typeof model.welcomeEmailAutomation, 'function'); + assert.equal(typeof model.automation, 'function'); }); it('has a member relationship', function () { diff --git a/ghost/core/test/unit/server/services/members/members-api/members-api.test.js b/ghost/core/test/unit/server/services/members/members-api/members-api.test.js index 62a6bdf8a58..2d299f92adf 100644 --- a/ghost/core/test/unit/server/services/members/members-api/members-api.test.js +++ b/ghost/core/test/unit/server/services/members/members-api/members-api.test.js @@ -130,7 +130,7 @@ describe('MembersAPI', function () { Comment: {}, MemberFeedback: {}, Outbox: {}, - WelcomeEmailAutomation: {}, + Automation: {}, AutomatedEmailRecipient: {}, Gift: {} }, diff --git a/ghost/core/test/unit/server/services/members/members-api/repositories/event-repository.test.js b/ghost/core/test/unit/server/services/members/members-api/repositories/event-repository.test.js index 00671a001cd..4881604a953 100644 --- a/ghost/core/test/unit/server/services/members/members-api/repositories/event-repository.test.js +++ b/ghost/core/test/unit/server/services/members/members-api/repositories/event-repository.test.js @@ -317,7 +317,7 @@ describe('EventRepository', function () { return { id: 'ae123', related: (rel) => { - if (rel === 'welcomeEmailAutomation') { + if (rel === 'automation') { return { id: 'auto123', get: key => (key === 'slug' ? 'member-welcome-email-free' : undefined) @@ -356,7 +356,7 @@ describe('EventRepository', function () { }); sinon.assert.calledOnceWithMatch(fake, { - withRelated: ['member', 'automatedEmail.welcomeEmailAutomation'], + withRelated: ['member', 'automatedEmail.automation'], filter: 'custom:true', order: 'created_at desc, id desc' }); @@ -370,7 +370,7 @@ describe('EventRepository', function () { }); sinon.assert.calledOnceWithMatch(fake, { - withRelated: ['member', 'automatedEmail.welcomeEmailAutomation'], + withRelated: ['member', 'automatedEmail.automation'], filter: 'custom:true', order: 'created_at desc, id desc' }); @@ -385,7 +385,7 @@ describe('EventRepository', function () { }); sinon.assert.calledOnceWithMatch(fake, { - withRelated: ['member', 'automatedEmail.welcomeEmailAutomation'], + withRelated: ['member', 'automatedEmail.automation'], filter: 'custom:true', order: 'created_at desc, id desc' }); diff --git a/ghost/core/test/unit/server/services/members/members-api/repositories/member-repository.test.js b/ghost/core/test/unit/server/services/members/members-api/repositories/member-repository.test.js index 17ffe68c6b1..e12a8f68752 100644 --- a/ghost/core/test/unit/server/services/members/members-api/repositories/member-repository.test.js +++ b/ghost/core/test/unit/server/services/members/members-api/repositories/member-repository.test.js @@ -1542,7 +1542,7 @@ describe('MemberRepository', function () { let MemberStatusEvent; let MemberSubscribeEvent; let newslettersService; - let WelcomeEmailAutomation; + let Automation; const oldNodeEnv = process.env.NODE_ENV; beforeEach(function () { @@ -1600,7 +1600,7 @@ describe('MemberRepository', function () { getAll: sinon.stub().resolves([]) }; - WelcomeEmailAutomation = { + Automation = { findOne: sinon.stub().resolves({ id: 'automation_id_free', get: sinon.stub().callsFake((key) => { @@ -1633,7 +1633,7 @@ describe('MemberRepository', function () { MemberStatusEvent, MemberSubscribeEventModel: MemberSubscribeEvent, newslettersService, - WelcomeEmailAutomation, + Automation, OfferRedemption: mockOfferRedemption }); @@ -1658,7 +1658,7 @@ describe('MemberRepository', function () { MemberStatusEvent, MemberSubscribeEventModel: MemberSubscribeEvent, newslettersService, - WelcomeEmailAutomation, + Automation, OfferRedemption: mockOfferRedemption }); @@ -1683,7 +1683,7 @@ describe('MemberRepository', function () { MemberStatusEvent, MemberSubscribeEventModel: MemberSubscribeEvent, newslettersService, - WelcomeEmailAutomation, + Automation, OfferRedemption: mockOfferRedemption }); @@ -1694,7 +1694,7 @@ describe('MemberRepository', function () { }); it('does NOT create automation run when welcome email is inactive', async function () { - WelcomeEmailAutomation.findOne.resolves({ + Automation.findOne.resolves({ get: sinon.stub().callsFake((key) => { const data = {status: 'inactive'}; return data[key]; @@ -1717,7 +1717,7 @@ describe('MemberRepository', function () { MemberStatusEvent, MemberSubscribeEventModel: MemberSubscribeEvent, newslettersService, - WelcomeEmailAutomation, + Automation, OfferRedemption: mockOfferRedemption }); @@ -1738,7 +1738,7 @@ describe('MemberRepository', function () { MemberStatusEvent, MemberSubscribeEventModel: MemberSubscribeEvent, newslettersService, - WelcomeEmailAutomation, + Automation, StripeCustomer, OfferRedemption: mockOfferRedemption }); @@ -1767,7 +1767,7 @@ describe('MemberRepository', function () { // The free welcome email should NOT be sent when stripeCustomer is present sinon.assert.notCalled(WelcomeEmailAutomationRun.add); - sinon.assert.notCalled(WelcomeEmailAutomation.findOne); + sinon.assert.notCalled(Automation.findOne); sinon.assert.notCalled(Member.transaction); }); }); @@ -1782,7 +1782,7 @@ describe('MemberRepository', function () { let MemberStatusEvent; let stripeAPIService; let productRepository; - let WelcomeEmailAutomation; + let Automation; let subscriptionData; beforeEach(function () { @@ -1908,7 +1908,7 @@ describe('MemberRepository', function () { update: sinon.stub().resolves({}) }; - WelcomeEmailAutomation = { + Automation = { findOne: sinon.stub().resolves({ id: 'automation_id_paid', get: sinon.stub().callsFake((key) => { @@ -1953,7 +1953,7 @@ describe('MemberRepository', function () { MemberStatusEvent, stripeAPIService, productRepository, - WelcomeEmailAutomation, + Automation, OfferRedemption: mockOfferRedemption }); @@ -2000,7 +2000,7 @@ describe('MemberRepository', function () { MemberStatusEvent, stripeAPIService, productRepository, - WelcomeEmailAutomation, + Automation, OfferRedemption: mockOfferRedemption }); @@ -2037,7 +2037,7 @@ describe('MemberRepository', function () { }) }); - WelcomeEmailAutomation.findOne.resolves({ + Automation.findOne.resolves({ id: 'automation_id_paid', get: sinon.stub().callsFake((key) => { const data = {status: 'inactive'}; @@ -2065,7 +2065,7 @@ describe('MemberRepository', function () { MemberStatusEvent, stripeAPIService, productRepository, - WelcomeEmailAutomation, + Automation, OfferRedemption: mockOfferRedemption }); @@ -2104,7 +2104,7 @@ describe('MemberRepository', function () { MemberStatusEvent, stripeAPIService, productRepository, - WelcomeEmailAutomation, + Automation, OfferRedemption: mockOfferRedemption }); @@ -2129,7 +2129,7 @@ describe('MemberRepository', function () { let MemberStatusEvent; let MemberSubscribeEvent; let newslettersService; - let WelcomeEmailAutomation; + let Automation; let productRepository; let memberAdd; @@ -2162,7 +2162,7 @@ describe('MemberRepository', function () { getAll: sinon.stub().resolves([]) }; - WelcomeEmailAutomation = { + Automation = { findOne: sinon.stub().resolves(null) }; @@ -2179,7 +2179,7 @@ describe('MemberRepository', function () { MemberStatusEvent, MemberSubscribeEventModel: MemberSubscribeEvent, newslettersService, - WelcomeEmailAutomation, + Automation, productRepository, OfferRedemption: mockOfferRedemption }); @@ -2292,7 +2292,7 @@ describe('MemberRepository', function () { let MemberStatusEvent; let MemberSubscribeEvent; let newslettersService; - let WelcomeEmailAutomation; + let Automation; let productRepository; let stripeAPIService; let memberEdit; @@ -2365,7 +2365,7 @@ describe('MemberRepository', function () { getAll: sinon.stub().resolves([]) }; - WelcomeEmailAutomation = { + Automation = { findOne: sinon.stub().resolves(null) }; @@ -2387,7 +2387,7 @@ describe('MemberRepository', function () { MemberStatusEvent, MemberSubscribeEventModel: MemberSubscribeEvent, newslettersService, - WelcomeEmailAutomation, + Automation, productRepository, stripeAPIService, OfferRedemption: mockOfferRedemption diff --git a/ghost/core/test/unit/server/services/outbox/handlers/member-created.test.js b/ghost/core/test/unit/server/services/outbox/handlers/member-created.test.js index 96425b50bd5..9d9d2987602 100644 --- a/ghost/core/test/unit/server/services/outbox/handlers/member-created.test.js +++ b/ghost/core/test/unit/server/services/outbox/handlers/member-created.test.js @@ -6,7 +6,7 @@ const {captureLoggerOutput, findByEvent} = require('../../../../../utils/logging describe('member-created handler', function () { let handler; let memberWelcomeEmailServiceStub; - let WelcomeEmailAutomationStub; + let AutomationStub; let AutomatedEmailRecipientStub; let logCapture; @@ -19,7 +19,7 @@ describe('member-created handler', function () { } }; - WelcomeEmailAutomationStub = { + AutomationStub = { findOne: sinon.stub().resolves({ id: 'automation123', related: sinon.stub().callsFake((relation) => { @@ -37,7 +37,7 @@ describe('member-created handler', function () { logCapture = captureLoggerOutput(); handler.__set__('memberWelcomeEmailService', memberWelcomeEmailServiceStub); - handler.__set__('WelcomeEmailAutomation', WelcomeEmailAutomationStub); + handler.__set__('Automation', AutomationStub); handler.__set__('AutomatedEmailRecipient', AutomatedEmailRecipientStub); }); @@ -102,7 +102,7 @@ describe('member-created handler', function () { }); it('logs warning when no automated email found for slug', async function () { - WelcomeEmailAutomationStub.findOne.resolves(null); + AutomationStub.findOne.resolves(null); await handler.handle({ payload: { diff --git a/ghost/core/test/unit/server/services/stats/subscriptions.test.js b/ghost/core/test/unit/server/services/stats/subscriptions.test.js index 153cbf4c940..ab936e563ae 100644 --- a/ghost/core/test/unit/server/services/stats/subscriptions.test.js +++ b/ghost/core/test/unit/server/services/stats/subscriptions.test.js @@ -229,7 +229,7 @@ describe('SubscriptionStatsService', function () { * @param {string} cadence * @param {string} date * - * @returns {(result: import('../../lib/subscriptions').SubscriptionHistoryEntry) => boolean} + * @returns {(result: import('../../../../../core/server/services/stats/subscription-stats-service').SubscriptionHistoryEntry) => boolean} **/ const finder = (tier, cadence, date) => (result) => { return result.tier === tier && result.cadence === cadence && result.date === date; @@ -319,7 +319,7 @@ describe('SubscriptionStatsService', function () { * @param {string} cadence * @param {string} date * - * @returns {(result: import('../../lib/subscriptions').SubscriptionHistoryEntry) => boolean} + * @returns {(result: import('../../../../../core/server/services/stats/subscription-stats-service').SubscriptionHistoryEntry) => boolean} **/ const finder = (tier, cadence, date) => (resultItem) => { return resultItem.tier === tier && resultItem.cadence === cadence && resultItem.date === date; diff --git a/knip.json b/knip.json index 6f820c094bd..eaaa5038788 100644 --- a/knip.json +++ b/knip.json @@ -2,5 +2,8 @@ "$schema": "https://unpkg.com/knip@6/schema.json", "ignoreWorkspaces": [ "ghost/admin" + ], + "ignore": [ + "ghost/core/test/utils/fixtures/themes/**/assets/built/**" ] } diff --git a/package.json b/package.json index 36d22001d06..a66a3e997cd 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "dev:storage": "DEV_COMPOSE_FILES='-f compose.dev.storage.yaml' pnpm nx run ghost-monorepo:docker:dev", "dev:stripe": "./docker/stripe/with-stripe.sh pnpm nx run ghost-monorepo:docker:dev", "dev:all": "DEV_COMPOSE_FILES='-f compose.dev.analytics.yaml -f compose.dev.storage.yaml' ./docker/stripe/with-stripe.sh pnpm nx run ghost-monorepo:docker:dev", + "dev:supervisor": "node scripts/dev-supervisor.js", + "dev:status": "node scripts/dev-supervisor.js status", "fix": "pnpm store prune && rimraf -g '**/node_modules' && pnpm install && pnpm nx reset", "knex-migrator": "pnpm --filter ghost run knex-migrator", "setup": "pnpm install && git submodule update --init --recursive", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f23352ed50d..afa978a9a16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,6 +90,9 @@ catalogs: '@vitest/coverage-v8': specifier: 4.1.5 version: 4.1.5 + bson-objectid: + specifier: 2.0.4 + version: 2.0.4 c8: specifier: 10.1.3 version: 10.1.3 @@ -160,8 +163,8 @@ catalogs: specifier: 4.5.0 version: 4.5.0 vitest: - specifier: 4.1.2 - version: 4.1.2 + specifier: 4.1.5 + version: 4.1.5 zod: specifier: 4.1.12 version: 4.1.12 @@ -368,7 +371,7 @@ importers: version: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) apps/admin: dependencies: @@ -483,7 +486,7 @@ importers: version: 5.1.4(typescript@5.9.3)(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@22.19.18)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@22.19.18)(@vitest/coverage-v8@4.1.5)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) apps/admin-x-design-system: dependencies: @@ -655,7 +658,7 @@ importers: version: 4.5.0(rollup@4.60.0)(typescript@5.9.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) apps/admin-x-framework: dependencies: @@ -713,7 +716,7 @@ importers: version: 4.7.0(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.5(vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 4.1.5(vitest@4.1.5) c8: specifier: 'catalog:' version: 10.1.3 @@ -752,7 +755,7 @@ importers: version: 4.5.0(rollup@4.60.0)(typescript@5.9.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) apps/admin-x-settings: dependencies: @@ -879,7 +882,7 @@ importers: version: 13.15.10 '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.5(vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@22.19.18)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 4.1.5(vitest@4.1.5) eslint: specifier: 'catalog:' version: 8.57.1 @@ -903,7 +906,7 @@ importers: version: 7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@22.19.18)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@22.19.18)(@vitest/coverage-v8@4.1.5)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) apps/announcement-bar: dependencies: @@ -919,7 +922,7 @@ importers: version: 4.7.0(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.5(vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 4.1.5(vitest@4.1.5) concurrently: specifier: 'catalog:' version: 8.2.2 @@ -940,7 +943,7 @@ importers: version: 4.5.0(rollup@4.60.0)(typescript@5.9.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) apps/comments-ui: dependencies: @@ -1013,12 +1016,12 @@ importers: version: 4.7.0(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.5(vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 4.1.5(vitest@4.1.5) autoprefixer: specifier: 10.4.21 version: 10.4.21(postcss@8.5.6) bson-objectid: - specifier: 2.0.4 + specifier: 'catalog:' version: 2.0.4 concurrently: specifier: 'catalog:' @@ -1029,12 +1032,6 @@ importers: eslint-plugin-i18next: specifier: 6.1.4 version: 6.1.4 - eslint-plugin-react-hooks: - specifier: 4.6.2 - version: 4.6.2(eslint@8.57.1) - eslint-plugin-react-refresh: - specifier: 'catalog:' - version: 0.4.24(eslint@8.57.1) eslint-plugin-tailwindcss: specifier: 3.18.2 version: 3.18.2(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.3)) @@ -1047,6 +1044,9 @@ importers: postcss: specifier: 'catalog:' version: 8.5.6 + postcss-import: + specifier: 16.1.1 + version: 16.1.1(postcss@8.5.6) sinon: specifier: 21.1.1 version: 21.1.1 @@ -1061,7 +1061,7 @@ importers: version: 4.5.0(rollup@4.60.0)(typescript@5.9.3)(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) apps/portal: dependencies: @@ -1098,7 +1098,7 @@ importers: version: 4.7.0(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.5(vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 4.1.5(vitest@4.1.5) concurrently: specifier: 'catalog:' version: 8.2.2 @@ -1134,7 +1134,7 @@ importers: version: 4.5.0(rollup@4.60.0)(typescript@5.9.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) apps/posts: dependencies: @@ -1207,7 +1207,7 @@ importers: version: 18.3.28 '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.5(vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 4.1.5(vitest@4.1.5) eslint: specifier: 'catalog:' version: 8.57.1 @@ -1234,7 +1234,7 @@ importers: version: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) apps/shade: dependencies: @@ -1391,7 +1391,7 @@ importers: version: 4.7.0(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.5(vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@22.19.18)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 4.1.5(vitest@4.1.5) c8: specifier: 'catalog:' version: 10.1.3 @@ -1448,7 +1448,7 @@ importers: version: 4.5.0(rollup@4.60.0)(typescript@5.9.3)(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@22.19.18)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@22.19.18)(@vitest/coverage-v8@4.1.5)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) apps/signup-form: dependencies: @@ -1498,12 +1498,6 @@ importers: eslint: specifier: 'catalog:' version: 8.57.1 - eslint-plugin-react-hooks: - specifier: 4.6.2 - version: 4.6.2(eslint@8.57.1) - eslint-plugin-react-refresh: - specifier: 'catalog:' - version: 0.4.24(eslint@8.57.1) eslint-plugin-tailwindcss: specifier: 3.18.2 version: 3.18.2(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.3)) @@ -1530,7 +1524,7 @@ importers: version: 4.5.0(rollup@4.60.0)(typescript@5.9.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) apps/sodo-search: dependencies: @@ -1561,7 +1555,7 @@ importers: version: 4.7.0(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.5(vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 4.1.5(vitest@4.1.5) concurrently: specifier: 'catalog:' version: 8.2.2 @@ -1588,7 +1582,7 @@ importers: version: 4.5.0(rollup@4.60.0)(typescript@5.9.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) apps/stats: dependencies: @@ -1646,7 +1640,7 @@ importers: version: 2.1.4 '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.5(vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@22.19.18)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 4.1.5(vitest@4.1.5) dotenv: specifier: 'catalog:' version: 17.3.1 @@ -1676,7 +1670,7 @@ importers: version: 4.5.0(rollup@4.60.0)(typescript@5.9.3)(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@22.19.18)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@22.19.18)(@vitest/coverage-v8@4.1.5)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) e2e: devDependencies: @@ -2297,7 +2291,7 @@ importers: specifier: 4.0.1 version: 4.0.1(express@4.21.2)(mysql2@3.18.1(@types/node@22.19.18))(sqlite3@5.1.7) bson-objectid: - specifier: 2.0.4 + specifier: 'catalog:' version: 2.0.4 cache-manager: specifier: 4.1.0 @@ -2656,7 +2650,7 @@ importers: version: 8.49.0(eslint@8.57.1)(typescript@5.9.3) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.5(vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@22.19.18)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 4.1.5(vitest@4.1.5) bunyan: specifier: 1.8.15 version: 1.8.15 @@ -2686,7 +2680,7 @@ importers: version: 4.0.0 html-validate: specifier: 8.29.0 - version: 8.29.0(jest-diff@29.7.0)(jest-snapshot@29.7.0)(jest@29.7.0(@types/node@22.19.18)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21(@swc/helpers@0.5.21))(@types/node@22.19.18)(typescript@5.9.3)))(vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@22.19.18)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 8.29.0(jest-diff@29.7.0)(jest-snapshot@29.7.0)(jest@29.7.0(@types/node@22.19.18)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21(@swc/helpers@0.5.21))(@types/node@22.19.18)(typescript@5.9.3)))(vitest@4.1.5) inquirer: specifier: 8.2.7 version: 8.2.7(@types/node@22.19.18) @@ -2746,7 +2740,7 @@ importers: version: 13.12.0 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@22.19.18)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@22.19.18)(@vitest/coverage-v8@4.1.5)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) optionalDependencies: '@tryghost/html-to-mobiledoc': specifier: 3.3.1 @@ -9323,11 +9317,11 @@ packages: '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} - '@vitest/expect@4.1.2': - resolution: {integrity: sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==} + '@vitest/expect@4.1.5': + resolution: {integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==} - '@vitest/mocker@4.1.2': - resolution: {integrity: sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==} + '@vitest/mocker@4.1.5': + resolution: {integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -9340,30 +9334,24 @@ packages: '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - '@vitest/pretty-format@4.1.2': - resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==} - '@vitest/pretty-format@4.1.5': resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==} - '@vitest/runner@4.1.2': - resolution: {integrity: sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==} + '@vitest/runner@4.1.5': + resolution: {integrity: sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==} - '@vitest/snapshot@4.1.2': - resolution: {integrity: sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==} + '@vitest/snapshot@4.1.5': + resolution: {integrity: sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==} '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} - '@vitest/spy@4.1.2': - resolution: {integrity: sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==} + '@vitest/spy@4.1.5': + resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==} '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@vitest/utils@4.1.2': - resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} - '@vitest/utils@4.1.5': resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} @@ -21442,18 +21430,20 @@ packages: yaml: optional: true - vitest@4.1.2: - resolution: {integrity: sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==} + vitest@4.1.5: + resolution: {integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.2 - '@vitest/browser-preview': 4.1.2 - '@vitest/browser-webdriverio': 4.1.2 - '@vitest/ui': 4.1.2 + '@vitest/browser-playwright': 4.1.5 + '@vitest/browser-preview': 4.1.5 + '@vitest/browser-webdriverio': 4.1.5 + '@vitest/coverage-istanbul': 4.1.5 + '@vitest/coverage-v8': 4.1.5 + '@vitest/ui': 4.1.5 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -21470,6 +21460,10 @@ packages: optional: true '@vitest/browser-webdriverio': optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true '@vitest/ui': optional: true happy-dom: @@ -30334,7 +30328,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@4.1.5(vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@22.19.18)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))': + '@vitest/coverage-v8@4.1.5(vitest@4.1.5)': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.1.5 @@ -30346,35 +30340,7 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@22.19.18)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - - '@vitest/coverage-v8@4.1.5(vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))': - dependencies: - '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.5 - ast-v8-to-istanbul: 1.0.0 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-reports: 3.2.0 - magicast: 0.5.2 - obug: 2.1.1 - std-env: 4.0.0 - tinyrainbow: 3.1.0 - vitest: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - - '@vitest/coverage-v8@4.1.5(vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))': - dependencies: - '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.5 - ast-v8-to-istanbul: 1.0.0 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-reports: 3.2.0 - magicast: 0.5.2 - obug: 2.1.1 - std-env: 4.0.0 - tinyrainbow: 3.1.0 - vitest: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/expect@3.2.4': dependencies: @@ -30384,36 +30350,36 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/expect@4.1.2': + '@vitest/expect@4.1.5': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.2 - '@vitest/utils': 4.1.2 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.2(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.5(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@vitest/spy': 4.1.2 + '@vitest/spy': 4.1.5 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.14(@types/node@22.19.18)(typescript@5.9.3) vite: 7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/mocker@4.1.2(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.5(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@vitest/spy': 4.1.2 + '@vitest/spy': 4.1.5 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.14(@types/node@25.6.0)(typescript@5.9.3) vite: 7.3.2(@types/node@25.6.0)(jiti@1.21.7)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/mocker@4.1.2(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.5(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@vitest/spy': 4.1.2 + '@vitest/spy': 4.1.5 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: @@ -30424,23 +30390,19 @@ snapshots: dependencies: tinyrainbow: 2.0.0 - '@vitest/pretty-format@4.1.2': - dependencies: - tinyrainbow: 3.1.0 - '@vitest/pretty-format@4.1.5': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.2': + '@vitest/runner@4.1.5': dependencies: - '@vitest/utils': 4.1.2 + '@vitest/utils': 4.1.5 pathe: 2.0.3 - '@vitest/snapshot@4.1.2': + '@vitest/snapshot@4.1.5': dependencies: - '@vitest/pretty-format': 4.1.2 - '@vitest/utils': 4.1.2 + '@vitest/pretty-format': 4.1.5 + '@vitest/utils': 4.1.5 magic-string: 0.30.21 pathe: 2.0.3 @@ -30448,7 +30410,7 @@ snapshots: dependencies: tinyspy: 4.0.4 - '@vitest/spy@4.1.2': {} + '@vitest/spy@4.1.5': {} '@vitest/utils@3.2.4': dependencies: @@ -30456,12 +30418,6 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@vitest/utils@4.1.2': - dependencies: - '@vitest/pretty-format': 4.1.2 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.0 - '@vitest/utils@4.1.5': dependencies: '@vitest/pretty-format': 4.1.5 @@ -38041,7 +37997,7 @@ snapshots: minimist: 1.2.8 selderee: 0.6.0 - html-validate@8.29.0(jest-diff@29.7.0)(jest-snapshot@29.7.0)(jest@29.7.0(@types/node@22.19.18)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21(@swc/helpers@0.5.21))(@types/node@22.19.18)(typescript@5.9.3)))(vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@22.19.18)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))): + html-validate@8.29.0(jest-diff@29.7.0)(jest-snapshot@29.7.0)(jest@29.7.0(@types/node@22.19.18)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21(@swc/helpers@0.5.21))(@types/node@22.19.18)(typescript@5.9.3)))(vitest@4.1.5): dependencies: '@html-validate/stylish': 4.3.0 '@sidvind/better-ajv-errors': 3.0.1(ajv@8.20.0) @@ -38055,7 +38011,7 @@ snapshots: jest: 29.7.0(@types/node@22.19.18)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21(@swc/helpers@0.5.21))(@types/node@22.19.18)(typescript@5.9.3)) jest-diff: 29.7.0 jest-snapshot: 29.7.0 - vitest: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@22.19.18)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@22.19.18)(@vitest/coverage-v8@4.1.5)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) html2canvas-objectfit-fix@1.2.0: dependencies: @@ -46688,15 +46644,15 @@ snapshots: tsx: 4.21.0 yaml: 2.8.3 - vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@22.19.18)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.5(@opentelemetry/api@1.9.1)(@types/node@22.19.18)(@vitest/coverage-v8@4.1.5)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: - '@vitest/expect': 4.1.2 - '@vitest/mocker': 4.1.2(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/pretty-format': 4.1.2 - '@vitest/runner': 4.1.2 - '@vitest/snapshot': 4.1.2 - '@vitest/spy': 4.1.2 - '@vitest/utils': 4.1.2 + '@vitest/expect': 4.1.5 + '@vitest/mocker': 4.1.5(msw@2.12.14(@types/node@22.19.18)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.18)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.5 + '@vitest/runner': 4.1.5 + '@vitest/snapshot': 4.1.5 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 @@ -46713,19 +46669,20 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 22.19.18 + '@vitest/coverage-v8': 4.1.5(vitest@4.1.5) jsdom: 29.1.1(@noble/hashes@1.8.0) transitivePeerDependencies: - msw - vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: - '@vitest/expect': 4.1.2 - '@vitest/mocker': 4.1.2(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/pretty-format': 4.1.2 - '@vitest/runner': 4.1.2 - '@vitest/snapshot': 4.1.2 - '@vitest/spy': 4.1.2 - '@vitest/utils': 4.1.2 + '@vitest/expect': 4.1.5 + '@vitest/mocker': 4.1.5(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.5 + '@vitest/runner': 4.1.5 + '@vitest/snapshot': 4.1.5 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 @@ -46742,19 +46699,20 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 25.6.0 + '@vitest/coverage-v8': 4.1.5(vitest@4.1.5) jsdom: 29.1.1(@noble/hashes@1.8.0) transitivePeerDependencies: - msw - vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: - '@vitest/expect': 4.1.2 - '@vitest/mocker': 4.1.2(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/pretty-format': 4.1.2 - '@vitest/runner': 4.1.2 - '@vitest/snapshot': 4.1.2 - '@vitest/spy': 4.1.2 - '@vitest/utils': 4.1.2 + '@vitest/expect': 4.1.5 + '@vitest/mocker': 4.1.5(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.5 + '@vitest/runner': 4.1.5 + '@vitest/snapshot': 4.1.5 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 @@ -46771,6 +46729,7 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 25.6.0 + '@vitest/coverage-v8': 4.1.5(vitest@4.1.5) jsdom: 29.1.1(@noble/hashes@1.8.0) transitivePeerDependencies: - msw diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index fd65561ec89..f2ebd1be16f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -36,6 +36,7 @@ catalog: '@typescript-eslint/parser': 8.49.0 '@vitejs/plugin-react': 4.7.0 '@vitest/coverage-v8': 4.1.5 + bson-objectid: 2.0.4 c8: 10.1.3 chai: 4.5.0 clsx: 2.1.1 @@ -59,7 +60,7 @@ catalog: validator: 13.12.0 vite: 7.3.2 vite-plugin-svgr: 4.5.0 - vitest: 4.1.2 + vitest: 4.1.5 zod: 4.1.12 catalogs: diff --git a/scripts/dev-supervisor.js b/scripts/dev-supervisor.js new file mode 100644 index 00000000000..18909c3b4fb --- /dev/null +++ b/scripts/dev-supervisor.js @@ -0,0 +1,358 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const {spawn, spawnSync} = require('child_process'); + +const rootDir = path.resolve(__dirname, '..'); +const stateDir = path.join(rootDir, '.ghost-dev'); +const stateFile = path.join(stateDir, 'dev-supervisor.json'); +const pnpmBin = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'; + +function ensureStateDir() { + fs.mkdirSync(stateDir, {recursive: true}); +} + +function writeState(state) { + ensureStateDir(); + fs.writeFileSync(stateFile, `${JSON.stringify(state, null, 2)}\n`); +} + +function readState() { + try { + return JSON.parse(fs.readFileSync(stateFile, 'utf8')); + } catch (error) { + return null; + } +} + +function removeState() { + try { + fs.unlinkSync(stateFile); + } catch (error) { + // Nothing to remove. + } +} + +function processExists(pid) { + try { + process.kill(pid, 0); + return true; + } catch (error) { + return false; + } +} + +function signalProcessGroup(pgid, signal) { + if (process.platform === 'win32') { + try { + process.kill(pgid, signal); + return true; + } catch (error) { + return false; + } + } + + try { + process.kill(-pgid, signal); + return true; + } catch (error) { + return false; + } +} + +function processGroupExists(pgid) { + if (process.platform === 'win32') { + return processExists(pgid); + } + + try { + process.kill(-pgid, 0); + return true; + } catch (error) { + return false; + } +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +function dockerComposeArgs() { + const args = ['compose', '-f', 'compose.dev.yaml']; + const extraComposeFiles = String(process.env.DEV_COMPOSE_FILES || '').trim(); + + if (extraComposeFiles) { + args.push(...extraComposeFiles.split(/\s+/)); + } + + return args; +} + +function runDockerDown() { + const result = spawnSync('docker', [...dockerComposeArgs(), 'down'], { + cwd: rootDir, + encoding: 'utf8', + env: process.env, + stdio: 'inherit' + }); + + return result.status === 0; +} + +function printCommand(command, args, env) { + const daemonMode = env.NX_DAEMON === 'false' ? 'disabled' : 'default'; + console.log(`[dev-supervisor] Starting: ${command} ${args.join(' ')}`); + console.log(`[dev-supervisor] Nx daemon: ${daemonMode}`); + if (env.NX_DAEMON === 'false') { + console.log('[dev-supervisor] Set GHOST_DEV_NX_DAEMON=1 to compare with the daemon enabled.'); + } +} + +function psOutput() { + const result = spawnSync('ps', ['-axo', 'pid,ppid,pgid,stat,%cpu,%mem,etime,command'], { + cwd: rootDir, + encoding: 'utf8' + }); + + if (result.error) { + return `Unable to run ps: ${result.error.message}\n`; + } + + return result.stdout || ''; +} + +function printMatchingProcesses() { + const repoPids = repoProcessIds(); + const patterns = [ + 'pnpm .*dev', + 'nx', + 'vite', + 'ember', + 'nodemon', + 'concurrently', + 'tsc', + 'tailwind', + 'dev-supervisor' + ]; + const matcher = new RegExp(patterns.join('|'), 'i'); + const lines = psOutput().split('\n').filter((line) => { + const pid = Number(line.trim().split(/\s+/)[0]); + return pid !== process.pid && pid !== process.ppid && repoPids.has(pid) && matcher.test(line); + }); + + if (!lines.length) { + console.log('No matching Ghost repo-local dev processes found.'); + return; + } + + console.log(lines.join('\n')); +} + +function repoProcessIds() { + const result = spawnSync('lsof', ['-nP', '-a', '-d', 'cwd'], { + cwd: rootDir, + encoding: 'utf8' + }); + + if (result.error || !result.stdout) { + return new Set(); + } + + const pids = new Set(); + + for (const line of result.stdout.split('\n').slice(1)) { + const parts = line.trim().split(/\s+/); + const pid = Number(parts[1]); + const cwd = parts.slice(8).join(' '); + + if (pid && (cwd === rootDir || cwd.startsWith(`${rootDir}${path.sep}`))) { + pids.add(pid); + } + } + + return pids; +} + +function printNxDaemonStatus() { + const result = spawnSync(pnpmBin, ['nx', 'daemon'], { + cwd: rootDir, + encoding: 'utf8', + env: process.env + }); + + const output = `${result.stdout || ''}${result.stderr || ''}`.trim(); + if (output) { + console.log(output); + } else if (result.error) { + console.log(`Unable to inspect Nx daemon: ${result.error.message}`); + } +} + +async function cleanupHelper() { + const pgidIndex = process.argv.indexOf('--pgid'); + const pgid = pgidIndex === -1 ? null : Number(process.argv[pgidIndex + 1]); + + if (!pgid) { + process.exit(0); + } + + await sleep(8000); + + if (processGroupExists(pgid)) { + signalProcessGroup(pgid, 'SIGTERM'); + } + + runDockerDown(); + + await sleep(3000); + + if (processGroupExists(pgid)) { + signalProcessGroup(pgid, 'SIGKILL'); + } +} + +function spawnCleanupHelper(pgid) { + let helper; + + try { + helper = spawn(process.execPath, [__filename, 'cleanup-helper', '--pgid', String(pgid)], { + cwd: rootDir, + detached: true, + stdio: 'ignore' + }); + } catch (error) { + console.error(`[dev-supervisor] Failed to start cleanup helper: ${error.message}`); + return; + } + + helper.on('error', (error) => { + console.error(`[dev-supervisor] Cleanup helper failed to start: ${error.message}`); + }); + + helper.unref(); +} + +function status() { + const state = readState(); + + console.log('Ghost dev supervisor'); + if (state) { + const running = processExists(state.childPid); + console.log(`State file: ${stateFile}`); + console.log(`Started: ${state.startedAt}`); + console.log(`Supervisor PID: ${state.supervisorPid}`); + console.log(`Child PID/PGID: ${state.childPid}`); + console.log(`Command: ${state.command} ${state.args.join(' ')}`); + console.log(`Nx daemon for supervised dev: ${state.nxDaemon}`); + console.log(`Child process running: ${running ? 'yes' : 'no'}`); + } else { + console.log(`State file: ${stateFile} (not present)`); + } + + console.log('\nNx daemon'); + printNxDaemonStatus(); + + console.log('\nMatching local dev processes'); + printMatchingProcesses(); +} + +function start() { + const args = ['nx', 'run', 'ghost-monorepo:docker:dev']; + const env = { + ...process.env + }; + + if (!['1', 'true', 'yes'].includes(String(process.env.GHOST_DEV_NX_DAEMON || '').toLowerCase())) { + env.NX_DAEMON = 'false'; + } + + printCommand(pnpmBin, args, env); + + const child = spawn(pnpmBin, args, { + cwd: rootDir, + detached: process.platform !== 'win32', + env, + stdio: 'inherit' + }); + + child.on('error', (error) => { + console.error(`[dev-supervisor] Failed to start dev command: ${error.message}`); + removeState(); + process.exit(1); + }); + + writeState({ + startedAt: new Date().toISOString(), + platform: `${os.platform()}-${os.arch()}`, + supervisorPid: process.pid, + childPid: child.pid, + command: pnpmBin, + args, + nxDaemon: env.NX_DAEMON === 'false' ? 'disabled' : 'default' + }); + + let shuttingDown = false; + + function shutdown(signal) { + if (shuttingDown) { + return; + } + + shuttingDown = true; + console.log(`\n[dev-supervisor] Received ${signal}; forwarding to dev process group ${child.pid}.`); + + signalProcessGroup(child.pid, signal === 'SIGINT' ? 'SIGINT' : 'SIGTERM'); + spawnCleanupHelper(child.pid); + + setTimeout(() => { + if (processGroupExists(child.pid)) { + console.log('[dev-supervisor] Dev process group is still running; sending SIGTERM.'); + signalProcessGroup(child.pid, 'SIGTERM'); + } + + console.log('[dev-supervisor] Ensuring Docker dev services are stopped.'); + runDockerDown(); + }, 8000).unref(); + + setTimeout(() => { + if (processGroupExists(child.pid)) { + console.log('[dev-supervisor] Dev process group is still running; sending SIGKILL.'); + signalProcessGroup(child.pid, 'SIGKILL'); + } + }, 11000).unref(); + } + + process.on('SIGINT', () => shutdown('SIGINT')); + process.on('SIGTERM', () => shutdown('SIGTERM')); + process.on('SIGHUP', () => shutdown('SIGHUP')); + + child.on('exit', (code, signal) => { + removeState(); + + if (shuttingDown) { + process.exit(0); + } + + if (signal) { + console.log(`[dev-supervisor] Dev process exited from ${signal}.`); + process.exit(1); + } + + process.exit(code || 0); + }); +} + +const command = process.argv[2]; + +if (command === 'status') { + status(); +} else if (command === 'cleanup-helper') { + cleanupHelper().catch((error) => { + console.error(error); + process.exit(1); + }); +} else { + start(); +}