@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 +20 -16
- package/config/eslint/README.md +33 -1
- package/config/eslint/index.js +4 -2
- package/config/eslint/presets/eslint.js +4 -50
- package/config/eslint/presets/stylistic.js +63 -2
- package/config/eslint/presets/uncategorized.js +0 -2
- package/config/prettier/README.md +1 -1
- package/config/typescript/README.md +1 -1
- package/dist/index.js +221 -101
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -59,19 +59,13 @@ stack release
|
|
|
59
59
|
|
|
60
60
|
<br>
|
|
61
61
|
|
|
62
|
-
##
|
|
62
|
+
## 🌍 Ecosystem
|
|
63
63
|
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
90
|
+
## 🏗️ Architecture
|
|
91
|
+
|
|
92
|
+
### Project creation workflow
|
|
97
93
|
|
|
98
|
-
|
|
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
|
|
package/config/eslint/README.md
CHANGED
|
@@ -1,7 +1,39 @@
|
|
|
1
1
|
<br>
|
|
2
2
|
<div align="center">
|
|
3
|
-
<h1>@adbayb/stack/
|
|
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 />
|
package/config/eslint/index.js
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
312
|
+
label: label$3("Fix linter issues"),
|
|
326
313
|
async handler (_, argv) {
|
|
327
|
-
await
|
|
314
|
+
await fixLinter(argv.operands);
|
|
328
315
|
}
|
|
329
316
|
}).task({
|
|
330
|
-
label: label$3("
|
|
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("
|
|
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(`
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
|
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
|
-
"
|
|
567
|
-
"
|
|
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: ${
|
|
689
|
+
description: `Run only one specified task (accepted value: ${ONLY_VALUES.join(", ")})`,
|
|
577
690
|
defaultValue: undefined
|
|
578
691
|
}).task({
|
|
579
|
-
label: label("
|
|
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("
|
|
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
|
|
711
|
+
await checkLinter(filenames);
|
|
593
712
|
},
|
|
594
|
-
skip: ifOnlyDefinedAndNotEqualTo("
|
|
713
|
+
skip: ifOnlyDefinedAndNotEqualTo("linter")
|
|
595
714
|
}).task({
|
|
596
|
-
label: label("
|
|
715
|
+
label: label("Check types"),
|
|
597
716
|
async handler () {
|
|
598
717
|
await checkTypes();
|
|
599
718
|
},
|
|
600
719
|
skip (context, argv) {
|
|
601
|
-
return ifOnlyDefinedAndNotEqualTo("
|
|
602
|
-
|
|
603
|
-
|
|
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("
|
|
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-
|
|
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
|
},
|