@planningcenter/tapestry-migration-cli 3.4.1 → 3.6.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.
Files changed (43) hide show
  1. package/README.md +59 -0
  2. package/dist/tapestry-react-shim.cjs +5 -1
  3. package/package.json +3 -3
  4. package/src/availableComponents.ts +14 -0
  5. package/src/components/button/transforms/tooltipToWrapper.test.ts +50 -3
  6. package/src/components/button/transforms/tooltipToWrapper.ts +18 -0
  7. package/src/components/dropdown/index.test.ts +67 -0
  8. package/src/components/dropdown/index.ts +4 -0
  9. package/src/components/dropdown/transforms/placementIdToMenu.test.ts +6 -1
  10. package/src/components/dropdown/transforms/placementIdToMenu.ts +1 -1
  11. package/src/components/dropdown/transforms/variantToKind.test.ts +292 -0
  12. package/src/components/dropdown/transforms/variantToKind.ts +138 -0
  13. package/src/components/dropdown/transforms/wrapIconTrigger.test.ts +336 -0
  14. package/src/components/dropdown/transforms/wrapIconTrigger.ts +233 -0
  15. package/src/components/flex/index.test.ts +140 -0
  16. package/src/components/flex/index.ts +40 -0
  17. package/src/components/flex/transforms/alignmentToAlign.test.ts +185 -0
  18. package/src/components/flex/transforms/alignmentToAlign.ts +73 -0
  19. package/src/components/flex/transforms/axisToDirection.test.ts +140 -0
  20. package/src/components/flex/transforms/axisToDirection.ts +51 -0
  21. package/src/components/flex/transforms/distributionToJustify.test.ts +214 -0
  22. package/src/components/flex/transforms/distributionToJustify.ts +76 -0
  23. package/src/components/flex/transforms/innerRefToRef.test.ts +100 -0
  24. package/src/components/flex/transforms/innerRefToRef.ts +14 -0
  25. package/src/components/flex/transforms/moveFlexImport.test.ts +202 -0
  26. package/src/components/flex/transforms/moveFlexImport.ts +14 -0
  27. package/src/components/flex/transforms/setDefaultAxis.test.ts +114 -0
  28. package/src/components/flex/transforms/setDefaultAxis.ts +29 -0
  29. package/src/components/flex/transforms/spacingToGap.test.ts +176 -0
  30. package/src/components/flex/transforms/spacingToGap.ts +49 -0
  31. package/src/components/select/index.ts +2 -0
  32. package/src/components/select/transforms/onChangeSignature.test.ts +224 -0
  33. package/src/components/select/transforms/onChangeSignature.ts +172 -0
  34. package/src/components/shared/actions/resolveConstantAttributeValue.test.ts +122 -0
  35. package/src/components/shared/actions/resolveConstantAttributeValue.ts +31 -0
  36. package/src/components/toggle-switch/index.ts +2 -0
  37. package/src/components/toggle-switch/transforms/onClickToOnChange.test.ts +210 -0
  38. package/src/components/toggle-switch/transforms/onClickToOnChange.ts +71 -0
  39. package/src/index.test.ts +79 -0
  40. package/src/index.ts +34 -18
  41. package/src/migrationList.ts +22 -0
  42. package/src/utils/componentLabel.test.ts +61 -0
  43. package/src/utils/componentLabel.ts +15 -0
