diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index f1ce3cc23402b..22c392d8b6d74 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -90,6 +90,23 @@ jobs: env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ secrets.TURBO_TEAM }} + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + POSTGRES_HOST: localhost + POSTGRES_PORT: 5432 + services: + postgres: + image: postgres + env: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 steps: - name: Checkout uses: actions/checkout@v3 diff --git a/packages/cli/create-medusa-app/jest.config.js b/packages/cli/create-medusa-app/jest.config.js new file mode 100644 index 0000000000000..9aba117b7b8ad --- /dev/null +++ b/packages/cli/create-medusa-app/jest.config.js @@ -0,0 +1,21 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +export default { + transform: { + "^.+\\.[jt]s?$": [ + "ts-jest", + { + tsconfig: "tsconfig.spec.json", + isolatedModules: false, + useESM: true, + } + ], + }, + preset: 'ts-jest', + testEnvironment: `node`, + moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`], + extensionsToTreatAsEsm: [".ts"], + moduleNameMapper: { + '(.+)\\.js': '$1' +}, + testTimeout: 300000 +} \ No newline at end of file diff --git a/packages/cli/create-medusa-app/package.json b/packages/cli/create-medusa-app/package.json index 8e510312dddcc..23cc8e0ca68ea 100644 --- a/packages/cli/create-medusa-app/package.json +++ b/packages/cli/create-medusa-app/package.json @@ -7,7 +7,8 @@ "bin": "dist/index.js", "license": "MIT", "scripts": { - "dev": "ts-node --esm src/index.ts", + "test": "cross-env NODE_OPTIONS=\"--experimental-vm-modules\" jest --passWithNoTests src", + "dev": "node --loader ts-node/esm src/index.ts", "start": "node dist/index.js", "build": "tsc", "watch": "tsc --watch", @@ -45,11 +46,14 @@ "@typescript-eslint/eslint-plugin": "^6.19.0", "@typescript-eslint/parser": "^6.19.0", "configstore": "^6.0.0", + "cross-env": "^7.0.3", "eslint": "^8.40.0", "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.7.0", "prettier": "^2.8.8", + "ts-jest": "^29.1.4", "ts-node": "^10.9.1", "typescript": "^5.0.4" }, diff --git a/packages/cli/create-medusa-app/src/commands/create.ts b/packages/cli/create-medusa-app/src/commands/create.ts index 05444bc7f633a..5ceea6e8e9203 100644 --- a/packages/cli/create-medusa-app/src/commands/create.ts +++ b/packages/cli/create-medusa-app/src/commands/create.ts @@ -1,7 +1,7 @@ import inquirer from "inquirer" import slugifyType from "slugify" import chalk from "chalk" -import { getDbClientAndCredentials, runCreateDb } from "../utils/create-db.js" +import runCreateDb, { getDbClientAndCredentials } from "../utils/create-db.js" import prepareProject from "../utils/prepare-project.js" import startMedusa from "../utils/start-medusa.js" import open from "open" @@ -9,7 +9,6 @@ import waitOn from "wait-on" import ora, { Ora } from "ora" import fs from "fs" import path from "path" -import isEmailImported from "validator/lib/isEmail.js" import logMessage from "../utils/log-message.js" import createAbortController, { isAbortError, @@ -21,7 +20,7 @@ import ProcessManager from "../utils/process-manager.js" import { nanoid } from "nanoid" import { displayFactBox, FactBoxOptions } from "../utils/facts.js" import { EOL } from "os" -import { runCloneRepo } from "../utils/clone-repo.js" +import runCloneRepo from "../utils/clone-repo.js" import { askForNextjsStarter, installNextjsStarter, diff --git a/packages/cli/create-medusa-app/src/utils/__tests__/clone.spec.ts b/packages/cli/create-medusa-app/src/utils/__tests__/clone.spec.ts new file mode 100644 index 0000000000000..0bca54e00b407 --- /dev/null +++ b/packages/cli/create-medusa-app/src/utils/__tests__/clone.spec.ts @@ -0,0 +1,33 @@ +import runCloneRepo from "../clone-repo.js" +import path from "path" +import { rm, stat } from "fs/promises" +import ora, { Ora } from "ora" +import { tmpdir } from "os" + +const projectName = "test-store" +const projectPath = path.join(tmpdir(), projectName) +const abortController = new AbortController() +const spinner: Ora = ora() + +describe("clone-repo", () => { + afterAll(async () => { + // delete project + await rm(projectPath, { + force: true, + recursive: true + }) + }) + + it("should clone the repo", async () => { + expect.assertions(1) + await runCloneRepo({ + projectName: projectPath, + abortController, + spinner + }) + + return expect((async () => { + await stat(projectPath) + })()).resolves.toBeUndefined() + }) +}) \ No newline at end of file diff --git a/packages/cli/create-medusa-app/src/utils/__tests__/create-db.spec.ts b/packages/cli/create-medusa-app/src/utils/__tests__/create-db.spec.ts new file mode 100644 index 0000000000000..bc40b73e9eb33 --- /dev/null +++ b/packages/cli/create-medusa-app/src/utils/__tests__/create-db.spec.ts @@ -0,0 +1,60 @@ +import ora, { Ora } from "ora" +import runCreateDb from "../create-db.js" +import pg from "pg" +import { nanoid } from "nanoid" +import formatConnectionString from "../format-connection-string.js" + +const dbName = `medusa-${nanoid(4)}` +const spinner: Ora = ora() + +describe("create-db", () => { + afterEach(async () => { + const client = new pg.Client({ + user: process.env.POSTGRES_USER || "postgres", + password: process.env.POSTGRES_PASSWORD || "" + }) + + await client.connect() + + await client.query(`DROP DATABASE IF EXISTS "${dbName}"`) + + await client.end() + }) + + it("should create local db", async () => { + expect.assertions(1) + const client = new pg.Client({ + user: process.env.POSTGRES_USER || "postgres", + password: process.env.POSTGRES_PASSWORD || "", + host: process.env.POSTGRES_HOST || "localhost", + port: parseInt(process.env.POSTGRES_PORT || "5432") + }) + await client.connect() + + let newClient = await runCreateDb({ + client, + dbName, + spinner + }) + + const connectionString = formatConnectionString({ + user: newClient.user, + password: newClient.password, + host: newClient.host, + db: dbName + }) + + await newClient.end() + + newClient = new pg.Client({ + connectionString + }) + + return expect((async () => { + await newClient.connect() + + await newClient.end() + })()).resolves.toBeUndefined() + }) + +}) \ No newline at end of file diff --git a/packages/cli/create-medusa-app/src/utils/__tests__/format-connection-string.spec.ts b/packages/cli/create-medusa-app/src/utils/__tests__/format-connection-string.spec.ts new file mode 100644 index 0000000000000..15574163f6802 --- /dev/null +++ b/packages/cli/create-medusa-app/src/utils/__tests__/format-connection-string.spec.ts @@ -0,0 +1,16 @@ +import formatConnectionString from "../format-connection-string.js" + +const CONNECTION_STRING_REGEX = /(postgresql|postgres):\/\/([^:@\s]*(?::[^@\s]*)?@)?(?[^\/\?\s]+)\b/ + +describe("format-connection-string", () => { + it("should format string correctly", () => { + const connectionString = formatConnectionString({ + user: "postgres", + password: "postgres", + host: "localhost", + db: "medusa" + }) + + expect(connectionString).toMatch(CONNECTION_STRING_REGEX) + }) +}) \ No newline at end of file diff --git a/packages/cli/create-medusa-app/src/utils/__tests__/get-env-variables.spec.ts b/packages/cli/create-medusa-app/src/utils/__tests__/get-env-variables.spec.ts new file mode 100644 index 0000000000000..bba7fe2fff132 --- /dev/null +++ b/packages/cli/create-medusa-app/src/utils/__tests__/get-env-variables.spec.ts @@ -0,0 +1,67 @@ +import getEnvVariables, { ADMIN_CORS, AUTH_CORS, DEFAULT_REDIS_URL, STORE_CORS } from "../get-env-variables.js" + +const envRegex = { + medusaOnboarding: /MEDUSA_ADMIN_ONBOARDING_TYPE=(?.+)/, + storeCors: /STORE_CORS=(?.+)/, + adminCors: /ADMIN_CORS=(?.+)/, + authCors: /AUTH_CORS=(?.+)/, + redisUrl: /REDIS_URL=(?.+)/, + jwtSecret: /JWT_SECRET=(?.+)/, + cookieSecret: /COOKIE_SECRET=(?.+)/, + db: /DATABASE_URL=(?.+)/, + postgresUrl: /POSTGRES_URL=(?.+)/, + onboardingNextjs: /MEDUSA_ADMIN_ONBOARDING_NEXTJS_DIRECTORY=(?.+)/, +} + +describe("get-env-variables", () => { + it("should retrieve default environment variables", () => { + const envVariables = getEnvVariables({ + // The URL itself doesn't matter here + dbConnectionString: "example-url" + }) + + const onboardingTypeMatch = envVariables.match(envRegex.medusaOnboarding) + expect(onboardingTypeMatch?.groups?.value).toEqual("default") + + const storeCorsMatch = envVariables.match(envRegex.storeCors) + expect(storeCorsMatch?.groups?.value).toEqual(STORE_CORS) + + const adminCorsMatch = envVariables.match(envRegex.adminCors) + expect(adminCorsMatch?.groups?.value).toEqual(ADMIN_CORS) + + const authCorsMatch = envVariables.match(envRegex.authCors) + expect(authCorsMatch?.groups?.value).toEqual(AUTH_CORS) + + const redisUrlMatch = envVariables.match(envRegex.redisUrl) + expect(redisUrlMatch?.groups?.value).toEqual(DEFAULT_REDIS_URL) + + const jwtSecretMatch = envVariables.match(envRegex.jwtSecret) + expect(jwtSecretMatch?.groups?.value).toEqual("supersecret") + + const cookieSecretMatch = envVariables.match(envRegex.cookieSecret) + expect(cookieSecretMatch?.groups?.value).toEqual("supersecret") + + expect(envVariables).toMatch(envRegex.db) + expect(envVariables).toMatch(envRegex.postgresUrl) + expect(envVariables).not.toMatch(envRegex.onboardingNextjs) + }) + + it("shouldn't retrieve db variables", () => { + const envVariables = getEnvVariables({ + dbConnectionString: "example-url", + skipDb: true + }) + + expect(envVariables).not.toMatch(envRegex.db) + expect(envVariables).not.toMatch(envRegex.postgresUrl) + }) + + it("should set next.js onboarding env variable", () => { + const envVariables = getEnvVariables({ + dbConnectionString: "example-url", + nextjsDirectory: "test" + }) + + expect(envVariables).toMatch(envRegex.onboardingNextjs) + }) +}) \ No newline at end of file diff --git a/packages/cli/create-medusa-app/src/utils/__tests__/postgres-client.spec.ts b/packages/cli/create-medusa-app/src/utils/__tests__/postgres-client.spec.ts new file mode 100644 index 0000000000000..52bee57b37809 --- /dev/null +++ b/packages/cli/create-medusa-app/src/utils/__tests__/postgres-client.spec.ts @@ -0,0 +1,27 @@ +import postgresClient from "../postgres-client.js" + +describe("postgres-client", () => { + it("should retrieve a connected client", async () => { + expect.assertions(1) + return expect((async () => { + const client = await postgresClient({ + user: process.env.POSTGRES_USER || "postgres", + password: process.env.POSTGRES_PASSWORD || "", + host: process.env.POSTGRES_HOST || "localhost", + port: parseInt(process.env.POSTGRES_PORT || "5432") + }) + + await client.end() + })()).resolves.toBeUndefined() + }) + + it("shouldn't connect to database", async () => { + expect.assertions(1) + return expect((async () => { + await postgresClient({ + user: "test", + password: "test" + }) + })()).rejects.toThrow() + }) +}) \ No newline at end of file diff --git a/packages/cli/create-medusa-app/src/utils/clone-repo.ts b/packages/cli/create-medusa-app/src/utils/clone-repo.ts index 46252abfb800d..3f30d81ffee56 100644 --- a/packages/cli/create-medusa-app/src/utils/clone-repo.ts +++ b/packages/cli/create-medusa-app/src/utils/clone-repo.ts @@ -15,7 +15,7 @@ type CloneRepoOptions = { const DEFAULT_REPO = "https://github.com/medusajs/medusa-starter-default" const V2_BRANCH = "feat/v2" -export default async function cloneRepo({ +export async function cloneRepo({ directoryName = "", repoUrl, abortController, @@ -32,7 +32,7 @@ export default async function cloneRepo({ ) } -export async function runCloneRepo({ +export default async function runCloneRepo({ projectName, repoUrl, abortController, @@ -40,7 +40,7 @@ export async function runCloneRepo({ verbose = false, }: { projectName: string - repoUrl: string + repoUrl?: string abortController: AbortController spinner: Ora verbose?: boolean diff --git a/packages/cli/create-medusa-app/src/utils/create-db.ts b/packages/cli/create-medusa-app/src/utils/create-db.ts index 69a69e5c66336..139916b9bee2e 100644 --- a/packages/cli/create-medusa-app/src/utils/create-db.ts +++ b/packages/cli/create-medusa-app/src/utils/create-db.ts @@ -12,11 +12,11 @@ type CreateDbOptions = { db: string } -export default async function createDb({ client, db }: CreateDbOptions) { +export async function createDb({ client, db }: CreateDbOptions) { await client.query(`CREATE DATABASE "${db}"`) } -export async function runCreateDb({ +export default async function runCreateDb({ client, dbName, spinner, diff --git a/packages/cli/create-medusa-app/src/utils/execute.ts b/packages/cli/create-medusa-app/src/utils/execute.ts index c5a93f15048a8..b5ea20368d7bf 100644 --- a/packages/cli/create-medusa-app/src/utils/execute.ts +++ b/packages/cli/create-medusa-app/src/utils/execute.ts @@ -45,7 +45,6 @@ const execute = async ( childProcess.signal && ["SIGINT", "SIGTERM"].includes(childProcess.signal) ) { - console.log("abortingggg") throw getAbortError() } diff --git a/packages/cli/create-medusa-app/src/utils/get-env-variables.ts b/packages/cli/create-medusa-app/src/utils/get-env-variables.ts new file mode 100644 index 0000000000000..ffa919de9260c --- /dev/null +++ b/packages/cli/create-medusa-app/src/utils/get-env-variables.ts @@ -0,0 +1,28 @@ +import { EOL } from "os" +import { PrepareOptions } from "./prepare-project.js" + +export const STORE_CORS = "http://localhost:8000,https://docs.medusajs.com,https://medusa-docs-v2-git-docs-v2-medusajs.vercel.app,https://medusa-resources-git-docs-v2-medusajs.vercel.app" +export const ADMIN_CORS = "http://localhost:7000,http://localhost:7001,https://docs.medusajs.com,https://medusa-docs-v2-git-docs-v2-medusajs.vercel.app,https://medusa-resources-git-docs-v2-medusajs.vercel.app" +export const AUTH_CORS = ADMIN_CORS +export const DEFAULT_REDIS_URL = "redis://localhost:6379" + +type Options = Pick + +export default function getEnvVariables({ + onboardingType = "default", + skipDb, + dbConnectionString, + nextjsDirectory +}: Options): string { + 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` + + if (!skipDb) { + env += `${EOL}DATABASE_URL=${dbConnectionString}${EOL}POSTGRES_URL=${dbConnectionString}` + } + + if (nextjsDirectory) { + env += `${EOL}MEDUSA_ADMIN_ONBOARDING_NEXTJS_DIRECTORY=${nextjsDirectory}` + } + + return env +} \ No newline at end of file diff --git a/packages/cli/create-medusa-app/src/utils/prepare-project.ts b/packages/cli/create-medusa-app/src/utils/prepare-project.ts index 659e25c78de2b..1d69c0caa6724 100644 --- a/packages/cli/create-medusa-app/src/utils/prepare-project.ts +++ b/packages/cli/create-medusa-app/src/utils/prepare-project.ts @@ -2,20 +2,15 @@ import fs from "fs" import path from "path" import { Ora } from "ora" import execute from "./execute.js" -import { EOL } from "os" import { displayFactBox, FactBoxOptions } from "./facts.js" import ProcessManager from "./process-manager.js" import { clearProject } from "./clear-project.js" import type { Client } from "pg" +import getEnvVariables from "./get-env-variables.js" const ADMIN_EMAIL = "admin@medusa-test.com" -// TODO remove preview links once we move to main docs -const STORE_CORS = "http://localhost:8000,https://docs.medusajs.com,https://medusa-docs-v2-git-docs-v2-medusajs.vercel.app,https://medusa-resources-git-docs-v2-medusajs.vercel.app" -const ADMIN_CORS = "http://localhost:7000,http://localhost:7001,https://docs.medusajs.com,https://medusa-docs-v2-git-docs-v2-medusajs.vercel.app,https://medusa-resources-git-docs-v2-medusajs.vercel.app" -const AUTH_CORS = ADMIN_CORS -const DEFAULT_REDIS_URL = "redis://localhost:6379" -type PrepareOptions = { +export type PrepareOptions = { directory: string dbConnectionString: string seed?: boolean @@ -73,15 +68,12 @@ export default async ({ let inviteToken: string | undefined = undefined // add environment variables - 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` - - if (!skipDb) { - env += `${EOL}DATABASE_URL=${dbConnectionString}${EOL}POSTGRES_URL=${dbConnectionString}` - } - - if (nextjsDirectory) { - env += `${EOL}MEDUSA_ADMIN_ONBOARDING_NEXTJS_DIRECTORY=${nextjsDirectory}` - } + let env = getEnvVariables({ + onboardingType, + skipDb, + dbConnectionString, + nextjsDirectory + }) fs.appendFileSync(path.join(directory, `.env`), env) diff --git a/packages/cli/create-medusa-app/tsconfig.spec.json b/packages/cli/create-medusa-app/tsconfig.spec.json new file mode 100644 index 0000000000000..25a9c9968cc36 --- /dev/null +++ b/packages/cli/create-medusa-app/tsconfig.spec.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 22d803599ca9a..f24b54ad09690 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14123,12 +14123,14 @@ __metadata: chalk: ^5.2.0 commander: ^10.0.1 configstore: ^6.0.0 + cross-env: ^7.0.3 eslint: ^8.40.0 eslint-config-google: ^0.14.0 eslint-config-prettier: ^8.8.0 eslint-plugin-prettier: ^4.2.1 glob: ^7.1.6 inquirer: ^9.2.2 + jest: ^29.7.0 medusa-telemetry: ^0.0.17 nanoid: ^4.0.2 node-emoji: ^2.0.2 @@ -14138,6 +14140,7 @@ __metadata: pg: ^8.10.0 prettier: ^2.8.8 slugify: ^1.6.6 + ts-jest: ^29.1.4 ts-node: ^10.9.1 typescript: ^5.0.4 uuid: ^9.0.0 @@ -28014,6 +28017,42 @@ __metadata: languageName: node linkType: hard +"ts-jest@npm:^29.1.4": + version: 29.1.4 + resolution: "ts-jest@npm:29.1.4" + dependencies: + bs-logger: 0.x + fast-json-stable-stringify: 2.x + jest-util: ^29.0.0 + json5: ^2.2.3 + lodash.memoize: 4.x + make-error: 1.x + semver: ^7.5.3 + yargs-parser: ^21.0.1 + peerDependencies: + "@babel/core": ">=7.0.0-beta.0 <8" + "@jest/transform": ^29.0.0 + "@jest/types": ^29.0.0 + babel-jest: ^29.0.0 + jest: ^29.0.0 + typescript: ">=4.3 <6" + peerDependenciesMeta: + "@babel/core": + optional: true + "@jest/transform": + optional: true + "@jest/types": + optional: true + babel-jest: + optional: true + esbuild: + optional: true + bin: + ts-jest: cli.js + checksum: 97def10be26a553e529dfacafe264fa9833d638052bc2b1ebe6301262ae2d3e43954f4d91f2d2d07cf92352cdd4fa163a86f8116a1f6bb8cef7060cddfec794b + languageName: node + linkType: hard + "ts-node@npm:^10.9.1": version: 10.9.2 resolution: "ts-node@npm:10.9.2"