From 1560d7ed5f5bc1900dbe36dca37f8a3566600435 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 9 Oct 2024 21:29:08 +0530 Subject: [PATCH] breaking: Standalone builds (#9496) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: FRMW-2742 In this PR, we fix the build output of the backend source code, which eliminates a lot of magic between the development and production environments. Right now, we only compile the source files from the `src` directory and write them within the `dist` directory. **Here's how the `src` directory with a custom module looks like** ``` src ├── modules │   └── hello │   ├── index.ts ``` **Here's the build output** ``` dist ├── modules │   └── hello │   ├── index.js ``` Let's imagine a file at the root of your project (maybe the `medusa-config.js` file) that wants to import the `modules/hello/index` file. How can we ensure that the import will work in both the development and production environments? If we write the import targeting the `src` directory, it will break in production because it should target the `dist` directory. ## Solution The solution is to compile everything within the project and mimic the file structure in the build output, not just the `src` directory. **Here's how the fixed output should look like** ``` dist ├── src │  ├── modules │  │   └── hello │  │   ├── index.js ├── medusa-config.js ├── yarn.lock ├── package.json ``` If you notice carefully, we also have `medusa-config.js`, `yarn.lock`, and `package.json` within the `dist` directory. We do so to create a standalone built application, something you can copy/paste to your server and run without relying on the original source code. - This results in small containers since you are not copying unnecessary files. - Clear distinction between the development and the production code. If you want to run the production server, then `cd` into the `dist` directory and run it from there. ## Changes in the PR - Breaking: Remove the `dist` and `build` folders. Instead, write them production artefacts within the `.medusa` directory as `.medusa/admin` and `.medusa/server`. - Breaking: Change the output of the `.medusa/server` folder to mimic the root project structure. - Refactor: Remove `Symbol.for("ts-node.register.instance")]` check to find from where to load the source code. - Refactor: Use `tsc` for creating the production build. This ensures we respect `tsconfig` settings when creating the build and also perform type-checking. Co-authored-by: Adrien de Peretti <25098370+adrien2p@users.noreply.github.com> --- .../modules/__tests__/event-bus/index.spec.ts | 2 +- integration-tests/modules/medusa-config.js | 3 +- integration-tests/modules/tsconfig.json | 3 +- integration-tests/tsconfig.json | 8 +- .../src/config/__tests__/index.spec.ts | 6 +- packages/core/framework/src/config/loader.ts | 11 +- .../src/http/__fixtures__/mocks/index.ts | 4 +- packages/core/modules-sdk/package.json | 3 +- .../test-service-resolved/index.ts | 1 + .../__fixtures__/test-service/index.ts | 1 + .../src/loaders/__tests__/register-modules.ts | 17 +- .../src/loaders/register-modules.ts | 5 +- .../core/types/src/common/config-module.ts | 6 +- .../common/__tests__/define-config.spec.ts | 8 + .../core/utils/src/common/define-config.ts | 10 +- .../core/utils/src/common/get-config-file.ts | 31 +- .../normalize-import-path-with-source.ts | 18 +- .../utils/src/graphql/graphql-to-ts-types.ts | 2 +- .../src/medusa-test-runner-utils/config.ts | 2 +- packages/medusa/src/commands/build.ts | 341 +++++++++++------- packages/medusa/src/commands/start.ts | 8 +- packages/medusa/src/loaders/admin.ts | 2 - .../src/loaders/helpers/resolve-plugins.ts | 9 +- packages/medusa/src/loaders/index.ts | 2 +- .../__tests__/index-engine-module.spec.ts | 4 +- .../__tests__/query-builder.spec.ts | 4 +- yarn.lock | 1 - 27 files changed, 292 insertions(+), 220 deletions(-) create mode 100644 packages/core/modules-sdk/src/loaders/__fixtures__/test-service-resolved/index.ts create mode 100644 packages/core/modules-sdk/src/loaders/__fixtures__/test-service/index.ts diff --git a/integration-tests/modules/__tests__/event-bus/index.spec.ts b/integration-tests/modules/__tests__/event-bus/index.spec.ts index 61e7b60cdd..3f8a8e02fa 100644 --- a/integration-tests/modules/__tests__/event-bus/index.spec.ts +++ b/integration-tests/modules/__tests__/event-bus/index.spec.ts @@ -1,7 +1,7 @@ import { MedusaContainer } from "@medusajs/types" import { Modules, composeMessage } from "@medusajs/utils" import { medusaIntegrationTestRunner } from "medusa-test-utils" -import testEventPayloadHandlerMock from "../../dist/subscribers/test-event-payload" +import testEventPayloadHandlerMock from "../../src/subscribers/test-event-payload" jest.setTimeout(30000) diff --git a/integration-tests/modules/medusa-config.js b/integration-tests/modules/medusa-config.js index 030678e49a..08f890690b 100644 --- a/integration-tests/modules/medusa-config.js +++ b/integration-tests/modules/medusa-config.js @@ -1,5 +1,5 @@ const { Modules } = require("@medusajs/utils") -const { FulfillmentModuleOptions } = require("@medusajs/fulfillment") + const DB_HOST = process.env.DB_HOST const DB_USERNAME = process.env.DB_USERNAME const DB_PASSWORD = process.env.DB_PASSWORD @@ -78,7 +78,6 @@ module.exports = { [Modules.SALES_CHANNEL]: true, [Modules.CART]: true, [Modules.WORKFLOW_ENGINE]: true, - [Modules.REGION]: true, [Modules.API_KEY]: true, [Modules.STORE]: true, [Modules.TAX]: true, diff --git a/integration-tests/modules/tsconfig.json b/integration-tests/modules/tsconfig.json index 4ed062890e..55e69b0614 100644 --- a/integration-tests/modules/tsconfig.json +++ b/integration-tests/modules/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../../_tsconfig.base.json", "include": ["src", "./medusa/**/*"], "exclude": [ - "./dist/**/*", + "dist", "__tests__", "helpers", "./**/helpers", @@ -10,4 +10,3 @@ "node_modules" ] } - diff --git a/integration-tests/tsconfig.json b/integration-tests/tsconfig.json index 4569ac3529..1f73dce637 100644 --- a/integration-tests/tsconfig.json +++ b/integration-tests/tsconfig.json @@ -1,11 +1,5 @@ { "extends": "../_tsconfig.base.json", "include": ["**/*"], - "exclude": [ - "dist", - "./**/helpers", - "./**/__snapshots__", - "node_modules" - ] + "exclude": ["dist", "./**/helpers", "./**/__snapshots__", "node_modules"] } - diff --git a/packages/core/framework/src/config/__tests__/index.spec.ts b/packages/core/framework/src/config/__tests__/index.spec.ts index 4aa8305afe..b38b5556ad 100644 --- a/packages/core/framework/src/config/__tests__/index.spec.ts +++ b/packages/core/framework/src/config/__tests__/index.spec.ts @@ -13,14 +13,14 @@ describe("configLoader", () => { expect(configModule).toBeUndefined() - configLoader(entryDirectory, "medusa-config.js") + configLoader(entryDirectory, "medusa-config") configModule = container.resolve(ContainerRegistrationKeys.CONFIG_MODULE) expect(configModule).toBeDefined() expect(configModule.projectConfig.databaseName).toBeUndefined() - configLoader(entryDirectory, "medusa-config-2.js") + configLoader(entryDirectory, "medusa-config-2") configModule = container.resolve(ContainerRegistrationKeys.CONFIG_MODULE) @@ -30,7 +30,7 @@ describe("configLoader", () => { process.env.MEDUSA_WORKER_MODE = "worker" - configLoader(entryDirectory, "medusa-config-2.js") + configLoader(entryDirectory, "medusa-config-2") configModule = container.resolve(ContainerRegistrationKeys.CONFIG_MODULE) diff --git a/packages/core/framework/src/config/loader.ts b/packages/core/framework/src/config/loader.ts index 9b8a438e27..698b64a761 100644 --- a/packages/core/framework/src/config/loader.ts +++ b/packages/core/framework/src/config/loader.ts @@ -30,17 +30,14 @@ export function configLoader( entryDirectory: string, configFileName: string ): ConfigModule { - const { configModule, error } = getConfigFile( - entryDirectory, - configFileName - ) + const config = getConfigFile(entryDirectory, configFileName) - if (error) { - handleConfigError(error) + if (config.error) { + handleConfigError(config.error) } return configManager.loadConfig({ - projectConfig: configModule, + projectConfig: config.configModule!, baseDir: entryDirectory, }) } diff --git a/packages/core/framework/src/http/__fixtures__/mocks/index.ts b/packages/core/framework/src/http/__fixtures__/mocks/index.ts index 2d48ddbce8..0d615a7682 100644 --- a/packages/core/framework/src/http/__fixtures__/mocks/index.ts +++ b/packages/core/framework/src/http/__fixtures__/mocks/index.ts @@ -4,7 +4,7 @@ export const customersGlobalMiddlewareMock = jest.fn() export const customersCreateMiddlewareMock = jest.fn() export const storeGlobalMiddlewareMock = jest.fn() -export const config: ConfigModule = { +export const config = { projectConfig: { databaseLogging: false, http: { @@ -17,4 +17,4 @@ export const config: ConfigModule = { }, featureFlags: {}, plugins: [], -} +} satisfies Partial diff --git a/packages/core/modules-sdk/package.json b/packages/core/modules-sdk/package.json index 136b7cf6be..71e8a0cf8d 100644 --- a/packages/core/modules-sdk/package.json +++ b/packages/core/modules-sdk/package.json @@ -47,8 +47,7 @@ "dependencies": { "@medusajs/orchestration": "^0.5.7", "@medusajs/types": "^1.11.16", - "@medusajs/utils": "^1.11.9", - "resolve-cwd": "^3.0.0" + "@medusajs/utils": "^1.11.9" }, "peerDependencies": { "@mikro-orm/core": "5.9.7", diff --git a/packages/core/modules-sdk/src/loaders/__fixtures__/test-service-resolved/index.ts b/packages/core/modules-sdk/src/loaders/__fixtures__/test-service-resolved/index.ts new file mode 100644 index 0000000000..b1c6ea436a --- /dev/null +++ b/packages/core/modules-sdk/src/loaders/__fixtures__/test-service-resolved/index.ts @@ -0,0 +1 @@ +export default {} diff --git a/packages/core/modules-sdk/src/loaders/__fixtures__/test-service/index.ts b/packages/core/modules-sdk/src/loaders/__fixtures__/test-service/index.ts new file mode 100644 index 0000000000..b1c6ea436a --- /dev/null +++ b/packages/core/modules-sdk/src/loaders/__fixtures__/test-service/index.ts @@ -0,0 +1 @@ +export default {} diff --git a/packages/core/modules-sdk/src/loaders/__tests__/register-modules.ts b/packages/core/modules-sdk/src/loaders/__tests__/register-modules.ts index 25b8160d72..f62d3b7000 100644 --- a/packages/core/modules-sdk/src/loaders/__tests__/register-modules.ts +++ b/packages/core/modules-sdk/src/loaders/__tests__/register-modules.ts @@ -3,13 +3,15 @@ import { ModulesDefinition } from "../../definitions" import { MODULE_RESOURCE_TYPE, MODULE_SCOPE } from "../../types" import { registerMedusaModule } from "../register-modules" -const RESOLVED_PACKAGE = "@medusajs/test-service-resolved" -jest.mock("resolve-cwd", () => jest.fn(() => RESOLVED_PACKAGE)) +const testServiceResolved = require.resolve( + "../__fixtures__/test-service-resolved" +) +const defaultTestService = require.resolve("../__fixtures__/test-service") describe("module definitions loader", () => { const defaultDefinition: ModuleDefinition = { key: "testService", - defaultPackage: "@medusajs/test-service", + defaultPackage: defaultTestService, label: "TestService", isRequired: false, defaultModuleDeclaration: { @@ -51,6 +53,7 @@ describe("module definitions loader", () => { it("Resolves a custom module without pre-defined definition", () => { const res = registerMedusaModule("customModulesABC", { + resolve: testServiceResolved, options: { test: 123, }, @@ -58,7 +61,7 @@ describe("module definitions loader", () => { expect(res).toEqual({ customModulesABC: expect.objectContaining({ - resolutionPath: "@medusajs/test-service-resolved", + resolutionPath: testServiceResolved, definition: expect.objectContaining({ key: "customModulesABC", label: "Custom: customModulesABC", @@ -146,7 +149,7 @@ describe("module definitions loader", () => { expect(res[defaultDefinition.key]).toEqual( expect.objectContaining({ - resolutionPath: RESOLVED_PACKAGE, + resolutionPath: defaultTestService, definition: defaultDefinition, options: {}, moduleDeclaration: { @@ -172,7 +175,7 @@ describe("module definitions loader", () => { expect(res[defaultDefinition.key]).toEqual( expect.objectContaining({ - resolutionPath: RESOLVED_PACKAGE, + resolutionPath: defaultTestService, definition: defaultDefinition, options: {}, moduleDeclaration: { @@ -221,7 +224,7 @@ describe("module definitions loader", () => { expect(res[defaultDefinition.key]).toEqual( expect.objectContaining({ - resolutionPath: RESOLVED_PACKAGE, + resolutionPath: defaultTestService, definition: defaultDefinition, options: { test: 123 }, moduleDeclaration: { diff --git a/packages/core/modules-sdk/src/loaders/register-modules.ts b/packages/core/modules-sdk/src/loaders/register-modules.ts index 02d2891717..b2162ec2df 100644 --- a/packages/core/modules-sdk/src/loaders/register-modules.ts +++ b/packages/core/modules-sdk/src/loaders/register-modules.ts @@ -11,7 +11,6 @@ import { isString, normalizeImportPathWithSource, } from "@medusajs/utils" -import resolveCwd from "resolve-cwd" import { ModulesDefinition } from "../definitions" import { MODULE_RESOURCE_TYPE, MODULE_SCOPE } from "../types" @@ -68,7 +67,7 @@ function getCustomModuleResolution( const originalPath = normalizeImportPathWithSource( (isString(moduleConfig) ? moduleConfig : moduleConfig.resolve) as string ) - const resolutionPath = resolveCwd(originalPath) + const resolutionPath = require.resolve(originalPath) const conf = isObject(moduleConfig) ? moduleConfig @@ -143,7 +142,7 @@ function getInternalModuleResolution( const originalPath = normalizeImportPathWithSource( (isString(moduleConfig) ? moduleConfig : moduleConfig.resolve) as string ) - resolutionPath = resolveCwd(originalPath) + resolutionPath = require.resolve(originalPath) } const moduleDeclaration = isObj ? moduleConfig : {} diff --git a/packages/core/types/src/common/config-module.ts b/packages/core/types/src/common/config-module.ts index 941ca7b2d9..65368072c0 100644 --- a/packages/core/types/src/common/config-module.ts +++ b/packages/core/types/src/common/config-module.ts @@ -48,7 +48,7 @@ export interface AdminOptions { * }) * ``` */ - path?: `/${string}` + path: `/${string}` /** * The directory where the admin build is outputted when you run the `build` command. * The default value is `./build`. @@ -63,7 +63,7 @@ export interface AdminOptions { * }) * ``` */ - outDir?: string + outDir: string /** * The URL of your Medusa application. This is useful to set when you deploy the Medusa application. * @@ -828,7 +828,7 @@ export type ConfigModule = { * }) * ``` */ - admin?: AdminOptions + admin: AdminOptions /** * On your Medusa backend, you can use [Plugins](https://docs.medusajs.com/development/plugins/overview) to add custom features or integrate third-party services. diff --git a/packages/core/utils/src/common/__tests__/define-config.spec.ts b/packages/core/utils/src/common/__tests__/define-config.spec.ts index 0a07a9b37e..046eccc547 100644 --- a/packages/core/utils/src/common/__tests__/define-config.spec.ts +++ b/packages/core/utils/src/common/__tests__/define-config.spec.ts @@ -7,6 +7,8 @@ describe("defineConfig", function () { { "admin": { "backendUrl": "http://localhost:9000", + "outDir": ".medusa/admin", + "path": "/app", }, "featureFlags": {}, "modules": { @@ -113,6 +115,8 @@ describe("defineConfig", function () { { "admin": { "backendUrl": "http://localhost:9000", + "outDir": ".medusa/admin", + "path": "/app", }, "featureFlags": {}, "modules": { @@ -222,6 +226,8 @@ describe("defineConfig", function () { { "admin": { "backendUrl": "http://localhost:9000", + "outDir": ".medusa/admin", + "path": "/app", }, "featureFlags": {}, "modules": { @@ -331,6 +337,8 @@ describe("defineConfig", function () { { "admin": { "backendUrl": "http://localhost:9000", + "outDir": ".medusa/admin", + "path": "/app", }, "featureFlags": {}, "modules": { diff --git a/packages/core/utils/src/common/define-config.ts b/packages/core/utils/src/common/define-config.ts index a5290dd709..45856e71c1 100644 --- a/packages/core/utils/src/common/define-config.ts +++ b/packages/core/utils/src/common/define-config.ts @@ -16,7 +16,13 @@ const DEFAULT_ADMIN_CORS = * make an application work seamlessly, but still provide you the ability * to override configuration as needed. */ -export function defineConfig(config: Partial = {}): ConfigModule { +export function defineConfig( + config: Partial< + Omit & { + admin: Partial + } + > = {} +): ConfigModule { const { http, ...restOfProjectConfig } = config.projectConfig || {} /** @@ -42,6 +48,8 @@ export function defineConfig(config: Partial = {}): ConfigModule { */ const admin: ConfigModule["admin"] = { backendUrl: process.env.MEDUSA_BACKEND_URL || DEFAULT_ADMIN_URL, + outDir: ".medusa/admin", + path: "/app", ...config.admin, } diff --git a/packages/core/utils/src/common/get-config-file.ts b/packages/core/utils/src/common/get-config-file.ts index f80bf7c3bc..1983f781b7 100644 --- a/packages/core/utils/src/common/get-config-file.ts +++ b/packages/core/utils/src/common/get-config-file.ts @@ -9,22 +9,27 @@ import { join } from "path" export function getConfigFile( rootDir: string, configName: string -): { configModule: TConfig; configFilePath: string; error?: any } { +): + | { configModule: null; configFilePath: string; error: Error } + | { configModule: TConfig; configFilePath: string; error: null } { const configPath = join(rootDir, configName) - let configFilePath = `` - let configModule - let err try { - configFilePath = require.resolve(configPath) - configModule = require(configFilePath) + const configFilePath = require.resolve(configPath) + const configExports = require(configFilePath) + return { + configModule: + configExports && "default" in configExports + ? configExports.default + : configExports, + configFilePath, + error: null, + } } catch (e) { - err = e + return { + configModule: null, + configFilePath: "", + error: e, + } } - - if (configModule && typeof configModule.default === "object") { - configModule = configModule.default - } - - return { configModule, configFilePath, error: err } } diff --git a/packages/core/utils/src/common/normalize-import-path-with-source.ts b/packages/core/utils/src/common/normalize-import-path-with-source.ts index 03b278f063..a295570371 100644 --- a/packages/core/utils/src/common/normalize-import-path-with-source.ts +++ b/packages/core/utils/src/common/normalize-import-path-with-source.ts @@ -9,16 +9,16 @@ export function normalizeImportPathWithSource( ): string { let normalizePath = path - /** - * If the project is running on ts-node all relative module resolution - * will target the src directory and otherwise the dist directory. - * If the path is not relative, then we can safely import from it and let the resolution - * happen under the hood. - */ if (normalizePath?.startsWith("./")) { - const sourceDir = process[Symbol.for("ts-node.register.instance")] - ? "src" - : "dist" + /** + * If someone is using the correct path pointing to the "src" directory + * then we are all good. Otherwise we will point to the "src" directory. + * + * In case of the production output. The app should be executed from within + * the "./build" directory and the "./build" directory will have the + * "./src" directory inside it. + */ + let sourceDir = normalizePath.startsWith("./src") ? "./" : "./src" normalizePath = join(process.cwd(), sourceDir, normalizePath) } diff --git a/packages/core/utils/src/graphql/graphql-to-ts-types.ts b/packages/core/utils/src/graphql/graphql-to-ts-types.ts index 0572c9f200..b449f6f252 100644 --- a/packages/core/utils/src/graphql/graphql-to-ts-types.ts +++ b/packages/core/utils/src/graphql/graphql-to-ts-types.ts @@ -54,7 +54,7 @@ async function generateTypes({ }) { const fileSystem = new FileSystem(outputDir) - let output = 'import "@medusajs/framework/types\n' + let output = 'import "@medusajs/framework/types"\n' output += await codegen(config) const entryPoints = buildEntryPointsTypeMap({ schema: output, joinerConfigs }) diff --git a/packages/medusa-test-utils/src/medusa-test-runner-utils/config.ts b/packages/medusa-test-utils/src/medusa-test-runner-utils/config.ts index 6af6315b2f..c4b2ee68a2 100644 --- a/packages/medusa-test-utils/src/medusa-test-runner-utils/config.ts +++ b/packages/medusa-test-utils/src/medusa-test-runner-utils/config.ts @@ -7,7 +7,7 @@ export async function configLoaderOverride( const { configManager } = await import("@medusajs/framework/config") const { configModule, error } = getConfigFile< ReturnType - >(entryDirectory, "medusa-config.js") + >(entryDirectory, "medusa-config") if (error) { throw new Error(error.message || "Error during config loading") diff --git a/packages/medusa/src/commands/build.ts b/packages/medusa/src/commands/build.ts index c76767d6ea..fe49314a50 100644 --- a/packages/medusa/src/commands/build.ts +++ b/packages/medusa/src/commands/build.ts @@ -1,170 +1,237 @@ +import path from "path" +import { rm, copyFile, access, constants } from "node:fs/promises" +import type tsStatic from "typescript" +import { logger } from "@medusajs/framework/logger" import { ConfigModule } from "@medusajs/framework/types" import { getConfigFile } from "@medusajs/framework/utils" -import { transformFile } from "@swc/core" -import { existsSync } from "node:fs" -import { copyFile, mkdir, readdir, rm, writeFile } from "node:fs/promises" -import path from "path" -type BuildArgs = { - directory: string +const ADMIN_FOLDER = "src/admin" +const INTEGRATION_TESTS_FOLDER = "integration-tests" + +/** + * Copies the file to the destination without throwing any + * errors if the source file is missing + */ +async function copy(source: string, destination: string) { + let sourceExists = false + try { + await access(source, constants.F_OK) + sourceExists = true + } catch (error) { + if (error.code !== "ENOENT") { + throw error + } + } + + if (sourceExists) { + await copyFile(path.join(source), path.join(destination)) + } } -type FileConfig = { - inputDir: string - outputDir: string - targetExtension?: string -} - -const INPUT_DIR = "./src" -const OUTPUT_DIR = "./dist" - -const COMPILE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"] -const IGNORE_EXTENSIONS = [".md"] - +/** + * Removes the directory and its children recursively and + * ignores any errors + */ async function clean(path: string) { await rm(path, { recursive: true }).catch(() => {}) } -async function findFiles(dir: string): Promise { - try { - const files = await readdir(dir, { withFileTypes: true }) - const paths = await Promise.all( - files.map(async (file) => { - const res = path.join(dir, file.name) - return file.isDirectory() ? findFiles(res) : res - }) - ) - return paths.flat() - } catch (e) { - console.error(`Failed to read directory ${dir}`) - console.error(e) - throw e - } -} - -const getOutputPath = (file: string, config: FileConfig) => { - const { inputDir, outputDir, targetExtension } = config - - const inputDirName = path.basename(inputDir) - const outputDirName = path.basename(outputDir) - - const relativePath = file.replace(inputDirName, outputDirName) - let outputPath = relativePath - - if (targetExtension) { - const currentExtension = path.extname(outputPath) - outputPath = outputPath.replace(currentExtension, targetExtension) - } - - return outputPath.replaceAll(path.sep, "/") -} - -const writeToOut = async ( - file: string, - content: string, - config: FileConfig -) => { - const outputPath = getOutputPath(file, config) - - await mkdir(outputPath.replace(/\/[^/]+$/, ""), { recursive: true }) - await writeFile(outputPath, content) -} - -async function copyToOut(file: string, config: FileConfig) { - const outputPath = getOutputPath(file, config) - - await mkdir(outputPath.replace(/\/[^/]+$/, ""), { recursive: true }) - await copyFile(file, outputPath) -} - -const medusaTransform = async (file: string) => { - if (COMPILE_EXTENSIONS.some((ext) => file.endsWith(ext))) { - const outputPath = getOutputPath(file, { - inputDir: INPUT_DIR, - outputDir: OUTPUT_DIR, - }) - const output = await transformFile(file, { - sourceFileName: path.relative(path.dirname(outputPath), file), - sourceMaps: "inline", - module: { - type: "commonjs", - }, - jsc: { - parser: { - syntax: "typescript", - decorators: true, - }, - transform: { - decoratorMetadata: true, - }, - target: "es2021", - externalHelpers: true, - }, - }) - await writeToOut(file, output.code, { - inputDir: INPUT_DIR, - outputDir: OUTPUT_DIR, - targetExtension: ".js", - }) - } else if (!IGNORE_EXTENSIONS.some((ext) => file.endsWith(ext))) { - // Copy non-ts files - await copyToOut(file, { inputDir: INPUT_DIR, outputDir: OUTPUT_DIR }) - } -} - -export default async function ({ directory }: BuildArgs) { - const started = Date.now() - - const { configModule, error } = getConfigFile( +/** + * Loads the medusa config file or exits with an error + */ +function loadMedusaConfig(directory: string) { + /** + * Parsing the medusa config file to ensure it is error + * free + */ + const { configModule, configFilePath, error } = getConfigFile( directory, "medusa-config" ) - if (error) { console.error(`Failed to load medusa-config.js`) console.error(error) - process.exit(1) + return } - const input = path.join(directory, INPUT_DIR) - const dist = path.join(directory, OUTPUT_DIR) + return { configFilePath, configModule } +} +/** + * Parses the tsconfig file or exits with an error in case + * the file is invalid + */ +function parseTSConfig(projectRoot: string, ts: typeof tsStatic) { + let tsConfigErrors: null | tsStatic.Diagnostic = null + + const tsConfig = ts.getParsedCommandLineOfConfigFile( + path.join(projectRoot, "tsconfig.json"), + { + inlineSourceMap: true, + excludes: [], + }, + { + ...ts.sys, + useCaseSensitiveFileNames: true, + getCurrentDirectory: () => projectRoot, + onUnRecoverableConfigFileDiagnostic: (error) => (tsConfigErrors = error), + } + ) + + if (tsConfigErrors) { + const compilerHost = ts.createCompilerHost({}) + console.error( + ts.formatDiagnosticsWithColorAndContext([tsConfigErrors], compilerHost) + ) + return + } + + if (tsConfig!.errors.length) { + const compilerHost = ts.createCompilerHost({}) + console.error( + ts.formatDiagnosticsWithColorAndContext(tsConfig!.errors, compilerHost) + ) + return + } + + return tsConfig! +} + +/** + * Builds the backend project using TSC + */ +async function buildBackend(projectRoot: string): Promise { + const startTime = process.hrtime() + logger.info("Compiling backend source...") + + const ts = await import("typescript") + const tsConfig = parseTSConfig(projectRoot, ts) + if (!tsConfig) { + logger.error("Unable to compile backend source") + return false + } + + const distFolder = tsConfig.options.outDir ?? ".medusa/server" + const dist = path.isAbsolute(distFolder) + ? distFolder + : path.join(projectRoot, distFolder) + + logger.info(`Removing existing "${path.relative(projectRoot, dist)}" folder`) await clean(dist) - const files = await findFiles(input) + /** + * Ignoring admin and integration tests from the compiled + * files + */ + const filesToCompile = tsConfig.fileNames.filter((fileName) => { + return ( + !fileName.includes(`${ADMIN_FOLDER}/`) && + !fileName.includes(`${INTEGRATION_TESTS_FOLDER}/`) + ) + }) - await Promise.all(files.map(medusaTransform)) + const program = ts.createProgram(filesToCompile, { + ...tsConfig.options, + ...{ + /** + * Disable inline source maps when the user has enabled + * source maps within the config file + */ + inlineSourceMap: !tsConfig.options.sourceMap, + }, + }) - const sources: string[] = [] + const emitResult = program.emit() + const diagnostics = ts + .getPreEmitDiagnostics(program) + .concat(emitResult.diagnostics) - const projectSource = path.join(directory, "src", "admin") - - if (existsSync(projectSource)) { - sources.push(projectSource) + /** + * Log errors (if any) + */ + if (diagnostics.length) { + console.error( + ts.formatDiagnosticsWithColorAndContext( + diagnostics, + ts.createCompilerHost({}) + ) + ) } + /** + * Exit early if no output is written to the disk + */ + if (emitResult.emitSkipped) { + logger.warn("Backend build completed without emitting any output") + return false + } + + /** + * Copying package manager files + */ + await copy( + path.join(projectRoot, "package.json"), + path.join(dist, "package.json") + ) + await copy(path.join(projectRoot, "yarn.lock"), path.join(dist, "yarn.lock")) + await copy(path.join(projectRoot, "pnpm.lock"), path.join(dist, "pnpm.lock")) + await copy( + path.join(projectRoot, "package-lock.json"), + path.join(dist, "package-lock.json") + ) + + const duration = process.hrtime(startTime) + const seconds = (duration[0] + duration[1] / 1e9).toFixed(2) + + if (diagnostics.length) { + logger.warn(`Backend build completed with errors (${seconds}s)`) + } else { + logger.info(`Backend build completed successfully (${seconds}s)`) + } + + return true +} + +/** + * Builds the frontend project using the "@medusajs/admin-bundler" + */ +async function buildFrontend(projectRoot: string): Promise { + const startTime = process.hrtime() + const configFile = loadMedusaConfig(projectRoot) + if (!configFile) { + return false + } + + const adminSource = path.join(projectRoot, ADMIN_FOLDER) const adminOptions = { disable: false, - path: "/app" as const, - outDir: "./build", - sources, - ...configModule.admin, + sources: [adminSource], + ...configFile.configModule.admin, } - if (!adminOptions.disable) { - try { - const { build: buildProductionBuild } = await import( - "@medusajs/admin-bundler" - ) - - await buildProductionBuild(adminOptions) - } catch (error) { - console.error("Failed to build admin") - console.error(error) - } + if (adminOptions.disable) { + return false } - const time = Date.now() - started + try { + logger.info("Compiling frontend source...") + const { build: buildProductionBuild } = await import( + "@medusajs/admin-bundler" + ) + await buildProductionBuild(adminOptions) + const duration = process.hrtime(startTime) + const seconds = (duration[0] + duration[1] / 1e9).toFixed(2) - console.log(`Build completed in ${time}ms`) + logger.info(`Frontend build completed successfully (${seconds}s)`) + return true + } catch (error) { + logger.error("Unable to compile frontend source") + console.error(error) + return false + } +} + +export default async function ({ directory }: { directory: string }) { + logger.info("Starting build...") + await Promise.all([buildBackend(directory), buildFrontend(directory)]) } diff --git a/packages/medusa/src/commands/start.ts b/packages/medusa/src/commands/start.ts index 59a74ed759..753fc61fee 100644 --- a/packages/medusa/src/commands/start.ts +++ b/packages/medusa/src/commands/start.ts @@ -17,7 +17,7 @@ import { MedusaModule } from "@medusajs/framework/modules-sdk" const EVERY_SIXTH_HOUR = "0 */6 * * *" const CRON_SCHEDULE = EVERY_SIXTH_HOUR -const INSTRUMENTATION_FILE = "instrumentation.js" +const INSTRUMENTATION_FILE = "instrumentation" /** * Imports the "instrumentation.js" file from the root of the @@ -27,7 +27,9 @@ const INSTRUMENTATION_FILE = "instrumentation.js" */ export async function registerInstrumentation(directory: string) { const fileSystem = new FileSystem(directory) - const exists = await fileSystem.exists(INSTRUMENTATION_FILE) + const exists = + (await fileSystem.exists(`${INSTRUMENTATION_FILE}.ts`)) || + (await fileSystem.exists(`${INSTRUMENTATION_FILE}.js`)) if (!exists) { return } @@ -83,7 +85,7 @@ async function start({ port, directory, types }) { }) if (gqlSchema && types) { - const outputDirGeneratedTypes = path.join(directory, ".medusa") + const outputDirGeneratedTypes = path.join(directory, ".medusa/types") await gqlSchemaToTypes({ outputDir: outputDirGeneratedTypes, filename: "remote-query-entry-points", diff --git a/packages/medusa/src/loaders/admin.ts b/packages/medusa/src/loaders/admin.ts index c9908f3872..0d2ce30bff 100644 --- a/packages/medusa/src/loaders/admin.ts +++ b/packages/medusa/src/loaders/admin.ts @@ -37,8 +37,6 @@ export default async function adminLoader({ const adminOptions: IntializedOptions = { disable: false, - path: "/app", - outDir: "./build", sources, ...admin, } diff --git a/packages/medusa/src/loaders/helpers/resolve-plugins.ts b/packages/medusa/src/loaders/helpers/resolve-plugins.ts index 52931c69aa..6d716be507 100644 --- a/packages/medusa/src/loaders/helpers/resolve-plugins.ts +++ b/packages/medusa/src/loaders/helpers/resolve-plugins.ts @@ -14,14 +14,7 @@ function createFileContentHash(path, files): string { } function getExtensionDirectoryPath() { - /** - * Grab directory for loading resources inside a starter kit from - * the medusa-config file. - * - * When using ts-node we will read resources from "src" directory - * otherwise from "dist" directory. - */ - return process[Symbol.for("ts-node.register.instance")] ? "src" : "dist" + return "src" } /** diff --git a/packages/medusa/src/loaders/index.ts b/packages/medusa/src/loaders/index.ts index c34a73d91e..a3ec7c14b3 100644 --- a/packages/medusa/src/loaders/index.ts +++ b/packages/medusa/src/loaders/index.ts @@ -118,7 +118,7 @@ async function loadEntrypoints( export async function initializeContainer( rootDirectory: string ): Promise { - configLoader(rootDirectory, "medusa-config.js") + configLoader(rootDirectory, "medusa-config") await featureFlagsLoader(join(__dirname, "feature-flags")) container.register({ diff --git a/packages/modules/index/integration-tests/__tests__/index-engine-module.spec.ts b/packages/modules/index/integration-tests/__tests__/index-engine-module.spec.ts index 470e438650..9ea6f476c7 100644 --- a/packages/modules/index/integration-tests/__tests__/index-engine-module.spec.ts +++ b/packages/modules/index/integration-tests/__tests__/index-engine-module.spec.ts @@ -14,7 +14,7 @@ import { import { EntityManager } from "@mikro-orm/postgresql" import { IndexData, IndexRelation } from "@models" import { asValue } from "awilix" -import { TestDatabaseUtils, initDb } from "medusa-test-utils" +import { initDb, TestDatabaseUtils } from "medusa-test-utils" import * as path from "path" import { EventBusServiceMock } from "../__fixtures__" import { dbName } from "../__fixtures__/medusa-config" @@ -100,7 +100,7 @@ let index!: IndexTypes.IIndexService const beforeAll_ = async () => { try { - configLoader(path.join(__dirname, "./../__fixtures__"), "medusa-config.js") + configLoader(path.join(__dirname, "./../__fixtures__"), "medusa-config") console.log(`Creating database ${dbName}`) await dbUtils.create(dbName) diff --git a/packages/modules/index/integration-tests/__tests__/query-builder.spec.ts b/packages/modules/index/integration-tests/__tests__/query-builder.spec.ts index 7eed0945db..b7482c466e 100644 --- a/packages/modules/index/integration-tests/__tests__/query-builder.spec.ts +++ b/packages/modules/index/integration-tests/__tests__/query-builder.spec.ts @@ -14,7 +14,7 @@ import { import { EntityManager } from "@mikro-orm/postgresql" import { IndexData, IndexRelation } from "@models" import { asValue } from "awilix" -import { TestDatabaseUtils, initDb } from "medusa-test-utils" +import { initDb, TestDatabaseUtils } from "medusa-test-utils" import path from "path" import { EventBusServiceMock } from "../__fixtures__" import { dbName } from "../__fixtures__/medusa-config" @@ -33,7 +33,7 @@ let medusaAppLoader!: MedusaAppLoader const beforeAll_ = async () => { try { - configLoader(path.join(__dirname, "./../__fixtures__"), "medusa-config.js") + configLoader(path.join(__dirname, "./../__fixtures__"), "medusa-config") console.log(`Creating database ${dbName}`) await dbUtils.create(dbName) diff --git a/yarn.lock b/yarn.lock index add8962f2e..8811001dc9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5837,7 +5837,6 @@ __metadata: cross-env: ^5.2.1 jest: ^29.7.0 pg: ^8.13.0 - resolve-cwd: ^3.0.0 rimraf: ^5.0.1 typescript: ^5.6.2 peerDependencies: