feat(dashboard,admin-vite-plugin,admin-bundler,admin-sdk): Rework admin extensions and introduce custom fields API (#9338)
This commit is contained in:
committed by
GitHub
parent
35e69d32f2
commit
d71343d6ab
@@ -0,0 +1,59 @@
|
||||
import fs from "fs/promises"
|
||||
import { File, parse, ParseResult, traverse } from "../babel"
|
||||
import { logger } from "../logger"
|
||||
import {
|
||||
generateHash,
|
||||
getConfigObjectProperties,
|
||||
getParserOptions,
|
||||
} from "../utils"
|
||||
import { getWidgetFilesFromSources } from "./helpers"
|
||||
|
||||
export async function generateWidgetHash(
|
||||
sources: Set<string>
|
||||
): Promise<string> {
|
||||
const files = await getWidgetFilesFromSources(sources)
|
||||
const contents = await Promise.all(files.map(getWidgetContents))
|
||||
const totalContent = contents
|
||||
.flatMap(({ config, defaultExport }) => [config, defaultExport])
|
||||
.filter(Boolean)
|
||||
.join("")
|
||||
|
||||
return generateHash(totalContent)
|
||||
}
|
||||
|
||||
async function getWidgetContents(
|
||||
file: string
|
||||
): Promise<{ config: string | null; defaultExport: string | null }> {
|
||||
const code = await fs.readFile(file, "utf-8")
|
||||
let ast: ParseResult<File>
|
||||
|
||||
try {
|
||||
ast = parse(code, getParserOptions(file))
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`An error occurred while parsing the file. Due to the error we cannot validate whether the widget has changed. If your changes aren't correctly reflected try restarting the dev server.`,
|
||||
{
|
||||
file,
|
||||
error: e,
|
||||
}
|
||||
)
|
||||
return { config: null, defaultExport: null }
|
||||
}
|
||||
|
||||
let configContent: string | null = null
|
||||
let defaultExportContent: string | null = null
|
||||
|
||||
traverse(ast, {
|
||||
ExportNamedDeclaration(path) {
|
||||
const properties = getConfigObjectProperties(path)
|
||||
if (properties) {
|
||||
configContent = code.slice(path.node.start!, path.node.end!)
|
||||
}
|
||||
},
|
||||
ExportDefaultDeclaration(path) {
|
||||
defaultExportContent = code.slice(path.node.start!, path.node.end!)
|
||||
},
|
||||
})
|
||||
|
||||
return { config: configContent, defaultExport: defaultExportContent }
|
||||
}
|
||||
203
packages/admin/admin-vite-plugin/src/widgets/generate-widgets.ts
Normal file
203
packages/admin/admin-vite-plugin/src/widgets/generate-widgets.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { InjectionZone, isValidInjectionZone } from "@medusajs/admin-shared"
|
||||
import fs from "fs/promises"
|
||||
import outdent from "outdent"
|
||||
import {
|
||||
File,
|
||||
isArrayExpression,
|
||||
isStringLiteral,
|
||||
isTemplateLiteral,
|
||||
ObjectProperty,
|
||||
parse,
|
||||
ParseResult,
|
||||
traverse,
|
||||
} from "../babel"
|
||||
import { logger } from "../logger"
|
||||
import {
|
||||
getConfigObjectProperties,
|
||||
getParserOptions,
|
||||
hasDefaultExport,
|
||||
} from "../utils"
|
||||
import { getWidgetFilesFromSources } from "./helpers"
|
||||
|
||||
type WidgetConfig = {
|
||||
Component: string
|
||||
zone: InjectionZone[]
|
||||
}
|
||||
|
||||
type ParsedWidgetConfig = {
|
||||
import: string
|
||||
widget: WidgetConfig
|
||||
}
|
||||
|
||||
export async function generateWidgets(sources: Set<string>) {
|
||||
const files = await getWidgetFilesFromSources(sources)
|
||||
const results = await getWidgetResults(files)
|
||||
|
||||
const imports = results.map((r) => r.import)
|
||||
const code = generateCode(results)
|
||||
|
||||
return {
|
||||
imports,
|
||||
code,
|
||||
}
|
||||
}
|
||||
|
||||
async function getWidgetResults(
|
||||
files: string[]
|
||||
): Promise<ParsedWidgetConfig[]> {
|
||||
return (await Promise.all(files.map(parseFile))).filter(
|
||||
(r) => r !== null
|
||||
) as ParsedWidgetConfig[]
|
||||
}
|
||||
|
||||
function generateCode(results: ParsedWidgetConfig[]): string {
|
||||
return outdent`
|
||||
widgets: [
|
||||
${results.map((r) => formatWidget(r.widget)).join(",\n")}
|
||||
]
|
||||
`
|
||||
}
|
||||
|
||||
function formatWidget(widget: WidgetConfig): string {
|
||||
return outdent`
|
||||
{
|
||||
Component: ${widget.Component},
|
||||
zone: [${widget.zone.map((z) => `"${z}"`).join(", ")}]
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
async function parseFile(
|
||||
file: string,
|
||||
index: number
|
||||
): Promise<ParsedWidgetConfig | null> {
|
||||
const code = await fs.readFile(file, "utf-8")
|
||||
let ast: ParseResult<File>
|
||||
|
||||
try {
|
||||
ast = parse(code, getParserOptions(file))
|
||||
} catch (e) {
|
||||
logger.error(`An error occurred while parsing the file.`, {
|
||||
file,
|
||||
error: e,
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
let fileHasDefaultExport = false
|
||||
|
||||
try {
|
||||
fileHasDefaultExport = await hasDefaultExport(ast)
|
||||
} catch (e) {
|
||||
logger.error(`An error occurred while checking for a default export.`, {
|
||||
file,
|
||||
error: e,
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
if (!fileHasDefaultExport) {
|
||||
return null
|
||||
}
|
||||
|
||||
let zone: InjectionZone[] | null
|
||||
|
||||
try {
|
||||
zone = await getWidgetZone(ast, file)
|
||||
} catch (e) {
|
||||
logger.error(`An error occurred while traversing the file.`, {
|
||||
file,
|
||||
error: e,
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
if (!zone) {
|
||||
logger.warn(`'zone' property is missing from the widget config.`, { file })
|
||||
return null
|
||||
}
|
||||
|
||||
const import_ = generateImport(file, index)
|
||||
const widget = generateWidget(zone, index)
|
||||
|
||||
return {
|
||||
widget,
|
||||
import: import_,
|
||||
}
|
||||
}
|
||||
|
||||
function generateWidgetComponentName(index: number): string {
|
||||
return `WidgetComponent${index}`
|
||||
}
|
||||
|
||||
function generateWidgetConfigName(index: number): string {
|
||||
return `WidgetConfig${index}`
|
||||
}
|
||||
|
||||
function generateImport(file: string, index: number): string {
|
||||
return `import ${generateWidgetComponentName(
|
||||
index
|
||||
)}, { config as ${generateWidgetConfigName(index)} } from "${file}"`
|
||||
}
|
||||
|
||||
function generateWidget(zone: InjectionZone[], index: number): WidgetConfig {
|
||||
return {
|
||||
Component: generateWidgetComponentName(index),
|
||||
zone: zone,
|
||||
}
|
||||
}
|
||||
|
||||
async function getWidgetZone(
|
||||
ast: ParseResult<File>,
|
||||
file: string
|
||||
): Promise<InjectionZone[] | null> {
|
||||
const zones: string[] = []
|
||||
|
||||
traverse(ast, {
|
||||
ExportNamedDeclaration(path) {
|
||||
const properties = getConfigObjectProperties(path)
|
||||
if (!properties) {
|
||||
return
|
||||
}
|
||||
|
||||
const zoneProperty = properties.find(
|
||||
(p) =>
|
||||
p.type === "ObjectProperty" &&
|
||||
p.key.type === "Identifier" &&
|
||||
p.key.name === "zone"
|
||||
) as ObjectProperty | undefined
|
||||
|
||||
if (!zoneProperty) {
|
||||
logger.warn(`'zone' property is missing from the widget config.`, {
|
||||
file,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (isTemplateLiteral(zoneProperty.value)) {
|
||||
logger.warn(
|
||||
`'zone' property cannot be a template literal (e.g. \`product.details.after\`).`,
|
||||
{ file }
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (isStringLiteral(zoneProperty.value)) {
|
||||
zones.push(zoneProperty.value.value)
|
||||
} else if (isArrayExpression(zoneProperty.value)) {
|
||||
const values: string[] = []
|
||||
|
||||
for (const element of zoneProperty.value.elements) {
|
||||
if (isStringLiteral(element)) {
|
||||
values.push(element.value)
|
||||
}
|
||||
}
|
||||
|
||||
zones.push(...values)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const validatedZones = zones.filter(isValidInjectionZone)
|
||||
return validatedZones.length > 0 ? validatedZones : null
|
||||
}
|
||||
11
packages/admin/admin-vite-plugin/src/widgets/helpers.ts
Normal file
11
packages/admin/admin-vite-plugin/src/widgets/helpers.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { crawl } from "../utils"
|
||||
|
||||
export async function getWidgetFilesFromSources(
|
||||
sources: Set<string>
|
||||
): Promise<string[]> {
|
||||
return (
|
||||
await Promise.all(
|
||||
Array.from(sources).map(async (source) => crawl(`${source}/widgets`))
|
||||
)
|
||||
).flat()
|
||||
}
|
||||
2
packages/admin/admin-vite-plugin/src/widgets/index.ts
Normal file
2
packages/admin/admin-vite-plugin/src/widgets/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./generate-widget-hash"
|
||||
export * from "./generate-widgets"
|
||||
Reference in New Issue
Block a user