Skip to content

Commit

Permalink
Fix up Typescript errors
Browse files Browse the repository at this point in the history
* MaybePromise<T> = T | Promise<T> no longer exported from svelte-kit
* svelte/store no longer exports Invalidator type (now () => void)
* svelte-exmarkdown v4 needed for correct Svelte 5 component types
* svelte-exmarkdown renderer prop needs casting as Component<any> so
  that Typescript won't complain about incompatible prop types
* Components no longer have a .render method, instead you import the
  render function from Svelte and call it
* svelte/compiler no longer exports walk, we're supposed to use
  estree-walker instead
* estree-walker doesn't think nodes should have 'Element' type, as its
  types are designed for JS syntax trees, not Svelte component trees
* Could not get Typescript to accept our componentMap in emails.ts, as
  each component had incompatible props. Punted by declaring its type as
  Record<EmailTemplate, any>.
  • Loading branch information
rmunn committed Nov 28, 2024
1 parent 52a1c10 commit da8fdd5
Show file tree
Hide file tree
Showing 15 changed files with 49 additions and 59 deletions.
3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,12 @@
"@vitejs/plugin-basic-ssl": "^1.1.0",
"css-tree": "^2.3.1",
"e2e-mailbox": "1.1.5",
"estree-walker": "^3.0.3",
"js-cookie": "^3.0.5",
"just-order-by": "^1.0.0",
"mjml": "^4.15.3",
"set-cookie-parser": "^2.6.0",
"svelte-exmarkdown": "^3.0.5",
"svelte-exmarkdown": "^4.0.1",
"svelte-intl-precompile": "^0.12.3",
"sveltekit-search-params": "^2.1.2",
"tus-js-client": "^4.1.0",
Expand Down
33 changes: 5 additions & 28 deletions frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { Icon } from '$lib/icons';
import Markdown from 'svelte-exmarkdown';
import { NewTabLinkRenderer } from '$lib/components/Markdown';
import type {Component} from 'svelte';
export let value: boolean;
</script>
Expand All @@ -21,6 +22,6 @@
{#if value}
<div class="alert alert-warning mt-4" transition:slide>
<Icon icon="i-mdi-alert-outline" />
<Markdown md={$t('project.confidential.suggestions')} plugins={[{ renderer: { a: NewTabLinkRenderer } }]} />
<Markdown md={$t('project.confidential.suggestions')} plugins={[{ renderer: { a: NewTabLinkRenderer as Component<any> } }]} />
</div>
{/if}
5 changes: 3 additions & 2 deletions frontend/src/lib/components/Users/CreateUserModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import CreateUser from '$lib/components/Users/CreateUser.svelte';
import Markdown from 'svelte-exmarkdown';
import { NewTabLinkRenderer } from '$lib/components/Markdown';
import type {Component} from 'svelte';
import Icon from '$lib/icons/Icon.svelte';
import { createEventDispatcher } from 'svelte';
Expand All @@ -31,11 +32,11 @@
<div>
<Markdown
md={$t('admin_dashboard.create_user_modal.help_create_single_guest_user', { helpLink: helpLinks.addProjectMember })}
plugins={[{ renderer: { a: NewTabLinkRenderer } }]}
plugins={[{ renderer: { a: NewTabLinkRenderer as Component<any> } }]}
/>
<Markdown
md={$t('admin_dashboard.create_user_modal.help_create_bulk_guest_users', { helpLink: helpLinks.bulkAddCreate })}
plugins={[{ renderer: { a: NewTabLinkRenderer } }]}
plugins={[{ renderer: { a: NewTabLinkRenderer as Component<any> } }]}
/>
</div>
</div>
Expand Down
24 changes: 14 additions & 10 deletions frontend/src/lib/email/emailRenderer.server.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,40 @@
import { parse, walk } from 'svelte/compiler';
import { parse } from 'svelte/compiler';
import { render as svelte5Render } from 'svelte/server';
import { walk } from 'estree-walker';

import type { ComponentType } from 'svelte';
import type {EmailTemplateProps} from '../../routes/email/emails';
import type { Component } from 'svelte';
import {EmailTemplate, type EmailTemplateProps} from '../../routes/email/emails';
import { LOCALE_CONTEXT_KEY } from '$lib/i18n';
import type { TemplateNode } from 'svelte/types/compiler/interfaces';
import mjml2html from 'mjml';
import { readable } from 'svelte/store';

type RenderResult = { head: string, html: string, css: string };
type RenderResult = { head: string, html: string };
export type RenderEmailResult = { subject: string, html: string };

function getSubject(head: string): string {
// using the Subject.svelte component a title should have been placed in the head.
// we need to parse the head and find the title and extract the text
const {html} = parse(head, { filename: 'file.html' });
const {html} = parse(head, { filename: 'file.html', modern: false });
// CAUTION: modern: true will become default in Svelte 6, at which point the node.type below will change to RegularElement
let subject: string | undefined;
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
walk(html as Parameters<typeof walk>[0], {
enter(node: TemplateNode, ..._) {
if (node.type === 'Element' && node.name === 'title') {
enter(node, ..._) {
if (node.type as string === 'Element' && 'name' in node && node.name === 'title') {
if ('children' in node && Array.isArray(node.children))
subject = node.children?.[0].data as string;
}
}
} as Parameters<typeof walk>[1]);
if (!subject) throw new Error('subject not found');
console.log(`Subject: ${subject}`);
return subject;
}

export function render(emailComponent: ComponentType, props: Omit<EmailTemplateProps, 'template'>, userLocale: string): RenderEmailResult {
export function render(emailComponent: Component<EmailTemplateProps>, props: EmailTemplateProps, userLocale: string): RenderEmailResult {
const context = new Map([[LOCALE_CONTEXT_KEY, readable(userLocale)]]);
// eslint-disable-next-line
const result: RenderResult = (emailComponent as any).render(props, { context }) as RenderResult;
const result: RenderResult = svelte5Render((emailComponent as any), { props, context });
const mjmlResult = mjml2html(result.html, { validationLevel: 'soft' });
if (mjmlResult.errors) {
console.error(mjmlResult.errors);
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/lib/forms/Checkbox.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import FormFieldError from './FormFieldError.svelte';
import { randomFormId } from './utils';
import { NewTabLinkRenderer } from '$lib/components/Markdown';
import type {Component} from 'svelte';
export let label: string;
export let value: boolean;
Expand All @@ -23,7 +24,7 @@
{#if description}
<label for={id} class="label pb-0">
<span class="label-text-alt">
<Markdown md={description} plugins={[{ renderer: { a: NewTabLinkRenderer } }]} />
<Markdown md={description} plugins={[{ renderer: { a: NewTabLinkRenderer as Component<any> } }]} />
</span>
</label>
{/if}
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/lib/forms/FormError.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import Markdown from 'svelte-exmarkdown';
import type { ErrorMessage } from './types';
import { NewTabLinkRenderer } from '$lib/components/Markdown';
import type {Component} from 'svelte';
export let error: ErrorMessage = undefined;
export let markdown = false;
Expand All @@ -11,7 +12,7 @@
{#if error}
<span class="label text-lg text-error" class:justify-end={right}>
{#if markdown}
<Markdown md={error} plugins={[{ renderer: { a: NewTabLinkRenderer } }]} />
<Markdown md={error} plugins={[{ renderer: { a: NewTabLinkRenderer as Component<any> } }]} />
{:else}
{error}
{/if}
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/lib/forms/FormField.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { randomFormId } from './utils';
import Markdown from 'svelte-exmarkdown';
import { NewTabLinkRenderer } from '$lib/components/Markdown';
import type {Component} from 'svelte';
import type { HelpLink } from '$lib/components/help';
import SupHelp from '$lib/components/help/SupHelp.svelte';
Expand Down Expand Up @@ -40,7 +41,7 @@
{#if description}
<label for={id} class="label pb-0">
<span class="label-text-alt description">
<Markdown md={description} plugins={[{ renderer: { a: NewTabLinkRenderer } }]} />
<Markdown md={description} plugins={[{ renderer: { a: NewTabLinkRenderer as Component<any> } }]} />
</span>
</label>
{/if}
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/lib/forms/RadioButtonGroup.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import FormFieldError from './FormFieldError.svelte';
import { randomFormId } from './utils';
import { NewTabLinkRenderer } from '$lib/components/Markdown';
import type {Component} from 'svelte';
export let buttons: SingleRadioButton[];
export let label: string; // This is for the group as a whole
Expand Down Expand Up @@ -44,7 +45,7 @@
{#if description}
<label for={id} class="label pb-0">
<span class="label-text-alt">
<Markdown md={description} plugins={[{ renderer: { a: NewTabLinkRenderer } }]} />
<Markdown md={description} plugins={[{ renderer: { a: NewTabLinkRenderer as Component<any> } }]} />
</span>
</label>
{/if}
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/lib/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
type Writable,
type Unsubscriber,
type Subscriber,
type Invalidator
} from 'svelte/store';
import { availableLocales, registerAll } from '$locales';
import type { Get } from 'type-fest';
Expand Down Expand Up @@ -107,7 +106,7 @@ export function initI18n(locale: string): { t: I18n, locale: Writable<string> }
}

export const locale: Readable<string> = {
subscribe(run: Subscriber<string>, invalidate?: Invalidator<string>): Unsubscriber {
subscribe(run: Subscriber<string>, invalidate?: () => void): Unsubscriber {
return useLocale().subscribe(run, invalidate);
}
};
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/lib/otel/otel.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { Resource } from '@opentelemetry/resources';
import { NodeSDK } from '@opentelemetry/sdk-node';
import { SemanticAttributes, SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import type { MaybePromise, RequestEvent, NavigationEvent } from '@sveltejs/kit';
import type { RequestEvent, NavigationEvent } from '@sveltejs/kit';
import {
traceFetch as _traceFetch,
traceEventAttributes,
Expand Down Expand Up @@ -62,7 +62,7 @@ export function getRootTraceparent(): string | undefined {

export async function traceRequest(
event: RequestEvent,
responseBuilder: (requestSpan: Span) => MaybePromise<Response>,
responseBuilder: (requestSpan: Span) => Response | Promise<Response>,
): Promise<Response> {
const tracparentContext = buildContextWithTraceparentBaggage(event);
return context.with(tracparentContext, () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import OpenInFlexButton from './OpenInFlexButton.svelte';
import SendReceiveUrlField from './SendReceiveUrlField.svelte';
import { NewTabLinkRenderer } from '$lib/components/Markdown';
import type {Component} from 'svelte';
export let project: Project;
let modal: Modal;
Expand All @@ -20,7 +21,7 @@
<h3>{$t('project_page.open_with_flex.button')}</h3>
<div class="alert alert-info mb-4 not-prose">
<span class="i-mdi-info-outline text-xl"></span>
<Markdown md={$t('project_page.open_with_flex.supported_version')} plugins={[{ renderer: { a: NewTabLinkRenderer } }]} />
<Markdown md={$t('project_page.open_with_flex.supported_version')} plugins={[{ renderer: { a: NewTabLinkRenderer as Component<any> } }]} />
</div>
<div class="not-prose">
<Markdown md={$t('project_page.open_with_flex.instructions')} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import { useNotifications } from '$lib/notify';
import { Duration, deriveAsync, deriveAsyncIfDefined } from '$lib/util/time';
import { getSearchParamValues } from '$lib/util/query-params';
import { onMount } from 'svelte';
import { onMount, type Component } from 'svelte';
import MemberBadge from '$lib/components/Badges/MemberBadge.svelte';
import { derived, writable, type Readable } from 'svelte/store';
import { concatAll } from '$lib/util/array';
Expand Down Expand Up @@ -287,7 +287,7 @@
{/each}
<label for="group-extra-projects" class="label pb-0">
<span class="label-text-alt">
<Markdown md={$t('project.create.maybe_related_description')} plugins={[{ renderer: { a: NewTabLinkRenderer } }]} />
<Markdown md={$t('project.create.maybe_related_description')} plugins={[{ renderer: { a: NewTabLinkRenderer as Component<any> } }]} />
</span>
</label>
</div>
Expand Down
11 changes: 7 additions & 4 deletions frontend/src/routes/email/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { render } from '$lib/email/emailRenderer.server';
import { json } from '@sveltejs/kit';
import type { RequestEvent } from './$types';
import { componentMap, EmailTemplate, type EmailTemplateProps } from './emails';
import type {Component} from 'svelte';

export async function POST(event: RequestEvent): Promise<Response> {
const request = await event.request.json() as EmailTemplateProps;
const {template, ...props} = request;
const component = componentMap[template];
if (!component) throw new Error(`Invalid email template ${template}.}`);
const {...props} = request;
const component = componentMap[props.template] as unknown as Component<EmailTemplateProps>;
if (!component) throw new Error(`Invalid email template ${props.template}.}`);
return json(render(component, props, event.locals.activeLocale));
}

Expand All @@ -17,7 +18,9 @@ export function GET(event: RequestEvent): Response {
const {type, ...props} = {
type: EmailTemplate.ForgotPassword,
name: 'John Doe',
lifetime: '3 days',
template: EmailTemplate.ForgotPassword,
resetUrl: 'https://example.com/reset'
};
return json(render(componentMap[type], props, event.locals.activeLocale));
return json(render(componentMap[type] as unknown as any, props, event.locals.activeLocale));
}
3 changes: 1 addition & 2 deletions frontend/src/routes/email/emails.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import ForgotPassword from '$lib/email/ForgotPassword.svelte';
import NewAdmin from '$lib/email/NewAdmin.svelte';
import type {ComponentType} from 'svelte';
import VerifyEmailAddress from '$lib/email/VerifyEmailAddress.svelte';
import PasswordChanged from '$lib/email/PasswordChanged.svelte';
import JoinProjectRequest from '$lib/email/JoinProjectRequest.svelte';
Expand Down Expand Up @@ -36,7 +35,7 @@ export const componentMap = {
[EmailTemplate.CreateProjectRequest]: CreateProjectRequest,
[EmailTemplate.ApproveProjectRequest]: ApproveProjectRequest,
[EmailTemplate.UserAdded]: UserAdded,
} satisfies Record<EmailTemplate, ComponentType>;
} satisfies Record<EmailTemplate, any>;

interface EmailTemplatePropsBase<T extends EmailTemplate> {
template: T;
Expand Down

0 comments on commit da8fdd5

Please sign in to comment.