@needle-tools/engine 4.14.0 → 4.15.0-next.f391a30

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.
Files changed (198) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/components.needle.json +1 -1
  3. package/dist/{gltf-progressive-BttGBXw6.umd.cjs → gltf-progressive-CMwJPwEt.umd.cjs} +1 -1
  4. package/dist/{gltf-progressive-Bm_6aEi4.js → gltf-progressive-CTlvpS3A.js} +1 -1
  5. package/dist/{gltf-progressive-T5WKTux5.min.js → gltf-progressive-DYL3SLVb.min.js} +1 -1
  6. package/dist/materialx-4jJLLe9Q.js +4174 -0
  7. package/dist/materialx-Bt9FHwco.min.js +158 -0
  8. package/dist/materialx-NDD0y4JY.umd.cjs +158 -0
  9. package/dist/{needle-engine.bundle-COL2Bar3.umd.cjs → needle-engine.bundle-C1BFRZDF.umd.cjs} +150 -140
  10. package/dist/{needle-engine.bundle-Z_gAD7Kg.js → needle-engine.bundle-DB4kLWO_.js} +6651 -6400
  11. package/dist/{needle-engine.bundle-NolzHLqO.min.js → needle-engine.bundle-DsTdfmeb.min.js} +151 -141
  12. package/dist/needle-engine.d.ts +345 -88
  13. package/dist/needle-engine.js +322 -322
  14. package/dist/needle-engine.min.js +1 -1
  15. package/dist/needle-engine.umd.cjs +1 -1
  16. package/dist/{postprocessing-06AXuvdv.min.js → postprocessing-BN-f4viE.min.js} +1 -1
  17. package/dist/{postprocessing-CPDcA21P.umd.cjs → postprocessing-DYmYOVm4.umd.cjs} +1 -1
  18. package/dist/{postprocessing-CI2x8Cln.js → postprocessing-De9ZpJrk.js} +1 -1
  19. package/dist/{three-examples-BMmNgNCN.umd.cjs → three-examples-BHqRVpO_.umd.cjs} +12 -12
  20. package/dist/{three-examples-CMYCd5nH.js → three-examples-C0ZCCA_K.js} +182 -192
  21. package/dist/{three-examples-CQl1fFZp.min.js → three-examples-DmTY8tGr.min.js} +14 -14
  22. package/lib/engine/api.d.ts +0 -2
  23. package/lib/engine/api.js +0 -2
  24. package/lib/engine/api.js.map +1 -1
  25. package/lib/engine/debug/debug.js +1 -1
  26. package/lib/engine/debug/debug.js.map +1 -1
  27. package/lib/engine/debug/debug_spatial_console.js +1 -1
  28. package/lib/engine/debug/debug_spatial_console.js.map +1 -1
  29. package/lib/engine/engine_accessibility.d.ts +77 -0
  30. package/lib/engine/engine_accessibility.js +162 -0
  31. package/lib/engine/engine_accessibility.js.map +1 -0
  32. package/lib/engine/engine_context.d.ts +2 -0
  33. package/lib/engine/engine_context.js +8 -1
  34. package/lib/engine/engine_context.js.map +1 -1
  35. package/lib/engine/engine_create_objects.js +1 -1
  36. package/lib/engine/engine_create_objects.js.map +1 -1
  37. package/lib/engine/engine_gizmos.js +1 -1
  38. package/lib/engine/engine_gizmos.js.map +1 -1
  39. package/lib/engine/engine_license.js +7 -2
  40. package/lib/engine/engine_license.js.map +1 -1
  41. package/lib/engine/engine_materialpropertyblock.d.ts +90 -4
  42. package/lib/engine/engine_materialpropertyblock.js +97 -7
  43. package/lib/engine/engine_materialpropertyblock.js.map +1 -1
  44. package/lib/engine/engine_math.d.ts +34 -1
  45. package/lib/engine/engine_math.js +34 -1
  46. package/lib/engine/engine_math.js.map +1 -1
  47. package/lib/engine/engine_networking.js +1 -1
  48. package/lib/engine/engine_networking.js.map +1 -1
  49. package/lib/engine/engine_types.d.ts +2 -0
  50. package/lib/engine/engine_types.js +2 -0
  51. package/lib/engine/engine_types.js.map +1 -1
  52. package/lib/engine/engine_utils.js +2 -2
  53. package/lib/engine/engine_utils.js.map +1 -1
  54. package/lib/engine/export/gltf/EXT_mesh_gpu_instancing_exporter.js.map +1 -0
  55. package/lib/engine/export/gltf/index.js +1 -1
  56. package/lib/engine/export/gltf/index.js.map +1 -1
  57. package/lib/engine/webcomponents/icons.js +3 -0
  58. package/lib/engine/webcomponents/icons.js.map +1 -1
  59. package/lib/engine/webcomponents/logo-element.d.ts +7 -3
  60. package/lib/engine/webcomponents/logo-element.js +21 -1
  61. package/lib/engine/webcomponents/logo-element.js.map +1 -1
  62. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +2 -2
  63. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js.map +1 -1
  64. package/lib/engine/webcomponents/needle menu/needle-menu.d.ts +10 -7
  65. package/lib/engine/webcomponents/needle menu/needle-menu.js +14 -4
  66. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  67. package/lib/engine/webcomponents/needle-button.d.ts +37 -11
  68. package/lib/engine/webcomponents/needle-button.js +42 -11
  69. package/lib/engine/webcomponents/needle-button.js.map +1 -1
  70. package/lib/engine/webcomponents/needle-engine.ar-overlay.js +10 -1
  71. package/lib/engine/webcomponents/needle-engine.ar-overlay.js.map +1 -1
  72. package/lib/engine/webcomponents/needle-engine.d.ts +13 -2
  73. package/lib/engine/webcomponents/needle-engine.js +23 -3
  74. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  75. package/lib/engine-components/Component.d.ts +1 -2
  76. package/lib/engine-components/Component.js +1 -3
  77. package/lib/engine-components/Component.js.map +1 -1
  78. package/lib/engine-components/DragControls.d.ts +1 -0
  79. package/lib/engine-components/DragControls.js +21 -0
  80. package/lib/engine-components/DragControls.js.map +1 -1
  81. package/lib/engine-components/NeedleMenu.d.ts +2 -0
  82. package/lib/engine-components/NeedleMenu.js +2 -0
  83. package/lib/engine-components/NeedleMenu.js.map +1 -1
  84. package/lib/engine-components/Networking.d.ts +28 -3
  85. package/lib/engine-components/Networking.js +28 -3
  86. package/lib/engine-components/Networking.js.map +1 -1
  87. package/lib/engine-components/ReflectionProbe.d.ts +25 -2
  88. package/lib/engine-components/ReflectionProbe.js +46 -2
  89. package/lib/engine-components/ReflectionProbe.js.map +1 -1
  90. package/lib/engine-components/Skybox.js +4 -2
  91. package/lib/engine-components/Skybox.js.map +1 -1
  92. package/lib/engine-components/export/gltf/GltfExport.js +1 -1
  93. package/lib/engine-components/export/gltf/GltfExport.js.map +1 -1
  94. package/lib/engine-components/export/usdz/ThreeUSDZExporter.js +2 -2
  95. package/lib/engine-components/export/usdz/USDZExporter.js +1 -1
  96. package/lib/engine-components/export/usdz/USDZExporter.js.map +1 -1
  97. package/lib/engine-components/export/usdz/extensions/behavior/BehaviourComponents.d.ts +15 -0
  98. package/lib/engine-components/export/usdz/extensions/behavior/BehaviourComponents.js +77 -0
  99. package/lib/engine-components/export/usdz/extensions/behavior/BehaviourComponents.js.map +1 -1
  100. package/lib/engine-components/export/usdz/extensions/behavior/PhysicsExtension.js +2 -2
  101. package/lib/engine-components/export/usdz/extensions/behavior/PhysicsExtension.js.map +1 -1
  102. package/lib/engine-components/postprocessing/Effects/Tonemapping.utils.d.ts +1 -1
  103. package/lib/engine-components/ui/Button.d.ts +1 -0
  104. package/lib/engine-components/ui/Button.js +11 -0
  105. package/lib/engine-components/ui/Button.js.map +1 -1
  106. package/lib/engine-components/ui/Text.d.ts +1 -0
  107. package/lib/engine-components/ui/Text.js +11 -0
  108. package/lib/engine-components/ui/Text.js.map +1 -1
  109. package/package.json +18 -14
  110. package/plugins/common/buildinfo.js +46 -10
  111. package/plugins/common/files.js +2 -1
  112. package/plugins/common/license.js +144 -69
  113. package/plugins/common/logger.js +172 -11
  114. package/plugins/common/needle-engine-skill.md +175 -0
  115. package/plugins/common/worker.js +5 -4
  116. package/plugins/types/userconfig.d.ts +40 -2
  117. package/plugins/vite/ai.js +71 -0
  118. package/plugins/vite/alias.js +6 -5
  119. package/plugins/vite/asap.js +6 -5
  120. package/plugins/vite/build-pipeline.js +224 -41
  121. package/plugins/vite/buildinfo.js +66 -6
  122. package/plugins/vite/copyfiles.js +41 -12
  123. package/plugins/vite/custom-element-data.js +26 -16
  124. package/plugins/vite/defines.js +8 -5
  125. package/plugins/vite/dependencies.js +16 -10
  126. package/plugins/vite/dependency-watcher.js +35 -7
  127. package/plugins/vite/drop-client.js +7 -5
  128. package/plugins/vite/drop.js +16 -14
  129. package/plugins/vite/editor-connection.js +18 -16
  130. package/plugins/vite/imports-logger.js +12 -2
  131. package/plugins/vite/index.js +8 -3
  132. package/plugins/vite/local-files-analysis.js +789 -0
  133. package/plugins/vite/local-files-core.js +992 -0
  134. package/plugins/vite/local-files-internals.js +28 -0
  135. package/plugins/vite/local-files-types.d.ts +111 -0
  136. package/plugins/vite/local-files-utils.js +359 -0
  137. package/plugins/vite/local-files.js +2 -441
  138. package/plugins/vite/logger.client.js +45 -35
  139. package/plugins/vite/logger.js +6 -3
  140. package/plugins/vite/logging.js +129 -0
  141. package/plugins/vite/meta.js +18 -4
  142. package/plugins/vite/needle-app.js +4 -3
  143. package/plugins/vite/peer.js +2 -1
  144. package/plugins/vite/pwa.js +33 -17
  145. package/plugins/vite/reload.js +24 -2
  146. package/src/engine/api.ts +0 -3
  147. package/src/engine/debug/debug.ts +1 -1
  148. package/src/engine/debug/debug_spatial_console.ts +5 -1
  149. package/src/engine/engine_accessibility.ts +198 -0
  150. package/src/engine/engine_context.ts +10 -1
  151. package/src/engine/engine_create_objects.ts +1 -1
  152. package/src/engine/engine_gizmos.ts +9 -5
  153. package/src/engine/engine_license.ts +7 -2
  154. package/src/engine/engine_materialpropertyblock.ts +102 -11
  155. package/src/engine/engine_math.ts +34 -1
  156. package/src/engine/engine_networking.ts +1 -1
  157. package/src/engine/engine_types.ts +5 -0
  158. package/src/engine/engine_utils.ts +2 -2
  159. package/src/engine/export/gltf/index.ts +1 -1
  160. package/src/engine/webcomponents/icons.ts +3 -0
  161. package/src/engine/webcomponents/logo-element.ts +24 -4
  162. package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +6 -2
  163. package/src/engine/webcomponents/needle menu/needle-menu.ts +23 -11
  164. package/src/engine/webcomponents/needle-button.ts +44 -13
  165. package/src/engine/webcomponents/needle-engine.ar-overlay.ts +13 -2
  166. package/src/engine/webcomponents/needle-engine.ts +31 -8
  167. package/src/engine-components/Component.ts +2 -5
  168. package/src/engine-components/DragControls.ts +29 -4
  169. package/src/engine-components/NeedleMenu.ts +5 -3
  170. package/src/engine-components/Networking.ts +29 -4
  171. package/src/engine-components/ReflectionProbe.ts +52 -9
  172. package/src/engine-components/Skybox.ts +4 -2
  173. package/src/engine-components/export/gltf/GltfExport.ts +1 -1
  174. package/src/engine-components/export/usdz/ThreeUSDZExporter.ts +2 -2
  175. package/src/engine-components/export/usdz/USDZExporter.ts +1 -1
  176. package/src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +108 -32
  177. package/src/engine-components/export/usdz/extensions/behavior/PhysicsExtension.ts +2 -2
  178. package/src/engine-components/ui/Button.ts +12 -0
  179. package/src/engine-components/ui/Text.ts +13 -0
  180. package/dist/materialx-CJyQZtjt.min.js +0 -90
  181. package/dist/materialx-DMs1E08Z.js +0 -4636
  182. package/dist/materialx-DaKKOoVk.umd.cjs +0 -90
  183. package/lib/engine/engine_test_utils.d.ts +0 -39
  184. package/lib/engine/engine_test_utils.js +0 -84
  185. package/lib/engine/engine_test_utils.js.map +0 -1
  186. package/lib/include/three/EXT_mesh_gpu_instancing_exporter.js.map +0 -1
  187. package/src/engine/engine_test_utils.ts +0 -109
  188. package/src/include/draco/draco_decoder.js +0 -34
  189. package/src/include/draco/draco_decoder.wasm +0 -0
  190. package/src/include/draco/draco_wasm_wrapper.js +0 -117
  191. package/src/include/ktx2/basis_transcoder.js +0 -19
  192. package/src/include/ktx2/basis_transcoder.wasm +0 -0
  193. package/src/include/needle/arial-msdf.json +0 -1472
  194. package/src/include/needle/arial.png +0 -0
  195. package/src/include/needle/poweredbyneedle.webp +0 -0
  196. /package/lib/{include/three → engine/export/gltf}/EXT_mesh_gpu_instancing_exporter.d.ts +0 -0
  197. /package/lib/{include/three → engine/export/gltf}/EXT_mesh_gpu_instancing_exporter.js +0 -0
  198. /package/src/{include/three → engine/export/gltf}/EXT_mesh_gpu_instancing_exporter.js +0 -0
