@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.
- package/.github/workflows/lockfile-check.yml +35 -0
- package/bun.lock +36 -110
- package/package.json +4 -3
- package/scripts/migrate/phase-report.ts +15 -7
- package/scripts/migrate/phase-scaffold.ts +17 -1
- package/scripts/migrate/phase-verify.ts +1 -0
- package/scripts/migrate/post-cleanup/rules.ts +349 -0
- package/scripts/migrate/post-cleanup/runner.test.ts +216 -0
- package/scripts/migrate/templates/lockfile-check-yml.test.ts +26 -0
- package/scripts/migrate/templates/lockfile-check-yml.ts +66 -0
- package/scripts/migrate/templates/package-json.ts +13 -1
- package/scripts/migrate/templates/server-entry.ts +39 -3
- package/scripts/migrate-post-cleanup.ts +5 -3
- package/scripts/migrate-to-cf-observability.test.ts +169 -0
- package/scripts/migrate-to-cf-observability.ts +611 -0
- package/scripts/migrate.ts +9 -4
- package/src/sdk/logger.test.ts +79 -0
- package/src/sdk/logger.ts +40 -2
- package/src/sdk/otel.test.ts +128 -15
- package/src/sdk/otel.ts +179 -98
- package/src/sdk/sampler.ts +17 -5
- package/src/sdk/workerEntry.ts +7 -4
|
@@ -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 &&
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
81
|
+
local-framework-duplicate auto-fixable subset,
|
|
82
|
+
lockfile-multiple, package-manager-missing).
|
|
83
|
+
Other rules — including 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);
|