@needle-tools/gltf-build-pipeline 3.0.0-next.7e6c33d → 3.0.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/CHANGELOG.md +47 -0
- package/dist/cli/index.js +79 -42
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,53 @@ All notable changes to this package will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
5
5
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [3.0.0] - 2026-06-23
|
|
8
|
+
|
|
9
|
+
First stable 3.0 release. Summary of the headline changes across the 3.0 alpha/beta cycle.
|
|
10
|
+
|
|
11
|
+
### Breaking
|
|
12
|
+
- The CLI now requires JWT authentication via `--auth-token`. A missing, malformed, or expired token aborts the run with a clear message that includes the pipeline version.
|
|
13
|
+
- The package ships as a CLI only.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- **Job cache**: a file-level cache that skips an entire transform when the same input + config was already processed.
|
|
17
|
+
- **Audio compression**: external audio references are re-encoded to Opus/OGG. Supported input formats are WAV, FLAC, and AIFF; already-lossy formats pass through unchanged.
|
|
18
|
+
- **PMREM CLI command**: `needle-gltf pmrem <input>` converts EXR and Radiance HDR files to PMREM KTX2.
|
|
19
|
+
- **Multiple file inputs**: `transform` accepts comma-separated paths (e.g. `needle-gltf transform a.glb,b.glb out/`).
|
|
20
|
+
- **Diagnostic logging**: authentication failures (with secrets redacted) and pipeline errors — compression failures and unhandled exceptions — are written to a log file to make issues easier to diagnose.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- VRM thumbnails (and other textures referenced only through an opaque extension) no longer dangle to the wrong image after prune/dedup re-indexing. A stale `KHR_texture_basisu.source` could point past the end of the image array and crash three.js on load (`Cannot read properties of undefined (reading 'uri')`), or silently resolve to a neighbouring image. Re-running the pipeline on an already-compressed VRM is now covered by tests.
|
|
24
|
+
- Audio URIs use forward slashes on every platform; on Windows a `path.join`-built clip path produced backslashes that loaders could not resolve (only affected audio in a subfolder).
|
|
25
|
+
- Cache restore now also restores external files (KTX2, PMREM) alongside the main GLB.
|
|
26
|
+
- PMREM NaN generation that caused artifacts in certain EXR files.
|
|
27
|
+
- xxHash WASM init race condition, now gated by `ensureHashReady`.
|
|
28
|
+
- LOD file detection no longer matches against directory names.
|
|
29
|
+
- toktx now probes the `ktx` binary to match what the compression step actually runs.
|
|
30
|
+
- `getConfig` returns a fresh copy instead of a shared mutable reference.
|
|
31
|
+
- `stats` tolerates undefined errors/warnings in `addFrom` and exits with code 0.
|
|
32
|
+
- Material-only GLBs are no longer incorrectly pruned.
|
|
33
|
+
- `createOpaqueExtension` return type fixed, downstream casts removed.
|
|
34
|
+
|
|
35
|
+
## [3.0.0-beta.3] - 2026-06-10
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
- Pipeline errors (including compression failures and unhandled exceptions) are now written to the log file to aid diagnosis.
|
|
39
|
+
|
|
40
|
+
## [3.0.0-beta.2] - 2026-06-10
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
- Authentication failures are now written to a log file (with secrets redacted) to make auth issues easier to diagnose.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
- The "Auth failed" message now includes the pipeline version.
|
|
47
|
+
|
|
48
|
+
## [3.0.0-beta.1] - 2026-06-10
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
- VRM thumbnail (and other textures referenced only through an opaque extension) no longer dangle to the wrong image after prune/dedup re-indexing. When an image was deduped, an extension-carried `KHR_texture_basisu.source` was copied verbatim and left stale — the thumbnail's source could point past the end of the image array and crash three.js on load (`Cannot read properties of undefined (reading 'uri')`), while other affected textures silently resolved to a neighbouring image. Re-running the pipeline on an already-compressed VRM is now covered by tests.
|
|
52
|
+
- Audio URIs now use forward slashes on every platform. On Windows the rewritten clip path was built with `path.join`, producing backslashes that loaders cannot resolve (only affected audio assets in a subfolder).
|
|
53
|
+
|
|
7
54
|
## [3.0.0-alpha] - 2026-05-26
|
|
8
55
|
|
|
9
56
|
### Breaking
|
package/dist/cli/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import path5 from "path";
|
|
|
8
8
|
import { Logger, NodeIO as NodeIO2, PropertyType as PropertyType12, Verbosity as Verbosity2 } from "@gltf-transform/core";
|
|
9
9
|
import { ALL_EXTENSIONS as ALL_EXTENSIONS3 } from "@gltf-transform/extensions";
|
|
10
10
|
import draco3d2 from "draco3dgltf";
|
|
11
|
-
import { existsSync as
|
|
11
|
+
import { existsSync as existsSync12, readFileSync as readFileSync8, statSync as statSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
12
12
|
import { MeshoptDecoder as MeshoptDecoder2, MeshoptEncoder as MeshoptEncoder3 } from "meshoptimizer";
|
|
13
13
|
import { dedup as dedup2, metalRough as metalRough2, prune as prune2, resample } from "@gltf-transform/functions";
|
|
14
14
|
|
|
@@ -689,7 +689,11 @@ var GenericWriter = class {
|
|
|
689
689
|
}
|
|
690
690
|
this.debug && console.log("Could not find texture, creating a new one: " + this._debugName);
|
|
691
691
|
let extensions = this.inputDefinition.extensions ? { ...this.inputDefinition.extensions } : void 0;
|
|
692
|
-
extensions && (delete extensions[NEEDLE_progressive_texture_settings.EXTENSION_NAME], extensions.EXT_texture_exr && this.inputObject.getMimeType() !== "image/exr" && delete extensions.EXT_texture_exr, Object.keys(extensions).length === 0 && (extensions = void 0))
|
|
692
|
+
if (extensions && (delete extensions[NEEDLE_progressive_texture_settings.EXTENSION_NAME], extensions.EXT_texture_exr && this.inputObject.getMimeType() !== "image/exr" && delete extensions.EXT_texture_exr, Object.keys(extensions).length === 0 && (extensions = void 0)), extensions)
|
|
693
|
+
for (let key of Object.keys(extensions)) {
|
|
694
|
+
let extDef = extensions[key];
|
|
695
|
+
extDef?.source !== void 0 && (extensions[key] = { ...extDef, source: imgIndex });
|
|
696
|
+
}
|
|
693
697
|
let textureDef = {
|
|
694
698
|
source: extensions && Object.keys(extensions).some((key) => extensions[key]?.source !== void 0) ? void 0 : imgIndex,
|
|
695
699
|
sampler: this.findOrCreateSampler(context.jsonDoc.json),
|
|
@@ -1190,7 +1194,7 @@ function registerExtensions(node) {
|
|
|
1190
1194
|
import { createTransform } from "@gltf-transform/functions";
|
|
1191
1195
|
|
|
1192
1196
|
// src/utils/version.gen.ts
|
|
1193
|
-
var version = "3.0.0
|
|
1197
|
+
var version = "3.0.0";
|
|
1194
1198
|
|
|
1195
1199
|
// src/utils/version.ts
|
|
1196
1200
|
function getVersion(_marklocal = !0) {
|
|
@@ -3600,15 +3604,38 @@ function needle_audio_transform(options) {
|
|
|
3600
3604
|
}
|
|
3601
3605
|
function computeOpusOggUri(sourceUri) {
|
|
3602
3606
|
let dir = dirname6(sourceUri), ext = extname(sourceUri), newName = `${basename3(sourceUri, ext)}.opus.ogg`;
|
|
3603
|
-
return dir === "." || dir === "" ? newName :
|
|
3607
|
+
return (dir === "." || dir === "" ? newName : `${dir}/${newName}`).replaceAll("\\", "/");
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
// src/utils/logfile.ts
|
|
3611
|
+
import { appendFileSync, existsSync as existsSync11, mkdirSync as mkdirSync8, renameSync, statSync as statSync5 } from "fs";
|
|
3612
|
+
import { join as join7 } from "path";
|
|
3613
|
+
import { homedir } from "os";
|
|
3614
|
+
var LOG_DIR = join7(homedir(), ".needle", "optimization-pipeline", "logs"), LOG_FILE = join7(LOG_DIR, "pipeline.log"), PREV_LOG_FILE = join7(LOG_DIR, "pipeline.prev.log"), MAX_LOG_BYTES = 1024 * 1024;
|
|
3615
|
+
function redactSecrets(message) {
|
|
3616
|
+
return message.replace(/([a-z][a-z0-9+.-]*:\/\/)[^/@\s]+@/gi, "$1").replace(/\b[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, "[REDACTED_JWT]").replace(/\bnc_[A-Za-z0-9_-]{6,}\b/g, "[REDACTED_TOKEN]");
|
|
3617
|
+
}
|
|
3618
|
+
function logToFile(message) {
|
|
3619
|
+
try {
|
|
3620
|
+
mkdirSync8(LOG_DIR, { recursive: !0 });
|
|
3621
|
+
try {
|
|
3622
|
+
existsSync11(LOG_FILE) && statSync5(LOG_FILE).size > MAX_LOG_BYTES && renameSync(LOG_FILE, PREV_LOG_FILE);
|
|
3623
|
+
} catch {
|
|
3624
|
+
}
|
|
3625
|
+
appendFileSync(LOG_FILE, `[${(/* @__PURE__ */ new Date()).toISOString()}] ${redactSecrets(message)}
|
|
3626
|
+
`);
|
|
3627
|
+
} catch {
|
|
3628
|
+
}
|
|
3604
3629
|
}
|
|
3605
3630
|
|
|
3606
3631
|
// src/scripts/pack-gltf.ts
|
|
3607
3632
|
async function packGLTF(inputFile, outputFile, options) {
|
|
3608
3633
|
options.logger || (options.logger = new Logger(Verbosity2.INFO));
|
|
3609
3634
|
let { debug, stats, packedFiles, useCache, logger } = options;
|
|
3610
|
-
if (!
|
|
3611
|
-
|
|
3635
|
+
if (!existsSync12(inputFile)) {
|
|
3636
|
+
let message = `WARN: can not pack, file does not exist: "${inputFile}"`;
|
|
3637
|
+
return logger.error(message), logToFile(message), !1;
|
|
3638
|
+
}
|
|
3612
3639
|
let cacheKey;
|
|
3613
3640
|
if (useCache && isMeshLOD(inputFile)) {
|
|
3614
3641
|
let existing = readFileSync8(inputFile);
|
|
@@ -3624,7 +3651,7 @@ async function packGLTF(inputFile, outputFile, options) {
|
|
|
3624
3651
|
try {
|
|
3625
3652
|
let config = options.config;
|
|
3626
3653
|
outputFile ? outputFile = getOutputPath(inputFile, outputFile) : outputFile = inputFile;
|
|
3627
|
-
let prevSizeInMB =
|
|
3654
|
+
let prevSizeInMB = statSync6(inputFile).size / 1024 / 1024;
|
|
3628
3655
|
logger.debug(`\u2192 Compressing ${prevSizeInMB.toFixed(1)} MB: ${inputFile}`);
|
|
3629
3656
|
let startTime = Date.now(), io = new NodeIO2().registerExtensions(ALL_EXTENSIONS3).registerExtensions(ALL_EXTENSIONS).registerExtensions([
|
|
3630
3657
|
NEEDLE_compression_texture,
|
|
@@ -3686,32 +3713,32 @@ async function packGLTF(inputFile, outputFile, options) {
|
|
|
3686
3713
|
let uri = tex.getURI();
|
|
3687
3714
|
if (uri && !uri.startsWith("data:")) {
|
|
3688
3715
|
let absPath = path5.resolve(outDir, uri);
|
|
3689
|
-
|
|
3716
|
+
existsSync12(absPath) && options.externalFiles.push(absPath);
|
|
3690
3717
|
}
|
|
3691
3718
|
}
|
|
3692
3719
|
for (let buf of root.listBuffers()) {
|
|
3693
3720
|
let uri = buf.getURI();
|
|
3694
3721
|
if (uri && !uri.startsWith("data:")) {
|
|
3695
3722
|
let absPath = path5.resolve(outDir, uri);
|
|
3696
|
-
|
|
3723
|
+
existsSync12(absPath) && options.externalFiles.push(absPath);
|
|
3697
3724
|
}
|
|
3698
3725
|
}
|
|
3699
3726
|
for (let ref of listAudioRefs()) {
|
|
3700
3727
|
let uri = ref.newUri ?? ref.sourceUri, absPath = path5.resolve(outDir, uri);
|
|
3701
|
-
|
|
3728
|
+
existsSync12(absPath) && options.externalFiles.push(absPath);
|
|
3702
3729
|
}
|
|
3703
3730
|
}
|
|
3704
|
-
logger.debug(`\u2190 Writing to ${outputFile}`), await writeNodeIO(io, outputFile, document), useCache && cacheKey != null &&
|
|
3705
|
-
let duration = Date.now() - startTime, sizeInMB =
|
|
3731
|
+
logger.debug(`\u2190 Writing to ${outputFile}`), await writeNodeIO(io, outputFile, document), useCache && cacheKey != null && existsSync12(outputFile) && addToCache(cacheKey, readFileSync8(outputFile));
|
|
3732
|
+
let duration = Date.now() - startTime, sizeInMB = statSync6(outputFile).size / 1024 / 1024, filename = path5.basename(outputFile), gpuMemoryInMB = totalGPUTextureMemory / 1024 / 1024;
|
|
3706
3733
|
return logger.info(`\u2190 Compressed '${filename}' ${prevSizeInMB.toFixed(1)} MB \u2192 ${sizeInMB.toFixed(1)} MB (GPU: ${gpuMemoryInMB.toFixed(1)} MB textures) in ${(duration / 1e3).toFixed(1)} sec`), stats && (stats.totalFilesProcessed++, stats.totalFileSizeInMB += sizeInMB, stats.totalFileSizeInMBBefore += prevSizeInMB, stats.totalGPUMemoryInMB += gpuMemoryInMB), !0;
|
|
3707
3734
|
} catch (err) {
|
|
3708
3735
|
if (err instanceof TestAssertionError) throw err;
|
|
3709
3736
|
let shortedStackTrace = err instanceof Error && err.stack ? err.stack.split(`
|
|
3710
3737
|
`).slice(0, 2).join(`
|
|
3711
|
-
`) : ""
|
|
3712
|
-
if (logger.error(`Packing of "${path5.basename(inputFile)}" using version ${getVersion()} failed with error:
|
|
3738
|
+
`) : "", message = `Packing of "${path5.basename(inputFile)}" using version ${getVersion()} failed with error:
|
|
3713
3739
|
${err instanceof Error ? err.message : String(err)}
|
|
3714
|
-
${shortedStackTrace}
|
|
3740
|
+
${shortedStackTrace}`;
|
|
3741
|
+
if (logger.error(message), logToFile(message), debug) throw err;
|
|
3715
3742
|
return !1;
|
|
3716
3743
|
}
|
|
3717
3744
|
}
|
|
@@ -3733,7 +3760,7 @@ async function calculateStats(inputfile, options) {
|
|
|
3733
3760
|
|
|
3734
3761
|
// src/config/index.ts
|
|
3735
3762
|
import { Logger as Logger3, Verbosity as Verbosity4 } from "@gltf-transform/core";
|
|
3736
|
-
import { existsSync as
|
|
3763
|
+
import { existsSync as existsSync13, readFileSync as readFileSync9, statSync as statSync7 } from "fs";
|
|
3737
3764
|
import path6, { dirname as dirname7, resolve as resolve2 } from "path";
|
|
3738
3765
|
var UsecaseOptions = ["product", "world"], defaultConfig = {
|
|
3739
3766
|
usecase: "default"
|
|
@@ -3748,9 +3775,9 @@ function createConfig(usecase) {
|
|
|
3748
3775
|
function getConfig(configPath, fallbackSearchDirectory, opts) {
|
|
3749
3776
|
let logger = opts?.logger;
|
|
3750
3777
|
if (logger || (logger = new Logger3(Verbosity4.WARN)), configPath != null) {
|
|
3751
|
-
if (configPath = resolve2(process.cwd(), configPath), !
|
|
3778
|
+
if (configPath = resolve2(process.cwd(), configPath), !existsSync13(configPath))
|
|
3752
3779
|
throw new Error(`No config found at "${configPath}"`);
|
|
3753
|
-
if (
|
|
3780
|
+
if (statSync7(configPath).isDirectory()) {
|
|
3754
3781
|
let res2 = searchConfig(configPath, logger);
|
|
3755
3782
|
if (res2)
|
|
3756
3783
|
return res2;
|
|
@@ -3771,9 +3798,9 @@ function getConfig(configPath, fallbackSearchDirectory, opts) {
|
|
|
3771
3798
|
}
|
|
3772
3799
|
var _cachedConfig;
|
|
3773
3800
|
function searchConfig(directory, logger) {
|
|
3774
|
-
for (
|
|
3801
|
+
for (statSync7(directory).isFile() && (directory = dirname7(directory)), directory = directory.replaceAll("\\", "/"); directory.length > 0; ) {
|
|
3775
3802
|
let configPath = path6.join(directory, "needle.config.json");
|
|
3776
|
-
if (
|
|
3803
|
+
if (existsSync13(configPath)) {
|
|
3777
3804
|
let content = readFileSync9(configPath, "utf8"), config = resolveConfig(configPath, content, logger);
|
|
3778
3805
|
if (config)
|
|
3779
3806
|
return config;
|
|
@@ -3805,7 +3832,7 @@ function resolveConfig(filename, content, logger) {
|
|
|
3805
3832
|
}
|
|
3806
3833
|
|
|
3807
3834
|
// src/cli/index.ts
|
|
3808
|
-
import { existsSync as
|
|
3835
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11, readdirSync as readdirSync3, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
3809
3836
|
import path7, { basename as basename4, dirname as dirname8, resolve as resolve3 } from "path";
|
|
3810
3837
|
|
|
3811
3838
|
// src/constants.ts
|
|
@@ -3871,6 +3898,13 @@ function trackError(error, context) {
|
|
|
3871
3898
|
return;
|
|
3872
3899
|
}
|
|
3873
3900
|
recentErrors.set(key, { time: now, count: (cached?.count || 0) + 1 });
|
|
3901
|
+
let ctx = [
|
|
3902
|
+
context?.mode && `mode=${context.mode}`,
|
|
3903
|
+
context?.usecase && `usecase=${context.usecase}`,
|
|
3904
|
+
context?.file && `file=${context.file}`
|
|
3905
|
+
].filter(Boolean).join(", ");
|
|
3906
|
+
logToFile(`[error] ${errorName}: ${message}${ctx ? ` (${ctx})` : ""}${stack ? `
|
|
3907
|
+
${stack}` : ""}`);
|
|
3874
3908
|
let properties = {
|
|
3875
3909
|
version: getVersion(),
|
|
3876
3910
|
error_name: errorName + (cached && cached.count > 1 ? ` x${cached.count}` : ""),
|
|
@@ -3922,9 +3956,9 @@ var PROD_KEYS = [
|
|
|
3922
3956
|
var trusted = [...PROD_KEYS], TRUSTED_KEYS = trusted;
|
|
3923
3957
|
|
|
3924
3958
|
// src/auth/verify.ts
|
|
3925
|
-
import { readFileSync as readFileSync10, existsSync as
|
|
3926
|
-
import { join as
|
|
3927
|
-
import { homedir } from "os";
|
|
3959
|
+
import { readFileSync as readFileSync10, existsSync as existsSync14 } from "fs";
|
|
3960
|
+
import { join as join9 } from "path";
|
|
3961
|
+
import { homedir as homedir2 } from "os";
|
|
3928
3962
|
var AuthError = class extends Error {
|
|
3929
3963
|
constructor(message) {
|
|
3930
3964
|
super(message), this.name = "AuthError";
|
|
@@ -3932,29 +3966,32 @@ var AuthError = class extends Error {
|
|
|
3932
3966
|
}, LOCAL_FETCH_TIMEOUT_MS = 2e3;
|
|
3933
3967
|
function getLocalServerToken() {
|
|
3934
3968
|
try {
|
|
3935
|
-
let file =
|
|
3936
|
-
if (!
|
|
3969
|
+
let file = join9(homedir2(), ".needle", "local-server-token");
|
|
3970
|
+
if (!existsSync14(file)) return null;
|
|
3937
3971
|
let data = JSON.parse(readFileSync10(file, "utf-8"));
|
|
3938
3972
|
return typeof data.token == "string" ? data.token : null;
|
|
3939
|
-
} catch {
|
|
3940
|
-
return null;
|
|
3973
|
+
} catch (err) {
|
|
3974
|
+
return logToFile(`[auth] failed reading local-server-token (${err instanceof Error ? err.name : "unknown error"})`), null;
|
|
3941
3975
|
}
|
|
3942
3976
|
}
|
|
3943
3977
|
async function tryFetchLocalToken(org) {
|
|
3978
|
+
let baseUrl = process.env.NEEDLE_LICENSE_SERVER_URL || "http://localhost:8424/api/license", safeServer = "local license server";
|
|
3944
3979
|
try {
|
|
3945
|
-
let
|
|
3946
|
-
org && url.searchParams.set("org", org)
|
|
3980
|
+
let url = new URL(baseUrl);
|
|
3981
|
+
org && url.searchParams.set("org", org), safeServer = `${url.origin}${url.pathname}`;
|
|
3947
3982
|
let headers = {}, serverToken = getLocalServerToken();
|
|
3948
3983
|
serverToken && (headers.Authorization = `Bearer ${serverToken}`);
|
|
3949
3984
|
let res = await fetch(url, {
|
|
3950
3985
|
signal: AbortSignal.timeout(LOCAL_FETCH_TIMEOUT_MS),
|
|
3951
3986
|
headers
|
|
3952
3987
|
});
|
|
3953
|
-
if (!res.ok)
|
|
3988
|
+
if (!res.ok)
|
|
3989
|
+
return logToFile(`[auth] license server (${safeServer}) returned HTTP ${res.status} ${res.statusText}${org ? ` \u2014 verify the --org value "${org}"` : ""}`), null;
|
|
3954
3990
|
let jwt = (await res.json()).needle_license_jwt;
|
|
3955
|
-
return typeof jwt == "string" && jwt.length > 0 ? jwt : null;
|
|
3956
|
-
} catch {
|
|
3957
|
-
|
|
3991
|
+
return typeof jwt == "string" && jwt.length > 0 ? jwt : (logToFile(`[auth] license server (${safeServer}) responded without a license token${org ? ` for org "${org}"` : ""}`), null);
|
|
3992
|
+
} catch (err) {
|
|
3993
|
+
let msg = err instanceof Error ? err.message : String(err);
|
|
3994
|
+
return logToFile(`[auth] could not reach license server (${safeServer})${org ? ` for org "${org}"` : ""}: ${msg}`), null;
|
|
3958
3995
|
}
|
|
3959
3996
|
}
|
|
3960
3997
|
async function verifyWithKey(token, key) {
|
|
@@ -4019,7 +4056,7 @@ async function gateAuth(authToken, org, logger) {
|
|
|
4019
4056
|
await requireAuth(authToken, typeof org == "string" ? org : void 0);
|
|
4020
4057
|
} catch (err) {
|
|
4021
4058
|
let message = err instanceof AuthError || err instanceof Error ? err.message : String(err);
|
|
4022
|
-
logger.error(`[Needle Build Pipeline] Auth failed: ${message}`), process.exit(ERROR_CODES.AUTH_FAILED);
|
|
4059
|
+
logger.error(`[Needle Build Pipeline] v${getVersion()} Auth failed: ${message}`), logToFile(`[Needle Build Pipeline] v${getVersion()} Auth failed: ${message}`), process.exit(ERROR_CODES.AUTH_FAILED);
|
|
4023
4060
|
}
|
|
4024
4061
|
}
|
|
4025
4062
|
program.cast(!1);
|
|
@@ -4073,7 +4110,7 @@ When caching is enabled then previously processed files will be detected and loa
|
|
|
4073
4110
|
}
|
|
4074
4111
|
let set = /* @__PURE__ */ new Set(), useCache = options.cache !== !1, debug = options.debug === !0, verbose = options.verbose === !0;
|
|
4075
4112
|
await foreachGLTF(inputPaths, async (file) => {
|
|
4076
|
-
if (
|
|
4113
|
+
if (existsSync15(file)) {
|
|
4077
4114
|
if (file != input && options.progressive === !0 && isLOD(file)) {
|
|
4078
4115
|
logger.debug(`\u2192 Ignore existing LOD file at ${file}`);
|
|
4079
4116
|
return;
|
|
@@ -4089,12 +4126,12 @@ When caching is enabled then previously processed files will be detected and loa
|
|
|
4089
4126
|
config
|
|
4090
4127
|
}, file) : void 0;
|
|
4091
4128
|
if (jobKey) {
|
|
4092
|
-
let outputDir = output ?
|
|
4129
|
+
let outputDir = output ? existsSync15(output) && isDirectory(output) ? output : dirname8(output) : dirname8(file), restored = tryRestoreJobResult(jobKey, outputDir, logger);
|
|
4093
4130
|
if (restored) {
|
|
4094
|
-
let inputFileSize =
|
|
4131
|
+
let inputFileSize = statSync8(file).size / 1024 / 1024;
|
|
4095
4132
|
for (let f of restored)
|
|
4096
4133
|
if (set.add(f), stats) {
|
|
4097
|
-
let sizeInMB =
|
|
4134
|
+
let sizeInMB = statSync8(f).size / 1024 / 1024;
|
|
4098
4135
|
stats.totalFilesProcessed++, stats.totalFileSizeInMB += sizeInMB, stats.totalFileSizeInMBBefore += inputFileSize;
|
|
4099
4136
|
}
|
|
4100
4137
|
logger.info(`\u2192 [CACHE] Restored ${restored.length} file(s) for job ${jobKey}`);
|
|
@@ -4165,7 +4202,7 @@ raw .exr / .hdr inputs.
|
|
|
4165
4202
|
`).argument("<input>", "Path to a .exr/.hdr file OR a directory containing .exr/.hdr files (recursive)", { validator: program.STRING }).argument("<output>", "Output file (when input is a single .exr/.hdr) or output directory. Defaults to writing next to the input file.", { default: "", validator: program.STRING }).option("--cache", 'Use "--cache False" to disable caching.', { validator: program.BOOLEAN, default: !0 }).option("--debug", 'Enable verbose output with "--debug True"', { validator: program.BOOLEAN, default: !1 }).option("--verbose", 'Enable verbose output with "--verbose True"', { validator: program.BOOLEAN, default: !1 }).option("--auth-token <jwt>", "Required. A JWT minted by needle-cloud (alternatively set NEEDLE_CLOUD_TOKEN). Note: an `nc_*` needle-cloud access token is NOT a JWT and will be rejected.", { validator: program.STRING }).option("--org <org>", "Organization ID for local license-server fallback (or set NEEDLE_CLOUD_ORG).", { validator: program.STRING }).hide().action(async ({ logger, args, options }) => {
|
|
4166
4203
|
warnUnknownOptions(options, logger), await gateAuth(options.authToken, options.org, logger), logger.level = options.debug || options.verbose ? "debug" : "info", await ensureHashReady();
|
|
4167
4204
|
let input = args.input.toString();
|
|
4168
|
-
|
|
4205
|
+
existsSync15(input) || (logger.error(`Input does not exist: ${input}`), process.exit(ERROR_CODES.INVALID_ARGS));
|
|
4169
4206
|
let inputFiles = [], inputIsDirectory = isDirectory(input);
|
|
4170
4207
|
if (inputIsDirectory ? collectPmremInputFiles(input, inputFiles) : isPmremInputFile(input) ? inputFiles.push(input) : (logger.error(`Input is not an .exr/.hdr file or directory: ${input}`), process.exit(ERROR_CODES.INVALID_ARGS)), inputFiles.length === 0) {
|
|
4171
4208
|
logger.warn(`No .exr/.hdr files found in ${input}`);
|
|
@@ -4204,7 +4241,7 @@ function collectPmremInputFiles(dir, out) {
|
|
|
4204
4241
|
for (let entry of readdirSync3(dir)) {
|
|
4205
4242
|
if (entry === "node_modules" || entry.startsWith(".")) continue;
|
|
4206
4243
|
let full = path7.join(dir, entry);
|
|
4207
|
-
|
|
4244
|
+
statSync8(full).isDirectory() ? collectPmremInputFiles(full, out) : isPmremInputFile(entry) && out.push(full);
|
|
4208
4245
|
}
|
|
4209
4246
|
}
|
|
4210
4247
|
function resolvePmremOutputPath(srcPath, input, inputIsDirectory, outputArg) {
|
|
@@ -4215,7 +4252,7 @@ function resolvePmremOutputPath(srcPath, input, inputIsDirectory, outputArg) {
|
|
|
4215
4252
|
let rel = path7.relative(input, srcPath), relDir = dirname8(rel), outDir = relDir === "." ? outputArg : path7.join(outputArg, relDir);
|
|
4216
4253
|
return ensureIsDirectory(outDir), path7.join(outDir, outName);
|
|
4217
4254
|
}
|
|
4218
|
-
return
|
|
4255
|
+
return existsSync15(outputArg) && isDirectory(outputArg) ? path7.join(outputArg, outName) : outputArg.toLowerCase().endsWith(".ktx2") ? (ensureIsDirectory(dirname8(outputArg)), outputArg) : (ensureIsDirectory(outputArg), path7.join(outputArg, outName));
|
|
4219
4256
|
}
|
|
4220
4257
|
program.run().catch((err) => {
|
|
4221
4258
|
if (!(err.message.includes("Unknown command") && !err.message.includes("Did you mean")))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@needle-tools/gltf-build-pipeline",
|
|
3
|
-
"version": "3.0.0
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Pipeline and tools for optimizing gltf files using gltf-transform and compression settings within glTF extensions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|