@adbayb/stack 0.0.0-next-d23a669 → 0.0.0-next-ece28ab

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
@@ -59,19 +59,13 @@ stack release
59
59
 
60
60
  <br>
61
61
 
62
- ## 🏗️ Architecture
62
+ ## 🌍 Ecosystem
63
63
 
64
- ```mermaid
65
- flowchart TD
66
- N("npm init @adbayb"):::strokeWidth -- Runs --> C("@adbayb/create"):::strokeWidth
67
- C -- Runs --> S("@adbayb/stack"):::strokeWidth
68
- S -- "Scaffolds (via template and shared configurations)" --> P("New project"):::strokeWidth
69
- style C fill:#daf2d7,stroke:#90cf8e
70
- style S fill:#daf2d7,stroke:#90cf8e
71
- classDef strokeWidth stroke-width:3px
72
- ```
64
+ ### Available configurations
73
65
 
74
- <br>
66
+ - [@adbayb/stack/eslint](https://github.com/adbayb/stack/blob/main/stack/config/eslint/) to align logical and stylistic rules across projects.
67
+ - [@adbayb/stack/prettier](https://github.com/adbayb/stack/blob/main/stack/config/prettier/) to align code formatting conventions across projects.
68
+ - [@adbayb/stack/typescript](https://github.com/adbayb/stack/blob/main/stack/config/typescript/) to align type checking and processing across projects.
75
69
 
76
70
  ### Used technologies
77
71
 
@@ -87,15 +81,25 @@ flowchart TD
87
81
  - [Scripts](https://conventionalscripts.org/)
88
82
  - [File Structure](https://nx.dev/concepts/decisions/folder-structure)
89
83
  - Monorepo-first structure to:
90
- - Keep a consistent file structure pattern across templates (ease template familiarity and onboarding)
91
- - Ease examples setup
92
- - Not clutter the published package with development-related context, metadata, and artifacts
84
+ - Keep a consistent file structure pattern across templates (ease template familiarity and onboarding).
85
+ - Ease examples setup.
86
+ - Not clutter the published package with development-related context, metadata, and artifacts.
93
87
 
94
88
  <br>
95
89
 
96
- ## 👣 Roadmap
90
+ ## 🏗️ Architecture
91
+
92
+ ### Project creation workflow
97
93
 
98
- - [ ] `stack check`: Lint packages (caret range for dependencies and strict ones for dev dependencies)
94
+ ```mermaid
95
+ flowchart TD
96
+ N("npm init @adbayb"):::strokeWidth -- Runs --> C("@adbayb/create"):::strokeWidth
97
+ C -- Runs --> S("@adbayb/stack create"):::strokeWidth
98
+ S -- "Scaffolds (via template and shared configurations)" --> P("New project"):::strokeWidth
99
+ style C fill:#daf2d7,stroke:#90cf8e
100
+ style S fill:#daf2d7,stroke:#90cf8e
101
+ classDef strokeWidth stroke-width:3px
102
+ ```
99
103
 
100
104
  <br>
101
105
 
@@ -1,7 +1,39 @@
1
1
  <br>
2
2
  <div align="center">
3
- <h1>@adbayb/stack/eslint</h1>
3
+ <h1>@adbayb/stack/typescript</h1>
4
4
  <strong>My opinionated linter standards</strong>
5
5
  </div>
6
6
  <br>
7
7
  <br>
8
+
9
+ ## ✨ Features
10
+
11
+ This package offers opinionated but extensible **[ESLint](https://eslint.org/)** configurations to align logical and stylistic rules across projects.
12
+
13
+ It allows to:
14
+
15
+ - Make collaboration across my projects effective (including onboarding with a consistent look and writing patterns).
16
+ - Include a minimum viable and required set of rules to foster maintainable, testable, secure, and green code over time.
17
+ - Accelerate development by reusing common configuration and convention defaults.
18
+
19
+ <br />
20
+
21
+ ## 🚀 Quick Start
22
+
23
+ ### 1️⃣ Installation
24
+
25
+ At the root level of your project, run the following command:
26
+
27
+ ```bash
28
+ pnpm add @adbayb/stack --save-dev --workspace-root
29
+ ```
30
+
31
+ ### 2️⃣ Usage
32
+
33
+ Create a `eslint.config.js` file and include the following:
34
+
35
+ ```ts
36
+ export { default } from "@adbayb/stack/eslint";
37
+ ```
38
+
39
+ <br />
@@ -1,6 +1,7 @@
1
1
  import { config as uncategorizedConfig } from "./presets/uncategorized.js";
2
2
  import { config as typescriptConfig } from "./presets/typescript.js";
3
3
  import { config as testConfig } from "./presets/test.js";
4
+ import { config as stylisticConfig } from "./presets/stylistic.js";
4
5
  import { config as sonarConfig } from "./presets/sonar.js";
5
6
  import { config as reactConfig } from "./presets/react.js";
6
7
  import { config as overridableConfig } from "./presets/overridable.js";
@@ -17,10 +18,11 @@ export default createConfig(
17
18
  ...eslintConfig,
18
19
  ...typescriptConfig,
19
20
  ...importConfig,
20
- ...nodeConfig,
21
- ...sonarConfig,
22
21
  ...jsdocConfig,
22
+ ...nodeConfig,
23
23
  ...reactConfig,
24
+ ...sonarConfig,
25
+ ...stylisticConfig,
24
26
  ...testConfig,
25
27
  ...uncategorizedConfig,
26
28
  ...overridableConfig,
@@ -5,8 +5,10 @@ import {
5
5
 
6
6
  export const config = [
7
7
  {
8
- // Specific ESLint rules for javascript-only files (as they're already handled for TypeScript files by the transpiler):
9
- // This rule list is taken from https://typescript-eslint.io/users/configs/#recommended
8
+ /*
9
+ * Specific ESLint rules for javascript-only files (as they're already handled for TypeScript files by the transpiler):
10
+ * This rule list is taken from https://typescript-eslint.io/users/configs/#recommended
11
+ */
10
12
  files: JAVASCRIPT_EXTENSIONS,
11
13
  rules: {
12
14
  "constructor-super": "error", // ts(2335) & ts(2377)
@@ -84,54 +86,6 @@ export const config = [
84
86
  "no-var": "error",
85
87
  "no-with": "error",
86
88
  "object-shorthand": ["error", "always"],
87
- "padding-line-between-statements": [
88
- "error",
89
- {
90
- blankLine: "always",
91
- next: "*",
92
- prev: "*",
93
- },
94
- {
95
- blankLine: "never",
96
- next: ["const", "let", "var"],
97
- prev: ["const", "let", "var"],
98
- },
99
- {
100
- blankLine: "never",
101
- next: ["case", "default"],
102
- prev: ["case", "default"],
103
- },
104
- {
105
- blankLine: "always",
106
- next: ["const", "let", "var"],
107
- prev: ["multiline-const", "multiline-let", "multiline-var"],
108
- },
109
- {
110
- blankLine: "always",
111
- next: ["multiline-const", "multiline-let", "multiline-var"],
112
- prev: ["const", "let", "var"],
113
- },
114
- {
115
- blankLine: "any",
116
- next: ["expression"],
117
- prev: ["expression"],
118
- },
119
- {
120
- blankLine: "any",
121
- next: ["const", "let", "var"],
122
- prev: ["cjs-import"],
123
- },
124
- {
125
- blankLine: "any",
126
- next: ["cjs-import", "import"],
127
- prev: ["cjs-import", "import"],
128
- },
129
- {
130
- blankLine: "any",
131
- next: ["cjs-export", "export"],
132
- prev: ["cjs-export", "export"],
133
- },
134
- ],
135
89
  "prefer-arrow-callback": ["error", { allowNamedFunctions: true }],
136
90
  "prefer-const": "error",
137
91
  "prefer-rest-params": "error",
@@ -1,8 +1,69 @@
1
- import { JAVASCRIPT_LIKE_EXTENSIONS } from "../constants";
1
+ import stylistic from "@stylistic/eslint-plugin";
2
+
3
+ import { JAVASCRIPT_LIKE_EXTENSIONS } from "../constants.js";
2
4
 
3
5
  export const config = [
4
6
  {
5
7
  files: JAVASCRIPT_LIKE_EXTENSIONS,
6
- rules: {},
8
+ plugins: {
9
+ "@stylistic": stylistic,
10
+ },
11
+ rules: {
12
+ "@stylistic/lines-between-class-members": ["error", "always"],
13
+ "@stylistic/multiline-comment-style": ["error", "starred-block"],
14
+ "@stylistic/padding-line-between-statements": [
15
+ "error",
16
+ {
17
+ blankLine: "always",
18
+ next: "*",
19
+ prev: "*",
20
+ },
21
+ {
22
+ blankLine: "never",
23
+ next: ["const", "let", "var"],
24
+ prev: ["const", "let", "var"],
25
+ },
26
+ {
27
+ blankLine: "never",
28
+ next: ["case", "default"],
29
+ prev: ["case", "default"],
30
+ },
31
+ {
32
+ blankLine: "always",
33
+ next: ["const", "let", "var"],
34
+ prev: ["multiline-const", "multiline-let", "multiline-var"],
35
+ },
36
+ {
37
+ blankLine: "always",
38
+ next: ["multiline-const", "multiline-let", "multiline-var"],
39
+ prev: ["const", "let", "var"],
40
+ },
41
+ {
42
+ blankLine: "any",
43
+ next: ["expression"],
44
+ prev: ["expression"],
45
+ },
46
+ {
47
+ blankLine: "any",
48
+ next: ["const", "let", "var"],
49
+ prev: ["cjs-import"],
50
+ },
51
+ {
52
+ blankLine: "any",
53
+ next: ["cjs-import", "import"],
54
+ prev: ["cjs-import", "import"],
55
+ },
56
+ {
57
+ blankLine: "any",
58
+ next: ["cjs-export", "export"],
59
+ prev: ["cjs-export", "export"],
60
+ },
61
+ ],
62
+ "@stylistic/spaced-comment": [
63
+ "error",
64
+ "always",
65
+ { block: { balanced: true } },
66
+ ],
67
+ },
7
68
  },
8
69
  ];
@@ -10,7 +10,6 @@ export const config = [
10
10
  "sort-keys-custom-order": sortKeysCustomOrderPlugin,
11
11
  },
12
12
  rules: {
13
- //#region sort-keys-custom-order
14
13
  "sort-keys-custom-order/object-keys": [
15
14
  "error",
16
15
  {
@@ -23,7 +22,6 @@ export const config = [
23
22
  orderedKeys: ["id", "key", "name", "title", "label", "description"],
24
23
  },
25
24
  ],
26
- //#endregion
27
25
  },
28
26
  },
29
27
  ...new FlatCompat().extends("plugin:mdx/recommended").map((mdxConfig) => ({
@@ -8,7 +8,7 @@
8
8
 
9
9
  ## ✨ Features
10
10
 
11
- This package offers an opinionated but extensible **[Prettier](https://prettier.io/)** configurations to align formatting rules and standards across my projects.
11
+ This package offers opinionated but extensible **[Prettier](https://prettier.io/)** configurations to align code formatting conventions across projects.
12
12
 
13
13
  It allows to:
14
14
 
@@ -8,7 +8,7 @@
8
8
 
9
9
  ## ✨ Features
10
10
 
11
- This package offers an opinionated but extensible **[TSConfigs](https://www.typescriptlang.org/tsconfig/)** to align TypeScript configurations across my projects.
11
+ This package offers opinionated but extensible **[TypeScript](https://www.typescriptlang.org/tsconfig/)** configurations to align type checking and processing across projects.
12
12
 
13
13
  It allows to:
14
14
 
package/dist/index.js CHANGED
@@ -2,15 +2,20 @@ 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 { existsSync, cpSync, readFileSync, renameSync, writeFileSync, readdirSync } from 'node:fs';
6
5
  import { writeFile, chmod, mkdir, symlink } from 'node:fs/promises';
6
+ import { existsSync, cpSync, readFileSync, renameSync, writeFileSync, readdirSync } from 'node:fs';
7
7
  import { fdir } from 'fdir';
8
8
 
9
- var version = "0.0.0-next-d23a669";
9
+ var version = "0.0.0-next-ece28ab";
10
10
 
11
11
  const VERSION = version;
12
12
 
13
13
  const require = createRequire(import.meta.url);
14
+ function assert(expectedCondition, createError) {
15
+ if (!expectedCondition) {
16
+ throw createError();
17
+ }
18
+ }
14
19
  /**
15
20
  * Helper to format log messages with a welcoming bot.
16
21
  * @param input - Message factory.
@@ -58,6 +63,14 @@ ${input.body}
58
63
  */ const resolveFromStackDirectory = (path)=>{
59
64
  return resolve(import.meta.dirname, "../", path);
60
65
  };
66
+ const createError = (bin, error)=>{
67
+ const errorMessage = `\`${bin}\` command failed.\n${String(error)}`;
68
+ if (error instanceof Error) {
69
+ error.message = errorMessage;
70
+ return error;
71
+ }
72
+ return new Error(errorMessage);
73
+ };
61
74
  const getNpmVersion = async ()=>{
62
75
  try {
63
76
  return await helpers.exec("pnpm -v");
@@ -66,10 +79,11 @@ const getNpmVersion = async ()=>{
66
79
  }
67
80
  };
68
81
  const getStackCommand = (command, isNodeRuntime = true)=>{
69
- // @note: `isNodeRuntime` allows executing node bin executables in a non node environment such as in git hooks context
70
- // Npx is used to make executable resolution independent from the build tool (npx is the built-in Node tool)
71
- // `--no` flag to prevent installation prompt and throw an error if the binary is not installed
72
- return [
82
+ /**
83
+ * `isNodeRuntime` allows executing node bin executables in a non node environment such as in git hooks context
84
+ * Npx is used to make executable resolution independent from the build tool (npx is the built-in Node tool)
85
+ * `--no` flag to prevent installation prompt and throw an error if the binary is not installed.
86
+ */ return [
73
87
  ...isNodeRuntime ? [] : [
74
88
  "npx --no"
75
89
  ],
@@ -120,60 +134,6 @@ const eslint = (options)=>async (files = [])=>{
120
134
  throw createError("eslint", error);
121
135
  }
122
136
  };
123
- const checkLints = eslint({
124
- isFixMode: false
125
- });
126
- const fixLints = eslint({
127
- isFixMode: true
128
- });
129
- const fixFormatting = async (files)=>{
130
- let prettierFiles = [];
131
- if (files.length === 0) {
132
- prettierFiles.push(`"**/!(${PRETTIER_IGNORE_FILES.join("|")})"`);
133
- } else {
134
- prettierFiles = files.filter((file)=>{
135
- 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
136
- file !== "README.md";
137
- });
138
- if (prettierFiles.length === 0) return Promise.resolve();
139
- }
140
- const args = [
141
- ...prettierFiles
142
- ];
143
- if (existsSync(resolveFromProjectDirectory(".gitignore"))) {
144
- args.push("--ignore-path .gitignore");
145
- }
146
- args.push("--write");
147
- args.push("--ignore-unknown");
148
- args.push("--no-error-on-unmatched-pattern");
149
- try {
150
- return await helpers.exec(`prettier ${args.join(" ")}`);
151
- } catch (error) {
152
- throw createError("prettier", error);
153
- }
154
- };
155
- const checkTypes = async ()=>{
156
- try {
157
- return await helpers.exec("pnpm --parallel exec tsc --noEmit");
158
- } catch (error) {
159
- throw createError("tsc", error);
160
- }
161
- };
162
- const checkCommit = async ()=>{
163
- try {
164
- return await helpers.exec('commitlint --extends "@commitlint/config-conventional" --edit');
165
- } catch (error) {
166
- throw createError("commitlint", error);
167
- }
168
- };
169
- const createError = (bin, error)=>{
170
- const errorMessage = `\`${bin}\` command failed.\n${String(error)}`;
171
- if (error instanceof Error) {
172
- error.message = errorMessage;
173
- return error;
174
- }
175
- return new Error(errorMessage);
176
- };
177
137
  const turbo = async (command, options = {
178
138
  hasLiveOutput: true
179
139
  })=>{
@@ -192,26 +152,17 @@ const changeset = async (command)=>{
192
152
  throw createError("changeset", error);
193
153
  }
194
154
  };
195
- const TYPESCRIPT_EXTENSIONS = [
196
- "ts",
197
- "tsx",
198
- "cts",
199
- "mts"
200
- ];
201
- /**
202
- * Extensions supported by ESLint.
203
- */ const ESLINT_EXTENSIONS = [
155
+ const ESLINT_EXTENSIONS = [
204
156
  "js",
205
157
  "jsx",
206
158
  "cjs",
207
159
  "mjs",
160
+ "ts",
161
+ "tsx",
162
+ "cts",
163
+ "mts",
208
164
  "md",
209
- "mdx",
210
- ...TYPESCRIPT_EXTENSIONS
211
- ];
212
- const PRETTIER_IGNORE_FILES = [
213
- "CHANGELOG.md",
214
- "pnpm-lock.yaml"
165
+ "mdx"
215
166
  ];
216
167
 
217
168
  const createWatchCommand = (program)=>{
@@ -265,6 +216,7 @@ const createReleaseCommand = (program)=>{
265
216
  description: "Publish package(s) to the registry"
266
217
  }).task({
267
218
  async handler () {
219
+ // TODO: label instead?
268
220
  helpers.message("New changelog entry\n");
269
221
  await changeset("changeset");
270
222
  },
@@ -292,12 +244,12 @@ const createInstallCommand = (program)=>{
292
244
  name: "install",
293
245
  description: "Setup initial requirements"
294
246
  }).task({
295
- label: label$4("Installing `git.pre-commit` hook"),
247
+ label: label$4("Install `git.pre-commit` hook"),
296
248
  async handler () {
297
249
  await installGitHook("pre-commit", `${getStackCommand("fix $(git status --porcelain | awk 'BEGIN{ ORS=\" \" } { print $2 }')", false)} && git add -A`);
298
250
  }
299
251
  }).task({
300
- label: label$4("Installing `git.commit-msg` hook"),
252
+ label: label$4("Install `git.commit-msg` hook"),
301
253
  async handler () {
302
254
  await installGitHook("commit-msg", getStackCommand("check --only commit", false));
303
255
  }
@@ -310,24 +262,59 @@ const installGitHook = async (hook, content)=>{
310
262
  return chmod(filename, "0755");
311
263
  };
312
264
 
265
+ const fixLinter = eslint({
266
+ isFixMode: true
267
+ });
268
+
269
+ const PRETTIER_IGNORE_FILES = [
270
+ "CHANGELOG.md",
271
+ "pnpm-lock.yaml"
272
+ ];
273
+ const fixFormatting = async (files)=>{
274
+ let prettierFiles = [];
275
+ if (files.length === 0) {
276
+ prettierFiles.push(`"**/!(${PRETTIER_IGNORE_FILES.join("|")})"`);
277
+ } else {
278
+ prettierFiles = files.filter((file)=>{
279
+ 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
280
+ file !== "README.md";
281
+ });
282
+ if (prettierFiles.length === 0) return Promise.resolve();
283
+ }
284
+ const args = [
285
+ ...prettierFiles
286
+ ];
287
+ if (existsSync(resolveFromProjectDirectory(".gitignore"))) {
288
+ args.push("--ignore-path .gitignore");
289
+ }
290
+ args.push("--write");
291
+ args.push("--ignore-unknown");
292
+ args.push("--no-error-on-unmatched-pattern");
293
+ try {
294
+ return await helpers.exec(`prettier ${args.join(" ")}`);
295
+ } catch (error) {
296
+ throw createError("prettier", error);
297
+ }
298
+ };
299
+
313
300
  const createFixCommand = (program)=>{
314
301
  program.command({
315
302
  name: "fix",
316
303
  description: "Fix auto-fixable issues"
317
304
  }).task({
318
- label: label$3("Preparing the project"),
305
+ label: label$3("Prepare the project"),
319
306
  async handler () {
320
307
  await turbo("build", {
321
308
  hasLiveOutput: false
322
309
  });
323
310
  }
324
311
  }).task({
325
- label: label$3("Fixing lints"),
312
+ label: label$3("Fix linter issues"),
326
313
  async handler (_, argv) {
327
- await fixLints(argv.operands);
314
+ await fixLinter(argv.operands);
328
315
  }
329
316
  }).task({
330
- label: label$3("Fixing formatting"),
317
+ label: label$3("Fix formatting issues"),
331
318
  async handler (_, argv) {
332
319
  await fixFormatting(argv.operands);
333
320
  }
@@ -378,7 +365,7 @@ const createCreateCommand = (program)=>{
378
365
  }
379
366
  }).task({
380
367
  key: "data",
381
- label: label$2("Evaluating contextual data"),
368
+ label: label$2("Evaluate contextual data"),
382
369
  async handler ({ inputDescription, inputName, inputUrl }) {
383
370
  const nodeVersion = (await request.get("https://resolve-node.vercel.app/lts", "text")).replace("v", "");
384
371
  const npmVersion = (await request.get("https://registry.npmjs.org/pnpm/latest", "json")).version;
@@ -398,7 +385,7 @@ const createCreateCommand = (program)=>{
398
385
  }
399
386
  }).task({
400
387
  label ({ data }) {
401
- return label$2(`Creating \`${data.projectName}\` folder`);
388
+ return label$2(`Create \`${data.projectName}\` folder`);
402
389
  },
403
390
  async handler ({ data }) {
404
391
  const projectPath = resolve(process.cwd(), data.projectName);
@@ -406,18 +393,18 @@ const createCreateCommand = (program)=>{
406
393
  process.chdir(projectPath);
407
394
  }
408
395
  }).task({
409
- label: label$2("Initializing `git`"),
396
+ label: label$2("Initialize `git`"),
410
397
  async handler ({ data }) {
411
398
  await helpers.exec("git init");
412
399
  await helpers.exec(`git remote add origin ${data.projectUrl}`);
413
400
  }
414
401
  }).task({
415
- label: label$2("Applying template"),
402
+ label: label$2("Apply template"),
416
403
  handler ({ data, inputTemplate }) {
417
404
  applyTemplate(inputTemplate, data);
418
405
  }
419
406
  }).task({
420
- label: label$2("Installing dependencies"),
407
+ label: label$2("Install dependencies"),
421
408
  async handler ({ data }) {
422
409
  const localDevDependencies = [
423
410
  "quickbundle",
@@ -435,7 +422,7 @@ const createCreateCommand = (program)=>{
435
422
  }
436
423
  }).task({
437
424
  key: "error",
438
- label: label$2("Cleaning up"),
425
+ label: label$2("Clean up"),
439
426
  async handler ({ data }) {
440
427
  try {
441
428
  // Symlink the package `README.md` file to the root project directory
@@ -517,7 +504,7 @@ const createCleanCommand = (program)=>{
517
504
  description: "Clean the project"
518
505
  }).task({
519
506
  key: "files",
520
- label: label$1("Retrieving removable assets"),
507
+ label: label$1("Retrieve removable files"),
521
508
  async handler () {
522
509
  const cachePath = "node_modules/.cache";
523
510
  const files = await retrieveIgnoredFiles();
@@ -561,10 +548,136 @@ const PRESERVE_FILES = [
561
548
  ".turbo"
562
549
  ];
563
550
 
564
- const onlyValues = [
551
+ const checkTypes = async ()=>{
552
+ try {
553
+ return await helpers.exec("pnpm --parallel exec tsc --noEmit");
554
+ } catch (error) {
555
+ throw createError("tsc", error);
556
+ }
557
+ };
558
+
559
+ const checkPackages = async ()=>{
560
+ const stdout = await helpers.exec("pnpm recursive ls --json");
561
+ 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 ?? {};
569
+ return {
570
+ name: pkg.name,
571
+ dependencies,
572
+ devDependencies,
573
+ peerDependencies
574
+ };
575
+ });
576
+ for (const pkg of packages){
577
+ // 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);
579
+ // Check version range accordingly to our dependency guidelines (ie. dev dependencies must be pinned and dependencies must have caret)
580
+ checkPackagesVersionRange(pkg);
581
+ }
582
+ };
583
+ const checkPackagesVersionRange = ({ name, dependencies, devDependencies, peerDependencies })=>{
584
+ for (const dependencyName of Object.keys(devDependencies)){
585
+ const version = devDependencies[dependencyName];
586
+ assertVersion(version, {
587
+ name: dependencyName,
588
+ consumedBy: name
589
+ });
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.`, {
591
+ name: dependencyName,
592
+ consumedBy: name
593
+ });
594
+ }
595
+ const isPreReleaseVersion = (version)=>/\d+\.\d+\.\d+-(alpha|beta|experimental|next|rc).*/.exec(version);
596
+ const hasNoCaret = (version)=>!isPreReleaseVersion(version) && !/^\^/.exec(version);
597
+ for (const dependencyName of Object.keys(dependencies)){
598
+ const version = dependencies[dependencyName];
599
+ assertVersion(version, {
600
+ name: dependencyName,
601
+ consumedBy: name
602
+ });
603
+ if (version !== "workspace:^" && hasNoCaret(version)) throw createPackageError(`As a dependency, \`${dependencyName}\` version must be prefixed with a caret (or set as "workspace:^" for local packages) to optimize the size (whether of installation or bundle output) on the consumer side.`, {
604
+ name: dependencyName,
605
+ consumedBy: name
606
+ });
607
+ }
608
+ for (const dependencyName of Object.keys(peerDependencies)){
609
+ const version = peerDependencies[dependencyName];
610
+ assertVersion(version, {
611
+ name: dependencyName,
612
+ consumedBy: name
613
+ });
614
+ if (hasNoCaret(version)) /*
615
+ * Why disallowing workspace protocol as a version resolver?
616
+ * To reduce the update frequency needs consumer-side and guarantee on our side the minimum compatible version,
617
+ * the best practice should be to keeping an explicit number version which represents the lowest compatible version from an API perspective.
618
+ */ throw createPackageError(`As a peer dependency, \`${dependencyName}\` version must be explicit (i.e. the "workspace:^" protocol a version resolver is not allowed) and prefixed with a caret to optimize the size (whether of installation or bundle output) on the consumer side.`, {
619
+ name: dependencyName,
620
+ consumedBy: name
621
+ });
622
+ }
623
+ };
624
+ const createPackagesVersionMismatchChecker = ()=>{
625
+ 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;
632
+ for (const dependencyName of Object.keys(dependencies)){
633
+ const depVersion = dependencies[dependencyName];
634
+ if (!depVersion) continue;
635
+ const storedVersion = store.get(dependencyName);
636
+ if (!storedVersion) {
637
+ store.set(dependencyName, depVersion);
638
+ continue;
639
+ }
640
+ const isSameMonorepoVersion = depVersion === storedVersion;
641
+ 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}\`.`, {
643
+ name: dependencyName,
644
+ consumedBy: pkgName
645
+ });
646
+ }
647
+ }
648
+ };
649
+ return (pkg)=>{
650
+ lint(pkg, "development");
651
+ lint(pkg, "production");
652
+ };
653
+ };
654
+ const createPackageError = (message, context)=>{
655
+ return createError("stack check", !context ? message : `\`${context.name}\` consumed by \`${context.consumedBy}\` doesn't conform to our package policy.\n${message}`);
656
+ };
657
+ function assertVersion(version, { name, consumedBy }) {
658
+ assert(version, ()=>createPackageError(`\`${name}\` must have a valid version specified (current version equals to \`${String(version)}\`).`, {
659
+ name,
660
+ consumedBy
661
+ }));
662
+ }
663
+
664
+ const checkLinter = eslint({
665
+ isFixMode: false
666
+ });
667
+
668
+ const checkCommit = async ()=>{
669
+ try {
670
+ return await helpers.exec('commitlint --extends "@commitlint/config-conventional" --edit');
671
+ } catch (error) {
672
+ throw createError("commitlint", error);
673
+ }
674
+ };
675
+
676
+ const ONLY_VALUES = [
565
677
  "commit",
566
- "lint",
567
- "type"
678
+ "linter",
679
+ "packages",
680
+ "types"
568
681
  ];
569
682
  const createCheckCommand = (program)=>{
570
683
  program.command({
@@ -573,10 +686,10 @@ const createCheckCommand = (program)=>{
573
686
  }).option({
574
687
  key: "only",
575
688
  name: "only",
576
- description: `Run only one specified task (accepted value: ${onlyValues.join(", ")})`,
689
+ description: `Run only one specified task (accepted value: ${ONLY_VALUES.join(", ")})`,
577
690
  defaultValue: undefined
578
691
  }).task({
579
- label: label("Preparing the project"),
692
+ label: label("Prepare the project"),
580
693
  async handler () {
581
694
  await turbo("build", {
582
695
  hasLiveOutput: false
@@ -586,24 +699,31 @@ const createCheckCommand = (program)=>{
586
699
  return only === "commit"; // No need to build if only commitlint is run
587
700
  }
588
701
  }).task({
589
- label: label("Checking linters"),
702
+ label: label("Check package guidelines"),
703
+ async handler () {
704
+ await checkPackages();
705
+ },
706
+ skip: ifOnlyDefinedAndNotEqualTo("packages")
707
+ }).task({
708
+ label: label("Check linter rules"),
590
709
  async handler (_, argv) {
591
710
  const filenames = argv.operands;
592
- await checkLints(filenames);
711
+ await checkLinter(filenames);
593
712
  },
594
- skip: ifOnlyDefinedAndNotEqualTo("lint")
713
+ skip: ifOnlyDefinedAndNotEqualTo("linter")
595
714
  }).task({
596
- label: label("Checking types"),
715
+ label: label("Check types"),
597
716
  async handler () {
598
717
  await checkTypes();
599
718
  },
600
719
  skip (context, argv) {
601
- return ifOnlyDefinedAndNotEqualTo("type")(context) || !hasDependency("typescript") || // For now, skip type-checking if some files are passed down
602
- // @see: https://github.com/microsoft/TypeScript/issues/27379
603
- argv.operands.length > 0;
720
+ return ifOnlyDefinedAndNotEqualTo("types")(context) || !hasDependency("typescript") || /**
721
+ * For now, skip type-checking if some files are passed down.
722
+ * @see https://github.com/microsoft/TypeScript/issues/27379
723
+ */ argv.operands.length > 0;
604
724
  }
605
725
  }).task({
606
- label: label("Checking commit"),
726
+ label: label("Check commit"),
607
727
  async handler () {
608
728
  await checkCommit();
609
729
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adbayb/stack",
3
- "version": "0.0.0-next-d23a669",
3
+ "version": "0.0.0-next-ece28ab",
4
4
  "description": "My opinionated JavaScript-based toolchain",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -42,7 +42,6 @@
42
42
  },
43
43
  "license": "MIT",
44
44
  "devDependencies": {
45
- "@types/eslint-config-prettier": "6.11.3",
46
45
  "@types/node": "22.8.6",
47
46
  "quickbundle": "2.6.0"
48
47
  },