voltra 2.0.0 → 2.1.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/build/cjs/config/normalize.js +93 -0
- package/build/cjs/config/normalize.js.map +1 -1
- package/build/cjs/index.js +5 -2
- package/build/cjs/index.js.map +1 -1
- package/build/cjs/platforms/android/apply.js +11 -2
- package/build/cjs/platforms/android/apply.js.map +1 -1
- package/build/cjs/platforms/android/generated.js +152 -130
- package/build/cjs/platforms/android/generated.js.map +1 -1
- package/build/cjs/platforms/android/gradle.js +120 -0
- package/build/cjs/platforms/android/gradle.js.map +1 -0
- package/build/cjs/platforms/ios/generated.js +246 -142
- package/build/cjs/platforms/ios/generated.js.map +1 -1
- package/build/cjs/platforms/ios/xcodeTarget.js +68 -0
- package/build/cjs/platforms/ios/xcodeTarget.js.map +1 -1
- package/build/cjs/platforms/shared/widgetModule.js +151 -0
- package/build/cjs/platforms/shared/widgetModule.js.map +1 -0
- package/build/esm/config/normalize.js +93 -0
- package/build/esm/config/normalize.js.map +1 -1
- package/build/esm/index.js +1 -0
- package/build/esm/index.js.map +1 -1
- package/build/esm/platforms/android/apply.js +11 -2
- package/build/esm/platforms/android/apply.js.map +1 -1
- package/build/esm/platforms/android/generated.js +152 -97
- package/build/esm/platforms/android/generated.js.map +1 -1
- package/build/esm/platforms/android/gradle.js +112 -0
- package/build/esm/platforms/android/gradle.js.map +1 -0
- package/build/esm/platforms/ios/generated.js +246 -109
- package/build/esm/platforms/ios/generated.js.map +1 -1
- package/build/esm/platforms/ios/xcodeTarget.js +69 -1
- package/build/esm/platforms/ios/xcodeTarget.js.map +1 -1
- package/build/esm/platforms/shared/widgetModule.js +111 -0
- package/build/esm/platforms/shared/widgetModule.js.map +1 -0
- package/build/types/config/normalize.d.ts.map +1 -1
- package/build/types/config/types.d.ts +32 -0
- package/build/types/config/types.d.ts.map +1 -1
- package/build/types/index.d.ts +3 -1
- package/build/types/index.d.ts.map +1 -1
- package/build/types/platforms/android/apply.d.ts.map +1 -1
- package/build/types/platforms/android/generated.d.ts.map +1 -1
- package/build/types/platforms/android/gradle.d.ts +18 -0
- package/build/types/platforms/android/gradle.d.ts.map +1 -0
- package/build/types/platforms/ios/generated.d.ts.map +1 -1
- package/build/types/platforms/ios/xcodeTarget.d.ts.map +1 -1
- package/build/types/platforms/shared/widgetModule.d.ts +9 -0
- package/build/types/platforms/shared/widgetModule.d.ts.map +1 -0
- package/package.json +2 -1
- package/src/apply/index.ts +309 -0
- package/src/apply/preflight.ts +172 -0
- package/src/bin.ts +14 -0
- package/src/commands/apply.ts +114 -0
- package/src/config/defaults.ts +34 -0
- package/src/config/load.ts +100 -0
- package/src/config/normalize.ts +562 -0
- package/src/config/types.ts +317 -0
- package/src/cosmiconfig.d.ts +21 -0
- package/src/dependencies/platformPackages.ts +75 -0
- package/src/discovery/android.ts +233 -0
- package/src/discovery/ios.ts +579 -0
- package/src/fs/path.ts +25 -0
- package/src/fs/readWrite.ts +102 -0
- package/src/git/status.ts +205 -0
- package/src/index.ts +204 -0
- package/src/platforms/android/apply.ts +106 -0
- package/src/platforms/android/generated.ts +1284 -0
- package/src/platforms/android/gradle.ts +155 -0
- package/src/platforms/android/manifest.ts +379 -0
- package/src/platforms/ios/apply.ts +139 -0
- package/src/platforms/ios/deploymentTarget.ts +47 -0
- package/src/platforms/ios/entitlements.ts +146 -0
- package/src/platforms/ios/generated.ts +1233 -0
- package/src/platforms/ios/mainAppEntitlements.ts +11 -0
- package/src/platforms/ios/plist.ts +321 -0
- package/src/platforms/ios/podfile.ts +255 -0
- package/src/platforms/ios/targetName.ts +11 -0
- package/src/platforms/ios/xcode.ts +181 -0
- package/src/platforms/ios/xcodeTarget.ts +899 -0
- package/src/platforms/shared/widgetModule.ts +153 -0
- package/src/reporting/clack.ts +67 -0
- package/src/reporting/summary.ts +116 -0
- package/src/state/diff.ts +21 -0
- package/src/state/files.ts +52 -0
- package/src/state/load.ts +66 -0
- package/src/state/save.ts +31 -0
- package/src/xml2js.d.ts +23 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import vm from 'node:vm'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
|
|
6
|
+
import * as babel from '@babel/core'
|
|
7
|
+
|
|
8
|
+
const MODULE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '']
|
|
9
|
+
|
|
10
|
+
export interface WidgetModuleBuildInfo {
|
|
11
|
+
isDev: false
|
|
12
|
+
metroUrl: null
|
|
13
|
+
appVersion: 'unknown'
|
|
14
|
+
voltraVersion: '1.4.1'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const DYNAMIC_WIDGET_BUILD_INFO: WidgetModuleBuildInfo = {
|
|
18
|
+
isDev: false,
|
|
19
|
+
metroUrl: null,
|
|
20
|
+
appVersion: 'unknown',
|
|
21
|
+
voltraVersion: '1.4.1',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function evaluateWidgetModuleExports(
|
|
25
|
+
projectRoot: string,
|
|
26
|
+
filePath: string,
|
|
27
|
+
createError: (message: string) => Error
|
|
28
|
+
): unknown {
|
|
29
|
+
const projectRequire = createProjectRequire(projectRoot)
|
|
30
|
+
const moduleCache = new Map<string, unknown>()
|
|
31
|
+
|
|
32
|
+
const customRequire = (moduleSpecifier: string, currentDir: string): unknown => {
|
|
33
|
+
if (!isLocalModule(moduleSpecifier)) {
|
|
34
|
+
return projectRequire(moduleSpecifier)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const resolvedModulePath = resolveModulePath(moduleSpecifier, currentDir)
|
|
38
|
+
|
|
39
|
+
if (!resolvedModulePath) {
|
|
40
|
+
throw createError(`Cannot resolve module '${moduleSpecifier}' from '${currentDir}'`)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const cachedModule = moduleCache.get(resolvedModulePath)
|
|
44
|
+
if (cachedModule !== undefined) {
|
|
45
|
+
return cachedModule
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const transpiledCode = transpileWidgetModule(projectRoot, resolvedModulePath, projectRequire, createError)
|
|
49
|
+
const moduleDir = path.dirname(resolvedModulePath)
|
|
50
|
+
const moduleRecord = { exports: {} as Record<string, unknown> }
|
|
51
|
+
moduleCache.set(resolvedModulePath, moduleRecord.exports)
|
|
52
|
+
|
|
53
|
+
const context = vm.createContext({
|
|
54
|
+
__dirname: moduleDir,
|
|
55
|
+
__filename: resolvedModulePath,
|
|
56
|
+
console,
|
|
57
|
+
exports: moduleRecord.exports,
|
|
58
|
+
module: moduleRecord,
|
|
59
|
+
process,
|
|
60
|
+
require: (specifier: string) => customRequire(specifier, moduleDir),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const script = new vm.Script(transpiledCode, { filename: resolvedModulePath })
|
|
64
|
+
script.runInContext(context)
|
|
65
|
+
|
|
66
|
+
moduleCache.set(resolvedModulePath, moduleRecord.exports)
|
|
67
|
+
return moduleRecord.exports
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return customRequire(filePath, path.dirname(filePath))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function transpileWidgetModule(
|
|
74
|
+
projectRoot: string,
|
|
75
|
+
filePath: string,
|
|
76
|
+
projectRequire: NodeRequire,
|
|
77
|
+
createError: (message: string) => Error
|
|
78
|
+
): string {
|
|
79
|
+
const source = fs.readFileSync(filePath, 'utf8')
|
|
80
|
+
const projectBabelConfigPath = resolveProjectBabelConfig(projectRoot)
|
|
81
|
+
const result = babel.transformSync(source, {
|
|
82
|
+
babelrc: false,
|
|
83
|
+
configFile: projectBabelConfigPath,
|
|
84
|
+
cwd: projectRoot,
|
|
85
|
+
filename: filePath,
|
|
86
|
+
presets: projectBabelConfigPath ? undefined : [resolveFallbackBabelPreset(projectRequire, createError)],
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
if (!result?.code) {
|
|
90
|
+
throw createError(`Babel transpilation failed for ${filePath}`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return result.code
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolveProjectBabelConfig(projectRoot: string): string | undefined {
|
|
97
|
+
const candidates = ['babel.config.js', 'babel.config.cjs', 'babel.config.mjs']
|
|
98
|
+
|
|
99
|
+
for (const candidate of candidates) {
|
|
100
|
+
const candidatePath = path.join(projectRoot, candidate)
|
|
101
|
+
if (fs.existsSync(candidatePath)) {
|
|
102
|
+
return candidatePath
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return undefined
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveFallbackBabelPreset(projectRequire: NodeRequire, createError: (message: string) => Error): string {
|
|
110
|
+
try {
|
|
111
|
+
return projectRequire.resolve('@react-native/babel-preset')
|
|
112
|
+
} catch {
|
|
113
|
+
try {
|
|
114
|
+
return projectRequire.resolve('babel-preset-expo')
|
|
115
|
+
} catch {
|
|
116
|
+
throw createError(
|
|
117
|
+
'Could not resolve a Babel preset for widget evaluation. Add a project babel.config.js or install @react-native/babel-preset.'
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function createProjectRequire(projectRoot: string): NodeRequire {
|
|
124
|
+
return createRequire(path.join(projectRoot, 'package.json'))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isLocalModule(moduleSpecifier: string): boolean {
|
|
128
|
+
return moduleSpecifier.startsWith('.') || moduleSpecifier.startsWith('/')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function resolveModulePath(moduleSpecifier: string, fromDir: string): string | null {
|
|
132
|
+
const basePath = path.resolve(fromDir, moduleSpecifier)
|
|
133
|
+
|
|
134
|
+
for (const extension of MODULE_EXTENSIONS) {
|
|
135
|
+
const candidate = `${basePath}${extension}`
|
|
136
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
137
|
+
return candidate
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!fs.existsSync(basePath) || !fs.statSync(basePath).isDirectory()) {
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const extension of MODULE_EXTENSIONS) {
|
|
146
|
+
const indexCandidate = path.join(basePath, `index${extension}`)
|
|
147
|
+
if (fs.existsSync(indexCandidate) && fs.statSync(indexCandidate).isFile()) {
|
|
148
|
+
return indexCandidate
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return null
|
|
153
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Readable, Writable } from 'node:stream'
|
|
2
|
+
|
|
3
|
+
import type * as ClackPrompts from '@clack/prompts'
|
|
4
|
+
|
|
5
|
+
import type { ApplySummary } from './summary'
|
|
6
|
+
|
|
7
|
+
interface CommonClackOptions {
|
|
8
|
+
input?: Readable
|
|
9
|
+
output?: Writable
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function renderApplySummary(summary: ApplySummary, options?: CommonClackOptions): Promise<void> {
|
|
13
|
+
const { outro, log } = await loadClackPrompts()
|
|
14
|
+
|
|
15
|
+
for (const kind of ['created', 'updated', 'deleted'] as const) {
|
|
16
|
+
const paths = summary.changes.filter((change) => change.kind === kind).map((change) => change.path)
|
|
17
|
+
|
|
18
|
+
for (const filePath of paths) {
|
|
19
|
+
log.step(`${capitalize(kind)} ${filePath}`, options)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const warning of summary.warnings ?? []) {
|
|
24
|
+
log.warn(normalizeClackMessage(warning), options)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
outro('Done', options)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function renderError(message: string, options?: CommonClackOptions): Promise<void> {
|
|
31
|
+
const { log } = await loadClackPrompts()
|
|
32
|
+
log.error(normalizeClackMessage(message), options)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function renderWarning(message: string, options?: CommonClackOptions): Promise<void> {
|
|
36
|
+
const { log } = await loadClackPrompts()
|
|
37
|
+
log.warn(normalizeClackMessage(message), options)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function renderCancelled(options?: CommonClackOptions): Promise<void> {
|
|
41
|
+
const { cancel } = await loadClackPrompts()
|
|
42
|
+
cancel('Cancelled.', options)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function renderIntro(options?: CommonClackOptions): Promise<void> {
|
|
46
|
+
const { intro } = await loadClackPrompts()
|
|
47
|
+
intro('Voltra', options)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function normalizeClackMessage(message: string): string {
|
|
51
|
+
return message
|
|
52
|
+
.split('\n')
|
|
53
|
+
.map((line) => line.replace(/^\[voltra\]\s*(Warning:\s*|Error:\s*|Preflight:\s*)?/, ''))
|
|
54
|
+
.join('\n')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function capitalize(value: string): string {
|
|
58
|
+
return value.charAt(0).toUpperCase() + value.slice(1)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function loadClackPrompts() {
|
|
62
|
+
const dynamicImport = new Function('specifier', 'return import(specifier)') as (
|
|
63
|
+
specifier: string
|
|
64
|
+
) => Promise<typeof ClackPrompts>
|
|
65
|
+
|
|
66
|
+
return dynamicImport('@clack/prompts')
|
|
67
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const PREFIX = '[voltra]'
|
|
2
|
+
|
|
3
|
+
export type ReportedChangeKind = 'created' | 'updated' | 'deleted'
|
|
4
|
+
|
|
5
|
+
export interface ReportedChange {
|
|
6
|
+
kind: ReportedChangeKind
|
|
7
|
+
path: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ApplySummary {
|
|
11
|
+
changes: ReportedChange[]
|
|
12
|
+
warnings?: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PreflightIssue {
|
|
16
|
+
message: string
|
|
17
|
+
path?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PreflightFailureReport {
|
|
21
|
+
summary?: string
|
|
22
|
+
issues: PreflightIssue[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class VoltraCliError extends Error {
|
|
26
|
+
readonly code: string
|
|
27
|
+
|
|
28
|
+
constructor(message: string, code = 'VOLTRA_CLI_ERROR') {
|
|
29
|
+
super(message)
|
|
30
|
+
this.name = 'VoltraCliError'
|
|
31
|
+
this.code = code
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class PreflightError extends VoltraCliError {
|
|
36
|
+
readonly report: PreflightFailureReport
|
|
37
|
+
|
|
38
|
+
constructor(report: PreflightFailureReport) {
|
|
39
|
+
super(formatPreflightFailure(report), 'VOLTRA_PREFLIGHT_FAILED')
|
|
40
|
+
this.name = 'PreflightError'
|
|
41
|
+
this.report = report
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function formatApplySummary(summary: ApplySummary): string {
|
|
46
|
+
const lines = [
|
|
47
|
+
`${PREFIX} Apply summary`,
|
|
48
|
+
`${PREFIX} Created: ${countChanges(summary.changes, 'created')}`,
|
|
49
|
+
`${PREFIX} Updated: ${countChanges(summary.changes, 'updated')}`,
|
|
50
|
+
`${PREFIX} Deleted: ${countChanges(summary.changes, 'deleted')}`,
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
for (const kind of ['created', 'updated', 'deleted'] as const) {
|
|
54
|
+
const paths = summary.changes.filter((change) => change.kind === kind).map((change) => change.path)
|
|
55
|
+
|
|
56
|
+
for (const filePath of paths) {
|
|
57
|
+
lines.push(`${PREFIX} ${capitalize(kind)} ${filePath}`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const warning of summary.warnings ?? []) {
|
|
62
|
+
lines.push(formatWarning(warning))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return lines.join('\n')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function formatDirtyWorktreeWarning(changeCount?: number): string {
|
|
69
|
+
if (!changeCount || changeCount <= 0) {
|
|
70
|
+
return `${PREFIX} Warning: git worktree has uncommitted changes.`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const noun = changeCount === 1 ? 'change' : 'changes'
|
|
74
|
+
return `${PREFIX} Warning: git worktree has ${changeCount} uncommitted ${noun}.`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function formatAmbiguousDiscoveryWarning(subject: string, candidates: string[]): string {
|
|
78
|
+
if (candidates.length === 0) {
|
|
79
|
+
return `${PREFIX} Warning: ${subject} is ambiguous.`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return `${PREFIX} Warning: ${subject} is ambiguous. Candidates: ${candidates.join(', ')}`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function formatWarning(message: string): string {
|
|
86
|
+
return `${PREFIX} Warning: ${message}`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function formatError(message: string): string {
|
|
90
|
+
return `${PREFIX} Error: ${message}`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function formatPreflightFailure(report: PreflightFailureReport): string {
|
|
94
|
+
const lines = [
|
|
95
|
+
report.summary ? formatError(report.summary) : formatError('Preflight failed.'),
|
|
96
|
+
...report.issues.map((issue) => formatPreflightIssue(issue)),
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
return lines.join('\n')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function formatPreflightIssue(issue: PreflightIssue): string {
|
|
103
|
+
if (!issue.path) {
|
|
104
|
+
return `${PREFIX} Preflight: ${issue.message}`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return `${PREFIX} Preflight: ${issue.path}: ${issue.message}`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function countChanges(changes: ReportedChange[], kind: ReportedChangeKind): number {
|
|
111
|
+
return changes.filter((change) => change.kind === kind).length
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function capitalize(value: string): string {
|
|
115
|
+
return value.charAt(0).toUpperCase() + value.slice(1)
|
|
116
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { normalizeTrackedStateFiles, normalizeTrackedStateFilesForDiff } from './files'
|
|
2
|
+
|
|
3
|
+
import type { VoltraState } from './load'
|
|
4
|
+
|
|
5
|
+
export interface VoltraStateDiff {
|
|
6
|
+
previousFiles: string[]
|
|
7
|
+
nextFiles: string[]
|
|
8
|
+
staleFiles: string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function diffVoltraState(previousState: VoltraState | undefined, nextFiles: string[]): VoltraStateDiff {
|
|
12
|
+
const previousFiles = normalizeTrackedStateFilesForDiff(previousState?.files)
|
|
13
|
+
const normalizedNextFiles = normalizeTrackedStateFiles(nextFiles, 'Next Voltra state files')
|
|
14
|
+
const nextFileSet = new Set(normalizedNextFiles)
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
previousFiles,
|
|
18
|
+
nextFiles: normalizedNextFiles,
|
|
19
|
+
staleFiles: previousFiles.filter((filePath) => !nextFileSet.has(filePath)),
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { normalizeRelativePath } from '../fs/path'
|
|
4
|
+
import { VoltraCliError } from '../reporting/summary'
|
|
5
|
+
|
|
6
|
+
export function normalizeTrackedStateFiles(files: string[] | unknown[], errorContext: string): string[] {
|
|
7
|
+
if (!Array.isArray(files)) {
|
|
8
|
+
throw new VoltraCliError(`${errorContext} must be an array.`)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const seen = new Set<string>()
|
|
12
|
+
const normalizedFiles: string[] = []
|
|
13
|
+
|
|
14
|
+
for (const filePath of files) {
|
|
15
|
+
if (typeof filePath !== 'string' || !filePath.trim()) {
|
|
16
|
+
throw new VoltraCliError(`${errorContext} must contain only non-empty relative paths.`)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const normalizedFilePath = normalizeTrackedStateFilePath(filePath)
|
|
20
|
+
|
|
21
|
+
if (path.isAbsolute(normalizedFilePath)) {
|
|
22
|
+
throw new VoltraCliError(`${errorContext} must contain only project-relative paths.`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (seen.has(normalizedFilePath)) {
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
seen.add(normalizedFilePath)
|
|
30
|
+
normalizedFiles.push(normalizedFilePath)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return normalizedFiles.sort((left, right) => left.localeCompare(right))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeTrackedStateFilePath(filePath: string): string {
|
|
37
|
+
const normalizedFilePath = normalizeRelativePath(path.posix.normalize(filePath))
|
|
38
|
+
|
|
39
|
+
if (normalizedFilePath === '..' || normalizedFilePath.startsWith('../')) {
|
|
40
|
+
throw new VoltraCliError('Voltra state files must contain only project-relative paths.')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return normalizedFilePath
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function normalizeTrackedStateFilesForDiff(files: string[] | undefined): string[] {
|
|
47
|
+
if (!files || files.length === 0) {
|
|
48
|
+
return []
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return normalizeTrackedStateFiles(files, 'Voltra state files')
|
|
52
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { pathExists, readJsonFile } from '../fs/readWrite'
|
|
4
|
+
import { VoltraCliError } from '../reporting/summary'
|
|
5
|
+
|
|
6
|
+
import { normalizeTrackedStateFiles } from './files'
|
|
7
|
+
|
|
8
|
+
const STATE_SCHEMA_VERSION = 1
|
|
9
|
+
const STATE_DIRECTORY_NAME = '.voltra'
|
|
10
|
+
const STATE_FILE_NAME = 'state.json'
|
|
11
|
+
|
|
12
|
+
export interface VoltraState {
|
|
13
|
+
schemaVersion: 1
|
|
14
|
+
files: string[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface RawVoltraState {
|
|
18
|
+
schemaVersion?: unknown
|
|
19
|
+
files?: unknown
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function loadVoltraState(projectRoot: string): Promise<VoltraState | undefined> {
|
|
23
|
+
const statePath = getVoltraStatePath(projectRoot)
|
|
24
|
+
|
|
25
|
+
if (!(await pathExists(statePath))) {
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let rawState: RawVoltraState
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
rawState = await readJsonFile<RawVoltraState>(statePath)
|
|
33
|
+
} catch (error: unknown) {
|
|
34
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
35
|
+
throw new VoltraCliError(`Failed to read Voltra state at ${statePath}: ${message}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return validateVoltraState(rawState, statePath)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getVoltraStatePath(projectRoot: string): string {
|
|
42
|
+
return path.join(projectRoot, STATE_DIRECTORY_NAME, STATE_FILE_NAME)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function validateVoltraState(rawState: RawVoltraState, statePath: string): VoltraState {
|
|
46
|
+
if (!rawState || typeof rawState !== 'object' || Array.isArray(rawState)) {
|
|
47
|
+
throw new VoltraCliError(`Voltra state at ${statePath} must be a JSON object.`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (rawState.schemaVersion !== STATE_SCHEMA_VERSION) {
|
|
51
|
+
throw new VoltraCliError(
|
|
52
|
+
`Unsupported Voltra state schema at ${statePath}: expected ${STATE_SCHEMA_VERSION}, received ${String(
|
|
53
|
+
rawState.schemaVersion
|
|
54
|
+
)}.`
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!Array.isArray(rawState.files)) {
|
|
59
|
+
throw new VoltraCliError(`Voltra state at ${statePath} must contain a files array.`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
schemaVersion: STATE_SCHEMA_VERSION,
|
|
64
|
+
files: normalizeTrackedStateFiles(rawState.files, `Voltra state at ${statePath} files`),
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { writeJsonFile } from '../fs/readWrite'
|
|
2
|
+
import { VoltraCliError } from '../reporting/summary'
|
|
3
|
+
|
|
4
|
+
import { normalizeTrackedStateFiles } from './files'
|
|
5
|
+
import { getVoltraStatePath } from './load'
|
|
6
|
+
|
|
7
|
+
import type { VoltraState } from './load'
|
|
8
|
+
|
|
9
|
+
const STATE_SCHEMA_VERSION = 1
|
|
10
|
+
|
|
11
|
+
export interface SaveVoltraStateInput {
|
|
12
|
+
files: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function saveVoltraState(projectRoot: string, input: SaveVoltraStateInput): Promise<VoltraState> {
|
|
16
|
+
const state: VoltraState = {
|
|
17
|
+
schemaVersion: STATE_SCHEMA_VERSION,
|
|
18
|
+
files: normalizeTrackedStateFiles(input.files, 'Voltra state files'),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const statePath = getVoltraStatePath(projectRoot)
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await writeJsonFile(statePath, state)
|
|
25
|
+
} catch (error: unknown) {
|
|
26
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
27
|
+
throw new VoltraCliError(`Failed to write Voltra state at ${statePath}: ${message}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return state
|
|
31
|
+
}
|
package/src/xml2js.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
declare module 'xml2js' {
|
|
2
|
+
export interface ParserOptions {
|
|
3
|
+
explicitArray?: boolean
|
|
4
|
+
explicitChildren?: boolean
|
|
5
|
+
preserveChildrenOrder?: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface BuilderOptions {
|
|
9
|
+
headless?: boolean
|
|
10
|
+
renderOpts?: {
|
|
11
|
+
pretty?: boolean
|
|
12
|
+
indent?: string
|
|
13
|
+
newline?: string
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class Builder {
|
|
18
|
+
constructor(options?: BuilderOptions)
|
|
19
|
+
buildObject(rootObj: unknown): string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseStringPromise(value: string, options?: ParserOptions): Promise<unknown>
|
|
23
|
+
}
|