feat: add support for loading admin extensions from the source (#10975)
This commit is contained in:
7
.changeset/neat-lamps-check.md
Normal file
7
.changeset/neat-lamps-check.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@medusajs/medusa": patch
|
||||
"@medusajs/framework": patch
|
||||
"@medusajs/types": patch
|
||||
---
|
||||
|
||||
feat: add support for loading admin extensions from the source
|
||||
@@ -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()
|
||||
|
||||
/**
|
||||
|
||||
@@ -968,6 +968,7 @@ export type InputConfig = Partial<
|
||||
|
||||
export type PluginDetails = {
|
||||
resolve: string
|
||||
adminResolve: string
|
||||
name: string
|
||||
id: string
|
||||
options: Record<string, unknown>
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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
|
||||
@@ -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(), `**`),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user