chore: reorganize docs apps (#7228)

* reorganize docs apps

* add README

* fix directory

* add condition for old docs
This commit is contained in:
Shahed Nasser authored and GitHub committed 2024-05-03 17:36:38 +03:00
1 parent 224ebb2154
commit 4fe28f5a95
6187 files changed
+601447 -598226

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
const path = require("path")
module.exports = {
extends: ["docs"],
parserOptions: {
project: true,
tsconfigRootDir: path.join(__dirname, "..", ".."),
},
}
+43
View File
@@ -0,0 +1,43 @@
{
"name": "build-scripts",
"version": "0.0.0",
"private": true,
"license": "MIT",
"publishConfig": {
"access": "public"
},
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"module": "./dist/index.js",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
},
"sideEffects": false,
"files": [
"dist/**"
],
"scripts": {
"build": "yarn clean && tsc",
"clean": "rimraf dist"
},
"dependencies": {
"remark-rehype-plugins": "*"
},
"devDependencies": {
"@types/node": "^20.11.20",
"eslint-config-docs": "*",
"rimraf": "^5.0.5",
"tsconfig": "*",
"types": "*",
"typescript": "^5.3.3"
},
"engines": {
"node": ">=18.17.0"
}
}
@@ -0,0 +1,159 @@
import type { RawSidebarItemType } from "types"
import findPageHeading from "./utils/find-page-heading.js"
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync } from "fs"
import path from "path"
import { sidebarAttachHrefCommonOptions } from "./index.js"
import { getFrontMatterUtil } from "remark-rehype-plugins"
import findMetadataTitle from "./utils/find-metadata-title.js"
type ItemsToAdd = RawSidebarItemType & {
sidebar_position?: number
}
async function getSidebarItems(
dir: string,
nested = false
): Promise<ItemsToAdd[]> {
const isRefDir = dir.startsWith("/references")
const basePath = isRefDir ? path.resolve() : path.resolve("app")
const fullPath = path.resolve(basePath, dir.replace(/^\//, ""))
const items: ItemsToAdd[] = []
const files = readdirSync(fullPath)
let mainPageIndex = -1
for (const file of files) {
const filePath = path.join(fullPath, file)
const fileBasename = path.basename(file)
if (fileBasename.startsWith("_")) {
continue
}
if (fileBasename !== "page.mdx" && statSync(filePath).isDirectory()) {
const newItems = await getSidebarItems(
filePath.replace(basePath, ""),
true
)
if (nested && newItems.length > 1) {
items.push(
...sidebarAttachHrefCommonOptions([
{
title:
fileBasename.charAt(0).toUpperCase() +
fileBasename.substring(1),
hasTitleStyling: true,
children: newItems,
},
])
)
} else {
items.push(...sidebarAttachHrefCommonOptions(newItems))
}
continue
}
const frontmatter = await getFrontMatterUtil(filePath)
if (frontmatter.sidebar_autogenerate_exclude) {
continue
}
const fileContent = frontmatter.sidebar_label
? ""
: readFileSync(filePath, "utf-8")
const newItem = sidebarAttachHrefCommonOptions([
{
path:
frontmatter.slug ||
filePath.replace(basePath, "").replace(`/${fileBasename}`, ""),
title:
frontmatter.sidebar_label ||
findMetadataTitle(fileContent) ||
findPageHeading(fileContent) ||
"",
},
])[0]
items.push({
...newItem,
sidebar_position: frontmatter.sidebar_position,
})
mainPageIndex = items.length - 1
}
if (mainPageIndex !== -1 && items.length > 1) {
// push all other items to be children of that page.
const mainPageChildren = [
...items.splice(0, mainPageIndex),
...items.splice(1),
]
mainPageChildren.sort(sortItems)
items[0].children = mainPageChildren
} else if (items.length > 1) {
items.sort(sortItems)
}
return items
}
async function checkItem(
item: RawSidebarItemType
): Promise<RawSidebarItemType> {
if (item.autogenerate_path) {
item.children = (await getSidebarItems(item.autogenerate_path)).map(
(child) => {
delete child.sidebar_position
return child
}
)
}
if (item.children) {
item.children = await Promise.all(
item.children.map(async (childItem) => await checkItem(childItem))
)
}
return item
}
export async function generateSidebar(sidebar: RawSidebarItemType[]) {
const path = await import("path")
const { writeFileSync } = await import("fs")
const normalizedSidebar = await Promise.all(
sidebar.map(async (item) => await checkItem(item))
)
const generatedDirPath = path.resolve("generated")
if (!existsSync(generatedDirPath)) {
mkdirSync(generatedDirPath)
}
// write normalized sidebar
writeFileSync(
path.resolve(generatedDirPath, "sidebar.mjs"),
`export const generatedSidebar = ${JSON.stringify(
normalizedSidebar,
null,
2
)}`,
{
flag: "w",
}
)
}
function sortItems(itemA: ItemsToAdd, itemB: ItemsToAdd): number {
const itemASidebarPosition = itemA.sidebar_position || 0
const itemBSidebarPosition = itemB.sidebar_position || 0
if (itemASidebarPosition > itemBSidebarPosition) {
return 1
}
return itemASidebarPosition < itemBSidebarPosition ? -1 : 0
}
+5
View File
@@ -0,0 +1,5 @@
export * from "./generate-sidebar.js"
export * from "./utils/find-metadata-title.js"
export * from "./utils/find-page-heading.js"
export * from "./utils/sidebar-attach-href-common-options.js"
@@ -0,0 +1,7 @@
const REGEX = /export const metadata = {[\s\S]*title: `(?<title>.*)`/
export default function findMetadataTitle(content: string): string | undefined {
const headingMatch = REGEX.exec(content)
return headingMatch?.groups?.title
}
@@ -0,0 +1,7 @@
const HEADING_REGEX = /# (?<title>.*)/
export default function findPageHeading(content: string): string | undefined {
const headingMatch = HEADING_REGEX.exec(content)
return headingMatch?.groups?.title
}
@@ -0,0 +1,16 @@
import { RawSidebarItemType } from "types"
const commonOptions: Partial<RawSidebarItemType> = {
loaded: true,
isPathHref: true,
}
export function sidebarAttachHrefCommonOptions(
sidebar: RawSidebarItemType[]
): RawSidebarItemType[] {
return sidebar.map((item) => ({
...commonOptions,
...item,
children: sidebarAttachHrefCommonOptions(item.children || []),
}))
}
+19
View File
@@ -0,0 +1,19 @@
{
"extends": ["tsconfig/base.json"],
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"tsBuildInfoFile": "./dist/.tsbuildinfo-client",
"noEmit": false,
"lib": ["es2022"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true,
},
"include": ["src"]
}
+1
View File
@@ -65,6 +65,7 @@
"@segment/analytics-next": "^1.55.0",
"@uidotdev/usehooks": "^2.4.1",
"algoliasearch": "^4.20.0",
"copy-text-to-clipboard": "^3.2.0",
"mermaid": "^10.9.0",
"npm-to-yarn": "^2.1.0",
"prism-react-renderer": "2.3.1",
@@ -11,7 +11,7 @@ import {
SearchSuggestionType,
SearchHitGroupName,
Tooltip,
LegacyLink,
Link,
} from "@/components"
import { useAiAssistant, useSearch } from "@/providers"
import { ArrowUturnLeft, XMarkMini } from "@medusajs/icons"
@@ -382,14 +382,10 @@ export const AiAssistant = () => {
tooltipChildren={
<>
This site is protected by reCAPTCHA and the{" "}
{/* TODO replace with Link once we move away from Docusaurus */}
<LegacyLink href="https://policies.google.com/privacy">
<Link href="https://policies.google.com/privacy">
Google Privacy Policy
</LegacyLink>{" "}
and{" "}
<LegacyLink href="https://policies.google.com/terms">
ToS
</LegacyLink>{" "}
</Link>{" "}
and <Link href="https://policies.google.com/terms">ToS</Link>{" "}
apply
</>
}
@@ -1,7 +1,7 @@
import React from "react"
import { ArrowUpRightOnBox } from "@medusajs/icons"
import clsx from "clsx"
import { LegacyLink } from "@/components"
import { Link } from "@/components"
export type CardProps = {
startIcon?: React.ReactNode
@@ -57,8 +57,7 @@ export const Card = ({
{showLinkIcon && (
<ArrowUpRightOnBox className="text-medusa-fg-subtle min-w-[20px]" />
)}
{/* TODO replace with Link once we move away from Docusaurus */}
<LegacyLink
<Link
href={href}
className="absolute left-0 top-0 h-full w-full rounded"
/>
@@ -46,11 +46,15 @@ export const ApiRunner = ({
[apiTestingOptions]
)
const [responseLogs, setResponseLogs] = useState<string[]>([])
const [responseCode, setResponseCode] = useState<string | undefined>()
const pushMessage = (...message: string[]) =>
setResponseLogs((prev) => [...prev, ...message])
const { runRequest } = useRequestRunner({
pushLog: pushMessage,
onFinish: () => setIsRunning(false),
onFinish: (_message, statusCode) => {
setIsRunning(false)
setResponseCode(statusCode)
},
replaceLog: (message) => setResponseLogs([message]),
})
@@ -149,6 +153,14 @@ export const ApiRunner = ({
collapsed={true}
blockStyle="subtle"
noReport={true}
badgeLabel={responseCode}
badgeColor={
!responseCode
? undefined
: responseCode.startsWith("2")
? "green"
: "red"
}
/>
)}
</>
@@ -3,7 +3,7 @@
import React, { useMemo, useState } from "react"
import clsx from "clsx"
import { HighlightProps, Highlight, themes } from "prism-react-renderer"
import { CopyButton, Tooltip, LegacyLink } from "@/components"
import { CopyButton, Tooltip, Link } from "@/components"
import { useColorMode } from "@/providers"
import { ExclamationCircle, PlaySolid, SquareTwoStack } from "@medusajs/icons"
import { CodeBlockHeader, CodeBlockHeaderMeta } from "./Header"
@@ -269,8 +269,7 @@ export const CodeBlock = ({
tokens.length > 1 && "px-[6px] pb-[6px]"
)}
>
{/* TODO replace with Link once we move away from Docusaurus */}
<LegacyLink
<Link
href={`${GITHUB_ISSUES_PREFIX}&title=${encodeURIComponent(
`Docs(Code Issue): `
)}`}
@@ -284,7 +283,7 @@ export const CodeBlock = ({
rel="noreferrer"
>
<ExclamationCircle className={clsx(iconColor)} />
</LegacyLink>
</Link>
</Tooltip>
)}
{!noCopy && (
@@ -1,5 +1,5 @@
import React from "react"
import { Badge, LegacyLink, Tooltip } from "@/components"
import { Badge, Link, Tooltip } from "@/components"
export type ExpandableNoticeProps = {
type: "request" | "method"
@@ -20,11 +20,8 @@ export const ExpandableNotice = ({
<>
If this {type} accepts an <code>expand</code>{" "}
{type === "request" ? "parameter" : "property or option"},
{/* TODO replace with Link once we move away from Docusaurus */}
<br /> this field can be <LegacyLink href={link}>
expanded
</LegacyLink>{" "}
into an object.
<br /> this field can be <Link href={link}>expanded</Link> into an
object.
</>
}
clickable
@@ -1,5 +1,5 @@
import React from "react"
import { Badge, LegacyLink, Tooltip } from "@/components"
import { Badge, Link, Tooltip } from "@/components"
export type FeatureFlagNoticeProps = {
featureFlag: string
@@ -22,13 +22,12 @@ export const FeatureFlagNotice = ({
<span className={tooltipTextClassName}>
To use this {type}, make sure to
<br />
{/* TODO replace with Link once we move away from Docusaurus */}
<LegacyLink
<Link
href="https://docs.medusajs.com/development/feature-flags/toggle"
target="__blank"
>
enable its feature flag: <code>{featureFlag}</code>
</LegacyLink>
</Link>
</span>
}
clickable
@@ -2,7 +2,7 @@
import React, { useEffect, useState } from "react"
import { request } from "@octokit/request"
import { LegacyLink } from "@/components"
import { Link } from "@/components"
export type SolutionsProps = {
feedback: boolean
@@ -69,14 +69,9 @@ export const Solutions = ({ feedback, message }: SolutionsProps) => {
<ul>
{possibleSolutions.map((solution) => (
<li key={solution.url} className="mb-docs_0.5 last:mb-0">
{/* TODO replace with Link once we move away from Docusaurus */}
<LegacyLink
href={solution.html_url}
target="_blank"
rel="noreferrer"
>
<Link href={solution.html_url} target="_blank" rel="noreferrer">
{solution.title}
</LegacyLink>
</Link>
</li>
))}
</ul>
@@ -1,28 +0,0 @@
import React from "react"
import clsx from "clsx"
export type LegacyLinkProps = {
href?: string
children?: React.ReactNode
className?: string
} & React.AllHTMLAttributes<HTMLAnchorElement>
export const LegacyLink = ({
href,
children,
className,
...rest
}: LegacyLinkProps) => {
return (
<a
href={href || ""}
{...rest}
className={clsx(
"text-medusa-fg-interactive hover:text-medusa-fg-interactive-hover",
className
)}
>
{children}
</a>
)
}
@@ -7,10 +7,10 @@ import {
Note,
Card,
CardList,
LegacyLink,
DetailsSummary,
DetailsProps,
ZoomImg,
Link,
} from "@/components"
import clsx from "clsx"
import { Text } from "@medusajs/ui"
@@ -54,13 +54,12 @@ export const MDXComponents: MDXComponentsType = {
>
{children}
{props.id && (
// TODO replace with Link once we move away from Docusaurus
<LegacyLink
<Link
href={`#${props.id}`}
className="opacity-0 group-hover/h2:opacity-100 transition-opacity ml-docs_0.5 inline-block"
>
#
</LegacyLink>
</Link>
)}
</h2>
)
@@ -81,13 +80,12 @@ export const MDXComponents: MDXComponentsType = {
>
{children}
{props.id && (
// TODO replace with Link once we move away from Docusaurus
<LegacyLink
<Link
href={`#${props.id}`}
className="opacity-0 group-hover/h3:opacity-100 transition-opacity ml-docs_0.5 inline-block"
>
#
</LegacyLink>
</Link>
)}
</h3>
)
@@ -1,7 +1,7 @@
import React from "react"
import ReactMarkdown from "react-markdown"
import { ReactMarkdownOptions } from "react-markdown/lib/react-markdown"
import { MDXComponents, LegacyLink } from "@/components"
import { MDXComponents, Link } from "@/components"
import clsx from "clsx"
import { NormalComponents } from "react-markdown/lib/complex-types"
import { SpecialComponents } from "react-markdown/lib/ast-to-react"
@@ -33,8 +33,7 @@ export const MarkdownContent = ({
</pre>
)
},
// TODO replace with Link once we move away from Docusaurus
a: LegacyLink,
a: Link,
}
}
{...props}
@@ -48,8 +48,6 @@ export const MermaidDiagram = ({ diagramContent }: MermaidDiagramProps) => {
setIsZoomed(shouldZoom)
}, [])
console.log(matchedRegex, result?.svg)
return (
<Suspense fallback={<Loading />}>
<ControlledZoom
@@ -18,6 +18,7 @@ export const NoteLayout = ({
type === "error" ||
type === "check"
const isWarningStyle = type === "warning"
const isSoonStyle = type === "soon"
return (
<div
@@ -26,6 +27,7 @@ export const NoteLayout = ({
isDefaultStyle &&
"bg-medusa-tag-neutral-bg border-medusa-tag-neutral-border",
isWarningStyle && "bg-medusa-tag-red-bg border-medusa-tag-red-border",
isSoonStyle && "bg-medusa-tag-blue-bg border-medusa-tag-blue-border",
"[&_a]:no-underline [&_a]:text-medusa-fg-interactive hover:[&_a]:text-medusa-fg-interactive-hover ",
"mb-docs_2 alert"
)}
@@ -36,6 +38,7 @@ export const NoteLayout = ({
className={clsx(
isDefaultStyle && "text-medusa-tag-neutral-text",
isWarningStyle && "text-medusa-tag-red-text",
isSoonStyle && "text-medusa-tag-blue-text",
"text-medium flex-1 [&>*:last-child]:mb-0",
"[&>p>code]:px-docs_0.5 [&>p>code]:text-code-label"
)}
@@ -45,7 +48,8 @@ export const NoteLayout = ({
className={clsx(
"text-compact-medium-plus block mb-docs_0.125",
isDefaultStyle && "text-medusa-fg-base",
isWarningStyle && "text-medusa-tag-red-text"
isWarningStyle && "text-medusa-tag-red-text",
isSoonStyle && "text-medusa-tag-blue-text"
)}
>
{title}
@@ -0,0 +1,27 @@
import React from "react"
import { NoteProps } from ".."
import { NoteLayout } from "../Layout"
import { LightBulb } from "@medusajs/icons"
import clsx from "clsx"
export const SoonNote = ({
title = "Coming soon",
icon,
...props
}: NoteProps) => {
return (
<NoteLayout
title={title}
icon={
icon || (
<LightBulb
className={clsx(
"inline-block mr-docs_0.125 text-medusa-tag-blue-icon"
)}
/>
)
}
{...props}
/>
)
}
@@ -4,9 +4,10 @@ import { DefaultNote } from "./Types/default"
import { SuccessNote } from "./Types/sucess"
import { ErrorNote } from "./Types/error"
import { CheckNote } from "./Types/checks"
import { SoonNote } from "./Types/soon"
export type NoteProps = {
type?: "default" | "warning" | "success" | "error" | "check"
type?: "default" | "warning" | "success" | "error" | "check" | "soon"
title?: string
children?: React.ReactNode
icon?: React.ReactNode
@@ -22,6 +23,8 @@ export const Note = ({ type = "default", ...props }: NoteProps) => {
return <ErrorNote type={type} {...props} />
case "check":
return <CheckNote type={type} {...props} />
case "soon":
return <SoonNote type={type} {...props} />
default:
return <DefaultNote type={type} {...props} />
}
@@ -16,7 +16,7 @@ export const Pagination = () => {
startIcon={<ChevronLeft />}
showLinkIcon={false}
href={previousPage.link}
className="max-w-[45%] ml-0 mr-auto"
className="max-w-[45%] ml-0 mr-auto items-center"
/>
)}
{nextPage && (
@@ -25,7 +25,7 @@ export const Pagination = () => {
endIcon={<ChevronRight />}
showLinkIcon={false}
href={nextPage.link}
className="max-w-[45%] mr-0 ml-auto"
className="max-w-[45%] mr-0 ml-auto items-center"
/>
)}
</div>
@@ -13,7 +13,7 @@ import {
import { SearchNoResult } from "../NoResults"
import { SearchHitGroupName } from "./GroupName"
import { useSearch } from "@/providers"
import { LegacyLink } from "@/components"
import { Link } from "@/components"
export type Hierarchy = "lvl0" | "lvl1" | "lvl2" | "lvl3" | "lvl4" | "lvl5"
@@ -205,8 +205,7 @@ export const SearchHits = ({
/>
</span>
)}
{/* TODO replace with Link once we move away from Docusaurus */}
<LegacyLink
<Link
href={item.url}
className="absolute top-0 left-0 h-full w-full"
target="_self"
@@ -4,23 +4,40 @@ import React from "react"
import { Tabs as UiTabs } from "@medusajs/ui"
import { ComponentProps } from "react"
import clsx from "clsx"
import { Key } from "types"
export const Tabs = (props: ComponentProps<typeof UiTabs> & Key) => (
<UiTabs {...props} />
type TabsProps = ComponentProps<typeof UiTabs> & {
layoutType?: "horizontal" | "vertical"
}
export const Tabs = ({
layoutType = "horizontal",
className,
...props
}: TabsProps) => (
<UiTabs
{...props}
className={clsx(
className,
layoutType === "vertical" && [
"flex gap-docs_1",
"[&_[role=tablist]]:flex-col [&_[role=tablist]]:items-start",
"[&_[role=tablist]+*]:flex-1 [&_[role=tablist]+*]:!mt-0",
]
)}
/>
)
export const TabsList = ({
className,
...props
}: ComponentProps<typeof UiTabs.List> & Key) => (
}: ComponentProps<typeof UiTabs.List>) => (
<UiTabs.List {...props} className={clsx(className, "gap-docs_0.5")} />
)
export const TabsTrigger = ({
className,
...props
}: ComponentProps<typeof UiTabs.Trigger> & Key) => (
}: ComponentProps<typeof UiTabs.Trigger>) => (
<UiTabs.Trigger
{...props}
className={clsx(
@@ -43,6 +60,6 @@ export const TabsContentWrapper = ({
<div {...props} className={clsx(className, "mt-docs_0.5")} />
)
export const TabsContent = (
props: ComponentProps<typeof UiTabs.Content> & Key
) => <UiTabs.Content {...props} />
export const TabsContent = (props: ComponentProps<typeof UiTabs.Content>) => (
<UiTabs.Content {...props} />
)
@@ -1,12 +1,11 @@
import { Switch } from "@medusajs/ui"
import clsx from "clsx"
import React from "react"
import { Key } from "types"
export const Toggle = ({
className,
...props
}: React.ComponentProps<typeof Switch> & Key) => {
}: React.ComponentProps<typeof Switch>) => {
return (
<Switch
{...props}
@@ -20,6 +20,7 @@ import {
TriangleRightMini,
} from "@medusajs/icons"
import { decodeStr, isInView } from "@/utils"
import { usePathname } from "next/navigation"
import { useIsBrowser } from "../../.."
type CommonProps = ParentCommonProps & {
@@ -39,9 +40,9 @@ const TypeListItem = ({
elementKey,
sectionTitle,
siteUrl = "",
pathname,
}: TypeListItemProps) => {
const isBrowser = useIsBrowser()
const pathname = usePathname()
const groupName = useMemo(() => {
switch (level) {
@@ -6,8 +6,6 @@ export type CommonProps = {
expandUrl?: string
sectionTitle?: string
siteUrl?: string
// TODO replace this with inferring pathname when using Next.js only
pathname?: string
}
export type Type = {
@@ -35,7 +33,6 @@ export const TypeList = ({
siteUrl,
sectionTitle,
expandUrl,
pathname,
...props
}: ParameterTypesType) => {
return (
@@ -52,7 +49,6 @@ export const TypeList = ({
expandUrl={expandUrl}
sectionTitle={sectionTitle}
siteUrl={siteUrl}
pathname={pathname}
/>
</Suspense>
</div>
@@ -30,7 +30,6 @@ export * from "./Kbd"
export * from "./Label"
export * from "./LearningPath"
export * from "./LegacyCodeTabs"
export * from "./LegacyLink"
export * from "./Link"
export * from "./Loading"
export * from "./Loading/Dots"
+94
View File
@@ -12,6 +12,12 @@ export const navbarItems: NavbarLinkProps[] = [
rel: "noreferrer",
href: `/`,
},
{
label: "Resources",
target: "_blank",
rel: "noreferrer",
href: `/resources`,
},
{
label: "User Guide",
target: "_blank",
@@ -38,6 +44,94 @@ export const navbarItems: NavbarLinkProps[] = [
},
]
/**
* TODO Uncomment for V2
*
* export const navbarItems: NavbarLinkProps[] = [
{
label: "Docs",
target: "_blank",
rel: "noreferrer",
href: `/`,
activeValuePattern: /^\/(?!api\/|ui\/|user-guide\/|resources\/)/,
},
{
label: "User Guide",
target: "_blank",
rel: "noreferrer",
href: `/user-guide`,
},
{
label: "Store API",
target: "_blank",
rel: "noreferrer",
href: `/api/store`,
},
{
label: "Admin API",
target: "_blank",
rel: "noreferrer",
href: `/api/admin`,
},
{
label: "UI",
target: "_blank",
rel: "noreferrer",
href: `/ui`,
activeValuePattern: /^\/ui\//,
},
{
label: "V2 Docs",
target: "_blank",
rel: "noreferrer",
href: `/v2`,
},
]
export const navbarItemsV2: NavbarLinkProps[] = [
{
label: "Docs",
target: "_blank",
rel: "noreferrer",
href: `/v2`,
activeValuePattern: /^\/v2\/(?!api\/|ui\/|user-guide\/|resources\/)/,
},
{
label: "Resources",
target: "_blank",
rel: "noreferrer",
href: `/v2/resources`,
activeValuePattern: /^\/v2\/resources\//,
},
{
label: "User Guide",
target: "_blank",
rel: "noreferrer",
href: `/v2/user-guide`,
activeValuePattern: /^\/v2\/user-guide/,
},
{
label: "Store API",
target: "_blank",
rel: "noreferrer",
href: `/api/store`,
},
{
label: "Admin API",
target: "_blank",
rel: "noreferrer",
href: `/api/admin`,
},
{
label: "UI",
target: "_blank",
rel: "noreferrer",
href: `/ui`,
activeValuePattern: /^\/ui\//,
},
]
*/
export const mobileSidebarItems: SidebarItemType[] = [
{
title: "Docs",
@@ -2,7 +2,7 @@ import { ApiTestingOptions } from "types"
export type useRequestRunnerProps = {
pushLog: (...message: string[]) => void
onFinish: (message: string) => void
onFinish: (message: string, statusCode: string) => void
replaceLog?: (message: string) => void
}
@@ -28,6 +28,8 @@ export const useRequestRunner = ({
).toString()}`
}
let responseCode = ""
fetch(requestUrl, {
method: apiRequestOptions.method,
headers: {
@@ -37,7 +39,10 @@ export const useRequestRunner = ({
? JSON.stringify(apiRequestOptions.bodyData)
: undefined,
})
.then(async (response) => response.json())
.then(async (response) => {
responseCode = `${response.status}`
return response.json()
})
.then((data) => {
const stringifiedData = JSON.stringify(data, undefined, 2)
replaceLog ? replaceLog(stringifiedData) : pushLog(stringifiedData)
@@ -50,7 +55,7 @@ export const useRequestRunner = ({
`ADMIN_CORS=http://localhost:7001,https://docs.medusajs.com`
)
})
.finally(() => onFinish(`Finished running request.`))
.finally(() => onFinish(`Finished running request.`, responseCode))
}
return {
@@ -0,0 +1,53 @@
"use client"
import React, { createContext, useContext, useState } from "react"
import { DocsConfig } from "types"
export type SiteConfigContextType = {
config: DocsConfig
setConfig: React.Dispatch<React.SetStateAction<DocsConfig>>
}
const SiteConfigContext = createContext<SiteConfigContextType | null>(null)
export type SiteConfigProviderProps = {
config?: DocsConfig
children?: React.ReactNode
}
export const SiteConfigProvider = ({
config: initConfig,
children,
}: SiteConfigProviderProps) => {
const [config, setConfig] = useState<DocsConfig>(
initConfig || {
baseUrl: "",
sidebar: {
top: [],
bottom: [],
mobile: [],
},
}
)
return (
<SiteConfigContext.Provider
value={{
config,
setConfig,
}}
>
{children}
</SiteConfigContext.Provider>
)
}
export const useSiteConfig = (): SiteConfigContextType => {
const context = useContext(SiteConfigContext)
if (!context) {
throw new Error("useSiteConfig must be used inside a SiteConfigProvider")
}
return context
}
@@ -10,3 +10,4 @@ export * from "./PageLoading"
export * from "./Pagination"
export * from "./Search"
export * from "./Sidebar"
export * from "./SiteConifg"
@@ -513,344 +513,6 @@ const paths: LearningPathType[] = [
},
},
},
// TODO: Eventually remove these learning paths
{
name: "rbac",
label: "Role-based access control (RBAC)",
description: "Implement roles and permissions for admin users in Medusa",
steps: [
{
title: "Create Role and Permission Entities",
path: "/development/entities/create",
description:
"When implementing RBAC, you typically require the availability of roles and permissions, both of which would require new entities. A role would include different permissions, such as the ability to access the products’ route, and it can be assigned to one or more users.",
},
{
title: "Extend Entities",
path: "/development/entities/extend-entity",
descriptionJSX: (
<>
To associate roles with users, you need to extend the{" "}
<code>User</code> entity to add the relation between it and the new{" "}
<code>Role</code> entity. You can also extend other entities that
are associated with your custom one, such as the <code>Store</code>{" "}
entity.
</>
),
},
{
title: "Create Guard Middleware",
path: "/development/api-routes/add-middleware",
description:
"To ensure that users who have the privilege can access an API Route, you must create a middleware that guards admin routes. This middleware will run on all authenticated admin requests to ensure that only allowed users can access an API Route.",
},
{
title: "Create Services",
path: "/development/services/create-service",
descriptionJSX: (
<>
For every entity you create, such as the <code>Role</code> and{" "}
<code>Permission</code> entities, you must create a service that
provides create, read, update, and delete (CRUD) functionalities at
the very least.
<br />
If you also extended entities, such as the <code>User</code> entity,
you may need to{" "}
<Link href="/development/services/extend-service">
extend its core service
</Link>{" "}
<code>UserService</code> as well to perform custom functionalities
related to your implementation.
</>
),
},
{
title: "Create API Routes",
path: "/development/api-routes/create",
descriptionJSX: (
<>
To manage the roles and permissions, you’ll need to create custom
API Routes, typically for Create, Read, Update, and Delete (CRUD)
operations.
<br />
After creating the API Routes, you may test adding roles and
permissions, and how they provide different access for different
roles and users.
</>
),
},
],
finish: {
type: "rating",
step: {
title: "Congratulations on implementing RBAC!",
description: "Please rate your experience using this recipe.",
eventName: "rating_path_rbac",
},
},
},
{
name: "entity-and-api",
label: "Create Entity and Expose it with API Routes",
description:
"Learn how to create a new table in your database, then create API Routes to expose and manipulate its data.",
steps: [
{
title: "Create entity",
path: "/development/entities/create",
description: "Create your entity, its migration, and repository.",
},
{
title: "Create service",
path: "/development/services/create-service",
description:
"A service is a class that defines helper methods for your entity. The service will be used by the API Routes to access or modify the entity's data.",
},
{
title: "Create API Routes",
path: "/development/api-routes/create",
},
],
finish: {
type: "rating",
step: {
title: "Congratulations on creating your entity and API Routes!",
description: "Please rate your experience using this recipe.",
eventName: "rating_path_entity-and-api",
},
},
},
{
name: "storefront",
label: "Create a Custom Storefront",
description:
"Learn how to create a custom storefront with your preferred language or framework.",
steps: [
{
title: "Choose your client",
path: "/medusa-react/overview",
descriptionJSX: (
<>
As your storefront connect to the Medusa backend, you need a way to
interact with the backend&apos;s REST APIs. There are three ways to
do so, based on your type of project:
<ul>
<li>
<Link href="/medusa-react/overview">Medusa React</Link>: Can be
used in any React-based project. For example, in a Next.js
storefront.
</li>
<li>
<Link href="/js-client/overview">Medusa JS Client</Link>: Can be
used in any JavaScript and NPM based project. For example, in a
Nuxt storefront.
</li>
<li>
<Link href={`https://docs.medusajs.com/api/store`}>
Store REST APIs
</Link>
: You can send requests directly to the API Routes without using
Medusa&apos;s clients.
</li>
</ul>
</>
),
},
{
title: "Set CORS configurations in Backend",
path: "/development/backend/configurations#admin_cors-and-store_cors",
description:
"To ensure your storefront can connect to the backend, make sure to configure the backend's CORS configuration based on your storefront's local or remote URL.",
},
{
title: "Create a Publishable API Key",
path: "/user-guide/settings/publishable-api-keys",
descriptionJSX: (
<>
A publishable API key allows you to associate a key with a sales
channel. Then, you can include that key in the headers of all your
requests.
<br />
You can create the publishable API key from the dashboard.
Alternatively, you can create it using the{" "}
<Link href="/development/publishable-api-keys/admin/manage-publishable-api-keys">
Admin APIs
</Link>
</>
),
},
{
title: "Use Publishable API Key",
path: "/development/publishable-api-keys/storefront/use-in-requests",
description:
"After creating the publishable API key and associating it with sales channels, you can pass it in the header of your requests to Store API Routes.",
},
{
title: "Add Region Selection",
path: "/modules/regions-and-currencies/storefront/use-regions",
description:
"In your storefront, you can allow your customers to view available regions and select their current region. This can affect the prices, discounts, and shipping and payment providers available to the customer.",
},
{
title: "Display Products",
path: "/modules/products/storefront/show-products",
description: "Display products to your customers in the storefront.",
},
{
title: "Implement Cart Functionalities",
path: "/modules/carts-and-checkout/storefront/implement-cart",
description:
"Allow your customers to add items to their cart, update them, and more in preparation for checkout.",
},
{
title: "Implement Checkout Flow",
path: "/modules/carts-and-checkout/storefront/implement-checkout-flow",
description:
"Implement the checkout flow that allows customers to handle shipping and payment, then place their orders.",
},
{
title: "Implement Customer Profiles",
path: "/modules/customers/storefront/implement-customer-profiles",
description:
"Allow customers to register, login, edit their profile information, and more.",
},
{
title: "More Commerce Functionalities",
path: "/modules/overview",
descriptionJSX: (
<>
This recipe guided you to create a storefront with basic
functionalities. You can add more functionalities to your storefront
based on your use case.
<ul>
<li>
The <Link href="/modules/overview">Commerce Modules</Link>{" "}
documentation holds various storefront-related how-to guides to
help you implement different features.
</li>
<li>
You can also checkout the{" "}
<Link href={`https://docs.medusajs.com/api/store`}>
Store REST APIs
</Link>{" "}
for a full list of available REST APIs.
</li>
</ul>
</>
),
},
],
finish: {
type: "rating",
step: {
title: "Congratulations on creating your storefront!",
description: "Please rate your experience using this recipe.",
eventName: "rating_path_storefront",
},
},
},
{
name: "plugin",
label: "Create a Plugin",
description:
"Learn how to create a plugin that can be re-used across Medusa backends.",
steps: [
{
title: "Setup plugin project",
path: "/development/backend/install",
description:
"A plugin is initially a Medusa backend with customizations. If you don't have a project ready, you can create one using Medusa's CLI tool.",
},
{
title: "Implement Customizations",
path: "/development/entities/create",
descriptionJSX: (
<>
Your plugin can hold backend and admin customizations. Those
include:
<ul>
<li>
<Link href="/development/entities/create">Create Entity</Link>
</li>
<li>
<Link href="/development/services/create-service">
Create Service
</Link>
</li>
<li>
<Link href="/development/api-routes/create">
Create an API Route
</Link>
</li>
<li>
<Link href="/development/events/create-subscriber">
Create Subscriber
</Link>
</li>
<li>
<Link href="/admin/widgets">Create Admin Widgets</Link>
</li>
<li>
<Link href="/admin/routes">Create Admin Routes</Link>
</li>
<li>
<Link href="/admin/setting-pages">
Create Admin Setting Pages
</Link>
</li>
<li>
<Link href="/development/search/create">
Create Search Service
</Link>
</li>
<li>
<Link href="/development/file-service/create-file-service">
Create File Service
</Link>
</li>
<li>
<Link href="/development/notification/create-notification-provider">
Create Notification Service
</Link>
</li>
</ul>
If you&apos;ve already made your custom development, you can skip to
the next step.
</>
),
},
{
title: "Change your package.json",
path: "/development/plugins/create#changes-to-packagejson",
descriptionJSX: (
<>
Once you&apos;re done making your customizations and you&apos;re
ready to publish your plugin, make changes to your{" "}
<code>package.json</code> in preparation for publishing.
</>
),
},
{
title: "Optionally test locally",
path: "/development/plugins/create#test-your-plugin",
description:
"If necessary, you can test your plugin in a separate local Medusa backend. It's recommended, however, to do your plugin testing within the plugin project.",
},
{
title: "Publish plugin",
path: "/development/plugins/publish",
description: "Publish your plugin on NPM.",
},
],
finish: {
type: "rating",
step: {
title: "Congratulations on creating your plugin!",
description: "Please rate your experience using this recipe.",
eventName: "rating_path_plugin",
},
},
},
]
// get a shallow copy
+9 -4
View File
@@ -17,8 +17,8 @@ module.exports = {
},
plugins: ["prettier", "markdown"],
ignorePatterns: [
"**/content/references/**",
"**/content/**/events-list.md"
"**/references/**",
"**/events-reference/**"
],
rules: {
"no-undef": "off",
@@ -32,7 +32,9 @@ module.exports = {
camelcase: "off",
"no-invalid-this": "off",
"max-len": [
"error",
// TODO change back to error once we figure out
// a way to use HTML comments in new MDX
"warn",
{
code: 64,
},
@@ -75,7 +77,7 @@ module.exports = {
"space-infix-ops": "off",
"eol-last": ["error", "always"],
"react/prop-types": "off",
"react/jsx-no-undef": "off"
"react/jsx-no-undef": "off",
},
overrides: [
{
@@ -106,6 +108,9 @@ module.exports = {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-var-requires": "off",
"prefer-rest-params": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-non-null-asserted-optional-chain": "off"
},
},
],
+2 -1
View File
@@ -6,7 +6,8 @@ module.exports = {
],
"rules": {
"react/react-in-jsx-scope": "off",
"@typescript-eslint/prefer-ts-expect-error": "off"
"@typescript-eslint/prefer-ts-expect-error": "off",
"valid-jsdoc": "off"
},
ignorePatterns: [
"next.config.js",
@@ -0,0 +1,9 @@
const path = require("path")
module.exports = {
extends: ["docs"],
parserOptions: {
project: true,
tsconfigRootDir: path.join(__dirname, "..", ".."),
},
}
@@ -0,0 +1,52 @@
{
"name": "remark-rehype-plugins",
"version": "0.0.0",
"private": true,
"license": "MIT",
"publishConfig": {
"access": "public"
},
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"module": "./dist/index.js",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
},
"sideEffects": false,
"files": [
"dist/**"
],
"scripts": {
"build": "yarn clean && tsc",
"clean": "rimraf dist"
},
"dependencies": {
"@cloudinary/url-gen": "^1.17.0",
"remark-frontmatter": "^5.0.0",
"remark-parse": "^11.0.0",
"remark-stringify": "^11.0.0",
"to-vfile": "^8.0.0",
"unified": "^11.0.4",
"unist-builder": "3.0.0",
"unist-util-visit": "4.1.2",
"vfile-matter": "^5.0.0"
},
"devDependencies": {
"@types/node": "^20.11.20",
"docs-ui": "*",
"eslint-config-docs": "*",
"rimraf": "^5.0.5",
"tsconfig": "*",
"types": "*",
"typescript": "^5.3.3"
},
"engines": {
"node": ">=18.17.0"
}
}
@@ -0,0 +1,31 @@
import { existsSync } from "fs"
import path from "path"
import type { Transformer } from "unified"
import type { UnistNode, UnistTree } from "./types/index.js"
export function brokenLinkCheckerPlugin(): Transformer {
return async (tree, file) => {
const { visit } = await import("unist-util-visit")
const currentPageFilePath = file.history[0].replace(
`/${path.basename(file.history[0])}`,
""
)
visit(tree as UnistTree, "element", (node: UnistNode) => {
if (node.tagName !== "a" || !node.properties?.href?.match(/page\.mdx?/)) {
return
}
// get absolute path of the URL
const linkedFilePath = path
.resolve(currentPageFilePath, node.properties.href)
.replace(/#.*$/, "")
// check if the file exists
if (!existsSync(linkedFilePath)) {
throw new Error(
`Broken link found! ${node.properties.href} linked in ${file.history[0]}`
)
}
})
}
}
@@ -0,0 +1,112 @@
import type { Transformer } from "unified"
import type { CloudinaryConfig, UnistNode, UnistTree } from "./types/index.js"
const cloudinaryImageRegex =
/^https:\/\/res.cloudinary.com\/.*\/upload\/v[0-9]+\/(?<imageId>.*)$/
export function cloudinaryImgRehypePlugin({
cloudinaryConfig,
}: {
cloudinaryConfig: CloudinaryConfig
}): Transformer {
return async (tree) => {
if (!cloudinaryConfig) {
return
}
const { visit } = await import("unist-util-visit")
const { Cloudinary } = await import("@cloudinary/url-gen")
const { byRadius } = await import(
"@cloudinary/url-gen/actions/roundCorners"
)
const {
pad,
imaggaScale,
imaggaCrop,
crop,
fit,
minimumPad,
fill,
scale,
limitFit,
thumbnail,
limitFill,
minimumFit,
limitPad,
fillPad,
} = await import("@cloudinary/url-gen/actions/resize")
const resizeActions = {
pad,
imaggaScale,
imaggaCrop,
crop,
fit,
minimumPad,
fill,
scale,
limitFit,
thumbnail,
limitFill,
minimumFit,
limitPad,
fillPad,
}
visit(tree as UnistTree, (node: UnistNode) => {
const matchingRegex = node.properties?.src?.match(cloudinaryImageRegex)
if (node.tagName !== "img" || !matchingRegex?.groups?.imageId) {
return
}
const cloudinary = new Cloudinary({
cloud: {
cloudName: cloudinaryConfig.cloudName,
},
})
const image = cloudinary.image(
matchingRegex.groups.imageId.replaceAll("%20", " ")
)
cloudinaryConfig.flags?.forEach((flag) => image.addTransformation(flag))
if (cloudinaryConfig.roundCorners) {
image.roundCorners(byRadius(cloudinaryConfig.roundCorners))
}
if (cloudinaryConfig.resize) {
const action =
resizeActions[
cloudinaryConfig.resize.action as keyof typeof resizeActions
]
let resizeAction = action()
if (node.properties?.width || node.properties?.height) {
if (node.properties?.width) {
resizeAction = resizeAction.width(node.properties.width)
}
if (node.properties?.height) {
resizeAction = resizeAction.height(node.properties.height)
}
} else if (cloudinaryConfig.resize.aspectRatio) {
resizeAction = resizeAction.aspectRatio(
cloudinaryConfig.resize.aspectRatio
)
} else {
if (cloudinaryConfig.resize.width) {
resizeAction = resizeAction.width(cloudinaryConfig.resize.width)
}
if (cloudinaryConfig.resize.height) {
resizeAction = resizeAction.height(cloudinaryConfig.resize.height)
}
}
image.resize(resizeAction)
}
node.properties!.src = image.toURL()
})
}
}
@@ -0,0 +1,45 @@
import type { Transformer } from "unified"
import type {
CrossProjectLinksOptions,
UnistNode,
UnistTree,
} from "./types/index.js"
const PROJECT_REGEX = /^!(?<area>[\w-]+)!/
export function crossProjectLinksPlugin({
baseUrl,
projectUrls,
}: CrossProjectLinksOptions): Transformer {
return async (tree) => {
const { visit } = await import("unist-util-visit")
visit(tree as UnistTree, "element", (node: UnistNode) => {
if (node.tagName !== "a" || !node.properties?.href) {
return
}
const projectArea = PROJECT_REGEX.exec(node.properties.href)
if (!projectArea?.groups?.area) {
return
}
const actualUrl = node.properties.href.replace(PROJECT_REGEX, "")
const base =
projectUrls &&
Object.hasOwn(projectUrls, projectArea.groups.area) &&
projectUrls[projectArea.groups.area]?.url
? projectUrls[projectArea.groups.area].url
: baseUrl
const path =
projectUrls &&
Object.hasOwn(projectUrls, projectArea.groups.area) &&
projectUrls[projectArea.groups.area]?.path
? projectUrls[projectArea.groups.area].path
: projectArea.groups.area
node.properties.href = `${base}/${path}${actualUrl}`
})
}
}
@@ -0,0 +1,11 @@
export * from "./broken-link-checker.js"
export * from "./cloudinary-img.js"
export * from "./cross-project-links.js"
export * from "./local-links.js"
export * from "./page-number.js"
export * from "./resolve-admonitions.js"
export * from "./type-list-link-fixer.js"
export * from "./utils/fix-link.js"
export * from "./utils/get-file-slug.js"
export * from "./utils/get-front-matter.js"
@@ -0,0 +1,40 @@
import path from "path"
import type { Transformer } from "unified"
import type { LocalLinkOptions, UnistNode, UnistTree } from "./types/index.js"
import { fixLinkUtil } from "./index.js"
export function localLinksRehypePlugin(options: LocalLinkOptions): Transformer {
const { filePath, basePath } = options || {}
return async (tree, file) => {
if (!file.cwd) {
return
}
if (!file.history.length) {
if (!filePath) {
return
}
file.history.push(filePath)
}
const { visit } = await import("unist-util-visit")
const currentPageFilePath = file.history[0].replace(
`/${path.basename(file.history[0])}`,
""
)
const appsPath = basePath || path.join(file.cwd, "app")
visit(tree as UnistTree, "element", (node: UnistNode) => {
if (node.tagName !== "a" || !node.properties?.href?.match(/page\.mdx?/)) {
return
}
node.properties.href = fixLinkUtil({
currentPageFilePath,
linkedPath: node.properties.href,
appsPath,
})
})
}
}
@@ -0,0 +1,86 @@
import path from "path"
import type { Transformer } from "unified"
import type { RawSidebarItemType } from "types"
import type { UnistTree } from "./types/index.js"
export function pageNumberRehypePlugin({
sidebar,
}: {
sidebar: RawSidebarItemType[]
}): Transformer {
return async (tree, file) => {
const { valueToEstree } = await import("estree-util-value-to-estree")
let number = ""
if (file.history.length) {
// get page path
const pageFilePath = file.history[0].replace(
path.join(file.cwd, "app"),
""
)
let pagePath = pageFilePath.replace(`/${path.basename(pageFilePath)}`, "")
if (!pagePath.length) {
pagePath = "/"
}
const pageSidebarItem = findSidebarItem(sidebar, pagePath)
if (pageSidebarItem?.number) {
number = pageSidebarItem?.number
}
}
;(tree as UnistTree).children.unshift({
type: "mdxjsEsm",
value: `export const pageNumber = "${number}"`,
data: {
estree: {
type: "Program",
body: [
{
type: "ExportNamedDeclaration",
declaration: {
type: "VariableDeclaration",
declarations: [
{
type: "VariableDeclarator",
id: {
type: "Identifier",
name: "pageNumber",
},
init: valueToEstree(number),
},
],
kind: "const",
},
specifiers: [],
source: null,
},
],
sourceType: "module",
},
},
})
}
}
function findSidebarItem(
sidebar: RawSidebarItemType[],
path: string
): RawSidebarItemType | undefined {
let foundItem = undefined
for (const item of sidebar) {
if (path === item.path) {
foundItem = item
} else if (item.children) {
foundItem = findSidebarItem(item.children, path)
}
if (foundItem) {
break
}
}
return foundItem
}
@@ -0,0 +1,47 @@
import { Transformer } from "unified"
import { UnistNode } from "./types/index.js"
const ALLOWED_NODE_NAMES = ["note", "tip", "info", "warning"]
export function resolveAdmonitionsPlugin(): Transformer {
return async (tree) => {
const { visit } = await import("unist-util-visit")
visit(tree as UnistNode, (node: UnistNode) => {
if (
(node.type !== "containerDirective" &&
node.type !== "leafDirective" &&
node.type !== "textDirective") ||
!node.name ||
!ALLOWED_NODE_NAMES.includes(node.name)
) {
return
}
let title: string | undefined
node.children?.some((childNode, childIndex) => {
if (childNode.data?.directiveLabel && childNode.children?.length) {
title = childNode.children.find((item) => item.type === "text")?.value
if (title) {
node.children?.splice(childIndex, 1)
}
}
return title !== undefined
})
if (!node.data) {
node.data = {}
}
node.data.hName = "Note"
node.data.hProperties = {
title:
title ||
(node.name !== "note"
? `${node.name.charAt(0).toUpperCase()}${node.name.substring(1)}`
: "Note"),
}
})
}
}
@@ -0,0 +1,156 @@
import path from "path"
import { Transformer } from "unified"
import {
TypeListLinkFixerOptions,
UnistNode,
UnistNodeWithData,
UnistTree,
} from "./types/index.js"
import { FixLinkOptions, fixLinkUtil } from "./index.js"
const LINK_REGEX = /\[(.*?)\]\((?<link>.*?)\)/gm
function matchLinks(
str: string,
linkOptions: Omit<FixLinkOptions, "linkedPath">
) {
let linkMatches
while ((linkMatches = LINK_REGEX.exec(str)) !== null) {
if (!linkMatches.groups?.link) {
return
}
const newUrl = fixLinkUtil({
...linkOptions,
linkedPath: linkMatches.groups.link,
})
str = str.replace(linkMatches.groups.link, newUrl)
}
return str
}
function traverseTypes(
types: Record<string, unknown>[],
linkOptions: Omit<FixLinkOptions, "linkedPath">
) {
return types.map((typeItem) => {
typeItem.type = matchLinks(typeItem.type as string, linkOptions)
typeItem.description = matchLinks(
typeItem.description as string,
linkOptions
)
if (typeItem.children) {
typeItem.children = traverseTypes(
typeItem.children as Record<string, unknown>[],
linkOptions
)
}
return typeItem
})
}
export function typeListLinkFixerPlugin(
options?: TypeListLinkFixerOptions
): Transformer {
const { filePath, basePath } = options || {}
return async (tree, file) => {
if (!file.cwd) {
return
}
if (!file.history.length) {
if (!filePath) {
return
}
file.history.push(filePath)
}
const { visit } = await import("unist-util-visit")
const currentPageFilePath = file.history[0].replace(
`/${path.basename(file.history[0])}`,
""
)
const appsPath = basePath || path.join(file.cwd, "app")
visit(tree as UnistTree, "mdxJsxFlowElement", (node: UnistNode) => {
if (node.name !== "TypeList") {
return
}
const typesAttributeIndex = node.attributes?.findIndex(
(attribute) => attribute.name === "types"
)
if (typesAttributeIndex === undefined || typesAttributeIndex === -1) {
return
}
const typesAttribute = node.attributes![typesAttributeIndex]
if (
!typesAttribute ||
!(typesAttribute.value as Record<string, unknown>)?.value
) {
return
}
const linkOptions = {
currentPageFilePath,
appsPath,
}
let newItems: Record<string, unknown>[]
try {
newItems = traverseTypes(
JSON.parse(
(typesAttribute.value as Record<string, unknown>).value as string
) as Record<string, unknown>[],
linkOptions
)
} catch (e) {
// eslint-disable-next-line no-console
console.log(
`[type-list-link-fixer-plugin]: An error occurred while parsing items for page ${file.history[0]}: ${e}`
)
return
}
;(
node.attributes![typesAttributeIndex].value as Record<string, unknown>
).value = JSON.stringify(newItems)
if (
(node as UnistNodeWithData).attributes![typesAttributeIndex].value?.data
?.estree?.body?.length
) {
const oldItems = (node as UnistNodeWithData).attributes[
typesAttributeIndex
].value.data!.estree!.body![0].expression!.elements!
;(node as UnistNodeWithData).attributes[
typesAttributeIndex
].value.data!.estree!.body![0].expression!.elements = newItems.map(
(newItem, index) => {
oldItems[index].properties = oldItems[index].properties.map(
(property) => {
if (Object.hasOwn(newItem, property.key.value)) {
property.value.value = newItem[property.key.value]
property.value.raw = JSON.stringify(
newItem[property.key.value]
)
}
return property
}
)
return oldItems[index]
}
)
}
})
}
}
@@ -0,0 +1,96 @@
import { Node } from "unist-builder"
export interface UnistNode extends Node {
type: string
name?: string
tagName?: string
value?: string
properties?: {
[key: string]: string
}
attributes?: {
name: string
value: unknown
type?: string
}[]
children?: UnistNode[]
}
export interface UnistNodeWithData extends UnistNode {
attributes: {
name: string
value: {
data?: {
estree?: {
body?: {
type?: string
expression?: {
type?: string
elements?: {
properties: AttributeProperty[]
}[]
}
}[]
}
}
value?: string
}
type?: string
}[]
}
export interface AttributeProperty {
key: {
value: string
raw: string
}
value: {
value: unknown
raw: string
}
}
export interface UnistTree extends Node {
children: UnistNode[]
}
export declare type CloudinaryConfig = {
cloudName?: string
flags?: string[]
resize?: {
action: string
width?: number
height?: number
aspectRatio?: string
}
roundCorners?: number
}
export declare type FrontMatter = {
slug?: string
sidebar_label?: string
sidebar_group?: string
sidebar_group_main?: boolean
sidebar_position?: number
sidebar_autogenerate_exclude?: boolean
}
export declare type CrossProjectLinksOptions = {
baseUrl: string
projectUrls?: {
[k: string]: {
url: string
path?: string
}
}
}
export declare type TypeListLinkFixerOptions = {
filePath?: string
basePath?: string
}
export declare type LocalLinkOptions = {
filePath?: string
basePath?: string
}
@@ -0,0 +1,27 @@
import path from "path"
export type FixLinkOptions = {
currentPageFilePath: string
linkedPath: string
appsPath: string
}
export function fixLinkUtil({
currentPageFilePath,
linkedPath,
appsPath: basePath,
}: FixLinkOptions) {
// get absolute path of the URL
const linkedFilePath = path
.resolve(currentPageFilePath, linkedPath)
.replace(basePath, "")
// persist hash in new URL
const hash = linkedFilePath.includes("#")
? linkedFilePath.substring(linkedFilePath.indexOf("#"))
: ""
return `${linkedFilePath.substring(
0,
linkedFilePath.indexOf(`/${path.basename(linkedFilePath)}`)
)}${hash}`
}
@@ -0,0 +1,12 @@
import { getFrontMatterUtil } from "./get-front-matter.js"
export async function getFileSlugUtil(
filePath: string
): Promise<string | undefined> {
const fileFrontmatter = await getFrontMatterUtil(filePath)
if (fileFrontmatter.slug) {
// add to slugs array
return fileFrontmatter.slug
}
}
@@ -0,0 +1,24 @@
import remarkFrontmatter from "remark-frontmatter"
import remarkParse from "remark-parse"
import remarkStringify from "remark-stringify"
import { unified } from "unified"
import { read } from "to-vfile"
import { matter } from "vfile-matter"
import { FrontMatter } from "../types/index.js"
export async function getFrontMatterUtil(
filePath: string
): Promise<FrontMatter> {
return (
await unified()
.use(remarkParse)
.use(remarkStringify)
.use(remarkFrontmatter, ["yaml"])
.use(() => {
return (tree, file) => {
matter(file)
}
})
.process(await read(filePath))
).data.matter as FrontMatter
}
@@ -0,0 +1,19 @@
{
"extends": ["tsconfig/base.json"],
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"tsBuildInfoFile": "./dist/.tsbuildinfo-client",
"noEmit": false,
"lib": ["es2022"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true,
},
"include": ["src"]
}
@@ -723,6 +723,16 @@ module.exports = {
".animate-bg-surface": {
"--animation-color": "var(--docs-bg-subtle-pressed)",
},
".code-block-highlight-dark": {
"*::selection": {
"background-color": "var(--docs-code-text-highlight)",
},
},
".code-block-highlight-light": {
"*::selection": {
"background-color": "var(--docs-bg-highlight)",
},
},
})
addComponents({
".btn-secondary-icon": {
+1
View File
@@ -6,6 +6,7 @@
"publishConfig": {
"access": "public"
},
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"module": "./dist/index.js",
+1
View File
@@ -5,4 +5,5 @@ export declare type DocsConfig = {
baseUrl: string
sidebar: SidebarSectionItemsType
filesBasePath?: string
useNextLinks?: boolean
}
-4
View File
@@ -1,7 +1,3 @@
export type Ref<T> =
| React.MutableRefObject<T | null>
| ((instance: T | null) => void)
export type Key = {
key?: string | number | null | undefined
}