Skip to content

Commit

Permalink
feat(dashboard,admin-sdk,admin-shared,admin-vite-plugin): Add support…
Browse files Browse the repository at this point in the history
… for UI extensions (#7383)

* intial work

* update lock

* add routes and fix HMR of configs

* cleanup

* rm imports

* rm debug from plugin

* address feedback

* address feedback
  • Loading branch information
kasperkristensen committed May 23, 2024
1 parent 521c252 commit f1176a0
Show file tree
Hide file tree
Showing 50 changed files with 1,359 additions and 1,091 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ module.exports = {

"./packages/admin-next/dashboard/tsconfig.json",
"./packages/admin-next/admin-sdk/tsconfig.json",
"./packages/admin-next/admin-shared/tsconfig.json",
"./packages/admin-next/admin-vite-plugin/tsconfig.json",

"./packages/inventory/tsconfig.spec.json",
Expand Down
3 changes: 1 addition & 2 deletions packages/admin-next/admin-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,19 @@
"devDependencies": {
"@medusajs/types": "^1.11.16",
"@types/compression": "^1.7.5",
"@types/connect-history-api-fallback": "^1.5.4",
"copyfiles": "^2.4.1",
"express": "^4.18.2",
"tsup": "^8.0.1",
"typescript": "^5.3.3"
},
"dependencies": {
"@medusajs/admin-shared": "0.0.1",
"@medusajs/admin-vite-plugin": "0.0.1",
"@medusajs/dashboard": "0.0.1",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.16",
"commander": "^11.1.0",
"compression": "^1.7.4",
"connect-history-api-fallback": "^2.0.0",
"deepmerge": "^4.3.1",
"glob": "^7.1.6",
"postcss": "^8.4.32",
Expand Down
4 changes: 4 additions & 0 deletions packages/admin-next/admin-sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { defineRouteConfig, defineWidgetConfig } from "@medusajs/admin-shared"

export { build } from "./lib/build"
export { develop } from "./lib/develop"
export { serve } from "./lib/serve"

export { defineRouteConfig, defineWidgetConfig }

export * from "./types"
14 changes: 6 additions & 8 deletions packages/admin-next/admin-sdk/src/lib/build.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import type { InlineConfig } from "vite"
import { BundlerOptions } from "../types"
import { getViteConfig } from "./config"

export async function build(options: BundlerOptions) {
const vite = await import("vite")

const viteConfig = await getViteConfig(options)

try {
await vite.build(
vite.mergeConfig(viteConfig, { mode: "production", logLevel: "silent" })
)
} catch (error) {
console.error(error)
throw new Error("Failed to build admin panel")
const buildConfig: InlineConfig = {
mode: "production",
logLevel: "error",
}

await vite.build(vite.mergeConfig(viteConfig, buildConfig))
}
37 changes: 19 additions & 18 deletions packages/admin-next/admin-sdk/src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { VIRTUAL_MODULES } from "@medusajs/admin-shared"
import path from "path"
import { Config } from "tailwindcss"
import type { InlineConfig } from "vite"
Expand All @@ -10,7 +11,7 @@ export async function getViteConfig(
): Promise<InlineConfig> {
const { searchForWorkspaceRoot } = await import("vite")
const { default: react } = await import("@vitejs/plugin-react")
const { default: inject } = await import("@medusajs/admin-vite-plugin")
const { default: medusa } = await import("@medusajs/admin-vite-plugin")

const getPort = await import("get-port")
const hmrPort = await getPort.default()
Expand All @@ -20,27 +21,23 @@ export async function getViteConfig(
const backendUrl = options.backendUrl ?? ""

return {
root: path.resolve(__dirname, "./"),
root,
base: options.path,
build: {
emptyOutDir: true,
outDir: path.resolve(process.cwd(), options.outDir),
},
optimizeDeps: {
include: ["@medusajs/dashboard", "react-dom/client"],
exclude: VIRTUAL_MODULES,
},
define: {
__BASE__: JSON.stringify(options.path),
__BACKEND_URL__: JSON.stringify(backendUrl),
},
server: {
open: true,
fs: {
allow: [
searchForWorkspaceRoot(process.cwd()),
path.resolve(__dirname, "../../medusa"),
path.resolve(__dirname, "../../app"),
],
allow: [searchForWorkspaceRoot(process.cwd())],
},
hmr: {
port: hmrPort,
Expand All @@ -51,28 +48,30 @@ export async function getViteConfig(
postcss: {
plugins: [
require("tailwindcss")({
config: createTailwindConfig(root),
config: createTailwindConfig(root, options.sources),
}),
],
},
},
/**
* TODO: Remove polyfills, they are currently only required for the
* `axios` dependency in the dashboard. Once we have the new SDK,
* we should remove this, and leave it up to the user to include
* polyfills if they need them.
*/
plugins: [
react(),
inject(),
medusa({
sources: options.sources,
}),
/**
* TODO: Remove polyfills, they are currently only required for the
* `axios` dependency in the dashboard. Once we have the new SDK,
* we should remove this, and leave it up to the user to include
* polyfills if they need them.
*/
nodePolyfills({
include: ["crypto", "util", "stream"],
}),
],
}
}

function createTailwindConfig(entry: string) {
function createTailwindConfig(entry: string, sources: string[] = []) {
const root = path.join(entry, "**/*.{js,ts,jsx,tsx}")
const html = path.join(entry, "index.html")

Expand All @@ -98,9 +97,11 @@ function createTailwindConfig(entry: string) {
// ignore
}

const extensions = sources.map((s) => path.join(s, "**/*.{js,ts,jsx,tsx}"))

const config: Config = {
presets: [require("@medusajs/ui-preset")],
content: [html, root, dashboard, ui],
content: [html, root, dashboard, ui, ...extensions],
darkMode: "class",
}

Expand Down
13 changes: 11 additions & 2 deletions packages/admin-next/admin-sdk/src/lib/develop.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import express from "express"
import type { InlineConfig } from "vite"

import { BundlerOptions } from "../types"
import { getViteConfig } from "./config"
Expand All @@ -10,14 +11,22 @@ export async function develop(options: BundlerOptions) {

try {
const viteConfig = await getViteConfig(options)

const developConfig: InlineConfig = {
mode: "development",
logLevel: "warn",
}

const server = await vite.createServer(
vite.mergeConfig(viteConfig, { logLevel: "info", mode: "development" })
vite.mergeConfig(viteConfig, developConfig)
)

router.use(server.middlewares)
} catch (error) {
console.error(error)
throw new Error("Could not start development server")
throw new Error(
"Failed to start admin development server. See error above."
)
}

return router
Expand Down
5 changes: 4 additions & 1 deletion packages/admin-next/admin-sdk/src/lib/serve.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import compression from "compression"
import { Request, Response, Router, static as static_ } from "express"
import fs from "fs"
import { ServerResponse } from "http"
Expand All @@ -24,7 +25,7 @@ export async function serve(options: ServeOptions) {

if (!indexExists) {
throw new Error(
`Could not find the admin UI build files. Please run \`npm run build\` or \`yarn build\` command and try again.`
`Could not find index.html in the admin build directory. Make sure to run 'medusa build' before starting the server.`
)
}

Expand All @@ -41,6 +42,8 @@ export async function serve(options: ServeOptions) {
res.setHeader("Vary", "Origin, Cache-Control")
}

router.use(compression())

router.get("/", sendHtml)
router.use(
static_(options.outDir, {
Expand Down
4 changes: 3 additions & 1 deletion packages/admin-next/admin-sdk/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { AdminOptions } from "@medusajs/types"

export type BundlerOptions = Required<Pick<AdminOptions, "outDir" | "path">> &
Pick<AdminOptions, "vite" | "backendUrl">
Pick<AdminOptions, "vite" | "backendUrl"> & {
sources?: string[]
}
2 changes: 1 addition & 1 deletion packages/admin-next/admin-sdk/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"exclude": ["tsup.config.ts", "node_modules", "dist"]
"exclude": ["tsup.config.cjs", "node_modules", "dist"]
}
5 changes: 4 additions & 1 deletion packages/admin-next/admin-shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
"author": "Kasper Kristensen <kasper@medusajs.com>",
"types": "dist/index.d.ts",
"main": "dist/index.js",
"module": "dist/index.mjs",
"files": [
"dist",
"package.json"
],
"scripts": {
"build": "tsc"
"build": "tsup"
},
"devDependencies": {
"@types/react": "^18.3.2",
"tsup": "^8.0.2",
"typescript": "^5.3.3"
},
"packageManager": "yarn@3.2.1"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./types"
export * from "./utils"
12 changes: 12 additions & 0 deletions packages/admin-next/admin-shared/src/extensions/config/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { ComponentType } from "react"

import { InjectionZone } from "../widgets"

export type WidgetConfig = {
zone: InjectionZone | InjectionZone[]
}

export type RouteConfig = {
label?: string
icon?: ComponentType
}
37 changes: 37 additions & 0 deletions packages/admin-next/admin-shared/src/extensions/config/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { RouteConfig, WidgetConfig } from "./types"

function createConfigHelper<TConfig extends Record<string, unknown>>(
config: TConfig
): TConfig {
return {
...config,
/**
* This property is required to allow the config to be exported,
* while still allowing HMR to work correctly.
*
* It tricks Fast Refresh into thinking that the config is a React component,
* which allows it to be updated without a full page reload.
*/
$$typeof: Symbol.for("react.memo"),
}
}

/**
* Define a widget configuration.
*
* @param config The widget configuration.
* @returns The widget configuration.
*/
export function defineWidgetConfig(config: WidgetConfig) {
return createConfigHelper(config)
}

/**
* Define a route configuration.
*
* @param config The route configuration.
* @returns The route configuration.
*/
export function defineRouteConfig(config: RouteConfig) {
return createConfigHelper(config)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ROUTE_IMPORTS = ["routes/pages", "routes/links"] as const
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./constants"
export * from "./types"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { ROUTE_IMPORTS } from "./constants"

export type RouteImport = (typeof ROUTE_IMPORTS)[number]
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { ROUTE_IMPORTS } from "../routes"
import { INJECTION_ZONES } from "../widgets"
import { getVirtualId, getWidgetImport, resolveVirtualId } from "./utils"

const VIRTUAL_WIDGET_MODULES = INJECTION_ZONES.map((zone) => {
return getVirtualId(getWidgetImport(zone))
})

const VIRTUAL_ROUTE_MODULES = ROUTE_IMPORTS.map((route) => {
return getVirtualId(route)
})

/**
* All virtual modules that are used in the admin panel. Virtual modules are used
* to inject custom widgets, routes and settings. A virtual module is imported using
* a string that corresponds to the id of the virtual module.
*
* @example
* ```ts
* import ProductDetailsBefore from "virtual:medusa/widgets/product/details/before"
* ```
*/
export const VIRTUAL_MODULES = [
...VIRTUAL_WIDGET_MODULES,
...VIRTUAL_ROUTE_MODULES,
]

/**
* Reolved paths to all virtual widget modules.
*/
export const RESOLVED_WIDGET_MODULES = VIRTUAL_WIDGET_MODULES.map((id) => {
return resolveVirtualId(id)
})

/**
* Reolved paths to all virtual route modules.
*/
export const RESOLVED_ROUTE_MODULES = VIRTUAL_ROUTE_MODULES.map((id) => {
return resolveVirtualId(id)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./constants"
export * from "./utils"
25 changes: 25 additions & 0 deletions packages/admin-next/admin-shared/src/extensions/virtual/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { InjectionZone } from "../widgets"

const PREFIX = "virtual:medusa/"

export const getVirtualId = (name: string) => {
return `${PREFIX}${name}`
}

export const resolveVirtualId = (id: string) => {
return `\0${id}`
}

export const getWidgetImport = (zone: InjectionZone) => {
return `widgets/${zone.replace(/\./g, "/")}`
}

export const getWidgetZone = (resolvedId: string): InjectionZone => {
const virtualPrefix = `\0${PREFIX}widgets/`

const zone = resolvedId
.replace(virtualPrefix, "")
.replace(/\//g, ".") as InjectionZone

return zone as InjectionZone
}
Loading

0 comments on commit f1176a0

Please sign in to comment.