breaking: Standalone builds (#9496)

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>
This commit is contained in:
Harminder VirkandAdrien de Peretti authored and GitHub committed 2024-10-09 15:59:08 +00:00
1 parent 5c9457bb4f
commit 1560d7ed5f
27 files changed
+292 -220

No files matched your search

@@ -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)
+4 -7
View File
@@ -30,17 +30,14 @@ export function configLoader(
entryDirectory: string,
configFileName: string
): ConfigModule {
const { configModule, error } = getConfigFile<ConfigModule>(
entryDirectory,
configFileName
)
const config = getConfigFile<ConfigModule>(entryDirectory, configFileName)
if (error) {
handleConfigError(error)
if (config.error) {
handleConfigError(config.error)
}
return configManager.loadConfig({
projectConfig: configModule,
projectConfig: config.configModule!,
baseDir: entryDirectory,
})
}
@@ -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<ConfigModule>
+1 -2
View File
@@ -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",
@@ -0,0 +1 @@
export default {}
@@ -0,0 +1 @@
export default {}
@@ -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: {
@@ -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 : {}
@@ -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.
@@ -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": {
@@ -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> = {}): ConfigModule {
export function defineConfig(
config: Partial<
Omit<ConfigModule, "admin"> & {
admin: Partial<ConfigModule["admin"]>
}
> = {}
): ConfigModule {
const { http, ...restOfProjectConfig } = config.projectConfig || {}
/**
@@ -42,6 +48,8 @@ export function defineConfig(config: Partial<ConfigModule> = {}): ConfigModule {
*/
const admin: ConfigModule["admin"] = {
backendUrl: process.env.MEDUSA_BACKEND_URL || DEFAULT_ADMIN_URL,
outDir: ".medusa/admin",
path: "/app",
...config.admin,
}
@@ -9,22 +9,27 @@ import { join } from "path"
export function getConfigFile<TConfig = unknown>(
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 }
}
@@ -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)
}
@@ -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 })