@astrojs/markdoc 0.3.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,9 +42,8 @@ npm install @astrojs/markdoc
42
42
 
43
43
  Then, apply this integration to your `astro.config.*` file using the `integrations` property:
44
44
 
45
- __`astro.config.mjs`__
46
-
47
- ```js ins={2} "markdoc()"
45
+ ```js ins={3} "markdoc()"
46
+ // astro.config.mjs
48
47
  import { defineConfig } from 'astro/config';
49
48
  import markdoc from '@astrojs/markdoc';
50
49
 
@@ -54,14 +53,15 @@ export default defineConfig({
54
53
  });
55
54
  ```
56
55
 
57
-
58
56
  ### Editor Integration
59
57
 
60
58
  [VS Code](https://code.visualstudio.com/) supports Markdown by default. However, for Markdoc editor support, you may wish to add the following setting in your VSCode config. This ensures authoring Markdoc files provides a Markdown-like editor experience.
61
59
 
62
60
  ```json title=".vscode/settings.json"
63
- "files.associations": {
61
+ {
62
+ "files.associations": {
64
63
  "*.mdoc": "markdown"
64
+ }
65
65
  }
66
66
  ```
67
67
 
@@ -113,16 +113,16 @@ export default defineMarkdocConfig({
113
113
  aside: {
114
114
  render: Aside,
115
115
  attributes: {
116
- // Markdoc requires type defs for each attribute.
117
- // These should mirror the `Props` type of the component
118
- // you are rendering.
119
- // See Markdoc's documentation on defining attributes
120
- // https://markdoc.dev/docs/attributes#defining-attributes
116
+ // Markdoc requires type defs for each attribute.
117
+ // These should mirror the `Props` type of the component
118
+ // you are rendering.
119
+ // See Markdoc's documentation on defining attributes
120
+ // https://markdoc.dev/docs/attributes#defining-attributes
121
121
  type: { type: String },
122
- }
122
+ },
123
123
  },
124
124
  },
125
- })
125
+ });
126
126
  ```
127
127
 
128
128
  This component can now be used in your Markdoc files with the `{% aside %}` tag. Children will be passed to your component's default slot:
@@ -132,7 +132,7 @@ This component can now be used in your Markdoc files with the `{% aside %}` tag.
132
132
 
133
133
  {% aside type="tip" %}
134
134
 
135
- Use tags like this fancy "aside" to add some *flair* to your docs.
135
+ Use tags like this fancy "aside" to add some _flair_ to your docs.
136
136
 
137
137
  {% /aside %}
138
138
  ```
@@ -155,7 +155,7 @@ export default defineMarkdocConfig({
155
155
  render: Heading,
156
156
  },
157
157
  },
158
- })
158
+ });
159
159
  ```
160
160
 
161
161
  All Markdown headings will render the `Heading.astro` component and pass the following `attributes` as component props:
@@ -192,9 +192,9 @@ export default defineMarkdocConfig({
192
192
  // Note: Shiki has countless langs built-in, including `.astro`!
193
193
  // https://github.com/shikijs/shiki/blob/main/docs/languages.md
194
194
  langs: [],
195
- })
195
+ }),
196
196
  ],
197
- })
197
+ });
198
198
  ```
199
199
 
200
200
  #### Prism
@@ -208,7 +208,7 @@ import prism from '@astrojs/markdoc/prism';
208
208
 
209
209
  export default defineMarkdocConfig({
210
210
  extends: [prism()],
211
- })
211
+ });
212
212
  ```
213
213
 
214
214
  📚 To learn about configuring Prism stylesheets, [see our syntax highlighting guide](https://docs.astro.build/en/guides/markdown-content/#prism-configuration).
@@ -228,7 +228,7 @@ export default defineMarkdocConfig({
228
228
  render: null, // default 'article'
229
229
  },
230
230
  },
231
- })
231
+ });
232
232
  ```
233
233
 
234
234
  ### Custom Markdoc nodes / elements
@@ -249,7 +249,7 @@ export default defineMarkdocConfig({
249
249
  render: Quote,
250
250
  },
251
251
  },
252
- })
252
+ });
253
253
  ```
254
254
 
255
255
  📚 [Find all of Markdoc's built-in nodes and node attributes on their documentation.](https://markdoc.dev/docs/nodes#built-in-nodes)
@@ -282,10 +282,10 @@ export default defineMarkdocConfig({
282
282
  render: ClientAside,
283
283
  attributes: {
284
284
  type: { type: String },
285
- }
285
+ },
286
286
  },
287
287
  },
288
- })
288
+ });
289
289
  ```
290
290
 
291
291
  ### Markdoc config
@@ -307,12 +307,12 @@ export default defineMarkdocConfig({
307
307
  japan: '🇯🇵',
308
308
  spain: '🇪🇸',
309
309
  france: '🇫🇷',
310
- }
311
- return countryToEmojiMap[country] ?? '🏳'
310
+ };
311
+ return countryToEmojiMap[country] ?? '🏳';
312
312
  },
313
313
  },
314
314
  },
315
- })
315
+ });
316
316
  ```
317
317
 
318
318
  Now, you can call this function from any Markdoc content entry:
