@astrojs/markdoc 0.3.0 → 0.3.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.
@@ -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)} />
@@ -6,6 +6,11 @@ import {
6
6
  createComponent,
7
7
  renderComponent,
8
8
  render,
9
+ renderScriptElement,
10
+ renderUniqueStylesheet,
11
+ createHeadAndContent,
12
+ unescapeHTML,
13
+ renderTemplate,
9
14
  HTMLString,
10
15
  isHTMLString,
11
16
  } from 'astro/runtime/server/index.js';
@@ -18,6 +23,9 @@ export type TreeNode =
18
23
  | {
19
24
  type: 'component';
20
25
  component: AstroInstance['default'];
26
+ collectedLinks?: string[];
27
+ collectedStyles?: string[];
28
+ collectedScripts?: string[];
21
29
  props: Record<string, any>;
22
30
  children: TreeNode[];
23
31
  }
@@ -39,20 +47,66 @@ export const ComponentNode = createComponent({
39
47
  )}`,
40
48
  };
41
49
  if (treeNode.type === 'component') {
42
- return renderComponent(
43
- result,
44
- treeNode.component.name,
45
- treeNode.component,
46
- treeNode.props,
47
- slots
50
+ let styles = '',
51
+ links = '',
52
+ scripts = '';
53
+ if (Array.isArray(treeNode.collectedStyles)) {
54
+ styles = treeNode.collectedStyles
55
+ .map((style: any) =>
56
+ renderUniqueStylesheet(result, {
57
+ type: 'inline',
58
+ content: style,
59
+ })
60
+ )
61
+ .join('');
62
+ }
63
+ if (Array.isArray(treeNode.collectedLinks)) {
64
+ links = treeNode.collectedLinks
65
+ .map((link: any) => {
66
+ return renderUniqueStylesheet(result, {
67
+ type: 'external',
68
+ src: link[0] === '/' ? link : '/' + link,
69
+ });
70
+ })
71
+ .join('');
72
+ }
73
+ if (Array.isArray(treeNode.collectedScripts)) {
74
+ scripts = treeNode.collectedScripts
75
+ .map((script: any) => renderScriptElement(script))
76
+ .join('');
77
+ }
78
+
79
+ const head = unescapeHTML(styles + links + scripts);
80
+
81
+ let headAndContent = createHeadAndContent(
82
+ head,
83
+ renderTemplate`${renderComponent(
84
+ result,
85
+ treeNode.component.name,
86
+ treeNode.component,
87
+ treeNode.props,
88
+ slots
89
+ )}`
48
90
  );
91
+
92
+ // Let the runtime know that this component is being used.
93
+ result.propagators.set(
94
+ {},
95
+ {
96
+ init() {
97
+ return headAndContent;
98
+ },
99
+ }
100
+ );
101
+
102
+ return headAndContent;
49
103
  }
50
104
  return renderComponent(result, treeNode.tag, treeNode.tag, treeNode.attributes, slots);
51
105
  },
52
- propagation: 'none',
106
+ propagation: 'self',
53
107
  });
54
108
 
55
- export function createTreeNode(node: RenderableTreeNode | RenderableTreeNode[]): TreeNode {
109
+ export async function createTreeNode(node: RenderableTreeNode | RenderableTreeNode[]): TreeNode {
56
110
  if (isHTMLString(node)) {
57
111
  return { type: 'text', content: node as HTMLString };
58
112
  } else if (typeof node === 'string' || typeof node === 'number') {
@@ -62,16 +116,17 @@ export function createTreeNode(node: RenderableTreeNode | RenderableTreeNode[]):
62
116
  type: 'component',
63
117
  component: Fragment,
64
118
  props: {},
65
- children: node.map((child) => createTreeNode(child)),
119
+ children: await Promise.all(node.map((child) => createTreeNode(child))),
66
120
  };
67
121
  } else if (node === null || typeof node !== 'object' || !Markdoc.Tag.isTag(node)) {
68
122
  return { type: 'text', content: '' };
69
123
  }
70
124
 
125
+ const children = await Promise.all(node.children.map((child) => createTreeNode(child)));
126
+
71
127
  if (typeof node.name === 'function') {
72
128
  const component = node.name;
73
129
  const props = node.attributes;
74
- const children = node.children.map((child) => createTreeNode(child));
75
130
 
76
131
  return {
77
132
  type: 'component',
@@ -79,12 +134,38 @@ export function createTreeNode(node: RenderableTreeNode | RenderableTreeNode[]):
79
134
  props,
80
135
  children,
81
136
  };
137
+ } else if (isPropagatedAssetsModule(node.name)) {
138
+ const { collectedStyles, collectedLinks, collectedScripts } = node.name;
139
+ const component = (await node.name.getMod())?.default ?? Fragment;
140
+ const props = node.attributes;
141
+
142
+ return {
143
+ type: 'component',
144
+ component,
145
+ collectedStyles,
146
+ collectedLinks,
147
+ collectedScripts,
148
+ props,
149
+ children,
150
+ };
82
151
  } else {
83
152
  return {
84
153
  type: 'element',
85
154
  tag: node.name,
86
155
  attributes: node.attributes,
87
- children: node.children.map((child) => createTreeNode(child)),
156
+ children,
88
157
  };
89
158
  }
90
159
  }
160
+
161
+ type PropagatedAssetsModule = {
162
+ __astroPropagation: true;
163
+ getMod: () => Promise<AstroInstance['default']>;
164
+ collectedStyles: string[];
165
+ collectedLinks: string[];
166
+ collectedScripts: string[];
167
+ };
168
+
169
+ function isPropagatedAssetsModule(module: any): module is PropagatedAssetsModule {
170
+ return typeof module === 'object' && module != null && '__astroPropagation' in module;
171
+ }
@@ -33,7 +33,7 @@ const heading = {
33
33
  // For components, pass down `level` as a prop,
34
34
  // alongside `__collectHeading` for our `headings` collector.
35
35
  // Avoid accidentally rendering `level` as an HTML attribute otherwise!
36
- typeof render === "function" ? { ...attributes, id: slug, __collectHeading: true, level } : { ...attributes, id: slug }
36
+ typeof render === "string" ? { ...attributes, id: slug } : { ...attributes, id: slug, __collectHeading: true, level }
37
37
  );
38
38
  return new Markdoc.Tag(render, tagProps, children);
39
39
  }
package/dist/index.js CHANGED
@@ -1,12 +1,26 @@
1
1
  import Markdoc from "@markdoc/markdoc";
2
2
  import fs from "node:fs";
3
3
  import { fileURLToPath, pathToFileURL } from "node:url";
4
- import { isValidUrl, MarkdocError, parseFrontmatter, prependForwardSlash } from "./utils.js";
4
+ import {
5
+ createNameHash,
6
+ hasContentFlag,
7
+ isValidUrl,
8
+ MarkdocError,
9
+ parseFrontmatter,
10
+ prependForwardSlash,
11
+ PROPAGATED_ASSET_FLAG
12
+ } from "./utils.js";
5
13
  import { emitESMImage } from "astro/assets";
6
14
  import { bold, red, yellow } from "kleur/colors";
7
15
  import path from "node:path";
16
+ import { normalizePath } from "vite";
8
17
  import { loadMarkdocConfig } from "./load-config.js";
9
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
+ });
10
24
  function markdocIntegration(legacyConfig) {
11
25
  if (legacyConfig) {
12
26
  console.log(
@@ -17,6 +31,7 @@ function markdocIntegration(legacyConfig) {
17
31
  process.exit(0);
18
32
  }
19
33
  let markdocConfigResult;
34
+ let markdocConfigResultId = "";
20
35
  return {
21
36
  name: "@astrojs/markdoc",
22
37
  hooks: {
@@ -26,14 +41,10 @@ function markdocIntegration(legacyConfig) {
26
41
  updateConfig,
27
42
  addContentEntryType
28
43
  } = params;
29
- updateConfig({
30
- vite: {
31
- ssr: {
32
- external: ["@astrojs/markdoc/prism", "@astrojs/markdoc/shiki"]
33
- }
34
- }
35
- });
36
44
  markdocConfigResult = await loadMarkdocConfig(astroConfig);
45
+ if (markdocConfigResult) {
46
+ markdocConfigResultId = normalizePath(fileURLToPath(markdocConfigResult.fileUrl));
47
+ }
37
48
  const userMarkdocConfig = (markdocConfigResult == null ? void 0 : markdocConfigResult.config) ?? {};
38
49
  function getEntryInfo({ fileUrl, contents }) {
39
50
  const parsed = parseFrontmatter(contents, fileURLToPath(fileUrl));
@@ -47,9 +58,13 @@ function markdocIntegration(legacyConfig) {
47
58
  addContentEntryType({
48
59
  extensions: [".mdoc"],
49
60
  getEntryInfo,
61
+ // Markdoc handles script / style propagation
62
+ // for Astro components internally
63
+ handlePropagation: false,
50
64
  async getRenderModule({ contents, fileUrl, viteId }) {
51
65
  const entry = getEntryInfo({ contents, fileUrl });
52
- const ast = Markdoc.parse(entry.body);
66
+ const tokens = markdocTokenizer.tokenize(entry.body);
67
+ const ast = Markdoc.parse(tokens);
53
68
  const pluginContext = this;
54
69
  const markdocConfig = await setupConfig(userMarkdocConfig);
55
70
  const filePath = fileURLToPath(fileUrl);
@@ -84,12 +99,14 @@ function markdocIntegration(legacyConfig) {
84
99
  filePath
85
100
  });
86
101
  }
87
- const res = `import { jsx as h } from 'astro/jsx-runtime';
102
+ const res = `import {
103
+ createComponent,
104
+ renderComponent,
105
+ } from 'astro/runtime/server/index.js';
88
106
  import { Renderer } from '@astrojs/markdoc/components';
89
107
  import { collectHeadings, setupConfig, setupConfigSync, Markdoc } from '@astrojs/markdoc/runtime';
90
- import * as entry from ${JSON.stringify(viteId + "?astroContentCollectionEntry")};
91
108
  ${markdocConfigResult ? `import _userConfig from ${JSON.stringify(
92
- markdocConfigResult.fileUrl.pathname
109
+ markdocConfigResultId
93
110
  )};
