mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor: implement ttl cache
This commit is contained in:
parent
1256711bcc
commit
e0fad2dccd
26 changed files with 202 additions and 54 deletions
|
@ -47,7 +47,7 @@
|
|||
"@logto/core-kit": "workspace:*",
|
||||
"@logto/schemas": "workspace:*",
|
||||
"@logto/shared": "workspace:*",
|
||||
"@silverhand/essentials": "^2.4.1",
|
||||
"@silverhand/essentials": "2.4.1",
|
||||
"chalk": "^5.0.0",
|
||||
"decamelize": "^6.0.0",
|
||||
"dotenv": "^16.0.0",
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
"@logto/core-kit": "workspace:*",
|
||||
"@logto/schemas": "workspace:*",
|
||||
"@logto/shared": "workspace:*",
|
||||
"@silverhand/essentials": "^2.4.1",
|
||||
"@silverhand/essentials": "2.4.1",
|
||||
"@withtyped/postgres": "^0.8.1",
|
||||
"@withtyped/server": "^0.8.1",
|
||||
"accepts": "^1.3.8",
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
"@parcel/transformer-svg-react": "2.8.3",
|
||||
"@silverhand/eslint-config": "2.0.1",
|
||||
"@silverhand/eslint-config-react": "2.0.1",
|
||||
"@silverhand/essentials": "^2.4.1",
|
||||
"@silverhand/essentials": "2.4.1",
|
||||
"@silverhand/ts-config": "2.0.3",
|
||||
"@silverhand/ts-config-react": "2.0.3",
|
||||
"@tsconfig/docusaurus": "^1.0.5",
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
"@logto/phrases-ui": "workspace:*",
|
||||
"@logto/schemas": "workspace:*",
|
||||
"@logto/shared": "workspace:*",
|
||||
"@silverhand/essentials": "^2.4.1",
|
||||
"@silverhand/essentials": "2.4.1",
|
||||
"aws-sdk": "^2.1329.0",
|
||||
"chalk": "^5.0.0",
|
||||
"clean-deep": "^3.4.0",
|
||||
|
@ -51,7 +51,6 @@
|
|||
"iconv-lite": "0.6.3",
|
||||
"jose": "^4.11.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"keyv": "^4.5.2",
|
||||
"koa": "^2.13.1",
|
||||
"koa-body": "^5.0.0",
|
||||
"koa-compose": "^4.1.0",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import Keyv from 'keyv';
|
||||
import { TtlCache } from '@logto/shared';
|
||||
import type { AnyAsyncFunction } from 'p-memoize';
|
||||
import pMemoize from 'p-memoize';
|
||||
|
||||
|
@ -10,14 +10,14 @@ export type WellKnownCacheKey = (typeof cacheKeys)[number];
|
|||
const buildKey = (tenantId: string, key: WellKnownCacheKey) => `${tenantId}:${key}` as const;
|
||||
|
||||
class WellKnownCache {
|
||||
// Not sure if we need guard value for `.has()` and `.get()`,
|
||||
// trust cache value for now.
|
||||
#keyv = new Keyv({ ttl: 180_000 /* 3 minutes */ });
|
||||
#cache = new TtlCache<string, unknown>(180_000 /* 3 minutes */);
|
||||
|
||||
/**
|
||||
* Use for centralized well-known data caching.
|
||||
*
|
||||
* WARN: You should store only well-known (public) data since it's a central cache.
|
||||
* WARN:
|
||||
* - You should store only well-known (public) data since it's a central cache.
|
||||
* - The cache does not guard types.
|
||||
*/
|
||||
use<FunctionToMemoize extends AnyAsyncFunction>(
|
||||
tenantId: string,
|
||||
|
@ -26,20 +26,24 @@ class WellKnownCache {
|
|||
) {
|
||||
return pMemoize(run, {
|
||||
cacheKey: () => buildKey(tenantId, key),
|
||||
cache: this.#keyv,
|
||||
// Trust cache value type
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
cache: this.#cache as TtlCache<string, Awaited<ReturnType<FunctionToMemoize>>>,
|
||||
});
|
||||
}
|
||||
|
||||
async invalidate(tenantId: string, keys: readonly WellKnownCacheKey[]) {
|
||||
return this.#keyv.delete(keys.map((key) => buildKey(tenantId, key)));
|
||||
invalidate(tenantId: string, keys: readonly WellKnownCacheKey[]) {
|
||||
for (const key of keys) {
|
||||
this.#cache.delete(buildKey(tenantId, key));
|
||||
}
|
||||
}
|
||||
|
||||
async invalidateAll(tenantId: string) {
|
||||
return this.invalidate(tenantId, cacheKeys);
|
||||
invalidateAll(tenantId: string) {
|
||||
this.invalidate(tenantId, cacheKeys);
|
||||
}
|
||||
|
||||
async set(tenantId: string, key: WellKnownCacheKey, value: unknown) {
|
||||
return this.#keyv.set(buildKey(tenantId, key), value);
|
||||
set(tenantId: string, key: WellKnownCacheKey, value: unknown) {
|
||||
this.#cache.set(buildKey(tenantId, key), value);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -49,8 +49,8 @@ const { getPhrases } = createPhraseLibrary(
|
|||
tenantId
|
||||
);
|
||||
|
||||
afterEach(async () => {
|
||||
await wellKnownCache.invalidateAll(tenantId);
|
||||
afterEach(() => {
|
||||
wellKnownCache.invalidateAll(tenantId);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
|
|
|
@ -146,7 +146,7 @@ export default function initOidc(
|
|||
},
|
||||
},
|
||||
interactions: {
|
||||
url: async (ctx, interaction) => {
|
||||
url: (ctx, interaction) => {
|
||||
const isDemoApp = interaction.params.client_id === demoAppApplicationId;
|
||||
|
||||
const appendParameters = (path: string) => {
|
||||
|
@ -158,7 +158,7 @@ export default function initOidc(
|
|||
case 'login': {
|
||||
// Always fetch the latest sign-in experience config for demo app (live preview)
|
||||
if (isDemoApp) {
|
||||
await wellKnownCache.invalidate(tenantId, ['sie', 'sie-full']);
|
||||
wellKnownCache.invalidate(tenantId, ['sie', 'sie-full']);
|
||||
}
|
||||
|
||||
const isSignUp =
|
||||
|
|
|
@ -195,7 +195,7 @@ export default async function submitInteraction(
|
|||
|
||||
// Normally we don't need to manually invalidate TTL cache.
|
||||
// This is for better OSS onboarding experience.
|
||||
await wellKnownCache.invalidate(tenantId, ['sie', 'sie-full']);
|
||||
wellKnownCache.invalidate(tenantId, ['sie', 'sie-full']);
|
||||
}
|
||||
|
||||
await assignInteractionResults(ctx, provider, { login: { accountId: id } });
|
||||
|
|
|
@ -17,7 +17,7 @@ export default function koaInteractionSie<StateT, ContextT, ResponseT>(
|
|||
): MiddlewareType<StateT, WithInteractionSieContext<ContextT>, ResponseT> {
|
||||
return async (ctx, next) => {
|
||||
if (noCache(ctx.headers)) {
|
||||
await wellKnownCache.invalidate(tenantId, ['sie']);
|
||||
wellKnownCache.invalidate(tenantId, ['sie']);
|
||||
}
|
||||
|
||||
const signInExperience = await getSignInExperience();
|
||||
|
|
|
@ -41,8 +41,8 @@ const phraseRequest = createRequester({
|
|||
tenantContext,
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await wellKnownCache.invalidateAll(tenantContext.id);
|
||||
afterEach(() => {
|
||||
wellKnownCache.invalidateAll(tenantContext.id);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
|
|
|
@ -59,8 +59,8 @@ const phraseRequest = createRequester({
|
|||
tenantContext,
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await wellKnownCache.invalidateAll(tenantContext.id);
|
||||
afterEach(() => {
|
||||
wellKnownCache.invalidateAll(tenantContext.id);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
|
|
|
@ -55,8 +55,8 @@ const tenantContext = new MockTenant(
|
|||
);
|
||||
|
||||
describe('GET /.well-known/sign-in-exp', () => {
|
||||
afterEach(async () => {
|
||||
await wellKnownCache.invalidateAll(tenantContext.id);
|
||||
afterEach(() => {
|
||||
wellKnownCache.invalidateAll(tenantContext.id);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ export default function wellKnownRoutes<T extends AnonymousRouter>(
|
|||
koaGuard({ response: guardFullSignInExperience, status: 200 }),
|
||||
async (ctx, next) => {
|
||||
if (noCache(ctx.headers)) {
|
||||
await wellKnownCache.invalidate(tenantId, ['sie', 'sie-full']);
|
||||
wellKnownCache.invalidate(tenantId, ['sie', 'sie-full']);
|
||||
}
|
||||
|
||||
ctx.body = await getFullSignInExperience();
|
||||
|
@ -60,7 +60,7 @@ export default function wellKnownRoutes<T extends AnonymousRouter>(
|
|||
}),
|
||||
async (ctx, next) => {
|
||||
if (noCache(ctx.headers)) {
|
||||
await wellKnownCache.invalidate(tenantId, ['sie', 'phrases-lng-tags', 'phrases']);
|
||||
wellKnownCache.invalidate(tenantId, ['sie', 'phrases-lng-tags', 'phrases']);
|
||||
}
|
||||
|
||||
const {
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
"@logto/schemas": "workspace:*",
|
||||
"@peculiar/webcrypto": "^1.3.3",
|
||||
"@silverhand/eslint-config": "2.0.1",
|
||||
"@silverhand/essentials": "^2.4.1",
|
||||
"@silverhand/essentials": "2.4.1",
|
||||
"@silverhand/ts-config": "2.0.3",
|
||||
"@types/expect-puppeteer": "^5.0.3",
|
||||
"@types/jest": "^29.4.0",
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import type { SignInExperience } from '@logto/schemas';
|
||||
|
||||
import { adminTenantApi } from '#src/api/api.js';
|
||||
import { adminTenantApi, authedAdminApi } from '#src/api/api.js';
|
||||
import { api } from '#src/api/index.js';
|
||||
import { generateUserId } from '#src/utils.js';
|
||||
|
||||
describe('.well-known api', () => {
|
||||
it('get /.well-known/sign-in-exp for console', async () => {
|
||||
|
@ -33,4 +34,19 @@ describe('.well-known api', () => {
|
|||
// Should support sign-in and register
|
||||
expect(response).toMatchObject({ signInMode: 'SignInAndRegister' });
|
||||
});
|
||||
|
||||
it('should use cached version if no-cache header is not present', async () => {
|
||||
const response1 = await api.get('.well-known/sign-in-exp').json<SignInExperience>();
|
||||
|
||||
const randomId = generateUserId();
|
||||
const customContent = { foo: randomId };
|
||||
await authedAdminApi.patch('sign-in-exp', { json: { customContent } }).json<SignInExperience>();
|
||||
|
||||
const response2 = await api
|
||||
.get('.well-known/sign-in-exp', { headers: { 'cache-control': '' } })
|
||||
.json<SignInExperience>();
|
||||
|
||||
expect(response2.customContent.foo).not.toBe(randomId);
|
||||
expect(response2).toStrictEqual(response1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@logto/language-kit": "workspace:*",
|
||||
"@silverhand/essentials": "^2.4.1",
|
||||
"@silverhand/essentials": "2.4.1",
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@logto/language-kit": "workspace:*",
|
||||
"@silverhand/essentials": "^2.4.1",
|
||||
"@silverhand/essentials": "2.4.1",
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@silverhand/eslint-config": "2.0.1",
|
||||
"@silverhand/essentials": "^2.4.1",
|
||||
"@silverhand/essentials": "2.4.1",
|
||||
"@silverhand/ts-config": "2.0.3",
|
||||
"@types/inquirer": "^9.0.0",
|
||||
"@types/jest": "^29.4.0",
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
"dependencies": {
|
||||
"@logto/core-kit": "workspace:*",
|
||||
"@logto/schemas": "workspace:*",
|
||||
"@silverhand/essentials": "^2.4.1",
|
||||
"@silverhand/essentials": "2.4.1",
|
||||
"chalk": "^5.0.0",
|
||||
"find-up": "^6.3.0",
|
||||
"nanoid": "^4.0.0",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export * from './function.js';
|
||||
export * from './object.js';
|
||||
export { default as findPackage } from './find-package.js';
|
||||
export * from './ttl-cache.js';
|
||||
|
|
84
packages/shared/src/utils/ttl-cache.test.ts
Normal file
84
packages/shared/src/utils/ttl-cache.test.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
import { TtlCache } from './ttl-cache.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
describe('TtlCache', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should return cached value after a long time if ttl is not set', () => {
|
||||
jest.setSystemTime(0);
|
||||
|
||||
const cache = new TtlCache();
|
||||
const someObject = Object.freeze({ foo: 'bar', baz: 123 });
|
||||
|
||||
cache.set('foo', someObject);
|
||||
|
||||
jest.setSystemTime(100_000_000);
|
||||
expect(cache.get('foo')).toBe(someObject);
|
||||
expect(cache.has('foo')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return cached value and honor ttl', () => {
|
||||
jest.setSystemTime(0);
|
||||
|
||||
const cache = new TtlCache(100);
|
||||
const someObject = Object.freeze({ foo: 'bar', baz: 123 });
|
||||
|
||||
cache.set(123, someObject);
|
||||
cache.set('foo', someObject, 99);
|
||||
|
||||
jest.setSystemTime(100);
|
||||
expect(cache.get(123)).toBe(someObject);
|
||||
expect(cache.has(123)).toBe(true);
|
||||
expect(cache.get('123')).toBe(undefined);
|
||||
expect(cache.has('123')).toBe(false);
|
||||
expect(cache.get('foo')).toBe(undefined);
|
||||
expect(cache.has('foo')).toBe(false);
|
||||
|
||||
jest.setSystemTime(101);
|
||||
expect(cache.get(123)).toBe(undefined);
|
||||
expect(cache.has(123)).toBe(false);
|
||||
});
|
||||
|
||||
it('should be able to delete value before ttl', () => {
|
||||
const cache = new TtlCache(100);
|
||||
const someObject = Object.freeze({ foo: 'bar', baz: 123 });
|
||||
|
||||
cache.set('foo', someObject);
|
||||
cache.delete('foo');
|
||||
cache.delete('bar');
|
||||
|
||||
expect(cache.get('foo')).toBe(undefined);
|
||||
expect(cache.has('foo')).toBe(false);
|
||||
});
|
||||
|
||||
it('should be able to clear all values', () => {
|
||||
const cache = new TtlCache(100);
|
||||
const someObject = Object.freeze({ foo: 'bar', baz: 123 });
|
||||
|
||||
cache.set('foo', someObject);
|
||||
cache.set('bar', someObject);
|
||||
cache.set(123, 456);
|
||||
cache.clear();
|
||||
|
||||
expect(cache.get('foo')).toBe(undefined);
|
||||
expect(cache.has('foo')).toBe(false);
|
||||
expect(cache.get('bar')).toBe(undefined);
|
||||
expect(cache.has('bar')).toBe(false);
|
||||
expect(cache.get(123)).toBe(undefined);
|
||||
expect(cache.has(123)).toBe(false);
|
||||
});
|
||||
|
||||
it('should throw undefined when value is undefined', () => {
|
||||
const cache = new TtlCache();
|
||||
expect(() => {
|
||||
cache.set(1, undefined);
|
||||
}).toThrow(TypeError);
|
||||
});
|
||||
});
|
46
packages/shared/src/utils/ttl-cache.ts
Normal file
46
packages/shared/src/utils/ttl-cache.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
export class TtlCache<Key, Value> {
|
||||
data = new Map<Key, Value>();
|
||||
expiration = new Map<Key, number>();
|
||||
|
||||
constructor(public readonly ttl = Number.POSITIVE_INFINITY) {}
|
||||
|
||||
#purge(key: Key) {
|
||||
const expiration = this.expiration.get(key);
|
||||
|
||||
if (expiration !== undefined && expiration < Date.now()) {
|
||||
this.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
set(key: Key, value: Value, ttl = this.ttl) {
|
||||
if (value === undefined) {
|
||||
throw new TypeError('Value cannot be undefined');
|
||||
}
|
||||
|
||||
this.expiration.set(key, Date.now() + ttl);
|
||||
this.data.set(key, value);
|
||||
}
|
||||
|
||||
get(key: Key): Value | undefined {
|
||||
this.#purge(key);
|
||||
|
||||
return this.data.get(key);
|
||||
}
|
||||
|
||||
has(key: Key) {
|
||||
this.#purge(key);
|
||||
|
||||
return this.data.has(key);
|
||||
}
|
||||
|
||||
delete(key: Key) {
|
||||
this.expiration.delete(key);
|
||||
|
||||
return this.data.delete(key);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.expiration.clear();
|
||||
this.data.clear();
|
||||
}
|
||||
}
|
|
@ -33,7 +33,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@logto/language-kit": "workspace:*",
|
||||
"@silverhand/essentials": "^2.4.1"
|
||||
"@silverhand/essentials": "2.4.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"zod": "^3.20.2"
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
"@jest/types": "^29.0.3",
|
||||
"@silverhand/eslint-config": "2.0.1",
|
||||
"@silverhand/eslint-config-react": "2.0.1",
|
||||
"@silverhand/essentials": "^2.4.1",
|
||||
"@silverhand/essentials": "2.4.1",
|
||||
"@silverhand/ts-config": "2.0.3",
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/jest": "^29.4.0",
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
"@react-spring/web": "^9.6.1",
|
||||
"@silverhand/eslint-config": "2.0.1",
|
||||
"@silverhand/eslint-config-react": "2.0.1",
|
||||
"@silverhand/essentials": "^2.4.1",
|
||||
"@silverhand/essentials": "2.4.1",
|
||||
"@silverhand/jest-config": "1.2.2",
|
||||
"@silverhand/ts-config": "2.0.3",
|
||||
"@silverhand/ts-config-react": "2.0.3",
|
||||
|
|
|
@ -32,7 +32,7 @@ importers:
|
|||
'@logto/schemas': workspace:*
|
||||
'@logto/shared': workspace:*
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
'@silverhand/essentials': ^2.4.1
|
||||
'@silverhand/essentials': 2.4.1
|
||||
'@silverhand/ts-config': 2.0.3
|
||||
'@types/inquirer': ^9.0.0
|
||||
'@types/jest': ^29.4.0
|
||||
|
@ -116,7 +116,7 @@ importers:
|
|||
'@logto/schemas': workspace:*
|
||||
'@logto/shared': workspace:*
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
'@silverhand/essentials': ^2.4.1
|
||||
'@silverhand/essentials': 2.4.1
|
||||
'@silverhand/jest-config': ^2.0.1
|
||||
'@silverhand/ts-config': 2.0.3
|
||||
'@types/accepts': ^1.3.5
|
||||
|
@ -196,7 +196,7 @@ importers:
|
|||
'@parcel/transformer-svg-react': 2.8.3
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
'@silverhand/eslint-config-react': 2.0.1
|
||||
'@silverhand/essentials': ^2.4.1
|
||||
'@silverhand/essentials': 2.4.1
|
||||
'@silverhand/ts-config': 2.0.3
|
||||
'@silverhand/ts-config-react': 2.0.3
|
||||
'@tsconfig/docusaurus': ^1.0.5
|
||||
|
@ -346,7 +346,7 @@ importers:
|
|||
'@logto/schemas': workspace:*
|
||||
'@logto/shared': workspace:*
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
'@silverhand/essentials': ^2.4.1
|
||||
'@silverhand/essentials': 2.4.1
|
||||
'@silverhand/ts-config': 2.0.3
|
||||
'@types/debug': ^4.1.7
|
||||
'@types/etag': ^1.8.1
|
||||
|
@ -385,7 +385,6 @@ importers:
|
|||
jest-matcher-specific-error: ^1.0.0
|
||||
jose: ^4.11.0
|
||||
js-yaml: ^4.1.0
|
||||
keyv: ^4.5.2
|
||||
koa: ^2.13.1
|
||||
koa-body: ^5.0.0
|
||||
koa-compose: ^4.1.0
|
||||
|
@ -444,7 +443,6 @@ importers:
|
|||
iconv-lite: 0.6.3
|
||||
jose: 4.11.0
|
||||
js-yaml: 4.1.0
|
||||
keyv: 4.5.2
|
||||
koa: 2.13.4
|
||||
koa-body: 5.0.0
|
||||
koa-compose: 4.1.0
|
||||
|
@ -577,7 +575,7 @@ importers:
|
|||
'@logto/schemas': workspace:*
|
||||
'@peculiar/webcrypto': ^1.3.3
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
'@silverhand/essentials': ^2.4.1
|
||||
'@silverhand/essentials': 2.4.1
|
||||
'@silverhand/ts-config': 2.0.3
|
||||
'@types/expect-puppeteer': ^5.0.3
|
||||
'@types/jest': ^29.4.0
|
||||
|
@ -629,7 +627,7 @@ importers:
|
|||
specifiers:
|
||||
'@logto/language-kit': workspace:*
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
'@silverhand/essentials': ^2.4.1
|
||||
'@silverhand/essentials': 2.4.1
|
||||
'@silverhand/ts-config': 2.0.3
|
||||
eslint: ^8.34.0
|
||||
lint-staged: ^13.0.0
|
||||
|
@ -652,7 +650,7 @@ importers:
|
|||
specifiers:
|
||||
'@logto/language-kit': workspace:*
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
'@silverhand/essentials': ^2.4.1
|
||||
'@silverhand/essentials': 2.4.1
|
||||
'@silverhand/ts-config': 2.0.3
|
||||
buffer: ^5.7.1
|
||||
eslint: ^8.34.0
|
||||
|
@ -681,7 +679,7 @@ importers:
|
|||
'@logto/phrases': workspace:*
|
||||
'@logto/phrases-ui': workspace:*
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
'@silverhand/essentials': ^2.4.1
|
||||
'@silverhand/essentials': 2.4.1
|
||||
'@silverhand/ts-config': 2.0.3
|
||||
'@types/inquirer': ^9.0.0
|
||||
'@types/jest': ^29.4.0
|
||||
|
@ -734,7 +732,7 @@ importers:
|
|||
'@logto/core-kit': workspace:*
|
||||
'@logto/schemas': workspace:*
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
'@silverhand/essentials': ^2.4.1
|
||||
'@silverhand/essentials': 2.4.1
|
||||
'@silverhand/ts-config': 2.0.3
|
||||
'@types/jest': ^29.4.0
|
||||
'@types/node': ^18.11.18
|
||||
|
@ -771,7 +769,7 @@ importers:
|
|||
specifiers:
|
||||
'@logto/language-kit': workspace:*
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
'@silverhand/essentials': ^2.4.1
|
||||
'@silverhand/essentials': 2.4.1
|
||||
'@silverhand/ts-config': 2.0.3
|
||||
'@types/node': ^18.11.18
|
||||
eslint: ^8.34.0
|
||||
|
@ -801,7 +799,7 @@ importers:
|
|||
'@logto/language-kit': workspace:*
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
'@silverhand/eslint-config-react': 2.0.1
|
||||
'@silverhand/essentials': ^2.4.1
|
||||
'@silverhand/essentials': 2.4.1
|
||||
'@silverhand/ts-config': 2.0.3
|
||||
'@types/color': ^3.0.3
|
||||
'@types/jest': ^29.4.0
|
||||
|
@ -890,7 +888,7 @@ importers:
|
|||
'@react-spring/web': ^9.6.1
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
'@silverhand/eslint-config-react': 2.0.1
|
||||
'@silverhand/essentials': ^2.4.1
|
||||
'@silverhand/essentials': 2.4.1
|
||||
'@silverhand/jest-config': 1.2.2
|
||||
'@silverhand/ts-config': 2.0.3
|
||||
'@silverhand/ts-config-react': 2.0.3
|
||||
|
|
Loading…
Reference in a new issue