Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(admin): Improve DX for deploying admin externally #3418

Merged
merged 18 commits into from
Mar 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/mean-ligers-fry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@medusajs/admin-ui": patch
"@medusajs/admin": patch
---

feat(admin,admin-ui): Updates the default behaviour of the plugin, and makes building for external deployment easier
18 changes: 16 additions & 2 deletions packages/admin-ui/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import dns from "dns"
import fse from "fs-extra"
import { resolve } from "path"
import vite from "vite"
import { AdminBuildConfig } from "./types"
import { getCustomViteConfig } from "./utils"
import { AdminDevConfig } from "./types/dev"
import { getCustomViteConfig, getCustomViteDevConfig } from "./utils"

async function build(options?: AdminBuildConfig) {
const config = getCustomViteConfig(options)
Expand All @@ -25,4 +27,16 @@ async function clean() {
throw new Error("Not implemented")
}

export { build, watch, clean }
async function dev(options: AdminDevConfig) {
// Resolve localhost for Node v16 and older.
// @see https://vitejs.dev/config/server-options.html#server-host.
dns.setDefaultResultOrder("verbatim")

const server = await vite.createServer(getCustomViteDevConfig(options))
await server.listen()

server.printUrls()
}

export { build, dev, watch, clean }
export type { AdminBuildConfig, AdminDevConfig }
4 changes: 4 additions & 0 deletions packages/admin-ui/src/types/dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type AdminDevConfig = {
backend?: string
port?: number
}
1 change: 1 addition & 0 deletions packages/admin-ui/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./build"
export * from "./dev"
export * from "./misc"
6 changes: 5 additions & 1 deletion packages/admin-ui/src/utils/format-base.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Base } from "../types"

