@decocms/start 6.4.4 → 6.4.5

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/start",
3
- "version": "6.4.4",
3
+ "version": "6.4.5",
4
4
  "type": "module",
5
5
  "description": "Deco framework for TanStack Start - CMS bridge, admin protocol, hooks, schema generation",
6
6
  "main": "./src/index.ts",
@@ -0,0 +1,63 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import { extractPlatform } from "./phase-analyze";
6
+
7
+ // `extractPlatform` runs four strategies in order:
8
+ // 1. deno.json imports referencing apps/{platform}/
9
+ // 2. apps/{platform}.ts file existence
10
+ // 3. apps/site.ts loose string match against platform name + "platform"
11
+ // 4. .deco/blocks filenames hinting at platform
12
+ // Strategy 3 is the false-positive trap that ate Magento sites before #211:
13
+ // helsinki/granadobr's apps/site.ts imports `apps/vtex/mod.ts` for the color
14
+ // palette, so the content matches `"vtex"` even though the real platform is
15
+ // declared via `apps/magento.ts`. The fix adds "magento" to the platforms
16
+ // list so Strategy 2 fires first and short-circuits before Strategy 3 can
17
+ // over-match.
18
+
19
+ describe("extractPlatform", () => {
20
+ let tmp: string;
21
+
22
+ beforeEach(() => {
23
+ tmp = fs.mkdtempSync(path.join(os.tmpdir(), "extract-platform-"));
24
+ fs.mkdirSync(path.join(tmp, "apps"), { recursive: true });
25
+ });
26
+
27
+ afterEach(() => {
28
+ fs.rmSync(tmp, { recursive: true, force: true });
29
+ });
30
+
31
+ it("detects magento from apps/magento.ts (granadobr/helsinki shape)", () => {
32
+ fs.writeFileSync(path.join(tmp, "apps", "magento.ts"), "export default {};\n");
33
+ // Also write a site.ts that imports vtex for color palettes — this is
34
+ // what tripped Strategy 3 before #211.
35
+ fs.writeFileSync(
36
+ path.join(tmp, "apps", "site.ts"),
37
+ `import { color as vtex } from "apps/vtex/mod.ts";\n` +
38
+ `export interface State { platform: string }\n`,
39
+ );
40
+ expect(extractPlatform(tmp)).toBe("magento");
41
+ });
42
+
43
+ it("detects vtex from apps/vtex.ts", () => {
44
+ fs.writeFileSync(path.join(tmp, "apps", "vtex.ts"), "export default { account: \"foo\" };\n");
45
+ expect(extractPlatform(tmp)).toBe("vtex");
46
+ });
47
+
48
+ it("falls back to custom when no platform signal exists", () => {
49
+ fs.writeFileSync(path.join(tmp, "apps", "site.ts"), "export default {};\n");
50
+ expect(extractPlatform(tmp)).toBe("custom");
51
+ });
52
+
53
+ it("prefers magento over vtex when both an apps/magento.ts AND a vtex string match exist", () => {
54
+ // Reproduce the exact granadobr trap: apps/magento.ts is the real signal,
55
+ // but apps/site.ts loosely contains "vtex" + "platform".
56
+ fs.writeFileSync(path.join(tmp, "apps", "magento.ts"), "export default {};\n");
57
+ fs.writeFileSync(
58
+ path.join(tmp, "apps", "site.ts"),
59
+ `import "apps/vtex/mod.ts";\nexport interface State { platform: string }\n`,
60
+ );
61
+ expect(extractPlatform(tmp)).toBe("magento");
62
+ });
63
+ });
@@ -467,8 +467,21 @@ function extractVtexAccount(sourceDir: string): string | null {
467
467
  return null;
468
468
  }
469
469
 
