Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,092 changes: 56 additions & 1,036 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@
"./scripts/generate-invoke": "./scripts/generate-invoke.ts",
"./scripts/migrate": "./scripts/migrate.ts",
"./scripts/tailwind-lint": "./scripts/tailwind-lint.ts",
"./vite": "./src/vite/plugin.js"
"./vite": "./src/vite/plugin.js",
"./daemon": "./src/daemon/index.ts"
},
"scripts": {
"build": "tsc",
Expand Down Expand Up @@ -90,7 +91,10 @@
"access": "public"
},
"dependencies": {
"tsx": "^4.19.0"
"@deco-cx/warp-node": "^0.3.16",
"fast-json-patch": "^3.1.0",
"tsx": "^4.19.0",
"ws": "^8.18.0"
},
"peerDependencies": {
"@microlabs/otel-cf-workers": ">=1.0.0-rc.0",
Expand Down Expand Up @@ -119,6 +123,7 @@
"@tanstack/react-query": "^5.96.0",
"@tanstack/store": "^0.9.1",
"@types/react": "^19.0.0",
"@types/ws": "^8.18.0",
"@types/react-dom": "^19.0.0",
"jsdom": "^29.0.0",
"knip": "^5.86.0",
Expand Down
6 changes: 3 additions & 3 deletions src/admin/decofile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getRevision, loadBlocks, setBlocks } from "../cms/loader";
import { clearLoaderCache } from "../sdk/cachedLoader";
import { invalidateMetaCache } from "./meta";
import { getRevision, loadBlocks, setBlocks } from "../cms/loader.ts";
import { clearLoaderCache } from "../sdk/cachedLoader.ts";
import { invalidateMetaCache } from "./meta.ts";

