Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for loading admin extensions from the source #10975

Merged
merged 5 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/neat-lamps-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@medusajs/medusa": patch
"@medusajs/framework": patch
"@medusajs/types": patch
---

feat: add support for loading admin extensions from the source
30 changes: 27 additions & 3 deletions packages/core/framework/src/build-tools/compiler.ts
Original file line number Diff line number Diff line change
@@ -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"

/**
Expand All @@ -27,6 +27,7 @@ export class Compiler {
#adminSourceFolder: string
#pluginsDistFolder: string
#backendIgnoreFiles: string[]
#pluginOptionsPath: string
#adminOnlyDistFolder: string
#tsCompiler?: typeof tsStatic

Expand All @@ -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",
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -440,6 +463,7 @@ export class Compiler {
* a file has changed.
*/
async developPluginBackend(onFileChange?: () => void) {
await this.#createPluginOptionsFile()
const ts = await this.#loadTSCompiler()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thetutlage just making sure: Can we guarantee that this file will never end up in the final build of the plugin which is published to NPM, or would it be safer to clean this up when the dev process is killed? If it ends up in the final NPM package, admin extensions would fail to load for the plugin.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean. Ideally, I expect the users to run npm run build at the time of publishing the plugin to npm. And that command will perform a cleanup + build and this file will never part of the published file.


/**
Expand Down
1 change: 1 addition & 0 deletions packages/core/types/src/common/config-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,7 @@ export type InputConfig = Partial<

export type PluginDetails = {
resolve: string
adminResolve: string
name: string
id: string
options: Record<string, unknown>
Expand Down
7 changes: 2 additions & 5 deletions packages/medusa/src/loaders/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
26 changes: 26 additions & 0 deletions packages/medusa/src/loaders/helpers/resolve-plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Record<string, any>> {
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
Expand All @@ -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,
})
Expand All @@ -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}`,
Expand Down Expand Up @@ -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(), `**`),
})
Expand Down
Loading