Skip to content

feat: introduce moonbeam#247

Merged
luxass merged 6 commits intomainfrom
introduce-moonbeam
Sep 11, 2025
Merged

feat: introduce moonbeam#247
luxass merged 6 commits intomainfrom
introduce-moonbeam

Conversation

@luxass
Copy link
Member

@luxass luxass commented Sep 10, 2025

🔗 Linked issue

📚 Description

This PR introduces a new tooling package called "moonbeam" (funny name i know). The job of this tooling package is to make the gap between the tsx and the build:openapi better.

Currently with the raw tsx setup, we need to build the shared package before we can build the openapi schema, this is because the tsx resolver resolves the import to the "@ucdjs/shared" dist instead of the raw source files.

So by migrating to moonbeam, we can get rid of this headache since it resolves it to the source files, and therefore doesn't require any packages to be built before the openapi schema.

This PR is generated using Claude Code, but based on a idea of mine.

Summary by CodeRabbit

  • New Features

    • Introduced “Moonbeam,” a workspace-aware module resolver/loader that auto-resolves monorepo package imports (prefers source with dist fallback) and works with tsx or Node.
  • Documentation

    • Added a usage guide for Moonbeam, including setup, features, and typical use cases.
  • Chores

    • Updated OpenAPI build script to use Moonbeam.
    • Adjusted task dependencies to ensure OpenAPI artifacts are generated before builds.
    • Minor lint configuration updates to align with workspace and pnpm settings.

* Deleted `tsconfig-paths` from `package.json`, `pnpm-lock.yaml`, and `pnpm-workspace.yaml` as it is no longer needed.
* Updated `turbo.json` to adjust build dependencies for better clarity and organization.
@changeset-bot
Copy link

changeset-bot bot commented Sep 10, 2025

⚠️ No Changeset found

Latest commit: 7502488

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 10, 2025

Walkthrough

Adds a new Moonbeam ESM loader package and integrates it into the API’s OpenAPI build script. Updates ESLint config in API and Moonbeam. Adjusts Turbo build dependencies (root adds an OpenAPI pre-step; API removes local dependsOn). Documents Moonbeam’s usage and behavior.

Changes

