feat(admin,admin-ui,medusa): Add Medusa Admin plugin (#3334)

This commit is contained in:
Kasper Fabricius Kristensen
2023-03-03 10:09:16 +01:00
committed by GitHub
parent d6b1ad1ccd
commit 40de54b010
928 changed files with 85441 additions and 384 deletions

View 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
}

View 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,
}
}

View 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
}

View 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)
})

View 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`)
}
}

View 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]
}

View File

@@ -0,0 +1,3 @@
export { loadConfig } from "./load-config"
export { reporter } from "./reporter"
export { validatePath } from "./validate-path"

View 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
}

View 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)}`)
},
}

View 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".`
)
}
}