sandstone-cli 2.2.0 → 2.3.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/bun.lock +234 -5
- package/lib/create.js +2 -2
- package/lib/index.js +3135 -3087
- package/package.json +7 -4
- package/src/commands/build/export.ts +295 -0
- package/src/commands/build/externalResources.ts +122 -0
- package/src/commands/build/index.ts +548 -0
- package/src/commands/create.ts +1 -1
- package/src/commands/dependency.ts +1 -1
- package/src/commands/index.ts +1 -1
- package/src/commands/watch.ts +37 -2
- package/src/ui/WatchUI.tsx +10 -2
- package/src/utils.ts +6 -0
- package/src/version.ts +1 -1
- package/src/commands/build.ts +0 -869
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { pathToFileURL } from 'node:url'
|
|
3
|
+
import fs from 'fs-extra'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
import { split } from 'obliterator'
|
|
6
|
+
|
|
7
|
+
import type { BuildResult, ResourceCounts } from '../../ui/types.js'
|
|
8
|
+
import { log, initLoggerNoFile, setSilent } from '../../ui/logger.js'
|
|
9
|
+
import { hash } from '../../utils.js'
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
type SandstoneCache,
|
|
13
|
+
checkSymlinksAvailable,
|
|
14
|
+
getClientPath,
|
|
15
|
+
getClientWorldPath,
|
|
16
|
+
createArchive,
|
|
17
|
+
preserveSymlink,
|
|
18
|
+
exportPack,
|
|
19
|
+
runExportHandler,
|
|
20
|
+
getExportPath,
|
|
21
|
+
cleanupOldSymlinks,
|
|
22
|
+
cleanupOldArchives,
|
|
23
|
+
} from './export.js'
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
type FileExclusions,
|
|
27
|
+
type FileHandler,
|
|
28
|
+
autoRegisterPackTypes,
|
|
29
|
+
processExternalResources,
|
|
30
|
+
} from './externalResources.js'
|
|
31
|
+
|
|
32
|
+
import type * as sandstone from 'sandstone'
|
|
33
|
+
import type { handlerReadFile, PackType } from 'sandstone/pack'
|
|
34
|
+
|
|
35
|
+
type SandstoneContext = ReturnType<typeof sandstone['getSandstoneContext']>
|
|
36
|
+
|
|
37
|
+
declare global {
|
|
38
|
+
interface RegExpConstructor {
|
|
39
|
+
escape(str: string): string;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type BuildOptions = {
|
|
44
|
+
// Flags
|
|
45
|
+
dry?: boolean
|
|
46
|
+
verbose?: boolean
|
|
47
|
+
root?: boolean
|
|
48
|
+
strictErrors?: boolean
|
|
49
|
+
production?: boolean
|
|
50
|
+
|
|
51
|
+
// Values
|
|
52
|
+
path: string
|
|
53
|
+
name?: string
|
|
54
|
+
namespace?: string
|
|
55
|
+
world?: string
|
|
56
|
+
clientPath?: string
|
|
57
|
+
serverPath?: string
|
|
58
|
+
|
|
59
|
+
enableSymlinks?: boolean
|
|
60
|
+
|
|
61
|
+
dependencies?: [string, string][]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface BuildContext {
|
|
65
|
+
sandstoneConfig: sandstone.SandstoneConfig
|
|
66
|
+
sandstonePack: sandstone.SandstonePack
|
|
67
|
+
resetSandstonePack: () => void
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Cache management
|
|
71
|
+
let cache: SandstoneCache = { files: {} }
|
|
72
|
+
|
|
73
|
+
async function loadCache(cacheFile: string): Promise<SandstoneCache> {
|
|
74
|
+
if (Object.keys(cache.files).length > 0) {
|
|
75
|
+
return cache
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const fileRead = await fs.readFile(cacheFile, 'utf8')
|
|
80
|
+
if (fileRead) {
|
|
81
|
+
const parsed = JSON.parse(fileRead)
|
|
82
|
+
cache = parsed.files ? parsed : { files: parsed }
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
cache = { files: {} }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return cache
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function saveCache(cacheFile: string, newCache: SandstoneCache) {
|
|
92
|
+
cache = newCache
|
|
93
|
+
await fs.ensureDir(path.dirname(cacheFile))
|
|
94
|
+
await fs.writeFile(cacheFile, JSON.stringify(cache))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Boilerplate resources to exclude from counts
|
|
98
|
+
const BOILERPLATE_NAMESPACES = new Set(['load', '__sandstone__'])
|
|
99
|
+
const BOILERPLATE_FUNCTIONS = new Set(['__init__'])
|
|
100
|
+
const BOILERPLATE_TAG = { namespace: 'minecraft', name: 'load' }
|
|
101
|
+
|
|
102
|
+
function isBoilerplateResource(resource: { path?: string[]; namespace?: string }): boolean {
|
|
103
|
+
const ns = resource.namespace || ''
|
|
104
|
+
const pathParts = resource.path || []
|
|
105
|
+
const name = pathParts[pathParts.length - 1] || ''
|
|
106
|
+
|
|
107
|
+
if (BOILERPLATE_NAMESPACES.has(ns)) return true
|
|
108
|
+
if (BOILERPLATE_FUNCTIONS.has(name)) return true
|
|
109
|
+
if (ns === BOILERPLATE_TAG.namespace && name === BOILERPLATE_TAG.name) return true
|
|
110
|
+
|
|
111
|
+
return false
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function countResources(sandstonePack: { core: { resourceNodes: Iterable<{ resource: unknown }> } }): ResourceCounts {
|
|
115
|
+
let functions = 0
|
|
116
|
+
let other = 0
|
|
117
|
+
|
|
118
|
+
for (const node of sandstonePack.core.resourceNodes) {
|
|
119
|
+
const resource = node.resource as { constructor?: { name?: string }; path?: string[]; namespace?: string }
|
|
120
|
+
|
|
121
|
+
if (isBoilerplateResource(resource)) continue
|
|
122
|
+
|
|
123
|
+
if (resource.constructor?.name === '_RawMCFunctionClass') {
|
|
124
|
+
functions++
|
|
125
|
+
} else {
|
|
126
|
+
other++
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { functions, other }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Process pack type's generated output (post-processing)
|
|
134
|
+
async function processPackTypeOutput(
|
|
135
|
+
packType: PackType,
|
|
136
|
+
outputPath: string
|
|
137
|
+
) {
|
|
138
|
+
await fs.ensureDir(outputPath)
|
|
139
|
+
|
|
140
|
+
if (packType.handleOutput) {
|
|
141
|
+
await packType.handleOutput(
|
|
142
|
+
'output',
|
|
143
|
+
(async (relativePath: string, encoding: BufferEncoding = 'utf8') =>
|
|
144
|
+
await fs.readFile(path.join(outputPath, relativePath), encoding)) as unknown as handlerReadFile,
|
|
145
|
+
async (relativePath: string, contents: any) => {
|
|
146
|
+
if (contents === undefined) {
|
|
147
|
+
await fs.unlink(path.join(outputPath, relativePath))
|
|
148
|
+
} else {
|
|
149
|
+
await fs.writeFile(
|
|
150
|
+
path.join(outputPath, relativePath),
|
|
151
|
+
contents
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function loadBuildContext(
|
|
160
|
+
cliOptions: BuildOptions,
|
|
161
|
+
folder: string,
|
|
162
|
+
): Promise<BuildContext> {
|
|
163
|
+
const configPath = path.join(folder, 'sandstone.config.ts')
|
|
164
|
+
const configUrl = pathToFileURL(configPath).toString()
|
|
165
|
+
const sandstoneConfig = (await import(configUrl)).default
|
|
166
|
+
|
|
167
|
+
const namespace = cliOptions.namespace || sandstoneConfig.namespace
|
|
168
|
+
const conflictStrategies: NonNullable<SandstoneContext['conflictStrategies']> = {}
|
|
169
|
+
|
|
170
|
+
if (sandstoneConfig.onConflict) {
|
|
171
|
+
for (const [resource, strategy] of Object.entries(sandstoneConfig.onConflict)) {
|
|
172
|
+
conflictStrategies[resource] = strategy as NonNullable<SandstoneContext['conflictStrategies']>[string]
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const sandstoneUrl = pathToFileURL(path.join(folder, 'node_modules', 'sandstone', 'dist', 'index.js'))
|
|
177
|
+
/* @ts-ignore */
|
|
178
|
+
const { createSandstonePack, resetSandstonePack } = (await import(sandstoneUrl)) as typeof sandstone
|
|
179
|
+
|
|
180
|
+
const context: SandstoneContext = {
|
|
181
|
+
workingDir: folder,
|
|
182
|
+
namespace,
|
|
183
|
+
packUid: sandstoneConfig.packUid,
|
|
184
|
+
packOptions: sandstoneConfig.packs,
|
|
185
|
+
conflictStrategies,
|
|
186
|
+
loadVersion: sandstoneConfig.loadVersion,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const sandstonePack = createSandstonePack(context)
|
|
190
|
+
|
|
191
|
+
return { sandstoneConfig, sandstonePack, resetSandstonePack }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
interface BuildProjectResult {
|
|
195
|
+
resourceCounts: ResourceCounts
|
|
196
|
+
sandstoneConfig: sandstone.SandstoneConfig
|
|
197
|
+
sandstonePack: sandstone.SandstonePack
|
|
198
|
+
resetSandstonePack: () => void
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function _buildProject(
|
|
202
|
+
cliOptions: BuildOptions,
|
|
203
|
+
folder: string,
|
|
204
|
+
silent = false,
|
|
205
|
+
existingContext?: BuildContext,
|
|
206
|
+
watching = false
|
|
207
|
+
): Promise<BuildProjectResult | undefined> {
|
|
208
|
+
// Read project package.json to get entrypoint
|
|
209
|
+
const packageJsonPath = path.join(folder, 'package.json')
|
|
210
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'))
|
|
211
|
+
|
|
212
|
+
const entrypoint = packageJson.module
|
|
213
|
+
if (!entrypoint) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
'No "module" field found in package.json. Please specify the entrypoint for your pack code.',
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const entrypointPath = path.join(folder, entrypoint)
|
|
220
|
+
|
|
221
|
+
// Load or use existing context
|
|
222
|
+
const { sandstoneConfig, sandstonePack, resetSandstonePack } = existingContext ??
|
|
223
|
+
await loadBuildContext(cliOptions, folder)
|
|
224
|
+
|
|
225
|
+
resetSandstonePack()
|
|
226
|
+
|
|
227
|
+
const { scripts, resources } = sandstoneConfig
|
|
228
|
+
const saveOptions = sandstoneConfig.saveOptions || {}
|
|
229
|
+
|
|
230
|
+
const outputFolder = path.join(folder, '.sandstone', 'output')
|
|
231
|
+
|
|
232
|
+
// Resolve export options
|
|
233
|
+
const worldName: string | undefined = cliOptions.world || saveOptions.world
|
|
234
|
+
const root: boolean | undefined = cliOptions.root !== undefined ? cliOptions.root : saveOptions.root
|
|
235
|
+
|
|
236
|
+
let clientPath = !cliOptions.production
|
|
237
|
+
? cliOptions.clientPath || saveOptions.clientPath
|
|
238
|
+
: undefined
|
|
239
|
+
|
|
240
|
+
if (worldName && !cliOptions.production) {
|
|
241
|
+
clientPath ??= await getClientPath()
|
|
242
|
+
if (clientPath) {
|
|
243
|
+
await getClientWorldPath(worldName, clientPath)
|
|
244
|
+
}
|
|
245
|
+
} else if (root && !cliOptions.production) {
|
|
246
|
+
clientPath ??= await getClientPath()
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const serverPath = !cliOptions.production
|
|
250
|
+
? cliOptions.serverPath || saveOptions.serverPath
|
|
251
|
+
: undefined
|
|
252
|
+
const packName: string = cliOptions.name ?? sandstoneConfig.name
|
|
253
|
+
|
|
254
|
+
if (worldName && root) {
|
|
255
|
+
throw new Error("Expected only 'world' or 'root'. Got both.")
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Run beforeAll script
|
|
259
|
+
await scripts?.beforeAll?.()
|
|
260
|
+
|
|
261
|
+
// Import user code
|
|
262
|
+
if (!silent) {
|
|
263
|
+
log('Compiling source...')
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
if (await fs.pathExists(entrypointPath)) {
|
|
268
|
+
const isBun = Object.hasOwn(globalThis, 'Bun')
|
|
269
|
+
const entrypointUrl = pathToFileURL(entrypointPath).toString()
|
|
270
|
+
|
|
271
|
+
if (watching && !isBun) {
|
|
272
|
+
await import(entrypointUrl, { with: { hot: 'true' } })
|
|
273
|
+
} else {
|
|
274
|
+
await import(entrypointUrl)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
} catch (e: any) {
|
|
278
|
+
// Enhance error with context, let callers handle logging
|
|
279
|
+
e.message = `While loading "${entrypointPath}":\n${e.message || e}`
|
|
280
|
+
throw e
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Add dependencies if specified
|
|
284
|
+
if (cliOptions.dependencies) {
|
|
285
|
+
for (const dependency of cliOptions.dependencies) {
|
|
286
|
+
sandstonePack.core.depend(...dependency)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Setup cache
|
|
291
|
+
const cacheFile = path.join(folder, '.sandstone', 'cache.json')
|
|
292
|
+
const oldCache = await loadCache(cacheFile)
|
|
293
|
+
const newCache: SandstoneCache = { files: {}, archives: [] }
|
|
294
|
+
|
|
295
|
+
const changedPackTypes = new Set<string>()
|
|
296
|
+
const newDirs = new Set<string>()
|
|
297
|
+
|
|
298
|
+
// Check symlink availability
|
|
299
|
+
newCache.canUseSymlinks = await checkSymlinksAvailable(oldCache.canUseSymlinks)
|
|
300
|
+
|
|
301
|
+
// Run beforeSave script
|
|
302
|
+
await scripts?.beforeSave?.()
|
|
303
|
+
|
|
304
|
+
// Auto-register pack types if existing resources are present
|
|
305
|
+
await autoRegisterPackTypes(folder, sandstonePack)
|
|
306
|
+
|
|
307
|
+
// File exclusion setup
|
|
308
|
+
const excludeOption = resources?.exclude
|
|
309
|
+
const fileExclusions: FileExclusions = excludeOption
|
|
310
|
+
? {
|
|
311
|
+
generated: ('generated' in excludeOption ? excludeOption.generated : excludeOption) as RegExp[] | undefined,
|
|
312
|
+
existing: ('existing' in excludeOption ? excludeOption.existing : excludeOption) as RegExp[] | undefined,
|
|
313
|
+
}
|
|
314
|
+
: false
|
|
315
|
+
|
|
316
|
+
const fileHandlers: FileHandler[] | false = (resources?.handle as FileHandler[]) || false
|
|
317
|
+
|
|
318
|
+
// Save the pack
|
|
319
|
+
const packTypes = await sandstonePack.save({
|
|
320
|
+
dry: cliOptions.dry ?? false,
|
|
321
|
+
verbose: cliOptions.verbose ?? false,
|
|
322
|
+
|
|
323
|
+
// TODO !: Remove this type cast after beta 2
|
|
324
|
+
fileHandler: (saveOptions.customFileHandler as ((relativePath: string, content: any) => Promise<void>) | undefined) ??
|
|
325
|
+
(async (relativePath: string, content: any) => {
|
|
326
|
+
let pathPass = true
|
|
327
|
+
if (fileExclusions && fileExclusions.generated) {
|
|
328
|
+
for (const exclude of fileExclusions.generated) {
|
|
329
|
+
if (!Array.isArray(exclude)) {
|
|
330
|
+
pathPass = !exclude.test(relativePath)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (fileHandlers) {
|
|
336
|
+
for (const handler of fileHandlers) {
|
|
337
|
+
if (handler.path.test(relativePath)) {
|
|
338
|
+
content = await handler.callback(content)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (pathPass) {
|
|
344
|
+
const hashValue = hash(content + relativePath)
|
|
345
|
+
newCache.files[relativePath] = hashValue
|
|
346
|
+
|
|
347
|
+
for (let dir = path.dirname(relativePath); dir && dir !== '.'; dir = path.dirname(dir)) {
|
|
348
|
+
newDirs.add(dir)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (oldCache.files[relativePath] === hashValue) {
|
|
352
|
+
return
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const packTypeDir = relativePath.split(/[/\\]/)[0]
|
|
356
|
+
changedPackTypes.add(packTypeDir)
|
|
357
|
+
|
|
358
|
+
const realPath = path.join(outputFolder, relativePath)
|
|
359
|
+
await fs.ensureDir(path.dirname(realPath))
|
|
360
|
+
return await fs.writeFile(realPath, content)
|
|
361
|
+
}
|
|
362
|
+
}),
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
// Process and export packs
|
|
366
|
+
const packTypesArray = [...packTypes]
|
|
367
|
+
|
|
368
|
+
if (!cliOptions.production) {
|
|
369
|
+
// Auto-detect client path if needed for client-side packs
|
|
370
|
+
const hasClientPacks = packTypesArray.some(([, pt]) => pt.networkSides === 'client')
|
|
371
|
+
if (hasClientPacks && !clientPath && (root || worldName)) {
|
|
372
|
+
clientPath = await getClientPath()
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const clientOnlyExport = !worldName && !root
|
|
376
|
+
|
|
377
|
+
for (const [, packType] of packTypesArray) {
|
|
378
|
+
const outputPath = path.join(outputFolder, packType.type)
|
|
379
|
+
|
|
380
|
+
// Process pack type output (post-processing generated files)
|
|
381
|
+
await processPackTypeOutput(packType, outputPath)
|
|
382
|
+
await processExternalResources(packType.type, folder, outputFolder, oldCache, newCache, changedPackTypes, newDirs, fileExclusions, fileHandlers)
|
|
383
|
+
|
|
384
|
+
// Determine export destinations
|
|
385
|
+
const shouldExportToClient = clientPath && !(clientOnlyExport && packType.networkSides !== 'client')
|
|
386
|
+
const shouldExportToServer = serverPath && packType.networkSides === 'server'
|
|
387
|
+
|
|
388
|
+
const clientDest = shouldExportToClient
|
|
389
|
+
? getExportPath(packType, clientPath!, 'client', packName, worldName, saveOptions.exportZips)
|
|
390
|
+
: undefined
|
|
391
|
+
const serverDest = shouldExportToServer
|
|
392
|
+
? getExportPath(packType, serverPath!, 'server', packName, worldName, saveOptions.exportZips)
|
|
393
|
+
: undefined
|
|
394
|
+
|
|
395
|
+
// Preserve existing symlinks (even if no files changed)
|
|
396
|
+
preserveSymlink(clientDest, oldCache, newCache)
|
|
397
|
+
preserveSymlink(serverDest, oldCache, newCache)
|
|
398
|
+
|
|
399
|
+
// Skip actual export if nothing changed
|
|
400
|
+
if (!changedPackTypes.has(packType.type)) continue
|
|
401
|
+
|
|
402
|
+
// Archive if configured
|
|
403
|
+
let archivedOutput = false
|
|
404
|
+
if (packType.archiveOutput && saveOptions.exportZips) {
|
|
405
|
+
archivedOutput = await createArchive(outputFolder, packName, packType, newCache)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Export to destinations
|
|
409
|
+
if (clientDest) {
|
|
410
|
+
await exportPack(clientDest, clientPath!, outputPath, outputFolder, folder, packName, packType, archivedOutput, saveOptions.exportZips, oldCache, newCache)
|
|
411
|
+
await runExportHandler(packType, 'client', clientDest)
|
|
412
|
+
}
|
|
413
|
+
if (serverDest) {
|
|
414
|
+
await exportPack(serverDest, serverPath!, outputPath, outputFolder, folder, packName, packType, archivedOutput, saveOptions.exportZips, oldCache, newCache)
|
|
415
|
+
await runExportHandler(packType, 'server', serverDest)
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
} else {
|
|
419
|
+
// Production mode: just process, no exports
|
|
420
|
+
for (const [, packType] of packTypesArray) {
|
|
421
|
+
const outputPath = path.join(outputFolder, packType.type)
|
|
422
|
+
await processPackTypeOutput(packType, outputPath)
|
|
423
|
+
await processExternalResources(packType.type, folder, outputFolder, oldCache, newCache, changedPackTypes, newDirs, fileExclusions, fileHandlers)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Clean up old files and directories
|
|
428
|
+
if (cliOptions.dry !== true) {
|
|
429
|
+
for (const file of Object.keys(oldCache.files)) {
|
|
430
|
+
if (!(file in newCache.files)) {
|
|
431
|
+
await fs.rm(path.join(outputFolder, file))
|
|
432
|
+
|
|
433
|
+
let dir: string | undefined = undefined
|
|
434
|
+
for (const segment of split(new RegExp(RegExp.escape(path.sep)), path.dirname(file))) {
|
|
435
|
+
dir = dir === undefined ? segment : path.join(dir, segment)
|
|
436
|
+
|
|
437
|
+
if (!newDirs.has(dir)) {
|
|
438
|
+
await fs.rm(path.join(outputFolder, dir), { force: true, recursive: true })
|
|
439
|
+
break
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
await cleanupOldArchives(outputFolder, oldCache, newCache)
|
|
446
|
+
await cleanupOldSymlinks(oldCache, newCache)
|
|
447
|
+
|
|
448
|
+
await saveCache(cacheFile, newCache)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Run afterAll script
|
|
452
|
+
await scripts?.afterAll?.()
|
|
453
|
+
|
|
454
|
+
// Count resources
|
|
455
|
+
const resourceCounts = countResources(sandstonePack)
|
|
456
|
+
|
|
457
|
+
const exports = [clientPath && 'client', serverPath && 'server'].filter(Boolean).join(' & ') || false
|
|
458
|
+
const countMsg = `${resourceCounts.functions} functions, ${resourceCounts.other} other resources`
|
|
459
|
+
if (!silent) {
|
|
460
|
+
log(`Pack(s) compiled! (${countMsg})${exports ? ` Exported to ${exports}.` : ''}`)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return { resourceCounts, sandstoneConfig, sandstonePack, resetSandstonePack }
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export async function _buildCommand(
|
|
467
|
+
opts: BuildOptions,
|
|
468
|
+
_folder?: string,
|
|
469
|
+
existingContext?: BuildContext,
|
|
470
|
+
watching = false
|
|
471
|
+
): Promise<BuildResult> {
|
|
472
|
+
const folder = _folder ?? opts.path
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
const result = await _buildProject(opts, folder, true, existingContext, watching)
|
|
476
|
+
return {
|
|
477
|
+
success: true,
|
|
478
|
+
resourceCounts: result?.resourceCounts ?? { functions: 0, other: 0 },
|
|
479
|
+
timestamp: Date.now(),
|
|
480
|
+
sandstoneConfig: result?.sandstoneConfig,
|
|
481
|
+
sandstonePack: result?.sandstonePack,
|
|
482
|
+
resetSandstonePack: result?.resetSandstonePack,
|
|
483
|
+
}
|
|
484
|
+
} catch (err: any) {
|
|
485
|
+
const errorMessage = err.message || String(err)
|
|
486
|
+
const stack = (err.stack as string) || ''
|
|
487
|
+
const cleanedStack = stack
|
|
488
|
+
.replace(/\?hot-hook=\d+/g, '')
|
|
489
|
+
.replace(/file:\/\/\//g, '')
|
|
490
|
+
.replace(/file:\/\//g, '')
|
|
491
|
+
// Stack includes message at top - extract only the trace lines to avoid duplication
|
|
492
|
+
const stackLines = cleanedStack.split('\n')
|
|
493
|
+
const traceStart = stackLines.findIndex(line => line.trimStart().startsWith('at '))
|
|
494
|
+
const stackTrace = traceStart >= 0 ? stackLines.slice(traceStart).join('\n') : ''
|
|
495
|
+
const formattedError = stackTrace ? `${errorMessage}\n${stackTrace}` : errorMessage
|
|
496
|
+
return {
|
|
497
|
+
success: false,
|
|
498
|
+
error: formattedError,
|
|
499
|
+
resourceCounts: { functions: 0, other: 0 },
|
|
500
|
+
timestamp: Date.now(),
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export async function buildCommand(opts: BuildOptions, _?: string): Promise<void>
|
|
506
|
+
export async function buildCommand(opts: BuildOptions, _folder: string | undefined, silent: true): Promise<BuildResult>
|
|
507
|
+
export async function buildCommand(opts: BuildOptions, _folder?: string, silent = false): Promise<BuildResult | void> {
|
|
508
|
+
const folder = (typeof _folder === 'string') ? _folder : opts.path
|
|
509
|
+
|
|
510
|
+
initLoggerNoFile()
|
|
511
|
+
setSilent(silent)
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
const result = await _buildProject(opts, folder, silent)
|
|
515
|
+
if (silent) {
|
|
516
|
+
return {
|
|
517
|
+
success: true,
|
|
518
|
+
resourceCounts: result?.resourceCounts ?? { functions: 0, other: 0 },
|
|
519
|
+
timestamp: Date.now(),
|
|
520
|
+
sandstoneConfig: result?.sandstoneConfig,
|
|
521
|
+
sandstonePack: result?.sandstonePack,
|
|
522
|
+
resetSandstonePack: result?.resetSandstonePack,
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
} catch (err: any) {
|
|
526
|
+
const errorMessage = err.message || String(err)
|
|
527
|
+
const stack = (err.stack as string) || ''
|
|
528
|
+
const cleanedStack = stack
|
|
529
|
+
.replace(/\?hot-hook=\d+/g, '')
|
|
530
|
+
.replace(/file:\/\/\//g, '')
|
|
531
|
+
.replace(/file:\/\//g, '')
|
|
532
|
+
// Stack includes message at top - extract only the trace lines to avoid duplication
|
|
533
|
+
const stackLines = cleanedStack.split('\n')
|
|
534
|
+
const traceStart = stackLines.findIndex(line => line.trimStart().startsWith('at '))
|
|
535
|
+
const stackTrace = traceStart >= 0 ? stackLines.slice(traceStart).join('\n') : ''
|
|
536
|
+
const formattedError = stackTrace ? `${errorMessage}\n${stackTrace}` : errorMessage
|
|
537
|
+
if (!silent) {
|
|
538
|
+
log(chalk.bgRed.white('BuildError') + chalk.gray(':'), formattedError)
|
|
539
|
+
process.exit(1)
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
success: false,
|
|
543
|
+
error: formattedError,
|
|
544
|
+
resourceCounts: { functions: 0, other: 0 },
|
|
545
|
+
timestamp: Date.now(),
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
package/src/commands/create.ts
CHANGED
|
@@ -130,7 +130,7 @@ export async function createCommand(_project: string, opts: CreateOptions) {
|
|
|
130
130
|
|
|
131
131
|
const sv = (v: string) => new SemVer(v)
|
|
132
132
|
|
|
133
|
-
const versions = [[sv('1.0.0-beta.
|
|
133
|
+
const versions = [[sv('1.0.0-beta.2'), sv(CLI_VERSION)]] as const
|
|
134
134
|
|
|
135
135
|
const version = await select({
|
|
136
136
|
message: 'Which version of Sandstone do you want to use? These are the only supported versions for new projects.',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs-extra'
|
|
2
2
|
import path from 'path'
|
|
3
3
|
import { exec } from 'child_process'
|
|
4
|
-
import { buildCommand } from './build.js'
|
|
4
|
+
import { buildCommand } from './build/index.js'
|
|
5
5
|
import { checkbox } from '@inquirer/prompts'
|
|
6
6
|
|
|
7
7
|
const _fetch = import('node-fetch')
|
package/src/commands/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { buildCommand } from './build.js'
|
|
1
|
+
export { buildCommand } from './build/index.js'
|
|
2
2
|
export { createCommand } from './create.js'
|
|
3
3
|
export { installNativeCommand, installVanillaCommand, uninstallVanillaCommand, refreshCommand } from './dependency.js'
|
|
4
4
|
export { watchCommand, type WatchOptions } from './watch.js'
|
package/src/commands/watch.ts
CHANGED
|
@@ -4,14 +4,49 @@ import React from 'react'
|
|
|
4
4
|
import { render } from 'ink'
|
|
5
5
|
|
|
6
6
|
import { normalizePath } from '../utils.js'
|
|
7
|
-
import { _buildCommand, type BuildOptions, type BuildContext
|
|
7
|
+
import { _buildCommand, type BuildOptions, type BuildContext } from './build/index.js'
|
|
8
8
|
import { WatchUI, getWatchUIAPI } from '../ui/WatchUI.js'
|
|
9
|
-
import { initLogger, log, logError, setLiveLogCallback } from '../ui/logger.js'
|
|
9
|
+
import { initLogger, log, logInfo, logWarn, logError, logDebug, logTrace, setLiveLogCallback } from '../ui/logger.js'
|
|
10
10
|
import type { TrackedChange, ChangeCategory } from '../ui/types.js'
|
|
11
11
|
import { hot } from '@sandstone-mc/hot-hook'
|
|
12
12
|
import fs from 'fs-extra'
|
|
13
13
|
import { join, relative } from 'node:path'
|
|
14
14
|
|
|
15
|
+
// Console capture for watch mode - wraps console to redirect output to our log file
|
|
16
|
+
const originalConsole = globalThis.console
|
|
17
|
+
let consoleWrapped = false
|
|
18
|
+
|
|
19
|
+
function enableConsoleCapture() {
|
|
20
|
+
if (consoleWrapped) return
|
|
21
|
+
consoleWrapped = true
|
|
22
|
+
|
|
23
|
+
;(globalThis.console as any).log = (...args: any[]) => log(...args)
|
|
24
|
+
;(globalThis.console as any).info = (...args: any[]) => logInfo(...args)
|
|
25
|
+
;(globalThis.console as any).warn = (...args: any[]) => logWarn(...args)
|
|
26
|
+
;(globalThis.console as any).error = (...args: any[]) => logError(args.join(' '))
|
|
27
|
+
;(globalThis.console as any).debug = (...args: any[]) => logDebug(...args)
|
|
28
|
+
|
|
29
|
+
;(globalThis.console as any).trace = (...args: any[]) => {
|
|
30
|
+
const traceObj = { stack: '' }
|
|
31
|
+
Error.captureStackTrace(traceObj, globalThis.console.trace)
|
|
32
|
+
const cleanedStack = traceObj.stack
|
|
33
|
+
.replace(/^Error\n/, '')
|
|
34
|
+
.replace(/\?hot-hook=\d+/g, '')
|
|
35
|
+
.replace(/file:\/\/\/?/g, '')
|
|
36
|
+
logTrace(...args, '\n' + cleanedStack)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function disableConsoleCapture() {
|
|
41
|
+
if (!consoleWrapped) return
|
|
42
|
+
consoleWrapped = false
|
|
43
|
+
|
|
44
|
+
const methodsToRestore = ['log', 'info', 'warn', 'error', 'debug', 'trace'] as const
|
|
45
|
+
for (const method of methodsToRestore) {
|
|
46
|
+
;(globalThis.console as any)[method] = originalConsole[method].bind(originalConsole)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
15
50
|
export interface WatchOptions extends BuildOptions {
|
|
16
51
|
manual?: boolean
|
|
17
52
|
library?: boolean
|
package/src/ui/WatchUI.tsx
CHANGED
|
@@ -182,9 +182,17 @@ export function WatchUI({ manual, onManualRebuild, exit }: WatchUIProps) {
|
|
|
182
182
|
|
|
183
183
|
const maxScroll = getMaxScroll()
|
|
184
184
|
if (key.upArrow) {
|
|
185
|
-
|
|
185
|
+
if (isError) {
|
|
186
|
+
setScrollOffset(prev => Math.max(0, prev - 1))
|
|
187
|
+
} else {
|
|
188
|
+
setScrollOffset(prev => Math.min(maxScroll, prev + 1))
|
|
189
|
+
}
|
|
186
190
|
} else if (key.downArrow) {
|
|
187
|
-
|
|
191
|
+
if (isError) {
|
|
192
|
+
setScrollOffset(prev => Math.min(maxScroll, prev + 1))
|
|
193
|
+
} else {
|
|
194
|
+
setScrollOffset(prev => Math.max(0, prev - 1))
|
|
195
|
+
}
|
|
188
196
|
}
|
|
189
197
|
|
|
190
198
|
if (manual && status === 'pending') {
|
package/src/utils.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import path from 'path'
|
|
3
3
|
import os from 'os'
|
|
4
|
+
import crypto from 'crypto'
|
|
4
5
|
import { execSync } from 'child_process'
|
|
5
6
|
import chalk from 'chalk-template'
|
|
6
7
|
|
|
8
|
+
/** Hash a string or buffer using MD5 */
|
|
9
|
+
export function hash(data: string | Buffer): string {
|
|
10
|
+
return crypto.createHash('md5').update(data).digest('hex')
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
/** Normalize a path to use forward slashes */
|
|
8
14
|
export const normalizePath = (p: string) => p.replaceAll('\\', '/')
|
|
9
15
|
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = '2.
|
|
1
|
+
export const CLI_VERSION = '2.3.0'
|