@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
@@ -0,0 +1,992 @@
1
+ // @ts-check
2
+ import {
3
+ SKYBOX_MAGIC_KEYWORDS,
4
+ buildAutoPolicy,
5
+ getLocalFilesAutoPolicy,
6
+ getWebXRProfilesForMode,
7
+ hasAutoFeatureSelection,
8
+ makeFilesLocalIsEnabled,
9
+ resolveOptions,
10
+ resolveSkyboxSelectionUrls,
11
+ resolveSkyboxValueToUrl,
12
+ shouldHandleUrlInAutoMode,
13
+ } from './local-files-analysis.js';
14
+
15
+ /** @typedef {import('./local-files-types.js').LocalizationContext} LocalizationContext */
16
+ /** @typedef {import('./local-files-types.js').LocalizationOptions} LocalizationOptions */
17
+ /** @typedef {import('./local-files-types.js').LocalizationStats} LocalizationStats */
18
+ /** @typedef {import('./local-files-types.js').AutoPolicy} AutoPolicy */
19
+ /** @typedef {import('./local-files-types.js').UrlHandler} UrlHandler */
20
+ import {
21
+ Cache,
22
+ downloadBinary,
23
+ downloadText,
24
+ ensureTrailingSlash,
25
+ fixRelativeNewURL,
26
+ getRelativeToBasePath,
27
+ getShortUrlName,
28
+ getValidFilename,
29
+ normalizeWebPath,
30
+ recordFailedDownload,
31
+ replaceAll,
32
+ finishMakeLocalProgress,
33
+ } from './local-files-utils.js';
34
+ import { needleBlue, needleDim, needleLog, needleSupportsColor } from './logging.js';
35
+
36
+ const debug = false;
37
+
38
+ /** @param {unknown} err @returns {string} */
39
+ function getErrMessage(err) { return err instanceof Error ? getErrMessage(err) : String(err); }
40
+
41
+ /** @type {UrlHandler[]} */
42
+ const urlHandlers = [
43
+ {
44
+ name: "Google Fonts CSS",
45
+ pattern: /["'`](https:\/\/fonts\.googleapis\.com\/css2?\?[^"'`]+)["'`]/g,
46
+ type: "css",
47
+ feature: "fonts",
48
+ },
49
+ {
50
+ name: "Google Fonts gstatic",
51
+ pattern: /["'`(](https:\/\/fonts\.gstatic\.com\/[^"'`)]+)["'`)]/g,
52
+ type: "binary",
53
+ feature: "fonts",
54
+ },
55
+ {
56
+ name: "QRCode.js",
57
+ pattern: /["'`](https:\/\/cdn\.jsdelivr\.net\/gh\/davidshimjs\/qrcodejs@[^"'`]+\/qrcode\.min\.js)["'`]/g,
58
+ type: "binary",
59
+ feature: "cdn-scripts",
60
+ },
61
+ {
62
+ name: "vConsole",
63
+ pattern: /["'`](https:\/\/cdn\.jsdelivr\.net\/npm\/vconsole@[^"'`]+\/dist\/vconsole\.min\.js)["'`]/g,
64
+ type: "binary",
65
+ feature: "cdn-scripts",
66
+ },
67
+ {
68
+ name: "HLS.js",
69
+ pattern: /["'`](https:\/\/cdn\.jsdelivr\.net\/npm\/hls\.js@[^"'`]+)["'`]/g,
70
+ type: "binary",
71
+ feature: "cdn-scripts",
72
+ },
73
+ {
74
+ name: "WebXR Input Profiles",
75
+ pattern: /["'`](https:\/\/cdn\.jsdelivr\.net\/npm\/@webxr-input-profiles\/assets@[^"'`]*\/dist\/profiles)\/?["'`]/g,
76
+ type: "webxr-profiles",
77
+ feature: "xr",
78
+ },
79
+ {
80
+ name: "Polyhaven",
81
+ pattern: /["'`](https:\/\/dl\.polyhaven\.org\/file\/[^"'`]+)["'`]/g,
82
+ type: "binary",
83
+ feature: "polyhaven",
84
+ },
85
+ {
86
+ name: "Needle CDN skybox",
87
+ pattern: /["'`](https:\/\/cdn\.needle\.tools\/static\/skybox\/[^"'`]+)["'`]/g,
88
+ type: "binary",
89
+ feature: "skybox",
90
+ },
91
+ {
92
+ name: "Needle CDN fonts",
93
+ pattern: /["'`](https:\/\/cdn\.needle\.tools\/static\/fonts\/[^"'`]+)["'`]/g,
94
+ type: "binary",
95
+ feature: "needle-fonts",
96
+ },
97
+ {
98
+ name: "Needle CDN models",
99
+ pattern: /["'`](https:\/\/cdn\.needle\.tools\/static\/models\/[^"'`]+)["'`]/g,
100
+ type: "binary",
101
+ feature: "needle-models",
102
+ },
103
+ {
104
+ name: "Needle CDN avatars",
105
+ pattern: /["'`](https:\/\/cdn\.needle\.tools\/static\/avatars\/[^"'`]+)["'`]/g,
106
+ type: "binary",
107
+ feature: "needle-avatars",
108
+ },
109
+ {
110
+ name: "Needle CDN branding",
111
+ pattern: /["'`](https:\/\/cdn\.needle\.tools\/static\/branding\/[^"'`]+)["'`]/g,
112
+ type: "binary",
113
+ feature: "needle-branding",
114
+ },
115
+ {
116
+ name: "Needle CDN basis decoder",
117
+ pattern: /["'`](https:\/\/cdn\.needle\.tools\/static\/three\/[^"'`]*\/basis2\/?)['"`]/g,
118
+ type: "decoder-dir",
119
+ decoderFiles: ["basis_transcoder.js", "basis_transcoder.wasm"],
120
+ localDirName: "basis",
121
+ feature: "ktx2",
122
+ },
123
+ {
124
+ name: "Draco decoder (gstatic)",
125
+ pattern: /["'`](https:\/\/www\.gstatic\.com\/draco\/versioned\/decoders\/[^"'`]*\/?)['"`]/g,
126
+ type: "decoder-dir",
127
+ decoderFiles: ["draco_decoder.js", "draco_decoder.wasm", "draco_wasm_wrapper.js"],
128
+ localDirName: "draco",
129
+ feature: "draco",
130
+ },
131
+ {
132
+ name: "Needle CDN MaterialX",
133
+ pattern: /["'`](https:\/\/cdn\.needle\.tools\/static\/materialx\/[^"'`]*\/?)['"`]/g,
134
+ type: "decoder-dir",
135
+ decoderFiles: ["JsMaterialXCore.wasm", "JsMaterialXGenShader.wasm", "JsMaterialXGenShader.data.txt"],
136
+ localDirName: "materialx",
137
+ feature: "materialx",
138
+ },
139
+ {
140
+ name: "Needle uploads",
141
+ pattern: /["'`](https:\/\/uploads\.needle\.tools\/include\/[^"'`]+)["'`]/g,
142
+ type: "binary",
143
+ feature: "needle-uploads",
144
+ },
145
+ {
146
+ name: "GitHub raw content",
147
+ pattern: /["'`](https:\/\/raw\.githubusercontent\.com\/[^"'`$]+\.[a-z]{2,5})["'`]/g,
148
+ type: "binary",
149
+ feature: "github-content",
150
+ },
151
+ {
152
+ name: "threejs.org models",
153
+ pattern: /["'`](https:\/\/threejs\.org\/examples\/models\/[^"'`]+)["'`]/g,
154
+ type: "binary",
155
+ feature: "threejs-models",
156
+ },
157
+ ];
158
+
159
+ export { makeFilesLocalIsEnabled };
160
+
161
+ /**
162
+ * @returns {LocalizationStats}
163
+ */
164
+ function createLocalizationStats() {
165
+ return {
166
+ fileCount: 0,
167
+ totalBytes: 0,
168
+ mimeCounts: new Map(),
169
+ };
170
+ }
171
+
172
+ /**
173
+ * @param {string} url
174
+ * @param {string} [fallback]
175
+ * @returns {string}
176
+ */
177
+ function inferMimeType(url, fallback = "application/octet-stream") {
178
+ const value = String(url || "").split("?")[0].toLowerCase();
179
+ if (value.endsWith(".css")) return "text/css";
180
+ if (value.endsWith(".json")) return "application/json";
181
+ if (value.endsWith(".js") || value.endsWith(".mjs")) return "application/javascript";
182
+ if (value.endsWith(".wasm")) return "application/wasm";
183
+ if (value.endsWith(".ttf")) return "font/ttf";
184
+ if (value.endsWith(".woff2")) return "font/woff2";
185
+ if (value.endsWith(".woff")) return "font/woff";
186
+ if (value.endsWith(".otf")) return "font/otf";
187
+ if (value.endsWith(".png")) return "image/png";
188
+ if (value.endsWith(".jpg") || value.endsWith(".jpeg")) return "image/jpeg";
189
+ if (value.endsWith(".webp")) return "image/webp";
190
+ if (value.endsWith(".exr")) return "image/exr";
191
+ if (value.endsWith(".glb")) return "model/gltf-binary";
192
+ if (value.endsWith(".gltf")) return "model/gltf+json";
193
+ return fallback;
194
+ }
195
+
196
+ /**
197
+ * @param {LocalizationStats} stats
198
+ * @param {string} url
199
+ * @param {number} sizeBytes
200
+ * @param {string | null | undefined} mimeType
201
+ */
202
+ function recordLocalizedAsset(stats, url, sizeBytes, mimeType) {
203
+ if (!stats || !Number.isFinite(sizeBytes) || sizeBytes < 0) return;
204
+ stats.fileCount += 1;
205
+ stats.totalBytes += sizeBytes;
206
+ const mime = mimeType || inferMimeType(url);
207
+ stats.mimeCounts.set(mime, (stats.mimeCounts.get(mime) || 0) + 1);
208
+ }
209
+
210
+ /**
211
+ * @param {string} command
212
+ * @param {unknown} _config
213
+ * @param {import('../types').userSettings} userSettings
214
+ * @returns {import('vite').Plugin | null}
215
+ */
216
+ export const needleMakeFilesLocal = (command, _config, userSettings) => {
217
+ if (!makeFilesLocalIsEnabled(userSettings)) {
218
+ return null;
219
+ }
220
+
221
+ const options = resolveOptions(userSettings);
222
+
223
+ const startupLines = ["Local files plugin is enabled"];
224
+ if (options.platform) startupLines.push("Platform: " + options.platform);
225
+ if (options.exclude?.length) startupLines.push("Custom excludes: " + options.exclude.join(", "));
226
+ if (options.features === "auto") startupLines.push("Feature detection: auto");
227
+ else if (Array.isArray(options.features) && options.features.length) startupLines.push("Features: " + options.features.join(", "));
228
+ if (options.excludeFeatures?.length) startupLines.push("Excluded features: " + options.excludeFeatures.join(", "));
229
+ if (options.packages?.length) startupLines.push("Package filter: " + options.packages.join(", "));
230
+ const projectDir = process.cwd();
231
+ const autoPolicy = getLocalFilesAutoPolicy(projectDir) ?? buildAutoPolicy(options, projectDir);
232
+
233
+ const activeHandlers = getActiveHandlers(options, autoPolicy);
234
+ {
235
+ const featureSet = Array.from(new Set(activeHandlers.map(h => h.feature)));
236
+ const allFeatureSet = Array.from(new Set(urlHandlers.map(h => h.feature)));
237
+ if (options.features === "auto") {
238
+ startupLines.push("Auto-detected features (" + featureSet.length + "/" + allFeatureSet.length + "): " + featureSet.join(", "));
239
+ }
240
+ else if (Array.isArray(options.features) && options.features.length || options.excludeFeatures?.length) {
241
+ startupLines.push("Active features (" + featureSet.length + "): " + featureSet.join(", "));
242
+ }
243
+ }
244
+ needleLog("needle:local-files", startupLines.join("\n"), "log", { dimBody: false });
245
+
246
+ const cache = new Cache();
247
+ /** @type {Map<string, string>} */
248
+ const failedDownloads = new Map();
249
+ const localizationStats = createLocalizationStats();
250
+
251
+ /** @type {import('vite').ResolvedConfig | null} */
252
+ let viteConfig = null;
253
+
254
+ const plugin = /** @type {import('vite').Plugin} */ ({
255
+ name: "needle:local-files",
256
+ apply: "build",
257
+ configResolved(config) {
258
+ viteConfig = config;
259
+ },
260
+ async buildStart() {
261
+ await prefetchConfiguredAssets({
262
+ pluginContext: this,
263
+ cache,
264
+ command,
265
+ viteConfig,
266
+ options,
267
+ autoPolicy,
268
+ failedDownloads,
269
+ localizationStats,
270
+ }, activeHandlers);
271
+ },
272
+ async transform(src, _id) {
273
+ if (options.packages?.length && _id) {
274
+ const matchesPackage = options.packages.some(pkg => _id.includes('/node_modules/' + pkg + '/') || _id.includes('\\node_modules\\' + pkg + '\\'));
275
+ if (!matchesPackage) {
276
+ const isProjectFile = !_id.includes('/node_modules/') && !_id.includes('\\node_modules\\');
277
+ if (!isProjectFile) return { code: src, map: null };
278
+ }
279
+ }
280
+ try {
281
+ const assetsDir = normalizeWebPath(ensureTrailingSlash(viteConfig?.build?.assetsDir || "assets"));
282
+ const isCssTransform = /\.css($|\?)/i.test(_id || "");
283
+ const currentDir = isCssTransform ? assetsDir : "";
284
+ src = await makeLocal(src, "ext/", currentDir, {
285
+ pluginContext: this,
286
+ cache,
287
+ command,
288
+ viteConfig,
289
+ options,
290
+ autoPolicy,
291
+ failedDownloads,
292
+ localizationStats,
293
+ }, activeHandlers);
294
+ src = fixRelativeNewURL(src);
295
+ }
296
+ catch (err) {
297
+ needleLog("needle:local-files", "Error in transform: " + getErrMessage(err), "error");
298
+ }
299
+ return {
300
+ code: src,
301
+ map: null,
302
+ };
303
+ },
304
+ renderChunk(code, chunk) {
305
+ if (!chunk.fileName?.endsWith(".js")) return null;
306
+ const fixed = fixRelativeNewURL(code);
307
+ if (fixed === code) return null;
308
+ return {
309
+ code: fixed,
310
+ map: null,
311
+ };
312
+ },
313
+ generateBundle(_options, bundle) {
314
+ for (const output of Object.values(bundle)) {
315
+ if (output.type !== "chunk") continue;
316
+ if (!output.fileName?.endsWith(".js")) continue;
317
+ const fixed = fixRelativeNewURL(output.code);
318
+ if (fixed !== output.code) output.code = fixed;
319
+ }
320
+ },
321
+ transformIndexHtml: {
322
+ order: 'pre',
323
+ async handler(html, _ctx) {
324
+ try {
325
+ html = await makeLocalHtml(html, "ext/", {
326
+ pluginContext: null,
327
+ cache,
328
+ command,
329
+ viteConfig,
330
+ options,
331
+ autoPolicy,
332
+ failedDownloads,
333
+ localizationStats,
334
+ }, activeHandlers);
335
+ }
336
+ catch (err) {
337
+ needleLog("needle:local-files", "Error in transformIndexHtml: " + getErrMessage(err), "error");
338
+ }
339
+ return html;
340
+ }
341
+ },
342
+ buildEnd() {
343
+ finishMakeLocalProgress();
344
+ const map = cache.map;
345
+ const shorten = (value, max = 140) => value.length > max ? `${value.slice(0, Math.max(0, max - 1))}…` : value;
346
+ const supportsColor = needleSupportsColor();
347
+ const key = (text) => supportsColor ? needleBlue(text) : text;
348
+ const localized = ["Local files summary"];
349
+ localized.push(key("Files made local") + " : " + localizationStats.fileCount);
350
+ localized.push(key("Total size") + " : " + (localizationStats.totalBytes / 1024).toFixed(2) + " kB");
351
+ localized.push(key("Unique mime types") + " : " + localizationStats.mimeCounts.size);
352
+ if (localizationStats.mimeCounts.size > 0) {
353
+ const mimeBreakdown = Array.from(localizationStats.mimeCounts.entries())
354
+ .sort((a, b) => b[1] - a[1])
355
+ .map(([mime, count]) => supportsColor ? `${mime}${needleDim(` ${count}×`)}` : `${mime} ${count}×`)
356
+ .join(", ");
357
+ localized.push(key("Mime types") + " : " + mimeBreakdown);
358
+ }
359
+ const remoteUrls = Array.from(map.keys()).filter(url => /^https?:\/\//i.test(url));
360
+ const previewLimit = 5;
361
+ needleLog("needle:local-files", localized.join("\n"), "log", { dimBody: false });
362
+ if (remoteUrls.length > 0) {
363
+ needleLog("needle:local-files", remoteUrls.slice(0, previewLimit).map(url => "- " + shorten(url)).join("\n") + (remoteUrls.length > previewLimit ? `\n... and ${remoteUrls.length - previewLimit} more` : ""), "log", { showHeader: false, dimBody: true });
364
+ }
365
+ if (failedDownloads.size > 0) {
366
+ const failed = ["Failed to make local:"];
367
+ for (const [url, message] of Array.from(failedDownloads.entries())) {
368
+ const shortName = getShortUrlName(url);
369
+ failed.push(" " + shortName + " (" + url + ")" + (message ? " - " + message : ""));
370
+ }
371
+ needleLog("needle:local-files", failed.join("\n"), "warn");
372
+ }
373
+ }
374
+ });
375
+ return plugin;
376
+ };
377
+
378
+ /**
379
+ * @param {LocalizationOptions} options
380
+ * @param {AutoPolicy | null} autoPolicy
381
+ * @returns {UrlHandler[]}
382
+ */
383
+ export function getActiveHandlers(options, autoPolicy) {
384
+ let handlers = urlHandlers;
385
+
386
+ const hasAuto = hasAutoFeatureSelection(options.features);
387
+
388
+ if (hasAuto && Array.isArray(options.features) && options.features.length) {
389
+ const detected = autoPolicy?.features ?? new Set();
390
+ const includeSet = new Set(options.features.filter(f => f !== "auto"));
391
+ handlers = handlers.filter(h => detected.has(h.feature) || includeSet.has(h.feature));
392
+ }
393
+ else if (hasAuto) {
394
+ const detected = autoPolicy?.features ?? new Set();
395
+ handlers = handlers.filter(h => detected.has(h.feature));
396
+ }
397
+ else if (Array.isArray(options.features) && options.features.length) {
398
+ const includeSet = new Set(options.features.filter(f => f !== "auto"));
399
+ handlers = handlers.filter(h => includeSet.has(h.feature));
400
+ }
401
+
402
+ if (options.excludeFeatures?.length) {
403
+ const excludeSet = new Set(options.excludeFeatures);
404
+ handlers = handlers.filter(h => !excludeSet.has(h.feature));
405
+ }
406
+
407
+ return handlers;
408
+ }
409
+
410
+ /**
411
+ * @param {string} src
412
+ * @returns {string}
413
+ */
414
+ function stripComments(src) {
415
+ let result = '';
416
+ let i = 0;
417
+ const len = src.length;
418
+
419
+ while (i < len) {
420
+ const ch = src[i];
421
+ const next = i + 1 < len ? src[i + 1] : '';
422
+
423
+ if (ch === '"' || ch === "'" || ch === '`') {
424
+ const quote = ch;
425
+ result += ch;
426
+ i++;
427
+ while (i < len) {
428
+ const c = src[i];
429
+ if (c === '\\') {
430
+ result += src[i] + (i + 1 < len ? src[i + 1] : '');
431
+ i += 2;
432
+ continue;
433
+ }
434
+ if (c === quote) {
435
+ result += c;
436
+ i++;
437
+ break;
438
+ }
439
+ result += c;
440
+ i++;
441
+ }
442
+ continue;
443
+ }
444
+
445
+ if (ch === '/' && next === '/') {
446
+ const prev = i > 0 ? src[i - 1] : '';
447
+ if (prev !== ':') {
448
+ while (i < len && src[i] !== '\n') {
449
+ result += ' ';
450
+ i++;
451
+ }
452
+ continue;
453
+ }
454
+ }
455
+
456
+ if (ch === '/' && next === '*') {
457
+ result += ' ';
458
+ i += 2;
459
+ while (i < len) {
460
+ if (src[i] === '*' && i + 1 < len && src[i + 1] === '/') {
461
+ result += ' ';
462
+ i += 2;
463
+ break;
464
+ }
465
+ result += src[i] === '\n' ? '\n' : ' ';
466
+ i++;
467
+ }
468
+ continue;
469
+ }
470
+
471
+ result += ch;
472
+ i++;
473
+ }
474
+
475
+ return result;
476
+ }
477
+
478
+ /**
479
+ * @param {string} url
480
+ * @param {LocalizationOptions} options
481
+ * @returns {boolean}
482
+ */
483
+ function shouldExclude(url, options) {
484
+ if (url.includes("${")) return true;
485
+
486
+ if (options.platform === "facebook-instant") {
487
+ if (url.includes("connect.facebook.net")) return true;
488
+ }
489
+
490
+ if (options.exclude) {
491
+ for (const pattern of options.exclude) {
492
+ if (typeof pattern === "string") {
493
+ if (url.includes(pattern)) return true;
494
+ } else if (pattern instanceof RegExp) {
495
+ if (pattern.test(url)) return true;
496
+ }
497
+ }
498
+ }
499
+ return false;
500
+ }
501
+
502
+ /**
503
+ * @param {string} src
504
+ * @param {string} stripped
505
+ * @param {string} basePath
506
+ * @param {string} currentDir
507
+ * @param {LocalizationContext} context
508
+ * @returns {Promise<string>}
509
+ */
510
+ async function expandTemplateUrls(src, stripped, basePath, currentDir, context) {
511
+ const expansions = context.options.templateExpansions;
512
+ if (!expansions?.length) return src;
513
+
514
+ for (const expansion of expansions) {
515
+ const { cdnPrefix, variables } = expansion;
516
+ if (!cdnPrefix || !variables || !Object.keys(variables).length) continue;
517
+
518
+ const localPrefix = expansion.localPrefix || deriveLocalPrefix(cdnPrefix);
519
+ const localBasePath = basePath + localPrefix + "/";
520
+
521
+ const escapedPrefix = cdnPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
522
+ const templateRegex = new RegExp('`' + escapedPrefix + '([^`]*\\$\\{[^`]*)`', 'g');
523
+
524
+ const templateSuffixes = new Set();
525
+ let match;
526
+ while ((match = templateRegex.exec(stripped)) !== null) {
527
+ templateSuffixes.add(match[1]);
528
+ }
529
+
530
+ if (templateSuffixes.size === 0) continue;
531
+
532
+ for (const suffix of Array.from(templateSuffixes)) {
533
+ const expandedSuffixes = expandVariables(suffix, variables);
534
+
535
+ for (const expanded of expandedSuffixes) {
536
+ const concreteUrl = cdnPrefix + expanded;
537
+ const localFilePath = localBasePath + expanded;
538
+
539
+ if (context.cache.getFromCache(concreteUrl)) continue;
540
+
541
+ try {
542
+ const data = await downloadBinary(concreteUrl);
543
+ if (context.command === 'build' && context.pluginContext) {
544
+ context.pluginContext.emitFile({
545
+ type: 'asset',
546
+ fileName: localFilePath,
547
+ source: data,
548
+ });
549
+ }
550
+ context.cache.addToCache(concreteUrl, localFilePath);
551
+ if (debug) console.log("[needle:local-files] Template expansion: " + concreteUrl + " → " + localFilePath);
552
+ }
553
+ catch (err) {
554
+ needleLog("needle:local-files", "Failed to download template expansion: " + concreteUrl + " - " + getErrMessage(err), "warn", { dimBody: false });
555
+ }
556
+ }
557
+ }
558
+
559
+ const replaceRegex = new RegExp('`' + escapedPrefix + '([^`]*\\$\\{[^`]*)`', 'g');
560
+ src = src.replace(replaceRegex, (/** @type {string} */ fullMatch, /** @type {string} */ rest) => {
561
+ const newPrefix = getRelativeToBasePath(localBasePath, currentDir);
562
+ return '`' + newPrefix + rest + '`';
563
+ });
564
+ }
565
+
566
+ return src;
567
+ }
568
+
569
+ /**
570
+ * @param {string} cdnPrefix
571
+ * @returns {string}
572
+ */
573
+ function deriveLocalPrefix(cdnPrefix) {
574
+ try {
575
+ const url = new URL(cdnPrefix);
576
+ const segments = url.pathname.split('/').filter(Boolean);
577
+ return segments[segments.length - 1] || 'cdn';
578
+ }
579
+ catch {
580
+ return 'cdn';
581
+ }
582
+ }
583
+
584
+ /**
585
+ * @param {string} template
586
+ * @param {Record<string, string[]>} variables
587
+ * @returns {string[]}
588
+ */
589
+ function expandVariables(template, variables) {
590
+ const varRefs = /** @type {string[]} */ ([]);
591
+ const varRegex = /\$\{(\w+)\}/g;
592
+ /** @type {RegExpExecArray | null} */
593
+ let m = null;
594
+ while ((m = varRegex.exec(template)) !== null) {
595
+ if (!varRefs.includes(m[1])) {
596
+ varRefs.push(m[1]);
597
+ }
598
+ }
599
+
600
+ for (const v of varRefs) {
601
+ if (!variables[v]?.length) return [];
602
+ }
603
+
604
+ if (varRefs.length === 0) return [template];
605
+
606
+ const results = /** @type {string[]} */ ([]);
607
+ /**
608
+ * @param {number} idx
609
+ * @param {Record<string, string>} current
610
+ */
611
+ function expand(idx, current) {
612
+ if (idx === varRefs.length) {
613
+ let expanded = template;
614
+ for (const [key, value] of Object.entries(current)) {
615
+ expanded = expanded.split('${' + key + '}').join(value);
616
+ }
617
+ results.push(expanded);
618
+ return;
619
+ }
620
+ const varName = varRefs[idx];
621
+ for (const value of variables[varName]) {
622
+ current[varName] = value;
623
+ expand(idx + 1, current);
624
+ }
625
+ }
626
+ expand(0, {});
627
+ return results;
628
+ }
629
+
630
+ /**
631
+ * @param {string} src
632
+ * @param {string} basePath
633
+ * @param {string} currentDir
634
+ * @param {LocalizationContext} context
635
+ * @param {UrlHandler[]} [handlers]
636
+ * @returns {Promise<string>}
637
+ */
638
+ export async function makeLocal(src, basePath, currentDir, context, handlers) {
639
+ if (!handlers) handlers = urlHandlers;
640
+
641
+ const stripped = stripComments(src);
642
+
643
+ for (const handler of handlers) {
644
+ handler.pattern.lastIndex = 0;
645
+
646
+ const matches = /** @type {string[]} */ ([]);
647
+ /** @type {RegExpExecArray | null} */
648
+ let match = null;
649
+ while ((match = handler.pattern.exec(stripped)) !== null) {
650
+ const url = match[1];
651
+ if (!url || url.length < 10) continue;
652
+ if (shouldExclude(url, context.options)) continue;
653
+ if (!shouldHandleUrlInAutoMode(url, handler, context)) continue;
654
+ if (url.endsWith("/") && handler.type !== "decoder-dir" && handler.type !== "webxr-profiles") continue;
655
+ matches.push(url);
656
+ }
657
+
658
+ if (matches.length === 0) continue;
659
+
660
+ const uniqueUrls = Array.from(new Set(matches));
661
+
662
+ for (const url of uniqueUrls) {
663
+ try {
664
+ const handlerBasePath = getFeatureBasePath(basePath, handler.feature);
665
+ if (handler.type === "decoder-dir") {
666
+ const localPath = await handleDecoderDir(url, handlerBasePath, currentDir, handler, context);
667
+ if (localPath) src = replaceAll(src, url, localPath);
668
+ }
669
+ else if (handler.type === "webxr-profiles") {
670
+ const localPath = await handleWebXRProfiles(url, handlerBasePath, currentDir, context);
671
+ if (localPath) src = replaceAll(src, url, localPath);
672
+ }
673
+ else if (handler.type === "css") {
674
+ const localPath = await handleCssUrl(url, handlerBasePath, currentDir, context, handlers);
675
+ if (localPath) src = replaceAll(src, url, localPath);
676
+ }
677
+ else {
678
+ const localPath = await handleBinaryUrl(url, handlerBasePath, currentDir, context);
679
+ if (localPath) src = replaceAll(src, url, localPath);
680
+ }
681
+ }
682
+ catch (err) {
683
+ recordFailedDownload(context, url, err);
684
+ }
685
+ }
686
+ }
687
+
688
+ src = await expandTemplateUrls(src, stripped, basePath, currentDir, context);
689
+
690
+ return src;
691
+ }
692
+
693
+ /**
694
+ * @param {string} html
695
+ * @param {string} basePath
696
+ * @param {LocalizationContext} context
697
+ * @param {UrlHandler[]} [handlers]
698
+ * @returns {Promise<string>}
699
+ */
700
+ export async function makeLocalHtml(html, basePath, context, handlers) {
701
+ if (!handlers) handlers = urlHandlers;
702
+
703
+ for (const handler of handlers) {
704
+ handler.pattern.lastIndex = 0;
705
+ /** @type {RegExpExecArray | null} */
706
+ let match = null;
707
+ const matches = /** @type {string[]} */ ([]);
708
+ while ((match = handler.pattern.exec(html)) !== null) {
709
+ const url = match[1];
710
+ if (!url || url.length < 10) continue;
711
+ if (shouldExclude(url, context.options)) continue;
712
+ if (!shouldHandleUrlInAutoMode(url, handler, context)) continue;
713
+ if (url.endsWith("/") && handler.type !== "decoder-dir" && handler.type !== "webxr-profiles") continue;
714
+ matches.push(url);
715
+ }
716
+
717
+ const uniqueUrls = Array.from(new Set(matches));
718
+ for (const url of uniqueUrls) {
719
+ try {
720
+ const handlerBasePath = getFeatureBasePath(basePath, handler.feature);
721
+ if (handler.type === "css") {
722
+ const localPath = await handleCssUrl(url, handlerBasePath, "", context, handlers);
723
+ if (localPath) html = replaceAll(html, url, localPath);
724
+ }
725
+ else {
726
+ const localPath = await handleBinaryUrl(url, handlerBasePath, "", context);
727
+ if (localPath) html = replaceAll(html, url, localPath);
728
+ }
729
+ }
730
+ catch (err) {
731
+ recordFailedDownload(context, url, err);
732
+ needleLog("needle:local-files", "Failed to make HTML URL local: " + url + " - " + getErrMessage(err), "warn", { dimBody: false });
733
+ }
734
+ }
735
+ }
736
+ return html;
737
+ }
738
+
739
+ /**
740
+ * @param {string} basePath
741
+ * @param {string} feature
742
+ * @returns {string}
743
+ */
744
+ export function getFeatureBasePath(basePath, feature) {
745
+ const normalized = basePath.endsWith("/") ? basePath : basePath + "/";
746
+ const mappedFeatureDir = mapFeatureToOutputDir(feature);
747
+ if (normalized.endsWith("/" + mappedFeatureDir + "/")) return normalized;
748
+ return normalized + mappedFeatureDir + "/";
749
+ }
750
+
751
+ /**
752
+ * @param {string} feature
753
+ * @returns {string}
754
+ */
755
+ function mapFeatureToOutputDir(feature) {
756
+ if (feature === "cdn-scripts") return "scripts";
757
+ if (feature === "needle-fonts") return "fonts";
758
+ if (feature === "needle-avatars") return "xr/avatars";
759
+ if (feature === "polyhaven") return "skybox";
760
+ return feature;
761
+ }
762
+
763
+ /**
764
+ * @param {string} url
765
+ * @param {string} basePath
766
+ * @param {string} currentDir
767
+ * @param {LocalizationContext} context
768
+ * @param {UrlHandler[]} handlers
769
+ * @returns {Promise<string|undefined>}
770
+ */
771
+ async function handleCssUrl(url, basePath, currentDir, context, handlers) {
772
+ const cached = context.cache.getFromCache(url);
773
+ if (cached) return cached;
774
+
775
+ let cssContent = await downloadText(url);
776
+ cssContent = await makeLocal(cssContent, basePath, basePath, context, handlers);
777
+ recordLocalizedAsset(context.localizationStats, url, Buffer.byteLength(cssContent, "utf8"), "text/css");
778
+
779
+ const familyNameMatch = /family=([^&]+)/.exec(url);
780
+ const familyName = familyNameMatch ? getValidFilename(familyNameMatch[1], cssContent) : getValidFilename(url, cssContent);
781
+ const fileName = "font-" + familyName + ".css";
782
+ const outputPath = basePath + fileName;
783
+
784
+ /** @type {string | undefined} */
785
+ let newPath;
786
+ if (context.command === 'build' && context.pluginContext) {
787
+ const referenceId = context.pluginContext.emitFile({
788
+ type: 'asset',
789
+ fileName: outputPath,
790
+ source: cssContent,
791
+ });
792
+ const localPath = "" + context.pluginContext.getFileName(referenceId);
793
+ newPath = getRelativeToBasePath(localPath, currentDir);
794
+ }
795
+ else {
796
+ const base64 = Buffer.from(cssContent).toString('base64');
797
+ newPath = "data:text/css;base64," + base64;
798
+ }
799
+ if (newPath) context.cache.addToCache(url, newPath);
800
+ return newPath;
801
+ }
802
+
803
+ /**
804
+ * @param {string} url
805
+ * @param {string} basePath
806
+ * @param {string} currentDir
807
+ * @param {LocalizationContext} context
808
+ * @returns {Promise<string|undefined>}
809
+ */
810
+ async function handleBinaryUrl(url, basePath, currentDir, context) {
811
+ const cached = context.cache.getFromCache(url);
812
+ if (cached) return cached;
813
+
814
+ const data = await downloadBinary(url);
815
+ recordLocalizedAsset(context.localizationStats, url, data.length, inferMimeType(url));
816
+ const filename = getValidFilename(url, data);
817
+
818
+ /** @type {string | undefined} */
819
+ let newPath;
820
+ if (context.command === 'build' && context.pluginContext) {
821
+ const referenceId = context.pluginContext.emitFile({
822
+ type: 'asset',
823
+ fileName: basePath + filename,
824
+ source: data,
825
+ });
826
+ const localPath = "" + context.pluginContext.getFileName(referenceId);
827
+ newPath = getRelativeToBasePath(localPath, currentDir);
828
+ }
829
+ else {
830
+ const base64 = Buffer.from(data).toString('base64');
831
+ newPath = "data:application/octet-stream;base64," + base64;
832
+ }
833
+ if (newPath) context.cache.addToCache(url, newPath);
834
+ return newPath;
835
+ }
836
+
837
+ /**
838
+ * @param {string} baseUrl
839
+ * @param {string} basePath
840
+ * @param {string} currentDir
841
+ * @param {UrlHandler} handler
842
+ * @param {LocalizationContext} context
843
+ * @returns {Promise<string>}
844
+ */
845
+ async function handleDecoderDir(baseUrl, basePath, currentDir, handler, context) {
846
+ const cached = context.cache.getFromCache(baseUrl);
847
+ if (cached) return cached;
848
+
849
+ const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
850
+ const localDir = ensureTrailingSlash(basePath);
851
+ const files = handler.decoderFiles || [];
852
+
853
+ for (const file of files) {
854
+ const fileUrl = normalizedBaseUrl + file;
855
+ const fileCached = context.cache.getFromCache(fileUrl);
856
+ if (fileCached) continue;
857
+
858
+ try {
859
+ const data = await downloadBinary(fileUrl);
860
+ recordLocalizedAsset(context.localizationStats, fileUrl, data.length, inferMimeType(fileUrl));
861
+ if (context.command === 'build' && context.pluginContext) {
862
+ const referenceId = context.pluginContext.emitFile({
863
+ type: 'asset',
864
+ fileName: localDir + file,
865
+ source: data,
866
+ });
867
+ const localPath = "" + context.pluginContext.getFileName(referenceId);
868
+ context.cache.addToCache(fileUrl, localPath);
869
+ }
870
+ }
871
+ catch (err) {
872
+ recordFailedDownload(context, fileUrl, err);
873
+ needleLog("needle:local-files", "Failed to download decoder file: " + fileUrl + " - " + getErrMessage(err), "warn", { dimBody: false });
874
+ }
875
+ }
876
+
877
+ const newBasePath = getRelativeToBasePath(localDir, currentDir);
878
+ context.cache.addToCache(baseUrl, newBasePath);
879
+ return newBasePath;
880
+ }
881
+
882
+ /**
883
+ * @param {string} baseUrl
884
+ * @param {string} basePath
885
+ * @param {string} currentDir
886
+ * @param {LocalizationContext} context
887
+ * @returns {Promise<string>}
888
+ */
889
+ export async function handleWebXRProfiles(baseUrl, basePath, currentDir, context) {
890
+ const cached = context.cache.getFromCache(baseUrl);
891
+ if (cached) return cached;
892
+
893
+ const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
894
+ const localDir = basePath + "webxr-profiles/";
895
+ const profiles = context.autoPolicy?.selectedWebXRProfiles?.length
896
+ ? context.autoPolicy.selectedWebXRProfiles
897
+ : getWebXRProfilesForMode(context.options.webxr);
898
+
899
+ try {
900
+ const profilesListUrl = normalizedBaseUrl + "profilesList.json";
901
+ const profilesList = await downloadBinary(profilesListUrl);
902
+ recordLocalizedAsset(context.localizationStats, profilesListUrl, profilesList.length, "application/json");
903
+ if (context.command === 'build' && context.pluginContext) {
904
+ context.pluginContext.emitFile({
905
+ type: 'asset',
906
+ fileName: localDir + "profilesList.json",
907
+ source: profilesList,
908
+ });
909
+ context.cache.addToCache(profilesListUrl, localDir + "profilesList.json");
910
+ }
911
+ }
912
+ catch (err) {
913
+ recordFailedDownload(context, normalizedBaseUrl + "profilesList.json", err);
914
+ needleLog("needle:local-files", "Failed to download profilesList.json: " + getErrMessage(err), "warn", { dimBody: false });
915
+ }
916
+
917
+ for (const profile of profiles) {
918
+ const profileDir = localDir + profile + "/";
919
+ const profileBaseUrl = normalizedBaseUrl + profile + "/";
920
+
921
+ const filesToDownload = [
922
+ "profile.json",
923
+ "left.glb",
924
+ "right.glb",
925
+ ];
926
+
927
+ for (const file of filesToDownload) {
928
+ const fileUrl = profileBaseUrl + file;
929
+ const fileCached = context.cache.getFromCache(fileUrl);
930
+ if (fileCached) continue;
931
+
932
+ try {
933
+ const data = await downloadBinary(fileUrl);
934
+ recordLocalizedAsset(context.localizationStats, fileUrl, data.length, inferMimeType(fileUrl));
935
+ if (context.command === 'build' && context.pluginContext) {
936
+ context.pluginContext.emitFile({
937
+ type: 'asset',
938
+ fileName: profileDir + file,
939
+ source: data,
940
+ });
941
+ context.cache.addToCache(fileUrl, profileDir + file);
942
+ }
943
+ }
944
+ catch (err) {
945
+ recordFailedDownload(context, fileUrl, err);
946
+ if (debug) needleLog("needle:local-files", "Failed to download WebXR profile file: " + fileUrl + " - " + getErrMessage(err), "warn", { dimBody: false });
947
+ }
948
+ }
949
+ }
950
+
951
+ const newBasePath = getRelativeToBasePath(localDir, currentDir);
952
+ context.cache.addToCache(baseUrl, newBasePath);
953
+ return newBasePath;
954
+ }
955
+
956
+ /**
957
+ * @param {LocalizationContext} context
958
+ * @param {UrlHandler[]} handlers
959
+ * @returns {Promise<void>}
960
+ */
961
+ async function prefetchConfiguredAssets(context, handlers) {
962
+ const skyboxHandler = handlers.find(h => h.feature === "skybox");
963
+ if (!skyboxHandler) return;
964
+
965
+ const configuredSkyboxUrls = resolveSkyboxSelectionUrls(context.options.skybox, new Set());
966
+ if (!configuredSkyboxUrls || configuredSkyboxUrls.size === 0) {
967
+ if (context.options.skybox === "all") {
968
+ for (const keyword of SKYBOX_MAGIC_KEYWORDS) {
969
+ const url = resolveSkyboxValueToUrl(keyword);
970
+ if (!url) continue;
971
+ try {
972
+ await handleBinaryUrl(url, getFeatureBasePath("ext/", "skybox"), "", context);
973
+ }
974
+ catch (err) {
975
+ recordFailedDownload(context, url, err);
976
+ if (debug) needleLog("needle:local-files", "Failed to prefetch skybox: " + url + " - " + getErrMessage(err), "warn", { dimBody: false });
977
+ }
978
+ }
979
+ }
980
+ return;
981
+ }
982
+
983
+ for (const url of configuredSkyboxUrls) {
984
+ try {
985
+ await handleBinaryUrl(url, getFeatureBasePath("ext/", "skybox"), "", context);
986
+ }
987
+ catch (err) {
988
+ recordFailedDownload(context, url, err);
989
+ if (debug) console.warn("[needle:local-files] Failed to prefetch configured skybox: " + url + " - " + getErrMessage(err));
990
+ }
991
+ }
992
+ }