@fernir2/saas-kit-cli 0.1.39 → 0.1.41-1096

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 (286) hide show
  1. package/base-repo/app-constants/alias-symbols.js +2 -2
  2. package/base-repo/app-constants/aliases.js +3 -1
  3. package/base-repo/app-constants/app-packages-names.js +4 -0
  4. package/base-repo/app-constants/command-names.js +4 -0
  5. package/base-repo/app-constants/internal-system-constants.js +8 -0
  6. package/base-repo/app-constants/project-paths.js +6 -3
  7. package/base-repo/constants/basic-types.js +4 -0
  8. package/base-repo/constants/ci-constants.js +7 -0
  9. package/base-repo/constants/http-constants.js +4 -0
  10. package/base-repo/constants/http-methods.js +6 -0
  11. package/base-repo/constants/internal-cli-constants.js +6 -0
  12. package/base-repo/constants/internal-common-strings.js +4 -0
  13. package/base-repo/constants/internal-extensions.js +6 -0
  14. package/base-repo/constants/packages.js +1 -1
  15. package/base-repo/constants/push-statuses.js +4 -0
  16. package/base-repo/constants/strings-constants.js +4 -0
  17. package/base-repo/constants/type-string-values.js +4 -0
  18. package/base-repo/process.js +4 -0
  19. package/cli/.env.example +8 -61
  20. package/cli/.prettierignore +15 -0
  21. package/cli/.prettierrc.json +8 -0
  22. package/cli/README.md +3 -3
  23. package/cli/bin/create.ts +635 -450
  24. package/cli/configs/drizzle-cli-config.ts +16 -15
  25. package/cli/configs/next-cli-config.ts +44 -78
  26. package/cli/configs/playwright-cli-config.ts +45 -35
  27. package/cli/configs/tsconfig.cli.json +35 -35
  28. package/cli/configs/tsconfig.server.json +13 -13
  29. package/cli/drizzle.config.ts +6 -6
  30. package/cli/next.config.js +5 -3
  31. package/cli/npm-commands/gen-meta.ts +9 -3
  32. package/cli/npm-commands/gen-schema.ts +3 -3
  33. package/cli/npm-commands/migrate-db.ts +17 -15
  34. package/cli/npm-commands/seed-db.ts +17 -15
  35. package/cli/package-template.json +54 -60
  36. package/cli/playwright.config.ts +6 -6
  37. package/cli/postcss.config.mjs +7 -7
  38. package/cli/public/handle.svg +23 -0
  39. package/cli/public/images/login-image-dark.webp +0 -0
  40. package/cli/public/images/login-image.webp +0 -0
  41. package/cli/public/images/logo.webp +0 -0
  42. package/cli/public/images/no-image.webp +0 -0
  43. package/cli/public/images/profile.webp +0 -0
  44. package/cli/public/images/search-not-found-result.webp +0 -0
  45. package/cli/public/logo.svg +1 -0
  46. package/cli/server.ts +48 -40
  47. package/cli/src/app/api/v1/(f)/[resourceName]/[id]/route.ts +18 -11
  48. package/cli/src/app/api/v1/(f)/[resourceName]/route.ts +23 -14
  49. package/cli/src/app/api/v1/(f)/[resourceName]/upsert/route.ts +6 -3
  50. package/cli/src/app/api/v1/(f)/blob/route.ts +14 -7
  51. package/cli/src/app/api/v1/(f)/log/route.ts +14 -7
  52. package/cli/src/app/api/v1/(f)/markdown/route.ts +6 -0
  53. package/cli/src/app/api/v1/(f)/other-user/[id]/route.ts +23 -14
  54. package/cli/src/app/api/v1/(f)/other-user/route.ts +14 -7
  55. package/cli/src/app/api/v1/(f)/password/forgot-password/route.ts +6 -3
  56. package/cli/src/app/api/v1/(f)/password/reset-password/route.ts +6 -3
  57. package/cli/src/app/api/v1/(f)/payment/method/route.ts +9 -4
  58. package/cli/src/app/api/v1/(f)/payment/route.ts +6 -3
  59. package/cli/src/app/api/v1/(f)/payment/verify-fail/route.ts +6 -3
  60. package/cli/src/app/api/v1/(f)/payment/verify-success/route.ts +6 -3
  61. package/cli/src/app/api/v1/(f)/search-result/route.ts +6 -3
  62. package/cli/src/app/api/v1/(f)/searchable-resources/route.ts +6 -3
  63. package/cli/src/app/api/v1/(f)/sign-in/route.ts +6 -3
  64. package/cli/src/app/api/v1/(f)/sign-out/route.ts +6 -3
  65. package/cli/src/app/api/v1/(f)/sign-up/route.ts +6 -3
  66. package/cli/src/app/api/v1/(f)/subscription/cancel/route.ts +6 -3
  67. package/cli/src/app/api/v1/(f)/subscription/create/route.ts +6 -3
  68. package/cli/src/app/api/v1/(f)/subscription/update/route.ts +6 -3
  69. package/cli/src/app/api/v1/(f)/uimeta/route.ts +6 -3
  70. package/cli/src/app/api/v1/(f)/uimetas/route.ts +6 -3
  71. package/cli/src/app/api/v1/(f)/user-feature/isenabled/route.ts +6 -3
  72. package/cli/src/app/api/v1/(f)/user-permission/route.ts +6 -3
  73. package/cli/src/app/api/v1/(f)/visible-workspace/route.ts +10 -5
  74. package/cli/src/app/api/v1/(f)/workspace/change/route.ts +10 -5
  75. package/cli/src/app/f/(not-signed-in)/edit-password/page.tsx +8 -8
  76. package/cli/src/app/f/(not-signed-in)/forgot-password/page.tsx +13 -13
  77. package/cli/src/app/f/(not-signed-in)/reset-password/page.tsx +11 -11
  78. package/cli/src/app/f/(not-signed-in)/sign-in/microsoft/page.tsx +8 -8
  79. package/cli/src/app/f/(not-signed-in)/sign-in/page.tsx +13 -13
  80. package/cli/src/app/f/(not-signed-in)/sign-up/page.tsx +13 -13
  81. package/cli/src/app/f/(signed-in)/dashboard/page.tsx +8 -8
  82. package/cli/src/app/f/(signed-in)/dynamiclayout/page.tsx +8 -8
  83. package/cli/src/app/f/(signed-in)/edituser/[id]/page.tsx +8 -8
  84. package/cli/src/app/f/(signed-in)/edituser/page.tsx +8 -8
  85. package/cli/src/app/f/(signed-in)/layout.tsx +5 -5
  86. package/cli/src/app/f/(signed-in)/payment-plans/page.tsx +8 -8
  87. package/cli/src/app/f/(signed-in)/statusboard/page.tsx +8 -8
  88. package/cli/src/app/f/(signed-in)/userlist/page.tsx +8 -8
  89. package/cli/src/app/f/(signed-in)/view/page.tsx +23 -9
  90. package/cli/src/app/f/api-docs/page.tsx +15 -15
  91. package/cli/src/app/globals.css +1 -1
  92. package/cli/src/app/http-wrappers.ts +23 -0
  93. package/cli/src/app/init-saas-kit.ts +12 -0
  94. package/cli/src/app/layout.tsx +43 -37
  95. package/cli/src/app/page.tsx +9 -9
  96. package/cli/src/app/styles/common.css +75 -71
  97. package/cli/src/app/styles/rich-text-editor.css +130 -130
  98. package/cli/templates/.env-template +8 -0
  99. package/cli/templates/gitignore-template +54 -0
  100. package/cli/test/custom-test.ts +20 -0
  101. package/cli/test/global-setup.ts +7 -3
  102. package/cli/test/global-teardown.ts +7 -0
  103. package/cli/tsconfig.json +15 -15
  104. package/cli/tsconfig.lint.json +5 -0
  105. package/cli/tsconfig.server.json +4 -14
  106. package/fd-toolbox/api/api-client.js +3 -3
  107. package/fd-toolbox/api/api-path-names.js +4 -1
  108. package/fd-toolbox/api/api-paths.js +2 -1
  109. package/fd-toolbox/api/base-api.js +5 -4
  110. package/fd-toolbox/auth/{login-states.js → internal-login-states.js} +3 -3
  111. package/fd-toolbox/auth/session-storage.js +1 -1
  112. package/fd-toolbox/auth/tokens.js +1 -1
  113. package/fd-toolbox/constants/common-user-fields.js +7 -0
  114. package/fd-toolbox/constants/constants.js +2 -2
  115. package/fd-toolbox/constants/environment-constants.js +4 -1
  116. package/fd-toolbox/constants/header-names.js +1 -1
  117. package/fd-toolbox/constants/meta-query-params.js +4 -1
  118. package/fd-toolbox/constants/odata-query-params.js +7 -0
  119. package/fd-toolbox/constants/public-files.js +5 -3
  120. package/fd-toolbox/constants/resource-folders.js +4 -0
  121. package/fd-toolbox/constants/toolbox-error-messages-constants.js +4 -0
  122. package/fd-toolbox/enums/enums.js +5 -2
  123. package/fd-toolbox/errors/error-handler.js +2 -2
  124. package/fd-toolbox/errors/error-statuses.js +4 -1
  125. package/fd-toolbox/errors/errors.js +1 -1
  126. package/fd-toolbox/errors/problem-details.js +4 -2
  127. package/fd-toolbox/functions/value-checking-functions.js +5 -3
  128. package/fd-toolbox/http/url/urls.js +4 -3
  129. package/fd-toolbox/infra/env-config.js +2 -2
  130. package/fd-toolbox/infra/env-functions.js +6 -4
  131. package/fd-toolbox/infra/env-schema.js +2 -4
  132. package/fd-toolbox/infra/toolbox-env-setting-keys.js +1 -1
  133. package/fd-toolbox/lib/environments.js +4 -3
  134. package/fd-toolbox/lib/utils.js +5 -2
  135. package/fd-toolbox/local-storage/local-storage.js +1 -1
  136. package/fd-toolbox/logging/loggers.js +2 -2
  137. package/fd-toolbox/notifications.js +2 -2
  138. package/fd-toolbox/odata/odata-filter-constants.js +4 -0
  139. package/fd-toolbox/odata/odata-formatting/odata-filters.js +9 -0
  140. package/fd-toolbox/odata/odata.js +12 -0
  141. package/fd-toolbox/paths/paths-names.js +1 -1
  142. package/fd-toolbox/redirect/redirect-functions.js +8 -0
  143. package/fd-toolbox/resources/resource-names.js +1 -1
  144. package/fd-toolbox/routing/login-routers.js +2 -1
  145. package/fd-toolbox/routing/paths.js +1 -1
  146. package/fd-toolbox/routing/routers.js +9 -0
  147. package/fd-toolbox/routing/routes.js +3 -2
  148. package/fd-toolbox/server/collections/single-funcs.js +8 -0
  149. package/fd-toolbox/server/constants/api-routes-constants.js +4 -0
  150. package/fd-toolbox/server/errors/error-dtos.js +4 -0
  151. package/fd-toolbox/server/framework/index.js +8 -0
  152. package/fd-toolbox/server/logging/latest-logs-store.js +6 -0
  153. package/fd-toolbox/server/logging/log-dtos.js +6 -0
  154. package/fd-toolbox/server/logging/logger.js +9 -0
  155. package/fd-toolbox/server/web/response-messages.js +4 -0
  156. package/fd-toolbox/strings/strings.js +2 -3
  157. package/fd-toolbox/types/ensure-type.js +6 -2
  158. package/fd-toolbox/url/urls.js +4 -0
  159. package/fd-toolbox-core/constants/meta-constants.js +4 -2
  160. package/fd-toolbox-core/core/name-of.js +1 -1
  161. package/fd-toolbox-core/types/resource-with-id.js +3 -1
  162. package/git/constants/git-constants.js +6 -0
  163. package/level2/cli/bin/index.js +2 -1
  164. package/level2/cli/create/bin/create.js +19 -10
  165. package/level2/npm-commands/build-npm/cli-contents.js +1 -1
  166. package/level2/npm-commands/build-npm/paths.js +6 -0
  167. package/package.json +48 -55
  168. package/base-repo/app-constants/alias-symbols.cjs.js +0 -6
  169. package/base-repo/app-constants/aliases.cjs.js +0 -8
  170. package/base-repo/app-constants/project-paths.cjs.js +0 -17
  171. package/base-repo/constants/create-app-constants.cjs.js +0 -9
  172. package/base-repo/constants/packages.cjs.js +0 -6
  173. package/base-repo/constants/packages.cjs2.js +0 -6
  174. package/base-repo/constants/packages2.js +0 -4
  175. package/cli/public/images/00000000-0000-0000-0000-000000000000.webp +0 -0
  176. package/cli/public/images/login-image-dark.jpg +0 -0
  177. package/cli/public/images/login-image.jpg +0 -0
  178. package/cli/public/images/no-image.png +0 -0
  179. package/cli/public/images/profile.png +0 -0
  180. package/cli/public/images/search-not-found-result.png +0 -0
  181. package/cli/public/images/toolbar-logo.png +0 -0
  182. package/cli/public/images/users/00000000-0000-0000-0000-000000000000.webp +0 -0
  183. package/cli/src/app/api/v1/(f)/preload/route.ts +0 -3
  184. package/cli/src/app/f/(signed-in)/lm/page.tsx +0 -8
  185. package/cli/src/app/f/(signed-in)/preload/page.tsx +0 -8
  186. package/cli/src/app/f/test/feed/page.tsx +0 -8
  187. package/cli/src/app/f/test/file-upload/page.tsx +0 -8
  188. package/cli/src/app/f/test/layout.tsx +0 -5
  189. package/cli/src/app/f/test/page.tsx +0 -8
  190. package/fd-toolbox/api/api-client.cjs.js +0 -19
  191. package/fd-toolbox/api/api-path-names.cjs.js +0 -9
  192. package/fd-toolbox/api/api-paths.cjs.js +0 -9
  193. package/fd-toolbox/api/base-api.cjs.js +0 -22
  194. package/fd-toolbox/auth/login-states.cjs.js +0 -18
  195. package/fd-toolbox/auth/session-storage.cjs.js +0 -12
  196. package/fd-toolbox/auth/tokens.cjs.js +0 -11
  197. package/fd-toolbox/constants/api-constants.cjs.js +0 -26
  198. package/fd-toolbox/constants/api-constants.js +0 -4
  199. package/fd-toolbox/constants/constants.cjs.js +0 -13
  200. package/fd-toolbox/constants/environment-constants.cjs.js +0 -7
  201. package/fd-toolbox/constants/extensions.cjs.js +0 -6
  202. package/fd-toolbox/constants/extensions.js +0 -4
  203. package/fd-toolbox/constants/header-names.cjs.js +0 -7
  204. package/fd-toolbox/constants/http-status-codes.cjs.js +0 -6
  205. package/fd-toolbox/constants/meta-query-params.cjs.js +0 -6
  206. package/fd-toolbox/constants/public-files.cjs.js +0 -14
  207. package/fd-toolbox/constants/representations.cjs.js +0 -6
  208. package/fd-toolbox/constants/representations.js +0 -4
  209. package/fd-toolbox/enums/enums.cjs.js +0 -26
  210. package/fd-toolbox/errors/error-handler.cjs.js +0 -10
  211. package/fd-toolbox/errors/error-statuses.cjs.js +0 -6
  212. package/fd-toolbox/errors/errors.cjs.js +0 -6
  213. package/fd-toolbox/errors/problem-details.cjs.js +0 -8
  214. package/fd-toolbox/functions/value-checking-functions.cjs.js +0 -10
  215. package/fd-toolbox/http/http-constants.cjs.js +0 -7
  216. package/fd-toolbox/http/http-constants.js +0 -4
  217. package/fd-toolbox/http/url/urls.cjs.js +0 -9
  218. package/fd-toolbox/infra/env-config.cjs.js +0 -8
  219. package/fd-toolbox/infra/env-functions.cjs.js +0 -16
  220. package/fd-toolbox/infra/env-schema.cjs.js +0 -12
  221. package/fd-toolbox/infra/env-setting-types.cjs.js +0 -6
  222. package/fd-toolbox/infra/env-setting-types.js +0 -4
  223. package/fd-toolbox/infra/env-store.cjs.js +0 -9
  224. package/fd-toolbox/infra/env-store.js +0 -7
  225. package/fd-toolbox/infra/toolbox-env-setting-keys.cjs.js +0 -6
  226. package/fd-toolbox/lib/environments.cjs.js +0 -25
  227. package/fd-toolbox/lib/utils.cjs.js +0 -29
  228. package/fd-toolbox/local-storage/local-storage.cjs.js +0 -12
  229. package/fd-toolbox/logging/loggers.cjs.js +0 -16
  230. package/fd-toolbox/logging/logging-constants.cjs.js +0 -6
  231. package/fd-toolbox/logging/logging-constants.js +0 -4
  232. package/fd-toolbox/notifications.cjs.js +0 -14
  233. package/fd-toolbox/odata/odata-constants.cjs.js +0 -11
  234. package/fd-toolbox/odata/odata-constants.js +0 -4
  235. package/fd-toolbox/odata/odata-enums.cjs.js +0 -8
  236. package/fd-toolbox/odata/odatas.cjs.js +0 -11
  237. package/fd-toolbox/odata/odatas.js +0 -9
  238. package/fd-toolbox/odata/services/odata-filters.cjs.js +0 -11
  239. package/fd-toolbox/odata/services/odata-filters.js +0 -9
  240. package/fd-toolbox/paths/paths-names.cjs.js +0 -9
  241. package/fd-toolbox/resources/resource-names.cjs.js +0 -6
  242. package/fd-toolbox/routing/login-routers.cjs.js +0 -10
  243. package/fd-toolbox/routing/paths.cjs.js +0 -6
  244. package/fd-toolbox/routing/routes.cjs.js +0 -14
  245. package/fd-toolbox/strings/strings-constants.cjs.js +0 -7
  246. package/fd-toolbox/strings/strings-constants.js +0 -4
  247. package/fd-toolbox/strings/strings.cjs.js +0 -10
  248. package/fd-toolbox/types/ensure-type.cjs.js +0 -22
  249. package/fd-toolbox-core/constants/meta-constants.cjs.js +0 -7
  250. package/fd-toolbox-core/constants/promises.cjs.js +0 -6
  251. package/fd-toolbox-core/core/name-of.cjs.js +0 -8
  252. package/fd-toolbox-core/enums/log-severities.cjs.js +0 -6
  253. package/fd-toolbox-core/types/resource-with-id.cjs.js +0 -9
  254. package/level2/cli/bin/index.cjs.js +0 -8
  255. package/level2/cli/create/bin/create.cjs.js +0 -42
  256. package/level2/npm-commands/build-npm/cli-contents.cjs.js +0 -9
  257. package/level2/npm-commands/build-npm/path.cjs.js +0 -13
  258. package/level2/npm-commands/build-npm/path.js +0 -6
  259. /package/cli/public/images/{companies → demo/companies}/00000000-0000-0000-0000-000000000000.webp +0 -0
  260. /package/cli/public/images/{companies → demo/companies}/004a196f-f9a7-49fc-9e05-606fffd0613c.webp +0 -0
  261. /package/cli/public/images/{companies → demo/companies}/497b26a6-3e91-4b27-8c24-f3b145fc6c7f.webp +0 -0
  262. /package/cli/public/images/{companies → demo/companies}/6cead29b-2572-4283-add6-f407b39d7135.webp +0 -0
  263. /package/cli/public/images/{companies → demo/companies}/71efdb77-4ecb-45e6-bc83-0fdff835c2e7.webp +0 -0
  264. /package/cli/public/images/{companies → demo/companies}/773602c8-a417-4afc-adb0-bb6856ac2970.webp +0 -0
  265. /package/cli/public/images/{companies → demo/companies}/9d09881a-cf60-438d-9edf-10f5864d4468.webp +0 -0
  266. /package/cli/public/images/{companies → demo/companies}/a1b2c3d4-e5f6-7890-1234-56789abcdef0.webp +0 -0
  267. /package/cli/public/images/{companies → demo/companies}/b04d7c5e-18de-4f44-b923-5bfb28bb33bb.webp +0 -0
  268. /package/cli/public/images/{companies → demo/companies}/c75c22fc-d295-472e-bdae-dbb8eb09cf18.webp +0 -0
  269. /package/cli/public/images/{companies → demo/companies}/cdf3efb4-33a2-485e-8d95-e0ab5a4cb6a5.webp +0 -0
  270. /package/cli/public/images/{companies → demo/companies}/d6fe4c6a-ecdb-40fc-a8e6-2044ef2b82d4.webp +0 -0
  271. /package/cli/public/images/{contacts → demo/contacts}/159e4c7a-ff5f-4162-8237-acec3ca8a759.webp +0 -0
  272. /package/cli/public/images/{contacts → demo/contacts}/65e07208-6b1f-4a59-b5e3-2f80f5741b18.webp +0 -0
  273. /package/cli/public/images/{contacts → demo/contacts}/6d1c5410-ef5b-4d37-8b8d-4ab8cfc87785.webp +0 -0
  274. /package/cli/public/images/{contacts → demo/contacts}/754c77d8-eefb-4a90-b07b-ed00980c88b4.webp +0 -0
  275. /package/cli/public/images/{contacts → demo/contacts}/9fd23cd9-9b94-4bb8-bb73-d315b93b8a5c.webp +0 -0
  276. /package/cli/public/images/{contacts → demo/contacts}/b2f746d1-6b3c-4c47-8e07-f73927c167cb.webp +0 -0
  277. /package/cli/public/images/{contacts → demo/contacts}/b515cb46-eab4-4862-9e8e-c2f5f217ae79.webp +0 -0
  278. /package/cli/public/images/{contacts → demo/contacts}/cf2eae72-e5ab-4d95-8f08-b6571b44f8eb.webp +0 -0
  279. /package/cli/public/images/{contacts → demo/contacts}/ec0e0285-22d9-48d4-b4b3-96388c37c807.webp +0 -0
  280. /package/cli/public/images/{contacts → demo/contacts}/ecb1a0f4-d244-4136-badf-c2bc72e3b678.webp +0 -0
  281. /package/cli/public/images/{users → demo/users}/02d1230f-5bfe-46b4-8ce1-d10f4aa918a7.webp +0 -0
  282. /package/cli/public/images/{users → demo/users}/166c5e4a-696f-4bf5-ab48-dbbb5e90d526.webp +0 -0
  283. /package/cli/public/images/{users → demo/users}/175a3f0c-692c-4503-87eb-ff95d6535e16.webp +0 -0
  284. /package/cli/public/images/{users → demo/users}/8398ee85-7546-4710-b628-44c98e4ba03a.webp +0 -0
  285. /package/cli/public/images/{users → demo/users}/979f8933-71b1-4df8-b691-566a901d3c76.webp +0 -0
  286. /package/cli/public/images/{users → demo/users}/fbe37132-31bc-4fc9-9bfb-aac7a1b61a7f.webp +0 -0
