@needle-tools/materialx 1.7.0-next.9ec2906 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@needle-tools/materialx",
3
3
  "description": "MaterialX material support for three.js and Needle Engine – render physically based MaterialX shaders in the browser via WebAssembly",
4
- "version": "1.7.0-next.9ec2906",
4
+ "version": "1.7.1",
5
5
  "type": "module",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
7
7
  "main": "index.js",
@@ -27,7 +27,10 @@
27
27
  "three": ">=0.160.0"
28
28
  },
29
29
  "scripts": {
30
- "test": "node --import=./tests/unit/register-json-loader.js --test tests/unit/**/*.test.js"
30
+ "test": "node --import=./tests/unit/register-json-loader.js --test tests/unit/**/*.test.js",
31
+ "test:runtime:node": "node tests/runtime/materialx-runtime-smoke.mjs",
32
+ "test:runtime:deno": "deno run --allow-read --no-lock --node-modules-dir=auto tests/runtime/materialx-runtime-smoke.mjs",
33
+ "test:runtime:bun": "bun tests/runtime/materialx-runtime-smoke.mjs"
31
34
  },
32
35
  "devDependencies": {
33
36
  "@needle-tools/engine": "4.x",
@@ -75,4 +78,4 @@
75
78
  "type": "git",
76
79
  "url": "https://github.com/needle-tools/needle-engine-materialx.git"
77
80
  }
