diff --git a/apps/web/src/services/ai/element-tools-dispatcher.ts b/apps/web/src/services/ai/element-tools-dispatcher.ts index 00d0ccdca..536c06d92 100644 --- a/apps/web/src/services/ai/element-tools-dispatcher.ts +++ b/apps/web/src/services/ai/element-tools-dispatcher.ts @@ -31,7 +31,16 @@ import { useHistoryStore } from '@/stores/history-store'; import { useCanvasStore } from '@/stores/canvas-store'; import { getElementShim, SUPPORTED_EMBEDDED_ELEMENT_TOOLS } from './element-tool-shims'; import { insertStreamingNode } from './design-canvas-ops'; -import { runBatchDesignDsl } from '@zseven-w/pen-mcp'; +// Import from the pen-mcp package's browser-safe `./dsl` subpath (not +// the top-level barrel). The barrel re-exports node-only modules — +// `document-manager`, `log-utils`, `theme-presets` — that pull in +// `node:fs` / `node:path`. Vite / esbuild resolve the barrel before +// tree-shaking can drop those branches, so the browser build fails +// on the unresolved node built-ins. The `./dsl` subpath points +// directly at `tools/batch-design-dsl.ts`, which is the only module +// this dispatcher actually needs and is kept rigorously browser-safe +// (enforced by `packages/pen-mcp/src/__tests__/batch-design-dsl-browser-safe.test.ts`). +import { runBatchDesignDsl } from '@zseven-w/pen-mcp/dsl'; import type { PenNode } from '@/types/pen'; /** @@ -438,11 +447,21 @@ async function applyBatchDesignDsl(dsl: string, ctx: DispatchContext): Promise 0) { return { status: 'failed', @@ -480,7 +499,18 @@ async function applyBatchDesignDsl(dsl: string, ctx: DispatchContext): Promise = + typeof activePageId === 'string' && activePageId.length > 0 + ? { dsl, pageId: activePageId } + : { dsl }; + const httpResult = await fallbackViaHttp('batch-design-dsl', 'batch_design', ctx, dslBody); // Note: HTTP may succeed even if in-browser threw. Preserve its // result as-is; only annotate the message when both paths fail. if (httpResult.status === 'applied') return httpResult; diff --git a/packages/pen-mcp/package.json b/packages/pen-mcp/package.json index d2b94c441..134efc0e0 100644 --- a/packages/pen-mcp/package.json +++ b/packages/pen-mcp/package.json @@ -24,6 +24,10 @@ ".": { "types": "./src/index.ts", "import": "./src/index.ts" + }, + "./dsl": { + "types": "./src/tools/batch-design-dsl.ts", + "import": "./src/tools/batch-design-dsl.ts" } }, "scripts": { diff --git a/packages/pen-mcp/src/__tests__/batch-design-dsl-browser-safe.test.ts b/packages/pen-mcp/src/__tests__/batch-design-dsl-browser-safe.test.ts index da3bc7822..4521e32ec 100644 --- a/packages/pen-mcp/src/__tests__/batch-design-dsl-browser-safe.test.ts +++ b/packages/pen-mcp/src/__tests__/batch-design-dsl-browser-safe.test.ts @@ -130,4 +130,20 @@ describe('batch-design-dsl — browser-safe transitive import tree', () => { const importsDocManager = wrapper.includes("from '../document-manager"); expect(importsDocManager).toBe(true); }); + + it('package.json exposes `./dsl` subpath export pointing at the browser-safe file', () => { + // Guards against a regression where someone removes the subpath + // export — apps/web imports `@zseven-w/pen-mcp/dsl` specifically + // to skip the barrel (which pulls node:fs via document-manager). + // If this key disappears, Vite falls back to the `.` export, the + // barrel resolves, and the browser build breaks. + const pkgPath = join(__dirname, '..', '..', 'package.json'); + const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { + exports?: Record; + }; + const dsl = pkg.exports?.['./dsl']; + expect(dsl, 'package.json must expose `./dsl` subpath export').toBeDefined(); + expect(dsl?.import).toMatch(/batch-design-dsl\.ts$/); + expect(dsl?.types).toMatch(/batch-design-dsl\.ts$/); + }); });