470
- function extractPlatform(sourceDir: string): Platform {
471
- const platforms: Platform[] = ["vtex", "shopify", "wake", "vnda", "linx", "nuvemshop"];
470
+ export function extractPlatform(sourceDir: string): Platform {
471
+ // Strategy 2 (apps/{p}.ts existence) is the most reliable signal, but it
472
+ // only matches when the platform name appears here — so a site whose only
473
+ // app file is `apps/magento.ts` previously fell through to Strategy 3
474
+ // and got mis-detected as "vtex" because site.ts imports
475
+ // `apps/vtex/mod.ts` for color palettes. See #211.
476
+ const platforms: Platform[] = [
477
+ "vtex",
478
+ "shopify",
479
+ "wake",
480
+ "vnda",
481
+ "linx",
482
+ "nuvemshop",
483
+ "magento",
484
+ ];
472
485
 
473
486
  // Strategy 1: Check deno.json imports for platform-specific app imports
474
487
  const denoPath = path.join(sourceDir, "deno.json");
@@ -0,0 +1,127 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { checks } from "./phase-verify";
6
+ import type { MigrationContext } from "./types";
7
+
8
+ /**
9
+ * The "No relative imports to deleted SDK files" check used to flag any
10
+ * relative import to `sdk/{clx,useId,useOffer,useVariantPossiblities,
11
+ * usePlatform}`. But `useOffer` and `useVariantPossiblities` are KEPT as
12
+ * site files (see RELATIVE_SDK_REWRITES in transforms/imports.ts) — so
13
+ * every site that legitimately imports them via a relative path was
14
+ * failing verify with a misleading error. See #212.
15
+ *
16
+ * Pull the specific check out of the registered array and exercise it on
17
+ * a fixture tree, so we don't have to satisfy the other 24 checks that
18
+ * `verify()` runs at once.
19
+ */
20
+
21
+ function makeCtx(sourceDir: string): MigrationContext {
22
+ return {
23
+ sourceDir,
24
+ siteName: "test-site",
25
+ platform: "custom",
26
+ vtexAccount: null,
27
+ gtmId: null,
28
+ importMap: {},
29
+ discoveredNpmDeps: {},
30
+ themeColors: {},
31
+ fontFamily: null,
32
+ files: [],
33
+ sectionMetas: [],
34
+ islandClassifications: [],
35
+ islandWrapperTargets: new Map(),
36
+ loaderInventory: [],
37
+ scaffoldedFiles: [],
38
+ transformedFiles: [],
39
+ deletedFiles: [],
40
+ movedFiles: [],
41
+ manualReviewItems: [],
42
+ frameworkFindings: [],
43
+ dryRun: false,
44
+ verbose: false,
45
+ };
46
+ }
47
+
48
+ const sdkCheck = checks.find(
49
+ (c) => c.name === "No relative imports to deleted SDK files",
50
+ );
51
+ if (!sdkCheck) {
52
+ throw new Error("verify check not found — name changed?");
53
+ }
54
+
55
+ function runCheck(ctx: MigrationContext): { ok: boolean; output: string } {
56
+ const lines: string[] = [];
57
+ const spy = vi.spyOn(console, "log").mockImplementation((...args: unknown[]) => {
58
+ lines.push(args.map((a) => String(a)).join(" "));
59
+ });
60
+ try {
61
+ const ok = sdkCheck!.fn(ctx);
62
+ return { ok, output: lines.join("\n") };
63
+ } finally {
64
+ spy.mockRestore();
65
+ }
66
+ }
67
+
68
+ describe("verify check: 'No relative imports to deleted SDK files'", () => {
69
+ let tmp: string;
70
+
71
+ beforeEach(() => {
72
+ tmp = fs.mkdtempSync(path.join(os.tmpdir(), "verify-sdk-"));
73
+ fs.mkdirSync(path.join(tmp, "src", "components"), { recursive: true });
74
+ });
75
+
76
+ afterEach(() => {
77
+ fs.rmSync(tmp, { recursive: true, force: true });
78
+ });
79
+
80
+ it("passes when src/ imports useOffer/useVariantPossiblities relatively (kept files, #212)", () => {
81
+ fs.writeFileSync(
82
+ path.join(tmp, "src", "components", "ProductCard.tsx"),
83
+ `import { useOffer } from "../../sdk/useOffer";\n` +
84
+ `import { useVariantPossibilities } from "../../sdk/useVariantPossiblities";\n` +
85
+ `export const x = 1;\n`,
86
+ );
87
+ const { ok, output } = runCheck(makeCtx(tmp));
88
+ expect(ok).toBe(true);
89
+ expect(output).toBe("");
90
+ });
91
+
92
+ it("fails when src/ imports clx/useId/usePlatform relatively (truly deleted)", () => {
93
+ fs.writeFileSync(
94
+ path.join(tmp, "src", "components", "Bad.tsx"),
95
+ `import { clx } from "../../sdk/clx";\n` +
96
+ `import { useId } from "../../sdk/useId";\n` +
97
+ `import { usePlatform } from "../../sdk/usePlatform";\n` +
98
+ `export const x = 1;\n`,
99
+ );
100
+ const { ok, output } = runCheck(makeCtx(tmp));
101
+ expect(ok).toBe(false);
102
+ // The improved error reports each offending line, not just the file.
103
+ expect(output).toMatch(/components\/Bad\.tsx:.*sdk\/clx/);
104
+ expect(output).toMatch(/components\/Bad\.tsx:.*sdk\/useId/);
105
+ expect(output).toMatch(/components\/Bad\.tsx:.*sdk\/usePlatform/);
106
+ });
107
+
108
+ it("ignores commented-out references to deleted SDK files", () => {
109
+ fs.writeFileSync(
110
+ path.join(tmp, "src", "components", "Docs.tsx"),
111
+ `// import { clx } from "../../sdk/clx"\n` +
112
+ `// from "../../sdk/useId"\n` +
113
+ `export const x = 1;\n`,
114
+ );
115
+ const { ok } = runCheck(makeCtx(tmp));
116
+ expect(ok).toBe(true);
117
+ });
118
+
119
+ it("matches both .ts-suffixed and unsuffixed import paths", () => {
120
+ fs.writeFileSync(
121
+ path.join(tmp, "src", "components", "WithExt.tsx"),
122
+ `import { clx } from "../../sdk/clx.ts";\nexport const x = 1;\n`,
123
+ );
124
+ const { ok } = runCheck(makeCtx(tmp));
125
+ expect(ok).toBe(false);
126
+ });
127
+ });
@@ -59,7 +59,7 @@ const MUST_NOT_EXIST = [
59
59
  "routes/_middleware.ts",
60
60
  ];
61
61
 
62
- const checks: Check[] = [
62
+ export const checks: Check[] = [
63
63
  {
64
64
  name: "All scaffolded files exist",
65
65
  severity: "error",
@@ -233,15 +233,24 @@ const checks: Check[] = [
233
233
  },
234
234
  },
235
235
  {
236
+ // Only flag imports to files that phase-cleanup actually deletes:
237
+ // `sdk/clx.ts`, `sdk/useId.ts`, `sdk/usePlatform.tsx`. Earlier versions
238
+ // also matched `useOffer` and `useVariantPossiblities`, which are
239
+ // explicitly KEPT as site files (RELATIVE_SDK_REWRITES in
240
+ // transforms/imports.ts skips them on purpose because sites customize
241
+ // them). That false positive made every Magento/custom-SDK migration
242
+ // fail verify even when the imports were valid. See #212.
236
243
  name: "No relative imports to deleted SDK files",
237
244
  severity: "error",
238
245
  fn: (ctx) => {
239
246
  const srcDir = path.join(ctx.sourceDir, "src");
240
247
  if (!fs.existsSync(srcDir)) return true;
241
- // Only match relative imports (../ or ./) to deleted SDK files, not @decocms/* package imports
242
- const bad = findFilesWithPattern(srcDir, /from\s+["'](?:\.\.?\/)[^"']*\/sdk\/(?:clx|useId|useOffer|useVariantPossiblities|usePlatform)["']/);
248
+ const pattern = /from\s+["'](?:\.\.?\/)[^"']*\/sdk\/(?:clx|useId|usePlatform)(?:\.tsx?)?["']/;
249
+ const bad = findMatchesWithPattern(srcDir, pattern);
243
250
  if (bad.length > 0) {
244
- console.log(` Still has relative imports to deleted SDK files: ${bad.join(", ")}`);
251
+ for (const { file, line } of bad) {
252
+ console.log(` ${file}: ${line.trim()}`);
253
+ }
245
254
  return false;
246
255
  }
247
256
  return true;
@@ -493,6 +502,37 @@ function findFilesWithPattern(
493
502
  return results;
494
503
  }
495
504
 
505
+ /**
506
+ * Like {@link findFilesWithPattern} but returns each offending line alongside
507
+ * its file, so the verify output can show *what* matched — not just *where*.
508
+ * Skips comment-only lines so a doc reference doesn't trigger a false fail.
509
+ */
510
+ function findMatchesWithPattern(
511
+ dir: string,
512
+ pattern: RegExp,
513
+ results: Array<{ file: string; line: string }> = [],
514
+ baseDir?: string,
515
+ ): Array<{ file: string; line: string }> {
516
+ const root = baseDir ?? dir;
517
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
518
+ for (const entry of entries) {
519
+ const fullPath = path.join(dir, entry.name);
520
+ if (entry.isDirectory()) {
521
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "server") continue;
522
+ findMatchesWithPattern(fullPath, pattern, results, root);
523
+ } else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
524
+ const content = fs.readFileSync(fullPath, "utf-8");
525
+ const rel = path.relative(root, fullPath);
526
+ for (const line of content.split("\n")) {
527
+ const trimmed = line.trimStart();
528
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
529
+ if (pattern.test(line)) results.push({ file: rel, line });
530
+ }
531
+ }
532
+ }
533
+ return results;
534
+ }
535
+
496
536
  export function verify(ctx: MigrationContext): boolean {
497
537
  logPhase("Verify (Smoke Test)");
498
538
 
@@ -5,6 +5,7 @@ export type Platform =
5
5
  | "wake"
6
6
  | "linx"
7
7
  | "nuvemshop"
8
+ | "magento"
8
9
  | "custom";
9
10
 
10
11
  export interface FileRecord {