diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index b89978663..1890b86fe 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -19,6 +19,10 @@ jobs: node-version: ${{ matrix.node-version }} - name: npm install, build, and test run: | + cd examples/snowpack + npm ci + cd ../.. + npm ci npm run build npm test diff --git a/examples/snowpack/snowpack.config.js b/examples/snowpack/snowpack.config.js index e85a9d7b2..821552181 100644 --- a/examples/snowpack/snowpack.config.js +++ b/examples/snowpack/snowpack.config.js @@ -10,11 +10,7 @@ module.exports = { '@snowpack/plugin-svelte', '@snowpack/plugin-vue', ], - packageOptions: { - external: [ - 'node-fetch' - ] - }, + packageOptions: {}, buildOptions: { out: '_site', }, diff --git a/src/dev.ts b/src/dev.ts index 93a890057..c6ad9ff7c 100644 --- a/src/dev.ts +++ b/src/dev.ts @@ -57,6 +57,8 @@ export default async function (astroConfig: AstroConfig) { break; } } + res.statusCode = 500; + res.end(formatErrorForBrowser(result.error)); break; } } @@ -66,3 +68,8 @@ export default async function (astroConfig: AstroConfig) { console.log(`Server running at http://${hostname}:${port}/`); }); } + +function formatErrorForBrowser(error: Error) { + // TODO make this pretty. + return error.toString(); +} \ No newline at end of file diff --git a/src/micromark-encode.ts b/src/micromark-encode.ts new file mode 100644 index 000000000..d205d13e3 --- /dev/null +++ b/src/micromark-encode.ts @@ -0,0 +1,35 @@ +import type { HtmlExtension, Token, Tokenize } from 'micromark/dist/shared-types'; + +const characterReferences = { + '"': 'quot', + '&': 'amp', + '<': 'lt', + '>': 'gt', + '{': 'lbrace', + '}': 'rbrace', +}; + +type EncodedChars = '"' | '&' | '<' | '>' | '{' | '}'; + +function encode(value: string): string { + return value.replace(/["&<>{}]/g, (raw: string) => { + return '&' + characterReferences[raw as EncodedChars] + ';'; + }); +} + +function encodeToken(this: Record void>) { + const token: Token = arguments[0]; + const serialize = (this.sliceSerialize as unknown) as (t: Token) => string; + const raw = (this.raw as unknown) as (s: string) => void; + const value = serialize(token); + raw(encode(value)); +} + +const plugin: HtmlExtension = { + exit: { + codeTextData: encodeToken, + codeFlowValue: encodeToken, + }, +}; + +export { plugin as encodeMarkdown }; \ No newline at end of file diff --git a/src/transform2.ts b/src/transform2.ts index 2f1e651cf..42a151b3c 100644 --- a/src/transform2.ts +++ b/src/transform2.ts @@ -8,6 +8,7 @@ import gfmHtml from 'micromark-extension-gfm/html.js'; import { CompileResult, TransformResult } from './@types/astro'; import { parse } from './compiler/index.js'; import { createMarkdownHeadersCollector } from './micromark-collect-headers.js'; +import { encodeMarkdown } from './micromark-encode.js'; import { defaultLogOptions } from './logger.js'; import { optimize } from './optimize/index.js'; import { codegen } from './codegen/index.js'; @@ -54,8 +55,9 @@ async function convertMdToJsx( const { data: _frontmatterData, content } = matter(contents); const { headers, headersExtension } = createMarkdownHeadersCollector(); const mdHtml = micromark(content, { + allowDangerousHtml: true, extensions: [gfmSyntax()], - htmlExtensions: [gfmHtml, headersExtension], + htmlExtensions: [gfmHtml, encodeMarkdown, headersExtension], }); const setupContext = { @@ -68,19 +70,26 @@ async function convertMdToJsx( }, }; + let imports = ''; + for(let [ComponentName, specifier] of Object.entries(_frontmatterData.import || {})) { + imports += `import ${ComponentName} from '${specifier}';\n`; + } + // 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(setupContext).replace(/\<\/script\>/g, ``); - return convertHmxToJsx( - `
{${JSON.stringify(mdHtml)}}
`, - { compileOptions, filename, fileID } - ); + const raw = `
${mdHtml}
`; + + const convertOptions = { compileOptions, filename, fileID }; + + return convertHmxToJsx(raw, convertOptions); } async function transformFromSource( diff --git a/test/fixtures/hmx-markdown/astro.config.mjs b/test/fixtures/hmx-markdown/astro.config.mjs new file mode 100644 index 000000000..0f0be4b94 --- /dev/null +++ b/test/fixtures/hmx-markdown/astro.config.mjs @@ -0,0 +1,9 @@ + +export default { + projectRoot: '.', + hmxRoot: './astro', + dist: './_site', + extensions: { + '.jsx': 'preact' + } +} \ No newline at end of file diff --git a/test/fixtures/hmx-markdown/astro/components/Example.jsx b/test/fixtures/hmx-markdown/astro/components/Example.jsx new file mode 100644 index 000000000..57bde3a95 --- /dev/null +++ b/test/fixtures/hmx-markdown/astro/components/Example.jsx @@ -0,0 +1,5 @@ +import { h } from 'preact'; + +export default function() { + return
Testing
+} \ No newline at end of file diff --git a/test/fixtures/hmx-markdown/astro/layouts/content.hmx b/test/fixtures/hmx-markdown/astro/layouts/content.hmx new file mode 100644 index 000000000..52f79400c --- /dev/null +++ b/test/fixtures/hmx-markdown/astro/layouts/content.hmx @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/test/fixtures/hmx-markdown/astro/pages/index.hmx b/test/fixtures/hmx-markdown/astro/pages/index.hmx new file mode 100644 index 000000000..19f888e04 --- /dev/null +++ b/test/fixtures/hmx-markdown/astro/pages/index.hmx @@ -0,0 +1,13 @@ + + + + + + +

Hello world!

\ No newline at end of file diff --git a/test/fixtures/hmx-markdown/astro/pages/post.md b/test/fixtures/hmx-markdown/astro/pages/post.md new file mode 100644 index 000000000..057b1febb --- /dev/null +++ b/test/fixtures/hmx-markdown/astro/pages/post.md @@ -0,0 +1,13 @@ +--- +layout: layouts/content.hmx +title: My Blog Post +description: This is a post about some stuff. +import: + Example: '../components/Example.jsx' +--- + +## Interesting Topic + +
Some content
+ + \ No newline at end of file diff --git a/test/fixtures/hmx-markdown/snowpack.config.js b/test/fixtures/hmx-markdown/snowpack.config.js new file mode 100644 index 000000000..2cbf0ef07 --- /dev/null +++ b/test/fixtures/hmx-markdown/snowpack.config.js @@ -0,0 +1,5 @@ +export default { + mount: { + + } +}; diff --git a/test/hmx-markdown.test.js b/test/hmx-markdown.test.js new file mode 100644 index 000000000..1a3a2e11c --- /dev/null +++ b/test/hmx-markdown.test.js @@ -0,0 +1,45 @@ +import { suite } from 'uvu'; +import * as assert from 'uvu/assert'; +import { createRuntime } from '../lib/runtime.js'; +import { loadConfig } from '../lib/config.js'; +import { doc } from './test-utils.js'; + +const HMXMD = suite('HMX Markdown'); + +let runtime, setupError; + +HMXMD.before(async () => { + const astroConfig = await loadConfig(new URL('./fixtures/hmx-markdown', import.meta.url).pathname); + + const logging = { + level: 'error', + dest: process.stderr + }; + + try { + runtime = await createRuntime(astroConfig, logging); + } catch(err) { + console.error(err); + setupError = err; + } +}); + +HMXMD.after(async () => { + runtime && runtime.shutdown(); +}); + +HMXMD('No errors creating a runtime', () => { + assert.equal(setupError, undefined); +}); + +HMXMD('Can load markdown pages with hmx', async () => { + const result = await runtime.load('/post'); + + assert.equal(result.statusCode, 200); + + const $ = doc(result.contents); + assert.ok($('#first').length, 'There is a div added in markdown'); + assert.ok($('#test').length, 'There is a div added via a component from markdown'); +}); + +HMXMD.run(); \ No newline at end of file diff --git a/test/react-component.test.js b/test/react-component.test.js index 0b6273922..d901c62b0 100644 --- a/test/react-component.test.js +++ b/test/react-component.test.js @@ -6,7 +6,7 @@ import { doc } from './test-utils.js'; const React = suite('React Components'); -let runtime; +let runtime, setupError; React.before(async () => { const astroConfig = await loadConfig(new URL('./fixtures/react-component', import.meta.url).pathname); @@ -20,7 +20,7 @@ React.before(async () => { runtime = await createRuntime(astroConfig, logging); } catch(err) { console.error(err); - throw err; + setupError = err; } }); @@ -28,6 +28,10 @@ React.after(async () => { await runtime.shutdown(); }); +React('No error creating the runtime', () => { + assert.equal(setupError, undefined); +}); + React('Can load hmx page', async () => { const result = await runtime.load('/'); diff --git a/test/snowpack-integration.test.js b/test/snowpack-integration.test.js index 8547ee7cd..033f5587d 100644 --- a/test/snowpack-integration.test.js +++ b/test/snowpack-integration.test.js @@ -10,7 +10,7 @@ const { readdir, stat } = fsPromises; const SnowpackDev = suite('snowpack.dev'); -let runtime, cwd; +let runtime, cwd, setupError; SnowpackDev.before(async () => { // Bug: Snowpack config is still loaded relative to the current working directory. @@ -28,7 +28,7 @@ SnowpackDev.before(async () => { runtime = await createRuntime(astroConfig, logging); } catch(err) { console.error(err); - throw err; + setupError = err; } }); @@ -58,6 +58,10 @@ async function* allPages(root) { } } +SnowpackDev('No error creating the runtime', () => { + assert.equal(setupError, undefined); +}); + SnowpackDev('Can load every page', async () => { const failed = [];