-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
/
index.ts
156 lines (130 loc) · 4.94 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import type { CompileResult, TransformResult } from '../@types/astro';
import type { CompileOptions } from '../@types/compiler.js';
import path from 'path';
import micromark from 'micromark';
import gfmSyntax from 'micromark-extension-gfm';
import matter from 'gray-matter';
import gfmHtml from 'micromark-extension-gfm/html.js';
import { parse } from '../parser/index.js';
import { createMarkdownHeadersCollector } from '../micromark-collect-headers.js';
import { encodeMarkdown } from '../micromark-encode.js';
import { optimize } from './optimize/index.js';
import { codegen } from './codegen.js';
function internalImport(internalPath: string) {
return `/_astro_internal/${internalPath}`;
}
interface ConvertAstroOptions {
compileOptions: CompileOptions;
filename: string;
fileID: string;
}
async function convertAstroToJsx(template: string, opts: ConvertAstroOptions): Promise<TransformResult> {
const { filename } = opts;
// 1. Parse
const ast = parse(template, {
filename,
});
// 2. Optimize the AST
await optimize(ast, opts);
// Turn AST into JSX
return await codegen(ast, opts);
}
async function convertMdToJsx(
contents: string,
{ compileOptions, filename, fileID }: { compileOptions: CompileOptions; filename: string; fileID: string }
): Promise<TransformResult> {
const { data: frontmatterData, content } = matter(contents);
const { headers, headersExtension } = createMarkdownHeadersCollector();
const mdHtml = micromark(content, {
allowDangerousHtml: true,
extensions: [gfmSyntax()],
htmlExtensions: [gfmHtml, encodeMarkdown, headersExtension],
});
// TODO: Warn if reserved word is used in "frontmatterData"
const contentData: any = {
...frontmatterData,
headers,
source: content,
};
let imports = '';
for (let [ComponentName, specifier] of Object.entries(frontmatterData.import || {})) {
imports += `import ${ComponentName} from '${specifier}';\n`;
}
// </script> can't be anywhere inside of a JS string, otherwise the HTML parser fails.
// Break it up here so that the HTML parser won't detect it.
const stringifiedSetupContext = JSON.stringify(contentData).replace(/\<\/script\>/g, `</scrip" + "t>`);
const raw = `---
${imports}
${frontmatterData.layout ? `import {__renderPage as __layout} from '${frontmatterData.layout}';` : 'const __layout = undefined;'}
export const __content = ${stringifiedSetupContext};
---
<section>${mdHtml}</section>`;
const convertOptions = { compileOptions, filename, fileID };
return await convertAstroToJsx(raw, convertOptions);
}
type SupportedExtensions = '.astro' | '.md';
async function transformFromSource(
contents: string,
{ compileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string }
): Promise<TransformResult> {
const fileID = path.relative(projectRoot, filename);
switch (path.extname(filename) as SupportedExtensions) {
case '.astro':
return await convertAstroToJsx(contents, { compileOptions, filename, fileID });
case '.md':
return await convertMdToJsx(contents, { compileOptions, filename, fileID });
default:
throw new Error('Not Supported!');
}
}
export async function compileComponent(
source: string,
{ compileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string }
): Promise<CompileResult> {
const sourceJsx = await transformFromSource(source, { compileOptions, filename, projectRoot });
const isPage = path.extname(filename) === '.md' || sourceJsx.items.some((item) => item.name === 'html');
// return template
let modJsx = `
import fetch from 'node-fetch';
// <script astro></script>
${sourceJsx.imports.join('\n')}
// \`__render()\`: Render the contents of the Astro module.
import { h, Fragment } from '${internalImport('h.js')}';
async function __render(props, ...children) {
${sourceJsx.script}
return h(Fragment, null, ${sourceJsx.items.map(({ jsx }) => jsx).join(',')});
}
export default __render;
`;
if (isPage) {
modJsx += `
// \`__renderPage()\`: Render the contents of the Astro module as a page. This is a special flow,
// triggered by loading a component directly by URL.
export async function __renderPage({request, children, props}) {
const currentChild = {
layout: typeof __layout === 'undefined' ? undefined : __layout,
content: typeof __content === 'undefined' ? undefined : __content,
__render,
};
import.meta.request = request;
const childBodyResult = await currentChild.__render(props, children);
// find layout, if one was given.
if (currentChild.layout) {
return currentChild.layout({
request,
props: {content: currentChild.content},
children: [childBodyResult],
});
}
return childBodyResult;
};\n`;
} else {
modJsx += `
export async function __renderPage() { throw new Error("No <html> page element found!"); }\n`;
}
return {
result: sourceJsx,
contents: modJsx,
css: sourceJsx.css,
};
}