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