tailwindcss-patch 8.7.4-alpha.0 → 9.0.0-alpha.2

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 (70) hide show
  1. package/README.md +65 -5
  2. package/dist/{chunk-ZXW4S356.js → chunk-TOAZIPHJ.js} +295 -285
  3. package/dist/{chunk-6ZDYMYHE.mjs → chunk-VDWTCQ74.mjs} +259 -249
  4. package/dist/cli.js +4 -4
  5. package/dist/cli.mjs +1 -1
  6. package/dist/index.d.mts +46 -134
  7. package/dist/index.d.ts +46 -134
  8. package/dist/index.js +5 -3
  9. package/dist/index.mjs +4 -2
  10. package/package.json +8 -3
  11. package/src/api/tailwindcss-patcher.ts +424 -0
  12. package/src/babel/index.ts +12 -0
  13. package/src/cache/context.ts +212 -0
  14. package/src/cache/store.ts +1440 -0
  15. package/src/cache/types.ts +71 -0
  16. package/src/cli.ts +20 -0
  17. package/src/commands/basic-handlers.ts +145 -0
  18. package/src/commands/cli.ts +56 -0
  19. package/src/commands/command-context.ts +77 -0
  20. package/src/commands/command-definitions.ts +102 -0
  21. package/src/commands/command-metadata.ts +68 -0
  22. package/src/commands/command-registrar.ts +39 -0
  23. package/src/commands/command-runtime.ts +33 -0
  24. package/src/commands/default-handler-map.ts +25 -0
  25. package/src/commands/migrate-config.ts +104 -0
  26. package/src/commands/migrate-handler.ts +67 -0
  27. package/src/commands/migration-aggregation.ts +100 -0
  28. package/src/commands/migration-args.ts +85 -0
  29. package/src/commands/migration-file-executor.ts +189 -0
  30. package/src/commands/migration-output.ts +115 -0
  31. package/src/commands/migration-report-loader.ts +26 -0
  32. package/src/commands/migration-report.ts +21 -0
  33. package/src/commands/migration-source.ts +318 -0
  34. package/src/commands/migration-target-files.ts +161 -0
  35. package/src/commands/migration-target-resolver.ts +34 -0
  36. package/src/commands/migration-types.ts +65 -0
  37. package/src/commands/restore-handler.ts +24 -0
  38. package/src/commands/status-handler.ts +17 -0
  39. package/src/commands/status-output.ts +60 -0
  40. package/src/commands/token-output.ts +30 -0
  41. package/src/commands/types.ts +137 -0
  42. package/src/commands/validate-handler.ts +42 -0
  43. package/src/commands/validate.ts +83 -0
  44. package/src/config/index.ts +25 -0
  45. package/src/config/workspace.ts +87 -0
  46. package/src/constants.ts +4 -0
  47. package/src/extraction/candidate-extractor.ts +354 -0
  48. package/src/index.ts +57 -0
  49. package/src/install/class-collector.ts +1 -0
  50. package/src/install/context-registry.ts +1 -0
  51. package/src/install/index.ts +5 -0
  52. package/src/install/patch-runner.ts +1 -0
  53. package/src/install/process-tailwindcss.ts +1 -0
  54. package/src/install/status.ts +1 -0
  55. package/src/logger.ts +5 -0
  56. package/src/options/legacy.ts +93 -0
  57. package/src/options/normalize.ts +262 -0
  58. package/src/options/types.ts +217 -0
  59. package/src/patching/operations/export-context/index.ts +110 -0
  60. package/src/patching/operations/export-context/postcss-v2.ts +235 -0
  61. package/src/patching/operations/export-context/postcss-v3.ts +249 -0
  62. package/src/patching/operations/extend-length-units.ts +197 -0
  63. package/src/patching/patch-runner.ts +46 -0
  64. package/src/patching/status.ts +262 -0
  65. package/src/runtime/class-collector.ts +105 -0
  66. package/src/runtime/collector.ts +148 -0
  67. package/src/runtime/context-registry.ts +65 -0
  68. package/src/runtime/process-tailwindcss.ts +115 -0
  69. package/src/types.ts +159 -0
  70. package/src/utils.ts +52 -0
