create-arui-scripts-app 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # create-arui-scripts-app
2
2
 
3
+ ## 1.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#577](https://github.com/core-ds/arui-scripts/pull/577) [`231321b`](https://github.com/core-ds/arui-scripts/commit/231321bf2707abbbd909931cbd358880e488a2b5) Thanks [@dmitrbrvsk](https://github.com/dmitrbrvsk)! - В CLI генерации приложения добавлена возможность выбор e2e фреймворка (Cypress, Playwright) и подключение простого роутинга через React Router
8
+
3
9
  ## 1.1.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -32,6 +32,7 @@ npx create-arui-scripts-app my-app --yes
32
32
  - `--ssr` / `--client-only` - SSR или только клиент
33
33
  - `--code-loader <swc|babel|tsc>` - транспилятор
34
34
  - `--test-runner <jest|vitest>` - тест-раннер
35
+ - `--e2e-framework <cypress|playwright|none>` - e2e фреймворк
35
36
  - `--css-modules` / `--no-css-modules` - CSS-модули
36
37
  - `--client-port` / `--server-port` - порты клиента и сервера
37
38
  - `--docker-registry` / `--presets` - docker registry и preset
@@ -43,6 +44,8 @@ npx create-arui-scripts-app my-app --yes
43
44
  - режим **SSR** (клиент + сервер, клиент в `src/client/`) или **clientOnly** (статика)
44
45
  - SSR-сервер на **Hapi** с рендерингом приложения (`renderToString`) и гидрацией на клиенте
45
46
  - транспилятор (**swc** / babel / tsc) и тест-раннер (**Jest** / Vitest)
47
+ - опционально **e2e** (**Cypress** / Playwright) с конфигом и хелперами
48
+ - опционально **React Router**
46
49
  - CSS-модули, полифилы (`core-js`), `experimentalReactCompiler`, docker registry, preset
47
50
  - опционально **arui-presets-lint** (eslint, prettier, stylelint, knip, secretlint, lefthook)
48
51
  - опциональная установка зависимостей сразу после генерации
@@ -59,8 +62,8 @@ npx create-arui-scripts-app my-app --yes --lint --install
59
62
  пример компонента со стилями и тестом, `global-definitions.d.ts`, `.gitignore`, `README.md`,
60
63
  `.yarnrc.yml` (с `nodeLinker: node-modules`),
61
64
  а также в зависимости от ответов - серверная точка входа на Hapi, store на RTK, полифилы,
62
- `vitest.config.ts` и конфиги `arui-presets-lint` (`eslint.config.mts`, `knip.ts`,
63
- `.secretlintrc.json`, `lefthook.yml`).
65
+ `vitest.config.ts`, e2e-каркас (Playwright или Cypress), роуты React Router и конфиги
66
+ `arui-presets-lint` (`eslint.config.mts`, `knip.ts`, `.secretlintrc.json`, `lefthook.yml`).
64
67
 
65
68
  Дальнейшая сборка и запуск через команды `arui-scripts` в созданном проекте.
66
69
  См. [документацию arui-scripts](../arui-scripts/README.md).
@@ -23,6 +23,9 @@ const VERSIONS = {
23
23
  tsJest: '^29.1.0',
24
24
  typesJest: '^29.5.0',
25
25
  vitest: '^4.1.5',
26
+ playwrightTest: '^1.57.0',
27
+ cypress: '^15.19.0',
28
+ reactRouterDom: '^7.6.0',
26
29
  aruiPresetsLint: '^11.0.0',
27
30
  };
