@adbayb/stack 2.4.0 → 2.6.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/bin/index.js CHANGED
@@ -3,6 +3,6 @@
3
3
  import { join } from "node:path";
4
4
  import { createRequire } from "node:module";
5
5
 
6
- const pkg = createRequire(import.meta.url)("../package.json");
6
+ const package_ = createRequire(import.meta.url)("../package.json");
7
7
 
8
- import(join("..", pkg.exports["."].default));
8
+ import(join("..", package_.exports["."].default));
@@ -1,3 +1,4 @@
1
+ import { config as unicornConfig } from "./presets/unicorn.js";
1
2
  import { config as uncategorizedConfig } from "./presets/uncategorized.js";
2
3
  import { config as typescriptConfig } from "./presets/typescript.js";
3
4
  import { config as testConfig } from "./presets/test.js";
@@ -24,6 +25,7 @@ export default createConfig(
24
25
  ...sonarConfig,
25
26
  ...stylisticConfig,
26
27
  ...testConfig,
28
+ ...unicornConfig,
27
29
  ...uncategorizedConfig,
28
30
  ...overridableConfig,
29
31
  );
@@ -66,19 +66,23 @@ export const config = [
66
66
  "no-regex-spaces": "error",
67
67
  "no-restricted-syntax": [
68
68
  "error",
69
- // https://medium.com/@hbarcelos/why-i-banned-null-from-my-js-code-and-why-you-should-too-13df90323cfa
69
+ /*
70
+ * https://medium.com/@hbarcelos/why-i-banned-null-from-my-js-code-and-why-you-should-too-13df90323cfa
71
+ * https://www.youtube.com/watch?v=PSGEjv3Tqo0&t=561s
72
+ */
70
73
  {
71
- message: "Use undefined instead of null",
74
+ message: "Use `undefined` instead of `null`.",
72
75
  selector: "VariableDeclarator > Literal[raw='null']",
73
76
  },
74
77
  {
75
- message: "Use undefined instead of null",
78
+ message: "Use `undefined` instead of `null`.",
76
79
  selector: "AssignmentExpression > Literal[raw='null']",
77
80
  },
78
81
  ],
79
82
  "no-self-assign": "error",
80
83
  "no-shadow-restricted-names": "error",
81
84
  "no-sparse-arrays": "error",
85
+ "no-template-curly-in-string": "error",
82
86
  "no-unexpected-multiline": "error",
83
87
  "no-unsafe-finally": "error",
84
88
  "no-unsafe-optional-chaining": "error",
@@ -17,7 +17,6 @@ export const config = [
17
17
  "import-x/no-absolute-path": "error",
18
18
  "import-x/no-amd": "error",
19
19
  "import-x/no-anonymous-default-export": "error",
20
- "import-x/no-commonjs": "error",
21
20
  "import-x/no-cycle": "error",
22
21
  "import-x/no-default-export": "error",
23
22
  "import-x/no-deprecated": "error",
@@ -1,11 +1,6 @@
1
- import tseslint from "typescript-eslint";
2
1
  import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
3
2
 
4
3
  export const config = [
5
- {
6
- files: ["**/*.{js,jsx,cjs,mjs}"],
7
- ...tseslint.configs.disableTypeChecked,
8
- },
9
4
  {
10
5
  // Relaxed rules for example-like folder, and [config-, story-, and test]-like files
11
6
  files: [
@@ -1,3 +1,5 @@
1
+ import reactWebApiPlugin from "eslint-plugin-react-web-api";
2
+ import reactHooksExtraPlugin from "eslint-plugin-react-hooks-extra";
1
3
  import reactHooksPlugin from "eslint-plugin-react-hooks";
2
4
  // eslint-disable-next-line depend/ban-dependencies
3
5
  import reactPlugin from "eslint-plugin-react";
@@ -8,16 +10,29 @@ export const config = [
8
10
  plugins: {
9
11
  "react": reactPlugin,
10
12
  "react-hooks": reactHooksPlugin,
13
+ "react-hooks-extra": reactHooksExtraPlugin,
14
+ "react-web-api": reactWebApiPlugin,
11
15
  },
12
16
  rules: {
17
+ "react-hooks-extra/no-direct-set-state-in-use-effect": "error",
18
+ "react-hooks-extra/no-direct-set-state-in-use-layout-effect": "error",
19
+ "react-hooks-extra/no-redundant-custom-hook": "error",
20
+ "react-hooks-extra/no-unnecessary-use-callback": "error",
21
+ "react-hooks-extra/no-unnecessary-use-memo": "error",
22
+ "react-hooks-extra/prefer-use-state-lazy-initialization": "error",
13
23
  "react-hooks/exhaustive-deps": "warn",
14
24
  "react-hooks/rules-of-hooks": "error",
25
+ "react-web-api/no-leaked-event-listener": "error",
26
+ "react-web-api/no-leaked-interval": "error",
27
+ "react-web-api/no-leaked-resize-observer": "error",
28
+ "react-web-api/no-leaked-timeout": "error",
15
29
  "react/boolean-prop-naming": [
16
30
  "error",
17
31
  { rule: "^(is|has)[A-Z]([A-Za-z0-9]?)+" },
18
32
  ],
19
33
  "react/button-has-type": "error",
20
34
  "react/checked-requires-onchange-or-readonly": "error",
35
+ "react/destructuring-assignment": ["error", "always"],
21
36
  "react/display-name": "error",
22
37
  "react/forbid-component-props": "error",
23
38
  "react/forward-ref-uses-ref": "error",
@@ -28,14 +43,13 @@ export const config = [
28
43
  "react/jsx-handler-names": "error",
29
44
  "react/jsx-key": "error",
30
45
  "react/jsx-no-bind": "error",
46
+ "react/jsx-no-comment-textnodes": "error",
31
47
  "react/jsx-no-constructed-context-values": "error",
32
48
  "react/jsx-no-leaked-render": "error",
33
49
  "react/jsx-no-script-url": "error",
34
50
  "react/jsx-no-target-blank": "error",
35
51
  "react/jsx-no-useless-fragment": "error",
36
- "react/jsx-pascal-case": "error",
37
52
  "react/jsx-props-no-spread-multi": "error",
38
- "react/jsx-sort-props": "error",
39
53
  "react/jsx-uses-react": "error",
40
54
  "react/jsx-uses-vars": "error",
41
55
  "react/no-access-state-in-setstate": "error",
@@ -56,6 +70,7 @@ export const config = [
56
70
  "react/no-this-in-sfc": "error",
57
71
  "react/no-typos": "error",
58
72
  "react/no-unescaped-entities": "error",
73
+ "react/no-unknown-property": "error",
59
74
  "react/no-unsafe": "error",
60
75
  "react/no-unstable-nested-components": "error",
61
76
  "react/no-unused-class-component-methods": "error",
@@ -63,7 +78,8 @@ export const config = [
63
78
  "react/no-will-update-set-state": "error",
64
79
  "react/prefer-read-only-props": "error",
65
80
  "react/prefer-stateless-function": "error",
66
- "react/self-closing-comp": "error",
81
+ "react/sort-comp": "error",
82
+ "react/state-in-constructor": ["error", "never"],
67
83
  "react/style-prop-object": "error",
68
84
  "react/void-dom-elements-no-children": "error",
69
85
  },
@@ -151,7 +151,6 @@ export const config = [
151
151
  "sonarjs/no-unenclosed-multiline-block": "error",
152
152
  "sonarjs/no-uniq-key": "error",
153
153
  "sonarjs/no-unsafe-unzip": "error",
154
- "sonarjs/no-unstable-nested-components": "error",
155
154
  "sonarjs/no-unthrown-error": "error",
156
155
  "sonarjs/no-unused-collection": "error",
157
156
  "sonarjs/no-useless-call": "error",
@@ -193,11 +192,9 @@ export const config = [
193
192
  "sonarjs/sonar-no-empty-character-class": "error",
194
193
  "sonarjs/sonar-no-fallthrough": "error",
195
194
  "sonarjs/sonar-no-invalid-regexp": "error",
196
- "sonarjs/sonar-no-magic-numbers": "error",
197
195
  "sonarjs/sonar-no-misleading-character-class": "error",
198
196
  "sonarjs/sonar-no-regex-spaces": "error",
199
- "sonarjs/sonar-no-unused-class-component-methods": "error",
200
- "sonarjs/sonar-prefer-regexp-exec": "error",
197
+ // "sonarjs/sonar-no-unused-class-component-methods": "error", // Disabled since it involves `eslint-plugin-react` activation
201
198
  "sonarjs/sql-queries": "error",
202
199
  "sonarjs/stable-tests": "error",
203
200
  "sonarjs/stateful-regex": "error",
@@ -9,6 +9,9 @@ export const config = [
9
9
  "@stylistic": stylistic,
10
10
  },
11
11
  rules: {
12
+ "@stylistic/jsx-pascal-case": "error",
13
+ "@stylistic/jsx-self-closing-comp": "error",
14
+ "@stylistic/jsx-sort-props": "error",
12
15
  "@stylistic/lines-between-class-members": ["error", "always"],
13
16
  "@stylistic/multiline-comment-style": ["error", "starred-block"],
14
17
  "@stylistic/padding-line-between-statements": [
@@ -59,6 +62,11 @@ export const config = [
59
62
  prev: ["cjs-export", "export"],
60
63
  },
61
64
  ],
65
+ "@stylistic/quotes": [
66
+ "error",
67
+ "double",
68
+ { allowTemplateLiterals: false, avoidEscape: true },
69
+ ],
62
70
  "@stylistic/spaced-comment": [
63
71
  "error",
64
72
  "always",
@@ -1,6 +1,10 @@
1
1
  import tseslint from "typescript-eslint";
2
2
 
3
- import { CWD, JAVASCRIPT_LIKE_EXTENSIONS } from "../constants.js";
3
+ import {
4
+ CWD,
5
+ JAVASCRIPT_EXTENSIONS,
6
+ JAVASCRIPT_LIKE_EXTENSIONS,
7
+ } from "../constants.js";
4
8
 
5
9
  export const config = [
6
10
  {
@@ -169,4 +173,8 @@ export const config = [
169
173
  "@typescript-eslint/use-unknown-in-catch-callback-variable": "error",
170
174
  },
171
175
  },
176
+ {
177
+ files: JAVASCRIPT_EXTENSIONS,
178
+ ...tseslint.configs.disableTypeChecked,
179
+ },
172
180
  ];
@@ -0,0 +1,138 @@
1
+ /* eslint-disable unicorn/string-content */
2
+ import unicornPlugin from "eslint-plugin-unicorn";
3
+
4
+ import { JAVASCRIPT_LIKE_EXTENSIONS } from "../constants.js";
5
+
6
+ export const config = [
7
+ {
8
+ files: JAVASCRIPT_LIKE_EXTENSIONS,
9
+ plugins: {
10
+ unicorn: unicornPlugin,
11
+ },
12
+ rules: {
13
+ "unicorn/better-regex": "error",
14
+ "unicorn/catch-error-name": [
15
+ "error",
16
+ {
17
+ name: "error",
18
+ },
19
+ ],
20
+ "unicorn/consistent-empty-array-spread": "error",
21
+ "unicorn/consistent-existence-index-check": "error",
22
+ "unicorn/consistent-function-scoping": "error",
23
+ "unicorn/custom-error-definition": "error",
24
+ "unicorn/error-message": "error",
25
+ "unicorn/escape-case": "error",
26
+ "unicorn/expiring-todo-comments": "error",
27
+ "unicorn/explicit-length-check": "error",
28
+ "unicorn/filename-case": [
29
+ "error",
30
+ {
31
+ cases: {
32
+ camelCase: true,
33
+ pascalCase: true,
34
+ },
35
+ },
36
+ ],
37
+ "unicorn/new-for-builtins": "error",
38
+ "unicorn/no-abusive-eslint-disable": "error",
39
+ "unicorn/no-array-callback-reference": "error",
40
+ "unicorn/no-array-push-push": "error",
41
+ "unicorn/no-await-in-promise-methods": "error",
42
+ "unicorn/no-console-spaces": "error",
43
+ "unicorn/no-document-cookie": "error",
44
+ "unicorn/no-empty-file": "error",
45
+ "unicorn/no-for-loop": "error",
46
+ "unicorn/no-instanceof-array": "error",
47
+ "unicorn/no-invalid-fetch-options": "error",
48
+ "unicorn/no-invalid-remove-event-listener": "error",
49
+ "unicorn/no-length-as-slice-end": "error",
50
+ "unicorn/no-new-buffer": "error",
51
+ "unicorn/no-object-as-default-parameter": "error",
52
+ "unicorn/no-process-exit": "error",
53
+ "unicorn/no-single-promise-in-promise-methods": "error",
54
+ "unicorn/no-static-only-class": "error",
55
+ "unicorn/no-typeof-undefined": "error",
56
+ "unicorn/no-unnecessary-polyfills": "error",
57
+ "unicorn/no-unreadable-array-destructuring": "error",
58
+ "unicorn/no-unreadable-iife": "error",
59
+ "unicorn/no-unused-properties": "error",
60
+ "unicorn/no-useless-fallback-in-spread": "error",
61
+ "unicorn/no-useless-length-check": "error",
62
+ "unicorn/no-useless-promise-resolve-reject": "error",
63
+ "unicorn/no-useless-spread": "error",
64
+ "unicorn/no-zero-fractions": "error",
65
+ "unicorn/number-literal-case": "error",
66
+ "unicorn/prefer-add-event-listener": "error",
67
+ "unicorn/prefer-array-find": "error",
68
+ "unicorn/prefer-array-flat": "error",
69
+ "unicorn/prefer-array-flat-map": "error",
70
+ "unicorn/prefer-array-index-of": "error",
71
+ "unicorn/prefer-array-some": "error",
72
+ "unicorn/prefer-blob-reading-methods": "error",
73
+ "unicorn/prefer-code-point": "error",
74
+ "unicorn/prefer-date-now": "error",
75
+ "unicorn/prefer-dom-node-append": "error",
76
+ "unicorn/prefer-dom-node-remove": "error",
77
+ "unicorn/prefer-dom-node-text-content": "error",
78
+ "unicorn/prefer-event-target": "error",
79
+ "unicorn/prefer-export-from": "error",
80
+ "unicorn/prefer-global-this": "error",
81
+ "unicorn/prefer-includes": "error",
82
+ "unicorn/prefer-keyboard-event-key": "error",
83
+ "unicorn/prefer-logical-operator-over-ternary": "error",
84
+ "unicorn/prefer-math-min-max": "error",
85
+ "unicorn/prefer-math-trunc": "error",
86
+ "unicorn/prefer-modern-dom-apis": "error",
87
+ "unicorn/prefer-modern-math-apis": "error",
88
+ "unicorn/prefer-module": "error",
89
+ "unicorn/prefer-native-coercion-functions": "error",
90
+ "unicorn/prefer-negative-index": "error",
91
+ "unicorn/prefer-number-properties": "error",
92
+ "unicorn/prefer-object-from-entries": "error",
93
+ "unicorn/prefer-optional-catch-binding": "error",
94
+ "unicorn/prefer-prototype-methods": "error",
95
+ "unicorn/prefer-query-selector": "error",
96
+ "unicorn/prefer-reflect-apply": "error",
97
+ "unicorn/prefer-regexp-test": "error",
98
+ "unicorn/prefer-set-has": "error",
99
+ "unicorn/prefer-set-size": "error",
100
+ "unicorn/prefer-spread": "error",
101
+ "unicorn/prefer-string-raw": "error",
102
+ "unicorn/prefer-string-replace-all": "error",
103
+ "unicorn/prefer-string-slice": "error",
104
+ "unicorn/prefer-string-starts-ends-with": "error",
105
+ "unicorn/prefer-string-trim-start-end": "error",
106
+ "unicorn/prefer-structured-clone": "error",
107
+ "unicorn/prefer-switch": "error",
108
+ "unicorn/prefer-ternary": "error",
109
+ "unicorn/prefer-top-level-await": "error",
110
+ "unicorn/prefer-type-error": "error",
111
+ "unicorn/prevent-abbreviations": [
112
+ "error",
113
+ {
114
+ allowList: {
115
+ props: true,
116
+ Props: true,
117
+ },
118
+ },
119
+ ],
120
+ "unicorn/relative-url-style": ["error", "always"],
121
+ "unicorn/require-array-join-separator": "error",
122
+ "unicorn/require-number-to-fixed-digits-argument": "error",
123
+ "unicorn/string-content": [
124
+ "error",
125
+ {
126
+ patterns: {
127
+ "->": "→",
128
+ "\\.\\.\\.": "…",
129
+ "^http:\\/\\/": String.raw`^https:\/\/`,
130
+ },
131
+ },
132
+ ],
133
+ "unicorn/switch-case-braces": "error",
134
+ "unicorn/text-encoding-identifier-case": "error",
135
+ "unicorn/throw-new-error": "error",
136
+ },
137
+ },
138
+ ];
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { helpers, termost } from 'termost';
2
2
  import process from 'node:process';
3
3
  import { resolve, join } from 'node:path';
4
4
  import { createRequire } from 'node:module';
5
- import { writeFile, chmod, mkdir, symlink } from 'node:fs/promises';
5
+ import { writeFile, chmod, mkdir, symlink, rm } from 'node:fs/promises';
6
6
  import { existsSync, cpSync, readFileSync, renameSync, writeFileSync, readdirSync } from 'node:fs';
7
7
  import { fdir } from 'fdir';
8
8
 
@@ -96,7 +96,7 @@ const getStackCommand = (command, isNodeRuntime = true)=>{
96
96
  const hasDependency = (packageName)=>{
97
97
  return Boolean(require.resolve(packageName));
98
98
  };
99
- const setPkgManager = async ()=>{
99
+ const setPackageManager = async ()=>{
100
100
  return helpers.exec("corepack enable");
101
101
  };
102
102
  const request = {
@@ -117,35 +117,53 @@ const eslint = (options)=>async (files = [])=>{
117
117
  eslintFiles.push(".");
118
118
  } else {
119
119
  eslintFiles = files.filter((file)=>{
120
- return ESLINT_EXTENSIONS.some((ext)=>file.endsWith(ext));
120
+ return ESLINT_EXTENSIONS.some((extension)=>file.endsWith(extension));
121
121
  });
122
- if (eslintFiles.length === 0) return Promise.resolve();
122
+ if (eslintFiles.length === 0) return;
123
123
  }
124
- const args = [
124
+ const arguments_ = [
125
125
  ...eslintFiles
126
126
  ];
127
- args.push("--cache");
128
- args.push(`--cache-location ${resolveFromProjectDirectory("node_modules/.cache/.eslintcache")}`);
129
- // @note: prevent errors when no matched file is found
130
- args.push("--no-error-on-unmatched-pattern");
127
+ arguments_.push("--cache", `--cache-location ${resolveFromProjectDirectory("node_modules/.cache/.eslintcache")}`, "--no-error-on-unmatched-pattern");
131
128
  if (options.isFixMode) {
132
- args.push("--fix");
129
+ arguments_.push("--fix");
133
130
  }
134
131
  try {
135
- return await helpers.exec(`eslint ${args.join(" ")}`);
132
+ return await helpers.exec(`eslint ${arguments_.join(" ")}`);
136
133
  } catch (error) {
137
134
  throw createError("eslint", error);
138
135
  }
139
136
  };
140
- const turbo = async (command, options = {
141
- hasLiveOutput: true
142
- })=>{
137
+ const turbo = async (command, options = {})=>{
143
138
  try {
144
- return await helpers.exec(`turbo run ${command}`, options);
139
+ const { excludeExamples = false, hasLiveOutput = true, ...restOptions } = options;
140
+ return await helpers.exec(`turbo run ${command} ${excludeExamples ? "--filter !@examples/*" : ""}`, {
141
+ ...restOptions,
142
+ hasLiveOutput
143
+ });
145
144
  } catch (error) {
146
145
  throw createError("turbo", error);
147
146
  }
148
147
  };
148
+ const logCheckableFiles = (files)=>{
149
+ if (files.length === 0) {
150
+ helpers.message("The whole project will be checked.", {
151
+ label: false,
152
+ lineBreak: {
153
+ end: true,
154
+ start: false
155
+ }
156
+ });
157
+ return;
158
+ }
159
+ helpers.message(files.join("\n "), {
160
+ label: "Following files will be checked:",
161
+ lineBreak: {
162
+ end: true,
163
+ start: false
164
+ }
165
+ });
166
+ };
149
167
  const changeset = async (command)=>{
150
168
  try {
151
169
  return await helpers.exec(command, {
@@ -168,9 +186,7 @@ const ESLINT_EXTENSIONS = [
168
186
  "mdx"
169
187
  ];
170
188
 
171
- var version = "2.4.0";
172
-
173
- const VERSION = version;
189
+ var version = "2.6.0";
174
190
 
175
191
  const createWatchCommand = (program)=>{
176
192
  program.command({
@@ -223,7 +239,6 @@ const createReleaseCommand = (program)=>{
223
239
  description: "Publish package(s) to the registry"
224
240
  }).task({
225
241
  async handler () {
226
- // TODO: label instead?
227
242
  helpers.message("New changelog entry\n");
228
243
  await changeset("changeset");
229
244
  },
@@ -253,7 +268,9 @@ const createInstallCommand = (program)=>{
253
268
  }).task({
254
269
  label: label$4("Install `git.pre-commit` hook"),
255
270
  async handler () {
256
- await installGitHook("pre-commit", `${getStackCommand("fix $(git status --porcelain | awk 'BEGIN{ ORS=\" \" } { print $2 }')", false)} && git add -A`);
271
+ const lineBreakMatcher = String.raw`\n|\r\n`;
272
+ const modifiedFilesCommand = `node -e 'console.log(require("child_process").execSync("git status --porcelain", {encoding: "utf8"}).split(/${lineBreakMatcher}/).filter(Boolean).map(item => item.split(" ").at(-1)).join(" "))'`;
273
+ await installGitHook("pre-commit", `${getStackCommand(`fix $(${modifiedFilesCommand})`, false)} && git add -A`);
257
274
  }
258
275
  }).task({
259
276
  label: label$4("Install `git.commit-msg` hook"),
@@ -286,19 +303,17 @@ const fixFormatting = async (files)=>{
286
303
  return !PRETTIER_IGNORE_FILES.some((filename)=>file.endsWith(filename)) && // The root `README.md` file is ignored to prevent error due to its symbolic link nature when specified explicitly as a file
287
304
  file !== "README.md";
288
305
  });
289
- if (prettierFiles.length === 0) return Promise.resolve();
306
+ if (prettierFiles.length === 0) return;
290
307
  }
291
- const args = [
308
+ const arguments_ = [
292
309
  ...prettierFiles
293
310
  ];
294
311
  if (existsSync(resolveFromProjectDirectory(".gitignore"))) {
295
- args.push("--ignore-path .gitignore");
312
+ arguments_.push("--ignore-path .gitignore");
296
313
  }
297
- args.push("--write");
298
- args.push("--ignore-unknown");
299
- args.push("--no-error-on-unmatched-pattern");
314
+ arguments_.push("--write", "--ignore-unknown", "--no-error-on-unmatched-pattern");
300
315
  try {
301
- return await helpers.exec(`prettier ${args.join(" ")}`);
316
+ return await helpers.exec(`prettier ${arguments_.join(" ")}`);
302
317
  } catch (error) {
303
318
  throw createError("prettier", error);
304
319
  }
@@ -308,10 +323,15 @@ const createFixCommand = (program)=>{
308
323
  program.command({
309
324
  name: "fix",
310
325
  description: "Fix auto-fixable issues"
326
+ }).task({
327
+ handler (_, argv) {
328
+ logCheckableFiles(argv.operands);
329
+ }
311
330
  }).task({
312
331
  label: label$3("Prepare the project"),
313
332
  async handler () {
314
333
  await turbo("build", {
334
+ excludeExamples: true,
315
335
  hasLiveOutput: false
316
336
  });
317
337
  }
@@ -336,7 +356,7 @@ const createCreateCommand = (program)=>{
336
356
  }).task({
337
357
  handler () {
338
358
  botMessage({
339
- title: `I'm Stack v${VERSION}, your bot assistant`,
359
+ title: `I'm Stack v${version}, your bot assistant`,
340
360
  description: "I can guarantee you a project creation in under 1 minute 🚀",
341
361
  type: "information"
342
362
  });
@@ -420,21 +440,21 @@ const createCreateCommand = (program)=>{
420
440
  }).task({
421
441
  label: label$2("Set up the package manager"),
422
442
  async handler () {
423
- await setPkgManager();
443
+ await setPackageManager();
424
444
  }
425
445
  }).task({
426
446
  label: label$2("Install dependencies"),
427
447
  async handler ({ data }) {
428
- const localDevDependencies = [
448
+ const localDevelopmentDependencies = [
429
449
  "quickbundle",
430
450
  "vitest"
431
451
  ];
432
- const globalDevDependencies = [
452
+ const globalDevelopmentDependencies = [
433
453
  "@adbayb/stack"
434
454
  ];
435
455
  try {
436
- await helpers.exec(`pnpm add ${globalDevDependencies.join(" ")} --save-dev --ignore-workspace-root-check`);
437
- await helpers.exec(`pnpm add ${localDevDependencies.join(" ")} --save-dev --filter ${data.projectName}`);
456
+ await helpers.exec(`pnpm add ${globalDevelopmentDependencies.join(" ")} --save-dev --ignore-workspace-root-check`);
457
+ await helpers.exec(`pnpm add ${localDevelopmentDependencies.join(" ")} --save-dev --filter ${data.projectName}`);
438
458
  await helpers.exec("pnpm install");
439
459
  } catch (error) {
440
460
  throw createError("pnpm", error);
@@ -476,7 +496,7 @@ const label$2 = (message)=>`${message} 🔨`;
476
496
  const projectRootPath = resolveFromProjectDirectory("./");
477
497
  const templateExpressionRegExp = /{{(.*?)}}/g;
478
498
  const evaluate = (content)=>{
479
- return content.replace(templateExpressionRegExp, (_, key)=>dataModel[key] || "");
499
+ return content.replaceAll(templateExpressionRegExp, (_, key)=>dataModel[key] || "");
480
500
  };
481
501
  /** Copy the template before mutations. */ cpSync(templateRootPath, projectRootPath, {
482
502
  force: true,
@@ -484,15 +504,15 @@ const label$2 = (message)=>`${message} 🔨`;
484
504
  });
485
505
  /** Template file mutations. */ new fdir().withBasePath().glob(`**/*${templateExtension}`).crawl(projectRootPath).sync().forEach((templateFilePath)=>{
486
506
  const projectFilePath = templateFilePath.slice(0, templateFilePath.lastIndexOf(templateExtension));
487
- const content = evaluate(readFileSync(templateFilePath, "utf-8"));
507
+ const content = evaluate(readFileSync(templateFilePath, "utf8"));
488
508
  renameSync(templateFilePath, projectFilePath);
489
- writeFileSync(projectFilePath, content, "utf-8");
509
+ writeFileSync(projectFilePath, content, "utf8");
490
510
  });
491
511
  /** Template folder mutations. */ new fdir().withBasePath().onlyDirs().filter((path)=>{
492
- return Boolean(templateExpressionRegExp.exec(path));
512
+ return templateExpressionRegExp.test(path);
493
513
  }).crawl(projectRootPath).sync()// Re-order from longest to lowest path length to apply first renaming operations on deepest file structure
494
514
  .sort((a, b)=>b.length - a.length).forEach((templateFolderPath)=>{
495
- const newPath = templateFolderPath.replace(templateExpressionRegExp, (_, dataModelKey)=>{
515
+ const newPath = templateFolderPath.replaceAll(templateExpressionRegExp, (_, dataModelKey)=>{
496
516
  return dataModel[dataModelKey];
497
517
  });
498
518
  renameSync(templateFolderPath, newPath);
@@ -524,7 +544,12 @@ const createCleanCommand = (program)=>{
524
544
  }
525
545
  }).task({
526
546
  handler ({ files }) {
527
- helpers.message(`Removed assets: ${files.join(", ")}\n`, {
547
+ helpers.message(files.join("\n "), {
548
+ label: "Removed assets",
549
+ lineBreak: {
550
+ end: false,
551
+ start: true
552
+ },
528
553
  type: "information"
529
554
  });
530
555
  },
@@ -535,14 +560,17 @@ const createCleanCommand = (program)=>{
535
560
  };
536
561
  const label$1 = (message)=>`${message} 🧹`;
537
562
  const cleanFiles = async (files)=>{
538
- return helpers.exec(`rm -rf ${files.join(" ")}`);
563
+ return Promise.all(files.map(async (file)=>rm(file, {
564
+ force: true,
565
+ recursive: true
566
+ })));
539
567
  };
540
568
  const isDirectoryExistAndNotEmpty = (path)=>{
541
569
  return existsSync(path) && readdirSync(path).length > 0;
542
570
  };
543
571
  const retrieveIgnoredFiles = async ()=>{
544
- const rawFiles = await helpers.exec(`git clean -fdXn | grep -v '${PRESERVE_FILES.join("\\|")}' | cut -c 14-`);
545
- return rawFiles.split(/\n/).filter(Boolean);
572
+ const rawFiles = await helpers.exec("git clean -fdXn");
573
+ return rawFiles.split(/\n|\r\n/).filter((cleanOutput)=>!PRESERVE_FILES.some((excludedFile)=>cleanOutput.includes(excludedFile))).map((cleanOutput)=>cleanOutput.split(" ").at(-1));
546
574
  };
547
575
  const PRESERVE_FILES = [
548
576
  "node_modules"
@@ -559,25 +587,25 @@ const checkTypes = async ()=>{
559
587
  const checkPackages = async ()=>{
560
588
  const stdout = await helpers.exec("pnpm recursive ls --json");
561
589
  const checkPackagesVersionMismatch = createPackagesVersionMismatchChecker();
562
- const packages = JSON.parse(stdout).map((pkg)=>{
563
- const pkgPath = join(pkg.path, "package.json");
564
- assert(pkg.name, ()=>createPackageError(`\`${pkgPath}\` must have a name field.`));
565
- const pkgContent = require(pkgPath);
566
- const peerDependencies = pkgContent.peerDependencies ?? {};
567
- const devDependencies = pkgContent.devDependencies ?? {};
568
- const dependencies = pkgContent.dependencies ?? {};
590
+ const packages = JSON.parse(stdout).map((package_)=>{
591
+ const packagePath = join(package_.path, "package.json");
592
+ assert(package_.name, ()=>createPackageError(`\`${packagePath}\` must have a name field.`));
593
+ const packageContent = require(packagePath);
594
+ const peerDependencies = packageContent.peerDependencies ?? {};
595
+ const devDependencies = packageContent.devDependencies ?? {};
596
+ const dependencies = packageContent.dependencies ?? {};
569
597
  return {
570
- name: pkg.name,
598
+ name: package_.name,
571
599
  dependencies,
572
600
  devDependencies,
573
601
  peerDependencies
574
602
  };
575
603
  });
576
- for (const pkg of packages){
604
+ for (const package_ of packages){
577
605
  // Check version mismatches to guarantee a single copy for a given package in the monorepo (use case: prevent singleton-like issues with React contexts)
578
- checkPackagesVersionMismatch(pkg);
606
+ checkPackagesVersionMismatch(package_);
579
607
  // Check version range accordingly to our dependency guidelines (ie. dev dependencies must be pinned and dependencies must have caret)
580
- checkPackagesVersionRange(pkg);
608
+ checkPackagesVersionRange(package_);
581
609
  }
582
610
  };
583
611
  const checkPackagesVersionRange = ({ name, dependencies, devDependencies, peerDependencies })=>{
@@ -587,13 +615,11 @@ const checkPackagesVersionRange = ({ name, dependencies, devDependencies, peerDe
587
615
  name: dependencyName,
588
616
  consumedBy: name
589
617
  });
590
- if (version !== "workspace:*" && !/^\d/.exec(version)) throw createPackageError(`As a dev dependency, \`${dependencyName}\` version must be fixed (or set as "workspace:*" for local packages) to reduce accidental breaking change risks due to an implicit semver upgrade.`, {
618
+ if (version !== "workspace:*" && !/^\d/.test(version)) throw createPackageError(`As a dev dependency, \`${dependencyName}\` version must be fixed (or set as "workspace:*" for local packages) to reduce accidental breaking change risks due to an implicit semver upgrade.`, {
591
619
  name: dependencyName,
592
620
  consumedBy: name
593
621
  });
594
622
  }
595
- const isPreReleaseVersion = (version)=>/\d+\.\d+\.\d+-(alpha|beta|experimental|next|rc).*/.exec(version);
596
- const hasNoCaret = (version)=>!isPreReleaseVersion(version) && !/^\^/.exec(version);
597
623
  for (const dependencyName of Object.keys(dependencies)){
598
624
  const version = dependencies[dependencyName];
599
625
  assertVersion(version, {
@@ -623,12 +649,12 @@ const checkPackagesVersionRange = ({ name, dependencies, devDependencies, peerDe
623
649
  };
624
650
  const createPackagesVersionMismatchChecker = ()=>{
625
651
  const monorepoDependencies = new Map();
626
- const monorepoDevDependencies = new Map();
627
- const lint = (pkg, type)=>{
628
- const pkgName = pkg.name;
629
- const isDev = type === "development";
630
- const store = isDev ? monorepoDevDependencies : monorepoDependencies;
631
- const dependencies = isDev ? pkg.devDependencies : pkg.dependencies;
652
+ const monorepoDevelopmentDependencies = new Map();
653
+ const lint = (package_, type)=>{
654
+ const packageName = package_.name;
655
+ const isDevelopment = type === "development";
656
+ const store = isDevelopment ? monorepoDevelopmentDependencies : monorepoDependencies;
657
+ const dependencies = isDevelopment ? package_.devDependencies : package_.dependencies;
632
658
  for (const dependencyName of Object.keys(dependencies)){
633
659
  const depVersion = dependencies[dependencyName];
634
660
  if (!depVersion) continue;
@@ -639,20 +665,20 @@ const createPackagesVersionMismatchChecker = ()=>{
639
665
  }
640
666
  const isSameMonorepoVersion = depVersion === storedVersion;
641
667
  if (!isSameMonorepoVersion) {
642
- throw createPackageError(`Mismatched versions: received version \`${depVersion}\` while others use \`${storedVersion}\`. To prevent issues with singleton-like code (React contexts, ...), please make sure to update all packages to use the same \`${dependencyName}\` version (either \`${storedVersion}\` or \`${depVersion}\`.`, {
668
+ throw createPackageError(`Mismatched versions: received version \`${depVersion}\` while others use \`${storedVersion}\`. To prevent issues with singleton-like code (React contexts, ), please make sure to update all packages to use the same \`${dependencyName}\` version (either \`${storedVersion}\` or \`${depVersion}\`).`, {
643
669
  name: dependencyName,
644
- consumedBy: pkgName
670
+ consumedBy: packageName
645
671
  });
646
672
  }
647
673
  }
648
674
  };
649
- return (pkg)=>{
650
- lint(pkg, "development");
651
- lint(pkg, "production");
675
+ return (package_)=>{
676
+ lint(package_, "development");
677
+ lint(package_, "production");
652
678
  };
653
679
  };
654
680
  const createPackageError = (message, context)=>{
655
- return createError("stack check", !context ? message : `\`${context.name}\` consumed by \`${context.consumedBy}\` doesn't conform to \`package.json\` guidelines.\n${message}`);
681
+ return createError("stack check", !context ? message : `\`${context.name}\` consumed by \`${context.consumedBy}\` doesn't conform to package guidelines.\n${message}`);
656
682
  };
657
683
  function assertVersion(version, { name, consumedBy }) {
658
684
  assert(version, ()=>createPackageError(`\`${name}\` must have a valid version specified (current version equals to \`${String(version)}\`).`, {
@@ -660,6 +686,8 @@ function assertVersion(version, { name, consumedBy }) {
660
686
  consumedBy
661
687
  }));
662
688
  }
689
+ const isPreReleaseVersion = (version)=>/\d+\.\d+\.\d+-(alpha|beta|experimental|next|rc).*/.exec(version);
690
+ const hasNoCaret = (version)=>!isPreReleaseVersion(version) && !version.startsWith("^");
663
691
 
664
692
  const checkLinter = eslint({
665
693
  isFixMode: false
@@ -688,15 +716,21 @@ const createCheckCommand = (program)=>{
688
716
  name: "only",
689
717
  description: `Run only one specified task (accepted value: ${ONLY_VALUES.join(", ")})`,
690
718
  defaultValue: undefined
719
+ }).task({
720
+ handler (_, argv) {
721
+ logCheckableFiles(argv.operands);
722
+ },
723
+ skip: ifOnlyDefinedAndNotEqualTo("linter")
691
724
  }).task({
692
725
  label: label("Prepare the project"),
693
726
  async handler () {
694
727
  await turbo("build", {
728
+ excludeExamples: true,
695
729
  hasLiveOutput: false
696
730
  });
697
731
  },
698
732
  skip ({ only }) {
699
- return only === "commit"; // No need to build if only commitlint is run
733
+ return only === "commit"; // No need to build if only commit is checked
700
734
  }
701
735
  }).task({
702
736
  label: label("Check package guidelines"),
@@ -759,7 +793,7 @@ const createProgram = (...commandFactories)=>{
759
793
  type: "error"
760
794
  });
761
795
  },
762
- version: VERSION
796
+ version: version
763
797
  });
764
798
  for (const commandBuilder of commandFactories){
765
799
  commandBuilder(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adbayb/stack",
3
- "version": "2.4.0",
3
+ "version": "2.6.0",
4
4
  "description": "My opinionated JavaScript-based toolchain",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -50,10 +50,10 @@
50
50
  "@changesets/cli": "^2.27.9",
51
51
  "@commitlint/cli": "^19.5.0",
52
52
  "@commitlint/config-conventional": "^19.5.0",
53
- "@eslint/compat": "^1.2.2",
54
- "@eslint/eslintrc": "^3.1.0",
53
+ "@eslint/compat": "^1.2.3",
54
+ "@eslint/eslintrc": "^3.2.0",
55
55
  "@stylistic/eslint-plugin": "^2.10.1",
56
- "@vitest/eslint-plugin": "^1.1.8",
56
+ "@vitest/eslint-plugin": "^1.1.10",
57
57
  "eslint-config-prettier": "^9.1.0",
58
58
  "eslint-import-resolver-typescript": "^3.6.3",
59
59
  "eslint-plugin-depend": "^0.12.0",
@@ -61,18 +61,21 @@
61
61
  "eslint-plugin-jest-formatting": "^3.1.0",
62
62
  "eslint-plugin-jsdoc": "^50.5.0",
63
63
  "eslint-plugin-mdx": "^3.1.5",
64
- "eslint-plugin-n": "^17.13.1",
64
+ "eslint-plugin-n": "^17.13.2",
65
65
  "eslint-plugin-prettier": "^5.2.1",
66
+ "eslint-plugin-react-hooks-extra": "^1.16.1",
66
67
  "eslint-plugin-react-hooks": "^5.0.0",
68
+ "eslint-plugin-react-web-api": "^1.16.1",
67
69
  "eslint-plugin-react": "^7.37.2",
68
70
  "eslint-plugin-sonarjs": "^2.0.4",
69
71
  "eslint-plugin-sort-keys-custom-order": "^2.2.0",
72
+ "eslint-plugin-unicorn": "^56.0.0",
70
73
  "eslint": "^9.14.0",
71
74
  "fdir": "^6.4.2",
72
75
  "globals": "^15.12.0",
73
76
  "prettier": "^3.3.3",
74
- "termost": "^1.2.0",
75
- "turbo": "^2.2.3",
77
+ "termost": "^1.4.0",
78
+ "turbo": "^2.3.0",
76
79
  "typescript-eslint": "^8.14.0",
77
80
  "typescript": "^5.6.3"
78
81
  },