first commit

This commit is contained in:
2026-01-30 14:25:12 +08:00
commit 8dd8d2668a
899 changed files with 90844 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
#!/usr/bin/env tsx
import './src/index.ts';

View File

@@ -0,0 +1,27 @@
{
"name": "@sa/scripts",
"version": "1.3.8",
"bin": {
"sa": "./bin.ts"
},
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": ["./src/*"]
}
},
"devDependencies": {
"@soybeanjs/changelog": "0.3.24",
"bumpp": "9.7.1",
"c12": "2.0.1",
"cac": "6.7.14",
"consola": "3.2.3",
"enquirer": "2.4.1",
"execa": "9.4.1",
"kolorist": "1.8.0",
"npm-check-updates": "17.1.4",
"rimraf": "6.0.1"
}
}

View File

@@ -0,0 +1,10 @@
import { generateChangelog, generateTotalChangelog } from '@soybeanjs/changelog';
import type { ChangelogOption } from '@soybeanjs/changelog';
export async function genChangelog(options?: Partial<ChangelogOption>, total = false) {
if (total) {
await generateTotalChangelog(options);
} else {
await generateChangelog(options);
}
}

View File

@@ -0,0 +1,5 @@
import { rimraf } from 'rimraf';
export async function cleanup(paths: string[]) {
await rimraf(paths, { glob: true });
}

View File

@@ -0,0 +1,84 @@
import path from 'node:path';
import { readFileSync } from 'node:fs';
import { prompt } from 'enquirer';
import { execCommand } from '../shared';
import { locales } from '../locales';
import type { Lang } from '../locales';
interface PromptObject {
types: string;
scopes: string;
description: string;
}
/**
* Git commit with Conventional Commits standard
*
* @param lang
*/
export async function gitCommit(lang: Lang = 'en-us') {
const { gitCommitMessages, gitCommitTypes, gitCommitScopes } = locales[lang];
const typesChoices = gitCommitTypes.map(([value, msg]) => {
const nameWithSuffix = `${value}:`;
const message = `${nameWithSuffix.padEnd(12)}${msg}`;
return {
name: value,
message
};
});
const scopesChoices = gitCommitScopes.map(([value, msg]) => ({
name: value,
message: `${value.padEnd(30)} (${msg})`
}));
const result = await prompt<PromptObject>([
{
name: 'types',
type: 'select',
message: gitCommitMessages.types,
choices: typesChoices
},
{
name: 'scopes',
type: 'select',
message: gitCommitMessages.scopes,
choices: scopesChoices
},
{
name: 'description',
type: 'text',
message: gitCommitMessages.description
}
]);
const breaking = result.description.startsWith('!') ? '!' : '';
const description = result.description.replace(/^!/, '').trim();
const commitMsg = `${result.types}(${result.scopes})${breaking}: ${description}`;
await execCommand('git', ['commit', '-m', commitMsg], { stdio: 'inherit' });
}
/** Git commit message verify */
export async function gitCommitVerify(lang: Lang = 'en-us', ignores: RegExp[] = []) {
const gitPath = await execCommand('git', ['rev-parse', '--show-toplevel']);
const gitMsgPath = path.join(gitPath, '.git', 'COMMIT_EDITMSG');
const commitMsg = readFileSync(gitMsgPath, 'utf8').trim();
if (ignores.some(regExp => regExp.test(commitMsg))) return;
const REG_EXP = /(?<type>[a-z]+)(?:\((?<scope>.+)\))?(?<breaking>!)?: (?<description>.+)/i;
if (!REG_EXP.test(commitMsg)) {
const errorMsg = locales[lang].gitCommitVerify;
throw new Error(errorMsg);
}
}

View File

@@ -0,0 +1,6 @@
export * from './git-commit';
export * from './cleanup';
export * from './update-pkg';
export * from './changelog';
export * from './release';
export * from './router';

View File

@@ -0,0 +1,12 @@
import { versionBump } from 'bumpp';
export async function release(execute = 'pnpm sa changelog', push = true) {
await versionBump({
files: ['**/package.json', '!**/node_modules'],
execute,
all: true,
tag: true,
commit: 'chore(projects): release v%s',
push
});
}

View File

@@ -0,0 +1,90 @@
import process from 'node:process';
import path from 'node:path';
import { writeFile } from 'node:fs/promises';
import { existsSync, mkdirSync } from 'node:fs';
import { prompt } from 'enquirer';
import { green, red } from 'kolorist';
interface PromptObject {
routeName: string;
addRouteParams: boolean;
routeParams: string;
}
/** generate route */
export async function generateRoute() {
const result = await prompt<PromptObject>([
{
name: 'routeName',
type: 'text',
message: 'please enter route name',
initial: 'demo-route_child'
},
{
name: 'addRouteParams',
type: 'confirm',
message: 'add route params?',
initial: false
}
]);
if (result.addRouteParams) {
const answers = await prompt<PromptObject>({
name: 'routeParams',
type: 'text',
message: 'please enter route params',
initial: 'id'
});
Object.assign(result, answers);
}
const PAGE_DIR_NAME_PATTERN = /^[\w-]+[0-9a-zA-Z]+$/;
if (!PAGE_DIR_NAME_PATTERN.test(result.routeName)) {
throw new Error(`${red('route name is invalid, it only allow letters, numbers, "-" or "_"')}.
For example:
(1) one level route: ${green('demo-route')}
(2) two level route: ${green('demo-route_child')}
(3) multi level route: ${green('demo-route_child_child')}
(4) group route: ${green('_ignore_demo-route')}'
`);
}
const PARAM_REG = /^\w+$/g;
if (result.routeParams && !PARAM_REG.test(result.routeParams)) {
throw new Error(red('route params is invalid, it only allow letters, numbers or "_".'));
}
const cwd = process.cwd();
const [dir, ...rest] = result.routeName.split('_') as string[];
let routeDir = path.join(cwd, 'src', 'views', dir);
if (rest.length) {
routeDir = path.join(routeDir, rest.join('_'));
}
if (!existsSync(routeDir)) {
mkdirSync(routeDir, { recursive: true });
} else {
throw new Error(red('route already exists'));
}
const fileName = result.routeParams ? `[${result.routeParams}].vue` : 'index.vue';
const vueTemplate = `<script setup lang="ts"></script>
<template>
<div>${result.routeName}</div>
</template>
<style scoped></style>
`;
const filePath = path.join(routeDir, fileName);
await writeFile(filePath, vueTemplate);
}

View File

@@ -0,0 +1,5 @@
import { execCommand } from '../shared';
export async function updatePkg(args: string[] = ['--deep', '-u']) {
execCommand('npx', ['ncu', ...args], { stdio: 'inherit' });
}

View File

@@ -0,0 +1,39 @@
import process from 'node:process';
import { loadConfig } from 'c12';
import type { CliOption } from '../types';
const defaultOptions: CliOption = {
cwd: process.cwd(),
cleanupDirs: [
'**/dist',
'**/package-lock.json',
'**/yarn.lock',
'**/pnpm-lock.yaml',
'**/node_modules',
'!node_modules/**'
],
ncuCommandArgs: ['--deep', '-u'],
changelogOptions: {},
gitCommitVerifyIgnores: [
/^((Merge pull request)|(Merge (.*?) into (.*?)|(Merge branch (.*?)))(?:\r?\n)*$)/m,
/^(Merge tag (.*?))(?:\r?\n)*$/m,
/^(R|r)evert (.*)/,
/^(amend|fixup|squash)!/,
/^(Merged (.*?)(in|into) (.*)|Merged PR (.*): (.*))/,
/^Merge remote-tracking branch(\s*)(.*)/,
/^Automatic merge(.*)/,
/^Auto-merged (.*?) into (.*)/
]
};
export async function loadCliOptions(overrides?: Partial<CliOption>, cwd = process.cwd()) {
const { config } = await loadConfig<Partial<CliOption>>({
name: 'soybean',
defaults: defaultOptions,
overrides,
cwd,
packageJson: true
});
return config as CliOption;
}

View File

@@ -0,0 +1,109 @@
import cac from 'cac';
import { blue, lightGreen } from 'kolorist';
import { version } from '../package.json';
import { cleanup, genChangelog, generateRoute, gitCommit, gitCommitVerify, release, updatePkg } from './commands';
import { loadCliOptions } from './config';
import type { Lang } from './locales';
type Command = 'cleanup' | 'update-pkg' | 'git-commit' | 'git-commit-verify' | 'changelog' | 'release' | 'gen-route';
type CommandAction<A extends object> = (args?: A) => Promise<void> | void;
type CommandWithAction<A extends object = object> = Record<Command, { desc: string; action: CommandAction<A> }>;
interface CommandArg {
/** Execute additional command after bumping and before git commit. Defaults to 'pnpm sa changelog' */
execute?: string;
/** Indicates whether to push the git commit and tag. Defaults to true */
push?: boolean;
/** Generate changelog by total tags */
total?: boolean;
/**
* The glob pattern of dirs to clean up
*
* If not set, it will use the default value
*
* Multiple values use "," to separate them
*/
cleanupDir?: string;
/**
* display lang of cli
*
* @default 'en-us'
*/
lang?: Lang;
}
export async function setupCli() {
const cliOptions = await loadCliOptions();
const cli = cac(blue('soybean-admin'));
cli
.version(lightGreen(version))
.option(
'-e, --execute [command]',
"Execute additional command after bumping and before git commit. Defaults to 'npx soy changelog'"
)
.option('-p, --push', 'Indicates whether to push the git commit and tag')
.option('-t, --total', 'Generate changelog by total tags')
.option(
'-c, --cleanupDir <dir>',
'The glob pattern of dirs to cleanup, If not set, it will use the default value, Multiple values use "," to separate them'
)
.option('-l, --lang <lang>', 'display lang of cli', { default: 'en-us', type: [String] })
.help();
const commands: CommandWithAction<CommandArg> = {
cleanup: {
desc: 'delete dirs: node_modules, dist, etc.',
action: async () => {
await cleanup(cliOptions.cleanupDirs);
}
},
'update-pkg': {
desc: 'update package.json dependencies versions',
action: async () => {
await updatePkg(cliOptions.ncuCommandArgs);
}
},
'git-commit': {
desc: 'git commit, generate commit message which match Conventional Commits standard',
action: async args => {
await gitCommit(args?.lang);
}
},
'git-commit-verify': {
desc: 'verify git commit message, make sure it match Conventional Commits standard',
action: async args => {
await gitCommitVerify(args?.lang, cliOptions.gitCommitVerifyIgnores);
}
},
changelog: {
desc: 'generate changelog',
action: async args => {
await genChangelog(cliOptions.changelogOptions, args?.total);
}
},
release: {
desc: 'release: update version, generate changelog, commit code',
action: async args => {
await release(args?.execute, args?.push);
}
},
'gen-route': {
desc: 'generate route',
action: async () => {
await generateRoute();
}
}
};
for (const [command, { desc, action }] of Object.entries(commands)) {
cli.command(command, lightGreen(desc)).action(action);
}
cli.parse();
}
setupCli();

View File

@@ -0,0 +1,82 @@
import { bgRed, green, red, yellow } from 'kolorist';
export type Lang = 'zh-cn' | 'en-us';
export const locales = {
'zh-cn': {
gitCommitMessages: {
types: '请选择提交类型',
scopes: '请选择提交范围',
description: `请输入描述信息(${yellow('!')}开头表示破坏性改动`
},
gitCommitTypes: [
['feat', '新功能'],
['feat-wip', '开发中的功能,比如某功能的部分代码'],
['fix', '修复Bug'],
['docs', '只涉及文档更新'],
['typo', '代码或文档勘误,比如错误拼写'],
['style', '修改代码风格,不影响代码含义的变更'],
['refactor', '代码重构,既不修复 bug 也不添加功能的代码变更'],
['perf', '可提高性能的代码更改'],
['optimize', '优化代码质量的代码更改'],
['test', '添加缺失的测试或更正现有测试'],
['build', '影响构建系统或外部依赖项的更改'],
['ci', '对 CI 配置文件和脚本的更改'],
['chore', '没有修改src或测试文件的其他变更'],
['revert', '还原先前的提交']
] as [string, string][],
gitCommitScopes: [
['projects', '项目'],
['packages', '包'],
['components', '组件'],
['hooks', '钩子函数'],
['utils', '工具函数'],
['types', 'TS类型声明'],
['styles', '代码风格'],
['deps', '项目依赖'],
['release', '发布项目新版本'],
['other', '其他的变更']
] as [string, string][],
gitCommitVerify: `${bgRed(' 错误 ')} ${red('git 提交信息必须符合 Conventional Commits 标准!')}\n\n${green(
'推荐使用命令 `pnpm commit` 生成符合 Conventional Commits 标准的提交信息。\n获取有关 Conventional Commits 的更多信息,请访问此链接: https://conventionalcommits.org'
)}`
},
'en-us': {
gitCommitMessages: {
types: 'Please select a type',
scopes: 'Please select a scope',
description: `Please enter a description (add prefix ${yellow('!')} to indicate breaking change)`
},
gitCommitTypes: [
['feat', 'A new feature'],
['feat-wip', 'Features in development, such as partial code for a certain feature'],
['fix', 'A bug fix'],
['docs', 'Documentation only changes'],
['typo', 'Code or document corrections, such as spelling errors'],
['style', 'Changes that do not affect the meaning of the code'],
['refactor', 'A code change that neither fixes a bug nor adds a feature'],
['perf', 'A code change that improves performance'],
['optimize', 'A code change that optimizes code quality'],
['test', 'Adding missing tests or correcting existing tests'],
['build', 'Changes that affect the build system or external dependencies'],
['ci', 'Changes to our CI configuration files and scripts'],
['chore', "Other changes that don't modify src or test files"],
['revert', 'Reverts a previous commit']
] as [string, string][],
gitCommitScopes: [
['projects', 'project'],
['packages', 'packages'],
['components', 'components'],
['hooks', 'hook functions'],
['utils', 'utils functions'],
['types', 'TS declaration'],
['styles', 'style'],
['deps', 'project dependencies'],
['release', 'release project'],
['other', 'other changes']
] as [string, string][],
gitCommitVerify: `${bgRed(' ERROR ')} ${red('git commit message must match the Conventional Commits standard!')}\n\n${green(
'Recommended to use the command `pnpm commit` to generate Conventional Commits compliant commit information.\nGet more info about Conventional Commits, follow this link: https://conventionalcommits.org'
)}`
}
} satisfies Record<Lang, Record<string, unknown>>;

View File

@@ -0,0 +1,7 @@
import type { Options } from 'execa';
export async function execCommand(cmd: string, args: string[], options?: Options) {
const { execa } = await import('execa');
const res = await execa(cmd, args, options);
return (res?.stdout as string)?.trim() || '';
}

View File

@@ -0,0 +1,31 @@
import type { ChangelogOption } from '@soybeanjs/changelog';
export interface CliOption {
/** The project root directory */
cwd: string;
/**
* Cleanup dirs
*
* Glob pattern syntax {@link https://github.com/isaacs/minimatch}
*
* @default
* ```json
* ["** /dist", "** /pnpm-lock.yaml", "** /node_modules", "!node_modules/**"]
* ```
*/
cleanupDirs: string[];
/**
* Npm-check-updates command args
*
* @default ['--deep', '-u']
*/
ncuCommandArgs: string[];
/**
* Options of generate changelog
*
* @link https://github.com/soybeanjs/changelog
*/
changelogOptions: Partial<ChangelogOption>;
/** The ignore pattern list of git commit verify */
gitCommitVerifyIgnores: RegExp[];
}

View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*", "typings/**/*"],
"exclude": ["node_modules", "dist"]
}