diff --git a/.changeset/neat-lamps-check.md b/.changeset/neat-lamps-check.md new file mode 100644 index 0000000000..205f4deada --- /dev/null +++ b/.changeset/neat-lamps-check.md @@ -0,0 +1,7 @@ +--- +"@medusajs/medusa": patch +"@medusajs/framework": patch +"@medusajs/types": patch +--- + +feat: add support for loading admin extensions from the source diff --git a/packages/core/framework/src/build-tools/compiler.ts b/packages/core/framework/src/build-tools/compiler.ts index 2b336e97f7..fe1d254f32 100644 --- a/packages/core/framework/src/build-tools/compiler.ts +++ b/packages/core/framework/src/build-tools/compiler.ts @@ -1,7 +1,7 @@ -import type { AdminOptions, ConfigModule, Logger } from "@medusajs/types" -import { getConfigFile } from "@medusajs/utils" -import { access, constants, copyFile, rm } from "fs/promises" import path from "path" +import { getConfigFile } from "@medusajs/utils" +import type { AdminOptions, ConfigModule, Logger } from "@medusajs/types" +import { rm, access, constants, copyFile, writeFile } from "fs/promises" import type tsStatic from "typescript" /** @@ -27,6 +27,7 @@ export class Compiler { #adminSourceFolder: string #pluginsDistFolder: string #backendIgnoreFiles: string[] + #pluginOptionsPath: string #adminOnlyDistFolder: string #tsCompiler?: typeof tsStatic @@ -37,6 +38,10 @@ export class Compiler { this.#adminSourceFolder = path.join(this.#projectRoot, "src/admin") this.#adminOnlyDistFolder = path.join(this.#projectRoot, ".medusa/admin") this.#pluginsDistFolder = path.join(this.#projectRoot, ".medusa/server") + this.#pluginOptionsPath = path.join( + this.#projectRoot, + ".medusa/server/medusa-plugin-options.json" + ) this.#backendIgnoreFiles = [ "integration-tests", "test", @@ -152,6 +157,24 @@ export class Compiler { return { configFilePath, configModule } } + /** + * Creates medusa-plugin-options.json file that contains some + * metadata related to the plugin, which could be helpful + * for MedusaJS loaders during development + */ + async #createPluginOptionsFile() { + await writeFile( + this.#pluginOptionsPath, + JSON.stringify( + { + srcDir: path.join(this.#projectRoot, "src"), + }, + null, + 2 + ) + ) + } + /** * Prints typescript diagnostic messages */ @@ -440,6 +463,7 @@ export class Compiler { * a file has changed. */ async developPluginBackend(onFileChange?: () => void) { + await this.#createPluginOptionsFile() const ts = await this.#loadTSCompiler() /** diff --git a/packages/core/types/src/common/config-module.ts b/packages/core/types/src/common/config-module.ts index 2796cf7d2f..d979be5ba8 100644 --- a/packages/core/types/src/common/config-module.ts +++ b/packages/core/types/src/common/config-module.ts @@ -968,6 +968,7 @@ export type InputConfig = Partial< export type PluginDetails = { resolve: string + adminResolve: string name: string id: string options: Record diff --git a/packages/medusa/src/loaders/__tests__/get-resolved-plugins.spec.ts b/packages/medusa/src/loaders/__tests__/get-resolved-plugins.spec.ts index 7857045da0..9058c0a4a1 100644 --- a/packages/medusa/src/loaders/__tests__/get-resolved-plugins.spec.ts +++ b/packages/medusa/src/loaders/__tests__/get-resolved-plugins.spec.ts @@ -34,6 +34,10 @@ describe("getResolvedPlugins | relative paths", () => { expect(plugins).toEqual([ { resolve: path.join(fs.basePath, "./plugins/dummy/.medusa/server/src"), + adminResolve: path.join( + fs.basePath, + "./plugins/dummy/.medusa/server/src/admin" + ), name: "my-dummy-plugin", id: "my-dummy-plugin", options: { apiKey: "asecret" }, @@ -71,6 +75,10 @@ describe("getResolvedPlugins | relative paths", () => { expect(plugins).toEqual([ { resolve: path.join(fs.basePath, "./plugins/dummy/.medusa/server/src"), + adminResolve: path.join( + fs.basePath, + "./plugins/dummy/.medusa/server/src/admin" + ), name: "my-dummy-plugin", id: "my-dummy-plugin", options: { apiKey: "asecret" }, @@ -108,6 +116,57 @@ describe("getResolvedPlugins | relative paths", () => { `Unable to resolve plugin "./plugins/dummy". Make sure the plugin directory has a package.json file` ) }) + + test("resolve admin source from medusa-plugin-options file", async () => { + await fs.createJson("plugins/dummy/package.json", { + name: "my-dummy-plugin", + version: "1.0.0", + }) + await fs.create( + "plugins/dummy/.medusa/server/src/modules/blog/index.js", + `` + ) + await fs.createJson( + "plugins/dummy/.medusa/server/medusa-plugin-options.json", + { + srcDir: path.join(fs.basePath, "plugins/dummy/src"), + } + ) + + const plugins = await getResolvedPlugins( + fs.basePath, + defineConfig({ + plugins: [ + { + resolve: "./plugins/dummy", + options: { + apiKey: "asecret", + }, + }, + ], + }), + false + ) + + expect(plugins).toEqual([ + { + resolve: path.join(fs.basePath, "./plugins/dummy/.medusa/server/src"), + adminResolve: path.join(fs.basePath, "./plugins/dummy/src/admin"), + name: "my-dummy-plugin", + id: "my-dummy-plugin", + options: { apiKey: "asecret" }, + version: "1.0.0", + modules: [ + { + options: { + apiKey: "asecret", + }, + resolve: "./plugins/dummy/.medusa/server/src/modules/blog", + }, + ], + }, + ]) + }) }) describe("getResolvedPlugins | package reference", () => { @@ -139,6 +198,10 @@ describe("getResolvedPlugins | package reference", () => { fs.basePath, "node_modules/@plugins/dummy/.medusa/server/src" ), + adminResolve: path.join( + fs.basePath, + "node_modules/@plugins/dummy/.medusa/server/src/admin" + ), name: "my-dummy-plugin", id: "my-dummy-plugin", options: { apiKey: "asecret" }, @@ -180,6 +243,10 @@ describe("getResolvedPlugins | package reference", () => { fs.basePath, "node_modules/@plugins/dummy/.medusa/server/src" ), + adminResolve: path.join( + fs.basePath, + "node_modules/@plugins/dummy/.medusa/server/src/admin" + ), name: "my-dummy-plugin", id: "my-dummy-plugin", options: { apiKey: "asecret" }, diff --git a/packages/medusa/src/loaders/admin.ts b/packages/medusa/src/loaders/admin.ts index 8cb2dda878..c5fa3c238b 100644 --- a/packages/medusa/src/loaders/admin.ts +++ b/packages/medusa/src/loaders/admin.ts @@ -33,12 +33,9 @@ export default async function adminLoader({ const { admin } = configModule const sources: string[] = [] - for (const plugin of plugins) { - const pluginSource = path.join(plugin.resolve, "admin") - - if (fs.existsSync(pluginSource)) { - sources.push(pluginSource) + if (fs.existsSync(plugin.adminResolve)) { + sources.push(plugin.adminResolve) } } diff --git a/packages/medusa/src/loaders/helpers/resolve-plugins.ts b/packages/medusa/src/loaders/helpers/resolve-plugins.ts index 8846dfd339..0418d81656 100644 --- a/packages/medusa/src/loaders/helpers/resolve-plugins.ts +++ b/packages/medusa/src/loaders/helpers/resolve-plugins.ts @@ -5,6 +5,8 @@ import { ConfigModule, PluginDetails } from "@medusajs/framework/types" const MEDUSA_APP_SOURCE_PATH = "src" const MEDUSA_PLUGIN_SOURCE_PATH = ".medusa/server/src" +const MEDUSA_PLUGIN_OPTIONS_FILE_PATH = + ".medusa/server/medusa-plugin-options.json" export const MEDUSA_PROJECT_NAME = "project-plugin" function createPluginId(name: string): string { @@ -41,6 +43,27 @@ async function resolvePluginPkgFile( } } +/** + * Reads the "medusa-plugin-options.json" file from the plugin root + * directory and returns its contents as an object. + */ +async function resolvePluginOptions( + pluginRootDir: string +): Promise> { + try { + const contents = await fs.readFile( + path.join(pluginRootDir, MEDUSA_PLUGIN_OPTIONS_FILE_PATH), + "utf-8" + ) + return JSON.parse(contents) + } catch (error) { + if (error.code === "MODULE_NOT_FOUND" || error.code === "ENOENT") { + return {} + } + throw error + } +} + /** * Finds the correct path for the plugin. If it is a local plugin it will be * found in the plugins folder. Otherwise we will look for the plugin in the @@ -60,6 +83,7 @@ async function resolvePlugin( const name = pkgJSON.contents.name || pluginPath const resolve = path.join(resolvedPath, MEDUSA_PLUGIN_SOURCE_PATH) + const pluginStaticOptions = await resolvePluginOptions(resolvedPath) const modules = await readDir(path.join(resolve, "modules"), { ignoreMissing: true, }) @@ -71,6 +95,7 @@ async function resolvePlugin( id: createPluginId(name), options: pluginOptions, version: pkgJSON.contents.version || "0.0.0", + adminResolve: path.join(pluginStaticOptions.srcDir ?? resolve, "admin"), modules: modules.map((mod) => { return { resolve: `${pluginPath}/${MEDUSA_PLUGIN_SOURCE_PATH}/modules/${mod.name}`, @@ -100,6 +125,7 @@ export async function getResolvedPlugins( resolve: extensionDirectory, name: MEDUSA_PROJECT_NAME, id: createPluginId(MEDUSA_PROJECT_NAME), + adminResolve: path.join(extensionDirectory, "admin"), options: configModule, version: createFileContentHash(process.cwd(), `**`), })