@needle-tools/engine 4.15.0-next.cecd8e7 → 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 (144) hide show
  1. package/components.needle.json +1 -1
  2. package/dist/{gltf-progressive-BttGBXw6.umd.cjs → gltf-progressive-CMwJPwEt.umd.cjs} +1 -1
  3. package/dist/{gltf-progressive-Bm_6aEi4.js → gltf-progressive-CTlvpS3A.js} +1 -1
  4. package/dist/{gltf-progressive-T5WKTux5.min.js → gltf-progressive-DYL3SLVb.min.js} +1 -1
  5. package/dist/materialx-4jJLLe9Q.js +4174 -0
  6. package/dist/materialx-Bt9FHwco.min.js +158 -0
  7. package/dist/materialx-NDD0y4JY.umd.cjs +158 -0
  8. package/dist/{needle-engine.bundle-JQGIFVRm.umd.cjs → needle-engine.bundle-C1BFRZDF.umd.cjs} +103 -101
  9. package/dist/{needle-engine.bundle-VZVrVbc3.js → needle-engine.bundle-DB4kLWO_.js} +2829 -2823
  10. package/dist/{needle-engine.bundle-CuAiLb-d.min.js → needle-engine.bundle-DsTdfmeb.min.js} +115 -113
  11. package/dist/needle-engine.d.ts +27 -46
  12. package/dist/needle-engine.js +287 -288
  13. package/dist/needle-engine.min.js +1 -1
  14. package/dist/needle-engine.umd.cjs +1 -1
  15. package/dist/{postprocessing-06AXuvdv.min.js → postprocessing-BN-f4viE.min.js} +1 -1
  16. package/dist/{postprocessing-CPDcA21P.umd.cjs → postprocessing-DYmYOVm4.umd.cjs} +1 -1
  17. package/dist/{postprocessing-CI2x8Cln.js → postprocessing-De9ZpJrk.js} +1 -1
  18. package/dist/{three-examples-BMmNgNCN.umd.cjs → three-examples-BHqRVpO_.umd.cjs} +12 -12
  19. package/dist/{three-examples-CMYCd5nH.js → three-examples-C0ZCCA_K.js} +182 -192
  20. package/dist/{three-examples-CQl1fFZp.min.js → three-examples-DmTY8tGr.min.js} +14 -14
  21. package/lib/engine/api.d.ts +0 -2
  22. package/lib/engine/api.js +0 -2
  23. package/lib/engine/api.js.map +1 -1
  24. package/lib/engine/debug/debug.js +1 -1
  25. package/lib/engine/debug/debug.js.map +1 -1
  26. package/lib/engine/debug/debug_spatial_console.js +1 -1
  27. package/lib/engine/debug/debug_spatial_console.js.map +1 -1
  28. package/lib/engine/engine_context.js +1 -1
  29. package/lib/engine/engine_context.js.map +1 -1
  30. package/lib/engine/engine_create_objects.js +1 -1
  31. package/lib/engine/engine_create_objects.js.map +1 -1
  32. package/lib/engine/engine_gizmos.js +1 -1
  33. package/lib/engine/engine_gizmos.js.map +1 -1
  34. package/lib/engine/engine_license.js +7 -2
  35. package/lib/engine/engine_license.js.map +1 -1
  36. package/lib/engine/engine_utils.js +2 -2
  37. package/lib/engine/engine_utils.js.map +1 -1
  38. package/lib/engine/export/gltf/EXT_mesh_gpu_instancing_exporter.js.map +1 -0
  39. package/lib/engine/export/gltf/index.js +1 -1
  40. package/lib/engine/export/gltf/index.js.map +1 -1
  41. package/lib/engine/webcomponents/logo-element.d.ts +6 -3
  42. package/lib/engine/webcomponents/logo-element.js +18 -0
  43. package/lib/engine/webcomponents/logo-element.js.map +1 -1
  44. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +2 -2
  45. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js.map +1 -1
  46. package/lib/engine/webcomponents/needle menu/needle-menu.d.ts +10 -7
  47. package/lib/engine/webcomponents/needle menu/needle-menu.js +14 -4
  48. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  49. package/lib/engine/webcomponents/needle-engine.ar-overlay.js +10 -1
  50. package/lib/engine/webcomponents/needle-engine.ar-overlay.js.map +1 -1
  51. package/lib/engine/webcomponents/needle-engine.d.ts +3 -0
  52. package/lib/engine/webcomponents/needle-engine.js +10 -0
  53. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  54. package/lib/engine-components/Component.js +0 -1
  55. package/lib/engine-components/Component.js.map +1 -1
  56. package/lib/engine-components/ReflectionProbe.d.ts +24 -2
  57. package/lib/engine-components/ReflectionProbe.js +27 -1
  58. package/lib/engine-components/ReflectionProbe.js.map +1 -1
  59. package/lib/engine-components/Skybox.js +4 -2
  60. package/lib/engine-components/Skybox.js.map +1 -1
  61. package/lib/engine-components/export/gltf/GltfExport.js +1 -1
  62. package/lib/engine-components/export/gltf/GltfExport.js.map +1 -1
  63. package/lib/engine-components/export/usdz/ThreeUSDZExporter.js +2 -2
  64. package/lib/engine-components/export/usdz/USDZExporter.js +1 -1
  65. package/lib/engine-components/export/usdz/USDZExporter.js.map +1 -1
  66. package/lib/engine-components/export/usdz/extensions/behavior/PhysicsExtension.js +2 -2
  67. package/lib/engine-components/export/usdz/extensions/behavior/PhysicsExtension.js.map +1 -1
  68. package/package.json +17 -13
  69. package/plugins/common/buildinfo.js +46 -10
  70. package/plugins/common/files.js +2 -1
  71. package/plugins/common/license.js +144 -69
  72. package/plugins/common/logger.js +172 -11
  73. package/plugins/common/needle-engine-skill.md +175 -0
  74. package/plugins/common/worker.js +5 -4
  75. package/plugins/types/userconfig.d.ts +40 -2
  76. package/plugins/vite/ai.js +71 -0
  77. package/plugins/vite/alias.js +6 -5
  78. package/plugins/vite/asap.js +6 -5
  79. package/plugins/vite/build-pipeline.js +224 -41
  80. package/plugins/vite/buildinfo.js +66 -6
  81. package/plugins/vite/copyfiles.js +41 -12
  82. package/plugins/vite/custom-element-data.js +26 -16
  83. package/plugins/vite/defines.js +8 -5
  84. package/plugins/vite/dependencies.js +16 -10
  85. package/plugins/vite/dependency-watcher.js +35 -7
  86. package/plugins/vite/drop-client.js +7 -5
  87. package/plugins/vite/drop.js +16 -14
  88. package/plugins/vite/editor-connection.js +18 -16
  89. package/plugins/vite/imports-logger.js +12 -2
  90. package/plugins/vite/index.js +8 -3
  91. package/plugins/vite/local-files-analysis.js +789 -0
  92. package/plugins/vite/local-files-core.js +992 -0
  93. package/plugins/vite/local-files-internals.js +28 -0
  94. package/plugins/vite/local-files-types.d.ts +111 -0
  95. package/plugins/vite/local-files-utils.js +359 -0
  96. package/plugins/vite/local-files.js +2 -441
  97. package/plugins/vite/logger.client.js +45 -35
  98. package/plugins/vite/logger.js +6 -3
  99. package/plugins/vite/logging.js +129 -0
  100. package/plugins/vite/meta.js +18 -4
  101. package/plugins/vite/needle-app.js +4 -3
  102. package/plugins/vite/peer.js +2 -1
  103. package/plugins/vite/pwa.js +33 -17
  104. package/plugins/vite/reload.js +24 -2
  105. package/src/engine/api.ts +0 -3
  106. package/src/engine/debug/debug.ts +1 -1
  107. package/src/engine/debug/debug_spatial_console.ts +5 -1
  108. package/src/engine/engine_context.ts +1 -1
  109. package/src/engine/engine_create_objects.ts +1 -1
  110. package/src/engine/engine_gizmos.ts +9 -5
  111. package/src/engine/engine_license.ts +7 -2
  112. package/src/engine/engine_utils.ts +2 -2
  113. package/src/engine/export/gltf/index.ts +1 -1
  114. package/src/engine/webcomponents/logo-element.ts +20 -3
  115. package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +6 -2
  116. package/src/engine/webcomponents/needle menu/needle-menu.ts +23 -11
  117. package/src/engine/webcomponents/needle-engine.ar-overlay.ts +13 -2
  118. package/src/engine/webcomponents/needle-engine.ts +13 -1
  119. package/src/engine-components/Component.ts +1 -2
  120. package/src/engine-components/ReflectionProbe.ts +32 -8
  121. package/src/engine-components/Skybox.ts +4 -2
  122. package/src/engine-components/export/gltf/GltfExport.ts +1 -1
  123. package/src/engine-components/export/usdz/ThreeUSDZExporter.ts +2 -2
  124. package/src/engine-components/export/usdz/USDZExporter.ts +1 -1
  125. package/src/engine-components/export/usdz/extensions/behavior/PhysicsExtension.ts +2 -2
  126. package/dist/materialx-CJyQZtjt.min.js +0 -90
  127. package/dist/materialx-DMs1E08Z.js +0 -4636
  128. package/dist/materialx-DaKKOoVk.umd.cjs +0 -90
  129. package/lib/engine/engine_test_utils.d.ts +0 -39
  130. package/lib/engine/engine_test_utils.js +0 -84
  131. package/lib/engine/engine_test_utils.js.map +0 -1
  132. package/lib/include/three/EXT_mesh_gpu_instancing_exporter.js.map +0 -1
  133. package/src/engine/engine_test_utils.ts +0 -109
  134. package/src/include/draco/draco_decoder.js +0 -34
  135. package/src/include/draco/draco_decoder.wasm +0 -0
  136. package/src/include/draco/draco_wasm_wrapper.js +0 -117
  137. package/src/include/ktx2/basis_transcoder.js +0 -19
  138. package/src/include/ktx2/basis_transcoder.wasm +0 -0
  139. package/src/include/needle/arial-msdf.json +0 -1472
  140. package/src/include/needle/arial.png +0 -0
  141. package/src/include/needle/poweredbyneedle.webp +0 -0
  142. /package/lib/{include/three → engine/export/gltf}/EXT_mesh_gpu_instancing_exporter.d.ts +0 -0
  143. /package/lib/{include/three → engine/export/gltf}/EXT_mesh_gpu_instancing_exporter.js +0 -0
  144. /package/src/{include/three → engine/export/gltf}/EXT_mesh_gpu_instancing_exporter.js +0 -0
