├── .dockerignore ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── app ├── .server │ └── posts.tsx ├── components │ ├── counter.tsx │ └── post.tsx ├── root.tsx ├── routes │ ├── _index.tsx │ ├── about.mdx │ ├── blog.first.mdx │ ├── blog.how-this-site-is-built.mdx │ ├── blog.markdown-and-mdx.mdx │ ├── blog.second.mdx │ ├── blog.tsx │ └── blog_._index.tsx └── tailwind.css ├── env.d.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── favicon.ico └── hero.png ├── tailwind.config.ts ├── tsconfig.json └── vite.config.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | .env 6 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This is intended to be a basic starting point for linting in your app. 3 | * It relies on recommended configs out of the box for simplicity, but you can 4 | * and should modify this configuration to best suit your team's needs. 5 | */ 6 | 7 | /** @type {import('eslint').Linter.Config} */ 8 | module.exports = { 9 | root: true, 10 | parserOptions: { 11 | ecmaVersion: "latest", 12 | sourceType: "module", 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | }, 17 | env: { 18 | browser: true, 19 | commonjs: true, 20 | es6: true, 21 | }, 22 | 23 | // Base config 24 | extends: ["eslint:recommended"], 25 | 26 | overrides: [ 27 | // React 28 | { 29 | files: ["**/*.{js,jsx,ts,tsx}"], 30 | plugins: ["react", "jsx-a11y"], 31 | extends: [ 32 | "plugin:react/recommended", 33 | "plugin:react/jsx-runtime", 34 | "plugin:react-hooks/recommended", 35 | "plugin:jsx-a11y/recommended", 36 | ], 37 | settings: { 38 | react: { 39 | version: "detect", 40 | }, 41 | formComponents: ["Form"], 42 | linkComponents: [ 43 | { name: "Link", linkAttribute: "to" }, 44 | { name: "NavLink", linkAttribute: "to" }, 45 | ], 46 | }, 47 | }, 48 | 49 | // Typescript 50 | { 51 | files: ["**/*.{ts,tsx}"], 52 | plugins: ["@typescript-eslint", "import"], 53 | parser: "@typescript-eslint/parser", 54 | settings: { 55 | "import/internal-regex": "^~/", 56 | "import/resolver": { 57 | node: { 58 | extensions: [".ts", ".tsx"], 59 | }, 60 | typescript: { 61 | alwaysTryTypes: true, 62 | }, 63 | }, 64 | }, 65 | extends: [ 66 | "plugin:@typescript-eslint/recommended", 67 | "plugin:import/recommended", 68 | "plugin:import/typescript", 69 | ], 70 | }, 71 | 72 | // Node 73 | { 74 | files: [".eslintrc.js"], 75 | env: { 76 | node: true, 77 | }, 78 | }, 79 | ], 80 | }; 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | .env 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Remix 🤝 MDX 2 | 3 | This template uses Vite to process MDX files in your Remix app. 4 | 5 | 👉 [Live Demo](https://remix-blog-mdx.fly.dev) 6 | 7 | Screenshot 2023-12-22 at 2 11 32 PM 8 | 9 | ## Setup 10 | 11 | ```shellscript 12 | npx create-remix@latest --template pcattori/remix-blog-mdx 13 | ``` 14 | 15 | ## Run 16 | 17 | Spin up the Vite dev server: 18 | 19 | ```shellscript 20 | npm run dev 21 | ``` 22 | 23 | Or build your app for production and run it: 24 | 25 | ```shellscript 26 | npm run build 27 | npm run start 28 | ``` 29 | -------------------------------------------------------------------------------- /app/.server/posts.tsx: -------------------------------------------------------------------------------- 1 | import { ServerBuild } from "@remix-run/node"; 2 | 3 | export type Frontmatter = { 4 | title: string; 5 | description: string; 6 | published: string; // YYYY-MM-DD 7 | featured: boolean; 8 | }; 9 | 10 | export type PostMeta = { 11 | slug: string; 12 | frontmatter: Frontmatter; 13 | }; 14 | 15 | export const getPosts = async (): Promise => { 16 | const modules = import.meta.glob<{ frontmatter: Frontmatter }>( 17 | "../routes/blog.*.mdx", 18 | { eager: true } 19 | ); 20 | const build = await import("virtual:remix/server-build"); 21 | const posts = Object.entries(modules).map(([file, post]) => { 22 | let id = file.replace("../", "").replace(/\.mdx$/, ""); 23 | let slug = build.routes[id].path; 24 | if (slug === undefined) throw new Error(`No route for ${id}`); 25 | 26 | return { 27 | slug, 28 | frontmatter: post.frontmatter, 29 | }; 30 | }); 31 | return sortBy(posts, (post) => post.frontmatter.published, "desc"); 32 | }; 33 | 34 | function sortBy( 35 | arr: T[], 36 | key: (item: T) => any, 37 | dir: "asc" | "desc" = "asc" 38 | ) { 39 | return arr.sort((a, b) => { 40 | const res = compare(key(a), key(b)); 41 | return dir === "asc" ? res : -res; 42 | }); 43 | } 44 | 45 | function compare(a: T, b: T): number { 46 | if (a < b) return -1; 47 | if (a > b) return 1; 48 | return 0; 49 | } 50 | -------------------------------------------------------------------------------- /app/components/counter.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export const Counter = () => { 4 | const [count, setCount] = useState(0); 5 | return ( 6 |
7 | 13 |

