feat(dashboard,admin-sdk,admin-shared,admin-vite-plugin): Add support 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
This commit is contained in:
committed by
GitHub
parent
521c252dee
commit
f1176a0673
@@ -84,6 +84,7 @@ module.exports = {
|
|||||||
|
|
||||||
"./packages/admin-next/dashboard/tsconfig.json",
|
"./packages/admin-next/dashboard/tsconfig.json",
|
||||||
"./packages/admin-next/admin-sdk/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/admin-next/admin-vite-plugin/tsconfig.json",
|
||||||
|
|
||||||
"./packages/inventory/tsconfig.spec.json",
|
"./packages/inventory/tsconfig.spec.json",
|
||||||
|
|||||||
@@ -15,20 +15,19 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@medusajs/types": "^1.11.16",
|
"@medusajs/types": "^1.11.16",
|
||||||
"@types/compression": "^1.7.5",
|
"@types/compression": "^1.7.5",
|
||||||
"@types/connect-history-api-fallback": "^1.5.4",
|
|
||||||
"copyfiles": "^2.4.1",
|
"copyfiles": "^2.4.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"tsup": "^8.0.1",
|
"tsup": "^8.0.1",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@medusajs/admin-shared": "0.0.1",
|
||||||
"@medusajs/admin-vite-plugin": "0.0.1",
|
"@medusajs/admin-vite-plugin": "0.0.1",
|
||||||
"@medusajs/dashboard": "0.0.1",
|
"@medusajs/dashboard": "0.0.1",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"commander": "^11.1.0",
|
"commander": "^11.1.0",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"connect-history-api-fallback": "^2.0.0",
|
|
||||||
"deepmerge": "^4.3.1",
|
"deepmerge": "^4.3.1",
|
||||||
"glob": "^7.1.6",
|
"glob": "^7.1.6",
|
||||||
"postcss": "^8.4.32",
|
"postcss": "^8.4.32",
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
import { defineRouteConfig, defineWidgetConfig } from "@medusajs/admin-shared"
|
||||||
|
|
||||||
export { build } from "./lib/build"
|
export { build } from "./lib/build"
|
||||||
export { develop } from "./lib/develop"
|
export { develop } from "./lib/develop"
|
||||||
export { serve } from "./lib/serve"
|
export { serve } from "./lib/serve"
|
||||||
|
|
||||||
|
export { defineRouteConfig, defineWidgetConfig }
|
||||||
|
|
||||||
export * from "./types"
|
export * from "./types"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { InlineConfig } from "vite"
|
||||||
import { BundlerOptions } from "../types"
|
import { BundlerOptions } from "../types"
|
||||||
import { getViteConfig } from "./config"
|
import { getViteConfig } from "./config"
|
||||||
|
|
||||||
@@ -5,13 +6,10 @@ export async function build(options: BundlerOptions) {
|
|||||||
const vite = await import("vite")
|
const vite = await import("vite")
|
||||||
|
|
||||||
const viteConfig = await getViteConfig(options)
|
const viteConfig = await getViteConfig(options)
|
||||||
|
const buildConfig: InlineConfig = {
|
||||||
try {
|
mode: "production",
|
||||||
await vite.build(
|
logLevel: "error",
|
||||||
vite.mergeConfig(viteConfig, { mode: "production", logLevel: "silent" })
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
throw new Error("Failed to build admin panel")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await vite.build(vite.mergeConfig(viteConfig, buildConfig))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { VIRTUAL_MODULES } from "@medusajs/admin-shared"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Config } from "tailwindcss"
|
import { Config } from "tailwindcss"
|
||||||
import type { InlineConfig } from "vite"
|
import type { InlineConfig } from "vite"
|
||||||
@@ -10,7 +11,7 @@ export async function getViteConfig(
|
|||||||
): Promise<InlineConfig> {
|
): Promise<InlineConfig> {
|
||||||
const { searchForWorkspaceRoot } = await import("vite")
|
const { searchForWorkspaceRoot } = await import("vite")
|
||||||
const { default: react } = await import("@vitejs/plugin-react")
|
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 getPort = await import("get-port")
|
||||||
const hmrPort = await getPort.default()
|
const hmrPort = await getPort.default()
|
||||||
@@ -20,7 +21,7 @@ export async function getViteConfig(
|
|||||||
const backendUrl = options.backendUrl ?? ""
|
const backendUrl = options.backendUrl ?? ""
|
||||||
|
|
||||||
return {
|
return {
|
||||||
root: path.resolve(__dirname, "./"),
|
root,
|
||||||
base: options.path,
|
base: options.path,
|
||||||
build: {
|
build: {
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
@@ -28,19 +29,15 @@ export async function getViteConfig(
|
|||||||
},
|
},
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
include: ["@medusajs/dashboard", "react-dom/client"],
|
include: ["@medusajs/dashboard", "react-dom/client"],
|
||||||
|
exclude: VIRTUAL_MODULES,
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
__BASE__: JSON.stringify(options.path),
|
__BASE__: JSON.stringify(options.path),
|
||||||
__BACKEND_URL__: JSON.stringify(backendUrl),
|
__BACKEND_URL__: JSON.stringify(backendUrl),
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
open: true,
|
|
||||||
fs: {
|
fs: {
|
||||||
allow: [
|
allow: [searchForWorkspaceRoot(process.cwd())],
|
||||||
searchForWorkspaceRoot(process.cwd()),
|
|
||||||
path.resolve(__dirname, "../../medusa"),
|
|
||||||
path.resolve(__dirname, "../../app"),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
hmr: {
|
hmr: {
|
||||||
port: hmrPort,
|
port: hmrPort,
|
||||||
@@ -51,20 +48,22 @@ export async function getViteConfig(
|
|||||||
postcss: {
|
postcss: {
|
||||||
plugins: [
|
plugins: [
|
||||||
require("tailwindcss")({
|
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: [
|
plugins: [
|
||||||
react(),
|
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({
|
nodePolyfills({
|
||||||
include: ["crypto", "util", "stream"],
|
include: ["crypto", "util", "stream"],
|
||||||
}),
|
}),
|
||||||
@@ -72,7 +71,7 @@ export async function getViteConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTailwindConfig(entry: string) {
|
function createTailwindConfig(entry: string, sources: string[] = []) {
|
||||||
const root = path.join(entry, "**/*.{js,ts,jsx,tsx}")
|
const root = path.join(entry, "**/*.{js,ts,jsx,tsx}")
|
||||||
const html = path.join(entry, "index.html")
|
const html = path.join(entry, "index.html")
|
||||||
|
|
||||||
@@ -98,9 +97,11 @@ function createTailwindConfig(entry: string) {
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extensions = sources.map((s) => path.join(s, "**/*.{js,ts,jsx,tsx}"))
|
||||||
|
|
||||||
const config: Config = {
|
const config: Config = {
|
||||||
presets: [require("@medusajs/ui-preset")],
|
presets: [require("@medusajs/ui-preset")],
|
||||||
content: [html, root, dashboard, ui],
|
content: [html, root, dashboard, ui, ...extensions],
|
||||||
darkMode: "class",
|
darkMode: "class",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import express from "express"
|
import express from "express"
|
||||||
|
import type { InlineConfig } from "vite"
|
||||||
|
|
||||||
import { BundlerOptions } from "../types"
|
import { BundlerOptions } from "../types"
|
||||||
import { getViteConfig } from "./config"
|
import { getViteConfig } from "./config"
|
||||||
@@ -10,14 +11,22 @@ export async function develop(options: BundlerOptions) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const viteConfig = await getViteConfig(options)
|
const viteConfig = await getViteConfig(options)
|
||||||
|
|
||||||
|
const developConfig: InlineConfig = {
|
||||||
|
mode: "development",
|
||||||
|
logLevel: "warn",
|
||||||
|
}
|
||||||
|
|
||||||
const server = await vite.createServer(
|
const server = await vite.createServer(
|
||||||
vite.mergeConfig(viteConfig, { logLevel: "info", mode: "development" })
|
vite.mergeConfig(viteConfig, developConfig)
|
||||||
)
|
)
|
||||||
|
|
||||||
router.use(server.middlewares)
|
router.use(server.middlewares)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(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
|
return router
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import compression from "compression"
|
||||||
import { Request, Response, Router, static as static_ } from "express"
|
import { Request, Response, Router, static as static_ } from "express"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import { ServerResponse } from "http"
|
import { ServerResponse } from "http"
|
||||||
@@ -24,7 +25,7 @@ export async function serve(options: ServeOptions) {
|
|||||||
|
|
||||||
if (!indexExists) {
|
if (!indexExists) {
|
||||||
throw new Error(
|
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.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +42,8 @@ export async function serve(options: ServeOptions) {
|
|||||||
res.setHeader("Vary", "Origin, Cache-Control")
|
res.setHeader("Vary", "Origin, Cache-Control")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
router.use(compression())
|
||||||
|
|
||||||
router.get("/", sendHtml)
|
router.get("/", sendHtml)
|
||||||
router.use(
|
router.use(
|
||||||
static_(options.outDir, {
|
static_(options.outDir, {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { AdminOptions } from "@medusajs/types"
|
import { AdminOptions } from "@medusajs/types"
|
||||||
|
|
||||||
export type BundlerOptions = Required<Pick<AdminOptions, "outDir" | "path">> &
|
export type BundlerOptions = Required<Pick<AdminOptions, "outDir" | "path">> &
|
||||||
Pick<AdminOptions, "vite" | "backendUrl">
|
Pick<AdminOptions, "vite" | "backendUrl"> & {
|
||||||
|
sources?: string[]
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,5 +17,5 @@
|
|||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
"exclude": ["tsup.config.ts", "node_modules", "dist"]
|
"exclude": ["tsup.config.cjs", "node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,17 @@
|
|||||||
"author": "Kasper Kristensen <kasper@medusajs.com>",
|
"author": "Kasper Kristensen <kasper@medusajs.com>",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/index.mjs",
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
"package.json"
|
"package.json"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc"
|
"build": "tsup"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.2",
|
||||||
|
"tsup": "^8.0.2",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@3.2.1"
|
"packageManager": "yarn@3.2.1"
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./types"
|
||||||
|
export * from "./utils"
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const ROUTE_IMPORTS = ["routes/pages", "routes/links"] as const
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./constants"
|
||||||
|
export * from "./types"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { ROUTE_IMPORTS } from "./constants"
|
||||||
|
|
||||||
|
export type RouteImport = (typeof ROUTE_IMPORTS)[number]
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./constants"
|
||||||
|
export * from "./utils"
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -1,64 +1,101 @@
|
|||||||
export const injectionZones = [
|
const ORDER_INJECTION_ZONES = [
|
||||||
// Order injection zones
|
|
||||||
"order.details.before",
|
"order.details.before",
|
||||||
"order.details.after",
|
"order.details.after",
|
||||||
"order.list.before",
|
"order.list.before",
|
||||||
"order.list.after",
|
"order.list.after",
|
||||||
// Draft order injection zones
|
] as const
|
||||||
|
|
||||||
|
const DRAFT_ORDER_INJECTION_ZONES = [
|
||||||
"draft_order.list.before",
|
"draft_order.list.before",
|
||||||
"draft_order.list.after",
|
"draft_order.list.after",
|
||||||
"draft_order.details.before",
|
"draft_order.details.before",
|
||||||
"draft_order.details.after",
|
"draft_order.details.after",
|
||||||
// Customer injection zones
|
] as const
|
||||||
|
|
||||||
|
const CUSTOMER_INJECTION_ZONES = [
|
||||||
"customer.details.before",
|
"customer.details.before",
|
||||||
"customer.details.after",
|
"customer.details.after",
|
||||||
"customer.list.before",
|
"customer.list.before",
|
||||||
"customer.list.after",
|
"customer.list.after",
|
||||||
// Customer group injection zones
|
] as const
|
||||||
|
|
||||||
|
const CUSTOMER_GROUP_INJECTION_ZONES = [
|
||||||
"customer_group.details.before",
|
"customer_group.details.before",
|
||||||
"customer_group.details.after",
|
"customer_group.details.after",
|
||||||
"customer_group.list.before",
|
"customer_group.list.before",
|
||||||
"customer_group.list.after",
|
"customer_group.list.after",
|
||||||
// Product injection zones
|
] as const
|
||||||
|
|
||||||
|
const PRODUCT_INJECTION_ZONES = [
|
||||||
"product.details.before",
|
"product.details.before",
|
||||||
"product.details.after",
|
"product.details.after",
|
||||||
"product.list.before",
|
"product.list.before",
|
||||||
"product.list.after",
|
"product.list.after",
|
||||||
"product.details.side.before",
|
"product.details.side.before",
|
||||||
"product.details.side.after",
|
"product.details.side.after",
|
||||||
// Product collection injection zones
|
] as const
|
||||||
|
|
||||||
|
const PRODUCT_COLLECTION_INJECTION_ZONES = [
|
||||||
"product_collection.details.before",
|
"product_collection.details.before",
|
||||||
"product_collection.details.after",
|
"product_collection.details.after",
|
||||||
"product_collection.list.before",
|
"product_collection.list.before",
|
||||||
"product_collection.list.after",
|
"product_collection.list.after",
|
||||||
// Product category injection zones
|
] as const
|
||||||
|
|
||||||
|
const PRODUCT_CATEGORY_INJECTION_ZONES = [
|
||||||
"product_category.details.before",
|
"product_category.details.before",
|
||||||
"product_category.details.after",
|
"product_category.details.after",
|
||||||
"product_category.list.before",
|
"product_category.list.before",
|
||||||
"product_category.list.after",
|
"product_category.list.after",
|
||||||
// Price list injection zones
|
] as const
|
||||||
|
|
||||||
|
const PRICE_LIST_INJECTION_ZONES = [
|
||||||
"price_list.details.before",
|
"price_list.details.before",
|
||||||
"price_list.details.after",
|
"price_list.details.after",
|
||||||
"price_list.list.before",
|
"price_list.list.before",
|
||||||
"price_list.list.after",
|
"price_list.list.after",
|
||||||
// Discount injection zones
|
] as const
|
||||||
|
|
||||||
|
const DISCOUNT_INJECTION_ZONES = [
|
||||||
"discount.details.before",
|
"discount.details.before",
|
||||||
"discount.details.after",
|
"discount.details.after",
|
||||||
"discount.list.before",
|
"discount.list.before",
|
||||||
"discount.list.after",
|
"discount.list.after",
|
||||||
// Promotion injection zones
|
] as const
|
||||||
|
|
||||||
|
const PROMOTION_INJECTION_ZONES = [
|
||||||
"promotion.details.before",
|
"promotion.details.before",
|
||||||
"promotion.details.after",
|
"promotion.details.after",
|
||||||
"promotion.list.before",
|
"promotion.list.before",
|
||||||
"promotion.list.after",
|
"promotion.list.after",
|
||||||
// Gift card injection zones
|
] as const
|
||||||
|
|
||||||
|
const GIFT_CARD_INJECTION_ZONES = [
|
||||||
"gift_card.details.before",
|
"gift_card.details.before",
|
||||||
"gift_card.details.after",
|
"gift_card.details.after",
|
||||||
"gift_card.list.before",
|
"gift_card.list.before",
|
||||||
"gift_card.list.after",
|
"gift_card.list.after",
|
||||||
"custom_gift_card.before",
|
"custom_gift_card.before",
|
||||||
"custom_gift_card.after",
|
"custom_gift_card.after",
|
||||||
// Login
|
] as const
|
||||||
"login.before",
|
|
||||||
"login.after",
|
const LOGIN_INJECTION_ZONES = ["login.before", "login.after"] as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All valid injection zones in the admin panel. An injection zone is a specific place
|
||||||
|
* in the admin panel where a plugin can inject custom widgets.
|
||||||
|
*/
|
||||||
|
export const INJECTION_ZONES = [
|
||||||
|
...ORDER_INJECTION_ZONES,
|
||||||
|
...DRAFT_ORDER_INJECTION_ZONES,
|
||||||
|
...CUSTOMER_INJECTION_ZONES,
|
||||||
|
...CUSTOMER_GROUP_INJECTION_ZONES,
|
||||||
|
...PRODUCT_INJECTION_ZONES,
|
||||||
|
...PRODUCT_COLLECTION_INJECTION_ZONES,
|
||||||
|
...PRODUCT_CATEGORY_INJECTION_ZONES,
|
||||||
|
...PRICE_LIST_INJECTION_ZONES,
|
||||||
|
...DISCOUNT_INJECTION_ZONES,
|
||||||
|
...PROMOTION_INJECTION_ZONES,
|
||||||
|
...GIFT_CARD_INJECTION_ZONES,
|
||||||
|
...LOGIN_INJECTION_ZONES,
|
||||||
] as const
|
] as const
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./constants"
|
||||||
|
export * from "./types"
|
||||||
|
export * from "./utils"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { INJECTION_ZONES } from "./constants"
|
||||||
|
|
||||||
|
export type InjectionZone = (typeof INJECTION_ZONES)[number]
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { INJECTION_ZONES } from "./constants"
|
||||||
|
import { InjectionZone } from "./types"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that the provided zone is a valid injection zone for a widget.
|
||||||
|
*/
|
||||||
|
export function isValidInjectionZone(zone: any): zone is InjectionZone {
|
||||||
|
return INJECTION_ZONES.includes(zone)
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from "./constants"
|
export * from "./extensions/config"
|
||||||
export * from "./types"
|
export * from "./extensions/virtual"
|
||||||
|
export * from "./extensions/widgets"
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
import { injectionZones } from "./constants"
|
|
||||||
|
|
||||||
export type InjectionZone = (typeof injectionZones)[number]
|
|
||||||
@@ -4,4 +4,5 @@ export default defineConfig({
|
|||||||
entry: ["./src/index.ts"],
|
entry: ["./src/index.ts"],
|
||||||
format: ["cjs", "esm"],
|
format: ["cjs", "esm"],
|
||||||
dts: true,
|
dts: true,
|
||||||
|
clean: true,
|
||||||
})
|
})
|
||||||
@@ -2,12 +2,13 @@
|
|||||||
"name": "@medusajs/admin-vite-plugin",
|
"name": "@medusajs/admin-vite-plugin",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
|
||||||
"module": "dist/index.mjs",
|
"module": "dist/index.mjs",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"import": "./dist/index.mjs",
|
"import": "./dist/index.mjs",
|
||||||
"require": "./dist/index.js"
|
"require": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
@@ -15,11 +16,11 @@
|
|||||||
"package.json"
|
"package.json"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup"
|
"build": "tsup",
|
||||||
|
"watch": "tsup --watch"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/types": "7.22.5",
|
"@babel/types": "7.22.5",
|
||||||
"@medusajs/admin-shared": "0.0.1",
|
|
||||||
"@types/babel__traverse": "7.20.5",
|
"@types/babel__traverse": "7.20.5",
|
||||||
"@types/node": "^20.10.4",
|
"@types/node": "^20.10.4",
|
||||||
"tsup": "8.0.1",
|
"tsup": "8.0.1",
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "7.23.5",
|
"@babel/parser": "7.23.5",
|
||||||
"@babel/traverse": "7.23.5",
|
"@babel/traverse": "7.23.5",
|
||||||
|
"@medusajs/admin-shared": "0.0.1",
|
||||||
"chokidar": "3.5.3",
|
"chokidar": "3.5.3",
|
||||||
"fdir": "6.1.1",
|
"fdir": "6.1.1",
|
||||||
"magic-string": "0.30.5"
|
"magic-string": "0.30.5"
|
||||||
|
|||||||
32
packages/admin-next/admin-vite-plugin/src/babel.ts
Normal file
32
packages/admin-next/admin-vite-plugin/src/babel.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { parse, type ParseResult, type ParserOptions } from "@babel/parser"
|
||||||
|
import _traverse, { type NodePath } from "@babel/traverse"
|
||||||
|
import {
|
||||||
|
ExportDefaultDeclaration,
|
||||||
|
ExportNamedDeclaration,
|
||||||
|
File,
|
||||||
|
ObjectProperty,
|
||||||
|
} from "@babel/types"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Depending on whether we are running the CJS or ESM build of the plugin, we
|
||||||
|
* need to import the default export of the `@babel/traverse` package in
|
||||||
|
* different ways.
|
||||||
|
*/
|
||||||
|
let traverse: typeof _traverse
|
||||||
|
|
||||||
|
if (typeof _traverse === "function") {
|
||||||
|
traverse = _traverse
|
||||||
|
} else {
|
||||||
|
traverse = (_traverse as any).default
|
||||||
|
}
|
||||||
|
|
||||||
|
export { parse, traverse }
|
||||||
|
export type {
|
||||||
|
ExportDefaultDeclaration,
|
||||||
|
ExportNamedDeclaration,
|
||||||
|
File,
|
||||||
|
NodePath,
|
||||||
|
ObjectProperty,
|
||||||
|
ParseResult,
|
||||||
|
ParserOptions,
|
||||||
|
}
|
||||||
@@ -1,885 +1,4 @@
|
|||||||
import { ParseResult, ParserOptions, parse } from "@babel/parser"
|
import { medusaVitePlugin, type MedusaVitePlugin } from "./plugin"
|
||||||
import _traverse, { NodePath } from "@babel/traverse"
|
|
||||||
import {
|
|
||||||
ExportDefaultDeclaration,
|
|
||||||
ExportNamedDeclaration,
|
|
||||||
File,
|
|
||||||
ObjectExpression,
|
|
||||||
ObjectProperty,
|
|
||||||
} from "@babel/types"
|
|
||||||
import chokidar from "chokidar"
|
|
||||||
import { fdir } from "fdir"
|
|
||||||
import fs from "fs/promises"
|
|
||||||
import MagicString from "magic-string"
|
|
||||||
import path from "path"
|
|
||||||
import { Logger, PluginOption, ViteDevServer } from "vite"
|
|
||||||
|
|
||||||
import { InjectionZone, injectionZones } from "@medusajs/admin-shared"
|
export default medusaVitePlugin
|
||||||
|
export type { MedusaVitePlugin }
|
||||||
const traverse = (_traverse as any).default as typeof _traverse
|
|
||||||
|
|
||||||
const VIRTUAL_PREFIX = "/@virtual/medusajs-admin-vite-plugin/"
|
|
||||||
const IMPORT_PREFIX = "medusa-admin:"
|
|
||||||
|
|
||||||
const WIDGET_MODULE = `${IMPORT_PREFIX}widgets/`
|
|
||||||
const WIDGET_MODULES = injectionZones.map((zone) => {
|
|
||||||
return `${WIDGET_MODULE}${zone.replace(/\./g, "/")}`
|
|
||||||
})
|
|
||||||
|
|
||||||
const ROUTE_PAGE_MODULE = `${IMPORT_PREFIX}routes/pages`
|
|
||||||
const ROUTE_LINK_MODULE = `${IMPORT_PREFIX}routes/links`
|
|
||||||
|
|
||||||
const ROUTE_MODULES = [ROUTE_PAGE_MODULE, ROUTE_LINK_MODULE]
|
|
||||||
|
|
||||||
const SETTING_PAGE_MODULE = `${IMPORT_PREFIX}settings/pages`
|
|
||||||
const SETTING_CARD_MODULE = `${IMPORT_PREFIX}settings/cards`
|
|
||||||
|
|
||||||
const SETTING_MODULE = [SETTING_PAGE_MODULE, SETTING_CARD_MODULE]
|
|
||||||
|
|
||||||
const MODULES = [...WIDGET_MODULES, ...ROUTE_MODULES, ...SETTING_MODULE]
|
|
||||||
|
|
||||||
type InjectArgs = {
|
|
||||||
sources?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type LoadModuleOptions =
|
|
||||||
| { type: "widget"; get: InjectionZone }
|
|
||||||
| { type: "route"; get: "page" | "link" }
|
|
||||||
| { type: "setting"; get: "page" | "card" }
|
|
||||||
|
|
||||||
export default function inject(args?: InjectArgs): PluginOption {
|
|
||||||
const _extensionGraph = new Map<string, Set<string>>()
|
|
||||||
const _sources = new Set<string>([...(args?.sources || [])])
|
|
||||||
|
|
||||||
let server: ViteDevServer
|
|
||||||
let watcher: chokidar.FSWatcher
|
|
||||||
let logger: Logger
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Traverses the directory and returns all files that ends with .tsx or .jsx,
|
|
||||||
* excluding files in subdirectories that starts with _.
|
|
||||||
*
|
|
||||||
* @param dir - The directory to traverse
|
|
||||||
* @param file - The file name to filter by without extension
|
|
||||||
* @param depth - The depth of the files to return
|
|
||||||
*/
|
|
||||||
async function traverseDirectory(
|
|
||||||
dir: string,
|
|
||||||
file?: string,
|
|
||||||
depth?: { min: number; max?: number }
|
|
||||||
) {
|
|
||||||
const baseDepth = dir.split(path.sep).length
|
|
||||||
|
|
||||||
const crawler = new fdir()
|
|
||||||
.withBasePath()
|
|
||||||
.exclude((dirName) => dirName.startsWith("_"))
|
|
||||||
.filter((path) => path.endsWith(".tsx") || path.endsWith(".jsx"))
|
|
||||||
|
|
||||||
if (file) {
|
|
||||||
crawler.filter(
|
|
||||||
(path) => path.endsWith(`${file}.tsx`) || path.endsWith(`${file}.jsx`)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (depth) {
|
|
||||||
crawler.filter((file) => {
|
|
||||||
const directoryDepth = file.split(path.sep).length - 1
|
|
||||||
|
|
||||||
if (depth.max && directoryDepth > baseDepth + depth.max) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (directoryDepth < baseDepth + depth.min) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return await crawler.crawl(dir).withPromise()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a module with a source map from a code string
|
|
||||||
*/
|
|
||||||
function generateModule(code: string) {
|
|
||||||
const magicString = new MagicString(code)
|
|
||||||
|
|
||||||
return {
|
|
||||||
code: magicString.toString(),
|
|
||||||
map: magicString.generateMap({ hires: true }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates that the default export of a file is a JSX component
|
|
||||||
*/
|
|
||||||
function validateDefaultExport(
|
|
||||||
path: NodePath<ExportDefaultDeclaration>,
|
|
||||||
ast: ParseResult<File>
|
|
||||||
) {
|
|
||||||
let hasComponentExport = false
|
|
||||||
const declaration = path.node.declaration
|
|
||||||
|
|
||||||
if (
|
|
||||||
declaration &&
|
|
||||||
(declaration.type === "Identifier" ||
|
|
||||||
declaration.type === "FunctionDeclaration")
|
|
||||||
) {
|
|
||||||
const exportName =
|
|
||||||
declaration.type === "Identifier"
|
|
||||||
? declaration.name
|
|
||||||
: declaration.id && declaration.id.name
|
|
||||||
|
|
||||||
if (exportName) {
|
|
||||||
try {
|
|
||||||
traverse(ast, {
|
|
||||||
VariableDeclarator({ node, scope }) {
|
|
||||||
let isDefaultExport = false
|
|
||||||
|
|
||||||
if (
|
|
||||||
node.id.type === "Identifier" &&
|
|
||||||
node.id.name === exportName
|
|
||||||
) {
|
|
||||||
isDefaultExport = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isDefaultExport) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
traverse(
|
|
||||||
node,
|
|
||||||
{
|
|
||||||
ReturnStatement(path) {
|
|
||||||
if (
|
|
||||||
path.node.argument?.type === "JSXElement" ||
|
|
||||||
path.node.argument?.type === "JSXFragment"
|
|
||||||
) {
|
|
||||||
hasComponentExport = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scope
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
console.error(
|
|
||||||
`An error occured while validating the default export of '${path}'. The following error must be resolved before continuing:\n${e}`
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return hasComponentExport
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the properties of the config object in an extension file
|
|
||||||
*/
|
|
||||||
function getProperties(path: NodePath<ExportNamedDeclaration>) {
|
|
||||||
const declaration = path.node.declaration
|
|
||||||
|
|
||||||
if (declaration && declaration.type === "VariableDeclaration") {
|
|
||||||
const configDeclaration = declaration.declarations.find(
|
|
||||||
(d) =>
|
|
||||||
d.type === "VariableDeclarator" &&
|
|
||||||
d.id.type === "Identifier" &&
|
|
||||||
d.id.name === "config"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
configDeclaration &&
|
|
||||||
configDeclaration.init?.type === "ObjectExpression"
|
|
||||||
) {
|
|
||||||
return configDeclaration.init.properties
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates that the provided zone is a valid injection zone for a widget
|
|
||||||
*/
|
|
||||||
function validateInjectionZone(zone: any): zone is InjectionZone {
|
|
||||||
return injectionZones.includes(zone)
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateWidgetConfig(
|
|
||||||
path: NodePath<ExportNamedDeclaration>,
|
|
||||||
zone?: InjectionZone
|
|
||||||
) {
|
|
||||||
const properties = getProperties(path)
|
|
||||||
|
|
||||||
if (!properties) {
|
|
||||||
return { zoneIsValid: false, zoneValue: undefined }
|
|
||||||
}
|
|
||||||
|
|
||||||
const zoneProperty = properties.find(
|
|
||||||
(p) =>
|
|
||||||
p.type === "ObjectProperty" &&
|
|
||||||
p.key.type === "Identifier" &&
|
|
||||||
p.key.name === "zone"
|
|
||||||
) as ObjectProperty | undefined
|
|
||||||
|
|
||||||
if (!zoneProperty) {
|
|
||||||
return { zoneIsValid: false, zoneValue: undefined }
|
|
||||||
}
|
|
||||||
|
|
||||||
let zoneIsValid = false
|
|
||||||
let zoneValue: string | string[] | undefined = undefined
|
|
||||||
|
|
||||||
if (zoneProperty.value.type === "StringLiteral") {
|
|
||||||
zoneIsValid = !zone
|
|
||||||
? validateInjectionZone(zoneProperty.value.value)
|
|
||||||
: zone === zoneProperty.value.value
|
|
||||||
zoneValue = zoneProperty.value.value
|
|
||||||
} else if (zoneProperty.value.type === "ArrayExpression") {
|
|
||||||
zoneIsValid = zoneProperty.value.elements.every((_zone) => {
|
|
||||||
if (!_zone || _zone.type !== "StringLiteral") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const isZoneMatch = !zone ? true : zone === _zone.value
|
|
||||||
|
|
||||||
return validateInjectionZone(_zone.value) && isZoneMatch
|
|
||||||
})
|
|
||||||
|
|
||||||
zoneValue = zoneProperty.value.elements
|
|
||||||
.map((e) => {
|
|
||||||
if (e && e.type === "StringLiteral") {
|
|
||||||
return e.value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(Boolean) as string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
return { zoneIsValid, zoneValue }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function validateWidget(file: string, zone?: InjectionZone) {
|
|
||||||
const content = await fs.readFile(file, "utf-8")
|
|
||||||
|
|
||||||
const parserOptions: ParserOptions = {
|
|
||||||
sourceType: "module",
|
|
||||||
plugins: ["jsx"],
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.endsWith(".tsx")) {
|
|
||||||
parserOptions.plugins?.push("typescript")
|
|
||||||
}
|
|
||||||
|
|
||||||
let ast: ParseResult<File>
|
|
||||||
|
|
||||||
try {
|
|
||||||
ast = parse(content, parserOptions)
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(
|
|
||||||
`An error occured while parsing the content of ${file}:\n${err}`,
|
|
||||||
{
|
|
||||||
error: err as Error,
|
|
||||||
timestamp: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return { isValidWidget: false, zoneValue: undefined }
|
|
||||||
}
|
|
||||||
|
|
||||||
let hasDefaultExport = false
|
|
||||||
let hasNamedExport = false
|
|
||||||
let zoneValue: string | string[] | undefined
|
|
||||||
|
|
||||||
try {
|
|
||||||
traverse(ast, {
|
|
||||||
ExportDefaultDeclaration(path) {
|
|
||||||
hasDefaultExport = validateDefaultExport(path, ast)
|
|
||||||
},
|
|
||||||
ExportNamedDeclaration(path) {
|
|
||||||
const { zoneIsValid, zoneValue: value } = validateWidgetConfig(
|
|
||||||
path,
|
|
||||||
zone
|
|
||||||
)
|
|
||||||
hasNamedExport = zoneIsValid
|
|
||||||
zoneValue = value
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`An error occured while validating the content of ${file}`, {
|
|
||||||
error: err as Error,
|
|
||||||
timestamp: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
return { isValidWidget: false, zoneValue: undefined }
|
|
||||||
}
|
|
||||||
|
|
||||||
return { isValidWidget: hasDefaultExport && hasNamedExport, zoneValue }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateWidgetEntrypoint(zone: InjectionZone) {
|
|
||||||
const files = (
|
|
||||||
await Promise.all(
|
|
||||||
Array.from(_sources).map(async (source) =>
|
|
||||||
traverseDirectory(`${source}/widgets`)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).flat()
|
|
||||||
|
|
||||||
const validatedWidgets = (
|
|
||||||
await Promise.all(
|
|
||||||
files.map(async (widget) => {
|
|
||||||
const { isValidWidget } = await validateWidget(widget, zone)
|
|
||||||
return isValidWidget ? widget : null
|
|
||||||
})
|
|
||||||
)
|
|
||||||
).filter(Boolean) as string[]
|
|
||||||
|
|
||||||
if (!validatedWidgets.length) {
|
|
||||||
const code = `export default {
|
|
||||||
widgets: [],
|
|
||||||
}`
|
|
||||||
|
|
||||||
return { module: generateModule(code), paths: [] }
|
|
||||||
}
|
|
||||||
|
|
||||||
const importString = validatedWidgets
|
|
||||||
.map((path, index) => `import WidgetExt${index} from "${path}";`)
|
|
||||||
.join("\n")
|
|
||||||
|
|
||||||
const exportString = `export default {
|
|
||||||
widgets: [${validatedWidgets
|
|
||||||
.map((_, index) => `{ Component: WidgetExt${index} }`)
|
|
||||||
.join(", ")}],
|
|
||||||
}`
|
|
||||||
|
|
||||||
const code = `${importString}\n${exportString}`
|
|
||||||
|
|
||||||
return { module: generateModule(code), paths: validatedWidgets }
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateRouteConfig(
|
|
||||||
path: NodePath<ExportNamedDeclaration>,
|
|
||||||
requireLink: boolean
|
|
||||||
) {
|
|
||||||
const properties = getProperties(path)
|
|
||||||
|
|
||||||
if (!properties) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const linkProperty = properties.find(
|
|
||||||
(p) =>
|
|
||||||
p.type === "ObjectProperty" &&
|
|
||||||
p.key.type === "Identifier" &&
|
|
||||||
p.key.name === "link"
|
|
||||||
) as ObjectProperty | undefined
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Link is optional unless requireLink is true.
|
|
||||||
*/
|
|
||||||
if (!linkProperty && !requireLink) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const linkValue = linkProperty?.value as ObjectExpression | undefined
|
|
||||||
|
|
||||||
if (!linkValue) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
let labelIsValid = false
|
|
||||||
|
|
||||||
if (
|
|
||||||
linkValue.properties.some(
|
|
||||||
(p) =>
|
|
||||||
p.type === "ObjectProperty" &&
|
|
||||||
p.key.type === "Identifier" &&
|
|
||||||
p.key.name === "label" &&
|
|
||||||
p.value.type === "StringLiteral"
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
labelIsValid = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return labelIsValid
|
|
||||||
}
|
|
||||||
|
|
||||||
async function validateRoute(file: string, requireLink: boolean) {
|
|
||||||
const content = await fs.readFile(file, "utf-8")
|
|
||||||
|
|
||||||
const parserOptions: ParserOptions = {
|
|
||||||
sourceType: "module",
|
|
||||||
plugins: ["jsx"],
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.endsWith(".tsx")) {
|
|
||||||
parserOptions.plugins?.push("typescript")
|
|
||||||
}
|
|
||||||
|
|
||||||
let ast: ParseResult<File>
|
|
||||||
|
|
||||||
try {
|
|
||||||
ast = parse(content, parserOptions)
|
|
||||||
} catch (err) {
|
|
||||||
logger.error("An error occured while validating a route.", {
|
|
||||||
error: err as Error,
|
|
||||||
timestamp: true,
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
let hasDefaultExport = false
|
|
||||||
let hasNamedExport = false
|
|
||||||
|
|
||||||
try {
|
|
||||||
traverse(ast, {
|
|
||||||
ExportDefaultDeclaration(path) {
|
|
||||||
hasDefaultExport = validateDefaultExport(path, ast)
|
|
||||||
},
|
|
||||||
ExportNamedDeclaration(path) {
|
|
||||||
hasNamedExport = validateRouteConfig(path, requireLink)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
logger.error("An error occured while validating a route.", {
|
|
||||||
error: err as Error,
|
|
||||||
timestamp: true,
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return hasDefaultExport && hasNamedExport
|
|
||||||
}
|
|
||||||
|
|
||||||
function createPath(file: string) {
|
|
||||||
return file
|
|
||||||
.replace(/.*\/admin\/(routes|settings)/, "")
|
|
||||||
.replace(/\[([^\]]+)\]/g, ":$1")
|
|
||||||
.replace(/\/page\.(tsx|jsx)/, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateRouteEntrypoint(get: "page" | "link") {
|
|
||||||
const files = (
|
|
||||||
await Promise.all(
|
|
||||||
Array.from(_sources).map(async (source) =>
|
|
||||||
traverseDirectory(`${source}/routes`, "page", { min: 1 })
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).flat()
|
|
||||||
|
|
||||||
const validatedRoutes = (
|
|
||||||
await Promise.all(
|
|
||||||
files.map(async (route) => {
|
|
||||||
const isValid = await validateRoute(route, get === "link")
|
|
||||||
return isValid ? route : null
|
|
||||||
})
|
|
||||||
)
|
|
||||||
).filter(Boolean) as string[]
|
|
||||||
|
|
||||||
if (!validatedRoutes.length) {
|
|
||||||
const code = `export default {
|
|
||||||
${get}s: [],
|
|
||||||
}`
|
|
||||||
|
|
||||||
return { module: generateModule(code), paths: [] }
|
|
||||||
}
|
|
||||||
|
|
||||||
const importString = validatedRoutes
|
|
||||||
.map((path, index) => {
|
|
||||||
return get === "page"
|
|
||||||
? `import RouteExt${index} from "${path}";`
|
|
||||||
: `import { config as routeConfig${index} } from "${path}";`
|
|
||||||
})
|
|
||||||
.join("\n")
|
|
||||||
|
|
||||||
const exportString = `export default {
|
|
||||||
${get}s: [${validatedRoutes
|
|
||||||
.map((file, index) => {
|
|
||||||
return get === "page"
|
|
||||||
? `{ path: "${createPath(file)}", file: "${file}" }`
|
|
||||||
: `{ path: "${createPath(file)}", ...routeConfig${index}.link }`
|
|
||||||
})
|
|
||||||
.join(", ")}],
|
|
||||||
}`
|
|
||||||
|
|
||||||
const code = `${importString}\n${exportString}`
|
|
||||||
|
|
||||||
return { module: generateModule(code), paths: validatedRoutes }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function validateSetting(file: string) {
|
|
||||||
const content = await fs.readFile(file, "utf-8")
|
|
||||||
|
|
||||||
const parserOptions: ParserOptions = {
|
|
||||||
sourceType: "module",
|
|
||||||
plugins: ["jsx"],
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.endsWith(".tsx")) {
|
|
||||||
parserOptions.plugins?.push("typescript")
|
|
||||||
}
|
|
||||||
|
|
||||||
let ast: ParseResult<File>
|
|
||||||
|
|
||||||
try {
|
|
||||||
ast = parse(content, parserOptions)
|
|
||||||
} catch (err) {
|
|
||||||
logger.error("An error occured while validating a setting.", {
|
|
||||||
error: err as Error,
|
|
||||||
timestamp: true,
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
let hasDefaultExport = false
|
|
||||||
let hasNamedExport = false
|
|
||||||
|
|
||||||
try {
|
|
||||||
traverse(ast, {
|
|
||||||
ExportDefaultDeclaration(path) {
|
|
||||||
hasDefaultExport = validateDefaultExport(path, ast)
|
|
||||||
},
|
|
||||||
ExportNamedDeclaration(path) {
|
|
||||||
hasNamedExport = validateSettingConfig(path)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
logger.error("An error occured while validating a setting.", {
|
|
||||||
error: err as Error,
|
|
||||||
timestamp: true,
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return hasDefaultExport && hasNamedExport
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateSettingConfig(path: NodePath<ExportNamedDeclaration>) {
|
|
||||||
const properties = getProperties(path)
|
|
||||||
|
|
||||||
if (!properties) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const cardProperty = properties.find(
|
|
||||||
(p) =>
|
|
||||||
p.type === "ObjectProperty" &&
|
|
||||||
p.key.type === "Identifier" &&
|
|
||||||
p.key.name === "card"
|
|
||||||
) as ObjectProperty | undefined
|
|
||||||
|
|
||||||
// Link property is required for settings
|
|
||||||
if (!cardProperty) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const cardValue = cardProperty.value as ObjectExpression
|
|
||||||
|
|
||||||
let hasLabel = false
|
|
||||||
let hasDescription = false
|
|
||||||
|
|
||||||
if (
|
|
||||||
cardValue.properties.some(
|
|
||||||
(p) =>
|
|
||||||
p.type === "ObjectProperty" &&
|
|
||||||
p.key.type === "Identifier" &&
|
|
||||||
p.key.name === "label" &&
|
|
||||||
p.value.type === "StringLiteral"
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
hasLabel = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
cardValue.properties.some(
|
|
||||||
(p) =>
|
|
||||||
p.type === "ObjectProperty" &&
|
|
||||||
p.key.type === "Identifier" &&
|
|
||||||
p.key.name === "description" &&
|
|
||||||
p.value.type === "StringLiteral"
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
hasDescription = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return hasLabel && hasDescription
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateSettingEntrypoint(get: "page" | "card") {
|
|
||||||
const files = (
|
|
||||||
await Promise.all(
|
|
||||||
Array.from(_sources).map(async (source) =>
|
|
||||||
traverseDirectory(`${source}/settings`, "page", { min: 1, max: 1 })
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).flat()
|
|
||||||
|
|
||||||
const validatedSettings = (
|
|
||||||
await Promise.all(
|
|
||||||
files.map(async (setting) => {
|
|
||||||
const isValid = await validateSetting(setting)
|
|
||||||
return isValid ? setting : null
|
|
||||||
})
|
|
||||||
)
|
|
||||||
).filter(Boolean) as string[]
|
|
||||||
|
|
||||||
if (!validatedSettings.length) {
|
|
||||||
const code = `export default {
|
|
||||||
${get}s: [],
|
|
||||||
}`
|
|
||||||
|
|
||||||
return { module: generateModule(code), paths: [] }
|
|
||||||
}
|
|
||||||
|
|
||||||
const importString = validatedSettings
|
|
||||||
.map((path, index) => {
|
|
||||||
return get === "page"
|
|
||||||
? `import SettingExt${index} from "${path}";`
|
|
||||||
: `import { config as settingConfig${index} } from "${path}";`
|
|
||||||
})
|
|
||||||
.join("\n")
|
|
||||||
|
|
||||||
const exportString = `export default {
|
|
||||||
${get}s: [${validatedSettings
|
|
||||||
.map((file, index) => {
|
|
||||||
return get === "page"
|
|
||||||
? `{ path: "${createPath(file)}", file: "${file}" }`
|
|
||||||
: `{ path: "${createPath(file)}", ...settingConfig${index}.card }`
|
|
||||||
})
|
|
||||||
.join(", ")}],
|
|
||||||
}`
|
|
||||||
|
|
||||||
const code = `${importString}\n${exportString}`
|
|
||||||
|
|
||||||
return { module: generateModule(code), paths: validatedSettings }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadModule(options: LoadModuleOptions) {
|
|
||||||
switch (options.type) {
|
|
||||||
case "widget": {
|
|
||||||
return await generateWidgetEntrypoint(options.get)
|
|
||||||
}
|
|
||||||
case "route": {
|
|
||||||
return await generateRouteEntrypoint(options.get)
|
|
||||||
}
|
|
||||||
case "setting": {
|
|
||||||
return await generateSettingEntrypoint(options.get)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getExtensionType(file: string) {
|
|
||||||
const normalizedPath = path.normalize(file)
|
|
||||||
|
|
||||||
if (normalizedPath.includes(path.normalize("/admin/widgets/"))) {
|
|
||||||
return "widget"
|
|
||||||
} else if (normalizedPath.includes(path.normalize("/admin/routes/"))) {
|
|
||||||
return "route"
|
|
||||||
} else if (normalizedPath.includes(path.normalize("/admin/settings/"))) {
|
|
||||||
return "setting"
|
|
||||||
} else {
|
|
||||||
return "none"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleWidgetChange(file: string) {
|
|
||||||
const { isValidWidget, zoneValue } = await validateWidget(file)
|
|
||||||
|
|
||||||
if (!isValidWidget || !zoneValue) {
|
|
||||||
_extensionGraph.delete(file)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const zoneValues = Array.isArray(zoneValue) ? zoneValue : [zoneValue]
|
|
||||||
|
|
||||||
for (const zone of zoneValues) {
|
|
||||||
const zonePath = zone.replace(/\./g, "/")
|
|
||||||
const moduleId = `${VIRTUAL_PREFIX}${WIDGET_MODULE}${zonePath}`
|
|
||||||
|
|
||||||
const module = server.moduleGraph.getModuleById(moduleId)
|
|
||||||
|
|
||||||
if (module) {
|
|
||||||
await server.reloadModule(module)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRouteChange(file: string) {
|
|
||||||
const isValidRoute = await validateRoute(file, false)
|
|
||||||
|
|
||||||
if (!isValidRoute) {
|
|
||||||
_extensionGraph.delete(file)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const moduleId of ROUTE_MODULES) {
|
|
||||||
const fullModuleId = `${VIRTUAL_PREFIX}${moduleId}`
|
|
||||||
const module = server.moduleGraph.getModuleById(fullModuleId)
|
|
||||||
|
|
||||||
if (module) {
|
|
||||||
await server.reloadModule(module)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSettingChange(file: string) {
|
|
||||||
const isValidSetting = await validateSetting(file)
|
|
||||||
|
|
||||||
if (!isValidSetting) {
|
|
||||||
_extensionGraph.delete(file)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const moduleId of SETTING_MODULE) {
|
|
||||||
const fullModuleId = `${VIRTUAL_PREFIX}${moduleId}`
|
|
||||||
const module = server.moduleGraph.getModuleById(fullModuleId)
|
|
||||||
|
|
||||||
if (module) {
|
|
||||||
await server.reloadModule(module)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleExtensionUnlink(file: string) {
|
|
||||||
const moduleIds = _extensionGraph.get(file)
|
|
||||||
|
|
||||||
if (!moduleIds) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const moduleId of moduleIds) {
|
|
||||||
const module = server.moduleGraph.getModuleById(moduleId)
|
|
||||||
|
|
||||||
if (module) {
|
|
||||||
_extensionGraph.delete(file)
|
|
||||||
await server.reloadModule(module)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadModuleAndUpdateGraph(
|
|
||||||
id: string,
|
|
||||||
options: LoadModuleOptions
|
|
||||||
) {
|
|
||||||
const { module, paths } = await loadModule(options)
|
|
||||||
|
|
||||||
for (const path of paths) {
|
|
||||||
const ids = _extensionGraph.get(path) || new Set<string>()
|
|
||||||
ids.add(id)
|
|
||||||
_extensionGraph.set(path, ids)
|
|
||||||
}
|
|
||||||
|
|
||||||
return module
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: "@medusajs/admin-vite-plugin",
|
|
||||||
configureServer(s) {
|
|
||||||
server = s
|
|
||||||
logger = s.config.logger
|
|
||||||
|
|
||||||
watcher = chokidar.watch(Array.from(_sources), {
|
|
||||||
persistent: true,
|
|
||||||
ignoreInitial: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
watcher.on("add", async (file) => {
|
|
||||||
const type = getExtensionType(file)
|
|
||||||
|
|
||||||
if (type === "none") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === "widget") {
|
|
||||||
await handleWidgetChange(file)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === "route") {
|
|
||||||
await handleRouteChange(file)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === "setting") {
|
|
||||||
await handleSettingChange(file)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
})
|
|
||||||
|
|
||||||
watcher.on("change", async (file) => {
|
|
||||||
const type = getExtensionType(file)
|
|
||||||
|
|
||||||
if (type === "none") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === "widget") {
|
|
||||||
await handleWidgetChange(file)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === "route") {
|
|
||||||
await handleRouteChange(file)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === "setting") {
|
|
||||||
await handleSettingChange(file)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
})
|
|
||||||
|
|
||||||
watcher.on("unlink", async (file) => {
|
|
||||||
await handleExtensionUnlink(file)
|
|
||||||
return
|
|
||||||
})
|
|
||||||
},
|
|
||||||
resolveId(id) {
|
|
||||||
if (MODULES.includes(id)) {
|
|
||||||
return VIRTUAL_PREFIX + id
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
async load(id: string) {
|
|
||||||
if (!id.startsWith(VIRTUAL_PREFIX)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const idNoPrefix = id.slice(VIRTUAL_PREFIX.length)
|
|
||||||
|
|
||||||
const moduleMap: Record<string, LoadModuleOptions> = {
|
|
||||||
[ROUTE_PAGE_MODULE]: { type: "route", get: "page" },
|
|
||||||
[ROUTE_LINK_MODULE]: { type: "route", get: "link" },
|
|
||||||
[SETTING_PAGE_MODULE]: { type: "setting", get: "page" },
|
|
||||||
[SETTING_CARD_MODULE]: { type: "setting", get: "card" },
|
|
||||||
}
|
|
||||||
|
|
||||||
if (WIDGET_MODULES.includes(idNoPrefix)) {
|
|
||||||
const zone = idNoPrefix
|
|
||||||
.replace(WIDGET_MODULE, "")
|
|
||||||
.replace(/\//g, ".") as InjectionZone
|
|
||||||
return loadModuleAndUpdateGraph(id, { type: "widget", get: zone })
|
|
||||||
}
|
|
||||||
|
|
||||||
const moduleOptions = moduleMap[idNoPrefix]
|
|
||||||
|
|
||||||
if (moduleOptions) {
|
|
||||||
return loadModuleAndUpdateGraph(id, moduleOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
async closeBundle() {
|
|
||||||
if (watcher) {
|
|
||||||
await watcher.close()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
794
packages/admin-next/admin-vite-plugin/src/plugin.ts
Normal file
794
packages/admin-next/admin-vite-plugin/src/plugin.ts
Normal file
@@ -0,0 +1,794 @@
|
|||||||
|
import {
|
||||||
|
InjectionZone,
|
||||||
|
RESOLVED_ROUTE_MODULES,
|
||||||
|
RESOLVED_WIDGET_MODULES,
|
||||||
|
VIRTUAL_MODULES,
|
||||||
|
getVirtualId,
|
||||||
|
getWidgetImport,
|
||||||
|
getWidgetZone,
|
||||||
|
isValidInjectionZone,
|
||||||
|
resolveVirtualId,
|
||||||
|
} from "@medusajs/admin-shared"
|
||||||
|
import { fdir } from "fdir"
|
||||||
|
import fs from "fs/promises"
|
||||||
|
import MagicString from "magic-string"
|
||||||
|
import path from "path"
|
||||||
|
import type * as Vite from "vite"
|
||||||
|
|
||||||
|
import {
|
||||||
|
ExportNamedDeclaration,
|
||||||
|
ObjectProperty,
|
||||||
|
parse,
|
||||||
|
traverse,
|
||||||
|
type ExportDefaultDeclaration,
|
||||||
|
type File,
|
||||||
|
type NodePath,
|
||||||
|
type ParseResult,
|
||||||
|
type ParserOptions,
|
||||||
|
} from "./babel"
|
||||||
|
|
||||||
|
const VALID_FILE_EXTENSIONS = [".tsx", ".jsx"]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the module type of a given file.
|
||||||
|
*/
|
||||||
|
function getModuleType(file: string) {
|
||||||
|
const normalizedPath = path.normalize(file)
|
||||||
|
|
||||||
|
if (normalizedPath.includes(path.normalize("/admin/widgets/"))) {
|
||||||
|
return "widget"
|
||||||
|
} else if (normalizedPath.includes(path.normalize("/admin/routes/"))) {
|
||||||
|
return "route"
|
||||||
|
} else {
|
||||||
|
return "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the parser options for a given file.
|
||||||
|
*/
|
||||||
|
function getParserOptions(file: string): ParserOptions {
|
||||||
|
const options: ParserOptions = {
|
||||||
|
sourceType: "module",
|
||||||
|
plugins: ["jsx"],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.endsWith(".tsx")) {
|
||||||
|
options.plugins?.push("typescript")
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a module with a source map from a code string
|
||||||
|
*/
|
||||||
|
function generateModule(code: string) {
|
||||||
|
const magicString = new MagicString(code)
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: magicString.toString(),
|
||||||
|
map: magicString.generateMap({ hires: true }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crawls a directory and returns all files that match the criteria.
|
||||||
|
*/
|
||||||
|
async function crawl(
|
||||||
|
dir: string,
|
||||||
|
file?: string,
|
||||||
|
depth?: { min: number; max?: number }
|
||||||
|
) {
|
||||||
|
const dirDepth = dir.split(path.sep).length
|
||||||
|
|
||||||
|
const crawler = new fdir()
|
||||||
|
.withBasePath()
|
||||||
|
.exclude((dirName) => dirName.startsWith("_"))
|
||||||
|
.filter((path) => {
|
||||||
|
return VALID_FILE_EXTENSIONS.some((ext) => path.endsWith(ext))
|
||||||
|
})
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
crawler.filter((path) => {
|
||||||
|
return VALID_FILE_EXTENSIONS.some((ext) => path.endsWith(file + ext))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depth) {
|
||||||
|
crawler.filter((file) => {
|
||||||
|
const pathDepth = file.split(path.sep).length - 1
|
||||||
|
|
||||||
|
if (depth.max && pathDepth > dirDepth + depth.max) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathDepth < dirDepth + depth.min) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return crawler.crawl(dir).withPromise()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts and returns the properties of a `config` object from a named export declaration.
|
||||||
|
*/
|
||||||
|
function getConfigObjectProperties(path: NodePath<ExportNamedDeclaration>) {
|
||||||
|
const declaration = path.node.declaration
|
||||||
|
|
||||||
|
if (declaration && declaration.type === "VariableDeclaration") {
|
||||||
|
const configDeclaration = declaration.declarations.find(
|
||||||
|
(d) =>
|
||||||
|
d.type === "VariableDeclarator" &&
|
||||||
|
d.id.type === "Identifier" &&
|
||||||
|
d.id.name === "config"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
configDeclaration &&
|
||||||
|
configDeclaration.init?.type === "CallExpression" &&
|
||||||
|
configDeclaration.init.arguments.length > 0 &&
|
||||||
|
configDeclaration.init.arguments[0].type === "ObjectExpression"
|
||||||
|
) {
|
||||||
|
return configDeclaration.init.arguments[0].properties
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates if the default export in a given AST is a component (JSX element or fragment).
|
||||||
|
*/
|
||||||
|
function isDefaultExportComponent(
|
||||||
|
path: NodePath<ExportDefaultDeclaration>,
|
||||||
|
ast: File
|
||||||
|
): boolean {
|
||||||
|
let hasComponentExport = false
|
||||||
|
const declaration = path.node.declaration
|
||||||
|
|
||||||
|
if (
|
||||||
|
declaration &&
|
||||||
|
(declaration.type === "Identifier" ||
|
||||||
|
declaration.type === "FunctionDeclaration")
|
||||||
|
) {
|
||||||
|
const exportName =
|
||||||
|
declaration.type === "Identifier"
|
||||||
|
? declaration.name
|
||||||
|
: declaration.id && declaration.id.name
|
||||||
|
|
||||||
|
if (exportName) {
|
||||||
|
try {
|
||||||
|
traverse(ast, {
|
||||||
|
VariableDeclarator({ node, scope }) {
|
||||||
|
let isDefaultExport = false
|
||||||
|
|
||||||
|
if (node.id.type === "Identifier" && node.id.name === exportName) {
|
||||||
|
isDefaultExport = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDefaultExport) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse(
|
||||||
|
node,
|
||||||
|
{
|
||||||
|
ReturnStatement(path) {
|
||||||
|
if (
|
||||||
|
path.node.argument?.type === "JSXElement" ||
|
||||||
|
path.node.argument?.type === "JSXFragment"
|
||||||
|
) {
|
||||||
|
hasComponentExport = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scope
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasComponentExport
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Widget utilities */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the widget configuration.
|
||||||
|
*/
|
||||||
|
function validateWidgetConfig(
|
||||||
|
path: NodePath<ExportNamedDeclaration>,
|
||||||
|
zone?: InjectionZone
|
||||||
|
): { zoneIsValid: boolean; zoneValue: string | string[] | null } {
|
||||||
|
let zoneIsValid = false
|
||||||
|
let zoneValue: string | string[] | null = null
|
||||||
|
|
||||||
|
const properties = getConfigObjectProperties(path)
|
||||||
|
|
||||||
|
if (!properties) {
|
||||||
|
return { zoneIsValid, zoneValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
const zoneProperty = properties.find(
|
||||||
|
(p) =>
|
||||||
|
p.type === "ObjectProperty" &&
|
||||||
|
p.key.type === "Identifier" &&
|
||||||
|
p.key.name === "zone"
|
||||||
|
) as ObjectProperty | undefined
|
||||||
|
|
||||||
|
if (!zoneProperty) {
|
||||||
|
return { zoneIsValid, zoneValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zoneProperty.value.type === "StringLiteral") {
|
||||||
|
zoneIsValid = !zone
|
||||||
|
? isValidInjectionZone(zoneProperty.value.value)
|
||||||
|
: zone === zoneProperty.value.value
|
||||||
|
zoneValue = zoneProperty.value.value
|
||||||
|
} else if (zoneProperty.value.type === "ArrayExpression") {
|
||||||
|
zoneIsValid = zoneProperty.value.elements.every((e) => {
|
||||||
|
if (!e || e.type !== "StringLiteral") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const isZoneMatch = !zone ? true : zone === e.value
|
||||||
|
|
||||||
|
return isValidInjectionZone(e.value) && isZoneMatch
|
||||||
|
})
|
||||||
|
|
||||||
|
const values: string[] = []
|
||||||
|
|
||||||
|
for (const element of zoneProperty.value.elements) {
|
||||||
|
if (element && element.type === "StringLiteral") {
|
||||||
|
values.push(element.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
zoneValue = values
|
||||||
|
}
|
||||||
|
|
||||||
|
return { zoneIsValid, zoneValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a widget file.
|
||||||
|
*/
|
||||||
|
async function validateWidget(
|
||||||
|
file: string,
|
||||||
|
zone?: InjectionZone
|
||||||
|
): Promise<
|
||||||
|
{ valid: true; zone: InjectionZone } | { valid: false; zone: null }
|
||||||
|
> {
|
||||||
|
let _zoneValue: string | string[] | null = null
|
||||||
|
|
||||||
|
const content = await fs.readFile(file, "utf-8")
|
||||||
|
const parserOptions = getParserOptions(file)
|
||||||
|
|
||||||
|
let ast: ParseResult<File>
|
||||||
|
|
||||||
|
try {
|
||||||
|
ast = parse(content, parserOptions)
|
||||||
|
} catch (e) {
|
||||||
|
return { valid: false, zone: _zoneValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasDefaultExport = false
|
||||||
|
let hasNamedExport = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
traverse(ast, {
|
||||||
|
ExportDefaultDeclaration(path) {
|
||||||
|
hasDefaultExport = isDefaultExportComponent(path, ast)
|
||||||
|
},
|
||||||
|
ExportNamedDeclaration(path) {
|
||||||
|
const { zoneIsValid, zoneValue } = validateWidgetConfig(path, zone)
|
||||||
|
|
||||||
|
hasNamedExport = zoneIsValid
|
||||||
|
_zoneValue = zoneValue
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
return { valid: false, zone: _zoneValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: hasNamedExport && hasDefaultExport, zone: _zoneValue as any }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateWidgetEntrypoint(
|
||||||
|
sources: Set<string>,
|
||||||
|
zone: InjectionZone
|
||||||
|
) {
|
||||||
|
const files = (
|
||||||
|
await Promise.all(
|
||||||
|
Array.from(sources).map(async (source) => crawl(`${source}/widgets`))
|
||||||
|
)
|
||||||
|
).flat()
|
||||||
|
|
||||||
|
const validatedWidgets = (
|
||||||
|
await Promise.all(
|
||||||
|
files.map(async (widget) => {
|
||||||
|
const { valid } = await validateWidget(widget, zone)
|
||||||
|
return valid ? widget : null
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).filter(Boolean) as string[]
|
||||||
|
|
||||||
|
if (!validatedWidgets.length) {
|
||||||
|
const code = `export default {
|
||||||
|
widgets: [],
|
||||||
|
}`
|
||||||
|
|
||||||
|
return { module: generateModule(code), paths: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const importString = validatedWidgets
|
||||||
|
.map((path, index) => `import WidgetExt${index} from "${path}";`)
|
||||||
|
.join("\n")
|
||||||
|
|
||||||
|
const exportString = `export default {
|
||||||
|
widgets: [${validatedWidgets
|
||||||
|
.map((_, index) => `{ Component: WidgetExt${index} }`)
|
||||||
|
.join(", ")}],
|
||||||
|
}`
|
||||||
|
|
||||||
|
const code = `${importString}\n${exportString}`
|
||||||
|
|
||||||
|
return { module: generateModule(code), paths: validatedWidgets }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Route utilities */
|
||||||
|
|
||||||
|
function validateRouteConfig(
|
||||||
|
path: NodePath<ExportNamedDeclaration>,
|
||||||
|
resolveMenuItem: boolean
|
||||||
|
) {
|
||||||
|
const properties = getConfigObjectProperties(path)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When resolving links for the sidebar, we a config to get the props needed to
|
||||||
|
* render the link correctly.
|
||||||
|
*
|
||||||
|
* If the user has not provided any config, then the route can never be a valid
|
||||||
|
* menu item, so we can skip the validation, and return false.
|
||||||
|
*/
|
||||||
|
if (!properties && resolveMenuItem) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A config is not required for a component to be a valid route.
|
||||||
|
*/
|
||||||
|
if (!properties) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelProperty = properties.find(
|
||||||
|
(p) =>
|
||||||
|
p.type === "ObjectProperty" &&
|
||||||
|
p.key.type === "Identifier" &&
|
||||||
|
p.key.name === "label"
|
||||||
|
) as ObjectProperty | undefined
|
||||||
|
|
||||||
|
const labelIsValid =
|
||||||
|
!labelProperty || labelProperty.value.type === "StringLiteral"
|
||||||
|
|
||||||
|
return labelIsValid
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateRoute(file: string, resolveMenuItem = false) {
|
||||||
|
const content = await fs.readFile(file, "utf-8")
|
||||||
|
const parserOptions = getParserOptions(file)
|
||||||
|
|
||||||
|
let ast: ParseResult<File>
|
||||||
|
|
||||||
|
try {
|
||||||
|
ast = parse(content, parserOptions)
|
||||||
|
} catch (_e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasDefaultExport = false
|
||||||
|
let hasNamedExport = resolveMenuItem ? false : true
|
||||||
|
|
||||||
|
try {
|
||||||
|
traverse(ast, {
|
||||||
|
ExportDefaultDeclaration(path) {
|
||||||
|
hasDefaultExport = isDefaultExportComponent(path, ast)
|
||||||
|
},
|
||||||
|
ExportNamedDeclaration(path) {
|
||||||
|
hasNamedExport = validateRouteConfig(path, resolveMenuItem)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (_e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasNamedExport && hasDefaultExport
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRoutePath(file: string) {
|
||||||
|
return file
|
||||||
|
.replace(/.*\/admin\/(routes|settings)/, "")
|
||||||
|
.replace(/\[([^\]]+)\]/g, ":$1")
|
||||||
|
.replace(/\/page\.(tsx|jsx)/, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateRouteEntrypoint(
|
||||||
|
sources: Set<string>,
|
||||||
|
type: "page" | "link",
|
||||||
|
base = ""
|
||||||
|
) {
|
||||||
|
const files = (
|
||||||
|
await Promise.all(
|
||||||
|
Array.from(sources).map(async (source) =>
|
||||||
|
crawl(`${source}/routes`, "page", { min: 1 })
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).flat()
|
||||||
|
|
||||||
|
const validatedRoutes = (
|
||||||
|
await Promise.all(
|
||||||
|
files.map(async (route) => {
|
||||||
|
const valid = await validateRoute(route, type === "link")
|
||||||
|
return valid ? route : null
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).filter(Boolean) as string[]
|
||||||
|
|
||||||
|
if (!validatedRoutes.length) {
|
||||||
|
const code = `export default {
|
||||||
|
${type}s: [],
|
||||||
|
}`
|
||||||
|
|
||||||
|
return { module: generateModule(code), paths: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const importString = validatedRoutes
|
||||||
|
.map((path, index) => {
|
||||||
|
return type === "page"
|
||||||
|
? `import RouteExt${index} from "${path}";`
|
||||||
|
: `import { config as routeConfig${index} } from "${path}";`
|
||||||
|
})
|
||||||
|
.join("\n")
|
||||||
|
|
||||||
|
const exportString = `export default {
|
||||||
|
${type}s: [${validatedRoutes
|
||||||
|
.map((file, index) => {
|
||||||
|
return type === "page"
|
||||||
|
? `{ path: "${createRoutePath(file)}", file: "${base + file}" }`
|
||||||
|
: `{ path: "${createRoutePath(file)}", ...routeConfig${index} }`
|
||||||
|
})
|
||||||
|
.join(", ")}],
|
||||||
|
}`
|
||||||
|
|
||||||
|
const code = `${importString}\n${exportString}`
|
||||||
|
|
||||||
|
return { module: generateModule(code), paths: validatedRoutes }
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoadModuleOptions =
|
||||||
|
| {
|
||||||
|
type: "widget"
|
||||||
|
get: InjectionZone
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "route"
|
||||||
|
get: "page" | "link"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MedusaVitePluginOptions = {
|
||||||
|
/**
|
||||||
|
* A list of directories to source extensions from.
|
||||||
|
*/
|
||||||
|
sources?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MedusaVitePlugin = (config?: MedusaVitePluginOptions) => Vite.Plugin
|
||||||
|
export const medusaVitePlugin: MedusaVitePlugin = (options) => {
|
||||||
|
const _extensionGraph = new Map<string, Set<string>>()
|
||||||
|
const _sources = new Set<string>(options?.sources ?? [])
|
||||||
|
let _base = ""
|
||||||
|
|
||||||
|
let server: Vite.ViteDevServer | undefined
|
||||||
|
let watcher: Vite.FSWatcher | undefined
|
||||||
|
|
||||||
|
async function loadModule(options: LoadModuleOptions) {
|
||||||
|
switch (options.type) {
|
||||||
|
case "widget": {
|
||||||
|
return await generateWidgetEntrypoint(_sources, options.get)
|
||||||
|
}
|
||||||
|
case "route":
|
||||||
|
return await generateRouteEntrypoint(_sources, options.get, _base)
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function register(id: string, options: LoadModuleOptions) {
|
||||||
|
const result = await loadModule(options)
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { module, paths } = result
|
||||||
|
|
||||||
|
for (const path of paths) {
|
||||||
|
const ids = _extensionGraph.get(path) || new Set<string>()
|
||||||
|
ids.add(id)
|
||||||
|
_extensionGraph.set(path, ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
return module
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleWidgetChange(file: string, event: "add" | "change") {
|
||||||
|
const { valid, zone } = await validateWidget(file)
|
||||||
|
const zoneValues = Array.isArray(zone) ? zone : [zone]
|
||||||
|
|
||||||
|
if (event === "change") {
|
||||||
|
/**
|
||||||
|
* If the file is in the extension graph, and it has become
|
||||||
|
* invalid, we need to remove it from the graph and reload all modules
|
||||||
|
* that import the widget.
|
||||||
|
*/
|
||||||
|
if (!valid) {
|
||||||
|
const extensionIds = _extensionGraph.get(file)
|
||||||
|
_extensionGraph.delete(file)
|
||||||
|
|
||||||
|
if (!extensionIds) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const moduleId of extensionIds) {
|
||||||
|
const module = server?.moduleGraph.getModuleById(moduleId)
|
||||||
|
|
||||||
|
if (module) {
|
||||||
|
await server?.reloadModule(module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the file is not in the extension graph, we need to add it.
|
||||||
|
* We also need to reload all modules that import the widget.
|
||||||
|
*/
|
||||||
|
if (!_extensionGraph.has(file)) {
|
||||||
|
const imports = new Set<string>()
|
||||||
|
|
||||||
|
for (const zoneValue of zoneValues) {
|
||||||
|
const zonePath = getWidgetImport(zoneValue)
|
||||||
|
const moduleId = getVirtualId(zonePath)
|
||||||
|
const resolvedModuleId = resolveVirtualId(moduleId)
|
||||||
|
const module = server?.moduleGraph.getModuleById(resolvedModuleId)
|
||||||
|
if (module) {
|
||||||
|
imports.add(resolvedModuleId)
|
||||||
|
await server?.reloadModule(module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_extensionGraph.set(file, imports)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event === "add") {
|
||||||
|
/**
|
||||||
|
* If a new file is added in /admin/widgets, but it is not valid,
|
||||||
|
* we don't need to do anything.
|
||||||
|
*/
|
||||||
|
if (!valid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a new file is added in /admin/widgets, and it is valid, we need to
|
||||||
|
* add it to the extension graph and reload all modules that need to import
|
||||||
|
* the widget so that they can be updated with the new widget.
|
||||||
|
*/
|
||||||
|
const imports = new Set<string>()
|
||||||
|
|
||||||
|
for (const zoneValue of zoneValues) {
|
||||||
|
const zonePath = getWidgetImport(zoneValue)
|
||||||
|
const moduleId = getVirtualId(zonePath)
|
||||||
|
const resolvedModuleId = resolveVirtualId(moduleId)
|
||||||
|
|
||||||
|
const module = server?.moduleGraph.getModuleById(resolvedModuleId)
|
||||||
|
|
||||||
|
if (module) {
|
||||||
|
imports.add(resolvedModuleId)
|
||||||
|
await server?.reloadModule(module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_extensionGraph.set(file, imports)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRouteChange(file: string, event: "add" | "change") {
|
||||||
|
const valid = await validateRoute(file)
|
||||||
|
|
||||||
|
if (event === "change") {
|
||||||
|
/**
|
||||||
|
* If the file is in the extension graph, and it has become
|
||||||
|
* invalid, we need to remove it from the graph and reload all modules
|
||||||
|
* that import the route.
|
||||||
|
*/
|
||||||
|
if (!valid) {
|
||||||
|
const extensionIds = _extensionGraph.get(file)
|
||||||
|
_extensionGraph.delete(file)
|
||||||
|
|
||||||
|
if (!extensionIds) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const moduleId of extensionIds) {
|
||||||
|
const module = server?.moduleGraph.getModuleById(moduleId)
|
||||||
|
|
||||||
|
if (module) {
|
||||||
|
await server?.reloadModule(module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the file is not in the extension graph, we need to add it.
|
||||||
|
* We also need to reload all modules that import the route.
|
||||||
|
*/
|
||||||
|
if (!_extensionGraph.has(file)) {
|
||||||
|
const moduleId = getVirtualId(file)
|
||||||
|
const resolvedModuleId = resolveVirtualId(moduleId)
|
||||||
|
const module = server?.moduleGraph.getModuleById(resolvedModuleId)
|
||||||
|
if (module) {
|
||||||
|
await server?.reloadModule(module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_extensionGraph.has(file)) {
|
||||||
|
const modules = _extensionGraph.get(file)
|
||||||
|
|
||||||
|
if (!modules) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const moduleId of modules) {
|
||||||
|
const module = server?.moduleGraph.getModuleById(moduleId)
|
||||||
|
|
||||||
|
if (module) {
|
||||||
|
await server?.reloadModule(module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event === "add") {
|
||||||
|
/**
|
||||||
|
* If a new file is added in /admin/routes, but it is not valid,
|
||||||
|
* we don't need to do anything.
|
||||||
|
*/
|
||||||
|
if (!valid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const imports = new Set<string>()
|
||||||
|
|
||||||
|
for (const resolvedModuleId of RESOLVED_ROUTE_MODULES) {
|
||||||
|
const module = server?.moduleGraph.getModuleById(resolvedModuleId)
|
||||||
|
if (module) {
|
||||||
|
imports.add(resolvedModuleId)
|
||||||
|
await server?.reloadModule(module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_extensionGraph.set(file, imports)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAddOrChange(path: string, event: "add" | "change") {
|
||||||
|
const type = getModuleType(path)
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "widget":
|
||||||
|
await handleWidgetChange(path, event)
|
||||||
|
break
|
||||||
|
case "route":
|
||||||
|
await handleRouteChange(path, event)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
// In all other cases we don't need to do anything.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUnlink(path: string) {
|
||||||
|
const moduleIds = _extensionGraph.get(path)
|
||||||
|
_extensionGraph.delete(path)
|
||||||
|
|
||||||
|
if (!moduleIds) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const moduleId of moduleIds) {
|
||||||
|
const module = server?.moduleGraph.getModuleById(moduleId)
|
||||||
|
|
||||||
|
if (module) {
|
||||||
|
await server?.reloadModule(module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "@medusajs/admin-vite-plugin",
|
||||||
|
enforce: "pre",
|
||||||
|
configResolved(config) {
|
||||||
|
if (config.server?.middlewareMode) {
|
||||||
|
/**
|
||||||
|
* If we are in middleware mode, we need to set the base to the <base> + "@fs".
|
||||||
|
*
|
||||||
|
* This ensures that the page components are lazy-loaded correctly.
|
||||||
|
*/
|
||||||
|
_base = `${config.base}@fs`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
configureServer(_server) {
|
||||||
|
server = _server
|
||||||
|
watcher = _server.watcher
|
||||||
|
|
||||||
|
_sources.forEach((source) => {
|
||||||
|
watcher?.add(source)
|
||||||
|
})
|
||||||
|
|
||||||
|
watcher.on("all", async (event, path) => {
|
||||||
|
switch (event) {
|
||||||
|
case "add":
|
||||||
|
case "change": {
|
||||||
|
await handleAddOrChange(path, event)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "unlinkDir":
|
||||||
|
case "unlink":
|
||||||
|
await handleUnlink(path)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
resolveId(id) {
|
||||||
|
if (VIRTUAL_MODULES.includes(id)) {
|
||||||
|
return resolveVirtualId(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
async load(id) {
|
||||||
|
if (RESOLVED_WIDGET_MODULES.includes(id)) {
|
||||||
|
const zone = getWidgetZone(id)
|
||||||
|
|
||||||
|
return register(id, { type: "widget", get: zone })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RESOLVED_ROUTE_MODULES.includes(id)) {
|
||||||
|
const type = id.includes("link") ? "link" : "page"
|
||||||
|
return register(id, { type: "route", get: type })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async closeBundle() {
|
||||||
|
if (watcher) {
|
||||||
|
await watcher.close()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
8
packages/admin-next/admin-vite-plugin/tsup.config.cjs
Normal file
8
packages/admin-next/admin-vite-plugin/tsup.config.cjs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from "tsup"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ["./src/index.ts"],
|
||||||
|
format: ["cjs", "esm"],
|
||||||
|
dts: true,
|
||||||
|
clean: true,
|
||||||
|
})
|
||||||
@@ -61,6 +61,7 @@
|
|||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@medusajs/admin-shared": "^0.0.1",
|
||||||
"@medusajs/admin-vite-plugin": "0.0.1",
|
"@medusajs/admin-vite-plugin": "0.0.1",
|
||||||
"@medusajs/types": "1.11.16",
|
"@medusajs/types": "1.11.16",
|
||||||
"@medusajs/ui-preset": "1.1.3",
|
"@medusajs/ui-preset": "1.1.3",
|
||||||
|
|||||||
@@ -13,12 +13,15 @@ import { Avatar, Text } from "@medusajs/ui"
|
|||||||
import * as Collapsible from "@radix-ui/react-collapsible"
|
import * as Collapsible from "@radix-ui/react-collapsible"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
import { ComponentType } from "react"
|
|
||||||
import { useStore } from "../../../hooks/api/store"
|
import { useStore } from "../../../hooks/api/store"
|
||||||
import { Skeleton } from "../../common/skeleton"
|
import { Skeleton } from "../../common/skeleton"
|
||||||
import { NavItem, NavItemProps } from "../../layout/nav-item"
|
import { NavItem, NavItemProps } from "../../layout/nav-item"
|
||||||
import { Shell } from "../../layout/shell"
|
import { Shell } from "../../layout/shell"
|
||||||
|
|
||||||
|
import routes from "virtual:medusa/routes/links"
|
||||||
|
import { settingsRouteRegex } from "../../../lib/extension-helpers"
|
||||||
|
import { Divider } from "../../common/divider"
|
||||||
|
|
||||||
export const MainLayout = () => {
|
export const MainLayout = () => {
|
||||||
return (
|
return (
|
||||||
<Shell>
|
<Shell>
|
||||||
@@ -34,7 +37,7 @@ const MainSidebar = () => {
|
|||||||
<div className="bg-ui-bg-subtle sticky top-0">
|
<div className="bg-ui-bg-subtle sticky top-0">
|
||||||
<Header />
|
<Header />
|
||||||
<div className="px-3">
|
<div className="px-3">
|
||||||
<div className="border-ui-border-strong h-px w-full border-b border-dashed" />
|
<Divider variant="dashed" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CoreRouteSection />
|
<CoreRouteSection />
|
||||||
@@ -157,7 +160,7 @@ const CoreRouteSection = () => {
|
|||||||
const coreRoutes = useCoreRoutes()
|
const coreRoutes = useCoreRoutes()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="flex flex-col gap-y-1 py-2">
|
<nav className="flex flex-col gap-y-1 py-3">
|
||||||
{coreRoutes.map((route) => {
|
{coreRoutes.map((route) => {
|
||||||
return <NavItem key={route.to} {...route} />
|
return <NavItem key={route.to} {...route} />
|
||||||
})}
|
})}
|
||||||
@@ -165,27 +168,31 @@ const CoreRouteSection = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const extensions = {
|
|
||||||
links: null as { path: string; label: string; icon?: ComponentType }[] | null,
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExtensionRouteSection = () => {
|
const ExtensionRouteSection = () => {
|
||||||
if (!extensions.links || extensions.links.length === 0) {
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const links = routes.links
|
||||||
|
|
||||||
|
const extensionLinks = links.filter(
|
||||||
|
(link) => !settingsRouteRegex.test(link.path)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!extensionLinks.length) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="px-3">
|
<div className="px-3">
|
||||||
<div className="border-ui-border-strong h-px w-full border-b border-dashed" />
|
<Divider variant="dashed" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-y-1 py-2">
|
<div className="flex flex-col gap-y-1 py-3">
|
||||||
<Collapsible.Root defaultOpen>
|
<Collapsible.Root defaultOpen>
|
||||||
<div className="px-4">
|
<div className="px-4">
|
||||||
<Collapsible.Trigger asChild className="group/trigger">
|
<Collapsible.Trigger asChild className="group/trigger">
|
||||||
<button className="text-ui-fg-subtle flex w-full items-center justify-between px-2">
|
<button className="text-ui-fg-subtle flex w-full items-center justify-between px-2">
|
||||||
<Text size="xsmall" weight="plus" leading="compact">
|
<Text size="xsmall" weight="plus" leading="compact">
|
||||||
Extensions
|
{t("nav.extensions")}
|
||||||
</Text>
|
</Text>
|
||||||
<div className="text-ui-fg-muted">
|
<div className="text-ui-fg-muted">
|
||||||
<ChevronDownMini className="group-data-[state=open]/trigger:hidden" />
|
<ChevronDownMini className="group-data-[state=open]/trigger:hidden" />
|
||||||
@@ -196,7 +203,7 @@ const ExtensionRouteSection = () => {
|
|||||||
</div>
|
</div>
|
||||||
<Collapsible.Content>
|
<Collapsible.Content>
|
||||||
<div className="flex flex-col gap-y-1 py-1 pb-4">
|
<div className="flex flex-col gap-y-1 py-1 pb-4">
|
||||||
{extensions.links.map((link) => {
|
{extensionLinks.map((link) => {
|
||||||
return (
|
return (
|
||||||
<NavItem
|
<NavItem
|
||||||
key={link.path}
|
key={link.path}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Text, clx } from "@medusajs/ui"
|
import { Text, clx } from "@medusajs/ui"
|
||||||
import * as Collapsible from "@radix-ui/react-collapsible"
|
import * as Collapsible from "@radix-ui/react-collapsible"
|
||||||
import { useEffect, useState } from "react"
|
import { ReactNode, useEffect, useState } from "react"
|
||||||
import { Link, useLocation } from "react-router-dom"
|
import { Link, useLocation } from "react-router-dom"
|
||||||
|
|
||||||
type ItemType = "core" | "extension"
|
type ItemType = "core" | "extension"
|
||||||
@@ -11,7 +11,7 @@ type NestedItemProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type NavItemProps = {
|
export type NavItemProps = {
|
||||||
icon?: React.ReactNode
|
icon?: ReactNode
|
||||||
label: string
|
label: string
|
||||||
to: string
|
to: string
|
||||||
items?: NestedItemProps[]
|
items?: NestedItemProps[]
|
||||||
@@ -55,7 +55,7 @@ export const NavItem = ({
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
className={clx(
|
className={clx(
|
||||||
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover flex items-center gap-x-2 rounded-md px-2 py-2.5 outline-none md:py-1.5",
|
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover flex items-center gap-x-2 rounded-md px-2 py-1 outline-none",
|
||||||
{
|
{
|
||||||
"bg-ui-bg-base hover:bg-ui-bg-base-hover shadow-elevation-card-rest":
|
"bg-ui-bg-base hover:bg-ui-bg-base-hover shadow-elevation-card-rest":
|
||||||
location.pathname === to ||
|
location.pathname === to ||
|
||||||
@@ -82,7 +82,7 @@ export const NavItem = ({
|
|||||||
</Text>
|
</Text>
|
||||||
</Collapsible.Trigger>
|
</Collapsible.Trigger>
|
||||||
<Collapsible.Content className="flex flex-col pt-1">
|
<Collapsible.Content className="flex flex-col pt-1">
|
||||||
<div className="flex h-[36px] w-full items-center gap-x-1 pl-2 md:hidden">
|
<div className="flex w-full items-center gap-x-1 pl-2 md:hidden">
|
||||||
<div
|
<div
|
||||||
role="presentation"
|
role="presentation"
|
||||||
className="flex h-full w-5 items-center justify-center"
|
className="flex h-full w-5 items-center justify-center"
|
||||||
@@ -92,7 +92,7 @@ export const NavItem = ({
|
|||||||
<Link
|
<Link
|
||||||
to={to}
|
to={to}
|
||||||
className={clx(
|
className={clx(
|
||||||
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover mb-2 mt-1 flex h-8 flex-1 items-center gap-x-2 rounded-md px-2 py-2.5 outline-none md:py-1.5",
|
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover mb-2 mt-1 flex flex-1 items-center gap-x-2 rounded-md px-2 py-1 outline-none",
|
||||||
{
|
{
|
||||||
"bg-ui-bg-base hover:bg-ui-bg-base text-ui-fg-base shadow-elevation-card-rest":
|
"bg-ui-bg-base hover:bg-ui-bg-base text-ui-fg-base shadow-elevation-card-rest":
|
||||||
location.pathname.startsWith(to),
|
location.pathname.startsWith(to),
|
||||||
@@ -109,7 +109,7 @@ export const NavItem = ({
|
|||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={item.to}
|
key={item.to}
|
||||||
className="flex h-[36px] items-center gap-x-1 pl-2"
|
className="flex h-[32px] items-center gap-x-1 pl-2"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="presentation"
|
role="presentation"
|
||||||
@@ -120,7 +120,7 @@ export const NavItem = ({
|
|||||||
<Link
|
<Link
|
||||||
to={item.to}
|
to={item.to}
|
||||||
className={clx(
|
className={clx(
|
||||||
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover flex h-8 flex-1 items-center gap-x-2 rounded-md px-2 py-2.5 outline-none first-of-type:mt-1 last-of-type:mb-2 md:py-1.5",
|
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover flex flex-1 items-center gap-x-2 rounded-md px-2 py-1 outline-none first-of-type:mt-1 last-of-type:mb-2",
|
||||||
{
|
{
|
||||||
"bg-ui-bg-base text-ui-fg-base hover:bg-ui-bg-base shadow-elevation-card-rest":
|
"bg-ui-bg-base text-ui-fg-base hover:bg-ui-bg-base shadow-elevation-card-rest":
|
||||||
location.pathname.startsWith(item.to),
|
location.pathname.startsWith(item.to),
|
||||||
@@ -142,7 +142,7 @@ export const NavItem = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Icon = ({ icon, type }: { icon?: React.ReactNode; type: ItemType }) => {
|
const Icon = ({ icon, type }: { icon?: ReactNode; type: ItemType }) => {
|
||||||
if (!icon) {
|
if (!icon) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { ArrowUturnLeft, MinusMini } from "@medusajs/icons"
|
import { ArrowUturnLeft, MinusMini } from "@medusajs/icons"
|
||||||
import { IconButton, Text } from "@medusajs/ui"
|
import { IconButton, Text } from "@medusajs/ui"
|
||||||
import * as Collapsible from "@radix-ui/react-collapsible"
|
import * as Collapsible from "@radix-ui/react-collapsible"
|
||||||
import { useEffect, useMemo, useState } from "react"
|
import { Fragment, useEffect, useMemo, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link, useLocation } from "react-router-dom"
|
import { Link, useLocation } from "react-router-dom"
|
||||||
|
|
||||||
|
import { settingsRouteRegex } from "../../../lib/extension-helpers"
|
||||||
|
import { Divider } from "../../common/divider"
|
||||||
import { NavItem, NavItemProps } from "../nav-item"
|
import { NavItem, NavItemProps } from "../nav-item"
|
||||||
import { Shell } from "../shell"
|
import { Shell } from "../shell"
|
||||||
|
|
||||||
|
import routes from "virtual:medusa/routes/links"
|
||||||
|
|
||||||
export const SettingsLayout = () => {
|
export const SettingsLayout = () => {
|
||||||
return (
|
return (
|
||||||
<Shell>
|
<Shell>
|
||||||
@@ -80,6 +84,21 @@ const useDeveloperRoutes = (): NavItemProps[] => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useExtensionRoutes = (): NavItemProps[] => {
|
||||||
|
const links = routes.links
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const settingsLinks = links.filter((link) =>
|
||||||
|
settingsRouteRegex.test(link.path)
|
||||||
|
)
|
||||||
|
|
||||||
|
return settingsLinks.map((link) => ({
|
||||||
|
label: link.label,
|
||||||
|
to: link.path,
|
||||||
|
}))
|
||||||
|
}, [links])
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure that the `from` prop is not another settings route, to avoid
|
* Ensure that the `from` prop is not another settings route, to avoid
|
||||||
* the user getting stuck in a navigation loop.
|
* the user getting stuck in a navigation loop.
|
||||||
@@ -95,6 +114,8 @@ const getSafeFromValue = (from: string) => {
|
|||||||
const SettingsSidebar = () => {
|
const SettingsSidebar = () => {
|
||||||
const routes = useSettingRoutes()
|
const routes = useSettingRoutes()
|
||||||
const developerRoutes = useDeveloperRoutes()
|
const developerRoutes = useDeveloperRoutes()
|
||||||
|
const extensionRoutes = useExtensionRoutes()
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
@@ -108,20 +129,24 @@ const SettingsSidebar = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="flex flex-1 flex-col justify-between overflow-y-auto">
|
<aside className="flex flex-1 flex-col justify-between overflow-y-auto">
|
||||||
<div className="px-3 py-2">
|
<div className="p-3">
|
||||||
<div className="flex items-center gap-x-3 p-1">
|
<div className="flex items-center gap-x-3 px-2 py-1.5">
|
||||||
<Link to={from} replace className="flex items-center justify-center">
|
<IconButton size="2xsmall" variant="transparent" asChild>
|
||||||
<IconButton size="small" variant="transparent">
|
<Link
|
||||||
|
to={from}
|
||||||
|
replace
|
||||||
|
className="flex items-center justify-center"
|
||||||
|
>
|
||||||
<ArrowUturnLeft />
|
<ArrowUturnLeft />
|
||||||
</IconButton>
|
</Link>
|
||||||
</Link>
|
</IconButton>
|
||||||
<Text leading="compact" weight="plus" size="small">
|
<Text leading="compact" weight="plus" size="small">
|
||||||
{t("nav.settings")}
|
{t("nav.settings")}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-3">
|
<div className="flex items-center justify-center px-3">
|
||||||
<div className="border-ui-border-strong h-px w-full border-b border-dashed" />
|
<Divider variant="dashed" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-1 flex-col overflow-y-auto">
|
<div className="flex flex-1 flex-col overflow-y-auto">
|
||||||
<Collapsible.Root defaultOpen className="py-3">
|
<Collapsible.Root defaultOpen className="py-3">
|
||||||
@@ -147,6 +172,9 @@ const SettingsSidebar = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Collapsible.Content>
|
</Collapsible.Content>
|
||||||
</Collapsible.Root>
|
</Collapsible.Root>
|
||||||
|
<div className="flex items-center justify-center px-3">
|
||||||
|
<Divider variant="dashed" />
|
||||||
|
</div>
|
||||||
<Collapsible.Root defaultOpen className="py-3">
|
<Collapsible.Root defaultOpen className="py-3">
|
||||||
<div className="px-3">
|
<div className="px-3">
|
||||||
<div className="text-ui-fg-muted flex h-7 items-center justify-between px-2">
|
<div className="text-ui-fg-muted flex h-7 items-center justify-between px-2">
|
||||||
@@ -170,6 +198,36 @@ const SettingsSidebar = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Collapsible.Content>
|
</Collapsible.Content>
|
||||||
</Collapsible.Root>
|
</Collapsible.Root>
|
||||||
|
{extensionRoutes.length > 0 && (
|
||||||
|
<Fragment>
|
||||||
|
<div className="flex items-center justify-center px-3">
|
||||||
|
<Divider variant="dashed" />
|
||||||
|
</div>
|
||||||
|
<Collapsible.Root defaultOpen className="py-3">
|
||||||
|
<div className="px-3">
|
||||||
|
<div className="text-ui-fg-muted flex h-7 items-center justify-between px-2">
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
{t("nav.extensions")}
|
||||||
|
</Text>
|
||||||
|
<Collapsible.Trigger asChild>
|
||||||
|
<IconButton size="2xsmall" variant="transparent">
|
||||||
|
<MinusMini className="text-ui-fg-muted" />
|
||||||
|
</IconButton>
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Collapsible.Content>
|
||||||
|
<div className="pt-0.5">
|
||||||
|
<nav className="flex flex-col gap-y-1">
|
||||||
|
{extensionRoutes.map((setting) => (
|
||||||
|
<NavItem key={setting.to} {...setting} />
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
)
|
)
|
||||||
|
|||||||
56
packages/admin-next/dashboard/src/lib/extension-helpers.ts
Normal file
56
packages/admin-next/dashboard/src/lib/extension-helpers.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { RouteObject } from "react-router-dom"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to test if a route is a settings route.
|
||||||
|
*/
|
||||||
|
export const settingsRouteRegex = /^\/settings\//
|
||||||
|
|
||||||
|
export const createRouteMap = (
|
||||||
|
routes: { path: string; file: string }[],
|
||||||
|
ignore?: string
|
||||||
|
): RouteObject[] => {
|
||||||
|
const root: RouteObject[] = []
|
||||||
|
|
||||||
|
const addRoute = (
|
||||||
|
pathSegments: string[],
|
||||||
|
file: string,
|
||||||
|
currentLevel: RouteObject[]
|
||||||
|
) => {
|
||||||
|
if (!pathSegments.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const [currentSegment, ...remainingSegments] = pathSegments
|
||||||
|
let route = currentLevel.find((r) => r.path === currentSegment)
|
||||||
|
|
||||||
|
if (!route) {
|
||||||
|
route = { path: currentSegment, children: [] }
|
||||||
|
currentLevel.push(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingSegments.length === 0) {
|
||||||
|
route.children ||= []
|
||||||
|
route.children.push({
|
||||||
|
path: "",
|
||||||
|
async lazy() {
|
||||||
|
const { default: Component } = await import(/* @vite-ignore */ file)
|
||||||
|
return { Component }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
route.children ||= []
|
||||||
|
addRoute(remainingSegments, file, route.children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
routes.forEach(({ path, file }) => {
|
||||||
|
// Remove the ignore segment from the path if it is provided
|
||||||
|
const cleanedPath = ignore
|
||||||
|
? path.replace(ignore, "").replace(/^\/+/, "")
|
||||||
|
: path.replace(/^\/+/, "")
|
||||||
|
const pathSegments = cleanedPath.split("/").filter(Boolean)
|
||||||
|
addRoute(pathSegments, file, root)
|
||||||
|
})
|
||||||
|
|
||||||
|
return root
|
||||||
|
}
|
||||||
40
packages/admin-next/dashboard/src/module.d.ts
vendored
40
packages/admin-next/dashboard/src/module.d.ts
vendored
@@ -1,4 +1,4 @@
|
|||||||
declare module "medusa-admin:widgets/*" {
|
declare module "virtual:medusa/widgets/*" {
|
||||||
const widgets: { Component: () => JSX.Element }[]
|
const widgets: { Component: () => JSX.Element }[]
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -6,34 +6,20 @@ declare module "medusa-admin:widgets/*" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "medusa-admin:routes/links" {
|
declare module "virtual:medusa/routes/pages" {
|
||||||
const links: { path: string; label: string; icon?: React.ComponentType }[]
|
const pages: { path: string; file: string }[]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
pages,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "virtual:medusa/routes/links" {
|
||||||
|
import type { ComponentType } from "react"
|
||||||
|
|
||||||
|
const links: { path: string; label: string; icon?: ComponentType }[]
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
links,
|
links,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "medusa-admin:routes/pages" {
|
|
||||||
const pages: { path: string; file: string }[]
|
|
||||||
|
|
||||||
export default {
|
|
||||||
pages,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "medusa-admin:settings/cards" {
|
|
||||||
const cards: { path: string; label: string; description: string }[]
|
|
||||||
|
|
||||||
export default {
|
|
||||||
cards,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "medusa-admin:settings/pages" {
|
|
||||||
const pages: { path: string; file: string }[]
|
|
||||||
|
|
||||||
export default {
|
|
||||||
pages,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
import { RouteObject } from "react-router-dom"
|
import routes from "virtual:medusa/routes/pages"
|
||||||
|
|
||||||
// import routes from "medusa-admin:routes/pages"
|
import { createRouteMap, settingsRouteRegex } from "../../lib/extension-helpers"
|
||||||
|
|
||||||
export const RouteExtensions: RouteObject[] = []
|
const pages = routes.pages
|
||||||
|
.filter((ext) => !settingsRouteRegex.test(ext.path))
|
||||||
|
.map((ext) => ext)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UI Route extensions.
|
* Core Route extensions.
|
||||||
*/
|
*/
|
||||||
// export const RouteExtensions: RouteObject[] = routes.pages.map((ext) => {
|
export const RouteExtensions = createRouteMap(pages)
|
||||||
// return {
|
|
||||||
// path: ext.path,
|
|
||||||
// async lazy() {
|
|
||||||
// const { default: Component } = await import(/* @vite-ignore */ ext.file)
|
|
||||||
// return { Component }
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|||||||
@@ -517,9 +517,9 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
...RouteExtensions,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
...RouteExtensions,
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1009,9 +1009,9 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
...SettingsExtensions,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
...SettingsExtensions,
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,19 +1,12 @@
|
|||||||
import { RouteObject } from "react-router-dom"
|
import routes from "virtual:medusa/routes/pages"
|
||||||
|
|
||||||
// import settings from "medusa-admin:settings/pages"
|
import { createRouteMap, settingsRouteRegex } from "../../lib/extension-helpers"
|
||||||
|
|
||||||
|
const pages = routes.pages
|
||||||
|
.filter((ext) => settingsRouteRegex.test(ext.path))
|
||||||
|
.map((ext) => ext)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UI Settings extensions.
|
* Settings Route extensions.
|
||||||
*/
|
*/
|
||||||
|
export const SettingsExtensions = createRouteMap(pages, "/settings")
|
||||||
export const SettingsExtensions: RouteObject[] = []
|
|
||||||
|
|
||||||
// export const SettingsExtensions: RouteObject[] = settings.pages.map((ext) => {
|
|
||||||
// return {
|
|
||||||
// path: `/settings${ext.path}`,
|
|
||||||
// async lazy() {
|
|
||||||
// const { default: Component } = await import(/* @vite-ignore */ ext.file)
|
|
||||||
// return { Component }
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import { ProductVariantSection } from "./components/product-variant-section"
|
|||||||
import { productLoader } from "./loader"
|
import { productLoader } from "./loader"
|
||||||
|
|
||||||
// import after from "medusa-admin:widgets/product/details/after"
|
// import after from "medusa-admin:widgets/product/details/after"
|
||||||
// import before from "medusa-admin:widgets/product/details/before"
|
// @ts-ignore - virtual module
|
||||||
|
// import obj from "virtual:config"
|
||||||
|
import before from "virtual:medusa/widgets/product/details/before"
|
||||||
// import sideAfter from "medusa-admin:widgets/product/details/side/after"
|
// import sideAfter from "medusa-admin:widgets/product/details/side/after"
|
||||||
// import sideBefore from "medusa-admin:widgets/product/details/side/before"
|
// import sideBefore from "medusa-admin:widgets/product/details/side/before"
|
||||||
|
|
||||||
@@ -38,14 +40,13 @@ export const ProductDetail = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-y-2">
|
<div className="flex flex-col gap-y-2">
|
||||||
{/* {before.widgets.map((w, i) => {
|
{before.widgets.map((w, i) => {
|
||||||
return (
|
return (
|
||||||
<div key={i}>
|
<div key={i}>
|
||||||
<w.Component />
|
<w.Component />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})} */}
|
})}
|
||||||
|
|
||||||
<div className="flex flex-col gap-x-4 lg:flex-row lg:items-start">
|
<div className="flex flex-col gap-x-4 lg:flex-row lg:items-start">
|
||||||
<div className="flex w-full flex-col gap-y-2">
|
<div className="flex w-full flex-col gap-y-2">
|
||||||
<ProductGeneralSection product={product} />
|
<ProductGeneralSection product={product} />
|
||||||
|
|||||||
@@ -6,5 +6,5 @@
|
|||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowSyntheticDefaultImports": true
|
"allowSyntheticDefaultImports": true
|
||||||
},
|
},
|
||||||
"include": ["vite.config.mts"]
|
"include": ["vite.config.js"]
|
||||||
}
|
}
|
||||||
|
|||||||
10
packages/admin-next/dashboard/tsup.config.cjs
Normal file
10
packages/admin-next/dashboard/tsup.config.cjs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { VIRTUAL_MODULES } from "@medusajs/admin-shared"
|
||||||
|
import { defineConfig } from "tsup"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ["./src/app.tsx"],
|
||||||
|
format: ["cjs", "esm"],
|
||||||
|
external: [...VIRTUAL_MODULES, "virtual:config"],
|
||||||
|
tsconfig: "tsconfig.build.json",
|
||||||
|
clean: true,
|
||||||
|
})
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { defineConfig } from "tsup"
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
entry: ["./src/app.tsx"],
|
|
||||||
format: ["cjs", "esm"],
|
|
||||||
external: [
|
|
||||||
"medusa-admin:settings/pages",
|
|
||||||
"medusa-admin:routes/pages",
|
|
||||||
"medusa-admin:widgets/promotion/list/after",
|
|
||||||
"medusa-admin:widgets/promotion/list/before",
|
|
||||||
"medusa-admin:widgets/promotion/details/after",
|
|
||||||
"medusa-admin:widgets/promotion/details/before",
|
|
||||||
"medusa-admin:widgets/product/list/after",
|
|
||||||
"medusa-admin:widgets/product/list/before",
|
|
||||||
"medusa-admin:widgets/product/details/after",
|
|
||||||
"medusa-admin:widgets/product/details/before",
|
|
||||||
"medusa-admin:widgets/product/details/side/after",
|
|
||||||
"medusa-admin:widgets/product/details/side/before",
|
|
||||||
"medusa-admin:routes/links",
|
|
||||||
],
|
|
||||||
tsconfig: "tsconfig.build.json",
|
|
||||||
clean: true,
|
|
||||||
})
|
|
||||||
@@ -5,9 +5,17 @@ import { defineConfig } from "vite"
|
|||||||
const BASE = "/"
|
const BASE = "/"
|
||||||
const BACKEND_URL = "http://localhost:9000"
|
const BACKEND_URL = "http://localhost:9000"
|
||||||
|
|
||||||
|
console.log(inject)
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), inject()],
|
plugins: [
|
||||||
|
react(),
|
||||||
|
inject({
|
||||||
|
debug: true,
|
||||||
|
sources: ["/Users/kasperkristensen/work/v2-server/src/admin"],
|
||||||
|
}),
|
||||||
|
],
|
||||||
define: {
|
define: {
|
||||||
__BASE__: JSON.stringify(BASE),
|
__BASE__: JSON.stringify(BASE),
|
||||||
__BACKEND_URL__: JSON.stringify(BACKEND_URL),
|
__BACKEND_URL__: JSON.stringify(BACKEND_URL),
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { ConfigModule } from "@medusajs/types"
|
import { ConfigModule } from "@medusajs/types"
|
||||||
import { transformFile } from "@swc/core"
|
import { transformFile } from "@swc/core"
|
||||||
import { getConfigFile } from "medusa-core-utils"
|
import { getConfigFile } from "medusa-core-utils"
|
||||||
import fs from "node:fs/promises"
|
import { existsSync } from "node:fs"
|
||||||
|
import { copyFile, mkdir, readdir, rm, writeFile } from "node:fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
type BuildArgs = {
|
type BuildArgs = {
|
||||||
@@ -21,12 +22,12 @@ const COMPILE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"]
|
|||||||
const IGNORE_EXTENSIONS = [".md"]
|
const IGNORE_EXTENSIONS = [".md"]
|
||||||
|
|
||||||
async function clean(path: string) {
|
async function clean(path: string) {
|
||||||
await fs.rm(path, { recursive: true }).catch(() => {})
|
await rm(path, { recursive: true }).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findFiles(dir: string): Promise<string[]> {
|
async function findFiles(dir: string): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
const files = await fs.readdir(dir, { withFileTypes: true })
|
const files = await readdir(dir, { withFileTypes: true })
|
||||||
const paths = await Promise.all(
|
const paths = await Promise.all(
|
||||||
files.map(async (file) => {
|
files.map(async (file) => {
|
||||||
const res = path.join(dir, file.name)
|
const res = path.join(dir, file.name)
|
||||||
@@ -64,16 +65,16 @@ const writeToOut = async (
|
|||||||
) => {
|
) => {
|
||||||
const outputPath = getOutputPath(file, config)
|
const outputPath = getOutputPath(file, config)
|
||||||
|
|
||||||
await fs.mkdir(outputPath.replace(/\/[^/]+$/, ""), { recursive: true })
|
await mkdir(outputPath.replace(/\/[^/]+$/, ""), { recursive: true })
|
||||||
await fs.writeFile(outputPath, content)
|
await writeFile(outputPath, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyToOut(file: string, config: FileConfig) {
|
async function copyToOut(file: string, config: FileConfig) {
|
||||||
const outputPath = getOutputPath(file, config)
|
const outputPath = getOutputPath(file, config)
|
||||||
const dirNameRegex = new RegExp("\\" + path.sep + "([^\\" + path.sep + "]+)$")
|
const dirNameRegex = new RegExp("\\" + path.sep + "([^\\" + path.sep + "]+)$")
|
||||||
|
|
||||||
await fs.mkdir(outputPath.replace(dirNameRegex, ""), { recursive: true })
|
await mkdir(outputPath.replace(dirNameRegex, ""), { recursive: true })
|
||||||
await fs.copyFile(file, outputPath)
|
await copyFile(file, outputPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
const medusaTransform = async (file: string) => {
|
const medusaTransform = async (file: string) => {
|
||||||
@@ -131,10 +132,19 @@ export default async function ({ directory }: BuildArgs) {
|
|||||||
|
|
||||||
await Promise.all(files.map(medusaTransform))
|
await Promise.all(files.map(medusaTransform))
|
||||||
|
|
||||||
|
const sources: string[] = []
|
||||||
|
|
||||||
|
const projectSource = path.join(directory, "src", "admin")
|
||||||
|
|
||||||
|
if (existsSync(projectSource)) {
|
||||||
|
sources.push(projectSource)
|
||||||
|
}
|
||||||
|
|
||||||
const adminOptions = {
|
const adminOptions = {
|
||||||
disable: false,
|
disable: false,
|
||||||
path: "/app" as const,
|
path: "/app" as const,
|
||||||
outDir: "./build",
|
outDir: "./build",
|
||||||
|
sources,
|
||||||
...configModule.admin,
|
...configModule.admin,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,43 @@
|
|||||||
import { AdminOptions, ConfigModule } from "@medusajs/types"
|
import { AdminOptions, ConfigModule } from "@medusajs/types"
|
||||||
import { Express } from "express"
|
import { Express } from "express"
|
||||||
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
app: Express
|
app: Express
|
||||||
adminConfig: ConfigModule["admin"]
|
configModule: ConfigModule
|
||||||
|
rootDirectory: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type IntializedOptions = Required<
|
type IntializedOptions = Required<
|
||||||
Pick<AdminOptions, "path" | "disable" | "outDir">
|
Pick<AdminOptions, "path" | "disable" | "outDir">
|
||||||
> &
|
> &
|
||||||
AdminOptions
|
AdminOptions & {
|
||||||
|
sources?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function adminLoader({
|
||||||
|
app,
|
||||||
|
configModule,
|
||||||
|
rootDirectory,
|
||||||
|
}: Options) {
|
||||||
|
const { admin } = configModule
|
||||||
|
|
||||||
|
const sources: string[] = []
|
||||||
|
|
||||||
|
const projectSource = path.join(rootDirectory, "src", "admin")
|
||||||
|
|
||||||
|
// check if the projectSource exists
|
||||||
|
if (fs.existsSync(projectSource)) {
|
||||||
|
sources.push(projectSource)
|
||||||
|
}
|
||||||
|
|
||||||
export default async function adminLoader({ app, adminConfig }: Options) {
|
|
||||||
const adminOptions: IntializedOptions = {
|
const adminOptions: IntializedOptions = {
|
||||||
disable: false,
|
disable: false,
|
||||||
path: "/app",
|
path: "/app",
|
||||||
outDir: "./build",
|
outDir: "./build",
|
||||||
...adminConfig,
|
sources,
|
||||||
|
...admin,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (adminOptions?.disable) {
|
if (adminOptions?.disable) {
|
||||||
|
|||||||
@@ -1,25 +1,24 @@
|
|||||||
import { createDefaultsWorkflow } from "@medusajs/core-flows"
|
import { createDefaultsWorkflow } from "@medusajs/core-flows"
|
||||||
import { ConfigModule, MedusaContainer } from "@medusajs/types"
|
import { ConfigModule, MedusaContainer, PluginDetails } from "@medusajs/types"
|
||||||
import { ContainerRegistrationKeys, promiseAll } from "@medusajs/utils"
|
import { ContainerRegistrationKeys, promiseAll } from "@medusajs/utils"
|
||||||
import { asValue } from "awilix"
|
import { asValue } from "awilix"
|
||||||
import { Express, NextFunction, Request, Response } from "express"
|
import { Express, NextFunction, Request, Response } from "express"
|
||||||
|
import glob from "glob"
|
||||||
import { createMedusaContainer } from "medusa-core-utils"
|
import { createMedusaContainer } from "medusa-core-utils"
|
||||||
|
import path from "path"
|
||||||
import requestIp from "request-ip"
|
import requestIp from "request-ip"
|
||||||
import { v4 } from "uuid"
|
import { v4 } from "uuid"
|
||||||
import path from "path"
|
|
||||||
import adminLoader from "./admin"
|
import adminLoader from "./admin"
|
||||||
import apiLoader from "./api"
|
import apiLoader from "./api"
|
||||||
import loadConfig from "./config"
|
import loadConfig from "./config"
|
||||||
import expressLoader from "./express"
|
import expressLoader from "./express"
|
||||||
import featureFlagsLoader from "./feature-flags"
|
import featureFlagsLoader from "./feature-flags"
|
||||||
import { registerWorkflows } from "./helpers/register-workflows"
|
import { registerWorkflows } from "./helpers/register-workflows"
|
||||||
|
import { getResolvedPlugins } from "./helpers/resolve-plugins"
|
||||||
|
import { SubscriberLoader } from "./helpers/subscribers"
|
||||||
import Logger from "./logger"
|
import Logger from "./logger"
|
||||||
import loadMedusaApp from "./medusa-app"
|
import loadMedusaApp from "./medusa-app"
|
||||||
import registerPgConnection from "./pg-connection"
|
import registerPgConnection from "./pg-connection"
|
||||||
import { SubscriberLoader } from "./helpers/subscribers"
|
|
||||||
import { getResolvedPlugins } from "./helpers/resolve-plugins"
|
|
||||||
import { PluginDetails } from "@medusajs/types"
|
|
||||||
import glob from "glob"
|
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
directory: string
|
directory: string
|
||||||
@@ -63,7 +62,8 @@ async function subscribersLoader(
|
|||||||
async function loadEntrypoints(
|
async function loadEntrypoints(
|
||||||
plugins: PluginDetails[],
|
plugins: PluginDetails[],
|
||||||
container: MedusaContainer,
|
container: MedusaContainer,
|
||||||
expressApp: Express
|
expressApp: Express,
|
||||||
|
rootDirectory: string
|
||||||
) {
|
) {
|
||||||
const configModule: ConfigModule = container.resolve(
|
const configModule: ConfigModule = container.resolve(
|
||||||
ContainerRegistrationKeys.CONFIG_MODULE
|
ContainerRegistrationKeys.CONFIG_MODULE
|
||||||
@@ -89,7 +89,7 @@ async function loadEntrypoints(
|
|||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
await adminLoader({ app: expressApp, adminConfig: configModule.admin })
|
await adminLoader({ app: expressApp, configModule, rootDirectory })
|
||||||
await subscribersLoader(plugins, container)
|
await subscribersLoader(plugins, container)
|
||||||
await apiLoader({
|
await apiLoader({
|
||||||
container,
|
container,
|
||||||
@@ -143,7 +143,8 @@ export default async ({
|
|||||||
const entrypointsShutdown = await loadEntrypoints(
|
const entrypointsShutdown = await loadEntrypoints(
|
||||||
plugins,
|
plugins,
|
||||||
container,
|
container,
|
||||||
expressApp
|
expressApp,
|
||||||
|
rootDirectory
|
||||||
)
|
)
|
||||||
await createDefaultsWorkflow(container).run()
|
await createDefaultsWorkflow(container).run()
|
||||||
|
|
||||||
|
|||||||
37
yarn.lock
37
yarn.lock
@@ -5009,16 +5009,15 @@ __metadata:
|
|||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@medusajs/admin-sdk@workspace:packages/admin-next/admin-sdk"
|
resolution: "@medusajs/admin-sdk@workspace:packages/admin-next/admin-sdk"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@medusajs/admin-shared": 0.0.1
|
||||||
"@medusajs/admin-vite-plugin": 0.0.1
|
"@medusajs/admin-vite-plugin": 0.0.1
|
||||||
"@medusajs/dashboard": 0.0.1
|
"@medusajs/dashboard": 0.0.1
|
||||||
"@medusajs/types": ^1.11.16
|
"@medusajs/types": ^1.11.16
|
||||||
"@types/compression": ^1.7.5
|
"@types/compression": ^1.7.5
|
||||||
"@types/connect-history-api-fallback": ^1.5.4
|
|
||||||
"@vitejs/plugin-react": ^4.2.1
|
"@vitejs/plugin-react": ^4.2.1
|
||||||
autoprefixer: ^10.4.16
|
autoprefixer: ^10.4.16
|
||||||
commander: ^11.1.0
|
commander: ^11.1.0
|
||||||
compression: ^1.7.4
|
compression: ^1.7.4
|
||||||
connect-history-api-fallback: ^2.0.0
|
|
||||||
copyfiles: ^2.4.1
|
copyfiles: ^2.4.1
|
||||||
deepmerge: ^4.3.1
|
deepmerge: ^4.3.1
|
||||||
express: ^4.18.2
|
express: ^4.18.2
|
||||||
@@ -5035,10 +5034,12 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
"@medusajs/admin-shared@0.0.1, @medusajs/admin-shared@workspace:packages/admin-next/admin-shared":
|
"@medusajs/admin-shared@0.0.1, @medusajs/admin-shared@^0.0.1, @medusajs/admin-shared@workspace:packages/admin-next/admin-shared":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@medusajs/admin-shared@workspace:packages/admin-next/admin-shared"
|
resolution: "@medusajs/admin-shared@workspace:packages/admin-next/admin-shared"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@types/react": ^18.3.2
|
||||||
|
tsup: ^8.0.2
|
||||||
typescript: ^5.3.3
|
typescript: ^5.3.3
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
@@ -5259,6 +5260,7 @@ __metadata:
|
|||||||
"@dnd-kit/core": ^6.1.0
|
"@dnd-kit/core": ^6.1.0
|
||||||
"@dnd-kit/sortable": ^8.0.0
|
"@dnd-kit/sortable": ^8.0.0
|
||||||
"@hookform/resolvers": 3.3.2
|
"@hookform/resolvers": 3.3.2
|
||||||
|
"@medusajs/admin-shared": ^0.0.1
|
||||||
"@medusajs/admin-vite-plugin": 0.0.1
|
"@medusajs/admin-vite-plugin": 0.0.1
|
||||||
"@medusajs/icons": 1.2.1
|
"@medusajs/icons": 1.2.1
|
||||||
"@medusajs/js-sdk": 0.0.1
|
"@medusajs/js-sdk": 0.0.1
|
||||||
@@ -11157,16 +11159,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@types/connect-history-api-fallback@npm:^1.5.4":
|
|
||||||
version: 1.5.4
|
|
||||||
resolution: "@types/connect-history-api-fallback@npm:1.5.4"
|
|
||||||
dependencies:
|
|
||||||
"@types/express-serve-static-core": "*"
|
|
||||||
"@types/node": "*"
|
|
||||||
checksum: 1b4035b627dcd714b05a22557f942e24a57ca48e7377dde0d2f86313fe685bc0a6566512a73257a55b5665b96c3041fb29228ac93331d8133011716215de8244
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@types/connect@npm:*":
|
"@types/connect@npm:*":
|
||||||
version: 3.4.38
|
version: 3.4.38
|
||||||
resolution: "@types/connect@npm:3.4.38"
|
resolution: "@types/connect@npm:3.4.38"
|
||||||
@@ -11255,7 +11247,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@types/express-serve-static-core@npm:*, @types/express-serve-static-core@npm:^4.17.33":
|
"@types/express-serve-static-core@npm:^4.17.33":
|
||||||
version: 4.19.0
|
version: 4.19.0
|
||||||
resolution: "@types/express-serve-static-core@npm:4.19.0"
|
resolution: "@types/express-serve-static-core@npm:4.19.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -11724,6 +11716,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/react@npm:^18.3.2":
|
||||||
|
version: 18.3.2
|
||||||
|
resolution: "@types/react@npm:18.3.2"
|
||||||
|
dependencies:
|
||||||
|
"@types/prop-types": "*"
|
||||||
|
csstype: ^3.0.2
|
||||||
|
checksum: 9fb2f1fcf7e889ee4ea7c3c5978df595c66e770e5fd3a245dbdd2589b9b911524c11dab25a6275d8af4e336e4cb5fa850d447884b84c335a187a338c89df99ba
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/resolve@npm:1.17.1":
|
"@types/resolve@npm:1.17.1":
|
||||||
version: 1.17.1
|
version: 1.17.1
|
||||||
resolution: "@types/resolve@npm:1.17.1"
|
resolution: "@types/resolve@npm:1.17.1"
|
||||||
@@ -15167,13 +15169,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"connect-history-api-fallback@npm:^2.0.0":
|
|
||||||
version: 2.0.0
|
|
||||||
resolution: "connect-history-api-fallback@npm:2.0.0"
|
|
||||||
checksum: 90fa8b16ab76e9531646cc70b010b1dbd078153730c510d3142f6cf07479ae8a812c5a3c0e40a28528dd1681a62395d0cfdef67da9e914c4772ac85d69a3ed87
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"connect-redis@npm:^5.0.0":
|
"connect-redis@npm:^5.0.0":
|
||||||
version: 5.2.0
|
version: 5.2.0
|
||||||
resolution: "connect-redis@npm:5.2.0"
|
resolution: "connect-redis@npm:5.2.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user