[
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Publish Package to NPM\n\non:\n  release:\n    types: [created]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions/setup-node@v2\n        with:\n          node-version: 16.x\n          registry-url: 'https://registry.npmjs.org'\n      - run: npm ci\n      - run: npm run build --if-present\n      - run: npm publish --access public\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}"
  },
  {
    "path": ".github/workflows/validate.yml",
    "content": "name: CI\n\non: [push]\n\njobs:\n  build:\n    strategy:\n      matrix:\n        platform: [ubuntu-latest, windows-latest]\n    runs-on: ${{ matrix.platform }}\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions/setup-node@v1\n        with:\n          node-version: 16.x\n      - name: npm install, build, and test\n        run: |\n          npm ci\n          npm run build --if-present\n          npm run test"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n.DS_Store\n*.log\nyarn.lock"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License\n\nCopyright (c) 2021- Stripe, Inc. (https://stripe.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# `@markdoc/next.js`\n\n> **Note**: this plugin will be treated as a beta version until `v1.0.0` is released.\n\nUsing the `@markdoc/next.js` plugin allows you to create custom `.md` and `.mdoc` pages in your Next.js apps, and automatically render them with [`markdoc`](https://github.com/markdoc/markdoc).\n\n## Setup\n\nThe first thing you'll need to do is install `@markdoc/next.js` and add it to your project's config.\n\n1. From your project, run this command to install `@markdoc/next.js`:\n   ```sh\n   npm install @markdoc/next.js @markdoc/markdoc\n   ```\n2. Open `next.config.js` and add the following code:\n\n   When using Webpack:\n\n   ```js\n   // next.config.js\n\n   const withMarkdoc = require('@markdoc/next.js');\n\n   module.exports = withMarkdoc(/* options */)({\n     pageExtensions: ['js', 'md'],\n   });\n   ```\n\n   For [Turbopack support](https://nextjs.org/docs/app/api-reference/turbopack), add the following configuration:\n\n   ```js\n   // next.config.js\n   module.exports = withMarkdoc({\n     dir: process.cwd(), // Required for Turbopack file resolution\n   })({\n     pageExtensions: ['js', 'md'],\n   });\n   ```\n\n3. Create a new Markdoc file in `pages/docs` named `getting-started.md`.\n\n   ```\n   pages\n   ├── _app.js\n   ├── docs\n   │   └── getting-started.md\n   ├── index.js\n   ```\n\n4. Add some content to `getting-started.md`:\n\n   ```md\n   ---\n   title: Get started with Markdoc\n   description: How to get started with Markdoc\n   ---\n\n   # Get started with Markdoc\n   ```\n\nSee [our docs](https://markdoc.dev/docs/nextjs) for more options.\n\n## Contributing\n\nContributions and feedback are welcomed and encouraged. Feel free to open PRs here, or open issues in the [Markdoc core repo](https://github.com/markdoc/markdoc).\n\nFollow these steps to set up the project:\n\n1. Run `npm install`\n1. Run `npm test`\n\n## Code of conduct\n\nThis project has adopted the Stripe [Code of conduct](https://github.com/markdoc/markdoc/blob/main/.github/CODE_OF_CONDUCT.md).\n\n## License\n\nThis project uses the [MIT license](LICENSE).\n"
  },
  {
    "path": "babel.config.json",
    "content": "{\n  \"env\": {\n    \"test\": {\n      \"plugins\": [\n        \"@babel/plugin-transform-modules-commonjs\"\n      ]\n    }\n  }\n}"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@markdoc/next.js\",\n  \"version\": \"0.5.0\",\n  \"author\": \"Stripe, Inc.\",\n  \"description\": \"Markdoc plugin for Next.js\",\n  \"license\": \"MIT\",\n  \"main\": \"./src/index.js\",\n  \"types\": \"./src/index.d.ts\",\n  \"exports\": {\n    \".\": \"./src/index.js\",\n    \"./*\": \"./src/*.js\"\n  },\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"dependencies\": {\n    \"js-yaml\": \"^4.1.0\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.16.12\",\n    \"@babel/plugin-transform-modules-commonjs\": \"^7.16.8\",\n    \"@markdoc/markdoc\": \"*\",\n    \"@types/jest\": \"^27.4.1\",\n    \"enhanced-resolve\": \"^5.10.0\",\n    \"jest\": \"^27.5.1\",\n    \"next\": \"*\",\n    \"react\": \"*\",\n    \"ts-jest\": \"^27.1.3\",\n    \"typescript\": \"4.6.2\"\n  },\n  \"peerDependencies\": {\n    \"@markdoc/markdoc\": \"*\",\n    \"next\": \"*\",\n    \"react\": \"*\"\n  },\n  \"directories\": {\n    \"test\": \"tests\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/markdoc/next.js.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/markdoc/next.js/issues\"\n  },\n  \"homepage\": \"https://markdoc.dev/docs/nextjs\",\n  \"jest\": {\n    \"preset\": \"ts-jest/presets/js-with-babel\"\n  }\n}"
  },
  {
    "path": "src/index.d.ts",
    "content": "import type {ElementType} from 'react';\nimport type {NextConfig} from 'next';\nimport type {Config, RenderableTreeNodes, Schema} from '@markdoc/markdoc';\nimport type {RuleSetConditionAbsolute} from 'webpack';\n\nexport type MarkdocNextJsPageProps = {\n  markdoc?: {\n    content: RenderableTreeNodes;\n    frontmatter: Record<string, any>;\n    file: {\n      path: string;\n    };\n  };\n};\n\nexport type MarkdocNextJsConfig = Config & {readonly source: string};\n\nexport type MarkdocNextJsSchema<O extends Object = {}> = Schema<\n  O & MarkdocNextJsConfig,\n  ElementType\n>;\n\nexport interface MarkdocNextJsOptions {\n  extension?: RuleSetConditionAbsolute;\n  mode?: 'static' | 'server';\n  options?: {\n    slots?: boolean;\n    allowComments?: boolean;\n  };\n  schemaPath?: string;\n  dir?: string;\n}\n\ndeclare function createMarkdocPlugin(\n  options?: MarkdocNextJsOptions\n): (config: NextConfig) => NextConfig;\n\nexport = createMarkdocPlugin;\n"
  },
  {
    "path": "src/index.js",
    "content": "function createTurbopackConfig(nextConfig, pluginOptions) {\n  const turbopack = nextConfig.turbopack;\n\n  if (!turbopack) {\n    return;\n  }\n\n  const extension = pluginOptions.extension || /\\.(md|mdoc)$/;\n\n  // Extract file extensions from regex pattern like /\\.(md|mdoc)$/ to create glob patterns for Turbopack\n  const extensionPatterns =\n    extension instanceof RegExp\n      ? extension.source\n          .match(/\\\\\\.\\(([^)]+)\\)\\$?/)?.[1]\n          ?.split('|')\n          .map((e) => `*.${e}`) || ['*.md', '*.mdoc']\n      : [extension];\n\n  const rules = extensionPatterns.reduce((acc, pattern) => {\n    acc[pattern] = {\n      loaders: [\n        {\n          loader: require.resolve('./loader'),\n          options: {\n            ...pluginOptions,\n          },\n        },\n      ],\n      as: '*.js',\n    };\n    return acc;\n  }, {});\n\n  return {\n    ...nextConfig.turbopack,\n    rules: {\n      ...nextConfig.turbopack.rules,\n      ...rules,\n    },\n  };\n}\n\nconst withMarkdoc =\n  (pluginOptions = {}) =>\n  (nextConfig = {}) => {\n    const extension = pluginOptions.extension || /\\.(md|mdoc)$/;\n\n    return Object.assign({}, nextConfig, {\n      webpack(config, options) {\n        config.module.rules.push({\n          test: extension,\n          use: [\n            // Adding the babel loader enables fast refresh\n            options.defaultLoaders.babel,\n            {\n              loader: require.resolve('./loader'),\n              options: {\n                appDir: options.defaultLoaders.babel.options.appDir,\n                pagesDir: options.defaultLoaders.babel.options.pagesDir,\n                ...pluginOptions,\n                dir: options.dir,\n              },\n            },\n          ],\n        });\n\n        if (typeof nextConfig.webpack === 'function') {\n          return nextConfig.webpack(config, options);\n        }\n\n        return config;\n      },\n\n      turbopack: createTurbopackConfig(nextConfig, pluginOptions),\n    });\n  };\n\nmodule.exports = withMarkdoc;\n"
  },
  {
    "path": "src/loader.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst Markdoc = require('@markdoc/markdoc');\n\nconst DEFAULT_SCHEMA_PATH = './markdoc';\n\nfunction normalize(s) {\n  return s.replace(/\\\\/g, path.win32.sep.repeat(2));\n}\n\nasync function gatherPartials(ast, schemaDir, tokenizer, parseOptions) {\n  let partials = {};\n\n  for (const node of ast.walk()) {\n    const file = node.attributes.file;\n\n    if (\n      node.type === 'tag' &&\n      node.tag === 'partial' &&\n      typeof file === 'string' &&\n      !partials[file]\n    ) {\n      const filepath = path.join(schemaDir, file);\n      // parsing is not done here because then we have to serialize and reload from JSON at runtime\n      const content = await fs.promises.readFile(filepath, {encoding: 'utf8'});\n\n      if (content) {\n        const tokens = tokenizer.tokenize(content);\n        const ast = Markdoc.parse(tokens, parseOptions);\n        partials = {\n          ...partials,\n          [file]: content,\n          ...(await gatherPartials.call(this, ast, schemaDir, tokenizer, parseOptions)),\n        };\n      }\n    }\n  }\n\n  return partials;\n}\n\n// Returning a JSX object is what allows fast refresh to work\nasync function load(source) {\n  // https://webpack.js.org/concepts/module-resolution/\n  const resolve = this.getResolve({\n    // https://webpack.js.org/api/loaders/#thisgetresolve\n    extensions: ['.js', '.jsx', '.json', '.ts', '.tsx', '...'],\n    preferRelative: true,\n  });\n\n  const {\n    dir, // Root directory from Next.js (contains next.config.js)\n    mode = 'static',\n    schemaPath = DEFAULT_SCHEMA_PATH,\n    options: {slots = false, ...options} = {\n      allowComments: true,\n    },\n    nextjsExports = ['metadata', 'revalidate'],\n    appDir = false,\n    pagesDir,\n  } = this.getOptions() || {};\n\n  const tokenizer = new Markdoc.Tokenizer(options);\n  const parseOptions = {slots};\n  const schemaDir = path.resolve(dir, schemaPath || DEFAULT_SCHEMA_PATH);\n  const tokens = tokenizer.tokenize(source);\n  const ast = Markdoc.parse(tokens, parseOptions);\n\n  // Determine if this is a page file by checking if it starts with the provided directories\n  const isPage = (appDir && this.resourcePath.startsWith(appDir)) || \n                 (pagesDir && this.resourcePath.startsWith(pagesDir));\n\n  // Grabs the path of the file relative to the `/{app,pages}` directory\n  // to pass into the app props later.\n  // This array access @ index 1 is safe since Next.js guarantees that\n  // all pages will be located under either {app,pages}/ or src/{app,pages}/\n  // https://nextjs.org/docs/app/building-your-application/configuring/src-directory\n  const filepath = this.resourcePath.split(appDir ? 'app' : 'pages')[1];\n\n  const partials = await gatherPartials.call(\n    this,\n    ast,\n    path.resolve(schemaDir, 'partials'),\n    tokenizer,\n    parseOptions\n  );\n\n  // IDEA: consider making this an option per-page\n  const dataFetchingFunction = mode === 'server' ? 'getServerSideProps' : 'getStaticProps';\n\n  let schemaCode = 'const schema = {};';\n  try {\n    const directoryExists = await fs.promises.stat(schemaDir);\n\n    // This creates import strings that cause the config to be imported runtime\n    async function importAtRuntime(variable) {\n      try {\n        const module = await resolve(schemaDir, variable);\n        return `import * as ${variable} from '${normalize(module)}'`;\n      } catch (error) {\n        return `const ${variable} = {};`;\n      }\n    }\n\n    if (directoryExists) {\n      schemaCode = `\n        ${await importAtRuntime('config')}\n        ${await importAtRuntime('tags')}\n        ${await importAtRuntime('nodes')}\n        ${await importAtRuntime('functions')}\n        const schema = {\n          tags: defaultObject(tags),\n          nodes: defaultObject(nodes),\n          functions: defaultObject(functions),\n          ...defaultObject(config),\n        };`\n        .trim()\n        .replace(/^\\s+/gm, '');\n    }\n  } catch (error) {\n    // Only throw module not found errors if user is passing a custom schemaPath\n    if (schemaPath && schemaPath !== DEFAULT_SCHEMA_PATH) {\n      throw new Error(`Cannot find module '${schemaPath}' at '${schemaDir}'`);\n    }\n  }\n\n  this.addContextDependency(schemaDir);\n\n  const nextjsExportsCode = nextjsExports\n    .map((name) => `export const ${name} = frontmatter.nextjs?.${name};`)\n    .join('\\n');\n\n  const result = `import React from 'react';\nimport yaml from 'js-yaml';\n// renderers is imported separately so Markdoc isn't sent to the client\nimport Markdoc, {renderers} from '@markdoc/markdoc'\n\nimport {getSchema, defaultObject} from '@markdoc/next.js/runtime';\n/**\n * Schema is imported like this so end-user's code is compiled using build-in babel/webpack configs.\n * This enables typescript/ESnext support\n */\n${schemaCode}\n\nconst tokenizer = new Markdoc.Tokenizer(${options ? JSON.stringify(options) : ''});\n\n/**\n * Source will never change at runtime, so parse happens at the file root\n */\nconst source = ${JSON.stringify(source)};\nconst filepath = ${JSON.stringify(filepath)};\nconst tokens = tokenizer.tokenize(source);\nconst parseOptions = ${JSON.stringify(parseOptions)};\nconst ast = Markdoc.parse(tokens, parseOptions);\n\n/**\n * Like the AST, frontmatter won't change at runtime, so it is loaded at file root.\n * This unblocks future features, such a per-page dataFetchingFunction.\n */\nconst frontmatter = ast.attributes.frontmatter\n  ? yaml.load(ast.attributes.frontmatter)\n  : {};\n\nconst {components, ...rest} = getSchema(schema)\n\n${isPage ? 'async ' : ''}function getMarkdocData(context = {}) {\n  const partials = ${JSON.stringify(partials)};\n\n  // Ensure Node.transformChildren is available\n  Object.keys(partials).forEach((key) => {\n    const tokens = tokenizer.tokenize(partials[key]);\n    partials[key] = Markdoc.parse(tokens, parseOptions);\n  });\n\n  const cfg = {\n    ...rest,\n    variables: {\n      ...(rest ? rest.variables : {}),\n      // user can't override this namespace\n      markdoc: {frontmatter},\n      // Allows users to eject from Markdoc rendering and pass in dynamic variables via getServerSideProps\n      ...(context.variables || {})\n    },\n    partials,\n    source,\n  };\n\n  /**\n   * transform must be called in dataFetchingFunction to support server-side rendering while\n   * accessing variables on the server\n   */\n  const content = ${isPage ? 'await ' : ''}Markdoc.transform(ast, cfg);\n\n  // Removes undefined\n  return JSON.parse(\n    JSON.stringify({\n      content,\n      frontmatter,\n      file: {\n        path: filepath,\n      },\n    })\n  );\n}\n\n${\n  appDir || !isPage\n    ? ''\n    : `export async function ${dataFetchingFunction}(context) {\n  return {\n    props: {\n      markdoc: await getMarkdocData(context),\n    },\n  };\n}`\n}\n${appDir && isPage ? nextjsExportsCode : ''}\nexport const markdoc = {frontmatter};\nexport default${appDir && isPage ? ' async' : ''} function MarkdocComponent(props) {\n  const markdoc = ${\n    isPage ? (appDir ? 'await getMarkdocData()' : 'props.markdoc') : 'getMarkdocData()'\n  };\n  // Only execute HMR code in development\n  return renderers.react(markdoc.content, React, {\n    components: {\n      ...components,\n      // Allows users to override default components at runtime, via their _app\n      ...props.components,\n    },\n  });\n}\n`;\n  return result;\n}\n\nmodule.exports = async function loader(source) {\n  const callback = this.async();\n  try {\n    const result = await load.call(this, source);\n    callback(null, result);\n  } catch (error) {\n    console.error(error);\n    callback(error);\n  }\n};\n"
  },
  {
    "path": "src/runtime.js",
    "content": "// IDEA: explore better displayName functions\nfunction displayName(name) {\n  // Pascal case\n  return name\n    .match(/[a-z]+/gi)\n    .map((word) => word.charAt(0).toUpperCase() + word.substr(1).toLowerCase())\n    .join('');\n}\n\nfunction transformRecord(config) {\n  const output = {};\n  const components = {};\n\n  if (config) {\n    Object.entries(config).forEach(([name, registration]) => {\n      if (output[name]) {\n        throw new Error(`\"${name}\" has already been declared`);\n      }\n\n      const componentName = registration.render ? displayName(name) : undefined;\n\n      output[name] = {\n        ...registration,\n        render: componentName,\n      };\n\n      if (componentName) {\n        components[componentName] = registration.render;\n      }\n    });\n  }\n\n  return {output, components};\n}\n\nexports.getSchema = function getSchema(schema) {\n  const {output: tags, components: tagComponents} = transformRecord(\n    schema.tags\n  );\n\n  const {output: nodes, components: nodeComponents} = transformRecord(\n    schema.nodes\n  );\n\n  return {\n    ...schema,\n    tags,\n    nodes,\n    components: {\n      ...tagComponents,\n      ...nodeComponents,\n    },\n  };\n};\n\nexports.defaultObject = function defaultObject(o) {\n  if (Object.prototype.hasOwnProperty.call(o, 'default')) return o.default;\n  return o || {};\n};\n"
  },
  {
    "path": "src/tags.js",
    "content": "const Head = require('next/head');\nconst Image = require('next/image');\nconst Link = require('next/link');\nconst Script = require('next/script');\n\nexports.comment = {\n  description: 'Use to comment the content itself',\n  attributes: {},\n  transform() {\n    return [];\n  },\n};\n\nexports.head = {\n  render: Head,\n  description: 'Renders a Next.js head tag',\n  attributes: {},\n};\n\nexports.image = {\n  render: Image,\n  description: 'Renders a Next.js image tag',\n  // https://nextjs.org/docs/app/api-reference/components/image\n  attributes: {\n    src: {\n      type: String,\n      required: true,\n    },\n    alt: {\n      type: String,\n      required: true,\n    },\n    width: {\n      type: Number,\n      required: true,\n    },\n    height: {\n      type: Number,\n      required: true,\n    },\n    fill: {\n      type: Boolean,\n    },\n    sizes: {\n      type: String,\n    },\n    quality: {\n      type: Number,\n    },\n    priority: {\n      type: Boolean,\n    },\n    placeholder: {\n      type: String,\n      matches: ['blur', 'empty'],\n    },\n    loading: {\n      type: String,\n      matches: ['lazy', 'eager'],\n    },\n    blurDataURL: {\n      type: String,\n    },\n  },\n};\n\nexports.link = {\n  render: Link,\n  description: 'Displays a Next.js link',\n  attributes: {\n    href: {\n      description: 'The path or URL to navigate to.',\n      type: String,\n      errorLevel: 'critical',\n      required: true,\n    },\n    as: {\n      description:\n        'Optional decorator for the path that will be shown in the browser URL bar.',\n      type: String,\n    },\n    passHref: {\n      description: 'Forces Link to send the href property to its child.',\n      type: Boolean,\n      default: false,\n    },\n    prefetch: {\n      description: 'Prefetch the page in the background.',\n      type: Boolean,\n    },\n    replace: {\n      description:\n        'Replace the current history state instead of adding a new url into the stack.',\n      type: Boolean,\n      default: false,\n    },\n    scroll: {\n      description: 'Scroll to the top of the page after a navigation.',\n      type: Boolean,\n      default: true,\n    },\n    shallow: {\n      description:\n        'Update the path of the current page without rerunning getStaticProps, getServerSideProps or getInitialProps.',\n      type: Boolean,\n      default: true,\n    },\n    locale: {\n      description: 'The active locale is automatically prepended.',\n      type: Boolean,\n    },\n    target: {\n      description: 'HTML attribute anchor target (\"_self\", \"_blank\", \"_parent\", \"_top\")',\n      type: String,\n    }\n  },\n};\n\nexports.script = {\n  render: Script,\n  description: 'Renders a Next.js script tag',\n  attributes: {\n    src: {\n      type: String,\n      errorLevel: 'critical',\n      required: true,\n    },\n    strategy: {\n      type: String,\n      matches: ['beforeInteractive', 'afterInteractive', 'lazyOnload'],\n    },\n  },\n};\n"
  },
  {
    "path": "tests/__snapshots__/index.test.js.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`HMR 1`] = `\n\"import React from 'react';\nimport yaml from 'js-yaml';\n// renderers is imported separately so Markdoc isn't sent to the client\nimport Markdoc, {renderers} from '@markdoc/markdoc'\n\nimport {getSchema, defaultObject} from '@markdoc/next.js/runtime';\n/**\n * Schema is imported like this so end-user's code is compiled using build-in babel/webpack configs.\n * This enables typescript/ESnext support\n */\nconst schema = {};\n\nconst tokenizer = new Markdoc.Tokenizer({\\\\\"allowComments\\\\\":true});\n\n/**\n * Source will never change at runtime, so parse happens at the file root\n */\nconst source = \\\\\"---\\\\\\\\ntitle: Custom title\\\\\\\\n---\\\\\\\\n\\\\\\\\n# {% $markdoc.frontmatter.title %}\\\\\\\\n\\\\\\\\n{% tag /%}\\\\\\\\n\\\\\";\nconst filepath = \\\\\"/test/index.md\\\\\";\nconst tokens = tokenizer.tokenize(source);\nconst parseOptions = {\\\\\"slots\\\\\":false};\nconst ast = Markdoc.parse(tokens, parseOptions);\n\n/**\n * Like the AST, frontmatter won't change at runtime, so it is loaded at file root.\n * This unblocks future features, such a per-page dataFetchingFunction.\n */\nconst frontmatter = ast.attributes.frontmatter\n  ? yaml.load(ast.attributes.frontmatter)\n  : {};\n\nconst {components, ...rest} = getSchema(schema)\n\nasync function getMarkdocData(context = {}) {\n  const partials = {};\n\n  // Ensure Node.transformChildren is available\n  Object.keys(partials).forEach((key) => {\n    const tokens = tokenizer.tokenize(partials[key]);\n    partials[key] = Markdoc.parse(tokens, parseOptions);\n  });\n\n  const cfg = {\n    ...rest,\n    variables: {\n      ...(rest ? rest.variables : {}),\n      // user can't override this namespace\n      markdoc: {frontmatter},\n      // Allows users to eject from Markdoc rendering and pass in dynamic variables via getServerSideProps\n      ...(context.variables || {})\n    },\n    partials,\n    source,\n  };\n\n  /**\n   * transform must be called in dataFetchingFunction to support server-side rendering while\n   * accessing variables on the server\n   */\n  const content = await Markdoc.transform(ast, cfg);\n\n  // Removes undefined\n  return JSON.parse(\n    JSON.stringify({\n      content,\n      frontmatter,\n      file: {\n        path: filepath,\n      },\n    })\n  );\n}\n\nexport async function getStaticProps(context) {\n  return {\n    props: {\n      markdoc: await getMarkdocData(context),\n    },\n  };\n}\n\nexport const markdoc = {frontmatter};\nexport default function MarkdocComponent(props) {\n  const markdoc = props.markdoc;\n  // Only execute HMR code in development\n  return renderers.react(markdoc.content, React, {\n    components: {\n      ...components,\n      // Allows users to override default components at runtime, via their _app\n      ...props.components,\n    },\n  });\n}\n\"\n`;\n\nexports[`app router 1`] = `\n\"import React from 'react';\nimport yaml from 'js-yaml';\n// renderers is imported separately so Markdoc isn't sent to the client\nimport Markdoc, {renderers} from '@markdoc/markdoc'\n\nimport {getSchema, defaultObject} from '@markdoc/next.js/runtime';\n/**\n * Schema is imported like this so end-user's code is compiled using build-in babel/webpack configs.\n * This enables typescript/ESnext support\n */\nconst schema = {};\n\nconst tokenizer = new Markdoc.Tokenizer({\\\\\"allowComments\\\\\":true});\n\n/**\n * Source will never change at runtime, so parse happens at the file root\n */\nconst source = \\\\\"---\\\\\\\\ntitle: Custom title\\\\\\\\n---\\\\\\\\n\\\\\\\\n# {% $markdoc.frontmatter.title %}\\\\\\\\n\\\\\\\\n{% tag /%}\\\\\\\\n\\\\\";\nconst filepath = \\\\\"/test/index.md\\\\\";\nconst tokens = tokenizer.tokenize(source);\nconst parseOptions = {\\\\\"slots\\\\\":false};\nconst ast = Markdoc.parse(tokens, parseOptions);\n\n/**\n * Like the AST, frontmatter won't change at runtime, so it is loaded at file root.\n * This unblocks future features, such a per-page dataFetchingFunction.\n */\nconst frontmatter = ast.attributes.frontmatter\n  ? yaml.load(ast.attributes.frontmatter)\n  : {};\n\nconst {components, ...rest} = getSchema(schema)\n\nasync function getMarkdocData(context = {}) {\n  const partials = {};\n\n  // Ensure Node.transformChildren is available\n  Object.keys(partials).forEach((key) => {\n    const tokens = tokenizer.tokenize(partials[key]);\n    partials[key] = Markdoc.parse(tokens, parseOptions);\n  });\n\n  const cfg = {\n    ...rest,\n    variables: {\n      ...(rest ? rest.variables : {}),\n      // user can't override this namespace\n      markdoc: {frontmatter},\n      // Allows users to eject from Markdoc rendering and pass in dynamic variables via getServerSideProps\n      ...(context.variables || {})\n    },\n    partials,\n    source,\n  };\n\n  /**\n   * transform must be called in dataFetchingFunction to support server-side rendering while\n   * accessing variables on the server\n   */\n  const content = await Markdoc.transform(ast, cfg);\n\n  // Removes undefined\n  return JSON.parse(\n    JSON.stringify({\n      content,\n      frontmatter,\n      file: {\n        path: filepath,\n      },\n    })\n  );\n}\n\n\nexport const metadata = frontmatter.nextjs?.metadata;\nexport const revalidate = frontmatter.nextjs?.revalidate;\nexport const markdoc = {frontmatter};\nexport default async function MarkdocComponent(props) {\n  const markdoc = await getMarkdocData();\n  // Only execute HMR code in development\n  return renderers.react(markdoc.content, React, {\n    components: {\n      ...components,\n      // Allows users to override default components at runtime, via their _app\n      ...props.components,\n    },\n  });\n}\n\"\n`;\n\nexports[`file output is correct 1`] = `\n\"import React from 'react';\nimport yaml from 'js-yaml';\n// renderers is imported separately so Markdoc isn't sent to the client\nimport Markdoc, {renderers} from '@markdoc/markdoc'\n\nimport {getSchema, defaultObject} from '@markdoc/next.js/runtime';\n/**\n * Schema is imported like this so end-user's code is compiled using build-in babel/webpack configs.\n * This enables typescript/ESnext support\n */\nconst schema = {};\n\nconst tokenizer = new Markdoc.Tokenizer({\\\\\"allowComments\\\\\":true});\n\n/**\n * Source will never change at runtime, so parse happens at the file root\n */\nconst source = \\\\\"---\\\\\\\\ntitle: Custom title\\\\\\\\n---\\\\\\\\n\\\\\\\\n# {% $markdoc.frontmatter.title %}\\\\\\\\n\\\\\\\\n{% tag /%}\\\\\\\\n\\\\\";\nconst filepath = \\\\\"/test/index.md\\\\\";\nconst tokens = tokenizer.tokenize(source);\nconst parseOptions = {\\\\\"slots\\\\\":false};\nconst ast = Markdoc.parse(tokens, parseOptions);\n\n/**\n * Like the AST, frontmatter won't change at runtime, so it is loaded at file root.\n * This unblocks future features, such a per-page dataFetchingFunction.\n */\nconst frontmatter = ast.attributes.frontmatter\n  ? yaml.load(ast.attributes.frontmatter)\n  : {};\n\nconst {components, ...rest} = getSchema(schema)\n\nasync function getMarkdocData(context = {}) {\n  const partials = {};\n\n  // Ensure Node.transformChildren is available\n  Object.keys(partials).forEach((key) => {\n    const tokens = tokenizer.tokenize(partials[key]);\n    partials[key] = Markdoc.parse(tokens, parseOptions);\n  });\n\n  const cfg = {\n    ...rest,\n    variables: {\n      ...(rest ? rest.variables : {}),\n      // user can't override this namespace\n      markdoc: {frontmatter},\n      // Allows users to eject from Markdoc rendering and pass in dynamic variables via getServerSideProps\n      ...(context.variables || {})\n    },\n    partials,\n    source,\n  };\n\n  /**\n   * transform must be called in dataFetchingFunction to support server-side rendering while\n   * accessing variables on the server\n   */\n  const content = await Markdoc.transform(ast, cfg);\n\n  // Removes undefined\n  return JSON.parse(\n    JSON.stringify({\n      content,\n      frontmatter,\n      file: {\n        path: filepath,\n      },\n    })\n  );\n}\n\nexport async function getStaticProps(context) {\n  return {\n    props: {\n      markdoc: await getMarkdocData(context),\n    },\n  };\n}\n\nexport const markdoc = {frontmatter};\nexport default function MarkdocComponent(props) {\n  const markdoc = props.markdoc;\n  // Only execute HMR code in development\n  return renderers.react(markdoc.content, React, {\n    components: {\n      ...components,\n      // Allows users to override default components at runtime, via their _app\n      ...props.components,\n    },\n  });\n}\n\"\n`;\n\nexports[`import as frontend component 1`] = `\n\"import React from 'react';\nimport yaml from 'js-yaml';\n// renderers is imported separately so Markdoc isn't sent to the client\nimport Markdoc, {renderers} from '@markdoc/markdoc'\n\nimport {getSchema, defaultObject} from '@markdoc/next.js/runtime';\n/**\n * Schema is imported like this so end-user's code is compiled using build-in babel/webpack configs.\n * This enables typescript/ESnext support\n */\nconst schema = {};\n\nconst tokenizer = new Markdoc.Tokenizer({\\\\\"allowComments\\\\\":true});\n\n/**\n * Source will never change at runtime, so parse happens at the file root\n */\nconst source = \\\\\"---\\\\\\\\ntitle: Custom title\\\\\\\\n---\\\\\\\\n\\\\\\\\n# {% $markdoc.frontmatter.title %}\\\\\\\\n\\\\\\\\n{% tag /%}\\\\\\\\n\\\\\";\nconst filepath = undefined;\nconst tokens = tokenizer.tokenize(source);\nconst parseOptions = {\\\\\"slots\\\\\":false};\nconst ast = Markdoc.parse(tokens, parseOptions);\n\n/**\n * Like the AST, frontmatter won't change at runtime, so it is loaded at file root.\n * This unblocks future features, such a per-page dataFetchingFunction.\n */\nconst frontmatter = ast.attributes.frontmatter\n  ? yaml.load(ast.attributes.frontmatter)\n  : {};\n\nconst {components, ...rest} = getSchema(schema)\n\nfunction getMarkdocData(context = {}) {\n  const partials = {};\n\n  // Ensure Node.transformChildren is available\n  Object.keys(partials).forEach((key) => {\n    const tokens = tokenizer.tokenize(partials[key]);\n    partials[key] = Markdoc.parse(tokens, parseOptions);\n  });\n\n  const cfg = {\n    ...rest,\n    variables: {\n      ...(rest ? rest.variables : {}),\n      // user can't override this namespace\n      markdoc: {frontmatter},\n      // Allows users to eject from Markdoc rendering and pass in dynamic variables via getServerSideProps\n      ...(context.variables || {})\n    },\n    partials,\n    source,\n  };\n\n  /**\n   * transform must be called in dataFetchingFunction to support server-side rendering while\n   * accessing variables on the server\n   */\n  const content = Markdoc.transform(ast, cfg);\n\n  // Removes undefined\n  return JSON.parse(\n    JSON.stringify({\n      content,\n      frontmatter,\n      file: {\n        path: filepath,\n      },\n    })\n  );\n}\n\n\n\nexport const markdoc = {frontmatter};\nexport default function MarkdocComponent(props) {\n  const markdoc = getMarkdocData();\n  // Only execute HMR code in development\n  return renderers.react(markdoc.content, React, {\n    components: {\n      ...components,\n      // Allows users to override default components at runtime, via their _app\n      ...props.components,\n    },\n  });\n}\n\"\n`;\n"
  },
  {
    "path": "tests/fixture.md",
    "content": "---\ntitle: Custom title\n---\n\n# {% $markdoc.frontmatter.title %}\n\n{% tag /%}\n"
  },
  {
    "path": "tests/index.test.js",
    "content": "const vm = require('vm');\nconst fs = require('fs');\nconst path = require('path');\nconst babel = require('@babel/core');\nconst React = require('react');\nconst enhancedResolve = require('enhanced-resolve');\nconst loader = require('../src/loader');\n\n// Mock the runtime module using Jest\njest.mock('@markdoc/next.js/runtime', () => require('../src/runtime'), {virtual: true});\n\nconst source = fs.readFileSync(require.resolve('./fixture.md'), 'utf-8');\n\n// https://stackoverflow.com/questions/53799385/how-can-i-convert-a-windows-path-to-posix-path-using-node-path\nfunction normalizeAbsolutePath(s) {\n  return s\n    .replace(/^[a-zA-Z]:/, '') // replace C: for Windows\n    .split(path.sep)\n    .join(path.posix.sep);\n}\n\nfunction normalizeOperatingSystemPaths(s) {\n  return s\n    .replace(normalizeAbsolutePath(process.cwd()), '.')\n    .split(path.sep)\n    .join(path.posix.sep)\n    .replace(/\\/r\\/n/g, '\\\\n');\n}\n\nfunction evaluate(output) {\n  const {code} = babel.transformSync(output);\n  const exports = {};\n\n  // https://stackoverflow.com/questions/38332094/how-can-i-mock-webpacks-require-context-in-jest\n  require.context = require.context = (base = '.') => {\n    const files = [];\n\n    function readDirectory(directory) {\n      fs.readdirSync(directory).forEach((file) => {\n        const fullPath = path.resolve(directory, file);\n\n        if (fs.statSync(fullPath).isDirectory()) {\n          readDirectory(fullPath);\n        }\n\n        files.push(fullPath);\n      });\n    }\n\n    readDirectory(path.resolve(__dirname, base));\n\n    return Object.assign(require, {keys: () => files});\n  };\n\n  vm.runInNewContext(code, {\n    exports,\n    require,\n    console,\n  });\n\n  return exports;\n}\n\nfunction options(config = {}) {\n  const dir = `${'/Users/someone/a-next-js-repo'}/${config.appDir ? 'app' : 'pages'}`;\n\n  const webpackThis = {\n    context: __dirname,\n    getOptions() {\n      return {\n        ...config,\n        dir: __dirname,\n        nextRuntime: 'nodejs',\n        appDir: config.appDir ? dir : undefined,\n        pagesDir: config.appDir ? undefined : dir,\n      };\n    },\n    getLogger() {\n      return console;\n    },\n    addDependency() {},\n    addContextDependency() {},\n    getResolve: (options) => {\n      const resolve = enhancedResolve.create(options);\n      return async (context, file) =>\n        new Promise((res, rej) =>\n          resolve(context, file, (err, result) => (err ? rej(err) : res(result)))\n        ).then(normalizeAbsolutePath);\n    },\n    resourcePath: dir + '/test/index.md',\n  };\n\n  return webpackThis;\n}\n\nasync function callLoader(config, source) {\n  return new Promise((res, rej) => {\n    config.async = () => (error, result) => {\n      if (error) {\n        rej(error);\n      } else {\n        res(result);\n      }\n    };\n    loader.call(config, source);\n  });\n}\n\ntest('should not fail build if default `schemaPath` is used', async () => {\n  await expect(callLoader(options(), source)).resolves.toEqual(expect.any(String));\n});\n\ntest('should fail build if invalid `schemaPath` is used', async () => {\n  await expect(callLoader(options({schemaPath: 'unknown_schema_path'}), source)).rejects.toThrow(\n    \"Cannot find module 'unknown_schema_path'\"\n  );\n});\n\ntest('file output is correct', async () => {\n  const output = await callLoader(options(), source);\n\n  expect(normalizeOperatingSystemPaths(output)).toMatchSnapshot();\n\n  const page = evaluate(output);\n\n  expect(evaluate(output)).toEqual({\n    default: expect.any(Function),\n    getStaticProps: expect.any(Function),\n    markdoc: {\n      frontmatter: {\n        title: 'Custom title',\n      },\n    },\n  });\n\n  const data = await page.getStaticProps({});\n  expect(data.props.markdoc).toEqual({\n    content: {\n      $$mdtype: 'Tag',\n      name: 'article',\n      attributes: {},\n      children: [\n        {\n          $$mdtype: 'Tag',\n          name: 'h1',\n          attributes: {},\n          children: ['Custom title'],\n        },\n      ],\n    },\n    frontmatter: {\n      title: 'Custom title',\n    },\n    file: {\n      path: '/test/index.md',\n    },\n  });\n\n  expect(page.default(data.props)).toEqual(\n    React.createElement('article', undefined, React.createElement('h1', undefined, 'Custom title'))\n  );\n});\n\ntest('app router', async () => {\n  const output = await callLoader(options({appDir: true}), source);\n\n  expect(normalizeOperatingSystemPaths(output)).toMatchSnapshot();\n\n  const page = evaluate(output);\n\n  expect(evaluate(output)).toEqual({\n    default: expect.any(Function),\n    markdoc: {\n      frontmatter: {\n        title: 'Custom title',\n      },\n    },\n  });\n\n  expect(await page.default({})).toEqual(\n    React.createElement('article', undefined, React.createElement('h1', undefined, 'Custom title'))\n  );\n});\n\ntest('app router metadata', async () => {\n  const output = await callLoader(\n    options({appDir: true}),\n    source.replace('---', '---\\nmetadata:\\n  title: Metadata title')\n  );\n\n  expect(output).toContain('export const metadata = frontmatter.nextjs?.metadata;');\n});\n\ntest.each([\n  [undefined, undefined],\n  ['./schemas/folders', 'markdoc1'],\n  ['./schemas/folders/', 'markdoc1'],\n  ['./schemas/files', 'markdoc2'],\n  ['schemas/files', 'markdoc2'],\n  ['schemas/typescript', source],\n])('Custom schema path (\"%s\")', async (schemaPath, expectedChild) => {\n  const output = await callLoader(options({schemaPath}), source);\n\n  const page = evaluate(output);\n\n  const data = await page.getStaticProps({});\n  expect(data.props.markdoc.content.children[0].children[0]).toEqual('Custom title');\n  expect(data.props.markdoc.content.children[1]).toEqual(expectedChild);\n});\n\ntest('Partials', async () => {\n  const output = await callLoader(\n    options({schemaPath: './schemas/partials'}),\n    `${source}\\n{% partial file=\"footer.md\" /%}`\n  );\n\n  const page = evaluate(output);\n\n  const data = await page.getStaticProps({});\n  expect(data.props.markdoc.content.children[1].children[0]).toEqual('footer');\n});\n\ntest('Ejected config', async () => {\n  const output = await callLoader(\n    options({schemaPath: './schemas/ejectedConfig'}),\n    `${source}\\n{% $product %}`\n  );\n\n  const page = evaluate(output);\n\n  const data = await page.getStaticProps({});\n  expect(data.props.markdoc.content.children[1]).toEqual('Extra value');\n  expect(data.props.markdoc.content.children[2].children[0]).toEqual('meal');\n});\n\ntest('HMR', async () => {\n  const output = await callLoader(\n    {\n      ...options(),\n      hot: true,\n    },\n    source\n  );\n\n  expect(normalizeOperatingSystemPaths(output)).toMatchSnapshot();\n});\n\ntest('mode=\"server\"', async () => {\n  const output = await callLoader(options({mode: 'server'}), source);\n\n  expect(evaluate(output)).toEqual({\n    default: expect.any(Function),\n    getServerSideProps: expect.any(Function),\n    markdoc: {\n      frontmatter: {\n        title: 'Custom title',\n      },\n    },\n  });\n});\n\ntest('import as frontend component', async () => {\n  const o = options();\n  // Use a non-page pathway\n  o.resourcePath = o.resourcePath.replace('pages/test/index.md', 'components/table.md');\n  const output = await callLoader(o, source);\n\n  expect(normalizeOperatingSystemPaths(output)).toMatchSnapshot();\n});\n\ntest('Turbopack configuration', () => {\n  const withMarkdoc = require('../src/index.js');\n  \n  // Test basic Turbopack configuration\n  const config = withMarkdoc()({\n    pageExtensions: ['js', 'md', 'mdoc'],\n    turbopack: {\n      rules: {},\n    },\n  });\n  \n  expect(config.turbopack).toBeDefined();\n  expect(config.turbopack.rules).toBeDefined();\n  expect(config.turbopack.rules['*.md']).toBeDefined();\n  expect(config.turbopack.rules['*.mdoc']).toBeDefined();\n  \n  // Verify rule structure\n  const mdRule = config.turbopack.rules['*.md'];\n  expect(mdRule.loaders).toHaveLength(1);\n  expect(mdRule.loaders[0].loader).toContain('loader');\n  expect(mdRule.as).toBe('*.js');\n  \n  // Test that existing turbopack config is preserved\n  const configWithExisting = withMarkdoc()({\n    pageExtensions: ['js', 'md'],\n    turbopack: {\n      rules: {\n        '*.svg': {\n          loaders: ['@svgr/webpack'],\n          as: '*.js',\n        },\n      },\n    },\n  });\n  \n  expect(configWithExisting.turbopack.rules['*.svg']).toBeDefined();\n  expect(configWithExisting.turbopack.rules['*.md']).toBeDefined();\n  \n  // Test custom extension\n  const configWithCustomExt = withMarkdoc({\n    extension: /\\.(markdown|mdx)$/,\n  })({\n    pageExtensions: ['js', 'markdown', 'mdx'],\n    turbopack: {\n      rules: {},\n    },\n  });\n  \n  expect(configWithCustomExt.turbopack.rules['*.markdown']).toBeDefined();\n  expect(configWithCustomExt.turbopack.rules['*.mdx']).toBeDefined();\n});\n"
  },
  {
    "path": "tests/schemas/ejectedConfig/config.js",
    "content": "export default {\n  extraValue: 'Extra value',\n  variables: {\n    product: 'meal',\n  },\n};\n"
  },
  {
    "path": "tests/schemas/ejectedConfig/tags.ts",
    "content": "import type {MarkdocNextJsSchema} from '../../../src';\n\nexport const tag: MarkdocNextJsSchema<{extraValue: string}> = {\n  transform(node, config: any) {\n    return config.extraValue;\n  },\n};\n"
  },
  {
    "path": "tests/schemas/files/nodes.js",
    "content": "export const link = {\n  transform() {\n    return 'link';\n  },\n};\n"
  },
  {
    "path": "tests/schemas/files/tags.js",
    "content": "export const tag = {\n  transform() {\n    return 'markdoc2';\n  },\n};\n"
  },
  {
    "path": "tests/schemas/folders/nodes/index.js",
    "content": "export const link = {\n  transform() {\n    return 'link';\n  },\n};\n"
  },
  {
    "path": "tests/schemas/folders/tags/index.js",
    "content": "export const tag = {\n  transform() {\n    return 'markdoc1';\n  },\n};\n"
  },
  {
    "path": "tests/schemas/partials/partials/footer.md",
    "content": "footer\n"
  },
  {
    "path": "tests/schemas/typescript/tags.ts",
    "content": "import type {MarkdocNextJsSchema} from '../../../src';\n\nexport const tag: MarkdocNextJsSchema = {\n  transform(node, config) {\n    return config.source;\n  },\n};\n"
  }
]