Skip to content

Commit

Permalink
Add error handling and refactor imports in markdown-remark integration
Browse files Browse the repository at this point in the history
  • Loading branch information
Adammatthiesen committed Jan 8, 2025
1 parent 9f5b574 commit ac60676
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 92 deletions.
4 changes: 4 additions & 0 deletions packages/markdown-remark/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
"#import-plugin": {
"browser": "./dist/processor/import-plugin-browser.js",
"default": "./dist/processor/import-plugin-default.js"
},
"#processor": {
"types": "./dist/processor/index.d.ts",
"default": "./dist/processor/index.js"
}
},
"files": [
Expand Down
29 changes: 29 additions & 0 deletions packages/markdown-remark/src/integration/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { AstroError } from 'astro/errors';

export class MarkdownRemarkError extends AstroError {
name = 'StudioCMS Markdown Remark Error';
}

// biome-ignore lint/suspicious/noExplicitAny: <explanation>
export function prefixError(err: any, prefix: string): any {
// If the error is an object with a `message` property, attempt to prefix the message
if (err?.message) {
try {
err.message = `${prefix}:\n${err.message}`;
return err;
} catch {
// Any errors here are ok, there's fallback code below
}
}

// If that failed, create a new error with the desired message and attempt to keep the stack
const wrappedError = new Error(`${prefix}${err ? `: ${err}` : ''}`);
try {
wrappedError.stack = err.stack;
wrappedError.cause = err;
} catch {
// It's ok if we could not set the stack or cause - the message is the most important part
}

return wrappedError;
}
10 changes: 3 additions & 7 deletions packages/markdown-remark/src/integration/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
HTMLString,
type MarkdownProcessorRenderOptions,
createMarkdownProcessor,
} from '../processor/index.js';
} from '#processor';
import { importComponentsKeys } from './runtime.js';
import { TransformToProcessor } from './schema.js';
import { shared } from './shared.js';
import type {
Expand All @@ -15,12 +16,7 @@ import type {
RenderComponents,
RenderResponse,
} from './types.js';
import {
createComponentProxy,
importComponentsKeys,
mergeRecords,
transformHTML,
} from './utils.js';
import { createComponentProxy, mergeRecords, transformHTML } from './utils.js';

export type { Props, RenderResponse } from './types.js';

Expand Down
38 changes: 38 additions & 0 deletions packages/markdown-remark/src/integration/runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { MarkdownRemarkError, prefixError } from './errors.js';

/**
* Imports components by their keys from the 'studiocms:markdown-remark/user-components' module.
*
* @param keys - An array of strings representing the keys of the components to import.
* @returns A promise that resolves to an object containing the imported components.
* @throws {MarkdownRemarkError} If any component fails to import, an error is thrown with a prefixed message.
*/
export async function importComponentsKeys() {
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
const predefinedComponents: Record<string, any> = {};

const mod = import('studiocms:markdown-remark/user-components').catch((e) => {
const newErr = prefixError(e, 'Failed to import user components');
console.error(newErr);
throw new MarkdownRemarkError(newErr.message, newErr.stack);
});

const componentKeys = (await mod).componentKeys;

for (const key of componentKeys) {
try {
predefinedComponents[key.toLowerCase()] = (await mod)[key.toLowerCase()];
} catch (e) {
if (e instanceof Error) {
const newErr = prefixError(e, `Failed to import component "${key}"`);
console.error(newErr);
throw new MarkdownRemarkError(newErr.message, newErr.stack);
}
const newErr = prefixError(new Error('Unknown error'), `Failed to import component "${key}"`);
console.error(newErr);
throw new MarkdownRemarkError(newErr.message, newErr.stack);
}
}

return predefinedComponents;
}
2 changes: 1 addition & 1 deletion packages/markdown-remark/src/integration/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { AstroConfig, SSRResult } from 'astro';
import type { RenderTemplateResult } from 'astro/runtime/server/render/astro/render-template.js';
import type { ComponentSlotValue } from 'astro/runtime/server/render/slot.js';
import type { SanitizeOptions } from 'ultrahtml/transformers/sanitize';
import type { HTMLString, MarkdownHeading } from '../processor/index.js';
import type { HTMLString, MarkdownHeading } from '../processor/types.js';
import type { StudioCMSMarkdownExtendedConfig } from './schema.js';

/**
Expand Down
102 changes: 18 additions & 84 deletions packages/markdown-remark/src/integration/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
import type { SSRResult } from 'astro';
import { AstroError } from 'astro/errors';
import { jsx } from 'astro/jsx-runtime';
import { renderJSX } from 'astro/runtime/server/jsx.js';
import { __unsafeHTML, transform } from 'ultrahtml';
import sanitize, { type SanitizeOptions } from 'ultrahtml/transformers/sanitize';
import swap from 'ultrahtml/transformers/swap';
import { decode } from './decoder/index.js';

/**
* Merges multiple records into a single record. If there are duplicate keys, the value from the last record with that key will be used.
*
* @param {...Record<string, any>[]} records - The records to merge.
* @returns {Record<string, any>} - The merged record.
*/
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
export function mergeRecords(...records: Record<string, any>[]): Record<string, any> {
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
const result: Record<string, any> = {};
for (const record of records) {
for (const [key, value] of Object.entries(record)) {
result[key.toLowerCase()] = value;
}
}
return result;
}

/**
* Creates a proxy for components that can either be strings or functions.
* If the component is a string, it is directly assigned to the proxy.
Expand Down Expand Up @@ -75,89 +92,6 @@ export function dedent(str: string): string {
return lns.map((ln) => (ln.startsWith(indent) ? ln.slice(indent.length) : ln)).join('\n');
}

/**
* Merges multiple records into a single record. If there are duplicate keys, the value from the last record with that key will be used.
*
* @param {...Record<string, any>[]} records - The records to merge.
* @returns {Record<string, any>} - The merged record.
*/
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
export function mergeRecords(...records: Record<string, any>[]): Record<string, any> {
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
const result: Record<string, any> = {};
for (const record of records) {
for (const [key, value] of Object.entries(record)) {
result[key.toLowerCase()] = value;
}
}
return result;
}

export class MarkdownRemarkError extends AstroError {
name = 'StudioCMS Markdown Remark Error';
}

// biome-ignore lint/suspicious/noExplicitAny: <explanation>
export function prefixError(err: any, prefix: string): any {
// If the error is an object with a `message` property, attempt to prefix the message
if (err?.message) {
try {
err.message = `${prefix}:\n${err.message}`;
return err;
} catch {
// Any errors here are ok, there's fallback code below
}
}

// If that failed, create a new error with the desired message and attempt to keep the stack
const wrappedError = new Error(`${prefix}${err ? `: ${err}` : ''}`);
try {
wrappedError.stack = err.stack;
wrappedError.cause = err;
} catch {
// It's ok if we could not set the stack or cause - the message is the most important part
}

return wrappedError;
}

/**
* Imports components by their keys from the 'studiocms:markdown-remark/user-components' module.
*
* @param keys - An array of strings representing the keys of the components to import.
* @returns A promise that resolves to an object containing the imported components.
* @throws {MarkdownRemarkError} If any component fails to import, an error is thrown with a prefixed message.
*/
export async function importComponentsKeys() {
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
const predefinedComponents: Record<string, any> = {};

const mod = import('studiocms:markdown-remark/user-components').catch((e) => {
const newErr = prefixError(e, 'Failed to import user components');
console.error(newErr);
throw new MarkdownRemarkError(newErr.message, newErr.stack);
});

const componentKeys = (await mod).componentKeys;

for (const key of componentKeys) {
try {
predefinedComponents[key.toLowerCase()] = (await mod)[key.toLowerCase()];
} catch (e) {
if (e instanceof Error) {
const newErr = prefixError(e, `Failed to import component "${key}"`);
console.error(newErr);
throw new MarkdownRemarkError(newErr.message, newErr.stack);
}
const newErr = prefixError(new Error('Unknown error'), `Failed to import component "${key}"`);
console.error(newErr);
throw new MarkdownRemarkError(newErr.message, newErr.stack);
}
}

return predefinedComponents;
}

export async function transformHTML(
html: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
Expand Down
2 changes: 2 additions & 0 deletions packages/markdown-remark/src/processor/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type { CreateShikiHighlighterOptions, ShikiHighlighterHighlightOptions }

export type { Node } from 'unist';

export type { HTMLString };

declare module 'vfile' {
interface DataMap {
astro: {
Expand Down

0 comments on commit ac60676

Please sign in to comment.