@decocms/start 4.3.0 → 4.5.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.
@@ -1,6 +1,17 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import type { MigrationContext } from "../types";
3
3
 
4
+ /**
5
+ * Fleet-wide canonical bun version. Bumped here propagates to all newly
6
+ * migrated sites via the `packageManager` field AND the
7
+ * `lockfile-check.yml` workflow's `bun-version` input. See
8
+ * MIGRATION_TOOLING_PLAN.md for the bun-canonical decision.
9
+ *
10
+ * Exported because the lockfile-check workflow template reads it
11
+ * directly to keep both files in lockstep.
12
+ */
13
+ export const CANONICAL_BUN_VERSION = "1.3.5";
14
+
4
15
  /**
5
16
  * Get the latest published version of an npm package.
6
17
  * Falls back to the provided default if the lookup fails.
@@ -120,7 +131,7 @@ export function generatePackageJson(ctx: MigrationContext): string {
120
131
  "format:check": 'prettier --check "src/**/*.{ts,tsx}"',
121
132
  knip: "knip",
122
133
  clean:
123
- "rm -rf node_modules .cache dist .wrangler/state node_modules/.vite && npm install",
134
+ "rm -rf node_modules .cache dist .wrangler/state node_modules/.vite && bun install",
124
135
  "tailwind:lint":
125
136
  "tsx scripts/tailwind-lint.ts",
126
137
  "tailwind:fix":
@@ -128,6 +139,7 @@ export function generatePackageJson(ctx: MigrationContext): string {
128
139
  },
129
140
  author: "deco.cx",
130
141
  license: "MIT",
