Compare commits

..

3 Commits

Author SHA1 Message Date
DarkSky
e69d868c27 fix: transaction error 2026-02-25 21:06:21 +08:00
DarkSky
895e774569 fix: static file handle & ws connect 2026-02-25 11:41:42 +08:00
DarkSky
79460072bb fix: old client compatibility 2026-02-24 23:58:10 +08:00
21 changed files with 475 additions and 418 deletions

View File

@@ -96,12 +96,20 @@ spec:
httpGet:
path: /info
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
initialDelaySeconds: {{ default .Values.probe.initialDelaySeconds .Values.probe.liveness.initialDelaySeconds }}
timeoutSeconds: {{ default .Values.probe.timeoutSeconds .Values.probe.liveness.timeoutSeconds }}
periodSeconds: {{ default .Values.probe.periodSeconds .Values.probe.liveness.periodSeconds }}
failureThreshold: {{ default .Values.probe.failureThreshold .Values.probe.liveness.failureThreshold }}
successThreshold: {{ default .Values.probe.successThreshold .Values.probe.liveness.successThreshold }}
readinessProbe:
httpGet:
path: /info
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
initialDelaySeconds: {{ default .Values.probe.initialDelaySeconds .Values.probe.readiness.initialDelaySeconds }}
timeoutSeconds: {{ default .Values.probe.timeoutSeconds .Values.probe.readiness.timeoutSeconds }}
periodSeconds: {{ default .Values.probe.periodSeconds .Values.probe.readiness.periodSeconds }}
failureThreshold: {{ default .Values.probe.failureThreshold .Values.probe.readiness.failureThreshold }}
successThreshold: {{ default .Values.probe.successThreshold .Values.probe.readiness.successThreshold }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}

View File

@@ -31,13 +31,21 @@ podSecurityContext:
resources:
limits:
cpu: '1'
memory: 2Gi
memory: 4Gi
requests:
cpu: '1'
memory: 2Gi
probe:
initialDelaySeconds: 20
timeoutSeconds: 5
periodSeconds: 10
failureThreshold: 6
successThreshold: 1
liveness:
initialDelaySeconds: 60
failureThreshold: 12
readiness: {}
services:
sync:

View File

