feat(dashboard,admin-vite-plugin,admin-bundler,admin-sdk): Rework admin extensions and introduce custom fields API (#9338)

This commit is contained in:
Kasper Fabricius Kristensen
2024-10-09 13:44:40 +02:00
committed by GitHub
parent 35e69d32f2
commit d71343d6ab
159 changed files with 5266 additions and 2226 deletions

View File

@@ -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 }
}

View 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
}

View 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()
}

View File

@@ -0,0 +1,2 @@
export * from "./generate-widget-hash"
export * from "./generate-widgets"