@decocms/start 0.36.2 → 0.36.4

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.2",
3
+ "version": "0.36.4",
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",
@@ -72,6 +72,24 @@ export function transform(ctx: MigrationContext): void {
72
72
  }
73
73
  }
74
74
 
75
+ // Flag files with HTMX patterns for manual React migration
76
+ if (/\bhx-(?:get|post|put|delete|trigger|target|swap|on|indicator|sync|select)\b/.test(result.content)) {
77
+ ctx.manualReviewItems.push({
78
+ file: targetPath,
79
+ reason: "HTMX attributes (hx-*) found — needs manual migration to React state/effects. HTMX server-side rendering (hx-get/hx-post with useSection) must be converted to React components with useState/useEffect or server functions.",
80
+ severity: "warning",
81
+ });
82
+ }
83
+
84
+ // Flag files with hx-on:click that use useScript (simpler pattern)
85
+ if (/hx-on:click=\{useScript/.test(result.content)) {
86
+ ctx.manualReviewItems.push({
87
+ file: targetPath,
88
+ reason: "hx-on:click with useScript found — convert to onClick with React event handler. The useScript serialization won't work as onClick value.",
89
+ severity: "warning",
90
+ });
91
+ }
92
+
75
93
  if (ctx.dryRun) {
76
94
  if (result.changed) {
77
95
  log(ctx, `[DRY] Would transform: ${record.path} → ${targetPath}`);
@@ -325,6 +325,20 @@ const checks: Check[] = [
325
325
  return true;
326
326
  },
327
327
  },
328
+ {
329
+ name: "No HTMX attributes (hx-*) in components",
330
+ severity: "warning",
331
+ fn: (ctx) => {
332
+ const srcDir = path.join(ctx.sourceDir, "src");
333
+ if (!fs.existsSync(srcDir)) return true;
334
+ const bad = findFilesWithPattern(srcDir, /\bhx-(?:get|post|put|delete|patch|trigger|target|swap|on|indicator|sync|select)\b/);
335
+ if (bad.length > 0) {
336
+ console.log(` HTMX attributes found (needs manual React migration): ${bad.join(", ")}`);
337
+ return false;
338
+ }
339
+ return true;
340
+ },
341
+ },
328
342
  ];
329
343
 
330
344
  function findFilesWithPattern(
@@ -38,6 +38,13 @@ export function transformDenoIsms(content: string): TransformResult {
38
38
  notes.push("Removed npm: prefix from imports");
39
39
  }
40
40
 
41
+ // @ts-ignore → @ts-expect-error (TypeScript 5+ prefers @ts-expect-error)
42
+ if (/@ts-ignore/.test(result)) {
43
+ result = result.replace(/@ts-ignore/g, "@ts-expect-error");
44
+ changed = true;
45
+ notes.push("Replaced @ts-ignore with @ts-expect-error");
46
+ }
47
+
41
48
  // Remove Deno.* API usages — flag for manual review
42
49
  if (result.includes("Deno.")) {
43
50
  notes.push("MANUAL: Deno.* API usage found — needs Node.js equivalent");
@@ -1,13 +1,9 @@
1
1
  /**
2
2
  * Auto-configures known apps from CMS blocks.
3
3
  *
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.
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
11
7
  *
12
8
  * Usage in setup.ts:
13
9
  * import { autoconfigApps } from "@decocms/start/apps/autoconfig";
@@ -20,96 +16,58 @@ import { onChange } from "../cms/loader";
20
16
  import { resolveSecret } from "../sdk/crypto";
21
17
 
22
18
  // ---------------------------------------------------------------------------
23
- // Block key@decocms/apps module mapping
19
+ // Known app block keys dynamic import + configure
24
20
  // ---------------------------------------------------------------------------
25
21
 
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
39
- // ---------------------------------------------------------------------------
40
-
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>>;
47
- }
48
-
49
- /**
50
- * Import app mod using static imports.
51
- * CF Workers can't catch errors from dynamic string template imports —
52
- * the vite plugin crashes with AssertionError before the catch runs.
53
- * Each known app gets a case here. When adding a new app to BLOCK_TO_APP,
54
- * also add a case to this switch.
55
- */
56
- async function importAppMod(appName: string): Promise<AppMod | null> {
57
- try {
58
- switch (appName) {
59
- case "resend":
60
- return await import("@decocms/apps/resend/mod");
61
- default:
62
- return null;
63
- }
64
- } catch {
65
- return null;
66
- }
22
+ interface AppAutoconfigurator {
23
+ /** Try to import, configure, and return invoke actions for this app */
24
+ (blockData: unknown): Promise<Record<string, InvokeAction>>;
67
25
  }
68
26
 
69
- async function loadAndConfigureApp(
70
- blockKey: string,
71
- appName: string,
72
- blockData: unknown,
73
- ): Promise<Record<string, InvokeAction>> {
74
- const mod = await importAppMod(appName);
75
- if (!mod) return {};
76
-
77
- try {
78
- const ok = await mod.configure(blockData, resolveSecret);
79
- if (!ok) {
80
- console.warn(
81
- `[autoconfig] ${blockKey}: configure() returned false.` +
82
- ` Set DECO_CRYPTO_KEY to decrypt CMS secrets, or set the app's env var fallback.`,
83
- );
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
+ const handler: InvokeAction = async (props: any) => sendEmail(props);
56
+ return {
57
+ "resend/actions/emails/send": handler,
58
+ "resend/actions/emails/send.ts": handler,
59
+ };
60
+ } catch {
61
+ // @decocms/apps not installed or doesn't have resend — skip
84
62
  return {};
85
63
  }
86
-
87
- console.log(`[autoconfig] ${blockKey}: configured (${Object.keys(mod.handlers).length} handlers)`);
88
- return mod.handlers;
89
- } catch (e) {
90
- console.warn(`[autoconfig] ${blockKey}:`, e);
91
- return {};
92
- }
93
- }
64
+ },
65
+ };
94
66
 
95
67
  // ---------------------------------------------------------------------------
96
68
  // Main
97
69
  // ---------------------------------------------------------------------------
98
70
 
99
- async function configureAll(blocks: Record<string, unknown>): Promise<Record<string, InvokeAction>> {
100
- const actions: Record<string, InvokeAction> = {};
101
-
102
- for (const [blockKey, appName] of Object.entries(BLOCK_TO_APP)) {
103
- const block = blocks[blockKey];
104
- if (!block) continue;
105
-
106
- const appActions = await loadAndConfigureApp(blockKey, appName, block);
107
- Object.assign(actions, appActions);
108
- }
109
-
110
- return actions;
111
- }
112
-
113
71
  /**
114
72
  * Auto-configure apps from CMS blocks.
115
73
  * Call in setup.ts after setBlocks(). Also re-runs on admin hot-reload.
@@ -117,7 +75,19 @@ async function configureAll(blocks: Record<string, unknown>): Promise<Record<str
117
75
  export async function autoconfigApps(blocks: Record<string, unknown>) {
118
76
  if (typeof document !== "undefined") return; // server-only
119
77
 
120
- const actions = await configureAll(blocks);
78
+ const actions: Record<string, InvokeAction> = {};
79
+
80
+ for (const [blockKey, configurator] of Object.entries(KNOWN_APPS)) {
81
+ const block = blocks[blockKey];
82
+ if (!block) continue;
83
+
84
+ try {
85
+ const appActions = await configurator(block);
86
+ Object.assign(actions, appActions);
87
+ } catch (e) {
88
+ console.warn(`[autoconfig] ${blockKey}:`, e);
89
+ }
90
+ }
121
91
 
122
92
  if (Object.keys(actions).length > 0) {
123
93
  setInvokeActions(() => ({ ...actions }));
@@ -126,7 +96,14 @@ export async function autoconfigApps(blocks: Record<string, unknown>) {
126
96
  // Re-configure on admin hot-reload
127
97
  onChange(async (newBlocks) => {
128
98
  if (typeof document !== "undefined") return;
129
- const updatedActions = await configureAll(newBlocks);
99
+ const updatedActions: Record<string, InvokeAction> = {};
100
+ for (const [blockKey, configurator] of Object.entries(KNOWN_APPS)) {
101
+ const block = newBlocks[blockKey];
102
+ if (!block) continue;
103
+ try {
104
+ Object.assign(updatedActions, await configurator(block));
105
+ } catch {}
106
+ }
130
107
  if (Object.keys(updatedActions).length > 0) {
131
108
  setInvokeActions(() => ({ ...updatedActions }));
132
109
  }
package/src/sdk/crypto.ts CHANGED
@@ -34,7 +34,7 @@ function hexToBytes(hex: string): Uint8Array {
34
34
  * Returns null if not set.
35
35
  */
36
36
  function getKeyFromEnv(): Promise<{ key: CryptoKey; iv: Uint8Array }> | null {
37
- const envKey = process.env.DECO_CRYPTO_KEY;
37
+ const envKey = getEnvVar("DECO_CRYPTO_KEY");
38
38
  if (!envKey) return null;
39
39
 
40
40
  return cachedKey ??= (async () => {
@@ -62,7 +62,7 @@ function getKeyFromEnv(): Promise<{ key: CryptoKey; iv: Uint8Array }> | null {
62
62
  * Check if the crypto key is available.
63
63
  */
64
64
  export function hasCryptoKey(): boolean {
65
- return !!process.env.DECO_CRYPTO_KEY;
65
+ return !!getEnvVar("DECO_CRYPTO_KEY");
66
66
  }
67
67
 
68
68
  /**
@@ -142,9 +142,44 @@ export async function resolveSecret(
142
142
 
143
143
  // 4. Environment variable fallback
144
144
  if (envVarName) {
145
- const envValue = process.env[envVarName];
145
+ const envValue = getEnvVar(envVarName);
146
146
  if (envValue) return envValue;
147
147
  }
148
148
 
149
149
  return null;
150
150
  }
151
+
152
+ /**
153
+ * Get an environment variable, checking process.env first, then .dev.vars file.
154
+ * Cloudflare Workers dev mode stores env vars in .dev.vars but they're only
155
+ * accessible via the `env` binding inside request handlers. During setup.ts
156
+ * (module-level init), we need to read the file directly.
157
+ */
158
+ function getEnvVar(name: string): string | undefined {
159
+ // 1. process.env (works in Node, may work in Workers with nodejs_compat)
160
+ if (typeof process !== "undefined" && process.env?.[name]) {
161
+ return process.env[name];
162
+ }
163
+
164
+ // 2. Read .dev.vars file (Cloudflare Workers dev mode)
165
+ try {
166
+ const fs = require("node:fs");
167
+ const path = require("node:path");
168
+ const devVarsPath = path.resolve(".dev.vars");
169
+ if (fs.existsSync(devVarsPath)) {
170
+ const content = fs.readFileSync(devVarsPath, "utf-8");
171
+ for (const line of content.split("\n")) {
172
+ const trimmed = line.trim();
173
+ if (trimmed.startsWith("#") || !trimmed.includes("=")) continue;
174
+ const eqIdx = trimmed.indexOf("=");
175
+ const key = trimmed.slice(0, eqIdx).trim();
176
+ const val = trimmed.slice(eqIdx + 1).trim();
177
+ if (key === name) return val;
178
+ }
179
+ }
180
+ } catch {
181
+ // fs not available (e.g., pure Worker runtime) — ignore
182
+ }
183
+
184
+ return undefined;
185
+ }