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