Count: {count}

14 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /app/components/post.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "@remix-run/react"; 2 | 3 | import type { PostMeta } from "~/.server/posts"; 4 | 5 | export const Post = ({ slug, frontmatter }: PostMeta) => { 6 | return ( 7 |
8 | 9 |

{frontmatter.title}

10 | 11 |

{frontmatter.description}

12 | 18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /app/root.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Links, 3 | LiveReload, 4 | Meta, 5 | MetaFunction, 6 | NavLink, 7 | Outlet, 8 | Scripts, 9 | ScrollRestoration, 10 | } from "@remix-run/react"; 11 | import { ReactNode } from "react"; 12 | 13 | import "~/tailwind.css"; 14 | 15 | const navLinkClass = ({ isActive }: { isActive: boolean }) => 16 | isActive ? "border-b-2 border-cyan-700" : ""; 17 | 18 | const Layout = (props: { children: ReactNode }) => ( 19 |
20 |
21 | 40 |
41 |
42 | {props.children} 43 |
44 | 53 |
54 | ); 55 | 56 | export const meta: MetaFunction = () => [ 57 | { title: "Remix 🤝 MDX" }, 58 | { 59 | name: "description", 60 | content: "Template showing off Remix's new MDX capabilities", 61 | }, 62 | ]; 63 | 64 | export default function App() { 65 | return ( 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /app/routes/_index.tsx: -------------------------------------------------------------------------------- 1 | import { json } from "@remix-run/node"; 2 | import { useLoaderData } from "@remix-run/react"; 3 | 4 | import { getPosts } from "~/.server/posts"; 5 | import { Post } from "~/components/post"; 6 | 7 | export const loader = async () => { 8 | const posts = await getPosts(); 9 | return json(posts.filter((post) => post.frontmatter.featured)); 10 | }; 11 | 12 | export default function Index() { 13 | const featuredPosts = useLoaderData(); 14 | 15 | return ( 16 |
17 |
18 |
19 |

Remix 🤝 MDX

20 |

21 | Powered by Vite plugins. Check out the{" "} 22 | 23 | code on Github 24 | 25 | . 26 |

27 |
28 |
29 |
30 |

✨ FEATURED ✨

31 |
    32 | {featuredPosts.map((post) => ( 33 |
  • 34 | 35 |
  • 36 | ))} 37 |
38 |
39 |
40 |
41 | Abstract sculpture with different colorful shapes 45 |
46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /app/routes/about.mdx: -------------------------------------------------------------------------------- 1 |
2 | # About 3 | 4 | This project shows off Remix's new MDX capabilities when using the [Vite plugin][remix-future-vite]. 5 | It also uses [Tailwind][tailwind] and the [typography plugin][tailwind-typography] for styling. 6 | 7 | Inspired by [Rajesh Babu's MDX template][remix-mdx-blog] ❤️ 8 | 9 | [remix-future-vite]: https://remix.run/docs/en/main/future/vite 10 | [tailwind]: https://tailwindcss.com/ 11 | [tailwind-typography]: https://tailwindcss.com/docs/typography-plugin 12 | [remix-mdx-blog]: https://github.com/rajeshdavidbabu/remix-mdx-blog 13 |
14 | -------------------------------------------------------------------------------- /app/routes/blog.first.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: First post 3 | description: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 4 | published: 2023-02-15 5 | --- 6 | 7 | # {frontmatter.title} 8 | 9 | 10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 11 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 12 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 13 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 14 | -------------------------------------------------------------------------------- /app/routes/blog.how-this-site-is-built.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: How this site is built 3 | description: Explanation of how Vite features and plugins work together to power Remix + MDX 4 | published: 2023-10-30 5 | featured: true 6 | --- 7 | 8 | # {frontmatter.title} 9 | 10 | This site uses [Vite][vite] and [Remark][remark] plugins that let you write MDX routes in your Remix app. 11 | These MDX routes are transformed to JS at build-time. 12 | 13 | Server-side utilities query the frontmatter of each MDX file so that pages can display lists of posts. 14 | 15 | The best part is that Remix has your back as your app gets more complex. 16 | Need to grab data your database? 17 | It's just Remix, so use a `loader`. 18 | Remix let's you use the _same_ mental model and the _same_ tools all the way from a simple blog to a complex app. 19 | No rearchitecting is required. 20 | 21 | ## Plugins 22 | 23 | MDX support in Vite is provided by [`@mdx-js/rollup`][mdx-js-rollup]. 24 | [Frontmatter][frontmatter] support in Remark is provided by [`remark-frontmatter`][remark-frontmatter] and [`remark-mdx-frontmatter`][remark-mdx-frontmatter]. 25 | 26 | Check out [`vite.config.ts`][vite-config] to see how these are wired together. 27 | 28 | ## MDX routes 29 | 30 | To take advantage of [nested routes][nested-routes], posts are under `/blog/` 31 | so that the `blog.tsx` parent route can handle layout and styling uniformly for all posts. 32 | 33 | The about page is a one-off route so it handles its own styling. 34 | There are probably other ways to handle styling without needing a surrounding `
`, but that seemed the simplest for now. 35 | It is still a nested route relative to the root route, so it gets the site layout for free. 36 | 37 | ## Querying MDX routes 38 | 39 | The home page (`/`) and blog archive (`/blog`) each want to display a subset of the posts. 40 | Specifically, they want to display metadata about each included post. 41 | 42 | The naive solution would be to read the files directly, say with `fs.readFileSync` and then parse the frontmatter. 43 | There are a couple issues with this approach. 44 | For one, the MDX routes are _routes_ not content and they will be built to a different location. 45 | Also, if you were using any other plugins for MDX or frontmatter, you'd have to duplicate that setup to parse the posts. 46 | 47 | Luckily, Vite's [glob imports][import-meta-glob] makes this easy. 48 | But relying on Vite, it handles transforming the MDX routes with the same transformation pipeline that is setup in `vite.config.ts`. 49 | 50 | Importantly, all frontmatter queries run behind a `loader` so that only the data needed by each page is sent over the network. 51 | Otherwise, chunks for _all_ posts would get sent to the browser even if a route like the home page only wanted to display metadata for a subset of the posts. 52 | 53 | ## Alternatives 54 | 55 | This approach handles MDX at build-time. 56 | For some use-cases, you might prefer to handle MDX at runtime. 57 | It's a bit more work since you have to setup a [Unified][unified] pipeline yourself, 58 | but it let's you model posts as _content_ rather than routes. 59 | That is, you would have a `blog.$slug.tsx` route that would dynamically load the MDX content for that post. 60 | 61 | There are different trade-offs here, so its up to you to decide which one is better. 62 | But the approach this site takes is often a simpler starting point. 63 | 64 | [vite]: https://vitejs.dev/ 65 | [remark]: https://github.com/remarkjs/remark 66 | [vite-config]: https://github.com/pcattori/remix-blog-mdx/blob/main/vite.config.ts 67 | [mdx-js-rollup]: https://mdxjs.com/packages/rollup/ 68 | [frontmatter]: https://mdxjs.com/guides/frontmatter/ 69 | [remark-frontmatter]: https://github.com/remarkjs/remark-frontmatter 70 | [remark-mdx-frontmatter]: https://github.com/remcohaszing/remark-mdx-frontmatter 71 | [nested-routes]: https://remix.run/docs/en/main/discussion/routes 72 | [import-meta-glob]: https://vitejs.dev/guide/features.html#glob-import 73 | [unified]: https://unifiedjs.com/ 74 | -------------------------------------------------------------------------------- /app/routes/blog.markdown-and-mdx.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown and MDX 3 | description: Demo of Markdown syntax and React components in MDX 4 | published: 2023-10-08 5 | featured: true 6 | --- 7 | 8 | # {frontmatter.title} 9 | 10 | This is an MDX test page that showcases various MDX elements and code blocks. 11 | 12 | ## MDX 13 | 14 | Let's try using a component! 15 | 16 | import { Counter } from "../components/counter.tsx"; 17 | 18 | 19 | 20 | ## Headings 21 | 22 | ### Level 3 Heading 23 | 24 | #### Level 4 Heading 25 | 26 | ##### Level 5 Heading 27 | 28 | ###### Level 6 Heading 29 | 30 | ## Paragraphs 31 | 32 | This is a paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam luctus felis vel risus lacinia, eu fringilla urna mattis. Sed maximus urna eu arcu blandit pulvinar. 33 | 34 | Etiam lobortis volutpat ligula, a facilisis purus. Sed vel felis blandit, sodales urna ac, varius mi. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. 35 | 36 | ## Lists 37 | 38 | ### Unordered List 39 | 40 | - Item 1 41 | - Item 2 42 | - Item 3 43 | 44 | ### Ordered List 45 | 46 | 1. Item 1 47 | 2. Item 2 48 | 3. Item 3 49 | 50 | ## Images 51 | 52 | ![Alt text](/hero.png "Optional title") 53 | 54 | ## Links 55 | 56 | [Link text](https://example.com) 57 | 58 | ## Blockquote 59 | 60 | > Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam luctus felis vel risus lacinia, eu fringilla urna mattis. 61 | 62 | ## Horizontal Rule 63 | 64 | --- 65 | 66 | ## Code Blocks 67 | 68 | ### Inline Code 69 | 70 | This is an example of `inline code`. 71 | 72 | ### Fenced Code Blocks 73 | 74 | ```javascript 75 | const add = (a, b) => a + b; 76 | console.log(add(2, 3)); // Output: 5 77 | ``` 78 | -------------------------------------------------------------------------------- /app/routes/blog.second.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Second post 3 | description: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 4 | published: 2023-08-31 5 | --- 6 | 7 | # {frontmatter.title} 8 | 9 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 10 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 11 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 12 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 13 | -------------------------------------------------------------------------------- /app/routes/blog.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "@remix-run/react"; 2 | 3 | export default function Component() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /app/routes/blog_._index.tsx: -------------------------------------------------------------------------------- 1 | import { json } from "@remix-run/node"; 2 | import { useLoaderData } from "@remix-run/react"; 3 | 4 | import { getPosts } from "~/.server/posts"; 5 | import { Post } from "~/components/post"; 6 | 7 | export const loader = async () => json(await getPosts()); 8 | 9 | export default function Component() { 10 | const posts = useLoaderData(); 11 | 12 | return ( 13 |
14 |
    15 | {posts.map((post) => ( 16 |
  • 17 | 18 |
  • 19 | ))} 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /app/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | @apply text-gray-900; 7 | } 8 | 9 | a:not(nav a) { 10 | @apply underline decoration-cyan-700 hover:decoration-cyan-900; 11 | } 12 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | declare module "virtual:remix/server-build" { 5 | import { ServerBuild } from "@remix-run/node"; 6 | export const routes: ServerBuild["routes"]; 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remix-blog-mdx", 3 | "private": true, 4 | "sideEffects": false, 5 | "type": "module", 6 | "scripts": { 7 | "build": "vite build && vite build --ssr", 8 | "dev": "vite dev", 9 | "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", 10 | "start": "remix-serve ./build/server/index.js", 11 | "typecheck": "tsc" 12 | }, 13 | "dependencies": { 14 | "@remix-run/node": "2.5.0", 15 | "@remix-run/react": "2.5.0", 16 | "@remix-run/serve": "2.5.0", 17 | "isbot": "^3.6.8", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0" 20 | }, 21 | "devDependencies": { 22 | "@flydotio/dockerfile": "^0.5.0", 23 | "@mdx-js/rollup": "^3.0.0", 24 | "@remix-run/dev": "2.5.0", 25 | "@tailwindcss/typography": "^0.5.10", 26 | "@types/react": "^18.2.20", 27 | "@types/react-dom": "^18.2.7", 28 | "@typescript-eslint/eslint-plugin": "^6.7.4", 29 | "autoprefixer": "^10.4.16", 30 | "eslint": "^8.38.0", 31 | "eslint-config-prettier": "^9.0.0", 32 | "eslint-import-resolver-typescript": "^3.6.1", 33 | "eslint-plugin-import": "^2.28.1", 34 | "eslint-plugin-jsx-a11y": "^6.7.1", 35 | "eslint-plugin-react": "^7.33.2", 36 | "eslint-plugin-react-hooks": "^4.6.0", 37 | "postcss": "^8.4.32", 38 | "rehype-pretty-code": "^0.12.3", 39 | "remark-frontmatter": "^5.0.0", 40 | "remark-mdx-frontmatter": "^4.0.0", 41 | "shikiji": "^0.9.10", 42 | "tailwindcss": "^3.4.0", 43 | "typescript": "^5.1.6", 44 | "vite": "^5.0.0", 45 | "vite-tsconfig-paths": "^4.2.1" 46 | }, 47 | "engines": { 48 | "node": ">=18.0.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | autoprefixer: {}, 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcattori/remix-blog-mdx/ec68b4a3a899d993c7ce0fd88cde061489660a8e/public/favicon.ico -------------------------------------------------------------------------------- /public/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcattori/remix-blog-mdx/ec68b4a3a899d993c7ce0fd88cde061489660a8e/public/hero.png -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | import typography from "@tailwindcss/typography"; 3 | 4 | export default { 5 | content: ["./app/**/*.{ts,tsx}"], 6 | 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [typography], 11 | } satisfies Config; 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["env.d.ts", "**/*.ts", "**/*.tsx"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "jsx": "react-jsx", 8 | "module": "ESNext", 9 | "moduleResolution": "Bundler", 10 | "resolveJsonModule": true, 11 | "target": "ES2022", 12 | "strict": true, 13 | "allowJs": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "~/*": ["./app/*"] 18 | }, 19 | 20 | // Remix takes care of building everything in `remix build`. 21 | "noEmit": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { unstable_vitePlugin as remix } from "@remix-run/dev"; 3 | import tsconfigPaths from "vite-tsconfig-paths"; 4 | import mdx from "@mdx-js/rollup"; 5 | import remarkFrontmatter from "remark-frontmatter"; 6 | import remarkMdxFrontmatter from "remark-mdx-frontmatter"; 7 | import rehypePrettyCode from "rehype-pretty-code"; 8 | 9 | export default defineConfig({ 10 | plugins: [ 11 | tsconfigPaths(), 12 | mdx({ 13 | remarkPlugins: [remarkFrontmatter, remarkMdxFrontmatter], 14 | rehypePlugins: [rehypePrettyCode], 15 | }), 16 | remix(), 17 | ], 18 | }); 19 | --------------------------------------------------------------------------------