mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 05:26:24 +09:00
## Summary This improves how agents discover, author, inspect, and verify Webstudio projects while reducing unnecessary MCP context usage. ## What changed - Adds an **Agents** section to Project Settings with ready-to-run setup commands and client configuration for Claude Code, Codex, Cursor, and VS Code. - Makes large list and schema responses compact and paginated by default, with **verbose** available when complete records or diagnostics are needed. - Applies consistent compact output to assets, instances, styles, design tokens, CLI schemas, and project-session metadata. - Returns actionable expression warnings for props, resources, and Collection bindings, including unknown variables and risky render-time mutation resources. - Improves Collection expression guidance so agents can bind array items with the correct scoped variables. - Reports rendered audit outcomes explicitly as complete, partial, confirmation-required, or failed. - Expands runtime output schemas so MCP clients can validate responses reliably. - Strengthens the packaged CLI smoke workflow with a realistic seven-page stdio MCP scenario. ## Architecture - Consolidates `project-build` from 65 file-oriented entrypoints to 11 architecture-owned surfaces. - Separates the core model, contracts, runtime authoring, transfer, state, project sessions, MCP, visual tooling, persistence codecs, and authenticated server services. - Isolates Node-only JSX evaluation from browser-safe transfer utilities and authenticated server modules. - Keeps persistence codecs independent from authenticated database services, preventing unrelated module initialization. - Defines shared instance-path types once and reuses them across runtime modules. - Preserves Immer initialization through explicit package side-effect metadata. - Adds package tests that prevent a return to source-file entrypoints or generic exposure of Node-only surfaces. ## Validation - Full repository checks, tests, typechecks, generated-file checks, and fixtures - All 163 Builder test files and 2,141 tests - All 71 project-build test files and 1,794 tests - Lint and package-boundary validation - Full packed CLI release smoke test, including install, preview, agent setup, seven-page stdio MCP workflow, rendered audit, and structured commands
201 lines
5.0 KiB
TypeScript
201 lines
5.0 KiB
TypeScript
import { nanoid } from "nanoid";
|
|
import { z } from "zod";
|
|
import { loadById, patchBuild } from "@webstudio-is/project/index.server";
|
|
import type { CompactBuild } from "@webstudio-is/project-build";
|
|
import {
|
|
loadBuildById,
|
|
loadDevBuildByProjectId,
|
|
} from "@webstudio-is/project-build/server";
|
|
import { buildPatchTransaction } from "@webstudio-is/protocol/schema";
|
|
import type { AppContext } from "@webstudio-is/trpc-interface/index.server";
|
|
import { serializePages } from "@webstudio-is/project-migrations/pages";
|
|
import { assertApiProjectPermit } from "./api-permits.server";
|
|
import { throwApiError } from "./api-errors.server";
|
|
|
|
export const loadBuildByProjectVersion = async (
|
|
ctx: AppContext,
|
|
projectId: string,
|
|
version: number
|
|
) => {
|
|
const build = await ctx.postgrest.client
|
|
.from("Build")
|
|
.select("id")
|
|
.eq("projectId", projectId)
|
|
.eq("version", version)
|
|
.is("deployment", null)
|
|
.order("createdAt", { ascending: false })
|
|
.limit(1);
|
|
|
|
if (build.error) {
|
|
throw build.error;
|
|
}
|
|
const buildId = build.data.at(0)?.id;
|
|
if (buildId === undefined) {
|
|
return throwApiError("NOT_FOUND", "Build version not found for project");
|
|
}
|
|
|
|
return await loadBuildById(ctx, buildId);
|
|
};
|
|
|
|
export const loadReadableDevBuild = async (
|
|
ctx: AppContext,
|
|
projectId: string
|
|
) => {
|
|
await assertApiProjectPermit(ctx, projectId, "view");
|
|
return await loadDevBuildByProjectId(ctx, projectId);
|
|
};
|
|
|
|
const buildInclude = z.enum([
|
|
"pages",
|
|
"folders",
|
|
"instances",
|
|
"props",
|
|
"styles",
|
|
"styleSources",
|
|
"styleSourceSelections",
|
|
"designTokens",
|
|
"assets",
|
|
"resources",
|
|
"variables",
|
|
"breakpoints",
|
|
"projectSettings",
|
|
"marketplaceProduct",
|
|
]);
|
|
|
|
export type BuildInclude = z.infer<typeof buildInclude>;
|
|
|
|
export const buildGetInput = z.object({
|
|
projectId: z.string(),
|
|
include: z.array(buildInclude).optional(),
|
|
version: z.number().int().optional(),
|
|
});
|
|
|
|
export const buildPatchInput = z.object({
|
|
projectId: z.string(),
|
|
baseVersion: z.number().int(),
|
|
transactions: z.array(buildPatchTransaction).min(1),
|
|
});
|
|
|
|
export const createBuildSnapshot = ({
|
|
build,
|
|
include,
|
|
projectId,
|
|
}: {
|
|
build: Awaited<ReturnType<typeof loadDevBuildByProjectId>>;
|
|
include: Set<BuildInclude>;
|
|
projectId: string;
|
|
}) => {
|
|
const snapshot: Record<string, unknown> = {
|
|
projectId,
|
|
buildId: build.id,
|
|
version: build.version,
|
|
};
|
|
const add = (name: BuildInclude, value: unknown) => {
|
|
if (include.has(name)) {
|
|
snapshot[name] = value;
|
|
}
|
|
};
|
|
|
|
if (include.has("pages") || include.has("folders")) {
|
|
const pages = serializePages(build.pages);
|
|
snapshot.homePageId = pages.homePageId;
|
|
snapshot.rootFolderId = pages.rootFolderId;
|
|
if (include.has("pages")) {
|
|
snapshot.redirects = pages.redirects;
|
|
snapshot.pageTemplates = pages.pageTemplates;
|
|
}
|
|
add("pages", pages.pages);
|
|
add("folders", pages.folders);
|
|
}
|
|
add("projectSettings", build.projectSettings);
|
|
add("instances", build.instances);
|
|
add("props", build.props);
|
|
add("styles", build.styles);
|
|
add("styleSources", build.styleSources);
|
|
add("styleSourceSelections", build.styleSourceSelections);
|
|
if (include.has("designTokens")) {
|
|
snapshot.designTokens = build.styleSources.filter(
|
|
(styleSource) => styleSource.type === "token"
|
|
);
|
|
}
|
|
add("resources", build.resources);
|
|
add("variables", build.dataSources);
|
|
add("breakpoints", build.breakpoints);
|
|
add("marketplaceProduct", build.marketplaceProduct);
|
|
|
|
return snapshot;
|
|
};
|
|
|
|
export const serializeProjectSummary = (
|
|
project: Awaited<ReturnType<typeof loadById>>
|
|
) => {
|
|
if (project === null) {
|
|
throwApiError("NOT_FOUND", "Project not found");
|
|
}
|
|
|
|
return {
|
|
id: project.id,
|
|
name: project.title,
|
|
domain: project.domain ?? undefined,
|
|
createdAt: project.createdAt,
|
|
updatedAt:
|
|
project.latestBuildVirtual?.createdAt ??
|
|
project.latestStaticBuild?.updatedAt ??
|
|
project.createdAt,
|
|
};
|
|
};
|
|
|
|
export const commitBuildTransactions = async ({
|
|
ctx,
|
|
projectId,
|
|
buildId,
|
|
clientVersion,
|
|
transactions,
|
|
}: {
|
|
ctx: AppContext;
|
|
projectId: string;
|
|
buildId: string;
|
|
clientVersion: number;
|
|
transactions: z.infer<typeof buildPatchTransaction>[];
|
|
}) => {
|
|
const result = await patchBuild(
|
|
{
|
|
buildId,
|
|
projectId,
|
|
clientVersion,
|
|
transactions,
|
|
},
|
|
ctx
|
|
);
|
|
if (result.status === "version_mismatched") {
|
|
throwApiError("CONFLICT", result.errors);
|
|
}
|
|
if (result.status === "error") {
|
|
throwApiError("BAD_REQUEST", result.errors);
|
|
}
|
|
if (result.status !== "ok") {
|
|
return throwApiError("BAD_REQUEST", "Unexpected patch result");
|
|
}
|
|
return { version: result.version };
|
|
};
|
|
|
|
export const commitBuildPatch = async ({
|
|
build,
|
|
ctx,
|
|
projectId,
|
|
payload,
|
|
}: {
|
|
build: CompactBuild;
|
|
ctx: AppContext;
|
|
projectId: string;
|
|
payload: z.infer<typeof buildPatchTransaction>["payload"];
|
|
}) => {
|
|
return commitBuildTransactions({
|
|
ctx,
|
|
projectId,
|
|
buildId: build.id,
|
|
clientVersion: build.version,
|
|
transactions: [{ id: nanoid(), payload }],
|
|
});
|
|
};
|