@adbayb/stack 2.39.0 → 2.40.0-next-7baa8ed

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 +1 -1
  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 +28 -0
  6. package/configs/typescript/index.json +1 -7
  7. package/dist/index.js +204 -162
  8. package/package.json +26 -44
  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} +2 -0
  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} +2 -0
  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,15 +84,14 @@ 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
96
  const request = { async get(url, responseType) {
96
97
  const response = await fetch(url);
@@ -100,26 +101,30 @@ const request = { async get(url, responseType) {
100
101
  })})`);
101
102
  return response[responseType === "text" ? "text" : "json"]();
102
103
  } };
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
- }
104
+ const oxlint = (options) => async (files = []) => {
112
105
  const arguments_ = [
113
- ...eslintFiles,
114
- "--cache",
115
- `--cache-location ${resolveFromProjectDirectory("node_modules/.cache/.eslintcache")}`,
106
+ ...files,
107
+ "--disable-nested-config",
116
108
  "--no-error-on-unmatched-pattern"
117
109
  ];
118
110
  if (options.isFixMode) arguments_.push("--fix");
119
111
  try {
120
- return await helpers.exec(`eslint ${arguments_.join(" ")}`);
112
+ return await helpers.exec(`oxlint ${arguments_.join(" ")}`);
113
+ } catch (error) {
114
+ throw createError("oxlint", error);
115
+ }
116
+ };
117
+ const oxfmt = (options) => async (files = []) => {
118
+ const arguments_ = [
119
+ ...files,
120
+ "--disable-nested-config",
121
+ "--no-error-on-unmatched-pattern",
122
+ options.isFixMode ? "--write" : "--check"
123
+ ];
124
+ try {
125
+ return await helpers.exec(`oxfmt ${arguments_.join(" ")}`);
121
126
  } catch (error) {
122
- throw createError("eslint", error);
127
+ throw createError("oxfmt", error);
123
128
  }
124
129
  };
125
130
  const turbo = async (command, options = {}) => {
@@ -159,18 +164,6 @@ const changeset = async (command) => {
159
164
  throw createError("changeset", error);
160
165
  }
161
166
  };
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
167
 
175
168
  //#endregion
176
169
  //#region src/commands/build.ts
@@ -185,7 +178,7 @@ const createBuildCommand = (program) => {
185
178
 
186
179
  //#endregion
187
180
  //#region src/commands/check/checkCode.ts
188
- const checkCode = eslint({ isFixMode: false });
181
+ const checkCode = oxlint({ isFixMode: false });
189
182
 
190
183
  //#endregion
191
184
  //#region src/commands/check/checkCommit.ts
@@ -294,14 +287,8 @@ const isExcluded = (version) => {
294
287
  const hasCaret = (version) => version.startsWith("^");
295
288
 
296
289
  //#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
- };
290
+ //#region src/commands/check/checkFormatting.ts
291
+ const checkFormatting = oxfmt({ isFixMode: false });
305
292
 
306
293
  //#endregion
307
294
  //#region src/commands/check/check.ts
@@ -309,7 +296,7 @@ const ONLY_VALUES = [
309
296
  "commit",
310
297
  "code",
311
298
  "dependency",
312
- "type"
299
+ "formatting"
313
300
  ];
314
301
  const createCheckCommand = (program) => {
315
302
  program.command({
@@ -342,6 +329,13 @@ const createCheckCommand = (program) => {
342
329
  },
343
330
  label: label$4("Check dependency compliance"),
344
331
  skip: ifFilterDefinedAndNotEqualTo("dependency")
332
+ }).task({
333
+ async handler(_, argv) {
334
+ const filenames = argv.operands;
335
+ await checkFormatting(filenames);
336
+ },
337
+ label: label$4("Check formatting compliance"),
338
+ skip: ifFilterDefinedAndNotEqualTo("formatting")
345
339
  }).task({
346
340
  async handler(_, argv) {
347
341
  const filenames = argv.operands;
@@ -349,14 +343,6 @@ const createCheckCommand = (program) => {
349
343
  },
350
344
  label: label$4("Check code compliance"),
351
345
  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
346
  }).task({
361
347
  async handler() {
362
348
  await checkCommit();
@@ -382,7 +368,7 @@ const createCleanCommand = (program) => {
382
368
  async handler() {
383
369
  const cachePath = "node_modules/.cache";
384
370
  const files = await retrieveIgnoredFiles();
385
- if (isDirectoryExistAndNotEmpty(resolveFromProjectDirectory(cachePath))) files.push(cachePath);
371
+ if (isDirectoryExistAndNotEmpty(resolveFromWorkingDirectory(cachePath))) files.push(cachePath);
386
372
  return files;
387
373
  },
388
374
  key: "files",
@@ -428,7 +414,7 @@ const PRESERVE_FILES = ["node_modules"];
428
414
 
429
415
  //#endregion
430
416
  //#region package.json
431
- var version = "2.39.0";
417
+ var version = "2.40.0-next-7baa8ed";
432
418
 
433
419
  //#endregion
434
420
  //#region src/commands/create.ts
@@ -439,12 +425,12 @@ const createCreateCommand = (program) => {
439
425
  }).task({ handler() {
440
426
  botMessage({
441
427
  description: "I can guarantee you a project creation in under 1 minute 🚀",
442
- title: `I'm Stack v${version}, your bot assistant`,
428
+ title: `I'm Stack v${version} 👋`,
443
429
  type: "information"