package/cli/bin/create.ts CHANGED
@@ -1,450 +1,635 @@
1
- import { execSync } from "child_process";
2
- import { promises as fsPromises, existsSync, mkdirSync, rmSync, Stats } from "fs";
3
- import { fileURLToPath } from "node:url";
4
- import * as path from "path";
5
- import fs from "node:fs";
6
- import {
7
- toolsFoldersToReplace,
8
- additionalFoldersToCreate,
9
- } from "@packages/base-repo/constants/create-app-constants";
10
-
11
- import { logStringError, logInfoRaw } from "@packages/fd-toolbox/logging/loggers";
12
-
13
- import { WithIndexer } from "@packages/fd-toolbox-core/types/with-indexer";
14
-
15
- import { projectName } from "@packages/fd-toolbox/constants/constants";
16
-
17
- import { allFilesToCopy } from "@packages/level2/npm-commands/build-npm/path";
18
-
19
- import { createError } from "@packages/fd-toolbox/errors/errors";
20
-
21
- import {
22
- files,
23
- folders,
24
- projectPaths,
25
- nonRootProjectPaths,
26
- } from "@packages/base-repo/app-constants/project-paths";
27
-
28
- import { npmPackages } from "@packages/base-repo/constants/packages";
29
-
30
- import { aliasSymbols } from "@packages/base-repo/app-constants/aliases";
31
-
32
- import { isString, isWithIndexer } from "@packages/fd-toolbox/types/ensure-type";
33
-
34
- import { cliEslintContent, cliScripts } from "@npm-commands/build-npm/cli-contents";
35
-
36
- interface PackageJson {
37
- name?: string;
38
- dependencies?: WithIndexer<string>;
39
- devDependencies?: WithIndexer<string>;
40
- [key: string]: unknown;
41
- }
42
-
43
- interface FileEntry {
44
- name: string;
45
- isDirectory(): boolean;
46
- }
47
-
48
- const requiredNodeVersion = "v20.0.0";
49
-
50
- if (process.version < requiredNodeVersion) {
51
- logStringError(`Node.js ${requiredNodeVersion}+ is required`);
52
- process.exit(1);
53
- }
54
-
55
- const filename = fileURLToPath(import.meta.url);
56
- const sourceDir = "../../../../";
57
- const binDir = path.dirname(filename);
58
- const repoRoot = path.resolve(binDir, sourceDir);
59
- const envLocalPath: string = path.join(binDir, `${sourceDir + folders.cli}/${files.envExample}`);
60
- const envTemplate: string = fs.readFileSync(envLocalPath, "utf8");
61
-
62
- const foldersToIgnore: string[] = [
63
- `${folders.cli}/${folders.constants}`,
64
- `${folders.cli}/${files.packageTemplate}`,
65
- `${folders.cli}/${files.envExample}`,
66
- `${folders.cli}/${folders.bin}`,
67
- `${folders.cli}/${folders.configs}`,
68
- ];
69
-
70
- function runCommand(command: string, cwd?: string) {
71
- logInfoRaw(`Executing: ${command}`);
72
-
73
- try {
74
- execSync(command, { stdio: "inherit", cwd: cwd ?? process.cwd() });
75
- } catch (error) {
76
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
77
- logStringError(`Error executing: ${command}`);
78
- logStringError(errorMessage);
79
- process.exit(1);
80
- }
81
- }
82
-
83
- function checkTargetFolder(targetDir: string) {
84
- if (existsSync(targetDir)) {
85
- logStringError(`Error: Folder "${path.basename(targetDir)}" already exists.`);
86
- process.exit(1);
87
- }
88
- }
89
-
90
- function getCreateNextAppVersion() {
91
- const packageJsonPath: string = path.resolve(repoRoot, files.packageJson);
92
- const packageJson: PackageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
93
-
94
- return String(packageJson.devDependencies?.["next"] ?? packageJson.dependencies?.["next"] ?? "latest");
95
- }
96
-
97
- async function createNextProject(projectTitle: string, targetDir: string) {
98
- logInfoRaw("Creating Next.js project...");
99
-
100
- const createNextAppVersion = getCreateNextAppVersion();
101
- const createCommand: string = [
102
- `npx --yes create-next-app@${createNextAppVersion} "${projectTitle}"`,
103
- "--typescript",
104
- "--tailwind",
105
- "--eslint",
106
- "--app",
107
- "--src-dir",
108
- "--turbopack",
109
- "--no-import-alias",
110
- "--no-git",
111
- "--react-compiler",
112
- ].join(" ");
113
-
114
- runCommand(createCommand, path.dirname(targetDir));
115
- logInfoRaw("Next.js project created!");
116
-
117
- const tsConfigPath = path.join(targetDir, "next.config.ts");
118
- const publicDir = path.join(targetDir, folders.public);
119
-
120
- await fsPromises.rm(tsConfigPath, { force: true });
121
- logInfoRaw("Removed default next.config.ts");
122
-
123
- await fsPromises.rm(publicDir, { recursive: true, force: true });
124
- logInfoRaw("Removed default public folder");
125
-
126
- await fsPromises.mkdir(publicDir);
127
- }
128
-
129
- async function createEnvFile(targetDir: string) {
130
- logInfoRaw(`Creating ${files.env} file with default values...`);
131
-
132
- const envPath: string = path.join(targetDir, files.env);
133
-
134
- try {
135
- await fsPromises.writeFile(envPath, envTemplate, "utf8");
136
- logInfoRaw(`${files.env} file created with values!`);
137
- } catch (error) {
138
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
139
- logStringError(`Error creating ${files.env} file:`);
140
- logStringError(errorMessage);
141
- throw error;
142
- }
143
- }
144
-
145
- async function overridePackageJson(targetDir: string) {
146
- const targetPackageJsonPath: string = path.join(targetDir, files.packageJson);
147
- // "name" is omitted in package.json; renamed to package-template.json so NX ignores it as a project.
148
- const cliPackageJsonPath: string = path.join(repoRoot, `${folders.cli}/${files.packageTemplate}`);
149
-
150
- try {
151
- const targetPackageJson: PackageJson = JSON.parse(
152
- await fsPromises.readFile(targetPackageJsonPath, "utf8"),
153
- );
154
- const name = targetPackageJson.name;
155
-
156
- const cliPackageJson: PackageJson = JSON.parse(await fsPromises.readFile(cliPackageJsonPath, "utf8"));
157
-
158
- const finalPackageJson: PackageJson = {
159
- ...cliPackageJson,
160
- name: name,
161
- };
162
-
163
- await fsPromises.writeFile(
164
- targetPackageJsonPath,
165
- `${JSON.stringify(finalPackageJson, null, 2)}\n`,
166
- "utf8",
167
- );
168
- } catch (error) {
169
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
170
- logStringError(`Error overriding ${files.packageJson}:`);
171
- logStringError(errorMessage);
172
- throw error;
173
- }
174
- }
175
-
176
- function replaceImports(content: string) {
177
- const importRegex = /(import\s+)([^'"]+)(\s+from\s+["'])([^"']+)(["'])/g;
178
-
179
- return content.replace(importRegex, (match, p1, importItems, p3, importPath, p5) => {
180
- if (!importPath.startsWith(aliasSymbols.atSign) || importPath.startsWith(npmPackages.saasKit)) {
181
- return match;
182
- }
183
-
184
- let newImportPath;
185
-
186
- if (
187
- importPath.startsWith(aliasSymbols.atSign + folders.server) ||
188
- importPath.includes(`${nonRootProjectPaths.server}`)
189
- ) {
190
- newImportPath = npmPackages.saasKitServer;
191
- } else {
192
- newImportPath = npmPackages.saasKit;
193
- }
194
-
195
- let newImportItems = importItems.trim();
196
-
197
- const isTypeImport = /^type\b/.test(newImportItems);
198
- const isNamespaceImport = /^\*\s+as\s+/.test(newImportItems);
199
-
200
- if (!isTypeImport && !isNamespaceImport && !newImportItems.startsWith("{")) {
201
- newImportItems = `{ ${newImportItems} }`;
202
- }
203
-
204
- return `${p1}${newImportItems}${p3}${newImportPath}${p5}`;
205
- });
206
- }
207
-
208
- async function copyProjectFiles(targetDir: string) {
209
- logInfoRaw(`Copying ${projectName} files...`);
210
-
211
- for (const file of allFilesToCopy) {
212
- const cliFile = `${folders.cli}/${file}`;
213
- const srcPath: string = path.join(repoRoot, cliFile);
214
-
215
- const processedFile = replaceToolsFolder(file);
216
-
217
- const destPath: string = path.join(targetDir, processedFile);
218
-
219
- if (existsSync(srcPath)) {
220
- const stats: Stats = await fsPromises.stat(srcPath);
221
-
222
- if (stats.isDirectory()) {
223
- await copyDirectoryContents(srcPath, destPath);
224
- } else {
225
- await copyAndProcessFile(srcPath, destPath);
226
- }
227
- } else {
228
- logInfoRaw(`Source file not found: ${cliFile}`);
229
- }
230
- }
231
-
232
- logInfoRaw("Files copied and imports updated!");
233
- }
234
-
235
- function replaceToolsFolder(file: string) {
236
- let processedFile = file;
237
-
238
- for (const [folderPrefix, replacements] of Object.entries(toolsFoldersToReplace)) {
239
- if (processedFile.startsWith(folderPrefix) && isWithIndexer<string>(replacements)) {
240
- const typedReplacements: WithIndexer<string> = replacements;
241
-
242
- for (const key in typedReplacements) {
243
- const value = typedReplacements[key];
244
-
245
- if (isString(value)) {
246
- processedFile = processedFile.replace(key, value);
247
- }
248
- }
249
-
250
- break;
251
- }
252
- }
253
-
254
- return processedFile;
255
- }
256
-
257
- function shouldIgnorePath(filePath: string, basePath: string, isDirectory = false) {
258
- const relativeFromRepo = path.relative(repoRoot, filePath).replace(/\\/g, "/");
259
- const normalizedFoldersToIgnore = foldersToIgnore.map((p) => p.replace(/\\/g, "/"));
260
-
261
- if (normalizedFoldersToIgnore.includes(relativeFromRepo)) {
262
- return true;
263
- }
264
-
265
- if (isDirectory) {
266
- const relativePath = path.relative(basePath, filePath).replace(/\\/g, "/");
267
- const pathParts = relativePath.split("/");
268
- return normalizedFoldersToIgnore.some((ignoreFolder) =>
269
- pathParts.some((part) => part === ignoreFolder),
270
- );
271
- }
272
-
273
- return false;
274
- }
275
-
276
- async function copyDirectoryContents(srcDir: string, destDir: string) {
277
- if (!existsSync(destDir)) {
278
- mkdirSync(destDir, { recursive: true });
279
- }
280
-
281
- try {
282
- const entries: FileEntry[] = await fsPromises.readdir(srcDir, { withFileTypes: true });
283
-
284
- for (const entry of entries) {
285
- const srcPath: string = path.join(srcDir, entry.name);
286
- const destPath: string = path.join(destDir, entry.name);
287
-
288
- if (!shouldIgnorePath(srcPath, srcDir, entry.isDirectory())) {
289
- if (entry.isDirectory()) {
290
- await copyDirectoryContents(srcPath, destPath);
291
- } else {
292
- await copyAndProcessFile(srcPath, destPath);
293
- }
294
- }
295
- }
296
- } catch (error) {
297
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
298
- logStringError(`Error reading directory ${srcDir}: ${errorMessage}`);
299
- throw error;
300
- }
301
- }
302
-
303
- async function copyAndProcessFile(srcPath: string, destPath: string) {
304
- const destDir: string = path.dirname(destPath);
305
-
306
- if (!existsSync(destDir)) {
307
- mkdirSync(destDir, { recursive: true });
308
- }
309
-
310
- const stats: Stats = await fsPromises.stat(srcPath);
311
-
312
- if (stats.isDirectory()) {
313
- await copyDirectoryContents(srcPath, destPath);
314
- } else {
315
- const ext = path.extname(srcPath).toLowerCase();
316
-
317
- const binaryExtensions = [".ico", ".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".pdf"];
318
-
319
- if (binaryExtensions.includes(ext)) {
320
- await fsPromises.copyFile(srcPath, destPath);
321
- return;
322
- }
323
-
324
- const content = await fsPromises.readFile(srcPath, "utf8");
325
- const processedContent = replaceImports(content);
326
- await fsPromises.writeFile(destPath, processedContent, "utf8");
327
- }
328
- }
329
-
330
- function createAdditionalFolders(targetDir: string) {
331
- for (const folder of additionalFoldersToCreate) {
332
- const folderPath: string = path.join(targetDir, folder);
333
-
334
- if (!existsSync(folderPath)) {
335
- mkdirSync(folderPath, { recursive: true });
336
- logInfoRaw(`Created folder: ${folder}`);
337
- }
338
- }
339
-
340
- logInfoRaw("Additional folders created!");
341
- }
342
-
343
- async function setupPreCommitHooks(targetDir: string) {
344
- const huskyDir = path.join(targetDir, folders.husky);
345
- const preCommitHook = path.join(huskyDir, files.preCommit);
346
- const commitMsgHook = path.join(huskyDir, files.commitMsg);
347
-
348
- mkdirSync(huskyDir, { recursive: true });
349
-
350
- await fsPromises.writeFile(preCommitHook, cliScripts.preCommit, "utf8");
351
- await fsPromises.writeFile(commitMsgHook, cliScripts.commitMsg, "utf8");
352
-
353
- runCommand(cliScripts.chmodPreCommit, targetDir);
354
- runCommand(cliScripts.chmodCommitMsg, targetDir);
355
- }
356
-
357
- async function setupEslintConfig(targetDir: string) {
358
- const eslintConfigMjsPath = path.join(targetDir, files.eslintConfigMjs);
359
-
360
- if (existsSync(eslintConfigMjsPath)) {
361
- await fsPromises.rm(eslintConfigMjsPath);
362
- }
363
-
364
- const eslintConfigJsPath = path.join(targetDir, files.eslintConfigJs);
365
- await fsPromises.writeFile(eslintConfigJsPath, cliEslintContent, "utf8");
366
- }
367
-
368
- async function enableRepoModeExtras(targetDir: string) {
369
- logInfoRaw("Enabling recommendation mode (-r)...");
370
-
371
- runCommand(cliScripts.gitInit, targetDir);
372
-
373
- runCommand(cliScripts.installDevHuskyTsx, targetDir);
374
-
375
- runCommand(cliScripts.setLint, targetDir);
376
-
377
- runCommand(cliScripts.installHusky, targetDir);
378
-
379
- await setupPreCommitHooks(targetDir);
380
- }
381
-
382
- async function updateLayoutTitle(targetDir: string, folderName: string) {
383
- const layoutPath = path.join(targetDir, `${projectPaths.app}/${files.layout}`);
384
-
385
- if (!existsSync(layoutPath)) {
386
- throw createError(`${files.layout} not found at ${layoutPath}, cannot update title.`);
387
- }
388
-
389
- let content = await fsPromises.readFile(layoutPath, "utf8");
390
- const titleRegex = /"SaaS Kit"/g;
391
-
392
- if (!titleRegex.test(content)) {
393
- throw createError(`SaaS Kit not found in ${files.layout}, cannot update title.`);
394
- }
395
-
396
- content = content.replace(titleRegex, `"${folderName}"`);
397
- await fsPromises.writeFile(layoutPath, content, "utf8");
398
- }
399
-
400
- export async function create(args: string[]) {
401
- try {
402
- if (args.length < 1) {
403
- logStringError("Error: Specify folder name. Example: npx create-saas-kit-app <folder-name>");
404
- process.exit(1);
405
- }
406
-
407
- const folderName: string = args[0];
408
- const enableRepoMode = args.includes("-r");
409
- const targetDir: string = path.resolve(process.cwd(), folderName);
410
-
411
- logInfoRaw(`\nCreating ${projectName} project "${folderName}"...\n`);
412
-
413
- try {
414
- checkTargetFolder(targetDir);
415
- await createNextProject(folderName, targetDir);
416
- await copyProjectFiles(targetDir);
417
- await overridePackageJson(targetDir);
418
- createAdditionalFolders(targetDir);
419
- await createEnvFile(targetDir);
420
- await updateLayoutTitle(targetDir, folderName);
421
- await setupEslintConfig(targetDir);
422
-
423
- if (enableRepoMode) {
424
- await enableRepoModeExtras(targetDir);
425
- }
426
-
427
- logInfoRaw(`\n${projectName} project "${folderName}" is ready!`);
428
- logInfoRaw("Next steps:");
429
- logInfoRaw(` cd ${folderName}`);
430
- logInfoRaw(" npm run prod");
431
- logInfoRaw(" npm start");
432
- } catch (error) {
433
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
434
- logStringError("Error:");
435
- logStringError(errorMessage);
436
-
437
- if (existsSync(targetDir)) {
438
- logInfoRaw("Cleaning up...");
439
- rmSync(targetDir, { recursive: true, force: true });
440
- }
441
-
442
- process.exit(1);
443
- }
444
- } catch (error) {
445
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
446
- logStringError("Unhandled error:");
447
- logStringError(errorMessage);
448
- process.exit(1);
449
- }
450
- }
1
+ // Approved temp
2
+ // eslint-disable-next-line custom-eslint-rules/must-limit-lines-without-comments
3
+ import { execFileSync, execSync } from "child_process";
4
+ import { promises as fsPromises, existsSync, mkdirSync, rmSync, Stats } from "fs";
5
+ import { fileURLToPath } from "node:url";
6
+ import { createInterface } from "node:readline/promises";
7
+ import process from "process";
8
+ import * as path from "path";
9
+ import fs from "node:fs";
10
+ import os from "os";
11
+ import { toolsFoldersToReplace, additionalFoldersToCreate } from "@base-repo/constants/create-app-constants";
12
+
13
+ import { logStringError, logInfoRaw } from "@fd-toolbox/logging/loggers";
14
+
15
+ import { WithIndexer } from "@fd-toolbox-core/types/with-indexer";
16
+
17
+ import { allFilesToCopy } from "@npm-commands/build-npm/paths";
18
+
19
+ import { createError } from "@fd-toolbox/errors/errors";
20
+
21
+ import {
22
+ files,
23
+ folders,
24
+ projectPaths,
25
+ nonRootProjectPaths,
26
+ projectName,
27
+ } from "@base-repo/app-constants/project-paths";
28
+
29
+ import { npmPackages } from "@base-repo/constants/packages";
30
+
31
+ import { aliasSymbols } from "@base-repo/app-constants/aliases";
32
+
33
+ import {
34
+ fileExtensions,
35
+ nodeOsPlatforms,
36
+ sourceDir,
37
+ } from "@base-repo/app-constants/internal-system-constants";
38
+
39
+ import { ensureString, isString, isWithIndexer } from "@fd-toolbox/types/ensure-type";
40
+
41
+ import { cliEslintContent, cliScripts } from "@npm-commands/build-npm/cli-contents";
42
+
43
+ import { ecmaVersion, stdioStatuses } from "@base-repo/constants/internal-cli-constants";
44
+ import { gitCommandArguments, gitCommandParts } from "@git/constants/git-constants";
45
+
46
+ import { commandNames, externalPackageNames } from "@base-repo/app-constants/command-names";
47
+
48
+ import { commonStatuses } from "@base-repo/constants/ci-constants";
49
+ import { basicErrorMessages } from "@base-repo/constants/internal-common-strings";
50
+
51
+ import { first } from "@fd-toolbox/server/collections/single-funcs";
52
+
53
+ interface PackageJson {
54
+ name?: string;
55
+ dependencies?: WithIndexer<string>;
56
+ devDependencies?: WithIndexer<string>;
57
+ [key: string]: unknown;
58
+ }
59
+
60
+ interface FileEntry {
61
+ name: string;
62
+ isDirectory(): boolean;
63
+ }
64
+
65
+ export const lifecycleStatuses = {
66
+ created: commonStatuses.created,
67
+ } as const;
68
+
69
+ const requiredNodeVersion = "v20.0.0";
70
+
71
+ if (process.version < requiredNodeVersion) {
72
+ logStringError(`Node.js ${requiredNodeVersion}+ is required`);
73
+ process.exit(1);
74
+ }
75
+
76
+ const filename = fileURLToPath(import.meta.url);
77
+ const binDir = path.dirname(filename);
78
+ const repoRoot = path.resolve(binDir, sourceDir);
79
+ const envLocalPath: string = path.join(binDir, `${sourceDir + folders.cli}/${files.envExample}`);
80
+ const envTemplate: string = fs.readFileSync(envLocalPath, "utf8");
81
+
82
+ const cliPackageTemplate = `${folders.cli}/${files.packageTemplate}`;
83
+ const gitIgnoreTemplate = `${folders.cli}/${folders.templates}/gitignore-template`;
84
+
85
+ const foldersToIgnore: string[] = [
86
+ nonRootProjectPaths.cliConstants,
87
+ cliPackageTemplate,
88
+ `${folders.cli}/${files.envExample}`,
89
+ `${folders.cli}/${folders.bin}`,
90
+ nonRootProjectPaths.cliConfigs,
91
+ `${folders.cli}/${folders.templates}`,
92
+ ];
93
+
94
+ const unknownError = basicErrorMessages.unknownError;
95
+ const playwright = externalPackageNames.playwright;
96
+ const project = "project";
97
+
98
+ export async function create(args: string[]) {
99
+ try {
100
+ if (args.length < 1) {
101
+ logStringError("Error: Specify folder name. Example: npx create-saas-kit-app <folder-name>");
102
+ process.exit(1);
103
+ }
104
+
105
+ const folderName = first(args);
106
+ const enableRepoMode = args.includes(gitCommandArguments.dashR);
107
+ const installPlaywright = await shouldInstallPlaywright(enableRepoMode);
108
+ const usePreCommit = await shouldUsePreCommit();
109
+ const targetDir: string = path.resolve(process.cwd(), folderName);
110
+
111
+ logInfoRaw(`\nCreating ${projectName} ${project} "${folderName}"...\n`);
112
+
113
+ try {
114
+ checkTargetFolder(targetDir);
115
+ await createNextProject(folderName, targetDir);
116
+ await copyProjectFiles(targetDir);
117
+ await overridePackageJson(targetDir, installPlaywright);
118
+
119
+ if (!installPlaywright) {
120
+ await removePlaywrightArtifacts(targetDir);
121
+ }
122
+
123
+ runCommandWithArgs("npm", ["install"], targetDir);
124
+
125
+ createAdditionalFolders(targetDir);
126
+ await createEnvFile(targetDir);
127
+ await updateLayoutTitle(targetDir, folderName);
128
+ await setupEslintConfig(targetDir);
129
+
130
+ if (enableRepoMode) {
131
+ await enableRepoModeExtras(targetDir, usePreCommit);
132
+ }
133
+
134
+ await normalizeLineEndings(targetDir);
135
+
136
+ const initialCommit = "0-initial-commit";
137
+ runCommandWithArgs(commandNames.git, [gitCommandParts.add, "."], targetDir);
138
+ runCommandWithArgs(
139
+ commandNames.git,
140
+ [gitCommandParts.commit, gitCommandArguments.message, initialCommit],
141
+ targetDir,
142
+ );
143
+ logInfoRaw(`${initialCommit} ${lifecycleStatuses.created}!`);
144
+
145
+ logInfoRaw(`\n${projectName} ${project} "${folderName}" is ready!`);
146
+ logInfoRaw("Next steps:");
147
+ logInfoRaw(` cd ${folderName}`);
148
+ logInfoRaw(" npm run start");
149
+ } catch (error) {
150
+ const errorMessage = error instanceof Error ? error.message : unknownError;
151
+ logStringError("Error:");
152
+ logStringError(errorMessage);
153
+
154
+ if (existsSync(targetDir)) {
155
+ logInfoRaw("Cleaning up...");
156
+ rmSync(targetDir, { recursive: true, force: true });
157
+ }
158
+
159
+ process.exit(1);
160
+ }
161
+ } catch (error) {
162
+ const errorMessage = error instanceof Error ? error.message : unknownError;
163
+ logStringError("Unhandled error:");
164
+ logStringError(errorMessage);
165
+ process.exit(1);
166
+ }
167
+ }
168
+
169
+ function runCommand(command: string, cwd?: string) {
170
+ logInfoRaw(`Executing: ${command}`);
171
+
172
+ try {
173
+ execSync(command, { stdio: stdioStatuses.inherit, cwd: cwd ?? process.cwd() });
174
+ } catch (error) {
175
+ const errorMessage = error instanceof Error ? error.message : unknownError;
176
+ logStringError(`Error executing: ${command}`);
177
+ logStringError(errorMessage);
178
+ process.exit(1);
179
+ }
180
+ }
181
+
182
+ function runCommandWithArgs(command: string, args: string[], cwd?: string) {
183
+ const commandText = `${command} ${args.join(" ")}`;
184
+ const workingDirectory = cwd ?? process.cwd();
185
+
186
+ logInfoRaw(`Executing: ${commandText}`);
187
+
188
+ try {
189
+ execFileSync(command, args, { stdio: stdioStatuses.inherit, cwd: workingDirectory, shell: true });
190
+ } catch (error) {
191
+ logStringError(`Error executing: ${commandText}`);
192
+ logStringError(error instanceof Error ? error.message : unknownError);
193
+ process.exit(1);
194
+ }
195
+ }
196
+
197
+ function checkTargetFolder(targetDir: string) {
198
+ if (existsSync(targetDir)) {
199
+ logStringError(`Error: Folder "${path.basename(targetDir)}" already exists.`);
200
+ process.exit(1);
201
+ }
202
+ }
203
+
204
+ function getCreateNextAppVersion() {
205
+ const packageJsonPath: string = path.resolve(repoRoot, files.packageJson);
206
+ const packageJson: PackageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
207
+ const nextVersion =
208
+ packageJson.devDependencies?.[externalPackageNames.next] ??
209
+ packageJson.dependencies?.[externalPackageNames.next];
210
+
211
+ return ensureString(nextVersion ?? ecmaVersion);
212
+ }
213
+
214
+ async function createNextProject(projectTitle: string, targetDir: string) {
215
+ logInfoRaw("Creating Next.js project...");
216
+
217
+ const createNextAppVersion = getCreateNextAppVersion();
218
+ const createCommand: string = [
219
+ `npx --yes create-next-app@${createNextAppVersion} "${projectTitle}"`,
220
+ "--typescript",
221
+ "--tailwind",
222
+ "--eslint",
223
+ "--app",
224
+ "--src-dir",
225
+ "--turbopack",
226
+ "--no-import-alias",
227
+ "--no-git",
228
+ "--react-compiler",
229
+ "--agents",
230
+ "--ts",
231
+ "--agents-md",
232
+ ].join(" ");
233
+
234
+ runCommand(createCommand, path.dirname(targetDir));
235
+ logInfoRaw(`Next.js ${project} ${lifecycleStatuses.created}!`);
236
+
237
+ const tsConfigPath = path.join(targetDir, "next.config.ts");
238
+ const publicDir = path.join(targetDir, folders.public);
239
+
240
+ await fsPromises.rm(tsConfigPath, { force: true });
241
+ logInfoRaw("Removed default next.config.ts");
242
+
243
+ await fsPromises.rm(publicDir, { recursive: true, force: true });
244
+ logInfoRaw("Removed default public folder");
245
+
246
+ await fsPromises.mkdir(publicDir);
247
+ }
248
+
249
+ async function createEnvFile(targetDir: string) {
250
+ logInfoRaw(`Creating ${files.dotEnv} file with default values...`);
251
+
252
+ const envPath: string = path.join(targetDir, files.dotEnv);
253
+
254
+ try {
255
+ await fsPromises.writeFile(envPath, envTemplate, "utf8");
256
+ logInfoRaw(`${files.dotEnv} file created with values!`);
257
+ } catch (error) {
258
+ const errorMessage = error instanceof Error ? error.message : unknownError;
259
+ logStringError(`Error creating ${files.dotEnv} file:`);
260
+ logStringError(errorMessage);
261
+ throw error;
262
+ }
263
+ }
264
+
265
+ async function overridePackageJson(targetDir: string, installPlaywright: boolean) {
266
+ const targetPackageJsonPath: string = path.join(targetDir, files.packageJson);
267
+ // "name" is omitted in package.json; renamed to package-template.json so NX ignores it as a project.
268
+ const cliPackageJsonPath: string = path.join(repoRoot, cliPackageTemplate);
269
+
270
+ try {
271
+ const targetPackageJson: PackageJson = JSON.parse(
272
+ await fsPromises.readFile(targetPackageJsonPath, "utf8"),
273
+ );
274
+ const name = targetPackageJson.name;
275
+
276
+ const cliPackageJson: PackageJson = JSON.parse(await fsPromises.readFile(cliPackageJsonPath, "utf8"));
277
+
278
+ const finalPackageJson: PackageJson = {
279
+ ...cliPackageJson,
280
+ name: name,
281
+ };
282
+
283
+ if (!installPlaywright && finalPackageJson.devDependencies) {
284
+ delete finalPackageJson.devDependencies[playwright];
285
+ }
286
+
287
+ await fsPromises.writeFile(
288
+ targetPackageJsonPath,
289
+ `${JSON.stringify(finalPackageJson, undefined, 2)}\n`,
290
+ "utf8",
291
+ );
292
+ } catch (error) {
293
+ const errorMessage = error instanceof Error ? error.message : unknownError;
294
+ logStringError(`Error overriding ${files.packageJson}:`);
295
+ logStringError(errorMessage);
296
+ throw error;
297
+ }
298
+ }
299
+
300
+ async function shouldInstallPlaywright(enableRepoMode: boolean) {
301
+ let shouldInstall = false;
302
+
303
+ if (enableRepoMode) {
304
+ shouldInstall = true;
305
+ } else if (!process.stdin.isTTY) {
306
+ logInfoRaw("No interactive terminal detected; skipping Playwright setup.");
307
+ } else {
308
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
309
+
310
+ try {
311
+ const answer = await prompt.question("Install Playwright? (y/N): ");
312
+ shouldInstall = /^y(es)?$/i.test(answer.trim());
313
+ } finally {
314
+ prompt.close();
315
+ }
316
+ }
317
+
318
+ return shouldInstall;
319
+ }
320
+
321
+ async function shouldUsePreCommit() {
322
+ let shouldUse = false;
323
+
324
+ if (!process.stdin.isTTY) {
325
+ logInfoRaw("No interactive terminal detected; skipping pre-commit setup.");
326
+ } else {
327
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
328
+
329
+ try {
330
+ const answer = await prompt.question("Set up pre-commit? (y/N): ");
331
+ shouldUse = /^y(es)?$/i.test(answer.trim());
332
+ } finally {
333
+ prompt.close();
334
+ }
335
+ }
336
+
337
+ return shouldUse;
338
+ }
339
+
340
+ async function removePlaywrightArtifacts(targetDir: string) {
341
+ const playwrightConfigPath = path.join(targetDir, files.playwrightConfig);
342
+ const playwrightConfigCliPath = path.join(targetDir, folders.configs, `${playwright}-cli-config.ts`);
343
+ const testDir = path.join(targetDir, folders.test);
344
+
345
+ await fsPromises.rm(playwrightConfigPath, { force: true });
346
+ await fsPromises.rm(playwrightConfigCliPath, { force: true });
347
+ await fsPromises.rm(testDir, { recursive: true, force: true });
348
+ }
349
+
350
+ function replaceImports(content: string) {
351
+ const importRegex = /(import\s+)([^'"]+)(\s+from\s+["'])([^"']+)(["'])/g;
352
+ const pathsToSkip = [
353
+ "@generated/schema",
354
+ "@generated/meta/crud-handler-map",
355
+ "@/app/init-saas-kit",
356
+ "@/app/http-wrappers",
357
+ ];
358
+
359
+ return content.replace(importRegex, (match, p1, importItems, p3, importPath, p5) => {
360
+ if (!importPath.startsWith(aliasSymbols.atSign) || importPath.startsWith(npmPackages.saasKit)) {
361
+ return match;
362
+ }
363
+
364
+ if (pathsToSkip.includes(importPath)) {
365
+ return match;
366
+ }
367
+
368
+ let newImportPath;
369
+
370
+ if (
371
+ importPath.startsWith(aliasSymbols.atSign + folders.server) ||
372
+ importPath.includes(nonRootProjectPaths.server)
373
+ ) {
374
+ newImportPath = npmPackages.saasKitServer;
375
+ } else {
376
+ newImportPath = npmPackages.saasKit;
377
+ }
378
+
379
+ let newImportItems = importItems.trim();
380
+
381
+ const isTypeImport = /^type\b/.test(newImportItems);
382
+ const isNamespaceImport = /^\*\s+as\s+/.test(newImportItems);
383
+
384
+ if (!isTypeImport && !isNamespaceImport && !newImportItems.startsWith("{")) {
385
+ newImportItems = `{ ${newImportItems} }`;
386
+ }
387
+
388
+ return `${p1}${newImportItems}${p3}${newImportPath}${p5}`;
389
+ });
390
+ }
391
+
392
+ async function copyProjectFiles(targetDir: string) {
393
+ logInfoRaw(`Copying ${projectName} files...`);
394
+
395
+ for (const file of allFilesToCopy) {
396
+ const cliFile = file === files.gitIgnore ? gitIgnoreTemplate : `${folders.cli}/${file}`;
397
+ const srcPath: string = path.join(repoRoot, cliFile);
398
+
399
+ const processedFile = replaceToolsFolder(file);
400
+
401
+ const destPath: string = path.join(targetDir, processedFile);
402
+
403
+ if (existsSync(srcPath)) {
404
+ const stats: Stats = await fsPromises.stat(srcPath);
405
+
406
+ if (stats.isDirectory()) {
407
+ await copyDirectoryContents(srcPath, destPath);
408
+ } else {
409
+ await copyAndProcessFile(srcPath, destPath);
410
+ }
411
+ } else {
412
+ logInfoRaw(`Source file not found: ${cliFile}`);
413
+ }
414
+ }
415
+
416
+ logInfoRaw("Files copied and imports updated!");
417
+ }
418
+
419
+ function replaceToolsFolder(file: string) {
420
+ let processedFile = file;
421
+
422
+ for (const [folderPrefix, replacements] of Object.entries(toolsFoldersToReplace)) {
423
+ if (processedFile.startsWith(folderPrefix) && isWithIndexer<string>(replacements)) {
424
+ const typedReplacements: WithIndexer<string> = replacements;
425
+
426
+ for (const key in typedReplacements) {
427
+ const value = typedReplacements[key];
428
+
429
+ if (isString(value)) {
430
+ processedFile = processedFile.replace(key, value);
431
+ }
432
+ }
433
+
434
+ break;
435
+ }
436
+ }
437
+
438
+ return processedFile;
439
+ }
440
+
441
+ function shouldIgnorePath(filePath: string, basePath: string, isDirectory = false) {
442
+ const relativeFromRepo = path.relative(repoRoot, filePath).replace(/\\/g, "/");
443
+ const normalizedFoldersToIgnore = foldersToIgnore.map((p) => p.replace(/\\/g, "/"));
444
+
445
+ if (normalizedFoldersToIgnore.includes(relativeFromRepo)) {
446
+ return true;
447
+ }
448
+
449
+ if (isDirectory) {
450
+ const relativePath = path.relative(basePath, filePath).replace(/\\/g, "/");
451
+ const pathParts = relativePath.split("/");
452
+ return normalizedFoldersToIgnore.some((ignoreFolder) =>
453
+ pathParts.some((part) => part === ignoreFolder),
454
+ );
455
+ }
456
+
457
+ return false;
458
+ }
459
+
460
+ async function copyDirectoryContents(srcDir: string, destDir: string) {
461
+ if (!existsSync(destDir)) {
462
+ mkdirSync(destDir, { recursive: true });
463
+ }
464
+
465
+ try {
466
+ const entries: FileEntry[] = await fsPromises.readdir(srcDir, { withFileTypes: true });
467
+
468
+ for (const entry of entries) {
469
+ const srcPath: string = path.join(srcDir, entry.name);
470
+ const destPath: string = path.join(destDir, entry.name);
471
+
472
+ if (!shouldIgnorePath(srcPath, srcDir, entry.isDirectory())) {
473
+ if (entry.isDirectory()) {
474
+ await copyDirectoryContents(srcPath, destPath);
475
+ } else {
476
+ await copyAndProcessFile(srcPath, destPath);
477
+ }
478
+ }
479
+ }
480
+ } catch (error) {
481
+ const errorMessage = error instanceof Error ? error.message : unknownError;
482
+ logStringError(`Error reading directory ${srcDir}: ${errorMessage}`);
483
+ throw error;
484
+ }
485
+ }
486
+
487
+ async function copyAndProcessFile(srcPath: string, destPath: string) {
488
+ const destDir: string = path.dirname(destPath);
489
+
490
+ if (!existsSync(destDir)) {
491
+ mkdirSync(destDir, { recursive: true });
492
+ }
493
+
494
+ const stats: Stats = await fsPromises.stat(srcPath);
495
+
496
+ if (stats.isDirectory()) {
497
+ await copyDirectoryContents(srcPath, destPath);
498
+ } else {
499
+ const ext = path.extname(srcPath).toLowerCase();
500
+
501
+ const binaryExtensions = [
502
+ fileExtensions.ico,
503
+ fileExtensions.png,
504
+ fileExtensions.jpg,
505
+ fileExtensions.jpeg,
506
+ fileExtensions.webp,
507
+ fileExtensions.gif,
508
+ fileExtensions.svg,
509
+ ".pdf",
510
+ ];
511
+
512
+ if (binaryExtensions.includes(ext)) {
513
+ await fsPromises.copyFile(srcPath, destPath);
514
+ return;
515
+ }
516
+
517
+ const content = await fsPromises.readFile(srcPath, "utf8");
518
+ const processedContent = replaceImports(content);
519
+ await fsPromises.writeFile(destPath, processedContent, "utf8");
520
+ }
521
+ }
522
+
523
+ function createAdditionalFolders(targetDir: string) {
524
+ for (const folder of additionalFoldersToCreate) {
525
+ const folderPath: string = path.join(targetDir, folder);
526
+
527
+ if (!existsSync(folderPath)) {
528
+ mkdirSync(folderPath, { recursive: true });
529
+ logInfoRaw(`Created folder: ${folder}`);
530
+ }
531
+ }
532
+
533
+ logInfoRaw("Additional folders created!");
534
+ }
535
+
536
+ async function setupPreCommitHooks(targetDir: string) {
537
+ const huskyDir = path.join(targetDir, folders.husky);
538
+ const preCommitHook = path.join(huskyDir, files.preCommit);
539
+ const commitMsgHook = path.join(huskyDir, files.commitMsg);
540
+
541
+ mkdirSync(huskyDir, { recursive: true });
542
+
543
+ await fsPromises.writeFile(preCommitHook, cliScripts.preCommit, "utf8");
544
+ await fsPromises.writeFile(commitMsgHook, cliScripts.commitMsg, "utf8");
545
+
546
+ runCommand(cliScripts.chmodPreCommit, targetDir);
547
+ runCommand(cliScripts.chmodCommitMsg, targetDir);
548
+ }
549
+
550
+ async function setupEslintConfig(targetDir: string) {
551
+ const eslintConfigMjsPath = path.join(targetDir, files.eslintConfigMjs);
552
+
553
+ if (existsSync(eslintConfigMjsPath)) {
554
+ await fsPromises.rm(eslintConfigMjsPath);
555
+ }
556
+
557
+ const eslintConfigJsPath = path.join(targetDir, files.eslintConfigJs);
558
+ await fsPromises.writeFile(eslintConfigJsPath, cliEslintContent, "utf8");
559
+ }
560
+
561
+ async function enableRepoModeExtras(targetDir: string, usePreCommit: boolean) {
562
+ logInfoRaw(`Enabling recommendation mode (${gitCommandArguments.dashR})...`);
563
+
564
+ runCommand(cliScripts.gitInit, targetDir);
565
+
566
+ runCommand(cliScripts.installDevHuskyTsx, targetDir);
567
+
568
+ runCommand(cliScripts.setLint, targetDir);
569
+
570
+ runCommand(cliScripts.installHusky, targetDir);
571
+
572
+ if (usePreCommit) {
573
+ await setupPreCommitHooks(targetDir);
574
+ }
575
+ }
576
+
577
+ async function updateLayoutTitle(targetDir: string, folderName: string) {
578
+ const layoutPath = path.join(targetDir, `${projectPaths.app}/${files.layout}`);
579
+ const cannotUpdateTitle = "Cannot update title";
580
+
581
+ if (!existsSync(layoutPath)) {
582
+ throw createError(`${files.layout} not found at ${layoutPath}, ${cannotUpdateTitle}.`);
583
+ }
584
+
585
+ let content = await fsPromises.readFile(layoutPath, "utf8");
586
+ const titleRegex = /title: projectName,/g;
587
+
588
+ if (!titleRegex.test(content)) {
589
+ throw createError(`projectName not found in ${files.layout}, ${cannotUpdateTitle}.`);
590
+ }
591
+
592
+ content = content.replace(titleRegex, `title: "${folderName}",`);
593
+
594
+ content = content.replace(/import\s*{\s*projectName\s*}\s*from\s*["'][^"']+["'];?\s*\n?/g, "");
595
+
596
+ await fsPromises.writeFile(layoutPath, content, "utf8");
597
+ }
598
+
599
+ async function normalizeLineEndings(targetDir: string) {
600
+ if (os.platform() !== nodeOsPlatforms.win32) return;
601
+
602
+ const textExtensions = [
603
+ fileExtensions.ts,
604
+ fileExtensions.tsx,
605
+ fileExtensions.js,
606
+ fileExtensions.json,
607
+ fileExtensions.css,
608
+ files.dotEnv,
609
+ fileExtensions.md,
610
+ fileExtensions.mjs,
611
+ ];
612
+
613
+ const directoriesToProcess: string[] = [targetDir];
614
+
615
+ while (directoriesToProcess.length > 0) {
616
+ const currentDir = directoriesToProcess.pop()!;
617
+ const entries = await fsPromises.readdir(currentDir, { withFileTypes: true });
618
+
619
+ for (const entry of entries) {
620
+ const fullPath = path.join(currentDir, entry.name);
621
+
622
+ if (entry.isDirectory()) {
623
+ directoriesToProcess.push(fullPath);
624
+ } else {
625
+ const ext = path.extname(fullPath).toLowerCase();
626
+
627
+ if (textExtensions.includes(ext) && existsSync(fullPath)) {
628
+ const content = await fsPromises.readFile(fullPath, "utf8");
629
+ const crlfContent = content.replace(/\r?\n/g, "\r\n");
630
+ await fsPromises.writeFile(fullPath, crlfContent, "utf8");
631
+ }
632
+ }
633
+ }
634
+ }
635
+ }