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