@decocms/apps 0.26.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Core types for the Deco app system on TanStack Start.
3
+ *
4
+ * Each app (vtex, shopify, resend, etc.) exports a `configure` function
5
+ * from its `mod.ts` that returns an `AppDefinition`.
6
+ *
7
+ * The framework's `autoconfigApps()` calls these generically.
8
+ */
9
+
10
+ import type { ComponentType } from "react";
11
+
12
+ export type AppHandler = (props: any, request: Request) => Promise<any>;
13
+
14
+ export interface SectionModule {
15
+ default: ComponentType<Record<string, unknown>>;
16
+ loader?: (...args: unknown[]) => Promise<unknown> | unknown;
17
+ LoadingFallback?: ComponentType;
18
+ ErrorFallback?: ComponentType<{ error: Error }>;
19
+ }
20
+
21
+ export interface AppManifest {
22
+ name: string;
23
+ /** Module namespace imports keyed by manifest path (e.g. "vtex/loaders/catalog"). */
24
+ loaders: Record<string, Record<string, unknown>>;
25
+ /** Module namespace imports keyed by manifest path (e.g. "vtex/actions/checkout"). */
26
+ actions: Record<string, Record<string, unknown>>;
27
+ /** Lazy-loaded section components keyed by manifest path (e.g. "vtex/sections/Analytics/Vtex"). */
28
+ sections?: Record<string, () => Promise<SectionModule>>;
29
+ }
30
+
31
+ export type AppMiddleware = (request: Request, next: () => Promise<Response>) => Promise<Response>;
32
+
33
+ export interface AppDefinition<TState = unknown> {
34
+ name: string;
35
+ manifest: AppManifest;
36
+ state: TState;
37
+ middleware?: AppMiddleware;
38
+ dependencies?: AppDefinition[];
39
+ resolvables?: Record<string, { __resolveType: string; [key: string]: unknown }>;
40
+ }
41
+
42
+ export type ResolveSecretFn = (value: unknown, envKey: string) => Promise<string | null>;
43
+
44
+ export interface AppPreview {
45
+ Component: ComponentType<Record<string, unknown>>;
46
+ props: Record<string, unknown>;
47
+ }
48
+
49
+ /**
50
+ * Standard contract for Deco apps with auto-configuration.
51
+ *
52
+ * Each app exports `configure` from its `mod.ts`.
53
+ * Apps that need invoke handlers (e.g. resend) also export a `handlers` map.
54
+ * The framework discovers and calls these generically.
55
+ */
56
+ export interface AppModContract<TState = unknown> {
57
+ configure: (
58
+ blockData: any,
59
+ resolveSecret: ResolveSecretFn,
60
+ ) => Promise<AppDefinition<TState> | null>;
61
+ handlers?: Record<string, AppHandler>;
62
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Utilities for extracting individual handlers from app manifests.
3
+ *
4
+ * Used by the framework to flatten module namespace imports into
5
+ * individual handler functions for setInvokeLoaders() / setInvokeActions().
6
+ */
7
+
8
+ import type { AppManifest } from "./app-types";
9
+
10
+ /**
11
+ * Extract individual handler functions from a manifest's module namespaces.
12
+ *
13
+ * Given a manifest with:
14
+ * loaders: { "vtex/loaders/catalog": { searchProducts, getProductByIdOrSku } }
15
+ *
16
+ * Returns:
17
+ * { "vtex/loaders/catalog/searchProducts": searchProducts, ... }
18
+ */
19
+ type AnyFn = (...args: never[]) => unknown;
20
+
21
+ export function extractHandlers(manifest: AppManifest): Record<string, AnyFn> {
22
+ const result: Record<string, AnyFn> = {};
23
+
24
+ for (const category of ["loaders", "actions"] as const) {
25
+ const modules = manifest[category];
26
+ for (const [moduleKey, moduleNamespace] of Object.entries(modules)) {
27
+ for (const [exportName, handler] of Object.entries(
28
+ moduleNamespace as Record<string, unknown>,
29
+ )) {
30
+ if (typeof handler === "function") {
31
+ result[`${moduleKey}/${exportName}`] = handler as AnyFn;
32
+ }
33
+ }
34
+ }
35
+ }
36
+
37
+ return result;
38
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * App composition utilities.
3
+ *
4
+ * Merges manifests and chains middleware from multiple AppDefinitions
5
+ * into a single resolved structure.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { resolveApps } from "@decocms/apps/commerce/resolve";
10
+ * import * as vtexApp from "@decocms/apps/vtex/mod";
11
+ * import * as resendApp from "@decocms/apps/resend/mod";
12
+ *
13
+ * const apps = await Promise.all([
14
+ * vtexApp.configure(blocks.vtex, resolveSecret),
15
+ * resendApp.configure(blocks.resend, resolveSecret),
16
+ * ]);
17
+ *
18
+ * const resolved = resolveApps(apps.filter(Boolean));
19
+ * // resolved.manifest — merged manifest from all apps
20
+ * // resolved.middleware — chained middleware (or undefined)
21
+ * ```
22
+ */
23
+
24
+ import type { AppDefinition, AppManifest, AppMiddleware } from "./app-types";
25
+
26
+ export interface ResolvedApps {
27
+ manifest: AppManifest;
28
+ middleware: AppMiddleware | undefined;
29
+ resolvables: Record<string, { __resolveType: string; [key: string]: unknown }>;
30
+ }
31
+
32
+ /**
33
+ * Resolve an array of app definitions into a single merged structure.
34
+ *
35
+ * - Manifests are merged (loaders/actions from all apps).
36
+ * - Middleware is chained in array order (first app runs outermost).
37
+ */
38
+ export function resolveApps(apps: AppDefinition[]): ResolvedApps {
39
+ const mergedManifest: AppManifest = {
40
+ name: "resolved",
41
+ loaders: {},
42
+ actions: {},
43
+ sections: {},
44
+ };
45
+
46
+ const middlewares: AppMiddleware[] = [];
47
+ const resolvables: Record<string, { __resolveType: string; [key: string]: unknown }> = {};
48
+
49
+ for (const app of flattenDependencies(apps)) {
50
+ Object.assign(mergedManifest.loaders, app.manifest.loaders);
51
+ Object.assign(mergedManifest.actions, app.manifest.actions);
52
+
53
+ if (app.manifest.sections) {
54
+ Object.assign(mergedManifest.sections!, app.manifest.sections);
55
+ }
56
+
57
+ if (app.resolvables) {
58
+ Object.assign(resolvables, app.resolvables);
59
+ }
60
+
61
+ if (app.middleware) {
62
+ middlewares.push(app.middleware);
63
+ }
64
+ }
65
+
66
+ return {
67
+ manifest: mergedManifest,
68
+ middleware: middlewares.length > 0 ? chainMiddleware(middlewares) : undefined,
69
+ resolvables,
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Flatten the dependency graph (depth-first, dependencies before dependents).
75
+ * Deduplicates by app name.
76
+ */
77
+ function flattenDependencies(apps: AppDefinition[]): AppDefinition[] {
78
+ const seen = new Set<string>();
79
+ const result: AppDefinition[] = [];
80
+
81
+ function visit(app: AppDefinition) {
82
+ if (seen.has(app.name)) return;
83
+ seen.add(app.name);
84
+
85
+ if (app.dependencies) {
86
+ for (const dep of app.dependencies) {
87
+ visit(dep);
88
+ }
89
+ }
90
+
91
+ result.push(app);
92
+ }
93
+
94
+ for (const app of apps) {
95
+ visit(app);
96
+ }
97
+
98
+ return result;
99
+ }
100
+
101
+ /**
102
+ * Chain multiple middleware functions into a single one.
103
+ * First middleware in the array runs outermost (wraps the rest).
104
+ */
105
+ function chainMiddleware(middlewares: AppMiddleware[]): AppMiddleware {
106
+ return async (request, next) => {
107
+ const run = async (i: number): Promise<Response> => {
108
+ if (i < 0) return next();
109
+ return middlewares[i](request, () => run(i - 1));
110
+ };
111
+
112
+ return run(middlewares.length - 1);
113
+ };
114
+ }
@@ -20,7 +20,4 @@ export const formatPrice = (
20
20
  price: number | undefined | null,
21
21
  currency = "BRL",
22
22
  locale = "pt-BR",
23
- ) =>
24
- price != null && Number.isFinite(price)
25
- ? formatter(currency, locale).format(price)
26
- : null;
23
+ ) => (price != null && Number.isFinite(price) ? formatter(currency, locale).format(price) : null);
package/package.json CHANGED
@@ -1,16 +1,20 @@
1
1
  {
2
2
  "name": "@decocms/apps",
3
- "version": "0.26.0",
3
+ "version": "0.28.0",
4
4
  "type": "module",
5
5
  "description": "Deco commerce apps for TanStack Start - Shopify, VTEX, commerce types, analytics utils",
6
6
  "exports": {
7
7
  "./commerce/types": "./commerce/types/commerce.ts",
8
+ "./commerce/app-types": "./commerce/app-types.ts",
9
+ "./commerce/resolve": "./commerce/resolve.ts",
10
+ "./commerce/manifest-utils": "./commerce/manifest-utils.ts",
8
11
  "./commerce/components/JsonLd": "./commerce/components/JsonLd.tsx",
9
12
  "./commerce/components/Image": "./commerce/components/Image.tsx",
10
13
  "./commerce/components/Picture": "./commerce/components/Picture.tsx",
11
14
  "./commerce/utils/*": "./commerce/utils/*.ts",
12
15
  "./commerce/sdk/*": "./commerce/sdk/*.ts",
13
16
  "./shopify": "./shopify/index.ts",
17
+ "./shopify/mod": "./shopify/mod.ts",
14
18
  "./shopify/client": "./shopify/client.ts",
15
19
  "./shopify/loaders/*": "./shopify/loaders/*.ts",
16
20
  "./shopify/actions/*": "./shopify/actions/*.ts",
@@ -18,6 +22,7 @@
18
22
  "./shopify/actions/user/*": "./shopify/actions/user/*.ts",
19
23
  "./shopify/utils/*": "./shopify/utils/*.ts",
20
24
  "./vtex": "./vtex/index.ts",
25
+ "./vtex/mod": "./vtex/mod.ts",
21
26
  "./vtex/client": "./vtex/client.ts",
22
27
  "./vtex/types": "./vtex/types.ts",
23
28
  "./vtex/actions": "./vtex/actions/index.ts",
@@ -37,13 +42,14 @@
37
42
  "./vtex/hooks/*": "./vtex/hooks/*.ts",
38
43
  "./vtex/inline-loaders/workflowProducts": "./vtex/inline-loaders/workflowProducts.ts",
39
44
  "./vtex/middleware": "./vtex/middleware.ts",
40
- "./vtex/invoke": "./vtex/invoke.ts",
41
45
  "./resend": "./resend/index.ts",
46
+ "./resend/mod": "./resend/mod.ts",
42
47
  "./resend/client": "./resend/client.ts",
43
48
  "./resend/types": "./resend/types.ts",
44
49
  "./resend/actions/send": "./resend/actions/send.ts"
45
50
  },
46
51
  "scripts": {
52
+ "generate:manifests": "tsx scripts/generate-manifests.ts",
47
53
  "typecheck": "tsc --noEmit",
48
54
  "lint:unused": "knip",
49
55
  "lint:unused:fix": "knip --fix",
@@ -73,7 +79,8 @@
73
79
  "shopify/",
74
80
  "vtex/",
75
81
  "resend/",
76
- "!**/__tests__/"
82
+ "!**/__tests__/",
83
+ "!scripts/"
77
84
  ],
78
85
  "engines": {
79
86
  "node": ">=18"
@@ -90,7 +97,7 @@
90
97
  },
91
98
  "devDependencies": {
92
99
  "@biomejs/biome": "^2.4.7",
93
- "@decocms/start": "^0.29.3",
100
+ "@decocms/start": "^0.38.0",
94
101
  "@semantic-release/exec": "^7.1.0",
95
102
  "@semantic-release/git": "^10.0.1",
96
103
  "@tanstack/react-query": "^5.90.21",
@@ -99,6 +106,7 @@
99
106
  "knip": "^5.86.0",
100
107
  "react": "^19.0.0",
101
108
  "react-dom": "^19.0.0",
109
+ "tsx": "^4.21.0",
102
110
  "typescript": "^5.9.3",
103
111
  "vitest": "^4.1.0"
104
112
  }
@@ -0,0 +1,15 @@
1
+ // AUTO-GENERATED by scripts/generate-manifests.ts — DO NOT EDIT
2
+ // This file is checked into source control and updated via: npm run generate:manifests
3
+ import * as actions_send from "./actions/send";
4
+
5
+ const manifest = {
6
+ name: "resend",
7
+ loaders: {},
8
+ actions: {
9
+ "resend/actions/send": actions_send,
10
+ },
11
+ sections: {},
12
+ } as const;
13
+
14
+ export type Manifest = typeof manifest;
15
+ export default manifest;
package/resend/mod.ts ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Resend app module — standard autoconfig contract.
3
+ *
4
+ * Exports `configure` and `handlers` following the AppModContract pattern.
5
+ * The framework's `autoconfigApps()` calls these generically — no hardcoded
6
+ * app knowledge needed in the framework.
7
+ */
8
+
9
+ import type { AppDefinition, AppHandler, ResolveSecretFn } from "../commerce/app-types";
10
+ import { sendEmail } from "./actions/send";
11
+ import { configureResend } from "./client";
12
+ import manifest from "./manifest.gen";
13
+ import type { ResendConfig } from "./types";
14
+
15
+ // -------------------------------------------------------------------------
16
+ // State
17
+ // -------------------------------------------------------------------------
18
+
19
+ export interface ResendState {
20
+ config: ResendConfig;
21
+ }
22
+
23
+ // -------------------------------------------------------------------------
24
+ // Configure
25
+ // -------------------------------------------------------------------------
26
+
27
+ /**
28
+ * Configure Resend from CMS block data.
29
+ * Returns an AppDefinition or null if missing credentials.
30
+ */
31
+ export async function configure(
32
+ block: any,
33
+ resolveSecret: ResolveSecretFn,
34
+ ): Promise<AppDefinition<ResendState> | null> {
35
+ const apiKey = await resolveSecret(block.apiKey, "RESEND_API_KEY");
36
+ if (!apiKey) return null;
37
+
38
+ const config: ResendConfig = {
39
+ apiKey,
40
+ emailFrom: block.emailFrom
41
+ ? `${block.emailFrom.name || "Contact"} <${block.emailFrom.domain || "onboarding@resend.dev"}>`
42
+ : undefined,
43
+ emailTo: block.emailTo,
44
+ subject: block.subject,
45
+ };
46
+
47
+ // Bridge: maintain global singleton for backward compat
48
+ configureResend(config);
49
+
50
+ return {
51
+ name: "resend",
52
+ manifest,
53
+ state: { config },
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Invoke handlers registered under /deco/invoke/{key}.
59
+ * Both with and without .ts suffix for compatibility.
60
+ */
61
+ export const handlers: Record<string, AppHandler> = {
62
+ "resend/actions/emails/send": (props) => sendEmail(props),
63
+ "resend/actions/emails/send.ts": (props) => sendEmail(props),
64
+ };
65
+
66
+ /** Placeholder preview for CMS editor — evolves when admin supports it. */
67
+ export const preview = undefined;
package/shopify/index.ts CHANGED
@@ -1,3 +1,6 @@
1
+ // App contract
2
+ export { configure, type ShopifyState } from "./mod";
3
+
1
4
  // Client & Config
2
5
 
3
6
  export { default as addItems } from "./actions/cart/addItems";
@@ -0,0 +1,39 @@
1
+ // AUTO-GENERATED by scripts/generate-manifests.ts — DO NOT EDIT
2
+ // This file is checked into source control and updated via: npm run generate:manifests
3
+
4
+ import * as actions_cart_addItems from "./actions/cart/addItems";
5
+ import * as actions_cart_updateCoupons from "./actions/cart/updateCoupons";
6
+ import * as actions_cart_updateItems from "./actions/cart/updateItems";
7
+ import * as actions_user_signIn from "./actions/user/signIn";
8
+ import * as actions_user_signUp from "./actions/user/signUp";
9
+ import * as loaders_cart from "./loaders/cart";
10
+ import * as loaders_ProductDetailsPage from "./loaders/ProductDetailsPage";
11
+ import * as loaders_ProductList from "./loaders/ProductList";
12
+ import * as loaders_ProductListingPage from "./loaders/ProductListingPage";
13
+ import * as loaders_RelatedProducts from "./loaders/RelatedProducts";
14
+ import * as loaders_shop from "./loaders/shop";
15
+ import * as loaders_user from "./loaders/user";
16
+
17
+ const manifest = {
18
+ name: "shopify",
19
+ loaders: {
20
+ "shopify/loaders/ProductDetailsPage": loaders_ProductDetailsPage,
21
+ "shopify/loaders/ProductList": loaders_ProductList,
22
+ "shopify/loaders/ProductListingPage": loaders_ProductListingPage,
23
+ "shopify/loaders/RelatedProducts": loaders_RelatedProducts,
24
+ "shopify/loaders/cart": loaders_cart,
25
+ "shopify/loaders/shop": loaders_shop,
26
+ "shopify/loaders/user": loaders_user,
27
+ },
28
+ actions: {
29
+ "shopify/actions/cart/addItems": actions_cart_addItems,
30
+ "shopify/actions/cart/updateCoupons": actions_cart_updateCoupons,
31
+ "shopify/actions/cart/updateItems": actions_cart_updateItems,
32
+ "shopify/actions/user/signIn": actions_user_signIn,
33
+ "shopify/actions/user/signUp": actions_user_signUp,
34
+ },
35
+ sections: {},
36
+ } as const;
37
+
38
+ export type Manifest = typeof manifest;
39
+ export default manifest;
package/shopify/mod.ts ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Shopify app module — standard autoconfig contract.
3
+ *
4
+ * Exports `configure` following the AppModContract pattern.
5
+ * The framework's `autoconfigApps()` calls these generically.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import * as shopifyApp from "@decocms/apps/shopify/mod";
10
+ *
11
+ * const app = await shopifyApp.configure(blocks["deco-shopify"], resolveSecret);
12
+ * if (app) {
13
+ * // app.manifest, app.state are available
14
+ * }
15
+ * ```
16
+ */
17
+
18
+ import type { AppDefinition, ResolveSecretFn } from "../commerce/app-types";
19
+ import { configureShopify, type ShopifyConfig } from "./client";
20
+ import manifest from "./manifest.gen";
21
+
22
+ // -------------------------------------------------------------------------
23
+ // State
24
+ // -------------------------------------------------------------------------
25
+
26
+ export interface ShopifyState {
27
+ config: ShopifyConfig;
28
+ }
29
+
30
+ // -------------------------------------------------------------------------
31
+ // Configure
32
+ // -------------------------------------------------------------------------
33
+
34
+ /**
35
+ * Configure the Shopify app from CMS block data.
36
+ * Returns an AppDefinition or null if required fields are missing.
37
+ */
38
+ export async function configure(
39
+ block: any,
40
+ resolveSecret: ResolveSecretFn,
41
+ ): Promise<AppDefinition<ShopifyState> | null> {
42
+ if (!block?.storeName) return null;
43
+
44
+ const storefrontAccessToken =
45
+ (await resolveSecret(block.storefrontAccessToken, "SHOPIFY_STOREFRONT_TOKEN")) ??
46
+ (typeof block.storefrontAccessToken === "string" ? block.storefrontAccessToken : null);
47
+
48
+ if (!storefrontAccessToken) return null;
49
+
50
+ const config: ShopifyConfig = {
51
+ storeName: block.storeName,
52
+ storefrontAccessToken,
53
+ publicUrl: block.publicUrl,
54
+ };
55
+
56
+ // Bridge: maintain global singleton for backward compat
57
+ configureShopify(config);
58
+
59
+ return {
60
+ name: "shopify",
61
+ manifest,
62
+ state: { config },
63
+ };
64
+ }
65
+
66
+ /** Placeholder preview for CMS editor — evolves when admin supports it. */
67
+ export const preview = undefined;