0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

refactor(console): refactor the code editor type definition (#5516)

* refactor(console): refactor the code editor type definition

refactor the code editor type definition

* refactor(console): extract type definition gen process

extract the type definition gen step to the build time. As typescript is not available at run time.

* fix(console): add generate to console build script

add generate to console build script

* fix(console): add prettier format

add prettier format
This commit is contained in:
simeng-li 2024-03-19 12:50:07 +08:00 committed by GitHub
parent 23bc5cdc8e
commit 08acdf73e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 251 additions and 62 deletions

3
.gitignore vendored
View file

@ -35,3 +35,6 @@ dump.rdb
# connectors
/packages/core/connectors
# console auto generated files
/packages/console/src/consts/jwt-customizer-type-definition.ts

12
packages/console/generate.sh Executable file
View file

@ -0,0 +1,12 @@
#!/bin/sh
# Clean up
rm -rf scripts-js/
# build the jwt-customizer-type-definition generate script
pnpm exec tsc -p tsconfig.scripts.gen.json
# clean up the existing generated jwt-customizer-type-definition file
rm -f src/consts/jwt-customizer-type-definition.ts
# run script
node scripts-js/generate-jwt-customizer-type-definition.js
# Clean up
rm -rf scripts-js/

View file

@ -11,11 +11,13 @@
"dist"
],
"scripts": {
"prepack": "pnpm generate",
"generate": "./generate.sh",
"precommit": "lint-staged",
"start": "parcel src/index.html",
"dev": "cross-env PORT=5002 parcel src/index.html --public-url ${CONSOLE_PUBLIC_URL:-/console} --no-cache --hmr-port 6002",
"check": "tsc --noEmit",
"build": "pnpm check && rm -rf dist && parcel build src/index.html --no-autoinstall --no-cache --public-url ${CONSOLE_PUBLIC_URL:-/console}",
"build": "pnpm generate && pnpm check && rm -rf dist && parcel build src/index.html --no-autoinstall --no-cache --public-url ${CONSOLE_PUBLIC_URL:-/console}",
"lint": "eslint --ext .ts --ext .tsx src",
"lint:report": "pnpm lint --format json --output-file report.json",
"stylelint": "stylelint \"src/**/*.scss\"",
@ -121,7 +123,8 @@
"ts-node": "^10.9.2",
"tslib": "^2.4.1",
"typescript": "^5.3.3",
"zod": "^3.22.4"
"zod": "^3.22.4",
"zod-to-ts": "^1.2.0"
},
"engines": {
"node": "^20.9.0"
@ -141,6 +144,12 @@
},
"eslintConfig": {
"extends": "@silverhand/react",
"parserOptions": {
"project": [
"./tsconfig.json",
"./tsconfig.scripts.gen.json"
]
},
"rules": {
"react/function-component-definition": [
"error",

View file

@ -0,0 +1,64 @@
import fs from 'node:fs';
import {
jwtCustomizerUserContextGuard,
accessTokenPayloadGuard,
clientCredentialsPayloadGuard,
} from '@logto/schemas';
import prettier from 'prettier';
import { type ZodTypeAny } from 'zod';
import { zodToTs, printNode } from 'zod-to-ts';
const filePath = 'src/consts/jwt-customizer-type-definition.ts';
const typeIdentifiers = `export enum JwtCustomizerTypeDefinitionKey {
JwtCustomizerUserContext = 'JwtCustomizerUserContext',
AccessTokenPayload = 'AccessTokenPayload',
ClientCredentialsPayload = 'ClientCredentialsPayload',
EnvironmentVariables = 'EnvironmentVariables',
};`;
const inferTsDefinitionFromZod = (zodSchema: ZodTypeAny, identifier: string): string => {
const { node } = zodToTs(zodSchema, identifier);
const typeDefinition = printNode(node);
return `type ${identifier} = ${typeDefinition};`;
};
// Create the jwt-customizer-type-definition.ts file
const createJwtCustomizerTypeDefinitions = async () => {
const jwtCustomizerUserContextTypeDefinition = inferTsDefinitionFromZod(
jwtCustomizerUserContextGuard,
'JwtCustomizerUserContext'
);
const accessTokenPayloadTypeDefinition = inferTsDefinitionFromZod(
accessTokenPayloadGuard,
'AccessTokenPayload'
);
const clientCredentialsPayloadTypeDefinition = inferTsDefinitionFromZod(
clientCredentialsPayloadGuard,
'ClientCredentialsPayload'
);
const fileContent = `/* This file is auto-generated. Do not modify it manually. */
${typeIdentifiers}
export const jwtCustomizerUserContextTypeDefinition = \`${jwtCustomizerUserContextTypeDefinition}\`;
export const accessTokenPayloadTypeDefinition = \`${accessTokenPayloadTypeDefinition}\`;
export const clientCredentialsPayloadTypeDefinition = \`${clientCredentialsPayloadTypeDefinition}\`;
`;
const formattedFileContent = await prettier.format(fileContent, {
parser: 'typescript',
tabWidth: 2,
singleQuote: true,
});
fs.writeFileSync(filePath, formattedFileContent);
};
void createJwtCustomizerTypeDefinitions();

View file

@ -17,7 +17,11 @@ import ScriptSection from './ScriptSection';
import SettingsSection from './SettingsSection';
import * as styles from './index.module.scss';
import { type JwtClaimsFormType } from './type';
import { formatResponseDataToFormData, formatFormDataToRequestData, getApiPath } from './utils';
import {
formatResponseDataToFormData,
formatFormDataToRequestData,
getApiPath,
} from './utils/format';
type Props = {
className?: string;
@ -100,7 +104,7 @@ function Main({
onDiscard={reset}
onSubmit={onSubmitHandler}
/>
<UnsavedChangesAlertModal hasUnsavedChanges={isDirty && !isSubmitting} />
<UnsavedChangesAlertModal hasUnsavedChanges={isDirty && !isSubmitting} onConfirm={reset} />
</>
);
}

View file

@ -24,6 +24,7 @@ type Props = {
activeModelName?: string;
setActiveModel?: (name: string) => void;
value?: string;
environmentVariablesDefinition?: string;
onChange?: (value: string | undefined) => void;
onMountHandler?: (editor: IStandaloneCodeEditor) => void;
};
@ -37,6 +38,7 @@ type Props = {
* @param {(name: string) => void} prop.setActiveModel - The callback function to set the active model. Used to switch between tabs.
* @param {string} prop.value - The value of the code editor for the current active model.
* @param {(value: string | undefined) => void} prop.onChange - The callback function to handle the value change of the code editor.
* @param {string} [prop.environmentVariablesDefinition] - The environment variables type definition for the script section.
*
* @returns
*/
@ -46,6 +48,7 @@ function MonacoCodeEditor({
models,
activeModelName,
value,
environmentVariablesDefinition,
setActiveModel,
onChange,
onMountHandler,
@ -71,19 +74,28 @@ function MonacoCodeEditor({
// Set the global declarations for the active model
// @see {@link https://microsoft.github.io/monaco-editor/typedoc/interfaces/languages.typescript.LanguageServiceDefaults.html#setExtraLibs}
if (activeModel.globalDeclarations) {
monaco.languages.typescript.typescriptDefaults.setExtraLibs([
{
content: activeModel.globalDeclarations,
filePath: `file:///global.d.ts`,
},
]);
if (activeModel.extraLibs) {
monaco.languages.typescript.typescriptDefaults.setExtraLibs(activeModel.extraLibs);
}
}, [activeModel, monaco]);
// Set the environment variables type definition for the active model
if (environmentVariablesDefinition) {
monaco.languages.typescript.typescriptDefaults.addExtraLib(
environmentVariablesDefinition,
'environmentVariables.d.ts'
);
}
}, [activeModel, monaco, environmentVariablesDefinition]);
const handleEditorWillMount = useCallback<BeforeMount>((monaco) => {
// Register the new logto theme
monaco.editor.defineTheme('logto-dark', logtoDarkTheme);
// Set the typescript compiler options
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
allowNonTsExtensions: true,
strictNullChecks: true,
});
}, []);
const handleEditorDidMount = useCallback<OnMount>(

View file

@ -4,6 +4,11 @@ export type IStandaloneThemeData = Parameters<Monaco['editor']['defineTheme']>[1
export type IStandaloneCodeEditor = Parameters<OnMount>[0];
type ExtraLibrary = {
content: string;
filePath: string;
};
export type ModelSettings = {
/** Used as the unique key for the monaco editor model @see {@link https://github.com/suren-atoyan/monaco-react?tab=readme-ov-file#multi-model-editor} */
name: string;
@ -19,7 +24,7 @@ export type ModelSettings = {
* @see {@link https://microsoft.github.io/monaco-editor/typedoc/interfaces/languages.typescript.LanguageServiceDefaults.html#setExtraLibs}
* We use this to load the global type declarations for the active model
*/
globalDeclarations?: string;
extraLibs?: ExtraLibrary[];
};
export type ModelControl = {

View file

@ -1,16 +1,17 @@
/* Code Editor for the custom JWT claims script. */
import { LogtoJwtTokenPath } from '@logto/schemas';
import { useMemo, useContext, useCallback } from 'react';
import { useFormContext, Controller } from 'react-hook-form';
import { useFormContext, Controller, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import Card from '@/ds-components/Card';
import { CodeEditorLoadingContext } from './CodeEditorLoadingContext';
import MonacoCodeEditor, { type ModelSettings } from './MonacoCodeEditor';
import { userJwtFile, machineToMachineJwtFile } from './config';
import * as styles from './index.module.scss';
import { type JwtClaimsFormType } from './type';
import { accessTokenJwtCustomizerModel, clientCredentialsModel } from './utils/config';
import { buildEnvironmentVariablesTypeDefinition } from './utils/type-definitions';
const titlePhrases = Object.freeze({
[LogtoJwtTokenPath.AccessToken]: 'user_jwt',
@ -21,12 +22,28 @@ function ScriptSection() {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { watch, control } = useFormContext<JwtClaimsFormType>();
const tokenType = watch('tokenType');
// Need to use useWatch hook to subscribe the mutation of the environmentVariables field
// Otherwise, the default watch function's return value won't mutate when the environmentVariables field changes
const envVariables = useWatch({
control,
name: 'environmentVariables',
});
const environmentVariablesTypeDefinition = useMemo(
() => buildEnvironmentVariablesTypeDefinition(envVariables),
[envVariables]
);
const { setIsMonacoLoaded } = useContext(CodeEditorLoadingContext);
const activeModel = useMemo<ModelSettings>(
() => (tokenType === LogtoJwtTokenPath.AccessToken ? userJwtFile : machineToMachineJwtFile),
() =>
tokenType === LogtoJwtTokenPath.AccessToken
? accessTokenJwtCustomizerModel
: clientCredentialsModel,
[tokenType]
);
@ -54,6 +71,7 @@ function ScriptSection() {
models={[activeModel]}
activeModelName={activeModel.name}
value={value}
environmentVariablesDefinition={environmentVariablesTypeDefinition}
onChange={(newValue) => {
// If the value is the same as the default code and the original form script value is undefined, reset the value to undefined as well
if (newValue === activeModel.defaultValue && !defaultValues?.script) {

View file

@ -7,13 +7,13 @@ import { useTranslation } from 'react-i18next';
import Table from '@/ds-components/Table';
import { type JwtClaimsFormType } from '../type';
import {
userDataDescription,
tokenDataDescription,
fetchExternalDataEditorOptions,
fetchExternalDataCodeExample,
} from '../config';
import { type JwtClaimsFormType } from '../type';
} from '../utils/config';
import EnvironmentVariablesField from './EnvironmentVariablesField';
import GuideCard, { CardType } from './GuideCard';

View file

@ -8,12 +8,12 @@ import Button from '@/ds-components/Button';
import Card from '@/ds-components/Card';
import MonacoCodeEditor, { type ModelControl } from '../MonacoCodeEditor/index.js';
import { type JwtClaimsFormType } from '../type.js';
import {
userTokenPayloadTestModel,
machineToMachineTokenPayloadTestModel,
userTokenContextTestModel,
} from '../config.js';
import { type JwtClaimsFormType } from '../type.js';
} from '../utils/config.js';
import TestResult, { type TestResultData } from './TestResult.js';
import * as styles from './index.module.scss';

View file

@ -11,7 +11,7 @@ import useApi from '@/hooks/use-api';
import useSwrFetcher from '@/hooks/use-swr-fetcher';
import { shouldRetryOnError } from '@/utils/request';
import { getApiPath } from './utils';
import { getApiPath } from './utils/format';
function useJwtCustomizer() {
const fetchApi = useApi({ hideErrorToast: true });

View file

@ -3,52 +3,40 @@ import { type EditorProps } from '@monaco-editor/react';
import TokenFileIcon from '@/assets/icons/token-file-icon.svg';
import UserFileIcon from '@/assets/icons/user-file-icon.svg';
import type { ModelSettings } from './MonacoCodeEditor/type.js';
import type { ModelSettings } from '../MonacoCodeEditor/type.js';
import {
JwtCustomizerTypeDefinitionKey,
buildAccessTokenJwtCustomizerContextTsDefinition,
buildClientCredentialsJwtCustomizerContextTsDefinition,
} from './type-definitions.js';
/**
* JWT token code editor configuration
*/
const userJwtGlobalDeclarations = `
const accessTokenJwtCustomizerDefinition = `
declare global {
export interface CustomJwtClaims extends Record<string, any> {}
/** The user info associated with the token.
*
* @param {string} id - The user id
* @param {string} [primaryEmail] - The user email
* @param {string} [primaryPhone] - The user phone
* @param {string} [username] - The user username
* @param {string} [name] - The user name
* @param {string} [avatar] - The user avatar
*
*/
export type User = {
id: string;
primaryEmail?: string;
primaryPhone?: string;
username?: string;
name?: string;
avatar?: string;
}
/** Logto internal data that can be used to pass additional information
* @param {User} user - The user info associated with the token.
* @param {${JwtCustomizerTypeDefinitionKey.JwtCustomizerUserContext}} user - The user info associated with the token.
*/
export type Data = {
user: User;
user: ${JwtCustomizerTypeDefinitionKey.JwtCustomizerUserContext};
}
export interface Exports {
/**
* This function is called to get custom claims for the JWT token.
*
* @param {string} token -The JWT token.
* @param {${JwtCustomizerTypeDefinitionKey.AccessTokenPayload}} token -The JWT token.
* @param {Data} data - Logto internal data that can be used to pass additional information
* @param {User} data.user - The user info associated with the token.
* @param {${JwtCustomizerTypeDefinitionKey.JwtCustomizerUserContext}} data.user - The user info associated with the token.
* @param {${JwtCustomizerTypeDefinitionKey.EnvironmentVariables}} envVariables - The environment variables.
*
* @returns The custom claims.
*/
getCustomJwtClaims: (token: string, data: Data) => Promise<CustomJwtClaims>;
getCustomJwtClaims: (token: ${JwtCustomizerTypeDefinitionKey.AccessTokenPayload}, data: Data, envVariables: ${JwtCustomizerTypeDefinitionKey.EnvironmentVariables}) => Promise<CustomJwtClaims>;
}
const exports: Exports;
@ -57,7 +45,7 @@ declare global {
export { exports as default };
`;
const machineToMachineJwtGlobalDeclarations = `
const clientCredentialsJwtCustomizerDefinition = `
declare global {
export interface CustomJwtClaims extends Record<string, any> {}
@ -65,11 +53,11 @@ declare global {
/**
* This function is called to get custom claims for the JWT token.
*
* @param {string} token -The JWT token.
* @param {${JwtCustomizerTypeDefinitionKey.ClientCredentialsPayload}} token -The JWT token.
*
* @returns The custom claims.
*/
getCustomJwtClaims: (token: string) => Promise<CustomJwtClaims>;
getCustomJwtClaims: (token: ${JwtCustomizerTypeDefinitionKey.ClientCredentialsPayload}, envVariables: ${JwtCustomizerTypeDefinitionKey.EnvironmentVariables}) => Promise<CustomJwtClaims>;
}
const exports: Exports;
@ -78,12 +66,13 @@ declare global {
export { exports as default };
`;
const defaultUserJwtClaimsCode = `/**
const defaultAccessTokenJwtCustomizerCode = `/**
* This function is called to get custom claims for the JWT token.
*
* @param {string} token -The JWT token.
* @param {${JwtCustomizerTypeDefinitionKey.AccessTokenPayload}} token -The JWT token.
* @param {Data} data - Logto internal data that can be used to pass additional information
* @param {User} data.user - The user info associated with the token.
* @param {${JwtCustomizerTypeDefinitionKey.JwtCustomizerUserContext}} data.user - The user info associated with the token.
* @param {${JwtCustomizerTypeDefinitionKey.EnvironmentVariables}} [envVariables] - The environment variables.
*
* @returns The custom claims.
*/
@ -92,10 +81,11 @@ exports.getCustomJwtClaims = async (token, data) => {
return {};
}`;
const defaultMachineToMachineJwtClaimsCode = `/**
const defaultClientCredentialsJwtCustomizerCode = `/**
* This function is called to get custom claims for the JWT token.
*
* @param {string} token -The JWT token.
* @param {${JwtCustomizerTypeDefinitionKey.ClientCredentialsPayload}} token -The JWT token.
* @param {${JwtCustomizerTypeDefinitionKey.EnvironmentVariables}} [envVariables] - The environment variables.
*
* @returns The custom claims.
*/
@ -104,20 +94,38 @@ exports.getCustomJwtClaims = async (token) => {
return {};
}`;
export const userJwtFile: ModelSettings = {
export const accessTokenJwtCustomizerModel: ModelSettings = {
name: 'user-jwt.ts',
title: 'TypeScript',
language: 'typescript',
defaultValue: defaultUserJwtClaimsCode,
globalDeclarations: userJwtGlobalDeclarations,
defaultValue: defaultAccessTokenJwtCustomizerCode,
extraLibs: [
{
content: accessTokenJwtCustomizerDefinition,
filePath: `file:///logto-jwt-customizer.d.ts`,
},
{
content: buildAccessTokenJwtCustomizerContextTsDefinition(),
filePath: `file:///logto-jwt-customizer-context.d.ts`,
},
],
};
export const machineToMachineJwtFile: ModelSettings = {
export const clientCredentialsModel: ModelSettings = {
name: 'machine-to-machine-jwt.ts',
title: 'TypeScript',
language: 'typescript',
defaultValue: defaultMachineToMachineJwtClaimsCode,
globalDeclarations: machineToMachineJwtGlobalDeclarations,
defaultValue: defaultClientCredentialsJwtCustomizerCode,
extraLibs: [
{
content: clientCredentialsJwtCustomizerDefinition,
filePath: `file:///logto-jwt-customizer.d.ts`,
},
{
content: buildClientCredentialsJwtCustomizerContextTsDefinition(),
filePath: `file:///logto-jwt-customizer-context.d.ts`,
},
],
};
/**

View file

@ -4,7 +4,7 @@ import {
type ClientCredentialsJwtCustomizer,
} from '@logto/schemas';
import type { JwtClaimsFormType } from './type';
import type { JwtClaimsFormType } from '../type';
const formatEnvVariablesResponseToFormData = (
enVariables?: AccessTokenJwtCustomizer['envVars']

View file

@ -0,0 +1,34 @@
import {
JwtCustomizerTypeDefinitionKey,
accessTokenPayloadTypeDefinition,
jwtCustomizerUserContextTypeDefinition,
clientCredentialsPayloadTypeDefinition,
} from '@/consts/jwt-customizer-type-definition';
import { type JwtClaimsFormType } from '../type';
export { JwtCustomizerTypeDefinitionKey } from '@/consts/jwt-customizer-type-definition';
export const buildAccessTokenJwtCustomizerContextTsDefinition = () => {
return `declare ${jwtCustomizerUserContextTypeDefinition}
declare ${accessTokenPayloadTypeDefinition}`;
};
export const buildClientCredentialsJwtCustomizerContextTsDefinition = () =>
`declare ${clientCredentialsPayloadTypeDefinition}`;
export const buildEnvironmentVariablesTypeDefinition = (
envVariables?: JwtClaimsFormType['environmentVariables']
) => {
const typeDefinition = envVariables
? `{
${envVariables
.filter(({ key }) => Boolean(key))
.map(({ key }) => `${key}: string`)
.join(';\n')}
}`
: 'undefined';
return `declare type ${JwtCustomizerTypeDefinitionKey.EnvironmentVariables} = ${typeDefinition}`;
};

View file

@ -0,0 +1,7 @@
{
"extends": "@silverhand/ts-config/tsconfig.base",
"compilerOptions": {
"outDir": "scripts-js"
},
"include": ["scripts"]
}

View file

@ -3200,6 +3200,9 @@ importers:
zod:
specifier: ^3.22.4
version: 3.22.4
zod-to-ts:
specifier: ^1.2.0
version: 1.2.0(typescript@5.3.3)(zod@3.22.4)
packages/core:
dependencies:
@ -21483,6 +21486,16 @@ packages:
resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
engines: {node: '>=12.20'}
/zod-to-ts@1.2.0(typescript@5.3.3)(zod@3.22.4):
resolution: {integrity: sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==}
peerDependencies:
typescript: ^4.9.4 || ^5.0.2
zod: ^3
dependencies:
typescript: 5.3.3
zod: 3.22.4
dev: true
/zod@3.22.4:
resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}
requiresBuild: true