Cohort / File(s) Summary
Moonbeam package (new)
tooling/moonbeam/README.md, tooling/moonbeam/package.json, tooling/moonbeam/src/esm-loader.mjs, tooling/moonbeam/src/register.mjs, tooling/moonbeam/eslint.config.js
Introduces Moonbeam: an ESM loader resolving workspace imports to src with dist fallback; provides register and loader entry points; adds docs, package metadata/exports, and ESLint config.
API build and orchestration
apps/api/package.json, apps/api/turbo.json, turbo.json
Switches OpenAPI build to use tsx with Moonbeam register and .ts entry; adds @ucdjs/moonbeam devDependency; removes API task-level dependsOn; root build now depends on @ucdjs/api-worker#build:openapi.
ESLint config (API)
apps/api/eslint.config.js
Adds type: "app" and pnpm: true to luxass config; formatters: true retained.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor Dev as Developer
    participant TSX as tsx (with --import @ucdjs/moonbeam/register)
    participant Reg as @ucdjs/moonbeam/register
    participant Ldr as Moonbeam ESM Loader
    participant FS as Filesystem
    participant Node as Node Resolver (nextResolve)

    Dev->>TSX: run build:openapi
    TSX->>Reg: import register.mjs
    Reg->>Ldr: register("./esm-loader.mjs")
    Note over Ldr: On import resolution

    Ldr->>FS: Detect workspace root (pnpm-workspace.yaml / workspaces)
    Ldr->>FS: Map packages/* by name

    alt specifier matches workspace pkg or subpath
        Ldr->>FS: Check src/<entry>.ts
        alt src exists
            Ldr-->>TSX: file://.../src/<entry>.ts (shortCircuit)
        else dist exists
            Ldr-->>TSX: file://.../dist/<entry>.js (shortCircuit)
        else no match
            Ldr->>Node: nextResolve(specifier, context)
            Node-->>TSX: resolved URL
        end
    else non-workspace import
        Ldr->>Node: nextResolve(specifier, context)
        Node-->>TSX: resolved URL
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “feat: introduce moonbeam” succinctly captures the primary change of adding the Moonbeam tooling package, uses conventional commit style, and omits extraneous details, making it clear and meaningful to anyone reviewing the project’s history.

Poem

In moonlit beams my whiskers gleam,
I hop through workspaces, swift as a dream—
src if it’s there, dist when it’s not,
A loader’s path, neatly wrought.
OpenAPI hums before the build’s start,
Carrots compiled, with artisanal art. 🥕✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch introduce-moonbeam

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added apps: api Changes related to the API. pkg: shared Changes related to the Shared package. labels Sep 10, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Sep 10, 2025

Preview Deployment for Web

The Web worker has been deployed successfully.

Preview URL: https://preview.ucdjs.dev

This preview was built from commit 7502488


🤖 This comment will be updated automatically when you push new commits to this PR.

@github-actions
Copy link
Contributor

github-actions bot commented Sep 10, 2025

Preview Deployment for Api

The Api worker has been deployed successfully.

Preview URL: https://preview.api.ucdjs.dev

This preview was built from commit 7502488


🤖 This comment will be updated automatically when you push new commits to this PR.

@codecov
Copy link

codecov bot commented Sep 10, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

* Introduced `eslint.config.js` for linting configuration.
* Updated `esm-loader.mjs` and `register.mjs` to use consistent import syntax and added TypeScript checking.
* Removed commented-out code in `register.mjs` for clarity.
@github-actions github-actions bot removed the pkg: shared Changes related to the Shared package. label Sep 10, 2025
@luxass luxass marked this pull request as ready for review September 10, 2025 19:02
Copilot AI review requested due to automatic review settings September 10, 2025 19:02
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces "moonbeam", a custom ESM loader tooling package that resolves workspace package imports to source files instead of built dist files, eliminating the need to build shared packages before generating OpenAPI schemas.

  • Creates a new moonbeam tooling package with workspace-aware module resolution
  • Updates the API worker to use moonbeam for OpenAPI schema generation
  • Removes build dependencies from the build:openapi task in turbo configuration

Reviewed Changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tooling/moonbeam/ New package implementing ESM loader for workspace resolution
apps/api/package.json Updates build:openapi script to use moonbeam loader
apps/api/turbo.json Removes build dependencies from build:openapi task
turbo.json Adds build:openapi as dependency for main build task
apps/api/eslint.config.js Minor ESLint configuration updates
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (11)
tooling/moonbeam/eslint.config.js (1)

1-6: ESLint config looks good; optionally declare this as tooling for clarity.

If supported by @luxass/eslint-config, set type to "tooling" for intent and possible rule tuning.

 export default luxass({
   pnpm: true,
+  type: "tooling",
 });
tooling/moonbeam/README.md (3)

24-27: Clarify Node.js version requirements for register API.

You mention Node.js 22.18+. The node:module register API requires Node 22.x; specify the minimum exact version you rely on (e.g., >=22.6) and note that using tsx still requires a Node runtime that supports register since this module calls it at import time.

-### With Node.js 22.18+
+### With Node.js (requires >=22.6 for node:module register)

node --import @ucdjs/moonbeam/register ./your-script.ts

+Note: Using `tsx --import` also runs on your system Node; ensure it’s Node >=22.6.

31-37: Mention src/dist file extension handling.

Call out which extensions are tried (e.g., src: .ts/.tsx/.mts; dist: .js/.mjs), so consumers know when fallback to dist or default resolution may occur.


21-22: Add a troubleshooting tip for enabling debug logs.

Since the loader logs discoveries, suggest a MOONBEAM_DEBUG flag to toggle verbosity and include a short snippet.

-```bash
-tsx --import @ucdjs/moonbeam/register ./your-script.ts
-```
+```bash
+# Optional: enable verbose logs
+MOONBEAM_DEBUG=1 tsx --import @ucdjs/moonbeam/register ./your-script.ts
+```
tooling/moonbeam/src/esm-loader.mjs (5)

10-34: Don’t hard-fail when no workspace root is found; degrade gracefully.

Throwing here will crash any script that accidentally preloads Moonbeam outside the monorepo. Prefer returning null/undefined and no-op resolution.

-function findWorkspaceRoot(startDir = __dirname) {
+function findWorkspaceRoot(startDir = __dirname) {
   let currentDir = startDir;
@@
-  throw new Error("Could not find workspace root. Make sure you have pnpm-workspace.yaml or package.json with workspaces field.");
+  return null; // No workspace detected; loader can no-op.
 }

36-60: Broaden workspace discovery and prepare simple helpers.

Only scanning workspaceRoot/packages misses apps/* and tooling/* packages commonly named @ucdjs/*. Also, add a tiny file-existence helper.

-function discoverWorkspacePackages(workspaceRoot) {
+function discoverWorkspacePackages(workspaceRoot) {
   const packages = new Map();
-  const packagesDir = path.join(workspaceRoot, "packages");
+  const roots = ["packages", "apps", "tooling"].map((d) => path.join(workspaceRoot, d));
 
-  if (existsSync(packagesDir)) {
-    const entries = readdirSync(packagesDir, { withFileTypes: true });
-    for (const entry of entries) {
-      if (entry.isDirectory()) {
-        const packageJsonPath = path.join(packagesDir, entry.name, "package.json");
+  for (const root of roots) {
+    if (!existsSync(root)) continue;
+    const entries = readdirSync(root, { withFileTypes: true });
+    for (const entry of entries) {
+      if (entry.isDirectory()) {
+        const packageJsonPath = path.join(root, entry.name, "package.json");
         if (existsSync(packageJsonPath)) {
           try {
             const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));
             if (pkg.name) {
-              packages.set(pkg.name, `packages/${entry.name}`);
+              // Store relative path from workspace root
+              const rel = path.relative(workspaceRoot, path.join(root, entry.name));
+              packages.set(pkg.name, rel);
             }
           } catch { }
         }
       }
     }
-  }
+  }
 
   return packages;
 }
+
+function firstExisting(paths) {
+  for (const p of paths) if (existsSync(p)) return p;
+  return null;
+}

62-71: Gate logs behind an env flag and handle “no workspace” gracefully.

Avoid noisy logs and accidental crashes outside a workspace. Also, don’t shadow the imported path module in the for-of loop.

-const workspaceRoot = findWorkspaceRoot();
-console.info(`🔍 Found workspace root: ${workspaceRoot}`);
-
-const workspacePackages = discoverWorkspacePackages(workspaceRoot);
-console.info(`🌙 Moonbeam loaded - found ${workspacePackages.size} workspace packages`);
-
-for (const [name, path] of workspacePackages) {
-  console.info(`  📦 ${name} -> ${path}`);
-}
+const DEBUG = process.env.MOONBEAM_DEBUG === "1";
+const workspaceRoot = findWorkspaceRoot();
+const workspacePackages = workspaceRoot ? discoverWorkspacePackages(workspaceRoot) : new Map();
+
+if (DEBUG && workspaceRoot) {
+  console.info(`🔍 Found workspace root: ${workspaceRoot}`);
+  console.info(`🌙 Moonbeam loaded - found ${workspacePackages.size} workspace packages`);
+  for (const [name, pkgPath] of workspacePackages) {
+    console.info(`  📦 ${name} -> ${pkgPath}`);
+  }
+} else if (DEBUG && !workspaceRoot) {
+  console.info("🌙 Moonbeam disabled: no workspace root detected");
+}

72-93: Support multiple index entry points and extensions for exact package specifiers.

Limit to src/index.ts and dist/index.js is brittle. Try common variants before falling back.

   if (workspacePackages.has(specifier)) {
     const packagePath = workspacePackages.get(specifier);
     const srcPath = path.join(workspaceRoot, packagePath, "src");
-    const indexPath = path.join(srcPath, "index.ts");
-
-    if (existsSync(indexPath)) {
-      return {
-        shortCircuit: true,
-        url: pathToFileURL(indexPath).href,
-      };
-    }
-
-    // fallback to dist
-    const distPath = path.join(workspaceRoot, packagePath, "dist", "index.js");
-    if (existsSync(distPath)) {
+    const srcCandidates = [
+      path.join(srcPath, "index.ts"),
+      path.join(srcPath, "index.tsx"),
+      path.join(srcPath, "mod.ts"),
+    ];
+    const distCandidates = [
+      path.join(workspaceRoot, packagePath, "dist", "index.js"),
+      path.join(workspaceRoot, packagePath, "dist", "index.mjs"),
+    ];
+    const hit = firstExisting(srcCandidates) || firstExisting(distCandidates);
+    if (hit) {
       return {
         shortCircuit: true,
-        url: pathToFileURL(distPath).href,
+        url: pathToFileURL(hit).href,
       };
     }
   }

95-116: Handle directory subpaths and multiple extensions.

Support both files (utils.ts/tsx/mts) and directory indexes (utils/index.ts etc.), with dist fallbacks.

   for (const [packageName, packagePath] of workspacePackages) {
     if (specifier.startsWith(`${packageName}/`)) {
       const subpath = specifier.slice(packageName.length + 1);
-      const srcPath = path.join(workspaceRoot, packagePath, "src", `${subpath}.ts`);
-
-      if (existsSync(srcPath)) {
-        return {
-          shortCircuit: true,
-          url: pathToFileURL(srcPath).href,
-        };
-      }
-
-      const distPath = path.join(workspaceRoot, packagePath, "dist", `${subpath}.js`);
-      if (existsSync(distPath)) {
-        return {
-          shortCircuit: true,
-          url: pathToFileURL(distPath).href,
-        };
-      }
+      const srcBase = path.join(workspaceRoot, packagePath, "src", subpath);
+      const distBase = path.join(workspaceRoot, packagePath, "dist", subpath);
+      const srcCandidates = [
+        `${srcBase}.ts`,
+        `${srcBase}.tsx`,
+        `${srcBase}.mts`,
+        path.join(srcBase, "index.ts"),
+        path.join(srcBase, "index.tsx"),
+        path.join(srcBase, "index.mts"),
+      ];
+      const distCandidates = [
+        `${distBase}.js`,
+        `${distBase}.mjs`,
+        path.join(distBase, "index.js"),
+        path.join(distBase, "index.mjs"),
+      ];
+      const hit = firstExisting(srcCandidates) || firstExisting(distCandidates);
+      if (hit) {
+        return { shortCircuit: true, url: pathToFileURL(hit).href };
+      }
     }
   }
tooling/moonbeam/package.json (2)

30-33: Optional: add a tiny sanity script for quick local check.

A no-op script like "check" that prints process.versions.node can help debug engine mismatches when invoked via pnpm -w.

   "scripts": {
     "clean": "git clean -xdf node_modules",
-    "lint": "eslint ."
+    "lint": "eslint .",
+    "check": "node -e \"console.log(process.versions.node)\""
   },

27-29: Node engine requirement is consistent (>=22.18) across all package.json. Optional: if Node.js >=22.17 suffices for node:module.register, lower the engine floor to >=22.17 and update every package.json accordingly.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2a9bfcd and 7502488.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • apps/api/eslint.config.js (1 hunks)
  • apps/api/package.json (2 hunks)
  • apps/api/turbo.json (0 hunks)
  • tooling/moonbeam/README.md (1 hunks)
  • tooling/moonbeam/eslint.config.js (1 hunks)
  • tooling/moonbeam/package.json (1 hunks)
  • tooling/moonbeam/src/esm-loader.mjs (1 hunks)
  • tooling/moonbeam/src/register.mjs (1 hunks)
  • turbo.json (1 hunks)
💤 Files with no reviewable changes (1)
  • apps/api/turbo.json
🧰 Additional context used
📓 Path-based instructions (4)
**/src/**

📄 CodeRabbit inference engine (AGENTS.md)

Include all source files under any src directory in coverage

Files:

  • tooling/moonbeam/src/register.mjs
  • tooling/moonbeam/src/esm-loader.mjs
tooling/**

📄 CodeRabbit inference engine (AGENTS.md)

Exclude the tooling/* directory from coverage

Files:

  • tooling/moonbeam/src/register.mjs
  • tooling/moonbeam/eslint.config.js
  • tooling/moonbeam/package.json
  • tooling/moonbeam/README.md
  • tooling/moonbeam/src/esm-loader.mjs
turbo.json

📄 CodeRabbit inference engine (AGENTS.md)

Manage build orchestration and caching via the root turbo.json

Files:

  • turbo.json
**/package.json

