@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.
- package/README.md +59 -0
- package/dist/tapestry-react-shim.cjs +5 -1
- package/package.json +3 -3
- package/src/availableComponents.ts +14 -0
- package/src/components/button/transforms/tooltipToWrapper.test.ts +50 -3
- package/src/components/button/transforms/tooltipToWrapper.ts +18 -0
- package/src/components/dropdown/index.test.ts +67 -0
- package/src/components/dropdown/index.ts +4 -0
- package/src/components/dropdown/transforms/placementIdToMenu.test.ts +6 -1
- package/src/components/dropdown/transforms/placementIdToMenu.ts +1 -1
- package/src/components/dropdown/transforms/variantToKind.test.ts +292 -0
- package/src/components/dropdown/transforms/variantToKind.ts +138 -0
- package/src/components/dropdown/transforms/wrapIconTrigger.test.ts +336 -0
- package/src/components/dropdown/transforms/wrapIconTrigger.ts +233 -0
- package/src/components/flex/index.test.ts +140 -0
- package/src/components/flex/index.ts +40 -0
- package/src/components/flex/transforms/alignmentToAlign.test.ts +185 -0
- package/src/components/flex/transforms/alignmentToAlign.ts +73 -0
- package/src/components/flex/transforms/axisToDirection.test.ts +140 -0
- package/src/components/flex/transforms/axisToDirection.ts +51 -0
- package/src/components/flex/transforms/distributionToJustify.test.ts +214 -0
- package/src/components/flex/transforms/distributionToJustify.ts +76 -0
- package/src/components/flex/transforms/innerRefToRef.test.ts +100 -0
- package/src/components/flex/transforms/innerRefToRef.ts +14 -0
- package/src/components/flex/transforms/moveFlexImport.test.ts +202 -0
- package/src/components/flex/transforms/moveFlexImport.ts +14 -0
- package/src/components/flex/transforms/setDefaultAxis.test.ts +114 -0
- package/src/components/flex/transforms/setDefaultAxis.ts +29 -0
- package/src/components/flex/transforms/spacingToGap.test.ts +176 -0
- package/src/components/flex/transforms/spacingToGap.ts +49 -0
- package/src/components/select/index.ts +2 -0
- package/src/components/select/transforms/onChangeSignature.test.ts +224 -0
- package/src/components/select/transforms/onChangeSignature.ts +172 -0
- package/src/components/shared/actions/resolveConstantAttributeValue.test.ts +122 -0
- package/src/components/shared/actions/resolveConstantAttributeValue.ts +31 -0
- package/src/components/toggle-switch/index.ts +2 -0
- package/src/components/toggle-switch/transforms/onClickToOnChange.test.ts +210 -0
- package/src/components/toggle-switch/transforms/onClickToOnChange.ts +71 -0
- package/src/index.test.ts +79 -0
- package/src/index.ts +34 -18
- package/src/migrationList.ts +22 -0
- package/src/utils/componentLabel.test.ts +61 -0
- 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([...
|
|
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,
|
|
45
|
+
const key = normalizeComponentName(componentName, AVAILABLE_CLI_COMPONENTS)
|
|
57
46
|
|
|
58
|
-
if (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: ${[...
|
|
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
|
+
}
|