2023-03-02 12:50:20 -05:00
import type { AstroIntegration , AstroConfig } from 'astro' ;
2023-02-06 11:21:17 -05:00
import type { InlineConfig } from 'vite' ;
2023-03-01 13:39:29 -05:00
import type { Config } from '@markdoc/markdoc' ;
import Markdoc from '@markdoc/markdoc' ;
2023-03-02 12:50:20 -05:00
import { getAstroConfigPath , MarkdocError , parseFrontmatter } from './utils.js' ;
2023-02-07 16:01:34 -05:00
import { fileURLToPath } from 'node:url' ;
2023-02-10 09:00:53 -05:00
import fs from 'node:fs' ;
2023-02-07 16:01:34 -05:00
2023-03-02 12:50:20 -05:00
export default function markdoc ( markdocConfig : Config = { } ) : AstroIntegration {
2023-03-01 13:35:53 -05:00
const entryBodyByFileIdCache = new Map < string , string > ( ) ;
2023-02-06 11:13:57 -05:00
return {
name : '@astrojs/markdoc' ,
hooks : {
2023-03-01 13:35:53 -05:00
'astro:config:setup' : async ( { updateConfig , config , addContentEntryType } : any ) = > {
2023-03-02 13:53:30 -05:00
function getEntryInfo ( { fileUrl , contents } : { fileUrl : URL ; contents : string } ) {
const parsed = parseFrontmatter ( contents , fileURLToPath ( fileUrl ) ) ;
entryBodyByFileIdCache . set ( fileUrl . pathname , parsed . content ) ;
return {
data : parsed.data ,
body : parsed.content ,
slug : parsed.data.slug ,
rawData : parsed.matter ,
} ;
}
2023-02-13 14:13:02 -05:00
const contentEntryType = {
2023-02-15 09:43:04 -05:00
extensions : [ '.mdoc' ] ,
2023-03-02 13:53:30 -05:00
getEntryInfo ,
2023-02-13 14:13:02 -05:00
contentModuleTypes : await fs . promises . readFile (
new URL ( '../template/content-module-types.d.ts' , import . meta . url ) ,
'utf-8'
) ,
} ;
2023-02-07 16:01:34 -05:00
addContentEntryType ( contentEntryType ) ;
2023-02-10 09:10:06 -05:00
2023-02-06 11:21:17 -05:00
const viteConfig : InlineConfig = {
plugins : [
{
name : '@astrojs/markdoc' ,
async transform ( code , id ) {
2023-02-15 09:43:04 -05:00
if ( ! id . endsWith ( '.mdoc' ) ) return ;
2023-03-01 13:35:53 -05:00
2023-03-02 12:50:20 -05:00
validateRenderProperties ( markdocConfig , config ) ;
2023-03-02 13:53:30 -05:00
const body =
entryBodyByFileIdCache . get ( id ) ? ?
// It's possible for Vite to attempt to transform a file before `getEntryInfo()` has run
getEntryInfo ( { fileUrl : new URL ( id , 'file://' ) , contents : code } ) . body ;
2023-03-01 13:38:53 -05:00
const ast = Markdoc . parse ( body ) ;
const content = Markdoc . transform ( ast , markdocConfig ) ;
2023-03-01 13:35:53 -05:00
2023-03-01 13:38:53 -05:00
return ` import { jsx as h } from 'astro/jsx-runtime'; \ nimport { Renderer } from '@astrojs/markdoc/components'; \ nconst transformedContent = ${ JSON . stringify (
content
) } ; \ nexport async function Content ( { components } ) { return h ( Renderer , { content : transformedContent , components } ) ; } \ nContent [ Symbol . for ( 'astro.needsHeadRendering' ) ] = true ; ` ;
2023-02-06 11:21:17 -05:00
} ,
} ,
] ,
} ;
updateConfig ( { vite : viteConfig } ) ;
2023-02-06 11:13:57 -05:00
} ,
} ,
} ;
}
2023-03-02 12:50:20 -05:00
function validateRenderProperties ( markdocConfig : Config , astroConfig : AstroConfig ) {
const tags = markdocConfig . tags ? ? { } ;
const nodes = markdocConfig . nodes ? ? { } ;
for ( const [ name , config ] of Object . entries ( tags ) ) {
validateRenderProperty ( { type : 'tag' , name , config , astroConfig } ) ;
}
for ( const [ name , config ] of Object . entries ( nodes ) ) {
validateRenderProperty ( { type : 'node' , name , config , astroConfig } ) ;
}
}
function validateRenderProperty ( {
name ,
config ,
type ,
astroConfig ,
} : {
name : string ;
config : { render? : string } ;
type : 'node' | 'tag' ;
astroConfig : Pick < AstroConfig , 'root' > ;
} ) {
if ( typeof config . render === 'string' && config . render . length === 0 ) {
throw new Error (
` Invalid ${ type } configuration: ${ JSON . stringify (
name
) } . The "render" property cannot be an empty string . `
) ;
}
if ( typeof config . render === 'string' && ! isCapitalized ( config . render ) ) {
const astroConfigPath = getAstroConfigPath ( fs , fileURLToPath ( astroConfig . root ) ) ;
throw new MarkdocError ( {
message : ` Invalid ${ type } configuration: ${ JSON . stringify (
name
2023-03-02 13:53:30 -05:00
) } . The "render" property must reference a capitalized component name . ` ,
hint : 'If you want to render to an HTML element, see our docs on rendering Markdoc manually [TODO docs link].' ,
2023-03-02 12:50:20 -05:00
location : astroConfigPath
? {
file : astroConfigPath ,
}
: undefined ,
} ) ;
}
}
function isCapitalized ( str : string ) {
return str . length > 0 && str [ 0 ] === str [ 0 ] . toUpperCase ( ) ;
}