📄 CodeRabbit inference engine (AGENTS.md)

Use ESM across the codebase (package.json "type": "module")

Files:

  • tooling/moonbeam/package.json
  • apps/api/package.json
🧠 Learnings (4)
📚 Learning: 2025-09-10T14:28:08.429Z
Learnt from: CR
PR: ucdjs/ucd#0
File: AGENTS.md:0-0
Timestamp: 2025-09-10T14:28:08.429Z
Learning: Applies to **/package.json : Use ESM across the codebase (package.json "type": "module")

Applied to files:

  • tooling/moonbeam/src/register.mjs
  • tooling/moonbeam/src/esm-loader.mjs
📚 Learning: 2025-09-10T14:28:08.429Z
Learnt from: CR
PR: ucdjs/ucd#0
File: AGENTS.md:0-0
Timestamp: 2025-09-10T14:28:08.429Z
Learning: Applies to turbo.json : Manage build orchestration and caching via the root turbo.json

Applied to files:

  • turbo.json
📚 Learning: 2025-06-28T08:01:22.596Z
Learnt from: luxass
PR: ucdjs/ucd#85
File: packages/fetch/package.json:37-38
Timestamp: 2025-06-28T08:01:22.596Z
Learning: In the ucdjs/ucd project, relative paths in npm scripts within packages (like `../../apps/api/.generated/openapi.json` in packages/fetch/package.json) resolve correctly even when run via Turborepo from the repo root, contrary to initial concerns about working directory changes.

