feat(admin,admin-ui,medusa): Add Medusa Admin plugin (#3334)
This commit is contained in:
committed by
GitHub
parent
d6b1ad1ccd
commit
40de54b010
78
packages/admin/src/api/index.ts
Normal file
78
packages/admin/src/api/index.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import express, { Request, Response, Router } from "express"
|
||||
import fse from "fs-extra"
|
||||
import { ServerResponse } from "http"
|
||||
import { resolve } from "path"
|
||||
import colors from "picocolors"
|
||||
import { PluginOptions } from "../types"
|
||||
import { reporter } from "../utils"
|
||||
|
||||
export default function (_rootDirectory: string, options: PluginOptions) {
|
||||
const app = Router()
|
||||
|
||||
const { serve = true, path = "app", outDir } = options
|
||||
|
||||
if (serve) {
|
||||
let buildPath: string
|
||||
let htmlPath: string
|
||||
|
||||
if (outDir) {
|
||||
buildPath = resolve(process.cwd(), outDir)
|
||||
htmlPath = resolve(buildPath, "index.html")
|
||||
} else {
|
||||
buildPath = resolve(
|
||||
require.resolve("@medusajs/admin-ui"),
|
||||
"..",
|
||||
"..",
|
||||
"build"
|
||||
)
|
||||
htmlPath = resolve(buildPath, "index.html")
|
||||
}
|
||||
|
||||
/**
|
||||
* The admin UI should always be built at this point, but in the
|
||||
* rare case that another plugin terminated a previous startup, the admin
|
||||
* may not have been built correctly. Here we check if the admin UI
|
||||
* build files exist, and if not, we throw an error, providing the
|
||||
* user with instructions on how to fix their build.
|
||||
*/
|
||||
try {
|
||||
fse.ensureFileSync(htmlPath)
|
||||
} catch (_err) {
|
||||
reporter.panic(
|
||||
new Error(
|
||||
`Could not find the admin UI build files. Please run ${colors.bold(
|
||||
"`medusa-admin build`"
|
||||
)} to build the admin UI.`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const html = fse.readFileSync(htmlPath, "utf-8")
|
||||
|
||||
const sendHtml = (_req: Request, res: Response) => {
|
||||
res.setHeader("Cache-Control", "no-cache")
|
||||
res.setHeader("Vary", "Origin, Cache-Control")
|
||||
res.send(html)
|
||||
}
|
||||
|
||||
const setStaticHeaders = (res: ServerResponse) => {
|
||||
res.setHeader("Cache-Control", "max-age=31536000, immutable")
|
||||
res.setHeader("Vary", "Origin, Cache-Control")
|
||||
}
|
||||
|
||||
app.get(`/${path}`, sendHtml)
|
||||
app.use(
|
||||
`/${path}`,
|
||||
express.static(buildPath, {
|
||||
setHeaders: setStaticHeaders,
|
||||
})
|
||||
)
|
||||
app.get(`/${path}/*`, sendHtml)
|
||||
} else {
|
||||
app.get(`/${path}`, (_req, res) => {
|
||||
res.send("Admin not enabled")
|
||||
})
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
48
packages/admin/src/commands/build.ts
Normal file
48
packages/admin/src/commands/build.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { build as buildAdmin } from "@medusajs/admin-ui"
|
||||
import ora from "ora"
|
||||
import { EOL } from "os"
|
||||
import { loadConfig, reporter, validatePath } from "../utils"
|
||||
|
||||
type BuildArgs = {
|
||||
outDir?: string
|
||||
backend?: string
|
||||
path?: string
|
||||
}
|
||||
|
||||
export default async function build(args: BuildArgs) {
|
||||
const { path, backend, outDir } = mergeArgs(args)
|
||||
|
||||
try {
|
||||
validatePath(path)
|
||||
} catch (err) {
|
||||
reporter.panic(err)
|
||||
}
|
||||
|
||||
const time = Date.now()
|
||||
const spinner = ora().start(`Building Admin UI${EOL}`)
|
||||
|
||||
await buildAdmin({
|
||||
build: {
|
||||
outDir: outDir,
|
||||
},
|
||||
globals: {
|
||||
base: path,
|
||||
backend: backend,
|
||||
},
|
||||
}).catch((err) => {
|
||||
spinner.fail(`Failed to build Admin UI${EOL}`)
|
||||
reporter.panic(err)
|
||||
})
|
||||
|
||||
spinner.succeed(`Admin UI build - ${Date.now() - time}ms`)
|
||||
}
|
||||
|
||||
const mergeArgs = (args: BuildArgs) => {
|
||||
const { path, backend, outDir } = loadConfig()
|
||||
|
||||
return {
|
||||
path: args.path || path,
|
||||
backend: args.backend || backend,
|
||||
outDir: args.outDir || outDir,
|
||||
}
|
||||
}
|
||||
15
packages/admin/src/commands/create-cli.ts
Normal file
15
packages/admin/src/commands/create-cli.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Command } from "commander"
|
||||
import build from "./build"
|
||||
|
||||
export async function createCli(): Promise<Command> {
|
||||
const program = new Command()
|
||||
|
||||
const buildCommand = program.command("build")
|
||||
buildCommand.description("Build the admin dashboard")
|
||||
buildCommand.option("-o, --out-dir <path>", "Output directory")
|
||||
buildCommand.option("-b, --backend <url>", "Backend URL")
|
||||
buildCommand.option("-p, --path <path>", "Base path")
|
||||
buildCommand.action(build)
|
||||
|
||||
return program
|
||||
}
|
||||
8
packages/admin/src/commands/index.ts
Normal file
8
packages/admin/src/commands/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createCli } from "./create-cli"
|
||||
|
||||
createCli()
|
||||
.then(async (cli) => cli.parseAsync(process.argv))
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
84
packages/admin/src/setup/index.ts
Normal file
84
packages/admin/src/setup/index.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { build } from "@medusajs/admin-ui"
|
||||
import fse from "fs-extra"
|
||||
import ora from "ora"
|
||||
import { EOL } from "os"
|
||||
import { resolve } from "path"
|
||||
import { loadConfig, reporter, validatePath } from "../utils"
|
||||
|
||||
export default async function setupAdmin() {
|
||||
const { path, backend, outDir } = loadConfig()
|
||||
|
||||
try {
|
||||
validatePath(path)
|
||||
} catch (err) {
|
||||
reporter.panic(err)
|
||||
}
|
||||
|
||||
let dir: string
|
||||
let shouldBuild = false
|
||||
|
||||
if (outDir) {
|
||||
dir = resolve(process.cwd(), outDir)
|
||||
} else {
|
||||
const uiPath = require.resolve("@medusajs/admin-ui")
|
||||
dir = resolve(uiPath, "..", "..", "build")
|
||||
}
|
||||
|
||||
try {
|
||||
await fse.ensureDir(dir)
|
||||
} catch (_e) {
|
||||
shouldBuild = true
|
||||
}
|
||||
|
||||
const manifestPath = resolve(dir, "build-manifest.json")
|
||||
|
||||
const buildOptions = {
|
||||
build: {
|
||||
outDir: outDir,
|
||||
},
|
||||
globals: {
|
||||
base: path,
|
||||
backend: backend,
|
||||
},
|
||||
}
|
||||
|
||||
try {
|
||||
const manifest = await fse.readJSON(manifestPath)
|
||||
|
||||
/**
|
||||
* If the manifest is not the same as the current build options,
|
||||
* we should rebuild the admin UI.
|
||||
*/
|
||||
if (JSON.stringify(manifest) !== JSON.stringify(buildOptions)) {
|
||||
shouldBuild = true
|
||||
}
|
||||
} catch (_e) {
|
||||
/**
|
||||
* If the manifest file does not exist, we should rebuild the admin UI.
|
||||
* This is the case when the admin UI is built for the first time.
|
||||
*/
|
||||
shouldBuild = true
|
||||
}
|
||||
|
||||
if (shouldBuild) {
|
||||
const time = Date.now()
|
||||
const spinner = ora().start(
|
||||
`Admin build is out of sync with the current configuration. Rebuild initialized${EOL}`
|
||||
)
|
||||
|
||||
await build({
|
||||
build: {
|
||||
outDir: outDir,
|
||||
},
|
||||
globals: {
|
||||
base: path,
|
||||
backend: backend,
|
||||
},
|
||||
}).catch((err) => {
|
||||
spinner.fail(`Failed to build Admin UI${EOL}`)
|
||||
reporter.panic(err)
|
||||
})
|
||||
|
||||
spinner.succeed(`Admin UI build - ${Date.now() - time}ms`)
|
||||
}
|
||||
}
|
||||
36
packages/admin/src/types/index.ts
Normal file
36
packages/admin/src/types/index.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
export type PluginOptions = {
|
||||
/**
|
||||
* Determines whether the admin dashboard should be served.
|
||||
*/
|
||||
serve?: boolean
|
||||
/**
|
||||
* The path to the admin dashboard. Should not be either prefixed or suffixed with a slash.
|
||||
* The chosen path cannot be one of the reserved paths: "admin", "store".
|
||||
* @default "app"
|
||||
*/
|
||||
path?: string
|
||||
/**
|
||||
* Backend to use for the admin dashboard. This should only be used if you
|
||||
* intend on hosting the dashboard separately from your Medusa server.
|
||||
* @default undefined
|
||||
*/
|
||||
backend?: string
|
||||
/**
|
||||
* The directory to output the build to. By default the plugin will build
|
||||
* the dashboard to the `build` directory of the `@medusajs/admin-ui` package.
|
||||
* If you intend on hosting the dashboard separately from your Medusa server,
|
||||
* you should use this option to specify a custom build directory, that you can
|
||||
* deploy to your host of choice.
|
||||
* @default undefined
|
||||
*/
|
||||
outDir?: string
|
||||
}
|
||||
|
||||
type PluginObject = {
|
||||
resolve: string
|
||||
options: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type ConfigModule = {
|
||||
plugins: [PluginObject | string]
|
||||
}
|
||||
3
packages/admin/src/utils/index.ts
Normal file
3
packages/admin/src/utils/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { loadConfig } from "./load-config"
|
||||
export { reporter } from "./reporter"
|
||||
export { validatePath } from "./validate-path"
|
||||
32
packages/admin/src/utils/load-config.ts
Normal file
32
packages/admin/src/utils/load-config.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { getConfigFile } from "medusa-core-utils"
|
||||
import { ConfigModule, PluginOptions } from "../types"
|
||||
|
||||
export const loadConfig = () => {
|
||||
const { configModule } = getConfigFile<ConfigModule>(
|
||||
process.cwd(),
|
||||
"medusa-config"
|
||||
)
|
||||
|
||||
const plugin = configModule.plugins.find(
|
||||
(p) =>
|
||||
(typeof p === "string" && p === "@medusajs/admin") ||
|
||||
(typeof p === "object" && p.resolve === "@medusajs/admin")
|
||||
)
|
||||
|
||||
let defaultConfig: PluginOptions = {
|
||||
serve: true,
|
||||
path: "app",
|
||||
}
|
||||
|
||||
if (typeof plugin !== "string") {
|
||||
const { options } = plugin as { options: PluginOptions }
|
||||
defaultConfig = {
|
||||
serve: options.serve ?? defaultConfig.serve,
|
||||
path: options.path ?? defaultConfig.path,
|
||||
backend: options.backend ?? defaultConfig.backend,
|
||||
outDir: options.outDir ?? defaultConfig.outDir,
|
||||
}
|
||||
}
|
||||
|
||||
return defaultConfig
|
||||
}
|
||||
19
packages/admin/src/utils/reporter.ts
Normal file
19
packages/admin/src/utils/reporter.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
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)}`)
|
||||
},
|
||||
}
|
||||
15
packages/admin/src/utils/validate-path.ts
Normal file
15
packages/admin/src/utils/validate-path.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export const validatePath = (path: string) => {
|
||||
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