fix(ai): dispatcher imports DSL executor via browser-safe subpath

[Codex P1] The browser-side element-tools-dispatcher imported
runBatchDesignDsl from the \`@zseven-w/pen-mcp\` package barrel.
That barrel re-exports node-only modules — document-manager,
log-utils, theme-presets — which import node:fs / node:path at
top level. Vite / esbuild resolve the barrel BEFORE tree-shaking
can drop those branches, so browser builds failed on unresolved
node built-ins.

Fix:
  - packages/pen-mcp/package.json: add \`./dsl\` subpath export
    pointing at tools/batch-design-dsl.ts — the pure executor
    file already guarded as browser-safe by the adjacent
    regression test.
  - apps/web dispatcher: switch import to
    \`@zseven-w/pen-mcp/dsl\`. No other changes — the re-exported
    symbols (runBatchDesignDsl / OpResult / ImageSearchFetcher /
    RunBatchDesignDslOptions) are identical shape.
  - batch-design-dsl-browser-safe.test.ts: add an assertion that
    package.json's exports field preserves the \`./dsl\` key
    pointing at the expected file. Without this, silently
    removing the subpath would re-introduce the browser-breaking
    resolution path.

The package barrel keeps its current export of runBatchDesignDsl
too (a few internal test files still import from it). Browser
callers should migrate to \`@zseven-w/pen-mcp/dsl\` per the JSDoc
note now in the dispatcher.
This commit is contained in:
Fini 2026-04-22 11:15:00 +08:00
parent e10b3a37c9
commit 0ce113c733
3 changed files with 53 additions and 3 deletions

View file

@ -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<D
try {
const { document } = useDocumentStore.getState();
const cloned = structuredClone(document) as typeof document;
// Route the DSL to whichever page the user is viewing. Without
// this, `runBatchDesignDsl` falls back to `doc.pages[0]` — on a
// multi-page document that means the generation lands on page 1
// while the UI shows the user's active page, producing invisible
// output. `activePageId` may be `null` on single-page docs, in
// which case the executor's default is correct (there's only one
// page).
const activePageId = useCanvasStore.getState().activePageId ?? undefined;
// runBatchDesignDsl mutates the doc in place + returns per-op
// results + errors. No image-search fetcher supplied — browser
// G() ops leave src empty, which the apps/web image pipeline
// (scanAndFillImages) will pick up and enrich after insert.
const { results, errors } = await runBatchDesignDsl(cloned, dsl, {});
const { results, errors } = await runBatchDesignDsl(cloned, dsl, {
pageId: activePageId,
});
if (errors.length > 0) {
return {
status: 'failed',
@ -480,7 +499,18 @@ async function applyBatchDesignDsl(dsl: string, ctx: DispatchContext): Promise<D
// still honors defaultParentId for the server-side element-tool
// paths that share the endpoint.
const message = err instanceof Error ? err.message : String(err);
const httpResult = await fallbackViaHttp('batch-design-dsl', 'batch_design', ctx, { dsl });
// Forward active pageId so the server-side path applies on the
// same page the user is viewing — same rationale as the in-browser
// path above. If activePageId is null (single-page doc), omit
// rather than send an explicit null so the server's default (first
// page) still works. The `pageId` field is read by
// server/api/mcp/exec-tool.post.ts:283 for the DSL branch.
const activePageId = useCanvasStore.getState().activePageId;
const dslBody: Record<string, unknown> =
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;

View file

@ -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": {

View file

@ -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<string, { types?: string; import?: string }>;
};
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$/);
});
});