diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index 7ab73c0a9..bc25bf5e9 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -21,7 +21,7 @@ jobs: fetch-depth: 0 - name: Setup Node and pnpm - uses: silverhand-io/actions-node-pnpm-run-steps@v2 + uses: silverhand-io/actions-node-pnpm-run-steps@v1.2.3 - name: Commitlint run: npx commitlint --from HEAD~${{ github.event.pull_request.commits }} --to HEAD diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 9b8c0d882..113c2a46f 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -19,10 +19,17 @@ jobs: - uses: actions/checkout@v3 - name: Setup Node and pnpm - uses: silverhand-io/actions-node-pnpm-run-steps@v2 + uses: silverhand-io/actions-node-pnpm-run-steps@v1.2.3 - name: Build - run: pnpm -r build + run: pnpm -- lerna run build --stream + + - name: Add the mock connectors for integration tests only + run: | + pnpm add-connector @logto/connector-mock-sms + pnpm add-connector @logto/connector-mock-email + pnpm add-connector @logto/connector-mock-social + working-directory: packages/core - name: Package run: ./package.sh @@ -53,14 +60,15 @@ jobs: cp tests/package.json ./ - name: Setup Node and pnpm - uses: silverhand-io/actions-node-pnpm-run-steps@v2 + uses: silverhand-io/actions-node-pnpm-run-steps@v1.2.3 with: run-install: false # Setup integration test - name: Install dependencies run: | - cd tests + mv tests /tmp/tests + cd /tmp/tests pnpm i pnpm prepack @@ -73,22 +81,15 @@ jobs: name: integration-test-${{ github.sha }} - name: Extract - working-directory: tests - run: | - npm run cli init -- -p ../logto --db postgres://postgres:postgres@localhost:5432 --no-oc --du ../logto.tar.gz - - - name: Add mock connectors - working-directory: tests - run: | - npm run cli connector add @logto/connector-mock-sms @logto/connector-mock-email @logto/connector-mock-social -- -p ../logto + run: tar -xzf logto.tar.gz - name: Run Logto + run: node . --from-root --all-yes & working-directory: logto/packages/core - run: node . & env: INTEGRATION_TEST: true NODE_ENV: production - DB_URL: postgres://postgres:postgres@localhost:5432 + DB_URL_DEFAULT: postgres://postgres:postgres@localhost:5432 - name: Sleep for 5 seconds run: sleep 5 @@ -96,7 +97,7 @@ jobs: # Test - name: Run tests run: | - cd tests/packages/integration-tests + cd /tmp/tests/packages/integration-tests pnpm start env: INTEGRATION_TESTS_LOGTO_URL: http://localhost:3001 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 96cec9aea..56b3c7ef7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v3 - name: Setup Node and pnpm - uses: silverhand-io/actions-node-pnpm-run-steps@v2 + uses: silverhand-io/actions-node-pnpm-run-steps@v1.2.3 - name: Build run: pnpm ci:build @@ -33,7 +33,7 @@ jobs: - uses: actions/checkout@v3 - name: Setup Node and pnpm - uses: silverhand-io/actions-node-pnpm-run-steps@v2 + uses: silverhand-io/actions-node-pnpm-run-steps@v1.2.3 - name: Prepack run: pnpm prepack @@ -51,7 +51,7 @@ jobs: - uses: actions/checkout@v3 - name: Setup Node and pnpm - uses: silverhand-io/actions-node-pnpm-run-steps@v2 + uses: silverhand-io/actions-node-pnpm-run-steps@v1.2.3 - name: Prepack run: pnpm prepack diff --git a/.github/workflows/master-codecov-report.yml b/.github/workflows/master-codecov-report.yml index be9e16c8a..1a1a02087 100644 --- a/.github/workflows/master-codecov-report.yml +++ b/.github/workflows/master-codecov-report.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v3 - name: Setup Node and pnpm - uses: silverhand-io/actions-node-pnpm-run-steps@v2 + uses: silverhand-io/actions-node-pnpm-run-steps@v1.2.3 - name: Prepack run: pnpm prepack diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e71f4fa5b..1a1cb4885 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -36,7 +36,7 @@ jobs: git_commit_gpgsign: true - name: Setup Node and pnpm - uses: silverhand-io/actions-node-pnpm-run-steps@v2 + uses: silverhand-io/actions-node-pnpm-run-steps@v1.2.3 - name: Configure Git user run: | @@ -46,10 +46,7 @@ jobs: - name: Publish to GitHub # add `no-verify-access` due to https://github.com/lerna/lerna/issues/2788 run: | - pnpm \ - --package=conventional-changelog-conventionalcommits \ - --package=lerna@^5.0.0 \ - dlx lerna publish \ + pnpm lerna publish \ -m "release: %s" \ --conventional-commits \ --preid=${{ github.event.inputs.preid }} \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f5609ffb6..39c4634d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,6 @@ concurrency: jobs: dockerize: - environment: ${{ startsWith(github.ref, 'refs/tags/') && 'release' || '' }} runs-on: ubuntu-latest steps: @@ -80,7 +79,6 @@ jobs: create-github-release: - environment: release runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') @@ -90,7 +88,7 @@ jobs: fetch-depth: 0 - name: Setup Node and pnpm - uses: silverhand-io/actions-node-pnpm-run-steps@v2 + uses: silverhand-io/actions-node-pnpm-run-steps@v1.2.3 - name: Extract changelog run: | @@ -102,7 +100,7 @@ jobs: > /tmp/changelog.txt - name: Build - run: pnpm -r build + run: pnpm -- lerna run build --stream - name: Package run: ./package.sh diff --git a/.github/workflows/upload-annotations.yml b/.github/workflows/upload-annotations.yml index 2ae60272d..7508e1e05 100644 --- a/.github/workflows/upload-annotations.yml +++ b/.github/workflows/upload-annotations.yml @@ -22,13 +22,13 @@ jobs: - uses: actions/checkout@v3 - name: Setup Node and pnpm - uses: silverhand-io/actions-node-pnpm-run-steps@v2 + uses: silverhand-io/actions-node-pnpm-run-steps@v1.2.3 - name: Prepack run: pnpm prepack - name: Lint with Report - run: pnpm -r --parallel lint:report && node merge-eslint-reports.js + run: pnpm -- lerna run --parallel lint:report && node merge-eslint-reports.js - name: Annotate Code Linting Results uses: ataylorme/eslint-annotate-action@1.2.0 diff --git a/.gitpod.yml b/.gitpod.yml index a53dc9db6..097744dd9 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -8,15 +8,16 @@ tasks: pnpm prepack cd packages/core pnpm build + pnpm add-official-connectors cd - - pnpm cli connector add --official command: | export ENDPOINT=$(gp url 3001) - pnpm cli db seed - pnpm -r --parallel --filter=!@logto/integration-tests dev + pnpm lerna --ignore=@logto/integration-test run --parallel dev env: + ALL_YES: 1 + NO_INQUIRY: 0 TRUST_PROXY_HEADER: 1 - DB_URL: postgres://postgres:p0stgr3s@127.0.0.1:5432 + DB_URL_DEFAULT: postgres://postgres:p0stgr3s@127.0.0.1:5432 ports: - name: Logto diff --git a/.husky/pre-commit b/.husky/pre-commit index 058f96add..f6bbe2ee7 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -FORCE_COLOR=1 pnpm -r --filter "[HEAD]" precommit +FORCE_COLOR=1 pnpm -- lerna run --concurrency 1 --stream precommit --since HEAD --exclude-dependents diff --git a/Dockerfile b/Dockerfile index 4fa529bb3..16310305d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,11 +11,12 @@ RUN apk add --no-cache python3 make g++ # Install dependencies and build RUN pnpm i -RUN pnpm -r build +RUN pnpm -- lerna run build --stream # Add official connectors +WORKDIR /etc/logto/packages/core +RUN pnpm add-official-connectors WORKDIR /etc/logto -RUN pnpm cli connector add --official # Prune dependencies for production RUN rm -rf node_modules packages/*/node_modules @@ -29,4 +30,5 @@ FROM node:16-alpine as app WORKDIR /etc/logto COPY --from=builder /etc/logto . EXPOSE 3001 +ENV NO_INQUIRY true ENTRYPOINT ["npm", "start"] diff --git a/README.md b/README.md index ed9dadde8..5ad6cdb9e 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ [![checks](https://img.shields.io/github/checks-status/logto-io/logto/master)](https://github.com/logto-io/logto/actions?query=branch%3Amaster) [![release](https://img.shields.io/github/v/release/logto-io/logto?color=7958FF)](https://github.com/logto-io/logto/releases) [![core coverage](https://img.shields.io/codecov/c/github/logto-io/logto?label=core%20coverage)](https://app.codecov.io/gh/logto-io/logto) -[![gitpod](https://img.shields.io/badge/gitpod-available-f09439)](https://gitpod.io/#https://github.com/logto-io/demo) +[![gitpod](https://img.shields.io/badge/gitpod-available-f09439)](https://gitpod.io/#https://github.com/logto-io/logto) [![render](https://img.shields.io/badge/render-deploy-5364e9)](https://render.com/deploy?repo=https://github.com/logto-io/logto) Logto[^info] helps you build the sign-in, auth, and user identity within minutes. @@ -44,7 +44,7 @@ Boringly, we call it "[customer identity access management](https://en.wikipedia #### Online demo (GitPod) -[Click here](https://gitpod.io/#https://github.com/logto-io/demo) to launch Logto via GitPod. Once you see the message like `App is running at https://3001-...gitpod.io` in the terminal, press Cmd (or Ctrl) and click the URL to continue your Logto journey. +[Click here](https://gitpod.io/#https://github.com/logto-io/logto) to launch Logto via GitPod. Once you see the message like `App is running at https://3001-...gitpod.io` in the terminal, press Cmd (or Ctrl) and click the URL to continue your Logto journey. #### Docker Compose diff --git a/commitlint.config.js b/commitlint.config.js index a2c1d2f00..c3ce6e7c9 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,14 +1,10 @@ const { rules } = require('@commitlint/config-conventional'); -const isCi = process.env.CI === 'true'; - /** @type {import('@commitlint/types').UserConfig} **/ module.exports = { extends: ['@commitlint/config-conventional'], rules: { 'type-enum': [2, 'always', [...rules['type-enum'][2], 'api', 'release']], - 'scope-enum': [2, 'always', ['connector', 'console', 'core', 'demo-app', 'test', 'phrases', 'schemas', 'shared', 'ui', 'deps', 'connector-core', 'cli']], - // Slightly increase the tolerance to allow the appending PR number - ...(isCi && { 'header-max-length': [2, 'always', 110] }) + 'scope-enum': [2, 'always', ['connector', 'console', 'core', 'demo-app', 'test', 'phrases', 'schemas', 'shared', 'ui', 'deps', 'connector-core', 'cli']] }, }; diff --git a/merge-eslint-reports.js b/merge-eslint-reports.js index 2012ec7d1..0a37c4436 100644 --- a/merge-eslint-reports.js +++ b/merge-eslint-reports.js @@ -3,7 +3,7 @@ const fs = require('fs'); const directories = fs.readdirSync('./packages'); const reports = directories // Filter out docs temporarily - .filter((dir) => !['docs', 'create'].includes(dir)) + .filter((dir) => dir !== 'docs') .map((dir) => fs.readFileSync(`./packages/${dir}/report.json`, { encoding: 'utf-8' })); const merged = []; diff --git a/package.json b/package.json index b4d131940..7e25a31ce 100644 --- a/package.json +++ b/package.json @@ -5,22 +5,24 @@ "scripts": { "preinstall": "npx only-allow pnpm", "version": "pnpm i --frozen-lockfile=false && git add pnpm-lock.yaml", + "lerna": "lerna", + "bootstrap": "lerna bootstrap", "prepare": "if test \"$NODE_ENV\" != \"production\" && test \"$CI\" != \"true\" ; then husky install ; fi", - "prepack": "pnpm -r prepack", - "dev": "pnpm -r prepack --incremental && pnpm -r --parallel --filter=!@logto/integration-tests dev", - "start": "cd packages/core && NODE_ENV=production node .", - "cli": "logto", - "alteration": "logto db alt", - "ci:build": "pnpm -r build", - "ci:lint": "pnpm -r --parallel lint", - "ci:stylelint": "pnpm -r --parallel stylelint", - "ci:test": "pnpm -r --parallel test:ci" + "prepack": "lerna run --stream prepack", + "dev": "lerna run --stream prepack -- --incremental && lerna --ignore=@logto/integration-tests run --parallel dev", + "start": "cd packages/core && NODE_ENV=production node . --from-root", + "alteration": "cd packages/core && pnpm alteration", + "ci:build": "lerna run --stream build", + "ci:lint": "lerna run --parallel lint", + "ci:stylelint": "lerna run --parallel stylelint", + "ci:test": "lerna run --parallel test:ci" }, "devDependencies": { "@commitlint/cli": "^17.0.0", "@commitlint/config-conventional": "^17.0.0", "@commitlint/types": "^17.0.0", "husky": "^8.0.0", + "lerna": "^5.0.0", "typescript": "^4.7.4" }, "workspaces": { @@ -43,12 +45,8 @@ "pnpm": { "peerDependencyRules": { "allowedVersions": { - "react": "^18.0.0", - "jest": "^29.1.2" + "react": "^18.0.0" } } - }, - "dependencies": { - "@logto/cli": "^1.0.0-beta.10" } } diff --git a/packages/cli/.gitignore b/packages/cli/.gitignore deleted file mode 100644 index 5b78a4af2..000000000 --- a/packages/cli/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -alteration-scripts/ -src/package-json.ts diff --git a/packages/cli/jest.config.ts b/packages/cli/jest.config.ts deleted file mode 100644 index f3ba355b8..000000000 --- a/packages/cli/jest.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { merge, Config } from '@silverhand/jest-config'; - -const config: Config.InitialOptions = merge({ - roots: ['./src'], -}); - -export default config; diff --git a/packages/cli/package.json b/packages/cli/package.json index d6b7c7630..df2c04f04 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -7,7 +7,8 @@ "license": "MPL-2.0", "main": "lib/index.js", "bin": { - "logto": "bin/logto" + "logto": "bin/logto", + "lg": "bin/logto" }, "files": [ "bin", @@ -19,14 +20,11 @@ }, "scripts": { "precommit": "lint-staged", - "prepare:package-json": "node -p \"'export const packageJson = ' + JSON.stringify(require('./package.json'), undefined, 2) + ';'\" > src/package-json.ts", - "build": "rimraf lib && pnpm prepare:package-json && tsc -p tsconfig.build.json", + "build": "rimraf lib && tsc", "start": "node .", - "start:dev": "ts-node --files src/index.ts", + "dev": "ts-node src/index.ts", "lint": "eslint --ext .ts src", "lint:report": "pnpm lint --format json --output-file report.json", - "test": "jest", - "test:ci": "jest", "prepack": "pnpm build" }, "engines": { @@ -36,41 +34,23 @@ "url": "https://github.com/logto-io/logto/issues" }, "dependencies": { - "@logto/schemas": "^1.0.0-beta.10", - "@logto/shared": "^1.0.0-beta.10", - "@silverhand/essentials": "^1.3.0", "chalk": "^4.1.2", - "decamelize": "^5.0.0", - "dotenv": "^16.0.0", - "fs-extra": "^10.1.0", "got": "^11.8.2", "hpagent": "^1.0.0", - "inquirer": "^8.2.2", - "nanoid": "^3.3.4", "ora": "^5.0.0", - "p-retry": "^4.6.1", - "roarr": "^7.11.0", + "prompts": "^2.4.2", "semver": "^7.3.7", - "slonik": "^30.0.0", - "slonik-interceptor-preset": "^1.2.10", - "slonik-sql-tag-raw": "^1.1.4", - "tar": "^6.1.11", - "yargs": "^17.6.0", - "zod": "^3.18.0" + "tar": "^6.1.11" }, "devDependencies": { - "@silverhand/eslint-config": "1.2.0", - "@silverhand/jest-config": "1.2.2", - "@silverhand/ts-config": "1.2.1", - "@types/fs-extra": "^9.0.13", - "@types/inquirer": "^8.2.1", - "@types/jest": "^29.1.2", + "@silverhand/eslint-config": "1.0.0", + "@silverhand/ts-config": "1.0.0", + "@types/decompress": "^4.2.4", "@types/node": "^16.0.0", + "@types/prompts": "^2.0.14", "@types/semver": "^7.3.12", "@types/tar": "^6.1.2", - "@types/yargs": "^17.0.13", "eslint": "^8.21.0", - "jest": "^29.1.2", "lint-staged": "^13.0.0", "prettier": "^2.7.1", "rimraf": "^3.0.2", @@ -78,10 +58,7 @@ "typescript": "^4.7.4" }, "eslintConfig": { - "extends": "@silverhand", - "ignorePatterns": [ - "src/package-json.ts" - ] + "extends": "@silverhand" }, "prettier": "@silverhand/eslint-config/.prettierrc" } diff --git a/packages/cli/src/commands/connector/add.ts b/packages/cli/src/commands/connector/add.ts deleted file mode 100644 index 931957187..000000000 --- a/packages/cli/src/commands/connector/add.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { CommandModule } from 'yargs'; - -import { log } from '../../utilities'; -import { addConnectors, addOfficialConnectors, inquireInstancePath } from './utils'; - -const add: CommandModule< - { path?: string }, - { packages?: string[]; path?: string; official: boolean } -> = { - command: ['add [packages...]', 'a', 'install', 'i'], - describe: 'Add specific Logto connectors', - builder: (yargs) => - yargs - .positional('packages', { - describe: 'The connector package names to add', - type: 'string', - array: true, - default: undefined, - }) - .option('official', { - alias: 'o', - type: 'boolean', - default: false, - describe: - 'Add all official connectors.\n' + - "If it's true, the specified package names will be ignored.", - }), - handler: async ({ packages: packageNames, path, official }) => { - const instancePath = await inquireInstancePath(path); - - if (official) { - await addOfficialConnectors(instancePath); - } else { - if (!packageNames?.length) { - log.error('No connector name provided'); - } - await addConnectors(instancePath, packageNames); - } - - log.info('Restart your Logto instance to get the changes reflected.'); - }, -}; - -export default add; diff --git a/packages/cli/src/commands/connector/index.ts b/packages/cli/src/commands/connector/index.ts deleted file mode 100644 index a5ff36788..000000000 --- a/packages/cli/src/commands/connector/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { noop } from '@silverhand/essentials'; -import { CommandModule } from 'yargs'; - -import add from './add'; -import list from './list'; -import remove from './remove'; - -const connector: CommandModule = { - command: ['connector', 'c', 'connectors'], - describe: 'Command for Logto connectors', - builder: (yargs) => - yargs - .option('path', { - alias: 'p', - type: 'string', - describe: 'The path to your Logto instance directory', - }) - .command(add) - .command(list) - .command(remove) - .demandCommand(1), - handler: noop, -}; - -export default connector; diff --git a/packages/cli/src/commands/connector/list.ts b/packages/cli/src/commands/connector/list.ts deleted file mode 100644 index ae2af5f11..000000000 --- a/packages/cli/src/commands/connector/list.ts +++ /dev/null @@ -1,30 +0,0 @@ -import chalk from 'chalk'; -import { CommandModule } from 'yargs'; - -import { getConnectorPackagesFrom, isOfficialConnector } from './utils'; - -const logConnectorNames = (type: string, names: string[]) => { - if (names.length === 0) { - return; - } - - console.log(); - console.log(chalk.blue(type)); - console.log(names.map((value) => ' ' + value).join('\n')); -}; - -const list: CommandModule<{ path?: string }, { path?: string }> = { - command: ['list', 'l'], - describe: 'List added Logto connectors', - handler: async ({ path: inputPath }) => { - const packages = await getConnectorPackagesFrom(inputPath); - const packageNames = packages.map(({ name }) => name); - const officialPackages = packageNames.filter((name) => isOfficialConnector(name)); - const thirdPartyPackages = packageNames.filter((name) => !isOfficialConnector(name)); - - logConnectorNames('official'.toUpperCase(), officialPackages); - logConnectorNames('3rd-party'.toUpperCase(), thirdPartyPackages); - }, -}; - -export default list; diff --git a/packages/cli/src/commands/connector/remove.ts b/packages/cli/src/commands/connector/remove.ts deleted file mode 100644 index c3a4f37da..000000000 --- a/packages/cli/src/commands/connector/remove.ts +++ /dev/null @@ -1,59 +0,0 @@ -import chalk from 'chalk'; -import fsExtra from 'fs-extra'; -import { CommandModule } from 'yargs'; - -import { log } from '../../utilities'; -import { getConnectorPackagesFrom } from './utils'; - -const remove: CommandModule<{ path?: string }, { path?: string; packages?: string[] }> = { - command: ['remove [packages...]', 'rm', 'delete'], - describe: 'Remove existing Logto connectors', - builder: (yargs) => - yargs.positional('packages', { - describe: 'The connector package names to remove', - type: 'string', - array: true, - default: undefined, - }), - handler: async ({ path: inputPath, packages: packageNames }) => { - if (!packageNames?.length) { - log.error('No connector name provided'); - } - - const existingPackages = await getConnectorPackagesFrom(inputPath); - const notFoundPackageNames = packageNames.filter( - (current) => !existingPackages.some(({ name }) => current === name) - ); - - if (notFoundPackageNames.length > 0) { - log.error( - `Cannot remove ${notFoundPackageNames - .map((name) => chalk.green(name)) - .join(', ')}: not found in your Logto instance directory` - ); - } - - const okSymbol = Symbol('Connector removed'); - const result = await Promise.all( - packageNames.map(async (current) => { - const packageInfo = existingPackages.find(({ name }) => name === current); - - try { - await fsExtra.remove(packageInfo?.path ?? ''); - - return okSymbol; - } catch (error: unknown) { - log.warn(`Error while removing ${chalk.green(packageInfo?.name)}`); - log.warn(error); - - return error; - } - }) - ); - const errorCount = result.filter((value) => value !== okSymbol).length; - - log.info(`Removed ${result.length - errorCount} connectors`); - }, -}; - -export default remove; diff --git a/packages/cli/src/commands/connector/utils.ts b/packages/cli/src/commands/connector/utils.ts deleted file mode 100644 index 48e876c7c..000000000 --- a/packages/cli/src/commands/connector/utils.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { exec } from 'child_process'; -import { existsSync } from 'fs'; -import { readFile, mkdir, unlink, readdir } from 'fs/promises'; -import path from 'path'; -import { promisify } from 'util'; - -import { conditionalString } from '@silverhand/essentials'; -import chalk from 'chalk'; -import { ensureDir, remove } from 'fs-extra'; -import inquirer from 'inquirer'; -import pRetry from 'p-retry'; -import tar from 'tar'; -import { z } from 'zod'; - -import { connectorDirectory } from '../../constants'; -import { log, oraPromise } from '../../utilities'; -import { defaultPath } from '../install/utils'; - -const coreDirectory = 'packages/core'; -const execPromise = promisify(exec); -export const npmPackResultGuard = z - .object({ - name: z.string(), - version: z.string(), - filename: z.string(), - }) - .array(); - -const buildPathErrorMessage = (value: string) => - `The path ${chalk.green(value)} does not contain a Logto instance, please try another.`; - -const validatePath = async (value: string) => { - const corePackageJsonPath = path.resolve(path.join(value, coreDirectory, 'package.json')); - - if (!existsSync(corePackageJsonPath)) { - return buildPathErrorMessage(value); - } - - const packageJson = await readFile(corePackageJsonPath, { encoding: 'utf8' }); - const packageName = await z - .object({ name: z.string() }) - .parseAsync(JSON.parse(packageJson)) - .then(({ name }) => name) - .catch(() => ''); - - if (packageName !== '@logto/core') { - return buildPathErrorMessage(value); - } - - return true; -}; - -export const inquireInstancePath = async (initialPath?: string) => { - const { instancePath } = await inquirer.prompt<{ instancePath: string }>( - { - name: 'instancePath', - message: 'Where is your Logto instance?', - type: 'input', - default: defaultPath, - filter: (value: string) => value.trim(), - validate: validatePath, - }, - { instancePath: initialPath } - ); - - // Validate for initialPath - const validated = await validatePath(instancePath); - - if (validated !== true) { - log.error(validated); - } - - return instancePath; -}; - -const packagePrefix = 'connector-'; - -export const normalizePackageName = (name: string) => - name - .split('/') - // Prepend prefix to the last fragment if needed - .map((fragment, index, array) => - index === array.length - 1 && !fragment.startsWith(packagePrefix) && !fragment.startsWith('@') - ? packagePrefix + fragment - : fragment - ) - .join('/'); - -const getConnectorDirectory = (instancePath: string) => - path.join(instancePath, coreDirectory, connectorDirectory); - -export const isOfficialConnector = (packageName: string) => - packageName.startsWith('@logto/connector-'); - -const getConnectorPackageName = async (directory: string) => { - const filePath = path.join(directory, 'package.json'); - - if (!existsSync(filePath)) { - return; - } - - const json = await readFile(filePath, 'utf8'); - const { name } = z.object({ name: z.string() }).parse(JSON.parse(json)); - - if (name.startsWith('connector-') || Boolean(name.split('/')[1]?.startsWith('connector-'))) { - return name; - } -}; - -export type ConnectorPackage = { - name: string; - path: string; -}; - -export const getConnectorPackagesFrom = async (instancePath?: string) => { - const directory = getConnectorDirectory(await inquireInstancePath(instancePath)); - const content = await readdir(directory, 'utf8'); - const rawPackages = await Promise.all( - content.map(async (value) => { - const currentDirectory = path.join(directory, value); - - return { name: await getConnectorPackageName(currentDirectory), path: currentDirectory }; - }) - ); - - return rawPackages.filter( - (packageInfo): packageInfo is ConnectorPackage => typeof packageInfo.name === 'string' - ); -}; - -export const addConnectors = async (instancePath: string, packageNames: string[]) => { - const cwd = getConnectorDirectory(instancePath); - - if (!existsSync(cwd)) { - await mkdir(cwd); - } - - log.info('Fetch connector metadata'); - - const results = await Promise.all( - packageNames - .map((name) => normalizePackageName(name)) - .map(async (packageName) => { - const run = async () => { - const { stdout } = await execPromise(`npm pack ${packageName} --json`, { cwd }); - const result = npmPackResultGuard.parse(JSON.parse(stdout)); - - if (!result[0]) { - throw new Error( - `Unable to execute ${chalk.green('npm pack')} on package ${chalk.green(packageName)}` - ); - } - - const { filename, name } = result[0]; - const escapedFilename = filename.replace(/\//g, '-').replace(/@/g, ''); - const tarPath = path.join(cwd, escapedFilename); - const packageDirectory = path.join(cwd, name.replace(/\//g, '-')); - - await remove(packageDirectory); - await ensureDir(packageDirectory); - await tar.extract({ cwd: packageDirectory, file: tarPath, strip: 1 }); - await unlink(tarPath); - - log.succeed(`Added ${chalk.green(name)}`); - }; - - try { - await pRetry(run, { retries: 2 }); - } catch (error: unknown) { - console.warn(`[${packageName}]`, error); - - return packageName; - } - }) - ); - - const errorPackages = results.filter(Boolean); - const errorCount = errorPackages.length; - - log.info( - errorCount - ? `Finished with ${errorCount} error${conditionalString(errorCount > 1 && 's')}.` - : 'Finished' - ); - - if (errorCount) { - log.warn('Failed to add ' + errorPackages.map((name) => chalk.green(name)).join(', ')); - } -}; - -const officialConnectorPrefix = '@logto/connector-'; - -const fetchOfficialConnectorList = async () => { - const { stdout } = await execPromise(`npm search ${officialConnectorPrefix} --json`); - const packages = z - .object({ name: z.string() }) - .transform(({ name }) => name) - .array() - .parse(JSON.parse(stdout)); - - return packages.filter((name) => - ['mock', 'kit'].every( - (excluded) => !name.slice(officialConnectorPrefix.length).startsWith(excluded) - ) - ); -}; - -export const addOfficialConnectors = async (instancePath: string) => { - const packages = await oraPromise(fetchOfficialConnectorList(), { - text: 'Fetch official connector list', - prefixText: chalk.blue('[info]'), - }); - await addConnectors(instancePath, packages); -}; diff --git a/packages/cli/src/commands/database/alteration/index.test.ts b/packages/cli/src/commands/database/alteration/index.test.ts deleted file mode 100644 index 4a2cc37bc..000000000 --- a/packages/cli/src/commands/database/alteration/index.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { createMockPool } from 'slonik'; - -import * as functions from '.'; -import * as queries from '../../../queries/logto-config'; -import { QueryType } from '../../../test-utilities'; -import { chooseAlterationsByVersion } from './version'; - -const mockQuery: jest.MockedFunction = jest.fn(); - -const pool = createMockPool({ - query: async (sql, values) => { - return mockQuery(sql, values); - }, -}); - -describe('getUndeployedAlterations()', () => { - const files = Object.freeze([ - { filename: '1.0.0-1663923770-a.js', path: '/alterations/1.0.0-1663923770-a.js' }, - { filename: '1.0.0-1663923771-b.js', path: '/alterations/1.0.0-1663923771-b.js' }, - { filename: '1.0.0-1663923772-c.js', path: '/alterations/1.0.0-1663923772-c.js' }, - ]); - - beforeEach(() => { - // `getAlterationFiles()` will ensure the order - jest.spyOn(functions, 'getAlterationFiles').mockResolvedValueOnce([...files]); - }); - - it('returns all files if database timestamp is 0', async () => { - jest.spyOn(queries, 'getCurrentDatabaseAlterationTimestamp').mockResolvedValueOnce(0); - - await expect(functions.getUndeployedAlterations(pool)).resolves.toEqual(files); - }); - - it('returns files whose timestamp is greater then database timestamp', async () => { - jest - .spyOn(queries, 'getCurrentDatabaseAlterationTimestamp') - .mockResolvedValueOnce(1_663_923_770); - - await expect(functions.getUndeployedAlterations(pool)).resolves.toEqual([files[1], files[2]]); - }); -}); - -describe('chooseAlterationsByVersion()', () => { - const files = Object.freeze( - [ - '1.0.0_beta.9-1663923770-a.js', - '1.0.0_beta.9-1663923771-b.js', - '1.0.0_beta.10-1663923772-c.js', - '1.0.0_beta.11-1663923773-c.js', - '1.0.0_beta.11-1663923774-c.js', - '1.0.0-1663923775-c.js', - '1.0.0-1663923776-c.js', - '1.0.1-1663923777-c.js', - '1.2.0-1663923778-c.js', - 'next-1663923778-c.js', - 'next-1663923779-c.js', - 'next-1663923780-c.js', - 'next1-1663923781-c.js', - ].map((filename) => ({ filename, path: '/alterations/' + filename })) - ); - - it('chooses nothing when input version is invalid', async () => { - await expect(chooseAlterationsByVersion(files, 'next1')).rejects.toThrow( - 'Invalid Version: next1' - ); - await expect(chooseAlterationsByVersion([], 'ok')).rejects.toThrow('Invalid Version: ok'); - }); - - it('chooses correct alteration files', async () => { - await Promise.all([ - expect(chooseAlterationsByVersion([], 'v1.0.0')).resolves.toEqual([]), - expect(chooseAlterationsByVersion(files, 'v1.0.0')).resolves.toEqual(files.slice(0, 7)), - expect(chooseAlterationsByVersion(files, 'v1.0.0-beta.10')).resolves.toEqual( - files.slice(0, 3) - ), - expect(chooseAlterationsByVersion(files, 'v1.1.0')).resolves.toEqual(files.slice(0, 8)), - expect(chooseAlterationsByVersion(files, 'v1.2.0')).resolves.toEqual(files.slice(0, 9)), - expect(chooseAlterationsByVersion(files, 'next')).resolves.toEqual(files.slice(0, 12)), - ]); - }); -}); diff --git a/packages/cli/src/commands/database/alteration/index.ts b/packages/cli/src/commands/database/alteration/index.ts deleted file mode 100644 index 6c45de635..000000000 --- a/packages/cli/src/commands/database/alteration/index.ts +++ /dev/null @@ -1,162 +0,0 @@ -import path from 'path'; - -import { AlterationScript } from '@logto/schemas/lib/types/alteration'; -import { findPackage } from '@logto/shared'; -import { conditionalString } from '@silverhand/essentials'; -import chalk from 'chalk'; -import { copy, existsSync, remove, readdir } from 'fs-extra'; -import { DatabasePool } from 'slonik'; -import { CommandModule } from 'yargs'; - -import { createPoolFromConfig } from '../../../database'; -import { - getCurrentDatabaseAlterationTimestamp, - updateDatabaseTimestamp, -} from '../../../queries/logto-config'; -import { getPathInModule, log } from '../../../utilities'; -import { AlterationFile } from './type'; -import { chooseAlterationsByVersion } from './version'; - -const alterationFilenameRegex = /-(\d+)-?.*\.js$/; - -const getTimestampFromFilename = (filename: string) => { - const match = alterationFilenameRegex.exec(filename); - - if (!match?.[1]) { - throw new Error(`Can not get timestamp: ${filename}`); - } - - return Number(match[1]); -}; - -const importAlterationScript = async (filePath: string): Promise => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const module = await import(filePath); - - // eslint-disable-next-line no-restricted-syntax - return module.default as AlterationScript; -}; - -export const getAlterationFiles = async (): Promise => { - const alterationDirectory = getPathInModule('@logto/schemas', 'alterations'); - - /** - * We copy all alteration scripts to the CLI package root directory, - * since they need a proper context that includes required dependencies (such as slonik) in `node_modules/`. - * While the original `@logto/schemas` may remove them in production. - */ - const packageDirectory = await findPackage( - // Until we migrate to ESM - // eslint-disable-next-line unicorn/prefer-module - __dirname - ); - - const localAlterationDirectory = path.resolve( - // Until we migrate to ESM - // eslint-disable-next-line unicorn/prefer-module - packageDirectory ?? __dirname, - 'alteration-scripts' - ); - - if (!existsSync(alterationDirectory)) { - return []; - } - - // We need to copy alteration files to execute in the CLI context to make `slonik` available - await remove(localAlterationDirectory); - await copy(alterationDirectory, localAlterationDirectory); - - const directory = await readdir(localAlterationDirectory); - const files = directory.filter((file) => alterationFilenameRegex.test(file)); - - return files - .slice() - .sort((file1, file2) => getTimestampFromFilename(file1) - getTimestampFromFilename(file2)) - .map((filename) => ({ path: path.join(localAlterationDirectory, filename), filename })); -}; - -export const getLatestAlterationTimestamp = async () => { - const files = await getAlterationFiles(); - const lastFile = files[files.length - 1]; - - if (!lastFile) { - return 0; - } - - return getTimestampFromFilename(lastFile.filename); -}; - -export const getUndeployedAlterations = async (pool: DatabasePool) => { - const databaseTimestamp = await getCurrentDatabaseAlterationTimestamp(pool); - const files = await getAlterationFiles(); - - return files.filter(({ filename }) => getTimestampFromFilename(filename) > databaseTimestamp); -}; - -const deployAlteration = async ( - pool: DatabasePool, - { path: filePath, filename }: AlterationFile -) => { - const { up } = await importAlterationScript(filePath); - - try { - await pool.transaction(async (connection) => { - await up(connection); - await updateDatabaseTimestamp(connection, getTimestampFromFilename(filename)); - }); - } catch (error: unknown) { - console.error(error); - - await pool.end(); - log.error( - `Error ocurred during running alteration ${chalk.blue(filename)}.\n\n` + - " This alteration didn't change anything since it was in a transaction.\n" + - ' Try to fix the error and deploy again.' - ); - } - - log.info(`Run alteration ${filename} succeeded`); -}; - -const alteration: CommandModule = { - command: ['alteration [target]', 'alt', 'alter'], - describe: 'Perform database alteration', - builder: (yargs) => - yargs - .positional('action', { - describe: 'The action to perform, now it only accepts `deploy`', - type: 'string', - demandOption: true, - }) - .positional('target', { - describe: 'The target Logto version for alteration', - type: 'string', - }), - handler: async ({ action, target }) => { - if (action !== 'deploy') { - log.error('Unsupported action'); - } - - const pool = await createPoolFromConfig(); - const alterations = await chooseAlterationsByVersion( - await getUndeployedAlterations(pool), - target - ); - - log.info( - `Found ${alterations.length} alteration${conditionalString( - alterations.length > 1 && 's' - )} to deploy` - ); - - // The await inside the loop is intended, alterations should run in order - for (const alteration of alterations) { - // eslint-disable-next-line no-await-in-loop - await deployAlteration(pool, alteration); - } - - await pool.end(); - }, -}; - -export default alteration; diff --git a/packages/cli/src/commands/database/alteration/type.ts b/packages/cli/src/commands/database/alteration/type.ts deleted file mode 100644 index dc94e658e..000000000 --- a/packages/cli/src/commands/database/alteration/type.ts +++ /dev/null @@ -1 +0,0 @@ -export type AlterationFile = { path: string; filename: string }; diff --git a/packages/cli/src/commands/database/alteration/version.ts b/packages/cli/src/commands/database/alteration/version.ts deleted file mode 100644 index befde14d0..000000000 --- a/packages/cli/src/commands/database/alteration/version.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { conditional } from '@silverhand/essentials'; -import chalk from 'chalk'; -import inquirer from 'inquirer'; -import { SemVer, compare, eq, gt } from 'semver'; - -import { findLastIndex, log } from '../../../utilities'; -import { AlterationFile } from './type'; - -const getVersionFromFilename = (filename: string) => { - try { - return new SemVer(filename.split('-')[0]?.replaceAll('_', '-') ?? 'unknown'); - } catch {} -}; - -const latestTag = 'latest'; -const nextTag = 'next'; - -export const chooseAlterationsByVersion = async ( - alterations: readonly AlterationFile[], - initialVersion?: string -) => { - if (initialVersion === nextTag) { - const endIndex = findLastIndex( - alterations, - ({ filename }) => - filename.startsWith(nextTag + '-') || Boolean(getVersionFromFilename(filename)) - ); - - if (endIndex === -1) { - return []; - } - - log.info(`Deploy target ${chalk.green(nextTag)}`); - - return alterations.slice(0, endIndex + 1); - } - - const versions = alterations - .map(({ filename }) => getVersionFromFilename(filename)) - .filter((version): version is SemVer => version instanceof SemVer) - // Cannot use `Set` to deduplicate since it's a class - .filter((version, index, self) => index === self.findIndex((another) => eq(version, another))) - .slice() - .sort((i, j) => compare(j, i)); - const initialSemVersion = conditional( - initialVersion && initialVersion !== latestTag && new SemVer(initialVersion) - ); - - if (!versions[0]) { - return []; - } - - const { version: targetVersion } = - initialVersion === latestTag - ? { version: versions[0] } - : await inquirer.prompt<{ version: SemVer }>( - { - type: 'list', - message: 'Choose the alteration target version', - name: 'version', - choices: versions.map((semVersion) => ({ - name: semVersion.version, - value: semVersion, - })), - }, - { - version: initialSemVersion, - } - ); - - log.info(`Deploy target ${chalk.green(targetVersion.version)}`); - - return alterations.filter(({ filename }) => { - const version = getVersionFromFilename(filename); - - return version && !gt(version, targetVersion); - }); -}; diff --git a/packages/cli/src/commands/database/config.ts b/packages/cli/src/commands/database/config.ts deleted file mode 100644 index eeb6252d4..000000000 --- a/packages/cli/src/commands/database/config.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { logtoConfigGuards, LogtoConfigKey, logtoConfigKeys } from '@logto/schemas'; -import { deduplicate } from '@silverhand/essentials'; -import chalk from 'chalk'; -import { CommandModule } from 'yargs'; - -import { createPoolFromConfig } from '../../database'; -import { getRowsByKeys, updateValueByKey } from '../../queries/logto-config'; -import { log } from '../../utilities'; - -const validKeysDisplay = chalk.green(logtoConfigKeys.join(', ')); - -type ValidateKeysFunction = { - (keys: string[]): asserts keys is LogtoConfigKey[]; - (key: string): asserts key is LogtoConfigKey; -}; - -const validateKeys: ValidateKeysFunction = (keys) => { - const invalidKey = (Array.isArray(keys) ? keys : [keys]).find( - // Using `.includes()` will result a type error - // eslint-disable-next-line unicorn/prefer-includes - (key) => !logtoConfigKeys.some((element) => element === key) - ); - - if (invalidKey) { - log.error( - `Invalid config key ${chalk.red(invalidKey)} found, expected one of ${validKeysDisplay}` - ); - } -}; - -export const getConfig: CommandModule = { - command: 'get-config [keys...]', - describe: 'Get config value(s) of the given key(s) in Logto database', - builder: (yargs) => - yargs - .positional('key', { - describe: `The key to get from database, one of ${validKeysDisplay}`, - type: 'string', - demandOption: true, - }) - .positional('keys', { - describe: 'The additional keys to get from database', - type: 'string', - array: true, - default: [], - }), - handler: async ({ key, keys }) => { - const queryKeys = deduplicate([key, ...keys]); - validateKeys(queryKeys); - - const pool = await createPoolFromConfig(); - const { rows } = await getRowsByKeys(pool, queryKeys); - await pool.end(); - - console.log( - queryKeys - .map((currentKey) => { - const value = rows.find(({ key }) => currentKey === key)?.value; - - return ( - chalk.magenta(currentKey) + - '=' + - (value === undefined ? chalk.gray(value) : chalk.green(JSON.stringify(value))) - ); - }) - .join('\n') - ); - }, -}; - -export const setConfig: CommandModule = { - command: 'set-config ', - describe: 'Set config value of the given key in Logto database', - builder: (yargs) => - yargs - .positional('key', { - describe: `The key to get from database, one of ${validKeysDisplay}`, - type: 'string', - demandOption: true, - }) - .positional('value', { - describe: 'The value to set, should be a valid JSON string', - type: 'string', - demandOption: true, - }), - handler: async ({ key, value }) => { - validateKeys(key); - - const guarded = logtoConfigGuards[key].parse(JSON.parse(value)); - - const pool = await createPoolFromConfig(); - await updateValueByKey(pool, key, guarded); - await pool.end(); - - log.info(`Update ${chalk.green(key)} succeeded`); - }, -}; diff --git a/packages/cli/src/commands/database/index.ts b/packages/cli/src/commands/database/index.ts deleted file mode 100644 index 265e559e8..000000000 --- a/packages/cli/src/commands/database/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { noop } from '@silverhand/essentials'; -import { CommandModule } from 'yargs'; - -import alteration from './alteration'; -import { getConfig, setConfig } from './config'; -import seed from './seed'; - -const database: CommandModule = { - command: ['database', 'db'], - describe: 'Commands for Logto database', - builder: (yargs) => - yargs.command(getConfig).command(setConfig).command(seed).command(alteration).demandCommand(1), - handler: noop, -}; - -export default database; diff --git a/packages/cli/src/commands/database/seed/index.ts b/packages/cli/src/commands/database/seed/index.ts deleted file mode 100644 index 526e44df7..000000000 --- a/packages/cli/src/commands/database/seed/index.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { readdir, readFile } from 'fs/promises'; -import path from 'path'; - -import { logtoConfigGuards, LogtoOidcConfigKey, seeds } from '@logto/schemas'; -import { buildApplicationSecret } from '@logto/shared'; -import chalk from 'chalk'; -import { DatabasePool, DatabaseTransactionConnection, sql } from 'slonik'; -import { raw } from 'slonik-sql-tag-raw'; -import { CommandModule } from 'yargs'; -import { z } from 'zod'; - -import { createPoolAndDatabaseIfNeeded, insertInto } from '../../../database'; -import { - getRowsByKeys, - isConfigsTableExists, - updateDatabaseTimestamp, - updateValueByKey, -} from '../../../queries/logto-config'; -import { getPathInModule, log, oraPromise } from '../../../utilities'; -import { getLatestAlterationTimestamp } from '../alteration'; -import { oidcConfigReaders } from './oidc-config'; - -const createTables = async (connection: DatabaseTransactionConnection) => { - const tableDirectory = getPathInModule('@logto/schemas', 'tables'); - const directoryFiles = await readdir(tableDirectory); - const tableFiles = directoryFiles.filter((file) => file.endsWith('.sql')); - const queries = await Promise.all( - tableFiles.map>(async (file) => [ - file, - await readFile(path.join(tableDirectory, file), 'utf8'), - ]) - ); - - // Await in loop is intended for better error handling - for (const [, query] of queries) { - // eslint-disable-next-line no-await-in-loop - await connection.query(sql`${raw(query)}`); - } -}; - -const seedTables = async (connection: DatabaseTransactionConnection) => { - const { - managementResource, - defaultSignInExperience, - createDefaultSetting, - createDemoAppApplication, - defaultRole, - } = seeds; - - await Promise.all([ - connection.query(insertInto(managementResource, 'resources')), - connection.query(insertInto(createDefaultSetting(), 'settings')), - connection.query(insertInto(defaultSignInExperience, 'sign_in_experiences')), - connection.query( - insertInto(createDemoAppApplication(buildApplicationSecret()), 'applications') - ), - connection.query(insertInto(defaultRole, 'roles')), - updateDatabaseTimestamp(connection, await getLatestAlterationTimestamp()), - ]); -}; - -const seedOidcConfigs = async (pool: DatabaseTransactionConnection) => { - const configGuard = z.object({ - key: z.nativeEnum(LogtoOidcConfigKey), - value: z.unknown(), - }); - const { rows } = await getRowsByKeys(pool, Object.values(LogtoOidcConfigKey)); - // Filter out valid keys that hold a valid value - const result = await Promise.all( - rows.map>(async (row) => { - try { - const { key, value } = await configGuard.parseAsync(row); - await logtoConfigGuards[key].parseAsync(value); - - return key; - } catch {} - }) - ); - const existingKeys = new Set(result.filter(Boolean)); - - const validOptions = Object.values(LogtoOidcConfigKey).filter((key) => { - const included = existingKeys.has(key); - - if (included) { - log.info(`Key ${chalk.green(key)} exists, skipping`); - } - - return !included; - }); - - // The awaits in loop is intended since we'd like to log info in sequence - /* eslint-disable no-await-in-loop */ - for (const key of validOptions) { - const { value, fromEnv } = await oidcConfigReaders[key](); - - if (fromEnv) { - log.info(`Read config ${chalk.green(key)} from env`); - } else { - log.info(`Generated config ${chalk.green(key)}`); - } - - await updateValueByKey(pool, key, value); - } - /* eslint-enable no-await-in-loop */ - - log.succeed('Seed OIDC config'); -}; - -const seedChoices = Object.freeze(['all', 'oidc'] as const); - -type SeedChoice = typeof seedChoices[number]; - -export const seedByPool = async (pool: DatabasePool, type: SeedChoice) => { - await pool.transaction(async (connection) => { - if (type !== 'oidc') { - await oraPromise(createTables(connection), { - text: 'Create tables', - prefixText: chalk.blue('[info]'), - }); - await oraPromise(seedTables(connection), { - text: 'Seed data', - prefixText: chalk.blue('[info]'), - }); - } - - await seedOidcConfigs(connection); - }); -}; - -const seed: CommandModule, { type: string; swe?: boolean }> = { - command: 'seed [type]', - describe: 'Create database then seed tables and data', - builder: (yargs) => - yargs - .option('swe', { - describe: 'Skip the seeding process when Logto configs table exists', - alias: 'skip-when-exists', - type: 'boolean', - }) - .positional('type', { - describe: 'Optional seed type', - type: 'string', - choices: seedChoices, - default: 'all', - }), - handler: async ({ type, swe }) => { - const pool = await createPoolAndDatabaseIfNeeded(); - - if (swe && (await isConfigsTableExists(pool))) { - log.info('Seeding skipped'); - await pool.end(); - - return; - } - - try { - // Cannot avoid `as` since the official type definition of `yargs` doesn't work. - // The value of `type` can be ensured, so it's safe to use `as` here. - // eslint-disable-next-line no-restricted-syntax - await seedByPool(pool, type as SeedChoice); - } catch (error: unknown) { - console.error(error); - console.log(); - log.warn( - 'Error ocurred during seeding your database.\n\n' + - ' Nothing has changed since the seeding process was in a transaction.\n' + - ' Try to fix the error and seed again.' - ); - } - await pool.end(); - }, -}; - -export default seed; diff --git a/packages/cli/src/commands/database/seed/oidc-config.ts b/packages/cli/src/commands/database/seed/oidc-config.ts deleted file mode 100644 index 5beee6f32..000000000 --- a/packages/cli/src/commands/database/seed/oidc-config.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { generateKeyPair } from 'crypto'; -import { readFile } from 'fs/promises'; -import { promisify } from 'util'; - -import { LogtoOidcConfigKey, LogtoOidcConfigType } from '@logto/schemas'; -import { getEnv, getEnvAsStringArray } from '@silverhand/essentials'; -import { nanoid } from 'nanoid'; - -const isBase64FormatPrivateKey = (key: string) => !key.includes('-'); - -/** - * Each config reader will do the following things in order: - * 1. Try to read value from env (mimic the behavior from the original core) - * 2. Generate value if #1 doesn't work - */ -export const oidcConfigReaders: { - [key in LogtoOidcConfigKey]: () => Promise<{ - value: LogtoOidcConfigType[key]; - fromEnv: boolean; - }>; -} = { - /** - * Try to read private keys with the following order: - * - * 1. From `process.env.OIDC_PRIVATE_KEYS`. - * 2. Fetch path from `process.env.OIDC_PRIVATE_KEY_PATHS` then read from that path. - * - * - * @returns The private keys for OIDC provider. - * @throws An error when failed to read a private key. - */ - [LogtoOidcConfigKey.PrivateKeys]: async () => { - // Direct keys in env - const privateKeys = getEnvAsStringArray('OIDC_PRIVATE_KEYS'); - - if (privateKeys.length > 0) { - return { - value: privateKeys.map((key) => { - if (isBase64FormatPrivateKey(key)) { - return Buffer.from(key, 'base64').toString('utf8'); - } - - return key; - }), - fromEnv: true, - }; - } - - // Read keys from files - const privateKeyPaths = getEnvAsStringArray('OIDC_PRIVATE_KEY_PATHS'); - - if (privateKeyPaths.length > 0) { - return { - value: await Promise.all(privateKeyPaths.map(async (path) => readFile(path, 'utf8'))), - fromEnv: true, - }; - } - - // Generate a new key - const { privateKey } = await promisify(generateKeyPair)('rsa', { - modulusLength: 4096, - publicKeyEncoding: { - type: 'spki', - format: 'pem', - }, - privateKeyEncoding: { - type: 'pkcs8', - format: 'pem', - }, - }); - - return { - value: [privateKey], - fromEnv: false, - }; - }, - [LogtoOidcConfigKey.CookieKeys]: async () => { - const envKey = 'OIDC_COOKIE_KEYS'; - const keys = getEnvAsStringArray(envKey); - - return { value: keys.length > 0 ? keys : [nanoid()], fromEnv: keys.length > 0 }; - }, - [LogtoOidcConfigKey.RefreshTokenReuseInterval]: async () => { - const envKey = 'OIDC_REFRESH_TOKEN_REUSE_INTERVAL'; - const raw = Number(getEnv(envKey)); - const value = Math.max(3, raw || 0); - - return { value, fromEnv: raw === value }; - }, -}; diff --git a/packages/cli/src/commands/install/index.ts b/packages/cli/src/commands/install/index.ts deleted file mode 100644 index b0cb27fd3..000000000 --- a/packages/cli/src/commands/install/index.ts +++ /dev/null @@ -1,110 +0,0 @@ -import chalk from 'chalk'; -import { CommandModule } from 'yargs'; - -import { getDatabaseUrlFromConfig } from '../../database'; -import { log } from '../../utilities'; -import { addOfficialConnectors } from '../connector/utils'; -import { - validateNodeVersion, - inquireInstancePath, - validateDatabase, - downloadRelease, - seedDatabase, - createEnv, - logFinale, - decompress, - inquireOfficialConnectors, - isUrl, -} from './utils'; - -export type InstallArgs = { - path?: string; - skipSeed: boolean; - officialConnectors?: boolean; - downloadUrl?: string; -}; - -const installLogto = async ({ path, skipSeed, officialConnectors, downloadUrl }: InstallArgs) => { - validateNodeVersion(); - - // Get instance path - const instancePath = await inquireInstancePath(path); - - // Validate if user has a valid database - await validateDatabase(); - - // Download and decompress - const tarPath = - !downloadUrl || isUrl(downloadUrl) ? await downloadRelease(downloadUrl) : downloadUrl; - await decompress(instancePath, tarPath); - - // Seed database - if (skipSeed) { - log.info( - `Skipped database seeding.\n\n' + ' You can use the ${chalk.green( - 'db seed' - )} command to seed database when ready.\n` - ); - } else { - await seedDatabase(instancePath); - } - - // Save to dot env - await createEnv(instancePath, await getDatabaseUrlFromConfig()); - - // Add official connectors - if (await inquireOfficialConnectors(officialConnectors)) { - await addOfficialConnectors(instancePath); - } else { - log.info( - 'Skipped adding official connectors.\n\n' + - ` You can use the ${chalk.green('connector add')} command to add connectors at any time.\n` - ); - } - - // Finale - logFinale(instancePath); -}; - -const install: CommandModule< - unknown, - { - p?: string; - ss: boolean; - oc?: boolean; - du?: string; - } -> = { - command: ['init', 'i', 'install'], - describe: 'Download and run the latest Logto release', - builder: (yargs) => - yargs.options({ - p: { - alias: 'path', - describe: 'Path of Logto, must be a non-existing path', - type: 'string', - }, - ss: { - alias: 'skip-seed', - describe: 'Skip Logto database seeding', - type: 'boolean', - default: false, - }, - oc: { - alias: 'official-connectors', - describe: 'Add official connectors after downloading Logto', - type: 'boolean', - }, - du: { - alias: 'download-url', - describe: 'URL for downloading Logto, can be a local path to tar', - type: 'string', - hidden: true, - }, - }), - handler: async ({ p, ss, oc, du }) => { - await installLogto({ path: p, skipSeed: ss, officialConnectors: oc, downloadUrl: du }); - }, -}; - -export default install; diff --git a/packages/cli/src/commands/install/utils.ts b/packages/cli/src/commands/install/utils.ts deleted file mode 100644 index 9776eec63..000000000 --- a/packages/cli/src/commands/install/utils.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { execSync } from 'child_process'; -import { existsSync } from 'fs'; -import { mkdir } from 'fs/promises'; -import os from 'os'; -import path from 'path'; - -import chalk from 'chalk'; -import { remove, writeFile } from 'fs-extra'; -import inquirer from 'inquirer'; -import * as semver from 'semver'; -import tar from 'tar'; - -import { createPoolAndDatabaseIfNeeded } from '../../database'; -import { cliConfig, ConfigKey, downloadFile, log, oraPromise, safeExecSync } from '../../utilities'; -import { seedByPool } from '../database/seed'; - -export const defaultPath = path.join(os.homedir(), 'logto'); -const pgRequired = new semver.SemVer('14.0.0'); - -export const validateNodeVersion = () => { - const required = new semver.SemVer('16.0.0'); - const current = new semver.SemVer(execSync('node -v', { encoding: 'utf8', stdio: 'pipe' })); - - if (required.compare(current) > 0) { - log.error(`Logto requires NodeJS >=${required.version}, but ${current.version} found.`); - } - - if (current.major > required.major) { - log.warn( - `Logto is tested under NodeJS ^${required.version}, but version ${current.version} found.` - ); - } -}; - -const validatePath = (value: string) => - existsSync(path.resolve(value)) - ? `The path ${chalk.green(value)} already exists, please try another.` - : true; - -export const inquireInstancePath = async (initialPath?: string) => { - const { instancePath } = await inquirer.prompt<{ instancePath: string }>( - { - name: 'instancePath', - message: 'Where should we create your Logto instance?', - type: 'input', - default: defaultPath, - filter: (value: string) => value.trim(), - validate: validatePath, - }, - { instancePath: initialPath } - ); - - // Validate for initialPath - const validated = validatePath(instancePath); - - if (validated !== true) { - log.error(validated); - } - - return instancePath; -}; - -export const validateDatabase = async () => { - if (cliConfig.has(ConfigKey.DatabaseUrl)) { - return; - } - - const { hasPostgresUrl } = await inquirer.prompt<{ hasPostgresUrl?: boolean }>({ - name: 'hasPostgresUrl', - message: `Logto requires PostgreSQL >=${pgRequired.version} but cannot find in the current environment.\n Do you have a remote PostgreSQL instance ready?`, - type: 'confirm', - when: () => { - const pgOutput = safeExecSync('postgres --version') ?? ''; - // Filter out all brackets in the output since Homebrew will append `(Homebrew)`. - const pgArray = pgOutput.split(' ').filter((value) => !value.startsWith('(')); - const pgCurrent = semver.coerce(pgArray[pgArray.length - 1]); - - return !pgCurrent || pgCurrent.compare(pgRequired) < 0; - }, - }); - - if (hasPostgresUrl === false) { - log.error('Logto requires a Postgres instance to run.'); - } -}; - -export const downloadRelease = async (url?: string) => { - const tarFilePath = path.resolve(os.tmpdir(), './logto.tar.gz'); - - log.info(`Download Logto to ${tarFilePath}`); - await downloadFile( - url ?? 'https://github.com/logto-io/logto/releases/latest/download/logto.tar.gz', - tarFilePath - ); - - return tarFilePath; -}; - -export const decompress = async (toPath: string, tarPath: string) => { - const run = async () => { - try { - await mkdir(toPath); - await tar.extract({ file: tarPath, cwd: toPath, strip: 1 }); - } catch (error: unknown) { - log.error(error); - } - }; - - return oraPromise( - run(), - { - text: `Decompress to ${toPath}`, - prefixText: chalk.blue('[info]'), - }, - true - ); -}; - -export const seedDatabase = async (instancePath: string) => { - try { - const pool = await createPoolAndDatabaseIfNeeded(); - await seedByPool(pool, 'all'); - await pool.end(); - } catch (error: unknown) { - console.error(error); - - await oraPromise(remove(instancePath), { - text: 'Clean up', - prefixText: chalk.blue('[info]'), - }); - - log.error( - 'Error occurred during seeding your Logto database. Nothing has changed since the seeding process was in a transaction.\n\n' + - ` To skip the database seeding, append ${chalk.green( - '--skip-seed' - )} to the command options.` - ); - } -}; - -export const createEnv = async (instancePath: string, databaseUrl: string) => { - const dotEnvPath = path.resolve(instancePath, '.env'); - await writeFile(dotEnvPath, `DB_URL=${databaseUrl}`, { - encoding: 'utf8', - }); - log.info(`Saved database URL to ${chalk.blue(dotEnvPath)}`); -}; - -export const logFinale = (instancePath: string) => { - const startCommand = `cd ${instancePath} && npm start`; - log.info( - `Use the command below to start Logto. Happy hacking!\n\n ${chalk.green(startCommand)}` - ); -}; - -export const inquireOfficialConnectors = async (initialAnswer?: boolean) => { - const { value } = await inquirer.prompt<{ value: boolean }>( - { - name: 'value', - message: 'Do you want to add official connectors?', - type: 'confirm', - default: true, - }, - { value: initialAnswer } - ); - - return value; -}; - -export const isUrl = (string: string) => { - try { - // On purpose to test - // eslint-disable-next-line no-new - new URL(string); - - return true; - } catch { - return false; - } -}; diff --git a/packages/cli/src/constants.ts b/packages/cli/src/constants.ts deleted file mode 100644 index a759ff0c4..000000000 --- a/packages/cli/src/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const connectorDirectory = 'connectors'; diff --git a/packages/cli/src/database.ts b/packages/cli/src/database.ts deleted file mode 100644 index 1da7f267c..000000000 --- a/packages/cli/src/database.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { SchemaLike } from '@logto/schemas'; -import { convertToPrimitiveOrSql } from '@logto/shared'; -import decamelize from 'decamelize'; -import { createPool, parseDsn, sql, stringifyDsn } from 'slonik'; -import { createInterceptors } from 'slonik-interceptor-preset'; -import { z } from 'zod'; - -import { ConfigKey, getCliConfigWithPrompt, log } from './utilities'; - -export const defaultDatabaseUrl = 'postgresql://localhost:5432/logto'; - -export const getDatabaseUrlFromConfig = async () => - (await getCliConfigWithPrompt({ - key: ConfigKey.DatabaseUrl, - readableKey: 'Logto database URL', - defaultValue: defaultDatabaseUrl, - })) ?? ''; - -export const createPoolFromConfig = async () => { - const databaseUrl = await getDatabaseUrlFromConfig(); - - return createPool(databaseUrl, { - interceptors: createInterceptors(), - }); -}; - -/** - * Create a database pool with the URL in CLI config; if no URL found, prompt to input. - * If the given database does not exists, it will try to create a new database by connecting to the maintenance database `postgres`. - * - * @returns A new database pool with the database URL in config. - */ -export const createPoolAndDatabaseIfNeeded = async () => { - try { - return await createPoolFromConfig(); - } catch (error: unknown) { - const result = z.object({ code: z.string() }).safeParse(error); - - // Database does not exist, try to create one - // https://www.postgresql.org/docs/14/errcodes-appendix.html - if (!(result.success && result.data.code === '3D000')) { - log.error(error); - } - - const databaseUrl = await getDatabaseUrlFromConfig(); - const dsn = parseDsn(databaseUrl); - // It's ok to fall back to '?' since: - // - Database name is required to connect in the previous pool - // - It will throw error when creating database using '?' - const databaseName = dsn.databaseName ?? '?'; - const maintenancePool = await createPool(stringifyDsn({ ...dsn, databaseName: 'postgres' }), { - interceptors: createInterceptors(), - }); - await maintenancePool.query(sql` - create database ${sql.identifier([databaseName])} - with - encoding = 'UTF8' - connection_limit = -1; - `); - await maintenancePool.end(); - - log.succeed(`Created database ${databaseName}`); - - return createPoolFromConfig(); - } -}; - -export const insertInto = (object: T, table: string) => { - const keys = Object.keys(object); - - return sql` - insert into ${sql.identifier([table])} - (${sql.join( - keys.map((key) => sql.identifier([decamelize(key)])), - sql`, ` - )}) - values (${sql.join( - keys.map((key) => convertToPrimitiveOrSql(key, object[key] ?? null)), - sql`, ` - )}) - `; -}; diff --git a/packages/cli/src/include.d/slonik-interceptor-preset.d.ts b/packages/cli/src/include.d/slonik-interceptor-preset.d.ts deleted file mode 100644 index 5e24372aa..000000000 --- a/packages/cli/src/include.d/slonik-interceptor-preset.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -declare module 'slonik-interceptor-preset' { - import { Interceptor } from 'slonik'; - - export const createInterceptors: (config?: { - benchmarkQueries: boolean; - logQueries: boolean; - normaliseQueries: boolean; - transformFieldNames: boolean; - }) => readonly Interceptor[]; -} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index e0596c9e4..1f3f6f4f9 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,55 +1,130 @@ +import { execSync } from 'child_process'; +import { existsSync } from 'fs'; +import { mkdir } from 'fs/promises'; +import os from 'os'; +import path from 'path'; + import chalk from 'chalk'; -import dotenv from 'dotenv'; -import yargs from 'yargs'; -import { hideBin } from 'yargs/helpers'; +import ora from 'ora'; +import * as prompts from 'prompts'; +import * as semver from 'semver'; +import tar from 'tar'; -import connector from './commands/connector'; -import database from './commands/database'; -import install from './commands/install'; -import { packageJson } from './package-json'; -import { cliConfig, ConfigKey } from './utilities'; +import { downloadFile, log, safeExecSync } from './utilities'; -void yargs(hideBin(process.argv)) - .version(false) - .option('env', { - alias: ['e', 'env-file'], - describe: 'The path to your `.env` file', - type: 'string', - }) - .option('db', { - alias: ['db-url', 'database-url'], - describe: 'The Postgres URL to Logto database', - type: 'string', - }) - .option('version', { - alias: 'v', - describe: 'Print Logto CLI version', - type: 'boolean', - global: false, - }) - .middleware(({ version }) => { - if (version) { - console.log(packageJson.name + ' v' + packageJson.version); - // eslint-disable-next-line unicorn/no-process-exit - process.exit(0); +const pgRequired = new semver.SemVer('14.0.0'); + +const validateNodeVersion = () => { + const required = new semver.SemVer('16.0.0'); + const current = new semver.SemVer(execSync('node -v', { encoding: 'utf8', stdio: 'pipe' })); + + if (required.compare(current) > 0) { + log.error(`Logto requires NodeJS >=${required.version}, but ${current.version} found.`); + } + + if (current.major > required.major) { + log.warn( + `Logto is tested under NodeJS ^${required.version}, but version ${current.version} found.` + ); + } +}; + +const getInstancePath = async () => { + const response = await prompts.default( + [ + { + name: 'instancePath', + message: 'Where should we create your logto instance?', + type: 'text', + initial: './logto', + format: (value: string) => path.resolve(value.trim()), + validate: (value: string) => + existsSync(value) ? 'That path already exists, please try another.' : true, + }, + { + name: 'hasPostgresUrl', + message: `Logto requires PostgreSQL >=${pgRequired.version} but cannot find in the current environment.\n Do you have a remote PostgreSQL instance ready?`, + type: () => { + const pgOutput = safeExecSync('postgres --version') ?? ''; + // Filter out all brackets in the output since Homebrew will append `(Homebrew)`. + const pgArray = pgOutput.split(' ').filter((value) => !value.startsWith('(')); + const pgCurrent = semver.coerce(pgArray[pgArray.length - 1]); + + return (!pgCurrent || pgCurrent.compare(pgRequired) < 0) && 'confirm'; + }, + format: (previous) => { + if (!previous) { + log.error('Logto requires a Postgres instance to run.'); + } + }, + }, + ], + { + onCancel: () => { + log.error('Operation cancelled'); + }, } - }, true) - .middleware(({ env, db: databaseUrl }) => { - dotenv.config({ path: env }); + ); - const initialDatabaseUrl = databaseUrl ?? process.env[ConfigKey.DatabaseUrl]; + return String(response.instancePath); +}; - if (initialDatabaseUrl) { - cliConfig.set(ConfigKey.DatabaseUrl, initialDatabaseUrl); - } - }) - .command(install) - .command(database) - .command(connector) - .demandCommand(1) - .showHelpOnFail(false, `Specify ${chalk.green('--help')} for available options`) - .strict() - .parserConfiguration({ - 'dot-notation': false, - }) - .parse(); +const tryStartInstance = async (instancePath: string) => { + const response = await prompts.default({ + name: 'startInstance', + message: 'Would you like to start Logto now?', + type: 'confirm', + initial: true, + }); + + const yes = Boolean(response.startInstance); + const startCommand = `cd ${instancePath} && npm start`; + + if (yes) { + execSync(startCommand, { stdio: 'inherit' }); + } else { + log.info(`You can use ${startCommand} to start Logto. Happy hacking!`); + } +}; + +const downloadRelease = async () => { + const tarFilePath = path.resolve(os.tmpdir(), './logto.tar.gz'); + + log.info(`Download Logto to ${tarFilePath}`); + await downloadFile( + 'https://github.com/logto-io/logto/releases/latest/download/logto.tar.gz', + tarFilePath + ); + + return tarFilePath; +}; + +const decompress = async (toPath: string, tarPath: string) => { + const decompressSpinner = ora({ + text: `Decompress to ${toPath}`, + prefixText: chalk.blue('[info]'), + }).start(); + + try { + await mkdir(toPath); + await tar.extract({ file: tarPath, cwd: toPath, strip: 1 }); + } catch { + decompressSpinner.fail(); + + return; + } + + decompressSpinner.succeed(); +}; + +const main = async () => { + validateNodeVersion(); + + const instancePath = await getInstancePath(); + const tarPath = await downloadRelease(); + + await decompress(instancePath, tarPath); + await tryStartInstance(instancePath); +}; + +void main(); diff --git a/packages/cli/src/queries/logto-config.test.ts b/packages/cli/src/queries/logto-config.test.ts deleted file mode 100644 index 2a407122d..000000000 --- a/packages/cli/src/queries/logto-config.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { AlterationStateKey, LogtoConfigs } from '@logto/schemas'; -import { convertToIdentifiers } from '@logto/shared'; -import { createMockPool, createMockQueryResult, sql } from 'slonik'; - -import { expectSqlAssert, QueryType } from '../test-utilities'; -import { updateDatabaseTimestamp, getCurrentDatabaseAlterationTimestamp } from './logto-config'; - -const mockQuery: jest.MockedFunction = jest.fn(); - -const pool = createMockPool({ - query: async (sql, values) => { - return mockQuery(sql, values); - }, -}); -const { table, fields } = convertToIdentifiers(LogtoConfigs); -const timestamp = 1_663_923_776; - -describe('getCurrentDatabaseAlterationTimestamp()', () => { - it('returns 0 if query failed (table not found)', async () => { - mockQuery.mockRejectedValueOnce({ code: '42P01' }); - - await expect(getCurrentDatabaseAlterationTimestamp(pool)).resolves.toBe(0); - }); - - it('returns 0 if the row is not found', async () => { - const expectSql = sql` - select * from ${table} where ${fields.key}=$1 - `; - - mockQuery.mockImplementationOnce(async (sql, values) => { - expectSqlAssert(sql, expectSql.sql); - expect(values).toEqual([AlterationStateKey.AlterationState]); - - return createMockQueryResult([]); - }); - - await expect(getCurrentDatabaseAlterationTimestamp(pool)).resolves.toBe(0); - }); - - it('returns 0 if the value is in bad format', async () => { - const expectSql = sql` - select * from ${table} where ${fields.key}=$1 - `; - - mockQuery.mockImplementationOnce(async (sql, values) => { - expectSqlAssert(sql, expectSql.sql); - expect(values).toEqual([AlterationStateKey.AlterationState]); - - return createMockQueryResult([{ value: 'some_value' }]); - }); - - await expect(getCurrentDatabaseAlterationTimestamp(pool)).resolves.toBe(0); - }); - - it('returns the timestamp from database', async () => { - const expectSql = sql` - select * from ${table} where ${fields.key}=$1 - `; - - mockQuery.mockImplementationOnce(async (sql, values) => { - expectSqlAssert(sql, expectSql.sql); - expect(values).toEqual([AlterationStateKey.AlterationState]); - - // @ts-expect-error createMockQueryResult doesn't support jsonb - return createMockQueryResult([{ value: { timestamp, updatedAt: 'now' } }]); - }); - - await expect(getCurrentDatabaseAlterationTimestamp(pool)).resolves.toEqual(timestamp); - }); -}); - -describe('updateDatabaseTimestamp()', () => { - const expectSql = sql` - insert into ${table} (${fields.key}, ${fields.value}) - values ($1, $2::jsonb) - on conflict (${fields.key}) do update set ${fields.value}=excluded.${fields.value} - `; - const updatedAt = '2022-09-21T06:32:46.583Z'; - - beforeAll(() => { - jest.useFakeTimers(); - jest.setSystemTime(new Date(updatedAt)); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - - it('sends upsert sql with timestamp and updatedAt', async () => { - mockQuery.mockImplementationOnce(async (sql, values) => { - expectSqlAssert(sql, expectSql.sql); - expect(values).toEqual([ - AlterationStateKey.AlterationState, - JSON.stringify({ timestamp, updatedAt }), - ]); - - return createMockQueryResult([]); - }); - - await updateDatabaseTimestamp(pool, timestamp); - }); -}); diff --git a/packages/cli/src/queries/logto-config.ts b/packages/cli/src/queries/logto-config.ts deleted file mode 100644 index d5fd51ac9..000000000 --- a/packages/cli/src/queries/logto-config.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { - AlterationState, - LogtoConfig, - logtoConfigGuards, - LogtoConfigKey, - LogtoConfigs, - AlterationStateKey, -} from '@logto/schemas'; -import { convertToIdentifiers } from '@logto/shared'; -import { Nullable } from '@silverhand/essentials'; -import { DatabasePool, DatabaseTransactionConnection, sql } from 'slonik'; -import { z } from 'zod'; - -const { table, fields } = convertToIdentifiers(LogtoConfigs); - -export const isConfigsTableExists = async (pool: DatabasePool) => { - const { rows } = await pool.query>( - sql`select to_regclass(${LogtoConfigs.table})` - ); - - return Boolean(rows[0]); -}; - -export const getRowsByKeys = async ( - pool: DatabasePool | DatabaseTransactionConnection, - keys: LogtoConfigKey[] -) => - pool.query(sql` - select ${sql.join([fields.key, fields.value], sql`,`)} from ${table} - where ${fields.key} in (${sql.join(keys, sql`,`)}) - `); - -export const updateValueByKey = async ( - pool: DatabasePool | DatabaseTransactionConnection, - key: T, - value: z.infer -) => - pool.query( - sql` - insert into ${table} (${fields.key}, ${fields.value}) - values (${key}, ${sql.jsonb(value)}) - on conflict (${fields.key}) do update set ${fields.value}=excluded.${fields.value} - ` - ); - -export const getCurrentDatabaseAlterationTimestamp = async (pool: DatabasePool) => { - try { - const result = await pool.maybeOne( - sql`select * from ${table} where ${fields.key}=${AlterationStateKey.AlterationState}` - ); - const parsed = logtoConfigGuards[AlterationStateKey.AlterationState].safeParse(result?.value); - - return (parsed.success && parsed.data.timestamp) || 0; - } catch (error: unknown) { - const result = z.object({ code: z.string() }).safeParse(error); - - // Relation does not exist, treat as 0 - // https://www.postgresql.org/docs/14/errcodes-appendix.html - if (result.success && result.data.code === '42P01') { - return 0; - } - - throw error; - } -}; - -export const updateDatabaseTimestamp = async ( - connection: DatabaseTransactionConnection, - timestamp: number -) => { - const value: AlterationState = { - timestamp, - updatedAt: new Date().toISOString(), - }; - - return updateValueByKey(connection, AlterationStateKey.AlterationState, value); -}; diff --git a/packages/cli/src/test-utilities.ts b/packages/cli/src/test-utilities.ts deleted file mode 100644 index e03d395e8..000000000 --- a/packages/cli/src/test-utilities.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Copied from core - -import { QueryResult, QueryResultRow } from 'slonik'; -import { PrimitiveValueExpression } from 'slonik/dist/src/types.d'; - -export type QueryType = ( - sql: string, - values: readonly PrimitiveValueExpression[] -) => Promise>; - -/** - * Slonik Query Mock Utils - **/ -export const expectSqlAssert = (sql: string, expectSql: string) => { - expect( - sql - .split('\n') - .map((row) => row.trim()) - .filter(Boolean) - ).toEqual( - expectSql - .split('\n') - .map((row) => row.trim()) - .filter(Boolean) - ); -}; diff --git a/packages/cli/src/utilities.ts b/packages/cli/src/utilities.ts index 62b14efb7..f5e60060d 100644 --- a/packages/cli/src/utilities.ts +++ b/packages/cli/src/utilities.ts @@ -1,12 +1,9 @@ import { execSync } from 'child_process'; import { createWriteStream } from 'fs'; -import path from 'path'; -import { conditionalString, Optional } from '@silverhand/essentials'; import chalk from 'chalk'; import got, { Progress } from 'got'; import { HttpsProxyAgent } from 'hpagent'; -import inquirer from 'inquirer'; import ora from 'ora'; export const safeExecSync = (command: string) => { @@ -17,23 +14,19 @@ export const safeExecSync = (command: string) => { type Log = Readonly<{ info: typeof console.log; - succeed: typeof console.log; warn: typeof console.log; - error: (...args: Parameters) => never; + error: typeof console.log; }>; export const log: Log = Object.freeze({ info: (...args) => { console.log(chalk.blue('[info]'), ...args); }, - succeed: (...args) => { - log.info(chalk.green('✔'), ...args); - }, warn: (...args) => { - console.warn(chalk.yellow('[warn]'), ...args); + console.log(chalk.yellow('[warn]'), ...args); }, error: (...args) => { - console.error(chalk.red('[error]'), ...args); + console.log(chalk.red('[error]'), ...args); // eslint-disable-next-line unicorn/no-process-exit process.exit(1); }, @@ -75,107 +68,3 @@ export const downloadFile = async (url: string, destination: string) => { }); }); }; - -export const getPathInModule = (moduleName: string, relativePath = '/') => - // https://stackoverflow.com/a/49455609/12514940 - path.join( - // Until we migrate to ESM - // eslint-disable-next-line unicorn/prefer-module - path.dirname(require.resolve(`${moduleName}/package.json`)), - relativePath - ); - -export const oraPromise = async ( - promise: PromiseLike, - options?: ora.Options, - exitOnError = false -) => { - const spinner = ora(options).start(); - - try { - const result = await promise; - spinner.succeed(); - - return result; - } catch (error: unknown) { - spinner.fail(); - - if (exitOnError) { - log.error(error); - } - - throw error; - } -}; - -export enum ConfigKey { - DatabaseUrl = 'DB_URL', -} - -export const cliConfig = new Map>(); - -export type GetCliConfigWithPrompt = { - key: ConfigKey; - readableKey: string; - comments?: string; - defaultValue?: string; -}; - -export const getCliConfigWithPrompt = async ({ - key, - readableKey, - comments, - defaultValue, -}: GetCliConfigWithPrompt) => { - if (cliConfig.has(key)) { - return cliConfig.get(key); - } - - const { input } = await inquirer - .prompt<{ input?: string }>({ - type: 'input', - name: 'input', - message: `Enter your ${readableKey}${conditionalString(comments && ' ' + comments)}`, - default: defaultValue, - }) - .catch(async (error) => { - if (error.isTtyError) { - log.error(`No ${readableKey} (${chalk.green(key)}) configured in option nor env`); - } - - // The type definition does not give us type except `any`, throw it directly will honor the original behavior. - // eslint-disable-next-line @typescript-eslint/no-throw-literal - throw error; - }); - - cliConfig.set(key, input); - - return input; -}; - -// https://stackoverflow.com/a/53187807/12514940 -/** - * Returns the index of the last element in the array where predicate is true, and -1 - * otherwise. - * @param array The source array to search in - * @param predicate find calls predicate once for each element of the array, in descending - * order, until it finds one where predicate returns true. If such an element is found, - * findLastIndex immediately returns that element index. Otherwise, findLastIndex returns -1. - */ -export function findLastIndex( - array: readonly T[], - predicate: (value: T, index: number, object: readonly T[]) => boolean -): number { - // eslint-disable-next-line @silverhand/fp/no-let - let { length } = array; - - // eslint-disable-next-line @silverhand/fp/no-mutation - while (length--) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - if (predicate(array[length]!, length, array)) { - return length; - } - } - - return -1; -} diff --git a/packages/cli/tsconfig.build.json b/packages/cli/tsconfig.build.json deleted file mode 100644 index b2142cfd9..000000000 --- a/packages/cli/tsconfig.build.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./tsconfig", - "include": ["src"], -} diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index ef675dbdc..747c9b09d 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -4,12 +4,9 @@ "outDir": "lib", "declaration": true, "module": "node16", - "target": "es2022", - "types": ["node", "jest"] + "target": "es2022" }, "include": [ - "src", - "jest.config.ts" - ], - "exclude": ["**/alteration-scripts"] + "src" + ] } diff --git a/packages/cli/tsconfig.test.json b/packages/cli/tsconfig.test.json deleted file mode 100644 index c68416b04..000000000 --- a/packages/cli/tsconfig.test.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "./tsconfig", - "compilerOptions": { - "allowJs": true - } -} diff --git a/packages/console/package.json b/packages/console/package.json index af1acf98c..0a001affb 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -18,8 +18,7 @@ }, "devDependencies": { "@fontsource/roboto-mono": "^4.5.7", - "@logto/core-kit": "1.0.0-beta.18", - "@logto/language-kit": "1.0.0-beta.16", + "@logto/core-kit": "^1.0.0-beta.13", "@logto/phrases": "^1.0.0-beta.10", "@logto/phrases-ui": "^1.0.0-beta.10", "@logto/react": "1.0.0-beta.8", @@ -29,11 +28,11 @@ "@parcel/transformer-mdx": "2.7.0", "@parcel/transformer-sass": "2.7.0", "@parcel/transformer-svg-react": "2.7.0", - "@silverhand/eslint-config": "1.2.0", - "@silverhand/eslint-config-react": "1.2.1", - "@silverhand/essentials": "^1.3.0", - "@silverhand/ts-config": "1.2.1", - "@silverhand/ts-config-react": "1.2.1", + "@silverhand/eslint-config": "1.0.0", + "@silverhand/eslint-config-react": "1.0.0", + "@silverhand/essentials": "^1.2.1", + "@silverhand/ts-config": "1.0.0", + "@silverhand/ts-config-react": "1.0.0", "@tsconfig/docusaurus": "^1.0.5", "@types/color": "^3.0.3", "@types/lodash.kebabcase": "^4.1.6", @@ -44,11 +43,9 @@ "@types/react-modal": "^3.13.1", "@types/react-syntax-highlighter": "^15.5.1", "classnames": "^2.3.1", - "clean-deep": "^3.4.0", "cross-env": "^7.0.3", "csstype": "^3.0.11", "dayjs": "^1.10.5", - "deepmerge": "^4.2.2", "dnd-core": "^16.0.0", "eslint": "^8.21.0", "history": "^5.3.0", @@ -92,7 +89,8 @@ "eslintConfig": { "extends": "@silverhand/react", "rules": { - "complexity": "off" + "complexity": "off", + "@typescript-eslint/prefer-nullish-coalescing": "off" } }, "stylelint": { diff --git a/packages/console/src/assets/images/arrow-down.svg b/packages/console/src/assets/images/arrow-down.svg deleted file mode 100644 index c91bc0bba..000000000 --- a/packages/console/src/assets/images/arrow-down.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/arrow-up.svg b/packages/console/src/assets/images/arrow-up.svg deleted file mode 100644 index f26a261fb..000000000 --- a/packages/console/src/assets/images/arrow-up.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/back.svg b/packages/console/src/assets/images/back.svg deleted file mode 100644 index a0585cb15..000000000 --- a/packages/console/src/assets/images/back.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/check-box-selected-disabled-dark.svg b/packages/console/src/assets/images/check-box-selected-disabled-dark.svg deleted file mode 100644 index e910d5932..000000000 --- a/packages/console/src/assets/images/check-box-selected-disabled-dark.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/packages/console/src/assets/images/check-box-selected-disabled.svg b/packages/console/src/assets/images/check-box-selected-disabled.svg deleted file mode 100644 index ec185beb8..000000000 --- a/packages/console/src/assets/images/check-box-selected-disabled.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/packages/console/src/assets/images/check-box-selected.svg b/packages/console/src/assets/images/check-box-selected.svg deleted file mode 100644 index b74093574..000000000 --- a/packages/console/src/assets/images/check-box-selected.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/packages/console/src/assets/images/check-box-unselected-dark.svg b/packages/console/src/assets/images/check-box-unselected-dark.svg deleted file mode 100644 index da9a4cc12..000000000 --- a/packages/console/src/assets/images/check-box-unselected-dark.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/check-box-unselected-disabled-dark.svg b/packages/console/src/assets/images/check-box-unselected-disabled-dark.svg deleted file mode 100644 index eedb62d3a..000000000 --- a/packages/console/src/assets/images/check-box-unselected-disabled-dark.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - diff --git a/packages/console/src/assets/images/check-box-unselected-disabled.svg b/packages/console/src/assets/images/check-box-unselected-disabled.svg deleted file mode 100644 index b3c477b6f..000000000 --- a/packages/console/src/assets/images/check-box-unselected-disabled.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/packages/console/src/assets/images/check-box-unselected.svg b/packages/console/src/assets/images/check-box-unselected.svg deleted file mode 100644 index ed74428e8..000000000 --- a/packages/console/src/assets/images/check-box-unselected.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/circle-minus.svg b/packages/console/src/assets/images/circle-minus.svg deleted file mode 100644 index c0beab55a..000000000 --- a/packages/console/src/assets/images/circle-minus.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/circle-plus.svg b/packages/console/src/assets/images/circle-plus.svg deleted file mode 100644 index 1d2315bc1..000000000 --- a/packages/console/src/assets/images/circle-plus.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/clear.svg b/packages/console/src/assets/images/clear.svg deleted file mode 100644 index f935ea9ec..000000000 --- a/packages/console/src/assets/images/clear.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/packages/console/src/assets/images/close.svg b/packages/console/src/assets/images/close.svg deleted file mode 100644 index 8311c5ecf..000000000 --- a/packages/console/src/assets/images/close.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/connector-platform-icon-native.svg b/packages/console/src/assets/images/connector-platform-icon-native.svg deleted file mode 100644 index 673450871..000000000 --- a/packages/console/src/assets/images/connector-platform-icon-native.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/connector-platform-icon-universal.svg b/packages/console/src/assets/images/connector-platform-icon-universal.svg deleted file mode 100644 index 5a16bd3e8..000000000 --- a/packages/console/src/assets/images/connector-platform-icon-universal.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/connector-platform-icon-web.svg b/packages/console/src/assets/images/connector-platform-icon-web.svg deleted file mode 100644 index c907d3354..000000000 --- a/packages/console/src/assets/images/connector-platform-icon-web.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/copy.svg b/packages/console/src/assets/images/copy.svg deleted file mode 100644 index 88abbf65e..000000000 --- a/packages/console/src/assets/images/copy.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/delete.svg b/packages/console/src/assets/images/delete.svg deleted file mode 100644 index edd960596..000000000 --- a/packages/console/src/assets/images/delete.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/draggable.svg b/packages/console/src/assets/images/draggable.svg deleted file mode 100644 index 58e008743..000000000 --- a/packages/console/src/assets/images/draggable.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - diff --git a/packages/console/src/assets/images/eye-closed.svg b/packages/console/src/assets/images/eye-closed.svg deleted file mode 100644 index 000cd9a25..000000000 --- a/packages/console/src/assets/images/eye-closed.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/eye.svg b/packages/console/src/assets/images/eye.svg deleted file mode 100644 index d0bebbfd4..000000000 --- a/packages/console/src/assets/images/eye.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/packages/console/src/assets/images/failed.svg b/packages/console/src/assets/images/failed.svg deleted file mode 100644 index e63b99a6e..000000000 --- a/packages/console/src/assets/images/failed.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/info.svg b/packages/console/src/assets/images/info.svg deleted file mode 100644 index 6df9efe1a..000000000 --- a/packages/console/src/assets/images/info.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/packages/console/src/assets/images/keyboard-arrow-down.svg b/packages/console/src/assets/images/keyboard-arrow-down.svg deleted file mode 100644 index 7ec0838ba..000000000 --- a/packages/console/src/assets/images/keyboard-arrow-down.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/keyboard-arrow-up.svg b/packages/console/src/assets/images/keyboard-arrow-up.svg deleted file mode 100644 index 52e5b1547..000000000 --- a/packages/console/src/assets/images/keyboard-arrow-up.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/logo.svg b/packages/console/src/assets/images/logo.svg deleted file mode 100644 index 9bc40cdfd..000000000 --- a/packages/console/src/assets/images/logo.svg +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/console/src/assets/images/minus.svg b/packages/console/src/assets/images/minus.svg deleted file mode 100644 index 6176fc2b3..000000000 --- a/packages/console/src/assets/images/minus.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/more.svg b/packages/console/src/assets/images/more.svg deleted file mode 100644 index 4e0be9362..000000000 --- a/packages/console/src/assets/images/more.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/phone-info.svg b/packages/console/src/assets/images/phone-info.svg deleted file mode 100644 index 5d5da2158..000000000 --- a/packages/console/src/assets/images/phone-info.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - diff --git a/packages/console/src/assets/images/plus.svg b/packages/console/src/assets/images/plus.svg deleted file mode 100644 index e57837787..000000000 --- a/packages/console/src/assets/images/plus.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/reset.svg b/packages/console/src/assets/images/reset.svg deleted file mode 100644 index bc8063383..000000000 --- a/packages/console/src/assets/images/reset.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/search.svg b/packages/console/src/assets/images/search.svg deleted file mode 100644 index d4561c4bf..000000000 --- a/packages/console/src/assets/images/search.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/sign-out.svg b/packages/console/src/assets/images/sign-out.svg deleted file mode 100644 index 162b86bb0..000000000 --- a/packages/console/src/assets/images/sign-out.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/success.svg b/packages/console/src/assets/images/success.svg deleted file mode 100644 index 647657505..000000000 --- a/packages/console/src/assets/images/success.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/tick.svg b/packages/console/src/assets/images/tick.svg deleted file mode 100644 index 875384e88..000000000 --- a/packages/console/src/assets/images/tick.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/assets/images/tip.svg b/packages/console/src/assets/images/tip.svg deleted file mode 100644 index 01c6eb22a..000000000 --- a/packages/console/src/assets/images/tip.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/console/src/components/Alert/index.module.scss b/packages/console/src/components/Alert/index.module.scss index 5c5e0bffd..8036346ea 100644 --- a/packages/console/src/components/Alert/index.module.scss +++ b/packages/console/src/components/Alert/index.module.scss @@ -16,7 +16,7 @@ .icon { width: 20px; height: 20px; - color: var(--color-text-secondary); + color: var(--color-icon); } .content { diff --git a/packages/console/src/components/Alert/index.tsx b/packages/console/src/components/Alert/index.tsx index ac32821bb..ae71ed2c3 100644 --- a/packages/console/src/components/Alert/index.tsx +++ b/packages/console/src/components/Alert/index.tsx @@ -2,8 +2,8 @@ import { AdminConsoleKey } from '@logto/phrases'; import classNames from 'classnames'; import { ReactNode } from 'react'; -import Info from '@/assets/images/info.svg'; import LinkButton from '@/components/LinkButton'; +import Info from '@/icons/Info'; import Button from '../Button'; import * as styles from './index.module.scss'; @@ -38,7 +38,7 @@ const Alert = ({ )} {action && onClick && (
-
)} diff --git a/packages/console/src/components/AppContent/components/Sidebar/components/Contact/index.module.scss b/packages/console/src/components/AppContent/components/Sidebar/components/Contact/index.module.scss index 1a9ba4768..fc8d799bd 100644 --- a/packages/console/src/components/AppContent/components/Sidebar/components/Contact/index.module.scss +++ b/packages/console/src/components/AppContent/components/Sidebar/components/Contact/index.module.scss @@ -26,7 +26,7 @@ .description { font: var(--font-body-medium); - color: var(--color-text-secondary); + color: var(--color-caption); } } diff --git a/packages/console/src/components/AppContent/components/Topbar/index.tsx b/packages/console/src/components/AppContent/components/Topbar/index.tsx index 0478b5e7c..6bb99e2fa 100644 --- a/packages/console/src/components/AppContent/components/Topbar/index.tsx +++ b/packages/console/src/components/AppContent/components/Topbar/index.tsx @@ -1,8 +1,8 @@ import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; -import Logo from '@/assets/images/logo.svg'; import Spacer from '@/components/Spacer'; +import Logo from '@/icons/Logo'; import GetStartedProgress from '@/pages/GetStarted/components/GetStartedProgress'; import UserInfo from '../UserInfo'; diff --git a/packages/console/src/components/AppContent/components/UserInfo/index.module.scss b/packages/console/src/components/AppContent/components/UserInfo/index.module.scss index 7e5525453..e93ecff05 100644 --- a/packages/console/src/components/AppContent/components/UserInfo/index.module.scss +++ b/packages/console/src/components/AppContent/components/UserInfo/index.module.scss @@ -36,7 +36,7 @@ .role { font: var(--font-body-small); - color: var(--color-text-secondary); + color: var(--color-caption); } } } @@ -58,7 +58,7 @@ } .signOutIcon { - color: var(--color-text-secondary); + color: var(--color-icon); } .spinner { diff --git a/packages/console/src/components/AppContent/components/UserInfo/index.tsx b/packages/console/src/components/AppContent/components/UserInfo/index.tsx index e37935890..5586296a1 100644 --- a/packages/console/src/components/AppContent/components/UserInfo/index.tsx +++ b/packages/console/src/components/AppContent/components/UserInfo/index.tsx @@ -1,13 +1,12 @@ import { useLogto, IdTokenClaims } from '@logto/react'; import classNames from 'classnames'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState, MouseEvent } from 'react'; import { useTranslation } from 'react-i18next'; -import SignOut from '@/assets/images/sign-out.svg'; import Dropdown, { DropdownItem } from '@/components/Dropdown'; import { Ring as Spinner } from '@/components/Spinner'; import { generateAvatarPlaceHolderById } from '@/consts/avatars'; -import { onKeyDownHandler } from '@/utilities/a11y'; +import SignOut from '@/icons/SignOut'; import UserInfoSkeleton from '../UserInfoSkeleton'; import * as styles from './index.module.scss'; @@ -44,18 +43,13 @@ const UserInfo = () => { <>
{ - setShowDropdown(true); - })} onClick={() => { setShowDropdown(true); }} > {/* TODO: revert after SDK updated */} - avatar +
{username}
@@ -72,7 +66,7 @@ const UserInfo = () => { } - onClick={(event) => { + onClick={(event: MouseEvent) => { event.stopPropagation(); if (isLoading) { diff --git a/packages/console/src/components/AppError/index.tsx b/packages/console/src/components/AppError/index.tsx index 218b0a3d9..23bee6bfc 100644 --- a/packages/console/src/components/AppError/index.tsx +++ b/packages/console/src/components/AppError/index.tsx @@ -4,10 +4,8 @@ import { useTranslation } from 'react-i18next'; import ErrorDark from '@/assets/images/error-dark.svg'; import Error from '@/assets/images/error.svg'; -import KeyboardArrowDown from '@/assets/images/keyboard-arrow-down.svg'; -import KeyboardArrowUp from '@/assets/images/keyboard-arrow-up.svg'; import { useTheme } from '@/hooks/use-theme'; -import { onKeyDownHandler } from '@/utilities/a11y'; +import { KeyboardArrowDown, KeyboardArrowUp } from '@/icons/Arrow'; import * as styles from './index.module.scss'; @@ -35,12 +33,7 @@ const AppError = ({ title, errorCode, errorMessage, callStack, children }: Props {errorMessage} {callStack && ( { - setIsDetailsOpen(!isDetailsOpen); - })} onClick={() => { setIsDetailsOpen(!isDetailsOpen); }} diff --git a/packages/console/src/components/ApplicationName/index.tsx b/packages/console/src/components/ApplicationName/index.tsx index 895760f7a..650fac058 100644 --- a/packages/console/src/components/ApplicationName/index.tsx +++ b/packages/console/src/components/ApplicationName/index.tsx @@ -17,7 +17,7 @@ const ApplicationName = ({ applicationId, isLink = false }: Props) => { const { data } = useSWR(!isAdminConsole && `/api/applications/${applicationId}`); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const name = (isAdminConsole ? <>Admin Console ({t('system_app')}) : data?.name) ?? '-'; + const name = (isAdminConsole ? <>Admin Console ({t('system_app')}) : data?.name) || '-'; if (isLink && !isAdminConsole) { return ( diff --git a/packages/console/src/components/AuditLogTable/components/EventName/index.tsx b/packages/console/src/components/AuditLogTable/components/EventName/index.tsx index c7284ccba..f3f10c82e 100644 --- a/packages/console/src/components/AuditLogTable/components/EventName/index.tsx +++ b/packages/console/src/components/AuditLogTable/components/EventName/index.tsx @@ -1,9 +1,9 @@ import classNames from 'classnames'; import { Link } from 'react-router-dom'; -import Failed from '@/assets/images/failed.svg'; -import Success from '@/assets/images/success.svg'; import { logEventTitle } from '@/consts/logs'; +import Failed from '@/icons/Failed'; +import Success from '@/icons/Success'; import * as styles from './index.module.scss'; diff --git a/packages/console/src/components/AuditLogTable/index.module.scss b/packages/console/src/components/AuditLogTable/index.module.scss index cff0275e0..66a32c543 100644 --- a/packages/console/src/components/AuditLogTable/index.module.scss +++ b/packages/console/src/components/AuditLogTable/index.module.scss @@ -6,7 +6,7 @@ align-items: center; .title { - color: var(--color-text-secondary); + color: var(--color-caption); font: var(--font-body-medium); } diff --git a/packages/console/src/components/Button/index.module.scss b/packages/console/src/components/Button/index.module.scss index 61820ff09..d61f06112 100644 --- a/packages/console/src/components/Button/index.module.scss +++ b/packages/console/src/components/Button/index.module.scss @@ -53,8 +53,9 @@ height: 30px; padding: 0 _.unit(3); - &.text { + &.plain { height: 24px; + padding: 0; } } @@ -62,9 +63,9 @@ height: 36px; padding: 0 _.unit(4); - &.text { - font: var(--font-subhead-1); + &.plain { height: 28px; + padding: 0; } } @@ -72,17 +73,16 @@ height: 44px; padding: 0 _.unit(6); - &.text { - // same as medium - font: var(--font-subhead-1); - height: 28px; + &.plain { + height: 28px; // same as medium + padding: 0; } } &.default { background: var(--color-layer-1); color: var(--color-text); - border-color: var(--color-border); + border-color: var(--color-outline); border-width: 1px; border-style: solid; @@ -196,13 +196,11 @@ } } - &.text { + &.plain { background: none; border-color: none; - font: var(--font-label-large); + font: var(--font-body-medium); color: var(--color-text-link); - padding: _.unit(0.5) _.unit(1); - border-radius: 4px; &:disabled { color: var(--color-disabled); @@ -213,7 +211,7 @@ } &:not(:disabled):hover { - background-color: var(--color-hover-variant); + text-decoration: underline; } } } diff --git a/packages/console/src/components/Button/index.tsx b/packages/console/src/components/Button/index.tsx index f5dd61d14..9f0ee18fb 100644 --- a/packages/console/src/components/Button/index.tsx +++ b/packages/console/src/components/Button/index.tsx @@ -8,7 +8,7 @@ import { Ring as Spinner } from '@/components/Spinner'; import DangerousRaw from '../DangerousRaw'; import * as styles from './index.module.scss'; -export type ButtonType = 'primary' | 'danger' | 'outline' | 'text' | 'default' | 'branding'; +export type ButtonType = 'primary' | 'danger' | 'outline' | 'plain' | 'default' | 'branding'; type BaseProps = Omit, 'type' | 'size' | 'title'> & { htmlType?: 'button' | 'submit' | 'reset'; diff --git a/packages/console/src/components/CardTitle/index.module.scss b/packages/console/src/components/CardTitle/index.module.scss index c29c3b8c5..be5798ae4 100644 --- a/packages/console/src/components/CardTitle/index.module.scss +++ b/packages/console/src/components/CardTitle/index.module.scss @@ -10,7 +10,7 @@ .subtitle { margin-top: _.unit(1); - color: var(--color-text-secondary); + color: var(--color-caption); } &.large { diff --git a/packages/console/src/components/Checkbox/Icon.tsx b/packages/console/src/components/Checkbox/Icon.tsx index f4f538609..bac157b6f 100644 --- a/packages/console/src/components/Checkbox/Icon.tsx +++ b/packages/console/src/components/Checkbox/Icon.tsx @@ -1,13 +1,13 @@ import { AppearanceMode } from '@logto/schemas'; -import CheckBoxSelectedDisabledDark from '@/assets/images/check-box-selected-disabled-dark.svg'; -import CheckBoxSelectedDisabled from '@/assets/images/check-box-selected-disabled.svg'; -import CheckBoxSelected from '@/assets/images/check-box-selected.svg'; -import CheckBoxUnselectedDark from '@/assets/images/check-box-unselected-dark.svg'; -import CheckBoxUnselectedDisabledDark from '@/assets/images/check-box-unselected-disabled-dark.svg'; -import CheckBoxUnselectedDisabled from '@/assets/images/check-box-unselected-disabled.svg'; -import CheckBoxUnselected from '@/assets/images/check-box-unselected.svg'; import { useTheme } from '@/hooks/use-theme'; +import CheckBoxSelected from '@/icons/CheckBoxSelected'; +import CheckBoxSelectedDisabled from '@/icons/CheckBoxSelectedDisabled'; +import CheckBoxSelectedDisabledDark from '@/icons/CheckBoxSelectedDisabledDark'; +import CheckBoxUnselected from '@/icons/CheckBoxUnselected'; +import CheckBoxUnselectedDark from '@/icons/CheckBoxUnselectedDark'; +import CheckBoxUnselectedDisabled from '@/icons/CheckBoxUnselectedDisabled'; +import CheckBoxUnselectedDisabledDark from '@/icons/CheckBoxUnselectedDisabledDark'; type Props = { className?: string; diff --git a/packages/console/src/components/CopyToClipboard/index.module.scss b/packages/console/src/components/CopyToClipboard/index.module.scss index ae68d6b67..542976b50 100644 --- a/packages/console/src/components/CopyToClipboard/index.module.scss +++ b/packages/console/src/components/CopyToClipboard/index.module.scss @@ -33,11 +33,6 @@ .copyIcon { margin-left: _.unit(3); - - svg { - width: 16px; - height: 16px; - } } } } diff --git a/packages/console/src/components/CopyToClipboard/index.tsx b/packages/console/src/components/CopyToClipboard/index.tsx index fd1b0aef8..9fb8b7d89 100644 --- a/packages/console/src/components/CopyToClipboard/index.tsx +++ b/packages/console/src/components/CopyToClipboard/index.tsx @@ -2,10 +2,9 @@ import classNames from 'classnames'; import { MouseEventHandler, useEffect, useMemo, useRef, useState } from 'react'; import { TFuncKey, useTranslation } from 'react-i18next'; -import Copy from '@/assets/images/copy.svg'; -import EyeClosed from '@/assets/images/eye-closed.svg'; -import Eye from '@/assets/images/eye.svg'; -import { onKeyDownHandler } from '@/utilities/a11y'; +import Copy from '@/icons/Copy'; +import Eye from '@/icons/Eye'; +import EyeClosed from '@/icons/EyeClosed'; import IconButton from '../IconButton'; import Tooltip from '../Tooltip'; @@ -58,11 +57,6 @@ const CopyToClipboard = ({ return (
{ - event.stopPropagation(); - })} onClick={(event) => { event.stopPropagation(); }} diff --git a/packages/console/src/components/DeleteConfirmModal/index.tsx b/packages/console/src/components/DeleteConfirmModal/index.tsx index d0471158c..6772d6872 100644 --- a/packages/console/src/components/DeleteConfirmModal/index.tsx +++ b/packages/console/src/components/DeleteConfirmModal/index.tsx @@ -40,7 +40,6 @@ const DeleteConfirmModal = ({ {children} {expectedInput && ( { return ( | KeyboardEvent) => void; + onClick?: (event: MouseEvent) => void; className?: string; children: ReactNode | Record; icon?: ReactNode; @@ -22,13 +20,7 @@ const DropdownItem = ({ iconClassName, type = 'default', }: Props) => ( -
  • +
  • {icon && {icon}} {children}
  • diff --git a/packages/console/src/components/Dropdown/index.tsx b/packages/console/src/components/Dropdown/index.tsx index ddb0a10bf..30c10056f 100644 --- a/packages/console/src/components/Dropdown/index.tsx +++ b/packages/console/src/components/Dropdown/index.tsx @@ -3,7 +3,6 @@ import { ReactNode, RefObject, useRef } from 'react'; import ReactModal from 'react-modal'; import usePosition, { HorizontalAlignment } from '@/hooks/use-position'; -import { onKeyDownHandler } from '@/utilities/a11y'; import * as styles from './index.module.scss'; @@ -62,13 +61,7 @@ const Dropdown = ({ >
    {title &&
    {title}
    } -
      +
        {children}
    diff --git a/packages/console/src/components/FormField/index.module.scss b/packages/console/src/components/FormField/index.module.scss index 3fd003454..fe1794d95 100644 --- a/packages/console/src/components/FormField/index.module.scss +++ b/packages/console/src/components/FormField/index.module.scss @@ -20,11 +20,11 @@ margin-left: _.unit(1); width: 16px; height: 16px; - color: var(--color-text-secondary); + color: var(--color-caption); } .required { font: var(--font-body-medium); - color: var(--color-text-secondary); + color: var(--color-caption); } } diff --git a/packages/console/src/components/FormField/index.tsx b/packages/console/src/components/FormField/index.tsx index 19baebc6b..f5f4c9965 100644 --- a/packages/console/src/components/FormField/index.tsx +++ b/packages/console/src/components/FormField/index.tsx @@ -3,7 +3,7 @@ import classNames from 'classnames'; import { ReactElement, ReactNode, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import Tip from '@/assets/images/tip.svg'; +import Tip from '@/icons/Tip'; import DangerousRaw from '../DangerousRaw'; import Spacer from '../Spacer'; diff --git a/packages/console/src/components/IconButton/index.module.scss b/packages/console/src/components/IconButton/index.module.scss index f77264270..a9c6db380 100644 --- a/packages/console/src/components/IconButton/index.module.scss +++ b/packages/console/src/components/IconButton/index.module.scss @@ -15,7 +15,7 @@ align-items: center; > svg { - color: var(--color-text-secondary); + color: var(--color-caption); } &:disabled { diff --git a/packages/console/src/components/IconButton/index.tsx b/packages/console/src/components/IconButton/index.tsx index 14113cde2..60dc4e313 100644 --- a/packages/console/src/components/IconButton/index.tsx +++ b/packages/console/src/components/IconButton/index.tsx @@ -1,49 +1,18 @@ -import { AdminConsoleKey } from '@logto/phrases'; -import { Nullable } from '@silverhand/essentials'; import classNames from 'classnames'; -import { ForwardedRef, forwardRef, HTMLProps, useImperativeHandle, useRef } from 'react'; -import { useTranslation } from 'react-i18next'; +import { HTMLProps } from 'react'; -import Tooltip from '../Tooltip'; import * as styles from './index.module.scss'; export type Props = Omit, 'size' | 'type'> & { size?: 'small' | 'medium' | 'large'; - tooltip?: AdminConsoleKey; }; -const IconButton = ( - { size = 'medium', children, className, tooltip, ...rest }: Props, - reference: ForwardedRef -) => { - const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const innerReference = useRef(null); - - useImperativeHandle, Nullable>( - reference, - () => innerReference.current - ); - +const IconButton = ({ size = 'medium', children, className, ...rest }: Props) => { return ( - <> - - {tooltip && ( - - )} - + ); }; -export default forwardRef(IconButton); +export default IconButton; diff --git a/packages/console/src/components/Index/index.tsx b/packages/console/src/components/Index/index.tsx index 35554eaf4..2a33a2a7d 100644 --- a/packages/console/src/components/Index/index.tsx +++ b/packages/console/src/components/Index/index.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; -import Tick from '@/assets/images/tick.svg'; +import Tick from '@/icons/Tick'; import * as styles from './index.module.scss'; diff --git a/packages/console/src/components/ItemPreview/index.module.scss b/packages/console/src/components/ItemPreview/index.module.scss index e8f53fa71..f1a89c1fe 100644 --- a/packages/console/src/components/ItemPreview/index.module.scss +++ b/packages/console/src/components/ItemPreview/index.module.scss @@ -21,7 +21,7 @@ .subtitle { font: var(--font-body-small); - color: var(--color-text-secondary); + color: var(--color-outline); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/packages/console/src/components/Markdown/index.module.scss b/packages/console/src/components/Markdown/index.module.scss index 86e0e268a..41f7efdd8 100644 --- a/packages/console/src/components/Markdown/index.module.scss +++ b/packages/console/src/components/Markdown/index.module.scss @@ -47,7 +47,7 @@ h2 { font: var(--font-title-medium); - color: var(--color-text-secondary); + color: var(--color-caption); margin: _.unit(6) 0 _.unit(3); } diff --git a/packages/console/src/components/ModalLayout/index.module.scss b/packages/console/src/components/ModalLayout/index.module.scss index 76777ddbe..1e1abf9a5 100644 --- a/packages/console/src/components/ModalLayout/index.module.scss +++ b/packages/console/src/components/ModalLayout/index.module.scss @@ -19,7 +19,7 @@ margin-bottom: _.unit(6); .closeIcon { - color: var(--color-text-secondary); + color: var(--color-icon); } } diff --git a/packages/console/src/components/ModalLayout/index.tsx b/packages/console/src/components/ModalLayout/index.tsx index 39b431671..cf6105507 100644 --- a/packages/console/src/components/ModalLayout/index.tsx +++ b/packages/console/src/components/ModalLayout/index.tsx @@ -2,7 +2,7 @@ import { AdminConsoleKey } from '@logto/phrases'; import classNames from 'classnames'; import { ReactNode } from 'react'; -import Close from '@/assets/images/close.svg'; +import Close from '@/icons/Close'; import Card from '../Card'; import CardTitle from '../CardTitle'; diff --git a/packages/console/src/components/MultiTextInput/index.module.scss b/packages/console/src/components/MultiTextInput/index.module.scss index d70a04d68..8e7cf2fb0 100644 --- a/packages/console/src/components/MultiTextInput/index.module.scss +++ b/packages/console/src/components/MultiTextInput/index.module.scss @@ -17,7 +17,7 @@ } .minusIcon { - color: var(--color-text-secondary); + color: var(--color-icon); } .addAnother { diff --git a/packages/console/src/components/MultiTextInput/index.tsx b/packages/console/src/components/MultiTextInput/index.tsx index e3b28690b..bfe003174 100644 --- a/packages/console/src/components/MultiTextInput/index.tsx +++ b/packages/console/src/components/MultiTextInput/index.tsx @@ -1,10 +1,11 @@ import { AdminConsoleKey } from '@logto/phrases'; +import classNames from 'classnames'; import { KeyboardEvent, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import Minus from '@/assets/images/minus.svg'; +import * as textButtonStyles from '@/components/TextButton/index.module.scss'; +import Minus from '@/icons/Minus'; -import Button from '../Button'; import ConfirmModal from '../ConfirmModal'; import IconButton from '../IconButton'; import TextInput from '../TextInput'; @@ -84,13 +85,9 @@ const MultiTextInput = ({ title, value, onChange, onKeyPress, error, placeholder )}
    ))} -
    ); diff --git a/packages/console/src/components/Select/index.module.scss b/packages/console/src/components/Select/index.module.scss index cd31d4f5f..89335599a 100644 --- a/packages/console/src/components/Select/index.module.scss +++ b/packages/console/src/components/Select/index.module.scss @@ -42,7 +42,7 @@ .icon { display: flex; margin-left: _.unit(3); - color: var(--color-text-secondary); + color: var(--color-icon); } .clear { diff --git a/packages/console/src/components/Select/index.tsx b/packages/console/src/components/Select/index.tsx index 2ca246f3b..c1dbe5ce0 100644 --- a/packages/console/src/components/Select/index.tsx +++ b/packages/console/src/components/Select/index.tsx @@ -1,10 +1,8 @@ import classNames from 'classnames'; import { ReactEventHandler, ReactNode, useRef, useState } from 'react'; -import Close from '@/assets/images/close.svg'; -import KeyboardArrowDown from '@/assets/images/keyboard-arrow-down.svg'; -import KeyboardArrowUp from '@/assets/images/keyboard-arrow-up.svg'; -import { onKeyDownHandler } from '@/utilities/a11y'; +import { KeyboardArrowDown, KeyboardArrowUp } from '@/icons/Arrow'; +import Close from '@/icons/Close'; import Dropdown, { DropdownItem } from '../Dropdown'; import IconButton from '../IconButton'; @@ -67,12 +65,6 @@ const Select = ({ className )} role="button" - tabIndex={0} - onKeyDown={onKeyDownHandler(() => { - if (!isReadOnly) { - setIsOpen(true); - } - })} onClick={() => { if (!isReadOnly) { setIsOpen(true); diff --git a/packages/console/src/components/TabNav/TabNavItem.module.scss b/packages/console/src/components/TabNav/TabNavItem.module.scss index 36f053a0c..78d4e907e 100644 --- a/packages/console/src/components/TabNav/TabNavItem.module.scss +++ b/packages/console/src/components/TabNav/TabNavItem.module.scss @@ -9,7 +9,7 @@ a { display: inline-block; - color: var(--color-text-secondary); + color: var(--color-caption); text-decoration: none; cursor: pointer; padding-bottom: _.unit(1); diff --git a/packages/console/src/components/TabNav/TabNavItem.tsx b/packages/console/src/components/TabNav/TabNavItem.tsx index 77214ff78..e7cf06289 100644 --- a/packages/console/src/components/TabNav/TabNavItem.tsx +++ b/packages/console/src/components/TabNav/TabNavItem.tsx @@ -1,8 +1,6 @@ import classNames from 'classnames'; import { Link, useLocation } from 'react-router-dom'; -import { onKeyDownHandler } from '@/utilities/a11y'; - import * as styles from './TabNavItem.module.scss'; type Props = { @@ -18,14 +16,7 @@ const TabNavItem = ({ children, href, isActive, onClick }: Props) => { return (
    - {href ? ( - {children} - ) : ( - // eslint-disable-next-line jsx-a11y/anchor-is-valid - - {children} - - )} + {href ? {children} : {children}}
    ); }; diff --git a/packages/console/src/components/TextButton/index.module.scss b/packages/console/src/components/TextButton/index.module.scss new file mode 100644 index 000000000..35fa5c37d --- /dev/null +++ b/packages/console/src/components/TextButton/index.module.scss @@ -0,0 +1,23 @@ +@use '@/scss/underscore' as _; + +.button { + display: inline-block; + font: var(--font-body-medium); + color: var(--color-text-link); + padding: _.unit(0.5) _.unit(1); + border-radius: _.unit(1); + text-decoration: none; + cursor: pointer; + + svg { + color: var(--color-primary); + } + + &:hover { + text-decoration: underline; + } + + &:focus { + outline: 2px solid var(--color-focused-variant); + } +} diff --git a/packages/console/src/components/TextInput/index.module.scss b/packages/console/src/components/TextInput/index.module.scss index c9867d40f..7750d3402 100644 --- a/packages/console/src/components/TextInput/index.module.scss +++ b/packages/console/src/components/TextInput/index.module.scss @@ -36,7 +36,7 @@ padding: 0; &::placeholder { - color: var(--color-placeholder); + color: var(--color-caption); } // Overwrite webkit auto-fill style @@ -51,7 +51,7 @@ &::-webkit-calendar-picker-indicator { background-image: none; - background-color: var(--color-text-secondary); + background-color: var(--color-icon); mask-image: url('../../assets/images/calendar.png'); mask-size: 20px 20px; width: 16px; @@ -62,7 +62,7 @@ &.disabled { background: var(--color-inverse-on-surface); - color: var(--color-text-secondary); + color: var(--color-caption); border-color: var(--color-border); } diff --git a/packages/console/src/components/Textarea/index.module.scss b/packages/console/src/components/Textarea/index.module.scss deleted file mode 100644 index 330348fda..000000000 --- a/packages/console/src/components/Textarea/index.module.scss +++ /dev/null @@ -1,29 +0,0 @@ -@use '@/scss/underscore' as _; - -.container { - border-radius: 6px; - border: 1px solid var(--color-border); - outline: 3px solid transparent; - padding: _.unit(2) _.unit(3); - - &:focus-within { - border-color: var(--color-primary); - outline-color: var(--color-focused-variant); - } - - textarea { - width: 100%; - height: 100%; - color: var(--color-text); - font: var(--font-body-medium); - background: transparent; - border: none; - outline: none; - resize: none; - padding: 0; - - &::placeholder { - color: var(--color-caption); - } - } -} diff --git a/packages/console/src/components/Textarea/index.tsx b/packages/console/src/components/Textarea/index.tsx deleted file mode 100644 index 0a6f40738..000000000 --- a/packages/console/src/components/Textarea/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import classNames from 'classnames'; -import { ForwardedRef, forwardRef, HTMLProps } from 'react'; - -import * as styles from './index.module.scss'; - -type Props = HTMLProps & { - className?: string; -}; - -const Textarea = ({ className, ...rest }: Props, reference: ForwardedRef) => { - return ( -
    -