@adbayb/stack 2.39.0 → 2.40.0-next-f8ad939

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 (56) hide show
  1. package/bin/index.js +2 -2
  2. package/configs/{prettier → oxfmt}/README.md +6 -14
  3. package/configs/{prettier/index.js → oxfmt/index.ts} +13 -9
  4. package/configs/{eslint → oxlint}/README.md +4 -4
  5. package/configs/oxlint/index.ts +30 -0
  6. package/configs/typescript/index.json +1 -7
  7. package/dist/index.js +229 -185
  8. package/package.json +30 -48
  9. package/templates/multi-projects/.vscode/extensions.json +1 -1
  10. package/templates/multi-projects/.vscode/settings.json +7 -13
  11. package/templates/{single-project/package.json.tmpl → multi-projects/package.json} +7 -13
  12. package/templates/multi-projects/{pnpm-workspace.yaml.tmpl → pnpm-workspace.yaml} +4 -2
  13. package/templates/single-project/.vscode/extensions.json +1 -1
  14. package/templates/single-project/.vscode/settings.json +7 -13
  15. package/templates/{multi-projects/package.json.tmpl → single-project/package.json} +7 -13
  16. package/templates/single-project/{pnpm-workspace.yaml.tmpl → pnpm-workspace.yaml} +4 -2
  17. package/configs/eslint/constants.js +0 -37
  18. package/configs/eslint/helpers.js +0 -5
  19. package/configs/eslint/index.js +0 -30
  20. package/configs/eslint/presets/dependencies.js +0 -14
  21. package/configs/eslint/presets/eslint.js +0 -133
  22. package/configs/eslint/presets/import.js +0 -65
  23. package/configs/eslint/presets/jsdoc.js +0 -78
  24. package/configs/eslint/presets/markdown.js +0 -1
  25. package/configs/eslint/presets/node.js +0 -51
  26. package/configs/eslint/presets/prettier.js +0 -1
  27. package/configs/eslint/presets/react.js +0 -111
  28. package/configs/eslint/presets/sonar.js +0 -201
  29. package/configs/eslint/presets/stylistic.js +0 -79
  30. package/configs/eslint/presets/test.js +0 -87
  31. package/configs/eslint/presets/typescript.js +0 -198
  32. package/configs/eslint/presets/unicorn.js +0 -257
  33. package/templates/multi-projects/.npmrc.tmpl +0 -1
  34. package/templates/single-project/.npmrc.tmpl +0 -1
  35. package/templates/multi-projects/.changeset/{config.json.tmpl → config.json} +0 -0
  36. package/templates/multi-projects/.changeset/{welcome.md.tmpl → welcome.md} +0 -0
  37. package/templates/multi-projects/.github/{PULL_REQUEST_TEMPLATE.md.tmpl → PULL_REQUEST_TEMPLATE.md} +0 -0
  38. package/templates/multi-projects/{.nvmrc.tmpl → .nvmrc} +0 -0
  39. package/templates/multi-projects/{CONTRIBUTING.md.tmpl → CONTRIBUTING.md} +0 -0
  40. package/templates/multi-projects/{LICENSE.tmpl → LICENSE} +0 -0
  41. package/templates/multi-projects/examples/default/{package.json.tmpl → package.json} +0 -0
  42. package/templates/multi-projects/examples/default/src/{index.ts.tmpl → index.ts} +0 -0
  43. package/templates/multi-projects/libraries/{{projectName}}/{README.md.tmpl → README.md} +0 -0
  44. package/templates/multi-projects/libraries/{{projectName}}/{package.json.tmpl → package.json} +10 -10
  45. package/templates/multi-projects/libraries/{{projectName}}/src/{index.test.ts.tmpl → index.test.ts} +0 -0
  46. package/templates/single-project/.changeset/{config.json.tmpl → config.json} +0 -0
  47. package/templates/single-project/.changeset/{welcome.md.tmpl → welcome.md} +0 -0
  48. package/templates/single-project/.github/{PULL_REQUEST_TEMPLATE.md.tmpl → PULL_REQUEST_TEMPLATE.md} +0 -0
  49. package/templates/single-project/{.nvmrc.tmpl → .nvmrc} +0 -0
  50. package/templates/single-project/{CONTRIBUTING.md.tmpl → CONTRIBUTING.md} +0 -0
  51. package/templates/single-project/{LICENSE.tmpl → LICENSE} +0 -0
  52. package/templates/single-project/examples/default/{package.json.tmpl → package.json} +0 -0
  53. package/templates/single-project/examples/default/src/{index.ts.tmpl → index.ts} +0 -0
  54. package/templates/single-project/{{projectName}}/{README.md.tmpl → README.md} +0 -0
  55. package/templates/single-project/{{projectName}}/{package.json.tmpl → package.json} +10 -10
  56. /package/templates/single-project/{{projectName}}/src/{index.test.ts.tmpl → index.test.ts} +0 -0
