@needle-tools/gltf-build-pipeline 3.0.0-beta → 3.0.0-beta.1.1b06064
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 +6 -0
- package/dist/cli/index.js +58 -30
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ 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-beta.1] - 2026-06-10
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- 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.
|
|
11
|
+
- 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).
|
|
12
|
+
|
|
7
13
|
## [3.0.0-alpha] - 2026-05-26
|
|
8
14
|
|
|
9
15
|
### Breaking
|
package/dist/cli/index.js
CHANGED
|
@@ -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-beta";
|
|
1197
|
+
var version = "3.0.0-beta.1.1b06064";
|
|
1194
1198
|
|
|
1195
1199
|
// src/utils/version.ts
|
|
1196
1200
|
function getVersion(_marklocal = !0) {
|
|
@@ -3600,7 +3604,7 @@ 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("\\", "/");
|
|
3604
3608
|
}
|
|
3605
3609
|
|
|
3606
3610
|
// src/scripts/pack-gltf.ts
|
|
@@ -3731,9 +3735,30 @@ async function calculateStats(inputfile, options) {
|
|
|
3731
3735
|
return createNewFilestats(inputfile, document, json);
|
|
3732
3736
|
}
|
|
3733
3737
|
|
|
3738
|
+
// src/utils/logfile.ts
|
|
3739
|
+
import { appendFileSync, existsSync as existsSync12, mkdirSync as mkdirSync8, renameSync, statSync as statSync6 } from "fs";
|
|
3740
|
+
import { join as join8 } from "path";
|
|
3741
|
+
import { homedir } from "os";
|
|
3742
|
+
var LOG_DIR = join8(homedir(), ".needle", "optimization-pipeline", "logs"), LOG_FILE = join8(LOG_DIR, "pipeline.log"), PREV_LOG_FILE = join8(LOG_DIR, "pipeline.prev.log"), MAX_LOG_BYTES = 1024 * 1024;
|
|
3743
|
+
function redactSecrets(message) {
|
|
3744
|
+
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]");
|
|
3745
|
+
}
|
|
3746
|
+
function logToFile(message) {
|
|
3747
|
+
try {
|
|
3748
|
+
mkdirSync8(LOG_DIR, { recursive: !0 });
|
|
3749
|
+
try {
|
|
3750
|
+
existsSync12(LOG_FILE) && statSync6(LOG_FILE).size > MAX_LOG_BYTES && renameSync(LOG_FILE, PREV_LOG_FILE);
|
|
3751
|
+
} catch {
|
|
3752
|
+
}
|
|
3753
|
+
appendFileSync(LOG_FILE, `[${(/* @__PURE__ */ new Date()).toISOString()}] ${redactSecrets(message)}
|
|
3754
|
+
`);
|
|
3755
|
+
} catch {
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
|
|
3734
3759
|
// src/config/index.ts
|
|
3735
3760
|
import { Logger as Logger3, Verbosity as Verbosity4 } from "@gltf-transform/core";
|
|
3736
|
-
import { existsSync as
|
|
3761
|
+
import { existsSync as existsSync13, readFileSync as readFileSync9, statSync as statSync7 } from "fs";
|
|
3737
3762
|
import path6, { dirname as dirname7, resolve as resolve2 } from "path";
|
|
3738
3763
|
var UsecaseOptions = ["product", "world"], defaultConfig = {
|
|
3739
3764
|
usecase: "default"
|
|
@@ -3748,9 +3773,9 @@ function createConfig(usecase) {
|
|
|
3748
3773
|
function getConfig(configPath, fallbackSearchDirectory, opts) {
|
|
3749
3774
|
let logger = opts?.logger;
|
|
3750
3775
|
if (logger || (logger = new Logger3(Verbosity4.WARN)), configPath != null) {
|
|
3751
|
-
if (configPath = resolve2(process.cwd(), configPath), !
|
|
3776
|
+
if (configPath = resolve2(process.cwd(), configPath), !existsSync13(configPath))
|
|
3752
3777
|
throw new Error(`No config found at "${configPath}"`);
|
|
3753
|
-
if (
|
|
3778
|
+
if (statSync7(configPath).isDirectory()) {
|
|
3754
3779
|
let res2 = searchConfig(configPath, logger);
|
|
3755
3780
|
if (res2)
|
|
3756
3781
|
return res2;
|
|
@@ -3771,9 +3796,9 @@ function getConfig(configPath, fallbackSearchDirectory, opts) {
|
|
|
3771
3796
|
}
|
|
3772
3797
|
var _cachedConfig;
|
|
3773
3798
|
function searchConfig(directory, logger) {
|
|
3774
|
-
for (
|
|
3799
|
+
for (statSync7(directory).isFile() && (directory = dirname7(directory)), directory = directory.replaceAll("\\", "/"); directory.length > 0; ) {
|
|
3775
3800
|
let configPath = path6.join(directory, "needle.config.json");
|
|
3776
|
-
if (
|
|
3801
|
+
if (existsSync13(configPath)) {
|
|
3777
3802
|
let content = readFileSync9(configPath, "utf8"), config = resolveConfig(configPath, content, logger);
|
|
3778
3803
|
if (config)
|
|
3779
3804
|
return config;
|
|
@@ -3805,7 +3830,7 @@ function resolveConfig(filename, content, logger) {
|
|
|
3805
3830
|
}
|
|
3806
3831
|
|
|
3807
3832
|
// src/cli/index.ts
|
|
3808
|
-
import { existsSync as
|
|
3833
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11, readdirSync as readdirSync3, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
3809
3834
|
import path7, { basename as basename4, dirname as dirname8, resolve as resolve3 } from "path";
|
|
3810
3835
|
|
|
3811
3836
|
// src/constants.ts
|
|
@@ -3922,9 +3947,9 @@ var PROD_KEYS = [
|
|
|
3922
3947
|
var trusted = [...PROD_KEYS], TRUSTED_KEYS = trusted;
|
|
3923
3948
|
|
|
3924
3949
|
// src/auth/verify.ts
|
|
3925
|
-
import { readFileSync as readFileSync10, existsSync as
|
|
3926
|
-
import { join as
|
|
3927
|
-
import { homedir } from "os";
|
|
3950
|
+
import { readFileSync as readFileSync10, existsSync as existsSync14 } from "fs";
|
|
3951
|
+
import { join as join9 } from "path";
|
|
3952
|
+
import { homedir as homedir2 } from "os";
|
|
3928
3953
|
var AuthError = class extends Error {
|
|
3929
3954
|
constructor(message) {
|
|
3930
3955
|
super(message), this.name = "AuthError";
|
|
@@ -3932,29 +3957,32 @@ var AuthError = class extends Error {
|
|
|
3932
3957
|
}, LOCAL_FETCH_TIMEOUT_MS = 2e3;
|
|
3933
3958
|
function getLocalServerToken() {
|
|
3934
3959
|
try {
|
|
3935
|
-
let file =
|
|
3936
|
-
if (!
|
|
3960
|
+
let file = join9(homedir2(), ".needle", "local-server-token");
|
|
3961
|
+
if (!existsSync14(file)) return null;
|
|
3937
3962
|
let data = JSON.parse(readFileSync10(file, "utf-8"));
|
|
3938
3963
|
return typeof data.token == "string" ? data.token : null;
|
|
3939
|
-
} catch {
|
|
3940
|
-
return null;
|
|
3964
|
+
} catch (err) {
|
|
3965
|
+
return logToFile(`[auth] failed reading local-server-token (${err instanceof Error ? err.name : "unknown error"})`), null;
|
|
3941
3966
|
}
|
|
3942
3967
|
}
|
|
3943
3968
|
async function tryFetchLocalToken(org) {
|
|
3969
|
+
let baseUrl = process.env.NEEDLE_LICENSE_SERVER_URL || "http://localhost:8424/api/license", safeServer = "local license server";
|
|
3944
3970
|
try {
|
|
3945
|
-
let
|
|
3946
|
-
org && url.searchParams.set("org", org)
|
|
3971
|
+
let url = new URL(baseUrl);
|
|
3972
|
+
org && url.searchParams.set("org", org), safeServer = `${url.origin}${url.pathname}`;
|
|
3947
3973
|
let headers = {}, serverToken = getLocalServerToken();
|
|
3948
3974
|
serverToken && (headers.Authorization = `Bearer ${serverToken}`);
|
|
3949
3975
|
let res = await fetch(url, {
|
|
3950
3976
|
signal: AbortSignal.timeout(LOCAL_FETCH_TIMEOUT_MS),
|
|
3951
3977
|
headers
|
|
3952
3978
|
});
|
|
3953
|
-
if (!res.ok)
|
|
3979
|
+
if (!res.ok)
|
|
3980
|
+
return logToFile(`[auth] license server (${safeServer}) returned HTTP ${res.status} ${res.statusText}${org ? ` \u2014 verify the --org value "${org}"` : ""}`), null;
|
|
3954
3981
|
let jwt = (await res.json()).needle_license_jwt;
|
|
3955
|
-
return typeof jwt == "string" && jwt.length > 0 ? jwt : null;
|
|
3956
|
-
} catch {
|
|
3957
|
-
|
|
3982
|
+
return typeof jwt == "string" && jwt.length > 0 ? jwt : (logToFile(`[auth] license server (${safeServer}) responded without a license token${org ? ` for org "${org}"` : ""}`), null);
|
|
3983
|
+
} catch (err) {
|
|
3984
|
+
let msg = err instanceof Error ? err.message : String(err);
|
|
3985
|
+
return logToFile(`[auth] could not reach license server (${safeServer})${org ? ` for org "${org}"` : ""}: ${msg}`), null;
|
|
3958
3986
|
}
|
|
3959
3987
|
}
|
|
3960
3988
|
async function verifyWithKey(token, key) {
|
|
@@ -4019,7 +4047,7 @@ async function gateAuth(authToken, org, logger) {
|
|
|
4019
4047
|
await requireAuth(authToken, typeof org == "string" ? org : void 0);
|
|
4020
4048
|
} catch (err) {
|
|
4021
4049
|
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);
|
|
4050
|
+
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
4051
|
}
|
|
4024
4052
|
}
|
|
4025
4053
|
program.cast(!1);
|
|
@@ -4073,7 +4101,7 @@ When caching is enabled then previously processed files will be detected and loa
|
|
|
4073
4101
|
}
|
|
4074
4102
|
let set = /* @__PURE__ */ new Set(), useCache = options.cache !== !1, debug = options.debug === !0, verbose = options.verbose === !0;
|
|
4075
4103
|
await foreachGLTF(inputPaths, async (file) => {
|
|
4076
|
-
if (
|
|
4104
|
+
if (existsSync15(file)) {
|
|
4077
4105
|
if (file != input && options.progressive === !0 && isLOD(file)) {
|
|
4078
4106
|
logger.debug(`\u2192 Ignore existing LOD file at ${file}`);
|
|
4079
4107
|
return;
|
|
@@ -4089,12 +4117,12 @@ When caching is enabled then previously processed files will be detected and loa
|
|
|
4089
4117
|
config
|
|
4090
4118
|
}, file) : void 0;
|
|
4091
4119
|
if (jobKey) {
|
|
4092
|
-
let outputDir = output ?
|
|
4120
|
+
let outputDir = output ? existsSync15(output) && isDirectory(output) ? output : dirname8(output) : dirname8(file), restored = tryRestoreJobResult(jobKey, outputDir, logger);
|
|
4093
4121
|
if (restored) {
|
|
4094
|
-
let inputFileSize =
|
|
4122
|
+
let inputFileSize = statSync8(file).size / 1024 / 1024;
|
|
4095
4123
|
for (let f of restored)
|
|
4096
4124
|
if (set.add(f), stats) {
|
|
4097
|
-
let sizeInMB =
|
|
4125
|
+
let sizeInMB = statSync8(f).size / 1024 / 1024;
|
|
4098
4126
|
stats.totalFilesProcessed++, stats.totalFileSizeInMB += sizeInMB, stats.totalFileSizeInMBBefore += inputFileSize;
|
|
4099
4127
|
}
|
|
4100
4128
|
logger.info(`\u2192 [CACHE] Restored ${restored.length} file(s) for job ${jobKey}`);
|
|
@@ -4165,7 +4193,7 @@ raw .exr / .hdr inputs.
|
|
|
4165
4193
|
`).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
4194
|
warnUnknownOptions(options, logger), await gateAuth(options.authToken, options.org, logger), logger.level = options.debug || options.verbose ? "debug" : "info", await ensureHashReady();
|
|
4167
4195
|
let input = args.input.toString();
|
|
4168
|
-
|
|
4196
|
+
existsSync15(input) || (logger.error(`Input does not exist: ${input}`), process.exit(ERROR_CODES.INVALID_ARGS));
|
|
4169
4197
|
let inputFiles = [], inputIsDirectory = isDirectory(input);
|
|
4170
4198
|
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
4199
|
logger.warn(`No .exr/.hdr files found in ${input}`);
|
|
@@ -4204,7 +4232,7 @@ function collectPmremInputFiles(dir, out) {
|
|
|
4204
4232
|
for (let entry of readdirSync3(dir)) {
|
|
4205
4233
|
if (entry === "node_modules" || entry.startsWith(".")) continue;
|
|
4206
4234
|
let full = path7.join(dir, entry);
|
|
4207
|
-
|
|
4235
|
+
statSync8(full).isDirectory() ? collectPmremInputFiles(full, out) : isPmremInputFile(entry) && out.push(full);
|
|
4208
4236
|
}
|
|
4209
4237
|
}
|
|
4210
4238
|
function resolvePmremOutputPath(srcPath, input, inputIsDirectory, outputArg) {
|
|
@@ -4215,7 +4243,7 @@ function resolvePmremOutputPath(srcPath, input, inputIsDirectory, outputArg) {
|
|
|
4215
4243
|
let rel = path7.relative(input, srcPath), relDir = dirname8(rel), outDir = relDir === "." ? outputArg : path7.join(outputArg, relDir);
|
|
4216
4244
|
return ensureIsDirectory(outDir), path7.join(outDir, outName);
|
|
4217
4245
|
}
|
|
4218
|
-
return
|
|
4246
|
+
return existsSync15(outputArg) && isDirectory(outputArg) ? path7.join(outputArg, outName) : outputArg.toLowerCase().endsWith(".ktx2") ? (ensureIsDirectory(dirname8(outputArg)), outputArg) : (ensureIsDirectory(outputArg), path7.join(outputArg, outName));
|
|
4219
4247
|
}
|
|
4220
4248
|
program.run().catch((err) => {
|
|
4221
4249
|
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-beta",
|
|
3
|
+
"version": "3.0.0-beta.1.1b06064",
|
|
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": {
|