@adbayb/stack 2.36.0-next-d491b30 → 2.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +756 -725
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,778 +1,809 @@
|
|
|
1
|
-
import { helpers, termost } from
|
|
2
|
-
import { createRequire } from
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { fdir } from
|
|
1
|
+
import { helpers, termost } from 'termost';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { resolve, join } from 'node:path';
|
|
4
|
+
import { existsSync, readdirSync, cpSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { rm, mkdir, symlink, writeFile, chmod } from 'node:fs/promises';
|
|
6
|
+
import { fdir } from 'fdir';
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
const require = createRequire(import.meta.url);
|
|
8
|
+
const require$1 = createRequire(import.meta.url);
|
|
10
9
|
function assert(expectedCondition, createError) {
|
|
11
|
-
|
|
10
|
+
if (!expectedCondition) {
|
|
11
|
+
throw createError();
|
|
12
|
+
}
|
|
12
13
|
}
|
|
13
14
|
/**
|
|
14
|
-
* Helper to format log messages with a welcoming bot.
|
|
15
|
-
* @param input - Message factory.
|
|
16
|
-
* @param input.title - Title input.
|
|
17
|
-
* @param input.description - Description input.
|
|
18
|
-
* @param input.body - Body input.
|
|
19
|
-
* @param input.type - Message type.
|
|
20
|
-
* @example
|
|
21
|
-
* botMessage(
|
|
22
|
-
* {
|
|
23
|
-
* title: "Oops, an error occurred",
|
|
24
|
-
* description:
|
|
25
|
-
* "Keep calm and carry on with some coffee ☕️",
|
|
26
|
-
* body: String(previousTaskError),
|
|
27
|
-
* type: "error",
|
|
28
|
-
* },
|
|
29
|
-
* );
|
|
30
|
-
*/
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
15
|
+
* Helper to format log messages with a welcoming bot.
|
|
16
|
+
* @param input - Message factory.
|
|
17
|
+
* @param input.title - Title input.
|
|
18
|
+
* @param input.description - Description input.
|
|
19
|
+
* @param input.body - Body input.
|
|
20
|
+
* @param input.type - Message type.
|
|
21
|
+
* @example
|
|
22
|
+
* botMessage(
|
|
23
|
+
* {
|
|
24
|
+
* title: "Oops, an error occurred",
|
|
25
|
+
* description:
|
|
26
|
+
* "Keep calm and carry on with some coffee ☕️",
|
|
27
|
+
* body: String(previousTaskError),
|
|
28
|
+
* type: "error",
|
|
29
|
+
* },
|
|
30
|
+
* );
|
|
31
|
+
*/ const botMessage = (input)=>{
|
|
32
|
+
const { type } = input;
|
|
33
|
+
const log = type === "error" ? console.error : console.log;
|
|
34
|
+
const colorByType = {
|
|
35
|
+
error: "red",
|
|
36
|
+
information: "blue",
|
|
37
|
+
success: "green"
|
|
38
|
+
};
|
|
39
|
+
log(helpers.format(`
|
|
34
40
|
╭─────╮
|
|
35
41
|
│ ◠ ◠ ${input.title}
|
|
36
42
|
│ ${type === "error" ? "◠" : "◡"} │ ${input.description}
|
|
37
43
|
╰─────╯
|
|
38
44
|
${input.body ? `
|
|
39
45
|
${input.body}
|
|
40
|
-
` : ""}`, {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
success: "green"
|
|
44
|
-
}[type] }));
|
|
46
|
+
` : ""}`, {
|
|
47
|
+
color: colorByType[type]
|
|
48
|
+
}));
|
|
45
49
|
};
|
|
46
50
|
/**
|
|
47
|
-
* Resolve a relative path to an absolute one resolved from the generated project root directory.
|
|
48
|
-
* @param path - The relative path.
|
|
49
|
-
* @returns The resolved absolute path.
|
|
50
|
-
* @example
|
|
51
|
-
* resolveFromProjectDirectory(".gitignore");
|
|
52
|
-
*/
|
|
53
|
-
|
|
54
|
-
return resolve(process.cwd(), path);
|
|
51
|
+
* Resolve a relative path to an absolute one resolved from the generated project root directory.
|
|
52
|
+
* @param path - The relative path.
|
|
53
|
+
* @returns The resolved absolute path.
|
|
54
|
+
* @example
|
|
55
|
+
* resolveFromProjectDirectory(".gitignore");
|
|
56
|
+
*/ const resolveFromProjectDirectory = (path)=>{
|
|
57
|
+
return resolve(process.cwd(), path);
|
|
55
58
|
};
|
|
56
59
|
/**
|
|
57
|
-
* Resolve a relative path to an absolute one resolved from the `stack` node module directory.
|
|
58
|
-
* @param path - The relative path.
|
|
59
|
-
* @returns The resolved absolute path.
|
|
60
|
-
* @example
|
|
61
|
-
* resolveFromStackDirectory("./templates");
|
|
62
|
-
*/
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
60
|
+
* Resolve a relative path to an absolute one resolved from the `stack` node module directory.
|
|
61
|
+
* @param path - The relative path.
|
|
62
|
+
* @returns The resolved absolute path.
|
|
63
|
+
* @example
|
|
64
|
+
* resolveFromStackDirectory("./templates");
|
|
65
|
+
*/ const resolveFromStackDirectory = (path)=>{
|
|
66
|
+
return resolve(import.meta.dirname, "../", path);
|
|
67
|
+
};
|
|
68
|
+
const createError = (bin, error)=>{
|
|
69
|
+
const errorMessage = `\`${bin}\` command failed.\n${String(error)}`;
|
|
70
|
+
if (error instanceof Error) {
|
|
71
|
+
error.message = errorMessage;
|
|
72
|
+
return error;
|
|
73
|
+
}
|
|
74
|
+
return new Error(errorMessage);
|
|
75
|
+
};
|
|
76
|
+
const getNpmVersion = async ()=>{
|
|
77
|
+
try {
|
|
78
|
+
return await helpers.exec("pnpm -v");
|
|
79
|
+
} catch {
|
|
80
|
+
throw createError("pnpm", "The project must use `pnpm` as a node package manager tool. Follow this installation guide https://pnpm.io/installation");
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const getStackCommand = (command)=>{
|
|
84
|
+
return `pnpm stack ${command}`;
|
|
85
|
+
};
|
|
86
|
+
const hasDependency = (packageName)=>{
|
|
87
|
+
return Boolean(require$1.resolve(packageName));
|
|
88
|
+
};
|
|
89
|
+
const setPackageManager = async ()=>{
|
|
90
|
+
/**
|
|
91
|
+
* Corepack is downloaded remotely to get always up-to-date npm registry fingerprints since they're hardcoded.
|
|
92
|
+
* @see {@link https://github.com/nodejs/corepack/issues/613}
|
|
93
|
+
*/ return helpers.exec("npx corepack enable");
|
|
94
|
+
};
|
|
95
|
+
const request = {
|
|
96
|
+
async get (url, responseType) {
|
|
97
|
+
const response = await fetch(url);
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
throw createError("fetch", `Failed to fetch resources from ${url} (${JSON.stringify({
|
|
100
|
+
status: response.status,
|
|
101
|
+
statusText: response.statusText
|
|
102
|
+
})})`);
|
|
103
|
+
}
|
|
104
|
+
return responseType === "text" ? response.text() : response.json();
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const eslint = (options)=>async (files = [])=>{
|
|
108
|
+
let eslintFiles = [];
|
|
109
|
+
if (files.length === 0) {
|
|
110
|
+
eslintFiles.push(".");
|
|
111
|
+
} else {
|
|
112
|
+
eslintFiles = files.filter((file)=>{
|
|
113
|
+
return ESLINT_EXTENSIONS.some((extension)=>file.endsWith(extension));
|
|
114
|
+
});
|
|
115
|
+
if (eslintFiles.length === 0) return;
|
|
116
|
+
}
|
|
117
|
+
const arguments_ = [
|
|
118
|
+
...eslintFiles,
|
|
119
|
+
"--cache",
|
|
120
|
+
`--cache-location ${resolveFromProjectDirectory("node_modules/.cache/.eslintcache")}`,
|
|
121
|
+
"--no-error-on-unmatched-pattern"
|
|
122
|
+
];
|
|
123
|
+
if (options.isFixMode) {
|
|
124
|
+
arguments_.push("--fix");
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
return await helpers.exec(`eslint ${arguments_.join(" ")}`);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
throw createError("eslint", error);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const turbo = async (command, options = {})=>{
|
|
133
|
+
try {
|
|
134
|
+
const { excludeExamples = false, hasLiveOutput = true, ...restOptions } = options;
|
|
135
|
+
return await helpers.exec(`turbo run ${command} ${excludeExamples ? "--filter !@examples/*" : ""}`, {
|
|
136
|
+
...restOptions,
|
|
137
|
+
hasLiveOutput
|
|
138
|
+
});
|
|
139
|
+
} catch (error) {
|
|
140
|
+
throw createError("turbo", error);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const logCheckableFiles = (files)=>{
|
|
144
|
+
if (files.length === 0) {
|
|
145
|
+
helpers.message("The whole project will be checked.", {
|
|
146
|
+
label: false,
|
|
147
|
+
lineBreak: {
|
|
148
|
+
end: true,
|
|
149
|
+
start: false
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
helpers.message(files.join("\n "), {
|
|
155
|
+
label: "Following files will be checked:",
|
|
156
|
+
lineBreak: {
|
|
157
|
+
end: true,
|
|
158
|
+
start: false
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
const changeset = async (command)=>{
|
|
163
|
+
try {
|
|
164
|
+
return await helpers.exec(command, {
|
|
165
|
+
hasLiveOutput: true
|
|
166
|
+
});
|
|
167
|
+
} catch (error) {
|
|
168
|
+
throw createError("changeset", error);
|
|
169
|
+
}
|
|
160
170
|
};
|
|
161
171
|
const ESLINT_EXTENSIONS = [
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
+
"js",
|
|
173
|
+
"jsx",
|
|
174
|
+
"cjs",
|
|
175
|
+
"mjs",
|
|
176
|
+
"ts",
|
|
177
|
+
"tsx",
|
|
178
|
+
"cts",
|
|
179
|
+
"mts",
|
|
180
|
+
"md",
|
|
181
|
+
"mdx"
|
|
172
182
|
];
|
|
173
183
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
184
|
+
const createBuildCommand = (program)=>{
|
|
185
|
+
program.command({
|
|
186
|
+
description: "Build the project in production mode",
|
|
187
|
+
name: "build"
|
|
188
|
+
}).task({
|
|
189
|
+
async handler () {
|
|
190
|
+
await turbo("build");
|
|
191
|
+
}
|
|
192
|
+
});
|
|
183
193
|
};
|
|
184
194
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
195
|
+
const checkCode = eslint({
|
|
196
|
+
isFixMode: false
|
|
197
|
+
});
|
|
188
198
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
throw createError("commitlint", error);
|
|
196
|
-
}
|
|
199
|
+
const checkCommit = async ()=>{
|
|
200
|
+
try {
|
|
201
|
+
return await helpers.exec('commitlint --extends "@commitlint/config-conventional" --edit');
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw createError("commitlint", error);
|
|
204
|
+
}
|
|
197
205
|
};
|
|
198
206
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
|
|
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
|
-
|
|
207
|
+
const checkDependency = async ()=>{
|
|
208
|
+
const stdout = await helpers.exec("pnpm recursive ls --json");
|
|
209
|
+
const checkDependencyVersionMismatch = createPackagesVersionMismatchChecker();
|
|
210
|
+
const packages = JSON.parse(stdout).map((package_)=>{
|
|
211
|
+
const packagePath = join(package_.path, "package.json");
|
|
212
|
+
assert(package_.name, ()=>createPackageError(`\`${packagePath}\` must have a name field.`));
|
|
213
|
+
const packageContent = require$1(packagePath);
|
|
214
|
+
const peerDependencies = packageContent.peerDependencies ?? {};
|
|
215
|
+
const devDependencies = packageContent.devDependencies ?? {};
|
|
216
|
+
const dependencies = packageContent.dependencies ?? {};
|
|
217
|
+
return {
|
|
218
|
+
dependencies,
|
|
219
|
+
devDependencies,
|
|
220
|
+
name: package_.name,
|
|
221
|
+
peerDependencies
|
|
222
|
+
};
|
|
223
|
+
});
|
|
224
|
+
for (const package_ of packages){
|
|
225
|
+
// Check version mismatches to guarantee a single copy for a given package in the monorepo (use case: prevent singleton-like issues with React contexts)
|
|
226
|
+
checkDependencyVersionMismatch(package_);
|
|
227
|
+
// Check version range accordingly to our dependency guidelines (ie. dev dependencies must be pinned and dependencies must have caret)
|
|
228
|
+
checkDependencyVersionRange(package_);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const checkDependencyVersionRange = ({ dependencies, devDependencies, name, peerDependencies })=>{
|
|
232
|
+
for (const dependencyName of Object.keys(devDependencies)){
|
|
233
|
+
const version = devDependencies[dependencyName];
|
|
234
|
+
assertVersion(version, {
|
|
235
|
+
consumedBy: name,
|
|
236
|
+
name: dependencyName
|
|
237
|
+
});
|
|
238
|
+
if (version !== "workspace:*" && !isExcluded(version) && !/^\d/.test(version)) throw createPackageError(`As a dev dependency, \`${dependencyName}\` version must be fixed (or set as "workspace:*" for local packages) to reduce accidental breaking change risks due to an implicit semver upgrade.`, {
|
|
239
|
+
consumedBy: name,
|
|
240
|
+
name: dependencyName
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
for (const dependencyName of Object.keys(dependencies)){
|
|
244
|
+
const version = dependencies[dependencyName];
|
|
245
|
+
assertVersion(version, {
|
|
246
|
+
consumedBy: name,
|
|
247
|
+
name: dependencyName
|
|
248
|
+
});
|
|
249
|
+
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.`, {
|
|
250
|
+
consumedBy: name,
|
|
251
|
+
name: dependencyName
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
for (const dependencyName of Object.keys(peerDependencies)){
|
|
255
|
+
const version = peerDependencies[dependencyName];
|
|
256
|
+
assertVersion(version, {
|
|
257
|
+
consumedBy: name,
|
|
258
|
+
name: dependencyName
|
|
259
|
+
});
|
|
260
|
+
if (!hasCaret(version) && !isExcluded(version)) /*
|
|
261
|
+
* Why disallowing workspace protocol as a version resolver?
|
|
262
|
+
* To reduce the update frequency needs consumer-side and guarantee on our side the minimum compatible version,
|
|
263
|
+
* the best practice should be to keeping an explicit number version which represents the lowest compatible version from an API perspective.
|
|
264
|
+
*/ 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.`, {
|
|
265
|
+
consumedBy: name,
|
|
266
|
+
name: dependencyName
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
const createPackagesVersionMismatchChecker = ()=>{
|
|
271
|
+
const monorepoDependencies = new Map();
|
|
272
|
+
const monorepoDevelopmentDependencies = new Map();
|
|
273
|
+
const lint = (package_, type)=>{
|
|
274
|
+
const packageName = package_.name;
|
|
275
|
+
const isDevelopment = type === "development";
|
|
276
|
+
const store = isDevelopment ? monorepoDevelopmentDependencies : monorepoDependencies;
|
|
277
|
+
const dependencies = isDevelopment ? package_.devDependencies : package_.dependencies;
|
|
278
|
+
for (const dependencyName of Object.keys(dependencies)){
|
|
279
|
+
const depVersion = dependencies[dependencyName];
|
|
280
|
+
if (!depVersion) continue;
|
|
281
|
+
const storedVersion = store.get(dependencyName);
|
|
282
|
+
if (!storedVersion) {
|
|
283
|
+
store.set(dependencyName, depVersion);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const isSameMonorepoVersion = depVersion === storedVersion;
|
|
287
|
+
if (!isSameMonorepoVersion) {
|
|
288
|
+
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}\`).`, {
|
|
289
|
+
consumedBy: packageName,
|
|
290
|
+
name: dependencyName
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
return (package_)=>{
|
|
296
|
+
lint(package_, "development");
|
|
297
|
+
lint(package_, "production");
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
const createPackageError = (message, context)=>{
|
|
301
|
+
return createError("stack check", context ? `\`${context.name}\` consumed by \`${context.consumedBy}\` doesn't conform to package guidelines.\n${message}` : message);
|
|
286
302
|
};
|
|
287
303
|
function assertVersion(version, { consumedBy, name }) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
304
|
+
assert(version, ()=>createPackageError(`\`${name}\` must have a valid version specified (current version equals to \`${String(version)}\`).`, {
|
|
305
|
+
consumedBy,
|
|
306
|
+
name
|
|
307
|
+
}));
|
|
292
308
|
}
|
|
293
|
-
const isExcluded = (version)
|
|
294
|
-
|
|
295
|
-
|
|
309
|
+
const isExcluded = (version)=>{
|
|
310
|
+
const isPreReleaseVersion = /\d+\.\d+\.\d+-(alpha|beta|experimental|next|rc).*/.exec(version);
|
|
311
|
+
const isNpmProtocol = version.startsWith("npm:");
|
|
312
|
+
return isNpmProtocol || isPreReleaseVersion;
|
|
296
313
|
};
|
|
297
|
-
const hasCaret = (version)
|
|
314
|
+
const hasCaret = (version)=>version.startsWith("^");
|
|
298
315
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
throw createError("tsc", error);
|
|
306
|
-
}
|
|
316
|
+
const checkType = async ()=>{
|
|
317
|
+
try {
|
|
318
|
+
return await helpers.exec("pnpm --parallel exec tsc --noEmit");
|
|
319
|
+
} catch (error) {
|
|
320
|
+
throw createError("tsc", error);
|
|
321
|
+
}
|
|
307
322
|
};
|
|
308
323
|
|
|
309
|
-
//#endregion
|
|
310
|
-
//#region src/commands/check/check.ts
|
|
311
324
|
const ONLY_VALUES = [
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
325
|
+
"commit",
|
|
326
|
+
"code",
|
|
327
|
+
"dependency",
|
|
328
|
+
"type"
|
|
316
329
|
];
|
|
317
|
-
const createCheckCommand = (program)
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
}
|
|
330
|
+
const createCheckCommand = (program)=>{
|
|
331
|
+
program.command({
|
|
332
|
+
description: "Check code health (static analysis)",
|
|
333
|
+
name: "check"
|
|
334
|
+
}).option({
|
|
335
|
+
defaultValue: undefined,
|
|
336
|
+
description: `Filter the compliance check to run (accepted value: ${ONLY_VALUES.join(", ")})`,
|
|
337
|
+
key: "filter",
|
|
338
|
+
name: "filter"
|
|
339
|
+
}).task({
|
|
340
|
+
handler (_, argv) {
|
|
341
|
+
logCheckableFiles(argv.operands);
|
|
342
|
+
},
|
|
343
|
+
skip: ifFilterDefinedAndNotEqualTo("code")
|
|
344
|
+
}).task({
|
|
345
|
+
async handler () {
|
|
346
|
+
await turbo("build", {
|
|
347
|
+
excludeExamples: true,
|
|
348
|
+
hasLiveOutput: false
|
|
349
|
+
});
|
|
350
|
+
},
|
|
351
|
+
label: label$4("Prepare the project"),
|
|
352
|
+
skip ({ filter }) {
|
|
353
|
+
return filter === "commit"; // No need to build if only commit is checked
|
|
354
|
+
}
|
|
355
|
+
}).task({
|
|
356
|
+
async handler () {
|
|
357
|
+
await checkDependency();
|
|
358
|
+
},
|
|
359
|
+
label: label$4("Check dependency compliance"),
|
|
360
|
+
skip: ifFilterDefinedAndNotEqualTo("dependency")
|
|
361
|
+
}).task({
|
|
362
|
+
async handler (_, argv) {
|
|
363
|
+
const filenames = argv.operands;
|
|
364
|
+
await checkCode(filenames);
|
|
365
|
+
},
|
|
366
|
+
label: label$4("Check code compliance"),
|
|
367
|
+
skip: ifFilterDefinedAndNotEqualTo("code")
|
|
368
|
+
}).task({
|
|
369
|
+
async handler () {
|
|
370
|
+
await checkType();
|
|
371
|
+
},
|
|
372
|
+
label: label$4("Check type compliance"),
|
|
373
|
+
skip (context, argv) {
|
|
374
|
+
return ifFilterDefinedAndNotEqualTo("type")(context) || !hasDependency("typescript") || /**
|
|
375
|
+
* For now, skip type-checking if some files are passed down.
|
|
376
|
+
* @see https://github.com/microsoft/TypeScript/issues/27379
|
|
377
|
+
*/ argv.operands.length > 0;
|
|
378
|
+
}
|
|
379
|
+
}).task({
|
|
380
|
+
async handler () {
|
|
381
|
+
await checkCommit();
|
|
382
|
+
},
|
|
383
|
+
label: label$4("Check commit compliance"),
|
|
384
|
+
skip (context) {
|
|
385
|
+
return context.filter !== "commit";
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
};
|
|
389
|
+
const label$4 = (message)=>`${message} 🧐`;
|
|
390
|
+
const ifFilterDefinedAndNotEqualTo = (filter)=>(context)=>{
|
|
391
|
+
return context.filter !== undefined && context.filter !== filter;
|
|
392
|
+
};
|
|
377
393
|
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
};
|
|
417
|
-
const label$3 = (message)
|
|
418
|
-
const cleanFiles = async (files)
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
};
|
|
424
|
-
const isDirectoryExistAndNotEmpty = (path)
|
|
425
|
-
|
|
426
|
-
};
|
|
427
|
-
const retrieveIgnoredFiles = async ()
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
394
|
+
const createCleanCommand = (program)=>{
|
|
395
|
+
program.command({
|
|
396
|
+
description: "Clean the project",
|
|
397
|
+
name: "clean"
|
|
398
|
+
}).task({
|
|
399
|
+
async handler () {
|
|
400
|
+
const cachePath = "node_modules/.cache";
|
|
401
|
+
const files = await retrieveIgnoredFiles();
|
|
402
|
+
if (isDirectoryExistAndNotEmpty(resolveFromProjectDirectory(cachePath))) {
|
|
403
|
+
files.push(cachePath);
|
|
404
|
+
}
|
|
405
|
+
return files;
|
|
406
|
+
},
|
|
407
|
+
key: "files",
|
|
408
|
+
label: label$3("Retrieve removable files")
|
|
409
|
+
}).task({
|
|
410
|
+
async handler ({ files }) {
|
|
411
|
+
if (files.length === 0) return;
|
|
412
|
+
await cleanFiles(files);
|
|
413
|
+
},
|
|
414
|
+
label ({ files }) {
|
|
415
|
+
return files.length > 0 ? label$3("Clean assets") : "Already clean ✨";
|
|
416
|
+
}
|
|
417
|
+
}).task({
|
|
418
|
+
handler ({ files }) {
|
|
419
|
+
helpers.message(files.join("\n "), {
|
|
420
|
+
label: "Removed assets",
|
|
421
|
+
lineBreak: {
|
|
422
|
+
end: false,
|
|
423
|
+
start: true
|
|
424
|
+
},
|
|
425
|
+
type: "information"
|
|
426
|
+
});
|
|
427
|
+
},
|
|
428
|
+
skip ({ files }) {
|
|
429
|
+
return files.length === 0;
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
};
|
|
433
|
+
const label$3 = (message)=>`${message} 🧹`;
|
|
434
|
+
const cleanFiles = async (files)=>{
|
|
435
|
+
return Promise.all(files.map(async (file)=>rm(file, {
|
|
436
|
+
force: true,
|
|
437
|
+
recursive: true
|
|
438
|
+
})));
|
|
439
|
+
};
|
|
440
|
+
const isDirectoryExistAndNotEmpty = (path)=>{
|
|
441
|
+
return existsSync(path) && readdirSync(path).length > 0;
|
|
442
|
+
};
|
|
443
|
+
const retrieveIgnoredFiles = async ()=>{
|
|
444
|
+
const rawFiles = await helpers.exec("git clean -fdXn");
|
|
445
|
+
return rawFiles.split(/\n|\r\n/).filter((cleanOutput)=>!PRESERVE_FILES.some((excludedFile)=>cleanOutput.includes(excludedFile))).map((cleanOutput)=>cleanOutput.split(" ").at(-1));
|
|
446
|
+
};
|
|
447
|
+
const PRESERVE_FILES = [
|
|
448
|
+
"node_modules"
|
|
449
|
+
];
|
|
431
450
|
|
|
432
|
-
|
|
433
|
-
//#region package.json
|
|
434
|
-
var version = "2.36.0-next-d491b30";
|
|
451
|
+
var version = "2.36.0";
|
|
435
452
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
453
|
+
const createCreateCommand = (program)=>{
|
|
454
|
+
program.command({
|
|
455
|
+
description: "Scaffold a new project",
|
|
456
|
+
name: "create"
|
|
457
|
+
}).task({
|
|
458
|
+
handler () {
|
|
459
|
+
botMessage({
|
|
460
|
+
description: "I can guarantee you a project creation in under 1 minute 🚀",
|
|
461
|
+
title: `I'm Stack v${version}, your bot assistant`,
|
|
462
|
+
type: "information"
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}).task({
|
|
466
|
+
async handler () {
|
|
467
|
+
// Check pnpm availability by verifying its version
|
|
468
|
+
await getNpmVersion();
|
|
469
|
+
},
|
|
470
|
+
label: label$2("Check pre-requisites")
|
|
471
|
+
}).input({
|
|
472
|
+
key: "inputName",
|
|
473
|
+
label: "What's your project name?",
|
|
474
|
+
type: "text"
|
|
475
|
+
}).input({
|
|
476
|
+
key: "inputDescription",
|
|
477
|
+
label: "How would you describe it?",
|
|
478
|
+
type: "text"
|
|
479
|
+
}).input({
|
|
480
|
+
defaultValue: "git@github.com:adbayb/xxx.git",
|
|
481
|
+
key: "inputUrl",
|
|
482
|
+
label: "Where will it be stored? (Git remote URL)",
|
|
483
|
+
type: "text"
|
|
484
|
+
}).input({
|
|
485
|
+
defaultValue: "single-project",
|
|
486
|
+
key: "inputTemplate",
|
|
487
|
+
label: "Which template you would like to apply?",
|
|
488
|
+
options: [
|
|
489
|
+
"single-project",
|
|
490
|
+
"multi-projects"
|
|
491
|
+
],
|
|
492
|
+
type: "select"
|
|
493
|
+
}).task({
|
|
494
|
+
async handler ({ inputDescription, inputName, inputUrl }) {
|
|
495
|
+
if (!inputName) {
|
|
496
|
+
throw createError("stack create", "The project name is not optional. Make sure to provide a valid value (non-empty string).");
|
|
497
|
+
}
|
|
498
|
+
const { repoName, repoOwner } = (inputUrl.startsWith("git") ? /^git@.*:(?<repoOwner>.*)\/(?<repoName>.*)\.git$/ : /^https?:\/\/.*\/(?<repoOwner>.*)\/(?<repoName>.*)\.git$/).exec(inputUrl)?.groups ?? {};
|
|
499
|
+
if (!repoOwner || !repoName) {
|
|
500
|
+
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.");
|
|
501
|
+
}
|
|
502
|
+
const nodeVersion = await request.get("https://resolve-node.vercel.app/lts", "text");
|
|
503
|
+
const { version: npmVersion } = await request.get("https://registry.npmjs.org/pnpm/latest", "json");
|
|
504
|
+
return {
|
|
505
|
+
licenseYear: new Date().getFullYear().toString(),
|
|
506
|
+
nodeVersion: nodeVersion.replace("v", ""),
|
|
507
|
+
npmVersion: String(npmVersion),
|
|
508
|
+
projectDescription: inputDescription.charAt(0).toUpperCase() + inputDescription.slice(1),
|
|
509
|
+
projectName: inputName.toLowerCase(),
|
|
510
|
+
projectUrl: inputUrl,
|
|
511
|
+
repoId: `${repoOwner}/${repoName}`
|
|
512
|
+
};
|
|
513
|
+
},
|
|
514
|
+
key: "data",
|
|
515
|
+
label: label$2("Check and format input")
|
|
516
|
+
}).task({
|
|
517
|
+
async handler ({ data }) {
|
|
518
|
+
const projectPath = resolve(process.cwd(), data.projectName);
|
|
519
|
+
await mkdir(projectPath);
|
|
520
|
+
process.chdir(projectPath);
|
|
521
|
+
},
|
|
522
|
+
label ({ data }) {
|
|
523
|
+
return label$2(`Create \`${data.projectName}\` folder`);
|
|
524
|
+
}
|
|
525
|
+
}).task({
|
|
526
|
+
async handler ({ data }) {
|
|
527
|
+
await helpers.exec("git init");
|
|
528
|
+
await helpers.exec(`git remote add origin ${data.projectUrl}`);
|
|
529
|
+
},
|
|
530
|
+
label: label$2("Initialize `git`")
|
|
531
|
+
}).task({
|
|
532
|
+
handler ({ data, inputTemplate }) {
|
|
533
|
+
applyTemplate(inputTemplate, data);
|
|
534
|
+
},
|
|
535
|
+
label: label$2("Apply template")
|
|
536
|
+
}).task({
|
|
537
|
+
async handler ({ data: { projectName }, inputTemplate }) {
|
|
538
|
+
await symlink(join(inputTemplate === "single-project" ? projectName : join("libraries", projectName), "README.md"), "./README.md");
|
|
539
|
+
},
|
|
540
|
+
label: label$2("Create a symlink to `README.md` file")
|
|
541
|
+
}).task({
|
|
542
|
+
async handler () {
|
|
543
|
+
await setPackageManager();
|
|
544
|
+
},
|
|
545
|
+
label: label$2("Set up the package manager")
|
|
546
|
+
}).task({
|
|
547
|
+
async handler ({ data }) {
|
|
548
|
+
const localDevelopmentDependencies = [
|
|
549
|
+
"quickbundle",
|
|
550
|
+
"vitest"
|
|
551
|
+
];
|
|
552
|
+
const globalDevelopmentDependencies = [
|
|
553
|
+
"@adbayb/stack"
|
|
554
|
+
];
|
|
555
|
+
try {
|
|
556
|
+
await helpers.exec(`pnpm add ${globalDevelopmentDependencies.join(" ")} --save-dev --ignore-workspace-root-check`);
|
|
557
|
+
await helpers.exec(`pnpm add ${localDevelopmentDependencies.join(" ")} --save-dev --filter ${data.projectName}`);
|
|
558
|
+
await helpers.exec("pnpm install");
|
|
559
|
+
} catch (error) {
|
|
560
|
+
throw createError("pnpm", error);
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
label: label$2("Install dependencies")
|
|
564
|
+
}).task({
|
|
565
|
+
async handler () {
|
|
566
|
+
await helpers.exec("stack install");
|
|
567
|
+
},
|
|
568
|
+
label: label$2("Run `stack install`")
|
|
569
|
+
}).task({
|
|
570
|
+
async handler () {
|
|
571
|
+
await helpers.exec("git add -A");
|
|
572
|
+
await helpers.exec('git commit -m "chore: initial commit"');
|
|
573
|
+
},
|
|
574
|
+
label: label$2("Commit")
|
|
575
|
+
}).task({
|
|
576
|
+
handler ({ data }) {
|
|
577
|
+
botMessage({
|
|
578
|
+
description: `Run \`cd ./${data.projectName}\` and Enjoy 🚀`,
|
|
579
|
+
title: "The project was successfully created",
|
|
580
|
+
type: "success"
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
};
|
|
585
|
+
const label$2 = (message)=>`${message} 🔨`;
|
|
554
586
|
/**
|
|
555
|
-
* A simple template engine to evaluate dynamic expressions and apply side effets (such as hydrating a content with values from an input object) on impacted template files.
|
|
556
|
-
* @param template - The selected template.
|
|
557
|
-
* @param dataModel - Data model mapping the template expression key with its corresponding value.
|
|
558
|
-
* @example
|
|
559
|
-
* applyTemplate(
|
|
560
|
-
* { toReplace: "value" },
|
|
561
|
-
* );
|
|
562
|
-
*/
|
|
563
|
-
const
|
|
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
|
-
});
|
|
587
|
+
* A simple template engine to evaluate dynamic expressions and apply side effets (such as hydrating a content with values from an input object) on impacted template files.
|
|
588
|
+
* @param template - The selected template.
|
|
589
|
+
* @param dataModel - Data model mapping the template expression key with its corresponding value.
|
|
590
|
+
* @example
|
|
591
|
+
* applyTemplate(
|
|
592
|
+
* { toReplace: "value" },
|
|
593
|
+
* );
|
|
594
|
+
*/ const applyTemplate = (template, dataModel)=>{
|
|
595
|
+
const templateExtension = ".tmpl";
|
|
596
|
+
const templateRootPath = resolveFromStackDirectory(join("./templates", template));
|
|
597
|
+
const projectRootPath = resolveFromProjectDirectory("./");
|
|
598
|
+
const templateExpressionRegExp = /{{(.*?)}}/g;
|
|
599
|
+
const evaluate = (content)=>{
|
|
600
|
+
return content.replaceAll(templateExpressionRegExp, (_, key)=>dataModel[key] || "");
|
|
601
|
+
};
|
|
602
|
+
/** Copy the template before mutations. */ cpSync(templateRootPath, projectRootPath, {
|
|
603
|
+
force: true,
|
|
604
|
+
recursive: true
|
|
605
|
+
});
|
|
606
|
+
/** Template file mutations. */ new fdir().withBasePath().glob(`**/*${templateExtension}`).crawl(projectRootPath).sync().forEach((templateFilePath)=>{
|
|
607
|
+
const projectFilePath = templateFilePath.slice(0, templateFilePath.lastIndexOf(templateExtension));
|
|
608
|
+
const content = evaluate(readFileSync(templateFilePath, "utf8"));
|
|
609
|
+
renameSync(templateFilePath, projectFilePath);
|
|
610
|
+
writeFileSync(projectFilePath, content, "utf8");
|
|
611
|
+
});
|
|
612
|
+
/** Template folder mutations. */ new fdir().withBasePath().onlyDirs().filter((path)=>{
|
|
613
|
+
return templateExpressionRegExp.test(path);
|
|
614
|
+
}).crawl(projectRootPath).sync()// Re-order from longest to lowest path length to apply first renaming operations on deepest file structure
|
|
615
|
+
.toSorted((a, b)=>b.length - a.length).forEach((templateFolderPath)=>{
|
|
616
|
+
const newPath = templateFolderPath.replaceAll(templateExpressionRegExp, (_, dataModelKey)=>{
|
|
617
|
+
return dataModel[dataModelKey];
|
|
618
|
+
});
|
|
619
|
+
renameSync(templateFolderPath, newPath);
|
|
620
|
+
});
|
|
591
621
|
};
|
|
592
622
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
const fixFormatting = async (files)
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
623
|
+
const PRETTIER_IGNORE_FILES = [
|
|
624
|
+
"pnpm-lock.yaml"
|
|
625
|
+
];
|
|
626
|
+
const fixFormatting = async (files)=>{
|
|
627
|
+
let prettierFiles = [];
|
|
628
|
+
if (files.length === 0) {
|
|
629
|
+
prettierFiles.push(`"**/!(${PRETTIER_IGNORE_FILES.join("|")})"`);
|
|
630
|
+
} else {
|
|
631
|
+
prettierFiles = files.filter((file)=>{
|
|
632
|
+
return !PRETTIER_IGNORE_FILES.some((filename)=>file.endsWith(filename));
|
|
633
|
+
});
|
|
634
|
+
if (prettierFiles.length === 0) return;
|
|
635
|
+
}
|
|
636
|
+
const arguments_ = [
|
|
637
|
+
...prettierFiles
|
|
638
|
+
];
|
|
639
|
+
if (existsSync(resolveFromProjectDirectory(".gitignore"))) {
|
|
640
|
+
arguments_.push("--ignore-path .gitignore");
|
|
641
|
+
}
|
|
642
|
+
arguments_.push("--write", "--ignore-unknown", "--no-error-on-unmatched-pattern");
|
|
643
|
+
try {
|
|
644
|
+
return await helpers.exec(`prettier ${arguments_.join(" ")}`);
|
|
645
|
+
} catch (error) {
|
|
646
|
+
throw createError("prettier", error);
|
|
647
|
+
}
|
|
613
648
|
};
|
|
614
649
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
650
|
+
const fixLinter = eslint({
|
|
651
|
+
isFixMode: true
|
|
652
|
+
});
|
|
618
653
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
};
|
|
647
|
-
const label$1 = (message)
|
|
654
|
+
const createFixCommand = (program)=>{
|
|
655
|
+
program.command({
|
|
656
|
+
description: "Fix auto-fixable issues",
|
|
657
|
+
name: "fix"
|
|
658
|
+
}).task({
|
|
659
|
+
handler (_, argv) {
|
|
660
|
+
logCheckableFiles(argv.operands);
|
|
661
|
+
}
|
|
662
|
+
}).task({
|
|
663
|
+
async handler () {
|
|
664
|
+
await turbo("build", {
|
|
665
|
+
excludeExamples: true,
|
|
666
|
+
hasLiveOutput: false
|
|
667
|
+
});
|
|
668
|
+
},
|
|
669
|
+
label: label$1("Prepare the project")
|
|
670
|
+
}).task({
|
|
671
|
+
async handler (_, argv) {
|
|
672
|
+
await fixLinter(argv.operands);
|
|
673
|
+
},
|
|
674
|
+
label: label$1("Fix linter issues")
|
|
675
|
+
}).task({
|
|
676
|
+
async handler (_, argv) {
|
|
677
|
+
await fixFormatting(argv.operands);
|
|
678
|
+
},
|
|
679
|
+
label: label$1("Fix formatting issues")
|
|
680
|
+
});
|
|
681
|
+
};
|
|
682
|
+
const label$1 = (message)=>`${message} 🚑`;
|
|
648
683
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
};
|
|
667
|
-
const label = (message)
|
|
668
|
-
const installGitHook = async (hook, content)
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
684
|
+
const createInstallCommand = (program)=>{
|
|
685
|
+
program.command({
|
|
686
|
+
description: "Install required setup",
|
|
687
|
+
name: "install"
|
|
688
|
+
}).task({
|
|
689
|
+
async handler () {
|
|
690
|
+
const lineBreakMatcher = String.raw`\n|\r\n`;
|
|
691
|
+
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(" "))')`);
|
|
692
|
+
await installGitHook("pre-commit", `${stackCommand} && git add -A`);
|
|
693
|
+
},
|
|
694
|
+
label: label("Install `git.pre-commit` hook")
|
|
695
|
+
}).task({
|
|
696
|
+
async handler () {
|
|
697
|
+
await installGitHook("commit-msg", getStackCommand("check --filter commit"));
|
|
698
|
+
},
|
|
699
|
+
label: label("Install `git.commit-msg` hook")
|
|
700
|
+
});
|
|
701
|
+
};
|
|
702
|
+
const label = (message)=>`${message} 📲`;
|
|
703
|
+
const installGitHook = async (hook, content)=>{
|
|
704
|
+
const filename = resolveFromProjectDirectory(`.git/hooks/${hook}`);
|
|
705
|
+
await writeFile(filename, content);
|
|
706
|
+
return chmod(filename, "0755");
|
|
672
707
|
};
|
|
673
708
|
|
|
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
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
return !context[validOption];
|
|
724
|
-
};
|
|
709
|
+
const createReleaseCommand = (program)=>{
|
|
710
|
+
program.command({
|
|
711
|
+
description: "Log, version, and publish package(s)",
|
|
712
|
+
name: "release"
|
|
713
|
+
}).option({
|
|
714
|
+
description: "Add a new changelog entry",
|
|
715
|
+
key: "log",
|
|
716
|
+
name: "log"
|
|
717
|
+
}).option({
|
|
718
|
+
description: "Add an empty changelog entry",
|
|
719
|
+
key: "emptyLog",
|
|
720
|
+
name: "empty-log"
|
|
721
|
+
}).option({
|
|
722
|
+
description: "Bump the package(s) version",
|
|
723
|
+
key: "tag",
|
|
724
|
+
name: "tag"
|
|
725
|
+
}).option({
|
|
726
|
+
description: "Publish package(s) to the registry",
|
|
727
|
+
key: "publish",
|
|
728
|
+
name: "publish"
|
|
729
|
+
}).task({
|
|
730
|
+
async handler () {
|
|
731
|
+
helpers.message("New changelog entry\n");
|
|
732
|
+
await changeset("changeset");
|
|
733
|
+
},
|
|
734
|
+
skip: ifNotEqualTo("log")
|
|
735
|
+
}).task({
|
|
736
|
+
async handler () {
|
|
737
|
+
helpers.message("New empty changelog entry\n");
|
|
738
|
+
await changeset("changeset --empty");
|
|
739
|
+
},
|
|
740
|
+
skip: ifNotEqualTo("emptyLog")
|
|
741
|
+
}).task({
|
|
742
|
+
async handler () {
|
|
743
|
+
helpers.message("Bumping the package(s) version\n");
|
|
744
|
+
await changeset("changeset version && pnpm install --no-frozen-lockfile");
|
|
745
|
+
},
|
|
746
|
+
skip: ifNotEqualTo("tag")
|
|
747
|
+
}).task({
|
|
748
|
+
async handler () {
|
|
749
|
+
helpers.message("Publishing package(s) to the registry\n");
|
|
750
|
+
await changeset("stack build && pnpm changeset publish");
|
|
751
|
+
},
|
|
752
|
+
skip: ifNotEqualTo("publish")
|
|
753
|
+
});
|
|
754
|
+
};
|
|
755
|
+
const ifNotEqualTo = (validOption)=>(context)=>{
|
|
756
|
+
return !context[validOption];
|
|
757
|
+
};
|
|
725
758
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
759
|
+
const createStartCommand = (program)=>{
|
|
760
|
+
program.command({
|
|
761
|
+
description: "Start the project in production mode",
|
|
762
|
+
name: "start"
|
|
763
|
+
}).task({
|
|
764
|
+
async handler () {
|
|
765
|
+
await turbo("start");
|
|
766
|
+
}
|
|
767
|
+
});
|
|
735
768
|
};
|
|
736
769
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
770
|
+
const createTestCommand = (program)=>{
|
|
771
|
+
program.command({
|
|
772
|
+
description: "Test the code execution",
|
|
773
|
+
name: "test"
|
|
774
|
+
}).task({
|
|
775
|
+
async handler () {
|
|
776
|
+
await turbo("test");
|
|
777
|
+
}
|
|
778
|
+
});
|
|
746
779
|
};
|
|
747
780
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
781
|
+
const createWatchCommand = (program)=>{
|
|
782
|
+
program.command({
|
|
783
|
+
description: "Build and start the project in development mode",
|
|
784
|
+
name: "watch"
|
|
785
|
+
}).task({
|
|
786
|
+
async handler () {
|
|
787
|
+
await turbo("watch");
|
|
788
|
+
}
|
|
789
|
+
});
|
|
757
790
|
};
|
|
758
791
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
792
|
+
const createProgram = (...commandFactories)=>{
|
|
793
|
+
const program = termost({
|
|
794
|
+
description: "Toolbox to easily scaffold and maintain a project",
|
|
795
|
+
name: "stack",
|
|
796
|
+
onException () {
|
|
797
|
+
botMessage({
|
|
798
|
+
description: "Keep calm and carry on with some coffee ☕️",
|
|
799
|
+
title: "Oops, an error occurred",
|
|
800
|
+
type: "error"
|
|
801
|
+
});
|
|
802
|
+
},
|
|
803
|
+
version: version
|
|
804
|
+
});
|
|
805
|
+
for (const commandBuilder of commandFactories){
|
|
806
|
+
commandBuilder(program);
|
|
807
|
+
}
|
|
775
808
|
};
|
|
776
809
|
createProgram(createCreateCommand, createInstallCommand, createCleanCommand, createCheckCommand, createFixCommand, createStartCommand, createBuildCommand, createWatchCommand, createTestCommand, createReleaseCommand);
|
|
777
|
-
|
|
778
|
-
//#endregion
|