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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"extends": "../../_tsconfig.base.json",
|
||||
"include": ["src", "./medusa/**/*"],
|
||||
"exclude": [
|
||||
"./dist/**/*",
|
||||
"dist",
|
||||
"__tests__",
|
||||
"helpers",
|
||||
"./**/helpers",
|
||||
@@ -10,4 +10,3 @@
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
{
|
||||
"extends": "../_tsconfig.base.json",
|
||||
"include": ["**/*"],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"./**/helpers",
|
||||
"./**/__snapshots__",
|
||||
"node_modules"
|
||||
]
|
||||
"exclude": ["dist", "./**/helpers", "./**/__snapshots__", "node_modules"]
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 })
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ export async function configLoaderOverride(
|
||||
const { configManager } = await import("@medusajs/framework/config")
|
||||
const { configModule, error } = getConfigFile<
|
||||
ReturnType<typeof configManager.loadConfig>
|
||||
>(entryDirectory, "medusa-config.js")
|
||||
>(entryDirectory, "medusa-config")
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message || "Error during config loading")
|
||||
|
||||
@@ -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<string[]> {
|
||||
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<ConfigModule>(
|
||||
/**
|
||||
* 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<ConfigModule>(
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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)])
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -37,8 +37,6 @@ export default async function adminLoader({
|
||||
|
||||
const adminOptions: IntializedOptions = {
|
||||
disable: false,
|
||||
path: "/app",
|
||||
outDir: "./build",
|
||||
sources,
|
||||
...admin,
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -118,7 +118,7 @@ async function loadEntrypoints(
|
||||
export async function initializeContainer(
|
||||
rootDirectory: string
|
||||
): Promise<MedusaContainer> {
|
||||
configLoader(rootDirectory, "medusa-config.js")
|
||||
configLoader(rootDirectory, "medusa-config")
|
||||
await featureFlagsLoader(join(__dirname, "feature-flags"))
|
||||
|
||||
container.register({
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user