@@ -3,6 +3,7 @@ import path from 'path';
3
3
  import { tryParseNeedleEngineSrcAttributeFromHtml } from '../common/needle-engine.js';
4
4
  import { preloadScriptPaths } from './dependencies.js';
5
5
  import { makeFilesLocalIsEnabled } from './local-files.js';
6
+ import { needleLog } from './logging.js';
6
7
 
7
8
  const code = `import('@needle-tools/engine/src/asap/needle-asap.ts');`
8
9
 
@@ -30,7 +31,7 @@ export const needleAsap = async (command, config, userSettings) => {
30
31
  logoSvg = assets.NEEDLE_LOGO_SVG_URL;
31
32
  }
32
33
  catch (err) {
33
- console.warn("Could not load needle logo svg", err.message);
34
+ needleLog("needle:asap", "Could not load needle logo svg: " + err.message, "warn", { dimBody: false });
34
35
  }
35
36
 
36
37
  /** @type {import("vite").ResolvedConfig | null} */
@@ -122,7 +123,7 @@ function fixMainTs() {
122
123
  if (existsSync(mainTsFilePath)) {
123
124
  let code = readFileSync(mainTsFilePath, 'utf-8');
124
125
  if (code.includes('import \"@needle-tools/engine\"')) {
125
- console.log("Change main.ts and replace needle engine import with async import");
126
+ needleLog("needle:asap", "Changed main.ts and replaced needle engine import with async import", "log", { dimBody: false });
126
127
  code = code.replace(/import \"@needle-tools\/engine\"/g, 'import("@needle-tools/engine") /* async import of needle engine */');
127
128
  writeFileSync(mainTsFilePath, code);
128
129
  }
@@ -151,7 +152,7 @@ function generateScriptPreloadLinks(_config, tags) {
151
152
  }
152
153
  }
153
154
  catch (err) {
154
- console.error("Error generating script preload links", err);
155
+ needleLog("needle:asap", "Error generating script preload links: " + err.message, "error", { dimBody: false });
155
156
  }
156
157
  }
157
158
 
@@ -201,11 +202,11 @@ function generateGltfPreloadLinks(config, html, tags) {
201
202
  filepath = decodeURIComponent(filepath);
202
203
  const fullpath = path.join(process.cwd(), filepath);
203
204
  if (!existsSync(fullpath)) {
204
- console.warn(`[needle:asap] Could not insert head preload link: file not found at \"${filepath}\"`);
205
+ needleLog("needle:asap", `Could not insert head preload link: file not found at \"${filepath}\"`, "warn", { dimBody: false });
205
206
  continue;
206
207
  }
207
208
  }
208
- console.log(`[needle:asap] Insert head glTF preload link: ${value}`);
209
+ needleLog("needle:asap", `Insert head glTF preload link: ${value}`);
209
210
  insertPreloadLink(tags, value, "model/gltf+json");
210
211
  }