94
111
  const userConfig = _userConfig ?? {};` : "const userConfig = {};"}${astroConfig.experimental.assets ? `
95
112
  import { experimentalAssetsConfig } from '@astrojs/markdoc/experimental-assets-config';
@@ -104,19 +121,29 @@ export function getHeadings() {
104
121
  instead of the Content component. Would remove double-transform and unlock variable resolution in heading slugs. */
105
122
  ""}
106
123
  const headingConfig = userConfig.nodes?.heading;
107
- const config = setupConfigSync(headingConfig ? { nodes: { heading: headingConfig } } : {}, entry);
124
+ const config = setupConfigSync(headingConfig ? { nodes: { heading: headingConfig } } : {});
108
125
  const ast = Markdoc.Ast.fromJSON(stringifiedAst);
109
126
  const content = Markdoc.transform(ast, config);
110
127
  return collectHeadings(Array.isArray(content) ? content : content.children);
111
128
  }
112
- export async function Content (props) {
113
- const config = await setupConfig({
114
- ...userConfig,
115
- variables: { ...userConfig.variables, ...props },
116
- }, entry);
117
129
 
118
- return h(Renderer, { config, stringifiedAst });
119
- }`;
130
+ export const Content = createComponent({
131
+ async factory(result, props) {
132
+ const config = await setupConfig({
133
+ ...userConfig,
134
+ variables: { ...userConfig.variables, ...props },
135
+ });
136
+
137
+ return renderComponent(
138
+ result,
139
+ Renderer.name,
140
+ Renderer,
141
+ { stringifiedAst, config },
142
+ {}
143
+ );
144
+ },
145
+ propagation: 'self',
146
+ });`;
120
147
  return { code: res };
121
148
  },
122
149
  contentModuleTypes: await fs.promises.readFile(
@@ -124,10 +151,53 @@ export async function Content (props) {
124
151
  "utf-8"
125
152
  )
126
153
  });
154
+ let rollupOptions = {};
155
+ if (markdocConfigResult) {
156
+ rollupOptions = {
157
+ output: {
158
+ // Split Astro components from your `markdoc.config`
159
+ // to only inject component styles and scripts at runtime.
160
+ manualChunks(id, { getModuleInfo }) {
161
+ var _a, _b;
162
+ if (markdocConfigResult && hasContentFlag(id, PROPAGATED_ASSET_FLAG) && ((_b = (_a = getModuleInfo(id)) == null ? void 0 : _a.importers) == null ? void 0 : _b.includes(markdocConfigResultId))) {
163
+ return createNameHash(id, [id]);
164
+ }
165
+ }
166
+ }
167
+ };
168
+ }
169
+ updateConfig({
170
+ vite: {
171
+ vite: {
172
+ ssr: {
173
+ external: ["@astrojs/markdoc/prism", "@astrojs/markdoc/shiki"]
174
+ }
175
+ },
176
+ build: {
177
+ rollupOptions
178
+ },
179
+ plugins: [
180
+ {
181
+ name: "@astrojs/markdoc:astro-propagated-assets",
182
+ enforce: "pre",
183
+ // Astro component styles and scripts should only be injected
184
+ // When a given Markdoc file actually uses that component.
185
+ // Add the `astroPropagatedAssets` flag to inject only when rendered.
186
+ resolveId(id, importer) {
187
+ if (importer === markdocConfigResultId && id.endsWith(".astro")) {
188
+ return this.resolve(id + "?astroPropagatedAssets", importer, {
189
+ skipSelf: true
190
+ });
191
+ }
192
+ }
193
+ }
194
+ ]
195
+ }
196
+ });
127
197
  },