@@ -8,6 +8,7 @@ export class MockEventBus {
emit = this.stub.emitAsync;
emitAsync = this.stub.emitAsync;
emitDetached = this.stub.emitAsync;
broadcast = this.stub.broadcast;
last<Event extends EventName>(

View File

@@ -7,7 +7,12 @@ const MOBILE_CLIENT_ORIGINS = new Set([
'capacitor://localhost',
'ionic://localhost',
]);
const DESKTOP_CLIENT_ORIGINS = new Set(['assets://.', 'assets://another-host']);
const DESKTOP_CLIENT_ORIGINS = new Set([
'assets://.',
'assets://another-host',
// for old versions of client, which use file:// as origin
'file://',
]);
export const CORS_ALLOWED_METHODS = [
'GET',
@@ -55,6 +60,19 @@ function isDevLoopbackOrigin(origin: string) {
}
}
function normalizeCorsOrigin(origin: string) {
try {
const parsed = new URL(origin);
// Some websocket clients send ws:// or wss:// as Origin.
if (parsed.protocol === 'ws:' || parsed.protocol === 'wss:') {
parsed.protocol = parsed.protocol === 'wss:' ? 'https:' : 'http:';
}
return parsed.origin;
} catch {
return null;
}
}
export function buildCorsAllowedOrigins(url: URLHelper) {
return new Set<string>([
...url.allowedOrigins,
@@ -75,6 +93,11 @@ export function isCorsOriginAllowed(
return true;
}
const normalizedOrigin = normalizeCorsOrigin(origin);
if (normalizedOrigin && allowedOrigins.has(normalizedOrigin)) {
return true;
}
if ((env.dev || env.testing) && isDevLoopbackOrigin(origin)) {
return true;
}

View File

@@ -88,12 +88,21 @@ export class EventBus
emit<T extends EventName>(event: T, payload: Events[T]) {
this.logger.debug(`Dispatch event: ${event}`);
// NOTE(@forehalo):
// Because all event handlers are wrapped in promisified metrics and cls context, they will always run in standalone tick.
// In which way, if handler throws, an unhandled rejection will be triggered and end up with process exiting.
// So we catch it here with `emitAsync`
this.emitter.emitAsync(event, payload).catch(e => {
this.emitter.emit('error', { event, payload, error: e });
this.dispatchAsync(event, payload);
return true;
}
/**
* Emit event in detached cls context to avoid inheriting current transaction.
*/
emitDetached<T extends EventName>(event: T, payload: Events[T]) {
this.logger.debug(`Dispatch event: ${event} (detached)`);
const requestId = this.cls.getId();
this.cls.run({ ifNested: 'override' }, () => {
this.cls.set(CLS_ID, requestId ?? genRequestId('event'));
this.dispatchAsync(event, payload);
});
return true;
@@ -166,6 +175,16 @@ export class EventBus
return this.emitter.waitFor(name, timeout);
}
private dispatchAsync<T extends EventName>(event: T, payload: Events[T]) {
// NOTE:
// Because all event handlers are wrapped in promisified metrics and cls context, they will always run in standalone tick.
// In which way, if handler throws, an unhandled rejection will be triggered and end up with process exiting.
// So we catch it here with `emitAsync`
this.emitter.emitAsync(event, payload).catch(e => {
this.emitter.emit('error', { event, payload, error: e });
});
}
private readonly bindEventHandlers = once(() => {
this.scanner.scan().forEach(({ event, handler, opts }) => {
this.on(event, handler, opts);

View File

@@ -1,6 +1,7 @@
import ava, { TestFn } from 'ava';
import Sinon from 'sinon';
import { buildCorsAllowedOrigins, isCorsOriginAllowed } from '../../cors';
import { ActionForbidden } from '../../error';
import { URLHelper } from '../url';
@@ -193,3 +194,19 @@ test('can get request base url with multiple hosts', t => {
t.is(url.requestOrigin, 'https://app.affine.local2');
t.is(url.requestBaseUrl, 'https://app.affine.local2');
});
test('should allow websocket secure origin by normalizing wss to https', t => {
const allowedOrigins = buildCorsAllowedOrigins({
allowedOrigins: ['https://app.affine.pro'],
} as any);
t.true(isCorsOriginAllowed('wss://app.affine.pro', allowedOrigins));
});
test('should allow desktop file origin', t => {
const allowedOrigins = buildCorsAllowedOrigins({
allowedOrigins: [],
} as any);
t.true(isCorsOriginAllowed('file://', allowedOrigins));
});

View File

@@ -181,3 +181,22 @@ test('should ignore update workspace content to database when parse workspace co
t.is(content!.name, null);
t.is(content!.avatarKey, null);
});
test('should ignore stale workspace when updating doc meta from snapshot event', async t => {
const { docReader, listener, models } = t.context;
const docId = randomUUID();
mock.method(docReader, 'parseDocContent', () => ({
title: 'test title',
summary: 'test summary',
}));
await models.workspace.delete(workspace.id);
await t.notThrowsAsync(async () => {
await listener.markDocContentCacheStale({
workspaceId: workspace.id,
docId,
blob: Buffer.from([0x01]),
});
});
});

View File

@@ -110,7 +110,7 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
});
if (isNewDoc) {
this.event.emit('doc.created', {
this.event.emitDetached('doc.created', {
workspaceId,
docId,
editor: editorId,
@@ -334,7 +334,7 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
});
if (updatedSnapshot) {
this.event.emit('doc.snapshot.updated', {
this.event.emitDetached('doc.snapshot.updated', {
workspaceId: snapshot.spaceId,
docId: snapshot.docId,
blob,

View File

@@ -1,12 +1,29 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { OnEvent } from '../../base';
import { Models } from '../../models';
import { PgWorkspaceDocStorageAdapter } from './adapters/workspace';
import { DocReader } from './reader';
const IGNORED_PRISMA_CODES = new Set(['P2003', 'P2025', 'P2028']);
function isIgnorableDocEventError(error: unknown) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
return IGNORED_PRISMA_CODES.has(error.code);
}
if (error instanceof Prisma.PrismaClientUnknownRequestError) {
return /transaction is aborted|transaction already closed/i.test(
error.message
);
}
return false;
}
@Injectable()
export class DocEventsListener {
private readonly logger = new Logger(DocEventsListener.name);
constructor(
private readonly docReader: DocReader,
private readonly models: Models,
@@ -20,21 +37,39 @@ export class DocEventsListener {
blob,
}: Events['doc.snapshot.updated']) {
await this.docReader.markDocContentCacheStale(workspaceId, docId);
const workspace = await this.models.workspace.get(workspaceId);
if (!workspace) {
this.logger.warn(
`Skip stale doc snapshot event for missing workspace ${workspaceId}/${docId}`
);
return;
}
const isDoc = workspaceId !== docId;
// update doc content to database
if (isDoc) {
const content = this.docReader.parseDocContent(blob);
if (!content) {
try {
if (isDoc) {
const content = this.docReader.parseDocContent(blob);
if (!content) {
return;
}
await this.models.doc.upsertMeta(workspaceId, docId, content);
} else {
// update workspace content to database
const content = this.docReader.parseWorkspaceContent(blob);
if (!content) {
return;
}
await this.models.workspace.update(workspaceId, content);
}
} catch (error) {
if (isIgnorableDocEventError(error)) {
const message = error instanceof Error ? error.message : String(error);
this.logger.warn(
`Ignore stale doc snapshot event for ${workspaceId}/${docId}: ${message}`
);
return;
}
await this.models.doc.upsertMeta(workspaceId, docId, content);
} else {
// update workspace content to database
const content = this.docReader.parseWorkspaceContent(blob);
if (!content) {
return;
}
await this.models.workspace.update(workspaceId, content);
throw error;
}
}

View File

@@ -0,0 +1,77 @@
import { randomUUID } from 'node:crypto';
import ava, { TestFn } from 'ava';
import {
createTestingModule,
type TestingModule,
} from '../../../__tests__/utils';
import { DocRole, Models, User, Workspace } from '../../../models';
import { EventsListener } from '../event';
import { PermissionModule } from '../index';
interface Context {
module: TestingModule;
models: Models;
listener: EventsListener;
}
const test = ava as TestFn<Context>;
let owner: User;
let workspace: Workspace;
test.before(async t => {
const module = await createTestingModule({ imports: [PermissionModule] });
t.context.module = module;
t.context.models = module.get(Models);
t.context.listener = module.get(EventsListener);
});
test.beforeEach(async t => {
await t.context.module.initTestingDB();
owner = await t.context.models.user.create({
email: `${randomUUID()}@affine.pro`,
});
workspace = await t.context.models.workspace.create(owner.id);
});
test.after.always(async t => {
await t.context.module.close();
});
test('should ignore default owner event when workspace does not exist', async t => {
await t.notThrowsAsync(async () => {
await t.context.listener.setDefaultPageOwner({
workspaceId: randomUUID(),
docId: randomUUID(),
editor: owner.id,
});
});
});
test('should ignore default owner event when editor does not exist', async t => {
await t.notThrowsAsync(async () => {
await t.context.listener.setDefaultPageOwner({
workspaceId: workspace.id,
docId: randomUUID(),
editor: randomUUID(),
});
});
});
test('should set owner when workspace and editor exist', async t => {
const docId = randomUUID();
await t.context.listener.setDefaultPageOwner({
workspaceId: workspace.id,
docId,
editor: owner.id,
});
const role = await t.context.models.docUser.get(
workspace.id,
docId,
owner.id
);
t.is(role?.type, DocRole.Owner);
});

View File

@@ -1,10 +1,27 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { OnEvent } from '../../base';
import { Models } from '../../models';
const IGNORED_PRISMA_CODES = new Set(['P2003', 'P2025', 'P2028']);
function isIgnorablePermissionEventError(error: unknown) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
return IGNORED_PRISMA_CODES.has(error.code);
}
if (error instanceof Prisma.PrismaClientUnknownRequestError) {
return /transaction is aborted|transaction already closed/i.test(
error.message
);
}
return false;
}
@Injectable()
export class EventsListener {
private readonly logger = new Logger(EventsListener.name);
constructor(private readonly models: Models) {}
@OnEvent('doc.created')
@@ -15,6 +32,33 @@ export class EventsListener {
return;
}
await this.models.docUser.setOwner(workspaceId, docId, editor);
const workspace = await this.models.workspace.get(workspaceId);
if (!workspace) {
this.logger.warn(
`Skip default doc owner event for missing workspace ${workspaceId}/${docId}`
);
return;
}
const user = await this.models.user.get(editor);
if (!user) {
this.logger.warn(
`Skip default doc owner event for missing editor ${workspaceId}/${docId}/${editor}`
);
return;
}
try {
await this.models.docUser.setOwner(workspaceId, docId, editor);
} catch (error) {
if (isIgnorablePermissionEventError(error)) {
const message = error instanceof Error ? error.message : String(error);
this.logger.warn(
`Ignore stale doc owner event for ${workspaceId}/${docId}/${editor}: ${message}`
);
return;
}
throw error;
}
}
}

View File

@@ -10,6 +10,16 @@ import { isMobileRequest } from '../utils/user-agent';
const staticPathRegex = /^\/(_plugin|assets|imgs|js|plugins|static)\//;
function isMissingStaticAssetError(error: unknown) {
if (!error || typeof error !== 'object') {
return false;
}
const err = error as { code?: string; status?: number; statusCode?: number };
return err.code === 'ENOENT' || err.status === 404 || err.statusCode === 404;
}
@Injectable()
export class StaticFilesResolver implements OnModuleInit {
constructor(
@@ -86,7 +96,18 @@ export class StaticFilesResolver implements OnModuleInit {
next();
return;
}
routeByUA(req, res, next, true);
routeByUA(
req,
res,
error => {
if (isMissingStaticAssetError(error)) {
res.status(404).end();
return;
}
next(error);
},
true
);
});
// /

View File

@@ -210,6 +210,9 @@ export class SpaceSyncGateway
private readonly server!: Server;
private connectionCount = 0;
private readonly socketUsers = new Map<string, string>();
private readonly localUserConnectionCounts = new Map<string, number>();
private unresolvedPresenceSockets = 0;
private flushTimer?: NodeJS.Timeout;
constructor(
@@ -224,7 +227,9 @@ export class SpaceSyncGateway
onModuleInit() {
this.flushTimer = setInterval(() => {
this.flushActiveUsersMinute().catch(error => {
this.logger.warn('Failed to flush active users minute', error as Error);
this.logger.warn(
`Failed to flush active users minute: ${this.formatError(error)}`
);
});
}, 60_000);
this.flushTimer.unref?.();
@@ -278,8 +283,7 @@ export class SpaceSyncGateway
};
} catch (error) {
this.logger.warn(
'Failed to merge updates for broadcast, falling back to batch',
error as Error
`Failed to merge updates for broadcast, falling back to batch: ${this.formatError(error)}`
);
return {
spaceType,
@@ -302,14 +306,20 @@ export class SpaceSyncGateway
this.connectionCount++;
this.logger.debug(`New connection, total: ${this.connectionCount}`);
metrics.socketio.gauge('connections').record(this.connectionCount);
this.attachPresenceUserId(client);
this.flushActiveUsersMinute().catch(error => {
this.logger.warn('Failed to flush active users minute', error as Error);
const userId = this.attachPresenceUserId(client);
this.trackConnectedSocket(client.id, userId);
void this.flushActiveUsersMinute({
aggregateAcrossCluster: false,
}).catch(error => {
this.logger.warn(
`Failed to flush active users minute: ${this.formatError(error)}`
);
});
}
handleDisconnect(_client: Socket) {
handleDisconnect(client: Socket) {
this.connectionCount = Math.max(0, this.connectionCount - 1);
this.trackDisconnectedSocket(client.id);
this.logger.debug(
`Connection disconnected, total: ${this.connectionCount}`
);
@@ -317,21 +327,24 @@ export class SpaceSyncGateway
void this.flushActiveUsersMinute({
aggregateAcrossCluster: false,
}).catch(error => {
this.logger.warn('Failed to flush active users minute', error as Error);
this.logger.warn(
`Failed to flush active users minute: ${this.formatError(error)}`
);
});
}
private attachPresenceUserId(client: Socket) {
private attachPresenceUserId(client: Socket): string | null {
const request = client.request as Request;
const userId = request.session?.user.id ?? request.token?.user.id;
if (typeof userId !== 'string' || !userId) {
this.logger.warn(
`Unable to resolve authenticated user id for socket ${client.id}`
);
return;
return null;
}
client.data[SOCKET_PRESENCE_USER_ID_KEY] = userId;
return userId;
}
private resolvePresenceUserId(socket: { data?: unknown }) {
@@ -345,6 +358,60 @@ export class SpaceSyncGateway
return typeof userId === 'string' && userId ? userId : null;
}
private trackConnectedSocket(socketId: string, userId: string | null) {
if (!userId) {
this.unresolvedPresenceSockets++;
return;
}
this.socketUsers.set(socketId, userId);
const prev = this.localUserConnectionCounts.get(userId) ?? 0;
this.localUserConnectionCounts.set(userId, prev + 1);
}
private trackDisconnectedSocket(socketId: string) {
const userId = this.socketUsers.get(socketId);
if (!userId) {
this.unresolvedPresenceSockets = Math.max(
0,
this.unresolvedPresenceSockets - 1
);
return;
}
this.socketUsers.delete(socketId);
const next = (this.localUserConnectionCounts.get(userId) ?? 1) - 1;
if (next <= 0) {
this.localUserConnectionCounts.delete(userId);
} else {
this.localUserConnectionCounts.set(userId, next);
}
}
private resolveLocalActiveUsers() {
if (this.unresolvedPresenceSockets > 0) {
return Math.max(0, this.connectionCount);
}
return this.localUserConnectionCounts.size;
}
private formatError(error: unknown) {
if (error instanceof Error) {
return error.stack ?? error.message;
}
if (typeof error === 'string') {
return error;
}
try {
return JSON.stringify(error);
} catch {
return String(error);
}
}
private async flushActiveUsersMinute(options?: {
aggregateAcrossCluster?: boolean;
}) {
@@ -352,7 +419,7 @@ export class SpaceSyncGateway
minute.setSeconds(0, 0);
const aggregateAcrossCluster = options?.aggregateAcrossCluster ?? true;
let activeUsers = Math.max(0, this.connectionCount);
let activeUsers = this.resolveLocalActiveUsers();
if (aggregateAcrossCluster) {
try {
const sockets = await this.server.fetchSockets();
@@ -377,8 +444,7 @@ export class SpaceSyncGateway
}
} catch (error) {
this.logger.warn(
'Failed to aggregate active users from sockets, using local value',
error as Error
`Failed to aggregate active users from sockets, using local value: ${this.formatError(error)}`
);
}
}

View File

@@ -2,6 +2,7 @@ import assert from 'node:assert';
import { Injectable } from '@nestjs/common';
import { Transactional } from '@nestjs-cls/transactional';
import type { TransactionalAdapterPrisma } from '@nestjs-cls/transactional-adapter-prisma';
import { WorkspaceDocUserRole } from '@prisma/client';
import { CanNotBatchGrantDocOwnerPermissions, PaginationInput } from '../base';
@@ -14,31 +15,20 @@ export class DocUserModel extends BaseModel {
* Set or update the [Owner] of a doc.
* The old [Owner] will be changed to [Manager] if there is already an [Owner].
*/
@Transactional()
@Transactional<TransactionalAdapterPrisma>({ timeout: 15000 })
async setOwner(workspaceId: string, docId: string, userId: string) {
const oldOwner = await this.db.workspaceDocUserRole.findFirst({
await this.db.workspaceDocUserRole.updateMany({
where: {
workspaceId,
docId,
type: DocRole.Owner,
userId: { not: userId },
},
data: {
type: DocRole.Manager,
},
});
if (oldOwner) {
await this.db.workspaceDocUserRole.update({
where: {
workspaceId_docId_userId: {
workspaceId,
docId,
userId: oldOwner.userId,
},
},
data: {
type: DocRole.Manager,
},
});
}
await this.db.workspaceDocUserRole.upsert({
where: {
workspaceId_docId_userId: {
@@ -57,16 +47,9 @@ export class DocUserModel extends BaseModel {
type: DocRole.Owner,
},
});
if (oldOwner) {
this.logger.log(
`Transfer doc owner of [${workspaceId}/${docId}] from [${oldOwner.userId}] to [${userId}]`
);
} else {
this.logger.log(
`Set doc owner of [${workspaceId}/${docId}] to [${userId}]`
);
}
this.logger.log(
`Set doc owner of [${workspaceId}/${docId}] to [${userId}]`
);
}
/**

View File

@@ -194,27 +194,13 @@ const PAGE_SIZE = 6;
export const BackupSettingPanel = () => {
const t = useI18n();
const backupService = useService(BackupService);
const [retryCount, setRetryCount] = useState(0);
useEffect(() => {
setRetryCount(0);
backupService.revalidate();
}, [backupService]);
const isLoading = useLiveData(backupService.isLoading$);
const backupWorkspaces = useLiveData(backupService.pageBackupWorkspaces$);
const backupCount = backupWorkspaces?.items.length ?? 0;
useEffect(() => {
// Workspace deletion may complete slightly after panel mount.
// Retry a few times to avoid showing stale empty state.
if (isLoading || backupCount > 0 || retryCount >= 4) return;
const timer = setTimeout(() => {
setRetryCount(current => current + 1);
backupService.revalidate();
}, 1000);
return () => clearTimeout(timer);
}, [backupCount, backupService, isLoading, retryCount]);
const [pageNum, setPageNum] = useState(0);

View File

@@ -137,26 +137,15 @@ test('delete workspace and then restore it from backup', async ({ page }) => {
);
//#endregion
await expect
.poll(
async () =>
(
await page.evaluate(async () => {
return await window.__apis?.workspace.getBackupWorkspaces();
})
)?.items.length,
{ timeout: 20000 }
)
.toBeGreaterThan(0);
await page.waitForTimeout(1000);
//#region 4. restore the workspace from backup
await page.getByTestId('slider-bar-workspace-setting-button').click();
await expect(page.getByTestId('setting-modal')).toBeVisible();
await page.getByTestId('backup-panel-trigger').click();
const backupWorkspaceItems = page.getByTestId('backup-workspace-item');
await expect(backupWorkspaceItems.first()).toBeVisible();
await backupWorkspaceItems.first().click();
await expect(page.getByTestId('backup-workspace-item')).toHaveCount(1);
await page.getByTestId('backup-workspace-item').click();
await page.getByRole('menuitem', { name: 'Enable local workspace' }).click();
const toast = page.locator(
'[data-sonner-toast]:has-text("Workspace enabled successfully")'

View File

@@ -36,7 +36,6 @@
"lodash-es": "^4.17.23",
"mime-types": "^3.0.0",
"mini-css-extract-plugin": "^2.9.2",
"minify-html-literals": "^1.3.5",
"node-loader": "^2.1.0",
"postcss": "^8.4.49",
"postcss-loader": "^8.1.1",

View File

@@ -22,9 +22,6 @@ import {
const require = createRequire(import.meta.url);
const IN_CI = !!process.env.CI;
const LIT_CSS_MINIFY_LOADER = Path.dir(import.meta.url).join(
'../webpack/lit-css-minify-loader.cjs'
).value;
const availableChannels = ['canary', 'beta', 'stable', 'internal'];
function getBuildConfigFromEnv(pkg: Package) {
@@ -148,69 +145,55 @@ export function createHTMLTargetConfig(
{
test: /\.ts$/,
exclude: /node_modules/,
use: compact([
!buildConfig.debug && {
loader: LIT_CSS_MINIFY_LOADER,
},
{
loader: 'swc-loader',
options: {
// https://swc.rs/docs/configuring-swc/
jsc: {
preserveAllComments: true,
parser: {
syntax: 'typescript',
dynamicImport: true,
topLevelAwait: false,
tsx: false,
decorators: true,
},
target: 'es2022',
externalHelpers: false,
transform: {
useDefineForClassFields: false,
decoratorVersion: '2022-03',
},
},
sourceMaps: true,
inlineSourcesContent: true,
loader: 'swc-loader',
options: {
// https://swc.rs/docs/configuring-swc/
jsc: {
preserveAllComments: true,
parser: {
syntax: 'typescript',
dynamicImport: true,
topLevelAwait: false,
tsx: false,
decorators: true,
},
target: 'es2022',
externalHelpers: false,
transform: {
useDefineForClassFields: false,
decoratorVersion: '2022-03',
},
},
]),
sourceMaps: true,
inlineSourcesContent: true,
},
},
{
test: /\.tsx$/,
exclude: /node_modules/,
use: compact([
!buildConfig.debug && {
loader: LIT_CSS_MINIFY_LOADER,
},
{
loader: 'swc-loader',
options: {
// https://swc.rs/docs/configuring-swc/
jsc: {
preserveAllComments: true,
parser: {
syntax: 'typescript',
dynamicImport: true,
topLevelAwait: false,
tsx: true,
decorators: true,
},
target: 'es2022',
externalHelpers: false,
transform: {
react: { runtime: 'automatic' },
useDefineForClassFields: false,
decoratorVersion: '2022-03',
},
},
sourceMaps: true,
inlineSourcesContent: true,
loader: 'swc-loader',
options: {
// https://swc.rs/docs/configuring-swc/
jsc: {
preserveAllComments: true,
parser: {
syntax: 'typescript',
dynamicImport: true,
topLevelAwait: false,
tsx: true,
decorators: true,
},
target: 'es2022',
externalHelpers: false,
transform: {
react: { runtime: 'automatic' },
useDefineForClassFields: false,
decoratorVersion: '2022-03',
},
},
]),
sourceMaps: true,
inlineSourcesContent: true,
},
},
{
test: /\.(png|jpg|gif|svg|webp|mp4|zip)$/,

View File

@@ -24,9 +24,6 @@ const require = createRequire(import.meta.url);
const cssnano = require('cssnano');
const IN_CI = !!process.env.CI;
const LIT_CSS_MINIFY_LOADER = Path.dir(import.meta.url).join(
'lit-css-minify-loader.cjs'
).value;
const availableChannels = ['canary', 'beta', 'stable', 'internal'];
function getBuildConfigFromEnv(pkg: Package) {
@@ -150,69 +147,55 @@ export function createHTMLTargetConfig(
{
test: /\.ts$/,
exclude: /node_modules/,
use: compact([
!buildConfig.debug && {
loader: LIT_CSS_MINIFY_LOADER,
},
{
loader: 'swc-loader',
options: {
// https://swc.rs/docs/configuring-swc/
jsc: {
preserveAllComments: true,
parser: {
syntax: 'typescript',
dynamicImport: true,
topLevelAwait: false,
tsx: false,
decorators: true,
},
target: 'es2022',
externalHelpers: false,
transform: {
useDefineForClassFields: false,
decoratorVersion: '2022-03',
},
},
sourceMaps: true,
inlineSourcesContent: true,
loader: 'swc-loader',
options: {
// https://swc.rs/docs/configuring-swc/
jsc: {
preserveAllComments: true,
parser: {
syntax: 'typescript',
dynamicImport: true,
topLevelAwait: false,
tsx: false,
decorators: true,
},
target: 'es2022',
externalHelpers: false,
transform: {
useDefineForClassFields: false,
decoratorVersion: '2022-03',
},
},
]),
sourceMaps: true,
inlineSourcesContent: true,
},
},
{
test: /\.tsx$/,
exclude: /node_modules/,
use: compact([
!buildConfig.debug && {
loader: LIT_CSS_MINIFY_LOADER,
},
{
loader: 'swc-loader',
options: {
// https://swc.rs/docs/configuring-swc/
jsc: {
preserveAllComments: true,
parser: {
syntax: 'typescript',
dynamicImport: true,
topLevelAwait: false,
tsx: true,
decorators: true,
},
target: 'es2022',
externalHelpers: false,
transform: {
react: { runtime: 'automatic' },
useDefineForClassFields: false,
decoratorVersion: '2022-03',
},
},
sourceMaps: true,
inlineSourcesContent: true,
loader: 'swc-loader',
options: {
// https://swc.rs/docs/configuring-swc/
jsc: {
preserveAllComments: true,
parser: {
syntax: 'typescript',
dynamicImport: true,
topLevelAwait: false,
tsx: true,
decorators: true,
},
target: 'es2022',
externalHelpers: false,
transform: {
react: { runtime: 'automatic' },
useDefineForClassFields: false,
decoratorVersion: '2022-03',
},
},
]),
sourceMaps: true,
inlineSourcesContent: true,
},
},
{
test: /\.(png|jpg|gif|svg|webp|mp4|zip)$/,

View File

@@ -1,31 +0,0 @@
const { minifyHTMLLiterals } = require('minify-html-literals');
/**
* Minify CSS in tagged template literals (e.g. lit `css```) while leaving
* HTML templates untouched to avoid parser regressions on dynamic templates.
*
* @type {import('webpack').LoaderDefinitionFunction}
*/
module.exports = function litCssMinifyLoader(source, sourceMap) {
if (typeof source !== 'string' || !source.includes('`')) {
return source;
}
try {
const result = minifyHTMLLiterals(source, {
fileName: this.resourcePath,
shouldMinify: () => false,
shouldMinifyCSS: template =>
!!template.tag && template.tag.toLowerCase().includes('css'),
});
if (!result) {
return source;
}
this.callback(null, result.code, result.map ?? sourceMap);
return;
} catch {
return source;
}
};

177
yarn.lock
View File

@@ -141,7 +141,6 @@ __metadata:
lodash-es: "npm:^4.17.23"
mime-types: "npm:^3.0.0"
mini-css-extract-plugin: "npm:^2.9.2"
minify-html-literals: "npm:^1.3.5"
node-loader: "npm:^2.1.0"
postcss: "npm:^8.4.49"
postcss-loader: "npm:^8.1.1"
@@ -17234,16 +17233,6 @@ __metadata:
languageName: node
linkType: hard
"@types/clean-css@npm:*":
version: 4.2.11
resolution: "@types/clean-css@npm:4.2.11"
dependencies:
"@types/node": "npm:*"
source-map: "npm:^0.6.0"
checksum: 10/385337a881c7870664d8987f12b9c814d835104dcf5f1737b74ab759ca68424ce93636cbff73ea9c41290c3dc2a92a4cc6246869bd9982255cfa28dcc2ccec93
languageName: node
linkType: hard
"@types/connect-history-api-fallback@npm:^1.5.4":
version: 1.5.4
resolution: "@types/connect-history-api-fallback@npm:1.5.4"
@@ -17765,17 +17754,6 @@ __metadata:
languageName: node
linkType: hard
"@types/html-minifier@npm:^3.5.3":
version: 3.5.3
resolution: "@types/html-minifier@npm:3.5.3"
dependencies:
"@types/clean-css": "npm:*"
"@types/relateurl": "npm:*"
"@types/uglify-js": "npm:*"
checksum: 10/bad3bece7ec0c29d81266a5884ade79a6d6fb5c10b9a7b79e17dae13290052776a1a5a571125fd5f47e396cd45304eafaec41b68bf42d0dd765356cf77a6e088
languageName: node
linkType: hard
"@types/http-assert@npm:*":
version: 1.5.6
resolution: "@types/http-assert@npm:1.5.6"
@@ -18228,13 +18206,6 @@ __metadata:
languageName: node
linkType: hard
"@types/relateurl@npm:*":
version: 0.2.33
resolution: "@types/relateurl@npm:0.2.33"
checksum: 10/a4b7876cc24da3eddc1202d9f57fb6cdd551ff3d884124365dd15012dde20c2b4c19eee9bcd3b17e7c43e8edbe82a33753a6c266e41e3761283d44e6234d47da
languageName: node
linkType: hard
"@types/resolve@npm:^1.20.2":
version: 1.20.6
resolution: "@types/resolve@npm:1.20.6"
@@ -18395,15 +18366,6 @@ __metadata:
languageName: node
linkType: hard
"@types/uglify-js@npm:*":
version: 3.17.5
resolution: "@types/uglify-js@npm:3.17.5"
dependencies:
source-map: "npm:^0.6.1"
checksum: 10/87368861a3f2df071905d698c9f7a4b825e2f69dd29530283594ccddd155d4a8ff7795021af28a97d938c9557a6ea23bc3d77e076a6cf3e02f6401849e067f61
languageName: node
linkType: hard
"@types/unist@npm:*, @types/unist@npm:^3.0.0":
version: 3.0.3
resolution: "@types/unist@npm:3.0.3"
@@ -20703,16 +20665,6 @@ __metadata:
languageName: node
linkType: hard
"camel-case@npm:^3.0.0":
version: 3.0.0
resolution: "camel-case@npm:3.0.0"
dependencies:
no-case: "npm:^2.2.0"
upper-case: "npm:^1.1.1"
checksum: 10/4190ed6ab8acf4f3f6e1a78ad4d0f3f15ce717b6bfa1b5686d58e4bcd29960f6e312dd746b5fa259c6d452f1413caef25aee2e10c9b9a580ac83e516533a961a
languageName: node
linkType: hard
"camel-case@npm:^4.1.2":
version: 4.1.2
resolution: "camel-case@npm:4.1.2"
@@ -21183,15 +21135,6 @@ __metadata:
languageName: node
linkType: hard
"clean-css@npm:^4.2.1":
version: 4.2.4
resolution: "clean-css@npm:4.2.4"
dependencies:
source-map: "npm:~0.6.0"
checksum: 10/4f64dbebfa29feb79be25d6f91239239179adc805c6d7442e2c728970ca23a75b5f238118477b4b78553b89e50f14a64fe35145ecc86b6badf971883c4ad2ffe
languageName: node
linkType: hard
"clean-css@npm:^5.2.2":
version: 5.3.3
resolution: "clean-css@npm:5.3.3"
@@ -21573,7 +21516,7 @@ __metadata:
languageName: node
linkType: hard
"commander@npm:^2.19.0, commander@npm:^2.20.0, commander@npm:^2.20.3":
"commander@npm:^2.20.0, commander@npm:^2.20.3":
version: 2.20.3
resolution: "commander@npm:2.20.3"
checksum: 10/90c5b6898610cd075984c58c4f88418a4fb44af08c1b1415e9854c03171bec31b336b7f3e4cefe33de994b3f12b03c5e2d638da4316df83593b9e82554e7e95b
@@ -26461,23 +26404,6 @@ __metadata:
languageName: node
linkType: hard
"html-minifier@npm:^4.0.0":
version: 4.0.0
resolution: "html-minifier@npm:4.0.0"
dependencies:
camel-case: "npm:^3.0.0"
clean-css: "npm:^4.2.1"
commander: "npm:^2.19.0"
he: "npm:^1.2.0"
param-case: "npm:^2.1.1"
relateurl: "npm:^0.2.7"
uglify-js: "npm:^3.5.1"
bin:
html-minifier: ./cli.js
checksum: 10/a1a49ee78a41eb3232f7aa51be25092d7634548e8996577b2bdab22dc9ac736594d35aab7fdf81fb5a0da11f7bc688f500c297b24fd312d48c0ce8739ed4f06f
languageName: node
linkType: hard
"html-parse-stringify2@github:locize/html-parse-stringify2":
version: 2.0.1
resolution: "html-parse-stringify2@https://github.com/locize/html-parse-stringify2.git#commit=d463109433b2c49c74a081044f54b2a6a1ccad7c"
@@ -29107,13 +29033,6 @@ __metadata:
languageName: node
linkType: hard
"lower-case@npm:^1.1.1":
version: 1.1.4
resolution: "lower-case@npm:1.1.4"
checksum: 10/0c4aebc459ba330bcc38d20cad26ee33111155ed09c09e7d7ec395997277feee3a4d8db541ed5ca555f20ddc5c65a3b23648d18fcd2a950376da6d0c2e01416e
languageName: node
linkType: hard
"lower-case@npm:^2.0.2":
version: 2.0.2
resolution: "lower-case@npm:2.0.2"
@@ -29222,15 +29141,6 @@ __metadata:
languageName: node
linkType: hard
"magic-string@npm:^0.25.0":
version: 0.25.9
resolution: "magic-string@npm:0.25.9"
dependencies:
sourcemap-codec: "npm:^1.4.8"
checksum: 10/87a14b944bd169821cbd54b169a7ab6b0348fd44b5497266dc555dd70280744e9e88047da9dcb95675bdc23b1ce33f13398b0f70b3be7b858225ccb1d185ff51
languageName: node
linkType: hard
"magic-string@npm:^0.30.0, magic-string@npm:^0.30.17, magic-string@npm:^0.30.21":
version: 0.30.21
resolution: "magic-string@npm:0.30.21"
@@ -30226,19 +30136,6 @@ __metadata:
languageName: node
linkType: hard
"minify-html-literals@npm:^1.3.5":
version: 1.3.5
resolution: "minify-html-literals@npm:1.3.5"
dependencies:
"@types/html-minifier": "npm:^3.5.3"
clean-css: "npm:^4.2.1"
html-minifier: "npm:^4.0.0"
magic-string: "npm:^0.25.0"
parse-literals: "npm:^1.2.1"
checksum: 10/9f5b50055e0df5763463f37136418af5a143169ffa1bafdc73571ab36563e5c49e4bf5068f7cc2cecd80b07d974e467e3879e750139e14c400b99843e9c23737
languageName: node
linkType: hard
"minimalistic-assert@npm:^1.0.0":
version: 1.0.1
resolution: "minimalistic-assert@npm:1.0.1"
@@ -30933,15 +30830,6 @@ __metadata:
languageName: node
linkType: hard
"no-case@npm:^2.2.0":
version: 2.3.2
resolution: "no-case@npm:2.3.2"
dependencies:
lower-case: "npm:^1.1.1"
checksum: 10/a92fc7c10f40477bb69c3ca00e2a12fd08f838204bcef66233cbe8a36c0ec7938ba0cdf3f0534b38702376cbfa26270130607c0b8460ea87f44d474919c39c91
languageName: node
linkType: hard
"no-case@npm:^3.0.4":
version: 3.0.4
resolution: "no-case@npm:3.0.4"
@@ -31874,15 +31762,6 @@ __metadata:
languageName: node
linkType: hard
"param-case@npm:^2.1.1":
version: 2.1.1
resolution: "param-case@npm:2.1.1"
dependencies:
no-case: "npm:^2.2.0"
checksum: 10/3a63dcb8d8dc7995a612de061afdc7bb6fe7bd0e6db994db8d4cae999ed879859fd24389090e1a0d93f4c9207ebf8c048c870f468a3f4767161753e03cb9ab58
languageName: node
linkType: hard
"param-case@npm:^3.0.4":
version: 3.0.4
resolution: "param-case@npm:3.0.4"
@@ -31967,15 +31846,6 @@ __metadata:
languageName: node
linkType: hard
"parse-literals@npm:^1.2.1":
version: 1.2.1
resolution: "parse-literals@npm:1.2.1"
dependencies:
typescript: "npm:^2.9.2 || ^3.0.0 || ^4.0.0"
checksum: 10/8f1ad2ed47887b555a909a402d47dfa94d57adc5f3f70b10543618eebfd77912295e54989971a580d4704994e498263ec2a2ceb0ed53fdd1a3b57449254f094e
languageName: node
linkType: hard
"parse-ms@npm:^4.0.0":
version: 4.0.0
resolution: "parse-ms@npm:4.0.0"
@@ -35837,7 +35707,7 @@ __metadata:
languageName: node
linkType: hard
"source-map@npm:^0.6.0, source-map@npm:^0.6.1, source-map@npm:~0.6.0, source-map@npm:~0.6.1":
"source-map@npm:^0.6.0, source-map@npm:~0.6.0, source-map@npm:~0.6.1":
version: 0.6.1
resolution: "source-map@npm:0.6.1"
checksum: 10/59ef7462f1c29d502b3057e822cdbdae0b0e565302c4dd1a95e11e793d8d9d62006cdc10e0fd99163ca33ff2071360cf50ee13f90440806e7ed57d81cba2f7ff
@@ -35851,13 +35721,6 @@ __metadata:
languageName: node
linkType: hard
"sourcemap-codec@npm:^1.4.8":
version: 1.4.8
resolution: "sourcemap-codec@npm:1.4.8"
checksum: 10/6fc57a151e982b5c9468362690c6d062f3a0d4d8520beb68a82f319c79e7a4d7027eeb1e396de0ecc2cd19491e1d602b2d06fd444feac9b63dd43fea4c55a857
languageName: node
linkType: hard
"space-separated-tokens@npm:^2.0.0":
version: 2.0.2
resolution: "space-separated-tokens@npm:2.0.2"
@@ -37503,16 +37366,6 @@ __metadata:
languageName: node
linkType: hard
"typescript@npm:^2.9.2 || ^3.0.0 || ^4.0.0":
version: 4.9.5
resolution: "typescript@npm:4.9.5"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 10/458f7220ab11e0fc191514cc41be1707645ec9a8c2d609448a448e18c522cef9646f58728f6811185a4c35613dacdf6c98cf8965c88b3541d0288c47291e4300
languageName: node
linkType: hard
"typescript@npm:~5.4.5":
version: 5.4.5
resolution: "typescript@npm:5.4.5"
@@ -37533,16 +37386,6 @@ __metadata:
languageName: node
linkType: hard
"typescript@patch:typescript@npm%3A^2.9.2 || ^3.0.0 || ^4.0.0#optional!builtin<compat/typescript>":
version: 4.9.5
resolution: "typescript@patch:typescript@npm%3A4.9.5#optional!builtin<compat/typescript>::version=4.9.5&hash=289587"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 10/5659316360b5cc2d6f5931b346401fa534107b68b60179cf14970e27978f0936c1d5c46f4b5b8175f8cba0430f522b3ce355b4b724c0ea36ce6c0347fab25afd
languageName: node
linkType: hard
"typescript@patch:typescript@npm%3A~5.4.5#optional!builtin<compat/typescript>":
version: 5.4.5
resolution: "typescript@patch:typescript@npm%3A5.4.5#optional!builtin<compat/typescript>::version=5.4.5&hash=5adc0c"
@@ -37576,15 +37419,6 @@ __metadata:
languageName: node
linkType: hard
"uglify-js@npm:^3.5.1":
version: 3.19.3
resolution: "uglify-js@npm:3.19.3"
bin:
uglifyjs: bin/uglifyjs
checksum: 10/6b9639c1985d24580b01bb0ab68e78de310d38eeba7db45bec7850ab4093d8ee464d80ccfaceda9c68d1c366efbee28573b52f95e69ac792354c145acd380b11
languageName: node
linkType: hard
"uid2@npm:1.0.0":
version: 1.0.0
resolution: "uid2@npm:1.0.0"
@@ -37974,13 +37808,6 @@ __metadata:
languageName: node
linkType: hard
"upper-case@npm:^1.1.1":
version: 1.1.3
resolution: "upper-case@npm:1.1.3"
checksum: 10/fc4101fdcd783ee963d49d279186688d4ba2fab90e78dbd001ad141522a66ccfe310932f25e70d5211b559ab205be8c24bf9c5520c7ab7dcd0912274c6d976a3
languageName: node
linkType: hard
"upper-case@npm:^2.0.2":
version: 2.0.2
resolution: "upper-case@npm:2.0.2"