Type Checking & Linting
Type-check the plain-JavaScript backend in place with checkJs and enforce the house style with ESLint.
The Medusa backend (the Kora framework in src/medusa/, the module loaders in src/modules/, and every module under modules/) is plain ESM JavaScript that runs directly through node index.js with no build step. It is still type-checked and linted in place: TypeScript verifies the JavaScript through checkJs, and ESLint enforces the correctness baseline plus the project house style. The web dashboard under src/dashboard/ keeps its own TypeScript and ESLint configuration and is excluded from both backend tools.
Commands
bun run typecheck
bun run typecheck:modules
bun run lint
bun run lint:fixtypecheck verifies the backend framework against tsconfig.backend.json. typecheck:modules verifies all modules in modules/ against tsconfig.module.json. Neither command emits output. lint reports across the assembled checkout, including every module worktree present in modules/.
Type Declarations
Type declarations reside in src/types/ rather than alongside runtime source files:
src/types/medusa/: Framework declarations and package augmentations (index.d.ts,canvas.d.ts,packages.d.ts).src/types/modules/: Module base classes and contracts (index.d.ts,types.d.ts).
package.json maps subpath imports with explicit types targets:
{
"imports": {
"#medusa": {
"types": "./src/types/medusa/index.d.ts",
"default": "./src/medusa/index.js"
},
"#medusa/modules": {
"types": "./src/types/modules/index.d.ts",
"default": "./src/modules/index.js"
}
}
}This structure gives module authors and editors complete type resolution and autocomplete for every base class and runtime service without building or transpiling.
Type Checking
tsconfig.backend.json type-checks the JavaScript without compiling it. There is no transpile and no output directory; node index.js remains the only runtime.
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"allowJs": true,
"checkJs": true,
"noEmit": true,
"strict": true,
"noImplicitAny": false,
"strictNullChecks": false
}
}NodeNext resolution matches how Node runs the code: explicit .js import extensions and the #medusa subpath imports declared in package.json. noImplicitAny and strictNullChecks are relaxed so an untyped codebase does not report thousands of implicit-any and possibly-null errors on day one. Both are ratchets to tighten as annotation coverage grows.
Adding Types with JSDoc
Types come from JSDoc annotations rather than TypeScript syntax, so files stay valid .js. A single @param clears every "property does not exist" error on a config-object parameter:
/**
* @param {{ accent?: string, components?: any[], spoiler?: boolean, id?: number }} [options]
*/
container({ accent, components = [], spoiler = false, id } = {}) {
const result = { type: 17, components: Array.isArray(components) ? components : [components] };
if (accent !== undefined) result.accent_color = accent;
if (typeof id === "number") result.id = id;
return result;
}JSDoc type blocks (/** */) are the one comment form the lint rules permit. They are type declarations, not prose, so they satisfy the no-comments rule while feeding checkJs its types.
Linting
eslint.config.js at the repository root is a flat config that layers the house style over @eslint/js recommended. It ignores src/dashboard/ and each module's resources/dashboard/, which lint under their own configuration.
Enforced House Rules
| Rule | Enforcement |
|---|---|
Optional chaining (?.) | Banned via no-restricted-syntax. Use explicit checks. |
Nullish coalescing (??) | Banned via no-restricted-syntax. Use explicit checks. |
else if | Banned via no-restricted-syntax. Use early returns, guard clauses, lookup tables or switch. |
| Comments | Banned via house/no-prose-comments. Only JSDoc /** */ blocks are allowed. |
var | Banned; use const or let. |
| Loose equality | eqeqeq requires === and !==. |
no-empty permits empty catch blocks, and no-unused-vars ignores unused function parameters, since handler and event signatures keep positional parameters they do not always consume.
The || value-fallback prohibition is not lint-enforced. There is no reliable way to distinguish a fallback (const x = a || b) from a boolean condition (if (a || b)) at the syntax level, so this rule is followed by convention rather than by the linter.
Module Worktrees
Every module is an isolated git worktree on its own module/<name> branch, so the backend tools run against the assembled checkout where all module directories are present. Type and lint errors reported inside modules/<name>/ belong to that module's branch and must be fixed and committed there, not on main. The root tsconfig.backend.json, tsconfig.module.json and eslint.config.js live on main only; module branches do not carry these files. Run bun run typecheck:modules to typecheck module worktrees against tsconfig.module.json.
Dashboard
The dashboard in src/dashboard/ is a Vite and React single-page app routed with React Router. It has no server code of its own: sign-in, page data (/api/view/...), dashboard actions (/api/action/<name>) and the /api/m/<module>/... proxy are served by the bot from src/medusa/handlers/dashboard/, which the backend typecheck and lint commands cover.
cd src/dashboard
bun x tsc --noEmit
bun x eslint .The dashboard lint config applies the same bans: optional chaining, nullish coalescing, else if and prose comments. Directive comments such as /* @vite-ignore */ and JSDoc blocks are allowed.
The root tsconfig.json type-checks module views under modules/*/resources/dashboard/ against the dashboard's @/ imports. src/dashboard/toolchain.js holds the tooling the bot loads at runtime: the Vite dev server used when SERVER.DASHBOARD.DEVELOPER is true, the esbuild compiler for module views and previews, and the Tailwind compiler for the classes they use.