**What** - #9338 had a regression which caused the import path in some virtual modules to be invalid on Windows. - This PR fixes the issue so we now again create the correct import paths, and adds tests to prevent this from slipping in again.
206 lines
4.5 KiB
TypeScript
206 lines
4.5 KiB
TypeScript
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,
|
|
normalizePath,
|
|
} 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 {
|
|
const path = normalizePath(file)
|
|
return `import ${generateWidgetComponentName(
|
|
index
|
|
)}, { config as ${generateWidgetConfigName(index)} } from "${path}"`
|
|
}
|
|
|
|
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
|
|
}
|