package/dist/index.js CHANGED
@@ -1,9 +1,8 @@
1
1
  import { helpers, termost } from "termost";
2
2
  import { createRequire } from "node:module";
3
- import { join, resolve } from "node:path";
4
- import { cpSync, existsSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
5
- import { chmod, mkdir, rm, symlink, writeFile } from "node:fs/promises";
6
- import { fdir } from "fdir";
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
7
  //#region src/helpers.ts
9
8
  const require = createRequire(import.meta.url);
@@ -12,21 +11,20 @@ function assert(expectedCondition, createError) {
12
11
  }
13
12
  /**
14
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
+ *
15
23
  * @param input - Message factory.
16
24
  * @param input.title - Title input.
17
25
  * @param input.description - Description input.
18
26
  * @param input.body - Body input.
19
27
  * @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
28
  */
31
29
  const botMessage = (input) => {
32
30
  const { type } = input;
@@ -46,23 +44,27 @@ ${input.body}
46
44
  };
47
45
  /**
48
46
  * Resolve a relative path to an absolute one resolved from the generated project root directory.
47
+ *
48
+ * @example
49
+ * resolveFromWorkingDirectory(".gitignore");
50
+ *
49
51
  * @param path - The relative path.
50
52
  * @returns The resolved absolute path.
51
- * @example
52
- * resolveFromProjectDirectory(".gitignore");
53
53
  */
54
- const resolveFromProjectDirectory = (path) => {
55
- return resolve(process.cwd(), path);
54
+ const resolveFromWorkingDirectory = (...path) => {
55
+ return resolve(process.cwd(), ...path);
56
56
  };
57
57
  /**
58
58
  * Resolve a relative path to an absolute one resolved from the `stack` node module directory.
59
+ *
60
+ * @example
61
+ * resolveFromPackageDirectory("./templates");
62
+ *
59
63
  * @param path - The relative path.
60
64
  * @returns The resolved absolute path.
61
- * @example
62
- * resolveFromStackDirectory("./templates");
63
65
  */
64
- const resolveFromStackDirectory = (path) => {
65
- return resolve(import.meta.dirname, "../", path);
66
+ const resolveFromPackageDirectory = (...path) => {
67
+ return resolve(import.meta.dirname, "../", ...path);
66
68
  };
67
69
  const createError = (bin, error) => {
68
70
  const errorMessage = `\`${bin}\` command failed.\n${String(error)}`;
@@ -72,7 +74,7 @@ const createError = (bin, error) => {
72
74
  }
73
75
  return new Error(errorMessage);
74
76
  };
75
- const getNpmVersion = async () => {
77
+ const getPnpmVersion = async () => {
76
78
  try {
77
79
  return await helpers.exec("pnpm -v");
78
80
  } catch {
@@ -82,44 +84,49 @@ const getNpmVersion = async () => {
82
84
  const getStackCommand = (command) => {
83
85
  return `pnpm stack ${command}`;
84
86
  };
85
- const hasDependency = (packageName) => {
86
- return Boolean(require.resolve(packageName));
87
- };
88
87
  const setPackageManager = async () => {
89
88
  /**
90
- * 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
+ *
91
92
  * @see {@link https://github.com/nodejs/corepack/issues/613}
92
93
  */
93
- return helpers.exec("npx corepack enable");
94
+ return helpers.exec("pnx corepack enable");
94
95
  };
95
- const request = { async get(url, responseType) {
96
+ async function getRequest(url, responseType) {
96
97
  const response = await fetch(url);
97
98
  if (!response.ok) throw createError("fetch", `Failed to fetch resources from ${url} (${JSON.stringify({
98
99
  status: response.status,
99
100
  statusText: response.statusText
100
101
  })})`);
101
- return response[responseType === "text" ? "text" : "json"]();
102
- } };
103
- const eslint = (options) => async (files = []) => {
104
- let eslintFiles = [];
105
- if (files.length === 0) eslintFiles.push(".");
106
- else {
107
- eslintFiles = files.filter((file) => {
108
- return ESLINT_EXTENSIONS.some((extension) => file.endsWith(extension));
109
- });
110
- if (eslintFiles.length === 0) return;
111
- }
112
- const arguments_ = [
113
- ...eslintFiles,
114
- "--cache",
115
- `--cache-location ${resolveFromProjectDirectory("node_modules/.cache/.eslintcache")}`,
102
+ if (responseType === "text") return response.text();
103
+ return response.json();
104
+ }
105
+ const request = { get: getRequest };
106
+ const oxlint = (options) => async (files = []) => {
107
+ const args = [
108
+ ...files,
109
+ "--disable-nested-config",
116
110
  "--no-error-on-unmatched-pattern"
117
111
  ];
118
- if (options.isFixMode) arguments_.push("--fix");
112
+ if (options.isFixMode) args.push("--fix");
113
+ try {
114
+ return await helpers.exec(`oxlint ${args.join(" ")}`);
115
+ } catch (error) {
116
+ throw createError("oxlint", error instanceof Error ? error : new Error(String(error)));
117
+ }
118
+ };
119
+ const oxfmt = (options) => async (files = []) => {
120
+ const args = [
121
+ ...files,
122
+ "--disable-nested-config",
123
+ "--no-error-on-unmatched-pattern",
124
+ options.isFixMode ? "--write" : "--check"
125
+ ];
119
126
  try {
120
- return await helpers.exec(`eslint ${arguments_.join(" ")}`);
127
+ return await helpers.exec(`oxfmt ${args.join(" ")}`);
121
128
  } catch (error) {
122
- throw createError("eslint", error);
129
+ throw createError("oxfmt", error instanceof Error ? error : new Error(String(error)));
123
130
  }
124
131
  };
125
132
  const turbo = async (command, options = {}) => {
@@ -130,7 +137,7 @@ const turbo = async (command, options = {}) => {
130
137
  hasLiveOutput
131
138
  });
132
139
  } catch (error) {
133
- throw createError("turbo", error);
140
+ throw createError("turbo", error instanceof Error ? error : new Error(String(error)));
134
141
  }
135
142
  };
136
143
  const logCheckableFiles = (files) => {
@@ -156,21 +163,9 @@ const changeset = async (command) => {
156
163
  try {
157
164
  return await helpers.exec(command, { hasLiveOutput: true });
158
165
  } catch (error) {
159
- throw createError("changeset", error);
166
+ throw createError("changeset", error instanceof Error ? error : new Error(String(error)));
160
167
  }
161
168
  };
162
- const ESLINT_EXTENSIONS = [
163
- "js",
164
- "jsx",
165
- "cjs",
166
- "mjs",
167
- "ts",
168
- "tsx",
169
- "cts",
170
- "mts",
171
- "md",
172
- "mdx"
173
- ];
174
169
 
175
170
  //#endregion
176
171
  //#region src/commands/build.ts
@@ -185,7 +180,7 @@ const createBuildCommand = (program) => {
185
180
 
186
181
  //#endregion
187
182
  //#region src/commands/check/checkCode.ts
188
- const checkCode = eslint({ isFixMode: false });
183
+ const checkCode = oxlint({ isFixMode: false });
189
184
 
190
185
  //#endregion
191
186
  //#region src/commands/check/checkCommit.ts
@@ -193,7 +188,7 @@ const checkCommit = async () => {
193
188
  try {
194
189
  return await helpers.exec("commitlint --extends \"@commitlint/config-conventional\" --edit");
195
190
  } catch (error) {
196
- throw createError("commitlint", error);
191
+ throw createError("commitlint", error instanceof Error ? error : new Error(String(error)));
197
192
  }
198
193
  };
199
194
 
@@ -202,22 +197,22 @@ const checkCommit = async () => {
202
197
  const checkDependency = async () => {
203
198
  const stdout = await helpers.exec("pnpm recursive ls --json");
204
199
  const checkDependencyVersionMismatch = createPackagesVersionMismatchChecker();
205
- const packages = JSON.parse(stdout).map((package_) => {
206
- const packagePath = join(package_.path, "package.json");
207
- assert(package_.name, () => createPackageError(`\`${packagePath}\` must have a name field.`));
200
+ const packages = JSON.parse(stdout).map((pkg) => {
201
+ const packagePath = join(pkg.path, "package.json");
202
+ assert(pkg.name, () => createPackageError(`\`${packagePath}\` must have a name field.`));
208
203
  const packageContent = require(packagePath);
209
204
  const peerDependencies = packageContent.peerDependencies ?? {};
210
205
  const devDependencies = packageContent.devDependencies ?? {};
211
206
  return {
212
207
  dependencies: packageContent.dependencies ?? {},
213
208
  devDependencies,
214
- name: package_.name,
209
+ name: pkg.name,
215
210
  peerDependencies
216
211
  };
217
212
  });
218
- for (const package_ of packages) {
219
- checkDependencyVersionMismatch(package_);
220
- checkDependencyVersionRange(package_);
213
+ for (const pkg of packages) {
214
+ checkDependencyVersionMismatch(pkg);
215
+ checkDependencyVersionRange(pkg);
221
216
  }
222
217
  };
223
218
  const checkDependencyVersionRange = ({ dependencies, devDependencies, name, peerDependencies }) => {
@@ -255,11 +250,11 @@ const checkDependencyVersionRange = ({ dependencies, devDependencies, name, peer
255
250
  const createPackagesVersionMismatchChecker = () => {
256
251
  const monorepoDependencies = /* @__PURE__ */ new Map();
257
252
  const monorepoDevelopmentDependencies = /* @__PURE__ */ new Map();
258
- const lint = (package_, type) => {
259
- const packageName = package_.name;
253
+ const lint = (pkg, type) => {
254
+ const packageName = pkg.name;
260
255
  const isDevelopment = type === "development";
261
256
  const store = isDevelopment ? monorepoDevelopmentDependencies : monorepoDependencies;
262
- const dependencies = package_[isDevelopment ? "devDependencies" : "dependencies"];
257
+ const dependencies = pkg[isDevelopment ? "devDependencies" : "dependencies"];
263
258
  for (const [dependencyName, dependencyVersion] of Object.entries(dependencies)) {
264
259
  if (!dependencyVersion) continue;
265
260
  const storedVersion = store.get(dependencyName);
@@ -273,9 +268,9 @@ const createPackagesVersionMismatchChecker = () => {
273
268
  });
274
269
  }
275
270
  };
276
- return (package_) => {
277
- lint(package_, "development");
278
- lint(package_, "production");
271
+ return (pkg) => {
272
+ lint(pkg, "development");
273
+ lint(pkg, "production");
279
274
  };
280
275
  };
281
276
  const createPackageError = (message, context) => {
@@ -294,14 +289,8 @@ const isExcluded = (version) => {
294
289
  const hasCaret = (version) => version.startsWith("^");
295
290
 
296
291
  //#endregion
297
- //#region src/commands/check/checkType.ts
298
- const checkType = async () => {
299
- try {
300
- return await helpers.exec("pnpm --parallel exec tsc --noEmit");
301
- } catch (error) {
302
- throw createError("tsc", error);
303
- }
304
- };
292
+ //#region src/commands/check/checkFormatting.ts
293
+ const checkFormatting = oxfmt({ isFixMode: false });
305
294
 
306
295
  //#endregion
307
296
  //#region src/commands/check/check.ts
@@ -309,7 +298,7 @@ const ONLY_VALUES = [
309
298
  "commit",
310
299
  "code",
311
300
  "dependency",
312
- "type"
301
+ "formatting"
313
302
  ];
314
303
  const createCheckCommand = (program) => {
315
304
  program.command({
@@ -342,6 +331,13 @@ const createCheckCommand = (program) => {
342
331
  },
343
332
  label: label$4("Check dependency compliance"),
344
333
  skip: ifFilterDefinedAndNotEqualTo("dependency")
334
+ }).task({
335
+ async handler(_, argv) {
336
+ const filenames = argv.operands;
337
+ await checkFormatting(filenames);
338
+ },
339
+ label: label$4("Check formatting compliance"),
340
+ skip: ifFilterDefinedAndNotEqualTo("formatting")
345
341
  }).task({
346
342
  async handler(_, argv) {
347
343
  const filenames = argv.operands;
@@ -349,14 +345,6 @@ const createCheckCommand = (program) => {
349
345
  },
350
346
  label: label$4("Check code compliance"),
351
347
  skip: ifFilterDefinedAndNotEqualTo("code")
352
- }).task({
353
- async handler() {
354
- await checkType();
355
- },
356
- label: label$4("Check type compliance"),
357
- skip(context, argv) {
358
- return ifFilterDefinedAndNotEqualTo("type")(context) || !hasDependency("typescript") || argv.operands.length > 0;
359
- }
360
348
  }).task({
361
349
  async handler() {
362
350
  await checkCommit();
@@ -382,7 +370,7 @@ const createCleanCommand = (program) => {
382
370
  async handler() {
383
371
  const cachePath = "node_modules/.cache";
384
372
  const files = await retrieveIgnoredFiles();
385
- if (isDirectoryExistAndNotEmpty(resolveFromProjectDirectory(cachePath))) files.push(cachePath);
373
+ if (isDirectoryExistAndNotEmpty(resolveFromWorkingDirectory(cachePath))) files.push(cachePath);
386
374
  return files;
387
375
  },
388
376
  key: "files",
@@ -422,13 +410,13 @@ const isDirectoryExistAndNotEmpty = (path) => {
422
410
  return existsSync(path) && readdirSync(path).length > 0;
423
411
  };
424
412
  const retrieveIgnoredFiles = async () => {
425
- 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));
413
+ 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) ?? "").filter(Boolean);
426
414
  };
427
415
  const PRESERVE_FILES = ["node_modules"];
428
416
 
429
417
  //#endregion
430
418
  //#region package.json
431
- var version = "2.39.0";
419
+ var version = "2.40.0-next-f8ad939";
432
420
 
433
421
  //#endregion
434
422
  //#region src/commands/create.ts
@@ -439,12 +427,12 @@ const createCreateCommand = (program) => {
439
427
  }).task({ handler() {
440
428
  botMessage({
441
429
  description: "I can guarantee you a project creation in under 1 minute 🚀",
442
- title: `I'm Stack v${version}, your bot assistant`,
430
+ title: `I'm Stack v${version} 👋`,
443
431
  type: "information"
444
432
  });
445
433
  } }).task({
446
434
  async handler() {
447
- await getNpmVersion();
435
+ await getPnpmVersion();
448
436
  },
449
437
  label: label$2("Check pre-requisites")
450
438
  }).input({
@@ -467,64 +455,95 @@ const createCreateCommand = (program) => {
467
455
  options: ["single-project", "multi-projects"],
468
456
  type: "select"
469
457
  }).task({
470
- async handler({ inputDescription, inputName, inputUrl }) {
458
+ async handler({ inputDescription, inputName, inputTemplate, inputUrl }) {
471
459
  if (!inputName) throw createError("stack create", "The project name is not optional. Make sure to provide a valid value (non-empty string).");
472
460
  const { repoName, repoOwner } = (inputUrl.startsWith("git") ? /^git@.*:(?<repoOwner>.*)\/(?<repoName>.*)\.git$/ : /^https?:\/\/.*\/(?<repoOwner>.*)\/(?<repoName>.*)\.git$/).exec(inputUrl)?.groups ?? {};
473
461
  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.");
474
462
  const nodeVersion = await request.get("https://resolve-node.vercel.app/lts", "text");
475
- const { version: npmVersion } = await request.get("https://registry.npmjs.org/pnpm/latest", "json");
463
+ const { version: pnpmVersion } = await request.get("https://registry.npmjs.org/pnpm/latest", "json");
464
+ const projectName = slugify(inputName);
476
465
  return {
477
466
  licenseYear: (/* @__PURE__ */ new Date()).getFullYear().toString(),
478
467
  nodeVersion: nodeVersion.replace("v", ""),
479
- npmVersion: String(npmVersion),
480
- projectDescription: inputDescription.charAt(0).toUpperCase() + inputDescription.slice(1),
481
- projectName: inputName.toLowerCase(),
468
+ pnpmVersion: String(pnpmVersion),
469
+ projectDescription: toCapitalLetter(inputDescription),
470
+ projectName,
482
471
  projectUrl: inputUrl,
483
- repoId: `${repoOwner}/${repoName}`
472
+ repoId: `${repoOwner}/${repoName}`,
473
+ templatePath: resolveFromPackageDirectory("templates", inputTemplate),
474
+ workingPath: resolveFromWorkingDirectory(projectName)
484
475
  };
485
476
  },
486
477
  key: "data",
487
478
  label: label$2("Check and format input")
488
- }).task({
489
- async handler({ data }) {
490
- const projectPath = resolve(process.cwd(), data.projectName);
491
- await mkdir(projectPath);
492
- process.chdir(projectPath);
479
+ }).input({
480
+ defaultValue: true,
481
+ key: "canRemoveExistingDirectoryInput",
482
+ label({ data: { projectName } }) {
483
+ return label$2(`\`${projectName}\` directory already exists, do you want to remove it?`);
493
484
  },
494
- label({ data }) {
495
- return label$2(`Create \`${data.projectName}\` folder`);
485
+ skip({ data: { workingPath } }) {
486
+ return !existsSync(workingPath);
487
+ },
488
+ type: "confirm",
489
+ validate({ canRemoveExistingDirectoryInput, data: { projectName } }) {
490
+ if (canRemoveExistingDirectoryInput) return void 0;
491
+ return createError("mkdir", `Remove or rename the \`${projectName}\` existing directory to apply the template from a clean state.`);
496
492
  }
497
493
  }).task({
498
- async handler({ data }) {
499
- await helpers.exec("git init");
500
- await helpers.exec(`git remote add origin ${data.projectUrl}`);
494
+ async handler({ canRemoveExistingDirectoryInput, data: { licenseYear, nodeVersion, pnpmVersion, projectDescription, projectName, projectUrl, repoId, templatePath, workingPath }, inputTemplate }) {
495
+ if (canRemoveExistingDirectoryInput) await rm(workingPath, {
496
+ force: true,
497
+ recursive: true
498
+ });
499
+ return createTemplateEngine(workingPath, {
500
+ projectName,
501
+ templateModel: {
502
+ licenseYear,
503
+ nodeVersion,
504
+ pnpmVersion,
505
+ projectDescription,
506
+ projectName,
507
+ projectUrl,
508
+ repoId
509
+ },
510
+ templateName: inputTemplate,
511
+ templatePath
512
+ });
501
513
  },
502
- label: label$2("Initialize `git`")
514
+ key: "templateEngine",
515
+ label({ data: { projectName }, inputTemplate }) {
516
+ return label$2(`Copy \`${inputTemplate}\` template to \`${projectName}\` directory`);
517
+ }
503
518
  }).task({
504
- handler({ data, inputTemplate }) {
505
- applyTemplate(inputTemplate, data);
519
+ async handler({ templateEngine }) {
520
+ await templateEngine.processContents();
521
+ await templateEngine.processPaths();
506
522
  },
507
- label: label$2("Apply template")
523
+ label() {
524
+ return label$2("Process template");
525
+ }
508
526
  }).task({
509
- async handler({ data: { projectName }, inputTemplate }) {
510
- await symlink(join(inputTemplate === "single-project" ? projectName : join("libraries", projectName), "README.md"), "./README.md");
527
+ async handler({ data: { projectUrl } }) {
528
+ await helpers.exec("git init");
529
+ await helpers.exec(`git remote add origin ${projectUrl}`);
511
530
  },
512
- label: label$2("Create a symlink to `README.md` file")
531
+ label: label$2("Initialize `git`")
513
532
  }).task({
514
533
  async handler() {
515
534
  await setPackageManager();
516
535
  },
517
536
  label: label$2("Set up the package manager")
518
537
  }).task({
519
- async handler({ data }) {
538
+ async handler({ data: { projectName } }) {
520
539
  const localDevelopmentDependencies = ["quickbundle", "vitest"];
521
540
  const globalDevelopmentDependencies = ["@adbayb/stack"];
522
541
  try {
523
542
  await helpers.exec(`pnpm add ${globalDevelopmentDependencies.join(" ")} --save-dev --ignore-workspace-root-check`);
524
- await helpers.exec(`pnpm add ${localDevelopmentDependencies.join(" ")} --save-dev --filter ${data.projectName}`);
543
+ await helpers.exec(`pnpm add ${localDevelopmentDependencies.join(" ")} --save-dev --filter ${projectName}`);
525
544
  await helpers.exec("pnpm install");
526
545
  } catch (error) {
527
- throw createError("pnpm", error);
546
+ throw createError("pnpm", error instanceof Error ? error : new Error(String(error)));
528
547
  }
529
548
  },
530
549
  label: label$2("Install dependencies")
@@ -539,79 +558,104 @@ const createCreateCommand = (program) => {
539
558
  await helpers.exec("git commit -m \"chore: initial commit\"");
540
559
  },
541
560
  label: label$2("Commit")
542
- }).task({ handler({ data }) {
561
+ }).task({ handler({ data: { projectName } }) {
543
562
  botMessage({
544
- description: `Run \`cd ./${data.projectName}\` and Enjoy 🚀`,
563
+ description: `Run \`cd ./${projectName}\` and Enjoy 🚀`,
545
564
  title: "The project was successfully created",
546
565
  type: "success"
547
566
  });
548
567
  } });
549
568
  };
550
569
  const label$2 = (message) => `${message} 🔨`;
551
- /**
552
- * 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.
553
- * @param template - The selected template.
554
- * @param dataModel - Data model mapping the template expression key with its corresponding value.
555
- * @example
556
- * applyTemplate(
557
- * { toReplace: "value" },
558
- * );
559
- */
560
- const applyTemplate = (template, dataModel) => {
561
- const templateExtension = ".tmpl";
562
- const templateRootPath = resolveFromStackDirectory(join("./templates", template));
563
- const projectRootPath = resolveFromProjectDirectory("./");
564
- const templateExpressionRegExp = /{{(.*?)}}/g;
565
- const evaluate = (content) => {
566
- return content.replaceAll(templateExpressionRegExp, (_, key) => dataModel[key] || "");
567
- };
568
- /** Copy the template before mutations. */
569
- cpSync(templateRootPath, projectRootPath, {
570
+ const slugify = (input) => {
571
+ return input.toLowerCase().replaceAll(/[^a-z0-9\s-]/g, "").trim().replaceAll(/\s+/g, "-").replaceAll(/-+/g, "-");
572
+ };
573
+ const toCapitalLetter = (input) => {
574
+ return input.charAt(0).toUpperCase() + input.slice(1);
575
+ };
576
+ const createTemplateEngine = async (workingPath, { projectName, templateModel, templateName, templatePath }) => {
577
+ if (!existsSync(workingPath)) await mkdir(workingPath);
578
+ await cp(templatePath, workingPath, {
570
579
  force: true,
571
580
  recursive: true
572
581
  });
573
- /** Template file mutations. */
574
- new fdir().withBasePath().glob(`**/*${templateExtension}`).crawl(projectRootPath).sync().forEach((templateFilePath) => {
575
- const projectFilePath = templateFilePath.slice(0, templateFilePath.lastIndexOf(templateExtension));
576
- const content = evaluate(readFileSync(templateFilePath, "utf8"));
577
- renameSync(templateFilePath, projectFilePath);
578
- writeFileSync(projectFilePath, content, "utf8");
582
+ const gitignoreFile = join(workingPath, ".gitignore.tmpl");
583
+ if (existsSync(gitignoreFile)) await rename(gitignoreFile, join(workingPath, ".gitignore"));
584
+ const eslintConfigFile = join(workingPath, "eslint.config.js.tmpl");
585
+ if (existsSync(eslintConfigFile)) await rename(eslintConfigFile, join(workingPath, "eslint.config.js"));
586
+ const templateEntries = await getTemplateEntries(workingPath);
587
+ process.chdir(workingPath);
588
+ return {
589
+ async processContents() {
590
+ await Promise.all(templateEntries.filter(({ type }) => type === "content").map(async (entry) => {
591
+ return writeFile(entry.path, setTemplateVariables(entry, templateModel));
592
+ }));
593
+ },
594
+ async processPaths() {
595
+ const sortedDirectoryEntries = templateEntries.filter(({ content, type }) => {
596
+ if (type === "path.directory" || type === "path.file") return hasTemplateVariable(basename(content));
597
+ return false;
598
+ }).toSorted(({ path: pathA }, { path: pathB }) => pathB.length - pathA.length);
599
+ for (const entry of sortedDirectoryEntries) {
600
+ const newPath = setTemplateVariables(entry, templateModel);
601
+ await rename(entry.path, newPath);
602
+ }
603
+ await symlink(join(templateName === "single-project" ? projectName : join("libraries", projectName), "README.md"), "./README.md");
604
+ }
605
+ };
606
+ };
607
+ const getTemplateEntries = async (path) => {
608
+ const entries = await readdir(path, {
609
+ recursive: true,
610
+ withFileTypes: true
579
611
  });
580
- /** Template folder mutations. */
581
- new fdir().withBasePath().onlyDirs().filter((path) => {
582
- return templateExpressionRegExp.test(path);
583
- }).crawl(projectRootPath).sync().toSorted((a, b) => b.length - a.length).forEach((templateFolderPath) => {
584
- renameSync(templateFolderPath, templateFolderPath.replaceAll(templateExpressionRegExp, (_, dataModelKey) => {
585
- return dataModel[dataModelKey];
586
- }));
612
+ return (await Promise.all(entries.map(async (entry) => {
613
+ const isDirectory = entry.isDirectory();
614
+ if (!isDirectory && !entry.isFile()) return [];
615
+ const entryPath = resolve(entry.parentPath, entry.name);
616
+ return (isDirectory ? [{
617
+ content: entryPath,
618
+ type: "path.directory"
619
+ }] : [{
620
+ content: entryPath,
621
+ type: "path.file"
622
+ }, {
623
+ content: await readFile(entryPath, "utf8"),
624
+ type: "content"
625
+ }]).map(({ content, type }) => {
626
+ if (!hasTemplateVariable(content)) return void 0;
627
+ return {
628
+ content,
629
+ path: entryPath,
630
+ type
631
+ };
632
+ }).filter((input) => Boolean(input));
633
+ }))).flat();
634
+ };
635
+ const setTemplateVariables = (entry, model) => {
636
+ return entry.content.replaceAll(TEMPLATE_VARIABLE_MATCHER, (match, dataModelKey) => {
637
+ return model[dataModelKey] ?? match;
587
638
  });
588
639
  };
640
+ const TEMPLATE_VARIABLE_MATCHER = /* @__PURE__ */ new RegExp(/{{(.*?)}}/g, "gi");
641
+ const hasTemplateVariable = (input) => {
642
+ /**
643
+ * TemplateVariableMatcher.test() is not used since the `RegExp` is stateful when the global is
644
+ * used leading to some unstable results (relying on latest `lastIndex` set (lastIndex specifies
645
+ * the index at which to start the next match)). String.search is stateless.
646
+ *
647
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test MDN documentation}.
648
+ */
649
+ return input.search(TEMPLATE_VARIABLE_MATCHER) >= 0;
650
+ };
589
651
 
590
652
  //#endregion
591
- //#region src/commands/fix/fixFormatting.ts
592
- const PRETTIER_IGNORE_FILES = ["pnpm-lock.yaml"];
593
- const fixFormatting = async (files) => {
594
- let prettierFiles = [];
595
- if (files.length === 0) prettierFiles.push(`"**/!(${PRETTIER_IGNORE_FILES.join("|")})"`);
596
- else {
597
- prettierFiles = files.filter((file) => {
598
- return PRETTIER_IGNORE_FILES.every((filename) => !file.endsWith(filename));
599
- });
600
- if (prettierFiles.length === 0) return;
601
- }
602
- const arguments_ = [...prettierFiles];
603
- if (existsSync(resolveFromProjectDirectory(".gitignore"))) arguments_.push("--ignore-path .gitignore");
604
- arguments_.push("--write", "--ignore-unknown", "--no-error-on-unmatched-pattern");
605
- try {
606
- return await helpers.exec(`prettier ${arguments_.join(" ")}`);
607
- } catch (error) {
608
- throw createError("prettier", error);
609
- }
610
- };
653
+ //#region src/commands/fix/fixCode.ts
654
+ const fixCode = oxlint({ isFixMode: true });
611
655
 
612
656
  //#endregion
613
- //#region src/commands/fix/fixLinter.ts
614
- const fixLinter = eslint({ isFixMode: true });
657
+ //#region src/commands/fix/fixFormatting.ts
658
+ const fixFormatting = oxfmt({ isFixMode: true });
615
659
 
616
660
  //#endregion
617
661
  //#region src/commands/fix/fix.ts
@@ -631,14 +675,14 @@ const createFixCommand = (program) => {
631
675
  label: label$1("Prepare the project")
632
676
  }).task({
633
677
  async handler(_, argv) {
634
- await fixLinter(argv.operands);
678
+ await fixFormatting(argv.operands);
635
679
  },
636
- label: label$1("Fix linter issues")
680
+ label: label$1("Fix formatting issues")
637
681
  }).task({
638
682
  async handler(_, argv) {
639
- await fixFormatting(argv.operands);
683
+ await fixCode(argv.operands);
640
684
  },
641
- label: label$1("Fix formatting issues")
685
+ label: label$1("Fix code issues")
642
686
  });
643
687
  };
644
688
  const label$1 = (message) => `${message} 🚑`;
@@ -663,7 +707,7 @@ const createInstallCommand = (program) => {
663
707
  };
664
708
  const label = (message) => `${message} 📲`;
665
709
  const installGitHook = async (hook, content) => {
666
- const filename = resolveFromProjectDirectory(`.git/hooks/${hook}`);
710
+ const filename = resolveFromWorkingDirectory(`.git/hooks/${hook}`);
667
711
  await writeFile(filename, content);
668
712
  return chmod(filename, "0755");
669
713
  };