@@ -359,8 +359,8 @@ import { defineMarkdocConfig } from '@astrojs/markdoc/config';
359
359
  export default defineMarkdocConfig({
360
360
  variables: {
361
361
  environment: process.env.IS_PROD ? 'prod' : 'dev',
362
- }
363
- })
362
+ },
363
+ });
364
364
  ```
365
365
 
366
366
  ### Access frontmatter from your Markdoc content
@@ -382,7 +382,7 @@ This can now be accessed as `$frontmatter` in your Markdoc.
382
382
 
383
383
  ## Examples
384
384
 
385
- * The [Astro Markdoc starter template](https://github.com/withastro/astro/tree/latest/examples/with-markdoc) shows how to use Markdoc files in your Astro project.
385
+ - The [Astro Markdoc starter template](https://github.com/withastro/astro/tree/latest/examples/with-markdoc) shows how to use Markdoc files in your Astro project.
386
386
 
387
387
  ## Troubleshooting
388
388
 
@@ -399,13 +399,8 @@ This package is maintained by Astro's Core team. You're welcome to submit an iss
399
399
  See [CHANGELOG.md](https://github.com/withastro/astro/tree/main/packages/integrations/markdoc/CHANGELOG.md) for a history of changes to this integration.
400
400
 
401
401
  [astro-integration]: https://docs.astro.build/en/guides/integrations-guide/
402
-
403
402
  [astro-components]: https://docs.astro.build/en/core-concepts/astro-components/
404
-
405
403
  [astro-content-collections]: https://docs.astro.build/en/guides/content-collections/
406
-
407
404
  [markdoc-tags]: https://markdoc.dev/docs/tags
408
-
409
405
  [markdoc-nodes]: https://markdoc.dev/docs/nodes
410
-
411
406
  [markdoc-variables]: https://markdoc.dev/docs/variables
@@ -89,7 +89,10 @@ export const ComponentNode = createComponent({
89
89
  );
90
90
 
91
91
  // Let the runtime know that this component is being used.
92
- result.propagators.set(
92
+ // `result.propagators` has been moved to `result._metadata.propagators`
93
+ // TODO: remove this fallback in the next markdoc integration major
94
+ const propagators = result._metadata.propagators || result.propagators;
95
+ propagators.set(
93
96
  {},
94
97
  {
95
98
  init() {
package/dist/config.d.ts CHANGED
@@ -1,7 +1,14 @@
1
1
  import type { Config, ConfigType as MarkdocConfig, MaybePromise, NodeType, Schema } from '@markdoc/markdoc';
2
2
  import _Markdoc from '@markdoc/markdoc';
3
3
  import type { AstroInstance } from 'astro';
4
- type Render = AstroInstance['default'] | string;
4
+ import { componentConfigSymbol } from './utils.js';
5
+ export type Render = ComponentConfig | AstroInstance['default'] | string;
6
+ export type ComponentConfig = {
7
+ type: 'package' | 'local';
8
+ path: string;
9
+ namedExport?: string;
10
+ [componentConfigSymbol]: true;
11
+ };
5
12
  export type AstroMarkdocConfig<C extends Record<string, any> = Record<string, any>> = Omit<MarkdocConfig, 'tags' | 'nodes'> & Partial<{
6
13
  tags: Record<string, Schema<Config, Render>>;
7
14
  nodes: Partial<Record<NodeType, Schema<Config, Render>>>;
@@ -47,4 +54,4 @@ export declare const nodes: {
47
54
  node: {};
48
55
  };
49
56
  export declare function defineMarkdocConfig(config: AstroMarkdocConfig): AstroMarkdocConfig;
50
- export {};
57
+ export declare function component(pathnameOrPkgName: string, namedExport?: string): ComponentConfig;
package/dist/config.js CHANGED
@@ -1,12 +1,26 @@
1
+ import { isRelativePath } from "@astrojs/internal-helpers/path";
1
2
  import _Markdoc from "@markdoc/markdoc";
2
3
  import { heading } from "./heading-ids.js";
4
+ import { componentConfigSymbol } from "./utils.js";
3
5
  const Markdoc = _Markdoc;
4
6
  const nodes = { ...Markdoc.nodes, heading };
5
7
  function defineMarkdocConfig(config) {
6
8
  return config;
7
9
  }
10
+ function component(pathnameOrPkgName, namedExport) {
11
+ return {
12
+ type: isNpmPackageName(pathnameOrPkgName) ? "package" : "local",
13
+ path: pathnameOrPkgName,
14
+ namedExport,
15
+ [componentConfigSymbol]: true
16
+ };
17
+ }
18
+ function isNpmPackageName(pathname) {
19
+ return !isRelativePath(pathname) && !pathname.startsWith("/");
20
+ }
8
21
  export {
9
22
  Markdoc,
23
+ component,
10
24
  defineMarkdocConfig,
11
25
  nodes
12
26
  };
@@ -0,0 +1,6 @@
1
+ import type { AstroConfig, ContentEntryType } from 'astro';
2
+ import type { MarkdocConfigResult } from './load-config.js';
3
+ export declare function getContentEntryType({ markdocConfigResult, astroConfig, }: {
4
+ astroConfig: AstroConfig;
5
+ markdocConfigResult?: MarkdocConfigResult;
6
+ }): Promise<ContentEntryType>;
@@ -0,0 +1,198 @@
1
+ import Markdoc from "@markdoc/markdoc";
2
+ import matter from "gray-matter";
3
+ import fs from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+ import { isComponentConfig, isValidUrl, MarkdocError, prependForwardSlash } from "./utils.js";
6
+ import { emitESMImage } from "astro/assets";
7
+ import path from "node:path";
8
+ import { setupConfig } from "./runtime.js";
9
+ async function getContentEntryType({
10
+ markdocConfigResult,
11
+ astroConfig
12
+ }) {
13
+ return {
14
+ extensions: [".mdoc"],
15
+ getEntryInfo,
16
+ handlePropagation: true,
17
+ async getRenderModule({ contents, fileUrl, viteId }) {
18
+ var _a, _b;
19
+ const entry = getEntryInfo({ contents, fileUrl });
20
+ const tokens = markdocTokenizer.tokenize(entry.body);
21
+ const ast = Markdoc.parse(tokens);
22
+ const usedTags = getUsedTags(ast);
23
+ const userMarkdocConfig = (markdocConfigResult == null ? void 0 : markdocConfigResult.config) ?? {};
24
+ const markdocConfigUrl = markdocConfigResult == null ? void 0 : markdocConfigResult.fileUrl;
25
+ let componentConfigByTagMap = {};
26
+ for (const tag of usedTags) {
27
+ const render = (_b = (_a = userMarkdocConfig.tags) == null ? void 0 : _a[tag]) == null ? void 0 : _b.render;
28
+ if (isComponentConfig(render)) {
29
+ componentConfigByTagMap[tag] = render;
30
+ }
31
+ }
32
+ let componentConfigByNodeMap = {};
33
+ for (const [nodeType, schema] of Object.entries(userMarkdocConfig.nodes ?? {})) {
34
+ const render = schema == null ? void 0 : schema.render;
35
+ if (isComponentConfig(render)) {
36
+ componentConfigByNodeMap[nodeType] = render;
37
+ }
38
+ }
39
+ const pluginContext = this;
40
+ const markdocConfig = await setupConfig(userMarkdocConfig);
41
+ const filePath = fileURLToPath(fileUrl);
42
+ const validationErrors = Markdoc.validate(
43
+ ast,
44
+ /* Raised generics issue with Markdoc core https://github.com/markdoc/markdoc/discussions/400 */
45
+ markdocConfig
46
+ ).filter((e) => {
47
+ return (
48
+ // Ignore `variable-undefined` errors.
49
+ // Variables can be configured at runtime,
50
+ // so we cannot validate them at build time.
51
+ e.error.id !== "variable-undefined" && (e.error.level === "error" || e.error.level === "critical")
52
+ );
53
+ });
54
+ if (validationErrors.length) {
55
+ const frontmatterBlockOffset = entry.rawData.split("\n").length + 2;
56
+ const rootRelativePath = path.relative(fileURLToPath(astroConfig.root), filePath);
57
+ throw new MarkdocError({
58
+ message: [
59
+ `**${String(rootRelativePath)}** contains invalid content:`,
60
+ ...validationErrors.map((e) => `- ${e.error.message}`)
61
+ ].join("\n"),
62
+ location: {
63
+ // Error overlay does not support multi-line or ranges.
64
+ // Just point to the first line.
65
+ line: frontmatterBlockOffset + validationErrors[0].lines[0],
66
+ file: viteId
67
+ }
68
+ });
69
+ }
70
+ if (astroConfig.experimental.assets) {
71
+ await emitOptimizedImages(ast.children, {
72
+ astroConfig,
73
+ pluginContext,
74
+ filePath
75
+ });
76
+ }
77
+ const res = `import { Renderer } from '@astrojs/markdoc/components';
78
+ import { createGetHeadings, createContentComponent } from '@astrojs/markdoc/runtime';
79
+ ${markdocConfigUrl ? `import markdocConfig from ${JSON.stringify(markdocConfigUrl.pathname)};` : "const markdocConfig = {};"}${astroConfig.experimental.assets ? `
80
+ import { experimentalAssetsConfig } from '@astrojs/markdoc/experimental-assets-config';
81
+ markdocConfig.nodes = { ...experimentalAssetsConfig.nodes, ...markdocConfig.nodes };` : ""}
82
+
83
+ ${getStringifiedImports(componentConfigByTagMap, "Tag", astroConfig.root)}
84
+ ${getStringifiedImports(componentConfigByNodeMap, "Node", astroConfig.root)}
85
+
86
+ const tagComponentMap = ${getStringifiedMap(componentConfigByTagMap, "Tag")};
87
+ const nodeComponentMap = ${getStringifiedMap(componentConfigByNodeMap, "Node")};
88
+
89
+ const stringifiedAst = ${JSON.stringify(
90
+ /* Double stringify to encode *as* stringified JSON */
91
+ JSON.stringify(ast)
92
+ )};
93
+
94
+ export const getHeadings = createGetHeadings(stringifiedAst, markdocConfig);
95
+ export const Content = createContentComponent(
96
+ Renderer,
97
+ stringifiedAst,
98
+ markdocConfig,
99
+ tagComponentMap,
100
+ nodeComponentMap,
101
+ )`;
102
+ return { code: res };
103
+ },
104
+ contentModuleTypes: await fs.promises.readFile(
105
+ new URL("../template/content-module-types.d.ts", import.meta.url),
106
+ "utf-8"
107
+ )
108
+ };
109
+ }
110
+ const markdocTokenizer = new Markdoc.Tokenizer({
111
+ // Strip <!-- comments --> from rendered output
112
+ // Without this, they're rendered as strings!
113
+ allowComments: true
114
+ });
115
+ function getUsedTags(markdocAst) {
116
+ const tags = /* @__PURE__ */ new Set();
117
+ const validationErrors = Markdoc.validate(markdocAst);
118
+ for (const { error } of validationErrors) {
119
+ if (error.id === "tag-undefined") {
120
+ const [, tagName] = error.message.match(/Undefined tag: '(.*)'/) ?? [];
121
+ tags.add(tagName);
122
+ }
123
+ }
124
+ return tags;
125
+ }
126
+ function getEntryInfo({ fileUrl, contents }) {
127
+ const parsed = parseFrontmatter(contents, fileURLToPath(fileUrl));
128
+ return {
129
+ data: parsed.data,
130
+ body: parsed.content,
131
+ slug: parsed.data.slug,
132
+ rawData: parsed.matter
133
+ };
134
+ }
135
+ async function emitOptimizedImages(nodeChildren, ctx) {
136
+ for (const node of nodeChildren) {
137
+ if (node.type === "image" && typeof node.attributes.src === "string" && shouldOptimizeImage(node.attributes.src)) {
138
+ const resolved = await ctx.pluginContext.resolve(node.attributes.src, ctx.filePath);
139
+ if ((resolved == null ? void 0 : resolved.id) && fs.existsSync(new URL(prependForwardSlash(resolved.id), "file://"))) {
140
+ const src = await emitESMImage(
141
+ resolved.id,
142
+ ctx.pluginContext.meta.watchMode,
143
+ ctx.pluginContext.emitFile,
144
+ { config: ctx.astroConfig }
145
+ );
146
+ node.attributes.__optimizedSrc = src;
147
+ } else {
148
+ throw new MarkdocError({
149
+ message: `Could not resolve image ${JSON.stringify(
150
+ node.attributes.src
151
+ )} from ${JSON.stringify(ctx.filePath)}. Does the file exist?`
152
+ });
153
+ }
154
+ }
155
+ await emitOptimizedImages(node.children, ctx);
156
+ }
157
+ }
158
+ function shouldOptimizeImage(src) {
159
+ return !isValidUrl(src) && !src.startsWith("/");
160
+ }
161
+ function getStringifiedImports(componentConfigMap, componentNamePrefix, root) {
162
+ let stringifiedComponentImports = "";
163
+ for (const [key, config] of Object.entries(componentConfigMap)) {
164
+ const importName = config.namedExport ? `{ ${config.namedExport} as ${componentNamePrefix + key} }` : componentNamePrefix + key;
165
+ const resolvedPath = config.type === "local" ? new URL(config.path, root).pathname : config.path;
166
+ stringifiedComponentImports += `import ${importName} from ${JSON.stringify(resolvedPath)};
167
+ `;
168
+ }
169
+ return stringifiedComponentImports;
170
+ }
171
+ function getStringifiedMap(componentConfigMap, componentNamePrefix) {
172
+ let stringifiedComponentMap = "{";
173
+ for (const key in componentConfigMap) {
174
+ stringifiedComponentMap += `${key}: ${componentNamePrefix + key},
175
+ `;
176
+ }
177
+ stringifiedComponentMap += "}";
178
+ return stringifiedComponentMap;
179
+ }
180
+ function parseFrontmatter(fileContents, filePath) {
181
+ try {
182
+ matter.clearCache();
183
+ return matter(fileContents);
184
+ } catch (e) {
185
+ if (e.name === "YAMLException") {
186
+ const err = e;
187
+ err.id = filePath;
188
+ err.loc = { file: e.id, line: e.mark.line + 1, column: e.mark.column };
189
+ err.message = e.reason;
190
+ throw err;
191
+ } else {
192
+ throw e;
193
+ }
194
+ }
195
+ }
196
+ export {
197
+ getContentEntryType
198
+ };
package/dist/index.js CHANGED
@@ -1,26 +1,9 @@
1
- import Markdoc from "@markdoc/markdoc";
2
- import crypto from "node:crypto";
3
- import fs from "node:fs";
4
- import { fileURLToPath, pathToFileURL } from "node:url";
1
+ import { bold, red } from "kleur/colors";
2
+ import { getContentEntryType } from "./content-entry-type.js";
5
3
  import {
6
- hasContentFlag,
7
- isValidUrl,
8
- MarkdocError,
9
- parseFrontmatter,
10
- prependForwardSlash,
11
- PROPAGATED_ASSET_FLAG
12
- } from "./utils.js";
13
- import { emitESMImage } from "astro/assets";
14
- import { bold, red, yellow } from "kleur/colors";
15
- import path from "node:path";
16
- import { normalizePath } from "vite";
17
- import { loadMarkdocConfig } from "./load-config.js";
18
- import { setupConfig } from "./runtime.js";
19
- const markdocTokenizer = new Markdoc.Tokenizer({
20
- // Strip <!-- comments --> from rendered output
21
- // Without this, they're rendered as strings!
22
- allowComments: true
23
- });
4
+ loadMarkdocConfig,
5
+ SUPPORTED_MARKDOC_CONFIG_FILES
6
+ } from "./load-config.js";
24
7
  function markdocIntegration(legacyConfig) {
25
8
  if (legacyConfig) {
26
9
  console.log(
@@ -31,224 +14,33 @@ function markdocIntegration(legacyConfig) {
31
14
  process.exit(0);
32
15
  }
33
16
  let markdocConfigResult;
34
- let markdocConfigResultId = "";
17
+ let astroConfig;
35
18
  return {
36
19
  name: "@astrojs/markdoc",
37
20
  hooks: {
38
21
  "astro:config:setup": async (params) => {
39
- const {
40
- config: astroConfig,
41
- updateConfig,
42
- addContentEntryType
43
- } = params;
22
+ const { updateConfig, addContentEntryType } = params;
23
+ astroConfig = params.config;
44
24
  markdocConfigResult = await loadMarkdocConfig(astroConfig);
45
- if (markdocConfigResult) {
46
- markdocConfigResultId = normalizePath(fileURLToPath(markdocConfigResult.fileUrl));
47
- }
48
- const userMarkdocConfig = (markdocConfigResult == null ? void 0 : markdocConfigResult.config) ?? {};
49
- function getEntryInfo({ fileUrl, contents }) {
50
- const parsed = parseFrontmatter(contents, fileURLToPath(fileUrl));
51
- return {
52
- data: parsed.data,
53
- body: parsed.content,
54
- slug: parsed.data.slug,
55
- rawData: parsed.matter
56
- };
57
- }
58
- addContentEntryType({
59
- extensions: [".mdoc"],
60
- getEntryInfo,
61
- // Markdoc handles script / style propagation
62
- // for Astro components internally
63
- handlePropagation: false,
64
- async getRenderModule({ contents, fileUrl, viteId }) {
65
- const entry = getEntryInfo({ contents, fileUrl });
66
- const tokens = markdocTokenizer.tokenize(entry.body);
67
- const ast = Markdoc.parse(tokens);
68
- const pluginContext = this;
69
- const markdocConfig = await setupConfig(userMarkdocConfig);
70
- const filePath = fileURLToPath(fileUrl);
71
- const validationErrors = Markdoc.validate(
72
- ast,
73
- /* Raised generics issue with Markdoc core https://github.com/markdoc/markdoc/discussions/400 */
74
- markdocConfig
75
- ).filter((e) => {
76
- return (
77
- // Ignore `variable-undefined` errors.
78
- // Variables can be configured at runtime,
79
- // so we cannot validate them at build time.
80
- e.error.id !== "variable-undefined" && (e.error.level === "error" || e.error.level === "critical")
81
- );
82
- });
83
- if (validationErrors.length) {
84
- const frontmatterBlockOffset = entry.rawData.split("\n").length + 2;
85
- const rootRelativePath = path.relative(fileURLToPath(astroConfig.root), filePath);
86
- throw new MarkdocError({
87
- message: [
88
- `**${String(rootRelativePath)}** contains invalid content:`,
89
- ...validationErrors.map((e) => `- ${e.error.message}`)
90
- ].join("\n"),
91
- location: {
92
- // Error overlay does not support multi-line or ranges.
93
- // Just point to the first line.
94
- line: frontmatterBlockOffset + validationErrors[0].lines[0],
95
- file: viteId
96
- }
97
- });
98
- }
99
- if (astroConfig.experimental.assets) {
100
- await emitOptimizedImages(ast.children, {
101
- astroConfig,
102
- pluginContext,
103
- filePath
104
- });
105
- }
106
- const res = `import {
107
- createComponent,
108
- renderComponent,
109
- } from 'astro/runtime/server/index.js';
110
- import { Renderer } from '@astrojs/markdoc/components';
111
- import { collectHeadings, setupConfig, setupConfigSync, Markdoc } from '@astrojs/markdoc/runtime';
112
- ${markdocConfigResult ? `import _userConfig from ${JSON.stringify(
113
- markdocConfigResultId
114
- )};
115
- const userConfig = _userConfig ?? {};` : "const userConfig = {};"}${astroConfig.experimental.assets ? `
116
- import { experimentalAssetsConfig } from '@astrojs/markdoc/experimental-assets-config';
117
- userConfig.nodes = { ...experimentalAssetsConfig.nodes, ...userConfig.nodes };` : ""}
118
- const stringifiedAst = ${JSON.stringify(
119
- /* Double stringify to encode *as* stringified JSON */
120
- JSON.stringify(ast)
121
- )};
122
- export function getHeadings() {
123
- ${/* Yes, we are transforming twice (once from `getHeadings()` and again from <Content /> in case of variables).
124
- TODO: propose new `render()` API to allow Markdoc variable passing to `render()` itself,
125
- instead of the Content component. Would remove double-transform and unlock variable resolution in heading slugs. */
126
- ""}
127
- const headingConfig = userConfig.nodes?.heading;
128
- const config = setupConfigSync(headingConfig ? { nodes: { heading: headingConfig } } : {});
129
- const ast = Markdoc.Ast.fromJSON(stringifiedAst);
130
- const content = Markdoc.transform(ast, config);
131
- return collectHeadings(Array.isArray(content) ? content : content.children);
132
- }
133
-
134
- export const Content = createComponent({
135
- async factory(result, props) {
136
- const config = await setupConfig({
137
- ...userConfig,
138
- variables: { ...userConfig.variables, ...props },
139
- });
140
-
141
- return renderComponent(
142
- result,
143
- Renderer.name,
144
- Renderer,
145
- { stringifiedAst, config },
146
- {}
147
- );
148
- },
149
- propagation: 'self',
150
- });`;
151
- return { code: res };
152
- },
153
- contentModuleTypes: await fs.promises.readFile(
154
- new URL("../template/content-module-types.d.ts", import.meta.url),
155
- "utf-8"
156
- )
157
- });
158
- let rollupOptions = {};
159
- if (markdocConfigResult) {
160
- rollupOptions = {
161
- output: {
162
- // Split Astro components from your `markdoc.config`
163
- // to only inject component styles and scripts at runtime.
164
- manualChunks(id, { getModuleInfo }) {
165
- var _a, _b;
166
- if (markdocConfigResult && hasContentFlag(id, PROPAGATED_ASSET_FLAG) && ((_b = (_a = getModuleInfo(id)) == null ? void 0 : _a.importers) == null ? void 0 : _b.includes(markdocConfigResultId))) {
167
- return createNameHash(id, [id]);
168
- }
169
- }
170
- }
171
- };
172
- }
25
+ addContentEntryType(await getContentEntryType({ markdocConfigResult, astroConfig }));
173
26
  updateConfig({
174
27
  vite: {
175
- vite: {
176
- ssr: {
177
- external: ["@astrojs/markdoc/prism", "@astrojs/markdoc/shiki"]
178
- }
179
- },
180
- build: {
181
- rollupOptions
182
- },
183
- plugins: [
184
- {
185
- name: "@astrojs/markdoc:astro-propagated-assets",
186
- enforce: "pre",
187
- // Astro component styles and scripts should only be injected
188
- // When a given Markdoc file actually uses that component.
189
- // Add the `astroPropagatedAssets` flag to inject only when rendered.
190
- resolveId(id, importer) {
191
- if (importer === markdocConfigResultId && id.endsWith(".astro")) {
192
- return this.resolve(id + "?astroPropagatedAssets", importer, {
193
- skipSelf: true
194
- });
195
- }
196
- }
197
- }
198
- ]
28
+ ssr: {
29
+ external: ["@astrojs/markdoc/prism", "@astrojs/markdoc/shiki"]
30
+ }
199
31
  }
200
32
  });
201
33
  },
202
34
  "astro:server:setup": async ({ server }) => {
203
35
  server.watcher.on("all", (event, entry) => {
204
- if (prependForwardSlash(pathToFileURL(entry).pathname) === markdocConfigResultId) {
205
- console.log(
206
- yellow(
207
- `${bold("[Markdoc]")} Restart the dev server for config changes to take effect.`
208
- )
209
- );
36
+ if (SUPPORTED_MARKDOC_CONFIG_FILES.some((f) => entry.endsWith(f))) {
37
+ server.restart();
210
38
  }
211
39
  });
212
40
  }
213
41
  }
214
42
  };
215
43
  }
216
- async function emitOptimizedImages(nodeChildren, ctx) {
217
- for (const node of nodeChildren) {
218
- if (node.type === "image" && typeof node.attributes.src === "string" && shouldOptimizeImage(node.attributes.src)) {
219
- const resolved = await ctx.pluginContext.resolve(node.attributes.src, ctx.filePath);
220
- if ((resolved == null ? void 0 : resolved.id) && fs.existsSync(new URL(prependForwardSlash(resolved.id), "file://"))) {
221
- const src = await emitESMImage(
222
- resolved.id,
223
- ctx.pluginContext.meta.watchMode,
224
- ctx.pluginContext.emitFile,
225
- { config: ctx.astroConfig }
226
- );
227
- node.attributes.__optimizedSrc = src;
228
- } else {
229
- throw new MarkdocError({
230
- message: `Could not resolve image ${JSON.stringify(
231
- node.attributes.src
232
- )} from ${JSON.stringify(ctx.filePath)}. Does the file exist?`
233
- });
234
- }
235
- }
236
- await emitOptimizedImages(node.children, ctx);
237
- }
238
- }
239
- function shouldOptimizeImage(src) {
240
- return !isValidUrl(src) && !src.startsWith("/");
241
- }
242
- function createNameHash(baseId, hashIds) {
243
- const baseName = baseId ? path.parse(baseId).name : "index";
244
- const hash = crypto.createHash("sha256");
245
- for (const id of hashIds) {
246
- hash.update(id, "utf-8");
247
- }
248
- const h = hash.digest("hex").slice(0, 8);
249
- const proposedName = baseName + "." + h;
250
- return proposedName;
251
- }
252
44
  export {
253
45
  markdocIntegration as default
254
46
  };
@@ -1,5 +1,6 @@
1
1
  import type { AstroConfig } from 'astro';
2
2
  import type { AstroMarkdocConfig } from './config.js';
3
+ export declare const SUPPORTED_MARKDOC_CONFIG_FILES: string[];
3
4
  export type MarkdocConfigResult = {
4
5
  config: AstroMarkdocConfig;
5
6
  fileUrl: URL;
@@ -1,6 +1,7 @@
1
1
  import { build as esbuild } from "esbuild";
2
2
  import * as fs from "node:fs";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { MarkdocError } from "./utils.js";
4
5
  const SUPPORTED_MARKDOC_CONFIG_FILES = [
5
6
  "markdoc.config.js",
6
7
  "markdoc.config.mjs",
@@ -18,7 +19,7 @@ async function loadMarkdocConfig(astroConfig) {
18
19
  }
19
20
  if (!markdocConfigUrl)
20
21
  return;
21
- const { code, dependencies } = await bundleConfigFile({
22
+ const { code } = await bundleConfigFile({
22
23
  markdocConfigUrl,
23
24
  astroConfig
24
25
  });
@@ -32,6 +33,7 @@ async function bundleConfigFile({
32
33
  markdocConfigUrl,
33
34
  astroConfig
34
35
  }) {
36
+ let markdocError;
35
37
  const result = await esbuild({
36
38
  absWorkingDir: fileURLToPath(astroConfig.root),
37
39
  entryPoints: [fileURLToPath(markdocConfigUrl)],
@@ -49,8 +51,12 @@ async function bundleConfigFile({
49
51
  name: "stub-astro-imports",
50
52
  setup(build) {
51
53
  build.onResolve({ filter: /.*\.astro$/ }, () => {
54
+ markdocError = new MarkdocError({
55
+ message: "`.astro` files are no longer supported in the Markdoc config.",
56
+ hint: "Use the `component()` utility to specify a component path instead."
57
+ });
52
58
  return {
53
- // Stub with an unused default export
59
+ // Stub with an unused default export.
54
60
  path: "data:text/javascript,export default true",
55
61
  external: true
56
62
  };
@@ -59,6 +65,8 @@ async function bundleConfigFile({
59
65
  }
60
66
  ]
61
67
  });
68
+ if (markdocError)
69
+ throw markdocError;
62
70
  const { text } = result.outputFiles[0];
63
71
  return {
64
72
  code: text,
@@ -78,5 +86,6 @@ async function loadConfigFromBundledFile(root, code) {
78
86
  }
79
87
  }
80
88
  export {
89
+ SUPPORTED_MARKDOC_CONFIG_FILES,
81
90
  loadMarkdocConfig
82
91
  };
package/dist/runtime.d.ts CHANGED
@@ -1,16 +1,19 @@
1
1
  import type { MarkdownHeading } from '@astrojs/markdown-remark';
2
- import { type RenderableTreeNode } from '@markdoc/markdoc';
2
+ import { type NodeType, type RenderableTreeNode } from '@markdoc/markdoc';
3
+ import type { AstroInstance } from 'astro';
3
4
  import type { AstroMarkdocConfig } from './config.js';
4
- /** Used to call `Markdoc.transform()` and `Markdoc.Ast` in runtime modules */
5
- export { default as Markdoc } from '@markdoc/markdoc';
6
5
  /**
7
6
  * Merge user config with default config and set up context (ex. heading ID slugger)
8
7
  * Called on each file's individual transform.
9
8
  * TODO: virtual module to merge configs per-build instead of per-file?
10
9
  */
11
- export declare function setupConfig(userConfig: AstroMarkdocConfig): Promise<Omit<AstroMarkdocConfig, 'extends'>>;
10
+ export declare function setupConfig(userConfig?: AstroMarkdocConfig): Promise<MergedConfig>;
12
11
  /** Used for synchronous `getHeadings()` function */
13
- export declare function setupConfigSync(userConfig: AstroMarkdocConfig): Omit<AstroMarkdocConfig, 'extends'>;
12
+ export declare function setupConfigSync(userConfig?: AstroMarkdocConfig): MergedConfig;
13
+ type MergedConfig = Required<Omit<AstroMarkdocConfig, 'extends'>>;
14
+ /** Merge function from `@markdoc/markdoc` internals */
15
+ export declare function mergeConfig(configA: AstroMarkdocConfig, configB: AstroMarkdocConfig): MergedConfig;
16
+ export declare function resolveComponentImports(markdocConfig: Required<Pick<AstroMarkdocConfig, 'tags' | 'nodes'>>, tagComponentMap: Record<string, AstroInstance['default']>, nodeComponentMap: Record<NodeType, AstroInstance['default']>): Required<Pick<AstroMarkdocConfig, "tags" | "nodes">>;
14
17
  /**
15
18
  * Get text content as a string from a Markdoc transform AST
16
19
  */
@@ -19,4 +22,7 @@ export declare function getTextContent(childNodes: RenderableTreeNode[]): string
19
22
  * Collect headings from Markdoc transform AST
20
23
  * for `headings` result on `render()` return value
21
24
  */
22
- export declare function collectHeadings(children: RenderableTreeNode[]): MarkdownHeading[];
25
+ export declare function collectHeadings(children: RenderableTreeNode[], collectedHeadings: MarkdownHeading[]): void;
26
+ export declare function createGetHeadings(stringifiedAst: string, userConfig: AstroMarkdocConfig): () => MarkdownHeading[];
27
+ export declare function createContentComponent(Renderer: AstroInstance['default'], stringifiedAst: string, userConfig: AstroMarkdocConfig, tagComponentMap: Record<string, AstroInstance['default']>, nodeComponentMap: Record<NodeType, AstroInstance['default']>): any;
28
+ export {};
package/dist/runtime.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import Markdoc from "@markdoc/markdoc";
2
+ import { createComponent, renderComponent } from "astro/runtime/server/index.js";
2
3
  import { setupHeadingConfig } from "./heading-ids.js";
3
- import { default as default2 } from "@markdoc/markdoc";
4
- async function setupConfig(userConfig) {
4
+ async function setupConfig(userConfig = {}) {
5
5
  let defaultConfig = setupHeadingConfig();
6
6
  if (userConfig.extends) {
7
7
  for (let extension of userConfig.extends) {
@@ -13,7 +13,7 @@ async function setupConfig(userConfig) {
13
13
  }
14
14
  return mergeConfig(defaultConfig, userConfig);
15
15
  }
16
- function setupConfigSync(userConfig) {
16
+ function setupConfigSync(userConfig = {}) {
17
17
  const defaultConfig = setupHeadingConfig();
18
18
  return mergeConfig(defaultConfig, userConfig);
19
19
  }
@@ -40,9 +40,30 @@ function mergeConfig(configA, configB) {
40
40
  variables: {
41
41
  ...configA.variables,
42
42
  ...configB.variables
43
+ },
44
+ partials: {
45
+ ...configA.partials,
46
+ ...configB.partials
47
+ },
48
+ validation: {
49
+ ...configA.validation,
50
+ ...configB.validation
43
51
  }
44
52
  };
45
53
  }
54
+ function resolveComponentImports(markdocConfig, tagComponentMap, nodeComponentMap) {
55
+ for (const [tag, render] of Object.entries(tagComponentMap)) {
56
+ const config = markdocConfig.tags[tag];
57
+ if (config)
58
+ config.render = render;
59
+ }
60
+ for (const [node, render] of Object.entries(nodeComponentMap)) {
61
+ const config = markdocConfig.nodes[node];
62
+ if (config)
63
+ config.render = render;
64
+ }
65
+ return markdocConfig;
66
+ }
46
67
  function getTextContent(childNodes) {
47
68
  let text = "";
48
69
  for (const node of childNodes) {
@@ -55,8 +76,7 @@ function getTextContent(childNodes) {
55
76
  return text;
56
77
  }
57
78
  const headingLevels = [1, 2, 3, 4, 5, 6];
58
- function collectHeadings(children) {
59
- let collectedHeadings = [];
79
+ function collectHeadings(children, collectedHeadings) {
60
80
  for (const node of children) {
61
81
  if (typeof node !== "object" || !Markdoc.Tag.isTag(node))
62
82
  continue;
@@ -77,14 +97,40 @@ function collectHeadings(children) {
77
97
  });
78
98
  }
79
99
  }
80
- collectedHeadings.concat(collectHeadings(node.children));
100
+ collectHeadings(node.children, collectedHeadings);
81
101
  }
82
- return collectedHeadings;
102
+ }
103
+ function createGetHeadings(stringifiedAst, userConfig) {
104
+ return function getHeadings() {
105
+ const config = setupConfigSync(userConfig);
106
+ const ast = Markdoc.Ast.fromJSON(stringifiedAst);
107
+ const content = Markdoc.transform(ast, config);
108
+ let collectedHeadings = [];
109
+ collectHeadings(Array.isArray(content) ? content : [content], collectedHeadings);
110
+ return collectedHeadings;
111
+ };
112
+ }
113
+ function createContentComponent(Renderer, stringifiedAst, userConfig, tagComponentMap, nodeComponentMap) {
114
+ return createComponent({
115
+ async factory(result, props) {
116
+ const withVariables = mergeConfig(userConfig, { variables: props });
117
+ const config = resolveComponentImports(
118
+ await setupConfig(withVariables),
119
+ tagComponentMap,
120
+ nodeComponentMap
121
+ );
122
+ return renderComponent(result, Renderer.name, Renderer, { stringifiedAst, config }, {});
123
+ },
124
+ propagation: "self"
125
+ });
83
126
  }
84
127
  export {
85
- default2 as Markdoc,
86
128
  collectHeadings,
129
+ createContentComponent,
130
+ createGetHeadings,
87
131
  getTextContent,
132
+ mergeConfig,
133
+ resolveComponentImports,
88
134
  setupConfig,
89
135
  setupConfigSync
90
136
  };
package/dist/utils.d.ts CHANGED
@@ -1,15 +1,9 @@
1
- import matter from 'gray-matter';
2
- /**
3
- * Match YAML exception handling from Astro core errors
4
- * @see 'astro/src/core/errors.ts'
5
- */
6
- export declare function parseFrontmatter(fileContents: string, filePath: string): matter.GrayMatterFile<string>;
1
+ import type { ComponentConfig } from './config.js';
7
2
  /**
8
3
  * Matches AstroError object with types like error codes stubbed out
9
4
  * @see 'astro/src/core/errors/errors.ts'
10
5
  */
11
6
  export declare class MarkdocError extends Error {
12
- errorCode: number;
13
7
  loc: ErrorLocation | undefined;
14
8
  title: string | undefined;
15
9
  hint: string | undefined;
@@ -46,4 +40,7 @@ export declare const PROPAGATED_ASSET_FLAG = "astroPropagatedAssets";
46
40
  * @see 'packages/astro/src/content/utils.ts'
47
41
  */
48
42
  export declare function hasContentFlag(viteId: string, flag: string): boolean;
43
+ /** Identifier for components imports passed as `tags` or `nodes` configuration. */
44
+ export declare const componentConfigSymbol: unique symbol;
45
+ export declare function isComponentConfig(value: unknown): value is ComponentConfig;
49
46
  export {};
package/dist/utils.js CHANGED
@@ -1,37 +1,8 @@
1
- import matter from "gray-matter";
2
- function parseFrontmatter(fileContents, filePath) {
3
- try {
4
- matter.clearCache();
5
- return matter(fileContents);
6
- } catch (e) {
7
- if (e.name === "YAMLException") {
8
- const err = e;
9
- err.id = filePath;
10
- err.loc = { file: e.id, line: e.mark.line + 1, column: e.mark.column };
11
- err.message = e.reason;
12
- throw err;
13
- } else {
14
- throw e;
15
- }
16
- }
17
- }
18
1
  class MarkdocError extends Error {
19
2
  constructor(props, ...params) {
20
3
  super(...params);
21
4
  this.type = "MarkdocError";
22
- const {
23
- // Use default code for unknown errors in Astro core
24
- // We don't have a best practice for integration error codes yet
25
- code = 99999,
26
- name,
27
- title = "MarkdocError",
28
- message,
29
- stack,
30
- location,
31
- hint,
32
- frame
33
- } = props;
34
- this.errorCode = code;
5
+ const { title = "MarkdocError", message, stack, location, hint, frame } = props;
35
6
  this.title = title;
36
7
  if (message)
37
8
  this.message = message;
@@ -57,11 +28,16 @@ function hasContentFlag(viteId, flag) {
57
28
  const flags = new URLSearchParams(viteId.split("?")[1] ?? "");
58
29
  return flags.has(flag);
59
30
  }
31
+ const componentConfigSymbol = Symbol.for("@astrojs/markdoc/component-config");
32
+ function isComponentConfig(value) {
33
+ return typeof value === "object" && value !== null && componentConfigSymbol in value;
34
+ }
60
35
  export {
61
36
  MarkdocError,
62
37
  PROPAGATED_ASSET_FLAG,
38
+ componentConfigSymbol,
63
39
  hasContentFlag,
40
+ isComponentConfig,
64
41
  isValidUrl,
65
- parseFrontmatter,
66
42
  prependForwardSlash
67
43
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@astrojs/markdoc",
3
3
  "description": "Add support for Markdoc in your Astro site",
4
- "version": "0.3.3",
4
+ "version": "0.4.1",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
7
7
  "author": "withastro",
@@ -56,9 +56,10 @@
56
56
  "template"
57
57
  ],
58
58
  "dependencies": {
59
+ "@astrojs/internal-helpers": "^0.1.0",
59
60
  "@astrojs/prism": "^2.1.2",
60
61
  "@markdoc/markdoc": "^0.3.0",
61
- "esbuild": "^0.17.12",
62
+ "esbuild": "^0.17.19",
62
63
  "github-slugger": "^2.0.0",
63
64
  "gray-matter": "^4.0.3",
64
65
  "kleur": "^4.1.5",
@@ -66,20 +67,20 @@
66
67
  "zod": "^3.17.3"
67
68
  },
68
69
  "peerDependencies": {
69
- "astro": "^2.6.3"
70
+ "astro": "^2.8.0"
70
71
  },
71
72
  "devDependencies": {
72
73
  "@astrojs/markdown-remark": "^2.2.1",
73
- "@types/chai": "^4.3.1",
74
+ "@types/chai": "^4.3.5",
74
75
  "@types/html-escaper": "^3.0.0",
75
76
  "@types/mocha": "^9.1.1",
76
- "chai": "^4.3.6",
77
+ "chai": "^4.3.7",
77
78
  "devalue": "^4.3.2",
78
- "linkedom": "^0.14.12",
79
+ "linkedom": "^0.14.26",
79
80
  "mocha": "^9.2.2",
80
- "rollup": "^3.20.1",
81
- "vite": "^4.3.1",
82
- "astro": "2.6.3",
81
+ "rollup": "^3.25.1",
82
+ "vite": "^4.3.9",
83
+ "astro": "2.8.0",
83
84
  "astro-scripts": "0.0.14"
84
85
  },
85
86
  "engines": {