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:
committed by
GitHub
parent
26c78bbc03
commit
f1a05f4725
146
packages/admin/src/utils/build-manifest.ts
Normal file
146
packages/admin/src/utils/build-manifest.ts
Normal 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
|
||||
}
|
||||
}
|
||||
42
packages/admin/src/utils/get-plugin-paths.ts
Normal file
42
packages/admin/src/utils/get-plugin-paths.ts
Normal 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
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
export { getPluginPaths } from "./get-plugin-paths"
|
||||
export { loadConfig } from "./load-config"
|
||||
export { reporter } from "./reporter"
|
||||
export { validatePath } from "./validate-path"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)}`)
|
||||
},
|
||||
}
|
||||
@@ -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".`
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user