@astrojs/markdoc 0.2.0 → 0.2.2

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.
@@ -1,4 +1,5 @@
1
1
  ---
2
+ //! astro-head-inject
2
3
  import type { Config } from '@markdoc/markdoc';
3
4
  import Markdoc from '@markdoc/markdoc';
4
5
  import { ComponentNode, createTreeNode } from './TreeNode.js';
@@ -14,4 +15,4 @@ const ast = Markdoc.Ast.fromJSON(stringifiedAst);
14
15
  const content = Markdoc.transform(ast, config);
15
16
  ---
16
17
 
17
- <ComponentNode treeNode={createTreeNode(content)} />
18
+ <ComponentNode treeNode={await createTreeNode(content)} />
@@ -2,7 +2,16 @@ import type { AstroInstance } from 'astro';
2
2
  import { Fragment } from 'astro/jsx-runtime';
3
3
  import type { RenderableTreeNode } from '@markdoc/markdoc';
4
4
  import Markdoc from '@markdoc/markdoc';
5
- import { createComponent, renderComponent, render } from 'astro/runtime/server/index.js';
5
+ import {
6
+ createComponent,
7
+ renderComponent,
8
+ render,
9
+ renderScriptElement,
10
+ renderUniqueStylesheet,
11
+ createHeadAndContent,
12
+ unescapeHTML,
13
+ renderTemplate,
14
+ } from 'astro/runtime/server/index.js';
6
15
 
7
16
  export type TreeNode =
