mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor: remove demo app db entity (#3165)
This commit is contained in:
parent
dfa17ba555
commit
e3f88f5250
12 changed files with 104 additions and 92 deletions
|
@ -4,7 +4,6 @@ import path from 'path';
|
||||||
import {
|
import {
|
||||||
defaultSignInExperience,
|
defaultSignInExperience,
|
||||||
createDefaultAdminConsoleConfig,
|
createDefaultAdminConsoleConfig,
|
||||||
createDemoAppApplication,
|
|
||||||
defaultTenantId,
|
defaultTenantId,
|
||||||
adminTenantId,
|
adminTenantId,
|
||||||
defaultManagementApi,
|
defaultManagementApi,
|
||||||
|
@ -125,8 +124,6 @@ export const seedTables = async (
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
connection.query(insertInto(createDefaultAdminConsoleConfig(defaultTenantId), 'logto_configs')),
|
connection.query(insertInto(createDefaultAdminConsoleConfig(defaultTenantId), 'logto_configs')),
|
||||||
connection.query(insertInto(defaultSignInExperience, 'sign_in_experiences')),
|
connection.query(insertInto(defaultSignInExperience, 'sign_in_experiences')),
|
||||||
// TODO: @gao remove demo app
|
|
||||||
connection.query(insertInto(createDemoAppApplication(defaultTenantId), 'applications')),
|
|
||||||
updateDatabaseTimestamp(connection, latestTimestamp),
|
updateDatabaseTimestamp(connection, latestTimestamp),
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import type { AdminConsoleKey } from '@logto/phrases';
|
import type { AdminConsoleKey } from '@logto/phrases';
|
||||||
import type { Application } from '@logto/schemas';
|
import { AppearanceMode } from '@logto/schemas';
|
||||||
import { AppearanceMode, demoAppApplicationId } from '@logto/schemas';
|
|
||||||
import { useContext, useMemo } from 'react';
|
import { useContext, useMemo } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import useSWR from 'swr';
|
|
||||||
|
|
||||||
import CheckDemoDark from '@/assets/images/check-demo-dark.svg';
|
import CheckDemoDark from '@/assets/images/check-demo-dark.svg';
|
||||||
import CheckDemo from '@/assets/images/check-demo.svg';
|
import CheckDemo from '@/assets/images/check-demo.svg';
|
||||||
|
@ -19,7 +17,6 @@ import SocialDark from '@/assets/images/social-dark.svg';
|
||||||
import Social from '@/assets/images/social.svg';
|
import Social from '@/assets/images/social.svg';
|
||||||
import { ConnectorsTabs } from '@/consts/page-tabs';
|
import { ConnectorsTabs } from '@/consts/page-tabs';
|
||||||
import { AppEndpointsContext } from '@/containers/AppEndpointsProvider';
|
import { AppEndpointsContext } from '@/containers/AppEndpointsProvider';
|
||||||
import { RequestError } from '@/hooks/use-api';
|
|
||||||
import useConfigs from '@/hooks/use-configs';
|
import useConfigs from '@/hooks/use-configs';
|
||||||
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
||||||
import { useTheme } from '@/hooks/use-theme';
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
|
@ -41,21 +38,7 @@ const useGetStartedMetadata = () => {
|
||||||
const { userEndpoint } = useContext(AppEndpointsContext);
|
const { userEndpoint } = useContext(AppEndpointsContext);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isLightMode = theme === AppearanceMode.LightMode;
|
const isLightMode = theme === AppearanceMode.LightMode;
|
||||||
const { data: demoApp, error } = useSWR<Application, RequestError>(
|
|
||||||
`api/applications/${demoAppApplicationId}`,
|
|
||||||
{
|
|
||||||
shouldRetryOnError: (error: unknown) => {
|
|
||||||
if (error instanceof RequestError) {
|
|
||||||
return error.status !== 404;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const isLoadingDemoApp = !demoApp && !error;
|
|
||||||
const hideDemo = error?.status === 404;
|
|
||||||
|
|
||||||
const data = useMemo(() => {
|
const data = useMemo(() => {
|
||||||
const metadataItems: GetStartedMetadata[] = [
|
const metadataItems: GetStartedMetadata[] = [
|
||||||
|
@ -66,7 +49,6 @@ const useGetStartedMetadata = () => {
|
||||||
icon: isLightMode ? CheckDemo : CheckDemoDark,
|
icon: isLightMode ? CheckDemo : CheckDemoDark,
|
||||||
buttonText: 'general.check_out',
|
buttonText: 'general.check_out',
|
||||||
isComplete: configs?.demoChecked,
|
isComplete: configs?.demoChecked,
|
||||||
isHidden: hideDemo,
|
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
void updateConfigs({ demoChecked: true });
|
void updateConfigs({ demoChecked: true });
|
||||||
window.open(new URL('/demo-app', userEndpoint), '_blank');
|
window.open(new URL('/demo-app', userEndpoint), '_blank');
|
||||||
|
@ -142,7 +124,6 @@ const useGetStartedMetadata = () => {
|
||||||
configs?.passwordlessConfigured,
|
configs?.passwordlessConfigured,
|
||||||
configs?.socialSignInConfigured,
|
configs?.socialSignInConfigured,
|
||||||
configs?.furtherReadingsChecked,
|
configs?.furtherReadingsChecked,
|
||||||
hideDemo,
|
|
||||||
updateConfigs,
|
updateConfigs,
|
||||||
userEndpoint,
|
userEndpoint,
|
||||||
navigate,
|
navigate,
|
||||||
|
@ -153,7 +134,7 @@ const useGetStartedMetadata = () => {
|
||||||
data,
|
data,
|
||||||
completedCount: data.filter(({ isComplete }) => isComplete).length,
|
completedCount: data.filter(({ isComplete }) => isComplete).length,
|
||||||
totalCount: data.length,
|
totalCount: data.length,
|
||||||
isLoading: isLoadingDemoApp,
|
isLoading: false,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { adminTenantId } from '@logto/schemas';
|
import { adminTenantId } from '@logto/schemas';
|
||||||
import { trySafe } from '@silverhand/essentials';
|
import type { Optional } from '@silverhand/essentials';
|
||||||
|
import { deduplicate, trySafe } from '@silverhand/essentials';
|
||||||
|
|
||||||
import type GlobalValues from './GlobalValues.js';
|
import type GlobalValues from './GlobalValues.js';
|
||||||
|
|
||||||
|
@ -23,3 +24,29 @@ export const getTenantEndpoint = (
|
||||||
|
|
||||||
return tenantUrl;
|
return tenantUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getTenantLocalhost = (
|
||||||
|
id: string,
|
||||||
|
{ urlSet, adminUrlSet, isDomainBasedMultiTenancy }: GlobalValues
|
||||||
|
): Optional<URL> => {
|
||||||
|
const adminUrl = trySafe(() => adminUrlSet.localhostUrl);
|
||||||
|
|
||||||
|
if (adminUrl && id === adminTenantId) {
|
||||||
|
return adminUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDomainBasedMultiTenancy) {
|
||||||
|
return trySafe(() => urlSet.localhostUrl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTenantUrls = (id: string, globalValues: GlobalValues): URL[] => {
|
||||||
|
const endpoint = getTenantEndpoint(id, globalValues);
|
||||||
|
const localhost = getTenantLocalhost(id, globalValues);
|
||||||
|
|
||||||
|
return deduplicate(
|
||||||
|
[endpoint.toString(), localhost?.toString()].filter(
|
||||||
|
(value): value is string => typeof value === 'string'
|
||||||
|
)
|
||||||
|
).map((element) => new URL(element));
|
||||||
|
};
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { demoAppApplicationId } from '@logto/schemas';
|
|
||||||
import type { MiddlewareType } from 'koa';
|
|
||||||
|
|
||||||
import type Queries from '#src/tenants/Queries.js';
|
|
||||||
|
|
||||||
export default function koaCheckDemoApp<StateT, ContextT, ResponseBodyT>(
|
|
||||||
queries: Queries
|
|
||||||
): MiddlewareType<StateT, ContextT, ResponseBodyT> {
|
|
||||||
const { findApplicationById } = queries.applications;
|
|
||||||
|
|
||||||
return async (ctx, next) => {
|
|
||||||
try {
|
|
||||||
await findApplicationById(demoAppApplicationId);
|
|
||||||
|
|
||||||
await next();
|
|
||||||
|
|
||||||
return;
|
|
||||||
} catch {
|
|
||||||
ctx.throw(404);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,13 +1,13 @@
|
||||||
import type { CreateApplication, OidcClientMetadata } from '@logto/schemas';
|
import type { CreateApplication } from '@logto/schemas';
|
||||||
import { ApplicationType, adminConsoleApplicationId, demoAppApplicationId } from '@logto/schemas';
|
import { ApplicationType, adminConsoleApplicationId, demoAppApplicationId } from '@logto/schemas';
|
||||||
import { tryThat } from '@logto/shared';
|
import { tryThat } from '@logto/shared';
|
||||||
import { deduplicate } from '@silverhand/essentials';
|
|
||||||
import { addSeconds } from 'date-fns';
|
import { addSeconds } from 'date-fns';
|
||||||
import type { AdapterFactory, AllClientMetadata } from 'oidc-provider';
|
import type { AdapterFactory, AllClientMetadata } from 'oidc-provider';
|
||||||
import { errors } from 'oidc-provider';
|
import { errors } from 'oidc-provider';
|
||||||
import snakecaseKeys from 'snakecase-keys';
|
import snakecaseKeys from 'snakecase-keys';
|
||||||
|
|
||||||
import { EnvSet, UserApps } from '#src/env-set/index.js';
|
import { EnvSet } from '#src/env-set/index.js';
|
||||||
|
import { getTenantUrls } from '#src/env-set/utils.js';
|
||||||
import type Queries from '#src/tenants/Queries.js';
|
import type Queries from '#src/tenants/Queries.js';
|
||||||
import { appendPath } from '#src/utils/url.js';
|
import { appendPath } from '#src/utils/url.js';
|
||||||
|
|
||||||
|
@ -28,18 +28,18 @@ const buildAdminConsoleClientMetadata = (envSet: EnvSet): AllClientMetadata => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildDemoAppUris = (
|
const buildDemoAppClientMetadata = (envSet: EnvSet): AllClientMetadata => {
|
||||||
oidcClientMetadata: OidcClientMetadata
|
const urls = getTenantUrls(envSet.tenantId, EnvSet.values).map((url) =>
|
||||||
): Pick<OidcClientMetadata, 'redirectUris' | 'postLogoutRedirectUris'> => {
|
appendPath(url, '/demo-app').toString()
|
||||||
const { urlSet } = EnvSet.values;
|
);
|
||||||
const urls = urlSet.deduplicated().map((url) => appendPath(url, UserApps.DemoApp).toString());
|
|
||||||
|
|
||||||
const data = {
|
return {
|
||||||
redirectUris: deduplicate([...urls, ...oidcClientMetadata.redirectUris]),
|
...getConstantClientMetadata(envSet, ApplicationType.SPA),
|
||||||
postLogoutRedirectUris: deduplicate([...urls, ...oidcClientMetadata.postLogoutRedirectUris]),
|
client_id: demoAppApplicationId,
|
||||||
|
client_name: 'Demo App',
|
||||||
|
redirect_uris: urls,
|
||||||
|
post_logout_redirect_uris: urls,
|
||||||
};
|
};
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function postgresAdapter(
|
export default function postgresAdapter(
|
||||||
|
@ -76,8 +76,6 @@ export default function postgresAdapter(
|
||||||
client_name,
|
client_name,
|
||||||
...getConstantClientMetadata(envSet, type),
|
...getConstantClientMetadata(envSet, type),
|
||||||
...snakecaseKeys(oidcClientMetadata),
|
...snakecaseKeys(oidcClientMetadata),
|
||||||
...(client_id === demoAppApplicationId &&
|
|
||||||
snakecaseKeys(buildDemoAppUris(oidcClientMetadata))),
|
|
||||||
// `node-oidc-provider` won't camelCase custom parameter keys, so we need to keep the keys camelCased
|
// `node-oidc-provider` won't camelCase custom parameter keys, so we need to keep the keys camelCased
|
||||||
...customClientMetadata,
|
...customClientMetadata,
|
||||||
});
|
});
|
||||||
|
@ -90,6 +88,10 @@ export default function postgresAdapter(
|
||||||
return buildAdminConsoleClientMetadata(envSet);
|
return buildAdminConsoleClientMetadata(envSet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (id === demoAppApplicationId) {
|
||||||
|
return buildDemoAppClientMetadata(envSet);
|
||||||
|
}
|
||||||
|
|
||||||
return transpileClient(
|
return transpileClient(
|
||||||
await tryThat(findApplicationById(id), new errors.InvalidClient(`invalid client ${id}`))
|
await tryThat(findApplicationById(id), new errors.InvalidClient(`invalid client ${id}`))
|
||||||
);
|
);
|
||||||
|
|
|
@ -25,7 +25,6 @@ const middlewareList = Object.freeze(
|
||||||
'oidc-error-handler',
|
'oidc-error-handler',
|
||||||
'slonik-error-handler',
|
'slonik-error-handler',
|
||||||
'spa-proxy',
|
'spa-proxy',
|
||||||
'check-demo-app',
|
|
||||||
'console-redirect-proxy',
|
'console-redirect-proxy',
|
||||||
].map((name) => [name, buildMockMiddleware(name)] as const)
|
].map((name) => [name, buildMockMiddleware(name)] as const)
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,7 +7,6 @@ import mount from 'koa-mount';
|
||||||
import type Provider from 'oidc-provider';
|
import type Provider from 'oidc-provider';
|
||||||
|
|
||||||
import { AdminApps, EnvSet, UserApps } from '#src/env-set/index.js';
|
import { AdminApps, EnvSet, UserApps } from '#src/env-set/index.js';
|
||||||
import koaCheckDemoApp from '#src/middleware/koa-check-demo-app.js';
|
|
||||||
import koaConnectorErrorHandler from '#src/middleware/koa-connector-error-handler.js';
|
import koaConnectorErrorHandler from '#src/middleware/koa-connector-error-handler.js';
|
||||||
import koaConsoleRedirectProxy from '#src/middleware/koa-console-redirect-proxy.js';
|
import koaConsoleRedirectProxy from '#src/middleware/koa-console-redirect-proxy.js';
|
||||||
import koaErrorHandler from '#src/middleware/koa-error-handler.js';
|
import koaErrorHandler from '#src/middleware/koa-error-handler.js';
|
||||||
|
@ -97,10 +96,7 @@ export default class Tenant implements TenantContext {
|
||||||
app.use(
|
app.use(
|
||||||
mount(
|
mount(
|
||||||
'/' + UserApps.DemoApp,
|
'/' + UserApps.DemoApp,
|
||||||
compose([
|
koaSpaProxy(mountedApps, UserApps.DemoApp, 5003, UserApps.DemoApp)
|
||||||
koaCheckDemoApp(this.queries),
|
|
||||||
koaSpaProxy(mountedApps, UserApps.DemoApp, 5003, UserApps.DemoApp),
|
|
||||||
])
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ApplicationType, demoAppApplicationId } from '@logto/schemas';
|
import { ApplicationType } from '@logto/schemas';
|
||||||
import { HTTPError } from 'got';
|
import { HTTPError } from 'got';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -9,12 +9,6 @@ import {
|
||||||
} from '#src/api/index.js';
|
} from '#src/api/index.js';
|
||||||
|
|
||||||
describe('admin console application', () => {
|
describe('admin console application', () => {
|
||||||
it('should get demo app details successfully', async () => {
|
|
||||||
const demoApp = await getApplication(demoAppApplicationId);
|
|
||||||
|
|
||||||
expect(demoApp.id).toBe(demoAppApplicationId);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create application successfully', async () => {
|
it('should create application successfully', async () => {
|
||||||
const applicationName = 'test-create-app';
|
const applicationName = 'test-create-app';
|
||||||
const applicationType = ApplicationType.SPA;
|
const applicationType = ApplicationType.SPA;
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { generateStandardId } from '@logto/core-kit';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import inquirer from 'inquirer';
|
||||||
|
import { sql } from 'slonik';
|
||||||
|
|
||||||
|
import type { AlterationScript } from '../lib/types/alteration.js';
|
||||||
|
|
||||||
|
const defaultTenantId = 'default';
|
||||||
|
|
||||||
|
const alteration: AlterationScript = {
|
||||||
|
up: async (pool) => {
|
||||||
|
const isCi = process.env.CI;
|
||||||
|
const { confirm } = await inquirer.prompt<{ confirm: boolean }>({
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'confirm',
|
||||||
|
message: String(
|
||||||
|
chalk.bold(chalk.yellow('***CAUTION***')) +
|
||||||
|
'\n' +
|
||||||
|
'The application `demo-app` will be removed from your database.\n' +
|
||||||
|
'Usually this is harmless since the demo app will be still functional with predefined data.\n' +
|
||||||
|
'Are you sure to continue?'
|
||||||
|
),
|
||||||
|
default: false,
|
||||||
|
when: !isCi,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isCi && !confirm) {
|
||||||
|
throw new Error('User cancelled alteration.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await pool.query(sql`
|
||||||
|
delete from applications where id = 'demo-app';
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
down: async (pool) => {
|
||||||
|
await pool.query(sql`
|
||||||
|
insert into applications
|
||||||
|
(tenant_id, id, secret, name, description, type, oidc_client_metadata)
|
||||||
|
values (
|
||||||
|
'default',
|
||||||
|
'demo-app',
|
||||||
|
${generateStandardId()},
|
||||||
|
'Demo App',
|
||||||
|
'Logto demo app.',
|
||||||
|
'SPA',
|
||||||
|
'{ "redirectUris": [], "postLogoutRedirectUris": [] }'::jsonb
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default alteration;
|
|
@ -48,6 +48,7 @@
|
||||||
"@types/node": "^18.11.18",
|
"@types/node": "^18.11.18",
|
||||||
"@types/pluralize": "^0.0.29",
|
"@types/pluralize": "^0.0.29",
|
||||||
"camelcase": "^7.0.0",
|
"camelcase": "^7.0.0",
|
||||||
|
"chalk": "^5.0.0",
|
||||||
"eslint": "^8.34.0",
|
"eslint": "^8.34.0",
|
||||||
"jest": "^29.1.2",
|
"jest": "^29.1.2",
|
||||||
"lint-staged": "^13.0.0",
|
"lint-staged": "^13.0.0",
|
||||||
|
|
|
@ -1,8 +1,3 @@
|
||||||
import { generateStandardId } from '@logto/core-kit';
|
|
||||||
|
|
||||||
import type { CreateApplication } from '../db-entries/index.js';
|
|
||||||
import { ApplicationType } from '../db-entries/index.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The fixed application ID for Admin Console.
|
* The fixed application ID for Admin Console.
|
||||||
*
|
*
|
||||||
|
@ -11,14 +6,3 @@ import { ApplicationType } from '../db-entries/index.js';
|
||||||
export const adminConsoleApplicationId = 'admin-console';
|
export const adminConsoleApplicationId = 'admin-console';
|
||||||
|
|
||||||
export const demoAppApplicationId = 'demo-app';
|
export const demoAppApplicationId = 'demo-app';
|
||||||
|
|
||||||
/** @deprecated Demo app database entity will be removed soon. */
|
|
||||||
export const createDemoAppApplication = (forTenantId: string): Readonly<CreateApplication> => ({
|
|
||||||
tenantId: forTenantId,
|
|
||||||
id: demoAppApplicationId,
|
|
||||||
secret: generateStandardId(),
|
|
||||||
name: 'Demo App',
|
|
||||||
description: 'Logto demo app.',
|
|
||||||
type: ApplicationType.SPA,
|
|
||||||
oidcClientMetadata: { redirectUris: [], postLogoutRedirectUris: [] },
|
|
||||||
});
|
|
||||||
|
|
|
@ -653,6 +653,7 @@ importers:
|
||||||
'@types/pluralize': ^0.0.29
|
'@types/pluralize': ^0.0.29
|
||||||
'@withtyped/server': ^0.8.0
|
'@withtyped/server': ^0.8.0
|
||||||
camelcase: ^7.0.0
|
camelcase: ^7.0.0
|
||||||
|
chalk: ^5.0.0
|
||||||
eslint: ^8.34.0
|
eslint: ^8.34.0
|
||||||
jest: ^29.1.2
|
jest: ^29.1.2
|
||||||
lint-staged: ^13.0.0
|
lint-staged: ^13.0.0
|
||||||
|
@ -680,6 +681,7 @@ importers:
|
||||||
'@types/node': 18.11.18
|
'@types/node': 18.11.18
|
||||||
'@types/pluralize': 0.0.29
|
'@types/pluralize': 0.0.29
|
||||||
camelcase: 7.0.0
|
camelcase: 7.0.0
|
||||||
|
chalk: 5.1.2
|
||||||
eslint: 8.34.0
|
eslint: 8.34.0
|
||||||
jest: 29.1.2_@types+node@18.11.18
|
jest: 29.1.2_@types+node@18.11.18
|
||||||
lint-staged: 13.0.0
|
lint-staged: 13.0.0
|
||||||
|
@ -5283,7 +5285,6 @@ packages:
|
||||||
/chalk/5.1.2:
|
/chalk/5.1.2:
|
||||||
resolution: {integrity: sha512-E5CkT4jWURs1Vy5qGJye+XwCkNj7Od3Af7CP6SujMetSMkLs8Do2RWJK5yx1wamHV/op8Rz+9rltjaTQWDnEFQ==}
|
resolution: {integrity: sha512-E5CkT4jWURs1Vy5qGJye+XwCkNj7Od3Af7CP6SujMetSMkLs8Do2RWJK5yx1wamHV/op8Rz+9rltjaTQWDnEFQ==}
|
||||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/char-regex/1.0.2:
|
/char-regex/1.0.2:
|
||||||
resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
|
resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
|
||||||
|
|
Loading…
Reference in a new issue