@astrojs/markdoc 0.0.0-head-prop-20230323183456

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.
Files changed (33) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/CHANGELOG.md +27 -0
  3. package/LICENSE +61 -0
  4. package/README.md +348 -0
  5. package/components/RenderNode.astro +30 -0
  6. package/components/Renderer.astro +21 -0
  7. package/components/astroNode.ts +51 -0
  8. package/components/index.ts +2 -0
  9. package/dist/index.d.ts +3 -0
  10. package/dist/index.js +104 -0
  11. package/dist/utils.d.ts +50 -0
  12. package/dist/utils.js +92 -0
  13. package/package.json +54 -0
  14. package/src/index.ts +122 -0
  15. package/src/utils.ts +147 -0
  16. package/template/content-module-types.d.ts +9 -0
  17. package/test/content-collections.test.js +258 -0
  18. package/test/fixtures/content-collections/astro.config.mjs +7 -0
  19. package/test/fixtures/content-collections/node_modules/.bin/astro +17 -0
  20. package/test/fixtures/content-collections/package.json +13 -0
  21. package/test/fixtures/content-collections/src/components/Code.astro +12 -0
  22. package/test/fixtures/content-collections/src/components/CustomMarquee.astro +1 -0
  23. package/test/fixtures/content-collections/src/content/blog/simple.mdoc +7 -0
  24. package/test/fixtures/content-collections/src/content/blog/with-components.mdoc +17 -0
  25. package/test/fixtures/content-collections/src/content/blog/with-config.mdoc +9 -0
  26. package/test/fixtures/content-collections/src/content/config.ts +12 -0
  27. package/test/fixtures/content-collections/src/pages/collection.json.js +10 -0
  28. package/test/fixtures/content-collections/src/pages/content-simple.astro +18 -0
  29. package/test/fixtures/content-collections/src/pages/content-with-components.astro +23 -0
  30. package/test/fixtures/content-collections/src/pages/content-with-config.astro +19 -0
  31. package/test/fixtures/content-collections/src/pages/entry.json.js +10 -0
  32. package/test/fixtures/content-collections/utils.js +8 -0
  33. package/tsconfig.json +10 -0
