@decocms/blocks-cli 7.21.1 → 7.22.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.
@@ -326,8 +326,12 @@ ${hasVtexAuthLoader ? `import vtexAuthLoader from "../loaders/vtex-auth-loader";
326
326
  performVtexLogout,
327
327
  parseVtexAuthJwt,
328
328
  } from "@decocms/apps-vtex/utils/authHelpers";
329
+ import { getUser, type Person } from "@decocms/apps-vtex/loaders/user";
330
+ import { getWishlist, type WishlistItem } from "@decocms/apps-vtex/loaders/wishlist";
331
+ import { addItem as addToWishlistItem, removeItem as removeFromWishlistItem } from "@decocms/apps-vtex/actions/wishlist";
329
332
 
330
333
  export type { OrderForm } from "./invoke.gen";
334
+ export type { Person, WishlistItem };
331
335
 
332
336
  function getVtexCookies(): string {
333
337
  return extractVtexCookiesFromHeader(getRequestHeader("cookie") ?? "");
@@ -361,9 +365,57 @@ const _getUserFromJwt = createServerFn({ method: "POST" }).handler(
361
365
  },
362
366
  );
363
367
 
368
+ // createUseUser/createUseWishlist (@decocms/apps-vtex/hooks) require
369
+ // invoke.vtex.loaders.user/wishlist and invoke.vtex.actions.addToWishlist/
370
+ // removeFromWishlist — without these the hooks throw on mount and take
371
+ // down the whole section (e.g. the Header) via the error boundary.
372
+ function getShopperId(): string | null {
373
+ return parseVtexAuthJwt(getRequestHeader("cookie") ?? "")?.email ?? null;
374
+ }
375
+
376
+ const _vtexUser = createServerFn({ method: "GET" }).handler(
377
+ async (): Promise<Person | null> => {
378
+ return getUser(getVtexCookies());
379
+ },
380
+ );
381
+
382
+ const _vtexWishlist = createServerFn({ method: "GET" }).handler(
383
+ async (): Promise<WishlistItem[]> => {
384
+ const shopperId = getShopperId();
385
+ if (!shopperId) return [];
386
+ return getWishlist(getVtexCookies(), { shopperId, allRecords: true });
387
+ },
388
+ );
389
+
390
+ const _addToWishlist = createServerFn({ method: "POST" })
391
+ .inputValidator(
392
+ (data: { data: { productId: string; sku: string; title?: string } }) => data,
393
+ )
394
+ .handler(async ({ data }): Promise<WishlistItem[]> => {
395
+ const shopperId = getShopperId();
396
+ if (!shopperId) throw new Error("User must be logged in to add to wishlist");
397
+ return addToWishlistItem(data.data, shopperId, getVtexCookies());
398
+ });
399
+
400
+ const _removeFromWishlist = createServerFn({ method: "POST" })
401
+ .inputValidator((data: { data: { id: string } }) => data)
402
+ .handler(async ({ data }): Promise<WishlistItem[]> => {
403
+ const shopperId = getShopperId();
404
+ if (!shopperId) throw new Error("User must be logged in to remove from wishlist");
405
+ return removeFromWishlistItem(data.data.id, shopperId, getVtexCookies());
406
+ });
407
+
364
408
  export const invoke = {
365
409
  vtex: {
366
- actions: vtexActions,
410
+ actions: {
411
+ ...vtexActions,
412
+ addToWishlist: _addToWishlist,
413
+ removeFromWishlist: _removeFromWishlist,
414
+ },
415
+ loaders: {
416
+ user: _vtexUser,
417
+ wishlist: _vtexWishlist,
418
+ },
367
419
  },
368
420
  site: {
369
421
  loaders: {
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Smoke tests for the `deco-upgrade-6-to-7` CLI (#367) — drives the script
3
+ * as a child process against a tmp site fixture, verifying:
4
+ * - dry-run prints what would change without touching disk
5
+ * - --write applies the import-specifier rewrites and package.json swap
6
+ * - second --write run is a no-op (idempotency)
7
+ */
8
+ import * as cp from "node:child_process";
9
+ import * as fs from "node:fs";
10
+ import * as os from "node:os";
11
+ import * as path from "node:path";
12
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
13
+
14
+ const SCRIPT = path.resolve(__dirname, "upgrade-6-to-7.ts");
15
+
16
+ function run(cwd: string, args: string[]): { stdout: string; code: number } {
17
+ const r = cp.spawnSync("npx", ["tsx", SCRIPT, ...args], { cwd, encoding: "utf8" });
18
+ return { stdout: r.stdout || "", code: r.status ?? 0 };
19
+ }
20
+
21
+ describe("deco-upgrade-6-to-7 CLI (#367)", () => {
22
+ let tmp: string;
23
+
24
+ beforeEach(() => {
25
+ tmp = fs.mkdtempSync(path.join(os.tmpdir(), "upgrade-6-to-7-"));
26
+ fs.mkdirSync(path.join(tmp, "src", "routes"), { recursive: true });
27
+ fs.writeFileSync(
28
+ path.join(tmp, "src", "routes", "$.tsx"),
29
+ `import { cmsRouteConfig } from "@decocms/start/routes";\n` +
30
+ `import { DecoPageRenderer } from "@decocms/start/hooks";\n`,
31
+ );
32
+ fs.writeFileSync(
33
+ path.join(tmp, "package.json"),
34
+ JSON.stringify(
35
+ { name: "fixture-site", dependencies: { "@decocms/start": "6.30.0", "@decocms/apps": "5.4.0" } },
36
+ null,
37
+ 2,
38
+ ),
39
+ );
40
+ });
41
+
42
+ afterEach(() => {
43
+ fs.rmSync(tmp, { recursive: true, force: true });
44
+ });
45
+
46
+ it("dry-run reports what would change without writing", () => {
47
+ const { stdout, code } = run(tmp, []);
48
+ expect(code).toBe(0);
49
+ expect(stdout).toContain("would upgrade");
50
+ expect(stdout).toContain("removing @decocms/start, @decocms/apps");
51
+
52
+ // Nothing actually changed on disk
53
+ const src = fs.readFileSync(path.join(tmp, "src", "routes", "$.tsx"), "utf-8");
54
+ expect(src).toContain("@decocms/start/routes");
55
+ const pkg = JSON.parse(fs.readFileSync(path.join(tmp, "package.json"), "utf-8"));
56
+ expect(pkg.dependencies).toHaveProperty("@decocms/start");
57
+ });
58
+
59
+ it("--write rewrites specifiers and package.json dependencies", () => {
60
+ const { code } = run(tmp, ["--write"]);
61
+ expect(code).toBe(0);
62
+
63
+ const src = fs.readFileSync(path.join(tmp, "src", "routes", "$.tsx"), "utf-8");
64
+ expect(src).toContain('import { cmsRouteConfig } from "@decocms/tanstack";');
65
+ expect(src).toContain('import { DecoPageRenderer } from "@decocms/tanstack";');
66
+ expect(src).not.toContain("@decocms/start");
67
+
68
+ const pkg = JSON.parse(fs.readFileSync(path.join(tmp, "package.json"), "utf-8"));
69
+ expect(pkg.dependencies).not.toHaveProperty("@decocms/start");
70
+ expect(pkg.dependencies).not.toHaveProperty("@decocms/apps");
71
+ expect(pkg.dependencies).toHaveProperty("@decocms/tanstack");
72
+ expect(pkg.dependencies).toHaveProperty("@decocms/blocks");
73
+ });
74
+
75
+ it("second --write run is idempotent (no-op)", () => {
76
+ run(tmp, ["--write"]);
77
+ const { stdout } = run(tmp, ["--write"]);
78
+ expect(stdout).toContain("Upgraded 0 file(s).");
79
+ });
80
+ });
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Upgrade a site off the frozen pre-split `@decocms/start@6.30.0` /
4
+ * `@decocms/apps@5.4.0` packages onto the current 7.x split
5
+ * (`@decocms/tanstack` + `@decocms/blocks` + `@decocms/blocks-admin` +
6
+ * `@decocms/apps-*`). See #367.
7
+ *
8
+ * This is NOT the Fresh→TanStack migrator (`deco-migrate`) — it assumes the
9
+ * site is already on TanStack Start and only needs its package dependency
10
+ * moved to the current split. Rewrites import specifiers (including the
11
+ * `@decocms/start/routes` / `@decocms/start/hooks` symbol fan-out and the
12
+ * `decoMetaRoute`/`RenderSection` renames — see scripts/lib/upgrade-6-to-7.ts
13
+ * for the full mapping) and updates package.json dependencies.
14
+ *
15
+ * Usage (from the site root):
16
+ * # dry-run (default): prints every file that would change, no writes
17
+ * npx -p @decocms/blocks-cli deco-upgrade-6-to-7
18
+ * # apply:
19
+ * npx -p @decocms/blocks-cli deco-upgrade-6-to-7 --write
20
+ *
21
+ * Options:
22
+ * --src-dir <dir> Source dir to scan (default: src)
23
+ * --write Apply changes (otherwise dry-run, exit 0)
24
+ * --help, -h Show this help
25
+ *
26
+ * Exit codes: 0 ok / dry-run; 2 error (bad dir)
27
+ */
28
+
29
+ import * as fs from "node:fs";
30
+ import * as path from "node:path";
31
+ import { upgradeFileToV7 } from "./lib/upgrade-6-to-7";
32
+
33
+ const FRAMEWORK_DEPS = [
34
+ "@decocms/blocks",
35
+ "@decocms/blocks-admin",
36
+ "@decocms/tanstack",
37
+ "@decocms/apps-commerce",
38
+ ];
39
+ const PLATFORM_APPS_PREFIX = "@decocms/apps-";
40
+
41
+ function parseArgs(argv: string[]) {
42
+ const has = (f: string) => argv.includes(f);
43
+ const val = (f: string, d: string) => {
44
+ const i = argv.indexOf(f);
45
+ return i >= 0 && argv[i + 1] ? argv[i + 1] : d;
46
+ };
47
+ return {
48
+ help: has("--help") || has("-h"),
49
+ write: has("--write"),
50
+ srcDir: val("--src-dir", "src"),
51
+ };
52
+ }
53
+
54
+ function walkTsFiles(dir: string, out: string[] = []): string[] {
55
+ if (!fs.existsSync(dir)) return out;
56
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
57
+ const full = path.join(dir, entry.name);
58
+ if (entry.isDirectory()) {
59
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
60
+ walkTsFiles(full, out);
61
+ } else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
62
+ out.push(full);
63
+ }
64
+ }
65
+ return out;
66
+ }
67
+
68
+ /** Removes @decocms/start and @decocms/apps (bare, monolith) from a deps
69
+ * object, and ensures the current split packages are present. Site- and
70
+ * platform-specific version pins are left to `bun install`/`npm install`
71
+ * against the fallback "latest" this writes — sites should pin exact
72
+ * versions afterward the same way the scaffolder does. */
73
+ function upgradeDeps(deps: Record<string, string> | undefined): {
74
+ next: Record<string, string>;
75
+ removed: string[];
76
+ added: string[];
77
+ } {
78
+ const next: Record<string, string> = { ...(deps ?? {}) };
79
+ const removed: string[] = [];
80
+ for (const key of Object.keys(next)) {
81
+ if (key === "@decocms/start" || key === "@decocms/apps") {
82
+ delete next[key];
83
+ removed.push(key);
84
+ }
85
+ }
86
+ const added: string[] = [];
87
+ if (removed.length > 0) {
88
+ for (const dep of FRAMEWORK_DEPS) {
89
+ if (!next[dep]) {
90
+ next[dep] = "latest";
91
+ added.push(dep);
92
+ }
93
+ }
94
+ }
95
+ return { next, removed, added };
96
+ }
97
+
98
+ function upgradePackageJson(pkgPath: string, write: boolean): void {
99
+ if (!fs.existsSync(pkgPath)) return;
100
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
101
+
102
+ const depsResult = upgradeDeps(pkg.dependencies);
103
+ const devDepsResult = upgradeDeps(pkg.devDependencies);
104
+
105
+ const removed = [...depsResult.removed, ...devDepsResult.removed];
106
+ if (removed.length === 0) return;
107
+
108
+ console.log(` package.json: removing ${removed.join(", ")}`);
109
+ const added = [...new Set([...depsResult.added, ...devDepsResult.added])];
110
+ if (added.length > 0) {
111
+ console.log(` package.json: adding ${added.join(", ")} at "latest" — pin exact versions after install`);
112
+ }
113
+
114
+ if (!write) return;
115
+ pkg.dependencies = depsResult.next;
116
+ pkg.devDependencies = devDepsResult.next;
117
+ fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
118
+ }
119
+
120
+ function main() {
121
+ const args = parseArgs(process.argv.slice(2));
122
+ if (args.help) {
123
+ console.log(
124
+ "Usage: deco-upgrade-6-to-7 [--src-dir <dir>] [--write]\n\n" +
125
+ "Upgrades a site off @decocms/start@6.30.0 / @decocms/apps@5.4.0 onto the\n" +
126
+ "current 7.x split. Dry-run by default; pass --write to apply.",
127
+ );
128
+ return;
129
+ }
130
+
131
+ const cwd = process.cwd();
132
+ const srcDir = path.resolve(cwd, args.srcDir);
133
+ if (!fs.existsSync(srcDir)) {
134
+ console.error(`Source dir not found: ${srcDir}`);
135
+ process.exitCode = 2;
136
+ return;
137
+ }
138
+
139
+ const files = walkTsFiles(srcDir);
140
+ let changedCount = 0;
141
+ const allNotes: Array<{ file: string; note: string }> = [];
142
+
143
+ for (const abs of files) {
144
+ const content = fs.readFileSync(abs, "utf-8");
145
+ const result = upgradeFileToV7(content);
146
+ if (!result.changed) continue;
147
+
148
+ changedCount++;
149
+ const rel = path.relative(cwd, abs);
150
+ console.log(`${args.write ? "upgraded" : "would upgrade"}: ${rel}`);
151
+ for (const note of result.notes) allNotes.push({ file: rel, note });
152
+
153
+ if (args.write) fs.writeFileSync(abs, result.content);
154
+ }
155
+
156
+ upgradePackageJson(path.join(cwd, "package.json"), args.write);
157
+
158
+ if (allNotes.length > 0) {
159
+ console.log("\nManual review needed:");
160
+ for (const { file, note } of allNotes) console.log(` [${file}] ${note}`);
161
+ }
162
+
163
+ console.log(
164
+ `\n${args.write ? "Upgraded" : "Would upgrade"} ${changedCount} file(s).` +
165
+ (args.write ? "" : " Re-run with --write to apply."),
166
+ );
167
+ }
168
+
169
+ main();