diff --git a/.changeset/dry-insects-burn.md b/.changeset/dry-insects-burn.md new file mode 100644 index 0000000000..fb646ba2fe --- /dev/null +++ b/.changeset/dry-insects-burn.md @@ -0,0 +1,6 @@ +--- +"@medusajs/medusa": patch +"@medusajs/framework": patch +--- + +fix: switch from tsc watch to chokidar diff --git a/packages/core/framework/package.json b/packages/core/framework/package.json index ad389bf4dd..56e2b1a9cb 100644 --- a/packages/core/framework/package.json +++ b/packages/core/framework/package.json @@ -82,6 +82,7 @@ "@medusajs/workflows-sdk": "~2.3.1", "@opentelemetry/api": "^1.9.0", "@types/express": "^4.17.17", + "chokidar": "^3.4.2", "compression": "1.7.4", "connect-redis": "5.2.0", "cookie-parser": "^1.4.6", diff --git a/packages/core/framework/src/build-tools/compiler.ts b/packages/core/framework/src/build-tools/compiler.ts index a0f821eb20..a13a9b2076 100644 --- a/packages/core/framework/src/build-tools/compiler.ts +++ b/packages/core/framework/src/build-tools/compiler.ts @@ -1,8 +1,9 @@ import path from "path" -import { getConfigFile } from "@medusajs/utils" -import type { AdminOptions, ConfigModule, Logger } from "@medusajs/types" -import { rm, access, constants, copyFile, writeFile, mkdir } from "fs/promises" +import chokidar from "chokidar" import type tsStatic from "typescript" +import { FileSystem, getConfigFile } from "@medusajs/utils" +import { rm, access, constants, copyFile } from "fs/promises" +import type { AdminOptions, ConfigModule, Logger } from "@medusajs/types" /** * The compiler exposes the opinionated APIs for compiling Medusa @@ -27,7 +28,6 @@ export class Compiler { #adminSourceFolder: string #pluginsDistFolder: string #backendIgnoreFiles: string[] - #pluginOptionsPath: string #adminOnlyDistFolder: string #tsCompiler?: typeof tsStatic @@ -38,10 +38,6 @@ export class Compiler { this.#adminSourceFolder = path.join(this.#projectRoot, "src/admin") this.#adminOnlyDistFolder = path.join(this.#projectRoot, ".medusa/admin") this.#pluginsDistFolder = path.join(this.#projectRoot, ".medusa/server") - this.#pluginOptionsPath = path.join( - this.#projectRoot, - ".medusa/server/medusa-plugin-options.json" - ) this.#backendIgnoreFiles = [ "integration-tests", "test", @@ -141,13 +137,14 @@ export class Compiler { } /** - * Ensures a directory exists + * Returns a boolean indicating if a file extension belongs + * to a JavaScript or TypeScript file */ - async #ensureDir(path: string, clean?: boolean) { - if (clean) { - await this.#clean(path) + #isScriptFile(filePath: string) { + if (filePath.endsWith(".ts") && !filePath.endsWith(".d.ts")) { + return true } - await mkdir(path, { recursive: true }) + return filePath.endsWith(".js") } /** @@ -167,24 +164,6 @@ export class Compiler { return { configFilePath, configModule } } - /** - * Creates medusa-plugin-options.json file that contains some - * metadata related to the plugin, which could be helpful - * for MedusaJS loaders during development - */ - async #createPluginOptionsFile() { - await writeFile( - this.#pluginOptionsPath, - JSON.stringify( - { - srcDir: path.join(this.#projectRoot, "src"), - }, - null, - 2 - ) - ) - } - /** * Prints typescript diagnostic messages */ @@ -437,7 +416,7 @@ export class Compiler { */ const { emitResult, diagnostics } = await this.#emitBuildOutput( tsConfig, - ["integration-tests", "test", "unit-tests", "src/admin"], + this.#backendIgnoreFiles, dist ) @@ -472,54 +451,71 @@ export class Compiler { * The "onFileChange" argument can be used to get notified when * a file has changed. */ - async developPluginBackend(onFileChange?: () => void) { - await this.#ensureDir(this.#pluginsDistFolder, true) - await this.#createPluginOptionsFile() - const ts = await this.#loadTSCompiler() + async developPluginBackend( + transformer: (filePath: string) => Promise, + onFileChange?: ( + filePath: string, + action: "add" | "change" | "unlink" + ) => void + ) { + const fs = new FileSystem(this.#pluginsDistFolder) + await fs.createJson("medusa-plugin-options.json", { + srcDir: path.join(this.#projectRoot, "src"), + }) - /** - * Format host is needed to print diagnostic messages - */ - const formatHost: tsStatic.FormatDiagnosticsHost = { - getCanonicalFileName: (path) => path, - getCurrentDirectory: ts.sys.getCurrentDirectory, - getNewLine: () => ts.sys.newLine, - } + const watcher = chokidar.watch(["."], { + ignoreInitial: true, + cwd: this.#projectRoot, + ignored: [ + /(^|[\\/\\])\../, + "node_modules", + "dist", + "static", + "private", + ".medusa/**/*", + ...this.#backendIgnoreFiles, + ], + }) - /** - * Creating a watcher compiler host to watch files and recompile - * them as they are changed - */ - const host = ts.createWatchCompilerHost( - this.#tsConfigPath, - { - outDir: this.#pluginsDistFolder, - noCheck: true, - }, - ts.sys, - ts.createEmitAndSemanticDiagnosticsBuilderProgram, - (diagnostic) => this.#printDiagnostics(ts, [diagnostic]), - (diagnostic) => { - if (typeof diagnostic.messageText === "string") { - this.#logger.info(diagnostic.messageText) - } else { - this.#logger.info( - ts.formatDiagnosticsWithColorAndContext([diagnostic], formatHost) - ) - } - }, - { - excludeDirectories: this.#backendIgnoreFiles, + watcher.on("add", async (file) => { + if (!this.#isScriptFile(file)) { + return } - ) + const relativePath = path.relative(this.#projectRoot, file) + const outputPath = relativePath.replace(/\.ts$/, ".js") - const origPostProgramCreate = host.afterProgramCreate - host.afterProgramCreate = (program) => { - origPostProgramCreate!(program) - onFileChange?.() - } + this.#logger.info(`${relativePath} updated: Republishing changes`) + await fs.create(outputPath, await transformer(file)) - ts.createWatchProgram(host) + onFileChange?.(file, "add") + }) + watcher.on("change", async (file) => { + if (!this.#isScriptFile(file)) { + return + } + const relativePath = path.relative(this.#projectRoot, file) + const outputPath = relativePath.replace(/\.ts$/, ".js") + + this.#logger.info(`${relativePath} updated: Republishing changes`) + await fs.create(outputPath, await transformer(file)) + + onFileChange?.(file, "change") + }) + watcher.on("unlink", async (file) => { + if (!this.#isScriptFile(file)) { + return + } + const relativePath = path.relative(this.#projectRoot, file) + const outputPath = relativePath.replace(/\.ts$/, ".js") + + this.#logger.info(`${relativePath} removed: Republishing changes`) + await fs.remove(outputPath) + onFileChange?.(file, "unlink") + }) + + watcher.on("ready", () => { + this.#logger.info("watching for file changes") + }) } async buildPluginAdminExtensions(bundler: { diff --git a/packages/medusa/src/commands/plugin/develop.ts b/packages/medusa/src/commands/plugin/develop.ts index 623d8a64a9..b808bdad29 100644 --- a/packages/medusa/src/commands/plugin/develop.ts +++ b/packages/medusa/src/commands/plugin/develop.ts @@ -1,4 +1,5 @@ import path from "path" +import * as swcCore from "@swc/core" import { execFile } from "child_process" import { logger } from "@medusajs/framework/logger" import { Compiler } from "@medusajs/framework/build-tools" @@ -10,9 +11,18 @@ export default async function developPlugin({ }) { let isBusy = false const compiler = new Compiler(directory, logger) + const parsedConfig = await compiler.loadTSConfigFile() + if (!parsedConfig) { + return + } + const yalcBin = path.join(path.dirname(require.resolve("yalc")), "yalc.js") - await compiler.developPluginBackend(async () => { + /** + * Publishes the build output to the registry and updates + * installations + */ + function publishChanges() { /** * Here we avoid multiple publish calls when the filesystem is * changed too quickly. This might result in stale content in @@ -46,5 +56,47 @@ export default async function developPlugin({ console.error(stderr) } ) - }) + } + + /** + * Transforms a given file using @swc/core + */ + async function transformFile(filePath: string) { + const output = await swcCore.transformFile(filePath, { + sourceMaps: "inline", + module: { + type: "commonjs", + strictMode: true, + noInterop: false, + }, + jsc: { + externalHelpers: false, + target: "es2021", + parser: { + syntax: "typescript", + tsx: true, + decorators: true, + dynamicImport: true, + }, + transform: { + legacyDecorator: true, + decoratorMetadata: true, + react: { + throwIfNamespace: false, + useBuiltins: false, + pragma: "React.createElement", + pragmaFrag: "React.Fragment", + importSource: "react", + runtime: "automatic", + }, + }, + keepClassNames: true, + baseUrl: directory, + }, + }) + return output.code + } + + await compiler.buildPluginBackend(parsedConfig) + await compiler.developPluginBackend(transformFile, publishChanges) } diff --git a/yarn.lock b/yarn.lock index a1052c33e9..3016a6f277 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5766,6 +5766,7 @@ __metadata: "@types/express": ^4.17.17 "@types/jsonwebtoken": ^8.5.9 awilix: ^8.0.1 + chokidar: ^3.4.2 compression: 1.7.4 connect-redis: 5.2.0 cookie-parser: ^1.4.6