@decocms/start 0.34.0 → 0.35.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.
package/package.json
CHANGED
|
@@ -275,15 +275,19 @@ const checks: Check[] = [
|
|
|
275
275
|
},
|
|
276
276
|
},
|
|
277
277
|
{
|
|
278
|
-
name: "
|
|
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
|
|
284
|
-
if (
|
|
285
|
-
|
|
286
|
-
|
|
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
|
},
|
|
@@ -1,5 +1,23 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
1
2
|
import type { MigrationContext } from "../types.ts";
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Get the latest published version of an npm package.
|
|
6
|
+
* Falls back to the provided default if the lookup fails.
|
|
7
|
+
*/
|
|
8
|
+
function getLatestVersion(pkg: string, fallback: string): string {
|
|
9
|
+
try {
|
|
10
|
+
const version = execSync(`npm view ${pkg} version`, {
|
|
11
|
+
encoding: "utf-8",
|
|
12
|
+
timeout: 10000,
|
|
13
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
14
|
+
}).trim();
|
|
15
|
+
return version || fallback;
|
|
16
|
+
} catch {
|
|
17
|
+
return fallback;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
3
21
|
/**
|
|
4
22
|
* Extract npm dependencies from deno.json import map.
|
|
5
23
|
* Entries like `"fuse.js": "npm:fuse.js@7.0.0"` become `"fuse.js": "^7.0.0"`.
|
|
@@ -40,6 +58,10 @@ export function generatePackageJson(ctx: MigrationContext): string {
|
|
|
40
58
|
...ctx.discoveredNpmDeps,
|
|
41
59
|
};
|
|
42
60
|
|
|
61
|
+
// Fetch latest versions from npm registry
|
|
62
|
+
const startVersion = getLatestVersion("@decocms/start", "0.34.0");
|
|
63
|
+
const appsVersion = getLatestVersion("@decocms/apps", "0.26.0");
|
|
64
|
+
|
|
43
65
|
const pkg = {
|
|
44
66
|
name: ctx.siteName,
|
|
45
67
|
version: "0.1.0",
|
|
@@ -72,8 +94,8 @@ export function generatePackageJson(ctx: MigrationContext): string {
|
|
|
72
94
|
author: "deco.cx",
|
|
73
95
|
license: "MIT",
|
|
74
96
|
dependencies: {
|
|
75
|
-
"@decocms/apps":
|
|
76
|
-
"@decocms/start":
|
|
97
|
+
"@decocms/apps": `^${appsVersion}`,
|
|
98
|
+
"@decocms/start": `^${startVersion}`,
|
|
77
99
|
"@tanstack/react-query": "5.90.21",
|
|
78
100
|
"@tanstack/react-router": "1.166.7",
|
|
79
101
|
"@tanstack/react-start": "1.166.8",
|
|
@@ -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
|
-
*
|
|
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
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
}
|