@@ -0,0 +1,28 @@
1
+ export {
2
+ getActiveHandlers,
3
+ getFeatureBasePath,
4
+ handleWebXRProfiles,
5
+ makeLocal,
6
+ makeLocalHtml,
7
+ } from './local-files-core.js';
8
+
9
+ export {
10
+ analyzeProjectGlbs,
11
+ buildAutoPolicy,
12
+ collectAssetGlbs,
13
+ collectIndexHtmlSkyboxUrls,
14
+ collectNeedleComponentExtensionBlobs,
15
+ detectAutoPolicy,
16
+ getWebXRProfilesForMode,
17
+ hasAutoFeatureSelection,
18
+ readGlbJsonChunk,
19
+ resolveSkyboxSelectionUrls,
20
+ resolveSkyboxValueToUrl,
21
+ shouldHandleUrlInAutoMode,
22
+ } from './local-files-analysis.js';
23
+
24
+ export {
25
+ fixDracoRangeQueryProbe,
26
+ fixRelativeNewURL,
27
+ getRelativeToBasePath,
28
+ } from './local-files-utils.js';
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Shared type declarations for the local-files plugin family.
3
+ * Referenced from local-files-utils.js, local-files-analysis.js, local-files-core.js, etc.
4
+ */
5
+
6
+ export interface UrlHandler {
7
+ name: string;
8
+ /** RegExp with the /g flag — match group 1 must capture the URL. */
9
+ pattern: RegExp;
10
+ type: "binary" | "css" | "decoder-dir" | "webxr-profiles";
11
+ feature: string;
12
+ decoderFiles?: string[];
13
+ localDirName?: string;
14
+ }
15
+
16
+ export interface LocalizationStats {
17
+ fileCount: number;
18
+ totalBytes: number;
19
+ mimeCounts: Map<string, number>;
20
+ }
21
+
22
+ export interface LocalizationOptions {
23
+ features?: string | string[];
24
+ excludeFeatures?: string[];
25
+ /** URL substrings or RegExps to skip. */
26
+ exclude?: (string | RegExp)[];
27
+ /** @deprecated use `exclude` */
28
+ excludeUrls?: string[];
29
+ /** e.g. "facebook-instant", "discord" */
30
+ platform?: string;
31
+ /** Only process files inside these npm packages. */
32
+ packages?: string[];
33
+ /** Skybox selection: URL, magic keyword, array of those, or "all". */
34
+ skybox?: string | string[];
35
+ /** WebXR controller profile mode: "minimal" | "quest" | "pico" | "all" */
36
+ webxr?: string;
37
+ templateExpansions?: Array<{
38
+ cdnPrefix: string;
39
+ variables: Record<string, string[]>;
40
+ localPrefix?: string;
41
+ }>;
42
+ /** Used when makeFilesLocal is an object — set to false to disable. */
43
+ enabled?: boolean;
44
+ }
45
+
46
+ export interface AutoPolicy {
47
+ features: Set<string>;
48
+ hasWebXR: boolean;
49
+ hasVideoPlayer: boolean;
50
+ allowedSkyboxUrls: Set<string> | null;
51
+ selectedWebXRProfiles: string[];
52
+ }
53
+
54
+ export interface VitePluginContext {
55
+ emitFile(asset: {
56
+ type: "asset" | "chunk";
57
+ fileName?: string;
58
+ name?: string;
59
+ source?: string | Uint8Array;
60
+ }): string;
61
+ getFileName(referenceId: string): string;
62
+ }
63
+
64
+ export interface LocalizationContext {
65
+ pluginContext: VitePluginContext | null;
66
+ cache: {
67
+ addToCache(key: string, value: Buffer | string): void;
68
+ getFromCache(key: string): Buffer | string | null;
69
+ readonly map: Map<string, Buffer | string>;
70
+ };
71
+ command: "build" | "serve";
72
+ viteConfig: { build?: { assetsDir?: string } } | null;
73
+ options: LocalizationOptions;
74
+ autoPolicy: AutoPolicy | null;
75
+ failedDownloads: Map<string, string>;
76
+ localizationStats: LocalizationStats;
77
+ }
78
+
79
+ export interface SceneAnalysisReport {
80
+ hasWebXRComponent: boolean;
81
+ hasVideoPlayerComponent: boolean;
82
+ extensions: Set<string>;
83
+ componentTypes: Set<string>;
84
+ componentCounts: Map<string, number>;
85
+ glbFileCount: number;
86
+ gltfFileCount: number;
87
+ totalSceneFileCount: number;
88
+ totalNodeCount: number;
89
+ totalVertexCount: number;
90
+ totalTextureCount: number;
91
+ totalMeshCount: number;
92
+ totalPrimitiveCount: number;
93
+ dracoPrimitiveCount: number;
94
+ meshoptBufferViewCount: number;
95
+ }
96
+
97
+ export interface SceneFile {
98
+ path: string;
99
+ type: "glb" | "gltf";
100
+ }
101
+
102
+ export interface NeedleComponentEntry {
103
+ name?: string;
104
+ type?: string;
105
+ guid?: string;
106
+ }
107
+
108
+ export interface NeedleComponentExtension {
109
+ components?: NeedleComponentEntry[];
110
+ builtin_components?: NeedleComponentEntry[];
111
+ }
@@ -0,0 +1,359 @@
1
+ // @ts-check
2
+ import https from 'https';
3
+ import http from 'http';
4
+ import { createHash } from 'crypto';
5
+ import { needleLog, setTransientLogLineCleaner } from './logging.js';
6
+
7
+ /** @typedef {import('./local-files-types.js').LocalizationContext} LocalizationContext */
8
+
9
+ const debug = false;
10
+ const DOWNLOAD_TIMEOUT_MS = 30_000;
11
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
12
+ let spinnerIndex = 0;
13
+ let spinnerActive = false;
14
+
15
+ setTransientLogLineCleaner(() => clearSpinnerLine());
16
+
17
+ function clearSpinnerLine() {
18
+ if (!process.stdout.isTTY || !spinnerActive) return;
19
+ process.stdout.write("\r\x1b[2K");
20
+ spinnerActive = false;
21
+ }
22
+
23
+ /** @param {string} url */
24
+ function updateMakeLocalProgress(url) {
25
+ if (!process.stdout.isTTY) {
26
+ needleLog("needle:local-files", "Make local: " + url, "log", { dimBody: true, showHeader: false });
27
+ return;
28
+ }
29
+ const frame = SPINNER_FRAMES[spinnerIndex++ % SPINNER_FRAMES.length];
30
+ const maxLength = Math.max(24, (process.stdout.columns || 120) - 4);
31
+ const message = `Make local: ${url}`;
32
+ const text = message.length > maxLength ? `${message.slice(0, Math.max(0, maxLength - 1))}…` : message;
33
+ process.stdout.write(`\r\x1b[2K${frame} ${text}\x1b[0K`);
34
+ spinnerActive = true;
35
+ }
36
+
37
+ export function finishMakeLocalProgress() {
38
+ clearSpinnerLine();
39
+ }
40
+
41
+ /**
42
+ * @param {string} src
43
+ * @param {string} search
44
+ * @param {string} replacement
45
+ * @returns {string}
46
+ */
47
+ export function replaceAll(src, search, replacement) {
48
+ return src.split(search).join(replacement);
49
+ }
50
+
51
+ export class Cache {
52
+ /** @type {Map<string, Buffer|string>} */
53
+ __cache = new Map();
54
+ /**
55
+ * @param {string} key
56
+ * @param {Buffer|string} value
57
+ */
58
+ addToCache(key, value) {
59
+ if (debug && this.__cache.has(key)) {
60
+ console.warn("Key " + key + " already exists in cache, overwriting.");
61
+ }
62
+ this.__cache.set(key, value);
63
+ }
64
+ /**
65
+ * @param {string} key
66
+ * @returns {Buffer|string|null}
67
+ */
68
+ getFromCache(key) {
69
+ return this.__cache.get(key) ?? null;
70
+ }
71
+ get map() {
72
+ return this.__cache;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * @param {string} path
78
+ * @param {string} basePath
79
+ * @returns {string}
80
+ */
81
+ export function getRelativeToBasePath(path, basePath) {
82
+ const normalizedPath = normalizeWebPath(path);
83
+ const normalizedBasePath = normalizeWebPath(basePath);
84
+
85
+ if (!normalizedBasePath) return normalizedPath;
86
+
87
+ if (normalizedPath.startsWith(normalizedBasePath)) {
88
+ return "./" + normalizedPath.substring(normalizedBasePath.length);
89
+ }
90
+
91
+ const baseSegments = normalizedBasePath.replace(/\/+$/g, "").split("/").filter(Boolean);
92
+ const backtrack = baseSegments.map(() => "..").join("/");
93
+ return (backtrack ? backtrack + "/" : "") + normalizedPath;
94
+ }
95
+
96
+ /**
97
+ * @param {string} path
98
+ * @returns {string}
99
+ */
100
+ export function normalizeWebPath(path) {
101
+ if (!path) return "";
102
+ return path.replace(/\\/g, "/");
103
+ }
104
+
105
+ /**
106
+ * @param {string} path
107
+ * @returns {string}
108
+ */
109
+ export function ensureTrailingSlash(path) {
110
+ if (!path) return "";
111
+ return path.endsWith("/") ? path : path + "/";
112
+ }
113
+
114
+ /**
115
+ * @param {string} src
116
+ * @returns {string}
117
+ */
118
+ export function fixRelativeNewURL(src) {
119
+ src = src.replace(
120
+ /(?<==\s*)(["'])((?:(?:\.{1,2}\/)|\/)?ext\/[^"']*\/)\1/g,
121
+ (/** @type {string} */ match, /** @type {string} */ quote, /** @type {string} */ path) => {
122
+ const runtimePath = path.startsWith("/ext/") ? path.substring(1) : path;
123
+ return `new URL(${quote}${runtimePath}${quote}, self.location?.href || ${quote}${quote}).href`;
124
+ }
125
+ );
126
+
127
+ src = src.replace(
128
+ /new\s+URL\s*\(\s*(["'`])((?:(?:\.{1,2}\/)|\/)?ext\/[^"'`]+)\1\s*\)/g,
129
+ (/** @type {string} */ match, /** @type {string} */ quote, /** @type {string} */ path) => {
130
+ const runtimePath = path.startsWith("/ext/") ? path.substring(1) : path;
131
+ return `new URL(${quote}${runtimePath}${quote}, self.location?.href)`;
132
+ }
133
+ );
134
+
135
+ return src;
136
+ }
137
+
138
+ /**
139
+ * @param {string} src
140
+ * @returns {string}
141
+ */
142
+ export function fixDracoRangeQueryProbe(src) {
143
+ return src;
144
+ }
145
+
146
+ /**
147
+ * @param {string} path
148
+ * @param {string|Buffer} content
149
+ * @returns {string}
150
+ */
151
+ export function getValidFilename(path, content) {
152
+ if (path.startsWith("http:") || path.startsWith("https:")) {
153
+ const url = new URL(path);
154
+ const pathParts = url.pathname.split('/');
155
+ const filename = pathParts.pop() || 'file';
156
+ const nameParts = filename.split('.');
157
+ const nameWithoutExt = nameParts.slice(0, -1).join('.');
158
+ path = nameWithoutExt + "-" + createContentMd5(url.host + pathParts.join('/')) + "." + (nameParts.pop() || 'unknown');
159
+ }
160
+
161
+ let name = path.replace(/[^a-z0-9_\-\.\+]/gi, '-');
162
+
163
+ const maxLength = 200;
164
+ if (path.length > maxLength) {
165
+ const hash = createContentMd5(content);
166
+ let ext = "";
167
+ const extIndex = name.lastIndexOf('.');
168
+ if (extIndex !== -1) {
169
+ ext = name.substring(extIndex + 1);
170
+ name = name.substring(0, extIndex);
171
+ }
172
+ name = name.substring(0, maxLength) + "-" + hash + (ext ? "." + ext : '');
173
+ }
174
+ return name;
175
+ }
176
+
177
+ /**
178
+ * @param {string} str
179
+ * @returns {string}
180
+ */
181
+ function createContentMd5(str) {
182
+ return createHash('md5')
183
+ .update(str)
184
+ .digest('hex');
185
+ }
186
+
187
+ /**
188
+ * @param {string} url
189
+ * @returns {Promise<string>}
190
+ */
191
+ export function downloadText(url) {
192
+ return new Promise((res, rej) => {
193
+ const timer = setTimeout(() => {
194
+ rej(new Error("Download timed out after " + DOWNLOAD_TIMEOUT_MS + "ms: " + url));
195
+ }, DOWNLOAD_TIMEOUT_MS);
196
+
197
+ const req = (url.startsWith("http://") ? http.get(url, (response) => {
198
+ if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
199
+ clearTimeout(timer);
200
+ return downloadText(response.headers.location).then(res).catch(rej);
201
+ }
202
+
203
+ if (response.statusCode !== 200) {
204
+ clearTimeout(timer);
205
+ clearSpinnerLine();
206
+ rej(new Error("Failed to download (" + response.statusCode + "): " + url));
207
+ return;
208
+ }
209
+
210
+ updateMakeLocalProgress(url);
211
+
212
+ let data = '';
213
+ response.on('data', (/** @type {Buffer|string} */ chunk) => {
214
+ data += chunk;
215
+ });
216
+ response.on('end', () => {
217
+ clearTimeout(timer);
218
+ res(data);
219
+ });
220
+ response.on('error', (err) => {
221
+ clearTimeout(timer);
222
+ rej(err);
223
+ });
224
+ }) : https.get(url, (response) => {
225
+ if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
226
+ clearTimeout(timer);
227
+ return downloadText(response.headers.location).then(res).catch(rej);
228
+ }
229
+
230
+ if (response.statusCode !== 200) {
231
+ clearTimeout(timer);
232
+ clearSpinnerLine();
233
+ rej(new Error("Failed to download (" + response.statusCode + "): " + url));
234
+ return;
235
+ }
236
+
237
+ updateMakeLocalProgress(url);
238
+
239
+ let data = '';
240
+ response.on('data', (/** @type {Buffer|string} */ chunk) => {
241
+ data += chunk;
242
+ });
243
+ response.on('end', () => {
244
+ clearTimeout(timer);
245
+ res(data);
246
+ });
247
+ response.on('error', (err) => {
248
+ clearTimeout(timer);
249
+ rej(err);
250
+ });
251
+ }));
252
+ req.on('error', (err) => {
253
+ clearTimeout(timer);
254
+ clearSpinnerLine();
255
+ rej(err);
256
+ });
257
+ });
258
+ }
259
+
260
+ /**
261
+ * @param {string} url
262
+ * @returns {Promise<Buffer>}
263
+ */
264
+ export function downloadBinary(url) {
265
+ return new Promise((res, rej) => {
266
+ const timer = setTimeout(() => {
267
+ rej(new Error("Download timed out after " + DOWNLOAD_TIMEOUT_MS + "ms: " + url));
268
+ }, DOWNLOAD_TIMEOUT_MS);
269
+
270
+ const req = (url.startsWith("http://") ? http.get(url, (response) => {
271
+ if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
272
+ clearTimeout(timer);
273
+ return downloadBinary(response.headers.location).then(res).catch(rej);
274
+ }
275
+
276
+ if (response.statusCode !== 200) {
277
+ clearTimeout(timer);
278
+ clearSpinnerLine();
279
+ rej(new Error("Failed to download (" + response.statusCode + "): " + url));
280
+ return;
281
+ }
282
+
283
+ updateMakeLocalProgress(url);
284
+
285
+ const chunks = /** @type {Buffer[]} */ ([]);
286
+ response.on('data', (/** @type {Buffer|string} */ chunk) => {
287
+ chunks.push(chunk);
288
+ });
289
+ response.on('end', () => {
290
+ clearTimeout(timer);
291
+ res(Buffer.concat(chunks));
292
+ });
293
+ response.on('error', (err) => {
294
+ clearTimeout(timer);
295
+ rej(err);
296
+ });
297
+ }) : https.get(url, (response) => {
298
+ if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
299
+ clearTimeout(timer);
300
+ return downloadBinary(response.headers.location).then(res).catch(rej);
301
+ }
302
+
303
+ if (response.statusCode !== 200) {
304
+ clearTimeout(timer);
305
+ clearSpinnerLine();
306
+ rej(new Error("Failed to download (" + response.statusCode + "): " + url));
307
+ return;
308
+ }
309
+
310
+ updateMakeLocalProgress(url);
311
+
312
+ const chunks = /** @type {Buffer[]} */ ([]);
313
+ response.on('data', (/** @type {Buffer|string} */ chunk) => {
314
+ chunks.push(chunk);
315
+ });
316
+ response.on('end', () => {
317
+ clearTimeout(timer);
318
+ res(Buffer.concat(chunks));
319
+ });
320
+ response.on('error', (err) => {
321
+ clearTimeout(timer);
322
+ rej(err);
323
+ });
324
+ }));
325
+ req.on('error', (err) => {
326
+ clearTimeout(timer);
327
+ clearSpinnerLine();
328
+ rej(err);
329
+ });
330
+ });
331
+ }
332
+
333
+ /**
334
+ * @param {LocalizationContext} context
335
+ * @param {string} url
336
+ * @param {unknown} err
337
+ */
338
+ export function recordFailedDownload(context, url, err) {
339
+ if (!url || !context?.failedDownloads) return;
340
+ const message = err?.message ? String(err.message) : "";
341
+ if (!context.failedDownloads.has(url)) {
342
+ context.failedDownloads.set(url, message);
343
+ }
344
+ }
345
+
346
+ /**
347
+ * @param {string} url
348
+ * @returns {string}
349
+ */
350
+ export function getShortUrlName(url) {
351
+ try {
352
+ const parsed = new URL(url);
353
+ const parts = parsed.pathname.split("/").filter(Boolean);
354
+ return parts[parts.length - 1] || parsed.host;
355
+ }
356
+ catch {
357
+ return url;
358
+ }
359
+ }