create-alistt69-kit 0.1.8 → 0.1.11

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/README.md CHANGED
@@ -1,9 +1,9 @@
1
- <table width="100%" style="border: none; padding: 0; margin: 0">
2
- <tr style="border: none; padding: 0; margin: 0">
3
- <td width="190" align="center" style="border: none; padding: 0; margin: 0">
1
+ <table width="100%">
2
+ <tr>
3
+ <td width="190" align="center">
4
4
  <img src="./assets/create-alistt69-kit-logo.svg" alt="Logo" width="170" height="170" style="margin-top: 50px;" />
5
5
  </td>
6
- <td style="border: none; padding: 10; margin: 0">
6
+ <td>
7
7
  <h1>create-alistt69-kit</h1>
8
8
 
9
9
  > **One command. Zero config fatigue.**
@@ -12,6 +12,7 @@
12
12
  [![npm version](https://img.shields.io/npm/v/create-alistt69-kit.svg)](https://www.npmjs.com/package/create-alistt69-kit)
13
13
  [![Node.js Version](https://img.shields.io/badge/node-%3E%3D18.18-brightgreen)](https://nodejs.org/)
14
14
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
15
+ [![CI](https://github.com/alistt69/create-alistt69-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/alistt69/create-alistt69-kit/actions/workflows/ci.yml)
15
16
  </td>
16
17
  </tr>
17
18
  </table>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-alistt69-kit",
3
- "version": "0.1.8",
3
+ "version": "0.1.11",
4
4
  "description": "Opinionated React + TypeScript + Webpack project generator by alistt69",
5
5
  "keywords": [
6
6
  "create",
@@ -33,7 +33,7 @@
33
33
  ],
34
34
  "scripts": {
35
35
  "dev": "node ./bin/index.js",
36
- "smoke": "node ./scripts/smoke.mjs"
36
+ "smoke": "node ./scripts/smoke/index.mjs"
37
37
  },
38
38
  "engines": {
39
39
  "node": ">=18.18.0"
@@ -8,7 +8,7 @@ export async function applyFeatures({ projectPath, selectedFeatureIds }) {
8
8
  throw new Error(`Unknown feature: ${featureId}`);
9
9
  }
10
10
 
11
- await feature.apply({
11
+ await feature.applyFeature({
12
12
  projectPath,
13
13
  });
14
14
  }
@@ -9,35 +9,11 @@ import {
9
9
  text,
10
10
  } from '@clack/prompts';
11
11
  import process from 'node:process';
12
+ import { defaultFeatureIds, featurePromptOptions, availableFeatureIdSet } from '../features/index.js';
12
13
  import { format } from '../utils/console-format.js';
13
14
  import { allowedPackageManagers } from '../utils/package-manager.js';
14
15
 
15
- const availableFeatures = [
16
- {
17
- value: 'eslint',
18
- label: 'ESLint + Stylistic',
19
- hint: 'JS/TS/React linting',
20
- },
21
- {
22
- value: 'stylelint',
23
- label: 'Stylelint',
24
- hint: 'SCSS/CSS linting',
25
- },
26
- {
27
- value: 'autoprefixer',
28
- label: 'Autoprefixer',
29
- hint: 'PostCSS vendor prefixes',
30
- },
31
- {
32
- value: 'react-router',
33
- label: 'React Router DOM',
34
- hint: 'Routing + FSD-like app/pages/shared',
35
- },
36
- ];
37
-
38
- const defaultFeatureIds = availableFeatures.map((feature) => feature.value);
39
16
  const defaultPackageManager = 'npm';
40
- const availableFeatureIdSet = new Set(defaultFeatureIds);
41
17
 
42
18
  function handleCancel(value) {
43
19
  if (!isCancel(value)) {
@@ -130,7 +106,7 @@ export async function collectProjectInfo(cliArgs = {}) {
130
106
 
131
107
  selectedFeatureIds = handleCancel(await multiselect({
132
108
  message: 'Remove unnecessary features',
133
- options: availableFeatures,
109
+ options: featurePromptOptions,
134
110
  initialValues: defaultFeatureIds,
135
111
  required: false,
136
112
  }));
@@ -56,6 +56,7 @@ export async function createProject(cliArgs = {}) {
56
56
  projectName,
57
57
  selectedFeatureIds,
58
58
  packageManager,
59
+ shouldInstallDependencies,
59
60
  });
60
61
  progress.stop('README generated');
61
62
 
@@ -1,9 +1,43 @@
1
1
  import { writeFile } from 'node:fs/promises';
2
2
  import { resolve } from 'node:path';
3
+
3
4
  import { featuresById } from '../features/index.js';
4
5
  import { readPackageJson } from '../utils/package-json.js';
5
6
  import { getRunScriptCommand } from '../utils/package-manager.js';
6
7
 
8
+ const defaultTooling = [
9
+ {
10
+ title: 'React',
11
+ docs: 'https://react.dev/',
12
+ description: 'UI library',
13
+ },
14
+ {
15
+ title: 'TypeScript',
16
+ docs: 'https://www.typescriptlang.org/',
17
+ description: 'Static typing',
18
+ },
19
+ {
20
+ title: 'Webpack',
21
+ docs: 'https://webpack.js.org/',
22
+ description: 'Bundling and build pipeline',
23
+ },
24
+ {
25
+ title: 'SCSS Modules',
26
+ docs: 'https://github.com/css-modules/css-modules',
27
+ description: 'Scoped styling',
28
+ },
29
+ {
30
+ title: 'SVGR',
31
+ docs: 'https://react-svgr.com/',
32
+ description: 'SVGs as React components',
33
+ },
34
+ {
35
+ title: 'Webpack Bundle Analyzer',
36
+ docs: 'https://github.com/webpack-contrib/webpack-bundle-analyzer',
37
+ description: 'Bundle size inspection',
38
+ },
39
+ ];
40
+
7
41
  const scriptDescriptions = {
8
42
  dev: 'Start development server',
9
43
  start: 'Start development server',
@@ -17,6 +51,22 @@ const scriptDescriptions = {
17
51
  'lint:styles:fix': 'Run Stylelint with autofix',
18
52
  };
19
53
 
54
+ function formatMarkdownList(items, fallback = '- None') {
55
+ if (items.length === 0) {
56
+ return fallback;
57
+ }
58
+
59
+ return items.join('\n');
60
+ }
61
+
62
+ function formatDefaultTooling() {
63
+ return formatMarkdownList(
64
+ defaultTooling.map(({ title, docs, description }) => (
65
+ `- [${title}](${docs}) — ${description}`
66
+ )),
67
+ );
68
+ }
69
+
20
70
  function formatFeatureList(selectedFeatureIds) {
21
71
  if (selectedFeatureIds.length === 0) {
22
72
  return '- None';
@@ -26,7 +76,13 @@ function formatFeatureList(selectedFeatureIds) {
26
76
  .map((featureId) => {
27
77
  const feature = featuresById.get(featureId);
28
78
 
29
- return `- ${feature?.title ?? featureId}`;
79
+ if (!feature) {
80
+ return `- ${featureId}`;
81
+ }
82
+
83
+ return feature.hint
84
+ ? `- ${feature.title} — ${feature.hint}`
85
+ : `- ${feature.title}`;
30
86
  })
31
87
  .join('\n');
32
88
  }
@@ -38,20 +94,57 @@ function formatScripts(packageManager, scripts) {
38
94
  return '_No scripts available._';
39
95
  }
40
96
 
41
- return entries.map(([scriptName]) => {
42
- const command = getRunScriptCommand(packageManager, scriptName);
43
- const description = scriptDescriptions[scriptName] ?? 'Project script';
97
+ return entries
98
+ .map(([scriptName]) => {
99
+ const command = getRunScriptCommand(packageManager, scriptName);
100
+ const description = scriptDescriptions[scriptName] ?? 'Project script';
44
101
 
45
- return `- \`${command}\` — ${description}`;
46
- }).join('\n');
102
+ return `- \`${command}\` — ${description}`;
103
+ })
104
+ .join('\n');
105
+ }
106
+
107
+ function formatQuickStart({
108
+ projectName,
109
+ packageManager,
110
+ shouldInstallDependencies,
111
+ }) {
112
+ const steps = [`cd ${projectName}`];
113
+
114
+ if (!shouldInstallDependencies) {
115
+ steps.push(packageManager === 'yarn' ? 'yarn' : `${packageManager} install`);
116
+ }
117
+
118
+ steps.push(getRunScriptCommand(packageManager, 'start'));
119
+
120
+ return [
121
+ shouldInstallDependencies
122
+ ? 'Dependencies are already installed.'
123
+ : 'Install dependencies first, then start the development server.',
124
+ '',
125
+ '```bash',
126
+ ...steps,
127
+ '```',
128
+ ].join('\n');
129
+ }
130
+
131
+ function formatProjectStructure() {
132
+ return [
133
+ '- `public/` — static assets and HTML template',
134
+ '- `src/` — application source code',
135
+ '- `src/app/` — app bootstrap, providers, entry-level setup',
136
+ '- `src/styles/` — global styles and shared styling layer',
137
+ '- `config/build/` — split webpack configuration',
138
+ ].join('\n');
47
139
  }
48
140
 
49
141
  export async function renderProjectReadme({
50
- projectPath,
51
- projectName,
52
- selectedFeatureIds,
53
- packageManager,
54
- }) {
142
+ projectPath,
143
+ projectName,
144
+ selectedFeatureIds,
145
+ packageManager,
146
+ shouldInstallDependencies,
147
+ }) {
55
148
  const packageJson = await readPackageJson(projectPath);
56
149
 
57
150
  const readmeContent = [
@@ -59,10 +152,30 @@ export async function renderProjectReadme({
59
152
  '',
60
153
  'Created with `create-alistt69-kit`.',
61
154
  '',
62
- '## Enabled features',
155
+ '## Overview',
156
+ '',
157
+ 'Starter project based on React + TypeScript + Webpack with optional tooling selected during scaffolding.',
158
+ '',
159
+ '## Included by default',
160
+ '',
161
+ formatDefaultTooling(),
162
+ '',
163
+ '## Selected optional features',
63
164
  '',
64
165
  formatFeatureList(selectedFeatureIds),
65
166
  '',
167
+ '## Quick start',
168
+ '',
169
+ formatQuickStart({
170
+ projectName,
171
+ packageManager,
172
+ shouldInstallDependencies,
173
+ }),
174
+ '',
175
+ '## Project structure',
176
+ '',
177
+ formatProjectStructure(),
178
+ '',
66
179
  '## Available scripts',
67
180
  '',
68
181
  formatScripts(packageManager, packageJson.scripts ?? {}),
@@ -1,23 +1,19 @@
1
- import { cp } from 'node:fs/promises';
2
1
  import { dirname, resolve } from 'node:path';
3
2
  import { fileURLToPath } from 'node:url';
4
- import { addDevDependencies } from '../../utils/package-json.js';
3
+
4
+ import { defineFeature } from '../define-feature.js';
5
5
 
6
6
  const currentFilePath = fileURLToPath(import.meta.url);
7
7
  const currentDirPath = dirname(currentFilePath);
8
8
 
9
- export const autoprefixerFeature = {
9
+ export const autoprefixerFeature = defineFeature({
10
10
  id: 'autoprefixer',
11
11
  title: 'Autoprefixer',
12
- apply: async ({ projectPath }) => {
13
- await addDevDependencies(projectPath, {
12
+ hint: 'PostCSS vendor prefixes',
13
+ packageJson: {
14
+ devDependencies: {
14
15
  autoprefixer: '^10.4.21',
15
- });
16
-
17
- const filesDirPath = resolve(currentDirPath, 'files');
18
-
19
- await cp(filesDirPath, projectPath, {
20
- recursive: true,
21
- });
16
+ },
22
17
  },
23
- };
18
+ copyFiles: resolve(currentDirPath, 'files'),
19
+ });
@@ -0,0 +1,33 @@
1
+ import { cp } from 'node:fs/promises';
2
+
3
+ import { patchPackageJson } from '../utils/package-json.js';
4
+
5
+ export function defineFeature({
6
+ id,
7
+ title,
8
+ hint,
9
+ packageJson,
10
+ apply,
11
+ copyFiles,
12
+ }) {
13
+ return {
14
+ id,
15
+ title,
16
+ hint,
17
+ async applyFeature(context) {
18
+ if (packageJson) {
19
+ await patchPackageJson(context.projectPath, packageJson);
20
+ }
21
+
22
+ if (copyFiles) {
23
+ await cp(copyFiles, context.projectPath, {
24
+ recursive: true,
25
+ });
26
+ }
27
+
28
+ if (apply) {
29
+ await apply(context);
30
+ }
31
+ },
32
+ };
33
+ }
@@ -1,35 +1,30 @@
1
- import { cp } from 'node:fs/promises';
2
1
  import { dirname, resolve } from 'node:path';
3
2
  import { fileURLToPath } from 'node:url';
4
- import { addDevDependencies, addScripts } from '../../utils/package-json.js';
3
+
4
+ import { defineFeature } from '../define-feature.js';
5
5
 
6
6
  const currentFilePath = fileURLToPath(import.meta.url);
7
7
  const currentDirPath = dirname(currentFilePath);
8
8
 
9
- export const eslintFeature = {
9
+ export const eslintFeature = defineFeature({
10
10
  id: 'eslint',
11
11
  title: 'ESLint + Stylistic',
12
- apply: async ({ projectPath }) => {
13
- await addDevDependencies(projectPath, {
14
- "eslint": "^9.0.0",
15
- "@eslint/js": "^9.0.0",
16
- "@stylistic/eslint-plugin": "^5.0.0",
17
- "typescript-eslint": "^8.0.0",
18
- "eslint-plugin-import": "^2.0.0",
19
- "eslint-plugin-react": "^7.0.0",
20
- "eslint-plugin-react-hooks": "^7.0.0",
21
- "eslint-plugin-unused-imports": "^4.0.0"
22
- });
23
-
24
- await addScripts(projectPath, {
12
+ hint: 'JS/TS/React linting',
13
+ packageJson: {
14
+ devDependencies: {
15
+ eslint: '^9.0.0',
16
+ '@eslint/js': '^9.0.0',
17
+ '@stylistic/eslint-plugin': '^5.0.0',
18
+ 'typescript-eslint': '^8.0.0',
19
+ 'eslint-plugin-import': '^2.0.0',
20
+ 'eslint-plugin-react': '^7.0.0',
21
+ 'eslint-plugin-react-hooks': '^7.0.0',
22
+ 'eslint-plugin-unused-imports': '^4.0.0',
23
+ },
24
+ scripts: {
25
25
  lint: 'eslint . --ext .js,.jsx,.ts,.tsx',
26
26
  'lint:fix': 'eslint . --ext .js,.jsx,.ts,.tsx --fix',
27
- });
28
-
29
- const filesDirPath = resolve(currentDirPath, 'files');
30
-
31
- await cp(filesDirPath, projectPath, {
32
- recursive: true,
33
- });
27
+ },
34
28
  },
35
- };
29
+ copyFiles: resolve(currentDirPath, 'files'),
30
+ });
@@ -12,4 +12,14 @@ export const features = [
12
12
 
13
13
  export const featuresById = new Map(
14
14
  features.map((feature) => [feature.id, feature]),
15
- );
15
+ );
16
+
17
+ export const featurePromptOptions = features.map((feature) => ({
18
+ value: feature.id,
19
+ label: feature.title,
20
+ hint: feature.hint,
21
+ }));
22
+
23
+ export const defaultFeatureIds = features.map((feature) => feature.id);
24
+
25
+ export const availableFeatureIdSet = new Set(defaultFeatureIds);
@@ -1,23 +1,19 @@
1
- import { cp } from 'node:fs/promises';
2
1
  import { dirname, resolve } from 'node:path';
3
2
  import { fileURLToPath } from 'node:url';
4
- import { addDependencies } from '../../utils/package-json.js';
3
+
4
+ import { defineFeature } from '../define-feature.js';
5
5
 
6
6
  const currentFilePath = fileURLToPath(import.meta.url);
7
7
  const currentDirPath = dirname(currentFilePath);
8
8
 
9
- export const reactRouterFeature = {
9
+ export const reactRouterFeature = defineFeature({
10
10
  id: 'react-router',
11
11
  title: 'React Router DOM',
12
- apply: async ({ projectPath }) => {
13
- await addDependencies(projectPath, {
12
+ hint: 'Routing + FSD-like app/pages/shared',
13
+ packageJson: {
14
+ dependencies: {
14
15
  'react-router-dom': '^7.13.2',
15
- });
16
-
17
- const filesDirPath = resolve(currentDirPath, 'files');
18
-
19
- await cp(filesDirPath, projectPath, {
20
- recursive: true,
21
- });
16
+ },
22
17
  },
23
- };
18
+ copyFiles: resolve(currentDirPath, 'files'),
19
+ });
@@ -1,29 +1,24 @@
1
- import { cp } from 'node:fs/promises';
2
1
  import { dirname, resolve } from 'node:path';
3
2
  import { fileURLToPath } from 'node:url';
4
- import { addDevDependencies, addScripts } from '../../utils/package-json.js';
3
+
4
+ import { defineFeature } from '../define-feature.js';
5
5
 
6
6
  const currentFilePath = fileURLToPath(import.meta.url);
7
7
  const currentDirPath = dirname(currentFilePath);
8
8
 
9
- export const stylelintFeature = {
9
+ export const stylelintFeature = defineFeature({
10
10
  id: 'stylelint',
11
11
  title: 'Stylelint',
12
- apply: async ({ projectPath }) => {
13
- await addDevDependencies(projectPath, {
12
+ hint: 'SCSS/CSS linting',
13
+ packageJson: {
14
+ devDependencies: {
14
15
  stylelint: '^17.6.0',
15
16
  'stylelint-config-standard-scss': '^17.0.0',
16
- });
17
-
18
- await addScripts(projectPath, {
17
+ },
18
+ scripts: {
19
19
  'lint:styles': 'stylelint "src/**/*.{scss,css}"',
20
20
  'lint:styles:fix': 'stylelint "src/**/*.{scss,css}" --fix',
21
- });
22
-
23
- const filesDirPath = resolve(currentDirPath, 'files');
24
-
25
- await cp(filesDirPath, projectPath, {
26
- recursive: true,
27
- });
21
+ },
28
22
  },
29
- };
23
+ copyFiles: resolve(currentDirPath, 'files'),
24
+ });
@@ -1,6 +1,37 @@
1
1
  import { readFile, writeFile } from 'node:fs/promises';
2
2
  import { resolve } from 'node:path';
3
3
 
4
+ function sortRecordEntries(record) {
5
+ if (!record) {
6
+ return record;
7
+ }
8
+
9
+ return Object.fromEntries(
10
+ Object.entries(record).sort(([leftName], [rightName]) => (
11
+ leftName.localeCompare(rightName)
12
+ )),
13
+ );
14
+ }
15
+
16
+ function mergeRecordEntries(currentRecord, entriesToAdd) {
17
+ if (!entriesToAdd || Object.keys(entriesToAdd).length === 0) {
18
+ return currentRecord;
19
+ }
20
+
21
+ return sortRecordEntries({
22
+ ...(currentRecord ?? {}),
23
+ ...entriesToAdd,
24
+ });
25
+ }
26
+
27
+ function normalizePackageJson(packageJson) {
28
+ packageJson.dependencies = sortRecordEntries(packageJson.dependencies);
29
+ packageJson.devDependencies = sortRecordEntries(packageJson.devDependencies);
30
+ packageJson.scripts = sortRecordEntries(packageJson.scripts);
31
+
32
+ return packageJson;
33
+ }
34
+
4
35
  export async function readPackageJson(projectPath) {
5
36
  const packageJsonPath = resolve(projectPath, 'package.json');
6
37
  const content = await readFile(packageJsonPath, 'utf8');
@@ -13,61 +44,54 @@ export async function writePackageJson(projectPath, packageJson) {
13
44
 
14
45
  await writeFile(
15
46
  packageJsonPath,
16
- `${JSON.stringify(packageJson, null, 4)}\n`,
47
+ `${JSON.stringify(normalizePackageJson(packageJson), null, 4)}\n`,
17
48
  'utf8',
18
49
  );
19
50
  }
20
51
 
21
- export async function addDependencies(projectPath, dependenciesToAdd) {
52
+ export async function updatePackageJson(projectPath, updater) {
22
53
  const packageJson = await readPackageJson(projectPath);
23
54
 
24
- packageJson.dependencies ??= {};
25
-
26
- for (const [dependencyName, dependencyVersion] of Object.entries(dependenciesToAdd)) {
27
- packageJson.dependencies[dependencyName] = dependencyVersion;
28
- }
29
-
30
- packageJson.dependencies = Object.fromEntries(
31
- Object.entries(packageJson.dependencies).sort(([leftName], [rightName]) => (
32
- leftName.localeCompare(rightName)
33
- )),
34
- );
55
+ await updater(packageJson);
35
56
 
36
57
  await writePackageJson(projectPath, packageJson);
37
- }
38
58
 
39
- export async function addDevDependencies(projectPath, dependenciesToAdd) {
40
- const packageJson = await readPackageJson(projectPath);
41
-
42
- packageJson.devDependencies ??= {};
59
+ return packageJson;
60
+ }
43
61
 
44
- for (const [dependencyName, dependencyVersion] of Object.entries(dependenciesToAdd)) {
45
- packageJson.devDependencies[dependencyName] = dependencyVersion;
46
- }
62
+ export async function patchPackageJson(projectPath, patch) {
63
+ return updatePackageJson(projectPath, (packageJson) => {
64
+ packageJson.dependencies = mergeRecordEntries(
65
+ packageJson.dependencies,
66
+ patch.dependencies,
67
+ );
68
+
69
+ packageJson.devDependencies = mergeRecordEntries(
70
+ packageJson.devDependencies,
71
+ patch.devDependencies,
72
+ );
73
+
74
+ packageJson.scripts = mergeRecordEntries(
75
+ packageJson.scripts,
76
+ patch.scripts,
77
+ );
78
+ });
79
+ }
47
80
 
48
- packageJson.devDependencies = Object.fromEntries(
49
- Object.entries(packageJson.devDependencies).sort(([leftName], [rightName]) => (
50
- leftName.localeCompare(rightName)
51
- )),
52
- );
81
+ export async function addDependencies(projectPath, dependenciesToAdd) {
82
+ return patchPackageJson(projectPath, {
83
+ dependencies: dependenciesToAdd,
84
+ });
85
+ }
53
86
 
54
- await writePackageJson(projectPath, packageJson);
87
+ export async function addDevDependencies(projectPath, dependenciesToAdd) {
88
+ return patchPackageJson(projectPath, {
89
+ devDependencies: dependenciesToAdd,
90
+ });
55
91
  }
56
92
 
57
93
  export async function addScripts(projectPath, scriptsToAdd) {
58
- const packageJson = await readPackageJson(projectPath);
59
-
60
- packageJson.scripts ??= {};
61
-
62
- for (const [scriptName, scriptValue] of Object.entries(scriptsToAdd)) {
63
- packageJson.scripts[scriptName] = scriptValue;
64
- }
65
-
66
- packageJson.scripts = Object.fromEntries(
67
- Object.entries(packageJson.scripts).sort(([leftName], [rightName]) => (
68
- leftName.localeCompare(rightName)
69
- )),
70
- );
71
-
72
- await writePackageJson(projectPath, packageJson);
94
+ return patchPackageJson(projectPath, {
95
+ scripts: scriptsToAdd,
96
+ });
73
97
  }