@decocms/start 0.34.1 → 0.35.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.34.1",
3
+ "version": "0.35.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",
@@ -40,6 +40,7 @@
40
40
  "./sdk/requestContext": "./src/sdk/requestContext.ts",
41
41
  "./sdk/createInvoke": "./src/sdk/createInvoke.ts",
42
42
  "./matchers/posthog": "./src/matchers/posthog.ts",
43
+ "./apps/autoconfig": "./src/apps/autoconfig.ts",
43
44
  "./matchers/builtins": "./src/matchers/builtins.ts",
44
45
  "./types/widgets": "./src/types/widgets.ts",
45
46
  "./routes": "./src/routes/index.ts",
@@ -275,15 +275,19 @@ const checks: Check[] = [
275
275
  },
276
276
  },
277
277
  {
278
- name: "No invoke.resend calls (should use sendEmail)",
278
+ name: "invoke.* calls have runtime.ts proxy available",
279
279
  severity: "warning",
280
280
  fn: (ctx) => {
281
281
  const srcDir = path.join(ctx.sourceDir, "src");
282
282
  if (!fs.existsSync(srcDir)) return true;
283
- const bad = findFilesWithPattern(srcDir, /invoke\.resend\./);
284
- if (bad.length > 0) {
285
- console.log(` Still uses invoke.resend: ${bad.join(", ")}`);
286
- return false;
283
+ const hasInvoke = findFilesWithPattern(srcDir, /\binvoke\.\w+\.\w+/);
284
+ if (hasInvoke.length > 0) {
285
+ // Check that runtime.ts exists and has the invoke proxy
286
+ const runtimePath = path.join(srcDir, "runtime.ts");
287
+ if (!fs.existsSync(runtimePath)) {
288
+ console.log(` Files use invoke.* but src/runtime.ts is missing: ${hasInvoke.join(", ")}`);
289
+ return false;
290
+ }
287
291
  }
288
292
  return true;
289
293
  },
@@ -43,11 +43,14 @@ import {
43
43
  setBlocks,
44
44
  } from "@decocms/start/cms";
45
45
  import { registerBuiltinMatchers } from "@decocms/start/matchers/builtins";
46
+ import { autoconfigApps } from "@decocms/start/apps/autoconfig";
46
47
 
47
48
  // -- CMS Blocks --
48
49
  // The Vite plugin intercepts the blocks.gen import and injects .deco/blocks/ data.
49
50
  if (typeof document === "undefined") {
50
51
  setBlocks(generatedBlocks);
52
+ // Auto-configure apps (Resend, etc.) from CMS blocks
53
+ autoconfigApps(generatedBlocks);
51
54
  }
52
55
 
53
56
  // -- Section Registry --
@@ -8,7 +8,10 @@ import type { TransformResult } from "../types.ts";
8
8
  * - `export const cacheKey = ...` (old cache key generation)
9
9
  * - `export const loader = (props, req, ctx) => ...` (old section loader pattern)
10
10
  * - `crypto.subtle.digestSync(...)` (Deno-only sync API)
11
- * - `invoke.resend.actions.emails.send(...)` → `sendEmail(...)` from @decocms/apps
11
+ *
12
+ * NOTE: invoke.* calls are NOT migrated — they are RPC calls to the server
13
+ * where the CMS config (API keys, etc.) is available. The runtime.ts invoke
14
+ * proxy handles routing them to /deco/invoke/*.
12
15
  */
13
16
  export function transformDeadCode(content: string): TransformResult {
14
17
  const notes: string[] = [];
@@ -68,33 +71,9 @@ export function transformDeadCode(content: string): TransformResult {
68
71
  notes.push("MANUAL: crypto.subtle.digestSync is Deno-only — replaced with crypto.subtle.digest (needs await)");
69
72
  }
70
73
 
71
- // Migrate invoke.resend.actions.emails.sendsendEmail from @decocms/apps
72
- if (result.includes("invoke.resend.actions.emails.send")) {
73
- // Replace the invoke call with sendEmail
74
- result = result.replace(
75
- /(?:await\s+)?invoke\.resend\.actions\.emails\.send\(/g,
76
- "await sendEmail(",
77
- );
78
-
79
- // Remove the old runtime import if only used for resend
80
- result = result.replace(
81
- /^import\s+\{\s*invoke\s*\}\s+from\s+["'][^"']*runtime["'];?\s*\n?/gm,
82
- "",
83
- );
84
-
85
- // Add the sendEmail import if not present
86
- if (!result.includes('"@decocms/apps/resend/actions/send"')) {
87
- result = `import { sendEmail } from "@decocms/apps/resend/actions/send";\n${result}`;
88
- }
89
-
90
- changed = true;
91
- notes.push("Migrated invoke.resend.actions.emails.send → sendEmail from @decocms/apps/resend");
92
- }
93
-
94
- // Generic invoke.X pattern — flag for manual review
95
- if (/\binvoke\.\w+\.\w+/.test(result) && !result.includes("sendEmail")) {
96
- notes.push("MANUAL: invoke.* calls found — need manual migration to server functions or @decocms/apps actions");
97
- }
74
+ // invoke.* calls are server RPC via runtime.ts proxy keep as-is
75
+ // The runtime.ts scaffolded file creates a proxy that routes to /deco/invoke/*
76
+ // where the CMS config (API keys, tokens) is available server-side.
98
77
 
99
78
  // Clean up blank lines
100
79
  result = result.replace(/\n{3,}/g, "\n\n");
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Auto-configures known apps from CMS blocks.
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
7
+ *
8
+ * Usage in setup.ts:
9
+ * import { autoconfigApps } from "@decocms/start/apps/autoconfig";
10
+ * setBlocks(generatedBlocks);
11
+ * await autoconfigApps(generatedBlocks);
12
+ */
13
+
14
+ import { setInvokeActions, type InvokeAction } from "../admin/invoke";
15
+ import { onChange } from "../cms/loader";
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Known app block keys → dynamic import + configure
19
+ // ---------------------------------------------------------------------------
20
+
21
+ interface AppAutoconfigurator {
22
+ /** Try to import, configure, and return invoke actions for this app */
23
+ (blockData: unknown): Promise<Record<string, InvokeAction>>;
24
+ }
25
+
26
+ const KNOWN_APPS: Record<string, AppAutoconfigurator> = {
27
+ "deco-resend": async (block: any) => {
28
+ try {
29
+ const [resendClient, resendActions] = await Promise.all([
30
+ import("@decocms/apps/resend/client" as string),
31
+ import("@decocms/apps/resend/actions/send" as string),
32
+ ]);
33
+ const { configureResend } = resendClient as { configureResend: (cfg: any) => void };
34
+ const { sendEmail } = resendActions as { sendEmail: (props: any) => Promise<any> };
35
+
36
+ const apiKey =
37
+ typeof block.apiKey === "string"
38
+ ? block.apiKey
39
+ : block.apiKey?.get?.() ?? "";
40
+
41
+ if (!apiKey) return {};
42
+
43
+ configureResend({
44
+ apiKey,
45
+ emailFrom: block.emailFrom
46
+ ? `${block.emailFrom.name || "Contact"} ${block.emailFrom.domain || "<onboarding@resend.dev>"}`
47
+ : undefined,
48
+ emailTo: block.emailTo,
49
+ subject: block.subject,
50
+ });
51
+
52
+ return {
53
+ "resend/actions/emails/send.ts": async (props: any) =>
54
+ sendEmail(props),
55
+ };
56
+ } catch {
57
+ // @decocms/apps not installed or doesn't have resend — skip
58
+ return {};
59
+ }
60
+ },
61
+ };
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Main
65
+ // ---------------------------------------------------------------------------
66
+
67
+ /**
68
+ * Auto-configure apps from CMS blocks.
69
+ * Call in setup.ts after setBlocks(). Also re-runs on admin hot-reload.
70
+ */
71
+ export async function autoconfigApps(blocks: Record<string, unknown>) {
72
+ if (typeof document !== "undefined") return; // server-only
73
+
74
+ const actions: Record<string, InvokeAction> = {};
75
+
76
+ for (const [blockKey, configurator] of Object.entries(KNOWN_APPS)) {
77
+ const block = blocks[blockKey];
78
+ if (!block) continue;
79
+
80
+ try {
81
+ const appActions = await configurator(block);
82
+ Object.assign(actions, appActions);
83
+ } catch (e) {
84
+ console.warn(`[autoconfig] ${blockKey}:`, e);
85
+ }
86
+ }
87
+
88
+ if (Object.keys(actions).length > 0) {
89
+ setInvokeActions(() => ({ ...actions }));
90
+ }
91
+
92
+ // Re-configure on admin hot-reload
93
+ onChange(async (newBlocks) => {
94
+ if (typeof document !== "undefined") return;
95
+ const updatedActions: Record<string, InvokeAction> = {};
96
+ for (const [blockKey, configurator] of Object.entries(KNOWN_APPS)) {
97
+ const block = newBlocks[blockKey];
98
+ if (!block) continue;
99
+ try {
100
+ Object.assign(updatedActions, await configurator(block));
101
+ } catch {}
102
+ }
103
+ if (Object.keys(updatedActions).length > 0) {
104
+ setInvokeActions(() => ({ ...updatedActions }));
105
+ }
106
+ });
107
+ }