128
198
  "astro:server:setup": async ({ server }) => {
129
199
  server.watcher.on("all", (event, entry) => {
130
- if (pathToFileURL(entry).pathname === (markdocConfigResult == null ? void 0 : markdocConfigResult.fileUrl.pathname)) {
200
+ if (prependForwardSlash(pathToFileURL(entry).pathname) === markdocConfigResultId) {
131
201
  console.log(
132
202
  yellow(
133
203
  `${bold("[Markdoc]")} Restart the dev server for config changes to take effect.`
package/dist/runtime.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import type { MarkdownHeading } from '@astrojs/markdown-remark';
2
2
  import { type RenderableTreeNode } from '@markdoc/markdoc';
3
- import type { ContentEntryModule } from 'astro';
4
3
  import type { AstroMarkdocConfig } from './config.js';
5
4
  /** Used to call `Markdoc.transform()` and `Markdoc.Ast` in runtime modules */
6
5
  export { default as Markdoc } from '@markdoc/markdoc';
@@ -11,7 +10,7 @@ export { default as Markdoc } from '@markdoc/markdoc';
11
10
  */
12
11
  export declare function setupConfig(userConfig: AstroMarkdocConfig): Promise<Omit<AstroMarkdocConfig, 'extends'>>;
13
12
  /** Used for synchronous `getHeadings()` function */
14
- export declare function setupConfigSync(userConfig: AstroMarkdocConfig, entry: ContentEntryModule): Omit<AstroMarkdocConfig, 'extends'>;
13
+ export declare function setupConfigSync(userConfig: AstroMarkdocConfig): Omit<AstroMarkdocConfig, 'extends'>;
15
14
  /**
16
15
  * Get text content as a string from a Markdoc transform AST
17
16
  */
package/dist/runtime.js CHANGED
@@ -13,11 +13,8 @@ async function setupConfig(userConfig) {
13
13
  }
14
14
  return mergeConfig(defaultConfig, userConfig);
15
15
  }
16
- function setupConfigSync(userConfig, entry) {
17
- let defaultConfig = {
18
- ...setupHeadingConfig(),
19
- variables: { entry }
20
- };
16
+ function setupConfigSync(userConfig) {
17
+ const defaultConfig = setupHeadingConfig();
21
18
  return mergeConfig(defaultConfig, userConfig);
22
19
  }
23
20
  function mergeConfig(configA, configB) {
package/dist/utils.d.ts CHANGED
@@ -37,4 +37,18 @@ interface ErrorProperties {
37
37
  */
38
38
  export declare function prependForwardSlash(str: string): string;
39
39
  export declare function isValidUrl(str: string): boolean;
40
+ /**
41
+ * Identifies Astro components with propagated assets
42
+ * @see 'packages/astro/src/content/consts.ts'
43
+ */
44
+ export declare const PROPAGATED_ASSET_FLAG = "astroPropagatedAssets";
45
+ /**
46
+ * @see 'packages/astro/src/content/utils.ts'
47
+ */
48
+ export declare function hasContentFlag(viteId: string, flag: string): boolean;
49
+ /**
50
+ * Create build hash for manual Rollup chunks.
51
+ * @see 'packages/astro/src/core/build/plugins/plugin-css.ts'
52
+ */
53
+ export declare function createNameHash(baseId: string, hashIds: string[]): string;
40
54
  export {};
package/dist/utils.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import matter from "gray-matter";
2
+ import crypto from "node:crypto";
3
+ import path from "node:path";
2
4
  function parseFrontmatter(fileContents, filePath) {
3
5
  try {
4
6
  matter.clearCache();
@@ -52,8 +54,26 @@ function isValidUrl(str) {
52
54
  return false;
53
55
  }
54
56
  }
57
+ const PROPAGATED_ASSET_FLAG = "astroPropagatedAssets";
58
+ function hasContentFlag(viteId, flag) {
59
+ const flags = new URLSearchParams(viteId.split("?")[1] ?? "");
60
+ return flags.has(flag);
61
+ }
62
+ function createNameHash(baseId, hashIds) {
63
+ const baseName = baseId ? path.parse(baseId).name : "index";
64
+ const hash = crypto.createHash("sha256");
65
+ for (const id of hashIds) {
66
+ hash.update(id, "utf-8");
67
+ }
68
+ const h = hash.digest("hex").slice(0, 8);
69
+ const proposedName = baseName + "." + h;
70
+ return proposedName;
71
+ }
55
72
  export {
56
73
  MarkdocError,
74
+ PROPAGATED_ASSET_FLAG,
75
+ createNameHash,
76
+ hasContentFlag,
57
77
  isValidUrl,
58
78
  parseFrontmatter,
59
79
  prependForwardSlash
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.0",
4
+ "version": "0.3.1",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
7
7
  "author": "withastro",
@@ -44,7 +44,7 @@
44
44
  "zod": "^3.17.3"
45
45
  },
46
46
  "peerDependencies": {
47
- "astro": "^2.5.6"
47
+ "astro": "^2.5.7"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@astrojs/markdown-remark": "^2.2.1",
@@ -57,7 +57,7 @@
57
57
  "mocha": "^9.2.2",
58
58
  "rollup": "^3.20.1",
59
59
  "vite": "^4.3.1",
60
- "astro": "2.5.6",
60
+ "astro": "2.5.7",
61
61
  "astro-scripts": "0.0.14"
62
62
  },
63
63
  "engines": {