mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-05 19:40:30 +08:00
feat(server): improve ci build (#15386)
#### PR Dependency Tree * **PR #15386** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved email rendering and formatting consistency. * Preserved calendar synchronization windows while removing reliance on date utility libraries. * Enhanced cleanup of Prisma engine files, including deduplication and space-saving reporting. * **Tests** * Updated email snapshots to validate formatted HTML output. * **Refactor** * Streamlined email component usage and centralized email rendering behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -64,13 +64,21 @@
|
||||
"@prisma/client": "^6.6.0",
|
||||
"@prisma/instrumentation": "^6.7.0",
|
||||
"@queuedash/api": "^3.16.0",
|
||||
"@react-email/components": "^0.5.7",
|
||||
"@react-email/body": "0.3.0",
|
||||
"@react-email/button": "0.2.1",
|
||||
"@react-email/container": "0.0.16",
|
||||
"@react-email/head": "0.0.13",
|
||||
"@react-email/html": "0.0.12",
|
||||
"@react-email/img": "0.0.12",
|
||||
"@react-email/link": "0.0.13",
|
||||
"@react-email/row": "0.0.13",
|
||||
"@react-email/section": "0.0.17",
|
||||
"@react-email/text": "0.1.6",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"bullmq": "^5.79.0",
|
||||
"commander": "^13.1.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cross-env": "^10.1.0",
|
||||
"date-fns": "^4.4.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"eventemitter2": "^6.4.9",
|
||||
"exa-js": "^2.4.0",
|
||||
@@ -81,7 +89,6 @@
|
||||
"graphql": "^16.13.2",
|
||||
"graphql-scalars": "^1.24.0",
|
||||
"graphql-upload": "^17.0.0",
|
||||
"html-validate": "^9.0.0",
|
||||
"htmlrewriter": "^0.0.12",
|
||||
"http-errors": "^2.0.0",
|
||||
"ioredis": "^5.11.1",
|
||||
@@ -102,7 +109,6 @@
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.2",
|
||||
"semver": "^7.7.4",
|
||||
"ses": "^1.15.0",
|
||||
"socket.io": "^4.8.3",
|
||||
"stripe": "^17.7.0",
|
||||
"tldts": "^7.0.19",
|
||||
@@ -119,6 +125,7 @@
|
||||
"@faker-js/faker": "^10.1.0",
|
||||
"@nestjs/swagger": "^11.2.7",
|
||||
"@nestjs/testing": "patch:@nestjs/testing@npm%3A11.1.18#~/.yarn/patches/@nestjs-testing-npm-11.1.18-32c0f6af12.patch",
|
||||
"@react-email/render": "1.4.0",
|
||||
"@types/cookie-parser": "^1.4.8",
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/express-serve-static-core": "^5.0.6",
|
||||
|
||||
@@ -484,7 +484,7 @@ async function prunePrismaQueryEngines(dirPath, keepTarget) {
|
||||
for (const name of entries) {
|
||||
if (
|
||||
name.startsWith('libquery_engine-') &&
|
||||
name.endsWith('.so.node') &&
|
||||
name.endsWith('.node') &&
|
||||
name !== keepName
|
||||
) {
|
||||
await fs.rm(path.join(dirPath, name), { force: true }).catch(() => {});
|
||||
@@ -492,6 +492,55 @@ async function prunePrismaQueryEngines(dirPath, keepTarget) {
|
||||
}
|
||||
}
|
||||
|
||||
async function hardlinkPrismaQueryEngines(appRoot, keepTarget) {
|
||||
const queryEngineName = `libquery_engine-${keepTarget}.so.node`;
|
||||
const paths = [
|
||||
path.join(appRoot, 'node_modules', '.prisma', 'client', queryEngineName),
|
||||
path.join(appRoot, 'node_modules', 'prisma', queryEngineName),
|
||||
path.join(appRoot, 'node_modules', '@prisma', 'engines', queryEngineName),
|
||||
];
|
||||
const stats = await Promise.all(
|
||||
paths.map(filePath => fs.lstat(filePath).catch(() => null))
|
||||
);
|
||||
|
||||
if (stats.some(stat => !stat?.isFile())) {
|
||||
debug('missing prisma query engine copy, skip hardlinking');
|
||||
return { linked: 0, savedBytes: 0 };
|
||||
}
|
||||
|
||||
const canonicalPath = paths[0];
|
||||
const canonicalStat = stats[0];
|
||||
const canonicalDigest = await sha256(canonicalPath);
|
||||
let linked = 0;
|
||||
let savedBytes = 0;
|
||||
|
||||
for (let i = 1; i < paths.length; i += 1) {
|
||||
const duplicatePath = paths[i];
|
||||
const duplicateStat = stats[i];
|
||||
if (
|
||||
!hasCompatibleHardlinkMetadata(canonicalStat, duplicateStat) ||
|
||||
(await sha256(duplicatePath)) !== canonicalDigest
|
||||
) {
|
||||
debug(`prisma query engine copy differs: ${duplicatePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
canonicalStat.dev === duplicateStat.dev &&
|
||||
canonicalStat.ino === duplicateStat.ino
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (await hardlinkDuplicate(canonicalPath, duplicatePath)) {
|
||||
linked += 1;
|
||||
savedBytes += duplicateStat.size;
|
||||
}
|
||||
}
|
||||
|
||||
return { linked, savedBytes };
|
||||
}
|
||||
|
||||
function runPrismaVersion(prismaBinPath, cwd) {
|
||||
const result = spawnSync(prismaBinPath, ['-v'], {
|
||||
cwd,
|
||||
@@ -518,7 +567,7 @@ async function prunePrismaEngines(appRoot, targetKey) {
|
||||
const prismaBinPath = path.join(appRoot, 'node_modules', '.bin', 'prisma');
|
||||
|
||||
if (!(await exists(prismaClientDir))) {
|
||||
return;
|
||||
return { linked: 0, savedBytes: 0 };
|
||||
}
|
||||
|
||||
const keepTarget = await pickExistingPrismaTarget(
|
||||
@@ -528,7 +577,7 @@ async function prunePrismaEngines(appRoot, targetKey) {
|
||||
|
||||
if (!keepTarget) {
|
||||
debug('no prisma keepTarget detected, skip prisma pruning');
|
||||
return;
|
||||
return { linked: 0, savedBytes: 0 };
|
||||
}
|
||||
|
||||
await prunePrismaQueryEngines(prismaClientDir, keepTarget);
|
||||
@@ -545,7 +594,7 @@ async function prunePrismaEngines(appRoot, targetKey) {
|
||||
|
||||
if (!(await exists(keepSchemaEngine))) {
|
||||
debug(`missing ${keepSchemaEngine}, skip pruning @prisma/engines`);
|
||||
return;
|
||||
return { linked: 0, savedBytes: 0 };
|
||||
}
|
||||
|
||||
const keepLibQueryEngine = `libquery_engine-${keepTarget}.so.node`;
|
||||
@@ -566,6 +615,8 @@ async function prunePrismaEngines(appRoot, targetKey) {
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
return hardlinkPrismaQueryEngines(appRoot, keepTarget);
|
||||
}
|
||||
|
||||
async function prunePrismaRuntimeArtifacts(nodeModulesDir) {
|
||||
@@ -738,7 +789,12 @@ await pruneOptionalNativeDeps(
|
||||
cpuPruneRegexes(targetKey)
|
||||
);
|
||||
|
||||
await prunePrismaEngines(APP_ROOT, targetKey);
|
||||
const prismaQueryEngineDedupe = await prunePrismaEngines(APP_ROOT, targetKey);
|
||||
log(
|
||||
`hardlinked prisma query engines: ${prismaQueryEngineDedupe.linked}, saved ${formatMiB(
|
||||
prismaQueryEngineDedupe.savedBytes
|
||||
)}`
|
||||
);
|
||||
|
||||
const nodeModulesDir = path.join(APP_ROOT, 'node_modules');
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { pretty } from '@react-email/render';
|
||||
import test from 'ava';
|
||||
|
||||
import { normalizeSMTPHeloHostname } from '../core/mail/utils';
|
||||
@@ -8,7 +9,7 @@ test('should render emails', async t => {
|
||||
for (const render of Object.values(Renderers)) {
|
||||
// @ts-expect-error use [PreviewProps]
|
||||
const content = await render();
|
||||
t.snapshot(content.html, content.subject);
|
||||
t.snapshot(await pretty(content.html), content.subject);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,7 +21,7 @@ test('should render mention email with empty doc title', async t => {
|
||||
title: '',
|
||||
},
|
||||
});
|
||||
t.snapshot(content.html, content.subject);
|
||||
t.snapshot(await pretty(content.html), content.subject);
|
||||
});
|
||||
|
||||
test('should normalize valid SMTP HELO hostnames', t => {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { format } from 'date-fns';
|
||||
|
||||
import { Bold } from './template';
|
||||
|
||||
export interface DateProps {
|
||||
@@ -7,5 +5,9 @@ export interface DateProps {
|
||||
}
|
||||
|
||||
export const IOSDate = (props: DateProps) => {
|
||||
return <Bold>{format(props.value, 'yyyy-MM-dd')}</Bold>;
|
||||
const year = String(props.value.getFullYear()).padStart(4, '0');
|
||||
const month = String(props.value.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(props.value.getDate()).padStart(2, '0');
|
||||
|
||||
return <Bold>{`${year}-${month}-${day}`}</Bold>;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Link } from '@react-email/components';
|
||||
import { Link } from '@react-email/link';
|
||||
|
||||
import { Bold } from './template';
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Container, Img, Link, Row, Section } from '@react-email/components';
|
||||
import { Container } from '@react-email/container';
|
||||
import { Img } from '@react-email/img';
|
||||
import { Link } from '@react-email/link';
|
||||
import { Row } from '@react-email/row';
|
||||
import { Section } from '@react-email/section';
|
||||
import type { CSSProperties } from 'react';
|
||||
|
||||
import { BasicTextStyle } from './common';
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import {
|
||||
Body,
|
||||
Button as EmailButton,
|
||||
Container,
|
||||
Head,
|
||||
Html,
|
||||
Img,
|
||||
Link,
|
||||
Row,
|
||||
Section,
|
||||
Text as EmailText,
|
||||
} from '@react-email/components';
|
||||
import { Body } from '@react-email/body';
|
||||
import { Button as EmailButton } from '@react-email/button';
|
||||
import { Container } from '@react-email/container';
|
||||
import { Head } from '@react-email/head';
|
||||
import { Html } from '@react-email/html';
|
||||
import { Img } from '@react-email/img';
|
||||
import { Link } from '@react-email/link';
|
||||
import { Row } from '@react-email/row';
|
||||
import { Section } from '@react-email/section';
|
||||
import { Text as EmailText } from '@react-email/text';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
import { BasicTextStyle } from './common';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { render as rawRender } from '@react-email/components';
|
||||
import { type ComponentType, createElement, type ReactElement } from 'react';
|
||||
import { type ComponentType, createElement } from 'react';
|
||||
|
||||
import { Comment, CommentMention, Mention } from './docs';
|
||||
import { render } from './render';
|
||||
import {
|
||||
TeamBecomeAdmin,
|
||||
TeamBecomeCollaborator,
|
||||
@@ -41,10 +41,6 @@ type EmailContent = {
|
||||
html: string;
|
||||
};
|
||||
|
||||
function render(component: ReactElement) {
|
||||
return rawRender(component, { pretty: env.testing });
|
||||
}
|
||||
|
||||
type Props<T> = T extends ComponentType<infer P> ? P : never;
|
||||
export type EmailRenderer<Props> = (props: Props) => Promise<EmailContent>;
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { createElement, type ReactElement, Suspense } from 'react';
|
||||
import { renderToReadableStream } from 'react-dom/server';
|
||||
|
||||
const doctype =
|
||||
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
|
||||
|
||||
export async function render(component: ReactElement) {
|
||||
const stream = await renderToReadableStream(
|
||||
createElement(Suspense, null, component),
|
||||
{ progressiveChunkSize: Number.POSITIVE_INFINITY }
|
||||
);
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
let html = '';
|
||||
|
||||
for await (const chunk of stream) {
|
||||
html += decoder.decode(chunk, { stream: true });
|
||||
}
|
||||
html += decoder.decode();
|
||||
|
||||
return `${doctype}${html.replace(/<!DOCTYPE.*?>/, '')}`;
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { randomUUID } from 'node:crypto';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Transactional } from '@nestjs-cls/transactional';
|
||||
import type { CalendarAccount, Prisma } from '@prisma/client';
|
||||
import { addDays, subDays } from 'date-fns';
|
||||
|
||||
import {
|
||||
CalendarProviderRequestError,
|
||||
@@ -819,9 +818,14 @@ export class CalendarService {
|
||||
|
||||
private getSyncWindow() {
|
||||
const now = this.now();
|
||||
const timeMin = new Date(now);
|
||||
const timeMax = new Date(now);
|
||||
timeMin.setDate(timeMin.getDate() - DEFAULT_PAST_DAYS);
|
||||
timeMax.setDate(timeMax.getDate() + DEFAULT_FUTURE_DAYS);
|
||||
|
||||
return {
|
||||
timeMin: subDays(now, DEFAULT_PAST_DAYS).toISOString(),
|
||||
timeMax: addDays(now, DEFAULT_FUTURE_DAYS).toISOString(),
|
||||
timeMin: timeMin.toISOString(),
|
||||
timeMax: timeMax.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user