78
- }
81
+ }
@@ -44,6 +44,8 @@ export interface MaterialXLoaderOptions {
44
44
  environmentRadianceMode?: MaterialXEnvironmentRadianceMode;
45
45
  /** Match Three.js glossy specular antialiasing. Defaults to true. */
46
46
  specularAntialiasing?: boolean;
47
+ /** Generate missing MikkTSpace tangents for tangent-dependent shaders. Defaults to true. */
48
+ generateTangents?: boolean;
47
49
  /** Flip texcoord V at the MaterialX texcoord node output. Defaults to false for standalone .mtlx creation and true for GLTFLoader integration. */
48
50
  hwTexcoordVerticalFlip?: boolean;
49
51
  /** Flip texcoord V inside MaterialX file texture sampling. Defaults to false for standalone .mtlx creation and true for GLTFLoader integration. */
@@ -358,6 +358,7 @@ export async function createMaterialXMaterial(mtlx, materialNodeNameOrIndex, loa
358
358
  context: context || {},
359
359
  environmentRadianceMode: options?.environmentRadianceMode,
360
360
  specularAntialiasing: options?.specularAntialiasing,
361
+ generateTangents: options?.generateTangents,
361
362
  parameters: {
362
363
  // MASK uses discard; BLEND uses Three.js transparent sorting and blending.
363
364
  transparent: renderAsTransparent,
@@ -71,10 +71,22 @@ function fromMatrix(matrix, dimension) {
71
71
  * @property {(path: string) => Promise<THREE.Texture | null | void>} getTexture - Get a texture by path
72
72
  */
73
73
 
74
+ /**
75
+ * @param {string} src
76
+ * @returns {HTMLImageElement | { src: string }}
77
+ */
78
+ function createPortableImage(src) {
79
+ if (typeof Image !== 'undefined') {
80
+ const image = new Image();
81
+ image.src = src;
82
+ return image;
83
+ }
84
+ return { src };
85
+ }
86
+
74
87
  const defaultTexture = new THREE.Texture();
75
88
  defaultTexture.needsUpdate = true;
76
- defaultTexture.image = new Image();
77
- defaultTexture.image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAANQTFRFr6+vGqg52AAAAAxJREFUeJxjZGBEgQAAWAAJLpjsTQAAAABJRU5ErkJggg=="
89
+ defaultTexture.image = createPortableImage("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAANQTFRFr6+vGqg52AAAAAxJREFUeJxjZGBEgQAAWAAJLpjsTQAAAABJRU5ErkJggg==");
78
90
  // defaultTexture.image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAB5QTFRFAAAABAQEw8PD////v7+/vb29Xl5eQEBA+/v7PDw8GPBYkgAAAB1JREFUeJxjZGBgYFQSABIUMlxgDGMGBtaIAnIZAKwQCSDYUEZEAAAAAElFTkSuQmCC";
79
91
  // defaultTexture.wrapS = THREE.RepeatWrapping;
80
92
  // defaultTexture.wrapT = THREE.RepeatWrapping;
@@ -84,8 +96,7 @@ defaultTexture.image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAA
84
96
 
85
97
  const defaultNormalTexture = new THREE.Texture();
86
98
  defaultNormalTexture.needsUpdate = true;
87
- defaultNormalTexture.image = new Image();
88
- defaultNormalTexture.image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIBAMAAAA2IaO4AAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAABJQTFRFgYH4gIH4gYH3gIH3gIH5gID4m94ORAAAADFJREFUeJxjZBBkfMdo9P/BB0aBj/8FGB0ufghgFGT4r8wo+P8rD2Pgo3sMjIz8jAwAMLoN0ZjS5hgAAAAASUVORK5CYII=";
99
+ defaultNormalTexture.image = createPortableImage("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIBAMAAAA2IaO4AAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAABJQTFRFgYH4gIH4gYH3gIH3gIH5gID4m94ORAAAADFJREFUeJxjZBBkfMdo9P/BB0aBj/8FGB0ufghgFGT4r8wo+P8rD2Pgo3sMjIz8jAwAMLoN0ZjS5hgAAAAASUVORK5CYII=");
89
100
 
90
101
  /**
91
102
  * @param {string} key
@@ -111,6 +122,75 @@ function addToCache(key, value) {
111
122
  if (debug) console.log('[MaterialX] Added to cache:', key, value);
112
123
  }
113
124
 
125
+ /**
126
+ * @param {string} value
127
+ * @returns {boolean}
128
+ */
129
+ function isAbsoluteTexturePath(value) {
130
+ return /^[a-z][a-z0-9+.-]*:/i.test(value) || value.startsWith("//") || value.startsWith("/");
131
+ }
132
+
133
+ /**
134
+ * @param {string} value
135
+ * @returns {boolean}
136
+ */
137
+ function isAbsoluteUrl(value) {
138
+ return /^[a-z][a-z0-9+.-]*:\/\//i.test(value);
139
+ }
140
+
141
+ /**
142
+ * @param {string} value
143
+ * @returns {string}
144
+ */
145
+ function withTrailingSlash(value) {
146
+ return value.endsWith(IMAGE_PATH_SEPARATOR) ? value : `${value}${IMAGE_PATH_SEPARATOR}`;
147
+ }
148
+
149
+ /**
150
+ * @param {string} value
151
+ * @returns {string}
152
+ */
153
+ function normalizeRelativeTexturePath(value) {
154
+ const suffixStart = value.search(/[?#]/);
155
+ const path = suffixStart >= 0 ? value.slice(0, suffixStart) : value;
156
+ const suffix = suffixStart >= 0 ? value.slice(suffixStart) : "";
157
+ const hasLeadingSlash = path.startsWith(IMAGE_PATH_SEPARATOR);
158
+ const parts = [];
159
+
160
+ for (const part of path.split(IMAGE_PATH_SEPARATOR)) {
161
+ if (!part || part === ".") continue;
162
+ if (part === "..") {
163
+ const previous = parts[parts.length - 1];
164
+ if (previous && previous !== "..") parts.pop();
165
+ else if (!hasLeadingSlash) parts.push(part);
166
+ continue;
167
+ }
168
+ parts.push(part);
169
+ }
170
+
171
+ const normalizedPath = `${hasLeadingSlash ? IMAGE_PATH_SEPARATOR : ""}${parts.join(IMAGE_PATH_SEPARATOR)}`;
172
+ return `${normalizedPath}${suffix}`;
173
+ }
174
+
175
+ /**
176
+ * @param {string} searchPath
177
+ * @param {string} value
178
+ * @returns {string}
179
+ */
180
+ function resolveTexturePath(searchPath, value) {
181
+ if (isAbsoluteTexturePath(value)) return value;
182
+ if (!searchPath) return value;
183
+
184
+ const normalizedSearchPath = searchPath.replace(/\/+$/, "");
185
+ if (!normalizedSearchPath) return value;
186
+
187
+ if (isAbsoluteUrl(normalizedSearchPath)) {
188
+ return new URL(value, withTrailingSlash(normalizedSearchPath)).href;
189
+ }
190
+
191
+ return normalizeRelativeTexturePath(`${normalizedSearchPath}${IMAGE_PATH_SEPARATOR}${value}`);
192
+ }
193
+
114
194
  /**
115
195
  * Get Three uniform from MaterialX value
116
196
  * @param {any} uniforms
@@ -154,19 +234,19 @@ function toThreeUniform(threeUniforms, uniforms, type, value, name, loaders, sea
154
234
  // Cache / reuse texture to avoid reload overhead.
155
235
  // Note: that data blobs and embedded data textures are not cached as they are transient data.
156
236
  let checkCache = true;
157
- let texturePath = searchPath + IMAGE_PATH_SEPARATOR + value;
237
+ let texturePath = value;
158
238
  if (value.startsWith('blob:')) {
159
- texturePath = value;
160
239
  checkCache = false;
161
240
  }
162
241
  else if (value.startsWith('data:')) {
163
- texturePath = value;
164
242
  checkCache = false;
165
243
  }
166
244
  else if (value.startsWith('http')) {
167
- texturePath = value;
168
245
  checkCache = true;
169
246
  }
247
+ else {
248
+ texturePath = resolveTexturePath(searchPath, value);
249
+ }
170
250
 
171
251
  const cacheKey = loaders.cacheKey?.length ? `${loaders.cacheKey}-${texturePath}` : texturePath;
172
252
  const cacheValue = checkCache ? tryGetFromCache(cacheKey) : null;
@@ -236,7 +316,7 @@ function toThreeUniform(threeUniforms, uniforms, type, value, name, loaders, sea
236
316
  case 'displacementshader':
237
317
  case 'volumeshader':
238
318
  case 'lightshader':
239
- // MaterialX closure/shader types — not real uniforms, skip silently
319
+ // MaterialX closure/shader types — not emitted uniforms, skip silently
240
320
  break;
241
321
  default:
242
322
  const key = type + ':' + name;
package/src/materialx.js CHANGED
@@ -17,6 +17,21 @@ function isTextureLike(value) {
17
17
  return !!value && typeof value === "object" && /** @type {{ isTexture?: unknown }} */(value).isTexture === true;
18
18
  }
19
19
 
20
+ function isNodeRuntime() {
21
+ return !!globalThis.process?.versions?.node && globalThis.process?.type !== "renderer";
22
+ }
23
+
24
+ async function getNodePackageAssetPaths() {
25
+ const nodeUrlModule = "node:url";
26
+ const { fileURLToPath } = await import(/* @vite-ignore */ nodeUrlModule);
27
+ const binDirectory = ["..", "bin"].join("/") + "/";
28
+ return [
29
+ "JsMaterialXCore.wasm",
30
+ "JsMaterialXGenShader.wasm",
31
+ "JsMaterialXGenShader.data.txt",
32
+ ].map(fileName => fileURLToPath(new URL(binDirectory + fileName, import.meta.url)));
33
+ }
34
+
20
35
 
21
36
  /**
22
37
  * Preloads the MaterialX WebAssembly module.
@@ -69,7 +84,10 @@ export async function ready() {
69
84
 
70
85
  const location = globalThis.NEEDLE_MATERIALX_LOCATION;
71
86
 
72
- if (location === "package" || location === "bin/" || location === "./bin/" || location === "../bin/") {
87
+ if (isNodeRuntime()) {
88
+ urls = await getNodePackageAssetPaths();
89
+ }
90
+ else if (location === "package" || location === "bin/" || location === "./bin/" || location === "../bin/") {
73
91
  // Use local files from the @needle-tools/materialx npm package.
74
92
  // Vite's ?url suffix copies these files to the output directory
75
93
  // and returns their URL automatically — no CDN download needed.
@@ -12,6 +12,7 @@ declare type MaterialXMaterialInitParameters = {
12
12
  parameters?: MaterialParameters;
13
13
  environmentRadianceMode?: MaterialXEnvironmentRadianceMode;
14
14
  specularAntialiasing?: boolean;
15
+ generateTangents?: boolean;
15
16
  debug?: boolean;
16
17
  }
17
18
 
@@ -38,6 +39,7 @@ export declare class MaterialXMaterial extends ShaderMaterial {
38
39
  envMapRotation: Euler;
39
40
  environmentRadianceMode: MaterialXEnvironmentRadianceMode;
40
41
  specularAntialiasing: boolean;
42
+ generateTangents: boolean;
41
43
  updateUniforms(environment: MaterialXEnvironment, _renderer: WebGLRenderer, object: Object3D, camera: Camera, time?: number, frame?: number): void;
42
44
 
43
45
  private updateEnvironmentUniforms(environment: MaterialXEnvironment, scene: Scene): void;
@@ -5,6 +5,11 @@ import { generateMaterialPropertiesForUniforms, getUniformValues, getLightTypeId
5
5
 
6
6
  export const DEFAULT_ENVIRONMENT_RADIANCE_MODE = "three-pmrem";
7
7
 
8
+ /** @type {Promise<{ computeMikkTSpaceTangents: Function, MikkTSpace: any }> | null} */
9
+ let mikkTSpaceTangentsPromise = null;
10
+ /** @type {WeakMap<BufferGeometry, Promise<boolean>>} */
11
+ const pendingTangentGenerations = new WeakMap();
12
+
8
13
  // Add helper matrices for uniform updates (similar to MaterialX example)
9
14
  const worldInverseMat = new Matrix4();
10
15
  const worldTransposeMat = new Matrix4();
@@ -91,6 +96,60 @@ function createThreeLightUniforms() {
91
96
  };
92
97
  }
93
98
 
99
+ function getMikkTSpaceTangents() {
100
+ mikkTSpaceTangentsPromise ??= Promise.all([
101
+ import('three/examples/jsm/utils/BufferGeometryUtils.js'),
102
+ import('three/examples/jsm/libs/mikktspace.module.js'),
103
+ ]).then(async ([utils, MikkTSpace]) => {
104
+ await MikkTSpace.ready;
105
+ return {
106
+ computeMikkTSpaceTangents: utils.computeMikkTSpaceTangents,
107
+ MikkTSpace,
108
+ };
109
+ });
110
+ return mikkTSpaceTangentsPromise;
111
+ }
112
+
113
+ /**
114
+ * @param {BufferGeometry} geometry
115
+ * @returns {Promise<boolean>}
116
+ */
117
+ function ensureGeometryTangents(geometry) {
118
+ if (geometry.attributes.tangent) return Promise.resolve(true);
119
+
120
+ let pending = pendingTangentGenerations.get(geometry);
121
+ if (pending) return pending;
122
+
123
+ pending = (async () => {
124
+ if (!geometry.attributes.position || !geometry.attributes.uv) {
125
+ console.warn('[MaterialX] Cannot generate tangents: geometry requires position and uv attributes.');
126
+ return false;
127
+ }
128
+
129
+ if (!geometry.attributes.normal) {
130
+ geometry.computeVertexNormals();
131
+ }
132
+
133
+ const { computeMikkTSpaceTangents, MikkTSpace } = await getMikkTSpaceTangents();
134
+ computeMikkTSpaceTangents(geometry, MikkTSpace);
135
+
136
+ if (geometry.attributes.tangent) {
137
+ geometry.attributes.tangent.needsUpdate = true;
138
+ return true;
139
+ }
140
+
141
+ return false;
142
+ })().catch(error => {
143
+ console.warn('[MaterialX] Failed to generate MikkTSpace tangents.', error);
144
+ return false;
145
+ }).finally(() => {
146
+ pendingTangentGenerations.delete(geometry);
147
+ });
148
+
149
+ pendingTangentGenerations.set(geometry, pending);
150
+ return pending;
151
+ }
152
+
94
153
  const CUBE_UV_REFLECTION_FUNCTIONS = `
95
154
  float mx_cubeuv_getFace(vec3 direction) {
96
155
  vec3 absDirection = abs(direction);
@@ -305,6 +364,7 @@ function patchImageAddressModes(fragmentShader) {
305
364
  * @property {import('three').MaterialParameters} [parameters] - Optional parameters
306
365
  * @property {"three-pmrem" | "materialx-prefiltered" | "materialx-fis"} [environmentRadianceMode]
307
366
  * @property {boolean} [specularAntialiasing] - Match Three.js glossy specular antialiasing. Defaults to true.
367
+ * @property {boolean} [generateTangents] - Generate missing MikkTSpace tangents for tangent-dependent shaders. Defaults to true.
308
368
  * @property {boolean} [debug] - Debug flag
309
369
  */
310
370
 
@@ -336,6 +396,7 @@ export class MaterialXMaterial extends ShaderMaterial {
336
396
  this.envMapRotation.copy(source.envMapRotation);
337
397
  this.environmentRadianceMode = source.environmentRadianceMode;
338
398
  this.specularAntialiasing = source.specularAntialiasing;
399
+ this.generateTangents = source.generateTangents;
339
400
  this.ready = source.ready;
340
401
  generateMaterialPropertiesForUniforms(this, this._shader.getStage('pixel'));
341
402
  generateMaterialPropertiesForUniforms(this, this._shader.getStage('vertex'));
@@ -349,6 +410,8 @@ export class MaterialXMaterial extends ShaderMaterial {
349
410
  _shader = null;
350
411
  /** @type {boolean} */
351
412
  _needsTangents = false;
413
+ /** @type {WeakSet<BufferGeometry>} */
414
+ _pendingTangentGeometries = new WeakSet();
352
415
  /** @type {Promise<void>} */
353
416
  ready = Promise.resolve();
354
417
 
@@ -368,6 +431,7 @@ export class MaterialXMaterial extends ShaderMaterial {
368
431
  /** @type {"three-pmrem" | "materialx-prefiltered" | "materialx-fis"} */
369
432
  let environmentRadianceMode = DEFAULT_ENVIRONMENT_RADIANCE_MODE;
370
433
  let specularAntialiasing = true;
434
+ let generateTangents = true;
371
435
 
372
436
  if (init) {
373
437
 
@@ -423,6 +487,7 @@ export class MaterialXMaterial extends ShaderMaterial {
423
487
  // the same way MeshStandardMaterial does.
424
488
  fragmentShader = fragmentShader.replace(/\bu_envLightIntensity\b/g, 'envMapIntensity');
425
489
  specularAntialiasing = init.specularAntialiasing ?? true;
490
+ generateTangents = init.generateTangents ?? true;
426
491
  if (specularAntialiasing) {
427
492
  fragmentShader = patchEnvironmentSpecularAntialiasing(fragmentShader);
428
493
  }
@@ -698,6 +763,7 @@ $2`
698
763
  this._needsTangents = vertexShader.includes('in vec4 tangent;') || vertexShader.includes('in vec3 tangent;');
699
764
  this.environmentRadianceMode = environmentRadianceMode;
700
765
  this.specularAntialiasing = specularAntialiasing;
766
+ this.generateTangents = generateTangents;
701
767
 
702
768
  Object.assign(this.uniforms,
703
769
  // Three.js light uniforms (required when lights: true). These use
@@ -760,10 +826,23 @@ $2`
760
826
  */
761
827
  onBeforeRender(renderer, _scene, camera, geometry, object, _group) {
762
828
  if (this._needsTangents && !geometry.attributes.tangent) {
763
- if (!this._missingTangentsWarned) {
829
+ if (this.generateTangents) {
830
+ if (!this._pendingTangentGeometries.has(geometry)) {
831
+ this._pendingTangentGeometries.add(geometry);
832
+ ensureGeometryTangents(geometry).then(generated => {
833
+ if (generated) {
834
+ this.needsUpdate = true;
835
+ } else if (!this._missingTangentsWarned) {
836
+ this._missingTangentsWarned = true;
837
+ console.warn(`[MaterialX] Tangents are required for this material (${this.name}) but could not be generated for the geometry.`);
838
+ }
839
+ }).finally(() => {
840
+ this._pendingTangentGeometries.delete(geometry);
841
+ });
842
+ }
843
+ } else if (!this._missingTangentsWarned) {
764
844
  this._missingTangentsWarned = true;
765
- console.warn(`[MaterialX] Tangents are required for this material (${this.name}) but not present in the geometry.`);
766
- // TODO: can we compute tangents here?
845
+ console.warn(`[MaterialX] Tangents are required for this material (${this.name}) but not present in the geometry. Automatic tangent generation is disabled.`);
767
846
  }
768
847
  }
769
848
  const time = this._context?.getTime?.() || getTime();
@@ -786,6 +865,8 @@ $2`
786
865
  environmentRadianceMode = DEFAULT_ENVIRONMENT_RADIANCE_MODE;
787
866
  /** @type {boolean} */
788
867
  specularAntialiasing = true;
868
+ /** @type {boolean} */
869
+ generateTangents = true;
789
870
 
790
871
  /**
791
872
  * @param {import("three").WebGLRenderer} _renderer
package/src/utils.js CHANGED
@@ -7,7 +7,9 @@
7
7
  * @returns {boolean|string} Parameter value or false if not found
8
8
  */
9
9
  export function getParam(name) {
10
- const urlParams = new URLSearchParams(window.location.search);
10
+ const search = globalThis.location?.search ?? globalThis.window?.location?.search;
11
+ if (!search) return false;
12
+ const urlParams = new URLSearchParams(search);
11
13
  const param = urlParams.get(name);
12
14
  if (param == null || param === "0" || param === "false") return false;
13
15
  if (param === "") return true;
@@ -35,22 +37,26 @@ export function getFrame() {
35
37
  return frame;
36
38
  }
37
39
 
38
- const performance = window.performance || /** @type {any} */ (window).webkitPerformance || /** @type {any} */ (window).mozPerformance;
40
+ const performanceApi = globalThis.performance
41
+ || globalThis.window?.performance
42
+ || /** @type {any} */ (globalThis.window)?.webkitPerformance
43
+ || /** @type {any} */ (globalThis.window)?.mozPerformance
44
+ || { now: () => Date.now() };
39
45
 
40
46
  function updateTime() {
41
- time = performance.now() / 1000; // Convert to seconds
47
+ time = performanceApi.now() / 1000; // Convert to seconds
42
48
  frame++;
43
- window.requestAnimationFrame(updateTime);
49
+ globalThis.requestAnimationFrame?.(updateTime);
44
50
  }
45
51
 
46
- window.requestAnimationFrame(updateTime);
52
+ globalThis.requestAnimationFrame?.(updateTime);
47
53
 
48
54
 
49
55
 
50
56
 
51
57
  export async function waitForNetworkIdle() {
52
- if (typeof requestIdleCallback !== "undefined") {
53
- return new Promise(res => requestIdleCallback(res));
58
+ if (typeof globalThis.requestIdleCallback !== "undefined") {
59
+ return new Promise(res => globalThis.requestIdleCallback(res));
54
60
  }
55
61
  else {
56
62
  console.debug("[MaterialX] Can not wait for network idle, using fallback");
@@ -61,5 +67,6 @@ export async function waitForNetworkIdle() {
61
67
 
62
68
  export function isDevEnvironment() {
63
69
  // check if we're in localhost or using an ip address
64
- return window.location.hostname === "localhost" || /^\d{1,3}(\.\d{1,3}){3}$/.test(window.location.hostname);
65
- }
70
+ const hostname = globalThis.location?.hostname ?? globalThis.window?.location?.hostname;
71
+ return hostname === "localhost" || !!hostname && /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname);
72
+ }