@decocms/start 0.36.0 → 0.36.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "0.36.0",
3
+ "version": "0.36.1",
4
4
  "type": "module",
5
5
  "description": "Deco framework for TanStack Start - CMS bridge, admin protocol, hooks, schema generation",
6
6
  "main": "./src/index.ts",
@@ -1,9 +1,13 @@
1
1
  /**
2
2
  * Auto-configures known apps from CMS blocks.
3
3
  *
4
- * Scans the decofile for known app block keys (e.g. "deco-resend") and:
5
- * 1. Configures the app client with CMS-provided credentials
6
- * 2. Registers invoke handlers so `invoke.app.actions.*` works via the proxy
4
+ * Scans the decofile for block keys matching known apps and dynamically imports
5
+ * their `mod.ts` from @decocms/apps. Each app mod exports:
6
+ * - `configure(blockData, resolveSecret)` configures the app client
7
+ * - `handlers` → record of invoke handler keys → handler functions
8
+ *
9
+ * Zero hardcoded app logic in the framework — all app-specific code lives in
10
+ * @decocms/apps/{app}/mod.ts.
7
11
  *
8
12
  * Usage in setup.ts:
9
13
  * import { autoconfigApps } from "@decocms/start/apps/autoconfig";
@@ -16,57 +20,82 @@ import { onChange } from "../cms/loader";
16
20
  import { resolveSecret } from "../sdk/crypto";
17
21
 
18
22
  // ---------------------------------------------------------------------------
19
- // Known app block keys dynamic import + configure
23
+ // Block key@decocms/apps module mapping
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /**
27
+ * Maps CMS block keys (e.g. "deco-resend") to their @decocms/apps module path.
28
+ * To add a new app, just add an entry here — no other code changes needed.
29
+ */
30
+ const BLOCK_TO_APP: Record<string, string> = {
31
+ "deco-resend": "resend",
32
+ // "deco-analytics": "analytics",
33
+ // "deco-shopify": "shopify",
34
+ // "deco-vtex": "vtex",
35
+ };
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Generic app loader
20
39
  // ---------------------------------------------------------------------------
21
40
 
