@tanstack/cli 0.0.7 → 0.48.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/dist/bin.js +7 -0
- package/dist/cli.js +481 -0
- package/dist/command-line.js +174 -0
- package/dist/dev-watch.js +290 -0
- package/dist/file-syncer.js +148 -0
- package/dist/index.js +1 -0
- package/dist/mcp/api.js +31 -0
- package/dist/mcp/tools.js +250 -0
- package/dist/mcp/types.js +37 -0
- package/dist/mcp.js +121 -0
- package/dist/options.js +162 -0
- package/dist/types/bin.d.ts +2 -0
- package/dist/types/cli.d.ts +16 -0
- package/dist/types/command-line.d.ts +10 -0
- package/dist/types/dev-watch.d.ts +27 -0
- package/dist/types/file-syncer.d.ts +18 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/mcp/api.d.ts +4 -0
- package/dist/types/mcp/tools.d.ts +2 -0
- package/dist/types/mcp/types.d.ts +217 -0
- package/dist/types/mcp.d.ts +6 -0
- package/dist/types/options.d.ts +8 -0
- package/dist/types/types.d.ts +25 -0
- package/dist/types/ui-environment.d.ts +2 -0
- package/dist/types/ui-prompts.d.ts +12 -0
- package/dist/types/utils.d.ts +8 -0
- package/dist/types.js +1 -0
- package/dist/ui-environment.js +52 -0
- package/dist/ui-prompts.js +244 -0
- package/dist/utils.js +30 -0
- package/package.json +46 -46
- package/src/bin.ts +6 -93
- package/src/cli.ts +692 -0
- package/src/command-line.ts +236 -0
- package/src/dev-watch.ts +430 -0
- package/src/file-syncer.ts +205 -0
- package/src/index.ts +1 -85
- package/src/mcp.ts +190 -0
- package/src/options.ts +260 -0
- package/src/types.ts +27 -0
- package/src/ui-environment.ts +74 -0
- package/src/ui-prompts.ts +322 -0
- package/src/utils.ts +38 -0
- package/tests/command-line.test.ts +304 -0
- package/tests/index.test.ts +9 -0
- package/tests/mcp.test.ts +225 -0
- package/tests/options.test.ts +304 -0
- package/tests/setupVitest.ts +6 -0
- package/tests/ui-environment.test.ts +97 -0
- package/tests/ui-prompts.test.ts +238 -0
- package/tsconfig.json +17 -0
- package/vitest.config.js +7 -0
- package/dist/bin.cjs +0 -761
- package/dist/bin.d.cts +0 -1
- package/dist/bin.d.mts +0 -1
- package/dist/bin.mjs +0 -760
- package/dist/index.cjs +0 -36
- package/dist/index.d.cts +0 -1172
- package/dist/index.d.mts +0 -1172
- package/dist/index.mjs +0 -3
- package/dist/template-CkAkdP8n.mjs +0 -2545
- package/dist/template-Cup47s9h.cjs +0 -2783
- package/src/api/fetch.test.ts +0 -114
- package/src/api/fetch.ts +0 -249
- package/src/cache/index.ts +0 -89
- package/src/commands/create.ts +0 -463
- package/src/commands/mcp.test.ts +0 -152
- package/src/commands/mcp.ts +0 -203
- package/src/engine/compile-with-addons.test.ts +0 -302
- package/src/engine/compile.test.ts +0 -404
- package/src/engine/compile.ts +0 -551
- package/src/engine/config-file.test.ts +0 -118
- package/src/engine/config-file.ts +0 -61
- package/src/engine/custom-addons/integration.ts +0 -323
- package/src/engine/custom-addons/shared.test.ts +0 -98
- package/src/engine/custom-addons/shared.ts +0 -281
- package/src/engine/custom-addons/template.test.ts +0 -288
- package/src/engine/custom-addons/template.ts +0 -124
- package/src/engine/template.test.ts +0 -256
- package/src/engine/template.ts +0 -269
- package/src/engine/types.ts +0 -336
- package/src/parse-gitignore.d.ts +0 -5
- package/src/templates/base.ts +0 -891
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_PACKAGE_MANAGER,
|
|
6
|
+
finalizeAddOns,
|
|
7
|
+
getFrameworkById,
|
|
8
|
+
getPackageManager,
|
|
9
|
+
loadStarter,
|
|
10
|
+
populateAddOnOptionsDefaults,
|
|
11
|
+
} from '@tanstack/create'
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
getCurrentDirectoryName,
|
|
15
|
+
sanitizePackageName,
|
|
16
|
+
validateProjectName,
|
|
17
|
+
} from './utils.js'
|
|
18
|
+
import type { Options } from '@tanstack/create'
|
|
19
|
+
|
|
20
|
+
import type { CliOptions } from './types.js'
|
|
21
|
+
|
|
22
|
+
export async function normalizeOptions(
|
|
23
|
+
cliOptions: CliOptions,
|
|
24
|
+
forcedMode?: string,
|
|
25
|
+
forcedAddOns?: Array<string>,
|
|
26
|
+
opts?: {
|
|
27
|
+
disableNameCheck?: boolean
|
|
28
|
+
forcedDeployment?: string
|
|
29
|
+
},
|
|
30
|
+
): Promise<Options | undefined> {
|
|
31
|
+
let projectName = (cliOptions.projectName ?? '').trim()
|
|
32
|
+
let targetDir: string
|
|
33
|
+
|
|
34
|
+
// Handle "." as project name - use current directory
|
|
35
|
+
if (projectName === '.') {
|
|
36
|
+
projectName = sanitizePackageName(getCurrentDirectoryName())
|
|
37
|
+
targetDir = resolve(process.cwd())
|
|
38
|
+
} else {
|
|
39
|
+
targetDir = resolve(process.cwd(), projectName)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!projectName && !opts?.disableNameCheck) {
|
|
43
|
+
return undefined
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (projectName) {
|
|
47
|
+
const { valid, error } = validateProjectName(projectName)
|
|
48
|
+
if (!valid) {
|
|
49
|
+
console.error(error)
|
|
50
|
+
process.exit(1)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let tailwind = !!cliOptions.tailwind
|
|
55
|
+
|
|
56
|
+
let mode: string =
|
|
57
|
+
forcedMode ||
|
|
58
|
+
(cliOptions.template === 'file-router' ? 'file-router' : 'code-router')
|
|
59
|
+
|
|
60
|
+
const starter = cliOptions.starter
|
|
61
|
+
? await loadStarter(cliOptions.starter)
|
|
62
|
+
: undefined
|
|
63
|
+
|
|
64
|
+
// TODO: Make this declarative
|
|
65
|
+
let typescript =
|
|
66
|
+
cliOptions.template === 'typescript' ||
|
|
67
|
+
cliOptions.template === 'file-router' ||
|
|
68
|
+
cliOptions.framework === 'solid'
|
|
69
|
+
|
|
70
|
+
if (starter) {
|
|
71
|
+
tailwind = starter.tailwind
|
|
72
|
+
typescript = starter.typescript
|
|
73
|
+
cliOptions.framework = starter.framework
|
|
74
|
+
mode = starter.mode
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const framework = getFrameworkById(cliOptions.framework || 'react-cra')!
|
|
78
|
+
|
|
79
|
+
if (
|
|
80
|
+
forcedMode &&
|
|
81
|
+
framework.supportedModes?.[forcedMode]?.forceTypescript !== undefined
|
|
82
|
+
) {
|
|
83
|
+
typescript = true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (cliOptions.framework === 'solid') {
|
|
87
|
+
tailwind = true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function selectAddOns() {
|
|
91
|
+
// Edge case for Windows Powershell
|
|
92
|
+
if (Array.isArray(cliOptions.addOns) && cliOptions.addOns.length === 1) {
|
|
93
|
+
const parseSeparatedArgs = cliOptions.addOns[0].split(' ')
|
|
94
|
+
if (parseSeparatedArgs.length > 1) {
|
|
95
|
+
cliOptions.addOns = parseSeparatedArgs
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (
|
|
100
|
+
Array.isArray(cliOptions.addOns) ||
|
|
101
|
+
starter?.dependsOn ||
|
|
102
|
+
forcedAddOns ||
|
|
103
|
+
cliOptions.toolchain ||
|
|
104
|
+
cliOptions.deployment
|
|
105
|
+
) {
|
|
106
|
+
const selectedAddOns = new Set<string>([
|
|
107
|
+
...(starter?.dependsOn || []),
|
|
108
|
+
...(forcedAddOns || []),
|
|
109
|
+
])
|
|
110
|
+
if (cliOptions.addOns && Array.isArray(cliOptions.addOns)) {
|
|
111
|
+
for (const a of cliOptions.addOns) {
|
|
112
|
+
selectedAddOns.add(a)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (cliOptions.toolchain) {
|
|
116
|
+
selectedAddOns.add(cliOptions.toolchain)
|
|
117
|
+
}
|
|
118
|
+
if (cliOptions.deployment) {
|
|
119
|
+
selectedAddOns.add(cliOptions.deployment)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!cliOptions.deployment && opts?.forcedDeployment) {
|
|
123
|
+
selectedAddOns.add(opts.forcedDeployment)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return await finalizeAddOns(framework, mode, Array.from(selectedAddOns))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return []
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const chosenAddOns = await selectAddOns()
|
|
133
|
+
|
|
134
|
+
if (chosenAddOns.length) {
|
|
135
|
+
typescript = true
|
|
136
|
+
|
|
137
|
+
// Check if any add-on explicitly requires tailwind
|
|
138
|
+
const addOnsRequireTailwind = chosenAddOns.some(
|
|
139
|
+
(addOn) => addOn.tailwind === true,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
// Only set tailwind to true if:
|
|
143
|
+
// 1. An add-on explicitly requires it, OR
|
|
144
|
+
// 2. User explicitly set it via CLI
|
|
145
|
+
if (addOnsRequireTailwind) {
|
|
146
|
+
tailwind = true
|
|
147
|
+
} else if (cliOptions.tailwind === true) {
|
|
148
|
+
tailwind = true
|
|
149
|
+
} else if (cliOptions.tailwind === false) {
|
|
150
|
+
tailwind = false
|
|
151
|
+
}
|
|
152
|
+
// If cliOptions.tailwind is undefined and no add-ons require it,
|
|
153
|
+
// leave tailwind as is (will be prompted in interactive mode)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Handle add-on configuration option
|
|
157
|
+
let addOnOptionsFromCLI = {}
|
|
158
|
+
if (cliOptions.addOnConfig) {
|
|
159
|
+
try {
|
|
160
|
+
addOnOptionsFromCLI = JSON.parse(cliOptions.addOnConfig)
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('Error parsing add-on config:', error)
|
|
163
|
+
process.exit(1)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
projectName: projectName,
|
|
169
|
+
targetDir,
|
|
170
|
+
framework,
|
|
171
|
+
mode,
|
|
172
|
+
typescript,
|
|
173
|
+
tailwind,
|
|
174
|
+
packageManager:
|
|
175
|
+
cliOptions.packageManager ||
|
|
176
|
+
getPackageManager() ||
|
|
177
|
+
DEFAULT_PACKAGE_MANAGER,
|
|
178
|
+
git: !!cliOptions.git,
|
|
179
|
+
install: cliOptions.install,
|
|
180
|
+
chosenAddOns,
|
|
181
|
+
addOnOptions: {
|
|
182
|
+
...populateAddOnOptionsDefaults(chosenAddOns),
|
|
183
|
+
...addOnOptionsFromCLI,
|
|
184
|
+
},
|
|
185
|
+
starter: starter,
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function validateDevWatchOptions(cliOptions: CliOptions): {
|
|
190
|
+
valid: boolean
|
|
191
|
+
error?: string
|
|
192
|
+
} {
|
|
193
|
+
if (!cliOptions.devWatch) {
|
|
194
|
+
return { valid: true }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Validate watch path exists
|
|
198
|
+
const watchPath = resolve(process.cwd(), cliOptions.devWatch)
|
|
199
|
+
if (!fs.existsSync(watchPath)) {
|
|
200
|
+
return {
|
|
201
|
+
valid: false,
|
|
202
|
+
error: `Watch path does not exist: ${watchPath}`,
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Validate it's a directory
|
|
207
|
+
const stats = fs.statSync(watchPath)
|
|
208
|
+
if (!stats.isDirectory()) {
|
|
209
|
+
return {
|
|
210
|
+
valid: false,
|
|
211
|
+
error: `Watch path is not a directory: ${watchPath}`,
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Ensure target directory is specified
|
|
216
|
+
if (!cliOptions.projectName && !cliOptions.targetDir) {
|
|
217
|
+
return {
|
|
218
|
+
valid: false,
|
|
219
|
+
error: 'Project name or target directory is required for dev watch mode',
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Check for framework structure
|
|
224
|
+
const hasAddOns = fs.existsSync(resolve(watchPath, 'add-ons'))
|
|
225
|
+
const hasAssets = fs.existsSync(resolve(watchPath, 'assets'))
|
|
226
|
+
const hasFrameworkJson = fs.existsSync(resolve(watchPath, 'framework.json'))
|
|
227
|
+
|
|
228
|
+
if (!hasAddOns && !hasAssets && !hasFrameworkJson) {
|
|
229
|
+
return {
|
|
230
|
+
valid: false,
|
|
231
|
+
error: `Watch path does not appear to be a valid framework directory: ${watchPath}`,
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return { valid: true }
|
|
236
|
+
}
|
package/src/dev-watch.ts
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import chokidar from 'chokidar'
|
|
5
|
+
import chalk from 'chalk'
|
|
6
|
+
import { temporaryDirectory } from 'tempy'
|
|
7
|
+
import {
|
|
8
|
+
createApp,
|
|
9
|
+
getFrameworkById,
|
|
10
|
+
registerFramework,
|
|
11
|
+
} from '@tanstack/create'
|
|
12
|
+
import { FileSyncer } from './file-syncer.js'
|
|
13
|
+
import { createUIEnvironment } from './ui-environment.js'
|
|
14
|
+
import type {
|
|
15
|
+
Environment,
|
|
16
|
+
Framework,
|
|
17
|
+
FrameworkDefinition,
|
|
18
|
+
Options,
|
|
19
|
+
} from '@tanstack/create'
|
|
20
|
+
import type { FSWatcher } from 'chokidar'
|
|
21
|
+
|
|
22
|
+
export interface DevWatchOptions {
|
|
23
|
+
watchPath: string
|
|
24
|
+
targetDir: string
|
|
25
|
+
framework: Framework
|
|
26
|
+
cliOptions: Options
|
|
27
|
+
packageManager: string
|
|
28
|
+
environment: Environment
|
|
29
|
+
frameworkDefinitionInitializers?: Array<() => FrameworkDefinition>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ChangeEvent {
|
|
33
|
+
type: 'add' | 'change' | 'unlink'
|
|
34
|
+
path: string
|
|
35
|
+
relativePath: string
|
|
36
|
+
timestamp: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class DebounceQueue {
|
|
40
|
+
private timer: NodeJS.Timeout | null = null
|
|
41
|
+
private changes: Set<string> = new Set()
|
|
42
|
+
private callback: (changes: Set<string>) => void
|
|
43
|
+
|
|
44
|
+
constructor(
|
|
45
|
+
callback: (changes: Set<string>) => void,
|
|
46
|
+
private delay: number = 1000,
|
|
47
|
+
) {
|
|
48
|
+
this.callback = callback
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
add(path: string): void {
|
|
52
|
+
this.changes.add(path)
|
|
53
|
+
|
|
54
|
+
if (this.timer) {
|
|
55
|
+
clearTimeout(this.timer)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.timer = setTimeout(() => {
|
|
59
|
+
const currentChanges = new Set(this.changes)
|
|
60
|
+
this.callback(currentChanges)
|
|
61
|
+
this.changes.clear()
|
|
62
|
+
}, this.delay)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
size(): number {
|
|
66
|
+
return this.changes.size
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
clear(): void {
|
|
70
|
+
if (this.timer) {
|
|
71
|
+
clearTimeout(this.timer)
|
|
72
|
+
this.timer = null
|
|
73
|
+
}
|
|
74
|
+
this.changes.clear()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class DevWatchManager {
|
|
79
|
+
private watcher: FSWatcher | null = null
|
|
80
|
+
private debounceQueue: DebounceQueue
|
|
81
|
+
private syncer: FileSyncer
|
|
82
|
+
private tempDir: string | null = null
|
|
83
|
+
private isBuilding = false
|
|
84
|
+
private buildCount = 0
|
|
85
|
+
|
|
86
|
+
constructor(private options: DevWatchOptions) {
|
|
87
|
+
this.syncer = new FileSyncer()
|
|
88
|
+
this.debounceQueue = new DebounceQueue((changes) => this.rebuild(changes))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async start(): Promise<void> {
|
|
92
|
+
// Validate watch path
|
|
93
|
+
if (!fs.existsSync(this.options.watchPath)) {
|
|
94
|
+
throw new Error(`Watch path does not exist: ${this.options.watchPath}`)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Validate target directory exists (should have been created by createApp)
|
|
98
|
+
if (!fs.existsSync(this.options.targetDir)) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Target directory does not exist: ${this.options.targetDir}`,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (this.options.cliOptions.install === false) {
|
|
105
|
+
throw new Error('Cannot use the --no-install flag when using --dev-watch')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Log startup with tree style
|
|
109
|
+
console.log()
|
|
110
|
+
console.log(chalk.bold('dev-watch'))
|
|
111
|
+
this.log.tree('', `watching: ${chalk.cyan(this.options.watchPath)}`)
|
|
112
|
+
this.log.tree('', `target: ${chalk.cyan(this.options.targetDir)}`)
|
|
113
|
+
this.log.tree('', 'ready', true)
|
|
114
|
+
|
|
115
|
+
// Setup signal handlers
|
|
116
|
+
process.on('SIGINT', () => this.cleanup())
|
|
117
|
+
process.on('SIGTERM', () => this.cleanup())
|
|
118
|
+
|
|
119
|
+
// Start watching
|
|
120
|
+
this.startWatcher()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async stop(): Promise<void> {
|
|
124
|
+
console.log()
|
|
125
|
+
this.log.info('Stopping dev watch mode...')
|
|
126
|
+
|
|
127
|
+
if (this.watcher) {
|
|
128
|
+
await this.watcher.close()
|
|
129
|
+
this.watcher = null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.debounceQueue.clear()
|
|
133
|
+
this.cleanup()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private startWatcher(): void {
|
|
137
|
+
const watcherConfig = {
|
|
138
|
+
ignored: [
|
|
139
|
+
'**/node_modules/**',
|
|
140
|
+
'**/.git/**',
|
|
141
|
+
'**/dist/**',
|
|
142
|
+
'**/build/**',
|
|
143
|
+
'**/.DS_Store',
|
|
144
|
+
'**/*.log',
|
|
145
|
+
this.tempDir!,
|
|
146
|
+
],
|
|
147
|
+
persistent: true,
|
|
148
|
+
ignoreInitial: true,
|
|
149
|
+
awaitWriteFinish: {
|
|
150
|
+
stabilityThreshold: 100,
|
|
151
|
+
pollInterval: 100,
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this.watcher = chokidar.watch(this.options.watchPath, watcherConfig)
|
|
156
|
+
|
|
157
|
+
this.watcher.on('add', (filePath) => this.handleChange('add', filePath))
|
|
158
|
+
this.watcher.on('change', (filePath) =>
|
|
159
|
+
this.handleChange('change', filePath),
|
|
160
|
+
)
|
|
161
|
+
this.watcher.on('unlink', (filePath) =>
|
|
162
|
+
this.handleChange('unlink', filePath),
|
|
163
|
+
)
|
|
164
|
+
this.watcher.on('error', (error) =>
|
|
165
|
+
this.log.error(`Watcher error: ${error.message}`),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
this.watcher.on('ready', () => {
|
|
169
|
+
// Already shown in startup, no need to repeat
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private handleChange(_type: ChangeEvent['type'], filePath: string): void {
|
|
174
|
+
const relativePath = path.relative(this.options.watchPath, filePath)
|
|
175
|
+
// Log change only once for the first file in debounce queue
|
|
176
|
+
if (this.debounceQueue.size() === 0) {
|
|
177
|
+
this.log.section('change detected')
|
|
178
|
+
this.log.subsection(`└─ ${relativePath}`)
|
|
179
|
+
} else {
|
|
180
|
+
this.log.subsection(`└─ ${relativePath}`)
|
|
181
|
+
}
|
|
182
|
+
this.debounceQueue.add(filePath)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private async rebuild(changes: Set<string>): Promise<void> {
|
|
186
|
+
if (this.isBuilding) {
|
|
187
|
+
this.log.warning('Build already in progress, skipping...')
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.isBuilding = true
|
|
192
|
+
this.buildCount++
|
|
193
|
+
const buildId = this.buildCount
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
this.log.section(`build #${buildId}`)
|
|
197
|
+
const startTime = Date.now()
|
|
198
|
+
|
|
199
|
+
if (!this.options.frameworkDefinitionInitializers) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
'There must be framework initalizers passed to frameworkDefinitionInitializers to use --dev-watch',
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const refreshedFrameworks =
|
|
206
|
+
this.options.frameworkDefinitionInitializers.map(
|
|
207
|
+
(frameworkInitalizer) => frameworkInitalizer(),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
const refreshedFramework = refreshedFrameworks.find(
|
|
211
|
+
(f) => f.id === this.options.framework.id,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if (!refreshedFramework) {
|
|
215
|
+
throw new Error('Could not identify the framework')
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Update the chosen addons to use the latest code
|
|
219
|
+
const chosenAddonIds = this.options.cliOptions.chosenAddOns.map(
|
|
220
|
+
(m) => m.id,
|
|
221
|
+
)
|
|
222
|
+
const updatedChosenAddons = refreshedFramework.addOns.filter((f) =>
|
|
223
|
+
chosenAddonIds.includes(f.id),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
// Create temp directory for this build using tempy
|
|
227
|
+
this.tempDir = temporaryDirectory()
|
|
228
|
+
|
|
229
|
+
// Register the scanned framework
|
|
230
|
+
registerFramework({
|
|
231
|
+
...refreshedFramework,
|
|
232
|
+
id: `${refreshedFramework.id}-updated`,
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
// Get the registered framework
|
|
236
|
+
const registeredFramework = getFrameworkById(
|
|
237
|
+
`${refreshedFramework.id}-updated`,
|
|
238
|
+
)
|
|
239
|
+
if (!registeredFramework) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`Failed to register framework: ${this.options.framework.id}`,
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Check if package.json was modified
|
|
246
|
+
const packageJsonModified = Array.from(changes).some(
|
|
247
|
+
(filePath) => path.basename(filePath) === 'package.json',
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
const updatedOptions: Options = {
|
|
251
|
+
...this.options.cliOptions,
|
|
252
|
+
chosenAddOns: updatedChosenAddons,
|
|
253
|
+
framework: registeredFramework,
|
|
254
|
+
targetDir: this.tempDir,
|
|
255
|
+
git: false,
|
|
256
|
+
install: packageJsonModified,
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Show package installation indicator if needed
|
|
260
|
+
if (packageJsonModified) {
|
|
261
|
+
this.log.tree(' ', `${chalk.yellow('⟳')} installing packages...`)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Create app in temp directory with silent environment
|
|
265
|
+
const silentEnvironment = createUIEnvironment(
|
|
266
|
+
this.options.environment.appName,
|
|
267
|
+
true,
|
|
268
|
+
)
|
|
269
|
+
await createApp(silentEnvironment, updatedOptions)
|
|
270
|
+
|
|
271
|
+
// Sync files to target directory
|
|
272
|
+
const syncResult = await this.syncer.sync(
|
|
273
|
+
this.tempDir,
|
|
274
|
+
this.options.targetDir,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
// Clean up temp directory after sync is complete
|
|
278
|
+
try {
|
|
279
|
+
await fs.promises.rm(this.tempDir, { recursive: true, force: true })
|
|
280
|
+
} catch (cleanupError) {
|
|
281
|
+
this.log.warning(
|
|
282
|
+
`Failed to clean up temp directory: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`,
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const elapsed = Date.now() - startTime
|
|
287
|
+
|
|
288
|
+
// Build tree-style summary
|
|
289
|
+
this.log.tree(' ', `duration: ${chalk.cyan(elapsed + 'ms')}`)
|
|
290
|
+
|
|
291
|
+
if (packageJsonModified) {
|
|
292
|
+
this.log.tree(' ', `packages: ${chalk.green('✓ installed')}`)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Always show the last item in tree without checking for files to show
|
|
296
|
+
const noMoreTreeItems =
|
|
297
|
+
syncResult.updated.length === 0 &&
|
|
298
|
+
syncResult.created.length === 0 &&
|
|
299
|
+
syncResult.errors.length === 0
|
|
300
|
+
|
|
301
|
+
if (syncResult.updated.length > 0) {
|
|
302
|
+
this.log.tree(
|
|
303
|
+
' ',
|
|
304
|
+
`updated: ${chalk.green(syncResult.updated.length + ' file' + (syncResult.updated.length > 1 ? 's' : ''))}`,
|
|
305
|
+
syncResult.created.length === 0 && syncResult.errors.length === 0,
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
if (syncResult.created.length > 0) {
|
|
309
|
+
this.log.tree(
|
|
310
|
+
' ',
|
|
311
|
+
`created: ${chalk.green(syncResult.created.length + ' file' + (syncResult.created.length > 1 ? 's' : ''))}`,
|
|
312
|
+
syncResult.errors.length === 0,
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
if (syncResult.errors.length > 0) {
|
|
316
|
+
this.log.tree(
|
|
317
|
+
' ',
|
|
318
|
+
`failed: ${chalk.red(syncResult.errors.length + ' file' + (syncResult.errors.length > 1 ? 's' : ''))}`,
|
|
319
|
+
true,
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// If nothing changed, show that
|
|
324
|
+
if (noMoreTreeItems) {
|
|
325
|
+
this.log.tree(' ', `no changes`, true)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Always show changed files with diffs
|
|
329
|
+
if (syncResult.updated.length > 0) {
|
|
330
|
+
syncResult.updated.forEach((update, index) => {
|
|
331
|
+
const isLastFile =
|
|
332
|
+
index === syncResult.updated.length - 1 &&
|
|
333
|
+
syncResult.created.length === 0
|
|
334
|
+
|
|
335
|
+
// For files with diffs, always use ├─
|
|
336
|
+
const fileIsLast = isLastFile && !update.diff
|
|
337
|
+
this.log.treeItem(' ', update.path, fileIsLast)
|
|
338
|
+
|
|
339
|
+
// Always show diff if available
|
|
340
|
+
if (update.diff) {
|
|
341
|
+
const diffLines = update.diff.split('\n')
|
|
342
|
+
const relevantLines = diffLines
|
|
343
|
+
.slice(4)
|
|
344
|
+
.filter(
|
|
345
|
+
(line) =>
|
|
346
|
+
line.startsWith('+') ||
|
|
347
|
+
line.startsWith('-') ||
|
|
348
|
+
line.startsWith('@'),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if (relevantLines.length > 0) {
|
|
352
|
+
// Always use │ to continue the tree line through the diff
|
|
353
|
+
const prefix = ' │ '
|
|
354
|
+
relevantLines.forEach((line) => {
|
|
355
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
356
|
+
console.log(chalk.gray(prefix) + ' ' + chalk.green(line))
|
|
357
|
+
} else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
358
|
+
console.log(chalk.gray(prefix) + ' ' + chalk.red(line))
|
|
359
|
+
} else if (line.startsWith('@')) {
|
|
360
|
+
console.log(chalk.gray(prefix) + ' ' + chalk.cyan(line))
|
|
361
|
+
}
|
|
362
|
+
})
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
})
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Show created files
|
|
369
|
+
if (syncResult.created.length > 0) {
|
|
370
|
+
syncResult.created.forEach((file, index) => {
|
|
371
|
+
const isLast = index === syncResult.created.length - 1
|
|
372
|
+
this.log.treeItem(' ', `${chalk.green('+')} ${file}`, isLast)
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Always show errors
|
|
377
|
+
if (syncResult.errors.length > 0) {
|
|
378
|
+
console.log() // Add spacing
|
|
379
|
+
syncResult.errors.forEach((err, index) => {
|
|
380
|
+
this.log.tree(
|
|
381
|
+
' ',
|
|
382
|
+
`${chalk.red('error:')} ${err}`,
|
|
383
|
+
index === syncResult.errors.length - 1,
|
|
384
|
+
)
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
} catch (error) {
|
|
388
|
+
this.log.error(
|
|
389
|
+
`Build #${buildId} failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
390
|
+
)
|
|
391
|
+
} finally {
|
|
392
|
+
this.isBuilding = false
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private cleanup(): void {
|
|
397
|
+
console.log()
|
|
398
|
+
console.log('Cleaning up...')
|
|
399
|
+
|
|
400
|
+
// Clean up temp directory
|
|
401
|
+
if (this.tempDir && fs.existsSync(this.tempDir)) {
|
|
402
|
+
try {
|
|
403
|
+
fs.rmSync(this.tempDir, { recursive: true, force: true })
|
|
404
|
+
} catch (error) {
|
|
405
|
+
this.log.error(
|
|
406
|
+
`Failed to clean up temp directory: ${error instanceof Error ? error.message : String(error)}`,
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
process.exit(0)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private log = {
|
|
415
|
+
tree: (prefix: string, msg: string, isLast = false) => {
|
|
416
|
+
const connector = isLast ? '└─' : '├─'
|
|
417
|
+
console.log(chalk.gray(prefix + connector) + ' ' + msg)
|
|
418
|
+
},
|
|
419
|
+
treeItem: (prefix: string, msg: string, isLast = false) => {
|
|
420
|
+
const connector = isLast ? '└─' : '├─'
|
|
421
|
+
console.log(chalk.gray(prefix + ' ' + connector) + ' ' + msg)
|
|
422
|
+
},
|
|
423
|
+
info: (msg: string) => console.log(msg),
|
|
424
|
+
error: (msg: string) => console.error(chalk.red('✗') + ' ' + msg),
|
|
425
|
+
success: (msg: string) => console.log(chalk.green('✓') + ' ' + msg),
|
|
426
|
+
warning: (msg: string) => console.log(chalk.yellow('⚠') + ' ' + msg),
|
|
427
|
+
section: (title: string) => console.log('\n' + chalk.bold('▸ ' + title)),
|
|
428
|
+
subsection: (msg: string) => console.log(' ' + msg),
|
|
429
|
+
}
|
|
430
|
+
}
|