@@ -0,0 +1,104 @@
1
+ import type { MigrationWrittenEntry } from './migration-file-executor'
2
+ import type {
3
+ ConfigFileMigrationReport,
4
+ MigrateConfigFilesOptions,
5
+ RestoreConfigFilesOptions,
6
+ RestoreConfigFilesResult,
7
+ } from './migration-types'
8
+ import path from 'pathe'
9
+ import { pkgName, pkgVersion } from '../constants'
10
+ import {
11
+ buildMigrationReport,
12
+ collectMigrationExecutionResult,
13
+ createMigrationAggregationState,
14
+ } from './migration-aggregation'
15
+ import { executeMigrationFile, restoreConfigEntries } from './migration-file-executor'
16
+
17
+ import { loadMigrationReportForRestore } from './migration-report-loader'
18
+ import { resolveMigrationTargetFiles } from './migration-target-resolver'
19
+
20
+ export { MIGRATION_REPORT_KIND, MIGRATION_REPORT_SCHEMA_VERSION } from './migration-report'
21
+ export { migrateConfigSource } from './migration-source'
22
+ export type { ConfigSourceMigrationResult } from './migration-source'
23
+ export { DEFAULT_CONFIG_FILENAMES } from './migration-target-files'
24
+ export type {
25
+ ConfigFileMigrationEntry,
26
+ ConfigFileMigrationReport,
27
+ MigrateConfigFilesOptions,
28
+ RestoreConfigFilesOptions,
29
+ RestoreConfigFilesResult,
30
+ } from './migration-types'
31
+
32
+ export async function migrateConfigFiles(options: MigrateConfigFilesOptions): Promise<ConfigFileMigrationReport> {
33
+ const cwd = path.resolve(options.cwd)
34
+ const dryRun = options.dryRun ?? false
35
+ const rollbackOnError = options.rollbackOnError ?? true
36
+ const backupDirectory = options.backupDir ? path.resolve(cwd, options.backupDir) : undefined
37
+ const targetFiles = await resolveMigrationTargetFiles({
38
+ cwd,
39
+ files: options.files,
40
+ workspace: options.workspace,
41
+ maxDepth: options.maxDepth,
42
+ include: options.include,
43
+ exclude: options.exclude,
44
+ })
45
+ const aggregation = createMigrationAggregationState()
46
+ const wroteEntries: MigrationWrittenEntry[] = []
47
+
48
+ for (const file of targetFiles) {
49
+ const result = await executeMigrationFile({
50
+ cwd,
51
+ file,
52
+ dryRun,
53
+ rollbackOnError,
54
+ wroteEntries,
55
+ ...(backupDirectory ? { backupDirectory } : {}),
56
+ })
57
+ collectMigrationExecutionResult(aggregation, result)
58
+ }
59
+
60
+ return buildMigrationReport(aggregation, {
61
+ cwd,
62
+ dryRun,
63
+ rollbackOnError,
64
+ ...(backupDirectory ? { backupDirectory } : {}),
65
+ toolName: pkgName,
66
+ toolVersion: pkgVersion,
67
+ })
68
+ }
69
+
70
+ export async function restoreConfigFiles(options: RestoreConfigFilesOptions): Promise<RestoreConfigFilesResult> {
71
+ const cwd = path.resolve(options.cwd)
72
+ const dryRun = options.dryRun ?? false
73
+ const strict = options.strict ?? false
74
+ const reportFile = path.resolve(cwd, options.reportFile)
75
+
76
+ const report = await loadMigrationReportForRestore(reportFile)
77
+ const {
78
+ scannedEntries,
79
+ restorableEntries,
80
+ restoredFiles,
81
+ missingBackups,
82
+ skippedEntries,
83
+ restored,
84
+ } = await restoreConfigEntries(report.entries, dryRun)
85
+
86
+ if (strict && missingBackups > 0) {
87
+ throw new Error(`Restore failed: ${missingBackups} backup file(s) missing in report ${reportFile}.`)
88
+ }
89
+
90
+ return {
91
+ cwd,
92
+ reportFile,
93
+ ...(report.reportKind === undefined ? {} : { reportKind: report.reportKind }),
94
+ ...(report.schemaVersion === undefined ? {} : { reportSchemaVersion: report.schemaVersion }),
95
+ dryRun,
96
+ strict,
97
+ scannedEntries,
98
+ restorableEntries,
99
+ restoredFiles,
100
+ missingBackups,
101
+ skippedEntries,
102
+ restored,
103
+ }
104
+ }
@@ -0,0 +1,67 @@
1
+ import type { TailwindcssPatchCommandContext } from './types'
2
+
3
+ import logger from '../logger'
4
+ import { migrateConfigFiles } from './migrate-config'
5
+ import { resolveMigrateCommandArgs } from './migration-args'
6
+ import {
7
+ createMigrationCheckFailureError,
8
+ logMigrationEntries,
9
+ logMigrationReportAsJson,
10
+ logMigrationSummary,
11
+ logNoMigrationConfigFilesWarning,
12
+ writeMigrationReportFile,
13
+ } from './migration-output'
14
+
15
+ export async function migrateCommandDefaultHandler(ctx: TailwindcssPatchCommandContext<'migrate'>) {
16
+ const { args } = ctx
17
+ const {
18
+ include,
19
+ exclude,
20
+ maxDepth,
21
+ checkMode,
22
+ dryRun,
23
+ hasInvalidMaxDepth,
24
+ } = resolveMigrateCommandArgs(args)
25
+
26
+ if (args.workspace && hasInvalidMaxDepth) {
27
+ logger.warn(`Invalid --max-depth value "${String(args.maxDepth)}", fallback to default depth.`)
28
+ }
29
+ const report = await migrateConfigFiles({
30
+ cwd: ctx.cwd,
31
+ dryRun,
32
+ ...(args.config ? { files: [args.config] } : {}),
33
+ ...(args.workspace ? { workspace: true } : {}),
34
+ ...(args.workspace && maxDepth !== undefined ? { maxDepth } : {}),
35
+ ...(args.backupDir ? { backupDir: args.backupDir } : {}),
36
+ ...(include ? { include } : {}),
37
+ ...(exclude ? { exclude } : {}),
38
+ })
39
+
40
+ if (args.reportFile) {
41
+ await writeMigrationReportFile(ctx.cwd, args.reportFile, report)
42
+ }
43
+
44
+ if (args.json) {
45
+ logMigrationReportAsJson(report)
46
+ if (checkMode && report.changedFiles > 0) {
47
+ throw createMigrationCheckFailureError(report.changedFiles)
48
+ }
49
+ if (report.scannedFiles === 0) {
50
+ logNoMigrationConfigFilesWarning()
51
+ }
52
+ return report
53
+ }
54
+
55
+ if (report.scannedFiles === 0) {
56
+ logNoMigrationConfigFilesWarning()
57
+ return report
58
+ }
59
+
60
+ logMigrationEntries(report, dryRun)
61
+ logMigrationSummary(report)
62
+
63
+ if (checkMode && report.changedFiles > 0) {
64
+ throw createMigrationCheckFailureError(report.changedFiles)
65
+ }
66
+ return report
67
+ }
@@ -0,0 +1,100 @@
1
+ import type { ExecuteMigrationFileResult } from './migration-file-executor'
2
+ import type { ConfigFileMigrationEntry, ConfigFileMigrationReport } from './migration-types'
3
+ import {
4
+ MIGRATION_REPORT_KIND,
5
+ MIGRATION_REPORT_SCHEMA_VERSION,
6
+ } from './migration-report'
7
+
8
+ export interface MigrationAggregationState {
9
+ scannedFiles: number
10
+ changedFiles: number
11
+ writtenFiles: number
12
+ backupsWritten: number
13
+ unchangedFiles: number
14
+ missingFiles: number
15
+ entries: ConfigFileMigrationEntry[]
16
+ }
17
+
18
+ export interface BuildMigrationReportContext {
19
+ cwd: string
20
+ dryRun: boolean
21
+ rollbackOnError: boolean
22
+ backupDirectory?: string
23
+ toolName: string
24
+ toolVersion: string
25
+ generatedAt?: string
26
+ }
27
+
28
+ export function createMigrationAggregationState(): MigrationAggregationState {
29
+ return {
30
+ scannedFiles: 0,
31
+ changedFiles: 0,
32
+ writtenFiles: 0,
33
+ backupsWritten: 0,
34
+ unchangedFiles: 0,
35
+ missingFiles: 0,
36
+ entries: [],
37
+ }
38
+ }
39
+
40
+ export function collectMigrationExecutionResult(
41
+ state: MigrationAggregationState,
42
+ result: ExecuteMigrationFileResult,
43
+ ) {
44
+ if (result.missing) {
45
+ state.missingFiles += 1
46
+ return
47
+ }
48
+
49
+ state.scannedFiles += 1
50
+ state.entries.push(result.entry)
51
+
52
+ if (result.changed) {
53
+ state.changedFiles += 1
54
+ if (result.wrote) {
55
+ state.writtenFiles += 1
56
+ }
57
+ if (result.backupWritten) {
58
+ state.backupsWritten += 1
59
+ }
60
+ }
61
+ else {
62
+ state.unchangedFiles += 1
63
+ }
64
+ }
65
+
66
+ export function buildMigrationReport(
67
+ state: MigrationAggregationState,
68
+ context: BuildMigrationReportContext,
69
+ ): ConfigFileMigrationReport {
70
+ const {
71
+ cwd,
72
+ dryRun,
73
+ rollbackOnError,
74
+ backupDirectory,
75
+ toolName,
76
+ toolVersion,
77
+ generatedAt = new Date().toISOString(),
78
+ } = context
79
+
80
+ return {
81
+ reportKind: MIGRATION_REPORT_KIND,
82
+ schemaVersion: MIGRATION_REPORT_SCHEMA_VERSION,
83
+ generatedAt,
84
+ tool: {
85
+ name: toolName,
86
+ version: toolVersion,
87
+ },
88
+ cwd,
89
+ dryRun,
90
+ rollbackOnError,
91
+ ...(backupDirectory ? { backupDirectory } : {}),
92
+ scannedFiles: state.scannedFiles,
93
+ changedFiles: state.changedFiles,
94
+ writtenFiles: state.writtenFiles,
95
+ backupsWritten: state.backupsWritten,
96
+ unchangedFiles: state.unchangedFiles,
97
+ missingFiles: state.missingFiles,
98
+ entries: state.entries,
99
+ }
100
+ }
@@ -0,0 +1,85 @@
1
+ import type { TailwindcssPatchCommandArgMap } from './types'
2
+
3
+ export interface ResolvedMigrateCommandArgs {
4
+ include: string[] | undefined
5
+ exclude: string[] | undefined
6
+ maxDepth: number | undefined
7
+ checkMode: boolean
8
+ dryRun: boolean
9
+ hasInvalidMaxDepth: boolean
10
+ }
11
+
12
+ export interface ResolvedRestoreCommandArgs {
13
+ reportFile: string
14
+ dryRun: boolean
15
+ strict: boolean
16
+ }
17
+
18
+ export interface ResolvedValidateCommandArgs {
19
+ reportFile: string
20
+ strict: boolean
21
+ }
22
+
23
+ export function normalizePatternArgs(value?: string | string[]) {
24
+ if (!value) {
25
+ return undefined
26
+ }
27
+ const raw = Array.isArray(value) ? value : [value]
28
+ const values = raw
29
+ .flatMap(item => item.split(','))
30
+ .map(item => item.trim())
31
+ .filter(Boolean)
32
+ return values.length > 0 ? values : undefined
33
+ }
34
+
35
+ function parseMaxDepth(value: TailwindcssPatchCommandArgMap['migrate']['maxDepth']) {
36
+ if (value === undefined) {
37
+ return {
38
+ maxDepth: undefined,
39
+ hasInvalidMaxDepth: false,
40
+ }
41
+ }
42
+ const parsed = Number(value)
43
+ if (!Number.isFinite(parsed) || parsed < 0) {
44
+ return {
45
+ maxDepth: undefined,
46
+ hasInvalidMaxDepth: true,
47
+ }
48
+ }
49
+ return {
50
+ maxDepth: Math.floor(parsed),
51
+ hasInvalidMaxDepth: false,
52
+ }
53
+ }
54
+
55
+ export function resolveMigrateCommandArgs(args: TailwindcssPatchCommandArgMap['migrate']): ResolvedMigrateCommandArgs {
56
+ const include = normalizePatternArgs(args.include)
57
+ const exclude = normalizePatternArgs(args.exclude)
58
+ const { maxDepth, hasInvalidMaxDepth } = parseMaxDepth(args.maxDepth)
59
+ const checkMode = args.check ?? false
60
+ const dryRun = args.dryRun ?? checkMode
61
+
62
+ return {
63
+ include,
64
+ exclude,
65
+ maxDepth,
66
+ checkMode,
67
+ dryRun,
68
+ hasInvalidMaxDepth,
69
+ }
70
+ }
71
+
72
+ export function resolveRestoreCommandArgs(args: TailwindcssPatchCommandArgMap['restore']): ResolvedRestoreCommandArgs {
73
+ return {
74
+ reportFile: args.reportFile ?? '.tw-patch/migrate-report.json',
75
+ dryRun: args.dryRun ?? false,
76
+ strict: args.strict ?? false,
77
+ }
78
+ }
79
+
80
+ export function resolveValidateCommandArgs(args: TailwindcssPatchCommandArgMap['validate']): ResolvedValidateCommandArgs {
81
+ return {
82
+ reportFile: args.reportFile ?? '.tw-patch/migrate-report.json',
83
+ strict: args.strict ?? false,
84
+ }
85
+ }
@@ -0,0 +1,189 @@
1
+ import type { ConfigFileMigrationEntry } from './migration-types'
2
+ import fs from 'fs-extra'
3
+
4
+ import path from 'pathe'
5
+ import { migrateConfigSource } from './migration-source'
6
+ import { resolveBackupRelativePath } from './migration-target-files'
7
+
8
+ export type MigrationExecutionEntry = ConfigFileMigrationEntry
9
+
10
+ export interface MigrationWrittenEntry {
11
+ file: string
12
+ source: string
13
+ entry: MigrationExecutionEntry
14
+ }
15
+
16
+ export interface ExecuteMigrationFileOptions {
17
+ cwd: string
18
+ file: string
19
+ dryRun: boolean
20
+ rollbackOnError: boolean
21
+ backupDirectory?: string
22
+ wroteEntries: MigrationWrittenEntry[]
23
+ }
24
+
25
+ export type ExecuteMigrationFileResult
26
+ = | {
27
+ missing: true
28
+ changed: false
29
+ wrote: false
30
+ backupWritten: false
31
+ }
32
+ | {
33
+ missing: false
34
+ changed: boolean
35
+ wrote: boolean
36
+ backupWritten: boolean
37
+ entry: MigrationExecutionEntry
38
+ }
39
+
40
+ export async function rollbackWrittenEntries(wroteEntries: MigrationWrittenEntry[]) {
41
+ let rollbackCount = 0
42
+ for (const written of [...wroteEntries].reverse()) {
43
+ try {
44
+ await fs.writeFile(written.file, written.source, 'utf8')
45
+ written.entry.written = false
46
+ written.entry.rolledBack = true
47
+ rollbackCount += 1
48
+ }
49
+ catch {
50
+ // Continue best-effort rollback to avoid leaving even more partial state.
51
+ }
52
+ }
53
+ return rollbackCount
54
+ }
55
+
56
+ export async function executeMigrationFile(options: ExecuteMigrationFileOptions): Promise<ExecuteMigrationFileResult> {
57
+ const {
58
+ cwd,
59
+ file,
60
+ dryRun,
61
+ rollbackOnError,
62
+ backupDirectory,
63
+ wroteEntries,
64
+ } = options
65
+
66
+ const exists = await fs.pathExists(file)
67
+ if (!exists) {
68
+ return {
69
+ missing: true,
70
+ changed: false,
71
+ wrote: false,
72
+ backupWritten: false,
73
+ }
74
+ }
75
+
76
+ const source = await fs.readFile(file, 'utf8')
77
+ const migrated = migrateConfigSource(source)
78
+ const entry: MigrationExecutionEntry = {
79
+ file,
80
+ changed: migrated.changed,
81
+ written: false,
82
+ rolledBack: false,
83
+ changes: migrated.changes,
84
+ }
85
+
86
+ if (!migrated.changed || dryRun) {
87
+ return {
88
+ missing: false,
89
+ changed: migrated.changed,
90
+ wrote: false,
91
+ backupWritten: false,
92
+ entry,
93
+ }
94
+ }
95
+
96
+ let backupWritten = false
97
+ try {
98
+ if (backupDirectory) {
99
+ const backupRelativePath = resolveBackupRelativePath(cwd, file)
100
+ const backupFile = path.resolve(backupDirectory, backupRelativePath)
101
+ await fs.ensureDir(path.dirname(backupFile))
102
+ await fs.writeFile(backupFile, source, 'utf8')
103
+ entry.backupFile = backupFile
104
+ backupWritten = true
105
+ }
106
+
107
+ await fs.writeFile(file, migrated.code, 'utf8')
108
+ entry.written = true
109
+ wroteEntries.push({ file, source, entry })
110
+
111
+ return {
112
+ missing: false,
113
+ changed: true,
114
+ wrote: true,
115
+ backupWritten,
116
+ entry,
117
+ }
118
+ }
119
+ catch (error) {
120
+ const rollbackCount = rollbackOnError && wroteEntries.length > 0
121
+ ? await rollbackWrittenEntries(wroteEntries)
122
+ : 0
123
+ const reason = error instanceof Error ? error.message : String(error)
124
+ const rollbackHint = rollbackOnError && rollbackCount > 0
125
+ ? ` Rolled back ${rollbackCount} previously written file(s).`
126
+ : ''
127
+ throw new Error(`Failed to write migrated config "${file}": ${reason}.${rollbackHint}`)
128
+ }
129
+ }
130
+
131
+ export interface RestoreReportEntry {
132
+ file?: string
133
+ backupFile?: string
134
+ }
135
+
136
+ export interface RestoreEntriesResult {
137
+ scannedEntries: number
138
+ restorableEntries: number
139
+ restoredFiles: number
140
+ missingBackups: number
141
+ skippedEntries: number
142
+ restored: string[]
143
+ }
144
+
145
+ export async function restoreConfigEntries(entries: RestoreReportEntry[], dryRun: boolean): Promise<RestoreEntriesResult> {
146
+ let scannedEntries = 0
147
+ let restorableEntries = 0
148
+ let restoredFiles = 0
149
+ let missingBackups = 0
150
+ let skippedEntries = 0
151
+ const restored: string[] = []
152
+
153
+ for (const entry of entries) {
154
+ scannedEntries += 1
155
+ const targetFile = entry.file ? path.resolve(entry.file) : undefined
156
+ const backupFile = entry.backupFile ? path.resolve(entry.backupFile) : undefined
157
+
158
+ if (!targetFile || !backupFile) {
159
+ skippedEntries += 1
160
+ continue
161
+ }
162
+
163
+ restorableEntries += 1
164
+
165
+ const backupExists = await fs.pathExists(backupFile)
166
+ if (!backupExists) {
167
+ missingBackups += 1
168
+ continue
169
+ }
170
+
171
+ if (!dryRun) {
172
+ const backupContent = await fs.readFile(backupFile, 'utf8')
173
+ await fs.ensureDir(path.dirname(targetFile))
174
+ await fs.writeFile(targetFile, backupContent, 'utf8')
175
+ }
176
+
177
+ restoredFiles += 1
178
+ restored.push(targetFile)
179
+ }
180
+
181
+ return {
182
+ scannedEntries,
183
+ restorableEntries,
184
+ restoredFiles,
185
+ missingBackups,
186
+ skippedEntries,
187
+ restored,
188
+ }
189
+ }
@@ -0,0 +1,115 @@
1
+ import type { ConfigFileMigrationReport, RestoreConfigFilesResult } from './migration-types'
2
+ import type { ValidateFailureSummary, ValidateJsonFailurePayload, ValidateJsonSuccessPayload } from './validate'
3
+
4
+ import process from 'node:process'
5
+ import fs from 'fs-extra'
6
+ import path from 'pathe'
7
+ import logger from '../logger'
8
+
9
+ function formatPathForLog(file: string) {
10
+ return file.replace(process.cwd(), '.')
11
+ }
12
+
13
+ export function createMigrationCheckFailureError(changedFiles: number) {
14
+ return new Error(`Migration check failed: ${changedFiles} file(s) still need migration.`)
15
+ }
16
+
17
+ export async function writeMigrationReportFile(
18
+ cwd: string,
19
+ reportFile: string,
20
+ report: ConfigFileMigrationReport,
21
+ ) {
22
+ const reportPath = path.resolve(cwd, reportFile)
23
+ await fs.ensureDir(path.dirname(reportPath))
24
+ await fs.writeJSON(reportPath, report, { spaces: 2 })
25
+ logger.info(`Migration report written: ${formatPathForLog(reportPath)}`)
26
+ }
27
+
28
+ export function logMigrationReportAsJson(report: ConfigFileMigrationReport) {
29
+ logger.log(JSON.stringify(report, null, 2))
30
+ }
31
+
32
+ export function logNoMigrationConfigFilesWarning() {
33
+ logger.warn('No config files found for migration.')
34
+ }
35
+
36
+ export function logMigrationEntries(report: ConfigFileMigrationReport, dryRun: boolean) {
37
+ for (const entry of report.entries) {
38
+ const fileLabel = formatPathForLog(entry.file)
39
+ if (!entry.changed) {
40
+ logger.info(`No changes: ${fileLabel}`)
41
+ continue
42
+ }
43
+ if (dryRun) {
44
+ logger.info(`[dry-run] ${fileLabel}`)
45
+ }
46
+ else {
47
+ logger.success(`Migrated: ${fileLabel}`)
48
+ }
49
+ for (const change of entry.changes) {
50
+ logger.info(` - ${change}`)
51
+ }
52
+ if (entry.backupFile) {
53
+ logger.info(` - backup: ${formatPathForLog(entry.backupFile)}`)
54
+ }
55
+ }
56
+ }
57
+
58
+ export function logMigrationSummary(report: ConfigFileMigrationReport) {
59
+ logger.info(
60
+ `Migration summary: scanned=${report.scannedFiles}, changed=${report.changedFiles}, written=${report.writtenFiles}, backups=${report.backupsWritten}, missing=${report.missingFiles}, unchanged=${report.unchangedFiles}`,
61
+ )
62
+ }
63
+
64
+ export function logRestoreResultAsJson(result: RestoreConfigFilesResult) {
65
+ logger.log(JSON.stringify(result, null, 2))
66
+ }
67
+
68
+ export function logRestoreSummary(result: RestoreConfigFilesResult) {
69
+ logger.info(
70
+ `Restore summary: scanned=${result.scannedEntries}, restorable=${result.restorableEntries}, restored=${result.restoredFiles}, missingBackups=${result.missingBackups}, skipped=${result.skippedEntries}`,
71
+ )
72
+ if (result.restored.length > 0) {
73
+ const preview = result.restored.slice(0, 5)
74
+ for (const file of preview) {
75
+ logger.info(` - ${formatPathForLog(file)}`)
76
+ }
77
+ if (result.restored.length > preview.length) {
78
+ logger.info(` ...and ${result.restored.length - preview.length} more`)
79
+ }
80
+ }
81
+ }
82
+
83
+ export function logValidateSuccessAsJson(result: RestoreConfigFilesResult) {
84
+ const payload: ValidateJsonSuccessPayload = {
85
+ ok: true,
86
+ ...result,
87
+ }
88
+ logger.log(JSON.stringify(payload, null, 2))
89
+ }
90
+
91
+ export function logValidateSuccessSummary(result: RestoreConfigFilesResult) {
92
+ logger.success(
93
+ `Migration report validated: scanned=${result.scannedEntries}, restorable=${result.restorableEntries}, missingBackups=${result.missingBackups}, skipped=${result.skippedEntries}`,
94
+ )
95
+
96
+ if (result.reportKind || result.reportSchemaVersion !== undefined) {
97
+ const kind = result.reportKind ?? 'unknown'
98
+ const schema = result.reportSchemaVersion === undefined ? 'unknown' : String(result.reportSchemaVersion)
99
+ logger.info(` metadata: kind=${kind}, schema=${schema}`)
100
+ }
101
+ }
102
+
103
+ export function logValidateFailureAsJson(summary: ValidateFailureSummary) {
104
+ const payload: ValidateJsonFailurePayload = {
105
+ ok: false,
106
+ reason: summary.reason,
107
+ exitCode: summary.exitCode,
108
+ message: summary.message,
109
+ }
110
+ logger.log(JSON.stringify(payload, null, 2))
111
+ }
112
+
113
+ export function logValidateFailureSummary(summary: ValidateFailureSummary) {
114
+ logger.error(`Validation failed [${summary.reason}] (exit ${summary.exitCode}): ${summary.message}`)
115
+ }
@@ -0,0 +1,26 @@
1
+ import type { RestoreReportEntry } from './migration-file-executor'
2
+
3
+ import fs from 'fs-extra'
4
+ import { assertMigrationReportCompatibility } from './migration-report'
5
+
6
+ export interface LoadedMigrationReportForRestore {
7
+ reportKind?: string
8
+ schemaVersion?: number
9
+ entries: RestoreReportEntry[]
10
+ }
11
+
12
+ export async function loadMigrationReportForRestore(reportFile: string): Promise<LoadedMigrationReportForRestore> {
13
+ const report = await fs.readJSON(reportFile) as {
14
+ reportKind?: string
15
+ schemaVersion?: number
16
+ entries?: RestoreReportEntry[]
17
+ }
18
+
19
+ assertMigrationReportCompatibility(report, reportFile)
20
+
21
+ return {
22
+ ...(report.reportKind === undefined ? {} : { reportKind: report.reportKind }),
23
+ ...(report.schemaVersion === undefined ? {} : { schemaVersion: report.schemaVersion }),
24
+ entries: Array.isArray(report.entries) ? report.entries : [],
25
+ }
26
+ }
@@ -0,0 +1,21 @@
1
+ export const MIGRATION_REPORT_KIND = 'tw-patch-migrate-report'
2
+ export const MIGRATION_REPORT_SCHEMA_VERSION = 1
3
+
4
+ export interface MigrationReportInput {
5
+ reportKind?: string
6
+ schemaVersion?: number
7
+ }
8
+
9
+ export function assertMigrationReportCompatibility(report: MigrationReportInput, reportFile: string) {
10
+ if (report.reportKind !== undefined && report.reportKind !== MIGRATION_REPORT_KIND) {
11
+ throw new Error(`Unsupported report kind "${report.reportKind}" in ${reportFile}.`)
12
+ }
13
+ if (
14
+ report.schemaVersion !== undefined
15
+ && (!Number.isInteger(report.schemaVersion) || report.schemaVersion > MIGRATION_REPORT_SCHEMA_VERSION)
16
+ ) {
17
+ throw new Error(
18
+ `Unsupported report schema version "${String(report.schemaVersion)}" in ${reportFile}. Current supported version is ${MIGRATION_REPORT_SCHEMA_VERSION}.`,
19
+ )
20
+ }
21
+ }