Просмотр исходного кода

fix(cli): resolve studio runtime entrypoints

Ma 7 месяцев назад
Родитель
Сommit
23d3a884d8
2 измененных файлов с 100 добавлено и 29 удалено
  1. 50 0
      packages/cli/src/__tests__/studio-runtime.test.ts
  2. 50 29
      packages/cli/src/commands/studio.ts

+ 50 - 0
packages/cli/src/__tests__/studio-runtime.test.ts

@@ -0,0 +1,50 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+const accessMock = vi.fn();
+
+vi.mock("node:fs/promises", () => ({
+  access: accessMock,
+}));
+
+describe("studio runtime resolution", () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+    vi.resetModules();
+  });
+
+  it("finds monorepo packages/studio sources from a project directory", async () => {
+    accessMock.mockImplementation(async (path: string) => {
+      if (path === "/repo/packages/studio/src/api/index.ts") {
+        return;
+      }
+      throw new Error(`missing: ${path}`);
+    });
+
+    const { resolveStudioLaunch } = await import("../commands/studio.js");
+    const launch = await resolveStudioLaunch("/repo/test-project");
+
+    expect(launch).toEqual({
+      studioEntry: "/repo/packages/studio/src/api/index.ts",
+      command: "npx",
+      args: ["tsx", "/repo/packages/studio/src/api/index.ts", "/repo/test-project"],
+    });
+  });
+
+  it("uses node for built JavaScript entries", async () => {
+    accessMock.mockImplementation(async (path: string) => {
+      if (path === "/repo/test-project/node_modules/@actalk/inkos-studio/dist/api/index.js") {
+        return;
+      }
+      throw new Error(`missing: ${path}`);
+    });
+
+    const { resolveStudioLaunch } = await import("../commands/studio.js");
+    const launch = await resolveStudioLaunch("/repo/test-project");
+
+    expect(launch).toEqual({
+      studioEntry: "/repo/test-project/node_modules/@actalk/inkos-studio/dist/api/index.js",
+      command: "node",
+      args: ["/repo/test-project/node_modules/@actalk/inkos-studio/dist/api/index.js", "/repo/test-project"],
+    });
+  });
+});

+ 50 - 29
packages/cli/src/commands/studio.ts

@@ -4,41 +4,62 @@ import { spawn } from "node:child_process";
 import { join } from "node:path";
 import { access } from "node:fs/promises";
 
+export interface StudioLaunchSpec {
+  readonly studioEntry: string;
+  readonly command: "npx" | "node";
+  readonly args: string[];
+}
+
+async function firstAccessiblePath(paths: readonly string[]): Promise<string | undefined> {
+  for (const path of paths) {
+    try {
+      await access(path);
+      return path;
+    } catch {
+      // continue
+    }
+  }
+  return undefined;
+}
+
+export async function resolveStudioLaunch(root: string): Promise<StudioLaunchSpec | null> {
+  const sourceEntry = await firstAccessiblePath([
+    join(root, "packages", "studio", "src", "api", "index.ts"),
+    join(root, "..", "packages", "studio", "src", "api", "index.ts"),
+    join(root, "..", "studio", "src", "api", "index.ts"),
+  ]);
+  if (sourceEntry) {
+    return {
+      studioEntry: sourceEntry,
+      command: "npx",
+      args: ["tsx", sourceEntry, root],
+    };
+  }
+
+  const builtEntry = await firstAccessiblePath([
+    join(root, "node_modules", "@actalk", "inkos-studio", "dist", "api", "index.js"),
+    join(root, "node_modules", "@actalk", "inkos-studio", "server.cjs"),
+  ]);
+  if (builtEntry) {
+    return {
+      studioEntry: builtEntry,
+      command: "node",
+      args: [builtEntry, root],
+    };
+  }
+
+  return null;
+}
+
 export const studioCommand = new Command("studio")
   .description("Start InkOS Studio web workbench")
   .option("-p, --port <port>", "Server port", "4567")
   .action(async (opts) => {
     const root = findProjectRoot();
     const port = opts.port;
+    const launch = await resolveStudioLaunch(root);
 
-    // Look for studio's built server entry
-    const studioPaths = [
-      join(root, "node_modules", "@actalk", "inkos-studio", "dist", "api", "index.js"),
-      join(root, "..", "studio", "src", "api", "index.ts"),
-    ];
-
-    // Try to find tsx or ts-node for running TypeScript
-    // In dev (monorepo), run studio's TS source directly via tsx
-    const studioDir = join(root, "..", "studio");
-    let studioEntry: string | undefined;
-
-    try {
-      await access(join(studioDir, "src", "api", "index.ts"));
-      studioEntry = join(studioDir, "src", "api", "index.ts");
-    } catch {
-      // Not in monorepo — look for built JS
-      for (const p of studioPaths) {
-        try {
-          await access(p);
-          studioEntry = p;
-          break;
-        } catch {
-          // continue
-        }
-      }
-    }
-
-    if (!studioEntry) {
+    if (!launch) {
       logError(
         "InkOS Studio not found. If you cloned the repo, run:\n" +
         "  cd packages/studio && pnpm install && pnpm build\n" +
@@ -49,7 +70,7 @@ export const studioCommand = new Command("studio")
 
     log(`Starting InkOS Studio on http://localhost:${port}`);
 
-    const child = spawn("npx", ["tsx", studioEntry], {
+    const child = spawn(launch.command, launch.args, {
       cwd: root,
       stdio: "inherit",
       env: { ...process.env, INKOS_STUDIO_PORT: port },