export function handleDecofileRead(): Response {
const blocks = loadBlocks();
Expand Down
3 changes: 3 additions & 0 deletions src/admin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ export {
composeMeta,
getRegisteredLoaders,
getRegisteredMatchers,
type ActionConfig,
type LoaderConfig,
type MatcherConfig,
type MetaResponse,
registerActionSchema,
registerActionSchemas,
registerLoaderSchema,
registerLoaderSchemas,
registerMatcherSchema,
Expand Down
47 changes: 36 additions & 11 deletions src/admin/meta.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
import { djb2Hex } from "../sdk/djb2";
import { composeMeta, type MetaResponse } from "./schema";
import { djb2Hex } from "../sdk/djb2.ts";
import { composeMeta, type MetaResponse } from "./schema.ts";

let metaData: MetaResponse | null = null;
let cachedEtag: string | null = null;
// Use globalThis to share meta state across module instances.
// The daemon middleware imports this module via native import() (outside Vite SSR),
// while setup.ts calls setMetaData() via Vite SSR — these are different module instances.
// globalThis bridges them so both see the same metaData.
const G = globalThis as unknown as {
__deco_meta_data?: MetaResponse | null;
__deco_meta_etag?: string | null;
};

function getMetaData(): MetaResponse | null {
return G.__deco_meta_data ?? null;
}

function setMetaDataInternal(data: MetaResponse | null) {
G.__deco_meta_data = data;
}

function getCachedEtag(): string | null {
return G.__deco_meta_etag ?? null;
}

function setCachedEtag(etag: string | null) {
G.__deco_meta_etag = etag;
}

/**
* Invalidate the cached ETag so the admin re-fetches meta after a
Expand All @@ -12,7 +34,7 @@ let cachedEtag: string | null = null;
* needed here, keeping this module safe for client-side bundles.
*/
export function invalidateMetaCache() {
cachedEtag = null;
setCachedEtag(null);
}

/**
Expand All @@ -21,8 +43,8 @@ export function invalidateMetaCache() {
* on top of the site-generated section schemas.
*/
export function setMetaData(data: MetaResponse) {
metaData = composeMeta(data);
cachedEtag = null;
setMetaDataInternal(composeMeta(data));
setCachedEtag(null);
}

/**
Expand All @@ -31,14 +53,17 @@ export function setMetaData(data: MetaResponse) {
* results in a different ETag, forcing admin to re-fetch.
*/
function getEtag(): string {
if (!cachedEtag) {
const str = JSON.stringify(metaData || {});
cachedEtag = `"meta-${djb2Hex(str)}"`;
let etag = getCachedEtag();
if (!etag) {
const str = JSON.stringify(getMetaData() || {});
etag = `"meta-${djb2Hex(str)}"`;
setCachedEtag(etag);
}
return cachedEtag;
return etag;
}

export function handleMeta(request: Request): Response {
const metaData = getMetaData();
if (!metaData) {
return new Response(JSON.stringify({ error: "Schema not initialized" }), {
status: 503,
Expand Down
64 changes: 64 additions & 0 deletions src/admin/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,64 @@ function getProductListLoaderKeys(): string[] {
return loaderRegistry.filter((l) => l.tags?.includes("product-list")).map((l) => l.key);
}

// ---------------------------------------------------------------------------
// Action definitions — dynamic registry
// ---------------------------------------------------------------------------

export interface ActionConfig {
key: string;
title: string;
namespace: string;
propsSchema: Record<string, any>;
}

const actionRegistry: ActionConfig[] = [];

/** Register a single action schema for the admin. */
export function registerActionSchema(config: ActionConfig) {
const idx = actionRegistry.findIndex((a) => a.key === config.key);
if (idx >= 0) {
actionRegistry[idx] = config;
} else {
actionRegistry.push(config);
}
}

/** Register multiple action schemas at once. */
export function registerActionSchemas(configs: ActionConfig[]) {
for (const config of configs) registerActionSchema(config);
}

function buildActionDefinitions() {
const definitions: Record<string, any> = {};
const manifestBlocks: Record<string, any> = {};

for (const action of actionRegistry) {
const defKey = toBase64(action.key);

definitions[defKey] = {
title: action.key,
type: "object",
required: ["__resolveType"],
properties: {
__resolveType: {
type: "string",
enum: [action.key],
default: action.key,
},
props: action.propsSchema,
},
};

manifestBlocks[action.key] = {
$ref: `#/definitions/${defKey}`,
namespace: action.namespace,
};
}

return { definitions, manifestBlocks };
}

// ---------------------------------------------------------------------------
// Matcher definitions — dynamic registry
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -774,6 +832,7 @@ export function composeMeta(siteMeta: MetaResponse): MetaResponse {
const fullSectionAnyOf = [...siteAnyOf, ...fwSections.extraAnyOf];
const page = buildPageSchema(fullSectionAnyOf);
const loaders = buildLoaderDefinitions();
const actions = buildActionDefinitions();
const matchers = buildMatcherDefinitions();

const sectionRefDef = { title: "Section", anyOf: fullSectionAnyOf };
Expand All @@ -786,6 +845,7 @@ export function composeMeta(siteMeta: MetaResponse): MetaResponse {
...fwSections.definitions,
...page.definitions,
...loaders.definitions,
...actions.definitions,
...matchers.definitions,
[SECTION_REF_DEF_KEY]: sectionRefDef,
[RESOLVABLE_LITERAL_KEY]: resolvableDef,
Expand Down Expand Up @@ -813,6 +873,10 @@ export function composeMeta(siteMeta: MetaResponse): MetaResponse {
...(siteMeta.manifest?.blocks?.loaders || {}),
...loaders.manifestBlocks,
},
actions: {
...(siteMeta.manifest?.blocks?.actions || {}),
...actions.manifestBlocks,
},
matchers: {
...(siteMeta.manifest?.blocks?.matchers || {}),
...matchers.manifestBlocks,
Expand Down
3 changes: 3 additions & 0 deletions src/admin/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ export {
} from "./invoke";
export { setMetaData } from "./meta";
export {
type ActionConfig,
type LoaderConfig,
type MatcherConfig,
registerActionSchema,
registerActionSchemas,
registerLoaderSchema,
registerLoaderSchemas,
registerMatcherSchema,
Expand Down
2 changes: 1 addition & 1 deletion src/cms/loader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as asyncHooks from "node:async_hooks";
import { djb2Hex } from "../sdk/djb2";
import { djb2Hex } from "../sdk/djb2.ts";

export type Resolvable = {
__resolveType?: string;
Expand Down
32 changes: 32 additions & 0 deletions src/cms/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getOnBeforeResolveProps, getSection, registerOnBeforeResolveProps } fro
import { isLayoutSection, runSingleSectionLoader } from "./sectionLoaders";
import { normalizeUrlsInObject } from "../sdk/normalizeUrls";
import { djb2Hex } from "../sdk/djb2";
import { registerLoaderSchemas, registerActionSchemas, type LoaderConfig, type ActionConfig } from "../admin/schema";

// globalThis-backed: share state across Vite server function split modules
const G = globalThis as any;
Expand Down Expand Up @@ -307,6 +308,37 @@ export function registerCommerceLoader(key: string, loader: CommerceLoader) {

export function registerCommerceLoaders(loaders: Record<string, CommerceLoader>) {
Object.assign(commerceLoaders, loaders);

// Auto-register loader + action schemas for the admin manifest.
// Separate actions (keys containing "/actions/") from loaders.
const loaderConfigs: LoaderConfig[] = [];
const actionConfigs: ActionConfig[] = [];

for (const key of Object.keys(loaders)) {
const namespace = key.startsWith("vtex/") ? "vtex" : "site";
const schema = { type: "object" as const, additionalProperties: true };

if (key.includes("/actions/")) {
actionConfigs.push({ key, title: key, namespace, propsSchema: schema });
} else {
loaderConfigs.push({ key, title: key, namespace, propsSchema: schema, tags: inferLoaderTags(key) });
}
}

registerLoaderSchemas(loaderConfigs);
registerActionSchemas(actionConfigs);
}

function inferLoaderTags(key: string): string[] {
if (
key.includes("productList") ||
key.includes("ProductList") ||
key.includes("ProductShelf") ||
key.includes("SearchResult")
) {
return ["product-list"];
}
return [];
}

/** Delete a single commerce loader by key. No-op if key is absent. */
Expand Down
Loading