@adbayb/stack 0.0.0-next-5dd260c → 0.0.0-next.d8e717b
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 +4 -4
- package/bin/index.js +2 -4
- package/configs/{prettier → oxfmt}/README.md +6 -14
- package/configs/oxfmt/index.ts +41 -0
- package/configs/{eslint → oxlint}/README.md +4 -4
- package/configs/oxlint/index.ts +319 -0
- package/configs/typescript/index.json +1 -7
- package/dist/configs/oxfmt.d.ts +32 -0
- package/dist/configs/oxfmt.js +37 -0
- package/dist/configs/oxlint.d.ts +273 -0
- package/dist/configs/oxlint.js +325 -0
- package/dist/index.js +819 -779
- package/package.json +44 -51
- package/templates/multi-projects/.github/renovate.json +0 -3
- package/templates/multi-projects/.github/workflows/continuous_delivery.yml +8 -14
- package/templates/multi-projects/.github/workflows/conventional_commit.yml +1 -1
- package/templates/multi-projects/.github/workflows/dependency_changelog.yml +1 -1
- package/templates/multi-projects/.github/workflows/workflow.yml +4 -4
- package/templates/multi-projects/.gitignore.tmpl +45 -32
- package/templates/multi-projects/.vscode/extensions.json +1 -6
- package/templates/multi-projects/.vscode/settings.json +10 -26
- package/templates/multi-projects/libraries/{{projectName}}/{package.json.tmpl → package.json} +15 -17
- package/templates/multi-projects/libraries/{{projectName}}/src/{index.test.ts.tmpl → index.test.ts} +2 -0
- package/templates/multi-projects/oxfmt.config.ts +1 -0
- package/templates/multi-projects/oxlint.config.ts +1 -0
- package/templates/{single-project/package.json.tmpl → multi-projects/package.json} +11 -5
- package/templates/multi-projects/pnpm-workspace.yaml +21 -0
- package/templates/single-project/.github/renovate.json +0 -3
- package/templates/single-project/.github/workflows/continuous_delivery.yml +8 -14
- package/templates/single-project/.github/workflows/conventional_commit.yml +1 -1
- package/templates/single-project/.github/workflows/dependency_changelog.yml +1 -1
- package/templates/single-project/.github/workflows/workflow.yml +4 -4
- package/templates/single-project/.gitignore.tmpl +45 -32
- package/templates/single-project/.vscode/extensions.json +1 -6
- package/templates/single-project/.vscode/settings.json +10 -26
- package/templates/single-project/oxfmt.config.ts +1 -0
- package/templates/single-project/oxlint.config.ts +1 -0
- package/templates/{multi-projects/package.json.tmpl → single-project/package.json} +11 -5
- package/templates/single-project/pnpm-workspace.yaml +20 -0
- package/templates/single-project/{{projectName}}/{package.json.tmpl → package.json} +15 -17
- package/templates/single-project/{{projectName}}/src/{index.test.ts.tmpl → index.test.ts} +2 -0
- package/configs/eslint/constants.js +0 -37
- package/configs/eslint/helpers.js +0 -5
- package/configs/eslint/index.js +0 -30
- package/configs/eslint/presets/dependencies.js +0 -14
- package/configs/eslint/presets/eslint.js +0 -133
- package/configs/eslint/presets/import.js +0 -65
- package/configs/eslint/presets/jsdoc.js +0 -78
- package/configs/eslint/presets/markdown.js +0 -1
- package/configs/eslint/presets/node.js +0 -50
- package/configs/eslint/presets/prettier.js +0 -1
- package/configs/eslint/presets/react.js +0 -101
- package/configs/eslint/presets/sonar.js +0 -202
- package/configs/eslint/presets/stylistic.js +0 -79
- package/configs/eslint/presets/test.js +0 -87
- package/configs/eslint/presets/typescript.js +0 -198
- package/configs/eslint/presets/unicorn.js +0 -143
- package/configs/prettier/index.js +0 -25
- package/templates/multi-projects/.npmrc.tmpl +0 -8
- package/templates/multi-projects/.nvmrc.tmpl +0 -1
- package/templates/multi-projects/eslint.config.js.tmpl +0 -1
- package/templates/multi-projects/pnpm-workspace.yaml.tmpl +0 -5
- package/templates/single-project/.npmrc.tmpl +0 -8
- package/templates/single-project/.nvmrc.tmpl +0 -1
- package/templates/single-project/eslint.config.js.tmpl +0 -1
- package/templates/single-project/pnpm-workspace.yaml.tmpl +0 -4
- /package/templates/multi-projects/.changeset/{config.json.tmpl → config.json} +0 -0
- /package/templates/multi-projects/.changeset/{welcome.md.tmpl → welcome.md} +0 -0
- /package/templates/multi-projects/.github/{PULL_REQUEST_TEMPLATE.md.tmpl → PULL_REQUEST_TEMPLATE.md} +0 -0
- /package/templates/multi-projects/{CONTRIBUTING.md.tmpl → CONTRIBUTING.md} +0 -0
- /package/templates/multi-projects/{LICENSE.tmpl → LICENSE} +0 -0
- /package/templates/multi-projects/examples/default/{package.json.tmpl → package.json} +0 -0
- /package/templates/multi-projects/examples/default/src/{index.ts.tmpl → index.ts} +0 -0
- /package/templates/multi-projects/libraries/{{projectName}}/{README.md.tmpl → README.md} +0 -0
- /package/templates/single-project/.changeset/{config.json.tmpl → config.json} +0 -0
- /package/templates/single-project/.changeset/{welcome.md.tmpl → welcome.md} +0 -0
- /package/templates/single-project/.github/{PULL_REQUEST_TEMPLATE.md.tmpl → PULL_REQUEST_TEMPLATE.md} +0 -0
- /package/templates/single-project/{CONTRIBUTING.md.tmpl → CONTRIBUTING.md} +0 -0
- /package/templates/single-project/{LICENSE.tmpl → LICENSE} +0 -0
- /package/templates/single-project/examples/default/{package.json.tmpl → package.json} +0 -0
- /package/templates/single-project/examples/default/src/{index.ts.tmpl → index.ts} +0 -0
- /package/templates/single-project/{{projectName}}/{README.md.tmpl → README.md} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,820 +1,860 @@
|
|
|
1
|
-
import { helpers, termost } from
|
|
2
|
-
import { createRequire } from
|
|
3
|
-
import {
|
|
4
|
-
import { existsSync, readdirSync
|
|
5
|
-
import {
|
|
6
|
-
import { fdir } from 'fdir';
|
|
1
|
+
import { helpers, termost } from "termost";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { basename, join, resolve } from "node:path";
|
|
4
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
5
|
+
import { chmod, cp, mkdir, readFile, readdir, rename, rm, symlink, writeFile } from "node:fs/promises";
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
7
|
+
//#region src/helpers.ts
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const assert = (expectedCondition, createError) => {
|
|
10
|
+
if (!expectedCondition) throw createError();
|
|
11
|
+
};
|
|
14
12
|
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
error: "red",
|
|
36
|
-
information: "blue",
|
|
37
|
-
success: "green"
|
|
38
|
-
};
|
|
39
|
-
log(helpers.format(`
|
|
13
|
+
* Helper to format log messages with a welcoming bot.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* botMessage({
|
|
17
|
+
* title: "Oops, an error occurred",
|
|
18
|
+
* description: "Keep calm and carry on with some coffee ☕️",
|
|
19
|
+
* body: String(previousTaskError),
|
|
20
|
+
* type: "error",
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* @param input - Message factory.
|
|
24
|
+
* @param input.title - Title input.
|
|
25
|
+
* @param input.description - Description input.
|
|
26
|
+
* @param input.body - Body input.
|
|
27
|
+
* @param input.type - Message type.
|
|
28
|
+
*/
|
|
29
|
+
const botMessage = (input) => {
|
|
30
|
+
const { type } = input;
|
|
31
|
+
const log = console[type === "error" ? "error" : "log"];
|
|
32
|
+
log(helpers.format(`
|
|
40
33
|
╭─────╮
|
|
41
34
|
│ ◠ ◠ ${input.title}
|
|
42
35
|
│ ${type === "error" ? "◠" : "◡"} │ ${input.description}
|
|
43
36
|
╰─────╯
|
|
44
37
|
${input.body ? `
|
|
45
38
|
${input.body}
|
|
46
|
-
` : ""}`, {
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
` : ""}`, { color: {
|
|
40
|
+
error: "red",
|
|
41
|
+
information: "blue",
|
|
42
|
+
success: "green"
|
|
43
|
+
}[type] }));
|
|
49
44
|
};
|
|
50
45
|
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
46
|
+
* Resolve a relative path to an absolute one resolved from the generated project root directory.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* resolveFromWorkingDirectory(".gitignore");
|
|
50
|
+
*
|
|
51
|
+
* @param path - The relative path.
|
|
52
|
+
* @returns The resolved absolute path.
|
|
53
|
+
*/
|
|
54
|
+
const resolveFromWorkingDirectory = (...path) => {
|
|
55
|
+
return resolve(process.cwd(), ...path);
|
|
58
56
|
};
|
|
59
57
|
/**
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
end: true,
|
|
174
|
-
start: false
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
};
|
|
178
|
-
const changeset = async (command)=>{
|
|
179
|
-
try {
|
|
180
|
-
return await helpers.exec(command, {
|
|
181
|
-
hasLiveOutput: true
|
|
182
|
-
});
|
|
183
|
-
} catch (error) {
|
|
184
|
-
throw createError("changeset", error);
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
const ESLINT_EXTENSIONS = [
|
|
188
|
-
"js",
|
|
189
|
-
"jsx",
|
|
190
|
-
"cjs",
|
|
191
|
-
"mjs",
|
|
192
|
-
"ts",
|
|
193
|
-
"tsx",
|
|
194
|
-
"cts",
|
|
195
|
-
"mts",
|
|
196
|
-
"md",
|
|
197
|
-
"mdx"
|
|
198
|
-
];
|
|
58
|
+
* Resolve a relative path to an absolute one resolved from the `stack` node module directory.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* resolveFromPackageDirectory("./templates");
|
|
62
|
+
*
|
|
63
|
+
* @param path - The relative path.
|
|
64
|
+
* @returns The resolved absolute path.
|
|
65
|
+
*/
|
|
66
|
+
const resolveFromPackageDirectory = (...path) => {
|
|
67
|
+
return resolve(import.meta.dirname, "../", ...path);
|
|
68
|
+
};
|
|
69
|
+
const createError = (bin, error) => {
|
|
70
|
+
const errorMessage = `\`${bin}\` command failed.\n${String(error)}`;
|
|
71
|
+
if (error instanceof Error) {
|
|
72
|
+
error.message = errorMessage;
|
|
73
|
+
return error;
|
|
74
|
+
}
|
|
75
|
+
return new Error(errorMessage);
|
|
76
|
+
};
|
|
77
|
+
const getPnpmVersion = async () => {
|
|
78
|
+
try {
|
|
79
|
+
return await helpers.exec("pnpm -v");
|
|
80
|
+
} catch {
|
|
81
|
+
throw createError("pnpm", "The project must use `pnpm` as a node package manager tool. Follow this installation guide https://pnpm.io/installation");
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const getStackCommand = (command) => {
|
|
85
|
+
return `pnpm stack ${command}`;
|
|
86
|
+
};
|
|
87
|
+
const setPackageManager = async () => {
|
|
88
|
+
/**
|
|
89
|
+
* Corepack is downloaded remotely to get always up-to-date npm registry fingerprints since
|
|
90
|
+
* they're hardcoded.
|
|
91
|
+
*
|
|
92
|
+
* @see {@link https://github.com/nodejs/corepack/issues/613}
|
|
93
|
+
*/
|
|
94
|
+
await helpers.exec("pnx corepack enable");
|
|
95
|
+
};
|
|
96
|
+
const request = { async get(url, responseType) {
|
|
97
|
+
const response = await fetch(url);
|
|
98
|
+
if (!response.ok) throw createError("fetch", `Failed to fetch resources from ${url} (${JSON.stringify({
|
|
99
|
+
status: response.status,
|
|
100
|
+
statusText: response.statusText
|
|
101
|
+
})})`);
|
|
102
|
+
return responseType === "text" ? response.text() : response.json();
|
|
103
|
+
} };
|
|
104
|
+
const oxlint = (options) => {
|
|
105
|
+
return async (files = []) => {
|
|
106
|
+
const args = [
|
|
107
|
+
...files,
|
|
108
|
+
"--disable-nested-config",
|
|
109
|
+
"--no-error-on-unmatched-pattern"
|
|
110
|
+
];
|
|
111
|
+
if (options.isFixMode) args.push("--fix-dangerously");
|
|
112
|
+
try {
|
|
113
|
+
return await helpers.exec(`oxlint ${args.join(" ")}`);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
throw createError("oxlint", error instanceof Error ? error : new Error(String(error)));
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
const oxfmt = (options) => {
|
|
120
|
+
return async (files = []) => {
|
|
121
|
+
const args = [
|
|
122
|
+
...files,
|
|
123
|
+
"--disable-nested-config",
|
|
124
|
+
"--no-error-on-unmatched-pattern",
|
|
125
|
+
options.isFixMode ? "--write" : "--check"
|
|
126
|
+
];
|
|
127
|
+
try {
|
|
128
|
+
return await helpers.exec(`oxfmt ${args.join(" ")}`);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
throw createError("oxfmt", error instanceof Error ? error : new Error(String(error)));
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
const turbo = async (command, options = {}) => {
|
|
135
|
+
try {
|
|
136
|
+
const { excludeExamples = false, hasLiveOutput = true, ...restOptions } = options;
|
|
137
|
+
return await helpers.exec(`turbo run ${command} ${excludeExamples ? "--filter !@examples/*" : ""}`, {
|
|
138
|
+
...restOptions,
|
|
139
|
+
hasLiveOutput
|
|
140
|
+
});
|
|
141
|
+
} catch (error) {
|
|
142
|
+
throw createError("turbo", error instanceof Error ? error : new Error(String(error)));
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const logCheckableFiles = (files) => {
|
|
146
|
+
if (files.length === 0) {
|
|
147
|
+
helpers.message("The whole project will be checked.", {
|
|
148
|
+
label: false,
|
|
149
|
+
lineBreak: {
|
|
150
|
+
end: true,
|
|
151
|
+
start: false
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
helpers.message(files.join("\n "), {
|
|
157
|
+
label: "Following files will be checked:",
|
|
158
|
+
lineBreak: {
|
|
159
|
+
end: true,
|
|
160
|
+
start: false
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
const changeset = async (command) => {
|
|
165
|
+
try {
|
|
166
|
+
return await helpers.exec(command, { hasLiveOutput: true });
|
|
167
|
+
} catch (error) {
|
|
168
|
+
throw createError("changeset", error instanceof Error ? error : new Error(String(error)));
|
|
169
|
+
}
|
|
170
|
+
};
|
|
199
171
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/commands/build.ts
|
|
174
|
+
const createBuildCommand = (program) => {
|
|
175
|
+
program.command({
|
|
176
|
+
name: "build",
|
|
177
|
+
description: "Build the project in production mode"
|
|
178
|
+
}).task({ async handler() {
|
|
179
|
+
await turbo("build");
|
|
180
|
+
} });
|
|
209
181
|
};
|
|
210
182
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
});
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/commands/check/checkCode.ts
|
|
185
|
+
const checkCode = oxlint({ isFixMode: false });
|
|
214
186
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/commands/check/checkCommit.ts
|
|
189
|
+
const checkCommit = async () => {
|
|
190
|
+
try {
|
|
191
|
+
return await helpers.exec("commitlint --extends \"@commitlint/config-conventional\" --edit");
|
|
192
|
+
} catch (error) {
|
|
193
|
+
throw createError("commitlint", error instanceof Error ? error : new Error(String(error)));
|
|
194
|
+
}
|
|
221
195
|
};
|
|
222
196
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const isPreReleaseVersion = /\d+\.\d+\.\d+-(alpha|beta|experimental|next|rc).*/.exec(version);
|
|
327
|
-
const isNpmProtocol = version.startsWith("npm:");
|
|
328
|
-
return isNpmProtocol || isPreReleaseVersion;
|
|
329
|
-
};
|
|
330
|
-
const hasCaret = (version)=>version.startsWith("^");
|
|
331
|
-
|
|
332
|
-
const checkType = async ()=>{
|
|
333
|
-
try {
|
|
334
|
-
return await helpers.exec("pnpm --parallel exec tsc --noEmit");
|
|
335
|
-
} catch (error) {
|
|
336
|
-
throw createError("tsc", error);
|
|
337
|
-
}
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/commands/check/checkDependency.ts
|
|
199
|
+
const checkDependency = async () => {
|
|
200
|
+
const stdout = await helpers.exec("pnpm recursive ls --json");
|
|
201
|
+
const checkDependencyVersionMismatch = createPackagesVersionMismatchChecker();
|
|
202
|
+
const packages = JSON.parse(stdout).map((pkg) => {
|
|
203
|
+
const packagePath = join(pkg.path, "package.json");
|
|
204
|
+
assert(pkg.name, () => {
|
|
205
|
+
return createPackageError(`\`${packagePath}\` must have a name field.`);
|
|
206
|
+
});
|
|
207
|
+
const packageContent = require(packagePath);
|
|
208
|
+
const peerDependencies = packageContent.peerDependencies ?? {};
|
|
209
|
+
const devDependencies = packageContent.devDependencies ?? {};
|
|
210
|
+
const dependencies = packageContent.dependencies ?? {};
|
|
211
|
+
return {
|
|
212
|
+
name: pkg.name,
|
|
213
|
+
dependencies,
|
|
214
|
+
devDependencies,
|
|
215
|
+
peerDependencies
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
for (const pkg of packages) {
|
|
219
|
+
checkDependencyVersionMismatch(pkg);
|
|
220
|
+
checkDependencyVersionRange(pkg);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
const STARTING_WITH_DIGIT_REGEXP = /^\d/u;
|
|
224
|
+
const checkDependencyVersionRange = ({ name, dependencies, devDependencies, peerDependencies }) => {
|
|
225
|
+
for (const [dependencyName, version] of Object.entries(devDependencies)) {
|
|
226
|
+
assertVersion(version, {
|
|
227
|
+
name: dependencyName,
|
|
228
|
+
consumedBy: name
|
|
229
|
+
});
|
|
230
|
+
if (version !== "workspace:*" && !isExcluded(version) && !STARTING_WITH_DIGIT_REGEXP.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.`, {
|
|
231
|
+
name: dependencyName,
|
|
232
|
+
consumedBy: name
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
for (const [dependencyName, version] of Object.entries(dependencies)) {
|
|
236
|
+
assertVersion(version, {
|
|
237
|
+
name: dependencyName,
|
|
238
|
+
consumedBy: name
|
|
239
|
+
});
|
|
240
|
+
if (version !== "workspace:^" && !hasCaret(version) && !isExcluded(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.`, {
|
|
241
|
+
name: dependencyName,
|
|
242
|
+
consumedBy: name
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
for (const [dependencyName, version] of Object.entries(peerDependencies)) {
|
|
246
|
+
assertVersion(version, {
|
|
247
|
+
name: dependencyName,
|
|
248
|
+
consumedBy: name
|
|
249
|
+
});
|
|
250
|
+
if (!hasCaret(version) && !isExcluded(version)) 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.`, {
|
|
251
|
+
name: dependencyName,
|
|
252
|
+
consumedBy: name
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
const createPackagesVersionMismatchChecker = () => {
|
|
257
|
+
const monorepoDependencies = /* @__PURE__ */ new Map();
|
|
258
|
+
const monorepoDevelopmentDependencies = /* @__PURE__ */ new Map();
|
|
259
|
+
const lint = (pkg, type) => {
|
|
260
|
+
const packageName = pkg.name;
|
|
261
|
+
const isDevelopment = type === "development";
|
|
262
|
+
const store = isDevelopment ? monorepoDevelopmentDependencies : monorepoDependencies;
|
|
263
|
+
const dependencies = pkg[isDevelopment ? "devDependencies" : "dependencies"];
|
|
264
|
+
for (const [dependencyName, dependencyVersion] of Object.entries(dependencies)) {
|
|
265
|
+
if (!dependencyVersion) continue;
|
|
266
|
+
const storedVersion = store.get(dependencyName);
|
|
267
|
+
if (!storedVersion) {
|
|
268
|
+
store.set(dependencyName, dependencyVersion);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (!(dependencyVersion === storedVersion)) throw createPackageError(`Mismatched versions: received version \`${dependencyVersion}\` 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 \`${dependencyVersion}\`).`, {
|
|
272
|
+
name: dependencyName,
|
|
273
|
+
consumedBy: packageName
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
return (pkg) => {
|
|
278
|
+
lint(pkg, "development");
|
|
279
|
+
lint(pkg, "production");
|
|
280
|
+
};
|
|
281
|
+
};
|
|
282
|
+
const createPackageError = (message, context) => {
|
|
283
|
+
return createError("stack check", context ? `\`${context.name}\` consumed by \`${context.consumedBy}\` doesn't conform to package guidelines.\n${message}` : message);
|
|
284
|
+
};
|
|
285
|
+
const assertVersion = (version, { name, consumedBy }) => {
|
|
286
|
+
assert(version, () => {
|
|
287
|
+
return createPackageError(`\`${name}\` must have a valid version specified (current version equals to \`${String(version)}\`).`, {
|
|
288
|
+
name,
|
|
289
|
+
consumedBy
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
};
|
|
293
|
+
const PRERELEASE_VERSION_REGEXP = /\d+\.\d+\.\d+-(alpha|beta|experimental|next|rc).*/u;
|
|
294
|
+
const isExcluded = (version) => {
|
|
295
|
+
const isPreReleaseVersion = PRERELEASE_VERSION_REGEXP.exec(version);
|
|
296
|
+
return version.startsWith("npm:") || isPreReleaseVersion;
|
|
297
|
+
};
|
|
298
|
+
const hasCaret = (version) => {
|
|
299
|
+
return version.startsWith("^");
|
|
338
300
|
};
|
|
339
301
|
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region src/commands/check/checkFormatting.ts
|
|
304
|
+
const checkFormatting = oxfmt({ isFixMode: false });
|
|
305
|
+
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region src/commands/check/check.ts
|
|
340
308
|
const ONLY_VALUES = [
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
309
|
+
"commit",
|
|
310
|
+
"code",
|
|
311
|
+
"dependency",
|
|
312
|
+
"formatting"
|
|
345
313
|
];
|
|
346
|
-
const createCheckCommand = (program)=>{
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
314
|
+
const createCheckCommand = (program) => {
|
|
315
|
+
program.command({
|
|
316
|
+
name: "check",
|
|
317
|
+
description: "Check code health (static analysis)"
|
|
318
|
+
}).option({
|
|
319
|
+
key: "filter",
|
|
320
|
+
name: "filter",
|
|
321
|
+
description: `Filter the compliance check to run (accepted value: ${ONLY_VALUES.join(", ")})`,
|
|
322
|
+
defaultValue: void 0
|
|
323
|
+
}).task({
|
|
324
|
+
handler(_, argv) {
|
|
325
|
+
logCheckableFiles(argv.operands);
|
|
326
|
+
},
|
|
327
|
+
skip: ifFilterDefinedAndNotEqualTo("code")
|
|
328
|
+
}).task({
|
|
329
|
+
label: label$4("Prepare the project"),
|
|
330
|
+
async handler() {
|
|
331
|
+
await turbo("build", {
|
|
332
|
+
excludeExamples: true,
|
|
333
|
+
hasLiveOutput: false
|
|
334
|
+
});
|
|
335
|
+
},
|
|
336
|
+
skip({ filter }) {
|
|
337
|
+
return filter === "commit";
|
|
338
|
+
}
|
|
339
|
+
}).task({
|
|
340
|
+
label: label$4("Check dependency compliance"),
|
|
341
|
+
async handler() {
|
|
342
|
+
await checkDependency();
|
|
343
|
+
},
|
|
344
|
+
skip: ifFilterDefinedAndNotEqualTo("dependency")
|
|
345
|
+
}).task({
|
|
346
|
+
label: label$4("Check formatting compliance"),
|
|
347
|
+
async handler(_, argv) {
|
|
348
|
+
const filenames = argv.operands;
|
|
349
|
+
await checkFormatting(filenames);
|
|
350
|
+
},
|
|
351
|
+
skip: ifFilterDefinedAndNotEqualTo("formatting")
|
|
352
|
+
}).task({
|
|
353
|
+
label: label$4("Check code compliance"),
|
|
354
|
+
async handler(_, argv) {
|
|
355
|
+
const filenames = argv.operands;
|
|
356
|
+
await checkCode(filenames);
|
|
357
|
+
},
|
|
358
|
+
skip: ifFilterDefinedAndNotEqualTo("code")
|
|
359
|
+
}).task({
|
|
360
|
+
label: label$4("Check commit compliance"),
|
|
361
|
+
async handler() {
|
|
362
|
+
await checkCommit();
|
|
363
|
+
},
|
|
364
|
+
skip(context) {
|
|
365
|
+
return context.filter !== "commit";
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
};
|
|
369
|
+
const label$4 = (message) => {
|
|
370
|
+
return `${message} 🧐`;
|
|
371
|
+
};
|
|
372
|
+
const ifFilterDefinedAndNotEqualTo = (filter) => {
|
|
373
|
+
return (context) => {
|
|
374
|
+
return context.filter !== void 0 && context.filter !== filter;
|
|
375
|
+
};
|
|
376
|
+
};
|
|
409
377
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
};
|
|
449
|
-
const label$3 = (message)
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
};
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
};
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
378
|
+
//#endregion
|
|
379
|
+
//#region src/commands/clean.ts
|
|
380
|
+
const createCleanCommand = (program) => {
|
|
381
|
+
program.command({
|
|
382
|
+
name: "clean",
|
|
383
|
+
description: "Clean the project"
|
|
384
|
+
}).task({
|
|
385
|
+
key: "files",
|
|
386
|
+
label: label$3("Retrieve removable files"),
|
|
387
|
+
async handler() {
|
|
388
|
+
const cachePath = "node_modules/.cache";
|
|
389
|
+
const files = await retrieveIgnoredFiles();
|
|
390
|
+
if (isDirectoryExistAndNotEmpty(resolveFromWorkingDirectory(cachePath))) files.push(cachePath);
|
|
391
|
+
return files;
|
|
392
|
+
}
|
|
393
|
+
}).task({
|
|
394
|
+
label({ files }) {
|
|
395
|
+
return files.length > 0 ? label$3("Clean assets") : "Already clean ✨";
|
|
396
|
+
},
|
|
397
|
+
async handler({ files }) {
|
|
398
|
+
if (files.length === 0) return;
|
|
399
|
+
await cleanFiles(files);
|
|
400
|
+
}
|
|
401
|
+
}).task({
|
|
402
|
+
handler({ files }) {
|
|
403
|
+
helpers.message(files.join("\n "), {
|
|
404
|
+
label: "Removed assets",
|
|
405
|
+
lineBreak: {
|
|
406
|
+
end: false,
|
|
407
|
+
start: true
|
|
408
|
+
},
|
|
409
|
+
type: "information"
|
|
410
|
+
});
|
|
411
|
+
},
|
|
412
|
+
skip({ files }) {
|
|
413
|
+
return files.length === 0;
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
};
|
|
417
|
+
const label$3 = (message) => {
|
|
418
|
+
return `${message} 🧹`;
|
|
419
|
+
};
|
|
420
|
+
const cleanFiles = async (files) => {
|
|
421
|
+
await Promise.all(files.map(async (file) => {
|
|
422
|
+
await rm(file, {
|
|
423
|
+
force: true,
|
|
424
|
+
recursive: true
|
|
425
|
+
});
|
|
426
|
+
}));
|
|
427
|
+
};
|
|
428
|
+
const isDirectoryExistAndNotEmpty = (path) => {
|
|
429
|
+
return existsSync(path) && readdirSync(path).length > 0;
|
|
430
|
+
};
|
|
431
|
+
const LINEBREAK_REGEXP = /\n|\r\n/u;
|
|
432
|
+
const retrieveIgnoredFiles = async () => {
|
|
433
|
+
return (await helpers.exec("git clean -fdXn")).split(LINEBREAK_REGEXP).filter((cleanOutput) => {
|
|
434
|
+
return PRESERVE_FILES.every((excludedFile) => {
|
|
435
|
+
return !cleanOutput.includes(excludedFile);
|
|
436
|
+
});
|
|
437
|
+
}).map((cleanOutput) => {
|
|
438
|
+
return cleanOutput.split(" ").at(-1) ?? "";
|
|
439
|
+
}).filter(Boolean);
|
|
440
|
+
};
|
|
441
|
+
const PRESERVE_FILES = ["node_modules"];
|
|
466
442
|
|
|
467
|
-
|
|
443
|
+
//#endregion
|
|
444
|
+
//#region package.json
|
|
445
|
+
var version = "0.0.0-next.d8e717b";
|
|
468
446
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
447
|
+
//#endregion
|
|
448
|
+
//#region src/commands/create.ts
|
|
449
|
+
const REPOSITORY_REGEXP = /^(?:git@.*:|https?:\/\/.*\/)(?<repoOwner>[^/]+)\/(?<repoName>[^/]+)\.git$/u;
|
|
450
|
+
const createCreateCommand = (program) => {
|
|
451
|
+
program.command({
|
|
452
|
+
name: "create",
|
|
453
|
+
description: "Scaffold a new project"
|
|
454
|
+
}).task({ handler() {
|
|
455
|
+
botMessage({
|
|
456
|
+
title: `I'm Stack v${version} 👋`,
|
|
457
|
+
description: "I can guarantee you a project creation in under 1 minute 🚀",
|
|
458
|
+
type: "information"
|
|
459
|
+
});
|
|
460
|
+
} }).task({
|
|
461
|
+
label: label$2("Check pre-requisites"),
|
|
462
|
+
async handler() {
|
|
463
|
+
await getPnpmVersion();
|
|
464
|
+
}
|
|
465
|
+
}).input({
|
|
466
|
+
key: "inputName",
|
|
467
|
+
label: "What's your project name?",
|
|
468
|
+
type: "text"
|
|
469
|
+
}).input({
|
|
470
|
+
key: "inputDescription",
|
|
471
|
+
label: "How would you describe it?",
|
|
472
|
+
type: "text"
|
|
473
|
+
}).input({
|
|
474
|
+
key: "inputUrl",
|
|
475
|
+
label: "Where will it be stored? (Git remote URL)",
|
|
476
|
+
defaultValue: "git@github.com:adbayb/xxx.git",
|
|
477
|
+
type: "text"
|
|
478
|
+
}).input({
|
|
479
|
+
key: "inputTemplate",
|
|
480
|
+
label: "Which template you would like to apply?",
|
|
481
|
+
defaultValue: "single-project",
|
|
482
|
+
options: ["single-project", "multi-projects"],
|
|
483
|
+
type: "select"
|
|
484
|
+
}).task({
|
|
485
|
+
key: "data",
|
|
486
|
+
label: label$2("Check and format input"),
|
|
487
|
+
async handler({ inputDescription, inputName, inputTemplate, inputUrl }) {
|
|
488
|
+
if (!inputName) throw createError("stack create", "The project name is not optional. Make sure to provide a valid value (non-empty string).");
|
|
489
|
+
const { repoName, repoOwner } = REPOSITORY_REGEXP.exec(inputUrl)?.groups ?? {};
|
|
490
|
+
if (!repoOwner || !repoName) throw createError("git", "The owner and repository name can not be extracted. Please make sure to follow either `/^git@.*:(?<repoOwner>.*)/(?<repoName>.*).git$/` or `/^https?://.*/(?<repoOwner>.*)/(?<repoName>.*).git$/` pattern.");
|
|
491
|
+
const nodeVersion = await request.get("https://resolve-node.vercel.app/lts", "text");
|
|
492
|
+
const { version: pnpmVersion } = await request.get("https://registry.npmjs.org/pnpm/latest", "json");
|
|
493
|
+
const projectName = slugify(inputName);
|
|
494
|
+
return {
|
|
495
|
+
licenseYear: (/* @__PURE__ */ new Date()).getFullYear().toString(),
|
|
496
|
+
nodeVersion: `^${Number(nodeVersion.replace("v", "").split(".")[0])}.0.0`,
|
|
497
|
+
pnpmVersion: String(pnpmVersion),
|
|
498
|
+
projectDescription: toCapitalLetter(inputDescription),
|
|
499
|
+
projectName,
|
|
500
|
+
projectUrl: inputUrl,
|
|
501
|
+
repoId: `${repoOwner}/${repoName}`,
|
|
502
|
+
templatePath: resolveFromPackageDirectory("templates", inputTemplate),
|
|
503
|
+
workingPath: resolveFromWorkingDirectory(projectName)
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
}).input({
|
|
507
|
+
key: "canRemoveExistingDirectoryInput",
|
|
508
|
+
label({ data: { projectName } }) {
|
|
509
|
+
return label$2(`\`${projectName}\` directory already exists, do you want to remove it?`);
|
|
510
|
+
},
|
|
511
|
+
defaultValue: true,
|
|
512
|
+
skip({ data: { workingPath } }) {
|
|
513
|
+
return !existsSync(workingPath);
|
|
514
|
+
},
|
|
515
|
+
type: "confirm",
|
|
516
|
+
validate({ canRemoveExistingDirectoryInput, data: { projectName } }) {
|
|
517
|
+
if (canRemoveExistingDirectoryInput) return;
|
|
518
|
+
return createError("mkdir", `Remove or rename the \`${projectName}\` existing directory to apply the template from a clean state.`);
|
|
519
|
+
}
|
|
520
|
+
}).task({
|
|
521
|
+
key: "templateEngine",
|
|
522
|
+
label({ data: { projectName }, inputTemplate }) {
|
|
523
|
+
return label$2(`Copy \`${inputTemplate}\` template to \`${projectName}\` directory`);
|
|
524
|
+
},
|
|
525
|
+
async handler({ canRemoveExistingDirectoryInput, data: { licenseYear, nodeVersion, pnpmVersion, projectDescription, projectName, projectUrl, repoId, templatePath, workingPath }, inputTemplate }) {
|
|
526
|
+
if (canRemoveExistingDirectoryInput) await rm(workingPath, {
|
|
527
|
+
force: true,
|
|
528
|
+
recursive: true
|
|
529
|
+
});
|
|
530
|
+
return createTemplateEngine(workingPath, {
|
|
531
|
+
projectName,
|
|
532
|
+
templateModel: {
|
|
533
|
+
licenseYear,
|
|
534
|
+
nodeVersion,
|
|
535
|
+
pnpmVersion,
|
|
536
|
+
projectDescription,
|
|
537
|
+
projectName,
|
|
538
|
+
projectUrl,
|
|
539
|
+
repoId
|
|
540
|
+
},
|
|
541
|
+
templateName: inputTemplate,
|
|
542
|
+
templatePath
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}).task({
|
|
546
|
+
label() {
|
|
547
|
+
return label$2("Process template");
|
|
548
|
+
},
|
|
549
|
+
async handler({ templateEngine }) {
|
|
550
|
+
await templateEngine.processContents();
|
|
551
|
+
await templateEngine.processPaths();
|
|
552
|
+
}
|
|
553
|
+
}).task({
|
|
554
|
+
label: label$2("Initialize `git`"),
|
|
555
|
+
async handler({ data: { projectUrl } }) {
|
|
556
|
+
await helpers.exec("git init");
|
|
557
|
+
await helpers.exec(`git remote add origin ${projectUrl}`);
|
|
558
|
+
}
|
|
559
|
+
}).task({
|
|
560
|
+
label: label$2("Set up the package manager"),
|
|
561
|
+
async handler() {
|
|
562
|
+
await setPackageManager();
|
|
563
|
+
}
|
|
564
|
+
}).task({
|
|
565
|
+
label: label$2("Install dependencies"),
|
|
566
|
+
async handler({ data: { projectName } }) {
|
|
567
|
+
const localDevelopmentDependencies = ["quickbundle", "vitest"];
|
|
568
|
+
const globalDevelopmentDependencies = ["@adbayb/stack"];
|
|
569
|
+
try {
|
|
570
|
+
await helpers.exec(`pnpm add ${globalDevelopmentDependencies.join(" ")} --save-dev --ignore-workspace-root-check`);
|
|
571
|
+
await helpers.exec(`pnpm add ${localDevelopmentDependencies.join(" ")} --save-dev --filter ${projectName}`);
|
|
572
|
+
await helpers.exec("pnpm install");
|
|
573
|
+
} catch (error) {
|
|
574
|
+
throw createError("pnpm", error instanceof Error ? error : new Error(String(error)));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}).task({
|
|
578
|
+
label: label$2("Run `stack install`"),
|
|
579
|
+
async handler() {
|
|
580
|
+
await helpers.exec("stack install");
|
|
581
|
+
}
|
|
582
|
+
}).task({
|
|
583
|
+
label: label$2("Commit"),
|
|
584
|
+
async handler() {
|
|
585
|
+
await helpers.exec("git add -A");
|
|
586
|
+
await helpers.exec("git commit -m \"chore: initial commit\"");
|
|
587
|
+
}
|
|
588
|
+
}).task({ handler({ data: { projectName } }) {
|
|
589
|
+
botMessage({
|
|
590
|
+
title: "The project was successfully created",
|
|
591
|
+
description: `Run \`cd ./${projectName}\` and Enjoy 🚀`,
|
|
592
|
+
type: "success"
|
|
593
|
+
});
|
|
594
|
+
} });
|
|
595
|
+
};
|
|
596
|
+
const label$2 = (message) => {
|
|
597
|
+
return `${message} 🔨`;
|
|
598
|
+
};
|
|
599
|
+
const slugify = (input) => {
|
|
600
|
+
return input.toLowerCase().replaceAll(/[^a-z0-9\s-]/gu, "").trim().replaceAll(/\s+/gu, "-").replaceAll(/-+/gu, "-");
|
|
601
|
+
};
|
|
602
|
+
const toCapitalLetter = (input) => {
|
|
603
|
+
return input.charAt(0).toUpperCase() + input.slice(1);
|
|
604
|
+
};
|
|
605
|
+
const createTemplateEngine = async (workingPath, { projectName, templateModel, templateName, templatePath }) => {
|
|
606
|
+
if (!existsSync(workingPath)) await mkdir(workingPath);
|
|
607
|
+
await cp(templatePath, workingPath, {
|
|
608
|
+
force: true,
|
|
609
|
+
recursive: true
|
|
610
|
+
});
|
|
611
|
+
const gitignoreFile = join(workingPath, ".gitignore.tmpl");
|
|
612
|
+
if (existsSync(gitignoreFile)) await rename(gitignoreFile, join(workingPath, ".gitignore"));
|
|
613
|
+
const templateEntries = await getTemplateEntries(workingPath);
|
|
614
|
+
process.chdir(workingPath);
|
|
615
|
+
return {
|
|
616
|
+
async processContents() {
|
|
617
|
+
await Promise.all(templateEntries.filter(({ type }) => {
|
|
618
|
+
return type === "content";
|
|
619
|
+
}).map(async (entry) => {
|
|
620
|
+
await writeFile(entry.path, setTemplateVariables(entry, templateModel));
|
|
621
|
+
}));
|
|
622
|
+
},
|
|
623
|
+
async processPaths() {
|
|
624
|
+
const sortedDirectoryEntries = templateEntries.filter(({ content, type }) => {
|
|
625
|
+
if (type === "path.directory" || type === "path.file") return hasTemplateVariable(basename(content));
|
|
626
|
+
return false;
|
|
627
|
+
}).toSorted(({ path: pathA }, { path: pathB }) => {
|
|
628
|
+
return pathB.length - pathA.length;
|
|
629
|
+
});
|
|
630
|
+
for (const entry of sortedDirectoryEntries) {
|
|
631
|
+
const newPath = setTemplateVariables(entry, templateModel);
|
|
632
|
+
await rename(entry.path, newPath);
|
|
633
|
+
}
|
|
634
|
+
await symlink(join(templateName === "single-project" ? projectName : join("libraries", projectName), "README.md"), "./README.md");
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
};
|
|
638
|
+
const getTemplateEntries = async (path) => {
|
|
639
|
+
const entries = await readdir(path, {
|
|
640
|
+
recursive: true,
|
|
641
|
+
withFileTypes: true
|
|
642
|
+
});
|
|
643
|
+
return (await Promise.all(entries.map(async (entry) => {
|
|
644
|
+
const isDirectory = entry.isDirectory();
|
|
645
|
+
if (!isDirectory && !entry.isFile()) return [];
|
|
646
|
+
const entryPath = resolve(entry.parentPath, entry.name);
|
|
647
|
+
return (isDirectory ? [{
|
|
648
|
+
content: entryPath,
|
|
649
|
+
type: "path.directory"
|
|
650
|
+
}] : [{
|
|
651
|
+
content: entryPath,
|
|
652
|
+
type: "path.file"
|
|
653
|
+
}, {
|
|
654
|
+
content: await readFile(entryPath, "utf8"),
|
|
655
|
+
type: "content"
|
|
656
|
+
}]).map(({ content, type }) => {
|
|
657
|
+
if (!hasTemplateVariable(content)) return;
|
|
658
|
+
return {
|
|
659
|
+
content,
|
|
660
|
+
path: entryPath,
|
|
661
|
+
type
|
|
662
|
+
};
|
|
663
|
+
}).filter((input) => {
|
|
664
|
+
return Boolean(input);
|
|
665
|
+
});
|
|
666
|
+
}))).flat();
|
|
667
|
+
};
|
|
668
|
+
const setTemplateVariables = (entry, model) => {
|
|
669
|
+
return entry.content.replaceAll(TEMPLATE_VARIABLE_MATCHER, (match, dataModelKey) => {
|
|
670
|
+
return model[dataModelKey] ?? match;
|
|
671
|
+
});
|
|
672
|
+
};
|
|
673
|
+
const TEMPLATE_VARIABLE_MATCHER = /\{\{(.*?)\}\}/giu;
|
|
674
|
+
const hasTemplateVariable = (input) => {
|
|
675
|
+
/**
|
|
676
|
+
* TemplateVariableMatcher.test() is not used since the `RegExp` is stateful when the global is
|
|
677
|
+
* used leading to some unstable results (relying on latest `lastIndex` set (lastIndex specifies
|
|
678
|
+
* the index at which to start the next match)). String.search is stateless.
|
|
679
|
+
*
|
|
680
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test MDN documentation}.
|
|
681
|
+
*/
|
|
682
|
+
return input.search(TEMPLATE_VARIABLE_MATCHER) >= 0;
|
|
637
683
|
};
|
|
638
684
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
const fixFormatting = async (files)=>{
|
|
643
|
-
let prettierFiles = [];
|
|
644
|
-
if (files.length === 0) {
|
|
645
|
-
prettierFiles.push(`"**/!(${PRETTIER_IGNORE_FILES.join("|")})"`);
|
|
646
|
-
} else {
|
|
647
|
-
prettierFiles = files.filter((file)=>{
|
|
648
|
-
return !PRETTIER_IGNORE_FILES.some((filename)=>file.endsWith(filename));
|
|
649
|
-
});
|
|
650
|
-
if (prettierFiles.length === 0) return;
|
|
651
|
-
}
|
|
652
|
-
const arguments_ = [
|
|
653
|
-
...prettierFiles
|
|
654
|
-
];
|
|
655
|
-
if (existsSync(resolveFromProjectDirectory(".gitignore"))) {
|
|
656
|
-
arguments_.push("--ignore-path .gitignore");
|
|
657
|
-
}
|
|
658
|
-
arguments_.push("--write", "--ignore-unknown", "--no-error-on-unmatched-pattern");
|
|
659
|
-
try {
|
|
660
|
-
return await helpers.exec(`prettier ${arguments_.join(" ")}`);
|
|
661
|
-
} catch (error) {
|
|
662
|
-
throw createError("prettier", error);
|
|
663
|
-
}
|
|
664
|
-
};
|
|
685
|
+
//#endregion
|
|
686
|
+
//#region src/commands/fix/fixCode.ts
|
|
687
|
+
const fixCode = oxlint({ isFixMode: true });
|
|
665
688
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
});
|
|
689
|
+
//#endregion
|
|
690
|
+
//#region src/commands/fix/fixFormatting.ts
|
|
691
|
+
const fixFormatting = oxfmt({ isFixMode: true });
|
|
669
692
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
};
|
|
698
|
-
const label$1 = (message)
|
|
693
|
+
//#endregion
|
|
694
|
+
//#region src/commands/fix/fix.ts
|
|
695
|
+
const createFixCommand = (program) => {
|
|
696
|
+
program.command({
|
|
697
|
+
name: "fix",
|
|
698
|
+
description: "Fix auto-fixable issues"
|
|
699
|
+
}).task({ handler(_, argv) {
|
|
700
|
+
logCheckableFiles(argv.operands);
|
|
701
|
+
} }).task({
|
|
702
|
+
label: label$1("Prepare the project"),
|
|
703
|
+
async handler() {
|
|
704
|
+
await turbo("build", {
|
|
705
|
+
excludeExamples: true,
|
|
706
|
+
hasLiveOutput: false
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
}).task({
|
|
710
|
+
label: label$1("Fix formatting issues"),
|
|
711
|
+
async handler(_, argv) {
|
|
712
|
+
await fixFormatting(argv.operands);
|
|
713
|
+
}
|
|
714
|
+
}).task({
|
|
715
|
+
label: label$1("Fix code issues"),
|
|
716
|
+
async handler(_, argv) {
|
|
717
|
+
await fixCode(argv.operands);
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
};
|
|
721
|
+
const label$1 = (message) => {
|
|
722
|
+
return `${message} 🚑`;
|
|
723
|
+
};
|
|
699
724
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
};
|
|
723
|
-
const
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
return chmod(filename, "0755");
|
|
725
|
+
//#endregion
|
|
726
|
+
//#region src/commands/install.ts
|
|
727
|
+
const createInstallCommand = (program) => {
|
|
728
|
+
program.command({
|
|
729
|
+
name: "install",
|
|
730
|
+
description: "Install required setup"
|
|
731
|
+
}).task({
|
|
732
|
+
label: label("Install `git.pre-commit` hook"),
|
|
733
|
+
async handler() {
|
|
734
|
+
const lineBreakMatcher = String.raw`\n|\r\n`;
|
|
735
|
+
const stackCommand = getStackCommand(`fix $(node -e 'console.log(require("child_process").execSync("git status --porcelain", {encoding: "utf8"}).split(/${lineBreakMatcher}/).filter(Boolean).map(item => item.split(" ").at(-1)).join(" "))')`);
|
|
736
|
+
await installGitHook("pre-commit", `${stackCommand} && git add -A`);
|
|
737
|
+
}
|
|
738
|
+
}).task({
|
|
739
|
+
label: label("Install `git.commit-msg` hook"),
|
|
740
|
+
async handler() {
|
|
741
|
+
await installGitHook("commit-msg", getStackCommand("check --filter commit"));
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
};
|
|
745
|
+
const label = (message) => {
|
|
746
|
+
return `${message} 📲`;
|
|
747
|
+
};
|
|
748
|
+
const installGitHook = async (hook, content) => {
|
|
749
|
+
const filename = resolveFromWorkingDirectory(`.git/hooks/${hook}`);
|
|
750
|
+
await writeFile(filename, content);
|
|
751
|
+
return chmod(filename, "0755");
|
|
728
752
|
};
|
|
729
753
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
754
|
+
//#endregion
|
|
755
|
+
//#region src/commands/release.ts
|
|
756
|
+
const createReleaseCommand = (program) => {
|
|
757
|
+
program.command({
|
|
758
|
+
name: "release",
|
|
759
|
+
description: "Log, version, and publish package(s)"
|
|
760
|
+
}).option({
|
|
761
|
+
key: "log",
|
|
762
|
+
name: "log",
|
|
763
|
+
description: "Add a new changelog entry"
|
|
764
|
+
}).option({
|
|
765
|
+
key: "emptyLog",
|
|
766
|
+
name: "empty-log",
|
|
767
|
+
description: "Add an empty changelog entry"
|
|
768
|
+
}).option({
|
|
769
|
+
key: "tag",
|
|
770
|
+
name: "tag",
|
|
771
|
+
description: "Bump the package(s) version"
|
|
772
|
+
}).option({
|
|
773
|
+
key: "publish",
|
|
774
|
+
name: "publish",
|
|
775
|
+
description: "Publish package(s) to the registry"
|
|
776
|
+
}).task({
|
|
777
|
+
async handler() {
|
|
778
|
+
helpers.message("New changelog entry\n");
|
|
779
|
+
await changeset("changeset");
|
|
780
|
+
},
|
|
781
|
+
skip: ifNotEqualTo("log")
|
|
782
|
+
}).task({
|
|
783
|
+
async handler() {
|
|
784
|
+
helpers.message("New empty changelog entry\n");
|
|
785
|
+
await changeset("changeset --empty");
|
|
786
|
+
},
|
|
787
|
+
skip: ifNotEqualTo("emptyLog")
|
|
788
|
+
}).task({
|
|
789
|
+
async handler() {
|
|
790
|
+
helpers.message("Bumping the package(s) version\n");
|
|
791
|
+
await changeset("changeset version");
|
|
792
|
+
},
|
|
793
|
+
skip: ifNotEqualTo("tag")
|
|
794
|
+
}).task({
|
|
795
|
+
async handler() {
|
|
796
|
+
helpers.message("Publishing package(s) to the registry\n");
|
|
797
|
+
await changeset("stack build && changeset publish");
|
|
798
|
+
},
|
|
799
|
+
skip: ifNotEqualTo("publish")
|
|
800
|
+
});
|
|
801
|
+
};
|
|
802
|
+
const ifNotEqualTo = (validOption) => {
|
|
803
|
+
return (context) => {
|
|
804
|
+
return !context[validOption];
|
|
805
|
+
};
|
|
806
|
+
};
|
|
769
807
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
808
|
+
//#endregion
|
|
809
|
+
//#region src/commands/start.ts
|
|
810
|
+
const createStartCommand = (program) => {
|
|
811
|
+
program.command({
|
|
812
|
+
name: "start",
|
|
813
|
+
description: "Start the project in production mode"
|
|
814
|
+
}).task({ async handler() {
|
|
815
|
+
await turbo("start");
|
|
816
|
+
} });
|
|
779
817
|
};
|
|
780
818
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
819
|
+
//#endregion
|
|
820
|
+
//#region src/commands/test.ts
|
|
821
|
+
const createTestCommand = (program) => {
|
|
822
|
+
program.command({
|
|
823
|
+
name: "test",
|
|
824
|
+
description: "Test the code execution"
|
|
825
|
+
}).task({ async handler() {
|
|
826
|
+
await turbo("test");
|
|
827
|
+
} });
|
|
790
828
|
};
|
|
791
829
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
830
|
+
//#endregion
|
|
831
|
+
//#region src/commands/watch.ts
|
|
832
|
+
const createWatchCommand = (program) => {
|
|
833
|
+
program.command({
|
|
834
|
+
name: "watch",
|
|
835
|
+
description: "Build and start the project in development mode"
|
|
836
|
+
}).task({ async handler() {
|
|
837
|
+
await turbo("watch");
|
|
838
|
+
} });
|
|
801
839
|
};
|
|
802
840
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
841
|
+
//#endregion
|
|
842
|
+
//#region src/index.ts
|
|
843
|
+
const createProgram = (...commandFactories) => {
|
|
844
|
+
const program = termost({
|
|
845
|
+
name: "stack",
|
|
846
|
+
description: "Toolbox to easily scaffold and maintain a project",
|
|
847
|
+
onException() {
|
|
848
|
+
botMessage({
|
|
849
|
+
title: "Oops, an error occurred",
|
|
850
|
+
description: "Keep calm and carry on with some coffee ☕️",
|
|
851
|
+
type: "error"
|
|
852
|
+
});
|
|
853
|
+
},
|
|
854
|
+
version
|
|
855
|
+
});
|
|
856
|
+
for (const commandBuilder of commandFactories) commandBuilder(program);
|
|
819
857
|
};
|
|
820
858
|
createProgram(createCreateCommand, createInstallCommand, createCleanCommand, createCheckCommand, createFixCommand, createStartCommand, createBuildCommand, createWatchCommand, createTestCommand, createReleaseCommand);
|
|
859
|
+
|
|
860
|
+
//#endregion
|