@@ -0,0 +1,79 @@
1
+ /**
2
+ * E2E regression test for `list --json`.
3
+ *
4
+ * Spawns the CLI exactly as consumers do — `npx tsx src/index.ts list --json` —
5
+ * and asserts the full stdout/stderr/exit-code contract.
6
+ */
7
+
8
+ import { spawnSync } from "node:child_process"
9
+ import { fileURLToPath } from "node:url"
10
+
11
+ import { describe, expect, it } from "vitest"
12
+
13
+ import { buildMigrationList } from "./migrationList"
14
+
15
+ const CLI_PATH = fileURLToPath(new URL("./index.ts", import.meta.url))
16
+
17
+ function runCli(args: string[]) {
18
+ return spawnSync("npx", ["tsx", CLI_PATH, ...args], {
19
+ encoding: "utf8",
20
+ // tsx cold-start can take a few seconds; allow up to 30 s.
21
+ timeout: 30_000,
22
+ })
23
+ }
24
+
25
+ describe("list --json (stdout/exit-code contract)", () => {
26
+ it("exits with code 0", () => {
27
+ const result = runCli(["list", "--json"])
28
+ expect(result.status).toBe(0)
29
+ })
30
+
31
+ it("emits valid JSON as the entire stdout (no stray lines)", () => {
32
+ const result = runCli(["list", "--json"])
33
+ // JSON.parse throws if there is ANY non-JSON content on stdout.
34
+ expect(() => JSON.parse(result.stdout)).not.toThrow()
35
+ })
36
+
37
+ it("payload matches buildMigrationList() (schemaVersion + migrations)", () => {
38
+ const result = runCli(["list", "--json"])
39
+ const payload = JSON.parse(result.stdout)
40
+ expect(payload).toEqual(buildMigrationList())
41
+ })
42
+
43
+ it("schemaVersion is exactly the integer 1", () => {
44
+ const result = runCli(["list", "--json"])
45
+ const { schemaVersion } = JSON.parse(result.stdout)
46
+ expect(schemaVersion).toBe(1)
47
+ expect(Number.isInteger(schemaVersion)).toBe(true)
48
+ })
49
+
50
+ it("stderr is empty on success", () => {
51
+ const result = runCli(["list", "--json"])
52
+ expect(result.stderr).toBe("")
53
+ })
54
+
55
+ it("does not require --path and makes no filesystem writes", () => {
56
+ // If --path were required, commander would write an error to stderr and exit 1.
57
+ // We assert the happy path — exit 0 with no --path — proving the flag is absent.
58
+ const result = runCli(["list", "--json"])
59
+ expect(result.status).toBe(0)
60
+ })
61
+ })
62
+
63
+ describe("list (human-readable, non-contractual)", () => {
64
+ it("exits with code 0 and writes component ids to stdout", () => {
65
+ const result = runCli(["list"])
66
+ expect(result.status).toBe(0)
67
+ expect(result.stdout).toContain("button")
68
+ expect(result.stdout).toContain("toggle-switch")
69
+ })
70
+ })
71
+
72
+ describe("diagnostics contract (nonzero exit → stderr)", () => {
73
+ it("exits nonzero and writes to stderr for an unknown subcommand", () => {
74
+ const result = runCli(["unknown-subcommand"])
75
+ // Commander writes "error: unknown command 'unknown-subcommand'" to stderr.
76
+ expect(result.status).not.toBe(0)
77
+ expect(result.stderr.length).toBeGreaterThan(0)
78
+ })
79
+ })
package/src/index.ts CHANGED
@@ -2,7 +2,10 @@
2
2
 
3
3
  import { Command } from "commander"
4
4
 
5
+ import { AVAILABLE_CLI_COMPONENTS } from "./availableComponents"
5
6
  import { runTransforms } from "./jscodeshiftRunner"
7
+ import { buildMigrationList } from "./migrationList"
8
+ import { formatComponentLabel } from "./utils/componentLabel"
6
9
  import { normalizeComponentName } from "./utils/componentNameNormalizer"
7
10
 
8
11
  const program = new Command()
@@ -11,24 +14,10 @@ program
11
14
  .name("tapestry-migration-cli")
12
15
  .description("CLI tool for Tapestry migrations")
13
16
 
14
- const COMPONENTS_SET = new Set([
15
- "button",
16
- "checkbox",
17
- "date-picker",
18
- "dropdown",
19
- "input",
20
- "link",
21
- "radio",
22
- "select",
23
- "text-area",
24
- "time-field",
25
- "toggle-switch",
26
- ])
27
-
28
17
  const COMPONENTS_SENTENCE = new Intl.ListFormat("en", {
29
18
  style: "long",
30
19
  type: "conjunction",
31
- }).format([...COMPONENTS_SET])
20
+ }).format([...AVAILABLE_CLI_COMPONENTS])
32
21
 
33
22
  program
34
23
  .command("run")
@@ -53,9 +42,9 @@ program
53
42
  "MIGRATION_REPORT.md"
54
43
  )
