create-acmekit-app 2.13.1

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 (59) hide show
  1. package/.turbo/turbo-build.log +1 -0
  2. package/CHANGELOG.md +3 -0
  3. package/README.md +58 -0
  4. package/dist/commands/create.js +8 -0
  5. package/dist/index.js +31 -0
  6. package/dist/utils/clone-repo.js +77 -0
  7. package/dist/utils/create-abort-controller.js +11 -0
  8. package/dist/utils/create-db.js +172 -0
  9. package/dist/utils/execute.js +51 -0
  10. package/dist/utils/facts.js +87 -0
  11. package/dist/utils/format-connection-string.js +17 -0
  12. package/dist/utils/get-config-store.js +10 -0
  13. package/dist/utils/get-current-os.js +10 -0
  14. package/dist/utils/log-message.js +21 -0
  15. package/dist/utils/logger.js +8 -0
  16. package/dist/utils/nextjs-utils.js +85 -0
  17. package/dist/utils/node-version.js +5 -0
  18. package/dist/utils/package-manager.js +266 -0
  19. package/dist/utils/postgres-client.js +9 -0
  20. package/dist/utils/prepare-project.js +184 -0
  21. package/dist/utils/process-manager.js +50 -0
  22. package/dist/utils/project-creator/acmekit-plugin-creator.js +93 -0
  23. package/dist/utils/project-creator/acmekit-project-creator.js +204 -0
  24. package/dist/utils/project-creator/creator.js +46 -0
  25. package/dist/utils/project-creator/index.js +4 -0
  26. package/dist/utils/project-creator/project-creator-factory.js +83 -0
  27. package/dist/utils/start-acmekit.js +13 -0
  28. package/dist/utils/update-package-versions.js +28 -0
  29. package/jest.config.cjs +14 -0
  30. package/package.json +48 -0
  31. package/src/commands/create.ts +12 -0
  32. package/src/index.ts +65 -0
  33. package/src/types.d.ts +1 -0
  34. package/src/utils/__tests__/create-abort-controller.test.ts +166 -0
  35. package/src/utils/__tests__/package-manager.test.ts +637 -0
  36. package/src/utils/clone-repo.ts +117 -0
  37. package/src/utils/create-abort-controller.ts +16 -0
  38. package/src/utils/create-db.ts +245 -0
  39. package/src/utils/execute.ts +86 -0
  40. package/src/utils/facts.ts +148 -0
  41. package/src/utils/format-connection-string.ts +29 -0
  42. package/src/utils/get-config-store.ts +17 -0
  43. package/src/utils/get-current-os.ts +10 -0
  44. package/src/utils/log-message.ts +28 -0
  45. package/src/utils/logger.ts +10 -0
  46. package/src/utils/nextjs-utils.ts +139 -0
  47. package/src/utils/node-version.ts +7 -0
  48. package/src/utils/package-manager.ts +334 -0
  49. package/src/utils/postgres-client.ts +23 -0
  50. package/src/utils/prepare-project.ts +325 -0
  51. package/src/utils/process-manager.ts +60 -0
  52. package/src/utils/project-creator/acmekit-plugin-creator.ts +127 -0
  53. package/src/utils/project-creator/acmekit-project-creator.ts +272 -0
  54. package/src/utils/project-creator/creator.ts +77 -0
  55. package/src/utils/project-creator/index.ts +4 -0
  56. package/src/utils/project-creator/project-creator-factory.ts +119 -0
  57. package/src/utils/start-acmekit.ts +26 -0
  58. package/src/utils/update-package-versions.ts +37 -0
  59. package/tsconfig.json +25 -0