142
+ packageManager: `bun@${CANONICAL_BUN_VERSION}`,
131
143
  dependencies: {
132
144
  "@decocms/apps": `^${appsVersion}`,
133
145
  "@decocms/start": `^${startVersion}`,
@@ -42,11 +42,23 @@ function generateWorkerEntry(ctx: MigrationContext): string {
42
42
  return `/**
43
43
  * Cloudflare Worker entry point.
44
44
  *
45
- * Wraps TanStack Start with admin protocol handlers and edge caching.
45
+ * Wraps TanStack Start with admin protocol handlers, edge caching, and
46
+ * the @decocms/start observability stack:
47
+ * - structured JSON logger (console.log) -> Cloudflare Workers Logs ->
48
+ * CF-managed OTLP push when wrangler.jsonc has the
49
+ * observability.logs.destinations block
50
+ * - @opentelemetry/api global tracer bridge (so withTracing() spans flow
51
+ * to whichever destination CF tracing pushes to)
52
+ * - request/cache metrics to AE (DECO_METRICS binding)
53
+ *
54
+ * Run \`npx -p @decocms/start deco-cf-observability --write\` after wiring
55
+ * wrangler.jsonc to add the CF-native observability block. See
56
+ * https://github.com/decocms/deco-start#observability
46
57
  */
47
58
  import "./setup";
48
59
  import handler, { createServerEntry } from "@tanstack/react-start/server-entry";
49
60
  import { createDecoWorkerEntry } from "@decocms/start/sdk/workerEntry";
61
+ import { instrumentWorker } from "@decocms/start/sdk/observability";
50
62
  import {
51
63
  handleMeta,
52
64
  handleDecofileRead,
@@ -60,7 +72,7 @@ ${isCommerce ? `
60
72
  ` : ""}
61
73
  const serverEntry = createServerEntry({ fetch: handler.fetch });
62
74
 
63
- export default createDecoWorkerEntry(serverEntry, {
75
+ const decoWorker = createDecoWorkerEntry(serverEntry, {
64
76
  admin: {
65
77
  handleMeta,
66
78
  handleDecofileRead,
@@ -69,6 +81,10 @@ export default createDecoWorkerEntry(serverEntry, {
69
81
  corsHeaders,
70
82
  },
71
83
  });
84
+
85
+ export default instrumentWorker(decoWorker, {
86
+ serviceName: "${ctx.siteName}",
87
+ });
72
88
  `;
73
89
  }
74
90
 
@@ -78,6 +94,7 @@ function generateVtexWorkerEntry(ctx: MigrationContext): string {
78
94
  return `import "./setup";
79
95
  import handler, { createServerEntry } from "@tanstack/react-start/server-entry";
80
96
  import { createDecoWorkerEntry } from "@decocms/start/sdk/workerEntry";
97
+ import { instrumentWorker } from "@decocms/start/sdk/observability";
81
98
  import {
82
99
  handleMeta,
83
100
  handleDecofileRead,
@@ -164,7 +181,7 @@ const decoWorker = createDecoWorkerEntry(serverEntry, {
164
181
  // A/B wrapper — KV-driven traffic split between TanStack and legacy origin
165
182
  // ---------------------------------------------------------------------------
166
183
 
167
- export default withABTesting(decoWorker, {
184
+ const abTestedWorker = withABTesting(decoWorker, {
168
185
  kvBinding: "SITES_KV",
169
186
  preHandler: (request, url) => {
170
187
  const redirect = matchRedirect(url.pathname, cmsRedirects);
@@ -183,6 +200,25 @@ export default withABTesting(decoWorker, {
183
200
  return shouldProxyToVtex(url.pathname);
184
201
  },
185
202
  });
203
+
204
+ // ---------------------------------------------------------------------------
205
+ // Observability wrap (outermost layer)
206
+ //
207
+ // As of @decocms/start 4.4.0, instrumentWorker uses Cloudflare-native
208
+ // observability by default:
209
+ // - logs: console.* -> CF Workers Logs -> CF-managed OTLP push
210
+ // (when wrangler.jsonc has observability.logs.destinations)
211
+ // - traces: @opentelemetry/api global tracer (bridged from withTracing)
212
+ // -> CF Workers Tracing -> CF-managed OTLP push
213
+ // - metrics: AE (DECO_METRICS) + app-side OTLP (until CF ships
214
+ // OTLP metrics export)
215
+ //
216
+ // Run \`npx -p @decocms/start deco-cf-observability --write\` after
217
+ // wiring wrangler.jsonc to inject the CF-native observability block.
218
+ // ---------------------------------------------------------------------------
219
+ export default instrumentWorker(abTestedWorker, {
220
+ serviceName: "${ctx.siteName}",
221
+ });
186
222
  `;
187
223
  }
188
224
 
@@ -78,9 +78,11 @@ function showHelp() {
78
78
  --fix Auto-apply mechanical fixes for the safe rules
79
79
  (dead-lib-shims, dead-runtime-shim, local-widgets-types,
80
80
  vtex-shim-regression swap subset, obsolete-vite-plugins,
81
- local-framework-duplicate auto-fixable subset).
82
- Other rules — including htmx-residue and the warn-only
83
- entries of local-framework-duplicate stay detect-only.
81
+ local-framework-duplicate auto-fixable subset,
82
+ lockfile-multiple, package-manager-missing).
83
+ Other rulesincluding htmx-residue, lockfile-missing,
84
+ lockfile-drift, and the warn-only entries of
85
+ local-framework-duplicate — stay detect-only.
84
86
  --json Emit machine-readable JSON instead of pretty text
85
87
  --strict Exit code 2 if any warning-severity findings exist
86
88
  --help, -h Show this help
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Smoke tests for the `migrate-to-cf-observability.ts` codemod.
3
+ *
4
+ * Drives the script as a child process against tmp wrangler.jsonc fixtures.
5
+ * Verifies the three behaviors that matter operationally:
6
+ * - replacing an existing `observability.logs` block (lebiscuit shape)
7
+ * - appending a new `observability` block when none exists
8
+ * - second run is a no-op (idempotency / CI guard)
9
+ * - result is valid JSONC (parses after stripping comments)
10
+ */
11
+ import * as cp from "node:child_process";
12
+ import * as fs from "node:fs";
13
+ import * as os from "node:os";
14
+ import * as path from "node:path";
15
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
16
+
17
+ const SCRIPT = path.resolve(__dirname, "migrate-to-cf-observability.ts");
18
+
19
+ function runCodemod(args: string[]): { stdout: string; stderr: string; code: number } {
20
+ const r = cp.spawnSync("npx", ["tsx", SCRIPT, ...args], { encoding: "utf8" });
21
+ return { stdout: r.stdout || "", stderr: r.stderr || "", code: r.status ?? 0 };
22
+ }
23
+
24
+ function stripJsoncComments(s: string): string {
25
+ return s.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
26
+ }
27
+
28
+ describe("migrate-to-cf-observability codemod", () => {
29
+ let tmpDir: string;
30
+ let wranglerPath: string;
31
+
32
+ beforeEach(() => {
33
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "cf-codemod-"));
34
+ wranglerPath = path.join(tmpDir, "wrangler.jsonc");
35
+ });
36
+
37
+ afterEach(() => {
38
+ fs.rmSync(tmpDir, { recursive: true, force: true });
39
+ });
40
+
41
+ it("replaces an existing observability.logs block in lebiscuit-shape config", () => {
42
+ fs.writeFileSync(
43
+ wranglerPath,
44
+ `{
45
+ "name": "lebiscuit-tanstack",
46
+ "compatibility_date": "2026-02-14",
47
+ "main": "./src/worker-entry.ts",
48
+ "kv_namespaces": [{ "binding": "SITES_KV", "id": "abc" }],
49
+ "version_metadata": { "binding": "CF_VERSION_METADATA" },
50
+ "analytics_engine_datasets": [
51
+ { "binding": "DECO_METRICS", "dataset": "deco_metrics_lebiscuit" }
52
+ ],
53
+ "observability": {
54
+ "logs": {
55
+ "enabled": true,
56
+ "invocation_logs": true
57
+ }
58
+ }
59
+ }
60
+ `,
61
+ );
62
+
63
+ const r = runCodemod(["--source", tmpDir, "--write"]);
64
+ expect(r.code).toBe(0);
65
+
66
+ const result = fs.readFileSync(wranglerPath, "utf8");
67
+ expect(result).toContain('"destinations": ["hyperdx-logs"]');
68
+ expect(result).toContain('"destinations": ["hyperdx-traces"]');
69
+ expect(result).toContain('"head_sampling_rate": 0.1');
70
+
71
+ // Result must be valid JSONC.
72
+ expect(() => JSON.parse(stripJsoncComments(result))).not.toThrow();
73
+
74
+ // Original key context preserved.
75
+ expect(result).toContain('"name": "lebiscuit-tanstack"');
76
+ expect(result).toContain('"binding": "DECO_METRICS"');
77
+ });
78
+
79
+ it("appends a new observability block when none exists", () => {
80
+ fs.writeFileSync(
81
+ wranglerPath,
82
+ `{
83
+ "name": "fresh-site",
84
+ "compatibility_date": "2026-02-14",
85
+ "main": "./src/worker-entry.ts"
86
+ }
87
+ `,
88
+ );
89
+
90
+ const r = runCodemod(["--source", tmpDir, "--write"]);
91
+ expect(r.code).toBe(0);
92
+
93
+ const result = fs.readFileSync(wranglerPath, "utf8");
94
+ expect(result).toContain('"observability"');
95
+ expect(result).toContain('"destinations": ["hyperdx-logs"]');
96
+ expect(() => JSON.parse(stripJsoncComments(result))).not.toThrow();
97
+ });
98
+
99
+ it("is idempotent: second run is a no-op", () => {
100
+ fs.writeFileSync(
101
+ wranglerPath,
102
+ `{
103
+ "name": "lebiscuit-tanstack",
104
+ "main": "./src/worker-entry.ts",
105
+ "observability": {
106
+ "logs": { "enabled": true }
107
+ }
108
+ }
109
+ `,
110
+ );
111
+
112
+ runCodemod(["--source", tmpDir, "--write"]);
113
+ const after1 = fs.readFileSync(wranglerPath, "utf8");
114
+
115
+ const r = runCodemod(["--source", tmpDir, "--write"]);
116
+ expect(r.code).toBe(0);
117
+ expect(r.stdout).toContain("already on CF-native");
118
+
119
+ const after2 = fs.readFileSync(wranglerPath, "utf8");
120
+ expect(after2).toBe(after1);
121
+ });
122
+
123
+ it("dry-run exits 1 and does not modify the file (CI signal)", () => {
124
+ const before = `{
125
+ "name": "site",
126
+ "main": "./src/worker-entry.ts"
127
+ }
128
+ `;
129
+ fs.writeFileSync(wranglerPath, before);
130
+
131
+ const r = runCodemod(["--source", tmpDir]);
132
+ expect(r.code).toBe(1);
133
+ expect(r.stdout).toContain("Dry-run");
134
+
135
+ expect(fs.readFileSync(wranglerPath, "utf8")).toBe(before);
136
+ });
137
+
138
+ it("respects --logs / --traces / --traces-rate / --persist flags", () => {
139
+ fs.writeFileSync(
140
+ wranglerPath,
141
+ `{
142
+ "name": "site",
143
+ "main": "./src/worker-entry.ts"
144
+ }
145
+ `,
146
+ );
147
+
148
+ runCodemod([
149
+ "--source",
150
+ tmpDir,
151
+ "--logs",
152
+ "my-logs",
153
+ "--traces",
154
+ "my-traces",
155
+ "--traces-rate",
156
+ "0.05",
157
+ "--persist",
158
+ "--write",
159
+ ]);
160
+
161
+ const result = fs.readFileSync(wranglerPath, "utf8");
162
+ expect(result).toContain('"destinations": ["my-logs"]');
163
+ expect(result).toContain('"destinations": ["my-traces"]');
164
+ expect(result).toContain('"head_sampling_rate": 0.05');
165
+ // Both blocks set persist:true (no logs-only persist flag).
166
+ const persistTrueCount = (result.match(/"persist": true/g) ?? []).length;
167
+ expect(persistTrueCount).toBe(2);
168
+ });
169
+ }, 30_000);