@decocms/blocks-cli 7.55.1 → 7.56.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/blocks-cli",
3
- "version": "7.55.1",
3
+ "version": "7.56.0",
4
4
  "type": "module",
5
5
  "description": "Deco codegen (generate-blocks, generate-schema, generate-invoke) and Fresh-to-TanStack migration tooling",
6
6
  "repository": {
@@ -33,7 +33,7 @@
33
33
  "lint:unused": "knip"
34
34
  },
35
35
  "dependencies": {
36
- "@decocms/blocks": "7.55.1",
36
+ "@decocms/blocks": "7.56.0",
37
37
  "ts-morph": "^27.0.0",
38
38
  "tsx": "^4.22.5"
39
39
  },
@@ -57,6 +57,43 @@ describe("transformCtxCompat", () => {
57
57
  expect(r.content).toContain("ctx?: AppContext");
58
58
  });
59
59
 
60
+ // The scanner walks raw characters, so without a literal/comment guard it
61
+ // rewrote `ctx.` inside strings too — patching the call site correctly while
62
+ // silently corrupting the message next to it.
63
+ it("does not rewrite ctx inside string literals", () => {
64
+ const r = transformCtxCompat(withLoader(" console.log('ctx.device:', ctx.device);"));
65
+ expect(r.content).toContain("'ctx.device:'");
66
+ expect(r.content).toContain("console.log('ctx.device:', ctx?.device)");
67
+ });
68
+
69
+ it("does not rewrite ctx inside double-quoted strings or escaped quotes", () => {
70
+ const r = transformCtxCompat(
71
+ withLoader(' const s = "he said \\"ctx.device\\""; return ctx.device;'),
72
+ );
73
+ expect(r.content).toContain('"he said \\"ctx.device\\""');
74
+ expect(r.content).toContain("return ctx?.device");
75
+ });
76
+
77
+ it("does not rewrite ctx inside comments", () => {
78
+ const r = transformCtxCompat(
79
+ withLoader(" // don't touch ctx.device here\n /* nor ctx.invoke */\n return ctx.device;"),
80
+ );
81
+ expect(r.content).toContain("// don't touch ctx.device here");
82
+ expect(r.content).toContain("/* nor ctx.invoke */");
83
+ expect(r.content).toContain("return ctx?.device");
84
+ });
85
+
86
+ it("does not rewrite template text but DOES rewrite ${} interpolation", () => {
87
+ const r = transformCtxCompat(withLoader(" const u = `ctx.device is ${ctx.vtex.account}`;"));
88
+ // literal text stays, the interpolated expression is real code
89
+ expect(r.content).toContain("`ctx.device is ${ctx?.vtex?.account}`");
90
+ });
91
+
92
+ it("handles nested template literals", () => {
93
+ const r = transformCtxCompat(withLoader(" const u = `a${`b${ctx.device}`}c`;"));
94
+ expect(r.content).toContain("`a${`b${ctx?.device}`}c`");
95
+ });
96
+
60
97
  it("does not match identifiers that merely contain ctx", () => {
61
98
  const src = withLoader(" const c = canvasCtx.foo;\n const d = a.ctx.bar;");
62
99
  const r = transformCtxCompat(src);
@@ -112,7 +112,99 @@ export function transformCtxCompat(content: string): TransformResult {
112
112
  let i = 0;
113
113
  let count = 0;
114
114
 
115
+ // Contexts we are inside of, innermost last. Without this the scanner walks
116
+ // raw characters and happily rewrites `ctx.` inside string literals and
117
+ // comments: `console.log('ctx.device:', ctx.device)` became
118
+ // `console.log('ctx?.device:', ctx?.device)` — the call site correctly
119
+ // patched, the message silently corrupted.
120
+ //
121
+ // A template literal pushes "template" (its text is data), and a `${` inside
122
+ // one pushes "expr" (real code, where rewriting must resume) — so
123
+ // `` `${ctx.device}` `` is still rewritten while `` `ctx.device` `` is not.
124
+ //
125
+ // Regex literals are deliberately NOT tracked: telling `/re/` from division
126
+ // needs real parsing, and guessing wrong would consume live code. A `ctx.`
127
+ // inside a regex in a loader is vanishingly rare next to that risk.
128
+ const stack: ("template" | "expr")[] = [];
129
+
115
130
  while (i < content.length) {
131
+ const inTemplateText = stack[stack.length - 1] === "template";
132
+
133
+ if (inTemplateText) {
134
+ if (content[i] === "\\") {
135
+ out += content.slice(i, i + 2);
136
+ i += 2;
137
+ continue;
138
+ }
139
+ if (content[i] === "`") {
140
+ stack.pop();
141
+ out += content[i];
142
+ i += 1;
143
+ continue;
144
+ }
145
+ if (content.startsWith("${", i)) {
146
+ stack.push("expr");
147
+ out += "${";
148
+ i += 2;
149
+ continue;
150
+ }
151
+ out += content[i];
152
+ i += 1;
153
+ continue;
154
+ }
155
+
156
+ // Line comment — copy to end of line.
157
+ if (content.startsWith("//", i)) {
158
+ const nl = content.indexOf("\n", i);
159
+ const end = nl === -1 ? content.length : nl;
160
+ out += content.slice(i, end);
161
+ i = end;
162
+ continue;
163
+ }
164
+
165
+ // Block comment — copy to the closing delimiter.
166
+ if (content.startsWith("/*", i)) {
167
+ const close = content.indexOf("*/", i + 2);
168
+ const end = close === -1 ? content.length : close + 2;
169
+ out += content.slice(i, end);
170
+ i = end;
171
+ continue;
172
+ }
173
+
174
+ // Single/double quoted string — copy to the unescaped closing quote.
175
+ if (content[i] === "'" || content[i] === '"') {
176
+ const quote = content[i];
177
+ let j = i + 1;
178
+ while (j < content.length) {
179
+ if (content[j] === "\\") {
180
+ j += 2;
181
+ continue;
182
+ }
183
+ if (content[j] === quote || content[j] === "\n") {
184
+ j += 1;
185
+ break;
186
+ }
187
+ j += 1;
188
+ }
189
+ out += content.slice(i, j);
190
+ i = j;
191
+ continue;
192
+ }
193
+
194
+ if (content[i] === "`") {
195
+ stack.push("template");
196
+ out += content[i];
197
+ i += 1;
198
+ continue;
199
+ }
200
+
201
+ if (content[i] === "}" && stack[stack.length - 1] === "expr") {
202
+ stack.pop();
203
+ out += content[i];
204
+ i += 1;
205
+ continue;
206
+ }
207
+
116
208
  const isCtxToken =
117
209
  content.startsWith("ctx", i) &&
118
210
  !isIdentChar(content[i - 1]) &&