211
212
  }
@@ -1,9 +1,14 @@
1
1
  import { ChildProcess, exec } from 'child_process';
2
2
  import { NEEDLE_CLOUD_CLI_NAME } from '../common/cloud.js';
3
3
  import { getOutputDirectory, loadConfig } from './config.js';
4
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs';
4
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'fs';
5
+ import { relative } from 'path';
5
6
  import { copyFilesSync } from '../common/files.js';
6
7
  import { delay } from '../common/timers.js';
8
+ import { needleBlue, needleDim, needleLog, needleSupportsColor, setTransientLogLineCleaner } from './logging.js';
9
+
10
+ const PIPELINE_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
11
+ const PIPELINE_STRUCTURED_LOG_PREFIX = "__needle_pipeline_log__:";
7
12
 
8
13
  /**
9
14
  * @param {import('../types').userSettings} config
@@ -35,10 +40,12 @@ env:
35
40
  // see https://linear.app/needle/issue/NE-3798
36
41
 
37
42
 
38
- /** @type {Promise<any>|null} */
43
+ /** @type {Promise<void>|null} */
39
44
  let buildPipelineTask;
40
45
  /** @type {null | {tempDirectory:string, outputDirectory:string}} */
41
46
  let buildPipelineTaskResults = null;
47
+ /** @type {null | string} */
48
+ let buildPipelineStepSummary = null;
42
49
 
43
50
  export function waitForBuildPipelineToFinish() {
44
51
  return buildPipelineTask;
@@ -100,7 +107,9 @@ export const needleBuildPipeline = async (command, config, userSettings) => {
100
107
  }
101
108
 
102
109
  if (!shouldRun) {
103
- log("Skipping build pipeline because this is a development build.\n- Invoke with `--production` to run the build pipeline.\n- For example \"vite build -- --production\".");
110
+ if (command === "build") {
111
+ log("Skipping build pipeline because this is a development build.\n- Invoke with `--production` to run the build pipeline.\n- For example \"vite build -- --production\".");
112
+ }
104
113
  await new Promise((resolve, _) => setTimeout(resolve, 1000));
105
114
  return null;
106
115
  }
@@ -113,6 +122,7 @@ export const needleBuildPipeline = async (command, config, userSettings) => {
113
122
 
114
123
  const verboseOutput = userSettings?.buildPipeline?.verbose || false;
115
124
  let taskHasCompleted = false;
125
+ let taskSucceeded = false;
116
126
 
117
127
  return {
118
128
  name: 'needle:buildpipeline',
@@ -148,26 +158,22 @@ export const needleBuildPipeline = async (command, config, userSettings) => {
148
158
  log("Build pipeline already running...");
149
159
  return;
150
160
  }
151
- let taskSucceeded = false;
152
161
  // start the compression process once vite is done copying the files
153
162
  buildPipelineTask = invokeBuildPipeline(userSettings, { verbose: verboseOutput })
154
163
  .then((res) => {
155
164
  if (verboseOutput) log("Build pipeline task result:", res);
156
165
  taskSucceeded = res;
157
166
  })
158
- .catch(err => {
159
- console.error("[needle-buildpipeline] - Error during build pipeline: " + err.message);
167
+ .catch((/** @type {{ message?: string }} */ err) => {
168
+ needleLog("needle-buildpipeline", "- Error during build pipeline: " + err.message, "error");
160
169
  if (verboseOutput) log("Error details:", err);
161
170
  })
162
171
  .finally(() => {
163
172
  taskHasCompleted = true;
164
173
  if (!taskSucceeded) {
165
- console.error("[needle-buildpipeline] - Build pipeline task did not succeed.");
174
+ needleLog("needle-buildpipeline", "- Build pipeline task did not succeed.", "error");
166
175
  throw new Error("[needle-buildpipeline] - Build pipeline failed. Please check the logs above for more information.");
167
176
  }
168
- else {
169
- log("Finished successfully");
170
- }
171
177
  });
172
178
  }
173
179
  },
@@ -175,21 +181,34 @@ export const needleBuildPipeline = async (command, config, userSettings) => {
175
181
  if (!buildPipelineTask) {
176
182
  return;
177
183
  }
178
- if (!taskHasCompleted) {
179
- log("Waiting for build pipeline to finish...");
180
- }
181
184
  // // this is the last hook that is called, so we can wait for the task to finish here
182
185
  return buildPipelineTask = buildPipelineTask?.then(() => {
186
+ const lines = /** @type {string[]} */ ([]);
187
+ if (buildPipelineStepSummary) {
188
+ lines.push(buildPipelineStepSummary);
189
+ }
190
+ if (taskSucceeded) {
191
+ lines.push(needleDim("✓ Finished successfully"));
192
+ }
183
193
  // Copy the results to their final output directory.
184
194
  if (buildPipelineTaskResults != null) {
185
- log(`Copying files from temporary output directory to final output directory at \"${buildPipelineTaskResults.outputDirectory}\"`);
186
- const ctx = { count: 0 }
195
+ const supportsColor = needleSupportsColor();
196
+ const key = (/** @type {string} */ text) => supportsColor ? needleBlue(text) : text;
197
+ const outputPath = relative(process.cwd(), buildPipelineTaskResults.outputDirectory).replaceAll("\\", "/") || ".";
198
+ const moved = getDirectoryStats(buildPipelineTaskResults.tempDirectory);
199
+ lines.push(`${key("Copying files")}: \"${outputPath}\"`);
200
+ const ctx = { count: 0, bytes: 0 }
187
201
  copyFilesSync(buildPipelineTaskResults.tempDirectory, buildPipelineTaskResults.outputDirectory, true, ctx);
188
- log(`Copied ${ctx.count} file(s)`);
202
+ lines.push(`${key("Copied files")}: ${ctx.count}`);
203
+ lines.push(`${key("Data moved")}: ${formatBytes(moved.totalBytes)} (${moved.fileCount} files)`);
189
204
  }
190
205
  else {
191
- log("No files to copy - build pipeline did not run or did not finish successfully");
206
+ lines.push("No files to copy - build pipeline did not run or did not finish successfully");
192
207
  }
208
+ if (lines.length > 0) {
209
+ needleLog("needle-buildpipeline", lines.join("\n"), "log", { dimBody: false });
210
+ }
211
+ buildPipelineStepSummary = null;
193
212
  });
194
213
  },
195
214
  }
@@ -216,21 +235,31 @@ async function fixPackageJson(packageJsonPath) {
216
235
  writeFileSync(packageJsonPath, fixed);
217
236
  }
218
237
 
219
- /** @param {any} args */
238
+ /** @param {...unknown} args */
220
239
  function log(...args) {
221
- console.log("[needle-buildpipeline]", ...args);
240
+ needleLog("needle-buildpipeline", args.join(" "));
222
241
  }
223
- /** @param {any} args */
242
+ /** @param {...unknown} args */
224
243
  function warn(...args) {
225
- console.warn("WARN: [needle-buildpipeline]", ...args);
244
+ needleLog("needle-buildpipeline", args.join(" "), "warn");
226
245
  }
227
246
 
247
+ /**
248
+ * @typedef {{ event?: string, phase?: string, target?: string, message?: string, level?: string }} BuildPipelinePayload
249
+ */
228
250
  /**
229
251
  * @param {import('../types').userSettings} opts
230
252
  * @param {{verbose?:boolean}} [options]
231
253
  * @returns {Promise<boolean>}
232
254
  */
233
255
  async function invokeBuildPipeline(opts, options = {}) {
256
+ const rel = (/** @type {string} */ pathValue) => {
257
+ const value = relative(process.cwd(), pathValue).replaceAll("\\", "/");
258
+ return value?.length ? value : ".";
259
+ };
260
+ const supportsColor = needleSupportsColor();
261
+ const key = (/** @type {string} */ text) => supportsColor ? needleBlue(text) : text;
262
+
234
263
  const installPath = "node_modules/@needle-tools/gltf-build-pipeline";
235
264
  const fullInstallPath = process.cwd() + "/" + installPath;
236
265
  const existsLocally = existsSync(fullInstallPath);
@@ -257,7 +286,7 @@ async function invokeBuildPipeline(opts, options = {}) {
257
286
  log("Max wait time exceeded - aborting...");
258
287
  return Promise.resolve(false);
259
288
  }
260
- if (iteration <= 0) log(`Waiting for output directory to be created... (${outputDirectory})`);
289
+ if (iteration <= 0) needleLog("needle-buildpipeline", `Waiting for output directory to be created... (${outputDirectory})`, "log", { leadingNewline: true });
261
290
  return delay(1000).then(() => waitForOutputDirectory(iteration + 1));
262
291
  }
263
292
  if (options?.verbose) log(`Output directory found after ${iteration} iteration(s) at "${outputDirectory}" - continuing...`);
@@ -268,7 +297,19 @@ async function invokeBuildPipeline(opts, options = {}) {
268
297
  return false;
269
298
  }
270
299
  const files = readdirSync(outputDirectory).filter(f => f.endsWith(".glb") || f.endsWith(".gltf") || f.endsWith(".vrm") || f.endsWith(".fbx"));
271
- log(`${files.length} file(s) to process in ${outputDirectory}`);
300
+ const filesBytes = files.reduce((total, file) => {
301
+ try {
302
+ return total + statSync(outputDirectory + "/" + file).size;
303
+ }
304
+ catch {
305
+ return total;
306
+ }
307
+ }, 0);
308
+ needleLog("needle-buildpipeline", [
309
+ `${key("Files to process")}: ${files.length} in ${rel(outputDirectory)}`,
310
+ `${key("Input size")}: ${formatBytes(filesBytes)}`,
311
+ existsSync(process.cwd() + "/node_modules/.needle/build-pipeline/output") ? needleDim("Removing temporary output directory") : undefined,
312
+ ].filter(Boolean), "log", { dimBody: false });
272
313
 
273
314
  /** @type {null | ChildProcess} */
274
315
  let proc = null;
@@ -290,7 +331,6 @@ async function invokeBuildPipeline(opts, options = {}) {
290
331
  // this is so that processes like sveltekit-static-adapter can run first and does not override already compressed files
291
332
  const tempOutputPath = process.cwd() + "/node_modules/.needle/build-pipeline/output";
292
333
  if (existsSync(tempOutputPath)) {
293
- log("Removing temporary output directory at " + tempOutputPath);
294
334
  rmSync(tempOutputPath, { recursive: true, force: true });
295
335
  }
296
336
  mkdirSync(tempOutputPath, { recursive: true });
@@ -307,6 +347,7 @@ async function invokeBuildPipeline(opts, options = {}) {
307
347
 
308
348
  // allow running the build pipeline in the cloud. It requires and access token to be set in the vite.config.js
309
349
  // this can be set via e.g. process.env.NEEDLE_CLOUD_TOKEN
350
+ const commandEnv = { ...process.env, NEEDLE_PIPELINE_STRUCTURED_LOGS: "1" };
310
351
  if (runInCloud) {
311
352
  if (!cloudAccessToken || !(typeof cloudAccessToken === "string") || cloudAccessToken.length <= 0) {
312
353
  throw new Error("No cloud access token configured. Please set it via process.env.NEEDLE_CLOUD_TOKEN or in the vite.config.js");
@@ -328,12 +369,12 @@ async function invokeBuildPipeline(opts, options = {}) {
328
369
  console.log("\n");
329
370
  const obfuscatedToken = `${cloudAccessToken.slice(0, 2)}*****${cloudAccessToken.slice(-2)}`;
330
371
  log(`Running compression in cloud ⛅ using access token: ${obfuscatedToken}`);
331
- proc = exec(cmd);
372
+ proc = exec(cmd, { env: commandEnv });
332
373
  }
333
374
  else if (existsLocally) {
334
375
  const cmd = `needle-gltf transform "${outputDirectory}" \"${tempOutputPath}\"`;
335
376
  log("Running command \"" + cmd + "\" at " + process.cwd() + "...");
336
- proc = exec(cmd, { cwd: installPath });
377
+ proc = exec(cmd, { cwd: installPath, env: commandEnv });
337
378
  }
338
379
  else {
339
380
  // First check if the user passed in a specific version to use via the vite config
@@ -353,33 +394,140 @@ async function invokeBuildPipeline(opts, options = {}) {
353
394
 
354
395
  const cmd = `npx --yes @needle-tools/gltf-build-pipeline@${version} transform "${outputDirectory}" \"${tempOutputPath}\"`;
355
396
  log(`Running compression locally using version '${version}'`);
356
- proc = exec(cmd);
397
+ proc = exec(cmd, { env: commandEnv });
398
+ }
399
+ let pipelineSpinnerIndex = 0;
400
+ let pipelineSpinnerActive = false;
401
+ let transformStepCount = 0;
402
+ let compressStepCount = 0;
403
+
404
+ function clearPipelineProgress() {
405
+ if (!process.stdout.isTTY || !pipelineSpinnerActive) return;
406
+ process.stdout.write("\r\x1b[2K");
407
+ pipelineSpinnerActive = false;
408
+ }
409
+
410
+ /** @param {string} text */
411
+ function updatePipelineProgress(text) {
412
+ if (!process.stdout.isTTY) return;
413
+ const frame = PIPELINE_SPINNER_FRAMES[pipelineSpinnerIndex++ % PIPELINE_SPINNER_FRAMES.length];
414
+ const maxLength = Math.max(24, (process.stdout.columns || 120) - 4);
415
+ const value = text.length > maxLength ? `${text.slice(0, Math.max(0, maxLength - 1))}…` : text;
416
+ process.stdout.write(`\r\x1b[2K${frame} ${value}\x1b[0K`);
417
+ pipelineSpinnerActive = true;
357
418
  }
358
- /** @param {any} data */
419
+
420
+ setTransientLogLineCleaner(() => clearPipelineProgress());
421
+
422
+ /** @param {Buffer|string} data */
359
423
  function onLog(data) {
360
424
  if (data.length <= 0) return;
361
- // ensure that it doesnt end with a newline
362
- while (data.endsWith("\n")) data = data.slice(0, -1);
363
- if (typeof data === "string") {
364
- if (data.startsWith("ERR:")) {
365
- console.error(data);
366
- return;
425
+ const str = String(data).replace(/\r/g, "");
426
+ const lines = str.split("\n");
427
+ for (let line of lines) {
428
+ if (!line?.trim().length) continue;
429
+
430
+ if (line.startsWith(PIPELINE_STRUCTURED_LOG_PREFIX)) {
431
+ let payload = /** @type {BuildPipelinePayload | null} */ (null);
432
+ try {
433
+ payload = /** @type {BuildPipelinePayload} */ (JSON.parse(line.slice(PIPELINE_STRUCTURED_LOG_PREFIX.length)));
434
+ }
435
+ catch {
436
+ payload = null;
437
+ }
438
+ if (payload) {
439
+ if (payload.event === "progress") {
440
+ if (payload.phase === "transform") transformStepCount++;
441
+ if (payload.phase === "compress") compressStepCount++;
442
+ updatePipelineProgress(`Build pipeline ${payload.phase === "compress" ? "Compressing" : "Transform"} ${payload.target || payload.message || ""}`.trim());
443
+ continue;
444
+ }
445
+
446
+ clearPipelineProgress();
447
+
448
+ if (payload.event === "summary") {
449
+ needleLog("needle-buildpipeline", payload.message || "Build pipeline summary", "log", { showHeader: false, leadingNewline: true });
450
+ continue;
451
+ }
452
+
453
+ const level = String(payload.level || "info").toLowerCase();
454
+ const message = payload.message || line;
455
+ if (level === "error") {
456
+ needleLog("needle-buildpipeline", message, "error", { dimBody: false, showHeader: false, leadingNewline: true });
457
+ }
458
+ else if (level === "warn") {
459
+ needleLog("needle-buildpipeline", message, "warn", { dimBody: false, showHeader: false, leadingNewline: true });
460
+ }
461
+ else {
462
+ needleLog("needle-buildpipeline", message, "log", { showHeader: false, leadingNewline: true });
463
+ }
464
+ continue;
465
+ }
367
466
  }
368
- else if (data.startsWith("WARN:")) {
369
- console.warn(data);
370
- return;
467
+
468
+ if (line.startsWith("info: [Needle Build Pipeline]") || line.startsWith("info: No \"gltf\" config found") || line.startsWith("info: No config found") || line.startsWith("Limit cache size to ") || line.startsWith("Current cache size is ")) {
469
+ continue;
371
470
  }
372
- // Ignore empty lines
373
- else if (data.trim().length <= 0) {
374
- return;
471
+
472
+ if (line.startsWith("[NEEDLE_progressive] Skipping")) {
473
+ continue;
375
474
  }
475
+
476
+ if (line.startsWith("objc[") || line.includes("Class GNotificationCenterDelegate is implemented in both") || line.includes("This may cause spurious casting failures and mysterious crashes")) {
477
+ continue;
478
+ }
479
+
480
+ if (line.startsWith("INFO: Environment variable 'NEEDLE_TOKTX' not set")) {
481
+ continue;
482
+ }
483
+
484
+ if (line.startsWith("metalRough: KHR_materials_pbrSpecularGlossiness not found")) {
485
+ continue;
486
+ }
487
+
488
+ if (line.startsWith("WARN: Could not validate image type")) {
489
+ continue;
490
+ }
491
+
492
+ const progressMatch = line.match(/^info:\s*→\s*(Transform|Compressing)\s+(.+)$/i);
493
+ if (progressMatch) {
494
+ if (progressMatch[1].toLowerCase() === "transform") transformStepCount++;
495
+ if (progressMatch[1].toLowerCase() === "compressing") compressStepCount++;
496
+ updatePipelineProgress(`Build pipeline ${progressMatch[1]} ${progressMatch[2]}`);
497
+ continue;
498
+ }
499
+
500
+ if (line.startsWith("info: ← Writing to ") || line.startsWith("info: ← Compressing done in ")) {
501
+ continue;
502
+ }
503
+
504
+ clearPipelineProgress();
505
+
506
+ if (line.startsWith("ERR:")) {
507
+ needleLog("needle-buildpipeline", line, "error", { dimBody: false, showHeader: false, leadingNewline: true });
508
+ continue;
509
+ }
510
+ else if (line.startsWith("WARN:")) {
511
+ needleLog("needle-buildpipeline", line, "warn", { dimBody: false, showHeader: false, leadingNewline: true });
512
+ continue;
513
+ }
514
+
515
+ const shouldDim = line.includes("Loaded compressed file from cache");
516
+ needleLog("needle-buildpipeline", line, "log", { showHeader: false, leadingNewline: true, dimBody: shouldDim });
376
517
  }
377
- log(data);
378
518
  }
379
519
  proc.stdout?.on('data', onLog);
380
520
  proc.stderr?.on('data', onLog);
381
521
  return new Promise((resolve, reject) => {
382
522
  proc.on('exit', (code) => {
523
+ clearPipelineProgress();
524
+ setTransientLogLineCleaner(null);
525
+
526
+ if (transformStepCount > 0 || compressStepCount > 0) {
527
+ buildPipelineStepSummary = `✓ Pipeline steps: transformed ${transformStepCount} file(s), compressed ${compressStepCount} file(s)`;
528
+ }
529
+ else buildPipelineStepSummary = null;
530
+
383
531
  if (code === null || code === undefined) {
384
532
  if (options?.verbose) log("Process exited with no code - assuming success");
385
533
  code = 0;
@@ -391,3 +539,38 @@ async function invokeBuildPipeline(opts, options = {}) {
391
539
  });
392
540
  });
393
541
  }
542
+
543
+ /** @param {number | undefined} bytes */
544
+ function formatBytes(bytes) {
545
+ const value = Number(bytes || 0);
546
+ if (value < 1024) return `${value} B`;
547
+ if (value < 1024 * 1024) return `${(value / 1024).toFixed(2)} kB`;
548
+ if (value < 1024 * 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(2)} MB`;
549
+ return `${(value / (1024 * 1024 * 1024)).toFixed(2)} GB`;
550
+ }
551
+
552
+ /** @param {string | null | undefined} directory */
553
+ function getDirectoryStats(directory) {
554
+ if (!directory || !existsSync(directory)) return { fileCount: 0, totalBytes: 0 };
555
+ let fileCount = 0;
556
+ let totalBytes = 0;
557
+ const entries = readdirSync(directory, { withFileTypes: true });
558
+ for (const entry of entries) {
559
+ const path = directory + "/" + entry.name;
560
+ if (entry.isDirectory()) {
561
+ const stats = getDirectoryStats(path);
562
+ fileCount += stats.fileCount;
563
+ totalBytes += stats.totalBytes;
564
+ }
565
+ else {
566
+ try {
567
+ const stat = statSync(path);
568
+ fileCount += 1;
569
+ totalBytes += stat.size;
570
+ }
571
+ catch {
572
+ }
573
+ }
574
+ }
575
+ return { fileCount, totalBytes };
576
+ }
@@ -1,6 +1,8 @@
1
- import { createBuildInfoFile } from '../common/buildinfo.js';
1
+ import { collectBuildDirectoryStats, createBuildInfoFile } from '../common/buildinfo.js';
2
+ import { closeLogStreams } from '../common/logger.js';
2
3
  import { waitForBuildPipelineToFinish } from './build-pipeline.js';
3
4
  import { getOutputDirectory } from './config.js';
5
+ import { needleBlue, needleDim, needleLog, needleSupportsColor } from './logging.js';
4
6
 
5
7
  let level = 0;
6
8
 
@@ -21,22 +23,80 @@ export const needleBuildInfo = (command, config, userSettings) => {
21
23
  },
22
24
  closeBundle: async () => {
23
25
  if (--level > 0) {
24
- console.log("[needle-buildinfo] - Skipped because of nested build");
26
+ needleLog("needle-buildinfo", "Skipped because of nested build");
25
27
  return;
26
28
  }
29
+ const buildDirectory = getOutputDirectory();
30
+ const beforeStats = collectBuildDirectoryStats(buildDirectory);
27
31
  const task = waitForBuildPipelineToFinish();
28
32
  if (task instanceof Promise) {
29
- console.log("[needle-buildinfo] - Waiting for build pipeline to finish");
30
- await task.catch(() => { }).finally(() => console.log("[needle-buildinfo] - Build pipeline finished!"));
33
+ needleLog("needle-buildinfo", "Waiting for build pipeline to finish");
34
+ await task.catch(() => { });
31
35
  }
32
36
  // wait for gzip
33
37
  await delay(500);
34
- const buildDirectory = getOutputDirectory();
35
- createBuildInfoFile(buildDirectory);
38
+ const result = createBuildInfoFile(buildDirectory, { log: false });
39
+ const closedHandles = closeDanglingNetworkHandles();
40
+ if (!result?.ok) {
41
+ needleLog("needle-buildinfo", result?.error || "Failed to create build info file", "warn", { dimBody: false });
42
+ return;
43
+ }
44
+ const supportsColor = needleSupportsColor();
45
+ const key = (text) => supportsColor ? needleBlue(text) : text;
46
+ const sizeDelta = (result.totalSizeBytes || 0) - (beforeStats.totalSizeBytes || 0);
47
+ const fileDelta = (result.fileCount || 0) - (beforeStats.fileCount || 0);
48
+ const lines = [
49
+ "Build pipeline finished!",
50
+ `${key("Before pipeline")}: ${beforeStats.fileCount} files (${formatBytes(beforeStats.totalSizeBytes)})`,
51
+ `${key("After pipeline")}: ${result.fileCount} files (${formatBytes(result.totalSizeBytes)})`,
52
+ `${key("Delta")}: ${(sizeDelta >= 0 ? "+" : "-") + formatBytes(Math.abs(sizeDelta))}, ${(fileDelta >= 0 ? "+" : "") + fileDelta} files`,
53
+ needleDim(`Begin collecting files in \"${result.buildDirectory}\"`),
54
+ needleDim(`Collected ${result.fileCount} files (${result.totalSizeInMB.toFixed(2)} MB). Writing build info to \"${result.buildInfoPath}\"`),
55
+ needleDim(`Build info file successfully written to \"${result.buildInfoPath}\"`),
56
+ ];
57
+ if (closedHandles > 0) lines.push(`Closed ${closedHandles} dangling network handle(s)`);
58
+ needleLog("needle-buildinfo", lines.join("\n"), "log", { dimBody: false });
59
+ closeLogStreams();
36
60
  }
37
61
  }
38
62
  }
39
63
 
64
+ function formatBytes(bytes) {
65
+ const value = Number(bytes || 0);
66
+ if (value < 1024) return `${value} B`;
67
+ if (value < 1024 * 1024) return `${(value / 1024).toFixed(2)} kB`;
68
+ if (value < 1024 * 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(2)} MB`;
69
+ return `${(value / (1024 * 1024 * 1024)).toFixed(2)} GB`;
70
+ }
71
+
40
72
  function delay(ms) {
41
73
  return new Promise(res => setTimeout(res, ms));
74
+ }
75
+
76
+ function closeDanglingNetworkHandles() {
77
+ /** @type {() => any[] | undefined} */
78
+ // @ts-ignore private node api
79
+ const getActiveHandles = process._getActiveHandles;
80
+ if (typeof getActiveHandles !== "function") {
81
+ return 0;
82
+ }
83
+
84
+ let closed = 0;
85
+ const handles = getActiveHandles() || [];
86
+ for (const handle of handles) {
87
+ if (!handle || handle === process.stdin || handle === process.stdout || handle === process.stderr) continue;
88
+ const hasNetworkShape = typeof handle.destroy === "function" && (typeof handle.remotePort === "number" || typeof handle.remoteAddress === "string");
89
+ if (!hasNetworkShape) continue;
90
+
91
+ if (typeof handle.remotePort === "number" && handle.remotePort > 0) {
92
+ try {
93
+ handle.destroy();
94
+ closed++;
95
+ }
96
+ catch {
97
+ // ignore cleanup failures
98
+ }
99
+ }
100
+ }
101
+ return closed;
42
102
  }
@@ -1,8 +1,9 @@
1
1
 
2
- import { resolve, join, isAbsolute } from 'path'
2
+ import { resolve, join, isAbsolute, relative } from 'path'
3
3
  import { existsSync, statSync, mkdirSync, readdirSync, copyFileSync, mkdir, rmSync } from 'fs';
4
4
  import { builtAssetsDirectory, tryLoadProjectConfig } from './config.js';
5
5
  import { copyFilesSync } from '../common/files.js';
6
+ import { needleBlue, needleDim, needleLog, needleSupportsColor } from './logging.js';
6
7
 
7
8
  const pluginName = "needle-copy-files";
8
9
 
@@ -35,11 +36,19 @@ export const needleCopyFiles = (command, config, userSettings) => {
35
36
  * @param {import('../types').userSettings} config
36
37
  */
37
38
  async function run(buildstep, config) {
38
- console.log(`[${pluginName}] - Copy files at ${buildstep}`);
39
+ const logLines = [needleDim(`Copy files at ${buildstep}`)];
39
40
  const copyIncludesFromEngine = config?.copyIncludesFromEngine ?? true;
41
+ const copied = { count: 0, bytes: 0 };
40
42
 
41
43
  const baseDir = process.cwd();
42
44
  const override = buildstep === "start";
45
+ const supportsColor = needleSupportsColor();
46
+ const key = (text) => supportsColor ? needleBlue(text) : text;
47
+ const rel = (pathValue) => {
48
+ const result = relative(baseDir, pathValue).replaceAll("\\", "/");
49
+ if (!result || result.length === 0) return ".";
50
+ return result;
51
+ };
43
52
 
44
53
  let assetsDirName = "assets";
45
54
  let outdirName = "dist";
@@ -57,9 +66,9 @@ async function run(buildstep, config) {
57
66
  // copy include from engine
58
67
  const engineIncludeDir = resolve(baseDir, 'node_modules', '@needle-tools', 'engine', 'src', 'include');
59
68
  if (existsSync(engineIncludeDir)) {
60
- console.log(`[${pluginName}] - Copy engine include to ${baseDir}/include`)
69
+ logLines.push(`Engine include: ${rel(engineIncludeDir)} -> ${rel(resolve(baseDir, 'include'))}`);
61
70
  const projectIncludeDir = resolve(baseDir, 'include');
62
- copyFilesSync(engineIncludeDir, projectIncludeDir);
71
+ copyFilesSync(engineIncludeDir, projectIncludeDir, true, copied);
63
72
  }
64
73
  }
65
74
 
@@ -85,8 +94,8 @@ async function run(buildstep, config) {
85
94
  const src = resolve(baseDir, entry);
86
95
  const dest = resolvePath(outDir, entry);
87
96
  if (existsSync(src) && dest) {
88
- console.log(`[${pluginName}] - Copy ${entry} to ${outdirName}/${entry}`)
89
- copyFilesSync(src, dest, override);
97
+ logLines.push(`Configured copy: ${rel(src)} -> ${rel(dest)}`);
98
+ copyFilesSync(src, dest, override, copied);
90
99
  }
91
100
  }
92
101
  }
@@ -98,21 +107,41 @@ async function run(buildstep, config) {
98
107
  // ensure that the target directory exists and is cleared if it already exists
99
108
  // otherwise we might run into issues where the build pipeline is running for already compressed files
100
109
  if (override && existsSync(targetDir)) {
101
- console.log(`[${pluginName}] - Clearing target directory \"${targetDir}\"`);
110
+ logLines.push(needleDim(`Clearing target directory "${rel(targetDir)}"`));
102
111
  rmSync(targetDir, { recursive: true, force: true });
103
112
  }
104
- console.log(`[${pluginName}] - Copy assets to ${outdirName}/${builtAssetsDirectory()}`)
105
- copyFilesSync(assetsDir, targetDir, override);
113
+ logLines.push(needleDim(`Assets: ${rel(assetsDir)} -> ${rel(targetDir)}`));
114
+ copyFilesSync(assetsDir, targetDir, override, copied);
106
115
  }
107
- else console.log(`WARN: No assets directory found. Skipping copy of ${assetsDirName} resolved to ${assetsDir}`)
116
+ else logLines.push(`No assets directory found. Skipping copy of ${assetsDirName} resolved to ${rel(assetsDir)}`);
108
117
 
109
118
  // copy include dir
110
119
  const includeDir = resolve(baseDir, 'include');
111
120
  if (existsSync(includeDir)) {
112
- console.log(`[${pluginName}] - Copy include to ${outdirName}/include`)
113
121
  const targetDir = resolve(outDir, 'include');
114
- copyFilesSync(includeDir, targetDir, override);
122
+ logLines.push(needleDim(`Include: ${rel(includeDir)} -> ${rel(targetDir)}`));
123
+ copyFilesSync(includeDir, targetDir, override, copied);
115
124
  }
125
+
126
+ logLines.push(`${key("Copied files")}: ${copied.count}`);
127
+ logLines.push(`${key("Copied size")}: ${formatBytes(copied.bytes || 0)}`);
128
+ if (buildstep === "end") {
129
+ logLines.push("");
130
+ logLines.push("✨ Happy creating! 🌵");
131
+ }
132
+
133
+ needleLog(pluginName, logLines.join("\n"), "log", {
134
+ leadingNewline: buildstep === "end",
135
+ dimBody: false,
136
+ });
137
+ }
138
+
139
+ function formatBytes(bytes) {
140
+ const value = Number(bytes || 0);
141
+ if (value < 1024) return `${value} B`;
142
+ if (value < 1024 * 1024) return `${(value / 1024).toFixed(2)} kB`;
143
+ if (value < 1024 * 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(2)} MB`;
144
+ return `${(value / (1024 * 1024 * 1024)).toFixed(2)} GB`;
116
145
  }
117
146
 
118
147
  /** resolves relative or absolute paths to a path inside the out directory