diff --git a/packages/create-rslib/package.json b/packages/create-rslib/package.json index 5d9d80ff6..b826f96f8 100644 --- a/packages/create-rslib/package.json +++ b/packages/create-rslib/package.json @@ -29,7 +29,7 @@ "test": "rstest" }, "dependencies": { - "create-rstack": "1.7.4" + "create-rstack": "1.7.6" }, "devDependencies": { "@rslib/tsconfig": "workspace:*", diff --git a/packages/create-rslib/src/index.ts b/packages/create-rslib/src/index.ts index cb38f4ce3..3cc2d1274 100644 --- a/packages/create-rslib/src/index.ts +++ b/packages/create-rslib/src/index.ts @@ -4,6 +4,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { type Argv, + BUILTIN_TOOLS, checkCancel, create, type ESLintTemplateName, @@ -16,15 +17,20 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); type TemplateName = 'react' | 'node' | 'vue'; -async function getTemplateName({ template }: Argv) { - if (typeof template === 'string') { - const pair = template.split('-'); - const lang = pair[pair.length - 1]; - if (lang && ['js', 'ts'].includes(lang)) { - return template; - } - // default to ts - return `${template}-ts`; +async function getTemplateName(argv: Argv) { + if (typeof argv.template === 'string') { + const pair = argv.template.split('-'); + const lang = pair[pair.length - 1] ?? 'ts'; + const rest = pair.slice(0, pair.length - 1).join('-'); + const tools = ( + typeof argv.tools === 'string' ? [argv.tools] : (argv.tools ?? []) + ).filter((tool) => !BUILTIN_TOOLS.includes(tool)); + + return composeTemplateName({ + template: rest, + lang: lang as Lang, + tools, + }); } const templateName = checkCancel( diff --git a/packages/create-rslib/test/helper.ts b/packages/create-rslib/test/helper.ts index 919ed5dff..428d86423 100644 --- a/packages/create-rslib/test/helper.ts +++ b/packages/create-rslib/test/helper.ts @@ -3,22 +3,7 @@ import { existsSync } from 'node:fs'; import path from 'node:path'; import { expect } from '@rstest/core'; import fse from 'fs-extra'; - -export const decomposeTemplateName = ( - name: string, -): { - template: string; - tools: string[]; - lang: string; -} => { - const [template, tools, lang] = name.split('-'); - const trimBrackets = (str: string) => str.replace(/^\[|\]$/g, ''); - return { - template: trimBrackets(template)!, - tools: tools === '[]' ? [] : trimBrackets(tools).split(','), - lang: lang!, - } as const; -}; +import type { Lang } from '../src/helpers'; export const expectPackageJson = ( pkgJson: Record, @@ -30,12 +15,19 @@ export const expectPackageJson = ( expect(pkgJson.devDependencies['@rslib/core']).toBeTruthy(); }; +export interface TemplateCase { + template: string; + lang: Lang; + tools: string[]; + label: string; +} + export const createAndValidate = ( cwd: string, - template: string, + templateCase: TemplateCase, { - name = `test-temp-${template}`, - tools = [], + name = `test-temp-${templateCase.label}`, + tools: additionalTools = [], clean = true, }: { name?: string; @@ -46,9 +38,19 @@ export const createAndValidate = ( const dir = path.join(cwd, name); fse.removeSync(dir); - let command = `node ../dist/index.js -d ${name} -t ${template}`; - if (tools.length) { - const toolsCmd = tools.map((tool) => `--tools ${tool}`).join(' '); + const templateArg = `${templateCase.template}-${templateCase.lang}`; + + let command = `node ../dist/index.js --dir ${name} --template ${templateArg}`; + + if (templateCase.tools.length) { + const templateToolsCmd = templateCase.tools + .map((tool) => `--tools ${tool}`) + .join(' '); + command += ` ${templateToolsCmd}`; + } + + if (additionalTools.length) { + const toolsCmd = additionalTools.map((tool) => `--tools ${tool}`).join(' '); command += ` ${toolsCmd}`; } @@ -57,33 +59,31 @@ export const createAndValidate = ( const pkgJson = fse.readJSONSync(path.join(dir, 'package.json')); expectPackageJson(pkgJson, path.basename(name)); - const templateData = decomposeTemplateName(template); - // tsconfig - if (templateData.lang === 'ts') { + if (templateCase.lang === 'ts') { expect(pkgJson.devDependencies.typescript).toBeTruthy(); expect(existsSync(path.join(dir, 'tsconfig.json'))).toBeTruthy(); } // tool - Vitest - if (templateData.tools.includes('vitest')) { + if (templateCase.tools.includes('vitest')) { for (const file of [ - `vitest.config.${templateData.lang}`, - `tests/index.test.${templateData.lang}${templateData.template === 'react' ? 'x' : ''}`, + `vitest.config.${templateCase.lang}`, + `tests/index.test.${templateCase.lang}${templateCase.template === 'react' ? 'x' : ''}`, ]) { expect(existsSync(path.join(dir, file))).toBeTruthy(); } expect(pkgJson.devDependencies.vitest).toBeTruthy(); - if (templateData.template === 'react') { + if (templateCase.template === 'react') { expect(pkgJson.devDependencies['@testing-library/react']).toBeTruthy(); } } // tool - Storybook - if (templateData.tools.includes('storybook')) { + if (templateCase.tools.includes('storybook')) { for (const file of [ - `.storybook/main.${templateData.lang}`, - `.storybook/preview.${templateData.lang}`, + `.storybook/main.${templateCase.lang}`, + `.storybook/preview.${templateCase.lang}`, ]) { expect(existsSync(path.join(dir, file))).toBeTruthy(); } diff --git a/packages/create-rslib/test/index.test.ts b/packages/create-rslib/test/index.test.ts index 4e4005d66..832eb55d5 100644 --- a/packages/create-rslib/test/index.test.ts +++ b/packages/create-rslib/test/index.test.ts @@ -1,79 +1,92 @@ import { existsSync } from 'node:fs'; import { join } from 'node:path'; import { describe, expect, test } from '@rstest/core'; -import { composeTemplateName, TEMPLATES } from '../src/helpers'; -import { createAndValidate } from './helper'; - -const CASES_NODE_DUAL = [ - '[node-dual]-[]-js', - '[node-dual]-[]-ts', - '[node-dual]-[rstest]-js', - '[node-dual]-[rstest]-ts', - '[node-dual]-[vitest]-js', - '[node-dual]-[vitest]-ts', +import { composeTemplateName, type Lang, TEMPLATES } from '../src/helpers'; +import { createAndValidate, type TemplateCase } from './helper'; + +const createCase = ( + template: string, + lang: Lang, + tools: string[] = [], +): TemplateCase => ({ + template, + lang, + tools, + label: composeTemplateName({ template, lang, tools }), +}); + +const CASES_NODE_DUAL: TemplateCase[] = [ + createCase('node-dual', 'js'), + createCase('node-dual', 'ts'), + createCase('node-dual', 'js', ['rstest']), + createCase('node-dual', 'ts', ['rstest']), + createCase('node-dual', 'js', ['vitest']), + createCase('node-dual', 'ts', ['vitest']), ]; -const CASES_NODE_ESM = [ - '[node-esm]-[]-js', - '[node-esm]-[]-ts', - '[node-esm]-[rstest]-js', - '[node-esm]-[rstest]-ts', - '[node-esm]-[vitest]-js', - '[node-esm]-[vitest]-ts', +const CASES_NODE_ESM: TemplateCase[] = [ + createCase('node-esm', 'js'), + createCase('node-esm', 'ts'), + createCase('node-esm', 'js', ['rstest']), + createCase('node-esm', 'ts', ['rstest']), + createCase('node-esm', 'js', ['vitest']), + createCase('node-esm', 'ts', ['vitest']), ]; -const CASES_REACT = [ - '[react]-[]-js', - '[react]-[]-ts', - '[react]-[rstest,storybook]-js', - '[react]-[rstest,storybook]-ts', - '[react]-[storybook,vitest]-js', - '[react]-[storybook,vitest]-ts', - '[react]-[storybook]-js', - '[react]-[rstest]-js', - '[react]-[vitest]-js', - '[react]-[storybook]-ts', - '[react]-[rstest]-ts', - '[react]-[vitest]-ts', +const CASES_REACT: TemplateCase[] = [ + createCase('react', 'js'), + createCase('react', 'ts'), + createCase('react', 'js', ['rstest', 'storybook']), + createCase('react', 'ts', ['rstest', 'storybook']), + createCase('react', 'js', ['storybook', 'vitest']), + createCase('react', 'ts', ['storybook', 'vitest']), + createCase('react', 'js', ['storybook']), + createCase('react', 'js', ['rstest']), + createCase('react', 'js', ['vitest']), + createCase('react', 'ts', ['storybook']), + createCase('react', 'ts', ['rstest']), + createCase('react', 'ts', ['vitest']), ]; -const CASES_VUE = [ - '[vue]-[]-js', - '[vue]-[]-ts', - '[vue]-[rstest,storybook]-js', - '[vue]-[rstest,storybook]-ts', - '[vue]-[storybook,vitest]-js', - '[vue]-[storybook,vitest]-ts', - '[vue]-[storybook]-js', - '[vue]-[storybook]-ts', - '[vue]-[rstest]-js', - '[vue]-[rstest]-ts', - '[vue]-[vitest]-js', - '[vue]-[vitest]-ts', +const CASES_VUE: TemplateCase[] = [ + createCase('vue', 'js'), + createCase('vue', 'ts'), + createCase('vue', 'js', ['rstest', 'storybook']), + createCase('vue', 'ts', ['rstest', 'storybook']), + createCase('vue', 'js', ['storybook', 'vitest']), + createCase('vue', 'ts', ['storybook', 'vitest']), + createCase('vue', 'js', ['storybook']), + createCase('vue', 'ts', ['storybook']), + createCase('vue', 'js', ['rstest']), + createCase('vue', 'ts', ['rstest']), + createCase('vue', 'js', ['vitest']), + createCase('vue', 'ts', ['vitest']), ]; +const ALL_CASES = [ + ...CASES_NODE_DUAL, + ...CASES_NODE_ESM, + ...CASES_REACT, + ...CASES_VUE, +]; + +const BASE_NODE_ESM_JS = createCase('node-esm', 'js'); + test('exhaust all cases', () => { - expect( - TEMPLATES.map((t) => - composeTemplateName({ - template: t.template, - lang: t.lang, - tools: Object.keys(t.tools || {}), - }), - ).sort(), - ).toEqual( - [ - ...CASES_NODE_DUAL, - ...CASES_NODE_ESM, - ...CASES_REACT, - ...CASES_VUE, - ].sort(), - ); + const expected = ALL_CASES.map((item) => item.label).sort(); + const actual = TEMPLATES.map((t) => + composeTemplateName({ + template: t.template, + lang: t.lang, + tools: Object.keys(t.tools || {}), + }), + ).sort(); + expect(actual).toEqual(expected); }); describe('node-dual', () => { for (const c of CASES_NODE_DUAL) { - test(`should create ${c} project as expected`, async () => { + test(`should create ${c.label} project as expected`, async () => { createAndValidate(__dirname, c); }); } @@ -81,7 +94,7 @@ describe('node-dual', () => { describe('node-esm', () => { for (const c of CASES_NODE_ESM) { - test(`should create ${c} project as expected`, async () => { + test(`should create ${c.label} project as expected`, async () => { createAndValidate(__dirname, c); }); } @@ -89,7 +102,7 @@ describe('node-esm', () => { describe('react', () => { for (const c of CASES_REACT) { - test(`should create ${c} project as expected`, async () => { + test(`should create ${c.label} project as expected`, async () => { createAndValidate(__dirname, c); }); } @@ -97,13 +110,13 @@ describe('react', () => { describe('custom path to create', () => { test('should allow to create project in sub dir', async () => { - createAndValidate(__dirname, '[node-esm]-[]-js', { + createAndValidate(__dirname, BASE_NODE_ESM_JS, { name: 'test-temp-dir/rslib-project', }); }); test('should allow to create project in relative dir', async () => { - createAndValidate(__dirname, '[node-esm]-[]-js', { + createAndValidate(__dirname, BASE_NODE_ESM_JS, { name: './test-temp-relative-dir', }); }); @@ -113,7 +126,7 @@ describe('linter and formatter', () => { test('should create project with eslint as expected', async () => { const { dir, pkgJson, clean } = createAndValidate( __dirname, - '[node-esm]-[]-js', + BASE_NODE_ESM_JS, { name: 'test-temp-eslint', tools: ['eslint'], @@ -128,7 +141,7 @@ describe('linter and formatter', () => { test('should create project with prettier as expected', async () => { const { dir, pkgJson, clean } = createAndValidate( __dirname, - '[node-esm]-[]-js', + BASE_NODE_ESM_JS, { name: 'test-temp-prettier', tools: ['prettier'], @@ -143,7 +156,7 @@ describe('linter and formatter', () => { test('should create project with eslint and prettier as expected', async () => { const { dir, pkgJson, clean } = createAndValidate( __dirname, - '[node-esm]-[]-js', + BASE_NODE_ESM_JS, { name: 'test-temp-eslint-prettier', tools: ['eslint', 'prettier'], @@ -160,7 +173,7 @@ describe('linter and formatter', () => { test('should create project with biome as expected', async () => { const { dir, pkgJson, clean } = createAndValidate( __dirname, - '[node-esm]-[]-js', + BASE_NODE_ESM_JS, { name: 'test-temp-eslint', tools: ['biome'], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ffc875e4..26b9db80a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -384,8 +384,8 @@ importers: packages/create-rslib: dependencies: create-rstack: - specifier: 1.7.4 - version: 1.7.4 + specifier: 1.7.6 + version: 1.7.6 devDependencies: '@rslib/tsconfig': specifier: workspace:* @@ -4089,8 +4089,8 @@ packages: create-hmac@1.1.7: resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==} - create-rstack@1.7.4: - resolution: {integrity: sha512-jKOlWgu4ybC+YByq4ImtC8llMQC7yeHcbwaNlg5f0u9IvVssoGX2WRB+SqIHTLTVjXZ4VZO8wDmS7CjUYfgM6A==} + create-rstack@1.7.6: + resolution: {integrity: sha512-S8CADmfLdA8uNucwkzhATb81yXWIWgEIjeBhVzoowcqKtq/mwjHy/sEZftivjbM+XRqdDjwEhjRIK/fmuvNhdA==} cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} @@ -10648,7 +10648,7 @@ snapshots: safe-buffer: 5.2.1 sha.js: 2.4.12 - create-rstack@1.7.4: {} + create-rstack@1.7.6: {} cron-parser@4.9.0: dependencies: diff --git a/website/docs/en/guide/start/quick-start.mdx b/website/docs/en/guide/start/quick-start.mdx index 95adce6f8..d84650d14 100644 --- a/website/docs/en/guide/start/quick-start.mdx +++ b/website/docs/en/guide/start/quick-start.mdx @@ -100,13 +100,13 @@ If you need to create a project in the current directory, you can set the target [create-rslib](https://www.npmjs.com/package/create-rslib) provides some CLI flags. By setting these CLI flags, you can skip the interactive selection steps and create the project with one command. -For example, to create an example project in the `my-project` directory with one command: +For example, to create a React + TypeScript + Storybook + Biome project in the `my-project` directory with one command: ```bash -npx create-rslib --dir my-project --template example +npx create-rslib --dir my-project --template react-ts --tools storybook --tools biome # Using abbreviations -npx create-rslib -d my-project -t example +npx create-rslib -d my-project -t react-ts --tools storybook --tools biome ``` All the CLI flags of `create-rslib`: diff --git a/website/docs/zh/guide/start/quick-start.mdx b/website/docs/zh/guide/start/quick-start.mdx index ac57223fb..7fb59e5e4 100644 --- a/website/docs/zh/guide/start/quick-start.mdx +++ b/website/docs/zh/guide/start/quick-start.mdx @@ -100,13 +100,13 @@ Biome 提供与 ESLint 和 Prettier 类似的代码检查和格式化功能。 [create-rslib](https://www.npmjs.com/package/create-rslib) 提供了一些 CLI 选项。通过设置这些 CLI 选项,你可以跳过交互式的选择步骤,一键创建项目。 -比如,一键创建 example 模版项目到 `my-project`: +比如,一键创建 React + TypeScript + Storybook + Biome 的项目到 `my-project`: ```bash -npx create-rslib --dir my-project --template example +npx create-rslib --dir my-project --template react-ts --tools storybook --tools biome # 使用缩写 -npx create-rslib -d my-project -t example +npx create-rslib -d my-project -t react-ts --tools storybook --tools biome ``` `create-rslib` 的完整的 CLI 选项如下: