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:
parent
e10b3a37c9
commit
0ce113c733
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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$/);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue