diff --git a/www/apps/api-reference/markdown/admin.mdx b/www/apps/api-reference/markdown/admin.mdx index 64a1f360fa785..e2eee7359eadd 100644 --- a/www/apps/api-reference/markdown/admin.mdx +++ b/www/apps/api-reference/markdown/admin.mdx @@ -832,7 +832,7 @@ If you click on the workflow, you'll view a reference of that workflow, includin This is useful if you want to extend an API route and pass additional data or perform custom actions. -Refer to [this guide](https://docs.medusajs.com/learn/customization/extend-features/extend-create-product) to find an example of extending an API route. +Refer to [this guide](!docs!/learn/customization/extend-features/extend-create-product) to find an example of extending an API route. Just Getting Started? -Check out the [Medusa v2 Documentation](https://docs.medusajs.com). +Check out the [Medusa v2 Documentation](!docs!). @@ -16,7 +16,7 @@ To use Medusa's JS SDK library, install the following packages in your project ( npm install @medusajs/js-sdk@latest @medusajs/types@latest ``` -Learn more about the JS SDK in [this documentation](https://docs.medusajs.com/resources/js-sdk). +Learn more about the JS SDK in [this documentation](!resources!/js-sdk). ### Download Full Reference diff --git a/www/apps/api-reference/markdown/store.mdx b/www/apps/api-reference/markdown/store.mdx index 04776d0207574..a68dc51cc718f 100644 --- a/www/apps/api-reference/markdown/store.mdx +++ b/www/apps/api-reference/markdown/store.mdx @@ -831,7 +831,7 @@ If you click on the workflow, you'll view a reference of that workflow, includin This is useful if you want to extend an API route and pass additional data or perform custom actions. -Refer to [this guide](https://docs.medusajs.com/learn/customization/extend-features/extend-create-product) to find an example of extending an API route. +Refer to [this guide](!docs!/learn/customization/extend-features/extend-create-product) to find an example of extending an API route. -Find a full list of the registered resources in the Medusa container and their registration key in [this reference](!resources!/resources/medusa-container-resources). You can use these resources in your custom workflows. +Find a full list of the registered resources in the Medusa container and their registration key in [this reference](!resources!/medusa-container-resources). You can use these resources in your custom workflows. \ No newline at end of file diff --git a/www/apps/book/app/learn/customization/extend-features/query-linked-records/page.mdx b/www/apps/book/app/learn/customization/extend-features/query-linked-records/page.mdx index 6bdeb92f1a36c..2449a7e5a54f1 100644 --- a/www/apps/book/app/learn/customization/extend-features/query-linked-records/page.mdx +++ b/www/apps/book/app/learn/customization/extend-features/query-linked-records/page.mdx @@ -6,7 +6,7 @@ export const metadata = { # {metadata.title} -In the previous chapters, you [defined a link](../define-link/page.mdx) between the [custom Brand Module](../../custom-features/module/page.mdx) and Medusa's [Product Module](!resources!/comerce-modules/product), then [extended the create-product flow](../extend-create-product/page.mdx) to link a product to a brand. +In the previous chapters, you [defined a link](../define-link/page.mdx) between the [custom Brand Module](../../custom-features/module/page.mdx) and Medusa's [Product Module](!resources!/commerce-modules/product), then [extended the create-product flow](../extend-create-product/page.mdx) to link a product to a brand. In this chapter, you'll learn how to retrieve a product's brand (and vice-versa) in two ways: Using Medusa's existing API route, or in customizations, such as a custom API route. diff --git a/www/apps/book/next.config.mjs b/www/apps/book/next.config.mjs index 390cead21ebd4..aa49639579e80 100644 --- a/www/apps/book/next.config.mjs +++ b/www/apps/book/next.config.mjs @@ -9,11 +9,27 @@ import { crossProjectLinksPlugin, } from "remark-rehype-plugins" import { sidebar } from "./sidebar.mjs" +import path from "path" const withMDX = mdx({ extension: /\.mdx?$/, options: { rehypePlugins: [ + [ + brokenLinkCheckerPlugin, + { + crossProjects: { + resources: { + projectPath: path.resolve("..", "resources"), + hasGeneratedSlugs: true, + }, + ui: { + projectPath: path.resolve("..", "ui"), + contentPath: "src/content/docs", + }, + }, + }, + ], [ crossProjectLinksPlugin, { @@ -37,7 +53,6 @@ const withMDX = mdx({ process.env.VERCEL_ENV === "production", }, ], - [brokenLinkCheckerPlugin], [localLinksRehypePlugin], [ rehypeMdxCodeProps, diff --git a/www/apps/resources/app/admin-components/components/forms/page.mdx b/www/apps/resources/app/admin-components/components/forms/page.mdx index 5051232fe2f5f..64a24647a5b89 100644 --- a/www/apps/resources/app/admin-components/components/forms/page.mdx +++ b/www/apps/resources/app/admin-components/components/forms/page.mdx @@ -11,7 +11,7 @@ export const metadata = { The Medusa Admin has two types of forms: 1. Create forms, created using the [FocusModal UI component](!ui!/components/focus-modal). -2. Edit or update forms, created using the [Drawer UI component](!ui!/ui/components/drawer). +2. Edit or update forms, created using the [Drawer UI component](!ui!/components/drawer). This guide explains how to create these two form types following the Medusa Admin's conventions. diff --git a/www/apps/resources/app/commerce-modules/payment/payment-flow/page.mdx b/www/apps/resources/app/commerce-modules/payment/payment-flow/page.mdx index 93f5cc3807a77..10742536de320 100644 --- a/www/apps/resources/app/commerce-modules/payment/payment-flow/page.mdx +++ b/www/apps/resources/app/commerce-modules/payment/payment-flow/page.mdx @@ -66,7 +66,7 @@ remoteLink.create({ -Learn more about the remote link in [this documentation](!docs!/advanced-development/module-links/remote-link). +Learn more about the remote link in [this documentation](!docs!/learn/advanced-development/module-links/remote-link). diff --git a/www/apps/resources/app/commerce-modules/product/guides/price-with-taxes/page.mdx b/www/apps/resources/app/commerce-modules/product/guides/price-with-taxes/page.mdx index d4903742cc27c..60c2d9701613d 100644 --- a/www/apps/resources/app/commerce-modules/product/guides/price-with-taxes/page.mdx +++ b/www/apps/resources/app/commerce-modules/product/guides/price-with-taxes/page.mdx @@ -14,7 +14,7 @@ In this document, you'll learn how to calculate a product variant's price with t You'll need the following resources for the taxes calculation: -1. [Query](!docs!/advanced-development/module-links/query) to retrieve the product's variants' prices for a context. Learn more about that in [this guide](../price/page.mdx). +1. [Query](!docs!/learn/advanced-development/module-links/query) to retrieve the product's variants' prices for a context. Learn more about that in [this guide](../price/page.mdx). 2. The Tax Module's main service to get the tax lines for each product. ```ts diff --git a/www/apps/resources/app/commerce-modules/promotion/extend/page.mdx b/www/apps/resources/app/commerce-modules/promotion/extend/page.mdx index 56b7489e75c25..cb7e5f4b00324 100644 --- a/www/apps/resources/app/commerce-modules/promotion/extend/page.mdx +++ b/www/apps/resources/app/commerce-modules/promotion/extend/page.mdx @@ -121,7 +121,7 @@ To do that, you'll consume the [promotionsCreated](/references/medusa-workflows/ -Learn more about workflow hooks in [this guide](!docs!/advanced-development/workflows/workflow-hooks). +Learn more about workflow hooks in [this guide](!docs!/learn/advanced-development/workflows/workflow-hooks). @@ -156,7 +156,7 @@ In the snippet above, you add a validation rule indicating that `custom_name` is -Learn more about additional data validation in [this guide](!docs!/advanced-development/api-routes/additional-data). +Learn more about additional data validation in [this guide](!docs!/learn/advanced-development/api-routes/additional-data). @@ -208,7 +208,7 @@ In the compensation function that undoes the step's actions in case of an error, -Learn more about compensation functions in [this guide](!docs!/advanced-development/workflows/compensation-function). +Learn more about compensation functions in [this guide](!docs!/learn/advanced-development/workflows/compensation-function). @@ -266,9 +266,9 @@ The workflow accepts as an input the created promotion and the `additional_data` In the workflow, you: -1. Use the `transform` utility to get the value of `custom_name` based on whether it's set in `additional_data`. Learn more about why you can't use conditional operators in a workflow without using `transform` in [this guide](!docs!/advanced-development/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). +1. Use the `transform` utility to get the value of `custom_name` based on whether it's set in `additional_data`. Learn more about why you can't use conditional operators in a workflow without using `transform` in [this guide](!docs!/learn/advanced-development/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). 2. Create the `Custom` record using the `createCustomStep`. -3. Use the `when-then` utility to link the promotion to the `Custom` record if it was created. Learn more about why you can't use if-then conditions in a workflow without using `when-then` in [this guide](!docs!/advanced-development/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). +3. Use the `when-then` utility to link the promotion to the `Custom` record if it was created. Learn more about why you can't use if-then conditions in a workflow without using `when-then` in [this guide](!docs!/learn/advanced-development/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). You'll next execute the workflow in the hook handler. @@ -379,7 +379,7 @@ Among the returned `promotion` object, you'll find a `custom` property which hol ### Retrieve using Query -You can also retrieve the `Custom` record linked to a promotion in your code using [Query](!docs!/advanced-development/module-links/query). +You can also retrieve the `Custom` record linked to a promotion in your code using [Query](!docs!/learn/advanced-development/module-links/query). For example: @@ -393,7 +393,7 @@ const { data: [promotion] } = await query.graph({ }) ``` -Learn more about how to use Query in [this guide](!docs!/advanced-development/module-links/query). +Learn more about how to use Query in [this guide](!docs!/learn/advanced-development/module-links/query). --- diff --git a/www/apps/resources/app/recipes/b2b/page.mdx b/www/apps/resources/app/recipes/b2b/page.mdx index 5a47fa580a03c..5b06d915a0d21 100644 --- a/www/apps/resources/app/recipes/b2b/page.mdx +++ b/www/apps/resources/app/recipes/b2b/page.mdx @@ -736,7 +736,7 @@ The Medusa Admin plugin can be extended to add widgets, new pages, and setting p icon: AcademicCapSolid, }, { - href: "!docs!/learn/advanced-development/admin/setting-pages", + href: "!docs!/learn/advanced-development/admin/ui-routes#create-settings-page", title: "Create Admin Setting Page", text: "Learn how to add new page to the Medusa Admin settings.", icon: AcademicCapSolid, diff --git a/www/apps/resources/app/recipes/subscriptions/examples/standard/page.mdx b/www/apps/resources/app/recipes/subscriptions/examples/standard/page.mdx index 7bd662c235c06..d78d629919cbc 100644 --- a/www/apps/resources/app/recipes/subscriptions/examples/standard/page.mdx +++ b/www/apps/resources/app/recipes/subscriptions/examples/standard/page.mdx @@ -2008,7 +2008,7 @@ This loops over the returned subscriptions and executes the `createSubscriptionO ### Further Reads -- [How to Create a Scheduled Job](!docs!/learn/basics/scheeduled-jobs) +- [How to Create a Scheduled Job](!docs!/learn/basics/scheduled-jobs) --- diff --git a/www/apps/resources/generated/sidebar.mjs b/www/apps/resources/generated/sidebar.mjs index c51bf4af4f279..d8899e0f635f8 100644 --- a/www/apps/resources/generated/sidebar.mjs +++ b/www/apps/resources/generated/sidebar.mjs @@ -9118,9 +9118,6 @@ export const generatedSidebar = [ } ] }, - { - "type": "separator" - }, { "loaded": true, "isPathHref": true, diff --git a/www/apps/resources/next.config.mjs b/www/apps/resources/next.config.mjs index 95a2a6dc49c2f..5065b8f3ff228 100644 --- a/www/apps/resources/next.config.mjs +++ b/www/apps/resources/next.config.mjs @@ -7,13 +7,27 @@ import { workflowDiagramLinkFixerPlugin, } from "remark-rehype-plugins" import mdxPluginOptions from "./mdx-options.mjs" +import path from "node:path" const withMDX = mdx({ extension: /\.mdx?$/, options: { rehypePlugins: [ + [ + brokenLinkCheckerPlugin, + { + crossProjects: { + docs: { + projectPath: path.resolve("..", "book"), + }, + ui: { + projectPath: path.resolve("..", "ui"), + contentPath: "src/content/docs", + }, + }, + }, + ], ...mdxPluginOptions.options.rehypePlugins, - [brokenLinkCheckerPlugin], [localLinksRehypePlugin], [typeListLinkFixerPlugin], [ diff --git a/www/apps/resources/sidebar.mjs b/www/apps/resources/sidebar.mjs index 7befd8c476ab9..7e7ec55154da8 100644 --- a/www/apps/resources/sidebar.mjs +++ b/www/apps/resources/sidebar.mjs @@ -2131,9 +2131,6 @@ export const sidebar = sidebarAttachHrefCommonOptions([ }, ], }, - { - type: "separator", - }, { type: "category", title: "General", diff --git a/www/apps/resources/utils/get-slugs.mjs b/www/apps/resources/utils/get-slugs.mjs index b3a6ef066fba5..b3e247b5bb65d 100644 --- a/www/apps/resources/utils/get-slugs.mjs +++ b/www/apps/resources/utils/get-slugs.mjs @@ -7,7 +7,7 @@ const monoRepoPath = path.resolve("..", "..", "..") /** * * @param {string} dir - The directory to search in - * @returns {Promise<{ origSlug: string; newSlug: string }[]>} + * @returns {Promise} */ export default async function getSlugs(options = {}) { let { dir, basePath = path.resolve("app"), baseSlug = basePath } = options @@ -15,7 +15,7 @@ export default async function getSlugs(options = {}) { dir = basePath } /** - * @type {{ origSlug: string; newSlug: string }[]} + * @type {import("types").SlugChange[]} */ const slugs = [] diff --git a/www/packages/remark-rehype-plugins/src/broken-link-checker.ts b/www/packages/remark-rehype-plugins/src/broken-link-checker.ts index b6c074ae4dc17..4d9eb542d0a4f 100644 --- a/www/packages/remark-rehype-plugins/src/broken-link-checker.ts +++ b/www/packages/remark-rehype-plugins/src/broken-link-checker.ts @@ -1,9 +1,253 @@ -import { existsSync } from "fs" +import { existsSync, readdirSync, readFileSync } from "fs" import path from "path" import type { Transformer } from "unified" -import type { UnistNode, UnistTree } from "./types/index.js" +import type { + BrokenLinkCheckerOptions, + UnistNode, + UnistNodeWithData, + UnistTree, +} from "./types/index.js" +import type { VFile } from "vfile" +import { parseCrossProjectLink } from "./utils/cross-project-link-utils.js" +import { SlugChange } from "types" +import getAttribute from "./utils/get-attribute.js" +import { estreeToJs } from "./utils/estree-to-js.js" +import { performActionOnLiteral } from "./utils/perform-action-on-literal.js" +import { MD_LINK_REGEX } from "./constants.js" -export function brokenLinkCheckerPlugin(): Transformer { +function getErrorMessage({ + link, + file, +}: { + link: string + file: VFile +}): string { + return `Broken link found! ${link} linked in ${file.history[0]}` +} + +function checkLocalLinkExists({ + link, + file, + currentPageFilePath, +}: { + link: string + file: VFile + currentPageFilePath: string +}) { + // get absolute path of the URL + const linkedFilePath = path + .resolve(currentPageFilePath, link) + .replace(/#.*$/, "") + // check if the file exists + if (!existsSync(linkedFilePath)) { + throw new Error( + getErrorMessage({ + link, + file, + }) + ) + } +} + +function mdxPageExists(pagePath: string): boolean { + if (!existsSync(pagePath)) { + // for projects that use a convention other than mdx + // check if an mdx file exists with the same name + if (existsSync(`${pagePath}.mdx`)) { + return true + } + return false + } + + if (existsSync(path.join(pagePath, "page.mdx"))) { + return true + } + + // for projects that use a convention other than mdx + // check if an mdx file exists with the same name + return readdirSync(pagePath).some((fileName) => fileName.endsWith(".mdx")) +} + +function componentChecker({ + node, + ...rest +}: { + node: UnistNodeWithData + file: VFile + currentPageFilePath: string + options: BrokenLinkCheckerOptions +}) { + if (!node.name) { + return + } + + let attributeName: string | undefined + + const maybeCheckAttribute = () => { + if (!attributeName) { + return + } + + const attribute = getAttribute(node, attributeName) + + if ( + !attribute || + typeof attribute.value === "string" || + !attribute.value.data?.estree + ) { + return + } + + const itemJsVar = estreeToJs(attribute.value.data.estree) + + if (!itemJsVar) { + return + } + + performActionOnLiteral(itemJsVar, (item) => { + checkLink({ + link: item.original.value as string, + ...rest, + }) + }) + } + + switch (node.name) { + case "Prerequisites": + case "CardList": + attributeName = "items" + break + case "Card": + attributeName = "href" + break + case "WorkflowDiagram": + attributeName = "workflow" + break + case "TypeList": + attributeName = "types" + break + } + + maybeCheckAttribute() +} + +function checkLink({ + link, + file, + currentPageFilePath, + options, +}: { + link: unknown | undefined + file: VFile + currentPageFilePath: string + options: BrokenLinkCheckerOptions +}) { + if (!link || typeof link !== "string") { + return + } + // try to remove hash + const hashIndex = link.lastIndexOf("#") + const likeWithoutHash = hashIndex !== -1 ? link.substring(0, hashIndex) : link + if (likeWithoutHash.match(/page\.mdx?$/)) { + checkLocalLinkExists({ + link: likeWithoutHash, + file, + currentPageFilePath, + }) + return + } + + const parsedLink = parseCrossProjectLink(likeWithoutHash) + + if (!parsedLink || !Object.hasOwn(options.crossProjects, parsedLink.area)) { + if (MD_LINK_REGEX.test(link)) { + // try fixing MDX links + let linkMatches + let tempLink = link + MD_LINK_REGEX.lastIndex = 0 + + while ((linkMatches = MD_LINK_REGEX.exec(tempLink)) !== null) { + if (!linkMatches.groups?.link) { + return + } + + checkLink({ + link: linkMatches.groups.link, + file, + currentPageFilePath, + options, + }) + + tempLink = tempLink.replace(linkMatches.groups.link, "") + // reset regex + MD_LINK_REGEX.lastIndex = 0 + } + } + return + } + + const projectOptions = options.crossProjects[parsedLink.area] + + const isReferenceLink = parsedLink.path.startsWith("/references") + const baseDir = isReferenceLink + ? "references" + : projectOptions.contentPath || "app" + const pagePath = isReferenceLink + ? parsedLink.path.replace(/^\/references/, "") + : parsedLink.path + // check if the file exists + if (mdxPageExists(path.join(projectOptions.projectPath, baseDir, pagePath))) { + return + } + + // file doesn't exist, check if slugs are enabled and generated + const generatedSlugsPath = path.join( + projectOptions.projectPath, + "generated", + "slug-changes.mjs" + ) + if (!projectOptions.hasGeneratedSlugs || !existsSync(generatedSlugsPath)) { + throw new Error( + getErrorMessage({ + link, + file, + }) + ) + } + + // get slugs from file + const generatedSlugContent = readFileSync(generatedSlugsPath, "utf-8") + const slugChanges: SlugChange[] = JSON.parse( + generatedSlugContent.substring(generatedSlugContent.indexOf("[")) + ) + const slugChange = slugChanges.find( + (change) => change.newSlug === parsedLink.path + ) + + if ( + !slugChange || + !mdxPageExists(path.join(projectOptions.projectPath, slugChange.origSlug)) + ) { + throw new Error( + getErrorMessage({ + link, + file, + }) + ) + } +} + +const allowedComponentNames = [ + "Card", + "CardList", + "Prerequisites", + "WorkflowDiagram", + "TypeList", +] + +export function brokenLinkCheckerPlugin( + options: BrokenLinkCheckerOptions +): Transformer { return async (tree, file) => { const { visit } = await import("unist-util-visit") @@ -12,20 +256,26 @@ export function brokenLinkCheckerPlugin(): Transformer { "" ) - visit(tree as UnistTree, "element", (node: UnistNode) => { - if (node.tagName !== "a" || !node.properties?.href?.match(/page\.mdx?/)) { - return + visit( + tree as UnistTree, + ["element", "mdxJsxFlowElement"], + (node: UnistNode) => { + if (node.tagName === "a" && node.properties?.href) { + checkLink({ + link: node.properties.href, + file, + currentPageFilePath, + options, + }) + } else if (node.name && allowedComponentNames.includes(node.name)) { + componentChecker({ + node: node as UnistNodeWithData, + file, + currentPageFilePath, + options, + }) + } } - // 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]}` - ) - } - }) + ) } } diff --git a/www/packages/remark-rehype-plugins/src/constants.ts b/www/packages/remark-rehype-plugins/src/constants.ts new file mode 100644 index 0000000000000..934cda4618ed3 --- /dev/null +++ b/www/packages/remark-rehype-plugins/src/constants.ts @@ -0,0 +1 @@ +export const MD_LINK_REGEX = /\[(.*?)\]\((?(![a-z]+!|\.).*?)\)/gm diff --git a/www/packages/remark-rehype-plugins/src/cross-project-links.ts b/www/packages/remark-rehype-plugins/src/cross-project-links.ts index 407dda58fcff0..3fe859358c3a3 100644 --- a/www/packages/remark-rehype-plugins/src/cross-project-links.ts +++ b/www/packages/remark-rehype-plugins/src/cross-project-links.ts @@ -1,18 +1,13 @@ -/* eslint-disable no-case-declarations */ import type { Transformer } from "unified" import type { CrossProjectLinksOptions, - ExpressionJsVar, UnistNode, UnistNodeWithData, UnistTree, } from "./types/index.js" import { estreeToJs } from "./utils/estree-to-js.js" import getAttribute from "./utils/get-attribute.js" -import { - isExpressionJsVarLiteral, - isExpressionJsVarObj, -} from "./utils/expression-is-utils.js" +import { performActionOnLiteral } from "./utils/perform-action-on-literal.js" const PROJECT_REGEX = /^!(?[\w-]+)!/ @@ -61,89 +56,65 @@ function componentFixer( return } - const fixProperty = (item: ExpressionJsVar) => { - if (!isExpressionJsVarObj(item)) { + let attributeName: string | undefined + + const maybeCheckAttribute = () => { + if (!attributeName) { return } - Object.entries(item).forEach(([key, value]) => { - if ( - (key !== "href" && key !== "link") || - !isExpressionJsVarLiteral(value) - ) { - return - } + const attribute = getAttribute(node, attributeName) + + if ( + !attribute || + typeof attribute.value === "string" || + !attribute.value.data?.estree + ) { + return + } + + const itemJsVar = estreeToJs(attribute.value.data.estree) + + if (!itemJsVar) { + return + } - value.original.value = matchAndFixLinks( - value.original.value as string, + performActionOnLiteral(itemJsVar, (item) => { + item.original.value = matchAndFixLinks( + item.original.value as string, options ) - value.original.raw = JSON.stringify(value.original.value) + item.original.raw = JSON.stringify(item.original.value) }) } switch (node.name) { case "CardList": - const itemsAttribute = getAttribute(node, "items") - - if ( - !itemsAttribute?.value || - typeof itemsAttribute.value === "string" || - !itemsAttribute.value.data?.estree - ) { - return - } - - const jsVar = estreeToJs(itemsAttribute.value.data.estree) - - if (!jsVar) { - return - } - - if (Array.isArray(jsVar)) { - jsVar.forEach(fixProperty) - } else { - fixProperty(jsVar) - } - return - case "Card": - const hrefAttribute = getAttribute(node, "href") - - if (!hrefAttribute?.value || typeof hrefAttribute.value !== "string") { - return - } - - hrefAttribute.value = matchAndFixLinks(hrefAttribute.value, options) - - return case "Prerequisites": - const prerequisitesItemsAttribute = getAttribute(node, "items") - - if ( - !prerequisitesItemsAttribute?.value || - typeof prerequisitesItemsAttribute.value === "string" || - !prerequisitesItemsAttribute.value.data?.estree - ) { - return - } - - const prerequisitesJsVar = estreeToJs( - prerequisitesItemsAttribute.value.data.estree - ) - - if (!prerequisitesJsVar) { - return - } - - if (Array.isArray(prerequisitesJsVar)) { - prerequisitesJsVar.forEach(fixProperty) - } else { - fixProperty(prerequisitesJsVar) - } - return + attributeName = "items" + break + case "Card": + attributeName = "href" + break + case "WorkflowDiagram": + attributeName = "workflow" + break + case "TypeList": + attributeName = "types" + break } + + maybeCheckAttribute() } +const allowedComponentNames = [ + "Card", + "CardList", + "Prerequisites", + "WorkflowDiagram", + "TypeList", +] + export function crossProjectLinksPlugin( options: CrossProjectLinksOptions ): Transformer { @@ -155,9 +126,7 @@ export function crossProjectLinksPlugin( ["element", "mdxJsxFlowElement"], (node: UnistNode) => { const isComponent = - node.name === "Card" || - node.name === "CardList" || - node.name === "Prerequisites" + node.name && allowedComponentNames.includes(node.name) const isLink = node.tagName === "a" && node.properties?.href if (!isComponent && !isLink) { return diff --git a/www/packages/remark-rehype-plugins/src/types/index.ts b/www/packages/remark-rehype-plugins/src/types/index.ts index 5cffb878ac8ce..faaad3a18c14a 100644 --- a/www/packages/remark-rehype-plugins/src/types/index.ts +++ b/www/packages/remark-rehype-plugins/src/types/index.ts @@ -118,6 +118,16 @@ export declare type CrossProjectLinksOptions = { useBaseUrl?: boolean } +export declare type BrokenLinkCheckerOptions = { + crossProjects: { + [k: string]: { + projectPath: string + contentPath?: string + hasGeneratedSlugs?: boolean + } + } +} + export declare type ComponentLinkFixerLinkType = "md" | "value" export declare type ComponentLinkFixerOptions = { diff --git a/www/packages/remark-rehype-plugins/src/utils/component-link-fixer.ts b/www/packages/remark-rehype-plugins/src/utils/component-link-fixer.ts index fff48ec155ef7..085190c7f8bcc 100644 --- a/www/packages/remark-rehype-plugins/src/utils/component-link-fixer.ts +++ b/www/packages/remark-rehype-plugins/src/utils/component-link-fixer.ts @@ -1,21 +1,13 @@ import path from "path" import { Transformer } from "unified" -import { - ComponentLinkFixerLinkType, - ExpressionJsVar, - UnistNodeWithData, - UnistTree, -} from "../types/index.js" +import { UnistNodeWithData, UnistTree } from "../types/index.js" import { FixLinkOptions, fixLinkUtil } from "../index.js" import getAttribute from "../utils/get-attribute.js" import { estreeToJs } from "../utils/estree-to-js.js" -import { - isExpressionJsVarLiteral, - isExpressionJsVarObj, -} from "../utils/expression-is-utils.js" import { ComponentLinkFixerOptions } from "../types/index.js" +import { performActionOnLiteral } from "./perform-action-on-literal.js" +import { MD_LINK_REGEX } from "../constants.js" -const MD_LINK_REGEX = /\[(.*?)\]\((?(![a-z]+!|\.).*?)\)/gm const VALUE_LINK_REGEX = /^(![a-z]+!|\.)/gm function matchMdLinks( @@ -59,33 +51,6 @@ function matchValueLink( }) } -function traverseJsVar( - item: ExpressionJsVar[] | ExpressionJsVar, - linkOptions: Omit, - checkLinksType: ComponentLinkFixerLinkType -) { - const linkFn = checkLinksType === "md" ? matchMdLinks : matchValueLink - if (Array.isArray(item)) { - item.forEach((item) => traverseJsVar(item, linkOptions, checkLinksType)) - } else if (isExpressionJsVarLiteral(item)) { - item.original.value = linkFn(item.original.value as string, linkOptions) - item.original.raw = JSON.stringify(item.original.value) - } else { - Object.values(item).forEach((value) => { - if (Array.isArray(value) || isExpressionJsVarObj(value)) { - return traverseJsVar(value, linkOptions, checkLinksType) - } - - if (!isExpressionJsVarLiteral(value)) { - return - } - - value.original.value = linkFn(value.original.value as string, linkOptions) - value.original.raw = JSON.stringify(value.original.value) - }) - } -} - export function componentLinkFixer( componentName: string, attributeName: string, @@ -117,12 +82,12 @@ export function componentLinkFixer( return } - const workflowAttribute = getAttribute(node, attributeName) + const attribute = getAttribute(node, attributeName) if ( - !workflowAttribute || - typeof workflowAttribute.value === "string" || - !workflowAttribute.value.data?.estree + !attribute || + typeof attribute.value === "string" || + !attribute.value.data?.estree ) { return } @@ -132,13 +97,17 @@ export function componentLinkFixer( appsPath, } - const itemJsVar = estreeToJs(workflowAttribute.value.data.estree) + const itemJsVar = estreeToJs(attribute.value.data.estree) if (!itemJsVar) { return } - traverseJsVar(itemJsVar, linkOptions, checkLinksType) + const linkFn = checkLinksType === "md" ? matchMdLinks : matchValueLink + performActionOnLiteral(itemJsVar, (item) => { + item.original.value = linkFn(item.original.value as string, linkOptions) + item.original.raw = JSON.stringify(item.original.value) + }) }) } } diff --git a/www/packages/remark-rehype-plugins/src/utils/cross-project-link-utils.ts b/www/packages/remark-rehype-plugins/src/utils/cross-project-link-utils.ts new file mode 100644 index 0000000000000..b75cdc5de1938 --- /dev/null +++ b/www/packages/remark-rehype-plugins/src/utils/cross-project-link-utils.ts @@ -0,0 +1,21 @@ +const PROJECT_REGEX = /^!(?[\w-]+)!/ + +export const parseCrossProjectLink = ( + link: string +): + | { + area: string + path: string + } + | undefined => { + const projectArea = PROJECT_REGEX.exec(link) + + if (!projectArea?.groups?.area) { + return undefined + } + + return { + area: projectArea.groups.area, + path: link.replace(PROJECT_REGEX, ""), + } +} diff --git a/www/packages/remark-rehype-plugins/src/utils/perform-action-on-literal.ts b/www/packages/remark-rehype-plugins/src/utils/perform-action-on-literal.ts new file mode 100644 index 0000000000000..80a47c5fd55ed --- /dev/null +++ b/www/packages/remark-rehype-plugins/src/utils/perform-action-on-literal.ts @@ -0,0 +1,28 @@ +import { ExpressionJsVar, ExpressionJsVarLiteral } from "../types/index.js" +import { + isExpressionJsVarLiteral, + isExpressionJsVarObj, +} from "./expression-is-utils.js" + +export const performActionOnLiteral = ( + item: ExpressionJsVar[] | ExpressionJsVar, + action: (item: ExpressionJsVarLiteral) => void +) => { + if (Array.isArray(item)) { + item.forEach((i) => performActionOnLiteral(i, action)) + } else if (isExpressionJsVarLiteral(item)) { + action(item) + } else { + Object.values(item).forEach((value) => { + if (Array.isArray(value) || isExpressionJsVarObj(value)) { + return performActionOnLiteral(value, action) + } + + if (!isExpressionJsVarLiteral(value)) { + return + } + + action(value) + }) + } +} diff --git a/www/packages/types/src/build-scripts.ts b/www/packages/types/src/build-scripts.ts new file mode 100644 index 0000000000000..2344ca8e06d85 --- /dev/null +++ b/www/packages/types/src/build-scripts.ts @@ -0,0 +1,5 @@ +export type SlugChange = { + origSlug: string + newSlug: string + filePath: string +} diff --git a/www/packages/types/src/index.ts b/www/packages/types/src/index.ts index 8bc8df0ebfe2c..67a85c06d02fe 100644 --- a/www/packages/types/src/index.ts +++ b/www/packages/types/src/index.ts @@ -1,4 +1,5 @@ export * from "./api-testing.js" +export * from "./build-scripts.js" export * from "./config.js" export * from "./general.js" export * from "./menu.js" diff --git a/www/yarn.lock b/www/yarn.lock index fcc0d66ee1f6b..257e5a40daf97 100644 --- a/www/yarn.lock +++ b/www/yarn.lock @@ -6805,6 +6805,7 @@ __metadata: react-transition-group: ^4.4.5 rehype-mdx-code-props: ^3.0.1 rehype-slug: ^6.0.0 + remark-rehype-plugins: "*" slugify: ^1.6.6 swr: ^2.2.0 tailwind: "*"