docs: support detecting broken link cross-projects (#10483)
* docs: support detecting broken link cross-projects * remove double separators
This commit is contained in:
1 parent
a76b533604
commit
e7e36f39fb
28 files changed
+486
-160
No files matched your search
@@ -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.
|
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.
|
||||||
|
|
||||||
<Feedback
|
<Feedback
|
||||||
event="survey_api-ref"
|
event="survey_api-ref"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import DownloadFull from "@/components/DownloadFull"
|
|||||||
|
|
||||||
<H3 className="!mt-0">Just Getting Started?</H3>
|
<H3 className="!mt-0">Just Getting Started?</H3>
|
||||||
|
|
||||||
Check out the [Medusa v2 Documentation](https://docs.medusajs.com).
|
Check out the [Medusa v2 Documentation](!docs!).
|
||||||
|
|
||||||
<Space bottom={8} />
|
<Space bottom={8} />
|
||||||
|
|
||||||
@@ -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
|
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
|
### Download Full Reference
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
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.
|
||||||
|
|
||||||
<Feedback
|
<Feedback
|
||||||
event="survey_api-ref"
|
event="survey_api-ref"
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import createMDX from "@next/mdx"
|
|||||||
import bundleAnalyzer from "@next/bundle-analyzer"
|
import bundleAnalyzer from "@next/bundle-analyzer"
|
||||||
import rehypeMdxCodeProps from "rehype-mdx-code-props"
|
import rehypeMdxCodeProps from "rehype-mdx-code-props"
|
||||||
import rehypeSlug from "rehype-slug"
|
import rehypeSlug from "rehype-slug"
|
||||||
|
import {
|
||||||
|
brokenLinkCheckerPlugin,
|
||||||
|
crossProjectLinksPlugin,
|
||||||
|
} from "remark-rehype-plugins"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
@@ -28,6 +33,48 @@ const nextConfig = {
|
|||||||
const withMDX = createMDX({
|
const withMDX = createMDX({
|
||||||
options: {
|
options: {
|
||||||
rehypePlugins: [
|
rehypePlugins: [
|
||||||
|
[
|
||||||
|
brokenLinkCheckerPlugin,
|
||||||
|
{
|
||||||
|
crossProjects: {
|
||||||
|
docs: {
|
||||||
|
projectPath: path.resolve("..", "book"),
|
||||||
|
},
|
||||||
|
resources: {
|
||||||
|
projectPath: path.resolve("..", "resources"),
|
||||||
|
hasGeneratedSlugs: true,
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
projectPath: path.resolve("..", "ui"),
|
||||||
|
contentPath: "src/content/docs",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
crossProjectLinksPlugin,
|
||||||
|
{
|
||||||
|
baseUrl: process.env.NEXT_PUBLIC_BASE_URL,
|
||||||
|
projectUrls: {
|
||||||
|
docs: {
|
||||||
|
url: process.env.NEXT_PUBLIC_DOCS_URL,
|
||||||
|
path: "",
|
||||||
|
},
|
||||||
|
resources: {
|
||||||
|
url: process.env.NEXT_PUBLIC_RESOURCES_URL,
|
||||||
|
},
|
||||||
|
"user-guide": {
|
||||||
|
url: process.env.NEXT_PUBLIC_USER_GUIDE_URL,
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
url: process.env.NEXT_PUBLIC_UI_URL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
useBaseUrl:
|
||||||
|
process.env.NODE_ENV === "production" ||
|
||||||
|
process.env.VERCEL_ENV === "production",
|
||||||
|
},
|
||||||
|
],
|
||||||
[
|
[
|
||||||
rehypeMdxCodeProps,
|
rehypeMdxCodeProps,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
"rehype-mdx-code-props": "^3.0.1",
|
"rehype-mdx-code-props": "^3.0.1",
|
||||||
"rehype-slug": "^6.0.0",
|
"rehype-slug": "^6.0.0",
|
||||||
|
"remark-rehype-plugins": "*",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"swr": "^2.2.0",
|
"swr": "^2.2.0",
|
||||||
"tailwind": "*",
|
"tailwind": "*",
|
||||||
|
|||||||
@@ -288,6 +288,6 @@ You can now execute this workflow in a custom API route, scheduled job, or subsc
|
|||||||
|
|
||||||
<Note title="Tip">
|
<Note title="Tip">
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
</Note>
|
</Note>
|
||||||
@@ -6,7 +6,7 @@ export const metadata = {
|
|||||||
|
|
||||||
# {metadata.title}
|
# {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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,27 @@ import {
|
|||||||
crossProjectLinksPlugin,
|
crossProjectLinksPlugin,
|
||||||
} from "remark-rehype-plugins"
|
} from "remark-rehype-plugins"
|
||||||
import { sidebar } from "./sidebar.mjs"
|
import { sidebar } from "./sidebar.mjs"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
const withMDX = mdx({
|
const withMDX = mdx({
|
||||||
extension: /\.mdx?$/,
|
extension: /\.mdx?$/,
|
||||||
options: {
|
options: {
|
||||||
rehypePlugins: [
|
rehypePlugins: [
|
||||||
|
[
|
||||||
|
brokenLinkCheckerPlugin,
|
||||||
|
{
|
||||||
|
crossProjects: {
|
||||||
|
resources: {
|
||||||
|
projectPath: path.resolve("..", "resources"),
|
||||||
|
hasGeneratedSlugs: true,
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
projectPath: path.resolve("..", "ui"),
|
||||||
|
contentPath: "src/content/docs",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
[
|
[
|
||||||
crossProjectLinksPlugin,
|
crossProjectLinksPlugin,
|
||||||
{
|
{
|
||||||
@@ -37,7 +53,6 @@ const withMDX = mdx({
|
|||||||
process.env.VERCEL_ENV === "production",
|
process.env.VERCEL_ENV === "production",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[brokenLinkCheckerPlugin],
|
|
||||||
[localLinksRehypePlugin],
|
[localLinksRehypePlugin],
|
||||||
[
|
[
|
||||||
rehypeMdxCodeProps,
|
rehypeMdxCodeProps,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const metadata = {
|
|||||||
The Medusa Admin has two types of forms:
|
The Medusa Admin has two types of forms:
|
||||||
|
|
||||||
1. Create forms, created using the [FocusModal UI component](!ui!/components/focus-modal).
|
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.
|
This guide explains how to create these two form types following the Medusa Admin's conventions.
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ remoteLink.create({
|
|||||||
|
|
||||||
<Note title="Tip">
|
<Note title="Tip">
|
||||||
|
|
||||||
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).
|
||||||
|
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
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.
|
2. The Tax Module's main service to get the tax lines for each product.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ To do that, you'll consume the [promotionsCreated](/references/medusa-workflows/
|
|||||||
|
|
||||||
<Note title="Tip">
|
<Note title="Tip">
|
||||||
|
|
||||||
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).
|
||||||
|
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@ In the snippet above, you add a validation rule indicating that `custom_name` is
|
|||||||
|
|
||||||
<Note title="Tip">
|
<Note title="Tip">
|
||||||
|
|
||||||
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).
|
||||||
|
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
@@ -208,7 +208,7 @@ In the compensation function that undoes the step's actions in case of an error,
|
|||||||
|
|
||||||
<Note title="Tip">
|
<Note title="Tip">
|
||||||
|
|
||||||
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).
|
||||||
|
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
@@ -266,9 +266,9 @@ The workflow accepts as an input the created promotion and the `additional_data`
|
|||||||
|
|
||||||
In the workflow, you:
|
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`.
|
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.
|
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
|
### 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:
|
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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -736,7 +736,7 @@ The Medusa Admin plugin can be extended to add widgets, new pages, and setting p
|
|||||||
icon: AcademicCapSolid,
|
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",
|
title: "Create Admin Setting Page",
|
||||||
text: "Learn how to add new page to the Medusa Admin settings.",
|
text: "Learn how to add new page to the Medusa Admin settings.",
|
||||||
icon: AcademicCapSolid,
|
icon: AcademicCapSolid,
|
||||||
|
|||||||
@@ -2008,7 +2008,7 @@ This loops over the returned subscriptions and executes the `createSubscriptionO
|
|||||||
|
|
||||||
### Further Reads
|
### Further Reads
|
||||||
|
|
||||||
- [How to Create a Scheduled Job](!docs!/learn/basics/scheeduled-jobs)
|
- [How to Create a Scheduled Job](!docs!/learn/basics/scheduled-jobs)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -9118,9 +9118,6 @@ export const generatedSidebar = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"type": "separator"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"loaded": true,
|
"loaded": true,
|
||||||
"isPathHref": true,
|
"isPathHref": true,
|
||||||
|
|||||||
@@ -7,13 +7,27 @@ import {
|
|||||||
workflowDiagramLinkFixerPlugin,
|
workflowDiagramLinkFixerPlugin,
|
||||||
} from "remark-rehype-plugins"
|
} from "remark-rehype-plugins"
|
||||||
import mdxPluginOptions from "./mdx-options.mjs"
|
import mdxPluginOptions from "./mdx-options.mjs"
|
||||||
|
import path from "node:path"
|
||||||
|
|
||||||
const withMDX = mdx({
|
const withMDX = mdx({
|
||||||
extension: /\.mdx?$/,
|
extension: /\.mdx?$/,
|
||||||
options: {
|
options: {
|
||||||
rehypePlugins: [
|
rehypePlugins: [
|
||||||
|
[
|
||||||
|
brokenLinkCheckerPlugin,
|
||||||
|
{
|
||||||
|
crossProjects: {
|
||||||
|
docs: {
|
||||||
|
projectPath: path.resolve("..", "book"),
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
projectPath: path.resolve("..", "ui"),
|
||||||
|
contentPath: "src/content/docs",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
...mdxPluginOptions.options.rehypePlugins,
|
...mdxPluginOptions.options.rehypePlugins,
|
||||||
[brokenLinkCheckerPlugin],
|
|
||||||
[localLinksRehypePlugin],
|
[localLinksRehypePlugin],
|
||||||
[typeListLinkFixerPlugin],
|
[typeListLinkFixerPlugin],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -2131,9 +2131,6 @@ export const sidebar = sidebarAttachHrefCommonOptions([
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: "separator",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: "category",
|
type: "category",
|
||||||
title: "General",
|
title: "General",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const monoRepoPath = path.resolve("..", "..", "..")
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} dir - The directory to search in
|
* @param {string} dir - The directory to search in
|
||||||
* @returns {Promise<{ origSlug: string; newSlug: string }[]>}
|
* @returns {Promise<import("types").SlugChange[]>}
|
||||||
*/
|
*/
|
||||||
export default async function getSlugs(options = {}) {
|
export default async function getSlugs(options = {}) {
|
||||||
let { dir, basePath = path.resolve("app"), baseSlug = basePath } = options
|
let { dir, basePath = path.resolve("app"), baseSlug = basePath } = options
|
||||||
@@ -15,7 +15,7 @@ export default async function getSlugs(options = {}) {
|
|||||||
dir = basePath
|
dir = basePath
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @type {{ origSlug: string; newSlug: string }[]}
|
* @type {import("types").SlugChange[]}
|
||||||
*/
|
*/
|
||||||
const slugs = []
|
const slugs = []
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,253 @@
|
|||||||
import { existsSync } from "fs"
|
import { existsSync, readdirSync, readFileSync } from "fs"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import type { Transformer } from "unified"
|
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) => {
|
return async (tree, file) => {
|
||||||
const { visit } = await import("unist-util-visit")
|
const { visit } = await import("unist-util-visit")
|
||||||
|
|
||||||
@@ -12,20 +256,26 @@ export function brokenLinkCheckerPlugin(): Transformer {
|
|||||||
""
|
""
|
||||||
)
|
)
|
||||||
|
|
||||||
visit(tree as UnistTree, "element", (node: UnistNode) => {
|
visit(
|
||||||
if (node.tagName !== "a" || !node.properties?.href?.match(/page\.mdx?/)) {
|
tree as UnistTree,
|
||||||
return
|
["element", "mdxJsxFlowElement"],
|
||||||
}
|
(node: UnistNode) => {
|
||||||
// get absolute path of the URL
|
if (node.tagName === "a" && node.properties?.href) {
|
||||||
const linkedFilePath = path
|
checkLink({
|
||||||
.resolve(currentPageFilePath, node.properties.href)
|
link: node.properties.href,
|
||||||
.replace(/#.*$/, "")
|
file,
|
||||||
// check if the file exists
|
currentPageFilePath,
|
||||||
if (!existsSync(linkedFilePath)) {
|
options,
|
||||||
throw new Error(
|
})
|
||||||
`Broken link found! ${node.properties.href} linked in ${file.history[0]}`
|
} else if (node.name && allowedComponentNames.includes(node.name)) {
|
||||||
)
|
componentChecker({
|
||||||
}
|
node: node as UnistNodeWithData,
|
||||||
|
file,
|
||||||
|
currentPageFilePath,
|
||||||
|
options,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const MD_LINK_REGEX = /\[(.*?)\]\((?<link>(![a-z]+!|\.).*?)\)/gm
|
||||||
@@ -1,18 +1,13 @@
|
|||||||
/* eslint-disable no-case-declarations */
|
|
||||||
import type { Transformer } from "unified"
|
import type { Transformer } from "unified"
|
||||||
import type {
|
import type {
|
||||||
CrossProjectLinksOptions,
|
CrossProjectLinksOptions,
|
||||||
ExpressionJsVar,
|
|
||||||
UnistNode,
|
UnistNode,
|
||||||
UnistNodeWithData,
|
UnistNodeWithData,
|
||||||
UnistTree,
|
UnistTree,
|
||||||
} from "./types/index.js"
|
} from "./types/index.js"
|
||||||
import { estreeToJs } from "./utils/estree-to-js.js"
|
import { estreeToJs } from "./utils/estree-to-js.js"
|
||||||
import getAttribute from "./utils/get-attribute.js"
|
import getAttribute from "./utils/get-attribute.js"
|
||||||
import {
|
import { performActionOnLiteral } from "./utils/perform-action-on-literal.js"
|
||||||
isExpressionJsVarLiteral,
|
|
||||||
isExpressionJsVarObj,
|
|
||||||
} from "./utils/expression-is-utils.js"
|
|
||||||
|
|
||||||
const PROJECT_REGEX = /^!(?<area>[\w-]+)!/
|
const PROJECT_REGEX = /^!(?<area>[\w-]+)!/
|
||||||
|
|
||||||
@@ -61,88 +56,64 @@ function componentFixer(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const fixProperty = (item: ExpressionJsVar) => {
|
let attributeName: string | undefined
|
||||||
if (!isExpressionJsVarObj(item)) {
|
|
||||||
|
const maybeCheckAttribute = () => {
|
||||||
|
if (!attributeName) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.entries(item).forEach(([key, value]) => {
|
const attribute = getAttribute(node, attributeName)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(key !== "href" && key !== "link") ||
|
!attribute ||
|
||||||
!isExpressionJsVarLiteral(value)
|
typeof attribute.value === "string" ||
|
||||||
|
!attribute.value.data?.estree
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
value.original.value = matchAndFixLinks(
|
const itemJsVar = estreeToJs(attribute.value.data.estree)
|
||||||
value.original.value as string,
|
|
||||||
|
if (!itemJsVar) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
performActionOnLiteral(itemJsVar, (item) => {
|
||||||
|
item.original.value = matchAndFixLinks(
|
||||||
|
item.original.value as string,
|
||||||
options
|
options
|
||||||
)
|
)
|
||||||
value.original.raw = JSON.stringify(value.original.value)
|
item.original.raw = JSON.stringify(item.original.value)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (node.name) {
|
switch (node.name) {
|
||||||
case "CardList":
|
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":
|
case "Prerequisites":
|
||||||
const prerequisitesItemsAttribute = getAttribute(node, "items")
|
attributeName = "items"
|
||||||
|
break
|
||||||
if (
|
case "Card":
|
||||||
!prerequisitesItemsAttribute?.value ||
|
attributeName = "href"
|
||||||
typeof prerequisitesItemsAttribute.value === "string" ||
|
break
|
||||||
!prerequisitesItemsAttribute.value.data?.estree
|
case "WorkflowDiagram":
|
||||||
) {
|
attributeName = "workflow"
|
||||||
return
|
break
|
||||||
|
case "TypeList":
|
||||||
|
attributeName = "types"
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
const prerequisitesJsVar = estreeToJs(
|
maybeCheckAttribute()
|
||||||
prerequisitesItemsAttribute.value.data.estree
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!prerequisitesJsVar) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(prerequisitesJsVar)) {
|
const allowedComponentNames = [
|
||||||
prerequisitesJsVar.forEach(fixProperty)
|
"Card",
|
||||||
} else {
|
"CardList",
|
||||||
fixProperty(prerequisitesJsVar)
|
"Prerequisites",
|
||||||
}
|
"WorkflowDiagram",
|
||||||
return
|
"TypeList",
|
||||||
}
|
]
|
||||||
}
|
|
||||||
|
|
||||||
export function crossProjectLinksPlugin(
|
export function crossProjectLinksPlugin(
|
||||||
options: CrossProjectLinksOptions
|
options: CrossProjectLinksOptions
|
||||||
@@ -155,9 +126,7 @@ export function crossProjectLinksPlugin(
|
|||||||
["element", "mdxJsxFlowElement"],
|
["element", "mdxJsxFlowElement"],
|
||||||
(node: UnistNode) => {
|
(node: UnistNode) => {
|
||||||
const isComponent =
|
const isComponent =
|
||||||
node.name === "Card" ||
|
node.name && allowedComponentNames.includes(node.name)
|
||||||
node.name === "CardList" ||
|
|
||||||
node.name === "Prerequisites"
|
|
||||||
const isLink = node.tagName === "a" && node.properties?.href
|
const isLink = node.tagName === "a" && node.properties?.href
|
||||||
if (!isComponent && !isLink) {
|
if (!isComponent && !isLink) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -118,6 +118,16 @@ export declare type CrossProjectLinksOptions = {
|
|||||||
useBaseUrl?: boolean
|
useBaseUrl?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export declare type BrokenLinkCheckerOptions = {
|
||||||
|
crossProjects: {
|
||||||
|
[k: string]: {
|
||||||
|
projectPath: string
|
||||||
|
contentPath?: string
|
||||||
|
hasGeneratedSlugs?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export declare type ComponentLinkFixerLinkType = "md" | "value"
|
export declare type ComponentLinkFixerLinkType = "md" | "value"
|
||||||
|
|
||||||
export declare type ComponentLinkFixerOptions = {
|
export declare type ComponentLinkFixerOptions = {
|
||||||
|
|||||||
@@ -1,21 +1,13 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { Transformer } from "unified"
|
import { Transformer } from "unified"
|
||||||
import {
|
import { UnistNodeWithData, UnistTree } from "../types/index.js"
|
||||||
ComponentLinkFixerLinkType,
|
|
||||||
ExpressionJsVar,
|
|
||||||
UnistNodeWithData,
|
|
||||||
UnistTree,
|
|
||||||
} from "../types/index.js"
|
|
||||||
import { FixLinkOptions, fixLinkUtil } from "../index.js"
|
import { FixLinkOptions, fixLinkUtil } from "../index.js"
|
||||||
import getAttribute from "../utils/get-attribute.js"
|
import getAttribute from "../utils/get-attribute.js"
|
||||||
import { estreeToJs } from "../utils/estree-to-js.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 { ComponentLinkFixerOptions } from "../types/index.js"
|
||||||
|
import { performActionOnLiteral } from "./perform-action-on-literal.js"
|
||||||
|
import { MD_LINK_REGEX } from "../constants.js"
|
||||||
|
|
||||||
const MD_LINK_REGEX = /\[(.*?)\]\((?<link>(![a-z]+!|\.).*?)\)/gm
|
|
||||||
const VALUE_LINK_REGEX = /^(![a-z]+!|\.)/gm
|
const VALUE_LINK_REGEX = /^(![a-z]+!|\.)/gm
|
||||||
|
|
||||||
function matchMdLinks(
|
function matchMdLinks(
|
||||||
@@ -59,33 +51,6 @@ function matchValueLink(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function traverseJsVar(
|
|
||||||
item: ExpressionJsVar[] | ExpressionJsVar,
|
|
||||||
linkOptions: Omit<FixLinkOptions, "linkedPath">,
|
|
||||||
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(
|
export function componentLinkFixer(
|
||||||
componentName: string,
|
componentName: string,
|
||||||
attributeName: string,
|
attributeName: string,
|
||||||
@@ -117,12 +82,12 @@ export function componentLinkFixer(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflowAttribute = getAttribute(node, attributeName)
|
const attribute = getAttribute(node, attributeName)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!workflowAttribute ||
|
!attribute ||
|
||||||
typeof workflowAttribute.value === "string" ||
|
typeof attribute.value === "string" ||
|
||||||
!workflowAttribute.value.data?.estree
|
!attribute.value.data?.estree
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -132,13 +97,17 @@ export function componentLinkFixer(
|
|||||||
appsPath,
|
appsPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemJsVar = estreeToJs(workflowAttribute.value.data.estree)
|
const itemJsVar = estreeToJs(attribute.value.data.estree)
|
||||||
|
|
||||||
if (!itemJsVar) {
|
if (!itemJsVar) {
|
||||||
return
|
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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
const PROJECT_REGEX = /^!(?<area>[\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, ""),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export type SlugChange = {
|
||||||
|
origSlug: string
|
||||||
|
newSlug: string
|
||||||
|
filePath: string
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from "./api-testing.js"
|
export * from "./api-testing.js"
|
||||||
|
export * from "./build-scripts.js"
|
||||||
export * from "./config.js"
|
export * from "./config.js"
|
||||||
export * from "./general.js"
|
export * from "./general.js"
|
||||||
export * from "./menu.js"
|
export * from "./menu.js"
|
||||||
|
|||||||
@@ -6805,6 +6805,7 @@ __metadata:
|
|||||||
react-transition-group: ^4.4.5
|
react-transition-group: ^4.4.5
|
||||||
rehype-mdx-code-props: ^3.0.1
|
rehype-mdx-code-props: ^3.0.1
|
||||||
rehype-slug: ^6.0.0
|
rehype-slug: ^6.0.0
|
||||||
|
remark-rehype-plugins: "*"
|
||||||
slugify: ^1.6.6
|
slugify: ^1.6.6
|
||||||
swr: ^2.2.0
|
swr: ^2.2.0
|
||||||
tailwind: "*"
|
tailwind: "*"
|
||||||
|
|||||||
Reference in new issue
Block a user