refactor: clean upload pipeline and restore draco delivery

This commit is contained in:
Tom Boullay
2026-04-29 16:29:32 +02:00
parent 097b8f6486
commit 498765db61
32 changed files with 769 additions and 215 deletions
+2 -39
View File
@@ -1,46 +1,9 @@
import { getAssetFamily } from './asset-naming'
export type AssetCategory = 'color' | 'diffuse' | 'roughness' | 'normal' | 'metalness' | 'height' | 'opacity' | 'orm' | 'ao' | 'assets'
import type { AssetCategory } from './types'
export function classifyAssetCategory(filename: string): AssetCategory {
const name = filename.replace(/\.[^.]+$/, '')
const family = getAssetFamily(name.split('_')[0])
if (family === 'color') {
return 'color'
}
if (family === 'diffuse') {
return 'diffuse'
}
if (family === 'roughness') {
return 'roughness'
}
if (family === 'normal') {
return 'normal'
}
if (family === 'metalness') {
return 'metalness'
}
if (family === 'height') {
return 'height'
}
if (family === 'opacity') {
return 'opacity'
}
if (family === 'orm') {
return 'orm'
}
if (family === 'ao') {
return 'ao'
}
return 'assets'
return family || 'assets'
}
+3 -3
View File
@@ -10,7 +10,7 @@ export const ASSET_FAMILIES = [
'ao',
] as const
export type AssetFamily = typeof ASSET_FAMILIES[number]
type AssetFamily = typeof ASSET_FAMILIES[number]
const ASSET_FAMILY_BY_KEY = new Map(ASSET_FAMILIES.map((family) => [family.toLowerCase(), family]))
const FORBIDDEN_ASSET_FAMILY_ALIASES: ReadonlyMap<string, AssetFamily> = new Map([
@@ -44,7 +44,7 @@ export function getAssetFamily(value: string): AssetFamily | undefined {
return ASSET_FAMILY_BY_KEY.get(value.toLowerCase())
}
export function getForbiddenAssetFamilyAlias(value: string): AssetFamily | undefined {
function getForbiddenAssetFamilyAlias(value: string): AssetFamily | undefined {
return FORBIDDEN_ASSET_FAMILY_ALIASES.get(value.toLowerCase())
}
@@ -143,6 +143,6 @@ export function getTextureNamingError(filename: string) {
return `Asset inconnu : ${filename}. Familles autorisees : ${formatAssetFamilies()}. Utilisez asset.png pour tout le modele ou asset_objet.png pour cibler un objet.`
}
export function formatAssetFamilies() {
function formatAssetFamilies() {
return ASSET_FAMILIES.join(', ')
}
+50
View File
@@ -0,0 +1,50 @@
import { join } from 'path'
import { existsSync } from 'fs'
import { execFile } from 'child_process'
import { promisify } from 'util'
const execFileAsync = promisify(execFile)
/**
* Compress a GLTF/GLB model using Blender's Draco compression.
* Returns { success: true } on success, or { success: false, error } on failure.
* GLB Draco is explicit: callers should fail instead of silently pushing heavy assets.
*/
export async function compressWithBlender(
inputPath: string,
outputPath: string,
): Promise<{ success: boolean; error?: string }> {
const blenderPath = process.env.BLENDER_PATH || 'blender'
const timeout = Number(process.env.BLENDER_TIMEOUT_MS || 600_000)
const scriptPath = join(process.cwd(), 'scripts', 'compress.py')
if (!existsSync(scriptPath)) {
return { success: false, error: 'scripts/compress.py introuvable' }
}
try {
await execFileAsync(
blenderPath,
[
'--background',
'--python', scriptPath,
'--',
'-i', inputPath,
'-o', outputPath,
'--draco-level', '7',
'--texture-size', '512',
'-q',
],
{ timeout },
)
if (!existsSync(outputPath)) {
return { success: false, error: "Blender n'a pas produit de fichier compresse" }
}
return { success: true }
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
return { success: false, error: `Compression Blender echouee: ${message}` }
}
}
+1 -1
View File
@@ -4,6 +4,6 @@ export function revokeEntryUrls(entry: FolderEntry) {
const urls = new Set<string>()
if (entry.modelUrl) urls.add(entry.modelUrl)
Object.values(entry.assetUrls || {}).forEach((url) => urls.add(url))
Object.values(entry.assetUrls).forEach((url) => urls.add(url))
urls.forEach((url) => URL.revokeObjectURL(url))
}
+1 -5
View File
@@ -1,7 +1,3 @@
// ---------------------------------------------------------------------------
// Client-side types — used by components and hooks (no Node.js Buffer)
// ---------------------------------------------------------------------------
type FileStatus = 'pending' | 'uploading' | 'success' | 'error'
export interface TextureFile {
@@ -20,7 +16,7 @@ export interface FolderEntry {
error?: string
filename?: string
modelUrl?: string
assetUrls?: Record<string, string>
assetUrls: Record<string, string>
viewerOpen?: boolean
warnings: string[]
driveStatus?: DriveStatus
+31 -14
View File
@@ -1,6 +1,23 @@
import type { AssetCategory } from './asset-classification'
import type { FileChange } from './types'
import type { PreparedAssetSummary } from './types'
import { ASSET_FAMILIES } from './asset-naming'
import type { AssetCategory, FileChange, PreparedAssetSummary } from './types'
const ASSET_SECTION_ORDER: AssetCategory[] = [...ASSET_FAMILIES, 'assets']
function getChangePrefix(change: FileChange) {
if (change === 'new') return '✅'
if (change === 'changed') return '🔄'
return null
}
function addGroupedAssetLine(
grouped: Map<AssetCategory, string[]>,
category: AssetCategory,
line: string,
) {
const current = grouped.get(category) || []
current.push(line)
grouped.set(category, current)
}
/**
* Build a formatted commit message based on the upload context.
@@ -25,7 +42,6 @@ export function buildCommitMessage(
const lines: string[] = [title, '']
// Model section — show status for new, changed, or unchanged
const modelSummary = assetSummaries.find((asset) => asset.kind === 'model')
const modelChange = fileChanges.get(modelFilename.toLowerCase())
if (modelChange === 'new') {
@@ -45,15 +61,16 @@ export function buildCommitMessage(
if (asset.kind === 'model' || !asset.category) continue
const change = fileChanges.get(asset.filename.toLowerCase())
if (change === 'new') {
const current = grouped.get(asset.category) || []
current.push(`${asset.filename}${asset.compressed ? ' (compressed)' : ''}`)
grouped.set(asset.category, current)
} else if (change === 'changed') {
const current = grouped.get(asset.category) || []
current.push(` 🔄 ${asset.filename}${asset.compressed ? ' (compressed)' : ''}`)
grouped.set(asset.category, current)
}
if (!change) continue
const prefix = getChangePrefix(change)
if (!prefix) continue
addGroupedAssetLine(
grouped,
asset.category,
` ${prefix} ${asset.filename}${asset.compressed ? ' (compressed)' : ''}`,
)
}
const sectionTitles: Record<AssetCategory, string> = {
@@ -69,7 +86,7 @@ export function buildCommitMessage(
assets: '🧩 Assets',
}
for (const category of ['color', 'diffuse', 'roughness', 'normal', 'metalness', 'height', 'opacity', 'orm', 'ao', 'assets'] as const) {
for (const category of ASSET_SECTION_ORDER) {
const entries = grouped.get(category)
if (!entries || entries.length === 0) continue
lines.push('')
+1 -1
View File
@@ -4,7 +4,7 @@ export const ASSET_EXTENSIONS = new Set(['.bin'])
export const ALL_ALLOWED_EXTENSIONS = new Set([...MODEL_EXTENSIONS, ...TEXTURE_EXTENSIONS, ...ASSET_EXTENSIONS])
/** Extensions tracked by Git LFS (must match .gitattributes) */
export const LFS_EXTENSIONS = new Set(['.gltf', '.bin', '.png', '.jpg', '.jpeg', '.webp'])
export const LFS_EXTENSIONS = new Set(['.glb', '.gltf', '.bin', '.png', '.jpg', '.jpeg', '.webp'])
export const TMP_DIR = '/tmp/assets'
+1 -3
View File
@@ -13,9 +13,7 @@ interface DiffResult {
* the remote file map.
*
* Rules:
* - Models: always re-pushed,
* but marked as 'unchanged' in the commit message when the folder already
* exists (we keep the current behavior of always delivering the model file).
* - Models: always re-pushed, but marked as unchanged when the remote file exists.
* - Textures: compared by size (not compressed, reliable).
* - Orphan remote files: classified as deletions.
*/
-4
View File
@@ -1,7 +1,3 @@
// ---------------------------------------------------------------------------
// Format bytes to human-readable string
// ---------------------------------------------------------------------------
export function formatBytes(bytes: number): string {
if (bytes <= 0) return '0 B'
const k = 1024
+4 -2
View File
@@ -6,8 +6,10 @@ import type { PushFile, RemoteFile } from './types'
const LFS_BATCH_SIZE = 100
type LogDetails = Record<string, string | number | boolean | undefined>
function isHttpError(err: unknown): err is { status: number } {
return typeof err === 'object' && err !== null && 'status' in err && typeof (err as Record<string, unknown>).status === 'number'
return isRecord(err) && typeof err.status === 'number'
}
function getOctokit(): Octokit {
@@ -51,7 +53,7 @@ function formatElapsed(startedAt: number) {
return `${((performance.now() - startedAt) / 1000).toFixed(1)}s`
}
function logInfo(step: string, action: string, startedAt: number, details?: Record<string, unknown>) {
function logInfo(step: string, action: string, startedAt: number, details?: LogDetails) {
console.info(`[INFO] ${step} -> ${action} | Timer: ${formatElapsed(startedAt)}`, details || '')
}
+9 -2
View File
@@ -3,11 +3,17 @@ import { NextRequest } from 'next/server'
import { sanitizeFilename } from './sanitize'
import { ALL_ALLOWED_EXTENSIONS, MODEL_EXTENSIONS, MAX_FILE_SIZE, TEXTURE_EXTENSIONS } from './constants'
import { getTextureNamingError } from './asset-naming'
import type { ParsedFile } from './types'
import type { GitModelMode, ParsedFile } from './types'
interface ParsedUpload {
folderName: string
files: ParsedFile[]
gitModelMode: GitModelMode
}
function parseGitModelMode(value: FormDataEntryValue | null): GitModelMode {
if (value === 'draco-glb' || value === 'keep-gltf') return value
throw new Error('Mode Git invalide')
}
export async function parseMultiUpload(req: NextRequest): Promise<ParsedUpload> {
@@ -15,6 +21,7 @@ export async function parseMultiUpload(req: NextRequest): Promise<ParsedUpload>
const folderValue = formData.get('folderName')
const folderName = typeof folderValue === 'string' ? folderValue.trim() || 'assets' : 'assets'
const safeFolderName = sanitizeFilename(folderName).replace(/[^a-zA-Z0-9-_]/g, '-')
const gitModelMode = parseGitModelMode(formData.get('gitModelMode'))
const rawFiles = formData.getAll('files')
const fileTypes = formData.getAll('fileTypes').filter((value): value is string => typeof value === 'string')
@@ -95,5 +102,5 @@ export async function parseMultiUpload(req: NextRequest): Promise<ParsedUpload>
throw new Error('Un seul fichier model.gltf est autorise')
}
return { folderName: safeFolderName, files: parsed }
return { folderName: safeFolderName, files: parsed, gitModelMode }
}
+80 -24
View File
@@ -1,23 +1,22 @@
import { extname } from 'path'
import { randomUUID } from 'crypto'
import { existsSync } from 'fs'
import { mkdir, readFile, rm, writeFile } from 'fs/promises'
import { extname, join } from 'path'
import { compressWithBlender } from '@/lib/blender'
import { compressTextureBuffer } from '@/lib/texture-compression'
import { classifyAssetCategory } from '@/lib/asset-classification'
import { normalizeTextureFilename } from '@/lib/asset-naming'
import { TEXTURE_EXTENSIONS } from '@/lib/constants'
import { TEXTURE_EXTENSIONS, TMP_DIR } from '@/lib/constants'
import { getModelAssetPath } from '@/lib/model-paths'
import type { ParsedFile, PreparedAssetSummary, PushFile } from '@/lib/types'
import type { GitModelMode, ParsedFile, PreparedAssetSummary, PreparedGitAssetsResult, PushFile } from '@/lib/types'
interface PrepareGitAssetsParams {
folderName: string
parsedFiles: ParsedFile[]
gitModelMode: GitModelMode
}
interface PrepareGitAssetsResult {
filesToPush: PushFile[]
modelFilename: string
assetSummaries: PreparedAssetSummary[]
compressed: boolean
compressionError?: string
}
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }
function getTextureFilenameMap(parsedFiles: ParsedFile[]) {
const filenameMap = new Map<string, string>()
@@ -51,14 +50,14 @@ function getReferencedFilename(uri: string) {
return cleanUri.split(/[\\/]/).pop()?.toLowerCase()
}
function rewriteGltfUris(value: unknown, filenameMap: Map<string, string>): unknown {
function rewriteGltfUris(value: JsonValue, filenameMap: Map<string, string>): JsonValue {
if (Array.isArray(value)) {
return value.map((entry) => rewriteGltfUris(entry, filenameMap))
}
if (!value || typeof value !== 'object') return value
const rewritten: Record<string, unknown> = {}
const rewritten: Record<string, JsonValue> = {}
for (const [key, entry] of Object.entries(value)) {
if (key === 'uri' && typeof entry === 'string') {
@@ -76,20 +75,20 @@ function rewriteGltfUris(value: unknown, filenameMap: Map<string, string>): unkn
function prepareModelBuffer(buffer: Buffer, filenameMap: Map<string, string>) {
if (filenameMap.size === 0) return buffer
const parsed: unknown = JSON.parse(buffer.toString('utf-8'))
const parsed = JSON.parse(buffer.toString('utf-8')) as JsonValue
return Buffer.from(JSON.stringify(rewriteGltfUris(parsed, filenameMap), null, 2), 'utf-8')
}
export async function prepareGitAssets({
folderName,
parsedFiles,
}: PrepareGitAssetsParams): Promise<PrepareGitAssetsResult> {
async function prepareSeparateFiles(
folderName: string,
parsedFiles: ParsedFile[],
textureFilenameMap: Map<string, string>,
) {
const filesToPush: PushFile[] = []
const assetSummaries: PreparedAssetSummary[] = []
let modelFilename = ''
let compressed = false
let compressionError: string | undefined
const textureFilenameMap = getTextureFilenameMap(parsedFiles)
for (const pf of parsedFiles) {
let content = pf.buffer
@@ -131,11 +130,68 @@ export async function prepareGitAssets({
})
}
return {
filesToPush,
modelFilename,
assetSummaries,
compressed,
compressionError,
return { filesToPush, modelFilename, assetSummaries, compressed, compressionError }
}
async function prepareDracoGlb(
folderName: string,
parsedFiles: ParsedFile[],
textureFilenameMap: Map<string, string>,
): Promise<PreparedGitAssetsResult> {
const tmpFolder = join(TMP_DIR, 'blender', `${folderName}-${randomUUID()}`)
const inputModelPath = join(tmpFolder, 'model.gltf')
const outputModelPath = join(tmpFolder, 'model.glb')
await mkdir(tmpFolder, { recursive: true })
try {
for (const pf of parsedFiles) {
if (pf.isModel) {
await writeFile(inputModelPath, prepareModelBuffer(pf.buffer, textureFilenameMap))
continue
}
const filename = textureFilenameMap.get(pf.filename.toLowerCase()) || pf.filename
const ext = extname(filename).toLowerCase()
const content = TEXTURE_EXTENSIONS.has(ext)
? (await compressTextureBuffer(filename, pf.buffer)).buffer
: pf.buffer
await writeFile(join(tmpFolder, filename), content)
}
const result = await compressWithBlender(inputModelPath, outputModelPath)
if (!result.success || !existsSync(outputModelPath)) {
throw new Error(result.error || 'Compression Blender echouee')
}
const content = await readFile(outputModelPath)
const modelFilename = 'model.glb'
return {
filesToPush: [{
path: getModelAssetPath(folderName, modelFilename),
contentBase64: content.toString('base64'),
}],
modelFilename,
assetSummaries: [{ filename: modelFilename, kind: 'model', compressed: true }],
compressed: true,
}
} finally {
await rm(tmpFolder, { recursive: true, force: true }).catch(() => {})
}
}
export async function prepareGitAssets({
folderName,
parsedFiles,
gitModelMode,
}: PrepareGitAssetsParams): Promise<PreparedGitAssetsResult> {
const textureFilenameMap = getTextureFilenameMap(parsedFiles)
if (gitModelMode === 'keep-gltf') {
return prepareSeparateFiles(folderName, parsedFiles, textureFilenameMap)
}
return prepareDracoGlb(folderName, parsedFiles, textureFilenameMap)
}
+24 -2
View File
@@ -1,5 +1,3 @@
import type { AssetCategory } from './asset-classification'
export interface ParsedFile {
filename: string
buffer: Buffer
@@ -11,8 +9,14 @@ export interface PushFile {
contentBase64: string
}
export type GitModelMode = 'draco-glb' | 'keep-gltf'
export type DriveAction = 'new' | 'replace'
export type FileChange = 'new' | 'changed' | 'unchanged'
export type AssetCategory = 'color' | 'diffuse' | 'roughness' | 'normal' | 'metalness' | 'height' | 'opacity' | 'orm' | 'ao' | 'assets'
export interface FileDiff {
name: string
status: 'changed' | 'new' | 'deleted'
@@ -29,3 +33,21 @@ export interface PreparedAssetSummary {
category?: AssetCategory
compressed: boolean
}
export interface StagingUploadResult {
stagingId: string
folderName: string
filesCount: number
}
export interface PreparedGitAssetsResult {
filesToPush: PushFile[]
modelFilename: string
assetSummaries: PreparedAssetSummary[]
compressed: boolean
compressionError?: string
}
export interface PreparedStageAssetsResult extends PreparedGitAssetsResult {
folderName: string
}
+23 -25
View File
@@ -1,31 +1,37 @@
import { isRecord } from './guards'
import type { FolderEntry } from './client-types'
import type { FileDiff } from './types'
import type { DriveAction, FileDiff, GitModelMode, StagingUploadResult } from './types'
export interface CheckResult {
exists: boolean
diffs: FileDiff[]
}
interface StageResult {
stagingId: string
folderName: string
filesCount: number
}
function getApiError(data: unknown, fallback: string) {
return isRecord(data) && typeof data.error === 'string' ? data.error : fallback
}
function getUploadJsonHeaders(secret: string) {
return {
'Content-Type': 'application/json',
'x-upload-secret': secret.trim(),
}
}
function isAbortError(err: unknown) {
return err instanceof DOMException && err.name === 'AbortError'
}
function isFileDiff(value: unknown): value is FileDiff {
return isRecord(value)
&& typeof value.name === 'string'
&& (value.status === 'new' || value.status === 'changed' || value.status === 'deleted')
}
function buildUploadFormData(folder: FolderEntry): FormData {
function buildUploadFormData(folder: FolderEntry, gitModelMode: GitModelMode): FormData {
const formData = new FormData()
formData.append('folderName', folder.folderName)
formData.append('gitModelMode', gitModelMode)
formData.append('files', folder.modelFile)
formData.append('fileTypes', 'model')
@@ -47,10 +53,7 @@ export async function checkFolderDiffs(
): Promise<CheckResult> {
const res = await fetch('/api/upload/check', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-upload-secret': secret.trim(),
},
headers: getUploadJsonHeaders(secret),
body: JSON.stringify({ stagingId }),
signal,
})
@@ -72,10 +75,11 @@ export async function checkFolderDiffs(
export async function stageUpload(
folder: FolderEntry,
gitModelMode: GitModelMode,
secret: string,
signal?: AbortSignal,
): Promise<StageResult> {
const formData = buildUploadFormData(folder)
): Promise<StagingUploadResult> {
const formData = buildUploadFormData(folder, gitModelMode)
const res = await fetch('/api/upload/stage', {
method: 'POST',
headers: { 'x-upload-secret': secret.trim() },
@@ -103,16 +107,13 @@ export async function stageUpload(
export async function uploadDrive(
stagingId: string,
secret: string,
action: 'new' | 'replace',
action: DriveAction,
signal?: AbortSignal,
): Promise<{ success: boolean; error?: string }> {
try {
const res = await fetch('/api/upload/drive', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-upload-secret': secret.trim(),
},
headers: getUploadJsonHeaders(secret),
body: JSON.stringify({ stagingId, action }),
signal,
})
@@ -122,7 +123,7 @@ export async function uploadDrive(
}
return { success: true }
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') {
if (isAbortError(err)) {
return { success: false, error: 'Upload annule' }
}
return { success: false, error: 'Erreur reseau (Drive)' }
@@ -140,10 +141,7 @@ export async function uploadGit(
try {
const res = await fetch('/api/upload/git', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-upload-secret': secret.trim(),
},
headers: getUploadJsonHeaders(secret),
body: JSON.stringify({ stagingId }),
signal,
})
@@ -158,7 +156,7 @@ export async function uploadGit(
onProgress(100)
return { success: true, filename: typeof data.folderName === 'string' ? data.folderName : undefined }
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') {
if (isAbortError(err)) {
return { success: false, error: 'Upload annule' }
}
return { success: false, error: 'Erreur reseau' }
+1 -2
View File
@@ -1,6 +1,5 @@
import { isRecord } from './guards'
type DriveAction = 'new' | 'replace'
import type { DriveAction } from './types'
interface StagingRequestBody {
stagingId: string
+23 -16
View File
@@ -5,7 +5,14 @@ import { existsSync } from 'fs'
import { TMP_DIR } from '@/lib/constants'
import { getModelAssetPath } from '@/lib/model-paths'
import { prepareGitAssets } from '@/lib/prepare-git-assets'
import type { ParsedFile, PreparedAssetSummary, PushFile } from '@/lib/types'
import type {
GitModelMode,
ParsedFile,
PreparedAssetSummary,
PreparedStageAssetsResult,
PushFile,
StagingUploadResult,
} from '@/lib/types'
const STAGING_ROOT = join(TMP_DIR, 'staging')
const STAGING_TTL_MS = 60 * 60 * 1000
@@ -26,20 +33,12 @@ interface StagedPreparedData {
interface StagingManifest {
stagingId: string
folderName: string
gitModelMode: GitModelMode
createdAt: number
originals: StagedOriginalFile[]
prepared?: StagedPreparedData
}
interface PreparedStageAssetsResult {
folderName: string
filesToPush: PushFile[]
modelFilename: string
assetSummaries: PreparedAssetSummary[]
compressed: boolean
compressionError?: string
}
function getStageDir(stagingId: string) {
return join(STAGING_ROOT, stagingId)
}
@@ -87,7 +86,11 @@ async function cleanupExpiredStagingUploads() {
}
}
export async function createStagingUpload(folderName: string, parsedFiles: ParsedFile[]) {
export async function createStagingUpload(
folderName: string,
parsedFiles: ParsedFile[],
gitModelMode: GitModelMode,
): Promise<StagingUploadResult> {
await cleanupExpiredStagingUploads()
const stagingId = randomUUID()
@@ -106,6 +109,7 @@ export async function createStagingUpload(folderName: string, parsedFiles: Parse
const manifest: StagingManifest = {
stagingId,
folderName,
gitModelMode,
createdAt: Date.now(),
originals,
}
@@ -137,12 +141,11 @@ async function readOriginalParsedFiles(stagingId: string, manifest: StagingManif
)
}
async function buildPreparedPushFiles(stagingId: string, manifest: StagingManifest): Promise<PushFile[]> {
async function buildPreparedPushFiles(stagingId: string, manifest: StagingManifest, prepared: StagedPreparedData): Promise<PushFile[]> {
const preparedDir = getPreparedDir(stagingId)
const preparedFiles = manifest.prepared?.assetSummaries || []
return Promise.all(
preparedFiles.map(async (file) => {
prepared.assetSummaries.map(async (file) => {
const buffer = await readFile(join(preparedDir, file.filename))
return {
path: getModelAssetPath(manifest.folderName, file.filename),
@@ -157,7 +160,11 @@ export async function ensurePreparedStagingAssets(stagingId: string): Promise<Pr
if (!manifest.prepared) {
const parsedFiles = await readOriginalParsedFiles(stagingId, manifest)
const prepared = await prepareGitAssets({ folderName: manifest.folderName, parsedFiles })
const prepared = await prepareGitAssets({
folderName: manifest.folderName,
parsedFiles,
gitModelMode: manifest.gitModelMode,
})
const preparedDir = getPreparedDir(stagingId)
await mkdir(preparedDir, { recursive: true })
@@ -181,7 +188,7 @@ export async function ensurePreparedStagingAssets(stagingId: string): Promise<Pr
return {
folderName: manifest.folderName,
filesToPush: await buildPreparedPushFiles(stagingId, manifest),
filesToPush: await buildPreparedPushFiles(stagingId, manifest, manifest.prepared),
modelFilename: manifest.prepared.modelFilename,
assetSummaries: manifest.prepared.assetSummaries,
compressed: manifest.prepared.compressed,
+1 -1
View File
@@ -14,7 +14,7 @@ interface GltfJson {
}
/** Discriminated union: either valid (with model) or invalid (with errors). */
export type ValidationResult =
type ValidationResult =
| { ok: true; model: File; textures: TextureFile[]; warnings: string[] }
| { ok: false; errors: string[] }