444
430
  });
445
431
  } }).task({
446
432
  async handler() {
447
- await getNpmVersion();
433
+ await getPnpmVersion();
448
434
  },
449
435
  label: label$2("Check pre-requisites")
450
436
  }).input({
@@ -467,61 +453,92 @@ const createCreateCommand = (program) => {
467
453
  options: ["single-project", "multi-projects"],
468
454
  type: "select"
469
455
  }).task({
470
- async handler({ inputDescription, inputName, inputUrl }) {
456
+ async handler({ inputDescription, inputName, inputTemplate, inputUrl }) {
471
457
  if (!inputName) throw createError("stack create", "The project name is not optional. Make sure to provide a valid value (non-empty string).");
472
458
  const { repoName, repoOwner } = (inputUrl.startsWith("git") ? /^git@.*:(?<repoOwner>.*)\/(?<repoName>.*)\.git$/ : /^https?:\/\/.*\/(?<repoOwner>.*)\/(?<repoName>.*)\.git$/).exec(inputUrl)?.groups ?? {};
473
459
  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
460
  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");
461
+ const { version: pnpmVersion } = await request.get("https://registry.npmjs.org/pnpm/latest", "json");
462
+ const projectName = slugify(inputName);
476
463
  return {
477
464
  licenseYear: (/* @__PURE__ */ new Date()).getFullYear().toString(),
478
465
  nodeVersion: nodeVersion.replace("v", ""),
479
- npmVersion: String(npmVersion),
480
- projectDescription: inputDescription.charAt(0).toUpperCase() + inputDescription.slice(1),
481
- projectName: inputName.toLowerCase(),
466
+ pnpmVersion: String(pnpmVersion),
467
+ projectDescription: toCapitalLetter(inputDescription),
468
+ projectName,
482
469
  projectUrl: inputUrl,
483
- repoId: `${repoOwner}/${repoName}`
470
+ repoId: `${repoOwner}/${repoName}`,
471
+ templatePath: resolveFromPackageDirectory("templates", inputTemplate),
472
+ workingPath: resolveFromWorkingDirectory(projectName)
484
473
  };
485
474
  },
486
475
  key: "data",
487
476
  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);
477
+ }).input({
478
+ defaultValue: true,
479
+ key: "canRemoveExistingDirectoryInput",
480
+ label({ data: { projectName } }) {
481
+ return label$2(`\`${projectName}\` directory already exists, do you want to remove it?`);
493
482
  },
494
- label({ data }) {
495
- return label$2(`Create \`${data.projectName}\` folder`);
483
+ skip({ data: { workingPath } }) {
484
+ return !existsSync(workingPath);
485
+ },
486
+ type: "confirm",
487
+ validate({ canRemoveExistingDirectoryInput, data: { projectName } }) {
488
+ if (canRemoveExistingDirectoryInput) return;
489
+ return createError("mkdir", `Remove or rename the \`${projectName}\` existing directory to apply the template from a clean state.`);
496
490
  }
497
491
  }).task({
498
- async handler({ data }) {
499
- await helpers.exec("git init");
500
- await helpers.exec(`git remote add origin ${data.projectUrl}`);
492
+ async handler({ canRemoveExistingDirectoryInput, data: { licenseYear, nodeVersion, pnpmVersion, projectDescription, projectName, projectUrl, repoId, templatePath, workingPath }, inputTemplate }) {
493
+ if (canRemoveExistingDirectoryInput) await rm(workingPath, {
494
+ force: true,
495
+ recursive: true
496
+ });
497
+ return createTemplateEngine(workingPath, {
498
+ projectName,
499
+ templateModel: {
500
+ licenseYear,
501
+ nodeVersion,
502
+ pnpmVersion,
503
+ projectDescription,
504
+ projectName,
505
+ projectUrl,
506
+ repoId
507
+ },
508
+ templateName: inputTemplate,
509
+ templatePath
510
+ });
501
511
  },
502
- label: label$2("Initialize `git`")
512
+ key: "templateEngine",
513
+ label({ data: { projectName }, inputTemplate }) {
514
+ return label$2(`Copy \`${inputTemplate}\` template to \`${projectName}\` directory`);
515
+ }
503
516
  }).task({
504
- handler({ data, inputTemplate }) {
505
- applyTemplate(inputTemplate, data);
517
+ async handler({ templateEngine }) {
518
+ await templateEngine.processContents();
519
+ await templateEngine.processPaths();
506
520
  },
507
- label: label$2("Apply template")
521
+ label() {
522
+ return label$2("Process template");
523
+ }
508
524
  }).task({
509
- async handler({ data: { projectName }, inputTemplate }) {
510
- await symlink(join(inputTemplate === "single-project" ? projectName : join("libraries", projectName), "README.md"), "./README.md");
525
+ async handler({ data: { projectUrl } }) {
526
+ await helpers.exec("git init");
527
+ await helpers.exec(`git remote add origin ${projectUrl}`);
511
528
  },
512
- label: label$2("Create a symlink to `README.md` file")
529
+ label: label$2("Initialize `git`")
513
530
  }).task({
514
531
  async handler() {
515
532
  await setPackageManager();
516
533
  },
517
534
  label: label$2("Set up the package manager")
518
535
  }).task({
519
- async handler({ data }) {
536
+ async handler({ data: { projectName } }) {
520
537
  const localDevelopmentDependencies = ["quickbundle", "vitest"];
521
538
  const globalDevelopmentDependencies = ["@adbayb/stack"];
522
539
  try {
523
540
  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}`);
541
+ await helpers.exec(`pnpm add ${localDevelopmentDependencies.join(" ")} --save-dev --filter ${projectName}`);
525
542
  await helpers.exec("pnpm install");
526
543
  } catch (error) {
527
544
  throw createError("pnpm", error);
@@ -539,79 +556,104 @@ const createCreateCommand = (program) => {
539
556
  await helpers.exec("git commit -m \"chore: initial commit\"");
540
557
  },
541
558
  label: label$2("Commit")
542
- }).task({ handler({ data }) {
559
+ }).task({ handler({ data: { projectName } }) {
543
560
  botMessage({
544
- description: `Run \`cd ./${data.projectName}\` and Enjoy 🚀`,
561
+ description: `Run \`cd ./${projectName}\` and Enjoy 🚀`,
545
562
  title: "The project was successfully created",
546
563
  type: "success"
547
564
  });
548
565
  } });
549
566
  };
550
567
  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, {
568
+ const slugify = (input) => {
569
+ return input.toLowerCase().replaceAll(/[^a-z0-9\s-]/g, "").trim().replaceAll(/\s+/g, "-").replaceAll(/-+/g, "-");
570
+ };
571
+ const toCapitalLetter = (input) => {
572
+ return input.charAt(0).toUpperCase() + input.slice(1);
573
+ };
574
+ const createTemplateEngine = async (workingPath, { projectName, templateModel, templateName, templatePath }) => {
575
+ if (!existsSync(workingPath)) await mkdir(workingPath);
576
+ await cp(templatePath, workingPath, {
570
577
  force: true,
571
578
  recursive: true
572
579
  });
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");
580
+ const gitignoreFile = join(workingPath, ".gitignore.tmpl");
581
+ if (existsSync(gitignoreFile)) await rename(gitignoreFile, join(workingPath, ".gitignore"));
582
+ const eslintConfigFile = join(workingPath, "eslint.config.js.tmpl");
583
+ if (existsSync(eslintConfigFile)) await rename(eslintConfigFile, join(workingPath, "eslint.config.js"));
584
+ const templateEntries = await getTemplateEntries(workingPath);
585
+ process.chdir(workingPath);
586
+ return {
587
+ async processContents() {
588
+ await Promise.all(templateEntries.filter(({ type }) => type === "content").map(async (entry) => {
589
+ return writeFile(entry.path, setTemplateVariables(entry, templateModel));
590
+ }));
591
+ },
592
+ async processPaths() {
593
+ const sortedDirectoryEntries = templateEntries.filter(({ content, type }) => {
594
+ if (type === "path.directory" || type === "path.file") return hasTemplateVariable(basename(content));
595
+ return false;
596
+ }).toSorted(({ path: pathA }, { path: pathB }) => pathB.length - pathA.length);
597
+ for (const entry of sortedDirectoryEntries) {
598
+ const newPath = setTemplateVariables(entry, templateModel);
599
+ await rename(entry.path, newPath);
600
+ }
601
+ await symlink(join(templateName === "single-project" ? projectName : join("libraries", projectName), "README.md"), "./README.md");
602
+ }
603
+ };
604
+ };
605
+ const getTemplateEntries = async (path) => {
606
+ const entries = await readdir(path, {
607
+ recursive: true,
608
+ withFileTypes: true
579
609
  });
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
- }));
610
+ return (await Promise.all(entries.map(async (entry) => {
611
+ const isDirectory = entry.isDirectory();
612
+ if (!isDirectory && !entry.isFile()) return [];
613
+ const entryPath = resolve(entry.parentPath, entry.name);
614
+ return (isDirectory ? [{
615
+ content: entryPath,
616
+ type: "path.directory"
617
+ }] : [{
618
+ content: entryPath,
619
+ type: "path.file"
620
+ }, {
621
+ content: await readFile(entryPath, "utf8"),
622
+ type: "content"
623
+ }]).map(({ content, type }) => {
624
+ if (!hasTemplateVariable(content)) return void 0;
625
+ return {
626
+ content,
627
+ path: entryPath,
628
+ type
629
+ };
630
+ }).filter((input) => Boolean(input));
631
+ }))).flat();
632
+ };
633
+ const setTemplateVariables = (entry, model) => {
634
+ return entry.content.replaceAll(TEMPLATE_VARIABLE_MATCHER, (match, dataModelKey) => {
635
+ return model[dataModelKey] ?? match;
587
636
  });
588
637
  };
638
+ const TEMPLATE_VARIABLE_MATCHER = /* @__PURE__ */ new RegExp(/{{(.*?)}}/g, "gi");
639
+ const hasTemplateVariable = (input) => {
640
+ /**
641
+ * TemplateVariableMatcher.test() is not used since the `RegExp` is stateful when the global is
642
+ * used leading to some unstable results (relying on latest `lastIndex` set (lastIndex specifies
643
+ * the index at which to start the next match)). String.search is stateless.
644
+ *
645
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test MDN documentation}.
646
+ */
647
+ return input.search(TEMPLATE_VARIABLE_MATCHER) >= 0;
648
+ };
589
649
 
590
650
  //#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
- };
651
+ //#region src/commands/fix/fixCode.ts
652
+ const fixCode = oxlint({ isFixMode: true });
611
653
 
