mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-10-30 00:27:43 +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
127 lines
3.5 KiB
TypeScript
127 lines
3.5 KiB
TypeScript
import { atom, computed } from "nanostores";
|
|
import type { DataSource, Resource, ResourceRequest } from "@webstudio-is/sdk";
|
|
import { restResourcesLoader } from "./router-utils";
|
|
import { computeExpression } from "@webstudio-is/project-build/runtime";
|
|
import { fetch } from "./fetch.client";
|
|
import { getResourceKey } from "./resource-utils";
|
|
|
|
const MAX_PENDING_RESOURCES = 5;
|
|
|
|
export { getResourceKey };
|
|
|
|
const queue = new Map<string, ResourceRequest>();
|
|
const pending = new Map<string, ResourceRequest>();
|
|
const cache = new Map<string, unknown>();
|
|
|
|
export const $resourcesCache = atom(cache);
|
|
|
|
const updateCache = () => {
|
|
$resourcesCache.set(new Map(cache));
|
|
};
|
|
|
|
const $pendingUpdater = atom({});
|
|
|
|
const updatePending = () => {
|
|
$pendingUpdater.set({});
|
|
};
|
|
|
|
export const $hasPendingResources = computed(
|
|
$pendingUpdater,
|
|
() => queue.size > 0 || pending.size > 0
|
|
);
|
|
|
|
const loadResources = async () => {
|
|
const list = Array.from(queue.values()).slice(0, MAX_PENDING_RESOURCES);
|
|
for (const resource of list) {
|
|
const key = getResourceKey(resource);
|
|
queue.delete(key);
|
|
pending.set(key, resource);
|
|
}
|
|
updatePending();
|
|
const response = await fetch(restResourcesLoader(), {
|
|
method: "POST",
|
|
body: JSON.stringify(Array.from(pending.values())),
|
|
});
|
|
if (response.ok) {
|
|
const results = new Map<string, unknown>(await response.json());
|
|
for (const [key, result] of results) {
|
|
cache.set(key, result);
|
|
pending.delete(key);
|
|
}
|
|
}
|
|
updateCache();
|
|
updatePending();
|
|
// restart loading until queue is empty
|
|
scheduleLoading();
|
|
};
|
|
|
|
let timeoutId: undefined | number;
|
|
|
|
const scheduleLoading = () => {
|
|
// scheduling will be restarted after finishing pending one
|
|
// skip when there is nothing in queue
|
|
if (pending.size > 0 || queue.size === 0) {
|
|
return;
|
|
}
|
|
window.clearTimeout(timeoutId);
|
|
// start loading after one second of "preload" call
|
|
timeoutId = window.setTimeout(loadResources, 1000);
|
|
};
|
|
|
|
export const preloadResource = (resource: ResourceRequest) => {
|
|
const key = getResourceKey(resource);
|
|
if (queue.has(key) || pending.has(key) || cache.has(key)) {
|
|
return;
|
|
}
|
|
// deduplicate resources in queue
|
|
queue.set(key, resource);
|
|
updatePending();
|
|
scheduleLoading();
|
|
};
|
|
|
|
export const invalidateResource = (resource: ResourceRequest) => {
|
|
const key = getResourceKey(resource);
|
|
cache.delete(key);
|
|
preloadResource(resource);
|
|
};
|
|
|
|
/**
|
|
* Invalidate the assets system resource.
|
|
* Call this when assets are uploaded, deleted, or modified to refresh expressions using assets.
|
|
*/
|
|
export const invalidateAssets = () => {
|
|
const url = "/$resources/assets";
|
|
// System resources always use GET with no params/headers/body
|
|
const systemResourceRequest: ResourceRequest = {
|
|
name: "assets",
|
|
method: "get",
|
|
url,
|
|
searchParams: [],
|
|
headers: [],
|
|
};
|
|
invalidateResource(systemResourceRequest);
|
|
};
|
|
|
|
export const computeResourceRequest = (
|
|
resource: Resource,
|
|
values: Map<DataSource["id"], unknown>
|
|
): ResourceRequest => {
|
|
const request: ResourceRequest = {
|
|
name: resource.name,
|
|
method: resource.method,
|
|
url: computeExpression(resource.url, values),
|
|
searchParams: (resource.searchParams ?? []).map(({ name, value }) => ({
|
|
name,
|
|
value: computeExpression(value, values),
|
|
})),
|
|
headers: resource.headers.map(({ name, value }) => ({
|
|
name,
|
|
value: computeExpression(value, values),
|
|
})),
|
|
};
|
|
if (resource.body !== undefined) {
|
|
request.body = computeExpression(resource.body, values);
|
|
}
|
|
return request;
|
|
};
|