@@ -0,0 +1,325 @@
1
+ import fs from "fs"
2
+ import path from "path"
3
+ import { Ora } from "ora"
4
+ import { ExecuteResult } from "./execute.js"
5
+ import { EOL } from "os"
6
+ import { displayFactBox, FactBoxOptions } from "./facts.js"
7
+ import ProcessManager from "./process-manager.js"
8
+ import type { Client } from "@acmekit/deps/pg"
9
+ import PackageManager from "./package-manager.js"
10
+ import { updatePackageVersions } from "./update-package-versions.js"
11
+
12
+ const ADMIN_EMAIL = "admin@acmekit-test.com"
13
+ let STORE_CORS = "http://localhost:8000"
14
+ let ADMIN_CORS = "http://localhost:5173,http://localhost:9000"
15
+ const DOCS_CORS = "https://docs.acmekit.com"
16
+ const AUTH_CORS = [ADMIN_CORS, STORE_CORS, DOCS_CORS].join(",")
17
+ STORE_CORS += `,${DOCS_CORS}`
18
+ ADMIN_CORS += `,${DOCS_CORS}`
19
+ const DEFAULT_REDIS_URL = "redis://localhost:6379"
20
+
21
+ type PreparePluginOptions = {
22
+ isPlugin: true
23
+ directory: string
24
+ projectName: string
25
+ spinner: Ora
26
+ processManager: ProcessManager
27
+ abortController?: AbortController
28
+ verbose?: boolean
29
+ packageManager: PackageManager
30
+ }
31
+
32
+ type PrepareProjectOptions = {
33
+ isPlugin: false
34
+ directory: string
35
+ dbName?: string
36
+ dbConnectionString: string
37
+ projectName: string
38
+ seed?: boolean
39
+ spinner: Ora
40
+ processManager: ProcessManager
41
+ abortController?: AbortController
42
+ skipDb?: boolean
43
+ migrations?: boolean
44
+ onboardingType?: "default" | "nextjs"
45
+ nextjsDirectory?: string
46
+ client: Client | null
47
+ verbose?: boolean
48
+ packageManager: PackageManager
49
+ version?: string
50
+ }
51
+
52
+ type PrepareOptions = PreparePluginOptions | PrepareProjectOptions
53
+
54
+ export default async <
55
+ T extends PrepareOptions,
56
+ Output = T extends { isPlugin: true } ? void : string | undefined
57
+ >(
58
+ prepareOptions: T
59
+ ): Promise<Output> => {
60
+ if (prepareOptions.isPlugin) {
61
+ return preparePlugin(prepareOptions) as Output
62
+ }
63
+
64
+ return prepareProject(prepareOptions) as Output
65
+ }
66
+
67
+ async function preparePlugin({
68
+ directory,
69
+ projectName,
70
+ spinner,
71
+ processManager,
72
+ abortController,
73
+ verbose = false,
74
+ packageManager,
75
+ }: PreparePluginOptions) {
76
+ // initialize execution options
77
+ const execOptions = {
78
+ cwd: directory,
79
+ signal: abortController?.signal,
80
+ }
81
+
82
+ const factBoxOptions: FactBoxOptions = {
83
+ interval: null,
84
+ spinner,
85
+ processManager,
86
+ message: "",
87
+ title: "",
88
+ verbose,
89
+ }
90
+
91
+ // Update package.json
92
+ const packageJsonPath = path.join(directory, "package.json")
93
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"))
94
+
95
+ // Update name
96
+ packageJson.name = projectName
97
+
98
+ // Add packageManager field to ensure consistent version usage
99
+ const packageManagerString = await packageManager.getPackageManagerString()
100
+ if (packageManagerString) {
101
+ packageJson.packageManager = packageManagerString
102
+ }
103
+
104
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2))
105
+
106
+ factBoxOptions.interval = displayFactBox({
107
+ ...factBoxOptions,
108
+ spinner,
109
+ title: "Installing dependencies...",
110
+ processManager,
111
+ })
112
+
113
+ await packageManager.installDependencies(execOptions)
114
+
115
+ factBoxOptions.interval = displayFactBox({
116
+ ...factBoxOptions,
117
+ message: "Installed Dependencies",
118
+ })
119
+
120
+ displayFactBox({ ...factBoxOptions, message: "Finished Preparation" })
121
+ }
122
+
123
+ async function prepareProject({
124
+ directory,
125
+ projectName,
126
+ dbName,
127
+ dbConnectionString,
128
+ seed,
129
+ spinner,
130
+ processManager,
131
+ abortController,
132
+ skipDb,
133
+ migrations,
134
+ onboardingType = "default",
135
+ nextjsDirectory = "",
136
+ client,
137
+ verbose = false,
138
+ packageManager,
139
+ version,
140
+ }: PrepareProjectOptions) {
141
+ // initialize execution options
142
+ const execOptions = {
143
+ cwd: directory,
144
+ signal: abortController?.signal,
145
+ }
146
+
147
+ const npxOptions = {
148
+ ...execOptions,
149
+ env: {
150
+ ...process.env,
151
+ npm_config_yes: "yes",
152
+ },
153
+ }
154
+
155
+ const factBoxOptions: FactBoxOptions = {
156
+ interval: null,
157
+ spinner,
158
+ processManager,
159
+ message: "",
160
+ title: "",
161
+ verbose,
162
+ }
163
+
164
+ // Update package.json
165
+ const packageJsonPath = path.join(directory, "package.json")
166
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"))
167
+
168
+ // Update name
169
+ packageJson.name = projectName
170
+
171
+ // Add packageManager field to ensure consistent version usage
172
+ const packageManagerString = await packageManager.getPackageManagerString()
173
+ if (packageManagerString) {
174
+ packageJson.packageManager = packageManagerString
175
+ }
176
+
177
+ // Update acmekit dependencies versions
178
+ if (version) {
179
+ updatePackageVersions(packageJson, version)
180
+ }
181
+
182
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2))
183
+
184
+ // initialize the invite token to return
185
+ let inviteToken: string | undefined = undefined
186
+
187
+ // add environment variables
188
+ let env = `MEDUSA_ADMIN_ONBOARDING_TYPE=${onboardingType}${EOL}STORE_CORS=${STORE_CORS}${EOL}ADMIN_CORS=${ADMIN_CORS}${EOL}AUTH_CORS=${AUTH_CORS}${EOL}REDIS_URL=${DEFAULT_REDIS_URL}${EOL}JWT_SECRET=supersecret${EOL}COOKIE_SECRET=supersecret`
189
+
190
+ if (!skipDb) {
191
+ if (dbName) {
192
+ env += `${EOL}DB_NAME=${dbName}`
193
+ dbConnectionString = dbConnectionString!.replace(dbName, "$DB_NAME")
194
+ }
195
+ env += `${EOL}DATABASE_URL=${dbConnectionString}`
196
+ }
197
+
198
+ if (nextjsDirectory) {
199
+ env += `${EOL}MEDUSA_ADMIN_ONBOARDING_NEXTJS_DIRECTORY=${nextjsDirectory}`
200
+ }
201
+
202
+ fs.appendFileSync(path.join(directory, `.env`), env)
203
+
204
+ factBoxOptions.interval = displayFactBox({
205
+ ...factBoxOptions,
206
+ spinner,
207
+ title: "Installing dependencies...",
208
+ processManager,
209
+ })
210
+
211
+ await packageManager.installDependencies(execOptions)
212
+
213
+ factBoxOptions.interval = displayFactBox({
214
+ ...factBoxOptions,
215
+ message: "Installed Dependencies",
216
+ })
217
+
218
+ if (!skipDb && migrations) {
219
+ factBoxOptions.interval = displayFactBox({
220
+ ...factBoxOptions,
221
+ title: "Running Migrations...",
222
+ })
223
+
224
+ // run migrations
225
+ const migrationExecResult = await packageManager.runAcmeKitCommand(
226
+ "db:migrate",
227
+ npxOptions,
228
+ {
229
+ verbose,
230
+ needOutput: true,
231
+ }
232
+ )
233
+
234
+ if (client) {
235
+ // check the migrations table is in the database
236
+ // to ensure that migrations ran
237
+ let errorOccurred = false
238
+ try {
239
+ const migrations = await client.query(
240
+ `SELECT count(tablename) from pg_tables WHERE tablename = 'mikro_orm_migrations'`
241
+ )
242
+ errorOccurred = migrations.rowCount == 0
243
+ } catch (e) {
244
+ // avoid error thrown if the migrations table
245
+ // doesn't exist
246
+ errorOccurred = true
247
+ }
248
+
249
+ // ensure that migrations actually ran in case of an uncaught error
250
+ if (
251
+ errorOccurred &&
252
+ (migrationExecResult.stderr || migrationExecResult.stdout)
253
+ ) {
254
+ throw new Error(
255
+ `An error occurred while running migrations: ${
256
+ migrationExecResult.stderr || migrationExecResult.stdout
257
+ }`
258
+ )
259
+ }
260
+ }
261
+
262
+ factBoxOptions.interval = displayFactBox({
263
+ ...factBoxOptions,
264
+ message: "Ran Migrations",
265
+ })
266
+
267
+ const userExecResult = (await packageManager.runAcmeKitCommand(
268
+ `user -e ${ADMIN_EMAIL} --invite`,
269
+ npxOptions,
270
+ { verbose, needOutput: true }
271
+ )) as ExecuteResult
272
+
273
+ // get invite token from stdout
274
+ const match = (userExecResult.stdout as string).match(
275
+ /Invite token: (?<token>.+)/
276
+ )
277
+ inviteToken = match?.groups?.token
278
+
279
+ // TODO for now we just seed the default data
280
+ // we should add onboarding seeding again if it makes
281
+ // since once we re-introduce the onboarding flow.
282
+ factBoxOptions.interval = displayFactBox({
283
+ ...factBoxOptions,
284
+ title: "Seeding database...",
285
+ })
286
+
287
+ await packageManager.runCommand("seed", execOptions)
288
+
289
+ displayFactBox({
290
+ ...factBoxOptions,
291
+ message: "Seeded database with demo data",
292
+ })
293
+ }
294
+
295
+ // if installation includes Next.js, retrieve the publishable API key
296
+ // from the backend and add it as an enviornment variable
297
+ if (nextjsDirectory && client) {
298
+ const apiKeys = await client.query(
299
+ `SELECT * FROM "api_key" WHERE type = 'publishable'`
300
+ )
301
+
302
+ if (apiKeys.rowCount) {
303
+ const nextjsEnvPath = path.join(
304
+ nextjsDirectory,
305
+ fs.existsSync(path.join(nextjsDirectory, ".env.local"))
306
+ ? ".env.local"
307
+ : ".env.template"
308
+ )
309
+
310
+ const originalContent = fs.readFileSync(nextjsEnvPath, "utf-8")
311
+
312
+ fs.writeFileSync(
313
+ nextjsEnvPath,
314
+ originalContent.replace(
315
+ "NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_test",
316
+ `NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=${apiKeys.rows[0].token}`
317
+ )
318
+ )
319
+ }
320
+ }
321
+
322
+ displayFactBox({ ...factBoxOptions, message: "Finished Preparation" })
323
+
324
+ return inviteToken
325
+ }
@@ -0,0 +1,60 @@
1
+ type ProcessOptions = {
2
+ process: Function
3
+ ignoreERESOLVE?: boolean
4
+ }
5
+
6
+ export default class ProcessManager {
7
+ intervals: NodeJS.Timeout[] = []
8
+ static MAX_RETRIES = 3
9
+
10
+ constructor() {
11
+ this.onTerminated(() => {
12
+ this.intervals.forEach((interval) => {
13
+ clearInterval(interval)
14
+ })
15
+ })
16
+ }
17
+
18
+ onTerminated(fn: () => Promise<void> | void) {
19
+ process.on("SIGTERM", async () => fn())
20
+ process.on("SIGINT", async () => fn())
21
+ }
22
+
23
+ addInterval(interval: NodeJS.Timeout) {
24
+ this.intervals.push(interval)
25
+ }
26
+
27
+ // when running commands with npx or npm sometimes they
28
+ // terminate with EAGAIN error unexpectedly
29
+ // this utility function allows retrying the process if
30
+ // EAGAIN occurs, or otherwise throw the error that occurs
31
+ async runProcess({ process, ignoreERESOLVE }: ProcessOptions) {
32
+ let processError = false
33
+ let retries = 0
34
+ do {
35
+ ++retries
36
+ try {
37
+ return await process()
38
+ } catch (error) {
39
+ if (
40
+ typeof error === "object" &&
41
+ error !== null &&
42
+ "code" in error &&
43
+ error?.code === "EAGAIN"
44
+ ) {
45
+ processError = true
46
+ } else if (
47
+ ignoreERESOLVE &&
48
+ typeof error === "object" &&
49
+ error !== null &&
50
+ "code" in error &&
51
+ error?.code === "ERESOLVE"
52
+ ) {
53
+ // ignore error
54
+ } else {
55
+ throw error
56
+ }
57
+ }
58
+ } while (processError && retries <= ProcessManager.MAX_RETRIES)
59
+ }
60
+ }
@@ -0,0 +1,127 @@
1
+ import { track } from "@acmekit/telemetry"
2
+ import boxen from "boxen"
3
+ import chalk from "chalk"
4
+ import { emojify } from "node-emoji"
5
+ import { EOL } from "os"
6
+ import { runCloneRepo } from "../clone-repo.js"
7
+ import { isAbortError } from "../create-abort-controller.js"
8
+ import { displayFactBox } from "../facts.js"
9
+ import logMessage from "../log-message.js"
10
+ import prepareProject from "../prepare-project.js"
11
+ import {
12
+ BaseProjectCreator,
13
+ ProjectCreator,
14
+ ProjectOptions,
15
+ } from "./creator.js"
16
+ import terminalLink from "terminal-link"
17
+
18
+ // Plugin Project Creator
19
+ export class PluginProjectCreator
20
+ extends BaseProjectCreator
21
+ implements ProjectCreator
22
+ {
23
+ constructor(projectName: string, options: ProjectOptions, args: string[]) {
24
+ super(projectName, options, args)
25
+ this.setupProcessManager()
26
+ }
27
+
28
+ async create(): Promise<void> {
29
+ track("CREATE_CLI_CMP")
30
+
31
+ logMessage({
32
+ message: `${emojify(
33
+ ":rocket:"
34
+ )} Starting plugin setup, this may take a few minutes.`,
35
+ })
36
+
37
+ this.spinner.start()
38
+ this.factBoxOptions.interval = displayFactBox({
39
+ ...this.factBoxOptions,
40
+ title: "Setting up plugin...",
41
+ })
42
+
43
+ try {
44
+ await this.cloneAndPreparePlugin()
45
+ this.spinner.succeed(chalk.green("Plugin Prepared"))
46
+ this.showSuccessMessage()
47
+ } catch (e: any) {
48
+ this.handleError(e)
49
+ }
50
+ }
51
+
52
+ private async cloneAndPreparePlugin(): Promise<void> {
53
+ await runCloneRepo({
54
+ projectName: this.projectPath,
55
+ repoUrl: this.options.repoUrl ?? "",
56
+ abortController: this.abortController,
57
+ spinner: this.spinner,
58
+ verbose: this.options.verbose,
59
+ isPlugin: true,
60
+ })
61
+
62
+ this.factBoxOptions.interval = displayFactBox({
63
+ ...this.factBoxOptions,
64
+ message: "Created plugin directory",
65
+ })
66
+
67
+ await prepareProject({
68
+ isPlugin: true,
69
+ directory: this.projectPath,
70
+ projectName: this.projectName,
71
+ spinner: this.spinner,
72
+ processManager: this.processManager,
73
+ abortController: this.abortController,
74
+ verbose: this.options.verbose,
75
+ packageManager: this.packageManager,
76
+ })
77
+ }
78
+
79
+ private handleError(e: any): void {
80
+ if (isAbortError(e)) {
81
+ process.exit()
82
+ }
83
+
84
+ this.spinner.stop()
85
+ logMessage({
86
+ message: `An error occurred while preparing plugin: ${e}`,
87
+ type: "error",
88
+ })
89
+ }
90
+
91
+ protected showSuccessMessage(): void {
92
+ logMessage({
93
+ message: boxen(
94
+ chalk.green(
95
+ `Change to the \`${
96
+ this.projectName
97
+ }\` directory to explore your AcmeKit plugin.${EOL}${EOL}Check out the ${terminalLink(
98
+ "AcmeKit plugin documentation",
99
+ "https://docs.acmekit.com/learn/fundamentals/plugins"
100
+ )} to start your development.${EOL}${EOL}Star us on ${terminalLink(
101
+ "GitHub",
102
+ "https://github.com/acmekit/acmekit/stargazers"
103
+ )} if you like what we're building.`
104
+ ),
105
+ {
106
+ titleAlignment: "center",
107
+ textAlignment: "center",
108
+ padding: 1,
109
+ margin: 1,
110
+ float: "center",
111
+ }
112
+ ),
113
+ })
114
+ }
115
+
116
+ protected setupProcessManager(): void {
117
+ this.processManager.onTerminated(async () => {
118
+ this.spinner.stop()
119
+
120
+ if (!this.printedMessage && this.isProjectCreated) {
121
+ this.printedMessage = true
122
+ this.showSuccessMessage()
123
+ }
124
+ return
125
+ })
126
+ }
127
+ }