Applied to files:

  • turbo.json
📚 Learning: 2025-07-20T05:37:40.565Z
Learnt from: luxass
PR: ucdjs/ucd#131
File: tooling/eslint-plugin/package.json:0-0
Timestamp: 2025-07-20T05:37:40.565Z
Learning: In the ucdjs/ucd project, internal tooling packages (private packages in the tooling/ directory) export TypeScript files directly without requiring a build step, unlike published packages which use tsdown to build and export from ./dist/. Examples include ucdjs/tsdown-config and ucdjs/eslint-plugin.

Applied to files:

  • turbo.json
  • tooling/moonbeam/package.json
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test (windows-latest)
  • GitHub Check: test (macos-latest)
🔇 Additional comments (8)
turbo.json (1)

15-18: Dependency wiring and outputs verified – no cycles introduced

apps/api/eslint.config.js (1)

6-9: LGTM: app + pnpm settings.

The added type: "app" and pnpm: true align with the monorepo setup and our learnings around pnpm-aware configs.

tooling/moonbeam/README.md (1)

13-16: Document directory subpath resolution.

Moonbeam resolves subpaths like @ucdjs/package/utils. Explicitly state whether utils/index.ts is supported in addition to utils.ts, and whether .tsx/.mts are recognized.

If not yet supported, align the README with current behavior or extend the loader (see suggestions in src/esm-loader.mjs comments).

tooling/moonbeam/src/esm-loader.mjs (1)

118-120: Fallback behavior is correct.

Deferring to nextResolve preserves default Node/tsx semantics when Moonbeam doesn’t match.

tooling/moonbeam/src/register.mjs (1)

3-5: Loader registration looks correct and minimal.

Using node:module.register with import.meta.url and a relative specifier is the right pattern here.

apps/api/package.json (2)

39-39: Dev dependency addition is appropriate.

Adding @ucdjs/moonbeam as a workspace devDependency matches usage via --import.


10-10: No change required: TSX already uses apps/api/tsconfig.json by default.
TSX will automatically locate and apply apps/api/tsconfig.json when running the script, so adding an explicit --tsconfig flag is redundant.

tooling/moonbeam/package.json (1)

22-26: Exports map is well-scoped.

Explicit subpath exports for ./register and ./esm are clear and avoid accidental top-level imports.

@luxass luxass merged commit 8b3b421 into main Sep 11, 2025
16 checks passed
@luxass luxass deleted the introduce-moonbeam branch September 11, 2025 03:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

apps: api Changes related to the API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant