Skip to content

Commit b6d5192

Browse files
authored
Add app router support (#30)
* Add initial app router support * Add `metadata` export * Revert "Add `metadata` export" This reverts commit 9999e2b. * Support passing `components` as a prop * Remove default props value
1 parent 43452e9 commit b6d5192

File tree

4 files changed

+173
-47
lines changed

4 files changed

+173
-47
lines changed

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const withMarkdoc =
1414
...pluginOptions,
1515
dir: options.dir,
1616
nextRuntime: options.nextRuntime,
17+
appDir: options.config.experimental.appDir,
1718
},
1819
},
1920
],

src/loader.js

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ async function load(source) {
5555
tokenizerOptions: {slots = false, ...tokenizerOptions} = {
5656
allowComments: true,
5757
},
58+
appDir = false,
5859
} = this.getOptions() || {};
5960

6061
const tokenizer = new Markdoc.Tokenizer(tokenizerOptions);
@@ -63,12 +64,12 @@ async function load(source) {
6364
const tokens = tokenizer.tokenize(source);
6465
const ast = Markdoc.parse(tokens, {slots});
6566

66-
// Grabs the path of the file relative to the `/pages` directory
67+
// Grabs the path of the file relative to the `/{app,pages}` directory
6768
// to pass into the app props later.
6869
// This array access @ index 1 is safe since Next.js guarantees that
69-
// all pages will be located under either pages/ or src/pages/
70+
// all pages will be located under either {app,pages}/ or src/{app,pages}/
7071
// https://nextjs.org/docs/advanced-features/src-directory
71-
const filepath = this.resourcePath.split('pages')[1];
72+
const filepath = this.resourcePath.split(appDir ? 'app' : 'pages')[1];
7273

7374
const partials = await gatherPartials.call(
7475
this,
@@ -154,7 +155,7 @@ const frontmatter = ast.attributes.frontmatter
154155
155156
const {components, ...rest} = getSchema(schema)
156157
157-
export async function ${dataFetchingFunction}(context) {
158+
async function getMarkdocData(context = {}) {
158159
const partials = ${JSON.stringify(partials)};
159160
160161
// Ensure Node.transformChildren is available
@@ -182,25 +183,30 @@ export async function ${dataFetchingFunction}(context) {
182183
*/
183184
const content = await Markdoc.transform(ast, cfg);
184185
186+
// Removes undefined
187+
return JSON.parse(
188+
JSON.stringify({
189+
content,
190+
frontmatter,
191+
file: {
192+
path: filepath,
193+
},
194+
})
195+
);
196+
}
197+
198+
${appDir ? '' : `export async function ${dataFetchingFunction}(context) {
185199
return {
186-
// Removes undefined
187-
props: JSON.parse(
188-
JSON.stringify({
189-
markdoc: {
190-
content,
191-
frontmatter,
192-
file: {
193-
path: filepath
194-
}
195-
},
196-
})
197-
),
200+
props: {
201+
markdoc: await getMarkdocData(context),
202+
},
198203
};
199-
}
204+
}`}
200205
201-
export default function MarkdocComponent(props) {
206+
export default${appDir ? ' async' : ''} function MarkdocComponent(props) {
207+
const markdoc = ${appDir ? 'await getMarkdocData()' : 'props.markdoc'};
202208
// Only execute HMR code in development
203-
return renderers.react(props.markdoc.content, React, {
209+
return renderers.react(markdoc.content, React, {
204210
components: {
205211
...components,
206212
// Allows users to override default components at runtime, via their _app

tests/__snapshots__/index.test.js.snap

Lines changed: 127 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const frontmatter = ast.attributes.frontmatter
3333
3434
const {components, ...rest} = getSchema(schema)
3535
36-
export async function getStaticProps(context) {
36+
async function getMarkdocData(context = {}) {
3737
const partials = {};
3838
3939
// Ensure Node.transformChildren is available
@@ -61,25 +61,119 @@ export async function getStaticProps(context) {
6161
*/
6262
const content = await Markdoc.transform(ast, cfg);
6363
64+
// Removes undefined
65+
return JSON.parse(
66+
JSON.stringify({
67+
content,
68+
frontmatter,
69+
file: {
70+
path: filepath,
71+
},
72+
})
73+
);
74+
}
75+
76+
export async function getStaticProps(context) {
6477
return {
65-
// Removes undefined
66-
props: JSON.parse(
67-
JSON.stringify({
68-
markdoc: {
69-
content,
70-
frontmatter,
71-
file: {
72-
path: filepath
73-
}
74-
},
75-
})
76-
),
78+
props: {
79+
markdoc: await getMarkdocData(context),
80+
},
7781
};
7882
}
7983
8084
export default function MarkdocComponent(props) {
85+
const markdoc = props.markdoc;
8186
// Only execute HMR code in development
82-
return renderers.react(props.markdoc.content, React, {
87+
return renderers.react(markdoc.content, React, {
88+
components: {
89+
...components,
90+
// Allows users to override default components at runtime, via their _app
91+
...props.components,
92+
},
93+
});
94+
}
95+
"
96+
`;
97+
98+
exports[`app router 1`] = `
99+
"import React from 'react';
100+
import yaml from 'js-yaml';
101+
// renderers is imported separately so Markdoc isn't sent to the client
102+
import Markdoc, {renderers} from '@markdoc/markdoc'
103+
104+
import {getSchema, defaultObject} from './src/runtime.js';
105+
/**
106+
* Schema is imported like this so end-user's code is compiled using build-in babel/webpack configs.
107+
* This enables typescript/ESnext support
108+
*/
109+
const schema = {};
110+
111+
const tokenizer = new Markdoc.Tokenizer({\\"allowComments\\":true});
112+
113+
/**
114+
* Source will never change at runtime, so parse happens at the file root
115+
*/
116+
const source = \\"---\\\\ntitle: Custom title\\\\n---\\\\n\\\\n# {% $markdoc.frontmatter.title %}\\\\n\\\\n{% tag /%}\\\\n\\";
117+
const filepath = undefined;
118+
const tokens = tokenizer.tokenize(source);
119+
const ast = Markdoc.parse(tokens, {slots: false});
120+
121+
/**
122+
* Like the AST, frontmatter won't change at runtime, so it is loaded at file root.
123+
* This unblocks future features, such a per-page dataFetchingFunction.
124+
*/
125+
const frontmatter = ast.attributes.frontmatter
126+
? yaml.load(ast.attributes.frontmatter)
127+
: {};
128+
129+
const {components, ...rest} = getSchema(schema)
130+
131+
async function getMarkdocData(context = {}) {
132+
const partials = {};
133+
134+
// Ensure Node.transformChildren is available
135+
Object.keys(partials).forEach((key) => {
136+
const tokens = tokenizer.tokenize(partials[key]);
137+
partials[key] = Markdoc.parse(tokens);
138+
});
139+
140+
const cfg = {
141+
...rest,
142+
variables: {
143+
...(rest ? rest.variables : {}),
144+
// user can't override this namespace
145+
markdoc: {frontmatter},
146+
// Allows users to eject from Markdoc rendering and pass in dynamic variables via getServerSideProps
147+
...(context.variables || {})
148+
},
149+
partials,
150+
source,
151+
};
152+
153+
/**
154+
* transform must be called in dataFetchingFunction to support server-side rendering while
155+
* accessing variables on the server
156+
*/
157+
const content = await Markdoc.transform(ast, cfg);
158+
159+
// Removes undefined
160+
return JSON.parse(
161+
JSON.stringify({
162+
content,
163+
frontmatter,
164+
file: {
165+
path: filepath,
166+
},
167+
})
168+
);
169+
}
170+
171+
172+
173+
export default async function MarkdocComponent(props) {
174+
const markdoc = await getMarkdocData();
175+
// Only execute HMR code in development
176+
return renderers.react(markdoc.content, React, {
83177
components: {
84178
...components,
85179
// Allows users to override default components at runtime, via their _app
@@ -123,7 +217,7 @@ const frontmatter = ast.attributes.frontmatter
123217
124218
const {components, ...rest} = getSchema(schema)
125219
126-
export async function getStaticProps(context) {
220+
async function getMarkdocData(context = {}) {
127221
const partials = {};
128222
129223
// Ensure Node.transformChildren is available
@@ -151,25 +245,30 @@ export async function getStaticProps(context) {
151245
*/
152246
const content = await Markdoc.transform(ast, cfg);
153247
248+
// Removes undefined
249+
return JSON.parse(
250+
JSON.stringify({
251+
content,
252+
frontmatter,
253+
file: {
254+
path: filepath,
255+
},
256+
})
257+
);
258+
}
259+
260+
export async function getStaticProps(context) {
154261
return {
155-
// Removes undefined
156-
props: JSON.parse(
157-
JSON.stringify({
158-
markdoc: {
159-
content,
160-
frontmatter,
161-
file: {
162-
path: filepath
163-
}
164-
},
165-
})
166-
),
262+
props: {
263+
markdoc: await getMarkdocData(context),
264+
},
167265
};
168266
}
169267
170268
export default function MarkdocComponent(props) {
269+
const markdoc = props.markdoc;
171270
// Only execute HMR code in development
172-
return renderers.react(props.markdoc.content, React, {
271+
return renderers.react(markdoc.content, React, {
173272
components: {
174273
...components,
175274
// Allows users to override default components at runtime, via their _app

tests/index.test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,26 @@ test('file output is correct', async () => {
157157
);
158158
});
159159

160+
test('app router', async () => {
161+
const output = await callLoader(options({ appDir: true }), source);
162+
163+
expect(normalizeOperatingSystemPaths(output)).toMatchSnapshot();
164+
165+
const page = evaluate(output);
166+
167+
expect(evaluate(output)).toEqual({
168+
default: expect.any(Function),
169+
});
170+
171+
expect(await page.default({})).toEqual(
172+
React.createElement(
173+
'article',
174+
undefined,
175+
React.createElement('h1', undefined, 'Custom title')
176+
)
177+
);
178+
});
179+
160180
test.each([
161181
[undefined, undefined],
162182
['./schemas/folders', 'markdoc1'],

0 commit comments

Comments
 (0)