@@ -0,0 +1,50 @@
1
+ /// <reference types="node" />
2
+ import type { AstroInstance } from 'astro';
3
+ import matter from 'gray-matter';
4
+ import type fsMod from 'node:fs';
5
+ /**
6
+ * Match YAML exception handling from Astro core errors
7
+ * @see 'astro/src/core/errors.ts'
8
+ */
9
+ export declare function parseFrontmatter(fileContents: string, filePath: string): matter.GrayMatterFile<string>;
10
+ /**
11
+ * Matches AstroError object with types like error codes stubbed out
12
+ * @see 'astro/src/core/errors/errors.ts'
13
+ */
14
+ export declare class MarkdocError extends Error {
15
+ errorCode: number;
16
+ loc: ErrorLocation | undefined;
17
+ title: string | undefined;
18
+ hint: string | undefined;
19
+ frame: string | undefined;
20
+ type: string;
21
+ constructor(props: ErrorProperties, ...params: any);
22
+ }
23
+ interface ErrorLocation {
24
+ file?: string;
25
+ line?: number;
26
+ column?: number;
27
+ }
28
+ interface ErrorProperties {
29
+ code?: number;
30
+ title?: string;
31
+ name?: string;
32
+ message?: string;
33
+ location?: ErrorLocation;
34
+ hint?: string;
35
+ stack?: string;
36
+ frame?: string;
37
+ }
38
+ /**
39
+ * Matches `search` function used for resolving `astro.config` files.
40
+ * Used by Markdoc for error handling.
41
+ * @see 'astro/src/core/config/config.ts'
42
+ */
43
+ export declare function getAstroConfigPath(fs: typeof fsMod, root: string): string | undefined;
44
+ /**
45
+ * @see 'astro/src/core/path.ts'
46
+ */
47
+ export declare function prependForwardSlash(str: string): string;
48
+ export declare function validateComponentsProp(components: Record<string, AstroInstance['default']>): void;
49
+ export declare function isCapitalized(str: string): boolean;
50
+ export {};
package/dist/utils.js ADDED
@@ -0,0 +1,92 @@
1
+ import z from "astro/zod";
2
+ import matter from "gray-matter";
3
+ import path from "node:path";
4
+ function parseFrontmatter(fileContents, filePath) {
5
+ try {
6
+ matter.clearCache();
7
+ return matter(fileContents);
8
+ } catch (e) {
9
+ if (e.name === "YAMLException") {
10
+ const err = e;
11
+ err.id = filePath;
12
+ err.loc = { file: e.id, line: e.mark.line + 1, column: e.mark.column };
13
+ err.message = e.reason;
14
+ throw err;
15
+ } else {
16
+ throw e;
17
+ }
18
+ }
19
+ }
20
+ class MarkdocError extends Error {
21
+ constructor(props, ...params) {
22
+ super(...params);
23
+ this.type = "MarkdocError";
24
+ const {
25
+ code = 99999,
26
+ name,
27
+ title = "MarkdocError",
28
+ message,
29
+ stack,
30
+ location,
31
+ hint,
32
+ frame
33
+ } = props;
34
+ this.errorCode = code;
35
+ this.title = title;
36
+ if (message)
37
+ this.message = message;
38
+ this.stack = stack ? stack : this.stack;
39
+ this.loc = location;
40
+ this.hint = hint;
41
+ this.frame = frame;
42
+ }
43
+ }
44
+ function getAstroConfigPath(fs, root) {
45
+ const paths = [
46
+ "astro.config.mjs",
47
+ "astro.config.js",
48
+ "astro.config.ts",
49
+ "astro.config.mts",
50
+ "astro.config.cjs",
51
+ "astro.config.cts"
52
+ ].map((p) => path.join(root, p));
53
+ for (const file of paths) {
54
+ if (fs.existsSync(file)) {
55
+ return file;
56
+ }
57
+ }
58
+ }
59
+ function prependForwardSlash(str) {
60
+ return str[0] === "/" ? str : "/" + str;
61
+ }
62
+ function validateComponentsProp(components) {
63
+ try {
64
+ componentsPropValidator.parse(components);
65
+ } catch (e) {
66
+ throw new MarkdocError({
67
+ message: e instanceof z.ZodError ? e.issues[0].message : "Invalid `components` prop. Ensure you are passing an object of components to <Content />"
68
+ });
69
+ }
70
+ }
71
+ const componentsPropValidator = z.record(
72
+ z.string().min(1, "Invalid `components` prop. Component names cannot be empty!").refine(
73
+ (value) => isCapitalized(value),
74
+ (value) => ({
75
+ message: `Invalid \`components\` prop: ${JSON.stringify(
76
+ value
77
+ )}. Component name must be capitalized. If you want to render HTML elements as components, try using a Markdoc node (https://docs.astro.build/en/guides/integrations-guide/markdoc/#render-markdoc-nodes--html-elements-as-astro-components)`
78
+ })
79
+ ),
80
+ z.any()
81
+ );
82
+ function isCapitalized(str) {
83
+ return str.length > 0 && str[0] === str[0].toUpperCase();
84
+ }
85
+ export {
86
+ MarkdocError,
87
+ getAstroConfigPath,
88
+ isCapitalized,
89
+ parseFrontmatter,
90
+ prependForwardSlash,
91
+ validateComponentsProp
92
+ };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@astrojs/markdoc",
3
+ "description": "Add support for Markdoc pages in your Astro site",
4
+ "version": "0.0.0-head-prop-20230323183456",
5
+ "type": "module",
6
+ "types": "./dist/index.d.ts",
7
+ "author": "withastro",
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/withastro/astro.git",
12
+ "directory": "packages/integrations/markdoc"
13
+ },
14
+ "keywords": [
15
+ "astro-integration",
16
+ "astro-component",
17
+ "markdoc"
18
+ ],
19
+ "bugs": "https://github.com/withastro/astro/issues",
20
+ "homepage": "https://docs.astro.build/en/guides/integrations-guide/markdoc/",
21
+ "exports": {
22
+ ".": "./dist/index.js",
23
+ "./components": "./components/index.ts",
24
+ "./package.json": "./package.json"
25
+ },
26
+ "dependencies": {
27
+ "@markdoc/markdoc": "^0.2.2",
28
+ "gray-matter": "^4.0.3",
29
+ "stringify-attributes": "^3.0.0",
30
+ "zod": "^3.17.3"
31
+ },
32
+ "devDependencies": {
33
+ "@types/chai": "^4.3.1",
34
+ "@types/html-escaper": "^3.0.0",
35
+ "@types/mocha": "^9.1.1",
36
+ "astro": "0.0.0-head-prop-20230323183456",
37
+ "astro-scripts": "0.0.14",
38
+ "chai": "^4.3.6",
39
+ "devalue": "^4.2.0",
40
+ "linkedom": "^0.14.12",
41
+ "mocha": "^9.2.2",
42
+ "vite": "^4.0.3"
43
+ },
44
+ "engines": {
45
+ "node": ">=16.12.0"
46
+ },
47
+ "scripts": {
48
+ "build": "astro-scripts build \"src/**/*.ts\" && tsc",
49
+ "build:ci": "astro-scripts build \"src/**/*.ts\"",
50
+ "dev": "astro-scripts dev \"src/**/*.ts\"",
51
+ "test": "mocha --exit --timeout 20000",
52
+ "test:match": "mocha --timeout 20000 -g"
53
+ }
54
+ }
package/src/index.ts ADDED
@@ -0,0 +1,122 @@
1
+ import type { Config } from '@markdoc/markdoc';
2
+ import Markdoc from '@markdoc/markdoc';
3
+ import type { AstroConfig, AstroIntegration, ContentEntryType, HookParameters } from 'astro';
4
+ import fs from 'node:fs';
5
+ import { fileURLToPath } from 'node:url';
6
+ import type { InlineConfig } from 'vite';
7
+ import {
8
+ getAstroConfigPath,
9
+ MarkdocError,
10
+ parseFrontmatter,
11
+ prependForwardSlash,
12
+ } from './utils.js';
13
+
14
+ type SetupHookParams = HookParameters<'astro:config:setup'> & {
15
+ // `contentEntryType` is not a public API
16
+ // Add type defs here
17
+ addContentEntryType: (contentEntryType: ContentEntryType) => void;
18
+ };
19
+
20
+ export default function markdoc(markdocConfig: Config = {}): AstroIntegration {
21
+ return {
22
+ name: '@astrojs/markdoc',
23
+ hooks: {
24
+ 'astro:config:setup': async (params) => {
25
+ const { updateConfig, config, addContentEntryType } = params as SetupHookParams;
26
+
27
+ function getEntryInfo({ fileUrl, contents }: { fileUrl: URL; contents: string }) {
28
+ const parsed = parseFrontmatter(contents, fileURLToPath(fileUrl));
29
+ return {
30
+ data: parsed.data,
31
+ body: parsed.content,
32
+ slug: parsed.data.slug,
33
+ rawData: parsed.matter,
34
+ };
35
+ }
36
+ addContentEntryType({
37
+ extensions: ['.mdoc'],
38
+ getEntryInfo,
39
+ contentModuleTypes: await fs.promises.readFile(
40
+ new URL('../template/content-module-types.d.ts', import.meta.url),
41
+ 'utf-8'
42
+ ),
43
+ });
44
+
45
+ const viteConfig: InlineConfig = {
46
+ plugins: [
47
+ {
48
+ name: '@astrojs/markdoc',
49
+ async transform(code, id) {
50
+ if (!id.endsWith('.mdoc')) return;
51
+
52
+ validateRenderProperties(markdocConfig, config);
53
+ const body = getEntryInfo({
54
+ // Can't use `pathToFileUrl` - Vite IDs are not plain file paths
55
+ fileUrl: new URL(prependForwardSlash(id), 'file://'),
56
+ contents: code,
57
+ }).body;
58
+ const ast = Markdoc.parse(body);
59
+ const content = Markdoc.transform(ast, markdocConfig);
60
+
61
+ return `import { jsx as h } from 'astro/jsx-runtime';\nimport { Renderer } from '@astrojs/markdoc/components';\nconst transformedContent = ${JSON.stringify(
62
+ content
63
+ )};\nexport async function Content ({ components }) { return h(Renderer, { content: transformedContent, components }); }\nContent[Symbol.for('astro.needsHeadRendering')] = true;`;
64
+ },
65
+ },
66
+ ],
67
+ };
68
+ updateConfig({ vite: viteConfig });
69
+ },
70
+ },
71
+ };
72
+ }
73
+
74
+ function validateRenderProperties(markdocConfig: Config, astroConfig: AstroConfig) {
75
+ const tags = markdocConfig.tags ?? {};
76
+ const nodes = markdocConfig.nodes ?? {};
77
+
78
+ for (const [name, config] of Object.entries(tags)) {
79
+ validateRenderProperty({ type: 'tag', name, config, astroConfig });
80
+ }
81
+ for (const [name, config] of Object.entries(nodes)) {
82
+ validateRenderProperty({ type: 'node', name, config, astroConfig });
83
+ }
84
+ }
85
+
86
+ function validateRenderProperty({
87
+ name,
88
+ config,
89
+ type,
90
+ astroConfig,
91
+ }: {
92
+ name: string;
93
+ config: { render?: string };
94
+ type: 'node' | 'tag';
95
+ astroConfig: Pick<AstroConfig, 'root'>;
96
+ }) {
97
+ if (typeof config.render === 'string' && config.render.length === 0) {
98
+ throw new Error(
99
+ `Invalid ${type} configuration: ${JSON.stringify(
100
+ name
101
+ )}. The "render" property cannot be an empty string.`
102
+ );
103
+ }
104
+ if (typeof config.render === 'string' && !isCapitalized(config.render)) {
105
+ const astroConfigPath = getAstroConfigPath(fs, fileURLToPath(astroConfig.root));
106
+ throw new MarkdocError({
107
+ message: `Invalid ${type} configuration: ${JSON.stringify(
108
+ name
109
+ )}. The "render" property must reference a capitalized component name.`,
110
+ hint: 'If you want to render to an HTML element, see our docs on rendering Markdoc manually: https://docs.astro.build/en/guides/integrations-guide/markdoc/#render-markdoc-nodes--html-elements-as-astro-components',
111
+ location: astroConfigPath
112
+ ? {
113
+ file: astroConfigPath,
114
+ }
115
+ : undefined,
116
+ });
117
+ }
118
+ }
119
+
120
+ function isCapitalized(str: string) {
121
+ return str.length > 0 && str[0] === str[0].toUpperCase();
122
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,147 @@
1
+ import type { AstroInstance } from 'astro';
2
+ import z from 'astro/zod';
3
+ import matter from 'gray-matter';
4
+ import type fsMod from 'node:fs';
5
+ import path from 'node:path';
6
+ import type { ErrorPayload as ViteErrorPayload } from 'vite';
7
+
8
+ /**
9
+ * Match YAML exception handling from Astro core errors
10
+ * @see 'astro/src/core/errors.ts'
11
+ */
12
+ export function parseFrontmatter(fileContents: string, filePath: string) {
13
+ try {
14
+ // `matter` is empty string on cache results
15
+ // clear cache to prevent this
16
+ (matter as any).clearCache();
17
+ return matter(fileContents);
18
+ } catch (e: any) {
19
+ if (e.name === 'YAMLException') {
20
+ const err: Error & ViteErrorPayload['err'] = e;
21
+ err.id = filePath;
22
+ err.loc = { file: e.id, line: e.mark.line + 1, column: e.mark.column };
23
+ err.message = e.reason;
24
+ throw err;
25
+ } else {
26
+ throw e;
27
+ }
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Matches AstroError object with types like error codes stubbed out
33
+ * @see 'astro/src/core/errors/errors.ts'
34
+ */
35
+ export class MarkdocError extends Error {
36
+ public errorCode: number;
37
+ public loc: ErrorLocation | undefined;
38
+ public title: string | undefined;
39
+ public hint: string | undefined;
40
+ public frame: string | undefined;
41
+
42
+ type = 'MarkdocError';
43
+
44
+ constructor(props: ErrorProperties, ...params: any) {
45
+ super(...params);
46
+
47
+ const {
48
+ // Use default code for unknown errors in Astro core
49
+ // We don't have a best practice for integration error codes yet
50
+ code = 99999,
51
+ name,
52
+ title = 'MarkdocError',
53
+ message,
54
+ stack,
55
+ location,
56
+ hint,
57
+ frame,
58
+ } = props;
59
+
60
+ this.errorCode = code;
61
+ this.title = title;
62
+ if (message) this.message = message;
63
+ // Only set this if we actually have a stack passed, otherwise uses Error's
64
+ this.stack = stack ? stack : this.stack;
65
+ this.loc = location;
66
+ this.hint = hint;
67
+ this.frame = frame;
68
+ }
69
+ }
70
+
71
+ interface ErrorLocation {
72
+ file?: string;
73
+ line?: number;
74
+ column?: number;
75
+ }
76
+
77
+ interface ErrorProperties {
78
+ code?: number;
79
+ title?: string;
80
+ name?: string;
81
+ message?: string;
82
+ location?: ErrorLocation;
83
+ hint?: string;
84
+ stack?: string;
85
+ frame?: string;
86
+ }
87
+
88
+ /**
89
+ * Matches `search` function used for resolving `astro.config` files.
90
+ * Used by Markdoc for error handling.
91
+ * @see 'astro/src/core/config/config.ts'
92
+ */
93
+ export function getAstroConfigPath(fs: typeof fsMod, root: string): string | undefined {
94
+ const paths = [
95
+ 'astro.config.mjs',
96
+ 'astro.config.js',
97
+ 'astro.config.ts',
98
+ 'astro.config.mts',
99
+ 'astro.config.cjs',
100
+ 'astro.config.cts',
101
+ ].map((p) => path.join(root, p));
102
+
103
+ for (const file of paths) {
104
+ if (fs.existsSync(file)) {
105
+ return file;
106
+ }
107
+ }
108
+ }
109
+
110
+ /**
111
+ * @see 'astro/src/core/path.ts'
112
+ */
113
+ export function prependForwardSlash(str: string) {
114
+ return str[0] === '/' ? str : '/' + str;
115
+ }
116
+
117
+ export function validateComponentsProp(components: Record<string, AstroInstance['default']>) {
118
+ try {
119
+ componentsPropValidator.parse(components);
120
+ } catch (e) {
121
+ throw new MarkdocError({
122
+ message:
123
+ e instanceof z.ZodError
124
+ ? e.issues[0].message
125
+ : 'Invalid `components` prop. Ensure you are passing an object of components to <Content />',
126
+ });
127
+ }
128
+ }
129
+
130
+ const componentsPropValidator = z.record(
131
+ z
132
+ .string()
133
+ .min(1, 'Invalid `components` prop. Component names cannot be empty!')
134
+ .refine(
135
+ (value) => isCapitalized(value),
136
+ (value) => ({
137
+ message: `Invalid \`components\` prop: ${JSON.stringify(
138
+ value
139
+ )}. Component name must be capitalized. If you want to render HTML elements as components, try using a Markdoc node (https://docs.astro.build/en/guides/integrations-guide/markdoc/#render-markdoc-nodes--html-elements-as-astro-components)`,
140
+ })
141
+ ),
142
+ z.any()
143
+ );
144
+
145
+ export function isCapitalized(str: string) {
146
+ return str.length > 0 && str[0] === str[0].toUpperCase();
147
+ }
@@ -0,0 +1,9 @@
1
+ declare module 'astro:content' {
2
+ interface Render {
3
+ '.mdoc': Promise<{
4
+ Content(props: {
5
+ components?: Record<string, import('astro').AstroInstance['default']>;
6
+ }): import('astro').MarkdownInstance<{}>['Content'];
7
+ }>;
8
+ }
9
+ }