export const formatBase = <T extends string>(base: T): Base<T> => {
export const formatBase = <T extends string>(base?: T): Base<T> => {
if (!base) {
return undefined
}

return `/${base}/`
}
16 changes: 8 additions & 8 deletions packages/admin-ui/src/utils/get-custom-vite-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ export const getCustomViteConfig = (config: AdminBuildConfig): InlineConfig => {
const uiPath = resolve(__dirname, "..", "..", "ui")

const globalReplacements = () => {
const base = globals.base || "app"

let backend = "/"
let backend = undefined

if (globals.backend) {
try {
Expand All @@ -26,10 +24,12 @@ export const getCustomViteConfig = (config: AdminBuildConfig): InlineConfig => {
}
}

return {
__BASE__: JSON.stringify(`/${base}`),
__MEDUSA_BACKEND_URL__: JSON.stringify(backend),
}
const global = {}

global["__BASE__"] = JSON.stringify(globals.base ? `/${globals.base}` : "/")
global["__MEDUSA_BACKEND_URL__"] = JSON.stringify(backend ? backend : "/")

return global
}

const buildConfig = (): BuildOptions => {
Expand All @@ -41,7 +41,7 @@ export const getCustomViteConfig = (config: AdminBuildConfig): InlineConfig => {
/**
* Default build directory is at the root of the `@medusajs/admin-ui` package.
*/
destDir = resolve(__dirname, "..", "..", "build")
destDir = resolve(process.cwd(), "build")
} else {
/**
* If a custom build directory is specified, it is resolved relative to the
Expand Down
24 changes: 24 additions & 0 deletions packages/admin-ui/src/utils/get-custom-vite-dev-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import react from "@vitejs/plugin-react"
import { resolve } from "path"
import { InlineConfig } from "vite"
import { AdminDevConfig } from "../types/dev"

export const getCustomViteDevConfig = ({
backend = "http://localhost:9000",
port = 7001,
}: AdminDevConfig): InlineConfig => {
const uiPath = resolve(__dirname, "..", "..", "ui")

return {
define: {
__BASE__: JSON.stringify("/"),
__MEDUSA_BACKEND_URL__: JSON.stringify(backend),
},
plugins: [react()],
root: uiPath,
mode: "development",
server: {
port,
},
}
}
1 change: 1 addition & 0 deletions packages/admin-ui/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./format-base"
export * from "./get-custom-vite-config"
export * from "./get-custom-vite-dev-config"
16 changes: 8 additions & 8 deletions packages/admin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ module.exports = {

The plugin can be configured with the following options:

| Option | Type | Description | Default |
| --------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| `serve` | `boolean?` | Whether to serve the admin dashboard or not. | `true` |
| `path` | `string?` | The path the admin server should run on. Should not be prefixed or suffixed with a slash. Cannot be one of the reserved paths: `"admin"` and `"store"`. | `"app"` |
| `outDir` | `string?` | Optional path for where to output the admin build files | `undefined` |
| `backend` | `string?` | URL to server. Should only be set if you plan on hosting the admin dashboard separately from your server | `undefined` |
| Option | Type | Description | Default |
| ------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| `serve` | `boolean?` | Whether to serve the admin dashboard or not. | `true` |
| `path` | `string?` | The path the admin server should run on. Should not be prefixed or suffixed with a slash. Cannot be one of the reserved paths: `"admin"` and `"store"`. | `"app"` |
| `outDir` | `string?` | Optional path for where to output the admin build files | `undefined` |
| `autoRebuild` | `boolean?` | Decides whether the admin UI should be rebuild if any changes or a missing build is detected during server startup | `false` |

**Hint**: You can import the PluginOptions type for inline documentation for the different options:

Expand All @@ -91,9 +91,9 @@ module.exports = {

## Building the admin dashboard

The admin will be built automatically the first time you start your server. Any subsequent changes to the plugin options will result in a rebuild of the admin dashboard.
The admin will be built automatically the first time you start your server if you have enabled `autoRebuild`. Any subsequent changes to the plugin options will result in a rebuild of the admin dashboard.

You may need to manually trigger a rebuild sometimes, for example after you have upgraded to a newer version of `@medusajs/admin`. You can do so by adding the following script to your `package.json`:
You may need to manually trigger a rebuild sometimes, for example after you have upgraded to a newer version of `@medusajs/admin`, or if you have disabled `autoRebuild`. You can do so by adding the following script to your `package.json`:

```json
{
Expand Down
4 changes: 3 additions & 1 deletion packages/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@
"dependencies": {
"@medusajs/admin-ui": "*",
"commander": "^10.0.0",
"dotenv": "^16.0.3",
"express": "^4.17.1",
"fs-extra": "^11.1.0",
"medusa-core-utils": "*",
"ora": "5.4.0",
"picocolors": "^1.0.0"
"picocolors": "^1.0.0",
"ts-dedent": "^2.2.0"
},
"peerDependencies": {
"@medusajs/medusa": "*"
Expand Down
24 changes: 11 additions & 13 deletions packages/admin/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,36 +13,34 @@ export default function (_rootDirectory: string, options: PluginOptions) {

if (serve) {
let buildPath: string
let htmlPath: string

// If an outDir is provided we use that, otherwise we default to "build".
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")
buildPath = resolve(process.cwd(), "build")
}

const 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) {

const indexExists = fse.existsSync(htmlPath)

if (!indexExists) {
reporter.panic(
new Error(
`Could not find the admin UI build files. Please run ${colors.bold(
"`medusa-admin build`"
)} to build the admin UI.`
)} or enable ${colors.bold(
`autoRebuild`
)} in the plugin options to build the admin UI.`
)
)
}
Expand Down
98 changes: 75 additions & 23 deletions packages/admin/src/commands/build.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,100 @@
import { build as buildAdmin } from "@medusajs/admin-ui"
import { AdminBuildConfig, build as buildAdmin } from "@medusajs/admin-ui"
import dotenv from "dotenv"
import fse from "fs-extra"
import ora from "ora"
import { EOL } from "os"
import { resolve } from "path"
import { loadConfig, reporter, validatePath } from "../utils"

type BuildArgs = {
deployment?: boolean
outDir?: string
backend?: string
path?: string
include?: string[]
includeDist?: string
}

let ENV_FILE_NAME = ""
switch (process.env.NODE_ENV) {
case "production":
ENV_FILE_NAME = ".env.production"
break
case "staging":
ENV_FILE_NAME = ".env.staging"
break
case "test":
ENV_FILE_NAME = ".env.test"
break
case "development":
default:
ENV_FILE_NAME = ".env"
break
}

try {
dotenv.config({ path: process.cwd() + "/" + ENV_FILE_NAME })
} catch (e) {
reporter.warn(`Failed to load environment variables from ${ENV_FILE_NAME}`)
}

export default async function build(args: BuildArgs) {
const { path, backend, outDir } = mergeArgs(args)
const { deployment, outDir: outDirArg, backend, include, includeDist } = args

try {
validatePath(path)
} catch (err) {
reporter.panic(err)
let config: AdminBuildConfig = {}

if (deployment) {
config = {
build: {
outDir: outDirArg,
},
globals: {
backend: backend || process.env.MEDUSA_BACKEND_URL,
},
}
} else {
const { path, outDir } = loadConfig()

try {
validatePath(path)
} catch (err) {
reporter.panic(err)
}

config = {
build: {
outDir: outDir,
},
globals: {
base: path,
},
}
}

const time = Date.now()
const spinner = ora().start(`Building Admin UI${EOL}`)

await buildAdmin({
build: {
outDir: outDir,
},
globals: {
base: path,
backend: backend,
},
...config,
}).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()
/**
* If we have specified files to include in the build, we copy them
* to the build directory.
*/
if (include && include.length > 0) {
const dist = outDirArg || resolve(process.cwd(), "build")

return {
path: args.path || path,
backend: args.backend || backend,
outDir: args.outDir || outDir,
try {
for (const filePath of include) {
await fse.copy(filePath, resolve(dist, includeDist, filePath))
}
} catch (err) {
reporter.panic(err)
}
}

spinner.succeed(`Admin UI build - ${Date.now() - time}ms`)
}
34 changes: 33 additions & 1 deletion packages/admin/src/commands/create-cli.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,47 @@
import { Command } from "commander"
import build from "./build"
import dev from "./dev"
import eject from "./eject"

export async function createCli(): Promise<Command> {
const program = new Command()

const buildCommand = program.command("build")
buildCommand.description("Build the admin dashboard")

buildCommand.option(
"--deployment",
"Build for deploying to and external host (e.g. Vercel)"
)

buildCommand.option("-o, --out-dir <path>", "Output directory")
buildCommand.option("-b, --backend <url>", "Backend URL")
buildCommand.option("-p, --path <path>", "Base path")
buildCommand.option(
"-i, --include [paths...]]",
"Paths to files that should be included in the build"
)
buildCommand.option(
"-d, --include-dist <path>",
"Path to where the files specified in the include option should be placed. Relative to the root of the build directory."
)

buildCommand.action(build)

const devCommand = program.command("dev")
devCommand.description("Start the admin dashboard in development mode")
devCommand.option("-p, --port <port>", "Port (default: 7001))")
devCommand.option(
"-b, --backend <url>",
"Backend URL (default http://localhost:9000)"
)
devCommand.action(dev)

const deployCommand = program.command("eject")
deployCommand.description(
"Eject the admin dashboard source code to a custom directory"
)
deployCommand.option("-o, --out-dir <path>", "Output directory")
deployCommand.action(eject)

return program
}
6 changes: 6 additions & 0 deletions packages/admin/src/commands/dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { AdminDevConfig } from "@medusajs/admin-ui"
import { dev as devAdmin } from "@medusajs/admin-ui"

export default async function dev(args: AdminDevConfig) {
await devAdmin(args)
}
Loading