├── .husky
├── commit-msg
└── pre-commit
├── pnpm-workspace.yaml
├── .npmrc
├── .prettierignore
├── packages
├── node-utils
│ ├── src
│ │ ├── index.ts
│ │ ├── fs.ts
│ │ └── pkg.ts
│ ├── tsup.config.ts
│ ├── package.json
│ ├── test
│ │ ├── pkg.spec.ts
│ │ └── bundle.spec.ts
│ ├── README.md
│ └── CHANGELOG.md
├── eslint-config
│ ├── src
│ │ ├── env.d.ts
│ │ ├── configs
│ │ │ ├── regexp.ts
│ │ │ ├── jsx.ts
│ │ │ ├── next.ts
│ │ │ ├── node.ts
│ │ │ ├── imports.ts
│ │ │ ├── markdown.ts
│ │ │ ├── react.ts
│ │ │ ├── typescript.ts
│ │ │ ├── javascript.ts
│ │ │ ├── unicorn.ts
│ │ │ └── vue.ts
│ │ ├── index.ts
│ │ ├── shared
│ │ │ ├── utils.ts
│ │ │ └── prettier-config.mjs
│ │ ├── types.ts
│ │ ├── globs.ts
│ │ ├── private-configs
│ │ │ ├── tailwindcss.ts
│ │ │ └── prettier.ts
│ │ └── define.ts
│ ├── tsup.config.ts
│ ├── CHANGELOG.md
│ ├── package.json
│ └── README.zh-CN.md
├── bassist
│ ├── src
│ │ └── index.ts
│ ├── README.md
│ └── package.json
├── tsconfig
│ ├── node.json
│ ├── web.json
│ ├── package.json
│ ├── CHANGELOG.md
│ ├── base.json
│ └── README.md
├── build-config
│ ├── CHANGELOG.md
│ ├── tsup.config.ts
│ ├── package.json
│ ├── README.zh-CN.md
│ ├── README.md
│ └── src
│ │ └── tsup.ts
├── changelog
│ ├── tsup.config.ts
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ └── README.md
├── release
│ ├── tsup.config.ts
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── src
│ │ ├── utils.ts
│ │ └── index.ts
│ └── README.md
├── utils
│ ├── test
│ │ ├── appearance.spec.ts
│ │ ├── device.spec.ts
│ │ ├── runtime.spec.ts
│ │ ├── random.spec.ts
│ │ ├── file.spec.ts
│ │ ├── storage.spec.ts
│ │ ├── clipboard.spec.ts
│ │ ├── index.html
│ │ ├── query.spec.ts
│ │ ├── performance.spec.ts
│ │ ├── regexp.spec.ts
│ │ ├── data.spec.ts
│ │ └── format.spec.ts
│ ├── tsup.config.ts
│ ├── src
│ │ ├── index.ts
│ │ ├── storage
│ │ │ ├── index.ts
│ │ │ ├── fallback.ts
│ │ │ └── base.ts
│ │ ├── file.ts
│ │ ├── clipboard
│ │ │ ├── fallback.ts
│ │ │ └── index.ts
│ │ ├── runtime.ts
│ │ ├── performance.ts
│ │ ├── random.ts
│ │ ├── appearance.ts
│ │ ├── regexp.ts
│ │ ├── query.ts
│ │ ├── device.ts
│ │ ├── load.ts
│ │ ├── data.ts
│ │ └── format.ts
│ ├── package.json
│ └── README.md
└── progress
│ ├── tsup.config.ts
│ ├── package.json
│ ├── src
│ ├── index.ts
│ └── types.ts
│ ├── CHANGELOG.md
│ └── README.md
├── assets
└── bassist.jpg
├── commitlint.config.js
├── .prettierrc.mjs
├── vitest.config.ts
├── scripts
├── build.ts
├── publish.ts
├── changelog.cts
└── utils.ts
├── .editorconfig
├── README.md
├── .gitignore
├── eslint.config.ts
├── turbo.json
├── tsconfig.json
├── LICENSE
├── .vscode
└── settings.json
└── package.json
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | pnpm exec commitlint --edit $1
2 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - packages/*
3 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | pnpm exec lint-staged --concurrent false
2 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | shamefully-hoist=true
2 | auto-install-peers=true
3 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | lib
3 | types
4 | CHANGELOG.md
5 |
--------------------------------------------------------------------------------
/packages/node-utils/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './fs'
2 | export * from './pkg'
3 |
--------------------------------------------------------------------------------
/assets/bassist.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chengpeiquan/bassist/HEAD/assets/bassist.jpg
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | extends: ['@commitlint/config-conventional'],
3 | }
4 |
--------------------------------------------------------------------------------
/packages/eslint-config/src/env.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'eslint-plugin-import'
2 | declare module '@next/eslint-plugin-next'
3 |
--------------------------------------------------------------------------------
/packages/bassist/src/index.ts:
--------------------------------------------------------------------------------
1 | export default function hello() {
2 | console.log('I play bass, so enjoy the name.')
3 | }
4 |
--------------------------------------------------------------------------------
/.prettierrc.mjs:
--------------------------------------------------------------------------------
1 | import prettierConfig from './packages/eslint-config/src/shared/prettier-config.mjs'
2 |
3 | export default prettierConfig
4 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config'
2 |
3 | export default defineConfig({
4 | test: {
5 | // ...
6 | },
7 | })
8 |
--------------------------------------------------------------------------------
/packages/bassist/README.md:
--------------------------------------------------------------------------------
1 | # bassist
2 |
3 | I play bass, so enjoy the name.
4 |
5 | ## License
6 |
7 | MIT License © 2022-PRESENT [chengpeiquan](https://github.com/chengpeiquan)
8 |
--------------------------------------------------------------------------------
/packages/tsconfig/node.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "./base.json",
4 | "display": "Node",
5 | "compilerOptions": {
6 | "types": ["node"]
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/node-utils/src/fs.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * On the `fse` variable, the `fs-extra` APIs are provided.
3 | *
4 | * @category Fs
5 | * @see https://github.com/jprichardson/node-fs-extra
6 | */
7 | export * as fse from 'fs-extra/esm'
8 |
--------------------------------------------------------------------------------
/packages/tsconfig/web.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "./base.json",
4 | "display": "Web",
5 | "compilerOptions": {
6 | "lib": ["ESNext", "DOM", "DOM.Iterable"]
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/build-config/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 0.1.0 (2025-07-06)
2 |
3 |
4 | ### Features
5 |
6 | * **build-config:** add tsup configuration utilities ([b9ce9df](https://github.com/chengpeiquan/bassist/commit/b9ce9df24cec3ba55f52f7469a07a7d29e229c66))
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/packages/changelog/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { createBaseConfig } from '@packages/build-config/src/tsup'
2 | import { defineConfig } from 'tsup'
3 | import pkg from './package.json'
4 |
5 | const config = createBaseConfig({ pkg })
6 |
7 | export default defineConfig(config)
8 |
--------------------------------------------------------------------------------
/packages/node-utils/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { createBaseConfig } from '@packages/build-config/src/tsup'
2 | import { defineConfig } from 'tsup'
3 | import pkg from './package.json'
4 |
5 | const config = createBaseConfig({ pkg })
6 |
7 | export default defineConfig(config)
8 |
--------------------------------------------------------------------------------
/packages/release/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { createBaseConfig } from '@packages/build-config/src/tsup'
2 | import { defineConfig } from 'tsup'
3 | import pkg from './package.json'
4 |
5 | const config = createBaseConfig({ pkg })
6 |
7 | export default defineConfig(config)
8 |
--------------------------------------------------------------------------------
/packages/utils/test/appearance.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 | import { isDark, isLight } from '..'
3 |
4 | describe('appearance', () => {
5 | it('Invalid data', () => {
6 | expect(isDark()).toBeFalsy()
7 | expect(isLight()).toBeFalsy()
8 | })
9 | })
10 |
--------------------------------------------------------------------------------
/packages/build-config/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup'
2 | import pkg from './package.json'
3 | import { createBaseConfig } from './src/tsup'
4 |
5 | const config = createBaseConfig({
6 | pkg,
7 | entry: {
8 | tsup: 'src/tsup.ts',
9 | },
10 | })
11 |
12 | export default defineConfig(config)
13 |
--------------------------------------------------------------------------------
/packages/utils/test/device.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 | import { isBrowser, isServer } from '..'
3 |
4 | describe('device', () => {
5 | it('Valid data', () => {
6 | expect(isServer).toBeTruthy()
7 | })
8 | it('Invalid data', () => {
9 | expect(isBrowser).toBeFalsy()
10 | })
11 | })
12 |
--------------------------------------------------------------------------------
/scripts/build.ts:
--------------------------------------------------------------------------------
1 | import { execSync } from 'child_process'
2 | import { getArgv } from './utils'
3 |
4 | async function run() {
5 | const { name } = getArgv()
6 |
7 | execSync(`turbo run build --filter @bassist/${name}`, {
8 | stdio: 'inherit',
9 | })
10 | }
11 |
12 | run().catch((e) => {
13 | console.log(e)
14 | })
15 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_size = 2
8 | indent_style = space
9 | insert_final_newline = true
10 | max_line_length = 80
11 | trim_trailing_whitespace = true
12 |
13 | [*.md]
14 | max_line_length = 0
15 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/packages/progress/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { BundleFormat, createBaseConfig } from '@packages/build-config/src/tsup'
2 | import { defineConfig } from 'tsup'
3 | import pkg from './package.json'
4 |
5 | const config = createBaseConfig({
6 | pkg,
7 | format: [BundleFormat.ESM, BundleFormat.CJS, BundleFormat.IIFE],
8 | })
9 |
10 | export default defineConfig(config)
11 |
--------------------------------------------------------------------------------
/packages/utils/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { BundleFormat, createBaseConfig } from '@packages/build-config/src/tsup'
2 | import { defineConfig } from 'tsup'
3 | import pkg from './package.json'
4 |
5 | const config = createBaseConfig({
6 | pkg,
7 | format: [BundleFormat.ESM, BundleFormat.CJS, BundleFormat.IIFE],
8 | })
9 |
10 | export default defineConfig(config)
11 |
--------------------------------------------------------------------------------
/packages/eslint-config/src/configs/regexp.ts:
--------------------------------------------------------------------------------
1 | import { configs } from 'eslint-plugin-regexp'
2 | import { getConfigName } from '../shared/utils'
3 | import { type FlatESLintConfig } from '../types'
4 |
5 | const recommendedConfigs = configs['flat/recommended']
6 |
7 | export const regexp: FlatESLintConfig[] = [
8 | {
9 | name: getConfigName('regexp'),
10 | ...recommendedConfigs,
11 | },
12 | ]
13 |
--------------------------------------------------------------------------------
/packages/eslint-config/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { createBaseConfig } from '@packages/build-config/src/tsup'
2 | import { defineConfig } from 'tsup'
3 | import pkg from './package.json'
4 |
5 | const config = createBaseConfig({
6 | pkg,
7 | entry: {
8 | index: 'src/index.ts',
9 | 'prettier-config': 'src/shared/prettier-config.mjs',
10 | },
11 | })
12 |
13 | export default defineConfig(config)
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bassist
2 |
3 | I play bass, so enjoy the name.
4 |
5 | This is my commonly used tool chain and configuration, which is stable and continuously maintained and updated.
6 |
7 | You can also use them directly.
8 |
9 |
10 |
11 |
12 |
13 | ## License
14 |
15 | MIT License © 2022 [chengpeiquan](https://github.com/chengpeiquan)
16 |
--------------------------------------------------------------------------------
/packages/utils/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './clipboard'
2 | export * from './storage'
3 | export * from './appearance'
4 | export * from './data'
5 | export * from './device'
6 | export * from './file'
7 | export * from './format'
8 | export * from './load'
9 | export * from './performance'
10 | export * from './query'
11 | export * from './random'
12 | export * from './regexp'
13 | export * from './runtime'
14 | export * from './ua'
15 |
--------------------------------------------------------------------------------
/packages/eslint-config/src/configs/jsx.ts:
--------------------------------------------------------------------------------
1 | import { GLOB_JSX, GLOB_TSX } from '../globs'
2 | import { getConfigName } from '../shared/utils'
3 | import { type FlatESLintConfig } from '../types'
4 |
5 | export const jsx: FlatESLintConfig[] = [
6 | {
7 | name: getConfigName('jsx'),
8 | files: [GLOB_JSX, GLOB_TSX],
9 | languageOptions: {
10 | parserOptions: {
11 | ecmaFeatures: {
12 | jsx: true,
13 | },
14 | },
15 | },
16 | },
17 | ]
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # log files
2 | yarn.lock
3 | package-lock.json
4 | examples/*/pnpm-lock.yaml
5 |
6 | # build output
7 | dist
8 |
9 | # cache files
10 | .turbo
11 |
12 | # dependencies
13 | node_modules
14 |
15 | # logs
16 | *.log
17 | npm-debug.log*
18 | yarn-debug.log*
19 | yarn-error.log*
20 | pnpm-debug.log*
21 |
22 |
23 | # environment variables
24 | .env
25 | .env.production
26 |
27 | # macOS-specific files
28 | .DS_Store
29 |
30 | # others
31 | .nyc_output
32 | coverage
33 | demo
34 | *.rar
35 | *.zip
36 |
--------------------------------------------------------------------------------
/packages/eslint-config/src/configs/next.ts:
--------------------------------------------------------------------------------
1 | import nextPlugin from '@next/eslint-plugin-next'
2 | import { getConfigName } from '../shared/utils'
3 | import { type FlatESLintConfig } from '../types'
4 |
5 | export { nextPlugin }
6 |
7 | export const next: FlatESLintConfig[] = [
8 | {
9 | name: getConfigName('next'),
10 | plugins: {
11 | '@next/next': nextPlugin,
12 | },
13 | rules: {
14 | ...nextPlugin.configs.recommended.rules,
15 | ...nextPlugin.configs['core-web-vitals'].rules,
16 | },
17 | },
18 | ]
19 |
--------------------------------------------------------------------------------
/packages/utils/test/runtime.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 | import {
3 | getRuntimeEnv,
4 | isDevRuntime,
5 | isProdRuntime,
6 | isTestRuntime,
7 | runtimeEnv,
8 | } from '..'
9 |
10 | describe('runtime', () => {
11 | it('Valid data', () => {
12 | expect(runtimeEnv).toBe('test')
13 | expect(getRuntimeEnv()).toBe('test')
14 | expect(isTestRuntime).toBeTruthy()
15 | })
16 | it('Invalid data', () => {
17 | expect(isDevRuntime).toBeFalsy()
18 | expect(isProdRuntime).toBeFalsy()
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/packages/utils/src/storage/index.ts:
--------------------------------------------------------------------------------
1 | import { BaseStorage } from './base'
2 |
3 | /**
4 | * LocalStorage that supports prefixes
5 | *
6 | * @category Storage
7 | */
8 | export class LocalStorage extends BaseStorage {
9 | constructor(prefix: string) {
10 | super(prefix, 'localStorage')
11 | }
12 | }
13 |
14 | /**
15 | * SessionStorage that supports prefixes
16 | *
17 | * @category Storage
18 | */
19 | export class SessionStorage extends BaseStorage {
20 | constructor(prefix: string) {
21 | super(prefix, 'sessionStorage')
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/eslint.config.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createGetConfigNameFactory,
3 | defineFlatConfig,
4 | imports,
5 | markdown,
6 | node,
7 | typescript,
8 | } from './packages/eslint-config/src'
9 |
10 | const getConfigName = createGetConfigNameFactory('@bassist/monorepo')
11 |
12 | export default defineFlatConfig(
13 | [
14 | ...imports,
15 | ...typescript,
16 | ...markdown,
17 | ...node,
18 |
19 | {
20 | name: getConfigName('ignore'),
21 | ignores: ['**/dist/**', '**/.build/**', '**/CHANGELOG.md'],
22 | },
23 | ],
24 | { tailwindcssEnabled: false },
25 | )
26 |
--------------------------------------------------------------------------------
/packages/utils/test/random.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 | import { inRange as isInRange, randomNumber, randomUserAgent } from '..'
3 |
4 | console.log(randomUserAgent())
5 | console.log(randomUserAgent())
6 | console.log(randomUserAgent())
7 |
8 | describe('randomNumber', () => {
9 | function inRange(value: number, min: number, max: number) {
10 | return isInRange({ num: value, min, max })
11 | }
12 |
13 | it('Valid data', () => {
14 | expect(inRange(randomNumber(), 0, 100)).toBeTruthy()
15 | expect(inRange(randomNumber(-100, -50), -100, -50)).toBeTruthy()
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/packages/tsconfig/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@bassist/tsconfig",
3 | "version": "0.1.1",
4 | "description": "Some TSConfig files for working with TypeScript projects by @chengpeiquan .",
5 | "author": "chengpeiquan ",
6 | "license": "MIT",
7 | "homepage": "https://github.com/chengpeiquan/bassist/tree/main/packages/tsconfig",
8 | "files": [
9 | "base.json",
10 | "web.json",
11 | "node.json"
12 | ],
13 | "main": "./base.json",
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/chengpeiquan/bassist.git"
17 | },
18 | "keywords": [
19 | "tsconfig"
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/scripts/publish.ts:
--------------------------------------------------------------------------------
1 | import { execSync } from 'child_process'
2 | import { getArgv } from './utils'
3 |
4 | async function run() {
5 | const { name, otp, tag } = getArgv()
6 |
7 | const publishArgs = [
8 | `pnpm --filter ${name} publish`,
9 | `--no-git-checks`,
10 | `--access public`,
11 | `${tag ? `--tag ${tag}` : ''}`,
12 | `${otp ? `--otp=${otp}` : ''}`,
13 | `--registry https://registry.npmjs.org/`,
14 | ].filter(Boolean)
15 |
16 | const commands = [`pnpm build:lib ${name}`, publishArgs.join(' ')]
17 | const cmd = commands.join(' && ')
18 | execSync(cmd)
19 | }
20 |
21 | run().catch((e) => {
22 | console.log(e)
23 | })
24 |
--------------------------------------------------------------------------------
/scripts/changelog.cts:
--------------------------------------------------------------------------------
1 | import { execSync } from 'child_process'
2 | import { resolve } from 'path'
3 | import { getArgv } from './utils'
4 |
5 | async function run() {
6 | const { name } = getArgv()
7 | const pkgPath = resolve(__dirname, `../packages/${name}`)
8 |
9 | const changelogArgs = [
10 | `conventional-changelog`,
11 | `--lerna-package ${name}`,
12 | `-p angular`,
13 | `-i CHANGELOG.md`,
14 | `-s`,
15 | `--commit-path=.`,
16 | ]
17 |
18 | const commands = [`cd ${pkgPath}`, changelogArgs.join(' ')]
19 | const cmd = commands.join(' && ')
20 | execSync(cmd)
21 | }
22 |
23 | run().catch((e) => {
24 | console.log(e)
25 | })
26 |
--------------------------------------------------------------------------------
/scripts/utils.ts:
--------------------------------------------------------------------------------
1 | import minimist from '@withtypes/minimist'
2 |
3 | /**
4 | * Get argv from Command Line
5 | */
6 | export function getArgv() {
7 | const argv = minimist(process.argv.slice(2), { string: ['_'] })
8 | const { _, otp, tag } = argv
9 | const [name] = _
10 |
11 | if (!name) {
12 | const errArgs = [
13 | '',
14 | '🚧 Missing package name to generate declaration files.',
15 | '',
16 | '💡 Related command arguments and options:',
17 | ' pnpm build:lib ',
18 | ' pnpm release [--otp] [--tag]',
19 | '',
20 | '',
21 | ]
22 | const errMsg = errArgs.join('\n')
23 | throw new Error(errMsg)
24 | }
25 |
26 | return { name, otp, tag }
27 | }
28 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "ui": "tui",
4 | "tasks": {
5 | "build": {
6 | "dependsOn": [
7 | "^build"
8 | ],
9 | "inputs": [
10 | "$TURBO_DEFAULT$",
11 | ".env*"
12 | ],
13 | "outputs": [
14 | "dist"
15 | ],
16 | "outputLogs": "full"
17 | },
18 | "lint": {
19 | "dependsOn": [
20 | "^lint"
21 | ],
22 | "outputLogs": "full"
23 | },
24 | "check-types": {
25 | "dependsOn": [
26 | "^check-types"
27 | ],
28 | "outputLogs": "full"
29 | },
30 | "dev": {
31 | "cache": false,
32 | "persistent": true,
33 | "outputLogs": "full"
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./packages/tsconfig/base.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "importHelpers": true,
6 | "experimentalDecorators": true,
7 | "rootDir": ".",
8 | "baseUrl": ".",
9 | "outDir": "./**/dist",
10 | "paths": {
11 | "@packages/*": ["./packages/*"],
12 | "@scripts/*": ["./scripts/*"]
13 | },
14 | "types": ["vite/client", "node"],
15 | "lib": ["ESNext", "DOM", "DOM.Iterable"],
16 | "declaration": true
17 | },
18 | "include": ["packages/**/*", "scripts/**/*", "eslint.config.ts"],
19 | "exclude": [
20 | "node_modules",
21 | "packages/**/dist",
22 | "packages/**/lib",
23 | "packages/**/types",
24 | "packages/**/.build"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/packages/eslint-config/src/configs/node.ts:
--------------------------------------------------------------------------------
1 | import nodePlugin from 'eslint-plugin-n'
2 | import { getConfigName } from '../shared/utils'
3 | import { type FlatESLintConfig } from '../types'
4 |
5 | export const node: FlatESLintConfig[] = [
6 | {
7 | name: getConfigName('node'),
8 | plugins: {
9 | n: nodePlugin,
10 | },
11 | rules: {
12 | 'n/handle-callback-err': ['error', '^(err|error)$'],
13 | 'n/no-deprecated-api': 'error',
14 | 'n/no-exports-assign': 'error',
15 | 'n/no-new-require': 'error',
16 | 'n/no-path-concat': 'error',
17 | 'n/prefer-global/buffer': ['error', 'never'],
18 | 'n/prefer-global/process': ['error', 'never'],
19 | 'n/process-exit-as-throw': 'error',
20 | },
21 | },
22 | ]
23 |
--------------------------------------------------------------------------------
/packages/tsconfig/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.1.1 (2023-08-06)
2 |
3 |
4 | ### Bug Fixes
5 |
6 | * **tsconfig:** remove baseUrl and paths, need to be set by the project itself ([e180a06](https://github.com/chengpeiquan/bassist/commit/e180a06e5532b061034d169556ca4c6bdd2ea8cb))
7 |
8 |
9 |
10 | # 0.1.0 (2023-08-06)
11 |
12 |
13 | ### Features
14 |
15 | * **tsconfig:** add base config ([795665e](https://github.com/chengpeiquan/bassist/commit/795665ec2ee51e313a71b45b6917455f72b8397a))
16 | * **tsconfig:** add node config ([71e9139](https://github.com/chengpeiquan/bassist/commit/71e91395531232314dc4a3bb665700e3530e14f1))
17 | * **tsconfig:** add web config ([7e9d7c5](https://github.com/chengpeiquan/bassist/commit/7e9d7c552a7884cf89ebf7365fbc2cb099096eb7))
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/packages/tsconfig/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Base",
4 | "compilerOptions": {
5 | "target": "ES2020",
6 | "module": "ESNext",
7 | "moduleResolution": "Bundler",
8 | "jsx": "preserve",
9 | "strict": true,
10 | "noUnusedLocals": true,
11 | "noUnusedParameters": true,
12 | "noFallthroughCasesInSwitch": true,
13 | "allowJs": false,
14 | "skipLibCheck": true,
15 | "resolveJsonModule": true,
16 | "esModuleInterop": true,
17 | "allowSyntheticDefaultImports": true,
18 | "forceConsistentCasingInFileNames": true,
19 | "useDefineForClassFields": true,
20 | "sourceMap": true,
21 | "isolatedModules": true
22 | },
23 | "exclude": ["node_modules"]
24 | }
25 |
--------------------------------------------------------------------------------
/packages/eslint-config/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './configs/imports'
2 | export * from './configs/javascript'
3 | export * from './configs/jsx'
4 | export * from './configs/markdown'
5 | export * from './configs/next'
6 | export * from './configs/node'
7 | export * from './configs/react'
8 | export * from './configs/regexp'
9 | export * from './configs/typescript'
10 | export * from './configs/unicorn'
11 | export * from './configs/vue'
12 |
13 | export * from './define'
14 |
15 | export * from './types'
16 |
17 | export { prettierPlugin } from './private-configs/prettier'
18 |
19 | export {
20 | tailwindcssPlugin,
21 | defaultTailwindcssSettings,
22 | type TailwindcssSettings,
23 | } from './private-configs/tailwindcss'
24 |
25 | export { createGetConfigNameFactory } from './shared/utils'
26 |
--------------------------------------------------------------------------------
/packages/bassist/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bassist",
3 | "version": "0.0.0",
4 | "description": "I play bass, so enjoy the name.",
5 | "author": "chengpeiquan ",
6 | "license": "MIT",
7 | "homepage": "https://github.com/chengpeiquan/bassist/tree/main/packages/bassist",
8 | "files": [
9 | "lib",
10 | "types"
11 | ],
12 | "main": "./lib/index.min.js",
13 | "module": "./lib/index.mjs",
14 | "types": "./types/index.d.ts",
15 | "exports": {
16 | ".": {
17 | "import": "./lib/index.mjs",
18 | "require": "./lib/index.cjs",
19 | "types": "./types/index.d.ts"
20 | }
21 | },
22 | "repository": {
23 | "type": "git",
24 | "url": "git+https://github.com/chengpeiquan/bassist.git"
25 | },
26 | "keywords": [
27 | "bass",
28 | "bassist"
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/packages/changelog/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # [0.3.0](https://github.com/chengpeiquan/bassist/compare/changelog@0.2.0...changelog@0.3.0) (2024-02-15)
2 |
3 |
4 | ### Features
5 |
6 | * **changelog:** let the CLI as a peer dependency to installed ([5ec81ce](https://github.com/chengpeiquan/bassist/commit/5ec81ceb6b0b322dbcbc4178cbf98fa292df04d3))
7 |
8 |
9 |
10 | # [0.2.0](https://github.com/chengpeiquan/bassist/compare/changelog@0.1.0...changelog@0.2.0) (2024-02-15)
11 |
12 |
13 | ### Features
14 |
15 | * **changelog:** simplify the steps to use ([e0ce531](https://github.com/chengpeiquan/bassist/commit/e0ce531ca885f7a46c6816e8b130a01214f841c9))
16 |
17 |
18 |
19 | # 0.1.0 (2024-02-14)
20 |
21 |
22 | ### Features
23 |
24 | * **changelog:** initial changelog CLI ([9b3785e](https://github.com/chengpeiquan/bassist/commit/9b3785e7c0dcfcdc1a318740dd667433a5575de9))
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/packages/utils/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@bassist/utils",
3 | "version": "0.17.0",
4 | "description": "Opinionated collection of common JavaScript / TypeScript utils by @chengpeiquan .",
5 | "author": "chengpeiquan ",
6 | "license": "MIT",
7 | "homepage": "https://jsdocs.io/package/@bassist/utils",
8 | "files": [
9 | "dist"
10 | ],
11 | "main": "./dist/index.min.js",
12 | "module": "./dist/index.mjs",
13 | "types": "./dist/index.d.ts",
14 | "exports": {
15 | ".": {
16 | "types": "./dist/index.d.ts",
17 | "import": "./dist/index.mjs",
18 | "require": "./dist/index.cjs"
19 | }
20 | },
21 | "repository": {
22 | "type": "git",
23 | "url": "https://github.com/chengpeiquan/bassist",
24 | "directory": "packages/utils"
25 | },
26 | "keywords": [
27 | "utils"
28 | ],
29 | "scripts": {
30 | "build": "tsup"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/eslint-config/src/shared/utils.ts:
--------------------------------------------------------------------------------
1 | const defaultPrefix = 'bassist'
2 |
3 | /**
4 | * A flexible tool function for generating ESLint configuration naming tools. It
5 | * helps you quickly splice configuration names, ensure consistent namespaces,
6 | * and facilitate the organization and management of complex rule sets.
7 | *
8 | * @param prefix - A string representing the prefix for your configuration
9 | * names.
10 | * @returns A function that concatenates the provided name segments with the
11 | * given prefix.
12 | */
13 | export const createGetConfigNameFactory = (prefix: string) => {
14 | const getConfigName = (...names: string[]): string => {
15 | const finalPrefix = prefix?.trim() || defaultPrefix
16 | return `${finalPrefix}/${names.join('/')}`
17 | }
18 |
19 | return getConfigName
20 | }
21 |
22 | // Provided for internal configuration
23 | export const getConfigName = createGetConfigNameFactory(defaultPrefix)
24 |
--------------------------------------------------------------------------------
/packages/utils/src/file.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Get file info via `mime` package
3 | *
4 | * @category File
5 | */
6 | export class FileInfo {
7 | mime: any
8 |
9 | constructor(theMimePackageInstance: any) {
10 | this.mime = theMimePackageInstance
11 | }
12 |
13 | getMimeType(path: string) {
14 | try {
15 | if (path.startsWith('data') && path.includes('base64')) {
16 | return path.split(',')[0].replace(/data:(.*);base64/, '$1')
17 | }
18 | return this.mime.getType(path) || ''
19 | } catch {
20 | return ''
21 | }
22 | }
23 |
24 | getExtensionFromMimeType(mimeType: string) {
25 | try {
26 | return this.mime.getExtension(mimeType) || ''
27 | } catch {
28 | return ''
29 | }
30 | }
31 |
32 | getExtension(path: string) {
33 | try {
34 | const mimeType = this.getMimeType(path)
35 | return this.getExtensionFromMimeType(mimeType)
36 | } catch {
37 | return ''
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/packages/progress/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@bassist/progress",
3 | "version": "0.2.2",
4 | "description": "Simple slim progress bars base on NProgress.",
5 | "author": "chengpeiquan ",
6 | "license": "MIT",
7 | "homepage": "https://github.com/chengpeiquan/bassist/tree/main/packages/progress",
8 | "files": [
9 | "dist"
10 | ],
11 | "main": "./dist/index.min.js",
12 | "module": "./dist/index.mjs",
13 | "types": "./dist/index.d.ts",
14 | "exports": {
15 | ".": {
16 | "types": "./dist/index.d.ts",
17 | "import": "./dist/index.mjs",
18 | "require": "./dist/index.cjs"
19 | }
20 | },
21 | "repository": {
22 | "type": "git",
23 | "url": "git+https://github.com/chengpeiquan/bassist.git"
24 | },
25 | "keywords": [
26 | "nprogress",
27 | "progress",
28 | "progress bar"
29 | ],
30 | "scripts": {
31 | "build": "tsup"
32 | },
33 | "dependencies": {
34 | "@bassist/utils": "workspace:^",
35 | "nprogress": "^0.2.0"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/packages/build-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@bassist/build-config",
3 | "version": "0.1.0",
4 | "description": "Opinionated collection of common build config by @chengpeiquan .",
5 | "author": "chengpeiquan ",
6 | "license": "MIT",
7 | "homepage": "https://jsdocs.io/package/@bassist/build-config",
8 | "files": [
9 | "dist"
10 | ],
11 | "exports": {
12 | "./tsup": {
13 | "types": "./dist/tsup.d.ts",
14 | "import": "./dist/tsup.mjs",
15 | "require": "./dist/tsup.cjs"
16 | }
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "https://github.com/chengpeiquan/bassist",
21 | "directory": "packages/build-config"
22 | },
23 | "keywords": [
24 | "build config",
25 | "tsup config"
26 | ],
27 | "scripts": {
28 | "build": "tsup"
29 | },
30 | "devDependencies": {
31 | "tsup": "^8.5.0"
32 | },
33 | "peerDependencies": {
34 | "tsup": ">=8.0.0"
35 | },
36 | "peerDependenciesMeta": {
37 | "tsup": {
38 | "optional": true
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/utils/src/clipboard/fallback.ts:
--------------------------------------------------------------------------------
1 | import { isBrowser } from '../device'
2 |
3 | export function fallbackWriteText(text: string) {
4 | if (!isBrowser) return false
5 |
6 | try {
7 | const textArea = document.createElement('textarea')
8 | textArea.value = text
9 |
10 | textArea.style.position = 'fixed'
11 | textArea.style.top = '-9999px'
12 | textArea.style.left = '-9999px'
13 | document.body.appendChild(textArea)
14 |
15 | textArea.focus()
16 | textArea.select()
17 |
18 | const successful = document.execCommand('copy')
19 | document.body.removeChild(textArea)
20 | return successful
21 | } catch {
22 | return false
23 | }
24 | }
25 |
26 | export function fallbackReadText() {
27 | if (!isBrowser) return ''
28 |
29 | try {
30 | const textarea = document.createElement('textarea')
31 | document.body.appendChild(textarea)
32 |
33 | textarea.focus()
34 | document.execCommand('paste')
35 |
36 | const text = textarea.value
37 | textarea.remove()
38 | return text
39 | } catch {
40 | return ''
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/packages/progress/src/index.ts:
--------------------------------------------------------------------------------
1 | import { loadRes, isBrowser, randomString } from '@bassist/utils'
2 | import nprogress from 'nprogress'
3 | import resource from 'nprogress/nprogress.css?inline'
4 | import type { Progress } from './types'
5 |
6 | loadRes({
7 | type: 'style',
8 | id: 'bassist-nprogress',
9 | resource,
10 | }).catch((e) => {
11 | console.log(e)
12 | })
13 |
14 | const progress = nprogress as Progress
15 |
16 | progress.setColor = function (color: string) {
17 | if (!isBrowser) return
18 |
19 | const style = `
20 | #nprogress .bar {
21 | background: ${color} !important;
22 | }
23 | #nprogress .peg {
24 | box-shadow: 0 0 10px ${color}, 0 0 5px ${color} !important;
25 | }
26 | #nprogress .spinner .spinner-icon {
27 | border-top-color: ${color} !important;
28 | border-left-color: ${color} !important;
29 | }
30 | `
31 |
32 | loadRes({
33 | type: 'style',
34 | id: `bassist-nprogress-theme-${randomString()}`,
35 | resource: style,
36 | }).catch((e) => {
37 | console.log(e)
38 | })
39 | }
40 |
41 | export default progress as Progress
42 |
--------------------------------------------------------------------------------
/packages/node-utils/src/pkg.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Check the package name is valid
3 | *
4 | * @category Pkg
5 | */
6 | export function isValidPackageName(packageName: string) {
7 | return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(
8 | packageName,
9 | )
10 | }
11 |
12 | /**
13 | * Format the package name to valid
14 | *
15 | * @category Pkg
16 | */
17 | export function toValidPackageName(name: string) {
18 | return name
19 | .trim()
20 | .toLowerCase()
21 | .replace(/\s+/g, '-')
22 | .replace(/^[._]/, '')
23 | .replace(/[^a-z0-9-~]+/g, '-')
24 | }
25 |
26 | /**
27 | * Get package manager info
28 | *
29 | * @category Pkg
30 | */
31 | export function getPackageManagerByUserAgent() {
32 | const defaultInfo = {
33 | name: '',
34 | version: '0.0.0',
35 | }
36 |
37 | try {
38 | const userAgent = process.env.npm_config_user_agent
39 | if (!userAgent) {
40 | return { ...defaultInfo }
41 | }
42 |
43 | const spec = userAgent.split(' ')[0]
44 | const [name, version] = spec.split('/')
45 | return { name, version }
46 | } catch {
47 | return { ...defaultInfo }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/packages/utils/test/file.spec.ts:
--------------------------------------------------------------------------------
1 | import mime from '@withtypes/mime'
2 | import { describe, expect, it } from 'vitest'
3 | import { FileInfo } from '..'
4 |
5 | const file = new FileInfo(mime)
6 |
7 | describe('file', () => {
8 | it('getMimeType', () => {
9 | expect(file.getMimeType('example.txt')).toBe('text/plain')
10 | expect(file.getMimeType('example.png')).toBe('image/png')
11 | expect(file.getMimeType('example')).toBe('')
12 | expect(
13 | file.getMimeType(
14 | 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACrElEQVR4',
15 | ),
16 | ).toBe('image/png')
17 | })
18 |
19 | it('getExtensionFromMimeType', () => {
20 | expect(file.getExtensionFromMimeType('text/plain')).toBe('txt')
21 | })
22 |
23 | it('getExtension', () => {
24 | expect(file.getExtension('example.txt')).toBe('txt')
25 | expect(file.getExtension('example.png')).toBe('png')
26 | expect(file.getExtension('example')).toBe('')
27 | expect(
28 | file.getExtension(
29 | 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACrElEQVR4',
30 | ),
31 | ).toBe('png')
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/packages/utils/test/storage.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 | import { LocalStorage } from '..'
3 |
4 | describe('storage', () => {
5 | it('LocalStorage Default', () => {
6 | const ls = new LocalStorage('test-ls-default')
7 | expect(ls.prefix).toBe('test-ls-default')
8 | expect(ls.count()).toBe(0)
9 | expect(ls.list()).toStrictEqual([])
10 | })
11 |
12 | it('LocalStorage With Data', () => {
13 | const ls = new LocalStorage('test-ls')
14 | expect(ls.prefix).toBe('test-ls')
15 |
16 | ls.set('foo', 'foo')
17 | expect(ls.count()).toBe(1)
18 | expect(ls.get('foo')).toBe('foo')
19 | expect(ls.list()).toStrictEqual(['foo'])
20 |
21 | ls.set('bar', 1)
22 | expect(ls.get('bar')).toBe(1)
23 |
24 | ls.set('baz', [1, 2, 3])
25 | expect(ls.get('baz')).toStrictEqual([1, 2, 3])
26 |
27 | ls.set('qux', {
28 | foo: 'foo',
29 | bar: 1,
30 | })
31 | expect(ls.get('qux')).toStrictEqual({
32 | foo: 'foo',
33 | bar: 1,
34 | })
35 |
36 | ls.remove('qux')
37 | expect(ls.get('qux')).toBeNull()
38 |
39 | ls.clear()
40 | expect(ls.count()).toBe(0)
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022-PRESENT chengpeiquan (https://github.com/chengpeiquan)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/progress/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [0.2.2](https://github.com/chengpeiquan/bassist/compare/progress@0.2.1...progress@0.2.2) (2023-08-19)
2 |
3 |
4 | ### Bug Fixes
5 |
6 | * **progress:** typo ([57d8a79](https://github.com/chengpeiquan/bassist/commit/57d8a7918869a24fa8f01a28d33c6a9cdeb57d3e))
7 |
8 |
9 |
10 | ## [0.2.1](https://github.com/chengpeiquan/bassist/compare/progress@0.2.0...progress@0.2.1) (2023-03-19)
11 |
12 |
13 | ### Bug Fixes
14 |
15 | * **progress:** update the utils dep to fix a loadRes bug ([b5e1de0](https://github.com/chengpeiquan/bassist/commit/b5e1de0641210ff87e7405481903cfd00a54eee5))
16 |
17 |
18 |
19 | # 0.2.0 (2023-02-08)
20 |
21 |
22 | ### Features
23 |
24 | * **progress:** optimize the type of package ([6b59427](https://github.com/chengpeiquan/bassist/commit/6b594271a69403292d91f04bfeff01d145935582))
25 |
26 |
27 |
28 | # 0.1.0 (2023-02-08)
29 |
30 |
31 | ### Features
32 |
33 | * **progress:** add progress ([f906324](https://github.com/chengpeiquan/bassist/commit/f906324086ac0bfed8573f39c19f34278c25a1ea))
34 | * **progress:** add setColor method ([fd3fcef](https://github.com/chengpeiquan/bassist/commit/fd3fcefffe242b355df05eb525a5547cccf4a2c0))
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/packages/node-utils/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@bassist/node-utils",
3 | "version": "0.5.0",
4 | "description": "Opinionated collection of common Node.js utils by @chengpeiquan .",
5 | "author": "chengpeiquan ",
6 | "license": "MIT",
7 | "homepage": "https://jsdocs.io/package/@bassist/node-utils",
8 | "files": [
9 | "dist"
10 | ],
11 | "main": "./dist/index.cjs",
12 | "module": "./dist/index.mjs",
13 | "types": "./dist/index.d.ts",
14 | "exports": {
15 | ".": {
16 | "types": "./dist/index.d.ts",
17 | "import": "./dist/index.mjs",
18 | "require": "./dist/index.cjs"
19 | }
20 | },
21 | "repository": {
22 | "type": "git",
23 | "url": "https://github.com/chengpeiquan/bassist",
24 | "directory": "packages/node-utils"
25 | },
26 | "keywords": [
27 | "utils",
28 | "node utils",
29 | "file system utils",
30 | "file utils",
31 | "fs utils",
32 | "fs extra"
33 | ],
34 | "scripts": {
35 | "build": "tsup"
36 | },
37 | "dependencies": {
38 | "@bassist/utils": "workspace:^",
39 | "fs-extra": "^11.3.0"
40 | },
41 | "devDependencies": {
42 | "@types/fs-extra": "^11.0.4"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/packages/eslint-config/src/types.ts:
--------------------------------------------------------------------------------
1 | import { type ESLint, type Linter } from 'eslint'
2 | import {
3 | type RequiredOptions as PrettierRequiredOptions,
4 | type BuiltInParserName as PrettierParser,
5 | type LiteralUnion as PrettierLiteralUnion,
6 | } from 'prettier'
7 | import { type Options as PrettierJsdocOptions } from 'prettier-plugin-jsdoc'
8 | import { type prettierLintMd } from 'prettier-plugin-lint-md'
9 |
10 | export type FlatESLintConfig = Linter.Config
11 |
12 | export type FlatESLintPlugin = ESLint.Plugin
13 |
14 | export type FlatESLintParser = Linter.Parser
15 |
16 | export type FlatESLintProcessor = Linter.Processor
17 |
18 | export type FlatESLintRules = Linter.RulesRecord
19 |
20 | /**
21 | * Prettier types
22 | */
23 | export { type PrettierParser }
24 |
25 | export interface PrettierOptions extends PrettierRequiredOptions {
26 | parser: PrettierLiteralUnion
27 | }
28 |
29 | export type PrettierLintMdOptions = NonNullable<
30 | Parameters[0]
31 | >
32 |
33 | export { type PrettierJsdocOptions }
34 |
35 | export type PartialPrettierExtendedOptions = Partial &
36 | Partial &
37 | Partial
38 |
--------------------------------------------------------------------------------
/packages/release/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [0.3.1](https://github.com/chengpeiquan/bassist/compare/release@0.3.0...release@0.3.1) (2025-05-22)
2 |
3 |
4 | ### Bug Fixes
5 |
6 | * **release:** internal dependency upgrade ([af59e48](https://github.com/chengpeiquan/bassist/commit/af59e486710ec4d40500d8b3229e89fc4ecb9ea2))
7 |
8 |
9 |
10 | # [0.3.0](https://github.com/chengpeiquan/bassist/compare/release@0.2.0...release@0.3.0) (2024-06-30)
11 |
12 |
13 | ### Features
14 |
15 | * **release:** use node-utils instead fs-extra ([3624f8e](https://github.com/chengpeiquan/bassist/commit/3624f8e19327f041815647ec032385561675308a))
16 |
17 |
18 |
19 | # [0.2.0](https://github.com/chengpeiquan/bassist/compare/release@0.1.0...release@0.2.0) (2024-02-15)
20 |
21 |
22 | ### Features
23 |
24 | * **release:** supports monorepo tag format ([aca815d](https://github.com/chengpeiquan/bassist/commit/aca815d715004b9a12ebaf58d4f3118cbb09ba1b))
25 |
26 |
27 |
28 | # [0.1.0](https://github.com/chengpeiquan/bassist/compare/release@0.1.0...release@0.1.0) (2024-02-15)
29 |
30 |
31 | ### Features
32 |
33 | * **release:** initial release CLI ([8089d04](https://github.com/chengpeiquan/bassist/commit/8089d0455ecd79df9965ce164cb0c06872e21e4e))
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/packages/changelog/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@bassist/changelog",
3 | "version": "0.3.0",
4 | "description": "Simple CHANGELOG generator by @chengpeiquan , based on conventional-changelog-cli.",
5 | "author": "chengpeiquan ",
6 | "license": "MIT",
7 | "homepage": "https://github.com/chengpeiquan/bassist/tree/main/packages/changelog",
8 | "files": [
9 | "dist"
10 | ],
11 | "bin": {
12 | "@bassist/changelog": "./dist/index.mjs",
13 | "changelog": "./dist/index.mjs"
14 | },
15 | "main": "./dist/index.cjs",
16 | "module": "./dist/index.mjs",
17 | "types": "./dist/index.d.ts",
18 | "exports": {
19 | ".": {
20 | "types": "./dist/index.d.ts",
21 | "import": "./dist/index.mjs",
22 | "require": "./dist/index.cjs"
23 | }
24 | },
25 | "repository": {
26 | "type": "git",
27 | "url": "git+https://github.com/chengpeiquan/bassist.git"
28 | },
29 | "keywords": [
30 | "changelog",
31 | "changelog generator",
32 | "generate changelog",
33 | "CHANGELOG.md"
34 | ],
35 | "scripts": {
36 | "build": "tsup"
37 | },
38 | "dependencies": {
39 | "@withtypes/minimist": "^0.1.1",
40 | "conventional-changelog-cli": "^5.0.0"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/packages/release/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@bassist/release",
3 | "version": "0.3.1",
4 | "description": "Simple GitHub release generator by @chengpeiquan , based on GitHub CLI.",
5 | "author": "chengpeiquan ",
6 | "license": "MIT",
7 | "homepage": "https://github.com/chengpeiquan/bassist/tree/main/packages/release",
8 | "files": [
9 | "dist"
10 | ],
11 | "bin": {
12 | "@bassist/release": "./dist/index.mjs",
13 | "release": "./dist/index.mjs"
14 | },
15 | "main": "./dist/index.cjs",
16 | "module": "./dist/index.mjs",
17 | "types": "./dist/index.d.ts",
18 | "exports": {
19 | ".": {
20 | "types": "./dist/index.d.ts",
21 | "import": "./dist/index.mjs",
22 | "require": "./dist/index.cjs"
23 | }
24 | },
25 | "repository": {
26 | "type": "git",
27 | "url": "https://github.com/chengpeiquan/bassist"
28 | },
29 | "keywords": [
30 | "release",
31 | "release generator",
32 | "generate release",
33 | "github release"
34 | ],
35 | "scripts": {
36 | "build": "tsup"
37 | },
38 | "dependencies": {
39 | "@bassist/node-utils": "workspace:^",
40 | "@bassist/utils": "workspace:^",
41 | "@withtypes/minimist": "^0.1.1"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/packages/changelog/src/index.ts:
--------------------------------------------------------------------------------
1 | import { execSync } from 'node:child_process'
2 | import minimist from '@withtypes/minimist'
3 | import pkg from '../package.json'
4 |
5 | async function run() {
6 | const argv = minimist(process.argv.slice(2), {
7 | string: ['_', 'preset', 'infile', 'lerna-package'],
8 | alias: {
9 | preset: 'p',
10 | infile: 'i',
11 | 'release-count': 'r',
12 | 'lerna-package': 'l',
13 | },
14 | })
15 |
16 | const {
17 | preset: presetValue,
18 | infile: infileValue,
19 | 'release-count': releaseCountValue,
20 | 'commit-path': commitPathValue,
21 | 'lerna-package': lernaPackageValue,
22 | } = argv
23 |
24 | const preset = presetValue || 'angular'
25 | const infile = infileValue || 'CHANGELOG.md'
26 | const releaseCount = releaseCountValue || 1
27 | const lernaPackage = lernaPackageValue || ''
28 | const commitPath = commitPathValue || './src'
29 |
30 | const changelogArgs = [
31 | `conventional-changelog`,
32 | lernaPackage ? `--lerna-package ${lernaPackage}` : '',
33 | `-p ${preset}`,
34 | `-i ${infile}`,
35 | `-r ${releaseCount}`,
36 | `-s`,
37 | `--commit-path=${commitPath}`,
38 | ].filter((i) => !!i)
39 |
40 | const cmd = changelogArgs.join(' ')
41 | execSync(cmd)
42 | }
43 |
44 | run().catch((e) => {
45 | console.log(`[${pkg.name}]`, e)
46 | })
47 |
--------------------------------------------------------------------------------
/packages/utils/test/clipboard.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 | import { clipboard } from '..'
3 |
4 | describe('clipboard', () => {
5 | it('Valid data', async () => {
6 | expect(await clipboard.read()).toBe('')
7 | })
8 | it('Invalid data', async () => {
9 | expect(clipboard.isSupported).toBeFalsy()
10 | expect(await clipboard.write('hello')).toBeFalsy()
11 | })
12 | })
13 |
14 | //
15 | // Hello World
16 |
17 | //
18 | //
19 | //
20 | //
21 | //
22 | //
23 | //
24 |
25 | // {{ text }}
26 | //
27 |
28 | //
50 |
--------------------------------------------------------------------------------
/packages/node-utils/test/pkg.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 | import {
3 | isValidPackageName,
4 | toValidPackageName,
5 | getPackageManagerByUserAgent,
6 | } from '..'
7 |
8 | describe('pkg', () => {
9 | it('isValidPackageName', () => {
10 | expect(isValidPackageName('hello')).toBeTruthy()
11 | expect(isValidPackageName('hello123')).toBeTruthy()
12 | expect(isValidPackageName('113hello')).toBeTruthy()
13 | expect(isValidPackageName('hello-world')).toBeTruthy()
14 | expect(isValidPackageName('hello_world')).toBeTruthy()
15 | expect(isValidPackageName('hello.world')).toBeTruthy()
16 | expect(isValidPackageName('@hello/world')).toBeTruthy()
17 | expect(isValidPackageName('@hello/w-orld')).toBeTruthy()
18 |
19 | expect(isValidPackageName('!hello')).toBeFalsy()
20 | expect(isValidPackageName('%hello')).toBeFalsy()
21 | expect(isValidPackageName('Hello')).toBeFalsy()
22 | expect(isValidPackageName('HELLO')).toBeFalsy()
23 | expect(isValidPackageName('hello world')).toBeFalsy()
24 | })
25 |
26 | it('toValidPackageName', () => {
27 | expect(toValidPackageName('HELLO')).toBe('hello')
28 | expect(toValidPackageName('hello world')).toBe('hello-world')
29 | })
30 |
31 | it('getPackageManagerByUserAgent', () => {
32 | console.log('userAgent', process.env.npm_config_user_agent)
33 | expect(getPackageManagerByUserAgent().name).toBe('pnpm')
34 | })
35 | })
36 |
--------------------------------------------------------------------------------
/packages/utils/src/runtime.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Common runtime environment types
3 | *
4 | * @category Runtime
5 | */
6 | export type RuntimeEnv = 'dev' | 'development' | 'test' | 'prod' | 'production'
7 |
8 | /**
9 | * Get current runtime environment
10 | *
11 | * @category Runtime
12 | * @precondition The `cross-env` package is installed
13 | */
14 | export function getRuntimeEnv() {
15 | try {
16 | return process.env.NODE_ENV as RuntimeEnv
17 | } catch {
18 | return undefined
19 | }
20 | }
21 |
22 | /**
23 | * Current runtime environment
24 | *
25 | * @category Runtime
26 | */
27 | export const runtimeEnv = getRuntimeEnv()
28 |
29 | /**
30 | * Determine whether the specified runtime environment is currently
31 | *
32 | * @category Runtime
33 | * @precondition The `cross-env` package is installed
34 | */
35 | export function checkRuntimeEnv(runtimeEnv: unknown): runtimeEnv is RuntimeEnv {
36 | try {
37 | return process.env.NODE_ENV === runtimeEnv
38 | } catch {
39 | return false
40 | }
41 | }
42 |
43 | /**
44 | * Determine whether the current runtime is development
45 | *
46 | * @category Runtime
47 | */
48 | export const isDevRuntime =
49 | checkRuntimeEnv('dev') || checkRuntimeEnv('development')
50 |
51 | /**
52 | * Determine whether the current runtime is test
53 | *
54 | * @category Runtime
55 | */
56 | export const isTestRuntime = checkRuntimeEnv('test')
57 |
58 | /**
59 | * Determine whether the current runtime is production
60 | *
61 | * @category Runtime
62 | */
63 | export const isProdRuntime =
64 | checkRuntimeEnv('prod') || checkRuntimeEnv('production')
65 |
--------------------------------------------------------------------------------
/packages/progress/src/types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Types provided from @types/nprogress
3 | */
4 | interface NProgressOptions {
5 | minimum: number
6 | template: string
7 | easing: string
8 | speed: number
9 | trickle: boolean
10 | trickleSpeed: number
11 | showSpinner: boolean
12 | parent: string
13 | positionUsing: string
14 | barSelector: string
15 | spinnerSelector: string
16 | }
17 |
18 | interface NProgress {
19 | version: string
20 | settings: NProgressOptions
21 | status: number | null
22 |
23 | configure(options: Partial): NProgress
24 | set(number: number): NProgress
25 | isStarted(): boolean
26 | start(): NProgress
27 | done(force?: boolean): NProgress
28 | inc(amount?: number): NProgress
29 | trickle(): NProgress
30 |
31 | /* Internal */
32 |
33 | render(fromStart?: boolean): HTMLDivElement
34 | remove(): void
35 | isRendered(): boolean
36 | getPositioningCSS(): 'translate3d' | 'translate' | 'margin'
37 | }
38 |
39 | /**
40 | * Types of functionality that this plugin extends
41 | */
42 | export interface Progress extends NProgress {
43 | /**
44 | * @example
45 | * use HEX
46 | * progress.setColor('#ff0000')
47 | *
48 | * @example
49 | * use RGB
50 | * progress.setColor('rgb(255, 0, 0)')
51 | *
52 | * @example
53 | * use RGBA
54 | * progress.setColor('rgba(255, 0, 0, 1)')
55 | *
56 | * @example
57 | * use CSS Variable
58 | * progress.setColor('var(--color-primary)')
59 | *
60 | * @param color - A valid CSS color value or CSS variable
61 | */
62 | setColor: (color: string) => void
63 | }
64 |
--------------------------------------------------------------------------------
/packages/eslint-config/src/configs/imports.ts:
--------------------------------------------------------------------------------
1 | import importPlugin from 'eslint-plugin-import-x'
2 | import { getConfigName } from '../shared/utils'
3 | import { type FlatESLintPlugin, type FlatESLintConfig } from '../types'
4 |
5 | export { importPlugin }
6 |
7 | export const imports: FlatESLintConfig[] = [
8 | {
9 | name: getConfigName('imports'),
10 | plugins: {
11 | import: importPlugin as unknown as FlatESLintPlugin,
12 | },
13 | rules: {
14 | 'import/first': 'error',
15 | 'import/no-mutable-exports': 'error',
16 | 'import/no-duplicates': 'error',
17 |
18 | // Some scenes must provide a default export
19 | // e.g. Configuration files, Next.js routing, etc.
20 | 'import/no-default-export': 'off',
21 |
22 | 'import/no-named-default': 'error',
23 | 'import/no-self-import': 'error',
24 | 'import/no-webpack-loader-syntax': 'error',
25 |
26 | 'import/order': [
27 | 'error',
28 | {
29 | groups: [
30 | 'builtin',
31 | 'external',
32 | 'internal',
33 | 'parent',
34 | 'sibling',
35 | 'index',
36 | 'object',
37 | 'type',
38 | ],
39 | pathGroups: [{ pattern: '@/**', group: 'internal' }],
40 | pathGroupsExcludedImportTypes: ['type'],
41 | alphabetize: {
42 | order: 'asc',
43 | orderImportKind: 'asc',
44 | caseInsensitive: false,
45 | },
46 | },
47 | ],
48 |
49 | 'import/newline-after-import': ['error', { count: 1 }],
50 | },
51 | },
52 | ]
53 |
--------------------------------------------------------------------------------
/packages/eslint-config/src/configs/markdown.ts:
--------------------------------------------------------------------------------
1 | import markdownPlugin from '@eslint/markdown'
2 | import {
3 | GLOB_EXCLUDE,
4 | GLOB_MARKDOWN,
5 | GLOB_MARKDOWN_CODE,
6 | GLOB_VUE,
7 | } from '../globs'
8 | import { getConfigName } from '../shared/utils'
9 | import { type FlatESLintConfig } from '../types'
10 |
11 | export { markdownPlugin }
12 |
13 | export const markdown: FlatESLintConfig[] = [
14 | ...markdownPlugin.configs.processor,
15 | ...markdownPlugin.configs.recommended,
16 | {
17 | name: getConfigName('markdown'),
18 | files: [GLOB_MARKDOWN_CODE, `${GLOB_MARKDOWN}/${GLOB_VUE}`],
19 | language: 'markdown/commonmark',
20 | rules: {
21 | '@typescript-eslint/comma-dangle': 'off',
22 | '@typescript-eslint/consistent-type-imports': 'off',
23 | '@typescript-eslint/no-extraneous-class': 'off',
24 | '@typescript-eslint/no-namespace': 'off',
25 | '@typescript-eslint/no-redeclare': 'off',
26 | '@typescript-eslint/no-require-imports': 'off',
27 | '@typescript-eslint/no-unused-expressions': 'off',
28 | '@typescript-eslint/no-unused-vars': 'off',
29 | '@typescript-eslint/no-use-before-define': 'off',
30 | 'no-alert': 'off',
31 | 'no-console': 'off',
32 | 'no-restricted-imports': 'off',
33 | 'no-undef': 'off',
34 | 'no-unused-expressions': 'off',
35 | 'no-unused-vars': 'off',
36 | 'node/prefer-global/buffer': 'off',
37 | 'node/prefer-global/process': 'off',
38 | 'unused-imports/no-unused-imports': 'off',
39 | 'unused-imports/no-unused-vars': 'off',
40 | },
41 | ignores: [...GLOB_EXCLUDE],
42 | },
43 | ]
44 |
--------------------------------------------------------------------------------
/packages/node-utils/test/bundle.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 | import { getBundleBanner } from '..'
3 | import pkg from '../package.json'
4 |
5 | describe('bundler', () => {
6 | it('getBundleBanner base', () => {
7 | expect(getBundleBanner(pkg)).toBe(
8 | [
9 | `/**`,
10 | ` * name: ${pkg.name}`,
11 | ` * version: v${pkg.version}`,
12 | ` * description: ${pkg.description}`,
13 | ` * author: ${pkg.author}`,
14 | ` * homepage: ${pkg.homepage}`,
15 | ` * license: ${pkg.license}`,
16 | ` */`,
17 | ].join('\n'),
18 | )
19 | })
20 |
21 | it('getBundleBanner bin', () => {
22 | expect(getBundleBanner(pkg, { bin: true })).toBe(
23 | [
24 | '#!/usr/bin/env node',
25 | '',
26 | `/**`,
27 | ` * name: ${pkg.name}`,
28 | ` * version: v${pkg.version}`,
29 | ` * description: ${pkg.description}`,
30 | ` * author: ${pkg.author}`,
31 | ` * homepage: ${pkg.homepage}`,
32 | ` * license: ${pkg.license}`,
33 | ` */`,
34 | ].join('\n'),
35 | )
36 | })
37 |
38 | it('getBundleBanner custom fields', () => {
39 | expect(getBundleBanner(pkg, { fields: ['name', 'version'] })).toBe(
40 | [
41 | `/**`,
42 | ` * name: ${pkg.name}`,
43 | ` * version: v${pkg.version}`,
44 | ` */`,
45 | ].join('\n'),
46 | )
47 | })
48 |
49 | it('getBundleBanner bad fields', () => {
50 | expect(
51 | getBundleBanner(pkg, { fields: ['foo', 'bar', 'dependencies'] }),
52 | ).toBe([`/**`, ` */`].join('\n'))
53 | })
54 | })
55 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "antfu",
4 | "apos",
5 | "Attributify",
6 | "baiduboxapp",
7 | "callees",
8 | "codemirror",
9 | "commitlint",
10 | "commonmark",
11 | "conventionalcommits",
12 | "EDITMSG",
13 | "escaperegexp",
14 | "ianvs",
15 | "IIFE",
16 | "infile",
17 | "kolorist",
18 | "middot",
19 | "mqqbrowser",
20 | "nprogress",
21 | "nuxt",
22 | "pnoop",
23 | "qqbrowser",
24 | "qzone",
25 | "rspack",
26 | "tailwindcss",
27 | "taze",
28 | "Tsup",
29 | "Weibo",
30 | "Weixin",
31 | "Windi",
32 | "windicss",
33 | "withtypes"
34 | ],
35 | "explorer.fileNesting.enabled": true,
36 | "explorer.fileNesting.patterns": {
37 | ".env": ".env.*",
38 | "tsconfig.json": "tsconfig.*.json, env.d.ts",
39 | "vite.config.*": "jsconfig*, vitest.config.*, cypress.config.*, playwright.config.*",
40 | "package.json": "package-lock.json, pnpm*, .yarnrc*, yarn*, .eslint*, eslint*, .oxlint*, oxlint*, .prettier*, prettier*, .editorconfig, postcss.config.js, tailwind.config.ts, tailwind.whitelist.js, turbo.json, commitlint.config.js"
41 | },
42 | "editor.formatOnSave": true,
43 | "editor.codeActionsOnSave": {
44 | "source.fixAll.eslint": "always",
45 | "source.fixAll.prettier": "always"
46 | },
47 | "editor.defaultFormatter": "esbenp.prettier-vscode",
48 | "eslint.useFlatConfig": true,
49 | "eslint.format.enable": true,
50 | "eslint.validate": [
51 | "javascript",
52 | "javascriptreact",
53 | "typescript",
54 | "typescriptreact"
55 | ],
56 | "prettier.configPath": "./.prettierrc.mjs"
57 | }
58 |
--------------------------------------------------------------------------------
/packages/eslint-config/src/configs/react.ts:
--------------------------------------------------------------------------------
1 | import reactPlugin from 'eslint-plugin-react'
2 | import reactHooksConfig from 'eslint-plugin-react-hooks'
3 | import reactRefresh from 'eslint-plugin-react-refresh'
4 | import globals from 'globals'
5 | import { GLOB_EXCLUDE, GLOB_JS, GLOB_JSX, GLOB_TS, GLOB_TSX } from '../globs'
6 | import { getConfigName } from '../shared/utils'
7 | import { type FlatESLintConfig, type FlatESLintRules } from '../types'
8 | import { tsParser, tsPlugin, typescript } from './typescript'
9 |
10 | export { reactPlugin }
11 |
12 | const reactCustomRules = {
13 | 'react/jsx-uses-react': 'error',
14 | 'react/jsx-uses-vars': 'error',
15 | } as unknown as FlatESLintRules
16 |
17 | export const react: FlatESLintConfig[] = [
18 | {
19 | name: getConfigName('react'),
20 | settings: {
21 | react: {
22 | version: 'detect',
23 | },
24 | },
25 | files: [GLOB_JS, GLOB_JSX, GLOB_TS, GLOB_TSX],
26 | plugins: {
27 | react: reactPlugin,
28 | 'react-hooks': reactHooksConfig,
29 | 'react-refresh': reactRefresh,
30 | '@typescript-eslint': tsPlugin,
31 | },
32 | languageOptions: {
33 | ecmaVersion: 'latest',
34 | parser: tsParser,
35 | parserOptions: {
36 | sourceType: 'module',
37 | ecmaFeatures: {
38 | jsx: true,
39 | },
40 | },
41 | globals: {
42 | ...globals.browser,
43 | },
44 | },
45 | rules: {
46 | ...typescript[0].rules,
47 | ...reactPlugin.configs.recommended.rules,
48 | ...reactHooksConfig.configs.recommended.rules,
49 | ...reactCustomRules,
50 | },
51 | ignores: [...GLOB_EXCLUDE],
52 | },
53 | ]
54 |
--------------------------------------------------------------------------------
/packages/utils/src/performance.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Put the program to sleep for a while
3 | *
4 | * @category Performance
5 | */
6 | export function sleep(ms: number): Promise {
7 | return new Promise((resolve) => {
8 | setTimeout(() => {
9 | resolve()
10 | }, ms)
11 | })
12 | }
13 |
14 | /**
15 | * When an event is triggered frequently, only execute the event processing
16 | * function once.
17 | *
18 | * @category Performance
19 | */
20 |
21 | export function debounce void>(
22 | func: T,
23 | wait = 200,
24 | ): (...args: Parameters) => void {
25 | let timeout: ReturnType | number
26 |
27 | return function (this: ThisParameterType, ...args: Parameters) {
28 | clearTimeout(timeout)
29 | timeout = setTimeout(() => {
30 | func.apply(this, args)
31 | }, wait)
32 | }
33 | }
34 |
35 | /**
36 | * Can control how often a function is called within a specified time interval
37 | *
38 | * @category Performance
39 | */
40 |
41 | export function throttle void>(
42 | func: T,
43 | wait: number,
44 | ) {
45 | let timeout: ReturnType | number | undefined
46 | let previous = 0
47 |
48 | return function (this: ThisParameterType, ...args: Parameters) {
49 | const now = Date.now()
50 | const remaining = wait - (now - previous)
51 |
52 | if (remaining <= 0) {
53 | clearTimeout(timeout)
54 | previous = now
55 | func.apply(this, args)
56 | } else if (!timeout) {
57 | timeout = setTimeout(() => {
58 | previous = Date.now()
59 | timeout = undefined
60 | func.apply(this, args)
61 | }, remaining)
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/packages/release/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { isObject } from '@bassist/utils'
2 |
3 | const GITHUB_URL = 'https://github.com/'
4 | const DETAILS_LABEL = 'CHANGELOG'
5 |
6 | // https://docs.npmjs.com/cli/v10/configuring-npm/package-json#repository
7 | interface Repository {
8 | type: string
9 | url: string
10 | directory: string
11 | }
12 |
13 | interface RepoInfo {
14 | repo: string
15 | directory: string
16 | }
17 |
18 | export function getRepo(repository?: Repository): RepoInfo | undefined {
19 | if (!repository || !isObject(repository)) return undefined
20 | const { url = '', directory = '' } = repository
21 | if (!url) return undefined
22 |
23 | if (url.startsWith('http')) {
24 | const repo = url.endsWith('.git') ? url.replace('.git', '') : url
25 | return { repo, directory }
26 | }
27 |
28 | if (url.startsWith('github:')) {
29 | const user = url.replace('github:', '')
30 | const repo = `${GITHUB_URL}${user}`
31 | return { repo, directory }
32 | }
33 |
34 | return undefined
35 | }
36 |
37 | function getTips(label = DETAILS_LABEL) {
38 | return ['Please refer to', label, 'for details.'].join(' ')
39 | }
40 |
41 | interface GetNotesOptions {
42 | repoInfo?: RepoInfo
43 | branch: string
44 | changelog: string
45 | }
46 |
47 | export function getNotes({ repoInfo, branch, changelog }: GetNotesOptions) {
48 | if (!repoInfo) return getTips()
49 |
50 | const url = [repoInfo.repo, 'blob', branch, repoInfo.directory, changelog]
51 | .filter((i) => !!i)
52 | .join('/')
53 |
54 | const label = `[${DETAILS_LABEL}](${url})`
55 | return getTips(label)
56 | }
57 |
58 | export function getName(name: string) {
59 | if (name.startsWith('@')) {
60 | const [, scopedName] = name.split('/')
61 | return scopedName
62 | }
63 | return name
64 | }
65 |
--------------------------------------------------------------------------------
/packages/release/src/index.ts:
--------------------------------------------------------------------------------
1 | import { execSync } from 'node:child_process'
2 | import { resolve } from 'node:path'
3 | import { fse } from '@bassist/node-utils'
4 | import minimist from '@withtypes/minimist'
5 | import pkg from '../package.json'
6 | import { getName, getNotes, getRepo } from './utils'
7 |
8 | async function run() {
9 | const argv = minimist(process.argv.slice(2), {
10 | string: ['_', 'branch', 'changelog'],
11 | alias: {
12 | branch: 'b',
13 | changelog: 'c',
14 | },
15 | })
16 |
17 | const { branch = 'main', changelog = 'CHANGELOG.md' } = argv
18 |
19 | const cwd = process.cwd()
20 | const pkgPath = resolve(cwd, './package.json')
21 | const pkgJson = fse.readJsonSync(pkgPath) || {}
22 |
23 | const { name, version, repository } = pkgJson
24 | if (!name) {
25 | throw new Error(`[${pkg.name}] Missing package name`)
26 | }
27 | if (!version) {
28 | throw new Error(`[${pkg.name}] Missing package version`)
29 | }
30 | if (!repository) {
31 | throw new Error(`[${pkg.name}] Missing package repository`)
32 | }
33 |
34 | const repoInfo = getRepo(repository)
35 | if (!repoInfo) {
36 | throw new Error(`[${pkg.name}] Unsupported repository information`)
37 | }
38 |
39 | const notes = getNotes({
40 | repoInfo,
41 | branch,
42 | changelog,
43 | })
44 |
45 | const isMonorepo = !!repoInfo.directory
46 | const pkgName = getName(name)
47 | const tagName = isMonorepo ? `${pkgName}@${version}` : `v${version}`
48 |
49 | const releaseArgs = [
50 | 'gh --version',
51 | `git tag -a ${tagName} -m "${tagName}"`,
52 | `git push origin ${tagName}`,
53 | `gh release create ${tagName} --title "${tagName}" --notes "${notes}"`,
54 | ]
55 |
56 | const cmd = releaseArgs.join(' && ')
57 | execSync(cmd)
58 | }
59 | run().catch((e) => {
60 | console.log(e)
61 | })
62 |
--------------------------------------------------------------------------------
/packages/eslint-config/src/globs.ts:
--------------------------------------------------------------------------------
1 | export const GLOB_SRC_EXT = '?([cm])[jt]s?(x)'
2 | export const GLOB_SRC = '**/*.?([cm])[jt]s?(x)'
3 |
4 | export const GLOB_JS = '**/*.?([cm])js'
5 | export const GLOB_JSX = '**/*.?([cm])jsx'
6 |
7 | export const GLOB_TS = '**/*.?([cm])ts'
8 | export const GLOB_TSX = '**/*.?([cm])tsx'
9 |
10 | export const GLOB_STYLE = '**/*.{c,le,sc}ss'
11 | export const GLOB_CSS = '**/*.css'
12 | export const GLOB_LESS = '**/*.less'
13 | export const GLOB_SCSS = '**/*.scss'
14 |
15 | export const GLOB_JSON = '**/*.json'
16 | export const GLOB_JSON5 = '**/*.json5'
17 | export const GLOB_JSONC = '**/*.jsonc'
18 |
19 | export const GLOB_MARKDOWN = '**/*.md'
20 | export const GLOB_MARKDOWN_CODE = `${GLOB_MARKDOWN}/${GLOB_SRC}`
21 |
22 | export const GLOB_VUE = '**/*.vue'
23 | export const GLOB_SVELTE = '**/*.svelte'
24 | export const GLOB_YAML = '**/*.y?(a)ml'
25 | export const GLOB_HTML = '**/*.htm?(l)'
26 |
27 | export const GLOB_ALL_SRC = [
28 | GLOB_SRC,
29 | GLOB_STYLE,
30 | GLOB_JSON,
31 | GLOB_JSON5,
32 | GLOB_MARKDOWN,
33 | GLOB_VUE,
34 | GLOB_YAML,
35 | GLOB_HTML,
36 | ] as const
37 |
38 | export const GLOB_NODE_MODULES = '**/node_modules'
39 |
40 | export const GLOB_DIST = '**/dist'
41 |
42 | export const GLOB_LOCKFILE = [
43 | '**/package-lock.json',
44 | '**/yarn.lock',
45 | '**/pnpm-lock.yaml',
46 | ] as const
47 |
48 | export const GLOB_EXCLUDE = [
49 | GLOB_NODE_MODULES,
50 | GLOB_DIST,
51 | ...GLOB_LOCKFILE,
52 |
53 | '**/output',
54 | '**/coverage',
55 | '**/temp',
56 | '**/fixtures',
57 | '**/.vitepress/cache',
58 | '**/.nuxt',
59 | '**/.vercel',
60 | '**/.changeset',
61 | '**/.idea',
62 | '**/.output',
63 | '**/.vite-inspect',
64 | '**/.svelte-kit',
65 |
66 | '**/CHANGELOG*.md',
67 | '**/*.min.*',
68 | '**/LICENSE*',
69 | '**/__snapshots__',
70 | '**/auto-import?(s).d.ts',
71 | '**/components.d.ts',
72 | ] as const
73 |
--------------------------------------------------------------------------------
/packages/tsconfig/README.md:
--------------------------------------------------------------------------------
1 | # @bassist/tsconfig
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Some TSConfig files for working with TypeScript projects by [@chengpeiquan](https://github.com/chengpeiquan) .
19 |
20 | ## Usage
21 |
22 | With npm(or yarn, or pnpm):
23 |
24 | ```bash
25 | npm install -D @bassist/tsconfig
26 | ```
27 |
28 | In the `tsconfig.json` file, use the `extends` field to extends these configuration.
29 |
30 | ```json
31 | {
32 | "extends": "@bassist/tsconfig/base.json",
33 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
34 | }
35 | ```
36 |
37 | ## Extendable
38 |
39 | Base:
40 |
41 | ```json
42 | {
43 | "extends": "@bassist/tsconfig/base.json"
44 | }
45 | ```
46 |
47 | Web:
48 |
49 | ```json
50 | {
51 | "extends": "@bassist/tsconfig/web.json"
52 | }
53 | ```
54 |
55 | Node:
56 |
57 | ```json
58 | {
59 | "extends": "@bassist/tsconfig/node.json"
60 | }
61 | ```
62 |
63 | ## Release Notes
64 |
65 | Please refer to [CHANGELOG](https://github.com/chengpeiquan/bassist/blob/main/packages/tsconfig/CHANGELOG.md) for details.
66 |
67 | ## License
68 |
69 | MIT License © 2023-PRESENT [chengpeiquan](https://github.com/chengpeiquan)
70 |
--------------------------------------------------------------------------------
/packages/utils/README.md:
--------------------------------------------------------------------------------
1 | # @bassist/utils
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Opinionated collection of common JavaScript / TypeScript utils by [@chengpeiquan](https://github.com/chengpeiquan) .
19 |
20 | - 🌳 Fully tree shakeable
21 | - 💪 Type Strong
22 | - 💡 No bundler required
23 | - 🦄 SSR Friendly
24 |
25 | ## Usage
26 |
27 | With npm(or yarn, or pnpm):
28 |
29 | ```bash
30 | npm install @bassist/utils
31 | ```
32 |
33 | In `.js` / `.ts` or other files:
34 |
35 | ```ts
36 | import { isMobile } from '@bassist/utils'
37 |
38 | if (isMobile()) {
39 | // do something...
40 | }
41 | ```
42 |
43 | With CDN:
44 |
45 | ```html
46 |
47 |
55 | ```
56 |
57 | ## Documentation
58 |
59 | See: [Documentation of utils](https://jsdocs.io/package/@bassist/utils)
60 |
61 | ## Release Notes
62 |
63 | Please refer to [CHANGELOG](https://github.com/chengpeiquan/bassist/blob/main/packages/utils/CHANGELOG.md) for details.
64 |
65 | ## License
66 |
67 | MIT License © 2022-PRESENT [chengpeiquan](https://github.com/chengpeiquan)
68 |
--------------------------------------------------------------------------------
/packages/utils/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Browser Test
8 |
9 |
10 |
11 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/packages/node-utils/README.md:
--------------------------------------------------------------------------------
1 | # @bassist/node-utils
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Opinionated collection of common Node.js utils by [@chengpeiquan](https://github.com/chengpeiquan) .
19 |
20 | - 🌳 Fully tree shakeable
21 | - 💪 Type Strong
22 | - ⚡ Simplify complex operations
23 | - 🚀 Built-in [fs-extra](https://github.com/jprichardson/node-fs-extra)
24 |
25 | > Note: This package is only for use in Node.js, don't use it in the browser.
26 |
27 | ## Usage
28 |
29 | With npm(or yarn, or pnpm):
30 |
31 | ```bash
32 | npm install @bassist/node-utils
33 | ```
34 |
35 | In `.js` / `.ts` or other files:
36 |
37 | ```ts
38 | import { getPackageManagerByUserAgent } from '@bassist/node-utils'
39 |
40 | console.log(getPackageManagerByUserAgent())
41 | // { name: 'pnpm', version: '7.26.0' }
42 | ```
43 |
44 | ## Documentation
45 |
46 | See: [Documentation of node-utils](https://jsdocs.io/package/@bassist/node-utils)
47 |
48 | About `fse` API, see: [fs-extra](https://github.com/jprichardson/node-fs-extra) .
49 |
50 | ## Release Notes
51 |
52 | Please refer to [CHANGELOG](https://github.com/chengpeiquan/bassist/blob/main/packages/node-utils/CHANGELOG.md) for details.
53 |
54 | ## License
55 |
56 | MIT License © 2022-PRESENT [chengpeiquan](https://github.com/chengpeiquan)
57 |
--------------------------------------------------------------------------------
/packages/utils/test/query.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 | import { extractQueryInfo, getQuery, parseQuery, stringifyQuery } from '..'
3 |
4 | describe('query', () => {
5 | it('parseQuery', () => {
6 | expect(parseQuery('https://example.com/?a=1&b=2')).toStrictEqual({
7 | a: '1',
8 | b: '2',
9 | })
10 |
11 | expect(parseQuery('https://example.com/?a= &b=')).toStrictEqual({
12 | a: ' ',
13 | b: '',
14 | })
15 |
16 | expect(
17 | parseQuery('https://example.com/?url=https%3A%2F%2Fexample.com'),
18 | ).toStrictEqual({
19 | url: 'https://example.com',
20 | })
21 |
22 | expect(parseQuery('https://example.com/?nums=1,2,3')).toStrictEqual({
23 | nums: '1,2,3',
24 | })
25 |
26 | expect(parseQuery('https://example.com/#/foo?a=1&b=2')).toStrictEqual({
27 | a: '1',
28 | b: '2',
29 | })
30 |
31 | expect(parseQuery('https://example.com/?a=1&b=2#/foo')).toStrictEqual({
32 | a: '1',
33 | b: '2',
34 | })
35 | })
36 |
37 | it('extractQueryInfo', () => {
38 | expect(
39 | extractQueryInfo('https://example.com/?path=%2Ffoo&a=1&b=2'),
40 | ).toStrictEqual({
41 | path: '/foo',
42 | params: {
43 | a: '1',
44 | b: '2',
45 | },
46 | })
47 |
48 | expect(extractQueryInfo('https://example.com/?a=1&b=2')).toStrictEqual({
49 | path: '',
50 | params: {
51 | a: '1',
52 | b: '2',
53 | },
54 | })
55 | })
56 |
57 | it('getQuery', () => {
58 | expect(getQuery('b', 'https://example.com/?a=1&b=2')).toBe('2')
59 |
60 | expect(
61 | getQuery('url', 'https://example.com/?url=https%3A%2F%2Fexample.com'),
62 | ).toBe('https://example.com')
63 | })
64 |
65 | it('stringifyQuery', () => {
66 | expect(
67 | stringifyQuery({
68 | a: 1,
69 | b: 2,
70 | }),
71 | ).toBe('a=1&b=2')
72 |
73 | expect(
74 | stringifyQuery({
75 | url: 'https://example.com',
76 | }),
77 | ).toBe('url=https%3A%2F%2Fexample.com')
78 | })
79 | })
80 |
--------------------------------------------------------------------------------
/packages/eslint-config/src/private-configs/tailwindcss.ts:
--------------------------------------------------------------------------------
1 | import tailwindcssPlugin from 'eslint-plugin-tailwindcss'
2 | import { getConfigName } from '../shared/utils'
3 | import { type FlatESLintConfig } from '../types'
4 |
5 | export { tailwindcssPlugin }
6 |
7 | const recommendedConfigs = tailwindcssPlugin.configs[
8 | 'flat/recommended'
9 | ] as FlatESLintConfig[]
10 |
11 | export interface TailwindcssSettings {
12 | callees?: string[]
13 | config?: string
14 | cssFiles?: string[]
15 | cssFilesRefreshRate?: number
16 | removeDuplicates?: boolean
17 | skipClassAttribute?: boolean
18 | whitelist?: string[]
19 | tags?: string[]
20 | classRegex?: string
21 | }
22 |
23 | export const defaultTailwindcssSettings: TailwindcssSettings = {
24 | callees: ['cn', 'classNames', 'clsx'],
25 |
26 | // `tailwind.config.ts` is no longer supported
27 | // https://github.com/tailwindlabs/tailwindcss/discussions/15352
28 | config: 'tailwind.config.js', // returned from `loadConfig()` utility if not provided
29 |
30 | cssFiles: ['!**/node_modules', '!**/.*', '!**/dist'],
31 | cssFilesRefreshRate: 5_000,
32 | removeDuplicates: true,
33 | skipClassAttribute: false,
34 | whitelist: ['-webkit-box'],
35 | tags: [], // can be set to e.g. ['tw'] for use in tw`bg-blue`
36 | classRegex: '^class(Name)?$', // can be modified to support custom attributes. E.g. "^tw$" for `twin.macro`
37 | }
38 |
39 | const mergeSettings = (inputSettings: TailwindcssSettings) => {
40 | if (inputSettings && Object.keys(inputSettings).length > 0) {
41 | return {
42 | ...defaultTailwindcssSettings,
43 | ...inputSettings,
44 | }
45 | }
46 |
47 | return { ...defaultTailwindcssSettings }
48 | }
49 |
50 | export const createTailwindcssConfig = (
51 | inputSettings: TailwindcssSettings = {},
52 | ) => {
53 | const resolvedSettings = mergeSettings(inputSettings)
54 |
55 | const tailwindcss: FlatESLintConfig[] = [
56 | ...recommendedConfigs,
57 | {
58 | name: getConfigName('tailwindcss', 'settings'),
59 | settings: {
60 | tailwindcss: resolvedSettings,
61 | },
62 | },
63 | ]
64 |
65 | return tailwindcss
66 | }
67 |
--------------------------------------------------------------------------------
/packages/utils/src/random.ts:
--------------------------------------------------------------------------------
1 | import { desktopUserAgents, mobileUserAgents, userAgents } from './ua'
2 |
3 | /**
4 | * Generate random number
5 | *
6 | * @category Random
7 | * @param min - The minimum value in the range
8 | * @param max - The maximum value in the range
9 | * @param roundingType - Round the result
10 | */
11 | export function randomNumber(
12 | min = 0,
13 | max = 100,
14 | roundingType: 'round' | 'ceil' | 'floor' = 'round',
15 | ) {
16 | return Math[roundingType](Math.random() * (max - min) + min)
17 | }
18 |
19 | /**
20 | * Generate random string
21 | *
22 | * @category Random
23 | * @param length - The length of the string to be returned
24 | */
25 | export function randomString(length = 10) {
26 | // https://github.com/ai/nanoid/blob/main/url-alphabet/index.js
27 | const dict =
28 | 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict'
29 |
30 | let str = ''
31 | let i = length
32 | const len = dict.length
33 | while (i--) str += dict[(Math.random() * len) | 0]
34 | return str
35 | }
36 |
37 | /**
38 | * Generate random boolean
39 | *
40 | * @category Random
41 | */
42 | export function randomBoolean() {
43 | const index = randomNumber(0, 1)
44 | return [true, false][index]
45 | }
46 |
47 | /**
48 | * Shuffle the array and sort it randomly
49 | *
50 | * @category Random
51 | */
52 | export function shuffle(arr: any[]): any[] {
53 | if (!Array.isArray(arr)) return arr
54 |
55 | for (let i = arr.length - 1; i > 0; i--) {
56 | const j: number = Math.floor(Math.random() * (i + 1))
57 | const item: any = arr[i]
58 | arr[i] = arr[j]
59 | arr[j] = item
60 | }
61 |
62 | return arr
63 | }
64 |
65 | /**
66 | * Randomly returns a mocked UA
67 | *
68 | * @category Random
69 | * @param device - Get a random UA on the specified device type
70 | */
71 | export function randomUserAgent(device?: 'desktop' | 'mobile') {
72 | const uaList =
73 | device === 'desktop'
74 | ? desktopUserAgents
75 | : device === 'mobile'
76 | ? mobileUserAgents
77 | : userAgents
78 |
79 | const index = randomNumber(0, uaList.length - 1)
80 | return uaList[index]
81 | }
82 |
--------------------------------------------------------------------------------
/packages/utils/src/storage/fallback.ts:
--------------------------------------------------------------------------------
1 | import { hasKey } from '../data'
2 |
3 | // A record to store instances of FallbackStorage based on their prefix
4 | const fallbackStorageRecord: Record = {}
5 |
6 | /**
7 | * FallbackStorage class provides a fallback storage implementation when running
8 | * outside the browser environment
9 | *
10 | * @category Storage
11 | */
12 | export class FallbackStorage {
13 | private data: Record
14 |
15 | /**
16 | * Creates an instance of FallbackStorage.
17 | *
18 | * @param prefix - The prefix to be added to the storage keys.
19 | */
20 | constructor(prefix: string) {
21 | this.data = {}
22 |
23 | const hasRecord = hasKey(fallbackStorageRecord, prefix)
24 |
25 | // If a record with the same prefix exists, use its data
26 | // Otherwise, store the current instance in the record
27 | this.data = hasRecord ? fallbackStorageRecord[prefix].data : {}
28 |
29 | if (!hasRecord) {
30 | fallbackStorageRecord[prefix] = this
31 | }
32 | }
33 |
34 | /**
35 | * Gets the number of stored items
36 | */
37 | get length() {
38 | return Object.keys(this.data).length
39 | }
40 |
41 | /**
42 | * Clears all stored items.
43 | */
44 | clear() {
45 | this.data = {}
46 | }
47 |
48 | /**
49 | * Retrieves the value associated with the specified key
50 | */
51 | getItem(key: string) {
52 | if (hasKey(this.data, key)) {
53 | return this.data[key]
54 | }
55 | return null
56 | }
57 |
58 | /**
59 | * Sets the value for the specified key.
60 | */
61 | setItem(key: string, value: string) {
62 | this.data[key] = value
63 | }
64 |
65 | /**
66 | * Removes the item associated with the specified key.
67 | */
68 | removeItem(key: string) {
69 | if (hasKey(this.data, key)) {
70 | // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
71 | delete this.data[key]
72 | }
73 | }
74 |
75 | /**
76 | * Retrieves the key at the specified index.
77 | */
78 | key(index: number) {
79 | const keys = Object.keys(this.data)
80 | if (index > keys.length) return null
81 | return keys[index]
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/packages/eslint-config/src/configs/typescript.ts:
--------------------------------------------------------------------------------
1 | import _tsPlugin from '@typescript-eslint/eslint-plugin'
2 | import __tsParser from '@typescript-eslint/parser'
3 | import { GLOB_TS, GLOB_TSX } from '../globs'
4 | import { getConfigName } from '../shared/utils'
5 | import {
6 | type FlatESLintConfig,
7 | type FlatESLintParser,
8 | type FlatESLintPlugin,
9 | } from '../types'
10 |
11 | const tsParser = __tsParser as FlatESLintParser
12 | const tsPlugin = _tsPlugin as unknown as FlatESLintPlugin
13 |
14 | export { tsParser, tsPlugin }
15 |
16 | const tsPluginConfigs = _tsPlugin.configs
17 | const recommendedConfigs = tsPluginConfigs['eslint-recommended']
18 |
19 | export const typescript: FlatESLintConfig[] = [
20 | {
21 | name: getConfigName('typescript', 'base'),
22 | files: [GLOB_TS, GLOB_TSX],
23 | languageOptions: {
24 | parser: tsParser,
25 | parserOptions: {
26 | sourceType: 'module',
27 | },
28 | },
29 | plugins: {
30 | '@typescript-eslint': tsPlugin,
31 | },
32 | rules: {
33 | ...recommendedConfigs.overrides![0].rules,
34 | ...tsPluginConfigs.strict.rules,
35 | '@typescript-eslint/no-redeclare': 'error',
36 | '@typescript-eslint/ban-ts-comment': 'off',
37 | '@typescript-eslint/ban-types': 'off',
38 | '@typescript-eslint/consistent-type-imports': [
39 | 'error',
40 | {
41 | fixStyle: 'inline-type-imports',
42 | disallowTypeAnnotations: false,
43 | },
44 | ],
45 | '@typescript-eslint/no-explicit-any': 'off',
46 | '@typescript-eslint/no-non-null-assertion': 'off',
47 | '@typescript-eslint/prefer-as-const': 'warn',
48 | },
49 | },
50 | {
51 | name: getConfigName('typescript', 'declaration'),
52 | files: ['**/*.d.ts'],
53 | rules: {
54 | 'import/no-duplicates': 'off',
55 | },
56 | },
57 | {
58 | name: getConfigName('typescript', 'test'),
59 | files: ['**/*.{test,spec}.ts?(x)'],
60 | rules: {
61 | 'no-unused-expressions': 'off',
62 | },
63 | },
64 | {
65 | name: getConfigName('typescript', 'js-compat'),
66 | files: ['**/*.js', '**/*.cjs'],
67 | rules: {
68 | '@typescript-eslint/no-var-requires': 'off',
69 | },
70 | },
71 | ]
72 |
--------------------------------------------------------------------------------
/packages/eslint-config/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # [0.3.0](https://github.com/chengpeiquan/bassist/compare/eslint-config@0.2.0...eslint-config@0.3.0) (2025-07-27)
2 |
3 |
4 | ### Features
5 |
6 | * **eslint-config:** upgrade dependencies and add pangu space formatting support for markdown ([499f888](https://github.com/chengpeiquan/bassist/commit/499f888a88f3abe8eb03c09655f36282481ec5b9))
7 |
8 |
9 |
10 | # [0.2.0](https://github.com/chengpeiquan/bassist/compare/eslint-config@0.1.3...eslint-config@0.2.0) (2025-07-06)
11 |
12 |
13 | ### Features
14 |
15 | * **eslint-config:** add built-in Prettier rules ([0b88636](https://github.com/chengpeiquan/bassist/commit/0b8863628fd7751fb3ec810d1204f2d40a462e02))
16 |
17 |
18 |
19 | ## [0.1.3](https://github.com/chengpeiquan/bassist/compare/eslint-config@0.1.2...eslint-config@0.1.3) (2025-05-22)
20 |
21 |
22 | ### Bug Fixes
23 |
24 | * **eslint-config:** update deps ([e6c47c7](https://github.com/chengpeiquan/bassist/commit/e6c47c70a6c51c757d9f958d9711f2da597f3ee4))
25 |
26 |
27 |
28 | ## [0.1.2](https://github.com/chengpeiquan/bassist/compare/eslint-config@0.1.1...eslint-config@0.1.2) (2025-03-09)
29 |
30 |
31 | ### Bug Fixes
32 |
33 | * **eslint-config:** export createGetConfigNameFactory ([2967b04](https://github.com/chengpeiquan/bassist/commit/2967b0443ea7f9c787d48d857132b5ece5142dc4))
34 | * **eslint-config:** no need for prettier sort import built-in, which will cause global lint errors ([419b065](https://github.com/chengpeiquan/bassist/commit/419b065dbcff242a742702aa9ef11fbc7266d7dd))
35 | * **eslint-config:** use eslint-plugin-import-x instead of eslint-plugin-import ([9bafd37](https://github.com/chengpeiquan/bassist/commit/9bafd371e31795bff8b393dc2afd46b3a2901918))
36 |
37 |
38 |
39 | ## [0.1.1](https://github.com/chengpeiquan/bassist/compare/eslint-config@0.1.0...eslint-config@0.1.1) (2025-03-09)
40 |
41 |
42 | ### Bug Fixes
43 |
44 | * **eslint-config:** missing javascript config name ([a817534](https://github.com/chengpeiquan/bassist/commit/a817534cf4f5351ee21f4fdf7a6b366ce960e49f))
45 |
46 |
47 |
48 | # 0.1.0 (2025-03-08)
49 |
50 |
51 | ### Features
52 |
53 | * **eslint-config:** add flat configurations for ESLint v9 ([3f649fb](https://github.com/chengpeiquan/bassist/commit/3f649fb55b7eee40b1e8fe79a4820b9f3a5ffde0))
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/packages/utils/src/appearance.ts:
--------------------------------------------------------------------------------
1 | import { isBrowser } from './device'
2 |
3 | /**
4 | * The preferred color scheme for the appearance of the user interface
5 | *
6 | * @category Appearance
7 | */
8 | export type PrefersColorScheme = 'light' | 'dark'
9 |
10 | /**
11 | * Dark mode media query
12 | *
13 | * @category Appearance
14 | */
15 | export const darkMediaQuery = isBrowser
16 | ? window.matchMedia('(prefers-color-scheme: dark)')
17 | : undefined
18 |
19 | /**
20 | * Checks if the user's preferred color scheme is dark
21 | *
22 | * @category Appearance
23 | */
24 | export function isDark() {
25 | if (!isBrowser) return false
26 | return darkMediaQuery!.matches
27 | }
28 |
29 | /**
30 | * Light mode media query
31 | *
32 | * @category Appearance
33 | */
34 | export const lightMediaQuery = isBrowser
35 | ? window.matchMedia('(prefers-color-scheme: light)')
36 | : undefined
37 |
38 | /**
39 | * Checks if the user's preferred color scheme is light
40 | *
41 | * @category Appearance
42 | */
43 | export function isLight() {
44 | if (!isBrowser) return false
45 | return lightMediaQuery!.matches
46 | }
47 |
48 | /**
49 | * Retrieves the user's preferred color scheme
50 | *
51 | * @category Appearance
52 | */
53 | export function getPrefersColorScheme(): PrefersColorScheme | undefined {
54 | if (isDark()) return 'dark'
55 | if (isLight()) return 'light'
56 | return undefined
57 | }
58 |
59 | /**
60 | * Portrait orientation media query
61 | *
62 | * @category Appearance
63 | */
64 | export const portraitMediaQuery = isBrowser
65 | ? window.matchMedia('(orientation: portrait)')
66 | : undefined
67 |
68 | /**
69 | * Check whether the current screen is in portrait mode
70 | *
71 | * @category Appearance
72 | */
73 | export function isPortrait() {
74 | if (!isBrowser) return false
75 | return portraitMediaQuery!.matches
76 | }
77 |
78 | /**
79 | * Landscape orientation media query
80 | *
81 | * @category Appearance
82 | */
83 | export const landscapeMediaQuery = isBrowser
84 | ? window.matchMedia('(orientation: landscape)')
85 | : undefined
86 |
87 | /**
88 | * Check whether the current screen is in landscape mode
89 | *
90 | * @category Appearance
91 | */
92 | export function isLandscape() {
93 | if (!isBrowser) return false
94 | return landscapeMediaQuery!.matches
95 | }
96 |
--------------------------------------------------------------------------------
/packages/eslint-config/src/shared/prettier-config.mjs:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {import('../types').PartialPrettierExtendedOptions} PartialPrettierExtendedOptions
3 | */
4 |
5 | /**
6 | * Prettier Config Plugins
7 | *
8 | * @type {NonNullable}
9 | */
10 | export const defaultPrettierPlugins = [
11 | 'prettier-plugin-jsdoc',
12 | 'prettier-plugin-lint-md',
13 | ]
14 |
15 | /**
16 | * Prettier Config
17 | *
18 | * @type {PartialPrettierExtendedOptions}
19 | */
20 | export default {
21 | /**
22 | * Prettier Options
23 | *
24 | * @see https://prettier.io/docs/options
25 | */
26 |
27 | // Maximum 80 characters per line
28 | printWidth: 80,
29 | // Use 2 spaces for indentation
30 | tabWidth: 2,
31 | // Use spaces instead of tabs for indentation
32 | useTabs: false,
33 | // Add semicolons at the end of statements
34 | semi: false,
35 | // Use single quotes instead of double quotes
36 | singleQuote: true,
37 | // Only quote object properties when necessary
38 | quoteProps: 'as-needed',
39 | // Use double quotes instead of single quotes in JSX
40 | jsxSingleQuote: false,
41 | // Add trailing commas wherever possible
42 | trailingComma: 'all',
43 | // Add spaces between brackets in object literals
44 | bracketSpacing: true,
45 | // Put the > of JSX elements at the end of the last line
46 | jsxBracketSameLine: false,
47 | // Always include parentheses around arrow function parameters
48 | arrowParens: 'always',
49 | // Format the entire file content
50 | rangeStart: 0,
51 | rangeEnd: Infinity,
52 | // Don't require @prettier pragma at the beginning of files
53 | requirePragma: false,
54 | // Don't automatically insert @prettier pragma at the beginning of files
55 | insertPragma: false,
56 | // Use default prose wrapping standard
57 | proseWrap: 'preserve',
58 | // Determine HTML line breaks based on display style
59 | htmlWhitespaceSensitivity: 'css',
60 | // Use LF line endings
61 | endOfLine: 'lf',
62 | // Explicitly specify plugins here to ensure Prettier loads custom plugins
63 | // (like jsdoc and lint-md) when run directly via CLI.
64 | plugins: [...defaultPrettierPlugins],
65 |
66 | /**
67 | * JSDoc Options
68 | *
69 | * @see https://github.com/hosseinmd/prettier-plugin-jsdoc#options
70 | */
71 |
72 | jsdocCommentLineStrategy: 'multiline',
73 | }
74 |
--------------------------------------------------------------------------------
/packages/utils/src/clipboard/index.ts:
--------------------------------------------------------------------------------
1 | import { isBrowser } from '../device'
2 | import { fallbackReadText, fallbackWriteText } from './fallback'
3 |
4 | /**
5 | * @category Interactive
6 | */
7 | export type WritableElement = HTMLInputElement | HTMLTextAreaElement
8 |
9 | /**
10 | * @category Interactive
11 | */
12 | export type CopyableElement = HTMLElement | WritableElement
13 |
14 | /**
15 | * Extensions based on the Clipboard API and with fallback mechanism
16 | *
17 | * @category Interactive
18 | */
19 | export class ClipboardInstance {
20 | /**
21 | * Determine whether the clipboard is supported
22 | */
23 | isSupported: boolean
24 |
25 | constructor() {
26 | this.isSupported = !isBrowser
27 | ? false
28 | : !!navigator.clipboard || !!document.execCommand
29 | }
30 |
31 | /**
32 | * Copy the text passed in or the text of the specified DOM element
33 | *
34 | * @example
35 | * ;```
36 | * clipboard.copy(document.querySelector('.foo'))
37 | * ```
38 | */
39 | async copy(el: CopyableElement) {
40 | if (!this.isSupported) return false
41 | const clipText = el.innerText || (el as WritableElement).value
42 | return await this.write(clipText)
43 | }
44 |
45 | /**
46 | * Cut the text passed in or the text of the specified DOM element
47 | *
48 | * @example
49 | * ;```
50 | * clipboard.cut(document.querySelector('.foo'))
51 | * ```
52 | */
53 | async cut(el: WritableElement) {
54 | if (!this.isSupported) return false
55 | const isOk = await this.copy(el)
56 | if (!isOk) return false
57 | el.value = ''
58 | return true
59 | }
60 |
61 | /**
62 | * Read the text content of the clipboard
63 | */
64 | async read() {
65 | if (!this.isSupported) return ''
66 | try {
67 | return await navigator!.clipboard.readText()
68 | } catch {
69 | return fallbackReadText()
70 | }
71 | }
72 |
73 | /**
74 | * Write text content to clipboard
75 | */
76 | async write(text: string) {
77 | if (!this.isSupported) return false
78 | try {
79 | await navigator!.clipboard.writeText(text)
80 | return true
81 | } catch {
82 | return fallbackWriteText(text)
83 | }
84 | }
85 | }
86 |
87 | /**
88 | * Initialized Clipboard Instance
89 | *
90 | * @category Interactive
91 | */
92 | export const clipboard = new ClipboardInstance()
93 |
--------------------------------------------------------------------------------
/packages/build-config/README.zh-CN.md:
--------------------------------------------------------------------------------
1 | # @bassist/build-config
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | 一些常用的构建工具配置,由 [@chengpeiquan](https://github.com/chengpeiquan) 精心打造。
19 |
20 | 目前提供了以下构建工具的配置:
21 |
22 | - [tsup](https://github.com/egoist/tsup): 基于 ESBuild,构建 TypeScript 库最便捷的工具
23 |
24 | ## 🤔 为什么需要这个?
25 |
26 | 单一仓库使用某些工具的配置确实已经足够方便,但如果有很多个仓库都用相似的配置,这个过程就会显得繁琐。
27 |
28 | 把一些常用的工具配置抽象出来共享,不同的项目可以更快完成配置。
29 |
30 | ## ⚡ 用法:基于 tsup
31 |
32 | - 🎯 **预设配置**: 提供开箱即用的 tsup 基础配置
33 | - 📦 **多格式支持**: 默认支持 CommonJS 和 ESM 格式
34 | - 🏷️ **自动 Banner**: 根据 package.json 自动生成文件头注释
35 | - 🧹 **自动清理**: 构建前自动清理输出目录
36 | - 📝 **类型声明**: 自动生成 TypeScript 类型声明文件
37 | - ⚡ **代码压缩**: 内置代码压缩功能
38 |
39 | ### 安装
40 |
41 | 使用常用的包管理器安装该包:
42 |
43 | ```bash
44 | npm install -D @bassist/build-config tsup
45 | ```
46 |
47 | **注意**: 该子模块需要 tsup 作为 peer dependency ,如上方安装命令,请确保已安装 tsup 。
48 |
49 | ### 用法
50 |
51 | 统一由 `@bassist/build-config/tsup` 提供所有 API 。
52 |
53 | 通常情况下进需要直接使用 `createBaseConfig` 返回的配置,可视情况传入自定义选项:
54 |
55 | ```ts
56 | import { createBaseConfig } from '@bassist/build-config/tsup'
57 | import { defineConfig } from 'tsup'
58 | import pkg from './package.json'
59 |
60 | const config = createBaseConfig({ pkg })
61 |
62 | export default defineConfig(config)
63 | ```
64 |
65 | 如果配置项不满足,可以通过对象解构与合并,自行传给 `defineConfig` API 。
66 |
67 | 更多 API 和配置项请查看源码 [tsup.ts](https://github.com/chengpeiquan/bassist/blob/main/packages/build-config/src/tsup.ts) 。
68 |
69 | ## 📝 发布日志
70 |
71 | 详细更新内容请参考 [CHANGELOG](https://github.com/chengpeiquan/bassist/blob/main/packages/build-config/CHANGELOG.md) 。
72 |
73 | ## 📜 License
74 |
75 | MIT License © 2025-PRESENT [chengpeiquan](https://github.com/chengpeiquan)
76 |
--------------------------------------------------------------------------------
/packages/eslint-config/src/private-configs/prettier.ts:
--------------------------------------------------------------------------------
1 | import { readFileSync } from 'node:fs'
2 | import { join } from 'node:path'
3 | import prettierConfig from 'eslint-config-prettier'
4 | import prettierPlugin from 'eslint-plugin-prettier'
5 | import { GLOB_EXCLUDE } from '../globs'
6 | import defaultPrettierConfig, {
7 | defaultPrettierPlugins,
8 | } from '../shared/prettier-config.mjs'
9 | import { getConfigName } from '../shared/utils'
10 | import {
11 | type PartialPrettierExtendedOptions,
12 | type FlatESLintConfig,
13 | } from '../types'
14 |
15 | export { prettierPlugin }
16 |
17 | const isValidPrettierRules = (
18 | prettierRules: unknown,
19 | ): prettierRules is PartialPrettierExtendedOptions => {
20 | return Object.prototype.toString.call(prettierRules) === '[object Object]'
21 | }
22 |
23 | const loadPrettierConfig = (cwd: string): PartialPrettierExtendedOptions => {
24 | try {
25 | const prettierrc = readFileSync(join(cwd, '.prettierrc'), 'utf-8')
26 | return prettierrc ? JSON.parse(prettierrc) : {}
27 | } catch {
28 | return { ...defaultPrettierConfig }
29 | }
30 | }
31 |
32 | const loadPrettierIgnore = (cwd: string): string[] => {
33 | try {
34 | const prettierignore = readFileSync(join(cwd, '.prettierignore'), 'utf-8')
35 | return prettierignore
36 | .split('\n')
37 | .map((line) => line.trim())
38 | .filter((line) => line && !line.startsWith('#'))
39 | } catch {
40 | return []
41 | }
42 | }
43 |
44 | export const createPrettierConfig = (
45 | cwd: string,
46 | inputConfig?: PartialPrettierExtendedOptions,
47 | ) => {
48 | const resolvedConfig = isValidPrettierRules(inputConfig)
49 | ? inputConfig
50 | : loadPrettierConfig(cwd)
51 |
52 | const { plugins = [] } = resolvedConfig
53 |
54 | const finalPrettierConfig: PartialPrettierExtendedOptions = {
55 | ...resolvedConfig,
56 |
57 | // Ensure plugins are unique to avoid duplicate loading
58 | plugins: Array.from(new Set([...plugins, ...defaultPrettierPlugins])),
59 | }
60 |
61 | const resolvedIgnore = loadPrettierIgnore(cwd)
62 |
63 | const finalIgnore = Array.from(new Set([...GLOB_EXCLUDE, ...resolvedIgnore]))
64 |
65 | const prettier: FlatESLintConfig[] = [
66 | {
67 | name: getConfigName('prettier'),
68 | plugins: {
69 | prettier: prettierPlugin,
70 | },
71 | rules: {
72 | ...prettierConfig.rules,
73 | 'prettier/prettier': ['warn', finalPrettierConfig],
74 | 'arrow-body-style': 'off',
75 | 'prefer-arrow-callback': 'off',
76 | },
77 | ignores: finalIgnore,
78 | },
79 | ]
80 |
81 | return prettier
82 | }
83 |
--------------------------------------------------------------------------------
/packages/utils/test/performance.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | type Mock,
3 | afterEach,
4 | beforeEach,
5 | describe,
6 | expect,
7 | it,
8 | vi,
9 | } from 'vitest'
10 | import { sleep, debounce, throttle } from '..'
11 |
12 | const mockFn = vi.fn()
13 |
14 | function advanceTimersByTime(ms: number) {
15 | return new Promise((resolve) => {
16 | setTimeout(resolve, ms)
17 | vi.advanceTimersByTime(ms)
18 | })
19 | }
20 |
21 | describe('performance', () => {
22 | it('sleep', async () => {
23 | async function diff(ms: number) {
24 | const start = Date.now()
25 | await sleep(ms)
26 | const end = Date.now()
27 | return end - start > ms
28 | }
29 |
30 | expect(await diff(0)).toBeTruthy()
31 | expect(await diff(500)).toBeTruthy()
32 | expect(await diff(2000)).toBeTruthy()
33 | expect(await diff(3000)).toBeTruthy()
34 | }, 100000)
35 |
36 | it('debounce', async () => {
37 | const delayedFn = debounce(mockFn, 100)
38 |
39 | it('should delay execution by specified time', async () => {
40 | delayedFn()
41 | expect(mockFn).not.toBeCalled()
42 | await advanceTimersByTime(100)
43 | expect(mockFn).toBeCalled()
44 | })
45 |
46 | it('should execute only the last call when called continuously', async () => {
47 | delayedFn()
48 | delayedFn()
49 | delayedFn()
50 | await advanceTimersByTime(100)
51 | expect(mockFn).toHaveBeenCalledTimes(1)
52 | })
53 |
54 | it('should pass arguments to the debounced function', async () => {
55 | delayedFn(1, 2, 3)
56 | await advanceTimersByTime(100)
57 | expect(mockFn).toBeCalledWith(1, 2, 3)
58 | })
59 | })
60 |
61 | it('throttle', async () => {
62 | let testFunction: Mock
63 | let throttledFunction: (this: unknown, ...args: any) => void
64 |
65 | beforeEach(() => {
66 | testFunction = vi.fn()
67 | throttledFunction = throttle(testFunction, 1000)
68 | vi.useFakeTimers()
69 | })
70 |
71 | afterEach(() => {
72 | vi.clearAllTimers()
73 | })
74 |
75 | it('should call the function immediately when called for the first time', () => {
76 | throttledFunction()
77 | expect(testFunction).toHaveBeenCalledTimes(1)
78 | })
79 |
80 | it('should not call the function again within the time limit', () => {
81 | throttledFunction()
82 | vi.advanceTimersByTime(500)
83 | throttledFunction()
84 | expect(testFunction).toHaveBeenCalledTimes(1)
85 | })
86 |
87 | it('should call the function again after the time limit', () => {
88 | throttledFunction()
89 | vi.advanceTimersByTime(1000)
90 | throttledFunction()
91 | expect(testFunction).toHaveBeenCalledTimes(2)
92 | })
93 | })
94 | })
95 |
--------------------------------------------------------------------------------
/packages/eslint-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@bassist/eslint-config",
3 | "version": "0.3.0",
4 | "description": "A modern flat ESLint configuration for ESLint, crafted by @chengpeiquan .",
5 | "author": "chengpeiquan ",
6 | "license": "MIT",
7 | "homepage": "https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/README.md",
8 | "files": [
9 | "dist"
10 | ],
11 | "main": "./dist/index.cjs",
12 | "module": "./dist/index.mjs",
13 | "types": "./dist/index.d.ts",
14 | "typesVersions": {
15 | "*": {
16 | "prettier-config": [
17 | "./dist/prettier-config.d.ts"
18 | ],
19 | "*": [
20 | "./dist/index.d.ts",
21 | "./dist/*"
22 | ]
23 | }
24 | },
25 | "exports": {
26 | "./prettier-config": {
27 | "types": "./dist/prettier-config.d.ts",
28 | "import": "./dist/prettier-config.mjs",
29 | "require": "./dist/prettier-config.cjs"
30 | },
31 | ".": {
32 | "types": "./dist/index.d.ts",
33 | "import": "./dist/index.mjs",
34 | "require": "./dist/index.cjs"
35 | }
36 | },
37 | "repository": {
38 | "type": "git",
39 | "url": "https://github.com/chengpeiquan/bassist"
40 | },
41 | "keywords": [
42 | "eslint",
43 | "eslint config",
44 | "eslint flat config"
45 | ],
46 | "scripts": {
47 | "build": "tsup"
48 | },
49 | "devDependencies": {
50 | "@lint-md/core": "^2.0.0",
51 | "@types/eslint-config-prettier": "^6.11.3",
52 | "@types/eslint-plugin-tailwindcss": "^3.17.0",
53 | "eslint": "^9.32.0",
54 | "prettier": "^3.6.2",
55 | "typescript": "^5.8.3"
56 | },
57 | "dependencies": {
58 | "@eslint/js": "^9.32.0",
59 | "@eslint/markdown": "^7.1.0",
60 | "@next/eslint-plugin-next": "^15.4.4",
61 | "@typescript-eslint/eslint-plugin": "^8.38.0",
62 | "@typescript-eslint/parser": "^8.38.0",
63 | "eslint-config-prettier": "^10.1.8",
64 | "eslint-plugin-import-x": "^4.16.1",
65 | "eslint-plugin-n": "^17.21.1",
66 | "eslint-plugin-prettier": "^5.5.3",
67 | "eslint-plugin-react": "^7.37.5",
68 | "eslint-plugin-react-hooks": "^5.2.0",
69 | "eslint-plugin-react-refresh": "^0.4.20",
70 | "eslint-plugin-regexp": "^2.9.0",
71 | "eslint-plugin-tailwindcss": "^3.18.2",
72 | "eslint-plugin-unicorn": "^57.0.0",
73 | "eslint-plugin-vue": "^9.33.0",
74 | "globals": "^16.3.0",
75 | "prettier-plugin-jsdoc": "^1.3.3",
76 | "prettier-plugin-lint-md": "^1.0.1",
77 | "vue-eslint-parser": "^9.4.3"
78 | },
79 | "peerDependencies": {
80 | "eslint": ">=9.0.0",
81 | "prettier": ">=3.0.0",
82 | "typescript": ">=5.0.0"
83 | },
84 | "peerDependenciesMeta": {
85 | "typescript": {
86 | "optional": true
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@bassist/monorepo",
3 | "version": "0.0.0",
4 | "description": "I play bass, so enjoy the name.",
5 | "author": "chengpeiquan ",
6 | "license": "MIT",
7 | "private": true,
8 | "packageManager": "pnpm@10.13.1",
9 | "type": "module",
10 | "scripts": {
11 | "build": "turbo run build",
12 | "build:lib": "jiti ./scripts/build",
13 | "pkg:publish": "jiti ./scripts/publish",
14 | "pkg:changelog": "jiti ./scripts/changelog.cts",
15 | "test": "vitest",
16 | "lint": "eslint packages --ext .js,.ts,.jsx,.tsx,.md",
17 | "lint:inspector": "npx @eslint/config-inspector",
18 | "format": "pnpm exec prettier --write packages",
19 | "mirror:get": "npm config get registry",
20 | "mirror:set": "npm config set registry https://registry.npmmirror.com",
21 | "mirror:rm": "npm config rm registry",
22 | "up": "npx taze minor -r -f -w -i",
23 | "backup": "git add . && git commit -m \"chore: backup\" && git push",
24 | "prepare": "husky install"
25 | },
26 | "dependencies": {
27 | "nprogress": "^0.2.0"
28 | },
29 | "devDependencies": {
30 | "@commitlint/cli": "^19.8.1",
31 | "@commitlint/config-conventional": "^19.8.1",
32 | "@eslint/js": "^9.32.0",
33 | "@eslint/markdown": "^7.1.0",
34 | "@lint-md/core": "^2.0.0",
35 | "@next/eslint-plugin-next": "^15.4.4",
36 | "@types/eslint-config-prettier": "^6.11.3",
37 | "@types/eslint-plugin-tailwindcss": "^3.17.0",
38 | "@types/fs-extra": "^11.0.4",
39 | "@types/node": "^24.1.0",
40 | "@types/nprogress": "^0.2.3",
41 | "@typescript-eslint/eslint-plugin": "^8.38.0",
42 | "@typescript-eslint/parser": "^8.38.0",
43 | "@withtypes/mime": "^0.1.2",
44 | "@withtypes/minimist": "^0.1.1",
45 | "conventional-changelog-cli": "^5.0.0",
46 | "eslint": "^9.32.0",
47 | "eslint-config-prettier": "^10.1.8",
48 | "eslint-plugin-import-x": "^4.16.1",
49 | "eslint-plugin-n": "^17.21.1",
50 | "eslint-plugin-prettier": "^5.5.3",
51 | "eslint-plugin-react": "^7.37.5",
52 | "eslint-plugin-react-hooks": "^5.2.0",
53 | "eslint-plugin-react-refresh": "^0.4.20",
54 | "eslint-plugin-regexp": "^2.9.0",
55 | "eslint-plugin-tailwindcss": "^3.18.2",
56 | "eslint-plugin-unicorn": "^57.0.0",
57 | "eslint-plugin-vue": "^9.33.0",
58 | "fs-extra": "^11.3.0",
59 | "globals": "^16.3.0",
60 | "husky": "^9.1.7",
61 | "jiti": "^2.5.1",
62 | "lint-staged": "^15.5.2",
63 | "prettier": "^3.6.2",
64 | "prettier-plugin-jsdoc": "^1.3.3",
65 | "prettier-plugin-lint-md": "^1.0.1",
66 | "tsup": "^8.5.0",
67 | "turbo": "^2.5.5",
68 | "typescript": "^5.8.3",
69 | "vitest": "^3.2.4",
70 | "vue-eslint-parser": "^9.4.3"
71 | },
72 | "lint-staged": {
73 | "*.{js,jsx,md,ts,tsx}": [
74 | "prettier --write",
75 | "eslint --fix"
76 | ]
77 | }
78 | }
--------------------------------------------------------------------------------
/packages/utils/src/regexp.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Verify the mobile phone number format in mainland China
3 | *
4 | * @category Regexp
5 | */
6 | export function isMob(phoneNumber: number | string) {
7 | return /^1[3456789]\d{9}$/.test(String(phoneNumber))
8 | }
9 |
10 | /**
11 | * Verify email format
12 | *
13 | * @category Regexp
14 | */
15 | export function isEmail(email: string) {
16 | return /^[A-Za-z\d]+([-_.][A-Za-z\d]+)*@([A-Za-z\d]+[-.])+[A-Za-z\d]{2,4}$/.test(
17 | email,
18 | )
19 | }
20 |
21 | /**
22 | * Verify url format
23 | *
24 | * @category Regexp
25 | */
26 | export function isUrl(url: string) {
27 | return /https?:\/\/[\w-]+(\.[\w-]+){1,2}(\/[\w-]{3,6}){0,2}(\?[\w_]{4,6}=[\w_]{4,6}(&[\w_]{4,6}=[\w_]{4,6}){0,2})?/.test(
28 | url,
29 | )
30 | }
31 |
32 | /**
33 | * Verify the ID card number format in mainland China
34 | *
35 | * @category Regexp
36 | */
37 | export function isIdCard(idCardNumber: string) {
38 | // 18-digit ID number
39 | const digit18 =
40 | /^([1-6][1-9]|50)\d{4}(18|19|20)\d{2}((0[1-9])|10|11|12)(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/
41 |
42 | // 15-digit ID number
43 | const digit15 =
44 | /^([1-6][1-9]|50)\d{4}\d{2}((0[1-9])|10|11|12)(([0-2][1-9])|10|20|30|31)\d{3}$/
45 |
46 | return digit18.test(idCardNumber) || digit15.test(idCardNumber)
47 | }
48 |
49 | /**
50 | * Verify the bank card number format in mainland China
51 | *
52 | * @category Regexp
53 | */
54 | export function isBankCard(bankCardNumber: string) {
55 | return /^([1-9]{1})(\d{15}|\d{16}|\d{18})$/.test(bankCardNumber)
56 | }
57 |
58 | /**
59 | * Verify the IP is IPv4
60 | *
61 | * @category Regexp
62 | */
63 | export function isIPv4(ip: string) {
64 | return /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/.test(
65 | ip,
66 | )
67 | }
68 |
69 | /**
70 | * Verify the IP is IPv6
71 | *
72 | * @category Regexp
73 | */
74 | export function isIPv6(ip: string) {
75 | return /^([\da-fA-F]{1,4}:){6}((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$|^::([\da-fA-F]{1,4}:){0,4}((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$|^([\da-fA-F]{1,4}:):([\da-fA-F]{1,4}:){0,3}((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$|^([\da-fA-F]{1,4}:){2}:([\da-fA-F]{1,4}:){0,2}((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$|^([\da-fA-F]{1,4}:){3}:([\da-fA-F]{1,4}:){0,1}((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$|^([\da-fA-F]{1,4}:){4}:((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$|^([\da-fA-F]{1,4}:){7}[\da-fA-F]{1,4}$|^:((:[\da-fA-F]{1,4}){1,6}|:)$|^[\da-fA-F]{1,4}:((:[\da-fA-F]{1,4}){1,5}|:)$|^([\da-fA-F]{1,4}:){2}((:[\da-fA-F]{1,4}){1,4}|:)$|^([\da-fA-F]{1,4}:){3}((:[\da-fA-F]{1,4}){1,3}|:)$|^([\da-fA-F]{1,4}:){4}((:[\da-fA-F]{1,4}){1,2}|:)$|^([\da-fA-F]{1,4}:){5}:([\da-fA-F]{1,4})?$|^([\da-fA-F]{1,4}:){6}:$/.test(
76 | ip,
77 | )
78 | }
79 |
--------------------------------------------------------------------------------
/packages/progress/README.md:
--------------------------------------------------------------------------------
1 | # @bassist/progress
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Simple slim progress bars base on [NProgress](https://www.npmjs.com/package/nprogress).
19 |
20 | ## Usage
21 |
22 | With npm(or yarn, or pnpm):
23 |
24 | ```bash
25 | npm install @bassist/progress
26 | ```
27 |
28 | In `.js` / `.ts` or other files:
29 |
30 | ```ts
31 | import progress from '@bassist/progress'
32 |
33 | // Used in the framework's router hooks.
34 | // Or in other scenarios (e.g. AJAX requests).
35 |
36 | router.beforeEach(() => {
37 | progress.start()
38 | })
39 |
40 | router.afterEach(() => {
41 | progress.done()
42 | })
43 | ```
44 |
45 | All configurations and options of [NProgress](https://www.npmjs.com/package/nprogress) are supported.
46 |
47 | ## Set Color
48 |
49 | This plugin extends NProgress's API and adds a `setColor` method.
50 |
51 | - Type Declarations:
52 |
53 | ```ts
54 | export interface Progress extends NProgress {
55 | /**
56 | *
57 | * @param color - A valid CSS color value or CSS variable
58 | *
59 | * @example use HEX
60 | * progress.setColor('#ff0000')
61 | *
62 | * @example use RGB
63 | * progress.setColor('rgb(255, 0, 0)')
64 | *
65 | * @example use RGBA
66 | * progress.setColor('rgba(255, 0, 0, 1)')
67 | *
68 | * @example use CSS Variable
69 | * progress.setColor('var(--color-primary)')
70 | */
71 | setColor: (color: string) => void
72 | }
73 | ```
74 |
75 | - Example:
76 |
77 | ```ts
78 | import progress from '@bassist/progress'
79 |
80 | // Set the color before start
81 | progress.setColor('#ff0000')
82 |
83 | // Then, used in the framework's router hooks.
84 | // Or in other scenarios (e.g. AJAX requests).
85 |
86 | router.beforeEach(() => {
87 | progress.start()
88 | })
89 |
90 | router.afterEach(() => {
91 | progress.done()
92 | })
93 | ```
94 |
95 | ## Release Notes
96 |
97 | Please refer to [CHANGELOG](https://github.com/chengpeiquan/bassist/blob/main/packages/progress/CHANGELOG.md) for details.
98 |
99 | ## License
100 |
101 | MIT License © 2022-PRESENT [chengpeiquan](https://github.com/chengpeiquan)
102 |
--------------------------------------------------------------------------------
/packages/utils/src/storage/base.ts:
--------------------------------------------------------------------------------
1 | import { isString } from '../data'
2 | import { isBrowser } from '../device'
3 | import { FallbackStorage } from './fallback'
4 |
5 | /**
6 | * @category Storage
7 | */
8 | export type StorageType = 'localStorage' | 'sessionStorage'
9 |
10 | /**
11 | * BaseStorage class provides a wrapper for browser storage or fallback storage.
12 | *
13 | * @category Storage
14 | */
15 | export class BaseStorage {
16 | prefix: string
17 | private storage: Storage | FallbackStorage
18 |
19 | /**
20 | * Creates an instance of BaseStorage
21 | *
22 | * @param prefix - The prefix to be added to the storage keys.
23 | * @param storageType - The type of storage to use (localStorage or
24 | * sessionStorage)
25 | */
26 | constructor(prefix: string, storageType: StorageType) {
27 | this.prefix = prefix
28 | this.storage = isBrowser ? window[storageType] : new FallbackStorage(prefix)
29 | }
30 |
31 | /**
32 | * Read stored data
33 | *
34 | * @returns The data in the format before storage
35 | * @tips The `key` doesn't need to be prefixed
36 | */
37 | get(key: string) {
38 | const localData = this.storage.getItem(`${this.prefix}-${key}`)
39 | if (!localData) return localData
40 |
41 | try {
42 | if (localData === 'true') return true
43 | if (localData === 'false') return false
44 | if (localData === 'null') return null
45 | if (localData === 'undefined') return undefined
46 | return JSON.parse(localData)
47 | } catch {
48 | return localData
49 | }
50 | }
51 |
52 | /**
53 | * Set storage data
54 | */
55 | set(key: string, value: any) {
56 | try {
57 | const data = isString(value) ? value : JSON.stringify(value)
58 | this.storage.setItem(`${this.prefix}-${key}`, data)
59 | } catch (e) {
60 | console.error(e)
61 | }
62 | }
63 |
64 | /**
65 | * Remove the specified storage data under the current prefix
66 | */
67 | remove(key: string) {
68 | this.storage.removeItem(`${this.prefix}-${key}`)
69 | }
70 |
71 | /**
72 | * Clear all stored data under the current prefix
73 | */
74 | clear() {
75 | const keys = this.list()
76 | keys.forEach((key) => {
77 | this.remove(key)
78 | })
79 | }
80 |
81 | /**
82 | * Count the number of storage related to the current prefix
83 | */
84 | count() {
85 | return this.list().length
86 | }
87 |
88 | /**
89 | * List storage keys associated under the current prefix
90 | *
91 | * @tips All keys without prefix
92 | */
93 | list() {
94 | if (!this.prefix) return []
95 | const result: string[] = []
96 | const count = this.storage.length
97 | for (let i = 0; i < count; i++) {
98 | const key = this.storage.key(i)
99 | if (key?.startsWith(this.prefix)) {
100 | result.push(key.replace(`${this.prefix}-`, ''))
101 | }
102 | }
103 | return result
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/packages/utils/src/query.ts:
--------------------------------------------------------------------------------
1 | import { isObject, isString } from './data'
2 | import { isBrowser } from './device'
3 |
4 | /**
5 | * @category Query
6 | */
7 | type QueryInfo = Record
8 |
9 | /**
10 | * @category Query
11 | */
12 | type QueryInfoObject = Record<
13 | string,
14 | string | number | boolean | undefined | null
15 | >
16 |
17 | /**
18 | * Parse URL Query parameters
19 | *
20 | * @category Query
21 | * @param url - By default, it is extracted from the browser URL, and this
22 | * parameter can be parsed from the specified URL
23 | * @returns Query parameter object, will convert `key1=value1&key2=value2` into
24 | * an object
25 | */
26 | export function parseQuery(url?: string) {
27 | let queryStringify = ''
28 |
29 | if (isBrowser) {
30 | const { search } = window.location
31 | queryStringify = search
32 | }
33 |
34 | if (isString(url) && url.startsWith('http')) {
35 | const index = url.indexOf('?')
36 | queryStringify = index === -1 ? '' : url.slice(index)
37 | }
38 |
39 | if (queryStringify.includes('#')) {
40 | const index = queryStringify.indexOf('#')
41 | queryStringify = queryStringify.slice(0, index)
42 | }
43 |
44 | if (!queryStringify.length) return {}
45 |
46 | const temp: QueryInfo = {}
47 | queryStringify
48 | .slice(1)
49 | .split('&')
50 | .forEach((str) => {
51 | const [k, v] = str.split('=')
52 | temp[k] = decodeURIComponent(v)
53 | })
54 | return temp
55 | }
56 |
57 | /**
58 | * Extract parameter information from URL Query
59 | *
60 | * @category Query
61 | * @returns An object containing the request path and parameters object `path`:
62 | * Jump path, the same as the routing name in the Web App `params`: Parameters
63 | * other than path
64 | */
65 | export function extractQueryInfo(url?: string): {
66 | path: string
67 | params: QueryInfo
68 | } {
69 | const query = parseQuery(url)
70 | const params: QueryInfo = {}
71 | Object.keys(query).forEach((k) => {
72 | if (k === 'path') return
73 | params[k] = query[k]
74 | })
75 |
76 | const path = query.path || ''
77 | return { path, params }
78 | }
79 |
80 | /**
81 | * Get the specified Query parameter
82 | *
83 | * @category Query
84 | * @param key - The parameter key name to get
85 | * @param url - By default, it is extracted from the browser URL, and this
86 | * parameter can be parsed from the specified URL
87 | */
88 | export function getQuery(key: string, url?: string) {
89 | const query = parseQuery(url)
90 | return query[key] || ''
91 | }
92 |
93 | /**
94 | * Serialize Query parameters information
95 | *
96 | * @category Query
97 | * @param queryInfoObject - The object of the Query parameter to use for
98 | * serialization
99 | */
100 | export function stringifyQuery(queryInfoObject: QueryInfoObject) {
101 | if (!isObject(queryInfoObject)) return ''
102 | return Object.keys(queryInfoObject)
103 | .map((key) => `${key}=${encodeURIComponent(String(queryInfoObject[key]))}`)
104 | .join('&')
105 | }
106 |
--------------------------------------------------------------------------------
/packages/eslint-config/src/define.ts:
--------------------------------------------------------------------------------
1 | import { createPrettierConfig } from './private-configs/prettier'
2 | import {
3 | createTailwindcssConfig,
4 | type TailwindcssSettings,
5 | } from './private-configs/tailwindcss'
6 | import {
7 | type PartialPrettierExtendedOptions,
8 | type FlatESLintConfig,
9 | } from './types'
10 |
11 | export interface DefineFlatConfigOptions {
12 | /**
13 | * Specifies the working directory for loading the `.prettierrc`
14 | * configuration.
15 | *
16 | * The config file should be in JSON format.
17 | *
18 | * @default process.cwd()
19 | */
20 | cwd?: string
21 |
22 | /**
23 | * If `prettierEnabled` is set to `false`, all Prettier-related rules and
24 | * configurations will be ignored, even if `prettierRules` are provided.
25 | *
26 | * @default true
27 | */
28 | prettierEnabled?: boolean
29 |
30 | /**
31 | * By default, this will read `.prettierrc` from the current working
32 | * directory, and the `.prettierrc` file must be written in JSON format.
33 | *
34 | * If you are not using a config file with JSON content, or a different config
35 | * file name, you can convert it to JSON rules and pass it in.
36 | *
37 | * After reading the custom configuration, it will be merged with the default
38 | * ESLint rules.
39 | *
40 | * @default Loads from .prettierrc file, falls back to default config
41 | *
42 | * @see https://prettier.io/docs/configuration.html
43 | */
44 | prettierRules?: PartialPrettierExtendedOptions
45 |
46 | /**
47 | * Tailwindcss rules are enabled by default. If they interfere with your
48 | * project, you can disable them with this option.
49 | *
50 | * @default true
51 | */
52 | tailwindcssEnabled?: boolean
53 |
54 | /**
55 | * If you need to override the configuration, you can pass the corresponding
56 | * options.
57 | *
58 | * If you want to merge configurations, you can import
59 | * `defaultTailwindcssSettings`, merge them yourself, and then pass the result
60 | * in.
61 | *
62 | * If an empty object `{}` is passed, the default settings will be used.
63 | *
64 | * @see https://github.com/francoismassart/eslint-plugin-tailwindcss/tree/v3.18.2
65 | */
66 | tailwindcssSettings?: TailwindcssSettings
67 | }
68 |
69 | /**
70 | * Define the ESLint configuration with optional Prettier integration.
71 | *
72 | * @param configs The base ESLint configurations.
73 | * @param options - Options for defining the configuration.
74 | * @returns The final ESLint configuration array.
75 | */
76 | export const defineFlatConfig = (
77 | configs: FlatESLintConfig[],
78 | options: DefineFlatConfigOptions = {},
79 | ) => {
80 | const {
81 | cwd = process.cwd(),
82 | prettierEnabled = true,
83 | prettierRules,
84 | tailwindcssEnabled = true,
85 | tailwindcssSettings,
86 | } = options
87 |
88 | const prettierConfig = prettierEnabled
89 | ? createPrettierConfig(cwd, prettierRules)
90 | : []
91 |
92 | const tailwindcssConfig = tailwindcssEnabled
93 | ? createTailwindcssConfig(tailwindcssSettings)
94 | : []
95 |
96 | return [...prettierConfig, ...tailwindcssConfig, ...configs]
97 | }
98 |
--------------------------------------------------------------------------------
/packages/eslint-config/src/configs/javascript.ts:
--------------------------------------------------------------------------------
1 | import jsConfig from '@eslint/js'
2 | import globals from 'globals'
3 | import { getConfigName } from '../shared/utils'
4 | import { type FlatESLintConfig } from '../types'
5 |
6 | export const javascript: FlatESLintConfig[] = [
7 | {
8 | ...jsConfig.configs.recommended,
9 | name: getConfigName('js', 'eslint-recommended'),
10 | },
11 | {
12 | name: getConfigName('js', 'base'),
13 | languageOptions: {
14 | globals: {
15 | ...globals.browser,
16 | ...globals.es2021,
17 | ...globals.node,
18 | },
19 | sourceType: 'module',
20 | },
21 | rules: {
22 | 'no-unused-vars': ['error', { args: 'none', ignoreRestSiblings: true }],
23 | 'no-constant-condition': 'warn',
24 | 'no-debugger': 'warn',
25 | 'no-console': ['warn', { allow: ['warn', 'error'] }],
26 | 'no-restricted-syntax': [
27 | 'error',
28 | 'ForInStatement',
29 | 'LabeledStatement',
30 | 'WithStatement',
31 | ],
32 | 'no-return-await': 'warn',
33 | 'no-empty': ['error', { allowEmptyCatch: true }],
34 | 'sort-imports': [
35 | 'error',
36 | {
37 | ignoreCase: false,
38 | ignoreDeclarationSort: true,
39 | ignoreMemberSort: false,
40 | memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'],
41 | allowSeparatedGroups: false,
42 | },
43 | ],
44 | 'dot-notation': 'warn',
45 |
46 | 'no-var': 'error',
47 | 'prefer-const': [
48 | 'warn',
49 | { destructuring: 'all', ignoreReadBeforeAssign: true },
50 | ],
51 | 'prefer-arrow-callback': [
52 | 'error',
53 | { allowNamedFunctions: false, allowUnboundThis: true },
54 | ],
55 | 'object-shorthand': [
56 | 'error',
57 | 'always',
58 | { ignoreConstructors: false, avoidQuotes: true },
59 | ],
60 | 'prefer-rest-params': 'error',
61 | 'prefer-spread': 'error',
62 | 'prefer-template': 'error',
63 | 'require-await': 'error',
64 |
65 | 'array-callback-return': 'error',
66 | 'block-scoped-var': 'error',
67 | eqeqeq: ['error', 'smart'],
68 | 'no-alert': 'warn',
69 | 'no-case-declarations': 'error',
70 | 'no-fallthrough': ['warn', { commentPattern: 'break[\\s\\w]*omitted' }],
71 | 'no-multi-str': 'error',
72 | 'no-with': 'error',
73 | 'no-void': 'error',
74 | 'no-duplicate-imports': 'error',
75 |
76 | 'no-unused-expressions': [
77 | 'error',
78 | {
79 | allowShortCircuit: true,
80 | allowTernary: true,
81 | allowTaggedTemplates: true,
82 | },
83 | ],
84 | 'no-lonely-if': 'error',
85 | 'prefer-exponentiation-operator': 'error',
86 | },
87 | },
88 | {
89 | name: getConfigName('js', 'scripts'),
90 | files: ['**/scripts/*', '**/cli.*'],
91 | rules: {
92 | 'no-console': 'off',
93 | },
94 | },
95 | {
96 | name: getConfigName('js', 'test'),
97 | files: ['**/*.{test,spec}.js?(x)'],
98 | rules: {
99 | 'no-unused-expressions': 'off',
100 | },
101 | },
102 | ]
103 |
--------------------------------------------------------------------------------
/packages/eslint-config/src/configs/unicorn.ts:
--------------------------------------------------------------------------------
1 | import unicornPlugin from 'eslint-plugin-unicorn'
2 | import { getConfigName } from '../shared/utils'
3 | import { type FlatESLintConfig } from '../types'
4 |
5 | export { unicornPlugin }
6 |
7 | export const unicorn: FlatESLintConfig[] = [
8 | {
9 | name: getConfigName('unicorn'),
10 | plugins: {
11 | unicorn: unicornPlugin,
12 | },
13 | rules: {
14 | 'unicorn/better-regex': 'error',
15 | 'unicorn/catch-error-name': 'error',
16 | 'unicorn/custom-error-definition': 'error',
17 | 'unicorn/error-message': 'error',
18 | 'unicorn/escape-case': 'error',
19 | 'unicorn/explicit-length-check': 'error',
20 | 'unicorn/filename-case': [
21 | 'error',
22 | {
23 | cases: { kebabCase: true, pascalCase: true },
24 | ignore: [/^[A-Z]+\..*$/],
25 | },
26 | ],
27 | 'unicorn/new-for-builtins': 'error',
28 | 'unicorn/no-array-callback-reference': 'error',
29 | 'unicorn/no-array-method-this-argument': 'error',
30 | 'unicorn/no-array-push-push': 'error',
31 | 'unicorn/no-console-spaces': 'error',
32 | 'unicorn/no-for-loop': 'error',
33 | 'unicorn/no-hex-escape': 'error',
34 | 'unicorn/no-instanceof-array': 'error',
35 | 'unicorn/no-invalid-remove-event-listener': 'error',
36 | 'unicorn/no-lonely-if': 'error',
37 | 'unicorn/no-new-array': 'error',
38 | 'unicorn/no-new-buffer': 'error',
39 | 'unicorn/no-static-only-class': 'error',
40 | 'unicorn/no-unnecessary-await': 'error',
41 | 'unicorn/no-zero-fractions': `error`,
42 | 'unicorn/prefer-add-event-listener': 'error',
43 | 'unicorn/prefer-array-find': 'error',
44 | 'unicorn/prefer-array-flat-map': 'error',
45 | 'unicorn/prefer-array-index-of': 'error',
46 | 'unicorn/prefer-array-some': 'error',
47 | 'unicorn/prefer-at': 'error',
48 | 'unicorn/prefer-blob-reading-methods': 'error',
49 | 'unicorn/prefer-date-now': 'error',
50 | 'unicorn/prefer-dom-node-append': 'error',
51 | 'unicorn/prefer-dom-node-dataset': 'error',
52 | 'unicorn/prefer-dom-node-remove': 'error',
53 | 'unicorn/prefer-dom-node-text-content': 'error',
54 | 'unicorn/prefer-includes': 'error',
55 | 'unicorn/prefer-keyboard-event-key': 'error',
56 | 'unicorn/prefer-math-trunc': 'error',
57 | 'unicorn/prefer-modern-dom-apis': 'error',
58 | 'unicorn/prefer-modern-math-apis': 'error',
59 | 'unicorn/prefer-negative-index': 'error',
60 | 'unicorn/prefer-node-protocol': 'error',
61 | 'unicorn/prefer-number-properties': 'error',
62 | 'unicorn/prefer-optional-catch-binding': 'error',
63 | 'unicorn/prefer-prototype-methods': 'error',
64 | 'unicorn/prefer-query-selector': 'error',
65 | 'unicorn/prefer-reflect-apply': 'error',
66 | 'unicorn/prefer-regexp-test': 'error',
67 | 'unicorn/prefer-string-replace-all': 'error',
68 | 'unicorn/prefer-string-slice': 'error',
69 | 'unicorn/prefer-string-starts-ends-with': 'error',
70 | 'unicorn/prefer-string-trim-start-end': 'error',
71 | 'unicorn/prefer-top-level-await': 'error',
72 | 'unicorn/prefer-type-error': 'error',
73 | 'unicorn/throw-new-error': 'error',
74 | },
75 | },
76 | ]
77 |
--------------------------------------------------------------------------------
/packages/build-config/README.md:
--------------------------------------------------------------------------------
1 | # @bassist/build-config
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Opinionated collection of common build tool configurations, carefully crafted by [@chengpeiquan](https://github.com/chengpeiquan).
19 |
20 | Currently provides configurations for the following build tools:
21 |
22 | - [tsup](https://github.com/egoist/tsup): The most convenient tool for building TypeScript libraries based on ESBuild
23 |
24 | ## 🤔 Why do we need this?
25 |
26 | While using certain tool configurations in a single repository is convenient enough, the process becomes tedious when many repositories use similar configurations.
27 |
28 | By abstracting and sharing commonly used tool configurations, different projects can complete their setup faster.
29 |
30 | ## ⚡ Usage: Based on tsup
31 |
32 | - 🎯 **Preset Configurations**: Provides out-of-the-box tsup base configurations
33 | - 📦 **Multi-format Support**: Default support for CommonJS and ESM formats
34 | - 🏷️ **Auto Banner**: Automatically generates file header comments based on package.json
35 | - 🧹 **Auto Clean**: Automatically cleans output directory before building
36 | - 📝 **Type Declarations**: Automatically generates TypeScript type declaration files
37 | - ⚡ **Code Minification**: Built-in code compression functionality
38 |
39 | ### Installation
40 |
41 | Install the package using your preferred package manager:
42 |
43 | ```bash
44 | npm install -D @bassist/build-config tsup
45 | ```
46 |
47 | **Note**: This submodule requires tsup as a peer dependency. As shown in the installation command above, please ensure tsup is installed.
48 |
49 | ### Usage
50 |
51 | All APIs are provided uniformly by `@bassist/build-config/tsup`.
52 |
53 | Typically, you only need to use the configuration returned by `createBaseConfig`, and you can pass custom options as needed:
54 |
55 | ```ts
56 | import { createBaseConfig } from '@bassist/build-config/tsup'
57 | import { defineConfig } from 'tsup'
58 | import pkg from './package.json'
59 |
60 | const config = createBaseConfig({ pkg })
61 |
62 | export default defineConfig(config)
63 | ```
64 |
65 | If the configuration options don't meet your needs, you can destructure and merge objects to pass them to the `defineConfig` API.
66 |
67 | For more APIs and configuration options, please check the source code [tsup.ts](https://github.com/chengpeiquan/bassist/blob/main/packages/build-config/src/tsup.ts).
68 |
69 | ## 📝 Release Notes
70 |
71 | Please refer to [CHANGELOG](https://github.com/chengpeiquan/bassist/blob/main/packages/build-config/CHANGELOG.md) for details.
72 |
73 | ## 📜 License
74 |
75 | MIT License © 2025-PRESENT [chengpeiquan](https://github.com/chengpeiquan)
76 |
--------------------------------------------------------------------------------
/packages/node-utils/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # [0.5.0](https://github.com/chengpeiquan/bassist/compare/node-utils@0.4.2...node-utils@0.5.0) (2025-07-06)
2 |
3 |
4 | ### Refactor
5 |
6 | * **node-utils:** migrate bundle.ts to @bassist/build-config package ([7e2f1c0](https://github.com/chengpeiquan/bassist/commit/7e2f1c09f6a0b2762063c8c3b328c094e0803a39))
7 |
8 |
9 |
10 | ## [0.4.2](https://github.com/chengpeiquan/bassist/compare/node-utils@0.4.1...node-utils@0.4.2) (2025-05-22)
11 |
12 |
13 | ### Bug Fixes
14 |
15 | * **node-utils:** reexport fse apis ([f2242c1](https://github.com/chengpeiquan/bassist/commit/f2242c18b179b41bae408692e53386618b8974f7))
16 |
17 |
18 |
19 | ## [0.4.1](https://github.com/chengpeiquan/bassist/compare/node-utils@0.4.0...node-utils@0.4.1) (2024-06-30)
20 |
21 |
22 | ### Bug Fixes
23 |
24 | * **node-utils:** missing types ([3de219a](https://github.com/chengpeiquan/bassist/commit/3de219ac8a7b95a7d6aed8342eb4e75726ba7717))
25 |
26 |
27 |
28 | # [0.4.0](https://github.com/chengpeiquan/bassist/compare/node-utils@0.3.0...node-utils@0.4.0) (2024-06-30)
29 |
30 |
31 | ### Bug Fixes
32 |
33 | * typescript overwrite warning ([ced9c2c](https://github.com/chengpeiquan/bassist/commit/ced9c2c8a162f63b8b0ae66f65c384177eb7b0e3))
34 |
35 |
36 | ### Features
37 |
38 | * **node-utils:** add bundle helpers ([62e3ee7](https://github.com/chengpeiquan/bassist/commit/62e3ee756743cc4b373f1dad021e1ea5e096b8a2))
39 | * **node-utils:** built-in fs-extra ([664731f](https://github.com/chengpeiquan/bassist/commit/664731f868f4aa2e1a6177e53196f6c0adc46ecc))
40 | * **node-utils:** use fs-extra instead of the original fs method encapsulation ([10d5cd7](https://github.com/chengpeiquan/bassist/commit/10d5cd747a9f75f2e49d39bd10efc36dfaf885fb))
41 |
42 |
43 |
44 | # [0.3.0](https://github.com/chengpeiquan/bassist/compare/node-utils@0.2.1...node-utils@0.3.0) (2023-08-19)
45 |
46 |
47 | ### Features
48 |
49 | * **node-utils:** change build scheme (internal mechanism) ([988ebda](https://github.com/chengpeiquan/bassist/commit/988ebda007077efcd8eaa3a310ad6a946e8ab54f))
50 |
51 |
52 |
53 | ## [0.2.1](https://github.com/chengpeiquan/bassist/compare/node-utils@0.2.0...node-utils@0.2.1) (2023-02-12)
54 |
55 |
56 | ### Bug Fixes
57 |
58 | * **node-utils:** deprecate getDirnameInEsModule ([1c3ae39](https://github.com/chengpeiquan/bassist/commit/1c3ae39fba0d7218a53868bcf156b6b71b2f3d14))
59 |
60 |
61 |
62 | # [0.2.0](https://github.com/chengpeiquan/bassist/compare/node-utils@0.1.2...node-utils@0.2.0) (2023-02-03)
63 |
64 |
65 | ### Features
66 |
67 | * **node-utils:** add a function to get __dirname in esm ([5abcf61](https://github.com/chengpeiquan/bassist/commit/5abcf611f4c73942b7a7b140d41dd0062b0ed848))
68 |
69 |
70 |
71 | ## [0.1.2](https://github.com/chengpeiquan/bassist/compare/node-utils@0.1.1...node-utils@0.1.2) (2023-01-30)
72 |
73 |
74 | ### Bug Fixes
75 |
76 | * **node-utils:** hide catch log ([f3c52d2](https://github.com/chengpeiquan/bassist/commit/f3c52d2c8c762403254225856da6028dec126598))
77 |
78 |
79 |
80 | ## [0.1.1](https://github.com/chengpeiquan/bassist/compare/node-utils@0.1.0...node-utils@0.1.1) (2023-01-30)
81 |
82 |
83 | ### Bug Fixes
84 |
85 | * **node-utils:** cannot delete non-empty directory ([276a8d1](https://github.com/chengpeiquan/bassist/commit/276a8d15c6c0189f41f0108f53e06ecb97e07ea8))
86 |
87 |
88 |
89 | ## 0.1.0 (2023-01-27)
90 |
91 |
92 | ### Features
93 |
94 | * **node-utils:** add some functional encapsulation of fs ([9137a79](https://github.com/chengpeiquan/bassist/commit/9137a7919e1a251a9a6e7bfc4c838bc86c708ebf))
95 | * **node-utils:** add some functional encapsulation of pkg ([59beebe](https://github.com/chengpeiquan/bassist/commit/59beebee09f616095c670c3f09a2d6321af090f0))
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/packages/build-config/src/tsup.ts:
--------------------------------------------------------------------------------
1 | import { type defineConfig, type Options } from 'tsup'
2 |
3 | /**
4 | * Bundler format based on tsup
5 | *
6 | * @category Tsup
7 | * @see https://tsup.egoist.dev/#bundle-formats
8 | */
9 | export enum BundleFormat {
10 | CJS = 'cjs',
11 | ESM = 'esm',
12 | IIFE = 'iife',
13 | }
14 |
15 | /**
16 | * Provides the extension of the JavaScript file according to the bundle format
17 | *
18 | * @category Tsup
19 | * @see https://tsup.egoist.dev/#output-extension
20 | */
21 | export const getBundleExtension = ({ format }: Record) => {
22 | switch (format) {
23 | case BundleFormat.CJS: {
24 | return { js: `.cjs` }
25 | }
26 | case BundleFormat.ESM: {
27 | return { js: `.mjs` }
28 | }
29 | default: {
30 | return { js: `.js` }
31 | }
32 | }
33 | }
34 |
35 | /**
36 | * @category Tsup
37 | */
38 | export const bundleBannerFields = [
39 | 'name',
40 | 'version',
41 | 'description',
42 | 'author',
43 | 'homepage',
44 | 'license',
45 | ] as const
46 |
47 | /**
48 | * @category Tsup
49 | */
50 | export interface GetBundleBannerOptions {
51 | /**
52 | * Generates a shebang that lets the script be executed using the Node.js
53 | * interpreter
54 | */
55 | bin?: boolean
56 |
57 | /**
58 | * Fields that need to be generated to Banner
59 | *
60 | * @default bundleBannerFields
61 | * @note Please make sure the value is a string
62 | */
63 | fields?: string[]
64 | }
65 |
66 | /**
67 | * Generate Banner content based on package.json
68 | *
69 | * @category Tsup
70 | * @param pkg - Contents of package.json
71 | * @param options - Options for adjusting output results
72 | * @returns Banner content for generated chunks
73 | * @see https://www.npmjs.com/package/vite-plugin-banner
74 | */
75 | export const getBundleBanner = (
76 | pkg: Record,
77 | { bin, fields: _fields }: GetBundleBannerOptions = {},
78 | ) => {
79 | if (Object.prototype.toString.call(pkg) !== '[object Object]') return ''
80 |
81 | const fields = Array.isArray(_fields)
82 | ? _fields
83 | : (bundleBannerFields as unknown as string[])
84 |
85 | const baseBanners: string[] = []
86 | baseBanners.push(`/**`)
87 |
88 | fields.forEach((k) => {
89 | const v = pkg[k]
90 | if (typeof v !== 'string') return
91 | const prefix = k === 'version' ? 'v' : ''
92 | baseBanners.push(` * ${k}: ${prefix}${v}`)
93 | })
94 |
95 | baseBanners.push(` */`)
96 |
97 | const banners = bin
98 | ? ['#!/usr/bin/env node', '', ...baseBanners]
99 | : baseBanners
100 |
101 | return banners.join('\n')
102 | }
103 |
104 | /**
105 | * @category Tsup
106 | */
107 | export type Config = ReturnType
108 |
109 | /**
110 | * @category Tsup
111 | */
112 | export interface CreateBaseConfigOptions
113 | extends Partial> {
114 | pkg: Record
115 | }
116 |
117 | /**
118 | * Create base tsup config
119 | *
120 | * @category Tsup
121 | * @see https://tsup.egoist.dev/#bundle-formats
122 | */
123 | export const createBaseConfig = (options: CreateBaseConfigOptions) => {
124 | const {
125 | pkg,
126 | entry = { index: 'src/index.ts' },
127 | globalName,
128 | outDir = 'dist',
129 | format = [BundleFormat.CJS, BundleFormat.ESM],
130 | } = options || {}
131 |
132 | return {
133 | entry,
134 | target: ['es2020'],
135 | format,
136 | globalName,
137 | outExtension: (ctx) => getBundleExtension(ctx),
138 | outDir,
139 | dts: true,
140 | banner: {
141 | js: getBundleBanner(pkg, { bin: !!pkg.bin }),
142 | },
143 | bundle: true,
144 | minify: true,
145 | clean: true,
146 | } as const satisfies Config
147 | }
148 |
--------------------------------------------------------------------------------
/packages/release/README.md:
--------------------------------------------------------------------------------
1 | # @bassist/release
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Simple GitHub release generator by [@chengpeiquan](https://github.com/chengpeiquan) , based on [GitHub CLI](https://cli.github.com/).
19 |
20 | If you're tired of having to release every time on GitHub web, you can use this package to make it easier and just run a single command.
21 |
22 | ## Prerequisite
23 |
24 | For the security of your account and to avoid Token leakage, you must first install GitHub CLI and complete the login on it.
25 |
26 | See: [GitHub CLI](https://cli.github.com/)
27 |
28 | And make sure you have Release permissions on the project's GitHub repository.
29 |
30 | There is another requirement, please configure the repository information of `package.json` according to the specifications of npm docs.
31 |
32 | See: [repository](https://docs.npmjs.com/cli/v10/configuring-npm/package-json#repository)
33 |
34 | e.g.
35 |
36 | For single-package repo:
37 |
38 | ```json
39 | {
40 | "repository": {
41 | "type": "git",
42 | "url": "https://github.com/chengpeiquan/bassist"
43 | }
44 | }
45 | ```
46 |
47 | For monorepo, you can specify the `directory` in which it lives:
48 |
49 | ```json
50 | {
51 | "repository": {
52 | "type": "git",
53 | "url": "https://github.com/chengpeiquan/bassist",
54 | "directory": "packages/utils"
55 | }
56 | }
57 | ```
58 |
59 | Currently supported URL formats are:
60 |
61 | - `https://github.com/chengpeiquan/bassist`
62 | - `https://github.com/chengpeiquan/bassist.git`
63 | - `github:chengpeiquan/bassist`
64 |
65 | ## Usage
66 |
67 | This is a CLI tool, you can install it locally and run it through commands such as pnpm exec.
68 |
69 | Install it:
70 |
71 | ```bash
72 | pnpm add -D @bassist/release
73 | ```
74 |
75 | In your `package.json` :
76 |
77 | ```json
78 | {
79 | "scripts": {
80 | "gen:release": "pnpm exec release"
81 | }
82 | }
83 | ```
84 |
85 | Run on command line:
86 |
87 | ```bash
88 | pnpm gen:release
89 | ```
90 |
91 | You can view the latest release information on the releases page of your GitHub repository.
92 |
93 | ## Options
94 |
95 | For most projects, the default settings are sufficient. If adjustments are sometimes needed, some options are provided to pass on.
96 |
97 | On the command line, options can be passed to the program, e.g. `--preset angular` by option, or `-p angular` by short flag.
98 |
99 | | Option | Short Flag | Default Value | Description |
100 | | :-------: | :--------: | :------------: | :--------------------------------------------- |
101 | | branch | b | `main` | The branch where the CHANGELOG file is located |
102 | | changelog | c | `CHANGELOG.md` | The file name of the change log |
103 |
104 | Btw: The paths are all based on `process.cwd()` , which is usually run from the root directory of the package (the directory where `package.json` is located).
105 |
106 | If there are any running problems, please provide a reproducible example in the [issue](https://github.com/chengpeiquan/bassist/issues) .
107 |
108 | ## Release Notes
109 |
110 | Please refer to [CHANGELOG](https://github.com/chengpeiquan/bassist/blob/main/packages/release/CHANGELOG.md) for details.
111 |
112 | ## License
113 |
114 | MIT License © 2023-PRESENT [chengpeiquan](https://github.com/chengpeiquan)
115 |
--------------------------------------------------------------------------------
/packages/eslint-config/src/configs/vue.ts:
--------------------------------------------------------------------------------
1 | import vuePlugin from 'eslint-plugin-vue'
2 | import _vueParser from 'vue-eslint-parser'
3 | import { GLOB_EXCLUDE, GLOB_VUE } from '../globs'
4 | import { getConfigName } from '../shared/utils'
5 | import {
6 | type FlatESLintConfig,
7 | type FlatESLintParser,
8 | type FlatESLintRules,
9 | } from '../types'
10 | import { tsParser, tsPlugin, typescript } from './typescript'
11 |
12 | const vueParser = _vueParser as unknown as FlatESLintParser
13 |
14 | export { vueParser, vuePlugin }
15 |
16 | export const reactivityTransform: FlatESLintConfig[] = [
17 | {
18 | name: getConfigName('vue', 'reactivity-transform'),
19 | languageOptions: {
20 | globals: {
21 | $: 'readonly',
22 | $$: 'readonly',
23 | $computed: 'readonly',
24 | $customRef: 'readonly',
25 | $ref: 'readonly',
26 | $shallowRef: 'readonly',
27 | $toRef: 'readonly',
28 | },
29 | },
30 | plugins: {
31 | vue: vuePlugin,
32 | },
33 | rules: {
34 | 'vue/no-setup-props-destructure': 'off',
35 | },
36 | },
37 | ]
38 |
39 | const vueCustomRules = {
40 | 'vue/component-tags-order': [
41 | 'off',
42 | { order: ['script', 'template', 'style'] },
43 | ],
44 | 'vue/custom-event-name-casing': ['error', 'camelCase'],
45 | 'vue/eqeqeq': ['error', 'smart'],
46 | 'vue/html-self-closing': [
47 | 'error',
48 | {
49 | html: {
50 | void: 'always',
51 | normal: 'always',
52 | component: 'always',
53 | },
54 | svg: 'always',
55 | math: 'always',
56 | },
57 | ],
58 | 'vue/max-attributes-per-line': 'off',
59 | 'vue/multi-word-component-names': 'off',
60 | 'vue/no-constant-condition': 'warn',
61 | 'vue/no-empty-pattern': 'error',
62 | 'vue/no-loss-of-precision': 'error',
63 | 'vue/no-unused-refs': 'error',
64 | 'vue/no-useless-v-bind': 'error',
65 | 'vue/no-v-html': 'off',
66 | 'vue/object-shorthand': [
67 | 'error',
68 | 'always',
69 | {
70 | ignoreConstructors: false,
71 | avoidQuotes: true,
72 | },
73 | ],
74 | 'vue/padding-line-between-blocks': ['error', 'always'],
75 | 'vue/prefer-template': 'error',
76 | 'vue/require-prop-types': 'off',
77 | 'vue/require-default-prop': 'off',
78 | } as unknown as FlatESLintRules
79 |
80 | const vue3Rules = {
81 | ...vuePlugin.configs.base.rules,
82 | ...vuePlugin.configs['vue3-essential'].rules,
83 | ...vuePlugin.configs['vue3-strongly-recommended'].rules,
84 | ...vuePlugin.configs['vue3-recommended'].rules,
85 | } as unknown as FlatESLintRules
86 |
87 | const vue2Rules = {
88 | ...vuePlugin.configs.base.rules,
89 | ...vuePlugin.configs.essential.rules,
90 | ...vuePlugin.configs['strongly-recommended'].rules,
91 | ...vuePlugin.configs.recommended.rules,
92 | } as unknown as FlatESLintRules
93 |
94 | const getVueConfig = (vueVersion: 'vue2' | 'vue3') => {
95 | const vueVersionRules = vueVersion === 'vue2' ? vue2Rules : vue3Rules
96 |
97 | const vueRules: FlatESLintConfig[] = [
98 | {
99 | name: getConfigName('vue', 'base'),
100 | files: [GLOB_VUE],
101 | plugins: {
102 | vue: vuePlugin,
103 | '@typescript-eslint': tsPlugin,
104 | },
105 | languageOptions: {
106 | ecmaVersion: 'latest',
107 | parser: vueParser,
108 | parserOptions: {
109 | parser: tsParser,
110 | sourceType: 'module',
111 | extraFileExtensions: ['.vue'],
112 | ecmaFeatures: {
113 | jsx: true,
114 | },
115 | },
116 | },
117 | processor: vuePlugin.processors['.vue'],
118 | rules: {
119 | ...typescript[0].rules,
120 | },
121 | ignores: [...GLOB_EXCLUDE],
122 | },
123 | {
124 | name: getConfigName('vue', vueVersion),
125 | plugins: {
126 | vue: vuePlugin,
127 | },
128 | rules: {
129 | ...vueVersionRules,
130 | ...vueCustomRules,
131 | },
132 | ignores: [...GLOB_EXCLUDE],
133 | },
134 | ...reactivityTransform,
135 | ]
136 |
137 | return vueRules
138 | }
139 |
140 | export const vueLegacy = getVueConfig('vue2')
141 | export const vue = getVueConfig('vue3')
142 |
--------------------------------------------------------------------------------
/packages/changelog/README.md:
--------------------------------------------------------------------------------
1 | # @bassist/changelog
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Simple CHANGELOG generator by [@chengpeiquan](https://github.com/chengpeiquan) , based on [conventional-changelog-cli](https://www.npmjs.com/package/conventional-changelog-cli).
19 |
20 | If you are tired of remembering long command names and command line configurations every time, you can use this package to simplify the operation.
21 |
22 | ## Usage
23 |
24 | This is a CLI tool, you can install it locally and run it through commands such as pnpm exec.
25 |
26 | Install it:
27 |
28 | ```bash
29 | pnpm add -D @bassist/changelog conventional-changelog-cli
30 | ```
31 |
32 | In your `package.json` :
33 |
34 | ```json
35 | {
36 | "scripts": {
37 | "gen:changelog": "pnpm exec changelog"
38 | }
39 | }
40 | ```
41 |
42 | Run on command line:
43 |
44 | ```bash
45 | pnpm gen:changelog
46 | ```
47 |
48 | You can see a CHANGELOG.md file in the project root directory, which will generate the software's change records based on your Git Logs.
49 |
50 | ## Implementation Principle
51 |
52 | In this package, the program will run the conventional-changelog CLI command to generate CHANGELOG, so `conventional-changelog-cli`, as the peerDependency of the package, also needs to be installed together.
53 |
54 | ## Options
55 |
56 | For most projects, the default settings are sufficient. If adjustments are sometimes needed, some options are provided to pass on.
57 |
58 | On the command line, options can be passed to the program, e.g. `--preset angular` by option, or `-p angular` by short flag.
59 |
60 | | Option | Short Flag | Default Value | Description |
61 | | :-----------: | :--------: | :------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
62 | | lerna-package | l | | Generate a changelog for a specific lerna package (:pkg-name@1.0.0) |
63 | | preset | p | `angular` | Name of the preset you want to use. Must be one of the following: `angular`, `atom`, `codemirror`, `conventionalcommits`, `ember`, `eslint`, `express`, `jquery` or `jshint` |
64 | | infile | i | `CHANGELOG.md` | Read the CHANGELOG from this file, and outputting to the same file |
65 | | release-count | r | `1` | How many releases to be generated from the latest, If `0` , the whole changelog will be regenerated and the outfile will be overwritten directory |
66 | | commit-path | | `./src` | Generate a changelog scoped to a specific directory |
67 |
68 | Btw: The paths are all based on `process.cwd()` , which is usually run from the root directory of the package (the directory where `package.json` is located).
69 |
70 | If there are any running problems, please provide a reproducible example in the [issue](https://github.com/chengpeiquan/bassist/issues), or use [conventional-changelog-cli](https://www.npmjs.com/package/conventional-changelog-cli) directly (need to configure it yourself)
71 |
72 | ## Release Notes
73 |
74 | Please refer to [CHANGELOG](https://github.com/chengpeiquan/bassist/blob/main/packages/changelog/CHANGELOG.md) for details.
75 |
76 | ## License
77 |
78 | MIT License © 2023-PRESENT [chengpeiquan](https://github.com/chengpeiquan)
79 |
--------------------------------------------------------------------------------
/packages/utils/src/device.ts:
--------------------------------------------------------------------------------
1 | import { getUserAgent } from './ua'
2 |
3 | /**
4 | * Checks if the code is being executed in a browser environment
5 | *
6 | * @category Device
7 | */
8 | export const isBrowser = typeof window !== 'undefined'
9 |
10 | /**
11 | * Checks if the code is being executed in a server environment
12 | *
13 | * @category Device
14 | */
15 | export const isServer = !isBrowser
16 |
17 | /**
18 | * Regular expression pattern to match mobile device user agents
19 | *
20 | * @category Device
21 | */
22 | export const mobileDevicesRegExp = /iPhone|phone|android|iPod|pad|iPad/i
23 |
24 | /**
25 | * Checks if the code is being executed on a mobile device
26 | *
27 | * @category Device
28 | */
29 | export function isMobile() {
30 | if (!isBrowser) return false
31 | return mobileDevicesRegExp.test(getUserAgent())
32 | }
33 |
34 | /**
35 | * Regular expression pattern to match tablet device user agents
36 | *
37 | * @category Device
38 | */
39 | export const tabletDevicesRegExp =
40 | /(ipad|tablet|(android(?!.*mobile))|(windows(?!.*phone)(.*touch))|kindle|playbook|silk|(puffin(?!.*(IP|AP|WP))))/i
41 |
42 | /**
43 | * Checks if the code is being executed on a tablet device
44 | *
45 | * @category Device
46 | */
47 | export function isTablet() {
48 | if (!isBrowser) return false
49 | if (!isMobile()) return false
50 | return tabletDevicesRegExp.test(getUserAgent())
51 | }
52 |
53 | /**
54 | * Checks if the code is being executed on a desktop device
55 | *
56 | * @category Device
57 | */
58 | export function isDesktop() {
59 | if (!isBrowser) return false
60 | return !isMobile()
61 | }
62 |
63 | /**
64 | * Regular expression pattern to match apple device user agents
65 | *
66 | * @category Device
67 | */
68 | export const appleDevicesRegExp = /(mac|iphone|ipod|ipad)/i
69 |
70 | /**
71 | * Checks if the code is being executed on an apple device
72 | *
73 | * @category Device
74 | */
75 | export function isAppleDevice() {
76 | if (!isBrowser) return false
77 | return appleDevicesRegExp.test(getUserAgent())
78 | }
79 |
80 | /**
81 | * Checks if the code is running on an Android device
82 | *
83 | * @category Device
84 | */
85 | export const isAndroid = /Android/i.test(getUserAgent())
86 |
87 | /**
88 | * Checks if the code is running on an iOS device
89 | *
90 | * @category Device
91 | */
92 | export const isIOS = /iPhone|iPod|iPad|iOS/i.test(getUserAgent())
93 |
94 | /**
95 | * Checks if the code is running in a Uni-App environment
96 | *
97 | * @category Device
98 | */
99 | export const isUniApp = /uni-app|html5plus/.test(getUserAgent())
100 |
101 | /**
102 | * Checks if the code is running in a WeChat (Weixin) environment
103 | *
104 | * @category Device
105 | */
106 | export const isWeixin = /MicroMessenger/i.test(getUserAgent())
107 |
108 | /**
109 | * Checks if the code is running in a QQ environment
110 | *
111 | * @category Device
112 | */
113 | export const isQQ = /\sQQ|mqqbrowser|qzone|qqbrowser/i.test(getUserAgent())
114 |
115 | /**
116 | * Checks if the code is running in a QQ Browser environment
117 | *
118 | * @category Device
119 | */
120 | export const isQQBrowser = /mqqbrowser|qqbrowser/i.test(getUserAgent())
121 |
122 | /**
123 | * Checks if the code is running in a Qzone environment
124 | *
125 | * @category Device
126 | */
127 | export const isQzone = /qzone\/.*_qz_([\d.]+)/i.test(getUserAgent())
128 |
129 | /**
130 | * Checks if the code is running in a Weibo environment
131 | *
132 | * @category Device
133 | */
134 | export const isWeibo = /(weibo).*weibo__([\d.]+)/i.test(getUserAgent())
135 |
136 | /**
137 | * Checks if the code is running in a Baidu Box App environment
138 | *
139 | * @category Device
140 | */
141 | export const isBaidu = /(baiduboxapp)\/([\d.]+)/i.test(getUserAgent())
142 |
143 | /**
144 | * @category Device
145 | */
146 | interface DeviceResizeWatcherOptions {
147 | // Executed when the page load done
148 | immediate: boolean
149 | }
150 |
151 | /**
152 | * Watches for page resize or orientation change events and executes the
153 | * callback
154 | *
155 | * @category Device
156 | * @param callback - The callback function to be executed
157 | * @param options - The options for the resize watcher `immediate`: Determines
158 | * whether the callback should be immediately executed on page load
159 | */
160 | export function watchResize(
161 | callback: () => void,
162 | { immediate }: DeviceResizeWatcherOptions = { immediate: true },
163 | ) {
164 | if (!isBrowser) return
165 | if (immediate) {
166 | window.addEventListener('load', callback, false)
167 | }
168 | window.addEventListener(
169 | 'orientationchange' in window ? 'orientationchange' : 'resize',
170 | callback,
171 | false,
172 | )
173 | }
174 |
--------------------------------------------------------------------------------
/packages/utils/test/regexp.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 | import { isMob, isEmail, isUrl, isIdCard, isBankCard, isIPv4, isIPv6 } from '..'
3 |
4 | describe('isMob', () => {
5 | it('Valid data', () => {
6 | expect(isMob(13800138000)).toBeTruthy()
7 | expect(isMob('13800138000')).toBeTruthy()
8 | expect(isMob('13100000000')).toBeTruthy()
9 | })
10 | it('Invalid data', () => {
11 | expect(isMob('13800138000 ')).toBeFalsy()
12 | expect(isMob('1380013800')).toBeFalsy()
13 | expect(isMob(123456)).toBeFalsy()
14 | expect(isMob('hello')).toBeFalsy()
15 | })
16 | })
17 |
18 | describe('isEmail', () => {
19 | it('Valid data', () => {
20 | expect(isEmail('abc@qq.com')).toBeTruthy()
21 | expect(isEmail('123456@qq.com')).toBeTruthy()
22 | expect(isEmail('test@163.com')).toBeTruthy()
23 | })
24 | it('Invalid data', () => {
25 | expect(isEmail('abc@qq.com ')).toBeFalsy()
26 | expect(isEmail('abc@q q.com ')).toBeFalsy()
27 | expect(isEmail('abc@qq')).toBeFalsy()
28 | expect(isEmail('@qq.com')).toBeFalsy()
29 | expect(isEmail('hello')).toBeFalsy()
30 | })
31 | })
32 |
33 | describe('isUrl', () => {
34 | it('Valid data', () => {
35 | expect(isUrl('http://example.com')).toBeTruthy()
36 | expect(isUrl('https://example.com')).toBeTruthy()
37 | expect(isUrl('https://example.com ')).toBeTruthy()
38 | expect(isUrl('https://example.com/')).toBeTruthy()
39 | expect(isUrl('https://foo.example.com')).toBeTruthy()
40 | expect(isUrl('https://foo.bar.example.com')).toBeTruthy()
41 | expect(isUrl('https://example.com/foo')).toBeTruthy()
42 | expect(isUrl('https://example.com/foo/bar')).toBeTruthy()
43 | expect(isUrl('https://example.com/foo/bar/baz')).toBeTruthy()
44 | expect(isUrl('https://example.com/foo?a=1')).toBeTruthy()
45 | expect(isUrl('https://example.com/foo?a=1&b=2')).toBeTruthy()
46 | expect(isUrl('https://example.com/foo?a=1#b=2')).toBeTruthy()
47 | expect(isUrl('https://example.com/foo#a=1?b=2')).toBeTruthy()
48 | })
49 | it('Invalid data', () => {
50 | expect(isUrl('http:example.com')).toBeFalsy()
51 | expect(isUrl('http:/example.com')).toBeFalsy()
52 | expect(isUrl('https://example..com')).toBeFalsy()
53 | expect(isUrl('https://foo..example.com')).toBeFalsy()
54 | expect(isUrl('hello')).toBeFalsy()
55 | })
56 | })
57 |
58 | describe('isIdCard', () => {
59 | // https://www.bjcourt.gov.cn/zxxx/indexOld.htm?jbfyId=17&zxxxlx=100013002
60 | // http://legal.people.com.cn/n1/2020/0424/c42510-31687296.html
61 | it('Valid data', () => {
62 | expect(isIdCard('110223790813697')).toBeTruthy()
63 | expect(isIdCard('110225196403026127')).toBeTruthy()
64 | expect(isIdCard('152221198906101419')).toBeTruthy()
65 | })
66 | it('Invalid data', () => {
67 | expect(isIdCard('1102237908136971')).toBeFalsy()
68 | expect(isIdCard('1102221974****4827')).toBeFalsy()
69 | expect(isIdCard('123456')).toBeFalsy()
70 | expect(isIdCard('hello')).toBeFalsy()
71 | })
72 | })
73 |
74 | describe('isBankCard', () => {
75 | // https://ddu1222.github.io/bankcard-validator/bcBuilder.html
76 | it('Valid data', () => {
77 | expect(isBankCard('5124255722414430')).toBeTruthy()
78 | expect(isBankCard('5149570635749446')).toBeTruthy()
79 | expect(isBankCard('4357458903454875')).toBeTruthy()
80 | expect(isBankCard('6223508057942120')).toBeTruthy()
81 | })
82 | it('Invalid data', () => {
83 | expect(isBankCard('151242557224144301')).toBeFalsy()
84 | expect(isBankCard('123456')).toBeFalsy()
85 | expect(isBankCard('hello')).toBeFalsy()
86 | })
87 | })
88 |
89 | describe('isIPv4', () => {
90 | it('Valid data', () => {
91 | expect(isIPv4('0.0.0.0')).toBeTruthy()
92 | expect(isIPv4('1.2.3.4')).toBeTruthy()
93 | expect(isIPv4('127.0.0.1')).toBeTruthy()
94 | expect(isIPv4('192.168.0.1')).toBeTruthy()
95 | expect(isIPv4('10.24.3.68')).toBeTruthy()
96 | expect(isIPv4('45.150.220.38')).toBeTruthy()
97 | expect(isIPv4('255.255.255.0')).toBeTruthy()
98 | })
99 | it('Invalid data', () => {
100 | expect(isIPv4('123')).toBeFalsy()
101 | expect(isIPv4('localhost')).toBeFalsy()
102 | expect(isIPv4('999.999.999.999')).toBeFalsy()
103 | expect(isIPv4('192.168.1')).toBeFalsy()
104 | })
105 | })
106 |
107 | describe('isIPv6', () => {
108 | it('Valid data', () => {
109 | expect(isIPv6('2001:0DB8:0000:0023:0008:0800:200C:417A')).toBeTruthy()
110 | expect(isIPv6('2001:DB8:0:23:8:800:200C:417A')).toBeTruthy()
111 | expect(isIPv6('FF01:0:0:0:0:0:0:1101')).toBeTruthy()
112 | expect(isIPv6('FF01::1101')).toBeTruthy()
113 | expect(isIPv6('0:0:0:0:0:0:0:1')).toBeTruthy()
114 | expect(isIPv6('::1')).toBeTruthy()
115 | expect(isIPv6('0:0:0:0:0:0:0:0')).toBeTruthy()
116 | expect(isIPv6('::')).toBeTruthy()
117 | expect(isIPv6('::192.168.0.1')).toBeTruthy()
118 | expect(isIPv6('::FFFF:192.168.0.1')).toBeTruthy()
119 | })
120 | it('Invalid data', () => {
121 | expect(isIPv6('123')).toBeFalsy()
122 | expect(isIPv6('localhost')).toBeFalsy()
123 | expect(isIPv6('999.999.999.999')).toBeFalsy()
124 | expect(isIPv6('192.168.1')).toBeFalsy()
125 | expect(isIPv6('0.0.0.0')).toBeFalsy()
126 | expect(isIPv6('1.2.3.4')).toBeFalsy()
127 | expect(isIPv6('127.0.0.1')).toBeFalsy()
128 | expect(isIPv6('192.168.0.1')).toBeFalsy()
129 | expect(isIPv6('10.24.3.68')).toBeFalsy()
130 | expect(isIPv6('45.150.220.38')).toBeFalsy()
131 | expect(isIPv6('255.255.255.0')).toBeFalsy()
132 | })
133 | })
134 |
--------------------------------------------------------------------------------
/packages/utils/test/data.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 | import {
3 | getDataType,
4 | isObject,
5 | isArray,
6 | inRange,
7 | isFunction,
8 | isAsyncFunction,
9 | isPromise,
10 | isEven,
11 | isOdd,
12 | } from '..'
13 |
14 | class Foo {
15 | bar() {}
16 | }
17 |
18 | const obj = {
19 | then() {},
20 | }
21 |
22 | describe('getDataType', () => {
23 | it('Valid data', () => {
24 | expect(getDataType('')).toBe('String')
25 | expect(getDataType(String(1))).toBe('String')
26 | expect(getDataType(new String(1))).toBe('String')
27 | expect(getDataType(null)).toBe('Null')
28 | expect(getDataType(undefined)).toBe('Undefined')
29 | expect(getDataType(new Date())).toBe('Date')
30 | expect(getDataType(/foo/)).toBe('RegExp')
31 | })
32 | })
33 |
34 | describe('isArray', () => {
35 | it('Valid data', () => {
36 | expect(isArray([])).toBeTruthy()
37 | expect(isArray(new Array(1))).toBeTruthy()
38 | expect(isArray(Array.from(new Set()))).toBeTruthy()
39 | })
40 | it('Invalid data', () => {
41 | expect(isArray({})).toBeFalsy()
42 | expect(isArray(new Object())).toBeFalsy()
43 | expect(isArray(Object.create([]))).toBeFalsy()
44 | expect(isArray(Object.create({ foo: 1 }))).toBeFalsy()
45 | expect(isArray('')).toBeFalsy()
46 | expect(isArray(null)).toBeFalsy()
47 | expect(isArray(undefined)).toBeFalsy()
48 | expect(isArray(Object(1))).toBeFalsy()
49 | })
50 | })
51 |
52 | describe('isAsyncFunction', () => {
53 | it('Valid data', () => {
54 | expect(isAsyncFunction(async () => {})).toBeTruthy()
55 | expect(isAsyncFunction(async function () {})).toBeTruthy()
56 | })
57 | it('Invalid data', () => {
58 | expect(isAsyncFunction(new Function())).toBeFalsy()
59 | expect(isAsyncFunction(function () {})).toBeFalsy()
60 | expect(isAsyncFunction(() => {})).toBeFalsy()
61 | expect(isAsyncFunction(Foo)).toBeFalsy()
62 | expect(isAsyncFunction(new Foo().bar)).toBeFalsy()
63 | })
64 | })
65 |
66 | describe('isEven', () => {
67 | it('Valid data', () => {
68 | expect(isEven(0)).toBeTruthy()
69 | expect(isEven(2)).toBeTruthy()
70 | expect(isEven(4)).toBeTruthy()
71 | })
72 | it('Invalid data', () => {
73 | expect(isEven(-1)).toBeFalsy()
74 | expect(isEven(1)).toBeFalsy()
75 | expect(isEven(1.5)).toBeFalsy()
76 | expect(isEven(3)).toBeFalsy()
77 | expect(isEven(15)).toBeFalsy()
78 | })
79 | })
80 |
81 | describe('isFunction', () => {
82 | it('Valid data', () => {
83 | expect(isFunction(new Function())).toBeTruthy()
84 | expect(isFunction(function () {})).toBeTruthy()
85 | expect(isFunction(() => {})).toBeTruthy()
86 | expect(isFunction(async () => {})).toBeTruthy()
87 | expect(isFunction(async function () {})).toBeTruthy()
88 | expect(isFunction(Foo)).toBeTruthy()
89 | expect(isFunction(new Foo().bar)).toBeTruthy()
90 | })
91 | it('Invalid data', () => {
92 | expect(isFunction(1)).toBeFalsy()
93 | expect(isFunction('1')).toBeFalsy()
94 | expect(isFunction(undefined)).toBeFalsy()
95 | expect(isFunction(null)).toBeFalsy()
96 | expect(isFunction({})).toBeFalsy()
97 | expect(isFunction([])).toBeFalsy()
98 | })
99 | })
100 |
101 | describe('isOdd', () => {
102 | it('Valid data', () => {
103 | expect(isOdd(-1)).toBeTruthy()
104 | expect(isOdd(1)).toBeTruthy()
105 | expect(isOdd(3)).toBeTruthy()
106 | expect(isOdd(15)).toBeTruthy()
107 | })
108 | it('Invalid data', () => {
109 | expect(isOdd(0)).toBeFalsy()
110 | expect(isOdd(1.5)).toBeFalsy()
111 | expect(isOdd(2)).toBeFalsy()
112 | expect(isOdd(4)).toBeFalsy()
113 | })
114 | })
115 |
116 | describe('isObject', () => {
117 | it('Valid data', () => {
118 | expect(isObject({})).toBeTruthy()
119 | expect(isObject(new Object())).toBeTruthy()
120 | expect(isObject(Object.create({ foo: 1 }))).toBeTruthy()
121 | })
122 | it('Invalid data', () => {
123 | expect(isObject('')).toBeFalsy()
124 | expect(isObject(null)).toBeFalsy()
125 | expect(isObject(undefined)).toBeFalsy()
126 | expect(isObject(Object(1))).toBeFalsy()
127 | })
128 | })
129 |
130 | describe('isPromise', () => {
131 | it('Valid data', () => {
132 | expect(isPromise(new Promise((r) => r()))).toBeTruthy()
133 | expect(isPromise(Promise.resolve())).toBeTruthy()
134 | expect(isPromise((async () => {})())).toBeTruthy()
135 | })
136 | it('Invalid data', () => {
137 | expect(isPromise(obj)).toBeFalsy()
138 | expect(isPromise('')).toBeFalsy()
139 | expect(isPromise(null)).toBeFalsy()
140 | expect(isPromise(undefined)).toBeFalsy()
141 | expect(isPromise(Object(1))).toBeFalsy()
142 | expect(isPromise({})).toBeFalsy()
143 | expect(isPromise(new Object())).toBeFalsy()
144 | expect(isPromise(Object.create({ foo: 1 }))).toBeFalsy()
145 | })
146 | })
147 |
148 | describe('inRange', () => {
149 | it('Valid data', () => {
150 | expect(inRange({ num: 1, min: 0, max: 5 })).toBeTruthy()
151 | expect(inRange({ num: 1, min: -5, max: 5 })).toBeTruthy()
152 | expect(inRange({ num: 1, min: 5, max: -5 })).toBeTruthy()
153 | expect(
154 | inRange({ num: -4, min: -5, max: 5, includeMin: false }),
155 | ).toBeTruthy()
156 | expect(inRange({ num: 4, min: -5, max: 5, includeMax: false })).toBeTruthy()
157 | })
158 | it('Invalid data', () => {
159 | expect(inRange({ num: 10, min: 0, max: 5 })).toBeFalsy()
160 | expect(inRange({ num: -99, min: -5, max: 5 })).toBeFalsy()
161 | expect(inRange({ num: NaN, min: 5, max: -5 })).toBeFalsy()
162 | expect(inRange({ num: -5, min: -5, max: 5, includeMin: false })).toBeFalsy()
163 | expect(inRange({ num: 5, min: -5, max: 5, includeMax: false })).toBeFalsy()
164 | })
165 | })
166 |
--------------------------------------------------------------------------------
/packages/utils/src/load.ts:
--------------------------------------------------------------------------------
1 | import { pnoop } from './data'
2 | import { isBrowser } from './device'
3 | import { getQuery } from './query'
4 | import { randomString } from './random'
5 |
6 | /**
7 | * @category Network
8 | */
9 | export type ResourcesSupportedWithLoadRes = 'js' | 'css' | 'style'
10 |
11 | type ResourcesElement = HTMLScriptElement | HTMLLinkElement | HTMLStyleElement
12 |
13 | /**
14 | * @category Network
15 | */
16 | export interface LoadResOptions {
17 | type: ResourcesSupportedWithLoadRes
18 | id: string
19 | resource: string
20 | }
21 |
22 | /**
23 | * Dynamic loading of resources
24 | *
25 | * @category Network
26 | */
27 | export function loadRes({ type, id, resource }: LoadResOptions) {
28 | return new Promise((resolve, reject) => {
29 | if (!isBrowser || document.querySelector(`#${id}`)) {
30 | reject()
31 | return
32 | }
33 |
34 | function bindStatus(el: ResourcesElement) {
35 | el.addEventListener('load', resolve)
36 | el.addEventListener('error', reject)
37 | el.addEventListener('abort', reject)
38 | }
39 |
40 | switch (type) {
41 | case 'js': {
42 | const script = document.createElement('script')
43 | script.id = id
44 | script.async = true
45 | script.src = resource
46 | bindStatus(script)
47 | document.head.appendChild(script)
48 | break
49 | }
50 |
51 | case 'css': {
52 | const link = document.createElement('link')
53 | link.id = id
54 | link.rel = 'stylesheet'
55 | link.href = resource
56 | bindStatus(link)
57 | document.head.appendChild(link)
58 | break
59 | }
60 |
61 | case 'style': {
62 | const style = document.createElement('style')
63 | style.id = id
64 | bindStatus(style)
65 | document.head.appendChild(style)
66 | style.appendChild(document.createTextNode(resource))
67 | break
68 | }
69 | }
70 | })
71 | }
72 |
73 | /**
74 | * JSON with Padding
75 | *
76 | * Note: JSONP is a method for sending JSON data without worrying about
77 | * cross-domain issues. JSONP does not use the XMLHttpRequest or Fetch, it uses
78 | * the `` tag instead.
79 | *
80 | * @category Network
81 | * @example
82 | * ```ts
83 | * interface Res {
84 | * code: number
85 | * data: string[]
86 | * msg: string
87 | * }
88 | *
89 | * // The default and server-side agreement is to use `callback` Query
90 | * const url = `https://example.com/data`
91 | *
92 | * // When no `callback` param passed, a random function name is created
93 | * // Equivalent to `https://example.com/data?callback=randomCallbackName`
94 | * // Pass the type of response as a generic to get a typed return value
95 | * const res = await jsonp(url)
96 | *
97 | * // You can also specify the `callback` function name
98 | * const callback = 'jsonp_callback_123456'
99 | * const res2 = await jsonp(url, callback)
100 | *
101 | * // If the server does not agree on the `callback` Query,
102 | * // you can specify other valid Query in this way.
103 | * const urlWithCallback = `https://example.com/data?cb=${callback}`
104 | * const res3 = await jsonp(urlWithCallback)
105 | * ```
106 | *
107 | * @param url - The Resource script URL.
108 | * @param callback - The Callback function name, Optional, it will be an
109 | * attribute name of `window`, so needs to be unique, If not passed, a random
110 | * function name will be automatically generated.
111 | * @see https://en.wikipedia.org/wiki/JSONP
112 | */
113 | export function jsonp(url: string, callback?: string) {
114 | return new Promise((resolve, reject) => {
115 | if (!isBrowser) {
116 | reject()
117 | return
118 | }
119 |
120 | const callbackByUrl = getQuery(url)
121 | const cb =
122 | callbackByUrl ||
123 | callback ||
124 | `jsonp_callback_${randomString().replace(/-/g, '_')}`
125 |
126 | // @ts-expect-error
127 | window[cb] = (data: T) => {
128 | try {
129 | // @ts-expect-error
130 | // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
131 | delete window[cb]
132 | } catch {
133 | // @ts-expect-error
134 | window[cb] = undefined
135 | }
136 | document.body.removeChild(script)
137 | resolve(data)
138 | }
139 |
140 | const separator = url.includes('?') ? '&' : '?'
141 | const scriptUrl = url.includes('callback')
142 | ? url
143 | : url + separator + 'callback=' + cb
144 |
145 | const script = document.createElement('script')
146 | script.src = scriptUrl
147 | script.onerror = reject
148 | document.body.appendChild(script)
149 | })
150 | }
151 |
152 | /**
153 | * Load a batch of images in concurrent mode
154 | *
155 | * @category Network
156 | */
157 | export function concurrentLoadImages(images: string[]) {
158 | const promises = []
159 |
160 | for (const path in images) {
161 | promises.push(
162 | new Promise((resolve, reject) => {
163 | const img = new Image()
164 | img.onload = resolve
165 | img.onerror = reject
166 | img.src = path
167 | }),
168 | )
169 | }
170 |
171 | return Promise.all(promises)
172 | }
173 |
174 | /**
175 | * Load a batch of images in serial mode
176 | *
177 | * @category Network
178 | */
179 | export async function serialLoadImages(images: string[]) {
180 | for (const path in images) {
181 | await concurrentLoadImages([path])
182 | }
183 | }
184 |
185 | /**
186 | * Preload images
187 | *
188 | * It can be used to preload large images in advance, or wait for the image to
189 | * be loaded before ending Loading and other usage scenarios.
190 | *
191 | * @category Network
192 | * @example
193 | * ;```ts
194 | * const images = [
195 | * 'https://example.com/1.jpg',
196 | * 'https://example.com/2.jpg',
197 | * 'https://example.com/3.jpg',
198 | * ]
199 | *
200 | * // Start loading, Show loading icon etc.
201 | * setLoading(true)
202 | *
203 | * // Wait for the images to be pre-rendered
204 | * await preloadImages(images)
205 | *
206 | * // End loading state
207 | * setLoading(false)
208 | * ```
209 | *
210 | * @param images - An array containing image urls
211 | * @param mode - Concurrent mode is used by default. If there are too many
212 | * pictures, you can choose serial mode.
213 | */
214 | export async function preloadImages(
215 | images: string[],
216 | mode: 'concurrent' | 'serial' = 'concurrent',
217 | ) {
218 | switch (mode) {
219 | case 'concurrent': {
220 | await concurrentLoadImages(images)
221 | break
222 | }
223 |
224 | case 'serial': {
225 | await serialLoadImages(images)
226 | break
227 | }
228 |
229 | default: {
230 | await pnoop()
231 | break
232 | }
233 | }
234 | }
235 |
--------------------------------------------------------------------------------
/packages/utils/test/format.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 | import {
3 | extractNumber,
4 | formatAmount,
5 | ellipsis,
6 | capitalize,
7 | kebabCase,
8 | camelCase,
9 | pascalCase,
10 | escapeRegExp,
11 | sortKeys,
12 | unique,
13 | excludeFields,
14 | formatTime,
15 | formatDuration,
16 | removeHtmlTags,
17 | toArray,
18 | ensurePrefix,
19 | ensureSuffix,
20 | } from '..'
21 |
22 | describe('format', () => {
23 | it('extractNumber', () => {
24 | expect(extractNumber('akdhaskd2')).toBe('2')
25 | expect(extractNumber('阿斯0达克哈1234克qqwer5')).toBe('12345')
26 | })
27 |
28 | it('formatAmount', () => {
29 | expect(formatAmount('4')).toBe('4.00')
30 | expect(formatAmount('1.5')).toBe('1.50')
31 | expect(formatAmount('0.44565')).toBe('0.45')
32 | })
33 |
34 | it('ellipsis', () => {
35 | expect(ellipsis('Hello World', 5)).toBe('Hello ...')
36 | expect(ellipsis('Hello World', 8)).toBe('Hello Wo ...')
37 | })
38 |
39 | it('capitalize', () => {
40 | expect(capitalize('4')).toBe('4')
41 | expect(capitalize('a')).toBe('A')
42 | expect(capitalize('abc')).toBe('Abc')
43 | expect(capitalize('abc')).toBe('Abc')
44 | expect(capitalize('abcDef')).toBe('AbcDef')
45 | expect(capitalize('!abc')).toBe('!abc')
46 | expect(capitalize('')).toBe('')
47 | })
48 |
49 | it('kebabCase', () => {
50 | expect(kebabCase('4')).toBe('4')
51 | expect(kebabCase('a')).toBe('a')
52 | expect(kebabCase('AbcDef')).toBe('abc-def')
53 | expect(kebabCase('Abc-Def')).toBe('abc--def')
54 | expect(kebabCase('abc_efg_hijklmn')).toBe('abc-efg-hijklmn')
55 | expect(kebabCase('!abc')).toBe('!abc')
56 | expect(kebabCase('')).toBe('')
57 | })
58 |
59 | it('camelCase', () => {
60 | expect(camelCase('4')).toBe('4')
61 | expect(camelCase('a')).toBe('a')
62 | expect(camelCase('AbcDef')).toBe('abcDef')
63 | expect(camelCase('Abc-Def')).toBe('abcDef')
64 | expect(camelCase('abc_efg_hijklmn')).toBe('abcEfgHijklmn')
65 | expect(camelCase('!abc')).toBe('!abc')
66 | expect(camelCase('')).toBe('')
67 | })
68 |
69 | it('pascalCase', () => {
70 | expect(pascalCase('4')).toBe('4')
71 | expect(pascalCase('a')).toBe('A')
72 | expect(pascalCase('AbcDef')).toBe('AbcDef')
73 | expect(pascalCase('Abc-Def')).toBe('AbcDef')
74 | expect(pascalCase('abc_efg_hijklmn')).toBe('AbcEfgHijklmn')
75 | expect(pascalCase('!abc')).toBe('!abc')
76 | expect(pascalCase('')).toBe('')
77 | })
78 |
79 | it('escapeRegExp', () => {
80 | expect(escapeRegExp('@bassist/utils')).toBe('@bassist/utils')
81 | expect(escapeRegExp('https://example.com/foo')).toBe(
82 | 'https://example\\.com/foo',
83 | )
84 | })
85 |
86 | it('sortKeys', () => {
87 | expect(
88 | sortKeys({
89 | c: 3,
90 | d: { c: 3, a: 1, b: 2 },
91 | a: 1,
92 | e: [
93 | { c: 3, a: 1, b: 2 },
94 | { c: 3, a: 1, b: 2 },
95 | ],
96 | b: 2,
97 | }),
98 | ).toEqual({
99 | a: 1,
100 | b: 2,
101 | c: 3,
102 | d: { a: 1, b: 2, c: 3 },
103 | e: [
104 | { a: 1, b: 2, c: 3 },
105 | { a: 1, b: 2, c: 3 },
106 | ],
107 | })
108 |
109 | expect(
110 | sortKeys([
111 | { c: 3, a: 1, b: 2 },
112 | { c: 3, a: 1, b: 2 },
113 | ]),
114 | ).toEqual([
115 | { a: 1, b: 2, c: 3 },
116 | { a: 1, b: 2, c: 3 },
117 | ])
118 |
119 | expect(null).toBeNull()
120 | expect(undefined).toBeUndefined()
121 | expect('foo').toBe('foo')
122 | })
123 |
124 | it('unique', () => {
125 | expect(
126 | unique({
127 | primaryKey: 'foo',
128 | list: [
129 | { foo: 1, bar: 1 },
130 | { foo: 1, bar: 2 },
131 | { foo: 2, bar: 1 },
132 | ],
133 | }),
134 | ).toEqual([
135 | { foo: 1, bar: 1 },
136 | { foo: 2, bar: 1 },
137 | ])
138 |
139 | expect(
140 | unique({
141 | primaryKey: 'bar',
142 | list: [
143 | { foo: 1, bar: 1 },
144 | { foo: 1, bar: 2 },
145 | { foo: 2, bar: 1 },
146 | ],
147 | }),
148 | ).toEqual([
149 | { foo: 1, bar: 1 },
150 | { foo: 1, bar: 2 },
151 | ])
152 |
153 | expect(
154 | unique({
155 | primaryKey: 'foo',
156 | list: [
157 | { foo: 1, bar: 1 },
158 | { foo: 2, bar: null },
159 | { foo: 3, bar: [1, 2, 3] },
160 | { foo: 3, bar: [] },
161 | ],
162 | }),
163 | ).toEqual([
164 | { foo: 1, bar: 1 },
165 | { foo: 2, bar: null },
166 | { foo: 3, bar: [1, 2, 3] },
167 | ])
168 | })
169 |
170 | it('excludeFields', () => {
171 | const obj = {
172 | foo: 'foo',
173 | bar: 'bar',
174 | baz: {
175 | foo: 'foo',
176 | bar: 'bar',
177 | },
178 | num: 1,
179 | bool: true,
180 | }
181 |
182 | expect(excludeFields(obj, ['foo', 'bar'])).toEqual({
183 | baz: {
184 | foo: 'foo',
185 | bar: 'bar',
186 | },
187 | num: 1,
188 | bool: true,
189 | })
190 |
191 | expect(excludeFields(obj, ['baz', 'num'])).toEqual({
192 | foo: 'foo',
193 | bar: 'bar',
194 | bool: true,
195 | })
196 |
197 | expect(excludeFields(obj, [])).toEqual(obj)
198 | expect(excludeFields(obj, ['test'])).toEqual(obj)
199 | })
200 |
201 | it('formatTime', () => {
202 | // A date string in standard ISO 8601 format
203 | expect(formatTime(new Date('2023-01-01'))).toBe('2023-01-01 08:00:00')
204 |
205 | // Non-standard date string format
206 | expect(formatTime(new Date('2023/01/01'))).toBe('2023-01-01 00:00:00')
207 |
208 | expect(formatTime(new Date('2023-01-01 14:05:59'))).toBe(
209 | '2023-01-01 14:05:59',
210 | )
211 | expect(formatTime(new Date('2023-01-01 00:05:59'))).toBe(
212 | '2023-01-01 00:05:59',
213 | )
214 | expect(formatTime(new Date('2023/01/01 00:05:59'))).toBe(
215 | '2023-01-01 00:05:59',
216 | )
217 | expect(formatTime(new Date('2023-01-01 14:05:59'), true)).toBe('2023-01-01')
218 | })
219 |
220 | it('formatDuration', () => {
221 | expect(
222 | formatDuration(+new Date('2023-01-02') - +new Date('2023-01-01')),
223 | ).toBe('1 天')
224 |
225 | expect(
226 | formatDuration(
227 | +new Date('2023-01-02 04:00:00') - +new Date('2023-01-01 00:00:00'),
228 | ),
229 | ).toBe('1 天 4 小时')
230 |
231 | expect(
232 | formatDuration(
233 | +new Date('2023-01-02 04:15:00') - +new Date('2023-01-01 00:00:00'),
234 | ),
235 | ).toBe('1 天 4 小时 15 分钟')
236 |
237 | expect(
238 | formatDuration(
239 | +new Date('2023-01-12 04:15:36') - +new Date('2023-01-01 00:00:00'),
240 | ),
241 | ).toBe('11 天 4 小时 15 分钟 36 秒')
242 |
243 | expect(
244 | formatDuration(
245 | +new Date('2023-01-12 04:15:36') - +new Date('2023-01-01 00:00:00'),
246 | {
247 | days: 'Days',
248 | hours: 'Hours',
249 | minutes: 'Minutes',
250 | seconds: 'Seconds',
251 | },
252 | ),
253 | ).toBe('11 Days 4 Hours 15 Minutes 36 Seconds')
254 | })
255 |
256 | it('removeHtmlTags', () => {
257 | expect(removeHtmlTags('hello>')).toBe('hello')
258 | expect(removeHtmlTags('
hello world>')).toBe('hello world')
259 | })
260 |
261 | it('toArray', () => {
262 | expect(toArray([1, 2, 3])).toEqual([1, 2, 3])
263 | expect(toArray(1)).toEqual([1])
264 | expect(toArray()).toEqual([])
265 | })
266 |
267 | it('ensurePrefix', () => {
268 | expect(ensurePrefix('https://', 'https://example.com')).toBe(
269 | 'https://example.com',
270 | )
271 | expect(ensurePrefix('https://', 'example.com')).toBe('https://example.com')
272 | })
273 |
274 | it('ensureSuffix', () => {
275 | expect(ensureSuffix('/', '/path/to')).toBe('/path/to/')
276 | expect(ensureSuffix('/', '/path/to/')).toBe('/path/to/')
277 | })
278 | })
279 |
--------------------------------------------------------------------------------
/packages/eslint-config/README.zh-CN.md:
--------------------------------------------------------------------------------
1 | # @bassist/eslint-config
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | [English](https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/README.md) | 简体中文
19 |
20 | 一款现代化的扁平 ESLint 配置,适用于 [ESLint](https://eslint.org/) V9 ,由 [@chengpeiquan](https://github.com/chengpeiquan) 精心打造。
21 |
22 | ## ⚡ 使用方法
23 |
24 | 使用此 ESLint 配置仅需三步:
25 |
26 | 1. 安装依赖(参考:[🚀 安装](#-安装))
27 | 2. 添加 ESLint 配置文件(参考:[📂 配置文件](#-配置文件))
28 | 3. 在 VS Code 的 `settings.json` 启用自动 Lint(参考:[🛠 VS Code 配置](#-vs-code-配置))
29 |
30 | 这个快速指南可以作为入门辅助,避免遗漏关键步骤 🚀 。
31 |
32 | ## 🚀 安装
33 |
34 | 使用常用的包管理器安装该包:
35 |
36 | ```bash
37 | npm install -D eslint @bassist/eslint-config
38 | ```
39 |
40 | **注意:** 需要 ESLint 版本 >= `9.0.0` ,以及 TypeScript 版本 >= `5.0.0`。
41 |
42 | 如果使用的是 `pnpm`,建议在项目根目录添加 `.npmrc` 文件,并包含以下配置,以更顺利地处理 peer 依赖:
43 |
44 | ```ini
45 | shamefully-hoist=true
46 | auto-install-peers=true
47 | ```
48 |
49 | > 如果仍在使用 ESLint v8,请参考旧版(已不再维护)包:[@bassist/eslint](https://www.npmjs.com/package/@bassist/eslint)。
50 |
51 | ## 📂 配置文件
52 |
53 | 在项目根目录创建 `eslint.config.js` 文件:
54 |
55 | ```js
56 | // eslint.config.js
57 | import { imports, typescript } from '@bassist/eslint-config'
58 |
59 | // 导出一个包含多个配置对象的数组
60 | export default [...imports, ...typescript]
61 | ```
62 |
63 | 然后在 `package.json` 中添加 "type": "module" :
64 |
65 | ```json
66 | {
67 | "type": "module",
68 | "scripts": {
69 | "lint": "eslint src",
70 | "lint:inspector": "npx @eslint/config-inspector"
71 | }
72 | }
73 | ```
74 |
75 | 运行 `npm run lint` 以检查代码,或运行 `npm run lint:inspector` 在 `http://localhost:7777` 查看可视化的 ESLint 配置。
76 |
77 | > 对于 TypeScript 配置文件(例如 `eslint.config.ts` ),需要 [额外的设置](https://eslint.org/docs/latest/use/configure/configuration-files#typescript-configuration-files) 。
78 |
79 | ```bash
80 | # 为 Node.js 提供运行时 TypeScript 和 ESM 支持
81 | # 才可以使用 `eslint.config.ts` 作为配置文件
82 | npm install -D jiti
83 | ```
84 |
85 | ## ✅ 类型安全的配置
86 |
87 | 为了增强类型安全性,可以使用 `defineFlatConfig`:
88 |
89 | ```js
90 | // @ts-check
91 | import { defineFlatConfig, imports, vue } from '@bassist/eslint-config'
92 |
93 | export default defineFlatConfig([
94 | ...imports,
95 | ...vue,
96 | // 添加更多自定义配置
97 | {
98 | // 为每个配置提供名称,以便在运行 `npm run lint:inspector` 时,
99 | // 可以在可视化工具中清晰展示
100 | name: 'my-custom-rule/vue',
101 | rules: {
102 | // 例如:默认情况下,该规则是 `off`
103 | 'vue/component-tags-order': 'error',
104 | },
105 | ignores: ['examples'],
106 | },
107 | ])
108 | ```
109 |
110 | ## 🛠 VS Code 配置
111 |
112 | 在 VS Code 工作区的 `settings.json` 添加以下配置,以启用自动 Lint 修复:
113 |
114 | ```json
115 | {
116 | "editor.formatOnSave": true,
117 | "editor.codeActionsOnSave": {
118 | "source.fixAll.eslint": "always",
119 | "source.fixAll.prettier": "always"
120 | },
121 | "editor.defaultFormatter": "esbenp.prettier-vscode",
122 | "eslint.useFlatConfig": true,
123 | "eslint.format.enable": true,
124 | "eslint.validate": [
125 | "javascript",
126 | "javascriptreact",
127 | "typescript",
128 | "typescriptreact"
129 | ],
130 | "prettier.configPath": "./.prettierrc.js"
131 | }
132 | ```
133 |
134 | 关于 `prettier.configPath` 请查看 [格式化工具](#格式化工具) 部分。
135 |
136 | ## 📘 API 参考
137 |
138 | ### defineFlatConfig
139 |
140 | 定义 ESLint 配置,可选支持 Prettier 和 Tailwind CSS。
141 |
142 | API 类型声明:
143 |
144 | ```ts
145 | /**
146 | * 定义 ESLint 配置,可选支持 Prettier 集成。
147 | *
148 | * @param configs 基础 ESLint 配置数组。
149 | * @param options - 配置选项。
150 | * @returns 最终的 ESLint 配置数组。
151 | */
152 | declare const defineFlatConfig: (
153 | configs: FlatESLintConfig[],
154 | options?: DefineFlatConfigOptions,
155 | ) => FlatESLintConfig[]
156 | ```
157 |
158 | 选项类型声明:
159 |
160 | ```ts
161 | interface DefineFlatConfigOptions {
162 | /**
163 | * 指定用于加载 `.prettierrc` 配置的工作目录。
164 | *
165 | * 配置文件应为 JSON 格式。
166 | *
167 | * @default process.cwd()
168 | */
169 | cwd?: string
170 |
171 | /**
172 | * 如果 `prettierEnabled` 设为 `false`,则所有与 Prettier 相关的规则和配置都将被忽略, 即使提供了
173 | * `prettierRules` 也不会生效。
174 | *
175 | * @default true
176 | */
177 | prettierEnabled?: boolean
178 |
179 | /**
180 | * 默认情况下,会从当前工作目录读取 `.prettierrc`,并且 `.prettierrc` 文件必须是 JSON 格式。
181 | *
182 | * 如果配置文件不是 JSON 格式,或者使用了不同的文件名,可以将其转换为 JSON 规则后传入。
183 | *
184 | * 读取自定义配置后,会与默认的 ESLint 规则合并。
185 | *
186 | * @see https://prettier.io/docs/configuration.html
187 | */
188 | prettierRules?: PartialPrettierExtendedOptions
189 |
190 | /**
191 | * Tailwind CSS 规则默认启用。如果它们影响了项目,可以通过该选项禁用。
192 | *
193 | * @default true
194 | */
195 | tailwindcssEnabled?: boolean
196 |
197 | /**
198 | * 如果需要覆盖 Tailwind CSS 配置,可以传入相应的选项。
199 | *
200 | * 如果想要合并配置,可以导入 `defaultTailwindcssSettings`,手动合并后再传入。
201 | *
202 | * 如果传入空对象 `{}`,则会使用默认设置。
203 | *
204 | * @see https://github.com/francoismassart/eslint-plugin-tailwindcss/tree/v3.18.2
205 | */
206 | tailwindcssSettings?: TailwindcssSettings
207 | }
208 | ```
209 |
210 | ### createGetConfigNameFactory
211 |
212 | `createGetConfigNameFactory` 是一个灵活的工具函数,用于生成 ESLint 配置命名工具。它可以快速拼接配置名称,确保命名空间一致,并便于组织和管理复杂的规则集。
213 |
214 | API 类型声明:
215 |
216 | ```ts
217 | /**
218 | * 一个灵活的工具函数,用于生成 ESLint 配置命名工具。 它可以快速拼接配置名称,确保命名空间一致,并便于组织和管理复杂的规则集。
219 | *
220 | * @param prefix - 表示配置名称前缀的字符串。
221 | * @returns 一个函数,该函数会将提供的名称片段与指定的前缀拼接在一起。
222 | */
223 | declare const createGetConfigNameFactory: (
224 | prefix: string,
225 | ) => (...names: string[]) => string
226 | ```
227 |
228 | 使用示例:
229 |
230 | ```ts
231 | import {
232 | createGetConfigNameFactory,
233 | defineFlatConfig,
234 | } from '@bassist/eslint-config'
235 |
236 | const getConfigName = createGetConfigNameFactory('my-prefix')
237 |
238 | export default defineFlatConfig([
239 | {
240 | name: getConfigName('ignore'), // --> `my-prefix/ignore`
241 | ignores: ['**/dist/**', '**/.build/**', '**/CHANGELOG.md'],
242 | },
243 | ])
244 | ```
245 |
246 | 为什么要使用它?
247 |
248 | - 一致性:强制执行清晰统一的配置命名模式。
249 | - 灵活性:允许为不同项目或范围自定义前缀。
250 | - 简化管理:便于组织和浏览大型 ESLint 配置。
251 |
252 | 这个工具在构建可复用的 ESLint 配置或维护复杂项目的规则集时尤其有用。
253 |
254 | ## 📦 导出的配置
255 |
256 | 这些是一些常用的配置,如果有额外需求,欢迎提交 PR!
257 |
258 | ### 语言支持
259 |
260 | - [JavaScript](https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/configs/javascript.ts)
261 | - [TypeScript](https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/configs/typescript.ts)
262 | - [JSX](https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/configs/jsx.ts)
263 | - [Markdown](https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/configs/markdown.ts)
264 |
265 | #### 框架支持
266 |
267 | - [Next.js](https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/configs/next.ts)
268 | - [React](https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/configs/react.ts)
269 | - [Vue (v2 and v3)](https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/configs/vue.ts)
270 |
271 | #### 格式化工具
272 |
273 | 格式化规则默认启用,不会单独导出。如需自定义配置,请通过 [defineFlatConfig API](#defineflatconfig) 的 `options` 传入。
274 |
275 | - [Prettier](https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/private-configs/prettier.ts) :
276 | - 默认会读取 `.prettierrc` 和 `.prettierignore` 的内容,并添加到 ESLint 规则中。
277 | - 如果预期的配置文件不存在,则会使用 [内置的 Prettier](https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/shared/prettier-config.mjs) 规则作为兜底规则。
278 | - 非以上配置文件并且不喜欢默认规则时,可以通过 [defineFlatConfig](#defineflatconfig) 的 `options.prettierRules` 将完整配置传递进来优先作为 ESLint 规则使用
279 | - [Tailwind CSS](https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/private-configs/tailwindcss.ts) :
280 | - 默认会将 `tailwind.config.js` 作为 Tailwind CSS 配置文件传入。
281 | - 非默认文件或者需要更改规则,可通过 `options.tailwindcssSettings` 传递
282 |
283 | #### 其它
284 |
285 | - [Node.js](https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/configs/node.ts)
286 | - [Imports](https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/configs/imports.ts)
287 | - [Regexp](https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/configs/regexp.ts)
288 | - [Unicorn](https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/src/configs/unicorn.ts)
289 |
290 | ## 📚 迁移指南
291 |
292 | - 扁平化配置(Flat Configs)不支持 ESLint 8.x 以下的版本。
293 | - `--ext` CLI 选项已被移除 ([#16991](https://github.com/eslint/eslint/issues/16991)) 。
294 |
295 | ## 📝 发布日志
296 |
297 | 详细更新内容请参考 [CHANGELOG](https://github.com/chengpeiquan/bassist/blob/main/packages/eslint-config/CHANGELOG.md) 。
298 |
299 | ## 📜 License
300 |
301 | MIT License © 2023-PRESENT [chengpeiquan](https://github.com/chengpeiquan)
302 |
--------------------------------------------------------------------------------
/packages/utils/src/data.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The actual type of the data
3 | *
4 | * @category Data
5 | */
6 | export type DataType =
7 | | 'Array'
8 | | 'ArrayBuffer'
9 | | 'AsyncFunction'
10 | | 'BigInt'
11 | | 'Blob'
12 | | 'Boolean'
13 | | 'Date'
14 | | 'Error'
15 | | 'File'
16 | | 'Function'
17 | | 'Map'
18 | | 'Math'
19 | | 'Null'
20 | | 'Number'
21 | | 'Object'
22 | | 'Promise'
23 | | 'Set'
24 | | 'String'
25 | | 'Symbol'
26 | | 'RegExp'
27 | | 'Undefined'
28 | | 'WeakMap'
29 | | 'WeakSet'
30 |
31 | /**
32 | * Get the real data type
33 | *
34 | * Solve the judgment error of the value wrapped in an Object e.g. Object(1n),
35 | * Object(1)
36 | *
37 | * @category Data
38 | */
39 | export function getDataType(target: any) {
40 | return Object.prototype.toString.call(target).slice(8, -1) as DataType
41 | }
42 |
43 | /**
44 | * Wrapper for `Array.isArray`, determine whether the data is Array
45 | *
46 | * @category Data
47 | */
48 | export function isArray(value: unknown): value is any[] {
49 | return Array.isArray(value)
50 | }
51 |
52 | /**
53 | * Determine whether the data is ArrayBuffer
54 | *
55 | * @category Data
56 | */
57 | export function isArrayBuffer(value: unknown): value is ArrayBuffer {
58 | return getDataType(value) === 'ArrayBuffer'
59 | }
60 |
61 | /**
62 | * Determine whether the data is AsyncFunction
63 | *
64 | * @category Data
65 | */
66 | export function isAsyncFunction(
67 | value: unknown,
68 | ): value is (...args: any) => Promise {
69 | if (!isFunction(value)) return false
70 | return getDataType(value) === 'AsyncFunction'
71 | }
72 |
73 | /**
74 | * Determine whether the data is BigInt
75 | *
76 | * @category Data
77 | */
78 | export function isBigInt(value: unknown): value is bigint {
79 | return getDataType(value) === 'BigInt'
80 | }
81 |
82 | /**
83 | * Determine whether the data is Blob
84 | *
85 | * @category Data
86 | */
87 | export function isBlob(value: unknown): value is Blob {
88 | return getDataType(value) === 'Blob'
89 | }
90 |
91 | /**
92 | * Determine whether the data is Boolean
93 | *
94 | * @category Data
95 | */
96 | export function isBoolean(value: unknown): value is boolean {
97 | return getDataType(value) === 'Boolean'
98 | }
99 |
100 | /**
101 | * Determine whether the data is Date
102 | *
103 | * @category Data
104 | */
105 | export function isDate(value: unknown): value is Date {
106 | return getDataType(value) === 'Date'
107 | }
108 |
109 | /**
110 | * Determine whether the data is Error
111 | *
112 | * @category Data
113 | */
114 | export function isError(value: unknown): value is Error {
115 | return getDataType(value) === 'Error'
116 | }
117 |
118 | /**
119 | * Determine whether the data is Even
120 | *
121 | * @category Data
122 | */
123 | export function isEven(value: unknown): value is number {
124 | if (!isInteger(value)) return false
125 | return value % 2 === 0
126 | }
127 |
128 | /**
129 | * Determine whether the data is File
130 | *
131 | * @category Data
132 | */
133 | export function isFile(value: unknown): value is File {
134 | return getDataType(value) === 'File'
135 | }
136 |
137 | /**
138 | * Wrapper for `Number.isFinite`, determine whether the data is finite
139 | *
140 | * @category Data
141 | */
142 | export function isFinite(value: unknown): value is number {
143 | return Number.isFinite(value)
144 | }
145 |
146 | /**
147 | * Determine whether the data is Function
148 | *
149 | * @category Data
150 | */
151 | export function isFunction(value: unknown): value is (...args: any) => any {
152 | return typeof value === 'function'
153 | }
154 |
155 | /**
156 | * Wrapper for `Number.isInteger`, determine whether the data is Integer
157 | *
158 | * @category Data
159 | */
160 | export function isInteger(value: unknown): value is number {
161 | return Number.isInteger(value)
162 | }
163 |
164 | /**
165 | * Determine whether the data is Map
166 | *
167 | * @category Data
168 | */
169 | export function isMap(value: unknown): value is Map {
170 | return getDataType(value) === 'Map'
171 | }
172 |
173 | /**
174 | * Determine whether the data is Math
175 | *
176 | * @category Data
177 | */
178 | export function isMath(value: unknown): value is Math {
179 | return getDataType(value) === 'Math'
180 | }
181 |
182 | /**
183 | * Wrapper for `Number.isNaN`, determine whether the data is NaN
184 | *
185 | * @category Data
186 | */
187 | export function isNaN(value: unknown): value is number {
188 | return Number.isNaN(value)
189 | }
190 |
191 | /**
192 | * Determine whether the data is Null
193 | *
194 | * @category Data
195 | */
196 | export function isNull(value: unknown): value is null {
197 | return getDataType(value) === 'Null'
198 | }
199 |
200 | /**
201 | * Determine whether the data is Number
202 | *
203 | * @category Data
204 | */
205 | export function isNumber(value: unknown): value is number {
206 | return getDataType(value) === 'Number'
207 | }
208 |
209 | /**
210 | * Determine whether the data is Odd
211 | *
212 | * @category Data
213 | */
214 | export function isOdd(value: unknown): value is number {
215 | if (!isInteger(value)) return false
216 | return value % 2 !== 0
217 | }
218 |
219 | /**
220 | * Determine whether the data is Object
221 | *
222 | * @category Data
223 | */
224 | export function isObject(value: unknown): value is Record {
225 | return getDataType(value) === 'Object'
226 | }
227 |
228 | /**
229 | * Determine whether the data is Promise
230 | *
231 | * @category Data
232 | */
233 | export function isPromise(value: unknown): value is Promise {
234 | return getDataType(value) === 'Promise'
235 | }
236 |
237 | /**
238 | * Wrapper for `Number.isSafeInteger`, determine whether the data is Safe
239 | * Integer
240 | *
241 | * @category Data
242 | */
243 | export function isSafeInteger(value: unknown): value is number {
244 | return Number.isSafeInteger(value)
245 | }
246 |
247 | /**
248 | * Determine whether the data is Promise
249 | *
250 | * @category Data
251 | */
252 | export function isSet(value: unknown): value is Set {
253 | return getDataType(value) === 'Set'
254 | }
255 |
256 | /**
257 | * Determine whether the data is String
258 | *
259 | * @category Data
260 | */
261 | export function isString(value: unknown): value is string {
262 | return getDataType(value) === 'String'
263 | }
264 |
265 | /**
266 | * Determine whether the data is Symbol
267 | *
268 | * @category Data
269 | */
270 | export function isSymbol(value: unknown): value is symbol {
271 | return getDataType(value) === 'Symbol'
272 | }
273 |
274 | /**
275 | * Determine whether the data is Undefined
276 | *
277 | * @category Data
278 | */
279 | export function isUndefined(value: unknown): value is undefined {
280 | return getDataType(value) === 'Undefined'
281 | }
282 |
283 | /**
284 | * Determine whether the data is WeakMap
285 | *
286 | * @category Data
287 | */
288 | export function isWeakMap(value: unknown): value is WeakMap {
289 | return getDataType(value) === 'WeakMap'
290 | }
291 |
292 | /**
293 | * Determine whether the data is WeakSet
294 | *
295 | * @category Data
296 | */
297 | export function isWeakSet(value: unknown): value is WeakSet {
298 | return getDataType(value) === 'WeakSet'
299 | }
300 |
301 | /**
302 | * Determine whether the data is RegExp
303 | *
304 | * @category Data
305 | */
306 | export function isRegExp(value: unknown): value is RegExp {
307 | return getDataType(value) === 'RegExp'
308 | }
309 |
310 | /**
311 | * Determine whether the specified key exists on the object
312 | *
313 | * @category Data
314 | */
315 | export function hasKey(obj: T, key: K): key is K {
316 | if (!isObject(obj)) return false
317 | return Object.prototype.hasOwnProperty.call(obj, key)
318 | }
319 |
320 | export const hasOwnProperty = hasKey
321 |
322 | /**
323 | * String to byte stream
324 | *
325 | * @category Data
326 | */
327 | export function getBytes(value: string) {
328 | const encoder = new TextEncoder()
329 | const bytes = encoder.encode(value)
330 | return bytes
331 | }
332 |
333 | /**
334 | * @category Data
335 | */
336 | export interface InRangeOptions {
337 | num: number
338 | min: number
339 | max: number
340 | includeMin?: boolean
341 | includeMax?: boolean
342 | }
343 |
344 | /**
345 | * Checks if a number is between minimum and maximum
346 | *
347 | * @category Data
348 | */
349 | export function inRange({
350 | num,
351 | min,
352 | max,
353 | includeMin = true,
354 | includeMax = true,
355 | }: InRangeOptions) {
356 | if (!isNumber(num) || !isNumber(min) || !isNumber(max)) return false
357 |
358 | const isMin = includeMin
359 | ? num >= Math.min(min, max)
360 | : num > Math.min(min, max)
361 |
362 | const isMax = includeMax
363 | ? num <= Math.max(min, max)
364 | : num < Math.max(min, max)
365 |
366 | return isMin && isMax
367 | }
368 |
369 | /**
370 | * No operation function type
371 | *
372 | * @category Data
373 | */
374 | export type NoOperationFunction = (...args: any) => void
375 |
376 | /**
377 | * A no operation function
378 | *
379 | * @category Data
380 | */
381 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
382 | export const noop: NoOperationFunction = (..._args) => void 0
383 |
384 | /**
385 | * Promisify No operation function type
386 | *
387 | * @category Data
388 | */
389 | export type PromisifyNoOperationFunction = (...args: any) => Promise
390 |
391 | /**
392 | * A promisify no operation function
393 | *
394 | * @category Data
395 | */
396 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
397 | export const pnoop: PromisifyNoOperationFunction = (..._args) =>
398 | new Promise((r) => r())
399 |
--------------------------------------------------------------------------------
/packages/utils/src/format.ts:
--------------------------------------------------------------------------------
1 | import { hasKey, isArray, isObject, isString } from './data'
2 |
3 | /**
4 | * Extract numbers from text
5 | *
6 | * @category Format
7 | * @param text - Text to be processed
8 | * @param startsWithZero - Preserve the 0-starting format like `002`
9 | */
10 | export function extractNumber(text: string | number, startsWithZero = false) {
11 | text = text ? String(text) : ''
12 | text = text.replace(/[^\d]/g, '')
13 |
14 | if (text && !startsWithZero) {
15 | text = parseInt(text)
16 | }
17 |
18 | return String(text)
19 | }
20 |
21 | /**
22 | * Format amount with two decimal places
23 | *
24 | * @category Format
25 | * @param amount - Amount to be processed
26 | */
27 | export function formatAmount(amount: string | number) {
28 | amount = String(amount)
29 | if (!amount) return '0.00'
30 |
31 | const arr = amount.split('.')
32 | const integer = arr[0]
33 | const decimal = arr[1]
34 |
35 | // no decimals
36 | if (arr.length === 1) {
37 | return `${integer}.00`
38 | }
39 |
40 | // 1 decimal place
41 | if (decimal.length === 1) {
42 | return `${amount}0`
43 | }
44 |
45 | // Uniform returns two decimal places
46 | return Number(amount).toFixed(2)
47 | }
48 |
49 | /**
50 | * Add ellipses to words that are out of length
51 | *
52 | * @category Format
53 | * @param word - The sentence to be processed
54 | * @param limit - The upper limit
55 | * @returns The processed word
56 | */
57 | export function ellipsis(word: string, limit: number): string {
58 | return String(word).length > limit
59 | ? String(word).slice(0, limit) + ' ...'
60 | : String(word)
61 | }
62 |
63 | /**
64 | * Capitalize the first letter
65 | *
66 | * @category Format
67 | */
68 | export function capitalize([first, ...rest]: string) {
69 | if (!first) return ''
70 | return first.toUpperCase() + rest.join('')
71 | }
72 |
73 | /**
74 | * Formatted in `kebab-case` style
75 | *
76 | * @category Format
77 | */
78 | export function kebabCase(word: string) {
79 | if (!word) return ''
80 | return word
81 | .replace(/([A-Z])/g, ' $1')
82 | .trim()
83 | .split(' ')
84 | .join('-')
85 | .replace(/_/g, '-')
86 | .toLowerCase()
87 | }
88 |
89 | /**
90 | * Formatted in `camelCase` style
91 | *
92 | * @category Format
93 | */
94 | export function camelCase([first, ...rest]: string) {
95 | if (!first) return ''
96 | const word = first.toLowerCase() + rest.join('')
97 | return word.replace(/[-_](\w)/g, (_, c) => (c ? c.toUpperCase() : ''))
98 | }
99 |
100 | /**
101 | * Formatted in `PascalCase` style
102 | *
103 | * @category Format
104 | */
105 | export function pascalCase(word: string) {
106 | if (!word) return ''
107 | return capitalize(camelCase(word))
108 | }
109 |
110 | /**
111 | * Escaping special characters for regular expressions
112 | *
113 | * @category Format
114 | * @copyright lodash.escaperegexp
115 | */
116 | export function escapeRegExp(name: string) {
117 | const reRegExpChar = /[\\^$.*+?()[\]{}|]/g
118 | const reHasRegExpChar = RegExp(reRegExpChar.source)
119 | return name && reHasRegExpChar.test(name)
120 | ? name.replace(reRegExpChar, '\\$&')
121 | : name
122 | }
123 |
124 | /**
125 | * Sort the keys of an object
126 | *
127 | * @category Format
128 | */
129 | export function sortKeys(target: any): any {
130 | if (!Array.isArray(target) && !isObject(target)) {
131 | return target
132 | }
133 |
134 | if (Array.isArray(target)) {
135 | return target.map((i) => sortKeys(i))
136 | }
137 |
138 | const keys = Object.keys(target).sort()
139 | const newObj: Record = {}
140 | keys.forEach((k) => {
141 | newObj[k] = sortKeys(target[k])
142 | })
143 | return newObj
144 | }
145 |
146 | /**
147 | * @category Format
148 | */
149 | interface UniqueOptions {
150 | /**
151 | * The key used to determine if there are duplicate values
152 | */
153 | primaryKey: keyof T
154 |
155 | /**
156 | * He original data list
157 | */
158 | list: T[]
159 | }
160 |
161 | /**
162 | * Deduplicate an array containing objects
163 | *
164 | * @category Format
165 | */
166 | export function unique({ primaryKey, list }: UniqueOptions): T[] {
167 | // Use the value as the key and store it in the dictionary
168 | const dict: Map = new Map()
169 | list.forEach((obj) => {
170 | const value = String(obj[primaryKey])
171 | if (dict.has(value)) return
172 | dict.set(value, obj)
173 | })
174 |
175 | // Return from dictionary to array
176 | const uniqueList: T[] = []
177 | dict.forEach((value) => {
178 | uniqueList.push(value)
179 | })
180 |
181 | return uniqueList
182 | }
183 |
184 | /**
185 | * Exclude specified fields from the object
186 | *
187 | * @category Format
188 | * @param object - An object as data source
189 | * @param fields - Field names to exclude
190 | * @returns A processed new object
191 | * @tips Only handle first-level fields
192 | */
193 | export function excludeFields(object: Record, fields: string[]) {
194 | if (!isObject) return object
195 |
196 | const newObject: Record = {}
197 | for (const key in object) {
198 | if (hasKey(object, key) && !fields.includes(key)) {
199 | newObject[key] = object[key]
200 | }
201 | }
202 | return newObject
203 | }
204 |
205 | /**
206 | * Format the time as `yyyy-MM-dd HH:mm:ss`
207 | *
208 | * Note: If the date separator is `-`, it will be parsed as a standard ISO 8601
209 | * formatted date string (e.g. 2023-01-01)
210 | *
211 | * If the date separator is `/`, it will be parsed according to the non-standard
212 | * date string format (e.g. 2023/01/01 )
213 | *
214 | * This will result in inconsistent processing, please be aware of such cases.
215 | * e.g. `+new Date('2023/01/01') !== +new Date('2023-01-01')`
216 | *
217 | * @category Format
218 | * @param time - A timestamp or a date object
219 | * @param dateOnly - If `true` , only returns `yyyy-MM-dd`
220 | */
221 | export function formatTime(time: number | Date, dateOnly?: boolean) {
222 | const date = new Date(time)
223 |
224 | const year = date.getFullYear()
225 | const month = ('0' + (date.getMonth() + 1)).slice(-2)
226 | const day = ('0' + date.getDate()).slice(-2)
227 | const hours = ('0' + date.getHours()).slice(-2)
228 | const minutes = ('0' + date.getMinutes()).slice(-2)
229 | const seconds = ('0' + date.getSeconds()).slice(-2)
230 |
231 | const formattedDate = dateOnly
232 | ? `${year}-${month}-${day}`
233 | : `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
234 |
235 | return formattedDate
236 | }
237 |
238 | /**
239 | * @category Format
240 | */
241 | interface FormatDurationUnit {
242 | days: string
243 | hours: string
244 | minutes: string
245 | seconds: string
246 | }
247 |
248 | /**
249 | * Generally used to format the display of two time gaps, such as countdown
250 | *
251 | * @category Format
252 | * @param timestamp - Timestamp with two time gaps
253 | * @param units - Time units, different languages can be passed in when i18n is
254 | * needed, the default is Simplified Chinese
255 | */
256 | export function formatDuration(
257 | timestamp: number,
258 | units: FormatDurationUnit = {
259 | days: '天',
260 | hours: '小时',
261 | minutes: '分钟',
262 | seconds: '秒',
263 | },
264 | ) {
265 | // Convert timestamp to seconds
266 | const totalSeconds = Math.floor(timestamp / 1000)
267 |
268 | const days = Math.floor(totalSeconds / (3600 * 24))
269 | const hours = Math.floor((totalSeconds % (3600 * 24)) / 3600)
270 | const minutes = Math.floor((totalSeconds % 3600) / 60)
271 | const seconds = Math.floor(totalSeconds % 60)
272 |
273 | // Build a formatted string
274 | const parts: string[] = []
275 | if (days > 0) {
276 | parts.push(`${days} ${units.days}`)
277 | }
278 | if (hours > 0) {
279 | parts.push(`${hours} ${units.hours}`)
280 | }
281 | if (minutes > 0) {
282 | parts.push(`${minutes} ${units.minutes}`)
283 | }
284 | if (seconds > 0) {
285 | parts.push(`${seconds} ${units.seconds}`)
286 | }
287 |
288 | // Return the formatted result
289 | return parts.length === 0 ? `0 ${units.seconds}` : parts.join(' ')
290 | }
291 |
292 | /**
293 | * Remove HTML tags
294 | *
295 | * @category Format
296 | * @param content HTML Codes
297 | */
298 | export function removeHtmlTags(content: string) {
299 | if (!isString(content)) return ''
300 | return content.replace(/<[^<>]+>/g, '')
301 | }
302 |
303 | /**
304 | * Remove HTML tags and escape sequence
305 | *
306 | * @category Format
307 | * @param content HTML Codes
308 | */
309 | export function html2text(content: string) {
310 | if (!isString(content)) return ''
311 | return removeHtmlTags(content)
312 | .replace(/ /g, ' ')
313 | .replace(/</g, '<')
314 | .replace(/>/g, '>')
315 | .replace(/&/g, '&')
316 | .replace(/"/g, '"')
317 | .replace(/'/g, '`')
318 | .replace(/¢/g, '¢')
319 | .replace(/£/g, '£')
320 | .replace(/¥/g, '¥')
321 | .replace(/€/g, '€')
322 | .replace(/§/g, '§')
323 | .replace(/©/g, '©')
324 | .replace(/®/g, '®')
325 | .replace(/™/g, '™')
326 | .replace(/×/g, '×')
327 | .replace(/×/g, '×')
328 | .replace(/÷/g, '÷')
329 | .replace(/·/g, '·')
330 | .replace(/—/g, '—')
331 | }
332 |
333 | /**
334 | * Make sure the data you get is an array
335 | *
336 | * @category Format
337 | */
338 | export function toArray(value?: T | T[]): T[] {
339 | value = value ?? []
340 | return isArray(value) ? value : [value]
341 | }
342 |
343 | /**
344 | * Ensure prefix of a string
345 | *
346 | * @category Format
347 | */
348 | export function ensurePrefix(prefix: string, str: string) {
349 | if (!str.startsWith(prefix)) return prefix + str
350 | return str
351 | }
352 |
353 | /**
354 | * Ensure suffix of a string
355 | *
356 | * @category Format
357 | */
358 | export function ensureSuffix(suffix: string, str: string) {
359 | if (!str.endsWith(suffix)) return str + suffix
360 | return str
361 | }
362 |
--------------------------------------------------------------------------------