feat(admin, admin-ui, medusa-js, medusa-react, medusa): Support Admin Extensions (#4761)

Co-authored-by: Rares Stefan <948623+StephixOne@users.noreply.github.com>
Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Kasper Fabricius Kristensen
2023-08-17 14:14:45 +02:00
committed by GitHub
parent 26c78bbc03
commit f1a05f4725
189 changed files with 14570 additions and 12773 deletions

View File

@@ -0,0 +1,146 @@
import { AdminOptions } from "@medusajs/admin-ui"
import fse from "fs-extra"
import isEqual from "lodash/isEqual"
import isNil from "lodash/isNil"
import path from "path"
import { BuildOptions } from "../types"
import { getPluginPaths } from "./get-plugin-paths"
const MANIFEST_PATH = path.resolve(
process.cwd(),
".cache",
"admin-build-manifest.json"
)
async function getPackageVersions(appDir: string) {
const packageJsonPath = path.resolve(appDir, "package.json")
try {
const { dependencies } = await fse.readJson(packageJsonPath)
return {
dependencies,
}
} catch (_err) {
return null
}
}
async function getLastTimeModifiedAt(appDir: string) {
const adminPath = path.resolve(appDir, "src", "admin")
// Get the most recent time a file in the admin directory was modified and do it recursively for all subdirectories and files
let mostRecentTimestamp = 0
const pathExists = await fse.pathExists(adminPath)
if (!pathExists) {
return mostRecentTimestamp
}
async function processFolder(dir: string) {
const files = await fse.readdir(dir)
for (const file of files) {
const filePath = path.join(dir, file)
const stats = await fse.stat(filePath)
if (stats.isDirectory()) {
await processFolder(filePath) // Recursively process subfolders
} else {
const { mtimeMs } = stats
mostRecentTimestamp = Math.max(mostRecentTimestamp, mtimeMs)
}
}
}
await processFolder(adminPath)
return mostRecentTimestamp
}
export async function createBuildManifest(
appDir: string,
options: BuildOptions
) {
const packageVersions = await getPackageVersions(appDir)
const lastModificationTime = await getLastTimeModifiedAt(appDir)
const plugins = await getPluginPaths()
const { dependencies } = packageVersions
const buildManifest = {
dependencies: dependencies,
modifiedAt: lastModificationTime,
plugins: plugins,
options,
}
await fse.outputFile(MANIFEST_PATH, JSON.stringify(buildManifest, null, 2))
}
export async function shouldBuild(appDir: string, options: AdminOptions) {
try {
const manifestExists = await fse.pathExists(MANIFEST_PATH)
if (!manifestExists) {
return true
}
const buildManifest = await fse.readJson(MANIFEST_PATH)
const {
dependencies: buildManifestDependencies,
modifiedAt: buildManifestModifiedAt,
plugins: buildManifestPlugins,
options: buildManifestOptions,
} = buildManifest
const optionsChanged = !isEqual(options, buildManifestOptions)
if (optionsChanged) {
return true
}
const packageVersions = await getPackageVersions(appDir)
if (!packageVersions) {
return true
}
const { dependencies } = packageVersions
const dependenciesChanged = !isEqual(
dependencies,
buildManifestDependencies
)
if (dependenciesChanged) {
return true
}
const modifiedAt = await getLastTimeModifiedAt(appDir)
if (isNil(modifiedAt)) {
return true
}
const lastModificationTimeChanged = modifiedAt !== buildManifestModifiedAt
if (lastModificationTimeChanged) {
return true
}
const plugins = await getPluginPaths()
const pluginsChanged = !isEqual(plugins, buildManifestPlugins)
if (pluginsChanged) {
return true
}
return false
} catch (_error) {
return true
}
}

View File

@@ -0,0 +1,42 @@
import { logger } from "@medusajs/admin-ui"
import type { ConfigModule } from "@medusajs/medusa"
import { getConfigFile } from "medusa-core-utils"
import path from "path"
function hasEnabledUI(options: Record<string, unknown>) {
return "enableUI" in options && options.enableUI === true
}
export async function getPluginPaths() {
const { configModule, error } = getConfigFile<ConfigModule>(
path.resolve(process.cwd()),
"medusa-config.js"
)
if (error) {
logger.panic("Error loading `medusa-config.js`")
}
const plugins = configModule.plugins || []
const paths: string[] = []
for (const p of plugins) {
if (typeof p === "string") {
continue
} else {
const options = p.options || {}
/**
* While the feature is in beta, we only want to load plugins that have
* enabled the UI explicitly. In the future, we will flip this check so
* we only exclude plugins that have set `enableUI` to false.
*/
if (hasEnabledUI(options)) {
paths.push(p.resolve)
}
}
}
return paths
}

View File

@@ -1,3 +1,2 @@
export { getPluginPaths } from "./get-plugin-paths"
export { loadConfig } from "./load-config"
export { reporter } from "./reporter"
export { validatePath } from "./validate-path"

View File

@@ -1,7 +1,8 @@
import type { ConfigModule } from "@medusajs/medusa"
import { getConfigFile } from "medusa-core-utils"
import { ConfigModule, PluginOptions } from "../types"
import { PluginOptions } from "../types"
export const loadConfig = () => {
export const loadConfig = (isDev?: boolean): PluginOptions | null => {
const { configModule } = getConfigFile<ConfigModule>(
process.cwd(),
"medusa-config"
@@ -13,21 +14,45 @@ export const loadConfig = () => {
(typeof p === "object" && p.resolve === "@medusajs/admin")
)
let defaultConfig: PluginOptions = {
if (!plugin) {
return null
}
let config: PluginOptions = {
serve: true,
autoRebuild: false,
path: "app",
path: isDev ? "/" : "/app",
outDir: "build",
backend: isDev ? "http://localhost:9000" : "/",
develop: {
open: true,
port: 7001,
},
}
if (typeof plugin !== "string") {
const { options } = plugin as { options: PluginOptions }
defaultConfig = {
serve: options.serve ?? defaultConfig.serve,
autoRebuild: options.autoRebuild ?? defaultConfig.autoRebuild,
path: options.path ?? defaultConfig.path,
outDir: options.outDir ?? defaultConfig.outDir,
const options = (plugin as { options: PluginOptions }).options ?? {}
const serve = options.serve !== undefined ? options.serve : config.serve
const serverUrl = serve
? config.backend
: options.backend
? options.backend
: "/"
config = {
serve,
autoRebuild: options.autoRebuild ?? config.autoRebuild,
path: options.path ?? config.path,
outDir: options.outDir ?? config.outDir,
backend: serverUrl,
develop: {
open: options.develop?.open ?? config.develop.open,
port: options.develop?.port ?? config.develop.port,
},
}
}
return defaultConfig
return config
}

View File

@@ -1,19 +0,0 @@
import colors from "picocolors"
const PREFIX = colors.cyan("[@medusajs/admin]")
export const reporter = {
panic: (err: Error) => {
console.error(`${PREFIX} ${colors.red(err.message)}`)
process.exit(1)
},
error: (message: string) => {
console.error(`${PREFIX} ${colors.red(message)}`)
},
info: (message: string) => {
console.log(`${PREFIX} ${colors.blue(message)}`)
},
warn: (message: string) => {
console.warn(`${PREFIX} ${colors.yellow(message)}`)
},
}

View File

@@ -1,19 +0,0 @@
export const validatePath = (path?: string) => {
if (!path) {
return
}
if (path.startsWith("/")) {
throw new Error(`Path cannot start with a slash.`)
}
if (path.endsWith("/")) {
throw new Error(`Path cannot end with a slash.`)
}
if (path === "admin" || path === "store") {
throw new Error(
`Path cannot be one of the reserved paths: "admin", "store".`
)
}
}