mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-26 18:55:57 +08:00
Compare commits
3 Commits
darksky/li
...
darksky/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e69d868c27 | ||
|
|
895e774569 | ||
|
|
79460072bb |
@@ -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 }}
|
||||
|
||||
10
.github/helm/affine/charts/front/values.yaml
vendored
10
.github/helm/affine/charts/front/values.yaml
vendored
@@ -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:
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -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]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
// /
|
||||
|
||||
@@ -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)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}]`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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")'
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)$/,
|
||||
|
||||
@@ -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)$/,
|
||||
|
||||
@@ -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
177
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user