-
Notifications
You must be signed in to change notification settings - Fork 45
fix(comark): surface parse failures instead of rendering an empty document #412
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -65,12 +65,15 @@ This is an alert component | |||||||||||
| // `parse` directly mutates `plugins` which creates an infinite effect loop | ||||||||||||
| // so we copy it before passing it in so it gets a regular JS array and we get to still | ||||||||||||
| // track dependencies from an external perspective | ||||||||||||
| parseMarkdown(content, { ...options, ...(unwrap ? { unwrap } : {}), plugins: [...plugins] }).then((result) => { | ||||||||||||
| if (currentVersion > appliedVersion) { | ||||||||||||
| appliedVersion = currentVersion | ||||||||||||
| parsed = result | ||||||||||||
| } | ||||||||||||
| }) | ||||||||||||
| parseMarkdown(content, { ...options, ...(unwrap ? { unwrap } : {}), plugins: [...plugins] }) | ||||||||||||
| .then((result) => { | ||||||||||||
| if (currentVersion > appliedVersion) { | ||||||||||||
| appliedVersion = currentVersion | ||||||||||||
| parsed = result | ||||||||||||
| } | ||||||||||||
| }) | ||||||||||||
| // Keep the last good document rendered and report the failure. | ||||||||||||
| .catch((error) => console.error('[comark] failed to parse markdown', error)) | ||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Advance If request B rejects while request A is pending, A can later pass the 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
| }) | ||||||||||||
| </script> | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import { describe, expect, it } from 'vitest' | ||
| import { createSSRApp, h, onErrorCaptured } from 'vue' | ||
| import { renderToString } from '@vue/server-renderer' | ||
| import type { ComarkPlugin } from 'comark' | ||
| import { Markdown } from '../src/components/Markdown.ts' | ||
|
|
||
| /** | ||
| * A failing parse used to resolve to `null` and render an empty document, which | ||
| * hid the error from the app. The initial parse now rejects, so the failure | ||
| * reaches `onErrorCaptured` instead of being rendered as empty content. | ||
| */ | ||
| describe('Markdown parse errors', () => { | ||
| it('surfaces an initial parse failure instead of rendering an empty document', async () => { | ||
| const failing: ComarkPlugin = { | ||
| name: 'failing', | ||
| post() { | ||
| throw new Error('plugin exploded') | ||
| }, | ||
| } | ||
|
|
||
| const captured: unknown[] = [] | ||
| const app = createSSRApp({ | ||
| setup() { | ||
| onErrorCaptured((error) => { | ||
| captured.push(error) | ||
| return false | ||
| }) | ||
| return () => h(Markdown, { value: '# Hello', plugins: [failing] }) | ||
| }, | ||
| }) | ||
|
|
||
| const html = await renderToString(app as any) | ||
|
|
||
| expect(captured).toHaveLength(1) | ||
| expect((captured[0] as Error).message).toBe('plugin exploded') | ||
| expect(html).not.toContain('comark-content') | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import { describe, it, expect } from 'vitest' | ||
| import { createSerializedTask } from '../src/utils/helpers.ts' | ||
|
|
||
| const tick = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)) | ||
|
|
||
| describe('createSerializedTask', () => { | ||
| it('runs calls strictly one at a time', async () => { | ||
| const order: string[] = [] | ||
| const task = createSerializedTask(async (name: string, delay: number) => { | ||
| await tick(delay) | ||
| order.push(name) | ||
| return name | ||
| }) | ||
|
|
||
| const slow = task('slow', 20) | ||
| const fast = task('fast', 0) | ||
|
|
||
| await Promise.all([slow, fast]) | ||
|
|
||
| expect(order).toEqual(['slow', 'fast']) | ||
| }) | ||
|
|
||
| it('rejects the caller instead of resolving null', async () => { | ||
| const task = createSerializedTask(async () => { | ||
| throw new Error('boom') | ||
| }) | ||
|
|
||
| await expect(task()).rejects.toThrow('boom') | ||
| }) | ||
|
|
||
| it('keeps running after a rejection', async () => { | ||
| let calls = 0 | ||
| const task = createSerializedTask(async () => { | ||
| calls++ | ||
| if (calls === 1) throw new Error('boom') | ||
| return calls | ||
| }) | ||
|
|
||
| await expect(task()).rejects.toThrow('boom') | ||
| await expect(task()).resolves.toBe(2) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Let the initial parse rejection propagate.
ngOnChangesstarts the first string parse, and malformed input can rejectcreateSerializedMarkdownParser. The unconditional.catch()logs and resolves that rejection, so configured Angular error handling cannot receive it. Attach this recovery handler only to later parses; those parses can retain the last good document.🤖 Prompt for AI Agents