612
654
  //#endregion
613
- //#region src/commands/fix/fixLinter.ts
614
- const fixLinter = eslint({ isFixMode: true });
655
+ //#region src/commands/fix/fixFormatting.ts
656
+ const fixFormatting = oxfmt({ isFixMode: true });
615
657
 
616
658
  //#endregion
617
659
  //#region src/commands/fix/fix.ts
@@ -631,14 +673,14 @@ const createFixCommand = (program) => {
631
673
  label: label$1("Prepare the project")
632
674
  }).task({
633
675
  async handler(_, argv) {
634
- await fixLinter(argv.operands);
676
+ await fixFormatting(argv.operands);
635
677
  },
636
- label: label$1("Fix linter issues")
678
+ label: label$1("Fix formatting issues")
637
679
  }).task({
638
680
  async handler(_, argv) {
639
- await fixFormatting(argv.operands);
681
+ await fixCode(argv.operands);
640
682
  },
641
- label: label$1("Fix formatting issues")
683
+ label: label$1("Fix code issues")
642
684
  });
643
685
  };
644
686
  const label$1 = (message) => `${message} 🚑`;
@@ -663,7 +705,7 @@ const createInstallCommand = (program) => {
663
705
  };
664
706
  const label = (message) => `${message} 📲`;
665
707
  const installGitHook = async (hook, content) => {
666
- const filename = resolveFromProjectDirectory(`.git/hooks/${hook}`);
708
+ const filename = resolveFromWorkingDirectory(`.git/hooks/${hook}`);
667
709
  await writeFile(filename, content);
668
710
  return chmod(filename, "0755");
669
711
  };
