From e9383f25e016c94295fd1210cd76e52be75883ea Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Tue, 15 Oct 2024 16:52:06 +0200 Subject: [PATCH 1/5] fix(utils): update medusa config resolution for consistency (#9591) --- packages/cli/medusa-dev-cli/src/index.js | 6 ++++++ .../src/config/__tests__/index.spec.ts | 6 +++--- packages/core/framework/src/config/loader.ts | 9 ++++++--- .../core/utils/src/common/get-config-file.ts | 18 ++++++++++-------- .../src/medusa-test-runner-utils/config.ts | 2 +- packages/medusa/src/commands/build.ts | 12 +++++------- packages/medusa/src/loaders/index.ts | 2 +- .../__tests__/index-engine-module.spec.ts | 5 ++++- .../__tests__/query-builder.spec.ts | 5 ++++- 9 files changed, 40 insertions(+), 25 deletions(-) diff --git a/packages/cli/medusa-dev-cli/src/index.js b/packages/cli/medusa-dev-cli/src/index.js index acc33f0aa7ee5..d551a6a091f8f 100644 --- a/packages/cli/medusa-dev-cli/src/index.js +++ b/packages/cli/medusa-dev-cli/src/index.js @@ -1,5 +1,11 @@ #!/usr/bin/env node +try { + require("ts-node").register({}) + require("tsconfig-paths").register({}) +} catch {} +require("dotenv").config() + const Configstore = require(`configstore`) const pkg = require(`../package.json`) const _ = require(`lodash`) diff --git a/packages/core/framework/src/config/__tests__/index.spec.ts b/packages/core/framework/src/config/__tests__/index.spec.ts index b38b5556adb71..4020c9b9ddc87 100644 --- a/packages/core/framework/src/config/__tests__/index.spec.ts +++ b/packages/core/framework/src/config/__tests__/index.spec.ts @@ -13,14 +13,14 @@ describe("configLoader", () => { expect(configModule).toBeUndefined() - configLoader(entryDirectory, "medusa-config") + await configLoader(entryDirectory, "medusa-config") configModule = container.resolve(ContainerRegistrationKeys.CONFIG_MODULE) expect(configModule).toBeDefined() expect(configModule.projectConfig.databaseName).toBeUndefined() - configLoader(entryDirectory, "medusa-config-2") + await configLoader(entryDirectory, "medusa-config-2") configModule = container.resolve(ContainerRegistrationKeys.CONFIG_MODULE) @@ -30,7 +30,7 @@ describe("configLoader", () => { process.env.MEDUSA_WORKER_MODE = "worker" - configLoader(entryDirectory, "medusa-config-2") + await configLoader(entryDirectory, "medusa-config-2") configModule = container.resolve(ContainerRegistrationKeys.CONFIG_MODULE) diff --git a/packages/core/framework/src/config/loader.ts b/packages/core/framework/src/config/loader.ts index 698b64a761534..18c555ff8407d 100644 --- a/packages/core/framework/src/config/loader.ts +++ b/packages/core/framework/src/config/loader.ts @@ -26,11 +26,14 @@ container.register( * @param entryDirectory The directory to find the config file from * @param configFileName The name of the config file to search for in the entry directory */ -export function configLoader( +export async function configLoader( entryDirectory: string, configFileName: string -): ConfigModule { - const config = getConfigFile(entryDirectory, configFileName) +): Promise { + const config = await getConfigFile( + entryDirectory, + configFileName + ) if (config.error) { handleConfigError(config.error) diff --git a/packages/core/utils/src/common/get-config-file.ts b/packages/core/utils/src/common/get-config-file.ts index 1983f781b794d..3438f312a6159 100644 --- a/packages/core/utils/src/common/get-config-file.ts +++ b/packages/core/utils/src/common/get-config-file.ts @@ -1,4 +1,5 @@ import { join } from "path" +import { dynamicImport } from "./dynamic-import" /** * Attempts to resolve the config file in a given root directory. @@ -6,22 +7,23 @@ import { join } from "path" * @param {string} configName - the name of the config file. * @return {object} an object containing the config module and its path as well as an error property if the config couldn't be loaded. */ -export function getConfigFile( +export async function getConfigFile( rootDir: string, configName: string -): +): Promise< | { configModule: null; configFilePath: string; error: Error } - | { configModule: TConfig; configFilePath: string; error: null } { + | { configModule: TConfig; configFilePath: string; error: null } +> { const configPath = join(rootDir, configName) try { - const configFilePath = require.resolve(configPath) - const configExports = require(configFilePath) + const configFilePath = join(process.cwd(), rootDir, configName) + const resolvedExports = await dynamicImport(configPath) return { configModule: - configExports && "default" in configExports - ? configExports.default - : configExports, + "default" in resolvedExports && resolvedExports.default + ? resolvedExports.default + : resolvedExports, configFilePath, error: null, } diff --git a/packages/medusa-test-utils/src/medusa-test-runner-utils/config.ts b/packages/medusa-test-utils/src/medusa-test-runner-utils/config.ts index c4b2ee68a2b76..a617c3d8de396 100644 --- a/packages/medusa-test-utils/src/medusa-test-runner-utils/config.ts +++ b/packages/medusa-test-utils/src/medusa-test-runner-utils/config.ts @@ -5,7 +5,7 @@ export async function configLoaderOverride( override: { clientUrl: string; debug?: boolean } ) { const { configManager } = await import("@medusajs/framework/config") - const { configModule, error } = getConfigFile< + const { configModule, error } = await getConfigFile< ReturnType >(entryDirectory, "medusa-config") diff --git a/packages/medusa/src/commands/build.ts b/packages/medusa/src/commands/build.ts index 79d080e8fd88c..53839e19a49f8 100644 --- a/packages/medusa/src/commands/build.ts +++ b/packages/medusa/src/commands/build.ts @@ -1,5 +1,5 @@ import path from "path" -import { rm, copyFile, access, constants } from "node:fs/promises" +import { access, constants, copyFile, rm } from "node:fs/promises" import type tsStatic from "typescript" import { logger } from "@medusajs/framework/logger" import { ConfigModule } from "@medusajs/framework/types" @@ -39,15 +39,13 @@ async function clean(path: string) { /** * Loads the medusa config file or exits with an error */ -function loadMedusaConfig(directory: string) { +async function loadMedusaConfig(directory: string) { /** * Parsing the medusa config file to ensure it is error * free */ - const { configModule, configFilePath, error } = getConfigFile( - directory, - "medusa-config" - ) + const { configModule, configFilePath, error } = + await getConfigFile(directory, "medusa-config") if (error) { console.error(`Failed to load medusa-config.js`) console.error(error) @@ -199,7 +197,7 @@ async function buildBackend(projectRoot: string): Promise { */ async function buildFrontend(projectRoot: string): Promise { const startTime = process.hrtime() - const configFile = loadMedusaConfig(projectRoot) + const configFile = await loadMedusaConfig(projectRoot) if (!configFile) { return false } diff --git a/packages/medusa/src/loaders/index.ts b/packages/medusa/src/loaders/index.ts index a3ec7c14b3543..dfc27634cdd26 100644 --- a/packages/medusa/src/loaders/index.ts +++ b/packages/medusa/src/loaders/index.ts @@ -118,7 +118,7 @@ async function loadEntrypoints( export async function initializeContainer( rootDirectory: string ): Promise { - configLoader(rootDirectory, "medusa-config") + await configLoader(rootDirectory, "medusa-config") await featureFlagsLoader(join(__dirname, "feature-flags")) container.register({ diff --git a/packages/modules/index/integration-tests/__tests__/index-engine-module.spec.ts b/packages/modules/index/integration-tests/__tests__/index-engine-module.spec.ts index 9ea6f476c7cd0..31cc6e79197b1 100644 --- a/packages/modules/index/integration-tests/__tests__/index-engine-module.spec.ts +++ b/packages/modules/index/integration-tests/__tests__/index-engine-module.spec.ts @@ -100,7 +100,10 @@ let index!: IndexTypes.IIndexService const beforeAll_ = async () => { try { - configLoader(path.join(__dirname, "./../__fixtures__"), "medusa-config") + await configLoader( + path.join(__dirname, "./../__fixtures__"), + "medusa-config" + ) console.log(`Creating database ${dbName}`) await dbUtils.create(dbName) diff --git a/packages/modules/index/integration-tests/__tests__/query-builder.spec.ts b/packages/modules/index/integration-tests/__tests__/query-builder.spec.ts index b7482c466e938..b99e4180621b7 100644 --- a/packages/modules/index/integration-tests/__tests__/query-builder.spec.ts +++ b/packages/modules/index/integration-tests/__tests__/query-builder.spec.ts @@ -33,7 +33,10 @@ let medusaAppLoader!: MedusaAppLoader const beforeAll_ = async () => { try { - configLoader(path.join(__dirname, "./../__fixtures__"), "medusa-config") + await configLoader( + path.join(__dirname, "./../__fixtures__"), + "medusa-config" + ) console.log(`Creating database ${dbName}`) await dbUtils.create(dbName) From e77a2ff0323fd1d04a9a6bb2e45ac98ac8e9d7d3 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Tue, 15 Oct 2024 17:28:45 +0200 Subject: [PATCH 2/5] fix(utils): Reversed module package missing references (#9589) **What** When resolving the modules through an array, if the module does not have a joiner config to rely on (not `queryable`, not using the `Module` util), then the key should be provided to register the module in the container. Eventually, the module author should provide that key to be used. The only exception is for the medusa modules where we have a map that allows us to resolve that key automatically from the path of the resolve value --- packages/core/utils/src/modules-sdk/definition.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/core/utils/src/modules-sdk/definition.ts b/packages/core/utils/src/modules-sdk/definition.ts index 25ff88a3639c4..7582f00f45669 100644 --- a/packages/core/utils/src/modules-sdk/definition.ts +++ b/packages/core/utils/src/modules-sdk/definition.ts @@ -63,6 +63,13 @@ export const REVERSED_MODULE_PACKAGE_NAMES = Object.entries( return acc }, {}) +// TODO: temporary fix until the event bus, cache and workflow engine are migrated to use providers and therefore only a single resolution will be good +REVERSED_MODULE_PACKAGE_NAMES["@medusajs/medusa/event-bus-redis"] = + Modules.EVENT_BUS +REVERSED_MODULE_PACKAGE_NAMES["@medusajs/medusa/cache-redis"] = Modules.CACHE +REVERSED_MODULE_PACKAGE_NAMES["@medusajs/medusa/workflow-engine-redis"] = + Modules.WORKFLOW_ENGINE + /** * Making modules be referenced as a type as well. */ From 4a03bdbb86820f424f062f273a0d24011e7552e3 Mon Sep 17 00:00:00 2001 From: "Carlos R. L. Rodrigues" <37986729+carlos-r-l-rodrigues@users.noreply.github.com> Date: Tue, 15 Oct 2024 12:40:24 -0300 Subject: [PATCH 3/5] feat(providers): locking redis (#9544) --- .eslintignore | 1 + .eslintrc.js | 1 + integration-tests/modules/medusa-config.js | 1 + .../src/cart/steps/reserve-inventory.ts | 46 +- .../reservation/steps/create-reservations.ts | 33 +- .../modules-sdk/src/loaders/module-loader.ts | 6 +- .../module-with-providers/index.ts | 11 + .../module-with-providers/provider-1/index.ts | 8 + .../provider-1/services/provider-service.ts | 3 + .../module-with-providers/provider-2/index.ts | 8 + .../provider-2/services/provider-service.ts | 1 + .../services/module-service.ts | 9 + .../utils/__tests__/load-internal.spec.ts | 603 +++++++++++------- .../src/loaders/utils/load-internal.ts | 392 +++++++++--- packages/core/modules-sdk/src/medusa-app.ts | 2 +- packages/core/types/src/modules-sdk/index.ts | 3 +- .../types/src/modules-sdk/module-provider.ts | 44 +- .../common/__tests__/define-config.spec.ts | 18 + .../core/utils/src/common/define-config.ts | 5 +- packages/core/utils/src/index.ts | 3 +- packages/core/utils/src/modules-sdk/index.ts | 1 + .../utils/src/modules-sdk/module-provider.ts | 19 + packages/medusa/package.json | 1 + packages/medusa/src/modules/locking-redis.ts | 6 + .../cache-redis/src/services/redis-cache.ts | 26 +- .../integration-tests/__tests__/index.spec.ts | 97 ++- packages/modules/locking/package.json | 6 + packages/modules/locking/src/index.ts | 4 +- .../modules/locking/src/loaders/providers.ts | 12 +- .../locking/src/providers/in-memory.ts | 49 +- .../providers/auth-emailpass/src/index.ts | 8 +- .../providers/auth-github/src/index.ts | 8 +- .../providers/auth-google/src/index.ts | 8 +- .../modules/providers/file-local/src/index.ts | 8 +- .../modules/providers/file-s3/src/index.ts | 8 +- .../providers/fulfillment-manual/src/index.ts | 8 +- .../providers/locking-redis/.gitignore | 4 + .../integration-tests/__tests__/index.spec.ts | 220 +++++++ .../providers/locking-redis/jest.config.js | 10 + .../providers/locking-redis/package.json | 48 ++ .../providers/locking-redis/src/index.ts | 11 + .../locking-redis/src/loaders/index.ts | 41 ++ .../locking-redis/src/services/redis-lock.ts | 280 ++++++++ .../locking-redis/src/types/index.ts | 45 ++ .../providers/locking-redis/tsconfig.json | 12 + .../providers/notification-local/src/index.ts | 8 +- .../notification-sendgrid/src/index.ts | 8 +- .../providers/payment-stripe/src/index.ts | 8 +- yarn.lock | 17 + 49 files changed, 1730 insertions(+), 449 deletions(-) create mode 100644 packages/core/modules-sdk/src/loaders/utils/__fixtures__/module-with-providers/index.ts create mode 100644 packages/core/modules-sdk/src/loaders/utils/__fixtures__/module-with-providers/provider-1/index.ts create mode 100644 packages/core/modules-sdk/src/loaders/utils/__fixtures__/module-with-providers/provider-1/services/provider-service.ts create mode 100644 packages/core/modules-sdk/src/loaders/utils/__fixtures__/module-with-providers/provider-2/index.ts create mode 100644 packages/core/modules-sdk/src/loaders/utils/__fixtures__/module-with-providers/provider-2/services/provider-service.ts create mode 100644 packages/core/modules-sdk/src/loaders/utils/__fixtures__/module-with-providers/services/module-service.ts create mode 100644 packages/core/utils/src/modules-sdk/module-provider.ts create mode 100644 packages/medusa/src/modules/locking-redis.ts create mode 100644 packages/modules/providers/locking-redis/.gitignore create mode 100644 packages/modules/providers/locking-redis/integration-tests/__tests__/index.spec.ts create mode 100644 packages/modules/providers/locking-redis/jest.config.js create mode 100644 packages/modules/providers/locking-redis/package.json create mode 100644 packages/modules/providers/locking-redis/src/index.ts create mode 100644 packages/modules/providers/locking-redis/src/loaders/index.ts create mode 100644 packages/modules/providers/locking-redis/src/services/redis-lock.ts create mode 100644 packages/modules/providers/locking-redis/src/types/index.ts create mode 100644 packages/modules/providers/locking-redis/tsconfig.json diff --git a/.eslintignore b/.eslintignore index 66fa041a02a90..ed0ba98e31562 100644 --- a/.eslintignore +++ b/.eslintignore @@ -33,6 +33,7 @@ packages/* !packages/workflow-engine-inmemory !packages/fulfillment !packages/fulfillment-manual +!packages/locking-redis !packages/index !packages/framework diff --git a/.eslintrc.js b/.eslintrc.js index f960018932f77..02c2ee5f161b3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -134,6 +134,7 @@ module.exports = { "./packages/modules/providers/file-s3/tsconfig.spec.json", "./packages/modules/providers/fulfillment-manual/tsconfig.spec.json", "./packages/modules/providers/payment-stripe/tsconfig.spec.json", + "./packages/modules/providers/locking-redis/tsconfig.spec.json", "./packages/framework/tsconfig.json", ], diff --git a/integration-tests/modules/medusa-config.js b/integration-tests/modules/medusa-config.js index 08f890690bd25..df3b3e6ae383a 100644 --- a/integration-tests/modules/medusa-config.js +++ b/integration-tests/modules/medusa-config.js @@ -62,6 +62,7 @@ module.exports = { resolve: "@medusajs/cache-inmemory", options: { ttl: 0 }, // Cache disabled }, + [Modules.LOCKING]: true, [Modules.STOCK_LOCATION]: { resolve: "@medusajs/stock-location-next", options: {}, diff --git a/packages/core/core-flows/src/cart/steps/reserve-inventory.ts b/packages/core/core-flows/src/cart/steps/reserve-inventory.ts index 66f4645d27c7c..b749933467a23 100644 --- a/packages/core/core-flows/src/cart/steps/reserve-inventory.ts +++ b/packages/core/core-flows/src/cart/steps/reserve-inventory.ts @@ -1,6 +1,5 @@ -import { IInventoryService } from "@medusajs/framework/types" import { MathBN, Modules } from "@medusajs/framework/utils" -import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" import { BigNumberInput } from "@medusajs/types" export interface ReserveVariantInventoryStepInput { @@ -22,34 +21,45 @@ export const reserveInventoryStepId = "reserve-inventory-step" export const reserveInventoryStep = createStep( reserveInventoryStepId, async (data: ReserveVariantInventoryStepInput, { container }) => { - const inventoryService = container.resolve( - Modules.INVENTORY - ) + const inventoryService = container.resolve(Modules.INVENTORY) - const items = data.items.map((item) => ({ - line_item_id: item.id, - inventory_item_id: item.inventory_item_id, - quantity: MathBN.mult(item.required_quantity, item.quantity), - allow_backorder: item.allow_backorder, - location_id: item.location_ids[0], - })) + const locking = container.resolve(Modules.LOCKING) - const reservations = await inventoryService.createReservationItems(items) + const inventoryItemIds: string[] = [] + + const items = data.items.map((item) => { + inventoryItemIds.push(item.inventory_item_id) + + return { + line_item_id: item.id, + inventory_item_id: item.inventory_item_id, + quantity: MathBN.mult(item.required_quantity, item.quantity), + allow_backorder: item.allow_backorder, + location_id: item.location_ids[0], + } + }) + + const reservations = await locking.execute(inventoryItemIds, async () => { + return await inventoryService.createReservationItems(items) + }) return new StepResponse(reservations, { reservations: reservations.map((r) => r.id), + inventoryItemIds, }) }, async (data, { container }) => { - if (!data) { + if (!data?.reservations?.length) { return } - const inventoryService = container.resolve( - Modules.INVENTORY - ) + const inventoryService = container.resolve(Modules.INVENTORY) + const locking = container.resolve(Modules.LOCKING) - await inventoryService.deleteReservationItems(data.reservations) + const inventoryItemIds = data.inventoryItemIds + await locking.execute(inventoryItemIds, async () => { + await inventoryService.deleteReservationItems(data.reservations) + }) return new StepResponse() } diff --git a/packages/core/core-flows/src/reservation/steps/create-reservations.ts b/packages/core/core-flows/src/reservation/steps/create-reservations.ts index 2c01d0ce696d0..d186798b2383a 100644 --- a/packages/core/core-flows/src/reservation/steps/create-reservations.ts +++ b/packages/core/core-flows/src/reservation/steps/create-reservations.ts @@ -1,5 +1,5 @@ -import { IInventoryService, InventoryTypes } from "@medusajs/framework/types" -import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk" +import { InventoryTypes } from "@medusajs/framework/types" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" @@ -10,22 +10,31 @@ export const createReservationsStepId = "create-reservations-step" export const createReservationsStep = createStep( createReservationsStepId, async (data: InventoryTypes.CreateReservationItemInput[], { container }) => { - const service = container.resolve(Modules.INVENTORY) + const service = container.resolve(Modules.INVENTORY) + const locking = container.resolve(Modules.LOCKING) - const created = await service.createReservationItems(data) + const inventoryItemIds = data.map((item) => item.inventory_item_id) - return new StepResponse( - created, - created.map((reservation) => reservation.id) - ) + const created = await locking.execute(inventoryItemIds, async () => { + return await service.createReservationItems(data) + }) + + return new StepResponse(created, { + reservations: created.map((reservation) => reservation.id), + inventoryItemIds: inventoryItemIds, + }) }, - async (createdIds, { container }) => { - if (!createdIds?.length) { + async (data, { container }) => { + if (!data?.reservations?.length) { return } - const service = container.resolve(Modules.INVENTORY) + const service = container.resolve(Modules.INVENTORY) + const locking = container.resolve(Modules.LOCKING) - await service.deleteReservationItems(createdIds) + const inventoryItemIds = data.inventoryItemIds + await locking.execute(inventoryItemIds, async () => { + await service.deleteReservationItems(data.reservations) + }) } ) diff --git a/packages/core/modules-sdk/src/loaders/module-loader.ts b/packages/core/modules-sdk/src/loaders/module-loader.ts index 05a9dd207c339..c0ec8b24f1222 100644 --- a/packages/core/modules-sdk/src/loaders/module-loader.ts +++ b/packages/core/modules-sdk/src/loaders/module-loader.ts @@ -82,11 +82,11 @@ async function loadModule( return } - return await loadInternalModule( + return await loadInternalModule({ container, resolution, logger, migrationOnly, - loaderOnly - ) + loaderOnly, + }) } diff --git a/packages/core/modules-sdk/src/loaders/utils/__fixtures__/module-with-providers/index.ts b/packages/core/modules-sdk/src/loaders/utils/__fixtures__/module-with-providers/index.ts new file mode 100644 index 0000000000000..3adb26aca99e5 --- /dev/null +++ b/packages/core/modules-sdk/src/loaders/utils/__fixtures__/module-with-providers/index.ts @@ -0,0 +1,11 @@ +import { ModuleExports } from "@medusajs/types" +import { ModuleService } from "./services/module-service" +import { Module } from "@medusajs/utils" + +const moduleExports: ModuleExports = { + service: ModuleService, +} + +export * from "./services/module-service" + +export default Module("module-with-providers", moduleExports) diff --git a/packages/core/modules-sdk/src/loaders/utils/__fixtures__/module-with-providers/provider-1/index.ts b/packages/core/modules-sdk/src/loaders/utils/__fixtures__/module-with-providers/provider-1/index.ts new file mode 100644 index 0000000000000..4775c124aaa2f --- /dev/null +++ b/packages/core/modules-sdk/src/loaders/utils/__fixtures__/module-with-providers/provider-1/index.ts @@ -0,0 +1,8 @@ +import { ModuleProviderService } from "./services/provider-service" +import { ModuleProvider } from "@medusajs/utils" + +export * from "./services/provider-service" + +export default ModuleProvider("provider-1", { + services: [ModuleProviderService], +}) diff --git a/packages/core/modules-sdk/src/loaders/utils/__fixtures__/module-with-providers/provider-1/services/provider-service.ts b/packages/core/modules-sdk/src/loaders/utils/__fixtures__/module-with-providers/provider-1/services/provider-service.ts new file mode 100644 index 0000000000000..328deec074cb5 --- /dev/null +++ b/packages/core/modules-sdk/src/loaders/utils/__fixtures__/module-with-providers/provider-1/services/provider-service.ts @@ -0,0 +1,3 @@ +export class ModuleProviderService { + static identifier = "provider-1" +} diff --git a/packages/core/modules-sdk/src/loaders/utils/__fixtures__/module-with-providers/provider-2/index.ts b/packages/core/modules-sdk/src/loaders/utils/__fixtures__/module-with-providers/provider-2/index.ts new file mode 100644 index 0000000000000..209d35ace64b6 --- /dev/null +++ b/packages/core/modules-sdk/src/loaders/utils/__fixtures__/module-with-providers/provider-2/index.ts @@ -0,0 +1,8 @@ +import { ModuleProvider2Service } from "./services/provider-service" +import { ModuleProvider } from "@medusajs/utils" + +export * from "./services/provider-service" + +export default ModuleProvider("provider-2", { + services: [ModuleProvider2Service], +}) diff --git a/packages/core/modules-sdk/src/loaders/utils/__fixtures__/module-with-providers/provider-2/services/provider-service.ts b/packages/core/modules-sdk/src/loaders/utils/__fixtures__/module-with-providers/provider-2/services/provider-service.ts new file mode 100644 index 0000000000000..7e581fe2b8690 --- /dev/null +++ b/packages/core/modules-sdk/src/loaders/utils/__fixtures__/module-with-providers/provider-2/services/provider-service.ts @@ -0,0 +1 @@ +export class ModuleProvider2Service {} diff --git a/packages/core/modules-sdk/src/loaders/utils/__fixtures__/module-with-providers/services/module-service.ts b/packages/core/modules-sdk/src/loaders/utils/__fixtures__/module-with-providers/services/module-service.ts new file mode 100644 index 0000000000000..dab412f7fc7e3 --- /dev/null +++ b/packages/core/modules-sdk/src/loaders/utils/__fixtures__/module-with-providers/services/module-service.ts @@ -0,0 +1,9 @@ +import { InternalModuleDeclaration } from "@medusajs/types" + +export class ModuleService { + constructor( + public container: Record, + public moduleOptions: Record, + public moduleDeclaration: InternalModuleDeclaration + ) {} +} diff --git a/packages/core/modules-sdk/src/loaders/utils/__tests__/load-internal.spec.ts b/packages/core/modules-sdk/src/loaders/utils/__tests__/load-internal.spec.ts index f2bcfdd5f1f2f..cef3444c607c6 100644 --- a/packages/core/modules-sdk/src/loaders/utils/__tests__/load-internal.spec.ts +++ b/packages/core/modules-sdk/src/loaders/utils/__tests__/load-internal.spec.ts @@ -1,5 +1,5 @@ import { IModuleService, ModuleResolution } from "@medusajs/types" -import { upperCaseFirst } from "@medusajs/utils" +import { createMedusaContainer, upperCaseFirst } from "@medusajs/utils" import { join } from "path" import { ModuleWithDmlMixedWithoutJoinerConfigFixtures, @@ -7,79 +7,253 @@ import { ModuleWithJoinerConfigFixtures, ModuleWithoutJoinerConfigFixtures, } from "../__fixtures__" -import { loadResources } from "../load-internal" +import { + getProviderRegistrationKey, + loadInternalModule, + loadResources, +} from "../load-internal" +import { ModuleProviderService as ModuleServiceWithProviderProvider1 } from "../__fixtures__/module-with-providers/provider-1" +import { ModuleProvider2Service as ModuleServiceWithProviderProvider2 } from "../__fixtures__/module-with-providers/provider-2" +import { ModuleService as ModuleServiceWithProvider } from "../__fixtures__/module-with-providers" -describe("load internal - load resources", () => { - describe("when loading the module resources from a path", () => { - test("should return the correct resources and generate the correct joiner config from a mix of DML entities and mikro orm entities", async () => { - const { ModuleService, EntityModel, dmlEntity } = - ModuleWithDmlMixedWithoutJoinerConfigFixtures +describe("load internal", () => { + describe("loadResources", () => { + describe("when loading the module resources from a path", () => { + test("should return the correct resources and generate the correct joiner config from a mix of DML entities and mikro orm entities", async () => { + const { ModuleService, EntityModel, dmlEntity } = + ModuleWithDmlMixedWithoutJoinerConfigFixtures - const moduleResolution: ModuleResolution = { - resolutionPath: join( - __dirname, - "../__fixtures__/module-with-dml-mixed-without-joiner-config" - ), - definition: { - key: "module-with-dml-mixed-without-joiner-config", - label: "Module with DML mixed without joiner config", - defaultPackage: false, - defaultModuleDeclaration: { - scope: "internal", - resources: "shared", + const moduleResolution: ModuleResolution = { + resolutionPath: join( + __dirname, + "../__fixtures__/module-with-dml-mixed-without-joiner-config" + ), + definition: { + key: "module-with-dml-mixed-without-joiner-config", + label: "Module with DML mixed without joiner config", + defaultPackage: false, + defaultModuleDeclaration: { + scope: "internal", + resources: "shared", + }, }, - }, - } + } + + expect( + (ModuleService.prototype as IModuleService).__joinerConfig + ).toBeUndefined() + + const resources = await loadResources({ + moduleResolution, + discoveryPath: moduleResolution.resolutionPath as string, + }) + + expect(resources).toBeDefined() + expect(resources.services).toHaveLength(1) + expect(resources.services[0]).toEqual(ModuleService) + expect(resources.models).toHaveLength(2) + expect(resources.models).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: upperCaseFirst(dmlEntity.name) }), + expect.objectContaining({ name: upperCaseFirst(EntityModel.name) }), + ]) + ) + expect(resources.repositories).toHaveLength(0) + expect(resources.loaders).toHaveLength(2) + expect(resources.loaders).toEqual([ + expect.objectContaining({ name: "connectionLoader" }), + expect.objectContaining({ name: "containerLoader" }), + ]) + expect(resources.moduleService).toEqual(ModuleService) + + expect( + (resources.moduleService.prototype as IModuleService).__joinerConfig + ).toBeDefined() + + const generatedJoinerConfig = ( + resources.moduleService.prototype as IModuleService + ).__joinerConfig?.()! + + expect(generatedJoinerConfig).toEqual( + expect.objectContaining({ + serviceName: "module-with-dml-mixed-without-joiner-config", + primaryKeys: ["id"], + linkableKeys: { + dml_entity_id: "DmlEntity", + entity_model_id: "EntityModel", + }, + alias: [ + { + name: ["dml_entity", "dml_entities"], + entity: "DmlEntity", + args: { + methodSuffix: "DmlEntities", + }, + }, + { + name: ["entity_model", "entity_models"], + entity: "EntityModel", + args: { + methodSuffix: "EntityModels", + }, + }, + ], + }) + ) + }) + + test("should return the correct resources and generate the correct joiner config from DML entities", async () => { + const { ModuleService, entityModel, dmlEntity } = + ModuleWithDmlWithoutJoinerConfigFixtures + + const moduleResolution: ModuleResolution = { + resolutionPath: join( + __dirname, + "../__fixtures__/module-with-dml-without-joiner-config" + ), + definition: { + key: "module-with-dml-without-joiner-config", + label: "Module with DML without joiner config", + defaultPackage: false, + defaultModuleDeclaration: { + scope: "internal", + resources: "shared", + }, + }, + } + + expect( + (ModuleService.prototype as IModuleService).__joinerConfig + ).toBeUndefined() + + const resources = await loadResources({ + moduleResolution, + discoveryPath: moduleResolution.resolutionPath as string, + }) + + expect(resources).toBeDefined() + expect(resources.services).toHaveLength(1) + expect(resources.services[0]).toEqual(ModuleService) + expect(resources.models).toHaveLength(2) + expect(resources.models).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: upperCaseFirst(dmlEntity.name) }), + expect.objectContaining({ name: upperCaseFirst(entityModel.name) }), + ]) + ) + expect(resources.repositories).toHaveLength(0) + expect(resources.loaders).toHaveLength(2) + expect(resources.loaders).toEqual([ + expect.objectContaining({ name: "connectionLoader" }), + expect.objectContaining({ name: "containerLoader" }), + ]) + expect(resources.moduleService).toEqual(ModuleService) - expect( - (ModuleService.prototype as IModuleService).__joinerConfig - ).toBeUndefined() + expect( + (resources.moduleService.prototype as IModuleService).__joinerConfig + ).toBeDefined() - const resources = await loadResources({ - moduleResolution, - discoveryPath: moduleResolution.resolutionPath as string, + const generatedJoinerConfig = ( + resources.moduleService.prototype as IModuleService + ).__joinerConfig?.()! + + expect(generatedJoinerConfig).toEqual( + expect.objectContaining({ + serviceName: "module-with-dml-without-joiner-config", + primaryKeys: ["id"], + linkableKeys: { + entity_model_id: "EntityModel", + dml_entity_id: "DmlEntity", + }, + alias: [ + { + name: ["entity_model", "entity_models"], + entity: "EntityModel", + args: { + methodSuffix: "EntityModels", + }, + }, + { + name: ["dml_entity", "dml_entities"], + entity: "DmlEntity", + args: { + methodSuffix: "DmlEntities", + }, + }, + ], + }) + ) }) - expect(resources).toBeDefined() - expect(resources.services).toHaveLength(1) - expect(resources.services[0]).toEqual(ModuleService) - expect(resources.models).toHaveLength(2) - expect(resources.models).toEqual( - expect.arrayContaining([ - expect.objectContaining({ name: upperCaseFirst(dmlEntity.name) }), - expect.objectContaining({ name: upperCaseFirst(EntityModel.name) }), + test("should return the correct resources and generate the correct joiner config from mikro orm entities", async () => { + const { ModuleService, EntityModel, Entity2 } = + ModuleWithoutJoinerConfigFixtures + + const moduleResolution: ModuleResolution = { + resolutionPath: join( + __dirname, + "../__fixtures__/module-without-joiner-config" + ), + definition: { + key: "module-without-joiner-config", + label: "Module without joiner config", + defaultPackage: false, + defaultModuleDeclaration: { + scope: "internal", + resources: "shared", + }, + }, + } + + expect( + (ModuleService.prototype as IModuleService).__joinerConfig + ).toBeUndefined() + + const resources = await loadResources({ + moduleResolution, + discoveryPath: moduleResolution.resolutionPath as string, + }) + + expect(resources).toBeDefined() + expect(resources.services).toHaveLength(1) + expect(resources.services[0]).toEqual(ModuleService) + expect(resources.models).toHaveLength(2) + expect(resources.models).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: upperCaseFirst(EntityModel.name) }), + expect.objectContaining({ name: upperCaseFirst(Entity2.name) }), + ]) + ) + expect(resources.repositories).toHaveLength(0) + expect(resources.loaders).toHaveLength(2) + expect(resources.loaders).toEqual([ + expect.objectContaining({ name: "connectionLoader" }), + expect.objectContaining({ name: "containerLoader" }), ]) - ) - expect(resources.repositories).toHaveLength(0) - expect(resources.loaders).toHaveLength(2) - expect(resources.loaders).toEqual([ - expect.objectContaining({ name: "connectionLoader" }), - expect.objectContaining({ name: "containerLoader" }), - ]) - expect(resources.moduleService).toEqual(ModuleService) - - expect( - (resources.moduleService.prototype as IModuleService).__joinerConfig - ).toBeDefined() - - const generatedJoinerConfig = ( - resources.moduleService.prototype as IModuleService - ).__joinerConfig?.()! - - expect(generatedJoinerConfig).toEqual( - expect.objectContaining({ - serviceName: "module-with-dml-mixed-without-joiner-config", + expect(resources.moduleService).toEqual(ModuleService) + + expect( + (resources.moduleService.prototype as IModuleService).__joinerConfig + ).toBeDefined() + + const generatedJoinerConfig = ( + resources.moduleService.prototype as IModuleService + ).__joinerConfig?.()! + + expect(generatedJoinerConfig).toEqual({ + serviceName: "module-without-joiner-config", primaryKeys: ["id"], linkableKeys: { - dml_entity_id: "DmlEntity", + entity2_id: "Entity2", entity_model_id: "EntityModel", }, + schema: "", alias: [ { - name: ["dml_entity", "dml_entities"], - entity: "DmlEntity", + name: ["entity2", "entity2s"], + entity: "Entity2", args: { - methodSuffix: "DmlEntities", + methodSuffix: "Entity2s", }, }, { @@ -91,240 +265,181 @@ describe("load internal - load resources", () => { }, ], }) - ) - }) + }) - test("should return the correct resources and generate the correct joiner config from DML entities", async () => { - const { ModuleService, entityModel, dmlEntity } = - ModuleWithDmlWithoutJoinerConfigFixtures + test("should return the correct resources and use the given joiner config", async () => { + const { ModuleService, EntityModel, Entity2 } = + ModuleWithJoinerConfigFixtures - const moduleResolution: ModuleResolution = { - resolutionPath: join( - __dirname, - "../__fixtures__/module-with-dml-without-joiner-config" - ), - definition: { - key: "module-with-dml-without-joiner-config", - label: "Module with DML without joiner config", - defaultPackage: false, - defaultModuleDeclaration: { - scope: "internal", - resources: "shared", + const moduleResolution: ModuleResolution = { + resolutionPath: join( + __dirname, + "../__fixtures__/module-with-joiner-config" + ), + definition: { + key: "module-without-joiner-config", + label: "Module without joiner config", + defaultPackage: false, + defaultModuleDeclaration: { + scope: "internal", + resources: "shared", + }, }, - }, - } + } - expect( - (ModuleService.prototype as IModuleService).__joinerConfig - ).toBeUndefined() + expect( + (ModuleService.prototype as IModuleService).__joinerConfig + ).toBeDefined() - const resources = await loadResources({ - moduleResolution, - discoveryPath: moduleResolution.resolutionPath as string, - }) + const resources = await loadResources({ + moduleResolution, + discoveryPath: moduleResolution.resolutionPath as string, + }) - expect(resources).toBeDefined() - expect(resources.services).toHaveLength(1) - expect(resources.services[0]).toEqual(ModuleService) - expect(resources.models).toHaveLength(2) - expect(resources.models).toEqual( - expect.arrayContaining([ - expect.objectContaining({ name: upperCaseFirst(dmlEntity.name) }), - expect.objectContaining({ name: upperCaseFirst(entityModel.name) }), + expect(resources).toBeDefined() + expect(resources.services).toHaveLength(1) + expect(resources.services[0]).toEqual(ModuleService) + expect(resources.models).toHaveLength(2) + expect(resources.models).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: upperCaseFirst(EntityModel.name) }), + expect.objectContaining({ name: upperCaseFirst(Entity2.name) }), + ]) + ) + expect(resources.repositories).toHaveLength(0) + expect(resources.loaders).toHaveLength(2) + expect(resources.loaders).toEqual([ + expect.objectContaining({ name: "connectionLoader" }), + expect.objectContaining({ name: "containerLoader" }), ]) - ) - expect(resources.repositories).toHaveLength(0) - expect(resources.loaders).toHaveLength(2) - expect(resources.loaders).toEqual([ - expect.objectContaining({ name: "connectionLoader" }), - expect.objectContaining({ name: "containerLoader" }), - ]) - expect(resources.moduleService).toEqual(ModuleService) - - expect( - (resources.moduleService.prototype as IModuleService).__joinerConfig - ).toBeDefined() - - const generatedJoinerConfig = ( - resources.moduleService.prototype as IModuleService - ).__joinerConfig?.()! - - expect(generatedJoinerConfig).toEqual( - expect.objectContaining({ - serviceName: "module-with-dml-without-joiner-config", + expect(resources.moduleService).toEqual(ModuleService) + + const generatedJoinerConfig = ( + resources.moduleService.prototype as IModuleService + ).__joinerConfig?.()! + + expect(generatedJoinerConfig).toEqual({ + serviceName: "module-service", primaryKeys: ["id"], - linkableKeys: { - entity_model_id: "EntityModel", - dml_entity_id: "DmlEntity", - }, + linkableKeys: {}, + schema: "", alias: [ { - name: ["entity_model", "entity_models"], - entity: "EntityModel", + name: ["custom_name"], + entity: "Custom", args: { - methodSuffix: "EntityModels", - }, - }, - { - name: ["dml_entity", "dml_entities"], - entity: "DmlEntity", - args: { - methodSuffix: "DmlEntities", + methodSuffix: "Customs", }, }, ], }) - ) + }) }) + }) - test("should return the correct resources and generate the correct joiner config from mikro orm entities", async () => { - const { ModuleService, EntityModel, Entity2 } = - ModuleWithoutJoinerConfigFixtures - + describe("loadInternalModule", () => { + test("should load the module and its providers using their identifier", async () => { const moduleResolution: ModuleResolution = { resolutionPath: join( __dirname, - "../__fixtures__/module-without-joiner-config" + "../__fixtures__/module-with-providers" ), + moduleDeclaration: { + scope: "internal", + resources: "shared", + }, definition: { - key: "module-without-joiner-config", - label: "Module without joiner config", + key: "module-with-providers", + label: "Module with providers", defaultPackage: false, defaultModuleDeclaration: { scope: "internal", resources: "shared", }, }, + options: { + providers: [ + { + resolve: join( + __dirname, + "../__fixtures__/module-with-providers/provider-1" + ), + id: "provider-1-id", + options: { + api_key: "test", + }, + }, + ], + }, } - expect( - (ModuleService.prototype as IModuleService).__joinerConfig - ).toBeUndefined() - - const resources = await loadResources({ - moduleResolution, - discoveryPath: moduleResolution.resolutionPath as string, + const container = createMedusaContainer() + await loadInternalModule({ + container: container, + resolution: moduleResolution, + logger: console as any, }) - expect(resources).toBeDefined() - expect(resources.services).toHaveLength(1) - expect(resources.services[0]).toEqual(ModuleService) - expect(resources.models).toHaveLength(2) - expect(resources.models).toEqual( - expect.arrayContaining([ - expect.objectContaining({ name: upperCaseFirst(EntityModel.name) }), - expect.objectContaining({ name: upperCaseFirst(Entity2.name) }), - ]) - ) - expect(resources.repositories).toHaveLength(0) - expect(resources.loaders).toHaveLength(2) - expect(resources.loaders).toEqual([ - expect.objectContaining({ name: "connectionLoader" }), - expect.objectContaining({ name: "containerLoader" }), - ]) - expect(resources.moduleService).toEqual(ModuleService) - - expect( - (resources.moduleService.prototype as IModuleService).__joinerConfig - ).toBeDefined() - - const generatedJoinerConfig = ( - resources.moduleService.prototype as IModuleService - ).__joinerConfig?.()! - - expect(generatedJoinerConfig).toEqual({ - serviceName: "module-without-joiner-config", - primaryKeys: ["id"], - linkableKeys: { - entity2_id: "Entity2", - entity_model_id: "EntityModel", - }, - schema: "", - alias: [ - { - name: ["entity2", "entity2s"], - entity: "Entity2", - args: { - methodSuffix: "Entity2s", - }, - }, - { - name: ["entity_model", "entity_models"], - entity: "EntityModel", - args: { - methodSuffix: "EntityModels", - }, - }, - ], - }) - }) + const moduleService = container.resolve(moduleResolution.definition.key) + const provider = (moduleService as any).container[ + getProviderRegistrationKey( + ModuleServiceWithProviderProvider1.identifier + ) + ] - test("should return the correct resources and use the given joiner config", async () => { - const { ModuleService, EntityModel, Entity2 } = - ModuleWithJoinerConfigFixtures + expect(moduleService).toBeInstanceOf(ModuleServiceWithProvider) + expect(provider).toBeInstanceOf(ModuleServiceWithProviderProvider1) + }) + test("should load the module and its providers using the provided id", async () => { const moduleResolution: ModuleResolution = { resolutionPath: join( __dirname, - "../__fixtures__/module-with-joiner-config" + "../__fixtures__/module-with-providers" ), + moduleDeclaration: { + scope: "internal", + resources: "shared", + }, definition: { - key: "module-without-joiner-config", - label: "Module without joiner config", + key: "module-with-providers", + label: "Module with providers", defaultPackage: false, defaultModuleDeclaration: { scope: "internal", resources: "shared", }, }, + options: { + providers: [ + { + resolve: join( + __dirname, + "../__fixtures__/module-with-providers/provider-2" + ), + id: "provider-2-id", + options: { + api_key: "test", + }, + }, + ], + }, } - expect( - (ModuleService.prototype as IModuleService).__joinerConfig - ).toBeDefined() - - const resources = await loadResources({ - moduleResolution, - discoveryPath: moduleResolution.resolutionPath as string, + const container = createMedusaContainer() + await loadInternalModule({ + container: container, + resolution: moduleResolution, + logger: console as any, }) - expect(resources).toBeDefined() - expect(resources.services).toHaveLength(1) - expect(resources.services[0]).toEqual(ModuleService) - expect(resources.models).toHaveLength(2) - expect(resources.models).toEqual( - expect.arrayContaining([ - expect.objectContaining({ name: upperCaseFirst(EntityModel.name) }), - expect.objectContaining({ name: upperCaseFirst(Entity2.name) }), - ]) - ) - expect(resources.repositories).toHaveLength(0) - expect(resources.loaders).toHaveLength(2) - expect(resources.loaders).toEqual([ - expect.objectContaining({ name: "connectionLoader" }), - expect.objectContaining({ name: "containerLoader" }), - ]) - expect(resources.moduleService).toEqual(ModuleService) - - const generatedJoinerConfig = ( - resources.moduleService.prototype as IModuleService - ).__joinerConfig?.()! - - expect(generatedJoinerConfig).toEqual({ - serviceName: "module-service", - primaryKeys: ["id"], - linkableKeys: {}, - schema: "", - alias: [ - { - name: ["custom_name"], - entity: "Custom", - args: { - methodSuffix: "Customs", - }, - }, - ], - }) + const moduleService = container.resolve(moduleResolution.definition.key) + const provider = (moduleService as any).container[ + getProviderRegistrationKey(moduleResolution.options!.providers![0].id) + ] + + expect(moduleService).toBeInstanceOf(ModuleServiceWithProvider) + expect(provider).toBeInstanceOf(ModuleServiceWithProviderProvider2) }) }) }) diff --git a/packages/core/modules-sdk/src/loaders/utils/load-internal.ts b/packages/core/modules-sdk/src/loaders/utils/load-internal.ts index 04417a20b4686..630114424a139 100644 --- a/packages/core/modules-sdk/src/loaders/utils/load-internal.ts +++ b/packages/core/modules-sdk/src/loaders/utils/load-internal.ts @@ -7,6 +7,9 @@ import { MedusaContainer, ModuleExports, ModuleLoaderFunction, + ModuleProvider, + ModuleProviderExports, + ModuleProviderLoaderFunction, ModuleResolution, } from "@medusajs/types" import { @@ -15,6 +18,8 @@ import { defineJoinerConfig, DmlEntity, dynamicImport, + isString, + MedusaModuleProviderType, MedusaModuleType, ModulesSdkUtils, toMikroOrmEntities, @@ -29,7 +34,7 @@ type ModuleResource = { services: Function[] models: Function[] repositories: Function[] - loaders: ModuleLoaderFunction[] + loaders: ModuleLoaderFunction[] | ModuleProviderLoaderFunction[] moduleService: Constructor normalizedPath: string } @@ -39,22 +44,36 @@ type MigrationFunction = ( moduleDeclaration?: InternalModuleDeclaration ) => Promise +type ResolvedModule = ModuleExports & { + discoveryPath: string +} + +type ResolvedModuleProvider = ModuleProviderExports & { + discoveryPath: string +} + +export const moduleProviderRegistrationKeyPrefix = "__providers__" + +/** + * Return the key used to register a module provider in the container + * @param {string} moduleKey + * @return {string} + */ +export function getProviderRegistrationKey(moduleKey: string): string { + return moduleProviderRegistrationKeyPrefix + moduleKey +} + export async function resolveModuleExports({ resolution, }: { resolution: ModuleResolution -}): Promise< - | (ModuleExports & { - discoveryPath: string - }) - | { error: any } -> { +}): Promise { let resolvedModuleExports: ModuleExports try { if (resolution.moduleExports) { // TODO: // If we want to benefit from the auto load mechanism, even if the module exports is provided, we need to ask for the module path - resolvedModuleExports = resolution.moduleExports + resolvedModuleExports = resolution.moduleExports as ModuleExports resolvedModuleExports.discoveryPath = resolution.resolutionPath as string } else { const module = await dynamicImport(resolution.resolutionPath as string) @@ -62,10 +81,12 @@ export async function resolveModuleExports({ if ("discoveryPath" in module) { const reExportedLoadedModule = await dynamicImport(module.discoveryPath) const discoveryPath = module.discoveryPath - resolvedModuleExports = reExportedLoadedModule.default + resolvedModuleExports = + reExportedLoadedModule.default ?? reExportedLoadedModule resolvedModuleExports.discoveryPath = discoveryPath as string } else { - resolvedModuleExports = (module as { default: ModuleExports }).default + resolvedModuleExports = + (module as { default: ModuleExports }).default ?? module resolvedModuleExports.discoveryPath = resolution.resolutionPath as string } @@ -90,13 +111,79 @@ export async function resolveModuleExports({ } } -export async function loadInternalModule( - container: MedusaContainer, - resolution: ModuleResolution, - logger: Logger, - migrationOnly?: boolean, - loaderOnly?: boolean +async function loadInternalProvider( + args: { + container: MedusaContainer + resolution: ModuleResolution + logger: Logger + migrationOnly?: boolean + loaderOnly?: boolean + }, + providers: ModuleProvider[] ): Promise<{ error?: Error } | void> { + const { container, resolution, logger, migrationOnly } = args + + const errors: { error?: Error }[] = [] + for (const provider of providers) { + const providerRes = provider.resolve as ModuleProviderExports + + const canLoadProvider = + providerRes && (isString(providerRes) || !providerRes?.services) + + if (!canLoadProvider) { + continue + } + + const res = await loadInternalModule({ + container, + resolution: { + ...resolution, + moduleExports: !isString(providerRes) ? providerRes : undefined, + definition: { + ...resolution.definition, + key: provider.id, + }, + resolutionPath: isString(provider.resolve) ? provider.resolve : false, + }, + logger, + migrationOnly, + loadingProviders: true, + }) + + if (res) { + errors.push(res) + } + } + + const errorMessages = errors.map((e) => e.error?.message).join("\n") + return errors.length + ? { + error: { + name: "ModuleProviderError", + message: `Errors while loading module providers for module ${resolution.definition.key}:\n${errorMessages}`, + stack: errors.map((e) => e.error?.stack).join("\n"), + }, + } + : undefined +} + +export async function loadInternalModule(args: { + container: MedusaContainer + resolution: ModuleResolution + logger: Logger + migrationOnly?: boolean + loaderOnly?: boolean + loadingProviders?: boolean +}): Promise<{ error?: Error } | void> { + const { + container, + resolution, + logger, + migrationOnly, + loaderOnly, + loadingProviders, + } = args + const keyName = !loaderOnly ? resolution.definition.key : resolution.definition.key + "__loaderOnly" @@ -121,7 +208,12 @@ export async function loadInternalModule( }) } - if (!loadedModule?.service && !moduleResources.moduleService) { + const loadedModule_ = loadedModule as ModuleExports + if ( + !loadingProviders && + !loadedModule_?.service && + !moduleResources.moduleService + ) { container.register({ [keyName]: asValue(undefined), }) @@ -133,20 +225,6 @@ export async function loadInternalModule( } } - if (migrationOnly) { - const moduleService_ = moduleResources.moduleService ?? loadedModule.service - - // Partially loaded module, only register the service __joinerConfig function to be able to resolve it later - const moduleService = { - __joinerConfig: moduleService_.prototype.__joinerConfig, - } - - container.register({ - [keyName]: asValue(moduleService), - }) - return - } - const localContainer = createMedusaContainer() const dependencies = resolution?.dependencies ?? [] @@ -177,6 +255,44 @@ export async function loadInternalModule( ) } + // if module has providers, load them + let providerOptions: any = undefined + if (!loadingProviders) { + const providers = (resolution?.options?.providers as any[]) ?? [] + + const res = await loadInternalProvider( + { + ...args, + container: localContainer, + }, + providers + ) + + if (res?.error) { + return res + } + } else { + providerOptions = (resolution?.options?.providers as any[]).find( + (p) => p.id === resolution.definition.key + )?.options + } + + if (migrationOnly && !loadingProviders) { + const moduleService_ = + moduleResources.moduleService ?? loadedModule_.service + + // Partially loaded module, only register the service __joinerConfig function to be able to resolve it later + const moduleService = { + __joinerConfig: moduleService_.prototype.__joinerConfig, + } + + container.register({ + [keyName]: asValue(moduleService), + }) + + return + } + const loaders = moduleResources.loaders ?? loadedModule?.loaders ?? [] const error = await runLoaders(loaders, { container, @@ -185,24 +301,56 @@ export async function loadInternalModule( resolution, loaderOnly, keyName, + providerOptions, }) if (error) { return error } - const moduleService = moduleResources.moduleService ?? loadedModule.service + if (loadingProviders) { + const loadedProvider_ = loadedModule as ModuleProviderExports - container.register({ - [keyName]: asFunction((cradle) => { - ;(moduleService as any).__type = MedusaModuleType - return new moduleService( - localContainer.cradle, - resolution.options, - resolution.moduleDeclaration + let moduleProviderServices = moduleResources.moduleService + ? [moduleResources.moduleService] + : loadedProvider_.services ?? loadedProvider_ + + if (!moduleProviderServices) { + return + } + + for (const moduleProviderService of moduleProviderServices) { + const modProvider_ = moduleProviderService as any + + modProvider_.identifier ??= keyName + modProvider_.__type = MedusaModuleProviderType + const registrationKey = getProviderRegistrationKey( + modProvider_.identifier ) - }).singleton(), - }) + container.register({ + [registrationKey]: asFunction((cradle) => { + ;(moduleProviderService as any).__type = MedusaModuleType + return new moduleProviderService( + localContainer.cradle, + resolution.options, + resolution.moduleDeclaration + ) + }).singleton(), + }) + } + } else { + const moduleService = moduleResources.moduleService ?? loadedModule_.service + container.register({ + [keyName]: asFunction((cradle) => { + ;(moduleService as any).__type = MedusaModuleType + return new moduleService( + localContainer.cradle, + resolution.options, + resolution.moduleDeclaration + ) + }).singleton(), + }) + } if (loaderOnly) { // The expectation is only to run the loader as standalone, so we do not need to register the service and we need to cleanup all services @@ -220,46 +368,114 @@ export async function loadModuleMigrations( revertMigration?: MigrationFunction generateMigration?: MigrationFunction }> { - const loadedModule = await resolveModuleExports({ + const mainLoadedModule = await resolveModuleExports({ resolution: { ...resolution, moduleExports }, }) - if ("error" in loadedModule) { - throw loadedModule.error + const loadedServices = [mainLoadedModule] as ( + | ResolvedModule + | ResolvedModuleProvider + )[] + + if (Array.isArray(resolution?.options?.providers)) { + for (const provider of (resolution.options as any).providers) { + const providerRes = provider.resolve as ModuleProviderExports + + const canLoadProvider = + providerRes && (isString(providerRes) || !providerRes?.services) + + if (!canLoadProvider) { + continue + } + + const loadedProvider = await resolveModuleExports({ + resolution: { + ...resolution, + moduleExports: !isString(providerRes) ? providerRes : undefined, + definition: { + ...resolution.definition, + key: provider.id, + }, + resolutionPath: isString(provider.resolve) ? provider.resolve : false, + }, + }) + loadedServices.push(loadedProvider as ResolvedModuleProvider) + } + } + + if ("error" in mainLoadedModule) { + throw mainLoadedModule.error } + const runMigrationsFn: ((...args) => Promise)[] = [] + const revertMigrationFn: ((...args) => Promise)[] = [] + const generateMigrationFn: ((...args) => Promise)[] = [] + try { - let runMigrations = loadedModule.runMigrations - let revertMigration = loadedModule.revertMigration - let generateMigration = loadedModule.generateMigration - - if (!runMigrations || !revertMigration) { - const moduleResources = await loadResources({ - moduleResolution: resolution, - discoveryPath: loadedModule.discoveryPath, - loadedModuleLoaders: loadedModule?.loaders, - }) + const migrationScripts: any[] = [] + for (const loadedModule of loadedServices) { + let runMigrationsCustom = loadedModule.runMigrations + let revertMigrationCustom = loadedModule.revertMigration + let generateMigrationCustom = loadedModule.generateMigration + + runMigrationsCustom && runMigrationsFn.push(runMigrationsCustom) + revertMigrationCustom && revertMigrationFn.push(revertMigrationCustom) + generateMigrationCustom && + generateMigrationFn.push(generateMigrationCustom) + + if (!runMigrationsCustom || !revertMigrationCustom) { + const moduleResources = await loadResources({ + moduleResolution: resolution, + discoveryPath: loadedModule.discoveryPath, + loadedModuleLoaders: loadedModule?.loaders, + }) - const migrationScriptOptions = { - moduleName: resolution.definition.key, - models: moduleResources.models, - pathToMigrations: join(moduleResources.normalizedPath, "migrations"), + migrationScripts.push({ + moduleName: resolution.definition.key, + models: moduleResources.models, + pathToMigrations: join(moduleResources.normalizedPath, "migrations"), + }) } - runMigrations ??= ModulesSdkUtils.buildMigrationScript( - migrationScriptOptions - ) - - revertMigration ??= ModulesSdkUtils.buildRevertMigrationScript( - migrationScriptOptions - ) + for (const migrationScriptOptions of migrationScripts) { + const migrationUp = + runMigrationsCustom ?? + ModulesSdkUtils.buildMigrationScript(migrationScriptOptions) + runMigrationsFn.push(migrationUp) + + const migrationDown = + revertMigrationCustom ?? + ModulesSdkUtils.buildRevertMigrationScript(migrationScriptOptions) + revertMigrationFn.push(migrationDown) + + const genMigration = + generateMigrationCustom ?? + ModulesSdkUtils.buildGenerateMigrationScript(migrationScriptOptions) + generateMigrationFn.push(genMigration) + } + } - generateMigration ??= ModulesSdkUtils.buildGenerateMigrationScript( - migrationScriptOptions - ) + const runMigrations = async (...args) => { + for (const migration of runMigrationsFn.filter(Boolean)) { + await migration.apply(migration, args) + } + } + const revertMigration = async (...args) => { + for (const migration of revertMigrationFn.filter(Boolean)) { + await migration.apply(migration, args) + } + } + const generateMigration = async (...args) => { + for (const migration of generateMigrationFn.filter(Boolean)) { + await migration.apply(migration, args) + } } - return { runMigrations, revertMigration, generateMigration } + return { + runMigrations, + revertMigration, + generateMigration, + } } catch { return {} } @@ -308,7 +524,7 @@ export async function loadResources({ moduleResolution: ModuleResolution discoveryPath: string logger?: Logger - loadedModuleLoaders?: ModuleLoaderFunction[] + loadedModuleLoaders?: ModuleLoaderFunction[] | ModuleProviderLoaderFunction[] }): Promise { logger ??= console as unknown as Logger loadedModuleLoaders ??= [] @@ -324,7 +540,8 @@ export async function loadResources({ const [moduleService, services, models, repositories] = await Promise.all([ dynamicImport(modulePath).then((moduleExports) => { - return moduleExports.default.service + const mod = moduleExports.default ?? moduleExports + return mod.service }), importAllFromDir(resolve(normalizedPath, "services")).catch( defaultOnFail @@ -365,11 +582,14 @@ export async function loadResources({ migrationPath: normalizedPath + "/migrations", }) - generateJoinerConfigIfNecessary({ - moduleResolution, - service: moduleService, - models: potentialModels, - }) + // if a module service is provided, we generate a joiner config + if (moduleService) { + generateJoinerConfigIfNecessary({ + moduleResolution, + service: moduleService, + models: potentialModels, + }) + } return { services: potentialServices, @@ -390,7 +610,15 @@ export async function loadResources({ async function runLoaders( loaders: Function[] = [], - { localContainer, container, logger, resolution, loaderOnly, keyName } + { + localContainer, + container, + logger, + resolution, + loaderOnly, + keyName, + providerOptions, + } ): Promise { try { for (const loader of loaders) { @@ -398,8 +626,9 @@ async function runLoaders( { container: localContainer, logger, - options: resolution.options, + options: providerOptions ?? resolution.options, dataLoaderOnly: loaderOnly, + moduleOptions: providerOptions ? resolution.options : undefined, }, resolution.moduleDeclaration as InternalModuleDeclaration ) @@ -418,14 +647,17 @@ async function runLoaders( } function prepareLoaders({ - loadedModuleLoaders = [] as ModuleLoaderFunction[], + loadedModuleLoaders = [] as + | ModuleLoaderFunction[] + | ModuleProviderLoaderFunction[], models, repositories, services, moduleResolution, migrationPath, }) { - const finalLoaders: ModuleLoaderFunction[] = [] + const finalLoaders: (ModuleLoaderFunction | ModuleProviderLoaderFunction)[] = + [] const toObjectReducer = (acc, curr) => { acc[curr.name] = curr diff --git a/packages/core/modules-sdk/src/medusa-app.ts b/packages/core/modules-sdk/src/medusa-app.ts index 0b7e4ad904555..0c737fbe7dd62 100644 --- a/packages/core/modules-sdk/src/medusa-app.ts +++ b/packages/core/modules-sdk/src/medusa-app.ts @@ -510,7 +510,7 @@ async function MedusaApp_({ modulePath: moduleResolution.resolutionPath as string, container: sharedContainer, options: moduleResolution.options, - moduleExports: moduleResolution.moduleExports, + moduleExports: moduleResolution.moduleExports as ModuleExports, } if (action === "revert") { diff --git a/packages/core/types/src/modules-sdk/index.ts b/packages/core/types/src/modules-sdk/index.ts index e953c9de25e9b..6713449154873 100644 --- a/packages/core/types/src/modules-sdk/index.ts +++ b/packages/core/types/src/modules-sdk/index.ts @@ -3,6 +3,7 @@ import { JoinerRelationship, JoinerServiceConfig } from "../joiner" import { MedusaContainer } from "../common" import { RepositoryService } from "../dal" import { Logger } from "../logger" +import { ModuleProviderExports } from "./module-provider" import { RemoteQueryGraph, RemoteQueryInput, @@ -86,7 +87,7 @@ export type ModuleResolution = { options?: Record dependencies?: string[] moduleDeclaration?: InternalModuleDeclaration | ExternalModuleDeclaration - moduleExports?: ModuleExports + moduleExports?: ModuleExports | ModuleProviderExports } export type ModuleDefinition = { diff --git a/packages/core/types/src/modules-sdk/module-provider.ts b/packages/core/types/src/modules-sdk/module-provider.ts index 33524247abe2a..851154fd66887 100644 --- a/packages/core/types/src/modules-sdk/module-provider.ts +++ b/packages/core/types/src/modules-sdk/module-provider.ts @@ -1,11 +1,47 @@ -import { Constructor } from "./index" +import { Logger } from "../logger" +import { + Constructor, + InternalModuleDeclaration, + MedusaContainer, +} from "./index" -export type ModuleProviderExports = { - services: Constructor[] +export type ProviderLoaderOptions> = { + container: MedusaContainer + options?: TOptions + logger?: Logger + moduleOptions: Record } +export type ModuleProviderExports = { + module?: string + services: Constructor[] + loaders?: ModuleProviderLoaderFunction[] + runMigrations?( + options: ProviderLoaderOptions, + moduleDeclaration?: any + ): Promise + revertMigration?( + options: ProviderLoaderOptions, + moduleDeclaration?: any + ): Promise + generateMigration?( + options: ProviderLoaderOptions, + moduleDeclaration?: any + ): Promise + /** + * Explicitly set the the true location of the module resources. + * Can be used to re-export the module from a different location and specify its original location. + */ + discoveryPath?: string +} + +export type ModuleProviderLoaderFunction = ( + options: ProviderLoaderOptions, + moduleDeclaration?: InternalModuleDeclaration +) => Promise + export type ModuleProvider = { - resolve: string | ModuleProviderExports + resolve: string | ModuleProviderExports id: string options?: Record is_default?: boolean diff --git a/packages/core/utils/src/common/__tests__/define-config.spec.ts b/packages/core/utils/src/common/__tests__/define-config.spec.ts index eaea563ed05ff..efb414002d32c 100644 --- a/packages/core/utils/src/common/__tests__/define-config.spec.ts +++ b/packages/core/utils/src/common/__tests__/define-config.spec.ts @@ -66,6 +66,9 @@ describe("defineConfig", function () { "inventory": { "resolve": "@medusajs/medusa/inventory-next", }, + "locking": { + "resolve": "@medusajs/medusa/locking", + }, "notification": { "options": { "providers": [ @@ -213,6 +216,9 @@ describe("defineConfig", function () { "inventory": { "resolve": "@medusajs/medusa/inventory-next", }, + "locking": { + "resolve": "@medusajs/medusa/locking", + }, "notification": { "options": { "providers": [ @@ -368,6 +374,9 @@ describe("defineConfig", function () { "inventory": { "resolve": "@medusajs/medusa/inventory-next", }, + "locking": { + "resolve": "@medusajs/medusa/locking", + }, "notification": { "options": { "providers": [ @@ -524,6 +533,9 @@ describe("defineConfig", function () { "inventory": { "resolve": "@medusajs/medusa/inventory-next", }, + "locking": { + "resolve": "@medusajs/medusa/locking", + }, "notification": { "options": { "providers": [ @@ -668,6 +680,9 @@ describe("defineConfig", function () { "inventory": { "resolve": "@medusajs/medusa/inventory-next", }, + "locking": { + "resolve": "@medusajs/medusa/locking", + }, "notification": { "options": { "providers": [ @@ -812,6 +827,9 @@ describe("defineConfig", function () { "inventory": { "resolve": "@medusajs/medusa/inventory-next", }, + "locking": { + "resolve": "@medusajs/medusa/locking", + }, "notification": { "options": { "providers": [ diff --git a/packages/core/utils/src/common/define-config.ts b/packages/core/utils/src/common/define-config.ts index db2e48991008f..9f83dbdf35891 100644 --- a/packages/core/utils/src/common/define-config.ts +++ b/packages/core/utils/src/common/define-config.ts @@ -8,10 +8,10 @@ import { Modules, REVERSED_MODULE_PACKAGE_NAMES, } from "../modules-sdk" -import { isString } from "./is-string" -import { resolveExports } from "./resolve-exports" import { isObject } from "./is-object" +import { isString } from "./is-string" import { normalizeImportPathWithSource } from "./normalize-import-path-with-source" +import { resolveExports } from "./resolve-exports" const DEFAULT_SECRET = "supersecret" const DEFAULT_ADMIN_URL = "http://localhost:9000" @@ -132,6 +132,7 @@ function resolveModules( { resolve: MODULE_PACKAGE_NAMES[Modules.CACHE] }, { resolve: MODULE_PACKAGE_NAMES[Modules.EVENT_BUS] }, { resolve: MODULE_PACKAGE_NAMES[Modules.WORKFLOW_ENGINE] }, + { resolve: MODULE_PACKAGE_NAMES[Modules.LOCKING] }, { resolve: MODULE_PACKAGE_NAMES[Modules.STOCK_LOCATION] }, { resolve: MODULE_PACKAGE_NAMES[Modules.INVENTORY] }, { resolve: MODULE_PACKAGE_NAMES[Modules.PRODUCT] }, diff --git a/packages/core/utils/src/index.ts b/packages/core/utils/src/index.ts index 6510e4470c878..5142f288daef5 100644 --- a/packages/core/utils/src/index.ts +++ b/packages/core/utils/src/index.ts @@ -1,4 +1,3 @@ -export * from "./graphql" export * from "./api-key" export * from "./auth" export * from "./bundles" @@ -12,6 +11,7 @@ export * from "./exceptions" export * from "./feature-flags" export * from "./file" export * from "./fulfillment" +export * from "./graphql" export * from "./inventory" export * from "./link" export * from "./modules-sdk" @@ -30,3 +30,4 @@ export * from "./totals/big-number" export * from "./user" export const MedusaModuleType = Symbol.for("MedusaModule") +export const MedusaModuleProviderType = Symbol.for("MedusaModuleProvider") diff --git a/packages/core/utils/src/modules-sdk/index.ts b/packages/core/utils/src/modules-sdk/index.ts index d5542028da117..c2df880e8716b 100644 --- a/packages/core/utils/src/modules-sdk/index.ts +++ b/packages/core/utils/src/modules-sdk/index.ts @@ -15,6 +15,7 @@ export * from "./medusa-service" export * from "./migration-scripts" export * from "./mikro-orm-cli-config-builder" export * from "./module" +export * from "./module-provider" export * from "./query-context" export * from "./types/links-config" export * from "./types/medusa-service" diff --git a/packages/core/utils/src/modules-sdk/module-provider.ts b/packages/core/utils/src/modules-sdk/module-provider.ts new file mode 100644 index 0000000000000..4381fd952bf27 --- /dev/null +++ b/packages/core/utils/src/modules-sdk/module-provider.ts @@ -0,0 +1,19 @@ +import { ModuleProviderExports } from "@medusajs/types" + +/** + * Wrapper to build the module provider export + * + * @param serviceName // The name of the module the provider is for + * @param services // The array of services that the module provides + * @param loaders // The loaders that the module provider provides + */ +export function ModuleProvider( + serviceName: string, + { services, loaders }: ModuleProviderExports +): ModuleProviderExports { + return { + module: serviceName, + services, + loaders, + } +} diff --git a/packages/medusa/package.json b/packages/medusa/package.json index 5c510c282a5d3..1d611aa7f190d 100644 --- a/packages/medusa/package.json +++ b/packages/medusa/package.json @@ -84,6 +84,7 @@ "@medusajs/inventory-next": "^0.0.3", "@medusajs/link-modules": "^0.2.11", "@medusajs/locking": "^0.0.1", + "@medusajs/locking-redis": "^0.0.1", "@medusajs/notification": "^0.1.2", "@medusajs/notification-local": "^0.0.1", "@medusajs/notification-sendgrid": "^0.0.1", diff --git a/packages/medusa/src/modules/locking-redis.ts b/packages/medusa/src/modules/locking-redis.ts new file mode 100644 index 0000000000000..4c389a39e0a5d --- /dev/null +++ b/packages/medusa/src/modules/locking-redis.ts @@ -0,0 +1,6 @@ +import RedisLockingProvider from "@medusajs/locking-redis" + +export * from "@medusajs/locking-redis" + +export default RedisLockingProvider +export const discoveryPath = require.resolve("@medusajs/locking-redis") diff --git a/packages/modules/cache-redis/src/services/redis-cache.ts b/packages/modules/cache-redis/src/services/redis-cache.ts index b794dffdba1e4..217af91fa181a 100644 --- a/packages/modules/cache-redis/src/services/redis-cache.ts +++ b/packages/modules/cache-redis/src/services/redis-cache.ts @@ -76,14 +76,28 @@ class RedisCacheService implements ICacheService { * @param key */ async invalidate(key: string): Promise { - const keys = await this.redis.keys(this.getCacheKey(key)) - const pipeline = this.redis.pipeline() + const pattern = this.getCacheKey(key) + let cursor = "0" + do { + const result = await this.redis.scan( + cursor, + "MATCH", + pattern, + "COUNT", + 100 + ) + cursor = result[0] + const keys = result[1] - keys.forEach(function (key) { - pipeline.del(key) - }) + if (keys.length > 0) { + const deletePipeline = this.redis.pipeline() + for (const key of keys) { + deletePipeline.del(key) + } - await pipeline.exec() + await deletePipeline.exec() + } + } while (cursor !== "0") } /** diff --git a/packages/modules/locking/integration-tests/__tests__/index.spec.ts b/packages/modules/locking/integration-tests/__tests__/index.spec.ts index d6c44467e75ed..a264d95ac0f72 100644 --- a/packages/modules/locking/integration-tests/__tests__/index.spec.ts +++ b/packages/modules/locking/integration-tests/__tests__/index.spec.ts @@ -1,5 +1,5 @@ import { ILockingModule } from "@medusajs/framework/types" -import { Modules } from "@medusajs/framework/utils" +import { Modules, promiseAll } from "@medusajs/framework/utils" import { moduleIntegrationTestRunner } from "medusa-test-utils" import { setTimeout } from "node:timers/promises" @@ -63,7 +63,7 @@ moduleIntegrationTestRunner({ expect(userReleased).toBe(false) await expect(anotherUserLock).rejects.toThrowError( - `"key_name" is already locked.` + `Failed to acquire lock for key "key_name"` ) const releasing = await service.release("key_name", { @@ -82,16 +82,20 @@ moduleIntegrationTestRunner({ ownerId: "user_id_000", } - expect(service.acquire(keyToLock, user_1)).resolves.toBeUndefined() + await expect( + service.acquire(keyToLock, user_1) + ).resolves.toBeUndefined() - expect(service.acquire(keyToLock, user_1)).resolves.toBeUndefined() + await expect( + service.acquire(keyToLock, user_1) + ).resolves.toBeUndefined() - expect(service.acquire(keyToLock, user_2)).rejects.toThrowError( - `"${keyToLock}" is already locked.` + await expect(service.acquire(keyToLock, user_2)).rejects.toThrowError( + `Failed to acquire lock for key "${keyToLock}"` ) - expect(service.acquire(keyToLock, user_2)).rejects.toThrowError( - `"${keyToLock}" is already locked.` + await expect(service.acquire(keyToLock, user_2)).rejects.toThrowError( + `Failed to acquire lock for key "${keyToLock}"` ) await service.acquire(keyToLock, user_1) @@ -104,6 +108,40 @@ moduleIntegrationTestRunner({ const release = await service.release(keyToLock, user_1) expect(release).toBe(true) }) + + it("should fail to acquire the same key when no owner is provided", async () => { + const keyToLock = "mySpecialKey" + + const user_2 = { + ownerId: "user_id_000", + } + + await expect(service.acquire(keyToLock)).resolves.toBeUndefined() + + await expect(service.acquire(keyToLock)).rejects.toThrow( + `Failed to acquire lock for key "${keyToLock}"` + ) + + await expect(service.acquire(keyToLock)).rejects.toThrow( + `Failed to acquire lock for key "${keyToLock}"` + ) + + await expect(service.acquire(keyToLock, user_2)).rejects.toThrow( + `Failed to acquire lock for key "${keyToLock}"` + ) + + await expect(service.acquire(keyToLock, user_2)).rejects.toThrow( + `Failed to acquire lock for key "${keyToLock}"` + ) + + const releaseNotLocked = await service.release(keyToLock, { + ownerId: "user_id_000", + }) + expect(releaseNotLocked).toBe(false) + + const release = await service.release(keyToLock) + expect(release).toBe(true) + }) }) it("should release lock in case of failure", async () => { @@ -118,5 +156,48 @@ moduleIntegrationTestRunner({ expect(fn_1).toBeCalledTimes(1) expect(fn_2).toBeCalledTimes(1) }) + + it("should release lock in case of timeout failure", async () => { + const fn_1 = jest.fn(async () => { + await setTimeout(1010) + return "fn_1" + }) + + const fn_2 = jest.fn(async () => { + return "fn_2" + }) + + const fn_3 = jest.fn(async () => { + return "fn_3" + }) + + const ops = [ + service + .execute("lock_key", fn_1, { + timeout: 1, + }) + .catch((e) => e), + + service + .execute("lock_key", fn_2, { + timeout: 1, + }) + .catch((e) => e), + + service + .execute("lock_key", fn_3, { + timeout: 2, + }) + .catch((e) => e), + ] + + const res = await promiseAll(ops) + + expect(res).toEqual(["fn_1", Error("Timed-out acquiring lock."), "fn_3"]) + + expect(fn_1).toHaveBeenCalledTimes(1) + expect(fn_2).toHaveBeenCalledTimes(0) + expect(fn_3).toHaveBeenCalledTimes(1) + }) }, }) diff --git a/packages/modules/locking/package.json b/packages/modules/locking/package.json index a78a0bdea018b..15e20f26cb035 100644 --- a/packages/modules/locking/package.json +++ b/packages/modules/locking/package.json @@ -8,6 +8,12 @@ "url": "https://github.com/medusajs/medusa", "directory": "packages/locking" }, + "files": [ + "dist", + "!dist/**/__tests__", + "!dist/**/__mocks__", + "!dist/**/__fixtures__" + ], "publishConfig": { "access": "public" }, diff --git a/packages/modules/locking/src/index.ts b/packages/modules/locking/src/index.ts index d432f2fa58a12..86a0ba40fef18 100644 --- a/packages/modules/locking/src/index.ts +++ b/packages/modules/locking/src/index.ts @@ -1,6 +1,6 @@ import { Module, Modules } from "@medusajs/framework/utils" -import { LockingModuleService } from "@services" -import loadProviders from "./loaders/providers" +import { default as loadProviders } from "./loaders/providers" +import LockingModuleService from "./services/locking-module" export default Module(Modules.LOCKING, { service: LockingModuleService, diff --git a/packages/modules/locking/src/loaders/providers.ts b/packages/modules/locking/src/loaders/providers.ts index 7e59bb9c90837..be2b39cc5b5a4 100644 --- a/packages/modules/locking/src/loaders/providers.ts +++ b/packages/modules/locking/src/loaders/providers.ts @@ -11,19 +11,13 @@ import { LockingIdentifiersRegistrationName, LockingProviderRegistrationPrefix, } from "@types" -import { Lifetime, asFunction, asValue } from "awilix" +import { Lifetime, aliasTo, asFunction, asValue } from "awilix" import { InMemoryLockingProvider } from "../providers/in-memory" -const registrationFn = async (klass, container, pluginOptions) => { +const registrationFn = async (klass, container) => { const key = LockingProviderService.getRegistrationIdentifier(klass) - container.register({ - [LockingProviderRegistrationPrefix + key]: asFunction( - (cradle) => new klass(cradle, pluginOptions.options), - { - lifetime: klass.LIFE_TIME || Lifetime.SINGLETON, - } - ), + [LockingProviderRegistrationPrefix + key]: aliasTo("__providers__" + key), }) container.registerAdd(LockingIdentifiersRegistrationName, asValue(key)) diff --git a/packages/modules/locking/src/providers/in-memory.ts b/packages/modules/locking/src/providers/in-memory.ts index acb327bb84851..2374f8c44287a 100644 --- a/packages/modules/locking/src/providers/in-memory.ts +++ b/packages/modules/locking/src/providers/in-memory.ts @@ -38,24 +38,27 @@ export class InMemoryLockingProvider implements ILockingProvider { timeout?: number } ): Promise { - keys = Array.isArray(keys) ? keys : [keys] - - const timeoutSeconds = args?.timeout ?? 5 + const timeout = Math.max(args?.timeout ?? 5, 1) + const timeoutSeconds = Number.isNaN(timeout) ? 1 : timeout + const cancellationToken = { cancelled: false } const promises: Promise[] = [] if (timeoutSeconds > 0) { - promises.push(this.getTimeout(timeoutSeconds)) + promises.push(this.getTimeout(timeoutSeconds, cancellationToken)) } promises.push( - this.acquire(keys, { - awaitQueue: true, - }) + this.acquire_( + keys, + { + expire: timeoutSeconds, + awaitQueue: true, + }, + cancellationToken + ) ) - await Promise.race(promises).catch(async (err) => { - await this.release(keys) - }) + await Promise.race(promises) try { return await job() @@ -71,6 +74,18 @@ export class InMemoryLockingProvider implements ILockingProvider { expire?: number awaitQueue?: boolean } + ): Promise { + return this.acquire_(keys, args) + } + + async acquire_( + keys: string | string[], + args?: { + ownerId?: string | null + expire?: number + awaitQueue?: boolean + }, + cancellationToken?: { cancelled: boolean } ): Promise { keys = Array.isArray(keys) ? keys : [keys] const { ownerId, expire } = args ?? {} @@ -100,7 +115,7 @@ export class InMemoryLockingProvider implements ILockingProvider { continue } - if (lock.ownerId === ownerId) { + if (lock.ownerId !== null && lock.ownerId === ownerId) { if (expire) { lock.expiration = now + expire * 1000 this.locks.set(key, lock) @@ -111,10 +126,14 @@ export class InMemoryLockingProvider implements ILockingProvider { if (lock.currentPromise && args?.awaitQueue) { await lock.currentPromise.promise + if (cancellationToken?.cancelled) { + return + } + return this.acquire(keys, args) } - throw new Error(`"${key}" is already locked.`) + throw new Error(`Failed to acquire lock for key "${key}"`) } } @@ -166,9 +185,13 @@ export class InMemoryLockingProvider implements ILockingProvider { } } - private async getTimeout(seconds: number): Promise { + private async getTimeout( + seconds: number, + cancellationToken: { cancelled: boolean } + ): Promise { return new Promise((_, reject) => { setTimeout(() => { + cancellationToken.cancelled = true reject(new Error("Timed-out acquiring lock.")) }, seconds * 1000) }) diff --git a/packages/modules/providers/auth-emailpass/src/index.ts b/packages/modules/providers/auth-emailpass/src/index.ts index 1b5c666fb3054..02f35016ebe65 100644 --- a/packages/modules/providers/auth-emailpass/src/index.ts +++ b/packages/modules/providers/auth-emailpass/src/index.ts @@ -1,10 +1,8 @@ -import { ModuleProviderExports } from "@medusajs/framework/types" +import { ModuleProvider, Modules } from "@medusajs/framework/utils" import { EmailPassAuthService } from "./services/emailpass" const services = [EmailPassAuthService] -const providerExport: ModuleProviderExports = { +export default ModuleProvider(Modules.AUTH, { services, -} - -export default providerExport +}) diff --git a/packages/modules/providers/auth-github/src/index.ts b/packages/modules/providers/auth-github/src/index.ts index 70fde0eefd0b8..190e1639db755 100644 --- a/packages/modules/providers/auth-github/src/index.ts +++ b/packages/modules/providers/auth-github/src/index.ts @@ -1,10 +1,8 @@ -import { ModuleProviderExports } from "@medusajs/framework/types" +import { ModuleProvider, Modules } from "@medusajs/framework/utils" import { GithubAuthService } from "./services/github" const services = [GithubAuthService] -const providerExport: ModuleProviderExports = { +export default ModuleProvider(Modules.AUTH, { services, -} - -export default providerExport +}) diff --git a/packages/modules/providers/auth-google/src/index.ts b/packages/modules/providers/auth-google/src/index.ts index 9245ab87564c1..4cea9bfbe5a32 100644 --- a/packages/modules/providers/auth-google/src/index.ts +++ b/packages/modules/providers/auth-google/src/index.ts @@ -1,10 +1,8 @@ -import { ModuleProviderExports } from "@medusajs/framework/types" +import { ModuleProvider, Modules } from "@medusajs/framework/utils" import { GoogleAuthService } from "./services/google" const services = [GoogleAuthService] -const providerExport: ModuleProviderExports = { +export default ModuleProvider(Modules.AUTH, { services, -} - -export default providerExport +}) diff --git a/packages/modules/providers/file-local/src/index.ts b/packages/modules/providers/file-local/src/index.ts index 92f1b8dbad2e6..2803b6c867c0f 100644 --- a/packages/modules/providers/file-local/src/index.ts +++ b/packages/modules/providers/file-local/src/index.ts @@ -1,10 +1,8 @@ -import { ModuleProviderExports } from "@medusajs/framework/types" +import { ModuleProvider, Modules } from "@medusajs/framework/utils" import { LocalFileService } from "./services/local-file" const services = [LocalFileService] -const providerExport: ModuleProviderExports = { +export default ModuleProvider(Modules.FILE, { services, -} - -export default providerExport +}) diff --git a/packages/modules/providers/file-s3/src/index.ts b/packages/modules/providers/file-s3/src/index.ts index 741be94297089..4851232c6fd80 100644 --- a/packages/modules/providers/file-s3/src/index.ts +++ b/packages/modules/providers/file-s3/src/index.ts @@ -1,10 +1,8 @@ -import { ModuleProviderExports } from "@medusajs/framework/types" +import { ModuleProvider, Modules } from "@medusajs/framework/utils" import { S3FileService } from "./services/s3-file" const services = [S3FileService] -const providerExport: ModuleProviderExports = { +export default ModuleProvider(Modules.FILE, { services, -} - -export default providerExport +}) diff --git a/packages/modules/providers/fulfillment-manual/src/index.ts b/packages/modules/providers/fulfillment-manual/src/index.ts index f7652183da504..93b20068a8207 100644 --- a/packages/modules/providers/fulfillment-manual/src/index.ts +++ b/packages/modules/providers/fulfillment-manual/src/index.ts @@ -1,10 +1,8 @@ -import { ModuleProviderExports } from "@medusajs/framework/types" +import { ModuleProvider, Modules } from "@medusajs/framework/utils" import { ManualFulfillmentService } from "./services/manual-fulfillment" const services = [ManualFulfillmentService] -const providerExport: ModuleProviderExports = { +export default ModuleProvider(Modules.FULFILLMENT, { services, -} - -export default providerExport +}) diff --git a/packages/modules/providers/locking-redis/.gitignore b/packages/modules/providers/locking-redis/.gitignore new file mode 100644 index 0000000000000..83cb36a41ea17 --- /dev/null +++ b/packages/modules/providers/locking-redis/.gitignore @@ -0,0 +1,4 @@ +dist +node_modules +.DS_store +yarn.lock diff --git a/packages/modules/providers/locking-redis/integration-tests/__tests__/index.spec.ts b/packages/modules/providers/locking-redis/integration-tests/__tests__/index.spec.ts new file mode 100644 index 0000000000000..960282039af05 --- /dev/null +++ b/packages/modules/providers/locking-redis/integration-tests/__tests__/index.spec.ts @@ -0,0 +1,220 @@ +import { ILockingModule } from "@medusajs/framework/types" +import { Modules, promiseAll } from "@medusajs/framework/utils" +import { moduleIntegrationTestRunner } from "medusa-test-utils" +import { setTimeout } from "node:timers/promises" + +jest.setTimeout(5000) + +const providerId = "locking-redis" +moduleIntegrationTestRunner({ + moduleName: Modules.LOCKING, + moduleOptions: { + providers: [ + { + id: providerId, + resolve: require.resolve("../../src"), + is_default: true, + options: { + redisUrl: process.env.REDIS_URL ?? "redis://localhost:6379", + }, + }, + ], + }, + testSuite: ({ service }) => { + describe("Locking Module Service", () => { + let stock = 5 + function replenishStock() { + stock = 5 + } + function hasStock() { + return stock > 0 + } + async function reduceStock() { + await setTimeout(10) + stock-- + } + async function buy() { + if (hasStock()) { + await reduceStock() + return true + } + return false + } + + beforeEach(async () => { + await service.releaseAll() + }) + + it("should execute functions respecting the key locked", async () => { + // 10 parallel calls to buy should oversell the stock + const prom: any[] = [] + for (let i = 0; i < 10; i++) { + prom.push(buy()) + } + await Promise.all(prom) + expect(stock).toBe(-5) + + replenishStock() + + // 10 parallel calls to buy with lock should not oversell the stock + const promWLock: any[] = [] + for (let i = 0; i < 10; i++) { + promWLock.push(service.execute("item_1", buy)) + } + await Promise.all(promWLock) + + expect(stock).toBe(0) + }) + + it("should acquire lock and release it", async () => { + await service.acquire("key_name", { + ownerId: "user_id_123", + }) + + const userReleased = await service.release("key_name", { + ownerId: "user_id_456", + }) + const anotherUserLock = service.acquire("key_name", { + ownerId: "user_id_456", + }) + + expect(userReleased).toBe(false) + await expect(anotherUserLock).rejects.toThrow( + `Failed to acquire lock for key "key_name"` + ) + + const releasing = await service.release("key_name", { + ownerId: "user_id_123", + }) + + expect(releasing).toBe(true) + }) + + it("should acquire lock and release it during parallel calls", async () => { + const keyToLock = "mySpecialKey" + const user_1 = { + ownerId: "user_id_456", + } + const user_2 = { + ownerId: "user_id_000", + } + + await expect( + service.acquire(keyToLock, user_1) + ).resolves.toBeUndefined() + + await expect( + service.acquire(keyToLock, user_1) + ).resolves.toBeUndefined() + + await expect(service.acquire(keyToLock, user_2)).rejects.toThrow( + `Failed to acquire lock for key "${keyToLock}"` + ) + + await expect(service.acquire(keyToLock, user_2)).rejects.toThrow( + `Failed to acquire lock for key "${keyToLock}"` + ) + + await service.acquire(keyToLock, user_1) + + const releaseNotLocked = await service.release(keyToLock, { + ownerId: "user_id_000", + }) + expect(releaseNotLocked).toBe(false) + + const release = await service.release(keyToLock, user_1) + expect(release).toBe(true) + }) + + it("should fail to acquire the same key when no owner is provided", async () => { + const keyToLock = "mySpecialKey" + + const user_2 = { + ownerId: "user_id_000", + } + + await expect(service.acquire(keyToLock)).resolves.toBeUndefined() + + await expect(service.acquire(keyToLock)).rejects.toThrow( + `Failed to acquire lock for key "${keyToLock}"` + ) + + await expect(service.acquire(keyToLock)).rejects.toThrow( + `Failed to acquire lock for key "${keyToLock}"` + ) + + await expect(service.acquire(keyToLock, user_2)).rejects.toThrow( + `Failed to acquire lock for key "${keyToLock}"` + ) + + await expect(service.acquire(keyToLock, user_2)).rejects.toThrow( + `Failed to acquire lock for key "${keyToLock}"` + ) + + const releaseNotLocked = await service.release(keyToLock, { + ownerId: "user_id_000", + }) + expect(releaseNotLocked).toBe(false) + + const release = await service.release(keyToLock) + expect(release).toBe(true) + }) + }) + + it("should release lock in case of failure", async () => { + const fn_1 = jest.fn(async () => { + throw new Error("Error") + }) + const fn_2 = jest.fn(async () => {}) + + await service.execute("lock_key", fn_1).catch(() => {}) + await service.execute("lock_key", fn_2).catch(() => {}) + + expect(fn_1).toHaveBeenCalledTimes(1) + expect(fn_2).toHaveBeenCalledTimes(1) + }) + + it("should release lock in case of timeout failure", async () => { + const fn_1 = jest.fn(async () => { + await setTimeout(1010) + return "fn_1" + }) + + const fn_2 = jest.fn(async () => { + return "fn_2" + }) + + const fn_3 = jest.fn(async () => { + return "fn_3" + }) + + const ops = [ + service + .execute("lock_key", fn_1, { + timeout: 1, + }) + .catch((e) => e), + + service + .execute("lock_key", fn_2, { + timeout: 1, + }) + .catch((e) => e), + + service + .execute("lock_key", fn_3, { + timeout: 5, + }) + .catch((e) => e), + ] + + const res = await promiseAll(ops) + + expect(res).toEqual(["fn_1", Error("Timed-out acquiring lock."), "fn_3"]) + + expect(fn_1).toHaveBeenCalledTimes(1) + expect(fn_2).toHaveBeenCalledTimes(0) + expect(fn_3).toHaveBeenCalledTimes(1) + }) + }, +}) diff --git a/packages/modules/providers/locking-redis/jest.config.js b/packages/modules/providers/locking-redis/jest.config.js new file mode 100644 index 0000000000000..818699559a62f --- /dev/null +++ b/packages/modules/providers/locking-redis/jest.config.js @@ -0,0 +1,10 @@ +const defineJestConfig = require("../../../../define_jest_config") +module.exports = defineJestConfig({ + moduleNameMapper: { + "^@models": "/src/models", + "^@services": "/src/services", + "^@repositories": "/src/repositories", + "^@types": "/src/types", + "^@utils": "/src/utils", + }, +}) diff --git a/packages/modules/providers/locking-redis/package.json b/packages/modules/providers/locking-redis/package.json new file mode 100644 index 0000000000000..ae05355d091f2 --- /dev/null +++ b/packages/modules/providers/locking-redis/package.json @@ -0,0 +1,48 @@ +{ + "name": "@medusajs/locking-redis", + "version": "0.0.1", + "description": "Redis Lock for Medusa", + "main": "dist/index.js", + "repository": { + "type": "git", + "url": "https://github.com/medusajs/medusa", + "directory": "packages/locking-redis" + }, + "files": [ + "dist", + "!dist/**/__tests__", + "!dist/**/__mocks__", + "!dist/**/__fixtures__" + ], + "engines": { + "node": ">=20" + }, + "author": "Medusa", + "license": "MIT", + "devDependencies": { + "@medusajs/framework": "^0.0.1", + "@swc/core": "^1.7.28", + "@swc/jest": "^0.2.36", + "jest": "^29.7.0", + "rimraf": "^5.0.1", + "typescript": "^5.6.2" + }, + "peerDependencies": { + "@medusajs/framework": "^0.0.1" + }, + "dependencies": { + "ioredis": "^5.4.1" + }, + "scripts": { + "watch": "tsc --build --watch", + "watch:test": "tsc --build tsconfig.spec.json --watch", + "resolve:aliases": "tsc --showConfig -p tsconfig.json > tsconfig.resolved.json && tsc-alias -p tsconfig.resolved.json && rimraf tsconfig.resolved.json", + "build": "rimraf dist && tsc --build && npm run resolve:aliases", + "test": "jest --passWithNoTests src", + "test:integration": "jest --runInBand --forceExit -- integration-tests/**/__tests__/**/*.spec.ts" + }, + "keywords": [ + "medusa-providers", + "medusa-providers-locking" + ] +} diff --git a/packages/modules/providers/locking-redis/src/index.ts b/packages/modules/providers/locking-redis/src/index.ts new file mode 100644 index 0000000000000..ef5860a634478 --- /dev/null +++ b/packages/modules/providers/locking-redis/src/index.ts @@ -0,0 +1,11 @@ +import { ModuleProvider, Modules } from "@medusajs/framework/utils" +import Loader from "./loaders" +import { RedisLockingProvider } from "./services/redis-lock" + +const services = [RedisLockingProvider] +const loaders = [Loader] + +export default ModuleProvider(Modules.LOCKING, { + services, + loaders, +}) diff --git a/packages/modules/providers/locking-redis/src/loaders/index.ts b/packages/modules/providers/locking-redis/src/loaders/index.ts new file mode 100644 index 0000000000000..5f8ad5ce12b88 --- /dev/null +++ b/packages/modules/providers/locking-redis/src/loaders/index.ts @@ -0,0 +1,41 @@ +import { Modules } from "@medusajs/framework/utils" +import { ProviderLoaderOptions } from "@medusajs/types" +import { RedisCacheModuleOptions } from "@types" +import { asValue } from "awilix" +import Redis from "ioredis" + +export default async ({ + container, + logger, + options, + moduleOptions, +}: ProviderLoaderOptions): Promise => { + const { redisUrl, redisOptions, namespace } = + options as RedisCacheModuleOptions + + if (!redisUrl) { + throw Error( + `No "redisUrl" provided in "${Modules.LOCKING}" module, "locking-redis" provider options. It is required for the "locking-redis" Module provider.` + ) + } + + const connection = new Redis(redisUrl, { + // Lazy connect to properly handle connection errors + lazyConnect: true, + ...(redisOptions ?? {}), + }) + + try { + await connection.connect() + logger?.info(`Connection to Redis in "locking-redis" provider established`) + } catch (err) { + logger?.error( + `An error occurred while connecting to Redis in provider "locking-redis": ${err}` + ) + } + + container.register({ + redisClient: asValue(connection), + prefix: asValue(namespace ?? "medusa_lock:"), + }) +} diff --git a/packages/modules/providers/locking-redis/src/services/redis-lock.ts b/packages/modules/providers/locking-redis/src/services/redis-lock.ts new file mode 100644 index 0000000000000..81c1e365176d4 --- /dev/null +++ b/packages/modules/providers/locking-redis/src/services/redis-lock.ts @@ -0,0 +1,280 @@ +import { promiseAll } from "@medusajs/framework/utils" +import { ILockingProvider } from "@medusajs/types" +import { RedisCacheModuleOptions } from "@types" +import { Redis } from "ioredis" +import { setTimeout } from "node:timers/promises" + +export class RedisLockingProvider implements ILockingProvider { + static identifier = "locking-redis" + + protected redisClient: Redis & { + acquireLock: ( + key: string, + ownerId: string, + ttl: number, + awaitQueue?: boolean + ) => Promise + releaseLock: (key: string, ownerId: string) => Promise + } + protected keyNamePrefix: string + protected waitLockingTimeout: number = 5 + protected defaultRetryInterval: number = 5 + protected maximumRetryInterval: number = 200 + + constructor({ redisClient, prefix }, options: RedisCacheModuleOptions) { + this.redisClient = redisClient + this.keyNamePrefix = prefix ?? "medusa_lock:" + + if (!isNaN(+options?.waitLockingTimeout!)) { + this.waitLockingTimeout = +options.waitLockingTimeout! + } + + if (!isNaN(+options?.defaultRetryInterval!)) { + this.defaultRetryInterval = +options.defaultRetryInterval! + } + + if (!isNaN(+options?.maximumRetryInterval!)) { + this.maximumRetryInterval = +options.maximumRetryInterval! + } + + // Define the custom command for acquiring locks + this.redisClient.defineCommand("acquireLock", { + numberOfKeys: 1, + lua: ` + local key = KEYS[1] + local ownerId = ARGV[1] + local ttl = tonumber(ARGV[2]) + local awaitQueue = ARGV[3] == 'true' + + local setArgs = {key, ownerId, 'NX'} + if ttl > 0 then + table.insert(setArgs, 'EX') + table.insert(setArgs, ttl) + end + + local setResult = redis.call('SET', unpack(setArgs)) + + if setResult then + return 1 + elseif not awaitQueue then + -- Key already exists; retrieve the current ownerId + local currentOwnerId = redis.call('GET', key) + if currentOwnerId == '*' then + return 0 + elseif currentOwnerId == ownerId then + setArgs = {key, ownerId, 'XX'} + if ttl > 0 then + table.insert(setArgs, 'EX') + table.insert(setArgs, ttl) + end + redis.call('SET', unpack(setArgs)) + return 1 + else + return 0 + end + else + return 0 + end + + `, + }) + + // Define the custom command for releasing locks + this.redisClient.defineCommand("releaseLock", { + numberOfKeys: 1, + lua: ` + local key = KEYS[1] + local ownerId = ARGV[1] + + if redis.call('GET', key) == ownerId then + return redis.call('DEL', key) + else + return 0 + end + `, + }) + } + + private getKeyName(key: string): string { + return `${this.keyNamePrefix}${key}` + } + + async execute( + keys: string | string[], + job: () => Promise, + args?: { + timeout?: number + } + ): Promise { + const timeout = Math.max(args?.timeout ?? this.waitLockingTimeout, 1) + const timeoutSeconds = Number.isNaN(timeout) ? 1 : timeout + + const cancellationToken = { cancelled: false } + const promises: Promise[] = [] + if (timeoutSeconds > 0) { + promises.push(this.getTimeout(timeoutSeconds, cancellationToken)) + } + + promises.push( + this.acquire_( + keys, + { + awaitQueue: true, + expire: args?.timeout ? timeoutSeconds : 0, + }, + cancellationToken + ) + ) + + await Promise.race(promises) + + try { + return await job() + } finally { + await this.release(keys) + } + } + + async acquire( + keys: string | string[], + args?: { + ownerId?: string + expire?: number + awaitQueue?: boolean + } + ): Promise { + return this.acquire_(keys, args) + } + + async acquire_( + keys: string | string[], + args?: { + ownerId?: string + expire?: number + awaitQueue?: boolean + }, + cancellationToken?: { cancelled: boolean } + ): Promise { + keys = Array.isArray(keys) ? keys : [keys] + + const timeout = Math.max(args?.expire ?? this.waitLockingTimeout, 1) + const timeoutSeconds = Number.isNaN(timeout) ? 1 : timeout + let retryTimes = 0 + + const ownerId = args?.ownerId ?? "*" + const awaitQueue = args?.awaitQueue ?? false + + const acquirePromises = keys.map(async (key) => { + const errMessage = `Failed to acquire lock for key "${key}"` + const keyName = this.getKeyName(key) + + const acquireLock = async () => { + while (true) { + if (cancellationToken?.cancelled) { + throw new Error(errMessage) + } + + const result = await this.redisClient.acquireLock( + keyName, + ownerId, + args?.expire ? timeoutSeconds : 0, + awaitQueue + ) + + if (result === 1) { + break + } else { + if (awaitQueue) { + // Wait for a short period before retrying + await setTimeout( + Math.min( + this.defaultRetryInterval + + (retryTimes / 10) * this.defaultRetryInterval, + this.maximumRetryInterval + ) + ) + retryTimes++ + } else { + throw new Error(errMessage) + } + } + } + } + + await acquireLock() + }) + + await promiseAll(acquirePromises) + } + + async release( + keys: string | string[], + args?: { + ownerId?: string | null + } + ): Promise { + const ownerId = args?.ownerId ?? "*" + keys = Array.isArray(keys) ? keys : [keys] + + const releasePromises = keys.map(async (key) => { + const keyName = this.getKeyName(key) + const result = await this.redisClient.releaseLock(keyName, ownerId) + return result === 1 + }) + + const results = await promiseAll(releasePromises) + + return results.every((released) => released) + } + + async releaseAll(args?: { ownerId?: string | null }): Promise { + const ownerId = args?.ownerId ?? "*" + + const pattern = `${this.keyNamePrefix}*` + let cursor = "0" + + do { + const result = await this.redisClient.scan( + cursor, + "MATCH", + pattern, + "COUNT", + 100 + ) + cursor = result[0] + const keys = result[1] + + if (keys.length > 0) { + const pipeline = this.redisClient.pipeline() + + keys.forEach((key) => { + pipeline.get(key) + }) + + const currentOwners = await pipeline.exec() + + const deletePipeline = this.redisClient.pipeline() + keys.forEach((key, idx) => { + const currentOwner = currentOwners?.[idx]?.[1] + + if (currentOwner === ownerId) { + deletePipeline.del(key) + } + }) + + await deletePipeline.exec() + } + } while (cursor !== "0") + } + + private async getTimeout( + seconds: number, + cancellationToken: { cancelled: boolean } + ): Promise { + return new Promise(async (_, reject) => { + await setTimeout(seconds * 1000) + cancellationToken.cancelled = true + reject(new Error("Timed-out acquiring lock.")) + }) + } +} diff --git a/packages/modules/providers/locking-redis/src/types/index.ts b/packages/modules/providers/locking-redis/src/types/index.ts new file mode 100644 index 0000000000000..702e0df1ec204 --- /dev/null +++ b/packages/modules/providers/locking-redis/src/types/index.ts @@ -0,0 +1,45 @@ +import { RedisOptions } from "ioredis" + +/** + * Module config type + */ +export type RedisCacheModuleOptions = { + /** + * Time to keep data in cache (in seconds) + */ + ttl?: number + + /** + * Redis connection string + */ + redisUrl?: string + + /** + * Redis client options + */ + redisOptions?: RedisOptions + + /** + * Prefix for event keys + * @default `medusa_lock:` + */ + namespace?: string + + /** + * Time to wait for lock (in seconds) + * @default 5 + */ + waitLockingTimeout?: number + + /** + * Default retry interval (in milliseconds) + * @default 5 + */ + defaultRetryInterval?: number + + /** + * Maximum retry interval (in milliseconds) + * @default 200 + */ + maximumRetryInterval?: number +} diff --git a/packages/modules/providers/locking-redis/tsconfig.json b/packages/modules/providers/locking-redis/tsconfig.json new file mode 100644 index 0000000000000..90f3a70b383ef --- /dev/null +++ b/packages/modules/providers/locking-redis/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../../_tsconfig.base.json", + "compilerOptions": { + "paths": { + "@models": ["./src/models"], + "@services": ["./src/services"], + "@repositories": ["./src/repositories"], + "@types": ["./src/types"], + "@utils": ["./src/utils"] + } + } +} diff --git a/packages/modules/providers/notification-local/src/index.ts b/packages/modules/providers/notification-local/src/index.ts index c0efc3f3e7409..7695781064fcb 100644 --- a/packages/modules/providers/notification-local/src/index.ts +++ b/packages/modules/providers/notification-local/src/index.ts @@ -1,10 +1,8 @@ -import { ModuleProviderExports } from "@medusajs/framework/types" +import { ModuleProvider, Modules } from "@medusajs/framework/utils" import { LocalNotificationService } from "./services/local" const services = [LocalNotificationService] -const providerExport: ModuleProviderExports = { +export default ModuleProvider(Modules.NOTIFICATION, { services, -} - -export default providerExport +}) diff --git a/packages/modules/providers/notification-sendgrid/src/index.ts b/packages/modules/providers/notification-sendgrid/src/index.ts index f49098cb2dc8a..f083881d10b12 100644 --- a/packages/modules/providers/notification-sendgrid/src/index.ts +++ b/packages/modules/providers/notification-sendgrid/src/index.ts @@ -1,10 +1,8 @@ -import { ModuleProviderExports } from "@medusajs/framework/types" +import { ModuleProvider, Modules } from "@medusajs/framework/utils" import { SendgridNotificationService } from "./services/sendgrid" const services = [SendgridNotificationService] -const providerExport: ModuleProviderExports = { +export default ModuleProvider(Modules.NOTIFICATION, { services, -} - -export default providerExport +}) diff --git a/packages/modules/providers/payment-stripe/src/index.ts b/packages/modules/providers/payment-stripe/src/index.ts index 5d7614763e44c..771c6b2b53e9e 100644 --- a/packages/modules/providers/payment-stripe/src/index.ts +++ b/packages/modules/providers/payment-stripe/src/index.ts @@ -1,4 +1,4 @@ -import { ModuleProviderExports } from "@medusajs/framework/types" +import { ModuleProvider, Modules } from "@medusajs/framework/utils" import { StripeBancontactService, StripeBlikService, @@ -17,8 +17,6 @@ const services = [ StripePrzelewy24Service, ] -const providerExport: ModuleProviderExports = { +export default ModuleProvider(Modules.PAYMENT, { services, -} - -export default providerExport +}) diff --git a/yarn.lock b/yarn.lock index 7be54ccba1837..d465b522f6d93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5738,6 +5738,22 @@ __metadata: languageName: unknown linkType: soft +"@medusajs/locking-redis@^0.0.1, @medusajs/locking-redis@workspace:packages/modules/providers/locking-redis": + version: 0.0.0-use.local + resolution: "@medusajs/locking-redis@workspace:packages/modules/providers/locking-redis" + dependencies: + "@medusajs/framework": ^0.0.1 + "@swc/core": ^1.7.28 + "@swc/jest": ^0.2.36 + ioredis: ^5.4.1 + jest: ^29.7.0 + rimraf: ^5.0.1 + typescript: ^5.6.2 + peerDependencies: + "@medusajs/framework": ^0.0.1 + languageName: unknown + linkType: soft + "@medusajs/locking@^0.0.1, @medusajs/locking@workspace:packages/modules/locking": version: 0.0.0-use.local resolution: "@medusajs/locking@workspace:packages/modules/locking" @@ -5873,6 +5889,7 @@ __metadata: "@medusajs/inventory-next": ^0.0.3 "@medusajs/link-modules": ^0.2.11 "@medusajs/locking": ^0.0.1 + "@medusajs/locking-redis": ^0.0.1 "@medusajs/notification": ^0.1.2 "@medusajs/notification-local": ^0.0.1 "@medusajs/notification-sendgrid": ^0.0.1 From 84fa6ccde543fcc1bbf89f2605d87742d03ba7bc Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Tue, 15 Oct 2024 17:59:47 +0200 Subject: [PATCH 4/5] chore: Update admin build/serve configuration (#9584) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Breaking changes** The `outDir` has been deprecated and wont be used anymore, instead all the path are computed internally following these rules - if admin is not `disabled` and the `build` command is run without the `--admin-only` flag, then the admin output dir will be `.medusa/server/public/admin` and it will be served from that same location from the medusa instance. - if admin is not `disabled` and the `build` command is run with the `--admin-only` flag, then the admin output dir will be `.medusa/admin` with the purpose of deploying the admin separately. ⚠️ (expect to receive a warning log) - if the admin is `disabled` and the `build` command is run with the `--admin-only` flag, then fallback to rule number 2 | admin enabled | medusa build --admin-only | output dir | |---|---|---| | true | true | `.medusa/admin` ⚠️ (expect to receive a warning log) | | true | false | `.medusa/server/public/admin` | | false | true | `.medusa/admin` | | false | false | none | ```diff // medusa-config.ts { // ... admin: { - outDir: 'some/path' } } ``` cc @kasperkristensen @sradevski @olivermrbl --- packages/admin/admin-bundler/src/types.ts | 3 +- packages/cli/medusa-cli/src/create-cli.ts | 12 ++- .../core/types/src/common/config-module.ts | 15 --- .../common/__tests__/define-config.spec.ts | 6 -- .../core/utils/src/common/define-config.ts | 1 - packages/medusa/src/commands/build.ts | 95 +++++++++++++++---- packages/medusa/src/loaders/admin.ts | 7 +- packages/medusa/src/utils/admin-consts.ts | 3 + packages/medusa/src/utils/index.ts | 1 + 9 files changed, 95 insertions(+), 48 deletions(-) create mode 100644 packages/medusa/src/utils/admin-consts.ts diff --git a/packages/admin/admin-bundler/src/types.ts b/packages/admin/admin-bundler/src/types.ts index ca56df216f035..491307037e484 100644 --- a/packages/admin/admin-bundler/src/types.ts +++ b/packages/admin/admin-bundler/src/types.ts @@ -1,6 +1,7 @@ import { AdminOptions } from "@medusajs/types" -export type BundlerOptions = Required> & +export type BundlerOptions = Required> & Pick & { + outDir: string sources?: string[] } diff --git a/packages/cli/medusa-cli/src/create-cli.ts b/packages/cli/medusa-cli/src/create-cli.ts index 653474caa8c53..f817e618c7463 100644 --- a/packages/cli/medusa-cli/src/create-cli.ts +++ b/packages/cli/medusa-cli/src/create-cli.ts @@ -354,9 +354,15 @@ function buildLocalCommands(cli, isLocalProject) { ), }) .command({ - command: `build`, - desc: `Build your project.`, - builder: (_) => _, + command: "build", + desc: "Build your project.", + builder: (_) => + _.option("admin-only", { + default: false, + type: "boolean", + describe: + "Only build the admin to serve it separately (outDir .medusa/admin)", + }), handler: handlerP( getCommandHandler(`build`, (args, cmd) => { process.env.NODE_ENV = process.env.NODE_ENV || `development` diff --git a/packages/core/types/src/common/config-module.ts b/packages/core/types/src/common/config-module.ts index e351ac678ea57..fcfc03a01c8aa 100644 --- a/packages/core/types/src/common/config-module.ts +++ b/packages/core/types/src/common/config-module.ts @@ -49,21 +49,6 @@ export interface AdminOptions { * ``` */ path: `/${string}` - /** - * The directory where the admin build is outputted when you run the `build` command. - * The default value is `./build`. - * - * @example - * ```js title="medusa-config.js" - * module.exports = defineConfig({ - * admin: { - * outDir: process.env.ADMIN_BUILD_DIR || `./build`, - * }, - * // ... - * }) - * ``` - */ - outDir: string /** * The URL of your Medusa application. This is useful to set when you deploy the Medusa application. * diff --git a/packages/core/utils/src/common/__tests__/define-config.spec.ts b/packages/core/utils/src/common/__tests__/define-config.spec.ts index efb414002d32c..929b432859f55 100644 --- a/packages/core/utils/src/common/__tests__/define-config.spec.ts +++ b/packages/core/utils/src/common/__tests__/define-config.spec.ts @@ -7,7 +7,6 @@ describe("defineConfig", function () { { "admin": { "backendUrl": "http://localhost:9000", - "outDir": ".medusa/admin", "path": "/app", }, "featureFlags": {}, @@ -154,7 +153,6 @@ describe("defineConfig", function () { { "admin": { "backendUrl": "http://localhost:9000", - "outDir": ".medusa/admin", "path": "/app", }, "featureFlags": {}, @@ -307,7 +305,6 @@ describe("defineConfig", function () { { "admin": { "backendUrl": "http://localhost:9000", - "outDir": ".medusa/admin", "path": "/app", }, "featureFlags": {}, @@ -466,7 +463,6 @@ describe("defineConfig", function () { { "admin": { "backendUrl": "http://localhost:9000", - "outDir": ".medusa/admin", "path": "/app", }, "featureFlags": {}, @@ -621,7 +617,6 @@ describe("defineConfig", function () { { "admin": { "backendUrl": "http://localhost:9000", - "outDir": ".medusa/admin", "path": "/app", }, "featureFlags": {}, @@ -771,7 +766,6 @@ describe("defineConfig", function () { { "admin": { "backendUrl": "http://localhost:9000", - "outDir": ".medusa/admin", "path": "/app", }, "featureFlags": {}, diff --git a/packages/core/utils/src/common/define-config.ts b/packages/core/utils/src/common/define-config.ts index 9f83dbdf35891..ae962cc674378 100644 --- a/packages/core/utils/src/common/define-config.ts +++ b/packages/core/utils/src/common/define-config.ts @@ -90,7 +90,6 @@ export function defineConfig(config: Config = {}): ConfigModule { */ const admin: ConfigModule["admin"] = { backendUrl: process.env.MEDUSA_BACKEND_URL || DEFAULT_ADMIN_URL, - outDir: ".medusa/admin", path: "/app", ...config.admin, } diff --git a/packages/medusa/src/commands/build.ts b/packages/medusa/src/commands/build.ts index 53839e19a49f8..7c95d962f8bb9 100644 --- a/packages/medusa/src/commands/build.ts +++ b/packages/medusa/src/commands/build.ts @@ -4,10 +4,35 @@ import type tsStatic from "typescript" import { logger } from "@medusajs/framework/logger" import { ConfigModule } from "@medusajs/framework/types" import { getConfigFile } from "@medusajs/framework/utils" +import { + ADMIN_ONLY_OUTPUT_DIR, + ADMIN_RELATIVE_OUTPUT_DIR, + ADMIN_SOURCE_DIR, +} from "../utils" -const ADMIN_FOLDER = "src/admin" const INTEGRATION_TESTS_FOLDER = "integration-tests" +function computeDist( + projectRoot: string, + tsConfig: { options: { outDir?: string } } +): string { + const distFolder = tsConfig.options.outDir ?? ".medusa/server" + return path.isAbsolute(distFolder) + ? distFolder + : path.join(projectRoot, distFolder) +} + +async function loadTsConfig(projectRoot: string) { + const ts = await import("typescript") + const tsConfig = parseTSConfig(projectRoot, ts) + if (!tsConfig) { + logger.error("Unable to compile backend source") + return false + } + + return tsConfig! +} + /** * Copies the file to the destination without throwing any * errors if the source file is missing @@ -98,21 +123,14 @@ function parseTSConfig(projectRoot: string, ts: typeof tsStatic) { /** * Builds the backend project using TSC */ -async function buildBackend(projectRoot: string): Promise { +async function buildBackend( + projectRoot: string, + tsConfig: tsStatic.ParsedCommandLine +): Promise { const startTime = process.hrtime() logger.info("Compiling backend source...") - const ts = await import("typescript") - const tsConfig = parseTSConfig(projectRoot, ts) - if (!tsConfig) { - logger.error("Unable to compile backend source") - return false - } - - const distFolder = tsConfig.options.outDir ?? ".medusa/server" - const dist = path.isAbsolute(distFolder) - ? distFolder - : path.join(projectRoot, distFolder) + const dist = computeDist(projectRoot, tsConfig) logger.info(`Removing existing "${path.relative(projectRoot, dist)}" folder`) await clean(dist) @@ -123,11 +141,12 @@ async function buildBackend(projectRoot: string): Promise { */ const filesToCompile = tsConfig.fileNames.filter((fileName) => { return ( - !fileName.includes(`${ADMIN_FOLDER}/`) && + !fileName.includes(`${ADMIN_SOURCE_DIR}/`) && !fileName.includes(`${INTEGRATION_TESTS_FOLDER}/`) ) }) + const ts = await import("typescript") const program = ts.createProgram(filesToCompile, { ...tsConfig.options, ...{ @@ -195,24 +214,41 @@ async function buildBackend(projectRoot: string): Promise { /** * Builds the frontend project using the "@medusajs/admin-bundler" */ -async function buildFrontend(projectRoot: string): Promise { +async function buildFrontend( + projectRoot: string, + adminOnly: boolean, + tsConfig: tsStatic.ParsedCommandLine +): Promise { const startTime = process.hrtime() const configFile = await loadMedusaConfig(projectRoot) if (!configFile) { return false } - const adminSource = path.join(projectRoot, ADMIN_FOLDER) + const dist = computeDist(projectRoot, tsConfig) + + const adminOutputPath = adminOnly + ? path.join(projectRoot, ADMIN_ONLY_OUTPUT_DIR) + : path.join(dist, ADMIN_RELATIVE_OUTPUT_DIR) + + const adminSource = path.join(projectRoot, ADMIN_SOURCE_DIR) const adminOptions = { disable: false, sources: [adminSource], ...configFile.configModule.admin, + outDir: adminOutputPath, } - if (adminOptions.disable) { + if (adminOptions.disable && !adminOnly) { return false } + if (!adminOptions.disable && adminOnly) { + logger.warn( + `You are building using the flag --admin-only but the admin is enabled in your medusa-config, If you intend to host the dashboard separately you should disable the admin in your medusa config` + ) + } + try { logger.info("Compiling frontend source...") const { build: buildProductionBuild } = await import( @@ -231,7 +267,28 @@ async function buildFrontend(projectRoot: string): Promise { } } -export default async function ({ directory }: { directory: string }) { +export default async function ({ + directory, + adminOnly, +}: { + directory: string + adminOnly: boolean +}): Promise { logger.info("Starting build...") - await Promise.all([buildBackend(directory), buildFrontend(directory)]) + + const tsConfig = await loadTsConfig(directory) + if (!tsConfig) { + return false + } + + const promises: Promise[] = [] + + if (!adminOnly) { + promises.push(buildBackend(directory, tsConfig)) + } + + promises.push(buildFrontend(directory, adminOnly, tsConfig)) + + await Promise.all(promises) + return true } diff --git a/packages/medusa/src/loaders/admin.ts b/packages/medusa/src/loaders/admin.ts index 0d2ce30bff92a..fa126ae03bf83 100644 --- a/packages/medusa/src/loaders/admin.ts +++ b/packages/medusa/src/loaders/admin.ts @@ -3,6 +3,7 @@ import { AdminOptions, ConfigModule } from "@medusajs/framework/types" import { Express } from "express" import fs from "fs" import path from "path" +import { ADMIN_RELATIVE_OUTPUT_DIR } from "../utils" type Options = { app: Express @@ -10,10 +11,9 @@ type Options = { rootDirectory: string } -type IntializedOptions = Required< - Pick -> & +type IntializedOptions = Required> & AdminOptions & { + outDir: string sources?: string[] } @@ -39,6 +39,7 @@ export default async function adminLoader({ disable: false, sources, ...admin, + outDir: path.join(rootDirectory, ADMIN_RELATIVE_OUTPUT_DIR), } if (adminOptions?.disable) { diff --git a/packages/medusa/src/utils/admin-consts.ts b/packages/medusa/src/utils/admin-consts.ts new file mode 100644 index 0000000000000..54a58297e3274 --- /dev/null +++ b/packages/medusa/src/utils/admin-consts.ts @@ -0,0 +1,3 @@ +export const ADMIN_SOURCE_DIR = "src/admin" +export const ADMIN_RELATIVE_OUTPUT_DIR = "./public/admin" +export const ADMIN_ONLY_OUTPUT_DIR = ".medusa/admin" diff --git a/packages/medusa/src/utils/index.ts b/packages/medusa/src/utils/index.ts index 35b8ff48d5a3d..c6f93f8a0202a 100644 --- a/packages/medusa/src/utils/index.ts +++ b/packages/medusa/src/utils/index.ts @@ -2,3 +2,4 @@ export * from "./clean-response-data" export * from "./exception-formatter" export * from "./middlewares" export * from "./define-middlewares" +export * from "./admin-consts" From 813efeae516fa15f89af8c62d46115aff8148e41 Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Tue, 15 Oct 2024 18:48:56 +0200 Subject: [PATCH 5/5] fix(admin-vite-plugin): Normalize file paths and add tests (#9595) **What** - #9338 had a regression which caused the import path in some virtual modules to be invalid on Windows. - This PR fixes the issue so we now again create the correct import paths, and adds tests to prevent this from slipping in again. --- packages/admin/admin-vite-plugin/package.json | 7 +- .../generate-custom-field-displays.ts | 5 +- .../generate-custom-field-forms.ts | 5 +- .../generate-custom-field-links.ts | 5 +- .../__tests__/generate-menu-items.spec.ts | 113 +++ .../routes/__tests__/generate-routes.spec.ts | 144 ++++ .../src/routes/generate-menu-items.ts | 10 +- packages/admin/admin-vite-plugin/src/utils.ts | 10 +- .../__tests__/generate-widgets.spec.ts | 83 +++ .../src/widgets/generate-widgets.ts | 4 +- .../admin-vite-plugin/tsconfig.build.json | 4 + .../admin/admin-vite-plugin/tsconfig.json | 2 +- .../{tsup.config.cjs => tsup.config.ts} | 1 + .../admin/admin-vite-plugin/vitest.config.ts | 9 + yarn.lock | 681 +++++++++++++++++- 15 files changed, 1069 insertions(+), 14 deletions(-) create mode 100644 packages/admin/admin-vite-plugin/src/routes/__tests__/generate-menu-items.spec.ts create mode 100644 packages/admin/admin-vite-plugin/src/routes/__tests__/generate-routes.spec.ts create mode 100644 packages/admin/admin-vite-plugin/src/widgets/__tests__/generate-widgets.spec.ts create mode 100644 packages/admin/admin-vite-plugin/tsconfig.build.json rename packages/admin/admin-vite-plugin/{tsup.config.cjs => tsup.config.ts} (81%) create mode 100644 packages/admin/admin-vite-plugin/vitest.config.ts diff --git a/packages/admin/admin-vite-plugin/package.json b/packages/admin/admin-vite-plugin/package.json index da5072fc2e832..8a1b3c332104f 100644 --- a/packages/admin/admin-vite-plugin/package.json +++ b/packages/admin/admin-vite-plugin/package.json @@ -22,14 +22,17 @@ ], "scripts": { "build": "tsup", - "watch": "tsup --watch" + "watch": "tsup --watch", + "test": "vitest --run", + "test:watch": "vitest" }, "devDependencies": { "@babel/types": "7.25.6", "@types/node": "^20.10.4", "tsup": "8.0.1", "typescript": "5.3.3", - "vite": "^5.2.11" + "vite": "^5.2.11", + "vitest": "^2.1.3" }, "peerDependencies": { "vite": "^5.0.0" diff --git a/packages/admin/admin-vite-plugin/src/custom-fields/generate-custom-field-displays.ts b/packages/admin/admin-vite-plugin/src/custom-fields/generate-custom-field-displays.ts index 2ac717b118fe2..d18ce42cb32c8 100644 --- a/packages/admin/admin-vite-plugin/src/custom-fields/generate-custom-field-displays.ts +++ b/packages/admin/admin-vite-plugin/src/custom-fields/generate-custom-field-displays.ts @@ -20,7 +20,7 @@ import { traverse, } from "../babel" import { logger } from "../logger" -import { crawl, getParserOptions } from "../utils" +import { crawl, getParserOptions, normalizePath } from "../utils" import { getConfigArgument, getModel, validateLink } from "./helpers" type CustomFieldDisplay = { @@ -288,5 +288,6 @@ function generateCustomFieldConfigName(index: number): string { } function generateImport(file: string, index: number): string { - return `import ${generateCustomFieldConfigName(index)} from "${file}"` + const path = normalizePath(file) + return `import ${generateCustomFieldConfigName(index)} from "${path}"` } diff --git a/packages/admin/admin-vite-plugin/src/custom-fields/generate-custom-field-forms.ts b/packages/admin/admin-vite-plugin/src/custom-fields/generate-custom-field-forms.ts index 2f11ee3ff94c3..1bb2c3d967bf2 100644 --- a/packages/admin/admin-vite-plugin/src/custom-fields/generate-custom-field-forms.ts +++ b/packages/admin/admin-vite-plugin/src/custom-fields/generate-custom-field-forms.ts @@ -29,7 +29,7 @@ import { traverse, } from "../babel" import { logger } from "../logger" -import { crawl, getParserOptions } from "../utils" +import { crawl, getParserOptions, normalizePath } from "../utils" import { getConfigArgument, getModel, validateLink } from "./helpers" type CustomFieldConfigField = { @@ -263,7 +263,8 @@ function generateCustomFieldConfigName(index: number): string { } function generateImport(file: string, index: number): string { - return `import ${generateCustomFieldConfigName(index)} from "${file}"` + const path = normalizePath(file) + return `import ${generateCustomFieldConfigName(index)} from "${path}"` } function getForms( diff --git a/packages/admin/admin-vite-plugin/src/custom-fields/generate-custom-field-links.ts b/packages/admin/admin-vite-plugin/src/custom-fields/generate-custom-field-links.ts index ac4e4fd7b6906..9c363b36f6e83 100644 --- a/packages/admin/admin-vite-plugin/src/custom-fields/generate-custom-field-links.ts +++ b/packages/admin/admin-vite-plugin/src/custom-fields/generate-custom-field-links.ts @@ -12,7 +12,7 @@ import { traverse, } from "../babel" import { logger } from "../logger" -import { crawl, getParserOptions } from "../utils" +import { crawl, getParserOptions, normalizePath } from "../utils" import { getConfigArgument, getModel } from "./helpers" type ParsedCustomFieldLink = { @@ -138,7 +138,8 @@ function generateCustomFieldConfigName(index: number): string { } function generateImport(file: string, index: number): string { - return `import ${generateCustomFieldConfigName(index)} from "${file}"` + const path = normalizePath(file) + return `import ${generateCustomFieldConfigName(index)} from "${path}"` } function getLink( diff --git a/packages/admin/admin-vite-plugin/src/routes/__tests__/generate-menu-items.spec.ts b/packages/admin/admin-vite-plugin/src/routes/__tests__/generate-menu-items.spec.ts new file mode 100644 index 0000000000000..cb8ccd98eaa39 --- /dev/null +++ b/packages/admin/admin-vite-plugin/src/routes/__tests__/generate-menu-items.spec.ts @@ -0,0 +1,113 @@ +import { describe, expect, it, vi } from "vitest" + +import fs from "fs/promises" +import * as utils from "../../utils" +import { generateMenuItems } from "../generate-menu-items" + +vi.mock("../../utils", async () => { + const actual = await vi.importActual("../../utils") + return { + ...actual, + crawl: vi.fn(), + } +}) + +vi.mock("fs/promises", () => ({ + default: { + readFile: vi.fn(), + }, +})) + +const mockFileContents = [ + ` + import { defineRouteConfig } from "@medusajs/admin-sdk" + + const Page = () => { + return
Page 1
+ } + + export const config = defineRouteConfig({ + label: "Page 1", + icon: "icon1", + }) + + export default Page + `, + ` + import { defineRouteConfig } from "@medusajs/admin-sdk" + + const Page = () => { + return
Page 2
+ } + + export const config = defineRouteConfig({ + label: "Page 2", + }) + + export default Page + `, +] + +const expectedMenuItems = ` + menuItems: [ + { + label: RouteConfig0.label, + icon: RouteConfig0.icon, + path: "/one", + }, + { + label: RouteConfig1.label, + icon: undefined, + path: "/two", + } + ] + ` + +describe("generateMenuItems", () => { + it("should generate menu items", async () => { + const mockFiles = [ + "Users/user/medusa/src/admin/routes/one/page.tsx", + "Users/user/medusa/src/admin/routes/two/page.tsx", + ] + vi.mocked(utils.crawl).mockResolvedValue(mockFiles) + + vi.mocked(fs.readFile).mockImplementation(async (file) => + Promise.resolve(mockFileContents[mockFiles.indexOf(file as string)]) + ) + + const result = await generateMenuItems( + new Set(["Users/user/medusa/src/admin"]) + ) + + expect(result.imports).toEqual([ + `import { config as RouteConfig0 } from "Users/user/medusa/src/admin/routes/one/page.tsx"`, + `import { config as RouteConfig1 } from "Users/user/medusa/src/admin/routes/two/page.tsx"`, + ]) + expect(utils.normalizeString(result.code)).toEqual( + utils.normalizeString(expectedMenuItems) + ) + }) + + it("should handle windows paths", async () => { + // Setup mocks + const mockFiles = [ + "C:\\medusa\\src\\admin\\routes\\one\\page.tsx", + "C:\\medusa\\src\\admin\\routes\\two\\page.tsx", + ] + vi.mocked(utils.crawl).mockResolvedValue(mockFiles) + + vi.mocked(fs.readFile).mockImplementation(async (file) => + Promise.resolve(mockFileContents[mockFiles.indexOf(file as string)]) + ) + + const result = await generateMenuItems(new Set(["C:\\medusa\\src\\admin"])) + + expect(result.imports).toEqual([ + `import { config as RouteConfig0 } from "C:/medusa/src/admin/routes/one/page.tsx"`, + `import { config as RouteConfig1 } from "C:/medusa/src/admin/routes/two/page.tsx"`, + ]) + expect(utils.normalizeString(result.code)).toEqual( + utils.normalizeString(expectedMenuItems) + ) + }) +}) diff --git a/packages/admin/admin-vite-plugin/src/routes/__tests__/generate-routes.spec.ts b/packages/admin/admin-vite-plugin/src/routes/__tests__/generate-routes.spec.ts new file mode 100644 index 0000000000000..6ef9e1f1fe75e --- /dev/null +++ b/packages/admin/admin-vite-plugin/src/routes/__tests__/generate-routes.spec.ts @@ -0,0 +1,144 @@ +import { describe, expect, it, vi } from "vitest" + +import { Stats } from "fs" +import fs from "fs/promises" +import * as utils from "../../utils" +import { generateRoutes } from "../generate-routes" + +// Mock the dependencies +vi.mock("../../utils", async () => { + const actual = await vi.importActual("../../utils") + return { + ...actual, + crawl: vi.fn(), + } +}) + +vi.mock("fs/promises", () => ({ + default: { + readFile: vi.fn(), + stat: vi.fn(), + }, +})) + +const mockFileContents = [ + ` + import { defineRouteConfig } from "@medusajs/admin-sdk" + + const Page = () => { + return
Page 1
+ } + + export const config = defineRouteConfig({ + label: "Page 1", + icon: "icon1", + }) + + export default Page + `, + ` + import { defineRouteConfig } from "@medusajs/admin-sdk" + + const Page = () => { + return
Page 2
+ } + + export const config = defineRouteConfig({ + label: "Page 2", + }) + + export default Page + `, +] + +const expectedRoutesWithoutLoaders = ` + routes: [ + { + Component: RouteComponent0, + loader: undefined, + path: "/one", + }, + { + Component: RouteComponent1, + loader: undefined, + path: "/two", + } + ] +` + +const expectedRoutesWithLoaders = ` + routes: [ + { + Component: RouteComponent0, + loader: RouteLoader0, + path: "/one", + }, + { + Component: RouteComponent1, + loader: RouteLoader1, + path: "/two", + } + ] +` + +describe("generateRoutes", () => { + it("should generate routes", async () => { + const mockFiles = [ + "Users/user/medusa/src/admin/routes/one/page.tsx", + "Users/user/medusa/src/admin/routes/two/page.tsx", + ] + vi.mocked(utils.crawl).mockResolvedValue(mockFiles) + + vi.mocked(fs.readFile).mockImplementation(async (file) => + Promise.resolve(mockFileContents[mockFiles.indexOf(file as string)]) + ) + + vi.mocked(fs.stat).mockRejectedValue(new Error("File not found")) + + const result = await generateRoutes( + new Set(["Users/user/medusa/src/admin"]) + ) + expect(utils.normalizeString(result.code)).toEqual( + utils.normalizeString(expectedRoutesWithoutLoaders) + ) + }) + it("should generate routes with loaders", async () => { + const mockFiles = [ + "Users/user/medusa/src/admin/routes/one/page.tsx", + "Users/user/medusa/src/admin/routes/two/page.tsx", + ] + vi.mocked(utils.crawl).mockResolvedValue(mockFiles) + + vi.mocked(fs.readFile).mockImplementation(async (file) => + Promise.resolve(mockFileContents[mockFiles.indexOf(file as string)]) + ) + + vi.mocked(fs.stat).mockResolvedValue({} as Stats) // We just want to mock that the check passes + + const result = await generateRoutes( + new Set(["Users/user/medusa/src/admin"]) + ) + expect(utils.normalizeString(result.code)).toEqual( + utils.normalizeString(expectedRoutesWithLoaders) + ) + }) + it("should handle windows paths", async () => { + const mockFiles = [ + "C:\\medusa\\src\\admin\\routes\\one\\page.tsx", + "C:\\medusa\\src\\admin\\routes\\two\\page.tsx", + ] + vi.mocked(utils.crawl).mockResolvedValue(mockFiles) + + vi.mocked(fs.readFile).mockImplementation(async (file) => + Promise.resolve(mockFileContents[mockFiles.indexOf(file as string)]) + ) + + vi.mocked(fs.stat).mockRejectedValue(new Error("File not found")) + + const result = await generateRoutes(new Set(["C:\\medusa\\src\\admin"])) + + expect(utils.normalizeString(result.code)).toEqual( + utils.normalizeString(expectedRoutesWithoutLoaders) + ) + }) +}) diff --git a/packages/admin/admin-vite-plugin/src/routes/generate-menu-items.ts b/packages/admin/admin-vite-plugin/src/routes/generate-menu-items.ts index 7c738903462c4..19ef309810501 100644 --- a/packages/admin/admin-vite-plugin/src/routes/generate-menu-items.ts +++ b/packages/admin/admin-vite-plugin/src/routes/generate-menu-items.ts @@ -2,7 +2,12 @@ import fs from "fs/promises" import { outdent } from "outdent" import { isIdentifier, isObjectProperty, parse, traverse } from "../babel" import { logger } from "../logger" -import { crawl, getConfigObjectProperties, getParserOptions } from "../utils" +import { + crawl, + getConfigObjectProperties, + getParserOptions, + normalizePath, +} from "../utils" import { getRoute } from "./helpers" type MenuItem = { @@ -90,7 +95,8 @@ async function parseFile( } function generateImport(file: string, index: number): string { - return `import { config as ${generateRouteConfigName(index)} } from "${file}"` + const path = normalizePath(file) + return `import { config as ${generateRouteConfigName(index)} } from "${path}"` } function generateMenuItem( diff --git a/packages/admin/admin-vite-plugin/src/utils.ts b/packages/admin/admin-vite-plugin/src/utils.ts index 0522ef49a00f0..7c3d1349558ab 100644 --- a/packages/admin/admin-vite-plugin/src/utils.ts +++ b/packages/admin/admin-vite-plugin/src/utils.ts @@ -17,7 +17,7 @@ import { } from "./babel" export function normalizePath(file: string) { - return path.normalize(file).split(path.sep).join("/") + return path.normalize(file.replace(/\\/g, "/")) } /** @@ -145,3 +145,11 @@ export function isFileInAdminSubdirectory( const normalizedPath = normalizePath(file) return normalizedPath.includes(`/src/admin/${subdirectory}/`) } + +/** + * Test util to normalize strings, so they can be compared without taking + * whitespace into account. + */ +export function normalizeString(str: string): string { + return str.replace(/\s+/g, " ").trim() +} diff --git a/packages/admin/admin-vite-plugin/src/widgets/__tests__/generate-widgets.spec.ts b/packages/admin/admin-vite-plugin/src/widgets/__tests__/generate-widgets.spec.ts new file mode 100644 index 0000000000000..2f31ca82b4f7e --- /dev/null +++ b/packages/admin/admin-vite-plugin/src/widgets/__tests__/generate-widgets.spec.ts @@ -0,0 +1,83 @@ +import { vi } from "vitest" + +import fs from "fs/promises" +import * as utils from "../../utils" +import { generateWidgets } from "../generate-widgets" + +vi.mock("../../utils", async () => { + const actual = await vi.importActual("../../utils") + return { + ...actual, + crawl: vi.fn(), + } +}) + +vi.mock("fs/promises", () => ({ + default: { + readFile: vi.fn(), + }, +})) + +const mockFileContents = [ + ` + import { defineWidgetConfig } from "@medusajs/admin-sdk" + + const Widget = () => { + return
Widget 1
+ } + + export const config = defineWidgetConfig({ + zone: "product.details.after", + }) + + export default Widget +`, +] + +const expectedWidgets = ` + widgets: [ + { + Component: WidgetComponent0, + zone: ["product.details.after"] + } + ] +` + +describe("generateWidgets", () => { + it("should generate widgets", async () => { + const mockFiles = ["Users/user/medusa/src/admin/widgets/widget.tsx"] + vi.mocked(utils.crawl).mockResolvedValue(mockFiles) + + vi.mocked(fs.readFile).mockImplementation(async (file) => + Promise.resolve(mockFileContents[mockFiles.indexOf(file as string)]) + ) + + const result = await generateWidgets( + new Set(["Users/user/medusa/src/admin"]) + ) + + expect(result.imports).toEqual([ + `import WidgetComponent0, { config as WidgetConfig0 } from "Users/user/medusa/src/admin/widgets/widget.tsx"`, + ]) + expect(utils.normalizeString(result.code)).toEqual( + utils.normalizeString(expectedWidgets) + ) + }) + it("should handle windows paths", async () => { + const mockFiles = ["C:\\medusa\\src\\admin\\widgets\\widget.tsx"] + vi.mocked(utils.crawl).mockResolvedValue(mockFiles) + + vi.mocked(fs.readFile).mockImplementation(async (file) => + Promise.resolve(mockFileContents[mockFiles.indexOf(file as string)]) + ) + + const result = await generateWidgets(new Set(["C:\\medusa\\src\\admin"])) + + expect(result.imports).toEqual([ + `import WidgetComponent0, { config as WidgetConfig0 } from "C:/medusa/src/admin/widgets/widget.tsx"`, + ]) + expect(utils.normalizeString(result.code)).toEqual( + utils.normalizeString(expectedWidgets) + ) + }) +}) diff --git a/packages/admin/admin-vite-plugin/src/widgets/generate-widgets.ts b/packages/admin/admin-vite-plugin/src/widgets/generate-widgets.ts index 5a96dbf973e57..af5960458316f 100644 --- a/packages/admin/admin-vite-plugin/src/widgets/generate-widgets.ts +++ b/packages/admin/admin-vite-plugin/src/widgets/generate-widgets.ts @@ -16,6 +16,7 @@ import { getConfigObjectProperties, getParserOptions, hasDefaultExport, + normalizePath, } from "../utils" import { getWidgetFilesFromSources } from "./helpers" @@ -135,9 +136,10 @@ function generateWidgetConfigName(index: number): string { } function generateImport(file: string, index: number): string { + const path = normalizePath(file) return `import ${generateWidgetComponentName( index - )}, { config as ${generateWidgetConfigName(index)} } from "${file}"` + )}, { config as ${generateWidgetConfigName(index)} } from "${path}"` } function generateWidget(zone: InjectionZone[], index: number): WidgetConfig { diff --git a/packages/admin/admin-vite-plugin/tsconfig.build.json b/packages/admin/admin-vite-plugin/tsconfig.build.json new file mode 100644 index 0000000000000..d853b83a4105b --- /dev/null +++ b/packages/admin/admin-vite-plugin/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["**/*.spec.ts", "vitest.config.ts", "tsup.config.ts"] +} diff --git a/packages/admin/admin-vite-plugin/tsconfig.json b/packages/admin/admin-vite-plugin/tsconfig.json index aa9f8ff0f3b68..ec7fcfec0c22a 100644 --- a/packages/admin/admin-vite-plugin/tsconfig.json +++ b/packages/admin/admin-vite-plugin/tsconfig.json @@ -18,5 +18,5 @@ "@babel/types": ["../../../node_modules/@babel/types"] } }, - "include": ["src"] + "include": ["src", "vitest.config.ts", "tsup.config.ts"] } diff --git a/packages/admin/admin-vite-plugin/tsup.config.cjs b/packages/admin/admin-vite-plugin/tsup.config.ts similarity index 81% rename from packages/admin/admin-vite-plugin/tsup.config.cjs rename to packages/admin/admin-vite-plugin/tsup.config.ts index f54ede878e0d8..adc4f96458ccc 100644 --- a/packages/admin/admin-vite-plugin/tsup.config.cjs +++ b/packages/admin/admin-vite-plugin/tsup.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from "tsup" export default defineConfig({ entry: ["./src/index.ts"], + tsconfig: "tsconfig.build.json", format: ["cjs", "esm"], dts: true, clean: true, diff --git a/packages/admin/admin-vite-plugin/vitest.config.ts b/packages/admin/admin-vite-plugin/vitest.config.ts new file mode 100644 index 0000000000000..b93b2bbe7688c --- /dev/null +++ b/packages/admin/admin-vite-plugin/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config" + +export default defineConfig({ + test: { + globals: true, + environment: "node", + include: ["**/*.spec.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], + }, +}) diff --git a/yarn.lock b/yarn.lock index d465b522f6d93..e91411582ae61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3345,6 +3345,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/aix-ppc64@npm:0.21.5" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/aix-ppc64@npm:0.23.1": version: 0.23.1 resolution: "@esbuild/aix-ppc64@npm:0.23.1" @@ -3373,6 +3380,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-arm64@npm:0.21.5" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.23.1": version: 0.23.1 resolution: "@esbuild/android-arm64@npm:0.23.1" @@ -3401,6 +3415,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-arm@npm:0.21.5" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.23.1": version: 0.23.1 resolution: "@esbuild/android-arm@npm:0.23.1" @@ -3429,6 +3450,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-x64@npm:0.21.5" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + "@esbuild/android-x64@npm:0.23.1": version: 0.23.1 resolution: "@esbuild/android-x64@npm:0.23.1" @@ -3457,6 +3485,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/darwin-arm64@npm:0.21.5" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/darwin-arm64@npm:0.23.1": version: 0.23.1 resolution: "@esbuild/darwin-arm64@npm:0.23.1" @@ -3485,6 +3520,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/darwin-x64@npm:0.21.5" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@esbuild/darwin-x64@npm:0.23.1": version: 0.23.1 resolution: "@esbuild/darwin-x64@npm:0.23.1" @@ -3513,6 +3555,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/freebsd-arm64@npm:0.21.5" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/freebsd-arm64@npm:0.23.1": version: 0.23.1 resolution: "@esbuild/freebsd-arm64@npm:0.23.1" @@ -3541,6 +3590,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/freebsd-x64@npm:0.21.5" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/freebsd-x64@npm:0.23.1": version: 0.23.1 resolution: "@esbuild/freebsd-x64@npm:0.23.1" @@ -3569,6 +3625,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-arm64@npm:0.21.5" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/linux-arm64@npm:0.23.1": version: 0.23.1 resolution: "@esbuild/linux-arm64@npm:0.23.1" @@ -3597,6 +3660,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-arm@npm:0.21.5" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@esbuild/linux-arm@npm:0.23.1": version: 0.23.1 resolution: "@esbuild/linux-arm@npm:0.23.1" @@ -3625,6 +3695,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ia32@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-ia32@npm:0.21.5" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/linux-ia32@npm:0.23.1": version: 0.23.1 resolution: "@esbuild/linux-ia32@npm:0.23.1" @@ -3653,6 +3730,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-loong64@npm:0.21.5" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.23.1": version: 0.23.1 resolution: "@esbuild/linux-loong64@npm:0.23.1" @@ -3681,6 +3765,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-mips64el@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-mips64el@npm:0.21.5" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + "@esbuild/linux-mips64el@npm:0.23.1": version: 0.23.1 resolution: "@esbuild/linux-mips64el@npm:0.23.1" @@ -3709,6 +3800,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ppc64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-ppc64@npm:0.21.5" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/linux-ppc64@npm:0.23.1": version: 0.23.1 resolution: "@esbuild/linux-ppc64@npm:0.23.1" @@ -3737,6 +3835,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-riscv64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-riscv64@npm:0.21.5" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + "@esbuild/linux-riscv64@npm:0.23.1": version: 0.23.1 resolution: "@esbuild/linux-riscv64@npm:0.23.1" @@ -3765,6 +3870,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-s390x@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-s390x@npm:0.21.5" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + "@esbuild/linux-s390x@npm:0.23.1": version: 0.23.1 resolution: "@esbuild/linux-s390x@npm:0.23.1" @@ -3793,6 +3905,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-x64@npm:0.21.5" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "@esbuild/linux-x64@npm:0.23.1": version: 0.23.1 resolution: "@esbuild/linux-x64@npm:0.23.1" @@ -3821,6 +3940,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/netbsd-x64@npm:0.21.5" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/netbsd-x64@npm:0.23.1": version: 0.23.1 resolution: "@esbuild/netbsd-x64@npm:0.23.1" @@ -3856,6 +3982,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/openbsd-x64@npm:0.21.5" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openbsd-x64@npm:0.23.1": version: 0.23.1 resolution: "@esbuild/openbsd-x64@npm:0.23.1" @@ -3884,6 +4017,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/sunos-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/sunos-x64@npm:0.21.5" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + "@esbuild/sunos-x64@npm:0.23.1": version: 0.23.1 resolution: "@esbuild/sunos-x64@npm:0.23.1" @@ -3912,6 +4052,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-arm64@npm:0.21.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/win32-arm64@npm:0.23.1": version: 0.23.1 resolution: "@esbuild/win32-arm64@npm:0.23.1" @@ -3940,6 +4087,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-ia32@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-ia32@npm:0.21.5" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/win32-ia32@npm:0.23.1": version: 0.23.1 resolution: "@esbuild/win32-ia32@npm:0.23.1" @@ -3968,6 +4122,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-x64@npm:0.21.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@esbuild/win32-x64@npm:0.23.1": version: 0.23.1 resolution: "@esbuild/win32-x64@npm:0.23.1" @@ -4984,6 +5145,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/sourcemap-codec@npm:^1.5.0": + version: 1.5.0 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" + checksum: 2eb864f276eb1096c3c11da3e9bb518f6d9fc0023c78344cdc037abadc725172c70314bdb360f2d4b7bffec7f5d657ce006816bc5d4ecb35e61b66132db00c18 + languageName: node + linkType: hard + "@jridgewell/trace-mapping@npm:0.3.9": version: 0.3.9 resolution: "@jridgewell/trace-mapping@npm:0.3.9" @@ -5136,6 +5304,7 @@ __metadata: tsup: 8.0.1 typescript: 5.3.3 vite: ^5.2.11 + vitest: ^2.1.3 peerDependencies: vite: ^5.0.0 languageName: unknown @@ -10792,6 +10961,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm-eabi@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.24.0" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@rollup/rollup-android-arm64@npm:4.17.2": version: 4.17.2 resolution: "@rollup/rollup-android-arm64@npm:4.17.2" @@ -10799,6 +10975,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm64@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-android-arm64@npm:4.24.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-arm64@npm:4.17.2": version: 4.17.2 resolution: "@rollup/rollup-darwin-arm64@npm:4.17.2" @@ -10806,6 +10989,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-arm64@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-darwin-arm64@npm:4.24.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-x64@npm:4.17.2": version: 4.17.2 resolution: "@rollup/rollup-darwin-x64@npm:4.17.2" @@ -10813,6 +11003,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-x64@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-darwin-x64@npm:4.24.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-gnueabihf@npm:4.17.2": version: 4.17.2 resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.17.2" @@ -10820,6 +11017,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-gnueabihf@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.24.0" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-musleabihf@npm:4.17.2": version: 4.17.2 resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.17.2" @@ -10827,6 +11031,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-musleabihf@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.24.0" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-gnu@npm:4.17.2": version: 4.17.2 resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.17.2" @@ -10834,6 +11045,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-gnu@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.24.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-musl@npm:4.17.2": version: 4.17.2 resolution: "@rollup/rollup-linux-arm64-musl@npm:4.17.2" @@ -10841,6 +11059,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-musl@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.24.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-powerpc64le-gnu@npm:4.17.2": version: 4.17.2 resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.17.2" @@ -10848,6 +11073,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-powerpc64le-gnu@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.24.0" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-riscv64-gnu@npm:4.17.2": version: 4.17.2 resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.17.2" @@ -10855,6 +11087,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-riscv64-gnu@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.24.0" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-s390x-gnu@npm:4.17.2": version: 4.17.2 resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.17.2" @@ -10862,6 +11101,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-s390x-gnu@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.24.0" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-gnu@npm:4.17.2": version: 4.17.2 resolution: "@rollup/rollup-linux-x64-gnu@npm:4.17.2" @@ -10869,6 +11115,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-gnu@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.24.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-musl@npm:4.17.2": version: 4.17.2 resolution: "@rollup/rollup-linux-x64-musl@npm:4.17.2" @@ -10876,6 +11129,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-musl@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.24.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-win32-arm64-msvc@npm:4.17.2": version: 4.17.2 resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.17.2" @@ -10883,6 +11143,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-arm64-msvc@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.24.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-win32-ia32-msvc@npm:4.17.2": version: 4.17.2 resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.17.2" @@ -10890,6 +11157,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-ia32-msvc@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.24.0" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@rollup/rollup-win32-x64-msvc@npm:4.17.2": version: 4.17.2 resolution: "@rollup/rollup-win32-x64-msvc@npm:4.17.2" @@ -10897,6 +11171,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-x64-msvc@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.24.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@rushstack/node-core-library@npm:4.2.0": version: 4.2.0 resolution: "@rushstack/node-core-library@npm:4.2.0" @@ -13085,6 +13366,13 @@ __metadata: languageName: node linkType: hard +"@types/estree@npm:1.0.6": + version: 1.0.6 + resolution: "@types/estree@npm:1.0.6" + checksum: cdfd751f6f9065442cd40957c07fd80361c962869aa853c1c2fd03e101af8b9389d8ff4955a43a6fcfa223dd387a089937f95be0f3eec21ca527039fd2d9859a + languageName: node + linkType: hard + "@types/estree@npm:^0.0.51": version: 0.0.51 resolution: "@types/estree@npm:0.0.51" @@ -14067,6 +14355,38 @@ __metadata: languageName: node linkType: hard +"@vitest/expect@npm:2.1.3": + version: 2.1.3 + resolution: "@vitest/expect@npm:2.1.3" + dependencies: + "@vitest/spy": 2.1.3 + "@vitest/utils": 2.1.3 + chai: ^5.1.1 + tinyrainbow: ^1.2.0 + checksum: 0837adcbb938feebcc083664afc5c4d12e42f1f2442b6f1bedc6b5650a8ff2448b1f10713b45afb099c839fb5cf766c971736267fa9b0fe2ac87f3e2d7f782c2 + languageName: node + linkType: hard + +"@vitest/mocker@npm:2.1.3": + version: 2.1.3 + resolution: "@vitest/mocker@npm:2.1.3" + dependencies: + "@vitest/spy": 2.1.3 + estree-walker: ^3.0.3 + magic-string: ^0.30.11 + peerDependencies: + "@vitest/spy": 2.1.3 + msw: ^2.3.5 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 03c80628d092244f21a0ba9041665fc75f987d0d11fab1ae0b7027ec21e503f65057e8c24b936602c5f852d83fbb183da13d05dba117c99785b41b3dafd105ce + languageName: node + linkType: hard + "@vitest/pretty-format@npm:2.0.5": version: 2.0.5 resolution: "@vitest/pretty-format@npm:2.0.5" @@ -14085,6 +14405,15 @@ __metadata: languageName: node linkType: hard +"@vitest/pretty-format@npm:2.1.3, @vitest/pretty-format@npm:^2.1.3": + version: 2.1.3 + resolution: "@vitest/pretty-format@npm:2.1.3" + dependencies: + tinyrainbow: ^1.2.0 + checksum: 5a6ee872a8adf5e2764f2b5b2276d8a2199be4ef14777ab693428caf359481851400af10b59721d4972289c955ffe7277954a662b04cfb10233824574c7074ba + languageName: node + linkType: hard + "@vitest/runner@npm:0.32.4": version: 0.32.4 resolution: "@vitest/runner@npm:0.32.4" @@ -14096,6 +14425,16 @@ __metadata: languageName: node linkType: hard +"@vitest/runner@npm:2.1.3": + version: 2.1.3 + resolution: "@vitest/runner@npm:2.1.3" + dependencies: + "@vitest/utils": 2.1.3 + pathe: ^1.1.2 + checksum: d5b077643265d10025e22fa64a0e54c3d4fddc23e05f9fcd143dbcc4080851b0df31985986e57890a974577a18d3af624758b6062801d7dd96f9b4f2eaf591f1 + languageName: node + linkType: hard + "@vitest/snapshot@npm:0.32.4": version: 0.32.4 resolution: "@vitest/snapshot@npm:0.32.4" @@ -14107,6 +14446,17 @@ __metadata: languageName: node linkType: hard +"@vitest/snapshot@npm:2.1.3": + version: 2.1.3 + resolution: "@vitest/snapshot@npm:2.1.3" + dependencies: + "@vitest/pretty-format": 2.1.3 + magic-string: ^0.30.11 + pathe: ^1.1.2 + checksum: a3dcea6a5f7581b6a34dc3bf5f7bd42a05e2ccf6e1171d9f1b759688aebe650e6412564d066aeaa45e83ac549d453b6a3edcf774a8ac728c0c639f8dc919039f + languageName: node + linkType: hard + "@vitest/spy@npm:0.32.4": version: 0.32.4 resolution: "@vitest/spy@npm:0.32.4" @@ -14125,6 +14475,15 @@ __metadata: languageName: node linkType: hard +"@vitest/spy@npm:2.1.3": + version: 2.1.3 + resolution: "@vitest/spy@npm:2.1.3" + dependencies: + tinyspy: ^3.0.0 + checksum: 8d85a5c2848c5bd81892af989aebad65d0c7ae74094aa98ad4f35ecf80755259c7a748a8e7bf683b2906fac29a51fc0ffa82f8fc073b36dbd8a0418261fccdba + languageName: node + linkType: hard + "@vitest/utils@npm:0.32.4": version: 0.32.4 resolution: "@vitest/utils@npm:0.32.4" @@ -14148,6 +14507,17 @@ __metadata: languageName: node linkType: hard +"@vitest/utils@npm:2.1.3": + version: 2.1.3 + resolution: "@vitest/utils@npm:2.1.3" + dependencies: + "@vitest/pretty-format": 2.1.3 + loupe: ^3.1.1 + tinyrainbow: ^1.2.0 + checksum: 55a044e43b84c0f8f573d8578107f26440678b6f506c8d9fee88b7ef120d19efd27c9be77985c107113b0f3f3db298dcee57074e1c1c214bee7a097fd08a209b + languageName: node + linkType: hard + "@vitest/utils@npm:^2.0.5": version: 2.1.2 resolution: "@vitest/utils@npm:2.1.2" @@ -18782,6 +19152,86 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:^0.21.3": + version: 0.21.5 + resolution: "esbuild@npm:0.21.5" + dependencies: + "@esbuild/aix-ppc64": 0.21.5 + "@esbuild/android-arm": 0.21.5 + "@esbuild/android-arm64": 0.21.5 + "@esbuild/android-x64": 0.21.5 + "@esbuild/darwin-arm64": 0.21.5 + "@esbuild/darwin-x64": 0.21.5 + "@esbuild/freebsd-arm64": 0.21.5 + "@esbuild/freebsd-x64": 0.21.5 + "@esbuild/linux-arm": 0.21.5 + "@esbuild/linux-arm64": 0.21.5 + "@esbuild/linux-ia32": 0.21.5 + "@esbuild/linux-loong64": 0.21.5 + "@esbuild/linux-mips64el": 0.21.5 + "@esbuild/linux-ppc64": 0.21.5 + "@esbuild/linux-riscv64": 0.21.5 + "@esbuild/linux-s390x": 0.21.5 + "@esbuild/linux-x64": 0.21.5 + "@esbuild/netbsd-x64": 0.21.5 + "@esbuild/openbsd-x64": 0.21.5 + "@esbuild/sunos-x64": 0.21.5 + "@esbuild/win32-arm64": 0.21.5 + "@esbuild/win32-ia32": 0.21.5 + "@esbuild/win32-x64": 0.21.5 + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: fa08508adf683c3f399e8a014a6382a6b65542213431e26206c0720e536b31c09b50798747c2a105a4bbba1d9767b8d3615a74c2f7bf1ddf6d836cd11eb672de + languageName: node + linkType: hard + "escalade@npm:^3.1.1, escalade@npm:^3.1.2": version: 3.1.2 resolution: "escalade@npm:3.1.2" @@ -23915,6 +24365,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.30.11": + version: 0.30.12 + resolution: "magic-string@npm:0.30.12" + dependencies: + "@jridgewell/sourcemap-codec": ^1.5.0 + checksum: 469f457d18af37dfcca8617086ea8a65bcd8b60ba8a1182cb024ce43e470ace3c9d1cb6bee58d3b311768fb16bc27bd50bdeebcaa63dadd0fd46cac4d2e11d5f + languageName: node + linkType: hard + "make-dir@npm:^3.0.0, make-dir@npm:^3.0.2": version: 3.1.0 resolution: "make-dir@npm:3.1.0" @@ -26796,6 +27255,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:^8.4.43": + version: 8.4.47 + resolution: "postcss@npm:8.4.47" + dependencies: + nanoid: ^3.3.7 + picocolors: ^1.1.0 + source-map-js: ^1.2.1 + checksum: 929f68b5081b7202709456532cee2a145c1843d391508c5a09de2517e8c4791638f71dd63b1898dba6712f8839d7a6da046c72a5e44c162e908f5911f57b5f44 + languageName: node + linkType: hard + "postgres-array@npm:~2.0.0": version: 2.0.0 resolution: "postgres-array@npm:2.0.0" @@ -28752,6 +29222,69 @@ __metadata: languageName: node linkType: hard +"rollup@npm:^4.20.0": + version: 4.24.0 + resolution: "rollup@npm:4.24.0" + dependencies: + "@rollup/rollup-android-arm-eabi": 4.24.0 + "@rollup/rollup-android-arm64": 4.24.0 + "@rollup/rollup-darwin-arm64": 4.24.0 + "@rollup/rollup-darwin-x64": 4.24.0 + "@rollup/rollup-linux-arm-gnueabihf": 4.24.0 + "@rollup/rollup-linux-arm-musleabihf": 4.24.0 + "@rollup/rollup-linux-arm64-gnu": 4.24.0 + "@rollup/rollup-linux-arm64-musl": 4.24.0 + "@rollup/rollup-linux-powerpc64le-gnu": 4.24.0 + "@rollup/rollup-linux-riscv64-gnu": 4.24.0 + "@rollup/rollup-linux-s390x-gnu": 4.24.0 + "@rollup/rollup-linux-x64-gnu": 4.24.0 + "@rollup/rollup-linux-x64-musl": 4.24.0 + "@rollup/rollup-win32-arm64-msvc": 4.24.0 + "@rollup/rollup-win32-ia32-msvc": 4.24.0 + "@rollup/rollup-win32-x64-msvc": 4.24.0 + "@types/estree": 1.0.6 + fsevents: ~2.3.2 + dependenciesMeta: + "@rollup/rollup-android-arm-eabi": + optional: true + "@rollup/rollup-android-arm64": + optional: true + "@rollup/rollup-darwin-arm64": + optional: true + "@rollup/rollup-darwin-x64": + optional: true + "@rollup/rollup-linux-arm-gnueabihf": + optional: true + "@rollup/rollup-linux-arm-musleabihf": + optional: true + "@rollup/rollup-linux-arm64-gnu": + optional: true + "@rollup/rollup-linux-arm64-musl": + optional: true + "@rollup/rollup-linux-powerpc64le-gnu": + optional: true + "@rollup/rollup-linux-riscv64-gnu": + optional: true + "@rollup/rollup-linux-s390x-gnu": + optional: true + "@rollup/rollup-linux-x64-gnu": + optional: true + "@rollup/rollup-linux-x64-musl": + optional: true + "@rollup/rollup-win32-arm64-msvc": + optional: true + "@rollup/rollup-win32-ia32-msvc": + optional: true + "@rollup/rollup-win32-x64-msvc": + optional: true + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: 77fb549c1de8afd1142d2da765adbb0cdab9f13c47df5217f00b5cf40b74219caa48c6ba2157f6249313ee81b6fa4c4fa8b3d2a0347ad6220739e00e580a808d + languageName: node + linkType: hard + "root@workspace:.": version: 0.0.0-use.local resolution: "root@workspace:." @@ -29565,6 +30098,13 @@ __metadata: languageName: node linkType: hard +"source-map-js@npm:^1.2.1": + version: 1.2.1 + resolution: "source-map-js@npm:1.2.1" + checksum: 7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf + languageName: node + linkType: hard + "source-map-support@npm:0.5.13": version: 0.5.13 resolution: "source-map-support@npm:0.5.13" @@ -29822,7 +30362,7 @@ __metadata: languageName: node linkType: hard -"std-env@npm:^3.3.3": +"std-env@npm:^3.3.3, std-env@npm:^3.7.0": version: 3.7.0 resolution: "std-env@npm:3.7.0" checksum: 60edf2d130a4feb7002974af3d5a5f3343558d1ccf8d9b9934d225c638606884db4a20d2fe6440a09605bca282af6b042ae8070a10490c0800d69e82e478f41e @@ -30620,6 +31160,20 @@ __metadata: languageName: node linkType: hard +"tinybench@npm:^2.9.0": + version: 2.9.0 + resolution: "tinybench@npm:2.9.0" + checksum: c3500b0f60d2eb8db65250afe750b66d51623057ee88720b7f064894a6cb7eb93360ca824a60a31ab16dab30c7b1f06efe0795b352e37914a9d4bad86386a20c + languageName: node + linkType: hard + +"tinyexec@npm:^0.3.0": + version: 0.3.0 + resolution: "tinyexec@npm:0.3.0" + checksum: 138a4f4241aea6b6312559508468ab275a31955e66e2f57ed206e0aaabecee622624f208c5740345f0a66e33478fd065e359ed1eb1269eb6fd4fa25d44d0ba3b + languageName: node + linkType: hard + "tinypool@npm:^0.5.0": version: 0.5.0 resolution: "tinypool@npm:0.5.0" @@ -30627,6 +31181,13 @@ __metadata: languageName: node linkType: hard +"tinypool@npm:^1.0.0": + version: 1.0.1 + resolution: "tinypool@npm:1.0.1" + checksum: 90939d6a03f1519c61007bf416632dc1f0b9c1a9dd673c179ccd9e36a408437384f984fc86555a5d040d45b595abc299c3bb39d354439e98a090766b5952e73d + languageName: node + linkType: hard + "tinyrainbow@npm:^1.2.0": version: 1.2.0 resolution: "tinyrainbow@npm:1.2.0" @@ -32174,6 +32735,20 @@ __metadata: languageName: node linkType: hard +"vite-node@npm:2.1.3": + version: 2.1.3 + resolution: "vite-node@npm:2.1.3" + dependencies: + cac: ^6.7.14 + debug: ^4.3.6 + pathe: ^1.1.2 + vite: ^5.0.0 + bin: + vite-node: vite-node.mjs + checksum: 1b06139880a8170651e025e8c35aa92a917f8ec8f24507cda5bf4be09843f6447e1f494932a8d7eb98124f1c8c9fee02283ef318ddd57e2b861d2d85a409a206 + languageName: node + linkType: hard + "vite-plugin-inspect@npm:^0.8.7": version: 0.8.7 resolution: "vite-plugin-inspect@npm:0.8.7" @@ -32243,6 +32818,49 @@ __metadata: languageName: node linkType: hard +"vite@npm:^5.0.0": + version: 5.4.9 + resolution: "vite@npm:5.4.9" + dependencies: + esbuild: ^0.21.3 + fsevents: ~2.3.3 + postcss: ^8.4.43 + rollup: ^4.20.0 + peerDependencies: + "@types/node": ^18.0.0 || >=20.0.0 + less: "*" + lightningcss: ^1.21.0 + sass: "*" + sass-embedded: "*" + stylus: "*" + sugarss: "*" + terser: ^5.4.0 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + bin: + vite: bin/vite.js + checksum: e9c59f2c639047e37c79bbbb151c7a55a3dc27932957cf4cf0447ee0bdcc1ddfd9b1fb3ba0465371c01ba3616d62561327855794c2d652213c3a10a32e6d369d + languageName: node + linkType: hard + "vite@npm:^5.2.11": version: 5.2.11 resolution: "vite@npm:5.2.11" @@ -32343,6 +32961,55 @@ __metadata: languageName: node linkType: hard +"vitest@npm:^2.1.3": + version: 2.1.3 + resolution: "vitest@npm:2.1.3" + dependencies: + "@vitest/expect": 2.1.3 + "@vitest/mocker": 2.1.3 + "@vitest/pretty-format": ^2.1.3 + "@vitest/runner": 2.1.3 + "@vitest/snapshot": 2.1.3 + "@vitest/spy": 2.1.3 + "@vitest/utils": 2.1.3 + chai: ^5.1.1 + debug: ^4.3.6 + magic-string: ^0.30.11 + pathe: ^1.1.2 + std-env: ^3.7.0 + tinybench: ^2.9.0 + tinyexec: ^0.3.0 + tinypool: ^1.0.0 + tinyrainbow: ^1.2.0 + vite: ^5.0.0 + vite-node: 2.1.3 + why-is-node-running: ^2.3.0 + peerDependencies: + "@edge-runtime/vm": "*" + "@types/node": ^18.0.0 || >=20.0.0 + "@vitest/browser": 2.1.3 + "@vitest/ui": 2.1.3 + happy-dom: "*" + jsdom: "*" + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@types/node": + optional: true + "@vitest/browser": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + bin: + vitest: vitest.mjs + checksum: 7688fdce37205e7f3b448039df216e103e3a52994af0201993e22decbb558d129a734001b991f3c3d80bf4a4ef91ca6a5665a7395d5b051249da60a0016eda36 + languageName: node + linkType: hard + "void-elements@npm:3.1.0": version: 3.1.0 resolution: "void-elements@npm:3.1.0" @@ -32637,6 +33304,18 @@ __metadata: languageName: node linkType: hard +"why-is-node-running@npm:^2.3.0": + version: 2.3.0 + resolution: "why-is-node-running@npm:2.3.0" + dependencies: + siginfo: ^2.0.0 + stackback: 0.0.2 + bin: + why-is-node-running: cli.js + checksum: 1cde0b01b827d2cf4cb11db962f3958b9175d5d9e7ac7361d1a7b0e2dc6069a263e69118bd974c4f6d0a890ef4eedfe34cf3d5167ec14203dbc9a18620537054 + languageName: node + linkType: hard + "widest-line@npm:^3.1.0": version: 3.1.0 resolution: "widest-line@npm:3.1.0"