feat(admin-bundler,admin-vite-plugin,medusa): Add support for loading Admin Extensions from plugins (#10869)

Should not be merged before https://github.com/medusajs/medusa/pull/10895

**What**
- Introduces a new `plugin` command to `admin-bundler`, currently not used anywhere but will be called from `medusa build:plugin`
- Discovers plugins with extensions and add passes the to `admin-vite-plugin`.
- Updates `admin-vite-plugin` so its able to read built admin extensions.

Resolves CMRC-830, CMRC-839
This commit is contained in:
Kasper Fabricius Kristensen
2025-01-13 11:45:33 +01:00
committed by GitHub
parent 253b642418
commit 1ba2fadf22
13 changed files with 316 additions and 92 deletions

View File

@@ -1,9 +1,16 @@
import {
NESTED_ROUTE_POSITIONS,
NestedRoutePosition,
} from "@medusajs/admin-shared"
import fs from "fs/promises"
import { outdent } from "outdent"
import {
File,
isIdentifier,
isObjectProperty,
isStringLiteral,
Node,
ObjectProperty,
parse,
ParseResult,
traverse,
@@ -16,7 +23,6 @@ import {
normalizePath,
} from "../utils"
import { getRoute } from "./helpers"
import { NESTED_ROUTE_POSITIONS } from "@medusajs/admin-shared"
type RouteConfig = {
label: boolean
@@ -142,48 +148,47 @@ async function getRouteConfig(file: string): Promise<RouteConfig | null> {
}
let config: RouteConfig | null = null
let configFound = false
try {
traverse(ast, {
ExportNamedDeclaration(path) {
/**
* For bundled files, the config will not be a named export,
* but instead a variable declaration.
*/
VariableDeclarator(path) {
if (configFound) {
return
}
const properties = getConfigObjectProperties(path)
if (!properties) {
return
}
const hasProperty = (name: string) =>
properties.some(
(prop) => isObjectProperty(prop) && isIdentifier(prop.key, { name })
)
config = processConfigProperties(properties, file)
const hasLabel = hasProperty("label")
if (!hasLabel) {
if (config) {
configFound = true
}
},
/**
* For unbundled files, the `config` will always be a named export.
*/
ExportNamedDeclaration(path) {
if (configFound) {
return
}
const nested = properties.find(
(prop) =>
isObjectProperty(prop) && isIdentifier(prop.key, { name: "nested" })
)
const nestedValue = nested ? (nested as any).value.value : undefined
if (nestedValue && !NESTED_ROUTE_POSITIONS.includes(nestedValue)) {
logger.error(
`Invalid nested route position: "${nestedValue}". Allowed values are: ${NESTED_ROUTE_POSITIONS.join(
", "
)}`,
{
file,
}
)
const properties = getConfigObjectProperties(path)
if (!properties) {
return
}
config = {
label: hasLabel,
icon: hasProperty("icon"),
nested: nestedValue,
config = processConfigProperties(properties, file)
if (config) {
configFound = true
}
},
})
@@ -197,6 +202,51 @@ async function getRouteConfig(file: string): Promise<RouteConfig | null> {
return config
}
function processConfigProperties(
properties: Node[],
file: string
): RouteConfig | null {
const hasProperty = (name: string) =>
properties.some(
(prop) => isObjectProperty(prop) && isIdentifier(prop.key, { name })
)
const hasLabel = hasProperty("label")
if (!hasLabel) {
return null
}
const nested = properties.find(
(prop) =>
isObjectProperty(prop) && isIdentifier(prop.key, { name: "nested" })
) as ObjectProperty | undefined
let nestedValue: string | undefined = undefined
if (isStringLiteral(nested?.value)) {
nestedValue = nested.value.value
}
if (
nestedValue &&
!NESTED_ROUTE_POSITIONS.includes(nestedValue as NestedRoutePosition)
) {
logger.error(
`Invalid nested route position: "${nestedValue}". Allowed values are: ${NESTED_ROUTE_POSITIONS.join(
", "
)}`,
{ file }
)
return null
}
return {
label: hasLabel,
icon: hasProperty("icon"),
nested: nestedValue,
}
}
function generateRouteConfigName(index: number): string {
return `RouteConfig${index}`
}

View File

@@ -23,7 +23,6 @@ type RouteResult = {
export async function generateRoutes(sources: Set<string>) {
const files = await getFilesFromSources(sources)
const results = await getRouteResults(files)
const imports = results.map((result) => result.imports).flat()
const code = generateCode(results)

View File

@@ -1,9 +1,16 @@
import { normalizePath } from "../utils"
import { normalizePath, VALID_FILE_EXTENSIONS } from "../utils"
export function getRoute(file: string): string {
const importPath = normalizePath(file)
return importPath
.replace(/.*\/admin\/(routes)/, "")
.replace(/\[([^\]]+)\]/g, ":$1")
.replace(/\/page\.(tsx|jsx)/, "")
.replace(
new RegExp(
`/page\\.(${VALID_FILE_EXTENSIONS.map((ext) => ext.slice(1)).join(
"|"
)})$`
),
""
)
}