28
31
  function buildContext(answers, aruiScriptsVersion) {
@@ -50,6 +53,10 @@ function buildContext(answers, aruiScriptsVersion) {
50
53
  dependencies['@reduxjs/toolkit'] = VERSIONS.reduxToolkit;
51
54
  dependencies['react-redux'] = VERSIONS.reactRedux;
52
55
  }
56
+ if (answers.useRouter) {
57
+ dependencies['react-router'] = VERSIONS.reactRouterDom;
58
+ dependencies['react-router-dom'] = VERSIONS.reactRouterDom;
59
+ }
53
60
  if (answers.polyfills) {
54
61
  dependencies['core-js'] = VERSIONS.coreJs;
55
62
  }
@@ -64,6 +71,12 @@ function buildContext(answers, aruiScriptsVersion) {
64
71
  else {
65
72
  devDependencies.vitest = VERSIONS.vitest;
66
73
  }
74
+ if (answers.e2eFramework === 'playwright') {
75
+ devDependencies['@playwright/test'] = VERSIONS.playwrightTest;
76
+ }
77
+ else if (answers.e2eFramework === 'cypress') {
78
+ devDependencies.cypress = VERSIONS.cypress;
79
+ }
67
80
  if (answers.useLint) {
68
81
  devDependencies['arui-presets-lint'] = VERSIONS.aruiPresetsLint;
69
82
  }
@@ -73,6 +86,8 @@ function buildContext(answers, aruiScriptsVersion) {
73
86
  clientOnly: answers.clientOnly,
74
87
  codeLoader: answers.codeLoader,
75
88
  testRunner: answers.testRunner,
89
+ e2eFramework: answers.e2eFramework,
90
+ useRouter: answers.useRouter,
76
91
  cssModules: answers.cssModules,
77
92
  clientServerPort: answers.clientServerPort,
78
93
  serverPort: answers.serverPort,
@@ -7,9 +7,12 @@ const app_styles_template_1 = require("./templates/app-styles.template");
7
7
  const app_test_template_1 = require("./templates/app-test.template");
8
8
  const arui_scripts_config_template_1 = require("./templates/arui-scripts-config.template");
9
9
  const client_entry_template_1 = require("./templates/client-entry.template");
10
+ const e2e_cypress_template_1 = require("./templates/e2e-cypress.template");
11
+ const e2e_playwright_template_1 = require("./templates/e2e-playwright.template");
10
12
  const lint_template_1 = require("./templates/lint.template");
11
13
  const misc_template_1 = require("./templates/misc.template");
12
14
  const package_json_template_1 = require("./templates/package-json.template");
15
+ const router_template_1 = require("./templates/router.template");
13
16
  const server_entry_template_1 = require("./templates/server-entry.template");
14
17
  const store_template_1 = require("./templates/store.template");
15
18
  const tsconfig_template_1 = require("./templates/tsconfig.template");
@@ -23,7 +26,7 @@ function buildFileMap(ctx) {
23
26
  'package.json': (0, package_json_template_1.packageJsonTemplate)(ctx),
24
27
  'arui-scripts.config.ts': (0, arui_scripts_config_template_1.aruiScriptsConfigTemplate)(ctx),
25
28
  'tsconfig.json': (0, tsconfig_template_1.tsconfigTemplate)(ctx),
26
- '.gitignore': (0, misc_template_1.gitignoreTemplate)(),
29
+ '.gitignore': (0, misc_template_1.gitignoreTemplate)(ctx),
27
30
  '.yarnrc.yml': (0, misc_template_1.yarnrcTemplate)(),
28
31
  'global-definitions.d.ts': (0, misc_template_1.globalDefinitionsTemplate)(),
29
32
  'README.md': (0, misc_template_1.readmeTemplate)(ctx),
@@ -46,11 +49,28 @@ function buildFileMap(ctx) {
46
49
  files[`${client}/store/hooks.ts`] = (0, store_template_1.storeHooksTemplate)();
47
50
  files[`${client}/store/counter-slice.ts`] = (0, store_template_1.counterSliceTemplate)();
48
51
  }
52
+ if (ctx.useRouter) {
53
+ files[`${client}/routes.tsx`] = (0, router_template_1.routesTemplate)();
54
+ files[`${client}/components/layout.tsx`] = (0, router_template_1.layoutTemplate)(ctx);
55
+ files[`${client}/pages/home.tsx`] = (0, router_template_1.homePageTemplate)(ctx);
56
+ files[`${client}/pages/about.tsx`] = (0, router_template_1.aboutPageTemplate)(ctx);
57
+ }
49
58
  if (ctx.useLint) {
50
59
  files['eslint.config.mts'] = (0, lint_template_1.eslintConfigTemplate)();
51
60
  files['knip.ts'] = (0, lint_template_1.knipConfigTemplate)();
52
61
  files['.secretlintrc.json'] = (0, lint_template_1.secretlintConfigTemplate)();
53
62
  files['lefthook.yml'] = (0, lint_template_1.lefthookConfigTemplate)();
54
63
  }
64
+ if (ctx.e2eFramework === 'playwright') {
65
+ files['playwright.config.ts'] = (0, e2e_playwright_template_1.playwrightConfigTemplate)(ctx);
66
+ files['e2e/helpers/index.ts'] = (0, e2e_playwright_template_1.playwrightHelpersTemplate)();
67
+ files['e2e/example.spec.ts'] = (0, e2e_playwright_template_1.playwrightExampleSpecTemplate)();
68
+ }
69
+ else if (ctx.e2eFramework === 'cypress') {
70
+ files['cypress.config.ts'] = (0, e2e_cypress_template_1.cypressConfigTemplate)(ctx);
71
+ files['cypress/support/e2e.ts'] = (0, e2e_cypress_template_1.cypressSupportE2eTemplate)();
72
+ files['cypress/support/commands.ts'] = (0, e2e_cypress_template_1.cypressSupportCommandsTemplate)();
73
+ files['cypress/e2e/example.cy.ts'] = (0, e2e_cypress_template_1.cypressExampleSpecTemplate)();
74
+ }
55
75
  return files;
56
76
  }
@@ -31,6 +31,13 @@ function createProgram(onInit = defaultInitHandler) {
31
31
  .addOption(new commander_1.Option('--ssr', 'Клиент + сервер').conflicts('clientOnly'))
32
32
  .addOption(new commander_1.Option('--code-loader <loader>', 'Транспилятор').choices(['swc', 'babel', 'tsc']))
33
33
  .addOption(new commander_1.Option('--test-runner <runner>', 'Тест-раннер').choices(['jest', 'vitest']))
34
+ .addOption(new commander_1.Option('--e2e-framework <framework>', 'e2e фреймворк').choices([
35
+ 'cypress',
36
+ 'playwright',
37
+ 'none',
38
+ ]))
39
+ .option('--router', 'Подключить React Router')
40
+ .option('--no-router', 'Без React Router')
34
41
  .option('--css-modules', 'CSS-модули')
35
42
  .option('--no-css-modules', 'Обычный css')
36
43
  .option('--client-port <port>', 'Порт dev-сервера', (value) => Number(value))
@@ -77,6 +84,9 @@ function mapOptsToFlags(opts) {
77
84
  if (typeof opts.testRunner === 'string') {
78
85
  flags.testRunner = opts.testRunner;
79
86
  }
87
+ if (typeof opts.e2eFramework === 'string') {
88
+ flags.e2eFramework = opts.e2eFramework;
89
+ }
80
90
  if (typeof opts.cssModules === 'boolean') {
81
91
  flags.cssModules = opts.cssModules;
82
92
  }
@@ -98,6 +108,9 @@ function mapOptsToFlags(opts) {
98
108
  if (typeof opts.reactCompiler === 'boolean') {
99
109
  flags.reactCompiler = opts.reactCompiler;
100
110
  }
111
+ if (typeof opts.router === 'boolean') {
112
+ flags.useRouter = opts.router;
113
+ }
101
114
  if (typeof opts.lint === 'boolean') {
102
115
  flags.useLint = opts.lint;
103
116
  }
@@ -1,4 +1,4 @@
1
- import { type CodeLoader, type InitAnswers, type TestRunner } from './types';
1
+ import { type CodeLoader, type E2eFramework, type InitAnswers, type TestRunner } from './types';
2
2
  export type CliFlags = {
3
3
  yes?: boolean;
4
4
  force?: boolean;
@@ -7,6 +7,8 @@ export type CliFlags = {
7
7
  clientOnly?: boolean;
8
8
  codeLoader?: CodeLoader;
9
9
  testRunner?: TestRunner;
10
+ e2eFramework?: E2eFramework;
11
+ useRouter?: boolean;
10
12
  cssModules?: boolean;
11
13
  clientServerPort?: number;
12
14
  serverPort?: number;
package/build/defaults.js CHANGED
@@ -10,6 +10,8 @@ function defaultAnswers(name) {
10
10
  clientOnly: false,
11
11
  codeLoader: 'swc',
12
12
  testRunner: 'jest',
13
+ e2eFramework: 'none',
14
+ useRouter: false,
13
15
  cssModules: true,
14
16
  clientServerPort: 8080,
15
17
  serverPort: 3000,
@@ -27,6 +29,8 @@ const ANSWER_FLAG_KEYS = [
27
29
  'clientOnly',
28
30
  'codeLoader',
29
31
  'testRunner',
32
+ 'e2eFramework',
33
+ 'useRouter',
30
34
  'cssModules',
31
35
  'clientServerPort',
32
36
  'serverPort',
@@ -44,7 +48,7 @@ function hasAnswerFlags(flags) {
44
48
  function answersFromFlags(defaultName, flags) {
45
49
  var _a, _b;
46
50
  const base = defaultAnswers(((_a = flags.name) === null || _a === void 0 ? void 0 : _a.trim()) || defaultName);
47
- return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, base), (flags.useRtk === undefined ? {} : { useRtk: flags.useRtk })), (flags.clientOnly === undefined ? {} : { clientOnly: flags.clientOnly })), (flags.codeLoader === undefined ? {} : { codeLoader: flags.codeLoader })), (flags.testRunner === undefined ? {} : { testRunner: flags.testRunner })), (flags.cssModules === undefined ? {} : { cssModules: flags.cssModules })), (flags.clientServerPort === undefined
51
+ return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, base), (flags.useRtk === undefined ? {} : { useRtk: flags.useRtk })), (flags.clientOnly === undefined ? {} : { clientOnly: flags.clientOnly })), (flags.codeLoader === undefined ? {} : { codeLoader: flags.codeLoader })), (flags.testRunner === undefined ? {} : { testRunner: flags.testRunner })), (flags.e2eFramework === undefined ? {} : { e2eFramework: flags.e2eFramework })), (flags.useRouter === undefined ? {} : { useRouter: flags.useRouter })), (flags.cssModules === undefined ? {} : { cssModules: flags.cssModules })), (flags.clientServerPort === undefined
48
52
  ? {}
49
53
  : { clientServerPort: flags.clientServerPort })), (flags.serverPort === undefined ? {} : { serverPort: flags.serverPort })), (flags.dockerRegistry === undefined ? {} : { dockerRegistry: flags.dockerRegistry })), (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: (((_b = flags.name) === null || _b === void 0 ? void 0 : _b.trim()) || defaultName).trim() });