8
17
  | {
@@ -12,6 +21,9 @@ export type TreeNode =
12
21
  | {
13
22
  type: 'component';
14
23
  component: AstroInstance['default'];
24
+ collectedLinks?: string[];
25
+ collectedStyles?: string[];
26
+ collectedScripts?: string[];
15
27
  props: Record<string, any>;
16
28
  children: TreeNode[];
17
29
  }
@@ -32,20 +44,67 @@ export const ComponentNode = createComponent({
32
44
  )}`,
33
45
  };
34
46
  if (treeNode.type === 'component') {
35
- return renderComponent(
36
- result,
37
- treeNode.component.name,
38
- treeNode.component,
39
- treeNode.props,
40
- slots
47
+ let styles = '',
48
+ links = '',
49
+ scripts = '';
50
+ if (Array.isArray(treeNode.collectedStyles)) {
51
+ styles = treeNode.collectedStyles
52
+ .map((style: any) =>
53
+ renderUniqueStylesheet({
54
+ type: 'inline',
55
+ content: style,
56
+ })
57
+ )
58
+ .join('');
59
+ }
60
+ if (Array.isArray(treeNode.collectedLinks)) {
61
+ links = treeNode.collectedLinks
62
+ .map((link: any) => {
63
+ return renderUniqueStylesheet(result, {
64
+ href: link[0] === '/' ? link : '/' + link,
65
+ });
66
+ })
67
+ .join('');
68
+ }
69
+ if (Array.isArray(treeNode.collectedScripts)) {
70
+ scripts = treeNode.collectedScripts
71
+ .map((script: any) => renderScriptElement(script))
72
+ .join('');
73
+ }
74
+
75
+ const head = unescapeHTML(styles + links + scripts);
76
+
77
+ let headAndContent = createHeadAndContent(
78
+ head,
79
+ renderTemplate`${renderComponent(
80
+ result,
81
+ treeNode.component.name,
82
+ treeNode.component,
83
+ treeNode.props,
84
+ slots
85
+ )}`
41
86
  );
87
+
88
+ // Let the runtime know that this component is being used.
89
+ result.propagators.set(
90
+ {},
91
+ {
92
+ init() {
93
+ return headAndContent;
94
+ },
95
+ }
96
+ );
97
+
98
+ return headAndContent;
42
99
  }
43
100
  return renderComponent(result, treeNode.tag, treeNode.tag, treeNode.attributes, slots);
44
101
  },
45
- propagation: 'none',
102
+ propagation: 'self',
46
103
  });
47
104
 
48
- export function createTreeNode(node: RenderableTreeNode | RenderableTreeNode[]): TreeNode {
105
+ export async function createTreeNode(
106
+ node: RenderableTreeNode | RenderableTreeNode[]
107
+ ): Promise<TreeNode> {
49
108
  if (typeof node === 'string' || typeof node === 'number') {
50
109
  return { type: 'text', content: String(node) };
51
110
  } else if (Array.isArray(node)) {
@@ -53,16 +112,17 @@ export function createTreeNode(node: RenderableTreeNode | RenderableTreeNode[]):
53
112
  type: 'component',
54
113
  component: Fragment,
55
114
  props: {},
56
- children: node.map((child) => createTreeNode(child)),
115
+ children: await Promise.all(node.map((child) => createTreeNode(child))),
57
116
  };
58
117
  } else if (node === null || typeof node !== 'object' || !Markdoc.Tag.isTag(node)) {
59
118
  return { type: 'text', content: '' };
60
119
  }
61
120
 
121
+ const children = await Promise.all(node.children.map((child) => createTreeNode(child)));
122
+
62
123
  if (typeof node.name === 'function') {
63
124
  const component = node.name;
64
125
  const props = node.attributes;
65
- const children = node.children.map((child) => createTreeNode(child));
66
126
 
67
127
  return {
68
128
  type: 'component',
@@ -70,12 +130,38 @@ export function createTreeNode(node: RenderableTreeNode | RenderableTreeNode[]):
70
130
  props,
71
131
  children,
72
132
  };
133
+ } else if (isPropagatedAssetsModule(node.name)) {
134
+ const { collectedStyles, collectedLinks, collectedScripts } = node.name;
135
+ const component = (await node.name.getMod())?.default ?? Fragment;
136
+ const props = node.attributes;
137
+
138
+ return {
139
+ type: 'component',
140
+ component,
141
+ collectedStyles,
142
+ collectedLinks,
143
+ collectedScripts,
144
+ props,
145
+ children,
146
+ };
73
147
  } else {
74
148
  return {
75
149
  type: 'element',
76
150
  tag: node.name,
77
151
  attributes: node.attributes,
78
- children: node.children.map((child) => createTreeNode(child)),
152
+ children,
79
153
  };
80
154
  }
81
155
  }
156
+
157
+ type PropagatedAssetsModule = {
158
+ __astroPropagation: true;
159
+ getMod: () => Promise<AstroInstance['default']>;
160
+ collectedStyles: string[];
161
+ collectedLinks: string[];
162
+ collectedScripts: string[];
163
+ };
164
+
165
+ function isPropagatedAssetsModule(module: any): module is PropagatedAssetsModule {
166
+ return typeof module === 'object' && module != null && '__astroPropagation' in module;
167
+ }
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { isValidUrl, MarkdocError, parseFrontmatter, prependForwardSlash } from
5
5
  import { emitESMImage } from "astro/assets";
6
6
  import { bold, red, yellow } from "kleur/colors";
7
7
  import { loadMarkdocConfig } from "./load-config.js";
8
- import { applyDefaultConfig } from "./runtime.js";
8
+ import { setupConfig } from "./runtime.js";
9
9
  function markdocIntegration(legacyConfig) {
10
10
  if (legacyConfig) {
11
11
  console.log(
@@ -20,7 +20,11 @@ function markdocIntegration(legacyConfig) {
20
20
  name: "@astrojs/markdoc",
21
21
  hooks: {
22
22
  "astro:config:setup": async (params) => {
23
- const { config: astroConfig, addContentEntryType } = params;
23
+ const {
24
+ config: astroConfig,
25
+ updateConfig,
26
+ addContentEntryType
27
+ } = params;
24
28
  markdocConfigResult = await loadMarkdocConfig(astroConfig);
25
29
  const userMarkdocConfig = (markdocConfigResult == null ? void 0 : markdocConfigResult.config) ?? {};
26
30
  function getEntryInfo({ fileUrl, contents }) {
@@ -35,10 +39,13 @@ function markdocIntegration(legacyConfig) {
35
39
  addContentEntryType({
36
40
  extensions: [".mdoc"],
37
41
  getEntryInfo,
42
+ // Markdoc handles script / style propagation
43
+ // for Astro components internally
44
+ handlePropagation: false,
38
45
  async getRenderModule({ entry, viteId }) {
39
46
  const ast = Markdoc.parse(entry.body);
40
47
  const pluginContext = this;
41
- const markdocConfig = applyDefaultConfig(userMarkdocConfig, entry);
48
+ const markdocConfig = setupConfig(userMarkdocConfig, entry);
42
49
  const validationErrors = Markdoc.validate(ast, markdocConfig).filter((e) => {
43
50
  return (
44
51
  // Ignore `variable-undefined` errors.
@@ -69,9 +76,12 @@ function markdocIntegration(legacyConfig) {
69
76
  filePath: entry._internal.filePath
70
77
  });
71
78
  }
72
- const res = `import { jsx as h } from 'astro/jsx-runtime';
79
+ const res = `import {
80
+ createComponent,
81
+ renderComponent,
82
+ } from 'astro/runtime/server/index.js';
73
83
  import { Renderer } from '@astrojs/markdoc/components';
74
- import { collectHeadings, applyDefaultConfig, Markdoc, headingSlugger } from '@astrojs/markdoc/runtime';
84
+ import { collectHeadings, setupConfig, Markdoc } from '@astrojs/markdoc/runtime';
75
85
  import * as entry from ${JSON.stringify(viteId + "?astroContentCollectionEntry")};
76
86
  ${markdocConfigResult ? `import _userConfig from ${JSON.stringify(
77
87
  markdocConfigResult.fileUrl.pathname
@@ -88,22 +98,30 @@ export function getHeadings() {
88
98
  TODO: propose new `render()` API to allow Markdoc variable passing to `render()` itself,
89
99
  instead of the Content component. Would remove double-transform and unlock variable resolution in heading slugs. */
90
100
  ""}
91
- headingSlugger.reset();
92
101
  const headingConfig = userConfig.nodes?.heading;
93
- const config = applyDefaultConfig(headingConfig ? { nodes: { heading: headingConfig } } : {}, entry);
102
+ const config = setupConfig(headingConfig ? { nodes: { heading: headingConfig } } : {}, entry);
94
103
  const ast = Markdoc.Ast.fromJSON(stringifiedAst);
95
104
  const content = Markdoc.transform(ast, config);
96
105
  return collectHeadings(Array.isArray(content) ? content : content.children);
97
106
  }
98
- export async function Content (props) {
99
- headingSlugger.reset();
100
- const config = applyDefaultConfig({
101
- ...userConfig,
102
- variables: { ...userConfig.variables, ...props },
103
- }, entry);
104
107
 
105
- return h(Renderer, { config, stringifiedAst });
106
- }`;
108
+ export const Content = createComponent({
109
+ factory(result, props) {
110
+ const config = setupConfig({
111
+ ...userConfig,
112
+ variables: { ...userConfig.variables, ...props },
113
+ }, entry);
114
+
115
+ return renderComponent(
116
+ result,
117
+ Renderer.name,
118
+ Renderer,
119
+ { stringifiedAst, config },
120
+ {}
121
+ );
122
+ },
123
+ propagation: 'self',
124
+ });`;
107
125
  return { code: res };
108
126
  },
109
127
  contentModuleTypes: await fs.promises.readFile(
@@ -111,6 +129,26 @@ export async function Content (props) {
111
129
  "utf-8"
112
130
  )
113
131
  });