55
44
  .action((componentName, options) => {
56
- const key = normalizeComponentName(componentName, COMPONENTS_SET)
45
+ const key = normalizeComponentName(componentName, AVAILABLE_CLI_COMPONENTS)
57
46
 
58
- if (key && COMPONENTS_SET.has(key)) {
47
+ if (key && AVAILABLE_CLI_COMPONENTS.has(key)) {
59
48
  console.log(`🎨 Migrating ${componentName} components...`)
60
49
  console.log(`📁 Target: ${options.path}`)
61
50
  console.log(
@@ -65,7 +54,7 @@ program
65
54
  } else {
66
55
  console.error(`❌ Invalid component: "${componentName}"`)
67
56
  console.error(
68
- `✅ Available components: ${[...COMPONENTS_SET].join(", ")}`
57
+ `✅ Available components: ${[...AVAILABLE_CLI_COMPONENTS].join(", ")}`
69
58
  )
70
59
  process.exit(1)
71
60
  }
@@ -105,6 +94,33 @@ program
105
94
  runTransforms("preflight", options)
106
95
  })
107
96
 
97
+ program
98
+ .command("list")
99
+ .description(
100
+ "List available migrations. Pass --json for a stable, versioned machine-readable contract."
101
+ )
102
+ .option(
103
+ "--json",
104
+ "Output migrations as JSON to stdout (diagnostics go to stderr; exits nonzero on failure)"
105
+ )
106
+ .action((options) => {
107
+ try {
108
+ if (options.json) {
109
+ // JSON ONLY on stdout — no emoji, no progress lines, no trailing text.
110
+ process.stdout.write(
111
+ JSON.stringify(buildMigrationList(), null, 2) + "\n"
112
+ )
113
+ } else {
114
+ for (const id of AVAILABLE_CLI_COMPONENTS) {
115
+ console.log(`${id} — ${formatComponentLabel(id)}`)
116
+ }
117
+ }
118
+ } catch (error) {
119
+ console.error(`❌ Failed to list migrations: ${(error as Error).message}`)
120
+ process.exit(1)
121
+ }
122
+ })
123
+
108
124
  program
109
125
  .command("help")
110
126
  .description("Show help information")
@@ -0,0 +1,22 @@
1
+ import { AVAILABLE_CLI_COMPONENTS } from "./availableComponents"
2
+ import { formatComponentLabel } from "./utils/componentLabel"
3
+
4
+ export const LIST_SCHEMA_VERSION = 1
5
+
6
+ /**
7
+ * Builds the versioned migration list payload used by `list --json`.
8
+ * Pure — no I/O, no side effects.
9
+ *
10
+ * @example
11
+ * buildMigrationList()
12
+ * // { schemaVersion: 1, migrations: [{ id: "button", label: "Button" }, ...] }
13
+ */
14
+ export function buildMigrationList() {
15
+ return {
16
+ migrations: [...AVAILABLE_CLI_COMPONENTS].map((id) => ({
17
+ id,
18
+ label: formatComponentLabel(id),
19
+ })),
20
+ schemaVersion: LIST_SCHEMA_VERSION,
21
+ }
22
+ }
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from "vitest"
2
+
3
+ import { AVAILABLE_CLI_COMPONENTS } from "../availableComponents"
4
+ import { buildMigrationList } from "../migrationList"
5
+ import { formatComponentLabel } from "./componentLabel"
6
+ import { normalizeComponentName } from "./componentNameNormalizer"
7
+
8
+ describe("formatComponentLabel", () => {
9
+ it("should title-case a single-word id", () => {
10
+ expect(formatComponentLabel("button")).toBe("Button")
11
+ expect(formatComponentLabel("input")).toBe("Input")
12
+ expect(formatComponentLabel("radio")).toBe("Radio")
13
+ })
14
+
15
+ it("should produce Spaced Title Case for multi-word ids", () => {
16
+ expect(formatComponentLabel("date-picker")).toBe("Date Picker")
17
+ expect(formatComponentLabel("text-area")).toBe("Text Area")
18
+ expect(formatComponentLabel("time-field")).toBe("Time Field")
19
+ expect(formatComponentLabel("toggle-switch")).toBe("Toggle Switch")
20
+ })
21
+ })
22
+
23
+ describe("buildMigrationList", () => {
24
+ it("should return schemaVersion 1 as an integer", () => {
25
+ const result = buildMigrationList()
26
+ expect(result.schemaVersion).toBe(1)
27
+ expect(Number.isInteger(result.schemaVersion)).toBe(true)
28
+ })
29
+
30
+ it("should return a nonempty migrations array", () => {
31
+ const { migrations } = buildMigrationList()
32
+ expect(migrations.length).toBeGreaterThan(0)
33
+ })
34
+
35
+ it("should give every migration a nonempty string id and label", () => {
36
+ for (const { id, label } of buildMigrationList().migrations) {
37
+ expect(typeof id).toBe("string")
38
+ expect(id.length).toBeGreaterThan(0)
39
+ expect(typeof label).toBe("string")
40
+ expect(label.length).toBeGreaterThan(0)
41
+ }
42
+ })
43
+
44
+ it("should have unique ids", () => {
45
+ const ids = buildMigrationList().migrations.map((m) => m.id)
46
+ expect(new Set(ids).size).toBe(ids.length)
47
+ })
48
+
49
+ it("should emit ids that are exactly accepted by `run` (present in AVAILABLE_CLI_COMPONENTS)", () => {
50
+ for (const { id } of buildMigrationList().migrations) {
51
+ expect(AVAILABLE_CLI_COMPONENTS.has(id)).toBe(true)
52
+ // Also verify the normalizer round-trips it — proving run would accept it
53
+ expect(normalizeComponentName(id, AVAILABLE_CLI_COMPONENTS)).toBe(id)
54
+ }
55
+ })
56
+
57
+ it("should include every id in AVAILABLE_CLI_COMPONENTS with no extras", () => {
58
+ const emittedIds = buildMigrationList().migrations.map((m) => m.id)
59
+ expect(emittedIds).toEqual([...AVAILABLE_CLI_COMPONENTS])
60
+ })
61
+ })
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Derives a human-readable display label from a kebab-case component id.
3
+ * Splits on hyphens and title-cases each word.
4
+ *
5
+ * @example
6
+ * formatComponentLabel("button") // "Button"
7
+ * formatComponentLabel("date-picker") // "Date Picker"
8
+ * formatComponentLabel("toggle-switch") // "Toggle Switch"
9
+ */
10
+ export function formatComponentLabel(id: string): string {
11
+ return id
12
+ .split("-")
13
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
14
+ .join(" ")
15
+ }