50
54
  }
@@ -57,6 +57,29 @@ function getQuestions(defaultName, prefill = {}) {
57
57
  { title: 'Vitest', value: 'vitest', description: 'Быстрее, на базе Vite' },
58
58
  ],
59
59
  },
60
+ {
61
+ type: unlessAnswered('e2eFramework', 'select'),
62
+ name: 'e2eFramework',
63
+ message: 'e2e фреймворк',
64
+ initial: 2,
65
+ choices: [
66
+ { title: 'Cypress', value: 'cypress', description: 'классический e2e фреймворк' },
67
+ {
68
+ title: 'Playwright',
69
+ value: 'playwright',
70
+ description: 'Быстрый и современный e2e фреймворк',
71
+ },
72
+ { title: 'Без e2e', value: 'none', description: 'Не подключать e2e' },
73
+ ],
74
+ },
75
+ {
76
+ type: unlessAnswered('useRouter', 'toggle'),
77
+ name: 'useRouter',
78
+ message: 'Подключить React Router',
79
+ initial: false,
80
+ active: 'да',
81
+ inactive: 'нет',
82
+ },
60
83
  {
61
84
  type: unlessAnswered('cssModules', 'toggle'),
62
85
  name: 'cssModules',
package/build/run.js CHANGED
@@ -137,6 +137,9 @@ function mergePromptAnswers(base, answers) {
137
137
  if (answers.testRunner !== undefined) {
138
138
  merged.testRunner = answers.testRunner;
139
139
  }
140
+ if (answers.e2eFramework !== undefined) {
141
+ merged.e2eFramework = answers.e2eFramework;
142
+ }
140
143
  if (answers.cssModules !== undefined) {
141
144
  merged.cssModules = Boolean(answers.cssModules);
142
145
  }
@@ -158,6 +161,9 @@ function mergePromptAnswers(base, answers) {
158
161
  if (answers.reactCompiler !== undefined) {
159
162
  merged.reactCompiler = Boolean(answers.reactCompiler);
160
163
  }
164
+ if (answers.useRouter !== undefined) {
165
+ merged.useRouter = Boolean(answers.useRouter);
166
+ }
161
167
  if (answers.useLint !== undefined) {
162
168
  merged.useLint = Boolean(answers.useLint);
163
169
  }
@@ -178,6 +184,8 @@ function printSuccess(context, targetDir, fileCount) {
178
184
  chalk_1.default.dim(context.clientOnly ? 'clientOnly' : 'SSR'),
179
185
  chalk_1.default.dim(context.codeLoader),
180
186
  chalk_1.default.dim(context.testRunner),
187
+ ...(context.e2eFramework !== 'none' ? [chalk_1.default.dim(context.e2eFramework)] : []),
188
+ ...(context.useRouter ? [chalk_1.default.dim('router')] : []),
181
189
  ...(context.useLint ? [chalk_1.default.dim('lint')] : []),
182
190
  ].join(chalk_1.default.dim(' · '));
183
191
  console.log();
@@ -5,6 +5,16 @@ function tsString(value) {
5
5
  return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
6
6
  }
7
7
  function appComponentTemplate(ctx) {
8
+ if (ctx.useRouter) {
9
+ return `import React from 'react';
10
+
11
+ import { AppRoutes } from '../routes';
12
+
13
+ export function App() {
14
+ return <AppRoutes />;
15
+ }
16
+ `;
17
+ }
8
18
  const styleImport = ctx.cssModules
9
19
  ? "import styles from './app.module.css';"
10
20
  : "import './app.css';";
@@ -2,12 +2,29 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.appStylesFileName = appStylesFileName;
4
4
  exports.appStylesTemplate = appStylesTemplate;
5
+ const CORE_COMPONENTS_VARS_IMPORT = "@import '@alfalab/core-components/vars';";
5
6
  function appStylesFileName(ctx) {
6
7
  return ctx.cssModules ? 'app.module.css' : 'app.css';
7
8
  }
8
9
  function appStylesTemplate(ctx) {
9
10
  if (ctx.cssModules) {
10
- return `.root {
11
+ const nav = ctx.useRouter
12
+ ? `
13
+ .nav {
14
+ display: flex;
15
+ align-items: center;
16
+ padding: var(--gap-16);
17
+ }
18
+
19
+ .navLink {
20
+ color: var(--color-light-text-primary);
21
+ text-decoration: none;
22
+ }
23
+ `
24
+ : '';
25
+ return `${CORE_COMPONENTS_VARS_IMPORT}
26
+
27
+ .root {
11
28
  padding: var(--gap-16);
12
29
  }
13
30
 
@@ -15,9 +32,25 @@ function appStylesTemplate(ctx) {
15
32
  margin: var(--gap-0) var(--gap-0) var(--gap-12);
16
33
  @mixin headline_small;
17
34
  }
18
- `;
35
+ ${nav}`;
19
36
  }
20
- return `.app {
37
+ const nav = ctx.useRouter
38
+ ? `
39
+ .app__nav {
40
+ display: flex;
41
+ align-items: center;
42
+ padding: var(--gap-16);
43
+ }
44
+
45
+ .app__nav-link {
46
+ color: var(--color-light-text-primary);
47
+ text-decoration: none;
48
+ }
49
+ `
50
+ : '';
51
+ return `${CORE_COMPONENTS_VARS_IMPORT}
52
+
53
+ .app {
21
54
  padding: var(--gap-16);
22
55
  }
23
56
 
@@ -25,5 +58,5 @@ function appStylesTemplate(ctx) {
25
58
  margin: var(--gap-0) var(--gap-0) var(--gap-12);
26
59
  @mixin headline_small;
27
60
  }
28
- `;
61
+ ${nav}`;
29
62
  }
@@ -9,14 +9,27 @@ const HMR_BLOCK = `if (process.env.NODE_ENV !== 'production' && module.hot) {
9
9
  });
10
10
  }
11
11
  `;
12
+ function wrapWithRouter(jsx, ctx) {
13
+ if (!ctx.useRouter) {
14
+ return jsx;
15
+ }
16
+ return `<BrowserRouter>${jsx}</BrowserRouter>`;
17
+ }
12
18
  function clientEntryTemplate(ctx) {
13
19
  return ctx.clientOnly ? clientOnlyEntryTemplate(ctx) : ssrEntryTemplate(ctx);
14
20
  }
21
+ function routerImport(ctx) {
22
+ return ctx.useRouter ? "\nimport { BrowserRouter } from 'react-router-dom';" : '';
23
+ }
15
24
  function ssrEntryTemplate(ctx) {
16
25
  if (ctx.useRtk) {
26
+ const appTree = wrapWithRouter(`
27
+ <Provider store={store}>
28
+ <App />
29
+ </Provider>`, ctx);
17
30
  return `import React from 'react';
18
31
  import { hydrateRoot } from 'react-dom/client';
19
- import { Provider } from 'react-redux';
32
+ import { Provider } from 'react-redux';${routerImport(ctx)}
20
33
 
21
34
  import { App } from './components/app';
22
35
  import { makeStore, type RootState } from './store';
@@ -31,32 +44,30 @@ const store = makeStore(window.__PRELOADED_STATE__);
31
44
  const targetElement = document.getElementById('react-app');
32
45
 
33
46
  const root = hydrateRoot(
34
- targetElement!,
35
- <Provider store={store}>
36
- <App />
37
- </Provider>,
47
+ targetElement!,${appTree},
38
48
  );
39
49
 
40
50
  function render(AppComponent: typeof App) {
41
- root.render(
51
+ root.render(${wrapWithRouter(`
42
52
  <Provider store={store}>
43
53
  <AppComponent />
44
- </Provider>,
54
+ </Provider>`, ctx)},
45
55
  );
46
56
  }
47
57
 
48
58
  ${HMR_BLOCK}`;
49
59
  }
60
+ const appTree = wrapWithRouter('<App />', ctx);
50
61
  return `import React from 'react';
51
- import { hydrateRoot } from 'react-dom/client';
62
+ import { hydrateRoot } from 'react-dom/client';${routerImport(ctx)}
52
63
 
53
64
  import { App } from './components/app';
54
65
 
55
66
  const targetElement = document.getElementById('react-app');
56
- const root = hydrateRoot(targetElement!, <App />);
67
+ const root = hydrateRoot(targetElement!, ${appTree});
57
68
 
58
69
  function render(AppComponent: typeof App) {
59
- root.render(<AppComponent />);
70
+ root.render(${wrapWithRouter('<AppComponent />', ctx)});
60
71
  }
61
72
 
62
73
  ${HMR_BLOCK}`;
@@ -65,7 +76,7 @@ function clientOnlyEntryTemplate(ctx) {
65
76
  if (ctx.useRtk) {
66
77
  return `import React from 'react';
67
78
  import { createRoot } from 'react-dom/client';
68
- import { Provider } from 'react-redux';
79
+ import { Provider } from 'react-redux';${routerImport(ctx)}
69
80
 
70
81
  import { App } from './components/app';
71
82
  import { makeStore } from './store';
@@ -75,10 +86,10 @@ const targetElement = document.getElementById('react-app');
75
86
  const root = createRoot(targetElement!);
76
87
 
77
88
  function render(AppComponent: typeof App) {
78
- root.render(
89
+ root.render(${wrapWithRouter(`
79
90
  <Provider store={store}>
80
91
  <AppComponent />
81
- </Provider>,
92
+ </Provider>`, ctx)},
82
93
  );
83
94
  }
84
95
 
@@ -87,7 +98,7 @@ render(App);
87
98
  ${HMR_BLOCK}`;
88
99
  }
89
100
  return `import React from 'react';
90
- import { createRoot } from 'react-dom/client';
101
+ import { createRoot } from 'react-dom/client';${routerImport(ctx)}
91
102
 
92
103
  import { App } from './components/app';
93
104
 
@@ -95,7 +106,7 @@ const targetElement = document.getElementById('react-app');
95
106
  const root = createRoot(targetElement!);
96
107
 
97
108
  function render(AppComponent: typeof App) {
98
- root.render(<AppComponent />);
109
+ root.render(${wrapWithRouter('<AppComponent />', ctx)});
99
110
  }
100
111
 
101
112
  render(App);
@@ -0,0 +1,5 @@
1
+ import { type TemplateContext } from '../types';
2
+ export declare function cypressConfigTemplate(ctx: TemplateContext): string;
3
+ export declare function cypressSupportE2eTemplate(): string;
4
+ export declare function cypressSupportCommandsTemplate(): string;
5
+ export declare function cypressExampleSpecTemplate(): string;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cypressConfigTemplate = cypressConfigTemplate;
4
+ exports.cypressSupportE2eTemplate = cypressSupportE2eTemplate;
5
+ exports.cypressSupportCommandsTemplate = cypressSupportCommandsTemplate;
6
+ exports.cypressExampleSpecTemplate = cypressExampleSpecTemplate;
7
+ function baseUrl(ctx) {
8
+ return `http://localhost:${ctx.clientServerPort}`;
9
+ }
10
+ function cypressConfigTemplate(ctx) {
11
+ return `import { defineConfig } from 'cypress';
12
+
13
+ export default defineConfig({
14
+ video: false,
15
+ e2e: {
16
+ baseUrl: '${baseUrl(ctx)}',
17
+ specPattern: 'cypress/e2e/**/*.cy.{ts,tsx}',
18
+ supportFile: 'cypress/support/e2e.ts',
19
+ },
20
+ });
21
+ `;
22
+ }
23
+ function cypressSupportE2eTemplate() {
24
+ return `import './commands';
25
+ `;
26
+ }
27
+ function cypressSupportCommandsTemplate() {
28
+ return `// Кастомные команды Cypress.
29
+ // Пример:
30
+ // Cypress.Commands.add('login', (email, password) => { ... })
31
+
32
+ export {};
33
+
34
+ declare global {
35
+ // eslint-disable-next-line @typescript-eslint/no-namespace
36
+ namespace Cypress {
37
+ interface Chainable {
38
+ // login(email: string, password: string): Chainable<void>;
39
+ }
40
+ }
41
+ }
42
+ `;
43
+ }
44
+ function cypressExampleSpecTemplate() {
45
+ return `describe('home page', () => {
46
+ it('loads', () => {
47
+ cy.visit('/');
48
+ cy.get('body').should('be.visible');
49
+ });
50
+ });
51
+ `;
52
+ }
@@ -0,0 +1,4 @@
1
+ import { type TemplateContext } from '../types';
2
+ export declare function playwrightConfigTemplate(ctx: TemplateContext): string;
3
+ export declare function playwrightHelpersTemplate(): string;
4
+ export declare function playwrightExampleSpecTemplate(): string;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.playwrightConfigTemplate = playwrightConfigTemplate;
4
+ exports.playwrightHelpersTemplate = playwrightHelpersTemplate;
5
+ exports.playwrightExampleSpecTemplate = playwrightExampleSpecTemplate;
6
+ function baseUrl(ctx) {
7
+ return `http://localhost:${ctx.clientServerPort}`;
8
+ }
9
+ function playwrightConfigTemplate(ctx) {
10
+ const url = baseUrl(ctx);
11
+ return `import { defineConfig, devices } from '@playwright/test';
12
+
13
+ export default defineConfig({
14
+ testDir: './e2e',
15
+ fullyParallel: true,
16
+ forbidOnly: !!process.env.CI,
17
+ retries: process.env.CI ? 2 : 0,
18
+ workers: process.env.CI ? 1 : undefined,
19
+ reporter: 'html',
20
+ use: {
21
+ baseURL: '${url}',
22
+ trace: 'on-first-retry',
23
+ },
24
+ projects: [
25
+ {
26
+ name: 'chromium',
27
+ use: { ...devices['Desktop Chrome'] },
28
+ },
29
+ ],
30
+ webServer: {
31
+ command: 'yarn start',
32
+ url: '${url}',
33
+ reuseExistingServer: !process.env.CI,
34
+ },
35
+ });
36
+ `;
37
+ }
38
+ function playwrightHelpersTemplate() {
39
+ return `import { type Page } from '@playwright/test';
40
+
41
+ export async function gotoHome(page: Page): Promise<void> {
42
+ await page.goto('/');
43
+ }
44
+ `;
45
+ }
46
+ function playwrightExampleSpecTemplate() {
47
+ return `import { expect, test } from '@playwright/test';
48
+
49
+ import { gotoHome } from './helpers';
50
+
51
+ test('home page loads', async ({ page }) => {
52
+ await gotoHome(page);
53
+ await expect(page.locator('body')).toBeVisible();
54
+ });
55
+ `;
56
+ }
@@ -1,5 +1,5 @@
1
1
  import { type TemplateContext } from '../types';
2
- export declare function gitignoreTemplate(): string;
2
+ export declare function gitignoreTemplate(ctx?: TemplateContext): string;
3
3
  export declare function globalDefinitionsTemplate(): string;
4
4
  export declare function polyfillsTemplate(): string;
5
5
  export declare function yarnrcTemplate(): string;
@@ -5,7 +5,23 @@ exports.globalDefinitionsTemplate = globalDefinitionsTemplate;
5
5
  exports.polyfillsTemplate = polyfillsTemplate;
6
6
  exports.yarnrcTemplate = yarnrcTemplate;
7
7
  exports.readmeTemplate = readmeTemplate;
8
- function gitignoreTemplate() {
8
+ function gitignoreTemplate(ctx) {
9
+ let e2eIgnores = '';
10
+ if ((ctx === null || ctx === void 0 ? void 0 : ctx.e2eFramework) === 'playwright') {
11
+ e2eIgnores = `
12
+ test-results/
13
+ playwright-report/
14
+ blob-report/
15
+ playwright/.cache/
16
+ `;
17
+ }
18
+ else if ((ctx === null || ctx === void 0 ? void 0 : ctx.e2eFramework) === 'cypress') {
19
+ e2eIgnores = `
20
+ cypress/videos/
21
+ cypress/screenshots/
22
+ cypress/downloads/
23
+ `;
24
+ }
9
25
  return `node_modules
10
26
  .build
11
27
  build.tar
@@ -18,7 +34,7 @@ build.tar
18
34
  !.yarn/releases
19
35
  !.yarn/sdks
20
36
  !.yarn/versions
21
- `;
37
+ ${e2eIgnores}`;
22
38
  }
23
39
  function globalDefinitionsTemplate() {
24
40
  return `declare module '*.png' {
@@ -62,6 +78,41 @@ function readmeTemplate(ctx) {
62
78
  yarn lint - eslint, stylelint, prettier, knip, secretlint
63
79
  yarn lint:fix - автофикс eslint/stylelint + format
64
80
  yarn format - prettier
81
+ `
82
+ : '';
83
+ let e2eSection = '';
84
+ if (ctx.e2eFramework === 'playwright') {
85
+ e2eSection = `
86
+ ## E2E (Playwright)
87
+
88
+ Перед первым запуском установите браузеры:
89
+
90
+ yarn playwright install
91
+
92
+ Команды:
93
+
94
+ yarn e2e - headless-прогон
95
+ yarn e2e:ui - UI-режим Playwright
96
+
97
+ Dev-сервер поднимается автоматически через webServer в playwright.config.ts.
98
+ `;
99
+ }
100
+ else if (ctx.e2eFramework === 'cypress') {
101
+ e2eSection = `
102
+ ## E2E (Cypress)
103
+
104
+ Сначала запустите приложение (\`yarn start\`), затем в другом терминале:
105
+
106
+ yarn e2e - headless-прогон
107
+ yarn e2e:open - интерактивный Cypress
108
+ `;
109
+ }
110
+ const routerSection = ctx.useRouter
111
+ ? `
112
+ ## Маршруты
113
+
114
+ / - главная (Home)
115
+ /about - о проекте (About)
65
116
  `
66
117
  : '';
67
118
  return `# ${ctx.name}
@@ -76,5 +127,5 @@ function readmeTemplate(ctx) {
76
127
  ## Установка
77
128
 
78
129
  yarn install
79
- `;
130
+ ${routerSection}${e2eSection}`;
80
131
  }
@@ -31,6 +31,14 @@ function packageJsonTemplate(ctx) {
31
31
  scripts['lint:fix'] =
32
32
  'yarn lint:styles --fix && yarn lint:scripts --fix && yarn format && yarn lint:secrets';
33
33
  }
34
+ if (ctx.e2eFramework === 'playwright') {
35
+ scripts.e2e = 'playwright test';
36
+ scripts['e2e:ui'] = 'playwright test --ui';
37
+ }
38
+ else if (ctx.e2eFramework === 'cypress') {
39
+ scripts.e2e = 'cypress run';
40
+ scripts['e2e:open'] = 'cypress open';
41
+ }
34
42
  const pkg = {
35
43
  name: ctx.name,
36
44
  version: '0.1.0',
@@ -0,0 +1,5 @@
1
+ import { type TemplateContext } from '../types';
2
+ export declare function layoutTemplate(ctx: TemplateContext): string;
3
+ export declare function homePageTemplate(ctx: TemplateContext): string;
4
+ export declare function aboutPageTemplate(ctx: TemplateContext): string;
5
+ export declare function routesTemplate(): string;
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.layoutTemplate = layoutTemplate;
4
+ exports.homePageTemplate = homePageTemplate;
5
+ exports.aboutPageTemplate = aboutPageTemplate;
6
+ exports.routesTemplate = routesTemplate;
7
+ function tsString(value) {
8
+ return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
9
+ }
10
+ function layoutTemplate(ctx) {
11
+ const styleImport = ctx.cssModules
12
+ ? "import styles from './app.module.css';"
13
+ : "import './app.css';";
14
+ const navClass = ctx.cssModules ? '{styles.nav}' : "'app__nav'";
15
+ const linkClass = ctx.cssModules ? '{styles.navLink}' : "'app__nav-link'";
16
+ return `import React from 'react';
17
+ import { Link, Outlet } from 'react-router-dom';
18
+
19
+ ${styleImport}
20
+
21
+ export function Layout() {
22
+ return (
23
+ <div>
24
+ <nav className=${navClass}>
25
+ <Link to='/' className=${linkClass}>
26
+ Главная
27
+ </Link>
28
+ <span aria-hidden={true}> · </span>
29
+ <Link to='/about' className=${linkClass}>
30
+ О проекте
31
+ </Link>
32
+ </nav>
33
+ <Outlet />
34
+ </div>
35
+ );
36
+ }
37
+ `;
38
+ }
39
+ function homePageTemplate(ctx) {
40
+ const styleImport = ctx.cssModules
41
+ ? "import styles from '../components/app.module.css';"
42
+ : "import '../components/app.css';";
43
+ const rootClass = ctx.cssModules ? '{styles.root}' : "'app'";
44
+ const titleClass = ctx.cssModules ? '{styles.title}' : "'app__title'";
45
+ const appNameLiteral = tsString(ctx.name);
46
+ const coreImports = `import { Button } from '@alfalab/core-components/button';
47
+ import { Gap } from '@alfalab/core-components/gap';
48
+ import { Typography } from '@alfalab/core-components/typography';`;
49
+ if (ctx.useRtk) {
50
+ return `import React from 'react';
51
+
52
+ ${coreImports}
53
+
54
+ import { decrement, increment } from '../store/counter-slice';
55
+ import { useAppDispatch, useAppSelector } from '../store/hooks';
56
+
57
+ ${styleImport}
58
+
59
+ const appName = ${appNameLiteral};
60
+
61
+ export function HomePage() {
62
+ const count = useAppSelector((state) => state.counter.value);
63
+ const dispatch = useAppDispatch();
64
+
65
+ return (
66
+ <div className=${rootClass}>
67
+ <Typography.Title view='medium' tag='h1' className=${titleClass}>
68
+ {appName}
69
+ </Typography.Title>
70
+ <Gap size={16} />
71
+ <Typography.Text view='primary-medium'>Счетчик: {count}</Typography.Text>
72
+ <Gap size={16} />
73
+ <Button view='accent' size={48} onClick={() => dispatch(increment())}>
74
+ +1
75
+ </Button>{' '}
76
+ <Button view='secondary' size={48} onClick={() => dispatch(decrement())}>
77
+ -1
78
+ </Button>
79
+ </div>
80
+ );
81
+ }
82
+ `;
83
+ }
84
+ return `import React, { useState } from 'react';
85
+
86
+ ${coreImports}
87
+
88
+ ${styleImport}
89
+
90
+ const appName = ${appNameLiteral};
91
+
92
+ export function HomePage() {
93
+ const [count, setCount] = useState(0);
94
+
95
+ return (
96
+ <div className=${rootClass}>
97
+ <Typography.Title view='medium' tag='h1' className=${titleClass}>
98
+ {appName}
99
+ </Typography.Title>
100
+ <Gap size={16} />
101
+ <Typography.Text view='primary-medium'>Счетчик: {count}</Typography.Text>
102
+ <Gap size={16} />
103
+ <Button view='accent' size={48} onClick={() => setCount((prev) => prev + 1)}>
104
+ +1
105
+ </Button>{' '}
106
+ <Button view='secondary' size={48} onClick={() => setCount((prev) => prev - 1)}>
107
+ -1
108
+ </Button>
109
+ </div>
110
+ );
111
+ }
112
+ `;
113
+ }
114
+ function aboutPageTemplate(ctx) {
115
+ const styleImport = ctx.cssModules
116
+ ? "import styles from '../components/app.module.css';"
117
+ : "import '../components/app.css';";
118
+ const rootClass = ctx.cssModules ? '{styles.root}' : "'app'";
119
+ const titleClass = ctx.cssModules ? '{styles.title}' : "'app__title'";
120
+ return `import React from 'react';
121
+
122
+ import { Typography } from '@alfalab/core-components/typography';
123
+
124
+ ${styleImport}
125
+
126
+ export function AboutPage() {
127
+ return (
128
+ <div className=${rootClass}>
129
+ <Typography.Title view='medium' tag='h1' className=${titleClass}>
130
+ О проекте
131
+ </Typography.Title>
132
+ <Typography.Text view='primary-medium'>
133
+ Страница для React Router
134
+ </Typography.Text>
135
+ </div>
136
+ );
137
+ }
138
+ `;
139
+ }
140
+ function routesTemplate() {
141
+ return `import React from 'react';
142
+ import { Route, Routes } from 'react-router-dom';
143
+
144
+ import { Layout } from './components/layout';
145
+ import { AboutPage } from './pages/about';
146
+ import { HomePage } from './pages/home';
147
+
148
+ export function AppRoutes() {
149
+ return (
150
+ <Routes>
151
+ <Route element={<Layout />}>
152
+ <Route index={true} element={<HomePage />} />
153
+ <Route path='about' element={<AboutPage />} />
154
+ </Route>
155
+ </Routes>
156
+ );
157
+ }
158
+ `;
159
+ }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.serverEntryTemplate = serverEntryTemplate;
4
4
  function serverEntryTemplate(ctx) {
5
5
  const reduxImport = ctx.useRtk ? "\nimport { Provider } from 'react-redux';" : '';
6
+ const routerImport = ctx.useRouter ? "\nimport { StaticRouter } from 'react-router';" : '';
6
7
  const storeImport = ctx.useRtk ? "\nimport { makeStore } from '../client/store';" : '';
7
8
  const renderPageFn = ctx.useRtk
8
9
  ? `function renderPage(appHtml: string, assets: Assets, preloadedState: string): string {
@@ -18,24 +19,41 @@ function serverEntryTemplate(ctx) {
18
19
 
19
20
  return \`<!DOCTYPE html><html lang="ru"><head><meta charset="utf-8" /><base href="/" />\${css}</head><body><div id="react-app">\${appHtml}</div>\${js}</body></html>\`;
20
21
  }`;
22
+ const appJsx = (() => {
23
+ let inner = '<App />';
24
+ if (ctx.useRtk) {
25
+ inner = `<Provider store={store}>
26
+ ${inner}
27
+ </Provider>`;
28
+ }
29
+ if (ctx.useRouter) {
30
+ inner = `<StaticRouter location={location}>
31
+ ${inner}
32
+ </StaticRouter>`;
33
+ }
34
+ return inner;
35
+ })();
21
36
  const handlerBody = ctx.useRtk
22
37
  ? ` const assets = await readAssetsManifest();
23
38
  const store = makeStore();
24
39
  const appHtml = renderToString(
25
- <Provider store={store}>
26
- <App />
27
- </Provider>,
40
+ ${appJsx},
28
41
  );
29
42
  // Экранируем символ "<", чтобы через состояние нельзя было внедрить теги в HTML
30
43
  const preloadedState = JSON.stringify(store.getState()).replace(/</g, '\\\\u003c');
31
44
 
32
45
  return renderPage(appHtml, assets, preloadedState);`
33
46
  : ` const assets = await readAssetsManifest();
34
- const appHtml = renderToString(<App />);
47
+ const appHtml = renderToString(${appJsx});
35
48
 
36
49
  return renderPage(appHtml, assets);`;
50
+ const routePath = ctx.useRouter ? '/{path*}' : '/';
51
+ const locationLine = ctx.useRouter
52
+ ? `
53
+ const location = request.url.pathname;`
54
+ : '';
37
55
  return `import React from 'react';
38
- import { renderToString } from 'react-dom/server';${reduxImport}
56
+ import { renderToString } from 'react-dom/server';${reduxImport}${routerImport}
39
57
  import Hapi from '@hapi/hapi';
40
58
  import Inert from '@hapi/inert';
41
59
  import path from 'node:path';
@@ -77,8 +95,8 @@ async function start() {
77
95
 
78
96
  server.route({
79
97
  method: 'GET',
80
- path: '/',
81
- handler: async () => {
98
+ path: '${routePath}',
99
+ handler: async (${ctx.useRouter ? 'request' : ''}) => {${locationLine}
82
100
  ${handlerBody}
83
101
  },
84
102
  });
@@ -2,9 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.tsconfigTemplate = tsconfigTemplate;
4
4
  function tsconfigTemplate(ctx) {
5
- const types = ctx.testRunner === 'jest'
6
- ? '["jest", "node", "webpack-env"]'
7
- : '["node", "webpack-env"]';
5
+ const types = ctx.testRunner === 'jest' ? '["jest", "node", "webpack-env"]' : '["node", "webpack-env"]';
8
6
  return `{
9
7
  "extends": "arui-scripts/tsconfig.json",
10
8
  "include": ["global-definitions.d.ts", "src/**/*.ts", "src/**/*.tsx"],
package/build/types.d.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  export type CodeLoader = 'swc' | 'babel' | 'tsc';
2
2
  export type TestRunner = 'jest' | 'vitest';
3
+ export type E2eFramework = 'cypress' | 'playwright' | 'none';
3
4
  export type InitAnswers = {
4
5
  name: string;
5
6
  useRtk: boolean;
6
7
  clientOnly: boolean;
7
8
  codeLoader: CodeLoader;
8
9
  testRunner: TestRunner;
10
+ e2eFramework: E2eFramework;
11
+ useRouter: boolean;
9
12
  cssModules: boolean;
10
13
  clientServerPort: number;
11
14
  serverPort: number;
@@ -22,6 +25,8 @@ export type TemplateContext = {
22
25
  clientOnly: boolean;
23
26
  codeLoader: CodeLoader;
24
27
  testRunner: TestRunner;
28
+ e2eFramework: E2eFramework;
29
+ useRouter: boolean;
25
30
  cssModules: boolean;
26
31
  clientServerPort: number;
27
32
  serverPort: number;
@@ -1,4 +1,4 @@
1
1
  /** Версия arui-scripts, которую scaffold кладёт в package.json нового проекта.
2
2
  * Генерируется скриптом scripts/sync-arui-scripts-version.js при сборке.
3
3
  */
4
- export declare const DEFAULT_ARUI_SCRIPTS_VERSION = "23.3.1";
4
+ export declare const DEFAULT_ARUI_SCRIPTS_VERSION = "23.5.1";
package/build/versions.js CHANGED
@@ -4,4 +4,4 @@ exports.DEFAULT_ARUI_SCRIPTS_VERSION = void 0;
4
4
  /** Версия arui-scripts, которую scaffold кладёт в package.json нового проекта.
5
5
  * Генерируется скриптом scripts/sync-arui-scripts-version.js при сборке.
6
6
  */
7
- exports.DEFAULT_ARUI_SCRIPTS_VERSION = '23.3.1';
7
+ exports.DEFAULT_ARUI_SCRIPTS_VERSION = '23.5.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-arui-scripts-app",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "CLI для генерации нового react приложения на arui-scripts",
5
5
  "license": "MPL-2.0",
6
6
  "repository": {