初始化项目
This commit is contained in:
3
packages/scripts/bin.ts
Normal file
3
packages/scripts/bin.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import './src/index.ts';
|
||||
27
packages/scripts/package.json
Normal file
27
packages/scripts/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@sa/scripts",
|
||||
"version": "1.0.0",
|
||||
"bin": {
|
||||
"sa": "./bin.ts"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@soybeanjs/changelog": "0.3.22",
|
||||
"bumpp": "9.4.0",
|
||||
"c12": "1.10.0",
|
||||
"cac": "6.7.14",
|
||||
"consola": "3.2.3",
|
||||
"enquirer": "^2.4.1",
|
||||
"execa": "8.0.1",
|
||||
"kolorist": "1.8.0",
|
||||
"npm-check-updates": "16.14.18",
|
||||
"rimraf": "5.0.5"
|
||||
}
|
||||
}
|
||||
10
packages/scripts/src/commands/changelog.ts
Normal file
10
packages/scripts/src/commands/changelog.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
5
packages/scripts/src/commands/cleanup.ts
Normal file
5
packages/scripts/src/commands/cleanup.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { rimraf } from 'rimraf';
|
||||
|
||||
export async function cleanup(paths: string[]) {
|
||||
await rimraf(paths, { glob: true });
|
||||
}
|
||||
86
packages/scripts/src/commands/git-commit.ts
Normal file
86
packages/scripts/src/commands/git-commit.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import path from 'node:path';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { prompt } from 'enquirer';
|
||||
import { bgRed, green, red, yellow } from 'kolorist';
|
||||
import { execCommand } from '../shared';
|
||||
import type { CliOption } from '../types';
|
||||
|
||||
interface PromptObject {
|
||||
types: string;
|
||||
scopes: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Git commit with Conventional Commits standard
|
||||
*
|
||||
* @param gitCommitTypes
|
||||
* @param gitCommitScopes
|
||||
*/
|
||||
export async function gitCommit(
|
||||
gitCommitTypes: CliOption['gitCommitTypes'],
|
||||
gitCommitScopes: CliOption['gitCommitScopes']
|
||||
) {
|
||||
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: 'Please select a type',
|
||||
choices: typesChoices
|
||||
},
|
||||
{
|
||||
name: 'scopes',
|
||||
type: 'select',
|
||||
message: 'Please select a scope',
|
||||
choices: scopesChoices
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'text',
|
||||
message: `Please enter a description (add prefix ${yellow('!')} to indicate breaking change)`
|
||||
}
|
||||
]);
|
||||
|
||||
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() {
|
||||
const gitPath = await execCommand('git', ['rev-parse', '--show-toplevel']);
|
||||
|
||||
const gitMsgPath = path.join(gitPath, '.git', 'COMMIT_EDITMSG');
|
||||
|
||||
const commitMsg = readFileSync(gitMsgPath, 'utf8').trim();
|
||||
|
||||
const REG_EXP = /(?<type>[a-z]+)(?:\((?<scope>.+)\))?(?<breaking>!)?: (?<description>.+)/i;
|
||||
|
||||
if (!REG_EXP.test(commitMsg)) {
|
||||
throw new Error(
|
||||
`${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'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
6
packages/scripts/src/commands/index.ts
Normal file
6
packages/scripts/src/commands/index.ts
Normal 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';
|
||||
12
packages/scripts/src/commands/release.ts
Normal file
12
packages/scripts/src/commands/release.ts
Normal 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
|
||||
});
|
||||
}
|
||||
90
packages/scripts/src/commands/router.ts
Normal file
90
packages/scripts/src/commands/router.ts
Normal 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);
|
||||
}
|
||||
5
packages/scripts/src/commands/update-pkg.ts
Normal file
5
packages/scripts/src/commands/update-pkg.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { execCommand } from '../shared';
|
||||
|
||||
export async function updatePkg(args: string[] = ['--deep', '-u']) {
|
||||
execCommand('npx', ['ncu', ...args], { stdio: 'inherit' });
|
||||
}
|
||||
53
packages/scripts/src/config/index.ts
Normal file
53
packages/scripts/src/config/index.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
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/**'
|
||||
],
|
||||
gitCommitTypes: [
|
||||
['feat', 'A new feature'],
|
||||
['fix', 'A bug fix'],
|
||||
['docs', 'Documentation only changes'],
|
||||
['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'],
|
||||
['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']
|
||||
],
|
||||
gitCommitScopes: [
|
||||
['projects', 'project'],
|
||||
['components', 'components'],
|
||||
['hooks', 'hook functions'],
|
||||
['utils', 'utils functions'],
|
||||
['types', 'TS declaration'],
|
||||
['styles', 'style'],
|
||||
['deps', 'project dependencies'],
|
||||
['release', 'release project'],
|
||||
['other', 'other changes']
|
||||
],
|
||||
ncuCommandArgs: ['--deep', '-u'],
|
||||
changelogOptions: {}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
101
packages/scripts/src/index.ts
Normal file
101
packages/scripts/src/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
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';
|
||||
|
||||
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 cleanup
|
||||
*
|
||||
* If not set, it will use the default value
|
||||
*
|
||||
* Multiple values use "," to separate them
|
||||
*/
|
||||
cleanupDir?: string;
|
||||
}
|
||||
|
||||
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'
|
||||
)
|
||||
.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 () => {
|
||||
await gitCommit(cliOptions.gitCommitTypes, cliOptions.gitCommitScopes);
|
||||
}
|
||||
},
|
||||
'git-commit-verify': {
|
||||
desc: 'verify git commit message, make sure it match Conventional Commits standard',
|
||||
action: async () => {
|
||||
await gitCommitVerify();
|
||||
}
|
||||
},
|
||||
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();
|
||||
7
packages/scripts/src/shared/index.ts
Normal file
7
packages/scripts/src/shared/index.ts
Normal 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?.trim() || '';
|
||||
}
|
||||
33
packages/scripts/src/types/index.ts
Normal file
33
packages/scripts/src/types/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
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[];
|
||||
/** Git commit types */
|
||||
gitCommitTypes: [string, string][];
|
||||
/** Git commit scopes */
|
||||
gitCommitScopes: [string, string][];
|
||||
/**
|
||||
* Npm-check-updates command args
|
||||
*
|
||||
* @default ['--deep', '-u']
|
||||
*/
|
||||
ncuCommandArgs: string[];
|
||||
/**
|
||||
* Options of generate changelog
|
||||
*
|
||||
* @link https://github.com/soybeanjs/changelog
|
||||
*/
|
||||
changelogOptions: Partial<ChangelogOption>;
|
||||
}
|
||||
20
packages/scripts/tsconfig.json
Normal file
20
packages/scripts/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user