22
- interface AppAutoconfigurator {
23
- /** Try to import, configure, and return invoke actions for this app */
24
- (blockData: unknown): Promise<Record<string, InvokeAction>>;
41
+ interface AppMod {
42
+ configure: (
43
+ blockData: unknown,
44
+ resolveSecret: (value: unknown, envKey: string) => Promise<string | null>,
45
+ ) => Promise<boolean>;
46
+ handlers: Record<string, (props: any, request: Request) => Promise<any>>;
25
47
  }
26
48
 
27
- const KNOWN_APPS: Record<string, AppAutoconfigurator> = {
28
- "deco-resend": async (block: any) => {
29
- try {
30
- const [resendClient, resendActions] = await Promise.all([
31
- import("@decocms/apps/resend/client" as string),
32
- import("@decocms/apps/resend/actions/send" as string),
33
- ]);
34
- const { configureResend } = resendClient as { configureResend: (cfg: any) => void };
35
- const { sendEmail } = resendActions as { sendEmail: (props: any) => Promise<any> };
36
-
37
- const apiKey = await resolveSecret(block.apiKey, "RESEND_API_KEY");
38
- if (!apiKey) {
39
- console.warn(
40
- "[autoconfig] deco-resend: no API key found." +
41
- " Set DECO_CRYPTO_KEY to decrypt CMS secrets, or set RESEND_API_KEY as fallback.",
42
- );
43
- return {};
44
- }
45
-
46
- configureResend({
47
- apiKey,
48
- emailFrom: block.emailFrom
49
- ? `${block.emailFrom.name || "Contact"} ${block.emailFrom.domain || "<onboarding@resend.dev>"}`
50
- : undefined,
51
- emailTo: block.emailTo,
52
- subject: block.subject,
53
- });
54
-
55
- return {
56
- "resend/actions/emails/send.ts": async (props: any) =>
57
- sendEmail(props),
58
- };
59
- } catch {
60
- // @decocms/apps not installed or doesn't have resend — skip
49
+ async function loadAndConfigureApp(
50
+ blockKey: string,
51
+ appName: string,
52
+ blockData: unknown,
53
+ ): Promise<Record<string, InvokeAction>> {
54
+ try {
55
+ // Dynamic import of @decocms/apps/{appName}/mod
56
+ const mod: AppMod = await import(
57
+ /* @vite-ignore */ `@decocms/apps/${appName}/mod`
58
+ );
59
+
60
+ const ok = await mod.configure(blockData, resolveSecret);
61
+ if (!ok) {
62
+ console.warn(
63
+ `[autoconfig] ${blockKey}: configure() returned false.` +
64
+ ` Set DECO_CRYPTO_KEY to decrypt CMS secrets, or set the app's env var fallback.`,
65
+ );
61
66
  return {};
62
67
  }
63
- },
64
- };
68
+
69
+ console.log(`[autoconfig] ${blockKey}: configured (${Object.keys(mod.handlers).length} handlers)`);
70
+ return mod.handlers;
71
+ } catch (e) {
72
+ // @decocms/apps not installed or app module doesn't exist — skip silently
73
+ if ((e as any)?.code === "ERR_MODULE_NOT_FOUND" || (e as any)?.message?.includes("Cannot find")) {
74
+ return {};
75
+ }
76
+ console.warn(`[autoconfig] ${blockKey}:`, e);
77
+ return {};
78
+ }
79
+ }
65
80
 
66
81
  // ---------------------------------------------------------------------------
67
82
  // Main
68
83
  // ---------------------------------------------------------------------------
69
84
 
85
+ async function configureAll(blocks: Record<string, unknown>): Promise<Record<string, InvokeAction>> {
86
+ const actions: Record<string, InvokeAction> = {};
87
+
88
+ for (const [blockKey, appName] of Object.entries(BLOCK_TO_APP)) {
89
+ const block = blocks[blockKey];
90
+ if (!block) continue;
91
+
92
+ const appActions = await loadAndConfigureApp(blockKey, appName, block);
93
+ Object.assign(actions, appActions);
94
+ }
95
+
96
+ return actions;
97
+ }
98
+
70
99
  /**
71
100
  * Auto-configure apps from CMS blocks.
72
101
  * Call in setup.ts after setBlocks(). Also re-runs on admin hot-reload.
@@ -74,19 +103,7 @@ const KNOWN_APPS: Record<string, AppAutoconfigurator> = {
74
103
  export async function autoconfigApps(blocks: Record<string, unknown>) {
75
104
  if (typeof document !== "undefined") return; // server-only
76
105
 
77
- const actions: Record<string, InvokeAction> = {};
78
-
79
- for (const [blockKey, configurator] of Object.entries(KNOWN_APPS)) {
80
- const block = blocks[blockKey];
81
- if (!block) continue;
82
-
83
- try {
84
- const appActions = await configurator(block);
85
- Object.assign(actions, appActions);
86
- } catch (e) {
87
- console.warn(`[autoconfig] ${blockKey}:`, e);
88
- }
89
- }
106
+ const actions = await configureAll(blocks);
90
107
 
91
108
  if (Object.keys(actions).length > 0) {
92
109
  setInvokeActions(() => ({ ...actions }));
@@ -95,14 +112,7 @@ export async function autoconfigApps(blocks: Record<string, unknown>) {
95
112
  // Re-configure on admin hot-reload
96
113
  onChange(async (newBlocks) => {
97
114
  if (typeof document !== "undefined") return;
98
- const updatedActions: Record<string, InvokeAction> = {};
99
- for (const [blockKey, configurator] of Object.entries(KNOWN_APPS)) {
100
- const block = newBlocks[blockKey];
101
- if (!block) continue;
102
- try {
103
- Object.assign(updatedActions, await configurator(block));
104
- } catch {}
105
- }
115
+ const updatedActions = await configureAll(newBlocks);
106
116
  if (Object.keys(updatedActions).length > 0) {
107
117
  setInvokeActions(() => ({ ...updatedActions }));
108
118
  }