tailwindcss-patch 9.0.0-alpha.1 → 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.
- package/README.md +20 -0
- package/dist/{chunk-Z6OMJZTU.js → chunk-TOAZIPHJ.js} +29 -11
- package/dist/{chunk-SWLOK2S6.mjs → chunk-VDWTCQ74.mjs} +29 -11
- package/dist/cli.js +4 -4
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +43 -35
- package/dist/index.d.ts +43 -35
- package/dist/index.js +2 -2
- package/dist/index.mjs +1 -1
- package/package.json +8 -3
- package/src/api/tailwindcss-patcher.ts +424 -0
- package/src/babel/index.ts +12 -0
- package/src/cache/context.ts +212 -0
- package/src/cache/store.ts +1440 -0
- package/src/cache/types.ts +71 -0
- package/src/cli.ts +20 -0
- package/src/commands/basic-handlers.ts +145 -0
- package/src/commands/cli.ts +56 -0
- package/src/commands/command-context.ts +77 -0
- package/src/commands/command-definitions.ts +102 -0
- package/src/commands/command-metadata.ts +68 -0
- package/src/commands/command-registrar.ts +39 -0
- package/src/commands/command-runtime.ts +33 -0
- package/src/commands/default-handler-map.ts +25 -0
- package/src/commands/migrate-config.ts +104 -0
- package/src/commands/migrate-handler.ts +67 -0
- package/src/commands/migration-aggregation.ts +100 -0
- package/src/commands/migration-args.ts +85 -0
- package/src/commands/migration-file-executor.ts +189 -0
- package/src/commands/migration-output.ts +115 -0
- package/src/commands/migration-report-loader.ts +26 -0
- package/src/commands/migration-report.ts +21 -0
- package/src/commands/migration-source.ts +318 -0
- package/src/commands/migration-target-files.ts +161 -0
- package/src/commands/migration-target-resolver.ts +34 -0
- package/src/commands/migration-types.ts +65 -0
- package/src/commands/restore-handler.ts +24 -0
- package/src/commands/status-handler.ts +17 -0
- package/src/commands/status-output.ts +60 -0
- package/src/commands/token-output.ts +30 -0
- package/src/commands/types.ts +137 -0
- package/src/commands/validate-handler.ts +42 -0
- package/src/commands/validate.ts +83 -0
- package/src/config/index.ts +25 -0
- package/src/config/workspace.ts +87 -0
- package/src/constants.ts +4 -0
- package/src/extraction/candidate-extractor.ts +354 -0
- package/src/index.ts +57 -0
- package/src/install/class-collector.ts +1 -0
- package/src/install/context-registry.ts +1 -0
- package/src/install/index.ts +5 -0
- package/src/install/patch-runner.ts +1 -0
- package/src/install/process-tailwindcss.ts +1 -0
- package/src/install/status.ts +1 -0
- package/src/logger.ts +5 -0
- package/src/options/legacy.ts +93 -0
- package/src/options/normalize.ts +262 -0
- package/src/options/types.ts +217 -0
- package/src/patching/operations/export-context/index.ts +110 -0
- package/src/patching/operations/export-context/postcss-v2.ts +235 -0
- package/src/patching/operations/export-context/postcss-v3.ts +249 -0
- package/src/patching/operations/extend-length-units.ts +197 -0
- package/src/patching/patch-runner.ts +46 -0
- package/src/patching/status.ts +262 -0
- package/src/runtime/class-collector.ts +105 -0
- package/src/runtime/collector.ts +148 -0
- package/src/runtime/context-registry.ts +65 -0
- package/src/runtime/process-tailwindcss.ts +115 -0
- package/src/types.ts +159 -0
- 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
|
+
}
|