package/package.json CHANGED
@@ -1,33 +1,23 @@
1
1
  {
2
2
  "name": "@adbayb/stack",
3
- "version": "2.39.0",
3
+ "version": "2.40.0-next-7baa8ed",
4
4
  "description": "My opinionated JavaScript-based toolchain",
5
5
  "keywords": [
6
- "stack",
6
+ "development",
7
7
  "scripts",
8
- "toolchain",
9
- "development"
8
+ "stack",
9
+ "toolchain"
10
10
  ],
11
- "repository": {
12
- "type": "git",
13
- "url": "https://github.com/adbayb/stack.git",
14
- "directory": "applications/stack"
15
- },
16
11
  "license": "MIT",
17
12
  "author": {
18
13
  "name": "Ayoub Adib",
19
14
  "email": "adbayb@gmail.com",
20
15
  "url": "https://twitter.com/adbayb"
21
16
  },
22
- "type": "module",
23
- "exports": {
24
- ".": {
25
- "source": "./src/index.ts",
26
- "default": "./dist/index.js"
27
- },
28
- "./eslint": "./configs/eslint/index.js",
29
- "./prettier": "./configs/prettier/index.js",
30
- "./typescript": "./configs/typescript/index.json"
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/adbayb/stack.git",
20
+ "directory": "applications/stack"
31
21
  },
32
22
  "bin": {
33
23
  "stack": "./bin/index.js"
@@ -38,44 +28,36 @@
38
28
  "dist",
39
29
  "templates"
40
30
  ],
31
+ "type": "module",
32
+ "exports": {
33
+ ".": {
34
+ "source": "./src/index.ts",
35
+ "default": "./dist/index.js"
36
+ },
37
+ "./oxfmt": "./configs/oxfmt/index.ts",
38
+ "./oxlint": "./configs/oxlint/index.ts",
39
+ "./typescript": "./configs/typescript/index.json"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public",
43
+ "registry": "https://registry.npmjs.org"
44
+ },
41
45
  "dependencies": {
42
46
  "@changesets/changelog-github": "^0.7.0",
43
47
  "@changesets/cli": "^2.31.0",
44
48
  "@commitlint/cli": "^21.0.2",
45
49
  "@commitlint/config-conventional": "^21.0.2",
46
- "@eslint-react/eslint-plugin": "^5.9.1",
47
- "@eslint/compat": "^2.1.0",
48
- "@stylistic/eslint-plugin": "^5.10.0",
49
- "@vitest/eslint-plugin": "^1.6.20",
50
- "eslint": "^10.5.0",
51
- "eslint-config-prettier": "^10.1.8",
52
- "eslint-import-resolver-typescript": "^4.4.5",
53
- "eslint-plugin-depend": "^1.5.0",
54
- "eslint-plugin-import-x": "^4.16.2",
55
- "eslint-plugin-jsdoc": "^63.0.6",
56
- "eslint-plugin-mdx": "^3.8.1",
57
- "eslint-plugin-n": "^18.1.0",
58
- "eslint-plugin-perfectionist": "^5.9.1",
59
- "eslint-plugin-prettier": "^5.5.6",
60
- "eslint-plugin-sonarjs": "^4.1.0",
61
- "eslint-plugin-unicorn": "^68.0.0",
62
- "fdir": "^6.5.0",
63
- "globals": "^17.6.0",
64
- "prettier": "^3.8.4",
65
- "prettier-plugin-packagejson": "^3.0.2",
50
+ "oxfmt": "^0.57.0",
51
+ "oxlint": "^1.72.0",
52
+ "oxlint-tsgolint": "^0.24.0",
66
53
  "termost": "^1.9.1",
67
54
  "turbo": "^2.9.18",
68
- "typescript": "^6.0.3",
69
- "typescript-eslint": "^8.61.1"
55
+ "typescript": "^6.0.3"
70
56
  },
71
57
  "devDependencies": {
72
58
  "@types/node": "24.13.2",
73
59
  "quickbundle": "3.0.0"
74
60
  },
75
- "publishConfig": {
76
- "access": "public",
77
- "registry": "https://registry.npmjs.org"
78
- },
79
61
  "scripts": {
80
62
  "build": "quickbundle build",
81
63
  "prestart": "pnpm build",