diff --git a/.changeset/nasty-squids-rush.md b/.changeset/nasty-squids-rush.md new file mode 100644 index 00000000..9f212078 --- /dev/null +++ b/.changeset/nasty-squids-rush.md @@ -0,0 +1,5 @@ +--- +'create-arui-scripts-app': minor +--- + +добавлена возможность подключать пакет arui-presets-lint (https://github.com/core-ds/arui-presets-lint/tree/master/packages/arui-presets-lint) diff --git a/packages/create-arui-scripts-app/README.md b/packages/create-arui-scripts-app/README.md index b862f7bc..1e886b21 100644 --- a/packages/create-arui-scripts-app/README.md +++ b/packages/create-arui-scripts-app/README.md @@ -35,7 +35,7 @@ npx create-arui-scripts-app my-app --yes - `--css-modules` / `--no-css-modules` - CSS-модули - `--client-port` / `--server-port` - порты клиента и сервера - `--docker-registry` / `--presets` - docker registry и preset -- `--polyfills` / `--react-compiler` / `--install` - и соответствующие `--no-*` +- `--polyfills` / `--react-compiler` / `--lint` / `--install` - и соответствующие `--no-*` ## Что настраивает мастер @@ -44,15 +44,24 @@ npx create-arui-scripts-app my-app --yes - SSR-сервер на **Hapi** с рендерингом приложения (`renderToString`) и гидрацией на клиенте - транспилятор (**swc** / babel / tsc) и тест-раннер (**Jest** / Vitest) - CSS-модули, полифилы (`core-js`), `experimentalReactCompiler`, docker registry, preset +- опционально **arui-presets-lint** (eslint, prettier, stylelint, knip, secretlint, lefthook) - опциональная установка зависимостей сразу после генерации +Пример с линтерами: + +```bash +npx create-arui-scripts-app my-app --yes --lint --install +``` + ## Результат Генерируются `package.json`, `arui-scripts.config.ts`, `tsconfig.json`, клиентская точка входа, пример компонента со стилями и тестом, `global-definitions.d.ts`, `.gitignore`, `README.md`, `.yarnrc.yml` (с `nodeLinker: node-modules`), -а также в зависимости от ответов - серверная точка входа на Hapi, store на RTK, полифилы и -`vitest.config.ts`. +а также в зависимости от ответов - серверная точка входа на Hapi, store на RTK, полифилы, +`vitest.config.ts` и конфиги `arui-presets-lint` (`eslint.config.mts`, `knip.ts`, +`.secretlintrc.json`, `lefthook.yml`). Дальнейшая сборка и запуск через команды `arui-scripts` в созданном проекте. См. [документацию arui-scripts](../arui-scripts/README.md). +При подключении lint: `yarn lint` / `yarn lint:fix`. diff --git a/packages/create-arui-scripts-app/src/__tests__/build-context.tests.ts b/packages/create-arui-scripts-app/src/__tests__/build-context.tests.ts index 232c459d..5b84d893 100644 --- a/packages/create-arui-scripts-app/src/__tests__/build-context.tests.ts +++ b/packages/create-arui-scripts-app/src/__tests__/build-context.tests.ts @@ -14,6 +14,7 @@ const base: InitAnswers = { presets: '', polyfills: false, reactCompiler: false, + useLint: false, install: false, }; @@ -82,4 +83,13 @@ describe('buildContext', () => { expect(vitestCtx.devDependencies).toHaveProperty('vitest'); expect(vitestCtx.devDependencies).not.toHaveProperty('ts-jest'); }); + + it('useLint добавляет arui-presets-lint', () => { + const withLint = buildContext({ ...base, useLint: true }, '1.0.0'); + const withoutLint = buildContext({ ...base, useLint: false }, '1.0.0'); + + expect(withLint.devDependencies['arui-presets-lint']).toBe('^11.0.0'); + expect(withLint.useLint).toBe(true); + expect(withoutLint.devDependencies).not.toHaveProperty('arui-presets-lint'); + }); }); diff --git a/packages/create-arui-scripts-app/src/__tests__/build-file-map.tests.ts b/packages/create-arui-scripts-app/src/__tests__/build-file-map.tests.ts index 3bdf6e40..5a10a0cc 100644 --- a/packages/create-arui-scripts-app/src/__tests__/build-file-map.tests.ts +++ b/packages/create-arui-scripts-app/src/__tests__/build-file-map.tests.ts @@ -15,6 +15,7 @@ const base: InitAnswers = { presets: '', polyfills: false, reactCompiler: false, + useLint: false, install: false, }; @@ -36,7 +37,7 @@ describe('buildFileMap', () => { 'README.md', 'src/client/index.tsx', 'src/client/components/app.tsx', - 'src/client/components/__tests__/app.test.tsx', + 'src/client/components/__tests__/app.test.ts', ]), ); }); @@ -74,7 +75,7 @@ describe('buildFileMap', () => { expect(ssr['src/client/index.tsx']).toBeDefined(); expect(ssr['src/index.tsx']).toBeUndefined(); - expect(ssr['arui-scripts.config.ts']).toContain('clientEntry: "./src/client"'); + expect(ssr['arui-scripts.config.ts']).toContain("clientEntry: './src/client'"); const spa = map({ clientOnly: true }); @@ -121,7 +122,7 @@ describe('buildFileMap', () => { it('выбранный codeLoader попадает в конфиг', () => { expect(map({ codeLoader: 'babel' })['arui-scripts.config.ts']).toContain( - 'codeLoader: "babel"', + "codeLoader: 'babel'", ); }); @@ -144,14 +145,15 @@ describe('buildFileMap', () => { presets: 'my-preset\\name', })['arui-scripts.config.ts']; - expect(config).toContain('dockerRegistry: "reg.io/org\'s"'); - expect(config).toContain('presets: "my-preset\\\\name"'); + expect(config).toContain("dockerRegistry: 'reg.io/org\\'s'"); + expect(config).toContain("presets: 'my-preset\\\\name'"); }); it('экранирует имя проекта в JSX', () => { const app = map({ name: "O'Reilly " })['src/client/components/app.tsx']; - expect(app).toContain(`{${JSON.stringify("O'Reilly ")}}`); + expect(app).toContain("const appName = 'O\\'Reilly ';"); + expect(app).toContain('{appName}'); }); it('App построен на core-components, конфиг подключает тему', () => { @@ -187,7 +189,7 @@ describe('buildFileMap', () => { expect(files['src/client/polyfills.ts']).toBeDefined(); expect(files['arui-scripts.config.ts']).toContain( - 'clientPolyfillsEntry: "./src/client/polyfills"', + "clientPolyfillsEntry: './src/client/polyfills'", ); }); @@ -217,4 +219,43 @@ describe('buildFileMap', () => { expect(files['vitest.config.ts']).toBeUndefined(); expect(files['package.json']).toContain('"preset": "arui-scripts"'); }); + + it('useLint создает конфиги и scripts arui-presets-lint', () => { + const files = map({ useLint: true }); + const pkg = JSON.parse(files['package.json']) as { + prettier: string; + stylelint: { extends: string }; + commitlint: { extends: string }; + scripts: Record; + devDependencies: Record; + }; + + expect(files['eslint.config.mts']).toContain('arui-presets-lint/eslint'); + expect(files['knip.ts']).toContain("import baseConfig from 'arui-presets-lint/knip'"); + expect(files['knip.ts']).toContain("'ts-jest'"); + expect(files['.secretlintrc.json']).toContain( + '@secretlint/secretlint-rule-preset-recommend', + ); + expect(files['lefthook.yml']).toContain( + './node_modules/arui-presets-lint/lefthook/index.yml', + ); + expect(pkg.prettier).toBe('arui-presets-lint/prettier'); + expect(pkg.stylelint.extends).toBe('arui-presets-lint/stylelint'); + expect(pkg.commitlint.extends).toBe('./node_modules/arui-presets-lint/commitlint'); + expect(pkg.scripts.lint).toContain('yarn lint:scripts'); + expect(pkg.scripts['lint:styles']).toBe('arui-presets-lint styles --max-warnings=0'); + expect(pkg.scripts['lint:scripts']).toBe('arui-presets-lint scripts --max-warnings=0'); + expect(pkg.devDependencies).toHaveProperty('arui-presets-lint'); + expect(files['README.md']).toContain('yarn lint'); + }); + + it('без useLint не создает lint-конфиги', () => { + const files = map({ useLint: false }); + + expect(files['eslint.config.mts']).toBeUndefined(); + expect(files['knip.ts']).toBeUndefined(); + expect(files['.secretlintrc.json']).toBeUndefined(); + expect(files['lefthook.yml']).toBeUndefined(); + expect(files['package.json']).not.toContain('arui-presets-lint'); + }); }); diff --git a/packages/create-arui-scripts-app/src/__tests__/create-program.tests.ts b/packages/create-arui-scripts-app/src/__tests__/create-program.tests.ts index c062c126..ba1beb02 100644 --- a/packages/create-arui-scripts-app/src/__tests__/create-program.tests.ts +++ b/packages/create-arui-scripts-app/src/__tests__/create-program.tests.ts @@ -22,10 +22,11 @@ describe('createProgram', () => { }); it('позитивные флаги дают true', async () => { - const flags = await parseFlags(['--rtk', '--css-modules', '--install']); + const flags = await parseFlags(['--rtk', '--css-modules', '--lint', '--install']); expect(flags.useRtk).toBe(true); expect(flags.cssModules).toBe(true); + expect(flags.useLint).toBe(true); expect(flags.install).toBe(true); }); @@ -35,6 +36,7 @@ describe('createProgram', () => { '--no-css-modules', '--no-polyfills', '--no-react-compiler', + '--no-lint', '--no-install', ]); @@ -42,6 +44,7 @@ describe('createProgram', () => { expect(flags.cssModules).toBe(false); expect(flags.polyfills).toBe(false); expect(flags.reactCompiler).toBe(false); + expect(flags.useLint).toBe(false); expect(flags.install).toBe(false); }); diff --git a/packages/create-arui-scripts-app/src/__tests__/install-dependencies.tests.ts b/packages/create-arui-scripts-app/src/__tests__/install-dependencies.tests.ts index 059184e1..29a4e812 100644 --- a/packages/create-arui-scripts-app/src/__tests__/install-dependencies.tests.ts +++ b/packages/create-arui-scripts-app/src/__tests__/install-dependencies.tests.ts @@ -7,7 +7,7 @@ jest.mock('child_process', () => ({ })); // eslint-disable-next-line import/first -import { installDependencies } from '../install-dependencies'; +import { installDependencies, installLefthook } from '../install-dependencies'; function fakeChild(exitCode: number) { const child = new EventEmitter() as EventEmitter & { @@ -72,4 +72,17 @@ describe('installDependencies', () => { /кодом 1[\s\S]*some output/, ); }); + + it('installLefthook вызывает npx --no-install lefthook install', async () => { + setPlatform('linux'); + spawnMock.mockImplementation(() => fakeChild(0)); + + await installLefthook('/target'); + + expect(spawnMock).toHaveBeenCalledWith( + 'npx', + ['--no-install', 'lefthook', 'install'], + expect.objectContaining({ cwd: '/target', shell: false }), + ); + }); }); diff --git a/packages/create-arui-scripts-app/src/__tests__/questions.tests.ts b/packages/create-arui-scripts-app/src/__tests__/questions.tests.ts index 6c9252d0..006f26aa 100644 --- a/packages/create-arui-scripts-app/src/__tests__/questions.tests.ts +++ b/packages/create-arui-scripts-app/src/__tests__/questions.tests.ts @@ -18,6 +18,7 @@ describe('getQuestions', () => { '', // presets false, // polyfills false, // reactCompiler + false, // useLint false, // install ]); @@ -41,6 +42,7 @@ describe('getQuestions', () => { '', // presets false, // polyfills false, // reactCompiler + false, // useLint false, // install ]); @@ -62,6 +64,7 @@ describe('getQuestions', () => { '', // presets false, // polyfills false, // reactCompiler + false, // useLint false, // install ]); @@ -91,6 +94,7 @@ describe('getQuestions', () => { '', // presets false, // polyfills false, // reactCompiler + true, // useLint false, // install ]); @@ -98,5 +102,6 @@ describe('getQuestions', () => { expect(answers.clientOnly).toBe(false); expect(answers.serverPort).toBeUndefined(); + expect(answers.useLint).toBe(true); }); }); diff --git a/packages/create-arui-scripts-app/src/__tests__/run.tests.ts b/packages/create-arui-scripts-app/src/__tests__/run.tests.ts index 3d060a53..577f17c5 100644 --- a/packages/create-arui-scripts-app/src/__tests__/run.tests.ts +++ b/packages/create-arui-scripts-app/src/__tests__/run.tests.ts @@ -116,6 +116,7 @@ describe('runInit', () => { '', // presets false, // polyfills false, // reactCompiler + false, // useLint false, // install ]); @@ -137,7 +138,7 @@ describe('runInit', () => { const config = await fs.readFile(path.join(target, 'arui-scripts.config.ts'), 'utf8'); expect(config).toContain('clientOnly: true'); - expect(config).toContain('codeLoader: "babel"'); + expect(config).toContain("codeLoader: 'babel'"); expect(config).toContain('clientServerPort: 8081'); }); diff --git a/packages/create-arui-scripts-app/src/build-context.ts b/packages/create-arui-scripts-app/src/build-context.ts index 88f14000..2d1dc4e5 100644 --- a/packages/create-arui-scripts-app/src/build-context.ts +++ b/packages/create-arui-scripts-app/src/build-context.ts @@ -22,6 +22,7 @@ const VERSIONS = { tsJest: '^29.1.0', typesJest: '^29.5.0', vitest: '^4.1.5', + aruiPresetsLint: '^11.0.0', } as const; export function buildContext(answers: InitAnswers, aruiScriptsVersion: string): TemplateContext { @@ -69,6 +70,10 @@ export function buildContext(answers: InitAnswers, aruiScriptsVersion: string): devDependencies.vitest = VERSIONS.vitest; } + if (answers.useLint) { + devDependencies['arui-presets-lint'] = VERSIONS.aruiPresetsLint; + } + return { name: answers.name, useRtk: answers.useRtk, @@ -82,6 +87,7 @@ export function buildContext(answers: InitAnswers, aruiScriptsVersion: string): presets: answers.presets.trim(), polyfills: answers.polyfills, reactCompiler: answers.reactCompiler, + useLint: answers.useLint, aruiScriptsVersion, dependencies, devDependencies, diff --git a/packages/create-arui-scripts-app/src/build-file-map.ts b/packages/create-arui-scripts-app/src/build-file-map.ts index 15e0a349..bfd51548 100644 --- a/packages/create-arui-scripts-app/src/build-file-map.ts +++ b/packages/create-arui-scripts-app/src/build-file-map.ts @@ -3,6 +3,12 @@ import { appStylesFileName, appStylesTemplate } from './templates/app-styles.tem import { appTestTemplate } from './templates/app-test.template'; import { aruiScriptsConfigTemplate } from './templates/arui-scripts-config.template'; import { clientEntryTemplate } from './templates/client-entry.template'; +import { + eslintConfigTemplate, + knipConfigTemplate, + lefthookConfigTemplate, + secretlintConfigTemplate, +} from './templates/lint.template'; import { gitignoreTemplate, globalDefinitionsTemplate, @@ -39,7 +45,7 @@ export function buildFileMap(ctx: TemplateContext): Record { [`${client}/index.tsx`]: clientEntryTemplate(ctx), [`${client}/components/app.tsx`]: appComponentTemplate(ctx), [`${client}/components/${appStylesFileName(ctx)}`]: appStylesTemplate(ctx), - [`${client}/components/__tests__/app.test.tsx`]: appTestTemplate(ctx), + [`${client}/components/__tests__/app.test.ts`]: appTestTemplate(ctx), }; if (!ctx.clientOnly) { @@ -60,5 +66,12 @@ export function buildFileMap(ctx: TemplateContext): Record { files[`${client}/store/counter-slice.ts`] = counterSliceTemplate(); } + if (ctx.useLint) { + files['eslint.config.mts'] = eslintConfigTemplate(); + files['knip.ts'] = knipConfigTemplate(); + files['.secretlintrc.json'] = secretlintConfigTemplate(); + files['lefthook.yml'] = lefthookConfigTemplate(); + } + return files; } diff --git a/packages/create-arui-scripts-app/src/create-program.ts b/packages/create-arui-scripts-app/src/create-program.ts index 3f959c67..da45b84e 100644 --- a/packages/create-arui-scripts-app/src/create-program.ts +++ b/packages/create-arui-scripts-app/src/create-program.ts @@ -39,6 +39,8 @@ export function createProgram(onInit: InitHandler = defaultInitHandler): Command .option('--no-polyfills', 'Без полифилов') .option('--react-compiler', 'Включить experimentalReactCompiler') .option('--no-react-compiler', 'Выключить experimentalReactCompiler') + .option('--lint', 'Подключить arui-presets-lint') + .option('--no-lint', 'Без arui-presets-lint') .option('--install', 'Установить зависимости после генерации') .option('--no-install', 'Не устанавливать зависимости') .showHelpAfterError('(используйте --help для справки)') @@ -111,6 +113,10 @@ export function mapOptsToFlags(opts: Record): CliFlags { flags.reactCompiler = opts.reactCompiler; } + if (typeof opts.lint === 'boolean') { + flags.useLint = opts.lint; + } + if (typeof opts.install === 'boolean') { flags.install = opts.install; } diff --git a/packages/create-arui-scripts-app/src/defaults.ts b/packages/create-arui-scripts-app/src/defaults.ts index db1186a5..f8e177d1 100644 --- a/packages/create-arui-scripts-app/src/defaults.ts +++ b/packages/create-arui-scripts-app/src/defaults.ts @@ -15,6 +15,7 @@ export type CliFlags = { presets?: string; polyfills?: boolean; reactCompiler?: boolean; + useLint?: boolean; install?: boolean; }; @@ -32,6 +33,7 @@ export function defaultAnswers(name: string): InitAnswers { presets: '', polyfills: false, reactCompiler: false, + useLint: false, install: false, }; } @@ -49,6 +51,7 @@ const ANSWER_FLAG_KEYS: Array = [ 'presets', 'polyfills', 'reactCompiler', + 'useLint', 'install', ]; @@ -75,6 +78,7 @@ export function answersFromFlags(defaultName: string, flags: CliFlags): InitAnsw ...(flags.presets === undefined ? {} : { presets: flags.presets }), ...(flags.polyfills === undefined ? {} : { polyfills: flags.polyfills }), ...(flags.reactCompiler === undefined ? {} : { reactCompiler: flags.reactCompiler }), + ...(flags.useLint === undefined ? {} : { useLint: flags.useLint }), ...(flags.install === undefined ? {} : { install: flags.install }), name: (flags.name?.trim() || defaultName).trim(), }; diff --git a/packages/create-arui-scripts-app/src/install-dependencies.ts b/packages/create-arui-scripts-app/src/install-dependencies.ts index afa15f65..a66cebbd 100644 --- a/packages/create-arui-scripts-app/src/install-dependencies.ts +++ b/packages/create-arui-scripts-app/src/install-dependencies.ts @@ -1,4 +1,6 @@ import { spawn } from 'child_process'; +import fs from 'fs'; +import path from 'path'; import shell from 'shelljs'; @@ -8,15 +10,10 @@ export function detectPackageManager(): PackageManager { return shell.which('yarn') ? 'yarn' : 'npm'; } -export function installDependencies( - targetDir: string, - packageManager: PackageManager = detectPackageManager(), -): Promise { - const args = packageManager === 'yarn' ? [] : ['install']; - +function runCommand(command: string, args: string[], cwd: string): Promise { return new Promise((resolve, reject) => { - const child = spawn(packageManager, args, { - cwd: targetDir, + const child = spawn(command, args, { + cwd, stdio: ['ignore', 'pipe', 'pipe'], // для windows shell: process.platform === 'win32', @@ -37,12 +34,27 @@ export function installDependencies( } else { reject( new Error( - `${packageManager} ${args.join( - ' ', - )} завершился с кодом ${code}\n${output.trim()}`, + `${command} ${args.join(' ')} завершился с кодом ${code}\n${output.trim()}`, ), ); } }); }); } + +export function installDependencies( + targetDir: string, + packageManager: PackageManager = detectPackageManager(), +): Promise { + const args = packageManager === 'yarn' ? [] : ['install']; + + return runCommand(packageManager, args, targetDir); +} + +export function hasGitRepository(targetDir: string): boolean { + return fs.existsSync(path.join(targetDir, '.git')); +} + +export function installLefthook(targetDir: string): Promise { + return runCommand('npx', ['--no-install', 'lefthook', 'install'], targetDir); +} diff --git a/packages/create-arui-scripts-app/src/questions.ts b/packages/create-arui-scripts-app/src/questions.ts index 6d8f4b66..913215b7 100644 --- a/packages/create-arui-scripts-app/src/questions.ts +++ b/packages/create-arui-scripts-app/src/questions.ts @@ -120,6 +120,14 @@ export function getQuestions(defaultName: string, prefill: CliFlags = {}): promp active: 'да', inactive: 'нет', }, + { + type: unlessAnswered('useLint', 'toggle'), + name: 'useLint', + message: 'Подключить arui-presets-lint', + initial: false, + active: 'да', + inactive: 'нет', + }, { type: unlessAnswered('install', 'toggle'), name: 'install', diff --git a/packages/create-arui-scripts-app/src/run.ts b/packages/create-arui-scripts-app/src/run.ts index 2d85a7a5..0ed11ab8 100644 --- a/packages/create-arui-scripts-app/src/run.ts +++ b/packages/create-arui-scripts-app/src/run.ts @@ -10,7 +10,9 @@ import { buildFileMap } from './build-file-map'; import { answersFromFlags, type CliFlags, hasAnswerFlags } from './defaults'; import { detectPackageManager, + hasGitRepository, installDependencies, + installLefthook, type PackageManager, } from './install-dependencies'; import { getQuestions } from './questions'; @@ -86,11 +88,40 @@ export async function runInit(options: RunInitOptions = {}): Promise { spinner.fail(chalk.red('Не удалось установить зависимости')); throw error; } + + if (initAnswers.useLint) { + await tryInstallLefthook(targetDir); + } } printNextSteps(targetDir, baseCwd, initAnswers, packageManager); } +async function tryInstallLefthook(targetDir: string): Promise { + if (!hasGitRepository(targetDir)) { + console.log( + ` ${chalk.dim( + 'lefthook: пропущено (нет .git). После git init выполните: npx --no-install lefthook install', + )}`, + ); + + return; + } + + const spinner = ora({ text: 'Устанавливаю git-хуки lefthook…', color: 'cyan' }).start(); + + try { + await installLefthook(targetDir); + spinner.succeed(chalk.green('Git-хуки lefthook установлены')); + } catch (error) { + spinner.fail(chalk.yellow('Не удалось установить lefthook')); + + if (error instanceof Error && error.message) { + console.log(` ${chalk.dim(error.message)}`); + } + } +} + async function resolveAnswers(defaultName: string, flags: CliFlags): Promise { const base = answersFromFlags(defaultName, flags); @@ -177,6 +208,10 @@ function mergePromptAnswers(base: InitAnswers, answers: prompts.Answers) merged.reactCompiler = Boolean(answers.reactCompiler); } + if (answers.useLint !== undefined) { + merged.useLint = Boolean(answers.useLint); + } + if (answers.install !== undefined) { merged.install = Boolean(answers.install); } @@ -199,6 +234,7 @@ function printSuccess(context: TemplateContext, targetDir: string, fileCount: nu chalk.dim(context.clientOnly ? 'clientOnly' : 'SSR'), chalk.dim(context.codeLoader), chalk.dim(context.testRunner), + ...(context.useLint ? [chalk.dim('lint')] : []), ].join(chalk.dim(' · ')); console.log(); @@ -245,6 +281,14 @@ function printNextSteps( steps.push(installCommand); } + if (answers.useLint && (!answers.install || !hasGitRepository(targetDir))) { + steps.push('npx --no-install lefthook install'); + } + + if (answers.useLint) { + steps.push(packageManager === 'yarn' ? 'yarn lint' : 'npm run lint'); + } + steps.push(startCommand); console.log(); diff --git a/packages/create-arui-scripts-app/src/templates/app-component.template.ts b/packages/create-arui-scripts-app/src/templates/app-component.template.ts index dbf312a5..0a9b5b5e 100644 --- a/packages/create-arui-scripts-app/src/templates/app-component.template.ts +++ b/packages/create-arui-scripts-app/src/templates/app-component.template.ts @@ -1,5 +1,9 @@ import { type TemplateContext } from '../types'; +function tsString(value: string): string { + return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`; +} + export function appComponentTemplate(ctx: TemplateContext): string { const styleImport = ctx.cssModules ? "import styles from './app.module.css';" @@ -7,6 +11,7 @@ export function appComponentTemplate(ctx: TemplateContext): string { const rootClass = ctx.cssModules ? '{styles.root}' : "'app'"; const titleClass = ctx.cssModules ? '{styles.title}' : "'app__title'"; + const appNameLiteral = tsString(ctx.name); const coreImports = `import { Button } from '@alfalab/core-components/button'; import { Gap } from '@alfalab/core-components/gap'; @@ -14,13 +19,10 @@ import { Typography } from '@alfalab/core-components/typography';`; const view = (count: string) => `
- {${JSON.stringify(ctx.name)}} + {appName} - - Счетчик: {${count}} - `; if (ctx.useRtk) { @@ -28,18 +30,19 @@ import { Typography } from '@alfalab/core-components/typography';`; ${coreImports} -import { useAppDispatch, useAppSelector } from '../store/hooks'; import { decrement, increment } from '../store/counter-slice'; +import { useAppDispatch, useAppSelector } from '../store/hooks'; ${styleImport} +const appName = ${appNameLiteral}; + export function App() { const count = useAppSelector((state) => state.counter.value); const dispatch = useAppDispatch(); return ( ${view('count')} - {' '} @@ -58,12 +61,13 @@ ${coreImports} ${styleImport} +const appName = ${appNameLiteral}; + export function App() { const [count, setCount] = useState(0); return ( ${view('count')} - {' '} diff --git a/packages/create-arui-scripts-app/src/templates/app-styles.template.ts b/packages/create-arui-scripts-app/src/templates/app-styles.template.ts index 359aded6..3a6f1fc1 100644 --- a/packages/create-arui-scripts-app/src/templates/app-styles.template.ts +++ b/packages/create-arui-scripts-app/src/templates/app-styles.template.ts @@ -7,25 +7,23 @@ export function appStylesFileName(ctx: TemplateContext): string { export function appStylesTemplate(ctx: TemplateContext): string { if (ctx.cssModules) { return `.root { - padding: 16px; - font-family: sans-serif; + padding: var(--gap-16); } .title { - margin: 0 0 12px; - font-size: 24px; + margin: var(--gap-0) var(--gap-0) var(--gap-12); + @mixin headline_small; } `; } return `.app { - padding: 16px; - font-family: sans-serif; + padding: var(--gap-16); } .app__title { - margin: 0 0 12px; - font-size: 24px; + margin: var(--gap-0) var(--gap-0) var(--gap-12); + @mixin headline_small; } `; } diff --git a/packages/create-arui-scripts-app/src/templates/arui-scripts-config.template.ts b/packages/create-arui-scripts-app/src/templates/arui-scripts-config.template.ts index afc2a60a..fa9977b9 100644 --- a/packages/create-arui-scripts-app/src/templates/arui-scripts-config.template.ts +++ b/packages/create-arui-scripts-app/src/templates/arui-scripts-config.template.ts @@ -1,8 +1,8 @@ import { type TemplateContext } from '../types'; -// Безопасная вставка строки в конфиг +// Безопасная вставка строки в конфиг (одинарные кавычки как у prettier) function tsString(value: string): string { - return JSON.stringify(value); + return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`; } export function aruiScriptsConfigTemplate(ctx: TemplateContext): string { diff --git a/packages/create-arui-scripts-app/src/templates/client-entry.template.ts b/packages/create-arui-scripts-app/src/templates/client-entry.template.ts index b9c04612..bda4690b 100644 --- a/packages/create-arui-scripts-app/src/templates/client-entry.template.ts +++ b/packages/create-arui-scripts-app/src/templates/client-entry.template.ts @@ -2,10 +2,9 @@ import { type TemplateContext } from '../types'; const HMR_BLOCK = `if (process.env.NODE_ENV !== 'production' && module.hot) { module.hot.accept('./components/app', () => { - // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires - const NextApp = require('./components/app').App; + const mod = require('./components/app') as { App: typeof App }; - render(NextApp); + render(mod.App); }); } `; diff --git a/packages/create-arui-scripts-app/src/templates/lint.template.ts b/packages/create-arui-scripts-app/src/templates/lint.template.ts new file mode 100644 index 00000000..65312f7b --- /dev/null +++ b/packages/create-arui-scripts-app/src/templates/lint.template.ts @@ -0,0 +1,53 @@ +export function eslintConfigTemplate(): string { + return `import { defineConfig, eslintConfig, TYPESCRIPT_SCRIPTS_SCOPE } from 'arui-presets-lint/eslint'; + +export default defineConfig(eslintConfig, [ + { + languageOptions: { + parserOptions: { + projectService: { + allowDefaultProject: [ + 'arui-scripts.config.ts', + 'eslint.config.mts', + 'knip.ts', + 'vitest.config.ts', + ], + }, + }, + }, + files: [TYPESCRIPT_SCRIPTS_SCOPE], + }, +]); +`; +} + +export function knipConfigTemplate(): string { + return `import baseConfig from 'arui-presets-lint/knip'; + +export default { + ...baseConfig, + // ts-jest подключается через jest.preset arui-scripts, прямых импортов в коде нет + ignoreDependencies: [...baseConfig.ignoreDependencies, 'ts-jest'], +}; +`; +} + +export function secretlintConfigTemplate(): string { + return `${JSON.stringify( + { + rules: [ + { + id: '@secretlint/secretlint-rule-preset-recommend', + }, + ], + }, + null, + 4, + )}\n`; +} + +export function lefthookConfigTemplate(): string { + return `extends: + - ./node_modules/arui-presets-lint/lefthook/index.yml +`; +} diff --git a/packages/create-arui-scripts-app/src/templates/misc.template.ts b/packages/create-arui-scripts-app/src/templates/misc.template.ts index acd194b0..9a551f94 100644 --- a/packages/create-arui-scripts-app/src/templates/misc.template.ts +++ b/packages/create-arui-scripts-app/src/templates/misc.template.ts @@ -57,6 +57,13 @@ defaultSemverRangePrefix: "" export function readmeTemplate(ctx: TemplateContext): string { const testCommand = ctx.testRunner === 'jest' ? 'arui-scripts test' : 'arui-scripts test:vitest'; + const lintSection = ctx.useLint + ? ` + yarn lint - eslint, stylelint, prettier, knip, secretlint + yarn lint:fix - автофикс eslint/stylelint + format + yarn format - prettier +` + : ''; return `# ${ctx.name} @@ -66,8 +73,7 @@ export function readmeTemplate(ctx: TemplateContext): string { arui-scripts start - запуск dev-сервера arui-scripts build - production-сборка - ${testCommand}${' '.repeat(Math.max(1, 20 - testCommand.length))}- запуск тестов - + ${testCommand}${' '.repeat(Math.max(1, 20 - testCommand.length))}- запуск тестов${lintSection} ## Установка yarn install diff --git a/packages/create-arui-scripts-app/src/templates/package-json.template.ts b/packages/create-arui-scripts-app/src/templates/package-json.template.ts index a6ea1566..563fabe4 100644 --- a/packages/create-arui-scripts-app/src/templates/package-json.template.ts +++ b/packages/create-arui-scripts-app/src/templates/package-json.template.ts @@ -21,6 +21,20 @@ export function packageJsonTemplate(ctx: TemplateContext): string { scripts['docker-build'] = 'arui-scripts docker-build'; } + if (ctx.useLint) { + scripts['lint:styles'] = 'arui-presets-lint styles --max-warnings=0'; + scripts['lint:scripts'] = 'arui-presets-lint scripts --max-warnings=0'; + scripts.format = 'arui-presets-lint format'; + scripts['format:check'] = 'arui-presets-lint format:check'; + scripts['lint:unused'] = 'arui-presets-lint knip'; + scripts['lint:unused:fix'] = 'arui-presets-lint knip --fix'; + scripts['lint:secrets'] = 'arui-presets-lint secretlint'; + scripts.lint = + 'yarn lint:styles && yarn lint:scripts && yarn format:check && yarn lint:unused && yarn lint:secrets'; + scripts['lint:fix'] = + 'yarn lint:styles --fix && yarn lint:scripts --fix && yarn format && yarn lint:secrets'; + } + const pkg: Record = { name: ctx.name, version: '0.1.0', @@ -31,6 +45,12 @@ export function packageJsonTemplate(ctx: TemplateContext): string { }, }; + if (ctx.useLint) { + pkg.prettier = 'arui-presets-lint/prettier'; + pkg.stylelint = { extends: 'arui-presets-lint/stylelint' }; + pkg.commitlint = { extends: './node_modules/arui-presets-lint/commitlint' }; + } + if (ctx.testRunner === 'jest') { pkg.jest = { preset: 'arui-scripts' }; } diff --git a/packages/create-arui-scripts-app/src/templates/server-entry.template.ts b/packages/create-arui-scripts-app/src/templates/server-entry.template.ts index 47116384..6b2f0b79 100644 --- a/packages/create-arui-scripts-app/src/templates/server-entry.template.ts +++ b/packages/create-arui-scripts-app/src/templates/server-entry.template.ts @@ -6,38 +6,17 @@ export function serverEntryTemplate(ctx: TemplateContext): string { const renderPageFn = ctx.useRtk ? `function renderPage(appHtml: string, assets: Assets, preloadedState: string): string { - const css = assets.css - .map((href) => '') - .join(''); - const js = assets.js.map((src) => '').join(''); - const state = ''; - - return ( - '' + - css + - '
' + - appHtml + - '
' + - state + - js + - '' - ); + const css = assets.css.map((href) => \`\`).join(''); + const js = assets.js.map((src) => \`\`).join(''); + const state = \`\`; + + return \`\${css}
\${appHtml}
\${state}\${js}\`; }` : `function renderPage(appHtml: string, assets: Assets): string { - const css = assets.css - .map((href) => '') - .join(''); - const js = assets.js.map((src) => '').join(''); - - return ( - '' + - css + - '
' + - appHtml + - '
' + - js + - '' - ); + const css = assets.css.map((href) => \`\`).join(''); + const js = assets.js.map((src) => \`\`).join(''); + + return \`\${css}
\${appHtml}
\${js}\`; }`; const handlerBody = ctx.useRtk @@ -57,12 +36,11 @@ export function serverEntryTemplate(ctx: TemplateContext): string { return renderPage(appHtml, assets);`; - return `import path from 'path'; - + return `import React from 'react'; +import { renderToString } from 'react-dom/server';${reduxImport} import Hapi from '@hapi/hapi'; import Inert from '@hapi/inert'; -import React from 'react'; -import { renderToString } from 'react-dom/server';${reduxImport} +import path from 'node:path'; import { readAssetsManifest } from '@alfalab/scripts-server'; @@ -108,10 +86,10 @@ ${handlerBody} }); await server.start(); - // eslint-disable-next-line no-console - console.log('Server is listening on ' + server.info.uri); + // eslint-disable-next-line no-console -- стартовый лог dev-сервера + console.log(\`Server is listening on \${server.info.uri}\`); } -start(); +void start(); `; -} +} \ No newline at end of file diff --git a/packages/create-arui-scripts-app/src/templates/store.template.ts b/packages/create-arui-scripts-app/src/templates/store.template.ts index 3aa95c4e..ab46a385 100644 --- a/packages/create-arui-scripts-app/src/templates/store.template.ts +++ b/packages/create-arui-scripts-app/src/templates/store.template.ts @@ -54,7 +54,7 @@ const counterSlice = createSlice({ }, }); -export const { increment, decrement } = counterSlice.actions; +export const { decrement, increment } = counterSlice.actions; export const counterReducer = counterSlice.reducer; `; } diff --git a/packages/create-arui-scripts-app/src/templates/tsconfig.template.ts b/packages/create-arui-scripts-app/src/templates/tsconfig.template.ts index b09d7546..db903899 100644 --- a/packages/create-arui-scripts-app/src/templates/tsconfig.template.ts +++ b/packages/create-arui-scripts-app/src/templates/tsconfig.template.ts @@ -2,15 +2,16 @@ import { type TemplateContext } from '../types'; export function tsconfigTemplate(ctx: TemplateContext): string { const types = - ctx.testRunner === 'jest' ? ['jest', 'node', 'webpack-env'] : ['node', 'webpack-env']; + ctx.testRunner === 'jest' + ? '["jest", "node", "webpack-env"]' + : '["node", "webpack-env"]'; - const tsconfig = { - extends: 'arui-scripts/tsconfig.json', - include: ['global-definitions.d.ts', 'src/**/*.ts', 'src/**/*.tsx'], - compilerOptions: { - types, - }, - }; - - return `${JSON.stringify(tsconfig, null, 4)}\n`; + return `{ + "extends": "arui-scripts/tsconfig.json", + "include": ["global-definitions.d.ts", "src/**/*.ts", "src/**/*.tsx"], + "compilerOptions": { + "types": ${types} + } +} +`; } diff --git a/packages/create-arui-scripts-app/src/types.ts b/packages/create-arui-scripts-app/src/types.ts index 77585bf0..144b1bb7 100644 --- a/packages/create-arui-scripts-app/src/types.ts +++ b/packages/create-arui-scripts-app/src/types.ts @@ -20,6 +20,8 @@ export type InitAnswers = { polyfills: boolean; // Включить experimentalReactCompiler reactCompiler: boolean; + // Подключить arui-presets-lint + useLint: boolean; // Установить зависимости сразу после генерации install: boolean; }; @@ -37,6 +39,7 @@ export type TemplateContext = { presets: string; polyfills: boolean; reactCompiler: boolean; + useLint: boolean; aruiScriptsVersion: string; dependencies: Record; devDependencies: Record; diff --git a/packages/create-arui-scripts-app/src/versions.ts b/packages/create-arui-scripts-app/src/versions.ts index 0a0ff798..a1ebef13 100644 --- a/packages/create-arui-scripts-app/src/versions.ts +++ b/packages/create-arui-scripts-app/src/versions.ts @@ -1,4 +1,4 @@ /** Версия arui-scripts, которую scaffold кладёт в package.json нового проекта. * Генерируется скриптом scripts/sync-arui-scripts-version.js при сборке. */ -export const DEFAULT_ARUI_SCRIPTS_VERSION = '23.2.0'; +export const DEFAULT_ARUI_SCRIPTS_VERSION = '23.3.0';