132
+ updateConfig({
133
+ vite: {
134
+ plugins: [
135
+ {
136
+ name: "@astrojs/markdoc:astro-propagated-assets",
137
+ enforce: "pre",
138
+ // Astro component styles and scripts should only be injected
139
+ // When a given Markdoc file actually uses that component.
140
+ // Add the `astroPropagatedAssets` flag to inject only when rendered.
141
+ resolveId(id, importer) {
142
+ if (importer === (markdocConfigResult == null ? void 0 : markdocConfigResult.fileUrl.pathname) && id.endsWith(".astro")) {
143
+ return this.resolve(id + "?astroPropagatedAssets", importer, {
144
+ skipSelf: true
145
+ });
146
+ }
147
+ }
148
+ }
149
+ ]
150
+ }
151
+ });
114
152
  },
115
153
  "astro:server:setup": async ({ server }) => {
116
154
  server.watcher.on("all", (event, entry) => {
@@ -1,4 +1,10 @@
1
- import { type Schema } from '@markdoc/markdoc';
1
+ import { type ConfigType, type Schema } from '@markdoc/markdoc';
2
2
  import Slugger from 'github-slugger';
3
- export declare const headingSlugger: Slugger;
3
+ type ConfigTypeWithCtx = ConfigType & {
4
+ ctx: {
5
+ headingSlugger: Slugger;
6
+ };
7
+ };
4
8
  export declare const heading: Schema;
9
+ export declare function setupHeadingConfig(): ConfigTypeWithCtx;
10
+ export {};
@@ -1,8 +1,7 @@
1
1
  import Markdoc from "@markdoc/markdoc";
2
2
  import Slugger from "github-slugger";
3
3
  import { getTextContent } from "../runtime.js";
4
- const headingSlugger = new Slugger();
5
- function getSlug(attributes, children) {
4
+ function getSlug(attributes, children, headingSlugger) {
6
5
  if (attributes.id && typeof attributes.id === "string") {
7
6
  return attributes.id;
8
7
  }
@@ -22,18 +21,29 @@ const heading = {
22
21
  var _a, _b;
23
22
  const { level, ...attributes } = node.transformAttributes(config);
24
23
  const children = node.transformChildren(config);
25
- const slug = getSlug(attributes, children);
24
+ const slug = getSlug(attributes, children, config.ctx.headingSlugger);
26
25
  const render = ((_b = (_a = config.nodes) == null ? void 0 : _a.heading) == null ? void 0 : _b.render) ?? `h${level}`;
27
26
  const tagProps = (
28
27
  // For components, pass down `level` as a prop,
29
28
  // alongside `__collectHeading` for our `headings` collector.
30
29
  // Avoid accidentally rendering `level` as an HTML attribute otherwise!
31
- typeof render === "function" ? { ...attributes, id: slug, __collectHeading: true, level } : { ...attributes, id: slug }
30
+ typeof render === "string" ? { ...attributes, id: slug } : { ...attributes, id: slug, __collectHeading: true, level }
32
31
  );
33
32
  return new Markdoc.Tag(render, tagProps, children);
34
33
  }
35
34
  };
35
+ function setupHeadingConfig() {
36
+ const headingSlugger = new Slugger();
37
+ return {
38
+ ctx: {
39
+ headingSlugger
40
+ },
41
+ nodes: {
42
+ heading
43
+ }
44
+ };
45
+ }
36
46
  export {
37
47
  heading,
38
- headingSlugger
48
+ setupHeadingConfig
39
49
  };
@@ -1,4 +1,4 @@
1
- export { headingSlugger } from './heading.js';
1
+ export { setupHeadingConfig } from './heading.js';
2
2
  export declare const nodes: {
3
3
  heading: import("@markdoc/markdoc").Schema;
4
4
  };
@@ -1,7 +1,7 @@
1
1
  import { heading } from "./heading.js";
2
- import { headingSlugger } from "./heading.js";
2
+ import { setupHeadingConfig } from "./heading.js";
3
3
  const nodes = { heading };
4
4
  export {
5
- headingSlugger,
6
- nodes
5
+ nodes,
6
+ setupHeadingConfig
7
7
  };
package/dist/runtime.d.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  import type { MarkdownHeading } from '@astrojs/markdown-remark';
2
2
  import { type ConfigType as MarkdocConfig, type RenderableTreeNode } from '@markdoc/markdoc';
3
3
  import type { ContentEntryModule } from 'astro';
4
- /** Used to reset Slugger cache on each build at runtime */
4
+ /** Used to call `Markdoc.transform()` and `Markdoc.Ast` in runtime modules */
5
5
  export { default as Markdoc } from '@markdoc/markdoc';
6
- export { headingSlugger } from './nodes/index.js';
7
- export declare function applyDefaultConfig(config: MarkdocConfig, entry: ContentEntryModule): MarkdocConfig;
6
+ /**
7
+ * Merge user config with default config and set up context (ex. heading ID slugger)
8
+ * Called on each file's individual transform
9
+ */
10
+ export declare function setupConfig(userConfig: MarkdocConfig, entry: ContentEntryModule): MarkdocConfig;
8
11
  /**
9
12
  * Get text content as a string from a Markdoc transform AST
10
13
  */
package/dist/runtime.js CHANGED
@@ -1,19 +1,34 @@
1
1
  import Markdoc from "@markdoc/markdoc";
2
- import { nodes as astroNodes } from "./nodes/index.js";
2
+ import { setupHeadingConfig } from "./nodes/index.js";
3
3
  import { default as default2 } from "@markdoc/markdoc";
4
- import { headingSlugger } from "./nodes/index.js";
5
- function applyDefaultConfig(config, entry) {
4
+ function setupConfig(userConfig, entry) {
5
+ const defaultConfig = {
6
+ // `setupXConfig()` could become a "plugin" convention as well?
7
+ ...setupHeadingConfig(),
8
+ variables: { entry }
9
+ };
10
+ return mergeConfig(defaultConfig, userConfig);
11
+ }
12
+ function mergeConfig(configA, configB) {
6
13
  return {
7
- ...config,
8
- variables: {
9
- entry,
10
- ...config.variables
14
+ ...configA,
15
+ ...configB,
16
+ tags: {
17
+ ...configA.tags,
18
+ ...configB.tags
11
19
  },
12
20
  nodes: {
13
- ...astroNodes,
14
- ...config.nodes
21
+ ...configA.nodes,
22
+ ...configB.nodes
23
+ },
24
+ functions: {
25
+ ...configA.functions,
26
+ ...configB.functions
27
+ },
28
+ variables: {
29
+ ...configA.variables,
30
+ ...configB.variables
15
31
  }
16
- // TODO: Syntax highlighting
17
32
  };
18
33
  }
19
34
  function getTextContent(childNodes) {
@@ -56,8 +71,7 @@ function collectHeadings(children) {
56
71
  }
57
72
  export {
58
73
  default2 as Markdoc,
59
- applyDefaultConfig,
60
74
  collectHeadings,
61
75
  getTextContent,
62
- headingSlugger
76
+ setupConfig
63
77
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@astrojs/markdoc",
3
3
  "description": "Add support for Markdoc pages in your Astro site",
4
- "version": "0.2.0",
4
+ "version": "0.2.2",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
7
7
  "author": "withastro",
@@ -40,7 +40,7 @@
40
40
  "zod": "^3.17.3"
41
41
  },
42
42
  "peerDependencies": {
43
- "astro": "^2.5.0"
43
+ "astro": "^2.5.3"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@astrojs/markdown-remark": "^2.2.1",
@@ -53,7 +53,7 @@
53
53
  "mocha": "^9.2.2",
54
54
  "rollup": "^3.20.1",
55
55
  "vite": "^4.3.1",
56
- "astro": "2.5.0",
56
+ "astro": "2.5.3",
57
57
  "astro-scripts": "0.0.14"
58
58
  },
59
59
  "engines": {