sandstone-cli 2.0.8 → 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/lib/create.js +8568 -17
- package/lib/index.js +41151 -80
- package/package.json +3 -2
- package/scripts/version.ts +4 -0
- package/src/commands/create.ts +2 -1
- package/src/commands/watch.ts +16 -5
- package/src/create.ts +12 -13
- package/src/index.ts +38 -44
- package/src/shared.ts +65 -23
- package/src/stubs/react-devtools-core.js +1 -0
- package/src/ui/WatchUI.tsx +2 -0
- package/src/version.ts +1 -0
- package/tsconfig.json +5 -3
- package/lib/commands/build.d.ts +0 -34
- package/lib/commands/build.js +0 -664
- package/lib/commands/create.d.ts +0 -8
- package/lib/commands/create.js +0 -162
- package/lib/commands/dependency.d.ts +0 -4
- package/lib/commands/dependency.js +0 -246
- package/lib/commands/index.d.ts +0 -4
- package/lib/commands/index.js +0 -4
- package/lib/commands/watch.d.ts +0 -6
- package/lib/commands/watch.js +0 -279
- package/lib/create.d.ts +0 -2
- package/lib/index.d.ts +0 -2
- package/lib/shared.d.ts +0 -1
- package/lib/shared.js +0 -20
- package/lib/ui/WatchUI.d.ts +0 -9
- package/lib/ui/WatchUI.js +0 -183
- package/lib/ui/logger.d.ts +0 -20
- package/lib/ui/logger.js +0 -189
- package/lib/ui/types.d.ts +0 -26
- package/lib/ui/types.js +0 -1
- package/lib/utils.d.ts +0 -34
- package/lib/utils.js +0 -105
package/lib/commands/watch.js
DELETED
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { subscribe } from '@parcel/watcher';
|
|
3
|
-
import React from 'react';
|
|
4
|
-
import { render } from 'ink';
|
|
5
|
-
import { normalizePath } from '../utils.js';
|
|
6
|
-
import { _buildCommand, enableConsoleCapture, disableConsoleCapture } from './build.js';
|
|
7
|
-
import { WatchUI, getWatchUIAPI } from '../ui/WatchUI.js';
|
|
8
|
-
import { initLogger, log, logError, setLiveLogCallback } from '../ui/logger.js';
|
|
9
|
-
import { hot } from '@sandstone-mc/hot-hook';
|
|
10
|
-
import fs from 'fs-extra';
|
|
11
|
-
import { join } from 'node:path';
|
|
12
|
-
export async function watchCommand(opts) {
|
|
13
|
-
let alreadyBuilding = false;
|
|
14
|
-
let needRebuild = false;
|
|
15
|
-
let pendingChanges = [];
|
|
16
|
-
let buildContext;
|
|
17
|
-
let hotInitialized = false;
|
|
18
|
-
let lastBuildFailed = false;
|
|
19
|
-
const folder = opts.library ? join(opts.path, 'test') : opts.path;
|
|
20
|
-
let subscription;
|
|
21
|
-
// Initialize logger
|
|
22
|
-
initLogger(folder);
|
|
23
|
-
// Set up live log callback to send to UI
|
|
24
|
-
setLiveLogCallback((level, args) => {
|
|
25
|
-
getWatchUIAPI()?.setLiveLog(level, args);
|
|
26
|
-
});
|
|
27
|
-
// Render Ink UI
|
|
28
|
-
let unmountInk;
|
|
29
|
-
const handleManualRebuild = () => {
|
|
30
|
-
if (pendingChanges.length > 0 && !alreadyBuilding) {
|
|
31
|
-
log('Manual rebuild triggered');
|
|
32
|
-
onFilesChange(pendingChanges);
|
|
33
|
-
pendingChanges = [];
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
const { unmount } = render(React.createElement(WatchUI, {
|
|
37
|
-
manual: opts.manual ?? false,
|
|
38
|
-
onManualRebuild: handleManualRebuild,
|
|
39
|
-
// Since this isn't SIGINT, its fine that we don't await this
|
|
40
|
-
exit: () => exit(subscription, unmountInk)
|
|
41
|
-
}), { patchConsole: false });
|
|
42
|
-
unmountInk = unmount;
|
|
43
|
-
async function onFilesChange(changes) {
|
|
44
|
-
// Synchronous check-and-set to prevent race conditions
|
|
45
|
-
if (alreadyBuilding) {
|
|
46
|
-
needRebuild = true;
|
|
47
|
-
// Accumulate changes for the next build
|
|
48
|
-
for (const change of changes) {
|
|
49
|
-
if (!pendingChanges.some(c => c.path === change.path)) {
|
|
50
|
-
pendingChanges.push(change);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
alreadyBuilding = true;
|
|
56
|
-
const api = getWatchUIAPI();
|
|
57
|
-
api?.setStatus('building');
|
|
58
|
-
api?.setChangedFiles(changes);
|
|
59
|
-
log('Building...', changes.map(c => c.path).join(', '));
|
|
60
|
-
const libChanges = opts.library && Object.hasOwn(globalThis, 'Bun') ? changes.filter((change) => !change.path.includes('test/')) : [];
|
|
61
|
-
if (libChanges.length !== 0) {
|
|
62
|
-
/* @ts-ignore */
|
|
63
|
-
const CLI = Bun.spawn(['bun', 'dev:build'], {
|
|
64
|
-
windowsHide: true,
|
|
65
|
-
windowsVerbatimArguments: true,
|
|
66
|
-
stdout: 'ignore',
|
|
67
|
-
stderr: 'ignore',
|
|
68
|
-
});
|
|
69
|
-
await CLI.exited;
|
|
70
|
-
}
|
|
71
|
-
// Initialize hot-hook only once on the first build
|
|
72
|
-
if (!hotInitialized) {
|
|
73
|
-
await hot.init({
|
|
74
|
-
root: join(folder, JSON.parse(await fs.readFile(join(folder, 'package.json'), 'utf-8'))['module']),
|
|
75
|
-
// Ensure sandstone remains a singleton so CLI and user code share the same pack instance
|
|
76
|
-
globalSingletons: ['**/node_modules/sandstone/**', '**/sandstone/dist/**'],
|
|
77
|
-
// Disable hot-hook's internal watcher - we use parcel watcher and notify hot-hook
|
|
78
|
-
watch: false,
|
|
79
|
-
});
|
|
80
|
-
hotInitialized = true;
|
|
81
|
-
}
|
|
82
|
-
if (Object.hasOwn(globalThis, 'Bun') && changes.length > 0) {
|
|
83
|
-
// Bun ignores query params for module caching and doesn't support MessagePort
|
|
84
|
-
// in register(), so hot-hook's invalidation mechanism is non-functional.
|
|
85
|
-
// Instead, clear Bun's module cache for project source files before re-importing.
|
|
86
|
-
const resolvedFolder = normalizePath(await fs.realpath(folder));
|
|
87
|
-
const resolvedRoot = opts.library ? normalizePath(await fs.realpath(opts.path)) : resolvedFolder;
|
|
88
|
-
let clearedCount = 0;
|
|
89
|
-
for (const key of Object.keys(require.cache)) {
|
|
90
|
-
const normalizedKey = normalizePath(key);
|
|
91
|
-
// Only clear modules within the project
|
|
92
|
-
if (!normalizedKey.startsWith(resolvedFolder) && !normalizedKey.startsWith(resolvedRoot))
|
|
93
|
-
continue;
|
|
94
|
-
// Keep sandstone singleton cached so CLI and user code share the same pack instance
|
|
95
|
-
if (normalizedKey.includes('/node_modules/sandstone/'))
|
|
96
|
-
continue;
|
|
97
|
-
delete require.cache[key];
|
|
98
|
-
clearedCount++;
|
|
99
|
-
}
|
|
100
|
-
// If recovering from a failed build but no modules were in cache, Bun had a parse error
|
|
101
|
-
// and won't be able to reimport. Exit and ask user to restart.
|
|
102
|
-
if (lastBuildFailed && clearedCount === 0) {
|
|
103
|
-
getWatchUIAPI()?.setStatus('error', 'Parse error - restart required');
|
|
104
|
-
unmountInk?.();
|
|
105
|
-
process.stderr.write('\n\x1b[33mBun encountered a parse error and cannot recover. Please restart the watch command.\x1b[0m\n\n');
|
|
106
|
-
process.exit(1);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
// Node.js path: use hot-hook's message port invalidation
|
|
111
|
-
for (const change of changes) {
|
|
112
|
-
hot.notifyFileChange(change.path);
|
|
113
|
-
}
|
|
114
|
-
if (libChanges.length !== 0) {
|
|
115
|
-
const libModuleFiles = await fs.readdir(join(opts.path, 'lib'), { recursive: true });
|
|
116
|
-
for (const file of libModuleFiles) {
|
|
117
|
-
hot.notifyFileChange(join(opts.path, 'lib', file));
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
// Small delay to let the loader process the invalidations
|
|
121
|
-
if (changes.length > 0) {
|
|
122
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
// Replace global console during build to capture user console.log without messing up Ink UI
|
|
126
|
-
enableConsoleCapture();
|
|
127
|
-
let result;
|
|
128
|
-
try {
|
|
129
|
-
result = await _buildCommand(opts, folder, buildContext, true);
|
|
130
|
-
}
|
|
131
|
-
finally {
|
|
132
|
-
disableConsoleCapture();
|
|
133
|
-
}
|
|
134
|
-
// Store context for subsequent builds
|
|
135
|
-
if (result.success && result.sandstoneConfig !== undefined) {
|
|
136
|
-
buildContext = {
|
|
137
|
-
sandstoneConfig: result.sandstoneConfig,
|
|
138
|
-
sandstonePack: result.sandstonePack,
|
|
139
|
-
resetSandstonePack: result.resetSandstonePack,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
api?.setBuildResult(result);
|
|
143
|
-
if (result.success) {
|
|
144
|
-
log(`Build successful: ${result.resourceCounts.functions} functions, ${result.resourceCounts.other} others`);
|
|
145
|
-
lastBuildFailed = false;
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
logError(result.error);
|
|
149
|
-
lastBuildFailed = true;
|
|
150
|
-
}
|
|
151
|
-
alreadyBuilding = false;
|
|
152
|
-
if (needRebuild) {
|
|
153
|
-
needRebuild = false;
|
|
154
|
-
// Use accumulated pending changes, then clear them
|
|
155
|
-
const nextChanges = [...pendingChanges];
|
|
156
|
-
pendingChanges = [];
|
|
157
|
-
await onFilesChange(nextChanges);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
let restartTimeout = null;
|
|
161
|
-
let debouncedChanges = []; // Accumulate changes during debounce period
|
|
162
|
-
let debounceScheduled = false; // Synchronous flag to prevent multiple timeouts
|
|
163
|
-
function restart() {
|
|
164
|
-
log('Restarting watch process...');
|
|
165
|
-
getWatchUIAPI()?.setStatus('restarting');
|
|
166
|
-
const [runtime, ...args] = process.argv;
|
|
167
|
-
const child = spawn(runtime, args, {
|
|
168
|
-
stdio: 'inherit',
|
|
169
|
-
detached: true,
|
|
170
|
-
});
|
|
171
|
-
child.unref();
|
|
172
|
-
unmountInk?.();
|
|
173
|
-
process.exit(0);
|
|
174
|
-
}
|
|
175
|
-
const handleEvents = (events) => {
|
|
176
|
-
// Whether changes require a full process restart
|
|
177
|
-
let needsRestart = false;
|
|
178
|
-
// Filter out irrelevant events and categorize
|
|
179
|
-
const trackedChanges = [];
|
|
180
|
-
for (const e of events) {
|
|
181
|
-
const eventPath = normalizePath(e.path);
|
|
182
|
-
const lockFile = eventPath.endsWith('.lock') ||
|
|
183
|
-
eventPath.endsWith('-lock.yml') ||
|
|
184
|
-
eventPath.endsWith('-lock.json');
|
|
185
|
-
if (lockFile ||
|
|
186
|
-
eventPath.includes('node_modules/') ||
|
|
187
|
-
eventPath.endsWith('sandstone.config.ts')) {
|
|
188
|
-
needsRestart = true;
|
|
189
|
-
}
|
|
190
|
-
const inSrc = eventPath.includes('src/');
|
|
191
|
-
const inResources = eventPath.includes('resources/');
|
|
192
|
-
const endsJs = eventPath.endsWith('.js');
|
|
193
|
-
const endsJson = eventPath.endsWith('.json');
|
|
194
|
-
const endsTs = eventPath.endsWith('.ts');
|
|
195
|
-
if (inSrc || inResources || endsJs || endsJson || endsTs) {
|
|
196
|
-
trackedChanges.push({
|
|
197
|
-
path: eventPath,
|
|
198
|
-
category: categorizeChange(eventPath),
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
if (trackedChanges.length === 0 && !needsRestart) {
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
if (needsRestart) {
|
|
206
|
-
if (restartTimeout) {
|
|
207
|
-
clearTimeout(restartTimeout);
|
|
208
|
-
}
|
|
209
|
-
// Debounce restart to allow package manager to finish
|
|
210
|
-
restartTimeout = setTimeout(restart, 500);
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
// Accumulate changes, deduplicating by path
|
|
214
|
-
for (const change of trackedChanges) {
|
|
215
|
-
if (!debouncedChanges.some(c => c.path === change.path)) {
|
|
216
|
-
debouncedChanges.push(change);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
// Use a synchronous flag to ensure only one timeout is scheduled
|
|
220
|
-
// This prevents race conditions when parcel watcher fires multiple callbacks rapidly
|
|
221
|
-
if (debounceScheduled)
|
|
222
|
-
return;
|
|
223
|
-
debounceScheduled = true;
|
|
224
|
-
setTimeout(() => {
|
|
225
|
-
debounceScheduled = false;
|
|
226
|
-
const changesToProcess = [...debouncedChanges];
|
|
227
|
-
debouncedChanges = []; // Clear for next batch
|
|
228
|
-
if (changesToProcess.length === 0) {
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
if (opts.manual) {
|
|
232
|
-
// In manual mode, accumulate changes and wait for user input
|
|
233
|
-
pendingChanges = [...pendingChanges, ...changesToProcess];
|
|
234
|
-
getWatchUIAPI()?.setStatus('pending');
|
|
235
|
-
getWatchUIAPI()?.setChangedFiles(pendingChanges);
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
// Auto mode - rebuild immediately
|
|
239
|
-
// onFilesChange handles the "already building" case internally
|
|
240
|
-
onFilesChange(changesToProcess);
|
|
241
|
-
}
|
|
242
|
-
}, 200);
|
|
243
|
-
};
|
|
244
|
-
log('Watch started');
|
|
245
|
-
// Initial build
|
|
246
|
-
await onFilesChange([]);
|
|
247
|
-
subscription = await subscribe(opts.path, (err, events) => {
|
|
248
|
-
if (err) {
|
|
249
|
-
logError(err);
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
handleEvents(events);
|
|
253
|
-
}, {
|
|
254
|
-
ignore: ['**/.git/**/*', '**/.sandstone/**/*', '**/resources/cache/**/*', '**/*tmp*', 'lib/**/*'],
|
|
255
|
-
});
|
|
256
|
-
// Handle cleanup on exit
|
|
257
|
-
process.on('SIGINT', async () => await exit(subscription, unmountInk));
|
|
258
|
-
}
|
|
259
|
-
async function exit(subscription, unmountInk) {
|
|
260
|
-
log('Watch stopped');
|
|
261
|
-
unmountInk?.();
|
|
262
|
-
await subscription.unsubscribe();
|
|
263
|
-
process.exit(0);
|
|
264
|
-
}
|
|
265
|
-
function categorizeChange(eventPath) {
|
|
266
|
-
if (eventPath.includes('src/'))
|
|
267
|
-
return 'src';
|
|
268
|
-
if (eventPath.includes('resources/'))
|
|
269
|
-
return 'resources';
|
|
270
|
-
if (eventPath.endsWith('sandstone.config.ts'))
|
|
271
|
-
return 'config';
|
|
272
|
-
if (eventPath.endsWith('.lock') ||
|
|
273
|
-
eventPath.endsWith('-lock.yml') ||
|
|
274
|
-
eventPath.endsWith('-lock.json') ||
|
|
275
|
-
eventPath.includes('node_modules/')) {
|
|
276
|
-
return 'dependencies';
|
|
277
|
-
}
|
|
278
|
-
return 'other';
|
|
279
|
-
}
|
package/lib/create.d.ts
DELETED
package/lib/index.d.ts
DELETED
package/lib/shared.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const BuildDeclares: Record<string, [string, string, RegExp, boolean]>;
|
package/lib/shared.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
export const BuildDeclares = {
|
|
2
|
-
// Flags
|
|
3
|
-
dry: ['-d, --dry', 'Do not save the pack. Mostly useful with `verbose`.'],
|
|
4
|
-
verbose: ['-f, --verbose', 'Fully log all resulting resources: functions, advancements...'],
|
|
5
|
-
root: ['-r, --root', 'Save the pack & resource pack in the .minecraft/datapacks & .minecraft/resource_packs folders. Override the value specified in the configuration file.'],
|
|
6
|
-
fullTrace: ['-t, --full-trace', 'Show the full stack trace on errors.'],
|
|
7
|
-
strictErrors: ['-s, --strict-errors', 'Stop pack compilation on type errors.'],
|
|
8
|
-
production: ['-p, --production', 'Runs Sandstone in production mode. This sets process.env.SANDSTONE_ENV to "production".'],
|
|
9
|
-
// Values
|
|
10
|
-
path: ['--path <path>', 'Path of the folder containing your sandstone workspace.', './'],
|
|
11
|
-
name: ['-n, --name <name>', 'Name of the datapack. Override the value specified in the configuration file.'],
|
|
12
|
-
namespace: ['-ns, --namespace <namespace>', 'The default namespace. Override the value specified in the configuration file.'],
|
|
13
|
-
world: ['-w, --world <name>', 'The name of the world to save the packs in. Override the value specified in the configuration file.'],
|
|
14
|
-
clientPath: ['-c, --client-path <path>', 'Path of the client folder. Override the value specified in the configuration file.'],
|
|
15
|
-
serverPath: ['--server-path <path>', 'Path of the server folder. Override the value specified in the configuration file.'],
|
|
16
|
-
// TODO: ssh
|
|
17
|
-
enableSymlinks: ['--enable-symlinks', 'Force enable/disable symlinks. Defaults to false. Useful if you want to enable symlinks on Windows.'],
|
|
18
|
-
manual: ['-m, --manual', 'Manual reload mode - press r or Enter to rebuild after changes.'],
|
|
19
|
-
library: ['-l, --library', 'Library mode - watches a library workspace based on the library project template.'],
|
|
20
|
-
}; // Haha TypeScript funny
|
package/lib/ui/WatchUI.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { WatchUIAPI } from './types.js';
|
|
2
|
-
interface WatchUIProps {
|
|
3
|
-
manual: boolean;
|
|
4
|
-
onManualRebuild?: () => void;
|
|
5
|
-
exit?: () => void;
|
|
6
|
-
}
|
|
7
|
-
export declare function WatchUI({ manual, onManualRebuild, exit }: WatchUIProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
-
export declare function getWatchUIAPI(): WatchUIAPI | undefined;
|
|
9
|
-
export {};
|
package/lib/ui/WatchUI.js
DELETED
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useCallback, useEffect } from 'react';
|
|
3
|
-
import { Box, Text, useInput } from 'ink';
|
|
4
|
-
import Spinner from 'ink-spinner';
|
|
5
|
-
import { format } from 'util';
|
|
6
|
-
import { drainLiveLogBuffer } from './logger.js';
|
|
7
|
-
const CONTENT_LINES = 8;
|
|
8
|
-
function formatChangedFiles(files) {
|
|
9
|
-
if (files.length === 0)
|
|
10
|
-
return 'No recent changes';
|
|
11
|
-
const paths = files.map(f => f.path.split(/[/\\]/).pop() || f.path);
|
|
12
|
-
if (paths.length <= 3)
|
|
13
|
-
return paths.join(', ');
|
|
14
|
-
return `${paths.slice(0, 3).join(', ')} +${paths.length - 3} more`;
|
|
15
|
-
}
|
|
16
|
-
function groupByCategory(files) {
|
|
17
|
-
const groups = {
|
|
18
|
-
src: [],
|
|
19
|
-
resources: [],
|
|
20
|
-
config: [],
|
|
21
|
-
dependencies: [],
|
|
22
|
-
other: [],
|
|
23
|
-
};
|
|
24
|
-
for (const file of files) {
|
|
25
|
-
const name = file.path.split(/[/\\]/).pop() || file.path;
|
|
26
|
-
groups[file.category].push(name);
|
|
27
|
-
}
|
|
28
|
-
return groups;
|
|
29
|
-
}
|
|
30
|
-
const categoryLabels = {
|
|
31
|
-
src: 'src',
|
|
32
|
-
resources: 'resources',
|
|
33
|
-
config: 'config',
|
|
34
|
-
dependencies: 'dependencies',
|
|
35
|
-
other: 'other',
|
|
36
|
-
};
|
|
37
|
-
function ContentDisplay({ mode, logLines, errorText, changes, scrollOffset }) {
|
|
38
|
-
let contentData = [];
|
|
39
|
-
if (mode === 'error' && errorText) {
|
|
40
|
-
contentData = errorText.split('\n').map(line => ({ text: line || ' ', color: 'red' }));
|
|
41
|
-
}
|
|
42
|
-
else if (mode === 'changes') {
|
|
43
|
-
const groups = groupByCategory(changes);
|
|
44
|
-
const nonEmpty = Object.entries(groups).filter(([, files]) => files.length > 0);
|
|
45
|
-
contentData.push({ text: 'Changes by category:', color: undefined });
|
|
46
|
-
for (const [category, files] of nonEmpty.slice(0, 4)) {
|
|
47
|
-
const fileList = files.slice(0, 3).join(', ') + (files.length > 3 ? ` +${files.length - 3} more` : '');
|
|
48
|
-
const suffix = category === 'dependencies' ? ' (restart required)' : '';
|
|
49
|
-
contentData.push({ text: ` ${categoryLabels[category]}: ${fileList}${suffix}`, color: 'cyan' });
|
|
50
|
-
}
|
|
51
|
-
if (nonEmpty.length === 0) {
|
|
52
|
-
contentData.push({ text: ' No changes tracked', color: 'gray' });
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
contentData = logLines.map(line => ({ text: line }));
|
|
57
|
-
}
|
|
58
|
-
const totalLines = contentData.length;
|
|
59
|
-
const hasMore = totalLines > CONTENT_LINES;
|
|
60
|
-
let visibleLines;
|
|
61
|
-
let scrollInfo = '';
|
|
62
|
-
if (mode === 'logs') {
|
|
63
|
-
const start = Math.max(0, totalLines - scrollOffset - CONTENT_LINES);
|
|
64
|
-
const end = Math.max(0, totalLines - scrollOffset);
|
|
65
|
-
visibleLines = contentData.slice(start, end);
|
|
66
|
-
if (hasMore) {
|
|
67
|
-
const canUp = scrollOffset < totalLines - CONTENT_LINES;
|
|
68
|
-
const canDown = scrollOffset > 0;
|
|
69
|
-
scrollInfo = `${canUp ? '▲' : ''}${canDown ? '▼' : ''} (${start + 1}-${end}/${totalLines})`;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
visibleLines = contentData.slice(scrollOffset, scrollOffset + CONTENT_LINES);
|
|
74
|
-
if (hasMore) {
|
|
75
|
-
const canUp = scrollOffset > 0;
|
|
76
|
-
const canDown = scrollOffset + CONTENT_LINES < totalLines;
|
|
77
|
-
scrollInfo = `${canUp ? '▲' : ''}${canDown ? '▼' : ''} (${scrollOffset + 1}-${scrollOffset + visibleLines.length}/${totalLines})`;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
const padding = CONTENT_LINES - visibleLines.length;
|
|
81
|
-
const paddingBefore = mode === 'logs' ? padding : 0;
|
|
82
|
-
const paddingAfter = mode === 'logs' ? 0 : padding;
|
|
83
|
-
return (_jsxs(_Fragment, { children: [Array.from({ length: paddingBefore }).map((_, i) => (_jsx(Text, { children: " " }, `pad-before-${i}`))), visibleLines.map((line, i) => (_jsx(Text, { color: line.color, children: line.text }, `content-${i}`))), Array.from({ length: paddingAfter }).map((_, i) => (_jsx(Text, { children: " " }, `pad-after-${i}`))), _jsx(Text, { color: "gray", children: scrollInfo || ' ' })] }));
|
|
84
|
-
}
|
|
85
|
-
export function WatchUI({ manual, onManualRebuild, exit }) {
|
|
86
|
-
const [status, setStatusState] = useState(manual ? 'pending' : 'watching');
|
|
87
|
-
const [reason, setReason] = useState();
|
|
88
|
-
const [changedFiles, setChangedFilesState] = useState([]);
|
|
89
|
-
const [buildResult, setBuildResultState] = useState(null);
|
|
90
|
-
const [logLines, setLogLinesState] = useState([]);
|
|
91
|
-
const [scrollOffset, setScrollOffset] = useState(0);
|
|
92
|
-
const isError = status === 'error' && buildResult?.error;
|
|
93
|
-
const isManualPending = manual && status === 'pending' && changedFiles.length > 0;
|
|
94
|
-
const contentMode = isError ? 'error' : isManualPending ? 'changes' : 'logs';
|
|
95
|
-
useEffect(() => {
|
|
96
|
-
setScrollOffset(0);
|
|
97
|
-
}, [contentMode, buildResult?.error]);
|
|
98
|
-
const setStatus = useCallback((newStatus, newReason) => {
|
|
99
|
-
setStatusState(newStatus);
|
|
100
|
-
setReason(newReason);
|
|
101
|
-
}, []);
|
|
102
|
-
const setChangedFiles = useCallback((files) => {
|
|
103
|
-
setChangedFilesState(files);
|
|
104
|
-
}, []);
|
|
105
|
-
const setBuildResult = useCallback((result) => {
|
|
106
|
-
setBuildResultState(result);
|
|
107
|
-
if (result.success) {
|
|
108
|
-
setStatusState(manual ? 'pending' : 'watching');
|
|
109
|
-
}
|
|
110
|
-
else {
|
|
111
|
-
setStatusState('error');
|
|
112
|
-
}
|
|
113
|
-
}, [manual]);
|
|
114
|
-
const setLiveLog = useCallback((level, args) => {
|
|
115
|
-
const formatted = format(...args).split('\n');
|
|
116
|
-
setLogLinesState((prev) => {
|
|
117
|
-
const newLines = [...prev];
|
|
118
|
-
newLines.push(`> ${level !== false ? `[${level}] ` : ''}${formatted[0]}`, ...formatted.slice(1).map((line) => `> ${line}`));
|
|
119
|
-
return newLines;
|
|
120
|
-
});
|
|
121
|
-
}, []);
|
|
122
|
-
const getMaxScroll = useCallback(() => {
|
|
123
|
-
if (isError && buildResult?.error) {
|
|
124
|
-
return Math.max(0, buildResult.error.split('\n').length - CONTENT_LINES);
|
|
125
|
-
}
|
|
126
|
-
else if (isManualPending) {
|
|
127
|
-
return 0;
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
return Math.max(0, logLines.length - CONTENT_LINES);
|
|
131
|
-
}
|
|
132
|
-
}, [isError, isManualPending, buildResult?.error, logLines.length]);
|
|
133
|
-
useInput((input, key) => {
|
|
134
|
-
if (input === 'q') {
|
|
135
|
-
exit();
|
|
136
|
-
}
|
|
137
|
-
const maxScroll = getMaxScroll();
|
|
138
|
-
if (key.upArrow) {
|
|
139
|
-
setScrollOffset(prev => Math.min(maxScroll, prev + 1));
|
|
140
|
-
}
|
|
141
|
-
else if (key.downArrow) {
|
|
142
|
-
setScrollOffset(prev => Math.max(0, prev - 1));
|
|
143
|
-
}
|
|
144
|
-
if (manual && status === 'pending') {
|
|
145
|
-
if (input === 'r' || key.return) {
|
|
146
|
-
onManualRebuild?.();
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
useEffect(() => {
|
|
151
|
-
const api = {
|
|
152
|
-
setStatus,
|
|
153
|
-
setChangedFiles,
|
|
154
|
-
setBuildResult,
|
|
155
|
-
setLiveLog,
|
|
156
|
-
};
|
|
157
|
-
globalThis.__watchUIAPI = api;
|
|
158
|
-
drainLiveLogBuffer();
|
|
159
|
-
return () => {
|
|
160
|
-
delete globalThis.__watchUIAPI;
|
|
161
|
-
};
|
|
162
|
-
}, [setStatus, setChangedFiles, setBuildResult, setLiveLog]);
|
|
163
|
-
const statusText = {
|
|
164
|
-
watching: 'Watching for changes...',
|
|
165
|
-
building: 'Building...',
|
|
166
|
-
restarting: 'Restarting...',
|
|
167
|
-
error: 'Build Error',
|
|
168
|
-
pending: 'Pending changes',
|
|
169
|
-
};
|
|
170
|
-
const showSpinner = status === 'building' || status === 'restarting';
|
|
171
|
-
const statusColor = status === 'error' ? 'red' : status === 'pending' ? 'yellow' : 'green';
|
|
172
|
-
const footerParts = [];
|
|
173
|
-
if (manual)
|
|
174
|
-
footerParts.push('R/Enter: rebuild');
|
|
175
|
-
if (logLines.length > CONTENT_LINES || (isError && buildResult?.error && buildResult.error.split('\n').length > CONTENT_LINES)) {
|
|
176
|
-
footerParts.push('↑↓: scroll');
|
|
177
|
-
}
|
|
178
|
-
footerParts.push('Q: exit');
|
|
179
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "yellow", children: ["Watch Mode", manual ? _jsx(Text, { color: "cyan", children: " (Manual)" }) : ''] }), _jsxs(Text, { children: [showSpinner && _jsxs(_Fragment, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " " })] }), _jsx(Text, { color: statusColor, children: statusText[status] }), reason && _jsxs(Text, { color: "gray", children: [" (", reason, ")"] })] }), _jsx(Text, { children: " " }), _jsx(ContentDisplay, { mode: contentMode, logLines: logLines, errorText: buildResult?.error ?? null, changes: changedFiles, scrollOffset: scrollOffset }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "gray", children: ["Changed: ", formatChangedFiles(changedFiles)] }), buildResult?.resourceCounts ? (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: buildResult.resourceCounts.functions }), " functions | ", _jsx(Text, { color: "cyan", children: buildResult.resourceCounts.other }), " others"] })) : (_jsx(Text, { color: "gray", children: "No build results yet" })), isError ? _jsx(Text, { color: "yellow", children: "Waiting for changes to retry..." }) : _jsx(Text, { children: " " }), _jsx(Text, { color: "gray", children: footerParts.join(' | ') })] }));
|
|
180
|
-
}
|
|
181
|
-
export function getWatchUIAPI() {
|
|
182
|
-
return globalThis.__watchUIAPI;
|
|
183
|
-
}
|
package/lib/ui/logger.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
declare let liveLogCallback: ((level: string | false, args: unknown[]) => void) | null;
|
|
2
|
-
export declare function initLogger(rootFolder: string): () => Promise<void>;
|
|
3
|
-
/**
|
|
4
|
-
* Initialize the logger without file writing.
|
|
5
|
-
* Use this for `sand build` where we want logging but no persistent log file.
|
|
6
|
-
*/
|
|
7
|
-
export declare function initLoggerNoFile(): void;
|
|
8
|
-
/**
|
|
9
|
-
* Set whether the logger should suppress live output.
|
|
10
|
-
*/
|
|
11
|
-
export declare function setSilent(value: boolean): void;
|
|
12
|
-
export declare function setLiveLogCallback(callback: typeof liveLogCallback): void;
|
|
13
|
-
export declare function drainLiveLogBuffer(): void;
|
|
14
|
-
export declare function log(...args: unknown[]): void;
|
|
15
|
-
export declare function logInfo(...args: unknown[]): void;
|
|
16
|
-
export declare function logWarn(...args: unknown[]): void;
|
|
17
|
-
export declare function logDebug(...args: unknown[]): void;
|
|
18
|
-
export declare function logTrace(...args: unknown[]): void;
|
|
19
|
-
export declare function logError(error: unknown): void;
|
|
20
|
-
export {};
|