@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 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 : join6(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 existsSync12, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
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), !existsSync12(configPath))
3776
+ if (configPath = resolve2(process.cwd(), configPath), !existsSync13(configPath))
3752
3777
  throw new Error(`No config found at "${configPath}"`);
3753
- if (statSync6(configPath).isDirectory()) {
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 (statSync6(directory).isFile() && (directory = dirname7(directory)), directory = directory.replaceAll("\\", "/"); directory.length > 0; ) {
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 (existsSync12(configPath)) {
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 existsSync14, readFileSync as readFileSync11, readdirSync as readdirSync3, statSync as statSync7, writeFileSync as writeFileSync7 } from "fs";
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 existsSync13 } from "fs";
3926
- import { join as join8 } from "path";
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 = join8(homedir(), ".needle", "local-server-token");
3936
- if (!existsSync13(file)) return null;
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 baseUrl = process.env.NEEDLE_LICENSE_SERVER_URL || "http://localhost:8424/api/license", url = new URL(baseUrl);
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) return null;
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
- return null;
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 (existsSync14(file)) {
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 ? existsSync14(output) && isDirectory(output) ? output : dirname8(output) : dirname8(file), restored = tryRestoreJobResult(jobKey, outputDir, logger);
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 = statSync7(file).size / 1024 / 1024;
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 = statSync7(f).size / 1024 / 1024;
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
- existsSync14(input) || (logger.error(`Input does not exist: ${input}`), process.exit(ERROR_CODES.INVALID_ARGS));
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
- statSync7(full).isDirectory() ? collectPmremInputFiles(full, out) : isPmremInputFile(entry) && out.push(full);
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 existsSync14(outputArg) && isDirectory(outputArg) ? path7.join(outputArg, outName) : outputArg.toLowerCase().endsWith(".ktx2") ? (ensureIsDirectory(dirname8(outputArg)), outputArg) : (ensureIsDirectory(outputArg), path7.join(outputArg, outName));
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": {